UNPKG

s2maps-gpu

Version:

S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.

172 lines (171 loc) 6.14 kB
import PointIndex from './pointIndex.js'; import { Tile, convert, idLevel, idToIJ } from 'gis-tools/index.js'; /** * Default comparator * @param a - first feature * @param b - comparison feature * @returns true if the `metadata.layer`s are the same */ function defaultCmp(a, b) { return a.metadata?.layer === b.metadata?.layer; } const DEFAULT_OPTIONS = { minzoom: 0, // min zoom to generate clusters on maxzoom: 16, // max zoom level to cluster the points on radius: 50, // cluster radius in pixels extent: 512, // tile extent (radius is calculated relative to it) nodeSize: 64, // size of the KD-tree leaf node, effects performance }; /** * # Point Cluster * * A point cluster for Web Mercator tiles */ export default class PointCluser { minzoom; maxzoom; options = DEFAULT_OPTIONS; base; indexes = []; points = []; faces = new Set([0]); projection = 'WM'; /** @param options - cluster options */ constructor(options = {}) { this.options = { ...this.options, ...options }; this.minzoom = this.options.minzoom; this.maxzoom = this.options.maxzoom; this.base = new PointIndex(this.options.nodeSize); let i = this.options.minzoom; while (i <= this.options.maxzoom) { this.indexes.push(new PointIndex(this.options.nodeSize)); i++; } } /** * Add a collection of points * @param data - a collection of points to add */ addManyPoints(data) { const features = convert('WG', data, undefined, true); for (const feature of features) { const { type, coordinates } = feature.geometry; if (type === 'Point') { this.addPoint(feature); } else if (type === 'MultiPoint') { for (const point of coordinates) { const { x: s, y: t, m } = point; this.addPoint({ type: 'VectorFeature', geometry: { type: 'Point', is3D: false, coordinates: { x: s, y: t, m } }, }); } } } } /** * Add a single point * @param point - a point to add */ addPoint(point) { const cluster = { ref: point, visited: false, sum: 1 }; this.base.add(point.geometry.coordinates.x, point.geometry.coordinates.y, cluster); } /** * Cluster the points * @param cmp - optional comparator function */ cluster(cmp) { if (cmp === undefined) cmp = defaultCmp; let zoom = this.options.maxzoom; while (zoom >= this.options.minzoom) { const currIndex = this.indexes[zoom]; if (zoom === this.options.maxzoom) { this.#cluster(zoom, this.base, currIndex, cmp); } else { this.#cluster(zoom, this.indexes[zoom + 1], currIndex, cmp); } zoom--; } } /** * Get a tile from the point cluster * @param id - the tile id * @returns a tile filled with cluster points that are in the tile */ getTile(id) { const { radius, extent, maxzoom } = this.options; const level = idLevel(id); const [zoom, i, j] = idToIJ(id, level); const tile = new Tile(id); const index = zoom < maxzoom ? this.indexes[zoom] : this.base; const z2 = Math.pow(2, zoom); const p = radius / extent; const top = (j - p) / z2; const bottom = (j + 1 + p) / z2; const results = []; results.push(...index.range((i - p) / z2, top, (i + 1 + p) / z2, bottom)); if (i === 0) results.push(...index.range((z2 - p) / z2, top, 1, bottom)); else if (i === z2 - 1.0) results.push(...index.range(0, top, (p + 1.0) / z2, bottom)); // lastly, build features for (const cluster of results) { const { ref, sum } = cluster.data; // prep layer const layerName = ref.metadata?.layer ?? 'default'; // prep feature tile.addFeature({ type: 'VectorFeature', geometry: { type: 'Point', is3D: false, coordinates: { x: cluster.x, y: cluster.y }, }, properties: { ...ref.properties, __cluster: sum > 1, __sum: sum }, }, layerName); } tile.transform(0, this.maxzoom); return tile; } /** * Cluster the points * @param level - the zoom level * @param queryIndex - the query index * @param currIndex - the current index to store the resulting clusters to * @param cmp - the comparator */ #cluster(level, queryIndex, currIndex, cmp) { const { extent } = this.options; const radius = this.options.radius / (extent * Math.pow(2, level)); for (const cluster of queryIndex.points) { if (cluster.data.visited) continue; cluster.data.visited = true; // prep the new cluster data let sum = cluster.data.sum; let x = cluster.x * sum; let y = cluster.y * sum; // joining all points found within radius for (const foundPoint of queryIndex.radius(cluster.x, cluster.y, radius)) { if (foundPoint.data.visited || !cmp(foundPoint.data.ref, cluster.data.ref)) continue; // add the point to the new cluster x += foundPoint.x * foundPoint.data.sum; y += foundPoint.y * foundPoint.data.sum; sum += foundPoint.data.sum; foundPoint.data.visited = true; } // create the new point and add it to the current index const newClusterPoint = { x: x / sum, y: y / sum, data: { ref: cluster.data.ref, visited: false, sum }, }; currIndex.addPoint(newClusterPoint); } } }