rmpca 0.1.1

Enterprise-grade route optimization engine — Chinese Postman Problem solver with Eulerian circuit detection, Lean 4 FFI boundary, and property-based testing
Documentation
use crate::geo::spatial::coord_distance;
use crate::geo::types::{Coordinate, Way as GeoWay};
use crate::optimizer::types::{Node, Way};
use petgraph::graph::{DiGraph, NodeIndex};
use std::collections::HashMap;

/// Builder for constructing directed graphs from various spatial data sources.
pub struct GraphBuilder {
    pub graph: DiGraph<Node, f64>,
    pub node_index: HashMap<String, NodeIndex>,
    pub spatial_registry: HashMap<String, Coordinate>,
    pub weight_multipliers: HashMap<(String, String), f64>,
}

impl GraphBuilder {
    #[must_use]
    pub fn new() -> Self {
        Self {
            graph: DiGraph::new(),
            node_index: HashMap::new(),
            spatial_registry: HashMap::new(),
            weight_multipliers: HashMap::new(),
        }
    }

    /// Set weight multipliers for specific segments (e.g., from Chronos predictions)
    #[allow(dead_code)]
    #[must_use]
    pub fn with_multipliers(mut self, multipliers: HashMap<(String, String), f64>) -> Self {
        self.weight_multipliers = multipliers;
        self
    }

    /// Build a directed graph from internal Way objects and a spatial registry.
    #[must_use]
    pub fn build_from_ways(mut self, ways: &[Way], registry: HashMap<String, Coordinate>) -> DiGraph<Node, f64> {
        self.spatial_registry = registry;
        
        for way in ways {
            let is_oneway = way.is_oneway();
            let mut prev_idx: Option<NodeIndex> = None;

            for node_id in &way.nodes {
                let idx = *self.node_index.entry(node_id.clone())
                    .or_insert_with(|| {
                        let coord = self.spatial_registry.get(node_id);
                        let (lat, lon) = coord.map_or((0.0, 0.0), |c| (c.lat, c.lon));
                        self.graph.add_node(Node::new(node_id.clone(), lat, lon))
                    });

                if let Some(prev) = prev_idx {
                    let prev_id = self.graph[prev].id.clone();
                    let distance = self.haversine_between(&prev_id, node_id);
                    
                    let multiplier = self.weight_multipliers.get(&(prev_id.clone(), node_id.clone())).copied().unwrap_or(1.0);
                    let weight = distance * multiplier;

                    self.graph.add_edge(prev, idx, weight);
                    if !is_oneway {
                        let rev_multiplier = self.weight_multipliers.get(&(node_id.clone(), prev_id.clone())).copied().unwrap_or(1.0);
                        self.graph.add_edge(idx, prev, distance * rev_multiplier);
                    }
                }
                prev_idx = Some(idx);
            }
        }
        self.graph
    }

    /// Build a directed graph from geo-specific Way objects (Overture/OSM).
    #[allow(dead_code)]
    #[must_use]
    pub fn build_from_geo_ways(mut self, ways: &[GeoWay]) -> DiGraph<Node, f64> {
        // First, populate spatial registry from way geometries
        for way in ways {
            for (node_id, coord) in way.node_ids.iter().zip(way.geometry.coordinates.iter()) {
                self.spatial_registry.insert(node_id.clone(), *coord);
            }
        }

        // Then, build the graph
        for way in ways {
            let is_oneway = way.is_oneway();
            let mut prev_idx: Option<NodeIndex> = None;

            for node_id in &way.node_ids {
                let idx = *self.node_index.entry(node_id.clone())
                    .or_insert_with(|| {
                        let coord = self.spatial_registry.get(node_id);
                        let (lat, lon) = coord.map_or((0.0, 0.0), |c| (c.lat, c.lon));
                        self.graph.add_node(Node::new(node_id.clone(), lat, lon))
                    });

                if let Some(prev) = prev_idx {
                    let prev_id = self.graph[prev].id.clone();
                    let distance = self.haversine_between(&prev_id, node_id);
                    
                    let multiplier = self.weight_multipliers.get(&(prev_id.clone(), node_id.clone())).copied().unwrap_or(1.0);
                    let weight = distance * multiplier;

                    self.graph.add_edge(prev, idx, weight);
                    if !is_oneway {
                        let rev_multiplier = self.weight_multipliers.get(&(node_id.clone(), prev_id.clone())).copied().unwrap_or(1.0);
                        self.graph.add_edge(idx, prev, distance * rev_multiplier);
                    }
                }
                prev_idx = Some(idx);
            }
        }
        self.graph
    }

    /// Accessor for the built spatial registry.
    #[allow(dead_code)]
    #[must_use]
    pub fn spatial_registry(&self) -> &HashMap<String, Coordinate> {
        &self.spatial_registry
    }

    /// Consume the builder and return the spatial registry.
    #[allow(dead_code)]
    #[must_use]
    pub fn into_spatial_registry(self) -> HashMap<String, Coordinate> {
        self.spatial_registry
    }

    fn haversine_between(&self, id_a: &str, id_b: &str) -> f64 {
        match (self.spatial_registry.get(id_a), self.spatial_registry.get(id_b)) {
            (Some(c1), Some(c2)) => coord_distance(c1, c2),
            _ => 0.0,
        }
    }
}

impl Default for GraphBuilder {
    fn default() -> Self {
        Self::new()
    }
}