Honest pins and paths: geocode precision, type-gated paths, impossible-speed stops (#236)
Backend: _geocode_location now returns {lat, lng, location_type}, so the
precision rides onto the call doc and, unchanged via _resolve_location_pair,
onto the incident pin. Display-only: every correlator read indexes lat/lng.
Frontend: pins styled by precision (rooftop / interpolated ring /
GEOMETRIC_CENTER dashed uncertainty circle labelled with the spoken place /
unverified pre-#23 pins ghosted). liveWall.pathStops() is shared by the map
and the call spine: only police/accident incidents with a pursuit tag draw a
numbered path; others get unnumbered call dots. Segments over >10 min gaps or
talkgroup changes are dashed, stops implying >8 km/min are ghosted off the
path, paths >20 km get a "may be merged" badge. New optional MapView prop
pulseIncidentIds (wired by #238).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6e462720a1
commit
6bf072973c
@@ -16,7 +16,10 @@ import { MachineOutputNotice } from "@/components/ui/MachineOutputNotice";
|
||||
import { SourceCredit, fetchedLabel } from "@/components/SourceCredit";
|
||||
import { InlinePlayer } from "@/components/InlinePlayer";
|
||||
import { canPlayAudio, hasAudio, playIncident, stop as stopAudio, useAudio } from "@/lib/audioQueue";
|
||||
import { CONTEXT_KM, NEAR_CAMERA_KM, bboxAround, contextFor, sampleRadarDbz, type LatLon, type NearCamera } from "@/lib/liveWall";
|
||||
import {
|
||||
CONTEXT_KM, MAY_BE_MERGED_KM, NEAR_CAMERA_KM, bboxAround, contextFor, pathStops, sampleRadarDbz, stopNumbers,
|
||||
type LatLon, type NearCamera,
|
||||
} from "@/lib/liveWall";
|
||||
import { useOutagesIn, type OutagesState } from "@/lib/useOutages";
|
||||
import { OUTAGE_POINT_KM, countyName, isUtilityHazard, matchOutages, staleCountiesAt } from "@/lib/outageMatch";
|
||||
|
||||
@@ -297,13 +300,12 @@ export function IncidentPanel({
|
||||
const color = sevColor(inc.severity);
|
||||
const ctx = useMemo(() => contextFor(points, aircraft, vessels, events), [points, aircraft, vessels, events]);
|
||||
const radar = useRadarDbz(points.at(-1) ?? null);
|
||||
// Stop numbers share the index with the map path: geocoded calls, oldest first.
|
||||
const stopOf = useMemo(() => {
|
||||
const m = new Map<string, number>();
|
||||
calls.filter((c) => c.location_coords).slice().sort((a, b) => a.started_at.localeCompare(b.started_at))
|
||||
.forEach((c, i) => m.set(c.call_id, i + 1));
|
||||
return m;
|
||||
}, [calls]);
|
||||
// Stop numbers come from the same pathStops() the map draws from, so the
|
||||
// spine numbers exactly what the map numbers (server-26#236): only movement
|
||||
// incidents get numbers, and a ghosted (impossible-speed) stop gets none.
|
||||
const path = useMemo(() => pathStops(inc, calls), [inc, calls]);
|
||||
const stopOf = useMemo(() => stopNumbers(path), [path]);
|
||||
const ghostIds = useMemo(() => new Set(path.stops.filter((s) => s.ghost).map((s) => s.call.call_id)), [path]);
|
||||
const spine = useMemo(() => calls.slice().sort((a, b) => b.started_at.localeCompare(a.started_at)), [calls]);
|
||||
// "Play incident" (server-26#233): every loaded call with a recording, oldest first.
|
||||
const playable = useMemo(() => spine.filter(hasAudio).map((c) => c.call_id).reverse(), [spine]);
|
||||
@@ -336,6 +338,12 @@ export function IncidentPanel({
|
||||
{inc.location && <span>{inc.location}</span>}
|
||||
<span className="font-mono">{elapsed(inc.started_at)} elapsed</span>
|
||||
<span>{inc.call_ids?.length ?? 0} calls</span>
|
||||
{path.mayBeMerged && (
|
||||
<span className="text-[10px] px-1 rounded border border-dashed border-line text-ink-2"
|
||||
title={`The calls' locations span ${path.lengthKm.toFixed(0)} km (over ${MAY_BE_MERGED_KM} km) — this may be more than one event`}>
|
||||
may be merged · {path.lengthKm.toFixed(0)} km
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Link href={`/incidents/${inc.incident_id}`} className="text-xs text-accent hover:underline mt-1 inline-block">Open incident page →</Link>
|
||||
</div>
|
||||
@@ -441,6 +449,7 @@ export function IncidentPanel({
|
||||
<ol className="space-y-2">
|
||||
{shown.map((c) => {
|
||||
const stop = stopOf.get(c.call_id);
|
||||
const ghost = ghostIds.has(c.call_id);
|
||||
const text = c.transcript_corrected || c.transcript;
|
||||
const isPlaying = c.call_id === playingCallId;
|
||||
return (
|
||||
@@ -448,7 +457,10 @@ export function IncidentPanel({
|
||||
className={`flex gap-2 text-xs rounded ${isPlaying ? "bg-raised ring-1 ring-accent -mx-1 px-1 py-0.5" : ""}`}
|
||||
aria-current={isPlaying ? "true" : undefined}>
|
||||
<span className="shrink-0 w-5 h-5 rounded-full flex items-center justify-center font-mono text-[10px]"
|
||||
style={stop ? { background: color, color: "var(--page)" } : { background: "var(--raised)", color: "var(--ink-muted)" }}>{stop ?? "·"}</span>
|
||||
title={ghost ? "Geocoded too far from the previous stop to reach in time — left off the path" : undefined}
|
||||
style={stop ? { background: color, color: "var(--page)" }
|
||||
: ghost ? { border: "1.5px dashed var(--ink-muted)", color: "var(--ink-muted)" }
|
||||
: { background: "var(--raised)", color: "var(--ink-muted)" }}>{stop ?? (ghost ? "?" : "·")}</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex gap-1.5 items-center">
|
||||
<span className="font-mono text-ink-muted">{hhmm(c.started_at)}</span>
|
||||
|
||||
Reference in New Issue
Block a user