s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
169 lines (168 loc) • 5.78 kB
JavaScript
import { convert, transformPoint } from 'gis-tools/index.js';
/**
* # Marker Source
*
* Store, process, and render markers. Handles both WM and S2 projections
*/
export default class MarkerSource {
name;
projection = 'S2';
isTimeFormat = false;
styleLayers;
idGen = 0;
0 = new Map();
1 = new Map();
2 = new Map();
3 = new Map();
4 = new Map();
5 = new Map();
session;
textEncoder = new TextEncoder();
/**
* @param name - name of the source
* @param session - the session associated with the source data
* @param projection - the projection to use (WM or S2)
* @param layers - the style layers associated with this source
*/
constructor(name, session, projection, layers) {
this.name = name;
this.session = session;
this.projection = projection;
this.styleLayers = layers;
}
/**
* Build the source
* @param _mapID - the id of the map (unused)
* @param metadata - the metadata associated with the source
*/
build(_mapID, metadata) {
const { projection } = this;
const json = metadata?.data;
if (json !== undefined) {
const features = convert(projection === 'WM' ? 'WG' : 'S2', json, true, true);
for (const feature of features) {
const { id, face, properties, metadata } = feature;
if (feature.geometry.type === 'Point') {
this.addMarker({ point: feature.geometry.coordinates, face, properties, id, metadata });
}
else if (feature.geometry.type === 'MultiPoint') {
for (const point of feature.geometry.coordinates) {
this.addMarker({ point, face, properties, id, metadata });
}
}
}
}
}
/**
* Add marker to the source
* @param marker - the marker to add
*/
addMarker(marker) {
let { point, face, properties, id, metadata } = marker;
// if no id, let's create one
if (id === undefined) {
id = this.idGen++;
if (this.idGen >= Number.MAX_SAFE_INTEGER)
this.idGen = 0;
}
// store
properties.__markerID = id;
this[face ?? 0].set(id, {
id,
metadata,
properties: { __markerID: id, ...properties },
geometry: point,
});
}
/**
* Delete marker(s)
* @param ids - the id(s) of the marker(s) to delete
*/
deleteMarkers(ids) {
for (const id of ids) {
this[0].delete(id);
this[1].delete(id);
this[2].delete(id);
this[3].delete(id);
this[4].delete(id);
this[5].delete(id);
}
}
/**
* Process a tile request
* @param mapID - the id of the map that is requesting data
* @param tile - the tile request
* @param flushMessage - the flush message function to call on completion
*/
tileRequest(mapID, tile, flushMessage) {
const { name } = this;
const { face, zoom, bbox, i, j } = tile;
const tileZoom = 1 << zoom;
const features = [];
// get bounds of tile
const [minS, minT, maxS, maxT] = bbox;
// find all markers in st bounds
for (const [, marker] of this[face]) {
const { properties, geometry } = marker;
const { x, y } = geometry;
if (x >= minS && x < maxS && y >= minT && y < maxT) {
const geometry = { x, y };
transformPoint(geometry, tileZoom, i, j);
features.push({
type: 'VectorFeature',
properties,
geometry: { type: 'Point', is3D: false, coordinates: geometry },
});
}
}
// if markers fit within bounds, create a tile
const length = features.length;
// Flush and return
if (length === 0) {
this._flush(mapID, tile);
return;
}
// build data object
const data = {
extent: 1,
face,
zoom,
i,
j,
layers: { default: { extent: 1, features, length: features.length } },
};
// encode for transfer
const uint8data = this.textEncoder.encode(JSON.stringify(data)).buffer;
// request a worker and post
const worker = this.session.requestWorker();
worker.postMessage({ mapID, type: 'jsondata', tile, sourceName: name, data: uint8data }, [
uint8data,
]);
// let the source know we are loading a layer
this.#sourceFlush(flushMessage);
}
/**
* If no data, we still have to let the tile worker know so it can prepare a proper flush
* as well as manage cases like "invert" type data.
* @param mapID - the id of the map that is requesting data
* @param tile - the tile request
*/
_flush(mapID, tile) {
const { textEncoder, session, name } = this;
// compress
const data = textEncoder.encode('{"layers":{}}').buffer;
// send off
const worker = session.requestWorker();
worker.postMessage({ mapID, type: 'jsondata', tile, sourceName: name, data }, [data]);
}
/**
* Flush protocol for the source
* @param flushMessage - the flush message function
*/
#sourceFlush(flushMessage) {
const { name } = this;
const layers = this.styleLayers.filter((layer) => layer.source === name);
for (const { layerIndex } of layers)
flushMessage.layersToBeLoaded.add(layerIndex);
}
}