Verified end-to-end on the local OSRM stack: - internal/traffic: 511NY OData client (lenient decode, raw archive), Overpass way matcher (class-window selection: highest-class road within 150m beats a closer service road), incident policy map, CSV builder - cmd/traffic: fetch | match | apply; apply copies the dataset (CoW) and re-customizes the traffic layer in ~15s (v26 customize has no --output-prefix; it writes contract files in place) - Proof: synthetic I-90 closure → 38s (static :5000) vs 142s (traffic :5002) on the same 0.87 km stretch - Policy speeds are 'assumed' provenance; R4: traffic routes must carry computed(traffic, as_of, policy=assumed)
105 lines
3.0 KiB
Go
105 lines
3.0 KiB
Go
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)
|
|
}
|