mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix: keep ALPR camera markers stable during rotation
This commit is contained in:
1 parent
277cd45154
commit
9547658dc7
6 files changed
+251
-9
No files matched your search
@@ -25,6 +25,9 @@ import { createAlprPresentation } from './presentation.js';
|
||||
* Manufacturer, operator and cameraType are optional display fields.
|
||||
* Source label and attribution { name, description, text, href } identify the
|
||||
* actual provider; href must be HTTPS. Provider payload parsing stays in source.
|
||||
* Context services remove obsolete records with
|
||||
* removeEntityContextsForLayer(layerId, { retainIds: Set<string> }), preserving
|
||||
* retained selection without dispatching a new selection event.
|
||||
*/
|
||||
export function createAlprCamerasLayer({ source, services } = {}) {
|
||||
if (typeof source?.fetch !== 'function')
|
||||
@@ -52,6 +55,7 @@ export function createAlprCamerasLayer({ source, services } = {}) {
|
||||
saturated: false,
|
||||
loading: false,
|
||||
abort: null,
|
||||
pendingQueryBox: null,
|
||||
retryTimer: null,
|
||||
retryDelayMs: 0,
|
||||
retryAt: 0,
|
||||
@@ -122,6 +126,7 @@ export function createAlprCamerasLayer({ source, services } = {}) {
|
||||
if (!box) {
|
||||
state.abort?.abort();
|
||||
state.abort = null;
|
||||
state.pendingQueryBox = null;
|
||||
state.loading = false;
|
||||
state.retrying = false;
|
||||
clearUnavailableRetry();
|
||||
@@ -146,6 +151,7 @@ export function createAlprCamerasLayer({ source, services } = {}) {
|
||||
if (state.abort) {
|
||||
state.abort.abort();
|
||||
state.abort = null;
|
||||
state.pendingQueryBox = null;
|
||||
state.loading = false;
|
||||
}
|
||||
renderRecords();
|
||||
@@ -155,10 +161,16 @@ export function createAlprCamerasLayer({ source, services } = {}) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// A move inside an in-flight query must not abort and restart that query.
|
||||
if (state.abort && boxContains(state.pendingQueryBox, box)) {
|
||||
renderRecords();
|
||||
return;
|
||||
}
|
||||
const queryBox = snapAlprBox(box);
|
||||
state.abort?.abort();
|
||||
const requestAbort = new AbortController();
|
||||
state.abort = requestAbort;
|
||||
state.pendingQueryBox = queryBox;
|
||||
state.loading = true;
|
||||
state.retrying = state.status === 'unavailable' || state.retryDelayMs > 0;
|
||||
setAlprStatus('loading');
|
||||
@@ -209,6 +221,7 @@ export function createAlprCamerasLayer({ source, services } = {}) {
|
||||
} finally {
|
||||
if (state.abort === requestAbort) {
|
||||
state.abort = null;
|
||||
state.pendingQueryBox = null;
|
||||
state.loading = false;
|
||||
state.retrying = false;
|
||||
governorRequestRender('alpr-status');
|
||||
@@ -253,6 +266,7 @@ export function createAlprCamerasLayer({ source, services } = {}) {
|
||||
clearTimeout(state.debounceTimer);
|
||||
state.abort?.abort();
|
||||
state.abort = null;
|
||||
state.pendingQueryBox = null;
|
||||
state.loading = false;
|
||||
state.retrying = false;
|
||||
if (state.dataSource) state.dataSource.show = false;
|
||||
|
||||
@@ -25,7 +25,43 @@ export function createAlprPresentation({ state, services, source }) {
|
||||
return Cesium.Color.fromCssColorString(ALPR_COLOR);
|
||||
}
|
||||
|
||||
// The horizon rectangle is unstable during low-angle orbits and can exclude
|
||||
// the ground point in the center of the screen. Bound nearby coverage around
|
||||
// that point instead; the camera-to-ground range keeps zoom-out queries capped.
|
||||
function viewportBox(viewer) {
|
||||
const camera = viewer?.camera;
|
||||
const canvas = viewer?.scene.canvas;
|
||||
if (typeof camera?.pickEllipsoid === 'function' && canvas) {
|
||||
const width = canvas.clientWidth || canvas.width;
|
||||
const height = canvas.clientHeight || canvas.height;
|
||||
if (!width || !height) return null;
|
||||
const focus = camera.pickEllipsoid(
|
||||
new Cesium.Cartesian2(width / 2, height / 2),
|
||||
viewer.scene.globe.ellipsoid,
|
||||
);
|
||||
if (!focus) return null;
|
||||
const location = Cesium.Cartographic.fromCartesian(focus);
|
||||
const range = Cesium.Cartesian3.distance(camera.positionWC, focus);
|
||||
const radius = Math.max(1000, 2 * range);
|
||||
const latitude = Cesium.Math.toDegrees(location.latitude);
|
||||
const longitude = Cesium.Math.toDegrees(location.longitude);
|
||||
const latSpan = radius / 111000;
|
||||
const lonSpan = latSpan / Math.cos(location.latitude);
|
||||
if (
|
||||
!Number.isFinite(latSpan + lonSpan) ||
|
||||
2 * Math.max(latSpan, lonSpan) > MAX_VIEWPORT_DEGREES ||
|
||||
Math.abs(latitude) + latSpan > 90 ||
|
||||
Math.abs(longitude) + lonSpan > 180
|
||||
)
|
||||
return null;
|
||||
return {
|
||||
south: latitude - latSpan,
|
||||
west: longitude - lonSpan,
|
||||
north: latitude + latSpan,
|
||||
east: longitude + lonSpan,
|
||||
};
|
||||
}
|
||||
// Rectangle-only viewers retain the same bounded-area contract.
|
||||
const rectangle = viewer?.camera?.computeViewRectangle(
|
||||
viewer.scene.globe.ellipsoid,
|
||||
);
|
||||
@@ -91,8 +127,19 @@ export function createAlprPresentation({ state, services, source }) {
|
||||
clearSelectedEntityContextForLayer(LAYER_ID);
|
||||
}
|
||||
governorRequestRender('alpr-render');
|
||||
clearRendered();
|
||||
const visibleIds = new Set(visible.map((record) => record.id));
|
||||
for (const entity of [...state.dataSource.entities.values]) {
|
||||
if (!visibleIds.has(entity.id)) state.dataSource.entities.remove(entity);
|
||||
}
|
||||
removeEntityContextsForLayer(LAYER_ID, { retainIds: visibleIds });
|
||||
for (const record of visible) {
|
||||
const existing = state.dataSource.entities.getById(record.id);
|
||||
if (existing?.gevAlprRecord === record) {
|
||||
existing.point.pixelSize = record.id === state.selectedId ? 12 : 8;
|
||||
existing.point.color =
|
||||
record.id === state.selectedId ? Cesium.Color.WHITE : markerColor();
|
||||
continue;
|
||||
}
|
||||
const color = markerColor();
|
||||
const selected = record.id === state.selectedId;
|
||||
const position = Cesium.Cartesian3.fromDegrees(
|
||||
@@ -128,12 +175,32 @@ export function createAlprPresentation({ state, services, source }) {
|
||||
clampToGround: true,
|
||||
};
|
||||
}
|
||||
const entity = state.dataSource.entities.add(entityDef);
|
||||
let entity = existing;
|
||||
// Keep the Cesium entity and its ground-clamping subscription while its
|
||||
// geometry is unchanged. Updating metadata must not rebuild the marker.
|
||||
const previous = entity?.gevAlprRecord;
|
||||
if (!entity) entity = state.dataSource.entities.add(entityDef);
|
||||
else {
|
||||
if (
|
||||
previous.latitude !== record.latitude ||
|
||||
previous.longitude !== record.longitude
|
||||
)
|
||||
entity.position = position;
|
||||
if (
|
||||
previous.latitude !== record.latitude ||
|
||||
previous.longitude !== record.longitude ||
|
||||
previous.directionDeg !== record.directionDeg
|
||||
)
|
||||
entity.polyline = entityDef.polyline;
|
||||
entity.point.pixelSize = selected ? 12 : 8;
|
||||
entity.point.color = selected ? Cesium.Color.WHITE : color;
|
||||
}
|
||||
entity.gevAlprRecord = record;
|
||||
entity.gevTrackedId = record.id;
|
||||
// The mapped camera datum has no elevation; it is not the clamped marker's
|
||||
// visual anchor. Only the selected marker samples the rendered surface,
|
||||
// at most once per second, through Cesium's public height APIs.
|
||||
entity.gevAlprDisplayPosition = null;
|
||||
entity.gevAlprDisplayPosition ??= null;
|
||||
entity.gevDisplayPosition = () => entity.gevAlprDisplayPosition;
|
||||
entity.gevLabelModel = {
|
||||
title: 'ALPR CAMERA',
|
||||
@@ -171,9 +238,7 @@ export function createAlprPresentation({ state, services, source }) {
|
||||
const selectedEntity = state.selectedId
|
||||
? state.dataSource.entities.getById(state.selectedId)
|
||||
: null;
|
||||
if (selectedEntity) selectEntityContext(selectedEntity);
|
||||
else state.selectedId = null;
|
||||
state.lastAnchorSampleAt = 0;
|
||||
if (!selectedEntity) state.selectedId = null;
|
||||
updateSelectedAnchor();
|
||||
// Start only when mapped data is actually displayed, not while an upstream
|
||||
// request or a zoom-in prompt could consume the five-second introduction.
|
||||
|
||||
Reference in new issue
Block a user