mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Each overlay is now its own Infrastructure row instead of a chip on one shared row: sea-marks (token 6) and ski-pistes (token 7), both enabled-only, built by createOsmOverlayLayers() from one descriptor list. Drops the option group and its share-codec defaults, gives each layer its own name, voice entry and CURRENT-STATE row, and shifts the token-ledger fixtures to the next free digit 8.
191 lines
6.1 KiB
JavaScript
191 lines
6.1 KiB
JavaScript
import * as Cesium from 'cesium';
|
|
import { NO_IMAGERY_HOST, resolveImageryHost } from '../maps/imageryHost.js';
|
|
|
|
/**
|
|
* @file Open-data raster overlays: OpenSeaMap sea marks and OpenSnowMap ski
|
|
* pistes (#10). Each overlay is its own layer with its own row under
|
|
* Infrastructure; both are built from the descriptors below.
|
|
*
|
|
* OpenRailwayMap and OpenInfraMap, also named in #10, are deliberately not
|
|
* included:
|
|
* - OpenRailwayMap's public tile server (tiles.openrailwaymap.org) only
|
|
* serves requests whose Referer is openrailwaymap.org; every other Referer,
|
|
* including none, gets 403. A tab on this app sends its own origin, so the
|
|
* tiles would fail, and embedding them would run against their tile-usage
|
|
* policy either way.
|
|
* - OpenInfraMap serves MapLibre vector tiles, not raster XYZ tiles. That
|
|
* needs a vector-tile rendering pipeline, not an imagery-layer drop-in.
|
|
*
|
|
* The overlays drape on whatever surface the active map stack offers (see
|
|
* `maps/imageryHost.js`): the globe on the OSM and Bing stacks, the Google
|
|
* Photorealistic tileset's own `imageryLayers` on Google 3D. Only a stack with
|
|
* neither leaves them hidden, and the row says so.
|
|
* @module data/osmOverlays
|
|
*/
|
|
|
|
/** @type {ReadonlyArray<{id:string,name:string,icon:string,source:string,urlTemplate:string,credit:string,maximumLevel:number}>} */
|
|
export const OSM_OVERLAYS = Object.freeze([
|
|
Object.freeze({
|
|
id: 'sea-marks',
|
|
name: 'Sea Marks',
|
|
icon: '⚓',
|
|
source: 'OpenSeaMap',
|
|
urlTemplate: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',
|
|
// Chart tiles CC BY-SA 2.0; the underlying data is OpenStreetMap (ODbL).
|
|
credit:
|
|
'Sea marks © OpenSeaMap contributors (CC BY-SA 2.0) · Data © OpenStreetMap contributors (ODbL)',
|
|
maximumLevel: 18,
|
|
}),
|
|
Object.freeze({
|
|
id: 'ski-pistes',
|
|
name: 'Ski Pistes',
|
|
icon: '⛷',
|
|
source: 'OpenSnowMap',
|
|
urlTemplate: 'https://www.opensnowmap.org/pistes/{z}/{x}/{y}.png',
|
|
// OpenSnowMap asks for "(c)www.opensnowmap.org" wherever its tiles appear.
|
|
credit:
|
|
'Ski pistes © www.opensnowmap.org (CC BY-SA) · Data © OpenStreetMap contributors (ODbL)',
|
|
maximumLevel: 18,
|
|
}),
|
|
]);
|
|
|
|
/** Why an enabled overlay is showing nothing, or null. */
|
|
export function osmOverlayHiddenReason(enabled, hostKind) {
|
|
return enabled && hostKind === 'none' ? NO_IMAGERY_HOST : null;
|
|
}
|
|
|
|
/**
|
|
* Create the layer for one overlay descriptor. Each application catalog gets
|
|
* its own instances; the Cesium factories are injectable so tests run without
|
|
* WebGL.
|
|
*/
|
|
export function createOsmOverlayLayer(
|
|
overlay,
|
|
{
|
|
host = resolveImageryHost,
|
|
createProvider = (descriptor) =>
|
|
new Cesium.UrlTemplateImageryProvider({
|
|
url: descriptor.urlTemplate,
|
|
credit: descriptor.credit,
|
|
maximumLevel: descriptor.maximumLevel,
|
|
}),
|
|
createImageryLayer = (provider) => new Cesium.ImageryLayer(provider),
|
|
} = {},
|
|
) {
|
|
if (!OSM_OVERLAYS.includes(overlay))
|
|
throw new TypeError(`Unknown open map overlay: ${overlay?.id}`);
|
|
let viewer = null;
|
|
let mapStack = null;
|
|
let unsubscribeMap = null;
|
|
let enabled = false;
|
|
let hostInfo = { collection: null, kind: 'none' };
|
|
/** The imagery layer this instance owns while enabled, and where it sits. */
|
|
let owned = null;
|
|
|
|
const usable = (collection) => collection && !collection.isDestroyed?.();
|
|
|
|
function detach(destroy) {
|
|
if (!owned) return;
|
|
const { collection, layer } = owned;
|
|
if (usable(collection) && collection.contains(layer))
|
|
collection.remove(layer, destroy);
|
|
else if (destroy && !layer.isDestroyed?.()) layer.destroy?.();
|
|
owned.collection = null;
|
|
if (destroy) owned = null;
|
|
}
|
|
|
|
function resolveHost() {
|
|
return (
|
|
host({ viewer, tileset: mapStack?.getImageryHostTileset?.() }) || {
|
|
collection: null,
|
|
kind: 'none',
|
|
}
|
|
);
|
|
}
|
|
|
|
/** Create, move or drop the owned imagery layer to match the state. */
|
|
function sync() {
|
|
if (!viewer) return;
|
|
hostInfo = enabled ? resolveHost() : { collection: null, kind: 'none' };
|
|
if (!enabled) {
|
|
detach(true);
|
|
} else {
|
|
owned ||= {
|
|
layer: createImageryLayer(createProvider(overlay)),
|
|
collection: null,
|
|
};
|
|
if (owned.collection !== hostInfo.collection) {
|
|
detach(false);
|
|
if (usable(hostInfo.collection)) {
|
|
// Appended, so it draws above the map stack's base imagery.
|
|
hostInfo.collection.add(owned.layer);
|
|
owned.collection = hostInfo.collection;
|
|
}
|
|
}
|
|
}
|
|
viewer.scene?.requestRender?.();
|
|
}
|
|
|
|
return {
|
|
id: overlay.id,
|
|
name: overlay.name,
|
|
icon: overlay.icon,
|
|
source: overlay.source,
|
|
updateInterval: 0,
|
|
init(nextViewer) {
|
|
viewer = nextViewer;
|
|
},
|
|
/** Follow map-stack switches so the drape moves with the surface. */
|
|
attachMapStackController(controller) {
|
|
unsubscribeMap?.();
|
|
unsubscribeMap = null;
|
|
mapStack = controller || null;
|
|
const off = controller?.subscribe?.(() => {
|
|
if (enabled) sync();
|
|
});
|
|
unsubscribeMap = typeof off === 'function' ? off : null;
|
|
},
|
|
enable() {
|
|
enabled = true;
|
|
sync();
|
|
},
|
|
disable() {
|
|
enabled = false;
|
|
sync();
|
|
},
|
|
update() {
|
|
return true;
|
|
},
|
|
getStats() {
|
|
// Without a controller subscription the stats poll doubles as the
|
|
// map-stack watch.
|
|
if (enabled && !unsubscribeMap) {
|
|
const next = resolveHost();
|
|
if (next.collection !== hostInfo.collection) sync();
|
|
}
|
|
return {
|
|
count: enabled ? 1 : 0,
|
|
error: osmOverlayHiddenReason(enabled, hostInfo.kind),
|
|
};
|
|
},
|
|
/** Owned layer count and host kind, for tests and live gates. */
|
|
diagnostics() {
|
|
return { host: hostInfo.kind, ownedCount: owned ? 1 : 0 };
|
|
},
|
|
destroy() {
|
|
enabled = false;
|
|
detach(true);
|
|
unsubscribeMap?.();
|
|
unsubscribeMap = null;
|
|
mapStack = null;
|
|
viewer = null;
|
|
hostInfo = { collection: null, kind: 'none' };
|
|
},
|
|
};
|
|
}
|
|
|
|
/** One layer per overlay, in panel order. */
|
|
export function createOsmOverlayLayers(options) {
|
|
return OSM_OVERLAYS.map((overlay) => createOsmOverlayLayer(overlay, options));
|
|
}
|