Skip to content

Commit 966f81b

Browse files
kylebarronvgeorge
andauthored
feat: Create richer Basemap class and deprecate basemap_style arg (#935)
### Change list - Add `basemap` parameter to `Map`. This now manages a class with more information than just the basemap style. In particular, this holds the render mode `"interleaved"`, `"overlaid"`, or `"reverse-controlled"` that was implemented on the JS side in #921 - Deprecate `basemap_style` parameter to `Map`. This is superseded by the `style` parameter to `MaplibreBasemap` - Rename `CartoBasemap` to `CartoStyle` - Add `before_id` parameter to the core `Layer`. When passed and the basemap render mode is `"interleaved"`, - Pass `interleaved` prop down to MapboxOverlay - Remove temporary `render_mode` from #921 **Rendering layer interleaved in maplibre layer stack:** <img width="817" height="505" alt="image" src="https://github.com/user-attachments/assets/c63148a9-daa0-4dd4-bb65-88cc13805060" /> TODO: - [ ] Validation for `beforeId`. If `beforeId` is passed to a layer, try to resolve the basemap style URL to a dict, and then validate that the string value of `beforeId` exists in that layer stack. Keep a cache of fetched styles so that you're not fetching them repeatedly. - [ ] Warn if `beforeId` is set when the basemap style is not `"interleaved"`? - [ ] (next PR): integration with https://github.com/geopandas/xyzservices (see #494) - [ ] (possibly future PR): examples of rendering different basemap modes For #494 --------- Co-authored-by: Vitor George <vitor.george@gmail.com>
1 parent 84c8add commit 966f81b

File tree

15 files changed

+441
-108
lines changed

15 files changed

+441
-108
lines changed

docs/api/basemap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# lonboard.basemap
22

3-
::: lonboard.basemap.CartoBasemap
3+
::: lonboard.basemap.CartoStyle

lonboard/_layer.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,14 @@ def _add_extension_traits(self, extensions: Sequence[BaseExtension]) -> None:
279279
for an example.
280280
"""
281281

282+
before_id = t.Unicode(None, allow_none=True).tag(sync=True)
283+
"""The ID of a layer in the Maplibre basemap layer stack. This deck.gl layer will be
284+
rendered just before the layer with the given ID.
285+
286+
This only has an effect when the map's `basemap` is a `MaplibreBasemap`, and the map
287+
is rendering in `"interleaved"` mode.
288+
"""
289+
282290

283291
def default_geoarrow_viewport(
284292
table: Table,

lonboard/_map.py

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import warnings
34
from pathlib import Path
45
from typing import IO, TYPE_CHECKING, Any, TextIO, overload
56

@@ -12,10 +13,9 @@
1213
from lonboard._html_export import map_to_html
1314
from lonboard._layer import BaseLayer
1415
from lonboard._viewport import compute_view
15-
from lonboard.basemap import CartoBasemap
16+
from lonboard.basemap import CartoStyle, MaplibreBasemap
1617
from lonboard.traits import (
1718
DEFAULT_INITIAL_VIEW_STATE,
18-
BasemapUrl,
1919
HeightTrait,
2020
VariableLengthTuple,
2121
ViewStateTrait,
@@ -69,6 +69,8 @@ class Map(BaseAnyWidget):
6969
def __init__(
7070
self,
7171
layers: BaseLayer | Sequence[BaseLayer],
72+
*,
73+
basemap_style: str | CartoStyle | None = None,
7274
**kwargs: Unpack[MapKwargs],
7375
) -> None:
7476
"""Create a new Map.
@@ -80,12 +82,28 @@ def __init__(
8082
layers: One or more layers to render on this map.
8183
8284
Keyword Args:
85+
basemap_style: DEPRECATED. Use `basemap` instead. A URL to a MapLibre-compatible basemap style.
86+
87+
Various styles are provided in [`lonboard.basemap`](https://developmentseed.org/lonboard/latest/api/basemap/).
88+
8389
kwargs: Passed on to class variables.
8490
8591
Returns:
8692
A Map object.
8793
8894
"""
95+
if basemap_style is not None:
96+
warnings.warn(
97+
"`basemap_style` is deprecated and will be removed in 0.14. Use `basemap` instead.",
98+
DeprecationWarning,
99+
stacklevel=2,
100+
)
101+
if "basemap" in kwargs:
102+
raise ValueError(
103+
"Cannot pass both `basemap_style` and `basemap`. Use only `basemap`.",
104+
)
105+
kwargs["basemap"] = MaplibreBasemap(style=basemap_style)
106+
89107
if isinstance(layers, BaseLayer):
90108
layers = [layers]
91109

@@ -161,8 +179,6 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
161179
Indicates if a click handler has been registered.
162180
"""
163181

164-
render_mode = t.Unicode(default_value="deck-first").tag(sync=True)
165-
166182
height = HeightTrait().tag(sync=True)
167183
"""Height of the map in pixels, or valid CSS height property.
168184
@@ -203,17 +219,43 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
203219
- Default: `5`
204220
"""
205221

206-
basemap_style = BasemapUrl(CartoBasemap.PositronNoLabels)
207-
"""
208-
A URL to a MapLibre-compatible basemap style.
222+
basemap: t.Instance[MaplibreBasemap | None] = t.Instance(
223+
MaplibreBasemap,
224+
allow_none=True,
225+
).tag(
226+
sync=True,
227+
**ipywidgets.widget_serialization,
228+
)
229+
"""A basemap instance.
209230
210-
Various styles are provided in [`lonboard.basemap`](https://developmentseed.org/lonboard/latest/api/basemap/).
231+
See [`lonboard.basemap.MaplibreBasemap`] for more information.
211232
212-
- Type: `str`, holding a URL hosting a basemap style.
213-
- Default
214-
[`lonboard.basemap.CartoBasemap.PositronNoLabels`][lonboard.basemap.CartoBasemap.PositronNoLabels]
233+
Pass `None` to disable rendering a basemap.
215234
"""
216235

236+
@property
237+
def basemap_style(self) -> str | None:
238+
"""The URL of the basemap style in use."""
239+
warnings.warn(
240+
"`basemap_style` is deprecated and will be removed in 0.14. Use `basemap` instead.",
241+
DeprecationWarning,
242+
stacklevel=2,
243+
)
244+
245+
if self.basemap is not None:
246+
return self.basemap.style
247+
248+
return None
249+
250+
@basemap_style.setter
251+
def basemap_style(self, value: str | CartoStyle) -> None:
252+
warnings.warn(
253+
"`basemap_style` is deprecated and will be removed in 0.14. Use `basemap` instead.",
254+
DeprecationWarning,
255+
stacklevel=2,
256+
)
257+
self.basemap = MaplibreBasemap(style=value)
258+
217259
custom_attribution = t.Union(
218260
[
219261
t.Unicode(allow_none=True),

lonboard/_viz.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
split_mixed_gdf,
2727
split_mixed_shapely_array,
2828
)
29-
from lonboard.basemap import CartoBasemap
29+
from lonboard.basemap import CartoStyle, MaplibreBasemap
3030

3131
if TYPE_CHECKING:
3232
import duckdb
@@ -212,8 +212,8 @@ def viz(
212212

213213
map_kwargs = map_kwargs if map_kwargs else {}
214214

215-
if "basemap_style" not in map_kwargs:
216-
map_kwargs["basemap_style"] = CartoBasemap.DarkMatter
215+
if "basemap_style" not in map_kwargs and "basemap" not in map_kwargs:
216+
map_kwargs["basemap"] = MaplibreBasemap(style=CartoStyle.DarkMatter)
217217

218218
return Map(layers=layers, **map_kwargs)
219219

lonboard/basemap.py

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,78 @@
11
"""Basemap helpers."""
22

3+
from __future__ import annotations
4+
35
from enum import Enum
6+
from typing import TYPE_CHECKING
7+
8+
import traitlets as t
9+
from typing_extensions import deprecated
10+
11+
from lonboard._base import BaseWidget
12+
from lonboard.traits import BasemapUrl
13+
14+
if TYPE_CHECKING:
15+
from typing import Literal
16+
17+
18+
class CartoStyle(str, Enum):
19+
"""Maplibre-supported vector basemap styles provided by Carto.
20+
21+
Refer to [Carto
22+
documentation](https://docs.carto.com/carto-for-developers/carto-for-react/guides/basemaps)
23+
for information on styles.
24+
"""
25+
26+
DarkMatter = "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
27+
"""A dark map style with labels.
28+
29+
![](https://carto.com/help/images/building-maps/basemaps/dark_labels.png)
30+
"""
31+
32+
DarkMatterNoLabels = (
33+
"https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json"
34+
)
35+
"""A dark map style without labels.
36+
37+
![](https://carto.com/help/images/building-maps/basemaps/dark_no_labels.png)
38+
"""
39+
40+
Positron = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
41+
"""A light map style with labels.
42+
43+
![](https://carto.com/help/images/building-maps/basemaps/positron_labels.png)
44+
"""
445

46+
PositronNoLabels = (
47+
"https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json"
48+
)
49+
"""A light map style without labels.
550
51+
![](https://carto.com/help/images/building-maps/basemaps/positron_no_labels.png)
52+
"""
53+
54+
Voyager = "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
55+
"""A light, colored map style with labels.
56+
57+
![](https://carto.com/help/images/building-maps/basemaps/voyager_labels.png)
58+
"""
59+
60+
VoyagerNoLabels = (
61+
"https://basemaps.cartocdn.com/gl/voyager-nolabels-gl-style/style.json"
62+
)
63+
"""A light, colored map style without labels.
64+
65+
![](https://carto.com/help/images/building-maps/basemaps/voyager_no_labels.png)
66+
"""
67+
68+
69+
# NOTE: this is fully duplicated because you can't subclass enums, and I don't see any
70+
# other way to provide a deprecation warning
71+
@deprecated(
72+
"CartoBasemap is deprecated, use CartoStyle instead. Will be removed in v0.14",
73+
)
674
class CartoBasemap(str, Enum):
7-
"""Basemap styles provided by Carto.
75+
"""Maplibre-supported vector basemap styles provided by Carto.
876
977
Refer to [Carto
1078
documentation](https://docs.carto.com/carto-for-developers/carto-for-react/guides/basemaps)
@@ -20,7 +88,10 @@ class CartoBasemap(str, Enum):
2088
DarkMatterNoLabels = (
2189
"https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json"
2290
)
23-
"""A dark map style without labels."""
91+
"""A dark map style without labels.
92+
93+
![](https://carto.com/help/images/building-maps/basemaps/dark_no_labels.png)
94+
"""
2495

2596
Positron = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
2697
"""A light map style with labels.
@@ -31,7 +102,10 @@ class CartoBasemap(str, Enum):
31102
PositronNoLabels = (
32103
"https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json"
33104
)
34-
"""A light map style without labels."""
105+
"""A light map style without labels.
106+
107+
![](https://carto.com/help/images/building-maps/basemaps/positron_no_labels.png)
108+
"""
35109

36110
Voyager = "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
37111
"""A light, colored map style with labels.
@@ -42,4 +116,62 @@ class CartoBasemap(str, Enum):
42116
VoyagerNoLabels = (
43117
"https://basemaps.cartocdn.com/gl/voyager-nolabels-gl-style/style.json"
44118
)
45-
"""A light, colored map style without labels."""
119+
"""A light, colored map style without labels.
120+
121+
![](https://carto.com/help/images/building-maps/basemaps/voyager_no_labels.png)
122+
"""
123+
124+
125+
class MaplibreBasemap(BaseWidget):
126+
"""A MapLibre GL JS basemap."""
127+
128+
def __init__(
129+
self,
130+
*,
131+
mode: Literal[
132+
"interleaved",
133+
"overlaid",
134+
"reverse-controlled",
135+
] = "reverse-controlled",
136+
style: str | CartoStyle = CartoStyle.PositronNoLabels,
137+
) -> None:
138+
"""Create a MapLibre GL JS basemap."""
139+
super().__init__(mode=mode, style=style)
140+
141+
mode = t.Enum(
142+
[
143+
"interleaved",
144+
"overlaid",
145+
"reverse-controlled",
146+
],
147+
default_value="reverse-controlled",
148+
).tag(sync=True)
149+
"""The basemap integration mode.
150+
151+
- **`"interleaved"`**:
152+
153+
The interleaved mode renders deck.gl layers into the same context created by MapLibre. If you need to mix deck.gl layers with MapLibre layers, e.g. having deck.gl surfaces below text labels, or objects occluding each other correctly in 3D, then you have to use this option.
154+
155+
- **`"overlaid"`**:
156+
157+
The overlaid mode renders deck.gl in a separate canvas inside the MapLibre's controls container. If your use case does not require interleaving, but you still want to use certain features of maplibre-gl, such as globe view, then you should use this option.
158+
159+
- **`"reverse-controlled"`**:
160+
161+
The reverse-controlled mode renders deck.gl above the MapLibre container and blocks any interaction to the base map.
162+
163+
If you need to have multiple views, you should use this option.
164+
165+
**Default**: `"reverse-controlled"`
166+
"""
167+
168+
style = BasemapUrl(CartoStyle.PositronNoLabels).tag(sync=True)
169+
"""
170+
A URL to a MapLibre-compatible basemap style.
171+
172+
Various styles are provided in [`lonboard.basemap`](https://developmentseed.org/lonboard/latest/api/basemap/).
173+
174+
- Type: `str`, holding a URL hosting a basemap style.
175+
- Default
176+
[`lonboard.basemap.CartoStyle.PositronNoLabels`][lonboard.basemap.CartoStyle.PositronNoLabels]
177+
"""

lonboard/traits.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1078,7 +1078,6 @@ def __init__(
10781078
**kwargs: Any,
10791079
) -> None:
10801080
super().__init__(*args, **kwargs)
1081-
self.tag(sync=True)
10821081

10831082
def validate(self, obj: HasTraits | None, value: Any) -> Any:
10841083
value = super().validate(obj, value)

lonboard/types/map.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99
from typing_extensions import TypedDict
1010

1111
if TYPE_CHECKING:
12-
from lonboard.basemap import CartoBasemap
12+
from lonboard.basemap import MaplibreBasemap
1313

1414

1515
class MapKwargs(TypedDict, total=False):
1616
"""Kwargs to pass into map constructor."""
1717

1818
height: int | str
19-
basemap_style: str | CartoBasemap
19+
basemap: MaplibreBasemap
2020
parameters: dict[str, Any]
2121
picking_radius: int
2222
show_tooltip: bool

0 commit comments

Comments
 (0)