mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
Retain the TeleGeography dataset and render cables and landings on Navara
This commit is contained in:
1 parent
2b4955c817
commit
5cea3e8cbb
13 files changed
+593
-12
No files matched your search
@@ -2372,6 +2372,7 @@
|
|||||||
"modules": [
|
"modules": [
|
||||||
"src/layers/submarineCables/geometry.js",
|
"src/layers/submarineCables/geometry.js",
|
||||||
"src/layers/submarineCables/index.js",
|
"src/layers/submarineCables/index.js",
|
||||||
|
"src/layers/submarineCables/session.js",
|
||||||
"src/layers/submarineCables/ingestion.js",
|
"src/layers/submarineCables/ingestion.js",
|
||||||
"src/layers/submarineCables/interaction.js",
|
"src/layers/submarineCables/interaction.js",
|
||||||
"src/layers/submarineCables/lifecycle.js",
|
"src/layers/submarineCables/lifecycle.js",
|
||||||
@@ -2960,6 +2961,7 @@
|
|||||||
"src/layers/submarineCables/bundledSource.js",
|
"src/layers/submarineCables/bundledSource.js",
|
||||||
"src/layers/submarineCables/geometry.js",
|
"src/layers/submarineCables/geometry.js",
|
||||||
"src/layers/submarineCables/index.js",
|
"src/layers/submarineCables/index.js",
|
||||||
|
"src/layers/submarineCables/session.js",
|
||||||
"src/layers/submarineCables/ingestion.js",
|
"src/layers/submarineCables/ingestion.js",
|
||||||
"src/layers/submarineCables/interaction.js",
|
"src/layers/submarineCables/interaction.js",
|
||||||
"src/layers/submarineCables/lifecycle.js",
|
"src/layers/submarineCables/lifecycle.js",
|
||||||
@@ -3674,7 +3676,8 @@
|
|||||||
"src/layers/installations/render/navara.js",
|
"src/layers/installations/render/navara.js",
|
||||||
"src/layers/awareness/render/navara.js",
|
"src/layers/awareness/render/navara.js",
|
||||||
"src/layers/infrastructure/render/navara.js",
|
"src/layers/infrastructure/render/navara.js",
|
||||||
"src/layers/infrastructure/render/navaraStems.js",
|
"src/layers/shared/render/navaraStems.js",
|
||||||
|
"src/layers/submarineCables/render/navara.js",
|
||||||
"src/layers/weather/render/navara.js",
|
"src/layers/weather/render/navara.js",
|
||||||
"src/layers/weather/render/navaraSurface.js",
|
"src/layers/weather/render/navaraSurface.js",
|
||||||
"src/layers/wind/render/navara.js",
|
"src/layers/wind/render/navara.js",
|
||||||
@@ -4033,6 +4036,7 @@
|
|||||||
"src/layers/satellites/tracking.js",
|
"src/layers/satellites/tracking.js",
|
||||||
"src/layers/submarineCables/geometry.js",
|
"src/layers/submarineCables/geometry.js",
|
||||||
"src/layers/submarineCables/index.js",
|
"src/layers/submarineCables/index.js",
|
||||||
|
"src/layers/submarineCables/session.js",
|
||||||
"src/layers/submarineCables/ingestion.js",
|
"src/layers/submarineCables/ingestion.js",
|
||||||
"src/layers/submarineCables/interaction.js",
|
"src/layers/submarineCables/interaction.js",
|
||||||
"src/layers/submarineCables/lifecycle.js",
|
"src/layers/submarineCables/lifecycle.js",
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { fromDegrees } from '../src/geo/cartesian3.js';
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||||
|
|
||||||
|
const base = new URL(process.argv[2]);
|
||||||
|
if (
|
||||||
|
!['localhost', '127.0.0.1'].includes(base.hostname) ||
|
||||||
|
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
|
||||||
|
base.port,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
throw new Error('Supply an unreserved local keyless preview URL');
|
||||||
|
base.search = '?engine=navara&welcome=0&look=cinematic';
|
||||||
|
base.hash =
|
||||||
|
'v=2&lat=55.3241&lon=10.7995&alt=25000&heading=0&pitch=-90&roll=0&map=sentinel2&l=';
|
||||||
|
const b = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--use-angle=metal', '--enable-gpu'],
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const p = await b.newPage();
|
||||||
|
await blockPaidQaMaps(p);
|
||||||
|
await p.setViewport({ width: 1024, height: 768 });
|
||||||
|
const errors = [];
|
||||||
|
p.on('pageerror', (e) => {
|
||||||
|
errors.push(e.message);
|
||||||
|
console.log('error', e.message.split('?')[0]);
|
||||||
|
});
|
||||||
|
const reads = [];
|
||||||
|
p.on('request', (req) => {
|
||||||
|
const path = new URL(req.url()).pathname;
|
||||||
|
if (/\/(cable-geo|landing-point-geo).*\.json$/.test(path)) reads.push(path);
|
||||||
|
});
|
||||||
|
p.on('console', (msg) => {
|
||||||
|
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
|
||||||
|
errors.push(msg.text());
|
||||||
|
});
|
||||||
|
await p.goto(base.href);
|
||||||
|
const ready = async (kind) => {
|
||||||
|
await p.waitForFunction(
|
||||||
|
(k) =>
|
||||||
|
window.__godsEyeView?.engine.kind === k &&
|
||||||
|
!document.querySelector('#globe-engine')?.disabled &&
|
||||||
|
!document.querySelector('.engine-switch-frame') &&
|
||||||
|
document.querySelector('#loading-screen.hidden'),
|
||||||
|
{ timeout: 90000 },
|
||||||
|
kind,
|
||||||
|
);
|
||||||
|
await p.evaluate(
|
||||||
|
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await ready('navara');
|
||||||
|
await p.evaluate(async () => {
|
||||||
|
const { dataManager, tileset } = window.__godsEyeView;
|
||||||
|
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
|
||||||
|
throw Error('Keyless preview required');
|
||||||
|
window.qaCables = dataManager.layers.get(
|
||||||
|
'telegeography-submarine-cables',
|
||||||
|
).module;
|
||||||
|
await dataManager.setEnabled(window.qaCables.id, true, { origin: 'user' });
|
||||||
|
});
|
||||||
|
for (const kind of ['navara', 'cesium', 'navara']) {
|
||||||
|
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||||
|
await p.select('#globe-engine', kind);
|
||||||
|
await ready(kind);
|
||||||
|
}
|
||||||
|
await p.waitForFunction(
|
||||||
|
() => window.qaCables.getStats().referenceLabelCount > 0,
|
||||||
|
{ timeout: 90000 },
|
||||||
|
);
|
||||||
|
const check = await p.evaluate(() => {
|
||||||
|
const { dataManager } = window.__godsEyeView;
|
||||||
|
const mod = dataManager.layers.get(
|
||||||
|
'telegeography-submarine-cables',
|
||||||
|
).module;
|
||||||
|
return { same: mod === window.qaCables, ...mod.getStats() };
|
||||||
|
});
|
||||||
|
console.log(kind, check);
|
||||||
|
assert.equal(check.same, true);
|
||||||
|
assert.equal(check.count, 2629);
|
||||||
|
assert.ok(check.referenceLabelCount <= 160);
|
||||||
|
assert.equal(check.error, null);
|
||||||
|
if (kind === 'navara') {
|
||||||
|
const screen = await p.evaluate(
|
||||||
|
(world) => {
|
||||||
|
const { engine } = window.__godsEyeView;
|
||||||
|
const at = engine.projector.worldToWindow(world, {});
|
||||||
|
const rect = engine.host.canvas.getBoundingClientRect();
|
||||||
|
return { x: rect.left + at.x, y: rect.top + at.y };
|
||||||
|
},
|
||||||
|
fromDegrees(10.799480647390464, 55.324102015186114),
|
||||||
|
);
|
||||||
|
await p.mouse.click(screen.x, screen.y);
|
||||||
|
await p.waitForFunction(
|
||||||
|
() => {
|
||||||
|
const c = window.__godsEyeView.engine.camera.pose();
|
||||||
|
return (
|
||||||
|
Math.abs(c.cartographic.height - 6500) < 80 &&
|
||||||
|
Math.abs(c.pitch + (52 * Math.PI) / 180) < 0.03
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ timeout: 20000 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.equal(reads.length, 2, JSON.stringify(reads));
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log(
|
||||||
|
'PASS full bundled cable dataset, bounded labels, landing-point fly-to and one read per file',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await b.close();
|
||||||
|
}
|
||||||
@@ -315,7 +315,7 @@ export function createApplicationCatalog({
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'telegeography-submarine-cables',
|
'telegeography-submarine-cables',
|
||||||
() => createApplicationCables({ source: sources.cables }),
|
() => sessionModel('telegeography-submarine-cables', () => createApplicationCables({ source: sources.cables, session: true })),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'local-firms',
|
'local-firms',
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { createSubmarineCableLayer } from '../../layers/submarineCables/index.js';
|
import { createSubmarineCableLayer } from '../../layers/submarineCables/index.js';
|
||||||
|
import { createCableSessionLayer } from '../../layers/submarineCables/session.js';
|
||||||
|
import { rendererFor } from './renderers.js';
|
||||||
import { overlayHost } from './overlayHost.js';
|
import { overlayHost } from './overlayHost.js';
|
||||||
/** Wire cable source geometry to the application overlay host. */
|
/** Wire cable source geometry to the application overlay host. */
|
||||||
export function createApplicationCables(options) {
|
export function createApplicationCables({ session = false, ...options }) {
|
||||||
return createSubmarineCableLayer({
|
const createCesium = (source) => createSubmarineCableLayer({
|
||||||
overlayHost,
|
overlayHost,
|
||||||
screenSpaceEventHandlerFactory: (canvas) =>
|
screenSpaceEventHandlerFactory: (canvas) =>
|
||||||
new Cesium.ScreenSpaceEventHandler(canvas),
|
new Cesium.ScreenSpaceEventHandler(canvas),
|
||||||
mapStackEventTarget: typeof window !== 'undefined' ? window : null,
|
mapStackEventTarget: typeof window !== 'undefined' ? window : null,
|
||||||
...options,
|
...options,
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
if (!session) return createCesium(options.source);
|
||||||
|
return createCableSessionLayer({
|
||||||
|
source: options.source,
|
||||||
|
createCesium,
|
||||||
|
createNavara: (engine) => rendererFor('telegeography-submarine-cables', engine, { overlayHost }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -69,6 +69,7 @@ test('the Navara mode registers migrated renderers once', async () => {
|
|||||||
'weather-cyclones',
|
'weather-cyclones',
|
||||||
'local-datacenters',
|
'local-datacenters',
|
||||||
'local-dams',
|
'local-dams',
|
||||||
|
'telegeography-submarine-cables',
|
||||||
'local-firms',
|
'local-firms',
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const NAVARA_CREDIT_KEYS = new Set([
|
|||||||
'eccc-aqhi',
|
'eccc-aqhi',
|
||||||
'wfigs',
|
'wfigs',
|
||||||
's2-ghostmaps',
|
's2-ghostmaps',
|
||||||
|
'telegeography',
|
||||||
'wind-gfs',
|
'wind-gfs',
|
||||||
'ecmwf-ifs',
|
'ecmwf-ifs',
|
||||||
'weather-noaa',
|
'weather-noaa',
|
||||||
@@ -119,6 +120,10 @@ export async function registerNavaraLayerRenderers() {
|
|||||||
return (engine, options) => createWeatherNavaraRenderer(engine, navaraViews.get(engine), options);
|
return (engine, options) => createWeatherNavaraRenderer(engine, navaraViews.get(engine), options);
|
||||||
},
|
},
|
||||||
])),
|
])),
|
||||||
|
'telegeography-submarine-cables': async () => {
|
||||||
|
const { createNavaraCables } = await import('../layers/submarineCables/render/navara.js');
|
||||||
|
return (engine, { overlayHost }) => createNavaraCables(engine, navaraViews.get(engine), overlayHost);
|
||||||
|
},
|
||||||
wind: async () => {
|
wind: async () => {
|
||||||
const { createWindNavaraRenderer } = await import('../layers/wind/render/navara.js');
|
const { createWindNavaraRenderer } = await import('../layers/wind/render/navara.js');
|
||||||
return (engine) => createWindNavaraRenderer(engine, navaraViews.get(engine));
|
return (engine) => createWindNavaraRenderer(engine, navaraViews.get(engine));
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { bindGroundShapePicks } from '../../shared/render/navaraGroundPicks.js';
|
|||||||
import { distanceToSegment } from '../../../globe/cpuPicks.js';
|
import { distanceToSegment } from '../../../globe/cpuPicks.js';
|
||||||
import { rgbaFromCss } from '../../shared/color.js';
|
import { rgbaFromCss } from '../../shared/color.js';
|
||||||
import { isPointerFree } from '../../../data/inputOwnership.js';
|
import { isPointerFree } from '../../../data/inputOwnership.js';
|
||||||
import { createStemBatch } from './navaraStems.js';
|
import { createStemBatch } from '../../shared/render/navaraStems.js';
|
||||||
|
|
||||||
export function createInfrastructureNavaraRenderer(
|
export function createInfrastructureNavaraRenderer(
|
||||||
engine,
|
engine,
|
||||||
@@ -263,7 +263,7 @@ export function createInfrastructureNavaraRenderer(
|
|||||||
depthTest: 'always',
|
depthTest: 'always',
|
||||||
point: { sizePx: 10, outlinePx: 2, outlineColor: [0, 0, 0, 1] },
|
point: { sizePx: 10, outlinePx: 2, outlineColor: [0, 0, 0, 1] },
|
||||||
});
|
});
|
||||||
stems = createStemBatch(native, color);
|
stems = createStemBatch(native, color, INFRA_LOD_ACTIVE_MAX);
|
||||||
for (const input of next.records) {
|
for (const input of next.records) {
|
||||||
const carto = geo.cartographic.fromDegrees(input.lon, input.lat);
|
const carto = geo.cartographic.fromDegrees(input.lon, input.lat);
|
||||||
const base = geo.cartesian3.fromDegrees(input.lon, input.lat),
|
const base = geo.cartesian3.fromDegrees(input.lon, input.lat),
|
||||||
|
|||||||
+4
-4
@@ -1,12 +1,11 @@
|
|||||||
import { LineSegments2 } from 'three/addons/lines/LineSegments2.js';
|
import { LineSegments2 } from 'three/addons/lines/LineSegments2.js';
|
||||||
import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js';
|
import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js';
|
||||||
import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
|
import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
|
||||||
import { INFRA_LOD_ACTIVE_MAX } from '../../../data/localGeojsonLod.js';
|
|
||||||
|
|
||||||
const KEY = 'gevInfrastructureStems';
|
const KEY = 'gevInfrastructureStems';
|
||||||
const views = new WeakSet();
|
const views = new WeakSet();
|
||||||
/** All visible stems use one draw and one fixed buffer; tips are a point bank. */
|
/** All visible stems use one draw and one fixed buffer; tips are a point bank. */
|
||||||
export function createStemBatch({ view, MeshDesc }, color) {
|
export function createStemBatch({ view, MeshDesc }, color, capacity, width = 3.5, opacity = 1) {
|
||||||
if (!views.has(view)) {
|
if (!views.has(view)) {
|
||||||
view.registerMesh(
|
view.registerMesh(
|
||||||
KEY,
|
KEY,
|
||||||
@@ -29,12 +28,13 @@ export function createStemBatch({ view, MeshDesc }, color) {
|
|||||||
);
|
);
|
||||||
views.add(view);
|
views.add(view);
|
||||||
}
|
}
|
||||||
const xyz = new Float32Array(6 * INFRA_LOD_ACTIVE_MAX);
|
const xyz = new Float32Array(6 * capacity);
|
||||||
const geometry = new LineSegmentsGeometry().setPositions(xyz);
|
const geometry = new LineSegmentsGeometry().setPositions(xyz);
|
||||||
geometry.instanceCount = 0;
|
geometry.instanceCount = 0;
|
||||||
const material = new LineMaterial({
|
const material = new LineMaterial({
|
||||||
color,
|
color,
|
||||||
linewidth: 3.5,
|
linewidth: width,
|
||||||
|
opacity,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
toneMapped: false,
|
toneMapped: false,
|
||||||
@@ -68,7 +68,7 @@ export function createLifecycle({ state, parts, source, mapStackEventTarget }) {
|
|||||||
// The gate is dirty-only; ask for the frame its sweep needs in case the
|
// The gate is dirty-only; ask for the frame its sweep needs in case the
|
||||||
// camera is parked and nothing else is rendering.
|
// camera is parked and nothing else is rendering.
|
||||||
(viewer || state._viewer)?.scene?.requestRender?.();
|
(viewer || state._viewer)?.scene?.requestRender?.();
|
||||||
void parts.ingestion.load(viewer || state._viewer);
|
return parts.ingestion.load(viewer || state._viewer);
|
||||||
},
|
},
|
||||||
|
|
||||||
disable() {
|
disable() {
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ export function createCableReferenceSweepGate({
|
|||||||
* @returns {boolean} True when the stem geometry was redefined.
|
* @returns {boolean} True when the stem geometry was redefined.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function updateCableReferenceStem(
|
export function updateCableReferenceTip(
|
||||||
record,
|
record,
|
||||||
cameraPositionWC,
|
cameraPositionWC,
|
||||||
canvasHeight,
|
canvasHeight,
|
||||||
@@ -289,6 +289,11 @@ export function updateCableReferenceStem(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
geo.cartesian3.clone(record.nextTip, record.tip);
|
geo.cartesian3.clone(record.nextTip, record.tip);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateCableReferenceStem(record, cameraPositionWC, canvasHeight, fov) {
|
||||||
|
if (!updateCableReferenceTip(record, cameraPositionWC, canvasHeight, fov)) return false;
|
||||||
record.stemPositionBufferIndex = 1 - record.stemPositionBufferIndex;
|
record.stemPositionBufferIndex = 1 - record.stemPositionBufferIndex;
|
||||||
const stemPositions =
|
const stemPositions =
|
||||||
record.stemPositionBuffers[record.stemPositionBufferIndex];
|
record.stemPositionBuffers[record.stemPositionBufferIndex];
|
||||||
|
|||||||
@@ -0,0 +1,268 @@
|
|||||||
|
import * as geo from '../../../geo/index.js';
|
||||||
|
import { Color } from 'three';
|
||||||
|
import { isPointerFree } from '../../../data/inputOwnership.js';
|
||||||
|
import {
|
||||||
|
featureLabel,
|
||||||
|
featureReference,
|
||||||
|
normalizeFeatures,
|
||||||
|
} from '../geometry.js';
|
||||||
|
import {
|
||||||
|
BASE_CABLE_COLOR,
|
||||||
|
BASE_LANDING_COLOR,
|
||||||
|
CABLE_OVERLAY_SOURCE_ID as id,
|
||||||
|
} from '../policy.js';
|
||||||
|
import {
|
||||||
|
cableReferencePriority,
|
||||||
|
clampLabel,
|
||||||
|
createCableOverlayEntry,
|
||||||
|
createCableOverlayPublisher,
|
||||||
|
createCableReferenceSweepGate,
|
||||||
|
selectCableReferenceLabelWinners,
|
||||||
|
updateCableReferenceTip,
|
||||||
|
} from '../overlay.js';
|
||||||
|
import { createStemBatch } from '../../shared/render/navaraStems.js';
|
||||||
|
import { bindGroundShapePicks } from '../../shared/render/navaraGroundPicks.js';
|
||||||
|
import { rgbaFromCss } from '../../shared/color.js';
|
||||||
|
import { distanceToSegment } from '../../../globe/cpuPicks.js';
|
||||||
|
|
||||||
|
export function createNavaraCables(engine, native, host) {
|
||||||
|
const { view } = native;
|
||||||
|
const publisher = createCableOverlayPublisher({ host }),
|
||||||
|
gate = createCableReferenceSweepGate();
|
||||||
|
const records = new Map(),
|
||||||
|
banks = [],
|
||||||
|
removers = [];
|
||||||
|
let source,
|
||||||
|
layer,
|
||||||
|
dataset,
|
||||||
|
lastLabels = null,
|
||||||
|
referenceLabelCount = 0;
|
||||||
|
const wake = () => engine.render.request(id);
|
||||||
|
function sweep() {
|
||||||
|
const position = engine.camera.livePosition;
|
||||||
|
if (!position || !gate.shouldRun({ positionWC: position })) return;
|
||||||
|
const { height } = engine.host.size(),
|
||||||
|
{ fovy } = engine.camera.pose();
|
||||||
|
for (const bank of banks) {
|
||||||
|
const visible = [];
|
||||||
|
for (const row of bank.rows) {
|
||||||
|
row.visible = engine.projector.horizonVisible(row.base);
|
||||||
|
if (!row.visible) continue;
|
||||||
|
row.distanceM = geo.cartesian3.distance(position, row.base);
|
||||||
|
updateCableReferenceTip(row, position, height, fovy);
|
||||||
|
bank.points.set(visible.length, {
|
||||||
|
position: row.tip,
|
||||||
|
pickId: row.id,
|
||||||
|
show: true,
|
||||||
|
color: bank.color,
|
||||||
|
});
|
||||||
|
visible.push(row);
|
||||||
|
}
|
||||||
|
bank.points.setCount(visible.length);
|
||||||
|
bank.points.commit();
|
||||||
|
bank.stems.set(visible, position);
|
||||||
|
}
|
||||||
|
const winners = selectCableReferenceLabelWinners([...records.values()]);
|
||||||
|
referenceLabelCount = winners.length;
|
||||||
|
const signature = winners
|
||||||
|
.map((r) => r.id + '/' + cableReferencePriority(r.distanceM))
|
||||||
|
.join('|');
|
||||||
|
if (signature !== lastLabels) {
|
||||||
|
lastLabels = signature;
|
||||||
|
publisher.publish(
|
||||||
|
winners.map((r) => {
|
||||||
|
r.entry.priority = cableReferencePriority(r.distanceM);
|
||||||
|
return r.entry;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function hide() {
|
||||||
|
for (const fn of removers.splice(0)) fn();
|
||||||
|
for (const bank of banks.splice(0)) {
|
||||||
|
bank.points?.destroy();
|
||||||
|
bank.stems?.destroy();
|
||||||
|
bank.surface?.destroy();
|
||||||
|
}
|
||||||
|
layer?.delete();
|
||||||
|
source?.delete();
|
||||||
|
source = layer = dataset = null;
|
||||||
|
records.clear();
|
||||||
|
gate.reset();
|
||||||
|
lastLabels = null;
|
||||||
|
referenceLabelCount = 0;
|
||||||
|
publisher.hide();
|
||||||
|
wake();
|
||||||
|
}
|
||||||
|
function show(next) {
|
||||||
|
if (next === dataset) return;
|
||||||
|
if (dataset) hide();
|
||||||
|
try {
|
||||||
|
const cables = normalizeFeatures(next.cables, 'cable'),
|
||||||
|
landings = normalizeFeatures(next.landingPoints, 'landing');
|
||||||
|
const shapes = [],
|
||||||
|
colors = new Map();
|
||||||
|
for (const [kind, features, css] of [
|
||||||
|
['cable', cables, BASE_CABLE_COLOR],
|
||||||
|
['landing-point', landings, BASE_LANDING_COLOR],
|
||||||
|
]) {
|
||||||
|
const rows = [],
|
||||||
|
color = rgbaFromCss(css).rgba();
|
||||||
|
for (const f of features) {
|
||||||
|
const ref = featureReference(f);
|
||||||
|
if (!ref) continue;
|
||||||
|
const key = id + ':' + kind + ':' + rows.length;
|
||||||
|
const base = geo.cartesian3.fromDegrees(ref.lon, ref.lat);
|
||||||
|
const row = {
|
||||||
|
id: key,
|
||||||
|
reference: ref,
|
||||||
|
label: clampLabel(featureLabel(f)),
|
||||||
|
kind,
|
||||||
|
base,
|
||||||
|
tip: geo.cartesian3.fromDegrees(ref.lon, ref.lat, 2500),
|
||||||
|
nextTip: geo.cartesian3.create(),
|
||||||
|
};
|
||||||
|
row.entry = createCableOverlayEntry(row);
|
||||||
|
records.set(key, row);
|
||||||
|
rows.push(row);
|
||||||
|
const geom = f.geometry;
|
||||||
|
if (geom.type === 'Point')
|
||||||
|
shapes.push({
|
||||||
|
id: key,
|
||||||
|
geometry: { type: 'point', position: geom.coordinates },
|
||||||
|
style: { pointRadius: 5 },
|
||||||
|
});
|
||||||
|
else
|
||||||
|
for (const path of geom.type === 'MultiLineString'
|
||||||
|
? geom.coordinates
|
||||||
|
: [geom.coordinates])
|
||||||
|
if (path.length > 1)
|
||||||
|
shapes.push({
|
||||||
|
id: key,
|
||||||
|
geometry: { type: 'line', positions: path },
|
||||||
|
style: { lineWidth: 2.5 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!rows.length) continue;
|
||||||
|
const bank = { rows, color };
|
||||||
|
banks.push(bank);
|
||||||
|
const point = {
|
||||||
|
sizePx: kind === 'cable' ? 7 : 8,
|
||||||
|
outlinePx: 1,
|
||||||
|
outlineColor: [0, 0, 0, 0.65],
|
||||||
|
};
|
||||||
|
const make = (role) =>
|
||||||
|
engine.movers.createSet({
|
||||||
|
id: id + '-' + kind + role,
|
||||||
|
kind: 'point',
|
||||||
|
capacity: rows.length,
|
||||||
|
depthTest: 'normal',
|
||||||
|
pickable: true,
|
||||||
|
point,
|
||||||
|
});
|
||||||
|
bank.points = make('-tip');
|
||||||
|
bank.stems = createStemBatch(
|
||||||
|
native,
|
||||||
|
css,
|
||||||
|
rows.length,
|
||||||
|
kind === 'cable' ? 2 : 2.4,
|
||||||
|
kind === 'cable' ? 0.58 : 0.68,
|
||||||
|
);
|
||||||
|
if (kind === 'landing-point') {
|
||||||
|
bank.surface = make('-base');
|
||||||
|
rows.forEach((row, i) =>
|
||||||
|
bank.surface.set(i, {
|
||||||
|
position: row.base,
|
||||||
|
show: true,
|
||||||
|
pickId: row.id,
|
||||||
|
color,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
bank.surface.setCount(rows.length);
|
||||||
|
bank.surface.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
source = view.addSource({
|
||||||
|
type: 'geojson',
|
||||||
|
data: { type: 'FeatureCollection', features: cables },
|
||||||
|
});
|
||||||
|
layer = view.addLayer({
|
||||||
|
type: 'vector',
|
||||||
|
source,
|
||||||
|
polyline: {
|
||||||
|
clampToGround: true,
|
||||||
|
width: 2.5,
|
||||||
|
lit: false,
|
||||||
|
color: 0xffffff,
|
||||||
|
opacity: 0.92,
|
||||||
|
transparent: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
layer.on('featureUpdated', ({ evaluator }) =>
|
||||||
|
evaluator.evaluate(({ properties }) => {
|
||||||
|
const css = properties?.color ?? BASE_CABLE_COLOR;
|
||||||
|
if (!colors.has(css)) colors.set(css, new Color(css));
|
||||||
|
return { color: colors.get(css) };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
layer.forceUpdate();
|
||||||
|
removers.push(bindGroundShapePicks(engine, id, shapes));
|
||||||
|
removers.push(
|
||||||
|
engine.picker.registerMeshPicker((at) => {
|
||||||
|
const hits = [];
|
||||||
|
for (const r of records.values()) {
|
||||||
|
if (!r.visible) continue;
|
||||||
|
const a = engine.projector.worldToWindow(r.base, {}),
|
||||||
|
b = engine.projector.worldToWindow(r.tip, {});
|
||||||
|
if (a && b && distanceToSegment(at, a, b) <= 2)
|
||||||
|
hits.push({
|
||||||
|
onTop: 0,
|
||||||
|
order: 0,
|
||||||
|
depth: r.distanceM,
|
||||||
|
result: { id: r.id, owner: id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return hits;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
removers.push(
|
||||||
|
engine.input.on(
|
||||||
|
'click',
|
||||||
|
(e) => {
|
||||||
|
if (!isPointerFree()) return;
|
||||||
|
const row = records.get(e.picked?.id);
|
||||||
|
if (!row) return;
|
||||||
|
engine.camera.flyTo({
|
||||||
|
destination: geo.cartesian3.fromDegrees(
|
||||||
|
row.reference.lon,
|
||||||
|
row.reference.lat,
|
||||||
|
6500,
|
||||||
|
),
|
||||||
|
orientation: {
|
||||||
|
heading: engine.camera.pose().heading,
|
||||||
|
pitch: geo.math.toRadians(-52),
|
||||||
|
roll: 0,
|
||||||
|
},
|
||||||
|
duration: 1.35,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ pick: true },
|
||||||
|
),
|
||||||
|
engine.camera.moveEnd.addEventListener(() => {
|
||||||
|
gate.markDirty();
|
||||||
|
wake();
|
||||||
|
}),
|
||||||
|
engine.frames.preRender.addEventListener(sweep),
|
||||||
|
);
|
||||||
|
dataset = next;
|
||||||
|
publisher.show();
|
||||||
|
gate.markDirty();
|
||||||
|
sweep();
|
||||||
|
wake();
|
||||||
|
} catch (e) {
|
||||||
|
hide();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { show, hide, destroy: hide, stats: () => ({ referenceLabelCount }) };
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/** The dataset is acquired once per activation, independently of scene attachment. */
|
||||||
|
export function createCableSessionLayer({
|
||||||
|
source,
|
||||||
|
createCesium,
|
||||||
|
createNavara,
|
||||||
|
}) {
|
||||||
|
const id = 'telegeography-submarine-cables';
|
||||||
|
let data,
|
||||||
|
request,
|
||||||
|
controller,
|
||||||
|
display,
|
||||||
|
enabled = false,
|
||||||
|
disposed = false,
|
||||||
|
updated = null,
|
||||||
|
error = null;
|
||||||
|
function read() {
|
||||||
|
if (data) return Promise.resolve(data);
|
||||||
|
if (!request) {
|
||||||
|
const token = new AbortController();
|
||||||
|
controller = token;
|
||||||
|
request = source
|
||||||
|
.fetch(token.signal)
|
||||||
|
.then((result) => {
|
||||||
|
token.signal.throwIfAborted();
|
||||||
|
if (
|
||||||
|
!Array.isArray(result?.cables?.features) ||
|
||||||
|
!Array.isArray(result?.landingPoints?.features)
|
||||||
|
)
|
||||||
|
throw Error('Invalid submarine cable dataset');
|
||||||
|
updated = Date.now();
|
||||||
|
return (data = result);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (controller === token) {
|
||||||
|
controller = request = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
let rendering = null;
|
||||||
|
function show() {
|
||||||
|
if (rendering) return rendering;
|
||||||
|
const owner = display;
|
||||||
|
if (!owner) return;
|
||||||
|
const result = (async () => {
|
||||||
|
try {
|
||||||
|
const next = await read();
|
||||||
|
if (enabled && display === owner && !disposed) {
|
||||||
|
error = null;
|
||||||
|
await owner.show(next);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (
|
||||||
|
!disposed &&
|
||||||
|
enabled &&
|
||||||
|
owner === display &&
|
||||||
|
e?.name !== 'AbortError'
|
||||||
|
)
|
||||||
|
error = e.message;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
rendering = result;
|
||||||
|
return result.finally(() => {
|
||||||
|
if (rendering === result) rendering = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: 'Submarine Cables',
|
||||||
|
icon: '≋',
|
||||||
|
source: source.label,
|
||||||
|
updateInterval: 0,
|
||||||
|
statsRefreshInterval: 500,
|
||||||
|
init(viewer, ctx) {
|
||||||
|
return this.attachRenderer(viewer, ctx);
|
||||||
|
},
|
||||||
|
attachRenderer(viewer, { engine } = {}) {
|
||||||
|
if (viewer) {
|
||||||
|
const legacy = createCesium({ label: source.label, fetch: read });
|
||||||
|
legacy.init(viewer);
|
||||||
|
let shown = false;
|
||||||
|
display = {
|
||||||
|
async show() {
|
||||||
|
if (!shown) {
|
||||||
|
await legacy.enable(viewer);
|
||||||
|
shown = !legacy.getStats().error;
|
||||||
|
} else if (!legacy.getStats().count) await legacy.update(viewer);
|
||||||
|
},
|
||||||
|
hide() {
|
||||||
|
shown = false;
|
||||||
|
legacy.disable();
|
||||||
|
},
|
||||||
|
destroy: () => legacy.destroy(viewer),
|
||||||
|
stats: () => legacy.getStats(),
|
||||||
|
};
|
||||||
|
} else display = createNavara(engine);
|
||||||
|
if (enabled) return show();
|
||||||
|
},
|
||||||
|
detachRenderer() {
|
||||||
|
const owner = display;
|
||||||
|
display = rendering = null;
|
||||||
|
owner?.destroy();
|
||||||
|
},
|
||||||
|
enable() {
|
||||||
|
enabled = true;
|
||||||
|
return show();
|
||||||
|
},
|
||||||
|
update() {
|
||||||
|
if (enabled) return show();
|
||||||
|
},
|
||||||
|
disable() {
|
||||||
|
enabled = false;
|
||||||
|
controller?.abort();
|
||||||
|
controller = request = rendering = null;
|
||||||
|
error = null;
|
||||||
|
display?.hide();
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
this.disable();
|
||||||
|
this.detachRenderer();
|
||||||
|
disposed = true;
|
||||||
|
data = null;
|
||||||
|
},
|
||||||
|
getStats() {
|
||||||
|
const s = display?.stats?.() ?? {};
|
||||||
|
return {
|
||||||
|
...s,
|
||||||
|
count:
|
||||||
|
enabled && data
|
||||||
|
? data.cables.features.length + data.landingPoints.features.length
|
||||||
|
: 0,
|
||||||
|
loading: Boolean(request) || Boolean(s.loading),
|
||||||
|
lastUpdate: updated,
|
||||||
|
error: error ?? s.error ?? null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createCableSessionLayer } from './session.js';
|
||||||
|
|
||||||
|
test('cables arriving during handoff belong to the incoming display', async () => {
|
||||||
|
let resolve,
|
||||||
|
reads = 0;
|
||||||
|
const shown = [];
|
||||||
|
const layer = createCableSessionLayer({
|
||||||
|
source: {
|
||||||
|
label: 'fixture',
|
||||||
|
fetch: () => {
|
||||||
|
reads++;
|
||||||
|
return new Promise((done) => {
|
||||||
|
resolve = done;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createNavara: (engine) => ({
|
||||||
|
show: (dataset) => shown.push([engine, dataset]),
|
||||||
|
destroy() {},
|
||||||
|
hide() {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
layer.init(null, { engine: 'outgoing' });
|
||||||
|
const pending = layer.enable();
|
||||||
|
layer.detachRenderer();
|
||||||
|
const handoff = layer.attachRenderer(null, { engine: 'incoming' });
|
||||||
|
const dataset = { cables: { features: [] }, landingPoints: { features: [] } };
|
||||||
|
resolve(dataset);
|
||||||
|
await Promise.all([pending, handoff]);
|
||||||
|
assert.deepEqual(shown, [['incoming', dataset]]);
|
||||||
|
assert.equal(reads, 1);
|
||||||
|
layer.destroy();
|
||||||
|
});
|
||||||
Reference in new issue
Block a user