@deck.gl/carto
Version:
CARTO official integration with Deck.gl. Build geospatial applications using CARTO and Deck.gl.
183 lines • 7.72 kB
JavaScript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
/* eslint-disable no-shadow */
import { GeoJsonLayer } from '@deck.gl/layers';
import { TileLayer } from '@deck.gl/geo-layers';
import { registerLoaders } from '@loaders.gl/core';
import { binaryToGeojson } from '@loaders.gl/gis';
import { CompositeLayer, _deepEqual as deepEqual } from '@deck.gl/core';
import { aggregateTile, clustersToBinary, computeAggregationStats, extractAggregationProperties } from "./cluster-utils.js";
import { DEFAULT_TILE_SIZE } from "../constants.js";
import QuadbinTileset2D from "./quadbin-tileset-2d.js";
import H3Tileset2D, { getHexagonResolution } from "./h3-tileset-2d.js";
import { getQuadbinPolygon } from "./quadbin-utils.js";
import { getResolution, cellToLatLng } from 'h3-js';
import CartoSpatialTileLoader from "./schema/carto-spatial-tile-loader.js";
import { TilejsonPropType, mergeLoadOptions } from "./utils.js";
registerLoaders([CartoSpatialTileLoader]);
function getScheme(tilesetClass) {
if (tilesetClass === H3Tileset2D)
return 'h3';
if (tilesetClass === QuadbinTileset2D)
return 'quadbin';
throw new Error('Invalid tileset class');
}
const defaultProps = {
data: TilejsonPropType,
clusterLevel: { type: 'number', value: 5, min: 1 },
getPosition: {
type: 'accessor',
value: ({ id }) => {
// Determine scheme based on ID type: H3 uses string IDs, Quadbin uses bigint IDs
if (typeof id === 'string') {
const [lat, lng] = cellToLatLng(id);
return [lng, lat];
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
return getQuadbinPolygon(id, 0.5).slice(2, 4);
}
},
getWeight: { type: 'accessor', value: 1 },
refinementStrategy: 'no-overlap',
tileSize: DEFAULT_TILE_SIZE
};
class ClusterGeoJsonLayer extends TileLayer {
initializeState() {
super.initializeState();
this.state.aggregationCache = new WeakMap();
this.state.scheme = getScheme(this.props.TilesetClass);
}
updateState(opts) {
const { props } = opts;
const scheme = getScheme(props.TilesetClass);
if (this.state.scheme !== scheme) {
// Clear caches when scheme changes
this.setState({ scheme, tileset: null });
this.state.aggregationCache = new WeakMap();
}
super.updateState(opts);
}
// eslint-disable-next-line max-statements
renderLayers() {
const visibleTiles = this.state.tileset?.tiles.filter((tile) => {
return tile.isLoaded && tile.content && this.state.tileset.isTileVisible(tile);
});
if (!visibleTiles?.length || !this.state.tileset) {
return null;
}
visibleTiles.sort((a, b) => b.zoom - a.zoom);
const { getPosition, getWeight } = this.props;
const { aggregationCache, scheme } = this.state;
const isH3 = scheme === 'h3';
const properties = extractAggregationProperties(visibleTiles[0]);
const data = [];
let needsUpdate = false;
const aggregationLevels = this._getAggregationLevels(visibleTiles);
for (const tile of visibleTiles) {
// Calculate aggregation based on viewport zoom
let tileAggregationCache = aggregationCache.get(tile.content);
if (!tileAggregationCache) {
tileAggregationCache = new Map();
aggregationCache.set(tile.content, tileAggregationCache);
}
const didAggregate = aggregateTile(tile, tileAggregationCache, aggregationLevels, properties, getPosition, getWeight, isH3 ? 'h3' : 'quadbin');
needsUpdate || (needsUpdate = didAggregate);
data.push(...tileAggregationCache.get(aggregationLevels));
}
data.sort((a, b) => Number(b.count - a.count));
const clusterIds = data?.map((tile) => tile.id);
needsUpdate || (needsUpdate = !deepEqual(clusterIds, this.state.clusterIds, 1));
this.setState({ clusterIds });
if (needsUpdate) {
const stats = computeAggregationStats(data, properties);
const binaryData = clustersToBinary(data);
binaryData.points.attributes = { stats };
this.setState({ data: binaryData });
}
const props = {
...this.props,
id: 'clusters',
data: this.state.data,
dataComparator: (data, oldData) => {
const newIds = data?.points?.properties?.map((tile) => tile.id);
const oldIds = oldData?.points?.properties?.map((tile) => tile.id);
return deepEqual(newIds, oldIds, 1);
}
};
return new GeoJsonLayer(this.getSubLayerProps(props));
}
getPickingInfo(params) {
const info = params.info;
if (info.index !== -1) {
const { data } = params.sourceLayer.props;
info.object = binaryToGeojson(data, {
globalFeatureId: info.index
});
}
return info;
}
_updateAutoHighlight(info) {
for (const layer of this.getSubLayers()) {
layer.updateAutoHighlight(info);
}
}
filterSubLayer() {
return true;
}
_getAggregationLevels(visibleTiles) {
const isH3 = this.state.scheme === 'h3';
const firstTile = visibleTiles[0];
// Resolution of data present in tiles
let tileResolution;
// Resolution of tiles that should be (eventually) visible in the viewport
let viewportResolution;
if (isH3) {
tileResolution = getResolution(firstTile.id);
viewportResolution = getHexagonResolution(this.context.viewport, this.state.tileset.opts.tileSize);
}
else {
tileResolution = firstTile.zoom;
viewportResolution = this.context.viewport.zoom;
}
const resolutionDiff = Math.round(viewportResolution - tileResolution);
const aggregationLevels = Math.round(this.props.clusterLevel) - resolutionDiff;
return aggregationLevels;
}
}
ClusterGeoJsonLayer.layerName = 'ClusterGeoJsonLayer';
ClusterGeoJsonLayer.defaultProps = defaultProps;
// Adapter layer around ClusterLayer that converts tileJSON into TileLayer API
class ClusterTileLayer extends CompositeLayer {
getLoadOptions() {
const tileJSON = this.props.data;
const scheme = tileJSON && 'scheme' in tileJSON ? tileJSON.scheme : 'quadbin';
return mergeLoadOptions(super.getLoadOptions(), {
fetch: { headers: { Authorization: `Bearer ${tileJSON.accessToken}` } },
cartoSpatialTile: { scheme }
});
}
renderLayers() {
const tileJSON = this.props.data;
if (!tileJSON)
return null;
const { tiles: data, maxresolution: maxZoom } = tileJSON;
const isH3 = tileJSON && 'scheme' in tileJSON && tileJSON.scheme === 'h3';
const TilesetClass = isH3 ? H3Tileset2D : QuadbinTileset2D;
return [
// @ts-ignore
new ClusterGeoJsonLayer(this.props, {
id: `cluster-geojson-layer-${this.props.id}`,
data,
// TODO: Tileset2D should be generic over TileIndex type
TilesetClass: TilesetClass,
maxZoom,
loadOptions: this.getLoadOptions()
})
];
}
}
ClusterTileLayer.layerName = 'ClusterTileLayer';
ClusterTileLayer.defaultProps = defaultProps;
export default ClusterTileLayer;
//# sourceMappingURL=cluster-tile-layer.js.map