/******************************************************************************
 * $Id$
 *
 * Project:  MapServer
 * Purpose:  OpenGIS Web Coverage Server (WCS) 2.0 implementation.
 * Author:   Stephan Meissl <stephan.meissl@eox.at>
 *           Fabian Schindler <fabian.schindler@eox.at>
 *           and the MapServer team.
 *
 ******************************************************************************
 * Copyright (c) 2010, 2011 EOX IT Services GmbH, Austria
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies of this Software or works derived from this Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ****************************************************************************/

#include "mapserver-config.h"
#if defined(USE_WCS_SVR)

#include <assert.h>
#include "mapserver.h"
#include "maperror.h"
#include "mapthread.h"
#include "mapows.h"
#include "mapwcs.h"
#include "mapgdal.h"
#include <float.h>
#include "gdal.h"
#include "cpl_port.h"
#include "maptime.h"
#include "mapprimitive.h"
#include "cpl_string.h"
#include <string.h>

#if defined(USE_LIBXML2)

#include <libxml/tree.h>
#include "maplibxml2.h"
#include <libxml/parser.h>

#endif /* defined(USE_LIBXML2) */

#include <string>

/************************************************************************/
/*                          msXMLStripIndentation                       */
/************************************************************************/

static void msXMLStripIndentation(char* ptr)
{
    /* Remove spaces between > and < to get properly indented result */
    char* afterLastClosingBracket = NULL;
    if( *ptr == ' ' )
        afterLastClosingBracket = ptr;
    while( *ptr != '\0' )
    {
        if( *ptr == '<' && afterLastClosingBracket != NULL )
        {
            memmove(afterLastClosingBracket, ptr, strlen(ptr) + 1);
            ptr = afterLastClosingBracket;
        }
        else if( *ptr == '>' )
        {
            afterLastClosingBracket = ptr + 1;
        }
        else if( *ptr != ' ' &&  *ptr != '\n' )
            afterLastClosingBracket = NULL;
        ptr ++;
    }
}



/************************************************************************/
/*                   msStringParseInteger()                             */
/*                                                                      */
/*      Tries to parse a string as a integer value. If not possible     */
/*      the value MS_WCS20_UNBOUNDED is stored as value.                */
/*      If no characters could be parsed, MS_FAILURE is returned. If at */
/*      least some characters could be parsed, MS_DONE is returned and  */
/*      only if all characters could be parsed, MS_SUCCESS is returned. */
/************************************************************************/

static int msStringParseInteger(const char *string, int *dest)
{
  char *parse_check;
  *dest = (int)strtol(string, &parse_check, 0);
  if(parse_check == string) {
    return MS_FAILURE;
  } else if(parse_check - strlen(string) != string) {
    return MS_DONE;
  }
  return MS_SUCCESS;
}

/************************************************************************/
/*                   msStringParseDouble()                              */
/*                                                                      */
/*      Tries to parse a string as a double value. If not possible      */
/*      the value 0 is stored as value.                                 */
/*      If no characters could be parsed, MS_FAILURE is returned. If at */
/*      least some characters could be parsed, MS_DONE is returned and  */
/*      only if all characters could be parsed, MS_SUCCESS is returned. */
/************************************************************************/

