diff --git a/docs/api-reference/mapbox/mapbox-layer.md b/docs/api-reference/mapbox/mapbox-layer.md index 5f3c2ff91ab..c75a6fde79b 100644 --- a/docs/api-reference/mapbox/mapbox-layer.md +++ b/docs/api-reference/mapbox/mapbox-layer.md @@ -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. diff --git a/docs/api-reference/mapbox/mapbox-overlay.md b/docs/api-reference/mapbox/mapbox-overlay.md new file mode 100644 index 00000000000..6da4f7bba8a --- /dev/null +++ b/docs/api-reference/mapbox/mapbox-overlay.md @@ -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. diff --git a/examples/get-started/pure-js/mapbox/app.js b/examples/get-started/pure-js/mapbox/app.js index 2ece91ade49..743c6b296b4 100644 --- a/examples/get-started/pure-js/mapbox/app.js +++ b/examples/get-started/pure-js/mapbox/app.js @@ -1,4 +1,4 @@ -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'; @@ -6,41 +6,16 @@ import mapboxgl from 'mapbox-gl'; 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', @@ -71,3 +46,6 @@ export const deck = new Deck({ }) ] }); + +map.addControl(deckOverlay); +map.addControl(new mapboxgl.NavigationControl()); diff --git a/examples/get-started/pure-js/mapbox/index.html b/examples/get-started/pure-js/mapbox/index.html index 71054d1f85e..b35787c9cc8 100644 --- a/examples/get-started/pure-js/mapbox/index.html +++ b/examples/get-started/pure-js/mapbox/index.html @@ -4,28 +4,18 @@ deck.gl example - + -
-
- -
+
diff --git a/modules/mapbox/package.json b/modules/mapbox/package.json index d671156edbd..d28ad37473b 100644 --- a/modules/mapbox/package.json +++ b/modules/mapbox/package.json @@ -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" } diff --git a/modules/mapbox/src/deck-utils.js b/modules/mapbox/src/deck-utils.js index 7b7a7f6fadc..e98f393d12b 100644 --- a/modules/mapbox/src/deck-utils.js +++ b/modules/mapbox/src/deck-utils.js @@ -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() }; } @@ -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 diff --git a/modules/mapbox/src/index.ts b/modules/mapbox/src/index.ts index 20039842eea..12058f71821 100644 --- a/modules/mapbox/src/index.ts +++ b/modules/mapbox/src/index.ts @@ -1 +1,2 @@ export {default as MapboxLayer} from './mapbox-layer'; +export {default as MapboxOverlay} from './mapbox-overlay'; diff --git a/modules/mapbox/src/mapbox-overlay.ts b/modules/mapbox/src/mapbox-overlay.ts new file mode 100644 index 00000000000..eeac0835b77 --- /dev/null +++ b/modules/mapbox/src/mapbox-overlay.ts @@ -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; + } + }; +} diff --git a/test/modules/mapbox/mapbox-layer.spec.js b/test/modules/mapbox/mapbox-layer.spec.js index a15cc2bf617..0b0cfdcc11d 100644 --- a/test/modules/mapbox/mapbox-layer.spec.js +++ b/test/modules/mapbox/mapbox-layer.spec.js @@ -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' ); diff --git a/yarn.lock b/yarn.lock index b20e744a5c9..2f08e0d9c73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2938,7 +2938,7 @@ resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== -"@types/geojson@^7946.0.7": +"@types/geojson@*", "@types/geojson@^7946.0.7": version "7946.0.8" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca" integrity sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA== @@ -2979,6 +2979,13 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.155.tgz#e2b4514f46a261fd11542e47519c20ebce7bc23a" integrity sha512-vEcX7S7aPhsBCivxMwAANQburHBtfN9RdyXFk84IJmu2Z4Hkg1tOFgaslRiEqqvoLtbCBi6ika1EMspE+NZ9Lg== +"@types/mapbox-gl@^2.6.3": + version "2.6.3" + resolved "https://registry.yarnpkg.com/@types/mapbox-gl/-/mapbox-gl-2.6.3.tgz#09e4992bb540fe5e024eebc5fbc315317cb13ffe" + integrity sha512-oF5eQmczkoPQfxfRSwpF9GcrWi3YleptJ67uiCQKps+7aKxwIbww0EHHqIrxvOg49l07+AZBtJU2FPKZm1jKAg== + dependencies: + "@types/geojson" "*" + "@types/mdast@^3.0.0": version "3.0.10" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af"