///|
/// RouteAble is an offline-first, explainable accessibility route planner.
/// Applications supply a small campus graph, community map, or compatible
/// public data and receive a route plus a human-readable rationale.
pub(all) enum MobilityProfile {
Wheelchair
LowVision
Stroller
Standard
} derive(Eq, Debug)
///|
pub(all) enum Surface {
Asphalt
Paving
Brick
Gravel
Indoor
} derive(Eq, Debug)
///|
pub(all) enum AlertLevel {
Info
Notice
Warning
Blocked
} derive(Eq, Debug)
///|
pub(all) struct Node {
id : Int
label : String
zone : String
x : Double
y : Double
has_rest_area : Bool
has_tactile_paving : Bool
} derive(Debug)
///|
pub(all) struct Edge {
id : Int
from : Int
to : Int
meters : Int
slope_percent : Int
steps : Int
lighting : Int
crossing : Int
construction : Bool
surface : Surface
width_cm : Int
covered : Bool
note : String
} derive(Debug)
///|
pub(all) struct ProfileConfig {
max_slope : Int
max_steps : Int
minimum_lighting : Int
minimum_width_cm : Int
avoid_crossings : Bool
avoid_gravel : Bool
rest_bonus : Double
} derive(Debug)
///|
pub(all) struct RouteSegment {
edge_id : Int
from_label : String
to_label : String
meters : Int
score : Double
reasons : Array[String]
alerts : Array[String]
} derive(Debug)
///|
pub(all) struct Route {
profile : MobilityProfile
node_ids : Array[Int]
segments : Array[RouteSegment]
total_meters : Int
total_score : Double
accessible : Bool
summary : String
warnings : Array[String]
} derive(Debug)
///|
pub(all) struct Graph {
nodes : Array[Node]
edges : Array[Edge]
} derive(Debug)
///|
pub fn profile_config(profile : MobilityProfile) -> ProfileConfig {
match profile {
Wheelchair =>
{
max_slope: 8,
max_steps: 0,
minimum_lighting: 2,
minimum_width_cm: 90,
avoid_crossings: false,
avoid_gravel: true,
rest_bonus: 8.0,
}
LowVision =>
{
max_slope: 14,
max_steps: 24,
minimum_lighting: 3,
minimum_width_cm: 70,
avoid_crossings: true,
avoid_gravel: false,
rest_bonus: 4.0,
}
Stroller =>
{
max_slope: 10,
max_steps: 0,
minimum_lighting: 2,
minimum_width_cm: 95,
avoid_crossings: false,
avoid_gravel: true,
rest_bonus: 6.0,
}
Standard =>
{
max_slope: 18,
max_steps: 100,
minimum_lighting: 1,
minimum_width_cm: 50,
avoid_crossings: false,
avoid_gravel: false,
rest_bonus: 1.0,
}
}
}
///|
pub fn profile_name(profile : MobilityProfile) -> String {
match profile {
Wheelchair => "wheelchair"
LowVision => "low vision"
Stroller => "stroller"
Standard => "walking"
}
}
///|
pub fn surface_name(surface : Surface) -> String {
match surface {
Asphalt => "asphalt"
Paving => "paving"
Brick => "brick"
Gravel => "gravel"
Indoor => "indoor"
}
}
///|
pub fn valid_graph(graph : Graph) -> Bool {
if graph.nodes.length() == 0 {
return false
}
let mut index = 0
while index < graph.nodes.length() {
if graph.nodes[index].id != index {
return false
}
index = index + 1
}
let mut edge_index = 0
while edge_index < graph.edges.length() {
let edge = graph.edges[edge_index]
if edge.from < 0 ||
edge.to < 0 ||
edge.from >= graph.nodes.length() ||
edge.to >= graph.nodes.length() {
return false
}
if edge.meters <= 0 ||
edge.width_cm <= 0 ||
edge.lighting < 0 ||
edge.lighting > 5 {
return false
}
edge_index = edge_index + 1
}
true
}
///|
pub fn edge_block_reason(edge : Edge, profile : MobilityProfile) -> String? {
let config = profile_config(profile)
if edge.construction {
return Some("该路段正在施工")
}
if edge.steps > config.max_steps {
return Some(
"包含 \{edge.steps} 级台阶,超过当前出行画像的限制",
)
}
if edge.slope_percent > config.max_slope {
return Some("坡度 \{edge.slope_percent}% 超过可接受范围")
}
if edge.width_cm < config.minimum_width_cm {
return Some("有效宽度 \{edge.width_cm}cm 不足")
}
if config.avoid_gravel && edge.surface == Gravel {
return Some("碎石路面对该出行方式不可通行")
}
None
}
///|
pub fn edge_alerts(edge : Edge, profile : MobilityProfile) -> Array[String] {
let config = profile_config(profile)
let alerts : Array[String] = []
if edge.lighting < config.minimum_lighting {
alerts.push("照明较弱,建议在白天通行")
}
if edge.crossing > 0 {
if config.avoid_crossings {
alerts.push(
"需要通过 \{edge.crossing} 个路口,建议使用语音导航",
)
} else {
alerts.push("需要通过 \{edge.crossing} 个路口,请注意来车")
}
}
if edge.slope_percent >= config.max_slope - 2 && edge.slope_percent > 0 {
alerts.push("坡度接近该画像阈值,请量力通行")
}
if !edge.covered {
alerts.push("该路段无遮雨连廊,雨天需注意湿滑")
}
alerts
}
///|
pub fn edge_reasons(edge : Edge, profile : MobilityProfile) -> Array[String] {
let reasons : Array[String] = []
if edge.steps == 0 {
reasons.push("全程无台阶")
} else {
reasons.push("包含 \{edge.steps} 级台阶")
}
if edge.slope_percent <= 4 {
reasons.push("坡度平缓(\{edge.slope_percent}%)")
} else {
reasons.push("坡度为 \{edge.slope_percent}%")
}
if edge.lighting >= 4 {
reasons.push("夜间照明良好")
}
if edge.width_cm >= 150 {
reasons.push("通行宽度充足")
}
if edge.surface == Indoor {
reasons.push("经过室内连廊")
}
if profile == LowVision && edge.crossing == 0 {
reasons.push("无需穿越车行路口")
}
reasons
}
///|
pub fn edge_score(edge : Edge, profile : MobilityProfile) -> Double? {
match edge_block_reason(edge, profile) {
Some(_) => None
None => {
let config = profile_config(profile)
let mut score = edge.meters.to_double()
score = score + edge.slope_percent.to_double() * 12.0
score = score + edge.steps.to_double() * 18.0
score = score + (5 - edge.lighting).to_double() * 9.0
let crossing_penalty = if config.avoid_crossings { 55.0 } else { 18.0 }
score = score + edge.crossing.to_double() * crossing_penalty
if edge.surface == Brick {
score = score + 15.0
}
if edge.surface == Gravel {
score = score + 60.0
}
if edge.width_cm < 120 {
score = score + (120 - edge.width_cm).to_double() * 1.5
}
if !edge.covered {
score = score + 5.0
}
Some(score)
}
}
}
///|
fn edge_between(graph : Graph, from : Int, to : Int) -> Edge? {
let mut index = 0
while index < graph.edges.length() {
let edge = graph.edges[index]
if edge.from == from && edge.to == to {
return Some(edge)
}
index = index + 1
}
None
}
///|
fn minimum_unvisited(distances : Array[Double], visited : Array[Bool]) -> Int {
let mut best_index = -1
let mut best_distance = 1.0e30
let mut index = 0
while index < distances.length() {
if !visited[index] && distances[index] < best_distance {
best_distance = distances[index]
best_index = index
}
index = index + 1
}
best_index
}
///|
/// Compute the lowest-friction route for a profile using constrained Dijkstra.
pub fn plan(
graph : Graph,
start : Int,
destination : Int,
profile : MobilityProfile,
) -> Route? {
if !valid_graph(graph) ||
start < 0 ||
destination < 0 ||
start >= graph.nodes.length() ||
destination >= graph.nodes.length() {
return None
}
let size = graph.nodes.length()
let distances = Array::make(size, 1.0e30)
let previous = Array::make(size, -1)
let visited = Array::make(size, false)
distances[start] = 0.0
let mut round = 0
while round < size {
let current = minimum_unvisited(distances, visited)
if current < 0 || current == destination {
break
}
visited[current] = true
let mut edge_index = 0
while edge_index < graph.edges.length() {
let edge = graph.edges[edge_index]
if edge.from == current {
match edge_score(edge, profile) {
Some(cost) => {
let candidate = distances[current] + cost
if candidate < distances[edge.to] {
distances[edge.to] = candidate
previous[edge.to] = current
}
}
None => ()
}
}
edge_index = edge_index + 1
}
round = round + 1
}
if distances[destination] >= 1.0e29 {
return None
}
let reversed : Array[Int] = []
let mut cursor = destination
reversed.push(cursor)
while cursor != start {
cursor = previous[cursor]
if cursor < 0 {
return None
}
reversed.push(cursor)
}
let node_ids : Array[Int] = []
let mut reverse_index = reversed.length()
while reverse_index > 0 {
reverse_index = reverse_index - 1
node_ids.push(reversed[reverse_index])
}
let segments : Array[RouteSegment] = []
let warnings : Array[String] = []
let mut total_meters = 0
let mut path_index = 0
while path_index + 1 < node_ids.length() {
let from = node_ids[path_index]
let to = node_ids[path_index + 1]
match edge_between(graph, from, to) {
Some(edge) => {
let alerts = edge_alerts(edge, profile)
let mut alert_index = 0
while alert_index < alerts.length() {
warnings.push(
"\{graph.nodes[from].label}→\{graph.nodes[to].label}:\{alerts[alert_index]}",
)
alert_index = alert_index + 1
}
match edge_score(edge, profile) {
Some(score) => {
total_meters = total_meters + edge.meters
segments.push({
edge_id: edge.id,
from_label: graph.nodes[from].label,
to_label: graph.nodes[to].label,
meters: edge.meters,
score,
reasons: edge_reasons(edge, profile),
alerts,
})
}
None => return None
}
}
None => return None
}
path_index = path_index + 1
}
let summary = "为\{profile_name(profile)}推荐 \{graph.nodes[start].label} 至 \{graph.nodes[destination].label},全程约 \{total_meters} 米,途经 \{segments.length()} 段可通行路段。"
Some({
profile,
node_ids,
segments,
total_meters,
total_score: distances[destination],
accessible: true,
summary,
warnings,
})
}
///|
pub fn route_steps(route : Route) -> Array[String] {
let lines : Array[String] = []
let mut index = 0
while index < route.segments.length() {
let segment = route.segments[index]
lines.push(
"\{index + 1}. 从\{segment.from_label}前往\{segment.to_label},约\{segment.meters}米。\{segment.reasons.join(";")}。",
)
index = index + 1
}
lines
}
///|
pub fn route_markdown(route : Route) -> String {
let mut text = "## \{profile_name(route.profile)}路线\n\n\{route.summary}\n\n### 分段说明\n"
let steps = route_steps(route)
let mut index = 0
while index < steps.length() {
text = text + steps[index] + "\n"
index = index + 1
}
if route.warnings.length() > 0 {
text = text + "\n### 通行提醒\n"
let mut warning_index = 0
while warning_index < route.warnings.length() {
text = text + "- " + route.warnings[warning_index] + "\n"
warning_index = warning_index + 1
}
}
text
}
///|
pub fn alternative_destinations(
graph : Graph,
start : Int,
profile : MobilityProfile,
) -> Array[Route] {
let routes : Array[Route] = []
let mut node_index = 0
while node_index < graph.nodes.length() {
if node_index != start {
match plan(graph, start, node_index, profile) {
Some(route) => routes.push(route)
None => ()
}
}
node_index = node_index + 1
}
routes
}
///|
pub fn nearest_rest_stop(graph : Graph, route : Route) -> Node? {
let mut index = 0
while index < route.node_ids.length() {
let node = graph.nodes[route.node_ids[index]]
if node.has_rest_area {
return Some(node)
}
index = index + 1
}
None
}
///|
pub fn accessibility_grade(route : Route) -> String {
let warning_count = route.warnings.length()
if warning_count == 0 &&
route.total_score < route.total_meters.to_double() * 1.25 {
"A:低阻力路线"
} else if warning_count <= 2 {
"B:可通行,建议留意提醒"
} else {
"C:可通行但存在多项风险"
}
}