mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
Draw local infrastructure on either globe from one retained dataset
This commit is contained in:
1 parent
868d061856
commit
c1d829ef51
12 files changed
+900
-6
No files matched your search
@@ -2715,6 +2715,7 @@
|
|||||||
"src/data/groundSnap.js",
|
"src/data/groundSnap.js",
|
||||||
"src/data/iconOrientation.js",
|
"src/data/iconOrientation.js",
|
||||||
"src/data/infrastructure.js",
|
"src/data/infrastructure.js",
|
||||||
|
"src/layers/infrastructure/index.js",
|
||||||
"src/data/installationFeedback.js",
|
"src/data/installationFeedback.js",
|
||||||
"src/data/labelArbiter.js",
|
"src/data/labelArbiter.js",
|
||||||
"src/data/idSwitches.js",
|
"src/data/idSwitches.js",
|
||||||
@@ -3672,6 +3673,8 @@
|
|||||||
"src/layers/earthquakes/render/navaraGrid.js",
|
"src/layers/earthquakes/render/navaraGrid.js",
|
||||||
"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/navaraStems.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/ghostMaps/render/navara.js",
|
"src/layers/ghostMaps/render/navara.js",
|
||||||
@@ -3811,6 +3814,7 @@
|
|||||||
"src/data/geoid.js",
|
"src/data/geoid.js",
|
||||||
"src/data/iconOrientation.js",
|
"src/data/iconOrientation.js",
|
||||||
"src/data/infrastructure.js",
|
"src/data/infrastructure.js",
|
||||||
|
"src/layers/infrastructure/index.js",
|
||||||
"src/data/installationFeedback.js",
|
"src/data/installationFeedback.js",
|
||||||
"src/data/satellitePass.js",
|
"src/data/satellitePass.js",
|
||||||
"src/data/labelArbiter.js",
|
"src/data/labelArbiter.js",
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
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=41.5340&lon=-90.5070&alt=20000&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 = { datacenters: 0, dams: 0 };
|
||||||
|
p.on('request', (req) => {
|
||||||
|
const path = new URL(req.url()).pathname;
|
||||||
|
for (const name of Object.keys(reads))
|
||||||
|
if (path.includes(name) && path.endsWith('.geojsonl')) reads[name]++;
|
||||||
|
});
|
||||||
|
await p.evaluateOnNewDocument(() => {
|
||||||
|
window.qaSelections = [];
|
||||||
|
window.addEventListener('gev:entity-selected', ({ detail }) => {
|
||||||
|
if (detail?.layerId?.startsWith('local-'))
|
||||||
|
window.qaSelections.push(detail.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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');
|
||||||
|
const ids = ['local-datacenters', 'local-dams'];
|
||||||
|
const dataset = await p.evaluate(async (ids) => {
|
||||||
|
const { dataManager, tileset } = window.__godsEyeView;
|
||||||
|
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
|
||||||
|
throw Error('Keyless preview required');
|
||||||
|
window.qaSites = [];
|
||||||
|
const records = [];
|
||||||
|
for (const id of ids) {
|
||||||
|
if (!(await dataManager.setEnabled(id, true, { origin: 'user' })))
|
||||||
|
throw Error(id + ' did not enable');
|
||||||
|
const mod = dataManager.layers.get(id).module;
|
||||||
|
window.qaSites.push(mod);
|
||||||
|
const all = mod.getAnalystRecords(Infinity);
|
||||||
|
records.push({
|
||||||
|
count: mod.getStats().count,
|
||||||
|
total: all.length,
|
||||||
|
name: all[0]?.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return records;
|
||||||
|
}, ids);
|
||||||
|
assert.ok(
|
||||||
|
dataset[0].count >= 4351 && dataset[1].count >= 704,
|
||||||
|
JSON.stringify(dataset),
|
||||||
|
);
|
||||||
|
const record = await p.evaluate(() =>
|
||||||
|
window.qaSites[0]
|
||||||
|
.getAnalystRecords(5000)
|
||||||
|
.find((r) => r.name === 'Geneseo Communications Inc'),
|
||||||
|
);
|
||||||
|
assert.ok(record);
|
||||||
|
const clickSite = async () => {
|
||||||
|
const screen = await p.evaluate(
|
||||||
|
(world) => {
|
||||||
|
const { engine } = window.__godsEyeView;
|
||||||
|
const at = engine.projector.worldToWindow(world, {});
|
||||||
|
const box = engine.host.canvas.getBoundingClientRect();
|
||||||
|
return { x: box.left + at.x, y: box.top + at.y };
|
||||||
|
},
|
||||||
|
fromDegrees(record.lon, record.lat),
|
||||||
|
);
|
||||||
|
await p.mouse.click(screen.x, screen.y);
|
||||||
|
await p.waitForFunction(
|
||||||
|
() => window.qaSelections.at(-1) === 'local-datacenters:342664739',
|
||||||
|
{ timeout: 15000 },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const counts = { ...reads };
|
||||||
|
for (const kind of ['navara', 'cesium', 'navara']) {
|
||||||
|
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||||
|
await p.evaluate(() => {
|
||||||
|
window.qaSelections = [];
|
||||||
|
});
|
||||||
|
await p.select('#globe-engine', kind);
|
||||||
|
await ready(kind);
|
||||||
|
await p.waitForFunction(
|
||||||
|
() => window.qaSelections.includes('local-datacenters:342664739'),
|
||||||
|
{ timeout: 15000 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, 1500));
|
||||||
|
const result = await p.evaluate(
|
||||||
|
(ids) =>
|
||||||
|
ids.map((id, i) => {
|
||||||
|
const mod = window.__godsEyeView.dataManager.layers.get(id).module;
|
||||||
|
return {
|
||||||
|
same: mod === window.qaSites[i],
|
||||||
|
lod: mod.getLodDiagnostics(),
|
||||||
|
count: mod.getStats().count,
|
||||||
|
error: mod.getStats().error,
|
||||||
|
name: mod.getAnalystRecords(1)[0].name,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
ids,
|
||||||
|
);
|
||||||
|
console.log(kind, JSON.stringify(result));
|
||||||
|
for (const [i, layer] of result.entries()) {
|
||||||
|
assert.ok(
|
||||||
|
layer.same &&
|
||||||
|
!layer.error &&
|
||||||
|
layer.lod.active > 0 &&
|
||||||
|
layer.lod.active <= layer.lod.budgetLimit,
|
||||||
|
);
|
||||||
|
assert.equal(layer.count, dataset[i].count);
|
||||||
|
assert.equal(layer.name, dataset[i].name);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
kind === 'navara' &&
|
||||||
|
!(await p.evaluate(() => window.qaSelections.length))
|
||||||
|
)
|
||||||
|
await clickSite();
|
||||||
|
assert.deepEqual(
|
||||||
|
reads,
|
||||||
|
counts,
|
||||||
|
'Do not reload bundled data on scene handoff',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert.equal(counts.datacenters, 1);
|
||||||
|
assert.equal(counts.dams, 1);
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log(
|
||||||
|
'PASS complete bundled datasets, bounded stems, retained selection and one read per file',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await b.close();
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ import { createApplicationGnssIntegrity } from './layers/gnss.js';
|
|||||||
import { createApplicationGhostMaps } from './layers/ghostMaps.js';
|
import { createApplicationGhostMaps } from './layers/ghostMaps.js';
|
||||||
import { createApplicationCables } from './layers/submarineCables.js';
|
import { createApplicationCables } from './layers/submarineCables.js';
|
||||||
import { createApplicationStreetLevel } from './layers/streetLevel.js';
|
import { createApplicationStreetLevel } from './layers/streetLevel.js';
|
||||||
|
import { createInfrastructureSessionLayer } from '../layers/infrastructure/index.js';
|
||||||
import { createInfrastructureLayers } from '../data/infrastructure.js';
|
import { createInfrastructureLayers } from '../data/infrastructure.js';
|
||||||
import { localGeoJsonServices } from './localGeojsonServices.js';
|
import { localGeoJsonServices } from './localGeojsonServices.js';
|
||||||
import { createBhoteKoshiEventLayer } from '../data/bhoteKoshiEvent.js';
|
import { createBhoteKoshiEventLayer } from '../data/bhoteKoshiEvent.js';
|
||||||
@@ -302,7 +303,11 @@ export function createApplicationCatalog({
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
['local-datacenters', 'local-dams'],
|
['local-datacenters', 'local-dams'],
|
||||||
() => createInfrastructureLayers(localGeoJsonServices),
|
() => sessionModel('local-infrastructure', () =>
|
||||||
|
createInfrastructureLayers(localGeoJsonServices, (config, services) =>
|
||||||
|
createInfrastructureSessionLayer(config, services, (engine, descriptor, ops) =>
|
||||||
|
rendererFor(config.id,engine,{config:descriptor,services:ops}))),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'telegeography-submarine-cables',
|
'telegeography-submarine-cables',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
registerEntityContext,
|
registerEntityContext,
|
||||||
removeEntityContextsForLayer,
|
removeEntityContextsForLayer,
|
||||||
selectEntityContext,
|
selectEntityContext,
|
||||||
|
getSelectedEntityContext,
|
||||||
} from '../data/contextStore.js';
|
} from '../data/contextStore.js';
|
||||||
import {
|
import {
|
||||||
clearOverlaySource,
|
clearOverlaySource,
|
||||||
@@ -22,6 +23,7 @@ export const localGeoJsonServices = Object.freeze({
|
|||||||
registerEntityContext,
|
registerEntityContext,
|
||||||
removeEntityContextsForLayer,
|
removeEntityContextsForLayer,
|
||||||
selectEntityContext,
|
selectEntityContext,
|
||||||
|
getSelectedEntityContext,
|
||||||
governorRequestRender,
|
governorRequestRender,
|
||||||
showOsmCredit,
|
showOsmCredit,
|
||||||
hideOsmCredit,
|
hideOsmCredit,
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ test('the Navara mode registers migrated renderers once', async () => {
|
|||||||
'weather-satellite',
|
'weather-satellite',
|
||||||
'weather-lightning',
|
'weather-lightning',
|
||||||
'weather-cyclones',
|
'weather-cyclones',
|
||||||
|
'local-datacenters',
|
||||||
|
'local-dams',
|
||||||
'local-firms',
|
'local-firms',
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ const NAVARA_CREDIT_KEYS = new Set([
|
|||||||
'weather-noaa',
|
'weather-noaa',
|
||||||
'weather-lightning',
|
'weather-lightning',
|
||||||
'weather-cyclones',
|
'weather-cyclones',
|
||||||
|
'dams',
|
||||||
'firms',
|
'firms',
|
||||||
'gbfs',
|
'gbfs',
|
||||||
'overture-military-names',
|
'overture-military-names',
|
||||||
@@ -120,6 +121,12 @@ export async function registerNavaraLayerRenderers() {
|
|||||||
const {createCyclonesNavaraRenderer} = await import('../layers/cyclones/render/navara.js');
|
const {createCyclonesNavaraRenderer} = await import('../layers/cyclones/render/navara.js');
|
||||||
return (engine,options) => createCyclonesNavaraRenderer(engine,navaraViews.get(engine).view,options);
|
return (engine,options) => createCyclonesNavaraRenderer(engine,navaraViews.get(engine).view,options);
|
||||||
},
|
},
|
||||||
|
...Object.fromEntries(['local-datacenters','local-dams'].map((id) => [
|
||||||
|
id, async () => {
|
||||||
|
const {createInfrastructureNavaraRenderer} = await import('../layers/infrastructure/render/navara.js');
|
||||||
|
return (engine,{config,services}) => createInfrastructureNavaraRenderer(engine,navaraViews.get(engine),config,services);
|
||||||
|
},
|
||||||
|
])),
|
||||||
's2-ghostmaps': async () => {
|
's2-ghostmaps': async () => {
|
||||||
const { createGhostMapsNavaraRenderer } = await import('../layers/ghostMaps/render/navara.js');
|
const { createGhostMapsNavaraRenderer } = await import('../layers/ghostMaps/render/navara.js');
|
||||||
return (engine) => createGhostMapsNavaraRenderer(engine, navaraViews.get(engine).view);
|
return (engine) => createGhostMapsNavaraRenderer(engine, navaraViews.get(engine).view);
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import { INFRASTRUCTURE_DATA_URLS } from '../sources/infrastructureData.js';
|
|||||||
* @param {object} services Caller-owned context, overlay and render operations.
|
* @param {object} services Caller-owned context, overlay and render operations.
|
||||||
* @returns {object[]} Datacenters then dams, with stable standalone identities.
|
* @returns {object[]} Datacenters then dams, with stable standalone identities.
|
||||||
*/
|
*/
|
||||||
export function createInfrastructureLayers(services) {
|
export function createInfrastructureLayers(services, createLayer = createLocalGeoJsonLayer) {
|
||||||
const datacentersUrl = INFRASTRUCTURE_DATA_URLS['local-datacenters'];
|
const datacentersUrl = INFRASTRUCTURE_DATA_URLS['local-datacenters'];
|
||||||
const damsUrl = INFRASTRUCTURE_DATA_URLS['local-dams'];
|
const damsUrl = INFRASTRUCTURE_DATA_URLS['local-dams'];
|
||||||
const datacenters = createLocalGeoJsonLayer(
|
const datacenters = createLayer(
|
||||||
{
|
{
|
||||||
id: 'local-datacenters',
|
id: 'local-datacenters',
|
||||||
url: datacentersUrl,
|
url: datacentersUrl,
|
||||||
@@ -25,7 +25,7 @@ export function createInfrastructureLayers(services) {
|
|||||||
services,
|
services,
|
||||||
);
|
);
|
||||||
|
|
||||||
const dams = createLocalGeoJsonLayer(
|
const dams = createLayer(
|
||||||
{
|
{
|
||||||
id: 'local-dams',
|
id: 'local-dams',
|
||||||
url: damsUrl,
|
url: damsUrl,
|
||||||
|
|||||||
@@ -307,6 +307,7 @@ export function createLocalGeoJsonLayer(
|
|||||||
icon = '📍',
|
icon = '📍',
|
||||||
source = 'Local JSONL',
|
source = 'Local JSONL',
|
||||||
osmDerived = false,
|
osmDerived = false,
|
||||||
|
readFeatures = null,
|
||||||
labels = true,
|
labels = true,
|
||||||
labelMax = DEFAULT_LABEL_MAX,
|
labelMax = DEFAULT_LABEL_MAX,
|
||||||
labelGridPx = DEFAULT_LABEL_GRID_PX,
|
labelGridPx = DEFAULT_LABEL_GRID_PX,
|
||||||
@@ -584,7 +585,8 @@ export function createLocalGeoJsonLayer(
|
|||||||
// windows (before vs after the add settles) need different cleanup.
|
// windows (before vs after the add settles) need different cleanup.
|
||||||
let addedToScene = false;
|
let addedToScene = false;
|
||||||
try {
|
try {
|
||||||
let features = _cachedFeatures;
|
let features = _cachedFeatures ?? (readFeatures ? await readFeatures() : null);
|
||||||
|
if (_destroyed) return;
|
||||||
if (!features) {
|
if (!features) {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
signal: _loadController.signal,
|
signal: _loadController.signal,
|
||||||
@@ -1062,6 +1064,14 @@ export function createLocalGeoJsonLayer(
|
|||||||
viewer.scene.requestRender?.();
|
viewer.scene.requestRender?.();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
restoreFeatureSelection(viewer, featureId) {
|
||||||
|
const entity = _dataSource?.entities.values.find((item) => String(item.id) === featureId);
|
||||||
|
if (!entity) return false;
|
||||||
|
viewer.selectedEntity = entity;
|
||||||
|
selectEntityContext(entity);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
disable: disableLayer,
|
disable: disableLayer,
|
||||||
|
|
||||||
destroy: (viewer) => {
|
destroy: (viewer) => {
|
||||||
@@ -1234,7 +1244,7 @@ function featureLabelFromProperties(props, layerId) {
|
|||||||
return clampLabel(text || layerTitle(layerId));
|
return clampLabel(text || layerTitle(layerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
function labelPriorityFromProperties(props, layerId) {
|
export function labelPriorityFromProperties(props, layerId) {
|
||||||
const tags = props.tags || {};
|
const tags = props.tags || {};
|
||||||
|
|
||||||
let score = 0;
|
let score = 0;
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import * as geo from '../../geo/index.js';
|
||||||
|
import {
|
||||||
|
mapAnalystRecord,
|
||||||
|
parseGeojsonLines,
|
||||||
|
} from '../../sources/infrastructureData.js';
|
||||||
|
import {
|
||||||
|
createLocalGeoJsonLayer,
|
||||||
|
localDatasetError,
|
||||||
|
} from '../../data/localGeojsonCore.js';
|
||||||
|
|
||||||
|
// Split multipart features exactly as the Cesium entity collection does, with stable IDs on both globes.
|
||||||
|
function dataset(raw) {
|
||||||
|
const features = [],
|
||||||
|
records = [],
|
||||||
|
used = new Set();
|
||||||
|
function part(feature, geometry, key) {
|
||||||
|
if (!geometry) return;
|
||||||
|
if (geometry.type === 'GeometryCollection') {
|
||||||
|
for (const g of geometry.geometries) part(feature, g, key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (geometry.type.startsWith('Multi')) {
|
||||||
|
for (const coordinates of geometry.coordinates)
|
||||||
|
part(feature, { type: geometry.type.slice(5), coordinates }, key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let id = key,
|
||||||
|
suffix = 2;
|
||||||
|
while (used.has(id)) id = key + '_' + suffix++;
|
||||||
|
used.add(id);
|
||||||
|
features.push({ ...feature, id, geometry });
|
||||||
|
let base;
|
||||||
|
if (geometry.type === 'Point')
|
||||||
|
base = geo.cartesian3.fromDegrees(...geometry.coordinates);
|
||||||
|
if (geometry.type === 'Polygon') {
|
||||||
|
const ring = geometry.coordinates[0];
|
||||||
|
if (ring.length)
|
||||||
|
base = geo.boundingSphere.fromPoints(
|
||||||
|
ring.map(([lon, lat, h]) => geo.cartesian3.fromDegrees(lon, lat, h)),
|
||||||
|
).center;
|
||||||
|
}
|
||||||
|
if (!base) return;
|
||||||
|
const pos = geo.cartographic.fromCartesian(base);
|
||||||
|
if (pos)
|
||||||
|
records.push({
|
||||||
|
id,
|
||||||
|
lon: geo.math.toDegrees(pos.longitude),
|
||||||
|
lat: geo.math.toDegrees(pos.latitude),
|
||||||
|
properties: feature.properties ?? {},
|
||||||
|
geometry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
raw.forEach((f, index) =>
|
||||||
|
part(
|
||||||
|
f,
|
||||||
|
f.geometry,
|
||||||
|
String(f.id ?? f.properties?.osm_id ?? 'feature-' + index),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return { features, records };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The parsed local file and the selected feature outlive any one scene. */
|
||||||
|
export function createInfrastructureSessionLayer(
|
||||||
|
config,
|
||||||
|
services,
|
||||||
|
nativeFactory,
|
||||||
|
) {
|
||||||
|
const { id, name, icon, source, url } = config;
|
||||||
|
let data = null,
|
||||||
|
request = null,
|
||||||
|
abort = null,
|
||||||
|
display = null,
|
||||||
|
enabled = false,
|
||||||
|
destroyed = false;
|
||||||
|
let updated = null,
|
||||||
|
error = null,
|
||||||
|
selectedId = null;
|
||||||
|
const read = () => {
|
||||||
|
if (data) return Promise.resolve(data);
|
||||||
|
if (!request) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
abort = controller;
|
||||||
|
request = (async () => {
|
||||||
|
const res = await fetch(url, { signal: controller.signal });
|
||||||
|
if (!res.ok) throw Error('HTTP ' + res.status);
|
||||||
|
const raw = parseGeojsonLines(await res.text());
|
||||||
|
controller.signal.throwIfAborted();
|
||||||
|
data = dataset(raw);
|
||||||
|
updated = Date.now();
|
||||||
|
error = null;
|
||||||
|
return data;
|
||||||
|
})().finally(() => {
|
||||||
|
if (controller === abort) {
|
||||||
|
request = null;
|
||||||
|
abort = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
};
|
||||||
|
const show = async () => {
|
||||||
|
try {
|
||||||
|
const next = await read();
|
||||||
|
if (!enabled || destroyed) return;
|
||||||
|
const active = display;
|
||||||
|
if (active) {
|
||||||
|
error = null;
|
||||||
|
await active.show(next);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
active &&
|
||||||
|
active === display &&
|
||||||
|
enabled &&
|
||||||
|
selectedId &&
|
||||||
|
!services.getSelectedEntityContext?.()
|
||||||
|
)
|
||||||
|
active.select(selectedId);
|
||||||
|
} catch (cause) {
|
||||||
|
if (!destroyed && cause?.name !== 'AbortError')
|
||||||
|
error = localDatasetError(cause);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
icon,
|
||||||
|
source,
|
||||||
|
updateInterval: 0,
|
||||||
|
statsRefreshInterval: 1000,
|
||||||
|
init(viewer, options = {}) {
|
||||||
|
if (viewer || options.engine) return this.attachRenderer(viewer, options);
|
||||||
|
},
|
||||||
|
attachRenderer(viewer, { engine = null } = {}) {
|
||||||
|
if (display) throw Error('Infrastructure display already attached');
|
||||||
|
if (viewer) {
|
||||||
|
const legacy = createLocalGeoJsonLayer(
|
||||||
|
{ ...config, readFeatures: async () => (await read()).features },
|
||||||
|
services,
|
||||||
|
);
|
||||||
|
display = {
|
||||||
|
show: async () => {
|
||||||
|
await legacy.enable(viewer);
|
||||||
|
error = legacy.getStats().error;
|
||||||
|
},
|
||||||
|
select: (key) => legacy.restoreFeatureSelection(viewer, key),
|
||||||
|
hide: () => legacy.disable(viewer),
|
||||||
|
destroy: () => legacy.destroy(viewer),
|
||||||
|
lod: legacy.getLodDiagnostics,
|
||||||
|
};
|
||||||
|
} else display = nativeFactory(engine, config, services);
|
||||||
|
if (enabled) return show();
|
||||||
|
},
|
||||||
|
detachRenderer() {
|
||||||
|
if (!display) return;
|
||||||
|
const selected = services.getSelectedEntityContext?.();
|
||||||
|
selectedId =
|
||||||
|
selected?.layerId === id
|
||||||
|
? String(selected.id).slice(id.length + 1)
|
||||||
|
: null;
|
||||||
|
display.destroy();
|
||||||
|
display = null;
|
||||||
|
},
|
||||||
|
enable(viewer) {
|
||||||
|
if (!display && viewer) this.attachRenderer(viewer);
|
||||||
|
enabled = true;
|
||||||
|
return show();
|
||||||
|
},
|
||||||
|
disable() {
|
||||||
|
enabled = false;
|
||||||
|
selectedId = null;
|
||||||
|
display?.hide();
|
||||||
|
},
|
||||||
|
update() {},
|
||||||
|
getStats: () => ({
|
||||||
|
count: data?.features.length ?? 0,
|
||||||
|
lastUpdate: updated,
|
||||||
|
error,
|
||||||
|
}),
|
||||||
|
getLodDiagnostics: () =>
|
||||||
|
display?.lod() ?? {
|
||||||
|
total: 0,
|
||||||
|
active: 0,
|
||||||
|
budgetLimit: 0,
|
||||||
|
computed: false,
|
||||||
|
},
|
||||||
|
getAnalystRecords(limit = 2000) {
|
||||||
|
if (!enabled || !data) return [];
|
||||||
|
return data.records
|
||||||
|
.slice(
|
||||||
|
0,
|
||||||
|
Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 2000,
|
||||||
|
)
|
||||||
|
.map((r) => mapAnalystRecord(r, id));
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
destroyed = true;
|
||||||
|
enabled = false;
|
||||||
|
abort?.abort();
|
||||||
|
display?.destroy();
|
||||||
|
display = data = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
import * as geo from '../../../geo/index.js';
|
||||||
|
import {
|
||||||
|
selectInfraLod,
|
||||||
|
applyInfraEvictionGrace,
|
||||||
|
INFRA_LOD_ACTIVE_MAX,
|
||||||
|
} from '../../../data/localGeojsonLod.js';
|
||||||
|
import {
|
||||||
|
createLocalInfrastructureOverlayEntry,
|
||||||
|
selectLocalInfrastructureOverlayCohort,
|
||||||
|
createLocalInfrastructureOverlayPublisher,
|
||||||
|
LOCAL_OVERLAY_COHORT_LIMIT,
|
||||||
|
labelPriorityFromProperties,
|
||||||
|
localInfrastructureOverlayCopy,
|
||||||
|
} from '../../../data/localGeojsonCore.js';
|
||||||
|
import { createNavaraPolygonDisplay } from '../../shared/render/navaraPolygons.js';
|
||||||
|
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';
|
||||||
|
|
||||||
|
export function createInfrastructureNavaraRenderer(
|
||||||
|
engine,
|
||||||
|
native,
|
||||||
|
config,
|
||||||
|
services,
|
||||||
|
) {
|
||||||
|
const { id, name, source, color, labelMax, labelGridPx } = config;
|
||||||
|
const rgba = rgbaFromCss(color).rgba();
|
||||||
|
const publisher = createLocalInfrastructureOverlayPublisher({
|
||||||
|
sourceId: id,
|
||||||
|
host: services.overlayHost,
|
||||||
|
});
|
||||||
|
const display = createNavaraPolygonDisplay(
|
||||||
|
engine,
|
||||||
|
native.view,
|
||||||
|
(tint, show) => ({
|
||||||
|
polyline: { color: tint, show, width: 2, clampToGround: true },
|
||||||
|
polygon: {
|
||||||
|
color: tint,
|
||||||
|
show,
|
||||||
|
opacity: 0.3,
|
||||||
|
transparent: true,
|
||||||
|
clampToGround: true,
|
||||||
|
outline: true,
|
||||||
|
outlineColor: tint,
|
||||||
|
outlineWidth: 2,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let dataset = null,
|
||||||
|
records = new Map(),
|
||||||
|
active = new Set(),
|
||||||
|
grace = new Map(),
|
||||||
|
shown = [],
|
||||||
|
budget = 0;
|
||||||
|
let lastWalk = -Infinity,
|
||||||
|
signature = '',
|
||||||
|
topology = '',
|
||||||
|
points = null,
|
||||||
|
stems = null,
|
||||||
|
unbindShapes = null;
|
||||||
|
let retryTimer = null;
|
||||||
|
const removers = [];
|
||||||
|
const wake = () => engine.render.request(id);
|
||||||
|
const key = (fid) => id + ':' + fid;
|
||||||
|
function footprints() {
|
||||||
|
const features = [],
|
||||||
|
shapes = [];
|
||||||
|
for (const feature of dataset.features) {
|
||||||
|
const g = feature.geometry;
|
||||||
|
if (
|
||||||
|
g.type !== 'LineString' &&
|
||||||
|
(g.type !== 'Polygon' || !active.has(feature.id))
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
features.push(feature);
|
||||||
|
shapes.push({
|
||||||
|
id: key(feature.id),
|
||||||
|
geometry:
|
||||||
|
g.type === 'LineString'
|
||||||
|
? { type: 'line', positions: g.coordinates }
|
||||||
|
: {
|
||||||
|
type: 'polygon',
|
||||||
|
outer: g.coordinates[0],
|
||||||
|
holes: g.coordinates.slice(1),
|
||||||
|
},
|
||||||
|
style: { lineWidth: 2 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
display.replace(features.length ? new Map([[color, features]]) : new Map());
|
||||||
|
unbindShapes?.();
|
||||||
|
unbindShapes = bindGroundShapePicks(engine, id, shapes);
|
||||||
|
}
|
||||||
|
function sweep(force = false) {
|
||||||
|
if (!dataset) return;
|
||||||
|
const now = performance.now();
|
||||||
|
if (!force && now - lastWalk < 450) return;
|
||||||
|
lastWalk = now;
|
||||||
|
const pose = engine.camera.pose(),
|
||||||
|
size = engine.host.size();
|
||||||
|
const sig = [
|
||||||
|
pose.position.x,
|
||||||
|
pose.position.y,
|
||||||
|
pose.position.z,
|
||||||
|
pose.heading,
|
||||||
|
pose.pitch,
|
||||||
|
pose.roll,
|
||||||
|
pose.fovy,
|
||||||
|
size.width,
|
||||||
|
size.height,
|
||||||
|
].join('/');
|
||||||
|
const sampling = shown.some(
|
||||||
|
(r) =>
|
||||||
|
!r.grounded &&
|
||||||
|
r.tries < 30 &&
|
||||||
|
now - r.lastSample >= 2000 &&
|
||||||
|
r.distance < 75000,
|
||||||
|
);
|
||||||
|
if (!force && signature === sig && !sampling) return;
|
||||||
|
if (force || signature !== sig) {
|
||||||
|
const candidates = [];
|
||||||
|
for (const r of records.values()) {
|
||||||
|
r.distance = geo.cartesian3.distance(pose.position, r.base);
|
||||||
|
candidates.push({
|
||||||
|
id: r.id,
|
||||||
|
priority: r.priority,
|
||||||
|
distanceM: r.distance,
|
||||||
|
inView: engine.projector.horizonVisible(r.base),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const result = selectInfraLod(candidates, {
|
||||||
|
cameraHeightM: pose.cartographic.height,
|
||||||
|
incumbentIds: active,
|
||||||
|
});
|
||||||
|
budget = result.budget.activeLimit;
|
||||||
|
const next = applyInfraEvictionGrace({
|
||||||
|
selectedIds: result.activeIds,
|
||||||
|
builtIds: [...active],
|
||||||
|
graceState: grace,
|
||||||
|
nowMs: now,
|
||||||
|
activeLimit: budget,
|
||||||
|
});
|
||||||
|
active = new Set(next.keepIds);
|
||||||
|
grace = next.graceState;
|
||||||
|
signature = sig;
|
||||||
|
const nextTopology = next.keepIds.slice().sort().join('/');
|
||||||
|
if (force || topology !== nextTopology) {
|
||||||
|
topology = nextTopology;
|
||||||
|
footprints();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shown = [];
|
||||||
|
for (const fid of active) {
|
||||||
|
const r = records.get(fid);
|
||||||
|
if (!engine.projector.horizonVisible(r.base)) continue;
|
||||||
|
const carto = r.carto;
|
||||||
|
if (
|
||||||
|
r.distance < 75000 &&
|
||||||
|
now - r.lastSample >= 2000 &&
|
||||||
|
(force || r.tries < 30)
|
||||||
|
) {
|
||||||
|
const terrain = engine.surface.terrainHeight(carto);
|
||||||
|
const sampled = engine.surface.supported
|
||||||
|
? engine.surface.sampleHeight(carto, [points])
|
||||||
|
: undefined;
|
||||||
|
const floor = Number.isFinite(sampled)
|
||||||
|
? Math.max(sampled, terrain ?? -Infinity)
|
||||||
|
: terrain;
|
||||||
|
r.lastSample = now;
|
||||||
|
r.tries++;
|
||||||
|
if (Number.isFinite(floor) && Math.abs(floor) <= 9000) {
|
||||||
|
r.floor = floor;
|
||||||
|
r.grounded = Number.isFinite(sampled) || !engine.surface.supported;
|
||||||
|
geo.cartesian3.fromRadians(
|
||||||
|
carto.longitude,
|
||||||
|
carto.latitude,
|
||||||
|
floor,
|
||||||
|
undefined,
|
||||||
|
r.base,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const height =
|
||||||
|
r.floor +
|
||||||
|
(Math.max(1, r.distance) * 2 * Math.tan(pose.fovy / 2) * 65) /
|
||||||
|
Math.max(1, size.height);
|
||||||
|
geo.cartesian3.fromRadians(
|
||||||
|
carto.longitude,
|
||||||
|
carto.latitude,
|
||||||
|
height,
|
||||||
|
undefined,
|
||||||
|
r.tip,
|
||||||
|
);
|
||||||
|
points.set(shown.length, {
|
||||||
|
position: r.tip,
|
||||||
|
color: rgba,
|
||||||
|
show: true,
|
||||||
|
pickId: key(fid),
|
||||||
|
});
|
||||||
|
shown.push(r);
|
||||||
|
}
|
||||||
|
points.setCount(shown.length);
|
||||||
|
points.commit();
|
||||||
|
stems.set(shown, pose.position);
|
||||||
|
publisher.publish(
|
||||||
|
selectLocalInfrastructureOverlayCohort(shown, {
|
||||||
|
maxEntries: labelMax,
|
||||||
|
gridPx: labelGridPx,
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT,
|
||||||
|
project: (r) => engine.projector.worldToWindow(r.tip, {}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!retryTimer &&
|
||||||
|
engine.surface.supported &&
|
||||||
|
shown.some((r) => !r.grounded && r.distance < 75000 && r.tries < 30)
|
||||||
|
) {
|
||||||
|
retryTimer = setTimeout(() => {
|
||||||
|
retryTimer = null;
|
||||||
|
wake();
|
||||||
|
}, 2050);
|
||||||
|
}
|
||||||
|
wake();
|
||||||
|
}
|
||||||
|
function select(fid) {
|
||||||
|
const row = records.get(fid);
|
||||||
|
if (!row) return false;
|
||||||
|
services.selectEntityContext(row);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function hide() {
|
||||||
|
clearTimeout(retryTimer);
|
||||||
|
retryTimer = null;
|
||||||
|
while (removers.length) removers.pop()();
|
||||||
|
unbindShapes?.();
|
||||||
|
unbindShapes = null;
|
||||||
|
publisher.hide();
|
||||||
|
display.destroy();
|
||||||
|
points?.destroy();
|
||||||
|
stems?.destroy();
|
||||||
|
points = stems = dataset = null;
|
||||||
|
records.clear();
|
||||||
|
active.clear();
|
||||||
|
grace.clear();
|
||||||
|
shown = [];
|
||||||
|
budget = 0;
|
||||||
|
signature = topology = '';
|
||||||
|
services.removeEntityContextsForLayer(id);
|
||||||
|
services.hideOsmCredit(engine, id);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
show(next) {
|
||||||
|
if (dataset === next) return;
|
||||||
|
dataset = next;
|
||||||
|
points = engine.movers.createSet({
|
||||||
|
id,
|
||||||
|
kind: 'point',
|
||||||
|
capacity: INFRA_LOD_ACTIVE_MAX,
|
||||||
|
pickable: true,
|
||||||
|
depthTest: 'always',
|
||||||
|
point: { sizePx: 10, outlinePx: 2, outlineColor: [0, 0, 0, 1] },
|
||||||
|
});
|
||||||
|
stems = createStemBatch(native, color);
|
||||||
|
for (const input of next.records) {
|
||||||
|
const carto = geo.cartographic.fromDegrees(input.lon, input.lat);
|
||||||
|
const base = geo.cartesian3.fromDegrees(input.lon, input.lat),
|
||||||
|
tip = geo.cartesian3.clone(base);
|
||||||
|
const priority = labelPriorityFromProperties(input.properties, id);
|
||||||
|
const row = {
|
||||||
|
...input,
|
||||||
|
carto,
|
||||||
|
base,
|
||||||
|
tip,
|
||||||
|
priority,
|
||||||
|
floor: 0,
|
||||||
|
tries: 0,
|
||||||
|
lastSample: -Infinity,
|
||||||
|
grounded: false,
|
||||||
|
};
|
||||||
|
row.entry = createLocalInfrastructureOverlayEntry({
|
||||||
|
id: row.id,
|
||||||
|
layerId: id,
|
||||||
|
position: tip,
|
||||||
|
properties: row.properties,
|
||||||
|
priority,
|
||||||
|
accent: color,
|
||||||
|
});
|
||||||
|
records.set(row.id, row);
|
||||||
|
services.registerEntityContext(row, {
|
||||||
|
id: key(row.id),
|
||||||
|
layerId: id,
|
||||||
|
layerName: name,
|
||||||
|
source,
|
||||||
|
label: localInfrastructureOverlayCopy(row.properties, id).title,
|
||||||
|
properties: row.properties,
|
||||||
|
latitude: row.lat,
|
||||||
|
longitude: row.lon,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
display.setVisible(true);
|
||||||
|
publisher.show();
|
||||||
|
if (next.features.length) services.showOsmCredit(engine, id);
|
||||||
|
removers.push(
|
||||||
|
engine.frames.preRender.addEventListener(() => sweep()),
|
||||||
|
engine.camera.moveEnd.addEventListener(() => sweep(true)),
|
||||||
|
);
|
||||||
|
removers.push(
|
||||||
|
engine.picker.registerMeshPicker((at) => {
|
||||||
|
const hits = [];
|
||||||
|
for (const row of shown) {
|
||||||
|
const a = engine.projector.worldToWindow(row.base, {}),
|
||||||
|
b = engine.projector.worldToWindow(row.tip, {});
|
||||||
|
if (a && b && distanceToSegment(at, a, b) <= 2.5)
|
||||||
|
hits.push({
|
||||||
|
onTop: 0,
|
||||||
|
order: 0,
|
||||||
|
depth: row.distance,
|
||||||
|
result: { id: key(row.id), owner: id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return hits;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
removers.push(
|
||||||
|
engine.input.on(
|
||||||
|
'click',
|
||||||
|
(event) => {
|
||||||
|
if (!isPointerFree() || event.picked?.owner !== id) return;
|
||||||
|
const fid = String(event.picked.id).slice(id.length + 1);
|
||||||
|
const row = records.get(fid);
|
||||||
|
const feature = row || next.features.find((f) => f.id === fid);
|
||||||
|
if (!feature) return;
|
||||||
|
if (row) select(fid);
|
||||||
|
const [lon, lat] = row
|
||||||
|
? [row.lon, row.lat]
|
||||||
|
: feature.geometry.coordinates[0];
|
||||||
|
engine.camera.flyTo({
|
||||||
|
destination: geo.cartesian3.fromDegrees(lon, lat, 5000),
|
||||||
|
duration: 1.5,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ pick: true },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
sweep(true);
|
||||||
|
},
|
||||||
|
select,
|
||||||
|
hide,
|
||||||
|
destroy() {
|
||||||
|
hide();
|
||||||
|
publisher.destroy();
|
||||||
|
},
|
||||||
|
lod: () => ({
|
||||||
|
total: records.size,
|
||||||
|
active: active.size,
|
||||||
|
budgetLimit: budget,
|
||||||
|
computed: Boolean(dataset),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
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) {
|
||||||
|
if (!views.has(view)) {
|
||||||
|
view.registerMesh(
|
||||||
|
KEY,
|
||||||
|
class extends MeshDesc {
|
||||||
|
constructor(host, ctx, config) {
|
||||||
|
super(host, ctx, config);
|
||||||
|
this.batch = config[KEY];
|
||||||
|
}
|
||||||
|
getPassKey() {
|
||||||
|
return 'transparent';
|
||||||
|
}
|
||||||
|
createMesh() {
|
||||||
|
return this.batch.line;
|
||||||
|
}
|
||||||
|
onDestroy() {
|
||||||
|
this.batch.dispose();
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
views.add(view);
|
||||||
|
}
|
||||||
|
const xyz = new Float32Array(6 * INFRA_LOD_ACTIVE_MAX);
|
||||||
|
const geometry = new LineSegmentsGeometry().setPositions(xyz);
|
||||||
|
geometry.instanceCount = 0;
|
||||||
|
const material = new LineMaterial({
|
||||||
|
color,
|
||||||
|
linewidth: 3.5,
|
||||||
|
depthWrite: false,
|
||||||
|
transparent: true,
|
||||||
|
toneMapped: false,
|
||||||
|
});
|
||||||
|
const line = new LineSegments2(geometry, material);
|
||||||
|
line.frustumCulled = false;
|
||||||
|
const holder = view.addMesh({
|
||||||
|
[KEY]: {
|
||||||
|
line,
|
||||||
|
dispose() {
|
||||||
|
geometry.dispose();
|
||||||
|
material.dispose();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
set(records, origin) {
|
||||||
|
let at = 0;
|
||||||
|
for (const { base, tip } of records)
|
||||||
|
for (const p of [base, tip]) {
|
||||||
|
xyz[at++] = p.x - origin.x;
|
||||||
|
xyz[at++] = p.y - origin.y;
|
||||||
|
xyz[at++] = p.z - origin.z;
|
||||||
|
}
|
||||||
|
line.position.set(origin.x, origin.y, origin.z);
|
||||||
|
line.updateMatrix();
|
||||||
|
line.matrixWorldNeedsUpdate = true;
|
||||||
|
geometry.instanceCount = records.length;
|
||||||
|
geometry.attributes.instanceStart.data.needsUpdate = true;
|
||||||
|
},
|
||||||
|
destroy: () => holder.delete(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createInfrastructureSessionLayer } from './index.js';
|
||||||
|
|
||||||
|
test('a bundled file delivered between planets keeps multipart IDs and is read only once', async (t) => {
|
||||||
|
let deliver,
|
||||||
|
reads = 0;
|
||||||
|
t.mock.method(globalThis, 'fetch', async () => {
|
||||||
|
reads++;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
deliver = resolve;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const captures = [];
|
||||||
|
const layer = createInfrastructureSessionLayer(
|
||||||
|
{ id: 'local-dams', name: 'Dams', url: '/dams' },
|
||||||
|
{},
|
||||||
|
() => ({
|
||||||
|
show: (data) => captures.push(data),
|
||||||
|
hide() {},
|
||||||
|
destroy() {},
|
||||||
|
lod() {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
layer.init(null, { engine: {} });
|
||||||
|
const enabled = layer.enable();
|
||||||
|
await new Promise(setImmediate);
|
||||||
|
layer.detachRenderer();
|
||||||
|
const ring = [
|
||||||
|
[10, 20],
|
||||||
|
[10.02, 20],
|
||||||
|
[10, 20.01],
|
||||||
|
[10, 20],
|
||||||
|
];
|
||||||
|
deliver(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'Feature',
|
||||||
|
id: 'd',
|
||||||
|
properties: { name: 'Dam' },
|
||||||
|
geometry: {
|
||||||
|
type: 'MultiPolygon',
|
||||||
|
coordinates: [[ring], [ring]],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await enabled;
|
||||||
|
assert.equal(layer.getAnalystRecords().length, 2);
|
||||||
|
assert.equal(captures.length, 0);
|
||||||
|
await layer.attachRenderer(null, { engine: {} });
|
||||||
|
layer.detachRenderer();
|
||||||
|
await layer.attachRenderer(null, { engine: {} });
|
||||||
|
assert.equal(captures[0], captures[1]);
|
||||||
|
assert.deepEqual(
|
||||||
|
captures[0].records.map((r) => r.id),
|
||||||
|
['d', 'd_2'],
|
||||||
|
);
|
||||||
|
assert.equal(reads, 1);
|
||||||
|
layer.destroy();
|
||||||
|
});
|
||||||
Reference in new issue
Block a user