Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(mapbox) leave additional views intact #6329

Merged
merged 17 commits into from
Feb 22, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/api-reference/mapbox/mapbox-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,58 @@ map.on('load', () => {
}
```

### Using Multiple Views from an Existing Deck Instance

This option allows one to take advantage of deck's multi-view system and render a mapbox base map onto any one MapView of your choice by setting the `views` array and a `layerFilter` callback.

- To use multiple views, define a `MapView` with the id `“mapbox”`. This view will receive the state that matches the base map at each render.
- If views are provided but the array does not contain this id, then a `MapView({id: 'mapbox'})` will be inserted at the bottom of the stack.
- If the views prop is not provided, then the default is a single `MapView({id: 'mapbox'})`.

```js
import {MapboxLayer} from '@deck.gl/mapbox';
import {Deck, MapView, OrthographicView} from '@deck.gl/core';
import {ScatterplotLayer} from '@deck.gl/layers';

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

const deck = new Deck({
gl: map.painter.context.gl,
views: [new MapView({id: 'mapbox'}), new OrthographicView({id: 'widget'})],
layerFilter: ({layer, viewport}) => {
const shouldDrawInWidget = layer.id.startsWith('widget');
if (viewport.id === 'widget') return shouldDrawInWidget;
return !shouldDrawInWidget;
},
layers: [
new ScatterplotLayer({
id: 'my-scatterplot',
data: [
{position: [-74.5, 40], size: 100}
],
getPosition: d => d.position,
getRadius: d => d.size,
getFillColor: [255, 0, 0]
}),
new ScatterplotLayer({
id: 'widget-scatterplot',
data: [
{position: [0, 0], size: 100}
],
getPosition: d => d.position,
getRadius: d => d.size,
getFillColor: [255, 0, 0]
})
]
});

// wait for map to be ready
map.on('load', () => {
// add to mapbox
map.addLayer(new MapboxLayer({id: 'my-scatterplot', deck}));
});
```

## Constructor

```js
Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference/mapbox/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Use deck.gl layers as custom Mapbox layers, enabling seamless interleaving of Ma

### Limitations

* deck.gl's multi-view system cannot be used.
chrisgervang marked this conversation as resolved.
Show resolved Hide resolved
* When using deck.gl's multi-view system, only one of the views can match the base map and receive interaction.
* Unless used with react-map-gl, WebGL2 based deck.gl features, such as attribute transitions and GPU accelerated aggregation layers cannot be used.
* Mapbox 2.0's terrain feature is currently not supported.

Expand Down
24 changes: 19 additions & 5 deletions modules/mapbox/src/deck-utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Deck, WebMercatorViewport} from '@deck.gl/core';
import {Deck, WebMercatorViewport, MapView} from '@deck.gl/core';

export function getDeckInstance({map, gl, deck}) {
// Only create one deck instance per context
Expand Down Expand Up @@ -28,7 +28,8 @@ export function getDeckInstance({map, gl, deck}) {
userData: {
isExternal: false,
mapboxLayers: new Set()
}
},
views: (deck && deck.props.views) || [new MapView({id: 'mapbox'})]
};

