Skip to content

Commit

Permalink
gdalwarp: add a heuristic to clamp northings when projecting from geo…
Browse files Browse the repository at this point in the history
…graphic to Mercator (typically EPSG:3857) (fixes #8730)
  • Loading branch information
rouault authored and github-actions[bot] committed Nov 21, 2023
1 parent 615595c commit bf16533
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 0 deletions.
69 changes: 69 additions & 0 deletions apps/gdalwarp_lib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<OGRCoordinateTransformation>(
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 =
Expand Down
35 changes: 35 additions & 0 deletions autotest/utilities/test_gdalwarp_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit bf16533

Please sign in to comment.