Skip to content
Draft
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
68 changes: 40 additions & 28 deletions lonboard/_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
from lonboard._html_export import map_to_html
from lonboard._layer import BaseLayer
from lonboard._viewport import compute_view
from lonboard.basemap import CartoBasemap
from lonboard.basemap import MaplibreBasemap
from lonboard.traits import (
DEFAULT_INITIAL_VIEW_STATE,
BasemapUrl,
HeightTrait,
VariableLengthTuple,
ViewStateTrait,
)
from lonboard.view import BaseView

if TYPE_CHECKING:
import sys
Expand Down Expand Up @@ -133,6 +133,37 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
_esm = bundler_output_dir / "index.js"
_css = bundler_output_dir / "index.css"

_has_click_handlers = t.Bool(default_value=False, allow_none=False).tag(sync=True)
Copy link
Member Author

Choose a reason for hiding this comment

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

This diff is from moving view_state lower so that it's immediately after the new views attribute; nothing was changed except for adding views

"""
Indicates if a click handler has been registered.
"""

render_mode = t.Unicode(default_value="deck-first").tag(sync=True)

height = HeightTrait().tag(sync=True)
"""Height of the map in pixels, or valid CSS height property.

This API is not yet stabilized and may change in the future.
"""

layers = VariableLengthTuple(t.Instance(BaseLayer)).tag(
sync=True,
**ipywidgets.widget_serialization,
)
"""One or more `Layer` objects to display on this map.
"""

views = VariableLengthTuple(t.Instance(BaseView)).tag(
sync=True,
**ipywidgets.widget_serialization,
)
"""A single View instance, or an array of View instances.

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.
"""

# TODO: change this view state to allow non-map view states if we have non-map views
# Also allow a list/tuple of view states for multiple views
view_state = ViewStateTrait()
"""
The view state of the map.
Expand All @@ -156,25 +187,6 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
once it's been initially rendered.

"""
_has_click_handlers = t.Bool(default_value=False, allow_none=False).tag(sync=True)
"""
Indicates if a click handler has been registered.
"""

render_mode = t.Unicode(default_value="deck-first").tag(sync=True)

height = HeightTrait().tag(sync=True)
"""Height of the map in pixels, or valid CSS height property.

This API is not yet stabilized and may change in the future.
"""

layers = VariableLengthTuple(t.Instance(BaseLayer)).tag(
sync=True,
**ipywidgets.widget_serialization,
)
"""One or more `Layer` objects to display on this map.
"""

show_tooltip = t.Bool(default_value=False).tag(sync=True)
"""
Expand Down Expand Up @@ -203,15 +215,15 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
- Default: `5`
"""

basemap_style = BasemapUrl(CartoBasemap.PositronNoLabels)
"""
A URL to a MapLibre-compatible basemap style.
basemap = t.Instance(MaplibreBasemap, allow_none=True).tag(
Copy link
Member Author

@kylebarron kylebarron Oct 8, 2025

Choose a reason for hiding this comment

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

This allows users to customize the basemap, and to turn off the basemap altogether by passing None.

Before merging we may keep basemap_style around to not break backwards compatibility, but I think documenting that the new API is

Map(basemap=MaplibreBasemap(basemap_style=basemap_style))

isn't too bad

sync=True,
**ipywidgets.widget_serialization,
)
"""A basemap instance.

Various styles are provided in [`lonboard.basemap`](https://developmentseed.org/lonboard/latest/api/basemap/).
See [`lonboard.basemap.MaplibreBasemap`] for more information.

- Type: `str`, holding a URL hosting a basemap style.
- Default
[`lonboard.basemap.CartoBasemap.PositronNoLabels`][lonboard.basemap.CartoBasemap.PositronNoLabels]
Pass `None` to disable rendering a basemap.
"""

custom_attribution = t.Union(
Expand Down
54 changes: 54 additions & 0 deletions lonboard/basemap.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"""Basemap helpers."""

from enum import Enum
from typing import Literal

import traitlets as t

from lonboard._base import BaseWidget
from lonboard.traits import BasemapUrl


class CartoBasemap(str, Enum):
Expand Down Expand Up @@ -43,3 +49,51 @@ class CartoBasemap(str, Enum):
"https://basemaps.cartocdn.com/gl/voyager-nolabels-gl-style/style.json"
)
"""A light, colored map style without labels."""


class MaplibreBasemap(BaseWidget):
"""A MapLibre GL JS basemap."""

def __init__(
self,
*,
mode: Literal[
"interleaved",
"overlaid",
"reverse-controlled",
] = "reverse-controlled",
basemap_style: str | CartoBasemap,
Comment on lines +60 to +65
Copy link
Member Author

Choose a reason for hiding this comment

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

Here the user can explicitly choose a rendering mode between interleaved (mapbox overlay set to interleaved), overlaid (mapbox overlay not using interleaved), or reverse-controlled, the existing behavior, which doesn't use mapbox overlay

) -> None:
"""Create a MapLibre GL JS basemap."""
super().__init__(mode=mode, basemap_style=basemap_style) # type: ignore

mode = t.Unicode().tag(sync=True)
"""The basemap integration mode.

- **`"interleaved"`**:

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.

- **`"overlaid"`**:

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.

- **`"reverse-controlled"`**:

The reverse-controlled mode renders deck.gl above the MapLibre container and blocks any interaction to the base map.

If you need to have multiple views, you should use this option.

**Default**: `"reverse-controlled"`
"""

basemap_style = BasemapUrl(CartoBasemap.PositronNoLabels)
"""
A URL to a MapLibre-compatible basemap style.

Various styles are provided in [`lonboard.basemap`](https://developmentseed.org/lonboard/latest/api/basemap/).

- Type: `str`, holding a URL hosting a basemap style.
- Default
[`lonboard.basemap.CartoBasemap.PositronNoLabels`][lonboard.basemap.CartoBasemap.PositronNoLabels]
"""
3 changes: 2 additions & 1 deletion lonboard/traits.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from traitlets.utils.sentinel import Sentinel

from lonboard._layer import BaseArrowLayer
from lonboard._map import Map

DEFAULT_INITIAL_VIEW_STATE = {
"latitude": 10,
Expand Down Expand Up @@ -943,7 +944,7 @@ def __init__(

self.tag(sync=True, to_json=serialize_view_state)

def validate(self, obj: Any, value: Any) -> None | ViewState:
def validate(self, obj: Map, value: Any) -> None | ViewState:
if value is None:
return None

Expand Down
6 changes: 4 additions & 2 deletions lonboard/types/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@
from typing_extensions import TypedDict

if TYPE_CHECKING:
from lonboard.basemap import CartoBasemap
from lonboard.basemap import MaplibreBasemap
from lonboard.view import BaseView


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

height: int | str
basemap_style: str | CartoBasemap
basemap: MaplibreBasemap
parameters: dict[str, Any]
picking_radius: int
show_tooltip: bool
show_side_panel: bool
use_device_pixels: int | float | bool
views: BaseView | list[BaseView] | tuple[BaseView, ...]
view_state: dict[str, Any]
Loading
Loading