UNPKG

@deck.gl/carto

Version:

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

182 lines (163 loc) 6.84 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import {cellToParent} from 'quadbin'; import {cellToParent as h3CellToParent, getResolution as getH3Resolution} from 'h3-js'; import {_Tile2DHeader as Tile2DHeader} from '@deck.gl/geo-layers'; import {Accessor, log} from '@deck.gl/core'; import {BinaryFeatureCollection} from '@loaders.gl/schema'; import {createBinaryPointFeature, createEmptyBinary} from '../utils'; export type Aggregation = 'any' | 'average' | 'count' | 'min' | 'max' | 'sum'; export type AggregationProperties<FeaturePropertiesT> = { aggregation: Aggregation; name: keyof FeaturePropertiesT; }[]; export type ClusteredFeaturePropertiesT<FeaturePropertiesT> = FeaturePropertiesT & { id: bigint | string; count: number; position: [number, number]; }; export type ParsedQuadbinCell<FeaturePropertiesT> = {id: bigint; properties: FeaturePropertiesT}; export type ParsedQuadbinTile<FeaturePropertiesT> = ParsedQuadbinCell<FeaturePropertiesT>[]; export type ParsedH3Cell<FeaturePropertiesT> = {id: string; properties: FeaturePropertiesT}; export type ParsedH3Tile<FeaturePropertiesT> = ParsedH3Cell<FeaturePropertiesT>[]; /** * Aggregates tile by specified properties, caching result in tile.userData * * @returns true if data was aggregated, false if cache used */ export function aggregateTile<FeaturePropertiesT>( tile: Tile2DHeader<ParsedQuadbinTile<FeaturePropertiesT> | ParsedH3Tile<FeaturePropertiesT>>, tileAggregationCache: Map<number, ClusteredFeaturePropertiesT<FeaturePropertiesT>[]>, aggregationLevels: number, properties: AggregationProperties<FeaturePropertiesT> = [], getPosition: Accessor< ParsedQuadbinCell<FeaturePropertiesT> | ParsedH3Cell<FeaturePropertiesT>, [number, number] >, getWeight: Accessor< ParsedQuadbinCell<FeaturePropertiesT> | ParsedH3Cell<FeaturePropertiesT>, number >, scheme: 'quadbin' | 'h3' = 'quadbin' ): boolean { if (!tile.content) return false; // Aggregate on demand and cache result if (!tile.userData) tile.userData = {}; const cell0 = tileAggregationCache.get(aggregationLevels)?.[0]; if (cell0) { // Have already aggregated this tile if (properties.every(property => property.name in cell0)) { // Use cached result return false; } // Aggregated properties have changed, re-aggregate tileAggregationCache.clear(); } const out: Record<string, any> = {}; for (const cell of tile.content) { let id = cell.id; const position = typeof getPosition === 'function' ? getPosition(cell, {} as any) : getPosition; // Aggregate by parent rid for (let i = 0; i < aggregationLevels - 1; i++) { if (scheme === 'h3') { const currentResolution = getH3Resolution(id as string); id = h3CellToParent(id as string, Math.max(0, currentResolution - 1)); } else { id = cellToParent(id as bigint); } } // Use string key for both H3 and Quadbin to avoid TypeScript Record<bigint, any> issues // https://github.com/microsoft/TypeScript/issues/46395 const parentId = String(id); if (!(parentId in out)) { out[parentId] = {id, count: 0, position: [0, 0]}; for (const {name, aggregation} of properties) { if (aggregation === 'any') { // Just pick first value for ANY out[parentId][name] = cell.properties[name]; } else { out[parentId][name] = 0; } } } // Layout props const prevTotalW = out[parentId].count; out[parentId].count += typeof getWeight === 'function' ? getWeight(cell, {} as any) : getWeight; const totalW = out[parentId].count; const W = totalW - prevTotalW; out[parentId].position[0] = (prevTotalW * out[parentId].position[0] + W * position[0]) / totalW; out[parentId].position[1] = (prevTotalW * out[parentId].position[1] + W * position[1]) / totalW; // Re-aggregate other properties so clusters can be styled for (const {name, aggregation} of properties) { const prevValue = out[parentId][name]; const value = cell.properties[name] as number; if (aggregation === 'average') { out[parentId][name] = (prevTotalW * prevValue + W * value) / totalW; } else if (aggregation === 'count' || aggregation === 'sum') { out[parentId][name] = prevValue + value; } else if (aggregation === 'max') { out[parentId][name] = Math.max(prevValue, value); } else if (aggregation === 'min') { out[parentId][name] = Math.min(prevValue, value); } } } tileAggregationCache.set(aggregationLevels, Object.values(out)); return true; } export function extractAggregationProperties<FeaturePropertiesT extends {}>( tile: Tile2DHeader<ParsedQuadbinTile<FeaturePropertiesT> | ParsedH3Tile<FeaturePropertiesT>> ): AggregationProperties<FeaturePropertiesT> { const properties: AggregationProperties<FeaturePropertiesT> = []; const validAggregations: Aggregation[] = ['any', 'average', 'count', 'min', 'max', 'sum']; for (const name of Object.keys(tile.content![0].properties)) { let aggregation = name.split('_').pop()!.toLowerCase() as Aggregation; if (!validAggregations.includes(aggregation)) { log.warn(`No valid aggregation present in ${name} property`)(); aggregation = 'any'; } properties.push({name: name as keyof FeaturePropertiesT, aggregation}); } return properties; } export function computeAggregationStats<FeaturePropertiesT>( data: ClusteredFeaturePropertiesT<FeaturePropertiesT>[], properties: AggregationProperties<FeaturePropertiesT> ) { const stats = {} as Record<keyof FeaturePropertiesT, {min: number; max: number}>; for (const {name, aggregation} of properties) { stats[name] = {min: Infinity, max: -Infinity}; if (aggregation !== 'any') { for (const d of data) { stats[name].min = Math.min(stats[name].min, d[name] as number); stats[name].max = Math.max(stats[name].max, d[name] as number); } } } return stats; } type BinaryFeatureCollectionWithStats<FeaturePropertiesT> = Omit< BinaryFeatureCollection, 'points' > & { points: BinaryFeatureCollection['points'] & { attributes?: { stats: Record<keyof FeaturePropertiesT, {min: number; max: number}>; }; }; }; export function clustersToBinary<FeaturePropertiesT>( data: ClusteredFeaturePropertiesT<FeaturePropertiesT>[] ): BinaryFeatureCollectionWithStats<FeaturePropertiesT> { const positions = new Float32Array(data.length * 2); const featureIds = new Uint16Array(data.length); for (let i = 0; i < data.length; i++) { positions.set(data[i].position, 2 * i); featureIds[i] = i; } return { ...createEmptyBinary(), points: createBinaryPointFeature(positions, featureIds, featureIds, {}, data) }; }