UNPKG

@pirireis/react-carto-map-gl

Version:

485 lines (415 loc) 11 kB
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);