Move Cameras Here
Dispatch gives a location far more often than a bearing. Until now the operator found each
camera that might see it, opened it, and moved it. Move cameras here does that in one
confirmation: pick a point, see who can look at it, move them together, and watch the frames
land. Operators with control rights, behind enable-aim-cameras.
Why it lives on the tile view
The location page is one camera. This action is about several, and the operator needs two things at once: a map to see who will move and how they converge, and a grid of frames to see that each one did. The tile view has both side by side, and its grid already follows the map. Aim mode narrows the grid to the plan's cameras — nearest first, wherever the map extent is — so the tiles the operator watches are exactly the cameras that will move. A plan started from a location page brings the operator to the tile view.
Where it can be started
| Entry | Point |
|---|---|
| Right-click / long-press / Get Coordinates | The coordinates card gets one primary action, Move cameras here |
| An ALERT marker on the map | The alert modal gets an aim button; alerts carry a real incident point |
| A located point (Locate on map chip) | Move cameras here beside Copy PTZ and Go to |
| An AI detection (AI alert modal) | Move cameras to this detection: the detection's own pan/tilt/zoom is cast through terrain to a point first — see the note below |
An AI hit on the map is the camera's arrow turned red; the detection record carries the camera's location, not the smoke's, and still frames carry no box coordinates. So the point is where the camera's aim met terrain when the detection fired, through the same geometry Locate uses, and the plan labels it approximate. Once the backend forwards the detection box, the cast goes through the box centre and the label goes away.
The plan
Candidates are every PTZ camera within 25 miles of the point, nearest first
(buildAimCandidates). For each: distance, true bearing, the camera's own pan and tilt for the
point with its boresight correction undone (cameraPoseForDirection), the tilt from the target's
ground elevation and the camera's eye height with earth curvature and refraction, and the zoom a
half-mile framing would need (zoomForFrameWidth, used only when that mode is chosen). Cameras that cannot be moved are
listed greyed with the reason: offline, no control rights, simulated.
Visibility is terrain line of sight (checkAimVisibility): the sight ray at the target's
apparent altitude, marched over the terrain model until it meets ground. Clear when the first
ground it meets is the target; blocked, with the ridge's distance, otherwise; unknown when terrain
data did not answer inside four seconds. Checks run in parallel for the whole set.
Pre-check: clear, controllable cameras that are not patrolling and not already pointing within a degree of the target, nearest first, up to eight, and one per site: two cameras on one mast see the same thing, so the nearest is ticked and its sibling left listed. Everything else stays listed and can be ticked. Once the operator ticks or unticks anything, the plan stops changing the selection.
The list is grouped so thirty candidates read as one short list: Ready (open, with Select all), Visibility unknown, Blocked by terrain and Unavailable (offline, no control, simulated), the last two folded under a count. The grid shows the tiles in the same order.
Zoom is the operator's call, as a three-way choice above the options: Keep each camera's (the default: the move changes pan and tilt only, since a tight framing was the wrong first look for most fires), Set all to a level on a 1.0–10.0 slider in the zoom slider's own units, or Frame ½ mi at each camera's distance. Each row shows the zoom it will be sent when one is.
Options, on by default: skip cameras already there; one camera per site.
What the operator sees
- Map (
AimMapLayer): a gold target ring; a sight line from every candidate to it, green when clear, grey dashed with the ridge marked when blocked, amber while checking or unknown; ticked cameras drawn heavier; blue while moving, green on arrival, red when skipped or failed. The map frames the point and the pre-checked cameras once. - Panel (
AimPanel, over the map): the list, the options, the count, one button naming the count. Hovering a row lights the camera's tile and its line through the hover state the grid and map already share. On phones and landscape phones the same panel is a bottom sheet (AimPanelHost), opened when the plan starts and reopened from the grid's strip (Choose cameras / Progress); the map there is the mobile map drawer, and the tiles are the page. - Grid (
AimScopeBar+CameraListComponent): narrows to the candidates with a strip naming the point and offering Exit. Candidate tiles stay mounted and in the live set so their poses and frames update. - Tiles (
AimTileBadge+ ring inCameraDetails): gold ring while selected, blue while moving, green when aimed, red when skipped; the badge says which. - Confirmation: the reason form operators already know, once, for the set
(
CameraMovementModalwith a camera-list override). The message is prefilled with the point and the operator's name so every camera's activity log reads the same. - After: per-camera progress; a camera another operator holds comes back skipped · leased by J. Ortiz; a failed request offers Retry; a move with no pose report in 45 seconds reads unconfirmed. The summary stays until Done. Undo all sends every moved camera back to the pose it had.
Guard rails
- Access is one hook (
useAimAccess): flag on, signed in, on the UCSD team (the user profile's EntracompanyNameor SQLagencycarries "UCSD", viauseAdvancedMovementAccess), control rights on at least one camera. Per-camera rights apply again inside the plan, and the PTZ request itself still needsCamera.Operateon the BFF, exactly as a double-click does. - Eight pre-checked, more only by hand, with a warning above eight.
- Leases are honoured, not forced: the lease check runs per camera, a refusal is reported and never retried silently.
- Patrolling cameras are flagged and not pre-checked.
- The plan leaves on Done, Exit, Escape (before anything moves), when access is lost, and after ten idle minutes; it always clears its map graphics.
- Arrival is read from the camera's reported pose (within 1.5°), which the stream sends for the subscribed tiles; the candidates are in the grid, so they are subscribed.
Pieces
| Piece | Role |
|---|---|
util/locate/aimPlan.ts | Candidates, pose, frame zoom, terrain visibility with a budget, arrival, words |
stores/aimStore.ts | Target, candidates, selection, results, options, idle timer input |
hooks/useAimPlan.ts / hooks/useAimMoves.ts | Resolves the plan; sends the moves, watches arrivals, undoes |
hooks/useStartAim.ts | startAim(target) for every entry point |
components/organisms/AimCameras/* | Panel, panel host (portal over the map, idle exit, redirect), scope bar, tile badge, map layer |
util/peakfinder/terrainRaycast.ts | terrainElevationAt, apparentAltitudeDeg exported for the plan |
lib/featureFlags.ts — enable-aim-cameras | Rollout gate; off by default, on for localhost |