/*****************************************************************************
 * $Id$
 *
 * Project:  MapServer
 * Purpose:  Implementation of WMS CONNECTIONTYPE - client to WMS servers
 * Author:   Daniel Morissette, DM Solutions Group (morissette@dmsolutions.ca)
 *
 *****************************************************************************
 * Copyright (c) 2001-2004, Daniel Morissette, DM Solutions Group Inc
 *
 * 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.h"
#include "maperror.h"
#include "mapogcsld.h"
#include "mapows.h"

#include <time.h>
#include <ctype.h>

#if defined(_WIN32) && !defined(__CYGWIN__)
#include <process.h>
#include <stdio.h>
#endif

#ifdef USE_GDAL
#  include "cpl_vsi.h"
#endif

void CleanVSIDir( const char *pszDir );

/**********************************************************************
 *                          msInitWmsParamsObj()
 *
 **********************************************************************/
int msInitWmsParamsObj(wmsParamsObj *wmsparams)
{
  wmsparams->onlineresource = NULL;
  wmsparams->params = msCreateHashTable();
  wmsparams->numparams=0;
  wmsparams->httpcookiedata = NULL;

  return MS_SUCCESS;
}

/**********************************************************************
 *                          msFreeWmsParamsObj()
 *
 * Frees the contents of the object, but not the object itself.
 **********************************************************************/
void msFreeWmsParamsObj(wmsParamsObj *wmsparams)
{
  msFree(wmsparams->onlineresource);
  wmsparams->onlineresource = NULL;

  msFreeHashTable(wmsparams->params);
  wmsparams->params = NULL;

  msFree(wmsparams->httpcookiedata);

  wmsparams->numparams=0;
}

/**********************************************************************
 *                          msSetWMSParamString()
 *
 **********************************************************************/

#ifdef USE_WMS_LYR
static int msSetWMSParamString(wmsParamsObj *psWMSParams,
                               const char *name, const char * value,
                               int urlencode)
{
  if (urlencode) {
    char *pszTmp;

    /*
     *  Special case handling for characters the WMS specification
     *  says should not be encoded, when they occur in certain
     *  parameters.
     *
     *  TODO: WMS 1.3 removes SRS and FORMAT from the set of
     *        exceptional cases.
     */
    if( strcmp(name,"LAYERS") == 0 ||
        strcmp(name,"STYLES") == 0 ||
        strcmp(name,"BBOX") == 0 ) {
      pszTmp = msEncodeUrlExcept(value,',');
    } else if ( strcmp(name,"SRS") == 0 ) {
      pszTmp = msEncodeUrlExcept(value,':');
    } else if ( strcmp(name,"FORMAT") == 0 ) {
      pszTmp = msEncodeUrlExcept(value,'/');
    } else {
      pszTmp = msEncodeUrl(value);
    }

    msInsertHashTable(psWMSParams->params, name, pszTmp);
    msFree(pszTmp);
  } else {
    msInsertHashTable(psWMSParams->params, name, value);
  }
  psWMSParams->numparams++;

  return MS_SUCCESS;
}
#endif /* def USE_WMS_LYR */

/**********************************************************************
 *                          msSetWMSParamInt()
 *
 **********************************************************************/

#ifdef USE_WMS_LYR
static int msSetWMSParamInt(wmsParamsObj *wmsparams,
                            const char *name, int value)
{
  char szBuf[100];

  snprintf(szBuf, sizeof(szBuf), "%d", value);
  msInsertHashTable(wmsparams->params, name, szBuf);
  wmsparams->numparams++;

  return MS_SUCCESS;
}
#endif /* def USE_WMS_LYR */

/**********************************************************************
 *                          msBuildWMSParamsUrl()
 *
 **********************************************************************/
static char *msBuildURLFromWMSParams(wmsParamsObj *wmsparams)
{
  const char *key, *value;
  size_t bufferSize = 0;
  int nLen;
  char *pszURL;

  /* Compute size required for URL buffer
   */
  nLen = strlen(wmsparams->onlineresource) + 3;

  key = msFirstKeyFromHashTable(wmsparams->params);
  while (key != NULL) {
    value = msLookupHashTable(wmsparams->params, key);
    nLen += strlen(key) + strlen(value) + 2;

    key = msNextKeyFromHashTable(wmsparams->params, key);
  }

  bufferSize = nLen+1;
  pszURL = (char*)msSmallMalloc(bufferSize);

  /* Start with the onlineresource value and append trailing '?' or '&'
   * if missing.
   */
  strcpy(pszURL, wmsparams->onlineresource);
  if (strchr(pszURL, '?') == NULL)
    strcat(pszURL, "?");
  else {
    char *c;
    c = pszURL+strlen(pszURL)-1;
    if (*c != '?' && *c != '&')
      strcpy(c+1, "&");
  }

  /* Now add all the parameters
   */
  nLen = strlen(pszURL);
  key = msFirstKeyFromHashTable(wmsparams->params);
  while (key != NULL) {
    value = msLookupHashTable(wmsparams->params, key);
    snprintf(pszURL+nLen, bufferSize-nLen, "%s=%s&", key, value);
    nLen += strlen(key) + strlen(value) + 2;
    key = msNextKeyFromHashTable(wmsparams->params, key);
  }

  /* Get rid of trailing '&'*/
  pszURL[nLen-1] = '\0';


  return pszURL;
}




#ifdef USE_WMS_LYR
/**********************************************************************
 *                          msBuildWMSLayerURLBase()
 *
 * Build the base of a GetMap or GetFeatureInfo URL using metadata.
 * The parameters to set are:
 *   VERSION
 *   LAYERS
 *   FORMAT
 *   TRANSPARENT
 *   STYLES
 *   QUERY_LAYERS (for queriable layers only)
 *
 * Returns a reference to a newly allocated string that should be freed
 * by the caller.
 **********************************************************************/
