Skip to content

Commit b2e6ae1

Browse files
authored
feat: Support globe view (#908)
### Change list - Ensure that the default value of `Map.basemap` is `MaplibreBasemap` if no value of `basemap` was passed. Add a test for this. - Define various View classes to map to deck.gl views. - Define View models on the TS side. - When a GlobeView is passed, pass `projection="globe"` to Maplibre - Render a dark background on the canvas when in globe view - Add `views` parameter to `Map`. For now this only supports a single view instance, but hopefully in the future it'll support more. This PR also sets the stage for non-geospatial map rendering and for multi-view support. ### todo: - [ ] Should we name the Map attribute `view` instead of `views`? Hopefully soon we'll support multi-views, though maybe not with a maplibre basemap. (#965 ) - [ ] View state validation (#948) - [ ] add something like `projection="globe"` to `viz` (#949) With this PR, to generate a map with globe view, someone would call ```py from lonboard import Map from lonboard.view import GlobeView from lonboard.basemap import MaplibreBasemap lonboard.Map( layer, views=GlobeView(), basemap=MaplibreBasemap(mode="interleaved"), ) ``` ## Globe view: <img width="787" height="685" alt="image" src="https://github.com/user-attachments/assets/7a22fafd-d48c-4cc3-b5f0-8d4393dcb035" /> Closes #886 (globe view), closes #375 (orthographic view) (though might need more work to allow non-spatial data) Relates to - #905 (multi-views), - #718 (multi-view), - #658 (repeating map view), - #637 (multiple linked plots), - #494 (basemap refactor), - #404 (swipe between two maps), - #375 (orthographic view)
1 parent a419424 commit b2e6ae1

File tree

12 files changed

+643
-24
lines changed

12 files changed

+643
-24
lines changed

eslint.config.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import globals from "globals";
21
import pluginJs from "@eslint/js";
3-
import tseslint from "typescript-eslint";
4-
import pluginReact from "eslint-plugin-react";
52
import eslintConfigPrettier from "eslint-config-prettier";
63
import pluginImport from "eslint-plugin-import";
4+
import pluginReact from "eslint-plugin-react";
5+
import globals from "globals";
6+
import tseslint from "typescript-eslint";
77

88
export default [
99
{ files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] },

lonboard/_map.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
VariableLengthTuple,
2121
ViewStateTrait,
2222
)
23+
from lonboard.view import BaseView
2324

2425
if TYPE_CHECKING:
2526
import sys
@@ -151,6 +152,8 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
151152
_esm = bundler_output_dir / "index.js"
152153
_css = bundler_output_dir / "index.css"
153154

155+
# TODO: change this view state to allow non-map view states if we have non-map views
156+
# Also allow a list/tuple of view states for multiple views
154157
view_state = ViewStateTrait()
155158
"""
156159
The view state of the map.
@@ -174,6 +177,7 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
174177
once it's been initially rendered.
175178
176179
"""
180+
177181
_has_click_handlers = t.Bool(default_value=False, allow_none=False).tag(sync=True)
178182
"""
179183
Indicates if a click handler has been registered.
@@ -192,6 +196,15 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
192196
"""One or more `Layer` objects to display on this map.
193197
"""
194198

199+
views: t.Instance[BaseView | None] = t.Instance(BaseView, allow_none=True).tag(
200+
sync=True,
201+
**ipywidgets.widget_serialization,
202+
)
203+
"""A View instance.
204+
205+
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.
206+
"""
207+
195208
show_tooltip = t.Bool(default_value=False).tag(sync=True)
196209
"""
197210
Whether to render a tooltip on hover on the map.

lonboard/traits.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from traitlets.utils.sentinel import Sentinel
4848

4949
from lonboard._layer import BaseArrowLayer
50+
from lonboard._map import Map
5051

