s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
319 lines (318 loc) • 13 kB
JavaScript
import { idParent, idToIJ } from 'gis-tools/index.js';
/**
* # Generic Data Source Container
*
* This class is wrapped by many other source types. It serves to handle all the generic cases
* of data sources like fetching metadata, handling flushes, and so on.
*/
export default class Source {
active = true;
/** Resolver letting us know when the source is built */
resolve = () => { };
ready = new Promise((resolve) => {
this.resolve = resolve;
});
name;
path;
type = 'vector'; // how to process the result
extension = 'pbf';
encoding = 'none';
scheme = 'xyz';
projection;
isTimeFormat = false;
attributions = {};
styleLayers;
layers;
minzoom = 0;
maxzoom = 20;
size = 512; // used for raster type sources
faces = new Set();
needsToken;
time;
session;
textEncoder = new TextEncoder();
/**
* @param name - name of the source
* @param projection - the projection used
* @param layers - the style layers that are associated with this source
* @param path - the path to the source to fetch data
* @param needsToken - flag indicating if the source requires a token in the fetch
* @param session - the session that works with the token to make valid fetch requests on source data behind an API
*/
constructor(name, projection, layers, path, needsToken = false, session) {
this.name = name;
this.projection = projection;
this.styleLayers = layers;
this.path = path;
this.needsToken = needsToken;
this.session = session;
}
// if this function runs, we assume default tile source
/**
* If this function runs, we assume a default quad-tree tile source
* @param mapID - the id of the map that is requesting data
* @param metadata - the metadata for the source
*/
async build(mapID, metadata) {
if (metadata === undefined)
metadata = await this._fetch(`${this.path}/metadata.json`, mapID, true);
if (metadata === undefined) {
this.active = false;
console.error(`FAILED TO extrapolate ${this.path} metadata`);
}
else {
this._buildMetadata(metadata, mapID);
}
}
/**
* Internal tool to builds the metadata for the source
* @param metadata - the source metadata
* @param mapID - the id of the map that we will be shipping the render data to
*/
_buildMetadata(metadata, mapID) {
this.active = true; // incase we use a "broken" aproach for metadata and insert later
const minzoom = Number(metadata.minzoom);
const maxzoom = Number(metadata.maxzoom);
this.minzoom = !isNaN(minzoom) ? minzoom : 0;
this.maxzoom = Math.min(!isNaN(maxzoom) ? maxzoom : 20, this.maxzoom);
if (Array.isArray(metadata.faces))
this.faces = new Set(metadata.faces ?? [0, 1, 2, 3, 4, 5]);
if (typeof metadata.extension === 'string')
this.extension = metadata.extension;
this.attributions = metadata.attributions ?? {};
this.type = parseMetaType(metadata.type);
if (typeof metadata.size === 'number')
this.size = metadata.size;
this.encoding = metadata.encoding ?? 'none';
if (typeof metadata.layers === 'object') {
// cleanup the fields property
this.layers = metadata.layers;
}
// other engines that have built data store layer data differently :
const vectorLayers = Array.isArray(metadata.vector_layers)
? metadata.vector_layers
: typeof metadata.json === 'string'
? JSON.parse(metadata.json).vector_layers
: undefined;
if (vectorLayers !== undefined) {
this.layers = {};
for (const layer of vectorLayers) {
if (layer.id === undefined)
continue;
const { minzoom, maxzoom } = layer;
this.layers[layer.id] = {
minzoom: minzoom ?? 0,
maxzoom: maxzoom ?? this.maxzoom,
drawTypes: [],
shape: {},
};
}
}
// time series data check
if (metadata.scheme !== undefined)
this.scheme = metadata.scheme;
else if (metadata.format !== undefined && metadata.format !== 'pbf') {
this.scheme = metadata.format;
this.isTimeFormat = metadata.format === 'tfzxy';
}
if (this.scheme === 'xyz')
this.faces.add(0);
if (this.isTimeFormat) {
postMessage({
mapID,
type: 'timesource',
sourceName: this.name,
interval: metadata.interval,
});
}
// once the metadata is complete, we should check if any tiles were queued
this.resolve();
// if attributions, we send them off
const attributions = { ...this.attributions };
if (Object.keys(attributions).length > 0)
postMessage({ mapID, type: 'attributions', attributions });
}
/**
* All tile requests undergo a basic check on whether that data exists
* within the metadata boundaries. layerIndexes exists to set a boundary
* of what layers the map is interested in (caused by style change add/edit layer)
* @param mapID - the id of the map
* @param tile - the tile
* @param flushMessage - the flush message
*/
async tileRequest(mapID, tile, flushMessage) {
const { layersToBeLoaded } = flushMessage;
// if the source isn't ready yet, we wait for the metadata to be built
await this.ready;
// inject layerIndexes
this.#getLayerIndexes(tile, layersToBeLoaded);
// now make requests for parent data as necessary
this.#getParentData(mapID, tile, layersToBeLoaded);
// pull out data, check if data exists in bounds, then request
const { active, minzoom, maxzoom, faces, name } = this;
const { face, zoom } = tile;
if (
// massive quality check to not over burden servers / lambdas with duds
active && // we have the correct properties to make proper requests
minzoom <= zoom &&
maxzoom >= zoom && // check zoom bounds
faces.has(face) // check the face exists
) {
// request
void this._tileRequest(mapID, tile, name);
}
else {
// flush to let tile know what layers should be cleaned
this._flush(mapID, tile, name);
}
}
/**
* Get the layer indexes that this tile is interested in
* @param tile - the tile request
* @param layersToLoad - the set of layers to load we are going to modify
*/
#getLayerIndexes(tile, layersToLoad) {
const { layers, styleLayers } = this;
const { zoom } = tile;
const layerIndexes = [];
if (layers === undefined)
return;
for (let l = 0, ll = styleLayers.length; l < ll; l++) {
const layer = styleLayers[l];
if (layer === undefined || layers[layer.layer] === undefined)
continue;
const { minzoom, maxzoom } = layers[layer.layer];
if (minzoom <= zoom && maxzoom >= zoom)
layerIndexes.push(layer.layerIndex);
}
tile.layerIndexes = layerIndexes;
for (const index of layerIndexes)
layersToLoad.add(index);
}
/**
* Get the parent data that this tile is interested in
* @param mapID - the id of the map that is requesting
* @param tile - the tile request
* @param layersToLoad - the set of layers to load
*/
#getParentData(mapID, tile, layersToLoad) {
const { layers, styleLayers, name } = this;
if (layers === undefined)
return;
// pull out data
const { time, face, zoom, id } = tile;
// setup parentLayers
const parentLayers = {};
// iterate over layers and found any data doesn't exist at current zoom but the style asks for
for (const { layer, layerIndex, maxzoom } of styleLayers) {
const sourceLayer = layers[layer];
const sourceLayerMaxZoom = sourceLayer?.maxzoom;
if (maxzoom > zoom && sourceLayer !== undefined && sourceLayerMaxZoom < zoom) {
// we have passed the limit at which this data is stored. Rather than
// processing the data more than once, we reference where to look for the layer
let pZoom = zoom;
let newID = id;
while (pZoom > sourceLayerMaxZoom) {
pZoom--;
newID = idParent(newID);
}
const newIDString = newID.toString();
// pull out i & j
const [, i, j] = idToIJ(newID, pZoom);
// store parent reference
if (parentLayers[newIDString] === undefined) {
parentLayers[newIDString] = {
time,
face,
id: newID,
zoom: pZoom,
i,
j,
layerIndexes: [],
};
}
parentLayers[newIDString].layerIndexes.push(layerIndex);
// filter out the index from the tile
tile.layerIndexes?.filter((index) => index !== layerIndex);
}
}
// if we stored any parent layers, make the necessary requests
for (const parent of Object.values(parentLayers)) {
for (const index of parent.layerIndexes)
layersToLoad.add(index);
void this._tileRequest(mapID, { ...tile, parent }, name);
}
}
/**
* If this function runs, we assume default quad-tree tile source.
* In the default case, we want the worker to process the data
* @param mapID - the id of the map to ship the eventual render data back to
* @param tile - the tile request
* @param sourceName - the source name the data to belongs to
*/
async _tileRequest(mapID, tile, sourceName) {
const { path, session, type, extension, size } = this;
const { parent } = tile;
const { time, face, zoom, i, j } = parent ?? tile;
const location = `${time !== undefined ? String(time) + '/' : ''}` +
(this.scheme === 'xyz'
? `${zoom}/${i}/${j}.${extension}`
: `${face}/${zoom}/${i}/${j}.${extension}`);
const data = await this._fetch(`${path}/${location}`, mapID);
if (data !== undefined) {
const worker = session.requestWorker();
worker.postMessage({ mapID, type, tile, sourceName, data, size }, [data]);
}
else {
this._flush(mapID, tile, sourceName);
}
}
/**
* 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
* @param tile - the tile request
* @param sourceName - the source name the data to belongs to
*/
_flush(mapID, tile, sourceName) {
const { textEncoder, session } = this;
// compress
const data = textEncoder.encode('{"layers":{}}').buffer;
// send off
const worker = session.requestWorker();
worker.postMessage({ mapID, type: 'jsondata', tile, sourceName, data }, [data]);
}
/**
* Fetch a tile
* @param path - the base path to the tile data
* @param mapID - the id of the map
* @param json - flag indicating if the data is json
* @returns the raw data or JSON metadata if found
*/
async _fetch(path, mapID, json = false) {
const headers = {};
if (this.needsToken) {
const Authorization = await this.session.requestSessionToken(mapID);
if (Authorization === 'failed')
return;
if (Authorization !== undefined)
headers.Authorization = Authorization;
}
const res = await fetch(path, { headers });
if (res.status !== 200 && res.status !== 206)
return;
if (json || (res.headers.get('content-type') ?? '').includes('application/json'))
return await res.json();
return (await res.arrayBuffer());
}
}
/**
* Basic parsing tool to ensure the source type is valid
* @param type - the source type
* @returns the parsed source type
*/
function parseMetaType(type = '') {
if (['vector', 'json', 'raster', 'raster-dem', 'sensor', 'overlay'].includes(type))
return type;
return 'vector';
}