diff --git a/README.md b/README.md index 11b0549..719631d 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ Generic map and routing utilities for Vehicle Routing Problems (VRP) and similar ## Features - **OSM Road Network**: Download and cache OpenStreetMap road data via Overpass API -- **R-Tree Spatial Indexing**: O(log n) coordinate snapping to road network -- **Shortest Path Routing**: Dijkstra/A* routing with travel times and distances +- **K-D Tree Spatial Indexing**: Nearest-node and nearest-segment snapping on the road network +- **Shortest Path Routing**: Dijkstra-style shortest paths for time and distance objectives - **Travel Time Matrix**: Compute all-pairs travel times with parallel computation -- **Route Geometry**: Full road-following geometries with Douglas-Peucker simplification +- **Route Geometry**: Node-snapped and edge-snapped route geometries with Douglas-Peucker simplification - **Polyline Encoding**: Google Polyline Algorithm for efficient route transmission - **Input Validation**: Fail-fast coordinate and bounding box validation - **Cache Management**: In-memory and file-based caching with inspection and eviction @@ -38,12 +38,12 @@ async fn main() -> RoutingResult<()> { let bbox = BoundingBox::from_coords(&locations).expand_for_routing(&locations); let config = NetworkConfig::default(); - let network = RoadNetwork::load_or_fetch(&bbox, &config, None).await?; - let matrix = network.compute_matrix(&locations, None).await; - let route = network.route(locations[0], locations[1])?; +let network = RoadNetwork::load_or_fetch(&bbox, &config, None).await?; +let matrix = network.compute_matrix(&locations, None).await; +let route = network.route(locations[0], locations[1])?; // snaps both points to nearest nodes - println!("Matrix size: {}", matrix.size()); - println!("Route duration: {} seconds", route.duration_seconds); +println!("Matrix size: {}", matrix.size()); +println!("Route duration: {} seconds", route.duration_seconds); Ok(()) } ``` @@ -212,16 +212,22 @@ let network: RoadNetwork = RoadNetwork::fetch(&bbox, &config, None).await?; #### Routing +`route` and `route_with` snap both endpoints to the nearest graph nodes before +searching. They currently call the shared `astar` implementation with a zero +heuristic, so the public search behavior is equivalent to Dijkstra's algorithm. +If you need geometry that starts and ends on the containing road segments rather +than at snapped nodes, use `snap_to_edge` with `route_edge_snapped`. + ```rust -use solverforge_maps::{Coord, RouteResult, RoutingError, Objective}; +use solverforge_maps::{Coord, Objective, RouteResult, RoutingError}; let from = Coord::new(39.95, -75.16); let to = Coord::new(39.96, -75.17); -// Route by minimum travel time (default) +// Route by minimum travel time (default). Endpoints are snapped to nearest nodes. let route: Result = network.route(from, to); -// Route with specific objective +// Route with specific objective. Public search still uses a zero heuristic today. let route = network.route_with(from, to, Objective::Time)?; // Minimize time let route = network.route_with(from, to, Objective::Distance)?; // Minimize distance @@ -234,6 +240,22 @@ println!("Geometry: {} points", route.geometry.len()); let simplified = route.simplify(10.0); // tolerance in meters ``` +#### Edge-Snapped Routing + +Use edge snapping when you want the returned geometry to begin and end on the +nearest road segments instead of the nearest graph nodes. + +```rust +use solverforge_maps::{Coord, RouteResult, RoutingError}; + +let from = Coord::new(39.95, -75.16); +let to = Coord::new(39.96, -75.17); + +let from_edge = network.snap_to_edge(from)?; +let to_edge = network.snap_to_edge(to)?; +let route: Result = network.route_edge_snapped(&from_edge, &to_edge); +``` + #### Coordinate Snapping ```rust @@ -253,7 +275,7 @@ if let Ok(snap) = snapped { println!("Snap distance: {:.1} meters", snap.snap_distance_m); } -// Route between pre-snapped coordinates (more efficient for repeated routing) +// Route between pre-snapped node locations (more efficient for repeated routing) let from_snap = network.snap_to_road_detailed(from)?; let to_snap = network.snap_to_road_detailed(to)?; let route = network.route_snapped(&from_snap, &to_snap)?; diff --git a/src/routing/network.rs b/src/routing/network.rs index 6a2189b..211f83e 100644 --- a/src/routing/network.rs +++ b/src/routing/network.rs @@ -298,8 +298,10 @@ impl RoadNetwork { /// Find a route between two coordinates. /// - /// This method snaps the coordinates to the nearest road network nodes - /// and then finds the shortest path by travel time. + /// This method snaps both coordinates to the nearest road-network nodes, + /// then runs the public travel-time search over those snapped nodes. + /// The current implementation passes a zero heuristic to `astar`, so its + /// behavior is equivalent to Dijkstra's algorithm. pub fn route(&self, from: Coord, to: Coord) -> Result { let start_snap = self.snap_to_road_detailed(from)?; let end_snap = self.snap_to_road_detailed(to)?; @@ -309,8 +311,8 @@ impl RoadNetwork { /// Find a route between two edge-snapped locations. /// - /// This handles the case where start and end are on road segments, - /// not necessarily at intersections. + /// Use this when start and end should stay on their containing road + /// segments instead of being snapped all the way to graph nodes. pub fn route_edge_snapped( &self, from: &EdgeSnappedLocation, @@ -424,6 +426,10 @@ impl RoadNetwork { } } + /// Find a route between two node-snapped coordinates. + /// + /// This expects `SnappedCoord` values produced by `snap_to_road_detailed` + /// and returns geometry along graph nodes, not projected edge endpoints. pub fn route_snapped( &self, from: &SnappedCoord, @@ -474,6 +480,10 @@ impl RoadNetwork { } } + /// Find a route between two coordinates with an explicit optimization objective. + /// + /// Like `route`, this method snaps to the nearest graph nodes first. The + /// current public search still uses a zero heuristic for both objectives. pub fn route_with( &self, from: Coord,