/******************************************************************************
 *
 * Project:  WMS Client Driver
 * Purpose:  Implementation of the OnEarth Tiled WMS minidriver.
 *           http://onearth.jpl.nasa.gov/tiled.html
 * Author:   Lucian Plesea (Lucian dot Plesea at jpl.nasa.gov)
 *           Adam Nowacki
 *
 ******************************************************************************
 * Copyright (c) 2007, Adam Nowacki
 * Copyright (c) 2011-2012, Even Rouault <even dot rouault at mines-paris dot org>
 *
 * 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 or substantial portions of the 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 "wmsdriver.h"
#include "minidriver_tiled_wms.h"

CPP_GDALWMSMiniDriverFactory(TiledWMS)

static char SIG[]="GDAL_WMS TiledWMS: ";

/*
 *\brief Read a number from an xml element
 */

static double getXMLNum(CPLXMLNode *poRoot, const char *pszPath, const char *pszDefault)
{
    return CPLAtof(CPLGetXMLValue(poRoot,pszPath,pszDefault));
}

/*
 *\brief Read a ColorEntry XML node, return a GDALColorEntry structure
 *
 */

static GDALColorEntry GetXMLColorEntry(CPLXMLNode *p)
{
    GDALColorEntry ce;
    ce.c1= static_cast<short>(getXMLNum(p,"c1","0"));
    ce.c2= static_cast<short>(getXMLNum(p,"c2","0"));
    ce.c3= static_cast<short>(getXMLNum(p,"c3","0"));
    ce.c4= static_cast<short>(getXMLNum(p,"c4","255"));
    return ce;
}


/************************************************************************/
/*                           SearchXMLSiblings()                        */
/************************************************************************/

/*
 * \brief Search for a sibling of the root node with a given name.
 *
 * Searches only the next siblings of the node passed in for the named element or attribute.
 * If the first character of the pszElement is '=', the search includes the psRoot node
 * 
 * @param psRoot the root node to search.  This should be a node of type
 * CXT_Element.  NULL is safe.
 *
 * @param pszElement the name of the element or attribute to search for.
 *
 *
 * @return The first matching node or NULL on failure. 
 */

static CPLXMLNode *SearchXMLSiblings( CPLXMLNode *psRoot, const char *pszElement )

{
    if( psRoot == NULL || pszElement == NULL )
        return NULL;

    // If the strings starts with '=', skip it and test the root
    // If not, start testing with the next sibling
    if (pszElement[0]=='=')
        pszElement++;
    else
        psRoot=psRoot->psNext;

    for (;psRoot!=NULL;psRoot=psRoot->psNext)
    {
        if ((psRoot->eType == CXT_Element ||
             psRoot->eType == CXT_Attribute)
             && EQUAL(pszElement,psRoot->pszValue))
            return psRoot;
    }
    return NULL;
}

/************************************************************************/
/*                        SearchLeafGroupName()                         */
/************************************************************************/

/*
 * \brief Search for a leaf TileGroup node by name.
 *
 * @param psRoot the root node to search.  This should be a node of type
 * CXT_Element.  NULL is safe.
 *
 * @param pszElement the name of the TileGroup to search for.
 *
 * @return The XML node of the matching TileGroup or NULL on failure.
 */

static CPLXMLNode *SearchLeafGroupName( CPLXMLNode *psRoot, const char *name )

{
    CPLXMLNode *ret=NULL;

    if( psRoot == NULL || name == NULL ) return NULL;

    // Has to be a leaf TileGroup with the right name
    if (NULL==SearchXMLSiblings(psRoot->psChild,"TiledGroup"))
    {
        if (EQUAL(name,CPLGetXMLValue(psRoot,"Name","")))
        {
            return psRoot;
        }
        else
        { // Try a sibling
            return SearchLeafGroupName(psRoot->psNext,name);
        }
    }
    else
    { // Is metagroup, try children then siblings
        ret=SearchLeafGroupName(psRoot->psChild,name);
        if (NULL!=ret) return ret;
        return SearchLeafGroupName(psRoot->psNext,name);
    }
}

/************************************************************************/
/*                             BandInterp()                             */
/************************************************************************/

/*
 * \brief Utility function to calculate color band interpretation.
 * Only handles Gray, GrayAlpha, RGB and RGBA, based on total band count
 *
 * @param nbands is the total number of bands in the image
 *
 * @param band is the band number, starting with 1
 *
 * @return GDALColorInterp of the band
 */

