/******************************************************************************
 * $Id: gdaljp2metadata.cpp 33579 2016-02-26 23:55:40Z goatbar $
 *
 * Project:  GDAL
 * Purpose:  GDALJP2Metadata - Read GeoTIFF and/or GML georef info.
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *           Even Rouault <even dot rouault at spatialys dot com>
 *
 ******************************************************************************
 * Copyright (c) 2005, Frank Warmerdam <warmerdam@pobox.com>
 * Copyright (c) 2010-2015, Even Rouault <even dot rouault at spatialys dot com>
 * Copyright (c) 2015, European Union Satellite Centre
 *
 * 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 "cpl_string.h"
#include "cpl_minixml.h"
#include "gt_wkt_srs_for_gdal.h"
#include "gdaljp2metadata.h"
#include "gdaljp2metadatagenerator.h"
#include "json.h"
#include "ogr_api.h"
#include "ogr_geometry.h"
#include "ogr_spatialref.h"

CPL_CVSID("$Id: gdaljp2metadata.cpp 33579 2016-02-26 23:55:40Z goatbar $");

static const unsigned char msi_uuid2[16] = {
    0xb1,0x4b,0xf8,0xbd,0x08,0x3d,0x4b,0x43,
    0xa5,0xae,0x8c,0xd7,0xd5,0xa6,0xce,0x03 };

static const unsigned char msig_uuid[16] = {
    0x96,0xA9,0xF1,0xF1,0xDC,0x98,0x40,0x2D,
    0xA7,0xAE,0xD6,0x8E,0x34,0x45,0x18,0x09 };

static const unsigned char xmp_uuid[16] = {
    0xBE,0x7A,0xCF,0xCB,0x97,0xA9,0x42,0xE8,
    0x9C,0x71,0x99,0x94,0x91,0xE3,0xAF,0xAC };

struct _GDALJP2GeoTIFFBox
{
    int    nGeoTIFFSize;
    GByte  *pabyGeoTIFFData;
};

static const int MAX_JP2GEOTIFF_BOXES = 2;

/************************************************************************/
/*                          GDALJP2Metadata()                           */
/************************************************************************/

GDALJP2Metadata::GDALJP2Metadata() :
    nGeoTIFFBoxesCount(0),
    pasGeoTIFFBoxes(NULL),
    nMSIGSize(0),
    pabyMSIGData(NULL),
    papszGMLMetadata(NULL),
    bHaveGeoTransform (FALSE),
    bPixelIsPoint(FALSE),
    pszProjection(NULL),
    nGCPCount(0),
    pasGCPList(NULL),
    papszRPCMD(NULL),
    papszMetadata(NULL),
    pszXMPMetadata(NULL),
    pszGDALMultiDomainMetadata(NULL),
    pszXMLIPR(NULL)
{
    adfGeoTransform[0] = 0.0;
    adfGeoTransform[1] = 1.0;
    adfGeoTransform[2] = 0.0;
    adfGeoTransform[3] = 0.0;
    adfGeoTransform[4] = 0.0;
    adfGeoTransform[5] = 1.0;
}

/************************************************************************/
/*                          ~GDALJP2Metadata()                          */
/************************************************************************/

GDALJP2Metadata::~GDALJP2Metadata()

{
    CPLFree( pszProjection );
    if( nGCPCount > 0 )
    {
        GDALDeinitGCPs( nGCPCount, pasGCPList );
        CPLFree( pasGCPList );
    }
    CSLDestroy(papszRPCMD);

    for( int i=0; i < nGeoTIFFBoxesCount; i++ )
    {
        CPLFree( pasGeoTIFFBoxes[i].pabyGeoTIFFData );
    }
    CPLFree( pasGeoTIFFBoxes );
    CPLFree( pabyMSIGData );
    CSLDestroy( papszGMLMetadata );
    CSLDestroy( papszMetadata );
    CPLFree( pszXMPMetadata );
    CPLFree( pszGDALMultiDomainMetadata );
    CPLFree( pszXMLIPR );
}

/************************************************************************/
/*                            ReadAndParse()                            */
/*                                                                      */
/*      Read a JP2 file and try to collect georeferencing               */
/*      information from the various available forms.  Returns TRUE     */
/*      if anything useful is found.                                    */
/************************************************************************/

int GDALJP2Metadata::ReadAndParse( const char *pszFilename )

{
    VSILFILE *fpLL;

    fpLL = VSIFOpenL( pszFilename, "rb" );

    if( fpLL == NULL )
    {
        CPLDebug( "GDALJP2Metadata", "Could not even open %s.",
                  pszFilename );

        return FALSE;
    }

    int bRet = ReadAndParse( fpLL );
    CPL_IGNORE_RET_VAL(VSIFCloseL( fpLL ));

/* -------------------------------------------------------------------- */
/*      If we still don't have a geotransform, look for a world         */
/*      file.                                                           */
/* -------------------------------------------------------------------- */
    if( !bHaveGeoTransform )
    {
        bHaveGeoTransform =
            GDALReadWorldFile( pszFilename, NULL, adfGeoTransform )
            || GDALReadWorldFile( pszFilename, ".wld", adfGeoTransform );
        bRet |= bHaveGeoTransform;
    }

    return bRet;
}


int GDALJP2Metadata::ReadAndParse( VSILFILE *fpLL )

{
    ReadBoxes( fpLL );

/* -------------------------------------------------------------------- */
/*      Try JP2GeoTIFF, GML and finally MSIG to get something.          */
/* -------------------------------------------------------------------- */
    if( !ParseJP2GeoTIFF() && !ParseGMLCoverageDesc() )
        ParseMSIG();

/* -------------------------------------------------------------------- */
/*      Return success either either of projection or geotransform      */
/*      or gcps.                                                        */
/* -------------------------------------------------------------------- */
    return bHaveGeoTransform
        || nGCPCount > 0
        || (pszProjection != NULL && strlen(pszProjection) > 0)
        || papszRPCMD != NULL;
}

/************************************************************************/
/*                           CollectGMLData()                           */
/*                                                                      */
/*      Read all the asoc boxes after this node, and store the          */
/*      contain xml documents along with the name from the label.       */
/************************************************************************/

void GDALJP2Metadata::CollectGMLData( GDALJP2Box *poGMLData )

{
    GDALJP2Box oChildBox( poGMLData->GetFILE() );

    if( !oChildBox.ReadFirstChild( poGMLData ) )
        return;

    while( strlen(oChildBox.GetType()) > 0 )
    {
        if( EQUAL(oChildBox.GetType(),"asoc") )
        {
            GDALJP2Box oSubChildBox( oChildBox.GetFILE() );

            char *pszLabel = NULL;
            char *pszXML = NULL;

            if( !oSubChildBox.ReadFirstChild( &oChildBox ) )
                break;

            while( strlen(oSubChildBox.GetType()) > 0 )
            {
                if( EQUAL(oSubChildBox.GetType(),"lbl ") )
                    pszLabel = (char *)oSubChildBox.ReadBoxData();
                else if( EQUAL(oSubChildBox.GetType(),"xml ") )
                {
                    pszXML = (char *) oSubChildBox.ReadBoxData();
                    GIntBig nXMLLength = oSubChildBox.GetDataLength();

                    // Some GML data contains \0 instead of \n !
                    // See http://trac.osgeo.org/gdal/ticket/5760
                    if( pszXML != NULL && nXMLLength < 100 * 1024 * 1024 )
                    {
                        GIntBig i;
                        /* coverity[tainted_data] */
                        for(i=nXMLLength-1; i >= 0; i--)
                        {
                            if( pszXML[i] == '\0' )
                                nXMLLength --;
                            else
                                break;
                        }
                        /* coverity[tainted_data] */
                        for(i=0;i<nXMLLength;i++)
                        {
                            if( pszXML[i] == '\0' )
                                break;
                        }
                        if( i < nXMLLength )
                        {
                            CPLPushErrorHandler(CPLQuietErrorHandler);
                            CPLXMLNode* psNode = CPLParseXMLString(pszXML);
                            CPLPopErrorHandler();
                            if( psNode == NULL )
                            {
                                CPLDebug("GMLJP2", "GMLJP2 data contains nul characters inside content. Replacing them by \\n");
                                /* coverity[tainted_data] */
                                for(i=0;i<nXMLLength;i++)
                                {
                                    if( pszXML[i] == '\0' )
                                        pszXML[i] = '\n';
                                }
                            }
                            else
                                CPLDestroyXMLNode(psNode);
                        }
                    }
                }

                if( !oSubChildBox.ReadNextChild( &oChildBox ) )
                    break;
            }

            if( pszLabel != NULL && pszXML != NULL )
            {
                papszGMLMetadata = CSLSetNameValue( papszGMLMetadata,
                                                    pszLabel, pszXML );

                if( strcmp(pszLabel, "gml.root-instance") == 0 &&
                    pszGDALMultiDomainMetadata == NULL &&
                    strstr(pszXML, "GDALMultiDomainMetadata") != NULL )
                {
                    CPLXMLNode* psTree = CPLParseXMLString(pszXML);
                    if( psTree != NULL )
                    {
                        CPLXMLNode* psGDALMDMD = CPLSearchXMLNode(psTree, "GDALMultiDomainMetadata");
                        if( psGDALMDMD )
                            pszGDALMultiDomainMetadata = CPLSerializeXMLTree(psGDALMDMD);
                    }
                    CPLDestroyXMLNode(psTree);
                }
            }

            CPLFree( pszLabel );
            CPLFree( pszXML );
        }

        if( !oChildBox.ReadNextChild( poGMLData ) )
            break;
    }
}

/************************************************************************/
/*                             ReadBoxes()                              */
/************************************************************************/

int GDALJP2Metadata::ReadBoxes( VSILFILE *fpVSIL )

{
    GDALJP2Box oBox( fpVSIL );
    int iBox = 0;

    if (!oBox.ReadFirst())
        return FALSE;

    while( strlen(oBox.GetType()) > 0 )
    {
#ifdef DEBUG
        if (CPLTestBool(CPLGetConfigOption("DUMP_JP2_BOXES", "NO")))
            oBox.DumpReadable(stderr);
#endif

/* -------------------------------------------------------------------- */
/*      Collect geotiff box.                                            */
/* -------------------------------------------------------------------- */
        if( EQUAL(oBox.GetType(),"uuid")
            && memcmp( oBox.GetUUID(), msi_uuid2, 16 ) == 0 )
        {
            /* Erdas JPEG2000 files can in some conditions contain 2 GeoTIFF */
            /* UUID boxes. One that is correct, another one that does not contain */
            /* correct georeferencing. So let's fetch at most 2 of them */
            /* for later analysis. */
            if( nGeoTIFFBoxesCount == MAX_JP2GEOTIFF_BOXES )
            {
                CPLDebug("GDALJP2", "Too many UUID GeoTIFF boxes. Ignoring this one");
            }
            else
            {
                int nGeoTIFFSize = (int) oBox.GetDataLength();
                GByte* pabyGeoTIFFData = oBox.ReadBoxData();
                if (pabyGeoTIFFData == NULL)
                {
                    CPLDebug("GDALJP2", "Cannot read data for UUID GeoTIFF box");
                }
                else
                {
                    pasGeoTIFFBoxes = (GDALJP2GeoTIFFBox*) CPLRealloc(
                        pasGeoTIFFBoxes, sizeof(GDALJP2GeoTIFFBox) * (nGeoTIFFBoxesCount + 1) );
                    pasGeoTIFFBoxes[nGeoTIFFBoxesCount].nGeoTIFFSize = nGeoTIFFSize;
                    pasGeoTIFFBoxes[nGeoTIFFBoxesCount].pabyGeoTIFFData = pabyGeoTIFFData;
                    nGeoTIFFBoxesCount ++;
                }
            }
        }

/* -------------------------------------------------------------------- */
/*      Collect MSIG box.                                               */
/* -------------------------------------------------------------------- */
        if( EQUAL(oBox.GetType(),"uuid")
            && memcmp( oBox.GetUUID(), msig_uuid, 16 ) == 0 )
        {
            if( nMSIGSize == 0 )
            {
                nMSIGSize = (int) oBox.GetDataLength();
                pabyMSIGData = oBox.ReadBoxData();

                if( nMSIGSize < 70
                    || pabyMSIGData == NULL
                    || memcmp( pabyMSIGData, "MSIG/", 5 ) != 0 )
                {
                    CPLFree( pabyMSIGData );
                    pabyMSIGData = NULL;
                    nMSIGSize = 0;
                }
            }
            else
            {
                CPLDebug("GDALJP2", "Too many UUID MSIG boxes. Ignoring this one");
            }
        }

/* -------------------------------------------------------------------- */
/*      Collect XMP box.                                                */
/* -------------------------------------------------------------------- */
        if( EQUAL(oBox.GetType(),"uuid")
            && memcmp( oBox.GetUUID(), xmp_uuid, 16 ) == 0 )
        {
            if( pszXMPMetadata == NULL )
            {
                pszXMPMetadata = (char*) oBox.ReadBoxData();
            }
            else
            {
                CPLDebug("GDALJP2", "Too many UUID XMP boxes. Ignoring this one");
            }
        }

/* -------------------------------------------------------------------- */
/*      Process asoc box looking for Labelled GML data.                 */
/* -------------------------------------------------------------------- */
        if( EQUAL(oBox.GetType(),"asoc") )
        {
            GDALJP2Box oSubBox( fpVSIL );

            if( oSubBox.ReadFirstChild( &oBox ) &&
                EQUAL(oSubBox.GetType(),"lbl ") )
            {
                char *pszLabel = (char *) oSubBox.ReadBoxData();
                if( pszLabel != NULL && EQUAL(pszLabel,"gml.data") )
                {
                    CollectGMLData( &oBox );
                }
                CPLFree( pszLabel );
            }
        }

/* -------------------------------------------------------------------- */
/*      Process simple xml boxes.                                       */
/* -------------------------------------------------------------------- */
        if( EQUAL(oBox.GetType(),"xml ") )
        {
            CPLString osBoxName;

            char *pszXML = (char *) oBox.ReadBoxData();
            if( pszXML != NULL && STARTS_WITH(pszXML, "<GDALMultiDomainMetadata>") )
            {
                if( pszGDALMultiDomainMetadata == NULL )
                {
                    pszGDALMultiDomainMetadata = pszXML;
                    pszXML = NULL;
                }
                else
                {
                    CPLDebug("GDALJP2", "Too many GDAL metadata boxes. Ignoring this one");
                }
            }
            else if( pszXML != NULL )
            {
                osBoxName.Printf( "BOX_%d", iBox++ );

                papszGMLMetadata = CSLSetNameValue( papszGMLMetadata,
                                                    osBoxName, pszXML );
            }
            CPLFree( pszXML );
        }

/* -------------------------------------------------------------------- */
/*      Check for a resd box in jp2h.                                   */
/* -------------------------------------------------------------------- */
        if( EQUAL(oBox.GetType(),"jp2h") )
        {
            GDALJP2Box oSubBox( fpVSIL );

            for( oSubBox.ReadFirstChild( &oBox );
                 strlen(oSubBox.GetType()) > 0;
                 oSubBox.ReadNextChild( &oBox ) )
            {
                if( EQUAL(oSubBox.GetType(),"res ") )
                {
                    GDALJP2Box oResBox( fpVSIL );

                    oResBox.ReadFirstChild( &oSubBox );

                    // We will use either the resd or resc box, which ever
                    // happens to be first.  Should we prefer resd?
                    unsigned char *pabyResData = NULL;
                    if( oResBox.GetDataLength() == 10 &&
                        (pabyResData = oResBox.ReadBoxData()) != NULL )
                    {
                        int nVertNum, nVertDen, nVertExp;
                        int nHorzNum, nHorzDen, nHorzExp;

                        nVertNum = pabyResData[0] * 256 + pabyResData[1];
                        nVertDen = pabyResData[2] * 256 + pabyResData[3];
                        nHorzNum = pabyResData[4] * 256 + pabyResData[5];
                        nHorzDen = pabyResData[6] * 256 + pabyResData[7];
                        nVertExp = pabyResData[8];
                        nHorzExp = pabyResData[9];

                        // compute in pixels/cm
                        double dfVertRes =
                            (nVertNum/(double)nVertDen) * pow(10.0,nVertExp)/100;
                        double dfHorzRes =
                            (nHorzNum/(double)nHorzDen) * pow(10.0,nHorzExp)/100;
                        CPLString osFormatter;

                        papszMetadata = CSLSetNameValue(
                            papszMetadata,
                            "TIFFTAG_XRESOLUTION",
                            osFormatter.Printf("%g",dfHorzRes) );

                        papszMetadata = CSLSetNameValue(
                            papszMetadata,
                            "TIFFTAG_YRESOLUTION",
                            osFormatter.Printf("%g",dfVertRes) );
                        papszMetadata = CSLSetNameValue(
                            papszMetadata,
                            "TIFFTAG_RESOLUTIONUNIT",
                            "3 (pixels/cm)" );

                        CPLFree( pabyResData );
                    }
                }
            }
        }

/* -------------------------------------------------------------------- */
/*      Collect IPR box.                                                */
/* -------------------------------------------------------------------- */
        if( EQUAL(oBox.GetType(),"jp2i") )
        {
            if( pszXMLIPR == NULL )
            {
                pszXMLIPR = (char*) oBox.ReadBoxData();
                CPLXMLNode* psNode = CPLParseXMLString(pszXMLIPR);
                if( psNode == NULL )
                {
                    CPLFree(pszXMLIPR);
                    pszXMLIPR = NULL;
                }
                else
                    CPLDestroyXMLNode(psNode);
            }
            else
            {
                CPLDebug("GDALJP2", "Too many IPR boxes. Ignoring this one");
            }
        }

        if (!oBox.ReadNext())
            break;
    }

    return TRUE;
}