if (deck) {
Expand Down Expand Up @@ -88,6 +89,7 @@ export function drawLayer(deck, map, layer) {
if (!deck.layerManager) {
return;
}

deck._drawLayers('mapbox-repaint', {
viewports: [currentViewport],
layerFilter: ({layer: deckLayer}) => layer.id === deckLayer.id,
Expand Down Expand Up @@ -122,6 +124,7 @@ function getViewport(deck, map, useMapboxProjection = true) {
return new WebMercatorViewport(
Object.assign(
{
id: 'mapbox',
x: 0,
y: 0,
width: deck.width,
Expand Down Expand Up @@ -154,10 +157,21 @@ function afterRender(deck, map) {
// Draw non-Mapbox layers
const mapboxLayerIds = Array.from(mapboxLayers, layer => layer.id);
const hasNonMapboxLayers = deck.props.layers.some(layer => !mapboxLayerIds.includes(layer.id));
if (hasNonMapboxLayers) {
let viewports = deck.getViewports();
const mapboxViewportIdx = viewports.findIndex(vp => vp.id === 'mapbox');
const hasNonMapboxViews = viewports.length > 1 || mapboxViewportIdx < 0;

if (hasNonMapboxLayers || hasNonMapboxViews) {
if (mapboxViewportIdx >= 0) {
viewports = viewports.slice();
viewports[mapboxViewportIdx] = getViewport(deck, map, false);
}

deck._drawLayers('mapbox-repaint', {
viewports: [getViewport(deck, map, false)],
layerFilter: ({layer}) => !mapboxLayerIds.includes(layer.id),
viewports,
layerFilter: params =>
(!deck.props.layerFilter || deck.props.layerFilter(params)) &&
(params.viewport.id !== 'mapbox' || !mapboxLayerIds.includes(params.layer.id)),
clearCanvas: false
});
}
Expand Down
8 changes: 3 additions & 5 deletions test/apps/mapbox-layers/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
Experimental integration with mapbox-layer API

You need to checkout the `custom-layers` branch in mapbox-gl-js and do a dev-build, and copy `mapbox-gl-dev.js` to this directory before running.
Integration with mapbox-layer API

```js
yarn start-local
```

This directory contains two independent examples:
- `app.js`: the pure-js example entry point.
- `react-app.js`: the React example entry point.
- `app.js`: the pure-js example entry point. URL: `/` (root)
- `react-app.js`: the React example entry point. URL: `/reactApp`
48 changes: 41 additions & 7 deletions test/apps/mapbox-layers/react-app.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
/* global document */
import React, {useState, useRef, useCallback} from 'react';
import {render} from 'react-dom';
import DeckGL, {ScatterplotLayer, ArcLayer} from 'deck.gl';
import DeckGL, {ScatterplotLayer, ArcLayer, TextLayer} from 'deck.gl';
import {StaticMap} from 'react-map-gl';

import {MapboxLayer} from '@deck.gl/mapbox';

import {mapboxBuildingLayer, deckPoiLayer, deckRouteLayer} from './layers';
import {MapView, OrthographicView} from '@deck.gl/core';

// Set your mapbox token here
const MAPBOX_TOKEN = process.env.MapboxAccessToken; // eslint-disable-line

const INITIAL_VIEW_STATE = {
longitude: -74.012,
latitude: 40.705,
zoom: 15.5,
bearing: -20,
pitch: 45
mapbox: {
longitude: -74.012,
latitude: 40.705,
zoom: 15.5,
bearing: -20,
pitch: 45
},
widget: {
target: [0, 0, 0],
zoom: 0
}
};

const mapboxView = new MapView({id: 'mapbox', controller: true});
const widgetView = new OrthographicView({
id: 'widget',
x: 20,
y: 20,
width: 150,
height: 36
});

function layerFilter({layer, viewport}) {
const shouldDrawInWidget = layer.id.startsWith('widget');
if (viewport.id === 'widget') return shouldDrawInWidget;
return !shouldDrawInWidget;
}

function getFirstTextLayerId(style) {
const layers = style.layers;
// Find the index of the first symbol (i.e. label) layer in the map style
Expand Down Expand Up @@ -47,16 +69,28 @@ function App() {
map.addLayer(new MapboxLayer({id: 'deckgl-tour-route', deck}));
}, []);

const layers = [new ScatterplotLayer(deckPoiLayer), new ArcLayer(deckRouteLayer)];
const layers = [
new ScatterplotLayer(deckPoiLayer),
new ArcLayer(deckRouteLayer),
new TextLayer({
id: 'widget-title',
data: [{position: [0, 0], text: 'New York City'}],
getSize: 18,
background: true,
backgroundPadding: [4, 4]
})
];

return (
<DeckGL
ref={deckRef}
layers={layers}
views={[mapboxView, widgetView]}
initialViewState={INITIAL_VIEW_STATE}
controller={true}
onWebGLInitialized={setGLContext}
glOptions={{stencil: true}}
layerFilter={layerFilter}
>
{glContext && (
<StaticMap
Expand Down
3 changes: 2 additions & 1 deletion test/apps/mapbox-layers/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const CONFIG = {
mode: 'development',

entry: {
app: resolve('./app.js')
app: resolve('./app.js'),
reactApp: resolve('./react-app.js')
},

module: {
Expand Down
124 changes: 123 additions & 1 deletion test/modules/mapbox/mapbox-layer.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test from 'tape-promise/tape';

import {Deck} from '@deck.gl/core';
import {Deck, MapView} from '@deck.gl/core';
import {ScatterplotLayer} from '@deck.gl/layers';
import {MapboxLayer} from '@deck.gl/mapbox';
import {gl} from '@deck.gl/test-utils';
Expand Down Expand Up @@ -41,6 +41,7 @@ class MockMapboxMap {
for (const id in this._layers) {
this._layers[id].render(gl);
}
this.emit('render');
}

getCenter() {
Expand All @@ -60,6 +61,17 @@ class MockMapboxMap {
}
}

class TestScatterplotLayer extends ScatterplotLayer {
draw(params) {
super.draw(params);
this.props.onAfterRedraw({
viewport: this.context.viewport,
layer: this
});
}
}
TestScatterplotLayer.layerName = 'TestScatterplotLayer';

test('MapboxLayer#onAdd, onRemove, setProps', t => {
const layers = Array.from(
{length: 2},
Expand Down Expand Up @@ -93,6 +105,7 @@ test('MapboxLayer#onAdd, onRemove, setProps', t => {
'Layer is added to deck'
);
t.deepEqual(deck.props.userData.mapboxVersion, {major: 1, minor: 10}, 'Mapbox version is parsed');
t.ok(deck.props.views[0].id === 'mapbox', 'mapbox view exists');

t.deepEqual(
deck.props.viewState,
Expand Down Expand Up @@ -164,6 +177,7 @@ test('MapboxLayer#external Deck', t => {
map.addLayer(layer);
t.is(layer.deck, deck, 'Used external Deck instance');
t.ok(deck.props.userData.mapboxVersion, 'Mapbox version is parsed');
t.ok(deck.props.views[0].id === 'mapbox', 'mapbox view exists');

map.emit('render');
t.pass('Map render does not throw');
Expand All @@ -182,3 +196,111 @@ test('MapboxLayer#external Deck', t => {
t.end();
};
});

test('MapboxLayer#external Deck multiple views supplied', t => {
const drawLog = [];
const onRedrawLayer = ({viewport, layer}) => {
drawLog.push([viewport.id, layer.id]);
};

const deck = new Deck({
gl,
views: [new MapView({id: 'view-two'}), new MapView({id: 'mapbox'})],
viewState: {
longitude: 0,
latitude: 0,
zoom: 1
},
layers: [
new TestScatterplotLayer({
id: 'scatterplot-map',
data: [],
getPosition: d => d.position,
getRadius: 10,
getFillColor: [255, 0, 0],
onAfterRedraw: onRedrawLayer
}),
new TestScatterplotLayer({
id: 'scatterplot-second-view',
data: [],
getPosition: d => d.position,
getRadius: 10,
getFillColor: [255, 0, 0],
onAfterRedraw: onRedrawLayer
})
],
layerFilter: ({viewport, layer}) => {
if (viewport.id === 'mapbox') return layer.id === 'scatterplot-map';
return layer.id === 'scatterplot-second-view';
}
});

const map = new MockMapboxMap({
center: {lng: -122.45, lat: 37.78},
zoom: 12
});
const layerDefaultView = new MapboxLayer({id: 'scatterplot-map', deck});
map.addLayer(layerDefaultView);

map.on('render', () => {
t.deepEqual(
drawLog,
[
['mapbox', 'scatterplot-map'],
['view-two', 'scatterplot-second-view']
],
'layers drawn into the correct views'
);

deck.finalize();

t.end();
});
});

test('MapboxLayer#external Deck custom views', t => {
const drawLog = [];
const onRedrawLayer = ({viewport, layer}) => {
drawLog.push([viewport.id, layer.id]);
};

const deck = new Deck({
gl,
views: [new MapView({id: 'view-two'})],
viewState: {
longitude: 0,
latitude: 0,
zoom: 1
},
layers: [
new TestScatterplotLayer({
id: 'scatterplot',
data: [],
getPosition: d => d.position,
getRadius: 10,
getFillColor: [255, 0, 0],
onAfterRedraw: onRedrawLayer
})
]
});

const map = new MockMapboxMap({
center: {lng: -122.45, lat: 37.78},
zoom: 12
});
map.addLayer(new MapboxLayer({id: 'scatterplot', deck}));
map.on('render', () => {
t.deepEqual(
drawLog,
[
['mapbox', 'scatterplot'],
['view-two', 'scatterplot']
],
'layer is drawn to both views'
);

deck.finalize();

t.end();
});
});