package traffic import ( "fmt" "os" "sort" "strings" ) // Policy maps a feed event type to the speed override + affected stretch. // Speeds are in km/h, applied to every graph edge on the matched stretch. // These are initial values — they should be tuned against measured traffic, // not asserted as fact (R4: provenance "assumed (policy)"). type Policy struct { // SpeedKmh is the speed applied to the affected stretch. SpeedKmh int // RadiusM is how far along the road the effect extends from the point. RadiusM float64 // Weight: when multiple incidents hit the same edge, the highest-weight // (most severe) policy wins. Weight int } // Policies covers the 511NY event types we expect. Unknown types are // ignored (no override) rather than guessed. var Policies = map[string]Policy{ "road closed": {5, 1500, 100}, "roadclosure": {5, 1500, 100}, "lane closure": {45, 800, 60}, "laneclosure": {45, 800, 60}, "accident": {25, 1200, 90}, "collision": {25, 1200, 90}, "road work": {50, 1000, 40}, "roadwork": {50, 1000, 40}, "construction": {50, 1000, 40}, "congestion": {30, 1500, 30}, "slow traffic": {30, 1500, 30}, "traffic": {30, 1500, 30}, "vehicle on road": {30, 800, 50}, "disabled vehicle": {25, 800, 70}, "incident": {25, 1000, 50}, } // PolicyFor normalizes a feed event type and looks up its policy. func PolicyFor(eventType string) (Policy, bool) { norm := strings.ToLower(strings.TrimSpace(eventType)) if p, ok := Policies[norm]; ok { return p, true } // substring fallback: "Accident - Multi Vehicle" etc. for key, p := range Policies { if strings.Contains(norm, key) { return p, true } } return Policy{}, false } // Assignment is one incident's contribution: a stretch of node pairs, a // speed, and a severity weight. type Assignment struct { Pair [2]int64 SpeedKmh int Weight int } // BuildCSV resolves assignments into OSRM segment-speed CSV rows. For each // unordered node pair, the highest-weight (most severe) speed wins; the OSRM // updater itself keys on (nodeA,nodeB), so conflicts must resolve here. func BuildCSV(assignments []Assignment) string { type key struct{ a, b int64 } type row struct{ pair key; speed, weight int } best := map[key]*row{} for _, as := range assignments { k := key{as.Pair[0], as.Pair[1]} if k.a > k.b { k.a, k.b = k.b, k.a } cur, ok := best[k] if !ok || as.Weight > cur.weight { best[k] = &row{pair: k, speed: as.SpeedKmh, weight: as.Weight} } } rows := make([]*row, 0, len(best)) for _, r := range best { rows = append(rows, r) } sort.Slice(rows, func(i, j int) bool { if rows[i].pair.a != rows[j].pair.a { return rows[i].pair.a < rows[j].pair.a } return rows[i].pair.b < rows[j].pair.b }) var b strings.Builder for _, r := range rows { fmt.Fprintf(&b, "%d,%d,%d\n", r.pair.a, r.pair.b, r.speed) } return b.String() } // WriteSpeedFile writes the CSV to disk. func WriteSpeedFile(path, csv string) error { return os.WriteFile(path, []byte(csv), 0o644) }