/****************************************************************************** * * Project: WCS Client Driver * Purpose: Implementation of Dataset class for WCS 1.0. * Author: Frank Warmerdam, warmerdam@pobox.com * ****************************************************************************** * Copyright (c) 2006, Frank Warmerdam * Copyright (c) 2008-2013, Even Rouault * Copyright (c) 2017, Ari Jolma * Copyright (c) 2017, Finnish Environment Institute * * 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 "cpl_http.h" #include "gmlutils.h" #include "gdal_frmts.h" #include "gdal_pam.h" #include "ogr_spatialref.h" #include "gmlcoverage.h" #include #include "wcsdataset.h" #include "wcsutils.h" using namespace WCSUtils; /************************************************************************/ /* GetExtent() */ /* */ /************************************************************************/ std::vector WCSDataset100::GetExtent(int nXOff, int nYOff, int nXSize, int nYSize, CPL_UNUSED int, CPL_UNUSED int) { std::vector extent; // WCS 1.0 extents are the outer edges of outer pixels. extent.push_back(adfGeoTransform[0] + (nXOff)*adfGeoTransform[1]); extent.push_back(adfGeoTransform[3] + (nYOff + nYSize) * adfGeoTransform[5]); extent.push_back(adfGeoTransform[0] + (nXOff + nXSize) * adfGeoTransform[1]); extent.push_back(adfGeoTransform[3] + (nYOff)*adfGeoTransform[5]); return extent; } /************************************************************************/ /* GetCoverageRequest() */ /* */ /************************************************************************/ CPLString WCSDataset100::GetCoverageRequest(CPL_UNUSED bool scaled, int nBufXSize, int nBufYSize, const std::vector &extent, CPLString osBandList) { /* -------------------------------------------------------------------- */ /* URL encode strings that could have questionable characters. */ /* -------------------------------------------------------------------- */ CPLString osCoverage = CPLGetXMLValue(psService, "CoverageName", ""); char *pszEncoded = CPLEscapeString(osCoverage, -1, CPLES_URL); osCoverage = pszEncoded; CPLFree(pszEncoded); CPLString osFormat = CPLGetXMLValue(psService, "PreferredFormat", ""); pszEncoded = CPLEscapeString(osFormat, -1, CPLES_URL); osFormat = pszEncoded; CPLFree(pszEncoded); /* -------------------------------------------------------------------- */ /* Do we have a time we want to use? */ /* -------------------------------------------------------------------- */ CPLString osTime; osTime = CSLFetchNameValueDef(papszSDSModifiers, "time", osDefaultTime); /* -------------------------------------------------------------------- */ /* Construct a "simple" GetCoverage request (WCS 1.0). */ /* -------------------------------------------------------------------- */ CPLString request = CPLGetXMLValue(psService, "ServiceURL", ""); request = CPLURLAddKVP(request, "SERVICE", "WCS"); request = CPLURLAddKVP(request, "REQUEST", "GetCoverage"); request = CPLURLAddKVP(request, "VERSION", CPLGetXMLValue(psService, "Version", "1.0.0")); request = CPLURLAddKVP(request, "COVERAGE", osCoverage.c_str()); request = CPLURLAddKVP(request, "FORMAT", osFormat.c_str()); request += CPLString().Printf( "&BBOX=%.15g,%.15g,%.15g,%.15g&WIDTH=%d&HEIGHT=%d&CRS=%s", extent[0], extent[1], extent[2], extent[3], nBufXSize, nBufYSize, osCRS.c_str()); CPLString extra = CPLGetXMLValue(psService, "Parameters", ""); if (extra != "") { std::vector pairs = Split(extra, "&"); for (unsigned int i = 0; i < pairs.size(); ++i) { std::vector pair = Split(pairs[i], "="); request = CPLURLAddKVP(request, pair[0], pair[1]); } } extra = CPLGetXMLValue(psService, "GetCoverageExtra", ""); if (extra != "") { std::vector pairs = Split(extra, "&"); for (unsigned int i = 0; i < pairs.size(); ++i) { std::vector pair = Split(pairs[i], "="); request = CPLURLAddKVP(request, pair[0], pair[1]); } } CPLString interpolation = CPLGetXMLValue(psService, "Interpolation", ""); if (interpolation == "") { // old undocumented key for interpolation in service interpolation = CPLGetXMLValue(psService, "Resample", ""); } if (interpolation != "") { request += "&INTERPOLATION=" + interpolation; } if (osTime != "") { request += "&time="; request += osTime; } if (osBandList != "") { request += CPLString().Printf("&%s=%s", osBandIdentifier.c_str(), osBandList.c_str()); } return request; } /************************************************************************/ /* DescribeCoverageRequest() */ /* */ /************************************************************************/ CPLString WCSDataset100::DescribeCoverageRequest() { CPLString request = CPLGetXMLValue(psService, "ServiceURL", ""); request = CPLURLAddKVP(request, "SERVICE", "WCS"); request = CPLURLAddKVP(request, "REQUEST", "DescribeCoverage"); request = CPLURLAddKVP(request, "VERSION", CPLGetXMLValue(psService, "Version", "1.0.0")); request = CPLURLAddKVP(request, "COVERAGE", CPLGetXMLValue(psService, "CoverageName", "")); CPLString extra = CPLGetXMLValue(psService, "Parameters", ""); if (extra != "") { std::vector pairs = Split(extra, "&"); for (unsigned int i = 0; i < pairs.size(); ++i) { std::vector pair = Split(pairs[i], "="); request = CPLURLAddKVP(request, pair[0], pair[1]); } } extra = CPLGetXMLValue(psService, "DescribeCoverageExtra", ""); if (extra != "") { std::vector pairs = Split(extra, "&"); for (unsigned int i = 0; i < pairs.size(); ++i) { std::vector pair = Split(pairs[i], "="); request = CPLURLAddKVP(request, pair[0], pair[1]); } } return request; } /************************************************************************/ /* CoverageOffering() */ /* */ /************************************************************************/ CPLXMLNode *WCSDataset100::CoverageOffering(CPLXMLNode *psDC) { return CPLGetXMLNode(psDC, "=CoverageDescription.CoverageOffering"); } /************************************************************************/ /* ExtractGridInfo() */ /* */ /* Collect info about grid from describe coverage for WCS 1.0.0 */ /* and above. */ /************************************************************************/ bool WCSDataset100::ExtractGridInfo() { CPLXMLNode *psCO = CPLGetXMLNode(psService, "CoverageOffering"); if (psCO == nullptr) return FALSE; /* -------------------------------------------------------------------- */ /* We need to strip off name spaces so it is easier to */ /* searchfor plain gml names. */ /* -------------------------------------------------------------------- */ CPLStripXMLNamespace(psCO, nullptr, TRUE); /* -------------------------------------------------------------------- */ /* Verify we have a Rectified Grid. */ /* -------------------------------------------------------------------- */ CPLXMLNode *psRG = CPLGetXMLNode(psCO, "domainSet.spatialDomain.RectifiedGrid"); if (psRG == nullptr) { CPLError(CE_Failure, CPLE_AppDefined, "Unable to find RectifiedGrid in CoverageOffering,\n" "unable to process WCS Coverage."); return FALSE; } /* -------------------------------------------------------------------- */ /* Extract size, geotransform and coordinate system. */ /* Projection is, if it is, from Point.srsName */ /* -------------------------------------------------------------------- */ char *pszProjection = nullptr; if (WCSParseGMLCoverage(psRG, &nRasterXSize, &nRasterYSize, adfGeoTransform, &pszProjection) != CE_None) { CPLFree(pszProjection); return FALSE; } if (pszProjection) m_oSRS.SetFromUserInput( pszProjection, OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get()); CPLFree(pszProjection); // MapServer have origin at pixel boundary if (CPLGetXMLBoolean(psService, "OriginAtBoundary")) { adfGeoTransform[0] += adfGeoTransform[1] * 0.5; adfGeoTransform[0] += adfGeoTransform[2] * 0.5; adfGeoTransform[3] += adfGeoTransform[4] * 0.5; adfGeoTransform[3] += adfGeoTransform[5] * 0.5; } /* -------------------------------------------------------------------- */ /* Fallback to nativeCRSs declaration. */ /* -------------------------------------------------------------------- */ const char *pszNativeCRSs = CPLGetXMLValue(psCO, "supportedCRSs.nativeCRSs", nullptr); if (pszNativeCRSs == nullptr) pszNativeCRSs = CPLGetXMLValue(psCO, "supportedCRSs.requestResponseCRSs", nullptr); if (pszNativeCRSs == nullptr) pszNativeCRSs = CPLGetXMLValue(psCO, "supportedCRSs.requestCRSs", nullptr); if (pszNativeCRSs == nullptr) pszNativeCRSs = CPLGetXMLValue(psCO, "supportedCRSs.responseCRSs", nullptr); if (pszNativeCRSs != nullptr && m_oSRS.IsEmpty()) { if (m_oSRS.SetFromUserInput( pszNativeCRSs, OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get()) == OGRERR_NONE) { CPLDebug("WCS", " element contents not parsable:\n%s", pszNativeCRSs); } } // We should try to use the services name for the CRS if possible. if (pszNativeCRSs != nullptr && (STARTS_WITH_CI(pszNativeCRSs, "EPSG:") || STARTS_WITH_CI(pszNativeCRSs, "AUTO:") || STARTS_WITH_CI(pszNativeCRSs, "Image ") || STARTS_WITH_CI(pszNativeCRSs, "Engineering ") || STARTS_WITH_CI(pszNativeCRSs, "OGC:"))) { osCRS = pszNativeCRSs; size_t nDivider = osCRS.find(" "); if (nDivider != std::string::npos) osCRS.resize(nDivider - 1); } /* -------------------------------------------------------------------- */ /* Do we have a coordinate system override? */ /* -------------------------------------------------------------------- */ const char *pszProjOverride = CPLGetXMLValue(psService, "SRS", nullptr); if (pszProjOverride) { if (m_oSRS.SetFromUserInput( pszProjOverride, OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get()) != OGRERR_NONE) { CPLError(CE_Failure, CPLE_AppDefined, " element contents not parsable:\n%s", pszProjOverride); return FALSE; } if (STARTS_WITH_CI(pszProjOverride, "EPSG:") || STARTS_WITH_CI(pszProjOverride, "AUTO:") || STARTS_WITH_CI(pszProjOverride, "OGC:") || STARTS_WITH_CI(pszProjOverride, "Image ") || STARTS_WITH_CI(pszProjOverride, "Engineering ")) osCRS = pszProjOverride; } /* -------------------------------------------------------------------- */ /* Build CRS name to use. */ /* -------------------------------------------------------------------- */ if (!m_oSRS.IsEmpty() && osCRS == "") { const char *pszAuth = m_oSRS.GetAuthorityName(nullptr); if (pszAuth != nullptr && EQUAL(pszAuth, "EPSG")) { pszAuth = m_oSRS.GetAuthorityCode(nullptr); if (pszAuth) { osCRS = "EPSG:"; osCRS += pszAuth; } else { CPLError(CE_Failure, CPLE_AppDefined, "Unable to define CRS to use."); return FALSE; } } } /* -------------------------------------------------------------------- */ /* Pick a format type if we don't already have one selected. */ /* */ /* We will prefer anything that sounds like TIFF, otherwise */ /* falling back to the first supported format. Should we */ /* consider preferring the nativeFormat if available? */ /* -------------------------------------------------------------------- */ if (CPLGetXMLValue(psService, "PreferredFormat", nullptr) == nullptr) { CPLXMLNode *psSF = CPLGetXMLNode(psCO, "supportedFormats"); CPLXMLNode *psNode; char **papszFormatList = nullptr; CPLString osPreferredFormat; int iFormat; if (psSF == nullptr) { CPLError( CE_Failure, CPLE_AppDefined, "No tag in service definition file, and no\n" " in coverageOffering."); return FALSE; } for (psNode = psSF->psChild; psNode != nullptr; psNode = psNode->psNext) { if (psNode->eType == CXT_Element && EQUAL(psNode->pszValue, "formats") && psNode->psChild != nullptr && psNode->psChild->eType == CXT_Text) { // This check is looking for deprecated WCS 1.0 capabilities // with multiple formats space delimited in a single // element per GDAL ticket 1748 (done by MapServer 4.10 and // earlier for instance). if (papszFormatList == nullptr && psNode->psNext == nullptr && strstr(psNode->psChild->pszValue, " ") != nullptr && strstr(psNode->psChild->pszValue, ";") == nullptr) { char **papszSubList = CSLTokenizeString(psNode->psChild->pszValue); papszFormatList = CSLInsertStrings(papszFormatList, -1, papszSubList); CSLDestroy(papszSubList); } else { papszFormatList = CSLAddString(papszFormatList, psNode->psChild->pszValue); } } } for (iFormat = 0; papszFormatList != nullptr && papszFormatList[iFormat] != nullptr; iFormat++) { if (osPreferredFormat.empty()) osPreferredFormat = papszFormatList[iFormat]; if (strstr(papszFormatList[iFormat], "tiff") != nullptr || strstr(papszFormatList[iFormat], "TIFF") != nullptr || strstr(papszFormatList[iFormat], "Tiff") != nullptr) { osPreferredFormat = papszFormatList[iFormat]; break; } } CSLDestroy(papszFormatList); if (!osPreferredFormat.empty()) { bServiceDirty = true; CPLCreateXMLElementAndValue(psService, "PreferredFormat", osPreferredFormat); } } /* -------------------------------------------------------------------- */ /* Try to identify a nodata value. For now we only support the */ /* singleValue mechanism. */ /* -------------------------------------------------------------------- */ if (CPLGetXMLValue(psService, "NoDataValue", nullptr) == nullptr) { const char *pszSV = CPLGetXMLValue( psCO, "rangeSet.RangeSet.nullValues.singleValue", nullptr); if (pszSV != nullptr && (CPLAtof(pszSV) != 0.0 || *pszSV == DIGIT_ZERO)) { bServiceDirty = true; CPLCreateXMLElementAndValue(psService, "NoDataValue", pszSV); } } /* -------------------------------------------------------------------- */ /* Do we have a Band range type. For now we look for a fairly */ /* specific configuration. The rangeset my have one axis named */ /* "Band", with a set of ascending numerical values. */ /* -------------------------------------------------------------------- */ osBandIdentifier = CPLGetXMLValue(psService, "BandIdentifier", ""); CPLXMLNode *psAD = CPLGetXMLNode( psService, "CoverageOffering.rangeSet.RangeSet.axisDescription.AxisDescription"); CPLXMLNode *psValues; if (osBandIdentifier.empty() && psAD != nullptr && (EQUAL(CPLGetXMLValue(psAD, "name", ""), "Band") || EQUAL(CPLGetXMLValue(psAD, "name", ""), "Bands")) && ((psValues = CPLGetXMLNode(psAD, "values")) != nullptr)) { CPLXMLNode *psSV; int iBand; osBandIdentifier = CPLGetXMLValue(psAD, "name", ""); for (psSV = psValues->psChild, iBand = 1; psSV != nullptr; psSV = psSV->psNext, iBand++) { if (psSV->eType != CXT_Element || !EQUAL(psSV->pszValue, "singleValue") || psSV->psChild == nullptr || psSV->psChild->eType != CXT_Text || atoi(psSV->psChild->pszValue) != iBand) { osBandIdentifier = ""; break; } } if (!osBandIdentifier.empty()) { bServiceDirty = true; CPLSetXMLValue(psService, "BandIdentifier", osBandIdentifier); } } /* -------------------------------------------------------------------- */ /* Do we have a temporal domain? If so, try to identify a */ /* default time value. */ /* -------------------------------------------------------------------- */ osDefaultTime = CPLGetXMLValue(psService, "DefaultTime", ""); CPLXMLNode *psTD = CPLGetXMLNode(psService, "CoverageOffering.domainSet.temporalDomain"); CPLString osServiceURL = CPLGetXMLValue(psService, "ServiceURL", ""); CPLString osCoverageExtra = CPLGetXMLValue(psService, "GetCoverageExtra", ""); if (psTD != nullptr) { CPLXMLNode *psTime; // collect all the allowed time positions. for (psTime = psTD->psChild; psTime != nullptr; psTime = psTime->psNext) { if (psTime->eType == CXT_Element && EQUAL(psTime->pszValue, "timePosition") && psTime->psChild != nullptr && psTime->psChild->eType == CXT_Text) aosTimePositions.push_back(psTime->psChild->pszValue); } // we will default to the last - likely the most recent - entry. if (!aosTimePositions.empty() && osDefaultTime.empty() && osServiceURL.ifind("time=") == std::string::npos && osCoverageExtra.ifind("time=") == std::string::npos) { osDefaultTime = aosTimePositions.back(); bServiceDirty = true; CPLCreateXMLElementAndValue(psService, "DefaultTime", osDefaultTime); } } return true; } /************************************************************************/ /* ParseCapabilities() */ /************************************************************************/ CPLErr WCSDataset100::ParseCapabilities(CPLXMLNode *Capabilities, CPL_UNUSED CPLString url) { CPLStripXMLNamespace(Capabilities, nullptr, TRUE); if (strcmp(Capabilities->pszValue, "WCS_Capabilities") != 0) { CPLError(CE_Failure, CPLE_AppDefined, "Error in capabilities document.\n"); return CE_Failure; } char **metadata = nullptr; CPLString path = "WCS_GLOBAL#"; CPLString key = path + "version"; metadata = CSLSetNameValue(metadata, key, Version()); for (CPLXMLNode *node = Capabilities->psChild; node != nullptr; node = node->psNext) { const char *attr = node->pszValue; if (node->eType == CXT_Attribute && EQUAL(attr, "updateSequence")) { key = path + "updateSequence"; CPLString value = CPLGetXMLValue(node, nullptr, ""); metadata = CSLSetNameValue(metadata, key, value); } } // identification metadata CPLString path2 = path; std::vector keys2; keys2.push_back("description"); keys2.push_back("name"); keys2.push_back("label"); keys2.push_back("fees"); keys2.push_back("accessConstraints"); CPLXMLNode *service = AddSimpleMetaData(&metadata, Capabilities, path2, "Service", keys2); if (service) { CPLString path3 = path2; std::vector keys3; keys3.push_back("individualName"); keys3.push_back("organisationName"); keys3.push_back("positionName"); CPLString kw = GetKeywords(service, "keywords", "keyword"); if (kw != "") { CPLString name = path + "keywords"; metadata = CSLSetNameValue(metadata, name, kw); } CPLXMLNode *party = AddSimpleMetaData(&metadata, service, path3, "responsibleParty", keys3); CPLXMLNode *info = CPLGetXMLNode(party, "contactInfo"); if (party && info) { CPLString path4 = path3 + "contactInfo."; std::vector keys4; keys4.push_back("deliveryPoint"); keys4.push_back("city"); keys4.push_back("administrativeArea"); keys4.push_back("postalCode"); keys4.push_back("country"); keys4.push_back("electronicMailAddress"); CPLString path5 = path4; std::vector keys5; keys5.push_back("voice"); keys5.push_back("facsimile"); AddSimpleMetaData(&metadata, info, path4, "address", keys4); AddSimpleMetaData(&metadata, info, path5, "phone", keys5); } } // provider metadata // operations metadata CPLString DescribeCoverageURL; DescribeCoverageURL = CPLGetXMLValue( CPLGetXMLNode( CPLGetXMLNode( CPLSearchXMLNode( CPLSearchXMLNode(Capabilities, "DescribeCoverage"), "Get"), "OnlineResource"), "href"), nullptr, ""); // if DescribeCoverageURL looks wrong (i.e. has localhost) should we change // it? this->SetMetadata(metadata, ""); CSLDestroy(metadata); metadata = nullptr; if (CPLXMLNode *contents = CPLGetXMLNode(Capabilities, "ContentMetadata")) { int index = 1; for (CPLXMLNode *summary = contents->psChild; summary != nullptr; summary = summary->psNext) { if (summary->eType != CXT_Element || !EQUAL(summary->pszValue, "CoverageOfferingBrief")) { continue; } CPLString path3; path3.Printf("SUBDATASET_%d_", index); index += 1; // the name and description of the subdataset: // GDAL Data Model: // The value of the _NAME is a string that can be passed to // GDALOpen() to access the file. CPLXMLNode *node = CPLGetXMLNode(summary, "name"); if (node) { CPLString key2 = path3 + "NAME"; CPLString name = CPLGetXMLValue(node, nullptr, ""); CPLString value = DescribeCoverageURL; value = CPLURLAddKVP(value, "VERSION", this->Version()); value = CPLURLAddKVP(value, "COVERAGE", name); metadata = CSLSetNameValue(metadata, key2, value); } else { CSLDestroy(metadata); CPLError(CE_Failure, CPLE_AppDefined, "Error in capabilities document.\n"); return CE_Failure; } node = CPLGetXMLNode(summary, "label"); if (node) { CPLString key2 = path3 + "DESC"; metadata = CSLSetNameValue(metadata, key2, CPLGetXMLValue(node, nullptr, "")); } else { CSLDestroy(metadata); CPLError(CE_Failure, CPLE_AppDefined, "Error in capabilities document.\n"); return CE_Failure; } // todo: compose global bounding box from lonLatEnvelope // further subdataset (coverage) parameters are parsed in // ParseCoverageCapabilities } } this->SetMetadata(metadata, "SUBDATASETS"); CSLDestroy(metadata); return CE_None; } void WCSDataset100::ParseCoverageCapabilities(CPLXMLNode *capabilities, const CPLString &coverage, CPLXMLNode *metadata) { CPLStripXMLNamespace(capabilities, nullptr, TRUE); if (CPLXMLNode *contents = CPLGetXMLNode(capabilities, "ContentMetadata")) { for (CPLXMLNode *summary = contents->psChild; summary != nullptr; summary = summary->psNext) { if (summary->eType != CXT_Element || !EQUAL(summary->pszValue, "CoverageOfferingBrief")) { continue; } CPLXMLNode *node = CPLGetXMLNode(summary, "name"); if (node) { CPLString name = CPLGetXMLValue(node, nullptr, ""); if (name != coverage) { continue; } } XMLCopyMetadata(summary, metadata, "label"); XMLCopyMetadata(summary, metadata, "description"); CPLString kw = GetKeywords(summary, "keywords", "keyword"); CPLAddXMLAttributeAndValue( CPLCreateXMLElementAndValue(metadata, "MDI", kw), "key", "keywords"); // skip metadataLink } } }