text: /******************************************************************************
   * $id: maptemplate.c 7725 2008-06-21 15:56:58Z sdlime $
   *
   * Project:  MapServer
   * Purpose:  Various template processing functions.
   * Author:   Steve Lime and the MapServer team.
   *
   ******************************************************************************
   * Copyright (c) 1996-2008 Regents of the University of Minnesota.
   *
   * 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 "maptemplate.h"
  #include "maphash.h"
  #include "mapserver.h"
  #include "maptile.h"
  #include "mapows.h"
  
  #include <sys/types.h>
  #include <sys/stat.h>
  #include <time.h>
  
  #include <ctype.h>
  
  
  static inline void IGUR_sizet(size_t ignored) { (void)ignored; }  /* Ignore GCC Unused Result */
  static inline void IGUR_voidp(void* ignored) { (void)ignored; }  /* Ignore GCC Unused Result */
  
  static char *olUrl = "//www.mapserver.org/lib/OpenLayers-ms60.js";
  static char *olTemplate = \
                            "<html>\n"
                            "<head>\n"
                            "<meta content=\"text/html;charset=utf-8\" http-equiv=\"Content-Type\">\n"
                            "  <title>MapServer Simple Viewer</title>\n"
                            "    <script type=\"text/javascript\" src=\"[openlayers_js_url]\"></script>\n"
                            "    <link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"//www.mapserver.org/_static/mapserver.ico\"/>\n"
                            "    </head>\n"
                            "    <body>\n"
                            "      <div style=\"width:[mapwidth]; height:[mapheight]\" id=\"map\"></div>\n"
                            "      <script defer=\"defer\" type=\"text/javascript\">\n"
                            "        var map = new OpenLayers.Map('map',\n"
                            "                                     {maxExtent: new OpenLayers.Bounds([minx],[miny],[maxx],[maxy]),\n"
                            "                                      maxResolution: [cellsize]});\n"
                            "        [openlayers_layer];\n"
                            "        map.addLayer(mslayer);\n"
                            "        map.zoomToMaxExtent();\n"
                            "      </script>\n"
                            "</body>\n"
                            "</html>";
  
  static char *olLayerMapServerTag = \
                                     "var mslayer = new OpenLayers.Layer.MapServer( \"MapServer Layer\",\n"
                                     "                                              \"[mapserv_onlineresource]\",\n"
                                     "                                              {layers: '[layers]'},\n"
                                     "                                              {singleTile: \"true\", ratio:1} )";
  
  static char *olLayerWMSTag = \
                               "var mslayer = new OpenLayers.Layer.WMS('MapServer Simple Viewer\',\n"
                               "                                   '[mapserv_onlineresource]',\n"
                               "                                   {layers: '[LAYERS]',\n"
                               "                                   bbox: '[minx],[miny],[maxx],[maxy]',\n"
                               "                                   width: [mapwidth], height: [mapheight], version: '[VERSION]', format:'[openlayers_format]'},"
                               "                                   {singleTile: \"true\", ratio:1, projection: '[openlayers_projection]'});\n";
  
  static char *processLine(mapservObj *mapserv, char *instr, FILE *stream, int mode);
  
  static int isValidTemplate(FILE *stream, const char *filename)
  {
    char buffer[MS_BUFFER_LENGTH];
  
    if(fgets(buffer, MS_BUFFER_LENGTH, stream) != NULL) {
      if(!strcasestr(buffer, MS_TEMPLATE_MAGIC_STRING)) {
        msSetError(MS_WEBERR, "Missing magic string, %s doesn't look like a MapServer template.", "isValidTemplate()", filename);
        return MS_FALSE;
      }
    }
  
    return MS_TRUE;
  }
  
  /*
   * Redirect to (only use in CGI)
   *
  */
  int msRedirect(char *url)
  {
    msIO_setHeader("Status","302 Found");
    msIO_setHeader("Uri","%s",url);
    msIO_setHeader("Location","%s",url);
    msIO_setHeader("Content-Type","text/html");
    msIO_sendHeaders();
    return MS_SUCCESS;
  }
  
  /*
  ** Sets the map extent under a variety of scenarios.
  */
  int setExtent(mapservObj *mapserv)
  {
    double cellx,celly,cellsize;
  
    if(mapserv->Mode == TILE) {
      if(MS_SUCCESS != msTileSetExtent(mapserv)) {
        return MS_FAILURE;
      }
    }
    switch(mapserv->CoordSource) {
      case FROMUSERBOX: /* user passed in a map extent */
        break;
      case FROMIMGBOX: /* fully interactive web, most likely with java front end */
        cellx = MS_CELLSIZE(mapserv->ImgExt.minx, mapserv->ImgExt.maxx, mapserv->ImgCols);
        celly = MS_CELLSIZE(mapserv->ImgExt.miny, mapserv->ImgExt.maxy, mapserv->ImgRows);
        mapserv->map->extent.minx = MS_IMAGE2MAP_X(mapserv->ImgBox.minx, mapserv->ImgExt.minx, cellx);
        mapserv->map->extent.maxx = MS_IMAGE2MAP_X(mapserv->ImgBox.maxx, mapserv->ImgExt.minx, cellx);
        mapserv->map->extent.maxy = MS_IMAGE2MAP_Y(mapserv->ImgBox.miny, mapserv->ImgExt.maxy, celly); /* y's are flip flopped because img/map coordinate systems are */
        mapserv->map->extent.miny = MS_IMAGE2MAP_Y(mapserv->ImgBox.maxy, mapserv->ImgExt.maxy, celly);
        break;
      case FROMIMGPNT:
        cellx = MS_CELLSIZE(mapserv->ImgExt.minx, mapserv->ImgExt.maxx, mapserv->ImgCols);
        celly = MS_CELLSIZE(mapserv->ImgExt.miny, mapserv->ImgExt.maxy, mapserv->ImgRows);
        mapserv->mappnt.x = MS_IMAGE2MAP_X(mapserv->ImgPnt.x, mapserv->ImgExt.minx, cellx);
        mapserv->mappnt.y = MS_IMAGE2MAP_Y(mapserv->ImgPnt.y, mapserv->ImgExt.maxy, celly);
  
        mapserv->map->extent.minx = mapserv->mappnt.x - .5*((mapserv->ImgExt.maxx - mapserv->ImgExt.minx)/mapserv->fZoom); /* create an extent around that point */
        mapserv->map->extent.miny = mapserv->mappnt.y - .5*((mapserv->ImgExt.maxy - mapserv->ImgExt.miny)/mapserv->fZoom);
        mapserv->map->extent.maxx = mapserv->mappnt.x + .5*((mapserv->ImgExt.maxx - mapserv->ImgExt.minx)/mapserv->fZoom);
        mapserv->map->extent.maxy = mapserv->mappnt.y + .5*((mapserv->ImgExt.maxy - mapserv->ImgExt.miny)/mapserv->fZoom);
        break;
      case FROMREFPNT:
        cellx = MS_CELLSIZE(mapserv->map->reference.extent.minx, mapserv->map->reference.extent.maxx, mapserv->map->reference.width);
        celly = MS_CELLSIZE(mapserv->map->reference.extent.miny, mapserv->map->reference.extent.maxy, mapserv->map->reference.height);
        mapserv->mappnt.x = MS_IMAGE2MAP_X(mapserv->RefPnt.x, mapserv->map->reference.extent.minx, cellx);
        mapserv->mappnt.y = MS_IMAGE2MAP_Y(mapserv->RefPnt.y, mapserv->map->reference.extent.maxy, celly);
  
        mapserv->map->extent.minx = mapserv->mappnt.x - .5*(mapserv->ImgExt.maxx - mapserv->ImgExt.minx); /* create an extent around that point */
        mapserv->map->extent.miny = mapserv->mappnt.y - .5*(mapserv->ImgExt.maxy - mapserv->ImgExt.miny);
        mapserv->map->extent.maxx = mapserv->mappnt.x + .5*(mapserv->ImgExt.maxx - mapserv->ImgExt.minx);
        mapserv->map->extent.maxy = mapserv->mappnt.y + .5*(mapserv->ImgExt.maxy - mapserv->ImgExt.miny);
        break;
      case FROMBUF:
        mapserv->map->extent.minx = mapserv->mappnt.x - mapserv->Buffer; /* create an extent around that point, using the buffer */
        mapserv->map->extent.miny = mapserv->mappnt.y - mapserv->Buffer;
        mapserv->map->extent.maxx = mapserv->mappnt.x + mapserv->Buffer;
        mapserv->map->extent.maxy = mapserv->mappnt.y + mapserv->Buffer;
        break;
      case FROMSCALE:
        cellsize = (mapserv->ScaleDenom/mapserv->map->resolution)/msInchesPerUnit(mapserv->map->units,0); /* user supplied a point and a scale denominator */
        mapserv->map->extent.minx = mapserv->mappnt.x - cellsize*(mapserv->map->width-1)/2.0;
        mapserv->map->extent.miny = mapserv->mappnt.y - cellsize*(mapserv->map->height-1)/2.0;
        mapserv->map->extent.maxx = mapserv->mappnt.x + cellsize*(mapserv->map->width-1)/2.0;
        mapserv->map->extent.maxy = mapserv->mappnt.y + cellsize*(mapserv->map->height-1)/2.0;
        break;
      default: /* use the default in the mapfile if it exists */
        if((mapserv->map->extent.minx == mapserv->map->extent.maxx) && (mapserv->map->extent.miny == mapserv->map->extent.maxy)) {
          msSetError(MS_WEBERR, "No way to generate map extent.", "mapserv()");
          return MS_FAILURE;
        }
    }
  
    mapserv->RawExt = mapserv->map->extent; /* save unaltered extent */
  
    return MS_SUCCESS;
  }
  
  int checkWebExtent(mapservObj *mapserv)
  {
    return MS_SUCCESS;
  }
  
  int checkWebScale(mapservObj *mapserv)
  {
    int status;
    rectObj work_extent = mapserv->map->extent;
  
    mapserv->map->cellsize = msAdjustExtent(&(work_extent), mapserv->map->width, mapserv->map->height); /* we do this cause we need a scale */
    if((status = msCalculateScale(work_extent, mapserv->map->units, mapserv->map->width, mapserv->map->height, mapserv->map->resolution, &mapserv->map->scaledenom)) != MS_SUCCESS) return status;
  
    if((mapserv->map->scaledenom < mapserv->map->web.minscaledenom) && (mapserv->map->web.minscaledenom > 0)) {
      if(mapserv->map->web.mintemplate) { /* use the template provided */
        if(TEMPLATE_TYPE(mapserv->map->web.mintemplate) == MS_FILE) {
          if((status = msReturnPage(mapserv, mapserv->map->web.mintemplate, BROWSE, NULL)) != MS_SUCCESS) return status;
        } else {
          if((status = msReturnURL(mapserv, mapserv->map->web.mintemplate, BROWSE)) != MS_SUCCESS) return status;
        }
      } else { /* force zoom = 1 (i.e. pan) */
        mapserv->fZoom = mapserv->Zoom = 1;
        mapserv->ZoomDirection = 0;
        mapserv->CoordSource = FROMSCALE;
        mapserv->ScaleDenom = mapserv->map->web.minscaledenom;
        mapserv->mappnt.x = (mapserv->map->extent.maxx + mapserv->map->extent.minx)/2; /* use center of bad extent */
        mapserv->mappnt.y = (mapserv->map->extent.maxy + mapserv->map->extent.miny)/2;
        setExtent(mapserv);
        mapserv->map->cellsize = msAdjustExtent(&(mapserv->map->extent), mapserv->map->width, mapserv->map->height);
        if((status = msCalculateScale(mapserv->map->extent, mapserv->map->units, mapserv->map->width, mapserv->map->height, mapserv->map->resolution, &mapserv->map->scaledenom)) != MS_SUCCESS) return status;
      }
    } else {
      if((mapserv->map->scaledenom > mapserv->map->web.maxscaledenom) && (mapserv->map->web.maxscaledenom > 0)) {
        if(mapserv->map->web.maxtemplate) { /* use the template provided */
          if(TEMPLATE_TYPE(mapserv->map->web.maxtemplate) == MS_FILE) {
            if((status = msReturnPage(mapserv, mapserv->map->web.maxtemplate, BROWSE, NULL)) != MS_SUCCESS) return status;
          } else {
            if((status = msReturnURL(mapserv, mapserv->map->web.maxtemplate, BROWSE)) != MS_SUCCESS) return status;
          }
        } else { /* force zoom = 1 (i.e. pan) */
          mapserv->fZoom = mapserv->Zoom = 1;
          mapserv->ZoomDirection = 0;
          mapserv->CoordSource = FROMSCALE;
          mapserv->ScaleDenom = mapserv->map->web.maxscaledenom;
          mapserv->mappnt.x = (mapserv->map->extent.maxx + mapserv->map->extent.minx)/2; /* use center of bad extent */
          mapserv->mappnt.y = (mapserv->map->extent.maxy + mapserv->map->extent.miny)/2;
          setExtent(mapserv);
          mapserv->map->cellsize = msAdjustExtent(&(mapserv->map->extent), mapserv->map->width, mapserv->map->height);
          if((status = msCalculateScale(mapserv->map->extent, mapserv->map->units, mapserv->map->width, mapserv->map->height, mapserv->map->resolution, &mapserv->map->scaledenom)) != MS_SUCCESS) return status;
        }
      }
    }
  
    return MS_SUCCESS;
  }
  
  int msReturnTemplateQuery(mapservObj *mapserv, char *queryFormat, char **papszBuffer)
  {
    imageObj *img = NULL;
    int i, status;
  
    outputFormatObj *outputFormat=NULL;
    mapObj *map = mapserv->map;
  
    if(!queryFormat) {
      msSetError(MS_WEBERR, "Return format/mime-type not specified.", "msReturnTemplateQuery()");
      return MS_FAILURE;
    }
  
    msApplyDefaultOutputFormats(map);
  
    i = msGetOutputFormatIndex(map, queryFormat); /* queryFormat can be a mime-type or name */
    if(i >= 0) outputFormat = map->outputformatlist[i];
  
    if(outputFormat) {
      if( MS_RENDERER_PLUGIN(outputFormat) ) {
        msInitializeRendererVTable(outputFormat);
      }
      
      /* if( MS_RENDERER_MVT(outputFormat) ) {
        if( mapserv != NULL )
  	checkWebScale(mapserv);
  
        status = msMVTWriteFromQuery(map, outputFormat, mapserv->sendheaders);
  
        return status;
        } */
  
      if( MS_RENDERER_OGR(outputFormat) ) {
        if( mapserv != NULL )
          checkWebScale(mapserv);
  
        status = msOGRWriteFromQuery(map, outputFormat, mapserv->sendheaders);
  
        return status;
      }
  
      if( !MS_RENDERER_TEMPLATE(outputFormat) ) { /* got an image format, return the query results that way */
        outputFormatObj *tempOutputFormat = map->outputformat; /* save format */
  
        if( mapserv != NULL )
          checkWebScale(mapserv);
  
        map->outputformat = outputFormat; /* override what was given for IMAGETYPE */
        img = msDrawMap(map, MS_TRUE);
        if(!img) return MS_FAILURE;
        map->outputformat = tempOutputFormat; /* restore format */
  
        if(mapserv == NULL || mapserv->sendheaders) {
          msIO_setHeader("Content-Type", "%s", MS_IMAGE_MIME_TYPE(outputFormat));
          msIO_sendHeaders();
        }
        status = msSaveImage(map, img, NULL);
        msFreeImage(img);
  
        return status;
      }
    }
  
    /*
    ** At this point we know we have a template of some sort, either the new style that references a or the old
    ** style made up of external files slammed together. Either way we may have to compute a query map and other
    ** images. We only create support images IF the querymap has status=MS_ON.
    */
    if(map->querymap.status && mapserv != NULL ) {
      checkWebScale(mapserv);
      if(msGenerateImages(mapserv, MS_TRUE, MS_TRUE) != MS_SUCCESS)
        return MS_FAILURE;
    }
  
    if(outputFormat) {
      const char *file = msGetOutputFormatOption( outputFormat, "FILE", NULL );
      if(!file) {
        msSetError(MS_WEBERR, "Template driver requires \"FILE\" format option be set.", "msReturnTemplateQuery()");
        return MS_FAILURE;
      }
  
      if(mapserv == NULL || mapserv->sendheaders) {
        const char *attachment = msGetOutputFormatOption( outputFormat, "ATTACHMENT", NULL );
        if(attachment)
          msIO_setHeader("Content-disposition","attachment; filename=%s", attachment);
        msIO_setHeader("Content-Type", "%s", outputFormat->mimetype);
        msIO_sendHeaders();
      }
      if((status = msReturnPage(mapserv, (char *) file, BROWSE, papszBuffer)) != MS_SUCCESS)
        return status;
    } else {
      if((status = msReturnNestedTemplateQuery(mapserv, queryFormat, papszBuffer)) != MS_SUCCESS)
        return status;
    }
  
    return MS_SUCCESS;
  }
  
  /*
  ** Is a particular layer or group on, that is was it requested explicitly by the user.
  */
  int isOn(mapservObj *mapserv, char *name, char *group)
  {
    int i;
  
    for(i=0; i<mapserv->NumLayers; i++) {
      if(name && strcmp(mapserv->Layers[i], name) == 0)  return(MS_TRUE);
      if(group && strcmp(mapserv->Layers[i], group) == 0) return(MS_TRUE);
    }
  
    return(MS_FALSE);
  }
  
  /************************************************************************/
  /*            int sortLayerByOrder(mapObj *map, char* pszOrder)         */
  /*                                                                      */
  /*      sorth the displaying in ascending or descending order.          */
  /************************************************************************/
  int sortLayerByOrder(mapObj *map, const char* pszOrder)
  {
    int *panCurrentOrder = NULL;
    int i = 0;
  
    if(!map) {
      msSetError(MS_WEBERR, "Invalid pointer.", "sortLayerByOrder()");
      return MS_FAILURE;
    }
    /* ==================================================================== */
    /*      The flag "ascending" is in fact not useful since the            */
    /*      default ordering is ascending.                                  */
    /* ==================================================================== */
  
    /* -------------------------------------------------------------------- */
    /*      the map->layerorder should be set at this point in the          */
    /*      sortLayerByMetadata.                                            */
    /* -------------------------------------------------------------------- */
    if(map->layerorder) {
      panCurrentOrder = (int*)msSmallMalloc(map->numlayers * sizeof(int));
      for (i=0; i<map->numlayers ; i++)
        panCurrentOrder[i] = map->layerorder[i];
  
      if(strcasecmp(pszOrder, "DESCENDING") == 0) {
        for (i=0; i<map->numlayers; i++)
          map->layerorder[i] = panCurrentOrder[map->numlayers-1-i];
      }
  
      free(panCurrentOrder);
    }
  
    return MS_SUCCESS;
  }
  
  
  /*!
   * This function set the map->layerorder
   * index order by the metadata collumn name
  */
  static int sortLayerByMetadata(mapObj *map, const char* pszMetadata)
  {
    int nLegendOrder1;
    int nLegendOrder2;
    int i, j;
    int tmp;
  
    if(!map) {
      msSetError(MS_WEBERR, "Invalid pointer.", "sortLayerByMetadata()");
      return MS_FAILURE;
    }
  
    /*
     * Initiate to default order (Reverse mapfile order)
     */
    if(map->layerorder) {
      int *pnLayerOrder;
  
      /* Backup the original layer order to be able to reverse it */
      pnLayerOrder = (int*)msSmallMalloc(map->numlayers * sizeof(int));
      for (i=0; i<map->numlayers ; i++)
        pnLayerOrder[i] = map->layerorder[i];
  
      /* Get a new layerorder array */
      free(map->layerorder);
      map->layerorder = (int*)msSmallMalloc(map->numlayers * sizeof(int));
  
      /* Reverse the layerorder array */
      for (i=0; i<map->numlayers ; i++)
        map->layerorder[i] = pnLayerOrder[map->numlayers - i - 1];
  
      free(pnLayerOrder);
    } else {
      map->layerorder = (int*)msSmallMalloc(map->numlayers * sizeof(int));
  
      for (i=0; i<map->numlayers ; i++)
        map->layerorder[i] = map->numlayers - i - 1;
    }
  
    if(!pszMetadata)
      return MS_SUCCESS;
  
    /*
     * Bubble sort algo (not very efficient)
     * should implement a kind of quick sort
     * alog instead
    */
    for (i=0; i<map->numlayers-1; i++) {
      for (j=0; j<map->numlayers-1-i; j++) {
        const char* pszLegendOrder1 = msLookupHashTable(&(GET_LAYER(map, map->layerorder[j+1])->metadata), pszMetadata);
        const char* pszLegendOrder2 = msLookupHashTable(&(GET_LAYER(map, map->layerorder[j])->metadata), pszMetadata);
  
        if(!pszLegendOrder1 || !pszLegendOrder2)
          continue;
  
        nLegendOrder1 = atoi(pszLegendOrder1);
        nLegendOrder2 = atoi(pszLegendOrder2);
  
        if(nLegendOrder1 < nLegendOrder2) {  /* compare the two neighbors */
          tmp = map->layerorder[j];         /* swap a[j] and a[j+1]      */
          map->layerorder[j] = map->layerorder[j+1];
          map->layerorder[j+1] = tmp;
        }
      }
    }
  
    return MS_SUCCESS;
  }
  
  /*
  ** This function return a pointer
  ** at the begining of the first occurence
  ** of pszTag in pszInstr.
  **
  ** Tag can be [TAG] or [TAG something]
  */
  char *findTag(char *pszInstr, char *pszTag)
  {
    char *pszTag1, *pszStart=NULL;
    char *pszTemp;
    int done=MS_FALSE;
    int length;
  
    if(!pszInstr || !pszTag) {
      msSetError(MS_WEBERR, "Invalid pointer.", "findTag()");
      return NULL;
    }
  
    length = strlen(pszTag) + 1; /* adding [ character to the beginning */
    pszTag1 = (char*) msSmallMalloc(length+1);
  
    strcpy(pszTag1, "[");
    strcat(pszTag1, pszTag);
  
    pszTemp = pszInstr;
    while(!done) {
      pszStart = strstr(pszTemp, pszTag1);
  
      if(pszStart == NULL)
        done = MS_TRUE; /* tag not found */
      else if((*(pszStart+length) == ']' || *(pszStart+length) == ' '))
        done = MS_TRUE; /* valid tag */
      else
        pszTemp += length; /* skip ahead and start over */
    }
  
    free(pszTag1);
  
    return pszStart;
  }
  
  /*
  ** This function return a pointer
  ** to the end of the tag in pszTag
  **
  ** The end of a tag is the next
  ** non-quoted ']' character.
  ** Return NULL if not found.
  */
  
  char *findTagEnd(const char *pszTag)
  {
    char *pszEnd = NULL,
          *pszTmp = (char*)pszTag;
  
    while (pszTmp != NULL) {
      if (*pszTmp == '"')
        pszTmp = strchr(pszTmp+1,'"');
      if ((pszTmp == NULL) || (*pszTmp == ']')) {
        pszEnd = pszTmp;
        pszTmp = NULL;
      } else
        pszTmp++;
    }
  
    return pszEnd;
  }
  
  /*
  ** Return a hashtableobj from instr of all
  ** arguments. hashtable must be freed by caller.
  */
  int getTagArgs(char* pszTag, char* pszInstr, hashTableObj **ppoHashTable)
  {
    char *pszStart, *pszEnd, *pszArgs;
    int nLength;
    char **papszArgs, **papszVarVal;
    int nArgs, nDummy;
    int i;
  
    if(!pszTag || !pszInstr) {
      msSetError(MS_WEBERR, "Invalid pointer.", "getTagArgs()");
      return MS_FAILURE;
    }
  
    /* set position to the begining of tag */
    pszStart = findTag(pszInstr, pszTag);
  
    if(pszStart) {
      /* find ending position */
      pszEnd = findTagEnd(pszStart);
  
      if(pszEnd) {
        /* skip the tag name */
        pszStart = pszStart + strlen(pszTag) + 1;
  
        /* get length of all args */
        nLength = pszEnd - pszStart;
  
        if(nLength > 0) { /* is there arguments ? */
          pszArgs = (char*)msSmallMalloc(nLength + 1);
          strlcpy(pszArgs, pszStart, nLength+1);
  
          if(!(*ppoHashTable))
            *ppoHashTable = msCreateHashTable();
  
          /* put all arguments seperate by space in a hash table */
          papszArgs = msStringTokenize(pszArgs, " ", &nArgs, MS_TRUE);
  
          /* msReturnTemplateQuerycheck all argument if they have values */
          for (i=0; i<nArgs; i++) {
            if(strlen(papszArgs[i]) == 0) {
              free(papszArgs[i]);
              continue;
            }
  
            if(strchr(papszArgs[i], '=')) {
              papszVarVal = msStringTokenize(papszArgs[i], "=", &nDummy, MS_FALSE);
              msInsertHashTable(*ppoHashTable, papszVarVal[0],
                                papszVarVal[1]);
              free(papszVarVal[0]);
              free(papszVarVal[1]);
              free(papszVarVal);
            } else /* no value specified. set it to 1 */
              msInsertHashTable(*ppoHashTable, papszArgs[i], "1");
  
            free(papszArgs[i]);
          }
          free(papszArgs);
          free(pszArgs);
        }
      }
    }
  
    return MS_SUCCESS;
  }
  
  /*
  ** Return a substring from instr between [tag] and [/tag]
  ** char * returned must be freed by caller.
  ** pszNextInstr will be a pointer at the end of the
  ** first occurence found.
  */
  int getInlineTag(char *pszTag, char *pszInstr, char **pszResult)
  {
    char *pszStart, *pszEnd=NULL,  *pszEndTag, *pszPatIn, *pszPatOut=NULL, *pszTmp;
    int nInst=0;
    int nLength;
  
    *pszResult = NULL;
  
    if(!pszInstr || !pszTag) {
      msSetError(MS_WEBERR, "Invalid pointer.", "getInlineTag()");
      return MS_FAILURE;
    }
  
    pszEndTag = (char*)msSmallMalloc(strlen(pszTag) + 3);
    strcpy(pszEndTag, "[/");
    strcat(pszEndTag, pszTag);
  
    /* find start tag */
    pszPatIn  = findTag(pszInstr, pszTag);
    pszPatOut = strstr(pszInstr, pszEndTag);
  
    pszStart = pszPatIn;
  
    pszTmp = pszInstr;
  
    if(pszPatIn) {
      do {
        if(pszPatIn && pszPatIn < pszPatOut) {
          nInst++;
  
          pszTmp = pszPatIn;
        }
  
        if(pszPatOut && ((pszPatIn == NULL) || pszPatOut < pszPatIn)) {
          pszEnd = pszPatOut;
          nInst--;
  
          pszTmp = pszPatOut;
        }
  
        pszPatIn  = findTag(pszTmp+1, pszTag);
        pszPatOut = strstr(pszTmp+1, pszEndTag);
  
      } while (pszTmp != NULL && nInst > 0);
    }
  
    if(pszStart && pszEnd) {
      /* find end of start tag */
      pszStart = strchr(pszStart, ']');
  
      if(pszStart) {
        pszStart++;
  
        nLength = pszEnd - pszStart;
  
        if(nLength > 0) {
          *pszResult = (char*)msSmallMalloc(nLength + 1);
  
          /* copy string beetween start and end tag */
          strlcpy(*pszResult, pszStart, nLength+1);
  
          (*pszResult)[nLength] = '\0';
        }
      } else {
        msSetError(MS_WEBERR, "Malformed [%s] tag.", "getInlineTag()", pszTag);
        return MS_FAILURE;
      }
    }
  
    msFree(pszEndTag);
  
    return MS_SUCCESS;
  }
  
  /*!
   * this function process all if tag in pszInstr.
   * this function return a modified pszInstr.
   * ht mus contain all variables needed by the function
   * to interpret if expression.
   *
   * If bLastPass is true then all tests for 'null' values will be
   * considered true if the corresponding value is not set.
  */
  int processIfTag(char **pszInstr, hashTableObj *ht, int bLastPass)
  {
    /*   char *pszNextInstr = pszInstr; */
    char *pszStart, *pszEnd=NULL;
    const char *pszName, *pszValue, *pszOperator, *pszHTValue;
    char *pszThen=NULL;
    char *pszIfTag;
    char *pszPatIn=NULL, *pszPatOut=NULL, *pszTmp;
    int nInst = 0;
    int bEmpty = 0;
    int nLength;
  
    hashTableObj *ifArgs=NULL;
  
    if(!*pszInstr) {
      msSetError(MS_WEBERR, "Invalid pointer.", "processIfTag()");
      return MS_FAILURE;
    }
  
    /* find the if start tag */
  
    pszStart  = findTag(*pszInstr, "if");
  
    while (pszStart) {
      pszPatIn  = findTag(pszStart, "if");
      pszPatOut = strstr(pszStart, "[/if]");
      pszTmp = pszPatIn;
  
      do {
        if(pszPatIn && pszPatIn < pszPatOut) {
          nInst++;
          pszTmp = pszPatIn;
        }
  
        if(pszPatOut && ((pszPatIn == NULL) || pszPatOut < pszPatIn)) {
          pszEnd = pszPatOut;
          nInst--;
          pszTmp = pszPatOut;
        }
  
        pszPatIn  = findTag(pszTmp+1, "if");
        pszPatOut = strstr(pszTmp+1, "[/if]");
  
      } while (pszTmp != NULL && nInst > 0);
  
      /* get the then string (if expression is true) */
      if(getInlineTag("if", pszStart, &pszThen) != MS_SUCCESS) {
        msSetError(MS_WEBERR, "Malformed then if tag.", "processIfTag()");
        return MS_FAILURE;
      }
  
      /* retrieve if tag args */
      if(getTagArgs("if", pszStart, &ifArgs) != MS_SUCCESS) {
        msSetError(MS_WEBERR, "Malformed args if tag.", "processIfTag()");
        return MS_FAILURE;
      }
  
      pszName = msLookupHashTable(ifArgs, "name");
      pszValue = msLookupHashTable(ifArgs, "value");
      pszOperator = msLookupHashTable(ifArgs, "oper");
      if(pszOperator == NULL) /* Default operator if not set is "eq" */
        pszOperator = "eq";
  
      bEmpty = 0;
  
      if(pszName) {
        /* build the complete if tag ([if all_args]then string[/if]) */
        /* to replace if by then string if expression is true */
        /* or by a white space if not. */
        nLength = pszEnd - pszStart;
        pszIfTag = (char*)msSmallMalloc(nLength + 6);
        strlcpy(pszIfTag, pszStart, nLength+1);
        pszIfTag[nLength] = '\0';
        strcat(pszIfTag, "[/if]");
  
        pszHTValue = msLookupHashTable(ht, pszName);
  
        if(strcmp(pszOperator, "neq") == 0) {
          if(pszValue && pszHTValue && strcasecmp(pszValue, pszHTValue) != 0) {
            *pszInstr = msReplaceSubstring(*pszInstr, pszIfTag, pszThen);
          } else if(pszHTValue) {
            *pszInstr = msReplaceSubstring(*pszInstr, pszIfTag, "");
            bEmpty = 1;
          }
        } else if(strcmp(pszOperator, "eq") == 0) {
          if(pszValue && pszHTValue && strcasecmp(pszValue, pszHTValue) == 0) {
            *pszInstr = msReplaceSubstring(*pszInstr, pszIfTag, pszThen);
          } else if(pszHTValue) {
            *pszInstr = msReplaceSubstring(*pszInstr, pszIfTag, "");
            bEmpty = 1;
          }
        } else if(strcmp(pszOperator, "isnull") == 0) {
          if(pszHTValue != NULL) {
            /* We met a non-null value... condition is false */
            *pszInstr = msReplaceSubstring(*pszInstr, pszIfTag, "");
            bEmpty = 1;
          } else if(bLastPass) {
            /* On last pass, if value is still null then condition is true */
            *pszInstr = msReplaceSubstring(*pszInstr, pszIfTag, pszThen);
          }
        } else if(strcmp(pszOperator, "isset") == 0) {
          if(pszHTValue != NULL) {
            /* Found a non-null value... condition is true */
            *pszInstr = msReplaceSubstring(*pszInstr, pszIfTag, pszThen);
          } else if(bLastPass) {
            /* On last pass, if value still not set then condition is false */
            *pszInstr = msReplaceSubstring(*pszInstr, pszIfTag, "");
            bEmpty = 1;
          }
        } else {
          msSetError(MS_WEBERR, "Unsupported operator (%s) in if tag.",  "processIfTag()", pszOperator);
          return MS_FAILURE;
        }
  
        if(pszIfTag)
          free(pszIfTag);
  
        pszIfTag = NULL;
      }
  
      if(pszThen)
        free (pszThen);
  
      pszThen=NULL;
  
      msFreeHashTable(ifArgs);
      ifArgs=NULL;
  
      /* find the if start tag */
      if(bEmpty)
        pszStart = findTag(pszStart, "if");
      else
        pszStart = findTag(pszStart + 1, "if");
    }
  
    return MS_SUCCESS;
  }
  
  /* Helper function to return the text before the supplied string2 in string1. */
  static char *getPreTagText(const char *string1, const char *string2)
  {
    int n;
    char *result, *tmpstr;
  
    if((tmpstr = strstr(string1, string2)) == NULL) return msStrdup(""); /* return an empty string */
  
    n = strlen(string1) - strlen(tmpstr);
    result = (char *) msSmallMalloc(n + 1);
    strlcpy(result, string1, n+1);
  
    return result;
  }
  
  /* Helper function to retunr the text after the supplied string2 in string1. */
  static char *getPostTagText(const char *string1, const char *string2)
  {
    char *tmpstr;
  
    if((tmpstr = strstr(string1, string2)) == NULL) return msStrdup(""); /* return an empty string */
  
    tmpstr += strlen(string2); /* skip string2 */
    return msStrdup(tmpstr);
  }
  
  /*
  ** Function to process a [feature ...] tag. This tag can *only* be found within
  ** a [resultset ...][/resultset] block.
  */
  static int processFeatureTag(mapservObj *mapserv, char **line, layerObj *layer)
  {
    char *preTag, *postTag; /* text before and after the tag */
  
    const char *argValue;
    char *tag, *tagInstance, *tagStart;
    hashTableObj *tagArgs=NULL;
  
    int limit=-1;
    const char *trimLast=NULL;
  
    int i, j, status;
  
    if(!*line) {
      msSetError(MS_WEBERR, "Invalid line pointer.", "processFeatureTag()");
      return(MS_FAILURE);
    }
  
    tagStart = findTag(*line, "feature");
    if(!tagStart) return(MS_SUCCESS); /* OK, just return; */
  
    /* check for any tag arguments */
    if(getTagArgs("feature", tagStart, &tagArgs) != MS_SUCCESS) return(MS_FAILURE);
    if(tagArgs) {
      argValue = msLookupHashTable(tagArgs, "limit");
      if(argValue) limit = atoi(argValue);
  
      argValue = msLookupHashTable(tagArgs, "trimlast");
      if(argValue) trimLast = argValue;
    }
  
    if(strstr(*line, "[/feature]") == NULL) { /* we know the closing tag must be here, if not throw an error */
      msSetError(MS_WEBERR, "[feature] tag found without closing [/feature].", "processFeatureTag()");
      msFreeHashTable(tagArgs);
      return(MS_FAILURE);
    }
  
    if(getInlineTag("feature", *line, &tag) != MS_SUCCESS) {
      msSetError(MS_WEBERR, "Malformed feature tag.", "processFeatureTag()");
      msFreeHashTable(tagArgs);
      return MS_FAILURE;
    }
  
    preTag = getPreTagText(*line, "[feature");
    postTag = getPostTagText(*line, "[/feature]");
  
    /* start rebuilding **line */
    free(*line);
    *line = preTag;
  
    /* we know the layer has query results or we wouldn't be in this code */
  
  #if 0
    status = msLayerOpen(layer); /* open the layer */
    if(status != MS_SUCCESS) return status;
    status = msLayerGetItems(layer); /* retrieve all the item names */
    if(status != MS_SUCCESS) return status;
  #endif
  
    if(layer->numjoins > 0) { /* initialize necessary JOINs here */
      for(j=0; j<layer->numjoins; j++) {
        status = msJoinConnect(layer, &(layer->joins[j]));
        if(status != MS_SUCCESS) {
          msFreeHashTable(tagArgs);
          msFree(postTag);
          msFree(tag);
          return status;
        }
      }
    }
  
    mapserv->LRN = 1; /* layer result counter */
    mapserv->resultlayer = layer;
    msInitShape(&(mapserv->resultshape));
  
    if(limit == -1) /* return all */
      limit = layer->resultcache->numresults;
    else
      limit = MS_MIN(limit, layer->resultcache->numresults);
  
    for(i=0; i<limit; i++) {
      status = msLayerGetShape(layer, &(mapserv->resultshape), &(layer->resultcache->results[i]));
      if(status != MS_SUCCESS) {
        msFreeHashTable(tagArgs);
        msFree(postTag);
        msFree(tag);
        return status;
      }
  
      mapserv->resultshape.classindex = msShapeGetClass(layer, layer->map, &mapserv->resultshape,  NULL, -1);
  
      /* prepare any necessary JOINs here (one-to-one only) */
      if(layer->numjoins > 0) {
        for(j=0; j<layer->numjoins; j++) {
          if(layer->joins[j].type == MS_JOIN_ONE_TO_ONE) {
            msJoinPrepare(&(layer->joins[j]), &(mapserv->resultshape));
            msJoinNext(&(layer->joins[j])); /* fetch the first row */
          }
        }
      }
  
      /*
      ** if necessary trim a few characters off the end of the tag
      */
      if(trimLast && (i == limit-1)) {
        char *ptr;
        if((ptr = strrstr(tag, trimLast)) != NULL)
          *ptr = '\0';
      }
  
      /* process the tag */
      tagInstance = processLine(mapserv, tag, NULL, QUERY); /* do substitutions */
      *line = msStringConcatenate(*line, tagInstance); /* grow the line */
  
      free(tagInstance);
      msFreeShape(&(mapserv->resultshape)); /* init too */
  
      mapserv->RN++; /* increment counters */
      mapserv->LRN++;
    }
  
    /* msLayerClose(layer); */
    mapserv->resultlayer = NULL; /* necessary? */
  
    *line = msStringConcatenate(*line, postTag);
  
    /*
    ** clean up
    */
    free(postTag);
    free(tag);
    msFreeHashTable(tagArgs);
  
    return(MS_SUCCESS);
  }
  
  /*
  ** Function to process a [resultset ...] tag.
  */
  static int processResultSetTag(mapservObj *mapserv, char **line, FILE *stream)
  {
    char lineBuffer[MS_BUFFER_LENGTH];
    int foundTagEnd;
  
    char *preTag, *postTag; /* text before and after the tag */
  
    char *tag, *tagStart;
    hashTableObj *tagArgs=NULL;
  
    const char *layerName=NULL;
    const char *nodata=NULL;
  
    int layerIndex=-1;
    layerObj *lp;
  
    if(!*line) {
      msSetError(MS_WEBERR, "Invalid line pointer.", "processResultSetTag()");
      return(MS_FAILURE);
    }
  
    tagStart = findTag(*line, "resultset");
    if(!tagStart) return(MS_SUCCESS); /* OK, just return; */
  
    while (tagStart) {
      /* initialize the tag arguments */
      layerName = NULL;
  
  
      /* check for any tag arguments */
      if(getTagArgs("resultset", tagStart, &tagArgs) != MS_SUCCESS) return(MS_FAILURE);
      if(tagArgs) {
        layerName = msLookupHashTable(tagArgs, "layer");
        nodata = msLookupHashTable(tagArgs, "nodata");
      }
  
      if(!layerName) {
        msSetError(MS_WEBERR, "[resultset] tag missing required 'layer' argument.", "processResultSetTag()");
        msFreeHashTable(tagArgs);
        return(MS_FAILURE);
      }
  
      layerIndex = msGetLayerIndex(mapserv->map, layerName);
      if(layerIndex>=mapserv->map->numlayers || layerIndex<0) {
        msSetError(MS_MISCERR, "Layer named '%s' does not exist.", "processResultSetTag()", layerName);
        msFreeHashTable(tagArgs);
        return MS_FAILURE;
      }
      lp = GET_LAYER(mapserv->map, layerIndex);
  
      if(strstr(*line, "[/resultset]") == NULL) { /* read ahead */
        if(!stream) {
          msSetError(MS_WEBERR, "Invalid file pointer.", "processResultSetTag()");
          msFreeHashTable(tagArgs);
          return(MS_FAILURE);
        }
  
        foundTagEnd = MS_FALSE;
        while(!foundTagEnd) {
          if(fgets(lineBuffer, MS_BUFFER_LENGTH, stream) != NULL) {
            *line = msStringConcatenate(*line, lineBuffer);
            if(strstr(*line, "[/resultset]") != NULL)
              foundTagEnd = MS_TRUE;
          } else
            break; /* ran out of file */
        }
        if(foundTagEnd == MS_FALSE) {
          msSetError(MS_WEBERR, "[resultset] tag found without closing [/resultset].", "processResultSetTag()");
          msFreeHashTable(tagArgs);
          return(MS_FAILURE);
        }
      }
  
      if(getInlineTag("resultset", *line, &tag) != MS_SUCCESS) {
        msSetError(MS_WEBERR, "Malformed resultset tag.", "processResultSetTag()");
        msFreeHashTable(tagArgs);
        return MS_FAILURE;
      }
  
      preTag = getPreTagText(*line, "[resultset"); /* TODO: need to handle tags in these */
      postTag = getPostTagText(*line, "[/resultset]");
  
      /* start rebuilding **line */
      free(*line);
      *line = preTag;
  
      if(lp->resultcache && lp->resultcache->numresults > 0) {
        /* probably will need a while-loop here to handle multiple instances of [feature ...] tags */
        if(processFeatureTag(mapserv, &tag, lp) != MS_SUCCESS) {
          msFreeHashTable(tagArgs);
          return(MS_FAILURE); /* TODO: how to handle */
        }
        *line = msStringConcatenate(*line, tag);
      } else if(nodata) {
        *line = msStringConcatenate(*line, nodata);
      }
  
      *line = msStringConcatenate(*line, postTag);
  
      /* clean up */
      free(postTag);
      free(tag);
  
      tagStart = findTag(*line, "resultset");
    }
    msFreeHashTable(tagArgs);
  
    return(MS_SUCCESS);
  }
  
  /*
  ** Function process a [include src="..."] tag.
  **
  ** TODO's:
  **   - allow URLs
  */
  static int processIncludeTag(mapservObj *mapserv, char **line, FILE *stream, int mode)
  {
    char *tag, *tagStart, *tagEnd;
    hashTableObj *tagArgs=NULL;
    int tagOffset, tagLength;
  
    char *content=NULL, *processedContent=NULL;
    const char *src=NULL;
  
    FILE *includeStream;
    char buffer[MS_BUFFER_LENGTH], path[MS_MAXPATHLEN];
  
    if(!*line) {
      msSetError(MS_WEBERR, "Invalid line pointer.", "processIncludeTag()");
      return(MS_FAILURE);
    }
  
    tagStart = findTag(*line, "include");
  
    /* It is OK to have no include tags, just return. */
    if( !tagStart ) return MS_SUCCESS;
  
    while( tagStart ) {
      tagOffset = tagStart - *line;
  
      /* check for any tag arguments */
      if(getTagArgs("include", tagStart, &tagArgs) != MS_SUCCESS) return(MS_FAILURE);
      if(tagArgs) {
        src = msLookupHashTable(tagArgs, "src");
      }
  
      if(!src) return(MS_SUCCESS); /* don't process the tag, could be something else so return MS_SUCCESS */
  
      if((includeStream = fopen(msBuildPath(path, mapserv->map->mappath, src), "r")) == NULL) {
        msSetError(MS_IOERR, "%s", "processIncludeTag()", src);
        return MS_FAILURE;
      }
  
      if(isValidTemplate(includeStream, src) != MS_TRUE) {
        fclose(includeStream);
        return MS_FAILURE;
      }
  
      while(fgets(buffer, MS_BUFFER_LENGTH, includeStream) != NULL)
        content = msStringConcatenate(content, buffer);
  
      /* done with included file handle */
      fclose(includeStream);
  
      /* find the end of the tag */
      tagEnd = findTagEnd(tagStart);
      tagEnd++;
  
      /* build the complete tag so we can do substitution */
      tagLength = tagEnd - tagStart;
      tag = (char *) msSmallMalloc(tagLength + 1);
      strlcpy(tag, tagStart, tagLength+1);
  
      /* process any other tags in the content */
      processedContent = processLine(mapserv, content, stream, mode);
  
      /* do the replacement */
      *line = msReplaceSubstring(*line, tag, processedContent);
  
      /* clean up */
      free(tag);
      tag = NULL;
      msFreeHashTable(tagArgs);
      tagArgs=NULL;
      free(content);
      free(processedContent);
  
      if((*line)[tagOffset] != '\0')
        tagStart = findTag(*line+tagOffset+1, "include");
      else
        tagStart = NULL;
    }
  
    return(MS_SUCCESS);
  }
  
  /*
  ** Function to process an [item ...] tag: line contains the tag, shape holds the attributes.
  */
  enum ITEM_ESCAPING {ESCAPE_HTML, ESCAPE_URL, ESCAPE_JSON, ESCAPE_NONE};
  
  static int processItemTag(layerObj *layer, char **line, shapeObj *shape)
  {
    int i, j;
  
    char *tag, *tagStart, *tagEnd;
    hashTableObj *tagArgs=NULL;
    int tagLength;
    char *encodedTagValue=NULL, *tagValue=NULL;
  
    const char *argValue=NULL;
  
    const char *name=NULL, *pattern=NULL;
    const char *format=NULL, *nullFormat=NULL;
    int precision;
    int padding;
    int uc, lc, commify;
    int escape;
  
    if(!*line) {
      msSetError(MS_WEBERR, "Invalid line pointer.", "processItemTag()");
      return(MS_FAILURE);
    }
  
    tagStart = findTag(*line, "item");
  
    if(!tagStart) return(MS_SUCCESS); /* OK, just return; */
  
    while (tagStart) {
      format = "$value"; /* initialize the tag arguments */
      nullFormat = "";
      precision = -1;
      padding = -1;
      name = pattern = NULL;
      uc = lc = commify = MS_FALSE;
      escape=ESCAPE_HTML;
  
      /* check for any tag arguments */
      if(getTagArgs("item", tagStart, &tagArgs) != MS_SUCCESS) return(MS_FAILURE);
      if(tagArgs) {
        argValue = msLookupHashTable(tagArgs, "name");
        if(argValue) name = argValue;
  
        argValue = msLookupHashTable(tagArgs, "pattern");
        if(argValue) pattern = argValue;
  
        argValue = msLookupHashTable(tagArgs, "precision");
        if(argValue) precision = atoi(argValue);
  
        argValue = msLookupHashTable(tagArgs, "padding");
        if (argValue) padding = atoi(argValue);
  
        argValue = msLookupHashTable(tagArgs, "format");
        if(argValue) format = argValue;
  
        argValue = msLookupHashTable(tagArgs, "nullformat");
        if(argValue) nullFormat = argValue;
  
        argValue = msLookupHashTable(tagArgs, "uc");
        if(argValue && strcasecmp(argValue, "true") == 0) uc = MS_TRUE;
  
        argValue = msLookupHashTable(tagArgs, "lc");
        if(argValue && strcasecmp(argValue, "true") == 0) lc = MS_TRUE;
  
        argValue = msLookupHashTable(tagArgs, "commify");
        if(argValue && strcasecmp(argValue, "true") == 0) commify = MS_TRUE;
  
        argValue = msLookupHashTable(tagArgs, "escape");
        if(argValue && strcasecmp(argValue, "url") == 0) escape = ESCAPE_URL;
        else if(argValue && strcasecmp(argValue, "none") == 0) escape = ESCAPE_NONE;
        else if(argValue && strcasecmp(argValue, "json") == 0) escape = ESCAPE_JSON;
  
        /* TODO: deal with sub strings */
      }
  
      if(!name) {
        msSetError(MS_WEBERR, "Item tag contains no name attribute.", "processItemTag()");
        return(MS_FAILURE);
      }
  
      for(i=0; i<layer->numitems; i++)
        if(strcasecmp(name, layer->items[i]) == 0) break;
  
      if(i == layer->numitems) {
        msSetError(MS_WEBERR, "Item name (%s) not found in layer item list.", "processItemTag()", name);
        return(MS_FAILURE);
      }
  
      /*
      ** now we know which item so build the tagValue
      */
      if(shape->values[i] && strlen(shape->values[i]) > 0) {
        char *itemValue=NULL;
  
        /* set tag text depending on pattern (if necessary), nullFormat can contain $value (#3637) */
        if(pattern && msEvalRegex(pattern, shape->values[i]) != MS_TRUE)
          tagValue = msStrdup(nullFormat);
        else
          tagValue = msStrdup(format);
  
        if(precision != -1) {
          char numberFormat[16];
  
          itemValue = (char *) msSmallMalloc(64); /* plenty big */
          snprintf(numberFormat, sizeof(numberFormat), "%%.%dlf", precision);
          snprintf(itemValue, 64, numberFormat, atof(shape->values[i]));
        } else
          itemValue = msStrdup(shape->values[i]);
  
        if(commify == MS_TRUE)
          itemValue = msCommifyString(itemValue);
  
        /* apply other effects */
        if(uc == MS_TRUE)
          for(j=0; j<strlen(itemValue); j++) itemValue[j] = toupper(itemValue[j]);
        if(lc == MS_TRUE)
          for(j=0; j<strlen(itemValue); j++) itemValue[j] = tolower(itemValue[j]);
  
        tagValue = msReplaceSubstring(tagValue, "$value", itemValue);
        msFree(itemValue);
  
        if (padding > 0 && padding < 1000) {
            int paddedSize = strlen(tagValue) + padding + 1;
            char *paddedValue = NULL;
            paddedValue = (char *) msSmallMalloc(paddedSize);
            snprintf(paddedValue, paddedSize, "%-*s", padding, tagValue);
            msFree(tagValue);
            tagValue = paddedValue;
        }
  
        if(!tagValue) {
          msSetError(MS_WEBERR, "Error applying item format.", "processItemTag()");
          return(MS_FAILURE); /* todo leaking... */
        }
      } else {
        tagValue = msStrdup(nullFormat); /* attribute value is NULL or empty */
      }
  
      /* find the end of the tag */
      tagEnd = findTagEnd(tagStart);
      tagEnd++;
  
      /* build the complete tag so we can do substitution */
      tagLength = tagEnd - tagStart;
      tag = (char *) msSmallMalloc(tagLength + 1);
      strlcpy(tag, tagStart, tagLength+1);
  
      /* do the replacement */
      switch(escape) {
        case ESCAPE_HTML:
          encodedTagValue = msEncodeHTMLEntities(tagValue);
          *line = msReplaceSubstring(*line, tag, encodedTagValue);
          break;
        case ESCAPE_JSON:
          encodedTagValue = msEscapeJSonString(tagValue);
          *line = msReplaceSubstring(*line, tag, encodedTagValue);
          break;
        case ESCAPE_URL:
          encodedTagValue = msEncodeUrl(tagValue);
          *line = msReplaceSubstring(*line, tag, encodedTagValue);
          break;
        case ESCAPE_NONE:
          *line = msReplaceSubstring(*line, tag, tagValue);
          break;
        default:
          break;
      }
  
      /* clean up */
      free(tag);
      tag = NULL;
      msFreeHashTable(tagArgs);
      tagArgs=NULL;
      msFree(tagValue);
      tagValue=NULL;
      msFree(encodedTagValue);
      encodedTagValue=NULL;
  
      tagStart = findTag(*line, "item");
    }
  
    return(MS_SUCCESS);
  }
  
  /*
  ** Function process any number of MapServer extent tags (e.g. shpext, mapext, etc...).
  */
  static int processExtentTag(mapservObj *mapserv, char **line, char *name, rectObj *extent, projectionObj *rectProj)
  {
    char *tag, *tagStart, *tagEnd;
    hashTableObj *tagArgs=NULL;
    int tagOffset, tagLength;
  
    char *encodedTagValue=NULL, *tagValue=NULL;
  
    rectObj tempExtent;
    int escape;
  
    char number[64]; /* holds a single number in the extent */
    char numberFormat[16];
  
    if(!*line) {
      msSetError(MS_WEBERR, "Invalid line pointer.", "processExtentTag()");
      return(MS_FAILURE);
    }
  
    tagStart = findTag(*line, name); /* this supports any extent */
  
    /* It is OK to have no include tags, just return. */
    if(!tagStart) return MS_SUCCESS;
  
    /* hack to handle tags like 'mapext_esc' easily */
    if(strstr(name, "_esc")) escape = ESCAPE_URL;
  
    while(tagStart) {
      double xExpand = 0, yExpand = 0; /* set tag argument defaults */
      int precision = -1;
      const char* format = "$minx $miny $maxx $maxy";
      const char* projectionString = NULL;
  
      if(strstr(name, "_esc"))
        escape = ESCAPE_URL;
      else
        escape = ESCAPE_HTML;
  
      tagOffset = tagStart - *line;
  
      /* check for any tag arguments */
      if(getTagArgs(name, tagStart, &tagArgs) != MS_SUCCESS) return(MS_FAILURE);
      if(tagArgs) {
        const char* argValue = msLookupHashTable(tagArgs, "expand");
        if(argValue) {
          if(strchr(argValue, '%') != NULL) {
            float f;
            sscanf(argValue, "%f%%", &f);
            xExpand = ((f/100.0)*(extent->maxx-extent->minx))/2;
            yExpand = ((f/100.0)*(extent->maxy-extent->miny))/2;
          } else {
            xExpand = atof(argValue);
            yExpand = xExpand;
          }
        }
  
        argValue = msLookupHashTable(tagArgs, "escape");
        if(argValue && strcasecmp(argValue, "url") == 0) escape = ESCAPE_URL;
        else if(argValue && strcasecmp(argValue, "none") == 0) escape = ESCAPE_NONE;
  
        argValue = msLookupHashTable(tagArgs, "format");
        if(argValue) format = argValue;
  
        argValue = msLookupHashTable(tagArgs, "precision");
        if(argValue) precision = atoi(argValue);
  
        argValue = msLookupHashTable(tagArgs, "proj");
        if(argValue) projectionString = argValue;
      }
  
      tempExtent.minx = extent->minx - xExpand;
      tempExtent.miny = extent->miny - yExpand;
      tempExtent.maxx = extent->maxx + xExpand;
      tempExtent.maxy = extent->maxy + yExpand;
  
      /* no big deal to convert from file to image coordinates, but what are the image parameters */
      if(rectProj && projectionString && strcasecmp(projectionString,"image") == 0) {
        precision = 0;
  
        /* if necessary, project the shape to match the map */
        if(msProjectionsDiffer(rectProj, &(mapserv->map->projection)))
          msProjectRect(rectProj, &mapserv->map->projection, &tempExtent);
  
        /* convert tempExtent to image coordinates based on the map extent and cellsize */
        tempExtent.minx = MS_MAP2IMAGE_X(tempExtent.minx, mapserv->map->extent.minx, mapserv->map->cellsize);
        tempExtent.miny = MS_MAP2IMAGE_Y(tempExtent.miny, mapserv->map->extent.maxy, mapserv->map->cellsize);
        tempExtent.maxx = MS_MAP2IMAGE_X(tempExtent.minx, mapserv->map->extent.minx, mapserv->map->cellsize);
        tempExtent.maxy = MS_MAP2IMAGE_Y(tempExtent.miny, mapserv->map->extent.maxy, mapserv->map->cellsize);
      } else if(rectProj && projectionString) {
        projectionObj projection;
        msInitProjection(&projection);
        msProjectionInheritContextFrom(&projection, &mapserv->map->projection);
  
        if(MS_SUCCESS != msLoadProjectionString(&projection, projectionString)) return MS_FAILURE;
  
        if(msProjectionsDiffer(rectProj, &projection))
          msProjectRect(rectProj, &projection, &tempExtent);
      }
  
      tagValue = msStrdup(format);
  
      if(precision != -1)
        snprintf(numberFormat, sizeof(numberFormat), "%%.%dlf", precision);
      else
        snprintf(numberFormat, sizeof(numberFormat), "%%f");
  
      snprintf(number, sizeof(number), numberFormat, tempExtent.minx);
      tagValue = msReplaceSubstring(tagValue, "$minx", number);
      snprintf(number, sizeof(number), numberFormat, tempExtent.miny);
      tagValue = msReplaceSubstring(tagValue, "$miny", number);
      snprintf(number, sizeof(number), numberFormat, tempExtent.maxx);
      tagValue = msReplaceSubstring(tagValue, "$maxx", number);
      snprintf(number, sizeof(number), numberFormat, tempExtent.maxy);
      tagValue = msReplaceSubstring(tagValue, "$maxy", number);
  
      /* find the end of the tag */
      tagEnd = findTagEnd(tagStart);
      tagEnd++;
  
      /* build the complete tag so we can do substitution */
      tagLength = tagEnd - tagStart;
      tag = (char *) msSmallMalloc(tagLength + 1);
      strlcpy(tag, tagStart, tagLength+1);
  
      /* do the replacement */
      switch(escape) {
        case ESCAPE_HTML:
          encodedTagValue = msEncodeHTMLEntities(tagValue);
          *line = msReplaceSubstring(*line, tag, encodedTagValue);
          break;
        case ESCAPE_URL:
          encodedTagValue = msEncodeUrl(tagValue);
          *line = msReplaceSubstring(*line, tag, encodedTagValue);
          break;
        case ESCAPE_NONE:
          *line = msReplaceSubstring(*line, tag, tagValue);
          break;
        default:
          break;
      }
  
      /* clean up */
      free(tag);
      tag = NULL;
      msFreeHashTable(tagArgs);
      tagArgs=NULL;
      msFree(tagValue);
      tagValue=NULL;
      msFree(encodedTagValue);
      encodedTagValue=NULL;
  
      if((*line)[tagOffset] != '\0')
        tagStart = findTag(*line+tagOffset+1, name);
      else
        tagStart = NULL;
    }
  
    return(MS_SUCCESS);
  }
  
  // RFC 77 TODO: Need to validate these changes with Assefa...
  static int processShplabelTag(layerObj *layer, char **line, shapeObj *origshape)
  {
    char *tag, *tagStart, *tagEnd;
    char *tagValue=NULL;
    hashTableObj *tagArgs=NULL;
    int tagOffset, tagLength;
    const char *format;
    const char *argValue=NULL;
    const char *projectionString=NULL;
    shapeObj tShape;
    int precision=0;
    int clip_to_map=MS_TRUE;
    int use_label_settings=MS_FALSE;
    double cellsize=0;
    int labelposvalid = MS_FALSE;
    pointObj labelPos;
    int status;
    char number[64]; /* holds a single number in the extent */
    char numberFormat[16];
    shapeObj *shape = NULL;
  
    if(!*line) {
      msSetError(MS_WEBERR, "Invalid line pointer.", "processShplabelTag()");
      return(MS_FAILURE);
    }
    if(msCheckParentPointer(layer->map,"map") == MS_FAILURE)
      return MS_FAILURE;
  
    tagStart = findTag(*line, "shplabel");
  
    /* It is OK to have no shplabel tags, just return. */
    if(!tagStart)
      return MS_SUCCESS;
  
    if(!origshape || origshape->numlines <= 0) { /* I suppose we need to make sure the part has vertices (need shape checker?) */
      msSetError(MS_WEBERR, "Null or empty shape.", "processShplabelTag()");
      return(MS_FAILURE);
    }
  
    while(tagStart) {
      if(shape) msFreeShape(shape);
      shape = (shapeObj *) msSmallMalloc(sizeof(shapeObj));
      msInitShape(shape);
      msCopyShape(origshape, shape);
  
      projectionString = NULL;
      format = "$x,$y";
      tagOffset = tagStart - *line;
  
      if(getTagArgs("shplabel", tagStart, &tagArgs) != MS_SUCCESS) return(MS_FAILURE);
      if(tagArgs) {
        argValue = msLookupHashTable(tagArgs, "format");
        if(argValue) format = argValue;
  
        argValue = msLookupHashTable(tagArgs, "precision");
        if(argValue) precision = atoi(argValue);
  
        argValue = msLookupHashTable(tagArgs, "proj");
        if(argValue) projectionString = argValue;
  
        argValue = msLookupHashTable(tagArgs, "clip_to_map");
        if(argValue) {
          if(strcasecmp(argValue,"false") == 0) clip_to_map = MS_FALSE;
        }
  
        argValue = msLookupHashTable(tagArgs, "use_label_settings");
        if(argValue) {
          if(strcasecmp(argValue,"true") == 0) use_label_settings = MS_TRUE;
        }
      }
  
      labelPos.x = -1;
      labelPos.y = -1;
      msInitShape(&tShape);
  
      tShape.type = MS_SHAPE_LINE;
      tShape.line = (lineObj *) msSmallMalloc(sizeof(lineObj));
      tShape.numlines = 1;
      tShape.line[0].point = NULL; /* initialize the line */
      tShape.line[0].numpoints = 0;
  
      if(layer->map->cellsize <= 0)
        cellsize = MS_MAX(MS_CELLSIZE(layer->map->extent.minx, layer->map->extent.maxx, layer->map->width), MS_CELLSIZE(layer->map->extent.miny, layer->map->extent.maxy, layer->map->height));
      else
        cellsize = layer->map->cellsize ;
  
      if(shape->type == MS_SHAPE_POINT) {
        labelposvalid = MS_FALSE;
        if(shape->numlines > 0 && shape->line[0].numpoints > 0) {
          labelposvalid = MS_TRUE;
          labelPos = shape->line[0].point[0];
          if(layer->transform == MS_TRUE) {
            if(layer->project && msProjectionsDiffer(&(layer->projection), &(layer->map->projection)))
            {
              if( layer->reprojectorLayerToMap == NULL )
              {
                  layer->reprojectorLayerToMap = msProjectCreateReprojector(
                      &layer->projection, &layer->map->projection);
              }
              if( layer->reprojectorLayerToMap )
              {
                  msProjectShapeEx(layer->reprojectorLayerToMap, shape);
              }
            }
  
            labelPos = shape->line[0].point[0];
            labelPos.x = MS_MAP2IMAGE_X(labelPos.x, layer->map->extent.minx, cellsize);
            labelPos.y = MS_MAP2IMAGE_Y(labelPos.y, layer->map->extent.maxy, cellsize);
          }
        }
      } else if(shape->type == MS_SHAPE_LINE) {
        labelposvalid = MS_FALSE;
        if(layer->transform == MS_TRUE) {
          if(layer->project && msProjectionsDiffer(&(layer->projection), &(layer->map->projection)))
          {
              if( layer->reprojectorLayerToMap == NULL )
              {
                  layer->reprojectorLayerToMap = msProjectCreateReprojector(
                      &layer->projection, &layer->map->projection);
              }
              if( layer->reprojectorLayerToMap )
              {
                  msProjectShapeEx(layer->reprojectorLayerToMap, shape);
              }
          }
  
          if(clip_to_map)
            msClipPolylineRect(shape, layer->map->extent);
  
          msTransformShapeToPixelRound(shape, layer->map->extent, cellsize);
        } else
          msOffsetShapeRelativeTo(shape, layer);
  
        if(shape->numlines > 0) {
          struct label_auto_result lar;
          memset(&lar,0,sizeof(struct label_auto_result));
          if(MS_UNLIKELY(MS_FAILURE == msPolylineLabelPoint(layer->map, shape, NULL, NULL, &lar, 0))) {
            free(lar.angles);
            free(lar.label_points);
            return MS_FAILURE;
          }
          if(lar.num_label_points > 0) {
            /* convert to geo */
            labelPos.x = lar.label_points[0].x;
            labelPos.y = lar.label_points[0].y;
            labelposvalid = MS_TRUE;
          }
          free(lar.angles);
          free(lar.label_points);
        }
      } else if (shape->type == MS_SHAPE_POLYGON) {
        labelposvalid = MS_FALSE;
        if(layer->transform == MS_TRUE) {
          if(layer->project && msProjectionsDiffer(&(layer->projection), &(layer->map->projection)))
          {
              if( layer->reprojectorLayerToMap == NULL )
              {
                  layer->reprojectorLayerToMap = msProjectCreateReprojector(
                      &layer->projection, &layer->map->projection);
              }
              if( layer->reprojectorLayerToMap )
              {
                  msProjectShapeEx(layer->reprojectorLayerToMap, shape);
              }
          }
  
          if(clip_to_map)
            msClipPolygonRect(shape, layer->map->extent);
  
          msTransformShapeToPixelRound(shape, layer->map->extent, cellsize);
        } else
          msOffsetShapeRelativeTo(shape, layer);
  
        if(shape->numlines > 0) {
          if(msPolygonLabelPoint(shape, &labelPos, -1) == MS_SUCCESS) {
            if(labelPos.x == -1 && labelPos.y == -1)
              labelposvalid = MS_FALSE;
            else
              labelposvalid = MS_TRUE;
          }
        }
      }
  
      if(labelposvalid == MS_TRUE) {
        pointObj p1;
        pointObj p2;
        int label_offset_x, label_offset_y;
        labelObj *label=NULL;
        label_bounds lbounds;
        lineObj lbounds_line;
        pointObj lbounds_point[5];
        double tmp;
  
        p1.x =labelPos.x;
        p1.y =labelPos.y;
  
        p2.x =labelPos.x;
        p2.y =labelPos.y;
        if(use_label_settings == MS_TRUE) {
  
          /* RFC 77: classes (and shapes) can have more than 1 piece of annotation, here we only use the first (index=0) */
          if(shape->classindex >= 0  && layer->class[shape->classindex]->numlabels > 0) {
            label = layer->class[shape->classindex]->labels[0];
            if(msGetLabelStatus(layer->map,layer,shape,label) == MS_ON) {
              char *annotext = msShapeGetLabelAnnotation(layer,shape,label);
              if(annotext) {
                textSymbolObj ts;
                initTextSymbol(&ts);
                msPopulateTextSymbolForLabelAndString(&ts, label, annotext, layer->scalefactor, 1.0, 0);
  
                label_offset_x = (int)(label->offsetx*layer->scalefactor);
                label_offset_y = (int)(label->offsety*layer->scalefactor);
                lbounds.poly = &lbounds_line;
                lbounds_line.numpoints = 5;
                lbounds_line.point = lbounds_point;
  
                p1 = get_metrics(&labelPos, label->position, ts.textpath, label_offset_x, label_offset_y, label->angle* MS_DEG_TO_RAD, 0, &lbounds);
  
                /* should we use the point returned from  get_metrics?. From few test done, It seems
                  to return the UL corner of the text. For now use the bounds.minx/miny */
                p1.x = lbounds.bbox.minx;
                p1.y = lbounds.bbox.miny;
                p2.x = lbounds.bbox.maxx;
                p2.y = lbounds.bbox.maxy;
                freeTextSymbol(&ts);
              }
            }
          }
        }
  
        /* y's are flipped because it is in image coordinate systems */
        p1.x = MS_IMAGE2MAP_X(p1.x, layer->map->extent.minx, cellsize);
        tmp = p1.y;
        p1.y = MS_IMAGE2MAP_Y(p2.y, layer->map->extent.maxy, cellsize);
        p2.x = MS_IMAGE2MAP_X(p2.x, layer->map->extent.minx, cellsize);
        p2.y = MS_IMAGE2MAP_Y(tmp, layer->map->extent.maxy, cellsize);
        if(layer->transform == MS_TRUE) {
          if(layer->project && msProjectionsDiffer(&(layer->projection), &(layer->map->projection))) {
            msProjectPoint(&layer->map->projection, &layer->projection, &p1);
            msProjectPoint(&layer->map->projection, &layer->projection, &p2);
          }
        }
        msAddPointToLine(&(tShape.line[0]), &p1);
        msAddPointToLine(&(tShape.line[0]), &p2);
      } else
        tShape.numlines = 0;
  
      if(projectionString && strcasecmp(projectionString,"image") == 0) {
        precision = 0;
  
        /* if necessary, project the shape to match the map */
        if(msProjectionsDiffer(&(layer->projection), &(layer->map->projection)))
        {
          if( layer->reprojectorLayerToMap == NULL )
          {
              layer->reprojectorLayerToMap = msProjectCreateReprojector(
                  &layer->projection, &layer->map->projection);
          }
          if( layer->reprojectorLayerToMap )
          {
              msProjectShapeEx(layer->reprojectorLayerToMap, &tShape);
          }
        }
  
        msClipPolylineRect(&tShape, layer->map->extent);
  
        msTransformShapeToPixelRound(&tShape, layer->map->extent, layer->map->cellsize);
      } else if(projectionString) {
        projectionObj projection;
        msInitProjection(&projection);
        msProjectionInheritContextFrom(&projection, &layer->map->projection);
  
        status = msLoadProjectionString(&projection, projectionString);
        if(status != MS_SUCCESS) return MS_FAILURE;
  
        if(msProjectionsDiffer(&(layer->projection), &projection))
          msProjectShape(&layer->projection, &projection, &tShape);
      }
  
      /* find the end of the tag */
      tagEnd = findTagEnd(tagStart);
      tagEnd++;
  
      /* build the complete tag so we can do substitution */
      tagLength = tagEnd - tagStart;
      tag = (char *) msSmallMalloc(tagLength + 1);
      strlcpy(tag, tagStart, tagLength+1);
  
      /* do the replacement */
      tagValue = msStrdup(format);
      if(precision > 0)
        snprintf(numberFormat, sizeof(numberFormat), "%%.%dlf", precision);
      else
        snprintf(numberFormat, sizeof(numberFormat), "%%f");
  
      if(tShape.numlines > 0) {
        if(strcasestr(tagValue, "$x") != 0) {
          snprintf(number, sizeof(number), numberFormat, tShape.line[0].point[0].x);
          tagValue = msReplaceSubstring(tagValue, "$x", number);
        }
        if(strcasestr(tagValue, "$y") != 0) {
          snprintf(number, sizeof(number), numberFormat, tShape.line[0].point[0].y);
          tagValue = msReplaceSubstring(tagValue, "$y", number);
        }
  
        if(strcasestr(tagValue, "$minx") != 0) {
          snprintf(number, sizeof(number), numberFormat, tShape.line[0].point[0].x);
          tagValue = msReplaceSubstring(tagValue, "$minx", number);
        }
        if(strcasestr(tagValue, "$miny") != 0) {
          snprintf(number, sizeof(number), numberFormat, tShape.line[0].point[0].y);
          tagValue = msReplaceSubstring(tagValue, "$miny", number);
        }
        if(strcasestr(tagValue, "$maxx") != 0) {
          snprintf(number, sizeof(number), numberFormat, tShape.line[0].point[1].x);
          tagValue = msReplaceSubstring(tagValue, "$maxx", number);
        }
        if(strcasestr(tagValue, "$maxy") != 0) {
          snprintf(number, sizeof(number), numberFormat, tShape.line[0].point[1].y);
          tagValue = msReplaceSubstring(tagValue, "$maxy", number);
        }
      }
  
      /* find the end of the tag */
      tagEnd = findTagEnd(tagStart);
      tagEnd++;
  
      /* build the complete tag so we can do substitution */
      tagLength = tagEnd - tagStart;
      tag = (char *) msSmallMalloc(tagLength + 1);
      strlcpy(tag, tagStart, tagLength+1);
  
      *line = msReplaceSubstring(*line, tag, tagValue);
  
      /* clean up */
      msFreeShape(&tShape);
      free(tag);
      tag = NULL;
      msFreeHashTable(tagArgs);
      tagArgs=NULL;
      msFree(tagValue);
      tagValue=NULL;
  
      if((*line)[tagOffset] != '\0')
        tagStart = findTag(*line+tagOffset+1, "shplabel");
      else
        tagStart = NULL;
    }
    if(shape)
      msFreeShape(shape);
  
    return(MS_SUCCESS);
  }
  
  /*
  ** Function to process a [date ...] tag
  */
  static int processDateTag(char **line)
  {
    struct tm *datetime;
    time_t t;
    int result;
    char *tag=NULL, *tagStart, *tagEnd;
    hashTableObj *tagArgs=NULL;
    int tagOffset, tagLength;
  #define DATE_BUFLEN 1024
    char datestr[DATE_BUFLEN];
    const char *argValue=NULL;
    const char *format, *tz; /* tag parameters */
  
    if(!*line) {
      msSetError(MS_WEBERR, "Invalid line pointer.", "processDateTag()");
      return(MS_FAILURE);
    }
  
    tagStart = findTag(*line, "date");
  
    /* It is OK to have no date tags, just return. */
    if( !tagStart )
      return MS_SUCCESS;
  
    while (tagStart) {
      /* set tag params to defaults */
      format = DEFAULT_DATE_FORMAT;
      tz = "";
  
      tagOffset = tagStart - *line;
  
      /* check for any tag arguments */
      if(getTagArgs("date", tagStart, &tagArgs) != MS_SUCCESS) return(MS_FAILURE);
  
      if(tagArgs) {
        argValue = msLookupHashTable(tagArgs, "format");
        if(argValue) format = argValue;
        argValue = msLookupHashTable(tagArgs, "tz");
        if(argValue) tz = argValue;
      }
  
      t = time(NULL);
      if( strncasecmp( tz, "gmt", 4 ) == 0 ) {
        datetime = gmtime(&t);
      } else {
        datetime = localtime(&t);
      }
      result = strftime(datestr, DATE_BUFLEN, format, datetime);
  
      /* Only do the replacement if the date was successfully written */
      if( result > 0 ) {
        /* find the end of the tag */
        tagEnd = findTagEnd(tagStart);
        tagEnd++;
  
        /* build the complete tag so we can do substitution */
        tagLength = tagEnd - tagStart;
        tag = (char *) msSmallMalloc(tagLength + 1);
        strlcpy(tag, tagStart, tagLength+1);
  
        /* do the replacement */
        *line = msReplaceSubstring(*line, tag, datestr);
      }
  
      /* clean up */
      msFree(tag);
      tag = NULL;
      msFreeHashTable(tagArgs);
      tagArgs=NULL;
  
      if((*line)[tagOffset] != '\0')
        tagStart = findTag(*line+tagOffset+1, "date");
      else
        tagStart = NULL;
    }
  
    return(MS_SUCCESS);
  
  }
  
  /*
  ** Function to process a [shpxy ...] tag: line contains the tag, shape holds the coordinates.
  **
  ** TODO's:
  **   - May need to change attribute names.
  **   - Need generalization routines (not here, but in mapprimative.c).
  **   - Try to avoid all the realloc calls.
  */
  static int processShpxyTag(layerObj *layer, char **line, shapeObj *shape)
  {
    int i,j,p;
    int status;
  
    char *tag, *tagStart, *tagEnd;
    hashTableObj *tagArgs=NULL;
    int tagOffset, tagLength;
  
    const char *argValue=NULL;
    char *pointFormat1=NULL, *pointFormat2=NULL;
    int pointFormatLength;
  
    /*
    ** Pointers to static strings, naming convention is:
    **   char 1/2 - x=x, y=y, c=coordinate, p=part, s=shape, ir=inner ring, or=outer ring
    **   last char - h=header, f=footer, s=seperator
    */
    const char *xh, *xf, *yh, *yf;
    const char *cs;
    const char *ph, *pf, *ps;
    const char *sh, *sf;
    const char *irh, *irf; /* inner ring: necessary for complex polygons */
    const char *orh, *orf; /* outer ring */
  
    int centroid;
    int precision;
  
    double scale_x, scale_y;
  
    const char *projectionString=NULL;
  
    shapeObj tShape;
    char *coords=NULL, point[128];
  
    if(!*line) {
      msSetError(MS_WEBERR, "Invalid line pointer.", "processShpxyTag()");
      return(MS_FAILURE);
    }
    if( msCheckParentPointer(layer->map,"map")==MS_FAILURE )
      return MS_FAILURE;
  
    tagStart = findTag(*line, "shpxy");
  
    /* It is OK to have no shpxy tags, just return. */
    if( !tagStart )
      return MS_SUCCESS;
  
    if(!shape || shape->numlines <= 0) { /* I suppose we need to make sure the part has vertices (need shape checker?) */
      msSetError(MS_WEBERR, "Null or empty shape.", "processShpxyTag()");
      return(MS_FAILURE);
    }
  
    while (tagStart) {
  #ifdef USE_GEOS
      double buffer = 0;
      int bufferUnits = -1;
  #endif
      xh = yh = yf = ph = pf = sh = sf = ""; /* initialize the tag arguments */
      xf= ",";
      irh = irf = orh = orf = "";
      ps = cs = " ";
  
      centroid = MS_FALSE;
      precision = 0;
      scale_x = scale_y = 1.0;
  
      projectionString = NULL;
  
      tagOffset = tagStart - *line;
  
      /* check for any tag arguments */
      if(getTagArgs("shpxy", tagStart, &tagArgs) != MS_SUCCESS) return(MS_FAILURE);
      if(tagArgs) {
        argValue = msLookupHashTable(tagArgs, "xh");
        if(argValue) xh = argValue;
        argValue = msLookupHashTable(tagArgs, "xf");
        if(argValue) xf = argValue;
  
        argValue = msLookupHashTable(tagArgs, "yh");
        if(argValue) yh = argValue;
        argValue = msLookupHashTable(tagArgs, "yf");
        if(argValue) yf = argValue;
  
        argValue = msLookupHashTable(tagArgs, "cs");
        if(argValue) cs = argValue;
  
        argValue = msLookupHashTable(tagArgs, "irh");
        if(argValue) irh = argValue;
        argValue = msLookupHashTable(tagArgs, "irf");
        if(argValue) irf = argValue;
  
        argValue = msLookupHashTable(tagArgs, "orh");
        if(argValue) orh = argValue;
        argValue = msLookupHashTable(tagArgs, "orf");
        if(argValue) orf = argValue;
  
        argValue = msLookupHashTable(tagArgs, "ph");
        if(argValue) ph = argValue;
        argValue = msLookupHashTable(tagArgs, "pf");
        if(argValue) pf = argValue;
        argValue = msLookupHashTable(tagArgs, "ps");
        if(argValue) ps = argValue;
  
        argValue = msLookupHashTable(tagArgs, "sh");
        if(argValue) sh = argValue;
        argValue = msLookupHashTable(tagArgs, "sf");
        if(argValue) sf = argValue;
  
  #ifdef USE_GEOS
        argValue = msLookupHashTable(tagArgs, "buffer");
        if(argValue) {
          buffer = atof(argValue);
          if(strstr(argValue, "px")) bufferUnits = MS_PIXELS; /* may support others at some point */
        }
  #endif
  
        argValue = msLookupHashTable(tagArgs, "precision");
        if(argValue) precision = atoi(argValue);
  
        argValue = msLookupHashTable(tagArgs, "scale");
        if(argValue) {
          scale_x = atof(argValue);
          scale_y = scale_x;
        }
  
        argValue = msLookupHashTable(tagArgs, "scale_x");
        if(argValue) scale_x = atof(argValue);
  
        argValue = msLookupHashTable(tagArgs, "scale_y");
        if(argValue) scale_y = atof(argValue);
  
        argValue = msLookupHashTable(tagArgs, "centroid");
        if(argValue)
          if(strcasecmp(argValue,"true") == 0) centroid = MS_TRUE;
  
        argValue = msLookupHashTable(tagArgs, "proj");
        if(argValue) projectionString = argValue;
      }
  
      /* build the per point format strings (version 1 contains the coordinate seperator, version 2 doesn't) */
      pointFormatLength = strlen(xh) + strlen(xf) + strlen(yh) + strlen(yf) + strlen(cs) + 12 + 1;
      pointFormat1 = (char *) msSmallMalloc(pointFormatLength);
      snprintf(pointFormat1, pointFormatLength, "%s%%.%dlf%s%s%%.%dlf%s%s", xh, precision, xf, yh, precision, yf, cs);
      pointFormat2 = (char *) msSmallMalloc(pointFormatLength);
      snprintf(pointFormat2, pointFormatLength, "%s%%.%dlf%s%s%%.%dlf%s", xh, precision, xf, yh, precision, yf);
  
      /* make a copy of the original shape or compute a centroid if necessary */
      msInitShape(&tShape);
      if(centroid == MS_TRUE) {
        pointObj p;
  
        p.x = (shape->bounds.minx + shape->bounds.maxx)/2;
        p.y = (shape->bounds.miny + shape->bounds.maxy)/2;
  
        tShape.type = MS_SHAPE_POINT;
        tShape.line = (lineObj *) msSmallMalloc(sizeof(lineObj));
        tShape.numlines = 1;
        tShape.line[0].point = NULL; /* initialize the line */
        tShape.line[0].numpoints = 0;
  
        msAddPointToLine(&(tShape.line[0]), &p);
      }
  
  #ifdef USE_GEOS
      else if(buffer != 0 && bufferUnits != MS_PIXELS) {
        shapeObj *bufferShape=NULL;
  
        bufferShape = msGEOSBuffer(shape, buffer);
        if(!bufferShape) {
          free(pointFormat1);
          free(pointFormat2);
          return(MS_FAILURE); /* buffer failed */
        }
        msCopyShape(bufferShape, &tShape);
        msFreeShape(bufferShape);
      }
  #endif
      else {
        status = msCopyShape(shape, &tShape);
        if(status != 0) {
          free(pointFormat1);
          free(pointFormat2);
          return(MS_FAILURE); /* copy failed */
        }
      }
  
      /* no big deal to convert from file to image coordinates, but what are the image parameters */
      if(projectionString && strcasecmp(projectionString,"image") == 0) {
        precision = 0;
  
        /* if necessary, project the shape to match the map */
        if(msProjectionsDiffer(&(layer->projection), &(layer->map->projection)))
        {
          if( layer->reprojectorLayerToMap == NULL )
          {
              layer->reprojectorLayerToMap = msProjectCreateReprojector(
                  &layer->projection, &layer->map->projection);
          }
          if( layer->reprojectorLayerToMap )
          {
              msProjectShapeEx(layer->reprojectorLayerToMap, &tShape);
          }
        }
  
        switch(tShape.type) {
          case(MS_SHAPE_POINT):
            /* no clipping necessary */
            break;
          case(MS_SHAPE_LINE):
            msClipPolylineRect(&tShape, layer->map->extent);
            break;
          case(MS_SHAPE_POLYGON):
            msClipPolygonRect(&tShape, layer->map->extent);
            break;
          default:
            /* TO DO: need an error message here */
            return(MS_FAILURE);
            break;
        }
        msTransformShapeToPixelRound(&tShape, layer->map->extent, layer->map->cellsize);
  
  #ifdef USE_GEOS
        if(buffer != 0 && bufferUnits == MS_PIXELS) {
          shapeObj *bufferShape=NULL;
  
          bufferShape = msGEOSBuffer(&tShape, buffer);
          if(!bufferShape) {
            if(!msIsDegenerateShape(&tShape)) /* If shape is degenerate this is expected. */
              return(MS_FAILURE); /* buffer failed */
          } else {
            msFreeShape(&tShape); /* avoid memory leak */
            msCopyShape(bufferShape, &tShape);
            msFreeShape(bufferShape);
          }
        }
  #endif
  
      } else if(projectionString) {
        projectionObj projection;
        msInitProjection(&projection);
        msProjectionInheritContextFrom(&projection, &(layer->projection));
  
        status = msLoadProjectionString(&projection, projectionString);
        if(status != MS_SUCCESS) return MS_FAILURE;
  
        if(msProjectionsDiffer(&(layer->projection), &projection))
          msProjectShape(&layer->projection, &projection, &tShape);
      }
  
      /* TODO: add thinning support here */
  
      /*
      ** build the coordinate string
      */
  
      if(strlen(sh) > 0) coords = msStringConcatenate(coords, sh);
  
      /* do we need to handle inner/outer rings */
      if(tShape.type == MS_SHAPE_POLYGON && strlen(orh) > 0 && strlen(irh) > 0) {
        int *outers;
        int firstPart; /* to keep track of inserting part separators before each part after the first */
        outers = msGetOuterList( &tShape );
        firstPart = 1;
        /* loop over rings looking for outers*/
        for(i=0; i<tShape.numlines; i++) {
          int *inners;
          if( outers[i] ) {
            /* this is an outer ring */
            if((!firstPart) && (strlen(ps) > 0)) coords = msStringConcatenate(coords, ps);
            firstPart = 0;
            if(strlen(ph) > 0) coords = msStringConcatenate(coords, ph);
            coords = msStringConcatenate(coords, orh);
            for(p=0; p<tShape.line[i].numpoints-1; p++) {
              snprintf(point, sizeof(point), pointFormat1, scale_x*tShape.line[i].point[p].x, scale_y*tShape.line[i].point[p].y);
              coords = msStringConcatenate(coords, point);
            }
            snprintf(point, sizeof(point), pointFormat2, scale_x*tShape.line[i].point[p].x, scale_y*tShape.line[i].point[p].y);
            coords = msStringConcatenate(coords, point);
            coords = msStringConcatenate(coords, orf);
  
            inners = msGetInnerList(&tShape, i, outers);
            /* loop over rings looking for inners to this outer */
            for(j=0; j<tShape.numlines; j++) {
              if( inners[j] ) {
                /* j is an inner ring of i */
                coords = msStringConcatenate(coords, irh);
                for(p=0; p<tShape.line[j].numpoints-1; p++) {
                  snprintf(point, sizeof(point), pointFormat1, scale_x*tShape.line[j].point[p].x, scale_y*tShape.line[j].point[p].y);
                  coords = msStringConcatenate(coords, point);
                }
                snprintf(point, sizeof(point), pointFormat2, scale_x*tShape.line[j].point[p].x, scale_y*tShape.line[j].point[p].y);
                coords = msStringConcatenate(coords, irf);
              }
            }
            free( inners );
            if(strlen(pf) > 0) coords = msStringConcatenate(coords, pf);
          }
        } /* end of loop over outer rings */
        free( outers );
      } else { /* output without ring formatting */
  
        for(i=0; i<tShape.numlines; i++) { /* e.g. part */
  
          /* skip degenerate parts, really should only happen with pixel output */
          if((tShape.type == MS_SHAPE_LINE && tShape.line[i].numpoints < 2) ||
              (tShape.type == MS_SHAPE_POLYGON && tShape.line[i].numpoints < 3))
            continue;
  
          if(strlen(ph) > 0) coords = msStringConcatenate(coords, ph);
  
          for(p=0; p<tShape.line[i].numpoints-1; p++) {
            snprintf(point, sizeof(point), pointFormat1, scale_x*tShape.line[i].point[p].x, scale_y*tShape.line[i].point[p].y);
            coords = msStringConcatenate(coords, point);
          }
          snprintf(point, sizeof(point), pointFormat2, scale_x*tShape.line[i].point[p].x, scale_y*tShape.line[i].point[p].y);
          coords = msStringConcatenate(coords, point);
  
          if(strlen(pf) > 0) coords = msStringConcatenate(coords, pf);
  
          if((i < tShape.numlines-1) && (strlen(ps) > 0)) coords = msStringConcatenate(coords, ps);
        }
      }
      if(strlen(sf) > 0) coords = msStringConcatenate(coords, sf);
  
      msFreeShape(&tShape);
  
      /* find the end of the tag */
      tagEnd = findTagEnd(tagStart);
      tagEnd++;
  
      /* build the complete tag so we can do substitution */
      tagLength = tagEnd - tagStart;
      tag = (char *) msSmallMalloc(tagLength + 1);
      strlcpy(tag, tagStart, tagLength+1);
  
      /* do the replacement */
      *line = msReplaceSubstring(*line, tag, coords);
  
      /* clean up */
      free(tag);
      tag = NULL;
      msFreeHashTable(tagArgs);
      tagArgs=NULL;
      free(pointFormat1);
      pointFormat1 = NULL;
      free(pointFormat2);
      pointFormat2 = NULL;
      free(coords);
      coords = NULL;
  
      if((*line)[tagOffset] != '\0')
        tagStart = findTag(*line+tagOffset+1, "shpxy");
      else
        tagStart = NULL;
    }
  
    return(MS_SUCCESS);
  }
  
  /*!
   * this function process all metadata
   * in pszInstr. ht mus contain all corresponding
   * metadata value.
   *
   * this function return a modified pszInstr
  */
  int processMetadata(char** pszInstr, hashTableObj *ht)
  {
    /* char *pszNextInstr = pszInstr; */
    char *pszEnd, *pszStart;
    char *pszMetadataTag;
    const char *pszHashName;
    const char *pszHashValue;
    int nLength, nOffset;
  
    hashTableObj *metadataArgs = NULL;
  
    if(!*pszInstr) {
      msSetError(MS_WEBERR, "Invalid pointer.", "processMetadata()");
      return MS_FAILURE;
    }
  
    /* set position to the begining of metadata tag */
    pszStart = findTag(*pszInstr, "metadata");
  
    while (pszStart) {
      /* get metadata args */
      if(getTagArgs("metadata", pszStart, &metadataArgs) != MS_SUCCESS)
        return MS_FAILURE;
  
      pszHashName = msLookupHashTable(metadataArgs, "name");
      pszHashValue = msLookupHashTable(ht, pszHashName);
  
      nOffset = pszStart - *pszInstr;
  
      if(pszHashName && pszHashValue) {
        /* set position to the end of metadata start tag */
        pszEnd = strchr(pszStart, ']');
        pszEnd++;
  
        /* build the complete metadata tag ([metadata all_args]) */
        /* to replace it by the corresponding value from ht */
        nLength = pszEnd - pszStart;
        pszMetadataTag = (char*)msSmallMalloc(nLength + 1);
        strlcpy(pszMetadataTag, pszStart, nLength+1);
  
        *pszInstr = msReplaceSubstring(*pszInstr, pszMetadataTag, pszHashValue);
  
        free(pszMetadataTag);
        pszMetadataTag=NULL;
      }
  
      msFreeHashTable(metadataArgs);
      metadataArgs=NULL;
  
  
      /* set position to the begining of the next metadata tag */
      if((*pszInstr)[nOffset] != '\0')
        pszStart = findTag(*pszInstr+nOffset+1, "metadata");
      else
        pszStart = NULL;
    }
  
    return MS_SUCCESS;
  }
  
  /*!
   * this function process all icon tag
   * from pszInstr.
   *
   * This func return a modified pszInstr.
  */
  int processIcon(mapObj *map, int nIdxLayer, int nIdxClass, char** pszInstr, char* pszPrefix)
  {
    int nWidth, nHeight, nLen;
    char szImgFname[1024], *pszFullImgFname=NULL, *pszImgTag;
    char szPath[MS_MAXPATHLEN];
    hashTableObj *myHashTable=NULL;
    FILE *fIcon;
  
    if(!map ||
        nIdxLayer > map->numlayers ||
        nIdxLayer < 0 ) {
      msSetError(MS_WEBERR, "Invalid pointer.", "processIcon()");
      return MS_FAILURE;
    }
  
    /* find the begining of tag */
    pszImgTag = strstr(*pszInstr, "[leg_icon");
  
    while (pszImgTag) {
      int i;
      char szStyleCode[512] = "";
      classObj *thisClass=NULL;
  
      /* It's okay to have no classes... we'll generate an empty icon in this case */
      if(nIdxClass >= 0 && nIdxClass < GET_LAYER(map, nIdxLayer)->numclasses)
        thisClass = GET_LAYER(map, nIdxLayer)->class[nIdxClass];
  
      if(getTagArgs("leg_icon", pszImgTag, &myHashTable) != MS_SUCCESS)
        return MS_FAILURE;
  
      /* if no specified width or height, set them to map default */
      if(!msLookupHashTable(myHashTable, "width") || !msLookupHashTable(myHashTable, "height")) {
        nWidth = map->legend.keysizex;
        nHeight= map->legend.keysizey;
      } else {
        nWidth  = atoi(msLookupHashTable(myHashTable, "width"));
        nHeight = atoi(msLookupHashTable(myHashTable, "height"));
      }
  
      /* Create a unique and predictable filename to cache the legend icons.
       * Include some key parameters from the first 2 styles
       */
      for(i=0; i<2 && thisClass && i<thisClass->numstyles; i++) {
        styleObj *style;
        char *pszSymbolNameHash = NULL;
        style = thisClass->styles[i];
        if(style->symbolname)
          pszSymbolNameHash = msHashString(style->symbolname);
  
        snprintf(szStyleCode+strlen(szStyleCode), 255,
                 "s%d_%x_%x_%x_%d_%s_%g",
                 i, MS_COLOR_GETRGB(style->color), MS_COLOR_GETRGB(style->backgroundcolor), MS_COLOR_GETRGB(style->outlinecolor),
                 style->symbol, pszSymbolNameHash?pszSymbolNameHash:"",
                 style->angle);
        msFree(pszSymbolNameHash);
      }
  
      snprintf(szImgFname, sizeof(szImgFname), "%s_%d_%d_%d_%d_%s.%s%c",
               pszPrefix, nIdxLayer, nIdxClass, nWidth, nHeight,
               szStyleCode, MS_IMAGE_EXTENSION(map->outputformat),'\0');
  
      pszFullImgFname = msStrdup(msBuildPath3(szPath, map->mappath,
                                              map->web.imagepath, szImgFname));
  
      /* check if icon already exist in cache */
      if((fIcon = fopen(pszFullImgFname, "r")) != NULL) {
        /* File already exists. No need to generate it again */
        fclose(fIcon);
      } else {
        /* Create an image corresponding to the current class */
        imageObj *img=NULL;
  
        if(thisClass == NULL) {
          /* Nonexistent class.  Create an empty image */
          img = msCreateLegendIcon(map, NULL, NULL, nWidth, nHeight, MS_TRUE);
        } else {
          img = msCreateLegendIcon(map, GET_LAYER(map, nIdxLayer),
                                   thisClass, nWidth, nHeight, MS_TRUE);
        }
  
        if(!img) {
          if(myHashTable)
            msFreeHashTable(myHashTable);
  
          msSetError(MS_IMGERR, "Error while creating image.", "processIcon()");
          return MS_FAILURE;
        }
  
        /* save it with a unique file name */
        if(msSaveImage(map, img, pszFullImgFname) != MS_SUCCESS) {
          if(myHashTable)
            msFreeHashTable(myHashTable);
  
          msFreeImage(img);
  
          msSetError(MS_IOERR, "Error saving GD image to disk (%s).", "processIcon()", pszFullImgFname);
          msFree(pszFullImgFname);
          return MS_FAILURE;
        }
  
        msFreeImage(img);
      }
  
      msFree(pszFullImgFname);
      pszFullImgFname = NULL;
  
      nLen = (strchr(pszImgTag, ']') + 1) - pszImgTag;
  
      if(nLen > 0) {
        char *pszTag;
  
        /* rebuid image tag ([leg_class_img all_args]) */
        /* to replace it by the image url */
        pszTag = (char*)msSmallMalloc(nLen + 1);
        strlcpy(pszTag, pszImgTag, nLen+1);
  
        pszFullImgFname = (char*)msSmallMalloc(strlen(map->web.imageurl) + strlen(szImgFname) + 1);
        strcpy(pszFullImgFname, map->web.imageurl);
        strcat(pszFullImgFname, szImgFname);
  
        *pszInstr = msReplaceSubstring(*pszInstr, pszTag, pszFullImgFname);
  
        msFree(pszFullImgFname);
        pszFullImgFname = NULL;
        msFree(pszTag);
  
        /* find the begining of tag */
        pszImgTag = strstr(*pszInstr, "[leg_icon");
      } else {
        pszImgTag = NULL;
      }
  
      if(myHashTable) {
        msFreeHashTable(myHashTable);
        myHashTable = NULL;
      }
    }
  
    return MS_SUCCESS;
  }
  
  /*!
   * Replace all tags from group template
   * with correct value.
   *
   * this function return a buffer containing
   * the template with correct values.
   *
   * buffer must be freed by caller.
  */
  int generateGroupTemplate(char* pszGroupTemplate, mapObj *map, char* pszGroupName, hashTableObj *oGroupArgs, char **pszTemp, char* pszPrefix)
  {
    hashTableObj *myHashTable;
    char pszStatus[3];
    char *pszClassImg;
    const char *pszOptFlag = NULL;
    int i, j;
    int nOptFlag = 15;
    int bShowGroup;
  
    *pszTemp = NULL;
  
    if(!pszGroupName || !pszGroupTemplate) {
      msSetError(MS_WEBERR, "Invalid pointer.", "generateGroupTemplate()");
      return MS_FAILURE;
    }
  
    /*
     * Get the opt_flag is any.
     */
    if(oGroupArgs)
      pszOptFlag = msLookupHashTable(oGroupArgs, "opt_flag");
  
    if(pszOptFlag)
      nOptFlag = atoi(pszOptFlag);
  
    /*
     * Check all layers, if one in the group
     * should be visible, print the group.
     * (Check for opt_flag)
     */
    bShowGroup = 0;
    for (j=0; j<map->numlayers; j++) {
      if(GET_LAYER(map, map->layerorder[j])->group &&
          strcmp(GET_LAYER(map, map->layerorder[j])->group, pszGroupName) == 0) {
        /* dont display layer is off. */
        if( (nOptFlag & 2) == 0 &&
            GET_LAYER(map, map->layerorder[j])->status == MS_OFF )
          bShowGroup = 0;
        else
          bShowGroup = 1;
  
        /* dont display layer is query. */
        if( (nOptFlag & 4) == 0  &&
            GET_LAYER(map, map->layerorder[j])->type == MS_LAYER_QUERY )
          bShowGroup = 0;
  
        /* dont display layer if out of scale. */
        if((nOptFlag & 1) == 0) {
          if(map->scaledenom > 0) {
            if((GET_LAYER(map, map->layerorder[j])->maxscaledenom > 0) &&
                (map->scaledenom > GET_LAYER(map, map->layerorder[j])->maxscaledenom))
              bShowGroup = 0;
            if((GET_LAYER(map, map->layerorder[j])->minscaledenom > 0) &&
                (map->scaledenom <= GET_LAYER(map, map->layerorder[j])->minscaledenom))
              bShowGroup = 0;
          }
        }
  
        /* The group contains one visible layer */
        /* Draw the group */
        if( bShowGroup )
          break;
      }
    }
  
    if( ! bShowGroup )
      return MS_SUCCESS;
  
    /*
     * Work from a copy
     */
    *pszTemp = (char*)msSmallMalloc(strlen(pszGroupTemplate) + 1);
    strcpy(*pszTemp, pszGroupTemplate);
  
    /*
     * Change group tags
     */
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_group_name]", pszGroupName);
  
  
    /*
     * Create a hash table that contain info
     * on current layer
     */
    myHashTable = msCreateHashTable();
  
    /*
     * Check for the first layer
     * that belong to this group.
     * Get his status and check for if.
     */
    for (j=0; j<map->numlayers; j++) {
      if(GET_LAYER(map, map->layerorder[j])->group && strcmp(GET_LAYER(map, map->layerorder[j])->group, pszGroupName) == 0) {
        snprintf(pszStatus, sizeof(pszStatus), "%d", GET_LAYER(map, map->layerorder[j])->status);
        msInsertHashTable(myHashTable, "layer_status", pszStatus);
        msInsertHashTable(myHashTable, "layer_visible", msLayerIsVisible(map, GET_LAYER(map, map->layerorder[j]))?"1":"0" );
        msInsertHashTable(myHashTable, "layer_queryable", msIsLayerQueryable(GET_LAYER(map, map->layerorder[j]))?"1":"0" );
        msInsertHashTable(myHashTable, "group_name", pszGroupName);
  
        if(processIfTag(pszTemp, myHashTable, MS_FALSE) != MS_SUCCESS)
          return MS_FAILURE;
  
        if(processIfTag(pszTemp, &(GET_LAYER(map, map->layerorder[j])->metadata), MS_FALSE) != MS_SUCCESS)
          return MS_FAILURE;
  
        if(processMetadata(pszTemp, &GET_LAYER(map, map->layerorder[j])->metadata) != MS_SUCCESS)
          return MS_FAILURE;
  
        break;
      }
    }
  
    msFreeHashTable(myHashTable);
  
    /*
     * Process all metadata tags
     * only web object is accessible
    */
    if(processMetadata(pszTemp, &(map->web.metadata)) != MS_SUCCESS)
      return MS_FAILURE;
  
    /*
     * check for if tag
    */
    if(processIfTag(pszTemp, &(map->web.metadata), MS_TRUE) != MS_SUCCESS)
      return MS_FAILURE;
  
    /*
     * Check if leg_icon tag exist
     * if so display the first layer first class icon
     */
    pszClassImg = strstr(*pszTemp, "[leg_icon");
    if(pszClassImg) {
      /* find first layer of this group */
      for (i=0; i<map->numlayers; i++)
        if(GET_LAYER(map, map->layerorder[i])->group && strcmp(GET_LAYER(map, map->layerorder[i])->group, pszGroupName) == 0)
          processIcon(map, map->layerorder[i], 0, pszTemp, pszPrefix);
    }
  
    return MS_SUCCESS;
  }
  
  /*!
   * Replace all tags from layer template
   * with correct value.
   *
   * this function return a buffer containing
   * the template with correct values.
   *
   * buffer must be freed by caller.
  */
  int generateLayerTemplate(char *pszLayerTemplate, mapObj *map, int nIdxLayer, hashTableObj *oLayerArgs, char **pszTemp, char* pszPrefix)
  {
    hashTableObj *myHashTable;
    char szStatus[10];
    char szType[10];
  
    int nOptFlag=0;
    const char *pszOptFlag = NULL;
    char *pszClassImg;
  
    char szTmpstr[128]; /* easily big enough for the couple of instances we need */
  
    *pszTemp = NULL;
  
    if(!pszLayerTemplate ||
        !map ||
        nIdxLayer > map->numlayers ||
        nIdxLayer < 0 ) {
      msSetError(MS_WEBERR, "Invalid pointer.", "generateLayerTemplate()");
      return MS_FAILURE;
    }
  
    if(oLayerArgs)
      pszOptFlag = msLookupHashTable(oLayerArgs, "opt_flag");
  
    if(pszOptFlag)
      nOptFlag = atoi(pszOptFlag);
  
    /* don't display deleted layers */
    if(GET_LAYER(map, nIdxLayer)->status == MS_DELETE)
      return MS_SUCCESS;
  
    /* dont display layer is off. */
    /* check this if Opt flag is not set */
    if((nOptFlag & 2) == 0 && GET_LAYER(map, nIdxLayer)->status == MS_OFF)
      return MS_SUCCESS;
  
    /* dont display layer is query. */
    /* check this if Opt flag is not set */
    if((nOptFlag & 4) == 0  && GET_LAYER(map, nIdxLayer)->type == MS_LAYER_QUERY)
      return MS_SUCCESS;
  
    /* dont display layer if out of scale. */
    /* check this if Opt flag is not set             */
    if((nOptFlag & 1) == 0) {
      if(map->scaledenom > 0) {
        if((GET_LAYER(map, nIdxLayer)->maxscaledenom > 0) && (map->scaledenom > GET_LAYER(map, nIdxLayer)->maxscaledenom))
          return MS_SUCCESS;
        if((GET_LAYER(map, nIdxLayer)->minscaledenom > 0) && (map->scaledenom <= GET_LAYER(map, nIdxLayer)->minscaledenom))
          return MS_SUCCESS;
      }
    }
  
    /*
     * Work from a copy
     */
    *pszTemp = msStrdup(pszLayerTemplate);
  
    /*
     * Change layer tags
     */
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_layer_name]", GET_LAYER(map, nIdxLayer)->name);
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_layer_group]", GET_LAYER(map, nIdxLayer)->group);
  
    snprintf(szTmpstr, sizeof(szTmpstr), "%d", nIdxLayer);
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_layer_index]", szTmpstr);
  
    snprintf(szTmpstr, sizeof(szTmpstr), "%g", GET_LAYER(map, nIdxLayer)->minscaledenom);
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_layer_minscale]", szTmpstr);
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_layer_minscaledenom]", szTmpstr);
    snprintf(szTmpstr, sizeof(szTmpstr), "%g", GET_LAYER(map, nIdxLayer)->maxscaledenom);
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_layer_maxscale]", szTmpstr);
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_layer_maxscaledenom]", szTmpstr);
  
    /*
     * Create a hash table that contain info
     * on current layer
     */
    myHashTable = msCreateHashTable();
  
    /*
     * for now, only status and type is required by template
     */
    snprintf(szStatus, sizeof(szStatus), "%d", GET_LAYER(map, nIdxLayer)->status);
    msInsertHashTable(myHashTable, "layer_status", szStatus);
  
    snprintf(szType, sizeof(szType), "%d", GET_LAYER(map, nIdxLayer)->type);
    msInsertHashTable(myHashTable, "layer_type", szType);
  
    msInsertHashTable(myHashTable, "layer_name", (GET_LAYER(map, nIdxLayer)->name)? GET_LAYER(map, nIdxLayer)->name : "");
    msInsertHashTable(myHashTable, "layer_group", (GET_LAYER(map, nIdxLayer)->group)? GET_LAYER(map, nIdxLayer)->group : "");
    msInsertHashTable(myHashTable, "layer_visible", msLayerIsVisible(map, GET_LAYER(map, nIdxLayer))?"1":"0" );
    msInsertHashTable(myHashTable, "layer_queryable", msIsLayerQueryable(GET_LAYER(map, nIdxLayer))?"1":"0" );
  
    if(processIfTag(pszTemp, myHashTable, MS_FALSE) != MS_SUCCESS)
      return MS_FAILURE;
  
    if(processIfTag(pszTemp, &(GET_LAYER(map, nIdxLayer)->metadata), MS_FALSE) != MS_SUCCESS)
      return MS_FAILURE;
  
    if(processIfTag(pszTemp, &(map->web.metadata), MS_TRUE) != MS_SUCCESS)
      return MS_FAILURE;
  
    msFreeHashTable(myHashTable);
  
    /*
     * Check if leg_icon tag exist
     * if so display the first class icon
     */
    pszClassImg = strstr(*pszTemp, "[leg_icon");
    if(pszClassImg) {
      processIcon(map, nIdxLayer, 0, pszTemp, pszPrefix);
    }
  
    /* process all metadata tags
     * only current layer and web object
     * metaddata are accessible
    */
    if(processMetadata(pszTemp, &GET_LAYER(map, nIdxLayer)->metadata) != MS_SUCCESS)
      return MS_FAILURE;
  
    if(processMetadata(pszTemp, &(map->web.metadata)) != MS_SUCCESS)
      return MS_FAILURE;
  
    return MS_SUCCESS;
  }
  
  /*!
   * Replace all tags from class template
   * with correct value.
   *
   * this function return a buffer containing
   * the template with correct values.
   *
   * buffer must be freed by caller.
  */
  int generateClassTemplate(char* pszClassTemplate, mapObj *map, int nIdxLayer, int nIdxClass, hashTableObj *oClassArgs, char **pszTemp, char* pszPrefix)
  {
    hashTableObj *myHashTable;
    char szStatus[10];
    char szType[10];
  
    char *pszClassImg;
    int nOptFlag=0;
    const char *pszOptFlag = NULL;
  
    char szTmpstr[128]; /* easily big enough for the couple of instances we need */
  
    *pszTemp = NULL;
  
    if(!pszClassTemplate ||
        !map ||
        nIdxLayer > map->numlayers ||
        nIdxLayer < 0 ||
        nIdxClass > GET_LAYER(map, nIdxLayer)->numclasses ||
        nIdxClass < 0) {
  
      msSetError(MS_WEBERR, "Invalid pointer.", "generateClassTemplate()");
      return MS_FAILURE;
    }
  
    if(oClassArgs)
      pszOptFlag = msLookupHashTable(oClassArgs, "Opt_flag");
  
    if(pszOptFlag)
      nOptFlag = atoi(pszOptFlag);
  
    /* don't display deleted layers */
    if(GET_LAYER(map, nIdxLayer)->status == MS_DELETE)
      return MS_SUCCESS;
  
    /* dont display class if layer is off. */
    /* check this if Opt flag is not set */
    if((nOptFlag & 2) == 0 && GET_LAYER(map, nIdxLayer)->status == MS_OFF)
      return MS_SUCCESS;
  
    /* dont display class if layer is query. */
    /* check this if Opt flag is not set       */
    if((nOptFlag & 4) == 0 && GET_LAYER(map, nIdxLayer)->type == MS_LAYER_QUERY)
      return MS_SUCCESS;
  
    /* dont display layer if out of scale. */
    /* check this if Opt flag is not set */
    if((nOptFlag & 1) == 0) {
      if(map->scaledenom > 0) {
        if((GET_LAYER(map, nIdxLayer)->maxscaledenom > 0) && (map->scaledenom > GET_LAYER(map, nIdxLayer)->maxscaledenom))
          return MS_SUCCESS;
        if((GET_LAYER(map, nIdxLayer)->minscaledenom > 0) && (map->scaledenom <= GET_LAYER(map, nIdxLayer)->minscaledenom))
          return MS_SUCCESS;
      }
    }
  
    /*
     * Work from a copy
     */
    *pszTemp = (char*)msSmallMalloc(strlen(pszClassTemplate) + 1);
    strcpy(*pszTemp, pszClassTemplate);
  
    /*
     * Change class tags
     */
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_class_name]", GET_LAYER(map, nIdxLayer)->class[nIdxClass]->name);
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_class_title]", GET_LAYER(map, nIdxLayer)->class[nIdxClass]->title);
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_layer_name]", GET_LAYER(map, nIdxLayer)->name);
  
    snprintf(szTmpstr, sizeof(szTmpstr), "%d", nIdxClass);
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_class_index]", szTmpstr);
  
    snprintf(szTmpstr, sizeof(szTmpstr), "%g", GET_LAYER(map, nIdxLayer)->class[nIdxClass]->minscaledenom);
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_class_minscale]", szTmpstr);
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_class_minscaledenom]", szTmpstr);
    snprintf(szTmpstr, sizeof(szTmpstr), "%g", GET_LAYER(map, nIdxLayer)->class[nIdxClass]->maxscaledenom);
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_class_maxscale]", szTmpstr);
    *pszTemp = msReplaceSubstring(*pszTemp, "[leg_class_maxscaledenom]", szTmpstr);
  
    /*
     * Create a hash table that contain info
     * on current layer
     */
    myHashTable = msCreateHashTable();
  
    /*
     * for now, only status, type, name and group are  required by template
     */
    snprintf(szStatus, sizeof(szStatus), "%d", GET_LAYER(map, nIdxLayer)->status);
    msInsertHashTable(myHashTable, "layer_status", szStatus);
  
    snprintf(szType, sizeof(szType), "%d", GET_LAYER(map, nIdxLayer)->type);
    msInsertHashTable(myHashTable, "layer_type", szType);
  
    msInsertHashTable(myHashTable, "layer_name",
                      (GET_LAYER(map, nIdxLayer)->name)? GET_LAYER(map, nIdxLayer)->name : "");
    msInsertHashTable(myHashTable, "layer_group",
                      (GET_LAYER(map, nIdxLayer)->group)? GET_LAYER(map, nIdxLayer)->group : "");
    msInsertHashTable(myHashTable, "layer_visible", msLayerIsVisible(map, GET_LAYER(map, nIdxLayer))?"1":"0" );
    msInsertHashTable(myHashTable, "layer_queryable", msIsLayerQueryable(GET_LAYER(map, nIdxLayer))?"1":"0" );
    msInsertHashTable(myHashTable, "class_name",
                      (GET_LAYER(map, nIdxLayer)->class[nIdxClass]->name)? GET_LAYER(map, nIdxLayer)->class[nIdxClass]->name : "");
  
    if(processIfTag(pszTemp, myHashTable, MS_FALSE) != MS_SUCCESS)
      return MS_FAILURE;
  
    if(processIfTag(pszTemp, &(GET_LAYER(map, nIdxLayer)->metadata), MS_FALSE) != MS_SUCCESS)
      return MS_FAILURE;
  
    if(processIfTag(pszTemp, &(map->web.metadata), MS_TRUE) != MS_SUCCESS)
      return MS_FAILURE;
  
    msFreeHashTable(myHashTable);
  
    /*
     * Check if leg_icon tag exist
     */
    pszClassImg = strstr(*pszTemp, "[leg_icon");
    if(pszClassImg) {
      processIcon(map, nIdxLayer, nIdxClass, pszTemp, pszPrefix);
    }
  
    /* process all metadata tags
     * only current layer and web object
     * metaddata are accessible
    */
    if(processMetadata(pszTemp, &GET_LAYER(map, nIdxLayer)->metadata) != MS_SUCCESS)
      return MS_FAILURE;
  
    if(processMetadata(pszTemp, &(map->web.metadata)) != MS_SUCCESS)
      return MS_FAILURE;
  
    return MS_SUCCESS;
  }
  
  char *generateLegendTemplate(mapservObj *mapserv)
  {
    FILE *stream;
    char *file = NULL;
    int length;
    char *pszResult = NULL;
    char *legGroupHtml = NULL;
    char *legLayerHtml = NULL;
    char *legClassHtml = NULL;
    char *legLayerHtmlCopy = NULL;
    char *legClassHtmlCopy = NULL;
    char *legGroupHtmlCopy = NULL;
  
    char *legHeaderHtml = NULL;
    char *legFooterHtml = NULL;
  
    char *pszPrefix = NULL;
    char *pszMapFname = NULL;
  
    struct stat tmpStat;
  
    const char *pszOrderMetadata = NULL;
    const char *pszOrder = NULL;
  
    int i,j,k;
    char **papszGroups = NULL;
    int nGroupNames = 0;
  
    int nLegendOrder = 0;
    const char *pszOrderValue;
  
    hashTableObj *groupArgs = NULL;
    hashTableObj *layerArgs = NULL;
    hashTableObj *classArgs = NULL;
  
    ms_regex_t re; /* compiled regular expression to be matched */
  
    int  *panCurrentDrawingOrder = NULL;
    char szPath[MS_MAXPATHLEN];
  
    if(ms_regcomp(&re, MS_TEMPLATE_EXPR, MS_REG_EXTENDED|MS_REG_NOSUB|MS_REG_ICASE) != 0) {
      msSetError(MS_IOERR, "Error regcomp.", "generateLegendTemplate()");
      return NULL;
    }
  
    if(ms_regexec(&re, mapserv->map->legend.template, 0, NULL, 0) != 0) { /* no match */
      msSetError(MS_IOERR, "Invalid template file name.", "generateLegendTemplate()");
      ms_regfree(&re);
      return NULL;
    }
    ms_regfree(&re);
  
    /* -------------------------------------------------------------------- */
    /*      Save the current drawing order. The drawing order is reset      */
    /*      at the end of the function.                                     */
    /* -------------------------------------------------------------------- */
    if(mapserv && mapserv->map && mapserv->map->numlayers > 0) {
      panCurrentDrawingOrder =
        (int *)msSmallMalloc(sizeof(int)*mapserv->map->numlayers);
  
      for (i=0; i<mapserv->map->numlayers; i++) {
        if(mapserv->map->layerorder)
          panCurrentDrawingOrder[i] = mapserv->map->layerorder[i];
        else
          panCurrentDrawingOrder[i] = i;
      }
    }
  
    /*
     * build prefix filename
     * for legend icon creation
    */
    for(i=0; i<mapserv->request->NumParams; i++) /* find the mapfile parameter first */
      if(strcasecmp(mapserv->request->ParamNames[i], "map") == 0) break;
  
    if(i == mapserv->request->NumParams) {
      if( getenv("MS_MAPFILE"))
        pszMapFname = msStringConcatenate(pszMapFname, getenv("MS_MAPFILE"));
    } else {
      if(getenv(mapserv->request->ParamValues[i])) /* an environment references the actual file to use */
        pszMapFname = msStringConcatenate(pszMapFname, getenv(mapserv->request->ParamValues[i]));
      else
        pszMapFname = msStringConcatenate(pszMapFname, mapserv->request->ParamValues[i]);
    }
  
    if(pszMapFname) {
      if(stat(pszMapFname, &tmpStat) != -1) {
        int nLen;
  
        nLen = (mapserv->map->name?strlen(mapserv->map->name):0)  + 50;
        pszPrefix = (char*)msSmallMalloc((nLen+1) * sizeof(char));
        snprintf(pszPrefix, nLen, "%s_%ld_%ld",
                 mapserv->map->name,
                 (long) tmpStat.st_size,
                 (long) tmpStat.st_mtime);
        pszPrefix[nLen] = '\0';
      }
  
      free(pszMapFname);
      pszMapFname = NULL;
    } else {
      /* -------------------------------------------------------------------- */
      /*      map file name may not be avaible when the template functions    */
      /*      are called from mapscript. Use the time stamp as prefix.        */
      /* -------------------------------------------------------------------- */
      char pszTime[20];
  
      snprintf(pszTime, sizeof(pszTime), "%ld", (long)time(NULL));
      pszPrefix = msStringConcatenate(pszPrefix, pszTime);
    }
  
    /* open template */
    if((stream = fopen(msBuildPath(szPath, mapserv->map->mappath, mapserv->map->legend.template), "r")) == NULL) {
      msSetError(MS_IOERR, "Error while opening template file.", "generateLegendTemplate()");
      if(pszResult)
        free(pszResult);
      pszResult=NULL;
      goto error;
    }
  
    fseek(stream, 0, SEEK_END);
    length = ftell(stream);
    rewind(stream);
  
    file = (char*)msSmallMalloc(length + 1);
  
    /*
     * Read all the template file
     */
    IGUR_sizet(fread(file, length, 1, stream));
    /* E. Rouault: the below issue is due to opening in "r" mode, which is a
     * synonymous of "rt" on Windows. In that mode \r\n are turned into \n,
     * consequently less bytes are written in the output buffer than requested.
     * A potential fix might be to open in "rb" mode, but is the code ready
     * to deal with Windows \r\n end of lines ? */
    /* Disabled for now due to Windows issue, see ticket #3814
       if( 1 != fread(file, length, 1, stream)) {
         msSetError(MS_IOERR, "Error while reading template file.", "generateLegendTemplate()");
         free(file);
         fclose(stream);
         return NULL;
       }
    */
    file[length] = '\0';
  
    if(msValidateContexts(mapserv->map) != MS_SUCCESS) { /* make sure there are no recursive REQUIRES or LABELREQUIRES expressions */
      if(pszResult)
        free(pszResult);
      pszResult=NULL;
      goto error;
    }
  
    /*
     * Seperate header/footer, groups, layers and class
     */
    getInlineTag("leg_header_html", file, &legHeaderHtml);
    getInlineTag("leg_footer_html", file, &legFooterHtml);
    getInlineTag("leg_group_html", file, &legGroupHtml);
    getInlineTag("leg_layer_html", file, &legLayerHtml);
    getInlineTag("leg_class_html", file, &legClassHtml);
  
    /*
     * Retrieve arguments of all three parts
     */
    if(legGroupHtml)
      if(getTagArgs("leg_group_html", file, &groupArgs) != MS_SUCCESS) {
        if(pszResult)
          free(pszResult);
        pszResult=NULL;
        goto error;
      }
  
    if(legLayerHtml)
      if(getTagArgs("leg_layer_html", file, &layerArgs) != MS_SUCCESS) {
        if(pszResult)
          free(pszResult);
        pszResult=NULL;
        goto error;
      }
  
    if(legClassHtml)
      if(getTagArgs("leg_class_html", file, &classArgs) != MS_SUCCESS) {
        if(pszResult)
          free(pszResult);
        pszResult=NULL;
        goto error;
      }
  
  
    mapserv->map->cellsize = msAdjustExtent(&(mapserv->map->extent),
                                            mapserv->map->width,
                                            mapserv->map->height);
    if(msCalculateScale(mapserv->map->extent, mapserv->map->units,
                        mapserv->map->width, mapserv->map->height,
                        mapserv->map->resolution, &mapserv->map->scaledenom) != MS_SUCCESS) {
      if(pszResult)
        free(pszResult);
      pszResult=NULL;
      goto error;
    }
  
    /* start with the header if present */
    if(legHeaderHtml) pszResult = msStringConcatenate(pszResult, legHeaderHtml);
  
    /********************************************************************/
  
    /*
     * order layers if order_metadata args is set
     * If not, keep default order
     */
    pszOrderMetadata = msLookupHashTable(layerArgs, "order_metadata");
  
    if(sortLayerByMetadata(mapserv->map, pszOrderMetadata) != MS_SUCCESS) {
      if(pszResult)
        free(pszResult);
      pszResult=NULL;
      goto error;
    }
  
    /* -------------------------------------------------------------------- */
    /*      if the order tag is set to ascending or descending, the         */
    /*      current order will be changed to correspond to that.            */
    /* -------------------------------------------------------------------- */
    pszOrder = msLookupHashTable(layerArgs, "order");
    if(pszOrder && ((strcasecmp(pszOrder, "ASCENDING") == 0) ||
                    (strcasecmp(pszOrder, "DESCENDING") == 0))) {
      if(sortLayerByOrder(mapserv->map, pszOrder) != MS_SUCCESS) {
        if(pszResult)
          free(pszResult);
        pszResult=NULL;
        goto error;
      }
    }
  
    if(legGroupHtml) {
      /* retrieve group names */
      papszGroups = msGetAllGroupNames(mapserv->map, &nGroupNames);
  
      for (i=0; i<nGroupNames; i++) {
        /* process group tags */
        if(generateGroupTemplate(legGroupHtml, mapserv->map, papszGroups[i], groupArgs, &legGroupHtmlCopy, pszPrefix) != MS_SUCCESS) {
          if(pszResult)
            free(pszResult);
          pszResult=NULL;
          goto error;
        }
  
        /* concatenate it to final result */
        pszResult = msStringConcatenate(pszResult, legGroupHtmlCopy);
  
        /*
                 if(!pszResult)
                 {
                    if(pszResult)
                      free(pszResult);
                    pszResult=NULL;
                    goto error;
                 }
        */
  
        if(legGroupHtmlCopy) {
          free(legGroupHtmlCopy);
          legGroupHtmlCopy = NULL;
        }
  
        /* for all layers in group */
        if(legLayerHtml) {
          for (j=0; j<mapserv->map->numlayers; j++) {
            /*
             * if order_metadata is set and the order
             * value is less than 0, dont display it
             */
            pszOrderMetadata = msLookupHashTable(layerArgs, "order_metadata");
            if(pszOrderMetadata) {
              pszOrderValue = msLookupHashTable(&(GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->metadata), pszOrderMetadata);
              if(pszOrderValue) {
                nLegendOrder = atoi(pszOrderValue);
                if(nLegendOrder < 0)
                  continue;
              }
            }
            if(mapserv->hittest && mapserv->hittest->layerhits[mapserv->map->layerorder[j]].status == 0) {
              continue;
            }
  
            if(GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->group && strcmp(GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->group, papszGroups[i]) == 0) {
              /* process all layer tags */
              if(generateLayerTemplate(legLayerHtml, mapserv->map, mapserv->map->layerorder[j], layerArgs, &legLayerHtmlCopy, pszPrefix) != MS_SUCCESS) {
                if(pszResult)
                  free(pszResult);
                pszResult=NULL;
                goto error;
              }
  
  
              /* concatenate to final result */
              pszResult = msStringConcatenate(pszResult, legLayerHtmlCopy);
  
              if(legLayerHtmlCopy) {
                free(legLayerHtmlCopy);
                legLayerHtmlCopy = NULL;
              }
  
  
              /* for all classes in layer */
              if(legClassHtml) {
                for (k=0; k<GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->numclasses; k++) {
                  /* process all class tags */
                  if(!GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->class[k]->name)
                    continue;
                  if(mapserv->hittest && mapserv->hittest->layerhits[mapserv->map->layerorder[j]].classhits[k].status == 0) {
                    continue;
                  }
  
                  if(generateClassTemplate(legClassHtml, mapserv->map, mapserv->map->layerorder[j], k, classArgs, &legClassHtmlCopy, pszPrefix) != MS_SUCCESS) {
                    if(pszResult)
                      free(pszResult);
                    pszResult=NULL;
                    goto error;
                  }
  
  
                  /* concatenate to final result */
                  pszResult = msStringConcatenate(pszResult, legClassHtmlCopy);
  
                  if(legClassHtmlCopy) {
                    free(legClassHtmlCopy);
                    legClassHtmlCopy = NULL;
                  }
                }
              }
            }
          }
        } else if(legClassHtml) { /* no layer template specified but class and group template */
          for (j=0; j<mapserv->map->numlayers; j++) {
            /*
             * if order_metadata is set and the order
             * value is less than 0, dont display it
             */
            pszOrderMetadata = msLookupHashTable(layerArgs, "order_metadata");
            if(pszOrderMetadata) {
              pszOrderValue = msLookupHashTable(&(GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->metadata), pszOrderMetadata);
              if(pszOrderValue) {
                nLegendOrder = atoi(pszOrderValue);
                if(nLegendOrder < 0)
                  continue;
              }
            }
            if(mapserv->hittest && mapserv->hittest->layerhits[mapserv->map->layerorder[j]].status == 0) {
              continue;
            }
  
            if(GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->group && strcmp(GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->group, papszGroups[i]) == 0) {
              /* for all classes in layer */
              if(legClassHtml) {
                for (k=0; k<GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->numclasses; k++) {
                  /* process all class tags */
                  if(!GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->class[k]->name)
                    continue;
                  if(mapserv->hittest && mapserv->hittest->layerhits[mapserv->map->layerorder[j]].classhits[k].status == 0) {
                    continue;
                  }
  
                  if(generateClassTemplate(legClassHtml, mapserv->map, mapserv->map->layerorder[j], k, classArgs, &legClassHtmlCopy, pszPrefix) != MS_SUCCESS) {
                    if(pszResult)
                      free(pszResult);
                    pszResult=NULL;
                    goto error;
                  }
  
  
                  /* concatenate to final result */
                  pszResult = msStringConcatenate(pszResult, legClassHtmlCopy);
  
                  if(legClassHtmlCopy) {
                    free(legClassHtmlCopy);
                    legClassHtmlCopy = NULL;
                  }
                }
              }
            }
          }
        }
      }
    } else {
      /* if no group template specified */
      if(legLayerHtml) {
        for (j=0; j<mapserv->map->numlayers; j++) {
          /*
           * if order_metadata is set and the order
           * value is less than 0, dont display it
           */
          pszOrderMetadata = msLookupHashTable(layerArgs, "order_metadata");
          if(pszOrderMetadata) {
            pszOrderValue = msLookupHashTable(&(GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->metadata), pszOrderMetadata);
            if(pszOrderValue) {
              nLegendOrder = atoi(pszOrderValue);
              if(nLegendOrder < 0)
                continue;
            } else
              nLegendOrder=0;
          }
          if(mapserv->hittest && mapserv->hittest->layerhits[mapserv->map->layerorder[j]].status == 0) {
            continue;
          }
  
          /* process a layer tags */
          if(generateLayerTemplate(legLayerHtml, mapserv->map, mapserv->map->layerorder[j], layerArgs, &legLayerHtmlCopy, pszPrefix) != MS_SUCCESS) {
            if(pszResult)
              free(pszResult);
            pszResult=NULL;
            goto error;
          }
  
          /* concatenate to final result */
          pszResult = msStringConcatenate(pszResult, legLayerHtmlCopy);
  
          if(legLayerHtmlCopy) {
            free(legLayerHtmlCopy);
            legLayerHtmlCopy = NULL;
          }
  
          /* for all classes in layer */
          if(legClassHtml) {
            for (k=0; k<GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->numclasses; k++) {
              /* process all class tags */
              if(!GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->class[k]->name)
                continue;
              if(mapserv->hittest && mapserv->hittest->layerhits[mapserv->map->layerorder[j]].classhits[k].status == 0) {
                continue;
              }
  
              if(generateClassTemplate(legClassHtml, mapserv->map, mapserv->map->layerorder[j], k, classArgs, &legClassHtmlCopy, pszPrefix) != MS_SUCCESS) {
                if(pszResult)
                  free(pszResult);
                pszResult=NULL;
                goto error;
              }
  
  
              /* concatenate to final result */
              pszResult = msStringConcatenate(pszResult, legClassHtmlCopy);
  
              if(legClassHtmlCopy) {
                free(legClassHtmlCopy);
                legClassHtmlCopy = NULL;
              }
            }
          }
        }
      } else { /* if no group and layer template specified */
        if(legClassHtml) {
          for (j=0; j<mapserv->map->numlayers; j++) {
            /*
             * if order_metadata is set and the order
             * value is less than 0, dont display it
             */
            pszOrderMetadata = msLookupHashTable(layerArgs, "order_metadata");
            if(pszOrderMetadata) {
              pszOrderValue = msLookupHashTable(&(GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->metadata), pszOrderMetadata);
              if(pszOrderValue) {
                nLegendOrder = atoi(pszOrderValue);
                if(nLegendOrder < 0)
                  continue;
              }
            }
            if(mapserv->hittest && mapserv->hittest->layerhits[mapserv->map->layerorder[j]].status == 0) {
              continue;
            }
  
            for (k=0; k<GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->numclasses; k++) {
              if(!GET_LAYER(mapserv->map, mapserv->map->layerorder[j])->class[k]->name)
                continue;
              if(mapserv->hittest && mapserv->hittest->layerhits[mapserv->map->layerorder[j]].classhits[k].status == 0) {
                continue;
              }
  
              if(generateClassTemplate(legClassHtml, mapserv->map, mapserv->map->layerorder[j], k, classArgs, &legClassHtmlCopy, pszPrefix) != MS_SUCCESS) {
                if(pszResult)
                  free(pszResult);
                pszResult=NULL;
                goto error;
              }
  
  
              pszResult = msStringConcatenate(pszResult, legClassHtmlCopy);
  
              if(legClassHtmlCopy) {
                free(legClassHtmlCopy);
                legClassHtmlCopy = NULL;
              }
            }
          }
        }
      }
    }
  
    /* finish with the footer if present */
    if(legFooterHtml) pszResult = msStringConcatenate(pszResult, legFooterHtml);
  
    /*
     * if we reach this point, that mean no error was generated.
     * So check if template is null and initialize it to <space>.
     */
    if(pszResult == NULL) {
      pszResult = msStringConcatenate(pszResult, " ");
    }
  
  
    /********************************************************************/
  
  error:
  
    if(papszGroups) {
      for (i=0; i<nGroupNames; i++)
        msFree(papszGroups[i]);
  
      msFree(papszGroups);
    }
  
    msFreeHashTable(groupArgs);
    msFreeHashTable(layerArgs);
    msFreeHashTable(classArgs);
  
    msFree(file);
  
    msFree(legGroupHtmlCopy);
    msFree(legLayerHtmlCopy);
    msFree(legClassHtmlCopy);
  
    msFree(legHeaderHtml);
    msFree(legFooterHtml);
  
    msFree(legGroupHtml);
    msFree(legLayerHtml);
    msFree(legClassHtml);
    msFree(pszPrefix);
  
    if(stream)
      fclose(stream);
  
    /* -------------------------------------------------------------------- */
    /*      Reset the layerdrawing order.                                   */
    /* -------------------------------------------------------------------- */
    if(panCurrentDrawingOrder && mapserv->map->layerorder) {
      for (i=0; i<mapserv->map->numlayers; i++)
        mapserv->map->layerorder[i] =  panCurrentDrawingOrder[i];
  
      free(panCurrentDrawingOrder);
    }
  
    return pszResult;
  }
  
  char *processOneToManyJoin(mapservObj* mapserv, joinObj *join)
  {
    int records=MS_FALSE;
    FILE *stream=NULL;
    char *outbuf;
    char line[MS_BUFFER_LENGTH], *tmpline;
    char szPath[MS_MAXPATHLEN];
  
    if((outbuf = msStrdup("")) == NULL) return(NULL); /* empty at first */
  
    msJoinPrepare(join, &(mapserv->resultshape)); /* execute the join */
    while(msJoinNext(join) == MS_SUCCESS) {
      /* First time through, deal with the header (if necessary) and open the main template. We only */
      /* want to do this if there are joined records. */
      if(records == MS_FALSE) {
        if(join->header != NULL) {
          if(stream) fclose(stream);
          if((stream = fopen(msBuildPath(szPath, mapserv->map->mappath, join->header), "r")) == NULL) {
            msSetError(MS_IOERR, "Error while opening join header file %s.", "processOneToManyJoin()", join->header);
            msFree(outbuf);
            return(NULL);
          }
  
          if(isValidTemplate(stream, join->header) != MS_TRUE) {
            fclose(stream);
            msFree(outbuf);
            return NULL;
          }
  
          /* echo file to the output buffer, no substitutions */
          while(fgets(line, MS_BUFFER_LENGTH, stream) != NULL) outbuf = msStringConcatenate(outbuf, line);
  
          fclose(stream);
          stream = NULL;
        }
  
        if((stream = fopen(msBuildPath(szPath, mapserv->map->mappath, join->template), "r")) == NULL) {
          msSetError(MS_IOERR, "Error while opening join template file %s.", "processOneToManyJoin()", join->template);
          msFree(outbuf);
          return(NULL);
        }
  
        if(isValidTemplate(stream, join->template) != MS_TRUE) {
          fclose(stream);
          msFree(outbuf);
          return NULL;
        }
  
        records = MS_TRUE;
      }
  
      while(fgets(line, MS_BUFFER_LENGTH, stream) != NULL) { /* now on to the end of the template */
        if(strchr(line, '[') != NULL) {
          tmpline = processLine(mapserv, line, NULL, QUERY); /* no multiline tags are allowed in a join */
          if(!tmpline) {
             msFree(outbuf);
             fclose(stream);
             return NULL;
          }
          outbuf = msStringConcatenate(outbuf, tmpline);
          free(tmpline);
        } else /* no subs, just echo */
          outbuf = msStringConcatenate(outbuf, line);
      }
  
      rewind(stream);
      IGUR_voidp(fgets(line, MS_BUFFER_LENGTH, stream)); /* skip the first line since it's the magic string */
    } /* next record */
  
    if(records==MS_TRUE && join->footer) {
      if(stream) fclose(stream);
      if((stream = fopen(msBuildPath(szPath, mapserv->map->mappath, join->footer), "r")) == NULL) {
        msSetError(MS_IOERR, "Error while opening join footer file %s.", "processOneToManyJoin()", join->footer);
        msFree(outbuf);
        return(NULL);
      }
  
      if(isValidTemplate(stream, join->footer) != MS_TRUE) {
        msFree(outbuf);
        fclose(stream);
        return NULL;
      }
  
      /* echo file to the output buffer, no substitutions */
      while(fgets(line, MS_BUFFER_LENGTH, stream) != NULL) outbuf = msStringConcatenate(outbuf, line);
  
      fclose(stream);
    }
  
    /* clear any data associated with the join */
    msFreeCharArray(join->values, join->numitems);
    join->values = NULL;
  
    return(outbuf);
  }
  
  /*
  ** Process a single line in the template. A few tags (e.g. [resultset]...[/resultset]) can be multi-line so
  ** we pass the filehandle to look ahead if necessary.
  */
  static char *processLine(mapservObj *mapserv, char *instr, FILE *stream, int mode)
  {
    int i, j;
  #define PROCESSLINE_BUFLEN 5120
    char repstr[PROCESSLINE_BUFLEN], substr[PROCESSLINE_BUFLEN], *outstr; /* repstr = replace string, substr = sub string */
    struct hashObj *tp=NULL;
    char *encodedstr;
  
    rectObj llextent;
    pointObj llpoint;
  
    outstr = msStrdup(instr); /* work from a copy */
  
    if(strstr(outstr, "[version]")) outstr = msReplaceSubstring(outstr, "[version]",  msGetVersion());
  
    snprintf(repstr, PROCESSLINE_BUFLEN, "%s%s%s.%s", mapserv->map->web.imageurl, mapserv->map->name, mapserv->Id, MS_IMAGE_EXTENSION(mapserv->map->outputformat));
    outstr = msReplaceSubstring(outstr, "[img]", repstr);
    snprintf(repstr, PROCESSLINE_BUFLEN, "%s%sref%s.%s", mapserv->map->web.imageurl, mapserv->map->name, mapserv->Id, MS_IMAGE_EXTENSION(mapserv->map->outputformat));
    outstr = msReplaceSubstring(outstr, "[ref]", repstr);
  
    if(strstr(outstr, "[errmsg")) {
      char *errmsg = msGetErrorString(";");
      if(!errmsg) errmsg = msStrdup("Error message buffer is empty."); /* should never happen, but just in case... */
      outstr = msReplaceSubstring(outstr, "[errmsg]", errmsg);
      encodedstr = msEncodeUrl(errmsg);
      outstr = msReplaceSubstring(outstr, "[errmsg_esc]", encodedstr);
      free(errmsg);
      free(encodedstr);
    }
  
    if(strstr(outstr, "[legend]")) {
      /* if there's a template legend specified, use it */
      if(mapserv->map->legend.template) {
        char *legendTemplate;
  
        legendTemplate = generateLegendTemplate(mapserv);
        if(legendTemplate) {
          outstr = msReplaceSubstring(outstr, "[legend]", legendTemplate);
  
          free(legendTemplate);
        } else /* error already generated by (generateLegendTemplate()) */
          return NULL;
      } else { /* if not display gif image with all legend icon */
        snprintf(repstr, PROCESSLINE_BUFLEN, "%s%sleg%s.%s", mapserv->map->web.imageurl, mapserv->map->name, mapserv->Id, MS_IMAGE_EXTENSION(mapserv->map->outputformat));
        outstr = msReplaceSubstring(outstr, "[legend]", repstr);
      }
    }
  
    snprintf(repstr, PROCESSLINE_BUFLEN, "%s%ssb%s.%s", mapserv->map->web.imageurl, mapserv->map->name, mapserv->Id, MS_IMAGE_EXTENSION(mapserv->map->outputformat));
    outstr = msReplaceSubstring(outstr, "[scalebar]", repstr);
  
    if(mapserv->savequery) {
      snprintf(repstr, PROCESSLINE_BUFLEN, "%s%s%s%s", mapserv->map->web.imagepath, mapserv->map->name, mapserv->Id, MS_QUERY_EXTENSION);
      outstr = msReplaceSubstring(outstr, "[queryfile]", repstr);
    }
  
    if(mapserv->savemap) {
      snprintf(repstr, PROCESSLINE_BUFLEN, "%s%s%s.map", mapserv->map->web.imagepath, mapserv->map->name, mapserv->Id);
      outstr = msReplaceSubstring(outstr, "[map]", repstr);
    }
  
    if(strstr(outstr,"[mapserv_onlineresource]")) {
      char *ol;
  #if defined(USE_WMS_SVR) || defined (USE_WFS_SVR) || defined (USE_WCS_SVR) || defined(USE_SOS_SVR) || defined(USE_WMS_LYR) || defined(USE_WFS_LYR)
      ol = msOWSGetOnlineResource(mapserv->map, "O", "onlineresource", mapserv->request);
  #else
      ol = msBuildOnlineResource(mapserv->map, mapserv->request);
  #endif
      outstr = msReplaceSubstring(outstr, "[mapserv_onlineresource]",ol);
      msFree(ol);
    }
  
    if(getenv("HTTP_HOST")) {
      snprintf(repstr, PROCESSLINE_BUFLEN, "%s", getenv("HTTP_HOST"));
      outstr = msReplaceSubstring(outstr, "[host]", repstr);
    }
    if(getenv("SERVER_PORT")) {
      snprintf(repstr, PROCESSLINE_BUFLEN, "%s", getenv("SERVER_PORT"));
      outstr = msReplaceSubstring(outstr, "[port]", repstr);
    }
  
    snprintf(repstr, PROCESSLINE_BUFLEN, "%s", mapserv->Id);
    outstr = msReplaceSubstring(outstr, "[id]", repstr);
  
    repstr[0] = '\0'; /* Layer list for a "POST" request */
    for(i=0; i<mapserv->NumLayers; i++) {
      strlcat(repstr, mapserv->Layers[i], sizeof(repstr));
      strlcat(repstr, " ", sizeof(repstr));
    }
    msStringTrimBlanks(repstr);
    encodedstr = msEncodeHTMLEntities(repstr);
    outstr = msReplaceSubstring(outstr, "[layers]", encodedstr);
    free(encodedstr);
  
    encodedstr = msEncodeUrl(repstr);
    outstr = msReplaceSubstring(outstr, "[layers_esc]", encodedstr);
    free(encodedstr);
  
    strcpy(repstr, ""); /* list of ALL layers that can be toggled */
    repstr[0] = '\0';
    for(i=0; i<mapserv->map->numlayers; i++) {
      if(GET_LAYER(mapserv->map, i)->status != MS_DEFAULT && GET_LAYER(mapserv->map, i)->name != NULL) {
        strlcat(repstr, GET_LAYER(mapserv->map, i)->name, sizeof(repstr));
        strlcat(repstr, " ", sizeof(repstr));
      }
    }
    msStringTrimBlanks(repstr);
    outstr = msReplaceSubstring(outstr, "[toggle_layers]", repstr);
  
    encodedstr = msEncodeUrl(repstr);
    outstr = msReplaceSubstring(outstr, "[toggle_layers_esc]", encodedstr);
    free(encodedstr);
  
    for(i=0; i<mapserv->map->numlayers; i++) { /* Set form widgets (i.e. checkboxes, radio and select lists), note that default layers don't show up here */
      if(isOn(mapserv, GET_LAYER(mapserv->map, i)->name, GET_LAYER(mapserv->map, i)->group) == MS_TRUE) {
        if(GET_LAYER(mapserv->map, i)->group) {
          snprintf(substr, PROCESSLINE_BUFLEN, "[%s_select]", GET_LAYER(mapserv->map, i)->group);
          outstr = msReplaceSubstring(outstr, substr, "selected=\"selected\"");
          snprintf(substr, PROCESSLINE_BUFLEN, "[%s_check]", GET_LAYER(mapserv->map, i)->group);
          outstr = msReplaceSubstring(outstr, substr, "checked=\"checked\"");
        }
        if(GET_LAYER(mapserv->map, i)->name) {
          snprintf(substr, PROCESSLINE_BUFLEN, "[%s_select]", GET_LAYER(mapserv->map, i)->name);
          outstr = msReplaceSubstring(outstr, substr, "selected=\"selected\"");
          snprintf(substr, PROCESSLINE_BUFLEN, "[%s_check]", GET_LAYER(mapserv->map, i)->name);
          outstr = msReplaceSubstring(outstr, substr, "checked=\"checked\"");
        }
      } else {
        if(GET_LAYER(mapserv->map, i)->group) {
          snprintf(substr, PROCESSLINE_BUFLEN, "[%s_select]", GET_LAYER(mapserv->map, i)->group);
          outstr = msReplaceSubstring(outstr, substr, "");
          snprintf(substr, PROCESSLINE_BUFLEN, "[%s_check]", GET_LAYER(mapserv->map, i)->group);
          outstr = msReplaceSubstring(outstr, substr, "");
        }
        if(GET_LAYER(mapserv->map, i)->name) {
          snprintf(substr, PROCESSLINE_BUFLEN, "[%s_select]", GET_LAYER(mapserv->map, i)->name);
          outstr = msReplaceSubstring(outstr, substr, "");
          snprintf(substr, PROCESSLINE_BUFLEN, "[%s_check]", GET_LAYER(mapserv->map, i)->name);
          outstr = msReplaceSubstring(outstr, substr, "");
        }
      }
    }
  
    for(i=-1; i<=1; i++) { /* make zoom direction persistant */
      if(mapserv->ZoomDirection == i) {
        snprintf(substr, sizeof(substr), "[zoomdir_%d_select]", i);
        outstr = msReplaceSubstring(outstr, substr, "selected=\"selected\"");
        snprintf(substr, sizeof(substr), "[zoomdir_%d_check]", i);
        outstr = msReplaceSubstring(outstr, substr, "checked=\"checked\"");
      } else {
        snprintf(substr, sizeof(substr), "[zoomdir_%d_select]", i);
        outstr = msReplaceSubstring(outstr, substr, "");
        snprintf(substr, sizeof(substr), "[zoomdir_%d_check]", i);
        outstr = msReplaceSubstring(outstr, substr, "");
      }
    }
  
    for(i=MINZOOM; i<=MAXZOOM; i++) { /* make zoom persistant */
      if(mapserv->Zoom == i) {
        snprintf(substr, sizeof(substr), "[zoom_%d_select]", i);
        outstr = msReplaceSubstring(outstr, substr, "selected=\"selected\"");
        snprintf(substr, sizeof(substr), "[zoom_%d_check]", i);
        outstr = msReplaceSubstring(outstr, substr, "checked=\"checked\"");
      } else {
        snprintf(substr, sizeof(substr), "[zoom_%d_select]", i);
        outstr = msReplaceSubstring(outstr, substr, "");
        snprintf(substr, sizeof(substr), "[zoom_%d_check]", i);
        outstr = msReplaceSubstring(outstr, substr, "");
      }
    }
  
    /* allow web object metadata access in template */
  
    /*
     * reworked by SG to use HashTable methods
     */
  
    if(&(mapserv->map->web.metadata) && strstr(outstr, "web_")) {
      for (j=0; j<MS_HASHSIZE; j++) {
        if(mapserv->map->web.metadata.items[j] != NULL) {
          for(tp=mapserv->map->web.metadata.items[j]; tp!=NULL; tp=tp->next) {
            snprintf(substr, PROCESSLINE_BUFLEN, "[web_%s]", tp->key);
            outstr = msReplaceSubstring(outstr, substr, tp->data);
            snprintf(substr, PROCESSLINE_BUFLEN, "[web_%s_esc]", tp->key);
  
            encodedstr = msEncodeUrl(tp->data);
            outstr = msReplaceSubstring(outstr, substr, encodedstr);
            free(encodedstr);
          }
        }
      }
    }
  
    /* allow layer metadata access in template */
    for(i=0; i<mapserv->map->numlayers; i++) {
      if(&(GET_LAYER(mapserv->map, i)->metadata) && GET_LAYER(mapserv->map, i)->name && strstr(outstr, GET_LAYER(mapserv->map, i)->name)) {
        for(j=0; j<MS_HASHSIZE; j++) {
          if(GET_LAYER(mapserv->map, i)->metadata.items[j] != NULL) {
            for(tp=GET_LAYER(mapserv->map, i)->metadata.items[j]; tp!=NULL; tp=tp->next) {
              snprintf(substr, PROCESSLINE_BUFLEN, "[%s_%s]", GET_LAYER(mapserv->map, i)->name, tp->key);
              if(GET_LAYER(mapserv->map, i)->status == MS_ON)
                outstr = msReplaceSubstring(outstr, substr, tp->data);
              else
                outstr = msReplaceSubstring(outstr, substr, "");
              snprintf(substr, PROCESSLINE_BUFLEN, "[%s_%s_esc]", GET_LAYER(mapserv->map, i)->name, tp->key);
              if(GET_LAYER(mapserv->map, i)->status == MS_ON) {
                encodedstr = msEncodeUrl(tp->data);
                outstr = msReplaceSubstring(outstr, substr, encodedstr);
                free(encodedstr);
              } else
                outstr = msReplaceSubstring(outstr, substr, "");
            }
          }
        }
      }
    }
  
    snprintf(repstr, sizeof(repstr), "%f", mapserv->mappnt.x);
    outstr = msReplaceSubstring(outstr, "[mapx]", repstr);
    snprintf(repstr, sizeof(repstr), "%f", mapserv->mappnt.y);
    outstr = msReplaceSubstring(outstr, "[mapy]", repstr);
  
    snprintf(repstr, sizeof(repstr), "%f", mapserv->map->extent.minx); /* Individual mapextent elements for spatial query building, deprecated. */
    outstr = msReplaceSubstring(outstr, "[minx]", repstr);
    snprintf(repstr, sizeof(repstr), "%f", mapserv->map->extent.maxx);
    outstr = msReplaceSubstring(outstr, "[maxx]", repstr);
    snprintf(repstr, sizeof(repstr), "%f", mapserv->map->extent.miny);
    outstr = msReplaceSubstring(outstr, "[miny]", repstr);
    snprintf(repstr, sizeof(repstr), "%f", mapserv->map->extent.maxy);
    outstr = msReplaceSubstring(outstr, "[maxy]", repstr);
  
    if(processDateTag( &outstr ) != MS_SUCCESS)
      return(NULL);
  
    if(processExtentTag(mapserv, &outstr, "mapext", &(mapserv->map->extent), &(mapserv->map->projection)) != MS_SUCCESS)
      return(NULL);
    if(processExtentTag(mapserv, &outstr, "mapext_esc", &(mapserv->map->extent), &(mapserv->map->projection)) != MS_SUCCESS) /* depricated */
      return(NULL);
  
    snprintf(repstr, sizeof(repstr), "%f", (mapserv->map->extent.maxx-mapserv->map->extent.minx)); /* useful for creating cachable extents (i.e. 0 0 dx dy) with legends and scalebars */
    outstr = msReplaceSubstring(outstr, "[dx]", repstr);
    snprintf(repstr, sizeof(repstr), "%f", (mapserv->map->extent.maxy-mapserv->map->extent.miny));
    outstr = msReplaceSubstring(outstr, "[dy]", repstr);
  
    snprintf(repstr, sizeof(repstr), "%f", mapserv->RawExt.minx); /* Individual raw extent elements for spatial query building, deprecated. */
    outstr = msReplaceSubstring(outstr, "[rawminx]", repstr);
    snprintf(repstr, sizeof(repstr), "%f", mapserv->RawExt.maxx);
    outstr = msReplaceSubstring(outstr, "[rawmaxx]", repstr);
    snprintf(repstr, sizeof(repstr), "%f", mapserv->RawExt.miny);
    outstr = msReplaceSubstring(outstr, "[rawminy]", repstr);
    snprintf(repstr, sizeof(repstr), "%f", mapserv->RawExt.maxy);
    outstr = msReplaceSubstring(outstr, "[rawmaxy]", repstr);
  
    if(processExtentTag(mapserv, &outstr, "rawext", &(mapserv->RawExt), &(mapserv->map->projection)) != MS_SUCCESS)
      return(NULL);
    if(processExtentTag(mapserv, &outstr, "rawext_esc", &(mapserv->RawExt), &(mapserv->map->projection)) != MS_SUCCESS) /* depricated */
      return(NULL);
  
    if((strstr(outstr, "lat]") || strstr(outstr, "lon]") || strstr(outstr, "lon_esc]"))
        && mapserv->map->projection.proj != NULL
        && !msProjIsGeographicCRS(&(mapserv->map->projection)) ) {
      llextent=mapserv->map->extent;
      llpoint=mapserv->mappnt;
      msProjectRect(&(mapserv->map->projection), &(mapserv->map->latlon), &llextent);
      msProjectPoint(&(mapserv->map->projection), &(mapserv->map->latlon), &llpoint);
  
      snprintf(repstr, sizeof(repstr), "%f", llpoint.x);
      outstr = msReplaceSubstring(outstr, "[maplon]", repstr);
      snprintf(repstr, sizeof(repstr), "%f", llpoint.y);
      outstr = msReplaceSubstring(outstr, "[maplat]", repstr);
  
      snprintf(repstr, sizeof(repstr), "%f", llextent.minx); /* map extent as lat/lon */
      outstr = msReplaceSubstring(outstr, "[minlon]", repstr);
      snprintf(repstr, sizeof(repstr), "%f", llextent.maxx);
      outstr = msReplaceSubstring(outstr, "[maxlon]", repstr);
      snprintf(repstr, sizeof(repstr), "%f", llextent.miny);
      outstr = msReplaceSubstring(outstr, "[minlat]", repstr);
      snprintf(repstr, sizeof(repstr), "%f", llextent.maxy);
      outstr = msReplaceSubstring(outstr, "[maxlat]", repstr);
  
      if(processExtentTag(mapserv, &outstr, "mapext_latlon", &(llextent), NULL) != MS_SUCCESS)
        return(NULL);
      if(processExtentTag(mapserv, &outstr, "mapext_latlon_esc", &(llextent), NULL) != MS_SUCCESS) /* depricated */
        return(NULL);
    }
  
    /* submitted by J.F (bug 1102) */
    if(mapserv->map->reference.status == MS_ON) {
      snprintf(repstr, sizeof(repstr), "%f", mapserv->map->reference.extent.minx); /* Individual reference map extent elements for spatial query building, depricated. */
      outstr = msReplaceSubstring(outstr, "[refminx]", repstr);
      snprintf(repstr, sizeof(repstr), "%f", mapserv->map->reference.extent.maxx);
      outstr = msReplaceSubstring(outstr, "[refmaxx]", repstr);
      snprintf(repstr, sizeof(repstr), "%f", mapserv->map->reference.extent.miny);
      outstr = msReplaceSubstring(outstr, "[refminy]", repstr);
      snprintf(repstr, sizeof(repstr), "%f", mapserv->map->reference.extent.maxy);
      outstr = msReplaceSubstring(outstr, "[refmaxy]", repstr);
  
      if(processExtentTag(mapserv, &outstr, "refext", &(mapserv->map->reference.extent), &(mapserv->map->projection)) != MS_SUCCESS)
        return(NULL);
      if(processExtentTag(mapserv, &outstr, "refext_esc", &(mapserv->map->reference.extent), &(mapserv->map->projection)) != MS_SUCCESS) /* depricated */
        return(NULL);
    }
  
    snprintf(repstr, sizeof(repstr), "%d %d", mapserv->map->width, mapserv->map->height);
    outstr = msReplaceSubstring(outstr, "[mapsize]", repstr);
  
    encodedstr = msEncodeUrl(repstr);
    outstr = msReplaceSubstring(outstr, "[mapsize_esc]", encodedstr);
    free(encodedstr);
  
    snprintf(repstr, sizeof(repstr), "%d", mapserv->map->width);
    outstr = msReplaceSubstring(outstr, "[mapwidth]", repstr);
    snprintf(repstr, sizeof(repstr), "%d", mapserv->map->height);
    outstr = msReplaceSubstring(outstr, "[mapheight]", repstr);
  
    snprintf(repstr, sizeof(repstr), "%f", mapserv->map->scaledenom);
    outstr = msReplaceSubstring(outstr, "[scale]", repstr);
    outstr = msReplaceSubstring(outstr, "[scaledenom]", repstr);
    snprintf(repstr, sizeof(repstr), "%f", mapserv->map->cellsize);
    outstr = msReplaceSubstring(outstr, "[cellsize]", repstr);
  
    snprintf(repstr, sizeof(repstr), "%.1f %.1f", (mapserv->map->width)/2.0, (mapserv->map->height)/2.0); /* not subtracting 1 from image dimensions (see bug 633) */
    outstr = msReplaceSubstring(outstr, "[center]", repstr);
    snprintf(repstr, sizeof(repstr), "%.1f", (mapserv->map->width)/2.0);
    outstr = msReplaceSubstring(outstr, "[center_x]", repstr);
    snprintf(repstr, sizeof(repstr), "%.1f", (mapserv->map->height)/2.0);
    outstr = msReplaceSubstring(outstr, "[center_y]", repstr);
  
    /* These are really for situations with multiple result sets only, but often used in header/footer   */
    snprintf(repstr, sizeof(repstr), "%d", mapserv->NR); /* total number of results */
    outstr = msReplaceSubstring(outstr, "[nr]", repstr);
    snprintf(repstr, sizeof(repstr), "%d", mapserv->NL); /* total number of layers with results */
    outstr = msReplaceSubstring(outstr, "[nl]", repstr);
  
    if(mapserv->resultlayer) {
      if(strstr(outstr, "[items]") != NULL) {
        char *itemstr=NULL;
  
        itemstr = msJoinStrings(mapserv->resultlayer->items, mapserv->resultlayer->numitems, ",");
        outstr = msReplaceSubstring(outstr, "[items]", itemstr);
        free(itemstr);
      }
  
      snprintf(repstr, sizeof(repstr), "%d", mapserv->NLR); /* total number of results within this layer */
      outstr = msReplaceSubstring(outstr, "[nlr]", repstr);
      snprintf(repstr, sizeof(repstr), "%d", mapserv->RN); /* sequential (eg. 1..n) result number within all layers */
      outstr = msReplaceSubstring(outstr, "[rn]", repstr);
      snprintf(repstr, sizeof(repstr), "%d", mapserv->LRN); /* sequential (eg. 1..n) result number within this layer */
      outstr = msReplaceSubstring(outstr, "[lrn]", repstr);
      outstr = msReplaceSubstring(outstr, "[cl]", mapserv->resultlayer->name); /* current layer name */
      /* if(resultlayer->description) outstr = msReplaceSubstring(outstr, "[cd]", resultlayer->description); */ /* current layer description */
  
      /* allow layer metadata access when there is a current result layer (implicitly a query template) */
      if(&(mapserv->resultlayer->metadata) && strstr(outstr, "[metadata_")) {
        for(i=0; i<MS_HASHSIZE; i++) {
          if(mapserv->resultlayer->metadata.items[i] != NULL) {
            for(tp=mapserv->resultlayer->metadata.items[i]; tp!=NULL; tp=tp->next) {
              snprintf(substr, PROCESSLINE_BUFLEN, "[metadata_%s]", tp->key);
              outstr = msReplaceSubstring(outstr, substr, tp->data);
  
              snprintf(substr, PROCESSLINE_BUFLEN, "[metadata_%s_esc]", tp->key);
              encodedstr = msEncodeUrl(tp->data);
              outstr = msReplaceSubstring(outstr, substr, encodedstr);
              free(encodedstr);
            }
          }
        }
      }
    }
  
    if(mode != QUERY) {
      if(processResultSetTag(mapserv, &outstr, stream) != MS_SUCCESS) {
        msFree(outstr);
        return(NULL);
      }
    } else { /* return shape and/or values */
  
      snprintf(repstr, sizeof(repstr), "%f %f", (mapserv->resultshape.bounds.maxx + mapserv->resultshape.bounds.minx)/2, (mapserv->resultshape.bounds.maxy + mapserv->resultshape.bounds.miny)/2);
      outstr = msReplaceSubstring(outstr, "[shpmid]", repstr);
      snprintf(repstr, sizeof(repstr), "%f", (mapserv->resultshape.bounds.maxx + mapserv->resultshape.bounds.minx)/2);
      outstr = msReplaceSubstring(outstr, "[shpmidx]", repstr);
      snprintf(repstr, sizeof(repstr), "%f", (mapserv->resultshape.bounds.maxy + mapserv->resultshape.bounds.miny)/2);
      outstr = msReplaceSubstring(outstr, "[shpmidy]", repstr);
  
      if(processExtentTag(mapserv, &outstr, "shpext", &(mapserv->resultshape.bounds), &(mapserv->resultlayer->projection)) != MS_SUCCESS)
        return(NULL);
      if(processExtentTag(mapserv, &outstr, "shpext_esc", &(mapserv->resultshape.bounds), &(mapserv->resultlayer->projection)) != MS_SUCCESS) /* depricated */
        return(NULL);
  
      snprintf(repstr, sizeof(repstr), "%d", mapserv->resultshape.classindex);
      outstr = msReplaceSubstring(outstr, "[shpclass]", repstr);
  
      if(processShpxyTag(mapserv->resultlayer, &outstr, &mapserv->resultshape) != MS_SUCCESS)
        return(NULL);
  
      if(processShplabelTag(mapserv->resultlayer, &outstr, &mapserv->resultshape) != MS_SUCCESS)
        return(NULL);
  
      snprintf(repstr, sizeof(repstr), "%f", mapserv->resultshape.bounds.minx);
      outstr = msReplaceSubstring(outstr, "[shpminx]", repstr);
      snprintf(repstr, sizeof(repstr), "%f", mapserv->resultshape.bounds.miny);
      outstr = msReplaceSubstring(outstr, "[shpminy]", repstr);
      snprintf(repstr, sizeof(repstr), "%f", mapserv->resultshape.bounds.maxx);
      outstr = msReplaceSubstring(outstr, "[shpmaxx]", repstr);
      snprintf(repstr, sizeof(repstr), "%f", mapserv->resultshape.bounds.maxy);
      outstr = msReplaceSubstring(outstr, "[shpmaxy]", repstr);
  
      snprintf(repstr, sizeof(repstr), "%ld", mapserv->resultshape.index);
      outstr = msReplaceSubstring(outstr, "[shpidx]", repstr);
      snprintf(repstr, sizeof(repstr), "%d", mapserv->resultshape.tileindex);
      outstr = msReplaceSubstring(outstr, "[tileidx]", repstr);
  
      /* return ALL attributes in one delimeted list */
      if(strstr(outstr, "[values]") != NULL) {
        char *valuestr=NULL;
  
        valuestr = msJoinStrings(mapserv->resultshape.values, mapserv->resultlayer->numitems, ",");
        outstr = msReplaceSubstring(outstr, "[values]", valuestr);
        free(valuestr);
      }
  
      for(i=0; i<mapserv->resultlayer->numitems; i++) {
        /* by default let's encode attributes for HTML presentation */
        snprintf(substr, PROCESSLINE_BUFLEN, "[%s]", mapserv->resultlayer->items[i]);
        if(strstr(outstr, substr) != NULL) {
          encodedstr = msEncodeHTMLEntities(mapserv->resultshape.values[i]);
          outstr = msReplaceSubstring(outstr, substr, encodedstr);
          free(encodedstr);
        }
  
        /* of course you might want to embed that data in URLs */
        snprintf(substr, PROCESSLINE_BUFLEN, "[%s_esc]", mapserv->resultlayer->items[i]);
        if(strstr(outstr, substr) != NULL) {
          encodedstr = msEncodeUrl(mapserv->resultshape.values[i]);
          outstr = msReplaceSubstring(outstr, substr, encodedstr);
          free(encodedstr);
        }
  
        /* or you might want to access the attributes unaltered */
        snprintf(substr, PROCESSLINE_BUFLEN, "[%s_raw]", mapserv->resultlayer->items[i]);
        if(strstr(outstr, substr) != NULL)
          outstr = msReplaceSubstring(outstr, substr, mapserv->resultshape.values[i]);
      }
  
      if(processItemTag(mapserv->resultlayer, &outstr, &mapserv->resultshape) != MS_SUCCESS)
        return(NULL);
  
      /* handle joins in this next section */
      for(i=0; i<mapserv->resultlayer->numjoins; i++) {
        if(mapserv->resultlayer->joins[i].values) { /* join has data */
          for(j=0; j<mapserv->resultlayer->joins[i].numitems; j++) {
            /* by default let's encode attributes for HTML presentation */
            snprintf(substr, PROCESSLINE_BUFLEN, "[%s_%s]", mapserv->resultlayer->joins[i].name, mapserv->resultlayer->joins[i].items[j]);
            if(strstr(outstr, substr) != NULL) {
              encodedstr = msEncodeHTMLEntities(mapserv->resultlayer->joins[i].values[j]);
              outstr = msReplaceSubstring(outstr, substr, encodedstr);
              free(encodedstr);
            }
  
            /* of course you might want to embed that data in URLs */
            snprintf(substr, PROCESSLINE_BUFLEN, "[%s_%s_esc]", mapserv->resultlayer->joins[i].name, mapserv->resultlayer->joins[i].items[j]);
            if(strstr(outstr, substr) != NULL) {
              encodedstr = msEncodeUrl(mapserv->resultlayer->joins[i].values[j]);
              outstr = msReplaceSubstring(outstr, substr, encodedstr);
              free(encodedstr);
            }
  
            /* or you might want to access the attributes unaltered */
            snprintf(substr, PROCESSLINE_BUFLEN, "[%s_%s_raw]", mapserv->resultlayer->joins[i].name, mapserv->resultlayer->joins[i].items[j]);
            if(strstr(outstr, substr) != NULL)
              outstr = msReplaceSubstring(outstr, substr, mapserv->resultlayer->joins[i].values[j]);
          }
        } else if(mapserv->resultlayer->joins[i].type ==  MS_JOIN_ONE_TO_MANY) { /* one-to-many join */
          char *joinTemplate=NULL;
  
          snprintf(substr, PROCESSLINE_BUFLEN, "[join_%s]", mapserv->resultlayer->joins[i].name);
          if(strstr(outstr, substr) != NULL) {
            joinTemplate = processOneToManyJoin(mapserv, &(mapserv->resultlayer->joins[i]));
            if(joinTemplate) {
              outstr = msReplaceSubstring(outstr, substr, joinTemplate);
              free(joinTemplate);
            } else
              return NULL;
          }
        }
      } /* next join */
  
    } /* end query mode specific substitutions */
  
    if(processIncludeTag(mapserv, &outstr, stream, mode) != MS_SUCCESS)
      return(NULL);
  
    for(i=0; i<mapserv->request->NumParams; i++) {
      /* Replace [variable] tags using values from URL. We cannot offer a
       * [variable_raw] option here due to the risk of XSS.
       *
       * Replacement is case-insensitive. (#4511)
       */
      snprintf(substr, PROCESSLINE_BUFLEN, "[%s]", mapserv->request->ParamNames[i]);
      encodedstr = msEncodeHTMLEntities(mapserv->request->ParamValues[i]);
      outstr = msCaseReplaceSubstring(outstr, substr, encodedstr);
      free(encodedstr);
  
      snprintf(substr, PROCESSLINE_BUFLEN, "[%s_esc]", mapserv->request->ParamNames[i]);
      encodedstr = msEncodeUrl(mapserv->request->ParamValues[i]);
      outstr = msCaseReplaceSubstring(outstr, substr, encodedstr);
      free(encodedstr);
    }
  
    return(outstr);
  }
  
  #define MS_TEMPLATE_BUFFER 1024 /* 1k */
  
  int msReturnPage(mapservObj *mapserv, char *html, int mode, char **papszBuffer)
  {
    FILE *stream;
    char line[MS_BUFFER_LENGTH], *tmpline;
    int   nBufferSize = 0;
    int   nCurrentSize = 0;
    int   nExpandBuffer = 0;
  
    ms_regex_t re; /* compiled regular expression to be matched */
    char szPath[MS_MAXPATHLEN];
  
    if(!html) {
      msSetError(MS_WEBERR, "No template specified", "msReturnPage()");
      return MS_FAILURE;
    }
  
    if(ms_regcomp(&re, MS_TEMPLATE_EXPR, MS_REG_EXTENDED|MS_REG_NOSUB|MS_REG_ICASE) != 0) {
      msSetError(MS_REGEXERR, NULL, "msReturnPage()");
      return MS_FAILURE;
    }
  
    if(ms_regexec(&re, html, 0, NULL, 0) != 0) { /* no match */
      ms_regfree(&re);
      msSetError(MS_WEBERR, "Malformed template name (%s).", "msReturnPage()", html);
      return MS_FAILURE;
    }
    ms_regfree(&re);
  
    if((stream = fopen(msBuildPath(szPath, mapserv->map->mappath, html), "r")) == NULL) {
      msSetError(MS_IOERR, "%s", "msReturnPage()", html);
      return MS_FAILURE;
    }
  
    if(isValidTemplate(stream, html) != MS_TRUE) {
      fclose(stream);
      return MS_FAILURE;
    }
  
    if(papszBuffer) {
      if((*papszBuffer) == NULL) {
        (*papszBuffer) = (char *)msSmallMalloc(MS_TEMPLATE_BUFFER);
        (*papszBuffer)[0] = '\0';
        nBufferSize = MS_TEMPLATE_BUFFER;
        nCurrentSize = 0;
        nExpandBuffer = 1;
      } else {
        nCurrentSize = strlen((*papszBuffer));
        nBufferSize = nCurrentSize;
        nExpandBuffer = (nCurrentSize/MS_TEMPLATE_BUFFER) + 1;
      }
    }
  
    while(fgets(line, MS_BUFFER_LENGTH, stream) != NULL) { /* now on to the end of the file */
  
      if(strchr(line, '[') != NULL) {
        tmpline = processLine(mapserv, line, stream, mode);
        if(!tmpline)
          return MS_FAILURE;
  
        if(papszBuffer) {
          if(nBufferSize <= (int)(nCurrentSize + strlen(tmpline) + 1)) {
            nExpandBuffer = (strlen(tmpline) /  MS_TEMPLATE_BUFFER) + 1;
            nBufferSize = MS_TEMPLATE_BUFFER*nExpandBuffer + strlen((*papszBuffer));
            (*papszBuffer) = (char *) msSmallRealloc((*papszBuffer),sizeof(char)*nBufferSize);
          }
          strcat((*papszBuffer), tmpline);
          nCurrentSize += strlen(tmpline);
        } else
          msIO_fwrite(tmpline, strlen(tmpline), 1, stdout);
  
        free(tmpline);
      } else {
        if(papszBuffer) {
          if(nBufferSize <= (int)(nCurrentSize + strlen(line))) {
            nExpandBuffer = (strlen(line) /  MS_TEMPLATE_BUFFER) + 1;
            nBufferSize = MS_TEMPLATE_BUFFER*nExpandBuffer + strlen((*papszBuffer));
            (*papszBuffer) = (char *)msSmallRealloc((*papszBuffer),sizeof(char)*nBufferSize);
          }
          strcat((*papszBuffer), line);
          nCurrentSize += strlen(line);
        } else
          msIO_fwrite(line, strlen(line), 1, stdout);
      }
      if(!papszBuffer)
        fflush(stdout);
    } /* next line */
  
    fclose(stream);
  
    return MS_SUCCESS;
  }
  
  int msReturnURL(mapservObj* ms, char* url, int mode)
  {
    char *tmpurl;
  
    if(url == NULL) {
      msSetError(MS_WEBERR, "Empty URL.", "msReturnURL()");
      return MS_FAILURE;
    }
  
    tmpurl = processLine(ms, url, NULL, mode); /* URL templates can't handle multi-line tags, hence the NULL file pointer */
  
    if(!tmpurl)
      return MS_FAILURE;
  
    msRedirect(tmpurl);
    free(tmpurl);
  
    return MS_SUCCESS;
  }
  
  /*
  ** Legacy query template parsing where you use headers, footers and such...
  */
  int msReturnNestedTemplateQuery(mapservObj* mapserv, char* pszMimeType, char **papszBuffer)
  {
    int status;
    int i,j,k;
    char buffer[1024];
    int nBufferSize =0;
    int nCurrentSize = 0;
    int nExpandBuffer = 0;
  
    char *template;
  
    layerObj *lp=NULL;
  
    if(papszBuffer) {
      (*papszBuffer) = (char *)msSmallMalloc(MS_TEMPLATE_BUFFER);
      (*papszBuffer)[0] = '\0';
      nBufferSize = MS_TEMPLATE_BUFFER;
      nCurrentSize = 0;
      nExpandBuffer = 1;
    }
  
    msInitShape(&(mapserv->resultshape));
  
    if((mapserv->Mode == ITEMQUERY) || (mapserv->Mode == QUERY)) { /* may need to handle a URL result set since these modes return exactly 1 result */
  
      for(i=(mapserv->map->numlayers-1); i>=0; i--) {
        lp = (GET_LAYER(mapserv->map, i));
  
        if(!lp->resultcache) continue;
        if(lp->resultcache->numresults > 0) break;
      }
  
      if(i >= 0) { /* at least if no result found, mapserver will display an empty template. */
  
        if(lp->resultcache->results[0].classindex >= 0 && lp->class[(int)(lp->resultcache->results[0].classindex)]->template)
          template = lp->class[(int)(lp->resultcache->results[0].classindex)]->template;
        else
          template = lp->template;
  
        if( template == NULL ) {
          msSetError(MS_WEBERR, "No template for layer %s or it's classes.", "msReturnNestedTemplateQuery()", lp->name );
          return MS_FAILURE;
        }
  
        if(TEMPLATE_TYPE(template) == MS_URL) {
          mapserv->resultlayer = lp;
  
  #if 0
          status = msLayerOpen(lp);
          if(status != MS_SUCCESS) return status;
  
          status = msLayerGetItems(lp); /* retrieve all the item names */
          if(status != MS_SUCCESS) return status;
  #endif
  
          status = msLayerGetShape(lp, &(mapserv->resultshape), &(lp->resultcache->results[0]));
          if(status != MS_SUCCESS) return status;
  
          if(lp->numjoins > 0) {
            for(k=0; k<lp->numjoins; k++) {
              status = msJoinConnect(lp, &(lp->joins[k]));
              if(status != MS_SUCCESS) return status;
  
              msJoinPrepare(&(lp->joins[k]), &(mapserv->resultshape));
              msJoinNext(&(lp->joins[k])); /* fetch the first row */
            }
          }
  
          if(papszBuffer == NULL) {
            if(msReturnURL(mapserv, template, QUERY) != MS_SUCCESS) return MS_FAILURE;
          }
  
          msFreeShape(&(mapserv->resultshape));
          /* msLayerClose(lp); */
          mapserv->resultlayer = NULL;
  
          return MS_SUCCESS;
        }
      }
    }
  
    /*
    ** Now we know we're making a template sandwich
    */
    mapserv->NR = mapserv->NL = 0;
    for(i=0; i<mapserv->map->numlayers; i++) { /* compute some totals */
      lp = (GET_LAYER(mapserv->map, i));
  
      if(!lp->resultcache) continue;
  
      if(lp->resultcache->numresults > 0) {
        mapserv->NL++;
        mapserv->NR += lp->resultcache->numresults;
      }
    }
  
    /*
    ** Is this step really necessary for buffered output? Legend and browse templates don't deal with mime-types
    ** so why should this. Note that new-style templates don't buffer the mime-type either.
    */
    if(papszBuffer && mapserv->sendheaders) {
      snprintf(buffer, sizeof(buffer), "Content-Type: %s%c%c", pszMimeType, 10, 10);
      if(nBufferSize <= (int)(nCurrentSize + strlen(buffer) + 1)) {
        nExpandBuffer++;
        (*papszBuffer) = (char *)msSmallRealloc((*papszBuffer), MS_TEMPLATE_BUFFER*nExpandBuffer);
        nBufferSize = MS_TEMPLATE_BUFFER*nExpandBuffer;
      }
      strcat((*papszBuffer), buffer);
      nCurrentSize += strlen(buffer);
    } else if(mapserv->sendheaders) {
      msIO_setHeader("Content-Type","%s",pszMimeType);
      msIO_sendHeaders();
    }
  
    if(mapserv->map->web.header) {
      if(msReturnPage(mapserv, mapserv->map->web.header, BROWSE, papszBuffer) != MS_SUCCESS) return MS_FAILURE;
    }
  
    mapserv->RN = 1; /* overall result number */
    for(i=0; i<mapserv->map->numlayers; i++) {
      mapserv->resultlayer = lp = (GET_LAYER(mapserv->map, mapserv->map->layerorder[i]));
  
      if(!lp->resultcache) continue;
      if(lp->resultcache->numresults <= 0) continue;
  
      mapserv->NLR = lp->resultcache->numresults;
  
  #if 0
      status = msLayerOpen(lp); /* open this layer */
      if(status != MS_SUCCESS) return status;
  
      status = msLayerGetItems(lp); /* retrieve all the item names */
      if(status != MS_SUCCESS) return status;
  #endif
  
      if(lp->numjoins > 0) { /* open any necessary JOINs here */
        for(k=0; k<lp->numjoins; k++) {
          status = msJoinConnect(lp, &(lp->joins[k]));
          if(status != MS_SUCCESS) return status;
        }
      }
  
      if(lp->header) {
        if(msReturnPage(mapserv, lp->header, BROWSE, papszBuffer) != MS_SUCCESS) return MS_FAILURE;
      }
  
      mapserv->LRN = 1; /* layer result number */
      for(j=0; j<lp->resultcache->numresults; j++) {
        status = msLayerGetShape(lp, &(mapserv->resultshape), &(lp->resultcache->results[j]));
        if(status != MS_SUCCESS) return status;
  
        /* prepare any necessary JOINs here (one-to-one only) */
        if(lp->numjoins > 0) {
          for(k=0; k<lp->numjoins; k++) {
            if(lp->joins[k].type == MS_JOIN_ONE_TO_ONE) {
              msJoinPrepare(&(lp->joins[k]), &(mapserv->resultshape));
              msJoinNext(&(lp->joins[k])); /* fetch the first row */
            }
          }
        }
  
        if(lp->resultcache->results[j].classindex >= 0 && lp->class[(int)(lp->resultcache->results[j].classindex)]->template)
          template = lp->class[(int)(lp->resultcache->results[j].classindex)]->template;
        else
          template = lp->template;
  
        if(msReturnPage(mapserv, template, QUERY, papszBuffer) != MS_SUCCESS) {
          msFreeShape(&(mapserv->resultshape));
          return MS_FAILURE;
        }
  
        msFreeShape(&(mapserv->resultshape)); /* init too */
  
        mapserv->RN++; /* increment counters */
        mapserv->LRN++;
      }
  
      if(lp->footer) {
        if(msReturnPage(mapserv, lp->footer, BROWSE, papszBuffer) != MS_SUCCESS) return MS_FAILURE;
      }
  
      /* msLayerClose(lp); */
      mapserv->resultlayer = NULL;
    }
  
    if(mapserv->map->web.footer)
      return msReturnPage(mapserv, mapserv->map->web.footer, BROWSE, papszBuffer);
  
    return MS_SUCCESS;
  }
  
  int msReturnOpenLayersPage(mapservObj *mapserv)
  {
    int i;
    char *buffer = NULL, *layer = NULL;
    const char *tmpUrl = NULL;
    char *openlayersUrl = olUrl;
    char *projection = NULL;
    char *format = NULL;
  
    /* 2 CGI parameters are used in the template. we need to transform them
     * to be sure the case match during the template processing. We also
     * need to search the SRS/CRS parameter to get the projection info. OGC
     * services version >= 1.3.0 uses CRS rather than SRS */
    for( i=0; i<mapserv->request->NumParams; i++) {
      if( (strcasecmp(mapserv->request->ParamNames[i], "SRS") == 0) ||
          (strcasecmp(mapserv->request->ParamNames[i], "CRS") == 0) ) {
        projection = mapserv->request->ParamValues[i];
      } else if(strcasecmp(mapserv->request->ParamNames[i], "LAYERS") == 0) {
        free(mapserv->request->ParamNames[i]);
        mapserv->request->ParamNames[i] = msStrdup("LAYERS");
      } else if(strcasecmp(mapserv->request->ParamNames[i], "VERSION") == 0) {
        free(mapserv->request->ParamNames[i]);
        mapserv->request->ParamNames[i] = msStrdup("VERSION");
      }
    }
    if(mapserv->map->outputformat->mimetype && *mapserv->map->outputformat->mimetype) {
      format = mapserv->map->outputformat->mimetype;
    }
  
    /* check if the environment variable or config MS_OPENLAYERS_JS_URL is set */
    tmpUrl = msGetConfigOption(mapserv->map, "MS_OPENLAYERS_JS_URL");
    if (tmpUrl)
      openlayersUrl = (char*)tmpUrl;
    else if (getenv("MS_OPENLAYERS_JS_URL"))
      openlayersUrl = getenv("MS_OPENLAYERS_JS_URL");
  
    if (mapserv->Mode == BROWSE) {
      msSetError(MS_WMSERR, "At least one layer name required in LAYERS.",
                 "msWMSLoadGetMapParams()");
      layer = processLine(mapserv, olLayerMapServerTag, NULL, BROWSE);
    } else
      layer = processLine(mapserv, olLayerWMSTag, NULL, BROWSE);
  
    buffer = processLine(mapserv, olTemplate, NULL, BROWSE);
    buffer = msReplaceSubstring(buffer, "[openlayers_js_url]", openlayersUrl);
    buffer = msReplaceSubstring(buffer, "[openlayers_layer]", layer);
    if (projection)
      buffer = msReplaceSubstring(buffer, "[openlayers_projection]", projection);
    if (format)
      buffer = msReplaceSubstring(buffer, "[openlayers_format]", format);
    else
      buffer = msReplaceSubstring(buffer, "[openlayers_format]", "image/jpeg");
    msIO_fwrite(buffer, strlen(buffer), 1, stdout);
    free(layer);
    free(buffer);
  
    return MS_SUCCESS;
  }
  
  mapservObj *msAllocMapServObj()
  {
    mapservObj *mapserv = msSmallMalloc(sizeof(mapservObj));
  
    mapserv->savemap=MS_FALSE;
    mapserv->savequery=MS_FALSE; /* should the query and/or map be saved  */
  
    mapserv->sendheaders = MS_TRUE;
  
    mapserv->request = msAllocCgiObj();
  
    mapserv->map=NULL;
  
    mapserv->NumLayers=0; /* number of layers specfied by a user */
    mapserv->MaxLayers=0; /* allocated size of Layers[] array */
    mapserv->Layers = NULL;
  
    mapserv->icon = NULL;
  
    mapserv->RawExt.minx=-1;
    mapserv->RawExt.miny=-1;
    mapserv->RawExt.maxx=-1;
    mapserv->RawExt.maxy=-1;
  
    mapserv->fZoom=1;
    mapserv->Zoom=1; /* default for browsing */
  
    mapserv->resultlayer=NULL;
  
    mapserv->UseShapes=MS_FALSE;
  
    mapserv->mappnt.x=-1;
    mapserv->mappnt.y=-1;
  
    mapserv->ZoomDirection=0; /* whether zooming in or out, default is pan or 0 */
  
    mapserv->Mode=BROWSE; /* can be BROWSE, QUERY, etc. */
  
    sprintf(mapserv->Id, "%ld%d", (long)time(NULL), (int)getpid());
  
    mapserv->CoordSource=NONE;
    mapserv->ScaleDenom=0;
  
    mapserv->ImgRows=-1;
    mapserv->ImgCols=-1;
  
    mapserv->ImgExt.minx=-1;
    mapserv->ImgExt.miny=-1;
    mapserv->ImgExt.maxx=-1;
    mapserv->ImgExt.maxy=-1;
  
    mapserv->ImgBox.minx=-1;
    mapserv->ImgBox.miny=-1;
    mapserv->ImgBox.maxx=-1;
    mapserv->ImgBox.maxy=-1;
  
    mapserv->RefPnt.x=-1;
    mapserv->RefPnt.y=-1;
    mapserv->ImgPnt.x=-1;
    mapserv->ImgPnt.y=-1;
  
    mapserv->Buffer=0;
  
    /*
    ** variables for multiple query results processing
    */
    mapserv->RN=0; /* overall result number */
    mapserv->LRN=0; /* result number within a layer */
    mapserv->NL=0; /* total number of layers with results */
    mapserv->NR=0; /* total number or results */
    mapserv->NLR=0; /* number of results in a layer */
  
    mapserv->SearchMap=MS_FALSE; /* apply pan/zoom BEFORE doing the query (e.g. query the output image rather than the input image) */
  
    mapserv->QueryFile=NULL;
    mapserv->QueryLayer=NULL;
    mapserv->SelectLayer=NULL;
    mapserv->QueryLayerIndex=-1;
    mapserv->SelectLayerIndex=-1;
    mapserv->QueryItem=NULL;
    mapserv->QueryString=NULL;
    mapserv->ShapeIndex=-1;
    mapserv->TileIndex=-1;
    mapserv->TileMode=TILE_GMAP;
    mapserv->TileCoords=NULL;
    mapserv->QueryCoordSource=NONE;
    mapserv->ZoomSize=0; /* zoom absolute magnitude (i.e. > 0) */
  
    mapserv->hittest = NULL;
  
    return mapserv;
  }
  
  void msFreeMapServObj(mapservObj* mapserv)
  {
    int i;
  
    if(mapserv) {
      if( mapserv->map ) {
        if(mapserv->hittest) {
          freeMapHitTests(mapserv->map,mapserv->hittest);
          free(mapserv->hittest);
        }
        msFreeMap(mapserv->map);
        mapserv->map = NULL;
      }
  
      if( mapserv->request ) {
        msFreeCgiObj(mapserv->request);
        mapserv->request = NULL;
      }
  
      for(i=0; i<mapserv->NumLayers; i++)
        msFree(mapserv->Layers[i]);
      msFree(mapserv->Layers);
  
      msFree(mapserv->icon);
  
      msFree(mapserv->QueryItem);
      msFree(mapserv->QueryString);
      msFree(mapserv->QueryLayer);
      msFree(mapserv->SelectLayer);
      msFree(mapserv->QueryFile);
  
      msFree(mapserv->TileCoords);
  
      msFree(mapserv);
    }
  }
  
  /*
  ** Ensure there is at least one free entry in the Layers array.
  **
  ** This function is safe to use for the initial allocation of the Layers[]
  ** array as well (i.e. when MaxLayers==0 and Layers==NULL)
  **
  ** Returns MS_SUCCESS/MS_FAILURE
  */
  int msGrowMapservLayers( mapservObj* mapserv )
  {
    /* Do we need to increase the size of Layers[] by MS_LAYER_ALLOCSIZE? */
    if(mapserv->NumLayers == mapserv->MaxLayers) {
      int i;
  
      if(mapserv->MaxLayers == 0) {
        /* initial allocation of array */
        mapserv->MaxLayers += MS_LAYER_ALLOCSIZE;
        mapserv->NumLayers = 0;
        mapserv->Layers = (char**)msSmallMalloc(mapserv->MaxLayers*sizeof(char*));
      } else {
        /* realloc existing array */
        mapserv->MaxLayers += MS_LAYER_ALLOCSIZE;
        mapserv->Layers = (char**)msSmallRealloc(mapserv->Layers, mapserv->MaxLayers*sizeof(char*));
      }
  
      if(mapserv->Layers == NULL) {
        msSetError(MS_MEMERR, "Failed to allocate memory for Layers array.", "msGrowMappservLayers()");
        return MS_FAILURE;
      }
  
      for(i=mapserv->NumLayers; i<mapserv->MaxLayers; i++) {
        mapserv->Layers[i] = NULL;
      }
    }
  
    return MS_SUCCESS;
  }
  
  /*
  ** Utility function to generate map, legend, scalebar and reference images.
  **
  ** Parameters:
  **   - mapserv: mapserv object (used to extract the map object).
  **   - bQueryMap: if set to TRUE a query map will be created instead of a regular map.
  **   - bReturnOnError: if set to TRUE, the function will return on the first error, else it will try to generate all the images.
  */
  int msGenerateImages(mapservObj *mapserv, int bQueryMap, int bReturnOnError)
  {
    char buffer[1024];
  
    if(mapserv) {
  
      /* render the map OR query map */
      if((!bQueryMap && mapserv->map->status == MS_ON) || (bQueryMap && mapserv->map->querymap.status == MS_ON)) {
        imageObj *image = NULL;
  
        image = msDrawMap(mapserv->map, bQueryMap);
        if(image) {
          snprintf(buffer, sizeof(buffer), "%s%s%s.%s", mapserv->map->web.imagepath, mapserv->map->name, mapserv->Id, MS_IMAGE_EXTENSION(mapserv->map->outputformat));
  
          if(msSaveImage(mapserv->map, image, buffer) != MS_SUCCESS && bReturnOnError) {
            msFreeImage(image);
            return MS_FAILURE;
          }
          msFreeImage(image);
        } else if(bReturnOnError)
          return MS_FAILURE;
      }
  
      /* render the legend */
      if(mapserv->map->legend.status == MS_ON) {
        imageObj *image = NULL;
        image = msDrawLegend(mapserv->map, MS_FALSE, NULL);
        if(image) {
          snprintf(buffer, sizeof(buffer), "%s%sleg%s.%s", mapserv->map->web.imagepath, mapserv->map->name, mapserv->Id, MS_IMAGE_EXTENSION(mapserv->map->outputformat));
  
          if(msSaveImage(mapserv->map, image, buffer) != MS_SUCCESS && bReturnOnError) {
            msFreeImage(image);
            return MS_FAILURE;
          }
          msFreeImage(image);
        } else if(bReturnOnError)
          return MS_FAILURE;
      }
  
      /* render the scalebar */
      if(mapserv->map->scalebar.status == MS_ON) {
        imageObj *image = NULL;
        image = msDrawScalebar(mapserv->map);
        if(image) {
          snprintf(buffer, sizeof(buffer), "%s%ssb%s.%s", mapserv->map->web.imagepath, mapserv->map->name, mapserv->Id, MS_IMAGE_EXTENSION(mapserv->map->outputformat));
          if(msSaveImage(mapserv->map, image, buffer) != MS_SUCCESS && bReturnOnError) {
            msFreeImage(image);
            return MS_FAILURE;
          }
          msFreeImage(image);
        } else if(bReturnOnError)
          return MS_FAILURE;
      }
  
      /* render the reference map */
      if(mapserv->map->reference.status == MS_ON) {
        imageObj *image;
        image = msDrawReferenceMap(mapserv->map);
        if(image) {
          snprintf(buffer, sizeof(buffer), "%s%sref%s.%s", mapserv->map->web.imagepath, mapserv->map->name, mapserv->Id, MS_IMAGE_EXTENSION(mapserv->map->outputformat));
          if(msSaveImage(mapserv->map, image, buffer) != MS_SUCCESS && bReturnOnError) {
            msFreeImage(image);
            return MS_FAILURE;
          }
          msFreeImage(image);
        } else if(bReturnOnError)
          return MS_FAILURE;
      }
  
    }
  
    return MS_SUCCESS;
  }
  
  /*
  ** Utility function to open a template file, process it and
  ** and return into a buffer the processed template. Uses the
  ** template file from the web object. Returns NULL if there is
  ** an error.
  */
  char *msProcessTemplate(mapObj *map, int bGenerateImages, char **names, char **values, int numentries)
  {
    char *pszBuffer = NULL;
  
    if(map) {
  
      /* Initialize object and set appropriate defaults. */
      mapservObj  *mapserv  = NULL;
      mapserv = msAllocMapServObj();
  
      mapserv->map = map;
      mapserv->Mode = BROWSE;
  
      if(names && values && numentries > 0) {
        msFreeCharArray(mapserv->request->ParamNames, mapserv->request->NumParams);
        msFreeCharArray(mapserv->request->ParamValues, mapserv->request->NumParams);
        mapserv->request->ParamNames = names;
        mapserv->request->ParamValues = values;
        mapserv->request->NumParams = numentries;
      }
  
      /*
      ** ISSUE/TODO : some of the name/values should be extracted and
      ** processed (ex imgext, layers, ...) as it is done in function
      ** loadform.
      */
  
      if(bGenerateImages)
        msGenerateImages(mapserv, MS_FALSE, MS_FALSE);
  
      /*
      ** Process the template.
      **
      ** TODO : use web minscaledenom/maxscaledenom depending on the scale.
      */
      if(msReturnPage(mapserv, mapserv->map->web.template, BROWSE, &pszBuffer) != MS_SUCCESS) {
        msFree(pszBuffer);
        pszBuffer = NULL;
      }
  
      /* Don't free the map and names and values arrays since they were passed by reference. */
      mapserv->map = NULL;
      mapserv->request->ParamNames = mapserv->request->ParamValues = NULL;
      mapserv->request->NumParams = 0;
      msFreeMapServObj(mapserv);
    }
  
    return pszBuffer;
  }
  
  /*
  ** Utility method to process the legend template.
  */
  char *msProcessLegendTemplate(mapObj *map, char **names, char **values, int numentries)
  {
    char *pszOutBuf = NULL;
  
    if(map && map->legend.template) {
  
      /* Initialize object and set appropriate defaults. */
      mapservObj  *mapserv  = NULL;
      mapserv = msAllocMapServObj();
  
      mapserv->map = map;
      mapserv->Mode = BROWSE;
  
      if(names && values && numentries > 0) {
        msFreeCharArray(mapserv->request->ParamNames, mapserv->request->NumParams);
        msFreeCharArray(mapserv->request->ParamValues, mapserv->request->NumParams);
        mapserv->request->ParamNames = names;
        mapserv->request->ParamValues = values;
        mapserv->request->NumParams = numentries;
      }
  
      pszOutBuf = generateLegendTemplate(mapserv);
  
      /* Don't free the map and names and values arrays since they were passed by reference. */
      mapserv->map = NULL;
      mapserv->request->ParamNames = mapserv->request->ParamValues = NULL;
      mapserv->request->NumParams = 0;
      msFreeMapServObj(mapserv);
    }
  
    return pszOutBuf;
  }
  
  /*
  ** Utility function that process a template file(s) used in the
  ** query and return the processed template(s) in a buffer.
  */
  char *msProcessQueryTemplate(mapObj *map, int bGenerateImages, char **names, char **values, int numentries)
  {
    char *pszBuffer = NULL;
  
    if(map) {
  
      /* Initialize object and set appropriate defaults. */
      mapservObj *mapserv = NULL;
      mapserv = msAllocMapServObj();
  
      mapserv->map = map;
      mapserv->Mode = QUERY;
  
      if(names && values && numentries > 0) {
        msFreeCharArray(mapserv->request->ParamNames, mapserv->request->NumParams);
        msFreeCharArray(mapserv->request->ParamValues, mapserv->request->NumParams);
        mapserv->request->ParamNames = names;
        mapserv->request->ParamValues = values;
        mapserv->request->NumParams = numentries;
      }
  
      if(bGenerateImages)
        msGenerateImages(mapserv, MS_TRUE, MS_FALSE);
  
      mapserv->sendheaders = MS_FALSE;
      msReturnTemplateQuery(mapserv, mapserv->map->web.queryformat, &pszBuffer);
  
      mapserv->map = NULL;
      mapserv->request->ParamNames = mapserv->request->ParamValues = NULL;
      mapserv->request->NumParams = 0;
      msFreeMapServObj(mapserv);
    }
  
    return pszBuffer;
  }
  