mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
Retain the Nepal locator presentation and draw it on either globe
This commit is contained in:
1 parent
605b9490f3
commit
811bcaff85
11 files changed
+409
-57
No files matched your search
@@ -3679,6 +3679,7 @@
|
|||||||
"src/layers/shared/render/navaraStems.js",
|
"src/layers/shared/render/navaraStems.js",
|
||||||
"src/layers/submarineCables/render/navara.js",
|
"src/layers/submarineCables/render/navara.js",
|
||||||
"src/layers/directions/render/navara.js",
|
"src/layers/directions/render/navara.js",
|
||||||
|
"src/layers/shared/render/navaraStory.js",
|
||||||
"src/layers/weather/render/navara.js",
|
"src/layers/weather/render/navara.js",
|
||||||
"src/layers/weather/render/navaraSurface.js",
|
"src/layers/weather/render/navaraSurface.js",
|
||||||
"src/layers/wind/render/navara.js",
|
"src/layers/wind/render/navara.js",
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { fromDegrees } from '../src/geo/cartesian3.js';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
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=28.3326&lon=85.4848&alt=18000&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]);
|
||||||
|
});
|
||||||
|
p.on('console', (msg) => {
|
||||||
|
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
|
||||||
|
errors.push(msg.text());
|
||||||
|
});
|
||||||
|
await p.goto(base.href);
|
||||||
|
const ready = async (kind) => {
|
||||||
|
await p.waitForFunction(
|
||||||
|
(k) =>
|
||||||
|
window.__godsEyeView?.engine.kind === k &&
|
||||||
|
!document.querySelector('#globe-engine')?.disabled &&
|
||||||
|
!document.querySelector('.engine-switch-frame') &&
|
||||||
|
document.querySelector('#loading-screen.hidden'),
|
||||||
|
{ timeout: 90000 },
|
||||||
|
kind,
|
||||||
|
);
|
||||||
|
await p.evaluate(
|
||||||
|
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await ready('navara');
|
||||||
|
await p.evaluate(async () => {
|
||||||
|
const { tileset, dataManager } = window.__godsEyeView;
|
||||||
|
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
|
||||||
|
throw Error('Keyless preview required');
|
||||||
|
window.qaLocator = dataManager.layers.get('bhote-koshi-locator').module;
|
||||||
|
window.qaLocator.setParams({ presentation: 'bhote-koshi-path-overview' });
|
||||||
|
await dataManager.setEnabled('bhote-koshi-locator', true, {
|
||||||
|
origin: 'user',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
for (const kind of ['navara', 'cesium', 'navara']) {
|
||||||
|
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||||
|
await p.select('#globe-engine', kind);
|
||||||
|
await ready(kind);
|
||||||
|
}
|
||||||
|
const { same, stats, params, at } = await p.evaluate(
|
||||||
|
(world) => {
|
||||||
|
const { engine, dataManager } = window.__godsEyeView,
|
||||||
|
mod = dataManager.layers.get('bhote-koshi-locator').module;
|
||||||
|
const pos = engine.projector.worldToWindow(world, {}),
|
||||||
|
box = engine.host.canvas.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
same: mod === window.qaLocator,
|
||||||
|
stats: mod.getStats(),
|
||||||
|
params: mod.getParams(),
|
||||||
|
at: { x: pos.x + box.left, y: pos.y + box.top },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
fromDegrees(85.47741, 28.33299, 1600),
|
||||||
|
);
|
||||||
|
assert.equal(same, true);
|
||||||
|
assert.equal(stats.count, 1);
|
||||||
|
assert.equal(params.presentation, 'bhote-koshi-path-overview');
|
||||||
|
// Look for the cyan river in a scene crop away from the UI rails.
|
||||||
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
const crop = async () =>
|
||||||
|
sharp(await p.screenshot())
|
||||||
|
.extract({ left: 400, top: 320, width: 230, height: 180 })
|
||||||
|
.removeAlpha()
|
||||||
|
.raw()
|
||||||
|
.toBuffer();
|
||||||
|
const rendered = await crop();
|
||||||
|
await p.evaluate(() =>
|
||||||
|
window.qaLocator.setParams({ presentation: 'nepal-context' }),
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
const baseline = await crop();
|
||||||
|
let added = 0;
|
||||||
|
for (let i = 0; i < rendered.length; i += 3) {
|
||||||
|
const contrast = rendered[i + 1] - rendered[i],
|
||||||
|
previous = baseline[i + 1] - baseline[i];
|
||||||
|
if (
|
||||||
|
rendered[i + 1] > 140 &&
|
||||||
|
rendered[i + 2] > 140 &&
|
||||||
|
contrast > 35 &&
|
||||||
|
contrast - previous > 18
|
||||||
|
)
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
kind,
|
||||||
|
stats.coverage,
|
||||||
|
'cyan pixels added by the path',
|
||||||
|
added,
|
||||||
|
at,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
added > 50,
|
||||||
|
'visible river differs from the same scene without a path',
|
||||||
|
);
|
||||||
|
await p.evaluate(() =>
|
||||||
|
window.qaLocator.setParams({ presentation: 'bhote-koshi-path-overview' }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await p.evaluate(() => {
|
||||||
|
window.qaLocator.setParams({ presentation: 'bhote-koshi-incident-places' });
|
||||||
|
window.qaLocator.setParams({ presentation: 'bhote-koshi-city-context' });
|
||||||
|
window.qaLocator.setParams({
|
||||||
|
presentation: 'bhote-koshi-trigger-record',
|
||||||
|
preview: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
await p.evaluate(() =>
|
||||||
|
window.__godsEyeView.dataManager.setEnabled('bhote-koshi-locator', false, {
|
||||||
|
origin: 'user',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log(
|
||||||
|
'PASS Nepal locator flood path and presentation retained, trigger and places update; both engines',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await b.close();
|
||||||
|
}
|
||||||
@@ -164,9 +164,10 @@ export function createApplicationCatalog({
|
|||||||
[
|
[
|
||||||
'bhote-koshi-locator',
|
'bhote-koshi-locator',
|
||||||
() =>
|
() =>
|
||||||
createBhoteKoshiLocatorLayer({
|
sessionModel('bhote-koshi-locator', () => createBhoteKoshiLocatorLayer({
|
||||||
boundaryResolver: nepalBoundaryResolver,
|
boundaryResolver: nepalBoundaryResolver,
|
||||||
}),
|
createNativeRenderer: (engine, data) => rendererFor('bhote-koshi-locator', engine, { data }),
|
||||||
|
})),
|
||||||
],
|
],
|
||||||
['flights', flights],
|
['flights', flights],
|
||||||
['military', military],
|
['military', military],
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ test('the Navara mode registers migrated renderers once', async () => {
|
|||||||
.available()
|
.available()
|
||||||
.map(({ id }) => id),
|
.map(({ id }) => id),
|
||||||
[
|
[
|
||||||
|
'bhote-koshi-locator',
|
||||||
'flights',
|
'flights',
|
||||||
'military',
|
'military',
|
||||||
'local-adsb',
|
'local-adsb',
|
||||||
|
|||||||
@@ -120,6 +120,10 @@ export async function registerNavaraLayerRenderers() {
|
|||||||
return (engine, options) => createWeatherNavaraRenderer(engine, navaraViews.get(engine), options);
|
return (engine, options) => createWeatherNavaraRenderer(engine, navaraViews.get(engine), options);
|
||||||
},
|
},
|
||||||
])),
|
])),
|
||||||
|
'bhote-koshi-locator': async () => {
|
||||||
|
const { drawStorySource } = await import('../layers/shared/render/navaraStory.js');
|
||||||
|
return (engine, { data }) => drawStorySource(engine, navaraViews.get(engine), data);
|
||||||
|
},
|
||||||
directions: async () => {
|
directions: async () => {
|
||||||
const { createDirectionsNavaraRenderer } = await import('../layers/directions/render/navara.js');
|
const { createDirectionsNavaraRenderer } = await import('../layers/directions/render/navara.js');
|
||||||
return (engine) => createDirectionsNavaraRenderer(engine, navaraViews.get(engine));
|
return (engine) => createDirectionsNavaraRenderer(engine, navaraViews.get(engine));
|
||||||
@@ -258,6 +262,7 @@ export async function createNavaraScene({
|
|||||||
view: globe.view,
|
view: globe.view,
|
||||||
alignDaylightToRestoredView: globe.alignDaylightToRestoredView,
|
alignDaylightToRestoredView: globe.alignDaylightToRestoredView,
|
||||||
MeshDesc: globe.MeshDesc,
|
MeshDesc: globe.MeshDesc,
|
||||||
|
NavaraColor: globe.NavaraColor,
|
||||||
EffectDesc: globe.EffectDesc,
|
EffectDesc: globe.EffectDesc,
|
||||||
NavaraEffect: globe.NavaraEffect,
|
NavaraEffect: globe.NavaraEffect,
|
||||||
DrapedMesh: globe.DrapedMesh,
|
DrapedMesh: globe.DrapedMesh,
|
||||||
|
|||||||
@@ -276,8 +276,9 @@ export function largestGeoJsonRing(geojson) {
|
|||||||
*/
|
*/
|
||||||
export function createBhoteKoshiLocatorLayer({
|
export function createBhoteKoshiLocatorLayer({
|
||||||
boundaryResolver = DEFAULT_BOUNDARY_RESOLVER,
|
boundaryResolver = DEFAULT_BOUNDARY_RESOLVER,
|
||||||
|
createNativeRenderer,
|
||||||
overlayHost = DEFAULT_OVERLAY_HOST,
|
overlayHost = DEFAULT_OVERLAY_HOST,
|
||||||
requestRender = governorRequestRender,
|
requestRender: requestSceneRender = governorRequestRender,
|
||||||
now = () => performance.now(),
|
now = () => performance.now(),
|
||||||
scheduleFrame = (callback) => requestAnimationFrame(callback),
|
scheduleFrame = (callback) => requestAnimationFrame(callback),
|
||||||
cancelFrame = (handle) => cancelAnimationFrame(handle),
|
cancelFrame = (handle) => cancelAnimationFrame(handle),
|
||||||
@@ -285,6 +286,12 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
cancelDelay = (handle) => clearTimeout(handle),
|
cancelDelay = (handle) => clearTimeout(handle),
|
||||||
} = {}) {
|
} = {}) {
|
||||||
let _viewer = null;
|
let _viewer = null;
|
||||||
|
let _engine = null;
|
||||||
|
let _native = null;
|
||||||
|
let _pausedApproach = null;
|
||||||
|
const cameraForScene = () => _engine?.camera ?? _viewer?.camera;
|
||||||
|
const creditTarget = () => _engine ?? _viewer;
|
||||||
|
const requestRender = reason => _engine?.kind === 'navara' ? _engine.render.request(reason) : requestSceneRender(reason);
|
||||||
let _dataSource = null;
|
let _dataSource = null;
|
||||||
let _enabled = false;
|
let _enabled = false;
|
||||||
let _boundaryReady = false;
|
let _boundaryReady = false;
|
||||||
@@ -332,7 +339,7 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
_removeTriggerMoveEnd = null;
|
_removeTriggerMoveEnd = null;
|
||||||
const wasInFlight = _triggerApproachInFlight;
|
const wasInFlight = _triggerApproachInFlight;
|
||||||
_triggerApproachInFlight = false;
|
_triggerApproachInFlight = false;
|
||||||
if (wasInFlight) _viewer?.camera?.cancelFlight?.();
|
if (wasInFlight) cameraForScene()?.cancelFlight?.();
|
||||||
if (!preserveCallout) _triggerCalloutStartedAt = Number.POSITIVE_INFINITY;
|
if (!preserveCallout) _triggerCalloutStartedAt = Number.POSITIVE_INFINITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,6 +356,8 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
function removeDataSource() {
|
function removeDataSource() {
|
||||||
stopAnimations();
|
stopAnimations();
|
||||||
overlayHost.clearSource(BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID);
|
overlayHost.clearSource(BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID);
|
||||||
|
_native?.destroy();
|
||||||
|
_native = null;
|
||||||
if (_dataSource && _viewer?.dataSources)
|
if (_dataSource && _viewer?.dataSources)
|
||||||
_viewer.dataSources.remove(_dataSource, true);
|
_viewer.dataSources.remove(_dataSource, true);
|
||||||
_dataSource = null;
|
_dataSource = null;
|
||||||
@@ -618,9 +627,16 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
_removeTriggerMoveEnd = null;
|
_removeTriggerMoveEnd = null;
|
||||||
_triggerCalloutStartedAt = now();
|
_triggerCalloutStartedAt = now();
|
||||||
requestRender('bhote-koshi-trigger-record-reveal');
|
requestRender('bhote-koshi-trigger-record-reveal');
|
||||||
const camera = _viewer?.camera;
|
flyTriggerStage(attempt, 'flying', TRIGGER_APPROACH_DURATION_SEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flyTriggerStage(attempt, phase, duration) {
|
||||||
|
const camera = cameraForScene();
|
||||||
if (!camera?.flyTo) return;
|
if (!camera?.flyTo) return;
|
||||||
const generation = _generation;
|
const orbit = phase === 'orbiting';
|
||||||
|
const pose = orbit ? TRIGGER_ORBIT_CAMERA : TRIGGER_APPROACH_CAMERA;
|
||||||
|
attempt.phase = phase;
|
||||||
|
attempt.endsAt = now() + duration * 1000;
|
||||||
_triggerApproachInFlight = true;
|
_triggerApproachInFlight = true;
|
||||||
const finish = () => {
|
const finish = () => {
|
||||||
if (_triggerApproachAttempt !== attempt) return;
|
if (_triggerApproachAttempt !== attempt) return;
|
||||||
@@ -628,46 +644,18 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
_triggerApproachInFlight = false;
|
_triggerApproachInFlight = false;
|
||||||
};
|
};
|
||||||
camera.flyTo({
|
camera.flyTo({
|
||||||
destination: Cesium.Cartesian3.fromDegrees(
|
destination: Cesium.Cartesian3.fromDegrees(pose.lon, pose.lat, pose.alt),
|
||||||
TRIGGER_APPROACH_CAMERA.lon,
|
|
||||||
TRIGGER_APPROACH_CAMERA.lat,
|
|
||||||
TRIGGER_APPROACH_CAMERA.alt,
|
|
||||||
),
|
|
||||||
orientation: {
|
orientation: {
|
||||||
heading: Cesium.Math.toRadians(TRIGGER_APPROACH_CAMERA.heading),
|
heading: Cesium.Math.toRadians(pose.heading),
|
||||||
pitch: Cesium.Math.toRadians(TRIGGER_APPROACH_CAMERA.pitch),
|
pitch: Cesium.Math.toRadians(pose.pitch),
|
||||||
roll: Cesium.Math.toRadians(TRIGGER_APPROACH_CAMERA.roll),
|
roll: Cesium.Math.toRadians(pose.roll),
|
||||||
},
|
},
|
||||||
duration: TRIGGER_APPROACH_DURATION_SEC,
|
duration,
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
easingFunction: orbit ? Cesium.EasingFunction.SINUSOIDAL_IN_OUT : Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||||
complete: () => {
|
complete: () => {
|
||||||
if (_triggerApproachAttempt !== attempt || attempt.phase !== 'flying')
|
if (_triggerApproachAttempt !== attempt) return;
|
||||||
return;
|
if (orbit || !_enabled || _presentation !== BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION) finish();
|
||||||
if (
|
else flyTriggerStage(attempt, 'orbiting', TRIGGER_ORBIT_DURATION_SEC);
|
||||||
!_enabled ||
|
|
||||||
generation !== _generation ||
|
|
||||||
_presentation !== BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION
|
|
||||||
) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
attempt.phase = 'orbiting';
|
|
||||||
camera.flyTo({
|
|
||||||
destination: Cesium.Cartesian3.fromDegrees(
|
|
||||||
TRIGGER_ORBIT_CAMERA.lon,
|
|
||||||
TRIGGER_ORBIT_CAMERA.lat,
|
|
||||||
TRIGGER_ORBIT_CAMERA.alt,
|
|
||||||
),
|
|
||||||
orientation: {
|
|
||||||
heading: Cesium.Math.toRadians(TRIGGER_ORBIT_CAMERA.heading),
|
|
||||||
pitch: Cesium.Math.toRadians(TRIGGER_ORBIT_CAMERA.pitch),
|
|
||||||
roll: Cesium.Math.toRadians(TRIGGER_ORBIT_CAMERA.roll),
|
|
||||||
},
|
|
||||||
duration: TRIGGER_ORBIT_DURATION_SEC,
|
|
||||||
easingFunction: Cesium.EasingFunction.SINUSOIDAL_IN_OUT,
|
|
||||||
complete: finish,
|
|
||||||
cancel: finish,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
cancel: finish,
|
cancel: finish,
|
||||||
});
|
});
|
||||||
@@ -681,7 +669,7 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
const attempt = { phase: 'armed' };
|
const attempt = { phase: 'armed' };
|
||||||
_triggerApproachAttempt = attempt;
|
_triggerApproachAttempt = attempt;
|
||||||
const start = () => startTriggerApproach(attempt);
|
const start = () => startTriggerApproach(attempt);
|
||||||
const camera = _viewer?.camera;
|
const camera = cameraForScene();
|
||||||
if (
|
if (
|
||||||
!camera?.moveStart?.addEventListener ||
|
!camera?.moveStart?.addEventListener ||
|
||||||
!camera?.moveEnd?.addEventListener
|
!camera?.moveEnd?.addEventListener
|
||||||
@@ -1046,11 +1034,42 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
requestRender('bhote-koshi-locator-presentation');
|
requestRender('bhote-koshi-locator-presentation');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init(viewer) {
|
function connectDisplay() {
|
||||||
|
if (!_dataSource) return;
|
||||||
|
if (_viewer) _viewer.dataSources.add(_dataSource);
|
||||||
|
else if (_engine) _native = createNativeRenderer(_engine, _dataSource);
|
||||||
|
registerDynamicCredit(creditTarget(), BHOTE_KOSHI_LOCATOR_CREDIT);
|
||||||
|
showOsmCredit(creditTarget(), BHOTE_KOSHI_LOCATOR_LAYER_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachRenderer(viewer, {engine = null} = {}) {
|
||||||
_viewer = viewer;
|
_viewer = viewer;
|
||||||
|
_engine = engine;
|
||||||
|
if (!_enabled) return true;
|
||||||
|
connectDisplay();
|
||||||
|
publishOverlayEntries({moving: _placeRevealMoving});
|
||||||
|
if (_pausedApproach) {
|
||||||
|
const {phase, remaining} = _pausedApproach;
|
||||||
|
_pausedApproach = null;
|
||||||
|
if (phase === 'armed') armTriggerApproach();
|
||||||
|
else flyTriggerStage((_triggerApproachAttempt = {}), phase, remaining);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detachRenderer() {
|
||||||
|
const phase = _triggerApproachAttempt?.phase;
|
||||||
|
if (phase && phase !== 'finished') {
|
||||||
|
_pausedApproach = {phase, remaining: Math.max(0, (_triggerApproachAttempt.endsAt - now()) / 1000)};
|
||||||
|
clearTriggerApproach({preserveCallout: true});
|
||||||
|
}
|
||||||
|
_native?.destroy();
|
||||||
|
_native = null;
|
||||||
|
if (_viewer && _dataSource) _viewer.dataSources.remove(_dataSource, false);
|
||||||
|
hideOsmCredit(creditTarget(), BHOTE_KOSHI_LOCATOR_LAYER_ID);
|
||||||
|
_engine = _viewer = null;
|
||||||
|
}
|
||||||
|
|
||||||
async function enable(viewer, { signal } = {}) {
|
async function enable(viewer, { signal } = {}) {
|
||||||
_viewer = viewer;
|
_viewer = viewer;
|
||||||
_enabled = true;
|
_enabled = true;
|
||||||
@@ -1062,9 +1081,7 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
const dataSource = new Cesium.CustomDataSource('bhote-koshi-locator');
|
const dataSource = new Cesium.CustomDataSource('bhote-koshi-locator');
|
||||||
_dataSource = dataSource;
|
_dataSource = dataSource;
|
||||||
renderPresentation();
|
renderPresentation();
|
||||||
viewer.dataSources.add(dataSource);
|
connectDisplay();
|
||||||
registerDynamicCredit(viewer, BHOTE_KOSHI_LOCATOR_CREDIT);
|
|
||||||
showOsmCredit(viewer, BHOTE_KOSHI_LOCATOR_LAYER_ID);
|
|
||||||
requestRender('bhote-koshi-locator-callout');
|
requestRender('bhote-koshi-locator-callout');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1100,7 +1117,8 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function disable() {
|
async function disable() {
|
||||||
hideOsmCredit(_viewer, BHOTE_KOSHI_LOCATOR_LAYER_ID);
|
hideOsmCredit(creditTarget(), BHOTE_KOSHI_LOCATOR_LAYER_ID);
|
||||||
|
_pausedApproach = null;
|
||||||
_enabled = false;
|
_enabled = false;
|
||||||
_boundaryReady = false;
|
_boundaryReady = false;
|
||||||
_boundaryResolved = false;
|
_boundaryResolved = false;
|
||||||
@@ -1113,7 +1131,7 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
|
|
||||||
async function destroy() {
|
async function destroy() {
|
||||||
await disable();
|
await disable();
|
||||||
_viewer = null;
|
_viewer = _engine = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1124,7 +1142,9 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
icon: '◎',
|
icon: '◎',
|
||||||
source: 'OpenStreetMap + GeoPera',
|
source: 'OpenStreetMap + GeoPera',
|
||||||
updateInterval: 0,
|
updateInterval: 0,
|
||||||
init,
|
init: attachRenderer,
|
||||||
|
attachRenderer,
|
||||||
|
detachRenderer,
|
||||||
enable,
|
enable,
|
||||||
disable,
|
disable,
|
||||||
update: async () => true,
|
update: async () => true,
|
||||||
|
|||||||
@@ -645,8 +645,8 @@ export const BHOTE_KOSHI_LOCATOR_CREDIT = {
|
|||||||
'(CC BY-NC 4.0)',
|
'(CC BY-NC 4.0)',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {Set<string>} Keys of dynamic credits already registered this session. */
|
/** Keys registered in each globe's attribution display. */
|
||||||
const _dynamicCreditKeys = new Set();
|
const dynamicCreditKeys = new WeakMap();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a conditional credit at the moment its data source activates.
|
* Register a conditional credit at the moment its data source activates.
|
||||||
@@ -660,9 +660,11 @@ export function registerDynamicCredit(target, credit) {
|
|||||||
const credits = creditsOf(target);
|
const credits = creditsOf(target);
|
||||||
if (!credits) return false;
|
if (!credits) return false;
|
||||||
if (!credit?.key || !credit?.html) return false;
|
if (!credit?.key || !credit?.html) return false;
|
||||||
if (_dynamicCreditKeys.has(credit.key)) return true;
|
let keys = dynamicCreditKeys.get(target);
|
||||||
|
if (!keys) dynamicCreditKeys.set(target, (keys = new Set()));
|
||||||
|
if (keys.has(credit.key)) return true;
|
||||||
credits.addStatic(credit.html, { onScreen: false });
|
credits.addStatic(credit.html, { onScreen: false });
|
||||||
_dynamicCreditKeys.add(credit.key);
|
keys.add(credit.key);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,9 @@ test('data credits go through the globe engine credits', async () => {
|
|||||||
engine.fake.credits.filter(({ html }) => html === 'D').length,
|
engine.fake.credits.filter(({ html }) => html === 'D').length,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
|
const other = createFakeEngine();
|
||||||
|
registerDynamicCredit(other, { key: 'engine-dyn', html: 'D' });
|
||||||
|
assert.equal(other.fake.credits.filter(({ html }) => html === 'D').length, 1);
|
||||||
engine.fake.resetCalls();
|
engine.fake.resetCalls();
|
||||||
assert.equal(showOsmCredit(engine, 'alpr', { openMapTiles: true }), true);
|
assert.equal(showOsmCredit(engine, 'alpr', { openMapTiles: true }), true);
|
||||||
const inline = engine.fake.credits.filter(({ onScreen }) => onScreen);
|
const inline = engine.fake.credits.filter(({ onScreen }) => onScreen);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// dynamic import() so Navara stays out of the Cesium-mode bundle.
|
// dynamic import() so Navara stays out of the Cesium-mode bundle.
|
||||||
|
|
||||||
import ThreeView, {
|
import ThreeView, {
|
||||||
|
Color as NavaraColor,
|
||||||
MeshDesc,
|
MeshDesc,
|
||||||
EffectDesc,
|
EffectDesc,
|
||||||
Effect as NavaraEffect,
|
Effect as NavaraEffect,
|
||||||
@@ -205,6 +206,7 @@ export async function createNavaraGlobe({
|
|||||||
return {
|
return {
|
||||||
view,
|
view,
|
||||||
engine,
|
engine,
|
||||||
|
NavaraColor,
|
||||||
MeshDesc,
|
MeshDesc,
|
||||||
EffectDesc,
|
EffectDesc,
|
||||||
NavaraEffect,
|
NavaraEffect,
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import * as geo from '../../../geo/index.js';
|
||||||
|
|
||||||
|
const val = (p) => (p?.getValue ? p.getValue() : p);
|
||||||
|
const rgba = (c) => [c.red, c.green, c.blue, c.alpha];
|
||||||
|
const tint = (c) =>
|
||||||
|
(Math.round(c.red * 255) << 16) |
|
||||||
|
(Math.round(c.green * 255) << 8) |
|
||||||
|
Math.round(c.blue * 255);
|
||||||
|
function samePath(old, points) {
|
||||||
|
return (
|
||||||
|
old?.length === points.length &&
|
||||||
|
points.every(
|
||||||
|
(p, i) => p.x === old[i].x && p.y === old[i].y && p.z === old[i].z,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function circleAtlas() {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = 128;
|
||||||
|
canvas.height = 64;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.fillStyle = 'white';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(32, 32, 29, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.lineWidth = 5;
|
||||||
|
ctx.strokeStyle = 'white';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(96, 32, 27, 0, Math.PI * 2);
|
||||||
|
ctx.stroke();
|
||||||
|
return { canvas, cells: { fill: [0, 0, 0.5, 1], rim: [0.5, 0, 1, 1] } };
|
||||||
|
}
|
||||||
|
|
||||||
|
// The authored story owns the clock and geographic graphics. This adapter only samples them
|
||||||
|
// while its scene is attached. Neither a Cesium viewer nor an additional globe is needed.
|
||||||
|
export function drawStorySource(engine, { view, NavaraColor }, data) {
|
||||||
|
const entries = new Map();
|
||||||
|
let atlas;
|
||||||
|
// Story graphics encode map colors. Compensate the planet's photographic
|
||||||
|
// exposure before its tone-mapping pass so a cyan overlay stays readable.
|
||||||
|
function displayColor(hex) {
|
||||||
|
const c = new NavaraColor().setHex(hex);
|
||||||
|
const exposure = Math.max(1, view.toneMappingExposure ?? 1);
|
||||||
|
const [r, g, b] = c.toArray();
|
||||||
|
return c.setRGBLinear(r / exposure, g / exposure, b / exposure);
|
||||||
|
}
|
||||||
|
function point(entity) {
|
||||||
|
const p = entity.point,
|
||||||
|
nearFar = val(p.scaleByDistance),
|
||||||
|
depth = val(p.disableDepthTestDistance);
|
||||||
|
const set = engine.movers.createSet({
|
||||||
|
id: data.name + '-' + entity.id,
|
||||||
|
kind: 'sprite',
|
||||||
|
capacity: 2,
|
||||||
|
pickable: false,
|
||||||
|
depthTest: depth === Infinity ? 'always' : 'normal',
|
||||||
|
sprite: {
|
||||||
|
sizePx: 64,
|
||||||
|
atlas: (atlas ??= circleAtlas()),
|
||||||
|
scaleByDistance: nearFar
|
||||||
|
? [nearFar.near, nearFar.nearValue, nearFar.far, nearFar.farValue]
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
paint(show) {
|
||||||
|
const position = val(entity.position);
|
||||||
|
if (!show || !position) {
|
||||||
|
set.setCount(0);
|
||||||
|
set.commit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let at = position;
|
||||||
|
if (val(p.heightReference) === 1) {
|
||||||
|
const carto = geo.cartographic.fromCartesian(position);
|
||||||
|
const h = engine.surface.terrainHeight(carto);
|
||||||
|
if (Number.isFinite(h))
|
||||||
|
at = geo.cartesian3.fromRadians(carto.longitude, carto.latitude, h);
|
||||||
|
}
|
||||||
|
const scale = Number(val(p.pixelSize)) / 58;
|
||||||
|
const color = val(p.color),
|
||||||
|
outline = val(p.outlineColor);
|
||||||
|
set.set(0, {
|
||||||
|
position: at,
|
||||||
|
show: true,
|
||||||
|
cell: 'fill',
|
||||||
|
color: rgba(color),
|
||||||
|
scale,
|
||||||
|
});
|
||||||
|
const hasOutline = Boolean(outline && val(p.outlineWidth));
|
||||||
|
if (hasOutline)
|
||||||
|
set.set(1, {
|
||||||
|
position: at,
|
||||||
|
show: true,
|
||||||
|
cell: 'rim',
|
||||||
|
color: rgba(outline),
|
||||||
|
scale,
|
||||||
|
});
|
||||||
|
set.setCount(hasOutline ? 2 : 1);
|
||||||
|
set.commit();
|
||||||
|
},
|
||||||
|
destroy: () => set.destroy(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function geometry(entity) {
|
||||||
|
const key = entity.polyline ? 'polyline' : 'polygon',
|
||||||
|
g = entity[key];
|
||||||
|
let handle,
|
||||||
|
previous = null,
|
||||||
|
visible = null;
|
||||||
|
return {
|
||||||
|
paint(show) {
|
||||||
|
const path =
|
||||||
|
key === 'polygon' ? val(g.hierarchy)?.positions : val(g.positions);
|
||||||
|
show = Boolean(show && path?.length >= (key === 'polygon' ? 3 : 2));
|
||||||
|
if (!show) {
|
||||||
|
if (handle && visible) handle.update({ visible: false });
|
||||||
|
visible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!handle || !samePath(previous, path)) {
|
||||||
|
const color = val(g.material)?.color ?? val(g.material?.color);
|
||||||
|
previous = path.map(({ x, y, z }) => ({ x, y, z }));
|
||||||
|
const geom = {
|
||||||
|
points: previous,
|
||||||
|
clampToGround: true,
|
||||||
|
color: displayColor(color ? tint(color) : 0x20e7f2),
|
||||||
|
opacity: color?.alpha ?? 1,
|
||||||
|
transparent: true,
|
||||||
|
...(key === 'polyline' ? { width: val(g.width) ?? 2 } : {}),
|
||||||
|
};
|
||||||
|
if (handle) handle.update({ [key]: geom, visible: true });
|
||||||
|
else
|
||||||
|
handle = view.addMesh({
|
||||||
|
[key]: geom,
|
||||||
|
visible: true,
|
||||||
|
lit: false,
|
||||||
|
pickable: false,
|
||||||
|
});
|
||||||
|
} else if (!visible) handle.update({ visible: true });
|
||||||
|
visible = true;
|
||||||
|
},
|
||||||
|
destroy: () => handle?.delete(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function frame() {
|
||||||
|
const live = new Set(data.entities.values);
|
||||||
|
for (const [e, item] of entries)
|
||||||
|
if (!live.has(e)) {
|
||||||
|
item.destroy();
|
||||||
|
entries.delete(e);
|
||||||
|
}
|
||||||
|
for (const e of live) {
|
||||||
|
let item = entries.get(e);
|
||||||
|
if (!item) {
|
||||||
|
if (!e.point && !e.polyline && !e.polygon) continue;
|
||||||
|
item = e.point ? point(e) : geometry(e);
|
||||||
|
entries.set(e, item);
|
||||||
|
}
|
||||||
|
item.paint(data.show !== false && val(e.show) !== false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const off = engine.frames.preRender.addEventListener(frame);
|
||||||
|
engine.render.request(data.name);
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
off();
|
||||||
|
for (const item of entries.values()) item.destroy();
|
||||||
|
entries.clear();
|
||||||
|
engine.render.request(data.name);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as geo from '../../../geo/index.js';
|
import * as geo from '../../../geo/index.js';
|
||||||
import { Color } from 'three';
|
|
||||||
import { isPointerFree } from '../../../data/inputOwnership.js';
|
import { isPointerFree } from '../../../data/inputOwnership.js';
|
||||||
import {
|
import {
|
||||||
featureLabel,
|
featureLabel,
|
||||||
@@ -201,7 +200,7 @@ export function createNavaraCables(engine, native, host) {
|
|||||||
layer.on('featureUpdated', ({ evaluator }) =>
|
layer.on('featureUpdated', ({ evaluator }) =>
|
||||||
evaluator.evaluate(({ properties }) => {
|
evaluator.evaluate(({ properties }) => {
|
||||||
const css = properties?.color ?? BASE_CABLE_COLOR;
|
const css = properties?.color ?? BASE_CABLE_COLOR;
|
||||||
if (!colors.has(css)) colors.set(css, new Color(css));
|
if (!colors.has(css)) colors.set(css, new native.NavaraColor().setStyle(css));
|
||||||
return { color: colors.get(css) };
|
return { color: colors.get(css) };
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user