mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 19:34:01 +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": [
|
||||
"src/layers/submarineCables/geometry.js",
|
||||
"src/layers/submarineCables/index.js",
|
||||
"src/layers/submarineCables/session.js",
|
||||
"src/layers/submarineCables/ingestion.js",
|
||||
"src/layers/submarineCables/interaction.js",
|
||||
"src/layers/submarineCables/lifecycle.js",
|
||||
@@ -2960,6 +2961,7 @@
|
||||
"src/layers/submarineCables/bundledSource.js",
|
||||
"src/layers/submarineCables/geometry.js",
|
||||
"src/layers/submarineCables/index.js",
|
||||
"src/layers/submarineCables/session.js",
|
||||
"src/layers/submarineCables/ingestion.js",
|
||||
"src/layers/submarineCables/interaction.js",
|
||||
"src/layers/submarineCables/lifecycle.js",
|
||||
@@ -3674,7 +3676,8 @@
|
||||
"src/layers/installations/render/navara.js",
|
||||
"src/layers/awareness/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/navaraSurface.js",
|
||||
"src/layers/wind/render/navara.js",
|
||||
@@ -4033,6 +4036,7 @@
|
||||
"src/layers/satellites/tracking.js",
|
||||
"src/layers/submarineCables/geometry.js",
|
||||
"src/layers/submarineCables/index.js",
|
||||
"src/layers/submarineCables/session.js",
|
||||
"src/layers/submarineCables/ingestion.js",
|
||||
"src/layers/submarineCables/interaction.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',
|
||||
() => createApplicationCables({ source: sources.cables }),
|
||||
() => sessionModel('telegeography-submarine-cables', () => createApplicationCables({ source: sources.cables, session: true })),
|
||||
],
|
||||
[
|
||||
'local-firms',
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { createSubmarineCableLayer } from '../../layers/submarineCables/index.js';
|
||||
import { createCableSessionLayer } from '../../layers/submarineCables/session.js';
|
||||
import { rendererFor } from './renderers.js';
|
||||
import { overlayHost } from './overlayHost.js';
|
||||
/** Wire cable source geometry to the application overlay host. */
|
||||
export function createApplicationCables(options) {
|
||||
return createSubmarineCableLayer({
|
||||
export function createApplicationCables({ session = false, ...options }) {
|
||||
const createCesium = (source) => createSubmarineCableLayer({
|
||||
overlayHost,
|
||||
screenSpaceEventHandlerFactory: (canvas) =>
|
||||
new Cesium.ScreenSpaceEventHandler(canvas),
|
||||
mapStackEventTarget: typeof window !== 'undefined' ? window : null,
|
||||
...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',
|
||||
'local-datacenters',
|
||||
'local-dams',
|
||||
'telegeography-submarine-cables',
|
||||
'local-firms',
|
||||
],
|
||||
);
|
||||
|
||||
@@ -59,6 +59,7 @@ const NAVARA_CREDIT_KEYS = new Set([
|
||||
'eccc-aqhi',
|
||||
'wfigs',
|
||||
's2-ghostmaps',
|
||||
'telegeography',
|
||||
'wind-gfs',
|
||||
'ecmwf-ifs',
|
||||
'weather-noaa',
|
||||
@@ -119,6 +120,10 @@ export async function registerNavaraLayerRenderers() {
|
||||
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 () => {
|
||||
const { createWindNavaraRenderer } = await import('../layers/wind/render/navara.js');
|
||||
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 { rgbaFromCss } from '../../shared/color.js';
|
||||
import { isPointerFree } from '../../../data/inputOwnership.js';
|
||||
import { createStemBatch } from './navaraStems.js';
|
||||
import { createStemBatch } from '../../shared/render/navaraStems.js';
|
||||
|
||||
export function createInfrastructureNavaraRenderer(
|
||||
engine,
|
||||
@@ -263,7 +263,7 @@ export function createInfrastructureNavaraRenderer(
|
||||
depthTest: 'always',
|
||||
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) {
|
||||
const carto = geo.cartographic.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 { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js';
|
||||
import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
|
||||
import { INFRA_LOD_ACTIVE_MAX } from '../../../data/localGeojsonLod.js';
|
||||
|
||||
const KEY = 'gevInfrastructureStems';
|
||||
const views = new WeakSet();
|
||||
/** 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)) {
|
||||
view.registerMesh(
|
||||
KEY,
|
||||
@@ -29,12 +28,13 @@ export function createStemBatch({ view, MeshDesc }, color) {
|
||||
);
|
||||
views.add(view);
|
||||
}
|
||||
const xyz = new Float32Array(6 * INFRA_LOD_ACTIVE_MAX);
|
||||
const xyz = new Float32Array(6 * capacity);
|
||||
const geometry = new LineSegmentsGeometry().setPositions(xyz);
|
||||
geometry.instanceCount = 0;
|
||||
const material = new LineMaterial({
|
||||
color,
|
||||
linewidth: 3.5,
|
||||
linewidth: width,
|
||||
opacity,
|
||||
depthWrite: false,
|
||||
transparent: true,
|
||||
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
|
||||
// camera is parked and nothing else is rendering.
|
||||
(viewer || state._viewer)?.scene?.requestRender?.();
|
||||
void parts.ingestion.load(viewer || state._viewer);
|
||||
return parts.ingestion.load(viewer || state._viewer);
|
||||
},
|
||||
|
||||
disable() {
|
||||
|
||||
@@ -260,7 +260,7 @@ export function createCableReferenceSweepGate({
|
||||
* @returns {boolean} True when the stem geometry was redefined.
|
||||
*/
|
||||
|
||||
export function updateCableReferenceStem(
|
||||
export function updateCableReferenceTip(
|
||||
record,
|
||||
cameraPositionWC,
|
||||
canvasHeight,
|
||||
@@ -289,6 +289,11 @@ export function updateCableReferenceStem(
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
const stemPositions =
|
||||
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