static int msStringParseDouble(const char *string, double *dest)
{
  char *parse_check = NULL;
  *dest = strtod(string, &parse_check);
  if(parse_check == string) {
    return MS_FAILURE;
  } else if(parse_check - strlen(string) != string) {
    return MS_DONE;
  }
  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSParseTimeOrScalar20()                         */
/*                                                                      */
/*      Parses a string, either as a time or a scalar value and         */
/*      writes the output into the timeScalarUnion.                     */
/************************************************************************/

static int msWCSParseTimeOrScalar20(timeScalarUnion *u, const char *string)
{
  struct tm time;
  if (string) {
    while(*string == ' ')
      string ++;
  }

  if (!string || strlen(string) == 0 || !u) {
    msSetError(MS_WCSERR, "Invalid string", "msWCSParseTimeOrScalar20()");
    return MS_WCS20_ERROR_VALUE;
  }
  /* if the string is equal to "*" it means the value
   *  of the interval is unbounded                    */
  if (EQUAL(string, "*")) {
    u->scalar = MS_WCS20_UNBOUNDED;
    u->unbounded = 1;
    return MS_WCS20_UNDEFINED_VALUE;
  }

  /* if returned a valid value, use it */
  if (msStringParseDouble(string, &(u->scalar)) == MS_SUCCESS) {
    return MS_WCS20_SCALAR_VALUE;
  }
  /* otherwise it might be a time value */
  msTimeInit(&time);
  if (msParseTime(string, &time) == MS_TRUE) {
    u->time = mktime(&time);
    return MS_WCS20_TIME_VALUE;
  }
  /* the value could neither be parsed as a double nor as a time value */
  else {
    msSetError(MS_WCSERR,
               "String %s could not be parsed to a time or scalar value",
               "msWCSParseTimeOrScalar20()",string);
    return MS_WCS20_ERROR_VALUE;
  }
}

/************************************************************************/
/*                   msWCSCreateSubsetObj20()                           */
/*                                                                      */
/*      Creates a new wcs20SubsetObj and initializes it to standard     */
/*      values.                                                         */
/************************************************************************/

static wcs20SubsetObjPtr msWCSCreateSubsetObj20()
{
  wcs20SubsetObjPtr subset = (wcs20SubsetObjPtr) malloc(sizeof(wcs20SubsetObj));
  MS_CHECK_ALLOC(subset, sizeof(wcs20SubsetObj), NULL);

  subset->axis = NULL;
  subset->crs = NULL;
  subset->min.scalar = subset->max.scalar = MS_WCS20_UNBOUNDED;
  subset->min.unbounded = subset->max.unbounded = 0;
  subset->operation = MS_WCS20_SLICE;

  return subset;
}

/************************************************************************/
/*                   msWCSFreeSubsetObj20()                             */
/*                                                                      */
/*      Frees a wcs20SubsetObj and releases all linked resources.       */
/************************************************************************/

static void msWCSFreeSubsetObj20(wcs20SubsetObjPtr subset)
{
  if (NULL == subset) {
    return;
  }
  msFree(subset->axis);
  msFree(subset->crs);
  msFree(subset);
}

/************************************************************************/
/*                   msWCSCreateAxisObj20()                             */
/*                                                                      */
/*      Creates a new wcs20AxisObj and initializes it to standard       */
/*      values.                                                         */
/************************************************************************/

static wcs20AxisObjPtr msWCSCreateAxisObj20()
{
  wcs20AxisObj *axis = (wcs20AxisObjPtr)malloc(sizeof(wcs20AxisObj));
  MS_CHECK_ALLOC(axis, sizeof(wcs20AxisObj), NULL);

  axis->name = NULL;
  axis->size = 0;
  axis->resolution = MS_WCS20_UNBOUNDED;
  axis->scale = MS_WCS20_UNBOUNDED;
  axis->resolutionUOM = NULL;
  axis->subset = NULL;

  return axis;
}

/************************************************************************/
/*                   msWCSFreeAxisObj20()                               */
/*                                                                      */
/*      Frees a wcs20AxisObj and releases all linked resources.         */
/************************************************************************/

static void msWCSFreeAxisObj20(wcs20AxisObjPtr axis)
{
  if(NULL == axis) {
    return;
  }

  msFree(axis->name);
  msFree(axis->resolutionUOM);
  msWCSFreeSubsetObj20(axis->subset);
  msFree(axis);
}

/************************************************************************/
/*                   msWCSFindAxis20()                                  */
/*                                                                      */
/*      Helper function to retrieve an axis by the name from a params   */
/*      object.                                                         */
/************************************************************************/

static wcs20AxisObjPtr msWCSFindAxis20(wcs20ParamsObjPtr params,
                                       const char *name)
{
  int i = 0;
  for(i = 0; i < params->numaxes; ++i) {
    if(EQUAL(params->axes[i]->name, name)) {
      return params->axes[i];
    }
  }
  return NULL;
}

/************************************************************************/
/*                   msWCSInsertAxisObj20()                             */
/*                                                                      */
/*      Helper function to insert an axis object into the axes list of  */
/*      a params object.                                                */
/************************************************************************/

static void msWCSInsertAxisObj20(wcs20ParamsObjPtr params, wcs20AxisObjPtr axis)
{
  params->numaxes++;
  params->axes = (wcs20AxisObjPtr*) msSmallRealloc(params->axes,
                 sizeof(wcs20AxisObjPtr) * (params->numaxes));
  params->axes[params->numaxes - 1] = axis;
}

/************************************************************************/
/*                   msWCSCreateParamsObj20()                           */
/*                                                                      */
/*      Creates a new wcs20ParamsObj and initializes it to standard     */
/*      values.                                                         */
/************************************************************************/

wcs20ParamsObjPtr msWCSCreateParamsObj20()
{
  wcs20ParamsObjPtr params
    = (wcs20ParamsObjPtr) malloc(sizeof(wcs20ParamsObj));
  MS_CHECK_ALLOC(params, sizeof(wcs20ParamsObj), NULL);

  params->version         = NULL;
  params->request         = NULL;
  params->service         = NULL;
  params->accept_versions = NULL;
  params->accept_languages = NULL;
  params->sections        = NULL;
  params->updatesequence  = NULL;
  params->ids             = NULL;
  params->width           = 0;
  params->height          = 0;
  params->resolutionX     = MS_WCS20_UNBOUNDED;
  params->resolutionY     = MS_WCS20_UNBOUNDED;
  params->scale           = MS_WCS20_UNBOUNDED;
  params->scaleX          = MS_WCS20_UNBOUNDED;
  params->scaleY          = MS_WCS20_UNBOUNDED;
  params->resolutionUnits = NULL;
  params->numaxes         = 0;
  params->axes            = NULL;
  params->format          = NULL;
  params->multipart       = 0;
  params->interpolation   = NULL;
  params->outputcrs       = NULL;
  params->subsetcrs       = NULL;
  params->bbox.minx = params->bbox.miny = -DBL_MAX;
  params->bbox.maxx = params->bbox.maxy =  DBL_MAX;
  params->range_subset    = NULL;
  params->format_options  = NULL;

  return params;
}

/************************************************************************/
/*                   msWCSFreeParamsObj20()                             */
/*                                                                      */
/*      Frees a wcs20ParamsObj and releases all linked resources.       */
/************************************************************************/

void msWCSFreeParamsObj20(wcs20ParamsObjPtr params)
{
  if (NULL == params) {
    return;
  }

  msFree(params->version);
  msFree(params->request);
  msFree(params->service);
  CSLDestroy(params->accept_versions);
  CSLDestroy(params->accept_languages);
  CSLDestroy(params->sections);
  msFree(params->updatesequence);
  CSLDestroy(params->ids);
  msFree(params->resolutionUnits);
  msFree(params->format);
  msFree(params->interpolation);
  msFree(params->outputcrs);
  msFree(params->subsetcrs);
  while(params->numaxes > 0) {
    params->numaxes -= 1;
    msWCSFreeAxisObj20(params->axes[params->numaxes]);
  }
  msFree(params->axes);
  CSLDestroy(params->range_subset);
  CSLDestroy(params->format_options);
  msFree(params);
}

/************************************************************************/
/*                   msWCSParseSubset20()                               */
/*                                                                      */
/*      Parses several string parameters and fills them into the        */
/*      subset object.                                                  */
/************************************************************************/

static int msWCSParseSubset20(wcs20SubsetObjPtr subset, const char *axis,
                              const char *crs, const char *min, const char *max)
{
  int ts1, ts2;
  ts1 = ts2 = MS_WCS20_UNDEFINED_VALUE;

  if (subset == NULL) {
    return MS_FAILURE;
  }

  if (axis == NULL || strlen(axis) == 0) {
    msSetError(MS_WCSERR, "Subset axis is not given.",
               "msWCSParseSubset20()");
    return MS_FAILURE;
  }

  subset->axis = msStrdup(axis);
  if (crs != NULL) {
    subset->crs = msStrdup(crs);
  }

  /* Parse first (probably only) part of interval/point;
   * check whether its a time value or a scalar value     */
  ts1 = msWCSParseTimeOrScalar20(&(subset->min), min);
  if (ts1 == MS_WCS20_ERROR_VALUE) {
    return MS_FAILURE;
  }

  /* check if its an interval */
  /* if there is a comma, then it is */
  if (max != NULL && strlen(max) > 0) {
    subset->operation = MS_WCS20_TRIM;

    /* Parse the second value of the interval */
    ts2 = msWCSParseTimeOrScalar20(&(subset->max), max);
    if (ts2 == MS_WCS20_ERROR_VALUE) {
      return MS_FAILURE;
    }

    /* if at least one boundary is defined, use that value */
    if ((ts1 == MS_WCS20_UNDEFINED_VALUE) ^ (ts2
        == MS_WCS20_UNDEFINED_VALUE)) {
      if (ts1 == MS_WCS20_UNDEFINED_VALUE) {
        ts1 = ts2;
      }
    }
    /* if time and scalar values do not fit, throw an error */
    else if (ts1 != MS_WCS20_UNDEFINED_VALUE && ts2
             != MS_WCS20_UNDEFINED_VALUE && ts1 != ts2) {
      msSetError(MS_WCSERR,
                 "Interval error: minimum is a %s value, maximum is a %s value",
                 "msWCSParseSubset20()", ts1 ? "time" : "scalar",
                 ts2 ? "time" : "scalar");
      return MS_FAILURE;
    }
    /* if both min and max are unbounded -> throw an error */
    if (subset->min.unbounded && subset->max.unbounded) {
      msSetError(MS_WCSERR, "Invalid values: no bounds could be parsed",
                 "msWCSParseSubset20()");
      return MS_FAILURE;
    }
  }
  /* there is no second value, therefore it is a point.
   * consequently set the operation to slice */
  else {
    subset->operation = MS_WCS20_SLICE;
    if (ts1 == MS_WCS20_UNDEFINED_VALUE) {
      msSetError(MS_WCSERR, "Invalid point value given",
                 "msWCSParseSubset20()");
      return MS_FAILURE;
    }
  }

  subset->timeOrScalar = ts1;

  /* check whether the min is smaller than the max */
  if (subset->operation == MS_WCS20_TRIM) {
    if(subset->timeOrScalar == MS_WCS20_SCALAR_VALUE && subset->min.scalar == MS_WCS20_UNBOUNDED) {
      subset->min.scalar = -MS_WCS20_UNBOUNDED;
    }

    if (subset->timeOrScalar == MS_WCS20_TIME_VALUE && subset->min.time
        > subset->max.time) {
      msSetError(MS_WCSERR,
                 "Minimum value of subset axis %s is larger than maximum value",
                 "msWCSParseSubset20()", subset->axis);
      return MS_FAILURE;
    }
    if (subset->timeOrScalar == MS_WCS20_SCALAR_VALUE && subset->min.scalar > subset->max.scalar) {
      msSetError(MS_WCSERR,
                 "Minimum value (%f) of subset axis '%s' is larger than maximum value (%f).",
                 "msWCSParseSubset20()", subset->min.scalar, subset->axis, subset->max.scalar);
      return MS_FAILURE;
    }
  }
  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSParseSubsetKVPString20()                      */
/*                                                                      */
/*      Creates a new wcs20SubsetObj, parses a string and fills         */
/*      the parsed values into the object. Returns NULL on failure.     */
/*      Subset string: axis [ , crs ] ( intervalOrPoint )               */
/************************************************************************/

static int msWCSParseSubsetKVPString20(wcs20SubsetObjPtr subset, char *string)
{
  char *axis, *crs, *min, *max;

  axis = string;
  crs = NULL;
  min = NULL;
  max = NULL;

  /* find first '(' */
  min = strchr(string, '(');

  /* if min could not be found, the string is invalid */
  if (min == NULL) {
    msSetError(MS_WCSERR, "Invalid axis subset string: '%s'",
               "msWCSParseSubsetKVPString20()", string);
    return MS_FAILURE;
  }
  /* set min to first letter */
  *min = '\0';
  ++min;

  /* cut the trailing ')' */
  if (min[strlen(min) - 1] == ')') {
    min[strlen(min) - 1] = '\0';
  }
  /* look if also a max is defined */
  max = strchr(min, ',');
  if (max != NULL) {
    *max = '\0';
    ++max;
  }

  /* look if also a crs is defined */
  crs = strchr(axis, ',');
  if (crs != NULL) {
    *crs = '\0';
    ++crs;
  }

  return msWCSParseSubset20(subset, axis, crs, min, max);
}

/************************************************************************/
/*                   msWCSParseSizeString20()                           */
/*                                                                      */
/*      Parses a string containing the axis and the size as an integer. */
/*      Size string: axis ( size )                                      */
/************************************************************************/

static int msWCSParseSizeString20(char *string, char *outAxis, size_t axisStringLen, int *outSize)
{
  char *number = NULL;
  char *check = NULL;

  /* find first '(', the character before the number */
  number = strchr(string, '(');

  if(NULL == number) {
    msSetError(MS_WCSERR, "Invalid size parameter value.",
               "msWCSParseSize20()");
    return MS_FAILURE;
  }

  /* cut trailing ')' */
  check = strchr(string, ')');
  if(NULL == check) {
    msSetError(MS_WCSERR, "Invalid size parameter value.",
               "msWCSParseSize20()");
    return MS_FAILURE;
  }
  *number = '\0';
  ++number;
  *check = '\0';

  strlcpy(outAxis, string, axisStringLen);

  /* parse size value */
  if(msStringParseInteger(number, outSize) != MS_SUCCESS) {
    msSetError(MS_WCSERR, "Parameter value '%s' is not a valid integer.",
               "msWCSParseSize20()", number);
    return MS_FAILURE;
  }

  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSParseResolutionString20()                     */
/*                                                                      */
/*      Parses a resolution string and returns the axis, the units of   */
/*      measure and the resolution value.                               */
/*      Subset string: axis ( value )                                   */
/************************************************************************/

static int msWCSParseResolutionString20(char *string,
                                        char *outAxis, size_t axisStringLen, double *outResolution)
{
  char *number = NULL;
  char *check = NULL;

  /* find brackets */
  number = strchr(string, '(');

  if(NULL == number) {
    msSetError(MS_WCSERR, "Invalid resolution parameter value : %s.",
               "msWCSParseSize20()", string);
    return MS_FAILURE;
  }

  /* cut trailing ')' */
  check = strchr(string, ')');
  if(NULL == check) {
    msSetError(MS_WCSERR, "Invalid size parameter value.",
               "msWCSParseSize20()");
    return MS_FAILURE;
  }

  *number = '\0';
  ++number;
  *check = '\0';

  strlcpy(outAxis, string, axisStringLen);

  if(msStringParseDouble(number, outResolution) != MS_SUCCESS) {
    *outResolution = MS_WCS20_UNBOUNDED;
    msSetError(MS_WCSERR, "Invalid resolution parameter value : %s.",
               "msWCSParseSize20()", number);
    return MS_FAILURE;
  }

  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSParseScaleString20()                          */
/*                                                                      */
/*      Parses a scale string and returns the axis, the and the value.  */
/*      Subset string: axis ( value )                                   */
/************************************************************************/

static int msWCSParseScaleString20(char *string,
                                  char *outAxis, size_t axisStringLen, double *outScale)
{
  char *number = NULL;
  char *check = NULL;

  /* find brackets */
  number = strchr(string, '(');

  if(NULL == number) {
    msSetError(MS_WCSERR, "Invalid resolution parameter value : %s.",
               "msWCSParseScaleString20()", string);
    return MS_FAILURE;
  }

  /* cut trailing ')' */
  check = strchr(string, ')');
  if(NULL == check || check < number) {
    msSetError(MS_WCSERR, "Invalid scale parameter value.",
               "msWCSParseScaleString20()");
    return MS_FAILURE;
  }

  *number = '\0';
  ++number;
  *check = '\0';

  strlcpy(outAxis, string, axisStringLen);

  if(msStringParseDouble(number, outScale) != MS_SUCCESS || *outScale <= 0.0) {
    *outScale = MS_WCS20_UNBOUNDED;
    msSetError(MS_WCSERR, "Invalid scale parameter value : %s.",
               "msWCSParseScaleString20()", number);
    return MS_FAILURE;
  }

  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSParseResolutionString20()                     */
/*                                                                      */
/*      Parses a resolution string and returns the axis, the units of   */
/*      measure and the resolution value.                               */
/*      Subset string: axis ( value )                                   */
/************************************************************************/

static int msWCSParseScaleExtentString20(char *string, char *outAxis,
                                         size_t axisStringLen,
                                         int *outMin, int *outMax)
{
  char *number = NULL;
  char *check = NULL;
  char *colon = NULL;

  /* find brackets */
  number = strchr(string, '(');

  if(NULL == number) {
    msSetError(MS_WCSERR, "Invalid extent parameter value : %s.",
               "msWCSParseScaleExtentString20()", string);
    return MS_FAILURE;
  }

  /* find colon */
  colon = strchr(string, ':');

  if(NULL == colon || colon < number) {
    msSetError(MS_WCSERR, "Invalid extent parameter value : %s.",
               "msWCSParseScaleExtentString20()", string);
    return MS_FAILURE;
  }

  /* cut trailing ')' */
  check = strchr(string, ')');

  if(NULL == check || check < colon) {
    msSetError(MS_WCSERR, "Invalid extent parameter value.",
               "msWCSParseScaleExtentString20()");
    return MS_FAILURE;
  }

  *number = '\0';
  ++number;
  *colon = '\0';
  ++colon;
  *check = '\0';

  strlcpy(outAxis, string, axisStringLen);

  if(msStringParseInteger(number, outMin) != MS_SUCCESS) {
    *outMin = 0;
    msSetError(MS_WCSERR, "Invalid min parameter value : %s.",
               "msWCSParseScaleExtentString20()", number);
    return MS_FAILURE;
  }
  else if(msStringParseInteger(colon, outMax) != MS_SUCCESS) {
    *outMax = 0;
    msSetError(MS_WCSERR, "Invalid resolution parameter value : %s.",
               "msWCSParseScaleExtentString20()", colon);
    return MS_FAILURE;
  }

  if (*outMin > *outMax) {
    msSetError(MS_WCSERR, "Invalid extent: lower part is higher than upper part.",
               "msWCSParseScaleExtentString20()");
    return MS_FAILURE;
  }

  return MS_SUCCESS;
}

#if defined(USE_LIBXML2)
/*
  Utility function to get the first child of a node with a given node name
  */

xmlNodePtr msLibXml2GetFirstChild(xmlNodePtr parent, const char *name) {
  xmlNodePtr node;
  if (!parent || !name) {
    return NULL;
  }

  XML_FOREACH_CHILD(parent, node) {
    XML_LOOP_IGNORE_COMMENT_OR_TEXT(node);
    if (EQUAL((char *)node->name, name)) {
      return node;
    }
  }
  return NULL;
}


/*
  Utility function to get the first child of a node with a given node name and
  namespace.
  */

xmlNodePtr msLibXml2GetFirstChildNs(xmlNodePtr parent, const char *name, xmlNsPtr ns) {
  xmlNodePtr node;
  if (!parent || !name || !ns) {
    return NULL;
  }

  XML_FOREACH_CHILD(parent, node) {
    XML_LOOP_IGNORE_COMMENT_OR_TEXT(node);
    if (EQUAL((char *)node->name, name) && ns == node->ns) {
      return node;
    }
  }
  return NULL;
}

/*
  Utility function to get the first child of a node with a given node name
  */

xmlNodePtr msLibXml2GetFirstChildElement(xmlNodePtr parent) {
  xmlNodePtr node;
  if (!parent) {
    return NULL;
  }

  XML_FOREACH_CHILD(parent, node) {
    if (node->type == XML_ELEMENT_NODE) {
      return node;
    }
  }
  return NULL;
}
#endif /* defined(USE_LIBXML2) */

/************************************************************************/
/*                   msWCSParseRequest20_XMLGetCapabilities()           */
/*                                                                      */
/*      Parses a DOM element, representing a GetCapabilities-request    */
/*      to a params object.                                             */
/************************************************************************/
#if defined(USE_LIBXML2)
static int msWCSParseRequest20_XMLGetCapabilities(
  xmlNodePtr root, wcs20ParamsObjPtr params)
{
  xmlNodePtr child;
  char *content = NULL;
  XML_FOREACH_CHILD(root, child) {
    XML_LOOP_IGNORE_COMMENT_OR_TEXT(child)
    else if (EQUAL((char *)child->name, "AcceptVersions")) {
      xmlNodePtr versionNode = NULL;
      XML_FOREACH_CHILD(child, versionNode) {
        XML_LOOP_IGNORE_COMMENT_OR_TEXT(versionNode);
        XML_ASSERT_NODE_NAME(versionNode, "Version");

        content = (char *)xmlNodeGetContent(versionNode);
        params->accept_versions = CSLAddString(params->accept_versions, content);
        xmlFree(content);
      }
    } else if(EQUAL((char *)child->name, "Sections")) {
      xmlNodePtr sectionNode = NULL;
      XML_FOREACH_CHILD(child, sectionNode) {
        XML_LOOP_IGNORE_COMMENT_OR_TEXT(sectionNode)
        XML_ASSERT_NODE_NAME(sectionNode, "Section");

        content = (char *)xmlNodeGetContent(sectionNode);
        params->sections = CSLAddString(params->sections, content);
        xmlFree(content);
      }
    } else if(EQUAL((char *)child->name, "UpdateSequence")) {
      params->updatesequence =
        (char *)xmlNodeGetContent(child);
    } else if(EQUAL((char *)child->name, "AcceptFormats")) {
      /* Maybe not necessary, since only format is xml.   */
      /* At least ignore it, to not generate an error.    */
    } else if(EQUAL((char *)child->name, "AcceptLanguages")) {
      xmlNodePtr languageNode;
      XML_FOREACH_CHILD(child, languageNode) {
        XML_LOOP_IGNORE_COMMENT_OR_TEXT(languageNode)
        XML_ASSERT_NODE_NAME(languageNode, "Language");

        content = (char *)xmlNodeGetContent(languageNode);
        params->accept_languages = CSLAddString(params->accept_languages, content);
        xmlFree(content);
      }
    } else {
      XML_UNKNOWN_NODE_ERROR(child);
    }
  }
  return MS_SUCCESS;
}
#endif

/************************************************************************/
/*                   msWCSParseRequest20_XMLDescribeCoverage()          */
/*                                                                      */
/*      Parses a DOM element, representing a DescribeCoverage-request   */
/*      to a params object.                                             */
/************************************************************************/
#if defined(USE_LIBXML2)
static int msWCSParseRequest20_XMLDescribeCoverage(
  xmlNodePtr root, wcs20ParamsObjPtr params)
{
  xmlNodePtr child;
  int numIds = 0;
  char *id;

  XML_FOREACH_CHILD(root, child) {
    XML_LOOP_IGNORE_COMMENT_OR_TEXT(child)
    XML_ASSERT_NODE_NAME(child, "CoverageID");

    /* Node content is the coverage ID */
    id = (char *)xmlNodeGetContent(child);
    if (id == NULL || strlen(id) == 0) {
      msSetError(MS_WCSERR, "CoverageID could not be parsed.",
                 "msWCSParseRequest20_XMLDescribeCoverage()");
      return MS_FAILURE;
    }
    /* insert coverage ID into the list */
    ++numIds;
    params->ids = CSLAddString(params->ids, (char *)id);
    xmlFree(id);
  }
  return MS_SUCCESS;
}
#endif

/************************************************************************/
/*                   msWCSParseRequest20_XMLGetCoverage()               */
/*                                                                      */
/*      Parses a DOM element, representing a GetCoverage-request to a   */
/*      params object.                                                  */
/************************************************************************/
#if defined(USE_LIBXML2)
static int msWCSParseRequest20_XMLGetCoverage(
  mapObj* map, xmlNodePtr root, wcs20ParamsObjPtr params)
{
  xmlNodePtr child;
  int numIds = 0;
  char *id;

  XML_FOREACH_CHILD(root, child) {
    XML_LOOP_IGNORE_COMMENT_OR_TEXT(child)
    else if (EQUAL((char *)child->name, "CoverageID")) {
      /* Node content is the coverage ID */
      id = (char *)xmlNodeGetContent(child);
      if (id == NULL || strlen(id) == 0) {
        msSetError(MS_WCSERR, "CoverageID could not be parsed.",
                   "msWCSParseRequest20_XMLGetCoverage()");
        return MS_FAILURE;
      }

      /* insert coverage ID into the list */
      ++numIds;
      params->ids = CSLAddString(params->ids, (char *)id);
      xmlFree(id);
    } else if (EQUAL((char *) child->name, "Format")) {
      msFree(params->format);
      params->format = (char *)xmlNodeGetContent(child);
    } else if (EQUAL((char *) child->name, "Mediatype")) {
      char *content = (char *)xmlNodeGetContent(child);
      if(content != NULL && (EQUAL(content, "multipart/mixed")
                          || EQUAL(content, "multipart/related"))) {
        params->multipart = MS_TRUE;
      }
      else {
        msSetError(MS_WCSERR, "Invalid value '%s' for parameter 'Mediatype'.",
                  "msWCSParseRequest20()", content);
        xmlFree(content);
        return MS_FAILURE;
      }
      xmlFree(content);
    } else if (EQUAL((char *) child->name, "DimensionTrim")) {
      wcs20AxisObjPtr axis = NULL;
      wcs20SubsetObjPtr subset = NULL;
      xmlNodePtr node = NULL;
      char *axisName = NULL, *min = NULL, *max = NULL, *crs = NULL;

      /* get strings for axis, min and max */
      XML_FOREACH_CHILD(child, node) {
        XML_LOOP_IGNORE_COMMENT_OR_TEXT(node)
        else if (EQUAL((char *)node->name, "Dimension")) {
          if (axisName != NULL) {
            msSetError(MS_WCSERR,
                       "Parameter 'Dimension' is already set.",
                       "msWCSParseRequest20_XMLGetCoverage()");
            return MS_FAILURE;
          }
          axisName = (char *) xmlNodeGetContent(node);
          crs = (char *) xmlGetProp(node, BAD_CAST "crs");
        } else if (EQUAL((char *)node->name, "trimLow")) {
          min = (char *) xmlNodeGetContent(node);
        } else if (EQUAL((char *)node->name, "trimHigh")) {
          max = (char *) xmlNodeGetContent(node);
        } else {
          msFree(axisName);
          msFree(min);
          msFree(max);
          msFree(crs);
          XML_UNKNOWN_NODE_ERROR(node);
        }
      }
      if(NULL == (subset = msWCSCreateSubsetObj20())) {
        msFree(axisName);
        msFree(min);
        msFree(max);
        msFree(crs);
        return MS_FAILURE;
      }

      /* min and max have to have a value */
      if(min == NULL ) {
        min = msStrdup("*");
      }
      if(max == NULL) {
        max = msStrdup("*");
      }
      if (msWCSParseSubset20(subset, axisName, crs, min, max)
          == MS_FAILURE) {
        msWCSFreeSubsetObj20(subset);
        msWCSException(map, "InvalidSubsetting", "subset", "2.0.1");
        return MS_DONE;
      }

      if(NULL == (axis = msWCSFindAxis20(params, subset->axis))) {
        if(NULL == (axis = msWCSCreateAxisObj20())) {
          msFree(axisName);
          msFree(min);
          msFree(max);
          msFree(crs);
          return MS_FAILURE;
        }
        axis->name = msStrdup(subset->axis);
        msWCSInsertAxisObj20(params, axis);
      }

      axis->subset = subset;

      /* cleanup */
      msFree(axisName);
      msFree(min);
      msFree(max);
      msFree(crs);
    } else if(EQUAL((char *) child->name, "DimensionSlice")) {
      msSetError(MS_WCSERR, "Operation '%s' is not supported by MapServer.",
                 "msWCSParseRequest20_XMLGetCoverage()", (char *)child->name);
      return MS_FAILURE;
    } else if(EQUAL((char *) child->name, "Size")) {
      wcs20AxisObjPtr axis;
      char *axisName;
      char *content;

      if(NULL == (axisName = (char *) xmlGetProp(child, BAD_CAST "dimension")) ) {
        msSetError(MS_WCSERR, "Attribute 'dimension' is missing in element 'Size'.",
                   "msWCSParseRequest20_XMLGetCoverage()");
        return MS_FAILURE;
      }

      if(NULL == (axis = msWCSFindAxis20(params, axisName))) {
        if(NULL == (axis = msWCSCreateAxisObj20())) {
          xmlFree(axisName);
          return MS_FAILURE;
        }
        axis->name = msStrdup(axisName);
        msWCSInsertAxisObj20(params, axis);
      }
      xmlFree(axisName);

      content = (char *)xmlNodeGetContent(child);
      if(msStringParseInteger(content, &(axis->size)) != MS_SUCCESS) {
        xmlFree(content);
        msSetError(MS_WCSERR, "Value of element 'Size' could not "
                   "be parsed to a valid integer.",
                   "msWCSParseRequest20_XMLGetCoverage()");
        return MS_FAILURE;
      }
      xmlFree(content);
    } else if(EQUAL((char *) child->name, "Resolution")) {
      wcs20AxisObjPtr axis;
      char *axisName;
      char *content;

      if(NULL == (axisName = (char *) xmlGetProp(child, BAD_CAST "dimension"))) {
        msSetError(MS_WCSERR, "Attribute 'dimension' is missing "
                   "in element 'Resolution'.",
                   "msWCSParseRequest20_XMLGetCoverage()");
        return MS_FAILURE;
      }

      if(NULL == (axis = msWCSFindAxis20(params, axisName))) {
        if(NULL == (axis = msWCSCreateAxisObj20())) {
          xmlFree(axisName);
          return MS_FAILURE;
        }
        axis->name = msStrdup(axisName);
        msWCSInsertAxisObj20(params, axis);
      }
      xmlFree(axisName);

      axis->resolutionUOM = (char *) xmlGetProp(child, BAD_CAST "uom");

      content = (char *)xmlNodeGetContent(child);
      if(msStringParseDouble(content, &(axis->resolution)) != MS_SUCCESS) {
        msSetError(MS_WCSERR, "Value of element 'Resolution' could not "
                   "be parsed to a valid value.",
                   "msWCSParseRequest20_XMLGetCoverage()");
        xmlFree(content);
        return MS_FAILURE;
      }
      xmlFree(content);
    } else if(EQUAL((char *) child->name, "Interpolation")) {
      /* Deprecated, use wcs:Extension/int:Interpolation/int:globalInterpolation */
      msFree(params->interpolation);
      params->interpolation = (char *) xmlNodeGetContent(child);
    } else if(EQUAL((char *) child->name, "OutputCRS")) {
      params->outputcrs = (char *) xmlNodeGetContent(child);
    } else if(EQUAL((char *) child->name, "rangeSubset")) {
      /* Deprecated, use wcs:Extension/rsub:RangeSubset */
      xmlNodePtr bandNode = NULL;
      XML_FOREACH_CHILD(child, bandNode) {
        char *content = NULL;
        XML_LOOP_IGNORE_COMMENT_OR_TEXT(bandNode);
        XML_ASSERT_NODE_NAME(bandNode, "band");

        content = (char *)xmlNodeGetContent(bandNode);
        params->range_subset =
          CSLAddString(params->range_subset, content);
        xmlFree(content);
      }
    } else if (EQUAL((char *)child->name, "Extension")) {
      xmlNodePtr extensionNode = NULL;
      XML_FOREACH_CHILD(child, extensionNode) {
        XML_LOOP_IGNORE_COMMENT_OR_TEXT(extensionNode);

        if (EQUAL((char *) extensionNode->name, "Scaling")) {
          xmlNodePtr scaleMethodNode = msLibXml2GetFirstChildElement(extensionNode);

          if (EQUAL((char *) scaleMethodNode->name, "ScaleByFactor")) {
            xmlNodePtr scaleFactorNode = msLibXml2GetFirstChildElement(scaleMethodNode);
            char *content;
            if (!scaleFactorNode || !EQUAL((char *)scaleFactorNode->name, "scaleFactor")) {
              msSetError(MS_WCSERR, "Missing 'scaleFactor' node.",
                         "msWCSParseRequest20_XMLGetCoverage()");
              return MS_FAILURE;
            }
            content = (char *)xmlNodeGetContent(scaleFactorNode);
            if (msStringParseDouble(content, &(params->scale)) != MS_SUCCESS
                || params->scale < 0.0) {
              msSetError(MS_WCSERR, "Invalid scaleFactor '%s'.",
                         "msWCSParseRequest20_XMLGetCoverage()", content);
              xmlFree(content);
              return MS_FAILURE;
            }
            xmlFree(content);
          }

          else if (EQUAL((char *) scaleMethodNode->name, "ScaleAxesByFactor")) {
            xmlNodePtr scaleAxisNode, axisNode, scaleFactorNode;
            char *axisName, *content;
            wcs20AxisObjPtr axis;

            XML_FOREACH_CHILD(scaleMethodNode, scaleAxisNode) {
              XML_LOOP_IGNORE_COMMENT_OR_TEXT(scaleAxisNode);

              if (!EQUAL((char *)scaleAxisNode->name, "ScaleAxis")) {
                msSetError(MS_WCSERR, "Invalid ScaleAxesByFactor.",
                           "msWCSParseRequest20_XMLGetCoverage()");
                return MS_FAILURE;
              }

              /* axis */
              if (NULL == (axisNode = msLibXml2GetFirstChild(scaleAxisNode, "axis"))) {
                msSetError(MS_WCSERR, "Missing axis node",
                           "msWCSParseRequest20_XMLGetCoverage()");
                return MS_FAILURE;
              }
              axisName = (char *)xmlNodeGetContent(axisNode);
              if(NULL == (axis = msWCSFindAxis20(params, axisName))) {
                if(NULL == (axis = msWCSCreateAxisObj20())) {
                  xmlFree(axisName);
                  return MS_FAILURE;
                }
                axis->name = msStrdup(axisName);
                msWCSInsertAxisObj20(params, axis);
              }
              xmlFree(axisName);

              if (axis->scale != MS_WCS20_UNBOUNDED) {
                msSetError(MS_WCSERR, "scaleFactor was already set for axis '%s'.",
                           "msWCSParseRequest20_XMLGetCoverage()", axis->name);
                return MS_FAILURE;
              }

              /* scaleFactor */
              if (NULL == (scaleFactorNode = msLibXml2GetFirstChild(scaleAxisNode, "scaleFactor"))) {
                msSetError(MS_WCSERR, "Missing scaleFactor node",
                           "msWCSParseRequest20_XMLGetCoverage()");
                return MS_FAILURE;
              }

              content = (char *)xmlNodeGetContent(scaleFactorNode);
              if (msStringParseDouble(content, &(axis->scale)) != MS_SUCCESS
                  || axis->scale < 0.0) {
                msSetError(MS_WCSERR, "Invalid scaleFactor '%s'.",
                           "msWCSParseRequest20_XMLGetCoverage()", content);
                xmlFree(content);
                return MS_FAILURE;
              }
              xmlFree(content);
            }
          }

          else if (EQUAL((char *) scaleMethodNode->name, "ScaleToSize")) {
            xmlNodePtr scaleAxisNode, axisNode, targetSizeNode;
            char *axisName, *content;
            wcs20AxisObjPtr axis;

            XML_FOREACH_CHILD(scaleMethodNode, scaleAxisNode) {
              XML_LOOP_IGNORE_COMMENT_OR_TEXT(scaleAxisNode);

              if (!EQUAL((char *)scaleAxisNode->name, "TargetAxisSize")) {
                msSetError(MS_WCSERR, "Invalid ScaleToSize.",
                           "msWCSParseRequest20_XMLGetCoverage()");
                return MS_FAILURE;
              }

              /* axis */
              if (NULL == (axisNode = msLibXml2GetFirstChild(scaleAxisNode, "axis"))) {
                msSetError(MS_WCSERR, "Missing axis node",
                           "msWCSParseRequest20_XMLGetCoverage()");
                return MS_FAILURE;
              }
              axisName = (char *)xmlNodeGetContent(axisNode);
              if(NULL == (axis = msWCSFindAxis20(params, axisName))) {
                if(NULL == (axis = msWCSCreateAxisObj20())) {
                  xmlFree(axisName);
                  return MS_FAILURE;
                }
                axis->name = msStrdup(axisName);
                msWCSInsertAxisObj20(params, axis);
              }
              xmlFree(axisName);

              if (axis->size != 0) {
                msSetError(MS_WCSERR, "targetSize was already set for axis '%s'.",
                           "msWCSParseRequest20_XMLGetCoverage()", axis->name);
                return MS_FAILURE;
              }

              /* targetSize */
              if (NULL == (targetSizeNode = msLibXml2GetFirstChild(scaleAxisNode, "targetSize"))) {
                msSetError(MS_WCSERR, "Missing targetSize node",
                           "msWCSParseRequest20_XMLGetCoverage()");
                return MS_FAILURE;
              }

              content = (char *)xmlNodeGetContent(targetSizeNode);
              if (msStringParseInteger(content, &(axis->size)) != MS_SUCCESS
                  || axis->size <= 0) {
                msSetError(MS_WCSERR, "Invalid targetSize '%s'.",
                           "msWCSParseRequest20_XMLGetCoverage()", content);
                xmlFree(content);
                return MS_FAILURE;
              }
              xmlFree(content);
            }
          }

          else if (EQUAL((char *) scaleMethodNode->name, "ScaleToExtent")) {
            xmlNodePtr scaleAxisNode, axisNode, lowNode, highNode;
            char *axisName, *content;
            wcs20AxisObjPtr axis;
            int low, high;

            XML_FOREACH_CHILD(scaleMethodNode, scaleAxisNode) {
              XML_LOOP_IGNORE_COMMENT_OR_TEXT(scaleAxisNode);

              if (!EQUAL((char *)scaleAxisNode->name, "TargetAxisExtent")) {
                msSetError(MS_WCSERR, "Invalid ScaleToExtent.",
                           "msWCSParseRequest20_XMLGetCoverage()");
                return MS_FAILURE;
              }

              /* axis */
              if (NULL == (axisNode = msLibXml2GetFirstChild(scaleAxisNode, "axis"))) {
                msSetError(MS_WCSERR, "Missing axis node",
                           "msWCSParseRequest20_XMLGetCoverage()");
                return MS_FAILURE;
              }
              axisName = (char *)xmlNodeGetContent(axisNode);
              if(NULL == (axis = msWCSFindAxis20(params, axisName))) {
                if(NULL == (axis = msWCSCreateAxisObj20())) {
                  xmlFree(axisName);
                  return MS_FAILURE;
                }
                axis->name = msStrdup(axisName);
                msWCSInsertAxisObj20(params, axis);
              }
              xmlFree(axisName);

              if (axis->size != 0) {
                msSetError(MS_WCSERR, "targetSize was already set for axis '%s'.",
                           "msWCSParseRequest20_XMLGetCoverage()", axis->name);
                return MS_FAILURE;
              }

              /* targetSize */
              if (NULL == (lowNode = msLibXml2GetFirstChild(scaleAxisNode, "low"))) {
                msSetError(MS_WCSERR, "Missing low node",
                           "msWCSParseRequest20_XMLGetCoverage()");
                return MS_FAILURE;
              }

              if (NULL == (highNode = msLibXml2GetFirstChild(scaleAxisNode, "high"))) {
                msSetError(MS_WCSERR, "Missing high node",
                           "msWCSParseRequest20_XMLGetCoverage()");
                return MS_FAILURE;
              }

              content = (char *)xmlNodeGetContent(lowNode);
              if (msStringParseInteger(content, &low) != MS_SUCCESS) {
                msSetError(MS_WCSERR, "Invalid low value '%s'.",
                           "msWCSParseRequest20_XMLGetCoverage()", content);
                xmlFree(content);
                return MS_FAILURE;
              }
              xmlFree(content);


              content = (char *)xmlNodeGetContent(highNode);
              if (msStringParseInteger(content, &high) != MS_SUCCESS) {
                msSetError(MS_WCSERR, "Invalid high value '%s'.",
                           "msWCSParseRequest20_XMLGetCoverage()", content);
                xmlFree(content);
                return MS_FAILURE;
              }
              xmlFree(content);

              if (high <= low) {
                msSetError(MS_WCSERR, "Invalid extent, high is lower than low.",
                           "msWCSParseRequest20_XMLGetCoverage()");
                return MS_FAILURE;
              }

              axis->size = high - low;
            }
          }
        }

        /* Range Subset */
        else if(EQUAL((char *) extensionNode->name, "RangeSubset")) {
          xmlNodePtr rangeItemNode = NULL;

          XML_FOREACH_CHILD(extensionNode, rangeItemNode) {
            xmlNodePtr rangeItemNodeChild = msLibXml2GetFirstChildElement(rangeItemNode);
            XML_LOOP_IGNORE_COMMENT_OR_TEXT(rangeItemNode);

            XML_ASSERT_NODE_NAME(rangeItemNode, "RangeItem");

            if (!rangeItemNodeChild) {
              msSetError(MS_WCSERR, "Missing RangeComponent or RangeInterval.",
                                    "msWCSParseRequest20_XMLGetCoverage()");
              return MS_FAILURE;
            }
            else if (EQUAL((char *) rangeItemNodeChild->name, "RangeComponent")) {
              char *content = (char *)xmlNodeGetContent(rangeItemNodeChild);
              params->range_subset =
                CSLAddString(params->range_subset, content);
              xmlFree(content);
            }
            else if (EQUAL((char *) rangeItemNodeChild->name, "RangeInterval")) {
              xmlNodePtr intervalNode = rangeItemNodeChild;
              xmlNodePtr startComponentNode = msLibXml2GetFirstChild(intervalNode, "startComponent");
              xmlNodePtr endComponentNode = msLibXml2GetFirstChild(intervalNode, "endComponent");
              char *start;
              char *stop;

              if (!startComponentNode || !endComponentNode) {
                msSetError(MS_WCSERR, "Wrong RangeInterval.",
                                      "msWCSParseRequest20_XMLGetCoverage()");
                return MS_FAILURE;
              }

              start = (char *)xmlNodeGetContent(startComponentNode);
              stop = (char *)xmlNodeGetContent(endComponentNode);

              std::string value(start);
              value += ':';
              value += stop;

              xmlFree(start);
              xmlFree(stop);

              params->range_subset =
                CSLAddString(params->range_subset, value.c_str());
            }
          }
        }

        else if (EQUAL((char *) extensionNode->name, "subsettingCrs")) {
          msFree(params->subsetcrs);
          params->subsetcrs = (char *)xmlNodeGetContent(extensionNode);
        }

        else if (EQUAL((char *) extensionNode->name, "outputCrs")) {
          msFree(params->outputcrs);
          params->outputcrs = (char *)xmlNodeGetContent(extensionNode);
        }

        else if (EQUAL((char *) extensionNode->name, "Interpolation")) {
          xmlNodePtr globalInterpolation = msLibXml2GetFirstChild(extensionNode, "globalInterpolation");
          char *content;
          if (globalInterpolation == NULL) {
            msSetError(MS_WCSERR, "Missing 'globalInterpolation' node.",
                                  "msWCSParseRequest20_XMLGetCoverage()");
            return MS_FAILURE;
          }
          content = (char *)xmlNodeGetContent(globalInterpolation);
          msFree(params->interpolation);
          /* TODO: use URIs/URLs once they are specified */
          params->interpolation = msStrdup(content);
          xmlFree(content);
        }

        /* GeoTIFF parameters */
        else if (EQUAL((char *) extensionNode->name, "parameters")
                && extensionNode->ns
                && EQUAL((char *)extensionNode->ns->href, "http://www.opengis.net/gmlcov/geotiff/1.0")) {

          xmlNodePtr parameter;

          XML_FOREACH_CHILD(extensionNode, parameter) {
            char *content;
            XML_LOOP_IGNORE_COMMENT_OR_TEXT(parameter);

            content = (char *)xmlNodeGetContent(parameter);

            params->format_options =
              CSLAddNameValue(params->format_options, (char *)parameter->name, content);
            xmlFree(content);
          }
        }
      }
    } else {
      XML_UNKNOWN_NODE_ERROR(child);
    }
  }
  return MS_SUCCESS;
}
#endif

/************************************************************************/
/*                   msWCSParseRequest20()                              */
/*                                                                      */
/*      Parses a CGI-request to a WCS 20 params object. It is           */
/*      either a POST or a GET request. In case of a POST request       */
/*      the xml content has to be parsed to a DOM structure             */
/*      before the parameters can be extracted.                         */
/************************************************************************/

int msWCSParseRequest20(mapObj *map,
                        cgiRequestObj *request,
                        owsRequestObj *ows_request,
                        wcs20ParamsObjPtr params)
{
  int i;
  if (params == NULL || request == NULL || ows_request == NULL) {
    msSetError(MS_WCSERR, "Internal error.", "msWCSParseRequest20()");
    return MS_FAILURE;
  }

  /* Copy arbitrary service, version and request. */
  params->service = msStrdup(ows_request->service);
  if(ows_request->version != NULL) {
    params->version = msStrdup(ows_request->version);
  }
  params->request = msStrdup(ows_request->request);


  /* Parse the POST request */
  if (request->type == MS_POST_REQUEST) {
#if defined(USE_LIBXML2)
    xmlDocPtr doc = static_cast<xmlDocPtr>(ows_request->document);
    xmlNodePtr root = NULL;
    const char *validate;
    int ret = MS_SUCCESS;

    /* parse to DOM-Structure and get root element */
    if(doc == NULL) {
      xmlErrorPtr error = xmlGetLastError();
      msSetError(MS_WCSERR, "XML parsing error: %s",
                 "msWCSParseRequest20()", error->message);
      return MS_FAILURE;
    }

    root = xmlDocGetRootElement(doc);

    validate = msOWSLookupMetadata(&(map->web.metadata), "CO", "validate_xml");
    if (validate != NULL && EQUAL(validate, "TRUE")) {
      char *schema_dir = msStrdup(msOWSLookupMetadata(&(map->web.metadata),
                                  "CO", "schemas_dir"));
      if (schema_dir != NULL
          && (params->version == NULL ||
              EQUALN(params->version, "2.0", 3))) {
        schema_dir = msStringConcatenate(schema_dir,
                                         "wcs/2.0.0/wcsAll.xsd");
        if (msOWSSchemaValidation(schema_dir, request->postrequest) != 0) {
          msSetError(MS_WCSERR, "Invalid POST request. "
                     "XML is not valid",
                     "msWCSParseRequest20()");
          return MS_FAILURE;
        }
      }
      msFree(schema_dir);
    }

    if(EQUAL(params->request, "GetCapabilities")) {
      ret = msWCSParseRequest20_XMLGetCapabilities(root, params);
    } else if(params->version != NULL && EQUALN(params->version, "2.0", 3)) {
      if(EQUAL(params->request, "DescribeCoverage")) {
        ret = msWCSParseRequest20_XMLDescribeCoverage(root, params);
      } else if(EQUAL(params->request, "GetCoverage")) {
        ret = msWCSParseRequest20_XMLGetCoverage(map, root, params);
      }
    }
    return ret;

#else /* defined(USE_LIBXML2) */
    /* TODO: maybe with CPLXML? */
    return MS_FAILURE;
#endif /* defined(USE_LIBXML2) */
  }

  /* Parse the KVP GET request */
  for (i = 0; i < request->NumParams; ++i) {
    char *key = NULL, *value = NULL;
    char **tokens;
    int num, j;
    key = request->ParamNames[i];
    value = request->ParamValues[i];

    if (EQUAL(key, "VERSION")) {
      continue;
    } else if (EQUAL(key, "REQUEST")) {
      continue;
    } else if (EQUAL(key, "SERVICE")) {
      continue;
    } else if (EQUAL(key, "ACCEPTVERSIONS")) {
      tokens = msStringSplit(value, ',', &num);
      for(j = 0; j < num; ++j) {
        params->accept_versions =
          CSLAddString(params->accept_versions, tokens[j]);
      }
      msFreeCharArray(tokens, num);
    } else if (EQUAL(key, "SECTIONS")) {
      tokens = msStringSplit(value, ',', &num);
      for(j = 0; j < num; ++j) {
        params->sections =
          CSLAddString(params->sections, tokens[j]);
      }
      msFreeCharArray(tokens, num);
    } else if (EQUAL(key, "UPDATESEQUENCE")) {
      msFree(params->updatesequence);
      params->updatesequence = msStrdup(value);
    } else if (EQUAL(key, "ACCEPTFORMATS")) {
      /* ignore */
    } else if (EQUAL(key, "ACCEPTLANGUAGES")) {
      if (params->accept_languages != NULL) {
        CSLDestroy(params->accept_languages);
      }
      params->accept_languages = CSLTokenizeString2(value, ",", 0);
    } else if (EQUAL(key, "COVERAGEID")) {
      if (params->ids != NULL) {
        msSetError(MS_WCSERR, "Parameter 'CoverageID' is already set. "
                   "For multiple IDs use a comma separated list.",
                   "msWCSParseRequest20()");
        return MS_FAILURE;
      }
      params->ids = CSLTokenizeString2(value, ",",0);
    } else if (EQUAL(key, "FORMAT")) {
      msFree(params->format);
      params->format = msStrdup(value);
    } else if (EQUAL(key, "MEDIATYPE")) {
      if(EQUAL(value, "multipart/mixed") || EQUAL(value, "multipart/related")) {
        params->multipart = MS_TRUE;
      }
      else {
         msSetError(MS_WCSERR, "Invalid value '%s' for parameter 'Mediatype'.",
                    "msWCSParseRequest20()", value);
         return MS_FAILURE;
      }
    } else if (EQUAL(key, "INTERPOLATION")) {
      msFree(params->interpolation);
      params->interpolation = msStrdup(value);
    } else if (EQUAL(key, "OUTPUTCRS")) {
      msFree(params->outputcrs);
      params->outputcrs = msStrdup(value);
    } else if (EQUAL(key, "SUBSETTINGCRS")) {
      msFree(params->subsetcrs);
      params->subsetcrs = msStrdup(value);
    } else if (EQUAL(key, "SCALEFACTOR")) {
      double scale = MS_WCS20_UNBOUNDED;
      if (params->scale != MS_WCS20_UNBOUNDED) {
        msSetError(MS_WCSERR, "Parameter 'SCALEFACTOR' already set.",
                    "msWCSParseRequest20()");
        return MS_FAILURE;
      } else if (msStringParseDouble(value, &scale) != MS_SUCCESS) {
        msSetError(MS_WCSERR, "Could not parse parameter 'SCALEFACTOR'.",
                   "msWCSParseRequest20()");
        return MS_FAILURE;
      } else if (scale <= 0.0) {
        msSetError(MS_WCSERR, "Invalid value for 'SCALEFACTOR'.",
                   "msWCSParseRequest20()");
        return MS_FAILURE;
      }
      params->scale = scale;
    } else if (EQUAL(key, "SCALEAXES")) {
      wcs20AxisObjPtr axis = NULL;
      tokens = msStringSplit(value, ',', &num);
      for(j = 0; j < num; ++j) {
        char axisName[500];
        double scale;

        if (msWCSParseScaleString20(tokens[j], axisName, sizeof(axisName), &scale) != MS_SUCCESS) {
          msFreeCharArray(tokens, num);
          return MS_FAILURE;
        }

        if(NULL == (axis = msWCSFindAxis20(params, axisName))) {
          if(NULL == (axis = msWCSCreateAxisObj20())) {
            msFreeCharArray(tokens, num);
            return MS_FAILURE;
          }
          axis->name = msStrdup(axisName);
          msWCSInsertAxisObj20(params, axis);
        }

        /* check if the size of the axis is already set */
        if(axis->scale != MS_WCS20_UNBOUNDED) {
          msFreeCharArray(tokens, num);
          msSetError(MS_WCSERR, "The scale of the axis is already set.",
                     "msWCSParseRequest20()");
          return MS_FAILURE;
        }
        axis->scale = scale;
      }
      msFreeCharArray(tokens, num);
    } else if (EQUAL(key, "SCALESIZE")) {
      wcs20AxisObjPtr axis = NULL;
      tokens = msStringSplit(value, ',', &num);
      for(j = 0; j < num; ++j) {
        char axisName[500];
        int size;

        if (msWCSParseSizeString20(tokens[j], axisName, sizeof(axisName), &size) != MS_SUCCESS) {
          msFreeCharArray(tokens, num);
          return MS_FAILURE;
        }

        if(NULL == (axis = msWCSFindAxis20(params, axisName))) {
          if(NULL == (axis = msWCSCreateAxisObj20())) {
            msFreeCharArray(tokens, num);
            return MS_FAILURE;
          }
          axis->name = msStrdup(axisName);
          msWCSInsertAxisObj20(params, axis);
        }

        /* check if the size of the axis is already set */
        if(axis->size != 0) {
          msFreeCharArray(tokens, num);
          msSetError(MS_WCSERR, "The size of the axis is already set.",
                     "msWCSParseRequest20()");
          return MS_FAILURE;
        }
        axis->size = size;
      }
      msFreeCharArray(tokens, num);
    } else if (EQUAL(key, "SCALEEXTENT")) {
      wcs20AxisObjPtr axis = NULL;
      /* No real support for scaleextent, we just interprete it as SCALESIZE */
      tokens = msStringSplit(value, ',', &num);
      for(j = 0; j < num; ++j) {
        char axisName[500];
        int min, max;

        if (msWCSParseScaleExtentString20(tokens[j], axisName, sizeof(axisName),
                                          &min, &max) != MS_SUCCESS) {
          msFreeCharArray(tokens, num);
          return MS_FAILURE;
        }

        if(NULL == (axis = msWCSFindAxis20(params, axisName))) {
          if(NULL == (axis = msWCSCreateAxisObj20())) {
            msFreeCharArray(tokens, num);
            return MS_FAILURE;
          }
          axis->name = msStrdup(axisName);
          msWCSInsertAxisObj20(params, axis);
        }

        /* check if the size of the axis is already set */
        if(axis->size != 0) {
          msFreeCharArray(tokens, num);
          msSetError(MS_WCSERR, "The size of the axis is already set.",
                     "msWCSParseRequest20()");
          return MS_FAILURE;
        }
        axis->size = max - min;
      }
      msFreeCharArray(tokens, num);
      /* We explicitly don't test for strict equality as the parameter name is supposed to be unique */
    } else if (EQUALN(key, "SIZE", 4)) {
      /* Deprecated scaling */
      wcs20AxisObjPtr axis = NULL;
      char axisName[500];
      int size = 0;

      if(msWCSParseSizeString20(value, axisName, sizeof(axisName), &size) == MS_FAILURE) {
        return MS_FAILURE;
      }

      if(NULL == (axis = msWCSFindAxis20(params, axisName))) {
        if(NULL == (axis = msWCSCreateAxisObj20())) {
          return MS_FAILURE;
        }
        axis->name = msStrdup(axisName);
        msWCSInsertAxisObj20(params, axis);
      }

      /* check if the size of the axis is already set */
      if(axis->size != 0) {
        msSetError(MS_WCSERR, "The size of the axis is already set.",
                   "msWCSParseRequest20()");
        return MS_FAILURE;
      }
      axis->size = size;
    /* We explicitly don't test for strict equality as the parameter name is supposed to be unique */
    } else if (EQUALN(key, "RESOLUTION", 10)) {
      wcs20AxisObjPtr axis = NULL;
      char axisName[500];
      double resolution = 0;

      if(msWCSParseResolutionString20(value, axisName, sizeof(axisName), &resolution) == MS_FAILURE) {
        return MS_FAILURE;
      }

      /* check if axis object already exists, otherwise create a new one */
      if(NULL == (axis = msWCSFindAxis20(params, axisName))) {
        if(NULL == (axis = msWCSCreateAxisObj20())) {
          return MS_FAILURE;
        }
        axis->name = msStrdup(axisName);
        msWCSInsertAxisObj20(params, axis);
      }

      /* check if the resolution of the axis is already set */
      if(axis->resolution != MS_WCS20_UNBOUNDED) {
        msSetError(MS_WCSERR, "The resolution of the axis is already set.",
                   "msWCSParseRequest20()");
        return MS_FAILURE;
      }
      axis->resolution = resolution;
    /* We explicitly don't test for strict equality as the parameter name is supposed to be unique */
    } else if (EQUALN(key, "SUBSET", 6)) {
      wcs20AxisObjPtr axis = NULL;
      wcs20SubsetObjPtr subset = msWCSCreateSubsetObj20();
      if(NULL == subset) {
        return MS_FAILURE;
      }
      if (msWCSParseSubsetKVPString20(subset, value) == MS_FAILURE) {
        msWCSFreeSubsetObj20(subset);
        msWCSException(map, "InvalidSubsetting", "subset", ows_request->version);
        return MS_DONE;
      }

      if(NULL == (axis = msWCSFindAxis20(params, subset->axis))) {
        if(NULL == (axis = msWCSCreateAxisObj20())) {
          return MS_FAILURE;
        }
        axis->name = msStrdup(subset->axis);
        msWCSInsertAxisObj20(params, axis);
      }

      if(NULL != axis->subset) {
        msSetError(MS_WCSERR, "The axis '%s' is already subsetted.",
                   "msWCSParseRequest20()", axis->name);
        msWCSFreeSubsetObj20(subset);
        msWCSException(map, "InvalidAxisLabel", "subset", ows_request->version);
        return MS_DONE;
      }
      axis->subset = subset;
    } else if(EQUAL(key, "RANGESUBSET")) {
      tokens = msStringSplit(value, ',', &num);
      for(j = 0; j < num; ++j) {
        params->range_subset =
          CSLAddString(params->range_subset, tokens[j]);
      }
      msFreeCharArray(tokens, num);
    } else if (EQUALN(key, "GEOTIFF:", 8)) {
      params->format_options =
        CSLAddNameValue(params->format_options, key, value);
    }
    /* Ignore all other parameters here */
  }

  return MS_SUCCESS;
}

#if defined(USE_LIBXML2)

/************************************************************************/
/*                   msWCSValidateAndFindSubsets20()                    */
/*                                                                      */
/*      Iterates over every axis in the parameters and checks if the    */
/*      axis name is in any string list. If found, but there already is */
/*      a sample found for this axis, an Error is returned. Also if no  */
/*      axis is found for a given axis, an error is returned.           */
/************************************************************************/
static int msWCSValidateAndFindAxes20(
  wcs20ParamsObjPtr params,
  wcs20AxisObjPtr outAxes[])
{
  static const int numAxis = 2;
  const char * const validXAxisNames[] = {"x", "xaxis", "x-axis", "x_axis", "long", "long_axis", "long-axis", "lon", "lon_axis", "lon-axis", NULL};
  const char * const validYAxisNames[] = {"y", "yaxis", "y-axis", "y_axis", "lat", "lat_axis", "lat-axis", NULL};
  const char * const * const validAxisNames[2] = { validXAxisNames, validYAxisNames };
  int iParamAxis, iAcceptedAxis, iName, i;

  for(i = 0; i < numAxis; ++i) {
    outAxes[i] = NULL;
  }

  /* iterate over all subsets */
  for(iParamAxis = 0; iParamAxis < params->numaxes; ++iParamAxis) {
    int found = 0;

    /* iterate over all given axes */
    for(iAcceptedAxis = 0; iAcceptedAxis < numAxis; ++iAcceptedAxis ) {
      /* iterate over all possible names for the current axis */
      for(iName = 0; validAxisNames[iAcceptedAxis][iName] != NULL ; ++iName) {
        /* compare axis name with current possible name */
        if(EQUAL(params->axes[iParamAxis]->name, validAxisNames[iAcceptedAxis][iName])) {
          /* if there is already a sample for the axis, throw error */
          if(outAxes[iAcceptedAxis] != NULL) {
            msSetError(MS_WCSERR, "The axis with the name '%s' corresponds "
                       "to the same axis as the subset with the name '%s'.",
                       "msWCSValidateAndFindAxes20()",
                       outAxes[iAcceptedAxis]->name, params->axes[iParamAxis]->name);
            return MS_FAILURE;
          }

          /* if match is found, save it */
          outAxes[iAcceptedAxis] = params->axes[iParamAxis];
          found = 1;
          break;
        }
      }
      if (found) {
        break;
      }
    }

    /* no valid representation for current subset found */
    /* exit and throw error                             */
    if(found == 0) {
      msSetError(MS_WCSERR, "Invalid subset axis '%s'.",
                 "msWCSValidateAndFindAxes20()", params->axes[iParamAxis]->name);
      return MS_FAILURE;
    }
  }
  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSPrepareNamespaces20()                         */
/*                                                                      */
/*      Inserts namespace definitions into the root node of a DOM       */
/*      structure.                                                      */
/************************************************************************/

static void msWCSPrepareNamespaces20(xmlDocPtr pDoc, xmlNodePtr psRootNode, mapObj* map, int addInspire)
{
  xmlNsPtr psXsiNs;
  char *schemaLocation = NULL;
  char *xsi_schemaLocation = NULL;

  xmlSetNs(psRootNode, xmlNewNs(psRootNode, BAD_CAST MS_OWSCOMMON_WCS_20_NAMESPACE_URI, BAD_CAST MS_OWSCOMMON_WCS_NAMESPACE_PREFIX));

  xmlNewNs(psRootNode, BAD_CAST MS_OWSCOMMON_OWS_20_NAMESPACE_URI,        BAD_CAST MS_OWSCOMMON_OWS_NAMESPACE_PREFIX);
  xmlNewNs(psRootNode, BAD_CAST MS_OWSCOMMON_OGC_NAMESPACE_URI,           BAD_CAST MS_OWSCOMMON_OGC_NAMESPACE_PREFIX );
  xmlNewNs(psRootNode, BAD_CAST MS_OWSCOMMON_W3C_XSI_NAMESPACE_URI,       BAD_CAST MS_OWSCOMMON_W3C_XSI_NAMESPACE_PREFIX);
  xmlNewNs(psRootNode, BAD_CAST MS_OWSCOMMON_W3C_XLINK_NAMESPACE_URI,     BAD_CAST MS_OWSCOMMON_W3C_XLINK_NAMESPACE_PREFIX);
  xmlNewNs(psRootNode, BAD_CAST MS_OWSCOMMON_WCS_20_NAMESPACE_URI,        BAD_CAST MS_OWSCOMMON_WCS_NAMESPACE_PREFIX);
  xmlNewNs(psRootNode, BAD_CAST MS_OWSCOMMON_GML_32_NAMESPACE_URI,        BAD_CAST MS_OWSCOMMON_GML_NAMESPACE_PREFIX);
  xmlNewNs(psRootNode, BAD_CAST MS_OWSCOMMON_GMLCOV_10_NAMESPACE_URI,     BAD_CAST MS_OWSCOMMON_GMLCOV_NAMESPACE_PREFIX);
  xmlNewNs(psRootNode, BAD_CAST MS_OWSCOMMON_SWE_20_NAMESPACE_URI,        BAD_CAST MS_OWSCOMMON_SWE_NAMESPACE_PREFIX);

  if (addInspire) {
    xmlNewNs(psRootNode, BAD_CAST MS_INSPIRE_COMMON_NAMESPACE_URI, BAD_CAST MS_INSPIRE_COMMON_NAMESPACE_PREFIX);
    xmlNewNs(psRootNode, BAD_CAST MS_INSPIRE_DLS_NAMESPACE_URI, BAD_CAST MS_INSPIRE_DLS_NAMESPACE_PREFIX);
  }

  psXsiNs = xmlSearchNs(pDoc, psRootNode, BAD_CAST MS_OWSCOMMON_W3C_XSI_NAMESPACE_PREFIX);

  schemaLocation = msEncodeHTMLEntities( msOWSGetSchemasLocation(map) );
  xsi_schemaLocation = msStrdup(MS_OWSCOMMON_WCS_20_NAMESPACE_URI);
  xsi_schemaLocation = msStringConcatenate(xsi_schemaLocation, " ");
  xsi_schemaLocation = msStringConcatenate(xsi_schemaLocation, schemaLocation);
  xsi_schemaLocation = msStringConcatenate(xsi_schemaLocation, MS_OWSCOMMON_WCS_20_SCHEMAS_LOCATION);
  xsi_schemaLocation = msStringConcatenate(xsi_schemaLocation, " ");

  if (addInspire) {
    xsi_schemaLocation = msStringConcatenate(xsi_schemaLocation, MS_INSPIRE_DLS_NAMESPACE_URI " ");
    xsi_schemaLocation = msStringConcatenate(xsi_schemaLocation, msOWSGetInspireSchemasLocation(map));
    xsi_schemaLocation = msStringConcatenate(xsi_schemaLocation, MS_INSPIRE_DLS_SCHEMA_LOCATION);
  }

  xmlNewNsProp(psRootNode, psXsiNs, BAD_CAST "schemaLocation", BAD_CAST xsi_schemaLocation);

  msFree(schemaLocation);
  msFree(xsi_schemaLocation);
}

/************************************************************************/
/*                   msWCSGetFormatList20()                             */
/*                                                                      */
/*      Copied from mapwcs.c.                                           */
/************************************************************************/

static char *msWCSGetFormatsList20( mapObj *map, layerObj *layer )
{
  char *format_list = msStrdup("");
  char **tokens = NULL, **formats = NULL;
  int  i, numtokens = 0, numformats;
  char *value;

  /* -------------------------------------------------------------------- */
  /*      Parse from layer metadata.                                      */
  /* -------------------------------------------------------------------- */
  if( layer != NULL
      && (value = msOWSGetEncodeMetadata( &(layer->metadata),"CO","formats",
                                          NULL )) != NULL ) {
    tokens = msStringSplit(value, ' ', &numtokens);
    msFree(value);
  }

  /* -------------------------------------------------------------------- */
  /*      Parse from map.web metadata.                                    */
  /* -------------------------------------------------------------------- */
  else if((value = msOWSGetEncodeMetadata( &(map->web.metadata), "CO", "formats",
                                           NULL)) != NULL ) {
    tokens = msStringSplit(value, ' ', &numtokens);
    msFree(value);
  }

  /* -------------------------------------------------------------------- */
  /*      Or generate from all configured raster output formats that      */
  /*      look plausible.                                                 */
  /* -------------------------------------------------------------------- */
  else {
    tokens = (char **) msSmallCalloc(map->numoutputformats,sizeof(char*));
    for( i = 0; i < map->numoutputformats; i++ ) {
      switch( map->outputformatlist[i]->renderer ) {
          /* seemingly normal raster format */
        case MS_RENDER_WITH_AGG:
        case MS_RENDER_WITH_RAWDATA:
          tokens[numtokens++] = msStrdup(map->outputformatlist[i]->name);
          break;
          /* rest of formats aren't really WCS compatible */
        default:
          break;
      }
    }
  }

  /* -------------------------------------------------------------------- */
  /*      Convert outputFormatObj names into mime types and remove        */
  /*      duplicates.                                                     */
  /* -------------------------------------------------------------------- */
  numformats = 0;
  formats = (char **) msSmallCalloc(sizeof(char*),numtokens);

  for( i = 0; i < numtokens; i++ ) {
    int format_i, j;
    const char *mimetype;

    for( format_i = 0; format_i < map->numoutputformats; format_i++ ) {
      if( EQUAL(map->outputformatlist[format_i]->name, tokens[i]) )
        break;
    }

    if( format_i == map->numoutputformats ) {
      msDebug("Failed to find outputformat info on format '%s', ignore.\n",
              tokens[i] );
      continue;
    }

    mimetype = map->outputformatlist[format_i]->mimetype;
    if( mimetype == NULL || strlen(mimetype) == 0 ) {
      msDebug("No mimetime for format '%s', ignoring.\n",
              tokens[i] );
      continue;
    }

    for( j = 0; j < numformats; j++ ) {
      if( EQUAL(mimetype,formats[j]) )
        break;
    }

    if( j < numformats ) {
      msDebug( "Format '%s' ignored since mimetype '%s' duplicates another outputFormatObj.\n",
               tokens[i], mimetype );
      continue;
    }

    formats[numformats++] = msStrdup(mimetype);
  }

  msFreeCharArray(tokens,numtokens);

  /* -------------------------------------------------------------------- */
  /*      Turn mimetype list into comma delimited form for easy use       */
  /*      with xml functions.                                             */
  /* -------------------------------------------------------------------- */
  for(i = 0; i < numformats; i++) {
    if(i > 0) {
      format_list = msStringConcatenate(format_list, (char *) ",");
    }
    format_list = msStringConcatenate(format_list, formats[i]);
  }
  msFreeCharArray(formats,numformats);

  return format_list;
}

/************************************************************************/
/*                   msWCSSwapAxes20                                    */
/*                                                                      */
/*      Helper function to determine if a SRS mandates swapped axes.    */
/************************************************************************/

static int msWCSSwapAxes20(char *srs_uri)
{
  int srid = 0;

  /* get SRID from the srs uri */
  if(srs_uri != NULL && strlen(srs_uri) > 0) {
    if(sscanf(srs_uri, "http://www.opengis.net/def/crs/EPSG/0/%d", &srid) != EOF)
      ;
    else if(sscanf(srs_uri, "http://www.opengis.net/def/crs/%d", &srid) != EOF)
      ;
    else
      srid = 0;
  }
  if (srid == 0)
    return MS_FALSE;

  return msIsAxisInverted(srid);
}

/************************************************************************/
/*                   msWCSCommon20_CreateBoundedBy()                    */
/*                                                                      */
/*      Inserts the BoundedBy section into an existing DOM structure.   */
/************************************************************************/

static void msWCSCommon20_CreateBoundedBy(wcs20coverageMetadataObjPtr cm,
    xmlNsPtr psGmlNs, xmlNodePtr psRoot, projectionObj *projection, int swapAxes)
{
  xmlNodePtr psBoundedBy, psEnvelope;
  char lowerCorner[100], upperCorner[100], axisLabels[100], uomLabels[100];

  psBoundedBy = xmlNewChild( psRoot, psGmlNs, BAD_CAST "boundedBy", NULL);
  {
    psEnvelope = xmlNewChild(psBoundedBy, psGmlNs, BAD_CAST "Envelope", NULL);
    {
      xmlNewProp(psEnvelope, BAD_CAST "srsName", BAD_CAST cm->srs_uri);

      if(projection->proj != NULL && msProjIsGeographicCRS(projection)) {
        if (swapAxes == MS_FALSE) {
          strlcpy(axisLabels, "long lat", sizeof(axisLabels));
        } else {
          strlcpy(axisLabels, "lat long", sizeof(axisLabels));
        }
        strlcpy(uomLabels, "deg deg", sizeof(uomLabels));
      } else {
        if (swapAxes == MS_FALSE) {
          strlcpy(axisLabels, "x y", sizeof(axisLabels));
        } else {
          strlcpy(axisLabels, "y x", sizeof(axisLabels));
        }
        strlcpy(uomLabels, "m m", sizeof(uomLabels));
      }
      xmlNewProp(psEnvelope, BAD_CAST "axisLabels", BAD_CAST axisLabels);
      xmlNewProp(psEnvelope, BAD_CAST "uomLabels", BAD_CAST uomLabels);
      xmlNewProp(psEnvelope, BAD_CAST "srsDimension", BAD_CAST "2");

      if (swapAxes == MS_FALSE) {
        snprintf(lowerCorner, sizeof(lowerCorner), "%.15g %.15g", cm->extent.minx, cm->extent.miny);
        snprintf(upperCorner, sizeof(upperCorner), "%.15g %.15g", cm->extent.maxx, cm->extent.maxy);
      } else {
        snprintf(lowerCorner, sizeof(lowerCorner), "%.15g %.15g", cm->extent.miny, cm->extent.minx);
        snprintf(upperCorner, sizeof(upperCorner), "%.15g %.15g", cm->extent.maxy, cm->extent.maxx);
      }

      xmlNewChild(psEnvelope, psGmlNs, BAD_CAST "lowerCorner", BAD_CAST lowerCorner);
      xmlNewChild(psEnvelope, psGmlNs, BAD_CAST "upperCorner", BAD_CAST upperCorner);
    }
  }
}

/************************************************************************/
/*                   msWCSCommon20_CreateDomainSet()                    */
/*                                                                      */
/*      Inserts the DomainSet section into an existing DOM structure.   */
/************************************************************************/

static void msWCSCommon20_CreateDomainSet(layerObj* layer, wcs20coverageMetadataObjPtr cm,
    xmlNsPtr psGmlNs, xmlNodePtr psRoot, projectionObj *projection, int swapAxes)
{
  xmlNodePtr psDomainSet, psGrid, psLimits, psGridEnvelope, psOrigin,
             psOffsetX, psOffsetY;
  char low[100], high[100], id[100], point[100];
  char offsetVector1[100], offsetVector2[100], axisLabels[100];

  psDomainSet = xmlNewChild( psRoot, psGmlNs, BAD_CAST "domainSet", NULL);
  {
    psGrid = xmlNewChild(psDomainSet, psGmlNs, BAD_CAST "RectifiedGrid", NULL);
    {
      double x0 = cm->geotransform[0]+cm->geotransform[1]/2+cm->geotransform[2]/2;
      double y0 = cm->geotransform[3]+cm->geotransform[4]/2+cm->geotransform[5]/2;
      double resx = cm->geotransform[1];
      double resy = cm->geotransform[5];

      xmlNewProp(psGrid, BAD_CAST "dimension", BAD_CAST "2");
      snprintf(id, sizeof(id), "grid_%s", layer->name);
      xmlNewNsProp(psGrid, psGmlNs, BAD_CAST "id", BAD_CAST id);

      psLimits = xmlNewChild(psGrid, psGmlNs, BAD_CAST "limits", NULL);
      {
        psGridEnvelope = xmlNewChild(psLimits, psGmlNs, BAD_CAST "GridEnvelope", NULL);
        {
          strlcpy(low, "0 0", sizeof(low));
          snprintf(high, sizeof(high), "%d %d", cm->xsize - 1, cm->ysize - 1);

          xmlNewChild(psGridEnvelope, psGmlNs, BAD_CAST "low", BAD_CAST low);
          xmlNewChild(psGridEnvelope, psGmlNs, BAD_CAST "high", BAD_CAST high);
        }
      }

      if(projection->proj != NULL && msProjIsGeographicCRS(projection)) {
        strlcpy(axisLabels, "long lat", sizeof(axisLabels));
      } else {
        strlcpy(axisLabels, "x y", sizeof(axisLabels));
      }

      xmlNewChild(psGrid, psGmlNs, BAD_CAST "axisLabels", BAD_CAST axisLabels);

      psOrigin = xmlNewChild(psGrid, psGmlNs, BAD_CAST "origin", NULL);
      {
        if (swapAxes == MS_FALSE) {
          snprintf(point, sizeof(point), "%f %f", x0, y0);
        } else {
          snprintf(point, sizeof(point), "%f %f", y0, x0);
        }
        psOrigin = xmlNewChild(psOrigin, psGmlNs, BAD_CAST "Point", NULL);
        snprintf(id, sizeof(id), "grid_origin_%s", layer->name);
        xmlNewNsProp(psOrigin, psGmlNs, BAD_CAST "id", BAD_CAST id);
        xmlNewProp(psOrigin, BAD_CAST "srsName", BAD_CAST cm->srs_uri);

        xmlNewChild(psOrigin, psGmlNs, BAD_CAST "pos", BAD_CAST point);
      }

      if (swapAxes == MS_FALSE) {
        snprintf(offsetVector1, sizeof(offsetVector1), "%f 0", resx);
        snprintf(offsetVector2, sizeof(offsetVector2), "0 %f", resy);
      } else {
        snprintf(offsetVector1, sizeof(offsetVector1), "0 %f", resx);
        snprintf(offsetVector2, sizeof(offsetVector2), "%f 0", resy);
      }
      psOffsetX = xmlNewChild(psGrid, psGmlNs, BAD_CAST "offsetVector", BAD_CAST offsetVector1);
      psOffsetY = xmlNewChild(psGrid, psGmlNs, BAD_CAST "offsetVector", BAD_CAST offsetVector2);

      xmlNewProp(psOffsetX, BAD_CAST "srsName", BAD_CAST cm->srs_uri);
      xmlNewProp(psOffsetY, BAD_CAST "srsName", BAD_CAST cm->srs_uri);
    }
  }
}

/************************************************************************/
/*                   msWCSCommon20_CreateRangeType()                    */
/*                                                                      */
/*      Inserts the RangeType section into an existing DOM structure.   */
/************************************************************************/

static void msWCSCommon20_CreateRangeType(wcs20coverageMetadataObjPtr cm, char *bands,
    xmlNsPtr psGmlcovNs, xmlNsPtr psSweNs, xmlNodePtr psRoot)
{
  xmlNodePtr psRangeType, psDataRecord, psField, psQuantity,
             psUom, psConstraint, psAllowedValues = NULL, psNilValues = NULL;
  char **arr = NULL;
  int num = 0;

  if(NULL != bands) {
    arr = msStringSplit(bands, ',', &num);
  }

  psRangeType = xmlNewChild( psRoot, psGmlcovNs, BAD_CAST "rangeType", NULL);
  psDataRecord  = xmlNewChild(psRangeType, psSweNs, BAD_CAST "DataRecord", NULL);

  /* iterate over every band */
  for(unsigned i = 0; i < cm->numbands; ++i) {
    /* only add bands that are in the range subset */
    if (NULL != arr && num > 0) {
      int found = MS_FALSE, j;
      for(j = 0; j < num; ++j) {
        int repr = 0;
        if(msStringParseInteger(arr[j], &repr) == MS_SUCCESS &&
           static_cast<unsigned>(repr) == i + 1) {
          found = MS_TRUE;
          break;
        }
      }
      if(found == MS_FALSE) {
        /* ignore this band since it is not in the range subset */
        continue;
      }
    }

    /* add field tag */
    psField = xmlNewChild(psDataRecord, psSweNs, BAD_CAST "field", NULL);

    if(cm->bands[i].name != NULL) {
      xmlNewProp(psField, BAD_CAST "name", BAD_CAST cm->bands[i].name);
    } else {
      xmlNewProp(psField, BAD_CAST "name", BAD_CAST "band");
    }
    /* add Quantity tag */
    psQuantity = xmlNewChild(psField, psSweNs, BAD_CAST "Quantity", NULL);
    if(cm->bands[i].definition != NULL) {
      xmlNewProp(psQuantity, BAD_CAST "definition", BAD_CAST cm->bands[i].definition);
    }
    if(cm->bands[i].description != NULL) {
      xmlNewChild(psQuantity, psSweNs, BAD_CAST "description", BAD_CAST cm->bands[i].description);
    }

    /* if there are given nilvalues -> add them to the first field */
    /* all other fields get a reference to these */
    if(cm->numnilvalues > 0) {
      psNilValues = xmlNewChild(
                      xmlNewChild(psQuantity, psSweNs, BAD_CAST "nilValues", NULL),
                      psSweNs, BAD_CAST "NilValues", NULL);
      for(unsigned j = 0; j < cm->numnilvalues; ++j) {
        xmlNodePtr psTemp =
          xmlNewChild(psNilValues, psSweNs, BAD_CAST "nilValue", BAD_CAST cm->nilvalues[j]);
        if(j < cm->numnilvalues)
          xmlNewProp(psTemp, BAD_CAST "reason", BAD_CAST cm->nilvalues_reasons[j]);
      }
    } else { /* create an empty nilValues tag */
      xmlNewChild(psQuantity, psSweNs, BAD_CAST "nilValues", NULL);
    }

    psUom = xmlNewChild(psQuantity, psSweNs, BAD_CAST "uom", NULL);
    if(cm->bands[i].uom != NULL) {
      xmlNewProp(psUom, BAD_CAST "code", BAD_CAST cm->bands[i].uom);
    } else {
      xmlNewProp(psUom, BAD_CAST "code", BAD_CAST "W.m-2.Sr-1");
    }

    /* add constraint */
    psConstraint = xmlNewChild(psQuantity, psSweNs, BAD_CAST "constraint", NULL);

    {
      char interval[100], significant_figures[100];
      psAllowedValues = xmlNewChild(psConstraint, psSweNs, BAD_CAST "AllowedValues", NULL);

      /* Interval */
      snprintf(interval, sizeof(interval), "%.5g %.5g", cm->bands[i].interval_min, cm->bands[i].interval_max);
      xmlNewChild(psAllowedValues, psSweNs, BAD_CAST "interval", BAD_CAST interval);

      /* Significant figures */
      snprintf(significant_figures, sizeof(significant_figures), "%d", cm->bands[i].significant_figures);
      xmlNewChild(psAllowedValues, psSweNs, BAD_CAST "significantFigures", BAD_CAST significant_figures);
    }
  }
  msFreeCharArray(arr,num);
}

/************************************************************************/
/*                   msWCSWriteDocument20()                             */
/*                                                                      */
/*      Writes out an xml structure to the stream.                      */
/************************************************************************/

static int msWCSWriteDocument20(xmlDocPtr psDoc)
{
  xmlChar *buffer = NULL;
  int size = 0;
  msIOContext *context = NULL;
  char *contenttype = NULL;

  if( msIO_needBinaryStdout() == MS_FAILURE ) {
    return MS_FAILURE;
  }

  if( EQUAL((char *)xmlDocGetRootElement(psDoc)->name, "RectifiedGridCoverage") )
    contenttype = msStrdup("application/gml+xml");
  else
    contenttype = msStrdup("text/xml");

  msIO_setHeader("Content-Type","%s; charset=UTF-8", contenttype);
  msIO_sendHeaders();
  msFree(contenttype);

  context = msIO_getHandler(stdout);

  xmlDocDumpFormatMemoryEnc(psDoc, &buffer, &size, "UTF-8", 1);
  msIO_contextWrite(context, buffer, size);
  xmlFree(buffer);

  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSWriteFile20()                                 */
/*                                                                      */
/*      Writes an image object to the stream. If multipart is set,      */
/*      then content sections are inserted.                             */
/************************************************************************/

static int msWCSWriteFile20(mapObj* map, imageObj* image, wcs20ParamsObjPtr params, int multipart)
{
  int status;
  char* filename = NULL;
  char *base_dir = NULL;
  const char *fo_filename;
  int i;

  fo_filename = msGetOutputFormatOption( image->format, "FILENAME", NULL );

  /* -------------------------------------------------------------------- */
  /*      Fetch the driver we will be using and check if it supports      */
  /*      VSIL IO.                                                        */
  /* -------------------------------------------------------------------- */
  if( EQUALN(image->format->driver,"GDAL/",5) ) {
    GDALDriverH hDriver;
    const char *pszExtension = image->format->extension;

    msAcquireLock( TLOCK_GDAL );
    hDriver = GDALGetDriverByName( image->format->driver+5 );
    if( hDriver == NULL ) {
      msReleaseLock( TLOCK_GDAL );
      msSetError( MS_MISCERR,
                  "Failed to find %s driver.",
                  "msWCSWriteFile20()",
                  image->format->driver+5 );
      return msWCSException(map, "NoApplicableCode", "mapserv",
                            params->version);
    }

    if( pszExtension == NULL )
      pszExtension = "img.tmp";

    if( msGDALDriverSupportsVirtualIOOutput(hDriver) ) {
      base_dir = msTmpFile(map, map->mappath, "/vsimem/wcsout", NULL);
      if( fo_filename )
        filename = msStrdup(CPLFormFilename(base_dir,
                                            fo_filename,NULL));
      else
        filename = msStrdup(CPLFormFilename(base_dir,
                                            "out", pszExtension ));

      /*            CleanVSIDir( "/vsimem/wcsout" ); */

      msReleaseLock( TLOCK_GDAL );
      status = msSaveImage(map, image, filename);
      if( status != MS_SUCCESS ) {
        msSetError(MS_MISCERR, "msSaveImage() failed",
                   "msWCSWriteFile20()");
        return msWCSException20(map, "NoApplicableCode", "mapserv",
                                params->version);
      }
    }
    msReleaseLock( TLOCK_GDAL );
  }

  /* -------------------------------------------------------------------- */
  /*      If we weren't able to write data under /vsimem, then we just    */
  /*      output a single "stock" filename.                               */
  /* -------------------------------------------------------------------- */
  if( filename == NULL ) {
    msOutputFormatResolveFromImage( map, image );
    if(multipart) {
      msIO_fprintf( stdout, "\r\n--wcs\r\n" );
      msIO_fprintf(
        stdout,
        "Content-Type: %s\r\n"
        "Content-Description: coverage data\r\n"
        "Content-Transfer-Encoding: binary\r\n",
        MS_IMAGE_MIME_TYPE(map->outputformat));

      if( fo_filename != NULL )
        msIO_fprintf( stdout,
                      "Content-ID: coverage/%s\r\n"
                      "Content-Disposition: INLINE; filename=%s\r\n\r\n",
                      fo_filename,
                      fo_filename);
      else
        msIO_fprintf( stdout,
                      "Content-ID: coverage/wcs.%s\r\n"
                      "Content-Disposition: INLINE\r\n\r\n",
                      MS_IMAGE_EXTENSION(map->outputformat));
    } else {
      msIO_setHeader("Content-Type","%s",MS_IMAGE_MIME_TYPE(map->outputformat));
      msIO_setHeader("Content-Description","coverage data");
      msIO_setHeader("Content-Transfer-Encoding","binary");

      if( fo_filename != NULL ) {
        msIO_setHeader("Content-ID","coverage/%s",fo_filename);
        msIO_setHeader("Content-Disposition","INLINE; filename=%s",fo_filename);
      } else {
        msIO_setHeader("Content-ID","coverage/wcs.%s",MS_IMAGE_EXTENSION(map->outputformat));
        msIO_setHeader("Content-Disposition","INLINE");
      }
      msIO_sendHeaders();
    }

    status = msSaveImage(map, image, NULL);
    if( status != MS_SUCCESS ) {
      msSetError( MS_MISCERR, "msSaveImage() failed", "msWCSWriteFile20()");
      return msWCSException(map, "NoApplicableCode", "mapserv", params->version);
    }
    if(multipart)
      msIO_fprintf( stdout, "\r\n--wcs--\r\n" );
    return MS_SUCCESS;
  }

  /* -------------------------------------------------------------------- */
  /*      When potentially listing multiple files, we take great care     */
  /*      to identify the "primary" file and list it first.  In fact      */
  /*      it is the only file listed in the coverages document.           */
  /* -------------------------------------------------------------------- */
  {
    char **all_files = CPLReadDir( base_dir );
    int count = CSLCount(all_files);

    if( msIO_needBinaryStdout() == MS_FAILURE )
      return MS_FAILURE;

    msAcquireLock( TLOCK_GDAL );
    for( i = count-1; i >= 0; i-- ) {
      const char *this_file = all_files[i];

      if( EQUAL(this_file,".") || EQUAL(this_file,"..") ) {
        all_files = CSLRemoveStrings( all_files, i, 1, NULL );
        continue;
      }

      if( i > 0 && EQUAL(this_file,CPLGetFilename(filename)) ) {
        all_files = CSLRemoveStrings( all_files, i, 1, NULL );
        all_files = CSLInsertString(all_files,0,CPLGetFilename(filename));
        i++;
      }
    }

    /* -------------------------------------------------------------------- */
    /*      Dump all the files in the memory directory as mime sections.    */
    /* -------------------------------------------------------------------- */
    count = CSLCount(all_files);

    if(count > 1 && multipart == MS_FALSE) {
      msDebug( "msWCSWriteFile20(): force multipart output without gml summary because we have multiple files in the result.\n" );

      multipart = MS_TRUE;
      msIO_setHeader("Content-Type","multipart/related; boundary=wcs");
      msIO_sendHeaders();
    }

    for( i = 0; i < count; i++ ) {
      const char *mimetype = NULL;
      VSILFILE *fp;
      unsigned char block[4000];
      int bytes_read;

      if( i == 0
          && !EQUAL(MS_IMAGE_MIME_TYPE(map->outputformat), "unknown") )
        mimetype = MS_IMAGE_MIME_TYPE(map->outputformat);

      if( mimetype == NULL )
        mimetype = "application/octet-stream";
      if(multipart) {
        msIO_fprintf( stdout, "\r\n--wcs\r\n" );
        msIO_fprintf(
          stdout,
          "Content-Type: %s\r\n"
          "Content-Description: coverage data\r\n"
          "Content-Transfer-Encoding: binary\r\n"
          "Content-ID: coverage/%s\r\n"
          "Content-Disposition: INLINE; filename=%s\r\n\r\n",
          mimetype,
          all_files[i],
          all_files[i]);
      } else {
        msIO_setHeader("Content-Type","%s",mimetype);
        msIO_setHeader("Content-Description","coverage data");
        msIO_setHeader("Content-Transfer-Encoding","binary");
        msIO_setHeader("Content-ID","coverage/%s",all_files[i]);
        msIO_setHeader("Content-Disposition","INLINE; filename=%s",all_files[i]);
        msIO_sendHeaders();
      }


      fp = VSIFOpenL(
             CPLFormFilename(base_dir, all_files[i], NULL),
             "rb" );
      if( fp == NULL ) {
        msReleaseLock( TLOCK_GDAL );
        msSetError( MS_MISCERR,
                    "Failed to open %s for streaming to stdout.",
                    "msWCSWriteFile20()", all_files[i] );
        return MS_FAILURE;
      }

      while( (bytes_read = VSIFReadL(block, 1, sizeof(block), fp)) > 0 )
        msIO_fwrite( block, 1, bytes_read, stdout );

      VSIFCloseL( fp );

      VSIUnlink( CPLFormFilename(base_dir, all_files[i], NULL) );
    }

    msFree(base_dir);
    msFree(filename);
    CSLDestroy( all_files );
    msReleaseLock( TLOCK_GDAL );
    if(multipart)
      msIO_fprintf( stdout, "\r\n--wcs--\r\n" );
    return MS_SUCCESS;
  }

  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSGetRangesetAxisMetadata20()                   */
/*                                                                      */
/*      Looks up a layers metadata for a specific axis information.     */
/************************************************************************/

static const char *msWCSLookupRangesetAxisMetadata20(hashTableObj *table,
    const char *axis, const char *item)
{
  char buf[500];
  const char* value;

  if(table == NULL || axis == NULL || item == NULL) {
    return NULL;
  }

  strlcpy(buf, axis, sizeof(buf));
  strlcat(buf, "_", sizeof(buf));
  strlcat(buf, item, sizeof(buf));
  if((value = msLookupHashTable(table, buf)) != NULL) {
    return value;
  }
  return msOWSLookupMetadata(table, "CO", buf);
}

/************************************************************************/
/*                   msWCSGetCoverageMetadata20()                       */
/*                                                                      */
/*      Inits a coverageMetadataObj. Uses msWCSGetCoverageMetadata()    */
/*      but exchanges the SRS URN by an URI for compliancy with 2.0.    */
/************************************************************************/

static int msWCSGetCoverageMetadata20(layerObj *layer, wcs20coverageMetadataObj *cm)
{
  char  *srs_uri = NULL;
  memset(cm,0,sizeof(wcs20coverageMetadataObj));
  if ( msCheckParentPointer(layer->map,"map") == MS_FAILURE )
    return MS_FAILURE;

  msOWSGetEPSGProj(&(layer->projection), &(layer->metadata), "CO", MS_TRUE, &(cm->srs_epsg));
  if(!cm->srs_epsg) {
    msOWSGetEPSGProj(&(layer->map->projection),
                                   &(layer->map->web.metadata), "CO", MS_TRUE, &cm->srs_epsg);
    if(!cm->srs_epsg) {
      msSetError(MS_WCSERR, "Unable to determine the SRS for this layer, "
                 "no projection defined and no metadata available.",
                 "msWCSGetCoverageMetadata20()");
      return MS_FAILURE;
    }
  }

  /* -------------------------------------------------------------------- */
  /*      Get the SRS in uri format.                                      */
  /* -------------------------------------------------------------------- */
  if((srs_uri = msOWSGetProjURI(&(layer->projection), &(layer->metadata),
                                "CO", MS_TRUE)) == NULL) {
    srs_uri = msOWSGetProjURI(&(layer->map->projection),
                              &(layer->map->web.metadata), "CO", MS_TRUE);
  }

  if( srs_uri != NULL ) {
    if( strlen(srs_uri) > sizeof(cm->srs_uri) - 1 ) {
      msSetError(MS_WCSERR, "SRS URI too long!",
                 "msWCSGetCoverageMetadata()");
      return MS_FAILURE;
    }

    strlcpy( cm->srs_uri, srs_uri, sizeof(cm->srs_uri) );
    msFree( srs_uri );
  } else {
    cm->srs_uri[0] = '\0';
  }

  /* setup nilvalues */
  cm->numnilvalues = 0;
  cm->nilvalues = NULL;
  cm->nilvalues_reasons = NULL;
  cm->native_format = NULL;

  /* -------------------------------------------------------------------- */
  /*      If we have "virtual dataset" metadata on the layer, then use    */
  /*      that in preference to inspecting the file(s).                   */
  /*      We require extent and either size or resolution.                */
  /* -------------------------------------------------------------------- */
  if( msOWSLookupMetadata(&(layer->metadata), "CO", "extent") != NULL
      && (msOWSLookupMetadata(&(layer->metadata), "CO", "resolution") != NULL
          || msOWSLookupMetadata(&(layer->metadata), "CO", "size") != NULL) ) {
    const char *value;

    /* get extent */
    cm->extent.minx = 0.0;
    cm->extent.maxx = 0.0;
    cm->extent.miny = 0.0;
    cm->extent.maxy = 0.0;
    if( msOWSGetLayerExtent( layer->map, layer, "CO", &cm->extent ) == MS_FAILURE )
      return MS_FAILURE;

    /* get resolution */
    cm->xresolution = 0.0;
    cm->yresolution = 0.0;
    if( (value = msOWSLookupMetadata(&(layer->metadata), "CO", "resolution")) != NULL ) {
      char **tokens;
      int n;

      tokens = msStringSplit(value, ' ', &n);
      if( tokens == NULL || n != 2 ) {
        msSetError( MS_WCSERR, "Wrong number of arguments for wcs|ows_resolution metadata.", "msWCSGetCoverageMetadata20()");
        msFreeCharArray( tokens, n );
        return MS_FAILURE;
      }
      cm->xresolution = atof(tokens[0]);
      cm->yresolution = atof(tokens[1]);
      msFreeCharArray( tokens, n );
    }

    /* get Size (in pixels and lines) */
    cm->xsize = 0;
    cm->ysize = 0;
    if( (value=msOWSLookupMetadata(&(layer->metadata), "CO", "size")) != NULL ) {
      char **tokens;
      int n;

      tokens = msStringSplit(value, ' ', &n);
      if( tokens == NULL || n != 2 ) {
        msSetError( MS_WCSERR, "Wrong number of arguments for wcs|ows_size metadata.", "msWCSGetCoverageMetadata20()");
        msFreeCharArray( tokens, n );
        return MS_FAILURE;
      }
      cm->xsize = atoi(tokens[0]);
      cm->ysize = atoi(tokens[1]);
      msFreeCharArray( tokens, n );
    }

    /* try to compute raster size */
    if( cm->xsize == 0 && cm->ysize == 0 && cm->xresolution != 0.0 && cm->yresolution != 0.0 && cm->extent.minx != cm->extent.maxx && cm->extent.miny != cm->extent.maxy ) {
      cm->xsize = (int) ((cm->extent.maxx - cm->extent.minx) / cm->xresolution + 0.5);
      cm->ysize = (int) fabs((cm->extent.maxy - cm->extent.miny) / cm->yresolution + 0.5);
    }

    /* try to compute raster resolution */
    if( (cm->xresolution == 0.0 || cm->yresolution == 0.0) && cm->xsize != 0 && cm->ysize != 0 ) {
      cm->xresolution = (cm->extent.maxx - cm->extent.minx) / cm->xsize;
      cm->yresolution = (cm->extent.maxy - cm->extent.miny) / cm->ysize;
    }

    /* do we have information to do anything */
    if( cm->xresolution == 0.0 || cm->yresolution == 0.0 || cm->xsize == 0 || cm->ysize == 0 ) {
      msSetError( MS_WCSERR, "Failed to collect extent and resolution for WCS coverage from metadata for layer '%s'.  Need value wcs|ows_resolution or wcs|ows_size values.", "msWCSGetCoverageMetadata20()", layer->name );
      return MS_FAILURE;
    }

    /* compute geotransform */
    cm->geotransform[0] = cm->extent.minx;
    cm->geotransform[1] = cm->xresolution;
    cm->geotransform[2] = 0.0;
    cm->geotransform[3] = cm->extent.maxy;
    cm->geotransform[4] = 0.0;
    cm->geotransform[5] = -fabs(cm->yresolution);

    /* get bands count, or assume 1 if not found */
    cm->numbands = 1;
    if( (value = msOWSLookupMetadata(&(layer->metadata), "CO", "bandcount")) != NULL) {
      int numbands = 0;
      msStringParseInteger(value, &numbands);
      cm->numbands = (size_t)numbands;
    }

    cm->bands = static_cast<wcs20rasterbandMetadataObj*>(msSmallCalloc(sizeof(wcs20rasterbandMetadataObj), cm->numbands));

    /* get bands type, or assume float if not found */
    cm->imagemode = MS_IMAGEMODE_FLOAT32;
    if( (value = msOWSLookupMetadata(&(layer->metadata), "CO", "imagemode")) != NULL ) {
      if( EQUAL(value,"INT16") )
        cm->imagemode = MS_IMAGEMODE_INT16;
      else if( EQUAL(value,"FLOAT32") )
        cm->imagemode = MS_IMAGEMODE_FLOAT32;
      else if( EQUAL(value,"BYTE") )
        cm->imagemode = MS_IMAGEMODE_BYTE;
      else {
        msSetError( MS_WCSERR, "Content of wcs|ows_imagemode (%s) not recognised.  Should be one of BYTE, INT16 or FLOAT32.", "msWCSGetCoverageMetadata20()", value );
        return MS_FAILURE;
      }
    }

    if( (value = msOWSLookupMetadata(&(layer->metadata), "CO", "native_format")) != NULL ) {
      cm->native_format = msStrdup(value);
    }

    /* determine nilvalues and reasons */
    {
      int n_nilvalues = 0, n_nilvalues_reasons = 0;
      char **t_nilvalues = NULL, **t_nilvalues_reasons = NULL;
      if( (value = msOWSLookupMetadata(&(layer->metadata), "CO", "nilvalues")) != NULL ) {
        t_nilvalues = msStringSplit(value, ' ', &n_nilvalues);
      } else if((value = msOWSLookupMetadata(&(layer->metadata), "CO", "rangeset_nullvalue")) != NULL) {
        t_nilvalues = msStringSplit(value, ' ', &n_nilvalues);
      }

      if( (value = msOWSLookupMetadata(&(layer->metadata), "CO", "nilvalues_reasons")) != NULL ) {
        t_nilvalues_reasons = msStringSplit(value, ' ', &n_nilvalues_reasons);
      }

      if(n_nilvalues > 0) {
        int i;
        cm->numnilvalues = n_nilvalues;
        cm->nilvalues = static_cast<char**>(msSmallCalloc(sizeof(char*), n_nilvalues));
        cm->nilvalues_reasons = static_cast<char**>(msSmallCalloc(sizeof(char*), n_nilvalues));
        for(i = 0; i < n_nilvalues; ++i) {
          cm->nilvalues[i] = msStrdup(t_nilvalues[i]);
          if(i < n_nilvalues_reasons) {
            cm->nilvalues_reasons[i] = msStrdup(t_nilvalues_reasons[i]);
          } else {
            cm->nilvalues_reasons[i] = msStrdup("");
          }
        }
      }
      msFreeCharArray(t_nilvalues, n_nilvalues);
      msFreeCharArray(t_nilvalues_reasons, n_nilvalues_reasons);
    }

    {
      int num_band_names = 0, j;
      char **band_names = NULL;

      const char *wcs11_band_names_key = "rangeset_axes";
      const char *wcs20_band_names_key = "band_names";

      const char *wcs11_interval_key = "interval";
      const char *wcs20_interval_key = "interval";
      const char *interval_key = NULL;

      const char *significant_figures_key = "significant_figures";

      const char *wcs11_keys[] =
      { "label", "semantic", "values_type", "values_semantic", "description" };
      const char *wcs20_keys[] =
      { "band_name", "band_interpretation", "band_uom", "band_definition", "band_description" };
      const char **keys = NULL;

      char **interval_array;
      int num_interval;

      wcs20rasterbandMetadataObj default_values;

      /* Decide whether WCS1.1 or WCS2.0 keys should be used */
      if( (value = msOWSLookupMetadata(&(layer->metadata), "CO", wcs20_band_names_key) ) != NULL ) {
        keys = wcs20_keys;
        interval_key = wcs20_interval_key;
        band_names = msStringSplit(value, ' ', &num_band_names);
      } else if( (value = msOWSLookupMetadata(&(layer->metadata), "CO", wcs11_band_names_key)) != NULL ) {
        keys = wcs11_keys;
        interval_key = wcs11_interval_key;
        /* "bands" has a special processing in WCS 1.0. See */
        /* msWCSSetDefaultBandsRangeSetInfo */
        if( EQUAL(value, "bands") )
        {
            num_band_names = cm->numbands;
            band_names = (char**) msSmallMalloc( sizeof(char*) * num_band_names );
            for( int i = 0; i < num_band_names; i++ )
            {
                char szName[30];
                snprintf(szName, sizeof(szName), "Band%d", i+1);
                band_names[i] = msStrdup(szName);
            }
        }
        else
        {
            /* WARNING: in WCS 1.x,, "rangeset_axes" has never been intended */
            /* to contain the list of band names... This code should probably */
            /* be removed */
            band_names = msStringSplit(value, ' ', &num_band_names);
        }
      }

      /* return with error when number of bands does not match    */
      /* the number of names                                      */
      if (static_cast<size_t>(num_band_names) != cm->numbands && num_band_names != 0) {
        msFreeCharArray(band_names, num_band_names);
        msSetError( MS_WCSERR,
                    "Wrong number of band names given in layer '%s'. "
                    "Expected %d, got %d.", "msWCSGetCoverageMetadata20()",
                    layer->name, (int)cm->numbands, num_band_names );
        return MS_FAILURE;
      }

      /* look up default metadata values */
      for(j = 1; j < 5; ++j) {
        if(keys != NULL) {
          default_values.values[j] = (char *)msOWSLookupMetadata(&(layer->metadata), "CO", keys[j]);
        }
      }

      /* set default values for interval and significant figures */
      switch(cm->imagemode) {
        case MS_IMAGEMODE_BYTE:
          default_values.interval_min = 0.;
          default_values.interval_max = 255.;
          default_values.significant_figures = 3;
          break;
        case MS_IMAGEMODE_INT16:
          default_values.interval_min = 0.;
          default_values.interval_max = (double)USHRT_MAX;
          default_values.significant_figures = 5;
          break;
        case MS_IMAGEMODE_FLOAT32:
          default_values.interval_min = -FLT_MAX;
          default_values.interval_max = FLT_MAX;
          default_values.significant_figures = 12;
          break;
        default:
          // other imagemodes are invalid (see above), just keep the compiler happy
          msFreeCharArray(band_names, num_band_names);
          return MS_FAILURE;
          break;
      }

      /* lookup default interval values */
      if (interval_key != NULL
          && (value = msOWSLookupMetadata(&(layer->metadata), "CO", interval_key)) != NULL) {
        interval_array = msStringSplit(value, ' ', &num_interval);

        if (num_interval != 2
            || msStringParseDouble(interval_array[0], &(default_values.interval_min)) != MS_SUCCESS
            || msStringParseDouble(interval_array[1], &(default_values.interval_max)) != MS_SUCCESS) {
          msFreeCharArray(band_names, num_band_names);
          msFreeCharArray(interval_array, num_interval);
          msSetError(MS_WCSERR, "Wrong interval format for default axis.",
                     "msWCSGetCoverageMetadata20()");
          return MS_FAILURE;
        }
        msFreeCharArray(interval_array, num_interval);
      }

      /* lookup default value for significant figures */
      if((value = msOWSLookupMetadata(&(layer->metadata), "CO", significant_figures_key)) != NULL) {
        if(msStringParseInteger(value, &(default_values.significant_figures)) != MS_SUCCESS) {
          msFreeCharArray(band_names, num_band_names);
          msSetError(MS_WCSERR, "Wrong significant figures format "
                     "for default axis.",
                     "msWCSGetCoverageMetadata20()");
          return MS_FAILURE;
        }
      }

      /* iterate over every band */
      for(unsigned i = 0; i < cm->numbands; ++i) {
        cm->bands[i].name = NULL;

        /* look up band metadata or copy defaults */
        if(num_band_names != 0) {
          cm->bands[i].name = msStrdup(band_names[i]);
          for(j = 1; j < 5; ++j) {
            value = (char *)msWCSLookupRangesetAxisMetadata20(&(layer->metadata),
                    cm->bands[i].name, keys[j]);
            if(value != NULL) {
              cm->bands[i].values[j] = msStrdup(value);
            } else if(default_values.values[j] != NULL) {
              cm->bands[i].values[j] = msStrdup(default_values.values[j]);
            }
          }
        }

        /* set up interval */
        value = (char *)msWCSLookupRangesetAxisMetadata20(&(layer->metadata),
                cm->bands[i].name, interval_key);
        if(value != NULL) {
          num_interval = 0;
          interval_array = msStringSplit(value, ' ', &num_interval);

          if (num_interval != 2
              || msStringParseDouble(interval_array[0], &(cm->bands[i].interval_min)) != MS_SUCCESS
              || msStringParseDouble(interval_array[1], &(cm->bands[i].interval_max)) != MS_SUCCESS) {
            msFreeCharArray(band_names, num_band_names);
            msFreeCharArray(interval_array, num_interval);
            msSetError(MS_WCSERR, "Wrong interval format for axis %s.",
                       "msWCSGetCoverageMetadata20()", cm->bands[i].name);
            return MS_FAILURE;
          }

          msFreeCharArray(interval_array, num_interval);
        } else {
          cm->bands[i].interval_min = default_values.interval_min;
          cm->bands[i].interval_max = default_values.interval_max;
        }

        /* set up significant figures */
        value = (char *)msWCSLookupRangesetAxisMetadata20(&(layer->metadata),
                cm->bands[i].name, significant_figures_key);
        if(value != NULL) {
          if(msStringParseInteger(value, &(cm->bands[i].significant_figures)) != MS_SUCCESS) {
            msFreeCharArray(band_names, num_band_names);
            msSetError(MS_WCSERR, "Wrong significant figures format "
                       "for axis %s.",
                       "msWCSGetCoverageMetadata20()", cm->bands[i].name);
            return MS_FAILURE;
          }
        } else {
          cm->bands[i].significant_figures = default_values.significant_figures;
        }
      }

      msFreeCharArray(band_names, num_band_names);
    }
  } else if( layer->data == NULL ) {
    /* no virtual metadata, not ok unless we're talking 1 image, hopefully we can fix that */
    msSetError( MS_WCSERR, "RASTER Layer with no DATA statement and no WCS virtual dataset metadata.  Tileindexed raster layers not supported for WCS without virtual dataset metadata (cm->extent, wcs_res, wcs_size).", "msWCSGetCoverageMetadata20()" );
    return MS_FAILURE;
  } else {
    /* work from the file (e.g. DATA) */
    GDALDatasetH hDS;
    GDALDriverH hDriver;
    GDALRasterBandH hBand;
    char szPath[MS_MAXPATHLEN];
    const char *driver_short_name, *driver_long_name;

    msGDALInitialize();

    msTryBuildPath3(szPath,  layer->map->mappath, layer->map->shapepath, layer->data);
    msAcquireLock( TLOCK_GDAL );
    {
        char** connectionoptions = msGetStringListFromHashTable(&(layer->connectionoptions));
        hDS = GDALOpenEx( szPath, GDAL_OF_RASTER, NULL,
                          (const char* const*)connectionoptions, NULL);
        CSLDestroy(connectionoptions);
    }
    if( hDS == NULL ) {
      msReleaseLock( TLOCK_GDAL );
      msSetError( MS_IOERR, "%s", "msWCSGetCoverageMetadata20()", CPLGetLastErrorMsg() );
      return MS_FAILURE;
    }

    msGetGDALGeoTransform( hDS, layer->map, layer, cm->geotransform );

    cm->xsize = GDALGetRasterXSize( hDS );
    cm->ysize = GDALGetRasterYSize( hDS );

    cm->extent.minx = cm->geotransform[0];
    cm->extent.maxx = cm->geotransform[0] + cm->geotransform[1] * cm->xsize + cm->geotransform[2] * cm->ysize;
    cm->extent.miny = cm->geotransform[3] + cm->geotransform[4] * cm->xsize + cm->geotransform[5] * cm->ysize;
    cm->extent.maxy = cm->geotransform[3];

    cm->xresolution = cm->geotransform[1];
    cm->yresolution = cm->geotransform[5];

    cm->numbands = GDALGetRasterCount( hDS );

    /* TODO nilvalues? */

    if( cm->numbands == 0 ) {
      msReleaseLock( TLOCK_GDAL );
      msSetError( MS_WCSERR, "Raster file %s has no raster bands.  This cannot be used in a layer.", "msWCSGetCoverageMetadata20()", layer->data );
      return MS_FAILURE;
    }

    hBand = GDALGetRasterBand( hDS, 1 );
    switch( GDALGetRasterDataType( hBand ) ) {
      case GDT_Byte:
        cm->imagemode = MS_IMAGEMODE_BYTE;
        break;
      case GDT_Int16:
        cm->imagemode = MS_IMAGEMODE_INT16;
        break;
      default:
        cm->imagemode = MS_IMAGEMODE_FLOAT32;
        break;
    }

    cm->bands = static_cast<wcs20rasterbandMetadataObj*>(msSmallCalloc(sizeof(wcs20rasterbandMetadataObj), cm->numbands));

    /* get as much band metadata as possible */
    for(unsigned i = 0; i < cm->numbands; ++i) {
      char bandname[32];
      GDALColorInterp colorInterp;
      hBand = GDALGetRasterBand(hDS, i + 1);
      snprintf(bandname, sizeof(bandname), "band%d", i+1);
      cm->bands[i].name = msStrdup(bandname);
      colorInterp = GDALGetRasterColorInterpretation(hBand);
      cm->bands[i].interpretation = msStrdup(GDALGetColorInterpretationName(colorInterp));
      cm->bands[i].uom = msStrdup(GDALGetRasterUnitType(hBand));
      if(strlen(cm->bands[i].uom) == 0) {
        msFree(cm->bands[i].uom);
        cm->bands[i].uom = NULL;
      }

      /* set up interval and significant figures */
      switch(cm->imagemode) {
        case MS_IMAGEMODE_BYTE:
        case MS_IMAGEMODE_PC256:
          cm->bands[i].interval_min = 0.;
          cm->bands[i].interval_max = 255.;
          cm->bands[i].significant_figures = 3;
          break;
        case MS_IMAGEMODE_INT16:
          cm->bands[i].interval_min = 0.;
          cm->bands[i].interval_max = (double)USHRT_MAX;
          cm->bands[i].significant_figures = 5;
          break;
        case MS_IMAGEMODE_FLOAT32:
          cm->bands[i].interval_min = -FLT_MAX;
          cm->bands[i].interval_max = FLT_MAX;
          cm->bands[i].significant_figures = 12;
          break;
      }
    }

    hDriver = GDALGetDatasetDriver(hDS);
    driver_short_name = GDALGetDriverShortName(hDriver);
    driver_long_name = GDALGetDriverLongName(hDriver);
    /* TODO: improve this, exchange strstr() */
    for(int i = 0; i < layer->map->numoutputformats; ++i) {
      if(strstr( layer->map->outputformatlist[i]->driver, driver_short_name) != NULL
          || strstr(layer->map->outputformatlist[i]->driver, driver_long_name) != NULL) {
        cm->native_format = msStrdup(layer->map->outputformatlist[i]->mimetype);
        break;
      }
    }

    GDALClose( hDS );
    msReleaseLock( TLOCK_GDAL );
  }

  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSClearCoverageMetadata20()                     */
/*                                                                      */
/*      Returns all dynamically allocated memory from a coverage meta-  */
/*      data object.                                                    */
/************************************************************************/

static int msWCSClearCoverageMetadata20(wcs20coverageMetadataObj *cm)
{
  msFree(cm->native_format);
  for(unsigned i = 0; i < cm->numnilvalues; ++i) {
    msFree(cm->nilvalues[i]);
    msFree(cm->nilvalues_reasons[i]);
  }
  msFree(cm->nilvalues);
  msFree(cm->nilvalues_reasons);

  for(unsigned i = 0; i < cm->numbands; ++i) {
    for(int j = 0; j < 5; ++j) {
      msFree(cm->bands[i].values[j]);
    }
  }
  msFree(cm->bands);
  msFree(cm->srs_epsg);
  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSException20()                                 */
/*                                                                      */
/*      Writes out an OWS compliant exception.                          */
/************************************************************************/

int msWCSException20(mapObj *map, const char *exceptionCode,
                     const char *locator, const char *version)
{
  int size = 0;
  const char *status = "400 Bad Request";
  char *errorString = NULL;
  char *schemasLocation = NULL;
  char *xsi_schemaLocation = NULL;
  char version_string[OWS_VERSION_MAXLEN];

  xmlDocPtr psDoc = NULL;
  xmlNodePtr psRootNode = NULL;
  xmlNodePtr psMainNode = NULL;
  xmlNsPtr psNsOws = NULL;
  xmlNsPtr psNsXsi = NULL;
  xmlChar *buffer = NULL;

  errorString = msGetErrorString("\n");
  schemasLocation = msEncodeHTMLEntities(msOWSGetSchemasLocation(map));

  psDoc = xmlNewDoc(BAD_CAST "1.0");

  psRootNode = xmlNewNode(NULL, BAD_CAST "ExceptionReport");
  psNsOws = xmlNewNs(psRootNode, BAD_CAST MS_OWSCOMMON_OWS_20_NAMESPACE_URI,
                     BAD_CAST MS_OWSCOMMON_OWS_NAMESPACE_PREFIX);
  xmlSetNs(psRootNode, psNsOws);

  psNsXsi = xmlNewNs(psRootNode, BAD_CAST MS_OWSCOMMON_W3C_XSI_NAMESPACE_URI, BAD_CAST MS_OWSCOMMON_W3C_XSI_NAMESPACE_PREFIX);

  /* add attributes to root element */
  xmlNewProp(psRootNode, BAD_CAST "version", BAD_CAST version);
  xmlNewProp(psRootNode, BAD_CAST "xml:lang", BAD_CAST msOWSGetLanguage(map, "exception"));

  /* get 2 digits version string: '2.0' */
  msOWSGetVersionString(OWS_2_0_0, version_string);
  version_string[3]= '\0';

  xsi_schemaLocation = msStrdup((char *)psNsOws->href);
  xsi_schemaLocation = msStringConcatenate(xsi_schemaLocation, " ");
  xsi_schemaLocation = msStringConcatenate(xsi_schemaLocation, (char *)schemasLocation);
  xsi_schemaLocation = msStringConcatenate(xsi_schemaLocation, "/ows/");
  xsi_schemaLocation = msStringConcatenate(xsi_schemaLocation, version_string);
  xsi_schemaLocation = msStringConcatenate(xsi_schemaLocation, "/owsExceptionReport.xsd");

  /* add namespace'd attributes to root element */
  xmlNewNsProp(psRootNode, psNsXsi, BAD_CAST "schemaLocation", BAD_CAST xsi_schemaLocation);

  /* add child element */
  psMainNode = xmlNewChild(psRootNode, NULL, BAD_CAST "Exception", NULL);

  /* add attributes to child */
  xmlNewProp(psMainNode, BAD_CAST "exceptionCode", BAD_CAST exceptionCode);

  if (locator != NULL) {
    xmlNewProp(psMainNode, BAD_CAST "locator", BAD_CAST locator);
  }

  if (errorString != NULL) {
    char* errorMessage = msEncodeHTMLEntities(errorString);
    xmlNewChild(psMainNode, NULL, BAD_CAST "ExceptionText", BAD_CAST errorMessage);
    msFree(errorMessage);
  }

  /*psRootNode = msOWSCommonExceptionReport(psNsOws, OWS_2_0_0,
          schemasLocation, version, msOWSGetLanguage(map, "exception"),
          exceptionCode, locator, errorMessage);*/

  xmlDocSetRootElement(psDoc, psRootNode);

  if (exceptionCode == NULL) {
    /* Do nothing */
  }
  else if(EQUAL(exceptionCode, "OperationNotSupported")
     || EQUAL(exceptionCode, "OptionNotSupported")) {
    status = "501 Not Implemented";
  }
  else if (EQUAL(exceptionCode, "NoSuchCoverage")
           || EQUAL(exceptionCode, "emptyCoverageIdList")
           || EQUAL(exceptionCode, "InvalidAxisLabel")
           || EQUAL(exceptionCode, "InvalidSubsetting")) {
    status = "404 Not Found";
  }

  msIO_setHeader("Status", "%s", status);
  msIO_setHeader("Content-Type","text/xml; charset=UTF-8");
  msIO_sendHeaders();

  xmlDocDumpFormatMemoryEnc(psDoc, &buffer, &size, "UTF-8", 1);

  msIO_printf("%s", buffer);

  /* free buffer and the document */
  free(errorString);
  free(schemasLocation);
  free(xsi_schemaLocation);
  xmlFree(buffer);
  xmlFreeDoc(psDoc);

  /* clear error since we have already reported it */
  msResetErrorList();
  return MS_FAILURE;
}

#define MAX_MIMES 20

static int msWCSGetCapabilities20_CreateProfiles(
  mapObj *map, xmlNodePtr psServiceIdentification, xmlNsPtr psOwsNs)
{
  xmlNodePtr psProfile, psTmpNode;
  char *available_mime_types[MAX_MIMES];
  int i = 0;

  /* even indices are urls, uneven are mime-types, or null*/
  const char * const urls_and_mime_types[] = {
    MS_WCS_20_PROFILE_CORE,     NULL,
    MS_WCS_20_PROFILE_KVP,      NULL,
    MS_WCS_20_PROFILE_POST,     NULL,
    MS_WCS_20_PROFILE_GML,      NULL,
    MS_WCS_20_PROFILE_GML_MULTIPART, NULL,
    MS_WCS_20_PROFILE_GML_SPECIAL, NULL,
    MS_WCS_20_PROFILE_GML_GEOTIFF, "image/tiff",
    MS_WCS_20_PROFILE_CRS,     NULL,
    MS_WCS_20_PROFILE_SCALING, NULL,
    MS_WCS_20_PROFILE_RANGESUBSET, NULL,
    MS_WCS_20_PROFILE_INTERPOLATION, NULL,
    NULL, NULL /* guardian */
  };

  /* navigate to node where profiles shall be inserted */
  for(psTmpNode = psServiceIdentification->children; psTmpNode->next != NULL; psTmpNode = psTmpNode->next) {
    if(EQUAL((char *)psTmpNode->name, "ServiceTypeVersion") && !EQUAL((char *)psTmpNode->next->name, "ServiceTypeVersion"))
      break;
  }

  /* get list of all available mime types */
  msGetOutputFormatMimeList(map, available_mime_types, MAX_MIMES);

  while(urls_and_mime_types[i] != NULL) {
    const char* mime_type = urls_and_mime_types[i+1];

    /* check if there is a mime type */
    if(mime_type != NULL) {
      /* check if the mime_type is in the list of outputformats */
      if(CSLPartialFindString(available_mime_types, mime_type) == -1)
        continue;
    }

    /* create a new node and attach it in the tree */
    psProfile = xmlNewNode(psOwsNs, BAD_CAST "Profile");
    xmlNodeSetContent(psProfile, BAD_CAST urls_and_mime_types[i]);
    xmlAddNextSibling(psTmpNode, psProfile);

    psTmpNode = psProfile;
    i += 2;
  }
  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSGetCapabilities20_CoverageSummary()           */
/*                                                                      */
/*      Helper function to create a short summary for a specific        */
/*      coverage.                                                       */
/************************************************************************/

static int msWCSGetCapabilities20_CoverageSummary(
  xmlDocPtr doc, xmlNodePtr psContents, layerObj *layer )
{
  wcs20coverageMetadataObj cm;
  int status;
  xmlNodePtr psCSummary;

  xmlNsPtr psWcsNs = xmlSearchNs( doc, xmlDocGetRootElement(doc), BAD_CAST "wcs" );

  status = msWCSGetCoverageMetadata20(layer, &cm);
  if(status != MS_SUCCESS) return MS_FAILURE;

  psCSummary = xmlNewChild( psContents, psWcsNs, BAD_CAST "CoverageSummary", NULL );
  xmlNewChild(psCSummary, psWcsNs, BAD_CAST "CoverageId", BAD_CAST layer->name);
  xmlNewChild(psCSummary, psWcsNs, BAD_CAST "CoverageSubtype", BAD_CAST "RectifiedGridCoverage");

  msWCS_11_20_PrintMetadataLinks(layer, doc, psCSummary);

  msWCSClearCoverageMetadata20(&cm);

  return MS_SUCCESS;
}


/************************************************************************/
/*                          msWCSAddInspireDSID20                       */
/************************************************************************/

static void msWCSAddInspireDSID20(mapObj *map,
                                  xmlNsPtr psNsInspireDls,
                                  xmlNsPtr psNsInspireCommon,
                                  xmlNodePtr pDlsExtendedCapabilities)
{
    const char* dsid_code = msOWSLookupMetadata(&(map->web.metadata), "CO", "inspire_dsid_code");
    const char* dsid_ns = msOWSLookupMetadata(&(map->web.metadata), "CO", "inspire_dsid_ns");
    if( dsid_code == NULL )
    {
        xmlAddChild(pDlsExtendedCapabilities, xmlNewComment(BAD_CAST "WARNING: Required metadata \"inspire_dsid_code\" missing"));
    }
    else
    {
        int ntokensCode = 0, ntokensNS = 0;
        char** tokensCode;
        char** tokensNS = NULL;
        int i;

        tokensCode = msStringSplit(dsid_code, ',', &ntokensCode);
        if( dsid_ns != NULL )
            tokensNS = msStringSplitComplex( dsid_ns, ",", &ntokensNS, MS_ALLOWEMPTYTOKENS);
        if( ntokensNS > 0 && ntokensNS != ntokensCode )
        {
            xmlAddChild(pDlsExtendedCapabilities,
                        xmlNewComment(BAD_CAST "WARNING: \"inspire_dsid_code\" and \"inspire_dsid_ns\" have not the same number of elements. Ignoring inspire_dsid_ns"));
            msFreeCharArray(tokensNS, ntokensNS);
            tokensNS = NULL;
            ntokensNS = 0;
        }
        for(i = 0; i<ntokensCode; i++ )
        {
            xmlNodePtr pSDSI = xmlNewNode(psNsInspireDls, BAD_CAST "SpatialDataSetIdentifier");
            xmlAddChild(pDlsExtendedCapabilities, pSDSI);
            xmlNewTextChild(pSDSI, psNsInspireCommon, BAD_CAST "Code", BAD_CAST tokensCode[i]);
            if( ntokensNS > 0 && tokensNS[i][0] != '\0' )
                xmlNewTextChild(pSDSI, psNsInspireCommon, BAD_CAST "Namespace", BAD_CAST tokensNS[i]);
        }
        msFreeCharArray(tokensCode, ntokensCode);
        msFreeCharArray(tokensNS, ntokensNS);
    }
}


/************************************************************************/
/*                   msWCSGetCapabilities20()                           */
/*                                                                      */
/*      Performs the GetCapabilities operation. Writes out a xml        */
/*      structure with server specific information and a summary        */
/*      of all available coverages.                                     */
/************************************************************************/

int msWCSGetCapabilities20(mapObj *map, cgiRequestObj *req,
                           wcs20ParamsObjPtr params, owsRequestObj *ows_request)
{
  xmlDocPtr psDoc = NULL;       /* document pointer */
  xmlNodePtr psRootNode,
             psOperationsNode,
             psNode;
  const char *updatesequence = NULL;
  xmlNsPtr psOwsNs = NULL,
           psXLinkNs = NULL,
           psWcsNs = NULL,
           psCrsNs = NULL,
           psIntNs = NULL;
  char *script_url=NULL, *script_url_encoded=NULL, *format_list=NULL;
  int i;
  xmlDocPtr pInspireTmpDoc = NULL;

  const char *inspire_capabilities = msOWSLookupMetadata(&(map->web.metadata), "CO", "inspire_capabilities");

  char *validated_language = msOWSLanguageNegotiation(map, "CO", params->accept_languages, CSLCount(params->accept_languages));

  /* -------------------------------------------------------------------- */
  /*      Create document.                                                */
  /* -------------------------------------------------------------------- */
  psDoc = xmlNewDoc(BAD_CAST "1.0");

  psRootNode = xmlNewNode(NULL, BAD_CAST "Capabilities");

  xmlDocSetRootElement(psDoc, psRootNode);

  /* -------------------------------------------------------------------- */
  /*      Name spaces                                                     */
  /* -------------------------------------------------------------------- */

  msWCSPrepareNamespaces20(psDoc, psRootNode, map, inspire_capabilities != NULL);

  /* lookup namespaces */
  psOwsNs = xmlSearchNs( psDoc, psRootNode, BAD_CAST MS_OWSCOMMON_OWS_NAMESPACE_PREFIX );
  psWcsNs = xmlSearchNs( psDoc, psRootNode, BAD_CAST MS_OWSCOMMON_WCS_NAMESPACE_PREFIX );
  xmlSearchNs( psDoc, psRootNode, BAD_CAST MS_OWSCOMMON_GML_NAMESPACE_PREFIX );
  psXLinkNs = xmlSearchNs( psDoc, psRootNode, BAD_CAST "xlink" );
  psCrsNs = xmlNewNs(psRootNode, BAD_CAST "http://www.opengis.net/wcs/crs/1.0", BAD_CAST "crs");
  psIntNs = xmlNewNs(psRootNode, BAD_CAST "http://www.opengis.net/wcs/interpolation/1.0", BAD_CAST "int");

  xmlSetNs(psRootNode, psWcsNs);

  xmlNewProp(psRootNode, BAD_CAST "version", BAD_CAST params->version );

  updatesequence = msOWSLookupMetadata(&(map->web.metadata), "CO", "updatesequence");
  if (params->updatesequence != NULL) {
    i = msOWSNegotiateUpdateSequence(params->updatesequence, updatesequence);
    if (i == 0) { /* current */
      msSetError(MS_WCSERR, "UPDATESEQUENCE parameter (%s) is equal to server (%s)",
                 "msWCSGetCapabilities20()", params->updatesequence, updatesequence);
      xmlFreeDoc(psDoc);
      return msWCSException(map, "CurrentUpdateSequence", "updatesequence",
                            params->version);
    }
    if (i > 0) { /* invalid */
      msSetError(MS_WCSERR, "UPDATESEQUENCE parameter (%s) is higher than server (%s)",
                 "msWCSGetCapabilities20()", params->updatesequence, updatesequence);
      xmlFreeDoc(psDoc);
      return msWCSException(map, "InvalidUpdateSequence", "updatesequence",
                            params->version);
    }
  }
  if(updatesequence != NULL) {
    xmlNewProp(psRootNode, BAD_CAST "updateSequence", BAD_CAST updatesequence);
  }

  /* -------------------------------------------------------------------- */
  /*      Service identification.                                         */
  /* -------------------------------------------------------------------- */
  if ( MS_WCS_20_CAPABILITIES_INCLUDE_SECTION(params, "ServiceIdentification") ) {
    psNode = xmlAddChild(psRootNode, msOWSCommonServiceIdentification(
                           psOwsNs, map, "OGC WCS", "2.0.1,1.1.1,1.0.0", "CO", validated_language));
    msWCSGetCapabilities20_CreateProfiles(map, psNode, psOwsNs);
  }

  /* Service Provider */
  if ( MS_WCS_20_CAPABILITIES_INCLUDE_SECTION(params, "ServiceProvider") ) {
    xmlAddChild(psRootNode,
                msOWSCommonServiceProvider(psOwsNs, psXLinkNs, map, "CO", validated_language));
  }

  /* -------------------------------------------------------------------- */
  /*      Operations metadata.                                            */
  /* -------------------------------------------------------------------- */
  if ( MS_WCS_20_CAPABILITIES_INCLUDE_SECTION(params, "OperationsMetadata") ) {
    if ((script_url = msOWSGetOnlineResource(map, "CO", "onlineresource", req)) == NULL
        || (script_url_encoded = msEncodeHTMLEntities(script_url)) == NULL) {
      xmlFreeDoc(psDoc);
      msSetError(MS_WCSERR, "Server URL not found", "msWCSGetCapabilities20()");
      return msWCSException(map, "NoApplicableCode", "mapserv", params->version);
    }
    free(script_url);

    psOperationsNode = xmlAddChild(psRootNode,msOWSCommonOperationsMetadata(psOwsNs));

    /* -------------------------------------------------------------------- */
    /*      GetCapabilities - add Sections and AcceptVersions?              */
    /* -------------------------------------------------------------------- */
    psNode = msOWSCommonOperationsMetadataOperation(
               psOwsNs, psXLinkNs,
               "GetCapabilities", OWS_METHOD_GETPOST, script_url_encoded);

    xmlAddChild(psNode->last->last->last,
                msOWSCommonOperationsMetadataDomainType(OWS_2_0_0, psOwsNs, "Constraint",
                    "PostEncoding", "XML"));
    xmlAddChild(psOperationsNode, psNode);

    /* -------------------------------------------------------------------- */
    /*      DescribeCoverage                                                */
    /* -------------------------------------------------------------------- */
    if (msOWSRequestIsEnabled(map, NULL, "C", "DescribeCoverage", MS_FALSE)) {
      psNode = msOWSCommonOperationsMetadataOperation(
                 psOwsNs, psXLinkNs,
                 "DescribeCoverage", OWS_METHOD_GETPOST, script_url_encoded);
      xmlAddChild(psNode->last->last->last,
                  msOWSCommonOperationsMetadataDomainType(OWS_2_0_0, psOwsNs, "Constraint",
                      "PostEncoding", "XML"));
      xmlAddChild(psOperationsNode, psNode);
    }

    /* -------------------------------------------------------------------- */
    /*      GetCoverage                                                     */
    /* -------------------------------------------------------------------- */
    if (msOWSRequestIsEnabled(map, NULL, "C", "GetCoverage", MS_FALSE)) {
      psNode = msOWSCommonOperationsMetadataOperation(
                 psOwsNs, psXLinkNs,
                 "GetCoverage", OWS_METHOD_GETPOST, script_url_encoded);

      xmlAddChild(psNode->last->last->last,
                  msOWSCommonOperationsMetadataDomainType(OWS_2_0_0, psOwsNs, "Constraint",
                      "PostEncoding", "XML"));
      xmlAddChild(psOperationsNode, psNode);
    }
    msFree(script_url_encoded);


    /* -------------------------------------------------------------------- */
    /*      Extended Capabilities for inspire                               */
    /* -------------------------------------------------------------------- */

    if (inspire_capabilities) {
      msIOContext* old_context;
      msIOContext* new_context;
      msIOBuffer* buffer;

      xmlNodePtr pRoot;
      xmlNodePtr pOWSExtendedCapabilities;
      xmlNodePtr pDlsExtendedCapabilities;
      xmlNodePtr pChild;

      xmlNsPtr psInspireCommonNs = xmlSearchNs( psDoc, psRootNode, BAD_CAST MS_INSPIRE_COMMON_NAMESPACE_PREFIX );
      xmlNsPtr psInspireDlsNs = xmlSearchNs( psDoc, psRootNode, BAD_CAST MS_INSPIRE_DLS_NAMESPACE_PREFIX );


      old_context = msIO_pushStdoutToBufferAndGetOldContext();
      msOWSPrintInspireCommonExtendedCapabilities(stdout, map, "CO", OWS_WARN,
                                                  "foo",
                                                  "xmlns:" MS_INSPIRE_COMMON_NAMESPACE_PREFIX "=\"" MS_INSPIRE_COMMON_NAMESPACE_URI "\" "
                                                  "xmlns:" MS_INSPIRE_DLS_NAMESPACE_PREFIX "=\"" MS_INSPIRE_DLS_NAMESPACE_URI "\" "
                                                  "xmlns:xsi=\"" MS_OWSCOMMON_W3C_XSI_NAMESPACE_URI "\"", validated_language, OWS_WCS);

      new_context = msIO_getHandler(stdout);
      buffer = (msIOBuffer *) new_context->cbData;

      /* Remove spaces between > and < to get properly indented result */
      msXMLStripIndentation( (char*) buffer->data );

      pInspireTmpDoc = xmlParseDoc((const xmlChar *)buffer->data);
      pRoot = xmlDocGetRootElement(pInspireTmpDoc);
      xmlReconciliateNs(psDoc, pRoot);

      pOWSExtendedCapabilities = xmlNewNode(psOwsNs, BAD_CAST "ExtendedCapabilities");
      xmlAddChild(psOperationsNode, pOWSExtendedCapabilities);

      pDlsExtendedCapabilities = xmlNewNode(psInspireDlsNs, BAD_CAST "ExtendedCapabilities");
      xmlAddChild(pOWSExtendedCapabilities, pDlsExtendedCapabilities);

      pChild = pRoot->children;
      while(pChild != NULL)
      {
          xmlNodePtr pNext = pChild->next;
          xmlUnlinkNode(pChild);
          xmlAddChild(pDlsExtendedCapabilities, pChild);
          pChild = pNext;
      }

      msWCSAddInspireDSID20(map, psInspireDlsNs, psInspireCommonNs, pDlsExtendedCapabilities);

      msIO_restoreOldStdoutContext(old_context);
    }
  }

  /* -------------------------------------------------------------------- */
  /*      Service metadata.                                               */
  /* -------------------------------------------------------------------- */

  if ( MS_WCS_20_CAPABILITIES_INCLUDE_SECTION(params, "ServiceMetadata") ) {
    xmlNodePtr psExtensionNode = NULL;
    psNode = xmlNewChild(psRootNode, psWcsNs, BAD_CAST "ServiceMetadata", NULL);

    /* Apply default formats */
    msApplyDefaultOutputFormats(map);

    /* Add formats list */
    format_list = msWCSGetFormatsList20(map, NULL);
    msLibXml2GenerateList(psNode, psWcsNs, "formatSupported", format_list, ',');
    msFree(format_list);

    psExtensionNode = xmlNewChild(psNode, psWcsNs, BAD_CAST "Extension", NULL);
    /* interpolations supported */
    {
      /* TODO: use URIs/URLs once they are specified */
      const char * const interpolation_methods[] = {"NEAREST", "AVERAGE", "BILINEAR"};
      int i;
      xmlNodePtr imNode = xmlNewChild(psExtensionNode, psIntNs,
                                      BAD_CAST "InterpolationMetadata", NULL);

      for (i=0; i < 3; ++i) {
        xmlNewChild(imNode, psIntNs, BAD_CAST "InterpolationSupported",
                    BAD_CAST interpolation_methods[i]);
      }
    }

    /* Report the supported CRSs */
    {
      char *crs_list = NULL;
      xmlNodePtr crsMetadataNode = xmlNewChild(psExtensionNode, psCrsNs,
                                               BAD_CAST "CrsMetadata", NULL);

      if((crs_list = msOWSGetProjURI(&(map->projection),
                                        &(map->web.metadata),
                                        "CO", MS_FALSE)) != NULL ) {
        msLibXml2GenerateList(crsMetadataNode, psCrsNs, "crsSupported",
                              crs_list, ' ');
        msFree(crs_list);
      } else {
        /* could not determine list of CRSs */
      }
    }
  }

  /* -------------------------------------------------------------------- */
  /*      Contents section.                                               */
  /* -------------------------------------------------------------------- */
  if ( MS_WCS_20_CAPABILITIES_INCLUDE_SECTION(params, "Contents") ) {
    psNode = xmlNewChild( psRootNode, psWcsNs, BAD_CAST "Contents", NULL );

    if(ows_request->numlayers == 0) {
      xmlAddChild(psNode,
                  xmlNewComment(BAD_CAST("WARNING: No WCS layers are enabled. "
                                         "Check wcs/ows_enable_request settings.")));
    } else {
      for(i = 0; i < map->numlayers; ++i) {
        layerObj *layer = map->layers[i];
        int       status;

        if(!msWCSIsLayerSupported(layer))
          continue;

        if (!msIntegerInArray(layer->index, ows_request->enabled_layers, ows_request->numlayers))
          continue;

        status = msWCSGetCapabilities20_CoverageSummary(
                   psDoc, psNode, layer );
        if(status != MS_SUCCESS) {
          msFree(validated_language);
          xmlFreeDoc(psDoc);
          xmlCleanupParser();
          return msWCSException(map, "Internal", "mapserv", params->version);
        }
      }
    }
  }
  /* -------------------------------------------------------------------- */
  /*      Write out the document and clean up.                            */
  /* -------------------------------------------------------------------- */
  msWCSWriteDocument20(psDoc);
  msFree(validated_language);
  xmlFreeDoc(psDoc);
  if( pInspireTmpDoc )
    xmlFreeDoc(pInspireTmpDoc);
  xmlCleanupParser();
  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSDescribeCoverage20_CoverageDescription()      */
/*                                                                      */
/*      Creates a description of a specific coverage with the three     */
/*      major sections: BoundedBy, DomainSet and RangeType.             */
/************************************************************************/

static int msWCSDescribeCoverage20_CoverageDescription(
    layerObj *layer, xmlDocPtr psDoc, xmlNodePtr psRootNode )
{
  int status, swapAxes;
  wcs20coverageMetadataObj cm;
  xmlNodePtr psCD;
  xmlNsPtr psWcsNs, psGmlNs, psGmlcovNs, psSweNs, psXLinkNs;
  psWcsNs = psGmlNs = psGmlcovNs = psSweNs = psXLinkNs = NULL;

  psWcsNs    = xmlSearchNs(psDoc, xmlDocGetRootElement(psDoc), BAD_CAST MS_OWSCOMMON_WCS_NAMESPACE_PREFIX);
  psGmlNs    = xmlSearchNs(psDoc, xmlDocGetRootElement(psDoc), BAD_CAST MS_OWSCOMMON_GML_NAMESPACE_PREFIX);
  psGmlcovNs = xmlSearchNs(psDoc, xmlDocGetRootElement(psDoc), BAD_CAST MS_OWSCOMMON_GMLCOV_NAMESPACE_PREFIX);
  psSweNs    = xmlSearchNs(psDoc, xmlDocGetRootElement(psDoc), BAD_CAST MS_OWSCOMMON_SWE_NAMESPACE_PREFIX);
  psXLinkNs  = xmlSearchNs(psDoc, xmlDocGetRootElement(psDoc), BAD_CAST MS_OWSCOMMON_W3C_XLINK_NAMESPACE_PREFIX);

  /* -------------------------------------------------------------------- */
  /*      Verify layer is processable.                                    */
  /* -------------------------------------------------------------------- */
  if( msCheckParentPointer(layer->map,"map") == MS_FAILURE )
    return MS_FAILURE;

  if(!msWCSIsLayerSupported(layer))
    return MS_SUCCESS;

  /* -------------------------------------------------------------------- */
  /*      Setup coverage metadata.                                        */
  /* -------------------------------------------------------------------- */
  status = msWCSGetCoverageMetadata20(layer, &cm);
  if(status != MS_SUCCESS)
    return status;

  swapAxes = msWCSSwapAxes20(cm.srs_uri);

  /* fill in bands rangeset info, if required. */
  /* msWCSSetDefaultBandsRangeSetInfo( NULL, &cm, layer ); */

  /* -------------------------------------------------------------------- */
  /*      Create CoverageDescription node.                                */
  /* -------------------------------------------------------------------- */
  psCD = xmlNewChild( psRootNode, psWcsNs, BAD_CAST "CoverageDescription", NULL );
  xmlNewNsProp(psCD, psGmlNs, BAD_CAST "id", BAD_CAST layer->name);

  /* -------------------------------------------------------------------- */
  /*      gml:boundedBy                                                   */
  /* -------------------------------------------------------------------- */
  msWCSCommon20_CreateBoundedBy(&cm, psGmlNs, psCD, &(layer->projection), swapAxes);

  xmlNewChild(psCD, psWcsNs, BAD_CAST "CoverageId", BAD_CAST layer->name);

  /* -------------------------------------------------------------------- */
  /*      gml:domainSet                                                   */
  /* -------------------------------------------------------------------- */
  msWCSCommon20_CreateDomainSet(layer, &cm, psGmlNs, psCD, &(layer->projection), swapAxes);

  /* -------------------------------------------------------------------- */
  /*      gmlcov:rangeType                                                */
  /* -------------------------------------------------------------------- */
  msWCSCommon20_CreateRangeType(&cm, NULL, psGmlcovNs, psSweNs, psCD);

  /* -------------------------------------------------------------------- */
  /*      wcs:ServiceParameters                                           */
  /* -------------------------------------------------------------------- */
  {
    xmlNodePtr psSP;

    psSP = xmlNewChild( psCD, psWcsNs, BAD_CAST "ServiceParameters", NULL);
    xmlNewChild(psSP, psWcsNs, BAD_CAST "CoverageSubtype", BAD_CAST "RectifiedGridCoverage");

    /* -------------------------------------------------------------------- */
    /*      SupportedCRS                                                    */
    /* -------------------------------------------------------------------- */
    /* for now, WCS 2.0 does not allow per coverage CRS definitions */
    /*{
      xmlNodePtr psSupportedCrss;
      char *owned_value;

      psSupportedCrss = xmlNewChild(psSP, psWcsNs,
                                    BAD_CAST "SupportedCRSs", NULL);

      if ((owned_value = msOWSGetProjURI(&(layer->projection),
                                         &(layer->metadata), "CO", MS_FALSE)) != NULL)
      { }
      else if ((owned_value = msOWSGetProjURI(&(layer->map->projection),
                                              &(layer->map->web.metadata), "CO", MS_FALSE)) != NULL)
      { }
      else {
        msDebug("missing required information, no SRSs defined.\n");
      }

      if (owned_value != NULL && strlen(owned_value) > 0) {
        msLibXml2GenerateList(psSupportedCrss, psWcsNs,
                              "SupportedCRS", owned_value, ' ');
      }

      xmlNewChild(psSupportedCrss, psWcsNs,
                  BAD_CAST "NativeCRS", BAD_CAST cm.srs_uri);

      msFree(owned_value);
    }*/

    /* -------------------------------------------------------------------- */
    /*      SupportedFormats                                                */
    /* -------------------------------------------------------------------- */
    /* for now, WCS 2.0 does not allow per coverage format definitions */
    /*{
      xmlNodePtr psSupportedFormats;
      char *format_list;

      psSupportedFormats =
      xmlNewChild(psSP, psWcsNs, BAD_CAST "SupportedFormats", NULL);

      format_list = msWCSGetFormatsList20(layer->map, layer);

      if (strlen(format_list) > 0) {
        msLibXml2GenerateList(psSupportedFormats, psWcsNs,
                              "SupportedFormat", format_list, ',');

      msFree(format_list);
    }*/


    /* -------------------------------------------------------------------- */
    /*      nativeFormat                                                    */
    /* -------------------------------------------------------------------- */
    xmlNewChild(psSP, psWcsNs,
                BAD_CAST "nativeFormat", BAD_CAST (cm.native_format ?
                    cm.native_format : ""));

    if (!cm.native_format) {
      msDebug("msWCSDescribeCoverage20_CoverageDescription(): "
              "No native format specified.\n");
    }
  }

  msWCSClearCoverageMetadata20(&cm);

  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSDescribeCoverage20()                          */
/*                                                                      */
/*      Implementation of the DescibeCoverage Operation. The result     */
/*      of this operation is a xml document, containing specific        */
/*      information about a coverage identified by an ID. The result    */
/*      is written on the stream.                                       */
/************************************************************************/

int msWCSDescribeCoverage20(mapObj *map, wcs20ParamsObjPtr params, owsRequestObj *ows_request)
{
  xmlDocPtr psDoc = NULL; /* document pointer */
  xmlNodePtr psRootNode;
  xmlNsPtr psWcsNs = NULL;
  int i, j;

  /* create DOM document and root node */
  psDoc = xmlNewDoc(BAD_CAST "1.0");
  psRootNode = xmlNewNode(NULL, BAD_CAST "CoverageDescriptions");
  xmlDocSetRootElement(psDoc, psRootNode);

  /* prepare initial namespace definitions */
  msWCSPrepareNamespaces20(psDoc, psRootNode, map, MS_FALSE);

  psWcsNs = xmlSearchNs(psDoc, psRootNode,
                        BAD_CAST MS_OWSCOMMON_WCS_NAMESPACE_PREFIX);
  xmlSetNs(psRootNode, psWcsNs);

  /* check if IDs are given */
  if (params->ids) {
    /* for each given ID in the ID-list */
    for (j = 0; params->ids[j]; j++) {
      i = msGetLayerIndex(map, params->ids[j]);
      if (i == -1 || (!msIntegerInArray(GET_LAYER(map, i)->index, ows_request->enabled_layers, ows_request->numlayers)) ) {
        msSetError(MS_WCSERR, "Unknown coverage: (%s)",
                   "msWCSDescribeCoverage20()", params->ids[j]);
        return msWCSException(map, "NoSuchCoverage", "coverage",
                              params->version);
      }
      /* create coverage description for the specified layer */
      if(msWCSDescribeCoverage20_CoverageDescription((GET_LAYER(map, i)),
          psDoc, psRootNode) == MS_FAILURE) {
        msSetError(MS_WCSERR, "Error retrieving coverage description.", "msWCSDescribeCoverage20()");
        return msWCSException(map, "MissingParameterValue", "coverage",
                              params->version);
      }
    }
  } else {
    /* Throw error, since IDs are mandatory */
    msSetError(MS_WCSERR, "Missing COVERAGEID parameter.", "msWCSDescribeCoverage20()");
    return msWCSException(map, "MissingParameterValue", "coverage",
                          params->version);
  }

  /* write out the DOM document to the stream */
  msWCSWriteDocument20(psDoc);

  /* cleanup */
  xmlFreeDoc(psDoc);
  xmlCleanupParser();

  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSGetCoverage_FinalizeParamsObj20()             */
/*                                                                      */
/*      Finalizes a wcs20ParamsObj for a GetCoverage operation. In the  */
/*      process, the params boundig box is adjusted to the subsets,     */
/*      width, height and resolution are determined and the subset crs  */
/*      is found out.                                                   */
/************************************************************************/

static int msWCSGetCoverage20_FinalizeParamsObj(wcs20ParamsObjPtr params, wcs20AxisObjPtr *axes)
{
  char *crs = NULL;
  int have_scale, have_size, have_resolution;
  int have_global_scale = (params->scale != MS_WCS20_UNBOUNDED) ? 1 : 0;

  if (axes[0] != NULL) {
    if(axes[0]->subset != NULL) {
      msDebug("Subset for X-axis found: %s\n", axes[0]->subset->axis);
      if (!axes[0]->subset->min.unbounded)
        params->bbox.minx = axes[0]->subset->min.scalar;
      if (!axes[0]->subset->max.unbounded)
        params->bbox.maxx = axes[0]->subset->max.scalar;
      crs = axes[0]->subset->crs;
    }
    params->width = axes[0]->size;
    params->resolutionX = axes[0]->resolution;
    params->scaleX = axes[0]->scale;

    if(axes[0]->resolutionUOM != NULL) {
      params->resolutionUnits = msStrdup(axes[0]->resolutionUOM);
    }

    have_scale = (params->scaleX != MS_WCS20_UNBOUNDED) ? 1 : 0;
    have_size = (params->width != 0) ? 1 : 0;
    have_resolution = (params->resolutionX != MS_WCS20_UNBOUNDED) ? 1 : 0;

    if ((have_global_scale + have_scale + have_size + have_resolution) > 1) {
      msSetError(MS_WCSERR, "Axis '%s' defines scale, size and/or resolution multiple times.",
                 "msWCSGetCoverage20_FinalizeParamsObj()", axes[0]->name);
      return MS_FAILURE;
    }
  }

  if (axes[1] != NULL) {
    if(axes[1]->subset != NULL) {
      msDebug("Subset for Y-axis found: %s\n", axes[1]->subset->axis);
      if (!axes[1]->subset->min.unbounded)
        params->bbox.miny = axes[1]->subset->min.scalar;
      if (!axes[1]->subset->max.unbounded)
        params->bbox.maxy = axes[1]->subset->max.scalar;
      if(crs != NULL && axes[0] != NULL && axes[0]->subset!= NULL) {
        if(!EQUAL(crs, axes[1]->subset->crs)) {
          msSetError(MS_WCSERR, "CRS for axis %s and axis %s are not the same.",
                     "msWCSGetCoverage20_FinalizeParamsObj()", axes[0]->name, axes[1]->name);
          return MS_FAILURE;
        }
      } else {
        crs = axes[1]->subset->crs;
      }
    }
    params->height = axes[1]->size;
    params->resolutionY = axes[1]->resolution;
    params->scaleY = axes[1]->scale;

    if(params->resolutionUnits == NULL && axes[1]->resolutionUOM != NULL) {
      params->resolutionUnits = msStrdup(axes[1]->resolutionUOM);
    } else if(params->resolutionUnits != NULL && axes[1]->resolutionUOM != NULL
              && !EQUAL(params->resolutionUnits, axes[1]->resolutionUOM)) {
      msSetError(MS_WCSERR, "The units of measure of the resolution for"
                 "axis %s and axis %s are not the same.",
                 "msWCSGetCoverage20_FinalizeParamsObj()", axes[0]->name, axes[1]->name);
      return MS_FAILURE;
    }

    have_scale = (params->scaleY != MS_WCS20_UNBOUNDED) ? 1 : 0;
    have_size = (params->height != 0) ? 1 : 0;
    have_resolution = (params->resolutionY != MS_WCS20_UNBOUNDED) ? 1 : 0;

    if ((have_global_scale + have_scale + have_size + have_resolution) > 1) {
      msSetError(MS_WCSERR, "Axis '%s' defines scale, size and/or resolution multiple times.",
                 "msWCSGetCoverage20_FinalizeParamsObj()", axes[1]->name);
      return MS_FAILURE;
    }
  }

  /* if scale is globally set, use this value instead */
  if (params->scale != MS_WCS20_UNBOUNDED) {
    params->scaleX = params->scaleY = params->scale;
  }

  /* check if projections are equal */
  if(crs != NULL) {
    if (params->subsetcrs && !EQUAL(crs, params->subsetcrs)) {
      /* already set and not equal -> raise exception */
      msSetError(MS_WCSERR, "SubsetCRS does not match the CRS of the axes: "
                 "'%s' != '%s'",
                 "msWCSCreateBoundingBox20()", params->subsetcrs, crs);
      return MS_FAILURE;
    }
    else if (params->subsetcrs) {
      /* equal, and already set -> do nothing */
    }
    else {
      /* not yet globally set -> set it */
      params->subsetcrs = msStrdup(crs);
    }
  } else if (!params->subsetcrs) {
    /* default to CRS of image */
    /* leave params->subsetcrs to null */
  }

  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSGetCoverage20_GetBands()                      */
/*                                                                      */
/*      Returns a string, containing a comma-separated list of band     */
/*      indices.                                                        */
/************************************************************************/

static int msWCSGetCoverage20_GetBands(layerObj *layer,
                                       wcs20ParamsObjPtr params, wcs20coverageMetadataObjPtr cm, char **bandlist)
{
  int maxlen, index;
  char *interval_stop;
  char **band_ids = NULL;

  /* if rangesubset parameter is not given, default to all bands */
  if(NULL == params->range_subset) {
    *bandlist = msStrdup("1");
    for(unsigned i = 1; i < cm->numbands; ++i) {
      char strnumber[12];
      snprintf(strnumber, sizeof(strnumber), ",%d", i + 1);
      *bandlist = msStringConcatenate(*bandlist, strnumber);
    }
    return MS_SUCCESS;
  }

  maxlen = cm->numbands * 4 * sizeof(char);
  *bandlist = static_cast<char*>( msSmallCalloc(sizeof(char), maxlen));

  /* Use WCS 2.0 metadata items in priority */
  {
    char* tmp = msOWSGetEncodeMetadata(&layer->metadata,
                               "CO", "band_names", NULL);

    if( NULL == tmp ) {
      /* Otherwise default to WCS 1.x*/
      tmp = msOWSGetEncodeMetadata(&layer->metadata,
                     "CO", "rangeset_axes", NULL);
      /* "bands" has a special processing in WCS 1.0. See */
      /* msWCSSetDefaultBandsRangeSetInfo */
      if( tmp != NULL && EQUAL(tmp, "bands") )
      {
        int num_band_names = cm->numbands;
        band_ids = (char**) msSmallCalloc( sizeof(char*), (num_band_names + 1) );
        for( int i = 0; i < num_band_names; i++ )
        {
            char szName[30];
            snprintf(szName, sizeof(szName), "Band%d", i+1);
            band_ids[i] = msStrdup(szName);
        }
      }
    }

    if(NULL != tmp && band_ids == NULL) {
      band_ids = CSLTokenizeString2(tmp, " ", 0);
    }
    msFree(tmp);
  }

  /* If we still don't have band names, use the band names from the coverage metadata */
  if (band_ids == NULL) {
    band_ids = (char**) CPLCalloc(sizeof(char*), (cm->numbands + 1));
    for (unsigned i = 0; i < cm->numbands; ++i) {
      band_ids[i] = CPLStrdup(cm->bands[i].name);
    }
  }

  /* Iterate over all supplied range */
  const int count = CSLCount(params->range_subset);
  for(int i = 0; i < count; ++i) {
    /* RangeInterval case: defined as "<start>:<stop>" */
    if ((interval_stop = strchr(params->range_subset[i], ':')) != NULL) {
      int start, stop;
      *interval_stop = '\0';
      ++interval_stop;

      /* check if the string represents an integer */
      if (msStringParseInteger(params->range_subset[i], &start) == MS_SUCCESS) {
      }
      /* get index of start clause, or raise an error if none was found */
      else if ((start = CSLFindString(band_ids, params->range_subset[i])) != -1) {
        start += 1; /* adjust index, since GDAL bands start with index 1 */
      }
      else {
        msSetError(MS_WCSERR, "'%s' is not a valid band identifier.",
               "msWCSGetCoverage20_GetBands()", params->range_subset[i]);
        return MS_FAILURE;
      }

      /* check if the string represents an integer */
      if (msStringParseInteger(interval_stop, &stop) == MS_SUCCESS) {
      }
      /* get index of stop clause, or raise an error if none was found */
      else if ((stop = CSLFindString(band_ids, interval_stop)) != -1) {
        stop += 1; /* adjust index, since GDAL bands start with index 1 */
      }
      else {
        msSetError(MS_WCSERR, "'%s' is not a valid band identifier.",
               "msWCSGetCoverage20_GetBands()", interval_stop);
        return MS_FAILURE;
      }

      /* Check whether or not start and stop are different and stop is higher than start */
      if (stop <= start) {
        msSetError(MS_WCSERR, "Invalid range interval given.",
               "msWCSGetCoverage20_GetBands()");
        return MS_FAILURE;
      } else if (start < 1 || static_cast<unsigned>(stop) > cm->numbands) {
        msSetError(MS_WCSERR, "Band interval is out of the valid range: 1-%d",
               "msWCSGetCoverage20_GetBands()", (int)cm->numbands);
        return MS_FAILURE;
      }

      /* expand the interval to a list of indices and push them on the list */
      for (int j = start; j <= stop; ++j) {
        char* tmp;
        if(i != 0 || j != start) {
          strlcat(*bandlist, ",", maxlen);
        }
        tmp = msIntToString(j);
        strlcat(*bandlist, tmp, maxlen);
        msFree(tmp);
      }
    }
    /* RangeComponent case */
    else {
        if(i != 0) {
        strlcat(*bandlist, ",", maxlen);
      }

      /* check if the string represents an integer */
      if(msStringParseInteger(params->range_subset[i], &index) == MS_SUCCESS) {
        char* tmp;
        if (index < 1 || static_cast<unsigned>(index) > cm->numbands) {
          msSetError(MS_WCSERR, "Band index is out of the valid range: 1-%d",
                 "msWCSGetCoverage20_GetBands()", (int)cm->numbands);
          return MS_FAILURE;
        }

        tmp = msIntToString((int)index);
        strlcat(*bandlist, tmp, maxlen);
        msFree(tmp);
        continue;
      }

      /* check if the string is equal to a band identifier    */
      /* if so, what is the index of the band                 */
      if((index = CSLFindString(band_ids, params->range_subset[i])) != -1) {
        char* tmp = msIntToString((int)index + 1);
        strlcat(*bandlist, tmp, maxlen);
        msFree(tmp);
      }
      else {
        msSetError(MS_WCSERR, "'%s' is not a valid band identifier.",
                   "msWCSGetCoverage20_GetBands()", params->range_subset[i]);
        return MS_FAILURE;
      }
    }
  }

  CSLDestroy(band_ids);
  return MS_SUCCESS;
}

/* Fixes CPLParseNameValue to allow ':' characters for namespaced keys */

static const char * fixedCPLParseNameValue(const char* string, char **key) {
  const char *value;
  size_t size;
  value = strchr(string, '=');

  if (value == NULL) {
    *key = NULL;
    return NULL;
  }

  size = value - string;
  *key = static_cast<char*>(msSmallMalloc(size + 1));
  strncpy(*key, string, size + 1);
  (*key)[size] = '\0';
  return value + 1;
}

/************************************************************************/
/*                   msWCSSetFormatParams20()                           */
/*                                                                      */
/*      Parses the given format options and sets the appropriate        */
/*      output format values.                                           */
/************************************************************************/

static int msWCSSetFormatParams20(outputFormatObj* format, char** format_options) {
  /* currently geotiff only */
  char *format_option;
  int i = 0;
  int is_geotiff = (format->mimetype && EQUAL(format->mimetype, "image/tiff"));

  if (!is_geotiff || !format_options) {
    /* Currently only geotiff available */
    return MS_SUCCESS;
  }

  format_option = format_options[0];
  while (format_option) {
    /* key, value */
    char *key;
    const char *value = fixedCPLParseNameValue(format_option, &key);

    if (!key) {
      msSetError(MS_WCSERR, "Could not deduct the option key.",
                 "msWCSSetFormatParams20()");
      return MS_FAILURE;
    }
    else if (!value) {
      msSetError(MS_WCSERR, "Missing value for parameter '%s'.",
                 "msWCSSetFormatParams20()", key);
      return MS_FAILURE;
    }

    if (EQUAL(key, "geotiff:compression") && is_geotiff) {
      /*COMPRESS=[JPEG/LZW/PACKBITS/DEFLATE/CCITTRLE/CCITTFAX3/CCITTFAX4/NONE]*/
      if (EQUAL(value, "None")) {
        msSetOutputFormatOption(format, "COMPRESS", "NONE");
      }
      else if (EQUAL(value, "PackBits")) {
        msSetOutputFormatOption(format, "COMPRESS", "PACKBITS");
      }
      else if (EQUAL(value, "Deflate")) {
        msSetOutputFormatOption(format, "COMPRESS", "DEFLATE");
      }
      else if (EQUAL(value, "Huffman")) {
        msSetOutputFormatOption(format, "COMPRESS", "CCITTRLE");
      }
      else if (EQUAL(value, "LZW")) {
        msSetOutputFormatOption(format, "COMPRESS", "LZW");
      }
      else if (EQUAL(value, "JPEG")) {
        msSetOutputFormatOption(format, "COMPRESS", "JPEG");
      }
      /* unsupported compression methods: CCITTFAX3/CCITTFAX4 */
      else {
        msSetError(MS_WCSERR, "Compression method '%s' not supported.",
                   "msWCSSetFormatParams20()", value);
        return MS_FAILURE;
      }
    }

    else if (EQUAL(key, "geotiff:jpeg_quality") && is_geotiff) {
      int quality;
      /* JPEG_QUALITY=[1-100] */
      if (msStringParseInteger(value, &quality) != MS_SUCCESS) {
        msSetError(MS_WCSERR, "Could not parse jpeg_quality value.",
                 "msWCSSetFormatParams20()");
        return MS_FAILURE;
      }
      else if (quality < 1 || quality > 100) {
        msSetError(MS_WCSERR, "Invalid jpeg_quality value '%d'.",
                   "msWCSSetFormatParams20()", quality);
        return MS_FAILURE;
      }
      msSetOutputFormatOption(format, "JPEG_QUALITY", value);
    }
    else if (EQUAL(key, "geotiff:predictor") && is_geotiff) {
      /* PREDICTOR=[None/Horizontal/FloatingPoint] */
      const char *predictor;
      if (EQUAL(value, "None") || EQUAL(value, "1")) {
        predictor = "1";
      }
      else if (EQUAL(value, "Horizontal") || EQUAL(value, "2")) {
        predictor = "2";
      }
      else if (EQUAL(value, "FloatingPoint") || EQUAL(value, "3")) {
        predictor = "3";
      }
      else {
        msSetError(MS_WCSERR, "Invalid predictor value '%s'.",
                   "msWCSSetFormatParams20()", value);
        return MS_FAILURE;
      }
      msSetOutputFormatOption(format, "PREDICTOR", predictor);
    }
    else if (EQUAL(key, "geotiff:interleave") && is_geotiff) {
      /* INTERLEAVE=[BAND,PIXEL] */
      if (EQUAL(value, "Band")) {
        msSetOutputFormatOption(format, "INTERLEAVE", "BAND");
      }
      else if (EQUAL(value, "Pixel")) {
        msSetOutputFormatOption(format, "INTERLEAVE", "PIXEL");
      }
      else {
        msSetError(MS_WCSERR, "Interleave method '%s' not supported.",
                   "msWCSSetFormatParams20()", value);
        return MS_FAILURE;
      }
    }
    else if (EQUAL(key, "geotiff:tiling") && is_geotiff) {
      /* TILED=YES */
      if (EQUAL(value, "true")) {
        msSetOutputFormatOption(format, "TILED", "YES");
      }
      else if (EQUAL(value, "false")) {
        msSetOutputFormatOption(format, "TILED", "NO");
      }
      else {
        msSetError(MS_WCSERR, "Invalid boolean value '%s'.",
                   "msWCSSetFormatParams20()", value);
        return MS_FAILURE;
      }
    }
    else if (EQUAL(key, "geotiff:tileheight") && is_geotiff) {
      /* BLOCKXSIZE=n */
      int tileheight;

      if (msStringParseInteger(value, &tileheight) != MS_SUCCESS) {
        msSetError(MS_WCSERR, "Could not parse tileheight value.",
                 "msWCSSetFormatParams20()");
        return MS_FAILURE;
      }
      else if (tileheight < 1 || tileheight % 16) {
        msSetError(MS_WCSERR, "Invalid tileheight value '%d'. "
                   "Must be greater than 0 and dividable by 16.",
                   "msWCSSetFormatParams20()", tileheight);
        return MS_FAILURE;
      }
      msSetOutputFormatOption(format, "BLOCKXSIZE", value);
    }
    else if (EQUAL(key, "geotiff:tilewidth") && is_geotiff) {
      /* BLOCKYSIZE=n */
      int tilewidth;

      if (msStringParseInteger(value, &tilewidth) != MS_SUCCESS) {
        msSetError(MS_WCSERR, "Could not parse tilewidth value.",
                 "msWCSSetFormatParams20()");
        return MS_FAILURE;
      }
      else if (tilewidth < 1 || tilewidth % 16) {
        msSetError(MS_WCSERR, "Invalid tilewidth value '%d'. "
                   "Must be greater than 0 and dividable by 16.",
                   "msWCSSetFormatParams20()", tilewidth);
        return MS_FAILURE;
      }
      msSetOutputFormatOption(format, "BLOCKYSIZE", value);
    }
    else if (EQUALN(key, "geotiff:", 8)) {
      msSetError(MS_WCSERR, "Unrecognized GeoTIFF parameter '%s'.",
                 "msWCSSetFormatParams20()", key);
      return MS_FAILURE;
    }

    msFree(key);

    /* fetch next option */
    format_option = format_options[++i];
  }

  return MS_SUCCESS;
}

/************************************************************************/
/*                   msWCSGetCoverage20()                               */
/*                                                                      */
/*      Implementation of the GetCoverage Operation. The coverage       */
/*      is either returned as an image or as a multipart xml/image.     */
/*      The result is written on the stream.                            */
/************************************************************************/

int msWCSGetCoverage20(mapObj *map, cgiRequestObj *request,
                       wcs20ParamsObjPtr params, owsRequestObj *ows_request)
{
  (void)request;
  layerObj *layer = NULL;
  wcs20coverageMetadataObj cm;
  imageObj *image = NULL;
  outputFormatObj *format = NULL;

  rectObj subsets, bbox;
  projectionObj imageProj;

  int status, i;
  double x_1, x_2, y_1, y_2;
  char *coverageName, *bandlist=NULL, numbands[12];

  int doDrawRasterLayerDraw = MS_TRUE;
  GDALDatasetH hDS = NULL;

  int widthFromComputationInImageCRS = 0;
  int heightFromComputationInImageCRS = 0;

  /* number of coverage ids should be 1 */
  if (params->ids == NULL || params->ids[0] == NULL) {
    msSetError(MS_WCSERR, "Required parameter CoverageID was not supplied.",
               "msWCSGetCoverage20()");
    return msWCSException(map, "MissingParameterValue", "coverage",
                          params->version);
  }
  if (params->ids[1] != NULL) {
    msSetError(MS_WCSERR, "GetCoverage operation supports only one coverage.",
               "msWCSGetCoverage20()");
    return msWCSException(map, "TooManyParameterValues", "coverage",
                          params->version);
  }

  /* find the right layer */
  layer = NULL;
  for(i = 0; i < map->numlayers; i++) {
    coverageName = msOWSGetEncodeMetadata(&(GET_LAYER(map, i)->metadata),
                                          "CO", "name",
                                          GET_LAYER(map, i)->name);
    if (EQUAL(coverageName, params->ids[0]) &&
        (msIntegerInArray(GET_LAYER(map, i)->index, ows_request->enabled_layers, ows_request->numlayers))) {
      layer = GET_LAYER(map, i);
      i = map->numlayers; /* to exit loop don't use break, we want to free resources first */
    }
    msFree(coverageName);
  }

  /* throw exception if no Layer was found */
  if (layer == NULL) {
    msSetError(MS_WCSERR,
               "COVERAGEID=%s not found, not in supported layer list. A layer might be disabled for \
this request. Check wcs/ows_enable_request settings.", "msWCSGetCoverage20()", params->ids[0]);
    return msWCSException(map, "NoSuchCoverage", "coverageid",
                          params->version);
  }
  /* retrieve coverage metadata  */
  status = msWCSGetCoverageMetadata20(layer, &cm);
  if (status != MS_SUCCESS) {
    msWCSClearCoverageMetadata20(&cm);
    return MS_FAILURE;
  }

  /* fill in bands rangeset info, if required.  */
  /* msWCSSetDefaultBandsRangeSetInfo(NULL, &cm, layer ); */

  /* set  resolution, size and maximum extent */
  layer->extent = map->extent = cm.extent;
  map->cellsize = cm.xresolution;
  map->width = cm.xsize;
  map->height = cm.ysize;

  /************************************************************************/
  /*      finalize the params object. determine subset crs and subset     */
  /*      bbox. Also project the image to the subset crs.                 */
  /************************************************************************/

  msInitProjection(&imageProj);
  msProjectionInheritContextFrom(&imageProj, &(layer->projection));
  if (msLoadProjectionString(&imageProj, cm.srs_epsg) == -1) {
    msFreeProjection(&imageProj);
    msWCSClearCoverageMetadata20(&cm);
    msSetError(MS_WCSERR,
               "Error loading CRS %s.",
               "msWCSGetCoverage20()", cm.srs_epsg);
    return msWCSException(map, "InvalidParameterValue",
                          "projection", params->version);
  }

  /* iterate over all subsets and check if they are valid*/
  for(i = 0; i < params->numaxes; ++i) {
    if(params->axes[i]->subset != NULL) {
      if(params->axes[i]->subset->timeOrScalar == MS_WCS20_TIME_VALUE) {
        msFreeProjection(&imageProj);
        msWCSClearCoverageMetadata20(&cm);
        msSetError(MS_WCSERR, "Time values for subsets are not supported. ",
                   "msWCSGetCoverage20()");
        return msWCSException(map, "InvalidSubsetting", "subset", params->version);
      }
      if(params->axes[i]->subset->operation == MS_WCS20_SLICE) {
        msFreeProjection(&imageProj);
        msWCSClearCoverageMetadata20(&cm);
        msSetError(MS_WCSERR, "Subset operation 'slice' is not supported.",
                   "msWCSGetCoverage20()");
        return msWCSException(map, "InvalidSubsetting", "subset", params->version);
      }
    }
  }

  {
    wcs20AxisObjPtr axes[2];
    if(msWCSValidateAndFindAxes20(params, axes) == MS_FAILURE) {
      msFreeProjection(&imageProj);
      msWCSClearCoverageMetadata20(&cm);
      return msWCSException(map, "InvalidAxisLabel", "subset", params->version);
    }
    if(msWCSGetCoverage20_FinalizeParamsObj(params, axes) == MS_FAILURE) {
      msFreeProjection(&imageProj);
      msWCSClearCoverageMetadata20(&cm);
      return msWCSException(map, "InvalidParameterValue", "extent", params->version);
    }
  }

  subsets = params->bbox;

  /* if no subsetCRS was specified use the coverages CRS 
     (Requirement 27 of the WCS 2.0 specification) */
  if (!params->subsetcrs) {
    params->subsetcrs = msStrdup(cm.srs_epsg);
  }

  if(EQUAL(params->subsetcrs, "imageCRS")) {
    /* subsets are in imageCRS; reproject them to real coordinates */
    rectObj orig_bbox = subsets;

    msFreeProjection(&(map->projection));
    map->projection = imageProj;

    if(subsets.minx != -DBL_MAX || subsets.maxx != DBL_MAX) {
      x_1 = cm.geotransform[0]
            + orig_bbox.minx * cm.geotransform[1]
            + orig_bbox.miny * cm.geotransform[2];
      // below +1 look suspicious to me (E. Rouault) and probably mean that
      // there are still issues with pixel-center vs pixel-corner convention
      // The resolution computed on wcs_20_post_getcov_trim_x_y_both_1px
      // with that is 11, whereas it should be 10
      x_2 = cm.geotransform[0]
            + (orig_bbox.maxx+1) * cm.geotransform[1]
            + (orig_bbox.maxy+1) * cm.geotransform[2];

      subsets.minx = MS_MIN(x_1, x_2);
      subsets.maxx = MS_MAX(x_1, x_2);
    }
    if(subsets.miny != -DBL_MAX || subsets.maxy != DBL_MAX) {
      // below +1 are suspicious. See bove comment in the minx/maxx cases
      y_1 = cm.geotransform[3]
            + (orig_bbox.maxx+1) * cm.geotransform[4]
            + (orig_bbox.maxy+1) * cm.geotransform[5];
      /*subsets.miny -= cm.geotransform[4]/2 + cm.geotransform[5]/2;*/
      y_2 = cm.geotransform[3]
            + orig_bbox.minx * cm.geotransform[4]
            + orig_bbox.miny * cm.geotransform[5];

      subsets.miny = MS_MIN(y_1, y_2);
      subsets.maxy = MS_MAX(y_1, y_2);
    }
  } else { /* if crs is not the 'imageCRS' */
    projectionObj subsetProj;

    /* if the subsets have a crs given, project the image extent to it */
    msInitProjection(&subsetProj);
    msProjectionInheritContextFrom(&subsetProj, &(layer->projection));
    if(msLoadProjectionString(&subsetProj, params->subsetcrs) != MS_SUCCESS) {
      msFreeProjection(&subsetProj);
      msFreeProjection(&imageProj);
      msWCSClearCoverageMetadata20(&cm);
      msSetError(MS_WCSERR,
                 "Error loading CRS %s.",
                 "msWCSGetCoverage20()", params->subsetcrs);
      return msWCSException(map, "InvalidParameterValue",
                            "projection", params->version);
    }

    if(msProjectionsDiffer(&imageProj, &subsetProj)) {

      /* Reprojection of source raster extent of (-180,-90,180,90) to any */
      /* projected CRS is going to exhibit strong anomalies. So instead */
      /* do the reverse, project the subset extent to the layer CRS, and */
      /* see how much the subset extent takes with respect to the source */
      /* raster extent. This is only used if output width and resolutionX (or */
      /* (height and resolutionY) are unknown. */
      if( ((params->width == 0 && params->resolutionX == MS_WCS20_UNBOUNDED) ||
           (params->height == 0 && params->resolutionY == MS_WCS20_UNBOUNDED)) &&
          (msProjIsGeographicCRS(&imageProj) &&
           !msProjIsGeographicCRS(&subsetProj) &&
           fabs(layer->extent.minx - -180.0) < 1e-5 &&
           fabs(layer->extent.miny - -90.0) < 1e-5 &&
           fabs(layer->extent.maxx - 180.0) < 1e-5 &&
           fabs(layer->extent.maxy - 90.0) < 1e-5) )
      {
          rectObj subsetInImageProj = subsets;
          if( msProjectRect(&subsetProj, &imageProj, &(subsetInImageProj)) == MS_SUCCESS )
          {
            subsetInImageProj.minx = MS_MAX(subsetInImageProj.minx, layer->extent.minx);
            subsetInImageProj.miny = MS_MAX(subsetInImageProj.miny, layer->extent.miny);
            subsetInImageProj.maxx = MS_MIN(subsetInImageProj.maxx, layer->extent.maxx);
            subsetInImageProj.maxy = MS_MIN(subsetInImageProj.maxy, layer->extent.maxy);
            {
                double total = ABS(layer->extent.maxx - layer->extent.minx);
                double part = ABS(subsetInImageProj.maxx - subsetInImageProj.minx);
                widthFromComputationInImageCRS = MS_NINT((part * map->width) / total);
            }
            {
                double total = ABS(layer->extent.maxy - layer->extent.miny);
                double part = ABS(subsetInImageProj.maxy - subsetInImageProj.miny);
                heightFromComputationInImageCRS = MS_NINT((part * map->height) / total);
            }
          }
      }

      msProjectRect(&imageProj, &subsetProj, &(layer->extent));
      map->extent = layer->extent;
      msFreeProjection(&(map->projection));
      map->projection = subsetProj;
      msFreeProjection(&imageProj);
    } else {
      msFreeProjection(&(map->projection));
      map->projection = imageProj;
      msFreeProjection(&subsetProj);
    }
  }

  /* create boundings of params subsets and image extent */
  if(msRectOverlap(&subsets, &(layer->extent)) == MS_FALSE) {
    /* extent and bbox do not overlap -> exit */
    msWCSClearCoverageMetadata20(&cm);
    msSetError(MS_WCSERR, "Image extent does not intersect with desired region.",
               "msWCSGetCoverage20()");
    return msWCSException(map, "ExtentError", "extent", params->version);
  }

  /* write combined bounding box */
  bbox.minx = MS_MAX(subsets.minx, map->extent.minx);
  bbox.miny = MS_MAX(subsets.miny, map->extent.miny);
  bbox.maxx = MS_MIN(subsets.maxx, map->extent.maxx);
  bbox.maxy = MS_MIN(subsets.maxy, map->extent.maxy);

  /* check if we are overspecified  */
  if ((params->width != 0 &&  params->resolutionX != MS_WCS20_UNBOUNDED)
      || (params->height != 0 && params->resolutionY != MS_WCS20_UNBOUNDED)) {
    msWCSClearCoverageMetadata20(&cm);
    msSetError(MS_WCSERR, "GetCoverage operation supports only one of SIZE or RESOLUTION per axis.",
               "msWCSGetCoverage20()");
    return msWCSException(map, "TooManyParameterValues", "coverage",
                          params->version);
  }

  /************************************************************************/
  /* check both axes: see if either size or resolution are given (and     */
  /* calculate the other value). If both are not given, calculate them    */
  /* from the bounding box.                                               */
  /************************************************************************/

  /* check x axis */
  if(params->width != 0) {
    /* TODO Unit Of Measure? */
    params->resolutionX = (bbox.maxx - bbox.minx) / params->width;
  } else if(params->resolutionX != MS_WCS20_UNBOUNDED) {
    params->width = MS_NINT((bbox.maxx - bbox.minx) / params->resolutionX);
  } else {
    if( widthFromComputationInImageCRS != 0 ) {
      params->width = widthFromComputationInImageCRS;
    } else if(ABS(bbox.maxx - bbox.minx) != ABS(map->extent.maxx - map->extent.minx)) {
      double total = ABS(map->extent.maxx - map->extent.minx),
             part = ABS(bbox.maxx - bbox.minx);
      params->width = MS_NINT((part * map->width) / total);
    } else {
      params->width = map->width;
    }

    params->resolutionX = (bbox.maxx - bbox.minx) / params->width;

    if (params->scaleX != MS_WCS20_UNBOUNDED) {
      params->resolutionX /= params->scaleX;
      params->width = (long)(((double) params->width) * params->scaleX);
    }
  }

  /* check y axis */
  if(params->height != 0) {
    params->resolutionY = (bbox.maxy - bbox.miny) / params->height;
  } else if(params->resolutionY != MS_WCS20_UNBOUNDED) {
    params->height = MS_NINT((bbox.maxy - bbox.miny) / params->resolutionY);
  } else {
    if( heightFromComputationInImageCRS != 0 ) {
      params->height = heightFromComputationInImageCRS;
    } else if(ABS(bbox.maxy - bbox.miny) != ABS(map->extent.maxy - map->extent.miny)) {
      double total = ABS(map->extent.maxy - map->extent.miny),
             part = ABS(bbox.maxy - bbox.miny);
      params->height = MS_NINT((part * map->height) / total);
    } else {
      params->height = map->height;
    }

    params->resolutionY = (bbox.maxy - bbox.miny) / params->height;

    if (params->scaleY != MS_WCS20_UNBOUNDED) {
      params->resolutionY /= params->scaleY;
      params->height = (long)(((double) params->height) * params->scaleY);
    }
  }

  /* WCS 2.0 is center of pixel oriented */
  rectObj bboxOriginIsCorner = bbox;
  bbox.minx += params->resolutionX * 0.5;
  bbox.maxx -= params->resolutionX * 0.5;
  bbox.miny += params->resolutionY * 0.5;
  bbox.maxy -= params->resolutionY * 0.5;

  /* if parameter 'outputcrs' is given, project the image to this crs */
  if(params->outputcrs != NULL) {
    projectionObj outputProj;

    msInitProjection(&outputProj);
    msProjectionInheritContextFrom(&outputProj, &(layer->projection));
    if(msLoadProjectionString(&outputProj, params->outputcrs) == -1) {
      msFreeProjection(&outputProj);
      msWCSClearCoverageMetadata20(&cm);
      return msWCSException(map, "InvalidParameterValue", "coverage",
                            params->version);
    }
    if(msProjectionsDiffer(&(map->projection), &outputProj)) {

      msDebug("msWCSGetCoverage20(): projecting to outputcrs %s\n", params->outputcrs);

      msProjectRect(&(map->projection), &outputProj, &bbox);

      /* recalculate resolutions, needed if UOM changes (e.g: deg -> m) */
      if( params->width == 1 || params->height == 1 )
      {
          // if one of the dimension is 1, we cannot use the center-of-pixel bbox,
          // but must use the origin-is-corner one.
          msProjectRect(&(map->projection), &outputProj, &bboxOriginIsCorner);
          params->resolutionX = (bboxOriginIsCorner.maxx - bboxOriginIsCorner.minx) / params->width;
          params->resolutionY = (bboxOriginIsCorner.maxy - bboxOriginIsCorner.miny) / params->height;
      }
      else
      {
          params->resolutionX = (bbox.maxx - bbox.minx) / (params->width - 1);
          params->resolutionY = (bbox.maxy - bbox.miny) / (params->height - 1);
      }

      msFreeProjection(&(map->projection));
      map->projection = outputProj;
    }
    else {
      msFreeProjection(&outputProj);
    }
  }

  /* set the bounding box as new map extent */
  map->extent = bbox;
  map->width = params->width;
  map->height = params->height;

  /* Are we exceeding the MAXSIZE limit on result size? */
  if(map->width > map->maxsize || map->height > map->maxsize ) {
    msWCSClearCoverageMetadata20(&cm);
    msSetError(MS_WCSERR, "Raster size out of range, width and height of "
               "resulting coverage must be no more than MAXSIZE=%d.",
               "msWCSGetCoverage20()", map->maxsize);

    return msWCSException(map, "InvalidParameterValue",
                          "size", params->version);
  }

  /* Mapserver only supports square cells */
  if (params->resolutionX <= params->resolutionY)
    map->cellsize = params->resolutionX;
  else
    map->cellsize = params->resolutionY;

  msDebug("msWCSGetCoverage20(): Set parameters from original"
          "data. Width: %d, height: %d, cellsize: %f, extent: %f,%f,%f,%f\n",
          map->width, map->height, map->cellsize, map->extent.minx,
          map->extent.miny, map->extent.maxx, map->extent.maxy);

  /**
   * Which format to use?
   *
   * 1) format parameter
   * 2) native format (from metadata) or GDAL format of the input dataset
   * 3) exception
   **/

  if (!params->format) {
    if (cm.native_format) {
      params->format = msStrdup(cm.native_format);
    }
  }

  if (!params->format) {
    msSetError(MS_WCSERR, "Output format could not be automatically determined. "
               "Use the FORMAT parameter to specify a format.",
               "msWCSGetCoverage20()");
    msWCSClearCoverageMetadata20(&cm);
    return msWCSException(map, "MissingParameterValue", "format",
                          params->version);
  }

  /*    make sure layer is on   */
  layer->status = MS_ON;

  if( params->width == 1 || params->height == 1 )
      msMapComputeGeotransformEx(map, params->resolutionX, params->resolutionY);
  else
      msMapComputeGeotransform(map);

  /*    fill in bands rangeset info, if required.  */
  /* msWCSSetDefaultBandsRangeSetInfo(params, &cm, layer); */
  /* msDebug("Bandcount: %d\n", cm.bandcount); */

  msApplyDefaultOutputFormats(map);

  if (msGetOutputFormatIndex(map, params->format) == -1) {
    msSetError(MS_WCSERR, "Unrecognized value '%s' for the FORMAT parameter.",
               "msWCSGetCoverage20()", params->format);
    msWCSClearCoverageMetadata20(&cm);
    return msWCSException(map, "InvalidParameterValue", "format",
                          params->version);
  }

  /* create a temporary outputformat (we likely will need to tweak parts) */
  format = msCloneOutputFormat(msSelectOutputFormat(map, params->format));
  msApplyOutputFormat(&(map->outputformat), format, MS_NOOVERRIDE);

  /* set format specific parameters */
  if (msWCSSetFormatParams20(format, params->format_options) != MS_SUCCESS) {
    msFree(bandlist);
    msWCSClearCoverageMetadata20(&cm);
    return msWCSException(map, "InvalidParameterValue", "format",
                          params->version);
  }

  if(msWCSGetCoverage20_GetBands(layer, params, &cm, &bandlist) != MS_SUCCESS) {
    msFree(bandlist);
    msWCSClearCoverageMetadata20(&cm);
    return msWCSException(map, "InvalidParameterValue", "rangesubset",
                          params->version);
  }
  msLayerSetProcessingKey(layer, "BANDS", bandlist);
  snprintf(numbands, sizeof(numbands), "%d", msCountChars(bandlist, ',')+1);
  msSetOutputFormatOption(map->outputformat, "BAND_COUNT", numbands);

  msWCSApplyLayerCreationOptions(layer, map->outputformat, bandlist);
  msWCSApplyLayerMetadataItemOptions(layer, map->outputformat, bandlist);

  /* check for the interpolation */
  /* Defaults to NEAREST */
  if(params->interpolation != NULL) {
    if(EQUALN(params->interpolation,"NEAREST",7)) {
      msLayerSetProcessingKey(layer, "RESAMPLE", "NEAREST");
    } else if(EQUAL(params->interpolation,"BILINEAR")) {
      msLayerSetProcessingKey(layer, "RESAMPLE", "BILINEAR");
    } else if(EQUAL(params->interpolation,"AVERAGE")) {
      msLayerSetProcessingKey(layer, "RESAMPLE", "AVERAGE");
    } else {
      msFree(bandlist);
      msSetError( MS_WCSERR, "'%s' specifies an unsupported interpolation method.",
                  "msWCSGetCoverage20()", params->interpolation );
      msWCSClearCoverageMetadata20(&cm);
      return msWCSException(map, "InvalidParameterValue", "interpolation", params->version);
    }
  } else {
    msLayerSetProcessingKey(layer, "RESAMPLE", "NEAREST");
  }

  /* since the dataset is only used in one layer, set it to be    */
  /* closed after drawing the layer. This normally defaults to    */
  /* DEFER and will produce a memory leak, because the dataset    */
  /* will not be closed.                                          */
  if( msLayerGetProcessingKey(layer, "CLOSE_CONNECTION") == NULL ) {
    msLayerSetProcessingKey(layer, "CLOSE_CONNECTION", "NORMAL");
  }

  if( layer->tileindex == NULL && layer->data != NULL &&
      strlen(layer->data) > 0 &&
      layer->connectiontype != MS_KERNELDENSITY )
  {
      if( msDrawRasterLayerLowCheckIfMustDraw(map, layer) )
      {
          char* decrypted_path = NULL;
          char szPath[MS_MAXPATHLEN];
          hDS = (GDALDatasetH)msDrawRasterLayerLowOpenDataset(
                                    map, layer, layer->data, szPath, &decrypted_path);
          msFree(decrypted_path);
          if( hDS )
          {
            msWCSApplyDatasetMetadataAsCreationOptions(layer, map->outputformat, bandlist, hDS);
            msWCSApplySourceDatasetMetadata(layer, map->outputformat, bandlist, hDS);
          }
      }
      else
      {
          doDrawRasterLayerDraw = MS_FALSE;
      }
  }

  /* create the image object  */
  if (MS_RENDERER_PLUGIN(map->outputformat)) {
    image = msImageCreate(map->width, map->height, map->outputformat,
                          map->web.imagepath, map->web.imageurl, map->resolution,
                          map->defresolution, &map->imagecolor);
  } else if (MS_RENDERER_RAWDATA(map->outputformat)) {
    image = msImageCreate(map->width, map->height, map->outputformat,
                          map->web.imagepath, map->web.imageurl, map->resolution,
                          map->defresolution, &map->imagecolor);
  } else {
    msFree(bandlist);
    msWCSClearCoverageMetadata20(&cm);
    msSetError(MS_WCSERR, "Map outputformat not supported for WCS!",
               "msWCSGetCoverage20()");
    msDrawRasterLayerLowCloseDataset(layer, hDS);
    return msWCSException(map, NULL, NULL, params->version);
  }

  if (image == NULL) {
    msFree(bandlist);
    msWCSClearCoverageMetadata20(&cm);
    msDrawRasterLayerLowCloseDataset(layer, hDS);
    return msWCSException(map, NULL, NULL, params->version);
  }

  if(layer->mask) {
    int maskLayerIdx = msGetLayerIndex(map,layer->mask);
    layerObj *maskLayer;
    outputFormatObj *altFormat;
    if(maskLayerIdx == -1) {
      msSetError(MS_MISCERR, "Layer (%s) references unknown mask layer (%s)", "msDrawLayer()",
                 layer->name,layer->mask);
      msFreeImage(image);
      msFree(bandlist);
      msWCSClearCoverageMetadata20(&cm);
      msDrawRasterLayerLowCloseDataset(layer, hDS);
      return msWCSException(map, NULL, NULL, params->version);
    }
    maskLayer = GET_LAYER(map, maskLayerIdx);
    if(!maskLayer->maskimage) {
      int i,retcode;
      int origstatus, origlabelcache;
      char *origImageType = msStrdup(map->imagetype);
      altFormat =  msSelectOutputFormat(map, "png24");
      msInitializeRendererVTable(altFormat);
      /* TODO: check the png24 format hasn't been tampered with, i.e. it's agg */
      maskLayer->maskimage= msImageCreate(image->width, image->height,altFormat,
                                          image->imagepath, image->imageurl, map->resolution, map->defresolution, NULL);
      if (!maskLayer->maskimage) {
        msSetError(MS_MISCERR, "Unable to initialize mask image.", "msDrawLayer()");
        msFreeImage(image);
        msFree(bandlist);
        msWCSClearCoverageMetadata20(&cm);
        msDrawRasterLayerLowCloseDataset(layer, hDS);
        return msWCSException(map, NULL, NULL, params->version);
      }

      /*
       * force the masked layer to status on, and turn off the labelcache so that
       * eventual labels are added to the temporary image instead of being added
       * to the labelcache
       */
      origstatus = maskLayer->status;
      origlabelcache = maskLayer->labelcache;
      maskLayer->status = MS_ON;
      maskLayer->labelcache = MS_OFF;

      /* draw the mask layer in the temporary image */
      retcode = msDrawLayer(map, maskLayer, maskLayer->maskimage);
      maskLayer->status = origstatus;
      maskLayer->labelcache = origlabelcache;
      if(retcode != MS_SUCCESS) {
        free(origImageType);
        msFreeImage(image);
        msFree(bandlist);
        msWCSClearCoverageMetadata20(&cm);
        msDrawRasterLayerLowCloseDataset(layer, hDS);
        return msWCSException(map, NULL, NULL, params->version);
      }
      /*
       * hack to work around bug #3834: if we have use an alternate renderer, the symbolset may contain
       * symbols that reference it. We want to remove those references before the altFormat is destroyed
       * to avoid a segfault and/or a leak, and so the the main renderer doesn't pick the cache up thinking
       * it's for him.
       */
      for(i=0; i<map->symbolset.numsymbols; i++) {
        if (map->symbolset.symbol[i]!=NULL) {
          symbolObj *s = map->symbolset.symbol[i];
          if(s->renderer == MS_IMAGE_RENDERER(maskLayer->maskimage)) {
            MS_IMAGE_RENDERER(maskLayer->maskimage)->freeSymbol(s);
            s->renderer = NULL;
          }
        }
      }
      /* set the imagetype from the original outputformat back (it was removed by msSelectOutputFormat() */
      msFree(map->imagetype);
      map->imagetype = origImageType;

    }
  }

  /* Actually produce the "grid". */
  if( MS_RENDERER_RAWDATA(map->outputformat) ) {
    if( doDrawRasterLayerDraw ) {
      status = msDrawRasterLayerLowWithDataset( map, layer, image, NULL, hDS );
    } else {
      status = MS_SUCCESS;
    }
  } else {
    rasterBufferObj rb;
    status = MS_IMAGE_RENDERER(image)->getRasterBufferHandle(image,&rb);
    if(MS_LIKELY(status == MS_SUCCESS)) {
      if( doDrawRasterLayerDraw ) {
        status = msDrawRasterLayerLowWithDataset( map, layer, image, &rb, hDS );
      } else {
        status = MS_SUCCESS;
      }
    }
  }

  msDrawRasterLayerLowCloseDataset(layer, hDS);

  if( status != MS_SUCCESS ) {
    msFree(bandlist);
    msFreeImage(image);
    msWCSClearCoverageMetadata20(&cm);
    return msWCSException(map, NULL, NULL, params->version );
  }

  /* GML+Image */
  /* Embed the image into multipart message */
  if(params->multipart == MS_TRUE) {
    xmlDocPtr psDoc = NULL;       /* document pointer */
    xmlNodePtr psRootNode, psRangeSet, psFile, psRangeParameters;
    xmlNsPtr psGmlNs = NULL,
             psGmlcovNs = NULL,
             psSweNs = NULL,
             psXLinkNs = NULL;
    wcs20coverageMetadataObj tmpCm;
    char *srs_uri, *default_filename;
    const char *filename;
    int swapAxes;

    /* Create Document  */
    psDoc = xmlNewDoc(BAD_CAST "1.0");
    psRootNode = xmlNewNode(NULL, BAD_CAST MS_WCS_GML_COVERAGETYPE_RECTIFIED_GRID_COVERAGE);
    xmlDocSetRootElement(psDoc, psRootNode);

    msWCSPrepareNamespaces20(psDoc, psRootNode, map, MS_FALSE);

    psGmlNs    = xmlSearchNs(psDoc, psRootNode, BAD_CAST MS_OWSCOMMON_GML_NAMESPACE_PREFIX);
    psGmlcovNs = xmlSearchNs(psDoc, psRootNode, BAD_CAST MS_OWSCOMMON_GMLCOV_NAMESPACE_PREFIX);
    psSweNs    = xmlSearchNs(psDoc, psRootNode, BAD_CAST MS_OWSCOMMON_SWE_NAMESPACE_PREFIX);
    xmlSearchNs(psDoc, psRootNode, BAD_CAST MS_OWSCOMMON_WCS_NAMESPACE_PREFIX);
    psXLinkNs  = xmlSearchNs(psDoc, psRootNode, BAD_CAST MS_OWSCOMMON_W3C_XLINK_NAMESPACE_PREFIX);

    xmlNewNsProp(psRootNode, psGmlNs, BAD_CAST "id", BAD_CAST layer->name);

    xmlSetNs(psRootNode, psGmlcovNs);

    srs_uri = msOWSGetProjURI(&map->projection, NULL, "CO", 1);

    tmpCm = cm;
    tmpCm.extent = map->extent;
    tmpCm.xsize = map->width;
    tmpCm.ysize = map->height;
    strlcpy(tmpCm.srs_uri, srs_uri, sizeof(tmpCm.srs_uri));

    tmpCm.xresolution = map->gt.geotransform[1];
    tmpCm.yresolution = map->gt.geotransform[5];

    tmpCm.extent.minx = MS_MIN(map->gt.geotransform[0], map->gt.geotransform[0] + map->width * tmpCm.xresolution);
    tmpCm.extent.miny = MS_MIN(map->gt.geotransform[3], map->gt.geotransform[3] + map->height * tmpCm.yresolution);
    tmpCm.extent.maxx = MS_MAX(map->gt.geotransform[0], map->gt.geotransform[0] + map->width * tmpCm.xresolution);
    tmpCm.extent.maxy = MS_MAX(map->gt.geotransform[3], map->gt.geotransform[3] + map->height * tmpCm.yresolution);

    swapAxes = msWCSSwapAxes20(srs_uri);
    msFree(srs_uri);

    /* Setup layer information  */
    msWCSCommon20_CreateBoundedBy(&tmpCm, psGmlNs, psRootNode, &(map->projection), swapAxes);
    msWCSCommon20_CreateDomainSet(layer, &tmpCm, psGmlNs, psRootNode, &(map->projection), swapAxes);

    psRangeSet = xmlNewChild(psRootNode, psGmlNs, BAD_CAST "rangeSet", NULL);
    psFile     = xmlNewChild(psRangeSet, psGmlNs, BAD_CAST "File", NULL);

    /* TODO: wait for updated specifications */
    psRangeParameters = xmlNewChild(psFile, psGmlNs, BAD_CAST "rangeParameters", NULL);

    default_filename = msStrdup("out.");
    default_filename = msStringConcatenate(default_filename, MS_IMAGE_EXTENSION(image->format));

    filename = msGetOutputFormatOption(image->format, "FILENAME", default_filename);
    std::string file_ref("cid:coverage/");
    file_ref += filename;

    msFree(default_filename);

    std::string role;
    if(EQUAL(MS_IMAGE_MIME_TYPE(map->outputformat), "image/tiff")) {
      role = MS_WCS_20_PROFILE_GML_GEOTIFF;
    } else {
      role = MS_IMAGE_MIME_TYPE(map->outputformat);
    }

    xmlNewNsProp(psRangeParameters, psXLinkNs, BAD_CAST "href", BAD_CAST file_ref.c_str());
    xmlNewNsProp(psRangeParameters, psXLinkNs, BAD_CAST "role", BAD_CAST role.c_str());
    xmlNewNsProp(psRangeParameters, psXLinkNs, BAD_CAST "arcrole", BAD_CAST "fileReference");

    xmlNewChild(psFile, psGmlNs, BAD_CAST "fileReference", BAD_CAST file_ref.c_str());
    xmlNewChild(psFile, psGmlNs, BAD_CAST "fileStructure", NULL);
    xmlNewChild(psFile, psGmlNs, BAD_CAST "mimeType", BAD_CAST MS_IMAGE_MIME_TYPE(map->outputformat));

    msWCSCommon20_CreateRangeType(&cm, bandlist, psGmlcovNs, psSweNs, psRootNode);

    msIO_setHeader("Content-Type","multipart/related; boundary=wcs");
    msIO_sendHeaders();
    msIO_printf("\r\n--wcs\r\n");

    msWCSWriteDocument20(psDoc);
    msWCSWriteFile20(map, image, params, 1);

    xmlFreeDoc(psDoc);
    xmlCleanupParser();
  /* just print out the file without gml */
  } else {
    msWCSWriteFile20(map, image, params, 0);
  }

  msFree(bandlist);
  msWCSClearCoverageMetadata20(&cm);
  msFreeImage(image);
  return MS_SUCCESS;
}

#endif /* defined(USE_LIBXML2) */

#endif /* defined(USE_WCS_SVR) */
