mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
Merge main into the Utilities panel, rebuilt on the Vue panel rail
main moved the panels to Vue views and a one-open-panel rail, and removed
the two templates this branch edited. This merge carries the branch onto
that architecture; it adds no behaviour of its own beyond the port.
- The Utilities panel is a RailPanel in LayerPanelsView with its own rail
item (handyman). LayerPanel renders the Directions rows into its list with
LayerRows, and falls back to Data Layers without it.
- Draw leaves DisplayPanel for DrawControls, DrawMarkList and DrawSwatches,
which render what the draw tool publishes to the display store and call
its actions, as GeoImport does. drawMarks.js words the rows; the DOM-built
drawMarkList.js is gone. A row's fly-to is an immediate navigation like
Import's ('drawing' gets its Cockpit refusal), the name field keeps a
draft through v-field-value, and the colours are one radio group with a
single tab stop.
- UiSegmented draws an option's icon, named by its label, for the shape
buttons.
- The catalogs take main's languages with this branch's strings, Arabic
included, and the tests follow the views to their new homes.
This commit is contained in:
commit
4ebb287d36
699 files changed
+46673
-20476
No files matched your search
@@ -6,7 +6,6 @@ import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { initDrawTool } from './drawTool.js';
|
||||
import { describeUnsavedWork } from '../i18n/index.js';
|
||||
import {
|
||||
isPointerFree,
|
||||
pointerOwner,
|
||||
@@ -47,7 +46,6 @@ class FakeElement {
|
||||
this.attributes = new Map();
|
||||
this.textContent = '';
|
||||
this.value = '';
|
||||
this.children = [];
|
||||
this.listeners = [];
|
||||
}
|
||||
setAttribute(name, value) {
|
||||
@@ -65,16 +63,6 @@ class FakeElement {
|
||||
);
|
||||
if (at >= 0) this.listeners.splice(at, 1);
|
||||
}
|
||||
querySelectorAll(selector) {
|
||||
if (selector === '.pp-mode-btn[data-shape]')
|
||||
return this.children.filter((child) => child.dataset.shape);
|
||||
const shape = /\.pp-mode-btn\[data-shape="([a-z]+)"\]/.exec(selector)?.[1];
|
||||
if (shape) return this.children.filter((child) => child.dataset.shape === shape);
|
||||
return [];
|
||||
}
|
||||
querySelector(selector) {
|
||||
return this.querySelectorAll(selector)[0] ?? null;
|
||||
}
|
||||
click() {
|
||||
return this.emit('click');
|
||||
}
|
||||
@@ -108,17 +96,10 @@ function fakeDom() {
|
||||
byId.set(id, element);
|
||||
return element;
|
||||
};
|
||||
// The control whose presence means the page has Draw, and a focused button
|
||||
// whose Enter is its own.
|
||||
make('draw-toggle', 'BUTTON');
|
||||
const modeRow = make('draw-mode-row');
|
||||
for (const shape of ['area', 'line', 'pin']) {
|
||||
const button = new FakeElement('BUTTON');
|
||||
button.dataset.shape = shape;
|
||||
modeRow.children.push(button);
|
||||
}
|
||||
make('draw-label-row');
|
||||
make('draw-colors');
|
||||
make('draw-clear', 'BUTTON');
|
||||
make('draw-hint', 'SPAN');
|
||||
|
||||
const body = new FakeElement('BODY');
|
||||
const documentDouble = {
|
||||
@@ -194,9 +175,15 @@ function fakeViewer() {
|
||||
}
|
||||
|
||||
function fakeAnnotations() {
|
||||
const calls = { annotate: [], clear: 0 };
|
||||
const calls = { annotate: [], clear: 0, restyle: [], setHidden: [], frame: [], remove: [] };
|
||||
const marks = [];
|
||||
const listeners = new Set();
|
||||
const changed = () => listeners.forEach((listener) => listener());
|
||||
const find = (id) => marks.find((mark) => mark.id === id);
|
||||
return {
|
||||
calls,
|
||||
marks,
|
||||
changed,
|
||||
async annotate(specs, options) {
|
||||
calls.annotate.push({ specs, options });
|
||||
return { drawn: specs.length };
|
||||
@@ -204,11 +191,38 @@ function fakeAnnotations() {
|
||||
clear() {
|
||||
calls.clear += 1;
|
||||
},
|
||||
list: () => marks,
|
||||
onChange(listener) {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
restyle(id, change) {
|
||||
calls.restyle.push([id, change]);
|
||||
Object.assign(find(id), change);
|
||||
changed();
|
||||
return true;
|
||||
},
|
||||
setHidden(id, hidden) {
|
||||
calls.setHidden.push([id, hidden]);
|
||||
find(id).hidden = hidden;
|
||||
changed();
|
||||
return true;
|
||||
},
|
||||
frame(id) {
|
||||
calls.frame.push(id);
|
||||
return true;
|
||||
},
|
||||
remove(id) {
|
||||
calls.remove.push(id);
|
||||
marks.splice(marks.indexOf(find(id)), 1);
|
||||
changed();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Stand up a tool against fresh doubles, with globals restored afterwards. */
|
||||
function harness() {
|
||||
function harness({ navigation = null } = {}) {
|
||||
resetPointerOwnership();
|
||||
const { byId, documentDouble } = fakeDom();
|
||||
const viewer = fakeViewer();
|
||||
@@ -217,14 +231,30 @@ function harness() {
|
||||
const previousWindow = globalThis.window;
|
||||
globalThis.document = documentDouble;
|
||||
globalThis.window = globalThis.window || {};
|
||||
const tool = initDrawTool({ viewer, annotations });
|
||||
const shown = { view: null };
|
||||
// The controls' actions, as the display store holds them.
|
||||
const controls = { actions: null };
|
||||
const tool = initDrawTool({
|
||||
viewer,
|
||||
annotations,
|
||||
navigation,
|
||||
publish: (view) => (shown.view = view),
|
||||
connect: (actions) => {
|
||||
controls.actions = actions;
|
||||
return () => (controls.actions = null);
|
||||
},
|
||||
});
|
||||
return {
|
||||
tool,
|
||||
shown,
|
||||
viewer,
|
||||
annotations,
|
||||
byId,
|
||||
documentDouble,
|
||||
element: (id) => byId.get(id),
|
||||
controls,
|
||||
/** A control's change, as the view sends it. */
|
||||
act: (name, ...args) => controls.actions?.[name]?.(...args),
|
||||
restore() {
|
||||
resetPointerOwnership();
|
||||
if (previousDocument === undefined) delete globalThis.document;
|
||||
@@ -240,38 +270,15 @@ const LEFT_DOUBLE_CLICK = Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK;
|
||||
|
||||
/* ── the tests ─────────────────────────────────────────────────────────── */
|
||||
|
||||
// First in the file: tools left by later tests would answer the same registry.
|
||||
test('a lasting mark or a half-drawn shape makes the language switch ask first', async () => {
|
||||
const h = harness();
|
||||
try {
|
||||
let marks = [];
|
||||
h.annotations.list = () => marks;
|
||||
assert.deepEqual(describeUnsavedWork(), [], 'an empty board loses nothing');
|
||||
marks = [{ id: 'a', ttlMs: 8000, expiring: false }];
|
||||
assert.deepEqual(describeUnsavedWork(), [], 'a fading mark is not work');
|
||||
marks = [{ id: 'b', ttlMs: null, expiring: false }];
|
||||
assert.deepEqual(describeUnsavedWork(), ['drawings on the map']);
|
||||
marks = [];
|
||||
h.element('draw-toggle').emit('click');
|
||||
h.tool.addVertex(-97.75, 30.26);
|
||||
assert.deepEqual(describeUnsavedWork(), ['drawings on the map'], 'a shape in progress');
|
||||
marks = [{ id: 'c', ttlMs: null, expiring: false }];
|
||||
await h.tool.destroy();
|
||||
assert.deepEqual(describeUnsavedWork(), [], 'a destroyed tool unregisters');
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('turning Draw on takes a lease and turning it off gives it back', async () => {
|
||||
const h = harness();
|
||||
try {
|
||||
assert.equal(isPointerFree(), true);
|
||||
h.element('draw-toggle').emit('click');
|
||||
h.act('toggleDraw');
|
||||
assert.equal(pointerOwner(), 'draw', 'the session owns the pointer');
|
||||
assert.equal(h.tool.active, true);
|
||||
|
||||
h.element('draw-toggle').emit('click');
|
||||
h.act('toggleDraw');
|
||||
assert.equal(isPointerFree(), true, 'leaving draw mode frees the pointer');
|
||||
assert.equal(h.tool.active, false);
|
||||
} finally {
|
||||
@@ -283,18 +290,18 @@ test('Draw refuses to start when something else holds the pointer', async () =>
|
||||
const h = harness();
|
||||
try {
|
||||
const other = claimPointer('directions');
|
||||
h.element('draw-toggle').emit('click');
|
||||
h.act('toggleDraw');
|
||||
assert.equal(h.tool.active, false, 'it must not half-start');
|
||||
assert.equal(pointerOwner(), 'directions', 'and must not steal the claim');
|
||||
// Named by its tool, not by the internal owner id.
|
||||
assert.equal(
|
||||
h.element('draw-hint').textContent,
|
||||
h.shown.view.hint,
|
||||
'Directions is using the pointer — close it first.',
|
||||
);
|
||||
assert.equal(h.tool.diagnostics().sceneHandler, false, 'no handler was bound');
|
||||
|
||||
releasePointer(other);
|
||||
h.element('draw-toggle').emit('click');
|
||||
h.act('toggleDraw');
|
||||
assert.equal(h.tool.active, true, 'and it starts once the pointer is free');
|
||||
} finally {
|
||||
h.restore();
|
||||
@@ -304,13 +311,13 @@ test('Draw refuses to start when something else holds the pointer', async () =>
|
||||
test('a destroyed instance cannot free the pointer its replacement holds', async () => {
|
||||
const first = harness();
|
||||
try {
|
||||
first.element('draw-toggle').emit('click');
|
||||
first.act('toggleDraw');
|
||||
assert.equal(pointerOwner(), 'draw');
|
||||
// The replacement takes over before the old one finishes tearing down.
|
||||
first.tool.setActive(false);
|
||||
const second = harness();
|
||||
try {
|
||||
second.element('draw-toggle').emit('click');
|
||||
second.act('toggleDraw');
|
||||
assert.equal(pointerOwner(), 'draw');
|
||||
await first.tool.destroy();
|
||||
assert.equal(
|
||||
@@ -335,7 +342,7 @@ test('both stock viewer click actions are borrowed for the session and given bac
|
||||
h.viewer.screenSpaceEventHandler.setInputAction(stockClick, LEFT_CLICK);
|
||||
h.viewer.screenSpaceEventHandler.setInputAction(stockDouble, LEFT_DOUBLE_CLICK);
|
||||
|
||||
h.element('draw-toggle').emit('click');
|
||||
h.act('toggleDraw');
|
||||
assert.equal(
|
||||
h.viewer.screenSpaceEventHandler.getInputAction(LEFT_CLICK),
|
||||
undefined,
|
||||
@@ -346,7 +353,7 @@ test('both stock viewer click actions are borrowed for the session and given bac
|
||||
undefined,
|
||||
);
|
||||
|
||||
h.element('draw-toggle').emit('click');
|
||||
h.act('toggleDraw');
|
||||
assert.equal(
|
||||
h.viewer.screenSpaceEventHandler.getInputAction(LEFT_CLICK),
|
||||
stockClick,
|
||||
@@ -364,7 +371,7 @@ test('both stock viewer click actions are borrowed for the session and given bac
|
||||
test('Enter finishes from the canvas and the label field, never from a focused button', async () => {
|
||||
const h = harness();
|
||||
try {
|
||||
h.element('draw-toggle').emit('click');
|
||||
h.act('toggleDraw');
|
||||
h.tool.setShape('area');
|
||||
h.tool.addVertex(-97.75, 30.26);
|
||||
h.tool.addVertex(-97.74, 30.26);
|
||||
@@ -394,7 +401,7 @@ test('Enter finishes from the canvas and the label field, never from a focused b
|
||||
test('the live preview hangs at the height the click landed on', async () => {
|
||||
const h = harness();
|
||||
try {
|
||||
h.element('draw-toggle').emit('click');
|
||||
h.act('toggleDraw');
|
||||
h.tool.setShape('line');
|
||||
h.tool.addVertex(-97.75, 30.26, 187.5);
|
||||
h.tool.addVertex(-97.74, 30.26, 212.25);
|
||||
@@ -448,7 +455,7 @@ test('destroy releases the pointer, the handler, the listeners and the stock act
|
||||
try {
|
||||
const stockClick = () => 'viewer picks and selects';
|
||||
h.viewer.screenSpaceEventHandler.setInputAction(stockClick, LEFT_CLICK);
|
||||
h.element('draw-toggle').emit('click');
|
||||
h.act('toggleDraw');
|
||||
h.tool.addVertex(-97.75, 30.26);
|
||||
assert.equal(pointerOwner(), 'draw');
|
||||
assert.ok(h.tool.diagnostics().domListeners > 0);
|
||||
@@ -462,7 +469,7 @@ test('destroy releases the pointer, the handler, the listeners and the stock act
|
||||
assert.equal(after.sceneHandler, false, 'the Cesium handler is gone');
|
||||
assert.equal(after.domListeners, 0, 'every DOM listener came off');
|
||||
assert.equal(h.documentDouble.listeners.length, 0, 'including the keydown');
|
||||
assert.equal(h.element('draw-toggle').listeners.length, 0);
|
||||
assert.equal(h.controls.actions, null, 'and the controls are disconnected');
|
||||
assert.equal(
|
||||
h.viewer.screenSpaceEventHandler.getInputAction(LEFT_CLICK),
|
||||
stockClick,
|
||||
@@ -480,15 +487,145 @@ test('destroy releases the pointer, the handler, the listeners and the stock act
|
||||
test('Clear wipes the board and the shape in progress', async () => {
|
||||
const h = harness();
|
||||
try {
|
||||
h.element('draw-toggle').emit('click');
|
||||
h.act('toggleDraw');
|
||||
h.tool.addVertex(-97.75, 30.26);
|
||||
h.tool.addVertex(-97.74, 30.26);
|
||||
assert.equal(h.tool.session.vertices.length, 2);
|
||||
|
||||
h.element('draw-clear').emit('click');
|
||||
h.act('clearDraw');
|
||||
assert.equal(h.annotations.calls.clear, 1, 'placed marks are removed');
|
||||
assert.equal(h.tool.session.vertices.length, 0, 'and so is the shape in progress');
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('the controls choose the shape and colour the next shape is drawn with; it is named in its row', async () => {
|
||||
const h = harness();
|
||||
try {
|
||||
h.act('toggleDraw');
|
||||
h.act('setDrawShape', 'line');
|
||||
h.act('setDrawColor', 'amber');
|
||||
assert.deepEqual(
|
||||
{ ...h.shown.view, hint: undefined },
|
||||
{ active: true, shape: 'line', color: 'amber', hint: undefined, marks: [] },
|
||||
'the view shows what the controls chose',
|
||||
);
|
||||
h.act('setDrawColor', 'purple');
|
||||
assert.equal(h.shown.view.color, 'primary', 'an unknown colour falls back');
|
||||
h.act('setDrawColor', 'cyan');
|
||||
h.tool.addVertex(-97.75, 30.26);
|
||||
// Choosing a shape again starts the open one over.
|
||||
h.act('setDrawShape', 'line');
|
||||
assert.equal(h.tool.session.vertices.length, 0);
|
||||
h.tool.addVertex(-97.75, 30.26);
|
||||
h.tool.addVertex(-97.74, 30.26);
|
||||
h.documentDouble.emit('keydown', { key: 'Enter', target: h.documentDouble.body });
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
const spec = h.annotations.calls.annotate[0].specs[0];
|
||||
assert.equal(spec.type, 'route', 'a drawn line is a manual route');
|
||||
assert.equal(spec.color, 'cyan');
|
||||
assert.ok(!spec.label, 'a shape is named afterwards, in its row');
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('each drawn shape is a row whose actions reach the board, and fly-to is a navigation', async () => {
|
||||
const navigations = [];
|
||||
const h = harness({
|
||||
navigation: {
|
||||
runImmediateNavigation: (noun, navigate) => {
|
||||
navigations.push(noun);
|
||||
return navigate();
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const board = h.annotations;
|
||||
board.marks.push(
|
||||
{ id: 'a1', type: 'route', manual: true, name: null, label: '950 m', color: 'amber', hidden: false },
|
||||
{ id: 'a2', type: 'pin', manual: false, label: 'Voice pin', color: 'primary', hidden: false },
|
||||
);
|
||||
board.changed();
|
||||
assert.deepEqual(
|
||||
h.shown.view.marks.map(({ id, placeholder, hidden }) => ({ id, placeholder, hidden })),
|
||||
[{ id: 'a1', placeholder: 'Line · 950 m', hidden: false }],
|
||||
'only hand-drawn marks are listed, an unnamed line by its length',
|
||||
);
|
||||
h.act('renameDrawMark', 'a1', 'Ridge walk');
|
||||
h.act('recolorDrawMark', 'a1', 'green');
|
||||
h.act('toggleDrawMark', 'a1');
|
||||
h.act('flyToDrawMark', 'a1');
|
||||
h.act('removeDrawMark', 'a1');
|
||||
assert.deepEqual(board.calls.restyle, [
|
||||
['a1', { name: 'Ridge walk' }],
|
||||
['a1', { color: 'green' }],
|
||||
]);
|
||||
assert.deepEqual(board.calls.setHidden, [['a1', true]]);
|
||||
assert.deepEqual(board.calls.frame, ['a1']);
|
||||
assert.deepEqual(navigations, ['drawing'], 'fly-to goes through the navigation policy');
|
||||
assert.deepEqual(board.calls.remove, ['a1']);
|
||||
assert.deepEqual(h.shown.view.marks, [], 'the row goes with its mark');
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('Backspace on the page removes the last vertex', () => {
|
||||
const h = harness();
|
||||
try {
|
||||
h.act('toggleDraw');
|
||||
h.tool.addVertex(-97.75, 30.26);
|
||||
h.tool.addVertex(-97.74, 30.26);
|
||||
h.documentDouble.emit('keydown', { key: 'Backspace', target: h.documentDouble.body });
|
||||
assert.equal(h.tool.session.vertices.length, 1);
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('keys on the shape list are the list\'s, not the drawing\'s', async () => {
|
||||
const h = harness();
|
||||
try {
|
||||
const listButton = new FakeElement('BUTTON');
|
||||
listButton.closest = (selector) => (selector === '#draw-mark-list' ? {} : null);
|
||||
const elsewhere = new FakeElement('INPUT', 'location-search');
|
||||
h.act('toggleDraw');
|
||||
h.tool.addVertex(-97.75, 30.26);
|
||||
h.tool.addVertex(-97.74, 30.26);
|
||||
h.documentDouble.emit('keydown', { key: 'Escape', target: listButton });
|
||||
assert.equal(h.tool.session.vertices.length, 2, 'Escape in the list keeps the shape');
|
||||
assert.equal(h.tool.active, true, 'and keeps Draw on');
|
||||
h.documentDouble.emit('keydown', { key: 'Enter', target: elsewhere });
|
||||
await Promise.resolve();
|
||||
assert.equal(h.annotations.calls.annotate.length, 0, 'another field keeps its Enter');
|
||||
h.documentDouble.emit('keydown', { key: 'Escape', target: h.documentDouble.body });
|
||||
assert.equal(h.tool.session.vertices.length, 0, 'Escape on the page drops the open shape');
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('a page without the Draw controls gets no draw tool', () => {
|
||||
resetPointerOwnership();
|
||||
const previousDocument = globalThis.document;
|
||||
globalThis.document = { getElementById: () => null };
|
||||
try {
|
||||
let connected = false;
|
||||
const tool = initDrawTool({
|
||||
viewer: fakeViewer(),
|
||||
annotations: fakeAnnotations(),
|
||||
connect: () => {
|
||||
connected = true;
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
assert.equal(tool, null);
|
||||
assert.equal(connected, false);
|
||||
} finally {
|
||||
if (previousDocument === undefined) delete globalThis.document;
|
||||
else globalThis.document = previousDocument;
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user