@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
text/typescript
// 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)
};
}