fix: clarify nearby ALPR counts and help locate cameras

This commit is contained in:
Sameh Khamis committed 2026-09-13 20:35:38 -07:00
1 parent 7068ee7dc7
commit fe1aa3b6f6
8 files changed
+186 -14

No files matched your search

+3
View File
@@ -2,6 +2,9 @@
## ALPR camera locations ## ALPR camera locations
- Label the loaded camera count as nearby, show a purple-dot legend, and add
SHOW NEAREST to frame and select a loaded camera when none are on screen.
- Keep nearby camera markers and selection stable during rotation, use bounded - Keep nearby camera markers and selection stable during rotation, use bounded
ground-centered coverage instead of the horizon rectangle, and reuse in-flight queries. ground-centered coverage instead of the horizon rectangle, and reuse in-flight queries.
+5
View File
@@ -9,6 +9,11 @@ attribution; rendering does not parse Overpass payloads. The standalone
registration supplies selection, picking, terrain and registration supplies selection, picking, terrain and
render services. Selected cards and Data attribution identify OpenStreetMap. render services. Selected cards and Data attribution identify OpenStreetMap.
Layer state and the existing voice layer tools include `alpr-cameras`. Layer state and the existing voice layer tools include `alpr-cameras`.
The row count explicitly says **nearby**: loaded records can be outside the
screen. A purple-dot legend identifies the fixed 8-pixel markers (12 pixels when
selected). **SHOW NEAREST** frames and selects the nearest loaded camera; it is
disabled with no loaded cameras or while following a tracked entity. This action
chooses from the current source snapshot.
Coverage follows a bounded neighborhood around the screen-center ground point, Coverage follows a bounded neighborhood around the screen-center ground point,
with a radius of twice the camera-to-ground range (at least 1 km), rather than with a radius of twice the camera-to-ground range (at least 1 km), rather than
extending to the horizon. Sky, dateline-crossing and wider-than-city views do not extending to the horizon. Sky, dateline-crossing and wider-than-city views do not
+42
View File
@@ -1082,3 +1082,45 @@ test('ground-centered orbits ignore horizon rectangles while sky, distant and da
h.restore(); h.restore();
} }
}); });
test('nearby count and discovery control frame a real loaded camera without fetching or stealing tracking', async () => {
const h = cameraHarness();
const flights = [];
h.viewer.camera.positionWC = Cesium.Cartesian3.fromDegrees(
-97.7431,
30.2672,
600,
);
h.viewer.camera.flyToBoundingSphere = (sphere, options) =>
flights.push({ sphere, options });
try {
assert.equal(alprCamerasLayer.getRowControls().chips[0].disabled, true);
await alprCamerasLayer.update();
assert.equal(alprCamerasLayer.getStats().countLabel, '1 nearby');
const controls = alprCamerasLayer.getRowControls();
assert.equal(controls.legend[0].label, 'Purple dots');
assert.equal(controls.chips[0].onClick(), true);
assert.equal(flights.length, 1);
assert.equal(getSelectedEntityContext().id, 'alpr:42');
const center = Cesium.Cartographic.fromCartesian(flights[0].sphere.center);
assert.ok(
Math.abs(Cesium.Math.toDegrees(center.latitude) - 30.2672) < 1e-9,
);
assert.equal(flights[0].options.offset.range, 800);
assert.equal(h.requests.length, 1);
h.viewer.trackedEntity = {};
assert.equal(alprCamerasLayer.getRowControls().chips[0].disabled, true);
assert.equal(
controls.chips[0].onClick(),
false,
'a stale button cannot steal the follow camera',
);
h.viewer.trackedEntity = undefined;
alprCamerasLayer.disable();
assert.equal(alprCamerasLayer.getStats().countLabel, '');
assert.equal(controls.chips[0].onClick(), false);
assert.equal(flights.length, 1);
} finally {
h.restore();
}
});
+34
View File
@@ -3302,3 +3302,37 @@ test('panel remount releases old listeners and destruction revokes retained cont
else globalThis.document = originalDocument; else globalThis.document = originalDocument;
} }
}); });
test('row action chips use live disabled state and descriptive counts without writing parameters', async () => {
const originalDocument = globalThis.document;
globalThis.document = { createElement: makeControlElement };
const mgr = new DataLayerManager({});
const layer = makeRowControlLayer();
let calls = 0, blocked = false;
layer.module.getStats = () => ({ count: 3, countLabel: '3 nearby' });
layer.module.getRowControls = () => ({ chips: [{ id: 'find', label: 'SHOW NEAREST', disabled: blocked, onClick: () => calls++ }] });
layer.module.setParams = () => { throw new Error('navigation must not write persistent params'); };
mgr.register(layer.module);
const container = makeControlElement();
try {
mgr.buildTogglePanel(container);
await mgr.setEnabled('satellites', true);
const row = container.querySelector('[data-layer-id="satellites"]');
assert.equal(row.querySelector('.data-count').textContent, '3 nearby');
const controls = row.querySelector('.data-toggle-controls');
const chip = collectByClass(controls, 'data-toggle-chip')[0];
controls.listeners.click({ target: chip });
assert.equal(calls, 1);
blocked = true;
controls.listeners.click({ target: chip });
assert.equal(calls, 1, 'live disabled descriptor wins before row repaint');
blocked = false;
await mgr.setEnabled('satellites', false);
controls.listeners.click({ target: chip });
assert.equal(calls, 1, 'disabled layer cannot navigate');
} finally {
await mgr.destroyAll();
if (originalDocument === undefined) delete globalThis.document;
else globalThis.document = originalDocument;
}
});
+31
View File
@@ -6,6 +6,7 @@ import {
MAX_RENDERED, MAX_RENDERED,
QUERY_SNAP_DEGREES, QUERY_SNAP_DEGREES,
QUERY_REUSE_MS, QUERY_REUSE_MS,
ALPR_COLOR,
} from './policy.js'; } from './policy.js';
import { import {
snapAlprBox, snapAlprBox,
@@ -76,6 +77,7 @@ export function createAlprCamerasLayer({ source, services } = {}) {
presentOnMapCredit, presentOnMapCredit,
renderRecords, renderRecords,
selectRecord, selectRecord,
focusNearest,
clearSelection, clearSelection,
updateSelectedAnchor, updateSelectedAnchor,
installInteraction, installInteraction,
@@ -301,9 +303,38 @@ export function createAlprCamerasLayer({ source, services } = {}) {
state.saturated = false; state.saturated = false;
state.viewer = null; state.viewer = null;
}, },
getRowControls() {
const count = state.dataSource?.entities.values.length || 0;
return {
chips: [
{
id: 'find-camera',
label: 'SHOW NEAREST',
title: state.viewer?.trackedEntity
? 'Stop following the current object before navigating to a camera'
: 'Move to the nearest loaded camera and show its details',
disabled:
!state.enabled || !count || Boolean(state.viewer?.trackedEntity),
onClick: focusNearest,
},
],
legend: [
{
label: 'Purple dots',
color: ALPR_COLOR,
count,
blurb:
'Nearby mapped cameras may be outside the screen. Dots stay 8 pixels wide as you zoom; click one for details.',
},
],
};
},
getStats() { getStats() {
return { return {
count: state.dataSource?.entities.values.length || 0, count: state.dataSource?.entities.values.length || 0,
countLabel: state.enabled
? `${state.dataSource?.entities.values.length || 0} nearby`
: '',
lastUpdate: state.lastUpdate, lastUpdate: state.lastUpdate,
stale: state.stale, stale: state.stale,
saturated: state.saturated, saturated: state.saturated,
+52
View File
@@ -245,6 +245,57 @@ export function createAlprPresentation({ state, services, source }) {
if (visible.length) presentOnMapCredit(); if (visible.length) presentOnMapCredit();
} }
function focusNearest() {
const camera = state.viewer?.camera;
if (!state.enabled || !camera?.positionWC || state.viewer.trackedEntity)
return false;
let nearest = null,
distance = Infinity;
for (const entity of state.dataSource.entities.values) {
const position = entity.position.getValue(Cesium.JulianDate.now());
const candidate = Cesium.Cartesian3.distanceSquared(
camera.positionWC,
position,
);
if (candidate < distance) {
nearest = entity;
distance = candidate;
}
}
if (!nearest) return false;
const record = state.recordById.get(nearest.id);
const location = Cesium.Cartographic.fromDegrees(
record.longitude,
record.latitude,
);
let height;
if (state.viewer.scene.sampleHeightSupported) {
try {
height = state.viewer.scene.sampleHeight(location, [nearest]);
} catch {
/* tiles may still be streaming */
}
}
if (!Number.isFinite(height) || height < -500 || height > 10000)
height = cachedGroundFloor(record.latitude, record.longitude);
const center = Cesium.Cartesian3.fromDegrees(
record.longitude,
record.latitude,
Number.isFinite(height) ? height : 0,
);
if (!selectRecord(nearest.id)) return false;
camera.flyToBoundingSphere(new Cesium.BoundingSphere(center, 30), {
duration: 1.2,
offset: new Cesium.HeadingPitchRange(
camera.heading || 0,
-Math.PI / 4,
800,
),
});
governorRequestRender('alpr-focus');
return true;
}
function selectRecord(id) { function selectRecord(id) {
const entity = state.dataSource?.entities.getById(id); const entity = state.dataSource?.entities.getById(id);
if (!entity || !state.recordById.has(id)) return false; if (!entity || !state.recordById.has(id)) return false;
@@ -343,6 +394,7 @@ export function createAlprPresentation({ state, services, source }) {
presentOnMapCredit, presentOnMapCredit,
renderRecords, renderRecords,
selectRecord, selectRecord,
focusNearest,
clearSelection, clearSelection,
updateSelectedAnchor, updateSelectedAnchor,
installInteraction, installInteraction,
+11 -14
View File
@@ -136,9 +136,7 @@ export class LayerPanel {
const count = document.createElement('span'); const count = document.createElement('span');
count.className = 'data-count'; count.className = 'data-count';
count.textContent = layer.stats.count count.textContent = this._layerCountText(layer.stats);
? this._formatCount(layer.stats.count)
: '—';
const toggle = document.createElement('button'); const toggle = document.createElement('button');
toggle.type = 'button'; toggle.type = 'button';
@@ -202,7 +200,9 @@ export class LayerPanel {
const chip = this._rowControlsFor(layer.id)?.chips?.find( const chip = this._rowControlsFor(layer.id)?.chips?.find(
(entry) => entry.id === button.dataset.chipId, (entry) => entry.id === button.dataset.chipId,
); );
if (chip?.params) if (!chip || chip.disabled || !this.isEnabled(layer.id)) return;
if (typeof chip.onClick === 'function') chip.onClick();
else if (chip.params)
this.setLayerParams(layer.id, chip.params, { origin: 'user' }); this.setLayerParams(layer.id, chip.params, { origin: 'user' });
}); });
row.appendChild(controls); row.appendChild(controls);
@@ -213,13 +213,12 @@ export class LayerPanel {
} }
} }
/** /** Qualify a loaded count when it does not mean items currently on screen. */
* Read a layer's optional row-control descriptor, tolerating a throw so one _layerCountText(stats) {
* misbehaving layer cannot blank the whole panel. Resolved from the registry if (typeof stats.countLabel === 'string' && stats.countLabel.trim())
* rather than the `getAll()` projection, which deliberately omits `module`. return stats.countLabel;
* @param {string} layerId Registered layer id. return stats.count ? this._formatCount(stats.count) : '—';
* @returns {{ chips?: Array<object>, legend?: Array<object> }|null} Descriptor. }
*/
/** /**
* Render a layer's row chips and color legend, and keep the whole block * Render a layer's row chips and color legend, and keep the whole block
@@ -307,9 +306,7 @@ export class LayerPanel {
const count = row.querySelector('.data-count'); const count = row.querySelector('.data-count');
if (count) { if (count) {
count.textContent = layer.stats.count count.textContent = this._layerCountText(layer.stats);
? this._formatCount(layer.stats.count)
: '—';
} }
const meta = row.querySelector('.data-toggle-meta'); const meta = row.querySelector('.data-toggle-meta');
+8
View File
@@ -1273,3 +1273,11 @@
#global-context-panel #cockpit-entry kbd { #global-context-panel #cockpit-entry kbd {
padding: 2px 4px; padding: 2px 4px;
} }
/* Qualified nearby counts must also fit beside the wider loading chip. */
.data-toggle-row[data-layer-id='alpr-cameras'] .data-toggle-top {
flex-wrap: wrap;
}
.data-toggle-row[data-layer-id='alpr-cameras'] .data-toggle-right {
margin-left: auto;
}