@pirireis/react-carto-map-gl
Version:
396 lines (344 loc) • 8.64 kB
JavaScript
import {PureComponent} from 'react';
import PropTypes from 'prop-types';
import carto from '@carto/carto-vl';
import MapContext from '../MapContext/MapContext';
import {isSourcemetadataEqual} from '../../utils/vizEquality';
import {isEqual} from 'lodash-es';
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
class Source extends PureComponent {
static propTypes = {
/**
* Source id.
*/
id: PropTypes.string.isRequired,
/**
* Source type.
*/
type: PropTypes.oneOf(['mapbox', 'carto-vl']).isRequired,
/**
* Source data type.
*/
dataType: PropTypes.oneOf(['geojson', 'vector', 'raster']).isRequired,
/**
* <em>Required if dataType of source is <b>vector</b>.</em>
*/
vectorTileUrl: PropTypes.string,
/**
* <em>Required if dataType of source is <b>raster</b>.</em>
*/
rasterTileUrl: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
/**
* <em>Required if dataType of source is <b>raster</b>.</em>
*/
tileSize: PropTypes.number,
/**
* carto-vl MVT metadata. <br />
* <em>Required if source is a <b>carto-vl</b> source with dataType <b>vector</b>.</em>
*/
metadata: PropTypes.object,
/**
* Geojson feature collection. <br />
* <em>Required if source is a <b>geojson</b> source</em>
*/
data: PropTypes.object,
/**
* Callback function that will trigger when <b>carto-vl</b> source is created. <br />
* @param cartoSource created carto source
*/
onCartoSourceReady: PropTypes.func,
/**
* Mapbox geojson cluster.
*/
cluster: PropTypes.bool,
/**
* Mapbox geojson cluster max zoom.
*/
clusterMaxZoom: PropTypes.number,
/**
* Mapbox geojson cluster radius.
*/
clusterRadius: PropTypes.number,
};
static defaultProps = {
cluster: false,
clusterMaxZoom: 14,
clusterRadius: 50,
};
state = {ready: true};
componentDidMount() {
this.createSource();
}
componentDidUpdate(prevProps, prevState, snapshot) {
const {
id,
vectorTileUrl,
rasterTileUrl,
dataType,
data,
metadata,
} = this.props;
const {
id: prevId,
vectorTileUrl: prevVectorTileUrl,
rasterTileUrl: prevRasterTileUrl,
dataType: prevDataType,
data: prevData,
metadata: prevMetadata,
} = prevProps;
if (
id !== prevId ||
vectorTileUrl !== prevVectorTileUrl ||
!isEqual(rasterTileUrl, prevRasterTileUrl) ||
!isSourcemetadataEqual(metadata, prevMetadata)
) {
this.removeSource().then(() => {
this.createSource();
});
return;
}
if (dataType === 'geojson' && prevDataType === 'geojson') {
this.updateGeoJSONSource(prevData, data);
}
}
componentWillUnmount() {
this.removeSource();
}
static contextType = MapContext;
removeSource = () => {
return new Promise((resolve, reject) => {
const {id, type} = this.props;
const {compare, map, cartoLayers} = this.context;
const source = this.context.sources[id].mapSource;
if (type === 'carto-vl') {
const layers = Object.keys(cartoLayers)
.filter(key => cartoLayers[key].sourceId === id)
.map(key => cartoLayers[key]);
layers.forEach(l => {
this.context.removeCartoLayer(l.id);
l.remove();
});
source.free();
this.context.removeSource(id);
resolve();
} else if (type === 'mapbox') {
if (compare) {
asyncForEach(['map', 'afterMap'], async target => {
const map = this.context[target];
if (map.getSource(id)) {
const {layers} = map.getStyle();
if (layers) {
layers.forEach(layer => {
if (layer.source === id) {
map.removeLayer(layer.id);
}
});
}
await this.context.removeSource(id);
map.removeSource(id);
}
});
resolve();
} else {
if (map.getSource(id)) {
const {layers} = map.getStyle();
if (layers) {
layers.forEach(layer => {
if (layer.source === id) {
map.removeLayer(layer.id);
}
});
}
this.context.removeSource(id);
map.removeSource(id);
resolve();
}
}
}
});
};
async updateGeoJSONSource(prevData, data) {
if (!isEqual(data, prevData)) {
const {compare, map, afterMap, cartoLayers} = this.context;
const {id, type, dataType} = this.props;
if (type === 'carto-vl') {
const layers = Object.keys(cartoLayers)
.filter(key => cartoLayers[key].sourceId === id)
.map(key => cartoLayers[key]);
const mapSource = new carto.source.GeoJSON(data);
// await this.context.removeSource(id);
this.context.addSource(id, {
type,
dataType,
mapSource,
});
layers.forEach(l => {
l.update(mapSource);
});
} else if (type === 'mapbox') {
if (compare) {
const source = map.getSource(id);
const afterSource = afterMap.getSource(id);
if (source !== undefined) {
source.setData(data);
}
if (afterSource !== undefined) {
afterSource.setData(data);
}
} else {
const source = map.getSource(id);
if (source !== undefined) {
source.setData(data);
}
}
}
}
}
createSource() {
const {dataType, type} = this.props;
if (type === 'mapbox' && dataType === 'geojson') {
this._createMapBoxGeoJSONSource();
} else if (type === 'mapbox' && dataType === 'vector') {
this._createMapBoxVectorSource();
} else if (type === 'mapbox' && dataType === 'raster') {
this._createMapBoxRasterSource();
} else if (type === 'carto-vl' && dataType === 'geojson') {
this._createCartoGeoJSONSource();
} else if (type === 'carto-vl' && dataType === 'vector') {
this._createCartoVectorSource();
}
}
_createMapBoxGeoJSONSource() {
const {
data,
id,
dataType,
type,
cluster,
clusterMaxZoom,
clusterRadius,
} = this.props;
const {compare, map, afterMap} = this.context;
if (compare) {
map.addSource(id, {
type: 'geojson',
data,
cluster,
clusterMaxZoom,
clusterRadius,
});
afterMap.addSource(id, {
type: 'geojson',
data,
cluster,
clusterMaxZoom,
clusterRadius,
});
} else {
map.addSource(id, {
type: 'geojson',
data,
cluster,
clusterMaxZoom,
clusterRadius,
});
}
const mapSource = map.getSource(id);
this.context.addSource(id, {
mapSource,
dataType,
type,
});
}
_createMapBoxVectorSource() {
const {vectorTileUrl, id, dataType, type} = this.props;
const {compare, map, afterMap} = this.context;
if (compare) {
const waiting = () => {
if (!afterMap.isStyleLoaded()) {
setTimeout(waiting, 200);
} else {
map.addSource(id, {
type: 'vector',
tiles: [vectorTileUrl],
});
afterMap.addSource(id, {
type: 'vector',
tiles: [vectorTileUrl],
});
const mapSource = map.getSource(id);
this.context.addSource(id, {
mapSource,
dataType,
type,
});
}
};
waiting();
} else {
map.addSource(id, {
type: 'vector',
tiles: [vectorTileUrl],
});
const mapSource = map.getSource(id);
this.context.addSource(id, {
mapSource,
dataType,
type,
});
}
}
async _createMapBoxRasterSource() {
const {rasterTileUrl, tileSize = 256, id, dataType, type} = this.props;
const {compare, map} = this.context;
if (compare) {
await asyncForEach(['map', 'afterMap'], async target => {
const map = this.context[target];
map.addSource(id, {
type: 'raster',
tiles: Array.isArray(rasterTileUrl) ? rasterTileUrl : [rasterTileUrl],
tileSize: tileSize,
});
});
} else {
map.addSource(id, {
type: 'raster',
tiles: Array.isArray(rasterTileUrl) ? rasterTileUrl : [rasterTileUrl],
tileSize: tileSize,
});
}
const mapSource = map.getSource(id);
this.context.addSource(id, {
mapSource,
dataType,
type,
});
}
_createCartoGeoJSONSource() {
const {data, id, dataType, type} = this.props;
const mapSource = new carto.source.GeoJSON(data);
this.props.onCartoSourceReady && this.props.onCartoSourceReady(mapSource);
this.context.addSource(id, {
mapSource,
dataType,
type,
});
}
_createCartoVectorSource() {
const {vectorTileUrl, metadata, id, dataType, type} = this.props;
const mapSource = new carto.source.MVT([vectorTileUrl], metadata);
this.props.onCartoSourceReady && this.props.onCartoSourceReady(mapSource);
this.context.addSource(id, {
mapSource,
dataType,
type,
});
}
render() {
return null;
}
}
export default Source;