Skip to content

Commit

Permalink
[mapbox] add MapboxOverlay (#6738)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress committed Mar 24, 2022
1 parent 70d55e1 commit 94ade66
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 51 deletions.
2 changes: 1 addition & 1 deletion docs/api-reference/mapbox/mapbox-layer.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MapboxLayer

`MapboxLayer` is an implementation of [mapbox](https://www.npmjs.com/package/mapbox-gl)'s custom layer API. By adding a `MapboxLayer` instance to an mapbox map, one can render any deck.gl layer inside the mapbox canvas / WebGL context. This is in contrast to the traditional deck.gl/mapbox integration where the deck.gl layers are rendered into a separate canvas over the base map.
`MapboxLayer` is an implementation of [Mapbox GL JS](https://www.npmjs.com/package/mapbox-gl)'s [CustomLayerInterface](https://docs.mapbox.com/mapbox-gl-js/api/properties/#customlayerinterface) API. By adding a `MapboxLayer` instance to an mapbox map, one can render any deck.gl layer inside the mapbox canvas / WebGL context. This is in contrast to the traditional deck.gl/mapbox integration where the deck.gl layers are rendered into a separate canvas over the base map.

See [mapbox documentation](https://www.mapbox.com/mapbox-gl-js/api/#map#addlayer) for how to add a layer to an existing layer stack.

Expand Down
63 changes: 63 additions & 0 deletions docs/api-reference/mapbox/mapbox-overlay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# MapboxOverlay

`MapboxOverlay` is an implementation of [Mapbox GL JS](https://www.npmjs.com/package/mapbox-gl)'s [IControl](https://docs.mapbox.com/mapbox-gl-js/api/markers/#icontrol) API. When adding a `MapboxOverlay` instance to an mapbox map, a deck.gl canvas overlaid on top of the base map, and synchronized with the map's camera.

## Example

```js
import {MapboxOverlay} from '@deck.gl/mapbox';
import {ScatterplotLayer} from '@deck.gl/layers';

const map = new mapboxgl.Map({...});

const overlay = new MapboxOverlay({
layers: [
new ScatterplotLayer({
id: 'my-scatterplot',
data: [
{position: [-74.5, 40], size: 100}
],
getPosition: d => d.position,
getRadius: d => d.size,
getColor: [255, 0, 0]
})
]
});

map.addControl(overlay);
```

## Constructor

```js
import {MapboxOverlay} from '@deck.gl/mapbox';
new MapboxOverlay(props);
```

`MapboxOverlay` accepts the same props as the [Deck](/docs/api-reference/core/deck.md) class, with the following exceptions:

- `views` - multi-view is not supported. There is only one `MapView` that synchronizes with the base map.
- `viewState` - managed internally.
- `controller` - always disabled (to use Mapbox's interaction handlers).

## Methods

##### setProps

Updates (partial) props of the underlying `Deck` instance. See [Deck.setProps](/docs/api-reference/core/deck.md#setprops).

##### pickObject

See [Deck.pickObject](/docs/api-reference/core/deck.md#pickobject).

##### pickObjects

See [Deck.pickObjects](/docs/api-reference/core/deck.md#pickobjects).

##### pickMultipleObjects

See [Deck.pickMultipleObjects](/docs/api-reference/core/deck.md#pickmultipleobjects).

##### finalize

Removes the control and deletes all resources.
40 changes: 9 additions & 31 deletions examples/get-started/pure-js/mapbox/app.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,21 @@
import {Deck} from '@deck.gl/core';
import {MapboxOverlay as DeckOverlay} from '@deck.gl/mapbox';
import {GeoJsonLayer, ArcLayer} from '@deck.gl/layers';
import mapboxgl from 'mapbox-gl';

// source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz
const AIR_PORTS =
'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson';

const INITIAL_VIEW_STATE = {
latitude: 51.47,
longitude: 0.45,
const map = new mapboxgl.Map({
container: 'map',
style: 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json',
center: [0.45, 51.47],
zoom: 4,
bearing: 0,
pitch: 30
};

const MAP_STYLE = 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json';

const map = new mapboxgl.Map({
container: 'map',
style: MAP_STYLE,
// Note: deck.gl will be in charge of interaction and event handling
interactive: false,
center: [INITIAL_VIEW_STATE.longitude, INITIAL_VIEW_STATE.latitude],
zoom: INITIAL_VIEW_STATE.zoom,
bearing: INITIAL_VIEW_STATE.bearing,
pitch: INITIAL_VIEW_STATE.pitch
});

export const deck = new Deck({
canvas: 'deck-canvas',
width: '100%',
height: '100%',
initialViewState: INITIAL_VIEW_STATE,
controller: true,
onViewStateChange: ({viewState}) => {
map.jumpTo({
center: [viewState.longitude, viewState.latitude],
zoom: viewState.zoom,
bearing: viewState.bearing,
pitch: viewState.pitch
});
},
const deckOverlay = new DeckOverlay({
layers: [
new GeoJsonLayer({
id: 'airports',
Expand Down Expand Up @@ -71,3 +46,6 @@ export const deck = new Deck({
})
]
});

map.addControl(deckOverlay);
map.addControl(new mapboxgl.NavigationControl());
16 changes: 3 additions & 13 deletions examples/get-started/pure-js/mapbox/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,18 @@
<meta charset='UTF-8' />
<title>deck.gl example</title>
<style>
#container {
#map {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#container > * {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.4.0/mapbox-gl.css' rel='stylesheet' />
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.13.0/mapbox-gl.css' rel='stylesheet' />
</head>
<body>
<div id="container">
<div id="map"></div>
<canvas id="deck-canvas"></canvas>
</div>
<div id="map"></div>
<script src='app.js'></script>
</body>
</html>
3 changes: 3 additions & 0 deletions modules/mapbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"build-bundle": "webpack --config ../../scripts/bundle.config.js",
"prepublishOnly": "npm run build-bundle && npm run build-bundle -- --env.dev"
},
"dependencies": {
"@types/mapbox-gl": "^2.6.3"
},
"peerDependencies": {
"@deck.gl/core": "^8.0.0"
}
Expand Down
8 changes: 4 additions & 4 deletions modules/mapbox/src/deck-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,15 @@ export function drawLayer(deck, map, layer) {
});
}

function getViewState(map) {
export function getViewState(map) {
const {lng, lat} = map.getCenter();
return {
longitude: lng,
latitude: lat,
zoom: map.getZoom(),
bearing: map.getBearing(),
pitch: map.getPitch()
pitch: map.getPitch(),
repeat: map.getRenderWorldCopies()
};
}

Expand All @@ -134,8 +135,7 @@ function getViewport(deck, map, useMapboxProjection = true) {
x: 0,
y: 0,
width: deck.width,
height: deck.height,
repeat: map.getRenderWorldCopies()
height: deck.height
},
getViewState(map),
useMapboxProjection
Expand Down
1 change: 1 addition & 0 deletions modules/mapbox/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export {default as MapboxLayer} from './mapbox-layer';
export {default as MapboxOverlay} from './mapbox-overlay';
175 changes: 175 additions & 0 deletions modules/mapbox/src/mapbox-overlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import {Deck} from '@deck.gl/core';
import {getViewState} from './deck-utils';

import type {Map, IControl, MapMouseEvent} from 'mapbox-gl';

/**
* Implements Mapbox [IControl](https://docs.mapbox.com/mapbox-gl-js/api/markers/#icontrol) interface
* Renders deck.gl layers over the base map and automatically synchronizes with the map's camera
*/
export default class MapboxOverlay implements IControl {
private _props: any;
private _deck: Deck;
private _map?: Map;
private _container?: HTMLDivElement;

constructor(props) {
this._props = {...props};
}

/** Update (partial) props of the underlying Deck instance. */
setProps(props: any): void {
Object.assign(this._props, props);

if ('viewState' in this._props) {
delete this._props.viewState;
}

if (this._deck) {
this._deck.setProps(this._props);
}
}

/** Called when the control is added to a map */
onAdd(map: Map) {
this._map = map;

/* global document */
const container = document.createElement('div');
Object.assign(container.style, {
position: 'absolute',
left: 0,
top: 0,
pointerEvents: 'none'
});
this._container = container;

this._deck = new Deck({
...this._props,
parent: container,
viewState: getViewState(map)
});

map.on('resize', this._updateContainerSize);
map.on('render', this._updateViewState);
map.on('mousemove', this._handleMouseEvent);
map.on('mouseout', this._handleMouseEvent);
map.on('click', this._handleMouseEvent);
map.on('dblclick', this._handleMouseEvent);

this._updateContainerSize();
return container;
}

/** Called when the control is removed from a map */
onRemove() {
const map = this._map;

if (map) {
map.off('resize', this._updateContainerSize);
map.off('render', this._updateViewState);
map.off('mousemove', this._handleMouseEvent);
map.off('mouseout', this._handleMouseEvent);
map.off('click', this._handleMouseEvent);
map.off('dblclick', this._handleMouseEvent);
}

this._deck?.finalize();
this._map = undefined;
this._container = undefined;
}

getDefaultPosition() {
return 'top-left';
}

/** Forwards the Deck.pickObject method */
pickObject(params) {
return this._deck && this._deck.pickObject(params);
}

/** Forwards the Deck.pickMultipleObjects method */
pickMultipleObjects(params) {
return this._deck && this._deck.pickMultipleObjects(params);
}

/** Forwards the Deck.pickObjects method */
pickObjects(params) {
return this._deck && this._deck.pickObjects(params);
}

/** Remove from map and releases all resources */
finalize() {
if (this._map) {
this._map.removeControl(this);
}
}

_updateContainerSize = () => {
if (this._map && this._container) {
const {clientWidth, clientHeight} = this._map.getContainer();
Object.assign(this._container.style, {
width: `${clientWidth}px`,
height: `${clientHeight}px`
});
}
};

_updateViewState = () => {
const deck = this._deck;
if (deck) {
deck.setProps({viewState: getViewState(this._map)});
// Redraw immediately if view state has changed
deck.redraw(false);
}
};

_handleMouseEvent = (event: MapMouseEvent) => {
const deck = this._deck;
if (!deck) {
return;
}

const mockEvent: {
type: string;
offsetCenter: {x: number; y: number};
srcEvent: MapMouseEvent;
tapCount?: number;
} = {
type: event.type,
offsetCenter: event.point,
srcEvent: event
};

switch (event.type) {
case 'click':
// Hack: because we do not listen to pointer down, perform picking now
deck._lastPointerDownInfo = deck.pickObject({
...mockEvent.offsetCenter,
radius: deck.props.pickingRadius
});
mockEvent.tapCount = 1;
deck._onEvent(mockEvent);
break;

case 'dblclick':
mockEvent.type = 'click';
mockEvent.tapCount = 2;
deck._onEvent(mockEvent);
break;

case 'mousemove':
mockEvent.type = 'pointermove';
deck._onPointerMove(mockEvent);
break;

case 'mouseout':
mockEvent.type = 'pointerleave';
deck._onPointerMove(mockEvent);
break;

default:
return;
}
};
}
3 changes: 2 additions & 1 deletion test/modules/mapbox/mapbox-layer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ test('MapboxLayer#onAdd, onRemove, setProps', t => {
latitude: 37.78,
zoom: 12,
bearing: 0,
pitch: 0
pitch: 0,
repeat: true
},
'viewState is set correctly'
);
Expand Down
Loading

0 comments on commit 94ade66

Please sign in to comment.