rapidgeo_simplify/
batch.rs

1//! Batch and parallel processing for multiple polylines.
2//!
3//! This module provides functions to simplify multiple polylines efficiently,
4//! with optional parallel processing using [Rayon](https://docs.rs/rayon/).
5//!
6//! # Features
7//!
8//! - Batch processing of multiple polylines
9//! - Parallel processing with automatic work stealing
10//! - Memory-efficient "into" variants that reuse output buffers
11//! - Hybrid serial/parallel processing based on segment size
12//!
13//! # Examples
14//!
15//! ```rust
16//! use rapidgeo_distance::LngLat;
17//! use rapidgeo_simplify::{batch::simplify_batch, SimplifyMethod};
18//!
19//! let polylines = vec![
20//!     vec![
21//!         LngLat::new_deg(-122.0, 37.0),
22//!         LngLat::new_deg(-121.5, 37.5),
23//!         LngLat::new_deg(-121.0, 37.0),
24//!     ],
25//!     vec![
26//!         LngLat::new_deg(-74.0, 40.0),
27//!         LngLat::new_deg(-73.5, 40.5),
28//!         LngLat::new_deg(-73.0, 40.0),
29//!     ],
30//! ];
31//!
32//! let simplified = simplify_batch(
33//!     &polylines,
34//!     1000.0, // 1km tolerance
35//!     SimplifyMethod::GreatCircleMeters,
36//! );
37//!
38//! assert_eq!(simplified.len(), polylines.len());
39//! ```
40
41use crate::{xt::PerpDistance, SimplifyMethod};
42use rapidgeo_distance::LngLat;
43
44#[cfg(feature = "batch")]
45use rayon::prelude::*;
46
47/// Threshold for switching between serial and parallel distance calculations.
48///
49/// Segments with fewer candidate points use serial processing to avoid
50/// the overhead of parallel task creation.
51const PARALLEL_DISTANCE_THRESHOLD: usize = 100;
52
53/// Errors that can occur during batch processing operations.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum BatchError {
56    /// The provided output buffer is too small for the input data.
57    ///
58    /// This occurs when using the "_into" variants with pre-allocated buffers
59    /// that don't have enough capacity for all input polylines.
60    BufferTooSmall {
61        /// Number of slots needed in the output buffer
62        needed: usize,
63        /// Number of slots actually provided
64        provided: usize,
65    },
66}
67
68/// Simplify multiple polylines in parallel.
69///
70/// Uses Rayon to process polylines across multiple threads with automatic
71/// work stealing for optimal load balancing.
72///
73/// # Arguments
74///
75/// * `polylines` - Slice of input polylines to simplify
76/// * `tolerance_m` - Distance threshold for all polylines
77/// * `method` - Distance calculation method to use
78///
79/// # Returns
80///
81/// Vector of simplified polylines in the same order as input
82///
83/// # Examples
84///
85/// ```rust
86/// # #[cfg(feature = "batch")]
87/// # {
88/// use rapidgeo_distance::LngLat;
89/// use rapidgeo_simplify::{batch::simplify_batch_par, SimplifyMethod};
90///
91/// let polylines = vec![
92///     vec![
93///         LngLat::new_deg(-122.0, 37.0),
94///         LngLat::new_deg(-121.0, 37.0),
95///     ],
96///     // ... more polylines
97/// ];
98///
99/// let simplified = simplify_batch_par(
100///     &polylines,
101///     1000.0,
102///     SimplifyMethod::GreatCircleMeters,
103/// );
104/// # }
105/// ```
106///
107/// # Performance
108///
109/// Parallel processing provides the most benefit when:
110/// - Processing many polylines (>= 10)
111/// - Polylines have many points (>= 1000 each)
112/// - Using computationally expensive distance methods (GreatCircleMeters)
113#[cfg(feature = "batch")]
114pub fn simplify_batch_par(
115    polylines: &[Vec<LngLat>],
116    tolerance_m: f64,
117    method: SimplifyMethod,
118) -> Vec<Vec<LngLat>> {
119    polylines
120        .par_iter()
121        .map(|polyline| {
122            let mut simplified = Vec::new();
123            crate::simplify_dp_into(polyline, tolerance_m, method, &mut simplified);
124            simplified
125        })
126        .collect()
127}
128
129/// Simplify multiple polylines in parallel into pre-allocated output buffers.
130///
131/// This variant avoids allocating new vectors for the output, instead reusing
132/// the provided output slice. Each output vector is cleared before use.
133///
134/// # Arguments
135///
136/// * `polylines` - Input polylines to simplify
137/// * `tolerance_m` - Distance threshold
138/// * `method` - Distance calculation method
139/// * `output` - Pre-allocated output vectors (must have at least `polylines.len()` capacity)
140///
141/// # Errors
142///
143/// Returns [`BatchError::BufferTooSmall`] if the output slice has fewer
144/// elements than the input polylines slice.
145///
146/// # Examples
147///
148/// ```rust
149/// # #[cfg(feature = "batch")]
150/// # {
151/// use rapidgeo_distance::LngLat;
152/// use rapidgeo_simplify::{batch::simplify_batch_par_into, SimplifyMethod};
153///
154/// let polylines = vec![
155///     vec![LngLat::new_deg(-122.0, 37.0), LngLat::new_deg(-121.0, 37.0)],
156/// ];
157///
158/// let mut output = vec![Vec::new(); polylines.len()];
159/// let result = simplify_batch_par_into(
160///     &polylines,
161///     1000.0,
162///     SimplifyMethod::GreatCircleMeters,
163///     &mut output,
164/// );
165///
166/// assert!(result.is_ok());
167/// assert_eq!(output[0].len(), 2); // Both endpoints kept
168/// # }
169/// ```
170#[cfg(feature = "batch")]
171pub fn simplify_batch_par_into(
172    polylines: &[Vec<LngLat>],
173    tolerance_m: f64,
174    method: SimplifyMethod,
175    output: &mut [Vec<LngLat>],
176) -> Result<(), BatchError> {
177    if output.len() < polylines.len() {
178        return Err(BatchError::BufferTooSmall {
179            needed: polylines.len(),
180            provided: output.len(),
181        });
182    }
183
184    output[..polylines.len()]
185        .par_iter_mut()
186        .zip(polylines.par_iter())
187        .for_each(|(out_polyline, in_polyline)| {
188            crate::simplify_dp_into(in_polyline, tolerance_m, method, out_polyline);
189        });
190
191    Ok(())
192}
193
194/// Simplify multiple polylines sequentially.
195///
196/// Processes polylines one at a time in a single thread. Use this when:
197/// - Processing few polylines
198/// - Polylines are small
199/// - Memory usage is more important than speed
200/// - The `batch` feature is not enabled
201///
202/// # Examples
203///
204/// ```rust
205/// use rapidgeo_distance::LngLat;
206/// use rapidgeo_simplify::{batch::simplify_batch, SimplifyMethod};
207///
208/// let polylines = vec![
209///     vec![
210///         LngLat::new_deg(-122.0, 37.0),
211///         LngLat::new_deg(-121.5, 37.5),
212///         LngLat::new_deg(-121.0, 37.0),
213///     ],
214/// ];
215///
216/// let simplified = simplify_batch(
217///     &polylines,
218///     1000.0,
219///     SimplifyMethod::GreatCircleMeters,
220/// );
221///
222/// assert_eq!(simplified.len(), 1);
223/// assert!(simplified[0].len() >= 2); // Endpoints preserved
224/// ```
225pub fn simplify_batch(
226    polylines: &[Vec<LngLat>],
227    tolerance_m: f64,
228    method: SimplifyMethod,
229) -> Vec<Vec<LngLat>> {
230    polylines
231        .iter()
232        .map(|polyline| {
233            let mut simplified = Vec::new();
234            crate::simplify_dp_into(polyline, tolerance_m, method, &mut simplified);
235            simplified
236        })
237        .collect()
238}
239
240/// Simplify multiple polylines sequentially into pre-allocated output buffers.
241///
242/// Sequential version of [`simplify_batch_par_into`]. Use when parallel
243/// processing is not needed or the `batch` feature is disabled.
244///
245/// # Examples
246///
247/// ```rust
248/// use rapidgeo_distance::LngLat;
249/// use rapidgeo_simplify::{batch::{simplify_batch_into, BatchError}, SimplifyMethod};
250///
251/// let polylines = vec![
252///     vec![LngLat::new_deg(-122.0, 37.0), LngLat::new_deg(-121.0, 37.0)],
253/// ];
254///
255/// let mut output = vec![Vec::new(); 2]; // More capacity than needed
256/// let result = simplify_batch_into(
257///     &polylines,
258///     1000.0,
259///     SimplifyMethod::GreatCircleMeters,
260///     &mut output,
261/// );
262///
263/// assert!(result.is_ok());
264/// assert_eq!(output[0].len(), 2); // Both endpoints kept
265/// assert_eq!(output[1].len(), 0); // Second slot unused
266/// ```
267pub fn simplify_batch_into(
268    polylines: &[Vec<LngLat>],
269    tolerance_m: f64,
270    method: SimplifyMethod,
271    output: &mut [Vec<LngLat>],
272) -> Result<(), BatchError> {
273    if output.len() < polylines.len() {
274        return Err(BatchError::BufferTooSmall {
275            needed: polylines.len(),
276            provided: output.len(),
277        });
278    }
279
280    for (out_polyline, in_polyline) in output[..polylines.len()].iter_mut().zip(polylines.iter()) {
281        crate::simplify_dp_into(in_polyline, tolerance_m, method, out_polyline);
282    }
283
284    Ok(())
285}
286
287/// Generate a simplification mask using parallel processing.
288///
289/// Parallel version of [`crate::simplify_dp_mask`] that uses multiple threads
290/// for distance calculations on large line segments.
291///
292/// # Examples
293///
294/// ```rust
295/// # #[cfg(feature = "batch")]
296/// # {
297/// use rapidgeo_distance::LngLat;
298/// use rapidgeo_simplify::{batch::simplify_dp_mask_par, SimplifyMethod};
299///
300/// let points = vec![
301///     LngLat::new_deg(-122.0, 37.0),
302///     LngLat::new_deg(-121.5, 37.5),
303///     LngLat::new_deg(-121.0, 37.0),
304/// ];
305///
306/// let mut mask = Vec::new();
307/// simplify_dp_mask_par(
308///     &points,
309///     1000.0,
310///     SimplifyMethod::GreatCircleMeters,
311///     &mut mask,
312/// );
313///
314/// assert_eq!(mask.len(), points.len());
315/// # }
316/// ```
317///
318/// # Performance Notes
319///
320/// - Switches to parallel processing when segments have > 100 candidate points
321/// - For smaller segments, uses serial processing to avoid overhead
322/// - Results are identical to the serial version
323#[cfg(feature = "batch")]
324pub fn simplify_dp_mask_par(
325    pts: &[LngLat],
326    tolerance_m: f64,
327    method: SimplifyMethod,
328    mask: &mut Vec<bool>,
329) {
330    use crate::xt::*;
331
332    match method {
333        SimplifyMethod::GreatCircleMeters => {
334            let backend = XtGreatCircle;
335            simplify_mask_par(pts, tolerance_m, &backend, mask);
336        }
337        SimplifyMethod::PlanarMeters => {
338            let midpoint = crate::compute_midpoint(pts);
339            let backend = XtEnu { origin: midpoint };
340            simplify_mask_par(pts, tolerance_m, &backend, mask);
341        }
342        SimplifyMethod::EuclidRaw => {
343            let backend = XtEuclid;
344            simplify_mask_par(pts, tolerance_m, &backend, mask);
345        }
346    }
347}
348
349/// Simplify a single polyline using parallel processing.
350///
351/// Parallel version of [`crate::simplify_dp_into`] that can provide better
352/// performance for very large polylines (thousands of points).
353///
354/// # Examples
355///
356/// ```rust
357/// # #[cfg(feature = "batch")]
358/// # {
359/// use rapidgeo_distance::LngLat;
360/// use rapidgeo_simplify::{batch::simplify_dp_into_par, SimplifyMethod};
361///
362/// let points = vec![
363///     LngLat::new_deg(-122.0, 37.0),
364///     LngLat::new_deg(-121.5, 37.5),
365///     LngLat::new_deg(-121.0, 37.0),
366/// ];
367///
368/// let mut simplified = Vec::new();
369/// let count = simplify_dp_into_par(
370///     &points,
371///     1000.0,
372///     SimplifyMethod::GreatCircleMeters,
373///     &mut simplified,
374/// );
375///
376/// assert_eq!(count, simplified.len());
377/// assert!(count >= 2); // Endpoints preserved
378/// # }
379/// ```
380#[cfg(feature = "batch")]
381pub fn simplify_dp_into_par(
382    pts: &[LngLat],
383    tolerance_m: f64,
384    method: SimplifyMethod,
385    out: &mut Vec<LngLat>,
386) -> usize {
387    out.clear();
388
389    let mut mask = vec![false; pts.len()];
390    simplify_dp_mask_par(pts, tolerance_m, method, &mut mask);
391
392    for (i, &keep) in mask.iter().enumerate() {
393        if keep {
394            out.push(pts[i]);
395        }
396    }
397
398    out.len()
399}
400
401/// Internal parallel implementation of the Douglas-Peucker algorithm.
402///
403/// This function implements the same algorithm as [`crate::dp::simplify_mask`]
404/// but uses parallel processing for distance calculations when processing
405/// large line segments.
406///
407/// # Hybrid Processing
408///
409/// - Segments with ≤ 100 candidate points: Serial processing
410/// - Segments with > 100 candidate points: Parallel processing with Rayon
411///
412/// This hybrid approach avoids the overhead of parallel task creation for
413/// small segments while gaining benefits for large segments.
414#[cfg(feature = "batch")]
415fn simplify_mask_par<D: PerpDistance + Sync>(
416    pts: &[LngLat],
417    tolerance_m: f64,
418    backend: &D,
419    mask: &mut Vec<bool>,
420) {
421    let n = pts.len();
422
423    mask.clear();
424    mask.resize(n, false);
425
426    if n <= 2 {
427        for item in mask.iter_mut().take(n) {
428            *item = true;
429        }
430        return;
431    }
432
433    // Check if all points are identical
434    let first_point = pts[0];
435    let all_identical = pts
436        .iter()
437        .all(|&p| p.lng_deg == first_point.lng_deg && p.lat_deg == first_point.lat_deg);
438
439    if all_identical {
440        for item in mask.iter_mut().take(n) {
441            *item = true;
442        }
443        return;
444    }
445
446    mask[0] = true;
447    mask[n - 1] = true;
448
449    let mut stack = Vec::new();
450    stack.push((0, n - 1));
451
452    while let Some((i, j)) = stack.pop() {
453        if j <= i + 1 {
454            continue;
455        }
456
457        let candidate_range = (i + 1)..j;
458        let num_candidates = candidate_range.len();
459
460        let (max_distance, max_index) = if num_candidates > PARALLEL_DISTANCE_THRESHOLD {
461            // Use parallel processing for large segments
462            candidate_range
463                .into_par_iter()
464                .map(|k| (backend.d_perp_m(pts[i], pts[j], pts[k]), k))
465                .reduce_with(|(max_dist, max_idx), (dist, idx)| {
466                    if dist > max_dist {
467                        (dist, idx)
468                    } else {
469                        (max_dist, max_idx)
470                    }
471                })
472                .unwrap_or((0.0, i + 1))
473        } else {
474            // Use serial processing for small segments
475            let mut max_distance = 0.0;
476            let mut max_index = i + 1;
477
478            for k in candidate_range {
479                let distance = backend.d_perp_m(pts[i], pts[j], pts[k]);
480                if distance > max_distance {
481                    max_distance = distance;
482                    max_index = k;
483                }
484            }
485
486            (max_distance, max_index)
487        };
488
489        if max_distance > tolerance_m {
490            mask[max_index] = true;
491            stack.push((i, max_index));
492            stack.push((max_index, j));
493        }
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500    use rapidgeo_distance::LngLat;
501
502    fn create_test_polylines() -> Vec<Vec<LngLat>> {
503        vec![
504            vec![
505                LngLat::new_deg(-122.4194, 37.7749), // SF
506                LngLat::new_deg(-121.5, 37.0),
507                LngLat::new_deg(-118.2437, 34.0522), // LA
508            ],
509            vec![
510                LngLat::new_deg(-74.0060, 40.7128), // NYC
511                LngLat::new_deg(-75.0, 40.0),
512                LngLat::new_deg(-87.6298, 41.8781), // Chicago
513            ],
514        ]
515    }
516
517    #[test]
518    fn test_simplify_batch() {
519        let polylines = create_test_polylines();
520        let simplified = simplify_batch(&polylines, 1000.0, SimplifyMethod::GreatCircleMeters);
521
522        assert_eq!(simplified.len(), 2);
523        // Each simplified polyline should have at least endpoints
524        for simplified_line in &simplified {
525            assert!(simplified_line.len() >= 2);
526        }
527    }
528
529    #[test]
530    fn test_simplify_batch_into() {
531        let polylines = create_test_polylines();
532        let mut output = vec![Vec::new(); 3]; // Larger than needed
533
534        let result = simplify_batch_into(
535            &polylines,
536            1000.0,
537            SimplifyMethod::GreatCircleMeters,
538            &mut output,
539        );
540
541        assert!(result.is_ok());
542        assert!(output[0].len() >= 2); // First polyline simplified
543        assert!(output[1].len() >= 2); // Second polyline simplified
544        assert_eq!(output[2].len(), 0); // Third slot unused
545    }
546
547    #[test]
548    fn test_simplify_batch_into_too_small() {
549        let polylines = create_test_polylines();
550        let mut output = vec![Vec::new(); 1]; // Too small!
551
552        let result = simplify_batch_into(
553            &polylines,
554            1000.0,
555            SimplifyMethod::GreatCircleMeters,
556            &mut output,
557        );
558
559        assert!(result.is_err());
560        match result.unwrap_err() {
561            BatchError::BufferTooSmall { needed, provided } => {
562                assert_eq!(needed, 2);
563                assert_eq!(provided, 1);
564            }
565        }
566    }
567
568    #[test]
569    #[cfg(feature = "batch")]
570    fn test_simplify_batch_par() {
571        let polylines = create_test_polylines();
572        let simplified = simplify_batch_par(&polylines, 1000.0, SimplifyMethod::GreatCircleMeters);
573
574        assert_eq!(simplified.len(), 2);
575        // Each simplified polyline should have at least endpoints
576        for simplified_line in &simplified {
577            assert!(simplified_line.len() >= 2);
578        }
579
580        // Compare with serial version
581        let serial_simplified =
582            simplify_batch(&polylines, 1000.0, SimplifyMethod::GreatCircleMeters);
583        assert_eq!(simplified, serial_simplified);
584    }
585
586    #[test]
587    #[cfg(feature = "batch")]
588    fn test_simplify_batch_par_into_too_small() {
589        let polylines = create_test_polylines();
590        let mut par_output = vec![Vec::new(); 1]; // Too small!
591
592        let result = simplify_batch_par_into(
593            &polylines,
594            1000.0,
595            SimplifyMethod::GreatCircleMeters,
596            &mut par_output,
597        );
598
599        assert!(result.is_err());
600        match result.unwrap_err() {
601            BatchError::BufferTooSmall { needed, provided } => {
602                assert_eq!(needed, 2);
603                assert_eq!(provided, 1);
604            }
605        }
606    }
607
608    #[test]
609    #[cfg(feature = "batch")]
610    fn test_simplify_batch_par_into() {
611        let polylines = create_test_polylines();
612        let mut par_output = vec![Vec::new(); 2];
613        let mut serial_output = vec![Vec::new(); 2];
614
615        let par_result = simplify_batch_par_into(
616            &polylines,
617            1000.0,
618            SimplifyMethod::GreatCircleMeters,
619            &mut par_output,
620        );
621        let serial_result = simplify_batch_into(
622            &polylines,
623            1000.0,
624            SimplifyMethod::GreatCircleMeters,
625            &mut serial_output,
626        );
627
628        assert!(par_result.is_ok());
629        assert!(serial_result.is_ok());
630        assert_eq!(par_output, serial_output);
631    }
632
633    #[test]
634    #[cfg(feature = "batch")]
635    fn test_simplify_dp_mask_par() {
636        let points = vec![
637            LngLat::new_deg(-122.0, 37.0),
638            LngLat::new_deg(-121.5, 37.5),
639            LngLat::new_deg(-121.0, 37.0),
640        ];
641
642        let mut par_mask = Vec::new();
643        let mut serial_mask = Vec::new();
644
645        simplify_dp_mask_par(
646            &points,
647            1000.0,
648            SimplifyMethod::GreatCircleMeters,
649            &mut par_mask,
650        );
651        crate::simplify_dp_mask(
652            &points,
653            1000.0,
654            SimplifyMethod::GreatCircleMeters,
655            &mut serial_mask,
656        );
657
658        assert_eq!(par_mask, serial_mask);
659        assert!(par_mask[0]); // First endpoint
660        assert!(par_mask[2]); // Last endpoint
661    }
662
663    #[test]
664    #[cfg(feature = "batch")]
665    fn test_simplify_dp_mask_par_planar_meters() {
666        let points = vec![
667            LngLat::new_deg(-122.0, 37.0),
668            LngLat::new_deg(-121.5, 37.5),
669            LngLat::new_deg(-121.0, 37.0),
670        ];
671
672        let mut par_mask = Vec::new();
673        let mut serial_mask = Vec::new();
674
675        simplify_dp_mask_par(&points, 1000.0, SimplifyMethod::PlanarMeters, &mut par_mask);
676        crate::simplify_dp_mask(
677            &points,
678            1000.0,
679            SimplifyMethod::PlanarMeters,
680            &mut serial_mask,
681        );
682
683        assert_eq!(par_mask, serial_mask);
684        assert!(par_mask[0]); // First endpoint
685        assert!(par_mask[2]); // Last endpoint
686    }
687
688    #[test]
689    #[cfg(feature = "batch")]
690    fn test_simplify_dp_mask_par_euclid_raw() {
691        let points = vec![
692            LngLat::new_deg(-122.0, 37.0),
693            LngLat::new_deg(-121.5, 37.5),
694            LngLat::new_deg(-121.0, 37.0),
695        ];
696
697        let mut par_mask = Vec::new();
698        let mut serial_mask = Vec::new();
699
700        simplify_dp_mask_par(&points, 0.5, SimplifyMethod::EuclidRaw, &mut par_mask);
701        crate::simplify_dp_mask(&points, 0.5, SimplifyMethod::EuclidRaw, &mut serial_mask);
702
703        assert_eq!(par_mask, serial_mask);
704        assert!(par_mask[0]); // First endpoint
705        assert!(par_mask[2]); // Last endpoint
706    }
707
708    #[test]
709    #[cfg(feature = "batch")]
710    fn test_simplify_dp_into_par() {
711        let points = vec![
712            LngLat::new_deg(-122.0, 37.0),
713            LngLat::new_deg(-121.5, 37.5),
714            LngLat::new_deg(-121.0, 37.0),
715        ];
716
717        let mut par_output = Vec::new();
718        let mut serial_output = Vec::new();
719
720        let par_count = simplify_dp_into_par(
721            &points,
722            1000.0,
723            SimplifyMethod::GreatCircleMeters,
724            &mut par_output,
725        );
726        let serial_count = crate::simplify_dp_into(
727            &points,
728            1000.0,
729            SimplifyMethod::GreatCircleMeters,
730            &mut serial_output,
731        );
732
733        assert_eq!(par_count, serial_count);
734        assert_eq!(par_output, serial_output);
735        assert_eq!(par_count, par_output.len());
736    }
737
738    #[test]
739    #[cfg(feature = "batch")]
740    fn test_parallel_threshold_behavior() {
741        // Create a large polyline to test parallel behavior
742        let mut large_polyline = Vec::new();
743        for i in 0..1000 {
744            large_polyline.push(LngLat::new_deg(
745                -122.0 + i as f64 * 0.001,
746                37.0 + (i as f64 * 0.1).sin() * 0.01,
747            ));
748        }
749
750        let mut par_mask = Vec::new();
751        let mut serial_mask = Vec::new();
752
753        simplify_dp_mask_par(
754            &large_polyline,
755            50.0,
756            SimplifyMethod::GreatCircleMeters,
757            &mut par_mask,
758        );
759        crate::simplify_dp_mask(
760            &large_polyline,
761            50.0,
762            SimplifyMethod::GreatCircleMeters,
763            &mut serial_mask,
764        );
765
766        // Results should be identical regardless of parallel/serial execution
767        assert_eq!(par_mask, serial_mask);
768        assert!(par_mask[0]); // First endpoint always kept
769        assert!(par_mask[par_mask.len() - 1]); // Last endpoint always kept
770    }
771
772    #[test]
773    fn test_different_methods_batch() {
774        let polylines = vec![vec![
775            LngLat::new_deg(-122.0, 37.0),
776            LngLat::new_deg(-121.5, 37.5),
777            LngLat::new_deg(-121.0, 37.0),
778        ]];
779
780        for method in [
781            SimplifyMethod::GreatCircleMeters,
782            SimplifyMethod::PlanarMeters,
783            SimplifyMethod::EuclidRaw,
784        ] {
785            let simplified = simplify_batch(&polylines, 1000.0, method);
786            assert_eq!(simplified.len(), 1);
787            assert!(simplified[0].len() >= 2); // At least endpoints preserved
788        }
789    }
790
791    #[test]
792    fn test_empty_and_small_polylines() {
793        let polylines = vec![
794            vec![],                                                             // Empty
795            vec![LngLat::new_deg(-122.0, 37.0)],                                // Single point
796            vec![LngLat::new_deg(-122.0, 37.0), LngLat::new_deg(-121.0, 37.0)], // Two points
797        ];
798
799        let simplified = simplify_batch(&polylines, 1000.0, SimplifyMethod::GreatCircleMeters);
800
801        assert_eq!(simplified.len(), 3);
802        assert_eq!(simplified[0].len(), 0); // Empty stays empty
803        assert_eq!(simplified[1].len(), 1); // Single point stays single
804        assert_eq!(simplified[2].len(), 2); // Two points stay two
805    }
806}