Skip to content

Fix split_map gray background when using vector layers#1300

Merged
giswqs merged 1 commit intomasterfrom
fix-split-map-vector-background
Mar 9, 2026
Merged

Fix split_map gray background when using vector layers#1300
giswqs merged 1 commit intomasterfrom
fix-split-map-vector-background

Conversation

@giswqs
Copy link
Member

@giswqs giswqs commented Mar 9, 2026

Summary

  • When using split_map with a GeoJSON/vector layer on one side and a raster (TIF) on the other, dragging the swipe divider showed a gray background behind the vector layer
  • This fix duplicates the raster TileLayer from the opposite side as a background for the vector side, so the GeoJSON is always overlaid on the raster imagery
  • Applied to both the Map.split_map() method and the module-level split_map() function

Test plan

  • Run split_map with a GeoJSON on the left and a raster on the right, drag the swipe divider and verify the vector layer shows the raster underneath instead of a gray background
  • Run split_map with two raster layers to verify no regression
  • Run split_map with a GeoDataFrame input to verify it also gets the raster background

When using split_map with a GeoJSON layer on one side and a raster on
the other, dragging the swipe divider showed a gray background behind
the vector layer. This duplicates the raster TileLayer from the
opposite side as a background for the vector side, so the GeoJSON is
always overlaid on the raster imagery.
Copilot AI review requested due to automatic review settings March 9, 2026 05:10
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a rendering artifact in split_map where using a vector/GeoJSON layer on one side could reveal a gray background while dragging the swipe divider, by ensuring a raster TileLayer background is present under the GeoJSON.

Changes:

  • Inject a background ipyleaflet.TileLayer underneath ipyleaflet.GeoJSON when the opposite side is a raster TileLayer.
  • Apply the same logic to both Map.split_map() and the module-level split_map() implementation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +7541 to +7563
if isinstance(left_layer, ipyleaflet.GeoJSON) and isinstance(
right_layer, ipyleaflet.TileLayer
):
bg = ipyleaflet.TileLayer(
url=right_layer.url,
name=right_layer.name,
attribution=right_layer.attribution,
max_zoom=right_layer.max_zoom,
)
left_layer = [bg, left_layer]

if isinstance(right_layer, ipyleaflet.GeoJSON) and isinstance(
left_layer, (ipyleaflet.TileLayer, list)
):
source = left_layer[0] if isinstance(left_layer, list) else left_layer
bg = ipyleaflet.TileLayer(
url=source.url,
name=source.name,
attribution=source.attribution,
max_zoom=source.max_zoom,
)
right_layer = [bg, right_layer]

Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This GeoJSON+TileLayer background-wrapping logic is duplicated in both Map.split_map() and the module-level split_map(). To reduce drift/maintenance risk, consider factoring it into a shared helper and calling it from both places.

Suggested change
if isinstance(left_layer, ipyleaflet.GeoJSON) and isinstance(
right_layer, ipyleaflet.TileLayer
):
bg = ipyleaflet.TileLayer(
url=right_layer.url,
name=right_layer.name,
attribution=right_layer.attribution,
max_zoom=right_layer.max_zoom,
)
left_layer = [bg, left_layer]
if isinstance(right_layer, ipyleaflet.GeoJSON) and isinstance(
left_layer, (ipyleaflet.TileLayer, list)
):
source = left_layer[0] if isinstance(left_layer, list) else left_layer
bg = ipyleaflet.TileLayer(
url=source.url,
name=source.name,
attribution=source.attribution,
max_zoom=source.max_zoom,
)
right_layer = [bg, right_layer]
def _wrap_geojson_with_background(left_layer, right_layer):
"""Ensure a GeoJSON layer is displayed over an appropriate TileLayer background.
When one side of the split map is a GeoJSON layer and the other side is a
TileLayer (or list containing a TileLayer as its first element), this helper
wraps the GeoJSON side together with a background TileLayer constructed from
the TileLayer side.
"""
if isinstance(left_layer, ipyleaflet.GeoJSON) and isinstance(
right_layer, ipyleaflet.TileLayer
):
bg = ipyleaflet.TileLayer(
url=right_layer.url,
name=right_layer.name,
attribution=right_layer.attribution,
max_zoom=right_layer.max_zoom,
)
left_layer = [bg, left_layer]
elif isinstance(right_layer, ipyleaflet.GeoJSON) and isinstance(
left_layer, (ipyleaflet.TileLayer, list)
):
source = left_layer[0] if isinstance(left_layer, list) else left_layer
bg = ipyleaflet.TileLayer(
url=source.url,
name=source.name,
attribution=source.attribution,
max_zoom=source.max_zoom,
)
right_layer = [bg, right_layer]
return left_layer, right_layer
left_layer, right_layer = _wrap_geojson_with_background(left_layer, right_layer)

