mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
fix: clarify nearby ALPR counts and help locate cameras
This commit is contained in:
1 parent
7068ee7dc7
commit
fe1aa3b6f6
8 files changed
+186
-14
No files matched your search
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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');
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user