UNPKG

s2-tools

Version:

A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.

216 lines 8.04 kB
import { Tile } from '../dataStructures'; import { convert } from '../geometry/tools/convert'; import { fromS2Points } from '../geometry/s1/chordAngle'; import { PointIndex } from './pointIndex'; import { addMut, divMutScalar, fromLonLat, fromST, mulScalar, normalize, toST, } from '../geometry/s2/point'; import { fromFacePosLevel, getVertices, level, range } from '../geometry'; /** * @param properties - the properties associated with the cluster * @returns - a new cluster */ function newCluster(properties) { return { properties, visited: false, sum: 1 }; } /** * Create a cluster with the correct sum * @param properties - the properties associated with the cluster * @param sum - the sum of the cluster * @returns - a new cluster with the correct sum and properties data */ function sumToCluster(properties, sum) { return { properties, visited: false, sum }; } /** * # Point Cluster * * ## Description * A cluster store to index points at each zoom level * * ## Usage * ```ts * import { PointCluster } from 's2-tools'; * const pointCluster = new PointCluster(); * * // add a lon-lat * pointCluster.insertLonLat(lon, lat, data); * // add an STPoint * pointCluster.insertFaceST(face, s, t, data); * * // after adding data build the clusters * await pointCluster.buildClusters(); * * // get the clusters for a tile * const tile = await pointCluster.getTile(id); * // or get the raw cluster data * const clusters = await pointCluster.getCellData(id); * ``` */ export class PointCluster { projection; layerName; minzoom; maxzoom; radius; extent = 512; // a 512x512 pixel tile indexes = new Map(); /** * @param data - if provided, the data to index * @param options - cluster options on how to build the cluster * @param maxzoomStore - the store to use for the maxzoom index */ constructor(data, options, maxzoomStore) { this.projection = options?.projection ?? 'S2'; this.layerName = options?.layerName ?? 'default'; this.minzoom = Math.max(options?.minzoom ?? 0, 0); this.maxzoom = Math.min(options?.maxzoom ?? 16, 29); this.radius = options?.radius ?? 40; for (let zoom = this.minzoom; zoom <= this.maxzoom; zoom++) { this.indexes.set(zoom, new PointIndex(options?.store)); } if (maxzoomStore !== undefined) { const maxzoomIndex = this.indexes.get(this.maxzoom); maxzoomIndex?.setStore(maxzoomStore); } // convert features if provided if (data !== undefined) { const features = convert(this.projection, data, false, undefined, this.maxzoom, true); for (const feature of features) { const face = feature.face ?? 0; const { type, coordinates } = feature.geometry; if (type === 'Point') { const { x: s, y: t } = coordinates; this.insertFaceST(face, s, t, feature.properties); } } } } /** * Add a point to the maxzoom index * @param point - the point to add * @param data - the data associated with the point */ insert(point, data) { const maxzoomIndex = this.indexes.get(this.maxzoom); maxzoomIndex?.insert(point, newCluster(data)); } /** * Add a lon-lat pair to the cluster * @param lon - longitude in degrees * @param lat - latitude in degrees * @param data - the data associated with the point */ insertLonLat(lon, lat, data) { this.insert(fromLonLat(lon, lat), data); } /** * Insert an STPoint to the index * @param face - the face of the cell * @param s - the s coordinate * @param t - the t coordinate * @param data - the data associated with the point */ insertFaceST(face, s, t, data) { this.insert(fromST(face, s, t), data); } /** * Build the clusters when done adding points * @param cmp_ - custom compare function */ async buildClusters(cmp_) { const { minzoom, maxzoom } = this; const cmp = cmp_ ?? ((_a, _b) => true); for (let zoom = maxzoom - 1; zoom >= minzoom; zoom--) { const curIndex = this.indexes.get(zoom); const queryIndex = this.indexes.get(zoom + 1); if (curIndex === undefined || queryIndex === undefined) throw new Error('Index not found'); await this.#cluster(zoom, queryIndex, curIndex, cmp); } // ensure all point indexes are sorted for (const index of this.indexes.values()) await index.sort(); } /** * @param zoom - the zoom level * @param queryIndex - the index to query * @param currIndex - the index to insert into * @param cmp - the compare function */ async #cluster(zoom, queryIndex, currIndex, cmp) { const radius = this.#getLevelRadius(zoom); for await (const clusterPoint of queryIndex) { const { point, data: clusterData } = clusterPoint; if (clusterData.visited) continue; clusterData.visited = true; // setup a new weighted cluster point const newClusterPoint = mulScalar(point, clusterData.sum); let newNumPoints = clusterData.sum; // joining all points found within radius const points = await queryIndex.searchRadius(point, radius); for (const { point: foundPoint, data: foundData } of points) { // only add points that match or have not been visited already if (!cmp(clusterData.properties, foundData.properties) || foundData.visited) continue; foundData.visited = true; // weighted add to newClusterPoint position addMut(newClusterPoint, mulScalar(foundPoint, foundData.sum)); newNumPoints += foundData.sum; } // finish position average divMutScalar(newClusterPoint, newNumPoints); normalize(newClusterPoint); // store the new cluster point currIndex.insert(newClusterPoint, sumToCluster(clusterData.properties, newNumPoints)); } } /** * @param id - the cell id * @returns - the data within the range of the tile id */ async getCellData(id) { const { minzoom, maxzoom, indexes } = this; const zoom = level(id); if (zoom < minzoom) return; const [min, max] = range(id); const levelIndex = indexes.get(Math.min(zoom, maxzoom)); return await levelIndex?.searchRange(min, max); } /** * @param id - the id of the vector tile * @returns - the vector tile */ async getTile(id) { const data = await this.getCellData(id); if (data === undefined) return; const tile = new Tile(id); for (const { point, data: cluster } of data) { const [face, s, t] = toST(point); const { sum, properties } = cluster; tile.addFeature({ type: 'VectorFeature', face, geometry: { is3D: false, type: 'Point', coordinates: { x: s, y: t, m: { sum } } }, properties, }, this.layerName); } // transform the geometry to be relative to the tile tile.transform(0, this.maxzoom); return tile; } /** * Get a S1ChordAngle relative to a tile zoom level * @param zoom - the zoom level to build a radius * @returns - the appropriate radius for the given zoom */ #getLevelRadius(zoom) { const multiplier = this.radius / this.extent; const cell = fromFacePosLevel(0, 0n, zoom); const [lo, hi] = getVertices(cell); const angle = fromS2Points(lo, hi); return angle * multiplier; } } //# sourceMappingURL=pointCluster.js.map