///|
/// Route quality analysis built on top of the planner's public route result.
/// These helpers make it possible for a UI to compare options without
/// duplicating accessibility policy outside MoonBit.
pub(all) struct RouteMetrics {
distance_meters : Int
average_slope : Double
total_crossings : Int
low_light_segments : Int
covered_segments : Int
rest_stops : Int
warning_count : Int
estimated_minutes : Int
} derive(Debug)
///|
pub(all) struct ProfileComparison {
profile : MobilityProfile
reachable : Bool
distance_meters : Int
friction_score : Double
grade : String
message : String
} derive(Debug)
///|
fn find_edge_by_id(graph : Graph, id : Int) -> Edge? {
let mut index = 0
while index < graph.edges.length() {
if graph.edges[index].id == id {
return Some(graph.edges[index])
}
index = index + 1
}
None
}
///|
pub fn walking_speed_meters_per_minute(profile : MobilityProfile) -> Int {
match profile {
Wheelchair => 48
LowVision => 55
Stroller => 52
Standard => 75
}
}
///|
pub fn estimate_minutes(
distance_meters : Int,
profile : MobilityProfile,
) -> Int {
if distance_meters <= 0 {
0
} else {
let speed = walking_speed_meters_per_minute(profile)
(distance_meters + speed - 1) / speed
}
}
///|
pub fn route_metrics(graph : Graph, route : Route) -> RouteMetrics {
let mut slope_total = 0
let mut crossings = 0
let mut low_light = 0
let mut covered = 0
let mut rests = 0
let mut index = 0
while index < route.segments.length() {
let segment = route.segments[index]
match find_edge_by_id(graph, segment.edge_id) {
Some(edge) => {
slope_total = slope_total + edge.slope_percent
crossings = crossings + edge.crossing
if edge.lighting <= 2 {
low_light = low_light + 1
}
if edge.covered {
covered = covered + 1
}
}
None => ()
}
index = index + 1
}
let mut node_index = 0
while node_index < route.node_ids.length() {
if graph.nodes[route.node_ids[node_index]].has_rest_area {
rests = rests + 1
}
node_index = node_index + 1
}
let average_slope = if route.segments.length() == 0 {
0.0
} else {
slope_total.to_double() / route.segments.length().to_double()
}
{
distance_meters: route.total_meters,
average_slope,
total_crossings: crossings,
low_light_segments: low_light,
covered_segments: covered,
rest_stops: rests,
warning_count: route.warnings.length(),
estimated_minutes: estimate_minutes(route.total_meters, route.profile),
}
}
///|
pub fn route_quality_label(metrics : RouteMetrics) -> String {
if metrics.warning_count == 0 &&
metrics.average_slope <= 4.0 &&
metrics.low_light_segments == 0 {
"excellent"
} else if metrics.warning_count <= 2 && metrics.average_slope <= 6.0 {
"good"
} else if metrics.warning_count <= 5 {
"usable with care"
} else {
"higher attention needed"
}
}
///|
pub fn route_briefing(graph : Graph, route : Route) -> String {
let metrics = route_metrics(graph, route)
let mut text = "Route quality: " + route_quality_label(metrics)
text = text + "\nDistance: \{metrics.distance_meters}m"
text = text + "\nEstimated time: \{metrics.estimated_minutes} min"
text = text + "\nAverage slope: \{metrics.average_slope} %"
text = text + "\nRoad crossings: \{metrics.total_crossings}"
text = text + "\nCovered segments: \{metrics.covered_segments}"
text = text + "\nRest stops: \{metrics.rest_stops}"
text
}
///|
pub fn compare_profiles(
graph : Graph,
start : Int,
destination : Int,
) -> Array[ProfileComparison] {
let profiles = [Wheelchair, LowVision, Stroller, Standard]
let comparisons : Array[ProfileComparison] = []
let mut index = 0
while index < profiles.length() {
let profile = profiles[index]
match plan(graph, start, destination, profile) {
Some(route) =>
comparisons.push({
profile,
reachable: true,
distance_meters: route.total_meters,
friction_score: route.total_score,
grade: accessibility_grade(route),
message: "A compatible route is available.",
})
None =>
comparisons.push({
profile,
reachable: false,
distance_meters: 0,
friction_score: 0.0,
grade: "unavailable",
message: "No route satisfies this profile's hard constraints.",
})
}
index = index + 1
}
comparisons
}
///|
pub fn reachable_profile_count(
graph : Graph,
start : Int,
destination : Int,
) -> Int {
let comparisons = compare_profiles(graph, start, destination)
let mut count = 0
let mut index = 0
while index < comparisons.length() {
if comparisons[index].reachable {
count = count + 1
}
index = index + 1
}
count
}
///|
pub fn route_has_rest_stop(graph : Graph, route : Route) -> Bool {
match nearest_rest_stop(graph, route) {
Some(_) => true
None => false
}
}
///|
pub fn route_can_be_recommended(graph : Graph, route : Route) -> Bool {
let metrics = route_metrics(graph, route)
route.accessible &&
metrics.distance_meters > 0 &&
metrics.average_slope <= 14.0
}