Skip to main content

Hit Tests and the Click Budget

A MapView reports where the pointer is; it does not know what a camera is. Every interaction on the map is therefore a hitTest followed by our own decision about which of the dozen possible hits matters, in which order, on which device. Get the order wrong and clicking a site opens one camera instead of six; get the scope wrong and every mouse move walks a hundred layer views. This page is the list of listeners, what each one hit-tests, and the rules that keep them from interfering.

Hover: the hit test that pays rent​

useFeatureMapHover.setupHoverHandlers registers one pointer-move listener. Every call passes an include list to hitTest: the Velocity camera layers on the map (points, beams and their persisted companions). Without include, each move hit-tests every layer view in the WebMap, about a hundred of them, and that was a measurable fraction of main-thread time during a pan. An isProcessing flag drops moves that arrive while the previous one is still resolving.

The decision order is fixed: a point hit (Locations or the Camera Feed, or a persisted companion of it) wins over a beam hit, because hovering a site should show every camera there. A site hit is de-duplicated on its camera id, which is read straight off the hit graphic (stream graphics carry every field; the feature layers load with outFields: ['*']); the site's cameras come from getCamerasBySiteId, and the beam seam is called with them. In tile view that is a show (replace the beam set); in location view it is an add that leaves the selected camera alone. Leaving both resets the cursor and the hovered-cameras store. No hover or tap ever queries a service: the feature-query fallback that used to run against the camera service was the single largest source of ArcGIS 429s, and it went with that service.

The arenaMove over the map to see what hitTest reports and what a click would do; switch the include list off and watch the meter.
0 moves · 0 layer views tested
hitTest results · top-most first

nothing but the basemap

a click here would…

nothing

A click on the basemap does nothing on desktop; on touch, the tap handler returns early so the map interaction proceeds.

Every pointer move hit-tests two layer views: the camera service and the arrows. Switch the include list off and the same moves walk every layer view in the WebMap.

Gotcha: the feature query fallback is the single largest 429 source we have had

If a hit graphic does not carry cId, the handler falls back to queryFeatures on the camera service for that ObjectId. Until the layer's outFields was set at init, that fallback ran on every hovered site: one network request per hover against a rate-limited service, and half of all ArcGIS 429s in production during August 2026. The fallback stays as a safety net. If you see it firing in the network tab, something has reset the layer's outFields.

The hovered-camera store is written from the map through a wrapper that sets a flag for 50 ms, so the effect that mirrors list hover onto the map does not echo a map hover back through the store and clear what the map just drew. Hover is bidirectional, and that flag is the only thing keeping it from oscillating.

Click on desktop, tap on mobile​

Two different listeners, never both:

  • Desktop registers addCameraClickHandler on click. It hit-tests the whole view (clusters need it), and resolves in this order: a cluster aggregate zooms to level 14 over 1.5 s and stops; otherwise an arrow, LOS or FOV hit, or a Velocity beam normalised to carry cId, becomes the "arrow" hit; a non-aggregate camera-service hit, or a Velocity point, becomes the "point" hit, enriched by one queryFeatures for the full feature. FeatureMap.onCameraClicked then opens the callout for one camera (arrow) or every camera at the site (point), and suppresses it when the clicked camera is already the one in the URL.
  • Mobile and landscape register setupTapHandlers on immediate-click, which fires without waiting for a double-click, with the same include list as hover. A camera hit opens the bottom drawer with the tile actions; no hit means the map interaction proceeds untouched.

The listeners are registered in an effect keyed on mapView and isMapReady, and torn down with it. The hover and tap setup functions are memoised on things that do not change per poll (the real-time store is read at event time through getState(), not subscribed), because an earlier version re-registered all three handlers on every SignalR frame and re-queried the visible pins each time.

Clicks on portal layers​

addLayerClickHandler registers a second click listener that hit-tests everything, closes any native popup when a camera was under the pointer, and hands the top-most graphic from a named layer to onLayerClicked. Today the only consumer is the Alerts layer: a click on an alert polygon toggles the alert modal with the graphic's attributes. Three guards sit in front of it: the area pick mode, the coordinates pick mode, and isAlertEditActive, because a click meant to reposition an alert's pin can land on the alert's own still-visible marker and close the modal mid-edit.

Pick modes own the next click​

Two features arm a mode where the next map click belongs to them: the area filter's "Pick on map" and the Get Coordinates button. Both follow the same template, in useAreaMapPick and useCoordinateMapPick, and any future "arm, then tap" feature should copy it:

  1. Save and disable view.popupEnabled; set a crosshair cursor on the container.
  2. Register a click listener that calls event.stopPropagation() and does its work (an include-scoped hit test against visible boundary layers; or the map point and its client coordinates).
  3. Disarm on completion, on Escape, and in an unmount cleanup, so a hidden map never comes back swallowing clicks.
  4. Restore the popup flag and the cursor.

Because handler order between separately registered listeners is not ours to rely on, the other click handlers also check the pick stores before acting. The two modes are mutually exclusive: arming one disarms the other. The area pick hit-tests only layers that are visible and on the map, and says so up front when none is, rather than letting the operator tap an empty basemap.

Right-click, long-press, and the callout​

The coordinates card opens three ways to the same state. A native contextmenu listener on the view container converts the mouse position to a map point with view.toMap. ArcGIS's hold gesture, filtered to pointerType === 'touch', covers phones and tablets, which have no right-click. And the pick mode above is the discoverable entry. All three set the same coordPopover* state and drop a small crosshair graphic at the point.

The camera callout is the other direction: React following a map point. CameraCallout projects its anchor with view.toScreen(), adds the container's own offset, and re-projects on a reactiveUtils.watch of extent, rotation and size, throttled to one update per animation frame, so the card stays glued to its marker through a pan. Placement is a pure function the feature lesson documents. One gotcha applies to every React surface positioned over the map: the view's container lives inside the <arcgis-map> element's shadow root, where the app's stylesheets do not reach, so sightings and what3words portal their chips into the light-DOM region labelled "Feature Map" instead.

Cluster hover​

Removed in October 2026 together with the legacy camera feature service it clustered; a StreamLayer has no feature reduction.

The rules​

  1. Always pass include to hitTest on hover-rate events.
  2. Read attributes off the hit graphic; a feature query on hover is a rate-limit bug.
  3. Point hits beat arrow hits. Clusters beat everything and never open a camera.
  4. One click listener per concern, each checking the pick stores before acting.
  5. A mode that swallows clicks disarms on unmount and on Escape, without exception.
  6. React overlays on the map portal into the light DOM and follow the view with reactiveUtils.

Wrapping up​

The map hears pointer moves, clicks, taps, holds and context menus; we decide what they mean by hit-testing a deliberately short list of layers in a fixed priority. Pick modes temporarily own the next click and clean up after themselves. Everything React draws over the map follows it through toScreen and a throttled watch.

Next: Two Filters, Two Properties.