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
JavaScript
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);
}
}
}