static int msBuildWMSLayerURLBase(mapObj *map, layerObj *lp,
                                  wmsParamsObj *psWMSParams)
{
  const char *pszOnlineResource, *pszVersion, *pszName, *pszFormat;
  const char *pszFormatList, *pszStyle, /* *pszStyleList,*/ *pszTime;
  const char *pszBgColor, *pszTransparent;
  const char *pszSLD=NULL, *pszStyleSLDBody=NULL, *pszVersionKeyword=NULL;
  const char *pszSLDBody=NULL, *pszSLDURL = NULL;
  char *pszSLDGenerated = NULL;

  /* If lp->connection is not set then use wms_onlineresource metadata */
  pszOnlineResource = lp->connection;
  if (pszOnlineResource == NULL)
    pszOnlineResource = msOWSLookupMetadata(&(lp->metadata),
                                            "MO", "onlineresource");

  pszVersion =        msOWSLookupMetadata(&(lp->metadata), "MO", "server_version");
  pszName =           msOWSLookupMetadata(&(lp->metadata), "MO", "name");
  pszFormat =         msOWSLookupMetadata(&(lp->metadata), "MO", "format");
  pszFormatList =     msOWSLookupMetadata(&(lp->metadata), "MO", "formatlist");
  pszStyle =          msOWSLookupMetadata(&(lp->metadata), "MO", "style");
  /*pszStyleList =      msOWSLookupMetadata(&(lp->metadata), "MO", "stylelist");*/
  pszTime =           msOWSLookupMetadata(&(lp->metadata), "MO", "time");
  pszSLDBody =        msOWSLookupMetadata(&(lp->metadata), "MO", "sld_body");
  pszSLDURL =         msOWSLookupMetadata(&(lp->metadata), "MO", "sld_url");
  pszBgColor =        msOWSLookupMetadata(&(lp->metadata), "MO", "bgcolor");
  pszTransparent =    msOWSLookupMetadata(&(lp->metadata), "MO", "transparent");

  if (pszOnlineResource==NULL || pszVersion==NULL || pszName==NULL) {
    msSetError(MS_WMSCONNERR,
               "One of wms_onlineresource, wms_server_version, wms_name "
               "metadata is missing in layer %s.  "
               "Please either provide a valid CONNECTION URL, or provide "
               "those values in the layer's metadata.\n",
               "msBuildWMSLayerURLBase()", lp->name);
    return MS_FAILURE;
  }

  psWMSParams->onlineresource = msStrdup(pszOnlineResource);

  if (strncmp(pszVersion, "1.0.7", 5) < 0)
    pszVersionKeyword = "WMTVER";
  else
    pszVersionKeyword = "VERSION";

  msSetWMSParamString(psWMSParams, pszVersionKeyword, pszVersion, MS_FALSE);
  msSetWMSParamString(psWMSParams, "SERVICE", "WMS",     MS_FALSE);
  msSetWMSParamString(psWMSParams, "LAYERS",  pszName,   MS_TRUE);

  if (pszFormat==NULL && pszFormatList==NULL) {
    msSetError(MS_WMSCONNERR,
               "At least wms_format or wms_formatlist is required for "
               "layer %s.  "
               "Please either provide a valid CONNECTION URL, or provide "
               "those values in the layer's metadata.\n",
               "msBuildWMSLayerURLBase()", lp->name);
    return MS_FAILURE;
  }

  if (pszFormat != NULL) {
    msSetWMSParamString(psWMSParams, "FORMAT",  pszFormat, MS_TRUE);
  } else {
    /* Look for the first format in list that matches */
    char **papszTok;
    int i, n;
    papszTok = msStringSplit(pszFormatList, ',', &n);

    for(i=0; pszFormat==NULL && i<n; i++) {
      if (0
#if defined USE_PNG
          || strcasecmp(papszTok[i], "PNG")
          || strcasecmp(papszTok[i], "image/png")
#endif
#if defined USE_JPEG
          || strcasecmp(papszTok[i], "JPEG")
          || strcasecmp(papszTok[i], "image/jpeg")
#endif
         ) {
        pszFormat = papszTok[i];
      }
    }

    if (pszFormat) {
      msSetWMSParamString(psWMSParams, "FORMAT",  pszFormat, MS_TRUE);
      msFreeCharArray(papszTok, n);
    } else {
      msSetError(MS_WMSCONNERR,
                 "Could not find a format that matches supported input "
                 "formats in wms_formatlist metdata in layer %s.  "
                 "Please either provide a valid CONNECTION URL, or "
                 "provide the required layer metadata.\n",
                 "msBuildWMSLayerURLBase()", lp->name);
      msFreeCharArray(papszTok, n);
      return MS_FAILURE;
    }
  }

  if (pszStyle==NULL) {
    /* When no style is selected, use "" which is a valid default. */
    pszStyle = "";
  } else {
    /* Was a wms_style_..._sld URL provided? */
    char szBuf[100];
    snprintf(szBuf, sizeof(szBuf), "style_%.80s_sld", pszStyle);
    pszSLD = msOWSLookupMetadata(&(lp->metadata), "MO", szBuf);
    snprintf(szBuf, sizeof(szBuf), "style_%.80s_sld_body", pszStyle);
    pszStyleSLDBody = msOWSLookupMetadata(&(lp->metadata), "MO", szBuf);

    if (pszSLD || pszStyleSLDBody) {
      /* SLD URL is set.  If this defn. came from a map context then */
      /* the style name may just be an internal name: "Style{%d}" if */
      /* that's the case then we should not pass this name via the URL */
      if (strncmp(pszStyle, "Style{", 6) == 0)
        pszStyle = "";
    }
  }

  /*  set STYLES no matter what, even if it's empty (i.e. "STYLES=")
   *  styles is a required param of WMS
   */

  msSetWMSParamString(psWMSParams, "STYLES", pszStyle, MS_TRUE);

  if (pszSLD != NULL) {
    /* Only SLD is set */
    msSetWMSParamString(psWMSParams, "SLD",    pszSLD,   MS_TRUE);
  } else if (pszStyleSLDBody != NULL) {
    /* SLDBODY are set */
    msSetWMSParamString(psWMSParams, "SLD_BODY", pszStyleSLDBody, MS_TRUE);
  }

  if (msIsLayerQueryable(lp)) {
    msSetWMSParamString(psWMSParams, "QUERY_LAYERS", pszName, MS_TRUE);
  }
  if (pszTime && strlen(pszTime) > 0) {
    msSetWMSParamString(psWMSParams, "TIME",   pszTime,  MS_TRUE);
  }

  /* if  the metadata wms_sld_body is set to AUTO, we generate
   * the sld based on classes found in the map file and send
   * it in the URL. If diffrent from AUTO, we are assuming that
   * it is a valid sld.
   */
  if (pszSLDBody) {
    if (strcasecmp(pszSLDBody, "AUTO") == 0) {
      if (pszVersion && strncmp(pszVersion, "1.3.0", 5) == 0)
        pszSLDGenerated = msSLDGenerateSLD(map, lp->index, "1.1.0");
      else
        pszSLDGenerated = msSLDGenerateSLD(map, lp->index, NULL);

      if (pszSLDGenerated) {
        msSetWMSParamString(psWMSParams, "SLD_BODY",
                            pszSLDGenerated, MS_TRUE);
        free(pszSLDGenerated);
      }
    } else {
      msSetWMSParamString(psWMSParams, "SLD_BODY", pszSLDBody, MS_TRUE);
    }

  }

  if (pszSLDURL) {
    msSetWMSParamString(psWMSParams, "SLD", pszSLDURL, MS_TRUE);
  }

  if (pszBgColor) {
    msSetWMSParamString(psWMSParams, "BGCOLOR", pszBgColor, MS_TRUE);
  }

  if (pszTransparent) {
    msSetWMSParamString(psWMSParams, "TRANSPARENT", pszTransparent, MS_TRUE);
  } else {
    msSetWMSParamString(psWMSParams, "TRANSPARENT", "TRUE", MS_TRUE);
  }

  return MS_SUCCESS;
}

#endif /* USE_WMS_LYR */


/**********************************************************************
 *                          msBuildWMSLayerURL()
 *
 * Build a GetMap or GetFeatureInfo URL.
 *
 * Returns a reference to a newly allocated string that should be freed
 * by the caller.
 **********************************************************************/

