Skip to content

Commit cc2c7dd

Browse files
committed
FIX: Image tiles crossing the dateline need special handling
When image tile requests crossing a dateline happen, there is a multipolygon that gets passed to the find images function. We don't necessarily want just the tiles that are within those geometries, but also the ones that could be between them.
1 parent a9a8fb8 commit cc2c7dd

File tree

2 files changed

+61
-1
lines changed

2 files changed

+61
-1
lines changed

lib/cartopy/io/img_tiles.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,40 @@ def _find_images(self, target_domain, target_z, start_tile=(0, 0, 0)):
125125
# Recursively drill down to the images at the target zoom.
126126
x0, x1, y0, y1 = self._tileextent(start_tile)
127127
domain = sgeom.box(x0, y0, x1, y1)
128-
if domain.intersects(target_domain):
128+
129+
# Handle MultiPolygon target domains (e.g., from projections crossing
130+
# the International Date Line). For better tile coverage, we check
131+
# intersection with any part of the MultiPolygon, and also include
132+
# tiles that span between disconnected parts to ensure continuity.
133+
intersects = False
134+
if hasattr(target_domain, 'geoms'):
135+
# MultiPolygon case
136+
for geom_part in target_domain.geoms:
137+
if domain.intersects(geom_part):
138+
intersects = True
139+
break
140+
141+
# include tiles that span between disconnected geometry parts
142+
# to ensure tiles get selected when crossing the dateline
143+
if not intersects and len(target_domain.geoms) > 1:
144+
bounds = [geom_part.bounds for geom_part in target_domain.geoms]
145+
x_ranges = [(b[0], b[2]) for b in bounds]
146+
y_ranges = [(b[1], b[3]) for b in bounds]
147+
148+
# Check if tile is within the overall bounding range
149+
overall_x_min = min(x_range[0] for x_range in x_ranges)
150+
overall_x_max = max(x_range[1] for x_range in x_ranges)
151+
overall_y_min = min(y_range[0] for y_range in y_ranges)
152+
overall_y_max = max(y_range[1] for y_range in y_ranges)
153+
154+
if (x0 <= overall_x_max and x1 >= overall_x_min and
155+
y0 <= overall_y_max and y1 >= overall_y_min):
156+
intersects = True
157+
else:
158+
# Single Polygon case
159+
intersects = domain.intersects(target_domain)
160+
161+
if intersects:
129162
if start_tile[2] == target_z:
130163
yield start_tile
131164
else:

lib/cartopy/tests/test_img_tiles.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,33 @@ def test_tile_find_images():
138138
[(7, 4, 4), (7, 5, 4), (8, 4, 4), (8, 5, 4)])
139139

140140

141+
def test_multipolygon_find_images():
142+
# Test that _find_images handles MultiPolygon geometries correctly,
143+
# particularly for orthographic projections that cross the dateline.
144+
gt = cimgt.GoogleTiles()
145+
146+
# Create a MultiPolygon that simulates the dateline crossing scenario
147+
# Two separate rectangles representing the split geometry parts
148+
geom1 = sgeom.box(-20037508, -8000000, -12000000, 8000000) # Left part
149+
geom2 = sgeom.box(12000000, -8000000, 20037508, 8000000) # Right part
150+
multi_geom = sgeom.MultiPolygon([geom1, geom2])
151+
152+
tiles = list(gt.find_images(multi_geom, 2))
153+
tile_x_coords = {tile[0] for tile in tiles}
154+
155+
# Should include all x coordinates from 0 to 3 for complete coverage
156+
expected_x_coords = {0, 1, 2, 3}
157+
assert tile_x_coords == expected_x_coords
158+
159+
# Request tiles for the individual pieces
160+
tiles_part1 = list(gt.find_images(geom1, 2))
161+
tiles_part2 = list(gt.find_images(geom2, 2))
162+
163+
# Combined individual parts should have fewer tiles than the MultiPolygon
164+
# because the MultiPolygon includes intermediate tiles for continuity
165+
assert len(tiles) > len(tiles_part1) + len(tiles_part2)
166+
167+
141168
@pytest.mark.network
142169
def test_image_for_domain():
143170
gt = cimgt.GoogleTiles()

0 commit comments

Comments
 (0)