Copilot uses AI. Check for mistakes.
Comment on lines +7541 to +7543
if isinstance(left_layer, ipyleaflet.GeoJSON) and isinstance(
right_layer, ipyleaflet.TileLayer
):
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are existing tests that exercise split_map(), but they don't validate the new behavior for GeoJSON/vector inputs (i.e., that a raster background layer is injected under the GeoJSON side). Consider adding a unit test that creates a GeoJSON layer + TileLayer, calls split_map, and asserts the SplitMapControl receives a background TileLayer + GeoJSON (and similarly covers Map.split_map where feasible).

Copilot uses AI. Check for mistakes.
Comment on lines +2154 to +2174
if isinstance(left_layer, ipyleaflet.GeoJSON) and isinstance(
right_layer, ipyleaflet.TileLayer
):
bg = ipyleaflet.TileLayer(
url=right_layer.url,
name=right_layer.name,
attribution=right_layer.attribution,
max_zoom=right_layer.max_zoom,
)
left_layer = [bg, left_layer]

if isinstance(right_layer, ipyleaflet.GeoJSON) and isinstance(
left_layer, (ipyleaflet.TileLayer, list)
):
source = left_layer[0] if isinstance(left_layer, list) else left_layer
bg = ipyleaflet.TileLayer(
url=source.url,
name=source.name,
attribution=source.attribution,
max_zoom=source.max_zoom,
)
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The background TileLayer clone only copies a few fields (url/name/attribution/max_zoom), but the source layer is commonly configured with additional traits (notably max_native_zoom is set earlier). This can make the background behave differently (e.g., tiles disappearing at higher zooms) and may not fully resolve the gray background issue. Consider cloning all relevant TileLayer properties (at least max_native_zoom/min_zoom/opacity and any args used to build the source layer), or centralize layer cloning in a helper to avoid missing fields.

Suggested change
if isinstance(left_layer, ipyleaflet.GeoJSON) and isinstance(
right_layer, ipyleaflet.TileLayer
):
bg = ipyleaflet.TileLayer(
url=right_layer.url,
name=right_layer.name,
attribution=right_layer.attribution,
max_zoom=right_layer.max_zoom,
)
left_layer = [bg, left_layer]
if isinstance(right_layer, ipyleaflet.GeoJSON) and isinstance(
left_layer, (ipyleaflet.TileLayer, list)
):
source = left_layer[0] if isinstance(left_layer, list) else left_layer
bg = ipyleaflet.TileLayer(
url=source.url,
name=source.name,
attribution=source.attribution,
max_zoom=source.max_zoom,
)
def _clone_tile_layer(layer: ipyleaflet.TileLayer) -> ipyleaflet.TileLayer:
"""Create a background TileLayer that closely matches the source layer."""
tile_kwargs: Dict[str, Any] = dict(
url=layer.url,
name=layer.name,
attribution=layer.attribution,
max_zoom=layer.max_zoom,
)
# Copy additional commonly used attributes when present to keep
# background behavior (e.g., zoom limits, opacity) consistent.
for attr in ("max_native_zoom", "min_zoom", "opacity"):
value = getattr(layer, attr, None)
if value is not None:
tile_kwargs[attr] = value
return ipyleaflet.TileLayer(**tile_kwargs)
if isinstance(left_layer, ipyleaflet.GeoJSON) and isinstance(
right_layer, ipyleaflet.TileLayer
):
bg = _clone_tile_layer(right_layer)
left_layer = [bg, left_layer]
if isinstance(right_layer, ipyleaflet.GeoJSON) and isinstance(
left_layer, (ipyleaflet.TileLayer, list)
):
source = left_layer[0] if isinstance(left_layer, list) else left_layer
bg = _clone_tile_layer(source)

Copilot uses AI. Check for mistakes.
Comment on lines +2159 to +2162
name=right_layer.name,
attribution=right_layer.attribution,
max_zoom=right_layer.max_zoom,
)
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The background TileLayer is given the same name as the source layer. With LayersControl enabled, duplicate layer names can cause confusing UI and can potentially overwrite entries (layers are typically keyed by name). Consider giving the background layer a distinct/internal name (e.g., suffix "(background)") or otherwise excluding it from layer controls.

