From bf165337efe69d078db2707145833a5f2189e02e Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Fri, 17 Nov 2023 12:38:08 +0100 Subject: [PATCH] gdalwarp: add a heuristic to clamp northings when projecting from geographic to Mercator (typically EPSG:3857) (fixes #8730) --- apps/gdalwarp_lib.cpp | 69 +++++++++++++++++++++++++ autotest/utilities/test_gdalwarp_lib.py | 35 +++++++++++++ 2 files changed, 104 insertions(+) diff --git a/apps/gdalwarp_lib.cpp b/apps/gdalwarp_lib.cpp index e49c35d91e14..9f8480661fee 100644 --- a/apps/gdalwarp_lib.cpp +++ b/apps/gdalwarp_lib.cpp @@ -3426,6 +3426,75 @@ static GDALDatasetH GDALWarpCreateOutput( if (EQUAL(pszFormat, "VRT")) bVRT = true; + // Special case for geographic to Mercator (typically EPSG:4326 to EPSG:3857) + // where latitudes close to 90 go to infinity + // We clamp latitudes between ~ -85 and ~ 85 degrees. + const char *pszDstSRS = CSLFetchNameValue(papszTO, "DST_SRS"); + if (nSrcCount == 1 && pszDstSRS && psOptions->dfMinX == 0.0 && + psOptions->dfMinY == 0.0 && psOptions->dfMaxX == 0.0 && + psOptions->dfMaxY == 0.0) + { + auto hSrcDS = pahSrcDS[0]; + const auto osSrcSRS = GetSrcDSProjection(pahSrcDS[0], papszTO); + OGRSpatialReference oSrcSRS; + oSrcSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); + oSrcSRS.SetFromUserInput(osSrcSRS.c_str()); + OGRSpatialReference oDstSRS; + oDstSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); + oDstSRS.SetFromUserInput(pszDstSRS); + const char *pszProjection = oDstSRS.GetAttrValue("PROJECTION"); + const char *pszMethod = CSLFetchNameValue(papszTO, "METHOD"); + double adfSrcGT[6]; + // This MAX_LAT values is equivalent to the semi_major_axis * PI + // easting/northing value only for EPSG:3857, but it is also quite + // reasonable for other Mercator projections + constexpr double MAX_LAT = 85.0511287798066; + constexpr double EPS = 1e-3; + const auto GetMinLon = [&adfSrcGT]() { return adfSrcGT[0]; }; + const auto GetMaxLon = [&adfSrcGT, hSrcDS]() + { return adfSrcGT[0] + adfSrcGT[1] * GDALGetRasterXSize(hSrcDS); }; + const auto GetMinLat = [&adfSrcGT, hSrcDS]() + { return adfSrcGT[3] + adfSrcGT[5] * GDALGetRasterYSize(hSrcDS); }; + const auto GetMaxLat = [&adfSrcGT]() { return adfSrcGT[3]; }; + if (oSrcSRS.IsGeographic() && !oSrcSRS.IsDerivedGeographic() && + pszProjection && EQUAL(pszProjection, SRS_PT_MERCATOR_1SP) && + oDstSRS.GetNormProjParm(SRS_PP_CENTRAL_MERIDIAN, 0.0) == 0 && + (pszMethod == nullptr || EQUAL(pszMethod, "GEOTRANSFORM")) && + CSLFetchNameValue(papszTO, "COORDINATE_OPERATION") == nullptr && + CSLFetchNameValue(papszTO, "SRC_METHOD") == nullptr && + CSLFetchNameValue(papszTO, "DST_METHOD") == nullptr && + GDALGetGeoTransform(hSrcDS, adfSrcGT) == CE_None && + adfSrcGT[2] == 0 && adfSrcGT[4] == 0 && adfSrcGT[5] < 0 && + GetMinLon() >= -180 - EPS && GetMaxLon() <= 180 + EPS && + ((GetMaxLat() > MAX_LAT && GetMinLat() < MAX_LAT) || + (GetMaxLat() > -MAX_LAT && GetMinLat() < -MAX_LAT)) && + GDALGetMetadata(hSrcDS, "GEOLOC_ARRAY") == nullptr && + GDALGetMetadata(hSrcDS, "RPC") == nullptr) + { + auto poCT = std::unique_ptr( + OGRCreateCoordinateTransformation(&oSrcSRS, &oDstSRS)); + if (poCT) + { + double xLL = std::max(GetMinLon(), -180.0); + double yLL = std::max(GetMinLat(), -MAX_LAT); + double xUR = std::min(GetMaxLon(), 180.0); + double yUR = std::min(GetMaxLat(), MAX_LAT); + if (poCT->Transform(1, &xLL, &yLL) && + poCT->Transform(1, &xUR, &yUR)) + { + psOptions->dfMinX = xLL; + psOptions->dfMinY = yLL; + psOptions->dfMaxX = xUR; + psOptions->dfMaxY = yUR; + CPLError(CE_Warning, CPLE_AppDefined, + "Clamping output bounds to (%f,%f) -> (%f, %f)", + psOptions->dfMinX, psOptions->dfMinY, + psOptions->dfMaxX, psOptions->dfMaxY); + } + } + } + } + /* If (-ts and -te) or (-tr and -te) are specified, we don't need to compute * the suggested output extent */ const bool bNeedsSuggestedWarpOutput = diff --git a/autotest/utilities/test_gdalwarp_lib.py b/autotest/utilities/test_gdalwarp_lib.py index 33e1637353fa..aefaaef42b86 100755 --- a/autotest/utilities/test_gdalwarp_lib.py +++ b/autotest/utilities/test_gdalwarp_lib.py @@ -2655,6 +2655,41 @@ def test_gdalwarp_lib_bug_4326_to_3857(): assert cs == 4672 +############################################################################### +# Test warping full world from EPSG:4326 to EPSG:3857 + + +def test_gdalwarp_lib_full_world_4326_to_3857(): + class MyHandler: + def __init__(self): + self.warning_raised = False + + def callback(self, err_type, err_no, err_msg): + if err_type == gdal.CE_Warning and "Clamping output bounds to" in err_msg: + self.warning_raised = True + + my_error_handler = MyHandler() + with gdaltest.error_handler(my_error_handler.callback): + ds = gdal.Warp( + "", + "../gdrivers/data/small_world.tif", + options="-f MEM -t_srs EPSG:3857", + ) + assert my_error_handler.warning_raised + assert ds.GetGeoTransform() == pytest.approx( + ( + -20037508.342789244, + 103286.12547829507, + 0.0, + 20037508.342789248, + 0.0, + -103286.12547829509, + ) + ) + assert ds.RasterXSize == 388 + assert ds.RasterYSize == 388 + + ############################################################################### # Test warping of single source to COG