/************************************************************************/
/*                          ParseJP2GeoTIFF()                           */
/************************************************************************/

int GDALJP2Metadata::ParseJP2GeoTIFF()

{
    if(! CPLTestBool(CPLGetConfigOption("GDAL_USE_GEOJP2", "TRUE")) )
        return FALSE;

    bool abValidProjInfo[MAX_JP2GEOTIFF_BOXES] = { false };
    char* apszProjection[MAX_JP2GEOTIFF_BOXES] = { NULL };
    double aadfGeoTransform[MAX_JP2GEOTIFF_BOXES][6];
    int anGCPCount[MAX_JP2GEOTIFF_BOXES] = { 0 };
    GDAL_GCP    *apasGCPList[MAX_JP2GEOTIFF_BOXES] = { NULL };
    int abPixelIsPoint[MAX_JP2GEOTIFF_BOXES] = { 0 };
    char** apapszRPCMD[MAX_JP2GEOTIFF_BOXES] = { NULL };

    int i;
    int nMax = MIN(nGeoTIFFBoxesCount, MAX_JP2GEOTIFF_BOXES);
    for(i=0; i < nMax; i++)
    {
    /* -------------------------------------------------------------------- */
    /*      Convert raw data into projection and geotransform.              */
    /* -------------------------------------------------------------------- */
        aadfGeoTransform[i][0] = 0;
        aadfGeoTransform[i][1] = 1;
        aadfGeoTransform[i][2] = 0;
        aadfGeoTransform[i][3] = 0;
        aadfGeoTransform[i][4] = 0;
        aadfGeoTransform[i][5] = 1;
        if( GTIFWktFromMemBufEx( pasGeoTIFFBoxes[i].nGeoTIFFSize,
                               pasGeoTIFFBoxes[i].pabyGeoTIFFData,
                               &apszProjection[i], aadfGeoTransform[i],
                               &anGCPCount[i], &apasGCPList[i],
                               &abPixelIsPoint[i], &apapszRPCMD[i] ) == CE_None )
        {
            if( apszProjection[i] != NULL && strlen(apszProjection[i]) != 0 )
                abValidProjInfo[i] = true;
        }
    }

    /* Detect which box is the better one */
    int iBestIndex = -1;
    for(i=0; i < nMax; i++)
    {
        if( abValidProjInfo[i] && iBestIndex < 0 )
        {
            iBestIndex = i;
        }
        else if( abValidProjInfo[i] && apszProjection[i] != NULL )
        {
            /* Anything else than a LOCAL_CS will probably be better */
            if( STARTS_WITH_CI(apszProjection[iBestIndex], "LOCAL_CS") )
                iBestIndex = i;
        }
    }

    if( iBestIndex < 0 )
    {
        for(i=0; i < nMax; i++)
        {
            if( aadfGeoTransform[i][0] != 0
                || aadfGeoTransform[i][1] != 1
                || aadfGeoTransform[i][2] != 0
                || aadfGeoTransform[i][3] != 0
                || aadfGeoTransform[i][4] != 0
                || aadfGeoTransform[i][5] != 1
                || anGCPCount[i] > 0
                || apapszRPCMD[i] != NULL )
            {
                iBestIndex = i;
            }
        }
    }

    if( iBestIndex >= 0 )
    {
        pszProjection = apszProjection[iBestIndex];
        memcpy(adfGeoTransform, aadfGeoTransform[iBestIndex], 6 * sizeof(double));
        nGCPCount = anGCPCount[iBestIndex];
        pasGCPList = apasGCPList[iBestIndex];
        bPixelIsPoint = abPixelIsPoint[iBestIndex];
        papszRPCMD = apapszRPCMD[iBestIndex];

        if( adfGeoTransform[0] != 0
            || adfGeoTransform[1] != 1
            || adfGeoTransform[2] != 0
            || adfGeoTransform[3] != 0
            || adfGeoTransform[4] != 0
            || adfGeoTransform[5] != 1 )
            bHaveGeoTransform = TRUE;

        if( pszProjection )
            CPLDebug( "GDALJP2Metadata",
                "Got projection from GeoJP2 (geotiff) box (%d): %s",
                iBestIndex, pszProjection );
    }

    /* Cleanup unused boxes */
    for(i=0; i < nMax; i++)
    {
        if( i != iBestIndex )
        {
            CPLFree( apszProjection[i] );
            if( anGCPCount[i] > 0 )
            {
                GDALDeinitGCPs( anGCPCount[i], apasGCPList[i] );
                CPLFree( apasGCPList[i] );
            }
            CSLDestroy( apapszRPCMD[i] );
        }
    }

    return iBestIndex >= 0;
}

/************************************************************************/
/*                             ParseMSIG()                              */
/************************************************************************/

int GDALJP2Metadata::ParseMSIG()

{
    if( nMSIGSize < 70 )
        return FALSE;

/* -------------------------------------------------------------------- */
/*      Try and extract worldfile parameters and adjust.                */
/* -------------------------------------------------------------------- */
    memcpy( adfGeoTransform + 0, pabyMSIGData + 22 + 8 * 4, 8 );
    memcpy( adfGeoTransform + 1, pabyMSIGData + 22 + 8 * 0, 8 );
    memcpy( adfGeoTransform + 2, pabyMSIGData + 22 + 8 * 2, 8 );
    memcpy( adfGeoTransform + 3, pabyMSIGData + 22 + 8 * 5, 8 );
    memcpy( adfGeoTransform + 4, pabyMSIGData + 22 + 8 * 1, 8 );
    memcpy( adfGeoTransform + 5, pabyMSIGData + 22 + 8 * 3, 8 );

    // data is in LSB (little endian) order in file.
    CPL_LSBPTR64( adfGeoTransform + 0 );
    CPL_LSBPTR64( adfGeoTransform + 1 );
    CPL_LSBPTR64( adfGeoTransform + 2 );
    CPL_LSBPTR64( adfGeoTransform + 3 );
    CPL_LSBPTR64( adfGeoTransform + 4 );
    CPL_LSBPTR64( adfGeoTransform + 5 );

    // correct for center of pixel vs. top left of pixel
    adfGeoTransform[0] -= 0.5 * adfGeoTransform[1];
    adfGeoTransform[0] -= 0.5 * adfGeoTransform[2];
    adfGeoTransform[3] -= 0.5 * adfGeoTransform[4];
    adfGeoTransform[3] -= 0.5 * adfGeoTransform[5];

    bHaveGeoTransform = TRUE;

    return TRUE;
}

/************************************************************************/
/*                         GetDictionaryItem()                          */
/************************************************************************/

static CPLXMLNode *
GetDictionaryItem( char **papszGMLMetadata, const char *pszURN )

{
    char *pszLabel;
    const char *pszFragmentId = NULL;
    int i;


    if( STARTS_WITH_CI(pszURN, "urn:jp2k:xml:") )
        pszLabel = CPLStrdup( pszURN + 13 );
    else if( STARTS_WITH_CI(pszURN, "urn:ogc:tc:gmljp2:xml:") )
        pszLabel = CPLStrdup( pszURN + 22 );
    else if( STARTS_WITH_CI(pszURN, "gmljp2://xml/") )
        pszLabel = CPLStrdup( pszURN + 13 );
    else
        pszLabel = CPLStrdup( pszURN );

/* -------------------------------------------------------------------- */
/*      Split out label and fragment id.                                */
/* -------------------------------------------------------------------- */
    for( i = 0; pszLabel[i] != '#'; i++ )
    {
        if( pszLabel[i] == '\0' )
        {
            CPLFree(pszLabel);
            return NULL;
        }
    }

    pszFragmentId = pszLabel + i + 1;
    pszLabel[i] = '\0';

/* -------------------------------------------------------------------- */
/*      Can we find an XML box with the desired label?                  */
/* -------------------------------------------------------------------- */
    const char *pszDictionary =
        CSLFetchNameValue( papszGMLMetadata, pszLabel );

    if( pszDictionary == NULL )
    {
        CPLFree(pszLabel);
        return NULL;
    }

/* -------------------------------------------------------------------- */
/*      Try and parse the dictionary.                                   */
/* -------------------------------------------------------------------- */
    CPLXMLNode *psDictTree = CPLParseXMLString( pszDictionary );

    if( psDictTree == NULL )
    {
        CPLFree(pszLabel);
        return NULL;
    }

    CPLStripXMLNamespace( psDictTree, NULL, TRUE );

    CPLXMLNode *psDictRoot = CPLSearchXMLNode( psDictTree, "=Dictionary" );

    if( psDictRoot == NULL )
    {
        CPLDestroyXMLNode( psDictTree );
        CPLFree(pszLabel);
        return NULL;
    }

/* -------------------------------------------------------------------- */
/*      Search for matching id.                                         */
/* -------------------------------------------------------------------- */
    CPLXMLNode *psEntry, *psHit = NULL;
    for( psEntry = psDictRoot->psChild;
         psEntry != NULL && psHit == NULL;
         psEntry = psEntry->psNext )
    {
        const char *pszId;

        if( psEntry->eType != CXT_Element )
            continue;

        if( !EQUAL(psEntry->pszValue,"dictionaryEntry") )
            continue;

        if( psEntry->psChild == NULL )
            continue;

        pszId = CPLGetXMLValue( psEntry->psChild, "id", "" );

        if( EQUAL(pszId, pszFragmentId) )
            psHit = CPLCloneXMLTree( psEntry->psChild );
    }

/* -------------------------------------------------------------------- */
/*      Cleanup                                                         */
/* -------------------------------------------------------------------- */
    CPLFree( pszLabel );
    CPLDestroyXMLNode( psDictTree );

    return psHit;
}


/************************************************************************/
/*                            GMLSRSLookup()                            */
/*                                                                      */
/*      Lookup an SRS in a dictionary inside this file.  We will get    */
/*      something like:                                                 */
/*        urn:jp2k:xml:CRSDictionary.xml#crs1112                        */
/*                                                                      */
/*      We need to split the filename from the fragment id, and         */
/*      lookup the fragment in the file if we can find it our           */
/*      list of labelled xml boxes.                                     */
/************************************************************************/

int GDALJP2Metadata::GMLSRSLookup( const char *pszURN )

{
    CPLXMLNode *psDictEntry = GetDictionaryItem( papszGMLMetadata, pszURN );

    if( psDictEntry == NULL )
        return FALSE;

/* -------------------------------------------------------------------- */
/*      Reserialize this fragment.                                      */
/* -------------------------------------------------------------------- */
    char *pszDictEntryXML = CPLSerializeXMLTree( psDictEntry );
    CPLDestroyXMLNode( psDictEntry );

/* -------------------------------------------------------------------- */
/*      Try to convert into an OGRSpatialReference.                     */
/* -------------------------------------------------------------------- */
    OGRSpatialReference oSRS;
    int bSuccess = FALSE;

    if( oSRS.importFromXML( pszDictEntryXML ) == OGRERR_NONE )
    {
        CPLFree( pszProjection );
        pszProjection = NULL;

        oSRS.exportToWkt( &pszProjection );
        bSuccess = TRUE;
    }

    CPLFree( pszDictEntryXML );

    return bSuccess;
}

/************************************************************************/
/*                        ParseGMLCoverageDesc()                        */
/************************************************************************/

int GDALJP2Metadata::ParseGMLCoverageDesc()

