Tuning the Map
Unlike the lessons before it, this page is a reference sheet — every tuning knob the map exposes, its current value, and which file to edit. Read How the Map Thinks first: changing a value here without that context is how symbology quietly breaks. Come back whenever a knob needs turning or the symptom table at the bottom needs consulting.
Almost everything lives in three files. Nothing here is runtime-configurable; these are source constants, and a change means a deploy.
| File | Owns |
|---|---|
util/map/featureMap/layerFactories.ts | Layer definitions, renderers, every colour and scale |
util/map/featureMap/geometryBuilders.ts | FOV wedge and LOS line geometry |
data/constants.ts | LOS length, portal item ID, camera layer name, pulse duration |
Arrows
Arrow SVGs are generated at module load, base64-encoded into data URLs, and cached — one per
state. There is no arrow asset file to swap. (public/images/arrow-triangle.svg exists but nothing
on this map reads it; ignore it.) To change the shape, edit the path inside
getFilledArrowSvgUrl / getUnfilledArrowSvgUrl.
| Knob | Current value | Where |
| ----------------------------------- | ------------------------ | ---------------------- | ---------- | ------------------- |
| AI-detection colour | #ff0000 | ARROW_COLORS.RED |
| Online colour | #000000 | ARROW_COLORS.BLACK |
| Offline colour (filled and outline) | #8f8f8f | ARROW_COLORS.GREY |
| Symbol footprint | 20×20 px, yoffset 14px | createArrowsRenderer |
| Visibility scale threshold | 3000000 | Layer minScale | 10000000 | createArrowsLayer |
| Rotation source | field p, geographic | rotationVariable |
Arrow size is a stepped visual variable against $view.scale, not a smooth ramp, and it is not
monotonic — the 2.5M step is smaller than the 5M step above it:
$view.scale | 50 000 | 100 000 | 500 000 | 1 000 000 | 2 500 000 | 5 000 000 |
|---|---|---|---|---|---|---|
| size (px) | 11 | 11 | 10 | 10 | 6 | 8 |
Line of sight and field of view
LOS colour is driven by the losColorCode attribute, bucketed from image age by
getLosColorCodeFromDate. Edit LOS_COLOR_MAP for the colours, or the thresholds in that function
for the buckets — but change both together, since the code names encode the buckets.
| Image age | losColorCode | Colour |
|---|---|---|
| under 5 min | under5min | #CB181D |
| 5–30 min | under30min | #FA6A4A |
| 30–60 min | under60min | #FDAE91 |
| 60–120 min | under120min | #FEE5D9 |
| over 120 min | over120min | #808080 |
| no timestamp | (unset) | #808080 — renderer default |
| Knob | Current value | Where |
|---|---|---|
| LOS line width | 1.25, with a 2.25 grey #5F6369 halo stroke behind it | LOS_WIDTH, createLosLineSymbol |
| Cursor-tracking LOS colour | black, [0, 0, 0, 1] | CURSOR_LOS_COLOR |
| LOS length | 40233.6 m (25 miles) | LOS_LENGTH in data/constants.ts |
| FOV fill and outline | [87, 89, 132, 0.5], outline width 0.5 | FOV_COLOR |
| FOV wedge angle | derived from camera model + zoom | calculateFovAngle |
FOV width is not a constant: calculateFovAngle(cameraModel, zoomValue) looks the angle up per
model, and falls back to 'Q6075-E' when the model is missing rather than failing. A camera
with an unknown model draws a plausible, wrong wedge. If zoomValue is 0 or less it returns
DEFAULT_FOV_ANGLE (60°) instead.
Clustering
Removed in October 2026 with the legacy camera feature service; the Velocity stream layers have no feature reduction.
Timing and scale constants
| Constant | Value | File |
|---|---|---|
EXTENT_CHANGE_DEBOUNCE_MS | 200 | useFeatureMapExtent.ts |
EXTENT_PERSIST_DEBOUNCE_MS | 500 | useFeatureMapExtent.ts |
AI_DETECTION_PULSE_DURATION_MS | 10_000 | data/constants.ts |
| AI pulse store clear delay | 10600 ms | useFeatureMapPolling.ts |
Environment
| Variable | Constant | Default |
|---|---|---|
NEXT_PUBLIC_MASTER_MAP_PORTAL_ITEM_ID | MASTER_MAP_PORTAL_ITEM_ID | db68c79771164142a90466c0cb64d712 |
Cameras come from the WebMap's ArcGIS Velocity layers, discovered by service name (see Cameras From the Stream). A WebMap without a Velocity Locations service draws no cameras and logs one error naming the item. Check that first when the basemap renders but cameras don't.
LAYERS_TO_IGNORE (topo labels + imagery labels + CLIENT_LAYERS) is what keeps basemaps and the
cursor-line layer out of the Layers Menu. Add a client-side layer and you must add its title to
CLIENT_LAYERS, or it shows up as a user-toggleable layer.
Adding a layer
Prefer the WebMap portal: add the layer in ArcGIS Online, save, and it loads automatically with
no code change. Only build a FeatureLayer in layerFactories.ts when the geometry is computed
client-side from camera state — that is the whole distinction between our four layers and
everything else on the map.
When it looks wrong
| Symptom | Most likely cause |
|---|---|
| No cameras, basemap fine | The WebMap has no Velocity Locations layer (one error line in the console names the item) |
| Arrow state lags a stale image | Timestamp-only changes under 60 s are deliberately ignored |
| No red arrows despite AI hits | User lacks the AI role; getArrowStateCode gates on hasAiAccess |
| Arrows vanish when zooming out | Working as designed past scale 3 000 000, unless isRecentlyMoved is set |
| Clusters never disappear when zooming in | Clustering was applied after layer.when() resolved, so maxScale was dropped |
| Cursor LOS blinks | Delete and add issued as two applyEdits calls instead of one |
Related
- How the Map Thinks — why these values are shaped the way they are
- Meet the Map — run the arrow-state function live