Skip to main content

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}