rapidgeo_polyline/batch.rs
1//! Batch operations for encoding and decoding multiple polylines in parallel.
2//!
3//! These functions automatically switch between sequential and parallel processing
4//! based on batch size. Parallel processing is beneficial for large batches
5//! (typically >50-100 items) but adds overhead for small batches.
6
7use crate::{decode, encode, simplify_coordinates, LngLat, PolylineResult};
8use rapidgeo_simplify::SimplifyMethod;
9use rayon::prelude::*;
10
11/// Encodes multiple coordinate sequences into polyline strings with automatic parallelization.
12///
13/// Automatically switches between sequential processing (for small batches <100 routes)
14/// and parallel processing (for large batches ≥100 routes) to optimize performance.
15/// Parallel processing uses all available CPU cores via the rayon crate.
16///
17/// # Arguments
18///
19/// * `coordinates_batch` - Slice of coordinate sequences, each in longitude, latitude order
20/// * `precision` - Decimal places to preserve (1-11, typically 5 or 6)
21///
22/// # Returns
23///
24/// Returns a vector of polyline strings in the same order as input, or the first
25/// error encountered during processing.
26///
27/// # Performance
28///
29/// - **Small batches** (<100): Sequential processing to avoid threading overhead
30/// - **Large batches** (≥100): Parallel processing across CPU cores
31/// - **Memory usage**: O(n × m) where n = batch size, m = average route length
32///
33/// # Examples
34///
35/// ```rust
36/// use rapidgeo_polyline::batch::encode_batch;
37/// use rapidgeo_distance::LngLat;
38///
39/// // Multiple delivery routes
40/// let routes = vec![
41/// // Route 1: Short local delivery
42/// vec![
43/// LngLat::new_deg(-122.4194, 37.7749), // San Francisco
44/// LngLat::new_deg(-122.4094, 37.7849), // Nearby location
45/// ],
46/// // Route 2: Cross-city route
47/// vec![
48/// LngLat::new_deg(-120.2, 38.5), // Sacramento area
49/// LngLat::new_deg(-120.95, 35.6), // Central Valley
50/// LngLat::new_deg(-126.453, 43.252), // Oregon
51/// ],
52/// ];
53///
54/// let polylines = encode_batch(&routes, 5)?;
55/// assert_eq!(polylines.len(), 2);
56///
57/// // Each polyline corresponds to input route
58/// for (route, polyline) in routes.iter().zip(polylines.iter()) {
59/// assert!(!polyline.is_empty() || route.is_empty());
60/// }
61/// # Ok::<(), rapidgeo_polyline::PolylineError>(())
62/// ```
63///
64/// # Large Batch Processing
65///
66/// ```rust
67/// use rapidgeo_polyline::batch::encode_batch;
68/// use rapidgeo_distance::LngLat;
69///
70/// // Simulate processing many GPS tracks (would use parallel processing)
71/// let large_batch: Vec<Vec<LngLat>> = (0..200)
72/// .map(|i| vec![
73/// LngLat::new_deg(-122.0 + i as f64 * 0.001, 37.0 + i as f64 * 0.001),
74/// LngLat::new_deg(-122.1 + i as f64 * 0.001, 37.1 + i as f64 * 0.001),
75/// ])
76/// .collect();
77///
78/// // Automatically uses parallel processing
79/// let encoded = encode_batch(&large_batch, 5)?;
80/// assert_eq!(encoded.len(), 200);
81/// # Ok::<(), rapidgeo_polyline::PolylineError>(())
82/// ```
83pub fn encode_batch(
84 coordinates_batch: &[Vec<LngLat>],
85 precision: u8,
86) -> PolylineResult<Vec<String>> {
87 if coordinates_batch.len() < 100 {
88 coordinates_batch
89 .iter()
90 .map(|coords| encode(coords, precision))
91 .collect()
92 } else {
93 coordinates_batch
94 .par_iter()
95 .map(|coords| encode(coords, precision))
96 .collect()
97 }
98}
99
100/// Decodes multiple polyline strings into coordinate sequences with automatic parallelization.
101///
102/// Processes multiple polyline strings simultaneously using parallel processing for
103/// large batches (≥100 polylines) and sequential processing for smaller batches
104/// to avoid threading overhead.
105///
106/// # Arguments
107///
108/// * `polylines` - Slice of polyline strings to decode (ASCII characters 63-126)
109/// * `precision` - Decimal places the polylines were encoded with (1-11, typically 5 or 6)
110///
111/// # Returns
112///
113/// Returns a vector of coordinate sequences in longitude, latitude order, preserving
114/// input order. Returns the first error encountered during processing.
115///
116/// # Performance
117///
118/// - **Small batches** (<100): Sequential processing
119/// - **Large batches** (≥100): Parallel processing across CPU cores
120/// - **Typical speed**: ~3-5 million coordinates/second per core
121///
122/// # Examples
123///
124/// ```rust
125/// use rapidgeo_polyline::batch::{encode_batch, decode_batch};
126/// use rapidgeo_distance::LngLat;
127///
128/// // Round-trip batch processing
129/// let original_routes = vec![
130/// vec![
131/// LngLat::new_deg(-122.4194, 37.7749), // San Francisco
132/// LngLat::new_deg(-122.4094, 37.7849),
133/// ],
134/// vec![
135/// LngLat::new_deg(-120.2, 38.5), // Sacramento
136/// LngLat::new_deg(-126.453, 43.252), // Oregon
137/// ],
138/// ];
139///
140/// // Encode batch to polylines
141/// let polylines = encode_batch(&original_routes, 5)?;
142///
143/// // Decode batch back to coordinates
144/// let decoded_routes = decode_batch(&polylines, 5)?;
145///
146/// assert_eq!(decoded_routes.len(), 2);
147/// assert_eq!(decoded_routes[0].len(), 2);
148/// assert_eq!(decoded_routes[1].len(), 2);
149/// # Ok::<(), rapidgeo_polyline::PolylineError>(())
150/// ```
151///
152/// # Processing Stored Polylines
153///
154/// ```rust
155/// use rapidgeo_polyline::batch::decode_batch;
156///
157/// // Polylines from database or API response
158/// let stored_polylines = vec![
159/// "_p~iF~ps|U_ulLnnqC_mqNvxq`@".to_string(), // Google test vector
160/// "u{~vFvyys@fS]".to_string(), // Another route
161/// "".to_string(), // Empty route
162/// ];
163///
164/// let coordinate_sequences = decode_batch(&stored_polylines, 5)?;
165///
166/// assert_eq!(coordinate_sequences.len(), 3);
167/// assert_eq!(coordinate_sequences[0].len(), 3); // 3 coordinates
168/// assert_eq!(coordinate_sequences[1].len(), 2); // 2 coordinates
169/// assert_eq!(coordinate_sequences[2].len(), 0); // Empty
170/// # Ok::<(), rapidgeo_polyline::PolylineError>(())
171/// ```
172pub fn decode_batch(polylines: &[String], precision: u8) -> PolylineResult<Vec<Vec<LngLat>>> {
173 if polylines.len() < 100 {
174 polylines
175 .iter()
176 .map(|polyline| decode(polyline, precision))
177 .collect()
178 } else {
179 polylines
180 .par_iter()
181 .map(|polyline| decode(polyline, precision))
182 .collect()
183 }
184}
185
186/// Encodes coordinate sequences from string slices in parallel.
187pub fn decode_batch_strs(polylines: &[&str], precision: u8) -> PolylineResult<Vec<Vec<LngLat>>> {
188 if polylines.len() < 100 {
189 polylines
190 .iter()
191 .map(|polyline| decode(polyline, precision))
192 .collect()
193 } else {
194 polylines
195 .par_iter()
196 .map(|polyline| decode(polyline, precision))
197 .collect()
198 }
199}
200
201/// Simplifies multiple coordinate sequences using Douglas-Peucker algorithm in parallel.
202///
203/// Uses parallel processing for improved performance when processing large numbers
204/// of coordinate sequences (typically beneficial for >50 sequences).
205///
206/// # Arguments
207///
208/// * `coordinates_batch` - A slice of coordinate sequences to simplify
209/// * `tolerance_m` - Simplification tolerance in meters
210/// * `method` - Distance calculation method for simplification
211///
212/// # Examples
213///
214/// ```
215/// use rapidgeo_polyline::batch::simplify_coordinates_batch;
216/// use rapidgeo_simplify::SimplifyMethod;
217/// use rapidgeo_distance::LngLat;
218///
219/// let batch = vec![
220/// vec![LngLat::new_deg(-120.2, 38.5), LngLat::new_deg(-120.4, 38.6), LngLat::new_deg(-120.95, 40.7)],
221/// vec![LngLat::new_deg(-126.453, 43.252), LngLat::new_deg(-126.5, 43.3), LngLat::new_deg(-122.4194, 37.7749)],
222/// ];
223///
224/// let simplified_batch = simplify_coordinates_batch(&batch, 1000.0, SimplifyMethod::GreatCircleMeters);
225/// assert_eq!(simplified_batch.len(), 2);
226/// ```
227pub fn simplify_coordinates_batch(
228 coordinates_batch: &[Vec<LngLat>],
229 tolerance_m: f64,
230 method: SimplifyMethod,
231) -> Vec<Vec<LngLat>> {
232 if coordinates_batch.len() < 50 {
233 coordinates_batch
234 .iter()
235 .map(|coords| simplify_coordinates(coords, tolerance_m, method))
236 .collect()
237 } else {
238 coordinates_batch
239 .par_iter()
240 .map(|coords| simplify_coordinates(coords, tolerance_m, method))
241 .collect()
242 }
243}
244
245/// Simplifies and encodes multiple coordinate sequences to polyline strings in parallel.
246///
247/// This combines simplification and encoding in a single operation for efficiency.
248///
249/// # Arguments
250///
251/// * `coordinates_batch` - A slice of coordinate sequences to simplify and encode
252/// * `tolerance_m` - Simplification tolerance in meters
253/// * `method` - Distance calculation method for simplification
254/// * `precision` - Number of decimal places to preserve (typically 5 or 6)
255///
256/// # Examples
257///
258/// ```
259/// use rapidgeo_polyline::batch::encode_simplified_batch;
260/// use rapidgeo_simplify::SimplifyMethod;
261/// use rapidgeo_distance::LngLat;
262///
263/// let batch = vec![
264/// vec![LngLat::new_deg(-120.2, 38.5), LngLat::new_deg(-120.95, 40.7)],
265/// vec![LngLat::new_deg(-126.453, 43.252), LngLat::new_deg(-122.4194, 37.7749)],
266/// ];
267///
268/// let encoded_batch = encode_simplified_batch(&batch, 1000.0, SimplifyMethod::GreatCircleMeters, 5).unwrap();
269/// assert_eq!(encoded_batch.len(), 2);
270/// ```
271pub fn encode_simplified_batch(
272 coordinates_batch: &[Vec<LngLat>],
273 tolerance_m: f64,
274 method: SimplifyMethod,
275 precision: u8,
276) -> PolylineResult<Vec<String>> {
277 if coordinates_batch.len() < 50 {
278 coordinates_batch
279 .iter()
280 .map(|coords| {
281 let simplified = simplify_coordinates(coords, tolerance_m, method);
282 encode(&simplified, precision)
283 })
284 .collect()
285 } else {
286 coordinates_batch
287 .par_iter()
288 .map(|coords| {
289 let simplified = simplify_coordinates(coords, tolerance_m, method);
290 encode(&simplified, precision)
291 })
292 .collect()
293 }
294}
295
296/// Decodes, simplifies, and re-encodes multiple polyline strings in parallel.
297///
298/// This is useful for bulk processing of existing polylines to reduce their complexity.
299///
300/// # Arguments
301///
302/// * `polylines` - A slice of polyline strings to process
303/// * `tolerance_m` - Simplification tolerance in meters
304/// * `method` - Distance calculation method for simplification
305/// * `precision` - Precision for decoding/encoding (typically 5 or 6)
306///
307/// # Examples
308///
309/// ```
310/// use rapidgeo_polyline::batch::simplify_polylines_batch;
311/// use rapidgeo_simplify::SimplifyMethod;
312///
313/// let polylines = vec![
314/// "_p~iF~ps|U_ulLnnqC".to_string(),
315/// "_mqNvxq`@".to_string(),
316/// ];
317///
318/// let simplified_batch = simplify_polylines_batch(&polylines, 1000.0, SimplifyMethod::GreatCircleMeters, 5).unwrap();
319/// assert_eq!(simplified_batch.len(), 2);
320/// ```
321pub fn simplify_polylines_batch(
322 polylines: &[String],
323 tolerance_m: f64,
324 method: SimplifyMethod,
325 precision: u8,
326) -> PolylineResult<Vec<String>> {
327 if polylines.len() < 50 {
328 polylines
329 .iter()
330 .map(|polyline| {
331 let coords = decode(polyline, precision)?;
332 let simplified = simplify_coordinates(&coords, tolerance_m, method);
333 encode(&simplified, precision)
334 })
335 .collect()
336 } else {
337 polylines
338 .par_iter()
339 .map(|polyline| {
340 let coords = decode(polyline, precision)?;
341 let simplified = simplify_coordinates(&coords, tolerance_m, method);
342 encode(&simplified, precision)
343 })
344 .collect()
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn test_encode_batch_small() {
354 let batch = vec![
355 vec![
356 LngLat::new_deg(-120.2, 38.5),
357 LngLat::new_deg(-120.95, 35.6),
358 ],
359 vec![LngLat::new_deg(-126.453, 43.252)],
360 vec![],
361 ];
362
363 let encoded = encode_batch(&batch, 5).unwrap();
364 assert_eq!(encoded.len(), 3);
365 assert!(!encoded[0].is_empty());
366 assert!(!encoded[1].is_empty());
367 assert_eq!(encoded[2], "");
368 }
369
370 #[test]
371 fn test_decode_batch_small() {
372 let polylines = vec![
373 "_p~iF~ps|U_ulLnnqC".to_string(),
374 "_mqNvxq`@".to_string(),
375 "".to_string(),
376 ];
377
378 let decoded = decode_batch(&polylines, 5).unwrap();
379 assert_eq!(decoded.len(), 3);
380 assert_eq!(decoded[0].len(), 2);
381 assert_eq!(decoded[1].len(), 1);
382 assert_eq!(decoded[2].len(), 0);
383 }
384
385 #[test]
386 fn test_decode_batch_strs() {
387 let polylines = vec!["_p~iF~ps|U_ulLnnqC", "_mqNvxq`@", ""];
388
389 let decoded = decode_batch_strs(&polylines, 5).unwrap();
390 assert_eq!(decoded.len(), 3);
391 assert_eq!(decoded[0].len(), 2);
392 assert_eq!(decoded[1].len(), 1);
393 assert_eq!(decoded[2].len(), 0);
394 }
395
396 #[test]
397 fn test_decode_batch_strs_parallel() {
398 let polyline_strs: Vec<&str> = (0..150).map(|_| "_p~iF~ps|U_ulLnnqC").collect();
399
400 let decoded = decode_batch_strs(&polyline_strs, 5).unwrap();
401 assert_eq!(decoded.len(), 150);
402 for coords in decoded {
403 assert_eq!(coords.len(), 2);
404 }
405 }
406
407 #[test]
408 fn test_large_batch_parallel() {
409 let coords = vec![
410 LngLat::new_deg(-120.2, 38.5),
411 LngLat::new_deg(-120.95, 35.6),
412 ];
413 let large_batch: Vec<Vec<LngLat>> = (0..150).map(|_| coords.clone()).collect();
414
415 let encoded = encode_batch(&large_batch, 5).unwrap();
416 assert_eq!(encoded.len(), 150);
417
418 let decoded = decode_batch(&encoded, 5).unwrap();
419 assert_eq!(decoded.len(), 150);
420
421 for decoded_coords in decoded {
422 assert_eq!(decoded_coords.len(), 2);
423 assert!((decoded_coords[0].lng_deg - coords[0].lng_deg).abs() < 0.00001);
424 assert!((decoded_coords[0].lat_deg - coords[0].lat_deg).abs() < 0.00001);
425 }
426 }
427
428 #[test]
429 fn test_batch_roundtrip() {
430 let batch = vec![
431 vec![
432 LngLat::new_deg(-120.2, 38.5),
433 LngLat::new_deg(-120.95, 35.6),
434 LngLat::new_deg(-126.453, 43.252),
435 ],
436 vec![LngLat::new_deg(-122.4194, 37.7749)],
437 vec![],
438 ];
439
440 let encoded = encode_batch(&batch, 5).unwrap();
441 let decoded = decode_batch(&encoded, 5).unwrap();
442
443 assert_eq!(batch.len(), decoded.len());
444
445 for (original_coords, decoded_coords) in batch.iter().zip(decoded.iter()) {
446 assert_eq!(original_coords.len(), decoded_coords.len());
447 for (original, decoded_coord) in original_coords.iter().zip(decoded_coords.iter()) {
448 assert!((original.lng_deg - decoded_coord.lng_deg).abs() < 0.00001);
449 assert!((original.lat_deg - decoded_coord.lat_deg).abs() < 0.00001);
450 }
451 }
452 }
453
454 #[test]
455 fn test_simplify_coordinates_batch_small() {
456 use rapidgeo_simplify::SimplifyMethod;
457
458 let batch = vec![
459 vec![
460 LngLat::new_deg(-122.0, 37.0),
461 LngLat::new_deg(-122.1, 37.1),
462 LngLat::new_deg(-122.2, 37.0),
463 ],
464 vec![
465 LngLat::new_deg(-120.0, 38.0),
466 LngLat::new_deg(-120.5, 38.5),
467 LngLat::new_deg(-121.0, 38.0),
468 ],
469 vec![], // Empty sequence
470 ];
471
472 let simplified =
473 simplify_coordinates_batch(&batch, 1000.0, SimplifyMethod::GreatCircleMeters);
474 assert_eq!(simplified.len(), 3);
475
476 // Each non-empty sequence should have at least 2 points (start/end)
477 assert!(simplified[0].len() >= 2);
478 assert!(simplified[1].len() >= 2);
479 assert_eq!(simplified[2].len(), 0); // Empty stays empty
480
481 // Endpoints should be preserved
482 assert_eq!(simplified[0][0], batch[0][0]);
483 assert_eq!(simplified[0].last().unwrap(), batch[0].last().unwrap());
484 }
485
486 #[test]
487 fn test_encode_simplified_batch() {
488 use rapidgeo_simplify::SimplifyMethod;
489
490 let batch = vec![
491 vec![
492 LngLat::new_deg(-122.0, 37.0),
493 LngLat::new_deg(-122.1, 37.1),
494 LngLat::new_deg(-122.2, 37.0),
495 ],
496 vec![LngLat::new_deg(-120.0, 38.0)], // Single point
497 ];
498
499 let encoded =
500 encode_simplified_batch(&batch, 1000.0, SimplifyMethod::GreatCircleMeters, 5).unwrap();
501 assert_eq!(encoded.len(), 2);
502
503 // All encoded strings should be valid
504 for polyline in &encoded {
505 if !polyline.is_empty() {
506 assert!(decode(polyline, 5).is_ok());
507 }
508 }
509 }
510
511 #[test]
512 fn test_simplify_polylines_batch() {
513 use rapidgeo_simplify::SimplifyMethod;
514
515 // Create some test polylines first
516 let coords1 = vec![
517 LngLat::new_deg(-122.0, 37.0),
518 LngLat::new_deg(-122.1, 37.1),
519 LngLat::new_deg(-122.2, 37.0),
520 ];
521 let coords2 = vec![
522 LngLat::new_deg(-120.0, 38.0),
523 LngLat::new_deg(-120.5, 38.5),
524 LngLat::new_deg(-121.0, 38.0),
525 ];
526
527 let polylines = vec![encode(&coords1, 5).unwrap(), encode(&coords2, 5).unwrap()];
528
529 let simplified =
530 simplify_polylines_batch(&polylines, 1000.0, SimplifyMethod::GreatCircleMeters, 5)
531 .unwrap();
532 assert_eq!(simplified.len(), 2);
533
534 // All simplified polylines should be valid and non-empty
535 for polyline in &simplified {
536 assert!(!polyline.is_empty());
537 let decoded = decode(polyline, 5).unwrap();
538 assert!(decoded.len() >= 2); // At least start and end
539 }
540 }
541
542 #[test]
543 fn test_simplify_polylines_batch_parallel() {
544 use rapidgeo_simplify::SimplifyMethod;
545
546 let coords = vec![
547 LngLat::new_deg(-122.0, 37.0),
548 LngLat::new_deg(-122.1, 37.1),
549 LngLat::new_deg(-122.2, 37.0),
550 ];
551
552 let polylines: Vec<String> = (0..60).map(|_| encode(&coords, 5).unwrap()).collect();
553
554 let simplified =
555 simplify_polylines_batch(&polylines, 1000.0, SimplifyMethod::GreatCircleMeters, 5)
556 .unwrap();
557 assert_eq!(simplified.len(), 60);
558
559 for polyline in &simplified {
560 assert!(!polyline.is_empty());
561 let decoded = decode(polyline, 5).unwrap();
562 assert!(decoded.len() >= 2);
563 }
564 }
565
566 #[test]
567 fn test_batch_simplification_parallel() {
568 use rapidgeo_simplify::SimplifyMethod;
569
570 // Create a large batch to trigger parallel processing
571 let coord_template = vec![
572 LngLat::new_deg(-122.0, 37.0),
573 LngLat::new_deg(-122.1, 37.1),
574 LngLat::new_deg(-122.2, 37.2),
575 LngLat::new_deg(-122.3, 37.1),
576 LngLat::new_deg(-122.4, 37.0),
577 ];
578 let large_batch: Vec<Vec<LngLat>> = (0..75).map(|_| coord_template.clone()).collect();
579
580 // Test parallel coordinate simplification
581 let simplified =
582 simplify_coordinates_batch(&large_batch, 1000.0, SimplifyMethod::GreatCircleMeters);
583 assert_eq!(simplified.len(), 75);
584
585 for coords in &simplified {
586 assert!(coords.len() >= 2); // At least endpoints
587 assert!(coords.len() <= coord_template.len()); // Not more than original
588 }
589
590 // Test parallel encode with simplification
591 let encoded =
592 encode_simplified_batch(&large_batch, 1000.0, SimplifyMethod::GreatCircleMeters, 5)
593 .unwrap();
594 assert_eq!(encoded.len(), 75);
595
596 // Verify all are valid polylines
597 for polyline in &encoded {
598 assert!(!polyline.is_empty());
599 assert!(decode(polyline, 5).is_ok());
600 }
601 }
602
603 #[test]
604 fn test_different_simplification_methods() {
605 use rapidgeo_simplify::SimplifyMethod;
606
607 let batch = vec![vec![
608 LngLat::new_deg(-122.0, 37.0),
609 LngLat::new_deg(-122.1, 37.1),
610 LngLat::new_deg(-122.2, 37.0),
611 ]];
612
613 for method in [
614 SimplifyMethod::GreatCircleMeters,
615 SimplifyMethod::PlanarMeters,
616 SimplifyMethod::EuclidRaw,
617 ] {
618 let simplified = simplify_coordinates_batch(&batch, 1000.0, method);
619 assert_eq!(simplified.len(), 1);
620 assert!(simplified[0].len() >= 2);
621
622 let encoded = encode_simplified_batch(&batch, 1000.0, method, 5).unwrap();
623 assert_eq!(encoded.len(), 1);
624 assert!(!encoded[0].is_empty());
625 }
626 }
627}