mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
feat: add mapped ALPR cameras with bounded source loading
Adapt the ALPR camera contribution to reusable layer components, retaining source attribution, bounded queries, selection and voice controls. Co-authored-by: Bilawal Sidhu <[email protected]>
This commit is contained in:
1 parent
ba7fda74d7
commit
4374c0fa60
28 files changed
+2358
-100
No files matched your search
@@ -0,0 +1,284 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import {
|
||||
LAYER_ID,
|
||||
MAX_VIEWPORT_DEGREES,
|
||||
MAX_RENDERED,
|
||||
DIRECTION_CONE_M,
|
||||
ALPR_COLOR,
|
||||
CREDIT_DISPLAY_MS,
|
||||
} from './policy.js';
|
||||
import { boxContains, destinationPointDeg } from './model.js';
|
||||
|
||||
export function createAlprPresentation({ state, services }) {
|
||||
const { governorRequestRender } = services.render;
|
||||
const {
|
||||
clearSelectedEntityContextForLayer,
|
||||
getSelectedEntityContext,
|
||||
registerEntityContext,
|
||||
removeEntityContextsForLayer,
|
||||
selectEntityContext,
|
||||
} = services.context;
|
||||
|
||||
const { cachedGroundFloor } = services.groundFloor;
|
||||
|
||||
function markerColor() {
|
||||
return Cesium.Color.fromCssColorString(ALPR_COLOR);
|
||||
}
|
||||
|
||||
function viewportBox(viewer) {
|
||||
const rectangle = viewer?.camera?.computeViewRectangle(
|
||||
viewer.scene.globe.ellipsoid,
|
||||
);
|
||||
if (!rectangle) return null;
|
||||
const south = Cesium.Math.toDegrees(rectangle.south);
|
||||
const north = Cesium.Math.toDegrees(rectangle.north);
|
||||
const west = Cesium.Math.toDegrees(rectangle.west);
|
||||
const east = Cesium.Math.toDegrees(rectangle.east);
|
||||
if (
|
||||
!Number.isFinite(south + north + west + east) ||
|
||||
east <= west ||
|
||||
north - south > MAX_VIEWPORT_DEGREES ||
|
||||
east - west > MAX_VIEWPORT_DEGREES
|
||||
)
|
||||
return null;
|
||||
return { south, west, north, east };
|
||||
}
|
||||
|
||||
function clearRendered() {
|
||||
if (state.dataSource?.entities) state.dataSource.entities.removeAll();
|
||||
removeEntityContextsForLayer(LAYER_ID);
|
||||
}
|
||||
|
||||
function hideOnMapCredit() {
|
||||
clearTimeout(state.creditTimer);
|
||||
state.creditTimer = null;
|
||||
if (state.credit)
|
||||
state.viewer?.creditDisplay?.removeStaticCredit(state.credit);
|
||||
governorRequestRender('alpr-credit');
|
||||
}
|
||||
|
||||
function presentOnMapCredit() {
|
||||
if (!state.enabled || state.creditPresented || !state.credit) return;
|
||||
state.creditPresented = true;
|
||||
state.viewer.creditDisplay?.addStaticCredit(state.credit);
|
||||
state.creditTimer = setTimeout(hideOnMapCredit, CREDIT_DISPLAY_MS);
|
||||
governorRequestRender('alpr-credit');
|
||||
}
|
||||
|
||||
function renderRecords() {
|
||||
const selectedContext = getSelectedEntityContext();
|
||||
const box = viewportBox(state.viewer);
|
||||
const visible = box
|
||||
? state.records
|
||||
.filter((record) =>
|
||||
boxContains(box, {
|
||||
south: record.latitude,
|
||||
north: record.latitude,
|
||||
west: record.longitude,
|
||||
east: record.longitude,
|
||||
}),
|
||||
)
|
||||
.slice(0, MAX_RENDERED)
|
||||
: [];
|
||||
// A refresh may retain its own selection, never reclaim one cleared or
|
||||
// replaced by an aircraft, another layer, or a voice action.
|
||||
if (
|
||||
selectedContext?.layerId !== LAYER_ID ||
|
||||
selectedContext.id !== state.selectedId ||
|
||||
!visible.some((record) => record.id === state.selectedId)
|
||||
) {
|
||||
state.selectedId = null;
|
||||
clearSelectedEntityContextForLayer(LAYER_ID);
|
||||
}
|
||||
governorRequestRender('alpr-render');
|
||||
clearRendered();
|
||||
for (const record of visible) {
|
||||
const color = markerColor();
|
||||
const selected = record.id === state.selectedId;
|
||||
const position = Cesium.Cartesian3.fromDegrees(
|
||||
record.longitude,
|
||||
record.latitude,
|
||||
);
|
||||
const entityDef = {
|
||||
id: record.id,
|
||||
position,
|
||||
point: {
|
||||
pixelSize: selected ? 12 : 8,
|
||||
color: selected ? Cesium.Color.WHITE : color,
|
||||
outlineColor: Cesium.Color.BLACK.withAlpha(0.8),
|
||||
outlineWidth: 1,
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
};
|
||||
if (Number.isFinite(record.directionDeg)) {
|
||||
const tip = destinationPointDeg(
|
||||
record.latitude,
|
||||
record.longitude,
|
||||
record.directionDeg,
|
||||
DIRECTION_CONE_M,
|
||||
);
|
||||
entityDef.polyline = {
|
||||
positions: [
|
||||
position,
|
||||
Cesium.Cartesian3.fromDegrees(tip.longitude, tip.latitude),
|
||||
],
|
||||
width: 2,
|
||||
material: color.withAlpha(0.85),
|
||||
clampToGround: true,
|
||||
};
|
||||
}
|
||||
const entity = state.dataSource.entities.add(entityDef);
|
||||
entity.gevTrackedId = record.id;
|
||||
// The raw OSM 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.gevDisplayPosition = () => entity.gevAlprDisplayPosition;
|
||||
entity.gevLabelModel = {
|
||||
title: 'ALPR CAMERA',
|
||||
details: [
|
||||
record.manufacturer ? `Manufacturer: ${record.manufacturer}` : null,
|
||||
record.operator ? `Operator: ${record.operator}` : null,
|
||||
record.cameraType ? `Type: ${record.cameraType}` : null,
|
||||
'Source: OpenStreetMap',
|
||||
].filter(Boolean),
|
||||
accent: color.toCssColorString(),
|
||||
};
|
||||
registerEntityContext(entity, {
|
||||
id: record.id,
|
||||
layerId: LAYER_ID,
|
||||
dataSource: state.dataSource,
|
||||
layerName: 'ALPR Cameras',
|
||||
source: 'OpenStreetMap contributors (ODbL 1.0; community mapped)',
|
||||
label: 'ALPR camera',
|
||||
latitude: record.latitude,
|
||||
longitude: record.longitude,
|
||||
properties: {
|
||||
operator: record.operator,
|
||||
manufacturer: record.manufacturer,
|
||||
cameraType: record.cameraType,
|
||||
zone: record.zone,
|
||||
directionDeg: record.directionDeg,
|
||||
ref: record.ref,
|
||||
lastVerified: record.lastVerified,
|
||||
osmId: record.osmId,
|
||||
sourceTag: record.source,
|
||||
},
|
||||
});
|
||||
}
|
||||
const selectedEntity = state.selectedId
|
||||
? state.dataSource.entities.getById(state.selectedId)
|
||||
: null;
|
||||
if (selectedEntity) selectEntityContext(selectedEntity);
|
||||
else state.selectedId = null;
|
||||
state.lastAnchorSampleAt = 0;
|
||||
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.
|
||||
if (visible.length) presentOnMapCredit();
|
||||
}
|
||||
|
||||
function selectRecord(id) {
|
||||
const entity = state.dataSource?.entities.getById(id);
|
||||
if (!entity || !state.recordById.has(id)) return false;
|
||||
clearSelection();
|
||||
state.selectedId = id;
|
||||
entity.point.pixelSize = 12;
|
||||
entity.point.color = Cesium.Color.WHITE;
|
||||
state.lastAnchorSampleAt = 0;
|
||||
selectEntityContext(entity);
|
||||
updateSelectedAnchor();
|
||||
governorRequestRender('alpr-selection');
|
||||
return true;
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
const entity = state.selectedId
|
||||
? state.dataSource?.entities.getById(state.selectedId)
|
||||
: null;
|
||||
const record = state.recordById.get(state.selectedId);
|
||||
if (entity && record) {
|
||||
entity.point.pixelSize = 8;
|
||||
entity.point.color = markerColor();
|
||||
}
|
||||
state.selectedId = null;
|
||||
clearSelectedEntityContextForLayer(LAYER_ID);
|
||||
governorRequestRender('alpr-selection');
|
||||
}
|
||||
|
||||
function updateSelectedAnchor() {
|
||||
if (!state.enabled || !state.selectedId) return;
|
||||
const selected = getSelectedEntityContext();
|
||||
if (selected?.id !== state.selectedId) {
|
||||
clearSelection();
|
||||
return;
|
||||
}
|
||||
const entity = state.dataSource?.entities.getById(state.selectedId);
|
||||
const record = state.recordById.get(state.selectedId);
|
||||
if (!entity || !record || Date.now() - state.lastAnchorSampleAt < 1000)
|
||||
return;
|
||||
state.lastAnchorSampleAt = Date.now();
|
||||
const scene = state.viewer.scene;
|
||||
const location = Cesium.Cartographic.fromDegrees(
|
||||
record.longitude,
|
||||
record.latitude,
|
||||
);
|
||||
let height;
|
||||
if (scene.sampleHeightSupported) {
|
||||
try {
|
||||
height = scene.sampleHeight(location, [entity]);
|
||||
} catch {
|
||||
/* tiles may still be streaming */
|
||||
}
|
||||
}
|
||||
if (!Number.isFinite(height) && scene.globe.show)
|
||||
height = scene.globe.getHeight?.(location);
|
||||
if (!Number.isFinite(height))
|
||||
height = cachedGroundFloor(record.latitude, record.longitude);
|
||||
if (!Number.isFinite(height)) return;
|
||||
const next = Cesium.Cartesian3.fromDegrees(
|
||||
record.longitude,
|
||||
record.latitude,
|
||||
height,
|
||||
);
|
||||
if (
|
||||
!entity.gevAlprDisplayPosition ||
|
||||
Cesium.Cartesian3.distance(next, entity.gevAlprDisplayPosition) > 0.1
|
||||
) {
|
||||
entity.gevAlprDisplayPosition = next;
|
||||
governorRequestRender('alpr-anchor');
|
||||
}
|
||||
}
|
||||
|
||||
function installInteraction(viewer) {
|
||||
if (state.clickHandler) return;
|
||||
state.clickHandler = new Cesium.ScreenSpaceEventHandler(
|
||||
viewer.scene.canvas,
|
||||
);
|
||||
state.clickHandler.setInputAction((click) => {
|
||||
if (!state.enabled) return;
|
||||
const picked = viewer.scene.pick(click.position);
|
||||
const id = typeof picked?.id?.id === 'string' ? picked.id.id : null;
|
||||
if (
|
||||
id &&
|
||||
state.recordById.has(id) &&
|
||||
(id !== state.selectedId || getSelectedEntityContext()?.id !== id)
|
||||
)
|
||||
selectRecord(id);
|
||||
else if (state.selectedId) clearSelection();
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
}
|
||||
return {
|
||||
markerColor,
|
||||
viewportBox,
|
||||
clearRendered,
|
||||
hideOnMapCredit,
|
||||
presentOnMapCredit,
|
||||
renderRecords,
|
||||
selectRecord,
|
||||
clearSelection,
|
||||
updateSelectedAnchor,
|
||||
installInteraction,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user