static GDALColorInterp BandInterp(int nbands, int band) {
    switch (nbands) {
      case 1: return GCI_GrayIndex;
      case 2: return ((band==1)?GCI_GrayIndex:GCI_AlphaBand);
      case 3: // RGB
      case 4: // RBGA
        if (band<3)
            return ((band==1)?GCI_RedBand:GCI_GreenBand);
        return ((band==3)?GCI_BlueBand:GCI_AlphaBand);
      default:
        return GCI_Undefined;
    }
}

/************************************************************************/
/*                              FindBbox()                              */
/************************************************************************/

/*
 * \brief Utility function to find the position of the bbox parameter value 
 * within a request string.  The search for the bbox is case insensitive
 *
 * @param in, the string to search into
 *
 * @return The position from the begining of the string or -1 if not found
 */

static int FindBbox(CPLString in) {

    size_t pos = in.ifind("&bbox=");
    if (pos == std::string::npos)
        return -1;
    else
        return (int)pos + 6;
}

/************************************************************************/
/*                         FindChangePattern()                          */
/************************************************************************/

/*
 * \brief Build the right request pattern based on the change request list
 * It only gets called on initialization
 * @param cdata, possible request strings, white space separated
 * @param substs, the list of substitutions to be applied
 * @param keys, the list of available substitution keys
 * @param ret The return value, a matching request or an empty string
 */

static void FindChangePattern( char *cdata,char **substs, char **keys, CPLString &ret)
{
    char **papszTokens=CSLTokenizeString2(cdata," \t\n\r",
                                           CSLT_STRIPLEADSPACES|CSLT_STRIPENDSPACES);
    ret.clear();

    int matchcount=CSLCount(substs);
    int keycount=CSLCount(keys);
    if (keycount<matchcount)
    {
        CSLDestroy(papszTokens);
        return;
    }

    // A valid string has only the keys in the substs list and none other
    for (int j=0;j<CSLCount(papszTokens);j++)
    {
        ret=papszTokens[j];  // The target string
        bool matches=true;

        for (int k=0;k<keycount;k++)
        {
            const char *key=keys[k];
            int sub_number=CSLPartialFindString(substs,key);
            if (sub_number!=-1)
            { // It is a listed match
                // But is the match for the key position?
                char *found_key=NULL;
                const char *found_value=CPLParseNameValue(substs[sub_number],&found_key);
                if (found_key!=NULL && EQUAL(found_key,key))
                {  // Should exits in the request
                    if (std::string::npos==ret.find(key))
                    {
                        matches=false;
                        break;
                    }
                    // Execute the substitution on the "ret" string
                    URLSearchAndReplace(&ret,key,found_value);
                }
                if (found_key!=NULL) CPLFree(found_key);
            }
            else
            {  // Key not in the subst list, should not match
                if (std::string::npos!=ret.find(key))
                {
                    matches=false;
                    break;
                }
            }
        } // Key loop
        if (matches)
        {
            CSLDestroy(papszTokens);
            return;  // We got the string ready, all keys accounted for and substs applied
        }
    }
    ret.clear();
    CSLDestroy(papszTokens);
}

GDALWMSMiniDriver_TiledWMS::GDALWMSMiniDriver_TiledWMS() {
    m_requests = NULL;
}

GDALWMSMiniDriver_TiledWMS::~GDALWMSMiniDriver_TiledWMS() {
    CSLDestroy(m_requests);
}


// Returns the scale of a WMS request as compared to the base resolution
double GDALWMSMiniDriver_TiledWMS::Scale(const char *request) {
    int bbox=FindBbox(request);
    if (bbox<0) return 0;
    double x,y,X,Y;
    sscanf(request+bbox,"%lf,%lf,%lf,%lf",&x,&y,&X,&Y);
    return (m_data_window.m_x1-m_data_window.m_x0)/(X-x)*m_bsx/m_data_window.m_sx;
}


// Finds, extracts, and returns the highest resolution request string from a list, starting at item i
CPLString GDALWMSMiniDriver_TiledWMS::GetLowestScale(char **& list,int i)
{
    CPLString req;
    double scale=-1;
    int position=-1;
    while (NULL!=list[i])
    {
        double tscale=Scale(list[i]);
        if (tscale>=scale)
        {
            scale=tscale;
            position=i;
        }
        i++;
    }
    if (position>-1)
    {
        req=list[position];
        list = CSLRemoveStrings(list,position,1,NULL);
    }
    return req;
}