{
    if(! CPLTestBool(CPLGetConfigOption("GDAL_USE_GMLJP2", "TRUE")) )
        return FALSE;

/* -------------------------------------------------------------------- */
/*      Do we have an XML doc that is apparently a coverage             */
/*      description?                                                    */
/* -------------------------------------------------------------------- */
    const char *pszCoverage = CSLFetchNameValue( papszGMLMetadata,
                                                 "gml.root-instance" );

    if( pszCoverage == NULL )
        return FALSE;

    CPLDebug( "GDALJP2Metadata", "Found GML Box:\n%s", pszCoverage );

/* -------------------------------------------------------------------- */
/*      Try parsing the XML.  Wipe any namespace prefixes.              */
/* -------------------------------------------------------------------- */
    CPLXMLNode *psXML = CPLParseXMLString( pszCoverage );

    if( psXML == NULL )
        return FALSE;

    CPLStripXMLNamespace( psXML, NULL, TRUE );

/* -------------------------------------------------------------------- */
/*      Isolate RectifiedGrid.  Eventually we will need to support      */
/*      other georeferencing objects.                                   */
/* -------------------------------------------------------------------- */
    CPLXMLNode *psRG = CPLSearchXMLNode( psXML, "=RectifiedGrid" );
    CPLXMLNode *psOriginPoint = NULL;
    const char *pszOffset1=NULL, *pszOffset2=NULL;

    if( psRG != NULL )
    {
        psOriginPoint = CPLGetXMLNode( psRG, "origin.Point" );


        CPLXMLNode *psOffset1 = CPLGetXMLNode( psRG, "offsetVector" );
        if( psOffset1 != NULL )
        {
            pszOffset1 = CPLGetXMLValue( psOffset1, "", NULL );
            pszOffset2 = CPLGetXMLValue( psOffset1->psNext, "=offsetVector",
                                         NULL );
        }
    }

/* -------------------------------------------------------------------- */
/*      If we are missing any of the origin or 2 offsets then give up.  */
/* -------------------------------------------------------------------- */
    if( psOriginPoint == NULL || pszOffset1 == NULL || pszOffset2 == NULL )
    {
        CPLDestroyXMLNode( psXML );
        return FALSE;
    }

/* -------------------------------------------------------------------- */
/*      Extract origin location.                                        */
/* -------------------------------------------------------------------- */
    OGRPoint *poOriginGeometry = NULL;
    const char *pszSRSName = NULL;

    if( psOriginPoint != NULL )
    {
        poOriginGeometry = (OGRPoint *)
            OGR_G_CreateFromGMLTree( psOriginPoint );

        if( poOriginGeometry != NULL
            && wkbFlatten(poOriginGeometry->getGeometryType()) != wkbPoint )
        {
            delete poOriginGeometry;
            poOriginGeometry = NULL;
        }

        // SRS?
        pszSRSName = CPLGetXMLValue( psOriginPoint, "srsName", NULL );
    }

/* -------------------------------------------------------------------- */
/*      Extract offset(s)                                               */
/* -------------------------------------------------------------------- */
    char **papszOffset1Tokens = NULL;
    char **papszOffset2Tokens = NULL;
    bool bSuccess = false;

    papszOffset1Tokens =
        CSLTokenizeStringComplex( pszOffset1, " ,", FALSE, FALSE );
    papszOffset2Tokens =
        CSLTokenizeStringComplex( pszOffset2, " ,", FALSE, FALSE );

    if( CSLCount(papszOffset1Tokens) >= 2
        && CSLCount(papszOffset2Tokens) >= 2
        && poOriginGeometry != NULL )
    {
        adfGeoTransform[0] = poOriginGeometry->getX();
        adfGeoTransform[1] = CPLAtof(papszOffset1Tokens[0]);
        adfGeoTransform[2] = CPLAtof(papszOffset2Tokens[0]);
        adfGeoTransform[3] = poOriginGeometry->getY();
        adfGeoTransform[4] = CPLAtof(papszOffset1Tokens[1]);
        adfGeoTransform[5] = CPLAtof(papszOffset2Tokens[1]);

        // offset from center of pixel.
        adfGeoTransform[0] -= adfGeoTransform[1]*0.5;
        adfGeoTransform[0] -= adfGeoTransform[2]*0.5;
        adfGeoTransform[3] -= adfGeoTransform[4]*0.5;
        adfGeoTransform[3] -= adfGeoTransform[5]*0.5;

        bSuccess = true;
        bHaveGeoTransform = TRUE;
    }

    CSLDestroy( papszOffset1Tokens );
    CSLDestroy( papszOffset2Tokens );

    if( poOriginGeometry != NULL )
        delete poOriginGeometry;

/* -------------------------------------------------------------------- */
/*      If we still don't have an srsName, check for it on the          */
/*      boundedBy Envelope.  Some products                              */
/*      (i.e. EuropeRasterTile23.jpx) use this as the only srsName      */
/*      delivery vehicle.                                               */
/* -------------------------------------------------------------------- */
    if( pszSRSName == NULL )
    {
        pszSRSName =
            CPLGetXMLValue( psXML,
                            "=FeatureCollection.boundedBy.Envelope.srsName",
                            NULL );
    }
/* -------------------------------------------------------------------- */
/*      Examples of DGIWG_Profile_of_JPEG2000_for_Georeference_Imagery.pdf */
/*      have srsName only on RectifiedGrid element.                     */
/* -------------------------------------------------------------------- */
    if( psRG != NULL && pszSRSName == NULL )
    {
        pszSRSName = CPLGetXMLValue( psRG,  "srsName", NULL );
    }

/* -------------------------------------------------------------------- */
/*      If we have gotten a geotransform, then try to interpret the     */
/*      srsName.                                                        */
/* -------------------------------------------------------------------- */
    bool bNeedAxisFlip = false;

    OGRSpatialReference oSRS;
    if( bSuccess && pszSRSName != NULL
        && (pszProjection == NULL || strlen(pszProjection) == 0) )
    {
        if( STARTS_WITH_CI(pszSRSName, "epsg:") )
        {
            if( oSRS.SetFromUserInput( pszSRSName ) == OGRERR_NONE )
                oSRS.exportToWkt( &pszProjection );
        }
        else if( (STARTS_WITH_CI(pszSRSName, "urn:")
                 && strstr(pszSRSName,":def:") != NULL
                 && oSRS.importFromURN(pszSRSName) == OGRERR_NONE) ||
                 /* GMLJP2 v2.0 uses CRS URL instead of URN */
                 /* See e.g. http://schemas.opengis.net/gmljp2/2.0/examples/minimalInstance.xml */
                 (STARTS_WITH_CI(pszSRSName, "http://www.opengis.net/def/crs/")                  && oSRS.importFromCRSURL(pszSRSName) == OGRERR_NONE) )
        {
            oSRS.exportToWkt( &pszProjection );

            // Per #2131
            if( oSRS.EPSGTreatsAsLatLong() || oSRS.EPSGTreatsAsNorthingEasting() )
            {
                CPLDebug( "GMLJP2", "Request axis flip for SRS=%s",
                          pszSRSName );
                bNeedAxisFlip = true;
            }
        }
        else if( !GMLSRSLookup( pszSRSName ) )
        {
            CPLDebug( "GDALJP2Metadata",
                      "Unable to evaluate SRSName=%s",
                      pszSRSName );
        }
    }

    if( pszProjection )
        CPLDebug( "GDALJP2Metadata",
                  "Got projection from GML box: %s",
                 pszProjection );

/* -------------------------------------------------------------------- */
/*      Do we need to flip the axes?                                    */
/* -------------------------------------------------------------------- */
    if( bNeedAxisFlip
        && CPLTestBool( CPLGetConfigOption( "GDAL_IGNORE_AXIS_ORIENTATION",
                                               "FALSE" ) ) )
    {
        bNeedAxisFlip = false;
        CPLDebug( "GMLJP2", "Suppressed axis flipping based on GDAL_IGNORE_AXIS_ORIENTATION." );
    }

    if( pszSRSName && bNeedAxisFlip )
    {
        // Suppress explicit axis order in SRS definition

        OGR_SRSNode *poGEOGCS = oSRS.GetAttrNode( "GEOGCS" );
        if( poGEOGCS != NULL )
            poGEOGCS->StripNodes( "AXIS" );

        OGR_SRSNode *poPROJCS = oSRS.GetAttrNode( "PROJCS" );
        if (poPROJCS != NULL && oSRS.EPSGTreatsAsNorthingEasting())
            poPROJCS->StripNodes( "AXIS" );

        CPLFree(pszProjection);
        oSRS.exportToWkt( &pszProjection );

    }

    /* Some Pleiades files have explicit <gml:axisName>Easting</gml:axisName> */
    /* <gml:axisName>Northing</gml:axisName> to override default EPSG order */
    if( bNeedAxisFlip && psRG != NULL )
    {
        int nAxisCount = 0;
        bool bFirstAxisIsEastOrLong = false;
        bool bSecondAxisIsNorthOrLat = false;
        for(CPLXMLNode* psIter = psRG->psChild; psIter != NULL; psIter = psIter->psNext )
        {
            if( psIter->eType == CXT_Element && strcmp(psIter->pszValue, "axisName") == 0 &&
                psIter->psChild != NULL && psIter->psChild->eType == CXT_Text )
            {
                if( nAxisCount == 0 &&
                    (STARTS_WITH_CI(psIter->psChild->pszValue, "EAST") ||
                     STARTS_WITH_CI(psIter->psChild->pszValue, "LONG") ) )
                {
                    bFirstAxisIsEastOrLong = true;
                }
                else if( nAxisCount == 1 &&
                         (STARTS_WITH_CI(psIter->psChild->pszValue, "NORTH") ||
                          STARTS_WITH_CI(psIter->psChild->pszValue, "LAT")) )
                {
                    bSecondAxisIsNorthOrLat = true;
                }
                nAxisCount ++;
            }
        }
        if( bFirstAxisIsEastOrLong && bSecondAxisIsNorthOrLat )
        {
            CPLDebug( "GMLJP2", "Disable axis flip because of explicit axisName disabling it" );
            bNeedAxisFlip = false;
        }
    }

    CPLDestroyXMLNode( psXML );
    psXML = NULL;
    psRG = NULL;

    if( bNeedAxisFlip )
    {
        double dfTemp;

        CPLDebug( "GMLJP2",
                  "Flipping axis orientation in GMLJP2 coverage description." );

        dfTemp = adfGeoTransform[0];
        adfGeoTransform[0] = adfGeoTransform[3];
        adfGeoTransform[3] = dfTemp;

        int swapWith1Index = 4;
        int swapWith2Index = 5;

        /* Look if we have GDAL_JP2K_ALT_OFFSETVECTOR_ORDER=TRUE as a XML comment */
        int bHasAltOffsetVectorOrderComment =
            strstr(pszCoverage, "GDAL_JP2K_ALT_OFFSETVECTOR_ORDER=TRUE") != NULL;

        if( bHasAltOffsetVectorOrderComment ||
            CPLTestBool( CPLGetConfigOption( "GDAL_JP2K_ALT_OFFSETVECTOR_ORDER",
                                                "FALSE" ) ) )
        {
            swapWith1Index = 5;
            swapWith2Index = 4;
            CPLDebug( "GMLJP2", "Choosing alternate GML \"<offsetVector>\" order based on "
                "GDAL_JP2K_ALT_OFFSETVECTOR_ORDER." );
        }

        dfTemp = adfGeoTransform[1];
        adfGeoTransform[1] = adfGeoTransform[swapWith1Index];
        adfGeoTransform[swapWith1Index] = dfTemp;

        dfTemp = adfGeoTransform[2];
        adfGeoTransform[2] = adfGeoTransform[swapWith2Index];
        adfGeoTransform[swapWith2Index] = dfTemp;

        /* Found in autotest/gdrivers/data/ll.jp2 */
        if( adfGeoTransform[1] == 0.0 && adfGeoTransform[2] < 0.0 &&
            adfGeoTransform[4] > 0.0 && adfGeoTransform[5] == 0.0 )
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                     "It is likely that the axis order of the GMLJP2 box is not "
                     "consistent with the EPSG order and that the resulting georeferencing "
                     "will be incorrect. Try setting GDAL_IGNORE_AXIS_ORIENTATION=TRUE if it is the case");
        }
    }

    return pszProjection != NULL && bSuccess;
}

/************************************************************************/
/*                           SetProjection()                            */
/************************************************************************/

void GDALJP2Metadata::SetProjection( const char *pszWKT )

{
    CPLFree( pszProjection );
    pszProjection = CPLStrdup(pszWKT);
}

/************************************************************************/
/*                              SetGCPs()                               */
/************************************************************************/

void GDALJP2Metadata::SetGCPs( int nCount, const GDAL_GCP *pasGCPsIn )

{
    if( nGCPCount > 0 )
    {
        GDALDeinitGCPs( nGCPCount, pasGCPList );
        CPLFree( pasGCPList );
    }

    nGCPCount = nCount;
    pasGCPList = GDALDuplicateGCPs(nGCPCount, pasGCPsIn);
}

/************************************************************************/
/*                          SetGeoTransform()                           */
/************************************************************************/

void GDALJP2Metadata::SetGeoTransform( double *padfGT )

{
    memcpy( adfGeoTransform, padfGT, sizeof(double) * 6 );
}

/************************************************************************/
/*                             SetRPCMD()                               */
/************************************************************************/

void GDALJP2Metadata::SetRPCMD( char** papszRPCMDIn )

{
    CSLDestroy( papszRPCMD );
    papszRPCMD = CSLDuplicate(papszRPCMDIn);
}

/************************************************************************/
/*                          CreateJP2GeoTIFF()                          */
/************************************************************************/

GDALJP2Box *GDALJP2Metadata::CreateJP2GeoTIFF()

{
/* -------------------------------------------------------------------- */
/*      Prepare the memory buffer containing the degenerate GeoTIFF     */
/*      file.                                                           */
/* -------------------------------------------------------------------- */
    int         nGTBufSize = 0;
    unsigned char *pabyGTBuf = NULL;

    if( GTIFMemBufFromWktEx( pszProjection, adfGeoTransform,
                             nGCPCount, pasGCPList,
                             &nGTBufSize, &pabyGTBuf, bPixelIsPoint,
                             papszRPCMD ) != CE_None )
        return NULL;

    if( nGTBufSize == 0 )
        return NULL;

/* -------------------------------------------------------------------- */
/*      Write to a box on the JP2 file.                                 */
/* -------------------------------------------------------------------- */
    GDALJP2Box *poBox;

    poBox = GDALJP2Box::CreateUUIDBox( msi_uuid2, nGTBufSize, pabyGTBuf );

    CPLFree( pabyGTBuf );

    return poBox;
}

/************************************************************************/
/*                     GetGMLJP2GeoreferencingInfo()                    */
/************************************************************************/