5152
DEFAULT_INITIAL_VIEW_STATE = {
5253
"latitude": 10,
@@ -943,7 +944,7 @@ def __init__(
943944

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

946-
def validate(self, obj: Any, value: Any) -> None | ViewState:
947+
def validate(self, obj: Map, value: Any) -> None | ViewState:
947948
if value is None:
948949
return None
949950

lonboard/types/map.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
if TYPE_CHECKING:
1212
from lonboard.basemap import MaplibreBasemap
13+
from lonboard.view import BaseView
1314

1415

1516
class MapKwargs(TypedDict, total=False):
@@ -22,4 +23,5 @@ class MapKwargs(TypedDict, total=False):
2223
show_tooltip: bool
2324
show_side_panel: bool
2425
use_device_pixels: int | float | bool
26+
views: BaseView | list[BaseView] | tuple[BaseView, ...]
2527
view_state: dict[str, Any]

lonboard/view.py

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import traitlets as t
2+
3+
from lonboard._base import BaseWidget
4+
5+
6+
class BaseView(BaseWidget):
7+
"""A deck.gl View.
8+
9+
The `View` class and its subclasses are used to specify where and how your deck.gl layers should be rendered. Applications typically instantiate at least one `View` subclass.
10+
11+
"""
12+
13+
x = t.Union([t.Int(), t.Unicode()], allow_none=True, default_value=None).tag(
14+
sync=True,
15+
)
16+
"""The x position of the view.
17+
18+
A relative (e.g. `'50%'`) or absolute position. Default `0`.
19+
"""
20+
21+
y = t.Union([t.Int(), t.Unicode()], allow_none=True, default_value=None).tag(
22+
sync=True,
23+
)
24+
"""The y position of the view.
25+
26+
A relative (e.g. `'50%'`) or absolute position. Default `0`.
27+
"""
28+
29+
width = t.Union([t.Int(), t.Unicode()], allow_none=True, default_value=None).tag(
30+
sync=True,
31+
)
32+
"""The width of the view.
33+
34+
A relative (e.g. `'50%'`) or absolute extent. Default `'100%'`.
35+
"""
36+
37+
height = t.Union([t.Int(), t.Unicode()], allow_none=True, default_value=None).tag(
38+
sync=True,
39+
)
40+
"""The height of the view.
41+
42+
A relative (e.g. `'50%'`) or absolute extent. Default `'100%'`.
43+
"""
44+
45+
46+
class FirstPersonView(BaseView):
47+
"""A deck.gl FirstPersonView.
48+
49+
The `FirstPersonView` class is a subclass of `View` that describes a camera placed at a provided location, looking towards the direction and orientation specified by viewState. The behavior is similar to that of a first-person game.
50+
"""
51+
52+
_view_type = t.Unicode("first-person-view").tag(sync=True)
53+
54+
projection_matrix = t.List(
55+
t.Float(),
56+
allow_none=True,
57+
default_value=None,
58+
minlen=16,
59+
maxlen=16,
60+
).tag(
61+
sync=True,
62+
)
63+
"""Projection matrix.
64+
65+
If `projectionMatrix` is not supplied, the `View` class will build a projection matrix from the following parameters:
66+
"""
67+
68+
fovy = t.Float(allow_none=True, default_value=None).tag(sync=True)
69+
"""Field of view covered by camera, in the perspective case. In degrees.
70+
71+
Default `50`.
72+
"""
73+
74+
near = t.Float(allow_none=True, default_value=None).tag(sync=True)
75+
"""Distance of near clipping plane.
76+
77+
Default `0.1`.
78+
"""
79+
80+
far = t.Float(allow_none=True, default_value=None).tag(sync=True)
81+
"""Distance of far clipping plane.
82+
83+
Default `1000`.
84+
"""
85+
86+
focal_distance = t.Float(allow_none=True, default_value=None).tag(sync=True)
87+
"""Modifier of viewport scale.
88+
89+
Corresponds to the number of pixels per meter. Default `1`.
90+
"""
91+
92+
93+
class GlobeView(BaseView):
94+
"""A deck.gl GlobeView.
95+
96+
The `GlobeView` class is a subclass of `View`. This view projects the earth into a 3D globe.
97+
"""
98+
99+
_view_type = t.Unicode("globe-view").tag(sync=True)
100+
101+
resolution = t.Float(allow_none=True, default_value=None).tag(sync=True)
102+
"""The resolution at which to turn flat features into 3D meshes, in degrees.
103+
104+
Smaller numbers will generate more detailed mesh. Default `10`.
105+
"""
106+
107+
near_z_multiplier = t.Float(allow_none=True, default_value=None).tag(sync=True)
108+
"""Scaler for the near plane, 1 unit equals to the height of the viewport.
109+
110+
Default to `0.1`. Overwrites the `near` parameter.
111+
"""
112+
113+
far_z_multiplier = t.Float(allow_none=True, default_value=None).tag(sync=True)
114+
"""Scaler for the far plane, 1 unit equals to the distance from the camera to the top edge of the screen.
115+
116+
Default to `2`. Overwrites the `far` parameter.
117+
"""
118+
119+
120+
class MapView(BaseView):
121+
"""A deck.gl MapView.
122+
123+
The `MapView` class is a subclass of `View`. This viewport creates a camera that looks at a geospatial location on a map from a certain direction. The behavior of `MapView` is generally modeled after that of Mapbox GL JS.
124+
"""
125+
126+
_view_type = t.Unicode("map-view").tag(sync=True)
127+
128+
repeat = t.Bool(allow_none=True, default_value=None).tag(sync=True)
129+
"""
130+
Whether to render multiple copies of the map at low zoom levels. Default `false`.
131+
"""
132+
133+
near_z_multiplier = t.Float(allow_none=True, default_value=None).tag(sync=True)
134+
"""Scaler for the near plane, 1 unit equals to the height of the viewport.
135+
136+
Default to `0.1`. Overwrites the `near` parameter.
137+
"""
138+
139+
far_z_multiplier = t.Float(allow_none=True, default_value=None).tag(sync=True)
140+
"""Scaler for the far plane, 1 unit equals to the distance from the camera to the top edge of the screen.
141+
142+
Default to `1.01`. Overwrites the `far` parameter.
143+
"""
144+
145+
projection_matrix = t.List(
146+
t.Float(),
147+
allow_none=True,
148+
default_value=None,
149+
minlen=16,
150+
maxlen=16,
151+
).tag(
152+
sync=True,
153+
)
154+
"""Projection matrix.
155+
156+
If `projectionMatrix` is not supplied, the `View` class will build a projection matrix from the following parameters:
157+
"""
158+
159+
fovy = t.Float(allow_none=True, default_value=None).tag(sync=True)
160+
"""Field of view covered by camera, in the perspective case. In degrees.
161+
162+
If not supplied, will be calculated from `altitude`.
163+
"""
164+
165+
altitude = t.Float(allow_none=True, default_value=None).tag(sync=True)
166+
"""Distance of the camera relative to viewport height.
167+
168+
Default `1.5`.
169+
"""
170+
171+
orthographic = t.Bool(allow_none=True, default_value=None).tag(sync=True)
172+
"""Whether to create an orthographic or perspective projection matrix.
173+
174+
Default is `false` (perspective projection).
175+
"""
176+
177+
178+
class OrbitView(BaseView):
179+
"""A deck.gl OrbitView.
180+
181+
The `OrbitView` class is a subclass of `View` that creates a 3D camera that rotates around a target position. It is usually used for the examination of a 3D scene in non-geospatial use cases.
182+
"""
183+
184+
_view_type = t.Unicode("orbit-view").tag(sync=True)
185+
186+
orbit_axis = t.Unicode(allow_none=True, default_value=None).tag(sync=True)
187+
"""Axis with 360 degrees rotating freedom, either `'Y'` or `'Z'`, default to `'Z'`."""
188+
189+
projection_matrix = t.List(
190+
t.Float(),
191+
allow_none=True,
192+
default_value=None,
193+
minlen=16,
194+
maxlen=16,
195+
).tag(
196+
sync=True,
197+
)
198+
"""Projection matrix.
199+
200+
If `projectionMatrix` is not supplied, the `View` class will build a projection matrix from the following parameters:
201+
"""
202+
203+
fovy = t.Float(allow_none=True, default_value=None).tag(sync=True)
204+
"""Field of view covered by camera, in the perspective case. In degrees.
205+
206+
Default `50`.
207+
"""
208+
209+
near = t.Float(allow_none=True, default_value=None).tag(sync=True)
210+
"""Distance of near clipping plane.
211+
212+
Default `0.1`.
213+
"""
214+
215+
far = t.Float(allow_none=True, default_value=None).tag(sync=True)
216+
"""Distance of far clipping plane.
217+
218+
Default `1000`.
219+
"""
220+
221+
orthographic = t.Bool(allow_none=True, default_value=None).tag(sync=True)
222+
"""Whether to create an orthographic or perspective projection matrix.
223+
224+
Default is `false` (perspective projection).
225+
"""
226+
227+
228+
class OrthographicView(BaseView):
229+
"""A deck.gl OrthographicView.
230+
231+
The `OrthographicView` class is a subclass of `View` that creates a top-down view of the XY plane. It is usually used for rendering 2D charts in non-geospatial use cases.
232+
"""
233+
234+
_view_type = t.Unicode("orthographic-view").tag(sync=True)
235+
236+
flip_y = t.Bool(allow_none=True, default_value=None).tag(sync=True)
237+
"""
238+
Whether to use top-left coordinates (`true`) or bottom-left coordinates (`false`).
239+
240+
Default `true`.
241+
"""
242+
243+
near = t.Float(allow_none=True, default_value=None).tag(sync=True)
244+
"""Distance of near clipping plane.
245+
246+
Default `0.1`.
247+
"""
248+
249+
far = t.Float(allow_none=True, default_value=None).tag(sync=True)
250+
"""Distance of far clipping plane.
251+
252+
Default `1000`.
253+
"""

0 commit comments

Comments
 (0)