@pirireis/react-carto-map-gl
Version:
485 lines (415 loc) • 11 kB
JavaScript
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import carto from '@carto/carto-vl';
import {isEqual, cloneDeep} from 'lodash-es';
import MapContext from '../MapContext/MapContext';
import diff from '../../utils/diff';
import {isVizEqual, isVizVariablesEqual} from '../../utils/vizEquality';
const LAYER_CREATE_TIMEOUT = 300;
class Layer extends PureComponent {
static propTypes = {
/**
* Layer id.
*/
id: PropTypes.string.isRequired,
/**
* Source id of layer.
*/
sourceId: PropTypes.string.isRequired,
/**
* Mapbox-gl layer type. <br />
* <em>Required if source of layer is a <b>mapbox</b> source.</em>
*/
type: PropTypes.oneOf([
'background',
'fill',
'line',
'symbol',
'raster',
'flow',
'circle',
'fill-extrusion',
'heatmap',
'hillshade',
]),
/**
* Mapbox-gl layout <br />
* <em>Required if source of layer is a <b>mapbox</b> source.</em>
*/
layout: PropTypes.object,
/** Mapbox-gl style paint. <br />
* <em>Required if source of layer is a <b>mapbox</b> source.</em>
*/
paint: PropTypes.object,
/** Mapbox-gl source-layer option for vector tile sources. <br />
* <em>Required if source of layer is a <b>mapbox</b> source.</em>
*/
sourceLayer: PropTypes.string,
/**
* Mapbox filter object. <br />
*/
filter: PropTypes.array,
/**
* Deck GL Flow object. <br />
*/
flow: PropTypes.object,
/**
* carto-vl style object. carto.Viz will be created from this object. <br />
* <em>Required if source of layer is a <b>carto-vl</b> source.</em>
*/
vizProperties: PropTypes.object,
/**
* Callback function that will trigger when <b>carto-vl</b> layer is created. <br />
* @param cartoLayer created carto layer
*/
onCartoLayerReady: PropTypes.func,
/**
* Callback function that will trigger when <b>carto-vl</b> layer is updated. <br />
* @param cartoLayer updated carto layer
*/
onCartoLayerUpdate: PropTypes.func,
/**
* Before id.
*/
beforeId: PropTypes.string,
/**
* carto.Viz blend duration on component updates.
*/
blendDuration: PropTypes.number,
/**
* Min zoom level.
*/
minZoom: PropTypes.number,
/**
* Max zoom level.
*/
maxZoom: PropTypes.number,
/**
* Visibility of layer.
*/
visible: PropTypes.bool,
/**
* target maps.
*/
targets: PropTypes.arrayOf(PropTypes.oneOf(['map', 'afterMap'])).isRequired,
};
static defaultProps = {
visible: true,
vizProperties: {},
flow: null,
minZoom: 0,
maxZoom: 24,
blendDuration: 500,
targets: ['map'],
};
componentDidMount() {
this.createLayer();
}
componentDidUpdate(prevProps) {
const {
id,
sourceId,
beforeId,
paint,
layout,
filter,
visible,
vizProperties,
minZoom,
maxZoom,
blendDuration,
targets,
type,
deckLayer,
deckLayerProps,
} = this.props;
const {
id: prevId,
beforeId: prevBeforeId,
paint: prevPaint,
layout: prevLayout,
filter: prevFilter,
visible: prevVisible,
vizProperties: prevVizProperties,
minZoom: prevMinZoom,
maxZoom: prevMaxZoom,
deckLayerProps: prevDeckLayerProps,
} = prevProps;
const sourceData = this.context.sources[sourceId];
const {cartoLayers, compare, deck} = this.context;
const targetMaps = compare ? targets : ['map'];
targetMaps.forEach(target => {
const map = this.context[target];
const cartoLayer = cartoLayers[`${target}_${id}`];
if (!sourceData) {
if (map.getLayer(id)) map.removeLayer(id);
return;
}
if (deckLayer) {
deck.setProps({
layers: deckLayer,
});
}
if (!map.getLayer(id) && type !== 'flow') {
this.createLayer();
return;
}
if (id !== prevId) {
if (map.getLayer(prevId)) map.removeLayer(prevId);
this.createLayer();
return;
}
if (maxZoom !== prevMaxZoom || minZoom !== prevMinZoom) {
map.setLayerZoomRange(id, minZoom, maxZoom);
}
if (beforeId !== prevBeforeId) {
map.moveLayer(id, beforeId);
}
if (sourceData.type === 'mapbox') {
if (!isEqual(paint, prevPaint)) {
diff(paint, prevPaint).forEach(([key, value]) => {
if (type === 'flow') {
/* const {map} = this.context;
const deckLayer = map.getLayer(id).implementation;
deckLayer.setProps({
[key]: value,
}); */
} else {
map.setPaintProperty(id, key, value);
}
});
}
if (map && !isEqual(layout, prevLayout)) {
diff(layout, prevLayout).forEach(([key, value]) => {
map.setLayoutProperty(id, key, value);
});
}
if (!isEqual(filter, prevFilter)) {
if (!filter) {
map.setFilter(id, undefined);
} else {
map.setFilter(id, filter);
}
}
if (map && visible !== prevVisible) {
map.setLayoutProperty(
id,
'visibility',
visible === true ? 'visible' : 'none',
);
}
}
if (sourceData.type === 'carto-vl' && cartoLayer) {
if (
vizProperties !== null &&
!isVizEqual(vizProperties, prevVizProperties)
) {
const {sourceId} = this.props;
const source = this.context.sources[sourceId].mapSource;
// Important for not to mutate vizProperties
const tempViz = cloneDeep(vizProperties);
if (typeof tempViz === 'object') {
Object.keys(tempViz).map(key => {
if (key === 'order') return;
if (
key === 'variables' &&
vizProperties &&
prevVizProperties &&
!isVizVariablesEqual(
vizProperties.variables,
prevVizProperties.variables,
)
) {
Object.keys(cartoLayer.vizObject.variables).map(key => {
delete cartoLayer.vizObject.variables[key];
});
Object.keys(vizProperties.variables).map(key => {
if (!cartoLayer.vizObject.variables[key]) return;
vizProperties.variables[key].parent =
cartoLayer.vizObject.variables[key].parent;
cartoLayer.vizObject.variables[key] =
vizProperties.variables[key];
});
// cartoLayer.update(source, cartoLayer.vizObject);
// map.removeLayer(prevId);
// this.createLayer();
return;
}
if (cartoLayer.vizObject[key].blendTo) {
cartoLayer.vizObject[key].blendTo(tempViz[key], blendDuration);
}
});
if (sourceData.dataType === 'geojson') {
cartoLayer.update(source, cartoLayer.vizObject);
}
if (this.props.onCartoLayerUpdate) {
const eventHandler = type => {
this.props.onCartoLayerUpdate(cartoLayer, type);
cartoLayer.off('updated', eventHandler);
};
cartoLayer.on('updated', eventHandler);
}
}
}
if (visible !== prevVisible) {
if (visible) cartoLayer.show();
else cartoLayer.hide();
}
}
});
// this._updateEventListeners(prevProps, this.props);
}
componentWillUnmount() {
const {id, targets} = this.props;
const {deck} = this.context;
const targetMaps = this.context.compare ? targets : ['map'];
deck.setProps({
layers: [],
});
targetMaps.forEach(target => {
const map = this.context[target];
if (map.getLayer(id)) map.removeLayer(id);
});
// const layer = cartoLayers[id];
// layer && layer.remove();
}
static contextType = MapContext;
createLayer() {
const {sourceId} = this.props;
const sourceData = this.context.sources[sourceId];
if (!sourceData) return;
switch (sourceData.type) {
case 'mapbox':
this._createMapBoxLayer();
break;
case 'carto-vl':
this._createCartoLayer();
break;
default:
break;
}
}
_createMapBoxLayer() {
const {
id,
type,
paint,
deckLayer,
filter,
sourceLayer,
visible,
beforeId,
layout,
minZoom,
maxZoom,
sourceId,
targets,
} = this.props;
const options = {
id,
type,
source: sourceId,
};
layout && (options.layout = layout);
paint && (options.paint = paint);
sourceLayer && (options['source-layer'] = sourceLayer);
filter && (options.filter = filter);
const targetMaps = this.context.compare ? targets : ['map'];
const {deck} = this.context;
targetMaps.forEach(target => {
const map = this.context[target];
if (map && map.style._loaded && map.getSource(sourceId)) {
if (type === 'flow' && deckLayer) {
deck.setProps({
layers: deckLayer,
});
} else {
if (beforeId && !map.getLayer(beforeId)) {
const interval = setInterval(() => {
clearInterval(interval);
this._createMapBoxLayer();
}, LAYER_CREATE_TIMEOUT);
return;
}
if (!map.getLayer(id)) map.addLayer(options, beforeId);
map.setLayoutProperty(
id,
'visibility',
visible === true ? 'visible' : 'none',
);
map.setLayerZoomRange(id, minZoom, maxZoom);
if (this.props.onLayerReady) this.props.onLayerReady(id);
}
} else {
map.once('sourcedata', () => this._createMapBoxLayer());
}
});
}
_createCartoLayer() {
const {
id,
beforeId,
vizProperties,
visible,
minZoom,
maxZoom,
sourceId,
targets,
} = this.props;
const {compare} = this.context;
const waiting = () => {
const mapLoaded = this.context[compare ? 'afterMap' : 'map'];
if (!mapLoaded.isStyleLoaded()) {
setTimeout(waiting, 200);
} else {
const targetMaps = compare ? targets : ['map'];
const layers = [];
targetMaps.forEach(target => {
const map = this.context[target];
if (!compare && beforeId && !map.getLayer(beforeId)) {
const interval = setInterval(() => {
clearInterval(interval);
this._createCartoLayer();
}, LAYER_CREATE_TIMEOUT);
return;
}
const source = this.context.sources[sourceId].mapSource;
// Important for not to mutate vizProperties
const tempViz = cloneDeep(vizProperties);
const vizObject = new carto.Viz(tempViz);
const layer = new carto.Layer(id, source, vizObject);
layer.vizObject = vizObject;
layer.addTo(map, beforeId);
if (visible) layer.show();
else layer.hide();
layers.push(layer);
map.setLayerZoomRange(id, minZoom, maxZoom);
if (this.props.onCartoLayerReady) {
const eventHandler = type => {
if (this.layerTime) clearTimeout(this.layerTime);
this.props.onCartoLayerReady(layer, type);
layer.off('updated', eventHandler);
};
this.layerTime = setTimeout(() => {
if (!(map.getZoom() > minZoom && map.getZoom() < maxZoom)) {
this.props.onCartoLayerReady(
layer,
'different dataframes required from source',
);
}
}, 1000);
layer.on('updated', eventHandler);
}
});
targetMaps.forEach((target, i) => {
this.context.addCartoLayer(sourceId, `${target}_${id}`, layers[i]);
});
}
};
waiting();
}
render() {
return null;
}
}
export default React.memo(Layer);