static int
msBuildWMSLayerURL(mapObj *map, layerObj *lp, int nRequestType,
                   int nClickX, int nClickY, int nFeatureCount,
                   const char *pszInfoFormat, rectObj *bbox_ret,
                   int *width_ret, int *height_ret,
                   wmsParamsObj *psWMSParams)
{
#ifdef USE_WMS_LYR
  char *pszEPSG = NULL;
  const char *pszVersion, *pszTmp, *pszRequestParam, *pszExceptionsParam,
        *pszLayer=NULL, *pszQueryLayers=NULL;
  rectObj bbox;
  int bbox_width = map->width, bbox_height = map->height;
  int nVersion=OWS_VERSION_NOTSET;

  if (lp->connectiontype != MS_WMS) {
    msSetError(MS_WMSCONNERR, "Call supported only for CONNECTIONTYPE WMS",
               "msBuildWMSLayerURL()");
    return MS_FAILURE;
  }

  /* ------------------------------------------------------------------
   * Find out request version
   * ------------------------------------------------------------------ */
  if (lp->connection == NULL ||
      ((pszVersion = strstr(lp->connection, "VERSION=")) == NULL &&
       (pszVersion = strstr(lp->connection, "version=")) == NULL &&
       (pszVersion = strstr(lp->connection, "WMTVER=")) == NULL &&
       (pszVersion = strstr(lp->connection, "wmtver=")) == NULL ) ) {
    /* CONNECTION missing or seems incomplete... try to build from metadata */
    if (msBuildWMSLayerURLBase(map, lp, psWMSParams) != MS_SUCCESS)
      return MS_FAILURE;  /* An error already produced. */

    /* If we received MS_SUCCESS then version must have been set */
    pszVersion = msLookupHashTable(psWMSParams->params, "VERSION");
    if (pszVersion ==NULL)
      pszVersion = msLookupHashTable(psWMSParams->params, "WMTVER");

    nVersion = msOWSParseVersionString(pszVersion);
  } else {
    /* CONNECTION string seems complete, start with that. */
    char *pszDelimiter;
    psWMSParams->onlineresource = msStrdup(lp->connection);

    /* Fetch version info */
    pszVersion = strchr(pszVersion, '=')+1;
    pszDelimiter = strchr(pszVersion, '&');
    if (pszDelimiter != NULL)
      *pszDelimiter = '\0';
    nVersion = msOWSParseVersionString(pszVersion);
    if (pszDelimiter != NULL)
      *pszDelimiter = '&';
  }

  switch (nVersion) {
    case OWS_1_0_8:
      nVersion = OWS_1_1_0;    /* 1.0.8 == 1.1.0 */
      break;
    case OWS_1_0_0:
    case OWS_1_0_1:
    case OWS_1_0_7:
    case OWS_1_1_0:
    case OWS_1_1_1:
      /* All is good, this is a supported version. */
      break;
    default:
      /* Not a supported version */
      msSetError(MS_WMSCONNERR, "MapServer supports only WMS 1.0.0 to 1.1.1 (please verify the VERSION parameter in the connection string).", "msBuildWMSLayerURL()");
      return MS_FAILURE;
  }

  /* ------------------------------------------------------------------
   * For GetFeatureInfo requests, make sure QUERY_LAYERS is included
   * ------------------------------------------------------------------ */
  if  (nRequestType == WMS_GETFEATUREINFO &&
       strstr(psWMSParams->onlineresource, "QUERY_LAYERS=") == NULL &&
       strstr(psWMSParams->onlineresource, "query_layers=") == NULL &&
       msLookupHashTable(psWMSParams->params, "QUERY_LAYERS") == NULL) {
    pszQueryLayers = msOWSLookupMetadata(&(lp->metadata), "MO", "name");

    if (pszQueryLayers == NULL) {
      msSetError(MS_WMSCONNERR, "wms_name not set or WMS Connection String must contain the QUERY_LAYERS parameter to support GetFeatureInfo requests (with name in uppercase).", "msBuildWMSLayerURL()");
      return MS_FAILURE;
    }
  }

  /* ------------------------------------------------------------------
   * For GetLegendGraphic requests, make sure LAYER is included
   * ------------------------------------------------------------------ */
  if  (nRequestType == WMS_GETLEGENDGRAPHIC &&
       strstr(psWMSParams->onlineresource, "LAYER=") == NULL &&
       strstr(psWMSParams->onlineresource, "layer=") == NULL &&
       msLookupHashTable(psWMSParams->params, "LAYER") == NULL) {
    pszLayer = msOWSLookupMetadata(&(lp->metadata), "MO", "name");

    if (pszLayer == NULL) {
      msSetError(MS_WMSCONNERR, "wms_name not set or WMS Connection String must contain the LAYER parameter to support GetLegendGraphic requests (with name in uppercase).", "msBuildWMSLayerURL()");
      return MS_FAILURE;
    }
  }

  /* ------------------------------------------------------------------
   * Figure the SRS we'll use for the request.
   * - Fetch the map SRS (if it's EPSG)
   * - Check if map SRS is listed in layer wms_srs metadata
   * - If map SRS is valid for this layer then use it
   * - Otherwise request layer in its default SRS and we'll reproject later
   * ------------------------------------------------------------------ */
  if ((pszEPSG = (char*)msOWSGetEPSGProj(&(map->projection),
                                         NULL, NULL, MS_TRUE)) != NULL &&
      (pszEPSG = msStrdup(pszEPSG)) != NULL &&
      (strncasecmp(pszEPSG, "EPSG:", 5) == 0 ||
       strncasecmp(pszEPSG, "AUTO:", 5) == 0) ) {
    const char *pszLyrEPSG, *pszFound;
    int nLen;
    char *pszPtr = NULL;

    /* If it's an AUTO projection then keep only id and strip off  */
    /* the parameters for now (we'll restore them at the end) */
    if (strncasecmp(pszEPSG, "AUTO:", 5) == 0) {
      if ((pszPtr = strchr(pszEPSG, ',')))
        *pszPtr = '\0';
    }

    nLen = strlen(pszEPSG);

    pszLyrEPSG = msOWSGetEPSGProj(&(lp->projection), &(lp->metadata),
                                  "MO", MS_FALSE);

    if (pszLyrEPSG == NULL ||
        (pszFound = strstr(pszLyrEPSG, pszEPSG)) == NULL ||
        ! ((*(pszFound+nLen) == '\0') || isspace(*(pszFound+nLen))) ) {
      /* Not found in Layer's list of SRS (including projection object) */
      free(pszEPSG);
      pszEPSG = NULL;
    }
    if (pszEPSG && pszPtr)
      *pszPtr = ',';  /* Restore full AUTO:... definition */
  }

  if (pszEPSG == NULL &&
      ((pszEPSG = (char*)msOWSGetEPSGProj(&(lp->projection), &(lp->metadata),
                                          "MO", MS_TRUE)) == NULL ||
       (pszEPSG = msStrdup(pszEPSG)) == NULL ||
       (strncasecmp(pszEPSG, "EPSG:", 5) != 0 &&
        strncasecmp(pszEPSG, "AUTO:", 5) != 0 ) ) ) {
    msSetError(MS_WMSCONNERR, "Layer must have an EPSG or AUTO projection code (in its PROJECTION object or wms_srs metadata)", "msBuildWMSLayerURL()");
    if (pszEPSG) free(pszEPSG);
    return MS_FAILURE;
  }

  /* ------------------------------------------------------------------
   * For an AUTO projection, set the Units,lon0,lat0 if not already set
   * ------------------------------------------------------------------ */
  if (strncasecmp(pszEPSG, "AUTO:", 5) == 0 &&
      strchr(pszEPSG, ',') == NULL) {
    pointObj oPoint;
    char *pszNewEPSG;

    /* Use center of map view for lon0,lat0 */
    oPoint.x = (map->extent.minx + map->extent.maxx)/2.0;
    oPoint.y = (map->extent.miny + map->extent.maxy)/2.0;
    msProjectPoint(&(map->projection), &(map->latlon), &oPoint);

    pszNewEPSG = (char*)msSmallMalloc(101*sizeof(char));

    snprintf(pszNewEPSG, 100, "%s,9001,%.16g,%.16g",
             pszEPSG, oPoint.x, oPoint.y);
    pszNewEPSG[100]='\0';
    free(pszEPSG);
    pszEPSG=pszNewEPSG;
  }

  /* ------------------------------------------------------------------
   * Set layer SRS.
   * ------------------------------------------------------------------ */
  /* No need to set lp->proj if it's already set to the right EPSG code */
  if ((pszTmp = msOWSGetEPSGProj(&(lp->projection), NULL, "MO", MS_TRUE)) == NULL ||
      strcasecmp(pszEPSG, pszTmp) != 0) {
    const char *ows_srs;

    /* no need to set lp->proj if it is already set and there is only
       one item in the _srs metadata for this layer - we will assume
       the projection block matches the _srs metadata (the search for ' '
       in ows_srs is a test to see if there are multiple EPSG: codes) */
    if( lp->projection.numargs == 0
        || (ows_srs = msOWSGetEPSGProj(NULL,&(lp->metadata), "MO", MS_FALSE)) == NULL
        || (strchr(ows_srs,' ') != NULL) ) {
      if (strncasecmp(pszEPSG, "EPSG:", 5) == 0) {
        char szProj[20];
        snprintf(szProj, sizeof(szProj), "init=epsg:%s", pszEPSG+5);

        if (msLoadProjectionString(&(lp->projection), szProj) != 0)
          return MS_FAILURE;
      } else {
        if (msLoadProjectionString(&(lp->projection), pszEPSG) != 0)
          return MS_FAILURE;
      }
    }
  }

  /* ------------------------------------------------------------------
   * Adjust for MapServer EXTENT being center of pixel and WMS BBOX being
   * edge of pixel (#2843).
   * ------------------------------------------------------------------ */
  bbox = map->extent;

  bbox.minx -= map->cellsize * 0.5;
  bbox.maxx += map->cellsize * 0.5;
  bbox.miny -= map->cellsize * 0.5;
  bbox.maxy += map->cellsize * 0.5;

  /* -------------------------------------------------------------------- */
  /*      Reproject if needed.                                            */
  /* -------------------------------------------------------------------- */
  if (msProjectionsDiffer(&(map->projection), &(lp->projection))) {
    msProjectRect(&(map->projection), &(lp->projection), &bbox);

    /* -------------------------------------------------------------------- */
    /*      Sometimes our remote WMS only accepts square pixel              */
    /*      requests.  If this is the case adjust adjust the number of      */
    /*      pixels or lines in the request so that the pixels are           */
    /*      square.                                                         */
    /* -------------------------------------------------------------------- */
    {
      const char *nonsquare_ok =
        msOWSLookupMetadata(&(lp->metadata),
                            "MO", "nonsquare_ok");

      /* assume nonsquare_ok is false */
      if( nonsquare_ok != NULL
          && (strcasecmp(nonsquare_ok,"no") == 0
              || strcasecmp(nonsquare_ok,"false") == 0) ) {
        double cellsize_x = (bbox.maxx-bbox.minx) / bbox_width;
        double cellsize_y = (bbox.maxy-bbox.miny) / bbox_height;

        if( cellsize_x < cellsize_y * 0.999999 ) {
          int new_bbox_height =
            ceil((cellsize_y/cellsize_x) * bbox_height);

          if (lp->debug)
            msDebug("NONSQUARE_OK=%s, adjusted HEIGHT from %d to %d to equalize cellsize at %g.\n",
                    nonsquare_ok,
                    bbox_height, new_bbox_height, cellsize_x );
          bbox_height = new_bbox_height;
        } else if( cellsize_y < cellsize_x * 0.999999 ) {
          int new_bbox_width =
            ceil((cellsize_x/cellsize_y) * bbox_width);

          if (lp->debug)
            msDebug("NONSQUARE_OK=%s, adjusted WIDTH from %d to %d to equalize cellsize at %g.\n",
                    nonsquare_ok,
                    bbox_width, new_bbox_width, cellsize_y );
          bbox_width = new_bbox_width;
        } else {
          if (lp->debug)
            msDebug("NONSQUARE_OK=%s, but cellsize was already square - no change.\n",
                    nonsquare_ok );
        }
      }
    }
  }

  /* -------------------------------------------------------------------- */
  /*      If the layer has predefined extents, and a predefined           */
  /*      projection that matches the request projection, then            */
  /*      consider restricting the BBOX to match the limits.              */
  /* -------------------------------------------------------------------- */
  if( bbox_width != 0 ) {
    const char *ows_srs;
    rectObj  layer_rect;

    ows_srs = msOWSGetEPSGProj(&(lp->projection), &(lp->metadata),
                               "MO", MS_FALSE);

    if( ows_srs && strchr(ows_srs,' ') == NULL
        && msOWSGetLayerExtent( map, lp, "MO", &layer_rect) == MS_SUCCESS ) {
      /* fulloverlap */
      if( msRectContained( &bbox, &layer_rect ) ) {
        /* no changes */
      }

      /* no overlap */
      else if( !msRectOverlap( &layer_rect, &bbox ) ) {
        bbox_width = 0;
        bbox_height = 0;
      }

      else {
        double cellsize_x = (bbox.maxx-bbox.minx) / bbox_width;
        double cellsize_y = (bbox.maxy-bbox.miny) / bbox_height;
        double cellsize = MIN(cellsize_x,cellsize_y);

        msRectIntersect( &bbox, &layer_rect );

        bbox_width = ceil((bbox.maxx - bbox.minx) / cellsize);
        bbox_height = ceil((bbox.maxy - bbox.miny) / cellsize);

        /* Force going through the resampler if we're going to receive a clipped BBOX (#4931) */
        if(msLayerGetProcessingKey(lp, "RESAMPLE") == NULL) {
          msLayerSetProcessingKey(lp, "RESAMPLE", "nearest");
        }
      }
    }
  }

  /* -------------------------------------------------------------------- */
  /*      Potentially return the bbox.                                    */
  /* -------------------------------------------------------------------- */
  if (bbox_ret != NULL)
    *bbox_ret = bbox;

  if( width_ret != NULL )
    *width_ret = bbox_width;

  if( height_ret != NULL )
    *height_ret = bbox_height;

  /* ------------------------------------------------------------------
   * Build the request URL.
   * At this point we set only the following parameters for GetMap:
   *   REQUEST
   *   SRS
   *   BBOX
   *
   * And for GetFeatureInfo:
   *   X
   *   Y
   *   INFO_FORMAT
   *   FEATURE_COUNT (only if nFeatureCount > 0)
   *
   * The connection string should contain all other required params,
   * including:
   *   VERSION
   *   LAYERS
   *   FORMAT
   *   TRANSPARENT
   *   STYLES
   *   QUERY_LAYERS (for queryable layers only)
   * ------------------------------------------------------------------ */

  if (nRequestType == WMS_GETFEATUREINFO) {
    char szBuf[100] = "";

    if (nVersion >= OWS_1_0_7)
      pszRequestParam = "GetFeatureInfo";
    else
      pszRequestParam = "feature_info";

    if (nVersion >= OWS_1_1_0)
      pszExceptionsParam = "application/vnd.ogc.se_xml";
    else if (nVersion > OWS_1_0_0)  /* 1.0.1 to 1.0.7 */
      pszExceptionsParam = "SE_XML";
    else
      pszExceptionsParam = "WMS_XML";

    msSetWMSParamString(psWMSParams, "REQUEST", pszRequestParam, MS_FALSE);
    msSetWMSParamInt(   psWMSParams, "WIDTH",   bbox_width);
    msSetWMSParamInt(   psWMSParams, "HEIGHT",  bbox_height);
    msSetWMSParamString(psWMSParams, "SRS",     pszEPSG, MS_FALSE);

    snprintf(szBuf, sizeof(szBuf), "%.15g,%.15g,%.15g,%.15g",
             bbox.minx, bbox.miny, bbox.maxx, bbox.maxy);
    msSetWMSParamString(psWMSParams, "BBOX",    szBuf, MS_TRUE);

    msSetWMSParamInt(   psWMSParams, "X",       nClickX);
    msSetWMSParamInt(   psWMSParams, "Y",       nClickY);

    msSetWMSParamString(psWMSParams, "EXCEPTIONS", pszExceptionsParam, MS_FALSE);
    msSetWMSParamString(psWMSParams, "INFO_FORMAT", pszInfoFormat, MS_TRUE);

    if (pszQueryLayers) { /* not set in CONNECTION string */
      msSetWMSParamString(psWMSParams, "QUERY_LAYERS", pszQueryLayers, MS_FALSE);
    }

    /* If FEATURE_COUNT <= 0 then don't pass this parameter */
    /* The spec states that FEATURE_COUNT must be greater than zero */
    /* and if not passed then the behavior is up to the server */
    if (nFeatureCount > 0) {
      msSetWMSParamInt(psWMSParams, "FEATURE_COUNT", nFeatureCount);
    }

  } else if (nRequestType == WMS_GETLEGENDGRAPHIC) {
    if(map->extent.maxx > map->extent.minx && map->width > 0 && map->height > 0) {
      char szBuf[20] = "";
      double scaledenom;
      msCalculateScale(map->extent, map->units, map->width, map->height,
                     map->resolution, &scaledenom);
      snprintf(szBuf, 20, "%g",scaledenom);
      msSetWMSParamString(psWMSParams, "SCALE", szBuf, MS_FALSE);
    }
    pszRequestParam = "GetLegendGraphic";

    pszExceptionsParam = msOWSLookupMetadata(&(lp->metadata),
                         "MO", "exceptions_format");
    if (pszExceptionsParam == NULL) {
      if (nVersion >= OWS_1_1_0)
        pszExceptionsParam = "application/vnd.ogc.se_inimage";
      else
        pszExceptionsParam = "INIMAGE";
    }

    if (pszLayer) { /* not set in CONNECTION string */
      msSetWMSParamString(psWMSParams, "LAYER", pszLayer, MS_FALSE);
    }

    msSetWMSParamString(psWMSParams, "REQUEST", pszRequestParam, MS_FALSE);
    msSetWMSParamString(psWMSParams, "SRS",     pszEPSG, MS_FALSE);

  } else { /* if (nRequestType == WMS_GETMAP) */
    char szBuf[100] = "";

    if (nVersion >= OWS_1_0_7)
      pszRequestParam = "GetMap";
    else
      pszRequestParam = "map";

    pszExceptionsParam = msOWSLookupMetadata(&(lp->metadata),
                         "MO", "exceptions_format");
    if (pszExceptionsParam == NULL) {
      if (nVersion >= OWS_1_1_0)
        pszExceptionsParam = "application/vnd.ogc.se_inimage";
      else
        pszExceptionsParam = "INIMAGE";
    }

    msSetWMSParamString(psWMSParams, "REQUEST", pszRequestParam, MS_FALSE);
    msSetWMSParamInt(   psWMSParams, "WIDTH",   bbox_width);
    msSetWMSParamInt(   psWMSParams, "HEIGHT",  bbox_height);
    msSetWMSParamString(psWMSParams, "SRS",     pszEPSG, MS_FALSE);

    snprintf(szBuf, sizeof(szBuf), "%.15g,%.15g,%.15g,%.15g",
             bbox.minx, bbox.miny, bbox.maxx, bbox.maxy);
    msSetWMSParamString(psWMSParams, "BBOX",    szBuf, MS_TRUE);
    msSetWMSParamString(psWMSParams, "EXCEPTIONS",  pszExceptionsParam, MS_FALSE);
  }

  free(pszEPSG);

  return MS_SUCCESS;

#else
  /* ------------------------------------------------------------------
   * WMS CONNECTION Support not included...
   * ------------------------------------------------------------------ */
  msSetError(MS_WMSCONNERR, "WMS CLIENT CONNECTION support is not available.",
             "msBuildWMSLayerURL()");
  return MS_FAILURE;

#endif /* USE_WMS_LYR */

}


