Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions lonboard/_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from typing import IO, TYPE_CHECKING, Any, TextIO, overload

import ipywidgets
import traitlets
import traitlets as t
from ipywidgets import CallbackDispatcher

Expand All @@ -31,8 +30,14 @@

from IPython.display import HTML # type: ignore

from lonboard._validators.types import TraitProposal
from lonboard.types.map import MapKwargs

if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self

if sys.version_info >= (3, 12):
from typing import Unpack
else:
Expand Down Expand Up @@ -221,6 +226,23 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
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.
"""

@t.validate("view")
def _validate_view(
self,
proposal: TraitProposal[t.Instance[BaseView | None], BaseView, Self],
) -> BaseView:
# if proposed view is a globe view, ensure that basemap is interleaved
if (
isinstance(proposal["value"], GlobeView)
and self.basemap is not None
and self.basemap.mode != "interleaved"
):
raise t.TraitError(
"GlobeView requires the basemap mode to be 'interleaved'. Please set `basemap.mode='interleaved'`.",
)

return proposal["value"]

show_tooltip = t.Bool(default_value=False).tag(sync=True)
"""
Whether to render a tooltip on hover on the map.
Expand Down Expand Up @@ -265,6 +287,27 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
Pass `None` to disable rendering a basemap.
"""

@t.validate("basemap")
def _validate_basemap(
self,
proposal: TraitProposal[
t.Instance[MaplibreBasemap | None],
MaplibreBasemap,
Self,
],
) -> MaplibreBasemap | None:
# If proposed basemap is not interleaved, ensure current view is not globe view
if (
proposal["value"] is not None
and proposal["value"].mode != "interleaved"
and isinstance(self.view, GlobeView)
):
raise t.TraitError(
"GlobeView requires the basemap mode to be 'interleaved'. Please set `basemap.mode='interleaved'`.",
)

return proposal["value"]

@property
def basemap_style(self) -> str | None:
"""The URL of the basemap style in use."""
Expand Down Expand Up @@ -673,7 +716,7 @@ def as_html(self) -> HTML:

return HTML(self.to_html())

@traitlets.default("view_state")
@t.default("view_state")
def _default_initial_view_state(self) -> dict[str, Any]:
if isinstance(self.view, (MapView, GlobeView)):
return compute_view(self.layers) # type: ignore
Expand Down
Empty file.
20 changes: 20 additions & 0 deletions lonboard/_validators/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from __future__ import annotations

from typing import Generic, TypedDict, TypeVar

from traitlets import HasTraits, TraitType

Trait = TypeVar("Trait", bound=TraitType)
Value = TypeVar("Value")
Owner = TypeVar("Owner", bound=HasTraits)


class TraitProposal(TypedDict, Generic[Trait, Value, Owner]):
"""The type of a traitlets proposal.

The input into a `@validate` method.
"""

trait: Trait
value: Value
owner: Owner
39 changes: 37 additions & 2 deletions tests/test_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,23 @@ def test_view_state_globe_view_dict():
"latitude": 37.8,
"zoom": 2.0,
}
m = Map([], view=GlobeView(), view_state=view_state)
m = Map(
[],
view=GlobeView(),
view_state=view_state,
basemap=MaplibreBasemap(mode="interleaved"),
)
assert m.view_state == GlobeViewState(**view_state)


def test_view_state_globe_view_instance():
view_state = GlobeViewState(longitude=-122.45, latitude=37.8, zoom=2.0)
m = Map([], view=GlobeView(), view_state=view_state)
m = Map(
[],
view=GlobeView(),
view_state=view_state,
basemap=MaplibreBasemap(mode="interleaved"),
)
assert m.view_state == view_state


Expand Down Expand Up @@ -129,6 +139,7 @@ def test_globe_view_state_partial_update():
[],
view=GlobeView(),
view_state={"longitude": -100, "latitude": 40, "zoom": 5},
basemap=MaplibreBasemap(mode="interleaved"),
)
m.set_view_state(latitude=45)
assert m.view_state == GlobeViewState(longitude=-100, latitude=45, zoom=5)
Expand All @@ -147,3 +158,27 @@ def test_set_view_state_orbit():
)
m.set_view_state(new_view_state)
assert m.view_state == new_view_state


def test_map_view_validate_globe_view_basemap():
with pytest.raises(
TraitError,
match=r"GlobeView requires the basemap mode to be 'interleaved'.",
):
Map([], view=GlobeView(), basemap=MaplibreBasemap(mode="overlaid"))

# Start with interleaved then try to set overlaid
m = Map([], view=GlobeView(), basemap=MaplibreBasemap(mode="interleaved"))
with pytest.raises(
TraitError,
match=r"GlobeView requires the basemap mode to be 'interleaved'.",
):
m.basemap = MaplibreBasemap(mode="overlaid")

# Start with overlaid then try to set to GlobeView
m = Map([], basemap=MaplibreBasemap(mode="overlaid"))
with pytest.raises(
TraitError,
match=r"GlobeView requires the basemap mode to be 'interleaved'.",
):
m.view = GlobeView()