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
- 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
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
render services. Selected cards and Data attribution identify OpenStreetMap.
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,
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
+42
View File
@@ -1082,3 +1082,45 @@ test('ground-centered orbits ignore horizon rectangles while sky, distant and da
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;
}
});
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,
QUERY_SNAP_DEGREES,
QUERY_REUSE_MS,
ALPR_COLOR,
} from './policy.js';
import {
snapAlprBox,
@@ -76,6 +77,7 @@ export function createAlprCamerasLayer({ source, services } = {}) {
presentOnMapCredit,
renderRecords,
selectRecord,
focusNearest,
clearSelection,
updateSelectedAnchor,
installInteraction,
@@ -301,9 +303,38 @@ export function createAlprCamerasLayer({ source, services } = {}) {
state.saturated = false;
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() {
return {
count: state.dataSource?.entities.values.length || 0,
countLabel: state.enabled
? `${state.dataSource?.entities.values.length || 0} nearby`
: '',
lastUpdate: state.lastUpdate,
stale: state.stale,
saturated: state.saturated,
+52
View File
@@ -245,6 +245,57 @@ export function createAlprPresentation({ state, services, source }) {
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) {
const entity = state.dataSource?.entities.getById(id);
if (!entity || !state.recordById.has(id)) return false;
@@ -343,6 +394,7 @@ export function createAlprPresentation({ state, services, source }) {
presentOnMapCredit,
renderRecords,
selectRecord,
focusNearest,
clearSelection,
updateSelectedAnchor,
installInteraction,
+11 -14
View File
@@ -136,9 +136,7 @@ export class LayerPanel {
const count = document.createElement('span');
count.className = 'data-count';
count.textContent = layer.stats.count
? this._formatCount(layer.stats.count)
: '—';
count.textContent = this._layerCountText(layer.stats);
const toggle = document.createElement('button');
toggle.type = 'button';
@@ -202,7 +200,9 @@ export class LayerPanel {
const chip = this._rowControlsFor(layer.id)?.chips?.find(
(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' });
});
row.appendChild(controls);
@@ -213,13 +213,12 @@ export class LayerPanel {
}
}
/**
* Read a layer's optional row-control descriptor, tolerating a throw so one
* misbehaving layer cannot blank the whole panel. Resolved from the registry
* rather than the `getAll()` projection, which deliberately omits `module`.
* @param {string} layerId Registered layer id.
* @returns {{ chips?: Array<object>, legend?: Array<object> }|null} Descriptor.
*/
/** Qualify a loaded count when it does not mean items currently on screen. */
_layerCountText(stats) {
if (typeof stats.countLabel === 'string' && stats.countLabel.trim())
return stats.countLabel;
return stats.count ? this._formatCount(stats.count) : '—';
}
/**
* 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');
if (count) {
count.textContent = layer.stats.count
? this._formatCount(layer.stats.count)
: '—';
count.textContent = this._layerCountText(layer.stats);
}
const meta = row.querySelector('.data-toggle-meta');
+8
View File
@@ -1273,3 +1273,11 @@
#global-context-panel #cockpit-entry kbd {
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;
}