/**********************************************************************
 *                          msWMSGetFeatureInfoURL()
 *
 * Build a GetFeatureInfo URL for this layer.
 *
 * Returns a reference to a newly allocated string that should be freed
 * by the caller.
 **********************************************************************/
char *msWMSGetFeatureInfoURL(mapObj *map, layerObj *lp,
                             int nClickX, int nClickY, int nFeatureCount,
                             const char *pszInfoFormat)
{
  wmsParamsObj sThisWMSParams;
  char *pszURL;

  msInitWmsParamsObj(&sThisWMSParams);

  if (msBuildWMSLayerURL(map, lp, WMS_GETFEATUREINFO,
                         nClickX, nClickY, nFeatureCount,
                         pszInfoFormat, NULL, NULL, NULL,
                         &sThisWMSParams)!= MS_SUCCESS) {
    return NULL;
  }

  pszURL = msBuildURLFromWMSParams(&sThisWMSParams);
  msFreeWmsParamsObj(&sThisWMSParams);

  return pszURL;
}

/**********************************************************************
 *                          msPrepareWMSLayerRequest()
 *
 **********************************************************************/

int msPrepareWMSLayerRequest(int nLayerId, mapObj *map, layerObj *lp,
                             int nRequestType, enum MS_CONNECTION_TYPE lastconnectiontype,
                             wmsParamsObj *psLastWMSParams,
                             int nClickX, int nClickY, int nFeatureCount, const char *pszInfoFormat,
                             httpRequestObj *pasReqInfo, int *numRequests)
{
#ifdef USE_WMS_LYR
  char *pszURL = NULL, *pszHTTPCookieData = NULL;
  const char *pszTmp;
  rectObj bbox;
  int bbox_width, bbox_height;
  int nTimeout, bOkToMerge, bForceSeparateRequest, bCacheToDisk;
  wmsParamsObj sThisWMSParams;

  if (lp->connectiontype != MS_WMS)
    return MS_FAILURE;

  msInitWmsParamsObj(&sThisWMSParams);

  /* ------------------------------------------------------------------
   * Build the request URL, this will also set layer projection and
   * compute BBOX in that projection.
   * ------------------------------------------------------------------ */


  if (nRequestType == WMS_GETMAP &&
      ( msBuildWMSLayerURL(map, lp, WMS_GETMAP,
                           0, 0, 0, NULL, &bbox, &bbox_width, &bbox_height,
                           &sThisWMSParams) != MS_SUCCESS) ) {
    /* an error was already reported. */
    msFreeWmsParamsObj(&sThisWMSParams);
    return MS_FAILURE;
  }

  else if (nRequestType == WMS_GETFEATUREINFO &&
           msBuildWMSLayerURL(map, lp, WMS_GETFEATUREINFO,
                              nClickX, nClickY, nFeatureCount, pszInfoFormat,
                              NULL, NULL, NULL,
                              &sThisWMSParams) != MS_SUCCESS ) {
    /* an error was already reported. */
    msFreeWmsParamsObj(&sThisWMSParams);
    return MS_FAILURE;
  } else if (nRequestType == WMS_GETLEGENDGRAPHIC &&
             msBuildWMSLayerURL(map, lp, WMS_GETLEGENDGRAPHIC,
                                0, 0, 0, NULL,
                                NULL, NULL, NULL,
                                &sThisWMSParams) != MS_SUCCESS ) {
    /* an error was already reported. */
    msFreeWmsParamsObj(&sThisWMSParams);
    return MS_FAILURE;
  }


  /* ------------------------------------------------------------------
   * Check if the request is empty, perhaps due to reprojection problems
   * or wms_extents restrictions.
   * ------------------------------------------------------------------ */
  if ((nRequestType == WMS_GETMAP) && (bbox_width == 0 || bbox_height == 0) ) {
    msFreeWmsParamsObj(&sThisWMSParams);
    return MS_SUCCESS;  /* No overlap. */
  }

  /* ------------------------------------------------------------------
   * Check if layer overlaps current view window (using wms_latlonboundingbox)
   * ------------------------------------------------------------------ */
  if ((pszTmp = msOWSLookupMetadata(&(lp->metadata),
                                    "MO", "latlonboundingbox")) != NULL) {
    char **tokens;
    int n;
    rectObj ext;

    tokens = msStringSplit(pszTmp, ' ', &n);
    if (tokens==NULL || n != 4) {
      msSetError(MS_WMSCONNERR, "Wrong number of arguments for 'wms_latlonboundingbox' metadata.",
                 "msDrawWMSLayer()");
      msFreeWmsParamsObj(&sThisWMSParams);
      return MS_FAILURE;
    }

    ext.minx = atof(tokens[0]);
    ext.miny = atof(tokens[1]);
    ext.maxx = atof(tokens[2]);
    ext.maxy = atof(tokens[3]);

    msFreeCharArray(tokens, n);

    /* Reproject latlonboundingbox to the selected SRS for the layer and */
    /* check if it overlaps the bbox that we calculated for the request */

    msProjectRect(&(map->latlon), &(lp->projection), &ext);
    if (!msRectOverlap(&bbox, &ext)) {
      /* No overlap... nothing to do */

      msFreeWmsParamsObj(&sThisWMSParams);
      return MS_SUCCESS;  /* No overlap. */
    }
  }

  /* ------------------------------------------------------------------
   * check to see if a the metadata wms_connectiontimeout is set. If it is
   * the case we will use it, else we use the default which is 30 seconds.
   * First check the metadata in the layer object and then in the map object.
   * ------------------------------------------------------------------ */
  nTimeout = 30;  /* Default is 30 seconds  */
  if ((pszTmp = msOWSLookupMetadata2(&(lp->metadata), &(map->web.metadata),
                                    "MO", "connectiontimeout")) != NULL) {
    nTimeout = atoi(pszTmp);
  }

  /* ------------------------------------------------------------------
   * Check if we want to use in memory images instead of writing to disk.
   * ------------------------------------------------------------------ */
  if ((pszTmp = msOWSLookupMetadata(&(lp->metadata),
                                    "MO", "cache_to_disk")) != NULL) {
    if( strcasecmp(pszTmp,"true") == 0
        || strcasecmp(pszTmp,"on") == 0
        || strcasecmp(pszTmp,"yes") == 0 )
      bCacheToDisk = MS_TRUE;
    else
      bCacheToDisk = atoi(pszTmp);
  } else
    bCacheToDisk = MS_FALSE;

  if( bCacheToDisk ) {
    /* We'll store the remote server's response to a tmp file. */
    if (map->web.imagepath == NULL || strlen(map->web.imagepath) == 0) {
      msSetError(MS_WMSERR,
                 "WEB.IMAGEPATH must be set to use WMS client connections.",
                 "msPrepareWMSLayerRequest()");
      return MS_FAILURE;
    }
  }

  /* ------------------------------------------------------------------
   * Check if layer can be merged with previous WMS layer requests
   * Metadata wms_force_separate_request can be set to 1 to prevent this
   * this layer from being combined with any other layer.
   * ------------------------------------------------------------------ */
  bForceSeparateRequest = MS_FALSE;
  if ((pszTmp = msOWSLookupMetadata(&(lp->metadata),
                                    "MO", "force_separate_request")) != NULL) {
    bForceSeparateRequest = atoi(pszTmp);
  }
  bOkToMerge = MS_FALSE;
  if (!bForceSeparateRequest &&
      lastconnectiontype == MS_WMS &&
      psLastWMSParams != NULL &&
      sThisWMSParams.numparams == psLastWMSParams->numparams &&
      strcmp(sThisWMSParams.onlineresource,
             psLastWMSParams->onlineresource) == 0) {
    const char *key, *value1, *value2;
    bOkToMerge = MS_TRUE;

    key = msFirstKeyFromHashTable(sThisWMSParams.params);
    while (key != NULL && bOkToMerge == MS_TRUE) {
      /* Skip parameters whose values can be different */
      if (!(strcmp(key, "LAYERS") == 0 ||
            strcmp(key, "QUERY_LAYERS") == 0 ||
            strcmp(key, "STYLES") == 0) ) {
        value1 = msLookupHashTable(psLastWMSParams->params, key);
        value2 = msLookupHashTable(sThisWMSParams.params, key);

        if (value1==NULL || value2==NULL ||
            strcmp(value1, value2) != 0) {
          bOkToMerge = MS_FALSE;
          break;
        }
      }
      key = msNextKeyFromHashTable(sThisWMSParams.params, key);
    }
  }

  /*------------------------------------------------------------------
   * Check to see if there's a HTTP Cookie to forward
   * If Cookie differ between the two connection, it's NOT OK to merge
   * the connection
   * ------------------------------------------------------------------ */
  if ((pszTmp = msOWSLookupMetadata(&(lp->metadata),
                                    "MO", "http_cookie")) != NULL) {
    if(strcasecmp(pszTmp, "forward") == 0) {
      pszTmp= msLookupHashTable(&(map->web.metadata),"http_cookie_data");
      if(pszTmp != NULL) {
        pszHTTPCookieData = msStrdup(pszTmp);
      }
    } else {
      pszHTTPCookieData = msStrdup(pszTmp);
    }
  } else if ((pszTmp = msOWSLookupMetadata(&(map->web.metadata),
                       "MO", "http_cookie")) != NULL) {
    if(strcasecmp(pszTmp, "forward") == 0) {
      pszTmp= msLookupHashTable(&(map->web.metadata),"http_cookie_data");
      if(pszTmp != NULL) {
        pszHTTPCookieData = msStrdup(pszTmp);
      }
    } else {
      pszHTTPCookieData = msStrdup(pszTmp);
    }
  }

  if(bOkToMerge && pszHTTPCookieData != sThisWMSParams.httpcookiedata) {
    if(pszHTTPCookieData == NULL || sThisWMSParams.httpcookiedata == NULL) {
      bOkToMerge = MS_FALSE;
    }
    if(strcmp(pszHTTPCookieData, sThisWMSParams.httpcookiedata) != 0) {
      bOkToMerge = MS_FALSE;
    }
  }

  if (bOkToMerge) {
    /* Merge both requests into sThisWMSParams
     */
    const char *value1, *value2;
    char *keys[] = {"LAYERS", "QUERY_LAYERS", "STYLES"};
    int i;

    for(i=0; i<3; i++) {
      value1 = msLookupHashTable(psLastWMSParams->params, keys[i]);
      value2 = msLookupHashTable(sThisWMSParams.params, keys[i]);
      if (value1 && value2) {
        char *pszBuf;
        int nLen;

        nLen = strlen(value1) + strlen(value2) +2;
        pszBuf = malloc(nLen);
        MS_CHECK_ALLOC(pszBuf, nLen, MS_FAILURE);

        snprintf(pszBuf, nLen, "%s,%s", value1, value2);
        msSetWMSParamString(&sThisWMSParams, keys[i], pszBuf,MS_FALSE);

        /* This key existed already, we don't want it counted twice */
        sThisWMSParams.numparams--;

        msFree(pszBuf);
      }
    }
  }


  /* ------------------------------------------------------------------
   * Build new request URL
   * ------------------------------------------------------------------ */
  pszURL = msBuildURLFromWMSParams(&sThisWMSParams);

  if (bOkToMerge && (*numRequests)>0) {
    /* ------------------------------------------------------------------
     * Update the last request in the array:  (*numRequests)-1
     * ------------------------------------------------------------------ */
    msFree(pasReqInfo[(*numRequests)-1].pszGetUrl);
    pasReqInfo[(*numRequests)-1].pszGetUrl = pszURL;
    pszURL = NULL;
    pasReqInfo[(*numRequests)-1].debug |= lp->debug;
    if (nTimeout > pasReqInfo[(*numRequests)-1].nTimeout)
      pasReqInfo[(*numRequests)-1].nTimeout = nTimeout;
  } else {
    /* ------------------------------------------------------------------
     * Add a request to the array (already preallocated)
     * ------------------------------------------------------------------ */
    pasReqInfo[(*numRequests)].nLayerId = nLayerId;
    pasReqInfo[(*numRequests)].pszGetUrl = pszURL;

    pszURL = NULL;
    pasReqInfo[(*numRequests)].pszHTTPCookieData = pszHTTPCookieData;
    pszHTTPCookieData = NULL;
    if( bCacheToDisk ) {
      pasReqInfo[(*numRequests)].pszOutputFile =
        msTmpFile(map, map->mappath, NULL, "wms.tmp");
    } else
      pasReqInfo[(*numRequests)].pszOutputFile = NULL;
    pasReqInfo[(*numRequests)].nStatus = 0;
    pasReqInfo[(*numRequests)].nTimeout = nTimeout;
    pasReqInfo[(*numRequests)].bbox   = bbox;
    pasReqInfo[(*numRequests)].width  = bbox_width;
    pasReqInfo[(*numRequests)].height = bbox_height;
    pasReqInfo[(*numRequests)].debug = lp->debug;

    if (msHTTPAuthProxySetup(&(map->web.metadata), &(lp->metadata),
                             pasReqInfo, *numRequests, map, "MO") != MS_SUCCESS)
      return MS_FAILURE;

    (*numRequests)++;
  }


  /* ------------------------------------------------------------------
   * Replace contents of psLastWMSParams with sThisWMSParams
   * unless bForceSeparateRequest is set in which case we make it empty
   * ------------------------------------------------------------------ */
  if (psLastWMSParams) {
    msFreeWmsParamsObj(psLastWMSParams);
    if (!bForceSeparateRequest)
      *psLastWMSParams = sThisWMSParams;
    else
      msInitWmsParamsObj(psLastWMSParams);
  } else {
    /* Can't copy it, so we just free it */
    msFreeWmsParamsObj(&sThisWMSParams);
  }

  return MS_SUCCESS;

#else
  /* ------------------------------------------------------------------
   * WMS CONNECTION Support not included...
   * ------------------------------------------------------------------ */
  msSetError(MS_WMSCONNERR, "WMS CLIENT CONNECTION support is not available.",
             "msDrawWMSLayer()");
  return(MS_FAILURE);

#endif /* USE_WMS_LYR */

}

