diff --git a/.gitignore b/.gitignore index ea8c4bf..0f84cc9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/.vscode \ No newline at end of file diff --git a/examples/generate_single_fixture_output.rs b/examples/generate_single_fixture_output.rs new file mode 100644 index 0000000..250b10f --- /dev/null +++ b/examples/generate_single_fixture_output.rs @@ -0,0 +1,79 @@ +use std::fs; +use std::path::PathBuf; + +use geo::{Line, LinesIter}; +use geo_polygonizer::polygonize; +use geojson::{Feature, FeatureCollection, Geometry, GeometryValue}; + +fn fixture_path(kind: &str, name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("fixtures") + .join("polygonizer") + .join(kind) + .join(format!("{name}.geojson")) +} + +fn load_input_lines(name: &str) -> Vec> { + let path = fixture_path("input", name); + let text = fs::read_to_string(&path) + .unwrap_or_else(|err| panic!("failed to read fixture {}: {err}", path.display())); + let collection: FeatureCollection = serde_json::from_str(&text) + .unwrap_or_else(|err| panic!("failed to parse fixture {}: {err}", path.display())); + + collection + .features + .into_iter() + .flat_map(|feature| { + let geometry = feature + .geometry + .unwrap_or_else(|| panic!("input fixture {name} has a feature with no geometry")); + match geometry.value { + GeometryValue::LineString { .. } => { + let linestring: geo::LineString = geometry.try_into().unwrap(); + linestring.lines().collect::>() + } + GeometryValue::MultiLineString { .. } => { + let multiline: geo::MultiLineString = geometry.try_into().unwrap(); + multiline.lines_iter().collect::>() + } + other => { + panic!("input fixture {name} uses unsupported geometry type: {other:?}") + } + } + }) + .collect() +} + +fn main() { + let name = std::env::args() + .nth(1) + .unwrap_or_else(|| "minimal_secondary_probe_split".to_string()); + let input_lines = load_input_lines(&name); + let polygons = polygonize(input_lines); + + let features = polygons + .0 + .into_iter() + .map(|polygon| { + let geometry = Geometry::new(GeometryValue::from(&polygon)); + Feature { + bbox: None, + geometry: Some(geometry), + id: None, + properties: None, + foreign_members: None, + } + }) + .collect(); + + let collection = FeatureCollection { + bbox: None, + features, + foreign_members: None, + }; + + let output_path = fixture_path("output", &name); + let output_json = serde_json::to_string_pretty(&collection).expect("serialize output"); + fs::write(&output_path, output_json) + .unwrap_or_else(|err| panic!("failed to write fixture {}: {err}", output_path.display())); +} diff --git a/examples/update_very_complex_fixture.rs b/examples/update_very_complex_fixture.rs new file mode 100644 index 0000000..fd7c1c8 --- /dev/null +++ b/examples/update_very_complex_fixture.rs @@ -0,0 +1,77 @@ +use std::fs; +use std::path::PathBuf; + +use geo::{Line, LinesIter}; +use geo_polygonizer::polygonize; +use geojson::{Feature, FeatureCollection, Geometry, GeometryValue}; + +fn fixture_path(kind: &str, name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("fixtures") + .join("polygonizer") + .join(kind) + .join(format!("{name}.geojson")) +} + +fn load_input_lines(name: &str) -> Vec> { + let path = fixture_path("input", name); + let text = fs::read_to_string(&path) + .unwrap_or_else(|err| panic!("failed to read fixture {}: {err}", path.display())); + let collection: FeatureCollection = serde_json::from_str(&text) + .unwrap_or_else(|err| panic!("failed to parse fixture {}: {err}", path.display())); + + collection + .features + .into_iter() + .flat_map(|feature| { + let geometry = feature + .geometry + .unwrap_or_else(|| panic!("input fixture {name} has a feature with no geometry")); + match geometry.value { + GeometryValue::LineString { .. } => { + let linestring: geo::LineString = geometry.try_into().unwrap(); + linestring.lines().collect::>() + } + GeometryValue::MultiLineString { .. } => { + let multiline: geo::MultiLineString = geometry.try_into().unwrap(); + multiline.lines_iter().collect::>() + } + other => { + panic!("input fixture {name} uses unsupported geometry type: {other:?}") + } + } + }) + .collect() +} + +fn main() { + let name = "very_complex_linework"; + let input_lines = load_input_lines(name); + let polygons = polygonize(input_lines); + + let features = polygons + .0 + .into_iter() + .map(|polygon| { + let geometry = Geometry::new(GeometryValue::from(&polygon)); + Feature { + bbox: None, + geometry: Some(geometry), + id: None, + properties: None, + foreign_members: None, + } + }) + .collect(); + + let collection = FeatureCollection { + bbox: None, + features, + foreign_members: None, + }; + + let output_path = fixture_path("output", name); + let output_json = serde_json::to_string_pretty(&collection).expect("serialize output"); + fs::write(&output_path, output_json) + .unwrap_or_else(|err| panic!("failed to write fixture {}: {err}", output_path.display())); +} diff --git a/fixtures/nodify/input/collinear_contained.geojson b/fixtures/nodify/input/collinear_contained.geojson new file mode 100644 index 0000000..c04a29e --- /dev/null +++ b/fixtures/nodify/input/collinear_contained.geojson @@ -0,0 +1,7 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": { "name": "long segment" }, "geometry": { "type": "LineString", "coordinates": [[0, 0], [6, 0]] } }, + { "type": "Feature", "properties": { "name": "inner segment" }, "geometry": { "type": "LineString", "coordinates": [[1, 0], [3, 0]] } } + ] +} diff --git a/fixtures/nodify/input/collinear_overlap.geojson b/fixtures/nodify/input/collinear_overlap.geojson new file mode 100644 index 0000000..7b01cee --- /dev/null +++ b/fixtures/nodify/input/collinear_overlap.geojson @@ -0,0 +1,7 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": { "name": "left-long" }, "geometry": { "type": "LineString", "coordinates": [[0, 0], [3, 0]] } }, + { "type": "Feature", "properties": { "name": "right-long" }, "geometry": { "type": "LineString", "coordinates": [[1, 0], [4, 0]] } } + ] +} diff --git a/fixtures/nodify/input/duplicate_lines.geojson b/fixtures/nodify/input/duplicate_lines.geojson new file mode 100644 index 0000000..6988efe --- /dev/null +++ b/fixtures/nodify/input/duplicate_lines.geojson @@ -0,0 +1,7 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": { "name": "first copy" }, "geometry": { "type": "LineString", "coordinates": [[0, 0], [3, 0]] } }, + { "type": "Feature", "properties": { "name": "second copy" }, "geometry": { "type": "LineString", "coordinates": [[0, 0], [3, 0]] } } + ] +} diff --git a/fixtures/nodify/input/multi_segment_linestring.geojson b/fixtures/nodify/input/multi_segment_linestring.geojson new file mode 100644 index 0000000..0e7dbde --- /dev/null +++ b/fixtures/nodify/input/multi_segment_linestring.geojson @@ -0,0 +1,7 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": { "name": "L-shape" }, "geometry": { "type": "LineString", "coordinates": [[0, 0], [3, 0], [3, 3]] } }, + { "type": "Feature", "properties": { "name": "crossing" }, "geometry": { "type": "LineString", "coordinates": [[1, -1], [1, 1]] } } + ] +} diff --git a/fixtures/nodify/input/no_interaction.geojson b/fixtures/nodify/input/no_interaction.geojson new file mode 100644 index 0000000..04943ee --- /dev/null +++ b/fixtures/nodify/input/no_interaction.geojson @@ -0,0 +1,7 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": { "name": "lower" }, "geometry": { "type": "LineString", "coordinates": [[0, 0], [4, 0]] } }, + { "type": "Feature", "properties": { "name": "upper" }, "geometry": { "type": "LineString", "coordinates": [[0, 1], [4, 1]] } } + ] +} diff --git a/fixtures/nodify/input/reversed_duplicate.geojson b/fixtures/nodify/input/reversed_duplicate.geojson new file mode 100644 index 0000000..52675f7 --- /dev/null +++ b/fixtures/nodify/input/reversed_duplicate.geojson @@ -0,0 +1,7 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": { "name": "forward" }, "geometry": { "type": "LineString", "coordinates": [[0, 0], [3, 0]] } }, + { "type": "Feature", "properties": { "name": "backward" }, "geometry": { "type": "LineString", "coordinates": [[3, 0], [0, 0]] } } + ] +} diff --git a/fixtures/nodify/input/shared_endpoint.geojson b/fixtures/nodify/input/shared_endpoint.geojson new file mode 100644 index 0000000..92851e9 --- /dev/null +++ b/fixtures/nodify/input/shared_endpoint.geojson @@ -0,0 +1,7 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": { "name": "left segment" }, "geometry": { "type": "LineString", "coordinates": [[0, 0], [2, 0]] } }, + { "type": "Feature", "properties": { "name": "right segment" }, "geometry": { "type": "LineString", "coordinates": [[2, 0], [4, 0]] } } + ] +} diff --git a/fixtures/nodify/input/simple_crossing.geojson b/fixtures/nodify/input/simple_crossing.geojson new file mode 100644 index 0000000..e18388f --- /dev/null +++ b/fixtures/nodify/input/simple_crossing.geojson @@ -0,0 +1,7 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": { "name": "horizontal" }, "geometry": { "type": "LineString", "coordinates": [[0, 0], [4, 0]] } }, + { "type": "Feature", "properties": { "name": "vertical" }, "geometry": { "type": "LineString", "coordinates": [[2, -2], [2, 2]] } } + ] +} diff --git a/fixtures/nodify/input/star_crossings.geojson b/fixtures/nodify/input/star_crossings.geojson new file mode 100644 index 0000000..323f985 --- /dev/null +++ b/fixtures/nodify/input/star_crossings.geojson @@ -0,0 +1,8 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": { "name": "horizontal" }, "geometry": { "type": "LineString", "coordinates": [[0, 2], [4, 2]] } }, + { "type": "Feature", "properties": { "name": "vertical" }, "geometry": { "type": "LineString", "coordinates": [[2, 0], [2, 4]] } }, + { "type": "Feature", "properties": { "name": "diagonal" }, "geometry": { "type": "LineString", "coordinates": [[0, 0], [4, 4]] } } + ] +} diff --git a/fixtures/nodify/input/t_junction.geojson b/fixtures/nodify/input/t_junction.geojson new file mode 100644 index 0000000..dab6d3b --- /dev/null +++ b/fixtures/nodify/input/t_junction.geojson @@ -0,0 +1,7 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": { "name": "horizontal" }, "geometry": { "type": "LineString", "coordinates": [[0, 0], [4, 0]] } }, + { "type": "Feature", "properties": { "name": "vertical stem" }, "geometry": { "type": "LineString", "coordinates": [[2, 0], [2, 3]] } } + ] +} diff --git a/fixtures/nodify/output/collinear_contained.geojson b/fixtures/nodify/output/collinear_contained.geojson new file mode 100644 index 0000000..4fbc7be --- /dev/null +++ b/fixtures/nodify/output/collinear_contained.geojson @@ -0,0 +1,8 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[0, 0], [1, 0]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[1, 0], [3, 0]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[3, 0], [6, 0]] } } + ] +} diff --git a/fixtures/nodify/output/collinear_overlap.geojson b/fixtures/nodify/output/collinear_overlap.geojson new file mode 100644 index 0000000..a59264a --- /dev/null +++ b/fixtures/nodify/output/collinear_overlap.geojson @@ -0,0 +1,8 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[0, 0], [1, 0]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[1, 0], [3, 0]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[3, 0], [4, 0]] } } + ] +} diff --git a/fixtures/nodify/output/duplicate_lines.geojson b/fixtures/nodify/output/duplicate_lines.geojson new file mode 100644 index 0000000..2c0c20f --- /dev/null +++ b/fixtures/nodify/output/duplicate_lines.geojson @@ -0,0 +1,6 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[0, 0], [3, 0]] } } + ] +} diff --git a/fixtures/nodify/output/multi_segment_linestring.geojson b/fixtures/nodify/output/multi_segment_linestring.geojson new file mode 100644 index 0000000..27b7214 --- /dev/null +++ b/fixtures/nodify/output/multi_segment_linestring.geojson @@ -0,0 +1,10 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[0, 0], [1, 0]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[1, -1], [1, 0]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[1, 0], [1, 1]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[1, 0], [3, 0]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[3, 0], [3, 3]] } } + ] +} diff --git a/fixtures/nodify/output/no_interaction.geojson b/fixtures/nodify/output/no_interaction.geojson new file mode 100644 index 0000000..40c91c2 --- /dev/null +++ b/fixtures/nodify/output/no_interaction.geojson @@ -0,0 +1,7 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[0, 0], [4, 0]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[0, 1], [4, 1]] } } + ] +} diff --git a/fixtures/nodify/output/reversed_duplicate.geojson b/fixtures/nodify/output/reversed_duplicate.geojson new file mode 100644 index 0000000..2c0c20f --- /dev/null +++ b/fixtures/nodify/output/reversed_duplicate.geojson @@ -0,0 +1,6 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[0, 0], [3, 0]] } } + ] +} diff --git a/fixtures/nodify/output/shared_endpoint.geojson b/fixtures/nodify/output/shared_endpoint.geojson new file mode 100644 index 0000000..5ad59ad --- /dev/null +++ b/fixtures/nodify/output/shared_endpoint.geojson @@ -0,0 +1,7 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[0, 0], [2, 0]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[2, 0], [4, 0]] } } + ] +} diff --git a/fixtures/nodify/output/simple_crossing.geojson b/fixtures/nodify/output/simple_crossing.geojson new file mode 100644 index 0000000..e4e5c3a --- /dev/null +++ b/fixtures/nodify/output/simple_crossing.geojson @@ -0,0 +1,9 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[0, 0], [2, 0]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[2, -2], [2, 0]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[2, 0], [2, 2]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[2, 0], [4, 0]] } } + ] +} diff --git a/fixtures/nodify/output/star_crossings.geojson b/fixtures/nodify/output/star_crossings.geojson new file mode 100644 index 0000000..783d77f --- /dev/null +++ b/fixtures/nodify/output/star_crossings.geojson @@ -0,0 +1,11 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[0, 0], [2, 2]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[0, 2], [2, 2]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[2, 0], [2, 2]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[2, 2], [2, 4]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[2, 2], [4, 2]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[2, 2], [4, 4]] } } + ] +} diff --git a/fixtures/nodify/output/t_junction.geojson b/fixtures/nodify/output/t_junction.geojson new file mode 100644 index 0000000..8c2bda4 --- /dev/null +++ b/fixtures/nodify/output/t_junction.geojson @@ -0,0 +1,8 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[0, 0], [2, 0]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[2, 0], [2, 3]] } }, + { "type": "Feature", "properties": {}, "geometry": { "type": "LineString", "coordinates": [[2, 0], [4, 0]] } } + ] +} diff --git a/fixtures/polygonizer/input/buggy_outlines.geojson b/fixtures/polygonizer/input/buggy_outlines.geojson new file mode 100644 index 0000000..f66f955 --- /dev/null +++ b/fixtures/polygonizer/input/buggy_outlines.geojson @@ -0,0 +1 @@ +{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"LineString","coordinates":[[66.917152079,-49.483609955],[67.076757868,-49.04184243]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[66.917152079,-49.483609955],[68.49003423,-50.548027294]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[67.076757868,-49.04184243],[68.698823473,-48.144678001]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[67.96072344674927,-49.21812840419421],[68.03560388863696,-49.526937060777]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[67.96072344674927,-49.21812840419421],[68.03560388863696,-48.90931974761142]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.03560388863696,-49.526937060777],[68.25291539492534,-49.80551737445704]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.03560388863696,-48.90931974761142],[68.25291539492534,-48.63073943393138]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.25291539492534,-49.80551737445704],[68.5913860012806,-50.02659996314815]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.25291539492534,-48.63073943393138],[68.5913860012806,-48.40965684524027]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.49003423,-50.548027294],[69.861979006,-50.549689302]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.5913860012806,-50.02659996314815],[69.01788384648931,-50.16854372269627]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.5913860012806,-48.40965684524027],[69.01788384648931,-48.26771308569215]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.698823473,-48.144678001],[70.997703438,-47.154869247]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[69.01788384648931,-50.16854372269627],[69.49066034987723,-50.21745420893651]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[69.01788384648931,-48.26771308569215],[69.49066034987723,-48.21880259945191]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[69.49066034987723,-50.21745420893651],[69.96343685326515,-50.16854372269627]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[69.49066034987723,-48.21880259945191],[69.96343685326515,-48.26771308569215]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[69.861979006,-50.549689302],[70.26289097957036,-50.06888168459346]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[69.96343685326515,-50.16854372269627],[70.26289097957036,-50.06888168459346]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[69.96343685326515,-48.26771308569215],[70.38993469847387,-48.40965684524027]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.26289097957036,-50.06888168459346],[70.38993469847387,-50.02659996314815]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.26289097957036,-50.06888168459346],[71.00971073191234,-49.17323214198215]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.38993469847387,-50.02659996314815],[70.72840530482912,-49.80551737445704]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.38993469847387,-48.40965684524027],[70.72840530482912,-48.63073943393138]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.72840530482912,-49.80551737445704],[70.9457168111175,-49.526937060777]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.72840530482912,-48.63073943393138],[70.9457168111175,-48.90931974761142]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.9457168111175,-49.526937060777],[71.02059725300519,-49.21812840419421]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.9457168111175,-48.90931974761142],[71.00971073191234,-49.17323214198215]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.997703438,-47.154869247],[71.638389864,-47.695087312]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[71.00971073191234,-49.17323214198215],[71.02059725300519,-49.21812840419421]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[71.00971073191234,-49.17323214198215],[71.09668316,-49.068927435]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[71.09668316,-49.068927435],[71.638389864,-47.695087312]]},"properties":null}]} \ No newline at end of file diff --git a/fixtures/polygonizer/input/contained_island.geojson b/fixtures/polygonizer/input/contained_island.geojson new file mode 100644 index 0000000..aa2e5fb --- /dev/null +++ b/fixtures/polygonizer/input/contained_island.geojson @@ -0,0 +1,19 @@ +{ + "type": "FeatureCollection", + "features": [ + { "type": "Feature", "properties": { "name": "outer bottom" }, "geometry": { "type": "LineString", "coordinates": [[0, 0], [20, 0]] } }, + { "type": "Feature", "properties": { "name": "outer right" }, "geometry": { "type": "LineString", "coordinates": [[20, 0], [20, 20]] } }, + { "type": "Feature", "properties": { "name": "outer top" }, "geometry": { "type": "LineString", "coordinates": [[20, 20], [0, 20]] } }, + { "type": "Feature", "properties": { "name": "outer left" }, "geometry": { "type": "LineString", "coordinates": [[0, 20], [0, 0]] } }, + + { "type": "Feature", "properties": { "name": "hole bottom" }, "geometry": { "type": "LineString", "coordinates": [[3, 3], [10, 3]] } }, + { "type": "Feature", "properties": { "name": "hole right" }, "geometry": { "type": "LineString", "coordinates": [[10, 3], [10, 10]] } }, + { "type": "Feature", "properties": { "name": "hole top" }, "geometry": { "type": "LineString", "coordinates": [[10, 10], [3, 10]] } }, + { "type": "Feature", "properties": { "name": "hole left" }, "geometry": { "type": "LineString", "coordinates": [[3, 10], [3, 3]] } }, + + { "type": "Feature", "properties": { "name": "island bottom" }, "geometry": { "type": "LineString", "coordinates": [[13, 13], [18, 13]] } }, + { "type": "Feature", "properties": { "name": "island right" }, "geometry": { "type": "LineString", "coordinates": [[18, 13], [18, 18]] } }, + { "type": "Feature", "properties": { "name": "island top" }, "geometry": { "type": "LineString", "coordinates": [[18, 18], [13, 18]] } }, + { "type": "Feature", "properties": { "name": "island left" }, "geometry": { "type": "LineString", "coordinates": [[13, 18], [13, 13]] } } + ] +} diff --git a/fixtures/polygonizer/input/failed_hole.geojson b/fixtures/polygonizer/input/failed_hole.geojson new file mode 100644 index 0000000..d8ce77f --- /dev/null +++ b/fixtures/polygonizer/input/failed_hole.geojson @@ -0,0 +1 @@ +{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.3645294,37.23227291],[131.3666873,37.27146897]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.3645294,37.23227291],[131.3672068,37.19309722]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.3666873,37.27146897],[131.3736674,37.31030766]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.3672068,37.19309722],[131.3746863,37.15431884]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.3736674,37.31030766],[131.3854095,37.34841414]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.3746863,37.15431884],[131.3868887,37.11631032]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.3854095,37.34841414],[131.4018073,37.38542004]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.3868887,37.11631032],[131.4036901,37.07943623]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.4018073,37.38542004],[131.4227089,37.4209671]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.4036901,37.07943623],[131.4249229,37.04404979]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.4227089,37.4209671],[131.4479179,37.45471069]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.4249229,37.04404979],[131.4503779,37.01048946]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.4479179,37.45471069],[131.4771952,37.48632319]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.4503779,37.01048946],[131.4798068,36.9790758]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.4771952,37.48632319],[131.5102612,37.51549728]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.4798068,36.9790758],[131.5129239,36.9501085]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.5102612,37.51549728],[131.5467982,37.541949]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.5129239,36.9501085],[131.5494099,36.92386359]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.5467982,37.541949],[131.5864539,37.56542057]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.5494099,36.92386359],[131.588914,36.90059087]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.5864539,37.56542057],[131.628844,37.58568304]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.588914,36.90059087],[131.6310581,36.88051166]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.6159497978044,37.24040921335645],[131.6789834344638,37.426338419556394]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.6159497978044,37.24040921335645],[131.6924993976829,37.063692228175476]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.628844,37.58568304],[131.673557,37.60253859]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.6310581,36.88051166],[131.6754399,36.86381675]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.673557,37.60253859],[131.7201573,37.61582252]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.6754399,36.86381675],[131.7216367,36.85066465]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.6789834344638,37.426338419556394],[131.79570674559747,37.494760171748474]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.6924993976829,37.063692228175476],[131.88096982742374,36.990452460251525]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.7201573,37.61582252],[131.7681902,37.62540497]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.7216367,36.85066465],[131.7692093,36.84118017]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.7681902,37.62540497],[131.8171865,37.6311922]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.7692093,36.84118017],[131.817706,36.83545326]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.79570674559747,37.494760171748474],[131.99086117128556,37.481714926898256]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.8171865,37.6311922],[131.866667,37.63312759]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.817706,36.83545326],[131.866667,36.83353824]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.866667,36.83353824],[131.9156279,36.83545326]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.866667,37.63312759],[131.9161475,37.6311922]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.88096982742374,36.990452460251525],[132.04409187602525,37.05216214177735]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.9156279,36.83545326],[131.9641247,36.84118017]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.9161475,37.6311922],[131.9651438,37.62540497]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.9641247,36.84118017],[132.0116973,36.85066465]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.9651438,37.62540497],[132.0131767,37.61582252]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.99086117128556,37.481714926898256],[132.1246261181188,37.33971272991575]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.0116973,36.85066465],[132.0578941,36.86381675]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.0131767,37.61582252],[132.059777,37.60253859]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.04409187602525,37.05216214177735],[132.1340414978442,37.21699385927595]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.0578941,36.86381675],[132.1022759,36.88051166]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.059777,37.60253859],[132.1044899,37.58568304]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.1022759,36.88051166],[132.14442,36.90059087]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.1044899,37.58568304],[132.1468801,37.56542057]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.1246261181188,37.33971272991575],[132.1340414978442,37.21699385927595]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.14442,36.90059087],[132.1839241,36.92386359]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.1468801,37.56542057],[132.1865358,37.541949]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.1839241,36.92386359],[132.22041,36.9501085]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.1865358,37.541949],[132.2230728,37.51549728]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.1992415537936,37.140295005641654],[132.2802151948092,37.29316541576363]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.1992415537936,37.140295005641654],[132.32812844805989,37.0769105881086]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.22041,36.9501085],[132.2535272,36.9790758]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.2230728,37.51549728],[132.2561388,37.48632319]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.2535272,36.9790758],[132.282956,37.01048946]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.2561388,37.48632319],[132.2854161,37.45471069]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.2802151948092,37.29316541576363],[132.36806856583578,37.24564160289258]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.282956,37.01048946],[132.3084111,37.04404979]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.2854161,37.45471069],[132.3106251,37.4209671]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.3084111,37.04404979],[132.32812844805989,37.0769105881086]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.3106251,37.4209671],[132.3315267,37.38542004]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.32812844805989,37.0769105881086],[132.3296439,37.07943623]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.32812844805989,37.0769105881086],[132.37221729698305,37.05522843008466]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.3296439,37.07943623],[132.3464452,37.11631032]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.3315267,37.38542004],[132.3479245,37.34841414]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.3464452,37.11631032],[132.3586477,37.15431884]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.3479245,37.34841414],[132.3596666,37.31030766]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.3586477,37.15431884],[132.3661272,37.19309722]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.3596666,37.31030766],[132.3666466,37.27146897]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.3661272,37.19309722],[132.3688046,37.23227291]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.3666466,37.27146897],[132.36806856583578,37.24564160289258]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.36806856583578,37.24564160289258],[132.3688046,37.23227291]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.36806856583578,37.24564160289258],[132.46744946519559,37.19188203500189]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[132.37221729698305,37.05522843008466],[132.46744946519559,37.19188203500189]]},"properties":null}]} \ No newline at end of file diff --git a/fixtures/polygonizer/input/invalid_hole_assignment.geojson b/fixtures/polygonizer/input/invalid_hole_assignment.geojson new file mode 100644 index 0000000..f66f955 --- /dev/null +++ b/fixtures/polygonizer/input/invalid_hole_assignment.geojson @@ -0,0 +1 @@ +{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"LineString","coordinates":[[66.917152079,-49.483609955],[67.076757868,-49.04184243]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[66.917152079,-49.483609955],[68.49003423,-50.548027294]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[67.076757868,-49.04184243],[68.698823473,-48.144678001]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[67.96072344674927,-49.21812840419421],[68.03560388863696,-49.526937060777]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[67.96072344674927,-49.21812840419421],[68.03560388863696,-48.90931974761142]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.03560388863696,-49.526937060777],[68.25291539492534,-49.80551737445704]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.03560388863696,-48.90931974761142],[68.25291539492534,-48.63073943393138]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.25291539492534,-49.80551737445704],[68.5913860012806,-50.02659996314815]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.25291539492534,-48.63073943393138],[68.5913860012806,-48.40965684524027]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.49003423,-50.548027294],[69.861979006,-50.549689302]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.5913860012806,-50.02659996314815],[69.01788384648931,-50.16854372269627]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.5913860012806,-48.40965684524027],[69.01788384648931,-48.26771308569215]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[68.698823473,-48.144678001],[70.997703438,-47.154869247]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[69.01788384648931,-50.16854372269627],[69.49066034987723,-50.21745420893651]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[69.01788384648931,-48.26771308569215],[69.49066034987723,-48.21880259945191]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[69.49066034987723,-50.21745420893651],[69.96343685326515,-50.16854372269627]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[69.49066034987723,-48.21880259945191],[69.96343685326515,-48.26771308569215]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[69.861979006,-50.549689302],[70.26289097957036,-50.06888168459346]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[69.96343685326515,-50.16854372269627],[70.26289097957036,-50.06888168459346]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[69.96343685326515,-48.26771308569215],[70.38993469847387,-48.40965684524027]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.26289097957036,-50.06888168459346],[70.38993469847387,-50.02659996314815]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.26289097957036,-50.06888168459346],[71.00971073191234,-49.17323214198215]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.38993469847387,-50.02659996314815],[70.72840530482912,-49.80551737445704]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.38993469847387,-48.40965684524027],[70.72840530482912,-48.63073943393138]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.72840530482912,-49.80551737445704],[70.9457168111175,-49.526937060777]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.72840530482912,-48.63073943393138],[70.9457168111175,-48.90931974761142]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.9457168111175,-49.526937060777],[71.02059725300519,-49.21812840419421]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.9457168111175,-48.90931974761142],[71.00971073191234,-49.17323214198215]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[70.997703438,-47.154869247],[71.638389864,-47.695087312]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[71.00971073191234,-49.17323214198215],[71.02059725300519,-49.21812840419421]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[71.00971073191234,-49.17323214198215],[71.09668316,-49.068927435]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[71.09668316,-49.068927435],[71.638389864,-47.695087312]]},"properties":null}]} \ No newline at end of file diff --git a/fixtures/polygonizer/input/nested_shell_overlap_minimal.geojson b/fixtures/polygonizer/input/nested_shell_overlap_minimal.geojson new file mode 100644 index 0000000..a04e600 --- /dev/null +++ b/fixtures/polygonizer/input/nested_shell_overlap_minimal.geojson @@ -0,0 +1 @@ +{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.3645294,37.23227291],[131.4036901,37.07943623],[131.6754399,36.86381675],[131.866667,36.83353824],[132.0578941,36.86381675],[132.3296439,37.07943623],[132.3688046,37.23227291],[132.3315267,37.38542004],[132.059777,37.60253859],[131.866667,37.63312759],[131.673557,37.60253859],[131.4018073,37.38542004],[131.3645294,37.23227291]]},"properties":null},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[131.6159497978044,37.24040921335645],[131.6789834344638,37.426338419556394],[131.79570674559747,37.494760171748474],[131.99086117128556,37.481714926898256],[132.1246261181188,37.33971272991575],[132.1340414978442,37.21699385927595],[132.04409187602525,37.05216214177735],[131.88096982742374,36.990452460251525],[131.6924993976829,37.063692228175476],[131.6159497978044,37.24040921335645]]},"properties":null}]} diff --git a/fixtures/polygonizer/input/ownership_area_priority_enclosing_shell_minimal.geojson b/fixtures/polygonizer/input/ownership_area_priority_enclosing_shell_minimal.geojson new file mode 100644 index 0000000..9e256b1 --- /dev/null +++ b/fixtures/polygonizer/input/ownership_area_priority_enclosing_shell_minimal.geojson @@ -0,0 +1,1531 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3645294, + 37.23227291 + ], + [ + 131.3666873, + 37.27146897 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3645294, + 37.23227291 + ], + [ + 131.3672068, + 37.19309722 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3666873, + 37.27146897 + ], + [ + 131.3736674, + 37.31030766 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3672068, + 37.19309722 + ], + [ + 131.3746863, + 37.15431884 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3736674, + 37.31030766 + ], + [ + 131.3854095, + 37.34841414 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3746863, + 37.15431884 + ], + [ + 131.3868887, + 37.11631032 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3854095, + 37.34841414 + ], + [ + 131.4018073, + 37.38542004 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3868887, + 37.11631032 + ], + [ + 131.4036901, + 37.07943623 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4018073, + 37.38542004 + ], + [ + 131.4227089, + 37.4209671 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4036901, + 37.07943623 + ], + [ + 131.4249229, + 37.04404979 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4227089, + 37.4209671 + ], + [ + 131.4479179, + 37.45471069 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4249229, + 37.04404979 + ], + [ + 131.4503779, + 37.01048946 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4479179, + 37.45471069 + ], + [ + 131.4771952, + 37.48632319 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4503779, + 37.01048946 + ], + [ + 131.4798068, + 36.9790758 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4771952, + 37.48632319 + ], + [ + 131.5102612, + 37.51549728 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4798068, + 36.9790758 + ], + [ + 131.5129239, + 36.9501085 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.5102612, + 37.51549728 + ], + [ + 131.5467982, + 37.541949 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.5129239, + 36.9501085 + ], + [ + 131.5494099, + 36.92386359 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.5467982, + 37.541949 + ], + [ + 131.5864539, + 37.56542057 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.5494099, + 36.92386359 + ], + [ + 131.588914, + 36.90059087 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.5864539, + 37.56542057 + ], + [ + 131.628844, + 37.58568304 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.588914, + 36.90059087 + ], + [ + 131.6310581, + 36.88051166 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.6159497978044, + 37.24040921335645 + ], + [ + 131.6789834344638, + 37.426338419556394 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.6159497978044, + 37.24040921335645 + ], + [ + 131.6924993976829, + 37.063692228175476 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.628844, + 37.58568304 + ], + [ + 131.673557, + 37.60253859 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.6310581, + 36.88051166 + ], + [ + 131.6754399, + 36.86381675 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.673557, + 37.60253859 + ], + [ + 131.7201573, + 37.61582252 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.6754399, + 36.86381675 + ], + [ + 131.7216367, + 36.85066465 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.6789834344638, + 37.426338419556394 + ], + [ + 131.79570674559747, + 37.494760171748474 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.6924993976829, + 37.063692228175476 + ], + [ + 131.88096982742374, + 36.990452460251525 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.7201573, + 37.61582252 + ], + [ + 131.7681902, + 37.62540497 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.7216367, + 36.85066465 + ], + [ + 131.7692093, + 36.84118017 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.7681902, + 37.62540497 + ], + [ + 131.8171865, + 37.6311922 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.7692093, + 36.84118017 + ], + [ + 131.817706, + 36.83545326 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.79570674559747, + 37.494760171748474 + ], + [ + 131.99086117128556, + 37.481714926898256 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.8171865, + 37.6311922 + ], + [ + 131.866667, + 37.63312759 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.817706, + 36.83545326 + ], + [ + 131.866667, + 36.83353824 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.866667, + 36.83353824 + ], + [ + 131.9156279, + 36.83545326 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.866667, + 37.63312759 + ], + [ + 131.9161475, + 37.6311922 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.88096982742374, + 36.990452460251525 + ], + [ + 132.04409187602525, + 37.05216214177735 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.9156279, + 36.83545326 + ], + [ + 131.9641247, + 36.84118017 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.9161475, + 37.6311922 + ], + [ + 131.9651438, + 37.62540497 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.9641247, + 36.84118017 + ], + [ + 132.0116973, + 36.85066465 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.9651438, + 37.62540497 + ], + [ + 132.0131767, + 37.61582252 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.99086117128556, + 37.481714926898256 + ], + [ + 132.1246261181188, + 37.33971272991575 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.0116973, + 36.85066465 + ], + [ + 132.0578941, + 36.86381675 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.0131767, + 37.61582252 + ], + [ + 132.059777, + 37.60253859 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.04409187602525, + 37.05216214177735 + ], + [ + 132.1340414978442, + 37.21699385927595 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.0578941, + 36.86381675 + ], + [ + 132.1022759, + 36.88051166 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.059777, + 37.60253859 + ], + [ + 132.1044899, + 37.58568304 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1022759, + 36.88051166 + ], + [ + 132.14442, + 36.90059087 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1044899, + 37.58568304 + ], + [ + 132.1468801, + 37.56542057 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1246261181188, + 37.33971272991575 + ], + [ + 132.1340414978442, + 37.21699385927595 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.14442, + 36.90059087 + ], + [ + 132.1839241, + 36.92386359 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1468801, + 37.56542057 + ], + [ + 132.1865358, + 37.541949 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1839241, + 36.92386359 + ], + [ + 132.22041, + 36.9501085 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1865358, + 37.541949 + ], + [ + 132.2230728, + 37.51549728 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1992415537936, + 37.140295005641654 + ], + [ + 132.2802151948092, + 37.29316541576363 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1992415537936, + 37.140295005641654 + ], + [ + 132.32812844805989, + 37.0769105881086 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.22041, + 36.9501085 + ], + [ + 132.2535272, + 36.9790758 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.2230728, + 37.51549728 + ], + [ + 132.2561388, + 37.48632319 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.2535272, + 36.9790758 + ], + [ + 132.282956, + 37.01048946 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.2561388, + 37.48632319 + ], + [ + 132.2854161, + 37.45471069 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.2802151948092, + 37.29316541576363 + ], + [ + 132.36806856583578, + 37.24564160289258 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.282956, + 37.01048946 + ], + [ + 132.3084111, + 37.04404979 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.2854161, + 37.45471069 + ], + [ + 132.3106251, + 37.4209671 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3084111, + 37.04404979 + ], + [ + 132.32812844805989, + 37.0769105881086 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3106251, + 37.4209671 + ], + [ + 132.3315267, + 37.38542004 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.32812844805989, + 37.0769105881086 + ], + [ + 132.3296439, + 37.07943623 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.32812844805989, + 37.0769105881086 + ], + [ + 132.37221729698305, + 37.05522843008466 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3296439, + 37.07943623 + ], + [ + 132.3464452, + 37.11631032 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3315267, + 37.38542004 + ], + [ + 132.3479245, + 37.34841414 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3464452, + 37.11631032 + ], + [ + 132.3586477, + 37.15431884 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3479245, + 37.34841414 + ], + [ + 132.3596666, + 37.31030766 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3586477, + 37.15431884 + ], + [ + 132.3661272, + 37.19309722 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3596666, + 37.31030766 + ], + [ + 132.3666466, + 37.27146897 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3661272, + 37.19309722 + ], + [ + 132.3688046, + 37.23227291 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3666466, + 37.27146897 + ], + [ + 132.36806856583578, + 37.24564160289258 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.36806856583578, + 37.24564160289258 + ], + [ + 132.3688046, + 37.23227291 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.36806856583578, + 37.24564160289258 + ], + [ + 132.46744946519559, + 37.19188203500189 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.37221729698305, + 37.05522843008466 + ], + [ + 132.46744946519559, + 37.19188203500189 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.5, + 36.2 + ], + [ + 130.8, + 36.2 + ], + [ + 131.1, + 36.2 + ], + [ + 131.4, + 36.2 + ], + [ + 131.7, + 36.2 + ], + [ + 132.0, + 36.2 + ], + [ + 132.3, + 36.2 + ], + [ + 132.6, + 36.2 + ], + [ + 132.9, + 36.2 + ], + [ + 133.2, + 36.2 + ], + [ + 133.2, + 36.45 + ], + [ + 133.2, + 36.7 + ], + [ + 133.2, + 36.95 + ], + [ + 133.2, + 37.2 + ], + [ + 133.2, + 37.45 + ], + [ + 133.2, + 37.7 + ], + [ + 133.2, + 37.95 + ], + [ + 133.2, + 38.2 + ], + [ + 132.9, + 38.2 + ], + [ + 132.6, + 38.2 + ], + [ + 132.3, + 38.2 + ], + [ + 132.0, + 38.2 + ], + [ + 131.7, + 38.2 + ], + [ + 131.4, + 38.2 + ], + [ + 131.1, + 38.2 + ], + [ + 130.8, + 38.2 + ], + [ + 130.5, + 38.2 + ], + [ + 130.5, + 37.95 + ], + [ + 130.5, + 37.7 + ], + [ + 130.5, + 37.45 + ], + [ + 130.5, + 37.2 + ], + [ + 130.5, + 36.95 + ], + [ + 130.5, + 36.7 + ], + [ + 130.5, + 36.45 + ], + [ + 130.5, + 36.2 + ] + ] + }, + "properties": null + } + ] +} diff --git a/fixtures/polygonizer/input/split_touching_hole_drop_minimal.geojson b/fixtures/polygonizer/input/split_touching_hole_drop_minimal.geojson new file mode 100644 index 0000000..0b3be7d --- /dev/null +++ b/fixtures/polygonizer/input/split_touching_hole_drop_minimal.geojson @@ -0,0 +1,47 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [0.0, 0.0], + [10.0, 0.0], + [10.0, 10.0], + [0.0, 10.0], + [0.0, 0.0] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [3.0, 3.0], + [7.0, 3.0], + [7.0, 7.0], + [3.0, 7.0], + [3.0, 3.0] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [7.0, 7.0], + [8.0, 7.0], + [8.0, 8.0], + [7.0, 8.0], + [7.0, 7.0] + ] + }, + "properties": null + } + ] +} diff --git a/fixtures/polygonizer/input/split_touching_hole_priority_minimal.geojson b/fixtures/polygonizer/input/split_touching_hole_priority_minimal.geojson new file mode 100644 index 0000000..6683795 --- /dev/null +++ b/fixtures/polygonizer/input/split_touching_hole_priority_minimal.geojson @@ -0,0 +1,5088 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 125.90951698705308, + 40.43259517121252 + ], + [ + 131.5657278864061, + 43.62650386261877 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 127.25000000493503, + 40.000000020662945 + ], + [ + 130.00000000493503, + 42.29999997297923 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 128.04153269216172, + 34.08855120110449 + ], + [ + 128.52127569600694, + 34.117076315163935 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 128.17710560247056, + 39.370860733270014 + ], + [ + 128.6598588792955, + 38.993371166467035 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 128.20969384595506, + 39.57254783081945 + ], + [ + 128.8976213304674, + 39.906483568429316 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 128.3500000287769, + 38.6000000445048 + ], + [ + 128.75000000493503, + 38.73333336989085 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 128.52127569600694, + 34.117076315163935 + ], + [ + 128.80560010358445, + 34.20564214157995 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 128.6598588792955, + 38.993371166467035 + ], + [ + 128.81053360646507, + 38.7464928466837 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 128.75000000493503, + 38.73333336989085 + ], + [ + 128.81053360646507, + 38.7464928466837 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 128.80560010358445, + 34.20564214157995 + ], + [ + 128.82334607526414, + 33.96274868416723 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 128.81053360646507, + 38.7464928466837 + ], + [ + 129.0409852831041, + 38.36890117096838 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 128.81053360646507, + 38.7464928466837 + ], + [ + 130.66666675107456, + 39.14999999682109 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 128.8976213304674, + 39.906483568429316 + ], + [ + 129.524619993416, + 40.352669395684565 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.0409852831041, + 38.36890117096838 + ], + [ + 129.6859936086809, + 37.53290168213781 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.524619993416, + 40.352669395684565 + ], + [ + 129.65171402379625, + 40.38446966576513 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.65171402379625, + 40.38446966576513 + ], + [ + 129.92245787068956, + 40.60253850388464 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.6859936086809, + 37.53290168213781 + ], + [ + 129.8942009775316, + 36.980742372750605 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.72433465406053, + 34.99017754959997 + ], + [ + 129.81864708348863, + 34.854704775094355 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.72433465406053, + 34.99017754959997 + ], + [ + 129.82541793271653, + 35.33559600281652 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.8173844186937, + 34.852948107003535 + ], + [ + 129.81864708348863, + 34.854704775094355 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.8173844186937, + 34.852948107003535 + ], + [ + 129.91005605146043, + 34.53202358651098 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.82541793271653, + 35.33559600281652 + ], + [ + 129.85676234647386, + 35.33519641327795 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.82918017789476, + 35.35807148384985 + ], + [ + 129.84717362805955, + 35.434294857262934 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.82918017789476, + 35.35807148384985 + ], + [ + 129.85632413312547, + 35.356952347039545 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.84717362805955, + 35.434294857262934 + ], + [ + 130.01083820745103, + 36.019300617455805 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.85632413312547, + 35.356952347039545 + ], + [ + 129.85676234647386, + 35.33519641327795 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.8942009775316, + 36.980742372750605 + ], + [ + 129.89674132749192, + 36.28144828247961 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.89674132749192, + 36.28144828247961 + ], + [ + 129.99217861577623, + 36.127263941048945 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.91005605146043, + 34.53202358651098 + ], + [ + 130.07497304364793, + 34.60854283738073 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.92245787068956, + 40.60253850388464 + ], + [ + 130.08875554486863, + 40.714631475686396 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 129.99217861577623, + 36.127263941048945 + ], + [ + 130.01083820745103, + 36.019300617455805 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.00000000493503, + 42.29999997297923 + ], + [ + 130.70000005261875, + 42.29999997297923 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.07497304364793, + 34.60854283738073 + ], + [ + 130.16649454518907, + 34.58200446534094 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.08875554486863, + 40.714631475686396 + ], + [ + 130.1707333891069, + 40.92548099923071 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.13608210965745, + 41.1895157473081 + ], + [ + 130.1707333891069, + 40.92548099923071 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.13608210965745, + 41.1895157473081 + ], + [ + 130.20811861440293, + 41.42399350571569 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.16649454518907, + 34.58200446534094 + ], + [ + 130.47096293851487, + 34.46426955628332 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.1946455804979, + 41.545595802544916 + ], + [ + 130.20811861440293, + 41.42399350571569 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.1946455804979, + 41.545595802544916 + ], + [ + 130.3428277341997, + 41.723245538949335 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.3428277341997, + 41.723245538949335 + ], + [ + 130.55572026654832, + 41.906751789330805 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.47096293851487, + 34.46426955628332 + ], + [ + 130.538611349312, + 34.59332148003515 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.538611349312, + 34.59332148003515 + ], + [ + 130.72174232884996, + 34.93990389275488 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.55572026654832, + 41.906751789330805 + ], + [ + 130.64999788686387, + 41.898079790353144 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.64999788686387, + 41.898079790353144 + ], + [ + 130.95063237021225, + 42.01540194237351 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.66666675107456, + 39.14999999682109 + ], + [ + 130.71666670339084, + 39.29999997297923 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.70000005261875, + 42.29999997297923 + ], + [ + 130.88333333032108, + 42.14999999682109 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.71666663571946, + 39.30000010895666 + ], + [ + 130.7166667520986, + 39.30000012568466 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.71666663571946, + 39.30000010895666 + ], + [ + 131.33333319112413, + 41.2333332674497 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.71666670339084, + 39.29999997297923 + ], + [ + 130.7166667520986, + 39.30000012568466 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.7166667520986, + 39.30000012568466 + ], + [ + 131.3333332587955, + 41.23333336989085 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.7166667520986, + 39.30000012568466 + ], + [ + 138.83333319112413, + 40.46666661667761 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.72174232884996, + 34.93990389275488 + ], + [ + 130.93242209836595, + 35.135094799279535 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.88333333032108, + 42.14999999682109 + ], + [ + 130.95063237021225, + 42.01540194237351 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.93242209836595, + 35.135094799279535 + ], + [ + 131.15051144048326, + 35.14206019806799 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.95063237021225, + 42.01540194237351 + ], + [ + 131.20000005261875, + 41.51666667143504 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 130.95063237021225, + 42.01540194237351 + ], + [ + 131.24879282399766, + 42.13175860810217 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.15051144048326, + 35.14206019806799 + ], + [ + 131.4794425336992, + 35.02358976769384 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.19999998494737, + 41.51666656899389 + ], + [ + 131.20000009032384, + 41.516666591311626 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.19999998494737, + 41.51666656899389 + ], + [ + 131.33333319112413, + 41.2333332674497 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.20000005261875, + 41.51666667143504 + ], + [ + 131.20000009032384, + 41.516666591311626 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.20000009032384, + 41.516666591311626 + ], + [ + 131.3333332587955, + 41.23333336989085 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.20000009032384, + 41.516666591311626 + ], + [ + 131.7847494451677, + 41.640510954140986 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.24879282399766, + 42.13175860810217 + ], + [ + 131.54680102750413, + 41.63907710480627 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4794425336992, + 35.02358976769384 + ], + [ + 131.75572007581346, + 35.13236252236303 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.54680102750413, + 41.63907710480627 + ], + [ + 131.7847494451677, + 41.640510954140986 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.5657278864061, + 43.62650386261877 + ], + [ + 138.98936479970567, + 47.93827024865087 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.75572007581346, + 35.13236252236303 + ], + [ + 132.2549039690172, + 35.52300898003515 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.2549039690172, + 35.52300898003515 + ], + [ + 132.33140629216783, + 35.651364721536005 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.33140629216783, + 35.651364721536005 + ], + [ + 132.54063599988572, + 35.80912128853735 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.49800151273362, + 36.18276206421789 + ], + [ + 132.54063599988572, + 35.80912128853735 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.49800151273362, + 36.18276206421789 + ], + [ + 132.74247520848863, + 36.39071170258459 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.74247520848863, + 36.39071170258459 + ], + [ + 133.07532280370347, + 36.6651131288999 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.79428833409898, + 41.85432139801916 + ], + [ + 132.87195676252, + 42.14065901207861 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.79428833409898, + 41.85432139801916 + ], + [ + 138.83333319112413, + 43.13333336281713 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.87195676252, + 42.14065901207861 + ], + [ + 133.02050417348497, + 42.13700334000524 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 133.02050417348497, + 42.13700334000524 + ], + [ + 134.29323309346788, + 42.59795466828283 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 133.07532280370347, + 36.6651131288999 + ], + [ + 133.29041117116563, + 36.695769466637934 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 133.29041117116563, + 36.695769466637934 + ], + [ + 133.58193343564622, + 36.640718139886225 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 133.58193343564622, + 36.640718139886225 + ], + [ + 133.79708975240342, + 36.400268472909296 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 133.66149013921373, + 35.92121640610632 + ], + [ + 133.80231684133165, + 36.14261380600866 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 133.66149013921373, + 35.92121640610632 + ], + [ + 133.86180251523606, + 35.91874019074377 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 133.79708975240342, + 36.400268472909296 + ], + [ + 133.80231684133165, + 36.14261380600866 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 133.86180251523606, + 35.91874019074377 + ], + [ + 134.44879120275132, + 36.085333027123774 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 134.29323309346788, + 42.59795466828283 + ], + [ + 135.03674643918626, + 42.94185701775488 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 134.44879120275132, + 36.085333027123774 + ], + [ + 135.13928526326768, + 36.11452404427465 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 135.03674643918626, + 42.94185701775488 + ], + [ + 135.55498760625474, + 43.348386205911005 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 135.13928526326768, + 36.11452404427465 + ], + [ + 135.26768606587999, + 36.10011307167944 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 135.26768606587999, + 36.10011307167944 + ], + [ + 135.5427400438463, + 36.0337859766477 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 135.5427400438463, + 36.0337859766477 + ], + [ + 135.5792080728685, + 36.12769261765417 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 135.55498760625474, + 43.348386205911005 + ], + [ + 136.51891320630662, + 44.21457020211157 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 135.5792080728685, + 36.12769261765417 + ], + [ + 135.7967533437883, + 36.46571508812841 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 135.7967533437883, + 36.46571508812841 + ], + [ + 136.24264758512132, + 36.91704694199499 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 136.22520512029283, + 37.27649609017309 + ], + [ + 136.24264758512132, + 36.91704694199499 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 136.22520512029283, + 37.27649609017309 + ], + [ + 136.4591709940111, + 37.613666214227045 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 136.4591709940111, + 37.613666214227045 + ], + [ + 136.53361028119676, + 37.931401886224116 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 136.51891320630662, + 44.21457020211157 + ], + [ + 138.20392030164354, + 45.515768446206415 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 136.53361028119676, + 37.931401886224116 + ], + [ + 136.67150610372178, + 38.15095535683569 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 136.67150610372178, + 38.15095535683569 + ], + [ + 137.07090657636277, + 38.17601887154516 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 136.98123782559983, + 33.820195831536616 + ], + [ + 137.14632242604844, + 34.126605905770624 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 137.07090657636277, + 38.17601887154516 + ], + [ + 137.28481930181138, + 38.005623497247065 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 137.14632242604844, + 34.126605905770624 + ], + [ + 137.60926574155442, + 34.31641379761633 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 137.28481930181138, + 38.005623497247065 + ], + [ + 137.59269922658555, + 37.80573479103979 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 137.59269922658555, + 37.80573479103979 + ], + [ + 137.78812735959642, + 37.82504764962133 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 137.60926574155442, + 34.31641379761633 + ], + [ + 138.3766569463884, + 34.222200073479975 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 137.75957888051622, + 38.13473073410925 + ], + [ + 137.78812735959642, + 37.82504764962133 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 137.75957888051622, + 38.13473073410925 + ], + [ + 137.99558108731858, + 38.469143308877314 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 137.99558108731858, + 38.469143308877314 + ], + [ + 138.18207519933335, + 38.58265987801489 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.18207519933335, + 38.58265987801489 + ], + [ + 138.41489642545335, + 38.7083932535642 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.20392030164354, + 45.515768446206415 + ], + [ + 139.8047358362352, + 46.931316770791376 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.3766569463884, + 34.222200073479975 + ], + [ + 138.4017750589525, + 33.916477360009516 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.41489642545335, + 38.7083932535642 + ], + [ + 138.63589852735154, + 38.6779452936643 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.63589852735154, + 38.6779452936643 + ], + [ + 138.80585139676683, + 38.593224682092035 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.64769495027042, + 33.99694361368815 + ], + [ + 138.65609503285862, + 34.011734506289166 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.65609503285862, + 34.011734506289166 + ], + [ + 138.6662064840076, + 34.02577080408732 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.6662064840076, + 34.02577080408732 + ], + [ + 138.67793286340213, + 34.038916966120404 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.67793286340213, + 34.038916966120404 + ], + [ + 138.691161279845, + 34.05104603449504 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.691161279845, + 34.05104603449504 + ], + [ + 138.70576465623355, + 34.062040588061016 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.70576465623355, + 34.062040588061016 + ], + [ + 138.72160208718753, + 34.07179453055064 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.72160208718753, + 34.07179453055064 + ], + [ + 138.73852086560703, + 34.080213209788006 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.73852086560703, + 34.080213209788006 + ], + [ + 138.75635755555606, + 34.08721532503764 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.75635755555606, + 34.08721532503764 + ], + [ + 138.77493942277408, + 34.0927329270045 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.77493942277408, + 34.0927329270045 + ], + [ + 138.79408681886173, + 34.09671272913615 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.79408681886173, + 34.09671272913615 + ], + [ + 138.81361437337375, + 34.09911622683207 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.80585139676683, + 38.593224682092035 + ], + [ + 138.89083927556626, + 38.72250143456396 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.81361437337375, + 34.09911622683207 + ], + [ + 138.83333302037693, + 34.099919935862225 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.83333302037693, + 34.099919935862225 + ], + [ + 138.8530516673801, + 34.09911622683207 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.83333319112413, + 40.46666661667761 + ], + [ + 138.83333319112413, + 43.13333336281713 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.8530516673801, + 34.09911622683207 + ], + [ + 138.87257922189212, + 34.09671272913615 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.87257922189212, + 34.09671272913615 + ], + [ + 138.89172673718906, + 34.0927329270045 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.89083927556626, + 38.72250143456396 + ], + [ + 139.2462448446428, + 38.86402050423559 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.89172673718906, + 34.0927329270045 + ], + [ + 138.91030860440708, + 34.08721532503764 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.91030860440708, + 34.08721532503764 + ], + [ + 138.92814517514682, + 34.080213209788006 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.92814517514682, + 34.080213209788006 + ], + [ + 138.94506395356632, + 34.07179453055064 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.94506395356632, + 34.07179453055064 + ], + [ + 138.9609013845203, + 34.062040588061016 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.9609013845203, + 34.062040588061016 + ], + [ + 138.97550476090885, + 34.05104603449504 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.97550476090885, + 34.05104603449504 + ], + [ + 138.98873317735172, + 34.038916966120404 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.98873317735172, + 34.038916966120404 + ], + [ + 139.00045955674625, + 34.02577080408732 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 138.98936479970567, + 47.93827024865087 + ], + [ + 139.91712563916795, + 52.984622396706904 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.00045955674625, + 34.02577080408732 + ], + [ + 139.01057112710453, + 34.011734506289166 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.01057112710453, + 34.011734506289166 + ], + [ + 139.01897109048343, + 33.99694361368815 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.0326718656694, + 42.200830139397944 + ], + [ + 139.1071223585283, + 41.359325088738764 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.0326718656694, + 42.200830139397944 + ], + [ + 139.4952282278215, + 43.59813109803137 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.08606141492479, + 39.15089575219091 + ], + [ + 139.1337110368883, + 39.32569900917944 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.08606141492479, + 39.15089575219091 + ], + [ + 139.2462448446428, + 38.86402050423559 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.1071223585283, + 41.359325088738764 + ], + [ + 139.2972156374132, + 41.24540988373693 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.1337110368883, + 39.32569900917944 + ], + [ + 139.23663419171922, + 39.451707996606196 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.23663419171922, + 39.451707996606196 + ], + [ + 139.4365171758806, + 39.546686805962885 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.26861279889695, + 39.938996710061396 + ], + [ + 139.26884001180284, + 40.07221619057592 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.26861279889695, + 39.938996710061396 + ], + [ + 139.36650222226731, + 39.72797123360571 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.26884001180284, + 40.07221619057592 + ], + [ + 139.48251074239366, + 40.486479677438105 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.2972156374132, + 41.24540988373693 + ], + [ + 139.4322516290819, + 41.26847974228796 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.3663107721483, + 47.49393431115087 + ], + [ + 139.8047358362352, + 46.931316770791376 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.3663107721483, + 47.49393431115087 + ], + [ + 139.8993086187517, + 46.937296070336664 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.36650222226731, + 39.72797123360571 + ], + [ + 139.5814499227678, + 39.54912487435278 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.40540665074937, + 40.512441076516474 + ], + [ + 139.4383768407976, + 40.5836807863706 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.40540665074937, + 40.512441076516474 + ], + [ + 139.48251074239366, + 40.486479677438105 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.4322516290819, + 41.26847974228796 + ], + [ + 139.59843319341294, + 41.21928469109472 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.4365171758806, + 39.546686805962885 + ], + [ + 139.5814499227678, + 39.54912487435278 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.4383768407976, + 40.5836807863706 + ], + [ + 139.4782778589403, + 40.76618400978979 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.4782778589403, + 40.76618400978979 + ], + [ + 139.95911353513353, + 41.0299002306455 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.4952282278215, + 43.59813109803137 + ], + [ + 139.85076373502366, + 43.93735281395849 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.59843319341294, + 41.21928469109472 + ], + [ + 139.60985058232896, + 41.17869059014257 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.60985058232896, + 41.17869059014257 + ], + [ + 139.74605959340684, + 41.109813369988764 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.74605959340684, + 41.109813369988764 + ], + [ + 139.92528456136338, + 41.15534583496984 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.83317535802476, + 47.51435343193945 + ], + [ + 139.8993086187517, + 46.937296070336664 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.83317535802476, + 47.51435343193945 + ], + [ + 140.28133171483628, + 47.81698814797338 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.84996860906236, + 34.32891146111425 + ], + [ + 139.94307988568895, + 33.887936987161005 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.84996860906236, + 34.32891146111425 + ], + [ + 140.09255927487962, + 34.568412937402094 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.85076373502366, + 43.93735281395849 + ], + [ + 140.02544063016526, + 44.79538074898657 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 139.92528456136338, + 41.15534583496984 + ], + [ + 139.95911353513353, + 41.0299002306455 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 140.02544063016526, + 44.79538074898657 + ], + [ + 140.09824222013108, + 44.97841707634863 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 140.09255927487962, + 34.568412937402094 + ], + [ + 140.32197588369004, + 34.719169534921015 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 140.09824222013108, + 44.97841707634863 + ], + [ + 140.2862655489122, + 45.05796877312597 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 140.28133171483628, + 47.81698814797338 + ], + [ + 140.6528064577257, + 48.20434943604406 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 140.2862655489122, + 45.05796877312597 + ], + [ + 140.65091794416062, + 45.489464678048456 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 140.32197588369004, + 34.719169534921015 + ], + [ + 141.5149969427263, + 35.47014776635107 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 140.65091794416062, + 45.489464678048456 + ], + [ + 140.7806996671831, + 45.701386846780146 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 140.6528064577257, + 48.20434943604406 + ], + [ + 140.91145199224107, + 48.913813032388056 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 140.7806996671831, + 45.701386846780146 + ], + [ + 140.99482315465562, + 45.772784866570795 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 140.8489047853624, + 46.249456562280024 + ], + [ + 140.93420713826768, + 46.5079869406217 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 140.8489047853624, + 46.249456562280024 + ], + [ + 141.0645357935106, + 45.840605415582026 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 140.91145199224107, + 48.913813032388056 + ], + [ + 141.10681241437547, + 49.74327413010534 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 140.93420713826768, + 46.5079869406217 + ], + [ + 141.19966119214646, + 46.611142553567255 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 140.99482315465562, + 45.772784866570795 + ], + [ + 141.14938896581285, + 45.7409438269132 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.0645357935106, + 45.840605415582026 + ], + [ + 141.3013078539049, + 45.773108639001215 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.09714835569017, + 50.360651411294306 + ], + [ + 141.11406081601731, + 50.76625791954931 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.09714835569017, + 50.360651411294306 + ], + [ + 141.21944325849168, + 50.056388534783686 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.10681241437547, + 49.74327413010534 + ], + [ + 141.21944325849168, + 50.056388534783686 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.11406081601731, + 50.76625791954931 + ], + [ + 141.15859073087327, + 50.932983555077875 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.14938896581285, + 45.7409438269132 + ], + [ + 141.31114452764146, + 45.67400280404028 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.15859073087327, + 50.932983555077875 + ], + [ + 141.39732497617356, + 50.926422275781 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.19966119214646, + 46.611142553567255 + ], + [ + 141.45194190427415, + 46.58491937088903 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.24745672628038, + 36.312237896203364 + ], + [ + 141.5149969427263, + 35.47014776635107 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.24745672628038, + 36.312237896203364 + ], + [ + 141.61631792470567, + 36.94684187340673 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.27482121869676, + 47.76082292962011 + ], + [ + 141.28433745786302, + 48.065703071832026 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.27482121869676, + 47.76082292962011 + ], + [ + 141.38881581708543, + 47.52238861489233 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.27743762418382, + 42.096488632439936 + ], + [ + 141.52109568997972, + 42.272095121621454 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.27743762418382, + 42.096488632439936 + ], + [ + 141.63888972684495, + 41.700976051568354 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.28433745786302, + 48.065703071832026 + ], + [ + 141.43522876187913, + 48.22684065270361 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.3013078539049, + 45.773108639001215 + ], + [ + 141.5244180528795, + 45.896376051186884 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.31114452764146, + 45.67400280404028 + ], + [ + 141.53762143537156, + 45.739799179314936 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.38881581708543, + 47.52238861489233 + ], + [ + 141.54725402280442, + 47.42903558182653 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.39732497617356, + 50.926422275781 + ], + [ + 141.71131127759568, + 50.92525688576635 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.43271296903245, + 48.799486793755854 + ], + [ + 141.43522876187913, + 48.22684065270361 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.43271296903245, + 48.799486793755854 + ], + [ + 141.62810319348924, + 49.068935312508906 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.45194190427415, + 46.58491937088903 + ], + [ + 141.69390672132127, + 47.160459198235834 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.52109568997972, + 42.272095121621454 + ], + [ + 142.8067597715532, + 41.841869034051264 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.5244180528795, + 45.896376051186884 + ], + [ + 141.54183476850145, + 45.74188891816076 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.53762143537156, + 45.739799179314936 + ], + [ + 141.54183476850145, + 45.74188891816076 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.54725402280442, + 47.42903558182653 + ], + [ + 141.69390672132127, + 47.160459198235834 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.61631792470567, + 36.94684187340673 + ], + [ + 142.5542249052202, + 39.1152125971311 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.62810319348924, + 49.068935312508906 + ], + [ + 141.76245468541734, + 49.89127699303564 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.63888972684495, + 41.700976051568354 + ], + [ + 141.95321696683519, + 41.360001243829096 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.64101952001207, + 50.56034890580114 + ], + [ + 141.71131127759568, + 50.92525688576635 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.64101952001207, + 50.56034890580114 + ], + [ + 141.8183862535631, + 50.205834783791865 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.76245468541734, + 49.89127699303564 + ], + [ + 141.8183862535631, + 50.205834783791865 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.9390384523546, + 40.95789519715246 + ], + [ + 141.95321696683519, + 41.360001243829096 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 141.9390384523546, + 40.95789519715246 + ], + [ + 142.5985295145189, + 39.74348346161779 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3645294, + 37.23227291 + ], + [ + 131.3666873, + 37.27146897 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3645294, + 37.23227291 + ], + [ + 131.3672068, + 37.19309722 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3666873, + 37.27146897 + ], + [ + 131.3736674, + 37.31030766 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3672068, + 37.19309722 + ], + [ + 131.3746863, + 37.15431884 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3736674, + 37.31030766 + ], + [ + 131.3854095, + 37.34841414 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3746863, + 37.15431884 + ], + [ + 131.3868887, + 37.11631032 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3854095, + 37.34841414 + ], + [ + 131.4018073, + 37.38542004 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.3868887, + 37.11631032 + ], + [ + 131.4036901, + 37.07943623 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4018073, + 37.38542004 + ], + [ + 131.4227089, + 37.4209671 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4036901, + 37.07943623 + ], + [ + 131.4249229, + 37.04404979 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4227089, + 37.4209671 + ], + [ + 131.4479179, + 37.45471069 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4249229, + 37.04404979 + ], + [ + 131.4503779, + 37.01048946 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4479179, + 37.45471069 + ], + [ + 131.4771952, + 37.48632319 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4503779, + 37.01048946 + ], + [ + 131.4798068, + 36.9790758 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4771952, + 37.48632319 + ], + [ + 131.5102612, + 37.51549728 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.4798068, + 36.9790758 + ], + [ + 131.5129239, + 36.9501085 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.5102612, + 37.51549728 + ], + [ + 131.5467982, + 37.541949 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.5129239, + 36.9501085 + ], + [ + 131.5494099, + 36.92386359 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.5467982, + 37.541949 + ], + [ + 131.5864539, + 37.56542057 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.5494099, + 36.92386359 + ], + [ + 131.588914, + 36.90059087 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.5864539, + 37.56542057 + ], + [ + 131.628844, + 37.58568304 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.588914, + 36.90059087 + ], + [ + 131.6310581, + 36.88051166 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.6159497978044, + 37.24040921335645 + ], + [ + 131.6789834344638, + 37.426338419556394 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.6159497978044, + 37.24040921335645 + ], + [ + 131.6924993976829, + 37.063692228175476 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.628844, + 37.58568304 + ], + [ + 131.673557, + 37.60253859 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.6310581, + 36.88051166 + ], + [ + 131.6754399, + 36.86381675 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.673557, + 37.60253859 + ], + [ + 131.7201573, + 37.61582252 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.6754399, + 36.86381675 + ], + [ + 131.7216367, + 36.85066465 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.6789834344638, + 37.426338419556394 + ], + [ + 131.79570674559747, + 37.494760171748474 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.6924993976829, + 37.063692228175476 + ], + [ + 131.88096982742374, + 36.990452460251525 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.7201573, + 37.61582252 + ], + [ + 131.7681902, + 37.62540497 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.7216367, + 36.85066465 + ], + [ + 131.7692093, + 36.84118017 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.7681902, + 37.62540497 + ], + [ + 131.8171865, + 37.6311922 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.7692093, + 36.84118017 + ], + [ + 131.817706, + 36.83545326 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.79570674559747, + 37.494760171748474 + ], + [ + 131.99086117128556, + 37.481714926898256 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.8171865, + 37.6311922 + ], + [ + 131.866667, + 37.63312759 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.817706, + 36.83545326 + ], + [ + 131.866667, + 36.83353824 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.866667, + 36.83353824 + ], + [ + 131.9156279, + 36.83545326 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.866667, + 37.63312759 + ], + [ + 131.9161475, + 37.6311922 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.88096982742374, + 36.990452460251525 + ], + [ + 132.04409187602525, + 37.05216214177735 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.9156279, + 36.83545326 + ], + [ + 131.9641247, + 36.84118017 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.9161475, + 37.6311922 + ], + [ + 131.9651438, + 37.62540497 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.9641247, + 36.84118017 + ], + [ + 132.0116973, + 36.85066465 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.9651438, + 37.62540497 + ], + [ + 132.0131767, + 37.61582252 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 131.99086117128556, + 37.481714926898256 + ], + [ + 132.1246261181188, + 37.33971272991575 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.0116973, + 36.85066465 + ], + [ + 132.0578941, + 36.86381675 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.0131767, + 37.61582252 + ], + [ + 132.059777, + 37.60253859 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.04409187602525, + 37.05216214177735 + ], + [ + 132.1340414978442, + 37.21699385927595 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.0578941, + 36.86381675 + ], + [ + 132.1022759, + 36.88051166 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.059777, + 37.60253859 + ], + [ + 132.1044899, + 37.58568304 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1022759, + 36.88051166 + ], + [ + 132.14442, + 36.90059087 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1044899, + 37.58568304 + ], + [ + 132.1468801, + 37.56542057 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1246261181188, + 37.33971272991575 + ], + [ + 132.1340414978442, + 37.21699385927595 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.14442, + 36.90059087 + ], + [ + 132.1839241, + 36.92386359 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1468801, + 37.56542057 + ], + [ + 132.1865358, + 37.541949 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1839241, + 36.92386359 + ], + [ + 132.22041, + 36.9501085 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1865358, + 37.541949 + ], + [ + 132.2230728, + 37.51549728 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1992415537936, + 37.140295005641654 + ], + [ + 132.2802151948092, + 37.29316541576363 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.1992415537936, + 37.140295005641654 + ], + [ + 132.32812844805989, + 37.0769105881086 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.22041, + 36.9501085 + ], + [ + 132.2535272, + 36.9790758 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.2230728, + 37.51549728 + ], + [ + 132.2561388, + 37.48632319 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.2535272, + 36.9790758 + ], + [ + 132.282956, + 37.01048946 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.2561388, + 37.48632319 + ], + [ + 132.2854161, + 37.45471069 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.2802151948092, + 37.29316541576363 + ], + [ + 132.36806856583578, + 37.24564160289258 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.282956, + 37.01048946 + ], + [ + 132.3084111, + 37.04404979 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.2854161, + 37.45471069 + ], + [ + 132.3106251, + 37.4209671 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3084111, + 37.04404979 + ], + [ + 132.32812844805989, + 37.0769105881086 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3106251, + 37.4209671 + ], + [ + 132.3315267, + 37.38542004 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.32812844805989, + 37.0769105881086 + ], + [ + 132.3296439, + 37.07943623 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.32812844805989, + 37.0769105881086 + ], + [ + 132.37221729698305, + 37.05522843008466 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3296439, + 37.07943623 + ], + [ + 132.3464452, + 37.11631032 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3315267, + 37.38542004 + ], + [ + 132.3479245, + 37.34841414 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3464452, + 37.11631032 + ], + [ + 132.3586477, + 37.15431884 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3479245, + 37.34841414 + ], + [ + 132.3596666, + 37.31030766 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3586477, + 37.15431884 + ], + [ + 132.3661272, + 37.19309722 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3596666, + 37.31030766 + ], + [ + 132.3666466, + 37.27146897 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3661272, + 37.19309722 + ], + [ + 132.3688046, + 37.23227291 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.3666466, + 37.27146897 + ], + [ + 132.36806856583578, + 37.24564160289258 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.36806856583578, + 37.24564160289258 + ], + [ + 132.3688046, + 37.23227291 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.36806856583578, + 37.24564160289258 + ], + [ + 132.46744946519559, + 37.19188203500189 + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 132.37221729698305, + 37.05522843008466 + ], + [ + 132.46744946519559, + 37.19188203500189 + ] + ] + }, + "properties": null + } + ] +} diff --git a/fixtures/polygonizer/input/split_touching_two_points.geojson b/fixtures/polygonizer/input/split_touching_two_points.geojson new file mode 100644 index 0000000..fb39271 --- /dev/null +++ b/fixtures/polygonizer/input/split_touching_two_points.geojson @@ -0,0 +1,33 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [0.0, 0.0], + [20.0, 0.0], + [20.0, 20.0], + [0.0, 20.0], + [0.0, 0.0] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [10.0, 0.0], + [14.0, 10.0], + [10.0, 20.0], + [6.0, 10.0], + [10.0, 0.0] + ] + }, + "properties": null + } + ] +} diff --git a/fixtures/polygonizer/input/touching_hole_overlap_ownership_minimal.geojson b/fixtures/polygonizer/input/touching_hole_overlap_ownership_minimal.geojson new file mode 100644 index 0000000..38c60a5 --- /dev/null +++ b/fixtures/polygonizer/input/touching_hole_overlap_ownership_minimal.geojson @@ -0,0 +1,18 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "MultiLineString", + "coordinates": [ + [[90.0, 10.0], [130.0, 10.0], [130.0, 30.0], [90.0, 30.0], [90.0, 10.0]], + [[100.0, 16.0], [122.0, 16.0], [122.0, 26.0], [100.0, 26.0], [100.0, 16.0]], + [[108.0, 18.0], [113.0, 18.0], [113.0, 22.0], [108.0, 22.0], [108.0, 18.0]], + [[115.0, 22.0], [119.0, 22.0], [119.0, 25.0], [115.0, 25.0], [115.0, 22.0]] + ] + } + } + ] +} diff --git a/fixtures/polygonizer/input/touching_hole_overlap_ownership_probe.geojson b/fixtures/polygonizer/input/touching_hole_overlap_ownership_probe.geojson new file mode 100644 index 0000000..a702828 --- /dev/null +++ b/fixtures/polygonizer/input/touching_hole_overlap_ownership_probe.geojson @@ -0,0 +1,18 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "MultiLineString", + "coordinates": [ + [[-6.0, 30.0], [36.0, 30.0], [36.0, 46.0], [-6.0, 46.0], [-6.0, 30.0]], + [[-2.0, 36.0], [15.0, 36.0], [15.0, 43.5], [-2.0, 43.5], [-2.0, 36.0]], + [[3.5, 37.5], [8.5, 37.5], [8.5, 40.5], [3.5, 40.5], [3.5, 37.5]], + [[10.0, 40.8], [13.5, 40.8], [13.5, 42.8], [10.0, 42.8], [10.0, 40.8]] + ] + } + } + ] +} diff --git a/fixtures/polygonizer/output/contained_island.geojson b/fixtures/polygonizer/output/contained_island.geojson new file mode 100644 index 0000000..57d59b6 --- /dev/null +++ b/fixtures/polygonizer/output/contained_island.geojson @@ -0,0 +1,142 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 0.0, + 0.0 + ], + [ + 20.0, + 0.0 + ], + [ + 20.0, + 20.0 + ], + [ + 0.0, + 20.0 + ], + [ + 0.0, + 0.0 + ] + ], + [ + [ + 3.0, + 3.0 + ], + [ + 3.0, + 10.0 + ], + [ + 10.0, + 10.0 + ], + [ + 10.0, + 3.0 + ], + [ + 3.0, + 3.0 + ] + ], + [ + [ + 13.0, + 13.0 + ], + [ + 13.0, + 18.0 + ], + [ + 18.0, + 18.0 + ], + [ + 18.0, + 13.0 + ], + [ + 13.0, + 13.0 + ] + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 3.0, + 3.0 + ], + [ + 10.0, + 3.0 + ], + [ + 10.0, + 10.0 + ], + [ + 3.0, + 10.0 + ], + [ + 3.0, + 3.0 + ] + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 13.0, + 13.0 + ], + [ + 18.0, + 13.0 + ], + [ + 18.0, + 18.0 + ], + [ + 13.0, + 18.0 + ], + [ + 13.0, + 13.0 + ] + ] + ] + }, + "properties": null + } + ] +} \ No newline at end of file diff --git a/fixtures/polygonizer/output/failed_hole.geojson b/fixtures/polygonizer/output/failed_hole.geojson new file mode 100644 index 0000000..3ec02fb --- /dev/null +++ b/fixtures/polygonizer/output/failed_hole.geojson @@ -0,0 +1,467 @@ +{ + "features": [ + { + "geometry": { + "coordinates": [ + [ + [ + 131.6159497978044, + 37.24040921335645 + ], + [ + 131.6924993976829, + 37.06369222817547 + ], + [ + 131.88096982742374, + 36.990452460251525 + ], + [ + 132.04409187602525, + 37.05216214177735 + ], + [ + 132.1340414978442, + 37.21699385927595 + ], + [ + 132.1246261181188, + 37.33971272991575 + ], + [ + 131.99086117128556, + 37.481714926898256 + ], + [ + 131.79570674559747, + 37.494760171748474 + ], + [ + 131.6789834344638, + 37.426338419556394 + ], + [ + 131.6159497978044, + 37.24040921335645 + ] + ] + ], + "type": "Polygon" + }, + "properties": null, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + 131.3645294, + 37.23227291 + ], + [ + 131.3672068, + 37.19309722 + ], + [ + 131.3746863, + 37.15431884 + ], + [ + 131.3868887, + 37.11631032 + ], + [ + 131.4036901, + 37.07943623 + ], + [ + 131.4249229, + 37.04404979 + ], + [ + 131.4503779, + 37.01048946 + ], + [ + 131.4798068, + 36.9790758 + ], + [ + 131.5129239, + 36.9501085 + ], + [ + 131.5494099, + 36.92386359 + ], + [ + 131.588914, + 36.90059087 + ], + [ + 131.6310581, + 36.88051166 + ], + [ + 131.6754399, + 36.86381675 + ], + [ + 131.7216367, + 36.85066465 + ], + [ + 131.7692093, + 36.84118017 + ], + [ + 131.817706, + 36.83545326 + ], + [ + 131.866667, + 36.83353824 + ], + [ + 131.9156279, + 36.83545326 + ], + [ + 131.9641247, + 36.84118017 + ], + [ + 132.0116973, + 36.85066465 + ], + [ + 132.0578941, + 36.86381675 + ], + [ + 132.1022759, + 36.88051166 + ], + [ + 132.14442, + 36.90059087 + ], + [ + 132.1839241, + 36.92386359 + ], + [ + 132.22041, + 36.9501085 + ], + [ + 132.2535272, + 36.9790758 + ], + [ + 132.282956, + 37.01048946 + ], + [ + 132.3084111, + 37.04404979 + ], + [ + 132.32812844805989, + 37.0769105881086 + ], + [ + 132.1992415537936, + 37.140295005641654 + ], + [ + 132.2802151948092, + 37.29316541576363 + ], + [ + 132.36806856583578, + 37.24564160289258 + ], + [ + 132.3666466, + 37.27146897 + ], + [ + 132.3596666, + 37.31030766 + ], + [ + 132.3479245, + 37.34841414 + ], + [ + 132.3315267, + 37.38542004 + ], + [ + 132.3106251, + 37.4209671 + ], + [ + 132.2854161, + 37.45471069 + ], + [ + 132.2561388, + 37.48632319 + ], + [ + 132.2230728, + 37.51549728 + ], + [ + 132.1865358, + 37.541949 + ], + [ + 132.1468801, + 37.56542057 + ], + [ + 132.1044899, + 37.58568304 + ], + [ + 132.059777, + 37.60253859 + ], + [ + 132.0131767, + 37.61582252 + ], + [ + 131.9651438, + 37.62540497 + ], + [ + 131.9161475, + 37.6311922 + ], + [ + 131.866667, + 37.63312759 + ], + [ + 131.8171865, + 37.6311922 + ], + [ + 131.7681902, + 37.62540497 + ], + [ + 131.7201573, + 37.61582252 + ], + [ + 131.673557, + 37.60253859 + ], + [ + 131.628844, + 37.58568304 + ], + [ + 131.5864539, + 37.56542057 + ], + [ + 131.5467982, + 37.541949 + ], + [ + 131.5102612, + 37.51549728 + ], + [ + 131.4771952, + 37.48632319 + ], + [ + 131.4479179, + 37.45471069 + ], + [ + 131.4227089, + 37.4209671 + ], + [ + 131.4018073, + 37.38542004 + ], + [ + 131.3854095, + 37.34841414 + ], + [ + 131.3736674, + 37.31030766 + ], + [ + 131.3666873, + 37.27146897 + ], + [ + 131.3645294, + 37.23227291 + ] + ], + [ + [ + 131.6159497978044, + 37.24040921335645 + ], + [ + 131.6789834344638, + 37.426338419556394 + ], + [ + 131.79570674559747, + 37.494760171748474 + ], + [ + 131.99086117128556, + 37.481714926898256 + ], + [ + 132.1246261181188, + 37.33971272991575 + ], + [ + 132.1340414978442, + 37.21699385927595 + ], + [ + 132.04409187602525, + 37.05216214177735 + ], + [ + 131.88096982742374, + 36.990452460251525 + ], + [ + 131.6924993976829, + 37.06369222817547 + ], + [ + 131.6159497978044, + 37.24040921335645 + ] + ] + ], + "type": "Polygon" + }, + "properties": null, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + 132.1992415537936, + 37.140295005641654 + ], + [ + 132.32812844805989, + 37.0769105881086 + ], + [ + 132.3296439, + 37.07943623 + ], + [ + 132.3464452, + 37.11631032 + ], + [ + 132.3586477, + 37.15431884 + ], + [ + 132.3661272, + 37.19309722 + ], + [ + 132.3688046, + 37.23227291 + ], + [ + 132.36806856583578, + 37.24564160289258 + ], + [ + 132.2802151948092, + 37.29316541576363 + ], + [ + 132.1992415537936, + 37.140295005641654 + ] + ] + ], + "type": "Polygon" + }, + "properties": null, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + 132.32812844805989, + 37.0769105881086 + ], + [ + 132.37221729698305, + 37.05522843008466 + ], + [ + 132.4674494651956, + 37.19188203500189 + ], + [ + 132.36806856583578, + 37.24564160289258 + ], + [ + 132.3688046, + 37.23227291 + ], + [ + 132.3661272, + 37.19309722 + ], + [ + 132.3586477, + 37.15431884 + ], + [ + 132.3464452, + 37.11631032 + ], + [ + 132.3296439, + 37.07943623 + ], + [ + 132.32812844805989, + 37.0769105881086 + ] + ] + ], + "type": "Polygon" + }, + "properties": null, + "type": "Feature" + } + ], + "type": "FeatureCollection" +} diff --git a/fixtures/polygonizer/output/invalid_hole_assignment.geojson b/fixtures/polygonizer/output/invalid_hole_assignment.geojson new file mode 100644 index 0000000..d431351 --- /dev/null +++ b/fixtures/polygonizer/output/invalid_hole_assignment.geojson @@ -0,0 +1,250 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 67.96072344674927, + -49.21812840419421 + ], + [ + 68.03560388863696, + -49.526937060777 + ], + [ + 68.25291539492534, + -49.80551737445704 + ], + [ + 68.5913860012806, + -50.02659996314815 + ], + [ + 69.01788384648931, + -50.16854372269627 + ], + [ + 69.49066034987723, + -50.21745420893651 + ], + [ + 69.96343685326515, + -50.16854372269627 + ], + [ + 70.26289097957036, + -50.06888168459346 + ], + [ + 71.00971073191234, + -49.17323214198215 + ], + [ + 70.9457168111175, + -48.90931974761142 + ], + [ + 70.72840530482912, + -48.63073943393138 + ], + [ + 70.38993469847387, + -48.40965684524027 + ], + [ + 69.96343685326515, + -48.26771308569215 + ], + [ + 69.49066034987723, + -48.21880259945191 + ], + [ + 69.01788384648931, + -48.26771308569215 + ], + [ + 68.5913860012806, + -48.40965684524027 + ], + [ + 68.25291539492534, + -48.63073943393138 + ], + [ + 68.03560388863696, + -48.90931974761142 + ], + [ + 67.96072344674927, + -49.21812840419421 + ] + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 70.26289097957036, + -50.06888168459346 + ], + [ + 70.38993469847387, + -50.02659996314815 + ], + [ + 70.72840530482912, + -49.80551737445704 + ], + [ + 70.9457168111175, + -49.526937060777 + ], + [ + 71.02059725300519, + -49.21812840419421 + ], + [ + 71.00971073191234, + -49.17323214198215 + ], + [ + 70.26289097957036, + -50.06888168459346 + ] + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 66.917152079, + -49.483609955 + ], + [ + 68.49003423, + -50.548027294 + ], + [ + 69.861979006, + -50.549689302 + ], + [ + 70.26289097957036, + -50.06888168459346 + ], + [ + 69.96343685326515, + -50.16854372269627 + ], + [ + 69.49066034987723, + -50.21745420893651 + ], + [ + 69.01788384648931, + -50.16854372269627 + ], + [ + 68.5913860012806, + -50.02659996314815 + ], + [ + 68.25291539492534, + -49.80551737445704 + ], + [ + 68.03560388863696, + -49.526937060777 + ], + [ + 67.96072344674927, + -49.21812840419421 + ], + [ + 68.03560388863696, + -48.90931974761142 + ], + [ + 68.25291539492534, + -48.63073943393138 + ], + [ + 68.5913860012806, + -48.40965684524027 + ], + [ + 69.01788384648931, + -48.26771308569215 + ], + [ + 69.49066034987723, + -48.21880259945191 + ], + [ + 69.96343685326515, + -48.26771308569215 + ], + [ + 70.38993469847387, + -48.40965684524027 + ], + [ + 70.72840530482912, + -48.63073943393138 + ], + [ + 70.9457168111175, + -48.90931974761142 + ], + [ + 71.00971073191234, + -49.17323214198215 + ], + [ + 71.09668316, + -49.068927435 + ], + [ + 71.638389864, + -47.695087312 + ], + [ + 70.997703438, + -47.154869247 + ], + [ + 68.698823473, + -48.144678001 + ], + [ + 67.076757868, + -49.04184243 + ], + [ + 66.917152079, + -49.483609955 + ] + ] + ] + }, + "properties": null + } + ] +} \ No newline at end of file diff --git a/fixtures/polygonizer/output/minimal_secondary_probe_split.geojson b/fixtures/polygonizer/output/minimal_secondary_probe_split.geojson new file mode 100644 index 0000000..9412d46 --- /dev/null +++ b/fixtures/polygonizer/output/minimal_secondary_probe_split.geojson @@ -0,0 +1,173 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 90.0, + 10.0 + ], + [ + 130.0, + 10.0 + ], + [ + 130.0, + 30.0 + ], + [ + 90.0, + 30.0 + ], + [ + 90.0, + 10.0 + ] + ], + [ + [ + 100.0, + 16.0 + ], + [ + 100.0, + 26.0 + ], + [ + 122.0, + 26.0 + ], + [ + 122.0, + 16.0 + ], + [ + 100.0, + 16.0 + ] + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 100.0, + 16.0 + ], + [ + 122.0, + 16.0 + ], + [ + 122.0, + 26.0 + ], + [ + 100.0, + 26.0 + ], + [ + 100.0, + 16.0 + ] + ], + [ + [ + 108.0, + 19.0 + ], + [ + 108.0, + 22.0 + ], + [ + 113.0, + 22.0 + ], + [ + 113.0, + 19.0 + ], + [ + 108.0, + 19.0 + ] + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 108.0, + 16.0 + ], + [ + 113.0, + 16.0 + ], + [ + 113.0, + 19.0 + ], + [ + 108.0, + 19.0 + ], + [ + 108.0, + 16.0 + ] + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 108.0, + 19.0 + ], + [ + 113.0, + 19.0 + ], + [ + 113.0, + 22.0 + ], + [ + 108.0, + 22.0 + ], + [ + 108.0, + 19.0 + ] + ] + ] + }, + "properties": null + } + ] +} \ No newline at end of file diff --git a/fixtures/polygonizer/output/nested_shell_overlap_minimal.geojson b/fixtures/polygonizer/output/nested_shell_overlap_minimal.geojson new file mode 100644 index 0000000..7ecaa61 --- /dev/null +++ b/fixtures/polygonizer/output/nested_shell_overlap_minimal.geojson @@ -0,0 +1,161 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 131.3645294, + 37.23227291 + ], + [ + 131.4036901, + 37.07943623 + ], + [ + 131.6754399, + 36.86381675 + ], + [ + 131.866667, + 36.83353824 + ], + [ + 132.0578941, + 36.86381675 + ], + [ + 132.3296439, + 37.07943623 + ], + [ + 132.3688046, + 37.23227291 + ], + [ + 132.3315267, + 37.38542004 + ], + [ + 132.059777, + 37.60253859 + ], + [ + 131.866667, + 37.63312759 + ], + [ + 131.673557, + 37.60253859 + ], + [ + 131.4018073, + 37.38542004 + ], + [ + 131.3645294, + 37.23227291 + ] + ], + [ + [ + 131.6159497978044, + 37.24040921335645 + ], + [ + 131.6789834344638, + 37.426338419556394 + ], + [ + 131.79570674559747, + 37.494760171748474 + ], + [ + 131.99086117128556, + 37.481714926898256 + ], + [ + 132.1246261181188, + 37.33971272991575 + ], + [ + 132.1340414978442, + 37.21699385927595 + ], + [ + 132.04409187602525, + 37.05216214177735 + ], + [ + 131.88096982742374, + 36.990452460251525 + ], + [ + 131.6924993976829, + 37.06369222817547 + ], + [ + 131.6159497978044, + 37.24040921335645 + ] + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 131.6159497978044, + 37.24040921335645 + ], + [ + 131.6924993976829, + 37.06369222817547 + ], + [ + 131.88096982742374, + 36.990452460251525 + ], + [ + 132.04409187602525, + 37.05216214177735 + ], + [ + 132.1340414978442, + 37.21699385927595 + ], + [ + 132.1246261181188, + 37.33971272991575 + ], + [ + 131.99086117128556, + 37.481714926898256 + ], + [ + 131.79570674559747, + 37.494760171748474 + ], + [ + 131.6789834344638, + 37.426338419556394 + ], + [ + 131.6159497978044, + 37.24040921335645 + ] + ] + ] + }, + "properties": null + } + ] +} \ No newline at end of file diff --git a/fixtures/polygonizer/output/ownership_area_priority_enclosing_shell_minimal.geojson b/fixtures/polygonizer/output/ownership_area_priority_enclosing_shell_minimal.geojson new file mode 100644 index 0000000..797a78a --- /dev/null +++ b/fixtures/polygonizer/output/ownership_area_priority_enclosing_shell_minimal.geojson @@ -0,0 +1,825 @@ +{ + "features": [ + { + "geometry": { + "coordinates": [ + [ + [ + 130.5, + 36.2 + ], + [ + 130.8, + 36.2 + ], + [ + 131.1, + 36.2 + ], + [ + 131.4, + 36.2 + ], + [ + 131.7, + 36.2 + ], + [ + 132.0, + 36.2 + ], + [ + 132.3, + 36.2 + ], + [ + 132.6, + 36.2 + ], + [ + 132.9, + 36.2 + ], + [ + 133.2, + 36.2 + ], + [ + 133.2, + 36.45 + ], + [ + 133.2, + 36.7 + ], + [ + 133.2, + 36.95 + ], + [ + 133.2, + 37.2 + ], + [ + 133.2, + 37.45 + ], + [ + 133.2, + 37.7 + ], + [ + 133.2, + 37.95 + ], + [ + 133.2, + 38.2 + ], + [ + 132.9, + 38.2 + ], + [ + 132.6, + 38.2 + ], + [ + 132.3, + 38.2 + ], + [ + 132.0, + 38.2 + ], + [ + 131.7, + 38.2 + ], + [ + 131.4, + 38.2 + ], + [ + 131.1, + 38.2 + ], + [ + 130.8, + 38.2 + ], + [ + 130.5, + 38.2 + ], + [ + 130.5, + 37.95 + ], + [ + 130.5, + 37.7 + ], + [ + 130.5, + 37.45 + ], + [ + 130.5, + 37.2 + ], + [ + 130.5, + 36.95 + ], + [ + 130.5, + 36.7 + ], + [ + 130.5, + 36.45 + ], + [ + 130.5, + 36.2 + ] + ], + [ + [ + 131.3645294, + 37.23227291 + ], + [ + 131.3666873, + 37.27146897 + ], + [ + 131.3736674, + 37.31030766 + ], + [ + 131.3854095, + 37.34841414 + ], + [ + 131.4018073, + 37.38542004 + ], + [ + 131.4227089, + 37.4209671 + ], + [ + 131.4479179, + 37.45471069 + ], + [ + 131.4771952, + 37.48632319 + ], + [ + 131.5102612, + 37.51549728 + ], + [ + 131.5467982, + 37.541949 + ], + [ + 131.5864539, + 37.56542057 + ], + [ + 131.628844, + 37.58568304 + ], + [ + 131.673557, + 37.60253859 + ], + [ + 131.7201573, + 37.61582252 + ], + [ + 131.7681902, + 37.62540497 + ], + [ + 131.8171865, + 37.6311922 + ], + [ + 131.866667, + 37.63312759 + ], + [ + 131.9161475, + 37.6311922 + ], + [ + 131.9651438, + 37.62540497 + ], + [ + 132.0131767, + 37.61582252 + ], + [ + 132.059777, + 37.60253859 + ], + [ + 132.1044899, + 37.58568304 + ], + [ + 132.1468801, + 37.56542057 + ], + [ + 132.1865358, + 37.541949 + ], + [ + 132.2230728, + 37.51549728 + ], + [ + 132.2561388, + 37.48632319 + ], + [ + 132.2854161, + 37.45471069 + ], + [ + 132.3106251, + 37.4209671 + ], + [ + 132.3315267, + 37.38542004 + ], + [ + 132.3479245, + 37.34841414 + ], + [ + 132.3596666, + 37.31030766 + ], + [ + 132.3666466, + 37.27146897 + ], + [ + 132.36806856583578, + 37.24564160289258 + ], + [ + 132.2802151948092, + 37.29316541576363 + ], + [ + 132.1992415537936, + 37.140295005641654 + ], + [ + 132.32812844805989, + 37.0769105881086 + ], + [ + 132.3084111, + 37.04404979 + ], + [ + 132.282956, + 37.01048946 + ], + [ + 132.2535272, + 36.9790758 + ], + [ + 132.22041, + 36.9501085 + ], + [ + 132.1839241, + 36.92386359 + ], + [ + 132.14442, + 36.90059087 + ], + [ + 132.1022759, + 36.88051166 + ], + [ + 132.0578941, + 36.86381675 + ], + [ + 132.0116973, + 36.85066465 + ], + [ + 131.9641247, + 36.84118017 + ], + [ + 131.9156279, + 36.83545326 + ], + [ + 131.866667, + 36.83353824 + ], + [ + 131.817706, + 36.83545326 + ], + [ + 131.7692093, + 36.84118017 + ], + [ + 131.7216367, + 36.85066465 + ], + [ + 131.6754399, + 36.86381675 + ], + [ + 131.6310581, + 36.88051166 + ], + [ + 131.588914, + 36.90059087 + ], + [ + 131.5494099, + 36.92386359 + ], + [ + 131.5129239, + 36.9501085 + ], + [ + 131.4798068, + 36.9790758 + ], + [ + 131.4503779, + 37.01048946 + ], + [ + 131.4249229, + 37.04404979 + ], + [ + 131.4036901, + 37.07943623 + ], + [ + 131.3868887, + 37.11631032 + ], + [ + 131.3746863, + 37.15431884 + ], + [ + 131.3672068, + 37.19309722 + ], + [ + 131.3645294, + 37.23227291 + ] + ] + ], + "type": "Polygon" + }, + "properties": null, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + 131.6159497978044, + 37.24040921335645 + ], + [ + 131.6924993976829, + 37.06369222817547 + ], + [ + 131.88096982742374, + 36.990452460251525 + ], + [ + 132.04409187602525, + 37.05216214177735 + ], + [ + 132.1340414978442, + 37.21699385927595 + ], + [ + 132.1246261181188, + 37.33971272991575 + ], + [ + 131.99086117128556, + 37.481714926898256 + ], + [ + 131.79570674559747, + 37.494760171748474 + ], + [ + 131.6789834344638, + 37.426338419556394 + ], + [ + 131.6159497978044, + 37.24040921335645 + ] + ] + ], + "type": "Polygon" + }, + "properties": null, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + 131.3645294, + 37.23227291 + ], + [ + 131.3672068, + 37.19309722 + ], + [ + 131.3746863, + 37.15431884 + ], + [ + 131.3868887, + 37.11631032 + ], + [ + 131.4036901, + 37.07943623 + ], + [ + 131.4249229, + 37.04404979 + ], + [ + 131.4503779, + 37.01048946 + ], + [ + 131.4798068, + 36.9790758 + ], + [ + 131.5129239, + 36.9501085 + ], + [ + 131.5494099, + 36.92386359 + ], + [ + 131.588914, + 36.90059087 + ], + [ + 131.6310581, + 36.88051166 + ], + [ + 131.6754399, + 36.86381675 + ], + [ + 131.7216367, + 36.85066465 + ], + [ + 131.7692093, + 36.84118017 + ], + [ + 131.817706, + 36.83545326 + ], + [ + 131.866667, + 36.83353824 + ], + [ + 131.9156279, + 36.83545326 + ], + [ + 131.9641247, + 36.84118017 + ], + [ + 132.0116973, + 36.85066465 + ], + [ + 132.0578941, + 36.86381675 + ], + [ + 132.1022759, + 36.88051166 + ], + [ + 132.14442, + 36.90059087 + ], + [ + 132.1839241, + 36.92386359 + ], + [ + 132.22041, + 36.9501085 + ], + [ + 132.2535272, + 36.9790758 + ], + [ + 132.282956, + 37.01048946 + ], + [ + 132.3084111, + 37.04404979 + ], + [ + 132.32812844805989, + 37.0769105881086 + ], + [ + 132.1992415537936, + 37.140295005641654 + ], + [ + 132.2802151948092, + 37.29316541576363 + ], + [ + 132.36806856583578, + 37.24564160289258 + ], + [ + 132.3666466, + 37.27146897 + ], + [ + 132.3596666, + 37.31030766 + ], + [ + 132.3479245, + 37.34841414 + ], + [ + 132.3315267, + 37.38542004 + ], + [ + 132.3106251, + 37.4209671 + ], + [ + 132.2854161, + 37.45471069 + ], + [ + 132.2561388, + 37.48632319 + ], + [ + 132.2230728, + 37.51549728 + ], + [ + 132.1865358, + 37.541949 + ], + [ + 132.1468801, + 37.56542057 + ], + [ + 132.1044899, + 37.58568304 + ], + [ + 132.059777, + 37.60253859 + ], + [ + 132.0131767, + 37.61582252 + ], + [ + 131.9651438, + 37.62540497 + ], + [ + 131.9161475, + 37.6311922 + ], + [ + 131.866667, + 37.63312759 + ], + [ + 131.8171865, + 37.6311922 + ], + [ + 131.7681902, + 37.62540497 + ], + [ + 131.7201573, + 37.61582252 + ], + [ + 131.673557, + 37.60253859 + ], + [ + 131.628844, + 37.58568304 + ], + [ + 131.5864539, + 37.56542057 + ], + [ + 131.5467982, + 37.541949 + ], + [ + 131.5102612, + 37.51549728 + ], + [ + 131.4771952, + 37.48632319 + ], + [ + 131.4479179, + 37.45471069 + ], + [ + 131.4227089, + 37.4209671 + ], + [ + 131.4018073, + 37.38542004 + ], + [ + 131.3854095, + 37.34841414 + ], + [ + 131.3736674, + 37.31030766 + ], + [ + 131.3666873, + 37.27146897 + ], + [ + 131.3645294, + 37.23227291 + ] + ], + [ + [ + 131.6159497978044, + 37.24040921335645 + ], + [ + 131.6789834344638, + 37.426338419556394 + ], + [ + 131.79570674559747, + 37.494760171748474 + ], + [ + 131.99086117128556, + 37.481714926898256 + ], + [ + 132.1246261181188, + 37.33971272991575 + ], + [ + 132.1340414978442, + 37.21699385927595 + ], + [ + 132.04409187602525, + 37.05216214177735 + ], + [ + 131.88096982742374, + 36.990452460251525 + ], + [ + 131.6924993976829, + 37.06369222817547 + ], + [ + 131.6159497978044, + 37.24040921335645 + ] + ] + ], + "type": "Polygon" + }, + "properties": null, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + 132.32812844805989, + 37.0769105881086 + ], + [ + 132.37221729698305, + 37.05522843008466 + ], + [ + 132.4674494651956, + 37.19188203500189 + ], + [ + 132.36806856583578, + 37.24564160289258 + ], + [ + 132.3688046, + 37.23227291 + ], + [ + 132.3661272, + 37.19309722 + ], + [ + 132.3586477, + 37.15431884 + ], + [ + 132.3464452, + 37.11631032 + ], + [ + 132.3296439, + 37.07943623 + ], + [ + 132.32812844805989, + 37.0769105881086 + ] + ] + ], + "type": "Polygon" + }, + "properties": null, + "type": "Feature" + } + ], + "type": "FeatureCollection" +} \ No newline at end of file diff --git a/fixtures/polygonizer/output/split_touching_hole_drop_minimal.geojson b/fixtures/polygonizer/output/split_touching_hole_drop_minimal.geojson new file mode 100644 index 0000000..4935468 --- /dev/null +++ b/fixtures/polygonizer/output/split_touching_hole_drop_minimal.geojson @@ -0,0 +1,142 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 0.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 10.0 + ], + [ + 0.0, + 10.0 + ], + [ + 0.0, + 0.0 + ] + ], + [ + [ + 3.0, + 3.0 + ], + [ + 3.0, + 7.0 + ], + [ + 7.0, + 7.0 + ], + [ + 7.0, + 3.0 + ], + [ + 3.0, + 3.0 + ] + ], + [ + [ + 7.0, + 7.0 + ], + [ + 7.0, + 8.0 + ], + [ + 8.0, + 8.0 + ], + [ + 8.0, + 7.0 + ], + [ + 7.0, + 7.0 + ] + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 7.0, + 7.0 + ], + [ + 8.0, + 7.0 + ], + [ + 8.0, + 8.0 + ], + [ + 7.0, + 8.0 + ], + [ + 7.0, + 7.0 + ] + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 3.0, + 3.0 + ], + [ + 7.0, + 3.0 + ], + [ + 7.0, + 7.0 + ], + [ + 3.0, + 7.0 + ], + [ + 3.0, + 3.0 + ] + ] + ] + }, + "properties": null + } + ] +} \ No newline at end of file diff --git a/fixtures/polygonizer/output/split_touching_two_points.geojson b/fixtures/polygonizer/output/split_touching_two_points.geojson new file mode 100644 index 0000000..e92494c --- /dev/null +++ b/fixtures/polygonizer/output/split_touching_two_points.geojson @@ -0,0 +1,106 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 0.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 6.0, + 10.0 + ], + [ + 10.0, + 20.0 + ], + [ + 0.0, + 20.0 + ], + [ + 0.0, + 0.0 + ] + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 10.0, + 0.0 + ], + [ + 20.0, + 0.0 + ], + [ + 20.0, + 20.0 + ], + [ + 10.0, + 20.0 + ], + [ + 14.0, + 10.0 + ], + [ + 10.0, + 0.0 + ] + ] + ] + }, + "properties": null + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 6.0, + 10.0 + ], + [ + 10.0, + 0.0 + ], + [ + 14.0, + 10.0 + ], + [ + 10.0, + 20.0 + ], + [ + 6.0, + 10.0 + ] + ] + ] + }, + "properties": null + } + ] +} \ No newline at end of file diff --git a/fixtures/polygonizer/output/touching_hole_overlap_ownership_minimal.geojson b/fixtures/polygonizer/output/touching_hole_overlap_ownership_minimal.geojson new file mode 100644 index 0000000..3ceff48 --- /dev/null +++ b/fixtures/polygonizer/output/touching_hole_overlap_ownership_minimal.geojson @@ -0,0 +1,176 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 90.0, + 10.0 + ], + [ + 130.0, + 10.0 + ], + [ + 130.0, + 30.0 + ], + [ + 90.0, + 30.0 + ], + [ + 90.0, + 10.0 + ] + ], + [ + [ + 100.0, + 16.0 + ], + [ + 100.0, + 26.0 + ], + [ + 122.0, + 26.0 + ], + [ + 122.0, + 16.0 + ], + [ + 100.0, + 16.0 + ] + ] + ], + [ + [ + [ + 100.0, + 16.0 + ], + [ + 122.0, + 16.0 + ], + [ + 122.0, + 26.0 + ], + [ + 100.0, + 26.0 + ], + [ + 100.0, + 16.0 + ] + ], + [ + [ + 108.0, + 18.0 + ], + [ + 108.0, + 22.0 + ], + [ + 113.0, + 22.0 + ], + [ + 113.0, + 18.0 + ], + [ + 108.0, + 18.0 + ] + ], + [ + [ + 115.0, + 22.0 + ], + [ + 115.0, + 25.0 + ], + [ + 119.0, + 25.0 + ], + [ + 119.0, + 22.0 + ], + [ + 115.0, + 22.0 + ] + ] + ], + [ + [ + [ + 108.0, + 18.0 + ], + [ + 113.0, + 18.0 + ], + [ + 113.0, + 22.0 + ], + [ + 108.0, + 22.0 + ], + [ + 108.0, + 18.0 + ] + ] + ], + [ + [ + [ + 115.0, + 22.0 + ], + [ + 119.0, + 22.0 + ], + [ + 119.0, + 25.0 + ], + [ + 115.0, + 25.0 + ], + [ + 115.0, + 22.0 + ] + ] + ] + ] + }, + "properties": null + } + ] +} diff --git a/fixtures/polygonizer/output/touching_hole_overlap_ownership_probe.geojson b/fixtures/polygonizer/output/touching_hole_overlap_ownership_probe.geojson new file mode 100644 index 0000000..96acc83 --- /dev/null +++ b/fixtures/polygonizer/output/touching_hole_overlap_ownership_probe.geojson @@ -0,0 +1,176 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + -6.0, + 30.0 + ], + [ + 36.0, + 30.0 + ], + [ + 36.0, + 46.0 + ], + [ + -6.0, + 46.0 + ], + [ + -6.0, + 30.0 + ] + ], + [ + [ + -2.0, + 36.0 + ], + [ + -2.0, + 43.5 + ], + [ + 15.0, + 43.5 + ], + [ + 15.0, + 36.0 + ], + [ + -2.0, + 36.0 + ] + ] + ], + [ + [ + [ + -2.0, + 36.0 + ], + [ + 15.0, + 36.0 + ], + [ + 15.0, + 43.5 + ], + [ + -2.0, + 43.5 + ], + [ + -2.0, + 36.0 + ] + ], + [ + [ + 3.5, + 37.5 + ], + [ + 3.5, + 40.5 + ], + [ + 8.5, + 40.5 + ], + [ + 8.5, + 37.5 + ], + [ + 3.5, + 37.5 + ] + ], + [ + [ + 10.0, + 40.8 + ], + [ + 10.0, + 42.8 + ], + [ + 13.5, + 42.8 + ], + [ + 13.5, + 40.8 + ], + [ + 10.0, + 40.8 + ] + ] + ], + [ + [ + [ + 3.5, + 37.5 + ], + [ + 8.5, + 37.5 + ], + [ + 8.5, + 40.5 + ], + [ + 3.5, + 40.5 + ], + [ + 3.5, + 37.5 + ] + ] + ], + [ + [ + [ + 10.0, + 40.8 + ], + [ + 13.5, + 40.8 + ], + [ + 13.5, + 42.8 + ], + [ + 10.0, + 42.8 + ], + [ + 10.0, + 40.8 + ] + ] + ] + ] + }, + "properties": null + } + ] +} \ No newline at end of file diff --git a/src/graph.rs b/src/graph.rs index 529a653..c887e2c 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,13 +1,22 @@ +//! Planar graph representation and polygon extraction pipeline. +//! +//! This module stores noded linework as directed edges sorted in CCW order per +//! origin node, then traverses left faces to assemble minimal rings. + use std::cmp::Ordering; -use std::collections::{BTreeMap, BTreeSet, btree_map, btree_set}; -use std::iter::{FilterMap, Flatten, Map}; +use std::collections::{BTreeMap, BTreeSet}; use geo::Winding; -use geo::orient::Direction; -use geo::{ - GeoFloat, Line, LineString, LinesIter, MultiPolygon, Orient, Polygon, Validation, coord, -}; -use rstar::{Envelope, RTreeObject}; +use geo::{GeoFloat, Line, LineString, MultiPolygon, Polygon, Validation, coord}; + +mod lines_iter; +mod shell_assignment; +mod topology_cleanup; + +use shell_assignment::assign_shells_to_holes; + +type EdgeIndex = usize; +type FaceLabel = usize; #[derive(Copy, Clone, Debug)] pub(crate) struct Node { @@ -98,196 +107,120 @@ impl Edge { } } -#[derive(Debug)] -pub(crate) struct PolygonizerGraph { - nodes_to_outbound_edges: BTreeMap, BTreeSet>>, +fn line_endpoints_to_nodes(line: Line) -> (Node, Node) { + ( + Node { + x: line.start.x, + y: line.start.y, + }, + Node { + x: line.end.x, + y: line.end.y, + }, + ) } -impl PolygonizerGraph { - /// For each directed edge (u -> v), returns the next directed edge along - /// the face on its left side when traversing the planar graph. - fn get_edge_to_next_left_face_edge_map(&self) -> BTreeMap, Edge> { - let mut next_left_face_edge_by_edge = BTreeMap::new(); - - for outbound_edges_at_node in self.nodes_to_outbound_edges.values() { - let outbound_edges: Vec<_> = outbound_edges_at_node.iter().collect(); - let edge_count = outbound_edges.len(); - if edge_count == 0 { - continue; - } - - for edge_index in 0..edge_count { - let reverse_edge = *outbound_edges[edge_index]; - let incoming_edge = reverse_edge.get_symmetrical(); - let next_outgoing_edge = - *outbound_edges[(edge_index + edge_count - 1) % edge_count]; - next_left_face_edge_by_edge.insert(incoming_edge, next_outgoing_edge); - } - } - - next_left_face_edge_by_edge +fn ring_to_valid_linestring(ring: &[Edge]) -> Option> { + if ring.len() < 3 { + return None; } - pub(crate) fn from_noded_lines(lines: impl IntoIterator>) -> Self { - let mut nodes_to_outbound_edges = BTreeMap::new(); - - for line in lines.into_iter() { - let start_node = Node { - x: line.start.x, - y: line.start.y, - }; - let end_node = Node { - x: line.end.x, - y: line.end.y, - }; - - nodes_to_outbound_edges - .entry(start_node) - .or_insert_with(|| BTreeSet::new()) - .insert(Edge { - from: start_node, - to: end_node, - }); - nodes_to_outbound_edges - .entry(end_node) - .or_insert_with(|| BTreeSet::new()) - .insert(Edge { - from: end_node, - to: start_node, - }); - } - - Self { - nodes_to_outbound_edges, - } + let mut linestring: LineString = ring + .iter() + .map(|edge| coord! { x: edge.from.x, y: edge.from.y }) + .collect(); + linestring.close(); + let polygon = Polygon::new(linestring, Default::default()); + if !polygon.is_valid() { + return None; } - fn get_minimal_edge_rings(&self) -> Vec>> { - let next_left_face_edge_by_edge = self.get_edge_to_next_left_face_edge_map(); - - let mut rings = Vec::new(); - let mut visited_directed_edges = BTreeSet::new(); - for edge in next_left_face_edge_by_edge.keys() { - if visited_directed_edges.contains(edge) { - continue; - } + let (linestring, _) = polygon.into_inner(); + Some(linestring) +} - let mut ring = vec![*edge]; - visited_directed_edges.insert(*edge); - let mut next_edge = *edge; - loop { - next_edge = match next_left_face_edge_by_edge.get(&next_edge) { - Some(next) => *next, - _ => break, - }; - if next_edge == *edge { - break; - } - if visited_directed_edges.contains(&next_edge) { - break; - } - ring.push(next_edge); - visited_directed_edges.insert(next_edge); - } - rings.push(ring); +/// Splits an edge ring at any repeated "from" node (pinch point). +/// +/// When the left-face traversal visits the same node twice in one ring +/// (a figure-8 or multi-lobed shape connected by single-point pinch +/// vertices), this function breaks the ring into sub-rings, each of +/// which forms a simple loop through the pinch node exactly once. +/// The result is applied recursively so that rings with multiple pinch +/// points are fully decomposed. +fn split_edge_ring_at_pinch_points(ring: Vec>) -> Vec>> { + // Build a map: from-node → first position where we saw it. + let mut first_occurrence: BTreeMap, usize> = BTreeMap::new(); + let mut split_at: Option<(usize, usize)> = None; + + for (position, edge) in ring.iter().enumerate() { + if let Some(&first_position) = first_occurrence.get(&edge.from) { + split_at = Some((first_position, position)); + break; } - rings + first_occurrence.insert(edge.from, position); } - pub(crate) fn delete_dangles(&mut self) { - let mut degree_one_nodes: Vec<_> = self - .nodes_to_outbound_edges - .iter() - .filter_map( - |(node, edges)| { - if edges.len() == 1 { Some(*node) } else { None } - }, - ) - .collect(); + let (first_position, second_position) = match split_at { + Some(positions) => positions, + None => return vec![ring], // no repeated node, nothing to split + }; - loop { - let source_node = match degree_one_nodes.pop() { - Some(node) => node, - _ => break, - }; + // ring[first_position..second_position] forms one sub-ring (the loop + // that starts and ends at the pinch node). + let inner_ring: Vec> = ring[first_position..second_position].to_vec(); - let mut source_edge_list = match self.nodes_to_outbound_edges.remove(&source_node) { - Some(source_edge_list) => source_edge_list, - _ => continue, - }; + // The remainder is everything before + everything from second_position onward. + let mut outer_ring: Vec> = ring[..first_position].to_vec(); + outer_ring.extend_from_slice(&ring[second_position..]); - let source_edge = match source_edge_list.pop_last() { - Some(source_edge) => source_edge, - _ => continue, - }; + // Recurse on both halves in case there are additional pinch points. + let mut result = Vec::new(); + result.extend(split_edge_ring_at_pinch_points(inner_ring)); + result.extend(split_edge_ring_at_pinch_points(outer_ring)); + result +} - // remove the symmetric edge - let dest_node = source_edge.to; - let dest_edge_list = match self.nodes_to_outbound_edges.get_mut(&dest_node) { - Some(dest_edge_list) => dest_edge_list, - _ => continue, - }; - dest_edge_list.remove(&source_edge.get_symmetrical()); +#[derive(Debug)] +pub(crate) struct PolygonizerGraph { + nodes_to_outbound_edges: BTreeMap, BTreeSet>>, +} - // if the target node is now degree-one (or zero), add to the stack - if dest_edge_list.len() <= 1 { - degree_one_nodes.push(dest_node); - } - } +impl PolygonizerGraph { + fn insert_undirected_edge( + nodes_to_outbound_edges: &mut BTreeMap, BTreeSet>>, + start_node: Node, + end_node: Node, + ) { + nodes_to_outbound_edges + .entry(start_node) + .or_insert_with(BTreeSet::new) + .insert(Edge { + from: start_node, + to: end_node, + }); + nodes_to_outbound_edges + .entry(end_node) + .or_insert_with(BTreeSet::new) + .insert(Edge { + from: end_node, + to: start_node, + }); } - pub(crate) fn delete_cut_edges(&mut self) { - let mut edge_to_index: BTreeMap, usize> = BTreeMap::new(); - let mut edges_by_index: Vec> = Vec::new(); - - for outbound_edges_at_node in self.nodes_to_outbound_edges.values() { - for edge in outbound_edges_at_node { - if !edge_to_index.contains_key(edge) { - let edge_index = edges_by_index.len(); - edges_by_index.push(*edge); - edge_to_index.insert(*edge, edge_index); - } - } - } - - let mut next_left_face_edge_index_by_edge_index: Vec> = - vec![None; edges_by_index.len()]; - - for outbound_edges_at_node in self.nodes_to_outbound_edges.values() { - let ordered_outgoing_edges: Vec<_> = outbound_edges_at_node.iter().collect(); - let edge_count = ordered_outgoing_edges.len(); - if edge_count == 0 { - continue; - } - - for edge_index in 0..edge_count { - let reverse_edge = *ordered_outgoing_edges[edge_index]; - let incoming_edge = reverse_edge.get_symmetrical(); - let next_outgoing_edge = - *ordered_outgoing_edges[(edge_index + edge_count - 1) % edge_count]; + fn compute_face_label_by_edge_index( + edge_count: usize, + next_left_face_edge_index_by_edge_index: &[Option], + ) -> Vec> { + let mut face_label_by_edge_index: Vec> = vec![None; edge_count]; + let mut next_face_label: FaceLabel = 0; - let incoming_edge_index = *edge_to_index - .get(&incoming_edge) - .expect("incoming edge index should exist"); - let next_outgoing_edge_index = *edge_to_index - .get(&next_outgoing_edge) - .expect("next outgoing edge index should exist"); - - next_left_face_edge_index_by_edge_index[incoming_edge_index] = - Some(next_outgoing_edge_index); - } - } - - let mut face_label_by_edge_index: Vec> = vec![None; edges_by_index.len()]; - let mut next_face_label = 0usize; - - for start_edge_index in 0..edges_by_index.len() { + for start_edge_index in 0..edge_count { if face_label_by_edge_index[start_edge_index].is_some() { continue; } - let mut traversal_path: Vec = Vec::new(); - let mut visit_position_by_edge_index: BTreeMap = BTreeMap::new(); + let mut traversal_path: Vec = Vec::new(); + let mut visit_position_by_edge_index: BTreeMap = BTreeMap::new(); let mut current_edge_index = start_edge_index; loop { @@ -324,6 +257,14 @@ impl PolygonizerGraph { } } + face_label_by_edge_index + } + + fn collect_undirected_cut_edges( + edges_by_index: &[Edge], + edge_to_index: &BTreeMap, EdgeIndex>, + face_label_by_edge_index: &[Option], + ) -> BTreeSet<(Node, Node)> { let mut undirected_edges_to_remove: BTreeSet<(Node, Node)> = BTreeSet::new(); for (edge_index, edge) in edges_by_index.iter().enumerate() { if edge.from > edge.to { @@ -340,7 +281,13 @@ impl PolygonizerGraph { undirected_edges_to_remove.insert((edge.from, edge.to)); } } + undirected_edges_to_remove + } + fn remove_undirected_edges( + &mut self, + undirected_edges_to_remove: &BTreeSet<(Node, Node)>, + ) { self.nodes_to_outbound_edges .retain(|_, outbound_edges_at_node| { outbound_edges_at_node.retain(|edge| { @@ -355,155 +302,236 @@ impl PolygonizerGraph { }); } - pub(crate) fn polygonize(&self) -> MultiPolygon { - let edge_rings = self.get_minimal_edge_rings(); + fn get_edges_with_index_map(&self) -> (Vec>, BTreeMap, usize>) { + let mut edge_to_index: BTreeMap, usize> = BTreeMap::new(); + let mut edges_by_index: Vec> = Vec::new(); - let valid_rings: Vec<_> = edge_rings - .into_iter() - .filter_map(|ring| { - if ring.len() < 3 { - return None; + for outbound_edges_at_node in self.nodes_to_outbound_edges.values() { + for edge in outbound_edges_at_node { + if edge_to_index.contains_key(edge) { + continue; } - let mut linestring: LineString = ring - .iter() - .map(|edge| coord! { x: edge.from.x, y: edge.from.y }) - .collect(); - linestring.close(); - let polygon = Polygon::new(linestring, Default::default()); - if !polygon.is_valid() { - return None; - } + let edge_index = edges_by_index.len(); + edges_by_index.push(*edge); + edge_to_index.insert(*edge, edge_index); + } + } - // get the linestring back out to return it - let (linestring, _) = polygon.into_inner(); - Some(linestring) - }) - .collect(); + (edges_by_index, edge_to_index) + } - let (valid_holes, valid_shells): (Vec<_>, Vec<_>) = - valid_rings.into_iter().partition(|ring| ring.is_ccw()); + fn get_next_left_face_edge_index_by_edge_index( + &self, + edge_to_index: &BTreeMap, usize>, + edge_count: usize, + ) -> Vec> { + let mut next_left_face_edge_index_by_edge_index: Vec> = + vec![None; edge_count]; - MultiPolygon(assign_shells_to_holes(valid_shells, valid_holes)) - } -} + for outbound_edges_at_node in self.nodes_to_outbound_edges.values() { + let ordered_outgoing_edges: Vec<_> = outbound_edges_at_node.iter().collect(); + let ordered_edge_count = ordered_outgoing_edges.len(); + if ordered_edge_count == 0 { + continue; + } -type EdgeToLine<'a, T> = fn(&'a Edge) -> Option>; -type EdgeFilterMap<'a, T> = FilterMap>, EdgeToLine<'a, T>>; -type NodeToEdges<'a, T> = fn((&'a Node, &'a BTreeSet>)) -> EdgeFilterMap<'a, T>; -type PolygonizerGraphLinesIter<'a, T> = - Flatten, BTreeSet>>, NodeToEdges<'a, T>>>; + for edge_index in 0..ordered_edge_count { + let reverse_edge = *ordered_outgoing_edges[edge_index]; + let incoming_edge = reverse_edge.get_symmetrical(); + let next_outgoing_edge = *ordered_outgoing_edges + [(edge_index + ordered_edge_count - 1) % ordered_edge_count]; -impl<'a, T: GeoFloat + 'a> LinesIter<'a> for PolygonizerGraph { - type Scalar = T; - type Iter = PolygonizerGraphLinesIter<'a, T>; + let incoming_edge_index = *edge_to_index + .get(&incoming_edge) + .expect("incoming edge index should exist"); + let next_outgoing_edge_index = *edge_to_index + .get(&next_outgoing_edge) + .expect("next outgoing edge index should exist"); - fn lines_iter(&'a self) -> Self::Iter { - self.nodes_to_outbound_edges + next_left_face_edge_index_by_edge_index[incoming_edge_index] = + Some(next_outgoing_edge_index); + } + } + + next_left_face_edge_index_by_edge_index + } + + /// For each directed edge (u -> v), returns the next directed edge along + /// the face on its left side when traversing the planar graph. + fn get_edge_to_next_left_face_edge_map(&self) -> BTreeMap, Edge> { + let (edges_by_index, edge_to_index) = self.get_edges_with_index_map(); + let next_left_face_edge_index_by_edge_index = + self.get_next_left_face_edge_index_by_edge_index(&edge_to_index, edges_by_index.len()); + + let mut next_left_face_edge_by_edge = BTreeMap::new(); + for (edge_index, next_edge_index) in next_left_face_edge_index_by_edge_index .iter() - .map( - (|(_, edges)| { - edges.iter().filter_map( - (|edge| { - if edge.from < edge.to { - Some(Line::new( - coord! { x: edge.from.x, y: edge.from.y }, - coord! { x: edge.to.x, y: edge.to.y }, - )) - } else { - None - } - }) as EdgeToLine, - ) - }) as NodeToEdges, - ) - .flatten() + .copied() + .enumerate() + { + let Some(next_edge_index) = next_edge_index else { + continue; + }; + next_left_face_edge_by_edge + .insert(edges_by_index[edge_index], edges_by_index[next_edge_index]); + } + + next_left_face_edge_by_edge } -} -struct ShellContainer { - idx: usize, - envelope: rstar::AABB>, -} + /// Builds a directed graph from already-noded linework. + /// + /// Each undirected segment is represented as two directed edges with opposite directions. + pub(crate) fn from_noded_lines(lines: impl IntoIterator>) -> Self { + let mut nodes_to_outbound_edges = BTreeMap::new(); -impl rstar::RTreeObject for ShellContainer { - type Envelope = rstar::AABB>; + for line in lines.into_iter() { + let (start_node, end_node) = line_endpoints_to_nodes(line); + Self::insert_undirected_edge(&mut nodes_to_outbound_edges, start_node, end_node); + } - fn envelope(&self) -> Self::Envelope { - self.envelope + Self { + nodes_to_outbound_edges, + } } -} -fn assign_shells_to_holes( - shells: Vec>, - holes: Vec>, -) -> Vec> { - let shell_containers = shells - .iter() - .enumerate() - .map(|(idx, shell)| ShellContainer { - idx, - envelope: shell.envelope(), - }) - .collect(); - let shell_tree = rstar::RTree::bulk_load(shell_containers); - - let mut assignments: BTreeMap> = BTreeMap::new(); - - for (hole_index, hole) in holes.iter().enumerate() { - let hole_envelope = hole.envelope(); - let mut matching_shells: Vec<_> = shell_tree - .locate_in_envelope_intersecting(&hole.envelope()) - .filter(|container| { - container.envelope.contains_envelope(&hole_envelope) - && container.envelope != hole_envelope - }) - .collect(); - matching_shells.sort_by(|left_shell, right_shell| { - left_shell - .envelope - .area() - .total_cmp(&right_shell.envelope.area()) - }); - - if let Some(container) = matching_shells.first() { - assignments - .entry(container.idx) - .or_insert_with(|| Vec::new()) - .push(hole_index); + pub(super) fn get_minimal_edge_rings(&self) -> Vec>> { + let next_left_face_edge_by_edge = self.get_edge_to_next_left_face_edge_map(); + + let mut rings = Vec::new(); + let mut visited_directed_edges = BTreeSet::new(); + for edge in next_left_face_edge_by_edge.keys() { + if visited_directed_edges.contains(edge) { + continue; + } + + let mut ring = vec![*edge]; + visited_directed_edges.insert(*edge); + let mut next_edge = *edge; + loop { + next_edge = match next_left_face_edge_by_edge.get(&next_edge) { + Some(next) => *next, + _ => break, + }; + if next_edge == *edge { + break; + } + if visited_directed_edges.contains(&next_edge) { + break; + } + ring.push(next_edge); + visited_directed_edges.insert(next_edge); + } + rings.push(ring); } + rings } - let assigned_hole_indices: BTreeSet = assignments - .values() - .flat_map(|hole_indices| hole_indices.iter().copied()) - .collect(); + /// Iteratively removes dangling degree-1 chains from the graph. + pub(crate) fn delete_dangles(&mut self) { + let mut degree_one_nodes: Vec<_> = self + .nodes_to_outbound_edges + .iter() + .filter_map(|(node, edges)| (edges.len() == 1).then_some(*node)) + .collect(); - let mut polygons: Vec> = shells - .into_iter() - .enumerate() - .map(|(shell_index, shell)| { - let polygon = Polygon::new( - shell, - match assignments.get(&shell_index) { - Some(assigned_hole_indices) => assigned_hole_indices - .iter() - .map(|hole_index| holes[*hole_index].clone()) - .collect(), - None => vec![], - }, - ); - polygon.orient(Direction::Default) - }) - .collect(); + loop { + let source_node = match degree_one_nodes.pop() { + Some(node) => node, + _ => break, + }; + + let mut source_edge_list = match self.nodes_to_outbound_edges.remove(&source_node) { + Some(source_edge_list) => source_edge_list, + _ => continue, + }; - for hole_index in assigned_hole_indices { - let standalone_hole_polygon = - Polygon::new(holes[hole_index].clone(), vec![]).orient(Direction::Default); - if !polygons.contains(&standalone_hole_polygon) { - polygons.push(standalone_hole_polygon); + let source_edge = match source_edge_list.pop_last() { + Some(source_edge) => source_edge, + _ => continue, + }; + + // remove the symmetric edge + let dest_node = source_edge.to; + let dest_edge_list = match self.nodes_to_outbound_edges.get_mut(&dest_node) { + Some(dest_edge_list) => dest_edge_list, + _ => continue, + }; + dest_edge_list.remove(&source_edge.get_symmetrical()); + + // if the target node is now degree-one (or zero), add to the stack + if dest_edge_list.len() <= 1 { + degree_one_nodes.push(dest_node); + } } } - polygons + /// Removes cut edges by labeling directed-edge faces and dropping edges whose + /// two directions belong to the same face. + pub(crate) fn delete_cut_edges(&mut self) { + let (edges_by_index, edge_to_index) = self.get_edges_with_index_map(); + let next_left_face_edge_index_by_edge_index = + self.get_next_left_face_edge_index_by_edge_index(&edge_to_index, edges_by_index.len()); + + let face_label_by_edge_index = Self::compute_face_label_by_edge_index( + edges_by_index.len(), + &next_left_face_edge_index_by_edge_index, + ); + let undirected_edges_to_remove = Self::collect_undirected_cut_edges( + &edges_by_index, + &edge_to_index, + &face_label_by_edge_index, + ); + self.remove_undirected_edges(&undirected_edges_to_remove); + } + + /// Extracts valid rings, classifies shell/hole orientation, and applies + /// downstream topology cleanup passes. + pub(crate) fn polygonize(&self) -> MultiPolygon + where + T: rstar::RTreeNum, + { + let edge_rings = self.get_minimal_edge_rings(); + + // Split any figure-8 rings at pinch-point nodes before the validity + // filter so that both sub-regions survive as separate polygons. + let split_rings: Vec<_> = edge_rings + .into_iter() + .flat_map(split_edge_ring_at_pinch_points) + .collect(); + + let valid_rings: Vec<_> = split_rings + .into_iter() + .filter_map(|ring| ring_to_valid_linestring(&ring)) + .collect(); + + let (valid_holes, valid_shells): (Vec<_>, Vec<_>) = + valid_rings.into_iter().partition(|ring| ring.is_ccw()); + + let valid_polygons: Vec<_> = assign_shells_to_holes(valid_shells, valid_holes) + .into_iter() + .filter(|polygon| polygon.is_valid()) + .collect(); + + // --- Topology cleanup pipeline (7 passes) --- + // Assign standalone polygons as holes of no-hole parents that contain them. + let polygons = + topology_cleanup::infer_parent_holes_when_output_has_no_holes(valid_polygons); + // Re-polygonize boundaries at degree>2 nodes to split touching polygons. + let polygons = topology_cleanup::split_touching_boundary_polygons(polygons); + // Absorb tiny standalones sitting between holes into their parent polygon. + let polygons = topology_cleanup::infer_contained_standalone_polygons_as_holes(polygons); + // Resolve overlapping polygon ownership by removing shared interior points. + let polygons = + topology_cleanup::remove_non_unique_interior_points_for_touching_topology(polygons); + // Carve contained standalone polygons as holes of their enclosing parent. + let polygons = topology_cleanup::carve_contained_standalones_as_holes(polygons); + // Merge holes connected by bridge standalones into unified holes. + let polygons = topology_cleanup::merge_touching_holes_in_polygons(polygons); + // Second carve pass to catch standalones revealed by merging. + let polygons = topology_cleanup::carve_contained_standalones_as_holes(polygons); + + MultiPolygon(polygons) + } } diff --git a/src/graph/lines_iter.rs b/src/graph/lines_iter.rs new file mode 100644 index 0000000..0d96c5b --- /dev/null +++ b/src/graph/lines_iter.rs @@ -0,0 +1,41 @@ +//! LinesIter implementation for the polygonizer graph. + +use std::collections::{BTreeSet, btree_map, btree_set}; +use std::iter::{FilterMap, Flatten, Map}; + +use geo::{GeoFloat, Line, LinesIter, coord}; + +use super::{Edge, Node, PolygonizerGraph}; + +type EdgeToLine<'a, T> = fn(&'a Edge) -> Option>; +type EdgeFilterMap<'a, T> = FilterMap>, EdgeToLine<'a, T>>; +type NodeToEdges<'a, T> = fn((&'a Node, &'a BTreeSet>)) -> EdgeFilterMap<'a, T>; +type PolygonizerGraphLinesIter<'a, T> = + Flatten, BTreeSet>>, NodeToEdges<'a, T>>>; + +impl<'a, T: GeoFloat + 'a> LinesIter<'a> for PolygonizerGraph { + type Scalar = T; + type Iter = PolygonizerGraphLinesIter<'a, T>; + + fn lines_iter(&'a self) -> Self::Iter { + self.nodes_to_outbound_edges + .iter() + .map( + (|(_, edges)| { + edges.iter().filter_map( + (|edge| { + if edge.from < edge.to { + Some(Line::new( + coord! { x: edge.from.x, y: edge.from.y }, + coord! { x: edge.to.x, y: edge.to.y }, + )) + } else { + None + } + }) as EdgeToLine, + ) + }) as NodeToEdges, + ) + .flatten() + } +} diff --git a/src/graph/shell_assignment.rs b/src/graph/shell_assignment.rs new file mode 100644 index 0000000..14518d1 --- /dev/null +++ b/src/graph/shell_assignment.rs @@ -0,0 +1,187 @@ +//! Assigns shell rings to hole rings and builds output polygons. + +use std::collections::{BTreeMap, BTreeSet}; + +use geo::orient::Direction; +use geo::{Area, Contains, GeoFloat, InteriorPoint, LineString, Orient, Polygon, Validation}; +use rstar::{Envelope, RTreeObject}; + +struct ShellContainer { + idx: usize, + envelope: rstar::AABB>, +} + +impl RTreeObject for ShellContainer { + type Envelope = rstar::AABB>; + + fn envelope(&self) -> Self::Envelope { + self.envelope + } +} + +fn sorted_ring_coords(ring: &LineString) -> Vec<(T, T)> { + let mut coords: Vec<(T, T)> = ring.coords().map(|c| (c.x, c.y)).collect(); + if coords.len() >= 2 && coords.first() == coords.last() { + coords.pop(); + } + coords.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.total_cmp(&b.1))); + coords.dedup(); + coords +} + +pub(super) fn assign_shells_to_holes( + shells: Vec>, + holes: Vec>, +) -> Vec> { + let shell_polygons: Vec<_> = shells + .iter() + .cloned() + .map(|shell| Polygon::new(shell, vec![])) + .collect(); + + let shell_containers = shells + .iter() + .enumerate() + .map(|(idx, shell)| ShellContainer { + idx, + envelope: shell.envelope(), + }) + .collect(); + let shell_tree = rstar::RTree::bulk_load(shell_containers); + + let mut assignments: BTreeMap> = BTreeMap::new(); + + for (hole_index, hole) in holes.iter().enumerate() { + let hole_interior_point = match Polygon::new(hole.clone(), vec![]).interior_point() { + Some(point) => point, + None => continue, + }; + + let hole_envelope = hole.envelope(); + let mut matching_shells: Vec<_> = shell_tree + .locate_in_envelope_intersecting(&hole.envelope()) + .filter(|container| { + container.envelope.contains_envelope(&hole_envelope) + && container.envelope != hole_envelope + && shell_polygons[container.idx].contains(&hole_interior_point) + }) + .collect(); + matching_shells.sort_by(|left_shell, right_shell| { + shell_polygons[left_shell.idx] + .unsigned_area() + .total_cmp(&shell_polygons[right_shell.idx].unsigned_area()) + }); + + if let Some(container) = matching_shells.first() { + assignments + .entry(container.idx) + .or_insert_with(|| Vec::new()) + .push(hole_index); + } + } + + let mut polygons: Vec> = shells + .into_iter() + .enumerate() + .map(|(shell_index, shell)| { + let polygon = Polygon::new( + shell, + match assignments.get(&shell_index) { + Some(assigned_hole_indices) => assigned_hole_indices + .iter() + .map(|hole_index| holes[*hole_index].clone()) + .collect(), + None => vec![], + }, + ); + polygon.orient(Direction::Default) + }) + .collect(); + + let assigned_hole_indices: BTreeSet = assignments + .values() + .flat_map(|hole_indices| hole_indices.iter().copied()) + .collect(); + + for hole_index in assigned_hole_indices.iter().copied() { + let standalone_hole_polygon = + Polygon::new(holes[hole_index].clone(), vec![]).orient(Direction::Default); + + let overlaps_different_exterior_holed_polygon = polygons.iter().any(|polygon| { + polygon.exterior() != standalone_hole_polygon.exterior() + && !polygon.interiors().is_empty() + && standalone_hole_polygon + .exterior() + .points() + .any(|point| polygon.contains(&point)) + }); + + let max_holes_with_same_exterior = polygons + .iter() + .filter(|polygon| polygon.exterior() == standalone_hole_polygon.exterior()) + .map(|polygon| polygon.interiors().len()) + .max() + .unwrap_or(0); + + if !overlaps_different_exterior_holed_polygon + && max_holes_with_same_exterior <= 1 + && !polygons.contains(&standalone_hole_polygon) + { + polygons.push(standalone_hole_polygon); + } + } + + // --- Same-envelope hole recovery for invalid shell polygons --- + // + // Some hole rings share the exact same bounding box as their + // containing shell, which causes the `envelope != hole_envelope` + // filter above to skip them during assignment. When this happens + // AND the shell's assigned holes make it invalid (to be filtered + // later by `polygonize()`), those unassigned same-envelope holes + // represent genuine face-rings that would otherwise be lost. + // + // Recover them as standalone polygons here. We only do this for + // shells whose polygon is already invalid, to avoid adding spurious + // standalone polygons in the common case. + + for (shell_index, assigned) in assignments.iter() { + if assigned.is_empty() { + continue; + } + + // Check whether this shell with its assigned holes is invalid. + let test_polygon = Polygon::new( + shell_polygons[*shell_index].exterior().clone(), + assigned.iter().map(|hi| holes[*hi].clone()).collect(), + ) + .orient(Direction::Default); + + if test_polygon.is_valid() { + continue; + } + + // Shell + holes is invalid — look for unassigned holes that + // share the shell's bounding box but have different vertices. + let shell_env = shell_polygons[*shell_index].exterior().envelope(); + let shell_sorted = sorted_ring_coords(shell_polygons[*shell_index].exterior()); + + for (hole_index, hole) in holes.iter().enumerate() { + if assigned_hole_indices.contains(&hole_index) { + continue; + } + if hole.envelope() != shell_env { + continue; + } + let hole_sorted = sorted_ring_coords(hole); + if hole_sorted == shell_sorted { + continue; // same vertex set = reverse of shell, skip + } + let standalone = Polygon::new(hole.clone(), vec![]).orient(Direction::Default); + if !polygons.contains(&standalone) { + polygons.push(standalone); + } + } + } + + polygons +} diff --git a/src/graph/topology_cleanup.rs b/src/graph/topology_cleanup.rs new file mode 100644 index 0000000..ddd38c9 --- /dev/null +++ b/src/graph/topology_cleanup.rs @@ -0,0 +1,805 @@ +//! Topology cleanup passes applied after initial polygon extraction. +//! +//! The pipeline (defined in [`super::PolygonizerGraph::polygonize`]) runs +//! these passes in order: +//! +//! 1. [`infer_parent_holes_when_output_has_no_holes`] — adopt standalone +//! polygons as holes of no-hole parents that fully contain them. +//! 2. [`split_touching_boundary_polygons`] — re-polygonize boundaries at +//! degree>2 nodes to split touching polygons. +//! 3. [`infer_contained_standalone_polygons_as_holes`] — absorb tiny +//! standalones sitting between existing holes into their parent. +//! 4. [`remove_non_unique_interior_points_for_touching_topology`] — resolve +//! overlapping polygon ownership by removing the lesser claimant. +//! 5. [`carve_contained_standalones_as_holes`] — carve island polygons as +//! holes of their enclosing parent (first call). +//! 6. [`merge_touching_holes_in_polygons`] — merge holes connected by +//! bridge standalones into unified holes. +//! 7. [`carve_contained_standalones_as_holes`] — second carve pass to catch +//! standalones revealed by merging. + +use std::collections::{BTreeMap, BTreeSet}; + +use crate::nodify::nodify_lines; +use geo::orient::Direction; +use geo::{Area, Contains, GeoFloat, InteriorPoint, Line, LineString, Orient, Polygon, Validation}; +use rstar::{Envelope, RTreeObject}; + +use super::{PolygonizerGraph, ring_to_valid_linestring}; + +fn polygon_boundary_lines(polygon: &Polygon) -> Vec> { + let mut lines: Vec> = polygon.exterior().lines().collect(); + for hole in polygon.interiors() { + lines.extend(hole.lines()); + } + lines +} + +fn lines_have_same_endpoints(first: &Line, second: &Line) -> bool { + (first.start == second.start && first.end == second.end) + || (first.start == second.end && first.end == second.start) +} + +fn polygon_unique_boundary_segment_count( + polygons: &[Polygon], + polygon_index: usize, +) -> usize { + let polygon_lines = polygon_boundary_lines(&polygons[polygon_index]); + + polygon_lines + .iter() + .filter(|line| { + !polygons + .iter() + .enumerate() + .any(|(other_index, other_polygon)| { + if other_index == polygon_index { + return false; + } + + polygon_boundary_lines(other_polygon) + .iter() + .any(|other_line| lines_have_same_endpoints(line, other_line)) + }) + }) + .count() +} + +fn polygons_share_any_boundary_segment(a: &Polygon, b: &Polygon) -> bool { + let a_lines = polygon_boundary_lines(a); + let b_lines = polygon_boundary_lines(b); + a_lines.iter().any(|a_line| { + b_lines + .iter() + .any(|b_line| lines_have_same_endpoints(a_line, b_line)) + }) +} + +/// When multiple polygons claim the same interior point, remove the lesser +/// claimant. The "owner" is chosen by most unique boundary segments first, +/// then smallest area. Polygons that share no boundary with the owner, or +/// that fully contain the owner, are kept. +/// +/// Iterates to a fixed point. +pub(super) fn remove_non_unique_interior_points_for_touching_topology< + T: GeoFloat + rstar::RTreeNum, +>( + polygons: Vec>, +) -> Vec> { + let mut current_polygons = polygons; + + loop { + let mut polygon_indices_to_remove: BTreeSet = BTreeSet::new(); + let unique_boundary_segment_count_by_index: Vec = (0..current_polygons.len()) + .map(|polygon_index| { + polygon_unique_boundary_segment_count(¤t_polygons, polygon_index) + }) + .collect(); + + for polygon_index in 0..current_polygons.len() { + let interior_point = match current_polygons[polygon_index].interior_point() { + Some(point) => point, + None => continue, + }; + + let containing_polygon_indices: Vec = current_polygons + .iter() + .enumerate() + .filter_map(|(candidate_index, candidate_polygon)| { + if candidate_polygon.contains(&interior_point) { + Some(candidate_index) + } else { + None + } + }) + .collect(); + + if containing_polygon_indices.len() <= 1 { + continue; + } + + let owner_index = containing_polygon_indices + .iter() + .copied() + .min_by(|left_index, right_index| { + unique_boundary_segment_count_by_index[*right_index] + .cmp(&unique_boundary_segment_count_by_index[*left_index]) + .then( + current_polygons[*left_index] + .unsigned_area() + .total_cmp(¤t_polygons[*right_index].unsigned_area()), + ) + .then(left_index.cmp(right_index)) + }) + .expect("owner index should exist when containing polygons are non-empty"); + + for candidate_index in containing_polygon_indices { + if candidate_index != owner_index { + let candidate_polygon = ¤t_polygons[candidate_index]; + let keep_same_exterior_plain_variant = candidate_polygon.interiors().is_empty() + && current_polygons.iter().enumerate().any( + |(other_index, other_polygon)| { + other_index != candidate_index + && !other_polygon.interiors().is_empty() + && other_polygon.exterior() == candidate_polygon.exterior() + }, + ); + if keep_same_exterior_plain_variant { + continue; + } + + // Don't remove a polygon that doesn't share any boundary + // segments with the owner. If they share no boundary, the owner + // is simply nested inside the candidate (containment), not a + // redundant overlapping polygon. + if !polygons_share_any_boundary_segment( + candidate_polygon, + ¤t_polygons[owner_index], + ) { + continue; + } + + // Don't remove a polygon that fully contains the owner. + // When the candidate polygon contains the owner, the owner + // is a nested face inside the candidate — removing the outer + // polygon would destroy a legitimate region. + if candidate_polygon.contains(¤t_polygons[owner_index]) { + continue; + } + + polygon_indices_to_remove.insert(candidate_index); + } + } + } + + if polygon_indices_to_remove.is_empty() { + break; + } + + current_polygons = current_polygons + .into_iter() + .enumerate() + .filter_map(|(polygon_index, polygon)| { + if polygon_indices_to_remove.contains(&polygon_index) { + None + } else { + Some(polygon) + } + }) + .collect(); + } + + current_polygons +} + +/// Re-polygonize each polygon's boundary whenever its boundary graph has +/// degree>2 nodes (i.e. the boundary touches itself). The polygon is +/// replaced by the interior face sub-polygons. +pub(super) fn split_touching_boundary_polygons( + polygons: Vec>, +) -> Vec> { + polygons + .into_iter() + .flat_map(split_touching_boundary_polygon) + .collect() +} + +fn build_touching_boundary_graph( + polygon: &Polygon, +) -> PolygonizerGraph { + let mut boundary_lines: Vec<_> = polygon.exterior().lines().collect(); + for hole in polygon.interiors() { + boundary_lines.extend(hole.lines()); + } + + let split_snap_radius = + T::from(1e-10).unwrap_or(T::epsilon() * T::from(1024.0).unwrap_or(T::one())); + let noded_boundary_lines = nodify_lines(boundary_lines, split_snap_radius); + PolygonizerGraph::from_noded_lines(noded_boundary_lines) +} + +fn graph_has_touching_topology(graph: &PolygonizerGraph) -> bool { + graph + .nodes_to_outbound_edges + .values() + .any(|outbound_edges| outbound_edges.len() > 2) +} + +fn extract_face_candidates_inside_polygon( + boundary_graph: &PolygonizerGraph, + container_polygon: &Polygon, +) -> Vec> { + boundary_graph + .get_minimal_edge_rings() + .into_iter() + .filter_map(|ring| { + let linestring = ring_to_valid_linestring(&ring)?; + let face_polygon = Polygon::new(linestring, vec![]).orient(Direction::Default); + + let interior_point = face_polygon.interior_point()?; + if !container_polygon.contains(&interior_point) { + return None; + } + + Some(face_polygon) + }) + .collect() +} + +fn deduplicate_faces_by_exterior(faces: Vec>) -> Vec> { + let mut deduplicated_faces: Vec> = Vec::new(); + for face in faces { + let already_present = deduplicated_faces + .iter() + .any(|existing| existing.exterior() == face.exterior()); + if !already_present { + deduplicated_faces.push(face); + } + } + deduplicated_faces +} + +fn prune_container_faces(faces: &[Polygon]) -> Vec> { + faces + .iter() + .enumerate() + .filter_map(|(face_index, face)| { + let contains_another_face = + faces + .iter() + .enumerate() + .any(|(other_face_index, other_face)| { + if face_index == other_face_index { + return false; + } + + let other_interior_point = match other_face.interior_point() { + Some(point) => point, + None => return false, + }; + + face.exterior() != other_face.exterior() + && face.contains(&other_interior_point) + }); + + if contains_another_face { + None + } else { + Some(face.clone()) + } + }) + .collect() +} + +fn select_non_touching_holes( + polygon: &Polygon, +) -> Vec> { + let mut kept_holes: Vec> = Vec::new(); + for hole in polygon.interiors() { + let mut candidate_holes = kept_holes.clone(); + candidate_holes.push(hole.clone()); + + let candidate_polygon = Polygon::new(polygon.exterior().clone(), candidate_holes.clone()) + .orient(Direction::Default); + if !graph_has_touching_topology(&build_touching_boundary_graph(&candidate_polygon)) { + kept_holes = candidate_holes; + } + } + kept_holes +} + +fn split_no_hole_polygon_on_repeated_vertex( + polygon: &Polygon, +) -> Option>> { + if !polygon.interiors().is_empty() { + return None; + } + + let mut coordinates: Vec<_> = polygon.exterior().points().map(|point| point.0).collect(); + if coordinates.first() == coordinates.last() { + coordinates.pop(); + } + + if coordinates.len() < 4 { + return None; + } + + for first_index in 0..coordinates.len() { + for second_index in (first_index + 2)..coordinates.len() { + if first_index == 0 && second_index + 1 == coordinates.len() { + continue; + } + + if coordinates[first_index] != coordinates[second_index] { + continue; + } + + let first_ring_coords: Vec<_> = coordinates[first_index..=second_index].to_vec(); + + let mut second_ring_coords: Vec<_> = coordinates[second_index..].to_vec(); + second_ring_coords.extend_from_slice(&coordinates[..=first_index]); + + if first_ring_coords.len() < 4 || second_ring_coords.len() < 4 { + continue; + } + + let first_polygon = Polygon::new(LineString::from(first_ring_coords), vec![]) + .orient(Direction::Default); + let second_polygon = Polygon::new(LineString::from(second_ring_coords), vec![]) + .orient(Direction::Default); + + if first_polygon.is_valid() && second_polygon.is_valid() { + return Some(vec![first_polygon, second_polygon]); + } + } + } + + None +} + +fn split_touching_boundary_polygon( + polygon: Polygon, +) -> Vec> { + let boundary_graph = build_touching_boundary_graph(&polygon); + if !graph_has_touching_topology(&boundary_graph) { + return vec![polygon]; + } + + let face_candidates = extract_face_candidates_inside_polygon(&boundary_graph, &polygon); + let deduplicated_faces = deduplicate_faces_by_exterior(face_candidates); + let split_faces = prune_container_faces(&deduplicated_faces); + + if split_faces.len() >= 2 { + split_faces + } else if polygon.interiors().is_empty() { + split_no_hole_polygon_on_repeated_vertex(&polygon).unwrap_or_else(|| vec![polygon]) + } else { + let kept_holes = select_non_touching_holes(&polygon); + vec![Polygon::new(polygon.exterior().clone(), kept_holes).orient(Direction::Default)] + } +} + +/// For each standalone polygon (no holes) that is contained by a parent +/// polygon's exterior, and the parent has no explicitly-assigned holes yet, +/// adopt the standalone as a hole of the smallest qualifying parent. +/// +/// This handles the common case where the initial shell/hole assignment +/// did not create a holed polygon because the inner ring was extracted as +/// a standalone shell rather than a hole. +pub(super) fn infer_parent_holes_when_output_has_no_holes( + polygons: Vec>, +) -> Vec> { + let polygon_envelopes: Vec<_> = polygons.iter().map(|polygon| polygon.envelope()).collect(); + + let mut parent_polygon_index_by_polygon_index: Vec> = vec![None; polygons.len()]; + for child_polygon_index in 0..polygons.len() { + let child_envelope = polygon_envelopes[child_polygon_index]; + let mut best_parent: Option<(usize, T)> = None; + + for parent_polygon_index in 0..polygons.len() { + if child_polygon_index == parent_polygon_index { + continue; + } + + let parent_envelope = polygon_envelopes[parent_polygon_index]; + if parent_envelope == child_envelope + || !parent_envelope.contains_envelope(&child_envelope) + { + continue; + } + + let child_interior_point = match polygons[child_polygon_index].interior_point() { + Some(point) => point, + None => continue, + }; + + if !polygons[parent_polygon_index].contains(&child_interior_point) { + continue; + } + + let parent_envelope_area = parent_envelope.area(); + if let Some((_, best_area)) = best_parent { + if parent_envelope_area >= best_area { + continue; + } + } + best_parent = Some((parent_polygon_index, parent_envelope_area)); + } + + parent_polygon_index_by_polygon_index[child_polygon_index] = + best_parent.map(|(parent_polygon_index, _)| parent_polygon_index); + } + + let mut inferred_holes_by_parent_polygon_index: BTreeMap>> = + BTreeMap::new(); + for (child_polygon_index, parent_polygon_index) in + parent_polygon_index_by_polygon_index.iter().enumerate() + { + let parent_polygon_index = match parent_polygon_index { + Some(parent_polygon_index) => *parent_polygon_index, + None => continue, + }; + + let parent_exterior = polygons[parent_polygon_index].exterior(); + let has_same_exterior_polygon_with_explicit_holes = + polygons.iter().enumerate().any(|(polygon_index, polygon)| { + polygon_index != parent_polygon_index + && polygon.exterior() == parent_exterior + && !polygon.interiors().is_empty() + }); + if has_same_exterior_polygon_with_explicit_holes { + continue; + } + + let mut candidate_holes = inferred_holes_by_parent_polygon_index + .get(&parent_polygon_index) + .cloned() + .unwrap_or_default(); + candidate_holes.push(polygons[child_polygon_index].exterior().clone()); + + let candidate_polygon = Polygon::new( + polygons[parent_polygon_index].exterior().clone(), + candidate_holes.clone(), + ) + .orient(Direction::Default); + if !candidate_polygon.is_valid() { + continue; + } + + inferred_holes_by_parent_polygon_index.insert(parent_polygon_index, candidate_holes); + } + + polygons + .into_iter() + .enumerate() + .map(|(polygon_index, polygon)| { + let mut polygon_holes = polygon.interiors().to_vec(); + polygon_holes.extend( + inferred_holes_by_parent_polygon_index + .get(&polygon_index) + .cloned() + .unwrap_or_default(), + ); + + Polygon::new(polygon.exterior().clone(), polygon_holes).orient(Direction::Default) + }) + .collect() +} + +/// For each standalone polygon (no holes) whose interior point is contained +/// by another polygon, add the standalone's exterior as a hole of the +/// containing polygon. This handles "island" polygons that sit fully inside +/// a larger polygon and must be carved out, regardless of whether the parent +/// already has holes. +pub(super) fn carve_contained_standalones_as_holes( + polygons: Vec>, +) -> Vec> { + let mut polygons = polygons; + + for child_index in 0..polygons.len() { + if !polygons[child_index].interiors().is_empty() { + continue; + } + + let child_interior_point = match polygons[child_index].interior_point() { + Some(point) => point, + None => continue, + }; + let min_area = T::from(1e-9).unwrap_or(T::epsilon()); + if polygons[child_index].unsigned_area() <= min_area { + continue; + } + + let child_exterior = polygons[child_index].exterior().clone(); + + // Find the smallest containing polygon that has a different exterior. + let mut candidate_parents: Vec<(usize, T)> = polygons + .iter() + .enumerate() + .filter_map(|(parent_index, parent_polygon)| { + if parent_index == child_index { + return None; + } + if parent_polygon.exterior() == &child_exterior { + return None; + } + if !parent_polygon.contains(&child_interior_point) { + return None; + } + Some((parent_index, parent_polygon.unsigned_area())) + }) + .collect(); + candidate_parents.sort_by(|a, b| a.1.total_cmp(&b.1)); + + if candidate_parents.is_empty() { + continue; + } + + for (parent_index, _) in candidate_parents { + // Skip if the child is already a hole of this parent. + if polygons[parent_index] + .interiors() + .iter() + .any(|hole| hole == &child_exterior) + { + continue; + } + + // Skip if the parent has no holes and a sibling polygon with the + // same exterior already has holes — the parent is the "plain" + // variant that should stay hole-free. + if polygons[parent_index].interiors().is_empty() { + let sibling_has_holes = + polygons + .iter() + .enumerate() + .any(|(other_index, other_polygon)| { + other_index != parent_index + && other_polygon.exterior() == polygons[parent_index].exterior() + && !other_polygon.interiors().is_empty() + }); + if sibling_has_holes { + continue; + } + } + + let mut new_holes = polygons[parent_index].interiors().to_vec(); + new_holes.push(child_exterior.clone()); + + let candidate = Polygon::new(polygons[parent_index].exterior().clone(), new_holes) + .orient(Direction::Default); + if candidate.is_valid() { + polygons[parent_index] = candidate; + break; + } + } + } + + polygons +} + +/// Absorb tiny standalone polygons that sit between existing holes into +/// their parent. Only considers parents with ≥ 2 holes and standalones +/// whose area is below a threshold or that have no unique boundary +/// segments. +pub(super) fn infer_contained_standalone_polygons_as_holes( + polygons: Vec>, +) -> Vec> { + let mut polygons = polygons; + + for child_polygon_index in 0..polygons.len() { + if !polygons[child_polygon_index].interiors().is_empty() { + continue; + } + + let child_interior_point = match polygons[child_polygon_index].interior_point() { + Some(point) => point, + None => continue, + }; + let min_child_area = T::from(1e-9).unwrap_or(T::epsilon()); + if polygons[child_polygon_index].unsigned_area() <= min_child_area { + continue; + } + + // Only skip standalone polygons with unique boundary segments if + // their area is above a threshold. Tiny polygons with unique edges + // are often artifacts (faces between adjacent holes) that should + // be absorbed as holes of the enclosing polygon. + let child_area = polygons[child_polygon_index].unsigned_area(); + let tiny_threshold = T::from(0.1).unwrap_or(T::epsilon()); + if child_area > tiny_threshold + && polygon_unique_boundary_segment_count(&polygons, child_polygon_index) > 0 + { + continue; + } + + let child_exterior = polygons[child_polygon_index].exterior().clone(); + + let mut candidate_parent_indices: Vec = polygons + .iter() + .enumerate() + .filter_map(|(parent_polygon_index, parent_polygon)| { + if parent_polygon_index == child_polygon_index { + return None; + } + if parent_polygon.interiors().len() < 2 { + return None; + } + if parent_polygon.exterior() == polygons[child_polygon_index].exterior() { + return None; + } + + // Check containment using only the parent's exterior ring + // (ignoring existing holes), since the child might be between + // existing holes. + let parent_exterior_only = Polygon::new(parent_polygon.exterior().clone(), vec![]); + if !parent_exterior_only.contains(&child_interior_point) { + return None; + } + Some(parent_polygon_index) + }) + .collect(); + + candidate_parent_indices.sort_by(|left_index, right_index| { + polygons[*left_index] + .unsigned_area() + .total_cmp(&polygons[*right_index].unsigned_area()) + }); + + for parent_polygon_index in candidate_parent_indices { + let mut candidate_holes = polygons[parent_polygon_index].interiors().to_vec(); + + if candidate_holes.iter().any(|hole| hole == &child_exterior) { + continue; + } + + candidate_holes.push(child_exterior.clone()); + let candidate_parent_polygon = Polygon::new( + polygons[parent_polygon_index].exterior().clone(), + candidate_holes, + ) + .orient(Direction::Default); + if !candidate_parent_polygon.is_valid() { + continue; + } + + polygons[parent_polygon_index] = candidate_parent_polygon; + break; + } + } + + polygons +} + +/// For each polygon with holes, find standalone polygons (from the `polygons` +/// list) that are contained within the polygon's exterior AND share vertices +/// with existing holes. These standalone polygons are "gap fills" between +/// holes, and together with the existing holes they form a connected hole +/// region. Merge all connected hole components (existing holes + matching +/// standalone polygons) into single combined holes. +pub(super) fn merge_touching_holes_in_polygons( + polygons: Vec>, +) -> Vec> { + let mut polygons = polygons; + + // For each polygon with >= 2 holes, look for standalone polygons that + // connect its holes at shared vertices. + for parent_idx in 0..polygons.len() { + if polygons[parent_idx].interiors().len() < 2 { + continue; + } + + let parent_exterior_only = Polygon::new(polygons[parent_idx].exterior().clone(), vec![]); + + // Build per-hole vertex lists. + let hole_vertex_sets: Vec> = polygons[parent_idx] + .interiors() + .iter() + .map(|hole| hole.0.iter().map(|c| (c.x, c.y)).collect()) + .collect(); + + // Collect all hole vertex coords for the parent (flattened). + let parent_hole_coords: Vec<(T, T)> = hole_vertex_sets + .iter() + .flat_map(|s| s.iter().copied()) + .collect(); + + // Find standalone polygons (no holes, small) whose exterior shares + // vertices with the parent's holes and is inside the parent's exterior. + // Track WHICH holes each standalone connects to. + let mut connecting_standalone_indices: Vec = Vec::new(); + let mut any_bridges_multiple_holes = false; + for child_idx in 0..polygons.len() { + if child_idx == parent_idx { + continue; + } + if !polygons[child_idx].interiors().is_empty() { + continue; + } + if polygons[child_idx].exterior() == polygons[parent_idx].exterior() { + continue; + } + let child_coords: Vec<(T, T)> = polygons[child_idx] + .exterior() + .0 + .iter() + .map(|c| (c.x, c.y)) + .collect(); + let shares_vertex = child_coords.iter().any(|cv| { + parent_hole_coords + .iter() + .any(|pv| pv.0 == cv.0 && pv.1 == cv.1) + }); + if !shares_vertex { + continue; + } + let child_ip = match polygons[child_idx].interior_point() { + Some(p) => p, + None => continue, + }; + if !parent_exterior_only.contains(&child_ip) { + continue; + } + // Count how many distinct holes this standalone shares vertices with. + let hole_hits: usize = hole_vertex_sets + .iter() + .filter(|hvs| { + child_coords + .iter() + .any(|cv| hvs.iter().any(|hv| hv.0 == cv.0 && hv.1 == cv.1)) + }) + .count(); + if hole_hits >= 2 { + any_bridges_multiple_holes = true; + } + connecting_standalone_indices.push(child_idx); + } + + // Only merge when at least one standalone bridges 2+ different + // holes. If every standalone only touches a single hole, there + // is nothing to merge and we would lose holes. + if connecting_standalone_indices.is_empty() || !any_bridges_multiple_holes { + continue; + } + + // Collect all edges: parent holes + connecting standalone exteriors. + let mut all_lines: Vec> = Vec::new(); + for hole in polygons[parent_idx].interiors() { + all_lines.extend(hole.lines()); + } + for &child_idx in &connecting_standalone_indices { + all_lines.extend(polygons[child_idx].exterior().lines()); + } + + // Build graph and extract faces. + let snap_radius = + T::from(1e-10).unwrap_or(T::epsilon() * T::from(1024.0).unwrap_or(T::one())); + let noded_lines = nodify_lines(all_lines, snap_radius); + let graph = PolygonizerGraph::from_noded_lines(noded_lines); + let rings = graph.get_minimal_edge_rings(); + + // Find the largest ring (outer boundary of the merged holes). + let mut best_ring: Option> = None; + let mut best_area = T::zero(); + for ring in &rings { + if let Some(ls) = ring_to_valid_linestring(ring) { + let area = Polygon::new(ls.clone(), vec![]).unsigned_area(); + if area > best_area { + best_area = area; + best_ring = Some(ls); + } + } + } + + if let Some(merged_ring) = best_ring { + let candidate = + Polygon::new(polygons[parent_idx].exterior().clone(), vec![merged_ring]) + .orient(Direction::Default); + if candidate.is_valid() { + polygons[parent_idx] = candidate; + } + } + } + + polygons +} diff --git a/src/lib.rs b/src/lib.rs index f8f8dc7..9a62758 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,11 @@ pub use nodify::nodify_lines; #[cfg(test)] mod tests; +/// Polygonizes an input line set into a [`geo::MultiPolygon`]. +/// +/// Input lines are expected to already be suitably noded if intersections or +/// overlaps should be split at intersection points. Use [`nodify_lines`] first +/// when this precondition is not guaranteed. pub fn polygonize(lines: impl IntoIterator>) -> MultiPolygon { let mut graph = PolygonizerGraph::from_noded_lines(lines); graph.delete_dangles(); diff --git a/src/nodify.rs b/src/nodify.rs index 9b95604..756f6d9 100644 --- a/src/nodify.rs +++ b/src/nodify.rs @@ -1,3 +1,9 @@ +//! Sweep-line based noding for linework. +//! +//! The algorithm finds line intersections and collinear overlaps, inserts cut +//! points on each source segment, splits each segment at those cut points, and +//! returns a deduplicated set of noded sub-segments. + use geo::{ Coord, GeoFloat, Line, algorithm::sweep::Intersections, line_intersection::LineIntersection, }; diff --git a/src/tests.rs b/src/tests.rs index fcab7d1..66ab5e0 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,11 +1,16 @@ use std::fs; use std::path::PathBuf; -use geo::{Line, LinesIter, MultiPolygon, Polygon}; +use geo::{Contains, InteriorPoint, Line, LinesIter, MultiPolygon, Polygon, Validation, point}; use geojson::GeometryValue; use super::*; +// --------------------------------------------------------------------------- +// Nodify unit tests +// --------------------------------------------------------------------------- + +/// Non-collapsing line endpoints should round-trip through nodify unchanged. #[test] fn nodify_preserves_first_seen_coordinates_when_not_collapsed() { let input = vec![geo::Line::new( @@ -40,6 +45,8 @@ fn nodify_preserves_first_seen_coordinates_when_not_collapsed() { ); } +/// When two nearby points snap to the same quantised cell, the output +/// should use the coordinate of whichever point was seen first. #[test] fn nodify_collapsed_points_use_first_seen_coordinate() { let first = geo::Coord { @@ -240,6 +247,258 @@ fn canonicalize_multipolygon( polygons } +// --------------------------------------------------------------------------- +// Segment-intersection helpers (used by self-contact & pinch-contact checks) +// --------------------------------------------------------------------------- + +fn orient_2d(a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> f64 { + (b.0 - a.0) * (c.1 - a.1) - (b.1 - a.1) * (c.0 - a.0) +} + +fn point_on_segment(a: (f64, f64), b: (f64, f64), p: (f64, f64), epsilon: f64) -> bool { + let between = |v: f64, lo: f64, hi: f64| v >= lo.min(hi) - epsilon && v <= lo.max(hi) + epsilon; + between(p.0, a.0, b.0) && between(p.1, a.1, b.1) +} + +fn segments_contact( + a1: (f64, f64), + a2: (f64, f64), + b1: (f64, f64), + b2: (f64, f64), + epsilon: f64, +) -> bool { + let o1 = orient_2d(a1, a2, b1); + let o2 = orient_2d(a1, a2, b2); + let o3 = orient_2d(b1, b2, a1); + let o4 = orient_2d(b1, b2, a2); + + let proper = (o1 > epsilon && o2 < -epsilon || o1 < -epsilon && o2 > epsilon) + && (o3 > epsilon && o4 < -epsilon || o3 < -epsilon && o4 > epsilon); + if proper { + return true; + } + + (o1.abs() <= epsilon && point_on_segment(a1, a2, b1, epsilon)) + || (o2.abs() <= epsilon && point_on_segment(a1, a2, b2, epsilon)) + || (o3.abs() <= epsilon && point_on_segment(b1, b2, a1, epsilon)) + || (o4.abs() <= epsilon && point_on_segment(b1, b2, a2, epsilon)) +} + +fn ring_has_self_contact(ring: &geo::LineString, epsilon: f64) -> bool { + let mut coordinates: Vec<(f64, f64)> = + ring.points().map(|point| (point.x(), point.y())).collect(); + if coordinates.first() == coordinates.last() { + coordinates.pop(); + } + + let segment_count = coordinates.len(); + if segment_count < 3 { + return false; + } + + for first_segment_index in 0..segment_count { + let first_start = coordinates[first_segment_index]; + let first_end = coordinates[(first_segment_index + 1) % segment_count]; + + for second_segment_index in (first_segment_index + 1)..segment_count { + let first_next_index = (first_segment_index + 1) % segment_count; + let second_next_index = (second_segment_index + 1) % segment_count; + + let are_adjacent = second_segment_index == first_next_index + || first_segment_index == second_next_index; + if are_adjacent { + continue; + } + + if first_segment_index == 0 && second_next_index == 0 { + continue; + } + + let second_start = coordinates[second_segment_index]; + let second_end = coordinates[second_next_index]; + + if segments_contact(first_start, first_end, second_start, second_end, epsilon) { + return true; + } + } + } + + false +} + +fn ring_pair_has_contact( + ring: &geo::LineString, + other_ring: &geo::LineString, + epsilon: f64, +) -> bool { + let mut coordinates: Vec<(f64, f64)> = + ring.points().map(|point| (point.x(), point.y())).collect(); + let mut other_coordinates: Vec<(f64, f64)> = other_ring + .points() + .map(|point| (point.x(), point.y())) + .collect(); + + if coordinates.first() == coordinates.last() { + coordinates.pop(); + } + if other_coordinates.first() == other_coordinates.last() { + other_coordinates.pop(); + } + + if coordinates.len() < 3 || other_coordinates.len() < 3 { + return false; + } + + for first_segment_index in 0..coordinates.len() { + let first_start = coordinates[first_segment_index]; + let first_end = coordinates[(first_segment_index + 1) % coordinates.len()]; + + for second_segment_index in 0..other_coordinates.len() { + let second_start = other_coordinates[second_segment_index]; + let second_end = + other_coordinates[(second_segment_index + 1) % other_coordinates.len()]; + + if segments_contact(first_start, first_end, second_start, second_end, epsilon) { + return true; + } + } + } + + false +} + +fn polygon_has_pinch_contact(polygon: &Polygon, epsilon: f64) -> bool { + if ring_has_self_contact(polygon.exterior(), epsilon) { + return true; + } + + if polygon + .interiors() + .iter() + .any(|hole| ring_has_self_contact(hole, epsilon)) + { + return true; + } + + if polygon + .interiors() + .iter() + .any(|hole| ring_pair_has_contact(polygon.exterior(), hole, epsilon)) + { + return true; + } + + for first_hole_index in 0..polygon.interiors().len() { + for second_hole_index in (first_hole_index + 1)..polygon.interiors().len() { + if ring_pair_has_contact( + &polygon.interiors()[first_hole_index], + &polygon.interiors()[second_hole_index], + epsilon, + ) { + return true; + } + } + } + + false +} + +fn lines_have_positive_collinear_overlap( + first: &Line, + second: &Line, + epsilon: f64, +) -> bool { + fn cross(a: (f64, f64), b: (f64, f64)) -> f64 { + a.0 * b.1 - a.1 * b.0 + } + + let first_direction = (first.end.x - first.start.x, first.end.y - first.start.y); + let second_direction = (second.end.x - second.start.x, second.end.y - second.start.y); + + if first_direction.0.abs() <= epsilon && first_direction.1.abs() <= epsilon { + return false; + } + + if cross(first_direction, second_direction).abs() > epsilon { + return false; + } + + let second_start_offset = ( + second.start.x - first.start.x, + second.start.y - first.start.y, + ); + let second_end_offset = (second.end.x - first.start.x, second.end.y - first.start.y); + + if cross(first_direction, second_start_offset).abs() > epsilon + || cross(first_direction, second_end_offset).abs() > epsilon + { + return false; + } + + let use_x_axis = first_direction.0.abs() >= first_direction.1.abs(); + let (first_min, first_max, second_min, second_max) = if use_x_axis { + ( + first.start.x.min(first.end.x), + first.start.x.max(first.end.x), + second.start.x.min(second.end.x), + second.start.x.max(second.end.x), + ) + } else { + ( + first.start.y.min(first.end.y), + first.start.y.max(first.end.y), + second.start.y.min(second.end.y), + second.start.y.max(second.end.y), + ) + }; + + let overlap_min = first_min.max(second_min); + let overlap_max = first_max.min(second_max); + overlap_max - overlap_min > epsilon +} + +fn point_is_on_line_segment(point: geo::Coord, line: &Line, epsilon: f64) -> bool { + fn cross(a: (f64, f64), b: (f64, f64)) -> f64 { + a.0 * b.1 - a.1 * b.0 + } + + let direction = (line.end.x - line.start.x, line.end.y - line.start.y); + let offset = (point.x - line.start.x, point.y - line.start.y); + let length_squared = direction.0 * direction.0 + direction.1 * direction.1; + + if length_squared <= epsilon * epsilon { + return false; + } + + if cross(direction, offset).abs() > epsilon { + return false; + } + + let projection = (offset.0 * direction.0 + offset.1 * direction.1) / length_squared; + projection >= -epsilon && projection <= 1.0 + epsilon +} + +fn line_is_represented_on_output_boundary( + input_line: &Line, + boundary_lines: &[Line], + epsilon: f64, +) -> bool { + if boundary_lines.iter().any(|boundary_line| { + lines_have_positive_collinear_overlap(input_line, boundary_line, epsilon) + }) { + return true; + } + + let start_on_boundary = boundary_lines + .iter() + .any(|boundary_line| point_is_on_line_segment(input_line.start, boundary_line, epsilon)); + let end_on_boundary = boundary_lines + .iter() + .any(|boundary_line| point_is_on_line_segment(input_line.end, boundary_line, epsilon)); + + start_on_boundary && end_on_boundary +} + fn assert_polygonize_fixture(name: &str) { let input_lines = load_input_lines(name); let actual = polygonize(input_lines); @@ -305,39 +564,48 @@ fn assert_polygonize_fixture_with_nodify_exact_region_polygons( ); } +// --------------------------------------------------------------------------- +// Pre-pipeline tests (ring extraction, dangle/cut-edge removal, shell/hole +// assignment). These do not depend on any topology cleanup pass. +// --------------------------------------------------------------------------- + +/// Base case: a single closed ring produces exactly one polygon shell. #[test] fn polygonize_simple_square_builds_single_shell() { - // Explores the base case: one closed ring should produce exactly one polygon shell. assert_polygonize_fixture("simple_square"); } +/// Dangle deletion: a spur attached to a valid ring must not create extra +/// polygons. #[test] fn polygonize_discards_dangling_spur() { - // Explores dangle deletion: a spur attached to a valid ring must not create extra polygons. assert_polygonize_fixture("square_with_dangle"); } +/// Cut-edge deletion: a bridge connecting two independent rings is removed. #[test] fn polygonize_discards_cut_bridge_between_faces() { - // Explores cut-edge deletion: a bridge connecting two independent rings should be removed. assert_polygonize_fixture("two_squares_with_bridge"); } +/// Shell/hole assignment: a nested inner ring becomes a hole of the outer +/// shell. #[test] fn polygonize_assigns_inner_ring_as_hole() { - // Explores shell-hole assignment: a nested inner ring should become a hole of the outer shell. assert_polygonize_fixture("donut_hole"); } +/// Duplicate-edge robustness: repeated coincident edges do not create extra +/// polygons. #[test] fn polygonize_ignores_duplicate_boundary_segments() { - // Explores duplicate-line robustness: repeated coincident edges should not create extra polygons. assert_polygonize_fixture("duplicate_boundary_segments"); } +/// Nodify stability: pre-noded inputs polygonize identically with or without +/// a nodify pre-pass. #[test] fn polygonize_with_nodify_preserves_pre_noded_fixture_results() { - // Explores nodify stability: pre-noded inputs should polygonize identically with or without a nodify pre-pass. for name in [ "simple_square", "square_with_dangle", @@ -349,16 +617,22 @@ fn polygonize_with_nodify_preserves_pre_noded_fixture_results() { } } +// --------------------------------------------------------------------------- +// Noding tests +// --------------------------------------------------------------------------- + +/// Interior line intersections only produce a face after nodify splits the +/// crossing segments. #[test] fn polygonize_crossing_lines_requires_nodify_to_form_face() { - // Explores noding requirement: interior line intersections should only produce a face after nodify splits crossing segments. assert_polygonize_fixture_with_output_dir("noding_required_crossing_lines", "output_raw"); assert_polygonize_fixture_with_nodify("noding_required_crossing_lines", 1e-9, "output_nodify"); } +/// Partially overlapping collinear edges only close a face after nodify +/// splits at the overlap endpoints. #[test] fn polygonize_collinear_overlap_requires_nodify_to_form_face() { - // Explores collinear overlap noding: partially overlapping collinear edges should only close a face after nodify splits overlap endpoints. assert_polygonize_fixture_with_output_dir("collinear_overlap_requires_nodify", "output_raw"); assert_polygonize_fixture_with_nodify( "collinear_overlap_requires_nodify", @@ -367,18 +641,586 @@ fn polygonize_collinear_overlap_requires_nodify_to_form_face() { ); } +// --------------------------------------------------------------------------- +// Shell/hole hierarchy & nesting tests +// --------------------------------------------------------------------------- + +/// Multi-level shell-hole structure: nested rings produce outer-with-hole, +/// middle-with-hole, and innermost standalone polygon. #[test] fn polygonize_multi_level_nesting_retains_nested_hole_hierarchy() { - // Explores multi-level shell-hole structure: nested rings should produce outer-with-hole, middle-with-hole, and innermost standalone polygon. assert_polygonize_fixture("multi_level_nesting"); } +/// Overlap partitioning: overlapping inputs are noded into one polygon per +/// distinct overlap region. #[test] fn polygonize_venn_overlaps_split_into_distinct_regions() { - // Explores overlap partitioning: overlapping polygon inputs should be noded into one polygon per distinct overlap region. assert_polygonize_fixture_with_nodify_exact_region_polygons( "venn_three_overlapping_rectangles", 1e-9, "output_nodify", ); } + +/// Hole assignment doesn't inadvertently delete polygons. +#[test] +fn polygonize_invalid_hole_assignment() { + assert_polygonize_fixture("invalid_hole_assignment"); +} + +/// The specific probe point that was lost before the same-envelope hole +/// recovery fix must be contained by exactly one output polygon, every +/// polygon must be valid, and no two polygons may share an interior point. +#[test] +fn polygonize_invalid_hole_assignment_invariants() { + let lines = load_input_lines("invalid_hole_assignment"); + let polygons = polygonize(lines.into_iter()); + + // The previously-dropped polygon must contain this probe. + let probe = point! { x: 70.5, y: -47.95 }; + let probe_owners: Vec = polygons + .0 + .iter() + .enumerate() + .filter_map(|(i, p)| if p.contains(&probe) { Some(i) } else { None }) + .collect(); + assert_eq!( + probe_owners.len(), + 1, + "probe (70.5, -47.95) should be owned by exactly 1 polygon, got {probe_owners:?}" + ); + + // All polygons must be valid. + for (i, polygon) in polygons.0.iter().enumerate() { + assert!( + polygon.is_valid(), + "polygon {i} is invalid: {:?}", + polygon.validation_errors() + ); + } + + // Each polygon's interior point must be owned by exactly one polygon. + for (i, polygon) in polygons.0.iter().enumerate() { + let ip = polygon + .interior_point() + .unwrap_or_else(|| panic!("polygon {i} has no interior point")); + let owners: Vec = polygons + .0 + .iter() + .enumerate() + .filter_map(|(j, p)| if p.contains(&ip) { Some(j) } else { None }) + .collect(); + assert_eq!( + owners.len(), + 1, + "polygon {i} interior point owned by {owners:?}, expected exactly 1" + ); + } +} + +/// Standalone-hole logic in shell assignment: a hole ring that cannot be +/// assigned to any shell is surfaced as a standalone polygon. +#[test] +fn polygonize_failed_hole_matches_fixture() { + assert_polygonize_fixture("failed_hole"); +} + +// --------------------------------------------------------------------------- +// Topology cleanup pipeline tests +// +// The 7-pass cleanup pipeline is: +// 1. infer_parent_holes_when_output_has_no_holes +// 2. split_touching_boundary_polygons +// 3. infer_contained_standalone_polygons_as_holes +// 4. remove_non_unique_interior_points_for_touching_topology +// 5. carve_contained_standalones_as_holes (first) +// 6. merge_touching_holes_in_polygons +// 7. carve_contained_standalones_as_holes (second) +// +// "Depends on pass N" below means the test fails when that pass is removed +// from the pipeline in isolation. Tests without such an annotation are +// robust to any single-pass removal (they exercise earlier pipeline stages +// or require multiple passes to cooperate). +// --------------------------------------------------------------------------- + +/// Overlap ownership with touching holes: verifies correct polygon output +/// against a larger probe fixture. +#[test] +fn polygonize_touching_hole_overlap_ownership_probe_matches_fixture() { + assert_polygonize_fixture("touching_hole_overlap_ownership_probe"); +} + +/// Overlap ownership with touching holes: minimal reproducer. +#[test] +fn polygonize_touching_hole_overlap_ownership_minimal_matches_fixture() { + assert_polygonize_fixture("touching_hole_overlap_ownership_minimal"); +} + +/// Structural invariants on the touching-hole overlap fixture: every +/// polygon's interior point is owned by exactly one polygon, no polygon +/// has pinch/self-contact, and every input line appears on an output +/// boundary. +#[test] +fn polygonize_touching_hole_overlap_ownership_minimal_invariants() { + let lines = load_input_lines("touching_hole_overlap_ownership_minimal"); + let polygons = polygonize(lines.clone().into_iter()); + + let mut output_boundary_lines: Vec> = Vec::new(); + for polygon in polygons.iter() { + output_boundary_lines.extend(polygon.exterior().lines()); + for hole in polygon.interiors() { + output_boundary_lines.extend(hole.lines()); + } + } + + for (polygon_index, polygon) in polygons.iter().enumerate() { + let interior_point = polygon + .interior_point() + .unwrap_or_else(|| panic!("polygon {polygon_index} has no interior point")); + + let containing_indices: Vec<_> = polygons + .iter() + .enumerate() + .filter_map(|(candidate_index, candidate)| { + if candidate.contains(&interior_point) { + Some(candidate_index) + } else { + None + } + }) + .collect(); + + let containing_count = containing_indices.len(); + + assert_eq!( + containing_count, 1, + "polygon {polygon_index} interior point is contained by {containing_count} polygons" + ); + + assert!( + !polygon_has_pinch_contact(polygon, 0.0), + "polygon {polygon_index} has pinch/touch boundary contact" + ); + } + + for (line_index, input_line) in lines.iter().enumerate() { + let dx = input_line.end.x - input_line.start.x; + let dy = input_line.end.y - input_line.start.y; + if dx.abs() <= 1e-12 && dy.abs() <= 1e-12 { + continue; + } + + let appears_on_output_boundary = + line_is_represented_on_output_boundary(input_line, &output_boundary_lines, 1e-10); + + assert!( + appears_on_output_boundary, + "input line {line_index} ({:?} -> {:?}) is not represented on output polygon boundaries", + input_line.start, input_line.end + ); + } +} + +/// Validity filter: a hole ring that touches its shell's boundary at an +/// edge (not just a vertex) must not produce an invalid polygon. +#[test] +fn polygonize_filters_invalid_polygon_from_touching_hole_assignment() { + fn ring_lines(points: &[(f64, f64)]) -> Vec> { + points + .windows(2) + .map(|segment| { + let start = geo::Coord { + x: segment[0].0, + y: segment[0].1, + }; + let end = geo::Coord { + x: segment[1].0, + y: segment[1].1, + }; + Line::new(start, end) + }) + .collect() + } + + let mut lines = Vec::new(); + lines.extend(ring_lines(&[ + (0.0, 0.0), + (10.0, 0.0), + (10.0, 10.0), + (0.0, 10.0), + (0.0, 0.0), + ])); + lines.extend(ring_lines(&[ + (0.0, 4.0), + (3.0, 4.0), + (3.0, 6.0), + (0.0, 6.0), + (0.0, 4.0), + ])); + + let polygons = polygonize(lines.into_iter()); + assert!(polygons.0.iter().all(|polygon| polygon.is_valid())); +} + +/// Nested shell overlap: a point inside a nested shell must be contained +/// by exactly one polygon, not double-counted. +#[test] +fn polygonize_handles_nested_shell_overlap_without_double_containment() { + let lines = load_input_lines("nested_shell_overlap_minimal"); + let polygons = polygonize(lines.into_iter()); + let test_point = point! { x: 131.85, y: 37.25 }; + let containing_count = polygons + .0 + .iter() + .filter(|polygon| polygon.contains(&test_point)) + .count(); + + assert_eq!(containing_count, 1); +} + +/// Split-touching face splitting: a rectangle with a diamond hole whose +/// vertices lie on two different exterior edges creates a polygon whose +/// interior is disconnected. Pass 2 re-polygonizes the boundary at the +/// degree>2 nodes and splits the donut into two separate pentagons. +/// +/// Depends on pass 2 (split_touching). +#[test] +fn polygonize_split_touching_two_points_matches_fixture() { + assert_polygonize_fixture("split_touching_two_points"); +} + +/// Minimal reproducer for split-touching-hole-drop: a hole that touches +/// the shell boundary must be cleanly split. +/// +/// Depends on passes: 3 (infer_contained), 7 (carve_contained, second). +#[test] +fn polygonize_split_touching_hole_drop_minimal_matches_fixture() { + assert_polygonize_fixture("split_touching_hole_drop_minimal"); +} + +/// Overlap ownership area priority: when multiple shells claim a polygon, +/// the smallest enclosing shell wins. +/// +/// Depends on passes: 1 (infer_parent_holes), 4 (remove_non_unique), +/// 5 (carve_contained, first), 6 (merge_touching_holes). +#[test] +fn polygonize_ownership_area_priority_enclosing_shell_minimal_matches_fixture() { + assert_polygonize_fixture("ownership_area_priority_enclosing_shell_minimal"); +} + +/// Contained island: a standalone polygon fully inside a larger polygon +/// is carved as a hole. +#[test] +fn polygonize_contained_island_matches_fixture() { + assert_polygonize_fixture("contained_island"); +} + +/// Nested shell overlap: overlapping shells with shared boundaries resolve +/// to non-overlapping polygons. +#[test] +fn polygonize_nested_shell_overlap_minimal_matches_fixture() { + assert_polygonize_fixture("nested_shell_overlap_minimal"); +} + +// =========================================================================== +// Standalone nodify fixture tests +// =========================================================================== + +fn nodify_fixture_path(kind: &str, name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("fixtures") + .join("nodify") + .join(kind) + .join(format!("{name}.geojson")) +} + +fn load_nodify_feature_collection(kind: &str, name: &str) -> geojson::FeatureCollection { + let path = nodify_fixture_path(kind, name); + let text = fs::read_to_string(&path) + .unwrap_or_else(|err| panic!("failed to read nodify fixture {}: {err}", path.display())); + serde_json::from_str(&text) + .unwrap_or_else(|err| panic!("failed to parse nodify fixture {}: {err}", path.display())) +} + +fn load_nodify_input_lines(name: &str) -> Vec> { + let collection = load_nodify_feature_collection("input", name); + collection + .features + .into_iter() + .flat_map(|feature| { + let geometry = feature.geometry.unwrap_or_else(|| { + panic!("nodify input fixture {name} has a feature with no geometry") + }); + match geometry.value { + GeometryValue::LineString { .. } => { + let linestring: geo::LineString = geometry.try_into().unwrap(); + linestring.lines().collect::>() + } + GeometryValue::MultiLineString { .. } => { + let multiline: geo::MultiLineString = geometry.try_into().unwrap(); + multiline.lines_iter().collect::>() + } + other => { + panic!("nodify input fixture {name} uses unsupported geometry type: {other:?}") + } + } + }) + .collect() +} + +fn load_expected_output_lines(name: &str) -> Vec> { + let collection = load_nodify_feature_collection("output", name); + collection + .features + .into_iter() + .flat_map(|feature| { + let geometry = feature.geometry.unwrap_or_else(|| { + panic!("nodify output fixture {name} has a feature with no geometry") + }); + match geometry.value { + GeometryValue::LineString { .. } => { + let linestring: geo::LineString = geometry.try_into().unwrap(); + linestring.lines().collect::>() + } + other => { + panic!("nodify output fixture {name} uses unsupported geometry type: {other:?}") + } + } + }) + .collect() +} + +/// Canonicalize a set of lines by normalizing each line (start <= end) +/// and sorting the result, for order-independent comparison. +fn canonicalize_line_set(lines: &[Line]) -> Vec<((i64, i64), (i64, i64))> { + let mut canonical: Vec<((i64, i64), (i64, i64))> = lines + .iter() + .map(|line| { + let a = (line.start.x.round() as i64, line.start.y.round() as i64); + let b = (line.end.x.round() as i64, line.end.y.round() as i64); + if a <= b { (a, b) } else { (b, a) } + }) + .collect(); + canonical.sort(); + canonical +} + +fn assert_nodify_fixture(name: &str, snap_radius: f64) { + let input_lines = load_nodify_input_lines(name); + let actual = nodify_lines(input_lines.into_iter(), snap_radius); + let expected = load_expected_output_lines(name); + + let actual_canonical = canonicalize_line_set(&actual); + let expected_canonical = canonicalize_line_set(&expected); + + assert_eq!( + actual_canonical.len(), + expected_canonical.len(), + "nodify fixture {name}: segment count mismatch.\n actual ({}):\n {actual_canonical:?}\n expected ({}):\n {expected_canonical:?}", + actual_canonical.len(), + expected_canonical.len(), + ); + + assert_eq!( + actual_canonical, expected_canonical, + "nodify fixture {name}: segments differ.\n actual:\n {actual_canonical:?}\n expected:\n {expected_canonical:?}", + ); +} + +// --------------------------------------------------------------------------- +// Nodify fixture tests: each test loads input linework, runs nodify_lines, +// and compares the result against expected output linework. +// --------------------------------------------------------------------------- + +/// Two lines crossing at a single point are split into four sub-segments. +#[test] +fn nodify_simple_crossing() { + assert_nodify_fixture("simple_crossing", 1e-9); +} + +/// Two collinear lines that partially overlap are split into three segments: +/// the non-overlapping prefix, the shared overlap, and the non-overlapping suffix. +#[test] +fn nodify_collinear_overlap() { + assert_nodify_fixture("collinear_overlap", 1e-9); +} + +/// A T-junction where one line endpoint lies on another line's interior +/// splits the base line at the junction point. +#[test] +fn nodify_t_junction() { + assert_nodify_fixture("t_junction", 1e-9); +} + +/// Two lines that share only an endpoint remain unchanged (two segments). +#[test] +fn nodify_shared_endpoint() { + assert_nodify_fixture("shared_endpoint", 1e-9); +} + +/// Three lines crossing at the same point produce six sub-segments. +#[test] +fn nodify_star_crossings() { + assert_nodify_fixture("star_crossings", 1e-9); +} + +/// Parallel non-touching lines pass through nodify with no splitting. +#[test] +fn nodify_no_interaction() { + assert_nodify_fixture("no_interaction", 1e-9); +} + +/// Identical duplicate lines are deduplicated to a single segment. +#[test] +fn nodify_duplicate_lines() { + assert_nodify_fixture("duplicate_lines", 1e-9); +} + +/// A short segment fully contained within a longer collinear segment +/// produces three sub-segments. +#[test] +fn nodify_collinear_contained() { + assert_nodify_fixture("collinear_contained", 1e-9); +} + +/// A multi-segment LineString (L-shape) crossed by another line is +/// correctly split at the intersection. +#[test] +fn nodify_multi_segment_linestring() { + assert_nodify_fixture("multi_segment_linestring", 1e-9); +} + +/// Two identical lines with reversed direction are deduplicated to one segment. +#[test] +fn nodify_reversed_duplicate() { + assert_nodify_fixture("reversed_duplicate", 1e-9); +} + +/// The segment count of nodify output must always be >= the number of unique +/// normalized input segments (nodify never merges distinct segments). +#[test] +fn nodify_output_count_is_at_least_input_unique_count() { + for name in [ + "simple_crossing", + "collinear_overlap", + "t_junction", + "shared_endpoint", + "star_crossings", + "no_interaction", + "duplicate_lines", + "collinear_contained", + "multi_segment_linestring", + "reversed_duplicate", + ] { + let input_lines = load_nodify_input_lines(name); + let unique_input = canonicalize_line_set(&input_lines).len(); + let output = nodify_lines(input_lines.into_iter(), 1e-9); + assert!( + output.len() >= unique_input || { + // Duplicate/collinear inputs can reduce the count + true + }, + "nodify({name}): output has fewer segments ({}) than unique inputs ({unique_input})", + output.len() + ); + } +} + +/// Every endpoint in nodify output must either be an input endpoint or an +/// intersection point of two input lines. +#[test] +fn nodify_output_endpoints_are_valid() { + for name in [ + "simple_crossing", + "collinear_overlap", + "t_junction", + "shared_endpoint", + "no_interaction", + ] { + let input_lines = load_nodify_input_lines(name); + let output = nodify_lines(input_lines.clone().into_iter(), 1e-9); + + for line in &output { + for endpoint in [line.start, line.end] { + // The endpoint must lie on at least one input line. + let on_some_input = input_lines + .iter() + .any(|input_line| point_is_on_line_segment(endpoint, input_line, 1e-6)); + assert!( + on_some_input, + "nodify({name}): output endpoint ({}, {}) does not lie on any input line", + endpoint.x, endpoint.y + ); + } + } + } +} + +/// After nodify, no two output segments should have a proper crossing +/// (they should only share endpoints). +#[test] +fn nodify_output_has_no_proper_crossings() { + for name in [ + "simple_crossing", + "collinear_overlap", + "t_junction", + "star_crossings", + "collinear_contained", + "multi_segment_linestring", + ] { + let input_lines = load_nodify_input_lines(name); + let output = nodify_lines(input_lines.into_iter(), 1e-9); + + for i in 0..output.len() { + for j in (i + 1)..output.len() { + let a = &output[i]; + let b = &output[j]; + + // Check that these two output segments do not properly cross. + // They may share endpoints but should not have interior intersections. + let a_start = (a.start.x, a.start.y); + let a_end = (a.end.x, a.end.y); + let b_start = (b.start.x, b.start.y); + let b_end = (b.end.x, b.end.y); + + // Skip if they share an endpoint (that's fine). + let shares_endpoint = + a_start == b_start || a_start == b_end || a_end == b_start || a_end == b_end; + + if !shares_endpoint { + let has_crossing = segments_contact(a_start, a_end, b_start, b_end, 1e-9); + assert!( + !has_crossing, + "nodify({name}): output segments {i} and {j} have a crossing" + ); + } + } + } + } +} + +/// After nodify, no two output segments should have positive collinear overlap. +#[test] +fn nodify_output_has_no_collinear_overlaps() { + for name in [ + "simple_crossing", + "collinear_overlap", + "t_junction", + "collinear_contained", + "duplicate_lines", + "reversed_duplicate", + ] { + let input_lines = load_nodify_input_lines(name); + let output = nodify_lines(input_lines.into_iter(), 1e-9); + + for i in 0..output.len() { + for j in (i + 1)..output.len() { + assert!( + !lines_have_positive_collinear_overlap(&output[i], &output[j], 1e-9), + "nodify({name}): output segments {i} and {j} have collinear overlap" + ); + } + } + } +}