mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
Keep imported files across engine switches and read them again on the next Cesium viewer
The Import list and each file's bytes belong to the page session; the data sources, pins, drop and gesture listeners belong to the attached viewer. Without a viewer the list stays and import or fly-to is refused.
This commit is contained in:
1 parent
111ba1626c
commit
17eea266f1
3 files changed
+297
-100
No files matched your search
+36
-8
@@ -1,7 +1,7 @@
|
|||||||
import { SceneDirector } from '../scenes/director.js';
|
import { SceneDirector } from '../scenes/director.js';
|
||||||
import { createSessionAnnotations } from '../annotations/index.js';
|
import { createSessionAnnotations } from '../annotations/index.js';
|
||||||
import { initDrawTool } from '../annotations/drawTool.js';
|
import { initDrawTool } from '../annotations/drawTool.js';
|
||||||
import { initGeoFileImport } from '../ui/geoFileImport.js';
|
import { createGeoFileImport } from '../ui/geoFileImport.js';
|
||||||
import { useDisplayStore } from '../ui/stores/display.js';
|
import { useDisplayStore } from '../ui/stores/display.js';
|
||||||
import { applicationPinia } from '../ui/vueRuntime.js';
|
import { applicationPinia } from '../ui/vueRuntime.js';
|
||||||
import { initImageryBoxTool } from '../ui/imageryBoxTool.js';
|
import { initImageryBoxTool } from '../ui/imageryBoxTool.js';
|
||||||
@@ -45,6 +45,28 @@ export function sessionAnnotations({
|
|||||||
return layerSession.annotations;
|
return layerSession.annotations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A tool of the page session (created once, destroyed with the page) when
|
||||||
|
* there is one, else of this application. `create()` may return null.
|
||||||
|
*/
|
||||||
|
function sessionTool(layerSession, name, defer, create) {
|
||||||
|
if (!layerSession) {
|
||||||
|
const tool = create();
|
||||||
|
defer(() => tool?.destroy());
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
const tools = (layerSession.tools ??= {});
|
||||||
|
if (!(name in tools)) {
|
||||||
|
const tool = (tools[name] = create());
|
||||||
|
layerSession.lifetime.signal.addEventListener(
|
||||||
|
'abort',
|
||||||
|
() => tool?.destroy(),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return tools[name];
|
||||||
|
}
|
||||||
|
|
||||||
function destroyAnnotations(annotations) {
|
function destroyAnnotations(annotations) {
|
||||||
if (window.__gevAnnotations === annotations) delete window.__gevAnnotations;
|
if (window.__gevAnnotations === annotations) delete window.__gevAnnotations;
|
||||||
annotations.destroy();
|
annotations.destroy();
|
||||||
@@ -152,17 +174,23 @@ export function createApplicationTools({
|
|||||||
});
|
});
|
||||||
defer(() => drawTool?.destroy());
|
defer(() => drawTool?.destroy());
|
||||||
// UTILITIES ▸ Import: the person's own GeoJSON/KML/KMZ/GPX files, each its
|
// UTILITIES ▸ Import: the person's own GeoJSON/KML/KMZ/GPX files, each its
|
||||||
// own data source. They live for the page session, so the application
|
// own data source. They live for the page session: an engine switch takes
|
||||||
// lifetime removes them and the drop listeners on the viewer container.
|
// them off this viewer, and the next Cesium viewer reads them again. The
|
||||||
const geoFileImport = initGeoFileImport({
|
// drop listeners belong to this viewer.
|
||||||
|
const geoFileImport = sessionTool(layerSession, 'geoImport', defer, () =>
|
||||||
|
createGeoFileImport({
|
||||||
|
navigation: styleManager,
|
||||||
|
ground: operations.surface?.groundFloor,
|
||||||
|
publish: (view) => displayStore.publishGeoImport(view),
|
||||||
|
connect: connectDisplay,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
geoFileImport?.attachScene({
|
||||||
viewer,
|
viewer,
|
||||||
navigation: styleManager,
|
navigation: styleManager,
|
||||||
ground: operations.surface?.groundFloor,
|
|
||||||
subscribeMapSource: (listener) => mapStackController?.subscribe?.(listener),
|
subscribeMapSource: (listener) => mapStackController?.subscribe?.(listener),
|
||||||
publish: (view) => displayStore.publishGeoImport(view),
|
|
||||||
connect: connectDisplay,
|
|
||||||
});
|
});
|
||||||
defer(() => geoFileImport?.destroy());
|
defer(() => geoFileImport?.detachScene());
|
||||||
// DATA ▸ Recent Imagery: the box tool claims the pointer like Draw and the
|
// DATA ▸ Recent Imagery: the box tool claims the pointer like Draw and the
|
||||||
// panel lives on the right rail, so both belong to the application
|
// panel lives on the right rail, so both belong to the application
|
||||||
// lifetime. The tileset lets the layer drape while the globe is hidden.
|
// lifetime. The tileset lets the layer drape while the globe is hidden.
|
||||||
|
|||||||
+199
-90
@@ -21,19 +21,37 @@ function localizedValues(values) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the Import control.
|
* Run the Import control on one viewer: the session's control (see
|
||||||
* @param {{viewer: object, load?: Function, document?: Document,
|
* createGeoFileImport) attached to `viewer`. Null without a viewer or the
|
||||||
* publish?: Function, connect?: Function}} deps
|
* Import markup.
|
||||||
|
*/
|
||||||
|
export function initGeoFileImport({ viewer, ...options }) {
|
||||||
|
if (!viewer) return null;
|
||||||
|
const tool = createGeoFileImport(options);
|
||||||
|
tool?.attachScene({ viewer });
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Import control for the page session.
|
||||||
|
* @param {{load?: Function, document?: Document, publish?: Function,
|
||||||
|
* connect?: Function}} deps
|
||||||
* `load(file, {viewer, color})` defaults to `loadGeoFile`; tests pass a double.
|
* `load(file, {viewer, color})` defaults to `loadGeoFile`; tests pass a double.
|
||||||
* `publish({rows, hint})` hands the file list and hint to the view that
|
* `publish({rows, hint})` hands the file list and hint to the view that
|
||||||
* renders them. `connect(actions)` answers the view's pick and row buttons
|
* renders them. `connect(actions)` answers the view's pick and row buttons
|
||||||
* and returns their disconnect.
|
* and returns their disconnect.
|
||||||
|
*
|
||||||
|
* The imported files belong to the session; their drawing belongs to the
|
||||||
|
* attached Cesium viewer. `attachScene({ viewer })` draws them (a file kept
|
||||||
|
* while no viewer was attached is read again from its bytes) and listens for
|
||||||
|
* drops and camera gestures there; `detachScene()` takes them off the viewer
|
||||||
|
* and keeps the list. Without a viewer (an engine without Cesium) the list
|
||||||
|
* stays, and import and fly-to wait for one.
|
||||||
* @returns {{destroy: Function, importFiles: Function, list: Function,
|
* @returns {{destroy: Function, importFiles: Function, list: Function,
|
||||||
* setVisible: Function, flyTo: Function, remove: Function,
|
* setVisible: Function, flyTo: Function, remove: Function,
|
||||||
* diagnostics: Function}|null}
|
* diagnostics: Function, attachScene: Function, detachScene: Function}|null}
|
||||||
*/
|
*/
|
||||||
export function initGeoFileImport({
|
export function createGeoFileImport({
|
||||||
viewer,
|
|
||||||
load = loadGeoFile,
|
load = loadGeoFile,
|
||||||
document: doc = globalThis.document,
|
document: doc = globalThis.document,
|
||||||
flyToSource = flyToDataSource,
|
flyToSource = flyToDataSource,
|
||||||
@@ -42,13 +60,13 @@ export function initGeoFileImport({
|
|||||||
subscribeMapSource = null,
|
subscribeMapSource = null,
|
||||||
publish = () => {},
|
publish = () => {},
|
||||||
connect = () => () => {},
|
connect = () => () => {},
|
||||||
}) {
|
} = {}) {
|
||||||
// A page without the Import control (the Utilities panel omitted, in
|
// A page without the Import control (the Utilities panel omitted, in
|
||||||
// LayerPanelsView) has no file import.
|
// LayerPanelsView) has no file import.
|
||||||
if (!viewer || !doc?.getElementById('geo-import-button')) return null;
|
if (!doc?.getElementById('geo-import-button')) return null;
|
||||||
|
|
||||||
const imports = [];
|
const imports = [];
|
||||||
const listeners = [];
|
let listeners = [];
|
||||||
let queue = Promise.resolve();
|
let queue = Promise.resolve();
|
||||||
let seq = 0;
|
let seq = 0;
|
||||||
let colorAt = 0;
|
let colorAt = 0;
|
||||||
@@ -57,7 +75,9 @@ export function initGeoFileImport({
|
|||||||
let cameraIntent = 0;
|
let cameraIntent = 0;
|
||||||
let hint = '';
|
let hint = '';
|
||||||
const lifetime = new AbortController();
|
const lifetime = new AbortController();
|
||||||
let removeMapSource;
|
// The attached viewer and the lifetime of that attachment.
|
||||||
|
let viewer = null;
|
||||||
|
let scene = null;
|
||||||
|
|
||||||
const listen = (target, type, listener, options) => {
|
const listen = (target, type, listener, options) => {
|
||||||
if (!target?.addEventListener) return;
|
if (!target?.addEventListener) return;
|
||||||
@@ -68,13 +88,13 @@ export function initGeoFileImport({
|
|||||||
hint = text || '';
|
hint = text || '';
|
||||||
present();
|
present();
|
||||||
};
|
};
|
||||||
const render = () => viewer.scene?.requestRender?.();
|
const render = () => viewer?.scene?.requestRender?.();
|
||||||
let groundTimer,
|
let groundTimer,
|
||||||
groundQueued = false,
|
groundQueued = false,
|
||||||
groundAgain = false;
|
groundAgain = false;
|
||||||
const groundRemovers = [];
|
let groundRemovers = [];
|
||||||
const scheduleGround = () => {
|
const scheduleGround = () => {
|
||||||
if (destroyed || !imports.length) return;
|
if (destroyed || !viewer || !imports.length) return;
|
||||||
if (groundQueued) {
|
if (groundQueued) {
|
||||||
groundAgain = true;
|
groundAgain = true;
|
||||||
return;
|
return;
|
||||||
@@ -82,17 +102,19 @@ export function initGeoFileImport({
|
|||||||
clearTimeout(groundTimer);
|
clearTimeout(groundTimer);
|
||||||
groundTimer = setTimeout(() => {
|
groundTimer = setTimeout(() => {
|
||||||
groundQueued = true;
|
groundQueued = true;
|
||||||
|
const attached = scene;
|
||||||
queue = queue
|
queue = queue
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
try {
|
try {
|
||||||
for (const item of imports) {
|
for (const item of imports) {
|
||||||
if (destroyed) return;
|
if (destroyed || scene !== attached) return;
|
||||||
if (item.visible) await item.refreshGround?.(lifetime.signal);
|
if (item.visible)
|
||||||
|
await item.refreshGround?.(attached.controller.signal);
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
} catch {
|
} catch {
|
||||||
if (!destroyed)
|
if (!destroyed && scene === attached)
|
||||||
setHint(
|
setHint(
|
||||||
t(
|
t(
|
||||||
'Ground heights could not be refreshed. Try the file again when the map has loaded.',
|
'Ground heights could not be refreshed. Try the file again when the map has loaded.',
|
||||||
@@ -108,15 +130,10 @@ export function initGeoFileImport({
|
|||||||
});
|
});
|
||||||
}, 200);
|
}, 200);
|
||||||
};
|
};
|
||||||
removeMapSource = subscribeMapSource?.(scheduleGround);
|
|
||||||
const ownEvent = (event, listener) => {
|
const ownEvent = (event, listener) => {
|
||||||
const remove = event?.addEventListener?.(listener);
|
const remove = event?.addEventListener?.(listener);
|
||||||
if (remove) groundRemovers.push(remove);
|
if (remove) groundRemovers.push(remove);
|
||||||
};
|
};
|
||||||
ownEvent(viewer.camera?.moveEnd, scheduleGround);
|
|
||||||
ownEvent(viewer.scene?.globe?.tileLoadProgressEvent, (pending) => {
|
|
||||||
if (!pending) scheduleGround();
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Publish the file list and hint; the view renders them. */
|
/** Publish the file list and hint; the view renders them. */
|
||||||
function present() {
|
function present() {
|
||||||
@@ -124,8 +141,6 @@ export function initGeoFileImport({
|
|||||||
rows: imports.map((item) => ({
|
rows: imports.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
// KML and KMZ draw with their own styles; their swatch says so
|
|
||||||
// instead of claiming a color the globe does not show.
|
|
||||||
ownStyles: item.ownStyles,
|
ownStyles: item.ownStyles,
|
||||||
color: item.color,
|
color: item.color,
|
||||||
title: `${item.name} — ${item.format.toUpperCase()}, ${tn(
|
title: `${item.name} — ${item.format.toUpperCase()}, ${tn(
|
||||||
@@ -166,16 +181,15 @@ export function initGeoFileImport({
|
|||||||
const item = find(id);
|
const item = find(id);
|
||||||
if (!item) return false;
|
if (!item) return false;
|
||||||
item.visible = Boolean(visible);
|
item.visible = Boolean(visible);
|
||||||
item.dataSource.show = item.visible;
|
if (item.dataSource) item.dataSource.show = item.visible;
|
||||||
if (item.visible) scheduleGround();
|
if (item.visible) scheduleGround();
|
||||||
present();
|
present();
|
||||||
render();
|
render();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not viewer.flyTo: it frames ground-clamped points underground until the
|
|
||||||
// destination's terrain has loaded (see ./flyToDataSource.js).
|
|
||||||
const fly = (item) => {
|
const fly = (item) => {
|
||||||
|
if (!viewer || !item.dataSource) return false;
|
||||||
if (!item.visible) setVisible(item.id, true);
|
if (!item.visible) setVisible(item.id, true);
|
||||||
flyToSource(viewer, item.dataSource, { ground });
|
flyToSource(viewer, item.dataSource, { ground });
|
||||||
return true;
|
return true;
|
||||||
@@ -187,7 +201,7 @@ export function initGeoFileImport({
|
|||||||
*/
|
*/
|
||||||
function flyTo(id) {
|
function flyTo(id) {
|
||||||
const item = find(id);
|
const item = find(id);
|
||||||
if (!item) return false;
|
if (!item || !viewer || !item.dataSource) return false;
|
||||||
if (typeof navigation?.runImmediateNavigation === 'function')
|
if (typeof navigation?.runImmediateNavigation === 'function')
|
||||||
return (
|
return (
|
||||||
navigation.runImmediateNavigation('file', () => fly(item)) !== false
|
navigation.runImmediateNavigation('file', () => fly(item)) !== false
|
||||||
@@ -209,18 +223,41 @@ export function initGeoFileImport({
|
|||||||
: true;
|
: true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Take one file's drawing off the viewer; the file stays in the list. */
|
||||||
|
function undraw(item, from = viewer) {
|
||||||
|
if (item.dataSource) from?.dataSources?.remove?.(item.dataSource, true);
|
||||||
|
item.revoke?.();
|
||||||
|
item.dataSource = null;
|
||||||
|
item.revoke = null;
|
||||||
|
item.refreshGround = null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Remove one imported file from the globe and the list. */
|
/** Remove one imported file from the globe and the list. */
|
||||||
function remove(id) {
|
function remove(id) {
|
||||||
const at = imports.findIndex((item) => item.id === id);
|
const at = imports.findIndex((item) => item.id === id);
|
||||||
if (at < 0) return false;
|
if (at < 0) return false;
|
||||||
const [item] = imports.splice(at, 1);
|
const [item] = imports.splice(at, 1);
|
||||||
viewer.dataSources?.remove?.(item.dataSource, true);
|
undraw(item);
|
||||||
item.revoke?.();
|
|
||||||
present();
|
present();
|
||||||
render();
|
render();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Read a file for the attached viewer. Null once that viewer went. */
|
||||||
|
async function draw(item, attached, loaded) {
|
||||||
|
item.dataSource = loaded.dataSource;
|
||||||
|
item.revoke = loaded.revoke;
|
||||||
|
item.refreshGround = loaded.refreshGround;
|
||||||
|
item.dataSource.show = item.visible;
|
||||||
|
await attached.viewer.dataSources.add(item.dataSource);
|
||||||
|
if (destroyed || scene !== attached) {
|
||||||
|
undraw(item, attached.viewer);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
loaded.mount?.();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import files, one after another. Returns one outcome per file.
|
* Import files, one after another. Returns one outcome per file.
|
||||||
* @param {Iterable<File>} files
|
* @param {Iterable<File>} files
|
||||||
@@ -230,20 +267,33 @@ export function initGeoFileImport({
|
|||||||
function importFiles(files) {
|
function importFiles(files) {
|
||||||
const list = [...(files || [])];
|
const list = [...(files || [])];
|
||||||
if (destroyed || !list.length) return Promise.resolve([]);
|
if (destroyed || !list.length) return Promise.resolve([]);
|
||||||
// Capture the user's intent when the files are chosen, including time
|
if (!viewer) {
|
||||||
// spent waiting for an earlier batch. Later navigation must still win.
|
// Import draws on the Cesium globe; this engine has none.
|
||||||
|
const error = t('Action unavailable or cancelled');
|
||||||
|
setHint(error);
|
||||||
|
return Promise.resolve(
|
||||||
|
list.map((file) => ({
|
||||||
|
name: String(file?.name || 'file'),
|
||||||
|
ok: false,
|
||||||
|
error,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
const generation = beginAutoFly();
|
const generation = beginAutoFly();
|
||||||
const intent = ++cameraIntent;
|
const intent = ++cameraIntent;
|
||||||
const run = queue.then(() => importBatch(list, generation, intent));
|
const attached = scene;
|
||||||
|
const run = queue.then(() =>
|
||||||
|
importBatch(list, generation, intent, attached),
|
||||||
|
);
|
||||||
queue = run.catch(() => {});
|
queue = run.catch(() => {});
|
||||||
return run;
|
return run;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importBatch(list, generation, intent) {
|
async function importBatch(list, generation, intent, attached) {
|
||||||
const outcomes = [];
|
const outcomes = [];
|
||||||
if (destroyed || !list.length) return outcomes;
|
if (destroyed || !list.length) return outcomes;
|
||||||
for (const file of list) {
|
for (const file of list) {
|
||||||
if (destroyed) break;
|
if (destroyed || scene !== attached) break;
|
||||||
const fileName = String(file?.name || 'file');
|
const fileName = String(file?.name || 'file');
|
||||||
if (imports.length >= MAX_IMPORTED_FILES) {
|
if (imports.length >= MAX_IMPORTED_FILES) {
|
||||||
outcomes.push({
|
outcomes.push({
|
||||||
@@ -259,7 +309,11 @@ export function initGeoFileImport({
|
|||||||
const color = COLORS[colorAt % COLORS.length];
|
const color = COLORS[colorAt % COLORS.length];
|
||||||
let loaded;
|
let loaded;
|
||||||
try {
|
try {
|
||||||
loaded = await load(file, { viewer, color, signal: lifetime.signal });
|
loaded = await load(file, {
|
||||||
|
viewer: attached.viewer,
|
||||||
|
color,
|
||||||
|
signal: attached.controller.signal,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
outcomes.push({
|
outcomes.push({
|
||||||
name: fileName,
|
name: fileName,
|
||||||
@@ -274,7 +328,7 @@ export function initGeoFileImport({
|
|||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (destroyed) {
|
if (destroyed || scene !== attached) {
|
||||||
loaded.revoke?.();
|
loaded.revoke?.();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -289,17 +343,13 @@ export function initGeoFileImport({
|
|||||||
removed: loaded.removed || 0,
|
removed: loaded.removed || 0,
|
||||||
color,
|
color,
|
||||||
visible: true,
|
visible: true,
|
||||||
dataSource: loaded.dataSource,
|
// The bytes, so a later viewer can read the file again.
|
||||||
revoke: loaded.revoke,
|
file,
|
||||||
refreshGround: loaded.refreshGround,
|
dataSource: null,
|
||||||
|
revoke: null,
|
||||||
|
refreshGround: null,
|
||||||
};
|
};
|
||||||
await viewer.dataSources.add(item.dataSource);
|
if (!(await draw(item, attached, loaded))) break;
|
||||||
if (destroyed) {
|
|
||||||
viewer.dataSources.remove?.(item.dataSource, true);
|
|
||||||
item.revoke?.();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
loaded.mount?.();
|
|
||||||
imports.push(item);
|
imports.push(item);
|
||||||
present();
|
present();
|
||||||
render();
|
render();
|
||||||
@@ -309,8 +359,6 @@ export function initGeoFileImport({
|
|||||||
id: item.id,
|
id: item.id,
|
||||||
removed: item.removed,
|
removed: item.removed,
|
||||||
});
|
});
|
||||||
// Fly to the first file of a batch that lands, so a drop shows its
|
|
||||||
// result, unless something else took the camera meanwhile.
|
|
||||||
if (
|
if (
|
||||||
outcomes.filter((o) => o.ok).length === 1 &&
|
outcomes.filter((o) => o.ok).length === 1 &&
|
||||||
intent === cameraIntent &&
|
intent === cameraIntent &&
|
||||||
@@ -318,12 +366,37 @@ export function initGeoFileImport({
|
|||||||
)
|
)
|
||||||
fly(item);
|
fly(item);
|
||||||
}
|
}
|
||||||
if (!destroyed) setHint(summarize(outcomes));
|
if (!destroyed && scene === attached) setHint(summarize(outcomes));
|
||||||
return outcomes;
|
return outcomes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- controls ----------------------------------------------------------
|
// Draw the files kept while no viewer was attached, as they were.
|
||||||
// The view opens the picker; its pick and the row buttons call these.
|
async function redraw(attached) {
|
||||||
|
for (const item of [...imports]) {
|
||||||
|
if (destroyed || scene !== attached) return;
|
||||||
|
if (item.dataSource || !imports.includes(item)) continue;
|
||||||
|
try {
|
||||||
|
const loaded = await load(item.file, {
|
||||||
|
viewer: attached.viewer,
|
||||||
|
color: item.color,
|
||||||
|
signal: attached.controller.signal,
|
||||||
|
});
|
||||||
|
if (destroyed || scene !== attached || !imports.includes(item)) {
|
||||||
|
loaded.revoke?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await draw(item, attached, loaded);
|
||||||
|
} catch {
|
||||||
|
if (destroyed || scene !== attached) return;
|
||||||
|
setHint(`${item.name}: ${t('That file could not be read.')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!destroyed && scene === attached) {
|
||||||
|
present();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const disconnect = connect({
|
const disconnect = connect({
|
||||||
importGeoFiles: (files) => void importFiles(files),
|
importGeoFiles: (files) => void importFiles(files),
|
||||||
toggleGeoImport: (id) => setVisible(id, !find(id)?.visible),
|
toggleGeoImport: (id) => setVisible(id, !find(id)?.visible),
|
||||||
@@ -331,45 +404,83 @@ export function initGeoFileImport({
|
|||||||
removeGeoImport: remove,
|
removeGeoImport: remove,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- drop onto the globe ----------------------------------------------
|
|
||||||
const container = viewer.container;
|
|
||||||
const gesture = () => {
|
const gesture = () => {
|
||||||
cameraIntent += 1;
|
cameraIntent += 1;
|
||||||
};
|
};
|
||||||
listen(viewer.scene?.canvas || container, 'pointerdown', gesture, {
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
listen(viewer.scene?.canvas || container, 'wheel', gesture, {
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
const hasFiles = (event) =>
|
const hasFiles = (event) =>
|
||||||
[...(event.dataTransfer?.types || [])].includes('Files');
|
[...(event.dataTransfer?.types || [])].includes('Files');
|
||||||
const setDragging = (on) =>
|
const setDragging = (on) =>
|
||||||
doc.body?.classList?.toggle('gev-geo-import-drag', on);
|
doc.body?.classList?.toggle('gev-geo-import-drag', on);
|
||||||
listen(container, 'dragenter', (event) => {
|
|
||||||
if (!hasFiles(event)) return;
|
function detachScene() {
|
||||||
event.preventDefault();
|
if (!scene) return;
|
||||||
dragDepth += 1;
|
const previous = scene;
|
||||||
setDragging(true);
|
scene = null;
|
||||||
});
|
previous.controller.abort();
|
||||||
listen(doc, 'dragover', (event) => {
|
clearTimeout(groundTimer);
|
||||||
if (!hasFiles(event)) return;
|
for (const remove of groundRemovers.splice(0)) remove();
|
||||||
event.preventDefault();
|
for (const [target, type, listener, options] of listeners.splice(0))
|
||||||
if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
|
target.removeEventListener(type, listener, options);
|
||||||
});
|
|
||||||
listen(container, 'dragleave', (event) => {
|
|
||||||
if (!hasFiles(event)) return;
|
|
||||||
dragDepth = Math.max(0, dragDepth - 1);
|
|
||||||
if (!dragDepth) setDragging(false);
|
|
||||||
});
|
|
||||||
listen(doc, 'drop', (event) => {
|
|
||||||
if (!hasFiles(event)) return;
|
|
||||||
event.preventDefault();
|
|
||||||
dragDepth = 0;
|
dragDepth = 0;
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
const files = [...(event.dataTransfer?.files || [])];
|
for (const item of imports) undraw(item, previous.viewer);
|
||||||
if (files.length) void importFiles(files);
|
viewer = null;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function attachScene({
|
||||||
|
viewer: next = null,
|
||||||
|
navigation: nextNavigation = navigation,
|
||||||
|
subscribeMapSource: nextMapSource = subscribeMapSource,
|
||||||
|
} = {}) {
|
||||||
|
if (destroyed) return;
|
||||||
|
detachScene();
|
||||||
|
if (!next) return;
|
||||||
|
viewer = next;
|
||||||
|
// The shell and the map sources of this attachment.
|
||||||
|
navigation = nextNavigation;
|
||||||
|
const offMapSource = nextMapSource?.(scheduleGround);
|
||||||
|
if (typeof offMapSource === 'function') groundRemovers.push(offMapSource);
|
||||||
|
const attached = (scene = { viewer, controller: new AbortController() });
|
||||||
|
ownEvent(viewer.camera?.moveEnd, scheduleGround);
|
||||||
|
ownEvent(viewer.scene?.globe?.tileLoadProgressEvent, (pending) => {
|
||||||
|
if (!pending) scheduleGround();
|
||||||
|
});
|
||||||
|
const container = viewer.container;
|
||||||
|
listen(viewer.scene?.canvas || container, 'pointerdown', gesture, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
listen(viewer.scene?.canvas || container, 'wheel', gesture, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
listen(container, 'dragenter', (event) => {
|
||||||
|
if (!hasFiles(event)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
dragDepth += 1;
|
||||||
|
setDragging(true);
|
||||||
|
});
|
||||||
|
listen(doc, 'dragover', (event) => {
|
||||||
|
if (!hasFiles(event)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
|
||||||
|
});
|
||||||
|
listen(container, 'dragleave', (event) => {
|
||||||
|
if (!hasFiles(event)) return;
|
||||||
|
dragDepth = Math.max(0, dragDepth - 1);
|
||||||
|
if (!dragDepth) setDragging(false);
|
||||||
|
});
|
||||||
|
listen(doc, 'drop', (event) => {
|
||||||
|
if (!hasFiles(event)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
dragDepth = 0;
|
||||||
|
setDragging(false);
|
||||||
|
const files = [...(event.dataTransfer?.files || [])];
|
||||||
|
if (files.length) void importFiles(files);
|
||||||
|
});
|
||||||
|
if (imports.some((item) => !item.dataSource)) {
|
||||||
|
const run = queue.then(() => redraw(attached));
|
||||||
|
queue = run.catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
present();
|
present();
|
||||||
|
|
||||||
@@ -387,26 +498,24 @@ export function initGeoFileImport({
|
|||||||
setVisible,
|
setVisible,
|
||||||
flyTo,
|
flyTo,
|
||||||
remove,
|
remove,
|
||||||
|
attachScene,
|
||||||
|
detachScene,
|
||||||
diagnostics: () => ({
|
diagnostics: () => ({
|
||||||
files: imports.length,
|
files: imports.length,
|
||||||
|
drawn: imports.filter((item) => item.dataSource).length,
|
||||||
entities: imports.reduce((n, item) => n + item.entityCount, 0),
|
entities: imports.reduce((n, item) => n + item.entityCount, 0),
|
||||||
listeners: listeners.length,
|
listeners: listeners.length,
|
||||||
|
attached: Boolean(viewer),
|
||||||
destroyed,
|
destroyed,
|
||||||
}),
|
}),
|
||||||
destroy() {
|
destroy() {
|
||||||
if (destroyed) return;
|
if (destroyed) return;
|
||||||
|
const last = viewer;
|
||||||
|
detachScene();
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
disconnect();
|
disconnect();
|
||||||
lifetime.abort();
|
lifetime.abort();
|
||||||
removeMapSource?.();
|
for (const item of imports.splice(0)) undraw(item, last);
|
||||||
clearTimeout(groundTimer);
|
|
||||||
for (const remove of groundRemovers) remove();
|
|
||||||
for (const item of imports.splice(0)) {
|
|
||||||
viewer.dataSources?.remove?.(item.dataSource, true);
|
|
||||||
item.revoke?.();
|
|
||||||
}
|
|
||||||
for (const [target, type, listener, options] of listeners.splice(0))
|
|
||||||
target.removeEventListener(type, listener, options);
|
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
setHint('');
|
setHint('');
|
||||||
if (globalThis.window?.__gevGeoImport === handle)
|
if (globalThis.window?.__gevGeoImport === handle)
|
||||||
|
|||||||
@@ -329,14 +329,19 @@ test('the application composition wires the import and owns its teardown', () =>
|
|||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
const drawAt = tools.indexOf('const drawTool = initDrawTool({');
|
const drawAt = tools.indexOf('const drawTool = initDrawTool({');
|
||||||
const importAt = tools.indexOf('const geoFileImport = initGeoFileImport({');
|
const importAt = tools.indexOf(
|
||||||
|
"const geoFileImport = sessionTool(layerSession, 'geoImport', defer, () =>",
|
||||||
|
);
|
||||||
assert.ok(importAt > drawAt && drawAt >= 0);
|
assert.ok(importAt > drawAt && drawAt >= 0);
|
||||||
// Fly-to goes through the application shell's navigation policy.
|
// Fly-to goes through the application shell's navigation policy.
|
||||||
assert.match(
|
assert.match(
|
||||||
tools.slice(importAt, importAt + 200),
|
tools.slice(importAt, importAt + 200),
|
||||||
/navigation: styleManager/,
|
/navigation: styleManager/,
|
||||||
);
|
);
|
||||||
assert.ok(tools.indexOf('defer(() => geoFileImport?.destroy());') > importAt);
|
// The session owns the files; this viewer only their drawing.
|
||||||
|
assert.ok(
|
||||||
|
tools.indexOf('defer(() => geoFileImport?.detachScene());') > importAt,
|
||||||
|
);
|
||||||
// The list renders from the store and its controls answer through it.
|
// The list renders from the store and its controls answer through it.
|
||||||
assert.match(
|
assert.match(
|
||||||
tools.slice(importAt, importAt + 400),
|
tools.slice(importAt, importAt + 400),
|
||||||
@@ -624,3 +629,58 @@ test('a refusal shows its counts in the language’s numerals, not the browser
|
|||||||
);
|
);
|
||||||
t.tool.destroy();
|
t.tool.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('an engine switch keeps the files and draws them again on the next viewer', async () => {
|
||||||
|
const t = setup();
|
||||||
|
await t.tool.importFiles([file('a.geojson'), file('b.geojson')]);
|
||||||
|
const [a, b] = rows(t);
|
||||||
|
press(t, b, 'toggle');
|
||||||
|
t.tool.detachScene();
|
||||||
|
// The drawing left the viewer; the list and its rows stayed.
|
||||||
|
assert.equal(t.removed.length, 2);
|
||||||
|
assert.deepEqual(t.revoked.sort(), ['a.geojson', 'b.geojson']);
|
||||||
|
assert.deepEqual(
|
||||||
|
rows(t).map((row) => row.id),
|
||||||
|
[a.id, b.id],
|
||||||
|
);
|
||||||
|
assert.equal(t.viewer.container.listeners.length, 0);
|
||||||
|
assert.equal(t.document.listeners.length, 0);
|
||||||
|
assert.equal(t.tool.flyTo(a.id), false);
|
||||||
|
const refused = await t.tool.importFiles([file('c.geojson')]);
|
||||||
|
assert.equal(refused[0].ok, false);
|
||||||
|
assert.equal(t.tool.list().length, 2);
|
||||||
|
|
||||||
|
// The next viewer reads the kept files again, as they were.
|
||||||
|
const next = {
|
||||||
|
container: new FakeElement('div', 'cesiumContainer'),
|
||||||
|
scene: { requestRender() {} },
|
||||||
|
dataSources: {
|
||||||
|
added: [],
|
||||||
|
add: async (ds) => next.dataSources.added.push(ds),
|
||||||
|
remove() {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
t.loads.length = 0;
|
||||||
|
t.tool.attachScene({ viewer: next });
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
assert.deepEqual(
|
||||||
|
t.loads.map(({ file, opts }) => [file.name, opts.viewer === next]),
|
||||||
|
[
|
||||||
|
['a.geojson', true],
|
||||||
|
['b.geojson', true],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.equal(next.dataSources.added.length, 2);
|
||||||
|
assert.equal(next.dataSources.added[1].show, false, 'b stays hidden');
|
||||||
|
assert.deepEqual(
|
||||||
|
t.tool.list().map(({ id, visible }) => [id, visible]),
|
||||||
|
[
|
||||||
|
[a.id, true],
|
||||||
|
[b.id, false],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.ok(next.container.listeners.length > 0);
|
||||||
|
assert.equal(t.tool.diagnostics().drawn, 2);
|
||||||
|
t.tool.destroy();
|
||||||
|
});
|
||||||
Reference in new issue
Block a user