int GDALJP2Metadata::GetGMLJP2GeoreferencingInfo( int& nEPSGCode,
                                                  double adfOrigin[2],
                                                  double adfXVector[2],
                                                  double adfYVector[2],
                                                  const char*& pszComment,
                                                  CPLString& osDictBox,
                                                  int& bNeedAxisFlip )
{

/* -------------------------------------------------------------------- */
/*      Try do determine a PCS or GCS code we can use.                  */
/* -------------------------------------------------------------------- */
    OGRSpatialReference oSRS;
    char *pszWKTCopy = (char *) pszProjection;
    nEPSGCode = 0;
    bNeedAxisFlip = FALSE;

    if( oSRS.importFromWkt( &pszWKTCopy ) != OGRERR_NONE )
        return FALSE;

    if( oSRS.IsProjected() )
    {
        const char *pszAuthName = oSRS.GetAuthorityName( "PROJCS" );

        if( pszAuthName != NULL && EQUAL(pszAuthName,"epsg") )
        {
            nEPSGCode = atoi(oSRS.GetAuthorityCode( "PROJCS" ));
        }
    }
    else if( oSRS.IsGeographic() )
    {
        const char *pszAuthName = oSRS.GetAuthorityName( "GEOGCS" );

        if( pszAuthName != NULL && EQUAL(pszAuthName,"epsg") )
        {
            nEPSGCode = atoi(oSRS.GetAuthorityCode( "GEOGCS" ));
        }
    }

    // Save error state as importFromEPSGA() will call CPLReset()
    CPLErrorNum errNo = CPLGetLastErrorNo();
    CPLErr eErr = CPLGetLastErrorType();
    CPLString osLastErrorMsg = CPLGetLastErrorMsg();

    // Determine if we need to flip axis. Reimport from EPSG and make
    // sure not to strip axis definitions to determine the axis order.
    if( nEPSGCode != 0 && oSRS.importFromEPSGA(nEPSGCode) == OGRERR_NONE )
    {
        if( oSRS.EPSGTreatsAsLatLong() || oSRS.EPSGTreatsAsNorthingEasting() )
        {
            bNeedAxisFlip = TRUE;
        }
    }

    // Restore error state
    CPLErrorSetState( eErr, errNo, osLastErrorMsg);

/* -------------------------------------------------------------------- */
/*      Prepare coverage origin and offset vectors.  Take axis          */
/*      order into account if needed.                                   */
/* -------------------------------------------------------------------- */
    adfOrigin[0] = adfGeoTransform[0] + adfGeoTransform[1] * 0.5
        + adfGeoTransform[4] * 0.5;
    adfOrigin[1] = adfGeoTransform[3] + adfGeoTransform[2] * 0.5
        + adfGeoTransform[5] * 0.5;
    adfXVector[0] = adfGeoTransform[1];
    adfXVector[1] = adfGeoTransform[2];

    adfYVector[0] = adfGeoTransform[4];
    adfYVector[1] = adfGeoTransform[5];

    if( bNeedAxisFlip
        && CPLTestBool( CPLGetConfigOption( "GDAL_IGNORE_AXIS_ORIENTATION",
                                               "FALSE" ) ) )
    {
        bNeedAxisFlip = FALSE;
        CPLDebug( "GMLJP2", "Suppressed axis flipping on write based on GDAL_IGNORE_AXIS_ORIENTATION." );
    }

    pszComment = "";
    if( bNeedAxisFlip )
    {
        double dfTemp;

        CPLDebug( "GMLJP2", "Flipping GML coverage axis order." );

        dfTemp = adfOrigin[0];
        adfOrigin[0] = adfOrigin[1];
        adfOrigin[1] = dfTemp;

        if( CPLTestBool( CPLGetConfigOption( "GDAL_JP2K_ALT_OFFSETVECTOR_ORDER",
                                                "FALSE" ) ) )
        {
            CPLDebug( "GMLJP2", "Choosing alternate GML \"<offsetVector>\" order based on "
                "GDAL_JP2K_ALT_OFFSETVECTOR_ORDER." );

            /* In this case the swapping is done in an "X" pattern */
            dfTemp = adfXVector[0];
            adfXVector[0] = adfYVector[1];
            adfYVector[1] = dfTemp;

            dfTemp = adfYVector[0];
            adfYVector[0] = adfXVector[1];
            adfXVector[1] = dfTemp;

            /* We add this as an XML comment so that we know we must do OffsetVector flipping on reading */
            pszComment = "              <!-- GDAL_JP2K_ALT_OFFSETVECTOR_ORDER=TRUE: First "
                         "value of offset is latitude/northing component of the "
                         "latitude/northing axis. -->\n";
        }
        else
        {
            dfTemp = adfXVector[0];
            adfXVector[0] = adfXVector[1];
            adfXVector[1] = dfTemp;

            dfTemp = adfYVector[0];
            adfYVector[0] = adfYVector[1];
            adfYVector[1] = dfTemp;
        }
    }

/* -------------------------------------------------------------------- */
/*      If we need a user defined CRSDictionary entry, prepare it       */
/*      here.                                                           */
/* -------------------------------------------------------------------- */
    if( nEPSGCode == 0 )
    {
        char *pszGMLDef = NULL;

        if( oSRS.exportToXML( &pszGMLDef, NULL ) == OGRERR_NONE )
        {
            char* pszWKT = NULL;
            oSRS.exportToWkt(&pszWKT);
            char* pszXMLEscapedWKT = CPLEscapeString(pszWKT, -1, CPLES_XML);
            CPLFree(pszWKT);
            osDictBox.Printf(
"<gml:Dictionary gml:id=\"CRSU1\" \n"
"        xmlns:gml=\"http://www.opengis.net/gml\"\n"
"        xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n"
"        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
"        xsi:schemaLocation=\"http://www.opengis.net/gml "
"http://schemas.opengis.net/gml/3.1.1/base/gml.xsd\">\n"
"  <gml:description>Dictionary for custom SRS %s</gml:description>\n"
"  <gml:name>Dictionary for custom SRS</gml:name>\n"
"  <gml:dictionaryEntry>\n"
"%s\n"
"  </gml:dictionaryEntry>\n"
"</gml:Dictionary>\n",
                     pszXMLEscapedWKT, pszGMLDef );
            CPLFree(pszXMLEscapedWKT);
        }
        CPLFree( pszGMLDef );
    }

    return TRUE;
}

/************************************************************************/
/*                          CreateGMLJP2()                              */
/************************************************************************/

GDALJP2Box *GDALJP2Metadata::CreateGMLJP2( int nXSize, int nYSize )

{
/* -------------------------------------------------------------------- */
/*      This is a backdoor to let us embed a literal gmljp2 chunk       */
/*      supplied by the user as an external file.  This is mostly       */
/*      for preparing test files with exotic contents.                  */
/* -------------------------------------------------------------------- */
    if( CPLGetConfigOption( "GMLJP2OVERRIDE", NULL ) != NULL )
    {
        VSILFILE *fp = VSIFOpenL( CPLGetConfigOption( "GMLJP2OVERRIDE",""), "r" );
        char *pszGML = NULL;

        if( fp == NULL )
        {
            CPLError( CE_Failure, CPLE_AppDefined,
                      "Unable to open GMLJP2OVERRIDE file." );
            return NULL;
        }

        CPL_IGNORE_RET_VAL(VSIFSeekL( fp, 0, SEEK_END ));
        int nLength = (int) VSIFTellL( fp );
        pszGML = (char *) CPLCalloc(1,nLength+1);
        CPL_IGNORE_RET_VAL(VSIFSeekL( fp, 0, SEEK_SET ));
        CPL_IGNORE_RET_VAL(VSIFReadL( pszGML, 1, nLength, fp ));
        CPL_IGNORE_RET_VAL(VSIFCloseL( fp ));

        GDALJP2Box *apoGMLBoxes[2];

        apoGMLBoxes[0] = GDALJP2Box::CreateLblBox( "gml.data" );
        apoGMLBoxes[1] =
            GDALJP2Box::CreateLabelledXMLAssoc( "gml.root-instance",
                                                pszGML );

        GDALJP2Box *poGMLData = GDALJP2Box::CreateAsocBox( 2, apoGMLBoxes);

        delete apoGMLBoxes[0];
        delete apoGMLBoxes[1];

        CPLFree( pszGML );

        return poGMLData;
    }

    int nEPSGCode;
    double adfOrigin[2];
    double adfXVector[2];
    double adfYVector[2];
    const char* pszComment = "";
    CPLString osDictBox;
    int bNeedAxisFlip = FALSE;
    if( !GetGMLJP2GeoreferencingInfo( nEPSGCode, adfOrigin,
                                      adfXVector, adfYVector,
                                      pszComment, osDictBox, bNeedAxisFlip ) )
    {
        return NULL;
    }

    char szSRSName[100];
    if( nEPSGCode != 0 )
        snprintf( szSRSName, sizeof(szSRSName), "urn:ogc:def:crs:EPSG::%d", nEPSGCode );
    else
        snprintf( szSRSName, sizeof(szSRSName), "%s",
                "gmljp2://xml/CRSDictionary.gml#ogrcrs1" );

    // Compute bounding box
    double dfX1 = adfGeoTransform[0];
    double dfX2 = adfGeoTransform[0] + nXSize * adfGeoTransform[1];
    double dfX3 = adfGeoTransform[0] +                               nYSize * adfGeoTransform[2];
    double dfX4 = adfGeoTransform[0] + nXSize * adfGeoTransform[1] + nYSize * adfGeoTransform[2];
    double dfY1 = adfGeoTransform[3];
    double dfY2 = adfGeoTransform[3] + nXSize * adfGeoTransform[4];
    double dfY3 = adfGeoTransform[3] +                               nYSize * adfGeoTransform[5];
    double dfY4 = adfGeoTransform[3] + nXSize * adfGeoTransform[4] + nYSize * adfGeoTransform[5];
    double dfLCX = MIN(MIN(dfX1,dfX2),MIN(dfX3,dfX4));
    double dfLCY = MIN(MIN(dfY1,dfY2),MIN(dfY3,dfY4));
    double dfUCX = MAX(MAX(dfX1,dfX2),MAX(dfX3,dfX4));
    double dfUCY = MAX(MAX(dfY1,dfY2),MAX(dfY3,dfY4));
    if( bNeedAxisFlip )
    {
        double dfTmp = dfLCX;
        dfLCX = dfLCY;
        dfLCY = dfTmp;

        dfTmp = dfUCX;
        dfUCX = dfUCY;
        dfUCY = dfTmp;
    }

/* -------------------------------------------------------------------- */
/*      For now we hardcode for a minimal instance format.              */
/* -------------------------------------------------------------------- */
    CPLString osDoc;

    osDoc.Printf(
"<gml:FeatureCollection\n"
"   xmlns:gml=\"http://www.opengis.net/gml\"\n"
"   xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
"   xsi:schemaLocation=\"http://www.opengis.net/gml http://schemas.opengis.net/gml/3.1.1/profiles/gmlJP2Profile/1.0.0/gmlJP2Profile.xsd\">\n"
"  <gml:boundedBy>\n"
"    <gml:Envelope srsName=\"%s\">\n"
"      <gml:lowerCorner>%.15g %.15g</gml:lowerCorner>\n"
"      <gml:upperCorner>%.15g %.15g</gml:upperCorner>\n"
"    </gml:Envelope>\n"
"  </gml:boundedBy>\n"
"  <gml:featureMember>\n"
"    <gml:FeatureCollection>\n"
"      <gml:featureMember>\n"
"        <gml:RectifiedGridCoverage dimension=\"2\" gml:id=\"RGC0001\">\n"
"          <gml:rectifiedGridDomain>\n"
"            <gml:RectifiedGrid dimension=\"2\">\n"
"              <gml:limits>\n"
"                <gml:GridEnvelope>\n"
"                  <gml:low>0 0</gml:low>\n"
"                  <gml:high>%d %d</gml:high>\n"
"                </gml:GridEnvelope>\n"
"              </gml:limits>\n"
"              <gml:axisName>x</gml:axisName>\n"
"              <gml:axisName>y</gml:axisName>\n"
"              <gml:origin>\n"
"                <gml:Point gml:id=\"P0001\" srsName=\"%s\">\n"
"                  <gml:pos>%.15g %.15g</gml:pos>\n"
"                </gml:Point>\n"
"              </gml:origin>\n"
"%s"
"              <gml:offsetVector srsName=\"%s\">%.15g %.15g</gml:offsetVector>\n"
"              <gml:offsetVector srsName=\"%s\">%.15g %.15g</gml:offsetVector>\n"
"            </gml:RectifiedGrid>\n"
"          </gml:rectifiedGridDomain>\n"
"          <gml:rangeSet>\n"
"            <gml:File>\n"
"              <gml:rangeParameters/>\n"
"              <gml:fileName>gmljp2://codestream/0</gml:fileName>\n"
"              <gml:fileStructure>Record Interleaved</gml:fileStructure>\n"
"            </gml:File>\n"
"          </gml:rangeSet>\n"
"        </gml:RectifiedGridCoverage>\n"
"      </gml:featureMember>\n"
"    </gml:FeatureCollection>\n"
"  </gml:featureMember>\n"
"</gml:FeatureCollection>\n",
             szSRSName, dfLCX, dfLCY, dfUCX, dfUCY,
             nXSize-1, nYSize-1, szSRSName, adfOrigin[0], adfOrigin[1],
             pszComment,
             szSRSName, adfXVector[0], adfXVector[1],
             szSRSName, adfYVector[0], adfYVector[1] );

/* -------------------------------------------------------------------- */
/*      Setup the gml.data label.                                       */
/* -------------------------------------------------------------------- */
    GDALJP2Box *apoGMLBoxes[5];
    int nGMLBoxes = 0;

    apoGMLBoxes[nGMLBoxes++] = GDALJP2Box::CreateLblBox( "gml.data" );

/* -------------------------------------------------------------------- */
/*      Setup gml.root-instance.                                        */
/* -------------------------------------------------------------------- */
    apoGMLBoxes[nGMLBoxes++] =
        GDALJP2Box::CreateLabelledXMLAssoc( "gml.root-instance", osDoc );

/* -------------------------------------------------------------------- */
/*      Add optional dictionary.                                        */
/* -------------------------------------------------------------------- */
    if( osDictBox.size() > 0 )
        apoGMLBoxes[nGMLBoxes++] =
            GDALJP2Box::CreateLabelledXMLAssoc( "CRSDictionary.gml",
                                                osDictBox );

/* -------------------------------------------------------------------- */
/*      Bundle gml.data boxes into an association.                      */
/* -------------------------------------------------------------------- */
    GDALJP2Box *poGMLData = GDALJP2Box::CreateAsocBox( nGMLBoxes, apoGMLBoxes);

/* -------------------------------------------------------------------- */
/*      Cleanup working boxes.                                          */
/* -------------------------------------------------------------------- */
    while( nGMLBoxes > 0 )
        delete apoGMLBoxes[--nGMLBoxes];

    return poGMLData;
}

/************************************************************************/
/*                      GDALGMLJP2GetXMLRoot()                          */
/************************************************************************/

static CPLXMLNode* GDALGMLJP2GetXMLRoot(CPLXMLNode* psNode)
{
    for( ; psNode != NULL; psNode = psNode->psNext )
    {
        if( psNode->eType == CXT_Element && psNode->pszValue[0] != '?' )
            return psNode;
    }
    return NULL;
}

/************************************************************************/
/*            GDALGMLJP2PatchFeatureCollectionSubstitutionGroup()       */
/************************************************************************/

static void GDALGMLJP2PatchFeatureCollectionSubstitutionGroup(CPLXMLNode* psRoot)
{
    /* GML 3.2 SF profile recommends the feature collection type to derive */
    /* from gml:AbstractGML to prevent it to be included in another feature */
    /* collection, but this is what we want to do. So patch that... */

    /* <xs:element name="FeatureCollection" type="ogr:FeatureCollectionType" substitutionGroup="gml:AbstractGML"/> */
    /* --> */
    /* <xs:element name="FeatureCollection" type="ogr:FeatureCollectionType" substitutionGroup="gml:AbstractFeature"/> */
    if( psRoot->eType == CXT_Element &&
        (strcmp(psRoot->pszValue, "schema") == 0 || strcmp(psRoot->pszValue, "xs:schema") == 0) )
    {
        for(CPLXMLNode* psIter = psRoot->psChild; psIter != NULL; psIter = psIter->psNext)
        {
            if( psIter->eType == CXT_Element &&
                (strcmp(psIter->pszValue, "element") == 0 || strcmp(psIter->pszValue, "xs:element") == 0) &&
                strcmp(CPLGetXMLValue(psIter, "name", ""), "FeatureCollection") == 0 &&
                strcmp(CPLGetXMLValue(psIter, "substitutionGroup", ""), "gml:AbstractGML") == 0 )
            {
                CPLDebug("GMLJP2", "Patching substitutionGroup=\"gml:AbstractGML\" to \"gml:AbstractFeature\"");
                CPLSetXMLValue( psIter, "#substitutionGroup", "gml:AbstractFeature" );
                break;
            }
        }
    }
}

/************************************************************************/
/*                          CreateGMLJP2V2()                            */
/************************************************************************/

class GMLJP2V2GMLFileDesc
{
    public:
        CPLString osFile;
        CPLString osRemoteResource;
        CPLString osNamespace;
        CPLString osSchemaLocation;
        int       bInline;
        int       bParentCoverageCollection;

            GMLJP2V2GMLFileDesc(): bInline(TRUE), bParentCoverageCollection(TRUE) {}
};

class GMLJP2V2AnnotationDesc
{
    public:
        CPLString osFile;
};

class GMLJP2V2MetadataDesc
{
    public:
        CPLString osFile;
        CPLString osContent;
        CPLString osTemplateFile;
        CPLString osSourceFile;
        int       bGDALMetadata;
        int       bParentCoverageCollection;

