Go to Position
Double-clicking the 60° image is the way to move a camera, but it has no answer to "look at 245°", "set it to 132.4 / −2 / 3.5" or "a touch more to the left". Until now the only path was to double-click repeatedly and watch the heading tick. Go to position adds a typed move: three fields, one Move button, the same pipeline as a double-click underneath.
Operators with control rights only, behind enable-go-to-position. No new button in the
control column.
Where it lives
- The heading pill. The pan readout on the top edge of the 60° image is already the number operators care about. For operators it is now a button: hovering it shows a pencil, clicking it drops the form down from the pill. Viewers see the plain readout exactly as before.
- The "…" menu has a
Go to position…row — the discoverable copy for people who look there first (it is where Adjust Focus and Adjust Brightness live). - The mobile more-options drawer has a
Go to positionrow that opens a bottom sheet. The drawer has no PTZ controls at all, so on a phone this is the only typed-move entry.
All three open the same thing through goToPositionStore.isOpen; GoToPositionHost
renders the popover on desktop and the sheet on phones and landscape phones.
The form
Go to / Move by. Two modes, remembered for the session.
- Go to prefills the three fields with the current pose, so the operator changes only what
they mean to. A blank field keeps the current value. A value starting with
+is read as a step even here —+never means an absolute value, so it is safe to. - Move by starts blank; every number is a signed step from where the camera is:
-5turns left five degrees,+2tilts up two,+1zooms in one slider unit.
Units match what operators already see. Pan is absolute degrees, shown 0–360 like the
tick (typing -30 is fine; it becomes 330.00). Tilt is degrees, −90 to 20. Zoom is in the
slider's units — 0.0 to 10.0, i.e. VAPIX ÷ 1000 — because that is the only zoom number
anyone sees; the request carries the VAPIX integer 1–9999. Typing 0 or 10 lands on the
real limits.
The summary line names the destination before anything moves: → 144.70° · 3.90° · 3.5× · ≈ 18° wide (the field of view comes from the camera model's optics table, so the zoom
number means something), or in Move by mode turn right 5.00° · tilt up 2.00° → 149.70° · 5.90°. A field that cannot be sent is highlighted with the reason (Tilt is −90.00° to 20.00°, That would zoom to 11.0×. Zoom is 0.0 to 10.0), and Move stays disabled while the
form resolves to "already there" or the camera is already moving.
Keys. Enter moves, Escape dismisses, Tab walks the fields.
What a move does
Move writes goToPositionTarget to cameraMovementStore. XYMouseTrackerOverlay (the
'camera' instance only) picks it up and sends it through moveCamera — the lease check,
the reason/message modal when one is needed, pendingPTZUpdate, "Camera Moving…", the
activity log — exactly as a double-click. Zoom is null when it was not changed, which the
request already treats as "keep the current zoom".
From Locate on map
Locate on map already turns a click on the smoke into a bearing and a
point. Its result card — and the Peak Finder overlay's own Locate card, and the point's chip on
the map — now print this camera's pan and tilt for that point (camera pan 213.40° · tilt −1.25°) and offer two buttons (PtzActions):
- Copy PTZ puts one plain line on the clipboard:
213.40, -1.25. Paste it into any field of this form and it spreads across pan and tilt (a third number is zoom in slider units), so the numbers travel to another operator or another screen as one paste, not three. - Go to (operators with control rights, while that camera is selected) leaves locate mode and opens this form prefilled.
The copied report carries the same pan/tilt line.
The pan/tilt is the inverse of the boresight correction Locate applies to the camera's aim
(cameraPoseForDirection in util/locate/sightline.ts): the sight line is cast in true
bearings, so a camera with a solved mount error needs its own numbers back, not the bearing.
Without a solve the two are identical.
The aim guide previews it
While the form is open and holds a valid, non-trivial destination, goToPositionStore.preview
carries it, and the aim guide (when it is on) draws it before the move is
sent: a dashed gold ring where the destination lands in the frame with a go to 144.70° · 3.90° chip, or, when the destination is outside the frame, a chevron on the frame edge in
the direction of the move with turn right 85.00° · tilt up 12.00° → … along the bottom. A
wrong digit is visible before the camera moves.
Pieces
| Piece | Role |
|---|---|
util/cameras/goToPosition.ts | Pure rules: parsing, absolute/relative resolution, limits, zoom unit conversion, words |
stores/goToPositionStore.ts | isOpen, mode, preview (session only) |
hooks/useGoToPositionAccess.ts | The one access rule every entry point and the host share |
GoToPositionForm | The shared body (popover and sheet) |
GoToPositionPopover / GoToPositionSheet | Desktop drop-down under the heading pill / mobile bottom sheet |
GoToPositionHost | Picks the surface; rendered by CameraImageWrapper beside the other photo overlays |
MeasurementTickOverlay | The heading pill becomes the button for operators |
cameraMovementStore.goToPositionTarget | The hand-off to the movement overlay |
lib/featureFlags.ts — enable-go-to-position | Rollout gate; off by default, on for localhost |
Guard rails
- Access is one hook: flag on, signed in, on the UCSD team (the user profile's Entra
companyNameor SQLagencycarries "UCSD", viauseAdvancedMovementAccess),canControlCamera, not in playback, movement slider in its normalzoommode. Losing access while the form is open closes it. - Only the
'camera'overlay instance sends the target, so the 360 overlay mounted alongside never doubles the move. - The popover swallows its own double-clicks so the image's handler never sees them.
- The draft is seeded at open and on a mode switch; it does not chase the live pose, or a frame landing mid-typing would overwrite what is being entered.
- No per-camera pan/tilt limits exist in the data today. Tilt uses the same ceiling the drag-pan code uses (20°) and a −90° floor; pan is unbounded and folded, as Axis wraps it.