From 72fec292d4098c17cec7343bf468815705b5386d Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Thu, 15 Oct 2026 20:55:59 +0400 Subject: [PATCH] feat(maps): map sources for the Navara engine createDefaultMapSources takes the engine kind. On Navara, Google 3D is available from the browser key alone (Navara builds the tiles itself and has no ion route to them), so it is the default and the recovery stack when a key is set; without one the default is Esri imagery. Bing (Cesium ion imagery) has no Navara path and is unavailable with a reason. World terrain still needs an ion token and falls back to the keyless terrain. MapStackController passes the kind of the engine it is given; Cesium sources are unchanged. --- src/mapStackController.js | 8 +++++- src/mapStackController.test.mjs | 25 ++++++++++++++++ src/maps/defaultSources.js | 38 ++++++++++++++++++------ src/maps/sourceFactories.test.mjs | 48 +++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 10 deletions(-) diff --git a/src/mapStackController.js b/src/mapStackController.js index 35fbabddb..3af4de8da 100644 --- a/src/mapStackController.js +++ b/src/mapStackController.js @@ -9,7 +9,13 @@ export class MapStackController extends MapSourceController { constructor(viewer, options = {}) { const googleApiKey = typeof window !== 'undefined' ? window.__GOOGLE_MAPS_API_KEY__ : ''; - const registry = createDefaultMapSources({ ...options, googleApiKey }); + // Sources for the page's engine: Navara builds its own from the + // descriptions (src/maps/defaultSources.js). + const registry = createDefaultMapSources({ + ...options, + googleApiKey, + engineKind: options.engine?.kind === 'navara' ? 'navara' : 'cesium', + }); super(viewer, { registry, initialStack: options.googleTileset diff --git a/src/mapStackController.test.mjs b/src/mapStackController.test.mjs index 417577fd1..1578ec815 100644 --- a/src/mapStackController.test.mjs +++ b/src/mapStackController.test.mjs @@ -22,3 +22,28 @@ test('the map stack controller applies its stacks through a given globe engine', assert.equal(engine.fake.callsOf('maps.setPhotorealTiles')[1].args[0], null); controller.destroy(); }); + +test('on a Navara engine the map stack controller opens on Google 3D from the browser key alone', async (t) => { + const previous = globalThis.window; + globalThis.window = { __GOOGLE_MAPS_API_KEY__: 'google-test' }; + t.after(() => { + if (previous === undefined) delete globalThis.window; + else globalThis.window = previous; + }); + const engine = createFakeEngine({ kind: 'navara' }); + const controller = new MapStackController(null, { engine }); + assert.equal(controller.getActiveId(), 'photoreal'); + assert.equal(controller.isStackAvailable('bing-aerial'), false); + await controller.setStack('photoreal'); + const [photoreal] = engine.fake.callsOf('maps.setPhotorealTiles')[0].args; + assert.equal(photoreal.kind, 'photoreal'); + assert.equal('tileset' in photoreal, false); + controller.destroy(); + window.__GOOGLE_MAPS_API_KEY__ = ''; + const keyless = new MapStackController(null, { + engine: createFakeEngine({ kind: 'navara' }), + }); + assert.equal(keyless.getActiveId(), 'esri-imagery'); + assert.equal(keyless.isStackAvailable('photoreal'), false); + keyless.destroy(); +}); diff --git a/src/maps/defaultSources.js b/src/maps/defaultSources.js index fa6a263ba..bb55e106a 100644 --- a/src/maps/defaultSources.js +++ b/src/maps/defaultSources.js @@ -4,12 +4,26 @@ import { keySetupRequirement } from '../keySetupCore.mjs'; import { imageryFactoryFor, ESRI_ATTRIBUTION_HTML } from './imagery.js'; import { terrainSourceFor } from './terrain.js'; -/** Select sources and setup guidance without putting provider branches in the controller. */ +const NAVARA_ION_IMAGERY_REASON = + 'Bing imagery is not available on the experimental Navara engine'; + +/** + * Select sources and setup guidance without putting provider branches in the + * controller. `engineKind` is the globe engine kind the sources are for: Cesium + * builds them from the factories (photoreal needs its loaded tileset); Navara + * builds them from the descriptions, so photoreal needs only the browser key + * (Navara has no ion route to Google tiles) and Cesium ion imagery (Bing) is + * unavailable. + */ export function createDefaultMapSources({ googleTileset = null, cesiumToken = '', googleApiKey = '', + engineKind = 'cesium', } = {}) { + if (engineKind !== 'cesium' && engineKind !== 'navara') + throw new TypeError(`Unknown globe engine: ${engineKind}`); + const navara = engineKind === 'navara'; const ionToken = String(cesiumToken || '').trim(); const hasIon = Boolean(ionToken); // Engine-neutral descriptions ride beside the Cesium factories: a terrain @@ -22,24 +36,30 @@ export function createDefaultMapSources({ }; const googleKey = String(googleApiKey || '').trim(); const hasGoogle = Boolean(googleKey); + const photorealAvailable = navara ? hasGoogle : Boolean(googleTileset); return { - defaultId: googleTileset ? 'photoreal' : 'esri-imagery', + defaultId: photorealAvailable ? 'photoreal' : 'esri-imagery', unknownId: 'photoreal', - recoveryId: googleTileset ? 'photoreal' : null, + recoveryId: photorealAvailable ? 'photoreal' : null, state: { hasCesiumIonToken: hasIon }, sources: MAP_STACKS.map((descriptor) => { + const ionImageryOnNavara = navara && descriptor.kind === 'ion'; const common = { descriptor, - available: !descriptor.requiresIon || hasIon, - unavailableReason: descriptor.requiresIon - ? keySetupRequirement('cesium-ion') - : null, + available: !ionImageryOnNavara && (!descriptor.requiresIon || hasIon), + unavailableReason: ionImageryOnNavara + ? NAVARA_ION_IMAGERY_REASON + : descriptor.requiresIon + ? keySetupRequirement('cesium-ion') + : null, }; if (descriptor.kind === 'photoreal') return { ...common, - available: Boolean(googleTileset), - unavailableReason: photorealUnavailableReason(hasIon || hasGoogle), + available: photorealAvailable, + unavailableReason: photorealUnavailableReason( + navara ? hasGoogle : hasIon || hasGoogle, + ), tileset: googleTileset, ...(googleKey && { googleApiKey: googleKey }), ...(hasIon && { cesiumToken: ionToken }), diff --git a/src/maps/sourceFactories.test.mjs b/src/maps/sourceFactories.test.mjs index 6aaf1d4fe..60089b6a6 100644 --- a/src/maps/sourceFactories.test.mjs +++ b/src/maps/sourceFactories.test.mjs @@ -239,3 +239,51 @@ test('default sources carry engine-neutral descriptions beside their Cesium fact assert.equal(osm.terrain.kind, 'keyless'); assert.equal(osm.terrain.accessToken, undefined); }); + +test('on the Navara engine photoreal keys on the browser key and ion imagery is unavailable', () => { + const byId = (registry, id) => + registry.sources.find(({ descriptor }) => descriptor.id === id); + // Navara builds Google tiles from the browser key alone; no Cesium tileset. + const keyed = createDefaultMapSources({ + engineKind: 'navara', + googleApiKey: 'google-test', + }); + assert.equal(keyed.defaultId, 'photoreal'); + assert.equal(keyed.recoveryId, 'photoreal'); + assert.equal(byId(keyed, 'photoreal').available, true); + assert.equal(byId(keyed, 'photoreal').googleApiKey, 'google-test'); + assert.equal(byId(keyed, 'photoreal').tileset, null); + // Bing (Cesium ion imagery) has no Navara path, with or without a token. + for (const registry of [ + keyed, + createDefaultMapSources({ engineKind: 'navara', cesiumToken: 'ion-test' }), + ]) { + for (const id of ['bing-aerial', 'bing-labels']) { + assert.equal(byId(registry, id).available, false); + assert.match(byId(registry, id).unavailableReason, /Navara/); + } + assert.equal(byId(registry, 'esri-imagery').available, true); + assert.equal(byId(registry, 'osm').available, true); + } + // The ion route to Google tiles is Cesium's; a token alone is not enough. + const ionOnly = createDefaultMapSources({ + engineKind: 'navara', + cesiumToken: 'ion-test', + }); + assert.equal(byId(ionOnly, 'photoreal').available, false); + assert.equal(ionOnly.defaultId, 'esri-imagery'); + assert.equal(byId(ionOnly, 'osm').terrain.kind, 'world'); + // No key: base imagery over keyless terrain. + const keyless = createDefaultMapSources({ engineKind: 'navara' }); + assert.equal(keyless.defaultId, 'esri-imagery'); + assert.equal(keyless.recoveryId, null); + assert.equal(byId(keyless, 'photoreal').available, false); + assert.equal(byId(keyless, 'esri-imagery').terrain.kind, 'keyless'); +}); + +test('default sources reject an unknown engine', () => { + assert.throws( + () => createDefaultMapSources({ engineKind: 'unreal' }), + /Unknown globe engine/, + ); +});