diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 171c65dd2..6712a4d4c 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -1252,6 +1252,7 @@ "src/layers/installations/lifecycle.js", "src/layers/installations/model.js", "src/layers/installations/policy.js", + "src/layers/installations/render/cesium.js", "src/layers/installations/rendering.js", "src/layers/installations/selection.js", "src/layers/installations/source.js", @@ -2130,6 +2131,7 @@ "src/layers/installations/lifecycle.js", "src/layers/installations/model.js", "src/layers/installations/policy.js", + "src/layers/installations/render/cesium.js", "src/layers/installations/rendering.js", "src/layers/installations/selection.js", "src/layers/installations/source.js", @@ -2968,6 +2970,7 @@ "src/layers/installations/lifecycle.js", "src/layers/installations/model.js", "src/layers/installations/policy.js", + "src/layers/installations/render/cesium.js", "src/layers/installations/rendering.js", "src/layers/installations/selection.js", "src/layers/installations/source.js", diff --git a/src/app/layers/militaryInstallations.js b/src/app/layers/militaryInstallations.js index 84359f74f..6c4376ab4 100644 --- a/src/app/layers/militaryInstallations.js +++ b/src/app/layers/militaryInstallations.js @@ -1,6 +1,8 @@ import { overlayHost } from './overlayHost.js'; import * as credits from '../../data/dataCredits.js'; import { createInstallationsLayer } from '../../layers/installations/index.js'; +import { createInstallationsCesiumRenderer } from '../../layers/installations/render/cesium.js'; +import { rendererFor } from './renderers.js'; import * as render from '../../renderGovernor.js'; import * as context from '../../data/contextStore.js'; import * as picking from '../../data/pickRegistry.js'; @@ -11,6 +13,10 @@ export function createApplicationInstallations({ surface, source }) { return createInstallationsLayer({ source, overlayHost, + rendererFactory: ({ engine, ...context }) => + engine?.kind === 'navara' + ? rendererFor('military-installations', engine, context) + : createInstallationsCesiumRenderer(context), services: { credits, render, diff --git a/src/layers/installations/controls.js b/src/layers/installations/controls.js index 130e8839b..e0e6f5867 100644 --- a/src/layers/installations/controls.js +++ b/src/layers/installations/controls.js @@ -144,21 +144,27 @@ export function createControls({ state: layerState, services, parts, source }) { */ focusById(id) { const record = layerState.recordById.get(String(id)); - if (!record || !layerState.engine?.camera?.flyToSphere) return false; + const camera = layerState.engine?.camera; + const flyToSphere = camera?.flyToSphere; + const flyToBoundingSphere = + layerState.viewer?.camera?.flyToBoundingSphere; + if (!record || (!flyToSphere && !flyToBoundingSphere)) return false; // No camera flight without a real selection: a flight plus a stale subject // reads as success to Context navigation and strands NEXT on this item. if (!parts.selection.selectRecord(record.id)) return false; - layerState.engine.camera.flyToSphere( - { - center: geo.cartesian3.fromDegrees( - record.longitude, - record.latitude, - parts.rendering.installationSurfaceHeightM(record), - ), - radius: 18000, - }, - { duration: 1.4 }, - ); + const sphere = { + center: geo.cartesian3.fromDegrees( + record.longitude, + record.latitude, + parts.rendering.installationSurfaceHeightM(record), + ), + radius: 18000, + }; + if (flyToSphere) flyToSphere.call(camera, sphere, { duration: 1.4 }); + else + flyToBoundingSphere.call(layerState.viewer.camera, sphere, { + duration: 1.4, + }); return true; }, diff --git a/src/layers/installations/index.js b/src/layers/installations/index.js index 87682bb16..65b9e922b 100644 --- a/src/layers/installations/index.js +++ b/src/layers/installations/index.js @@ -1,41 +1,73 @@ -import { createNamedMarkers } from './namedMarkers.js'; -import { createModel } from './model.js'; -import { createRendering } from './rendering.js'; import { createIngestion } from './ingestion.js'; import { createViewport } from './viewport.js'; -import { createSelection } from './selection.js'; import { createControls } from './controls.js'; -import { createLifecycle } from './lifecycle.js'; import { createState } from './state.js'; +import { createModel } from './model.js'; /** Construct one layer with its own scene state and supplied application services. */ -export function createInstallationsLayer({ services, source, overlayHost }) { +export function createInstallationsLayer({ + services, + source, + overlayHost, + rendererFactory, +}) { if ( typeof source?.getMappedSites !== 'function' || typeof source?.searchNearby !== 'function' ) throw new TypeError('An installation source is required'); + if (typeof rendererFactory !== 'function') + throw new TypeError('An installation renderer factory is required'); const state = createState({ services }); const parts = {}; const context = { state, services, parts, source, overlayHost }; parts.model = createModel(context); - parts.rendering = createRendering(context); - parts.namedMarkers = createNamedMarkers(context); parts.ingestion = createIngestion(context); parts.viewport = createViewport(context); - parts.selection = createSelection(context); parts.controls = createControls(context); - parts.lifecycle = createLifecycle(context); + let renderer = null; + const lifecycle = { + init(viewer, options = {}) { + if (renderer) + throw new Error('Installations layer is already initialized'); + const engine = options.engine ?? null; + renderer = rendererFactory({ ...context, viewer, engine }); + if (!renderer?.lifecycle?.methods) + throw new Error('Installation renderer is unavailable'); + Object.assign(parts, renderer); + return renderer.lifecycle.methods.init(viewer, options); + }, + enable(...args) { + return renderer?.lifecycle?.methods.enable(...args); + }, + disable(...args) { + return renderer?.lifecycle?.methods.disable(...args); + }, + destroy(...args) { + try { + return renderer?.lifecycle?.methods.destroy(...args); + } finally { + renderer = null; + for (const key of [ + 'rendering', + 'namedMarkers', + 'selection', + 'lifecycle', + ]) + delete parts[key]; + } + }, + }; return Object.assign( {}, parts.controls.methods, - parts.lifecycle.methods, + lifecycle, parts.ingestion?.methods, { approximateSurfaceDistanceM: parts.model.approximateSurfaceDistanceM, classifyGoogleMilitaryPlace: parts.model.classifyGoogleMilitaryPlace, installationSourceLabel: parts.model.installationSourceLabel, - installationSurfaceHeightM: parts.rendering.installationSurfaceHeightM, + installationSurfaceHeightM: parts.model.installationSurfaceHeightM, installationWithinViewport: parts.model.installationWithinViewport, installationResponseSaturated: parts.model.installationResponseSaturated, installationRetryDelayMs: parts.viewport.installationRetryDelayMs, diff --git a/src/layers/installations/lifecycle.js b/src/layers/installations/lifecycle.js index 928645f0e..4d575d91e 100644 --- a/src/layers/installations/lifecycle.js +++ b/src/layers/installations/lifecycle.js @@ -1,5 +1,6 @@ import * as Cesium from 'cesium'; import { LAYER_ID } from './policy.js'; +import { engineFromViewer } from '../../globe/cesium/engineFromViewer.js'; import { claimCameraSensitivity, releaseCameraSensitivity, @@ -16,6 +17,7 @@ export function createLifecycle({ const methods = { init(viewer, { engine = null } = {}) { + engine ||= engineFromViewer(viewer); layerState.viewer = viewer; layerState.engine = engine; layerState.dataSource = new Cesium.CustomDataSource( diff --git a/src/layers/installations/model.js b/src/layers/installations/model.js index e5aad2730..59f81bc23 100644 --- a/src/layers/installations/model.js +++ b/src/layers/installations/model.js @@ -43,6 +43,14 @@ export function createModel({ state: layerState, services, parts, source }) { return rgbaFromCss(COLOR_BY_CLASS[record.class] || '#9ca6b0'); } + function installationSurfaceHeightM(record) { + const floor = services.ground?.cachedGroundFloor?.( + record?.latitude, + record?.longitude, + ); + return services.ground?.floorAltitudeM?.(null, floor) ?? 0; + } + /** * Classify a Places text-search result without turning a name match into a * mapped military-land claim. Google currently has no documented military @@ -144,6 +152,7 @@ export function createModel({ state: layerState, services, parts, source }) { return { approximateSurfaceDistanceM, colorFor, + installationSurfaceHeightM, classifyGoogleMilitaryPlace, installationSourceLabel, installationWithinViewport, diff --git a/src/layers/installations/render/cesium.js b/src/layers/installations/render/cesium.js new file mode 100644 index 000000000..4ce68ca27 --- /dev/null +++ b/src/layers/installations/render/cesium.js @@ -0,0 +1,14 @@ +import { createLifecycle } from '../lifecycle.js'; +import { createNamedMarkers } from '../namedMarkers.js'; +import { createRendering } from '../rendering.js'; +import { createSelection } from '../selection.js'; + +/** Compose the existing Cesium-owned scene, input and entity adapters. */ +export function createInstallationsCesiumRenderer(context) { + return { + lifecycle: createLifecycle(context), + namedMarkers: createNamedMarkers(context), + rendering: createRendering(context), + selection: createSelection(context), + }; +}