use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::Read;
use std::time::Instant;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TurnPenalties {
pub left: f64,
pub right: f64,
pub u_turn: f64,
}
impl Default for TurnPenalties {
fn default() -> Self {
Self {
left: 1.0,
right: 0.0,
u_turn: 5.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptimizeRequest {
pub cache_file: String,
pub route_file: Option<String>,
pub turn_penalties: TurnPenalties,
pub depot: Option<(f64, f64)>,
pub oneway_mode: OnewayMode,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum OnewayMode {
Ignore,
Respect,
Reverse,
}
impl Default for OnewayMode {
fn default() -> Self {
Self::Respect
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptimizeResult {
pub total_distance_km: f64,
pub total_segments: usize,
pub deadhead_distance_km: f64,
pub efficiency_pct: f64,
pub turns: TurnSummary,
pub elapsed_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TurnSummary {
pub left: u32,
pub right: u32,
pub u_turn: u32,
pub straight: u32,
}
#[derive(Debug, Clone)]
pub struct RmpNode {
pub lat: f64,
pub lon: f64,
}
#[derive(Debug, Clone)]
pub struct RmpEdge {
pub from: u32,
pub to: u32,
pub weight_m: f64,
pub oneway: u8,
}
pub fn haversine_m(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let r = 6_371_000.0;
let dlat = (lat2 - lat1).to_radians();
let dlon = (lon2 - lon1).to_radians();
let a = (dlat / 2.0).sin().powi(2)
+ lat1.to_radians().cos() * lat2.to_radians().cos() * (dlon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
r * c
}
pub fn classify_turn(bearing_delta: f64) -> &'static str {
let d = bearing_delta.normalize(-180.0, 180.0);
if d.abs() <= 45.0 {
"straight"
} else if d > 45.0 && d <= 135.0 {
"right"
} else if d < -45.0 && d >= -135.0 {
"left"
} else {
"u_turn"
}
}
const RMP_MAGIC: &[u8; 4] = b"RMP1";
pub fn read_rmp_file(data: &[u8]) -> anyhow::Result<(Vec<RmpNode>, Vec<RmpEdge>)> {
if data.len() < 4 || &data[..4] != RMP_MAGIC {
anyhow::bail!("Invalid .rmp file: missing RMP1 magic bytes");
}
if data.len() < 12 {
anyhow::bail!("Invalid .rmp file: header too short");
}
let node_count = u32::from_le_bytes(data[4..8].try_into()?) as usize;
let edge_count = u32::from_le_bytes(data[8..12].try_into()?) as usize;
let nodes_end = 12 + node_count * 16;
let edges_end = nodes_end + edge_count * 17;
let expected_len = edges_end + 4;
if data.len() < expected_len {
anyhow::bail!(
"Invalid .rmp file: expected {} bytes, got {}",
expected_len,
data.len()
);
}
let mut nodes = Vec::with_capacity(node_count);
for i in 0..node_count {
let offset = 12 + i * 16;
let lat = f64::from_le_bytes(data[offset..offset + 8].try_into()?);
let lon = f64::from_le_bytes(data[offset + 8..offset + 16].try_into()?);
nodes.push(RmpNode { lat, lon });
}
let mut edges = Vec::with_capacity(edge_count);
for i in 0..edge_count {
let offset = nodes_end + i * 17;
let from = u32::from_le_bytes(data[offset..offset + 4].try_into()?);
let to = u32::from_le_bytes(data[offset + 4..offset + 8].try_into()?);
let weight_m = f64::from_le_bytes(data[offset + 8..offset + 16].try_into()?);
let oneway = data[offset + 16];
edges.push(RmpEdge {
from,
to,
weight_m,
oneway,
});
}
let expected_crc = u32::from_le_bytes(
data[edges_end..edges_end + 4].try_into()?,
);
let actual_crc = crc32fast::hash(&data[..edges_end]);
if expected_crc != actual_crc {
anyhow::bail!(
"Invalid .rmp file: CRC32 mismatch (expected {}, got {})",
expected_crc,
actual_crc
);
}
Ok((nodes, edges))
}
fn bearing(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let dlon = (lon2 - lon1).to_radians();
let lat1_r = lat1.to_radians();
let lat2_r = lat2.to_radians();
let x = dlon.cos() * lat2_r.sin();
let y = lat1_r.cos() * lat2_r.sin() - lat1_r.sin() * lat2_r.cos() * dlon.sin();
let bearing_rad = y.atan2(x);
(bearing_rad.to_degrees() + 360.0) % 360.0
}
pub fn run_optimize(req: &OptimizeRequest) -> anyhow::Result<OptimizeResult> {
let start = Instant::now();
let mut file_data = Vec::new();
{
let mut file = std::fs::File::open(&req.cache_file)
.map_err(|e| anyhow::anyhow!("Failed to open .rmp file '{}': {}", req.cache_file, e))?;
file.read_to_end(&mut file_data)?;
}
let (nodes, edges) = read_rmp_file(&file_data)?;
if nodes.is_empty() || edges.is_empty() {
return Ok(OptimizeResult {
total_distance_km: 0.0,
total_segments: 0,
deadhead_distance_km: 0.0,
efficiency_pct: 100.0,
turns: TurnSummary {
left: 0,
right: 0,
u_turn: 0,
straight: 0,
},
elapsed_ms: start.elapsed().as_millis() as u64,
});
}
let n = nodes.len();
let mut adj: Vec<Vec<AdjEntry>> = vec![vec![]; n];
for (idx, edge) in edges.iter().enumerate() {
let from = edge.from as usize;
let to = edge.to as usize;
adj[from].push(AdjEntry {
to: edge.to,
weight_m: edge.weight_m,
edge_idx: idx,
});
match req.oneway_mode {
OnewayMode::Ignore => {
adj[to].push(AdjEntry {
to: edge.from,
weight_m: edge.weight_m,
edge_idx: idx,
});
}
OnewayMode::Respect => {
if edge.oneway == 0 {
adj[to].push(AdjEntry {
to: edge.from,
weight_m: edge.weight_m,
edge_idx: idx,
});
}
}
OnewayMode::Reverse => {
if edge.oneway == 1 {
adj[to].push(AdjEntry {
to: edge.from,
weight_m: edge.weight_m,
edge_idx: idx,
});
adj[from].retain(|e| e.edge_idx != idx);
} else {
adj[to].push(AdjEntry {
to: edge.from,
weight_m: edge.weight_m,
edge_idx: idx,
});
}
}
}
}
let mut degrees = vec![0usize; n];
for (i, adj_list) in adj.iter().enumerate() {
degrees[i] = adj_list.len();
}
let odd_vertices: Vec<usize> = (0..n).filter(|&i| degrees[i] % 2 != 0).collect();
let mut duplicate_edges: Vec<(usize, usize, f64, usize)> = Vec::new(); let mut matched = vec![false; n];
for i in 0..n {
if degrees[i] % 2 == 0 {
matched[i] = true;
}
}
for &u in &odd_vertices {
if matched[u] {
continue;
}
let mut best_v = None;
let mut best_dist = f64::MAX;
for &v in &odd_vertices {
if v == u || matched[v] {
continue;
}
let dist = haversine_m(nodes[u].lat, nodes[u].lon, nodes[v].lat, nodes[v].lon);
if dist < best_dist {
best_dist = dist;
best_v = Some(v);
}
}
if let Some(v) = best_v {
matched[u] = true;
matched[v] = true;
duplicate_edges.push((u, v, best_dist, usize::MAX)); }
}
let deadhead_edge_idx = usize::MAX;
for &(u, v, weight, eidx) in &duplicate_edges {
adj[u].push(AdjEntry {
to: v as u32,
weight_m: weight,
edge_idx: eidx,
});
adj[v].push(AdjEntry {
to: u as u32,
weight_m: weight,
edge_idx: eidx,
});
}
let start_node = if let Some((dep_lat, dep_lon)) = req.depot {
let mut best_node = 0;
let mut best_dist = f64::MAX;
for (i, node) in nodes.iter().enumerate() {
let dist = haversine_m(dep_lat, dep_lon, node.lat, node.lon);
if dist < best_dist {
best_dist = dist;
best_node = i;
}
}
best_node
} else if !odd_vertices.is_empty() {
odd_vertices[0]
} else {
0
};
let mut adj_clone = adj.clone();
let mut stack = vec![start_node as u32];
let mut circuit: Vec<u32> = Vec::new();
while !stack.is_empty() {
let v = *stack.last().unwrap() as usize;
let mut found = false;
while !adj_clone[v].is_empty() {
let edge = adj_clone[v].pop().unwrap();
if let Some(pos) = adj_clone[edge.to as usize]
.iter()
.position(|e| e.to == v as u32 && e.edge_idx == edge.edge_idx && e.weight_m == edge.weight_m)
{
adj_clone[edge.to as usize].remove(pos);
}
stack.push(edge.to);
found = true;
break;
}
if !found {
stack.pop();
circuit.push(v as u32);
}
}
circuit.reverse();
let mut total_distance_m = 0.0;
let mut deadhead_distance_m = 0.0;
let mut total_segments = 0usize;
let mut turns = TurnSummary {
left: 0,
right: 0,
u_turn: 0,
straight: 0,
};
let mut edge_traversal_count: HashMap<usize, u32> = HashMap::new();
for i in 0..circuit.len().saturating_sub(1) {
let u = circuit[i];
let v = circuit[i + 1];
let edge_info = adj[u as usize]
.iter()
.find(|e| e.to == v)
.or_else(|| adj[v as usize].iter().find(|e| e.to == u));
if let Some(e) = edge_info {
total_distance_m += e.weight_m;
total_segments += 1;
if e.edge_idx == deadhead_edge_idx {
deadhead_distance_m += e.weight_m;
} else {
let count = edge_traversal_count.entry(e.edge_idx).or_insert(0);
*count += 1;
if *count > 1 {
deadhead_distance_m += e.weight_m;
}
}
}
}
if circuit.len() > 2 {
for i in 1..circuit.len().saturating_sub(1) {
let prev = circuit[i - 1] as usize;
let curr = circuit[i] as usize;
let next = circuit[i + 1] as usize;
if prev == curr || curr == next {
continue;
}
let b_in = bearing(
nodes[prev].lat,
nodes[prev].lon,
nodes[curr].lat,
nodes[curr].lon,
);
let b_out = bearing(
nodes[curr].lat,
nodes[curr].lon,
nodes[next].lat,
nodes[next].lon,
);
let b_in_reverse = (b_in + 180.0).normalize(0.0, 360.0);
let delta = b_out - b_in_reverse;
match classify_turn(delta) {
"left" => turns.left += 1,
"right" => turns.right += 1,
"u_turn" => turns.u_turn += 1,
_ => turns.straight += 1,
}
}
}
let effective_distance_m = total_distance_m - deadhead_distance_m;
let efficiency_pct = if total_distance_m > 0.0 {
(effective_distance_m / total_distance_m) * 100.0
} else {
100.0
};
if let Some(ref route_path) = req.route_file {
let route_json = serde_json::json!({
"route": circuit,
"total_distance_km": total_distance_m / 1000.0,
"deadhead_distance_km": deadhead_distance_m / 1000.0,
"efficiency_pct": efficiency_pct,
"nodes": nodes.iter().enumerate().map(|(i, n)| serde_json::json!({
"id": i,
"lat": n.lat,
"lon": n.lon,
})).collect::<Vec<_>>(),
});
let json_str = serde_json::to_string_pretty(&route_json)?;
std::fs::write(route_path, json_str)?;
}
let elapsed_ms = start.elapsed().as_millis() as u64;
Ok(OptimizeResult {
total_distance_km: total_distance_m / 1000.0,
total_segments,
deadhead_distance_km: deadhead_distance_m / 1000.0,
efficiency_pct,
turns,
elapsed_ms,
})
}
trait NormalizeAngle {
fn normalize(self, lower: f64, upper: f64) -> f64;
}
impl NormalizeAngle for f64 {
fn normalize(self, lower: f64, upper: f64) -> f64 {
let width = upper - lower;
let mut val = self;
while val < lower {
val += width;
}
while val >= upper {
val -= width;
}
val
}
}
#[derive(Debug, Clone)]
struct AdjEntry {
to: u32,
weight_m: f64,
edge_idx: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_classify_turn_straight() {
assert_eq!(classify_turn(0.0), "straight");
assert_eq!(classify_turn(10.0), "straight");
assert_eq!(classify_turn(-10.0), "straight");
}
#[test]
fn test_classify_turn_left() {
assert_eq!(classify_turn(-90.0), "left");
}
#[test]
fn test_classify_turn_right() {
assert_eq!(classify_turn(90.0), "right");
}
#[test]
fn test_classify_turn_uturn() {
assert_eq!(classify_turn(170.0), "u_turn");
assert_eq!(classify_turn(-170.0), "u_turn");
}
#[test]
fn test_haversine_m_known_distance() {
let dist = haversine_m(40.7128, -74.0060, 34.0522, -118.2437);
assert!((dist - 3_935_000.0).abs() < 10_000.0);
}
#[test]
fn test_read_rmp_file_invalid() {
let result = read_rmp_file(&[]);
assert!(result.is_err());
}
#[test]
fn test_read_rmp_file_bad_magic() {
let data = b"BADM\x00\x00\x00\x00\x00\x00\x00\x00";
let result = read_rmp_file(data);
assert!(result.is_err());
}
#[test]
fn test_optimize_simple_network() {
let nodes = vec![
(40.7128_f64, -74.006_f64), (40.748_f64, -73.985_f64), (40.678_f64, -73.944_f64), ];
let edges = vec![
(0u32, 1u32, 5000.0f64, 0u8), (1u32, 2u32, 6000.0f64, 0u8), (2u32, 0u32, 7000.0f64, 0u8), ];
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(b"RMP1");
buf.extend_from_slice(&(nodes.len() as u32).to_le_bytes());
buf.extend_from_slice(&(edges.len() as u32).to_le_bytes());
for (lat, lon) in &nodes {
buf.extend_from_slice(&(*lat).to_le_bytes());
buf.extend_from_slice(&(*lon).to_le_bytes());
}
for (from, to, weight_m, oneway) in &edges {
buf.extend_from_slice(&from.to_le_bytes());
buf.extend_from_slice(&to.to_le_bytes());
buf.extend_from_slice(&weight_m.to_le_bytes());
buf.push(*oneway);
}
let crc = crc32fast::hash(&buf);
buf.extend_from_slice(&crc.to_le_bytes());
let temp_path = "/tmp/v2rmp_test_optimize_simple.rmp";
std::fs::write(temp_path, &buf).unwrap();
let req = OptimizeRequest {
cache_file: temp_path.to_string(),
route_file: None,
turn_penalties: TurnPenalties::default(),
depot: None,
oneway_mode: OnewayMode::Ignore,
};
let result = run_optimize(&req).unwrap();
assert!(result.total_distance_km > 0.0);
assert!(result.total_segments > 0);
let _ = std::fs::remove_file(temp_path);
}
}