mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Fixes the 51 P0–P2 findings from the full review of the i18n branch and the issues found while checking every new string on screen in ru and es. - Translate errors and statuses that still reached ru/es in English: feed and tile errors, provider statuses, the ALPR chip, the traffic mode chip, voice plan step labels, fire feed values, receiver bands, scene save reasons and the Cesium attribution link. - Show preset city names in the UI language (src/ui/placeNames.js); data and everything sent to the model stay English. - Share layer names between the layer panel, HUD and voice card (src/ui/layerNames.js). - Keep long ru/es strings whole: voice card error lines, analyst titles, the voice tier status, the Nepal evidence button, the ALPR retry chip. English layout is unchanged. - Ask before a language switch drops unsaved work. - Catalog fixes: glossary terms (пуск, пресет), gender agreement and consistent "peticiones" in es, fewer calques in both languages. - Fix English text that was itself wrong (e.g. "Exit cockpit to fly to a weather", raw tool ids in voice plan steps). Catalogs: es/ru 2535 messages each, complete. Co-Authored-By: Claude Opus 5.5 <[email protected]>
296 lines
9.1 KiB
JavaScript
296 lines
9.1 KiB
JavaScript
/**
|
|
* Box selection for the Recent Imagery layer: press on the world, drag, and
|
|
* release to hand the layer a degrees box. Follows the draw tool's ownership
|
|
* rules — the pointer is claimed for the session, the viewer's own click
|
|
* actions are borrowed and given back, and `destroy()` returns everything —
|
|
* and freezes the camera only while a drag is in progress.
|
|
*/
|
|
import * as Cesium from 'cesium';
|
|
import { pickWorldFromScreen } from '../annotations/annotationResolver.js';
|
|
import {
|
|
claimPointer,
|
|
pointerOwner,
|
|
releasePointer,
|
|
} from '../data/inputOwnership.js';
|
|
import { validateBox } from '../layers/recentImagery/model.js';
|
|
import { msg } from '../i18n/index.js';
|
|
|
|
/** The id this tool claims the pointer under. */
|
|
export const IMAGERY_BOX_POINTER_OWNER = 'recent-imagery-box';
|
|
|
|
/**
|
|
* Wire the box tool.
|
|
* @param {{ viewer: object, onBox: (box: object) => void, onCancel?: (reason: string, message?: string, box?: object) => void, onActive?: (active: boolean) => void, onEscape?: () => boolean, cesium?: object, pickWorld?: Function, documentRef?: object }} deps
|
|
* `onEscape` runs before the tool cancels on Escape; returning `true` claims
|
|
* the key (it is still consumed) and leaves the tool armed.
|
|
* @returns {{ start: () => boolean, cancel: (reason?: string) => void, isActive: () => boolean, destroy: () => Promise<void> } | null}
|
|
*/
|
|
export function initImageryBoxTool({
|
|
viewer,
|
|
onBox,
|
|
onCancel,
|
|
onActive,
|
|
onEscape,
|
|
cesium = Cesium,
|
|
pickWorld = pickWorldFromScreen,
|
|
documentRef = globalThis.document,
|
|
} = {}) {
|
|
if (!viewer?.scene || typeof onBox !== 'function') return null;
|
|
|
|
let active = false;
|
|
let destroyed = false;
|
|
/** Drag corners in degrees; `anchor` is null while no drag is live. */
|
|
let anchor = null;
|
|
let corner = null;
|
|
let handler = null;
|
|
let lease = null;
|
|
let savedSingleClick = null;
|
|
let savedDoubleClick = null;
|
|
let savedCameraInputs = null;
|
|
|
|
const dragBox = () =>
|
|
anchor && {
|
|
west: Math.min(anchor.lon, corner.lon),
|
|
south: Math.min(anchor.lat, corner.lat),
|
|
east: Math.max(anchor.lon, corner.lon),
|
|
north: Math.max(anchor.lat, corner.lat),
|
|
};
|
|
|
|
const dataSource = new cesium.CustomDataSource(
|
|
'gev-recent-imagery-box-preview',
|
|
);
|
|
let attaching = Promise.resolve(viewer.dataSources.add(dataSource)).catch(
|
|
() => null,
|
|
);
|
|
const stroke = cesium.Color.fromCssColorString('#8be9ff');
|
|
const previewRectangle = () => {
|
|
const box = dragBox();
|
|
return box
|
|
? cesium.Rectangle.fromDegrees(box.west, box.south, box.east, box.north)
|
|
: undefined;
|
|
};
|
|
const previewOutline = () => {
|
|
const box = dragBox();
|
|
if (!box) return [];
|
|
return cesium.Cartesian3.fromDegreesArray([
|
|
box.west,
|
|
box.south,
|
|
box.east,
|
|
box.south,
|
|
box.east,
|
|
box.north,
|
|
box.west,
|
|
box.north,
|
|
box.west,
|
|
box.south,
|
|
]);
|
|
};
|
|
const fill = dataSource.entities.add({
|
|
show: false,
|
|
rectangle: {
|
|
coordinates: new cesium.CallbackProperty(previewRectangle, false),
|
|
material: stroke.withAlpha(0.12),
|
|
classificationType: cesium.ClassificationType.BOTH,
|
|
},
|
|
});
|
|
// A rectangle graphic cannot draw a dashed edge on the ground, so the
|
|
// outline is its own draped polyline traced round the same corners.
|
|
const outline = dataSource.entities.add({
|
|
show: false,
|
|
polyline: {
|
|
positions: new cesium.CallbackProperty(previewOutline, false),
|
|
width: 2,
|
|
clampToGround: true,
|
|
classificationType: cesium.ClassificationType.BOTH,
|
|
material: new cesium.PolylineDashMaterialProperty({
|
|
color: stroke.withAlpha(0.95),
|
|
dashLength: 12,
|
|
}),
|
|
},
|
|
});
|
|
|
|
const requestRender = () => viewer.scene.requestRender?.();
|
|
const setPreview = (visible) => {
|
|
fill.show = visible;
|
|
outline.show = visible;
|
|
requestRender();
|
|
};
|
|
const worldAt = (position) => {
|
|
if (!position) return null;
|
|
const canvas = viewer.scene.canvas;
|
|
const w = canvas?.clientWidth || canvas?.width || 1;
|
|
const h = canvas?.clientHeight || canvas?.height || 1;
|
|
const point = pickWorld(viewer, position.x / w, position.y / h);
|
|
const lon = Number(point?.lon);
|
|
const lat = Number(point?.lat);
|
|
return Number.isFinite(lon) && Number.isFinite(lat) ? { lon, lat } : null;
|
|
};
|
|
const cameraController = () => viewer.scene.screenSpaceCameraController;
|
|
|
|
function freezeCamera() {
|
|
const controller = cameraController();
|
|
if (!controller || savedCameraInputs !== null) return;
|
|
savedCameraInputs = controller.enableInputs;
|
|
controller.enableInputs = false;
|
|
}
|
|
function thawCamera() {
|
|
const controller = cameraController();
|
|
if (controller && savedCameraInputs !== null)
|
|
controller.enableInputs = savedCameraInputs;
|
|
savedCameraInputs = null;
|
|
}
|
|
|
|
function endDrag() {
|
|
anchor = null;
|
|
corner = null;
|
|
thawCamera();
|
|
setPreview(false);
|
|
}
|
|
|
|
const onDown = (event) => {
|
|
if (!active || destroyed || anchor) return;
|
|
const point = worldAt(event.position);
|
|
if (!point) {
|
|
onCancel?.('sky', msg('Press on the ground, not the sky'));
|
|
return;
|
|
}
|
|
anchor = point;
|
|
corner = point;
|
|
freezeCamera();
|
|
setPreview(true);
|
|
};
|
|
const onMove = (event) => {
|
|
const point = anchor && !destroyed && worldAt(event.endPosition);
|
|
if (!point) return;
|
|
corner = point;
|
|
requestRender();
|
|
};
|
|
const onUp = (event) => {
|
|
if (!anchor || destroyed) return;
|
|
corner = worldAt(event.position) || corner;
|
|
const box = dragBox();
|
|
const result = validateBox(box);
|
|
if (result.reason === 'degenerate')
|
|
result.message = msg('Drag to size the box');
|
|
endDrag();
|
|
setActive(false);
|
|
if (result.ok) onBox(result.box);
|
|
else onCancel?.(result.reason, result.message, box);
|
|
};
|
|
// Escape is taken from a document capture listener, ahead of every other
|
|
// handler, so the interceptor runs first: when it claims the key (the
|
|
// panel exits a live comparison), the tool stays armed and the key still
|
|
// goes no further; otherwise Escape cancels the session.
|
|
const onKey = (event) => {
|
|
if (!active || destroyed || event.key !== 'Escape') return;
|
|
event.preventDefault?.();
|
|
event.stopImmediatePropagation?.();
|
|
if (onEscape?.() === true) return;
|
|
cancel('escape');
|
|
};
|
|
|
|
function bindScene() {
|
|
if (handler) return;
|
|
handler = new cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
|
handler.setInputAction(onDown, cesium.ScreenSpaceEventType.LEFT_DOWN);
|
|
handler.setInputAction(onMove, cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
|
handler.setInputAction(onUp, cesium.ScreenSpaceEventType.LEFT_UP);
|
|
const stock = viewer.screenSpaceEventHandler;
|
|
if (stock) {
|
|
savedSingleClick =
|
|
stock.getInputAction(cesium.ScreenSpaceEventType.LEFT_CLICK) || null;
|
|
savedDoubleClick =
|
|
stock.getInputAction(cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK) ||
|
|
null;
|
|
stock.removeInputAction(cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
stock.removeInputAction(cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
|
|
}
|
|
documentRef?.addEventListener?.('keydown', onKey, true);
|
|
documentRef?.body?.classList?.add?.('gev-imagery-box');
|
|
}
|
|
|
|
function releaseScene() {
|
|
if (handler) {
|
|
handler.destroy();
|
|
handler = null;
|
|
}
|
|
const stock = viewer.screenSpaceEventHandler;
|
|
if (stock && savedSingleClick) {
|
|
stock.setInputAction(
|
|
savedSingleClick,
|
|
cesium.ScreenSpaceEventType.LEFT_CLICK,
|
|
);
|
|
}
|
|
if (stock && savedDoubleClick) {
|
|
stock.setInputAction(
|
|
savedDoubleClick,
|
|
cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK,
|
|
);
|
|
}
|
|
savedSingleClick = null;
|
|
savedDoubleClick = null;
|
|
documentRef?.removeEventListener?.('keydown', onKey, true);
|
|
documentRef?.body?.classList?.remove?.('gev-imagery-box');
|
|
}
|
|
|
|
function setActive(next) {
|
|
if (next === active) return active;
|
|
if (next) {
|
|
lease = claimPointer(IMAGERY_BOX_POINTER_OWNER);
|
|
if (!lease) {
|
|
onCancel?.(
|
|
'pointer-busy',
|
|
`${pointerOwner()} is using the pointer — close it first`,
|
|
);
|
|
return active;
|
|
}
|
|
active = true;
|
|
bindScene();
|
|
} else {
|
|
if (anchor) endDrag();
|
|
releaseScene();
|
|
releasePointer(lease);
|
|
lease = null;
|
|
active = false;
|
|
}
|
|
onActive?.(active);
|
|
return active;
|
|
}
|
|
|
|
function cancel(reason = 'cancel') {
|
|
if (!active || destroyed) return;
|
|
setActive(false);
|
|
onCancel?.(reason);
|
|
}
|
|
|
|
return {
|
|
/** Begin a box session. False when the pointer is held elsewhere. */
|
|
start() {
|
|
return !destroyed && setActive(true);
|
|
},
|
|
cancel,
|
|
isActive() {
|
|
return active;
|
|
},
|
|
/** Returns everything; resolves once the preview is detached. */
|
|
destroy() {
|
|
if (destroyed) return attaching;
|
|
if (active) setActive(false);
|
|
destroyed = true;
|
|
releaseScene();
|
|
releasePointer(lease);
|
|
lease = null;
|
|
thawCamera();
|
|
dataSource.entities.removeAll();
|
|
attaching = attaching.then(() => {
|
|
try {
|
|
viewer.dataSources.remove(dataSource, true);
|
|
} catch {
|
|
/* viewer already disposed */
|
|
}
|
|
});
|
|
return attaching;
|
|
},
|
|
};
|
|
}
|