Skip to content

Commit 7f14643

Browse files
authored
feat: Validate that GlobeView is only used with interleaved basemap mode (#1012)
After discussion with @felixpalmer , we established that the only deck.gl rendering mode that really supports Globe view is interleaved-maplibre (or no basemap at all). This adds validators to the `Map` class to enforce that you can't add a GlobeView when the basemap is overlaid or reverse-controlled, and you can't set such a basemap when the view is a globe view. ref #1009
1 parent 4779096 commit 7f14643

File tree

4 files changed

+102
-4
lines changed

4 files changed

+102
-4
lines changed

lonboard/_map.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from typing import IO, TYPE_CHECKING, Any, TextIO, overload
77

88
import ipywidgets
9-
import traitlets
109
import traitlets as t
1110
from ipywidgets import CallbackDispatcher
1211

@@ -31,8 +30,14 @@
3130

3231
from IPython.display import HTML # type: ignore
3332

33+
from lonboard._validators.types import TraitProposal
3434
from lonboard.types.map import MapKwargs
3535

36+
if sys.version_info >= (3, 11):
37+
from typing import Self
38+
else:
39+
from typing_extensions import Self
40+
3641
if sys.version_info >= (3, 12):
3742
from typing import Unpack
3843
else:
@@ -221,6 +226,23 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
221226
Views represent the "camera(s)" (essentially viewport dimensions and projection matrices) that you look at your data with. deck.gl offers multiple view types for both geospatial and non-geospatial use cases. Read the [Views and Projections](https://deck.gl/docs/developer-guide/views) guide for the concept and examples.
222227
"""
223228

229+
@t.validate("view")
230+
def _validate_view(
231+
self,
232+
proposal: TraitProposal[t.Instance[BaseView | None], BaseView, Self],
233+
) -> BaseView:
234+
# if proposed view is a globe view, ensure that basemap is interleaved
235+
if (
236+
isinstance(proposal["value"], GlobeView)
237+
and self.basemap is not None
238+
and self.basemap.mode != "interleaved"
239+
):
240+
raise t.TraitError(
241+
"GlobeView requires the basemap mode to be 'interleaved'. Please set `basemap.mode='interleaved'`.",
242+
)
243+
244+
return proposal["value"]
245+
224246
show_tooltip = t.Bool(default_value=False).tag(sync=True)
225247
"""
226248
Whether to render a tooltip on hover on the map.
@@ -265,6 +287,27 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
265287
Pass `None` to disable rendering a basemap.
266288
"""
267289

290+
@t.validate("basemap")
291+
def _validate_basemap(
292+
self,
293+
proposal: TraitProposal[
294+
t.Instance[MaplibreBasemap | None],
295+
MaplibreBasemap,
296+
Self,
297+
],
298+
) -> MaplibreBasemap | None:
299+
# If proposed basemap is not interleaved, ensure current view is not globe view
300+
if (
301+
proposal["value"] is not None
302+
and proposal["value"].mode != "interleaved"
303+
and isinstance(self.view, GlobeView)
304+
):
305+
raise t.TraitError(
306+
"GlobeView requires the basemap mode to be 'interleaved'. Please set `basemap.mode='interleaved'`.",
307+
)
308+
309+
return proposal["value"]
310+
268311
@property
269312
def basemap_style(self) -> str | None:
270313
"""The URL of the basemap style in use."""
@@ -673,7 +716,7 @@ def as_html(self) -> HTML:
673716

674717
return HTML(self.to_html())
675718

676-
@traitlets.default("view_state")
719+
@t.default("view_state")
677720
def _default_initial_view_state(self) -> dict[str, Any]:
678721
if isinstance(self.view, (MapView, GlobeView)):
679722
return compute_view(self.layers) # type: ignore

lonboard/_validators/__init__.py

Whitespace-only changes.

lonboard/_validators/types.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from __future__ import annotations
2+
3+
from typing import Generic, TypedDict, TypeVar
4+
5+
from traitlets import HasTraits, TraitType
6+
7+
Trait = TypeVar("Trait", bound=TraitType)
8+
Value = TypeVar("Value")
9+
Owner = TypeVar("Owner", bound=HasTraits)
10+
11+
12+
class TraitProposal(TypedDict, Generic[Trait, Value, Owner]):
13+
"""The type of a traitlets proposal.
14+
15+
The input into a `@validate` method.
16+
"""
17+
18+
trait: Trait
19+
value: Value
20+
owner: Owner

tests/test_map.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,23 @@ def test_view_state_globe_view_dict():
7878
"latitude": 37.8,
7979
"zoom": 2.0,
8080
}
81-
m = Map([], view=GlobeView(), view_state=view_state)
81+
m = Map(
82+
[],
83+
view=GlobeView(),
84+
view_state=view_state,
85+
basemap=MaplibreBasemap(mode="interleaved"),
86+
)
8287
assert m.view_state == GlobeViewState(**view_state)
8388

8489

8590
def test_view_state_globe_view_instance():
8691
view_state = GlobeViewState(longitude=-122.45, latitude=37.8, zoom=2.0)
87-
m = Map([], view=GlobeView(), view_state=view_state)
92+
m = Map(
93+
[],
94+
view=GlobeView(),
95+
view_state=view_state,
96+
basemap=MaplibreBasemap(mode="interleaved"),
97+
)
8898
assert m.view_state == view_state
8999

90100

@@ -129,6 +139,7 @@ def test_globe_view_state_partial_update():
129139
[],
130140
view=GlobeView(),
131141
view_state={"longitude": -100, "latitude": 40, "zoom": 5},
142+
basemap=MaplibreBasemap(mode="interleaved"),
132143
)
133144
m.set_view_state(latitude=45)
134145
assert m.view_state == GlobeViewState(longitude=-100, latitude=45, zoom=5)
@@ -147,3 +158,27 @@ def test_set_view_state_orbit():
147158
)
148159
m.set_view_state(new_view_state)
149160
assert m.view_state == new_view_state
161+
162+
163+
def test_map_view_validate_globe_view_basemap():
164+
with pytest.raises(
165+
TraitError,
166+
match=r"GlobeView requires the basemap mode to be 'interleaved'.",
167+
):
168+
Map([], view=GlobeView(), basemap=MaplibreBasemap(mode="overlaid"))
169+
170+
# Start with interleaved then try to set overlaid
171+
m = Map([], view=GlobeView(), basemap=MaplibreBasemap(mode="interleaved"))
172+
with pytest.raises(
173+
TraitError,
174+
match=r"GlobeView requires the basemap mode to be 'interleaved'.",
175+
):
176+
m.basemap = MaplibreBasemap(mode="overlaid")
177+
178+
# Start with overlaid then try to set to GlobeView
179+
m = Map([], basemap=MaplibreBasemap(mode="overlaid"))
180+
with pytest.raises(
181+
TraitError,
182+
match=r"GlobeView requires the basemap mode to be 'interleaved'.",
183+
):
184+
m.view = GlobeView()

0 commit comments

Comments
 (0)