UNPKG

@deck.gl/carto

Version:

CARTO official integration with Deck.gl. Build geospatial applications using CARTO and Deck.gl.

183 lines 7.72 kB
// 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