Copilot uses AI. Check for mistakes.
Comment on lines +2154 to +2156
if isinstance(left_layer, ipyleaflet.GeoJSON) and isinstance(
right_layer, ipyleaflet.TileLayer
):
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This GeoJSON+TileLayer background-wrapping logic is duplicated in both Map.split_map() and the module-level split_map(). To reduce drift/maintenance risk, consider factoring it into a shared helper and calling it from both places.

Copilot uses AI. Check for mistakes.
Comment on lines +7541 to +7561
if isinstance(left_layer, ipyleaflet.GeoJSON) and isinstance(
right_layer, ipyleaflet.TileLayer
):
bg = ipyleaflet.TileLayer(
url=right_layer.url,
name=right_layer.name,
attribution=right_layer.attribution,
max_zoom=right_layer.max_zoom,
)
left_layer = [bg, left_layer]

if isinstance(right_layer, ipyleaflet.GeoJSON) and isinstance(
left_layer, (ipyleaflet.TileLayer, list)
):
source = left_layer[0] if isinstance(left_layer, list) else left_layer
bg = ipyleaflet.TileLayer(
url=source.url,
name=source.name,
attribution=source.attribution,
max_zoom=source.max_zoom,
)
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The background TileLayer clone only copies a few fields (url/name/attribution/max_zoom), but the source layer is commonly configured with additional traits (notably max_native_zoom is set earlier). This can make the background behave differently (e.g., tiles disappearing at higher zooms) and may not fully resolve the gray background issue. Consider cloning all relevant TileLayer properties (at least max_native_zoom/min_zoom/opacity and any args used to build the source layer), or centralize layer cloning in a helper to avoid missing fields.

Suggested change
if isinstance(left_layer, ipyleaflet.GeoJSON) and isinstance(
right_layer, ipyleaflet.TileLayer
):
bg = ipyleaflet.TileLayer(
url=right_layer.url,
name=right_layer.name,
attribution=right_layer.attribution,
max_zoom=right_layer.max_zoom,
)
left_layer = [bg, left_layer]
if isinstance(right_layer, ipyleaflet.GeoJSON) and isinstance(
left_layer, (ipyleaflet.TileLayer, list)
):
source = left_layer[0] if isinstance(left_layer, list) else left_layer
bg = ipyleaflet.TileLayer(
url=source.url,
name=source.name,
attribution=source.attribution,
max_zoom=source.max_zoom,
)
def _clone_tilelayer_for_background(
src_layer: ipyleaflet.TileLayer,
) -> ipyleaflet.TileLayer:
"""Clone a TileLayer for background use, preserving key display traits."""
clone_kwargs: Dict[str, Any] = {
"url": src_layer.url,
"name": src_layer.name,
"attribution": src_layer.attribution,
}
# Preserve important zoom/opacity traits when present.
for attr in ("min_zoom", "max_zoom", "max_native_zoom", "opacity"):
value = getattr(src_layer, attr, None)
if value is not None:
clone_kwargs[attr] = value
return ipyleaflet.TileLayer(**clone_kwargs)
if isinstance(left_layer, ipyleaflet.GeoJSON) and isinstance(
right_layer, ipyleaflet.TileLayer
):
bg = _clone_tilelayer_for_background(right_layer)
left_layer = [bg, left_layer]
if isinstance(right_layer, ipyleaflet.GeoJSON) and isinstance(
left_layer, (ipyleaflet.TileLayer, list)
):
source = left_layer[0] if isinstance(left_layer, list) else left_layer
bg = _clone_tilelayer_for_background(source)

Copilot uses AI. Check for mistakes.
Comment on lines +7545 to +7549
url=right_layer.url,
name=right_layer.name,
attribution=right_layer.attribution,
max_zoom=right_layer.max_zoom,
)
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The background TileLayer is given the same name as the source layer. With LayersControl enabled, duplicate layer names can cause confusing UI and can potentially overwrite entries (layers are typically keyed by name). Consider giving the background layer a distinct/internal name (e.g., suffix "(background)") or otherwise excluding it from layer controls.

Copilot uses AI. Check for mistakes.
@giswqs giswqs merged commit d35e2c5 into master Mar 9, 2026
22 of 23 checks passed
@giswqs giswqs deleted the fix-split-map-vector-background branch March 9, 2026 05:16
@github-actions
Copy link

github-actions bot commented Mar 9, 2026

@github-actions github-actions bot temporarily deployed to pull request March 9, 2026 05:16 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants