///|
/// Forecast each route segment from a single live speed and congestion value.
pub fn forecast_breakdown(
route : RouteProfile,
speed~ : Double,
congestion~ : Double,
policy? : SpeedPolicy = default_speed_policy(),
) -> RouteBreakdown {
let segments : Array[SegmentEta] = []
let mut total_distance = 0.0
let mut total_eta = 0.0
let mut confidence_total = 0.0
for segment in route.segments {
let local_speed = predict_segment_speed(segment, speed, congestion, policy~)
let confidence = speed_confidence(
segment,
if speed <= 0.0 {
local_speed
} else {
speed
},
local_speed,
)
let eta_seconds = safe_div(segment.distance, local_speed, 0.0)
segments.push({
segment_id: segment.segment_id,
distance: segment.distance,
speed: local_speed,
eta_seconds,
confidence,
})
total_distance += segment.distance
total_eta += eta_seconds
confidence_total += confidence * segment.distance
}
{
segments,
total_distance,
total_eta_seconds: total_eta,
confidence: safe_div(confidence_total, total_distance, 0.0),
}
}
///|
/// Sum segment ETA values without recomputing speeds.
pub fn sum_segment_eta(segments : Array[SegmentEta]) -> Double {
let mut total = 0.0
for segment in segments {
total += segment.eta_seconds
}
total
}
///|
/// Forecast a route using a weekly calendar context.
pub fn forecast_breakdown_at(
route : RouteProfile,
speed~ : Double,
calendar : RouteCalendar,
weekday~ : Int,
second_of_day~ : Double,
policy? : SpeedPolicy = default_speed_policy(),
) -> DetailedBreakdown {
let congestion = calendar.congestion_at(weekday, second_of_day)
let total = forecast_breakdown(route, speed~, congestion~, policy~)
let forecasts : Array[SegmentForecast] = []
for index, segment in route.segments {
let eta = total.segments[index]
forecasts.push({
index,
segment,
eta,
regime: regime_for_congestion(congestion),
congestion,
})
}
{ forecasts, total, weekday: normalize_weekday(weekday), second_of_day }
}
///|
/// Return segment ETAs as a cumulative timeline.
pub fn cumulative_segment_eta(segments : Array[SegmentEta]) -> Array[Double] {
let result : Array[Double] = []
let mut total = 0.0
for segment in segments {
total += segment.eta_seconds
result.push(total)
}
result
}
///|
/// Return the slowest segment by ETA, if the route is non-empty.
pub fn bottleneck_segment(segments : Array[SegmentEta]) -> SegmentEta? {
if segments.length() == 0 {
None
} else {
let mut best = segments[0]
for segment in segments[1:] {
if segment.eta_seconds > best.eta_seconds {
best = segment
}
}
Some(best)
}
}
///|
/// Scale a breakdown to a new remaining-distance fraction.
pub fn scale_breakdown(
breakdown : RouteBreakdown,
remaining_fraction~ : Double,
) -> RouteBreakdown {
let fraction = clamp(remaining_fraction, 0.0, 1.0)
let segments : Array[SegmentEta] = []
for segment in breakdown.segments {
segments.push({
..segment,
distance: segment.distance * fraction,
eta_seconds: segment.eta_seconds * fraction,
})
}
{
..breakdown,
segments,
total_distance: breakdown.total_distance * fraction,
total_eta_seconds: breakdown.total_eta_seconds * fraction,
}
}