Port ALPR camera visuals into the shared layer

Add camera badges, gradient direction wedges and animated tactical selection cards while preserving bounded loading and layer lifecycle. Align the ALPR data-layer header and retain native pick ownership for overlay badges.

Co-authored-by: manjunath22466 <[email protected]>
This commit is contained in:
Sameh Khamisandmanjunath22466 committed 2026-09-14 17:35:24 -07:00
1 parent 687edc4da7
commit 87629ac175
21 files changed
+951 -97

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+6 -3
View File
@@ -69,7 +69,8 @@ export function createAlprCamerasLayer({ source, services } = {}) {
lastQueryBox: null,
};
const {
markerColor,
initOverlay,
destroyOverlay,
viewportBox,
clearRendered,
hideOnMapCredit,
@@ -250,6 +251,7 @@ export function createAlprCamerasLayer({ source, services } = {}) {
viewer.camera.moveEnd.addEventListener(scheduleLoad);
state.postRenderRemove =
viewer.scene.postRender?.addEventListener(updateSelectedAnchor);
initOverlay();
installInteraction(viewer);
},
enable() {
@@ -281,6 +283,7 @@ export function createAlprCamerasLayer({ source, services } = {}) {
},
destroy(viewer = state.viewer) {
this.disable();
destroyOverlay();
state.moveEndRemove?.();
state.moveEndRemove = null;
state.postRenderRemove?.();
@@ -320,11 +323,11 @@ export function createAlprCamerasLayer({ source, services } = {}) {
],
legend: [
{
label: 'Purple dots',
label: 'Camera badges',
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.',
'Cyan cameras turn coral when selected. Wedges illustrate mapped direction, not measured coverage. Nearby cameras may be outside the screen.',
},
],
};
+298
View File
@@ -0,0 +1,298 @@
import * as Cesium from 'cesium';
import {
MAX_CANVAS_FRUSTUMS,
MARKER_ICON_SIZE,
SELECTED_MARKER_ICON_SIZE,
} from './policy.js';
import {
MARKER_IMAGE,
SELECTED_IMAGE,
BRACKETS_IMAGE,
directionWedgePositions,
paintDirectionWedge,
validAlprGroundHeight,
} from './visuals.js';
/** Per-layer presentation on a caller-owned overlay; Cesium remains the fallback. */
export function createAlprOverlay({ state, services }) {
let lane,
removeMapListener,
retryTimer,
retryUntil = 0;
let records = [],
hits = [],
images = [],
destroyed = false;
let surfaceRegime;
const requestPaint = () => {
if (!destroyed && state.enabled) lane?.requestPaint();
};
function nativeVisible(entity, visible) {
if (!entity) return;
const billboard = entity.billboard;
// Keep a faint native pick target under the canvas badge so sibling layer
// handlers recognize ALPR ownership instead of treating the click as empty.
if (!visible && !entity.gevAlprNativeAppearance) {
entity.gevAlprNativeAppearance = {
position: entity.position,
heightReference: billboard.heightReference,
scaleByDistance: billboard.scaleByDistance,
color: billboard.color,
};
entity.position = entity.gevAlprCanvasPosition;
entity.gevAlprPickPosition = entity.position;
billboard.heightReference = Cesium.HeightReference.NONE;
billboard.scaleByDistance = undefined;
billboard.color = Cesium.Color.WHITE.withAlpha(0.01);
} else if (visible && entity.gevAlprNativeAppearance) {
const saved = entity.gevAlprNativeAppearance;
if (entity.position === entity.gevAlprPickPosition)
entity.position = saved.position;
billboard.heightReference = saved.heightReference;
billboard.scaleByDistance = saved.scaleByDistance;
billboard.color = saved.color;
entity.gevAlprNativeAppearance = null;
entity.gevAlprPickPosition = null;
}
billboard.show = visible;
if (entity.polyline) entity.polyline.show = visible;
if (entity.polygon) entity.polygon.show = visible;
}
function resetAnchors() {
for (const entity of state.dataSource.entities.values) {
entity.gevAlprCanvasPosition = null;
entity.gevAlprWedge = null;
entity.gevAlprDisplayPosition = null;
nativeVisible(entity, true);
}
state.lastAnchorSampleAt = 0;
retryUntil = Date.now() + 15000;
requestPaint();
}
function anchorFor(record, entity) {
if (entity.gevAlprCanvasPosition) return entity.gevAlprCanvasPosition;
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 {
/* streaming tiles */
}
}
if (!validAlprGroundHeight(height) && scene.globe.show)
height = scene.globe.getHeight?.(location);
if (!validAlprGroundHeight(height))
height = services.groundFloor.cachedGroundFloor(
record.latitude,
record.longitude,
);
if (!validAlprGroundHeight(height)) return null;
entity.gevAlprCanvasPosition = Cesium.Cartesian3.fromDegrees(
record.longitude,
record.latitude,
height,
);
entity.gevAlprWedge = directionWedgePositions(record, height);
return entity.gevAlprCanvasPosition;
}
function paint({ ctx, width, height, keyhole, occluder }) {
hits = [];
if (!state.enabled || destroyed) return;
let unresolved = false;
const painted = [];
for (const record of records) {
const entity = state.dataSource.entities.getById(record.id);
if (!entity) continue;
const selected = record.id === state.selectedId;
const image = images[selected ? 1 : 0];
if (
!image?.complete ||
!image.naturalWidth ||
(selected && !images[2]?.naturalWidth)
) {
nativeVisible(entity, true);
continue;
}
const anchor = anchorFor(record, entity);
if (!anchor) {
nativeVisible(entity, true);
unresolved = true;
continue;
}
nativeVisible(entity, false);
if (occluder && !occluder.isPointVisible(anchor)) continue;
const scene = state.viewer.scene;
const origin = Cesium.SceneTransforms.worldToWindowCoordinates(
scene,
anchor,
);
if (!origin) continue;
const alpha =
services.overlays.keyholeAlpha?.(origin.x, origin.y, keyhole) ?? 1;
if (!(alpha > 0)) continue;
const wedge = entity.gevAlprWedge;
if (wedge) {
const left = Cesium.SceneTransforms.worldToWindowCoordinates(
scene,
wedge[1],
);
const right = Cesium.SceneTransforms.worldToWindowCoordinates(
scene,
wedge[2],
);
if (left && right) {
ctx.save();
ctx.globalAlpha *= alpha;
paintDirectionWedge(ctx, origin, left, right, selected);
ctx.restore();
}
}
if (
origin.x < -60 ||
origin.x > width + 60 ||
origin.y < -60 ||
origin.y > height + 60
)
continue;
entity.billboard.show = true;
painted.push({ record, origin, selected, image, alpha });
if (selected) entity.gevAlprDisplayPosition = anchor;
}
// Paint all glyphs after the wedges so one camera's cone cannot wash out another.
for (const { record, origin, selected, image, alpha } of painted) {
const size = selected ? SELECTED_MARKER_ICON_SIZE : MARKER_ICON_SIZE;
ctx.save();
ctx.globalAlpha *= alpha;
ctx.drawImage(
image,
origin.x - size / 2,
origin.y - size / 2,
size,
size,
);
if (selected)
ctx.drawImage(
images[2],
origin.x - size / 2,
origin.y - size / 2,
size,
size,
);
ctx.restore();
hits.push({ id: record.id, x: origin.x, y: origin.y, radius: size / 2 });
}
if (unresolved && !retryTimer && Date.now() < retryUntil) {
retryTimer = setTimeout(() => {
retryTimer = null;
requestPaint();
}, 250);
}
}
return {
init() {
destroyed = false;
if (!services.overlays?.registerPaintLane || typeof Image === 'undefined')
return;
lane = services.overlays.registerPaintLane('selected', paint, {
id: `alpr-${Cesium.createGuid()}`,
active: false,
});
images = [MARKER_IMAGE, SELECTED_IMAGE, BRACKETS_IMAGE].map((src) => {
const image = new Image();
image.onload = requestPaint;
image.onerror = requestPaint;
image.src = src;
return image;
});
surfaceRegime = state.viewer.scene.globe.show;
removeMapListener = services.overlays.subscribeMapStack?.((event) => {
if (event.detail?.status !== 'ready') return;
const next = state.viewer.scene.globe.show;
if (next === surfaceRegime) return;
surfaceRegime = next;
resetAnchors();
});
},
sync(visible) {
if (!lane) return;
for (const record of records)
nativeVisible(state.dataSource.entities.getById(record.id), true);
const position = state.viewer.camera.positionWC;
records = visible
.filter(
(record) =>
Number.isFinite(record.directionDeg) &&
record.id !== state.selectedId,
)
.map((record) => ({
record,
distance: position
? Cesium.Cartesian3.distanceSquared(
position,
Cesium.Cartesian3.fromDegrees(
record.longitude,
record.latitude,
),
)
: 0,
}))
.sort((a, b) => a.distance - b.distance)
.slice(0, MAX_CANVAS_FRUSTUMS)
.map((item) => item.record);
const selected = visible.find((record) => record.id === state.selectedId);
if (selected) {
if (records.length >= MAX_CANVAS_FRUSTUMS) records.pop();
records.push(selected);
}
hits = [];
retryUntil = Date.now() + 15000;
lane.setActive(state.enabled && records.length > 0);
requestPaint();
},
pick(position) {
if (!state.enabled || !position) return null;
let closest,
distance = Infinity;
for (const hit of hits) {
const next = (hit.x - position.x) ** 2 + (hit.y - position.y) ** 2;
if (next <= hit.radius ** 2 && next <= distance) {
closest = hit.id;
distance = next;
}
}
return closest || null;
},
clear() {
clearTimeout(retryTimer);
retryTimer = null;
for (const record of records)
nativeVisible(state.dataSource?.entities.getById(record.id), true);
records = [];
hits = [];
lane?.setActive(false);
},
destroy() {
this.clear();
destroyed = true;
removeMapListener?.();
removeMapListener = null;
lane?.unregister();
lane = null;
for (const image of images) {
image.onload = null;
image.onerror = null;
}
images = [];
},
};
}
+201
View File
@@ -0,0 +1,201 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { createAlprOverlay } from './overlay.js';
import { directionWedgePositions, alprLabelDetails } from './visuals.js';
function harness(t, count = 2) {
const previousImage = globalThis.Image;
const project = Cesium.SceneTransforms.worldToWindowCoordinates;
globalThis.Image = class {
complete = true;
naturalWidth = 128;
};
Cesium.SceneTransforms.worldToWindowCoordinates = () => ({ x: 100, y: 100 });
const records = Array.from({ length: count }, (_, i) => ({
id: `camera:${i}`,
latitude: 30 + i / 10000,
longitude: -97,
directionDeg: 90,
}));
const entities = new Cesium.EntityCollection();
for (const record of records)
entities.add({
id: record.id,
billboard: { image: 'fallback', show: true },
polyline: { show: true },
polygon: { show: true },
});
const state = {
enabled: true,
selectedId: null,
dataSource: { entities },
viewer: {
camera: { positionWC: Cesium.Cartesian3.fromDegrees(-97, 30, 500) },
scene: { globe: { show: true, getHeight: () => 150 } },
},
};
let paint,
mapListener,
active,
removed = 0,
paints = 0;
const overlay = createAlprOverlay({
state,
services: {
groundFloor: { cachedGroundFloor: () => null },
overlays: {
registerPaintLane(_lane, painter) {
paint = painter;
return {
setActive(value) {
active = value;
},
requestPaint() {
paints++;
},
unregister() {
removed++;
},
};
},
keyholeAlpha: () => 1,
subscribeMapStack(fn) {
mapListener = fn;
return () => {
mapListener = null;
};
},
},
},
});
overlay.init();
t.after(() => {
overlay.destroy();
if (previousImage === undefined) delete globalThis.Image;
else globalThis.Image = previousImage;
Cesium.SceneTransforms.worldToWindowCoordinates = project;
});
const draw = [];
const ctx = {
globalAlpha: 1,
save() {},
restore() {},
beginPath() {},
moveTo() {},
lineTo() {},
closePath() {},
stroke() {},
fill() {
draw.push('wedge');
},
createLinearGradient: () => ({ addColorStop() {} }),
drawImage(image) {
draw.push(image.src);
},
};
return {
state,
records,
overlay,
entities,
draw,
paint: (occluder) => paint({ ctx, width: 800, height: 600, occluder }),
mapChanged() {
mapListener({ detail: { status: 'ready' } });
},
get active() {
return active;
},
get removed() {
return removed;
},
get paints() {
return paints;
},
};
}
test('a selected camera without a bearing gets its badge and brackets, never a fabricated wedge', (t) => {
const h = harness(t, 1);
h.records[0].directionDeg = null;
h.state.selectedId = h.records[0].id;
h.overlay.sync(h.records);
h.paint();
assert.equal(h.draw.length, 2);
assert.ok(h.draw[0].endsWith('alpr-marker-selected.png'));
assert.ok(h.draw[1].endsWith('alpr-marker-selected-brackets.png'));
assert.equal(h.overlay.pick({ x: 100, y: 100 }), 'camera:0');
assert.equal(h.entities.getById('camera:0').billboard.show.getValue(), true);
assert.equal(
h.entities.getById('camera:0').billboard.color.getValue().alpha,
0.01,
);
assert.equal(directionWedgePositions(h.records[0]), null);
});
test('the overlay bounds its cohort and promotes selection while leaving farther cameras native', (t) => {
const h = harness(t, 70);
h.state.selectedId = 'camera:69';
h.overlay.sync(h.records);
h.paint();
const hidden = h.entities.values.filter((e) => e.gevAlprNativeAppearance);
assert.equal(hidden.length, 64);
assert.ok(hidden.some((e) => e.id === 'camera:69'));
assert.equal(
h.overlay.pick({ x: 100, y: 100 }),
'camera:69',
'selected glyph paints and picks on top',
);
const firstGlyph = h.draw.findIndex((value) => value !== 'wedge');
assert.equal(firstGlyph, 64, 'all wedges paint before any glyph');
});
test('surface changes invalidate cached anchors, and disable/disposal release painting and hits', (t) => {
const h = harness(t, 1);
h.overlay.sync(h.records);
h.paint();
const entity = h.entities.getById('camera:0');
const first = entity.gevAlprCanvasPosition;
h.paint();
assert.equal(
entity.gevAlprCanvasPosition,
first,
'stable anchor across frames',
);
h.state.viewer.scene.globe.show = false;
h.mapChanged();
assert.equal(entity.gevAlprCanvasPosition, null);
h.state.enabled = false;
h.overlay.clear();
assert.equal(h.active, false);
assert.equal(h.overlay.pick({ x: 100, y: 100 }), null);
assert.equal(entity.billboard.show.getValue(), true);
assert.equal(entity.billboard.color, undefined);
assert.equal(entity.position, undefined);
h.overlay.destroy();
assert.equal(h.removed, 1);
});
test('an unresolved surface preserves the native marker; the horizon removes overlay hits', (t) => {
const h = harness(t, 1);
h.state.viewer.scene.globe.getHeight = () => -15000;
h.overlay.sync(h.records);
h.paint();
assert.equal(h.entities.getById('camera:0').billboard.show.getValue(), true);
assert.equal(h.draw.length, 0);
h.state.viewer.scene.globe.getHeight = () => 150;
h.paint({ isPointVisible: () => false });
assert.equal(h.overlay.pick({ x: 100, y: 100 }), null);
assert.equal(h.draw.length, 0);
});
test('adapter labels never claim a custom source is public OSM data', () => {
assert.deepEqual(
alprLabelDetails(
{ manufacturer: 'Vendor', operator: 'Vendor' },
{ label: 'Custom directory' },
),
['Source: Custom directory', 'VENDOR'],
);
});
+9 -4
View File
@@ -33,8 +33,9 @@ export const QUERY_LIMIT = 1500;
* stayed false and `count` still reported them — a lie about coverage. */
export const MAX_RENDERED = 1500;
/** Meters — length of the facing-direction indicator line, when a camera reports one. */
export const DIRECTION_CONE_M = 25;
/** Meters — illustrative facing wedge depth, when a camera reports a bearing. */
export const DIRECTION_CONE_M = 90;
export const DIRECTION_CONE_HALF_ANGLE_DEG = 20;
export const EARTH_MEAN_RADIUS_M = 6371008.8;
@@ -47,8 +48,12 @@ export const QUERY_SNAP_DEGREES = 0.05;
* for this long before asking Overpass again. */
export const QUERY_REUSE_MS = 10 * 60 * 1000;
/** One vendor-neutral color, distinct from CCTV's cyan markers. */
export const ALPR_COLOR = '#c084fc';
/** Vendor-neutral camera badge palette, with coral selection. */
export const ALPR_COLOR = '#52d4ff';
export const ALPR_SELECTED_COLOR = '#ff6474';
export const MARKER_ICON_SIZE = 38;
export const SELECTED_MARKER_ICON_SIZE = 60;
export const MAX_CANVAS_FRUSTUMS = 64;
/** OSM attribution may collapse after five seconds; full credit stays in Data attribution. */
export const CREDIT_DISPLAY_MS = 5000;
+102 -47
View File
@@ -3,11 +3,22 @@ import {
LAYER_ID,
MAX_VIEWPORT_DEGREES,
MAX_RENDERED,
DIRECTION_CONE_M,
ALPR_COLOR,
ALPR_SELECTED_COLOR,
MARKER_ICON_SIZE,
SELECTED_MARKER_ICON_SIZE,
CREDIT_DISPLAY_MS,
} from './policy.js';
import { boxContains, destinationPointDeg } from './model.js';
import { boxContains } from './model.js';
import { createAlprOverlay } from './overlay.js';
import {
MARKER_IMAGE,
SELECTED_IMAGE,
alprDisplayId,
alprLabelDetails,
directionWedgePositions,
validAlprGroundHeight,
} from './visuals.js';
export function createAlprPresentation({ state, services, source }) {
const { governorRequestRender } = services.render;
@@ -20,6 +31,31 @@ export function createAlprPresentation({ state, services, source }) {
} = services.context;
const { cachedGroundFloor } = services.groundFloor;
const overlay = createAlprOverlay({ state, services });
let visibleRecords = [];
let selectionStartedAt = 0;
function updateAppearance(entity, selected) {
const color = Cesium.Color.fromCssColorString(
selected ? ALPR_SELECTED_COLOR : ALPR_COLOR,
);
entity.billboard.image = selected ? SELECTED_IMAGE : MARKER_IMAGE;
entity.billboard.width = entity.billboard.height = selected
? SELECTED_MARKER_ICON_SIZE
: MARKER_ICON_SIZE;
if (entity.polyline) {
entity.polyline.width = selected ? 3 : 1.75;
entity.polyline.material = color.withAlpha(selected ? 0.98 : 0.82);
}
if (entity.polygon)
entity.polygon.material = color.withAlpha(selected ? 0.8 : 0.2);
if (entity.gevLabelModel) {
entity.gevLabelModel.accent = color.toCssColorString();
entity.gevLabelModel.leaderAnimationStartedAt = selected
? selectionStartedAt
: 0;
}
}
function markerColor() {
return Cesium.Color.fromCssColorString(ALPR_COLOR);
@@ -81,6 +117,8 @@ export function createAlprPresentation({ state, services, source }) {
}
function clearRendered() {
overlay.clear();
visibleRecords = [];
if (state.dataSource?.entities) state.dataSource.entities.removeAll();
removeEntityContextsForLayer(LAYER_ID);
}
@@ -116,6 +154,7 @@ export function createAlprPresentation({ state, services, source }) {
)
.slice(0, MAX_RENDERED)
: [];
visibleRecords = visible;
// A refresh may retain its own selection, never reclaim one cleared or
// replaced by an aircraft, another layer, or a voice action.
if (
@@ -135,9 +174,7 @@ export function createAlprPresentation({ state, services, source }) {
for (const record of visible) {
const existing = state.dataSource.entities.getById(record.id);
if (existing?.gevAlprRecord === record) {
existing.point.pixelSize = record.id === state.selectedId ? 12 : 8;
existing.point.color =
record.id === state.selectedId ? Cesium.Color.WHITE : markerColor();
updateAppearance(existing, record.id === state.selectedId);
continue;
}
const color = markerColor();
@@ -149,31 +186,30 @@ export function createAlprPresentation({ state, services, source }) {
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,
billboard: {
image: selected ? SELECTED_IMAGE : MARKER_IMAGE,
width: selected ? SELECTED_MARKER_ICON_SIZE : MARKER_ICON_SIZE,
height: selected ? SELECTED_MARKER_ICON_SIZE : MARKER_ICON_SIZE,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
scaleByDistance: new Cesium.NearFarScalar(350, 1.15, 4_000_000, 0.48),
},
};
if (Number.isFinite(record.directionDeg)) {
const tip = destinationPointDeg(
record.latitude,
record.longitude,
record.directionDeg,
DIRECTION_CONE_M,
);
const wedge = directionWedgePositions(record);
if (wedge) {
entityDef.polyline = {
positions: [
position,
Cesium.Cartesian3.fromDegrees(tip.longitude, tip.latitude),
],
width: 2,
material: color.withAlpha(0.85),
positions: [wedge[1], position, wedge[2]],
width: selected ? 3 : 1.75,
material: color.withAlpha(0.82),
clampToGround: true,
};
entityDef.polygon = {
hierarchy: wedge,
material: color.withAlpha(0.2),
height: 0,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
classificationType: Cesium.ClassificationType.BOTH,
};
}
let entity = existing;
// Keep the Cesium entity and its ground-clamping subscription while its
@@ -184,16 +220,21 @@ export function createAlprPresentation({ state, services, source }) {
if (
previous.latitude !== record.latitude ||
previous.longitude !== record.longitude
)
) {
entity.position = position;
entity.gevAlprCanvasPosition = null;
entity.gevAlprDisplayPosition = null;
}
if (
previous.latitude !== record.latitude ||
previous.longitude !== record.longitude ||
previous.directionDeg !== record.directionDeg
)
) {
entity.polyline = entityDef.polyline;
entity.point.pixelSize = selected ? 12 : 8;
entity.point.color = selected ? Cesium.Color.WHITE : color;
entity.polygon = entityDef.polygon;
entity.gevAlprCanvasPosition = null;
entity.gevAlprWedge = null;
}
}
entity.gevAlprRecord = record;
entity.gevTrackedId = record.id;
@@ -203,15 +244,19 @@ export function createAlprPresentation({ state, services, source }) {
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: ${source.attribution?.name || source.label || 'Camera source'}`,
].filter(Boolean),
title: alprDisplayId(record),
details: alprLabelDetails(record, source),
accent: color.toCssColorString(),
cardStyle: 'tactical',
selected: true,
leaderStyle: 'elbow',
leaderAnimationMs: 440,
leaderAnimationStartedAt: selected ? selectionStartedAt : 0,
leaderDrawRatio: 0.68,
anchorRadiusPx: SELECTED_MARKER_ICON_SIZE / 2,
anchorRadiusScale: null,
};
updateAppearance(entity, selected);
registerEntityContext(entity, {
id: record.id,
layerId: LAYER_ID,
@@ -240,6 +285,8 @@ export function createAlprPresentation({ state, services, source }) {
: null;
if (!selectedEntity) state.selectedId = null;
updateSelectedAnchor();
overlay.sync(visible);
if (selectedEntity) services.overlays?.refreshReadout?.(selectedEntity);
// 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();
@@ -276,14 +323,14 @@ export function createAlprPresentation({ state, services, source }) {
/* tiles may still be streaming */
}
}
if (!Number.isFinite(height) && state.viewer.scene.globe.show)
if (!validAlprGroundHeight(height) && state.viewer.scene.globe.show)
height = state.viewer.scene.globe.getHeight?.(location);
if (!Number.isFinite(height) || height < -500 || height > 10000)
if (!validAlprGroundHeight(height))
height = cachedGroundFloor(record.latitude, record.longitude);
const center = Cesium.Cartesian3.fromDegrees(
record.longitude,
record.latitude,
Number.isFinite(height) ? height : 0,
validAlprGroundHeight(height) ? height : 0,
);
if (!selectRecord(nearest.id)) return false;
camera.flyToBoundingSphere(new Cesium.BoundingSphere(center, 30), {
@@ -303,11 +350,12 @@ export function createAlprPresentation({ state, services, source }) {
if (!entity || !state.recordById.has(id)) return false;
clearSelection();
state.selectedId = id;
entity.point.pixelSize = 12;
entity.point.color = Cesium.Color.WHITE;
selectionStartedAt = globalThis.performance?.now?.() ?? Date.now();
updateAppearance(entity, true);
state.lastAnchorSampleAt = 0;
selectEntityContext(entity);
updateSelectedAnchor();
overlay.sync(visibleRecords);
governorRequestRender('alpr-selection');
return true;
}
@@ -317,11 +365,10 @@ export function createAlprPresentation({ state, services, source }) {
? 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();
}
if (entity && record) updateAppearance(entity, false);
state.selectedId = null;
selectionStartedAt = 0;
overlay.sync(visibleRecords);
clearSelectedEntityContextForLayer(LAYER_ID);
governorRequestRender('alpr-selection');
}
@@ -335,6 +382,10 @@ export function createAlprPresentation({ state, services, source }) {
}
const entity = state.dataSource?.entities.getById(state.selectedId);
const record = state.recordById.get(state.selectedId);
if (entity?.gevAlprCanvasPosition) {
entity.gevAlprDisplayPosition = entity.gevAlprCanvasPosition;
return;
}
if (!entity || !record || Date.now() - state.lastAnchorSampleAt < 1000)
return;
state.lastAnchorSampleAt = Date.now();
@@ -351,11 +402,11 @@ export function createAlprPresentation({ state, services, source }) {
/* tiles may still be streaming */
}
}
if (!Number.isFinite(height) && scene.globe.show)
if (!validAlprGroundHeight(height) && scene.globe.show)
height = scene.globe.getHeight?.(location);
if (!Number.isFinite(height))
if (!validAlprGroundHeight(height))
height = cachedGroundFloor(record.latitude, record.longitude);
if (!Number.isFinite(height)) return;
if (!validAlprGroundHeight(height)) return;
const next = Cesium.Cartesian3.fromDegrees(
record.longitude,
record.latitude,
@@ -378,7 +429,9 @@ export function createAlprPresentation({ state, services, source }) {
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;
const id =
overlay.pick(click.position) ||
(typeof picked?.id?.id === 'string' ? picked.id.id : null);
if (
id &&
state.recordById.has(id) &&
@@ -389,6 +442,8 @@ export function createAlprPresentation({ state, services, source }) {
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}
return {
initOverlay: () => overlay.init(),
destroyOverlay: () => overlay.destroy(),
markerColor,
viewportBox,
clearRendered,
+106
View File
@@ -0,0 +1,106 @@
import * as Cesium from 'cesium';
import { destinationPointDeg } from './model.js';
import { DIRECTION_CONE_M, DIRECTION_CONE_HALF_ANGLE_DEG } from './policy.js';
// Camera artwork and visual treatment contributed by Manjunath (@manjunath22466).
// Module-relative assets also resolve when the layer is consumed by another app.
export const MARKER_IMAGE = new URL(
'./assets/alpr-marker-normal.png',
import.meta.url,
).href;
export const SELECTED_IMAGE = new URL(
'./assets/alpr-marker-selected.png',
import.meta.url,
).href;
export const BRACKETS_IMAGE = new URL(
'./assets/alpr-marker-selected-brackets.png',
import.meta.url,
).href;
/** Compact display label; selection identity always uses the full record id. */
export function alprDisplayId(record) {
if (!Number.isSafeInteger(record.osmId)) return 'ALPR CAMERA';
return `ALPR-${String(record.osmId).slice(-4).padStart(4, '0')}`;
}
/** Show only supplied metadata, with the actual source named for custom adapters. */
export function alprLabelDetails(record, source) {
const sourceName =
source.attribution?.name || source.label || 'Camera source';
const details = [
sourceName === 'OpenStreetMap' ? 'OSM MAPPED' : `Source: ${sourceName}`,
];
if (Number.isFinite(record.directionDeg))
details.push(`DIRECTION ${Math.round(record.directionDeg)}°`);
const equipment = [
...new Set(
[record.manufacturer, record.operator, record.cameraType]
.map((value) =>
String(value || '')
.trim()
.toUpperCase(),
)
.filter(Boolean),
),
];
if (equipment.length) details.push(equipment.join(' · '));
if (sourceName === 'OpenStreetMap') details.push('PUBLIC MAP DATA');
return details;
}
/** Illustrative bearing wedge, not measured field of view or operating range. */
export function directionWedgePositions(record, heightM = 0) {
if (!Number.isFinite(record.directionDeg)) return null;
const left = destinationPointDeg(
record.latitude,
record.longitude,
record.directionDeg - DIRECTION_CONE_HALF_ANGLE_DEG,
DIRECTION_CONE_M,
);
const right = destinationPointDeg(
record.latitude,
record.longitude,
record.directionDeg + DIRECTION_CONE_HALF_ANGLE_DEG,
DIRECTION_CONE_M,
);
return [
Cesium.Cartesian3.fromDegrees(record.longitude, record.latitude, heightM),
Cesium.Cartesian3.fromDegrees(left.longitude, left.latitude, heightM),
Cesium.Cartesian3.fromDegrees(right.longitude, right.latitude, heightM),
];
}
/** Paint the original cyan/coral gradient with a crisp V-shaped boundary. */
export function paintDirectionWedge(ctx, origin, left, right, selected) {
const rgb = selected ? '255, 100, 116' : '82, 212, 255';
const gradient = ctx.createLinearGradient(
origin.x,
origin.y,
(left.x + right.x) / 2,
(left.y + right.y) / 2,
);
const alpha = selected ? [0.8, 0.48, 0.22, 0.06] : [0.34, 0.2, 0.09, 0.025];
[0, 0.36, 0.72, 1].forEach((stop, i) =>
gradient.addColorStop(stop, `rgba(${rgb}, ${alpha[i]})`),
);
ctx.beginPath();
ctx.moveTo(origin.x, origin.y);
ctx.lineTo(left.x, left.y);
ctx.lineTo(right.x, right.y);
ctx.closePath();
ctx.fillStyle = gradient;
ctx.fill();
ctx.beginPath();
ctx.moveTo(left.x, left.y);
ctx.lineTo(origin.x, origin.y);
ctx.lineTo(right.x, right.y);
ctx.strokeStyle = `rgba(${rgb}, ${selected ? 0.98 : 0.76})`;
ctx.lineWidth = selected ? 2.5 : 1.5;
ctx.lineJoin = 'round';
ctx.stroke();
}
/** Reject transient tile/terrain samples outside plausible mapped ground heights. */
export function validAlprGroundHeight(height) {
return Number.isFinite(height) && height >= -500 && height <= 10000;
}