@deck.gl/geo-layers
Version:
deck.gl layers supporting geospatial use cases and GIS formats
363 lines (300 loc) • 10.4 kB
JavaScript
import {log} from '@deck.gl/core';
import {Matrix4} from '@math.gl/core';
import {MVTWorkerLoader} from '@loaders.gl/mvt';
import {binaryToGeojson} from '@loaders.gl/gis';
import {COORDINATE_SYSTEM} from '@deck.gl/core';
import {ClipExtension} from '@deck.gl/extensions';
import TileLayer from '../tile-layer/tile-layer';
import {getURLFromTemplate, isURLTemplate} from '../tile-layer/utils';
import {transform} from './coordinate-transform';
import findIndexBinary from './find-index-binary';
import {GeoJsonLayer} from '@deck.gl/layers';
const WORLD_SIZE = 512;
const defaultProps = {
...GeoJsonLayer.defaultProps,
uniqueIdProperty: {type: 'string', value: ''},
highlightedFeatureId: null,
loaders: [MVTWorkerLoader],
binary: true
};
export default class MVTLayer extends TileLayer {
initializeState() {
super.initializeState();
// GlobeView doesn't work well with binary data
const binary = this.context.viewport.resolution !== undefined ? false : this.props.binary;
this.setState({
binary,
data: null,
tileJSON: null
});
}
get isLoaded() {
return this.state.data && this.state.tileset && super.isLoaded;
}
updateState({props, oldProps, context, changeFlags}) {
if (changeFlags.dataChanged) {
this._updateTileData();
}
if (this.state.data) {
super.updateState({props, oldProps, context, changeFlags});
this._setWGS84PropertyForTiles();
}
const {highlightColor} = props;
if (highlightColor !== oldProps.highlightColor && Array.isArray(highlightColor)) {
this.setState({highlightColor});
}
}
/* eslint-disable complexity */
async _updateTileData() {
let {data} = this.props;
let tileJSON = null;
if (typeof data === 'string' && !isURLTemplate(data)) {
const {onDataLoad, fetch} = this.props;
this.setState({data: null, tileJSON: null});
try {
tileJSON = await fetch(data, {propName: 'data', layer: this, loaders: []});
} catch (error) {
this.raiseError(error, 'loading TileJSON');
data = null;
}
if (onDataLoad) {
onDataLoad(tileJSON);
}
} else if (data.tilejson) {
tileJSON = data;
}
if (tileJSON) {
data = tileJSON.tiles;
}
this.setState({data, tileJSON});
}
_getTilesetOptions(props) {
const opts = super._getTilesetOptions(props);
const {tileJSON} = this.state;
if (tileJSON) {
if (Number.isFinite(tileJSON.minzoom) && tileJSON.minzoom > props.minZoom) {
opts.minZoom = tileJSON.minzoom;
}
if (
Number.isFinite(tileJSON.maxzoom) &&
(!Number.isFinite(props.maxZoom) || tileJSON.maxzoom < props.maxZoom)
) {
opts.maxZoom = tileJSON.maxzoom;
}
}
return opts;
}
/* eslint-disable complexity */
renderLayers() {
if (!this.state.data) return null;
return super.renderLayers();
}
getTileData(tile) {
const url = getURLFromTemplate(this.state.data, tile);
if (!url) {
return Promise.reject('Invalid URL');
}
let loadOptions = this.getLoadOptions();
const {binary} = this.state;
const {fetch} = this.props;
const {signal, x, y, z} = tile;
loadOptions = {
...loadOptions,
mimeType: 'application/x-protobuf',
mvt: {
...loadOptions?.mvt,
coordinates: this.context.viewport.resolution ? 'wgs84' : 'local',
tileIndex: {x, y, z}
// Local worker debug
// workerUrl: `modules/mvt/dist/mvt-loader.worker.js`
// Set worker to null to skip web workers
// workerUrl: null
},
gis: binary ? {format: 'binary'} : {}
};
return fetch(url, {propName: 'data', layer: this, loadOptions, signal});
}
renderSubLayers(props) {
const {tile} = props;
const worldScale = Math.pow(2, tile.z);
const xScale = WORLD_SIZE / worldScale;
const yScale = -xScale;
const xOffset = (WORLD_SIZE * tile.x) / worldScale;
const yOffset = WORLD_SIZE * (1 - tile.y / worldScale);
const modelMatrix = new Matrix4().scale([xScale, yScale, 1]);
props.autoHighlight = false;
if (!this.context.viewport.resolution) {
props.modelMatrix = modelMatrix;
props.coordinateOrigin = [xOffset, yOffset, 0];
props.coordinateSystem = COORDINATE_SYSTEM.CARTESIAN;
props.extensions = [...(props.extensions || []), new ClipExtension()];
}
const subLayers = super.renderSubLayers(props);
if (this.state.binary && !(subLayers instanceof GeoJsonLayer)) {
log.warn('renderSubLayers() must return GeoJsonLayer when using binary:true')();
}
return subLayers;
}
_updateAutoHighlight(info) {
const {uniqueIdProperty} = this.props;
const {hoveredFeatureId, hoveredFeatureLayerName} = this.state;
const hoveredFeature = info.object;
let newHoveredFeatureId;
let newHoveredFeatureLayerName;
if (hoveredFeature) {
newHoveredFeatureId = getFeatureUniqueId(hoveredFeature, uniqueIdProperty);
newHoveredFeatureLayerName = getFeatureLayerName(hoveredFeature);
}
let {highlightColor} = this.props;
if (typeof highlightColor === 'function') {
highlightColor = highlightColor(info);
}
if (
hoveredFeatureId !== newHoveredFeatureId ||
hoveredFeatureLayerName !== newHoveredFeatureLayerName
) {
this.setState({
highlightColor,
hoveredFeatureId: newHoveredFeatureId,
hoveredFeatureLayerName: newHoveredFeatureLayerName
});
}
}
getPickingInfo(params) {
const info = super.getPickingInfo(params);
const isWGS84 = this.context.viewport.resolution;
if (this.state.binary && info.index !== -1) {
const {data} = params.sourceLayer.props;
info.object = binaryToGeojson(data, {globalFeatureId: info.index});
}
if (info.object && !isWGS84) {
info.object = transformTileCoordsToWGS84(info.object, info.tile.bbox, this.context.viewport);
}
return info;
}
getSubLayerPropsByTile(tile) {
return {
highlightedObjectIndex: this.getHighlightedObjectIndex(tile),
highlightColor: this.state.highlightColor
};
}
getHighlightedObjectIndex(tile) {
const {hoveredFeatureId, hoveredFeatureLayerName, binary} = this.state;
const {uniqueIdProperty, highlightedFeatureId} = this.props;
const data = tile.content;
const isHighlighted = isFeatureIdDefined(highlightedFeatureId);
const isFeatureIdPresent = isFeatureIdDefined(hoveredFeatureId) || isHighlighted;
if (!isFeatureIdPresent) {
return -1;
}
const featureIdToHighlight = isHighlighted ? highlightedFeatureId : hoveredFeatureId;
// Iterable data
if (Array.isArray(data)) {
return data.findIndex(feature => {
const isMatchingId = getFeatureUniqueId(feature, uniqueIdProperty) === featureIdToHighlight;
const isMatchingLayer =
isHighlighted || getFeatureLayerName(feature) === hoveredFeatureLayerName;
return isMatchingId && isMatchingLayer;
});
// Non-iterable data
} else if (data && binary) {
// Get the feature index of the selected item to highlight
return findIndexBinary(
data,
uniqueIdProperty,
featureIdToHighlight,
isHighlighted ? '' : hoveredFeatureLayerName
);
}
return -1;
}
_pickObjects(maxObjects) {
const {deck, viewport} = this.context;
const width = viewport.width;
const height = viewport.height;
const x = viewport.x;
const y = viewport.y;
const layerIds = [this.id];
return deck.pickObjects({x, y, width, height, layerIds, maxObjects});
}
getRenderedFeatures(maxFeatures = null) {
const features = this._pickObjects(maxFeatures);
const featureCache = new Set();
const renderedFeatures = [];
for (const f of features) {
const featureId = getFeatureUniqueId(f.object, this.props.uniqueIdProperty);
if (featureId === undefined) {
// we have no id for the feature, we just add to the list
renderedFeatures.push(f.object);
} else if (!featureCache.has(featureId)) {
// Add removing duplicates
featureCache.add(featureId);
renderedFeatures.push(f.object);
}
}
return renderedFeatures;
}
_setWGS84PropertyForTiles() {
const propName = 'dataInWGS84';
const {tileset} = this.state;
tileset.selectedTiles.forEach(tile => {
if (!tile.hasOwnProperty(propName)) {
// eslint-disable-next-line accessor-pairs
Object.defineProperty(tile, propName, {
get: () => {
// Still loading or encountered an error
if (!tile.content) {
return null;
}
if (this.state.binary && Array.isArray(tile.content) && !tile.content.length) {
// TODO: @loaders.gl/mvt returns [] when no content. It should return a valid empty binary.
// https://github.com/visgl/loaders.gl/pull/1137
return [];
}
if (tile._contentWGS84 === undefined) {
// Create a cache to transform only once
const content = this.state.binary ? binaryToGeojson(tile.content) : tile.content;
tile._contentWGS84 = content.map(feature =>
transformTileCoordsToWGS84(feature, tile.bbox, this.context.viewport)
);
}
return tile._contentWGS84;
}
});
}
});
}
}
function getFeatureUniqueId(feature, uniqueIdProperty) {
if (uniqueIdProperty) {
return feature.properties[uniqueIdProperty];
}
if ('id' in feature) {
return feature.id;
}
return undefined;
}
function getFeatureLayerName(feature) {
return feature.properties?.layerName || null;
}
function isFeatureIdDefined(value) {
return value !== undefined && value !== null && value !== '';
}
function transformTileCoordsToWGS84(object, bbox, viewport) {
const feature = {
...object,
geometry: {
type: object.geometry.type
}
};
// eslint-disable-next-line accessor-pairs
Object.defineProperty(feature.geometry, 'coordinates', {
get: () => {
const wgs84Geom = transform(object.geometry, bbox, viewport);
return wgs84Geom.coordinates;
}
});
return feature;
}
MVTLayer.layerName = 'MVTLayer';
MVTLayer.defaultProps = defaultProps;