Add the Data Library: live regional datasets, tiles and packs

DATA LAYERS keeps its curated core layers; regional and niche sources
live in the Data Library, opened from Browse data library at the foot
of the panel. Each entry is a JSON manifest in src/library/entries/
pointing at a live upstream (query API, provider-hosted file or tile
endpoint), so nothing is bundled and a self-hosted instance hosts no
data. Sources without CORS go through one allowlisted /api/library
route. Kinds: dataset (GeoJSON, CSV, JSON records, ArcGIS, partitioned
GeoParquet), tiles (XYZ, WMS/ArcGIS bbox export, terrarium relief) and
pack. Selections persist and travel in share links as lib=, separate
from the core layer tokens. Library rows get Zoom and Remove icon
chips that work while the layer is off, rendered through RailChips.
Strings are translated in every locale.
This commit is contained in:
Said Turksever committed 2026-11-12 12:26:45 +00:00
1 parent 10a788db78
commit db80d82b8d
182 files changed
+15805 -44

No files matched your search

+42
View File
@@ -1,4 +1,5 @@
import { cancelCameraArrival } from './cameraArrival.js';
import { libraryEntryIdOf } from '../library/manifest.js';
function cloneLayerParams(value) {
if (Array.isArray(value)) return value.map(cloneLayerParams);
@@ -97,6 +98,7 @@ export class LayerLifecycle {
this._registrationDispositions = null;
this._allowQaRegistration = allowQaRegistration === true;
this._qaLayerIds = new Set();
this._libraryLayerIds = new Set();
}
register(layerModule) {
@@ -125,6 +127,41 @@ export class LayerLifecycle {
return destroyed;
}
/**
* Register a Data Library layer after the catalog is sealed. Only ids in
* the library namespace (`lib-<entry id>`) are accepted, so this seam can
* never stand in for a core layer, and library layers never join the core
* share-token registry: their selection travels in `lib=` instead.
* @param {object} layerModule
* @returns {string} The registered id.
*/
registerLibraryLayer(layerModule) {
if (!this._registrationsFinalized)
throw new Error('Library layers register after the catalog is sealed');
if (!libraryEntryIdOf(layerModule?.id))
throw new Error('Library layer ids must use the lib- namespace');
this._registerLayer(layerModule);
this._libraryLayerIds.add(layerModule.id);
this._publishActivity({ type: 'registrations', layerId: layerModule.id });
return layerModule.id;
}
/** Destroy a layer registered through `registerLibraryLayer`. */
async unregisterLibraryLayer(layerId) {
if (!this._libraryLayerIds.has(layerId)) return false;
const destroyed = await this.destroyLayer(layerId);
if (destroyed) {
this._libraryLayerIds.delete(layerId);
this._publishActivity({ type: 'registrations', layerId });
}
return destroyed;
}
/** Whether a registered layer came from the Data Library. */
isLibraryLayer(layerId) {
return this._libraryLayerIds.has(layerId);
}
_registerLayer(layerModule) {
if (!layerModule || typeof layerModule.id !== 'string' || !layerModule.id) {
throw new Error('Data layer must provide a stable id');
@@ -2231,6 +2268,7 @@ export class LayerLifecycle {
}
this.layers.delete(layerId);
this._qaLayerIds.delete(layerId);
this._libraryLayerIds.delete(layerId);
return true;
}
@@ -2274,6 +2312,10 @@ export class LayerLifecycle {
// showInTogglePanel). The layer reports stats.keyRequired while that
// key is absent; the panel builds the guidance text from the pair.
requiresKeyId: entry.module.requiresKeyId || null,
// Data Library entry id; the panel groups these under Library.
libraryEntryId: this._libraryLayerIds.has(id)
? libraryEntryIdOf(id)
: null,
enabled: entry.enabled,
lifecycleState: entry.lifecycleState,
lifecycleUncertain: entry.lifecycleUncertain,