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))