Retain the TeleGeography dataset and render cables and landings on Navara

This commit is contained in:
German Khokhlov committed 2026-11-03 03:38:15 +04:00
1 parent 2b4955c817
commit 5cea3e8cbb
13 files changed
+593 -12

No files matched your search

+5 -1
View File
@@ -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();
}
+1 -1
View File
@@ -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',
+11 -2
View File
@@ -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 }),
});
}
+1
View File
@@ -69,6 +69,7 @@ test('the Navara mode registers migrated renderers once', async () => {
'weather-cyclones',
'local-datacenters',
'local-dams',
'telegeography-submarine-cables',
'local-firms',
],
);
+5
View File
@@ -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));
+2 -2
View File
@@ -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),
@@ -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,
+1 -1
View File
@@ -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() {
+6 -1
View File
@@ -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];
+268
View File
@@ -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 }) };
}
+139
View File
@@ -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();
});