use anyhow::Context;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::Read;
use std::time::Instant;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompileRequest {
pub input_geojson: String,
pub output_rmp: String,
pub compress: bool,
pub road_classes: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompileResult {
pub input_size_bytes: u64,
pub output_size_bytes: u64,
pub node_count: usize,
pub edge_count: usize,
pub elapsed_ms: u64,
}
const RMP_MAGIC: &[u8; 4] = b"RMP1";
pub fn run_compile(req: &CompileRequest) -> anyhow::Result<CompileResult> {
let start = Instant::now();
let mut input_data = Vec::new();
{
let mut file = std::fs::File::open(&req.input_geojson)
.with_context(|| format!("Failed to open input GeoJSON: {}", req.input_geojson))?;
file.read_to_end(&mut input_data)?;
}
let input_size_bytes = input_data.len() as u64;
let geojson: geojson::FeatureCollection = serde_json::from_slice(&input_data)
.with_context(|| "Failed to parse GeoJSON FeatureCollection")?;
let mut node_map: HashMap<(i64, i64), u32> = HashMap::new();
let mut nodes: Vec<(f64, f64)> = Vec::new(); let mut edges: Vec<(u32, u32, f64, u8)> = Vec::new();
for feature in &geojson.features {
let geometry = match feature.geometry.as_ref() {
Some(g) => g,
None => continue,
};
let oneway = feature
.properties
.as_ref()
.and_then(|props| props.get("oneway"))
.and_then(|v| v.as_str())
.map(|s| {
if matches!(s, "yes" | "1" | "true") { 1u8 } else { 0u8 }
})
.unwrap_or(0);
let line_strings: Vec<&Vec<Vec<f64>>> = match &geometry.value {
geojson::Value::LineString(coords) => vec![coords],
geojson::Value::MultiLineString(multi) => multi.iter().collect(),
_ => continue,
};
for coords in line_strings {
if coords.len() < 2 {
continue;
}
let coord_points: Vec<(f64, f64)> = coords
.iter()
.filter(|p| p.len() >= 2)
.map(|p| (p[1], p[0])) .collect();
if coord_points.len() < 2 {
continue;
}
for window in coord_points.windows(2) {
let (lat1, lon1) = window[0];
let (lat2, lon2) = window[1];
let weight_m = haversine_distance_m(lat1, lon1, lat2, lon2);
let from_node = get_or_create_node(&mut node_map, &mut nodes, lat1, lon1);
let to_node = get_or_create_node(&mut node_map, &mut nodes, lat2, lon2);
edges.push((from_node, to_node, weight_m, oneway));
}
}
}
let node_count = nodes.len();
let edge_count = edges.len();
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(RMP_MAGIC);
buf.extend_from_slice(&(node_count as u32).to_le_bytes());
buf.extend_from_slice(&(edge_count 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());
std::fs::write(&req.output_rmp, &buf)
.with_context(|| format!("Failed to write output file: {}", req.output_rmp))?;
let output_size_bytes = buf.len() as u64;
let elapsed_ms = start.elapsed().as_millis() as u64;
Ok(CompileResult {
input_size_bytes,
output_size_bytes,
node_count,
edge_count,
elapsed_ms,
})
}
pub fn is_rmp_file(data: &[u8]) -> bool {
data.len() >= 4 && &data[..4] == RMP_MAGIC
}
fn get_or_create_node(
node_map: &mut HashMap<(i64, i64), u32>,
nodes: &mut Vec<(f64, f64)>,
lat: f64,
lon: f64,
) -> u32 {
let key = ((lat * 1e6) as i64, (lon * 1e6) as i64);
*node_map.entry(key).or_insert_with(|| {
let id = nodes.len() as u32;
nodes.push((lat, lon));
id
})
}
fn haversine_distance_m(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
const EARTH_RADIUS_M: f64 = 6_371_000.0;
let lat1_rad = lat1.to_radians();
let lat2_rad = lat2.to_radians();
let delta_lat = (lat2 - lat1).to_radians();
let delta_lon = (lon2 - lon1).to_radians();
let a = (delta_lat / 2.0).sin().powi(2)
+ lat1_rad.cos() * lat2_rad.cos() * (delta_lon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
EARTH_RADIUS_M * c
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::optimize::read_rmp_file;
#[test]
fn test_is_rmp_file_valid() {
let data = b"RMP1\x00\x00\x00\x00\x00\x00\x00\x00extra";
assert!(is_rmp_file(data));
}
#[test]
fn test_is_rmp_file_invalid() {
let data = b"GARBAGE_data_here";
assert!(!is_rmp_file(data));
}
#[test]
fn test_compile_roundtrip() {
let geojson = serde_json::json!({
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[-73.985, 40.748], [-73.944, 40.678]]
},
"properties": {"class": "residential"}
},
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[-73.944, 40.678], [-74.006, 40.7128]]
},
"properties": {"class": "primary", "oneway": "yes"}
}
]
});
let input_path = "/tmp/v2rmp_test_compile_input.geojson";
let output_path = "/tmp/v2rmp_test_compile_output.rmp";
std::fs::write(input_path, serde_json::to_string_pretty(&geojson).unwrap()).unwrap();
let req = CompileRequest {
input_geojson: input_path.to_string(),
output_rmp: output_path.to_string(),
compress: false,
road_classes: vec![],
};
let result = run_compile(&req).unwrap();
assert_eq!(result.node_count, 3); assert_eq!(result.edge_count, 2);
let output_data = std::fs::read(output_path).unwrap();
assert!(output_data.len() >= 4);
assert_eq!(&output_data[..4], b"RMP1");
let (nodes, edges) = read_rmp_file(&output_data).unwrap();
assert_eq!(nodes.len(), 3);
assert_eq!(edges.len(), 2);
assert_eq!(edges[0].from, 0);
assert_eq!(edges[0].to, 1);
assert_eq!(edges[1].from, 1);
assert_eq!(edges[1].to, 2);
assert_eq!(edges[1].oneway, 1);
let _ = std::fs::remove_file(input_path);
let _ = std::fs::remove_file(output_path);
}
#[test]
fn test_haversine_distance_m() {
let dist = haversine_distance_m(40.7128, -74.0060, 34.0522, -118.2437);
assert!((dist - 3_935_000.0).abs() < 10_000.0);
}
}