            GMLJP2V2MetadataDesc(): bGDALMetadata(FALSE), bParentCoverageCollection(TRUE) {}
};

class GMLJP2V2StyleDesc
{
    public:
        CPLString osFile;
        int       bParentCoverageCollection;

            GMLJP2V2StyleDesc(): bParentCoverageCollection(TRUE) {}
};

class GMLJP2V2ExtensionDesc
{
    public:
        CPLString osFile;
        int       bParentCoverageCollection;

            GMLJP2V2ExtensionDesc(): bParentCoverageCollection(TRUE) {}
};
class GMLJP2V2BoxDesc
{
    public:
        CPLString osFile;
        CPLString osLabel;
};

GDALJP2Box *GDALJP2Metadata::CreateGMLJP2V2( int nXSize, int nYSize,
                                             const char* pszDefFilename,
                                             GDALDataset* poSrcDS )

{
    CPLString osRootGMLId = "ID_GMLJP2_0";
    CPLString osGridCoverage;
    CPLString osGridCoverageFile;
    bool bCRSURL = true;
    std::vector<GMLJP2V2MetadataDesc> aoMetadata;
    std::vector<GMLJP2V2AnnotationDesc> aoAnnotations;
    std::vector<GMLJP2V2GMLFileDesc> aoGMLFiles;
    std::vector<GMLJP2V2StyleDesc> aoStyles;
    std::vector<GMLJP2V2ExtensionDesc> aoExtensions;
    std::vector<GMLJP2V2BoxDesc> aoBoxes;

/* -------------------------------------------------------------------- */
/*      Parse definition file.                                          */
/* -------------------------------------------------------------------- */
    if( pszDefFilename && !EQUAL(pszDefFilename, "YES") && !EQUAL(pszDefFilename, "TRUE") )
    {
        GByte* pabyContent = NULL;
        if( pszDefFilename[0] != '{' )
        {
            if( !VSIIngestFile( NULL, pszDefFilename, &pabyContent, NULL, -1 ) )
                return NULL;
        }

/*
{
    "#doc" : "Unless otherwise specified, all elements are optional",

    "#root_instance_doc": "Describe content of the GMLJP2CoverageCollection",
    "root_instance": {
        "#gml_id_doc": "Specify GMLJP2CoverageCollection id here. Default is ID_GMLJP2_0",
        "gml_id": "some_gml_id",

        "#grid_coverage_file_doc": [
            "External XML file, whose root might be a GMLJP2GridCoverage, ",
            "GMLJP2RectifiedGridCoverage or a GMLJP2ReferenceableGridCoverage",
            "If not specified, GDAL will auto-generate a GMLJP2RectifiedGridCoverage" ],
        "grid_coverage_file": "gmljp2gridcoverage.xml",

        "#crs_url_doc": [
            "true for http://www.opengis.net/def/crs/EPSG/0/XXXX CRS URL.",
            "If false, use CRS URN. Default value is true" ],
        "crs_url": true,

        "#metadata_doc": [ "An array of metadata items. Can be either strings, with ",
                           "a filename or directly inline XML content, or either ",
                           "a more complete description." ],
        "metadata": [

            "dcmetadata.xml",

            {
                "#file_doc": "Can use relative or absolute paths. Exclusive of content, gdal_metadata and generated_metadata.",
                "file": "dcmetadata.xml",

                "#gdal_metadata_doc": "Whether to serialize GDAL metadata as GDALMultiDomainMetadata",
                "gdal_metadata": false,

                "#dynamic_metadata_doc":
                    [ "The metadata file will be generated from a template and a source file.",
                      "The template is a valid GMLJP2 metadata XML tree with placeholders like",
                      "{{{XPATH(some_xpath_expression)}}}",
                      "that are evaluated from the source XML file. Typical use case",
                      "is to generate a gmljp2:eopMetadata from the XML metadata",
                      "provided by the image provider in their own particular format." ],
                "dynamic_metadata" :
                {
                    "template": "my_template.xml",
                    "source": "my_source.xml"
                },

                "#content": "Exclusive of file. Inline XML metadata content",
                "content": "<gmljp2:metadata>Some simple textual metadata</gmljp2:metadata>",

                "#parent_node": ["Where to put the metadata.",
                                 "Under CoverageCollection (default) or GridCoverage" ],
                "parent_node": "CoverageCollection"
            },
        ],

        "#annotations_doc": [ "An array of filenames, either directly KML files",
                              "or other vector files recognized by GDAL that ",
                              "will be translated on-the-fly as KML" ],
        "annotations": [
            "my.kml"
        ],

        "#gml_filelist_doc" :[
            "An array of GML files. Can be either GML filenames, ",
            "or a more complete description" ],
        "gml_filelist": [

            "my.gml",

            {
                "#file_doc": "Can use relative or absolute paths. Exclusive of remote_resource",
                "file": "converted/test_0.gml",

                "#remote_resource_doc": "URL of a feature collection that must be referenced through a xlink:href",
                "remote_resource": "http://svn.osgeo.org/gdal/trunk/autotest/ogr/data/expected_gml_gml32.gml",

                "#namespace_doc": ["The namespace in schemaLocation for which to substitute",
                                  "its original schemaLocation with the one provided below.",
                                  "Ignored for a remote_resource"],
                "namespace": "http://example.com",

                "#schema_location_doc": ["Value of the substituted schemaLocation. ",
                                         "Typically a schema box label (link)",
                                         "Ignored for a remote_resource"],
                "schema_location": "gmljp2://xml/schema_0.xsd",

                "#inline_doc": [
                    "Whether to inline the content, or put it in a separate xml box. Default is true",
                    "Ignored for a remote_resource." ],
                "inline": true,

                "#parent_node": ["Where to put the FeatureCollection.",
                                 "Under CoverageCollection (default) or GridCoverage" ],
                "parent_node": "CoverageCollection"
            }
        ],

        "#styles_doc: [ "An array of styles. For example SLD files" ],
        "styles" : [
            {
                "#file_doc": "Can use relative or absolute paths.",
                "file": "my.sld",

                "#parent_node": ["Where to put the FeatureCollection.",
                                 "Under CoverageCollection (default) or GridCoverage" ],
                "parent_node": "CoverageCollection"
            }
        ],

        "#extensions_doc: [ "An array of extensions." ],
        "extensions" : [
            {
                "#file_doc": "Can use relative or absolute paths.",
                "file": "my.xml",

                "#parent_node": ["Where to put the FeatureCollection.",
                                 "Under CoverageCollection (default) or GridCoverage" ],
                "parent_node": "CoverageCollection"
            }
        ]
    },

    "#boxes_doc": "An array to describe the content of XML asoc boxes",
    "boxes": [
        {
            "#file_doc": "can use relative or absolute paths. Required",
            "file": "converted/test_0.xsd",

            "#label_doc": ["the label of the XML box. If not specified, will be the ",
                          "filename without the directory part." ],
            "label": "schema_0.xsd"
        }
    ]
}
*/

        json_tokener* jstok = NULL;
        json_object* poObj = NULL;

        jstok = json_tokener_new();
        poObj = json_tokener_parse_ex(jstok, pabyContent ? (const char*) pabyContent : pszDefFilename, -1);
        CPLFree(pabyContent);
        if( jstok->err != json_tokener_success)
        {
            CPLError( CE_Failure, CPLE_AppDefined,
                        "JSON parsing error: %s (at offset %d)",
                        json_tokener_error_desc(jstok->err), jstok->char_offset);
            json_tokener_free(jstok);
            return NULL;
        }
        json_tokener_free(jstok);

        json_object* poRootInstance = json_object_object_get(poObj, "root_instance");
        if( poRootInstance && json_object_get_type(poRootInstance) == json_type_object )
        {
            json_object* poGMLId = json_object_object_get(poRootInstance, "gml_id");
            if( poGMLId && json_object_get_type(poGMLId) == json_type_string )
                osRootGMLId = json_object_get_string(poGMLId);

            json_object* poGridCoverageFile = json_object_object_get(poRootInstance, "grid_coverage_file");
            if( poGridCoverageFile && json_object_get_type(poGridCoverageFile) == json_type_string )
                osGridCoverageFile = json_object_get_string(poGridCoverageFile);

            json_object* poCRSURL = json_object_object_get(poRootInstance, "crs_url");
            if( poCRSURL && json_object_get_type(poCRSURL) == json_type_boolean )
                bCRSURL = CPL_TO_BOOL(json_object_get_boolean(poCRSURL));


            json_object* poMetadatas = json_object_object_get(poRootInstance, "metadata");
            if( poMetadatas && json_object_get_type(poMetadatas) == json_type_array )
            {
                for(int i=0;i<json_object_array_length(poMetadatas);i++)
                {
                    json_object* poMetadata = json_object_array_get_idx(poMetadatas, i);
                    if( poMetadata && json_object_get_type(poMetadata) == json_type_string )
                    {
                        GMLJP2V2MetadataDesc oDesc;
                        const char* pszStr = json_object_get_string(poMetadata);
                        if( pszStr[0] == '<' )
                            oDesc.osContent = pszStr;
                        else
                            oDesc.osFile = pszStr;
                        aoMetadata.push_back(oDesc);
                    }
                    else if ( poMetadata && json_object_get_type(poMetadata) == json_type_object )
                    {
                        const char* pszFile = NULL;
                        json_object* poFile = json_object_object_get(poMetadata, "file");
                        if( poFile && json_object_get_type(poFile) == json_type_string )
                            pszFile = json_object_get_string(poFile);

                        const char* pszContent = NULL;
                        json_object* poContent = json_object_object_get(poMetadata, "content");
                        if( poContent && json_object_get_type(poContent) == json_type_string )
                            pszContent = json_object_get_string(poContent);

                        const char* pszTemplate = NULL;
                        const char* pszSource = NULL;
                        json_object* poDynamicMetadata = json_object_object_get(poMetadata, "dynamic_metadata");
                        if( poDynamicMetadata && json_object_get_type(poDynamicMetadata) == json_type_object )
                        {
#ifdef HAVE_LIBXML2
                            if( CPLTestBool(CPLGetConfigOption("GDAL_DEBUG_PROCESS_DYNAMIC_METADATA", "YES")) )
                            {
                                json_object* poTemplate = json_object_object_get(poDynamicMetadata, "template");
                                if( poTemplate && json_object_get_type(poTemplate) == json_type_string )
                                    pszTemplate = json_object_get_string(poTemplate);

                                json_object* poSource = json_object_object_get(poDynamicMetadata, "source");
                                if( poSource && json_object_get_type(poSource) == json_type_string )
                                    pszSource = json_object_get_string(poSource);
                            }
                            else
#endif
                            {
                            CPLError(CE_Warning, CPLE_NotSupported,
                                     "dynamic_metadata not supported since libxml2 is not available");
                            }
                        }

                        int bGDALMetadata = FALSE;
                        json_object* poGDALMetadata = json_object_object_get(poMetadata, "gdal_metadata");
                        if( poGDALMetadata && json_object_get_type(poGDALMetadata) == json_type_boolean )
                            bGDALMetadata = json_object_get_boolean(poGDALMetadata);

                        if( pszFile != NULL || pszContent != NULL ||
                            (pszTemplate != NULL && pszSource != NULL) ||
                            bGDALMetadata )
                        {
                            GMLJP2V2MetadataDesc oDesc;
                            if( pszFile )
                                oDesc.osFile = pszFile;
                            if( pszContent )
                                oDesc.osContent = pszContent;
                            if( pszTemplate )
                                oDesc.osTemplateFile = pszTemplate;
                            if( pszSource )
                                oDesc.osSourceFile = pszSource;
                            oDesc.bGDALMetadata = bGDALMetadata;

                            json_object* poLocation = json_object_object_get(poMetadata, "parent_node");
                            if( poLocation && json_object_get_type(poLocation) == json_type_string )
                            {
                                const char* pszLocation = json_object_get_string(poLocation);
                                if( EQUAL(pszLocation, "CoverageCollection") )
                                    oDesc.bParentCoverageCollection  = TRUE;
                                else if( EQUAL(pszLocation, "GridCoverage") )
                                    oDesc.bParentCoverageCollection = FALSE;
                                else
                                    CPLError(CE_Warning, CPLE_NotSupported,
                                             "metadata[].parent_node should be CoverageCollection or GridCoverage");
                            }

                            aoMetadata.push_back(oDesc);
                        }
                    }
                }
            }

            json_object* poAnnotations = json_object_object_get(poRootInstance, "annotations");
            if( poAnnotations && json_object_get_type(poAnnotations) == json_type_array )
            {
                for(int i=0;i<json_object_array_length(poAnnotations);i++)
                {
                    json_object* poAnnotation = json_object_array_get_idx(poAnnotations, i);
                    if( poAnnotation && json_object_get_type(poAnnotation) == json_type_string )
                    {
                        GMLJP2V2AnnotationDesc oDesc;
                        oDesc.osFile = json_object_get_string(poAnnotation);
                        aoAnnotations.push_back(oDesc);
                    }
                }
            }

            json_object* poGMLFileList = json_object_object_get(poRootInstance, "gml_filelist");
            if( poGMLFileList && json_object_get_type(poGMLFileList) == json_type_array )
            {
                for(int i=0;i<json_object_array_length(poGMLFileList);i++)
                {
                    json_object* poGMLFile = json_object_array_get_idx(poGMLFileList, i);
                    if( poGMLFile && json_object_get_type(poGMLFile) == json_type_object )
                    {
                        const char* pszFile = NULL;
                        json_object* poFile = json_object_object_get(poGMLFile, "file");
                        if( poFile && json_object_get_type(poFile) == json_type_string )
                            pszFile = json_object_get_string(poFile);

                        const char* pszRemoteResource = NULL;
                        json_object* poRemoteResource = json_object_object_get(poGMLFile, "remote_resource");
                        if( poRemoteResource && json_object_get_type(poRemoteResource) == json_type_string )
                            pszRemoteResource = json_object_get_string(poRemoteResource);

                        if( pszFile || pszRemoteResource )
                        {
                            GMLJP2V2GMLFileDesc oDesc;
                            if( pszFile )
                                oDesc.osFile = pszFile;
                            else if( pszRemoteResource )
                                oDesc.osRemoteResource = pszRemoteResource;

                            json_object* poNamespace = json_object_object_get(poGMLFile, "namespace");
                            if( poNamespace && json_object_get_type(poNamespace) == json_type_string )
                                oDesc.osNamespace = json_object_get_string(poNamespace);

                            json_object* poSchemaLocation = json_object_object_get(poGMLFile, "schema_location");
                            if( poSchemaLocation && json_object_get_type(poSchemaLocation) == json_type_string )
                                oDesc.osSchemaLocation = json_object_get_string(poSchemaLocation);

                            json_object* poInline = json_object_object_get(poGMLFile, "inline");
                            if( poInline && json_object_get_type(poInline) == json_type_boolean )
                                oDesc.bInline = json_object_get_boolean(poInline);


                            json_object* poLocation = json_object_object_get(poGMLFile, "parent_node");
                            if( poLocation && json_object_get_type(poLocation) == json_type_string )
                            {
                                const char* pszLocation = json_object_get_string(poLocation);
                                if( EQUAL(pszLocation, "CoverageCollection") )
                                    oDesc.bParentCoverageCollection  = TRUE;
                                else if( EQUAL(pszLocation, "GridCoverage") )
                                    oDesc.bParentCoverageCollection = FALSE;
                                else
                                    CPLError(CE_Warning, CPLE_NotSupported,
                                             "gml_filelist[].parent_node should be CoverageCollection or GridCoverage");
                            }

                            aoGMLFiles.push_back(oDesc);
                        }
                    }
                    else if( poGMLFile && json_object_get_type(poGMLFile) == json_type_string )
                    {
                        GMLJP2V2GMLFileDesc oDesc;
                        oDesc.osFile = json_object_get_string(poGMLFile);
                        aoGMLFiles.push_back(oDesc);
                    }
                }
            }

            json_object* poStyles = json_object_object_get(poRootInstance, "styles");
            if( poStyles && json_object_get_type(poStyles) == json_type_array )
            {
                for(int i=0;i<json_object_array_length(poStyles);i++)
                {
                    json_object* poStyle = json_object_array_get_idx(poStyles, i);
                    if( poStyle && json_object_get_type(poStyle) == json_type_object )
                    {
                        const char* pszFile = NULL;
                        json_object* poFile = json_object_object_get(poStyle, "file");
                        if( poFile && json_object_get_type(poFile) == json_type_string )
                            pszFile = json_object_get_string(poFile);

                        if( pszFile )
                        {
                            GMLJP2V2StyleDesc oDesc;
                            oDesc.osFile = pszFile;

                            json_object* poLocation = json_object_object_get(poStyle, "parent_node");
                            if( poLocation && json_object_get_type(poLocation) == json_type_string )
                            {
                                const char* pszLocation = json_object_get_string(poLocation);
                                if( EQUAL(pszLocation, "CoverageCollection") )
                                    oDesc.bParentCoverageCollection  = TRUE;
                                else if( EQUAL(pszLocation, "GridCoverage") )
                                    oDesc.bParentCoverageCollection = FALSE;
                                else
                                    CPLError(CE_Warning, CPLE_NotSupported,
                                             "styles[].parent_node should be CoverageCollection or GridCoverage");
                            }

                            aoStyles.push_back(oDesc);
                        }
                    }
                    else if( poStyle && json_object_get_type(poStyle) == json_type_string )
                    {
                        GMLJP2V2StyleDesc oDesc;
                        oDesc.osFile = json_object_get_string(poStyle);
                        aoStyles.push_back(oDesc);
                    }
                }
            }

            json_object* poExtensions = json_object_object_get(poRootInstance, "extensions");
            if( poExtensions && json_object_get_type(poExtensions) == json_type_array )
            {
                for(int i=0;i<json_object_array_length(poExtensions);i++)
                {
                    json_object* poExtension = json_object_array_get_idx(poExtensions, i);
                    if( poExtension && json_object_get_type(poExtension) == json_type_object )
                    {
                        const char* pszFile = NULL;
                        json_object* poFile = json_object_object_get(poExtension, "file");
                        if( poFile && json_object_get_type(poFile) == json_type_string )
                            pszFile = json_object_get_string(poFile);

                        if( pszFile )
                        {
                            GMLJP2V2ExtensionDesc oDesc;
                            oDesc.osFile = pszFile;

                            json_object* poLocation = json_object_object_get(poExtension, "parent_node");
                            if( poLocation && json_object_get_type(poLocation) == json_type_string )
                            {
                                const char* pszLocation = json_object_get_string(poLocation);
                                if( EQUAL(pszLocation, "CoverageCollection") )
                                    oDesc.bParentCoverageCollection  = TRUE;
                                else if( EQUAL(pszLocation, "GridCoverage") )
                                    oDesc.bParentCoverageCollection = FALSE;
                                else
                                    CPLError(CE_Warning, CPLE_NotSupported,
                                             "extensions[].parent_node should be CoverageCollection or GridCoverage");
                            }

                            aoExtensions.push_back(oDesc);
                        }
                    }
                    else if( poExtension && json_object_get_type(poExtension) == json_type_string )
                    {
                        GMLJP2V2ExtensionDesc oDesc;
                        oDesc.osFile = json_object_get_string(poExtension);
                        aoExtensions.push_back(oDesc);
                    }
                }
            }
        }

        json_object* poBoxes = json_object_object_get(poObj, "boxes");
        if( poBoxes && json_object_get_type(poBoxes) == json_type_array )
        {
            for(int i=0;i<json_object_array_length(poBoxes);i++)
            {
                json_object* poBox = json_object_array_get_idx(poBoxes, i);
                if( poBox && json_object_get_type(poBox) == json_type_object )
                {
                    json_object* poFile = json_object_object_get(poBox, "file");
                    if( poFile && json_object_get_type(poFile) == json_type_string )
                    {
                        GMLJP2V2BoxDesc oDesc;
                        oDesc.osFile = json_object_get_string(poFile);

                        json_object* poLabel = json_object_object_get(poBox, "label");
                        if( poLabel && json_object_get_type(poLabel) == json_type_string )
                            oDesc.osLabel = json_object_get_string(poLabel);
                        else
                            oDesc.osLabel = CPLGetFilename(oDesc.osFile);

                        aoBoxes.push_back(oDesc);
                    }
                }
                else if( poBox && json_object_get_type(poBox) == json_type_string )
                {
                    GMLJP2V2BoxDesc oDesc;
                    oDesc.osFile = json_object_get_string(poBox);
                    oDesc.osLabel = CPLGetFilename(oDesc.osFile);
                    aoBoxes.push_back(oDesc);
                }
            }
        }

        json_object_put(poObj);

        // Check that if a GML file points to an internal schemaLocation,
        // the matching box really exists.
        for(int i=0;i<(int)aoGMLFiles.size();i++)
        {
            if( aoGMLFiles[i].osSchemaLocation.size() &&
                STARTS_WITH(aoGMLFiles[i].osSchemaLocation, "gmljp2://xml/") )            {
                const char* pszLookedLabel =
                    aoGMLFiles[i].osSchemaLocation.c_str() + strlen("gmljp2://xml/");
                bool bFound = false;
                for(int j=0; !bFound && j<(int)aoBoxes.size();j++)
                    bFound = (strcmp(pszLookedLabel, aoBoxes[j].osLabel) == 0);
                if( !bFound )
                {
                    CPLError(CE_Warning, CPLE_AppDefined,
                             "GML file %s has a schema_location=%s, but no box with label %s is defined",
                             aoGMLFiles[i].osFile.c_str(),
                             aoGMLFiles[i].osSchemaLocation.c_str(),
                             pszLookedLabel);
                }
            }
        }

        // Read custom grid coverage file
        if( osGridCoverageFile.size() > 0 )
        {
            CPLXMLNode* psTmp = CPLParseXMLFile(osGridCoverageFile);
            if( psTmp == NULL )
                return NULL;
            CPLXMLNode* psTmpRoot = GDALGMLJP2GetXMLRoot(psTmp);
            if( psTmpRoot )
            {
                char* pszTmp = CPLSerializeXMLTree(psTmpRoot);
                osGridCoverage = pszTmp;
                CPLFree(pszTmp);
            }
            CPLDestroyXMLNode(psTmp);
        }
    }

    CPLString osDictBox;
    CPLString osDoc;

    if( osGridCoverage.size() == 0 )
    {
/* -------------------------------------------------------------------- */
/*      Prepare GMLJP2RectifiedGridCoverage                             */
/* -------------------------------------------------------------------- */
        int nEPSGCode = 0;
        double adfOrigin[2];
        double adfXVector[2];
        double adfYVector[2];
        const char* pszComment = "";
        int bNeedAxisFlip = FALSE;
        if( !GetGMLJP2GeoreferencingInfo( nEPSGCode, adfOrigin,
                                        adfXVector, adfYVector,
                                        pszComment, osDictBox, bNeedAxisFlip ) )
        {
            return NULL;
        }

        char szSRSName[100] = {0};
        if( nEPSGCode != 0 )
        {
            if( bCRSURL )
                snprintf( szSRSName, sizeof(szSRSName),
                          "http://www.opengis.net/def/crs/EPSG/0/%d", nEPSGCode );
            else
                snprintf( szSRSName, sizeof(szSRSName),
                          "urn:ogc:def:crs:EPSG::%d", nEPSGCode );
        }
        else
            snprintf( szSRSName, sizeof(szSRSName), "%s",
                    "gmljp2://xml/CRSDictionary.gml#ogrcrs1" );

        osGridCoverage.Printf(
"   <gmljp2:GMLJP2RectifiedGridCoverage gml:id=\"RGC_1_%s\">\n"
"     <gml:domainSet>\n"
"      <gml:RectifiedGrid gml:id=\"RGC_1_GRID_%s\" dimension=\"2\" srsName=\"%s\">\n"
"       <gml:limits>\n"
"         <gml:GridEnvelope>\n"
"           <gml:low>0 0</gml:low>\n"
"           <gml:high>%d %d</gml:high>\n"
"         </gml:GridEnvelope>\n"
"       </gml:limits>\n"
"       <gml:axisName>x</gml:axisName>\n"
"       <gml:axisName>y</gml:axisName>\n"
"       <gml:origin>\n"
"         <gml:Point gml:id=\"P0001\" srsName=\"%s\">\n"
"           <gml:pos>%.15g %.15g</gml:pos>\n"
"         </gml:Point>\n"
"       </gml:origin>\n"
"%s"
"       <gml:offsetVector srsName=\"%s\">%.15g %.15g</gml:offsetVector>\n"
"       <gml:offsetVector srsName=\"%s\">%.15g %.15g</gml:offsetVector>\n"
"      </gml:RectifiedGrid>\n"
"     </gml:domainSet>\n"
"     <gml:rangeSet>\n"
"      <gml:File>\n"
"        <gml:rangeParameters/>\n"
"        <gml:fileName>gmljp2://codestream/0</gml:fileName>\n"
"        <gml:fileStructure>inapplicable</gml:fileStructure>\n"
"      </gml:File>\n"
"     </gml:rangeSet>\n"
"     <gmlcov:rangeType/>\n"
"   </gmljp2:GMLJP2RectifiedGridCoverage>\n",
            osRootGMLId.c_str(),
            osRootGMLId.c_str(),
            szSRSName,
            nXSize-1, nYSize-1, szSRSName, adfOrigin[0], adfOrigin[1],
            pszComment,
            szSRSName, adfXVector[0], adfXVector[1],
            szSRSName, adfYVector[0], adfYVector[1] );
    }

/* -------------------------------------------------------------------- */
/*      Main node.                                                      */
/* -------------------------------------------------------------------- */
    osDoc.Printf(
//"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<gmljp2:GMLJP2CoverageCollection gml:id=\"%s\"\n"
"     xmlns:gml=\"http://www.opengis.net/gml/3.2\"\n"
"     xmlns:gmlcov=\"http://www.opengis.net/gmlcov/1.0\"\n"
"     xmlns:gmljp2=\"http://www.opengis.net/gmljp2/2.0\"\n"
"     xmlns:swe=\"http://www.opengis.net/swe/2.0\"\n"
"     xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
"     xsi:schemaLocation=\"http://www.opengis.net/gmljp2/2.0 http://schemas.opengis.net/gmljp2/2.0/gmljp2.xsd\">\n"
"  <gml:gridDomain/>\n"
"  <gml:rangeSet>\n"
"   <gml:File>\n"
"     <gml:rangeParameters/>\n"
"     <gml:fileName>gmljp2://codestream</gml:fileName>\n"
"     <gml:fileStructure>inapplicable</gml:fileStructure>\n"
"   </gml:File>\n"
"  </gml:rangeSet>\n"
"  <gmlcov:rangeType/>\n"
"  <gmljp2:featureMember>\n"
"%s"
"  </gmljp2:featureMember>\n"
"</gmljp2:GMLJP2CoverageCollection>\n",
                 osRootGMLId.c_str(),
                 osGridCoverage.c_str() );

/* -------------------------------------------------------------------- */
/*      Process metadata, annotations and features collections.         */
/* -------------------------------------------------------------------- */
    std::vector<CPLString> aosTmpFiles;
    bool bRootHasXLink = false;
    if( aoMetadata.size() || aoAnnotations.size() || aoGMLFiles.size() ||
        aoStyles.size() || aoExtensions.size() )
    {
        CPLXMLNode* psRoot = CPLParseXMLString(osDoc);
        CPLAssert(psRoot);
        CPLXMLNode* psGMLJP2CoverageCollection = GDALGMLJP2GetXMLRoot(psRoot);
        CPLAssert(psGMLJP2CoverageCollection);

        for( int i=0; i < (int)aoMetadata.size(); i++ )
        {
            CPLXMLNode* psMetadata;
            if( aoMetadata[i].osFile.size() )
                psMetadata = CPLParseXMLFile(aoMetadata[i].osFile);
            else if( aoMetadata[i].osContent.size() )
                psMetadata = CPLParseXMLString(aoMetadata[i].osContent);
            else if( aoMetadata[i].bGDALMetadata )
            {
                psMetadata = CreateGDALMultiDomainMetadataXML(poSrcDS, TRUE);
                if( psMetadata )
                {
                    CPLSetXMLValue(psMetadata, "#xmlns", "http://gdal.org");
                    CPLXMLNode* psNewMetadata = CPLCreateXMLNode(NULL, CXT_Element, "gmljp2:metadata");
                    CPLAddXMLChild(psNewMetadata, psMetadata);
                    psMetadata = psNewMetadata;
                }
            }
            else
                psMetadata = GDALGMLJP2GenerateMetadata(aoMetadata[i].osTemplateFile,
                                                        aoMetadata[i].osSourceFile);
            if( psMetadata == NULL )
                continue;
            CPLXMLNode* psMetadataRoot = GDALGMLJP2GetXMLRoot(psMetadata);
            if( psMetadataRoot )
            {
                if( strcmp(psMetadataRoot->pszValue, "eop:EarthObservation") == 0 )
                {
                    CPLXMLNode* psNewMetadata = CPLCreateXMLNode(NULL, CXT_Element, "gmljp2:eopMetadata");
                    CPLAddXMLChild(psNewMetadata, CPLCloneXMLTree(psMetadataRoot));
                    CPLDestroyXMLNode(psMetadata);
                    psMetadata = psMetadataRoot = psNewMetadata;
                }
                if( strcmp(psMetadataRoot->pszValue, "gmljp2:isoMetadata") != 0 &&
                    strcmp(psMetadataRoot->pszValue, "gmljp2:eopMetadata") != 0 &&
                    strcmp(psMetadataRoot->pszValue, "gmljp2:dcMetadata") != 0 &&
                    strcmp(psMetadataRoot->pszValue, "gmljp2:metadata") != 0 )
                {
                    CPLError(CE_Warning, CPLE_AppDefined,
                             "The metadata root node should be one of gmljp2:isoMetadata, "
                             "gmljp2:eopMetadata, gmljp2:dcMetadata or gmljp2:metadata");
                }
                else if( aoMetadata[i].bParentCoverageCollection )
                {
                    /* Insert the gmlcov:metadata link as the next sibling of */
                    /* GMLJP2CoverageCollection.rangeType */
                    CPLXMLNode* psRangeType =
                        CPLGetXMLNode(psGMLJP2CoverageCollection, "gmlcov:rangeType");
                    CPLAssert(psRangeType);
                    CPLXMLNode* psNodeAfterWhichToInsert = psRangeType;
                    CPLXMLNode* psNext = psNodeAfterWhichToInsert->psNext;
                    while( psNext != NULL && psNext->eType == CXT_Element &&
                           strcmp(psNext->pszValue, "gmlcov:metadata") == 0 )
                    {
                        psNodeAfterWhichToInsert = psNext;
                        psNext = psNext->psNext;
                    }
                    psNodeAfterWhichToInsert->psNext = NULL;
                    CPLXMLNode* psGMLCovMetadata = CPLCreateXMLNode(
                        psGMLJP2CoverageCollection, CXT_Element, "gmlcov:metadata" );
                    psGMLCovMetadata->psNext = psNext;
                    CPLXMLNode* psGMLJP2Metadata = CPLCreateXMLNode(
                        psGMLCovMetadata, CXT_Element, "gmljp2:Metadata" );
                    CPLAddXMLChild( psGMLJP2Metadata, CPLCloneXMLTree(psMetadataRoot) );
                }
                else
                {
                    /* Insert the gmlcov:metadata link as the last child of */
                    /* GMLJP2RectifiedGridCoverage typically */
                    CPLXMLNode* psFeatureMemberOfGridCoverage =
                        CPLGetXMLNode(psGMLJP2CoverageCollection, "gmljp2:featureMember");
                    CPLAssert(psFeatureMemberOfGridCoverage);
                    CPLXMLNode* psGridCoverage = psFeatureMemberOfGridCoverage->psChild;
                    CPLAssert(psGridCoverage);
                    CPLXMLNode* psGMLCovMetadata = CPLCreateXMLNode(
                        psGridCoverage, CXT_Element, "gmlcov:metadata" );
                    CPLXMLNode* psGMLJP2Metadata = CPLCreateXMLNode(
                        psGMLCovMetadata, CXT_Element, "gmljp2:Metadata" );
                    CPLAddXMLChild( psGMLJP2Metadata, CPLCloneXMLTree(psMetadataRoot) );
                }
            }
            CPLDestroyXMLNode(psMetadata);
        }

        // Examples of inline or reference feature collections can be found
        // in http://schemas.opengis.net/gmljp2/2.0/examples/gmljp2.xml

        for( int i=0; i < (int)aoGMLFiles.size(); i++ )
        {
            // Is the file already a GML file ?
            CPLXMLNode* psGMLFile = NULL;
            if( aoGMLFiles[i].osFile.size() )
            {
                if( EQUAL(CPLGetExtension(aoGMLFiles[i].osFile), "gml") ||
                    EQUAL(CPLGetExtension(aoGMLFiles[i].osFile), "xml") )
                {
                    psGMLFile = CPLParseXMLFile(aoGMLFiles[i].osFile);
                }
                GDALDriverH hDrv = NULL;
                if( psGMLFile == NULL )
                {
                    hDrv = GDALIdentifyDriver(aoGMLFiles[i].osFile, NULL);
                    if( hDrv == NULL )
                    {
                        CPLError(CE_Failure, CPLE_AppDefined,
                                "%s is no a GDAL recognized file",
                                aoGMLFiles[i].osFile.c_str());
                        continue;
                    }
                }
                GDALDriverH hGMLDrv = GDALGetDriverByName("GML");
                if( psGMLFile == NULL && hDrv == hGMLDrv )
                {
                    // Yes, parse it
                    psGMLFile = CPLParseXMLFile(aoGMLFiles[i].osFile);
                }
                else if( psGMLFile == NULL )
                {
                    if( hGMLDrv == NULL )
                    {
                        CPLError(CE_Failure, CPLE_AppDefined,
                                "Cannot translate %s to GML",
                                aoGMLFiles[i].osFile.c_str());
                        continue;
                    }

                    // On-the-fly translation to GML 3.2
                    GDALDatasetH hSrcDS = GDALOpenEx(aoGMLFiles[i].osFile, 0, NULL, NULL, NULL);
                    if( hSrcDS )
                    {
                        CPLString osTmpFile = CPLSPrintf("/vsimem/gmljp2/%p/%d/%s.gml",
                                                        this,
                                                        i,
                                                        CPLGetBasename(aoGMLFiles[i].osFile));
                        char* apszOptions[2];
                        apszOptions[0] = (char*) "FORMAT=GML3.2";
                        apszOptions[1] = NULL;
                        GDALDatasetH hDS = GDALCreateCopy(hGMLDrv, osTmpFile, hSrcDS,
                                                        FALSE, apszOptions, NULL, NULL);
                        if( hDS )
                        {
                            GDALClose(hDS);
                            psGMLFile = CPLParseXMLFile(osTmpFile);
                            aoGMLFiles[i].osFile = osTmpFile;
                            VSIUnlink(osTmpFile);
                            aosTmpFiles.push_back(CPLResetExtension(osTmpFile, "xsd"));
                        }
                        else
                        {
                            CPLError(CE_Failure, CPLE_AppDefined,
                                    "Conversion of %s to GML failed",
                                    aoGMLFiles[i].osFile.c_str());
                        }
                    }
                    GDALClose(hSrcDS);
                }
                if( psGMLFile == NULL )
                    continue;
            }

            CPLXMLNode* psGMLFileRoot = psGMLFile ? GDALGMLJP2GetXMLRoot(psGMLFile) : NULL;
            if( psGMLFileRoot || aoGMLFiles[i].osRemoteResource.size() )
            {
                CPLXMLNode *node_f;
                if( aoGMLFiles[i].bParentCoverageCollection )
                {
                    // Insert in gmljp2:featureMember.gmljp2:GMLJP2Features.gmljp2:feature
                    CPLXMLNode *node_fm = CPLCreateXMLNode(
                            psGMLJP2CoverageCollection, CXT_Element, "gmljp2:featureMember" );

                    CPLXMLNode *node_gf = CPLCreateXMLNode(
                            node_fm, CXT_Element, "gmljp2:GMLJP2Features" );


                    CPLSetXMLValue(node_gf, "#gml:id", CPLSPrintf("%s_GMLJP2Features_%d",
                                                                    osRootGMLId.c_str(),
                                                                    i));

                    node_f = CPLCreateXMLNode( node_gf, CXT_Element, "gmljp2:feature"  );
                }
                else
                {
                    CPLXMLNode* psFeatureMemberOfGridCoverage =
                        CPLGetXMLNode(psGMLJP2CoverageCollection, "gmljp2:featureMember");
                    CPLAssert(psFeatureMemberOfGridCoverage);
                    CPLXMLNode* psGridCoverage = psFeatureMemberOfGridCoverage->psChild;
                    CPLAssert(psGridCoverage);
                    node_f = CPLCreateXMLNode( psGridCoverage, CXT_Element, "gmljp2:feature"  );
                }

                if( !aoGMLFiles[i].bInline || aoGMLFiles[i].osRemoteResource.size() )
                {
                    if( !bRootHasXLink )
                    {
                        bRootHasXLink = true;
                        CPLSetXMLValue(psGMLJP2CoverageCollection, "#xmlns:xlink",
                                       "http://www.w3.org/1999/xlink");
                    }
                }

                if( aoGMLFiles[i].osRemoteResource.size() )
                {
                    CPLSetXMLValue(node_f, "#xlink:href",
                                   aoGMLFiles[i].osRemoteResource.c_str());
                    continue;
                }

                CPLString osTmpFile;
                if( !aoGMLFiles[i].bInline || aoGMLFiles[i].osRemoteResource.size() )
                {
                    osTmpFile = CPLSPrintf("/vsimem/gmljp2/%p/%d/%s.gml",
                                           this,
                                           i,
                                           CPLGetBasename(aoGMLFiles[i].osFile));
                    aosTmpFiles.push_back(osTmpFile);

                    GMLJP2V2BoxDesc oDesc;
                    oDesc.osFile = osTmpFile;
                    oDesc.osLabel = CPLGetFilename(oDesc.osFile);
                    aoBoxes.push_back(oDesc);

                    CPLSetXMLValue(node_f, "#xlink:href",
                            CPLSPrintf("gmljp2://xml/%s", oDesc.osLabel.c_str()));
                }

                if( CPLGetXMLNode(psGMLFileRoot, "xmlns") == NULL &&
                    CPLGetXMLNode(psGMLFileRoot, "xmlns:gml") == NULL )
                {
                    CPLSetXMLValue(psGMLFileRoot, "#xmlns",
                                   "http://www.opengis.net/gml/3.2");
                }

                // modify the gml id making it unique for this document
                CPLXMLNode* psGMLFileGMLId =
                    CPLGetXMLNode(psGMLFileRoot, "gml:id");
                if( psGMLFileGMLId && psGMLFileGMLId->eType == CXT_Attribute )
                    CPLSetXMLValue( psGMLFileGMLId, "",
                                    CPLSPrintf("%s_%d_%s",
                                                osRootGMLId.c_str(), i,
                                                psGMLFileGMLId->psChild->pszValue) );
                psGMLFileGMLId = NULL;
                //PrefixAllGMLIds(psGMLFileRoot, CPLSPrintf("%s_%d_", osRootGMLId.c_str(), i));

                // replace schema location
                CPLXMLNode* psSchemaLocation =
                    CPLGetXMLNode(psGMLFileRoot, "xsi:schemaLocation");
                if( psSchemaLocation && psSchemaLocation->eType == CXT_Attribute )
                {
                    char **papszTokens = CSLTokenizeString2(
                        psSchemaLocation->psChild->pszValue, " \t\n",
                        CSLT_HONOURSTRINGS | CSLT_STRIPLEADSPACES | CSLT_STRIPENDSPACES);
                    CPLString osSchemaLocation;

                    if( CSLCount(papszTokens) == 2 &&
                        aoGMLFiles[i].osNamespace.size() == 0 &&
                        aoGMLFiles[i].osSchemaLocation.size() )
                    {
                        osSchemaLocation += papszTokens[0];
                        osSchemaLocation += " ";
                        osSchemaLocation += aoGMLFiles[i].osSchemaLocation;
                    }

                    else if( CSLCount(papszTokens) == 2 &&
                                (aoGMLFiles[i].osNamespace.size() == 0 ||
                                strcmp(papszTokens[0], aoGMLFiles[i].osNamespace) == 0) &&
                                aoGMLFiles[i].osSchemaLocation.size() == 0 )
                    {
                        VSIStatBufL sStat;
                        CPLString osXSD;
                        if( CSLCount(papszTokens) == 2 &&
                            !CPLIsFilenameRelative(papszTokens[1]) &&
                            VSIStatL(papszTokens[1], &sStat) == 0 )
                        {
                            osXSD = papszTokens[1];
                        }
                        else if( CSLCount(papszTokens) == 2 &&
                                CPLIsFilenameRelative(papszTokens[1]) &&
                                VSIStatL(CPLFormFilename(CPLGetDirname(aoGMLFiles[i].osFile),
                                                        papszTokens[1], NULL),
                                        &sStat) == 0 )
                        {
                            osXSD = CPLFormFilename(CPLGetDirname(aoGMLFiles[i].osFile),
                                                        papszTokens[1], NULL);
                        }
                        if( osXSD.size() )
                        {
                            GMLJP2V2BoxDesc oDesc;
                            oDesc.osFile = osXSD;
                            oDesc.osLabel = CPLGetFilename(oDesc.osFile);
                            osSchemaLocation += papszTokens[0];
                            osSchemaLocation += " ";
                            osSchemaLocation += "gmljp2://xml/";
                            osSchemaLocation += oDesc.osLabel;
                            int j;
                            for( j=0; j<(int)aoBoxes.size(); j++)
                            {
                                if( aoBoxes[j].osLabel == oDesc.osLabel )
                                    break;
                            }
                            if( j == (int)aoBoxes.size() )
                                aoBoxes.push_back(oDesc);
                        }
                    }

                    else if( (CSLCount(papszTokens) % 2) == 0 )
                    {
                        for(char** papszIter = papszTokens; *papszIter; papszIter += 2 )
                        {
                            if( osSchemaLocation.size() )
                                osSchemaLocation += " ";
                            if( aoGMLFiles[i].osNamespace.size() &&
                                aoGMLFiles[i].osSchemaLocation.size() &&
                                strcmp(papszIter[0], aoGMLFiles[i].osNamespace) == 0 )
                            {
                                osSchemaLocation += papszIter[0];
                                osSchemaLocation += " ";
                                osSchemaLocation += aoGMLFiles[i].osSchemaLocation;
                            }
                            else
                            {
                                osSchemaLocation += papszIter[0];
                                osSchemaLocation += " ";
                                osSchemaLocation += papszIter[1];
                            }
                        }
                    }
                    CSLDestroy(papszTokens);
                    CPLSetXMLValue( psSchemaLocation, "", osSchemaLocation);
                }

                if( aoGMLFiles[i].bInline )
                    CPLAddXMLChild(node_f, CPLCloneXMLTree(psGMLFileRoot));
                else
                    CPLSerializeXMLTreeToFile( psGMLFile, osTmpFile );
            }
            CPLDestroyXMLNode(psGMLFile);
        }

        // Cf http://schemas.opengis.net/gmljp2/2.0/examples/gmljp2_annotation.xml
        for( int i=0; i < (int)aoAnnotations.size(); i++ )
        {
            // Is the file already a KML file ?
            CPLXMLNode* psKMLFile = NULL;
            if( EQUAL(CPLGetExtension(aoAnnotations[i].osFile), "kml") )
                 psKMLFile = CPLParseXMLFile(aoAnnotations[i].osFile);
            GDALDriverH hDrv = NULL;
            if( psKMLFile == NULL )
            {
                hDrv = GDALIdentifyDriver(aoAnnotations[i].osFile, NULL);
                if( hDrv == NULL )
                {
                    CPLError(CE_Failure, CPLE_AppDefined, "%s is no a GDAL recognized file",
                             aoAnnotations[i].osFile.c_str());
                    continue;
                }
            }
            GDALDriverH hKMLDrv = GDALGetDriverByName("KML");
            GDALDriverH hLIBKMLDrv = GDALGetDriverByName("LIBKML");
            if( psKMLFile == NULL && (hDrv == hKMLDrv || hDrv == hLIBKMLDrv) )
            {
                // Yes, parse it
                psKMLFile = CPLParseXMLFile(aoAnnotations[i].osFile);
            }
            else if( psKMLFile == NULL )
            {
                if( hKMLDrv == NULL && hLIBKMLDrv == NULL )
                {
                    CPLError(CE_Failure, CPLE_AppDefined, "Cannot translate %s to KML",
                             aoAnnotations[i].osFile.c_str());
                    continue;
                }

                // On-the-fly translation to KML
                GDALDatasetH hSrcDS = GDALOpenEx(aoAnnotations[i].osFile, 0, NULL, NULL, NULL);
                if( hSrcDS )
                {
                    CPLString osTmpFile = CPLSPrintf("/vsimem/gmljp2/%p/%d/%s.kml",
                                                     this,
                                                     i,
                                                     CPLGetBasename(aoAnnotations[i].osFile));
                    char* apszOptions[2];
                    apszOptions[0] = NULL;
                    apszOptions[1] = NULL;
                    GDALDatasetH hDS = GDALCreateCopy(hLIBKMLDrv ? hLIBKMLDrv : hKMLDrv,
                                                      osTmpFile, hSrcDS,
                                                      FALSE, apszOptions, NULL, NULL);
                    if( hDS )
                    {
                        GDALClose(hDS);
                        psKMLFile = CPLParseXMLFile(osTmpFile);
                        aoAnnotations[i].osFile = osTmpFile;
                        VSIUnlink(osTmpFile);
                    }
                    else
                    {
                        CPLError(CE_Failure, CPLE_AppDefined, "Conversion of %s to KML failed",
                                  aoAnnotations[i].osFile.c_str());
                    }
                }
                GDALClose(hSrcDS);
            }
            if( psKMLFile == NULL )
                continue;

            CPLXMLNode* psKMLFileRoot = GDALGMLJP2GetXMLRoot(psKMLFile);
            if( psKMLFileRoot )
            {
                CPLXMLNode* psFeatureMemberOfGridCoverage =
                    CPLGetXMLNode(psGMLJP2CoverageCollection, "gmljp2:featureMember");
                CPLAssert(psFeatureMemberOfGridCoverage);
                CPLXMLNode* psGridCoverage = psFeatureMemberOfGridCoverage->psChild;
                CPLAssert(psGridCoverage);
                CPLXMLNode *psAnnotation = CPLCreateXMLNode(
                            psGridCoverage, CXT_Element, "gmljp2:annotation" );

                /* Add a xsi:schemaLocation if not already present */
                if( psKMLFileRoot->eType == CXT_Element &&
                    strcmp(psKMLFileRoot->pszValue, "kml") == 0 &&
                    CPLGetXMLNode(psKMLFileRoot, "xsi:schemaLocation") == NULL &&
                    strcmp(CPLGetXMLValue(psKMLFileRoot, "xmlns", ""),
                           "http://www.opengis.net/kml/2.2") == 0  )
                {
                    CPLSetXMLValue(psKMLFileRoot, "#xsi:schemaLocation",
                                   "http://www.opengis.net/kml/2.2 http://schemas.opengis.net/kml/2.2.0/ogckml22.xsd");
                }

                CPLAddXMLChild(psAnnotation, CPLCloneXMLTree(psKMLFileRoot));
            }
            CPLDestroyXMLNode(psKMLFile);
        }

        // Add styles
        for( int i=0; i < (int)aoStyles.size(); i++ )
        {
            CPLXMLNode* psStyle = CPLParseXMLFile(aoStyles[i].osFile);
            if( psStyle == NULL )
                continue;

            CPLXMLNode* psStyleRoot = GDALGMLJP2GetXMLRoot(psStyle);
            if( psStyleRoot )
            {
                CPLXMLNode *psGMLJP2Style;
                if( aoStyles[i].bParentCoverageCollection )
                {
                    psGMLJP2Style = CPLCreateXMLNode( psGMLJP2CoverageCollection, CXT_Element, "gmljp2:style"  );
                }
                else
                {
                    CPLXMLNode* psFeatureMemberOfGridCoverage =
                        CPLGetXMLNode(psGMLJP2CoverageCollection, "gmljp2:featureMember");
                    CPLAssert(psFeatureMemberOfGridCoverage);
                    CPLXMLNode* psGridCoverage = psFeatureMemberOfGridCoverage->psChild;
                    CPLAssert(psGridCoverage);
                    psGMLJP2Style = CPLCreateXMLNode( psGridCoverage, CXT_Element, "gmljp2:style"  );
                }

                // Add dummy namespace for validation purposes if needed
                if( strchr(psStyleRoot->pszValue, ':') == NULL &&
                    CPLGetXMLValue(psStyleRoot, "xmlns", NULL) == NULL )
                {
                    CPLSetXMLValue(psStyleRoot, "#xmlns", "http://undefined_namespace");
                }

                CPLAddXMLChild( psGMLJP2Style, CPLCloneXMLTree(psStyleRoot) );
            }
            CPLDestroyXMLNode(psStyle);
        }

        // Add extensions
        for( int i=0; i < (int)aoExtensions.size(); i++ )
        {
            CPLXMLNode* psExtension = CPLParseXMLFile(aoExtensions[i].osFile);
            if( psExtension == NULL )
                continue;

            CPLXMLNode* psExtensionRoot = GDALGMLJP2GetXMLRoot(psExtension);
            if( psExtensionRoot )
            {
                CPLXMLNode *psGMLJP2Extension;
                if( aoExtensions[i].bParentCoverageCollection )
                {
                    psGMLJP2Extension = CPLCreateXMLNode( psGMLJP2CoverageCollection, CXT_Element, "gmljp2:extension"  );
                }
                else
                {
                    CPLXMLNode* psFeatureMemberOfGridCoverage =
                        CPLGetXMLNode(psGMLJP2CoverageCollection, "gmljp2:featureMember");
                    CPLAssert(psFeatureMemberOfGridCoverage);
                    CPLXMLNode* psGridCoverage = psFeatureMemberOfGridCoverage->psChild;
                    CPLAssert(psGridCoverage);
                    psGMLJP2Extension = CPLCreateXMLNode( psGridCoverage, CXT_Element, "gmljp2:extension"  );
                }

                // Add dummy namespace for validation purposes if needed
                if( strchr(psExtensionRoot->pszValue, ':') == NULL &&
                    CPLGetXMLValue(psExtensionRoot, "xmlns", NULL) == NULL )
                {
                    CPLSetXMLValue(psExtensionRoot, "#xmlns", "http://undefined_namespace");
                }

                CPLAddXMLChild( psGMLJP2Extension, CPLCloneXMLTree(psExtensionRoot) );
            }
            CPLDestroyXMLNode(psExtension);
        }

        char* pszRoot = CPLSerializeXMLTree(psRoot);
        CPLDestroyXMLNode(psRoot);
        psRoot = NULL;
        osDoc = pszRoot;
        CPLFree(pszRoot);
        pszRoot = NULL;
    }

/* -------------------------------------------------------------------- */
/*      Setup the gml.data label.                                       */
/* -------------------------------------------------------------------- */
    std::vector<GDALJP2Box *> apoGMLBoxes;

    apoGMLBoxes.push_back(GDALJP2Box::CreateLblBox( "gml.data" ));

/* -------------------------------------------------------------------- */
/*      Setup gml.root-instance.                                        */
/* -------------------------------------------------------------------- */
    apoGMLBoxes.push_back(
        GDALJP2Box::CreateLabelledXMLAssoc( "gml.root-instance", osDoc ));

/* -------------------------------------------------------------------- */
/*      Add optional dictionary.                                        */
/* -------------------------------------------------------------------- */
    if( osDictBox.size() > 0 )
        apoGMLBoxes.push_back(
            GDALJP2Box::CreateLabelledXMLAssoc( "CRSDictionary.gml",
                                                osDictBox ) );

/* -------------------------------------------------------------------- */
/*      Additional user specified boxes.                                */
/* -------------------------------------------------------------------- */
    for( int i=0; i < (int)aoBoxes.size(); i++ )
    {
        GByte* pabyContent = NULL;
        if( VSIIngestFile( NULL, aoBoxes[i].osFile, &pabyContent, NULL, -1 ) )
        {
            CPLXMLNode* psNode = CPLParseXMLString((const char*)pabyContent);
            CPLFree(pabyContent);
            pabyContent = NULL;
            if( psNode )
            {
                CPLXMLNode* psRoot = GDALGMLJP2GetXMLRoot(psNode);
                if( psRoot )
                {
                    GDALGMLJP2PatchFeatureCollectionSubstitutionGroup(psRoot);
                    pabyContent = (GByte*) CPLSerializeXMLTree(psRoot);
                    apoGMLBoxes.push_back(
                        GDALJP2Box::CreateLabelledXMLAssoc( aoBoxes[i].osLabel,
                                                            (const char*)pabyContent ) );
                }
                CPLDestroyXMLNode (psNode);
            }
        }
        CPLFree(pabyContent);
    }

/* -------------------------------------------------------------------- */
/*      Bundle gml.data boxes into an association.                      */
/* -------------------------------------------------------------------- */
    GDALJP2Box *poGMLData = GDALJP2Box::CreateAsocBox( (int)apoGMLBoxes.size(),
                                                       &apoGMLBoxes[0]);

/* -------------------------------------------------------------------- */
/*      Cleanup working boxes.                                          */
/* -------------------------------------------------------------------- */
    for( int i=0; i < (int)apoGMLBoxes.size(); i++ )
        delete apoGMLBoxes[i];

    for( int i=0; i < (int)aosTmpFiles.size(); i++ )
    {
        VSIUnlink(aosTmpFiles[i]);
    }

    return poGMLData;
}

