mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +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
|
||||
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user