/**********************************************************************
 *                          msDrawWMSLayerLow()
 *
 **********************************************************************/

int msDrawWMSLayerLow(int nLayerId, httpRequestObj *pasReqInfo,
                      int numRequests, mapObj *map, layerObj *lp, imageObj *img)
{
#ifdef USE_WMS_LYR
  int status = MS_SUCCESS;
  int iReq = -1;
  char szPath[MS_MAXPATHLEN];
  int currenttype;
  int currentconnectiontype;
  int numclasses;
  char *mem_filename = NULL;

  /* ------------------------------------------------------------------
   * Find the request info for this layer in the array, based on nLayerId
   * ------------------------------------------------------------------ */
  for(iReq=0; iReq<numRequests; iReq++) {
    if (pasReqInfo[iReq].nLayerId == nLayerId)
      break;
  }

  if (iReq == numRequests) {
    /* This layer was skipped or was included in a multi-layers
     * request ... nothing to do.
     */
    return MS_SUCCESS;
  }

  if ( !MS_HTTP_SUCCESS( pasReqInfo[iReq].nStatus ) ) {
    /* ====================================================================
          Failed downloading layer... we log an error but we still return
          SUCCESS here so that the layer is only skipped intead of aborting
          the whole draw map.
     ==================================================================== */
    msSetError(MS_WMSERR,
               "WMS GetMap request failed for layer '%s' (Status %d: %s).",
               "msDrawWMSLayerLow()",
               (lp->name?lp->name:"(null)"),
               pasReqInfo[iReq].nStatus, pasReqInfo[iReq].pszErrBuf );

    return MS_SUCCESS;
  }

  /* ------------------------------------------------------------------
   * Check the Content-Type of the response to see if we got an exception,
   * if yes then try to parse it and pass the info to msSetError().
   * We log an error but we still return SUCCESS here so that the layer
   * is only skipped intead of aborting the whole draw map.
   * ------------------------------------------------------------------ */
  if (pasReqInfo[iReq].pszContentType &&
      (strcmp(pasReqInfo[iReq].pszContentType, "text/xml") == 0 ||
       strcmp(pasReqInfo[iReq].pszContentType, "application/vnd.ogc.se_xml") == 0)) {
    FILE *fp;
    char szBuf[MS_BUFFER_LENGTH];

    if( pasReqInfo[iReq].pszOutputFile ) {
      fp = fopen(pasReqInfo[iReq].pszOutputFile, "r");
      if (fp) {
        /* TODO: For now we'll only read the first chunk and return it
         * via msSetError()... we should really try to parse the XML
         * and extract the exception code/message though
         */
        size_t nSize;

        nSize = fread(szBuf, sizeof(char), MS_BUFFER_LENGTH-1, fp);
        if (nSize < MS_BUFFER_LENGTH)
          szBuf[nSize] = '\0';
        else {
          strlcpy(szBuf, "(!!!)", sizeof(szBuf)); /* This should never happen */
        }

        fclose(fp);

        /* We're done with the remote server's response... delete it. */
        if (!lp->debug)
          unlink(pasReqInfo[iReq].pszOutputFile);
      } else {
        strlcpy(szBuf, "(Failed to open exception response)", sizeof(szBuf));
      }
    } else {
      strlcpy( szBuf, pasReqInfo[iReq].result_data, MS_BUFFER_LENGTH );
    }

    if (lp->debug)
      msDebug("WMS GetMap request got XML exception for layer '%s': %s.",
              (lp->name?lp->name:"(null)"), szBuf );

    msSetError(MS_WMSERR,
               "WMS GetMap request got XML exception for layer '%s': %s.",
               "msDrawWMSLayerLow()",
               (lp->name?lp->name:"(null)"), szBuf );

    return MS_SUCCESS;
  }

  /* ------------------------------------------------------------------
   * If the output was written to a memory buffer, then we will need
   * to attach a "VSI" name to this buffer.
   * ------------------------------------------------------------------ */
  if( pasReqInfo[iReq].pszOutputFile == NULL ) {
    CleanVSIDir( "/vsimem/msout" );
    mem_filename = msTmpFile(map, NULL, "/vsimem/msout/", "img.tmp" );

    VSIFCloseL(
      VSIFileFromMemBuffer( mem_filename,
                            (GByte*) pasReqInfo[iReq].result_data,
                            (vsi_l_offset) pasReqInfo[iReq].result_size,
                            FALSE ) );
  }

  /* ------------------------------------------------------------------
   * Prepare layer for drawing, reprojecting the image received from the
   * server if needed...
   * ------------------------------------------------------------------ */
  /* keep the current type that will be restored at the end of this  */
  /* function. */
  currenttype = lp->type;
  currentconnectiontype = lp->connectiontype;
  lp->type = MS_LAYER_RASTER;
  lp->connectiontype = MS_SHAPEFILE;

  /* set the classes to 0 so that It won't do client side */
  /* classification if an sld was set. */
  numclasses = lp->numclasses;

  /* ensure the file connection is closed right away after the layer */
  /* is rendered */
  msLayerSetProcessingKey( lp, "CLOSE_CONNECTION", "NORMAL");

  if (msOWSLookupMetadata(&(lp->metadata), "MO", "sld_body") ||
      msOWSLookupMetadata(&(lp->metadata), "MO", "sld_url"))
    lp->numclasses = 0;

  if (lp->data) free(lp->data);
  if( mem_filename != NULL )
    lp->data = mem_filename;
  else
    lp->data =  msStrdup(pasReqInfo[iReq].pszOutputFile);

  /* #3138 If PROCESSING "RESAMPLE=..." is set we cannot use the simple case */
  if (!msProjectionsDiffer(&(map->projection), &(lp->projection)) &&
      (msLayerGetProcessingKey(lp, "RESAMPLE") == NULL) ) {
    /* The simple case... no reprojection needed... render layer directly. */
    lp->transform = MS_FALSE;
    /* if (msDrawRasterLayerLow(map, lp, img) != 0) */
    /* status = MS_FAILURE; */
    if (msDrawLayer(map, lp, img) != 0)
      status = MS_FAILURE;
  } else {
    FILE *fp;
    char *wldfile;
    /* OK, we have to resample the raster to map projection... */
    lp->transform = MS_TRUE;
    msLayerSetProcessingKey( lp, "LOAD_WHOLE_IMAGE", "YES" );

    /* Create a world file with raster extents */
    /* One line per value, in this order: cx, 0, 0, cy, ulx, uly */
    wldfile = msBuildPath(szPath, lp->map->mappath, lp->data);
    if (wldfile && (strlen(wldfile)>=3))
      strcpy(wldfile+strlen(wldfile)-3, "wld");
    if (wldfile && (fp = VSIFOpenL(wldfile, "wt")) != NULL) {
      double dfCellSizeX = MS_CELLSIZE(pasReqInfo[iReq].bbox.minx,
                                       pasReqInfo[iReq].bbox.maxx,
                                       pasReqInfo[iReq].width);
      double dfCellSizeY = MS_CELLSIZE(pasReqInfo[iReq].bbox.maxy,
                                       pasReqInfo[iReq].bbox.miny,
                                       pasReqInfo[iReq].height);
      char world_text[5000];

      sprintf( world_text, "%.12f\n0\n0\n%.12f\n%.12f\n%.12f\n",
               dfCellSizeX,
               dfCellSizeY,
               pasReqInfo[iReq].bbox.minx + dfCellSizeX * 0.5,
               pasReqInfo[iReq].bbox.maxy + dfCellSizeY * 0.5 );

      VSIFWriteL( world_text, 1, strlen(world_text), fp );
      VSIFCloseL( fp );

      /* GDAL should be called to reproject automatically. */
      if (msDrawLayer(map, lp, img) != 0)
        status = MS_FAILURE;

      if (!lp->debug)
        VSIUnlink( wldfile );
    } else {
      msSetError(MS_WMSCONNERR,
                 "Unable to create wld file for WMS slide.",
                 "msDrawWMSLayer()");
      status = MS_FAILURE;
    }

  }

  /* We're done with the remote server's response... delete it. */
  if (!lp->debug)
    VSIUnlink(lp->data);

  /* restore prveious type */
  lp->type = currenttype;
  lp->connectiontype = currentconnectiontype;

  /* restore previous numclasses */
  lp->numclasses = numclasses;

  free(lp->data);
  lp->data = NULL;

  return status;

#else
  /* ------------------------------------------------------------------
   * WMS CONNECTION Support not included...
   * ------------------------------------------------------------------ */
  msSetError(MS_WMSCONNERR, "WMS CLIENT CONNECTION support is not available.",
             "msDrawWMSLayer()");
  return(MS_FAILURE);

#endif /* USE_WMS_LYR */

}


