Skip to main content

Two Filters, Two Properties

An operator ticks Offline, lights Active, and scopes the map to Sonoma County. Three different controls, one expectation: the map shows offline cameras that are active and in Sonoma, and the cluster counts, the tile grid and the polling budget all agree. ArcGIS gives a feature layer two ways to hide features, and the reason this app uses both is that they compose differently. This page is the division of labour and the geometry behind the second half.

Property one: the definition expression​

The Type checkboxes and the navbar spotlights (Active, AI detected) resolve, per camera, through shouldCameraBeFiltered, the predicate the camera-filters lesson walks through. The map does not re-implement that predicate. A filter effect in useFeatureMapLayers runs it over every static camera, collects two id lists, and writes the shorter of an allowlist and a denylist as the layer's definitionExpression:

Two filters, composedTick types and spotlights, add an area and drag it; the two where clauses update live and the cluster counts what survives both.
types (OR)spotlights (AND)
definitionExpression1=1layerView.filternull (no scope)rendered40 of 40 cameras · cluster 27

Tick types and spotlights to build the definition expression — the shorter of an allowlist and a denylist. Then add an area scope: it lands on the layer view, never on the same property.

The expression is applied to the camera service and to all three client layers in one go (and forwarded to the Velocity layers when they are active), and only when it differs from the last one applied. That last check is the biggest performance win in the file: most real-time updates change nothing about which cameras pass, and a definition-expression write on a hosted layer is a request. Two more rules live in the effect. On the location page the selected camera is always moved to the shown list, so opening a camera from a link never makes its own pin vanish; and the expression is computed from filterConditions and the API id sets only, deliberately not from the viewport or the real-time map, because those change on every scroll and poll.

Property two: the layer-view filter​

The area scope is not another condition. It is a gate that ANDs ahead of everything else, kept in its own store, and on the map it is applied to the layer view, not the layer:

const layerView = await view.whenLayerView(layer); // bounded by an 8 s timeout
layerView.filter = where ? { where } : null; // null clears it; no "1=1" object lingers

Three reasons, each sufficient on its own. A layer-view filter is evaluated client-side, so it applies instantly with no request. It composes with the definition expression instead of fighting it for the same property, which would be a last-writer-wins bug that only shows when both filters are on. And clustering aggregates after the layer-view filter, so cluster counts stay truthful under a scope instead of counting cameras the operator cannot see.

The whenLayerView timeout matters: the promise never settles for a layer that is not on the map, and without the bound a parked companion would leave the scope bar spinning forever. Streams are the one case that cannot take the filter from this hook, since a StreamLayer has no filter of its own and its layer view's filter already carries the beam gate and the handoff. The hook publishes its clause on cameraAreaFilterStore.mapAreaWhere, first and unconditionally, and the stream's single writer composes it in.

Resolving an area, without asking the camera service​

Everything from "the operator ticked Battalion 3" to a set of camera ids runs client-side in useFeatureMapAreaFilter and util/map/featureMap/areaFilterGeometry.ts:

  1. Fetch the boundary. One queryFeatures on the boundary layer by objectIds, geometry back in WGS84 (outSpatialReference), generalised with maxAllowableOffset equivalent to 50 m. County polygons carry tens of thousands of vertices; 50 m is far finer than camera spacing and cuts the count by orders of magnitude. The tolerance is a fixed distance, not a multiple of the view resolution, because at country zoom a resolution-derived tolerance would be kilometres and put cameras on the wrong side of a line. Results are cached per scope.
  2. Union multiple areas with unionOperator.executeMany, so two counties mean "either".
  3. Buffer with geodesicBufferOperator when the operator set miles, in true metres.
  4. Match cameras from staticCameraStore. Mode in is a bounding-box prefilter then intersectsOperator on the camera point. Mode looking builds each shortlisted camera's FOV wedge with the same createFovPolygon the map draws, origin trim set to zero, and tests the wedge against the (accelerated) scope geometry; a ridge camera outside a battalion but pointed into it is exactly the one an incident wants. either is the union.
  5. Draw the boundaries and buffer on a hidden GraphicsLayer in the scope colour, because the source layer may well be switched off and an invisible filter that hides cameras is the one failure we cannot ship.
  6. Apply the shortest predicate as the layer-view filter, and publish the matched set to the store for the grid and the polling budget.

The geometry operators are lazy WASM chunks with no retry of their own; the hook warms them when the feature is enabled and wraps each load in three attempts, because a single dropped packet on cellular used to fail a resolve outright. Every stage checks a request counter so a slow resolve never overwrites a newer one, and any failure fails open: the filter is cleared and every camera stays visible rather than hidden behind a scope that never resolved.

Boundary layers, parked or not​

A layer qualifies as a boundary source when it is a queryable polygon feature layer and not one of ours. With lazy loading there is a wrinkle: a parked layer has never fetched its metadata, and the WebMap JSON does not carry geometryType, so it cannot be classified until loaded. useAreaBoundaryLayerList therefore primes unresolved candidates when the Camera Options drawer opens, metadata only, four at a time so a burst of eighty requests cannot trip the rate limit, and the list fills in as they resolve. A saved or shared scope whose layer is no longer in the WebMap at all is dropped with a notice rather than left erroring on every load.

Who writes what, one more time​

PropertyWriterNever written by
definitionExpressionthe filter effect in useFeatureMapLayersthe area hook, pick modes, streams' area clause
layerView.filterthe area hook (legacy layers, companions)the type filters
layerView.filter on a streamapplyVelocityLayerFilters onlythe area hook (it publishes to the store instead)

If you add a per-camera map filter, it takes the layer-view property, ANDs with whatever is there, and gets exactly one writer. The two-month bug where scoped-out real-time cameras stayed on the map came from a well-meaning write to a property that did not exist.

Wrapping up​

Type and spotlight filters become the shortest cId predicate on the layer; the area scope becomes a layer-view filter that composes with it and keeps clusters honest; streams receive the scope through the store and one composer. Areas resolve client-side from generalised boundaries, unions, geodesic buffers and the same FOV geometry the map draws, and fail open.

Next: Drawing on Top.