Skip to main content

Moving a Camera on a Mountain

An operator sees a wisp of smoke at the edge of a camera frame. They double-click it. Three hundred miles away, on a ridge above a canyon, a motor turns and the camera swings to center on that exact spot.

This chapter is about everything that happens between the double-click and the motor. It's the most consequential feature in the app — a real machine moves, other agencies are watching the same camera, and every movement is audited — so it's worth learning properly. The good news: the whole system hangs on one rule, and once you have it, every movement path in the codebase becomes predictable.

The one rule: only the overlay drives

Here it is: exactly one component ever sends a PTZ command. XYMouseTrackerOverlay — a transparent div stretched over the camera image — is the only code in the app that calls the movement endpoints.

But wait, you've seen the UI. There's a Home button. A zoom slider. AI alerts have a "Move Camera Here." The activity log can replay a movement. None of them are wired to the API?

None of them. Think of cameraMovementStore as a request board: every button writes its wish onto the board ("go home," "zoom to 4," "move to this alert"), and the overlay — the only licensed driver — watches the board and executes. That indirection is why a mounted overlay is a prerequisite for any movement at all, and why debugging is simple: whatever moved the camera, the trace runs through one file.

The journey, stop by stop

double-click / drag on the overlay
→ transformMousePositionToImage() pixels → pan/tilt degrees
→ moveCamera(payload, isLeaseConfirmed)
→ IsCameraLeased(cameraId) who holds the camera right now?
→ leased by someone else, or first movement → CameraMovementModal (reason code)
→ otherwise straight through
→ callMoveCamera() rounds zoom, POSTs /ptz
→ SignalR pushes the new position + lease timer back into the stores

Two stops deserve a pause.

The math stop. A pixel means nothing to a camera; it thinks in degrees. The transform that converts "you clicked 78% across the frame" into "+19.1° of pan" depends on the camera's current zoom — the same pixel is a different angle when zoomed in. That arithmetic gets its own lesson (the overlay) and a full deep dive (PTZ Pixel Math).

The lease stop. Cameras are shared between agencies, so before moving, the app asks the backend who holds this camera right now — on every movement, not once per session. Your first movement always raises the reason-code modal (there's no stored payload yet); after that, the stored payload is reused as a template and the modal stays out of your way — unless someone else has taken the camera. The full multi-operator protocol is the camera-lease deep dive.

Two tools, three panel modes

Two independent switches shape what your input means, and confusing them is the classic first-week mistake:

  • cameraMovementMode (panTool | zoomTool) decides what a drag does: pan the camera, or draw a box the camera zooms into. Double-click behaves the same in both.
  • movementSliderMode (zoom | focus | brightness) decides what the control panel shows — and it gates movement itself: the overlay is only mounted in zoom mode, or while a focus/brightness change is being applied.

One axis is about your gesture, the other about the panel. Keep them separate in your head and the control components (next lesson after rendering) read easily.

Why the modal asks "why"

Every movement is a statement to every other agency watching that camera — so every movement carries a reason code, audited into the activity log. The codes (CameraMovementConstants.ts): Life Safety, AI Smoke Check, Report of Fire, Monitor Fire, Area of Interest, Weather Activity, Testing System, DEMO/Training, Camera Moved Home, Focus Adjustment, Brightness Adjustment, Other Reason.

Two details worth knowing before you analyze that audit trail: choosing Life Safety auto-sets the high-priority flag, and two codes are never picked by a human — Camera Moved Home is written by moveToHome(), and the focus/brightness codes come from those panels. The code distribution is not a pure record of user intent.

Your path through this chapter

  1. Rendering the camera view — how the image gets on screen, and why the 360° strip isn't a scroll container.
  2. The control panel — the buttons, their gates, and their three mutually exclusive layouts.
  3. The overlay — where gestures become degrees, with two live demos.
  4. Image-to-map calculations — the formula reference behind it all.

Take them in order — each one leans on the last.