int msWMSLayerExecuteRequest(mapObj *map, int nOWSLayers, int nClickX, int nClickY,
                             int nFeatureCount, const char *pszInfoFormat, int type)
{
#ifdef USE_WMS_LYR

  msIOContext *context;

  httpRequestObj *pasReqInfo;
  wmsParamsObj sLastWMSParams;
  int i, numReq = 0;

  pasReqInfo = (httpRequestObj *)msSmallMalloc((nOWSLayers+1)*sizeof(httpRequestObj));
  msHTTPInitRequestObj(pasReqInfo, nOWSLayers+1);
  msInitWmsParamsObj(&sLastWMSParams);

  /* Generate the http request */
  for (i=0; i<map->numlayers; i++) {
    if (GET_LAYER(map,map->layerorder[i])->status == MS_ON) {
      if (type == WMS_GETFEATUREINFO &&
          msPrepareWMSLayerRequest(map->layerorder[i], map, GET_LAYER(map,map->layerorder[i]),
                                   WMS_GETFEATUREINFO,
                                   MS_WMS, &sLastWMSParams,
                                   nClickX, nClickY, nFeatureCount, pszInfoFormat,
                                   pasReqInfo, &numReq) == MS_FAILURE) {
        msFreeWmsParamsObj(&sLastWMSParams);
        msFree(pasReqInfo);
        return MS_FAILURE;
      } else if (msPrepareWMSLayerRequest(map->layerorder[i], map, GET_LAYER(map,map->layerorder[i]),
                                          WMS_GETLEGENDGRAPHIC,
                                          MS_WMS, &sLastWMSParams,
                                          0, 0, 0, NULL,
                                          pasReqInfo, &numReq) == MS_FAILURE) {
        msFreeWmsParamsObj(&sLastWMSParams);
        msFree(pasReqInfo);
        return MS_FAILURE;
      }
    }
  }

  if (msOWSExecuteRequests(pasReqInfo, numReq, map, MS_FALSE) == MS_FAILURE) {
    msHTTPFreeRequestObj(pasReqInfo, numReq);
    msFree(pasReqInfo);
    msFreeWmsParamsObj(&sLastWMSParams);
    return MS_FAILURE;
  }

  context = msIO_getHandler( stdout );
  if( context == NULL ) {
    msHTTPFreeRequestObj(pasReqInfo, numReq);
    msFree(pasReqInfo);
    msFreeWmsParamsObj(&sLastWMSParams);
    return MS_FAILURE;
  }

  msIO_printf("Content-Type: %s%c%c",pasReqInfo[0].pszContentType, 10,10);

  if( pasReqInfo[0].pszOutputFile ) {
    FILE *fp;
    char szBuf[MS_BUFFER_LENGTH];

    fp = fopen(pasReqInfo[0].pszOutputFile, "r");
    if (fp) {
      while(1) {
        size_t nSize;
        nSize = fread(szBuf, sizeof(char), MS_BUFFER_LENGTH-1, fp);
        if (nSize > 0)
          msIO_contextWrite( context,
                             szBuf,
                             nSize);
        if (nSize != MS_BUFFER_LENGTH-1)
          break;
      }
      fclose(fp);
      if (!map->debug)
        unlink(pasReqInfo[0].pszOutputFile);
    } else {
      msSetError(MS_IOERR, "'%s'.",
                 "msWMSLayerExecuteRequest()", pasReqInfo[0].pszOutputFile);
      return MS_FAILURE;
    }
  } else {
    msIO_contextWrite( context,
                       pasReqInfo[0].result_data,
                       pasReqInfo[0].result_size );
  }

  msHTTPFreeRequestObj(pasReqInfo, numReq);
  msFree(pasReqInfo);
  msFreeWmsParamsObj(&sLastWMSParams);

  return MS_SUCCESS;
#else
  /* ------------------------------------------------------------------
   * WMS CONNECTION Support not included...
   * ------------------------------------------------------------------ */
  msSetError(MS_WMSCONNERR, "WMS CLIENT CONNECTION support is not available.",
             "msWMSLayerExecuteRequest()");
  return(MS_FAILURE);

#endif /* USE_WMS_LYR */

}
