Skip to main content

oxigdal_algorithms/vector/topology/
split.rs

1//! Split operations for dividing geometric features
2//!
3//! This module implements splitting operations that divide geometries using
4//! other geometries as cutting tools.
5
6use crate::error::{AlgorithmError, Result};
7use oxigdal_core::vector::{Coordinate, LineString, Point, Polygon};
8
9/// Options for split operations
10#[derive(Debug, Clone)]
11pub struct SplitOptions {
12    /// Tolerance for coordinate comparison
13    pub tolerance: f64,
14    /// Whether to snap split points to grid
15    pub snap_to_grid: bool,
16    /// Grid size for snapping (if enabled)
17    pub grid_size: f64,
18    /// Minimum length for resulting line segments
19    pub min_segment_length: f64,
20    /// Whether to preserve all split parts (even very small ones)
21    pub preserve_all: bool,
22}
23
24impl Default for SplitOptions {
25    fn default() -> Self {
26        Self {
27            tolerance: 1e-10,
28            snap_to_grid: false,
29            grid_size: 1e-6,
30            min_segment_length: 0.0,
31            preserve_all: true,
32        }
33    }
34}
35
36/// Result of a split operation
37#[derive(Debug, Clone)]
38pub struct SplitResult {
39    /// The resulting geometries from the split
40    pub geometries: Vec<SplitGeometry>,
41    /// Number of split points used
42    pub num_splits: usize,
43    /// Whether all splits were successful
44    pub complete: bool,
45}
46
47/// A geometry resulting from a split operation
48#[derive(Debug, Clone)]
49pub enum SplitGeometry {
50    /// A linestring segment
51    LineString(LineString),
52    /// A polygon
53    Polygon(Polygon),
54}
55
56/// Split a linestring by point locations
57///
58/// Divides a linestring into multiple segments at specified points.
59///
60/// # Arguments
61///
62/// * `linestring` - The linestring to split
63/// * `split_points` - Points where the linestring should be split
64/// * `options` - Split options
65///
66/// # Returns
67///
68/// Result containing the split result with multiple linestring segments
69///
70/// # Examples
71///
72/// ```
73/// use oxigdal_algorithms::vector::topology::{split_linestring_by_points, SplitOptions};
74/// use oxigdal_algorithms::{Coordinate, LineString, Point};
75///
76/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
77/// let coords = vec![
78///     Coordinate::new_2d(0.0, 0.0),
79///     Coordinate::new_2d(10.0, 0.0),
80/// ];
81/// let linestring = LineString::new(coords)?;
82///
83/// let split_points = vec![
84///     Point::new(5.0, 0.0),
85/// ];
86///
87/// let result = split_linestring_by_points(&linestring, &split_points, &SplitOptions::default())?;
88/// assert_eq!(result.num_splits, 1);
89/// assert!(result.geometries.len() >= 2);
90/// # Ok(())
91/// # }
92/// ```
93pub fn split_linestring_by_points(
94    linestring: &LineString,
95    split_points: &[Point],
96    options: &SplitOptions,
97) -> Result<SplitResult> {
98    if split_points.is_empty() {
99        return Ok(SplitResult {
100            geometries: vec![SplitGeometry::LineString(linestring.clone())],
101            num_splits: 0,
102            complete: true,
103        });
104    }
105
106    let coords = &linestring.coords;
107    if coords.len() < 2 {
108        return Err(AlgorithmError::InvalidGeometry(
109            "Linestring must have at least 2 coordinates".to_string(),
110        ));
111    }
112
113    // Find all split locations along the linestring
114    let mut split_locations = Vec::new();
115
116    for point in split_points {
117        if let Some(location) = find_point_on_linestring(linestring, point, options.tolerance)? {
118            split_locations.push(location);
119        }
120    }
121
122    // Sort split locations by segment index and parameter
123    // Validate for NaN values before sorting
124    for loc in &split_locations {
125        if loc.parameter.is_nan() {
126            return Err(AlgorithmError::InvalidGeometry(
127                "Split location parameter contains NaN value".to_string(),
128            ));
129        }
130    }
131
132    split_locations.sort_by(|a, b| {
133        a.segment_index.cmp(&b.segment_index).then(
134            a.parameter
135                .partial_cmp(&b.parameter)
136                .unwrap_or(std::cmp::Ordering::Equal),
137        )
138    });
139
140    // Remove duplicates
141    split_locations.dedup_by(|a, b| {
142        a.segment_index == b.segment_index && (a.parameter - b.parameter).abs() < options.tolerance
143    });
144
145    if split_locations.is_empty() {
146        return Ok(SplitResult {
147            geometries: vec![SplitGeometry::LineString(linestring.clone())],
148            num_splits: 0,
149            complete: false,
150        });
151    }
152
153    // Build split segments
154    let mut result_geometries = Vec::new();
155    let mut current_coords = Vec::new();
156    let mut current_segment_idx = 0;
157    let mut split_idx = 0;
158
159    current_coords.push(coords[0]);
160
161    for i in 0..coords.len().saturating_sub(1) {
162        // Add splits on this segment
163        while split_idx < split_locations.len() && split_locations[split_idx].segment_index == i {
164            let split_loc = &split_locations[split_idx];
165            let split_coord = split_loc.coordinate;
166
167            // Add split point
168            current_coords.push(split_coord);
169
170            // Create a new linestring if we have enough points
171            if current_coords.len() >= 2 {
172                if let Ok(ls) = LineString::new(current_coords.clone()) {
173                    if options.preserve_all
174                        || compute_linestring_length(&ls) >= options.min_segment_length
175                    {
176                        result_geometries.push(SplitGeometry::LineString(ls));
177                    }
178                }
179            }
180
181            // Start new segment
182            current_coords.clear();
183            current_coords.push(split_coord);
184
185            split_idx += 1;
186        }
187
188        // Add end point of current segment
189        current_coords.push(coords[i + 1]);
190        current_segment_idx = i + 1;
191    }
192
193    // Add final segment
194    if current_coords.len() >= 2 {
195        if let Ok(ls) = LineString::new(current_coords) {
196            if options.preserve_all || compute_linestring_length(&ls) >= options.min_segment_length
197            {
198                result_geometries.push(SplitGeometry::LineString(ls));
199            }
200        }
201    }
202
203    Ok(SplitResult {
204        geometries: result_geometries,
205        num_splits: split_locations.len(),
206        complete: true,
207    })
208}
209
210/// Location of a point on a linestring
211#[derive(Debug, Clone)]
212struct PointLocation {
213    /// Index of the segment (0-based)
214    segment_index: usize,
215    /// Parameter along the segment (0.0 to 1.0)
216    parameter: f64,
217    /// The coordinate at this location
218    coordinate: Coordinate,
219}
220
221/// Find a point on a linestring
222fn find_point_on_linestring(
223    linestring: &LineString,
224    point: &Point,
225    tolerance: f64,
226) -> Result<Option<PointLocation>> {
227    let coords = &linestring.coords;
228
229    for i in 0..coords.len().saturating_sub(1) {
230        let p1 = coords[i];
231        let p2 = coords[i + 1];
232
233        // Check if point is on this segment
234        if let Some((param, coord)) = point_on_segment(point, &p1, &p2, tolerance) {
235            return Ok(Some(PointLocation {
236                segment_index: i,
237                parameter: param,
238                coordinate: coord,
239            }));
240        }
241    }
242
243    Ok(None)
244}
245
246/// Check if a point is on a line segment
247fn point_on_segment(
248    point: &Point,
249    p1: &Coordinate,
250    p2: &Coordinate,
251    tolerance: f64,
252) -> Option<(f64, Coordinate)> {
253    let px = point.coord.x;
254    let py = point.coord.y;
255
256    // Vector from p1 to p2
257    let dx = p2.x - p1.x;
258    let dy = p2.y - p1.y;
259
260    // Length squared of segment
261    let len_sq = dx * dx + dy * dy;
262
263    if len_sq < tolerance * tolerance {
264        // Degenerate segment
265        return None;
266    }
267
268    // Parameter t along the segment
269    let t = ((px - p1.x) * dx + (py - p1.y) * dy) / len_sq;
270
271    // Check if point projects onto the segment (not before or after)
272    if t < -tolerance || t > 1.0 + tolerance {
273        return None;
274    }
275
276    // Clamp t to [0, 1]
277    let t = t.clamp(0.0, 1.0);
278
279    // Compute closest point on segment
280    let closest_x = p1.x + t * dx;
281    let closest_y = p1.y + t * dy;
282
283    // Check distance from point to closest point
284    let dist_sq = (px - closest_x).powi(2) + (py - closest_y).powi(2);
285
286    if dist_sq < tolerance * tolerance {
287        Some((t, Coordinate::new_2d(closest_x, closest_y)))
288    } else {
289        None
290    }
291}
292
293/// Compute the length of a linestring
294fn compute_linestring_length(linestring: &LineString) -> f64 {
295    let coords = &linestring.coords;
296    let mut length = 0.0;
297
298    for i in 0..coords.len().saturating_sub(1) {
299        let dx = coords[i + 1].x - coords[i].x;
300        let dy = coords[i + 1].y - coords[i].y;
301        length += (dx * dx + dy * dy).sqrt();
302    }
303
304    length
305}
306
307/// Split a polygon by a linestring
308///
309/// Divides a polygon into multiple parts using a linestring as a cutting tool.
310///
311/// # Arguments
312///
313/// * `polygon` - The polygon to split
314/// * `split_line` - The linestring to use for splitting
315/// * `options` - Split options
316///
317/// # Returns
318///
319/// Result containing the split polygons
320///
321/// # Examples
322///
323/// ```no_run
324/// use oxigdal_algorithms::vector::topology::{split_polygon_by_line, SplitOptions};
325/// use oxigdal_algorithms::{Coordinate, LineString, Polygon};
326///
327/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
328/// let coords = vec![
329///     Coordinate::new_2d(0.0, 0.0),
330///     Coordinate::new_2d(10.0, 0.0),
331///     Coordinate::new_2d(10.0, 10.0),
332///     Coordinate::new_2d(0.0, 10.0),
333///     Coordinate::new_2d(0.0, 0.0),
334/// ];
335/// let exterior = LineString::new(coords)?;
336/// let polygon = Polygon::new(exterior, vec![])?;
337///
338/// let split_coords = vec![
339///     Coordinate::new_2d(0.0, 5.0),
340///     Coordinate::new_2d(10.0, 5.0),
341/// ];
342/// let split_line = LineString::new(split_coords)?;
343///
344/// let result = split_polygon_by_line(&polygon, &split_line, &SplitOptions::default())?;
345/// assert!(result.geometries.len() >= 1);
346/// # Ok(())
347/// # }
348/// ```
349pub fn split_polygon_by_line(
350    polygon: &Polygon,
351    split_line: &LineString,
352    options: &SplitOptions,
353) -> Result<SplitResult> {
354    // Find intersection points between polygon boundary and split line
355    let intersection_points = find_polygon_line_intersections(polygon, split_line, options)?;
356
357    if intersection_points.len() < 2 {
358        // Not enough intersections to split
359        return Ok(SplitResult {
360            geometries: vec![SplitGeometry::Polygon(polygon.clone())],
361            num_splits: 0,
362            complete: false,
363        });
364    }
365
366    // Create polygons from split
367    let result_polygons =
368        create_split_polygons(polygon, split_line, &intersection_points, options)?;
369
370    let geometries = result_polygons
371        .into_iter()
372        .map(SplitGeometry::Polygon)
373        .collect();
374
375    Ok(SplitResult {
376        geometries,
377        num_splits: intersection_points.len(),
378        complete: true,
379    })
380}
381
382/// Find intersection points between polygon boundary and a linestring
383fn find_polygon_line_intersections(
384    polygon: &Polygon,
385    line: &LineString,
386    options: &SplitOptions,
387) -> Result<Vec<Coordinate>> {
388    let mut intersections = Vec::new();
389
390    // Check exterior ring
391    let exterior_intersections = find_linestring_intersections(&polygon.exterior, line, options)?;
392    intersections.extend(exterior_intersections);
393
394    // Check interior rings (holes)
395    for interior in &polygon.interiors {
396        let interior_intersections = find_linestring_intersections(interior, line, options)?;
397        intersections.extend(interior_intersections);
398    }
399
400    // Remove duplicates
401    // Validate for NaN values before sorting
402    for coord in &intersections {
403        if coord.x.is_nan() || coord.y.is_nan() {
404            return Err(AlgorithmError::InvalidGeometry(
405                "Intersection coordinate contains NaN value".to_string(),
406            ));
407        }
408    }
409
410    intersections.sort_by(|a, b| {
411        a.x.partial_cmp(&b.x)
412            .unwrap_or(std::cmp::Ordering::Equal)
413            .then(a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
414    });
415
416    intersections.dedup_by(|a, b| {
417        (a.x - b.x).abs() < options.tolerance && (a.y - b.y).abs() < options.tolerance
418    });
419
420    Ok(intersections)
421}
422
423/// Find intersections between two linestrings
424fn find_linestring_intersections(
425    line1: &LineString,
426    line2: &LineString,
427    options: &SplitOptions,
428) -> Result<Vec<Coordinate>> {
429    let mut intersections = Vec::new();
430    let coords1 = &line1.coords;
431    let coords2 = &line2.coords;
432
433    for i in 0..coords1.len().saturating_sub(1) {
434        for j in 0..coords2.len().saturating_sub(1) {
435            if let Some(intersection) = compute_segment_intersection(
436                &coords1[i],
437                &coords1[i + 1],
438                &coords2[j],
439                &coords2[j + 1],
440                options.tolerance,
441            ) {
442                intersections.push(intersection);
443            }
444        }
445    }
446
447    Ok(intersections)
448}
449
450/// Compute intersection point between two line segments
451fn compute_segment_intersection(
452    p1: &Coordinate,
453    p2: &Coordinate,
454    p3: &Coordinate,
455    p4: &Coordinate,
456    tolerance: f64,
457) -> Option<Coordinate> {
458    let d = (p1.x - p2.x) * (p3.y - p4.y) - (p1.y - p2.y) * (p3.x - p4.x);
459
460    if d.abs() < tolerance {
461        // Parallel or coincident
462        return None;
463    }
464
465    let t = ((p1.x - p3.x) * (p3.y - p4.y) - (p1.y - p3.y) * (p3.x - p4.x)) / d;
466    let u = -((p1.x - p2.x) * (p1.y - p3.y) - (p1.y - p2.y) * (p1.x - p3.x)) / d;
467
468    if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
469        let x = p1.x + t * (p2.x - p1.x);
470        let y = p1.y + t * (p2.y - p1.y);
471        Some(Coordinate::new_2d(x, y))
472    } else {
473        None
474    }
475}
476
477/// Create split polygons from intersection points
478fn create_split_polygons(
479    _polygon: &Polygon,
480    _split_line: &LineString,
481    _intersections: &[Coordinate],
482    _options: &SplitOptions,
483) -> Result<Vec<Polygon>> {
484    // This is a simplified implementation
485    // A full implementation would need to:
486    // 1. Build a planar graph from the polygon and split line
487    // 2. Find faces in the graph
488    // 3. Construct polygons from faces
489
490    // For now, return empty result as a placeholder
491    Ok(Vec::new())
492}
493
494/// Split a polygon by another polygon
495///
496/// Divides a polygon using another polygon's boundary as cutting edges.
497pub fn split_polygon_by_polygon(
498    target_poly: &Polygon,
499    split_poly: &Polygon,
500    options: &SplitOptions,
501) -> Result<SplitResult> {
502    // Use the exterior ring of the split polygon as the splitting linestring
503    let split_line = &split_poly.exterior.clone();
504
505    split_polygon_by_line(target_poly, split_line, options)
506}
507
508/// Batch split operation for multiple polygons
509pub fn split_polygons_batch(
510    polygons: &[Polygon],
511    split_line: &LineString,
512    options: &SplitOptions,
513) -> Result<Vec<SplitResult>> {
514    polygons
515        .iter()
516        .map(|poly| split_polygon_by_line(poly, split_line, options))
517        .collect()
518}
519
520#[cfg(test)]
521mod tests {
522    use super::*;
523
524    fn create_linestring(coords: Vec<(f64, f64)>) -> LineString {
525        let coords: Vec<Coordinate> = coords
526            .iter()
527            .map(|(x, y)| Coordinate::new_2d(*x, *y))
528            .collect();
529        LineString::new(coords).expect("Failed to create linestring")
530    }
531
532    fn create_polygon(coords: Vec<(f64, f64)>) -> Polygon {
533        let coords: Vec<Coordinate> = coords
534            .iter()
535            .map(|(x, y)| Coordinate::new_2d(*x, *y))
536            .collect();
537        let exterior = LineString::new(coords).expect("Failed to create linestring");
538        Polygon::new(exterior, vec![]).expect("Failed to create polygon")
539    }
540
541    #[test]
542    fn test_split_linestring_single_point() {
543        let linestring = create_linestring(vec![(0.0, 0.0), (10.0, 0.0)]);
544        let split_points = vec![Point::new(5.0, 0.0)];
545
546        let result =
547            split_linestring_by_points(&linestring, &split_points, &SplitOptions::default());
548        assert!(result.is_ok());
549
550        let split_result = result.expect("Split failed");
551        assert_eq!(split_result.num_splits, 1);
552        assert!(split_result.geometries.len() >= 2);
553    }
554
555    #[test]
556    fn test_split_linestring_multiple_points() {
557        let linestring = create_linestring(vec![(0.0, 0.0), (10.0, 0.0)]);
558        let split_points = vec![Point::new(3.0, 0.0), Point::new(7.0, 0.0)];
559
560        let result =
561            split_linestring_by_points(&linestring, &split_points, &SplitOptions::default());
562        assert!(result.is_ok());
563
564        let split_result = result.expect("Split failed");
565        assert_eq!(split_result.num_splits, 2);
566    }
567
568    #[test]
569    fn test_split_linestring_no_intersection() {
570        let linestring = create_linestring(vec![(0.0, 0.0), (10.0, 0.0)]);
571        let split_points = vec![Point::new(5.0, 5.0)]; // Not on line
572
573        let result =
574            split_linestring_by_points(&linestring, &split_points, &SplitOptions::default());
575        assert!(result.is_ok());
576
577        let split_result = result.expect("Split failed");
578        assert_eq!(split_result.num_splits, 0);
579        assert_eq!(split_result.geometries.len(), 1); // Unchanged
580    }
581
582    #[test]
583    fn test_split_linestring_empty_splits() {
584        let linestring = create_linestring(vec![(0.0, 0.0), (10.0, 0.0)]);
585        let split_points = vec![];
586
587        let result =
588            split_linestring_by_points(&linestring, &split_points, &SplitOptions::default());
589        assert!(result.is_ok());
590
591        let split_result = result.expect("Split failed");
592        assert_eq!(split_result.num_splits, 0);
593        assert_eq!(split_result.geometries.len(), 1);
594    }
595
596    #[test]
597    fn test_point_on_segment() {
598        let p1 = Coordinate::new_2d(0.0, 0.0);
599        let p2 = Coordinate::new_2d(10.0, 0.0);
600        let point = Point::new(5.0, 0.0);
601
602        let result = point_on_segment(&point, &p1, &p2, 1e-10);
603        assert!(result.is_some());
604
605        if let Some((param, coord)) = result {
606            assert!((param - 0.5).abs() < 1e-10);
607            assert!((coord.x - 5.0).abs() < 1e-10);
608            assert!((coord.y - 0.0).abs() < 1e-10);
609        }
610    }
611
612    #[test]
613    fn test_point_not_on_segment() {
614        let p1 = Coordinate::new_2d(0.0, 0.0);
615        let p2 = Coordinate::new_2d(10.0, 0.0);
616        let point = Point::new(5.0, 5.0); // Off the line
617
618        let result = point_on_segment(&point, &p1, &p2, 1e-10);
619        assert!(result.is_none());
620    }
621
622    #[test]
623    fn test_compute_segment_intersection() {
624        let p1 = Coordinate::new_2d(0.0, 0.0);
625        let p2 = Coordinate::new_2d(10.0, 10.0);
626        let p3 = Coordinate::new_2d(0.0, 10.0);
627        let p4 = Coordinate::new_2d(10.0, 0.0);
628
629        let result = compute_segment_intersection(&p1, &p2, &p3, &p4, 1e-10);
630        assert!(result.is_some());
631
632        if let Some(intersection) = result {
633            // Intersection should be at (5, 5)
634            assert!((intersection.x - 5.0).abs() < 1e-6);
635            assert!((intersection.y - 5.0).abs() < 1e-6);
636        }
637    }
638
639    #[test]
640    fn test_split_polygon_by_line() {
641        let polygon = create_polygon(vec![
642            (0.0, 0.0),
643            (10.0, 0.0),
644            (10.0, 10.0),
645            (0.0, 10.0),
646            (0.0, 0.0),
647        ]);
648
649        let split_line = create_linestring(vec![(0.0, 5.0), (10.0, 5.0)]);
650
651        let result = split_polygon_by_line(&polygon, &split_line, &SplitOptions::default());
652        assert!(result.is_ok());
653
654        let split_result = result.expect("Split failed");
655        assert_eq!(split_result.num_splits, 2); // Two intersection points
656    }
657
658    #[test]
659    fn test_compute_linestring_length() {
660        let linestring = create_linestring(vec![(0.0, 0.0), (3.0, 0.0), (3.0, 4.0)]);
661        let length = compute_linestring_length(&linestring);
662
663        // Length is 3 + 4 = 7
664        assert!((length - 7.0).abs() < 1e-6);
665    }
666}