/************************************************************************/
/*                 CreateGDALMultiDomainMetadataXML()                   */
/************************************************************************/

CPLXMLNode* GDALJP2Metadata::CreateGDALMultiDomainMetadataXML(
                                       GDALDataset* poSrcDS,
                                       int bMainMDDomainOnly )
{
    GDALMultiDomainMetadata oLocalMDMD;
    char** papszSrcMD = CSLDuplicate(poSrcDS->GetMetadata());
    /* Remove useless metadata */
    papszSrcMD = CSLSetNameValue(papszSrcMD, GDALMD_AREA_OR_POINT, NULL);
    papszSrcMD = CSLSetNameValue(papszSrcMD, "TIFFTAG_RESOLUTIONUNIT", NULL);
    papszSrcMD = CSLSetNameValue(papszSrcMD, "TIFFTAG_XREpsMasterXMLNodeSOLUTION", NULL);
    papszSrcMD = CSLSetNameValue(papszSrcMD, "TIFFTAG_YRESOLUTION", NULL);
    papszSrcMD = CSLSetNameValue(papszSrcMD, "Corder", NULL); /* from JP2KAK */
    if( poSrcDS->GetDriver() != NULL &&
        EQUAL(poSrcDS->GetDriver()->GetDescription(), "JP2ECW") )
    {
        papszSrcMD = CSLSetNameValue(papszSrcMD, "COMPRESSION_RATE_TARGET", NULL);
        papszSrcMD = CSLSetNameValue(papszSrcMD, "COLORSPACE", NULL);
        papszSrcMD = CSLSetNameValue(papszSrcMD, "VERSION", NULL);
    }

    bool bHasMD = false;
    if( papszSrcMD && *papszSrcMD )
    {
        bHasMD = true;
        oLocalMDMD.SetMetadata(papszSrcMD);
    }
    CSLDestroy(papszSrcMD);

    if( !bMainMDDomainOnly )
    {
        char** papszMDList = poSrcDS->GetMetadataDomainList();
        for( char** papszMDListIter = papszMDList;
            papszMDListIter && *papszMDListIter; ++papszMDListIter )
        {
            if( !EQUAL(*papszMDListIter, "") &&
                !EQUAL(*papszMDListIter, "IMAGE_STRUCTURE") &&
                !EQUAL(*papszMDListIter, "JPEG2000") &&
                !STARTS_WITH_CI(*papszMDListIter, "xml:BOX_") &&
                !EQUAL(*papszMDListIter, "xml:gml.root-instance") &&
                !EQUAL(*papszMDListIter, "xml:XMP") &&
                !EQUAL(*papszMDListIter, "xml:IPR") )
            {
                papszSrcMD = poSrcDS->GetMetadata(*papszMDListIter);
                if( papszSrcMD && *papszSrcMD )
                {
                    bHasMD = true;
                    oLocalMDMD.SetMetadata(papszSrcMD, *papszMDListIter);
                }
            }
        }
        CSLDestroy(papszMDList);
    }

    CPLXMLNode* psMasterXMLNode = NULL;
    if( bHasMD )
    {
        CPLXMLNode* psXMLNode = oLocalMDMD.Serialize();
        psMasterXMLNode = CPLCreateXMLNode( NULL, CXT_Element,
                                                    "GDALMultiDomainMetadata" );
        psMasterXMLNode->psChild = psXMLNode;
    }
    return psMasterXMLNode;
}

