diff --git a/lib/src/boolean/compare_segments.rs b/lib/src/boolean/compare_segments.rs index 6c40ec3..1ed81d9 100644 --- a/lib/src/boolean/compare_segments.rs +++ b/lib/src/boolean/compare_segments.rs @@ -92,7 +92,14 @@ where // left and right endpoints. I think in order to properly support self-overlapping // segments we must return Ordering::Equal if and only if segments are the same // by identity (the Rc::ptr_eq above). - less_if(se_old_l.contour_id < se_new_l.contour_id) + if se_old_l.contour_id != se_new_l.contour_id { + less_if(se_old_l.contour_id < se_new_l.contour_id) + } else { + less_if( + ( Rc::as_ptr(se_old_l) as *const () ) + < ( Rc::as_ptr(se_new_l) as *const () ) + ) + } } else { // Fallback to purely temporal-based comparison. Since `less_if` already // encodes "earlier-is-less" semantics, no comparison is needed. diff --git a/tests/fixtures/generic_test_cases/daef_foo_tri2.geojson b/tests/fixtures/generic_test_cases/daef_foo_tri2.geojson new file mode 100644 index 0000000..9494e1a --- /dev/null +++ b/tests/fixtures/generic_test_cases/daef_foo_tri2.geojson @@ -0,0 +1,83 @@ +{ + "features": [ + { + "geometry": { + "coordinates": [ + [ + [62.5, 62.5], + [187.5, 187.5], + [237.5, 62.5], + [62.5, 62.5] + ], + [ + [162.5, 62.5], + [362.5, 187.5], + [362.5, 62.5], + [162.5, 62.5] + ] + ], + "type": "Polygon" + }, + "properties": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [0, 240], + [300, 240], + [150, 30], + [0, 240] + ] + ], + "type": "Polygon" + }, + "properties": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + [62.5, 62.5], + [126.78571428571429, 62.5], + [100, 100], + [62.5, 62.5] + ] + ], + [ + [ + [173.21428571428572, 62.5], + [181.85483870967744, 74.5967741935484], + [173.21428571428572, 62.5] + ] + ], + [ + [ + [187.5, 187.5], + [214.42307692307693, 120.19230769230768], + [300, 240], + [187.5, 187.5] + ] + ], + [ + [ + [237.5, 62.5], + [362.5, 62.5], + [362.5, 187.5], + [237.5, 62.5] + ] + ] + ], + "type": "MultiPolygon" + }, + "properties": { + "operation": "diff" + }, + "type": "Feature" + } + ], + "type": "FeatureCollection" +} diff --git a/tests/fixtures/generic_test_cases/daef_holed_tri2.geojson b/tests/fixtures/generic_test_cases/daef_holed_tri2.geojson new file mode 100644 index 0000000..1b2509a --- /dev/null +++ b/tests/fixtures/generic_test_cases/daef_holed_tri2.geojson @@ -0,0 +1,43 @@ +{ + "features": [ + { + "geometry": { + "coordinates": [ + [ + [0, 0], + [300, 0], + [150, 300], + [0, 0], + [0, 300], + [0, 0] + ], + [ + [60, 60], + [150, 240], + [240, 60], + [60, 60] + ] + ], + "type": "Polygon" + }, + "properties": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [0, 240], + [300, 240], + [150, 30], + [0, 240] + ] + ], + "type": "Polygon" + }, + "properties": {}, + "type": "Feature" + } + ], + "type": "FeatureCollection" +} diff --git a/tests/fixtures/generic_test_cases/daef_swapped_twointersecting_empty.geojson b/tests/fixtures/generic_test_cases/daef_swapped_twointersecting_empty.geojson new file mode 100644 index 0000000..36c63ef --- /dev/null +++ b/tests/fixtures/generic_test_cases/daef_swapped_twointersecting_empty.geojson @@ -0,0 +1,110 @@ +{ + "features": [ + { + "geometry": { + "coordinates": [], + "type": "Polygon" + }, + "properties": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + 75, + 75 + ], + [ + 275, + 225 + ], + [ + 75, + 375 + ], + [ + 75, + 75 + ] + ], + [ + [ + 175, + 225 + ], + [ + 375, + 75 + ], + [ + 375, + 375 + ], + [ + 175, + 225 + ] + ] + ], + "type": "Polygon" + }, + "properties": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + [ + 75, + 75 + ], + [ + 275, + 225 + ], + [ + 75, + 375 + ], + [ + 75, + 75 + ] + ], + [ + [ + 175, + 225 + ], + [ + 375, + 75 + ], + [ + 375, + 375 + ], + [ + 175, + 225 + ] + ] + ], + [ + [] + ] + ], + "type": "MultiPolygon" + }, + "properties": { + "operation": "xor" + }, + "type": "Feature" + } + ], + "type": "FeatureCollection" +} diff --git a/tests/fixtures/generic_test_cases/daef_twointersectingcontours_empty.geojson b/tests/fixtures/generic_test_cases/daef_twointersectingcontours_empty.geojson new file mode 100644 index 0000000..1fc384d --- /dev/null +++ b/tests/fixtures/generic_test_cases/daef_twointersectingcontours_empty.geojson @@ -0,0 +1,64 @@ +{ + "features": [ + { + "geometry": { + "coordinates": [ + [ + [75, 75], + [275, 225], + [75, 375], + [75, 75] + ], + [ + [175, 225], + [375, 75], + [375, 375], + [175, 225] + ] + ], + "type": "Polygon" + }, + "properties": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + ], + "type": "Polygon" + }, + "properties": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + [75, 75], + [275, 225], + [75, 375], + [75, 75] + ], + [ + [175, 225], + [375, 75], + [375, 375], + [175, 225] + ] + ], + [ + [ + ] + ] + ], + "type": "MultiPolygon" + }, + "properties": { + "operation": "xor" + }, + "type": "Feature" + } + ], + "type": "FeatureCollection" +} diff --git a/tests/fixtures/generic_test_cases/issue90.geojson.deactivated b/tests/fixtures/generic_test_cases/issue90.geojson similarity index 100% rename from tests/fixtures/generic_test_cases/issue90.geojson.deactivated rename to tests/fixtures/generic_test_cases/issue90.geojson diff --git a/tests/fixtures/generic_test_cases/many_vertical1.geojson b/tests/fixtures/generic_test_cases/many_vertical1.geojson new file mode 100644 index 0000000..38eacd9 --- /dev/null +++ b/tests/fixtures/generic_test_cases/many_vertical1.geojson @@ -0,0 +1,183 @@ +{ + "features": [ + { + "geometry": { + "coordinates": [ + [ + [ + [-1, 0], + [0.5, -0.9595632051193486], + [0.5, -0.8579278836042261], + [-1, 0] + ] + ], + [ + [ + [-1, 0], + [0.5, -0.8257414005969186], + [0.5, -0.2331169623484446], + [-1, 0] + ] + ], + [ + [ + [-1, 0], + [0.5, -0.15269040132219058], + [0.5, -0.12482557747461498], + [-1, 0] + ] + ], + [ + [ + [-1, 0], + [0.5, 0.05778983950580896], + [0.5, 0.08976636599379373], + [-1, 0] + ] + ], + [ + [ + [-1, 0], + [0.5, 0.0976270078546495], + [0.5, 0.13608912218786462], + [-1, 0] + ] + ], + [ + [ + [-1, 0], + [0.5, 0.20552675214328775], + [0.5, 0.29178822613331223], + [-1, 0] + ] + ], + [ + [ + [-1, 0], + [0.5, 0.43037873274483895], + [0.5, 0.556313501899701], + [-1, 0] + ] + ], + [ + [ + [-1, 0], + [0.5, 0.5834500761653292], + [0.5, 0.665239691095876], + [-1, 0] + ] + ], + [ + [ + [-1, 0], + [0.5, 0.7400242964936383], + [0.5, 0.7835460015641595], + [-1, 0] + ] + ], + [ + [ + [-1, 0], + [0.5, 0.8511932765853221], + [0.5, 0.9273255210020586], + [-1, 0] + ] + ] + ], + "type": "MultiPolygon" + }, + "properties": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + [1, 0], + [-0.5, -0.9997712503653102], + [-0.5, -0.9452248136041477], + [1, 0] + ] + ], + [ + [ + [1, 0], + [-0.5, -0.8153228104624044], + [-0.5, -0.7192261228095325], + [1, 0] + ] + ], + [ + [ + [1, 0], + [-0.5, -0.7064882183657739], + [-0.5, -0.6274795772446582], + [1, 0] + ] + ], + [ + [ + [1, 0], + [-0.5, -0.6037970218302424], + [-0.5, -0.5910955005369651], + [1, 0] + ] + ], + [ + [ + [1, 0], + [-0.5, -0.39533485473632046], + [-0.5, -0.3088785459139045], + [1, 0] + ] + ], + [ + [ + [1, 0], + [-0.5, -0.20646505153866013], + [-0.5, -0.165955990594852], + [1, 0] + ] + ], + [ + [ + [1, 0], + [-0.5, -0.16539039526574606], + [-0.5, -0.1616109711934104], + [1, 0] + ] + ], + [ + [ + [1, 0], + [-0.5, 0.07763346800671389], + [-0.5, 0.11737965689150331], + [1, 0] + ] + ], + [ + [ + [1, 0], + [-0.5, 0.34093502035680445], + [-0.5, 0.370439000793519], + [1, 0] + ] + ], + [ + [ + [1, 0], + [-0.5, 0.4406489868843162], + [-0.5, 0.7562348727818908], + [1, 0] + ] + ] + ], + "type": "MultiPolygon" + }, + "properties": {}, + "type": "Feature" + } + ], + "type": "FeatureCollection" +} diff --git a/tests/fixtures/generic_test_cases/overlapping_segments4.geojson b/tests/fixtures/generic_test_cases/overlapping_segments4.geojson new file mode 100644 index 0000000..2f08836 --- /dev/null +++ b/tests/fixtures/generic_test_cases/overlapping_segments4.geojson @@ -0,0 +1,336 @@ +{ + "features": [ + { + "geometry": { + "coordinates": [ + [ + [ + [-1, 2], + [1, 2], + [1, 3], + [-1, 3], + [-1, 2] + ] + ], + [ + [ + [-1, 4], + [1, 4], + [1, 5], + [-1, 5], + [-1, 4] + ] + ], + [ + [ + [-1, 6], + [1, 6], + [1, 7], + [-1, 7], + [-1, 6] + ] + ], + [ + [ + [-1, 8], + [1, 8], + [1, 9], + [-1, 9], + [-1, 8] + ] + ], + [ + [ + [-1, 10], + [1, 10], + [1, 11], + [-1, 11], + [-1, 10] + ] + ], + [ + [ + [-1, 12], + [1, 12], + [1, 13], + [-1, 13], + [-1, 12] + ] + ], + [ + [ + [-1, 14], + [1, 14], + [1, 15], + [-1, 15], + [-1, 14] + ] + ], + [ + [ + [-1, 16], + [1, 16], + [1, 17], + [-1, 17], + [-1, 16] + ] + ], + [ + [ + [-1, 18], + [1, 18], + [1, 19], + [-1, 19], + [-1, 18] + ] + ], + [ + [ + [-1, 20], + [1, 20], + [1, 21], + [-1, 21], + [-1, 20] + ] + ] + ], + "type": "MultiPolygon" + }, + "properties": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + [1, 2], + [2, 2], + [2, 2.5], + [1, 2.5], + [1, 2] + ] + ], + [ + [ + [1, 4.5], + [2, 4.5], + [2, 5], + [1, 5], + [1, 4.5] + ] + ], + [ + [ + [1, 6.25], + [2, 6.25], + [2, 6.75], + [1, 6.75], + [1, 6.25] + ] + ], + [ + [ + [1, 8], + [2, 8], + [2, 9], + [1, 9], + [1, 8] + ] + ], + [ + [ + [1, 9.5], + [2, 9.5], + [2, 10.5], + [1, 10.5], + [1, 9.5] + ] + ], + [ + [ + [-1, 12], + [-2, 12], + [-2, 12.5], + [-1, 12.5], + [-1, 12] + ] + ], + [ + [ + [-1, 14.5], + [-2, 14.5], + [-2, 15], + [-1, 15], + [-1, 14.5] + ] + ], + [ + [ + [-1, 16.25], + [-2, 16.25], + [-2, 16.75], + [-1, 16.75], + [-1, 16.25] + ] + ], + [ + [ + [-1, 18], + [-2, 18], + [-2, 19], + [-1, 19], + [-1, 18] + ] + ], + [ + [ + [-1, 19.5], + [-2, 19.5], + [-2, 20.5], + [-1, 20.5], + [-1, 19.5] + ] + ] + ], + "type": "MultiPolygon" + }, + "properties": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + [-2, 12], + [-1, 12], + [1, 12], + [1, 13], + [-1, 13], + [-1, 12.5], + [-2, 12.5], + [-2, 12] + ] + ], + [ + [ + [-2, 14.5], + [-1, 14.5], + [-1, 14], + [1, 14], + [1, 15], + [-1, 15], + [-2, 15], + [-2, 14.5] + ] + ], + [ + [ + [-2, 16.25], + [-1, 16.25], + [-1, 16], + [1, 16], + [1, 17], + [-1, 17], + [-1, 16.75], + [-2, 16.75], + [-2, 16.25] + ] + ], + [ + [ + [-2, 18], + [-1, 18], + [1, 18], + [1, 19], + [-1, 19], + [-2, 19], + [-2, 18] + ] + ], + [ + [ + [-2, 19.5], + [-1, 19.5], + [-1, 20], + [1, 20], + [1, 21], + [-1, 21], + [-1, 20.5], + [-2, 20.5], + [-2, 19.5] + ] + ], + [ + [ + [-1, 2], + [1, 2], + [2, 2], + [2, 2.5], + [1, 2.5], + [1, 3], + [-1, 3], + [-1, 2] + ] + ], + [ + [ + [-1, 4], + [1, 4], + [1, 4.5], + [2, 4.5], + [2, 5], + [1, 5], + [-1, 5], + [-1, 4] + ] + ], + [ + [ + [-1, 6], + [1, 6], + [1, 6.25], + [2, 6.25], + [2, 6.75], + [1, 6.75], + [1, 7], + [-1, 7], + [-1, 6] + ] + ], + [ + [ + [-1, 8], + [1, 8], + [2, 8], + [2, 9], + [1, 9], + [-1, 9], + [-1, 8] + ] + ], + [ + [ + [-1, 10], + [1, 10], + [1, 9.5], + [2, 9.5], + [2, 10.5], + [1, 10.5], + [1, 11], + [-1, 11], + [-1, 10] + ] + ] + ], + "type": "MultiPolygon" + }, + "properties": { + "operation": "union" + }, + "type": "Feature" + } + ], + "type": "FeatureCollection" +} diff --git a/tests/fixtures/generic_test_cases/rust_issue12.geojson b/tests/fixtures/generic_test_cases/rust_issue12.geojson.deactivated similarity index 100% rename from tests/fixtures/generic_test_cases/rust_issue12.geojson rename to tests/fixtures/generic_test_cases/rust_issue12.geojson.deactivated diff --git a/tests/fixtures/generic_test_cases/self_overlaps1.geojson b/tests/fixtures/generic_test_cases/self_overlaps1.geojson new file mode 100644 index 0000000..d45ae28 --- /dev/null +++ b/tests/fixtures/generic_test_cases/self_overlaps1.geojson @@ -0,0 +1,242 @@ +{ + "features": [ + { + "geometry": { + "coordinates": [ + [ + [ + [0, 1], + [0, 2], + [1, 2], + [1, 1], + [0, 1] + ] + ], + [ + [ + [1, 1], + [1, 2], + [2, 2], + [2, 1], + [1, 1] + ] + ], + [ + [ + [3, 1], + [3, 2], + [4, 2], + [4, 1], + [3, 1] + ] + ], + [ + [ + [3, 2], + [3, 3], + [4, 3], + [4, 2], + [3, 2] + ] + ], + [ + [ + [5, 1], + [5, 2], + [6, 2], + [5, 1] + ] + ], + [ + [ + [6, 2], + [6, 1], + [5, 1], + [6, 2] + ] + ] + ], + "type": "MultiPolygon" + }, + "properties": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + [0, -1], + [0, -2], + [1, -2], + [1, -1], + [0, -1] + ] + ], + [ + [ + [1, -1], + [1, -2], + [2, -2], + [2, -1], + [1, -1] + ] + ], + [ + [ + [3, -1], + [3, -2], + [4, -2], + [4, -1], + [3, -1] + ] + ], + [ + [ + [3, -2], + [3, -3], + [4, -3], + [4, -2], + [3, -2] + ] + ], + [ + [ + [5, -1], + [5, -2], + [6, -2], + [5, -1] + ] + ], + [ + [ + [6, -2], + [6, -1], + [5, -1], + [6, -2] + ] + ] + ], + "type": "MultiPolygon" + }, + "properties": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + [0, 1], + [0, 2], + [1, 2], + [1, 1], + [0, 1] + ] + ], + [ + [ + [1, 1], + [1, 2], + [2, 2], + [2, 1], + [1, 1] + ] + ], + [ + [ + [3, 1], + [3, 2], + [4, 2], + [4, 1], + [3, 1] + ] + ], + [ + [ + [3, 2], + [3, 3], + [4, 3], + [4, 2], + [3, 2] + ] + ], + [ + [ + [5, 1], + [5, 2], + [6, 2], + [5, 1] + ] + ], + [ + [ + [6, 2], + [6, 1], + [5, 1], + [6, 2] + ] + ], + [ + [ + [0, -1], + [0, -2], + [1, -2], + [1, -1], + [0, -1] + ] + ], + [ + [ + [1, -1], + [1, -2], + [2, -2], + [2, -1], + [1, -1] + ] + ], + [ + [ + [3, -1], + [3, -2], + [4, -2], + [4, -1], + [3, -1] + ] + ], + [ + [ + [3, -2], + [3, -3], + [4, -3], + [4, -2], + [3, -2] + ] + ], + [ + [ + [5, -1], + [5, -2], + [6, -2], + [5, -1] + ] + ], + [ + [ + [6, -2], + [6, -1], + [5, -1], + [6, -2] + ] + ] + ], + "type": "MultiPolygon" + }, + "properties": { + "operation": "union" + }, + "type": "Feature" + } + ], + "type": "FeatureCollection" +} diff --git a/tests/fixtures/issue17-panic.geojson b/tests/fixtures/issue17-panic.geojson new file mode 100644 index 0000000..c5daddd --- /dev/null +++ b/tests/fixtures/issue17-panic.geojson @@ -0,0 +1,58 @@ +{ + "features": [ + { + "geometry": { + "coordinates": [ + [ + [ + [-7.3309603, 0.0], + [-1.7766104, 0.0], + [0.0, 0.0], + [0.0, 10.0], + [-5.016306, 10.0], + [-3.4043214688483463, 5.024271628631857], + [-7.3309603, 0.0] + ] + ] + ], + "type": "MultiPolygon" + }, + "properties": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [-7.3309603, 10.0], + [-7.3309603, 0.0], + [-2.5664105, 6.096408], + [-2.5664105, 8.428329] + ] + ], + "type": "Polygon" + }, + "properties": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + [0, 0], + [1, 1], + [1, 0] + ] + ] + ], + "type": "MultiPolygon" + }, + "properties": { + "operation": "union" + }, + "type": "Feature" + } + ], + "type": "FeatureCollection" +} diff --git a/tests/src/generic_test_cases.rs b/tests/src/generic_test_cases.rs index d1a3237..f9cfc62 100644 --- a/tests/src/generic_test_cases.rs +++ b/tests/src/generic_test_cases.rs @@ -4,6 +4,8 @@ use std::thread::Result; use geo::MultiPolygon; +use crate::helper::mp_is_equal; + use super::compact_geojson::write_compact_geojson; use super::helper::{apply_operation, convert_to_feature, extract_expected_result, load_test_case, TestOperation}; @@ -61,10 +63,10 @@ fn run_generic_test_case(filename: &str, regenerate: bool) -> Vec { Result::Err(_) => failures.push(format!("{} / {:?} / {:?} has panicked", filename, op, result_tag)), Result::Ok(result) => { let assertion_result = std::panic::catch_unwind(|| { - assert_eq!( - *result, expected_result.result, - "{} / {:?} / {:?} has result deviation", - filename, op, result_tag, + assert!( + mp_is_equal(result, &expected_result.result), + "{} / {:?} / {:?} has result deviation\nleft = {:?}\nright = {:?}", + filename, op, result_tag, result, expected_result.result, ) }); if assertion_result.is_err() { diff --git a/tests/src/helper.rs b/tests/src/helper.rs index 5ccfbea..67ef40d 100644 --- a/tests/src/helper.rs +++ b/tests/src/helper.rs @@ -1,11 +1,11 @@ use geo_booleanop::boolean::BooleanOp; -use geo::{Coordinate, MultiPolygon, Polygon}; +use geo::{Coordinate, LineString, MultiPolygon, Polygon}; use geojson::{Feature, GeoJson, Geometry, Value}; use serde_json::{json, Map}; -use std::convert::TryInto; +use std::{collections::BTreeSet, convert::TryInto}; use std::fs::File; use std::io::prelude::*; use std::iter::FromIterator; @@ -42,6 +42,56 @@ pub fn apply_operation(p1: &MultiPolygon, p2: &MultiPolygon, op: TestO } } +pub fn ring_is_equal(r1: &LineString, r2: &LineString) -> bool { + if r1.0.len() != r2.0.len() { + return false; + } + if r1.0.len() < 3 { + r1 == r2 + } else { + let l = r1.0.len() - 1; + (0..l).any(|shift| { + (0..l).all(|i| { + r1.0[i] == r2.0[(i + shift) % l] + }) + }) + } +} + +pub fn poly_is_equal(p1: &Polygon, p2: &Polygon) -> bool { + if !ring_is_equal(p1.exterior(), p2.exterior()) { return false; } + if p1.interiors().len() != p2.interiors().len() { return false; } + let mut matched_in_p2: BTreeSet = BTreeSet::new(); + for r1 in p1.interiors().iter() { + let did_match = p2.interiors().iter().enumerate().find(|(j, r2)| { + !matched_in_p2.contains(&j) && ring_is_equal(r1, r2) + }); + if let Some((j, _)) = did_match { + matched_in_p2.insert(j); + } else { + return false; + } + } + return true; +} + +pub fn mp_is_equal(p1: &MultiPolygon, p2: &MultiPolygon) -> bool { + // We want to look for any permutation of p2 match p1 + if p1.0.len() != p2.0.len() { return false; } + let mut matched_in_p2: BTreeSet = BTreeSet::new(); + for poly1 in p1.0.iter() { + let did_match = p2.0.iter().enumerate().find(|(j, poly2)| { + !matched_in_p2.contains(&j) && poly_is_equal(poly1, poly2) + }); + if let Some((j, _)) = did_match { + matched_in_p2.insert(j); + } else { + return false; + } + } + return true; +} + // ---------------------------------------------------------------------------- // Fixture loading // ----------------------------------------------------------------------------