mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
style: format the reusable infrastructure component
This commit is contained in:
1 parent
04f4d54bfe
commit
f70b5a3631
4 files changed
+422
-155
No files matched your search
@@ -1,8 +1,12 @@
|
|||||||
import { createLocalGeoJsonLayer } from './localGeojsonCore.js';
|
import { createLocalGeoJsonLayer } from './localGeojsonCore.js';
|
||||||
|
|
||||||
// Resolved by Vite in builds and relative to this module in other consumers.
|
// Resolved by Vite in builds and relative to this module in other consumers.
|
||||||
const datacentersUrl = new URL('./local_data/datacenters/datacenters.geojsonl', import.meta.url).href;
|
const datacentersUrl = new URL(
|
||||||
const damsUrl = new URL('./local_data/dams/dams.geojsonl', import.meta.url).href;
|
'./local_data/datacenters/datacenters.geojsonl',
|
||||||
|
import.meta.url,
|
||||||
|
).href;
|
||||||
|
const damsUrl = new URL('./local_data/dams/dams.geojsonl', import.meta.url)
|
||||||
|
.href;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create fresh datacenter and dam layers without starting or loading them.
|
* Create fresh datacenter and dam layers without starting or loading them.
|
||||||
@@ -10,7 +14,8 @@ const damsUrl = new URL('./local_data/dams/dams.geojsonl', import.meta.url).href
|
|||||||
* @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) {
|
||||||
const datacenters = createLocalGeoJsonLayer({
|
const datacenters = createLocalGeoJsonLayer(
|
||||||
|
{
|
||||||
id: 'local-datacenters',
|
id: 'local-datacenters',
|
||||||
url: datacentersUrl,
|
url: datacentersUrl,
|
||||||
name: 'Datacenters',
|
name: 'Datacenters',
|
||||||
@@ -20,9 +25,12 @@ export function createInfrastructureLayers(services) {
|
|||||||
labels: true,
|
labels: true,
|
||||||
labelMax: 700,
|
labelMax: 700,
|
||||||
labelGridPx: 138,
|
labelGridPx: 138,
|
||||||
}, services);
|
},
|
||||||
|
services,
|
||||||
|
);
|
||||||
|
|
||||||
const dams = createLocalGeoJsonLayer({
|
const dams = createLocalGeoJsonLayer(
|
||||||
|
{
|
||||||
id: 'local-dams',
|
id: 'local-dams',
|
||||||
url: damsUrl,
|
url: damsUrl,
|
||||||
name: 'Dams',
|
name: 'Dams',
|
||||||
@@ -32,7 +40,9 @@ export function createInfrastructureLayers(services) {
|
|||||||
labels: true,
|
labels: true,
|
||||||
labelMax: 900,
|
labelMax: 900,
|
||||||
labelGridPx: 132,
|
labelGridPx: 132,
|
||||||
}, services);
|
},
|
||||||
|
services,
|
||||||
|
);
|
||||||
|
|
||||||
return [datacenters, dams];
|
return [datacenters, dams];
|
||||||
}
|
}
|
||||||
@@ -12,11 +12,18 @@ function services() {
|
|||||||
return {
|
return {
|
||||||
records,
|
records,
|
||||||
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
|
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
|
||||||
registerEntityContext(entity, metadata) { records.set(metadata.id, { entity, ...metadata }); },
|
registerEntityContext(entity, metadata) {
|
||||||
selectEntityContext(entity) { selection = entity; },
|
records.set(metadata.id, { entity, ...metadata });
|
||||||
clearSelectedEntityContextForLayer() { selection = undefined; },
|
},
|
||||||
|
selectEntityContext(entity) {
|
||||||
|
selection = entity;
|
||||||
|
},
|
||||||
|
clearSelectedEntityContextForLayer() {
|
||||||
|
selection = undefined;
|
||||||
|
},
|
||||||
removeEntityContextsForLayer(id) {
|
removeEntityContextsForLayer(id) {
|
||||||
for (const [key, record] of records) if (record.layerId === id) records.delete(key);
|
for (const [key, record] of records)
|
||||||
|
if (record.layerId === id) records.delete(key);
|
||||||
},
|
},
|
||||||
governorRequestRender() {},
|
governorRequestRender() {},
|
||||||
selection: () => selection,
|
selection: () => selection,
|
||||||
@@ -24,7 +31,12 @@ function services() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test('package exports import without an application, DOM, fetch, or timers', () => {
|
test('package exports import without an application, DOM, fetch, or timers', () => {
|
||||||
const result = spawnSync(process.execPath, ['--input-type=module', '-e', `
|
const result = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
[
|
||||||
|
'--input-type=module',
|
||||||
|
'-e',
|
||||||
|
`
|
||||||
globalThis.fetch = () => { throw new Error('unexpected fetch'); };
|
globalThis.fetch = () => { throw new Error('unexpected fetch'); };
|
||||||
globalThis.setTimeout = () => { throw new Error('unexpected timer'); };
|
globalThis.setTimeout = () => { throw new Error('unexpected timer'); };
|
||||||
for (const key of ['window', 'document']) {
|
for (const key of ['window', 'document']) {
|
||||||
@@ -33,51 +45,99 @@ test('package exports import without an application, DOM, fetch, or timers', ()
|
|||||||
await import('gods-eye-view/infrastructure');
|
await import('gods-eye-view/infrastructure');
|
||||||
await import('gods-eye-view/infrastructure/geojson');
|
await import('gods-eye-view/infrastructure/geojson');
|
||||||
await import('gods-eye-view/infrastructure/lod');
|
await import('gods-eye-view/infrastructure/lod');
|
||||||
`], { cwd: new URL('../..', import.meta.url), encoding: 'utf8' });
|
`,
|
||||||
|
],
|
||||||
|
{ cwd: new URL('../..', import.meta.url), encoding: 'utf8' },
|
||||||
|
);
|
||||||
assert.equal(result.status, 0, result.stderr);
|
assert.equal(result.status, 0, result.stderr);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('infrastructure factory preserves identity and creates independent state without loading', t => {
|
test('infrastructure factory preserves identity and creates independent state without loading', (t) => {
|
||||||
t.mock.method(globalThis, 'fetch', () => { throw new Error('factory must not fetch'); });
|
t.mock.method(globalThis, 'fetch', () => {
|
||||||
|
throw new Error('factory must not fetch');
|
||||||
|
});
|
||||||
const first = createInfrastructureLayers(services());
|
const first = createInfrastructureLayers(services());
|
||||||
const second = createInfrastructureLayers(services());
|
const second = createInfrastructureLayers(services());
|
||||||
assert.deepEqual(first.map(({ id, name, source }) => ({ id, name, source })), [
|
assert.deepEqual(
|
||||||
|
first.map(({ id, name, source }) => ({ id, name, source })),
|
||||||
|
[
|
||||||
{ id: 'local-datacenters', name: 'Datacenters', source: 'Local' },
|
{ id: 'local-datacenters', name: 'Datacenters', source: 'Local' },
|
||||||
{ id: 'local-dams', name: 'Dams', source: 'USACE' },
|
{ id: 'local-dams', name: 'Dams', source: 'USACE' },
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
first.forEach((layer, index) => {
|
first.forEach((layer, index) => {
|
||||||
assert.notEqual(layer, second[index]);
|
assert.notEqual(layer, second[index]);
|
||||||
layer.destroy();
|
layer.destroy();
|
||||||
assert.deepEqual(second[index].getStats(), { count: 0, lastUpdate: null, error: null });
|
assert.deepEqual(second[index].getStats(), {
|
||||||
|
count: 0,
|
||||||
|
lastUpdate: null,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
second[index].destroy();
|
second[index].destroy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('dataset URLs still name the complete bundled sources', () => {
|
test('dataset URLs still name the complete bundled sources', () => {
|
||||||
for (const [file, count] of [['datacenters', 4351], ['dams', 704]]) {
|
for (const [file, count] of [
|
||||||
const lines = readFileSync(new URL(`./local_data/${file}/${file}.geojsonl`, import.meta.url), 'utf8')
|
['datacenters', 4351],
|
||||||
.split('\n').filter(line => line.trim());
|
['dams', 704],
|
||||||
|
]) {
|
||||||
|
const lines = readFileSync(
|
||||||
|
new URL(`./local_data/${file}/${file}.geojsonl`, import.meta.url),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line.trim());
|
||||||
assert.equal(lines.length, count);
|
assert.equal(lines.length, count);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('two viewers use their supplied contexts and dispose independently', async t => {
|
test('two viewers use their supplied contexts and dispose independently', async (t) => {
|
||||||
t.mock.method(globalThis, 'fetch', async () => ({ ok: true, text: async () => JSON.stringify({
|
t.mock.method(globalThis, 'fetch', async () => ({
|
||||||
type: 'Feature', id: 'same-id', properties: { name: 'Dam' },
|
ok: true,
|
||||||
geometry: { type: 'Polygon', coordinates: [[[0, 0], [0.01, 0], [0, 0.01], [0, 0]]] },
|
text: async () =>
|
||||||
}) }));
|
JSON.stringify({
|
||||||
|
type: 'Feature',
|
||||||
|
id: 'same-id',
|
||||||
|
properties: { name: 'Dam' },
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: [
|
||||||
|
[
|
||||||
|
[0, 0],
|
||||||
|
[0.01, 0],
|
||||||
|
[0, 0.01],
|
||||||
|
[0, 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
const hosts = [services(), services()];
|
const hosts = [services(), services()];
|
||||||
const instances = hosts.map(host => {
|
const instances = hosts.map((host) => {
|
||||||
const sources = new Cesium.DataSourceCollection();
|
const sources = new Cesium.DataSourceCollection();
|
||||||
let click;
|
let click;
|
||||||
const layer = createLocalGeoJsonLayer({
|
const layer = createLocalGeoJsonLayer(
|
||||||
id: 'local-dams', name: 'Dams', color: '#0088ff', url: '/dams.geojsonl',
|
{
|
||||||
screenSpaceEventHandlerFactory: () => ({ setInputAction(fn) { click = fn; }, destroy() {} }),
|
id: 'local-dams',
|
||||||
}, host);
|
name: 'Dams',
|
||||||
|
color: '#0088ff',
|
||||||
|
url: '/dams.geojsonl',
|
||||||
|
screenSpaceEventHandlerFactory: () => ({
|
||||||
|
setInputAction(fn) {
|
||||||
|
click = fn;
|
||||||
|
},
|
||||||
|
destroy() {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
host,
|
||||||
|
);
|
||||||
const viewer = {
|
const viewer = {
|
||||||
dataSources: sources,
|
dataSources: sources,
|
||||||
scene: {
|
scene: {
|
||||||
canvas: {}, preRender: new Cesium.Event(), requestRender() {},
|
canvas: {},
|
||||||
|
preRender: new Cesium.Event(),
|
||||||
|
requestRender() {},
|
||||||
screenSpaceCameraController: { enableInputs: true },
|
screenSpaceCameraController: { enableInputs: true },
|
||||||
pick: () => ({ id: sources.get(0).entities.values[0] }),
|
pick: () => ({ id: sources.get(0).entities.values[0] }),
|
||||||
},
|
},
|
||||||
@@ -85,7 +145,9 @@ test('two viewers use their supplied contexts and dispose independently', async
|
|||||||
};
|
};
|
||||||
return { layer, viewer, click: () => click({ position: {} }) };
|
return { layer, viewer, click: () => click({ position: {} }) };
|
||||||
});
|
});
|
||||||
t.after(() => instances.forEach(({ layer, viewer }) => layer.destroy(viewer)));
|
t.after(() =>
|
||||||
|
instances.forEach(({ layer, viewer }) => layer.destroy(viewer)),
|
||||||
|
);
|
||||||
await Promise.all(instances.map(({ layer, viewer }) => layer.enable(viewer)));
|
await Promise.all(instances.map(({ layer, viewer }) => layer.enable(viewer)));
|
||||||
assert.equal(hosts[0].records.size, 1);
|
assert.equal(hosts[0].records.size, 1);
|
||||||
assert.equal(hosts[1].records.size, 1);
|
assert.equal(hosts[1].records.size, 1);
|
||||||
@@ -109,22 +171,35 @@ test('consumer build includes only infrastructure code and resolves assets under
|
|||||||
write: false,
|
write: false,
|
||||||
assetsInlineLimit: 0,
|
assetsInlineLimit: 0,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: fileURLToPath(import.meta.resolve('gods-eye-view/infrastructure')),
|
input: fileURLToPath(
|
||||||
|
import.meta.resolve('gods-eye-view/infrastructure'),
|
||||||
|
),
|
||||||
external: ['cesium'],
|
external: ['cesium'],
|
||||||
preserveEntrySignatures: 'strict',
|
preserveEntrySignatures: 'strict',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const output = result.output;
|
const output = result.output;
|
||||||
const entry = output.find(item => item.type === 'chunk' && item.isEntry);
|
const entry = output.find((item) => item.type === 'chunk' && item.isEntry);
|
||||||
const sources = Object.keys(entry.modules).filter(id => id.endsWith('.js'));
|
const sources = Object.keys(entry.modules).filter((id) => id.endsWith('.js'));
|
||||||
assert.deepEqual(sources.map(id => id.split('/').at(-1)).sort(), [
|
assert.deepEqual(sources.map((id) => id.split('/').at(-1)).sort(), [
|
||||||
'infrastructure.js', 'localGeojsonCore.js', 'localGeojsonLod.js',
|
'infrastructure.js',
|
||||||
|
'localGeojsonCore.js',
|
||||||
|
'localGeojsonLod.js',
|
||||||
]);
|
]);
|
||||||
assert.deepEqual(entry.imports, ['cesium'], 'the viewer supplies the same Cesium dependency');
|
assert.deepEqual(
|
||||||
|
entry.imports,
|
||||||
|
['cesium'],
|
||||||
|
'the viewer supplies the same Cesium dependency',
|
||||||
|
);
|
||||||
for (const name of ['datacenters', 'dams']) {
|
for (const name of ['datacenters', 'dams']) {
|
||||||
const asset = output.find(item => item.type === 'asset' && item.fileName.includes(name));
|
const asset = output.find(
|
||||||
|
(item) => item.type === 'asset' && item.fileName.includes(name),
|
||||||
|
);
|
||||||
assert.ok(asset, `${name} must be emitted`);
|
assert.ok(asset, `${name} must be emitted`);
|
||||||
assert.ok(entry.code.includes(`/example/${asset.fileName}`), `${name} must retain the consumer base path`);
|
assert.ok(
|
||||||
|
entry.code.includes(`/example/${asset.fileName}`),
|
||||||
|
`${name} must retain the consumer base path`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
+218
-83
@@ -15,7 +15,8 @@ const LOCAL_OVERLAY_COLLISION_CAPACITY = 96;
|
|||||||
const LOCAL_OVERLAY_CELL_SURPLUS = 2;
|
const LOCAL_OVERLAY_CELL_SURPLUS = 2;
|
||||||
const LOCAL_OVERLAY_MAX_DISTANCE_M = 14000000;
|
const LOCAL_OVERLAY_MAX_DISTANCE_M = 14000000;
|
||||||
const LOCAL_OVERLAY_FADE_START_M = 250000;
|
const LOCAL_OVERLAY_FADE_START_M = 250000;
|
||||||
const LOCAL_OVERLAY_FADE_START_RATIO = LOCAL_OVERLAY_FADE_START_M / LOCAL_OVERLAY_MAX_DISTANCE_M;
|
const LOCAL_OVERLAY_FADE_START_RATIO =
|
||||||
|
LOCAL_OVERLAY_FADE_START_M / LOCAL_OVERLAY_MAX_DISTANCE_M;
|
||||||
// Stems are anchored at ellipsoid height 0, but high-elevation features
|
// Stems are anchored at ellipsoid height 0, but high-elevation features
|
||||||
// (e.g. dams in river canyons) sit hundreds of meters above the ellipsoid,
|
// (e.g. dams in river canyons) sit hundreds of meters above the ellipsoid,
|
||||||
// burying the short close-in stem inside the photoreal mesh. Once the
|
// burying the short close-in stem inside the photoreal mesh. Once the
|
||||||
@@ -63,8 +64,12 @@ export function localInfrastructureOverlayCopy(properties, layerId) {
|
|||||||
props.capacity,
|
props.capacity,
|
||||||
]);
|
]);
|
||||||
const line = [operator, capacity]
|
const line = [operator, capacity]
|
||||||
.filter((value, index, values) => value && values.indexOf(value) === index)
|
.filter(
|
||||||
.filter((value) => value.toLocaleLowerCase() !== title.toLocaleLowerCase())
|
(value, index, values) => value && values.indexOf(value) === index,
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(value) => value.toLocaleLowerCase() !== title.toLocaleLowerCase(),
|
||||||
|
)
|
||||||
.join(' · ');
|
.join(' · ');
|
||||||
if (line) details.push(clampCardLine(line));
|
if (line) details.push(clampCardLine(line));
|
||||||
} else if (layerId === 'local-dams') {
|
} else if (layerId === 'local-dams') {
|
||||||
@@ -147,21 +152,29 @@ export function createLocalInfrastructureOverlayEntry({
|
|||||||
* @param {number} [options.cohortLimit=Infinity] Host-safe materialization cap.
|
* @param {number} [options.cohortLimit=Infinity] Host-safe materialization cap.
|
||||||
* @returns {object[]} Bounded overlay entries for shared-host arbitration.
|
* @returns {object[]} Bounded overlay entries for shared-host arbitration.
|
||||||
*/
|
*/
|
||||||
export function selectLocalInfrastructureOverlayCohort(records, {
|
export function selectLocalInfrastructureOverlayCohort(
|
||||||
|
records,
|
||||||
|
{
|
||||||
maxEntries,
|
maxEntries,
|
||||||
gridPx,
|
gridPx,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
project,
|
project,
|
||||||
cohortLimit = Number.POSITIVE_INFINITY,
|
cohortLimit = Number.POSITIVE_INFINITY,
|
||||||
}) {
|
},
|
||||||
|
) {
|
||||||
const sourceCap = Math.max(0, Math.floor(Number(maxEntries) || 0));
|
const sourceCap = Math.max(0, Math.floor(Number(maxEntries) || 0));
|
||||||
const materializationCap = Number.isFinite(Number(cohortLimit))
|
const materializationCap = Number.isFinite(Number(cohortLimit))
|
||||||
? Math.max(0, Math.floor(Number(cohortLimit)))
|
? Math.max(0, Math.floor(Number(cohortLimit)))
|
||||||
: Number.POSITIVE_INFINITY;
|
: Number.POSITIVE_INFINITY;
|
||||||
const cap = Math.min(sourceCap, materializationCap);
|
const cap = Math.min(sourceCap, materializationCap);
|
||||||
const cellSize = Math.max(1, Number(gridPx) || 1);
|
const cellSize = Math.max(1, Number(gridPx) || 1);
|
||||||
if (!Array.isArray(records) || records.length === 0 || cap === 0 || typeof project !== 'function') {
|
if (
|
||||||
|
!Array.isArray(records) ||
|
||||||
|
records.length === 0 ||
|
||||||
|
cap === 0 ||
|
||||||
|
typeof project !== 'function'
|
||||||
|
) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,8 +183,13 @@ export function selectLocalInfrastructureOverlayCohort(records, {
|
|||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const screen = project(record);
|
const screen = project(record);
|
||||||
if (!Number.isFinite(screen?.x) || !Number.isFinite(screen?.y)) continue;
|
if (!Number.isFinite(screen?.x) || !Number.isFinite(screen?.y)) continue;
|
||||||
if (screen.x < -padding || screen.x > width + padding
|
if (
|
||||||
|| screen.y < -padding || screen.y > height + padding) continue;
|
screen.x < -padding ||
|
||||||
|
screen.x > width + padding ||
|
||||||
|
screen.y < -padding ||
|
||||||
|
screen.y > height + padding
|
||||||
|
)
|
||||||
|
continue;
|
||||||
const key = `${Math.floor(screen.x / cellSize)}:${Math.floor(screen.y / cellSize)}`;
|
const key = `${Math.floor(screen.x / cellSize)}:${Math.floor(screen.y / cellSize)}`;
|
||||||
let contenders = cells.get(key);
|
let contenders = cells.get(key);
|
||||||
if (!contenders) {
|
if (!contenders) {
|
||||||
@@ -189,7 +207,8 @@ export function selectLocalInfrastructureOverlayCohort(records, {
|
|||||||
}
|
}
|
||||||
primary.sort(compareLocalOverlayRecords);
|
primary.sort(compareLocalOverlayRecords);
|
||||||
surplus.sort(compareLocalOverlayRecords);
|
surplus.sort(compareLocalOverlayRecords);
|
||||||
if (primary.length >= cap) return primary.slice(0, cap).map((record) => record.entry);
|
if (primary.length >= cap)
|
||||||
|
return primary.slice(0, cap).map((record) => record.entry);
|
||||||
const candidates = primary.concat(surplus.slice(0, cap - primary.length));
|
const candidates = primary.concat(surplus.slice(0, cap - primary.length));
|
||||||
return candidates.map((record) => record.entry);
|
return candidates.map((record) => record.entry);
|
||||||
}
|
}
|
||||||
@@ -201,10 +220,7 @@ export function selectLocalInfrastructureOverlayCohort(records, {
|
|||||||
* @param {object} [options.host] Test seam for the three host lifecycle calls.
|
* @param {object} [options.host] Test seam for the three host lifecycle calls.
|
||||||
* @returns {{show:function():void,publish:function(object[]):void,hide:function():void,destroy:function():void}}
|
* @returns {{show:function():void,publish:function(object[]):void,hide:function():void,destroy:function():void}}
|
||||||
*/
|
*/
|
||||||
export function createLocalInfrastructureOverlayPublisher({
|
export function createLocalInfrastructureOverlayPublisher({ sourceId, host }) {
|
||||||
sourceId,
|
|
||||||
host,
|
|
||||||
}) {
|
|
||||||
let visible = false;
|
let visible = false;
|
||||||
let published = false;
|
let published = false;
|
||||||
let destroyed = false;
|
let destroyed = false;
|
||||||
@@ -227,17 +243,26 @@ export function createLocalInfrastructureOverlayPublisher({
|
|||||||
},
|
},
|
||||||
publish(entries) {
|
publish(entries) {
|
||||||
if (destroyed || !visible) return;
|
if (destroyed || !visible) return;
|
||||||
if (lastPublication && entries.length === lastPublication.length
|
if (
|
||||||
&& entries.every((entry, index) => {
|
lastPublication &&
|
||||||
|
entries.length === lastPublication.length &&
|
||||||
|
entries.every((entry, index) => {
|
||||||
const previous = lastPublication[index];
|
const previous = lastPublication[index];
|
||||||
return entry === previous.entry
|
return (
|
||||||
&& entry.position?.x === previous.x
|
entry === previous.entry &&
|
||||||
&& entry.position?.y === previous.y
|
entry.position?.x === previous.x &&
|
||||||
&& entry.position?.z === previous.z;
|
entry.position?.y === previous.y &&
|
||||||
})) return;
|
entry.position?.z === previous.z
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return;
|
||||||
host.setEntries(sourceId, entries, sourceOptions);
|
host.setEntries(sourceId, entries, sourceOptions);
|
||||||
lastPublication = entries.map(entry => ({
|
lastPublication = entries.map((entry) => ({
|
||||||
entry, x: entry.position?.x, y: entry.position?.y, z: entry.position?.z,
|
entry,
|
||||||
|
x: entry.position?.x,
|
||||||
|
y: entry.position?.y,
|
||||||
|
z: entry.position?.z,
|
||||||
}));
|
}));
|
||||||
published = entries.length > 0;
|
published = entries.length > 0;
|
||||||
},
|
},
|
||||||
@@ -288,7 +313,8 @@ export function localDatasetError(error) {
|
|||||||
* One live instance per layer id is allowed in a given viewer/context/overlay host.
|
* One live instance per layer id is allowed in a given viewer/context/overlay host.
|
||||||
* Destroy the previous instance before replacing it. Importing creates no layers.
|
* Destroy the previous instance before replacing it. Importing creates no layers.
|
||||||
*/
|
*/
|
||||||
export function createLocalGeoJsonLayer({
|
export function createLocalGeoJsonLayer(
|
||||||
|
{
|
||||||
id,
|
id,
|
||||||
url,
|
url,
|
||||||
name,
|
name,
|
||||||
@@ -298,16 +324,20 @@ export function createLocalGeoJsonLayer({
|
|||||||
labels = true,
|
labels = true,
|
||||||
labelMax = DEFAULT_LABEL_MAX,
|
labelMax = DEFAULT_LABEL_MAX,
|
||||||
labelGridPx = DEFAULT_LABEL_GRID_PX,
|
labelGridPx = DEFAULT_LABEL_GRID_PX,
|
||||||
screenSpaceEventHandlerFactory = (canvas) => new Cesium.ScreenSpaceEventHandler(canvas),
|
screenSpaceEventHandlerFactory = (canvas) =>
|
||||||
projectToWindow = (scene, position) => Cesium.SceneTransforms.worldToWindowCoordinates(scene, position),
|
new Cesium.ScreenSpaceEventHandler(canvas),
|
||||||
}, {
|
projectToWindow = (scene, position) =>
|
||||||
|
Cesium.SceneTransforms.worldToWindowCoordinates(scene, position),
|
||||||
|
},
|
||||||
|
{
|
||||||
overlayHost,
|
overlayHost,
|
||||||
registerEntityContext,
|
registerEntityContext,
|
||||||
selectEntityContext,
|
selectEntityContext,
|
||||||
clearSelectedEntityContextForLayer,
|
clearSelectedEntityContextForLayer,
|
||||||
removeEntityContextsForLayer,
|
removeEntityContextsForLayer,
|
||||||
governorRequestRender,
|
governorRequestRender,
|
||||||
}) {
|
},
|
||||||
|
) {
|
||||||
let _dataSource = null;
|
let _dataSource = null;
|
||||||
let _enabled = false;
|
let _enabled = false;
|
||||||
let _clickHandler = null;
|
let _clickHandler = null;
|
||||||
@@ -483,7 +513,8 @@ export function createLocalGeoJsonLayer({
|
|||||||
|
|
||||||
// 1. Initialize data source
|
// 1. Initialize data source
|
||||||
if (!_dataSource) {
|
if (!_dataSource) {
|
||||||
if (!_loadPromise) _loadPromise = (async () => {
|
if (!_loadPromise)
|
||||||
|
_loadPromise = (async () => {
|
||||||
_loadController = new AbortController();
|
_loadController = new AbortController();
|
||||||
const baseColor = Cesium.Color.fromCssColorString(color);
|
const baseColor = Cesium.Color.fromCssColorString(color);
|
||||||
|
|
||||||
@@ -498,7 +529,9 @@ 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 {
|
||||||
const response = await fetch(url, { signal: _loadController.signal });
|
const response = await fetch(url, {
|
||||||
|
signal: _loadController.signal,
|
||||||
|
});
|
||||||
if (_destroyed) return;
|
if (_destroyed) return;
|
||||||
// A 404 returns an HTML body that would otherwise die in JSON.parse
|
// A 404 returns an HTML body that would otherwise die in JSON.parse
|
||||||
// one line later, reported as a parse error for a missing file.
|
// one line later, reported as a parse error for a missing file.
|
||||||
@@ -507,13 +540,13 @@ export function createLocalGeoJsonLayer({
|
|||||||
}
|
}
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
if (_destroyed) return;
|
if (_destroyed) return;
|
||||||
const lines = text.split('\n').filter(l => l.trim().length > 0);
|
const lines = text.split('\n').filter((l) => l.trim().length > 0);
|
||||||
|
|
||||||
const features = lines.map(line => JSON.parse(line));
|
const features = lines.map((line) => JSON.parse(line));
|
||||||
|
|
||||||
const geojson = {
|
const geojson = {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features
|
features,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Natively parse into entities and use it as our _dataSource
|
// Natively parse into entities and use it as our _dataSource
|
||||||
@@ -562,9 +595,17 @@ export function createLocalGeoJsonLayer({
|
|||||||
feature.polygon.outlineColor = baseColor;
|
feature.polygon.outlineColor = baseColor;
|
||||||
|
|
||||||
// Calculate center point for the stem
|
// Calculate center point for the stem
|
||||||
const hierarchy = feature.polygon.hierarchy?.getValue(Cesium.JulianDate.now());
|
const hierarchy = feature.polygon.hierarchy?.getValue(
|
||||||
if (hierarchy && hierarchy.positions && hierarchy.positions.length > 0) {
|
Cesium.JulianDate.now(),
|
||||||
pos = Cesium.BoundingSphere.fromPoints(hierarchy.positions).center;
|
);
|
||||||
|
if (
|
||||||
|
hierarchy &&
|
||||||
|
hierarchy.positions &&
|
||||||
|
hierarchy.positions.length > 0
|
||||||
|
) {
|
||||||
|
pos = Cesium.BoundingSphere.fromPoints(
|
||||||
|
hierarchy.positions,
|
||||||
|
).center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -575,8 +616,16 @@ export function createLocalGeoJsonLayer({
|
|||||||
const groundHeight = 0; // Ellipsoid surface until a scene sample lands
|
const groundHeight = 0; // Ellipsoid surface until a scene sample lands
|
||||||
const tipHeight = 2000; // Initial Stem height
|
const tipHeight = 2000; // Initial Stem height
|
||||||
|
|
||||||
const base = Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, groundHeight);
|
const base = Cesium.Cartesian3.fromRadians(
|
||||||
const tip = Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, tipHeight);
|
carto.longitude,
|
||||||
|
carto.latitude,
|
||||||
|
groundHeight,
|
||||||
|
);
|
||||||
|
const tip = Cesium.Cartesian3.fromRadians(
|
||||||
|
carto.longitude,
|
||||||
|
carto.latitude,
|
||||||
|
tipHeight,
|
||||||
|
);
|
||||||
const properties = propertyObject(feature);
|
const properties = propertyObject(feature);
|
||||||
const recordId = String(feature.id ?? i);
|
const recordId = String(feature.id ?? i);
|
||||||
|
|
||||||
@@ -591,15 +640,22 @@ export function createLocalGeoJsonLayer({
|
|||||||
dataSource: loaded,
|
dataSource: loaded,
|
||||||
label: featureLabelFromProperties(properties, id),
|
label: featureLabelFromProperties(properties, id),
|
||||||
properties,
|
properties,
|
||||||
latitude: Number(Cesium.Math.toDegrees(carto.latitude).toFixed(6)),
|
latitude: Number(
|
||||||
longitude: Number(Cesium.Math.toDegrees(carto.longitude).toFixed(6)),
|
Cesium.Math.toDegrees(carto.latitude).toFixed(6),
|
||||||
|
),
|
||||||
|
longitude: Number(
|
||||||
|
Cesium.Math.toDegrees(carto.longitude).toFixed(6),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Constant properties are refreshed on the existing 450 ms source
|
// Constant properties are refreshed on the existing 450 ms source
|
||||||
// cadence. Cesium no longer evaluates 2-3 callbacks per entity on
|
// cadence. Cesium no longer evaluates 2-3 callbacks per entity on
|
||||||
// every frame, while the point/stem pick surface stays native.
|
// every frame, while the point/stem pick surface stays native.
|
||||||
feature.position = tip;
|
feature.position = tip;
|
||||||
const stemPositionBuffers = [[base, tip], [base, tip]];
|
const stemPositionBuffers = [
|
||||||
|
[base, tip],
|
||||||
|
[base, tip],
|
||||||
|
];
|
||||||
feature.polyline = new Cesium.PolylineGraphics({
|
feature.polyline = new Cesium.PolylineGraphics({
|
||||||
positions: stemPositionBuffers[0],
|
positions: stemPositionBuffers[0],
|
||||||
width: 3.5,
|
width: 3.5,
|
||||||
@@ -629,14 +685,16 @@ export function createLocalGeoJsonLayer({
|
|||||||
groundSampled: false,
|
groundSampled: false,
|
||||||
lastGroundSampleMs: 0,
|
lastGroundSampleMs: 0,
|
||||||
priority,
|
priority,
|
||||||
entry: labels ? createLocalInfrastructureOverlayEntry({
|
entry: labels
|
||||||
|
? createLocalInfrastructureOverlayEntry({
|
||||||
id: recordId,
|
id: recordId,
|
||||||
layerId: id,
|
layerId: id,
|
||||||
position: tip,
|
position: tip,
|
||||||
properties,
|
properties,
|
||||||
priority,
|
priority,
|
||||||
accent: color,
|
accent: color,
|
||||||
}) : null,
|
})
|
||||||
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Setup finished — publish it.
|
// Setup finished — publish it.
|
||||||
@@ -652,7 +710,11 @@ export function createLocalGeoJsonLayer({
|
|||||||
// before (or inside) add() never reached the collection, and
|
// before (or inside) add() never reached the collection, and
|
||||||
// removing then would race Cesium's pending insert.
|
// removing then would race Cesium's pending insert.
|
||||||
if (addedToScene) {
|
if (addedToScene) {
|
||||||
try { viewer?.dataSources?.remove(loaded, true); } catch { /* already gone */ }
|
try {
|
||||||
|
viewer?.dataSources?.remove(loaded, true);
|
||||||
|
} catch {
|
||||||
|
/* already gone */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (_destroyed) return;
|
if (_destroyed) return;
|
||||||
removeEntityContextsForLayer(id);
|
removeEntityContextsForLayer(id);
|
||||||
@@ -664,7 +726,9 @@ export function createLocalGeoJsonLayer({
|
|||||||
if (_destroyed) return;
|
if (_destroyed) return;
|
||||||
// 2. Install native global click handler
|
// 2. Install native global click handler
|
||||||
if (!_clickHandler) {
|
if (!_clickHandler) {
|
||||||
_clickHandler = screenSpaceEventHandlerFactory(viewer.scene.canvas);
|
_clickHandler = screenSpaceEventHandlerFactory(
|
||||||
|
viewer.scene.canvas,
|
||||||
|
);
|
||||||
_clickHandler.setInputAction((click) => {
|
_clickHandler.setInputAction((click) => {
|
||||||
if (!_enabled) return;
|
if (!_enabled) return;
|
||||||
const picked = viewer.scene.pick(click.position);
|
const picked = viewer.scene.pick(click.position);
|
||||||
@@ -679,15 +743,21 @@ export function createLocalGeoJsonLayer({
|
|||||||
|
|
||||||
if (entity.polyline) {
|
if (entity.polyline) {
|
||||||
// If it's a stem, fly to the base
|
// If it's a stem, fly to the base
|
||||||
const positions = entity.polyline.positions.getValue(Cesium.JulianDate.now());
|
const positions = entity.polyline.positions.getValue(
|
||||||
|
Cesium.JulianDate.now(),
|
||||||
|
);
|
||||||
if (positions && positions.length > 0) {
|
if (positions && positions.length > 0) {
|
||||||
targetPos = positions[0];
|
targetPos = positions[0];
|
||||||
}
|
}
|
||||||
} else if (entity.polygon && entity.polygon.hierarchy) {
|
} else if (entity.polygon && entity.polygon.hierarchy) {
|
||||||
// If it's a polygon, just fly to its center
|
// If it's a polygon, just fly to its center
|
||||||
const hierarchy = entity.polygon.hierarchy.getValue(Cesium.JulianDate.now());
|
const hierarchy = entity.polygon.hierarchy.getValue(
|
||||||
|
Cesium.JulianDate.now(),
|
||||||
|
);
|
||||||
if (hierarchy && hierarchy.positions.length > 0) {
|
if (hierarchy && hierarchy.positions.length > 0) {
|
||||||
targetPos = Cesium.BoundingSphere.fromPoints(hierarchy.positions).center;
|
targetPos = Cesium.BoundingSphere.fromPoints(
|
||||||
|
hierarchy.positions,
|
||||||
|
).center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,10 +768,18 @@ export function createLocalGeoJsonLayer({
|
|||||||
viewer.scene.screenSpaceCameraController.enableInputs = false;
|
viewer.scene.screenSpaceCameraController.enableInputs = false;
|
||||||
|
|
||||||
viewer.camera.flyTo({
|
viewer.camera.flyTo({
|
||||||
destination: Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, 5000),
|
destination: Cesium.Cartesian3.fromRadians(
|
||||||
|
carto.longitude,
|
||||||
|
carto.latitude,
|
||||||
|
5000,
|
||||||
|
),
|
||||||
duration: 1.5,
|
duration: 1.5,
|
||||||
complete: () => { viewer.scene.screenSpaceCameraController.enableInputs = true; },
|
complete: () => {
|
||||||
cancel: () => { viewer.scene.screenSpaceCameraController.enableInputs = true; },
|
viewer.scene.screenSpaceCameraController.enableInputs = true;
|
||||||
|
},
|
||||||
|
cancel: () => {
|
||||||
|
viewer.scene.screenSpaceCameraController.enableInputs = true;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -741,7 +819,10 @@ export function createLocalGeoJsonLayer({
|
|||||||
const probe = shouldRecomputeInfraLod({
|
const probe = shouldRecomputeInfraLod({
|
||||||
nowMs: now,
|
nowMs: now,
|
||||||
lastProbeMs: _lastLodProbeMs,
|
lastProbeMs: _lastLodProbeMs,
|
||||||
movedSqM: Cesium.Cartesian3.distanceSquared(cameraPos, _lastLodCameraPos),
|
movedSqM: Cesium.Cartesian3.distanceSquared(
|
||||||
|
cameraPos,
|
||||||
|
_lastLodCameraPos,
|
||||||
|
),
|
||||||
cameraHeightM: viewer.camera.positionCartographic?.height,
|
cameraHeightM: viewer.camera.positionCartographic?.height,
|
||||||
});
|
});
|
||||||
_lastLodProbeMs = probe.lastProbeMs;
|
_lastLodProbeMs = probe.lastProbeMs;
|
||||||
@@ -750,7 +831,10 @@ export function createLocalGeoJsonLayer({
|
|||||||
if (probe.recompute) _stemGeometryDirty = true;
|
if (probe.recompute) _stemGeometryDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const occluder = new Cesium.EllipsoidalOccluder(Cesium.Ellipsoid.WGS84, cameraPos);
|
const occluder = new Cesium.EllipsoidalOccluder(
|
||||||
|
Cesium.Ellipsoid.WGS84,
|
||||||
|
cameraPos,
|
||||||
|
);
|
||||||
const visibleOverlayRecords = [];
|
const visibleOverlayRecords = [];
|
||||||
const refreshStemGeometry = _stemGeometryDirty;
|
const refreshStemGeometry = _stemGeometryDirty;
|
||||||
|
|
||||||
@@ -762,7 +846,8 @@ export function createLocalGeoJsonLayer({
|
|||||||
// Capability can arrive late (WebGL context restore, a tileset that
|
// Capability can arrive late (WebGL context restore, a tileset that
|
||||||
// finally supports sampling). A parked camera has no moveEnd to
|
// finally supports sampling). A parked camera has no moveEnd to
|
||||||
// re-open a spent budget, so the false→true edge does it.
|
// re-open a spent budget, so the false→true edge does it.
|
||||||
if (canSampleGround && _lastGroundSampleCapability === false) _groundRetryArms = 0;
|
if (canSampleGround && _lastGroundSampleCapability === false)
|
||||||
|
_groundRetryArms = 0;
|
||||||
_lastGroundSampleCapability = canSampleGround;
|
_lastGroundSampleCapability = canSampleGround;
|
||||||
let groundRetryPending = false;
|
let groundRetryPending = false;
|
||||||
let groundSampleProgress = false;
|
let groundSampleProgress = false;
|
||||||
@@ -814,7 +899,8 @@ export function createLocalGeoJsonLayer({
|
|||||||
const record = _stemRecords[i];
|
const record = _stemRecords[i];
|
||||||
const isActive = !_lodComputed || _activeLodIds.has(record.id);
|
const isActive = !_lodComputed || _activeLodIds.has(record.id);
|
||||||
const isVisible = isActive && occluder.isPointVisible(record.base);
|
const isVisible = isActive && occluder.isPointVisible(record.base);
|
||||||
if (record.entity.show !== isVisible) record.entity.show = isVisible;
|
if (record.entity.show !== isVisible)
|
||||||
|
record.entity.show = isVisible;
|
||||||
|
|
||||||
// Records outside the globe-LOD active set carry no visible stem,
|
// Records outside the globe-LOD active set carry no visible stem,
|
||||||
// so skip every per-frame cost for them — geometry trig, the
|
// so skip every per-frame cost for them — geometry trig, the
|
||||||
@@ -823,20 +909,32 @@ export function createLocalGeoJsonLayer({
|
|||||||
if (!isActive) continue;
|
if (!isActive) continue;
|
||||||
|
|
||||||
const wasGroundSampled = record.groundSampled;
|
const wasGroundSampled = record.groundSampled;
|
||||||
const terrainFloorChanged = refreshLocalTerrainFloor(viewer, record);
|
const terrainFloorChanged = refreshLocalTerrainFloor(
|
||||||
|
viewer,
|
||||||
|
record,
|
||||||
|
);
|
||||||
if (refreshStemGeometry || terrainFloorChanged) {
|
if (refreshStemGeometry || terrainFloorChanged) {
|
||||||
updateLocalStemGeometry(viewer, record, now);
|
updateLocalStemGeometry(viewer, record, now);
|
||||||
} else if (canSampleGround && !record.groundSampled
|
} else if (
|
||||||
&& now - record.lastGroundSampleMs >= GROUND_SAMPLE_RETRY_MS) {
|
canSampleGround &&
|
||||||
|
!record.groundSampled &&
|
||||||
|
now - record.lastGroundSampleMs >= GROUND_SAMPLE_RETRY_MS
|
||||||
|
) {
|
||||||
// Capability first: without it the distance below is pure waste,
|
// Capability first: without it the distance below is pure waste,
|
||||||
// once per ungrounded record per walk, forever.
|
// once per ungrounded record per walk, forever.
|
||||||
const distance = Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base);
|
const distance = Cesium.Cartesian3.distance(
|
||||||
if (distance < GROUND_SAMPLE_MAX_DISTANCE_M
|
viewer.camera.positionWC,
|
||||||
&& sampleLocalGroundHeight(viewer, record, now)) {
|
record.base,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
distance < GROUND_SAMPLE_MAX_DISTANCE_M &&
|
||||||
|
sampleLocalGroundHeight(viewer, record, now)
|
||||||
|
) {
|
||||||
updateLocalStemGeometry(viewer, record, now, distance);
|
updateLocalStemGeometry(viewer, record, now, distance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!wasGroundSampled && record.groundSampled) groundSampleProgress = true;
|
if (!wasGroundSampled && record.groundSampled)
|
||||||
|
groundSampleProgress = true;
|
||||||
// Still unsampled AND close enough for a retry to succeed: this
|
// Still unsampled AND close enough for a retry to succeed: this
|
||||||
// layer has no hold and no periodic update, so under the idle
|
// layer has no hold and no periodic update, so under the idle
|
||||||
// governor the retry's preRender never arrives on a parked camera
|
// governor the retry's preRender never arrives on a parked camera
|
||||||
@@ -845,9 +943,15 @@ export function createLocalGeoJsonLayer({
|
|||||||
// Gated on a sampleable scene and in-range records only, so a far
|
// Gated on a sampleable scene and in-range records only, so a far
|
||||||
// camera (or a keyless scene) stays fully idle; the distance is
|
// camera (or a keyless scene) stays fully idle; the distance is
|
||||||
// only computed for still-unsampled stems.
|
// only computed for still-unsampled stems.
|
||||||
if (canSampleGround && !record.groundSampled && !groundRetryPending
|
if (
|
||||||
&& Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base)
|
canSampleGround &&
|
||||||
< GROUND_SAMPLE_MAX_DISTANCE_M) {
|
!record.groundSampled &&
|
||||||
|
!groundRetryPending &&
|
||||||
|
Cesium.Cartesian3.distance(
|
||||||
|
viewer.camera.positionWC,
|
||||||
|
record.base,
|
||||||
|
) < GROUND_SAMPLE_MAX_DISTANCE_M
|
||||||
|
) {
|
||||||
groundRetryPending = true;
|
groundRetryPending = true;
|
||||||
}
|
}
|
||||||
if (isVisible && record.entry) visibleOverlayRecords.push(record);
|
if (isVisible && record.entry) visibleOverlayRecords.push(record);
|
||||||
@@ -859,14 +963,17 @@ export function createLocalGeoJsonLayer({
|
|||||||
if (groundRetryPending) scheduleGroundRetryRender(viewer);
|
if (groundRetryPending) scheduleGroundRetryRender(viewer);
|
||||||
|
|
||||||
const canvas = viewer.scene.canvas;
|
const canvas = viewer.scene.canvas;
|
||||||
const cohort = selectLocalInfrastructureOverlayCohort(visibleOverlayRecords, {
|
const cohort = selectLocalInfrastructureOverlayCohort(
|
||||||
|
visibleOverlayRecords,
|
||||||
|
{
|
||||||
maxEntries: labelMax,
|
maxEntries: labelMax,
|
||||||
gridPx: labelGridPx,
|
gridPx: labelGridPx,
|
||||||
width: canvas.clientWidth || canvas.width || 0,
|
width: canvas.clientWidth || canvas.width || 0,
|
||||||
height: canvas.clientHeight || canvas.height || 0,
|
height: canvas.clientHeight || canvas.height || 0,
|
||||||
cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT,
|
cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT,
|
||||||
project: (record) => projectToWindow(viewer.scene, record.tip),
|
project: (record) => projectToWindow(viewer.scene, record.tip),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
_overlayPublisher.publish(cohort);
|
_overlayPublisher.publish(cohort);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -913,7 +1020,7 @@ export function createLocalGeoJsonLayer({
|
|||||||
_count = 0;
|
_count = 0;
|
||||||
_lastUpdate = null;
|
_lastUpdate = null;
|
||||||
_error = null;
|
_error = null;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -923,24 +1030,36 @@ function compareLocalOverlayRecords(a, b) {
|
|||||||
|
|
||||||
function insertLocalCellContender(contenders, record) {
|
function insertLocalCellContender(contenders, record) {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
while (index < contenders.length && compareLocalOverlayRecords(contenders[index], record) <= 0) {
|
while (
|
||||||
|
index < contenders.length &&
|
||||||
|
compareLocalOverlayRecords(contenders[index], record) <= 0
|
||||||
|
) {
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
contenders.splice(index, 0, record);
|
contenders.splice(index, 0, record);
|
||||||
if (contenders.length > LOCAL_OVERLAY_CELL_SURPLUS) contenders.length = LOCAL_OVERLAY_CELL_SURPLUS;
|
if (contenders.length > LOCAL_OVERLAY_CELL_SURPLUS)
|
||||||
|
contenders.length = LOCAL_OVERLAY_CELL_SURPLUS;
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshLocalTerrainFloor(viewer, record) {
|
function refreshLocalTerrainFloor(viewer, record) {
|
||||||
const globe = viewer.scene.globe;
|
const globe = viewer.scene.globe;
|
||||||
if (!record.groundSampled || !globe?.show || globe.tilesLoaded === false) return false;
|
if (!record.groundSampled || !globe?.show || globe.tilesLoaded === false)
|
||||||
if (Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base)
|
return false;
|
||||||
>= GROUND_SAMPLE_MAX_DISTANCE_M) return false;
|
if (
|
||||||
|
Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base) >=
|
||||||
|
GROUND_SAMPLE_MAX_DISTANCE_M
|
||||||
|
)
|
||||||
|
return false;
|
||||||
// Camera motion can refine terrain after the first successful depth sample.
|
// Camera motion can refine terrain after the first successful depth sample.
|
||||||
// Reuse the loaded mesh on existing bounded walks; never arm a timer or do
|
// Reuse the loaded mesh on existing bounded walks; never arm a timer or do
|
||||||
// another GPU readback just to maintain this floor. Keep roof elevations.
|
// another GPU readback just to maintain this floor. Keep roof elevations.
|
||||||
const height = globe.getHeight?.(record.carto);
|
const height = globe.getHeight?.(record.carto);
|
||||||
if (!Number.isFinite(height) || Math.abs(height) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M
|
if (
|
||||||
|| height <= record.groundHeight) return false;
|
!Number.isFinite(height) ||
|
||||||
|
Math.abs(height) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M ||
|
||||||
|
height <= record.groundHeight
|
||||||
|
)
|
||||||
|
return false;
|
||||||
setLocalGroundHeight(record, height);
|
setLocalGroundHeight(record, height);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -957,13 +1076,22 @@ function sampleLocalGroundHeight(viewer, record, now) {
|
|||||||
} catch {
|
} catch {
|
||||||
return false; // tiles not ready; retry on a later bounded update
|
return false; // tiles not ready; retry on a later bounded update
|
||||||
}
|
}
|
||||||
if (!Number.isFinite(sampled) || Math.abs(sampled) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M) return false;
|
if (
|
||||||
|
!Number.isFinite(sampled) ||
|
||||||
|
Math.abs(sampled) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M
|
||||||
|
)
|
||||||
|
return false;
|
||||||
// A coarse scene-depth sample can lie below the terrain already loaded by
|
// A coarse scene-depth sample can lie below the terrain already loaded by
|
||||||
// the visible globe. Keep that terrain as a floor while allowing roofs and
|
// the visible globe. Keep that terrain as a floor while allowing roofs and
|
||||||
// valid below-sea-level elevations. Photoreal scenes hide the globe, so its
|
// valid below-sea-level elevations. Photoreal scenes hide the globe, so its
|
||||||
// inactive terrain must not constrain their geometry.
|
// inactive terrain must not constrain their geometry.
|
||||||
const terrainHeight = globe?.show ? globe.getHeight?.(record.carto) : undefined;
|
const terrainHeight = globe?.show
|
||||||
if (Number.isFinite(terrainHeight) && Math.abs(terrainHeight) <= GROUND_SAMPLE_MAX_ABS_HEIGHT_M) {
|
? globe.getHeight?.(record.carto)
|
||||||
|
: undefined;
|
||||||
|
if (
|
||||||
|
Number.isFinite(terrainHeight) &&
|
||||||
|
Math.abs(terrainHeight) <= GROUND_SAMPLE_MAX_ABS_HEIGHT_M
|
||||||
|
) {
|
||||||
sampled = Math.max(sampled, terrainHeight);
|
sampled = Math.max(sampled, terrainHeight);
|
||||||
}
|
}
|
||||||
record.groundSampled = true;
|
record.groundSampled = true;
|
||||||
@@ -987,12 +1115,13 @@ function updateLocalStemGeometry(viewer, record, now, knownDistance = null) {
|
|||||||
const distance = Number.isFinite(knownDistance)
|
const distance = Number.isFinite(knownDistance)
|
||||||
? knownDistance
|
? knownDistance
|
||||||
: Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base);
|
: Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base);
|
||||||
if (distance < GROUND_SAMPLE_MAX_DISTANCE_M) sampleLocalGroundHeight(viewer, record, now);
|
if (distance < GROUND_SAMPLE_MAX_DISTANCE_M)
|
||||||
|
sampleLocalGroundHeight(viewer, record, now);
|
||||||
// Keep the intended screen-size scaling in close-up views too. A 5 km
|
// Keep the intended screen-size scaling in close-up views too. A 5 km
|
||||||
// minimum made a marker hundreds of metres tall beside a nearby building.
|
// minimum made a marker hundreds of metres tall beside a nearby building.
|
||||||
const effectiveDistance = Math.max(distance, 1);
|
const effectiveDistance = Math.max(distance, 1);
|
||||||
const canvasHeight = viewer.scene.canvas.clientHeight || 1080;
|
const canvasHeight = viewer.scene.canvas.clientHeight || 1080;
|
||||||
const fov = viewer.camera.frustum.fov || (Math.PI / 3);
|
const fov = viewer.camera.frustum.fov || Math.PI / 3;
|
||||||
const targetPx = 65;
|
const targetPx = 65;
|
||||||
const fovFactor = 2 * Math.tan(fov / 2) * (targetPx / canvasHeight);
|
const fovFactor = 2 * Math.tan(fov / 2) * (targetPx / canvasHeight);
|
||||||
const tipHeight = record.groundHeight + effectiveDistance * fovFactor;
|
const tipHeight = record.groundHeight + effectiveDistance * fovFactor;
|
||||||
@@ -1003,12 +1132,16 @@ function updateLocalStemGeometry(viewer, record, now, knownDistance = null) {
|
|||||||
Cesium.Ellipsoid.WGS84,
|
Cesium.Ellipsoid.WGS84,
|
||||||
record.nextTip,
|
record.nextTip,
|
||||||
);
|
);
|
||||||
if (Cesium.Cartesian3.distanceSquared(record.tip, record.nextTip) <= LOCAL_STEM_TIP_EPSILON_SQ) {
|
if (
|
||||||
|
Cesium.Cartesian3.distanceSquared(record.tip, record.nextTip) <=
|
||||||
|
LOCAL_STEM_TIP_EPSILON_SQ
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Cesium.Cartesian3.clone(record.nextTip, record.tip);
|
Cesium.Cartesian3.clone(record.nextTip, record.tip);
|
||||||
record.stemPositionBufferIndex = 1 - record.stemPositionBufferIndex;
|
record.stemPositionBufferIndex = 1 - record.stemPositionBufferIndex;
|
||||||
const stemPositions = record.stemPositionBuffers[record.stemPositionBufferIndex];
|
const stemPositions =
|
||||||
|
record.stemPositionBuffers[record.stemPositionBufferIndex];
|
||||||
stemPositions[0] = record.base;
|
stemPositions[0] = record.base;
|
||||||
stemPositions[1] = record.tip;
|
stemPositions[1] = record.tip;
|
||||||
record.entity.position.setValue(record.tip);
|
record.entity.position.setValue(record.tip);
|
||||||
@@ -1050,7 +1183,8 @@ function labelPriorityFromProperties(props, layerId) {
|
|||||||
|
|
||||||
function propertyObject(entity) {
|
function propertyObject(entity) {
|
||||||
const source = entity?.properties;
|
const source = entity?.properties;
|
||||||
const raw = typeof source?.getValue === 'function'
|
const raw =
|
||||||
|
typeof source?.getValue === 'function'
|
||||||
? source.getValue(Cesium.JulianDate.now())
|
? source.getValue(Cesium.JulianDate.now())
|
||||||
: source || {};
|
: source || {};
|
||||||
return unwrapProperties(raw);
|
return unwrapProperties(raw);
|
||||||
@@ -1061,7 +1195,8 @@ function unwrapProperties(value) {
|
|||||||
if (Array.isArray(value)) return value.map(unwrapProperties);
|
if (Array.isArray(value)) return value.map(unwrapProperties);
|
||||||
const out = {};
|
const out = {};
|
||||||
for (const [key, entry] of Object.entries(value)) {
|
for (const [key, entry] of Object.entries(value)) {
|
||||||
out[key] = entry && typeof entry.getValue === 'function'
|
out[key] =
|
||||||
|
entry && typeof entry.getValue === 'function'
|
||||||
? unwrapProperties(entry.getValue(Cesium.JulianDate.now()))
|
? unwrapProperties(entry.getValue(Cesium.JulianDate.now()))
|
||||||
: unwrapProperties(entry);
|
: unwrapProperties(entry);
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-32
@@ -58,8 +58,10 @@ export function infraLodBudget(cameraHeightM) {
|
|||||||
const height = Number.isFinite(cameraHeightM)
|
const height = Number.isFinite(cameraHeightM)
|
||||||
? Math.max(0, cameraHeightM)
|
? Math.max(0, cameraHeightM)
|
||||||
: INFRA_LOD_GLOBAL_HEIGHT_M;
|
: INFRA_LOD_GLOBAL_HEIGHT_M;
|
||||||
if (height >= INFRA_LOD_GLOBAL_HEIGHT_M) return { activeLimit: INFRA_LOD_ACTIVE_MIN };
|
if (height >= INFRA_LOD_GLOBAL_HEIGHT_M)
|
||||||
if (height >= INFRA_LOD_REGIONAL_HEIGHT_M) return { activeLimit: INFRA_LOD_ACTIVE_MID };
|
return { activeLimit: INFRA_LOD_ACTIVE_MIN };
|
||||||
|
if (height >= INFRA_LOD_REGIONAL_HEIGHT_M)
|
||||||
|
return { activeLimit: INFRA_LOD_ACTIVE_MID };
|
||||||
return { activeLimit: INFRA_LOD_ACTIVE_MAX };
|
return { activeLimit: INFRA_LOD_ACTIVE_MAX };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,24 +106,33 @@ export const INFRA_LOD_MAX_DISTANCE_PENALTY = 200;
|
|||||||
* @param {number} [options.maxDistancePenalty]
|
* @param {number} [options.maxDistancePenalty]
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
export function infraRankScore(priority, distanceM, isIncumbent, {
|
export function infraRankScore(
|
||||||
|
priority,
|
||||||
|
distanceM,
|
||||||
|
isIncumbent,
|
||||||
|
{
|
||||||
incumbentBonus = INFRA_LOD_INCUMBENT_BONUS,
|
incumbentBonus = INFRA_LOD_INCUMBENT_BONUS,
|
||||||
farM = INFRA_LOD_FAR_M,
|
farM = INFRA_LOD_FAR_M,
|
||||||
maxDistancePenalty = INFRA_LOD_MAX_DISTANCE_PENALTY,
|
maxDistancePenalty = INFRA_LOD_MAX_DISTANCE_PENALTY,
|
||||||
} = {}) {
|
} = {},
|
||||||
|
) {
|
||||||
const p = Number.isFinite(priority) ? priority : 0;
|
const p = Number.isFinite(priority) ? priority : 0;
|
||||||
const far = Number.isFinite(farM) && farM > 0 ? farM : INFRA_LOD_FAR_M;
|
const far = Number.isFinite(farM) && farM > 0 ? farM : INFRA_LOD_FAR_M;
|
||||||
const d = Number.isFinite(distanceM) && distanceM >= 0 ? distanceM : far;
|
const d = Number.isFinite(distanceM) && distanceM >= 0 ? distanceM : far;
|
||||||
const penaltyCap = Number.isFinite(maxDistancePenalty) && maxDistancePenalty >= 0
|
const penaltyCap =
|
||||||
|
Number.isFinite(maxDistancePenalty) && maxDistancePenalty >= 0
|
||||||
? maxDistancePenalty
|
? maxDistancePenalty
|
||||||
: INFRA_LOD_MAX_DISTANCE_PENALTY;
|
: INFRA_LOD_MAX_DISTANCE_PENALTY;
|
||||||
const bonus = isIncumbent && Number.isFinite(incumbentBonus) ? incumbentBonus : 0;
|
const bonus =
|
||||||
|
isIncumbent && Number.isFinite(incumbentBonus) ? incumbentBonus : 0;
|
||||||
return p + bonus - penaltyCap * Math.min(1, d / far);
|
return p + bonus - penaltyCap * Math.min(1, d / far);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Infinity-safe distance for a total sort order. */
|
/** Infinity-safe distance for a total sort order. */
|
||||||
function sortableDistance(distanceM) {
|
function sortableDistance(distanceM) {
|
||||||
return Number.isFinite(distanceM) && distanceM >= 0 ? distanceM : Number.MAX_VALUE;
|
return Number.isFinite(distanceM) && distanceM >= 0
|
||||||
|
? distanceM
|
||||||
|
: Number.MAX_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,35 +153,49 @@ function sortableDistance(distanceM) {
|
|||||||
* @param {Iterable<string>|Set<string>} [options.incumbentIds] Ids that currently hold an active stem.
|
* @param {Iterable<string>|Set<string>} [options.incumbentIds] Ids that currently hold an active stem.
|
||||||
* @returns {{activeIds:string[], budget:{activeLimit:number}}}
|
* @returns {{activeIds:string[], budget:{activeLimit:number}}}
|
||||||
*/
|
*/
|
||||||
export function selectInfraLod(candidates, { cameraHeightM, incumbentIds } = {}) {
|
export function selectInfraLod(
|
||||||
|
candidates,
|
||||||
|
{ cameraHeightM, incumbentIds } = {},
|
||||||
|
) {
|
||||||
const budget = infraLodBudget(cameraHeightM);
|
const budget = infraLodBudget(cameraHeightM);
|
||||||
const incumbents = incumbentIds instanceof Set ? incumbentIds : new Set(incumbentIds || []);
|
const incumbents =
|
||||||
|
incumbentIds instanceof Set ? incumbentIds : new Set(incumbentIds || []);
|
||||||
|
|
||||||
const byId = new Map();
|
const byId = new Map();
|
||||||
for (const candidate of Array.isArray(candidates) ? candidates : []) {
|
for (const candidate of Array.isArray(candidates) ? candidates : []) {
|
||||||
if (!candidate || typeof candidate.id !== 'string' || !candidate.id) continue;
|
if (!candidate || typeof candidate.id !== 'string' || !candidate.id)
|
||||||
|
continue;
|
||||||
if (candidate.inView !== true) continue;
|
if (candidate.inView !== true) continue;
|
||||||
const distanceM = Number.isFinite(candidate.distanceM) && candidate.distanceM >= 0
|
const distanceM =
|
||||||
|
Number.isFinite(candidate.distanceM) && candidate.distanceM >= 0
|
||||||
? candidate.distanceM
|
? candidate.distanceM
|
||||||
: Number.POSITIVE_INFINITY;
|
: Number.POSITIVE_INFINITY;
|
||||||
const normalized = {
|
const normalized = {
|
||||||
id: candidate.id,
|
id: candidate.id,
|
||||||
distanceM,
|
distanceM,
|
||||||
score: infraRankScore(candidate.priority, candidate.distanceM, incumbents.has(candidate.id)),
|
score: infraRankScore(
|
||||||
|
candidate.priority,
|
||||||
|
candidate.distanceM,
|
||||||
|
incumbents.has(candidate.id),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
const current = byId.get(normalized.id);
|
const current = byId.get(normalized.id);
|
||||||
if (!current
|
if (
|
||||||
|| normalized.score > current.score
|
!current ||
|
||||||
|| (normalized.score === current.score && normalized.distanceM < current.distanceM)) {
|
normalized.score > current.score ||
|
||||||
|
(normalized.score === current.score &&
|
||||||
|
normalized.distanceM < current.distanceM)
|
||||||
|
) {
|
||||||
byId.set(normalized.id, normalized);
|
byId.set(normalized.id, normalized);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ranked = [...byId.values()].sort((a, b) => (
|
const ranked = [...byId.values()].sort(
|
||||||
b.score - a.score
|
(a, b) =>
|
||||||
|| sortableDistance(a.distanceM) - sortableDistance(b.distanceM)
|
b.score - a.score ||
|
||||||
|| a.id.localeCompare(b.id)
|
sortableDistance(a.distanceM) - sortableDistance(b.distanceM) ||
|
||||||
));
|
a.id.localeCompare(b.id),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeIds: ranked.slice(0, budget.activeLimit).map((entry) => entry.id),
|
activeIds: ranked.slice(0, budget.activeLimit).map((entry) => entry.id),
|
||||||
@@ -220,7 +245,9 @@ export function applyInfraEvictionGrace({
|
|||||||
graceMs = INFRA_LOD_GRACE_MS,
|
graceMs = INFRA_LOD_GRACE_MS,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const selectedSet = new Set(
|
const selectedSet = new Set(
|
||||||
(Array.isArray(selectedIds) ? selectedIds : []).filter((id) => typeof id === 'string' && id),
|
(Array.isArray(selectedIds) ? selectedIds : []).filter(
|
||||||
|
(id) => typeof id === 'string' && id,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const keepIds = [...selectedSet];
|
const keepIds = [...selectedSet];
|
||||||
const evictIds = [];
|
const evictIds = [];
|
||||||
@@ -241,8 +268,13 @@ export function applyInfraEvictionGrace({
|
|||||||
|
|
||||||
// Under cap pressure, grace-period stems go first: oldest-in-grace first
|
// Under cap pressure, grace-period stems go first: oldest-in-grace first
|
||||||
// (longest chance to return), then more misses, then id for determinism.
|
// (longest chance to return), then more misses, then id for determinism.
|
||||||
graced.sort((a, b) => a.since - b.since || b.misses - a.misses || a.id.localeCompare(b.id));
|
graced.sort(
|
||||||
const cap = Number.isFinite(activeLimit) ? Math.max(0, Math.floor(activeLimit)) : INFRA_LOD_ACTIVE_MAX;
|
(a, b) =>
|
||||||
|
a.since - b.since || b.misses - a.misses || a.id.localeCompare(b.id),
|
||||||
|
);
|
||||||
|
const cap = Number.isFinite(activeLimit)
|
||||||
|
? Math.max(0, Math.floor(activeLimit))
|
||||||
|
: INFRA_LOD_ACTIVE_MAX;
|
||||||
const capacity = Math.max(0, cap - keepIds.length);
|
const capacity = Math.max(0, cap - keepIds.length);
|
||||||
const overflow = Math.max(0, graced.length - capacity);
|
const overflow = Math.max(0, graced.length - capacity);
|
||||||
for (let i = 0; i < graced.length; i++) {
|
for (let i = 0; i < graced.length; i++) {
|
||||||
@@ -251,7 +283,10 @@ export function applyInfraEvictionGrace({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
keepIds.push(graced[i].id);
|
keepIds.push(graced[i].id);
|
||||||
nextGrace.set(graced[i].id, { misses: graced[i].misses, since: graced[i].since });
|
nextGrace.set(graced[i].id, {
|
||||||
|
misses: graced[i].misses,
|
||||||
|
since: graced[i].since,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { keepIds, evictIds, graceState: nextGrace };
|
return { keepIds, evictIds, graceState: nextGrace };
|
||||||
@@ -307,13 +342,21 @@ export const INFRA_LOD_MOTION_EPSILON_MIN_M = 250;
|
|||||||
* @param {number} [options.minM]
|
* @param {number} [options.minM]
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
export function infraLodMotionEpsilonM(cameraHeightM, {
|
export function infraLodMotionEpsilonM(
|
||||||
|
cameraHeightM,
|
||||||
|
{
|
||||||
ratio = INFRA_LOD_MOTION_EPSILON_RATIO,
|
ratio = INFRA_LOD_MOTION_EPSILON_RATIO,
|
||||||
minM = INFRA_LOD_MOTION_EPSILON_MIN_M,
|
minM = INFRA_LOD_MOTION_EPSILON_MIN_M,
|
||||||
} = {}) {
|
} = {},
|
||||||
const floor = Number.isFinite(minM) && minM >= 0 ? minM : INFRA_LOD_MOTION_EPSILON_MIN_M;
|
) {
|
||||||
const scale = Number.isFinite(ratio) && ratio >= 0 ? ratio : INFRA_LOD_MOTION_EPSILON_RATIO;
|
const floor =
|
||||||
const height = Number.isFinite(cameraHeightM) && cameraHeightM > 0 ? cameraHeightM : 0;
|
Number.isFinite(minM) && minM >= 0 ? minM : INFRA_LOD_MOTION_EPSILON_MIN_M;
|
||||||
|
const scale =
|
||||||
|
Number.isFinite(ratio) && ratio >= 0
|
||||||
|
? ratio
|
||||||
|
: INFRA_LOD_MOTION_EPSILON_RATIO;
|
||||||
|
const height =
|
||||||
|
Number.isFinite(cameraHeightM) && cameraHeightM > 0 ? cameraHeightM : 0;
|
||||||
return Math.max(floor, height * scale);
|
return Math.max(floor, height * scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,14 +398,18 @@ export function shouldRecomputeInfraLod({
|
|||||||
motionEpsilonM,
|
motionEpsilonM,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const now = Number.isFinite(nowMs) ? nowMs : 0;
|
const now = Number.isFinite(nowMs) ? nowMs : 0;
|
||||||
const last = Number.isFinite(lastProbeMs) ? lastProbeMs : Number.NEGATIVE_INFINITY;
|
const last = Number.isFinite(lastProbeMs)
|
||||||
const interval = Number.isFinite(probeIntervalMs) && probeIntervalMs >= 0
|
? lastProbeMs
|
||||||
|
: Number.NEGATIVE_INFINITY;
|
||||||
|
const interval =
|
||||||
|
Number.isFinite(probeIntervalMs) && probeIntervalMs >= 0
|
||||||
? probeIntervalMs
|
? probeIntervalMs
|
||||||
: INFRA_LOD_MOTION_PROBE_INTERVAL_MS;
|
: INFRA_LOD_MOTION_PROBE_INTERVAL_MS;
|
||||||
if (now - last < interval) return { recompute: false, lastProbeMs: last };
|
if (now - last < interval) return { recompute: false, lastProbeMs: last };
|
||||||
|
|
||||||
const movedSq = Number.isFinite(movedSqM) && movedSqM > 0 ? movedSqM : 0;
|
const movedSq = Number.isFinite(movedSqM) && movedSqM > 0 ? movedSqM : 0;
|
||||||
const epsilon = Number.isFinite(motionEpsilonM) && motionEpsilonM >= 0
|
const epsilon =
|
||||||
|
Number.isFinite(motionEpsilonM) && motionEpsilonM >= 0
|
||||||
? motionEpsilonM
|
? motionEpsilonM
|
||||||
: infraLodMotionEpsilonM(cameraHeightM);
|
: infraLodMotionEpsilonM(cameraHeightM);
|
||||||
return { recompute: movedSq > epsilon * epsilon, lastProbeMs: now };
|
return { recompute: movedSq > epsilon * epsilon, lastProbeMs: now };
|
||||||
|
|||||||
Reference in new issue
Block a user