refactor(maps): separate source factories and scene coordination

This commit is contained in:
Sameh Khamis committed 2026-09-13 18:59:29 -07:00
1 parent 561a781099
commit e59282dc9c
22 files changed
+1807 -798

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
import { keySetupRequirement } from '../keySetupCore.mjs';
/**
* Why Google 3D is unavailable, phrased so the tooltip and toast recommend the
* RIGHT fix. With no credentials the fix is a key (or the ion route); with a
* key or ion token configured, the tileset failed for another reason —
* restrictions, quota, an EEA-billed key, or the network — and telling the
* user to add a key they already added is the wrong advice.
* @param {boolean} hasCredentials
* @returns {string}
*/
export function photorealUnavailableReason(hasCredentials) {
if (hasCredentials)
return "Google 3D tiles unavailable — check the key's API restrictions, quota, or network";
return `${keySetupRequirement('google-maps')} — or a Cesium ion token for the ion-hosted route`;
}
+40
View File
@@ -0,0 +1,40 @@
import * as Cesium from 'cesium';
export const MAP_STACKS = [
{
id: 'photoreal',
label: 'Google 3D',
shortLabel: '3D',
kind: 'photoreal',
requiresIon: false,
},
{
id: 'bing-aerial',
label: 'Bing Aerial',
shortLabel: 'Aerial',
kind: 'ion',
style: Cesium.IonWorldImageryStyle.AERIAL,
requiresIon: true,
},
{
id: 'bing-labels',
label: 'Bing Labels',
shortLabel: 'Labels',
kind: 'ion',
style: Cesium.IonWorldImageryStyle.AERIAL_WITH_LABELS,
requiresIon: true,
},
{
id: 'esri-imagery',
label: 'Esri Satellite',
shortLabel: 'SAT',
kind: 'esri-imagery',
requiresIon: false,
},
{
id: 'osm',
label: 'OSM',
shortLabel: 'OSM',
kind: 'osm',
requiresIon: false,
},
];
+335
View File
@@ -0,0 +1,335 @@
import { indexMapSources } from './registry.js';
import * as Cesium from 'cesium';
import { createMapCredits } from './credits.js';
/** Coordinate source lifetimes and scene changes; the registry owns provider choices. */
export class MapSourceController {
constructor(
viewer,
{
registry,
initialStack,
onChange = null,
onError = null,
requestRender = () => viewer?.scene?.requestRender?.(),
createImageryLayer = (provider) => new Cesium.ImageryLayer(provider),
},
) {
this.viewer = viewer;
this._registry = registry;
this._sources = indexMapSources(registry.sources);
this._activeId = this.isStackAvailable(initialStack)
? initialStack
: registry.defaultId;
this._onChange = onChange;
this._onError = onError;
this._requestRender = requestRender;
this._createImageryLayer = createImageryLayer;
this._credits = createMapCredits(viewer);
this._abort = new AbortController();
this._imageryProviders = new Map();
this._terrainProviders = new Map();
this._tilesets = new Map();
this._ownedTilesets = new Set();
this._disposed = new WeakSet();
this._switchGen = 0;
this._isSwitching = false;
this._lastError = null;
this._imageryLayer = null;
this._activeImageryProvider = null;
this._removeImageryErrorListener = null;
this._terrainMode = null;
this._destroyed = false;
}
getStack(id) {
return this._sources.get(id)?.descriptor || null;
}
getStacks() {
return [...this._sources.values()].map(({ descriptor }) => {
const stack = descriptor;
const available = this.isStackAvailable(stack.id);
return {
...stack,
available,
unavailableReason: available ? null : this._unavailableReason(stack),
};
});
}
isStackAvailable(id) {
const source = this._sources.get(id);
return Boolean(source && source.available !== false);
}
_unavailableReason(stack) {
return (
this._sources.get(stack?.id)?.unavailableReason ||
`${stack?.label || 'This map stack'} is unavailable`
);
}
getActiveId() {
return this._activeId;
}
getActiveStack() {
return this.getStack(this._activeId);
}
getSwitchGeneration() {
return this._switchGen;
}
getState(status = this._isSwitching ? 'switching' : 'ready') {
return {
activeId: this._activeId,
activeStack: this.getActiveStack(),
stacks: this.getStacks(),
status,
lastError: this._lastError,
...this._registry.state,
};
}
_emitChange(status) {
this._onChange?.(this.getState(status));
}
async setStack(id, { silent = false } = {}) {
if (this._destroyed) return this.getState();
const stack = this.getStack(id) || this.getStack(this._registry.unknownId);
if (!stack) return null;
if (!this.isStackAvailable(stack.id)) {
const message = this._unavailableReason(stack);
this._lastError = message;
this._onError?.(message, stack);
return this.getState();
}
const gen = ++this._switchGen;
this._isSwitching = true;
this._lastError = null;
if (!silent) this._emitChange('switching');
try {
const activation = await this._activate(stack, gen);
if (gen !== this._switchGen) return this.getState();
this._activeId = activation?.effectiveStackId || stack.id;
if (activation?.fallbackMessage) {
this._lastError = activation.fallbackMessage;
this._onError?.(activation.fallbackMessage, stack);
}
this._requestRender('map-stack');
if (!silent) this._emitChange('ready');
} catch (error) {
if (gen !== this._switchGen) return this.getState();
const message = error?.message || String(error);
this._lastError = message;
this._onError?.(message, stack);
const recovery = this.getStack(this._registry.recoveryId);
if (
recovery &&
recovery.id !== stack.id &&
this.isStackAvailable(recovery.id)
) {
await this._activate(recovery, gen);
if (gen !== this._switchGen) return this.getState();
this._activeId = recovery.id;
}
if (!silent) this._emitChange('error');
} finally {
if (gen === this._switchGen) this._isSwitching = false;
}
return this.getState();
}
_activate(stack, gen) {
const source = this._sources.get(stack.id);
return source.imagery
? this._activateGlobeStack(stack, gen)
: this._activateTileset(source, gen);
}
async _activateTileset(source, gen) {
let tileset = source.tileset;
if (!tileset) {
if (!source.createTileset)
throw new Error(`Missing 3D source: ${source.descriptor.id}`);
tileset = await this._cached(
this._tilesets,
source.descriptor.id,
async () => {
const value = await source.createTileset({
signal: this._abort.signal,
});
if (this._destroyed) {
this._dispose(value);
return value;
}
value.show = false;
this.viewer.scene.primitives.add(value);
this._ownedTilesets.add(value);
return value;
},
);
}
if (gen !== this._switchGen) return;
this._removeImageryLayer();
this._credits.show(source.credit || null);
this._showTileset(tileset);
this.viewer.scene.globe.show = false;
// Terrain is intentionally untouched while the globe is hidden.
}
_showTileset(active) {
for (const source of this._sources.values())
if (source.tileset) source.tileset.show = source.tileset === active;
for (const tileset of this._ownedTilesets)
tileset.show = tileset === active;
}
async _activateGlobeStack(stack, gen) {
const resolution = await this._getImageryProvider(stack);
if (gen !== this._switchGen) return;
this._removeImageryLayer();
this._imageryLayer = this._createImageryLayer(resolution.provider);
this._activeImageryProvider = resolution.provider;
this.viewer.imageryLayers.add(this._imageryLayer, 0);
const source = this._sources.get(resolution.effectiveStackId);
this._credits.show(source?.credit || null);
this._watchProvider(resolution, gen);
this._showTileset(null);
this.viewer.scene.globe.show = true;
this._activeId = resolution.effectiveStackId;
if (source?.terrain && source.terrain.id !== this._terrainMode) {
const terrain = source.terrain;
const result = await this._cached(
this._terrainProviders,
terrain.id,
() => terrain.create({ signal: this._abort.signal }),
);
if (gen !== this._switchGen) return;
if (result.terrain) this.viewer.scene.setTerrain(result.terrain);
else this.viewer.terrainProvider = result.provider;
this._terrainMode = terrain.id;
}
return resolution;
}
_cached(cache, id, create) {
if (cache.has(id)) return cache.get(id);
const promise = Promise.resolve()
.then(() => {
this._abort.signal.throwIfAborted();
return create();
})
.catch((error) => {
if (cache.get(id) === promise) cache.delete(id);
throw error;
});
cache.set(id, promise);
return promise;
}
_getImageryProvider(stack, visited = new Set()) {
if (visited.has(stack.id))
return Promise.reject(new Error('Map source fallback cycle'));
return this._cached(this._imageryProviders, stack.id, async () => {
const source = this._sources.get(stack.id);
try {
const provider = await source.imagery({ signal: this._abort.signal });
if (this._destroyed) this._dispose(provider);
return { provider, effectiveStackId: stack.id, fallbackMessage: null };
} catch (error) {
const fallback = source.constructionFallback;
if (this._destroyed || !fallback || !this.isStackAvailable(fallback.id))
throw error;
const next = new Set(visited).add(stack.id);
const resolution = await this._getImageryProvider(
this.getStack(fallback.id),
next,
);
return { ...resolution, fallbackMessage: fallback.message };
}
});
}
_watchProvider(resolution, gen) {
const fallback = this._sources.get(
resolution.effectiveStackId,
)?.tileFailureFallback;
const errorEvent = resolution.provider?.errorEvent;
if (!fallback || !errorEvent?.addEventListener) return;
let failures = 0;
let pending = false;
this._removeImageryErrorListener = errorEvent.addEventListener((error) => {
if (
gen !== this._switchGen ||
this._activeImageryProvider !== resolution.provider
)
return;
const retryCount = Number(error?.timesRetried);
failures =
Number.isInteger(retryCount) && retryCount >= 0
? Math.max(failures + 1, retryCount + 1)
: failures + 1;
if (failures < fallback.threshold || pending) return;
pending = true;
this._onError?.(
fallback.message,
this.getStack(resolution.effectiveStackId),
);
const expectedGen = this._switchGen + 1;
void this.setStack(fallback.id, { silent: true })
.then((state) => {
if (
!this._destroyed &&
this._switchGen === expectedGen &&
state?.activeId === fallback.id
) {
this._lastError = fallback.message;
this._emitChange('error');
}
})
.finally(() => {
pending = false;
});
});
}
_removeImageryLayer() {
this._removeImageryErrorListener?.();
this._removeImageryErrorListener = null;
if (this._imageryLayer)
this.viewer.imageryLayers.remove(this._imageryLayer, true);
this._imageryLayer = null;
this._activeImageryProvider = null;
}
_dispose(value) {
if (!value || this._disposed.has(value)) return;
this._disposed.add(value);
if (typeof value.destroy === 'function' && !value.isDestroyed?.())
value.destroy();
}
destroy() {
if (this._destroyed) return;
this._destroyed = true;
this._switchGen++;
this._abort.abort();
this._isSwitching = false;
this._removeImageryLayer();
this._credits.destroy();
for (const promise of this._imageryProviders.values())
void Promise.resolve(promise).then(
(value) => this._dispose(value.provider),
() => {},
);
for (const promise of this._terrainProviders.values())
void Promise.resolve(promise).then(
(value) => this._dispose(value.provider),
() => {},
);
for (const tileset of this._ownedTilesets) {
this.viewer.scene.primitives.remove(tileset);
this._dispose(tileset);
}
this._imageryProviders.clear();
this._terrainProviders.clear();
this._tilesets.clear();
this._ownedTilesets.clear();
this._onChange = null;
this._onError = null;
}
}
+300
View File
@@ -0,0 +1,300 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { MapSourceController } from './controller.js';
import { createDefaultMapSources } from './defaultSources.js';
function event() {
const listeners = new Set();
return {
addEventListener(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
},
raise(value = {}) {
for (const fn of [...listeners]) fn(value);
},
get size() {
return listeners.size;
},
};
}
function fixture(registry, options = {}) {
const imagery = [],
credits = new Set(),
primitives = [],
removed = [],
changes = [];
const viewer = {
scene: {
globe: { show: true },
requestRender() {},
frameState: {
creditDisplay: {
addStaticCredit(value) {
credits.add(value);
},
removeStaticCredit(value) {
credits.delete(value);
},
},
},
primitives: {
add(value) {
primitives.push(value);
},
remove(value) {
primitives.splice(primitives.indexOf(value), 1);
value.destroy?.();
},
},
},
imageryLayers: {
add(value) {
imagery.push(value);
},
remove(value, destroy) {
imagery.splice(imagery.indexOf(value), 1);
removed.push({ value, destroy });
},
},
};
const controller = new MapSourceController(viewer, {
registry,
createImageryLayer: (provider) => ({ provider }),
onChange: (state) => changes.push(state),
...options,
});
return { viewer, controller, imagery, credits, primitives, removed, changes };
}
const settle = async () => {
for (let i = 0; i < 20; i++) await Promise.resolve();
};
const descriptor = (id) => ({ id, label: id, kind: 'imagery' });
function publicFixture() {
const tileset = { show: true };
const registry = createDefaultMapSources({ googleTileset: tileset });
const providers = new Map();
for (const source of registry.sources) {
if (!source.imagery) continue;
const provider = { id: source.descriptor.id, errorEvent: event() };
providers.set(source.descriptor.id, provider);
source.imagery = async () => provider;
source.terrain = {
id: 'keyless',
create: async () => ({ provider: { id: 'terrain' } }),
};
}
return { ...fixture(registry), registry, providers, tileset };
}
test('an additional imagery source needs no controller branch and owns its cached resources', async () => {
let reads = 0,
destroys = 0,
terrainReads = 0;
const provider = {
destroy() {
destroys++;
},
};
const terrain = {
id: 'custom-floor',
create: async () => {
terrainReads++;
return { provider: { id: 'terrain' } };
},
};
const registry = {
defaultId: 'custom',
sources: [
{
descriptor: descriptor('custom'),
imagery: async () => {
reads++;
return provider;
},
terrain,
},
],
};
const env = fixture(registry);
await env.controller.setStack('custom');
await env.controller.setStack('custom');
assert.equal(env.controller.getActiveId(), 'custom');
assert.equal(env.viewer.terrainProvider.id, 'terrain');
assert.equal(reads, 1);
assert.equal(terrainReads, 1);
assert.equal(env.imagery.length, 1);
assert.equal(env.removed[0].destroy, true);
env.controller.destroy();
env.controller.destroy();
await settle();
assert.equal(env.imagery.length, 0);
assert.equal(destroys, 1);
});
test('a destroyed controller aborts creation and disposes a late provider without touching the scene', async () => {
let resolve,
signal,
destroyed = 0;
const env = fixture({
defaultId: 'slow',
sources: [
{
descriptor: descriptor('slow'),
imagery: (request) => {
signal = request.signal;
return new Promise((done) => {
resolve = done;
});
},
},
],
});
const loading = env.controller.setStack('slow');
await settle();
env.controller.destroy();
assert.equal(signal.aborted, true);
resolve({
destroy() {
destroyed++;
},
});
await loading;
await settle();
assert.equal(destroyed, 1);
assert.equal(env.imagery.length, 0);
assert.deepEqual(
env.changes.map((state) => state.status),
['switching'],
);
});
test('slow terrain cannot overwrite a newer source and a 3D view never starts unused terrain', async () => {
const env = publicFixture();
let resolve,
reads = 0;
const osm = env.registry.sources.find(
(source) => source.descriptor.id === 'osm',
);
osm.terrain = {
id: 'slow-floor',
create: () => {
reads++;
return new Promise((done) => {
resolve = done;
});
},
};
await env.controller.setStack('photoreal');
assert.equal(reads, 0);
const loading = env.controller.setStack('osm');
await settle();
await env.controller.setStack('photoreal');
resolve({ provider: { id: 'late-terrain' } });
await loading;
assert.equal(env.viewer.terrainProvider, undefined);
assert.equal(env.viewer.scene.globe.show, false);
assert.equal(env.tileset.show, true);
assert.equal(env.controller.getActiveId(), 'photoreal');
env.controller.destroy();
});
test('Esri construction fallback reports and attributes the source actually rendered', async () => {
const env = publicFixture();
env.registry.sources.find(
(source) => source.descriptor.id === 'esri-imagery',
).imagery = async () => {
throw new Error('unreachable');
};
await env.controller.setStack('esri-imagery');
assert.equal(env.controller.getActiveId(), 'osm');
assert.equal(
env.controller.getState().lastError,
'Esri Satellite is unavailable; using OSM',
);
assert.equal(env.imagery[0].provider, env.providers.get('osm'));
assert.equal(env.credits.size, 0);
env.controller.destroy();
});
test('one Esri tile failure stays put, two fall back, and stale errors cannot replace a selection', async () => {
const env = publicFixture();
await env.controller.setStack('esri-imagery');
assert.equal(env.credits.size, 1);
const errorEvent = env.providers.get('esri-imagery').errorEvent;
errorEvent.raise();
await settle();
assert.equal(env.controller.getActiveId(), 'esri-imagery');
errorEvent.raise();
await settle();
assert.equal(env.controller.getActiveId(), 'osm');
assert.equal(env.credits.size, 0);
assert.equal(
env.controller.getState().lastError,
'Esri Satellite tile requests failed; using OSM',
);
assert.equal(errorEvent.size, 0);
await env.controller.setStack('photoreal');
errorEvent.raise({ timesRetried: 9 });
await settle();
assert.equal(env.controller.getActiveId(), 'photoreal');
env.controller.destroy();
});
test('tooltips and rejected selection share the registry reason, including retired map IDs', async () => {
const errors = [],
env = publicFixture();
env.controller._onError = (message) => errors.push(message);
const reason = env.controller
.getStacks()
.find((stack) => stack.id === 'bing-aerial').unavailableReason;
await env.controller.setStack('bing-aerial');
assert.deepEqual(errors, [reason]);
assert.match(reason, /CESIUM_ION_TOKEN/);
await env.controller.setStack('bing-road');
assert.equal(env.controller.getActiveId(), 'photoreal');
env.controller.destroy();
});
test('a factory-owned 3D tileset is released after late completion and supplied tilesets stay caller-owned', async () => {
let resolve,
destroyed = 0;
const supplied = {
show: true,
destroy() {
assert.fail('caller-owned tileset');
},
};
const env = fixture({
defaultId: 'three',
sources: [
{
descriptor: { id: 'three' },
createTileset: () =>
new Promise((done) => {
resolve = done;
}),
},
{ descriptor: { id: 'supplied' }, tileset: supplied },
],
});
const loading = env.controller.setStack('three');
await settle();
env.controller.destroy();
resolve({
destroy() {
destroyed++;
},
});
await loading;
assert.equal(destroyed, 1);
assert.equal(env.primitives.length, 0);
});
test('invalid source graphs fail before constructing or caching a provider', () => {
const a = { descriptor: descriptor('a'), constructionFallback: { id: 'b' } };
const b = { descriptor: descriptor('b'), constructionFallback: { id: 'a' } };
assert.throws(() => fixture({ sources: [a, b] }), /fallback cycle/);
assert.throws(() => fixture({ sources: [a, a] }), /unique/);
assert.throws(() => fixture({ sources: [a] }), /Unknown map fallback/);
});
+25
View File
@@ -0,0 +1,25 @@
import * as Cesium from 'cesium';
/** Static credits follow the source actually on screen, including fallback. */
export function createMapCredits(viewer) {
let active = null;
let markup = null;
return {
show(html) {
if (html === markup) return;
const display = viewer?.scene?.frameState?.creditDisplay;
if (!display) return;
try {
if (active) display.removeStaticCredit(active);
active = html ? new Cesium.Credit(html, true) : null;
markup = html;
if (active) display.addStaticCredit(active);
} catch {
/* Switching remains usable on older credit displays. */
}
},
destroy() {
this.show(null);
},
};
}
+74
View File
@@ -0,0 +1,74 @@
import { MAP_STACKS } from './catalog.js';
import { photorealUnavailableReason } from './availability.js';
import { keySetupRequirement } from '../keySetupCore.mjs';
import {
createOsmImagery,
createEsriImagery,
createIonImagery,
ESRI_ATTRIBUTION_HTML,
} from './imagery.js';
import { createWorldTerrain, createKeylessTerrain } from './terrain.js';
/** Select sources and setup guidance without putting provider branches in the controller. */
export function createDefaultMapSources({
googleTileset = null,
cesiumToken = '',
googleApiKey = '',
} = {}) {
const ionToken = String(cesiumToken || '').trim();
const hasIon = Boolean(ionToken);
const hasGoogle = Boolean(String(googleApiKey || '').trim());
const terrain = {
id: hasIon ? 'world' : 'keyless',
create: hasIon
? (request) => createWorldTerrain(ionToken, request)
: createKeylessTerrain,
};
return {
defaultId: googleTileset ? 'photoreal' : 'esri-imagery',
unknownId: 'photoreal',
recoveryId: googleTileset ? 'photoreal' : null,
state: { hasCesiumIonToken: hasIon },
sources: MAP_STACKS.map((descriptor) => {
const common = {
descriptor,
available: !descriptor.requiresIon || hasIon,
unavailableReason: descriptor.requiresIon
? keySetupRequirement('cesium-ion')
: null,
};
if (descriptor.kind === 'photoreal')
return {
...common,
available: Boolean(googleTileset),
unavailableReason: photorealUnavailableReason(hasIon || hasGoogle),
tileset: googleTileset,
};
const imagery =
descriptor.kind === 'ion'
? () => createIonImagery(descriptor.style, ionToken)
: descriptor.id === 'osm'
? createOsmImagery
: createEsriImagery;
return {
...common,
imagery,
terrain,
...(descriptor.id === 'esri-imagery'
? {
credit: ESRI_ATTRIBUTION_HTML,
constructionFallback: {
id: 'osm',
message: 'Esri Satellite is unavailable; using OSM',
},
tileFailureFallback: {
id: 'osm',
threshold: 2,
message: 'Esri Satellite tile requests failed; using OSM',
},
}
: {}),
};
}),
};
}
+81
View File
@@ -0,0 +1,81 @@
const clean = (value) => String(value || '').trim();
/**
* Decide which map provider can deliver the best startup experience.
* @param {{googleApiKey?: string, cesiumToken?: string}} credentials
* @returns {'google-direct'|'google-ion'|'osm'}
*/
export function selectMapStartupRoute({
googleApiKey = '',
cesiumToken = '',
} = {}) {
if (clean(googleApiKey)) return 'google-direct';
if (clean(cesiumToken)) return 'google-ion';
return 'osm';
}
/**
* Load Google Photorealistic 3D Tiles through direct Google access when
* configured, otherwise through Cesium ion's hosted Google asset. If the
* direct request fails and an ion token is available, ion is the recovery path.
*
* @param {object} Cesium
* @param {{googleApiKey?: string, cesiumToken?: string}} credentials
* @returns {Promise<{tileset: object|null, route: 'google-direct'|'google-ion'|'osm', errors: Error[]}>}
*/
export async function loadPhotorealisticTileset(
Cesium,
{ googleApiKey = '', cesiumToken = '' } = {},
) {
const googleKey = clean(googleApiKey);
const ionToken = clean(cesiumToken);
const errors = [];
const attempts = [];
if (googleKey) attempts.push({ route: 'google-direct', googleKey });
if (ionToken) attempts.push({ route: 'google-ion', googleKey: undefined });
for (const attempt of attempts) {
try {
const tileset = attempt.googleKey
? await createGoogleDirectTileset(Cesium, attempt.googleKey)
: await createGoogleIonTileset(Cesium, ionToken);
return { tileset, route: attempt.route, errors };
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
}
}
return { tileset: null, route: 'osm', errors };
}
/** Pass credentials to the source instead of changing SDK-wide defaults. */
export function createGoogleDirectTileset(Cesium, key) {
key = clean(key);
if (!key) throw new Error('Google 3D requires an explicit browser key');
return Cesium.createGooglePhotorealistic3DTileset({
key,
onlyUsingWithGoogleGeocoder: true,
});
}
export async function createGoogleIonTileset(
Cesium,
accessToken,
{ signal } = {},
) {
accessToken = clean(accessToken);
if (!accessToken)
throw new Error('Google 3D through ion requires an explicit token');
signal?.throwIfAborted();
const resource = await Cesium.IonResource.fromAssetId(2275207, {
accessToken,
});
signal?.throwIfAborted();
// Match the installed SDK's Google helper rendering/cache defaults.
return Cesium.Cesium3DTileset.fromUrl(resource, {
cacheBytes: 1536 * 1024 * 1024,
maximumCacheOverflowBytes: 1024 * 1024 * 1024,
enableCollision: true,
});
}
+29
View File
@@ -0,0 +1,29 @@
import * as Cesium from 'cesium';
// Attribution and service rights are documented in DATA_SOURCES.md.
export const ESRI_ATTRIBUTION_HTML =
'<a href="https://www.esri.com" target="_blank" rel="noopener">Powered by Esri</a>';
export function createOsmImagery() {
return new Cesium.OpenStreetMapImageryProvider({
url: 'https://tile.openstreetmap.org/',
credit: '© OpenStreetMap contributors',
});
}
export function createEsriImagery() {
return Cesium.ArcGisMapServerImageryProvider.fromUrl(
'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer',
{
credit:
'Powered by Esri — Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community',
enablePickFeatures: false,
},
);
}
export function createIonImagery(style, accessToken) {
accessToken = String(accessToken || '').trim();
if (!accessToken) throw new Error('Ion imagery requires an explicit token');
return Cesium.IonImageryProvider.fromAssetId(style, { accessToken });
}
+21
View File
@@ -0,0 +1,21 @@
/** Catch invalid source graphs before a cached fallback can wait on itself. */
export function indexMapSources(sources) {
const index = new Map();
for (const source of sources) {
const id = source?.descriptor?.id;
if (typeof id !== 'string' || !id || index.has(id))
throw new TypeError('Map source IDs must be nonempty and unique');
index.set(id, source);
}
for (const id of index.keys()) {
const seen = new Set();
let next = id;
while (next) {
if (seen.has(next)) throw new TypeError('Map source fallback cycle');
if (!index.has(next)) throw new TypeError('Unknown map fallback source');
seen.add(next);
next = index.get(next).constructionFallback?.id;
}
}
return index;
}
+68
View File
@@ -0,0 +1,68 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { createIonImagery } from './imagery.js';
import { createWorldTerrain } from './terrain.js';
test('imagery and terrain pass their own ion token without relying on SDK defaults', async () => {
const originalImagery = Cesium.IonImageryProvider.fromAssetId;
const originalResource = Cesium.IonResource.fromAssetId;
const originalTerrain = Cesium.CesiumTerrainProvider.fromUrl;
const calls = [];
const defaultToken = Cesium.Ion.defaultAccessToken;
try {
Cesium.IonImageryProvider.fromAssetId = async (id, options) => {
calls.push({ kind: 'imagery', id, options });
return { id };
};
Cesium.IonResource.fromAssetId = async (id, options) => {
calls.push({ kind: 'resource', id, options });
return { id };
};
Cesium.CesiumTerrainProvider.fromUrl = async (resource, options) => {
calls.push({ kind: 'terrain', resource, options });
return { id: 'terrain' };
};
await createIonImagery(Cesium.IonWorldImageryStyle.AERIAL, 'imagery-token');
const result = await createWorldTerrain('terrain-token');
assert.equal(calls[0].options.accessToken, 'imagery-token');
assert.equal(calls[1].options.accessToken, 'terrain-token');
assert.equal(calls[1].id, 1);
assert.equal(calls[2].options.requestVertexNormals, true);
assert.equal(result.provider.id, 'terrain');
assert.equal(Cesium.Ion.defaultAccessToken, defaultToken);
} finally {
Cesium.IonImageryProvider.fromAssetId = originalImagery;
Cesium.IonResource.fromAssetId = originalResource;
Cesium.CesiumTerrainProvider.fromUrl = originalTerrain;
}
});
test('cancellation after ion metadata prevents terrain construction', async () => {
const originalResource = Cesium.IonResource.fromAssetId;
const originalTerrain = Cesium.CesiumTerrainProvider.fromUrl;
const controller = new AbortController();
try {
Cesium.IonResource.fromAssetId = async () => {
controller.abort();
return {};
};
Cesium.CesiumTerrainProvider.fromUrl = () =>
assert.fail('cancelled terrain construction');
await assert.rejects(
createWorldTerrain('test-token', { signal: controller.signal }),
{ name: 'AbortError' },
);
} finally {
Cesium.IonResource.fromAssetId = originalResource;
Cesium.CesiumTerrainProvider.fromUrl = originalTerrain;
}
});
test('credentialed source factories reject an omitted token instead of consuming an SDK default', async () => {
assert.throws(
() => createIonImagery(Cesium.IonWorldImageryStyle.AERIAL, ''),
/explicit token/,
);
await assert.rejects(createWorldTerrain(' '), /explicit ion token/);
});
+35
View File
@@ -0,0 +1,35 @@
import * as Cesium from 'cesium';
/** These factories are lazy: a hidden globe must not trigger terrain loading. */
export async function createWorldTerrain(accessToken, { signal } = {}) {
accessToken = String(accessToken || '').trim();
if (!accessToken)
throw new Error('World terrain requires an explicit ion token');
signal?.throwIfAborted();
const resource = await Cesium.IonResource.fromAssetId(1, { accessToken });
signal?.throwIfAborted();
return {
provider: await Cesium.CesiumTerrainProvider.fromUrl(resource, {
requestVertexNormals: true,
requestWaterMask: false,
ellipsoid: Cesium.Ellipsoid.WGS84,
}),
};
}
export async function createKeylessTerrain() {
try {
// Re:Earth / Mapterhorn ellipsoidal quantized mesh, CC BY 4.0.
return {
provider: await Cesium.CesiumTerrainProvider.fromUrl(
'https://terrain.reearth.land/cesium-mesh/ellipsoid',
),
};
} catch (error) {
console.warn(
'[MapStack] Re:Earth terrain unavailable, falling back to flat ellipsoid terrain:',
error,
);
return { provider: new Cesium.EllipsoidTerrainProvider() };
}
}