Skip to main content

The Click That Offers a Camera

Click a camera arrow on the map and… nothing navigates. That's deliberate — an operator scanning the map mid-incident shouldn't be yanked into a camera view by a stray click. Instead, a quiet snackbar appears bottom-right: "View Bald Mountain North" — an invitation, not a command. Sites with several cameras get a dropdown to pick one. That snackbar is GraphicNotification (components/molecules/MapNotifications/), and it's a nice little case study in driving React UI from non-React code.

It takes no props. Everything comes from graphicNotificationStore, which is what lets non-React code — the map's click handler — open it.

The click path

FeatureMap.onCameraClicked(arrowHit, pointHit) reads the store imperatively via getState(), because it runs from an ArcGIS event handler rather than a render:

useGraphicNotificationStore.getState().showGraphicNotification(cameras, siteId);

An arrow hit passes that one camera. A point hit resolves the clicked camera's sid, then passes every camera at that site via getCamerasBySiteId. Two guards suppress the notification entirely: clicking the camera already open (?camera= in the URL), and clicking a single-camera site that is already open.

FeatureMap renders the component only on desktop — !isMobile && !isLandscape. On mobile and landscape the same click opens MoreOptionsDrawerContent in a bottom drawer instead, so the notification path and the drawer path never both fire.

What the store does, and what it doesn't

showGraphicNotification normalises whatever camera model it is handed (CameraLocationAndImageModel or AllStaticCamera) into a flat shape — cId→id, cn→name, isRecentlyMoved→lastMoved, time→lastMovedTime, plus siteLabel — sorts by lastMovedTime descending, and sets open: true.

It does not pick the primary camera. That selection lives in the component, because it depends on the URL, which the store can't see.

closeGraphicNotification sets open: false and deliberately leaves cameras populated, so reopening at the same site doesn't need the data passed again.

Primary camera selection

When cameras changes, a useEffect runs:

candidates      = cameras where id ≠ ?camera= in URL
primaryCamera = candidates.find(lastMoved) ?? candidates[0] ?? cameras[0]

A recently-moved camera wins, because it is the one most likely to be showing something. The final fallback to cameras[0] covers the case where the only camera at the site is the one already open. On cleanup the effect calls setPrimaryCamera(null).

What the reader sees

With one camera: a single link, View <name>, truncated to 20 characters by truncateWithEllipsis. With more than one: the same link plus a CameraMenu chevron.

CameraMenu sorts its items alphabetically by siteLabel (null sorts first, since it coerces to ''), renders each as Camera <siteLabel>, and disables the item matching the current ?camera= param. Selecting an item calls setPrimaryCamera — it retargets the link, it does not navigate. The user still has to click the main link. The chevron icon mirrors open/closed state.

Clicking the link closes the notification, then pushes /?camera=<id>. The new param immediately triggers the second effect, which closes it again — harmless, since it is already closed.

It closes on four events: the link click, the ✕ icon, a 30-second autoHideDuration, and any change to the ?camera= param.

Triggering it from elsewhere

Any code can open the notification. Use getState() from event handlers and callbacks registered outside React; use the hook only to read state inside JSX.

import { useGraphicNotificationStore } from '@/stores/graphicNotificationStore';

useGraphicNotificationStore.getState().showGraphicNotification(cameras, siteId);

Wrapping up

A store-driven snackbar that non-React map code opens with one getState() call, a primary-camera election that favors whoever moved last, and a link that retargets before it navigates. That completes the map chapter — next feature: camera movement.