File size: 9,571 Bytes
44dceed |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 |
//! Geometry utilities for route visualization.
//!
//! Implements Google Polyline encoding for efficient route transmission.
//! See: <https://developers.google.com/maps/documentation/utilities/polylinealgorithm>
use crate::domain::{Vehicle, VehicleRoutePlan};
use utoipa::ToSchema;
/// Encodes a sequence of coordinates using Google Polyline Algorithm.
///
/// The algorithm encodes latitude/longitude pairs as an ASCII string for
/// efficient transmission. Each coordinate is encoded as the difference
/// from the previous point, with 5 decimal places of precision.
///
/// # Examples
///
/// ```
/// use vehicle_routing::geometry::encode_polyline;
///
/// // Single point encodes to non-empty string
/// let encoded = encode_polyline(&[(38.5, -120.2)]);
/// assert!(!encoded.is_empty());
///
/// // Empty input gives empty output
/// let empty = encode_polyline(&[]);
/// assert!(empty.is_empty());
///
/// // Two points create a line
/// let line = encode_polyline(&[(38.5, -120.2), (40.7, -120.95)]);
/// assert!(!line.is_empty());
/// ```
pub fn encode_polyline(coords: &[(f64, f64)]) -> String {
if coords.is_empty() {
return String::new();
}
let mut result = String::new();
let mut prev_lat = 0i64;
let mut prev_lng = 0i64;
for &(lat, lng) in coords {
// Convert to fixed-point with 5 decimal places
let lat_e5 = (lat * 1e5).round() as i64;
let lng_e5 = (lng * 1e5).round() as i64;
// Encode deltas
encode_value(lat_e5 - prev_lat, &mut result);
encode_value(lng_e5 - prev_lng, &mut result);
prev_lat = lat_e5;
prev_lng = lng_e5;
}
result
}
/// Encodes a single signed value using the polyline algorithm.
fn encode_value(value: i64, output: &mut String) {
// Left-shift and invert if negative
let mut encoded = if value < 0 {
!((value) << 1)
} else {
(value) << 1
};
// Break into 5-bit chunks, OR with 0x20 if more chunks follow
while encoded >= 0x20 {
output.push(char::from_u32(((encoded & 0x1f) | 0x20) as u32 + 63).unwrap());
encoded >>= 5;
}
output.push(char::from_u32(encoded as u32 + 63).unwrap());
}
/// Decodes a Google Polyline string back to coordinates.
///
/// # Examples
///
/// ```
/// use vehicle_routing::geometry::{encode_polyline, decode_polyline};
///
/// let original = vec![(38.5, -120.2), (40.7, -120.95), (43.252, -126.453)];
/// let encoded = encode_polyline(&original);
/// let decoded = decode_polyline(&encoded);
///
/// // Check round-trip (within 5 decimal places precision)
/// assert_eq!(decoded.len(), original.len());
/// for (orig, dec) in original.iter().zip(decoded.iter()) {
/// assert!((orig.0 - dec.0).abs() < 0.00001);
/// assert!((orig.1 - dec.1).abs() < 0.00001);
/// }
/// ```
pub fn decode_polyline(encoded: &str) -> Vec<(f64, f64)> {
let mut coords = Vec::new();
let mut lat = 0i64;
let mut lng = 0i64;
let bytes = encoded.as_bytes();
let mut i = 0;
while i < bytes.len() {
// Decode latitude delta
let (lat_delta, consumed) = decode_value(&bytes[i..]);
i += consumed;
lat += lat_delta;
if i >= bytes.len() {
break;
}
// Decode longitude delta
let (lng_delta, consumed) = decode_value(&bytes[i..]);
i += consumed;
lng += lng_delta;
coords.push((lat as f64 / 1e5, lng as f64 / 1e5));
}
coords
}
/// Decodes a single value, returning (value, bytes_consumed).
fn decode_value(bytes: &[u8]) -> (i64, usize) {
let mut result = 0i64;
let mut shift = 0;
let mut consumed = 0;
for &b in bytes {
consumed += 1;
let chunk = (b as i64) - 63;
result |= (chunk & 0x1f) << shift;
shift += 5;
if chunk < 0x20 {
break;
}
}
// Invert if negative (check LSB)
if result & 1 != 0 {
result = !(result >> 1);
} else {
result >>= 1;
}
(result, consumed)
}
/// Encoded route segment for a vehicle's route.
#[derive(Debug, Clone, serde::Serialize, ToSchema)]
pub struct EncodedSegment {
/// Vehicle index.
pub vehicle_idx: usize,
/// Vehicle name.
pub vehicle_name: String,
/// Encoded polyline string (Google format).
pub polyline: String,
/// Number of points in the route.
pub point_count: usize,
}
/// Generates encoded polylines for all vehicle routes.
///
/// Returns segments for each vehicle with non-empty routes.
///
/// # Examples
///
/// ```
/// use vehicle_routing::domain::{Location, Visit, Vehicle, VehicleRoutePlan};
/// use vehicle_routing::geometry::encode_routes;
///
/// let depot = Location::new(0, 39.95, -75.16);
/// let loc_a = Location::new(1, 39.96, -75.17);
/// let loc_b = Location::new(2, 39.94, -75.15);
///
/// let locations = vec![depot.clone(), loc_a.clone(), loc_b.clone()];
/// let visits = vec![
/// Visit::new(0, "A", loc_a),
/// Visit::new(1, "B", loc_b),
/// ];
/// let mut vehicle = Vehicle::new(0, "Alpha", 100, depot);
/// vehicle.visits = vec![0, 1]; // A -> B
///
/// let mut plan = VehicleRoutePlan::new("test", locations, visits, vec![vehicle]);
///
/// // Set up route geometries (normally done by init_routing)
/// // Route: depot(0) -> A(1) -> B(2) -> depot(0)
/// plan.route_geometries.insert((0, 1), vec![(39.95, -75.16), (39.96, -75.17)]);
/// plan.route_geometries.insert((1, 2), vec![(39.96, -75.17), (39.94, -75.15)]);
/// plan.route_geometries.insert((2, 0), vec![(39.94, -75.15), (39.95, -75.16)]);
///
/// let segments = encode_routes(&plan);
/// assert_eq!(segments.len(), 1); // One vehicle with visits
/// assert_eq!(segments[0].vehicle_name, "Alpha");
/// assert_eq!(segments[0].point_count, 4); // depot -> A -> B -> depot
/// ```
pub fn encode_routes(plan: &VehicleRoutePlan) -> Vec<EncodedSegment> {
plan.vehicles
.iter()
.filter(|v| !v.visits.is_empty())
.map(|vehicle| {
let coords = get_route_coords(plan, vehicle);
let polyline = encode_polyline(&coords);
EncodedSegment {
vehicle_idx: vehicle.id,
vehicle_name: vehicle.name.clone(),
polyline,
point_count: coords.len(),
}
})
.collect()
}
/// Gets coordinates for a vehicle's complete route (depot -> visits -> depot).
///
/// Uses stored route geometries from road network routing.
/// Returns empty if route geometries are not initialized.
fn get_route_coords(plan: &VehicleRoutePlan, vehicle: &Vehicle) -> Vec<(f64, f64)> {
let mut coords = Vec::new();
let depot_idx = vehicle.home_location.index;
// Build the sequence of location indices: depot -> visits -> depot
let visit_loc_indices: Vec<usize> = vehicle
.visits
.iter()
.filter_map(|&v| plan.get_visit(v).map(|visit| visit.location.index))
.collect();
let route: Vec<usize> = std::iter::once(depot_idx)
.chain(visit_loc_indices)
.chain(std::iter::once(depot_idx))
.collect();
// Process each leg
for i in 0..route.len().saturating_sub(1) {
let from_idx = route[i];
let to_idx = route[i + 1];
if let Some(geometry) = plan.route_geometry(from_idx, to_idx) {
// Use stored road geometry
// Skip first point of subsequent segments to avoid duplicates
let skip = if coords.is_empty() { 0 } else { 1 };
coords.extend(geometry.iter().skip(skip).copied());
} else {
// Fallback: use direct lat/lng when road geometry unavailable
if coords.is_empty() {
if let Some(from_loc) = plan.get_location(from_idx) {
coords.push((from_loc.latitude, from_loc.longitude));
}
}
if let Some(to_loc) = plan.get_location(to_idx) {
coords.push((to_loc.latitude, to_loc.longitude));
}
}
}
coords
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_decode_roundtrip() {
let coords = vec![
(38.5, -120.2),
(40.7, -120.95),
(43.252, -126.453),
];
let encoded = encode_polyline(&coords);
let decoded = decode_polyline(&encoded);
assert_eq!(decoded.len(), coords.len());
for (orig, dec) in coords.iter().zip(decoded.iter()) {
assert!((orig.0 - dec.0).abs() < 0.00001);
assert!((orig.1 - dec.1).abs() < 0.00001);
}
}
#[test]
fn test_known_encoding() {
// Known encoding from Google's examples
let coords = vec![(38.5, -120.2), (40.7, -120.95), (43.252, -126.453)];
let encoded = encode_polyline(&coords);
// The encoding should be deterministic
assert!(!encoded.is_empty());
// Verify we can decode it back
let decoded = decode_polyline(&encoded);
assert_eq!(decoded.len(), 3);
}
#[test]
fn test_empty_coords() {
let encoded = encode_polyline(&[]);
assert!(encoded.is_empty());
let decoded = decode_polyline("");
assert!(decoded.is_empty());
}
#[test]
fn test_single_point() {
let coords = vec![(0.0, 0.0)];
let encoded = encode_polyline(&coords);
let decoded = decode_polyline(&encoded);
assert_eq!(decoded.len(), 1);
assert!((decoded[0].0).abs() < 0.00001);
assert!((decoded[0].1).abs() < 0.00001);
}
}
|