diff --git a/autotest/gdrivers/data/wmts/WMTSCapabilities_THEMIS_NightIR_ControlledMosaics_100m_v2_oct2018.xml b/autotest/gdrivers/data/wmts/WMTSCapabilities_THEMIS_NightIR_ControlledMosaics_100m_v2_oct2018.xml new file mode 100644 index 000000000000..3dceeaa00d2b --- /dev/null +++ b/autotest/gdrivers/data/wmts/WMTSCapabilities_THEMIS_NightIR_ControlledMosaics_100m_v2_oct2018.xml @@ -0,0 +1,102 @@ + + + + + THEMIS_NightIR_ControlledMosaics_100m_v2_oct2018 + OGC WMTS + 1.0.0 + + + + + + + + + RESTful + + + + + + + + + + + + + + RESTful + + + + + + + + + + + + THEMIS_NightIR_ControlledMosaics_100m_v2_oct2018 + THEMIS_NightIR_ControlledMosaics_100m_v2_oct2018 + + -179.9999997 -65.0006576 + 179.9998849 65.0007535 + + + -179.9999997 -65.0006576 + 179.9998849 65.0007535 + + + + image/png + + default028mm + + + + + default + The tile matrix set that has scale values calculated based on the dpi defined by OGC specification (dpi assumes 0.28mm as the physical distance of a pixel). + default028mm + urn:ogc:def:crs:EPSG::104905 + 02.7922763629807472E+08-180.0 90.02562562.01.0 +11.3961381814903736E+08-180.0 90.02562564.02.0 +26.9806909074518681E+07-180.0 90.02562568.04.0 +33.4903454537259340E+07-180.0 90.025625616.08.0 +41.7451727268629670E+07-180.0 90.025625632.016.0 +58.7258636343148351E+06-180.0 90.025625664.032.0 +64.3629318171574175E+06-180.0 90.0256256128.064.0 +72.1814659085787088E+06-180.0 90.0256256256.0128.0 +81.0907329542893544E+06-180.0 90.0256256512.0256.0 +95.4536647714467719E+05-180.0 90.02562561024.0512.0 + + + + + + + diff --git a/autotest/gdrivers/wmts.py b/autotest/gdrivers/wmts.py index f0e7b985157b..6a6ea68a1a59 100755 --- a/autotest/gdrivers/wmts.py +++ b/autotest/gdrivers/wmts.py @@ -2002,3 +2002,32 @@ def test_wmts_force_opening_no_match(): drv = gdal.IdentifyDriverEx("data/byte.tif", allowed_drivers=["WMTS"]) assert drv is None + + +############################################################################### +# Test bug fix for https://github.com/OSGeo/gdal/issues/11387 + + +@pytest.mark.require_proj(9) +@gdaltest.enable_exceptions() +def test_wmts_read_esri_code_disguised_as_epsg_and_wrong_axis_order(): + + with gdaltest.error_handler(): + with gdal.Open( + "data/wmts/WMTSCapabilities_THEMIS_NightIR_ControlledMosaics_100m_v2_oct2018.xml" + ) as ds: + assert gdal.GetLastErrorMsg().startswith( + "Auto-correcting wrongly swapped TileMatrix.TopLeftCorner coordinates" + ) + assert ds.GetSpatialRef().GetAuthorityName(None) == "ESRI" + assert ds.GetSpatialRef().GetAuthorityCode(None) == "104905" + assert ds.GetGeoTransform() == pytest.approx( + ( + -180.0, + 0.0013717509172233527, + 0.0, + 65.00121128452162, + 0.0, + -0.0013717509172233527, + ) + ) diff --git a/autotest/osr/osr_epsg.py b/autotest/osr/osr_epsg.py index 0053cc91eec5..ce08219337dc 100755 --- a/autotest/osr/osr_epsg.py +++ b/autotest/osr/osr_epsg.py @@ -545,3 +545,31 @@ def test_osr_epsg_EPSGTreatsAsLatLong_for_CompoundCRS(): srs = osr.SpatialReference() srs.ImportFromEPSG(6697) assert srs.EPSGTreatsAsLatLong() == 1 + + +############################################################################### +# Test importing a ESRI code as a EPSG code + + +@pytest.mark.require_proj(9) +def test_osr_epsg_import_esri_code(): + + srs = osr.SpatialReference() + with gdal.quiet_errors(): + srs.ImportFromEPSG(104905) + + assert srs.GetAuthorityName(None) == "ESRI" + assert srs.GetAuthorityCode(None) == "104905" + + +############################################################################### +# Test importing a non-existent ESRI code presented as a EPSG code + + +def test_osr_epsg_import_invalid_code_that_might_have_been_esri(): + + srs = osr.SpatialReference() + with pytest.raises( + Exception, match="PROJ: proj_create_from_database: crs not found" + ): + srs.ImportFromEPSG(987654) diff --git a/frmts/http/httpdriver.cpp b/frmts/http/httpdriver.cpp index a7563c9063dc..3b15354e9543 100644 --- a/frmts/http/httpdriver.cpp +++ b/frmts/http/httpdriver.cpp @@ -11,6 +11,7 @@ * SPDX-License-Identifier: MIT ****************************************************************************/ +#include "cpl_error_internal.h" #include "cpl_string.h" #include "cpl_http.h" #include "gdal_frmts.h" @@ -133,11 +134,27 @@ static GDALDataset *HTTPOpen(GDALOpenInfo *poOpenInfo) /* Try opening this result as a gdaldataset. */ /* -------------------------------------------------------------------- */ /* suppress errors as not all drivers support /vsimem */ - CPLPushErrorHandler(CPLQuietErrorHandler); - GDALDataset *poDS = (GDALDataset *)GDALOpenEx( - osResultFilename, poOpenInfo->nOpenFlags & ~GDAL_OF_SHARED, - poOpenInfo->papszAllowedDrivers, poOpenInfo->papszOpenOptions, nullptr); - CPLPopErrorHandler(); + + GDALDataset *poDS; + std::vector aoErrors; + { + CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler); + CPLInstallErrorHandlerAccumulator(aoErrors); + poDS = GDALDataset::Open(osResultFilename, + poOpenInfo->nOpenFlags & ~GDAL_OF_SHARED, + poOpenInfo->papszAllowedDrivers, + poOpenInfo->papszOpenOptions, nullptr); + CPLUninstallErrorHandlerAccumulator(); + } + + // Re-emit silenced errors if open was successful + if (poDS) + { + for (const auto &oError : aoErrors) + { + CPLError(oError.type, oError.no, "%s", oError.msg.c_str()); + } + } // The JP2OpenJPEG driver may need to reopen the file, hence this special // behavior @@ -171,10 +188,10 @@ static GDALDataset *HTTPOpen(GDALOpenInfo *poOpenInfo) } else { - poDS = (GDALDataset *)GDALOpenEx( - osTempFilename, poOpenInfo->nOpenFlags & ~GDAL_OF_SHARED, - poOpenInfo->papszAllowedDrivers, poOpenInfo->papszOpenOptions, - nullptr); + poDS = GDALDataset::Open(osTempFilename, + poOpenInfo->nOpenFlags & ~GDAL_OF_SHARED, + poOpenInfo->papszAllowedDrivers, + poOpenInfo->papszOpenOptions, nullptr); if (VSIUnlink(osTempFilename) != 0 && poDS != nullptr) poDS->MarkSuppressOnClose(); /* VSIUnlink() may not work on windows */ diff --git a/frmts/wmts/wmtsdataset.cpp b/frmts/wmts/wmtsdataset.cpp index 09affe85072b..ab4d4eb893dc 100644 --- a/frmts/wmts/wmtsdataset.cpp +++ b/frmts/wmts/wmtsdataset.cpp @@ -158,7 +158,8 @@ class WMTSDataset final : public GDALPamDataset const char *pszOperation); static int ReadTMS(CPLXMLNode *psContents, const CPLString &osIdentifier, const CPLString &osMaxTileMatrixIdentifier, - int nMaxZoomLevel, WMTSTileMatrixSet &oTMS); + int nMaxZoomLevel, WMTSTileMatrixSet &oTMS, + bool &bHasWarnedAutoSwap); static int ReadTMLimits( CPLXMLNode *psTMSLimits, std::map &aoMapTileMatrixLimits); @@ -599,10 +600,9 @@ CPLString WMTSDataset::FixCRSName(const char *pszCRS) int WMTSDataset::ReadTMS(CPLXMLNode *psContents, const CPLString &osIdentifier, const CPLString &osMaxTileMatrixIdentifier, - int nMaxZoomLevel, WMTSTileMatrixSet &oTMS) + int nMaxZoomLevel, WMTSTileMatrixSet &oTMS, + bool &bHasWarnedAutoSwap) { - bool bHasWarnedAutoSwap = false; - for (CPLXMLNode *psIter = psContents->psChild; psIter != nullptr; psIter = psIter->psNext) { @@ -629,9 +629,10 @@ int WMTSDataset::ReadTMS(CPLXMLNode *psContents, const CPLString &osIdentifier, pszSupportedCRS); return FALSE; } - int bSwap = !STARTS_WITH_CI(pszSupportedCRS, "EPSG:") && - (oTMS.oSRS.EPSGTreatsAsLatLong() || - oTMS.oSRS.EPSGTreatsAsNorthingEasting()); + const bool bSwap = + !STARTS_WITH_CI(pszSupportedCRS, "EPSG:") && + (CPL_TO_BOOL(oTMS.oSRS.EPSGTreatsAsLatLong()) || + CPL_TO_BOOL(oTMS.oSRS.EPSGTreatsAsNorthingEasting())); CPLXMLNode *psBB = CPLGetXMLNode(psIter, "BoundingBox"); oTMS.bBoundingBoxValid = false; if (psBB != nullptr) @@ -740,16 +741,21 @@ int WMTSDataset::ReadTMS(CPLXMLNode *psContents, const CPLString &osIdentifier, } // Hack for http://osm.geobretagne.fr/gwc01/service/wmts?request=getcapabilities - if (STARTS_WITH_CI(l_pszIdentifier, "EPSG:4326:") && - oTM.dfTLY == -180.0) + // or https://trek.nasa.gov/tiles/Mars/EQ/THEMIS_NightIR_ControlledMosaics_100m_v2_oct2018/1.0.0/WMTSCapabilities.xml + if (oTM.dfTLY == -180.0 && + (STARTS_WITH_CI(l_pszIdentifier, "EPSG:4326:") || + (oTMS.oSRS.IsGeographic() && oTM.dfTLX == 90))) { if (!bHasWarnedAutoSwap) { bHasWarnedAutoSwap = true; CPLError(CE_Warning, CPLE_AppDefined, "Auto-correcting wrongly swapped " - "TileMatrix.TopLeftCorner coordinates. This " - "should be reported to the server administrator."); + "TileMatrix.TopLeftCorner coordinates. " + "They should be in latitude, longitude order " + "but are presented in longitude, latitude order. " + "This should be reported to the server " + "administrator."); } std::swap(oTM.dfTLX, oTM.dfTLY); } @@ -1246,6 +1252,8 @@ GDALDataset *WMTSDataset::Open(GDALOpenInfo *poOpenInfo) std::map aoMapBoundingBox; std::map aoMapTileMatrixLimits; std::map aoMapDimensions; + bool bHasWarnedAutoSwap = false; + bool bHasWarnedAutoSwapBoundingBox = false; // Collect TileMatrixSet identifiers std::set oSetTMSIdentifiers; @@ -1431,7 +1439,8 @@ GDALDataset *WMTSDataset::Open(GDALOpenInfo *poOpenInfo) // 13-082_WMTS_Simple_Profile/schemas/wmts/1.0/profiles/WMTSSimple/examples/wmtsGetCapabilities_response_OSM.xml WMTSTileMatrixSet oTMS; if (ReadTMS(psContents, osSingleTileMatrixSet, - CPLString(), -1, oTMS)) + CPLString(), -1, oTMS, + bHasWarnedAutoSwap)) { osCRS = oTMS.osSRS; } @@ -1448,9 +1457,10 @@ GDALDataset *WMTSDataset::Open(GDALOpenInfo *poOpenInfo) !osUpperCorner.empty() && oSRS.SetFromUserInput(FixCRSName(osCRS)) == OGRERR_NONE) { - int bSwap = !STARTS_WITH_CI(osCRS, "EPSG:") && - (oSRS.EPSGTreatsAsLatLong() || - oSRS.EPSGTreatsAsNorthingEasting()); + const bool bSwap = + !STARTS_WITH_CI(osCRS, "EPSG:") && + (CPL_TO_BOOL(oSRS.EPSGTreatsAsLatLong()) || + CPL_TO_BOOL(oSRS.EPSGTreatsAsNorthingEasting())); char **papszLC = CSLTokenizeString(osLowerCorner); char **papszUC = CSLTokenizeString(osUpperCorner); if (CSLCount(papszLC) == 2 && CSLCount(papszUC) == 2) @@ -1460,6 +1470,30 @@ GDALDataset *WMTSDataset::Open(GDALOpenInfo *poOpenInfo) sEnvelope.MinY = CPLAtof(papszLC[(bSwap) ? 0 : 1]); sEnvelope.MaxX = CPLAtof(papszUC[(bSwap) ? 1 : 0]); sEnvelope.MaxY = CPLAtof(papszUC[(bSwap) ? 0 : 1]); + + if (bSwap && oSRS.IsGeographic() && + (std::fabs(sEnvelope.MinY) > 90 || + std::fabs(sEnvelope.MaxY) > 90)) + { + if (!bHasWarnedAutoSwapBoundingBox) + { + bHasWarnedAutoSwapBoundingBox = true; + CPLError( + CE_Warning, CPLE_AppDefined, + "Auto-correcting wrongly swapped " + "ows:%s coordinates. " + "They should be in latitude, longitude " + "order " + "but are presented in longitude, latitude " + "order. " + "This should be reported to the server " + "administrator.", + psSubIter->pszValue); + } + std::swap(sEnvelope.MinX, sEnvelope.MinY); + std::swap(sEnvelope.MaxX, sEnvelope.MaxY); + } + aoMapBoundingBox[osCRS] = sEnvelope; } CSLDestroy(papszLC); @@ -1568,7 +1602,7 @@ GDALDataset *WMTSDataset::Open(GDALOpenInfo *poOpenInfo) WMTSTileMatrixSet oTMS; if (!ReadTMS(psContents, osSelectTMS, osMaxTileMatrixIdentifier, - nUserMaxZoomLevel, oTMS)) + nUserMaxZoomLevel, oTMS, bHasWarnedAutoSwap)) { CPLDestroyXMLNode(psXML); delete poDS; @@ -2303,7 +2337,10 @@ GDALDataset *WMTSDataset::Open(GDALOpenInfo *poOpenInfo) nTileY, (bExtendBeyondDateLine) ? nSizeX1 : nSizeX, nSizeY, oTM.nTileWidth, oTM.nTileHeight, nBands, GDALGetDataTypeName(eDataType), osOtherXML.c_str())); - GDALDataset *poWMSDS = (GDALDataset *)GDALOpenEx( + const auto eLastErrorType = CPLGetLastErrorType(); + const auto eLastErrorNum = CPLGetLastErrorNo(); + const std::string osLastErrorMsg = CPLGetLastErrorMsg(); + GDALDataset *poWMSDS = GDALDataset::Open( osStr, GDAL_OF_RASTER | GDAL_OF_SHARED | GDAL_OF_VERBOSE_ERROR, nullptr, nullptr, nullptr); if (poWMSDS == nullptr) @@ -2312,6 +2349,11 @@ GDALDataset *WMTSDataset::Open(GDALOpenInfo *poOpenInfo) delete poDS; return nullptr; } + // Restore error state to what it was prior to WMS dataset opening + // if WMS dataset opening did not cause any new error to be emitted + if (CPLGetLastErrorType() == CE_None) + CPLErrorSetState(eLastErrorType, eLastErrorNum, + osLastErrorMsg.c_str()); VRTDatasetH hVRTDS = VRTCreate(nRasterXSize, nRasterYSize); for (int iBand = 1; iBand <= nBands; iBand++) diff --git a/ogr/ogrspatialreference.cpp b/ogr/ogrspatialreference.cpp index ed44578b07a4..dd0c683ffc14 100644 --- a/ogr/ogrspatialreference.cpp +++ b/ogr/ogrspatialreference.cpp @@ -4569,6 +4569,14 @@ OGRErr OGRSpatialReference::importFromURNPart(const char *pszAuthority, OGRErr OGRSpatialReference::importFromURN(const char *pszURN) { + constexpr const char *EPSG_URN_CRS_PREFIX = "urn:ogc:def:crs:EPSG::"; + if (STARTS_WITH(pszURN, EPSG_URN_CRS_PREFIX) && + CPLGetValueType(pszURN + strlen(EPSG_URN_CRS_PREFIX)) == + CPL_VALUE_INTEGER) + { + return importFromEPSG(atoi(pszURN + strlen(EPSG_URN_CRS_PREFIX))); + } + TAKE_OPTIONAL_LOCK(); #if PROJ_AT_LEAST_VERSION(8, 1, 0) @@ -11923,12 +11931,56 @@ OGRErr OGRSpatialReference::importFromEPSGA(int nCode) CPLString osCode; osCode.Printf("%d", nCode); - auto obj = - proj_create_from_database(d->getPROJContext(), "EPSG", osCode.c_str(), - PJ_CATEGORY_CRS, true, nullptr); - if (!obj) + PJ *obj; + if (nCode <= 100000) { - return OGRERR_FAILURE; + obj = proj_create_from_database(d->getPROJContext(), "EPSG", + osCode.c_str(), PJ_CATEGORY_CRS, true, + nullptr); + if (!obj) + { + return OGRERR_FAILURE; + } + } + else + { + // Likely to be an ESRI CRS... + CPLErr eLastErrorType = CE_None; + CPLErrorNum eLastErrorNum = CPLE_None; + std::string osLastErrorMsg; + bool bIsESRI = false; + { + CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler); + CPLErrorReset(); + obj = proj_create_from_database(d->getPROJContext(), "EPSG", + osCode.c_str(), PJ_CATEGORY_CRS, + true, nullptr); + if (!obj) + { + eLastErrorType = CPLGetLastErrorType(); + eLastErrorNum = CPLGetLastErrorNo(); + osLastErrorMsg = CPLGetLastErrorMsg(); + obj = proj_create_from_database(d->getPROJContext(), "ESRI", + osCode.c_str(), PJ_CATEGORY_CRS, + true, nullptr); + if (obj) + bIsESRI = true; + } + } + if (!obj) + { + if (eLastErrorType != CE_None) + CPLError(eLastErrorType, eLastErrorNum, "%s", + osLastErrorMsg.c_str()); + return OGRERR_FAILURE; + } + if (bIsESRI) + { + CPLError(CE_Warning, CPLE_AppDefined, + "EPSG:%d is not a valid CRS code, but ESRI:%d is. " + "Assuming ESRI:%d was meant", + nCode, nCode, nCode); + } } if (bUseNonDeprecated && proj_is_deprecated(obj))