/************************************************************************/
/*                CreateGDALMultiDomainMetadataXMLBox()                 */
/************************************************************************/

GDALJP2Box *GDALJP2Metadata::CreateGDALMultiDomainMetadataXMLBox(
                                       GDALDataset* poSrcDS,
                                       int bMainMDDomainOnly )
{
    CPLXMLNode* psMasterXMLNode = CreateGDALMultiDomainMetadataXML(
                                       poSrcDS, bMainMDDomainOnly );
    if( psMasterXMLNode == NULL )
        return NULL;
    char* pszXML = CPLSerializeXMLTree(psMasterXMLNode);
    CPLDestroyXMLNode(psMasterXMLNode);

    GDALJP2Box* poBox = new GDALJP2Box();
    poBox->SetType("xml ");
    poBox->SetWritableData(static_cast<int>(strlen(pszXML) + 1), (const GByte*)pszXML);
    CPLFree(pszXML);

    return poBox;
}

/************************************************************************/
/*                         WriteXMLBoxes()                              */
/************************************************************************/

GDALJP2Box** GDALJP2Metadata::CreateXMLBoxes( GDALDataset* poSrcDS,
                                              int* pnBoxes )
{
    GDALJP2Box** papoBoxes = NULL;
    *pnBoxes = 0;
    char** papszMDList = poSrcDS->GetMetadataDomainList();
    for( char** papszMDListIter = papszMDList;
        papszMDListIter && *papszMDListIter; ++papszMDListIter )
    {
        /* Write metadata that look like originating from JP2 XML boxes */
        /* as a standalone JP2 XML box */
        if( STARTS_WITH_CI(*papszMDListIter, "xml:BOX_") )
        {
            char** papszSrcMD = poSrcDS->GetMetadata(*papszMDListIter);
            if( papszSrcMD && *papszSrcMD )
            {
                GDALJP2Box* poBox = new GDALJP2Box();
                poBox->SetType("xml ");
                poBox->SetWritableData(static_cast<int>(strlen(*papszSrcMD) + 1),
                                       (const GByte*)*papszSrcMD);
                papoBoxes = (GDALJP2Box**)CPLRealloc(papoBoxes,
                                        sizeof(GDALJP2Box*) * (*pnBoxes + 1));
                papoBoxes[(*pnBoxes) ++] = poBox;
            }
        }
    }
    CSLDestroy(papszMDList);
    return papoBoxes;
}

