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}