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/, + ); +});