Skip to content

Commit b8f2bd9

Browse files
authored
feat: Support two render modes: Standard/deck.gl-first and MapboxOverlay (#921)
As described in the [upstream deck.gl docs](https://deck.gl/docs/developer-guide/base-maps/using-with-maplibre), there are three ways to support using deck.gl with Maplibre: _interleaved_, _overlaid_, and _reverse-controlled_. The first two are supported via `MapboxOverlay` with a prop `interleaved: true|false`, while the latter is implemented by having Maplibre be a child of the deck.gl Map. There are worthwhile reasons to support all of these modes. We need to use interleaved or overlaid to support globe view, while reverse-controlled better supports multiple deck.gl views. This PR refactors the map component in `index.tsx` into two separate React components: a deck.gl-first renderer (i.e. "reverse-controlled") and a MapboxOverlay renderer. The idea is that this will pair with #908 to give users more control over various ways of rendering maps. This is backwards-compatible because we default to reverse-controlled, the existing default. Closes #890, closes #437, for #886, for #718
1 parent a42faf3 commit b8f2bd9

File tree

6 files changed

+161
-56
lines changed

6 files changed

+161
-56
lines changed

lonboard/_map.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None:
179179
Indicates if a click handler has been registered.
180180
"""
181181

182+
render_mode = t.Unicode(default_value="deck-first").tag(sync=True)
183+
182184
height = HeightTrait().tag(sync=True)
183185
"""Height of the map in pixels, or valid CSS height property.
184186

src/index.tsx

Lines changed: 50 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import * as React from "react";
22
import { useEffect, useCallback, useState, useRef } from "react";
33
import { createRender, useModelState, useModel } from "@anywidget/react";
44
import type { Initialize, Render } from "@anywidget/types";
5-
import Map from "react-map-gl/maplibre";
6-
import DeckGL from "@deck.gl/react";
75
import { MapViewState, PickingInfo, type Layer } from "@deck.gl/core";
86
import { BaseLayerModel, initializeLayer } from "./model/index.js";
9-
import type { WidgetModel } from "@jupyter-widgets/base";
7+
import type { IWidgetManager, WidgetModel } from "@jupyter-widgets/base";
108
import { initParquetWasm } from "./parquet.js";
119
import { isDefined, loadChildModels } from "./util.js";
1210
import { v4 as uuidv4 } from "uuid";
@@ -25,6 +23,9 @@ import throttle from "lodash.throttle";
2523
import SidePanel from "./sidepanel/index";
2624
import { getTooltip } from "./tooltip/index.js";
2725
import { DeckGLRef } from "@deck.gl/react";
26+
import OverlayRenderer from "./renderers/overlay.js";
27+
import { MapRendererProps } from "./renderers/types.js";
28+
import DeckFirstRenderer from "./renderers/deck-first.js";
2829

2930
await initParquetWasm();
3031

@@ -116,6 +117,7 @@ function App() {
116117
);
117118
const [parameters] = useModelState<object>("parameters");
118119
const [customAttribution] = useModelState<string>("custom_attribution");
120+
const [renderMode] = useModelState<string>("render_mode");
119121

120122
// initialViewState is the value of view_state on the Python side. This is
121123
// called `initial` here because it gets passed in to deck's
@@ -156,7 +158,7 @@ function App() {
156158
const loadAndUpdateLayers = async () => {
157159
try {
158160
const childModels = await loadChildModels(
159-
model.widget_manager,
161+
model.widget_manager as IWidgetManager,
160162
childLayerIds,
161163
);
162164

@@ -229,6 +231,45 @@ function App() {
229231
[isOnMapHoverEventEnabled, justClicked],
230232
);
231233

234+
const mapRenderProps: MapRendererProps = {
235+
mapStyle: mapStyle || DEFAULT_MAP_STYLE,
236+
customAttribution,
237+
deckRef,
238+
initialViewState: ["longitude", "latitude", "zoom"].every((key) =>
239+
Object.keys(initialViewState).includes(key),
240+
)
241+
? initialViewState
242+
: DEFAULT_INITIAL_VIEW_STATE,
243+
layers: bboxSelectPolygonLayer
244+
? layers.concat(bboxSelectPolygonLayer)
245+
: layers,
246+
getTooltip: (showTooltip && getTooltip) || undefined,
247+
getCursor: () => (isDrawingBBoxSelection ? "crosshair" : "grab"),
248+
pickingRadius: pickingRadius,
249+
onClick: onMapClickHandler,
250+
onHover: onMapHoverHandler,
251+
// @ts-expect-error useDevicePixels should allow number
252+
// https://github.com/visgl/deck.gl/pull/9826
253+
useDevicePixels: isDefined(useDevicePixels) ? useDevicePixels : true,
254+
onViewStateChange: (event) => {
255+
const { viewState } = event;
256+
257+
// This condition is necessary to confirm that the viewState is
258+
// of type MapViewState.
259+
if ("latitude" in viewState) {
260+
const { longitude, latitude, zoom, pitch, bearing } = viewState;
261+
setViewState({
262+
longitude,
263+
latitude,
264+
zoom,
265+
pitch,
266+
bearing,
267+
});
268+
}
269+
},
270+
parameters: parameters || {},
271+
};
272+
232273
return (
233274
<div
234275
className="lonboard"
@@ -252,58 +293,11 @@ function App() {
252293
/>
253294
)}
254295
<div className="bg-red-800 h-full w-full relative">
255-
<DeckGL
256-
ref={deckRef}
257-
style={{ width: "100%", height: "100%" }}
258-
initialViewState={
259-
["longitude", "latitude", "zoom"].every((key) =>
260-
Object.keys(initialViewState).includes(key),
261-
)
262-
? initialViewState
263-
: DEFAULT_INITIAL_VIEW_STATE
264-
}
265-
controller={true}
266-
layers={
267-
bboxSelectPolygonLayer
268-
? layers.concat(bboxSelectPolygonLayer)
269-
: layers
270-
}
271-
getTooltip={(showTooltip && getTooltip) || undefined}
272-
getCursor={() => (isDrawingBBoxSelection ? "crosshair" : "grab")}
273-
pickingRadius={pickingRadius}
274-
onClick={onMapClickHandler}
275-
onHover={onMapHoverHandler}
276-
useDevicePixels={
277-
isDefined(useDevicePixels) ? useDevicePixels : true
278-
}
279-
// https://deck.gl/docs/api-reference/core/deck#_typedarraymanagerprops
280-
_typedArrayManagerProps={{
281-
overAlloc: 1,
282-
poolSize: 0,
283-
}}
284-
onViewStateChange={(event) => {
285-
const { viewState } = event;
286-
287-
// This condition is necessary to confirm that the viewState is
288-
// of type MapViewState.
289-
if ("latitude" in viewState) {
290-
const { longitude, latitude, zoom, pitch, bearing } = viewState;
291-
setViewState({
292-
longitude,
293-
latitude,
294-
zoom,
295-
pitch,
296-
bearing,
297-
});
298-
}
299-
}}
300-
parameters={parameters || {}}
301-
>
302-
<Map
303-
mapStyle={mapStyle || DEFAULT_MAP_STYLE}
304-
customAttribution={customAttribution}
305-
></Map>
306-
</DeckGL>
296+
{renderMode === "overlay" ? (
297+
<OverlayRenderer {...mapRenderProps} />
298+
) : (
299+
<DeckFirstRenderer {...mapRenderProps} />
300+
)}
307301
</div>
308302
</div>
309303
</div>

src/renderers/deck-first.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import DeckGL from "@deck.gl/react";
2+
import React from "react";
3+
import Map from "react-map-gl/maplibre";
4+
import type { MapRendererProps } from "./types";
5+
6+
/**
7+
* DeckFirst renderer: DeckGL wraps Map component
8+
*
9+
* In this rendering mode, deck.gl is the parent component that manages
10+
* the canvas and view state, with the map rendered as a child component.
11+
* This is the traditional approach where deck.gl has full control over
12+
* the rendering pipeline.
13+
*/
14+
const DeckFirstRenderer: React.FC<MapRendererProps> = (mapProps) => {
15+
// Remove maplibre-specific props before passing to DeckGL
16+
const { mapStyle, customAttribution, deckRef, ...deckProps } = mapProps;
17+
return (
18+
<DeckGL
19+
ref={deckRef}
20+
style={{ width: "100%", height: "100%" }}
21+
controller={true}
22+
// https://deck.gl/docs/api-reference/core/deck#_typedarraymanagerprops
23+
_typedArrayManagerProps={{
24+
overAlloc: 1,
25+
poolSize: 0,
26+
}}
27+
{...deckProps}
28+
>
29+
<Map mapStyle={mapStyle} customAttribution={customAttribution}></Map>
30+
</DeckGL>
31+
);
32+
};
33+
34+
export default DeckFirstRenderer;

src/renderers/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default as DeckFirst } from "./deck-first";
2+
export { default as Overlay } from "./overlay";
3+
export type { MapRendererProps } from "./types";

src/renderers/overlay.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from "react";
2+
import Map, { useControl } from "react-map-gl/maplibre";
3+
import { MapboxOverlay, MapboxOverlayProps } from "@deck.gl/mapbox";
4+
import type { MapRendererProps } from "./types";
5+
6+
/**
7+
* DeckGLOverlay component that integrates deck.gl with react-map-gl
8+
*
9+
* Uses the useControl hook to create a MapboxOverlay instance that
10+
* renders deck.gl layers on top of the base map.
11+
*/
12+
function DeckGLOverlay(props: MapboxOverlayProps) {
13+
const overlay = useControl(() => new MapboxOverlay(props));
14+
overlay.setProps(props);
15+
return null;
16+
}
17+
18+
/**
19+
* Overlay renderer: Map wraps DeckGLOverlay component
20+
*
21+
* In this rendering mode, the map is the parent component that controls
22+
* the view state, with deck.gl layers rendered as an overlay using the
23+
* MapboxOverlay. This approach gives the base map more control and can
24+
* enable features like interleaved rendering between map and deck layers.
25+
*/
26+
const OverlayRenderer: React.FC<MapRendererProps> = (mapProps) => {
27+
// Remove maplibre-specific props before passing to DeckGL
28+
const { mapStyle, customAttribution, initialViewState, ...deckProps } =
29+
mapProps;
30+
return (
31+
<Map
32+
reuseMaps
33+
initialViewState={initialViewState}
34+
mapStyle={mapStyle}
35+
attributionControl={{ customAttribution }}
36+
style={{ width: "100%", height: "100%" }}
37+
>
38+
<DeckGLOverlay
39+
// https://deck.gl/docs/api-reference/core/deck#_typedarraymanagerprops
40+
_typedArrayManagerProps={{
41+
overAlloc: 1,
42+
poolSize: 0,
43+
}}
44+
{...deckProps}
45+
/>
46+
</Map>
47+
);
48+
};
49+
50+
export default OverlayRenderer;

src/renderers/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { DeckProps, View } from "@deck.gl/core";
2+
import type { DeckGLRef } from "@deck.gl/react";
3+
import type { RefObject } from "react";
4+
5+
type ViewOrViews = View | View[] | null;
6+
export type MapRendererProps<ViewsT extends ViewOrViews = null> = Pick<
7+
DeckProps<ViewsT>,
8+
| "getCursor"
9+
| "getTooltip"
10+
| "initialViewState"
11+
| "layers"
12+
| "onClick"
13+
| "onHover"
14+
| "onViewStateChange"
15+
| "parameters"
16+
| "pickingRadius"
17+
| "useDevicePixels"
18+
> & {
19+
mapStyle: string;
20+
customAttribution: string;
21+
deckRef?: RefObject<DeckGLRef | null>;
22+
};

0 commit comments

Comments
 (0)