/*
 *\Brief Initialize minidriver with info from the server
 */

CPLErr GDALWMSMiniDriver_TiledWMS::Initialize(CPLXMLNode *config)
{
    CPLErr ret = CE_None;
    CPLXMLNode *tileServiceConfig=NULL;
    CPLHTTPResult *psResult=NULL;
    CPLXMLNode *TG=NULL;

    char **requests=NULL;
    char **substs=NULL;
    char **keys=NULL;

    for (int once=1;once;once--) { // Something to break out of
        // Parse info from the service

        m_end_url = CPLGetXMLValue(config,"AdditionalArgs","");
        m_base_url = CPLGetXMLValue(config, "ServerURL", "");
        if (m_base_url.empty()) {
            CPLError(ret=CE_Failure, CPLE_AppDefined, "%s ServerURL missing.",SIG);
            break;
        }

        CPLString tiledGroupName (CPLGetXMLValue(config, "TiledGroupName", ""));
        if (tiledGroupName.empty()) {
            CPLError(ret=CE_Failure, CPLE_AppDefined, "%s TiledGroupName missing.",SIG);
            break;
        }

        // Change strings, key is an attribute, value is the value of the Change node
        // Multiple substitutions are possible
        TG=CPLSearchXMLNode(config, "Change");
        while(TG!=NULL) {
            CPLString name=CPLGetXMLValue(TG,"key","");
            if (name.empty()) {
                CPLError(ret=CE_Failure, CPLE_AppDefined,
                    "%s Change element needs a non-empty \"key\" attribute",SIG);
                break;
            }
            substs=CSLSetNameValue(substs,name,CPLGetXMLValue(TG,"",""));
            TG=SearchXMLSiblings(TG,"Change");
        }
        if (ret!=CE_None) break;

        CPLString getTileServiceUrl = m_base_url + "request=GetTileService";
        psResult = CPLHTTPFetch(getTileServiceUrl, NULL);

        if (NULL==psResult) {
            CPLError(ret=CE_Failure, CPLE_AppDefined, "%s Can't use HTTP", SIG);
            break;
        }

        if ((psResult->nStatus!=0)||(NULL==psResult->pabyData)||('\0'==psResult->pabyData[0])) {
            CPLError(ret=CE_Failure, CPLE_AppDefined, "%s Server response error on GetTileService.",SIG);
            break;
        }

        if (NULL==(tileServiceConfig=CPLParseXMLString((const char*)psResult->pabyData))) {
            CPLError(ret=CE_Failure,CPLE_AppDefined, "%s Error parsing the GetTileService response.",SIG);
            break;
        }

        if (NULL==(TG=CPLSearchXMLNode(tileServiceConfig, "TiledPatterns"))) {
            CPLError(ret=CE_Failure,CPLE_AppDefined,
                "%s Can't locate TiledPatterns in server response.",SIG);
            break;
        }

        // Get the global base_url and bounding box, these can be overwritten at the tileGroup level
        // They are just pointers into existing structures, cleanup is not required
        const char *global_base_url=CPLGetXMLValue(tileServiceConfig,"TiledPatterns.OnlineResource.xlink:href","");
        CPLXMLNode *global_latlonbbox=CPLGetXMLNode(tileServiceConfig, "TiledPatterns.LatLonBoundingBox");
        CPLXMLNode *global_bbox=CPLGetXMLNode(tileServiceConfig, "TiledPatterns.BoundingBox");

        if (NULL==(TG=SearchLeafGroupName(TG->psChild,tiledGroupName))) {
            CPLError(ret=CE_Failure,CPLE_AppDefined,
                "%s Can't locate TiledGroup ""%s"" in server response.",SIG,
                tiledGroupName.c_str());
            break;
        }

        int band_count=atoi(CPLGetXMLValue(TG, "Bands", "3"));

        if (!GDALCheckBandCount(band_count, FALSE)) {
            CPLError(ret=CE_Failure,CPLE_AppDefined,"%s%s",SIG,
                "Invalid number of bands in server response");
            break;
        }

        // Collect all keys defined by this tileset
        if (NULL!=CPLGetXMLNode(TG,"Key")) {
            CPLXMLNode *node=CPLGetXMLNode(TG,"Key");
                while (NULL!=node) {
                    const char *val=CPLGetXMLValue(node,NULL,NULL);
                    if (val) keys=CSLAddString(keys,val);
                    node=SearchXMLSiblings(node,"Key");
                }
        }

       // Data values are attributes, they include NoData Min and Max
       if (0!=CPLGetXMLNode(TG,"DataValues")) {
           const char *nodata=CPLGetXMLValue(TG,"DataValues.NoData",NULL);
           if (nodata!=NULL) m_parent_dataset->WMSSetNoDataValue(nodata);
           const char *min=CPLGetXMLValue(TG,"DataValues.min",NULL);
           if (min!=NULL) m_parent_dataset->WMSSetMinValue(min);
           const char *max=CPLGetXMLValue(TG,"DataValues.max",NULL);
           if (max!=NULL) m_parent_dataset->WMSSetMaxValue(max);
       }

        m_parent_dataset->WMSSetBandsCount(band_count);
        m_parent_dataset->WMSSetDataType(GDALGetDataTypeByName(CPLGetXMLValue(TG, "DataType", "Byte")));
        m_projection_wkt=CPLGetXMLValue(TG, "Projection","");

        m_base_url=CPLGetXMLValue(TG,"OnlineResource.xlink:href",global_base_url);
        if (m_base_url[0]=='\0') {
            CPLError(ret=CE_Failure,CPLE_AppDefined, "%s%s",SIG,
                "Can't locate OnlineResource in the server response.");
            break;
        }

        // Bounding box, local, global, local lat-lon, global lat-lon, in this order
        CPLXMLNode *bbox = CPLGetXMLNode(TG, "BoundingBox");
        if (NULL==bbox) bbox=global_bbox;
        if (NULL==bbox) bbox=CPLGetXMLNode(TG, "LatLonBoundingBox");
        if (NULL==bbox) bbox=global_latlonbbox;

        if (NULL==bbox) {
            CPLError(ret=CE_Failure,CPLE_AppDefined,"%s%s",SIG,
                "Can't locate the LatLonBoundingBox in server response.");
            break;
        }

        m_data_window.m_x0=atof(CPLGetXMLValue(bbox,"minx","0"));
        m_data_window.m_x1=atof(CPLGetXMLValue(bbox,"maxx","-1"));
        m_data_window.m_y0=atof(CPLGetXMLValue(bbox,"maxy","0"));
        m_data_window.m_y1=atof(CPLGetXMLValue(bbox,"miny","-1"));

        if ((m_data_window.m_x1-m_data_window.m_x0)<0) {
            CPLError(ret=CE_Failure,CPLE_AppDefined,"%s%s", SIG,
                "Coordinate order in BBox, problem in server response");
            break;
        }

        // Is there a palette?
        //
        // Format is
        // <Palette>
        //   <Size>N</Size> : Optional
        //   <Model>RGBA|RGB|CMYK|HSV|HLS|L</Model> :mandatory
        //   <Entry idx=i c1=v1 c2=v2 c3=v3 c4=v4/> :Optional
        //   <Entry .../>
        // </Palette>
        // the idx attribute is optional, it autoincrements
        // The entries are actually vertices, interpolation takes place inside
        // The palette starts initialized with zeros
        // HSV and HLS are the similar, with c2 and c3 swapped
        // RGB or RGBA are same
        //

        GDALColorTable *poColorTable=NULL;

        if ((band_count==1) && CPLGetXMLNode(TG,"Palette")) {

            CPLXMLNode *node=CPLGetXMLNode(TG,"Palette");

            int entries=static_cast<int>(getXMLNum(node,"Size","255"));
            GDALPaletteInterp eInterp=GPI_RGB;

            CPLString pModel=CPLGetXMLValue(node,"Model","RGB");
            if (!pModel.empty() && pModel.find("RGB")!=std::string::npos)
                eInterp=GPI_RGB;
            else {
                CPLError(CE_Failure, CPLE_AppDefined,
                    "%s Palette Model %s is unknown, use RGB or RGBA",
                    SIG, pModel.c_str());
                return CE_Failure;
            }

            if ((entries>0)&&(entries<257)) {
                int start_idx, end_idx;
                GDALColorEntry ce_start={0,0,0,255},ce_end={0,0,0,255};

                // Create it and initialize it to nothing
                poColorTable = new GDALColorTable(eInterp);
                poColorTable->CreateColorRamp(0,&ce_start,entries-1,&ce_end);
                // Read the values
                CPLXMLNode *p=CPLGetXMLNode(node,"Entry");
                if (p) {
                    // Initialize the first entry
                    start_idx=static_cast<int>(getXMLNum(p,"idx","0"));
                    ce_start=GetXMLColorEntry(p);
                    if (start_idx<0) {
                        CPLError(CE_Failure, CPLE_AppDefined,
                            "%s Palette index %d not allowed",SIG,start_idx);
                        delete poColorTable;
                        return CE_Failure;
                    }
                    poColorTable->SetColorEntry(start_idx,&ce_start);
                    while (NULL!=(p=SearchXMLSiblings(p,"Entry"))) {
                        // For every entry, create a ramp
                        ce_end=GetXMLColorEntry(p);
                        end_idx=static_cast<int>(getXMLNum(p,"idx",CPLString().FormatC(start_idx+1).c_str()));
                        if ((end_idx<=start_idx)||(start_idx>=entries)) {
                            CPLError(CE_Failure, CPLE_AppDefined,
                                "%s Index Error at index %d",SIG,end_idx);
                            delete poColorTable;
                            return CE_Failure;
                        }
                        poColorTable->CreateColorRamp(start_idx,&ce_start,
                            end_idx,&ce_end);
                        ce_start=ce_end;
                        start_idx=end_idx;
                    }
                }
                m_parent_dataset->SetColorTable(poColorTable);
            } else {
                CPLError(CE_Failure, CPLE_AppDefined,"%s Palette definition error",SIG);
                return CE_Failure;
            }
        }

        int overview_count=0;
        CPLXMLNode *Pattern=TG->psChild;

        m_bsx=m_bsy=-1;
        m_data_window.m_sx=m_data_window.m_sy=0;

        for (int once=1;once;once--) { // Something to break out of
            while ((NULL!=Pattern)&&(NULL!=(Pattern=SearchXMLSiblings(Pattern,"=TilePattern")))) {
                int mbsx,mbsy;

                CPLString request;
                FindChangePattern(Pattern->psChild->pszValue,substs,keys,request);

                char **papszTokens=CSLTokenizeString2(request,"&",0);

                const char* pszWIDTH = CSLFetchNameValue(papszTokens,"WIDTH");
                const char* pszHEIGHT = CSLFetchNameValue(papszTokens,"HEIGHT");
                if (pszWIDTH == NULL || pszHEIGHT == NULL)
                {
                    CPLError(ret=CE_Failure,CPLE_AppDefined,"%s%s",SIG,
                        "Cannot find width and/or height parameters.");
                    overview_count=0;
                    CSLDestroy(papszTokens);
                    break;
                }

                mbsx=atoi(pszWIDTH);
                mbsy=atoi(pszHEIGHT);
                if (m_projection_wkt.empty()) {
                    m_projection_wkt = CSLFetchNameValueDef(papszTokens,"SRS", "");
                    if (!m_projection_wkt.empty())
                        m_projection_wkt=ProjToWKT(m_projection_wkt);
                }

                if (-1==m_bsx) m_bsx=mbsx;
                if (-1==m_bsy) m_bsy=mbsy;
                if ((m_bsy!=mbsy)||(m_bsy!=mbsy)) {
                    CPLError(ret=CE_Failure,CPLE_AppDefined,"%s%s",SIG,
                        "Tileset uses different block sizes.");
                    overview_count=0;
                    CSLDestroy(papszTokens);
                    break;
                }

                double x,y,X,Y;
                if (sscanf(CSLFetchNameValueDef(papszTokens,"BBOX", ""),"%lf,%lf,%lf,%lf",&x,&y,&X,&Y)!=4)
                {
                    CPLError(ret=CE_Failure,CPLE_AppDefined,
                        "%s Error parsing BBOX, pattern %d\n",SIG,overview_count+1);
                    CSLDestroy(papszTokens);
                    break;
                }
                // Pick the largest size
                int sx=static_cast<int>((m_data_window.m_x1-m_data_window.m_x0)/(X-x)*m_bsx);
                int sy=static_cast<int>(fabs((m_data_window.m_y1-m_data_window.m_y0)/(Y-y)*m_bsy));
                if (sx>m_data_window.m_sx) m_data_window.m_sx=sx;
                if (sy>m_data_window.m_sy) m_data_window.m_sy=sy;
                CSLDestroy(papszTokens);

                // Only use overlays where the top coordinate is within a pixel from the top of coverage
                double pix_off,temp;
                pix_off=m_bsy*modf(fabs((Y-m_data_window.m_y0)/(Y-y)),&temp);
                if ((pix_off<1)||((m_bsy-pix_off)<1)) {
                    requests=CSLAddString(requests,request);
                    overview_count++;
                } else
                    CPLError(CE_Warning,CPLE_AppDefined,
                    "%s Overlay size %dX%d can't be used due to alignment",SIG,sx,sy);

                Pattern=Pattern->psNext;

            }

            // The tlevel is needed, the tx and ty are not used by this minidriver
            m_data_window.m_tlevel = 0;
            m_data_window.m_tx = 0;
            m_data_window.m_ty = 0;

            // Make sure the parent_dataset values are set before creating the bands
            m_parent_dataset->WMSSetBlockSize(m_bsx,m_bsy);
            m_parent_dataset->WMSSetRasterSize(m_data_window.m_sx,m_data_window.m_sy);

            m_parent_dataset->WMSSetDataWindow(m_data_window);
            //m_parent_dataset->WMSSetOverviewCount(overview_count);
            m_parent_dataset->WMSSetClamp(false);

            // Ready for the Rasterband creation
            for (int i=0;i<overview_count;i++) {
                CPLString request=GetLowestScale(requests,i);
                double scale=Scale(request);

                // Base scale should be very close to 1
                if ((0==i)&&(fabs(scale-1) > 1e-6)) {
                    CPLError(ret=CE_Failure,CPLE_AppDefined,"%s%s",SIG,
                        "Base resolution pattern missing.");
                    break;
                }

                // Prepare the request and insert it back into the list
                // Find returns an answer relative to the original string start!
                size_t startBbox=FindBbox(request);
                size_t endBbox=request.find('&',startBbox);
                if (endBbox==std::string::npos) endBbox=request.size();
                request.replace(startBbox,endBbox-startBbox,"${GDAL_BBOX}");
                requests = CSLInsertString(requests,i,request);

                // Create the Rasterband or overview
                for (int j = 1; j <= band_count; j++) {
                    if (i!=0)
                        m_parent_dataset->mGetBand(j)->AddOverview(scale);
                    else { // Base resolution
                        GDALWMSRasterBand *band=new
                            GDALWMSRasterBand(m_parent_dataset,j,1);
                        if (poColorTable!=NULL) band->SetColorInterpretation(GCI_PaletteIndex);
                        else band->SetColorInterpretation(BandInterp(band_count,j));
                        m_parent_dataset->mSetBand(j, band);
                    };
                }
            }
            if ((overview_count==0)||(m_bsx<1)||(m_bsy<1)) {
                CPLError(ret=CE_Failure,CPLE_AppDefined,
                    "%s No usable TilePattern elements found",SIG);
                break;
            }
        }
    }

    CSLDestroy(keys);
    CSLDestroy(substs);
    if (tileServiceConfig) CPLDestroyXMLNode(tileServiceConfig);
    if (psResult) CPLHTTPDestroyResult(psResult);

    m_requests=requests;
    return ret;
}

void GDALWMSMiniDriver_TiledWMS::GetCapabilities(GDALWMSMiniDriverCapabilities *caps) {
    caps->m_capabilities_version = 1;
    caps->m_has_arb_overviews = 0;
    caps->m_has_image_request = 1;
    caps->m_has_tiled_image_requeset = 1;
    caps->m_max_overview_count = 32;
}


// not called
void GDALWMSMiniDriver_TiledWMS::ImageRequest(CPL_UNUSED CPLString *url,
                                              CPL_UNUSED const GDALWMSImageRequestInfo &iri) {
}

void GDALWMSMiniDriver_TiledWMS::TiledImageRequest(CPLString *url, const GDALWMSImageRequestInfo &iri, const GDALWMSTiledImageRequestInfo &tiri) {
    *url = m_base_url;
    URLAppend(url,CSLGetField(m_requests,-tiri.m_level));
    URLSearchAndReplace(url,"${GDAL_BBOX}","%013.8f,%013.8f,%013.8f,%013.8f",
                        iri.m_x0,iri.m_y1,iri.m_x1,iri.m_y0);
    URLAppend(url,m_end_url);
}

const char *GDALWMSMiniDriver_TiledWMS::GetProjectionInWKT() {
    return m_projection_wkt.c_str();
}

