From 4c6d723e5365bd0c9f1bd495475977b278530d2b Mon Sep 17 00:00:00 2001 From: ybai08 <271969568+ybai08@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:22:21 -0500 Subject: [PATCH] Clip instead of squeeze out-of-range latitudes in mercator_transform When lat_bounds went beyond the Web Mercator limit (+-85.0511), the input rows were placed between the clipped bounds, so data from the full range was squeezed into it. For a global (-90, 90) image the rows were shifted towards the equator by up to ~5 degrees. Lay the input rows out over the original bounds and only clip the output, as the docstring describes. --- folium/utilities.py | 7 ++++--- tests/test_raster_layers.py | 2 +- tests/test_utilities.py | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/folium/utilities.py b/folium/utilities.py index e9807ad91d..d621929ad0 100644 --- a/folium/utilities.py +++ b/folium/utilities.py @@ -232,7 +232,7 @@ def mercator_transform( lat_bounds : length 2 tuple Minimal and maximal value of the latitude of the image. Bounds must be between -85.051128779806589 and 85.051128779806589 - otherwise they will be clipped to that values. + otherwise the image will be clipped to that range. origin : ['upper' | 'lower'], optional, default 'upper' Place the [0,0] index of the array in the upper left or lower left @@ -261,8 +261,9 @@ def mercator(x): if origin == "upper": array = array[::-1, :, :] - lats = lat_min + np.linspace(0.5 / height, 1.0 - 0.5 / height, height) * ( - lat_max - lat_min + # The input rows span the full lat_bounds; only the output is clipped. + lats = lat_bounds[0] + np.linspace(0.5 / height, 1.0 - 0.5 / height, height) * ( + lat_bounds[1] - lat_bounds[0] ) latslats = mercator(lat_min) + np.linspace( 0.5 / height_out, 1.0 - 0.5 / height_out, height_out diff --git a/tests/test_raster_layers.py b/tests/test_raster_layers.py index 918568b3cb..755709cc1a 100644 --- a/tests/test_raster_layers.py +++ b/tests/test_raster_layers.py @@ -94,7 +94,7 @@ def test_image_overlay(): url = ( "data:image/png;base64," "iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAA" - "AF0lEQVR42mP4z8AARFDw/z/DeiA5H4QBV60H6ABl9ZIAAAAASUVORK5CYII=" + "AF0lEQVR42mP4z8AARFDw/z/DciC5DoQBV78H/mpWZYkAAAAASUVORK5CYII=" ) assert io.url == url diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 26d346ca39..82fb010d1f 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -12,6 +12,7 @@ get_obj_in_upper_tree, if_pandas_df_convert_to_numpy, javascript_identifier_path_to_array_notation, + mercator_transform, normalize_bounds_type, parse_font_size, parse_options, @@ -292,3 +293,21 @@ def test_parse_font_size_valid(value, expected): def test_parse_font_size_invalid(value, error_message): with pytest.raises(ValueError, match=error_message): parse_font_size(value) + + +def test_mercator_transform_clips_bounds_beyond_mercator_limit(): + # Each row holds the latitude of its centre, from -89.5 (bottom) to 89.5. + height = 180 + lats = np.linspace(-90, 90, height + 1)[:-1] + 0.5 + data = np.repeat(lats[:, None], 2, axis=1) + + out = mercator_transform(data, (-90, 90), origin="lower") + + # The output only covers the Web Mercator range, so every output row must + # hold (up to interpolation, well within one 1 degree input pixel) the + # latitude of its own centre: rows beyond +-85.05 are dropped, not + # squeezed into the output. + # Output row centres in Web Mercator y (degrees); +-85.05 maps to +-180. + y = np.linspace(-180, 180, height + 1)[:-1] + 180 / height + expected = np.degrees(np.arctan(np.sinh(np.radians(y)))) + np.testing.assert_allclose(out[:, 0, 0], expected, atol=0.05)