Skip to content

Commit

Permalink
feat: Support for custom mapbox layer
Browse files Browse the repository at this point in the history
Instead of using DeckGl as the container for Mapbox, we use Mapbox
as the container for DeckGl layer. This lets us insert the deckgl layer
between the mapbox layers, which is very handy when dealing with labels.

We use DeckGL's custom Mapbox layer here. Since DeckGL's custom layer
need method on the prototype, we can copy the layer props with spreading,
otherwise we lose the methods. This is why the `layer` prop is added
on the Layer so that we can pass the custom layer without any copying.
  • Loading branch information
ptbrowne committed May 2, 2022
1 parent b2bdb91 commit f846087
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 40 deletions.
54 changes: 31 additions & 23 deletions examples/deckgl-overlay/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,42 @@
import * as React from 'react';
import {render} from 'react-dom';
import DeckGL, {ArcLayer} from 'deck.gl';
import Map from 'react-map-gl';
import {ArcLayer} from 'deck.gl';
import {MapboxLayer} from '@deck.gl/mapbox';
import Map, {Layer, MapboxMap} from 'react-map-gl';
import ControlPanel from './control-panel';

const TOKEN = ''; // Set your mapbox token here

export default function App() {
const arcLayer = new ArcLayer({
data: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/bart-segments.json',
getSourcePosition: d => d.from.coordinates,
getTargetPosition: d => d.to.coordinates,
getSourceColor: [255, 200, 0],
getTargetColor: [0, 140, 255],
getWidth: 12
});
const [overLabels, setOverLabels] = React.useState(true);
const layer = React.useMemo(() => {
return new MapboxLayer({
id: 'arcs',
type: ArcLayer,
data: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/bart-segments.json',
getSourcePosition: d => d.from.coordinates,
getTargetPosition: d => d.to.coordinates,
getSourceColor: [255, 200, 0],
getTargetColor: [0, 140, 255],
getWidth: 12
});
}, []);

return (
<DeckGL
initialViewState={{
longitude: -122.45,
latitude: 37.75,
zoom: 11,
bearing: 0,
pitch: 60
}}
controller={true}
layers={[arcLayer]}
>
<Map mapStyle="mapbox://styles/mapbox/light-v9" mapboxAccessToken={TOKEN} />
</DeckGL>
<>
<ControlPanel overLabels={overLabels} setOverLabels={setOverLabels} />
<Map
initialViewState={{
zoom: 9,
longitude: -122.431297,
latitude: 37.787994
}}
mapStyle="mapbox://styles/mapbox/light-v9"
mapboxAccessToken={TOKEN}
>
<Layer layer={layer} beforeId={overLabels ? 'water-label' : 'country-label-lg'} />
</Map>
</>
);
}

Expand Down
32 changes: 32 additions & 0 deletions examples/deckgl-overlay/src/control-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';

function Checkbox({name, value, onChange}) {
return (
<div key={name} className="input">
<label>{name}</label>
<input type="checkbox" checked={value} onChange={evt => onChange(name, evt.target.checked)} />
</div>
);
}

function ControlPanel({
overLabels,
setOverLabels
}: {
overLabels: boolean;
setOverLabels: (v: boolean) => void;
}) {
return (
<div className="control-panel">
<h3>Deck.gl layer</h3>
<p>A deck.gl overlay can be used and inserted inside the Mapbox layers.</p>
<Checkbox
name="Arcs over labels"
value={overLabels}
onChange={(name: string, v: boolean) => setOverLabels(v)}
/>
</div>
);
}

export default ControlPanel;
57 changes: 40 additions & 17 deletions src/components/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,30 @@ import {deepEqual} from '../utils/deep-equal';

import type {MapboxMap, AnyLayer} from '../types';

export type LayerProps = AnyLayer & {
export type DeprecatedLayerProps = AnyLayer & {
id?: string;
/** If set, the layer will be inserted before the specified layer */
beforeId?: string;
};

export type LayerProps =
| {
layer: AnyLayer;
beforeId?: string;
}
| DeprecatedLayerProps;

/* eslint-disable complexity, max-statements */
function updateLayer(map: MapboxMap, id: string, props: LayerProps, prevProps: LayerProps) {
function updateLayer(map: MapboxMap, id: string, props: AnyLayer, prevProps: AnyLayer) {
assert(props.id === prevProps.id, 'layer id changed');
assert(props.type === prevProps.type, 'layer type changed');

if (props.type === 'custom' || prevProps.type === 'custom') {
return;
}

const {layout = {}, paint = {}, filter, minzoom, maxzoom, beforeId} = props;
const {layout = {}, paint = {}, filter, minzoom, maxzoom} = props;

if (beforeId !== prevProps.beforeId) {
map.moveLayer(id, beforeId);
}
if (layout !== prevProps.layout) {
const prevLayout = prevProps.layout || {};
for (const key in layout) {
Expand Down Expand Up @@ -59,27 +63,40 @@ function updateLayer(map: MapboxMap, id: string, props: LayerProps, prevProps: L
}
}

function createLayer(map: MapboxMap, id: string, props: LayerProps) {
function isMapStyleLoaded(map: MapboxMap) {
// @ts-ignore
if (map.style && map.style._loaded && (!('source' in props) || map.getSource(props.source))) {
const options: LayerProps = {...props, id};
delete options.beforeId;
return map.style && map.style._loaded;
}

function createLayer(map: MapboxMap, id: string, layerProps: LayerProps, beforeId: string) {
// @ts-ignore
if (isMapStyleLoaded(map) && (!('source' in layerProps) || map.getSource(layerProps.source))) {
// @ts-ignore
map.addLayer(options, props.beforeId);
map.addLayer(layerProps, beforeId);
}
}

/* eslint-enable complexity, max-statements */

let layerCounter = 0;

function Layer(props: LayerProps) {
function Layer(props: LayerProps & {layer?: AnyLayer}) {
const map: MapboxMap = useContext(MapContext).map.getMap();
const propsRef = useRef(props);

const layerProps = useMemo(() => {
if (props.layer) {
return props.layer;
}
const res = {...props};
delete res.beforeId;
return res as DeprecatedLayerProps;
}, [props.layer, props]);

const layerPropsRef = useRef(layerProps);
const [, setStyleLoaded] = useState(0);
const beforeId = props.beforeId;

const id = useMemo(() => props.id || `jsx-layer-${layerCounter++}`, []);
const id = useMemo(() => layerProps.id || `jsx-layer-${layerCounter++}`, []);

useEffect(() => {
if (map) {
Expand All @@ -102,16 +119,22 @@ function Layer(props: LayerProps) {
const layer = map && map.style && map.getLayer(id);
if (layer) {
try {
updateLayer(map, id, props, propsRef.current);
updateLayer(map, id, layerProps, layerPropsRef.current);
} catch (error) {
console.warn(error); // eslint-disable-line
}
} else {
createLayer(map, id, props);
createLayer(map, id, layerProps, beforeId);
}

useEffect(() => {
if (beforeId && isMapStyleLoaded(map)) {
map.moveLayer(id, beforeId);
}
}, [beforeId]);

// Store last rendered props
propsRef.current = props;
layerPropsRef.current = layerProps;

return null;
}
Expand Down

0 comments on commit f846087

Please sign in to comment.