mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(installations): add Navara renderer
This commit is contained in:
1 parent
965a82144c
commit
1a694baf5a
27 files changed
+1426
-47
No files matched your search
@@ -452,17 +452,17 @@
|
|||||||
"src/data/rocketLaunches.js",
|
"src/data/rocketLaunches.js",
|
||||||
"src/layers/satellites/source.test.mjs",
|
"src/layers/satellites/source.test.mjs",
|
||||||
"src/layers/launches/source.test.mjs",
|
"src/layers/launches/source.test.mjs",
|
||||||
"src/layers/installations/lifecycle.js",
|
"src/layers/installations/render/cesium/lifecycle.js",
|
||||||
"src/layers/installations/viewport.js",
|
"src/layers/installations/viewport.js",
|
||||||
"src/layers/installations/controls.js",
|
"src/layers/installations/controls.js",
|
||||||
"src/layers/installations/source.js",
|
"src/layers/installations/source.js",
|
||||||
"src/layers/installations/state.js",
|
"src/layers/installations/state.js",
|
||||||
"src/layers/installations/policy.js",
|
"src/layers/installations/policy.js",
|
||||||
"src/layers/installations/rendering.js",
|
"src/layers/installations/render/cesium/rendering.js",
|
||||||
"src/layers/installations/ingestion.js",
|
"src/layers/installations/ingestion.js",
|
||||||
"src/layers/installations/index.js",
|
"src/layers/installations/index.js",
|
||||||
"src/layers/installations/model.js",
|
"src/layers/installations/model.js",
|
||||||
"src/layers/installations/selection.js",
|
"src/layers/installations/render/cesium/selection.js",
|
||||||
"src/layers/awareness/focus.js",
|
"src/layers/awareness/focus.js",
|
||||||
"src/layers/awareness/lifecycle.js",
|
"src/layers/awareness/lifecycle.js",
|
||||||
"src/layers/awareness/dependencies.js",
|
"src/layers/awareness/dependencies.js",
|
||||||
|
|||||||
@@ -1249,12 +1249,12 @@
|
|||||||
"src/layers/installations/controls.js",
|
"src/layers/installations/controls.js",
|
||||||
"src/layers/installations/index.js",
|
"src/layers/installations/index.js",
|
||||||
"src/layers/installations/ingestion.js",
|
"src/layers/installations/ingestion.js",
|
||||||
"src/layers/installations/lifecycle.js",
|
"src/layers/installations/render/cesium/lifecycle.js",
|
||||||
"src/layers/installations/model.js",
|
"src/layers/installations/model.js",
|
||||||
"src/layers/installations/policy.js",
|
"src/layers/installations/policy.js",
|
||||||
"src/layers/installations/render/cesium.js",
|
"src/layers/installations/render/cesium.js",
|
||||||
"src/layers/installations/rendering.js",
|
"src/layers/installations/render/cesium/rendering.js",
|
||||||
"src/layers/installations/selection.js",
|
"src/layers/installations/render/cesium/selection.js",
|
||||||
"src/layers/installations/source.js",
|
"src/layers/installations/source.js",
|
||||||
"src/layers/installations/state.js",
|
"src/layers/installations/state.js",
|
||||||
"src/layers/installations/viewport.js",
|
"src/layers/installations/viewport.js",
|
||||||
@@ -1272,7 +1272,7 @@
|
|||||||
"src/data/local_data/osm_military_names/names.json",
|
"src/data/local_data/osm_military_names/names.json",
|
||||||
"src/data/bundledJson.js",
|
"src/data/bundledJson.js",
|
||||||
"src/data/retryableLoad.js",
|
"src/data/retryableLoad.js",
|
||||||
"src/layers/installations/namedMarkers.js",
|
"src/layers/installations/render/cesium/namedMarkers.js",
|
||||||
"src/data/infrastructureOverlayEntry.js",
|
"src/data/infrastructureOverlayEntry.js",
|
||||||
"src/geo/index.js",
|
"src/geo/index.js",
|
||||||
"src/geo/math.js",
|
"src/geo/math.js",
|
||||||
@@ -2128,12 +2128,12 @@
|
|||||||
"src/layers/installations/controls.js",
|
"src/layers/installations/controls.js",
|
||||||
"src/layers/installations/index.js",
|
"src/layers/installations/index.js",
|
||||||
"src/layers/installations/ingestion.js",
|
"src/layers/installations/ingestion.js",
|
||||||
"src/layers/installations/lifecycle.js",
|
"src/layers/installations/render/cesium/lifecycle.js",
|
||||||
"src/layers/installations/model.js",
|
"src/layers/installations/model.js",
|
||||||
"src/layers/installations/policy.js",
|
"src/layers/installations/policy.js",
|
||||||
"src/layers/installations/render/cesium.js",
|
"src/layers/installations/render/cesium.js",
|
||||||
"src/layers/installations/rendering.js",
|
"src/layers/installations/render/cesium/rendering.js",
|
||||||
"src/layers/installations/selection.js",
|
"src/layers/installations/render/cesium/selection.js",
|
||||||
"src/layers/installations/source.js",
|
"src/layers/installations/source.js",
|
||||||
"src/layers/installations/state.js",
|
"src/layers/installations/state.js",
|
||||||
"src/layers/installations/viewport.js",
|
"src/layers/installations/viewport.js",
|
||||||
@@ -2625,7 +2625,7 @@
|
|||||||
"src/data/militaryNames.js",
|
"src/data/militaryNames.js",
|
||||||
"src/sources/requestDeadline.js",
|
"src/sources/requestDeadline.js",
|
||||||
"src/data/local_data/osm_military_names/names.json",
|
"src/data/local_data/osm_military_names/names.json",
|
||||||
"src/layers/installations/namedMarkers.js",
|
"src/layers/installations/render/cesium/namedMarkers.js",
|
||||||
"src/data/infrastructureOverlayEntry.js",
|
"src/data/infrastructureOverlayEntry.js",
|
||||||
"src/sources/infrastructureData.js",
|
"src/sources/infrastructureData.js",
|
||||||
"src/tools/functions.js",
|
"src/tools/functions.js",
|
||||||
@@ -2700,6 +2700,8 @@
|
|||||||
"src/layers/earthquakes/render/navara.js",
|
"src/layers/earthquakes/render/navara.js",
|
||||||
"src/layers/earthquakes/render/navaraGeometry.js",
|
"src/layers/earthquakes/render/navaraGeometry.js",
|
||||||
"src/layers/earthquakes/render/navaraGrid.js",
|
"src/layers/earthquakes/render/navaraGrid.js",
|
||||||
|
"src/layers/installations/render/navara.js",
|
||||||
|
"src/layers/installations/render/navaraGeometry.js",
|
||||||
"src/layers/firms/rectangle.js",
|
"src/layers/firms/rectangle.js",
|
||||||
"src/layers/firms/render/cesium.js",
|
"src/layers/firms/render/cesium.js",
|
||||||
"src/layers/firms/render/navara.js",
|
"src/layers/firms/render/navara.js",
|
||||||
@@ -2967,12 +2969,12 @@
|
|||||||
"src/layers/installations/controls.js",
|
"src/layers/installations/controls.js",
|
||||||
"src/layers/installations/index.js",
|
"src/layers/installations/index.js",
|
||||||
"src/layers/installations/ingestion.js",
|
"src/layers/installations/ingestion.js",
|
||||||
"src/layers/installations/lifecycle.js",
|
"src/layers/installations/render/cesium/lifecycle.js",
|
||||||
"src/layers/installations/model.js",
|
"src/layers/installations/model.js",
|
||||||
"src/layers/installations/policy.js",
|
"src/layers/installations/policy.js",
|
||||||
"src/layers/installations/render/cesium.js",
|
"src/layers/installations/render/cesium.js",
|
||||||
"src/layers/installations/rendering.js",
|
"src/layers/installations/render/cesium/rendering.js",
|
||||||
"src/layers/installations/selection.js",
|
"src/layers/installations/render/cesium/selection.js",
|
||||||
"src/layers/installations/source.js",
|
"src/layers/installations/source.js",
|
||||||
"src/layers/installations/state.js",
|
"src/layers/installations/state.js",
|
||||||
"src/layers/installations/viewport.js",
|
"src/layers/installations/viewport.js",
|
||||||
@@ -3265,7 +3267,7 @@
|
|||||||
"src/data/militaryNames.js",
|
"src/data/militaryNames.js",
|
||||||
"src/sources/requestDeadline.js",
|
"src/sources/requestDeadline.js",
|
||||||
"src/data/local_data/osm_military_names/names.json",
|
"src/data/local_data/osm_military_names/names.json",
|
||||||
"src/layers/installations/namedMarkers.js",
|
"src/layers/installations/render/cesium/namedMarkers.js",
|
||||||
"src/data/infrastructureOverlayEntry.js",
|
"src/data/infrastructureOverlayEntry.js",
|
||||||
"src/sources/infrastructureData.js",
|
"src/sources/infrastructureData.js",
|
||||||
"src/geo/index.js",
|
"src/geo/index.js",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
rendererFor,
|
rendererFor,
|
||||||
} from './layers/renderers.js';
|
} from './layers/renderers.js';
|
||||||
|
|
||||||
test('the Navara mode registers the aircraft layers’ and vessels’ renderers, once', async () => {
|
test('the Navara mode registers migrated renderers once', async () => {
|
||||||
assert.equal(layerAvailableOn('military', 'navara'), false);
|
assert.equal(layerAvailableOn('military', 'navara'), false);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
registerNavaraLayerRenderers(),
|
registerNavaraLayerRenderers(),
|
||||||
@@ -26,6 +26,7 @@ test('the Navara mode registers the aircraft layers’ and vessels’ renderers,
|
|||||||
'ais-live-vessels',
|
'ais-live-vessels',
|
||||||
'earthquakes',
|
'earthquakes',
|
||||||
'local-firms',
|
'local-firms',
|
||||||
|
'military-installations',
|
||||||
])
|
])
|
||||||
assert.equal(layerAvailableOn(id, 'navara'), true, id);
|
assert.equal(layerAvailableOn(id, 'navara'), true, id);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
@@ -40,6 +41,7 @@ test('the Navara mode registers the aircraft layers’ and vessels’ renderers,
|
|||||||
'satellites',
|
'satellites',
|
||||||
'rocket-launches',
|
'rocket-launches',
|
||||||
'ais-live-vessels',
|
'ais-live-vessels',
|
||||||
|
'military-installations',
|
||||||
'local-firms',
|
'local-firms',
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,6 +62,15 @@ const NAVARA_CREDIT_KEYS = new Set([
|
|||||||
*/
|
*/
|
||||||
export async function registerNavaraLayerRenderers() {
|
export async function registerNavaraLayerRenderers() {
|
||||||
const loaders = {
|
const loaders = {
|
||||||
|
'military-installations': async () => {
|
||||||
|
const { createInstallationsNavaraRenderer } =
|
||||||
|
await import('../layers/installations/render/navara.js');
|
||||||
|
return (engine, options) =>
|
||||||
|
createInstallationsNavaraRenderer(engine, {
|
||||||
|
...options,
|
||||||
|
...navaraViews.get(engine),
|
||||||
|
});
|
||||||
|
},
|
||||||
'local-firms': async () => {
|
'local-firms': async () => {
|
||||||
const { createFirmsNavaraRenderer } =
|
const { createFirmsNavaraRenderer } =
|
||||||
await import('../layers/firms/render/navara.js');
|
await import('../layers/firms/render/navara.js');
|
||||||
@@ -151,6 +160,8 @@ export async function createNavaraScene({
|
|||||||
MeshDesc: globe.MeshDesc,
|
MeshDesc: globe.MeshDesc,
|
||||||
EffectDesc: globe.EffectDesc,
|
EffectDesc: globe.EffectDesc,
|
||||||
NavaraEffect: globe.NavaraEffect,
|
NavaraEffect: globe.NavaraEffect,
|
||||||
|
DrapedMesh: globe.DrapedMesh,
|
||||||
|
setupMaterialForDrape: globe.setupMaterialForDrape,
|
||||||
});
|
});
|
||||||
defer(() => navaraViews.delete(engine));
|
defer(() => navaraViews.delete(engine));
|
||||||
// The layers drawn on Navara (their render/navara.js), before the catalog
|
// The layers drawn on Navara (their render/navara.js), before the catalog
|
||||||
|
|||||||
@@ -34,8 +34,13 @@ export const OPENMAPTILES_CREDIT = {
|
|||||||
key: 'openfreemap',
|
key: 'openfreemap',
|
||||||
html: 'Vector tiles: <a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a> © <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a>',
|
html: 'Vector tiles: <a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a> © <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a>',
|
||||||
};
|
};
|
||||||
|
export const GOOGLE_PLACES_CREDIT = {
|
||||||
|
key: 'google-maps-places',
|
||||||
|
html: 'Google Maps Places: <a href="https://developers.google.com/maps/documentation/places/web-service/policies" target="_blank" rel="noopener">Google Maps</a>',
|
||||||
|
};
|
||||||
|
|
||||||
const osmDisplays = new WeakMap();
|
const osmDisplays = new WeakMap();
|
||||||
|
const googlePlacesDisplays = new WeakMap();
|
||||||
const viewerCredits = new WeakMap();
|
const viewerCredits = new WeakMap();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,6 +119,36 @@ export function hideOsmCredit(target, owner) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Keep Places attribution visible only while a layer displays Places results. */
|
||||||
|
export function showGooglePlacesCredit(target, owner) {
|
||||||
|
const credits = creditsOf(target);
|
||||||
|
if (!credits || !owner) return false;
|
||||||
|
let state = googlePlacesDisplays.get(target);
|
||||||
|
if (!state) {
|
||||||
|
state = { owners: new Set(), remove: null };
|
||||||
|
googlePlacesDisplays.set(target, state);
|
||||||
|
}
|
||||||
|
if (state.owners.has(owner)) return false;
|
||||||
|
state.owners.add(owner);
|
||||||
|
state.remove ||= credits.addStatic(GOOGLE_PLACES_CREDIT.html, {
|
||||||
|
onScreen: true,
|
||||||
|
});
|
||||||
|
credits.request();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Release this layer's Places attribution when it stops displaying results. */
|
||||||
|
export function hideGooglePlacesCredit(target, owner) {
|
||||||
|
const state = googlePlacesDisplays.get(target);
|
||||||
|
if (!state?.owners.delete(owner)) return false;
|
||||||
|
if (!state.owners.size) {
|
||||||
|
state.remove?.();
|
||||||
|
state.remove = null;
|
||||||
|
}
|
||||||
|
creditsOf(target)?.request();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export const DATA_CREDITS = [
|
export const DATA_CREDITS = [
|
||||||
// ── Live sources ────────────────────────────────────────────────
|
// ── Live sources ────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -160,16 +160,49 @@ test('data credits go through the globe engine credits', async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Google Places attribution is visible only while a layer displays Places data', async () => {
|
||||||
|
const { showGooglePlacesCredit, hideGooglePlacesCredit } =
|
||||||
|
await import('./dataCredits.js');
|
||||||
|
const { createFakeEngine } = await import('../globe/testing/fakeEngine.js');
|
||||||
|
const engine = createFakeEngine();
|
||||||
|
assert.equal(showGooglePlacesCredit(engine, 'installations'), true);
|
||||||
|
assert.equal(showGooglePlacesCredit(engine, 'installations'), false);
|
||||||
|
assert.equal(showGooglePlacesCredit(engine, 'other-layer'), true);
|
||||||
|
const visible = engine.fake.credits.filter(({ onScreen }) => onScreen);
|
||||||
|
assert.equal(visible.length, 1);
|
||||||
|
assert.match(visible[0].html, /Google Maps Places/);
|
||||||
|
assert.match(
|
||||||
|
visible[0].html,
|
||||||
|
/developers\.google\.com\/maps\/documentation\/places/,
|
||||||
|
);
|
||||||
|
assert.ok(engine.fake.callsOf('render.request').length > 0);
|
||||||
|
assert.equal(hideGooglePlacesCredit(engine, 'installations'), true);
|
||||||
|
assert.equal(
|
||||||
|
engine.fake.credits.filter(({ onScreen }) => onScreen).length,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
assert.equal(hideGooglePlacesCredit(engine, 'other-layer'), true);
|
||||||
|
assert.equal(hideGooglePlacesCredit(engine, 'installations'), false);
|
||||||
|
assert.equal(
|
||||||
|
engine.fake.credits.filter(({ onScreen }) => onScreen).length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('without a viewer or engine the OSM and dynamic credits skip cleanly', async () => {
|
test('without a viewer or engine the OSM and dynamic credits skip cleanly', async () => {
|
||||||
const {
|
const {
|
||||||
registerDataCredits,
|
registerDataCredits,
|
||||||
registerDynamicCredit,
|
registerDynamicCredit,
|
||||||
showOsmCredit,
|
showOsmCredit,
|
||||||
hideOsmCredit,
|
hideOsmCredit,
|
||||||
|
showGooglePlacesCredit,
|
||||||
|
hideGooglePlacesCredit,
|
||||||
} = await import('./dataCredits.js');
|
} = await import('./dataCredits.js');
|
||||||
// The Navara mode has no viewer; a layer-side call with it must not throw.
|
// The Navara mode has no viewer; a layer-side call with it must not throw.
|
||||||
assert.doesNotThrow(() => registerDataCredits(null, [{ html: 'A' }]));
|
assert.doesNotThrow(() => registerDataCredits(null, [{ html: 'A' }]));
|
||||||
assert.equal(showOsmCredit(null, 'owner'), false);
|
assert.equal(showOsmCredit(null, 'owner'), false);
|
||||||
assert.equal(hideOsmCredit(null, 'owner'), false);
|
assert.equal(hideOsmCredit(null, 'owner'), false);
|
||||||
|
assert.equal(showGooglePlacesCredit(null, 'owner'), false);
|
||||||
|
assert.equal(hideGooglePlacesCredit(null, 'owner'), false);
|
||||||
assert.equal(registerDynamicCredit(null, { key: 'k', html: 'K' }), false);
|
assert.equal(registerDynamicCredit(null, { key: 'k', html: 'K' }), false);
|
||||||
});
|
});
|
||||||
@@ -121,7 +121,7 @@ test('every scene click handler consults ownership before it picks', () => {
|
|||||||
['src/layers/bikeshare/selection.js', 'viewer.scene.pick(click.position)'],
|
['src/layers/bikeshare/selection.js', 'viewer.scene.pick(click.position)'],
|
||||||
['src/layers/firms/selection.js', 'scene.pick(click.position)'],
|
['src/layers/firms/selection.js', 'scene.pick(click.position)'],
|
||||||
[
|
[
|
||||||
'src/layers/installations/selection.js',
|
'src/layers/installations/render/cesium/selection.js',
|
||||||
'viewer.scene.pick(click.position)',
|
'viewer.scene.pick(click.position)',
|
||||||
],
|
],
|
||||||
['src/layers/launches/render/cesium.js', 'drillPick(movement.position'],
|
['src/layers/launches/render/cesium.js', 'drillPick(movement.position'],
|
||||||
|
|||||||
@@ -276,13 +276,18 @@ test('tracking layers write gevLabelModel and expose only their cached display p
|
|||||||
'flights.js',
|
'flights.js',
|
||||||
'militaryFlights.js',
|
'militaryFlights.js',
|
||||||
'satellites.js',
|
'satellites.js',
|
||||||
'militaryInstallations.js',
|
|
||||||
].map(async (name) => [name, readLayerSource(new URL(`./${name}`, import.meta.url))]));
|
].map(async (name) => [name, readLayerSource(new URL(`./${name}`, import.meta.url))]));
|
||||||
const sources = Object.fromEntries(files);
|
const sources = Object.fromEntries(files);
|
||||||
for (const [name, source] of files) {
|
for (const [name, source] of files) {
|
||||||
assert.ok(source.includes('.gevLabelModel ='), `${name} writes the explicit model directly`);
|
assert.ok(source.includes('.gevLabelModel ='), `${name} writes the explicit model directly`);
|
||||||
assert.ok(source.includes('.gevDisplayPosition ='), `${name} exposes a display-position cache`);
|
assert.ok(source.includes('.gevDisplayPosition ='), `${name} exposes a display-position cache`);
|
||||||
}
|
}
|
||||||
|
const installations = await readFile(
|
||||||
|
new URL('../layers/installations/render/cesium/rendering.js', import.meta.url),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
assert.ok(installations.includes('entity.gevLabelModel ='));
|
||||||
|
assert.ok(installations.includes('entity.gevDisplayPosition = () => displayPosition'));
|
||||||
assert.match(sources['flights.js'], /gevDisplayPosition\s*=\s*parts\.motion\._trackedDisplayCached/);
|
assert.match(sources['flights.js'], /gevDisplayPosition\s*=\s*parts\.motion\._trackedDisplayCached/);
|
||||||
assert.match(sources['militaryFlights.js'], /gevDisplayPosition\s*=\s*parts\.motion\._trackedDisplayCached/);
|
assert.match(sources['militaryFlights.js'], /gevDisplayPosition\s*=\s*parts\.motion\._trackedDisplayCached/);
|
||||||
assert.ok(sources['satellites.js'].includes('gevDisplayPosition = _trackedDisplayCached'));
|
assert.ok(sources['satellites.js'].includes('gevDisplayPosition = _trackedDisplayCached'));
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import ThreeView, {
|
|||||||
MeshDesc,
|
MeshDesc,
|
||||||
EffectDesc,
|
EffectDesc,
|
||||||
Effect as NavaraEffect,
|
Effect as NavaraEffect,
|
||||||
|
DrapedMesh,
|
||||||
|
setupMaterialForDrape,
|
||||||
} from '@navaramap/three';
|
} from '@navaramap/three';
|
||||||
import * as geo from '../../geo/index.js';
|
import * as geo from '../../geo/index.js';
|
||||||
import { DefaultPlugin } from '@navaramap/three-default-plugin';
|
import { DefaultPlugin } from '@navaramap/three-default-plugin';
|
||||||
@@ -193,6 +195,8 @@ export async function createNavaraGlobe({
|
|||||||
MeshDesc,
|
MeshDesc,
|
||||||
EffectDesc,
|
EffectDesc,
|
||||||
NavaraEffect,
|
NavaraEffect,
|
||||||
|
DrapedMesh,
|
||||||
|
setupMaterialForDrape,
|
||||||
destroy() {
|
destroy() {
|
||||||
if (destroyed) return;
|
if (destroyed) return;
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ export function createIngestion({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadInstallations() {
|
async function loadInstallations() {
|
||||||
if (!layerState.enabled || !layerState.viewer) return;
|
if (!layerState.enabled || (!layerState.viewer && !layerState.engine))
|
||||||
|
return;
|
||||||
const { box, coverage } = parts.viewport.loadArea(layerState.viewer);
|
const { box, coverage } = parts.viewport.loadArea(layerState.viewer);
|
||||||
// Guidance, not a fault: the layer chose not to query because the view is
|
// Guidance, not a fault: the layer chose not to query because the view is
|
||||||
// unbounded (a global view, or Cockpit looking at the horizon). Keep it out
|
// unbounded (a global view, or Cockpit looking at the horizon). Keep it out
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import * as credits from '../../data/dataCredits.js';
|
import * as credits from '../../data/dataCredits.js';
|
||||||
import { createLifecycle } from './lifecycle.js';
|
import { createLifecycle } from './render/cesium/lifecycle.js';
|
||||||
import { LAYER_ID } from './policy.js';
|
import { LAYER_ID } from './policy.js';
|
||||||
|
|
||||||
for (const source of [
|
for (const source of [
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import * as C from 'cesium';
|
import * as C from 'cesium';
|
||||||
import { createNamedMarkers } from './namedMarkers.js';
|
import { createNamedMarkers } from './render/cesium/namedMarkers.js';
|
||||||
import { createLifecycle } from './lifecycle.js';
|
import { createLifecycle } from './render/cesium/lifecycle.js';
|
||||||
import { createLocalInfrastructureOverlayEntry } from '../../data/localGeojsonCore.js';
|
import { createLocalInfrastructureOverlayEntry } from '../../data/localGeojsonCore.js';
|
||||||
|
|
||||||
function setup(t) {
|
function setup(t) {
|
||||||
@@ -183,3 +183,17 @@ test('Contacts can select a point outside the normal title cap or an unnamed pol
|
|||||||
}
|
}
|
||||||
h.lifecycle.destroy(h.state.viewer);
|
h.lifecycle.destroy(h.state.viewer);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('selected markers without an explicit namedArea flag stay hidden safely', (t) => {
|
||||||
|
const h = setup(t);
|
||||||
|
const record = {
|
||||||
|
id: 'google:places-qa-01',
|
||||||
|
name: 'Candidate',
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
};
|
||||||
|
h.state.selectedId = record.id;
|
||||||
|
assert.doesNotThrow(() => h.markers.sync([record]));
|
||||||
|
h.markers.visit((_id, point) => assert.equal(point.show, false));
|
||||||
|
h.lifecycle.destroy(h.state.viewer);
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@ import test from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { createViewport } from './viewport.js';
|
import { createViewport } from './viewport.js';
|
||||||
import { createLifecycle } from './lifecycle.js';
|
import { createLifecycle } from './render/cesium/lifecycle.js';
|
||||||
|
|
||||||
test('camera changes debounce, deduplicate moveEnd, cancel superseded loads and recover from wide views', (t) => {
|
test('camera changes debounce, deduplicate moveEnd, cancel superseded loads and recover from wide views', (t) => {
|
||||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||||
@@ -158,6 +158,49 @@ test('an aborting real ingestion cannot clear the next scheduled view key', asyn
|
|||||||
assert.equal(state.cameraLoadKey, nextKey);
|
assert.equal(state.cameraLoadKey, nextKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Navara installation ingestion loads from its engine camera without a viewer', async () => {
|
||||||
|
const { createIngestion } = await import('./ingestion.js');
|
||||||
|
const rectangle = Cesium.Rectangle.fromDegrees(-123, 37, -122, 38);
|
||||||
|
const state = {
|
||||||
|
enabled: true,
|
||||||
|
viewer: null,
|
||||||
|
engine: { camera: { viewRectangle: () => rectangle } },
|
||||||
|
records: [],
|
||||||
|
recordById: new Map(),
|
||||||
|
coverage: { kind: 'viewport' },
|
||||||
|
};
|
||||||
|
const requests = [];
|
||||||
|
const parts = {
|
||||||
|
model: { installationWithinViewport: () => true },
|
||||||
|
rendering: { renderRecords() {}, warmInstallationFloors() {} },
|
||||||
|
namedMarkers: { sync() {} },
|
||||||
|
};
|
||||||
|
const context = {
|
||||||
|
state,
|
||||||
|
parts,
|
||||||
|
services: {
|
||||||
|
render: { governorRequestRender() {} },
|
||||||
|
ground: { resolveGroundFloorCellsBounded: async () => {} },
|
||||||
|
},
|
||||||
|
source: {
|
||||||
|
async getMappedSites(box) {
|
||||||
|
requests.push(box);
|
||||||
|
return { records: [], status: 'ready', wide: false, saturated: false };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
parts.viewport = createViewport(context);
|
||||||
|
const ingestion = createIngestion(context);
|
||||||
|
|
||||||
|
await ingestion.loadInstallations();
|
||||||
|
|
||||||
|
assert.equal(requests.length, 1);
|
||||||
|
assert.ok(Math.abs(requests[0].west + 123) < 1e-10);
|
||||||
|
assert.ok(Math.abs(requests[0].south - 37) < 1e-10);
|
||||||
|
assert.equal(state.loading, false);
|
||||||
|
assert.ok(state.lastUpdate);
|
||||||
|
});
|
||||||
|
|
||||||
test('wide views refresh screen decluttering when the globe rectangle stays unchanged', (t) => {
|
test('wide views refresh screen decluttering when the globe rectangle stays unchanged', (t) => {
|
||||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||||
const camera = {
|
const camera = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLifecycle } from '../lifecycle.js';
|
import { createLifecycle } from './cesium/lifecycle.js';
|
||||||
import { createNamedMarkers } from '../namedMarkers.js';
|
import { createNamedMarkers } from './cesium/namedMarkers.js';
|
||||||
import { createRendering } from '../rendering.js';
|
import { createRendering } from './cesium/rendering.js';
|
||||||
import { createSelection } from '../selection.js';
|
import { createSelection } from './cesium/selection.js';
|
||||||
|
|
||||||
/** Compose the existing Cesium-owned scene, input and entity adapters. */
|
/** Compose the existing Cesium-owned scene, input and entity adapters. */
|
||||||
export function createInstallationsCesiumRenderer(context) {
|
export function createInstallationsCesiumRenderer(context) {
|
||||||
|
|||||||
+6
-4
@@ -1,10 +1,10 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { LAYER_ID } from './policy.js';
|
import { LAYER_ID } from '../../policy.js';
|
||||||
import { engineFromViewer } from '../../globe/cesium/engineFromViewer.js';
|
import { engineFromViewer } from '../../../../globe/cesium/engineFromViewer.js';
|
||||||
import {
|
import {
|
||||||
claimCameraSensitivity,
|
claimCameraSensitivity,
|
||||||
releaseCameraSensitivity,
|
releaseCameraSensitivity,
|
||||||
} from '../../data/cameraSensitivity.js';
|
} from '../../../../data/cameraSensitivity.js';
|
||||||
|
|
||||||
export function createLifecycle({
|
export function createLifecycle({
|
||||||
state: layerState,
|
state: layerState,
|
||||||
@@ -23,7 +23,7 @@ export function createLifecycle({
|
|||||||
layerState.dataSource = new Cesium.CustomDataSource(
|
layerState.dataSource = new Cesium.CustomDataSource(
|
||||||
'military-installations',
|
'military-installations',
|
||||||
);
|
);
|
||||||
viewer.dataSources.add(layerState.dataSource);
|
const added = viewer.dataSources.add(layerState.dataSource);
|
||||||
const camera = engine?.camera ?? viewer.camera;
|
const camera = engine?.camera ?? viewer.camera;
|
||||||
const cameraChanged = () => {
|
const cameraChanged = () => {
|
||||||
if (!layerState.contextAnchor) parts.viewport.scheduleLoad();
|
if (!layerState.contextAnchor) parts.viewport.scheduleLoad();
|
||||||
@@ -32,6 +32,7 @@ export function createLifecycle({
|
|||||||
layerState.changedRemove =
|
layerState.changedRemove =
|
||||||
camera.changed?.addEventListener(cameraChanged);
|
camera.changed?.addEventListener(cameraChanged);
|
||||||
parts.selection.installInteraction(viewer);
|
parts.selection.installInteraction(viewer);
|
||||||
|
return added;
|
||||||
},
|
},
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
@@ -64,6 +65,7 @@ export function createLifecycle({
|
|||||||
disable() {
|
disable() {
|
||||||
layerState.enabled = false;
|
layerState.enabled = false;
|
||||||
services.credits?.hideOsmCredit?.(layerState.viewer, LAYER_ID);
|
services.credits?.hideOsmCredit?.(layerState.viewer, LAYER_ID);
|
||||||
|
services.credits?.hideGooglePlacesCredit?.(layerState.viewer, LAYER_ID);
|
||||||
parts.namedMarkers?.hide();
|
parts.namedMarkers?.hide();
|
||||||
releaseCameraSensitivity(layerState.viewer?.camera, LAYER_ID);
|
releaseCameraSensitivity(layerState.viewer?.camera, LAYER_ID);
|
||||||
layerState.contextAnchor = null;
|
layerState.contextAnchor = null;
|
||||||
+6
-4
@@ -2,9 +2,9 @@ import * as Cesium from 'cesium';
|
|||||||
import {
|
import {
|
||||||
MILITARY_LABEL_CAP,
|
MILITARY_LABEL_CAP,
|
||||||
MILITARY_POINT_CAP,
|
MILITARY_POINT_CAP,
|
||||||
} from '../../data/militaryNames.js';
|
} from '../../../../data/militaryNames.js';
|
||||||
import { LAYER_ID, COLOR_BY_CLASS } from './policy.js';
|
import { LAYER_ID, COLOR_BY_CLASS } from '../../policy.js';
|
||||||
import { createInfrastructureOverlayEntry } from '../../data/infrastructureOverlayEntry.js';
|
import { createInfrastructureOverlayEntry } from '../../../../data/infrastructureOverlayEntry.js';
|
||||||
|
|
||||||
/** Bounded point markers; the shared overlay owns text projection and decluttering. */
|
/** Bounded point markers; the shared overlay owns text projection and decluttering. */
|
||||||
export function createNamedMarkers({ state, services, parts, overlayHost }) {
|
export function createNamedMarkers({ state, services, parts, overlayHost }) {
|
||||||
@@ -34,7 +34,9 @@ export function createNamedMarkers({ state, services, parts, overlayHost }) {
|
|||||||
for (let index = 0; index < ordered.length; index++) {
|
for (let index = 0; index < ordered.length; index++) {
|
||||||
const marker = ordered[index];
|
const marker = ordered[index];
|
||||||
const point = marker.point;
|
const point = marker.point;
|
||||||
point.show = marker.named && occluder.isPointVisible(point.position);
|
point.show = Boolean(
|
||||||
|
marker.named && occluder.isPointVisible(point.position),
|
||||||
|
);
|
||||||
if (!point.show) continue;
|
if (!point.show) continue;
|
||||||
const projected = Cesium.SceneTransforms.worldToWindowCoordinates(
|
const projected = Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||||
viewer.scene,
|
viewer.scene,
|
||||||
+13
-2
@@ -1,5 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { LAYER_ID, MAX_RENDERED, COLOR_BY_CLASS } from './policy.js';
|
import { LAYER_ID, MAX_RENDERED, COLOR_BY_CLASS } from '../../policy.js';
|
||||||
|
|
||||||
export function createRendering({
|
export function createRendering({
|
||||||
state: layerState,
|
state: layerState,
|
||||||
@@ -154,6 +154,16 @@ export function createRendering({
|
|||||||
return selected ? [...rendered, selected] : rendered;
|
return selected ? [...rendered, selected] : rendered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncPlacesCredit(records) {
|
||||||
|
const usesPlaces = records.some((record) =>
|
||||||
|
record.sources?.some((source) => source.name === 'Google Maps Places'),
|
||||||
|
);
|
||||||
|
if (usesPlaces)
|
||||||
|
services.credits?.showGooglePlacesCredit?.(layerState.viewer, LAYER_ID);
|
||||||
|
else
|
||||||
|
services.credits?.hideGooglePlacesCredit?.(layerState.viewer, LAYER_ID);
|
||||||
|
}
|
||||||
|
|
||||||
function renderRecords({ claimSelection = false } = {}) {
|
function renderRecords({ claimSelection = false } = {}) {
|
||||||
// Context navigation can select another layer without a canvas click.
|
// Context navigation can select another layer without a canvas click.
|
||||||
// A delayed floor/data repaint must not steal that newer selection back.
|
// A delayed floor/data repaint must not steal that newer selection back.
|
||||||
@@ -194,6 +204,7 @@ export function createRendering({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
else services.credits?.hideOsmCredit?.(layerState.viewer, LAYER_ID);
|
else services.credits?.hideOsmCredit?.(layerState.viewer, LAYER_ID);
|
||||||
|
syncPlacesCredit(layerState.records);
|
||||||
const records = renderableRecords();
|
const records = renderableRecords();
|
||||||
const keys = new Map(
|
const keys = new Map(
|
||||||
records.map((record) => [record.id, renderKey(record)]),
|
records.map((record) => [record.id, renderKey(record)]),
|
||||||
@@ -257,7 +268,7 @@ export function createRendering({
|
|||||||
Cesium.Cartesian3.fromDegrees(lon, lat),
|
Cesium.Cartesian3.fromDegrees(lon, lat),
|
||||||
),
|
),
|
||||||
width: 2,
|
width: 2,
|
||||||
material: color.withAlpha(0.85),
|
material: new Cesium.ColorMaterialProperty(color.withAlpha(0.85)),
|
||||||
clampToGround: true,
|
clampToGround: true,
|
||||||
classificationType: Cesium.ClassificationType.BOTH,
|
classificationType: Cesium.ClassificationType.BOTH,
|
||||||
},
|
},
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as Cesium from 'cesium';
|
||||||
|
import { rgbaFromCss } from '../../../shared/color.js';
|
||||||
|
import { createRendering } from './rendering.js';
|
||||||
|
|
||||||
|
test('Cesium installation outlines use a valid material and retain ground clamping', () => {
|
||||||
|
const id = 'osm:way:980001';
|
||||||
|
const footprint = [
|
||||||
|
[-122.438, 37.758],
|
||||||
|
[-122.402, 37.758],
|
||||||
|
[-122.402, 37.782],
|
||||||
|
[-122.438, 37.782],
|
||||||
|
];
|
||||||
|
const record = {
|
||||||
|
id,
|
||||||
|
kind: 'installation',
|
||||||
|
class: 'airfield',
|
||||||
|
name: 'QA Airfield',
|
||||||
|
latitude: 37.77,
|
||||||
|
longitude: -122.42,
|
||||||
|
footprint,
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
enabled: true,
|
||||||
|
selectedId: null,
|
||||||
|
records: [record],
|
||||||
|
recordById: new Map([[id, record]]),
|
||||||
|
renderedKeys: new Map(),
|
||||||
|
dataSource: new Cesium.CustomDataSource('military-installations'),
|
||||||
|
viewer: { scene: { globe: { show: false } } },
|
||||||
|
};
|
||||||
|
const services = {
|
||||||
|
ground: { floorAltitudeM: () => 125, cachedGroundFloor: () => ({}) },
|
||||||
|
anchors: { warmFireAnchorFloors: async () => false },
|
||||||
|
render: { governorRequestRender() {} },
|
||||||
|
context: {
|
||||||
|
getSelectedEntityContext: () => null,
|
||||||
|
registerEntityContext() {},
|
||||||
|
removeEntityContextsForLayer() {},
|
||||||
|
selectEntityContext() {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const parts = {
|
||||||
|
model: {
|
||||||
|
colorFor: () => rgbaFromCss('#ff8000'),
|
||||||
|
installationSourceLabel: () => 'OpenStreetMap',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Object.assign(
|
||||||
|
parts,
|
||||||
|
createRendering({ state, services, parts, source: {} }),
|
||||||
|
);
|
||||||
|
|
||||||
|
parts.renderRecords();
|
||||||
|
|
||||||
|
const time = Cesium.JulianDate.now();
|
||||||
|
const marker = state.dataSource.entities.getById(id);
|
||||||
|
const outline = state.dataSource.entities.getById(`${id}:outline:0`);
|
||||||
|
assert.equal(
|
||||||
|
marker.point.heightReference.getValue(time),
|
||||||
|
Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||||
|
);
|
||||||
|
assert.ok(outline.polyline.material instanceof Cesium.ColorMaterialProperty);
|
||||||
|
assert.equal(outline.polyline.clampToGround.getValue(time), true);
|
||||||
|
for (const position of outline.polyline.positions.getValue(time))
|
||||||
|
assert.ok(
|
||||||
|
Math.abs(
|
||||||
|
Cesium.Cartographic.fromCartesian(position).height,
|
||||||
|
) < 0.01,
|
||||||
|
);
|
||||||
|
});
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { isPointerFree } from '../../data/inputOwnership.js';
|
import { isPointerFree } from '../../../../data/inputOwnership.js';
|
||||||
import { LAYER_ID } from './policy.js';
|
import { LAYER_ID } from '../../policy.js';
|
||||||
|
|
||||||
export function createSelection({
|
export function createSelection({
|
||||||
state: layerState,
|
state: layerState,
|
||||||
@@ -0,0 +1,659 @@
|
|||||||
|
import { Effect, EffectAttribute } from 'postprocessing';
|
||||||
|
import * as geo from '../../../geo/index.js';
|
||||||
|
import { selectionDispatcherFor } from '../../../data/selectionDispatcher.js';
|
||||||
|
import { createInfrastructureOverlayEntry } from '../../../data/infrastructureOverlayEntry.js';
|
||||||
|
import {
|
||||||
|
MILITARY_LABEL_CAP,
|
||||||
|
MILITARY_POINT_CAP,
|
||||||
|
} from '../../../data/militaryNames.js';
|
||||||
|
import { COLOR_BY_CLASS, LAYER_ID, MAX_RENDERED } from '../policy.js';
|
||||||
|
import {
|
||||||
|
createInstallationDrapedMesh,
|
||||||
|
createInstallationOutlineDrapedMesh,
|
||||||
|
} from './navaraGeometry.js';
|
||||||
|
|
||||||
|
const registeredViews = new WeakSet();
|
||||||
|
const KEY = 'gevInstallationsDrape';
|
||||||
|
const BLACK = [0, 0, 0, 0.8];
|
||||||
|
const IDLE_SHADER = /* glsl */ `
|
||||||
|
void mainImage(const in vec4 inputColor,const in vec2 uv,out vec4 outputColor) {
|
||||||
|
outputColor=inputColor;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function createDrapeEffectClass(EffectDesc, NavaraEffect) {
|
||||||
|
return class InstallationDrapeDesc extends EffectDesc {
|
||||||
|
static key = KEY;
|
||||||
|
static insertAfter = ['mrt'];
|
||||||
|
|
||||||
|
constructor(view, ctx, config) {
|
||||||
|
super(view, ctx, config);
|
||||||
|
this.owner = config[KEY].owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
createPass() {
|
||||||
|
this.effect = new Effect('GevInstallationDrape', IDLE_SHADER, {
|
||||||
|
attributes: EffectAttribute.CONVOLUTION,
|
||||||
|
});
|
||||||
|
return new NavaraEffect(this.view.camera.raw, this.effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const mrt = this.find('mrt');
|
||||||
|
this.owner.globeNormal.value = mrt?.globeNormalBuffer ?? null;
|
||||||
|
this.owner.globeDepth.value = mrt?.globeDepthBuffer ?? null;
|
||||||
|
const scene = this.ctx.scenes.draped;
|
||||||
|
this.owner.scene = scene;
|
||||||
|
for (const mesh of this.owner.meshes)
|
||||||
|
if (mesh.parent !== scene) scene.add(mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy() {
|
||||||
|
for (const mesh of this.owner.meshes) {
|
||||||
|
if (mesh.parent) mesh.parent.remove(mesh);
|
||||||
|
mesh.geometry.dispose();
|
||||||
|
mesh.material.dispose();
|
||||||
|
}
|
||||||
|
this.owner.meshes.clear();
|
||||||
|
this.owner.scene = null;
|
||||||
|
this.effect?.dispose();
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPointSet(engine, { id, sizePx, capacity, order }) {
|
||||||
|
return engine.movers.createSet({
|
||||||
|
id,
|
||||||
|
kind: 'point',
|
||||||
|
capacity,
|
||||||
|
order,
|
||||||
|
depthTest: 'normal',
|
||||||
|
pickable: true,
|
||||||
|
point: { sizePx, outlinePx: 1, outlineColor: BLACK },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createInstallationsNavaraRenderer(
|
||||||
|
engine,
|
||||||
|
{
|
||||||
|
state,
|
||||||
|
services,
|
||||||
|
parts,
|
||||||
|
source,
|
||||||
|
overlayHost,
|
||||||
|
view,
|
||||||
|
EffectDesc,
|
||||||
|
NavaraEffect,
|
||||||
|
DrapedMesh,
|
||||||
|
setupMaterialForDrape,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!engine?.movers ||
|
||||||
|
!view?.registerEffect ||
|
||||||
|
!EffectDesc ||
|
||||||
|
!NavaraEffect ||
|
||||||
|
!DrapedMesh ||
|
||||||
|
!setupMaterialForDrape
|
||||||
|
)
|
||||||
|
throw new TypeError('Installations require a Navara view');
|
||||||
|
if (!registeredViews.has(view)) {
|
||||||
|
view.registerEffect(KEY, createDrapeEffectClass(EffectDesc, NavaraEffect));
|
||||||
|
registeredViews.add(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
const layerState = state;
|
||||||
|
const owner = {
|
||||||
|
meshes: new Set(),
|
||||||
|
scene: null,
|
||||||
|
globeNormal: { value: null },
|
||||||
|
globeDepth: { value: null },
|
||||||
|
};
|
||||||
|
const normalSet = createPointSet(engine, {
|
||||||
|
id: `${LAYER_ID}-normal`,
|
||||||
|
sizePx: 9,
|
||||||
|
capacity: MAX_RENDERED + 1,
|
||||||
|
order: 10,
|
||||||
|
});
|
||||||
|
const selectedSet = createPointSet(engine, {
|
||||||
|
id: `${LAYER_ID}-selected`,
|
||||||
|
sizePx: 13,
|
||||||
|
capacity: 1,
|
||||||
|
order: 11,
|
||||||
|
});
|
||||||
|
const namedSet = createPointSet(engine, {
|
||||||
|
id: `${LAYER_ID}-named`,
|
||||||
|
sizePx: 7,
|
||||||
|
capacity: MILITARY_POINT_CAP,
|
||||||
|
order: 9,
|
||||||
|
});
|
||||||
|
const contextObjects = new Map();
|
||||||
|
const namedRecords = new Map();
|
||||||
|
const selectionMeshes = new Set();
|
||||||
|
const outlinesById = new Map();
|
||||||
|
let drapeHandle = null;
|
||||||
|
let visible = false;
|
||||||
|
let fillRecord = null;
|
||||||
|
let interactionRemove = null;
|
||||||
|
let removeContextListeners = null;
|
||||||
|
let destroyed = false;
|
||||||
|
|
||||||
|
const displayPosition = (record) =>
|
||||||
|
geo.cartesian3.fromDegrees(
|
||||||
|
record.longitude,
|
||||||
|
record.latitude,
|
||||||
|
parts.model.installationSurfaceHeightM(record),
|
||||||
|
);
|
||||||
|
const rgba = (record, selected = false) => {
|
||||||
|
const color = parts.model.colorFor(record);
|
||||||
|
return selected
|
||||||
|
? [1, 1, 1, 1]
|
||||||
|
: [color.red, color.green, color.blue, color.alpha];
|
||||||
|
};
|
||||||
|
const schedule = () => engine.render.request('installations-render');
|
||||||
|
|
||||||
|
function syncPlacesCredit(records) {
|
||||||
|
const usesPlaces = records.some((record) =>
|
||||||
|
record.sources?.some((item) => item.name === 'Google Maps Places'),
|
||||||
|
);
|
||||||
|
if (usesPlaces)
|
||||||
|
services.credits?.showGooglePlacesCredit?.(engine, LAYER_ID);
|
||||||
|
else services.credits?.hideGooglePlacesCredit?.(engine, LAYER_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceOwnedMeshes(previous, next) {
|
||||||
|
for (const mesh of previous) {
|
||||||
|
if (mesh.parent) mesh.parent.remove(mesh);
|
||||||
|
mesh.geometry.dispose();
|
||||||
|
mesh.material.dispose();
|
||||||
|
owner.meshes.delete(mesh);
|
||||||
|
}
|
||||||
|
previous.clear();
|
||||||
|
for (const mesh of next) {
|
||||||
|
previous.add(mesh);
|
||||||
|
owner.meshes.add(mesh);
|
||||||
|
}
|
||||||
|
syncDrapeEffect();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMeshList(next) {
|
||||||
|
replaceOwnedMeshes(selectionMeshes, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncDrapeEffect() {
|
||||||
|
const show = visible && owner.meshes.size > 0;
|
||||||
|
if (show && !drapeHandle)
|
||||||
|
drapeHandle = view.addEffect({ [KEY]: { owner }, visible: true });
|
||||||
|
else if (drapeHandle) drapeHandle.update({ visible: show });
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillMeshes(record) {
|
||||||
|
const footprints =
|
||||||
|
record.footprints || (record.footprint ? [[record.footprint]] : []);
|
||||||
|
const color = parts.model.colorFor(record).toCssColorString();
|
||||||
|
const meshes = [];
|
||||||
|
for (const rings of footprints) {
|
||||||
|
const mesh = createInstallationDrapedMesh(record, rings, {
|
||||||
|
color,
|
||||||
|
opacity: 0.22,
|
||||||
|
globeNormal: owner.globeNormal,
|
||||||
|
globeDepth: owner.globeDepth,
|
||||||
|
DrapedMesh,
|
||||||
|
setupMaterialForDrape,
|
||||||
|
});
|
||||||
|
if (mesh) meshes.push(mesh);
|
||||||
|
}
|
||||||
|
return meshes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelectionFill() {
|
||||||
|
fillRecord = null;
|
||||||
|
setMeshList([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncSelectionFill() {
|
||||||
|
const record =
|
||||||
|
layerState.enabled && layerState.recordById.get(layerState.selectedId);
|
||||||
|
if (record === fillRecord) return;
|
||||||
|
fillRecord = record ?? null;
|
||||||
|
setMeshList(record ? fillMeshes(record) : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncOutlines(records) {
|
||||||
|
const canvasHeight = Math.max(1, view.canvas?.clientHeight || 1);
|
||||||
|
const cameraHeight = Math.max(0, engine.camera.height?.() || 0);
|
||||||
|
const fov = ((view.camera?.raw?.fov || 60) * Math.PI) / 180;
|
||||||
|
const widthM = Math.max(
|
||||||
|
1,
|
||||||
|
(2 * cameraHeight * Math.tan(fov / 2) * 2) / canvasHeight,
|
||||||
|
);
|
||||||
|
const desired = new Map(
|
||||||
|
records
|
||||||
|
.filter((record) => !record.pointOnly && !record.namedArea)
|
||||||
|
.map((record) => [record.id, record]),
|
||||||
|
);
|
||||||
|
for (const [id, entry] of outlinesById) {
|
||||||
|
if (desired.get(id) === entry.record && entry.widthM === widthM) continue;
|
||||||
|
replaceOwnedMeshes(entry.meshes, []);
|
||||||
|
outlinesById.delete(id);
|
||||||
|
}
|
||||||
|
for (const [id, record] of desired) {
|
||||||
|
if (outlinesById.has(id)) continue;
|
||||||
|
const lines =
|
||||||
|
record.outlineLines || (record.footprint ? [record.footprint] : []);
|
||||||
|
const meshes = lines
|
||||||
|
.map((line) =>
|
||||||
|
createInstallationOutlineDrapedMesh(record, line, {
|
||||||
|
color: parts.model.colorFor(record).toCssColorString(),
|
||||||
|
widthM,
|
||||||
|
globeNormal: owner.globeNormal,
|
||||||
|
globeDepth: owner.globeDepth,
|
||||||
|
DrapedMesh,
|
||||||
|
setupMaterialForDrape,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.filter(Boolean);
|
||||||
|
const owned = new Set(meshes);
|
||||||
|
for (const mesh of owned) owner.meshes.add(mesh);
|
||||||
|
outlinesById.set(id, { record, widthM, meshes: owned });
|
||||||
|
}
|
||||||
|
syncDrapeEffect();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearOutlines() {
|
||||||
|
for (const entry of outlinesById.values())
|
||||||
|
replaceOwnedMeshes(entry.meshes, []);
|
||||||
|
outlinesById.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerContext(record) {
|
||||||
|
let context = contextObjects.get(record.id);
|
||||||
|
if (!context) {
|
||||||
|
context = {};
|
||||||
|
contextObjects.set(record.id, context);
|
||||||
|
}
|
||||||
|
context.gevContextId = record.id;
|
||||||
|
context.gevLabelModel = {
|
||||||
|
title: record.name || 'MAPPED INSTALLATION',
|
||||||
|
details: [
|
||||||
|
String(record.class || 'installation')
|
||||||
|
.replaceAll('_', ' ')
|
||||||
|
.toUpperCase(),
|
||||||
|
...(record.memberNames || []).slice(0, 3),
|
||||||
|
],
|
||||||
|
accent: COLOR_BY_CLASS[record.class] || '#9ca6b0',
|
||||||
|
};
|
||||||
|
services.context.registerEntityContext(context, {
|
||||||
|
id: record.id,
|
||||||
|
layerId: LAYER_ID,
|
||||||
|
layerName:
|
||||||
|
record.kind === 'place_candidate'
|
||||||
|
? 'Military Site Search Candidates'
|
||||||
|
: 'Mapped Military Installations',
|
||||||
|
source: parts.model.installationSourceLabel(record),
|
||||||
|
label: record.name,
|
||||||
|
latitude: record.latitude,
|
||||||
|
longitude: record.longitude,
|
||||||
|
properties: {
|
||||||
|
class: record.class,
|
||||||
|
memberNames: record.memberNames || [],
|
||||||
|
primaryType: record.primaryType || null,
|
||||||
|
placeTypes: Array.isArray(record.placeTypes) ? record.placeTypes : [],
|
||||||
|
validation: record.validation,
|
||||||
|
retrievedAt: record.retrievedAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRecords({ claimSelection = false } = {}) {
|
||||||
|
if (destroyed) return;
|
||||||
|
const selectedContext = services.context.getSelectedEntityContext();
|
||||||
|
if (
|
||||||
|
!claimSelection &&
|
||||||
|
layerState.selectedId &&
|
||||||
|
selectedContext?.id !== layerState.selectedId
|
||||||
|
)
|
||||||
|
layerState.selectedId = null;
|
||||||
|
if (
|
||||||
|
layerState.selectedId &&
|
||||||
|
!layerState.recordById.has(layerState.selectedId)
|
||||||
|
) {
|
||||||
|
const replacement = layerState.records.find((record) =>
|
||||||
|
record.aliasIds?.includes(layerState.selectedId),
|
||||||
|
);
|
||||||
|
layerState.selectedId = replacement?.id ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const records = layerState.records.filter(
|
||||||
|
(record) => !record.pointOnly && !record.namedArea,
|
||||||
|
);
|
||||||
|
const hasOsm = layerState.records.some((record) =>
|
||||||
|
record.sources?.some((item) => item.name === 'OpenStreetMap'),
|
||||||
|
);
|
||||||
|
if (hasOsm)
|
||||||
|
services.credits?.showOsmCredit?.(engine, LAYER_ID, {
|
||||||
|
openMapTiles: layerState.records.some((record) =>
|
||||||
|
record.sources?.some(
|
||||||
|
(item) =>
|
||||||
|
item.name === 'OpenStreetMap' && item.id?.startsWith('tile:'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
else services.credits?.hideOsmCredit?.(engine, LAYER_ID);
|
||||||
|
syncPlacesCredit(layerState.records);
|
||||||
|
const normal = records
|
||||||
|
.filter((record) => record.id !== layerState.selectedId)
|
||||||
|
.slice(0, MAX_RENDERED);
|
||||||
|
const selected = layerState.records.find(
|
||||||
|
(record) => record.id === layerState.selectedId && !record.pointOnly,
|
||||||
|
);
|
||||||
|
normalSet.setCount(normal.length);
|
||||||
|
for (let index = 0; index < normal.length; index++) {
|
||||||
|
const record = normal[index];
|
||||||
|
normalSet.set(index, {
|
||||||
|
position: displayPosition(record),
|
||||||
|
color: rgba(record),
|
||||||
|
pickId: record.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
selectedSet.setCount(selected ? 1 : 0);
|
||||||
|
if (selected)
|
||||||
|
selectedSet.set(0, {
|
||||||
|
position: displayPosition(selected),
|
||||||
|
color: rgba(selected, true),
|
||||||
|
pickId: selected.id,
|
||||||
|
});
|
||||||
|
normalSet.commit();
|
||||||
|
selectedSet.commit();
|
||||||
|
|
||||||
|
const retainIds = new Set(
|
||||||
|
[...normal, ...(selected ? [selected] : [])].map((r) => r.id),
|
||||||
|
);
|
||||||
|
for (const record of [...normal, ...(selected ? [selected] : [])]) {
|
||||||
|
const entity = registerContext(record);
|
||||||
|
if (
|
||||||
|
record.id === layerState.selectedId &&
|
||||||
|
(claimSelection || !selectedContext)
|
||||||
|
)
|
||||||
|
services.context.selectEntityContext(entity);
|
||||||
|
}
|
||||||
|
services.context.removeEntityContextsForLayer(LAYER_ID, { retainIds });
|
||||||
|
parts.namedMarkers.sync(layerState.records);
|
||||||
|
syncOutlines([...normal, ...(selected ? [selected] : [])]);
|
||||||
|
syncSelectionFill();
|
||||||
|
schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectRecord(id) {
|
||||||
|
if (!layerState.recordById.has(id)) return false;
|
||||||
|
layerState.selectedId = id;
|
||||||
|
renderRecords({ claimSelection: true });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deselect() {
|
||||||
|
if (!layerState.selectedId) return;
|
||||||
|
layerState.selectedId = null;
|
||||||
|
services.context.clearSelectedEntityContextForLayer(LAYER_ID);
|
||||||
|
renderRecords();
|
||||||
|
}
|
||||||
|
|
||||||
|
const namedMarkers = {
|
||||||
|
enable() {
|
||||||
|
namedSet.setCount(namedRecords.size);
|
||||||
|
overlayHost.setVisible(LAYER_ID, true);
|
||||||
|
},
|
||||||
|
hide() {
|
||||||
|
namedSet.setCount(0);
|
||||||
|
namedSet.commit();
|
||||||
|
overlayHost.clearSource(LAYER_ID);
|
||||||
|
overlayHost.setVisible(LAYER_ID, false);
|
||||||
|
},
|
||||||
|
sync(records) {
|
||||||
|
namedRecords.clear();
|
||||||
|
const named = records
|
||||||
|
.filter((record) => record.namedArea || record.pointOnly)
|
||||||
|
.sort((a, b) => b.areaM2 - a.areaM2)
|
||||||
|
.slice(0, MILITARY_POINT_CAP);
|
||||||
|
const entries = [];
|
||||||
|
for (let index = 0; index < named.length; index++) {
|
||||||
|
const record = named[index];
|
||||||
|
namedRecords.set(record.id, record);
|
||||||
|
const position = geo.cartesian3.fromDegrees(
|
||||||
|
record.longitude,
|
||||||
|
record.latitude,
|
||||||
|
record.pointOnly ? 0 : parts.model.installationSurfaceHeightM(record),
|
||||||
|
);
|
||||||
|
namedSet.set(index, {
|
||||||
|
position,
|
||||||
|
color: rgba(record),
|
||||||
|
pickId: record.id,
|
||||||
|
});
|
||||||
|
const entry = createInfrastructureOverlayEntry({
|
||||||
|
id: record.id,
|
||||||
|
source: LAYER_ID,
|
||||||
|
position,
|
||||||
|
title: record.name,
|
||||||
|
priority: record.areaM2 || 0,
|
||||||
|
accent: COLOR_BY_CLASS[record.class] || '#9ca6b0',
|
||||||
|
});
|
||||||
|
const selected = record.id === layerState.selectedId;
|
||||||
|
entry.variant = selected ? 'selected' : 'card';
|
||||||
|
entry.selected = selected;
|
||||||
|
entry.details = selected
|
||||||
|
? [
|
||||||
|
String(record.class || 'installation')
|
||||||
|
.replaceAll('_', ' ')
|
||||||
|
.toUpperCase(),
|
||||||
|
...(record.memberNames || []).slice(0, 3),
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
namedSet.setCount(visible ? named.length : 0);
|
||||||
|
namedSet.commit();
|
||||||
|
overlayHost.setEntries(LAYER_ID, entries, {
|
||||||
|
cohortLimit: MILITARY_POINT_CAP,
|
||||||
|
collisionCapacity: MILITARY_LABEL_CAP,
|
||||||
|
maxVisible: MILITARY_LABEL_CAP - (layerState.selectedId ? 1 : 0),
|
||||||
|
moving: false,
|
||||||
|
visible,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
visit(visitor) {
|
||||||
|
for (const record of namedRecords.values())
|
||||||
|
visitor(record.id, {
|
||||||
|
position: geo.cartesian3.fromDegrees(
|
||||||
|
record.longitude,
|
||||||
|
record.latitude,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
stats: () => ({
|
||||||
|
namedMarkers: namedRecords.size,
|
||||||
|
pointsOnScreen: 0,
|
||||||
|
labelsOnScreen: 0,
|
||||||
|
labelCap: MILITARY_LABEL_CAP,
|
||||||
|
}),
|
||||||
|
destroy() {
|
||||||
|
this.hide();
|
||||||
|
namedRecords.clear();
|
||||||
|
namedSet.destroy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const rendering = {
|
||||||
|
installationSurfaceHeightM: parts.model.installationSurfaceHeightM,
|
||||||
|
clearSelectionFill,
|
||||||
|
syncSelectionFill,
|
||||||
|
clearRendered() {
|
||||||
|
normalSet.setCount(0);
|
||||||
|
selectedSet.setCount(0);
|
||||||
|
normalSet.commit();
|
||||||
|
selectedSet.commit();
|
||||||
|
clearOutlines();
|
||||||
|
clearSelectionFill();
|
||||||
|
services.context.removeEntityContextsForLayer(LAYER_ID);
|
||||||
|
contextObjects.clear();
|
||||||
|
},
|
||||||
|
renderableRecords() {
|
||||||
|
const records = layerState.records
|
||||||
|
.filter((record) => !record.pointOnly && !record.namedArea)
|
||||||
|
.slice(0, MAX_RENDERED);
|
||||||
|
const selected = layerState.recordById.get(layerState.selectedId);
|
||||||
|
return selected && !records.some((record) => record.id === selected.id)
|
||||||
|
? [...records, selected]
|
||||||
|
: records;
|
||||||
|
},
|
||||||
|
renderRecords,
|
||||||
|
warmInstallationFloors(records) {
|
||||||
|
const cold = records
|
||||||
|
.filter(
|
||||||
|
(record) =>
|
||||||
|
services.ground.cachedGroundFloor(
|
||||||
|
record.latitude,
|
||||||
|
record.longitude,
|
||||||
|
) == null,
|
||||||
|
)
|
||||||
|
.map((record) => ({ lat: record.latitude, lon: record.longitude }));
|
||||||
|
if (!cold.length) return;
|
||||||
|
services.anchors.warmFireAnchorFloors(cold).then(() => {
|
||||||
|
if (!layerState.enabled || destroyed) return;
|
||||||
|
if (
|
||||||
|
cold.some(
|
||||||
|
(point) =>
|
||||||
|
services.ground.cachedGroundFloor(point.lat, point.lon) != null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
renderRecords();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const selection = {
|
||||||
|
selectRecord,
|
||||||
|
installInteraction() {
|
||||||
|
if (interactionRemove) return;
|
||||||
|
const dispatcher = selectionDispatcherFor(engine);
|
||||||
|
if (!dispatcher)
|
||||||
|
throw new Error('Installation selection dispatcher unavailable');
|
||||||
|
interactionRemove = dispatcher.register(LAYER_ID, {
|
||||||
|
owns: (id) => layerState.recordById.has(id),
|
||||||
|
onSelect: selectRecord,
|
||||||
|
onEmptyClick: deselect,
|
||||||
|
});
|
||||||
|
layerState.clickHandler = { destroy: interactionRemove };
|
||||||
|
const events = [
|
||||||
|
'gev:entity-selected',
|
||||||
|
'gev:entity-selection-cleared',
|
||||||
|
'gev:awareness-subject-selected',
|
||||||
|
];
|
||||||
|
const sync = (event) => {
|
||||||
|
if (!layerState.enabled) return;
|
||||||
|
const selected =
|
||||||
|
event?.type === 'gev:awareness-subject-selected'
|
||||||
|
? event.detail
|
||||||
|
: services.context.getSelectedEntityContext();
|
||||||
|
const next =
|
||||||
|
selected?.layerId === LAYER_ID &&
|
||||||
|
layerState.recordById.has(selected.id)
|
||||||
|
? selected.id
|
||||||
|
: null;
|
||||||
|
if (next === layerState.selectedId) return;
|
||||||
|
layerState.selectedId = next;
|
||||||
|
renderRecords();
|
||||||
|
};
|
||||||
|
for (const event of events)
|
||||||
|
globalThis.window?.addEventListener?.(event, sync);
|
||||||
|
removeContextListeners = () => {
|
||||||
|
for (const event of events)
|
||||||
|
globalThis.window?.removeEventListener?.(event, sync);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
interactionRemove?.();
|
||||||
|
interactionRemove = null;
|
||||||
|
removeContextListeners?.();
|
||||||
|
removeContextListeners = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const lifecycle = {
|
||||||
|
methods: {
|
||||||
|
init(viewer, { engine: initEngine = engine } = {}) {
|
||||||
|
layerState.viewer = viewer;
|
||||||
|
layerState.engine = initEngine;
|
||||||
|
const cameraChanged = () => {
|
||||||
|
if (!layerState.contextAnchor) parts.viewport.scheduleLoad();
|
||||||
|
};
|
||||||
|
layerState.moveEndRemove =
|
||||||
|
engine.camera.moveEnd.addEventListener(cameraChanged);
|
||||||
|
layerState.changedRemove =
|
||||||
|
engine.camera.changed.addEventListener(cameraChanged);
|
||||||
|
selection.installInteraction(engine);
|
||||||
|
},
|
||||||
|
enable() {
|
||||||
|
visible = true;
|
||||||
|
layerState.enabled = true;
|
||||||
|
for (const set of [normalSet, selectedSet]) set.setCount(0);
|
||||||
|
normalSet.commit();
|
||||||
|
selectedSet.commit();
|
||||||
|
namedMarkers.enable();
|
||||||
|
namedMarkers.sync(layerState.records || []);
|
||||||
|
services.picking.registerPickOwner(LAYER_ID, (id) =>
|
||||||
|
layerState.recordById.has(id),
|
||||||
|
);
|
||||||
|
renderRecords();
|
||||||
|
},
|
||||||
|
disable() {
|
||||||
|
visible = false;
|
||||||
|
layerState.enabled = false;
|
||||||
|
services.credits?.hideOsmCredit?.(engine, LAYER_ID);
|
||||||
|
syncPlacesCredit([]);
|
||||||
|
namedMarkers.hide();
|
||||||
|
layerState.contextAnchor = null;
|
||||||
|
layerState.contextPosition = null;
|
||||||
|
layerState.coverage = { kind: 'viewport' };
|
||||||
|
layerState.cameraLoadKey = null;
|
||||||
|
layerState.cameraLoadOwner = null;
|
||||||
|
services.picking.unregisterPickOwner(LAYER_ID);
|
||||||
|
parts.viewport.clearUnavailableRetry();
|
||||||
|
clearTimeout(layerState.timer);
|
||||||
|
layerState.abort?.abort();
|
||||||
|
layerState.abort = null;
|
||||||
|
layerState.loading = false;
|
||||||
|
normalSet.setCount(0);
|
||||||
|
selectedSet.setCount(0);
|
||||||
|
normalSet.commit();
|
||||||
|
selectedSet.commit();
|
||||||
|
clearOutlines();
|
||||||
|
clearSelectionFill();
|
||||||
|
services.context.clearSelectedEntityContextForLayer(LAYER_ID);
|
||||||
|
layerState.selectedId = null;
|
||||||
|
parts.ingestion.setInstallationStatus('idle');
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
source.destroy?.();
|
||||||
|
this.disable();
|
||||||
|
layerState.moveEndRemove?.();
|
||||||
|
layerState.changedRemove?.();
|
||||||
|
layerState.moveEndRemove = layerState.changedRemove = null;
|
||||||
|
selection.destroy();
|
||||||
|
rendering.clearRendered();
|
||||||
|
namedMarkers.destroy();
|
||||||
|
drapeHandle?.destroy();
|
||||||
|
drapeHandle = null;
|
||||||
|
normalSet.destroy();
|
||||||
|
selectedSet.destroy();
|
||||||
|
owner.scene = null;
|
||||||
|
layerState.viewer = null;
|
||||||
|
layerState.engine = null;
|
||||||
|
destroyed = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return { lifecycle, namedMarkers, rendering, selection };
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { Mesh } from 'three';
|
||||||
|
import { createInstallationsNavaraRenderer } from './navara.js';
|
||||||
|
|
||||||
|
test('selected installation fill is reused and removed with its scene resources', () => {
|
||||||
|
const record = {
|
||||||
|
id: 'osm:way:12',
|
||||||
|
longitude: 179.9,
|
||||||
|
latitude: 70,
|
||||||
|
class: 'military_land',
|
||||||
|
footprint: [
|
||||||
|
[179.88, 69.99],
|
||||||
|
[179.92, 69.99],
|
||||||
|
[179.92, 70.01],
|
||||||
|
[179.88, 70.01],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
enabled: true,
|
||||||
|
records: [record],
|
||||||
|
recordById: new Map([[record.id, record]]),
|
||||||
|
selectedId: null,
|
||||||
|
};
|
||||||
|
const color = {
|
||||||
|
red: 0.6,
|
||||||
|
green: 0.6,
|
||||||
|
blue: 0.6,
|
||||||
|
alpha: 1,
|
||||||
|
toCssColorString: () => '#9ca6b0',
|
||||||
|
};
|
||||||
|
const contexts = new Map();
|
||||||
|
let selectedContext = null;
|
||||||
|
const services = {
|
||||||
|
context: {
|
||||||
|
getSelectedEntityContext: () => selectedContext,
|
||||||
|
registerEntityContext: (entity, value) => contexts.set(value.id, entity),
|
||||||
|
selectEntityContext: (entity) => {
|
||||||
|
selectedContext = {
|
||||||
|
id: entity.gevContextId,
|
||||||
|
layerId: 'military-installations',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
removeEntityContextsForLayer() {},
|
||||||
|
},
|
||||||
|
picking: { registerPickOwner() {}, unregisterPickOwner() {} },
|
||||||
|
};
|
||||||
|
const sets = [];
|
||||||
|
const engine = {
|
||||||
|
movers: {
|
||||||
|
createSet(options) {
|
||||||
|
const set = {
|
||||||
|
options,
|
||||||
|
setCount() {},
|
||||||
|
set() {},
|
||||||
|
commit() {},
|
||||||
|
destroy() {},
|
||||||
|
};
|
||||||
|
sets.push(set);
|
||||||
|
return set;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
camera: { height: () => 1_000 },
|
||||||
|
render: { request() {} },
|
||||||
|
};
|
||||||
|
let effectConfig;
|
||||||
|
const view = {
|
||||||
|
canvas: { clientHeight: 800 },
|
||||||
|
camera: { raw: { fov: 60 } },
|
||||||
|
registerEffect() {},
|
||||||
|
addEffect(config) {
|
||||||
|
effectConfig = config;
|
||||||
|
return { update() {}, destroy() {} };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let meshCount = 0;
|
||||||
|
class TestDrapedMesh extends Mesh {
|
||||||
|
constructor(geometry, material) {
|
||||||
|
super(geometry, material);
|
||||||
|
meshCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class TestEffectDesc {}
|
||||||
|
const parts = {
|
||||||
|
model: {
|
||||||
|
colorFor: () => color,
|
||||||
|
installationSurfaceHeightM: () => 0,
|
||||||
|
installationSourceLabel: () => 'OpenStreetMap',
|
||||||
|
},
|
||||||
|
namedMarkers: { sync() {}, enable() {}, hide() {}, destroy() {} },
|
||||||
|
};
|
||||||
|
const renderer = createInstallationsNavaraRenderer(engine, {
|
||||||
|
state,
|
||||||
|
services,
|
||||||
|
parts,
|
||||||
|
source: {},
|
||||||
|
overlayHost: {
|
||||||
|
setVisible() {},
|
||||||
|
setEntries() {},
|
||||||
|
clearSource() {},
|
||||||
|
},
|
||||||
|
view,
|
||||||
|
EffectDesc: TestEffectDesc,
|
||||||
|
NavaraEffect: class {},
|
||||||
|
DrapedMesh: TestDrapedMesh,
|
||||||
|
setupMaterialForDrape() {},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderer.lifecycle.methods.enable();
|
||||||
|
assert.equal(
|
||||||
|
meshCount,
|
||||||
|
1,
|
||||||
|
'the ordinary outline is present before selection',
|
||||||
|
);
|
||||||
|
assert.equal(renderer.selection.selectRecord(record.id), true);
|
||||||
|
assert.equal(meshCount, 2, 'selection adds one fill beside the outline');
|
||||||
|
const mesh = [...effectConfig.gevInstallationsDrape.owner.meshes].find(
|
||||||
|
(item) => !item.userData.installationOutline,
|
||||||
|
);
|
||||||
|
let disposed = false;
|
||||||
|
mesh.geometry.addEventListener('dispose', () => {
|
||||||
|
disposed = true;
|
||||||
|
});
|
||||||
|
const scene = {
|
||||||
|
add(child) {
|
||||||
|
child.parent = this;
|
||||||
|
},
|
||||||
|
remove(child) {
|
||||||
|
child.parent = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
scene.add(mesh);
|
||||||
|
renderer.rendering.renderRecords();
|
||||||
|
assert.equal(meshCount, 2, 'an unchanged selection keeps its geometry');
|
||||||
|
|
||||||
|
renderer.rendering.clearSelectionFill();
|
||||||
|
assert.equal(mesh.parent, null);
|
||||||
|
assert.equal(disposed, true);
|
||||||
|
assert.equal(
|
||||||
|
[...effectConfig.gevInstallationsDrape.owner.meshes].some(
|
||||||
|
(item) => item.userData.installationOutline,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
'clearing selection preserves the ordinary outline',
|
||||||
|
);
|
||||||
|
assert.equal(sets.length, 3);
|
||||||
|
});
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import {
|
||||||
|
DoubleSide,
|
||||||
|
ExtrudeGeometry,
|
||||||
|
MeshLambertMaterial,
|
||||||
|
Path,
|
||||||
|
Shape,
|
||||||
|
} from 'three';
|
||||||
|
import * as geo from '../../../geo/index.js';
|
||||||
|
|
||||||
|
export const INSTALLATION_DRAPE_DEPTH_M = 40000;
|
||||||
|
|
||||||
|
function localPoint([longitude, latitude], origin, axes) {
|
||||||
|
const point = geo.cartesian3.fromDegrees(longitude, latitude);
|
||||||
|
const delta = geo.cartesian3.subtract(point, origin, geo.cartesian3.create());
|
||||||
|
return {
|
||||||
|
x: geo.cartesian3.dot(delta, axes.east),
|
||||||
|
y: geo.cartesian3.dot(delta, axes.north),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRing(path, ring, origin, axes) {
|
||||||
|
const points = ring.map((point) => localPoint(point, origin, axes));
|
||||||
|
if (points.length < 3) return false;
|
||||||
|
path.moveTo(points[0].x, points[0].y);
|
||||||
|
for (const point of points.slice(1)) path.lineTo(point.x, point.y);
|
||||||
|
path.closePath();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function offsetLine(line, halfWidth, origin, axes) {
|
||||||
|
const points = [];
|
||||||
|
for (const point of line) {
|
||||||
|
const local = localPoint(point, origin, axes);
|
||||||
|
const previous = points.at(-1);
|
||||||
|
if (
|
||||||
|
!previous ||
|
||||||
|
Math.hypot(local.x - previous.x, local.y - previous.y) >= 0.01
|
||||||
|
)
|
||||||
|
points.push(local);
|
||||||
|
}
|
||||||
|
const closed =
|
||||||
|
points.length > 2 &&
|
||||||
|
Math.hypot(points[0].x - points.at(-1).x, points[0].y - points.at(-1).y) <
|
||||||
|
1;
|
||||||
|
if (closed) points.pop();
|
||||||
|
if (points.length < 2) return null;
|
||||||
|
|
||||||
|
const side = (index, direction) => {
|
||||||
|
const previous = points[index === 0 ? points.length - 1 : index - 1];
|
||||||
|
const current = points[index];
|
||||||
|
const next = points[index === points.length - 1 ? 0 : index + 1];
|
||||||
|
const before = { x: current.x - previous.x, y: current.y - previous.y };
|
||||||
|
const after = { x: next.x - current.x, y: next.y - current.y };
|
||||||
|
const beforeLength = Math.hypot(before.x, before.y);
|
||||||
|
const afterLength = Math.hypot(after.x, after.y);
|
||||||
|
const beforeNormal = {
|
||||||
|
x: -before.y / beforeLength,
|
||||||
|
y: before.x / beforeLength,
|
||||||
|
};
|
||||||
|
const afterNormal = { x: -after.y / afterLength, y: after.x / afterLength };
|
||||||
|
if (!closed && (index === 0 || index === points.length - 1)) {
|
||||||
|
const normal = index === 0 ? afterNormal : beforeNormal;
|
||||||
|
return {
|
||||||
|
x: current.x + normal.x * halfWidth * direction,
|
||||||
|
y: current.y + normal.y * halfWidth * direction,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const miter = {
|
||||||
|
x: beforeNormal.x + afterNormal.x,
|
||||||
|
y: beforeNormal.y + afterNormal.y,
|
||||||
|
};
|
||||||
|
const length = Math.hypot(miter.x, miter.y);
|
||||||
|
if (length < 1e-6)
|
||||||
|
return {
|
||||||
|
x: current.x + afterNormal.x * halfWidth * direction,
|
||||||
|
y: current.y + afterNormal.y * halfWidth * direction,
|
||||||
|
};
|
||||||
|
miter.x /= length;
|
||||||
|
miter.y /= length;
|
||||||
|
const denominator = miter.x * afterNormal.x + miter.y * afterNormal.y;
|
||||||
|
const distance =
|
||||||
|
halfWidth * Math.min(4, 1 / Math.max(0.25, Math.abs(denominator)));
|
||||||
|
return {
|
||||||
|
x: current.x + miter.x * distance * direction,
|
||||||
|
y: current.y + miter.y * distance * direction,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const outer = points.map((_, index) => side(index, 1));
|
||||||
|
if (!closed) {
|
||||||
|
const inner = points.map((_, index) => side(index, -1)).reverse();
|
||||||
|
return [...outer, ...inner];
|
||||||
|
}
|
||||||
|
return { outer, inner: points.map((_, index) => side(index, -1)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a real DrapedMesh volume for one footprint, including its holes. */
|
||||||
|
export function createInstallationDrapedMesh(
|
||||||
|
record,
|
||||||
|
rings,
|
||||||
|
{
|
||||||
|
color,
|
||||||
|
opacity = 0.22,
|
||||||
|
globeNormal,
|
||||||
|
globeDepth,
|
||||||
|
DrapedMesh,
|
||||||
|
setupMaterialForDrape,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const origin = geo.cartesian3.fromDegrees(record.longitude, record.latitude);
|
||||||
|
const frame = geo.transforms.eastNorthUpToFixedFrame(origin);
|
||||||
|
const axes = {
|
||||||
|
east: geo.cartesian3.create(frame[0], frame[1], frame[2]),
|
||||||
|
north: geo.cartesian3.create(frame[4], frame[5], frame[6]),
|
||||||
|
};
|
||||||
|
const shape = new Shape();
|
||||||
|
if (!addRing(shape, rings[0] || [], origin, axes)) return null;
|
||||||
|
for (const ring of rings.slice(1)) {
|
||||||
|
const hole = new Path();
|
||||||
|
if (addRing(hole, ring, origin, axes)) shape.holes.push(hole);
|
||||||
|
}
|
||||||
|
|
||||||
|
const geometry = new ExtrudeGeometry(shape, {
|
||||||
|
depth: INSTALLATION_DRAPE_DEPTH_M,
|
||||||
|
bevelEnabled: false,
|
||||||
|
steps: 1,
|
||||||
|
curveSegments: 1,
|
||||||
|
});
|
||||||
|
geometry.translate(0, 0, -INSTALLATION_DRAPE_DEPTH_M / 2);
|
||||||
|
const material = new MeshLambertMaterial({
|
||||||
|
color,
|
||||||
|
opacity,
|
||||||
|
transparent: opacity < 1,
|
||||||
|
depthWrite: false,
|
||||||
|
side: DoubleSide,
|
||||||
|
});
|
||||||
|
setupMaterialForDrape?.(material, globeNormal, globeDepth);
|
||||||
|
const mesh = new DrapedMesh(geometry, material);
|
||||||
|
mesh.matrix.fromArray(frame);
|
||||||
|
mesh.matrixAutoUpdate = false;
|
||||||
|
mesh.matrixWorldNeedsUpdate = true;
|
||||||
|
mesh.userData.installationId = record.id;
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a ground-classified, fixed-world-width ribbon around a native outline. */
|
||||||
|
export function createInstallationOutlineDrapedMesh(
|
||||||
|
record,
|
||||||
|
line,
|
||||||
|
{ color, widthM, globeNormal, globeDepth, DrapedMesh, setupMaterialForDrape },
|
||||||
|
) {
|
||||||
|
const origin = geo.cartesian3.fromDegrees(record.longitude, record.latitude);
|
||||||
|
const frame = geo.transforms.eastNorthUpToFixedFrame(origin);
|
||||||
|
const axes = {
|
||||||
|
east: geo.cartesian3.create(frame[0], frame[1], frame[2]),
|
||||||
|
north: geo.cartesian3.create(frame[4], frame[5], frame[6]),
|
||||||
|
};
|
||||||
|
const outline = offsetLine(line, Math.max(1, widthM / 2), origin, axes);
|
||||||
|
if (!outline) return null;
|
||||||
|
|
||||||
|
const shape = new Shape();
|
||||||
|
const addLocalRing = (path, points) => {
|
||||||
|
if (points.length < 3) return false;
|
||||||
|
path.moveTo(points[0].x, points[0].y);
|
||||||
|
for (const point of points.slice(1)) path.lineTo(point.x, point.y);
|
||||||
|
path.closePath();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if (Array.isArray(outline)) {
|
||||||
|
if (!addLocalRing(shape, outline)) return null;
|
||||||
|
} else {
|
||||||
|
if (!addLocalRing(shape, outline.outer)) return null;
|
||||||
|
const hole = new Path();
|
||||||
|
if (addLocalRing(hole, outline.inner.slice().reverse()))
|
||||||
|
shape.holes.push(hole);
|
||||||
|
}
|
||||||
|
|
||||||
|
const geometry = new ExtrudeGeometry(shape, {
|
||||||
|
depth: INSTALLATION_DRAPE_DEPTH_M,
|
||||||
|
bevelEnabled: false,
|
||||||
|
steps: 1,
|
||||||
|
curveSegments: 1,
|
||||||
|
});
|
||||||
|
geometry.translate(0, 0, -INSTALLATION_DRAPE_DEPTH_M / 2);
|
||||||
|
const material = new MeshLambertMaterial({
|
||||||
|
color,
|
||||||
|
opacity: 0.85,
|
||||||
|
transparent: true,
|
||||||
|
depthWrite: false,
|
||||||
|
side: DoubleSide,
|
||||||
|
});
|
||||||
|
setupMaterialForDrape?.(material, globeNormal, globeDepth);
|
||||||
|
const mesh = new DrapedMesh(geometry, material);
|
||||||
|
mesh.matrix.fromArray(frame);
|
||||||
|
mesh.matrixAutoUpdate = false;
|
||||||
|
mesh.matrixWorldNeedsUpdate = true;
|
||||||
|
mesh.userData.installationId = record.id;
|
||||||
|
mesh.userData.installationOutline = true;
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { Mesh, MeshLambertMaterial } from 'three';
|
||||||
|
import * as geo from '../../../geo/index.js';
|
||||||
|
import {
|
||||||
|
createInstallationDrapedMesh,
|
||||||
|
createInstallationOutlineDrapedMesh,
|
||||||
|
INSTALLATION_DRAPE_DEPTH_M,
|
||||||
|
} from './navaraGeometry.js';
|
||||||
|
|
||||||
|
const record = {
|
||||||
|
id: 'osm:way:12',
|
||||||
|
longitude: 179.9,
|
||||||
|
latitude: 70,
|
||||||
|
};
|
||||||
|
const texture = { value: null };
|
||||||
|
class TestDrapedMesh extends Mesh {
|
||||||
|
constructor(geometry, material) {
|
||||||
|
super(geometry, material);
|
||||||
|
this.drapedEnable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('installation footprint becomes an engine DrapedMesh with hole and ground refs', () => {
|
||||||
|
const mesh = createInstallationDrapedMesh(
|
||||||
|
record,
|
||||||
|
[
|
||||||
|
[
|
||||||
|
[179.88, 69.99],
|
||||||
|
[179.92, 69.99],
|
||||||
|
[179.92, 70.01],
|
||||||
|
[179.88, 70.01],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[179.895, 69.995],
|
||||||
|
[179.905, 69.995],
|
||||||
|
[179.905, 70.005],
|
||||||
|
[179.895, 70.005],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
{
|
||||||
|
color: '#9ca6b0',
|
||||||
|
opacity: 0.22,
|
||||||
|
globeNormal: texture,
|
||||||
|
globeDepth: texture,
|
||||||
|
DrapedMesh: TestDrapedMesh,
|
||||||
|
setupMaterialForDrape: (material, normal, depth) => {
|
||||||
|
material.userData.drapeRefs = [normal, depth];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(mesh instanceof TestDrapedMesh);
|
||||||
|
assert.ok(mesh.material instanceof MeshLambertMaterial);
|
||||||
|
assert.equal(mesh.userData.installationId, record.id);
|
||||||
|
assert.equal(mesh.geometry.parameters.shapes.holes.length, 1);
|
||||||
|
mesh.geometry.computeBoundingBox();
|
||||||
|
assert.equal(
|
||||||
|
mesh.geometry.boundingBox.max.z - mesh.geometry.boundingBox.min.z,
|
||||||
|
INSTALLATION_DRAPE_DEPTH_M,
|
||||||
|
);
|
||||||
|
assert.deepEqual(mesh.material.userData.drapeRefs, [texture, texture]);
|
||||||
|
assert.equal(mesh.matrixAutoUpdate, false);
|
||||||
|
assert.deepEqual(
|
||||||
|
[
|
||||||
|
mesh.matrix.elements[12],
|
||||||
|
mesh.matrix.elements[13],
|
||||||
|
mesh.matrix.elements[14],
|
||||||
|
],
|
||||||
|
Object.values(
|
||||||
|
geo.cartesian3.fromDegrees(record.longitude, record.latitude),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
mesh.geometry.dispose();
|
||||||
|
mesh.material.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid installation footprint rings do not allocate a mesh', () => {
|
||||||
|
assert.equal(
|
||||||
|
createInstallationDrapedMesh(
|
||||||
|
record,
|
||||||
|
[
|
||||||
|
[
|
||||||
|
[0, 0],
|
||||||
|
[1, 1],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
{
|
||||||
|
color: '#9ca6b0',
|
||||||
|
globeNormal: texture,
|
||||||
|
globeDepth: texture,
|
||||||
|
DrapedMesh: TestDrapedMesh,
|
||||||
|
setupMaterialForDrape() {},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ordinary installation outlines are draped ribbons across the date line', () => {
|
||||||
|
const mesh = createInstallationOutlineDrapedMesh(
|
||||||
|
record,
|
||||||
|
[
|
||||||
|
[179.88, 69.99],
|
||||||
|
[-179.92, 69.99],
|
||||||
|
[-179.92, 70.01],
|
||||||
|
[179.88, 70.01],
|
||||||
|
[179.88, 69.99],
|
||||||
|
],
|
||||||
|
{
|
||||||
|
color: '#9ca6b0',
|
||||||
|
widthM: 12,
|
||||||
|
globeNormal: texture,
|
||||||
|
globeDepth: texture,
|
||||||
|
DrapedMesh: TestDrapedMesh,
|
||||||
|
setupMaterialForDrape: (material, normal, depth) => {
|
||||||
|
material.userData.drapeRefs = [normal, depth];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(mesh instanceof TestDrapedMesh);
|
||||||
|
assert.equal(mesh.userData.installationOutline, true);
|
||||||
|
assert.equal(mesh.geometry.parameters.shapes.holes.length, 1);
|
||||||
|
mesh.geometry.computeBoundingBox();
|
||||||
|
assert.ok(
|
||||||
|
mesh.geometry.boundingBox.max.x - mesh.geometry.boundingBox.min.x < 20_000,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
mesh.geometry.boundingBox.max.z - mesh.geometry.boundingBox.min.z,
|
||||||
|
INSTALLATION_DRAPE_DEPTH_M,
|
||||||
|
);
|
||||||
|
assert.deepEqual(mesh.material.userData.drapeRefs, [texture, texture]);
|
||||||
|
mesh.geometry.dispose();
|
||||||
|
mesh.material.dispose();
|
||||||
|
});
|
||||||
@@ -2,9 +2,9 @@ import test from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import * as context from '../../data/contextStore.js';
|
import * as context from '../../data/contextStore.js';
|
||||||
import { createSelection } from './selection.js';
|
import { createSelection } from './render/cesium/selection.js';
|
||||||
import { createRendering } from './rendering.js';
|
import { createRendering } from './render/cesium/rendering.js';
|
||||||
import { createNamedMarkers } from './namedMarkers.js';
|
import { createNamedMarkers } from './render/cesium/namedMarkers.js';
|
||||||
|
|
||||||
function harness(t) {
|
function harness(t) {
|
||||||
const previousWindow = globalThis.window;
|
const previousWindow = globalThis.window;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import * as C from 'cesium';
|
import * as C from 'cesium';
|
||||||
import { createRendering } from './rendering.js';
|
import { createRendering } from './render/cesium/rendering.js';
|
||||||
import { createLifecycle } from './lifecycle.js';
|
import { createLifecycle } from './render/cesium/lifecycle.js';
|
||||||
|
|
||||||
function harness() {
|
function harness() {
|
||||||
const ring = [[-97, 30], [-96.99, 30], [-96.99, 30.01], [-97, 30]];
|
const ring = [[-97, 30], [-96.99, 30], [-96.99, 30.01], [-97, 30]];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createNamedMarkers } from '../layers/installations/namedMarkers.js';
|
import { createNamedMarkers } from '../layers/installations/render/cesium/namedMarkers.js';
|
||||||
import { expandApplicationHtml } from '../../build/application-html.js';
|
import { expandApplicationHtml } from '../../build/application-html.js';
|
||||||
import { readStylesheet } from '../testSupport/readStylesheet.mjs';
|
import { readStylesheet } from '../testSupport/readStylesheet.mjs';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ test('distinct named bases separated by one seam quantization step retain separa
|
|||||||
test('named A+B to B to A+B preserves canonical identity, parent title and selection', async () => {
|
test('named A+B to B to A+B preserves canonical identity, parent title and selection', async () => {
|
||||||
const C = await import('cesium');
|
const C = await import('cesium');
|
||||||
const { createRendering } =
|
const { createRendering } =
|
||||||
await import('../layers/installations/rendering.js');
|
await import('../layers/installations/render/cesium/rendering.js');
|
||||||
const aliases = new Map();
|
const aliases = new Map();
|
||||||
const named = (n, footprint, areaM2) =>
|
const named = (n, footprint, areaM2) =>
|
||||||
record(String(n * 10 + 2), footprint, {
|
record(String(n * 10 + 2), footprint, {
|
||||||
@@ -260,7 +260,7 @@ test('named A+B to B to A+B preserves canonical identity, parent title and selec
|
|||||||
test('cold names reconcile selection through enrichment, identical groups at another zoom and wide-point handoff', async () => {
|
test('cold names reconcile selection through enrichment, identical groups at another zoom and wide-point handoff', async () => {
|
||||||
const C = await import('cesium');
|
const C = await import('cesium');
|
||||||
const { createRendering } =
|
const { createRendering } =
|
||||||
await import('../layers/installations/rendering.js');
|
await import('../layers/installations/render/cesium/rendering.js');
|
||||||
const { retainNamedHandoff } =
|
const { retainNamedHandoff } =
|
||||||
await import('../layers/installations/ingestion.js');
|
await import('../layers/installations/ingestion.js');
|
||||||
const aliases = new Map();
|
const aliases = new Map();
|
||||||
|
|||||||
Reference in new issue
Block a user