/************************************************************************/
/*                          CreateXMPBox()                              */
/************************************************************************/

GDALJP2Box *GDALJP2Metadata::CreateXMPBox ( GDALDataset* poSrcDS )
{
    char** papszSrcMD = poSrcDS->GetMetadata("xml:XMP");
    GDALJP2Box* poBox = NULL;
    if( papszSrcMD && * papszSrcMD )
    {
        poBox = GDALJP2Box::CreateUUIDBox(xmp_uuid,
                                          static_cast<int>(strlen(*papszSrcMD) + 1),
                                          (const GByte*)*papszSrcMD);
    }
    return poBox;
}

/************************************************************************/
/*                          CreateIPRBox()                              */
/************************************************************************/

GDALJP2Box *GDALJP2Metadata::CreateIPRBox ( GDALDataset* poSrcDS )
{
    char** papszSrcMD = poSrcDS->GetMetadata("xml:IPR");
    GDALJP2Box* poBox = NULL;
    if( papszSrcMD && * papszSrcMD )
    {
        poBox = new GDALJP2Box();
        poBox->SetType("jp2i");
        poBox->SetWritableData(static_cast<int>(strlen(*papszSrcMD) + 1),
                                        (const GByte*)*papszSrcMD);
    }
    return poBox;
}

/************************************************************************/
/*                           IsUUID_MSI()                              */
/************************************************************************/

int GDALJP2Metadata::IsUUID_MSI(const GByte *abyUUID)
{
    return memcmp(abyUUID, msi_uuid2, 16) == 0;
}

/************************************************************************/
/*                           IsUUID_XMP()                               */
/************************************************************************/

int GDALJP2Metadata::IsUUID_XMP(const GByte *abyUUID)
{
    return memcmp(abyUUID, xmp_uuid, 16) == 0;
}
