From 5b848bbc36a5ab6ffbc71ec5dae099662ad1822f Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Thu, 16 Nov 2023 20:43:54 -0500 Subject: [PATCH 1/4] Merge branch 'main' --- .github/workflows/test.yml | 72 +- Cargo.toml | 11 +- bors.toml | 3 - geo-bool-ops-benches/benches/boolean_ops.rs | 10 +- geo-postgis/Cargo.toml | 2 +- geo-types/CHANGES.md | 15 +- geo-types/Cargo.toml | 8 +- geo-types/src/geometry/coord.rs | 36 + geo-types/src/geometry/geometry_collection.rs | 2 +- geo-types/src/geometry/line.rs | 10 +- geo-types/src/geometry/line_string.rs | 16 +- geo-types/src/geometry/multi_line_string.rs | 32 +- geo-types/src/geometry/multi_point.rs | 57 +- geo-types/src/geometry/multi_polygon.rs | 2 +- geo-types/src/geometry/point.rs | 29 + geo-types/src/geometry/polygon.rs | 32 +- geo-types/src/lib.rs | 32 +- geo-types/src/macros.rs | 26 +- geo-types/src/wkt_macro.rs | 328 ++++++++ geo/CHANGES.md | 49 +- geo/Cargo.toml | 18 +- geo/benches/area.rs | 6 +- geo/benches/concave_hull.rs | 6 +- geo/benches/contains.rs | 10 +- geo/benches/convex_hull.rs | 6 +- geo/benches/euclidean_distance.rs | 7 +- geo/benches/extremes.rs | 6 +- geo/benches/frechet_distance.rs | 4 +- geo/benches/geodesic_distance.rs | 7 +- geo/benches/intersection.rs | 7 +- geo/benches/monotone_subdiv.rs | 137 +++ geo/benches/relate.rs | 12 +- geo/benches/rotate.rs | 6 +- geo/benches/simplify.rs | 6 +- geo/benches/simplifyvw.rs | 6 +- geo/benches/utils/random.rs | 50 +- geo/benches/vincenty_distance.rs | 7 +- geo/benches/winding_order.rs | 7 +- geo/examples/algorithm.rs | 5 +- geo/examples/types.rs | 2 - geo/src/algorithm/affine_ops.rs | 137 ++- geo/src/algorithm/area.rs | 18 +- geo/src/algorithm/bearing.rs | 3 +- geo/src/algorithm/centroid.rs | 79 +- geo/src/algorithm/convert_angle_unit.rs | 84 ++ geo/src/algorithm/convex_hull/graham.rs | 4 +- geo/src/algorithm/convex_hull/mod.rs | 2 +- geo/src/algorithm/convex_hull/qhull.rs | 8 +- geo/src/algorithm/coords_iter.rs | 223 ++--- geo/src/algorithm/densify.rs | 13 + geo/src/algorithm/densify_haversine.rs | 263 ++++++ geo/src/algorithm/euclidean_distance.rs | 372 ++++++--- geo/src/algorithm/extremes.rs | 2 +- geo/src/algorithm/geodesic_bearing.rs | 6 +- geo/src/algorithm/geodesic_intermediate.rs | 3 +- geo/src/algorithm/hausdorff_distance.rs | 136 +++ geo/src/algorithm/haversine_bearing.rs | 3 +- geo/src/algorithm/haversine_closest_point.rs | 637 ++++++++++++++ geo/src/algorithm/haversine_destination.rs | 29 +- geo/src/algorithm/haversine_intermediate.rs | 3 +- geo/src/algorithm/k_nearest_concave_hull.rs | 8 +- geo/src/algorithm/line_intersection.rs | 2 +- geo/src/algorithm/line_locate_point.rs | 3 +- geo/src/algorithm/linestring_segment.rs | 308 +++++++ geo/src/algorithm/minimum_rotated_rect.rs | 10 +- geo/src/algorithm/mod.rs | 38 + geo/src/algorithm/monotone/builder.rs | 377 +++++++++ geo/src/algorithm/monotone/mod.rs | 78 ++ geo/src/algorithm/monotone/mono_poly.rs | 164 ++++ geo/src/algorithm/monotone/segment.rs | 118 +++ geo/src/algorithm/monotone/sweep.rs | 219 +++++ geo/src/algorithm/monotone/tests.rs | 114 +++ geo/src/algorithm/outlier_detection.rs | 8 +- .../index/rstar_edge_set_intersector.rs | 2 +- .../relate/geomgraph/intersection_matrix.rs | 132 ++- .../geomgraph/robust_line_intersector.rs | 2 +- geo/src/algorithm/rhumb/bearing.rs | 91 ++ geo/src/algorithm/rhumb/destination.rs | 80 ++ geo/src/algorithm/rhumb/distance.rs | 99 +++ geo/src/algorithm/rhumb/intermediate.rs | 143 ++++ geo/src/algorithm/rhumb/length.rs | 74 ++ geo/src/algorithm/rhumb/mod.rs | 165 ++++ geo/src/algorithm/rotate.rs | 18 +- geo/src/algorithm/scale.rs | 12 +- geo/src/algorithm/simplify.rs | 3 +- geo/src/algorithm/simplify_vw.rs | 315 +++---- geo/src/algorithm/skew.rs | 4 +- geo/src/algorithm/sweep/active.rs | 52 +- geo/src/algorithm/sweep/im_segment.rs | 6 +- geo/src/algorithm/sweep/iter.rs | 2 +- geo/src/algorithm/sweep/line_or_point.rs | 215 +++-- geo/src/algorithm/sweep/mod.rs | 2 +- geo/src/algorithm/sweep/vec_set.rs | 8 +- geo/src/algorithm/translate.rs | 4 +- geo/src/algorithm/triangulate_spade.rs | 782 ++++++++++++++++++ geo/src/algorithm/vector_ops.rs | 389 +++++++++ geo/src/lib.rs | 123 +-- geo/src/utils.rs | 12 +- 98 files changed, 6367 insertions(+), 927 deletions(-) delete mode 100644 bors.toml create mode 100644 geo-types/src/wkt_macro.rs create mode 100644 geo/benches/monotone_subdiv.rs create mode 100644 geo/src/algorithm/convert_angle_unit.rs create mode 100644 geo/src/algorithm/densify_haversine.rs create mode 100644 geo/src/algorithm/hausdorff_distance.rs create mode 100644 geo/src/algorithm/haversine_closest_point.rs create mode 100644 geo/src/algorithm/linestring_segment.rs create mode 100644 geo/src/algorithm/monotone/builder.rs create mode 100644 geo/src/algorithm/monotone/mod.rs create mode 100644 geo/src/algorithm/monotone/mono_poly.rs create mode 100644 geo/src/algorithm/monotone/segment.rs create mode 100644 geo/src/algorithm/monotone/sweep.rs create mode 100644 geo/src/algorithm/monotone/tests.rs create mode 100644 geo/src/algorithm/rhumb/bearing.rs create mode 100644 geo/src/algorithm/rhumb/destination.rs create mode 100644 geo/src/algorithm/rhumb/distance.rs create mode 100644 geo/src/algorithm/rhumb/intermediate.rs create mode 100644 geo/src/algorithm/rhumb/length.rs create mode 100644 geo/src/algorithm/rhumb/mod.rs create mode 100644 geo/src/algorithm/triangulate_spade.rs create mode 100644 geo/src/algorithm/vector_ops.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f4b6246999..502188fb57 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,18 +6,17 @@ on: - trying - release/** pull_request: + merge_group: schedule: [cron: "45 6 * * *"] name: Run tests jobs: # The `ci-result` job doesn't actually test anything - it just aggregates the - # overall build status for bors, otherwise our bors.toml would need an entry + # overall build status, otherwise the merge queue would need an entry # for each individual job produced by the job-matrix. # - # Ref: https://github.com/rust-lang/crater/blob/9ab6f9697c901c4a44025cf0a39b73ad5b37d198/.github/workflows/bors.yml#L125-L149 - # - # ALL THE SUBSEQUENT JOBS NEED THEIR `name` ADDED TO THE `needs` SECTION OF THIS JOB! - ci-result: + # ALL THE SUBSEQUENT JOBS NEED THEIR `name` ADDED TO THE `needs` SECTION OF both "ci result" JOBS! + ci-success: name: ci result runs-on: ubuntu-latest needs: @@ -28,12 +27,23 @@ jobs: - geo_fuzz - geo_traits - bench + if: success() steps: - name: Mark the job as a success - if: success() run: exit 0 + ci-failure: + name: ci result + runs-on: ubuntu-latest + needs: + - lint + - geo_types + - geo + - geo_postgis + - geo_fuzz + - bench + if: failure() + steps: - name: Mark the job as a failure - if: "!success()" run: exit 1 lint: @@ -44,12 +54,12 @@ jobs: matrix: container_image: # Use the latest stable version. No need for older versions. - - "georust/geo-ci:rust-1.66" + - "georust/geo-ci:proj-9.2.1-rust-1.72" container: image: ${{ matrix.container_image }} steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - run: rustup component add rustfmt clippy - run: cargo fmt --all -- --check - run: cargo clippy --all-features --all-targets -- -Dwarnings @@ -68,15 +78,15 @@ jobs: # giving us about 6 months of coverage. # # Minimum supported rust version (MSRV) - - "georust/geo-ci:rust-1.63" + - "georust/geo-ci:proj-9.2.1-rust-1.65" # Two most recent releases - we omit older ones for expedient CI - - "georust/geo-ci:rust-1.65" - - "georust/geo-ci:rust-1.66" + - "georust/geo-ci:proj-9.2.1-rust-1.71" + - "georust/geo-ci:proj-9.2.1-rust-1.72" container: image: ${{ matrix.container_image }} steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - run: rustup target add thumbv7em-none-eabihf - run: cargo check --all-targets --no-default-features - run: cargo check --lib --target thumbv7em-none-eabihf --no-default-features -F use-rstar_0_9,serde @@ -96,15 +106,15 @@ jobs: # giving us about 6 months of coverage. # # Minimum supported rust version (MSRV) - - "georust/geo-ci:rust-1.63" + - "georust/geo-ci:proj-9.2.1-rust-1.65" # Two most recent releases - we omit older ones for expedient CI - - "georust/geo-ci:rust-1.65" - - "georust/geo-ci:rust-1.66" + - "georust/geo-ci:proj-9.2.1-rust-1.71" + - "georust/geo-ci:proj-9.2.1-rust-1.72" container: image: ${{ matrix.container_image }} steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - run: cargo check --all-targets --no-default-features # we don't want to test `proj-network` because it only enables the `proj` feature - run: cargo test --features "use-proj use-serde" @@ -149,15 +159,15 @@ jobs: # giving us about 6 months of coverage. # # Minimum supported rust version (MSRV) - - "georust/geo-ci:rust-1.63" + - "georust/geo-ci:proj-9.2.1-rust-1.65" # Two most recent releases - we omit older ones for expedient CI - - "georust/geo-ci:rust-1.65" - - "georust/geo-ci:rust-1.66" + - "georust/geo-ci:proj-9.2.1-rust-1.71" + - "georust/geo-ci:proj-9.2.1-rust-1.72" container: image: ${{ matrix.container_image }} steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - run: cargo check --all-targets - run: cargo test @@ -172,12 +182,12 @@ jobs: matrix: container_image: # Fuzz only on latest - - "georust/geo-ci:rust-1.66" + - "georust/geo-ci:proj-9.2.1-rust-1.72" container: image: ${{ matrix.container_image }} steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - run: cargo build --bins bench: @@ -185,8 +195,20 @@ jobs: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '[skip ci]')" container: - image: georust/geo-ci:rust-1.66 + image: georust/geo-ci:proj-9.2.1-rust-1.72 steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - run: cargo bench --no-run + + docs: + name: build documentation + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[skip ci]')" + container: + image: georust/geo-ci:proj-9.2.1-rust-1.72 + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - run: RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps + diff --git a/Cargo.toml b/Cargo.toml index ad2a3e4a52..8735f54f52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,14 @@ [workspace] -members = ["geo", "geo-types", "geo-traits", "geo-postgis", "geo-test-fixtures", "jts-test-runner", "geo-bool-ops-benches"] +resolver = "2" +members = [ + "geo", + "geo-types", + "geo-traits", + "geo-postgis", + "geo-test-fixtures", + "jts-test-runner", + "geo-bool-ops-benches", +] [patch.crates-io] diff --git a/bors.toml b/bors.toml deleted file mode 100644 index bfa4661a10..0000000000 --- a/bors.toml +++ /dev/null @@ -1,3 +0,0 @@ -status = [ - "ci result", -] diff --git a/geo-bool-ops-benches/benches/boolean_ops.rs b/geo-bool-ops-benches/benches/boolean_ops.rs index 6ac0b7e5e6..3eb63830e7 100644 --- a/geo-bool-ops-benches/benches/boolean_ops.rs +++ b/geo-bool-ops-benches/benches/boolean_ops.rs @@ -46,7 +46,7 @@ fn run_complex(c: &mut Criterion) { |b, _| { b.iter_batched( polys.sampler(), - |&(ref poly, ref poly2, _, _)| poly.intersection(poly2), + |(poly, poly2, _, _)| poly.intersection(poly2), BatchSize::SmallInput, ); }, @@ -55,7 +55,7 @@ fn run_complex(c: &mut Criterion) { group.bench_with_input(BenchmarkId::new("bops::union", steps), &(), |b, _| { b.iter_batched( polys.sampler(), - |&(ref poly, ref poly2, _, _)| poly.union(poly2), + |(poly, poly2, _, _)| poly.union(poly2), BatchSize::SmallInput, ); }); @@ -66,7 +66,7 @@ fn run_complex(c: &mut Criterion) { |b, _| { b.iter_batched( polys.sampler(), - |&(_, _, ref poly, ref poly2)| OtherBooleanOp::intersection(poly, poly2), + |(_, _, poly, poly2)| OtherBooleanOp::intersection(poly, poly2), BatchSize::SmallInput, ); }, @@ -75,7 +75,7 @@ fn run_complex(c: &mut Criterion) { group.bench_with_input(BenchmarkId::new("rgbops::union", steps), &(), |b, _| { b.iter_batched( polys.sampler(), - |&(_, _, ref poly, ref poly2)| OtherBooleanOp::union(poly, poly2), + |(_, _, poly, poly2)| OtherBooleanOp::union(poly, poly2), BatchSize::SmallInput, ); }); @@ -83,7 +83,7 @@ fn run_complex(c: &mut Criterion) { group.bench_with_input(BenchmarkId::new("geo::relate", steps), &(), |b, _| { b.iter_batched( polys.sampler(), - |&(ref poly, ref poly2, _, _)| poly.relate(poly2).is_intersects(), + |(poly, poly2, _, _)| poly.relate(poly2).is_intersects(), BatchSize::SmallInput, ); }); diff --git a/geo-postgis/Cargo.toml b/geo-postgis/Cargo.toml index aec0321dc5..9334193794 100644 --- a/geo-postgis/Cargo.toml +++ b/geo-postgis/Cargo.toml @@ -8,7 +8,7 @@ documentation = "https://docs.rs/geo-postgis/" readme = "../README.md" keywords = ["gis", "geo", "geography", "geospatial", "postgis"] description = "Conversion between `geo-types` and `postgis` types." -rust-version = "1.63" +rust-version = "1.65" edition = "2021" [dependencies] diff --git a/geo-types/CHANGES.md b/geo-types/CHANGES.md index a347c6e09b..c3d959910f 100644 --- a/geo-types/CHANGES.md +++ b/geo-types/CHANGES.md @@ -1,8 +1,19 @@ # Changes -## Unreleased +## 0.7.12 -- Add new changes here. +* Add `Polygon::try_exterior_mut` and `Polygon::try_interiors_mut`. + +* Add `wkt!` macro to define geometries at compile time. + + +## 0.7.11 +* Bump rstar dependency + + +## 0.7.10 + +* Implement `From<&Line>` for `LineString` ## 0.7.9 diff --git a/geo-types/Cargo.toml b/geo-types/Cargo.toml index 26977ebad1..ab59eff55f 100644 --- a/geo-types/Cargo.toml +++ b/geo-types/Cargo.toml @@ -1,18 +1,18 @@ [package] name = "geo-types" -version = "0.7.9" +version = "0.7.12" license = "MIT OR Apache-2.0" repository = "https://github.com/georust/geo" documentation = "https://docs.rs/geo-types/" readme = "../README.md" keywords = ["gis", "geo", "geography", "geospatial"] description = "Geospatial primitive data types" -rust-version = "1.63" +rust-version = "1.65" edition = "2021" [features] default = ["std"] -std = ["approx/std", "num-traits/std", "serde/std"] +std = ["approx?/std", "num-traits/std", "serde?/std"] # Prefer `use-rstar` feature rather than enabling rstar directly. # rstar integration relies on the optional approx crate, but implicit features cannot yet enable other features. # See: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#namespaced-features @@ -21,6 +21,7 @@ use-rstar = ["use-rstar_0_8"] use-rstar_0_8 = ["rstar_0_8", "approx"] use-rstar_0_9 = ["rstar_0_9", "approx"] use-rstar_0_10 = ["rstar_0_10", "approx"] +use-rstar_0_11 = ["rstar_0_11", "approx"] [dependencies] approx = { version = ">= 0.4.0, < 0.6.0", optional = true, default-features = false } @@ -29,6 +30,7 @@ num-traits = { version = "0.2", default-features = false, features = ["libm"] } rstar_0_8 = { package = "rstar", version = "0.8", optional = true } rstar_0_9 = { package = "rstar", version = "0.9", optional = true } rstar_0_10 = { package = "rstar", version = "0.10", optional = true } +rstar_0_11 = { package = "rstar", version = "0.11", optional = true } serde = { version = "1", optional = true, default-features = false, features = ["alloc", "derive"] } [dev-dependencies] diff --git a/geo-types/src/geometry/coord.rs b/geo-types/src/geometry/coord.rs index 0a8012d5d3..ddb306afa6 100644 --- a/geo-types/src/geometry/coord.rs +++ b/geo-types/src/geometry/coord.rs @@ -426,3 +426,39 @@ where } } } + +#[cfg(feature = "rstar_0_11")] +impl ::rstar_0_11::Point for Coord +where + T: ::num_traits::Float + ::rstar_0_11::RTreeNum, +{ + type Scalar = T; + + const DIMENSIONS: usize = 2; + + #[inline] + fn generate(mut generator: impl FnMut(usize) -> Self::Scalar) -> Self { + coord! { + x: generator(0), + y: generator(1), + } + } + + #[inline] + fn nth(&self, index: usize) -> Self::Scalar { + match index { + 0 => self.x, + 1 => self.y, + _ => unreachable!(), + } + } + + #[inline] + fn nth_mut(&mut self, index: usize) -> &mut Self::Scalar { + match index { + 0 => &mut self.x, + 1 => &mut self.y, + _ => unreachable!(), + } + } +} diff --git a/geo-types/src/geometry/geometry_collection.rs b/geo-types/src/geometry/geometry_collection.rs index ce5bb23383..a870f1ff70 100644 --- a/geo-types/src/geometry/geometry_collection.rs +++ b/geo-types/src/geometry/geometry_collection.rs @@ -318,7 +318,7 @@ where return false; } - let mut mp_zipper = self.into_iter().zip(other.into_iter()); + let mut mp_zipper = self.into_iter().zip(other); mp_zipper.all(|(lhs, rhs)| lhs.abs_diff_eq(rhs, epsilon)) } } diff --git a/geo-types/src/geometry/line.rs b/geo-types/src/geometry/line.rs index d12c1bfd2e..f4e30a1080 100644 --- a/geo-types/src/geometry/line.rs +++ b/geo-types/src/geometry/line.rs @@ -221,7 +221,12 @@ impl + CoordNum> AbsDiffEq for Line { } } -#[cfg(any(feature = "rstar_0_8", feature = "rstar_0_9", feature = "rstar_0_10"))] +#[cfg(any( + feature = "rstar_0_8", + feature = "rstar_0_9", + feature = "rstar_0_10", + feature = "rstar_0_11" +))] macro_rules! impl_rstar_line { ($rstar:ident) => { impl ::$rstar::RTreeObject for Line @@ -257,6 +262,9 @@ impl_rstar_line!(rstar_0_9); #[cfg(feature = "rstar_0_10")] impl_rstar_line!(rstar_0_10); +#[cfg(feature = "rstar_0_11")] +impl_rstar_line!(rstar_0_11); + #[cfg(test)] mod test { use super::*; diff --git a/geo-types/src/geometry/line_string.rs b/geo-types/src/geometry/line_string.rs index 9ff71fb24e..2bbe303f43 100644 --- a/geo-types/src/geometry/line_string.rs +++ b/geo-types/src/geometry/line_string.rs @@ -346,6 +346,12 @@ impl>> From> for LineString { impl From> for LineString { fn from(line: Line) -> Self { + LineString::from(&line) + } +} + +impl From<&Line> for LineString { + fn from(line: &Line) -> Self { Self(vec![line.start, line.end]) } } @@ -480,7 +486,12 @@ impl + CoordNum> AbsDiffEq for LineString { } } -#[cfg(any(feature = "rstar_0_8", feature = "rstar_0_9", feature = "rstar_0_10"))] +#[cfg(any( + feature = "rstar_0_8", + feature = "rstar_0_9", + feature = "rstar_0_10", + feature = "rstar_0_11" +))] macro_rules! impl_rstar_line_string { ($rstar:ident) => { impl ::$rstar::RTreeObject for LineString @@ -530,6 +541,9 @@ impl_rstar_line_string!(rstar_0_9); #[cfg(feature = "rstar_0_10")] impl_rstar_line_string!(rstar_0_10); +#[cfg(feature = "rstar_0_11")] +impl_rstar_line_string!(rstar_0_11); + #[cfg(test)] mod test { use super::*; diff --git a/geo-types/src/geometry/multi_line_string.rs b/geo-types/src/geometry/multi_line_string.rs index 5cc84b5150..11b0c8ff88 100644 --- a/geo-types/src/geometry/multi_line_string.rs +++ b/geo-types/src/geometry/multi_line_string.rs @@ -189,7 +189,7 @@ where return false; } - let mut mp_zipper = self.into_iter().zip(other.into_iter()); + let mut mp_zipper = self.into_iter().zip(other); mp_zipper.all(|(lhs, rhs)| lhs.abs_diff_eq(rhs, epsilon)) } } @@ -197,29 +197,21 @@ where #[cfg(test)] mod test { use super::*; - use crate::line_string; + use crate::{line_string, wkt}; #[test] fn test_iter() { - let multi: Vec> = vec![ - line_string![(x: 0, y: 0), (x: 2, y: 0), (x: 1, y: 2), (x:0, y:0)], - line_string![(x: 10, y: 10), (x: 12, y: 10), (x: 11, y: 12), (x:10, y:10)], - ]; - let multi: MultiLineString = MultiLineString::new(multi); + let multi: MultiLineString = wkt! { + MULTILINESTRING((0 0,2 0,1 2,0 0), (10 10,12 10,11 12,10 10)) + }; let mut first = true; for p in &multi { if first { - assert_eq!( - p, - &line_string![(x: 0, y: 0), (x: 2, y: 0), (x: 1, y: 2), (x:0, y:0)] - ); + assert_eq!(p, &wkt! { LINESTRING(0 0,2 0,1 2,0 0) }); first = false; } else { - assert_eq!( - p, - &line_string![(x: 10, y: 10), (x: 12, y: 10), (x: 11, y: 12), (x:10, y:10)] - ); + assert_eq!(p, &wkt! { LINESTRING(10 10,12 10,11 12,10 10) }); } } @@ -227,16 +219,10 @@ mod test { first = true; for p in &multi { if first { - assert_eq!( - p, - &line_string![(x: 0, y: 0), (x: 2, y: 0), (x: 1, y: 2), (x:0, y:0)] - ); + assert_eq!(p, &wkt! { LINESTRING(0 0,2 0,1 2,0 0) }); first = false; } else { - assert_eq!( - p, - &line_string![(x: 10, y: 10), (x: 12, y: 10), (x: 11, y: 12), (x:10, y:10)] - ); + assert_eq!(p, &wkt! { LINESTRING(10 10,12 10,11 12,10 10) }); } } } diff --git a/geo-types/src/geometry/multi_point.rs b/geo-types/src/geometry/multi_point.rs index c59622013d..ff3c16c6f8 100644 --- a/geo-types/src/geometry/multi_point.rs +++ b/geo-types/src/geometry/multi_point.rs @@ -35,16 +35,16 @@ use core::iter::FromIterator; pub struct MultiPoint(pub Vec>); impl>> From for MultiPoint { - /// Convert a single `Point` (or something which can be converted to a `Point`) into a - /// one-member `MultiPoint` + /// Convert a single `Point` (or something which can be converted to a + /// `Point`) into a one-member `MultiPoint` fn from(x: IP) -> Self { Self(vec![x.into()]) } } impl>> From> for MultiPoint { - /// Convert a `Vec` of `Points` (or `Vec` of things which can be converted to a `Point`) into a - /// `MultiPoint`. + /// Convert a `Vec` of `Points` (or `Vec` of things which can be converted + /// to a `Point`) into a `MultiPoint`. fn from(v: Vec) -> Self { Self(v.into_iter().map(|p| p.into()).collect()) } @@ -90,6 +90,14 @@ impl MultiPoint { Self(value) } + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + pub fn iter(&self) -> impl Iterator> { self.0.iter() } @@ -170,7 +178,7 @@ where return false; } - let mut mp_zipper = self.into_iter().zip(other.into_iter()); + let mut mp_zipper = self.into_iter().zip(other); mp_zipper.all(|(lhs, rhs)| lhs.abs_diff_eq(rhs, epsilon)) } } @@ -178,11 +186,11 @@ where #[cfg(test)] mod test { use super::*; - use crate::point; + use crate::{point, wkt}; #[test] fn test_iter() { - let multi = MultiPoint::new(vec![point![x: 0, y: 0], point![x: 10, y: 10]]); + let multi = wkt! { MULTIPOINT(0 0,10 10) }; let mut first = true; for p in &multi { @@ -208,7 +216,7 @@ mod test { #[test] fn test_iter_mut() { - let mut multi = MultiPoint::new(vec![point![x: 0, y: 0], point![x: 10, y: 10]]); + let mut multi = wkt! { MULTIPOINT(0 0,10 10) }; for point in &mut multi { point.0.x += 1; @@ -235,26 +243,25 @@ mod test { fn test_relative_eq() { let delta = 1e-6; - let multi = MultiPoint::new(vec![point![x: 0., y: 0.], point![x: 10., y: 10.]]); + let multi = wkt! { MULTIPOINT(0. 0.,10. 10.) }; + + let mut multi_x = multi.clone(); + *multi_x.0[0].x_mut() += delta; - let multi_x = MultiPoint::new(vec![point![x: 0., y: 0.], point![x: 10.+delta, y: 10.]]); assert!(multi.relative_eq(&multi_x, 1e-2, 1e-2)); assert!(multi.relative_ne(&multi_x, 1e-12, 1e-12)); - let multi_y = MultiPoint::new(vec![point![x: 0., y: 0.], point![x: 10., y: 10.+delta]]); + let mut multi_y = multi.clone(); + *multi_y.0[0].y_mut() += delta; assert!(multi.relative_eq(&multi_y, 1e-2, 1e-2)); assert!(multi.relative_ne(&multi_y, 1e-12, 1e-12)); // Under-sized but otherwise equal. - let multi_undersized = MultiPoint::new(vec![point![x: 0., y: 0.]]); + let multi_undersized = wkt! { MULTIPOINT(0. 0.) }; assert!(multi.relative_ne(&multi_undersized, 1., 1.)); // Over-sized but otherwise equal. - let multi_oversized = MultiPoint::new(vec![ - point![x: 0., y: 0.], - point![x: 10., y: 10.], - point![x: 10., y:100.], - ]); + let multi_oversized = wkt! { MULTIPOINT(0. 0.,10. 10.,10. 100.) }; assert!(multi.relative_ne(&multi_oversized, 1., 1.)); } @@ -262,26 +269,24 @@ mod test { fn test_abs_diff_eq() { let delta = 1e-6; - let multi = MultiPoint::new(vec![point![x: 0., y: 0.], point![x: 10., y: 10.]]); + let multi = wkt! { MULTIPOINT(0. 0.,10. 10.) }; - let multi_x = MultiPoint::new(vec![point![x: 0., y: 0.], point![x: 10.+delta, y: 10.]]); + let mut multi_x = multi.clone(); + *multi_x.0[0].x_mut() += delta; assert!(multi.abs_diff_eq(&multi_x, 1e-2)); assert!(multi.abs_diff_ne(&multi_x, 1e-12)); - let multi_y = MultiPoint::new(vec![point![x: 0., y: 0.], point![x: 10., y: 10.+delta]]); + let mut multi_y = multi.clone(); + *multi_y.0[0].y_mut() += delta; assert!(multi.abs_diff_eq(&multi_y, 1e-2)); assert!(multi.abs_diff_ne(&multi_y, 1e-12)); // Under-sized but otherwise equal. - let multi_undersized = MultiPoint::new(vec![point![x: 0., y: 0.]]); + let multi_undersized = wkt! { MULTIPOINT(0. 0.) }; assert!(multi.abs_diff_ne(&multi_undersized, 1.)); // Over-sized but otherwise equal. - let multi_oversized = MultiPoint::new(vec![ - point![x: 0., y: 0.], - point![x: 10., y: 10.], - point![x: 10., y:100.], - ]); + let multi_oversized = wkt! { MULTIPOINT(0. 0.,10. 10.,10. 100.) }; assert!(multi.abs_diff_ne(&multi_oversized, 1.)); } } diff --git a/geo-types/src/geometry/multi_polygon.rs b/geo-types/src/geometry/multi_polygon.rs index 15116353e3..86f68768d0 100644 --- a/geo-types/src/geometry/multi_polygon.rs +++ b/geo-types/src/geometry/multi_polygon.rs @@ -166,7 +166,7 @@ where return false; } - let mut mp_zipper = self.into_iter().zip(other.into_iter()); + let mut mp_zipper = self.into_iter().zip(other); mp_zipper.all(|(lhs, rhs)| lhs.abs_diff_eq(rhs, epsilon)) } } diff --git a/geo-types/src/geometry/point.rs b/geo-types/src/geometry/point.rs index 2d6ca23811..16ca9a8d65 100644 --- a/geo-types/src/geometry/point.rs +++ b/geo-types/src/geometry/point.rs @@ -674,6 +674,35 @@ where } } +#[cfg(feature = "rstar_0_11")] +impl ::rstar_0_11::Point for Point +where + T: ::num_traits::Float + ::rstar_0_11::RTreeNum, +{ + type Scalar = T; + + const DIMENSIONS: usize = 2; + + fn generate(mut generator: impl FnMut(usize) -> Self::Scalar) -> Self { + Point::new(generator(0), generator(1)) + } + + fn nth(&self, index: usize) -> Self::Scalar { + match index { + 0 => self.0.x, + 1 => self.0.y, + _ => unreachable!(), + } + } + fn nth_mut(&mut self, index: usize) -> &mut Self::Scalar { + match index { + 0 => &mut self.0.x, + 1 => &mut self.0.y, + _ => unreachable!(), + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/geo-types/src/geometry/polygon.rs b/geo-types/src/geometry/polygon.rs index 1fffd3a2ed..95b24195d7 100644 --- a/geo-types/src/geometry/polygon.rs +++ b/geo-types/src/geometry/polygon.rs @@ -246,6 +246,16 @@ impl Polygon { self.exterior.close(); } + /// Fallible alternative to [`exterior_mut`](Polygon::exterior_mut). + pub fn try_exterior_mut(&mut self, f: F) -> Result<(), E> + where + F: FnOnce(&mut LineString) -> Result<(), E>, + { + f(&mut self.exterior)?; + self.exterior.close(); + Ok(()) + } + /// Return a slice of the interior `LineString` rings. /// /// # Examples @@ -350,6 +360,18 @@ impl Polygon { } } + /// Fallible alternative to [`interiors_mut`](Self::interiors_mut). + pub fn try_interiors_mut(&mut self, f: F) -> Result<(), E> + where + F: FnOnce(&mut [LineString]) -> Result<(), E>, + { + f(&mut self.interiors)?; + for interior in &mut self.interiors { + interior.close(); + } + Ok(()) + } + /// Add an interior ring to the `Polygon`. /// /// The new `LineString` interior ring [will be closed]: @@ -545,7 +567,12 @@ impl + CoordNum> AbsDiffEq for Polygon { } } -#[cfg(any(feature = "rstar_0_8", feature = "rstar_0_9", feature = "rstar_0_10"))] +#[cfg(any( + feature = "rstar_0_8", + feature = "rstar_0_9", + feature = "rstar_0_10", + feature = "rstar_0_11" +))] macro_rules! impl_rstar_polygon { ($rstar:ident) => { impl $rstar::RTreeObject for Polygon @@ -569,3 +596,6 @@ impl_rstar_polygon!(rstar_0_9); #[cfg(feature = "rstar_0_10")] impl_rstar_polygon!(rstar_0_10); + +#[cfg(feature = "rstar_0_11")] +impl_rstar_polygon!(rstar_0_11); diff --git a/geo-types/src/lib.rs b/geo-types/src/lib.rs index da37dc5fab..4b6a4fdfc1 100644 --- a/geo-types/src/lib.rs +++ b/geo-types/src/lib.rs @@ -68,6 +68,7 @@ //! - `use-rstar_0_8`: Allows geometry types to be inserted into [rstar] R*-trees (`rstar v0.8`) //! - `use-rstar_0_9`: Allows geometry types to be inserted into [rstar] R*-trees (`rstar v0.9`) //! - `use-rstar_0_10`: Allows geometry types to be inserted into [rstar] R*-trees (`rstar v0.10`) +//! - `use-rstar_0_11`: Allows geometry types to be inserted into [rstar] R*-trees (`rstar v0.11`) //! //! This library can be used in `#![no_std]` environments if the default `std` feature is disabled. At //! the moment, the `arbitrary` and `use-rstar_0_8` features require `std`. This may change in a @@ -83,7 +84,6 @@ //! [rstar]: https://github.com/Stoeoef/rstar //! [Serde]: https://serde.rs/ extern crate alloc; -extern crate num_traits; use core::fmt::Debug; use num_traits::{Float, Num, NumCast}; @@ -92,9 +92,6 @@ use num_traits::{Float, Num, NumCast}; #[macro_use] extern crate serde; -#[cfg(feature = "rstar_0_8")] -extern crate rstar_0_8; - #[cfg(test)] #[macro_use] extern crate approx; @@ -133,10 +130,18 @@ pub use error::Error; #[macro_use] mod macros; +#[macro_use] +mod wkt_macro; + #[cfg(feature = "arbitrary")] mod arbitrary; -#[cfg(any(feature = "rstar_0_8", feature = "rstar_0_9", feature = "rstar_0_10"))] +#[cfg(any( + feature = "rstar_0_8", + feature = "rstar_0_9", + feature = "rstar_0_10", + feature = "rstar_0_11" +))] #[doc(hidden)] pub mod private_utils; @@ -145,8 +150,6 @@ pub mod _alloc { //! Needed to access these types from `alloc` in macros when the std feature is //! disabled and the calling context is missing `extern crate alloc`. These are //! _not_ meant for public use. - - pub use ::alloc::boxed::Box; pub use ::alloc::vec; } @@ -273,6 +276,21 @@ mod tests { assert_relative_eq!(25.999999999999996, l.distance_2(&Point::new(4.0, 10.0))); } + #[cfg(feature = "rstar_0_11")] + #[test] + /// ensure Line's SpatialObject impl is correct + fn line_test_0_11() { + use rstar_0_11::primitives::Line as RStarLine; + use rstar_0_11::{PointDistance, RTreeObject}; + + let rl = RStarLine::new(Point::new(0.0, 0.0), Point::new(5.0, 5.0)); + let l = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 5., y: 5. }); + assert_eq!(rl.envelope(), l.envelope()); + // difference in 15th decimal place + assert_relative_eq!(26.0, rl.distance_2(&Point::new(4.0, 10.0))); + assert_relative_eq!(25.999999999999996, l.distance_2(&Point::new(4.0, 10.0))); + } + #[test] fn test_rects() { let r = Rect::new(coord! { x: -1., y: -1. }, coord! { x: 1., y: 1. }); diff --git a/geo-types/src/macros.rs b/geo-types/src/macros.rs index 491a8e73a9..170d382cb4 100644 --- a/geo-types/src/macros.rs +++ b/geo-types/src/macros.rs @@ -123,7 +123,7 @@ macro_rules! coord { /// [`LineString`]: ./line_string/struct.LineString.html #[macro_export] macro_rules! line_string { - () => { $crate::LineString::new(vec![]) }; + () => { $crate::LineString::new($crate::_alloc::vec![]) }; ( $(( $($tag:tt : $val:expr),* $(,)? )),* $(,)? @@ -139,11 +139,9 @@ macro_rules! line_string { $(,)? ) => { $crate::LineString::new( - <[_]>::into_vec( - $crate::_alloc::Box::new( - [$($coord), *] - ) - ) + $crate::_alloc::vec![ + $($coord),* + ] ) }; } @@ -216,7 +214,7 @@ macro_rules! line_string { /// [`Polygon`]: ./struct.Polygon.html #[macro_export] macro_rules! polygon { - () => { $crate::Polygon::new($crate::line_string![], vec![]) }; + () => { $crate::Polygon::new($crate::line_string![], $crate::_alloc::vec![]) }; ( exterior: [ $(( $($exterior_tag:tt : $exterior_val:expr),* $(,)? )),* @@ -262,15 +260,11 @@ macro_rules! polygon { $crate::line_string![ $($exterior_coord), * ], - <[_]>::into_vec( - $crate::_alloc::Box::new( - [ - $( - $crate::line_string![$($interior_coord),*] - ), * - ] - ) - ) + $crate::_alloc::vec![ + $( + $crate::line_string![$($interior_coord),*] + ), * + ] ) }; ( diff --git a/geo-types/src/wkt_macro.rs b/geo-types/src/wkt_macro.rs new file mode 100644 index 0000000000..df39e2667a --- /dev/null +++ b/geo-types/src/wkt_macro.rs @@ -0,0 +1,328 @@ +/// Creates a [`crate::geometry`] from a +/// [WKT](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry) literal. +/// +/// This is evaluated at compile time, so you don't need to worry about runtime errors from inavlid +/// WKT syntax. +/// +/// Note that `POINT EMPTY` is not accepted because it is not representable as a `geo_types::Point`. +/// +/// ``` +/// use geo_types::wkt; +/// let point = wkt! { POINT(1.0 2.0) }; +/// assert_eq!(point.x(), 1.0); +/// assert_eq!(point.y(), 2.0); +/// +/// let geometry_collection = wkt! { +/// GEOMETRYCOLLECTION( +/// POINT(1.0 2.0), +/// LINESTRING EMPTY, +/// POLYGON((0.0 0.0,1.0 0.0,1.0 1.0,0.0 0.0)) +/// ) +/// }; +/// assert_eq!(geometry_collection.len(), 3); +/// ``` +#[macro_export] +macro_rules! wkt { + // Hide distracting implementation details from the generated rustdoc. + ($($wkt:tt)+) => { + { + $crate::wkt_internal!($($wkt)+) + } + }; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! wkt_internal { + (POINT EMPTY) => { + compile_error!("EMPTY points are not supported in geo-types") + }; + (POINT($x: literal $y: literal)) => { + $crate::point!(x: $x, y: $y) + }; + (POINT $($tail: tt)*) => { + compile_error!("Invalid POINT wkt"); + }; + (LINESTRING EMPTY) => { + $crate::line_string![] + }; + (LINESTRING ($($x: literal $y: literal),+)) => { + $crate::line_string![ + $($crate::coord!(x: $x, y: $y)),* + ] + }; + (LINESTRING ()) => { + compile_error!("use `EMPTY` instead of () for an empty collection") + }; + (LINESTRING $($tail: tt)*) => { + compile_error!("Invalid LINESTRING wkt"); + }; + (POLYGON EMPTY) => { + $crate::polygon![] + }; + (POLYGON ( $exterior_tt: tt )) => { + $crate::Polygon::new($crate::wkt!(LINESTRING $exterior_tt), $crate::_alloc::vec![]) + }; + (POLYGON( $exterior_tt: tt, $($interiors_tt: tt),+ )) => { + $crate::Polygon::new( + $crate::wkt!(LINESTRING $exterior_tt), + $crate::_alloc::vec![ + $($crate::wkt!(LINESTRING $interiors_tt)),* + ] + ) + }; + (POLYGON ()) => { + compile_error!("use `EMPTY` instead of () for an empty collection") + }; + (POLYGON $($tail: tt)*) => { + compile_error!("Invalid POLYGON wkt"); + }; + (MULTIPOINT EMPTY) => { + $crate::MultiPoint($crate::_alloc::vec![]) + }; + (MULTIPOINT ()) => { + compile_error!("use `EMPTY` instead of () for an empty collection") + }; + (MULTIPOINT ($($x: literal $y: literal),* )) => { + $crate::MultiPoint( + $crate::_alloc::vec![$($crate::point!(x: $x, y: $y)),*] + ) + }; + (MULTIPOINT $($tail: tt)*) => { + compile_error!("Invalid MULTIPOINT wkt"); + }; + (MULTILINESTRING EMPTY) => { + $crate::MultiLineString($crate::_alloc::vec![]) + }; + (MULTILINESTRING ()) => { + compile_error!("use `EMPTY` instead of () for an empty collection") + }; + (MULTILINESTRING ( $($line_string_tt: tt),* )) => { + $crate::MultiLineString($crate::_alloc::vec![ + $($crate::wkt!(LINESTRING $line_string_tt)),* + ]) + }; + (MULTILINESTRING $($tail: tt)*) => { + compile_error!("Invalid MULTILINESTRING wkt"); + }; + (MULTIPOLYGON EMPTY) => { + $crate::MultiPolygon($crate::_alloc::vec![]) + }; + (MULTIPOLYGON ()) => { + compile_error!("use `EMPTY` instead of () for an empty collection") + }; + (MULTIPOLYGON ( $($polygon_tt: tt),* )) => { + $crate::MultiPolygon($crate::_alloc::vec![ + $($crate::wkt!(POLYGON $polygon_tt)),* + ]) + }; + (MULTIPOLYGON $($tail: tt)*) => { + compile_error!("Invalid MULTIPOLYGON wkt"); + }; + (GEOMETRYCOLLECTION EMPTY) => { + $crate::GeometryCollection($crate::_alloc::vec![]) + }; + (GEOMETRYCOLLECTION ()) => { + compile_error!("use `EMPTY` instead of () for an empty collection") + }; + (GEOMETRYCOLLECTION ( $($el_type:tt $el_tt: tt),* )) => { + $crate::GeometryCollection($crate::_alloc::vec![ + $($crate::Geometry::from($crate::wkt!($el_type $el_tt))),* + ]) + }; + (GEOMETRYCOLLECTION $($tail: tt)*) => { + compile_error!("Invalid GEOMETRYCOLLECTION wkt"); + }; + ($name: ident ($($tail: tt)*)) => { + compile_error!("Unknown type. Must be one of POINT, LINESTRING, POLYGON, MULTIPOINT, MULTILINESTRING, MULTIPOLYGON, or GEOMETRYCOLLECTION"); + }; +} + +#[cfg(test)] +mod test { + use crate::geometry::*; + use alloc::vec; + + #[test] + fn point() { + let point = wkt! { POINT(1.0 2.0) }; + assert_eq!(point.x(), 1.0); + assert_eq!(point.y(), 2.0); + + let point = wkt! { POINT(1.0 2.0) }; + assert_eq!(point.x(), 1.0); + assert_eq!(point.y(), 2.0); + + // This (rightfully) fails to compile because geo-types doesn't support "empty" points + // wkt! { POINT EMPTY } + } + + #[test] + fn empty_line_string() { + let line_string: LineString = wkt! { LINESTRING EMPTY }; + assert_eq!(line_string.0.len(), 0); + + // This (rightfully) fails to compile because its invalid wkt + // wkt! { LINESTRING() } + } + + #[test] + fn line_string() { + let line_string = wkt! { LINESTRING(1.0 2.0,3.0 4.0) }; + assert_eq!(line_string.0.len(), 2); + assert_eq!(line_string[0], coord! { x: 1.0, y: 2.0 }); + } + + #[test] + fn empty_polygon() { + let polygon: Polygon = wkt! { POLYGON EMPTY }; + assert_eq!(polygon.exterior().0.len(), 0); + assert_eq!(polygon.interiors().len(), 0); + + // This (rightfully) fails to compile because its invalid wkt + // wkt! { POLYGON() } + } + + #[test] + fn polygon() { + let polygon = wkt! { POLYGON((1.0 2.0)) }; + assert_eq!(polygon.exterior().0.len(), 1); + assert_eq!(polygon.exterior().0[0], coord! { x: 1.0, y: 2.0 }); + + let polygon = wkt! { POLYGON((1.0 2.0,3.0 4.0)) }; + // Note: an extra coord is added to close the linestring + assert_eq!(polygon.exterior().0.len(), 3); + assert_eq!(polygon.exterior().0[0], coord! { x: 1.0, y: 2.0 }); + assert_eq!(polygon.exterior().0[1], coord! { x: 3.0, y: 4.0 }); + assert_eq!(polygon.exterior().0[2], coord! { x: 1.0, y: 2.0 }); + + let polygon = wkt! { POLYGON((1.0 2.0), (1.1 2.1)) }; + assert_eq!(polygon.exterior().0.len(), 1); + assert_eq!(polygon.interiors().len(), 1); + + assert_eq!(polygon.exterior().0[0], coord! { x: 1.0, y: 2.0 }); + assert_eq!(polygon.interiors()[0].0[0], coord! { x: 1.1, y: 2.1 }); + + let polygon = wkt! { POLYGON((1.0 2.0,3.0 4.0), (1.1 2.1,3.1 4.1), (1.2 2.2,3.2 4.2)) }; + assert_eq!(polygon.exterior().0.len(), 3); + assert_eq!(polygon.interiors().len(), 2); + assert_eq!(polygon.interiors()[1][1], coord! { x: 3.2, y: 4.2 }); + } + + #[test] + fn empty_multi_point() { + let multipoint: MultiPoint = wkt! { MULTIPOINT EMPTY }; + assert!(multipoint.0.is_empty()); + // This (rightfully) fails to compile because its invalid wkt + // wkt! { MULTIPOINT() } + } + + #[test] + fn multi_point() { + let multi_point = wkt! { MULTIPOINT(1.0 2.0) }; + assert_eq!(multi_point.0, vec![point! { x: 1.0, y: 2.0}]); + + let multi_point = wkt! { MULTIPOINT(1.0 2.0,3.0 4.0) }; + assert_eq!( + multi_point.0, + vec![point! { x: 1.0, y: 2.0}, point! { x: 3.0, y: 4.0}] + ); + } + + #[test] + fn empty_multi_line_string() { + let multi_line_string: MultiLineString = wkt! { MULTILINESTRING EMPTY }; + assert_eq!(multi_line_string.0, vec![]); + // This (rightfully) fails to compile because its invalid wkt + // wkt! { MULTILINESTRING() } + } + #[test] + fn multi_line_string() { + let multi_line_string = wkt! { MULTILINESTRING ((1.0 2.0,3.0 4.0)) }; + assert_eq!(multi_line_string.0.len(), 1); + assert_eq!(multi_line_string.0[0].0[1], coord! { x: 3.0, y: 4.0 }); + let multi_line_string = wkt! { MULTILINESTRING ((1.0 2.0,3.0 4.0),(5.0 6.0,7.0 8.0)) }; + assert_eq!(multi_line_string.0.len(), 2); + assert_eq!(multi_line_string.0[1].0[1], coord! { x: 7.0, y: 8.0 }); + + let multi_line_string = wkt! { MULTILINESTRING ((1.0 2.0,3.0 4.0),EMPTY) }; + assert_eq!(multi_line_string.0.len(), 2); + assert_eq!(multi_line_string.0[1].0.len(), 0); + } + + #[test] + fn empty_multi_polygon() { + let multi_polygon: MultiPolygon = wkt! { MULTIPOLYGON EMPTY }; + assert!(multi_polygon.0.is_empty()); + + // This (rightfully) fails to compile because its invalid wkt + // wkt! { MULTIPOLYGON() } + } + + #[test] + fn multi_line_polygon() { + let multi_polygon = wkt! { MULTIPOLYGON (((1.0 2.0))) }; + assert_eq!(multi_polygon.0.len(), 1); + assert_eq!(multi_polygon.0[0].exterior().0[0], coord! { x: 1.0, y: 2.0}); + + let multi_polygon = wkt! { MULTIPOLYGON (((1.0 2.0,3.0 4.0), (1.1 2.1,3.1 4.1), (1.2 2.2,3.2 4.2)),((1.0 2.0))) }; + assert_eq!(multi_polygon.0.len(), 2); + assert_eq!( + multi_polygon.0[0].interiors()[1].0[0], + coord! { x: 1.2, y: 2.2} + ); + + let multi_polygon = wkt! { MULTIPOLYGON (((1.0 2.0,3.0 4.0), (1.1 2.1,3.1 4.1), (1.2 2.2,3.2 4.2)), EMPTY) }; + assert_eq!(multi_polygon.0.len(), 2); + assert_eq!( + multi_polygon.0[0].interiors()[1].0[0], + coord! { x: 1.2, y: 2.2} + ); + assert!(multi_polygon.0[1].exterior().0.is_empty()); + } + + #[test] + fn empty_geometry_collection() { + let geometry_collection: GeometryCollection = wkt! { GEOMETRYCOLLECTION EMPTY }; + assert!(geometry_collection.is_empty()); + + // This (rightfully) fails to compile because its invalid wkt + // wkt! { MULTIPOLYGON() } + } + + #[test] + fn geometry_collection() { + let geometry_collection = wkt! { + GEOMETRYCOLLECTION ( + POINT (40.0 10.0), + LINESTRING (10.0 10.0, 20.0 20.0, 10.0 40.0), + POLYGON ((40.0 40.0, 20.0 45.0, 45.0 30.0, 40.0 40.0)) + ) + }; + assert_eq!(geometry_collection.len(), 3); + + let line_string = match &geometry_collection[1] { + Geometry::LineString(line_string) => line_string, + _ => panic!( + "unexpected geometry: {geometry:?}", + geometry = geometry_collection[1] + ), + }; + assert_eq!(line_string.0[1], coord! {x: 20.0, y: 20.0 }); + } + + #[test] + fn other_numeric_types() { + let point: Point = wkt!(POINT(1 2)); + assert_eq!(point.x(), 1i32); + assert_eq!(point.y(), 2i32); + + let point: Point = wkt!(POINT(1 2)); + assert_eq!(point.x(), 1u64); + assert_eq!(point.y(), 2u64); + + let point: Point = wkt!(POINT(1.0 2.0)); + assert_eq!(point.x(), 1.0f32); + assert_eq!(point.y(), 2.0f32); + } +} diff --git a/geo/CHANGES.md b/geo/CHANGES.md index fa38c4a575..a4a8cdae7a 100644 --- a/geo/CHANGES.md +++ b/geo/CHANGES.md @@ -1,9 +1,56 @@ # Changes -## Unreleased +## 0.27.0 + +* Use `CachedEnvelope` in R-Trees when computing euclidean distance between polygons + * +* Add an `inverse` method to `AffineTransform` + * +* Fix `Densify` trait to avoid panic with empty line string. + * +* Add `DensifyHaversine` trait to densify spherical line geometry. + * +* Add `LineStringSegmentize` trait to split a single `LineString` into `n` `LineStrings` as a `MultiLineString`. + * +* Add `EuclideanDistance` implementations for all remaining geometries. + * +* Add `HausdorffDistance` algorithm trait to calculate the Hausdorff distance between any two geometries. + * +* Add `matches` method to IntersectionMatrix for ergonomic de-9im comparisons. + * +* Simplify `CoordsIter` and `MinimumRotatedRect` `trait`s with GATs by removing an unneeded trait lifetime. + * +* Add `ToDegrees` and `ToRadians` traits. + * +* Add rhumb-line operations analogous to several current haversine operations: `RhumbBearing`, `RhumbDestination`, `RhumbDistance`, `RhumbIntermediate`, `RhumbLength`. + * +* Fix coordinate wrapping in `HaversineDestination` + * +* Add `wkt!` macro to define geometries at compile time. + * +* Add `TriangulateSpade` trait which provides (un)constrained Delaunay Triangulations for all `geo_types` via the `spade` crate + * +* Add `len()` and `is_empty()` to `MultiPoint` + * + +## 0.26.0 + +* Implement "Closest Point" from a `Point` on a `Geometry` using spherical geometry. +* Bump CI containers to use libproj 9.2.1 +* **BREAKING**: Bump rstar and robust dependencies + + +## 0.25.1 - Add `TriangulateEarcut` algorithm trait to triangulate polygons with the earcut algorithm. - +- Add `Vector2DOps` trait to algorithims module and implemented it for `Coord` + - + +- Add a fast point-in-polygon query datastructure that pre-processes a `Polygon` as a set of monotone polygons. Ref. `crate::algorithm::MonotonicPolygons`. + - + + ## 0.25.0 diff --git a/geo/Cargo.toml b/geo/Cargo.toml index 1a13372c91..e587c1299e 100644 --- a/geo/Cargo.toml +++ b/geo/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "geo" description = "Geospatial primitives and algorithms" -version = "0.25.0" +version = "0.27.0" license = "MIT OR Apache-2.0" repository = "https://github.com/georust/geo" documentation = "https://docs.rs/geo/" @@ -9,24 +9,26 @@ readme = "../README.md" keywords = ["gis", "geo", "geography", "geospatial"] autobenches = true edition = "2021" -rust-version = "1.63" +rust-version = "1.65" +categories = ["science::geo"] [features] -default = ["earcutr"] +default = ["earcutr", "spade"] use-proj = ["proj"] proj-network = ["use-proj", "proj/network"] use-serde = ["serde", "geo-types/serde"] [dependencies] earcutr = { version = "0.4.2", optional = true } +spade = { version = "2.2.0", optional = true } float_next_after = "1.0.0" -geo-types = { version = "0.7.9", features = ["approx", "use-rstar_0_10"] } +geo-types = { version = "0.7.12", features = ["approx", "use-rstar_0_11"] } geographiclib-rs = { version = "0.2.3", default-features = false } log = "0.4.11" num-traits = "0.2" proj = { version = "0.27.0", optional = true } -robust = "0.2.2" -rstar = "0.10.0" +robust = "1.1.0" +rstar = "0.11.0" serde = { version = "1.0", optional = true, features = ["derive"] } [dev-dependencies] @@ -104,3 +106,7 @@ harness = false [[bench]] name = "winding_order" harness = false + +[[bench]] +name = "monotone_subdiv" +harness = false diff --git a/geo/benches/area.rs b/geo/benches/area.rs index 85c77b09b1..b295a89887 100644 --- a/geo/benches/area.rs +++ b/geo/benches/area.rs @@ -1,8 +1,4 @@ -#[macro_use] -extern crate criterion; -extern crate geo; - -use criterion::Criterion; +use criterion::{criterion_group, criterion_main, Criterion}; use geo::Area; use geo::Polygon; diff --git a/geo/benches/concave_hull.rs b/geo/benches/concave_hull.rs index 71d243095e..923a54d7c4 100644 --- a/geo/benches/concave_hull.rs +++ b/geo/benches/concave_hull.rs @@ -1,8 +1,4 @@ -#[macro_use] -extern crate criterion; -extern crate geo; - -use criterion::Criterion; +use criterion::{criterion_group, criterion_main, Criterion}; use geo::ConcaveHull; use geo::{Coord, CoordNum}; diff --git a/geo/benches/contains.rs b/geo/benches/contains.rs index 564b22441f..56080b7355 100644 --- a/geo/benches/contains.rs +++ b/geo/benches/contains.rs @@ -1,12 +1,6 @@ -#[macro_use] -extern crate criterion; -#[macro_use] -extern crate geo; - +use criterion::{criterion_group, criterion_main, Criterion}; use geo::contains::Contains; -use geo::{polygon, Line, Point, Polygon}; - -use criterion::Criterion; +use geo::{point, polygon, Line, Point, Polygon}; fn criterion_benchmark(c: &mut Criterion) { c.bench_function("point in simple polygon", |bencher| { diff --git a/geo/benches/convex_hull.rs b/geo/benches/convex_hull.rs index ef3cbe3e6b..94ff419656 100644 --- a/geo/benches/convex_hull.rs +++ b/geo/benches/convex_hull.rs @@ -1,8 +1,4 @@ -#[macro_use] -extern crate criterion; -extern crate geo; - -use criterion::Criterion; +use criterion::{criterion_group, criterion_main, Criterion}; use geo::prelude::*; use geo::{Coord, CoordNum}; diff --git a/geo/benches/euclidean_distance.rs b/geo/benches/euclidean_distance.rs index 6020e90795..6a1c85d60a 100644 --- a/geo/benches/euclidean_distance.rs +++ b/geo/benches/euclidean_distance.rs @@ -1,8 +1,5 @@ -#[macro_use] -extern crate criterion; -extern crate geo; -use geo::convex_hull::ConvexHull; -use geo::euclidean_distance::EuclideanDistance; +use criterion::{criterion_group, criterion_main}; +use geo::algorithm::{ConvexHull, EuclideanDistance}; use geo::{polygon, Polygon}; fn criterion_benchmark(c: &mut criterion::Criterion) { diff --git a/geo/benches/extremes.rs b/geo/benches/extremes.rs index 02be6916e3..4380401d64 100644 --- a/geo/benches/extremes.rs +++ b/geo/benches/extremes.rs @@ -1,8 +1,4 @@ -#[macro_use] -extern crate criterion; -extern crate geo; - -use criterion::Criterion; +use criterion::{criterion_group, criterion_main, Criterion}; use geo::prelude::*; use geo::Polygon; diff --git a/geo/benches/frechet_distance.rs b/geo/benches/frechet_distance.rs index 53183d9801..0377c9633a 100644 --- a/geo/benches/frechet_distance.rs +++ b/geo/benches/frechet_distance.rs @@ -1,6 +1,4 @@ -#[macro_use] -extern crate criterion; -extern crate geo; +use criterion::{criterion_group, criterion_main}; use geo::frechet_distance::FrechetDistance; fn criterion_benchmark(c: &mut criterion::Criterion) { diff --git a/geo/benches/geodesic_distance.rs b/geo/benches/geodesic_distance.rs index f9192d8392..e4cfb5f227 100644 --- a/geo/benches/geodesic_distance.rs +++ b/geo/benches/geodesic_distance.rs @@ -1,8 +1,5 @@ -#[macro_use] -extern crate criterion; -extern crate geo; - -use geo::prelude::*; +use criterion::{criterion_group, criterion_main}; +use geo::algorithm::GeodesicDistance; fn criterion_benchmark(c: &mut criterion::Criterion) { c.bench_function("geodesic distance f64", |bencher| { diff --git a/geo/benches/intersection.rs b/geo/benches/intersection.rs index ac48ac38cb..f0bcaeeba9 100644 --- a/geo/benches/intersection.rs +++ b/geo/benches/intersection.rs @@ -1,9 +1,4 @@ -#[macro_use] -extern crate criterion; -extern crate geo; -extern crate geo_test_fixtures; - -use criterion::Criterion; +use criterion::{criterion_group, criterion_main, Criterion}; use geo::intersects::Intersects; use geo::MultiPolygon; diff --git a/geo/benches/monotone_subdiv.rs b/geo/benches/monotone_subdiv.rs new file mode 100644 index 0000000000..55bde5a24e --- /dev/null +++ b/geo/benches/monotone_subdiv.rs @@ -0,0 +1,137 @@ +use std::fmt::Display; +use std::panic::catch_unwind; + +use criterion::measurement::Measurement; +use geo::coordinate_position::CoordPos; +use geo::monotone::monotone_subdivision; +use geo::{CoordinatePosition, MapCoords, Polygon}; + +use criterion::{ + criterion_group, criterion_main, BatchSize, BenchmarkGroup, BenchmarkId, Criterion, +}; +use geo_types::{Coord, Rect}; +use wkt::ToWkt; + +#[path = "utils/random.rs"] +mod random; +use rand::thread_rng; +use random::*; + +fn criterion_benchmark_pt_in_poly(c: &mut Criterion) { + let pt_samples = Samples::from_fn(512, || { + uniform_point(&mut thread_rng(), Rect::new((-1., -1.), (1., 1.))) + }); + + for size in [16, 64, 512, 1024, 2048] { + let mut grp = c.benchmark_group("rand pt-in-poly steppy-polygon (worst case)".to_string()); + let poly = steppy_polygon(&mut thread_rng(), size); + bench_pt_in_poly(&mut grp, poly, size, &pt_samples) + } + for size in [16, 64, 512, 1024, 2048] { + let mut grp = c.benchmark_group("rand pt-in-poly steppy-polygon (best case)".to_string()); + let poly = steppy_polygon(&mut thread_rng(), size).map_coords(|c| (c.y, c.x).into()); + bench_pt_in_poly(&mut grp, poly, size, &pt_samples) + } + for size in [16, 64, 512, 1024, 2048] { + let mut grp = c.benchmark_group("rand pt-in-poly circular-polygon".to_string()); + let poly = circular_polygon(&mut thread_rng(), size); + bench_pt_in_poly(&mut grp, poly, size, &pt_samples) + } +} + +fn bench_pt_in_poly( + g: &mut BenchmarkGroup, + polygon: Polygon, + param: I, + samples: &Samples>, +) where + T: Measurement, + I: Display + Copy, +{ + let mon = match catch_unwind(|| monotone_subdivision([polygon.clone()])) { + Ok(m) => m, + Err(_) => { + panic!( + "Monotone subdivision failed for polygon: {}", + polygon.to_wkt() + ); + } + }; + + g.bench_with_input( + BenchmarkId::new("Simple point-in-poly", param), + &(), + |b, _| { + b.iter_batched( + samples.sampler(), + |pt| { + polygon.coordinate_position(pt); + }, + BatchSize::SmallInput, + ); + }, + ); + g.bench_with_input( + BenchmarkId::new("Pre-processed point-in-poly", param), + &(), + |b, _| { + b.iter_batched( + samples.sampler(), + |pt| { + mon.iter() + .filter(|mp| mp.coordinate_position(pt) == CoordPos::Inside) + .count(); + }, + BatchSize::SmallInput, + ); + }, + ); +} + +fn criterion_benchmark_monotone_subdiv(c: &mut Criterion) { + for size in [16, 64, 2048, 32000] { + let mut grp = c.benchmark_group("monotone_subdiv steppy-polygon (worst case)".to_string()); + let poly_fn = |size| steppy_polygon(&mut thread_rng(), size); + bench_monotone_subdiv(&mut grp, poly_fn, size) + } + for size in [16, 64, 2048, 32000] { + let mut grp = c.benchmark_group("monotone_subdiv steppy-polygon (best case)".to_string()); + let poly_fn = + |size| steppy_polygon(&mut thread_rng(), size).map_coords(|c| (c.y, c.x).into()); + bench_monotone_subdiv(&mut grp, poly_fn, size) + } + for size in [16, 64, 2048, 32000] { + let mut grp = c.benchmark_group("monotone_subdiv circular-polygon".to_string()); + let poly_fn = |size| circular_polygon(&mut thread_rng(), size); + bench_monotone_subdiv(&mut grp, poly_fn, size) + } +} + +fn bench_monotone_subdiv(g: &mut BenchmarkGroup, mut f: F, param: usize) +where + T: Measurement, + F: FnMut(usize) -> Polygon, +{ + let samples = Samples::from_fn(16, || f(param)); + g.bench_with_input( + BenchmarkId::new("Montone subdivision", param), + &(), + |b, _| { + b.iter_batched( + samples.sampler(), + |pt| { + let mon = monotone_subdivision([pt.clone()]); + mon.len(); + }, + BatchSize::SmallInput, + ); + }, + ); +} + +criterion_group!( + benches, + criterion_benchmark_pt_in_poly, + criterion_benchmark_monotone_subdiv +); +criterion_main!(benches); diff --git a/geo/benches/relate.rs b/geo/benches/relate.rs index b6c50276a8..6678842182 100644 --- a/geo/benches/relate.rs +++ b/geo/benches/relate.rs @@ -1,12 +1,6 @@ -#[macro_use] -extern crate criterion; -extern crate geo; - -use crate::geo::relate::Relate; -use crate::geo::rotate::Rotate; -use crate::geo::translate::Translate; -use criterion::{BatchSize, Criterion}; -use geo::{LineString, Polygon}; +use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; +use geo::algorithm::{Relate, Rotate, Translate}; +use geo::geometry::{LineString, Polygon}; fn criterion_benchmark(c: &mut Criterion) { c.bench_function("relate overlapping 50-point polygons", |bencher| { diff --git a/geo/benches/rotate.rs b/geo/benches/rotate.rs index 2dc4e144cd..e4250acab6 100644 --- a/geo/benches/rotate.rs +++ b/geo/benches/rotate.rs @@ -1,8 +1,4 @@ -#[macro_use] -extern crate criterion; -extern crate geo; - -use criterion::Criterion; +use criterion::{criterion_group, criterion_main, Criterion}; use geo::prelude::*; fn criterion_benchmark(c: &mut Criterion) { diff --git a/geo/benches/simplify.rs b/geo/benches/simplify.rs index c6b98dbdf7..2f5cb4345e 100644 --- a/geo/benches/simplify.rs +++ b/geo/benches/simplify.rs @@ -1,8 +1,4 @@ -#[macro_use] -extern crate criterion; -extern crate geo; - -use criterion::Criterion; +use criterion::{criterion_group, criterion_main, Criterion}; use geo::simplify::Simplify; fn criterion_benchmark(c: &mut Criterion) { diff --git a/geo/benches/simplifyvw.rs b/geo/benches/simplifyvw.rs index dfd2aeb71c..350c104564 100644 --- a/geo/benches/simplifyvw.rs +++ b/geo/benches/simplifyvw.rs @@ -1,8 +1,4 @@ -#[macro_use] -extern crate criterion; -extern crate geo; - -use criterion::Criterion; +use criterion::{criterion_group, criterion_main, Criterion}; use geo::prelude::*; use geo::simplify_vw::SimplifyVwPreserve; diff --git a/geo/benches/utils/random.rs b/geo/benches/utils/random.rs index 42447991cf..84db4de330 100644 --- a/geo/benches/utils/random.rs +++ b/geo/benches/utils/random.rs @@ -1,7 +1,7 @@ #![allow(unused)] use std::f64::consts::PI; -use geo::algorithm::{ConcaveHull, ConvexHull, MapCoords, Rotate}; +use geo::algorithm::{BoundingRect, ConcaveHull, ConvexHull, MapCoords, Rotate}; use geo::geometry::*; use rand::{thread_rng, Rng}; @@ -76,32 +76,50 @@ pub fn circular_polygon(mut rng: R, steps: usize) -> Polygon { pub fn steppy_polygon(mut rng: R, steps: usize) -> Polygon { let mut ring = Vec::with_capacity(2 * steps); - let ystep = 1.0; - let nudge_std = ystep / 1000.0; + let y_step = 10.0; + let nudge_std = y_step / 1000.0; let mut y = 0.0; let normal = Normal::new(0.0, nudge_std * nudge_std).unwrap(); - let shift = 50.0; + let x_shift = 100.0; ring.push((0.0, 0.0).into()); (0..steps).for_each(|_| { - let x: f64 = rng.sample::(Standard) * shift / 2.; - let x = (x * 10.) as i64 as f64 / 10.; - y += ystep; - // y += normal.sample(&mut rng); + let x: f64 = rng.sample::(Standard); + y += y_step; ring.push((x, y).into()); }); - ring.push((shift, y).into()); + ring.push((x_shift, y).into()); (0..steps).for_each(|_| { - let x: f64 = rng.sample::(Standard) * shift; - let x = (x * 10.) as i64 as f64 / 10.; - y -= ystep; + let x: f64 = rng.sample::(Standard); + y -= y_step; // y += normal.sample(&mut rng); - ring.push((shift + x, y).into()); + ring.push((x_shift + x, y).into()); }); - Polygon::new(LineString(ring), vec![]) + normalize_polygon(Polygon::new(LineString(ring), vec![])) } +/// Normalizes polygon to fit and fill `[-1, 1] X [-1, 1]` square. +/// +/// Uses `MapCoord` and `BoundingRect` +pub fn normalize_polygon(poly: Polygon) -> Polygon { + let bounds = poly.bounding_rect().unwrap(); + let dims = bounds.max() - bounds.min(); + let x_scale = 2. / dims.x; + let y_scale = 2. / dims.y; + + let x_shift = -bounds.min().x * x_scale - 1.; + let y_shift = -bounds.min().y * y_scale - 1.; + poly.map_coords(|mut c| { + c.x *= x_scale; + c.x += x_shift; + c.y *= y_scale; + c.y += y_shift; + c + }) +} + +#[derive(Debug, Clone)] pub struct Samples(Vec); impl Samples { pub fn sampler<'a>(&'a self) -> impl FnMut() -> &'a T { @@ -116,4 +134,8 @@ impl Samples { pub fn from_fn T>(size: usize, mut proc: F) -> Self { Self((0..size).map(|_| proc()).collect()) } + + pub fn map U>(self, mut proc: F) -> Samples { + Samples(self.0.into_iter().map(proc).collect()) + } } diff --git a/geo/benches/vincenty_distance.rs b/geo/benches/vincenty_distance.rs index 4abcf76875..ac7cf8915c 100644 --- a/geo/benches/vincenty_distance.rs +++ b/geo/benches/vincenty_distance.rs @@ -1,8 +1,5 @@ -#[macro_use] -extern crate criterion; -extern crate geo; - -use geo::prelude::*; +use criterion::{criterion_group, criterion_main}; +use geo::algorithm::VincentyDistance; fn criterion_benchmark(c: &mut criterion::Criterion) { c.bench_function("vincenty distance f32", |bencher| { diff --git a/geo/benches/winding_order.rs b/geo/benches/winding_order.rs index 219947e942..c3e58caecd 100644 --- a/geo/benches/winding_order.rs +++ b/geo/benches/winding_order.rs @@ -1,8 +1,5 @@ -#[macro_use] -extern crate criterion; -extern crate geo; - -use geo::prelude::*; +use criterion::{criterion_group, criterion_main}; +use geo::algorithm::Winding; fn criterion_benchmark(c: &mut criterion::Criterion) { c.bench_function("winding order: winding_order (f32)", |bencher| { diff --git a/geo/examples/algorithm.rs b/geo/examples/algorithm.rs index 694f19c41b..d5355a799b 100644 --- a/geo/examples/algorithm.rs +++ b/geo/examples/algorithm.rs @@ -1,7 +1,4 @@ -#[macro_use] -extern crate geo; - -use geo::Centroid; +use geo::{line_string, Centroid}; fn main() { let linestring = geo::line_string![ diff --git a/geo/examples/types.rs b/geo/examples/types.rs index 38c9302c57..63cbb9b26f 100644 --- a/geo/examples/types.rs +++ b/geo/examples/types.rs @@ -1,5 +1,3 @@ -extern crate geo; - use geo::Point; use geo_types::point; diff --git a/geo/src/algorithm/affine_ops.rs b/geo/src/algorithm/affine_ops.rs index 978ff3b272..81bff41429 100644 --- a/geo/src/algorithm/affine_ops.rs +++ b/geo/src/algorithm/affine_ops.rs @@ -1,5 +1,10 @@ +use num_traits::ToPrimitive; + +#[cfg(any(feature = "approx", test))] +use approx::{AbsDiffEq, RelativeEq}; + use crate::{Coord, CoordFloat, CoordNum, MapCoords, MapCoordsInPlace}; -use std::fmt; +use std::{fmt, ops::Mul, ops::Neg}; /// Apply an [`AffineTransform`] like [`scale`](AffineTransform::scale), /// [`skew`](AffineTransform::skew), or [`rotate`](AffineTransform::rotate) to a @@ -58,8 +63,8 @@ impl + MapCoords> Affin /// /// Note that affine ops are **already implemented** on most `geo-types` primitives, using this module. /// -/// Affine transforms using the same numeric type (e.g. [`CoordFloat`](crate::CoordFloat)) can be **composed**, -/// and the result can be applied to geometries using e.g. [`MapCoords`](crate::MapCoords). This allows the +/// Affine transforms using the same numeric type (e.g. [`CoordFloat`]) can be **composed**, +/// and the result can be applied to geometries using e.g. [`MapCoords`]. This allows the /// efficient application of transforms: an arbitrary number of operations can be chained. /// These are then composed, producing a final transformation matrix which is applied to the geometry coordinates. /// @@ -277,6 +282,41 @@ impl AffineTransform { } } +impl AffineTransform { + /// Return the inverse of a given transform. Composing a transform with its inverse yields + /// the [identity matrix](Self::identity) + #[must_use] + pub fn inverse(&self) -> Option + where + ::Output: Mul, + <::Output as Mul>::Output: ToPrimitive, + { + let a = self.0[0][0]; + let b = self.0[0][1]; + let xoff = self.0[0][2]; + let d = self.0[1][0]; + let e = self.0[1][1]; + let yoff = self.0[1][2]; + + let determinant = a * e - b * d; + + if determinant == T::zero() { + return None; // The matrix is not invertible + } + let inv_det = T::one() / determinant; + + // If conversion of either the b or d matrix value fails, bail out + Some(Self::new( + e * inv_det, + T::from(-b * inv_det)?, + (b * yoff - e * xoff) * inv_det, + T::from(-d * inv_det)?, + a * inv_det, + (d * xoff - a * yoff) * inv_det, + )) + } +} + impl fmt::Debug for AffineTransform { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("AffineTransform") @@ -387,10 +427,80 @@ impl AffineTransform { } } +#[cfg(any(feature = "approx", test))] +impl RelativeEq for AffineTransform +where + T: AbsDiffEq + CoordNum + RelativeEq, +{ + #[inline] + fn default_max_relative() -> Self::Epsilon { + T::default_max_relative() + } + + /// Equality assertion within a relative limit. + /// + /// # Examples + /// + /// ``` + /// use geo_types::AffineTransform; + /// use geo_types::point; + /// + /// let a = AffineTransform::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); + /// let b = AffineTransform::new(1.01, 2.02, 3.03, 4.04, 5.05, 6.06); + /// + /// approx::assert_relative_eq!(a, b, max_relative=0.1) + /// approx::assert_relative_ne!(a, b, max_relative=0.055) + /// ``` + #[inline] + fn relative_eq( + &self, + other: &Self, + epsilon: Self::Epsilon, + max_relative: Self::Epsilon, + ) -> bool { + let mut mp_zipper = self.0.iter().flatten().zip(other.0.iter().flatten()); + mp_zipper.all(|(lhs, rhs)| lhs.relative_eq(rhs, epsilon, max_relative)) + } +} + +#[cfg(any(feature = "approx", test))] +impl AbsDiffEq for AffineTransform +where + T: AbsDiffEq + CoordNum, + T::Epsilon: Copy, +{ + type Epsilon = T; + + #[inline] + fn default_epsilon() -> Self::Epsilon { + T::default_epsilon() + } + + /// Equality assertion with an absolute limit. + /// + /// # Examples + /// + /// ``` + /// use geo_types::MultiPoint; + /// use geo_types::point; + /// + /// let a = AffineTransform::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); + /// let b = AffineTransform::new(1.01, 2.02, 3.03, 4.04, 5.05, 6.06); + /// + /// approx::abs_diff_eq!(a, b, epsilon=0.1) + /// approx::abs_diff_ne!(a, b, epsilon=0.055) + /// ``` + #[inline] + fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { + let mut mp_zipper = self.0.iter().flatten().zip(other.0.iter().flatten()); + mp_zipper.all(|(lhs, rhs)| lhs.abs_diff_eq(rhs, epsilon)) + } +} + #[cfg(test)] mod tests { use super::*; - use crate::{polygon, Point}; + use crate::{wkt, Point}; // given a matrix with the shape // [[a, b, xoff], @@ -424,16 +534,29 @@ mod tests { // scaled once, but equal to 2 + 2 let scale_c = AffineTransform::default().scaled(4.0, 4.0, p0); assert_ne!(&scale_a.0, &scale_b.0); - assert_eq!(&scale_a.0, &scale_c.0); + assert_relative_eq!(&scale_a, &scale_c); } #[test] fn affine_transformed() { let transform = AffineTransform::translate(1.0, 1.0).scaled(2.0, 2.0, (0.0, 0.0)); - let mut poly = polygon![(x: 0.0, y: 0.0), (x: 0.0, y: 2.0), (x: 1.0, y: 2.0)]; + let mut poly = wkt! { POLYGON((0.0 0.0,0.0 2.0,1.0 2.0)) }; poly.affine_transform_mut(&transform); - let expected = polygon![(x: 1.0, y: 1.0), (x: 1.0, y: 5.0), (x: 3.0, y: 5.0)]; + let expected = wkt! { POLYGON((1.0 1.0,1.0 5.0,3.0 5.0)) }; + assert_eq!(expected, poly); + } + #[test] + fn affine_transformed_inverse() { + let transform = AffineTransform::translate(1.0, 1.0).scaled(2.0, 2.0, (0.0, 0.0)); + let tinv = transform.inverse().unwrap(); + let identity = transform.compose(&tinv); + // test really only needs this, but let's be sure + assert!(identity.is_identity()); + + let mut poly = wkt! { POLYGON((0.0 0.0,0.0 2.0,1.0 2.0)) }; + let expected = poly.clone(); + poly.affine_transform_mut(&identity); assert_eq!(expected, poly); } } diff --git a/geo/src/algorithm/area.rs b/geo/src/algorithm/area.rs index 351a68ca6a..6aec9d21af 100644 --- a/geo/src/algorithm/area.rs +++ b/geo/src/algorithm/area.rs @@ -262,7 +262,7 @@ where #[cfg(test)] mod test { use crate::Area; - use crate::{coord, polygon, Line, MultiPolygon, Polygon, Rect, Triangle}; + use crate::{coord, polygon, wkt, Line, MultiPolygon, Polygon, Rect, Triangle}; // Area of the polygon #[test] @@ -273,18 +273,12 @@ mod test { #[test] fn area_one_point_polygon_test() { - let poly = polygon![(x: 1., y: 0.)]; + let poly = wkt! { POLYGON((1. 0.)) }; assert_relative_eq!(poly.signed_area(), 0.); } #[test] fn area_polygon_test() { - let polygon = polygon![ - (x: 0., y: 0.), - (x: 5., y: 0.), - (x: 5., y: 6.), - (x: 0., y: 6.), - (x: 0., y: 0.) - ]; + let polygon = wkt! { POLYGON((0. 0.,5. 0.,5. 6.,0. 6.,0. 0.)) }; assert_relative_eq!(polygon.signed_area(), 30.); } #[test] @@ -526,6 +520,10 @@ mod test { ], ]; // Value from shapely - assert_relative_eq!(poly.unsigned_area(), 0.006547948219252177, max_relative = 0.0001); + assert_relative_eq!( + poly.unsigned_area(), + 0.006547948219252177, + max_relative = 0.0001 + ); } } diff --git a/geo/src/algorithm/bearing.rs b/geo/src/algorithm/bearing.rs index 9f58e129e5..40c53591a8 100644 --- a/geo/src/algorithm/bearing.rs +++ b/geo/src/algorithm/bearing.rs @@ -9,8 +9,7 @@ pub trait Bearing { /// # Examples /// /// ``` - /// # #[macro_use] extern crate approx; - /// # + /// # use approx::assert_relative_eq; /// use geo::Bearing; /// use geo::Point; /// diff --git a/geo/src/algorithm/centroid.rs b/geo/src/algorithm/centroid.rs index a7e2d0c3f8..8718295f55 100644 --- a/geo/src/algorithm/centroid.rs +++ b/geo/src/algorithm/centroid.rs @@ -692,7 +692,7 @@ impl WeightedCentroid { #[cfg(test)] mod test { use super::*; - use crate::{coord, line_string, point, polygon}; + use crate::{coord, line_string, point, polygon, wkt}; /// small helper to create a coordinate fn c(x: T, y: T) -> Coord { @@ -783,10 +783,13 @@ mod test { } #[test] fn multilinestring_test() { - let v1 = line_string![(x: 0.0, y: 0.0), (x: 1.0, y: 10.0)]; - let v2 = line_string![(x: 1.0, y: 10.0), (x: 2.0, y: 0.0), (x: 3.0, y: 1.0)]; - let v3 = line_string![(x: -12.0, y: -100.0), (x: 7.0, y: 8.0)]; - let mls = MultiLineString::new(vec![v1, v2, v3]); + let mls = wkt! { + MULTILINESTRING( + (0.0 0.0,1.0 10.0), + (1.0 10.0,2.0 0.0,3.0 1.0), + (-12.0 -100.0,7.0 8.0) + ) + }; assert_relative_eq!( mls.centroid().unwrap(), point![x: -1.9097834383655845, y: -37.683866439745714] @@ -801,9 +804,7 @@ mod test { #[test] fn polygon_one_point_test() { let p = point![ x: 2., y: 1. ]; - let v = Vec::new(); - let linestring = line_string![p.0]; - let poly = Polygon::new(linestring, v); + let poly = polygon![p.0]; assert_relative_eq!(poly.centroid().unwrap(), p); } @@ -857,68 +858,42 @@ mod test { #[test] fn polygon_hole_test() { // hexagon - let ls1 = LineString::from(vec![ - (5.0, 1.0), - (4.0, 2.0), - (4.0, 3.0), - (5.0, 4.0), - (6.0, 4.0), - (7.0, 3.0), - (7.0, 2.0), - (6.0, 1.0), - (5.0, 1.0), - ]); - - let ls2 = LineString::from(vec![(5.0, 1.3), (5.5, 2.0), (6.0, 1.3), (5.0, 1.3)]); - - let ls3 = LineString::from(vec![(5., 2.3), (5.5, 3.0), (6., 2.3), (5., 2.3)]); - - let p1 = Polygon::new(ls1, vec![ls2, ls3]); + let p1 = wkt! { POLYGON( + (5.0 1.0,4.0 2.0,4.0 3.0,5.0 4.0,6.0 4.0,7.0 3.0,7.0 2.0,6.0 1.0,5.0 1.0), + (5.0 1.3,5.5 2.0,6.0 1.3,5.0 1.3), + (5.0 2.3,5.5 3.0,6.0 2.3,5.0 2.3) + ) }; let centroid = p1.centroid().unwrap(); assert_relative_eq!(centroid, point!(x: 5.5, y: 2.5518518518518523)); } #[test] fn flat_polygon_test() { - let poly = Polygon::new( - LineString::from(vec![p(0., 1.), p(1., 1.), p(0., 1.)]), - vec![], - ); + let poly = wkt! { POLYGON((0. 1.,1. 1.,0. 1.)) }; assert_eq!(poly.centroid(), Some(p(0.5, 1.))); } #[test] fn multi_poly_with_flat_polygon_test() { - let poly = Polygon::new( - LineString::from(vec![p(0., 0.), p(1., 0.), p(0., 0.)]), - vec![], - ); - let multipoly = MultiPolygon::new(vec![poly]); + let multipoly = wkt! { MULTIPOLYGON(((0. 0.,1. 0.,0. 0.))) }; assert_eq!(multipoly.centroid(), Some(p(0.5, 0.))); } #[test] fn multi_poly_with_multiple_flat_polygon_test() { - let p1 = Polygon::new( - LineString::from(vec![p(1., 1.), p(1., 3.), p(1., 1.)]), - vec![], - ); - let p2 = Polygon::new( - LineString::from(vec![p(2., 2.), p(6., 2.), p(2., 2.)]), - vec![], - ); - let multipoly = MultiPolygon::new(vec![p1, p2]); + let multipoly = wkt! { MULTIPOLYGON( + ((1. 1.,1. 3.,1. 1.)), + ((2. 2.,6. 2.,2. 2.)) + )}; + assert_eq!(multipoly.centroid(), Some(p(3., 2.))); } #[test] fn multi_poly_with_only_points_test() { - let p1 = Polygon::new( - LineString::from(vec![p(1., 1.), p(1., 1.), p(1., 1.)]), - vec![], - ); + let p1 = wkt! { POLYGON((1. 1.,1. 1.,1. 1.)) }; assert_eq!(p1.centroid(), Some(p(1., 1.))); - let p2 = Polygon::new( - LineString::from(vec![p(2., 2.), p(2., 2.), p(2., 2.)]), - vec![], - ); - let multipoly = MultiPolygon::new(vec![p1, p2]); + + let multipoly = wkt! { MULTIPOLYGON( + ((1. 1.,1. 1.,1. 1.)), + ((2. 2., 2. 2.,2. 2.)) + ) }; assert_eq!(multipoly.centroid(), Some(p(1.5, 1.5))); } #[test] diff --git a/geo/src/algorithm/convert_angle_unit.rs b/geo/src/algorithm/convert_angle_unit.rs new file mode 100644 index 0000000000..cbaa3dd11e --- /dev/null +++ b/geo/src/algorithm/convert_angle_unit.rs @@ -0,0 +1,84 @@ +use geo_types::Coord; +use geo_types::CoordFloat; + +use crate::{MapCoords, MapCoordsInPlace}; + +pub trait ToRadians: + Sized + MapCoords + MapCoordsInPlace +{ + fn to_radians(&self) -> Self { + self.map_coords(|Coord { x, y }| Coord { + x: x.to_radians(), + y: y.to_radians(), + }) + } + + fn to_radians_in_place(&mut self) { + self.map_coords_in_place(|Coord { x, y }| Coord { + x: x.to_radians(), + y: y.to_radians(), + }) + } +} +impl + MapCoordsInPlace> ToRadians for G {} + +pub trait ToDegrees: + Sized + MapCoords + MapCoordsInPlace +{ + fn to_degrees(&self) -> Self { + self.map_coords(|Coord { x, y }| Coord { + x: x.to_degrees(), + y: y.to_degrees(), + }) + } + + fn to_degrees_in_place(&mut self) { + self.map_coords_in_place(|Coord { x, y }| Coord { + x: x.to_degrees(), + y: y.to_degrees(), + }) + } +} +impl + MapCoordsInPlace> ToDegrees for G {} + +#[cfg(test)] +mod tests { + use std::f64::consts::PI; + + use approx::assert_relative_eq; + use geo_types::Line; + + use super::*; + + fn line_degrees_mock() -> Line { + Line::new((90.0, 180.), (0., -90.)) + } + + fn line_radians_mock() -> Line { + Line::new((PI / 2., PI), (0., -PI / 2.)) + } + + #[test] + fn converts_to_radians() { + assert_relative_eq!(line_radians_mock(), line_degrees_mock().to_radians()) + } + + #[test] + fn converts_to_radians_in_place() { + let mut line = line_degrees_mock(); + line.to_radians_in_place(); + assert_relative_eq!(line_radians_mock(), line) + } + + #[test] + fn converts_to_degrees() { + assert_relative_eq!(line_degrees_mock(), line_radians_mock().to_degrees()) + } + + #[test] + fn converts_to_degrees_in_place() { + let mut line = line_radians_mock(); + line.to_degrees_in_place(); + assert_relative_eq!(line_degrees_mock(), line) + } +} diff --git a/geo/src/algorithm/convex_hull/graham.rs b/geo/src/algorithm/convex_hull/graham.rs index 5f9ca3c152..90087d43e6 100644 --- a/geo/src/algorithm/convex_hull/graham.rs +++ b/geo/src/algorithm/convex_hull/graham.rs @@ -98,7 +98,7 @@ mod test { #[test] fn test_graham_hull_ccw() { - let initial = vec![ + let initial = [ (1.0, 0.0), (2.0, 1.0), (1.75, 1.1), @@ -119,7 +119,7 @@ mod test { #[test] fn graham_hull_test2() { - let v = vec![ + let v = [ (0, 10), (1, 1), (10, 0), diff --git a/geo/src/algorithm/convex_hull/mod.rs b/geo/src/algorithm/convex_hull/mod.rs index bc746cfca5..c11f286d53 100644 --- a/geo/src/algorithm/convex_hull/mod.rs +++ b/geo/src/algorithm/convex_hull/mod.rs @@ -49,7 +49,7 @@ use crate::utils::lex_cmp; impl<'a, T, G> ConvexHull<'a, T> for G where T: GeoNum, - G: CoordsIter<'a, Scalar = T>, + G: CoordsIter, { type Scalar = T; diff --git a/geo/src/algorithm/convex_hull/qhull.rs b/geo/src/algorithm/convex_hull/qhull.rs index 4d894ca8a9..c11fac3b07 100644 --- a/geo/src/algorithm/convex_hull/qhull.rs +++ b/geo/src/algorithm/convex_hull/qhull.rs @@ -157,7 +157,7 @@ mod test { #[test] // test whether output is ccw fn quick_hull_test_ccw() { - let initial = vec![ + let initial = [ (1.0, 0.0), (2.0, 1.0), (1.75, 1.1), @@ -166,7 +166,7 @@ mod test { (1.0, 0.0), ]; let mut v: Vec<_> = initial.iter().map(|e| coord! { x: e.0, y: e.1 }).collect(); - let correct = vec![(1.0, 0.0), (2.0, 1.0), (1.0, 2.0), (0.0, 1.0), (1.0, 0.0)]; + let correct = [(1.0, 0.0), (2.0, 1.0), (1.0, 2.0), (0.0, 1.0), (1.0, 0.0)]; let v_correct: Vec<_> = correct.iter().map(|e| coord! { x: e.0, y: e.1 }).collect(); let res = quick_hull(&mut v); assert_eq!(res.0, v_correct); @@ -175,7 +175,7 @@ mod test { #[test] fn quick_hull_test_ccw_maintain() { // initial input begins at min y, is oriented ccw - let initial = vec![ + let initial = [ (0., 0.), (2., 0.), (2.5, 1.75), @@ -211,7 +211,7 @@ mod test { // Initial input begins at min x, but not min y // There are three points with same x. // Output should not contain the middle point. - let initial = vec![ + let initial = [ (-1., 0.), (-1., -1.), (-1., 1.), diff --git a/geo/src/algorithm/coords_iter.rs b/geo/src/algorithm/coords_iter.rs index b749614215..816c82c051 100644 --- a/geo/src/algorithm/coords_iter.rs +++ b/geo/src/algorithm/coords_iter.rs @@ -8,9 +8,13 @@ use std::{fmt, iter, marker, slice}; type CoordinateChainOnce = iter::Chain>, iter::Once>>; /// Iterate over geometry coordinates. -pub trait CoordsIter<'a> { - type Iter: Iterator>; - type ExteriorIter: Iterator>; +pub trait CoordsIter { + type Iter<'a>: Iterator> + where + Self: 'a; + type ExteriorIter<'a>: Iterator> + where + Self: 'a; type Scalar: CoordNum; /// Iterate over all exterior and (if any) interior coordinates of a geometry. @@ -32,7 +36,7 @@ pub trait CoordsIter<'a> { /// assert_eq!(Some(geo::coord! { x: 30., y: 40. }), iter.next()); /// assert_eq!(None, iter.next()); /// ``` - fn coords_iter(&'a self) -> Self::Iter; + fn coords_iter(&self) -> Self::Iter<'_>; /// Return the number of coordinates in a geometry. /// @@ -50,7 +54,7 @@ pub trait CoordsIter<'a> { /// /// assert_eq!(3, ls.coords_count()); /// ``` - fn coords_count(&'a self) -> usize; + fn coords_count(&self) -> usize; /// Iterate over all exterior coordinates of a geometry. /// @@ -88,28 +92,28 @@ pub trait CoordsIter<'a> { /// assert_eq!(Some(geo::coord! { x: 1., y: 0. }), iter.next()); /// assert_eq!(None, iter.next()); /// ``` - fn exterior_coords_iter(&'a self) -> Self::ExteriorIter; + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_>; } // ┌──────────────────────────┐ // │ Implementation for Point │ // └──────────────────────────┘ -impl<'a, T: CoordNum> CoordsIter<'a> for Point { - type Iter = iter::Once>; - type ExteriorIter = Self::Iter; +impl CoordsIter for Point { + type Iter<'a> = iter::Once> where T: 'a; + type ExteriorIter<'a> = Self::Iter<'a> where T: 'a; type Scalar = T; - fn coords_iter(&'a self) -> Self::Iter { + fn coords_iter(&self) -> Self::Iter<'_> { iter::once(self.0) } /// Return the number of coordinates in the `Point`. - fn coords_count(&'a self) -> usize { + fn coords_count(&self) -> usize { 1 } - fn exterior_coords_iter(&'a self) -> Self::ExteriorIter { + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_> { self.coords_iter() } } @@ -118,21 +122,23 @@ impl<'a, T: CoordNum> CoordsIter<'a> for Point { // │ Implementation for Line │ // └─────────────────────────┘ -impl<'a, T: CoordNum> CoordsIter<'a> for Line { - type Iter = iter::Chain>, iter::Once>>; - type ExteriorIter = Self::Iter; +impl CoordsIter for Line { + type Iter<'a> = iter::Chain>, iter::Once>> + where T: 'a; + type ExteriorIter<'a> = Self::Iter<'a> + where T: 'a; type Scalar = T; - fn coords_iter(&'a self) -> Self::Iter { + fn coords_iter(&self) -> Self::Iter<'_> { iter::once(self.start).chain(iter::once(self.end)) } /// Return the number of coordinates in the `Line`. - fn coords_count(&'a self) -> usize { + fn coords_count(&self) -> usize { 2 } - fn exterior_coords_iter(&'a self) -> Self::ExteriorIter { + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_> { self.coords_iter() } } @@ -143,21 +149,23 @@ impl<'a, T: CoordNum> CoordsIter<'a> for Line { type LineStringIter<'a, T> = iter::Copied>>; -impl<'a, T: CoordNum + 'a> CoordsIter<'a> for LineString { - type Iter = LineStringIter<'a, T>; - type ExteriorIter = Self::Iter; +impl CoordsIter for LineString { + type Iter<'a> = LineStringIter<'a, T> + where T: 'a; + type ExteriorIter<'a> = Self::Iter<'a> + where T: 'a; type Scalar = T; - fn coords_iter(&'a self) -> Self::Iter { + fn coords_iter(&self) -> Self::Iter<'_> { self.0.iter().copied() } /// Return the number of coordinates in the `LineString`. - fn coords_count(&'a self) -> usize { + fn coords_count(&self) -> usize { self.0.len() } - fn exterior_coords_iter(&'a self) -> Self::ExteriorIter { + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_> { self.coords_iter() } } @@ -171,19 +179,21 @@ type PolygonIter<'a, T> = iter::Chain< iter::Flatten>, LineString>>, >; -impl<'a, T: CoordNum + 'a> CoordsIter<'a> for Polygon { - type Iter = PolygonIter<'a, T>; - type ExteriorIter = LineStringIter<'a, T>; +impl CoordsIter for Polygon { + type Iter<'a> = PolygonIter<'a, T> + where T: 'a; + type ExteriorIter<'a> = LineStringIter<'a, T> + where T: 'a; type Scalar = T; - fn coords_iter(&'a self) -> Self::Iter { + fn coords_iter(&self) -> Self::Iter<'_> { self.exterior() .coords_iter() .chain(MapCoordsIter(self.interiors().iter(), marker::PhantomData).flatten()) } /// Return the number of coordinates in the `Polygon`. - fn coords_count(&'a self) -> usize { + fn coords_count(&self) -> usize { self.exterior().coords_count() + self .interiors() @@ -192,7 +202,7 @@ impl<'a, T: CoordNum + 'a> CoordsIter<'a> for Polygon { .sum::() } - fn exterior_coords_iter(&'a self) -> Self::ExteriorIter { + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_> { self.exterior().coords_iter() } } @@ -201,21 +211,23 @@ impl<'a, T: CoordNum + 'a> CoordsIter<'a> for Polygon { // │ Implementation for MultiPoint │ // └───────────────────────────────┘ -impl<'a, T: CoordNum + 'a> CoordsIter<'a> for MultiPoint { - type Iter = iter::Flatten>, Point>>; - type ExteriorIter = Self::Iter; +impl CoordsIter for MultiPoint { + type Iter<'a> = iter::Flatten>, Point>> + where T: 'a; + type ExteriorIter<'a> = Self::Iter<'a> + where T: 'a; type Scalar = T; - fn coords_iter(&'a self) -> Self::Iter { + fn coords_iter(&self) -> Self::Iter<'_> { MapCoordsIter(self.0.iter(), marker::PhantomData).flatten() } /// Return the number of coordinates in the `MultiPoint`. - fn coords_count(&'a self) -> usize { + fn coords_count(&self) -> usize { self.0.len() } - fn exterior_coords_iter(&'a self) -> Self::ExteriorIter { + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_> { self.coords_iter() } } @@ -224,24 +236,27 @@ impl<'a, T: CoordNum + 'a> CoordsIter<'a> for MultiPoint { // │ Implementation for MultiLineString │ // └────────────────────────────────────┘ -impl<'a, T: CoordNum + 'a> CoordsIter<'a> for MultiLineString { - type Iter = iter::Flatten>, LineString>>; - type ExteriorIter = Self::Iter; +impl CoordsIter for MultiLineString { + type Iter<'a> = + iter::Flatten>, LineString>> + where T: 'a; + type ExteriorIter<'a> = Self::Iter<'a> + where T: 'a; type Scalar = T; - fn coords_iter(&'a self) -> Self::Iter { + fn coords_iter(&self) -> Self::Iter<'_> { MapCoordsIter(self.0.iter(), marker::PhantomData).flatten() } /// Return the number of coordinates in the `MultiLineString`. - fn coords_count(&'a self) -> usize { + fn coords_count(&self) -> usize { self.0 .iter() .map(|line_string| line_string.coords_count()) .sum() } - fn exterior_coords_iter(&'a self) -> Self::ExteriorIter { + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_> { self.coords_iter() } } @@ -250,22 +265,24 @@ impl<'a, T: CoordNum + 'a> CoordsIter<'a> for MultiLineString { // │ Implementation for MultiPolygon │ // └─────────────────────────────────┘ -impl<'a, T: CoordNum + 'a> CoordsIter<'a> for MultiPolygon { - type Iter = iter::Flatten>, Polygon>>; - type ExteriorIter = - iter::Flatten>, Polygon>>; +impl CoordsIter for MultiPolygon { + type Iter<'a> = iter::Flatten>, Polygon>> + where T: 'a; + type ExteriorIter<'a> = + iter::Flatten>, Polygon>> + where T: 'a; type Scalar = T; - fn coords_iter(&'a self) -> Self::Iter { + fn coords_iter(&self) -> Self::Iter<'_> { MapCoordsIter(self.0.iter(), marker::PhantomData).flatten() } /// Return the number of coordinates in the `MultiPolygon`. - fn coords_count(&'a self) -> usize { + fn coords_count(&self) -> usize { self.0.iter().map(|polygon| polygon.coords_count()).sum() } - fn exterior_coords_iter(&'a self) -> Self::ExteriorIter { + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_> { MapExteriorCoordsIter(self.0.iter(), marker::PhantomData).flatten() } } @@ -274,21 +291,23 @@ impl<'a, T: CoordNum + 'a> CoordsIter<'a> for MultiPolygon { // │ Implementation for GeometryCollection │ // └───────────────────────────────────────┘ -impl<'a, T: CoordNum + 'a> CoordsIter<'a> for GeometryCollection { - type Iter = Box> + 'a>; - type ExteriorIter = Box> + 'a>; +impl CoordsIter for GeometryCollection { + type Iter<'a> = Box> + 'a> + where T: 'a; + type ExteriorIter<'a> = Box> + 'a> + where T: 'a; type Scalar = T; - fn coords_iter(&'a self) -> Self::Iter { + fn coords_iter(&self) -> Self::Iter<'_> { Box::new(self.0.iter().flat_map(|geometry| geometry.coords_iter())) } /// Return the number of coordinates in the `GeometryCollection`. - fn coords_count(&'a self) -> usize { + fn coords_count(&self) -> usize { self.0.iter().map(|geometry| geometry.coords_count()).sum() } - fn exterior_coords_iter(&'a self) -> Self::ExteriorIter { + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_> { Box::new( self.0 .iter() @@ -304,12 +323,14 @@ impl<'a, T: CoordNum + 'a> CoordsIter<'a> for GeometryCollection { type RectIter = iter::Chain, iter::Once>>, iter::Once>>; -impl<'a, T: CoordNum + 'a> CoordsIter<'a> for Rect { - type Iter = RectIter; - type ExteriorIter = Self::Iter; +impl CoordsIter for Rect { + type Iter<'a> = RectIter + where T: 'a; + type ExteriorIter<'a> = Self::Iter<'a> + where T: 'a; type Scalar = T; - fn coords_iter(&'a self) -> Self::Iter { + fn coords_iter(&self) -> Self::Iter<'_> { iter::once(coord! { x: self.min().x, y: self.min().y, @@ -332,11 +353,11 @@ impl<'a, T: CoordNum + 'a> CoordsIter<'a> for Rect { /// /// Note: Although a `Rect` is represented by two coordinates, it is /// spatially represented by four, so this method returns `4`. - fn coords_count(&'a self) -> usize { + fn coords_count(&self) -> usize { 4 } - fn exterior_coords_iter(&'a self) -> Self::ExteriorIter { + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_> { self.coords_iter() } } @@ -345,23 +366,25 @@ impl<'a, T: CoordNum + 'a> CoordsIter<'a> for Rect { // │ Implementation for Triangle │ // └─────────────────────────────┘ -impl<'a, T: CoordNum + 'a> CoordsIter<'a> for Triangle { - type Iter = iter::Chain, iter::Once>>; - type ExteriorIter = Self::Iter; +impl CoordsIter for Triangle { + type Iter<'a> = iter::Chain, iter::Once>> + where T: 'a; + type ExteriorIter<'a> = Self::Iter<'a> + where T: 'a; type Scalar = T; - fn coords_iter(&'a self) -> Self::Iter { + fn coords_iter(&self) -> Self::Iter<'_> { iter::once(self.0) .chain(iter::once(self.1)) .chain(iter::once(self.2)) } /// Return the number of coordinates in the `Triangle`. - fn coords_count(&'a self) -> usize { + fn coords_count(&self) -> usize { 3 } - fn exterior_coords_iter(&'a self) -> Self::ExteriorIter { + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_> { self.coords_iter() } } @@ -370,12 +393,14 @@ impl<'a, T: CoordNum + 'a> CoordsIter<'a> for Triangle { // │ Implementation for Geometry │ // └─────────────────────────────┘ -impl<'a, T: CoordNum + 'a> CoordsIter<'a> for Geometry { - type Iter = GeometryCoordsIter<'a, T>; - type ExteriorIter = GeometryExteriorCoordsIter<'a, T>; +impl CoordsIter for Geometry { + type Iter<'a> = GeometryCoordsIter<'a, T> + where T: 'a; + type ExteriorIter<'a> = GeometryExteriorCoordsIter<'a, T> + where T: 'a; type Scalar = T; - fn coords_iter(&'a self) -> Self::Iter { + fn coords_iter(&self) -> Self::Iter<'_> { match self { Geometry::Point(g) => GeometryCoordsIter::Point(g.coords_iter()), Geometry::Line(g) => GeometryCoordsIter::Line(g.coords_iter()), @@ -393,10 +418,10 @@ impl<'a, T: CoordNum + 'a> CoordsIter<'a> for Geometry { } crate::geometry_delegate_impl! { /// Return the number of coordinates in the `Geometry`. - fn coords_count(&'a self) -> usize; + fn coords_count(&self) -> usize; } - fn exterior_coords_iter(&'a self) -> Self::ExteriorIter { + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_> { match self { Geometry::Point(g) => GeometryExteriorCoordsIter::Point(g.exterior_coords_iter()), Geometry::Line(g) => GeometryExteriorCoordsIter::Line(g.exterior_coords_iter()), @@ -433,13 +458,13 @@ pub struct MapCoordsIter< 'a, T: 'a + CoordNum, Iter1: Iterator, - Iter2: 'a + CoordsIter<'a>, + Iter2: 'a + CoordsIter, >(Iter1, marker::PhantomData); -impl<'a, T: 'a + CoordNum, Iter1: Iterator, Iter2: CoordsIter<'a>> Iterator +impl<'a, T: 'a + CoordNum, Iter1: Iterator, Iter2: CoordsIter> Iterator for MapCoordsIter<'a, T, Iter1, Iter2> { - type Item = Iter2::Iter; + type Item = Iter2::Iter<'a>; fn next(&mut self) -> Option { self.0.next().map(|g| g.coords_iter()) @@ -457,13 +482,13 @@ pub struct MapExteriorCoordsIter< 'a, T: 'a + CoordNum, Iter1: Iterator, - Iter2: 'a + CoordsIter<'a>, + Iter2: 'a + CoordsIter, >(Iter1, marker::PhantomData); -impl<'a, T: 'a + CoordNum, Iter1: Iterator, Iter2: CoordsIter<'a>> Iterator +impl<'a, T: 'a + CoordNum, Iter1: Iterator, Iter2: CoordsIter> Iterator for MapExteriorCoordsIter<'a, T, Iter1, Iter2> { - type Item = Iter2::ExteriorIter; + type Item = Iter2::ExteriorIter<'a>; fn next(&mut self) -> Option { self.0.next().map(|g| g.exterior_coords_iter()) @@ -477,16 +502,16 @@ impl<'a, T: 'a + CoordNum, Iter1: Iterator, Iter2: CoordsIter< // Utility to transform Geometry into Iterator #[doc(hidden)] pub enum GeometryCoordsIter<'a, T: CoordNum + 'a> { - Point( as CoordsIter<'a>>::Iter), - Line( as CoordsIter<'a>>::Iter), - LineString( as CoordsIter<'a>>::Iter), - Polygon( as CoordsIter<'a>>::Iter), - MultiPoint( as CoordsIter<'a>>::Iter), - MultiLineString( as CoordsIter<'a>>::Iter), - MultiPolygon( as CoordsIter<'a>>::Iter), - GeometryCollection( as CoordsIter<'a>>::Iter), - Rect( as CoordsIter<'a>>::Iter), - Triangle( as CoordsIter<'a>>::Iter), + Point( as CoordsIter>::Iter<'a>), + Line( as CoordsIter>::Iter<'a>), + LineString( as CoordsIter>::Iter<'a>), + Polygon( as CoordsIter>::Iter<'a>), + MultiPoint( as CoordsIter>::Iter<'a>), + MultiLineString( as CoordsIter>::Iter<'a>), + MultiPolygon( as CoordsIter>::Iter<'a>), + GeometryCollection( as CoordsIter>::Iter<'a>), + Rect( as CoordsIter>::Iter<'a>), + Triangle( as CoordsIter>::Iter<'a>), } impl<'a, T: CoordNum> Iterator for GeometryCoordsIter<'a, T> { @@ -550,16 +575,16 @@ impl<'a, T: CoordNum + Debug> fmt::Debug for GeometryCoordsIter<'a, T> { // Utility to transform Geometry into Iterator #[doc(hidden)] pub enum GeometryExteriorCoordsIter<'a, T: CoordNum + 'a> { - Point( as CoordsIter<'a>>::ExteriorIter), - Line( as CoordsIter<'a>>::ExteriorIter), - LineString( as CoordsIter<'a>>::ExteriorIter), - Polygon( as CoordsIter<'a>>::ExteriorIter), - MultiPoint( as CoordsIter<'a>>::ExteriorIter), - MultiLineString( as CoordsIter<'a>>::ExteriorIter), - MultiPolygon( as CoordsIter<'a>>::ExteriorIter), - GeometryCollection( as CoordsIter<'a>>::ExteriorIter), - Rect( as CoordsIter<'a>>::ExteriorIter), - Triangle( as CoordsIter<'a>>::ExteriorIter), + Point( as CoordsIter>::ExteriorIter<'a>), + Line( as CoordsIter>::ExteriorIter<'a>), + LineString( as CoordsIter>::ExteriorIter<'a>), + Polygon( as CoordsIter>::ExteriorIter<'a>), + MultiPoint( as CoordsIter>::ExteriorIter<'a>), + MultiLineString( as CoordsIter>::ExteriorIter<'a>), + MultiPolygon( as CoordsIter>::ExteriorIter<'a>), + GeometryCollection( as CoordsIter>::ExteriorIter<'a>), + Rect( as CoordsIter>::ExteriorIter<'a>), + Triangle( as CoordsIter>::ExteriorIter<'a>), } impl<'a, T: CoordNum> Iterator for GeometryExteriorCoordsIter<'a, T> { diff --git a/geo/src/algorithm/densify.rs b/geo/src/algorithm/densify.rs index e861efb45a..2c158f03bb 100644 --- a/geo/src/algorithm/densify.rs +++ b/geo/src/algorithm/densify.rs @@ -106,7 +106,12 @@ where type Output = LineString; fn densify(&self, max_distance: T) -> Self::Output { + if self.0.is_empty() { + return LineString::new(vec![]); + } + let mut new_line = vec![]; + self.lines() .for_each(|line| densify_line(line, &mut new_line, max_distance)); // we're done, push the last coordinate on to finish @@ -207,6 +212,14 @@ mod tests { assert_eq!(densified, correct_polygon); } + #[test] + fn test_empty_linestring_densify() { + let linestring = LineString::::new(vec![]); + let max_dist = 2.0; + let densified = linestring.densify(max_dist); + assert!(densified.0.is_empty()); + } + #[test] fn test_linestring_densify() { let linestring: LineString = diff --git a/geo/src/algorithm/densify_haversine.rs b/geo/src/algorithm/densify_haversine.rs new file mode 100644 index 0000000000..bd7cb94360 --- /dev/null +++ b/geo/src/algorithm/densify_haversine.rs @@ -0,0 +1,263 @@ +use num_traits::FromPrimitive; + +use crate::{ + CoordFloat, CoordsIter, Line, LineString, MultiLineString, MultiPolygon, Point, Polygon, Rect, + Triangle, +}; + +use crate::{HaversineIntermediate, HaversineLength}; + +/// Returns a new spherical geometry containing both existing and new interpolated coordinates with +/// a maximum distance of `max_distance` between them. +/// +/// Note: `max_distance` must be greater than 0. +/// +/// ## Units +/// +/// `max_distance`: meters +/// +/// # Examples +/// ``` +/// use geo::{coord, Line, LineString}; +/// use geo::DensifyHaversine; +/// +/// let line = Line::new(coord! {x: 0.0, y: 0.0}, coord! { x: 0.0, y: 1.0 }); +/// // known output +/// let output: LineString = vec![[0.0, 0.0], [0.0, 0.5], [0.0, 1.0]].into(); +/// // densify +/// let dense = line.densify_haversine(100000.0); +/// assert_eq!(dense, output); +///``` +pub trait DensifyHaversine { + type Output; + + fn densify_haversine(&self, max_distance: F) -> Self::Output; +} + +// Helper for densification trait +fn densify_line( + line: Line, + container: &mut Vec>, + max_distance: T, +) { + assert!(max_distance > T::zero()); + container.push(line.start_point()); + let num_segments = (line.haversine_length() / max_distance) + .ceil() + .to_u64() + .unwrap(); + // distance "unit" for this line segment + let frac = T::one() / T::from(num_segments).unwrap(); + for segment_idx in 1..num_segments { + let ratio = frac * T::from(segment_idx).unwrap(); + let start = line.start; + let end = line.end; + let interpolated_point = + Point::from(start).haversine_intermediate(&Point::from(end), ratio); + container.push(interpolated_point); + } +} + +impl DensifyHaversine for MultiPolygon +where + T: CoordFloat + FromPrimitive, + Line: HaversineLength, + LineString: HaversineLength, +{ + type Output = MultiPolygon; + + fn densify_haversine(&self, max_distance: T) -> Self::Output { + MultiPolygon::new( + self.iter() + .map(|polygon| polygon.densify_haversine(max_distance)) + .collect(), + ) + } +} + +impl DensifyHaversine for Polygon +where + T: CoordFloat + FromPrimitive, + Line: HaversineLength, + LineString: HaversineLength, +{ + type Output = Polygon; + + fn densify_haversine(&self, max_distance: T) -> Self::Output { + let densified_exterior = self.exterior().densify_haversine(max_distance); + let densified_interiors = self + .interiors() + .iter() + .map(|ring| ring.densify_haversine(max_distance)) + .collect(); + Polygon::new(densified_exterior, densified_interiors) + } +} + +impl DensifyHaversine for MultiLineString +where + T: CoordFloat + FromPrimitive, + Line: HaversineLength, + LineString: HaversineLength, +{ + type Output = MultiLineString; + + fn densify_haversine(&self, max_distance: T) -> Self::Output { + MultiLineString::new( + self.iter() + .map(|linestring| linestring.densify_haversine(max_distance)) + .collect(), + ) + } +} + +impl DensifyHaversine for LineString +where + T: CoordFloat + FromPrimitive, + Line: HaversineLength, + LineString: HaversineLength, +{ + type Output = LineString; + + fn densify_haversine(&self, max_distance: T) -> Self::Output { + if self.coords_count() == 0 { + return LineString::new(vec![]); + } + + let mut new_line = vec![]; + self.lines() + .for_each(|line| densify_line(line, &mut new_line, max_distance)); + // we're done, push the last coordinate on to finish + new_line.push(self.points().last().unwrap()); + LineString::from(new_line) + } +} + +impl DensifyHaversine for Line +where + T: CoordFloat + FromPrimitive, + Line: HaversineLength, + LineString: HaversineLength, +{ + type Output = LineString; + + fn densify_haversine(&self, max_distance: T) -> Self::Output { + let mut new_line = vec![]; + densify_line(*self, &mut new_line, max_distance); + // we're done, push the last coordinate on to finish + new_line.push(self.end_point()); + LineString::from(new_line) + } +} + +impl DensifyHaversine for Triangle +where + T: CoordFloat + FromPrimitive, + Line: HaversineLength, + LineString: HaversineLength, +{ + type Output = Polygon; + + fn densify_haversine(&self, max_distance: T) -> Self::Output { + self.to_polygon().densify_haversine(max_distance) + } +} + +impl DensifyHaversine for Rect +where + T: CoordFloat + FromPrimitive, + Line: HaversineLength, + LineString: HaversineLength, +{ + type Output = Polygon; + + fn densify_haversine(&self, max_distance: T) -> Self::Output { + self.to_polygon().densify_haversine(max_distance) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::coord; + + #[test] + fn test_polygon_densify() { + let exterior: LineString = vec![ + [4.925, 45.804], + [4.732, 45.941], + [4.935, 46.513], + [5.821, 46.103], + [5.627, 45.611], + [5.355, 45.883], + [4.925, 45.804], + ] + .into(); + + let polygon = Polygon::new(exterior, vec![]); + + let output_exterior: LineString = vec![ + [4.925, 45.804], + [4.732, 45.941], + [4.8329711649985505, 46.2270449096239], + [4.935, 46.513], + [5.379659133344039, 46.30885540136222], + [5.821, 46.103], + [5.723570877658867, 45.85704103535437], + [5.627, 45.611], + [5.355, 45.883], + [4.925, 45.804], + ] + .into(); + + let dense = polygon.densify_haversine(50000.0); + assert_relative_eq!(dense.exterior(), &output_exterior); + } + + #[test] + fn test_linestring_densify() { + let linestring: LineString = vec![ + [-3.202, 55.9471], + [-3.2012, 55.9476], + [-3.1994, 55.9476], + [-3.1977, 55.9481], + [-3.196, 55.9483], + [-3.1947, 55.9487], + [-3.1944, 55.9488], + [-3.1944, 55.949], + ] + .into(); + + let output: LineString = vec![ + [-3.202, 55.9471], + [-3.2012, 55.9476], + [-3.2002999999999995, 55.94760000327935], + [-3.1994, 55.9476], + [-3.1985500054877773, 55.94785000292509], + [-3.1977, 55.9481], + [-3.196, 55.9483], + [-3.1947, 55.9487], + [-3.1944, 55.9488], + [-3.1944, 55.949], + ] + .into(); + + let dense = linestring.densify_haversine(110.0); + assert_relative_eq!(dense, output); + } + + #[test] + fn test_line_densify() { + let output: LineString = vec![[0.0, 0.0], [0.0, 0.5], [0.0, 1.0]].into(); + let line = Line::new(coord! {x: 0.0, y: 0.0}, coord! { x: 0.0, y: 1.0 }); + let dense = line.densify_haversine(100000.0); + assert_relative_eq!(dense, output); + } + + #[test] + fn test_empty_linestring() { + let linestring: LineString = LineString::new(vec![]); + let dense = linestring.densify_haversine(10.0); + assert_eq!(0, dense.coords_count()); + } +} diff --git a/geo/src/algorithm/euclidean_distance.rs b/geo/src/algorithm/euclidean_distance.rs index 43f594491e..acde6ae073 100644 --- a/geo/src/algorithm/euclidean_distance.rs +++ b/geo/src/algorithm/euclidean_distance.rs @@ -2,11 +2,12 @@ use crate::utils::{coord_pos_relative_to_ring, CoordPos}; use crate::EuclideanLength; use crate::Intersects; use crate::{ - Coord, GeoFloat, GeoNum, Line, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, - Polygon, Triangle, + Coord, GeoFloat, GeoNum, Geometry, GeometryCollection, Line, LineString, MultiLineString, + MultiPoint, MultiPolygon, Point, Polygon, Rect, Triangle, }; use num_traits::{float::FloatConst, Bounded, Float, Signed}; +use rstar::primitives::CachedEnvelope; use rstar::RTree; use rstar::RTreeNum; @@ -131,20 +132,6 @@ where } } -impl EuclideanDistance> for Point -where - T: GeoFloat, -{ - /// Minimum distance from a Point to a MultiPoint - fn euclidean_distance(&self, points: &MultiPoint) -> T { - points - .0 - .iter() - .map(|p| self.euclidean_distance(p)) - .fold(::max_value(), |accum, val| accum.min(val)) - } -} - impl EuclideanDistance> for Point where T: GeoFloat, @@ -165,19 +152,6 @@ where } } -impl EuclideanDistance> for Point -where - T: GeoFloat, -{ - /// Minimum distance from a Point to a MultiLineString - fn euclidean_distance(&self, mls: &MultiLineString) -> T { - mls.0 - .iter() - .map(|ls| self.euclidean_distance(ls)) - .fold(::max_value(), |accum, val| accum.min(val)) - } -} - impl EuclideanDistance> for Point where T: GeoFloat, @@ -209,34 +183,6 @@ where } } -impl EuclideanDistance> for Point -where - T: GeoFloat, -{ - /// Minimum distance from a Point to a MultiPolygon - fn euclidean_distance(&self, mpolygon: &MultiPolygon) -> T { - mpolygon - .0 - .iter() - .map(|p| self.euclidean_distance(p)) - .fold(::max_value(), |accum, val| accum.min(val)) - } -} - -// ┌────────────────────────────────┐ -// │ Implementations for MultiPoint │ -// └────────────────────────────────┘ - -impl EuclideanDistance> for MultiPoint -where - T: GeoFloat, -{ - /// Minimum distance from a MultiPoint to a Point - fn euclidean_distance(&self, point: &Point) -> T { - point.euclidean_distance(self) - } -} - // ┌──────────────────────────┐ // │ Implementations for Line │ // └──────────────────────────┘ @@ -323,20 +269,6 @@ where } } -/// Line to MultiPolygon distance -impl EuclideanDistance> for Line -where - T: GeoFloat + FloatConst + Signed + RTreeNum, -{ - fn euclidean_distance(&self, mpolygon: &MultiPolygon) -> T { - mpolygon - .0 - .iter() - .map(|p| self.euclidean_distance(p)) - .fold(Bounded::max_value(), |accum, val| accum.min(val)) - } -} - // ┌────────────────────────────────┐ // │ Implementations for LineString │ // └────────────────────────────────┘ @@ -400,20 +332,6 @@ where } } -// ┌─────────────────────────────────────┐ -// │ Implementations for MultiLineString │ -// └─────────────────────────────────────┘ - -impl EuclideanDistance> for MultiLineString -where - T: GeoFloat, -{ - /// Minimum distance from a MultiLineString to a Point - fn euclidean_distance(&self, point: &Point) -> T { - point.euclidean_distance(self) - } -} - // ┌─────────────────────────────┐ // │ Implementations for Polygon │ // └─────────────────────────────┘ @@ -480,47 +398,167 @@ where } } -// ┌──────────────────────────────────┐ -// │ Implementations for MultiPolygon │ -// └──────────────────────────────────┘ +// ┌────────────────────────────────────────┐ +// │ Implementations for Rect and Triangle │ +// └────────────────────────────────────────┘ -impl EuclideanDistance> for MultiPolygon -where - T: GeoFloat, -{ - /// Minimum distance from a MultiPolygon to a Point - fn euclidean_distance(&self, point: &Point) -> T { - point.euclidean_distance(self) - } +/// Implements Euclidean distance for Triangles and Rects by converting them to polygons. +macro_rules! impl_euclidean_distance_for_polygonlike_geometry { + ($for:ty, [$($target:ty),*]) => { + $( + impl EuclideanDistance for $for + where + T: GeoFloat + Signed + RTreeNum + FloatConst, + { + fn euclidean_distance(&self, other: &$target) -> T { + other.euclidean_distance(&self.to_polygon()) + } + } + )* + }; } -/// MultiPolygon to Line distance -impl EuclideanDistance> for MultiPolygon -where - T: GeoFloat + FloatConst + Signed + RTreeNum, -{ - fn euclidean_distance(&self, other: &Line) -> T { - other.euclidean_distance(self) - } +impl_euclidean_distance_for_polygonlike_geometry!(Triangle, [Point, MultiPoint, Line, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection, Rect, Triangle]); +impl_euclidean_distance_for_polygonlike_geometry!(Rect, [Point, MultiPoint, Line, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection, Rect, Triangle]); + +/// Implements Euclidean distance for other geometry types to Triangles and Rects by converting the Triangle or Rect to a polygon. +macro_rules! impl_euclidean_distance_to_polygonlike_geometry { + ($for:ty, [$($target:ty),*]) => { + $( + impl EuclideanDistance for $for + where + T: GeoFloat + Signed + RTreeNum + FloatConst, + { + fn euclidean_distance(&self, other: &$target) -> T { + other.to_polygon().euclidean_distance(self) + } + } + )* + }; +} + +impl_euclidean_distance_to_polygonlike_geometry!(Point, [Rect, Triangle]); +impl_euclidean_distance_to_polygonlike_geometry!(MultiPoint, [Rect, Triangle]); +impl_euclidean_distance_to_polygonlike_geometry!(Line, [Rect, Triangle]); +impl_euclidean_distance_to_polygonlike_geometry!(LineString, [Rect, Triangle]); +impl_euclidean_distance_to_polygonlike_geometry!(MultiLineString, [Rect, Triangle]); +impl_euclidean_distance_to_polygonlike_geometry!(Polygon, [Rect, Triangle]); +impl_euclidean_distance_to_polygonlike_geometry!(MultiPolygon, [Rect, Triangle]); +impl_euclidean_distance_to_polygonlike_geometry!(GeometryCollection, [Rect, Triangle]); + +// ┌───────────────────────────────────────────┐ +// │ Implementations for multi geometry types │ +// └───────────────────────────────────────────┘ + +/// Euclidean distance implementation for multi geometry types. +macro_rules! impl_euclidean_distance_for_iter_geometry { + ($for:ty, [$($target:ty),*]) => { + $( + impl EuclideanDistance for $for + where + T: GeoFloat + FloatConst + RTreeNum, + { + fn euclidean_distance(&self, target: &$target) -> T { + self + .iter() + .map(|g| g.euclidean_distance(target)) + .fold(::max_value(), |accum, val| accum.min(val)) + } + } + )* + }; +} + +impl_euclidean_distance_for_iter_geometry!(MultiPoint, [Point, MultiPoint, Line, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection]); +impl_euclidean_distance_for_iter_geometry!(MultiLineString, [Point, MultiPoint, Line, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection]); +impl_euclidean_distance_for_iter_geometry!(MultiPolygon, [Point, MultiPoint, Line, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection]); +impl_euclidean_distance_for_iter_geometry!(GeometryCollection, [Point, MultiPoint, Line, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection]); + +/// Euclidean distance implementation for other geometry types to multi geometry types, +/// using the multi geometry type's implementation. +macro_rules! impl_euclidean_distance_from_iter_geometry { + ($for:ty, [$($target:ty),*]) => { + $( + impl EuclideanDistance for $for + where + T: GeoFloat + FloatConst + RTreeNum + { + fn euclidean_distance(&self, target: &$target) -> T { + target.euclidean_distance(self) + } + } + )* + }; +} + +// This macro is used to implement EuclideanDistance to multi geometry types for non-multi geometry types. +// Rect and Triangle are omitted here because those implementations are included in the Rect and Triangle section above. +impl_euclidean_distance_from_iter_geometry!(Point, [MultiPoint, MultiLineString, MultiPolygon, GeometryCollection]); +impl_euclidean_distance_from_iter_geometry!(Line, [MultiPoint, MultiLineString, MultiPolygon, GeometryCollection]); +impl_euclidean_distance_from_iter_geometry!(LineString, [MultiPoint, MultiLineString, MultiPolygon, GeometryCollection]); +impl_euclidean_distance_from_iter_geometry!(Polygon, [MultiPoint, MultiLineString, MultiPolygon, GeometryCollection]); + +// ┌─────────────────────────────────────────────────────────┐ +// │ Implementation to Geometry for every geometry type │ +// └─────────────────────────────────────────────────────────┘ + +/// Euclidean distance implementation for every specific Geometry type to Geometry. +macro_rules! impl_euclidean_distance_to_geometry_for_specific { + ([$($for:ty),*]) => { + $( + impl EuclideanDistance> for $for + where + T: GeoFloat + FloatConst + RTreeNum, + { + fn euclidean_distance(&self, geom: &Geometry) -> T { + match geom { + Geometry::Point(p) => self.euclidean_distance(p), + Geometry::Line(l) => self.euclidean_distance(l), + Geometry::LineString(ls) => self.euclidean_distance(ls), + Geometry::Polygon(p) => self.euclidean_distance(p), + Geometry::MultiPoint(mp) => self.euclidean_distance(mp), + Geometry::MultiLineString(mls) => self.euclidean_distance(mls), + Geometry::MultiPolygon(mp) => self.euclidean_distance(mp), + Geometry::GeometryCollection(gc) => self.euclidean_distance(gc), + Geometry::Rect(r) => self.euclidean_distance(r), + Geometry::Triangle(t) => self.euclidean_distance(t), + } + } + } + )* + }; } +impl_euclidean_distance_to_geometry_for_specific!([Point, MultiPoint, Line, LineString, MultiLineString, Polygon, MultiPolygon, Triangle, Rect, GeometryCollection]); + // ┌──────────────────────────────┐ -// │ Implementations for Triangle │ +// │ Implementation for Geometry │ // └──────────────────────────────┘ -impl EuclideanDistance> for Triangle +/// Euclidean distance implementation for Geometry to every specific Geometry type. +macro_rules! impl_euclidean_distance_to_specific_for_geometry { + ([$($for:ty),*]) => { + $( + impl EuclideanDistance for Geometry + where + T: GeoFloat + FloatConst + RTreeNum + { + crate::geometry_delegate_impl! { + fn euclidean_distance(&self, other: &$for) -> T; + } + } + )* + }; +} + +impl_euclidean_distance_to_specific_for_geometry!([Point, MultiPoint, Line, LineString, MultiLineString, Polygon, MultiPolygon, Triangle, Rect, GeometryCollection]); + +impl EuclideanDistance for Geometry where - T: GeoFloat, + T: GeoFloat + FloatConst, { - fn euclidean_distance(&self, point: &Point) -> T { - if self.intersects(point) { - return T::zero(); - } - - [(self.0, self.1), (self.1, self.2), (self.2, self.0)] - .iter() - .map(|edge| ::geo_types::private_utils::line_segment_distance(point.0, edge.0, edge.1)) - .fold(::max_value(), |accum, val| accum.min(val)) + crate::geometry_delegate_impl! { + fn euclidean_distance(&self, other: &Geometry) -> T; } } @@ -548,8 +586,8 @@ pub fn nearest_neighbour_distance(geom1: &LineString, geom2: &LineString> = RTree::bulk_load(geom1.lines().collect::>()); - let tree_b: RTree> = RTree::bulk_load(geom2.lines().collect::>()); + let tree_a = RTree::bulk_load(geom1.lines().map(CachedEnvelope::new).collect()); + let tree_b = RTree::bulk_load(geom2.lines().map(CachedEnvelope::new).collect()); // Return minimum distance between all geom a points and geom b lines, and all geom b points and geom a lines geom2 .points() @@ -917,7 +955,7 @@ mod test { #[test] // test edge-vertex minimum distance fn test_minimum_polygon_distance() { - let points_raw = vec![ + let points_raw = [ (126., 232.), (126., 212.), (112., 202.), @@ -933,7 +971,7 @@ mod test { .collect::>(); let poly1 = Polygon::new(LineString::from(points), vec![]); - let points_raw_2 = vec![ + let points_raw_2 = [ (188., 231.), (189., 207.), (174., 196.), @@ -953,7 +991,7 @@ mod test { #[test] // test vertex-vertex minimum distance fn test_minimum_polygon_distance_2() { - let points_raw = vec![ + let points_raw = [ (118., 200.), (153., 179.), (106., 155.), @@ -966,7 +1004,7 @@ mod test { .collect::>(); let poly1 = Polygon::new(LineString::from(points), vec![]); - let points_raw_2 = vec![ + let points_raw_2 = [ (242., 186.), (260., 146.), (182., 175.), @@ -984,7 +1022,7 @@ mod test { #[test] // test edge-edge minimum distance fn test_minimum_polygon_distance_3() { - let points_raw = vec![ + let points_raw = [ (182., 182.), (182., 168.), (138., 160.), @@ -997,7 +1035,7 @@ mod test { .collect::>(); let poly1 = Polygon::new(LineString::from(points), vec![]); - let points_raw_2 = vec![ + let points_raw_2 = [ (232., 196.), (234., 150.), (194., 165.), @@ -1171,4 +1209,90 @@ mod test { assert_eq!(p1.euclidean_distance(&p4), 50.0f64); assert_eq!(p2.euclidean_distance(&p3), 50.0f64); } + #[test] + fn all_types_geometry_collection_test() { + let p = Point::new(0.0, 0.0); + let line = Line::from([(-1.0, -1.0), (-2.0, -2.0)]); + let ls = LineString::from(vec![(0.0, 0.0), (1.0, 10.0), (2.0, 0.0)]); + let poly = Polygon::new( + LineString::from(vec![(0.0, 0.0), (1.0, 10.0), (2.0, 0.0), (0.0, 0.0)]), + vec![], + ); + let tri = Triangle::from([(0.0, 0.0), (1.0, 10.0), (2.0, 0.0)]); + let rect = Rect::new((0.0, 0.0), (-1.0, -1.0)); + + let ls1 = LineString::from(vec![(0.0, 0.0), (1.0, 10.0), (2.0, 0.0), (0.0, 0.0)]); + let ls2 = LineString::from(vec![(3.0, 0.0), (4.0, 10.0), (5.0, 0.0), (3.0, 0.0)]); + let p1 = Polygon::new(ls1, vec![]); + let p2 = Polygon::new(ls2, vec![]); + let mpoly = MultiPolygon::new(vec![p1, p2]); + + let v = vec![ + Point::new(0.0, 10.0), + Point::new(1.0, 1.0), + Point::new(10.0, 0.0), + Point::new(1.0, -1.0), + Point::new(0.0, -10.0), + Point::new(-1.0, -1.0), + Point::new(-10.0, 0.0), + Point::new(-1.0, 1.0), + Point::new(0.0, 10.0), + ]; + let mpoint = MultiPoint::new(v); + + let v1 = LineString::from(vec![(0.0, 0.0), (1.0, 10.0)]); + let v2 = LineString::from(vec![(1.0, 10.0), (2.0, 0.0), (3.0, 1.0)]); + let mls = MultiLineString::new(vec![v1, v2]); + + let gc = GeometryCollection(vec![ + Geometry::Point(p), + Geometry::Line(line), + Geometry::LineString(ls), + Geometry::Polygon(poly), + Geometry::MultiPoint(mpoint), + Geometry::MultiLineString(mls), + Geometry::MultiPolygon(mpoly), + Geometry::Triangle(tri), + Geometry::Rect(rect), + ]); + + let test_p = Point::new(50., 50.); + assert_relative_eq!(test_p.euclidean_distance(&gc), 60.959002616512684); + + let test_multipoint = MultiPoint::new(vec![test_p]); + assert_relative_eq!(test_multipoint.euclidean_distance(&gc), 60.959002616512684); + + let test_line = Line::from([(50., 50.), (60., 60.)]); + assert_relative_eq!(test_line.euclidean_distance(&gc), 60.959002616512684); + + let test_ls = LineString::from(vec![(50., 50.), (60., 60.), (70., 70.)]); + assert_relative_eq!(test_ls.euclidean_distance(&gc), 60.959002616512684); + + let test_mls = MultiLineString::new(vec![test_ls]); + assert_relative_eq!(test_mls.euclidean_distance(&gc), 60.959002616512684); + + let test_poly = Polygon::new( + LineString::from(vec![ + (50., 50.), + (60., 50.), + (60., 60.), + (55., 55.), + (50., 50.), + ]), + vec![], + ); + assert_relative_eq!(test_poly.euclidean_distance(&gc), 60.959002616512684); + + let test_multipoly = MultiPolygon::new(vec![test_poly]); + assert_relative_eq!(test_multipoly.euclidean_distance(&gc), 60.959002616512684); + + let test_tri = Triangle::from([(50., 50.), (60., 50.), (55., 55.)]); + assert_relative_eq!(test_tri.euclidean_distance(&gc), 60.959002616512684); + + let test_rect = Rect::new(coord! { x: 50., y: 50. }, coord! { x: 60., y: 60. }); + assert_relative_eq!(test_rect.euclidean_distance(&gc), 60.959002616512684); + + let test_gc = GeometryCollection(vec![Geometry::Rect(test_rect)]); + assert_relative_eq!(test_gc.euclidean_distance(&gc), 60.959002616512684); + } } diff --git a/geo/src/algorithm/extremes.rs b/geo/src/algorithm/extremes.rs index d70144220b..893be0163c 100644 --- a/geo/src/algorithm/extremes.rs +++ b/geo/src/algorithm/extremes.rs @@ -44,7 +44,7 @@ pub struct Outcome { impl<'a, T, G> Extremes<'a, T> for G where - G: CoordsIter<'a, Scalar = T>, + G: CoordsIter, T: CoordNum, { fn extremes(&'a self) -> Option> { diff --git a/geo/src/algorithm/geodesic_bearing.rs b/geo/src/algorithm/geodesic_bearing.rs index 222bf14635..d3ad16ec46 100644 --- a/geo/src/algorithm/geodesic_bearing.rs +++ b/geo/src/algorithm/geodesic_bearing.rs @@ -13,8 +13,7 @@ pub trait GeodesicBearing { /// # Examples /// /// ``` - /// # #[macro_use] extern crate approx; - /// # + /// # use approx::assert_relative_eq; /// use geo::GeodesicBearing; /// use geo::Point; /// @@ -35,8 +34,7 @@ pub trait GeodesicBearing { /// # Examples /// /// ``` - /// # #[macro_use] extern crate approx; - /// # + /// # use approx::assert_relative_eq; /// use geo::GeodesicBearing; /// use geo::Point; /// diff --git a/geo/src/algorithm/geodesic_intermediate.rs b/geo/src/algorithm/geodesic_intermediate.rs index b6e4e36fdd..a4d43836cb 100644 --- a/geo/src/algorithm/geodesic_intermediate.rs +++ b/geo/src/algorithm/geodesic_intermediate.rs @@ -9,8 +9,7 @@ pub trait GeodesicIntermediate { /// # Examples /// /// ``` - /// # #[macro_use] extern crate approx; - /// # + /// # use approx::assert_relative_eq; /// use geo::GeodesicIntermediate; /// use geo::Point; /// diff --git a/geo/src/algorithm/hausdorff_distance.rs b/geo/src/algorithm/hausdorff_distance.rs new file mode 100644 index 0000000000..bdb0a35d9f --- /dev/null +++ b/geo/src/algorithm/hausdorff_distance.rs @@ -0,0 +1,136 @@ +use crate::algorithm::EuclideanDistance; +use crate::CoordsIter; +use crate::GeoFloat; +use geo_types::{Coord, Point}; +use num_traits::Bounded; + +/// Determine the distance between two geometries using the [Hausdorff distance formula]. +/// +/// Hausdorff distance is used to compare two point sets. It measures the maximum euclidean +/// distance of a point in one set to the nearest point in another set. Hausdorff distance +/// is often used to measure the amount of mismatch between two sets. +/// +/// [Hausdorff distance formula]: https://en.wikipedia.org/wiki/Hausdorff_distance + +pub trait HausdorffDistance +where + T: GeoFloat, +{ + fn hausdorff_distance(&self, rhs: &Rhs) -> T + where + Rhs: CoordsIter; +} + +impl HausdorffDistance for G +where + T: GeoFloat, + G: CoordsIter, +{ + fn hausdorff_distance(&self, rhs: &Rhs) -> T + where + Rhs: CoordsIter, + { + // calculate from A -> B + let hd1 = self + .coords_iter() + .map(|c| { + rhs.coords_iter() + .map(|c2| c.euclidean_distance(&c2)) + .fold(::max_value(), |accum, val| accum.min(val)) + }) + .fold(::min_value(), |accum, val| accum.max(val)); + + // Calculate from B -> A + let hd2 = rhs + .coords_iter() + .map(|c| { + self.coords_iter() + .map(|c2| c.euclidean_distance(&c2)) + .fold(::max_value(), |accum, val| accum.min(val)) + }) + .fold(::min_value(), |accum, val| accum.max(val)); + + // The max of the two + hd1.max(hd2) + } +} + +// ┌───────────────────────────┐ +// │ Implementations for Coord │ +// └───────────────────────────┘ + +impl HausdorffDistance for Coord +where + T: GeoFloat, +{ + fn hausdorff_distance(&self, rhs: &Rhs) -> T + where + Rhs: CoordsIter, + { + Point::from(*self).hausdorff_distance(rhs) + } +} + +#[cfg(test)] +mod test { + use crate::HausdorffDistance; + use crate::{line_string, polygon, MultiPoint, MultiPolygon}; + + #[test] + fn hd_mpnt_mpnt() { + let p1: MultiPoint<_> = vec![(0., 0.), (1., 2.)].into(); + let p2: MultiPoint<_> = vec![(2., 3.), (1., 2.)].into(); + assert_relative_eq!(p1.hausdorff_distance(&p2), 2.236068, epsilon = 1.0e-6); + } + + #[test] + fn hd_mpnt_poly() { + let p1: MultiPoint<_> = vec![(0., 0.), (1., 2.)].into(); + let poly = polygon![ + (x: 1., y: -3.1), (x: 3.7, y: 2.7), + (x: 0.9, y: 7.6), (x: -4.8, y: 6.7), + (x: -7.5, y: 0.9), (x: -4.7, y: -4.), + (x: 1., y: -3.1) + ]; + + assert_relative_eq!(p1.hausdorff_distance(&poly), 7.553807, epsilon = 1.0e-6) + } + + #[test] + fn hd_mpnt_lns() { + let p1: MultiPoint<_> = vec![(0., 0.), (1., 2.)].into(); + let lns = line_string![ + (x: 1., y: -3.1), (x: 3.7, y: 2.7), + (x: 0.9, y: 7.6), (x: -4.8, y: 6.7), + (x: -7.5, y: 0.9), (x: -4.7, y: -4.), + (x: 1., y: -3.1) + ]; + + assert_relative_eq!(p1.hausdorff_distance(&lns), 7.553807, epsilon = 1.0e-6) + } + + #[test] + fn hd_mpnt_mply() { + let p1: MultiPoint<_> = vec![(0., 0.), (1., 2.)].into(); + let multi_polygon = MultiPolygon::new(vec![ + polygon![ + (x: 0.0f32, y: 0.0), + (x: 2.0, y: 0.0), + (x: 2.0, y: 1.0), + (x: 0.0, y: 1.0), + ], + polygon![ + (x: 1.0, y: 1.0), + (x: -2.0, y: 1.0), + (x: -2.0, y: -1.0), + (x: 1.0, y: -1.0), + ], + ]); + + assert_relative_eq!( + p1.hausdorff_distance(&multi_polygon), + 2.236068, + epsilon = 1.0e-6 + ) + } +} diff --git a/geo/src/algorithm/haversine_bearing.rs b/geo/src/algorithm/haversine_bearing.rs index 5cd3f088d6..37b7cd6113 100644 --- a/geo/src/algorithm/haversine_bearing.rs +++ b/geo/src/algorithm/haversine_bearing.rs @@ -11,8 +11,7 @@ pub trait HaversineBearing { /// # Examples /// /// ``` - /// # #[macro_use] extern crate approx; - /// # + /// # use approx::assert_relative_eq; /// use geo::HaversineBearing; /// use geo::Point; /// diff --git a/geo/src/algorithm/haversine_closest_point.rs b/geo/src/algorithm/haversine_closest_point.rs new file mode 100644 index 0000000000..283989c145 --- /dev/null +++ b/geo/src/algorithm/haversine_closest_point.rs @@ -0,0 +1,637 @@ +use crate::{haversine_distance::HaversineDistance, HaversineBearing}; +use crate::{Closest, Contains}; +use crate::{CoordsIter, GeoFloat, HaversineDestination, Point, MEAN_EARTH_RADIUS}; +use geo_types::{ + Coord, Geometry, GeometryCollection, Line, LineString, MultiLineString, MultiPoint, + MultiPolygon, Polygon, Rect, Triangle, +}; + +use num_traits::FromPrimitive; + +/// Calculates the closest `Point` on a geometry from a given `Point` in sperical coordinates. +/// +/// Similar to [`ClosestPoint`](crate::ClosestPoint) but for spherical coordinates: +/// * Longitude (x) in the [-180; 180] degrees range. +/// * Latitude (y) in the [-90; 90] degrees range. +/// +/// The implemetation is based on . +/// +/// See [`Closest`] for a description of the return states. +/// +/// Note: This may return `Closest::Intersection` even for non-intersecting geometies if they are +/// very close to the input. +/// +/// Example: +/// ``` +/// # use geo::HaversineClosestPoint; +/// # use geo::{Point, Line, Closest}; +/// use approx::assert_relative_eq; +/// let line = Line::new(Point::new(-85.93942, 32.11055), Point::new(-84.74905, 32.61454)); +/// let p_from = Point::new(-84.75625, 31.81056); +/// if let Closest::SinglePoint(pt) = line.haversine_closest_point(&p_from) { +/// assert_relative_eq!(pt, Point::new(-85.13337428852164, 32.45365659858937), epsilon = 1e-6); +/// } else { +/// panic!("Closest::SinglePoint expected"); +/// } +/// ``` +pub trait HaversineClosestPoint +where + T: GeoFloat + FromPrimitive, +{ + fn haversine_closest_point(&self, from: &Point) -> Closest; +} + +// Implement for references as well as types +impl<'a, T, G> HaversineClosestPoint for &'a G +where + G: HaversineClosestPoint, + T: GeoFloat + FromPrimitive, +{ + fn haversine_closest_point(&self, from: &Point) -> Closest { + (*self).haversine_closest_point(from) + } +} + +impl HaversineClosestPoint for Point +where + T: GeoFloat + FromPrimitive, +{ + fn haversine_closest_point(&self, pt: &Point) -> Closest { + if self == pt { + Closest::Intersection(*self) + } else { + Closest::SinglePoint(*self) + } + } +} + +impl HaversineClosestPoint for Coord +where + T: GeoFloat + FromPrimitive, +{ + fn haversine_closest_point(&self, pt: &Point) -> Closest { + Point::from(*self).haversine_closest_point(pt) + } +} + +impl HaversineClosestPoint for Line +where + T: GeoFloat + FromPrimitive, +{ + fn haversine_closest_point(&self, from: &Point) -> Closest { + let p1 = self.start_point(); + let p2 = self.end_point(); + + // Optimization if the point s exactly one of the ends of the arc. + if p1 == *from { + return Closest::Intersection(p1); + } + + if p2 == *from { + return Closest::Intersection(p2); + } + + // This can probably be done cheaper + let d3 = p2.haversine_distance(&p1); + if d3 <= T::epsilon() { + // I think here it should be return Closest::SinglePoint(p1) + // If the line segment is degenerated to a point, that point is still the closest + // (instead of indeterminate as in the Cartesian case). + + return Closest::SinglePoint(p1); + } + + let pi = T::from(std::f64::consts::PI).unwrap(); + let crs_ad = p1.haversine_bearing(*from).to_radians(); + let crs_ab = p1.haversine_bearing(p2).to_radians(); + let crs_ba = if crs_ab > T::zero() { + crs_ab - pi + } else { + crs_ab + pi + }; + let crs_bd = p2.haversine_bearing(*from).to_radians(); + let d_crs1 = crs_ad - crs_ab; + let d_crs2 = crs_bd - crs_ba; + + let d1 = p1.haversine_distance(from); + + // d1, d2, d3 are in principle not needed, only the sign matters + let projection1 = d_crs1.cos(); + let projection2 = d_crs2.cos(); + + if projection1.is_sign_positive() && projection2.is_sign_positive() { + let earth_radius = T::from(MEAN_EARTH_RADIUS).unwrap(); + let xtd = (((d1 / earth_radius).sin() * d_crs1.sin()).asin()).abs(); + let atd = earth_radius * (((d1 / earth_radius).cos() / xtd.cos()).acos()).abs(); + + if xtd < T::epsilon() { + return Closest::Intersection(*from); + } else { + return Closest::SinglePoint(p1.haversine_destination(crs_ab.to_degrees(), atd)); + } + } + + // Projected falls outside the GC Arc + // Return shortest distance pt, project either on point sp1 or sp2 + let d2 = p2.haversine_distance(from); + if d1 < d2 { + return Closest::SinglePoint(p1); + } + Closest::SinglePoint(p2) + } +} + +impl HaversineClosestPoint for LineString +where + T: GeoFloat + FromPrimitive, +{ + // This is a naive implementation + fn haversine_closest_point(&self, from: &Point) -> Closest { + if self.coords_count() == 0 { + return Closest::Indeterminate; // Empty LineString + } + + let mut min_distance = num_traits::Float::max_value(); + let mut rv = Closest::Indeterminate; + for line in self.lines() { + match line.haversine_closest_point(from) { + Closest::Intersection(_) => todo!(), // For the time being this does not happen. + Closest::SinglePoint(pt) => { + let dist = pt.haversine_distance(from); + if dist < min_distance { + min_distance = dist; + rv = Closest::SinglePoint(pt); + } + } + // If there is a case where we cannot figure out the closest, + // Then it needs to be intereminate, instead of skipping + Closest::Indeterminate => return Closest::Indeterminate, + } + } + + rv + } +} + +fn closest_closed_simple_poly(lines: I, from: &Point) -> (Closest, T) +where + T: GeoFloat + FromPrimitive, + I: IntoIterator>, +{ + let mut min_distance = num_traits::Float::max_value(); + let mut rv = Closest::Indeterminate; + for line in lines { + match line.haversine_closest_point(from) { + Closest::Intersection(_) => todo!(), // For the time being this does not happen. + Closest::SinglePoint(pt) => { + let dist = pt.haversine_distance(from); + if dist < min_distance { + min_distance = dist; + rv = Closest::SinglePoint(pt); + } + } + + // If there is a case where we cannot figure out the closest, + // Then it needs to be intereminate, instead of skipping + // This however never happens for a Line/Point, which is the case here + Closest::Indeterminate => return (Closest::Indeterminate, T::zero()), + } + } + + (rv, min_distance) +} + +impl HaversineClosestPoint for Triangle +where + T: GeoFloat + FromPrimitive, +{ + fn haversine_closest_point(&self, from: &Point) -> Closest { + if self.contains(from) { + return Closest::Intersection(*from); + } + + closest_closed_simple_poly(self.to_lines(), from).0 + } +} + +impl HaversineClosestPoint for Rect +where + T: GeoFloat + FromPrimitive, +{ + fn haversine_closest_point(&self, from: &Point) -> Closest { + if self.contains(from) { + return Closest::Intersection(*from); + } + + closest_closed_simple_poly(self.to_lines(), from).0 + } +} + +impl HaversineClosestPoint for Polygon +where + T: GeoFloat + FromPrimitive, +{ + #[warn(unused_assignments)] + fn haversine_closest_point(&self, from: &Point) -> Closest { + if self.contains(from) { + return Closest::Intersection(*from); + } + + if self.exterior_coords_iter().count() < 3 { + // Not really a polygon + return Closest::Indeterminate; + } + + let (mut rv, mut min_distance) = closest_closed_simple_poly(self.exterior().lines(), from); + + match rv { + // Would not happen as it should be caught at the beginning of the function + Closest::Intersection(_) => return rv, + Closest::SinglePoint(_) => {} + // Would not happen either. See other geometries for an explanation. + // This is for future proof + Closest::Indeterminate => return rv, + } + + // Could be inside a inner ring + for ls in self.interiors() { + match closest_closed_simple_poly(ls.lines(), from) { + // Would not happen as it should be caught at the beginning of the function + (Closest::Intersection(pt), _) => return Closest::Intersection(pt), + (Closest::SinglePoint(pt), dist) => { + if min_distance > dist { + min_distance = dist; + rv = Closest::SinglePoint(pt); + } + } + // Would not happen either. + (Closest::Indeterminate, _) => unreachable!(), + } + } + + rv + } +} + +fn multi_geometry_nearest(iter: I, from: &Point) -> Closest +where + T: GeoFloat + FromPrimitive, + G: HaversineClosestPoint, + I: IntoIterator, +{ + let mut min_distance = ::max_value(); + let mut rv = Closest::Indeterminate; + + for c in iter { + match c.haversine_closest_point(from) { + // This mean on top of the line. + Closest::Intersection(pt) => return Closest::Intersection(pt), + Closest::SinglePoint(pt) => { + let dist = pt.haversine_distance(from); + if dist < min_distance { + min_distance = dist; + rv = Closest::SinglePoint(pt); + } + } + Closest::Indeterminate => return Closest::Indeterminate, + } + } + rv +} + +impl HaversineClosestPoint for MultiPoint +where + T: GeoFloat + FromPrimitive, +{ + fn haversine_closest_point(&self, from: &Point) -> Closest { + multi_geometry_nearest(self, from) + } +} + +impl HaversineClosestPoint for MultiLineString +where + T: GeoFloat + FromPrimitive, +{ + fn haversine_closest_point(&self, from: &Point) -> Closest { + multi_geometry_nearest(self, from) + } +} + +impl HaversineClosestPoint for MultiPolygon +where + T: GeoFloat + FromPrimitive, +{ + fn haversine_closest_point(&self, from: &Point) -> Closest { + multi_geometry_nearest(self, from) + } +} + +impl HaversineClosestPoint for Geometry +where + T: GeoFloat + FromPrimitive, +{ + crate::geometry_delegate_impl! { + fn haversine_closest_point(&self, from: &Point) -> Closest; + } +} + +impl HaversineClosestPoint for GeometryCollection +where + T: GeoFloat + FromPrimitive, +{ + fn haversine_closest_point(&self, from: &Point) -> Closest { + multi_geometry_nearest(self, from) + } +} + +#[cfg(test)] +mod test { + use wkt::TryFromWkt; + + use super::*; + + #[test] + fn point_to_point() { + let p_1 = Point::new(-84.74905, 32.61454); + let p_2 = Point::new(-85.93942, 32.11055); + + if let Closest::SinglePoint(p) = p_1.haversine_closest_point(&p_2) { + assert_relative_eq!(p_1, p); + } else { + panic!("Expecing Closest::SinglePoint"); + } + + if let Closest::SinglePoint(p) = p_2.haversine_closest_point(&p_1) { + assert_relative_eq!(p_2, p); + } else { + panic!("Expecing Closest::SinglePoint"); + } + + if let Closest::Intersection(p) = p_2.haversine_closest_point(&p_2) { + assert_relative_eq!(p_2, p); + } else { + panic!("Expecing Closest::Intersection"); + } + } + + #[test] + fn point_to_line_1() { + let p_1 = Point::new(-84.74905, 32.61454); + let p_2 = Point::new(-85.93942, 32.11055); + let line = Line::new(p_2, p_1); + + let p_from = Point::new(-84.75625, 31.81056); + if let Closest::SinglePoint(pt) = line.haversine_closest_point(&p_from) { + assert_relative_eq!(pt, Point::new(-85.13337428852164, 32.45365659858937)); + } else { + panic!("Did not get Closest::SinglePoint!"); + } + + let p_from = Point::new(-85.67211, 32.39774); + if let Closest::SinglePoint(pt) = line.haversine_closest_point(&p_from) { + assert_relative_eq!(pt, Point::new(-85.58999680564376, 32.26023534389268)); + } else { + panic!("Did not get Closest::SinglePoint!"); + } + } + + #[test] + fn point_to_line_intersection() { + let p_1 = Point::new(-84.74905, 32.61454); + let p_2 = Point::new(-85.93942, 32.11055); + let line = Line::new(p_2, p_1); + + if let Closest::Intersection(pt) = line.haversine_closest_point(&p_1) { + assert!(pt == p_1); + } else { + panic!("Did not get Closest::Intersection!"); + } + } + + #[test] + fn point_to_line_intersection_2() { + let p_1 = Point::new(-84.74905, 32.61454); + let p_2 = Point::new(-85.93942, 32.11055); + let line = Line::new(p_2, p_1); + + let p_from = Point::new(-85.13337428852164, 32.45365659858937); + if let Closest::Intersection(pt) = line.haversine_closest_point(&p_from) { + assert_relative_eq!(pt, p_from); + } else { + panic!("Did not get Closest::Intersection!"); + } + } + + #[test] + fn point_to_line_intersection_2_f32() { + let p_1 = Point::new(-84.74905f32, 32.61454f32); + let p_2 = Point::new(-85.93942f32, 32.11055f32); + let line = Line::new(p_2, p_1); + + let p_from = Point::new(-85.13337f32, 32.453656f32); + if let Closest::Intersection(pt) = line.haversine_closest_point(&p_from) { + assert_relative_eq!(pt, p_from); + } else { + panic!("Did not get Closest::Intersection!"); + } + } + + // Across the pole + #[test] + fn point_to_line_across_equator() { + let p_1 = Point::new(-38.424_794_871_794_916, 75.137_388_461_538_48); + let p_2 = Point::new(-28.608_712_820_512_863, -85.278_057_692_307_67); + let line = Line::new(p_2, p_1); + let p_from = Point::new(-25.86062, -87.32053); + + if let Closest::SinglePoint(pt) = line.haversine_closest_point(&p_from) { + assert_relative_eq!( + pt, + Point::new(-28.608_712_820_512_864, -85.278_057_692_307_67) + ); + } else { + panic!("Did not get Closest::SinglePoint!"); + } + } + + #[test] + fn point_to_line_across_close_to_north_pole() { + let p_1 = Point::new(-37.244_921_874_999_99, 79.508_612_500_000_03); + let p_2 = Point::new(50.596_875_000_000_01, 81.054_628_125_000_02); + let line = Line::new(p_2, p_1); + let p_from = Point::new(8.15172, 77.40041); + + if let Closest::SinglePoint(pt) = line.haversine_closest_point(&p_from) { + assert_relative_eq!( + pt, + Point::new(5.481_094_923_165_54, 82.998_280_987_615_33), + epsilon = 1.0e-6 + ); + } else { + panic!("Did not get Closest::SinglePoint!"); + } + } + + #[test] + fn point_to_linestring() { + let wkt = "LineString (3.86503906250000284 11.71231367187503736, 9.48691406250000568 17.3341886718750402, + 13.28167968750000227 15.50707929687503395, 15.95207031249999829 9.18246992187503963, + 7.73007812500000568 8.33918867187503565, 16.0926171875000108 2.8578605468750311, + 23.26050781250000909 6.3715324218750311, 24.66597656250000625 14.24215742187503508, + 20.23875000000001023 13.6799699218750419, 19.11437500000000966 10.72848554687503508, + 18.20082031249999943 13.60969648437503565, 16.79535156250000227 17.54500898437503054, + 20.09820312500001194 17.26391523437503395, 22.27667968750000682 15.64762617187503224, + 24.24433593750001137 18.24774335937503622, 18.97382812500001137 18.38829023437503452)"; + + let linestring = LineString::try_from_wkt_str(wkt).unwrap(); + + let p_from = Point::new(17.02374, 10.57037); + + if let Closest::SinglePoint(pt) = linestring.haversine_closest_point(&p_from) { + assert_relative_eq!( + pt, + Point::new(15.611386947136054, 10.006831648991811), + epsilon = 1.0e-6 + ); + } else { + panic!("Did not get Closest::SinglePoint!"); + } + } + + #[test] + fn point_to_empty_linestring() { + let linestring = LineString::new(vec![]); + + let p_from = Point::new(17.02374, 10.57037); + + assert!(linestring.haversine_closest_point(&p_from) == Closest::Indeterminate); + } + + #[test] + fn point_to_poly_outside() { + let wkt = "Polygon ((-10.99779296875000156 13.36373945312502087, -11.05049804687500092 13.85565351562501846, + -10.21600097656250128 13.9171427734375186, -9.63624511718750121 14.47054609375001988, + -8.2307763671875005 14.32121503906251903, -7.50168945312500135 13.65361738281252002, -7.50168945312500135 12.80155195312502059, + -7.61588378906250085 13.50428632812501917, -7.76521484375000171 13.71510664062502016, -8.11658203125000099 13.87322187500002002, + -8.27469726562500085 13.23197675781251981, -7.78278320312500149 12.7049259765625191, -8.25712890625000107 11.76501875000002073, + -9.03892089843750135 11.91434980468751981, -10.33897949218750156 11.51906171875002016, + -11.02414550781250213 12.46775312500001931, -9.0037841796875 12.33599042968752002, -8.46794921875000028 12.69614179687502009, + -8.67876953125000128 13.39009199218751966, -8.44159667968750149 13.88200605468751903, -9.12676269531250028 14.10161054687501903, + -9.68016601562500156 13.51307050781251995, -10.18964843750000071 13.02994062500001959, -10.99779296875000156 13.36373945312502087), + (-8.59092773437500057 12.32720625000001924, -8.48551757812500185 12.0461125000000191, + -8.16928710937500213 12.37112714843751959, -8.09022949218750043 12.74884687500001945, -8.59092773437500057 12.32720625000001924), + (-10.42682128906250227 13.5569914062500203, -10.26870605468750242 13.38130781250001888, + -9.8822021484375 13.58334394531252087, -9.84706542968750043 13.79416425781252009, -10.42682128906250227 13.5569914062500203))"; + + let poly = Polygon::try_from_wkt_str(wkt).unwrap(); + + let p_from = Point::new(-8.95108, 12.82790); + + if let Closest::SinglePoint(pt) = poly.haversine_closest_point(&p_from) { + assert_relative_eq!( + pt, + Point::new(-8.732575801021413, 12.518536164563992), + epsilon = 1.0e-6 + ); + } else { + panic!("Did not get Closest::SinglePoint!"); + } + } + + #[test] + fn point_to_poly_outside_in_inner_ring() { + let wkt = "Polygon ((-10.99779296875000156 13.36373945312502087, -11.05049804687500092 13.85565351562501846, + -10.21600097656250128 13.9171427734375186, -9.63624511718750121 14.47054609375001988, + -8.2307763671875005 14.32121503906251903, -7.50168945312500135 13.65361738281252002, -7.50168945312500135 12.80155195312502059, + -7.61588378906250085 13.50428632812501917, -7.76521484375000171 13.71510664062502016, -8.11658203125000099 13.87322187500002002, + -8.27469726562500085 13.23197675781251981, -7.78278320312500149 12.7049259765625191, -8.25712890625000107 11.76501875000002073, + -9.03892089843750135 11.91434980468751981, -10.33897949218750156 11.51906171875002016, + -11.02414550781250213 12.46775312500001931, -9.0037841796875 12.33599042968752002, -8.46794921875000028 12.69614179687502009, + -8.67876953125000128 13.39009199218751966, -8.44159667968750149 13.88200605468751903, -9.12676269531250028 14.10161054687501903, + -9.68016601562500156 13.51307050781251995, -10.18964843750000071 13.02994062500001959, -10.99779296875000156 13.36373945312502087), + (-8.59092773437500057 12.32720625000001924, -8.48551757812500185 12.0461125000000191, + -8.16928710937500213 12.37112714843751959, -8.09022949218750043 12.74884687500001945, -8.59092773437500057 12.32720625000001924), + (-10.42682128906250227 13.5569914062500203, -10.26870605468750242 13.38130781250001888, + -9.8822021484375 13.58334394531252087, -9.84706542968750043 13.79416425781252009, -10.42682128906250227 13.5569914062500203))"; + + let poly = Polygon::try_from_wkt_str(wkt).unwrap(); + + let p_from = Point::new(-8.38752, 12.29866); + + if let Closest::SinglePoint(pt) = poly.haversine_closest_point(&p_from) { + assert_relative_eq!( + pt, + Point::new(-8.310007197809414, 12.226641293789331), + epsilon = 1.0e-6 + ); + } else { + panic!("Did not get Closest::SinglePoint!"); + } + } + + #[test] + fn point_to_poly_inside() { + let wkt = "Polygon ((-10.99779296875000156 13.36373945312502087, -11.05049804687500092 13.85565351562501846, + -10.21600097656250128 13.9171427734375186, -9.63624511718750121 14.47054609375001988, + -8.2307763671875005 14.32121503906251903, -7.50168945312500135 13.65361738281252002, -7.50168945312500135 12.80155195312502059, + -7.61588378906250085 13.50428632812501917, -7.76521484375000171 13.71510664062502016, -8.11658203125000099 13.87322187500002002, + -8.27469726562500085 13.23197675781251981, -7.78278320312500149 12.7049259765625191, -8.25712890625000107 11.76501875000002073, + -9.03892089843750135 11.91434980468751981, -10.33897949218750156 11.51906171875002016, + -11.02414550781250213 12.46775312500001931, -9.0037841796875 12.33599042968752002, -8.46794921875000028 12.69614179687502009, + -8.67876953125000128 13.39009199218751966, -8.44159667968750149 13.88200605468751903, -9.12676269531250028 14.10161054687501903, + -9.68016601562500156 13.51307050781251995, -10.18964843750000071 13.02994062500001959, -10.99779296875000156 13.36373945312502087), + (-8.59092773437500057 12.32720625000001924, -8.48551757812500185 12.0461125000000191, + -8.16928710937500213 12.37112714843751959, -8.09022949218750043 12.74884687500001945, -8.59092773437500057 12.32720625000001924), + (-10.42682128906250227 13.5569914062500203, -10.26870605468750242 13.38130781250001888, + -9.8822021484375 13.58334394531252087, -9.84706542968750043 13.79416425781252009, -10.42682128906250227 13.5569914062500203))"; + + let poly = Polygon::try_from_wkt_str(wkt).unwrap(); + + let p_from = Point::new(-10.08341, 11.98792); + + if let Closest::Intersection(pt) = poly.haversine_closest_point(&p_from) { + assert_relative_eq!(pt, p_from); + } else { + panic!("Did not get Closest::Intersection!"); + } + } + + #[test] + fn point_to_multi_polygon() { + let wkt = "MultiPolygon (((-10.99779296875000156 13.36373945312502087, -11.05049804687500092 13.85565351562501846, + -10.21600097656250128 13.9171427734375186, -9.63624511718750121 14.47054609375001988, -8.2307763671875005 14.32121503906251903, + -7.50168945312500135 13.65361738281252002, -7.50168945312500135 12.80155195312502059, + -7.61588378906250085 13.50428632812501917, -7.76521484375000171 13.71510664062502016, -8.11658203125000099 13.87322187500002002, + -8.27469726562500085 13.23197675781251981, -7.78278320312500149 12.7049259765625191, -8.25712890625000107 11.76501875000002073, + -9.03892089843750135 11.91434980468751981, -10.33897949218750156 11.51906171875002016, + -11.02414550781250213 12.46775312500001931, -9.0037841796875 12.33599042968752002, + -8.46794921875000028 12.69614179687502009, -8.67876953125000128 13.39009199218751966, -8.44159667968750149 13.88200605468751903, + -9.12676269531250028 14.10161054687501903, -9.68016601562500156 13.51307050781251995, + -10.18964843750000071 13.02994062500001959, -10.99779296875000156 13.36373945312502087), + (-8.59092773437500057 12.32720625000001924, -8.48551757812500185 12.0461125000000191, -8.16928710937500213 12.37112714843751959, + -8.09022949218750043 12.74884687500001945, -8.59092773437500057 12.32720625000001924), + (-10.42682128906250227 13.5569914062500203, -10.26870605468750242 13.38130781250001888, -9.8822021484375 13.58334394531252087, + -9.84706542968750043 13.79416425781252009, -10.42682128906250227 13.5569914062500203)), + ((-8.99417648315430007 12.71261213378908828, -9.08641036987305029 12.51057600097658806, + -8.83606124877929844 12.48861555175783877, -8.69990646362304787 12.6818675048828382, + -8.74382736206055 12.77410139160158842, -8.86680587768555029 12.87951154785158892, + -8.99417648315430007 12.71261213378908828)),((-8.99856857299804958 13.68326398925784027, + -9.45095382690429986 13.16499738769534034, -9.48609054565430121 13.45926740722659076, + -9.34993576049805064 13.7403611572265909, -8.91511886596680014 13.90726057128909154, + -8.99856857299804958 13.68326398925784027)))"; + + let poly = MultiPolygon::try_from_wkt_str(wkt).unwrap(); + + let p_from = Point::new(-8.95108, 12.82790); + + if let Closest::SinglePoint(pt) = poly.haversine_closest_point(&p_from) { + assert_relative_eq!( + pt, + Point::new(-8.922208260289914, 12.806949983368323), + epsilon = 1.0e-6 + ); + } else { + panic!("Did not get Closest::SinglePoint!"); + } + } +} diff --git a/geo/src/algorithm/haversine_destination.rs b/geo/src/algorithm/haversine_destination.rs index 3398a51f4a..0f000f3dc3 100644 --- a/geo/src/algorithm/haversine_destination.rs +++ b/geo/src/algorithm/haversine_destination.rs @@ -1,4 +1,4 @@ -use crate::{CoordFloat, Point, MEAN_EARTH_RADIUS}; +use crate::{utils::normalize_longitude, CoordFloat, Point, MEAN_EARTH_RADIUS}; use num_traits::FromPrimitive; /// Returns a new Point using the distance to the existing Point and a bearing for the direction @@ -18,10 +18,11 @@ pub trait HaversineDestination { /// ```rust /// use geo::HaversineDestination; /// use geo::Point; + /// use approx::assert_relative_eq; /// /// let p_1 = Point::new(9.177789688110352, 48.776781529534965); /// let p_2 = p_1.haversine_destination(45., 10000.); - /// assert_eq!(p_2, Point::new(9.274409949623548, 48.84033274015048)) + /// assert_relative_eq!(p_2, Point::new(9.274409949623548, 48.84033274015048), epsilon = 1e-6) /// ``` fn haversine_destination(&self, bearing: T, distance: T) -> Point; } @@ -44,21 +45,25 @@ where .atan2(rad.cos() - center_lat.sin() * lat.sin()) + center_lng; - Point::new(lng.to_degrees(), lat.to_degrees()) + Point::new(normalize_longitude(lng.to_degrees()), lat.to_degrees()) } } #[cfg(test)] mod test { use super::*; - use crate::HaversineDistance; + use crate::{HaversineBearing, HaversineDistance}; use num_traits::pow; #[test] fn returns_a_new_point() { let p_1 = Point::new(9.177789688110352, 48.776781529534965); let p_2 = p_1.haversine_destination(45., 10000.); - assert_eq!(p_2, Point::new(9.274409949623548, 48.84033274015048)); + assert_relative_eq!( + p_2, + Point::new(9.274409949623548, 48.84033274015048), + epsilon = 1.0e-6 + ); let distance = p_1.haversine_distance(&p_2); assert_relative_eq!(distance, 10000., epsilon = 1.0e-6) } @@ -80,4 +85,18 @@ mod test { assert_relative_eq!(p_1.x(), p_2.x(), epsilon = 1.0e-6); assert!(p_2.y() > p_1.y()) } + + #[test] + fn should_wrap_correctly() { + let pt1 = Point::new(170.0, -30.0); + let pt2 = Point::new(-170.0, -30.0); + + for (start, end) in [(pt1, pt2), (pt2, pt1)] { + let bearing = start.haversine_bearing(end); + let results: Vec<_> = (0..8) + .map(|n| start.haversine_destination(bearing, n as f64 * 250_000.)) + .collect(); + assert!(results.iter().all(|pt| pt.x() >= -180.0 && pt.x() <= 180.0)); + } + } } diff --git a/geo/src/algorithm/haversine_intermediate.rs b/geo/src/algorithm/haversine_intermediate.rs index 21ac0296cf..5678f36746 100644 --- a/geo/src/algorithm/haversine_intermediate.rs +++ b/geo/src/algorithm/haversine_intermediate.rs @@ -9,8 +9,7 @@ pub trait HaversineIntermediate { /// # Examples /// /// ``` - /// # #[macro_use] extern crate approx; - /// # + /// # use approx::assert_relative_eq; /// use geo::HaversineIntermediate; /// use geo::Point; /// diff --git a/geo/src/algorithm/k_nearest_concave_hull.rs b/geo/src/algorithm/k_nearest_concave_hull.rs index 52e3e3b2fc..6f65b454f5 100644 --- a/geo/src/algorithm/k_nearest_concave_hull.rs +++ b/geo/src/algorithm/k_nearest_concave_hull.rs @@ -323,11 +323,11 @@ where mod tests { use super::*; use crate::coords_iter::CoordsIter; - use crate::geo_types::coord; + use geo_types::coord; #[test] fn coord_ordering() { - let coords = vec![ + let coords = [ coord!(x: 1.0, y: 1.0), coord!(x: -1.0, y: 0.0), coord!(x: 0.0, y: 1.0), @@ -387,7 +387,7 @@ mod tests { let poly = concave_hull(coords.iter(), 3); assert_eq!(poly.exterior().coords_count(), 12); - let must_not_be_in = vec![&coords[6]]; + let must_not_be_in = [&coords[6]]; for coord in poly.exterior().coords_iter() { for not_coord in must_not_be_in.iter() { assert_ne!(&coord, *not_coord); @@ -397,7 +397,7 @@ mod tests { #[test] fn empty_hull() { - let actual: Polygon = concave_hull(vec![].iter(), 3); + let actual: Polygon = concave_hull([].iter(), 3); let expected = Polygon::new(LineString::new(vec![]), vec![]); assert_eq!(actual, expected); } diff --git a/geo/src/algorithm/line_intersection.rs b/geo/src/algorithm/line_intersection.rs index f811ab1a2f..9da62a8985 100644 --- a/geo/src/algorithm/line_intersection.rs +++ b/geo/src/algorithm/line_intersection.rs @@ -312,7 +312,7 @@ fn proper_intersection(p: Line, q: Line) -> Coord { #[cfg(test)] mod test { use super::*; - use crate::geo_types::coord; + use geo_types::coord; /// Based on JTS test `testCentralEndpointHeuristicFailure` /// > Following cases were failures when using the CentralEndpointIntersector heuristic. diff --git a/geo/src/algorithm/line_locate_point.rs b/geo/src/algorithm/line_locate_point.rs index 34635381b5..cf2a78cab9 100644 --- a/geo/src/algorithm/line_locate_point.rs +++ b/geo/src/algorithm/line_locate_point.rs @@ -109,8 +109,7 @@ where #[cfg(test)] mod test { use super::*; - use crate::geo_types::coord; - use crate::point; + use crate::{coord, point}; use num_traits::Float; #[test] diff --git a/geo/src/algorithm/linestring_segment.rs b/geo/src/algorithm/linestring_segment.rs new file mode 100644 index 0000000000..426732c167 --- /dev/null +++ b/geo/src/algorithm/linestring_segment.rs @@ -0,0 +1,308 @@ +use crate::line_interpolate_point::LineInterpolatePoint; +use crate::{Coord, Densify, EuclideanLength, LineString, LinesIter, MultiLineString}; + +/// Segments a LineString into `n` equal length LineStrings as a MultiLineString. +/// `None` will be returned when `n` is equal to 0 or when a point +/// cannot be interpolated on a `Line` segment. +/// +/// +/// # Examples +/// ``` +/// use geo::{LineString, MultiLineString, LineStringSegmentize, Coord}; +/// // Create a simple line string +/// let lns: LineString = vec![[0.0, 0.0], [1.0, 2.0], [3.0, 6.0]].into(); +/// // Segment it into 6 LineStrings inside of a MultiLineString +/// let segmentized = lns.line_segmentize(6).unwrap(); +/// +/// // Recreate the MultiLineString from scratch +/// // this is the inner vector used to create the MultiLineString +/// let all_lines = vec![ +/// LineString::new(vec![Coord { x: 0.0, y: 0.0 }, Coord { x: 0.5, y: 1.0 }]), +/// LineString::new(vec![Coord { x: 0.5, y: 1.0 }, Coord { x: 1.0, y: 2.0 }]), +/// LineString::new(vec![Coord { x: 1.0, y: 2.0 }, Coord { x: 1.5, y: 3.0 }]), +/// LineString::new(vec![Coord { x: 1.5, y: 3.0 }, Coord { x: 2.0, y: 4.0 }]), +/// LineString::new(vec![Coord { x: 2.0, y: 4.0 }, Coord { x: 2.5, y: 5.0 }]), +/// LineString::new(vec![Coord { x: 2.5, y: 5.0 }, Coord { x: 3.0, y: 6.0 }]) +/// ]; +/// +/// // Create the MultiLineString +/// let mlns = MultiLineString::new(all_lines); +/// +/// // Compare the two +/// assert_eq!(mlns, segmentized); +///``` +pub trait LineStringSegmentize { + fn line_segmentize(&self, n: usize) -> Option; +} + +impl LineStringSegmentize for LineString { + fn line_segmentize(&self, n: usize) -> Option { + // Return None if n is 0 or the maximum usize + if (n == usize::MIN) || (n == usize::MAX) { + return None; + } else if n == 1 { + let mlns = MultiLineString::from(self.clone()); + return Some(mlns); + } + + // Vec to allocate the new LineString segments Coord Vec + // will be iterated over at end to create new vecs + let mut res_coords: Vec> = Vec::with_capacity(n); + + // calculate total length to track cumulative against + let total_length = self.euclidean_length().abs(); + + // tracks total length + let mut cum_length = 0_f64; + + // calculate the target fraction for the first iteration + // fraction will change based on each iteration + let segment_prop = (1_f64) / (n as f64); + let segment_length = total_length * segment_prop; + + // densify the LineString so that each `Line` segment is not longer + // than the segment length ensuring that we will never partition one + // Line more than once. + let densified = self.densify(segment_length); + + // if the densified line is exactly equal to the number of requested + // segments, return early. This will happen when a LineString has + // exactly 2 coordinates + if densified.lines().count() == n { + let linestrings = densified + .lines() + .map(LineString::from) + .collect::>(); + + return Some(MultiLineString::new(linestrings)); + }; + + // count the number of lines that will be iterated through + let n_lines = densified.lines().count(); + + let lns = densified.lines_iter(); + // instantiate the first Vec + let mut ln_vec: Vec = Vec::new(); + + // iterate through each line segment in the LineString + for (i, segment) in lns.enumerate() { + // All iterations only keep track of the second coordinate + // in the Line. We need to push the first coordinate in the + // first line string to ensure the linestring starts at the + // correct place + if i == 0 { + ln_vec.push(segment.start) + } + + let length = segment.euclidean_length().abs(); + + // update cumulative length + cum_length += length; + + if (cum_length >= segment_length) && (i != (n_lines - 1)) { + let remainder = cum_length - segment_length; + // if we get None, we exit the function and return None + let endpoint = segment.line_interpolate_point((length - remainder) / length)?; + + // add final coord to ln_vec + ln_vec.push(endpoint.into()); + + // now we drain all elements from the vector into an iterator + // this will be collected into a vector to be pushed into the + // results coord vec of vec + let to_push = ln_vec.drain(..); + + // now collect & push this vector into the results vector + res_coords.push(to_push.collect::>()); + + // now add the last endpoint as the first coord + // and the endpoint of the linesegment as well only + if i != n_lines { + ln_vec.push(endpoint.into()); + } + + cum_length = remainder; + } + + // push the end coordinate into the Vec to continue + // building the linestring + ln_vec.push(segment.end); + } + + // push the last linestring vector which isn't done by the for loop + res_coords.push(ln_vec); + + // collect the coords into vectors of LineStrings so we can createa + // a multi linestring + let res_lines = res_coords + .into_iter() + .map(LineString::new) + .collect::>(); + + Some(MultiLineString::new(res_lines)) + } +} + +#[cfg(test)] +mod test { + use approx::RelativeEq; + + use super::*; + use crate::{EuclideanLength, LineString}; + + #[test] + fn n_elems_bug() { + // Test for an edge case that seems to fail: + // https://github.com/georust/geo/issues/1075 + // https://github.com/JosiahParry/rsgeo/issues/28 + + let linestring: LineString = vec![ + [324957.69921197, 673670.123131518], + [324957.873557727, 673680.139281405], + [324959.863123514, 673686.784106964], + [324961.852683597, 673693.428933452], + [324963.822867622, 673698.960855279], + [324969.636546456, 673709.992098018], + [324976.718443977, 673722.114520549], + [324996.443964294, 673742.922904206], + ] + .into(); + let segments = linestring.line_segmentize(2).unwrap(); + assert_eq!(segments.0.len(), 2); + let segments = linestring.line_segmentize(3).unwrap(); + assert_eq!(segments.0.len(), 3); + let segments = linestring.line_segmentize(4).unwrap(); + assert_eq!(segments.0.len(), 4); + + assert_eq!(segments.euclidean_length(), linestring.euclidean_length()); + } + + #[test] + fn long_end_segment() { + let linestring: LineString = vec![ + [325581.792390628, 674398.495901267], + [325585.576868499, 674400.657039341], + [325589.966469742, 674401.694493658], + [325593.750940609, 674403.855638851], + [325599.389217394, 674404.871546368], + [325604.422360924, 674407.011146146], + [325665.309662534, 674424.885671739], + ] + .into(); + + let segments = linestring.line_segmentize(5).unwrap(); + assert_eq!(segments.0.len(), 5); + assert_relative_eq!( + linestring.euclidean_length(), + segments.euclidean_length(), + epsilon = f64::EPSILON + ); + } + + #[test] + fn two_coords() { + let linestring: LineString = vec![[0.0, 0.0], [0.0, 1.0]].into(); + + let segments = linestring.line_segmentize(5).unwrap(); + assert_eq!(segments.0.len(), 5); + assert_relative_eq!( + linestring.euclidean_length(), + segments.euclidean_length(), + epsilon = f64::EPSILON + ); + } + + #[test] + fn long_middle_segments() { + let linestring: LineString = vec![ + [325403.816883668, 673966.295402012], + [325410.280933752, 673942.805501254], + [325410.280933752, 673942.805501254], + [325439.782082601, 673951.201057316], + [325439.782082601, 673951.201057316], + [325446.064640793, 673953.318876004], + [325446.064640793, 673953.318876004], + [325466.14184472, 673958.537886844], + [325466.14184472, 673958.537886844], + [325471.799973648, 673960.666539074], + [325471.799973648, 673960.666539074], + [325518.255916084, 673974.335722824], + [325518.255916084, 673974.335722824], + [325517.669972133, 673976.572326305], + [325517.669972133, 673976.572326305], + [325517.084028835, 673978.808929878], + [325517.084028835, 673978.808929878], + [325515.306972763, 673984.405833764], + [325515.306972763, 673984.405833764], + [325513.549152184, 673991.115645844], + [325513.549152184, 673991.115645844], + [325511.772106396, 673996.712551354], + ] + .into(); + + let segments = linestring.line_segmentize(5).unwrap(); + assert_eq!(segments.0.len(), 5); + + assert_relative_eq!( + linestring.euclidean_length(), + segments.euclidean_length(), + epsilon = f64::EPSILON + ); + } + + #[test] + // that 0 returns None and that usize::MAX returns None + fn n_is_zero() { + let linestring: LineString = vec![[-1.0, 0.0], [0.5, 1.0], [1.0, 2.0]].into(); + let segments = linestring.line_segmentize(0); + assert!(segments.is_none()) + } + + #[test] + fn n_is_max() { + let linestring: LineString = vec![[-1.0, 0.0], [0.5, 1.0], [1.0, 2.0]].into(); + let segments = linestring.line_segmentize(usize::MAX); + assert!(segments.is_none()) + } + + #[test] + fn n_greater_than_lines() { + let linestring: LineString = vec![[-1.0, 0.0], [0.5, 1.0], [1.0, 2.0]].into(); + let segments = linestring.line_segmentize(5).unwrap(); + + // assert that there are n linestring segments + assert_eq!(segments.0.len(), 5); + + // assert that the lines are equal length + let lens = segments + .into_iter() + .map(|x| x.euclidean_length()) + .collect::>(); + + let first = lens[0]; + + assert!(lens + .iter() + .all(|x| first.relative_eq(x, f64::EPSILON, 1e-10))) + } + + #[test] + // test the cumulative length is the same + fn cumul_length() { + let linestring: LineString = vec![[0.0, 0.0], [1.0, 1.0], [1.0, 2.0], [3.0, 3.0]].into(); + let segments = linestring.line_segmentize(2).unwrap(); + + assert_relative_eq!( + linestring.euclidean_length(), + segments.euclidean_length(), + epsilon = f64::EPSILON + ) + } + + #[test] + fn n_elems() { + let linestring: LineString = vec![[0.0, 0.0], [1.0, 1.0], [1.0, 2.0], [3.0, 3.0]].into(); + let segments = linestring.line_segmentize(2).unwrap(); + assert_eq!(segments.0.len(), 2) + } +} diff --git a/geo/src/algorithm/minimum_rotated_rect.rs b/geo/src/algorithm/minimum_rotated_rect.rs index bcab7770b4..73ecc3aba8 100644 --- a/geo/src/algorithm/minimum_rotated_rect.rs +++ b/geo/src/algorithm/minimum_rotated_rect.rs @@ -28,19 +28,19 @@ use crate::{ /// ]) /// ); /// ``` -pub trait MinimumRotatedRect<'a, T> { +pub trait MinimumRotatedRect { type Scalar: GeoNum; - fn minimum_rotated_rect(&'a self) -> Option>; + fn minimum_rotated_rect(&self) -> Option>; } -impl<'a, T, G> MinimumRotatedRect<'a, T> for G +impl MinimumRotatedRect for G where T: CoordFloat + GeoFloat + GeoNum, - G: CoordsIter<'a, Scalar = T>, + G: CoordsIter, { type Scalar = T; - fn minimum_rotated_rect(&'a self) -> Option> { + fn minimum_rotated_rect(&self) -> Option> { let convex_poly = ConvexHull::convex_hull(self); let mut min_area: T = Float::max_value(); let mut min_angle: T = T::zero(); diff --git a/geo/src/algorithm/mod.rs b/geo/src/algorithm/mod.rs index bba8a95d2c..791ad98fee 100644 --- a/geo/src/algorithm/mod.rs +++ b/geo/src/algorithm/mod.rs @@ -56,6 +56,10 @@ pub use contains::Contains; pub mod convert; pub use convert::{Convert, TryConvert}; +/// Convert coordinate angle units between radians and degrees. +pub mod convert_angle_unit; +pub use convert_angle_unit::{ToDegrees, ToRadians}; + /// Calculate the convex hull of a `Geometry`. pub mod convex_hull; pub use convex_hull::ConvexHull; @@ -76,6 +80,10 @@ pub use coords_iter::CoordsIter; pub mod densify; pub use densify::Densify; +/// Densify spherical geometry components +pub mod densify_haversine; +pub use densify_haversine::DensifyHaversine; + /// Dimensionality of a geometry and its boundary, based on OGC-SFA. pub mod dimensions; pub use dimensions::HasDimensions; @@ -120,6 +128,10 @@ pub use geodesic_intermediate::GeodesicIntermediate; pub mod geodesic_length; pub use geodesic_length::GeodesicLength; +/// Calculate the Hausdorff distance between two geometries. +pub mod hausdorff_distance; +pub use hausdorff_distance::HausdorffDistance; + /// Calculate the bearing to another `Point`, in degrees. pub mod haversine_bearing; pub use haversine_bearing::HaversineBearing; @@ -140,6 +152,10 @@ pub use haversine_intermediate::HaversineIntermediate; pub mod haversine_length; pub use haversine_length::HaversineLength; +/// Calculate the closest point on a Great Circle arc geometry to a given point. +pub mod haversine_closest_point; +pub use haversine_closest_point::HaversineClosestPoint; + /// Calculate a representative `Point` inside a `Geometry` pub mod interior_point; pub use interior_point::InteriorPoint; @@ -172,6 +188,10 @@ pub use line_locate_point::LineLocatePoint; pub mod lines_iter; pub use lines_iter::LinesIter; +/// Split a LineString into n segments +pub mod linestring_segment; +pub use linestring_segment::LineStringSegmentize; + /// Apply a function to all `Coord`s of a `Geometry`. pub mod map_coords; pub use map_coords::{MapCoords, MapCoordsInPlace}; @@ -232,6 +252,16 @@ pub mod triangulate_earcut; #[cfg(feature = "earcutr")] pub use triangulate_earcut::TriangulateEarcut; +/// Triangulate polygons using an (un)constrained [Delaunay Triangulation](https://en.wikipedia.org/wiki/Delaunay_triangulation) algorithm. +#[cfg(feature = "spade")] +pub mod triangulate_spade; +#[cfg(feature = "spade")] +pub use triangulate_spade::TriangulateSpade; + +/// Vector Operations for 2D coordinates +mod vector_ops; +pub use vector_ops::Vector2DOps; + /// Calculate the Vincenty distance between two `Point`s. pub mod vincenty_distance; pub use vincenty_distance::VincentyDistance; @@ -255,3 +285,11 @@ pub mod sweep; pub mod outlier_detection; pub use outlier_detection::OutlierDetection; + +/// Monotonic polygon subdivision +pub mod monotone; +pub use monotone::{monotone_subdivision, MonoPoly, MonotonicPolygons}; + +/// Rhumb-line-related algorithms and utils +pub mod rhumb; +pub use rhumb::{RhumbBearing, RhumbDestination, RhumbDistance, RhumbIntermediate, RhumbLength}; diff --git a/geo/src/algorithm/monotone/builder.rs b/geo/src/algorithm/monotone/builder.rs new file mode 100644 index 0000000000..2e1328f12e --- /dev/null +++ b/geo/src/algorithm/monotone/builder.rs @@ -0,0 +1,377 @@ +//! Polygon monotone subdivision algorithm +//! +//! This implementation is based on these awesome [lecture notes] by David +//! Mount. The broad idea is to run a left-right planar sweep on the segments +//! of the polygon and try to iteratively extend parallel monotone chains. +//! +//! [lecture notes]: +//! //www.cs.umd.edu/class/spring2020/cmsc754/Lects/lect05-triangulate.pdf + +use super::{MonoPoly, SimpleSweep}; +use crate::{ + sweep::{EventType, LineOrPoint, SweepPoint}, + *, +}; +use std::{cell::Cell, mem::replace}; + +/// Construct a monotone subdivision (along the X-axis) of an iterator of polygons. +/// +/// Returns the set of monotone polygons that make up the subdivision. The +/// input polygon(s) must be a valid `MultiPolygon` (see the validity section in +/// [`MultiPolygon`]). In particular, each polygon must be simple, not +/// self-intersect, and contain only finite coordinates; further, the polygons +/// must have distinct interiors, and their boundaries may only intersect at +/// finite points. +pub fn monotone_subdivision>>( + iter: I, +) -> Vec> { + Builder::from_polygons_iter(iter).build() +} + +pub(super) struct Builder { + sweep: SimpleSweep, + chains: Vec>>, + outputs: Vec>, +} + +impl Builder { + /// Create a new builder from a polygon. + pub fn from_polygons_iter>>(iter: I) -> Self { + let iter = iter.into_iter().flat_map(|polygon| { + let (ext, ints) = polygon.into_inner(); + Some(ext) + .into_iter() + .chain(ints) + .flat_map(|ls| -> Vec<_> { ls.lines().collect() }) + .filter_map(|line| { + if line.start == line.end { + None + } else { + let line = LineOrPoint::from(line); + debug!("adding line {:?}", line); + Some((line, Default::default())) + } + }) + }); + Self { + sweep: SimpleSweep::new(iter), + chains: Vec::new(), + outputs: Vec::new(), + } + } + pub fn build(mut self) -> Vec> { + while self.process_next_pt() {} + self.outputs + } + + fn process_next_pt(&mut self) -> bool { + // Step 1. Get all the incoming and outgoing segments at the next point, + // and sort each of them by sweep ordering. + let mut incoming = vec![]; + let mut outgoing = vec![]; + + let pt = if let Some(pt) = self.sweep.next_point(|seg, ev| match ev { + EventType::LineRight => { + let rt = seg.line().right(); + incoming.push(seg); + let chain_idx = incoming.last().unwrap().payload().chain_idx.get(); + self.chains[chain_idx].as_mut().unwrap().fix_top(*rt); + } + EventType::LineLeft => { + outgoing.push(seg); + } + _ => unreachable!("unexpected event type"), + }) { + pt + } else { + return false; + }; + incoming.sort_by(|a, b| a.partial_cmp(b).unwrap()); + outgoing.sort_by(|a, b| a.partial_cmp(b).unwrap()); + + info!( + "\n\nprocessing point {:?}, #in={}, #out={}", + pt, + incoming.len(), + outgoing.len() + ); + + // Step 2. Calculate region below the point, and if any previous point + // registered a help. + let bot_segment = self.sweep.prev_active_from_geom(pt.into()); + let (bot_region, bot_help) = bot_segment + .as_ref() + .map(|seg| (seg.payload().next_is_inside.get(), seg.payload().help.get())) + .unwrap_or((false, None)); + debug!("bot region: {:?}", bot_region); + debug!("bot segment: {:?}", bot_segment.as_ref().map(|s| s.line())); + + // Step 3. Reduce incoming segments. Any two consecutive incoming + // segment that encloses the input region should now complete a + // mono-polygon; so we `finish` their chains. Thus, we should be left + // with at-most two incoming segments. + if !incoming.is_empty() { + let n = incoming.len(); + + #[allow(clippy::bool_to_int_with_if)] + let start_idx = if bot_region { 1 } else { 0 }; + let ub_idx = n - (n - start_idx) % 2; + debug!("reducing incoming segments: {n} -> {start_idx}..{ub_idx}"); + + let mut iter = incoming.drain(start_idx..ub_idx); + while let Some(first) = iter.next() { + let second = iter.next().unwrap(); + + let fc = self.chains[first.payload().chain_idx.get()].take().unwrap(); + let sc = self.chains[second.payload().chain_idx.get()] + .take() + .unwrap(); + + // Any help registered on the first segment should be considered. + if let Some(help) = first.payload().help.get() { + first.payload().help.set(None); + let mut fhc = self.chains[help[0]].take().unwrap(); + let mut shc = self.chains[help[1]].take().unwrap(); + fhc.push(*pt); + shc.push(*pt); + self.outputs.push(fc.finish_with(fhc)); + self.outputs.push(shc.finish_with(sc)); + } else { + self.outputs.push(fc.finish_with(sc)); + } + } + } + debug_assert!(incoming.len() <= 2); + // Handle help on bot segment and reduce further to at-most two chain + // indices that need to be extended. + let in_chains = if let Some(h) = bot_help { + debug!("serving to help: {h:?}"); + bot_segment.as_ref().unwrap().payload().help.set(None); + if !incoming.is_empty() { + let sc = self.chains[incoming[0].payload().chain_idx.get()] + .take() + .unwrap(); + let mut shc = self.chains[h[1]].take().unwrap(); + shc.push(*pt); + self.chains[h[0]].as_mut().unwrap().push(*pt); + self.outputs.push(shc.finish_with(sc)); + if incoming.len() == 1 { + (Some(h[0]), None) + } else { + let last_idx = if let Some(h) = incoming[1].payload().help.get() { + let mut fhc = self.chains[h[0]].take().unwrap(); + let fc = self.chains[incoming[1].payload().chain_idx.get()] + .take() + .unwrap(); + fhc.push(*pt); + self.chains[h[1]].as_mut().unwrap().push(*pt); + self.outputs.push(fc.finish_with(fhc)); + h[1] + } else { + incoming[1].payload().chain_idx.get() + }; + (Some(h[0]), Some(last_idx)) + } + } else { + self.chains[h[0]].as_mut().unwrap().push(*pt); + self.chains[h[1]].as_mut().unwrap().push(*pt); + (Some(h[0]), Some(h[1])) + } + } else if incoming.is_empty() { + (None, None) + } else { + let last_incoming = incoming.last().unwrap(); + let last_idx = if let Some(h) = last_incoming.payload().help.get() { + let mut fhc = self.chains[h[0]].take().unwrap(); + let fc = self.chains[last_incoming.payload().chain_idx.get()] + .take() + .unwrap(); + fhc.push(*pt); + self.chains[h[1]].as_mut().unwrap().push(*pt); + self.outputs.push(fc.finish_with(fhc)); + h[1] + } else { + last_incoming.payload().chain_idx.get() + }; + if incoming.len() == 1 { + (Some(last_idx), None) + } else { + (Some(incoming[0].payload().chain_idx.get()), Some(last_idx)) + } + }; + + // Step 4. Reduce outgoing segments. Any two consecutive outgoing + // segment that encloses the input region can be started as a new + // region. This again reduces the outgoing list to atmost two segments. + if !outgoing.is_empty() { + let n = outgoing.len(); + #[allow(clippy::bool_to_int_with_if)] + let start_idx = if bot_region { 1 } else { 0 }; + let ub_idx = n - (n - start_idx) % 2; + debug!("reducing outgoing segments: {n} -> {start_idx}..{ub_idx}"); + let mut iter = outgoing.drain(start_idx..ub_idx); + while let Some(first) = iter.next() { + let second = iter.next().unwrap(); + + let bot = first.line().right(); + let top = second.line().right(); + self.chains + .extend(Chain::from_segment_pair(*pt, *bot, *top).map(Some)); + first.payload().next_is_inside.set(true); + second.payload().next_is_inside.set(false); + first.payload().chain_idx.set(self.chains.len() - 2); + second.payload().chain_idx.set(self.chains.len() - 1); + } + } + debug_assert!(outgoing.len() <= 2); + + // Step 5. Tie up incoming and outgoing as applicable + debug!("in_chains: {in_chains:?}"); + match in_chains { + (None, None) => { + // No incoming segments left after reduction. Since we have + // reduced the outgoing, the only case is the "split vertex" or + // "<" case. Here, we will use the helper_chain to extend the + // chain. + if !outgoing.is_empty() { + assert!(outgoing.len() == 2); + let first = &outgoing[0]; + let second = &outgoing[1]; + let bot_segment = bot_segment.as_ref().unwrap(); + + let idx = bot_segment + .payload() + .helper_chain + .get() + .unwrap_or_else(|| bot_segment.payload().chain_idx.get()); + let mut new_chains = self.chains[idx].as_mut().unwrap().swap_at_top(*pt); + new_chains[0].push(*first.line().right()); + new_chains[1].push(*second.line().right()); + self.chains.extend(new_chains.map(Some)); + first.payload().next_is_inside.set(false); + second.payload().next_is_inside.set(true); + first.payload().chain_idx.set(self.chains.len() - 2); + second.payload().chain_idx.set(self.chains.len() - 1); + + bot_segment + .payload() + .helper_chain + .set(Some(self.chains.len() - 2)); + } else { + debug_assert!(!bot_region); + } + } + (Some(idx), None) => { + assert!(outgoing.len() == 1); + let first = &outgoing[0]; + let bot = first.line().right(); + self.chains[idx].as_mut().unwrap().push(*bot); + first.payload().next_is_inside.set(!bot_region); + first.payload().chain_idx.set(idx); + if let Some(b) = bot_segment { + b.payload().helper_chain.set(Some(idx)) + } + } + (Some(idx), Some(jdx)) => { + if !outgoing.is_empty() { + assert!(outgoing.len() == 2); + let first = &outgoing[0]; + let second = &outgoing[1]; + let bot = first.line().right(); + let top = second.line().right(); + self.chains[idx].as_mut().unwrap().push(*bot); + self.chains[jdx].as_mut().unwrap().push(*top); + first.payload().next_is_inside.set(false); + second.payload().next_is_inside.set(true); + first.payload().chain_idx.set(idx); + second.payload().chain_idx.set(jdx); + } else { + debug!("registering help: [{}, {}]", idx, jdx); + bot_segment + .as_ref() + .unwrap() + .payload() + .help + .set(Some([idx, jdx])); + } + if let Some(b) = bot_segment { + b.payload().helper_chain.set(Some(idx)) + } + } + _ => unreachable!(), + } + + true + } +} + +pub(super) struct Chain(LineString); + +impl std::fmt::Debug for Chain { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let bot: Vec> = self.0 .0.iter().map(|c| (*c).into()).collect(); + f.debug_tuple("Chain").field(&bot).finish() + } +} + +impl Chain { + pub fn from_segment_pair(start: Coord, first: Coord, second: Coord) -> [Self; 2] { + let [x, y, z] = [SweepPoint::from(start), first.into(), second.into()]; + debug!("Creating chain from {x:?} -> {y:?}"); + debug!(" {x:?} -> {z:?}"); + [ + Chain(line_string![start, first]), + Chain(line_string![start, second]), + ] + } + + pub fn fix_top(&mut self, pt: Coord) { + *self.0 .0.last_mut().unwrap() = pt; + } + + pub fn swap_at_top(&mut self, pt: Coord) -> [Self; 2] { + let top = self.0 .0.pop().unwrap(); + let prev = *self.0 .0.last().unwrap(); + debug!( + "chain swap: {:?} -> {:?}", + SweepPoint::from(top), + SweepPoint::from(pt) + ); + debug!("\tprev = {:?}", SweepPoint::from(prev)); + self.0 .0.push(pt); + + let old_chain = Chain(replace(&mut self.0 .0, vec![prev, top]).into()); + let new_chain = Chain(vec![prev, pt].into()); + + let lp1 = LineOrPoint::from((prev.into(), top.into())); + let lp2 = LineOrPoint::from((prev.into(), pt.into())); + if lp1 > lp2 { + [old_chain, new_chain] + } else { + [new_chain, old_chain] + } + } + + pub fn push(&mut self, pt: Coord) { + debug!("chain push: {:?} -> {:?}", self.0 .0.last().unwrap(), pt); + self.0 .0.push(pt); + } + + pub fn finish_with(self, other: Self) -> MonoPoly { + assert!( + self.0 .0[0] == other.0 .0[0] + && self.0 .0.last().unwrap() == other.0 .0.last().unwrap(), + "chains must finish with same start/end points" + ); + debug!("finishing {self:?} with {other:?}"); + MonoPoly::new(other.0, self.0) + } +} + +#[derive(Debug, Default, Clone)] +struct Info { + next_is_inside: Cell, + helper_chain: Cell>, + help: Cell>, + chain_idx: Cell, +} diff --git a/geo/src/algorithm/monotone/mod.rs b/geo/src/algorithm/monotone/mod.rs new file mode 100644 index 0000000000..ce311a190e --- /dev/null +++ b/geo/src/algorithm/monotone/mod.rs @@ -0,0 +1,78 @@ +mod mono_poly; +use crate::{Coord, GeoNum, Intersects, MultiPolygon, Polygon}; +pub use mono_poly::MonoPoly; + +mod segment; +use segment::RcSegment; +pub(crate) use segment::Segment; + +mod sweep; +pub(crate) use sweep::SimpleSweep; + +mod builder; +pub use builder::monotone_subdivision; + +/// A multi-polygon represented as a collection of (disjoint) monotone polygons. +/// +/// This structure is optimized for point-in-polygon queries, and is typically +/// much faster than the equivalent method on `Polygon`. This is because a +/// single monotone polygon can be tested for intersection with a point in +/// `O(log n)` time, where `n` is the number of vertices in the polygon. In +/// contrast, the equivalent method on `Polygon` is `O(n)`. Typically, a +/// polygon can be sub-divided into a small number of monotone polygons, thus +/// providing a significant speed-up. +/// +/// # Example +/// +/// Construct a `MonotonicPolygons` from a `Polygon`, or a `MultiPolygon` using +/// `MontonicPolygons::from`, and query point intersection via the +/// `Intersects` trait. +/// +/// ```rust +/// use geo::prelude::*; +/// use geo::{polygon, coord}; +/// +/// let polygon = polygon![ +/// (x: -2., y: 1.), +/// (x: 1., y: 3.), +/// (x: 4., y: 1.), +/// (x: 1., y: -1.), +/// (x: -2., y: 1.), +/// ]; +/// let mp = MonotonicPolygons::from(polygon); +/// assert!(mp.intersects(&coord!(x: -2., y: 1.))); +/// ``` +#[derive(Clone, Debug)] +pub struct MonotonicPolygons(Vec>); + +impl MonotonicPolygons { + /// Get a reference to the monotone polygons. + pub fn subdivisions(&self) -> &Vec> { + &self.0 + } + + /// Reduce to inner `Vec` of monotone polygons. + pub fn into_subdivisions(self) -> Vec> { + self.0 + } +} +impl From> for MonotonicPolygons { + fn from(poly: Polygon) -> Self { + Self(monotone_subdivision([poly])) + } +} + +impl From> for MonotonicPolygons { + fn from(mp: MultiPolygon) -> Self { + Self(monotone_subdivision(mp.0)) + } +} + +impl Intersects> for MonotonicPolygons { + fn intersects(&self, other: &Coord) -> bool { + self.0.iter().any(|mp| mp.intersects(other)) + } +} + +#[cfg(test)] +mod tests; diff --git a/geo/src/algorithm/monotone/mono_poly.rs b/geo/src/algorithm/monotone/mono_poly.rs new file mode 100644 index 0000000000..381ec876b7 --- /dev/null +++ b/geo/src/algorithm/monotone/mono_poly.rs @@ -0,0 +1,164 @@ +use geo_types::{private_utils::get_bounding_rect, Line}; + +use crate::{ + coordinate_position::CoordPos, sweep::SweepPoint, BoundingRect, Coord, CoordinatePosition, + GeoNum, HasKernel, Intersects, Kernel, LineString, Orientation, Polygon, Rect, +}; + +/// Monotone polygon +/// +/// A monotone polygon is a polygon that can be decomposed into two monotone +/// chains (along the X-axis). This implies any vertical line intersects the +/// polygon at most twice (or not at all). These polygons support +/// point-in-polygon queries in `O(log n)` time; use the `Intersects` +/// trait to query. +/// +/// This structure cannot be directly constructed. Use +/// `crate::algorithm::monotone_subdivision` algorithm to obtain a +/// `Vec`. Consider using `MonotonicPolygons` instead if you are not +/// interested in the individual monotone polygons. +#[derive(Clone, PartialEq)] +pub struct MonoPoly { + top: LineString, + bot: LineString, + bounds: Rect, +} + +impl BoundingRect for MonoPoly { + type Output = Rect; + + fn bounding_rect(&self) -> Self::Output { + self.bounds + } +} +impl std::fmt::Debug for MonoPoly { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let top: Vec> = self.top.0.iter().map(|c| (*c).into()).collect(); + let bot: Vec> = self.bot.0.iter().map(|c| (*c).into()).collect(); + f.debug_struct("MonoPoly") + .field("top", &top) + .field("bot", &bot) + .finish() + } +} + +impl MonoPoly { + /// Create a monotone polygon from the top and bottom chains. + /// + /// Note: each chain must be a strictly increasing sequence (in the lexigraphic + /// order), with the same start and end points. Further, the top chain must be + /// strictly above the bottom chain except at the end-points. Not all these + /// conditions are checked, and the algorithm may panic if they are not met. + pub(super) fn new(top: LineString, bot: LineString) -> Self { + debug_assert_eq!(top.0.first(), bot.0.first()); + debug_assert_eq!(top.0.last(), bot.0.last()); + debug_assert_ne!(top.0.first(), top.0.last()); + let bounds = get_bounding_rect(top.0.iter().chain(bot.0.iter()).cloned()).unwrap(); + Self { top, bot, bounds } + } + + /// Get a reference to the mono poly's top chain. + #[must_use] + pub fn top(&self) -> &LineString { + &self.top + } + + /// Get a reference to the mono poly's bottom chain. + #[must_use] + pub fn bot(&self) -> &LineString { + &self.bot + } + + /// Convert self to (top, bottom) pair of chains. + pub fn into_ls_pair(self) -> (LineString, LineString) { + (self.top, self.bot) + } + + /// Get the pair of segments in the chain that intersects the line parallel + /// to the Y-axis at the given x-coordinate. Ties are broken by picking the + /// segment with lower index, i.e. the segment closer to the start of the + /// chains. + pub fn bounding_segment(&self, x: T) -> Option<(Line, Line)> { + // binary search for the segment that contains the x coordinate. + let tl_idx = self.top.0.partition_point(|c| c.x < x); + if tl_idx == 0 && self.top.0[0].x != x { + return None; + } + let bl_idx = self.bot.0.partition_point(|c| c.x < x); + if bl_idx == 0 { + debug_assert_eq!(tl_idx, 0); + debug_assert_eq!(self.bot.0[0].x, x); + return Some(( + Line::new(self.top.0[0], self.top.0[1]), + Line::new(self.bot.0[0], self.bot.0[1]), + )); + } else { + debug_assert_ne!(tl_idx, 0); + } + + Some(( + Line::new(self.top.0[tl_idx - 1], self.top.0[tl_idx]), + Line::new(self.bot.0[bl_idx - 1], self.bot.0[bl_idx]), + )) + } + + /// Convert self into a [`Polygon`]. + pub fn into_polygon(self) -> Polygon { + let mut down = self.bot.0; + let mut top = self.top.0; + + down.reverse(); + assert_eq!(down.first(), top.last()); + top.extend(down.drain(1..)); + + let geom = LineString(top); + debug_assert!(geom.is_closed()); + + Polygon::new(geom, vec![]) + } +} + +impl CoordinatePosition for MonoPoly { + type Scalar = T; + + fn calculate_coordinate_position( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + if !self.bounds.intersects(coord) { + return; + } + let (top, bot) = if let Some(t) = self.bounding_segment(coord.x) { + t + } else { + return; + }; + + match ::Ker::orient2d(top.start, *coord, top.end) { + Orientation::Clockwise => return, + Orientation::Collinear => { + *is_inside = true; + *boundary_count += 1; + return; + } + _ => {} + } + match ::Ker::orient2d(bot.start, *coord, bot.end) { + Orientation::CounterClockwise => (), + Orientation::Collinear => { + *is_inside = true; + *boundary_count += 1; + } + _ => { + *is_inside = true; + } + } + } +} +impl Intersects> for MonoPoly { + fn intersects(&self, other: &Coord) -> bool { + self.coordinate_position(other) != CoordPos::Outside + } +} diff --git a/geo/src/algorithm/monotone/segment.rs b/geo/src/algorithm/monotone/segment.rs new file mode 100644 index 0000000000..0c7eab4342 --- /dev/null +++ b/geo/src/algorithm/monotone/segment.rs @@ -0,0 +1,118 @@ +use std::cell::{Ref, RefCell}; +use std::{cmp::Ordering, fmt::Debug, rc::Rc}; + +use crate::sweep::{Event, EventType, LineOrPoint, SweepPoint}; +use crate::GeoNum; + +/// A segment in the sweep line algorithm. +/// +/// Consists of a line and a payload. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Segment { + line: LineOrPoint, + payload: P, +} + +impl Segment {} + +impl PartialOrd for Segment { + fn partial_cmp(&self, other: &Self) -> Option { + self.line.partial_cmp(&other.line) + } +} + +impl PartialEq for Segment { + fn eq(&self, other: &Self) -> bool { + self.partial_cmp(other) == Some(Ordering::Equal) + } +} + +impl From> for Segment { + fn from(line: LineOrPoint) -> Self { + Segment { line, payload: () } + } +} + +impl From<(LineOrPoint, P)> for Segment { + fn from((line, payload): (LineOrPoint, P)) -> Self { + Segment { line, payload } + } +} + +#[derive(Debug)] +pub(crate) struct RcSegment(Rc>>); + +impl Clone for RcSegment { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl RcSegment { + pub(crate) fn split_at(&self, pt: SweepPoint) -> Self { + debug!("Splitting segment {:?} at {:?}", self, pt); + let mut borrow = RefCell::borrow_mut(&self.0); + let right = borrow.line.right(); + borrow.line = LineOrPoint::from((borrow.line.left(), pt)); + + let new_seg = Segment::from((LineOrPoint::from((pt, right)), borrow.payload.clone())); + Self(Rc::new(new_seg.into())) + } +} + +impl RcSegment { + pub(crate) fn payload(&self) -> Ref

{ + let borrow = RefCell::borrow(&self.0); + Ref::map(borrow, |s| &s.payload) + } + + pub(crate) fn line(&self) -> LineOrPoint { + RefCell::borrow(&self.0).line + } + + #[inline] + pub fn events(&self) -> [Event>; 2] { + let geom = RefCell::borrow(&self.0).line; + let left = geom.left(); + let right = geom.right(); + [ + Event { + point: left, + ty: if geom.is_line() { + EventType::LineLeft + } else { + EventType::PointLeft + }, + payload: self.clone(), + }, + Event { + point: right, + ty: if geom.is_line() { + EventType::LineRight + } else { + EventType::PointRight + }, + payload: self.clone(), + }, + ] + } +} + +impl From> for RcSegment { + fn from(value: Segment) -> Self { + RcSegment(Rc::new(value.into())) + } +} + +// Implement partial eq, parital ord, and eq for RcSegment +impl PartialEq for RcSegment { + fn eq(&self, other: &Self) -> bool { + self.0.eq(&other.0) + } +} + +impl PartialOrd for RcSegment { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.partial_cmp(&other.0) + } +} diff --git a/geo/src/algorithm/monotone/sweep.rs b/geo/src/algorithm/monotone/sweep.rs new file mode 100644 index 0000000000..40b4f0b8c7 --- /dev/null +++ b/geo/src/algorithm/monotone/sweep.rs @@ -0,0 +1,219 @@ +use crate::sweep::{Active, Event, EventType, LineOrPoint, SweepPoint, VecSet}; +use crate::{GeoNum, Orientation}; +use std::{collections::BinaryHeap, fmt::Debug}; + +use super::{RcSegment, Segment}; + +/// Simple planar sweep algorithm. +/// +/// Performs a planar sweep along the x-axis on a set of line or points. This +/// can be initialized with a iterator of line segments or points, along with an +/// optional payload. +/// +/// The sweep is used to: +/// +/// - iterate over all end-points of the input line or points in lex. order +/// +/// - query the set of active segments at the current iteration point: these are +/// the segments currently intersecting the sweep line, and are ordered by their +/// position on the line +/// +/// # Note +/// +/// This is a simpler version, which does not support intersections that are +/// interior to both segments. That is, every intersection between two segments +/// should be at the end point of at least one of them. In particular, overlaps +/// are also not supported (will panic). + +pub(crate) struct SimpleSweep { + events: BinaryHeap>>, + active_segments: VecSet>>, +} + +impl SimpleSweep { + pub(crate) fn new(iter: I) -> Self + where + I: IntoIterator, + D: Into>, + { + let iter = iter.into_iter(); + let size = { + let (min_size, max_size) = iter.size_hint(); + max_size.unwrap_or(min_size) + }; + let mut events = BinaryHeap::with_capacity(size); + let active_segments = VecSet::default(); + + for cr in iter { + let segment = RcSegment::from(cr.into()); + events.extend(segment.events()); + } + + SimpleSweep { + events, + active_segments, + } + } + + /// Progress and obtain the next sweep point along with the set of segments + /// ending at the current sweep point. + /// + /// The segments are returned as per the ordering of their `EventType`; in + /// particular, all segments ending at the current sweep point are returned + /// before the ones starting at the current sweep point. The idx of the + /// first segment starting at the current sweep point is returned in the + /// `split_idx` parameter. + pub(crate) fn next_point, EventType)>( + &mut self, + mut f: F, + ) -> Option> { + let point = self.peek_point(); + while let Some(pt) = point { + let ev = self.events.pop().unwrap(); + self.handle_event(ev, &mut |ev| { + let segment = ev.payload; + let ty = ev.ty; + f(segment, ty); + }); + if self.peek_point() != Some(pt) { + break; + } + } + point + } + + fn handle_event(&mut self, event: Event>, cb: &mut F) + where + F: FnMut(Event>), + { + // We may get spurious events from adjusting the line segment. Ignore. + if event.point != event.payload.line().left() && event.point != event.payload.line().right() + { + return; + } + + use EventType::*; + let segment = &event.payload; + trace!( + "handling event: {pt:?} ({ty:?}) @ {seg:?}", + pt = event.point, + ty = event.ty, + seg = segment, + ); + + match &event.ty { + LineLeft => { + let mut idx = self.active_segments.index_not_of(segment); + for is_next in [false, true] { + let (active, split) = if !is_next { + if idx > 0 { + let active = &self.active_segments[idx - 1]; + (active, self.check_interior_intersection(active, segment)) + } else { + continue; + } + } else if idx < self.active_segments.len() { + let active = &self.active_segments[idx]; + (active, self.check_interior_intersection(active, segment)) + } else { + continue; + }; + + match split { + SplitResult::SplitA(pt) => { + let new_seg = active.split_at(pt); + let [_, ev] = active.events(); + self.events.push(ev); + self.events.extend(new_seg.events()); + } + SplitResult::SplitB(pt) => { + let new_seg = segment.split_at(pt); + let [_, ev] = segment.events(); + self.events.push(ev); + self.events.extend(new_seg.events()); + } + SplitResult::None => {} + } + + // Special case: if we split at the current event point, then + // we have LineRight events in the queue that have to be + // processed before this. + + // There's always a top as this is a left event. + while self.events.peek().unwrap() > &event { + debug_assert_eq!(self.events.peek().unwrap().ty, LineRight); + debug_assert_eq!(self.events.peek().unwrap().point, event.point); + + let ev = self.events.pop().unwrap(); + self.handle_event(ev, cb); + if !is_next { + idx -= 1; + } + } + } + + self.active_segments.insert_at(idx, segment.clone()); + } + LineRight => { + let idx = self.active_segments.index_of(segment); + self.active_segments.remove_at(idx); + + if idx > 0 && idx < self.active_segments.len() { + let prev = &self.active_segments[idx - 1]; + let next = &self.active_segments[idx]; + self.check_interior_intersection(prev, next); + } + } + _ => {} + } + cb(event); + } + + #[inline] + pub(super) fn peek_point(&self) -> Option> { + self.events.peek().map(|e| e.point) + } + + pub(super) fn prev_active_from_geom(&self, geom: LineOrPoint) -> Option> { + let part_idx = self.active_segments.partition_point(|s| s.line() < geom); + if part_idx == 0 { + None + } else { + Some(self.active_segments[part_idx - 1].0.clone()) + } + } + + /// Check if the two segments intersect at a point interior to one of them. + fn check_interior_intersection( + &self, + a: &RcSegment, + b: &RcSegment, + ) -> SplitResult { + let la = a.line(); + let lb = b.line(); + + let lal = la.left(); + let lar = la.right(); + + let lbl = lb.left(); + let lbr = lb.right(); + + if lal < lbl && lbl < lar && la.orient2d(*lbl) == Orientation::Collinear { + SplitResult::SplitA(lbl) + } else if lal < lbr && lbr < lar && la.orient2d(*lbr) == Orientation::Collinear { + SplitResult::SplitA(lbr) + } else if lbl < lal && lal < lbr && lb.orient2d(*lal) == Orientation::Collinear { + SplitResult::SplitB(lal) + } else if lbl < lar && lar < lbr && lb.orient2d(*lar) == Orientation::Collinear { + SplitResult::SplitB(lar) + } else { + SplitResult::None + } + } +} + +enum SplitResult { + SplitA(SweepPoint), + SplitB(SweepPoint), + None, +} diff --git a/geo/src/algorithm/monotone/tests.rs b/geo/src/algorithm/monotone/tests.rs new file mode 100644 index 0000000000..ebb337cbe3 --- /dev/null +++ b/geo/src/algorithm/monotone/tests.rs @@ -0,0 +1,114 @@ +use std::{fmt::Display, str::FromStr}; + +use approx::RelativeEq; +use geo_types::Polygon; +use num_traits::Signed; +use wkt::{ToWkt, TryFromWkt}; + +use crate::{ + area::twice_signed_ring_area, coordinate_position::CoordPos, dimensions::Dimensions, + monotone::monotone_subdivision, GeoFloat, GeoNum, Relate, +}; + +pub(super) fn init_log() { + use pretty_env_logger::env_logger; + use std::io::Write; + let _ = env_logger::builder() + .format(|buf, record| writeln!(buf, "[{}] - {}", record.level(), record.args())) + .filter_level(log::LevelFilter::Debug) + .is_test(true) + .try_init(); +} + +fn twice_polygon_area(poly: &Polygon) -> T { + let mut area = twice_signed_ring_area(poly.exterior()).abs(); + for interior in poly.interiors() { + area = area - twice_signed_ring_area(interior).abs(); + } + area +} + +fn check_monotone_subdivision(wkt: &str) { + init_log(); + eprintln!("input: {wkt}"); + let input = Polygon::::try_from_wkt_str(wkt).unwrap(); + let area = twice_polygon_area(&input); + let subdivisions = monotone_subdivision([input.clone()]); + eprintln!("Got {} subdivisions", subdivisions.len()); + + let mut sub_area = T::zero(); + for (i, d1) in subdivisions.iter().enumerate() { + for (j, d2) in subdivisions.iter().enumerate() { + if i >= j { + continue; + } + let p1 = d1.clone().into_polygon(); + let p2 = d2.clone().into_polygon(); + let im = p1.relate(&p2); + let intin = im.get(CoordPos::Inside, CoordPos::Inside); + assert!(intin == Dimensions::Empty); + } + } + for div in subdivisions { + let (mut top, bot) = div.into_ls_pair(); + top.0.extend(bot.0.into_iter().rev().skip(1)); + if !top.is_closed() { + // This branch is for debugging + // It will never be reached unless assertions elsewhere are commented. + error!("Got an unclosed line string"); + error!("{}", top.to_wkt()); + } else { + let poly = Polygon::new(top, vec![]); + sub_area = sub_area + twice_polygon_area(&poly); + info!("{}", poly.to_wkt()); + + let im = poly.relate(&input); + assert!(im.is_within()); + } + } + assert_relative_eq!(area, sub_area); +} + +#[test] +fn test_monotone_subdivision_simple() { + let input = "POLYGON((0 0,5 5,3 0,5 -5,0 0))"; + check_monotone_subdivision::(input); +} + +#[test] +fn test_monotone_subdivision_merge_split() { + let input = "POLYGON((-5 -5, -3 0, -5 5, 5 5,3 0,5 -5))"; + check_monotone_subdivision::(input); +} + +#[test] +fn test_complex() { + let input = "POLYGON ((140 300, 140 100, 140 70, 340 220, 187 235, 191 285, 140 300), + (140 100, 150 100, 150 110, 140 100))"; + check_monotone_subdivision::(input); +} + +#[test] +fn test_complex2() { + let input = "POLYGON ((100 100, 200 150, 100 200, 200 250, 100 300, 400 300, + 300 200, 400 100, 100 100))"; + check_monotone_subdivision::(input); +} + +#[test] +fn test_complex3() { + let input = "POLYGON((0 0,11.9 1,5.1 2,6.6 3,13.3 4, + 20.4 5,11.5 6,1.3 7,19.4 8,15.4 9,2.8 10,7.0 11, + 13.7 12,24.0 13,2.6 14,9.6 15,0.2 16,250 16, + 67.1 15,66.1 14,61.2 13,76.4 12,75.1 11,88.3 10, + 75.3 9,63.8 8,84.2 7,77.5 6,95.9 5,83.8 4, + 86.9 3,64.5 2,68.3 1,99.6 0,0 0))"; + check_monotone_subdivision::(input); +} + +#[test] +fn test_tangent() { + let input = "POLYGON ((60 60, 60 200, 240 200, 240 60, 60 60), + (60 140, 110 170, 110 100, 80 100, 60 140))"; + check_monotone_subdivision::(input); +} diff --git a/geo/src/algorithm/outlier_detection.rs b/geo/src/algorithm/outlier_detection.rs index 96f680ce0c..db4b7207b7 100644 --- a/geo/src/algorithm/outlier_detection.rs +++ b/geo/src/algorithm/outlier_detection.rs @@ -341,7 +341,7 @@ mod tests { #[test] fn test_lof() { // third point is an outlier - let v = vec![ + let v = [ Point::new(0.0, 0.0), Point::new(0.0, 1.0), Point::new(3.0, 0.0), @@ -354,7 +354,7 @@ mod tests { #[test] fn test_lof2() { // fourth point is an outlier - let v = vec![ + let v = [ Point::new(0.0, 0.0), Point::new(1.0, 0.0), Point::new(1.0, 1.0), @@ -366,7 +366,7 @@ mod tests { #[test] fn test_lof3() { // second point is an outlier, sort and reverse so scores are in descending order - let v = vec![ + let v = [ Point::new(0.0, 0.0), Point::new(0.0, 3.0), Point::new(1.0, 0.0), @@ -430,7 +430,7 @@ mod tests { #[test] fn test_lof5() { // third point is an outlier - let v = vec![ + let v = [ Point::new(0.0, 0.0), Point::new(0.0, 1.0), Point::new(3.0, 0.0), diff --git a/geo/src/algorithm/relate/geomgraph/index/rstar_edge_set_intersector.rs b/geo/src/algorithm/relate/geomgraph/index/rstar_edge_set_intersector.rs index 282b9f745b..63c7b2b69e 100644 --- a/geo/src/algorithm/relate/geomgraph/index/rstar_edge_set_intersector.rs +++ b/geo/src/algorithm/relate/geomgraph/index/rstar_edge_set_intersector.rs @@ -26,7 +26,7 @@ where F: GeoFloat + rstar::RTreeNum, { fn new(i: usize, edge: &'a RefCell>) -> Self { - use crate::rstar::RTreeObject; + use rstar::RTreeObject; let p1 = edge.borrow().coords()[i]; let p2 = edge.borrow().coords()[i + 1]; Self { diff --git a/geo/src/algorithm/relate/geomgraph/intersection_matrix.rs b/geo/src/algorithm/relate/geomgraph/intersection_matrix.rs index 324daa3e25..f8a345a9c7 100644 --- a/geo/src/algorithm/relate/geomgraph/intersection_matrix.rs +++ b/geo/src/algorithm/relate/geomgraph/intersection_matrix.rs @@ -1,5 +1,8 @@ use crate::{coordinate_position::CoordPos, dimensions::Dimensions}; +use crate::geometry_cow::GeometryCow::Point; +use std::str::FromStr; + /// Models a *Dimensionally Extended Nine-Intersection Model (DE-9IM)* matrix. /// /// DE-9IM matrix values (such as "212FF1FF2") specify the topological relationship between @@ -261,9 +264,64 @@ impl IntersectionMatrix { pub fn get(&self, lhs: CoordPos, rhs: CoordPos) -> Dimensions { self.0[lhs][rhs] } + + /// Does the intersection matrix match the provided de-9im specification string? + /// + /// A de-9im spec string must be 9 characters long, and each character + /// must be one of the following: + /// + /// - 0: matches a 0-dimensional (point) intersection + /// - 1: matches a 1-dimensional (line) intersection + /// - 2: matches a 2-dimensional (area) intersection + /// - f or F: matches only empty dimensions + /// - t or T: matches anything non-empty + /// - *: matches anything + /// + /// ``` + /// use geo::algorithm::Relate; + /// use geo::geometry::Polygon; + /// use wkt::TryFromWkt; + /// + /// let a = Polygon::::try_from_wkt_str("POLYGON((0 0,4 0,4 4,0 4,0 0))").expect("valid WKT"); + /// let b = Polygon::::try_from_wkt_str("POLYGON((1 1,4 0,4 4,0 4,1 1))").expect("valid WKT"); + /// let im = a.relate(&b); + /// assert!(im.matches("212F11FF2").expect("valid de-9im spec")); + /// assert!(im.matches("TTT***FF2").expect("valid de-9im spec")); + /// assert!(!im.matches("TTT***FFF").expect("valid de-9im spec")); + /// ``` + pub fn matches(&self, spec: &str) -> Result { + if spec.len() != 9 { + return Err(InvalidInputError::new(format!( + "de-9im specification must be exactly 9 characters. Got {len}", + len = spec.len() + ))); + } + + let mut chars = spec.chars(); + for a in &[CoordPos::Inside, CoordPos::OnBoundary, CoordPos::Outside] { + for b in &[CoordPos::Inside, CoordPos::OnBoundary, CoordPos::Outside] { + let dim_spec = dimension_matcher::DimensionMatcher::try_from( + chars.next().expect("already validated length is 9"), + )?; + if !dim_spec.matches(self.0[*a][*b]) { + return Ok(false); + } + } + } + Ok(true) + } } -impl std::str::FromStr for IntersectionMatrix { +/// Build an IntersectionMatrix based on a string specification. +/// ``` +/// use geo::algorithm::relate::IntersectionMatrix; +/// use std::str::FromStr; +/// +/// let intersection_matrix = IntersectionMatrix::from_str("212101212").expect("valid de-9im specification"); +/// assert!(intersection_matrix.is_intersects()); +/// assert!(!intersection_matrix.is_contains()); +/// ``` +impl FromStr for IntersectionMatrix { type Err = InvalidInputError; fn from_str(str: &str) -> Result { let mut im = IntersectionMatrix::empty(); @@ -271,3 +329,75 @@ impl std::str::FromStr for IntersectionMatrix { Ok(im) } } + +pub(crate) mod dimension_matcher { + use super::Dimensions; + use super::InvalidInputError; + + /// A single letter from a de-9im matching specification like "1*T**FFF*" + pub(crate) enum DimensionMatcher { + Anything, + NonEmpty, + Exact(Dimensions), + } + + impl DimensionMatcher { + pub fn matches(&self, dim: Dimensions) -> bool { + match (self, dim) { + (Self::Anything, _) => true, + (DimensionMatcher::NonEmpty, d) => d != Dimensions::Empty, + (DimensionMatcher::Exact(a), b) => a == &b, + } + } + } + + impl TryFrom for DimensionMatcher { + type Error = InvalidInputError; + + fn try_from(value: char) -> Result { + Ok(match value { + '*' => Self::Anything, + 't' | 'T' => Self::NonEmpty, + 'f' | 'F' => Self::Exact(Dimensions::Empty), + '0' => Self::Exact(Dimensions::ZeroDimensional), + '1' => Self::Exact(Dimensions::OneDimensional), + '2' => Self::Exact(Dimensions::TwoDimensional), + _ => { + return Err(InvalidInputError::new(format!( + "invalid de-9im specification character: {value}" + ))) + } + }) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn subject() -> IntersectionMatrix { + // Topologically, this is a nonsense IM + IntersectionMatrix::from_str("F00111222").unwrap() + } + + #[test] + fn matches_exactly() { + assert!(subject().matches("F00111222").unwrap()); + } + + #[test] + fn doesnt_match() { + assert!(!subject().matches("222222222").unwrap()); + } + + #[test] + fn matches_truthy() { + assert!(subject().matches("FTTTTTTTT").unwrap()); + } + + #[test] + fn matches_wildcard() { + assert!(subject().matches("F0011122*").unwrap()); + } +} diff --git a/geo/src/algorithm/relate/geomgraph/robust_line_intersector.rs b/geo/src/algorithm/relate/geomgraph/robust_line_intersector.rs index dc014d4998..4acfde3b58 100644 --- a/geo/src/algorithm/relate/geomgraph/robust_line_intersector.rs +++ b/geo/src/algorithm/relate/geomgraph/robust_line_intersector.rs @@ -1,8 +1,8 @@ use super::{LineIntersection, LineIntersector}; use crate::kernels::{Kernel, Orientation, RobustKernel}; -use crate::num_traits::Zero; use crate::{BoundingRect, Contains, Intersects}; use crate::{Coord, GeoFloat, Line, Rect}; +use num_traits::Zero; /// A robust version of [LineIntersector](traits.LineIntersector). #[derive(Clone)] diff --git a/geo/src/algorithm/rhumb/bearing.rs b/geo/src/algorithm/rhumb/bearing.rs new file mode 100644 index 0000000000..c1c6e3c853 --- /dev/null +++ b/geo/src/algorithm/rhumb/bearing.rs @@ -0,0 +1,91 @@ +use num_traits::FromPrimitive; + +use crate::{CoordFloat, Point}; + +use super::RhumbCalculations; + +/// Returns the bearing to another Point in degrees. +/// +/// Bullock, R.: Great Circle Distances and Bearings Between Two Locations, 2007. +/// () + +pub trait RhumbBearing { + /// Returns the bearing to another Point in degrees along a [rhumb line], where North is 0° and East is 90°. + /// + /// # Examples + /// + /// ``` + /// # use approx::assert_relative_eq; + /// use geo::RhumbBearing; + /// use geo::Point; + /// + /// let p_1 = Point::new(9.177789688110352, 48.776781529534965); + /// let p_2 = Point::new(9.274348757829898, 48.84037308229984); + /// let bearing = p_1.rhumb_bearing(p_2); + /// assert_relative_eq!(bearing, 45., epsilon = 1.0e-6); + /// ``` + /// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line + + fn rhumb_bearing(&self, point: Point) -> T; +} + +impl RhumbBearing for Point +where + T: CoordFloat + FromPrimitive, +{ + fn rhumb_bearing(&self, point: Point) -> T { + let three_sixty = T::from(360.0f64).unwrap(); + + let calculations = RhumbCalculations::new(self, &point); + (calculations.theta().to_degrees() + three_sixty) % three_sixty + } +} + +#[cfg(test)] +mod test { + use crate::point; + use crate::RhumbBearing; + use crate::RhumbDestination; + + #[test] + fn north_bearing() { + let p_1 = point!(x: 9., y: 47.); + let p_2 = point!(x: 9., y: 48.); + let bearing = p_1.rhumb_bearing(p_2); + assert_relative_eq!(bearing, 0.); + } + + #[test] + fn equatorial_east_bearing() { + let p_1 = point!(x: 9., y: 0.); + let p_2 = point!(x: 10., y: 0.); + let bearing = p_1.rhumb_bearing(p_2); + assert_relative_eq!(bearing, 90.); + } + + #[test] + fn east_bearing() { + let p_1 = point!(x: 9., y: 10.); + let p_2 = point!(x: 18.131938299366652, y: 10.); + + let bearing = p_1.rhumb_bearing(p_2); + assert_relative_eq!(bearing, 90.); + } + + #[test] + fn northeast_bearing() { + let p_1 = point!(x: 9.177789688110352f64, y: 48.776781529534965); + let p_2 = point!(x: 9.274348757829898, y: 48.84037308229984); + let bearing = p_1.rhumb_bearing(p_2); + assert_relative_eq!(bearing, 45., epsilon = 1.0e-6); + } + + #[test] + fn consistent_with_destination() { + let p_1 = point!(x: 9.177789688110352f64, y: 48.776781529534965); + let p_2 = p_1.rhumb_destination(45., 10000.); + + let b_1 = p_1.rhumb_bearing(p_2); + assert_relative_eq!(b_1, 45., epsilon = 1.0e-6); + } +} diff --git a/geo/src/algorithm/rhumb/destination.rs b/geo/src/algorithm/rhumb/destination.rs new file mode 100644 index 0000000000..a9b9be85e0 --- /dev/null +++ b/geo/src/algorithm/rhumb/destination.rs @@ -0,0 +1,80 @@ +use crate::{CoordFloat, Point, MEAN_EARTH_RADIUS}; +use num_traits::FromPrimitive; + +use super::calculate_destination; + +/// Returns the destination Point having travelled the given distance along a [rhumb line] +/// from the origin geometry with the given bearing +/// +/// *Note*: this implementation uses a mean earth radius of 6371.088 km, based on the [recommendation of +/// the IUGG](ftp://athena.fsv.cvut.cz/ZFG/grs80-Moritz.pdf) +pub trait RhumbDestination { + /// Returns the destination Point having travelled the given distance along a [rhumb line] + /// from the origin Point with the given bearing + /// + /// # Units + /// + /// - `bearing`: degrees, zero degrees is north + /// - `distance`: meters + /// + /// # Examples + /// + /// ``` + /// use geo::RhumbDestination; + /// use geo::Point; + /// + /// let p_1 = Point::new(9.177789688110352, 48.776781529534965); + /// let p_2 = p_1.rhumb_destination(45., 10000.); + /// assert_eq!(p_2, Point::new(9.274348757829898, 48.84037308229984)) + /// ``` + /// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line + fn rhumb_destination(&self, bearing: T, distance: T) -> Point; +} + +impl RhumbDestination for Point +where + T: CoordFloat + FromPrimitive, +{ + fn rhumb_destination(&self, bearing: T, distance: T) -> Point { + let delta = distance / T::from(MEAN_EARTH_RADIUS).unwrap(); // angular distance in radians + let lambda1 = self.x().to_radians(); + let phi1 = self.y().to_radians(); + let theta = bearing.to_radians(); + + calculate_destination(delta, lambda1, phi1, theta) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::RhumbDistance; + use num_traits::pow; + + #[test] + fn returns_a_new_point() { + let p_1 = Point::new(9.177789688110352, 48.776781529534965); + let p_2 = p_1.rhumb_destination(45., 10000.); + assert_eq!(p_2, Point::new(9.274348757829898, 48.84037308229984)); + let distance = p_1.rhumb_distance(&p_2); + assert_relative_eq!(distance, 10000., epsilon = 1.0e-6) + } + + #[test] + fn direct_and_indirect_destinations_are_close() { + let p_1 = Point::new(9.177789688110352, 48.776781529534965); + let p_2 = p_1.rhumb_destination(45., 10000.); + let square_edge = { pow(10000., 2) / 2f64 }.sqrt(); + let p_3 = p_1.rhumb_destination(0., square_edge); + let p_4 = p_3.rhumb_destination(90., square_edge); + assert_relative_eq!(p_4, p_2, epsilon = 1.0e-3); + } + + #[test] + fn bearing_zero_is_north() { + let p_1 = Point::new(9.177789688110352, 48.776781529534965); + let p_2 = p_1.rhumb_destination(0., 1000.); + assert_relative_eq!(p_1.x(), p_2.x(), epsilon = 1.0e-6); + assert!(p_2.y() > p_1.y()) + } +} diff --git a/geo/src/algorithm/rhumb/distance.rs b/geo/src/algorithm/rhumb/distance.rs new file mode 100644 index 0000000000..a0fc8fd6b3 --- /dev/null +++ b/geo/src/algorithm/rhumb/distance.rs @@ -0,0 +1,99 @@ +use crate::{CoordFloat, Point, MEAN_EARTH_RADIUS}; +use num_traits::FromPrimitive; + +use super::RhumbCalculations; + +/// Determine the distance between two geometries along a [rhumb line]. +/// +/// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line +/// +/// *Note*: this implementation uses a mean earth radius of 6371.088 km, based on the [recommendation of +/// the IUGG](ftp://athena.fsv.cvut.cz/ZFG/grs80-Moritz.pdf) +pub trait RhumbDistance { + /// Determine the distance between along the [rhumb line] between two geometries. + /// + /// # Units + /// + /// - return value: meters + /// + /// # Examples + /// + /// ```rust + /// use geo::prelude::*; + /// use geo::point; + /// + /// // New York City + /// let p1 = point!(x: -74.006f64, y: 40.7128f64); + /// + /// // London + /// let p2 = point!(x: -0.1278f64, y: 51.5074f64); + /// + /// let distance = p1.rhumb_distance(&p2); + /// + /// assert_eq!( + /// 5_794_129., // meters + /// distance.round() + /// ); + /// ``` + /// + /// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line + fn rhumb_distance(&self, rhs: &Rhs) -> T; +} + +impl RhumbDistance> for Point +where + T: CoordFloat + FromPrimitive, +{ + fn rhumb_distance(&self, rhs: &Point) -> T { + let calculations = RhumbCalculations::new(self, rhs); + calculations.delta() * T::from(MEAN_EARTH_RADIUS).unwrap() + } +} + +#[cfg(test)] +mod test { + use crate::Point; + use crate::RhumbDistance; + + #[test] + fn distance1_test() { + let a = Point::new(0., 0.); + let b = Point::new(1., 0.); + assert_relative_eq!( + a.rhumb_distance(&b), + 111195.0802335329_f64, + epsilon = 1.0e-6 + ); + } + + #[test] + fn distance2_test() { + let a = Point::new(-72.1235, 42.3521); + let b = Point::new(72.1260, 70.612); + assert_relative_eq!( + a.rhumb_distance(&b), + 8903668.508603323_f64, + epsilon = 1.0e-6 + ); + } + + #[test] + fn distance3_test() { + // this input comes from issue #100 + let a = Point::new(-77.036585, 38.897448); + let b = Point::new(-77.009080, 38.889825); + assert_relative_eq!( + a.rhumb_distance(&b), + 2526.7031699343006_f64, + epsilon = 1.0e-6 + ); + } + + #[test] + fn distance3_test_f32() { + // this input comes from issue #100 + let a = Point::::new(-77.03658, 38.89745); + let b = Point::::new(-77.00908, 38.889825); + assert_relative_eq!(a.rhumb_distance(&b), 2526.7273_f32, epsilon = 1.0e-6); + } +} diff --git a/geo/src/algorithm/rhumb/intermediate.rs b/geo/src/algorithm/rhumb/intermediate.rs new file mode 100644 index 0000000000..54651c6ec1 --- /dev/null +++ b/geo/src/algorithm/rhumb/intermediate.rs @@ -0,0 +1,143 @@ +use crate::{CoordFloat, Point, MEAN_EARTH_RADIUS}; +use num_traits::FromPrimitive; + +use super::RhumbCalculations; + +/// Returns a new Point along a rhumb line between two existing points + +pub trait RhumbIntermediate { + /// Returns a new Point along a [rhumb line] between two existing points. + /// + /// # Examples + /// + /// ```rust + /// # use approx::assert_relative_eq; + /// use geo::RhumbIntermediate; + /// use geo::Point; + /// + /// let p1 = Point::new(10.0, 20.0); + /// let p2 = Point::new(125.0, 25.0); + /// let i20 = p1.rhumb_intermediate(&p2, 0.2); + /// let i50 = p1.rhumb_intermediate(&p2, 0.5); + /// let i80 = p1.rhumb_intermediate(&p2, 0.8); + /// let i20_should = Point::new(32.7, 21.0); + /// let i50_should = Point::new(67.0, 22.5); + /// let i80_should = Point::new(101.7, 24.0); + /// assert_relative_eq!(i20.x(), i20_should.x(), epsilon = 0.2); + /// assert_relative_eq!(i20.y(), i20_should.y(), epsilon = 0.2); + /// assert_relative_eq!(i50.x(), i50_should.x(), epsilon = 0.2); + /// assert_relative_eq!(i50.y(), i50_should.y(), epsilon = 0.2); + /// assert_relative_eq!(i80.x(), i80_should.x(), epsilon = 0.2); + /// assert_relative_eq!(i80.y(), i80_should.y(), epsilon = 0.2); + /// ``` + /// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line + + fn rhumb_intermediate(&self, other: &Point, f: T) -> Point; + fn rhumb_intermediate_fill( + &self, + other: &Point, + max_dist: T, + include_ends: bool, + ) -> Vec>; +} + +impl RhumbIntermediate for Point +where + T: CoordFloat + FromPrimitive, +{ + fn rhumb_intermediate(&self, other: &Point, f: T) -> Point { + let calculations = RhumbCalculations::new(self, other); + calculations.intermediate(f) + } + + fn rhumb_intermediate_fill( + &self, + other: &Point, + max_dist: T, + include_ends: bool, + ) -> Vec> { + let max_delta = max_dist / T::from(MEAN_EARTH_RADIUS).unwrap(); + let calculations = RhumbCalculations::new(self, other); + calculations.intermediate_fill(max_delta, include_ends) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::RhumbIntermediate; + + #[test] + fn f_is_zero_or_one_test() { + let p1 = Point::new(10.0, 20.0); + let p2 = Point::new(15.0, 25.0); + let i0 = p1.rhumb_intermediate(&p2, 0.0); + let i100 = p1.rhumb_intermediate(&p2, 1.0); + assert_relative_eq!(i0.x(), p1.x(), epsilon = 1.0e-6); + assert_relative_eq!(i0.y(), p1.y(), epsilon = 1.0e-6); + assert_relative_eq!(i100.x(), p2.x(), epsilon = 1.0e-6); + assert_relative_eq!(i100.y(), p2.y(), epsilon = 1.0e-6); + } + + #[test] + fn various_f_values_test() { + let p1 = Point::new(10.0, 20.0); + let p2 = Point::new(125.0, 25.0); + let i20 = p1.rhumb_intermediate(&p2, 0.2); + let i50 = p1.rhumb_intermediate(&p2, 0.5); + let i80 = p1.rhumb_intermediate(&p2, 0.8); + let i20_should = Point::new(32.6766, 21.0); + let i50_should = Point::new(66.9801, 22.5); + let i80_should = Point::new(101.6577, 24.0); + assert_relative_eq!(i20.x(), i20_should.x(), epsilon = 0.2); + assert_relative_eq!(i20.y(), i20_should.y(), epsilon = 0.2); + assert_relative_eq!(i50.x(), i50_should.x(), epsilon = 0.2); + assert_relative_eq!(i50.y(), i50_should.y(), epsilon = 0.2); + assert_relative_eq!(i80.x(), i80_should.x(), epsilon = 0.2); + assert_relative_eq!(i80.y(), i80_should.y(), epsilon = 0.2); + } + + #[test] + fn should_be_straight_across_test() { + let p1 = Point::new(0.0, 10.0); + let p2 = Point::new(180.0, 10.0); + let i50 = p1.rhumb_intermediate(&p2, 0.5); + let i50_should = Point::new(90.0, 10.0); + assert_relative_eq!(i50.x(), i50_should.x(), epsilon = 1.0e-6); + assert_relative_eq!(i50.y(), i50_should.y(), epsilon = 1.0e-6); + } + + #[test] + fn should_be_start_end_test() { + let p1 = Point::new(30.0, 40.0); + let p2 = Point::new(40.0, 50.0); + let max_dist = 1500000.0; // meters + let include_ends = true; + let route = p1.rhumb_intermediate_fill(&p2, max_dist, include_ends); + assert_eq!(route, vec![p1, p2]); + } + + #[test] + fn should_add_i50_test() { + let p1 = Point::new(30.0, 40.0); + let p2 = Point::new(40.0, 50.0); + let max_dist = 1000000.0; // meters + let include_ends = true; + let i50 = p1.clone().rhumb_intermediate(&p2, 0.5); + let route = p1.rhumb_intermediate_fill(&p2, max_dist, include_ends); + assert_eq!(route, vec![p1, i50, p2]); + } + + #[test] + fn should_add_i25_i50_i75_test() { + let p1 = Point::new(30.0, 40.0); + let p2 = Point::new(40.0, 50.0); + let max_dist = 400000.0; // meters + let include_ends = true; + let i25 = p1.clone().rhumb_intermediate(&p2, 0.25); + let i50 = p1.clone().rhumb_intermediate(&p2, 0.5); + let i75 = p1.clone().rhumb_intermediate(&p2, 0.75); + let route = p1.rhumb_intermediate_fill(&p2, max_dist, include_ends); + assert_eq!(route, vec![p1, i25, i50, i75, p2]); + } +} diff --git a/geo/src/algorithm/rhumb/length.rs b/geo/src/algorithm/rhumb/length.rs new file mode 100644 index 0000000000..ce544ece42 --- /dev/null +++ b/geo/src/algorithm/rhumb/length.rs @@ -0,0 +1,74 @@ +use num_traits::FromPrimitive; + +use crate::RhumbDistance; +use crate::{CoordFloat, Line, LineString, MultiLineString}; + +/// Determine the length of a geometry assuming each segment is a [rhumb line]. +/// +/// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line +/// +/// *Note*: this implementation uses a mean earth radius of 6371.088 km, based on the [recommendation of +/// the IUGG](ftp://athena.fsv.cvut.cz/ZFG/grs80-Moritz.pdf) +pub trait RhumbLength { + /// Determine the length of a geometry assuming each segment is a [rhumb line]. + /// + /// # Units + /// + /// - return value: meters + /// + /// # Examples + /// + /// ``` + /// use geo::prelude::*; + /// use geo::LineString; + /// + /// let linestring = LineString::::from(vec![ + /// // New York City + /// (-74.006, 40.7128), + /// // London + /// (-0.1278, 51.5074), + /// ]); + /// + /// let length = linestring.rhumb_length(); + /// + /// assert_eq!( + /// 5_794_129., // meters + /// length.round() + /// ); + /// ``` + /// + /// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line + fn rhumb_length(&self) -> T; +} + +impl RhumbLength for Line +where + T: CoordFloat + FromPrimitive, +{ + fn rhumb_length(&self) -> T { + let (start, end) = self.points(); + start.rhumb_distance(&end) + } +} + +impl RhumbLength for LineString +where + T: CoordFloat + FromPrimitive, +{ + fn rhumb_length(&self) -> T { + self.lines().fold(T::zero(), |total_length, line| { + total_length + line.rhumb_length() + }) + } +} + +impl RhumbLength for MultiLineString +where + T: CoordFloat + FromPrimitive, +{ + fn rhumb_length(&self) -> T { + self.0 + .iter() + .fold(T::zero(), |total, line| total + line.rhumb_length()) + } +} diff --git a/geo/src/algorithm/rhumb/mod.rs b/geo/src/algorithm/rhumb/mod.rs new file mode 100644 index 0000000000..7ffe9f8b7d --- /dev/null +++ b/geo/src/algorithm/rhumb/mod.rs @@ -0,0 +1,165 @@ +//! This module provides rhumb-line (a.k.a. loxodrome) geometry operations. +//! The distance, destination, and bearing implementations are adapted in part +//! from their equivalents in [Turf.js](https://turfjs.org/), which in turn are +//! adapted from the Movable Type +//! [spherical geodesy tools](https://www.movable-type.co.uk/scripts/latlong.html). +//! Turf.js is copyright its authors and the geodesy tools are copyright Chris +//! Veness; both are available under an MIT license. + +use crate::{point, utils::normalize_longitude, CoordFloat, Point}; +use num_traits::FromPrimitive; + +mod distance; +pub use distance::RhumbDistance; + +mod bearing; +pub use bearing::RhumbBearing; + +mod destination; +pub use destination::RhumbDestination; + +mod intermediate; +pub use intermediate::RhumbIntermediate; + +mod length; +pub use length::RhumbLength; + +struct RhumbCalculations { + from: Point, + to: Point, + phi1: T, + delta_lambda: T, + delta_phi: T, + delta_psi: T, +} + +impl RhumbCalculations { + fn new(from: &Point, to: &Point) -> Self { + let pi = T::from(std::f64::consts::PI).unwrap(); + let two = T::one() + T::one(); + let four = two + two; + + let phi1 = from.y().to_radians(); + let phi2 = to.y().to_radians(); + let mut delta_lambda = (to.x() - from.x()).to_radians(); + // if delta_lambda is over 180° take shorter rhumb line across the anti-meridian: + if delta_lambda > pi { + delta_lambda = delta_lambda - (two * pi); + } + if delta_lambda < -pi { + delta_lambda = delta_lambda + (two * pi); + } + + let delta_psi = ((phi2 / two + pi / four).tan() / (phi1 / two + pi / four).tan()).ln(); + let delta_phi = phi2 - phi1; + + RhumbCalculations { + from: *from, + to: *to, + phi1, + delta_lambda, + delta_phi, + delta_psi, + } + } + + fn theta(&self) -> T { + self.delta_lambda.atan2(self.delta_psi) + } + + fn delta(&self) -> T { + let threshold = T::from(10.0e-12).unwrap(); + let q = if self.delta_psi > threshold { + self.delta_phi / self.delta_psi + } else { + self.phi1.cos() + }; + + (self.delta_phi * self.delta_phi + q * q * self.delta_lambda * self.delta_lambda).sqrt() + } + + fn intermediate(&self, fraction: T) -> Point { + let delta = fraction * self.delta(); + let theta = self.theta(); + let lambda1 = self.from.x().to_radians(); + calculate_destination(delta, lambda1, self.phi1, theta) + } + + fn intermediate_fill(&self, max_delta: T, include_ends: bool) -> Vec> { + let theta = self.theta(); + let lambda1 = self.from.x().to_radians(); + + let total_delta = self.delta(); + + if total_delta <= max_delta { + return if include_ends { + vec![self.from, self.to] + } else { + vec![] + }; + } + + let number_of_points = (total_delta / max_delta).ceil(); + let interval = T::one() / number_of_points; + + let mut current_step = interval; + let mut points = if include_ends { + vec![self.from] + } else { + vec![] + }; + + while current_step < T::one() { + let delta = total_delta * current_step; + let point = calculate_destination(delta, lambda1, self.phi1, theta); + points.push(point); + current_step = current_step + interval; + } + + if include_ends { + points.push(self.to); + } + + points + } +} + +fn calculate_destination( + delta: T, + lambda1: T, + phi1: T, + theta: T, +) -> Point { + let pi = T::from(std::f64::consts::PI).unwrap(); + let two = T::one() + T::one(); + let four = two + two; + let threshold = T::from(10.0e-12).unwrap(); + + let delta_phi = delta * theta.cos(); + let mut phi2 = phi1 + delta_phi; + + // check for some daft bugger going past the pole, normalise latitude if so + if phi2.abs() > pi / two { + phi2 = if phi2 > T::zero() { + pi - phi2 + } else { + -pi - phi2 + }; + } + + let delta_psi = ((phi2 / two + pi / four).tan() / (phi1 / two + pi / four).tan()).ln(); + // E-W course becomes ill-conditioned with 0/0 + let q = if delta_psi.abs() > threshold { + delta_phi / delta_psi + } else { + phi1.cos() + }; + + let delta_lambda = (delta * theta.sin()) / q; + let lambda2 = lambda1 + delta_lambda; + + point! { + x: normalize_longitude(lambda2.to_degrees()), + y: phi2.to_degrees(), + } +} diff --git a/geo/src/algorithm/rotate.rs b/geo/src/algorithm/rotate.rs index 4d8b5baee0..8aca4ed5bf 100644 --- a/geo/src/algorithm/rotate.rs +++ b/geo/src/algorithm/rotate.rs @@ -9,9 +9,9 @@ use crate::CoordFloat; /// ## Performance /// /// If you will be performing multiple transformations, like [`Scale`](crate::Scale), -/// [`Skew`](crate::Skew), [`Translate`](crate::Translate), or [`Rotate`](crate::Rotate), it is more +/// [`Skew`](crate::Skew), [`Translate`](crate::Translate) or [`Rotate`], it is more /// efficient to compose the transformations and apply them as a single operation using the -/// [`AffineOps`](crate::AffineOps) trait. +/// [`AffineOps`] trait. pub trait Rotate { /// Rotate a geometry around its [centroid](Centroid) by an angle, in degrees /// @@ -78,7 +78,7 @@ pub trait Rotate { /// point!(x: 10.0, y: 0.0), /// ); /// - /// assert_eq!(rotated, line_string![ + /// approx::assert_relative_eq!(rotated, line_string![ /// (x: 2.9289321881345245, y: 7.071067811865475), /// (x: 10.0, y: 7.0710678118654755), /// (x: 17.071067811865476, y: 7.0710678118654755) @@ -197,7 +197,11 @@ mod test { point!(x: 0.6464466094067263, y: 0.8535533905932737), point!(x: 1.353553390593274, y: 1.560660171779821), ]); - assert_relative_eq!(multi_points.rotate_around_center(45.), expected_for_center); + assert_relative_eq!( + multi_points.rotate_around_center(45.), + expected_for_center, + epsilon = 1e-15 + ); } #[test] @@ -357,7 +361,11 @@ mod test { fn test_rotate_around_point_arbitrary() { let p = Point::new(5.0, 10.0); let rotated = p.rotate_around_point(-45., Point::new(10., 34.)); - assert_eq!(rotated, Point::new(-10.506096654409877, 20.564971157455595)); + assert_relative_eq!( + rotated, + Point::new(-10.506096654409877, 20.564971157455595), + epsilon = 1e-14 + ); } #[test] fn test_rotate_line() { diff --git a/geo/src/algorithm/scale.rs b/geo/src/algorithm/scale.rs index 579448b2cf..8b2bdd6547 100644 --- a/geo/src/algorithm/scale.rs +++ b/geo/src/algorithm/scale.rs @@ -4,10 +4,10 @@ use crate::{AffineOps, AffineTransform, BoundingRect, Coord, CoordFloat, CoordNu /// /// ## Performance /// -/// If you will be performing multiple transformations, like [`Scale`](crate::Scale), +/// If you will be performing multiple transformations, like [`Scale`], /// [`Skew`](crate::Skew), [`Translate`](crate::Translate), or [`Rotate`](crate::Rotate), it is more /// efficient to compose the transformations and apply them as a single operation using the -/// [`AffineOps`](crate::AffineOps) trait. +/// [`AffineOps`] trait. pub trait Scale { /// Scale a geometry from it's bounding box center. /// @@ -66,15 +66,15 @@ pub trait Scale { /// /// ``` /// use geo::Scale; - /// use geo::{LineString, line_string}; + /// use geo::{LineString, line_string, Coord}; /// /// let ls: LineString = line_string![(x: 0., y: 0.), (x: 10., y: 10.)]; /// - /// let scaled = ls.scale_xy(2., 4.); + /// let scaled = ls.scale_around_point(2., 4., Coord { x: 100., y: 100. }); /// /// assert_eq!(scaled, line_string![ - /// (x: -5., y: -15.), - /// (x: 15., y: 25.) + /// (x: -100., y: -300.), + /// (x: -80., y: -260.) /// ]); /// ``` #[must_use] diff --git a/geo/src/algorithm/simplify.rs b/geo/src/algorithm/simplify.rs index 80d8852844..1151c76986 100644 --- a/geo/src/algorithm/simplify.rs +++ b/geo/src/algorithm/simplify.rs @@ -299,8 +299,7 @@ where #[cfg(test)] mod test { use super::*; - use crate::geo_types::coord; - use crate::{line_string, polygon}; + use crate::{coord, line_string, polygon}; #[test] fn recursion_test() { diff --git a/geo/src/algorithm/simplify_vw.rs b/geo/src/algorithm/simplify_vw.rs index 69f240a0a6..11d5a1335b 100644 --- a/geo/src/algorithm/simplify_vw.rs +++ b/geo/src/algorithm/simplify_vw.rs @@ -6,49 +6,50 @@ use crate::{ use std::cmp::Ordering; use std::collections::BinaryHeap; +use rstar::primitives::CachedEnvelope; use rstar::{RTree, RTreeNum}; -/// Store triangle information -// current is the candidate point for removal +/// Store triangle information. Area is used for ranking in the priority queue and determining removal #[derive(Debug)] -struct VScore +struct VScore where T: CoordFloat, { left: usize, + /// The current [Point] index in the original [LineString]: The candidate for removal current: usize, right: usize, area: T, - // `visvalingam_preserve` uses `intersector`, `visvalingam` does not - intersector: I, + // `visvalingam_preserve` uses `intersector`, `visvalingam` does not, so it's always false + intersector: bool, } // These impls give us a min-heap -impl Ord for VScore +impl Ord for VScore where T: CoordFloat, { - fn cmp(&self, other: &VScore) -> Ordering { + fn cmp(&self, other: &VScore) -> Ordering { other.area.partial_cmp(&self.area).unwrap() } } -impl PartialOrd for VScore +impl PartialOrd for VScore where T: CoordFloat, { - fn partial_cmp(&self, other: &VScore) -> Option { + fn partial_cmp(&self, other: &VScore) -> Option { Some(self.cmp(other)) } } -impl Eq for VScore where T: CoordFloat {} +impl Eq for VScore where T: CoordFloat {} -impl PartialEq for VScore +impl PartialEq for VScore where T: CoordFloat, { - fn eq(&self, other: &VScore) -> bool + fn eq(&self, other: &VScore) -> bool where T: CoordFloat, { @@ -110,17 +111,18 @@ where current: i + 1, left: i, right: i + 2, - intersector: (), + intersector: false, }) - .collect::>>(); + .collect::>>(); // While there are still points for which the associated triangle // has an area below the epsilon while let Some(smallest) = pq.pop() { - // This triangle's area is above epsilon, so skip it if smallest.area > *epsilon { - continue; + // no need to keep trying: the min-heap ensures that we process triangles in order + // so if we see one that exceeds the tolerance we're done: everything else is too big + break; } - // This triangle's area is below epsilon: eliminate the associated point + // This triangle's area is below epsilon: the associated point is a candidate for removal let (left, right) = adjacent[smallest.current]; // A point in this triangle has been removed since this VScore // was created, so skip it @@ -135,27 +137,9 @@ where adjacent[right as usize] = (left, rr); adjacent[smallest.current] = (0, 0); - // Now recompute the adjacent triangle(s), using left and right adjacent points - let choices = [(ll, left, right), (left, right, rr)]; - for &(ai, current_point, bi) in &choices { - if ai as usize >= max || bi as usize >= max { - // Out of bounds, i.e. we're on one edge - continue; - } - let area = Triangle::new( - orig.0[ai as usize], - orig.0[current_point as usize], - orig.0[bi as usize], - ) - .unsigned_area(); - pq.push(VScore { - area, - current: current_point as usize, - left: ai as usize, - right: bi as usize, - intersector: (), - }); - } + // Recompute the adjacent triangle(s), using left and right adjacent points + // this may add new triangles to the heap + recompute_triangles(&smallest, orig, &mut pq, ll, left, right, rr, max, epsilon); } // Filter out the points that have been deleted, returning remaining point indices orig.0 @@ -166,6 +150,59 @@ where .collect::>() } +/// Recompute adjacent triangle(s) using left and right adjacent points, and push onto heap +/// +/// This is used for both standard and topology-preserving variants. +#[allow(clippy::too_many_arguments)] +fn recompute_triangles( + smallest: &VScore, + orig: &LineString, + pq: &mut BinaryHeap>, + ll: i32, + left: i32, + right: i32, + rr: i32, + max: usize, + epsilon: &T, +) where + T: CoordFloat, +{ + let choices = [(ll, left, right), (left, right, rr)]; + for &(ai, current_point, bi) in &choices { + if ai as usize >= max || bi as usize >= max { + // Out of bounds, i.e. we're on one edge + continue; + } + let area = Triangle::new( + orig.0[ai as usize], + orig.0[current_point as usize], + orig.0[bi as usize], + ) + .unsigned_area(); + + // This logic only applies to VW-Preserve + // smallest.current's removal causes a self-intersection, and this point precedes it + // we ensure it gets removed next by demoting its area to negative epsilon + // we check that current_point is less than smallest.current because + // if it's larger the point in question comes AFTER smallest.current: we only want to remove + // the point that comes BEFORE smallest.current + let area = if smallest.intersector && (current_point as usize) < smallest.current { + -*epsilon + } else { + area + }; + + let v = VScore { + area, + current: current_point as usize, + left: ai as usize, + right: bi as usize, + intersector: false, + }; + pq.push(v) + } +} + // Wrapper for visvalingam_indices, mapping indices back to points fn visvalingam(orig: &LineString, epsilon: &T) -> Vec> where @@ -208,7 +245,7 @@ where { let mut rings = vec![]; // Populate R* tree with exterior and interior samples, if any - let mut tree: RTree> = RTree::bulk_load( + let mut tree: RTree> = RTree::bulk_load( exterior .lines() .chain( @@ -217,6 +254,7 @@ where .flat_map(|ring| *ring) .flat_map(|line_string| line_string.lines()), ) + .map(CachedEnvelope::new) .collect::>(), ); @@ -250,7 +288,7 @@ where fn visvalingam_preserve( orig: &LineString, epsilon: &T, - tree: &mut RTree>, + tree: &mut RTree>>, ) -> Vec> where T: CoordFloat + RTreeNum + HasKernel, @@ -290,13 +328,15 @@ where right: i + 2, intersector: false, }) - .collect::>>(); + .collect::>>(); // While there are still points for which the associated triangle // has an area below the epsilon while let Some(mut smallest) = pq.pop() { if smallest.area > *epsilon { - continue; + // No need to continue: we've already seen all the candidate triangles; + // the min-heap guarantees it + break; } if counter <= INITIAL_MIN { // we can't remove any more points no matter what @@ -310,14 +350,18 @@ where } // if removal of this point causes a self-intersection, we also remove the previous point // that removal alters the geometry, removing the self-intersection - // HOWEVER if we're within 2 points of the absolute minimum, we can't remove this point or the next + // HOWEVER if we're within 1 point of the absolute minimum, we can't remove this point or the next // because we could then no longer form a valid geometry if removal of next also caused an intersection. // The simplification process is thus over. smallest.intersector = tree_intersect(tree, &smallest, &orig.0); if smallest.intersector && counter <= MIN_POINTS { break; } - // We've got a valid triangle, and its area is smaller than epsilon, so + let (ll, _) = adjacent[left as usize]; + let (_, rr) = adjacent[right as usize]; + adjacent[left as usize] = (ll, right); + adjacent[right as usize] = (left, rr); + // We've got a valid triangle, and its area is smaller than the tolerance, so // remove it from the simulated "linked list" adjacent[smallest.current] = (0, 0); counter -= 1; @@ -326,48 +370,17 @@ where let middle_point = Point::from(orig.0[smallest.current]); let right_point = Point::from(orig.0[right as usize]); - let line_1 = Line::new(left_point, middle_point); - let line_2 = Line::new(middle_point, right_point); + let line_1 = CachedEnvelope::new(Line::new(left_point, middle_point)); + let line_2 = CachedEnvelope::new(Line::new(middle_point, right_point)); assert!(tree.remove(&line_1).is_some()); assert!(tree.remove(&line_2).is_some()); // Restore continuous line segment - tree.insert(Line::new(left_point, right_point)); + tree.insert(CachedEnvelope::new(Line::new(left_point, right_point))); - // Now recompute the adjacent triangle(s), using left and right adjacent points - let (ll, _) = adjacent[left as usize]; - let (_, rr) = adjacent[right as usize]; - adjacent[left as usize] = (ll, right); - adjacent[right as usize] = (left, rr); - let choices = [(ll, left, right), (left, right, rr)]; - for &(ai, current_point, bi) in &choices { - if ai as usize >= max || bi as usize >= max { - // Out of bounds, i.e. we're on one edge - continue; - } - let new = Triangle::new( - orig.0[ai as usize], - orig.0[current_point as usize], - orig.0[bi as usize], - ); - // The current point causes a self-intersection, and this point precedes it - // we ensure it gets removed next by demoting its area to negative epsilon - let temp_area = if smallest.intersector && (current_point as usize) < smallest.current { - -*epsilon - } else { - new.unsigned_area() - }; - let new_triangle = VScore { - area: temp_area, - current: current_point as usize, - left: ai as usize, - right: bi as usize, - intersector: false, - }; - - // push re-computed triangle onto heap - pq.push(new_triangle); - } + // Recompute the adjacent triangle(s), using left and right adjacent points + // this may add new triangles to the heap + recompute_triangles(&smallest, orig, &mut pq, ll, left, right, rr, max, epsilon); } // Filter out the points that have been deleted, returning remaining points orig.0 @@ -377,54 +390,49 @@ where .collect() } -/// is p1 -> p2 -> p3 wound counterclockwise? -#[inline] -fn ccw(p1: Point, p2: Point, p3: Point) -> bool -where - T: CoordFloat + HasKernel, -{ - let o = ::Ker::orient2d(p1.into(), p2.into(), p3.into()); - o == Orientation::CounterClockwise -} - -/// checks whether line segments with p1-p4 as their start and endpoints touch or cross -fn cartesian_intersect(p1: Point, p2: Point, p3: Point, p4: Point) -> bool -where - T: CoordFloat + HasKernel, -{ - (ccw(p1, p3, p4) ^ ccw(p2, p3, p4)) & (ccw(p1, p2, p3) ^ ccw(p1, p2, p4)) -} - -/// check whether a triangle's edges intersect with any other edges of the LineString -fn tree_intersect(tree: &RTree>, triangle: &VScore, orig: &[Coord]) -> bool +/// Check whether the new candidate line segment intersects with any existing geometry line segments +/// +/// In order to do this efficiently, the rtree is queried for any existing segments which fall within +/// the bounding box of the new triangle created by the candidate segment +fn tree_intersect( + tree: &RTree>>, + triangle: &VScore, + orig: &[Coord], +) -> bool where T: CoordFloat + RTreeNum + HasKernel, { - let point_a = orig[triangle.left]; - let point_c = orig[triangle.right]; + let new_segment_start = orig[triangle.left]; + let new_segment_end = orig[triangle.right]; + // created by candidate point removal + let new_segment = CachedEnvelope::new(Line::new( + Point::from(orig[triangle.left]), + Point::from(orig[triangle.right]), + )); let bounding_rect = Triangle::new( orig[triangle.left], orig[triangle.current], orig[triangle.right], ) .bounding_rect(); - let br = Point::new(bounding_rect.min().x, bounding_rect.min().y); - let tl = Point::new(bounding_rect.max().x, bounding_rect.max().y); - tree.locate_in_envelope_intersecting(&rstar::AABB::from_corners(br, tl)) - .any(|c| { - // triangle start point, end point - let (ca, cb) = c.points(); - ca.0 != point_a - && ca.0 != point_c - && cb.0 != point_a - && cb.0 != point_c - && cartesian_intersect(ca, cb, Point::from(point_a), Point::from(point_c)) - }) + tree.locate_in_envelope_intersecting(&rstar::AABB::from_corners( + bounding_rect.min().into(), + bounding_rect.max().into(), + )) + .any(|candidate| { + // line start point, end point + let (candidate_start, candidate_end) = candidate.points(); + candidate_start.0 != new_segment_start + && candidate_start.0 != new_segment_end + && candidate_end.0 != new_segment_start + && candidate_end.0 != new_segment_end + && new_segment.intersects(&**candidate) + }) } /// Simplifies a geometry. /// -/// Polygons are simplified by running the algorithm on all their constituent rings. This may +/// Polygons are simplified by running the algorithm on all their constituent rings. This may /// result in invalid Polygons, and has no guarantee of preserving topology. Multi* objects are /// simplified by simplifying all their constituent geometries individually. /// @@ -434,6 +442,9 @@ pub trait SimplifyVw { /// /// See [here](https://bost.ocks.org/mike/simplify/) for a graphical explanation /// + /// # Note + /// The tolerance used to remove a point is `epsilon`, in keeping with GEOS. JTS uses `epsilon ^ 2`. + /// /// # Examples /// /// ``` @@ -503,9 +514,9 @@ pub trait SimplifyVwIdx { T: CoordFloat; } -/// Simplifies a geometry, preserving its topology by removing self-intersections +/// Simplifies a geometry, attempting to preserve its topology by removing self-intersections /// -/// An epsilon less than or equal to zero will return an unaltered version of the geometry. +/// An epsilon less than or equal to zero will return an unaltered version of the geometry pub trait SimplifyVwPreserve { /// Returns the simplified representation of a geometry, using a topology-preserving variant of the /// [Visvalingam-Whyatt](http://www.tandfonline.com/doi/abs/10.1179/000870493786962263) algorithm. @@ -522,12 +533,15 @@ pub trait SimplifyVwPreserve { /// (117.0, 48.0)` and `(117.0, 48.0), (300,0, 40.0)`. By removing it, /// a new triangle with indices `(0, 3, 4)` is formed, which does not cause a self-intersection. /// - /// **Note**: it is possible for the simplification algorithm to displace a Polygon's interior ring outside its shell. + /// # Notes /// - /// **Note**: if removal of a point causes a self-intersection, but the geometry only has `n + 2` - /// points remaining (4 for a `LineString`, 6 for a `Polygon`), the point is retained and the + /// - It is possible for the simplification algorithm to displace a Polygon's interior ring outside its shell. + /// - The algorithm does **not** guarantee a valid output geometry, especially on smaller geometries. + /// - If removal of a point causes a self-intersection, but the geometry only has `n + 1` + /// points remaining (3 for a `LineString`, 5 for a `Polygon`), the point is retained and the /// simplification process ends. This is because there is no guarantee that removal of two points will remove /// the intersection, but removal of further points would leave too few points to form a valid geometry. + /// - The tolerance used to remove a point is `epsilon`, in keeping with GEOS. JTS uses `epsilon ^ 2` /// /// # Examples /// @@ -595,7 +609,8 @@ where { fn simplify_vw_preserve(&self, epsilon: &T) -> Polygon { let mut simplified = - vwp_wrapper::<_, 4, 6>(self.exterior(), Some(self.interiors()), epsilon); + // min_points was formerly 6, but that's too conservative for small polygons + vwp_wrapper::<_, 4, 5>(self.exterior(), Some(self.interiors()), epsilon); let exterior = LineString::from(simplified.remove(0)); let interiors = simplified.into_iter().map(LineString::from).collect(); Polygon::new(exterior, interiors) @@ -669,12 +684,38 @@ where #[cfg(test)] mod test { - use super::{cartesian_intersect, visvalingam, vwp_wrapper, SimplifyVw, SimplifyVwPreserve}; + use super::{visvalingam, vwp_wrapper, SimplifyVw, SimplifyVwPreserve}; use crate::{ - line_string, point, polygon, Coord, LineString, MultiLineString, MultiPolygon, Point, - Polygon, + line_string, polygon, Coord, LineString, MultiLineString, MultiPolygon, Point, Polygon, }; + // See https://github.com/georust/geo/issues/1049 + #[test] + #[should_panic] + fn vwp_bug() { + let pol = polygon![ + (x: 1., y: 4.), + (x: 3., y: 4.), + (x: 1., y: 1.), + (x: 7., y: 0.), + (x: 1., y: 0.), + (x: 0., y: 1.), + (x: 1., y: 4.), + ]; + let simplified = pol.simplify_vw_preserve(&2.25); + assert_eq!( + simplified, + polygon![ + (x: 1., y: 4.), + (x: 3., y: 4.), + (x: 1., y: 1.), + (x: 7., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 4.), + ] + ); + } + #[test] fn visvalingam_test() { // this is the PostGIS example @@ -686,29 +727,13 @@ mod test { (x: 10.0, y: 10.0) ]; - let correct = vec![(5.0, 2.0), (7.0, 25.0), (10.0, 10.0)]; + let correct = [(5.0, 2.0), (7.0, 25.0), (10.0, 10.0)]; let correct_ls: Vec<_> = correct.iter().map(|e| Coord::from((e.0, e.1))).collect(); let simplified = visvalingam(&ls, &30.); assert_eq!(simplified, correct_ls); } #[test] - fn vwp_intersection_test() { - // does the intersection check always work - let a = point!(x: 1., y: 3.); - let b = point!(x: 3., y: 1.); - let c = point!(x: 3., y: 3.); - let d = point!(x: 1., y: 1.); - // cw + ccw - assert!(cartesian_intersect(a, b, c, d)); - // ccw + ccw - assert!(cartesian_intersect(b, a, c, d)); - // cw + cw - assert!(cartesian_intersect(a, b, d, c)); - // ccw + cw - assert!(cartesian_intersect(b, a, d, c)); - } - #[test] fn simple_vwp_test() { // this LineString will have a self-intersection if the point with the // smallest associated area is removed @@ -728,7 +753,7 @@ mod test { ]; let simplified = vwp_wrapper::<_, 2, 4>(&ls, None, &668.6); // this is the correct, non-intersecting LineString - let correct = vec![ + let correct = [ (10., 60.), (126., 31.), (280., 19.), @@ -837,7 +862,7 @@ mod test { #[test] fn multilinestring() { // this is the PostGIS example - let points = vec![ + let points = [ (5.0, 2.0), (3.0, 8.0), (6.0, 20.0), @@ -846,7 +871,7 @@ mod test { ]; let points_ls: Vec<_> = points.iter().map(|e| Point::new(e.0, e.1)).collect(); - let correct = vec![(5.0, 2.0), (7.0, 25.0), (10.0, 10.0)]; + let correct = [(5.0, 2.0), (7.0, 25.0), (10.0, 10.0)]; let correct_ls: Vec<_> = correct.iter().map(|e| Point::new(e.0, e.1)).collect(); let mline = MultiLineString::new(vec![LineString::from(points_ls)]); diff --git a/geo/src/algorithm/skew.rs b/geo/src/algorithm/skew.rs index 5cc2cdbf20..f4427fe93b 100644 --- a/geo/src/algorithm/skew.rs +++ b/geo/src/algorithm/skew.rs @@ -5,9 +5,9 @@ use crate::{AffineOps, AffineTransform, BoundingRect, Coord, CoordFloat, CoordNu /// ## Performance /// /// If you will be performing multiple transformations, like [`Scale`](crate::Scale), -/// [`Skew`](crate::Skew), [`Translate`](crate::Translate), or [`Rotate`](crate::Rotate), it is more +/// [`Skew`], [`Translate`](crate::Translate), or [`Rotate`](crate::Rotate), it is more /// efficient to compose the transformations and apply them as a single operation using the -/// [`AffineOps`](crate::AffineOps) trait. +/// [`AffineOps`] trait. /// pub trait Skew { /// An affine transformation which skews a geometry, sheared by a uniform angle along the x and diff --git a/geo/src/algorithm/sweep/active.rs b/geo/src/algorithm/sweep/active.rs index afd57525ab..f051a79d9a 100644 --- a/geo/src/algorithm/sweep/active.rs +++ b/geo/src/algorithm/sweep/active.rs @@ -1,10 +1,4 @@ -use std::{ - borrow::Borrow, - cmp::Ordering, - collections::BTreeSet, - fmt::Debug, - ops::{Bound, Deref}, -}; +use std::{borrow::Borrow, cmp::Ordering, fmt::Debug, ops::Deref}; /// A segment currently active in the sweep. /// @@ -19,10 +13,10 @@ use std::{ /// compiler?). #[derive(Debug, Clone, Copy, PartialEq)] #[repr(transparent)] -pub(super) struct Active(pub(super) T); +pub(in crate::algorithm) struct Active(pub(in crate::algorithm) T); impl Active { - pub(super) fn active_ref(t: &T) -> &Active { + pub(in crate::algorithm) fn active_ref(t: &T) -> &Active { unsafe { std::mem::transmute(t) } } } @@ -65,7 +59,7 @@ impl PartialOrd for Active { } /// Trait abstracting a container of active segments. -pub(super) trait ActiveSet: Default { +pub(in crate::algorithm) trait ActiveSet: Default { type Seg; fn previous_find) -> bool>( &self, @@ -86,41 +80,3 @@ pub(super) trait ActiveSet: Default { fn insert_active(&mut self, segment: Self::Seg); fn remove_active(&mut self, segment: &Self::Seg); } - -impl ActiveSet for BTreeSet> { - type Seg = T; - - fn previous_find) -> bool>( - &self, - segment: &Self::Seg, - mut f: F, - ) -> Option<&Active> { - self.range::, _>(( - Bound::Unbounded, - Bound::Excluded(Active::active_ref(segment)), - )) - .rev() - .find(|&a| f(a)) - } - fn next_find) -> bool>( - &self, - segment: &Self::Seg, - mut f: F, - ) -> Option<&Active> { - self.range::, _>(( - Bound::Excluded(Active::active_ref(segment)), - Bound::Unbounded, - )) - .find(|&a| f(a)) - } - - fn insert_active(&mut self, segment: Self::Seg) { - let result = self.insert(Active(segment)); - debug_assert!(result); - } - - fn remove_active(&mut self, segment: &Self::Seg) { - let result = self.remove(Active::active_ref(segment)); - debug_assert!(result); - } -} diff --git a/geo/src/algorithm/sweep/im_segment.rs b/geo/src/algorithm/sweep/im_segment.rs index bbe0f84302..a8cb05421f 100644 --- a/geo/src/algorithm/sweep/im_segment.rs +++ b/geo/src/algorithm/sweep/im_segment.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, cmp::Ordering, fmt::Debug, rc::Rc}; +use std::{borrow::Cow, cell::RefCell, cmp::Ordering, fmt::Debug, rc::Rc}; use super::*; @@ -145,7 +145,7 @@ impl IMSegment { let segment_geom = RefCell::borrow(&segment.inner).geom; let mut child = RefCell::borrow(&parent.inner).overlapping.as_ref().cloned(); - let mut tgt = segment.clone(); + let mut tgt = Cow::Borrowed(&segment); while let Some(child_seg) = child { let child_inner_seg = RefCell::borrow(&child_seg.inner); @@ -162,7 +162,7 @@ impl IMSegment { RefCell::borrow_mut(&new_segment.inner).is_overlapping = true; } - tgt = new_segment; + tgt = Cow::Owned(new_segment); child = child_overlapping.as_ref().cloned(); } } diff --git a/geo/src/algorithm/sweep/iter.rs b/geo/src/algorithm/sweep/iter.rs index e0f24a98b1..70a8bd2508 100644 --- a/geo/src/algorithm/sweep/iter.rs +++ b/geo/src/algorithm/sweep/iter.rs @@ -362,7 +362,7 @@ pub(super) mod tests { fn overlap_intersect() { init_log(); - let input = vec![ + let input = [ Line::from([(0., 0.), (1., 1.)]), [(1., 0.), (0., 1.)].into(), [(0., 0.5), (1., 0.5)].into(), diff --git a/geo/src/algorithm/sweep/line_or_point.rs b/geo/src/algorithm/sweep/line_or_point.rs index 7f09cf9abf..7bd26f935e 100644 --- a/geo/src/algorithm/sweep/line_or_point.rs +++ b/geo/src/algorithm/sweep/line_or_point.rs @@ -3,6 +3,7 @@ use std::{cmp::Ordering, ops::Deref}; use super::SweepPoint; use crate::{ line_intersection::line_intersection, Coord, GeoFloat, GeoNum, Kernel, Line, LineIntersection, + Orientation, }; /// Either a line segment or a point. @@ -11,38 +12,42 @@ use crate::{ /// segment must have distinct points (use the `Point` variant if the /// coordinates are the equal). #[derive(Clone, Copy)] -pub struct LineOrPoint { - left: SweepPoint, - right: SweepPoint, +pub enum LineOrPoint { + Point(SweepPoint), + Line { + left: SweepPoint, + right: SweepPoint, + }, } impl std::fmt::Debug for LineOrPoint { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple(if self.is_line() { "LPt" } else { "Pt" }) - .field(&self.left.x_y()) - .field(&self.right.x_y()) - .finish() + match self { + LineOrPoint::Point(p) => f.debug_tuple("Pt").field(&p.x_y()).finish(), + LineOrPoint::Line { left, right } => f + .debug_tuple("LPt") + .field(&left.x_y()) + .field(&right.x_y()) + .finish(), + } } } impl From> for LineOrPoint { fn from(pt: SweepPoint) -> Self { - Self { - left: pt, - right: pt, - } + Self::Point(pt) } } impl From<(SweepPoint, SweepPoint)> for LineOrPoint { - fn from(pt: (SweepPoint, SweepPoint)) -> Self { - let (start, end) = pt; + fn from((start, end): (SweepPoint, SweepPoint)) -> Self { match start.cmp(&end) { - Ordering::Less => Self { + Ordering::Less => Self::Line { left: start, right: end, }, - _ => Self { + Ordering::Equal => Self::Point(start), + Ordering::Greater => Self::Line { left: end, right: start, }, @@ -62,10 +67,7 @@ impl From> for LineOrPoint { /// Convert from a [`Coord`] impl From> for LineOrPoint { fn from(c: Coord) -> Self { - Self { - left: c.into(), - right: c.into(), - } + Self::Point(c.into()) } } @@ -73,23 +75,32 @@ impl LineOrPoint { /// Checks if the variant is a line. #[inline] pub fn is_line(&self) -> bool { - self.left != self.right + matches!(self, Self::Line { .. }) } /// Return a [`Line`] representation of self. #[inline] pub fn line(&self) -> Line { - Line::new(*self.left, *self.right) + match self { + LineOrPoint::Point(p) => Line::new(**p, **p), + LineOrPoint::Line { left, right } => Line::new(**left, **right), + } } #[inline] pub fn left(&self) -> SweepPoint { - self.left + match self { + LineOrPoint::Point(p) => *p, + LineOrPoint::Line { left, .. } => *left, + } } #[inline] pub fn right(&self) -> SweepPoint { - self.right + match self { + LineOrPoint::Point(p) => *p, + LineOrPoint::Line { right, .. } => *right, + } } #[cfg(test)] @@ -99,11 +110,26 @@ impl LineOrPoint { #[inline] pub fn end_points(&self) -> (SweepPoint, SweepPoint) { - (self.left, self.right) + match self { + LineOrPoint::Point(p) => (*p, *p), + LineOrPoint::Line { left, right } => (*left, *right), + } } pub fn new(left: SweepPoint, right: SweepPoint) -> Self { - Self { left, right } + if left == right { + Self::Point(left) + } else { + Self::Line { left, right } + } + } + + pub fn orient2d(&self, other: Coord) -> Orientation { + let (left, right) = match self { + LineOrPoint::Point(p) => (**p, **p), + LineOrPoint::Line { left, right } => (**left, **right), + }; + T::Ker::orient2d(left, right, other) } } @@ -126,9 +152,9 @@ impl PartialEq for LineOrPoint { /// centered at its coordinates. impl PartialOrd for LineOrPoint { fn partial_cmp(&self, other: &Self) -> Option { - match (self.is_line(), other.is_line()) { - (false, false) => { - if self.left == other.left { + match (self, other) { + (LineOrPoint::Point(p), LineOrPoint::Point(o)) => { + if p == o { Some(Ordering::Equal) } else { // Unequal points do not satisfy pre-condition and @@ -136,35 +162,44 @@ impl PartialOrd for LineOrPoint { None } } - (false, true) => other.partial_cmp(self).map(Ordering::reverse), - (true, false) => { - let (p, q) = self.end_points(); - let r = other.left; - if r > q || p > r { + (LineOrPoint::Point(_), LineOrPoint::Line { .. }) => { + other.partial_cmp(self).map(Ordering::reverse) + } + (LineOrPoint::Line { left, right }, LineOrPoint::Point(p)) => { + if p > right || left > p { return None; } Some( - T::Ker::orient2d(*p, *q, *r) + T::Ker::orient2d(**left, **right, **p) .as_ordering() .then(Ordering::Greater), ) } - (true, true) => { - let (p1, q1) = self.end_points(); - let (p2, q2) = other.end_points(); - if p1 > p2 { + ( + LineOrPoint::Line { + left: left_a, + right: right_a, + }, + LineOrPoint::Line { + left: left_b, + right: right_b, + }, + ) => { + if left_a > left_b { return other.partial_cmp(self).map(Ordering::reverse); } - if p1 >= q2 || p2 >= q1 { + if left_a >= right_b || left_b >= right_a { return None; } // Assertion: p1 <= p2 // Assertion: pi < q_j Some( - T::Ker::orient2d(*p1, *q1, *p2) + T::Ker::orient2d(**left_a, **right_a, **left_b) .as_ordering() - .then_with(|| T::Ker::orient2d(*p1, *q1, *q2).as_ordering()), + .then_with(|| { + T::Ker::orient2d(**left_a, **right_a, **right_b).as_ordering() + }), ) } } @@ -179,39 +214,41 @@ impl LineOrPoint { debug_assert!(other.is_line(), "tried to intersect with a point variant!"); let line = other.line(); - if !self.is_line() { - let p = self.left; - use crate::Intersects; - if line.intersects(&*p) { - Some(*self) - } else { - None + match self { + LineOrPoint::Point(p) => { + use crate::Intersects; + if line.intersects(&**p) { + Some(*self) + } else { + None + } } - } else { - line_intersection(self.line(), line).map(|l| match l { - LineIntersection::SinglePoint { - intersection, - is_proper, - } => { - let mut pt = intersection; - if is_proper && (&pt == self.left.deref()) { - if self.left.x == self.right.x { - pt.y = pt.y.next_after(T::infinity()); - } else { - pt.x = pt.x.next_after(T::infinity()); + LineOrPoint::Line { left, right } => { + line_intersection(self.line(), line).map(|l| match l { + LineIntersection::SinglePoint { + intersection, + is_proper, + } => { + let mut pt = intersection; + if is_proper && (&pt == left.deref()) { + if left.x == right.x { + pt.y = pt.y.next_after(T::infinity()); + } else { + pt.x = pt.x.next_after(T::infinity()); + } } + pt.into() } - pt.into() - } - LineIntersection::Collinear { intersection } => intersection.into(), - }) + LineIntersection::Collinear { intersection } => intersection.into(), + }) + } } } pub fn intersect_line_ordered(&self, other: &Self) -> Option { let ord = self.partial_cmp(other); match self.intersect_line(other) { - Some(lp) if !lp.is_line() => { + Some(Self::Point(p)) => { // NOTE: A key issue with using non-exact numbers (f64, etc.) in // this algo. is that line-intersection may return // counter-intuitive points. @@ -238,38 +275,34 @@ impl LineOrPoint { // specifically for this algo., that can track the neighbors of // tree-nodes, and fix / report this issue. The crate // `btree-slab` seems like a great starting point. - let pt = lp.left; - let (mut x, y) = pt.x_y(); + let (mut x, y) = p.x_y(); - let c = self.left; + let c = self.left(); if x == c.x && y < c.y { x = x.next_after(T::infinity()); } - let pt: SweepPoint<_> = Coord { x, y }.into(); + let p = Coord { x, y }.into(); debug_assert!( - pt >= self.left, - "line intersection before first line: {pt:?}\n\tLine({lp1:?} - {lp2:?}) X Line({lp3:?} - {lp4:?})", - lp1 = self.left, - lp2 = self.right, - lp3 = other.left, - lp4 = other.right, + p >= self.left(), + "line intersection before first line: {p:?}\n\tLine({lp1:?} - {lp2:?}) X Line({lp3:?} - {lp4:?})", + lp1 = self.left(), + lp2 = self.right(), + lp3 = other.left(), + lp4 = other.right(), ); debug_assert!( - pt >= other.left, - "line intersection before second line: {pt:?}\n\tLine({lp1:?} - {lp2:?}) X Line({lp3:?} - {lp4:?})", - lp1 = self.left, - lp2 = self.right, - lp3 = other.left, - lp4 = other.right, + p >= other.left(), + "line intersection before second line: {p:?}\n\tLine({lp1:?} - {lp2:?}) X Line({lp3:?} - {lp4:?})", + lp1 = self.left(), + lp2 = self.right(), + lp3 = other.left(), + lp4 = other.right(), ); if let Some(ord) = ord { - let l1 = LineOrPoint::from((self.left, pt)); - let l2 = LineOrPoint { - left: other.left, - right: pt, - }; + let l1 = LineOrPoint::from((self.left(), p)); + let l2 = LineOrPoint::from((other.left(), p)); let cmp = l1.partial_cmp(&l2).unwrap(); if l1.is_line() && l2.is_line() && cmp.then(ord) != ord { debug!( @@ -278,18 +311,18 @@ impl LineOrPoint { l2 = other ); debug!("\tparts: {l1:?}, {l2:?}"); - debug!("\tintersection: {pt:?} {cmp:?}"); + debug!("\tintersection: {p:?} {cmp:?}"); // RM: This is a complicated intersection that is changing the ordering. // Heuristic: approximate with a trivial intersection point that preserves the topology. - return Some(if self.left > other.left { - self.left.into() + return Some(if self.left() > other.left() { + self.left().into() } else { - other.left.into() + other.left().into() }); } } - Some((*pt).into()) + Some(Self::Point(p)) } e => e, } diff --git a/geo/src/algorithm/sweep/mod.rs b/geo/src/algorithm/sweep/mod.rs index 820eeda033..d8786c64ae 100644 --- a/geo/src/algorithm/sweep/mod.rs +++ b/geo/src/algorithm/sweep/mod.rs @@ -14,7 +14,7 @@ mod segment; use segment::{Segment, SplitSegments}; mod active; -use active::{Active, ActiveSet}; +pub(super) use active::{Active, ActiveSet}; mod im_segment; use im_segment::IMSegment; diff --git a/geo/src/algorithm/sweep/vec_set.rs b/geo/src/algorithm/sweep/vec_set.rs index 49b3c49862..c79da697f9 100644 --- a/geo/src/algorithm/sweep/vec_set.rs +++ b/geo/src/algorithm/sweep/vec_set.rs @@ -16,6 +16,13 @@ impl Default for VecSet { } impl VecSet> { + pub fn partition_point

(&self, mut pred: P) -> usize + where + P: FnMut(&T) -> bool, + { + self.data.partition_point(|s| pred(&s.0)) + } + pub fn index_of(&self, segment: &T) -> usize { self.data .binary_search(Active::active_ref(segment)) @@ -31,7 +38,6 @@ impl VecSet> { self.data.len() } - #[allow(unused)] pub fn insert_at(&mut self, idx: usize, segment: T) { self.data.insert(idx, Active(segment)) } diff --git a/geo/src/algorithm/translate.rs b/geo/src/algorithm/translate.rs index 2ec9cf40ef..51a7208d81 100644 --- a/geo/src/algorithm/translate.rs +++ b/geo/src/algorithm/translate.rs @@ -6,9 +6,9 @@ pub trait Translate { /// ## Performance /// /// If you will be performing multiple transformations, like [`Scale`](crate::Scale), - /// [`Skew`](crate::Skew), [`Translate`](crate::Translate), or [`Rotate`](crate::Rotate), it is more + /// [`Skew`](crate::Skew), [`Translate`], or [`Rotate`](crate::Rotate), it is more /// efficient to compose the transformations and apply them as a single operation using the - /// [`AffineOps`](crate::AffineOps) trait. + /// [`AffineOps`] trait. /// /// # Examples /// diff --git a/geo/src/algorithm/triangulate_spade.rs b/geo/src/algorithm/triangulate_spade.rs new file mode 100644 index 0000000000..fb38ff4f19 --- /dev/null +++ b/geo/src/algorithm/triangulate_spade.rs @@ -0,0 +1,782 @@ +use geo_types::{Coord, Line, Point, Triangle}; +use spade::{ + ConstrainedDelaunayTriangulation, DelaunayTriangulation, Point2, SpadeNum, Triangulation, +}; + +use crate::{ + line_intersection::line_intersection, CoordsIter, EuclideanDistance, GeoFloat, + LineIntersection, LinesIter, +}; +use crate::{Centroid, Contains}; + +// ======== Config ============ + +/// Collection of parameters that influence the precision of the algorithm in some sense (see +/// explanations on fields of this struct) +/// +/// This implements the `Default` trait and you can just use it most of the time +#[derive(Debug, Clone)] +pub struct SpadeTriangulationConfig { + /// Coordinates within this radius are snapped to the same position. For any two `Coords` there's + /// no real way to influence the decision when choosing the snapper and the snappee + snap_radius: T, +} + +impl Default for SpadeTriangulationConfig +where + T: SpadeTriangulationFloat, +{ + fn default() -> Self { + Self { + snap_radius: >::from(0.000_1), + } + } +} + +// ====== Error ======== + +#[derive(Debug)] +pub enum TriangulationError { + SpadeError(spade::InsertionError), + LoopTrap, + ConstraintFailure, +} + +impl std::fmt::Display for TriangulationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for TriangulationError {} + +pub type TriangulationResult = Result; + +// ======= Float trait ======== + +pub trait SpadeTriangulationFloat: GeoFloat + SpadeNum {} +impl SpadeTriangulationFloat for T {} + +// ======= Triangulation trait ========= + +pub type Triangles = Vec>; + +// seal the trait that needs to be implemented for TriangulateSpade to be implemented. This is done +// so that we don't leak these weird methods on the public interface. +mod private { + use super::*; + pub trait TriangulationRequirementTrait<'a, T> + where + T: SpadeTriangulationFloat, + { + /// collect all the lines that are relevant for triangulations from the geometric object that + /// should be triangulated. + /// + /// intersecting lines are allowed + fn lines(&'a self) -> Vec>; + /// collect all the coords that are relevant for triangulations from the geometric object that + /// should be triangulated + fn coords(&'a self) -> Vec>; + /// define a predicate that decides if a point is inside of the object (used for constrained triangulation) + fn contains_point(&'a self, p: Point) -> bool; + + // processing of the lines that prepare the lines for triangulation. + // + // `spade` has the general limitation that constraint lines cannot intersect or else it + // will panic. This is why we need to manually split up the lines into smaller parts at the + // intersection point + // + // there's also a preprocessing step which tries to minimize the risk of failure of the algo + // through edge cases (thin/flat triangles are prevented as much as possible & lines are deduped, ...) + fn cleanup_lines(lines: Vec>, snap_radius: T) -> TriangulationResult>> { + let (known_coords, lines) = preprocess_lines(lines, snap_radius); + prepare_intersection_contraint(lines, known_coords, snap_radius) + } + } +} + +/// Triangulate polygons using a [Delaunay +/// Triangulation](https://en.wikipedia.org/wiki/Delaunay_triangulation) +/// +/// This trait contains both constrained and unconstrained triangulation methods. To read more +/// about the differences of these methods also consult [this +/// page](https://en.wikipedia.org/wiki/Constrained_Delaunay_triangulation) +pub trait TriangulateSpade<'a, T>: private::TriangulationRequirementTrait<'a, T> +where + T: SpadeTriangulationFloat, +{ + /// returns a triangulation that's solely based on the points of the geometric object + /// + /// The triangulation is guaranteed to be Delaunay + /// + /// Note that the lines of the triangulation don't necessarily follow the lines of the input + /// geometry. If you wish to achieve that take a look at the `constrained_triangulation` and the + /// `constrained_outer_triangulation` functions. + /// + /// ```rust + /// use geo::TriangulateSpade; + /// use geo::{Polygon, LineString, Coord}; + /// let u_shape = Polygon::new( + /// LineString::new(vec![ + /// Coord { x: 0.0, y: 0.0 }, + /// Coord { x: 1.0, y: 0.0 }, + /// Coord { x: 1.0, y: 1.0 }, + /// Coord { x: 2.0, y: 1.0 }, + /// Coord { x: 2.0, y: 0.0 }, + /// Coord { x: 3.0, y: 0.0 }, + /// Coord { x: 3.0, y: 3.0 }, + /// Coord { x: 0.0, y: 3.0 }, + /// ]), + /// vec![], + /// ); + /// let unconstrained_triangulation = u_shape.unconstrained_triangulation().unwrap(); + /// let num_triangles = unconstrained_triangulation.len(); + /// assert_eq!(num_triangles, 8); + /// ``` + /// + fn unconstrained_triangulation(&'a self) -> TriangulationResult> { + let points = self.coords(); + points + .into_iter() + .map(to_spade_point) + .try_fold(DelaunayTriangulation::>::new(), |mut tris, p| { + tris.insert(p).map_err(TriangulationError::SpadeError)?; + Ok(tris) + }) + .map(triangulation_to_triangles) + } + + /// returns triangulation that's based on the points of the geometric object and also + /// incorporates the lines of the input geometry + /// + /// The triangulation is not guaranteed to be Delaunay because of the constraint lines + /// + /// This outer triangulation also contains triangles that are not included in the input + /// geometry if it wasn't convex. Here's an example: + /// + /// ```text + /// ┌──────────────────┐ + /// │\ __/│ + /// │ \ __/ / │ + /// │ \ __/ / │ + /// │ \ __/ / │ + /// │ \/ / │ + /// │ ┌──────┐ │ + /// │ /│\:::::│\ │ + /// │ / │:\::::│ \ │ + /// │ / │::\:::│ \ │ + /// │ / │:::\::│ \ │ + /// │/ │::::\:│ \│ + /// └─────┘______└─────┘ + /// ``` + /// + /// ```rust + /// use geo::TriangulateSpade; + /// use geo::{Polygon, LineString, Coord}; + /// let u_shape = Polygon::new( + /// LineString::new(vec![ + /// Coord { x: 0.0, y: 0.0 }, + /// Coord { x: 1.0, y: 0.0 }, + /// Coord { x: 1.0, y: 1.0 }, + /// Coord { x: 2.0, y: 1.0 }, + /// Coord { x: 2.0, y: 0.0 }, + /// Coord { x: 3.0, y: 0.0 }, + /// Coord { x: 3.0, y: 3.0 }, + /// Coord { x: 0.0, y: 3.0 }, + /// ]), + /// vec![], + /// ); + /// // we use the default [`SpadeTriangulationConfig`] here + /// let constrained_outer_triangulation = + /// u_shape.constrained_outer_triangulation(Default::default()).unwrap(); + /// let num_triangles = constrained_outer_triangulation.len(); + /// assert_eq!(num_triangles, 8); + /// ``` + /// + /// The outer triangulation of the top down U-shape contains extra triangles marked + /// with ":". If you want to exclude those, take a look at `constrained_triangulation` + fn constrained_outer_triangulation( + &'a self, + config: SpadeTriangulationConfig, + ) -> TriangulationResult> { + let lines = self.lines(); + let lines = Self::cleanup_lines(lines, config.snap_radius)?; + lines + .into_iter() + .map(to_spade_line) + .try_fold( + ConstrainedDelaunayTriangulation::>::new(), + |mut cdt, [start, end]| { + let start = cdt.insert(start).map_err(TriangulationError::SpadeError)?; + let end = cdt.insert(end).map_err(TriangulationError::SpadeError)?; + // safety check (to prevent panic) whether we can add the line + if !cdt.can_add_constraint(start, end) { + return Err(TriangulationError::ConstraintFailure); + } + cdt.add_constraint(start, end); + Ok(cdt) + }, + ) + .map(triangulation_to_triangles) + } + + /// returns triangulation that's based on the points of the geometric object and also + /// incorporates the lines of the input geometry + /// + /// The triangulation is not guaranteed to be Delaunay because of the constraint lines + /// + /// This triangulation only contains triangles that are included in the input geometry. + /// Here's an example: + /// + /// ```text + /// ┌──────────────────┐ + /// │\ __/│ + /// │ \ __/ / │ + /// │ \ __/ / │ + /// │ \ __/ / │ + /// │ \/ / │ + /// │ ┌──────┐ │ + /// │ /│ │\ │ + /// │ / │ │ \ │ + /// │ / │ │ \ │ + /// │ / │ │ \ │ + /// │/ │ │ \│ + /// └─────┘ └─────┘ + /// ``` + /// + /// ```rust + /// use geo::TriangulateSpade; + /// use geo::{Polygon, LineString, Coord}; + /// let u_shape = Polygon::new( + /// LineString::new(vec![ + /// Coord { x: 0.0, y: 0.0 }, + /// Coord { x: 1.0, y: 0.0 }, + /// Coord { x: 1.0, y: 1.0 }, + /// Coord { x: 2.0, y: 1.0 }, + /// Coord { x: 2.0, y: 0.0 }, + /// Coord { x: 3.0, y: 0.0 }, + /// Coord { x: 3.0, y: 3.0 }, + /// Coord { x: 0.0, y: 3.0 }, + /// ]), + /// vec![], + /// ); + /// // we use the default [`SpadeTriangulationConfig`] here + /// let constrained_triangulation = u_shape.constrained_triangulation(Default::default()).unwrap(); + /// let num_triangles = constrained_triangulation.len(); + /// assert_eq!(num_triangles, 6); + /// ``` + /// + /// Compared to the `constrained_outer_triangulation` it only includes the triangles + /// inside of the input geometry + fn constrained_triangulation( + &'a self, + config: SpadeTriangulationConfig, + ) -> TriangulationResult> { + self.constrained_outer_triangulation(config) + .map(|triangles| { + triangles + .into_iter() + .filter(|triangle| { + let center = triangle.centroid(); + self.contains_point(center) + }) + .collect::>() + }) + } +} + +/// conversion from spade triangulation back to geo triangles +fn triangulation_to_triangles(triangulation: T) -> Triangles +where + T: Triangulation>, + F: SpadeTriangulationFloat, +{ + triangulation + .inner_faces() + .map(|face| face.positions()) + .map(|points| points.map(|p| Coord:: { x: p.x, y: p.y })) + .map(Triangle::from) + .collect::>() +} + +// ========== Triangulation trait impls ============ + +// everything that satisfies the requirement methods automatically implements the triangulation +impl<'a, T, G> TriangulateSpade<'a, T> for G +where + T: SpadeTriangulationFloat, + G: private::TriangulationRequirementTrait<'a, T>, +{ +} + +impl<'a, 'l, T, G> private::TriangulationRequirementTrait<'a, T> for G +where + 'a: 'l, + T: SpadeTriangulationFloat, + G: LinesIter<'l, Scalar = T> + CoordsIter + Contains>, +{ + fn coords(&'a self) -> Vec> { + self.coords_iter().collect::>() + } + + fn lines(&'a self) -> Vec> { + self.lines_iter().collect() + } + + fn contains_point(&'a self, p: Point) -> bool { + self.contains(&p) + } +} + +// it would be cool to impl the trait for GS: AsRef<[G]> but I wasn't able to get this to compile +// (yet) + +impl<'a, T, G> private::TriangulationRequirementTrait<'a, T> for Vec +where + T: SpadeTriangulationFloat, + G: TriangulateSpade<'a, T>, +{ + fn coords(&'a self) -> Vec> { + self.iter().flat_map(|g| g.coords()).collect::>() + } + + fn lines(&'a self) -> Vec> { + self.iter().flat_map(|g| g.lines()).collect::>() + } + + fn contains_point(&'a self, p: Point) -> bool { + self.iter().any(|g| g.contains_point(p)) + } +} + +impl<'a, T, G> private::TriangulationRequirementTrait<'a, T> for &[G] +where + T: SpadeTriangulationFloat, + G: TriangulateSpade<'a, T>, +{ + fn coords(&'a self) -> Vec> { + self.iter().flat_map(|g| g.coords()).collect::>() + } + + fn lines(&'a self) -> Vec> { + self.iter().flat_map(|g| g.lines()).collect::>() + } + + fn contains_point(&'a self, p: Point) -> bool { + self.iter().any(|g| g.contains_point(p)) + } +} + +// ========== Triangulation trait impl helpers ============ + +fn prepare_intersection_contraint( + mut lines: Vec>, + mut known_points: Vec>, + snap_radius: T, +) -> Result>, TriangulationError> { + // Rule 2 of "Power of 10" rules (NASA) + // safety net. We can't prove that the `while let` loop isn't going to run infinitely, so + // we abort after a fixed amount of iterations. In case that the iteration seems to loop + // indefinitely this check will return an Error indicating the infinite loop. + let mut loop_count = 1000; + let mut loop_check = || { + loop_count -= 1; + (loop_count != 0) + .then_some(()) + .ok_or(TriangulationError::LoopTrap) + }; + + while let Some((indices, intersection)) = { + let mut iter = iter_line_pairs(&lines); + iter.find_map(find_intersecting_lines_fn) + } { + loop_check()?; + let [l0, l1] = remove_lines_by_index(indices, &mut lines); + let new_lines = split_lines([l0, l1], intersection); + let new_lines = cleanup_filter_lines(new_lines, &lines, &mut known_points, snap_radius); + + lines.extend(new_lines); + } + + Ok(lines) +} + +/// iterates over all combinations (a,b) of lines in a vector where a != b +fn iter_line_pairs( + lines: &[Line], +) -> impl Iterator); 2]> { + lines.iter().enumerate().flat_map(|(idx0, line0)| { + lines + .iter() + .enumerate() + .filter(move |(idx1, line1)| *idx1 != idx0 && line0 != *line1) + .map(move |(idx1, line1)| [(idx0, line0), (idx1, line1)]) + }) +} + +/// checks whether two lines are intersecting and if so, checks the intersection to not be ill +/// formed +/// +/// returns +/// - [usize;2] : sorted indexes of lines, smaller one comes first +/// - intersection : type of intersection +fn find_intersecting_lines_fn( + [(idx0, line0), (idx1, line1)]: [(usize, &Line); 2], +) -> Option<([usize; 2], LineIntersection)> { + line_intersection(*line0, *line1) + .filter(|intersection| { + match intersection { + // intersection is not located in both lines + LineIntersection::SinglePoint { is_proper, .. } if !is_proper => false, + // collinear intersection is length zero line + LineIntersection::Collinear { intersection } + if intersection.start == intersection.end => + { + false + } + _ => true, + } + }) + .map(|intersection| ([idx0, idx1], intersection)) +} + +/// removes two lines by index in a safe way since the second index can be invalidated after +/// the first line was removed (remember `.remove(idx)` returns the element and shifts the tail +/// of the vector in direction of its start to close the gap) +fn remove_lines_by_index( + mut indices: [usize; 2], + lines: &mut Vec>, +) -> [Line; 2] { + indices.sort(); + let [idx0, idx1] = indices; + let l1 = lines.remove(idx1); + let l0 = lines.remove(idx0); + [l0, l1] +} + +/// split lines based on intersection kind: +/// +/// - intersection point: create 4 new lines from the existing line's endpoints to the intersection +/// point +/// - collinear: create 3 new lines (before overlap, overlap, after overlap) +fn split_lines( + [l0, l1]: [Line; 2], + intersection: LineIntersection, +) -> Vec> { + match intersection { + LineIntersection::SinglePoint { intersection, .. } => [ + (l0.start, intersection), + (l0.end, intersection), + (l1.start, intersection), + (l1.end, intersection), + ] + .map(|(a, b)| Line::new(a, b)) + .to_vec(), + LineIntersection::Collinear { .. } => { + let mut points = [l0.start, l0.end, l1.start, l1.end]; + // sort points by their coordinate values to resolve ambiguities + points.sort_by(|a, b| { + a.x.partial_cmp(&b.x) + .expect("sorting points by coordinate x failed") + .then_with(|| { + a.y.partial_cmp(&b.y) + .expect("sorting points by coordinate y failed") + }) + }); + // since all points are on one line we can just create new lines from consecutive + // points after sorting + points + .windows(2) + .map(|win| Line::new(win[0], win[1])) + .collect::>() + } + } +} + +/// new lines from the `split_lines` function may contain a variety of ill formed lines, this +/// function cleans all of these cases up +fn cleanup_filter_lines( + lines_need_check: Vec>, + existing_lines: &[Line], + known_points: &mut Vec>, + snap_radius: T, +) -> Vec> { + lines_need_check + .into_iter() + .map(|mut line| { + line.start = snap_or_register_point(line.start, known_points, snap_radius); + line.end = snap_or_register_point(line.end, known_points, snap_radius); + line + }) + .filter(|l| l.start != l.end) + .filter(|l| !existing_lines.contains(l)) + .filter(|l| !existing_lines.contains(&Line::new(l.end, l.start))) + .collect::>() +} + +/// snap point to the nearest existing point if it's close enough +/// +/// snap_radius can be configured via the third parameter of this function +fn snap_or_register_point( + point: Coord, + known_points: &mut Vec>, + snap_radius: T, +) -> Coord { + known_points + .iter() + // find closest + .min_by(|a, b| { + a.euclidean_distance(&point) + .partial_cmp(&b.euclidean_distance(&point)) + .expect("Couldn't compare coordinate distances") + }) + // only snap if closest is within epsilone range + .filter(|nearest_point| nearest_point.euclidean_distance(&point) < snap_radius) + .cloned() + // otherwise register and use input point + .unwrap_or_else(|| { + known_points.push(point); + point + }) +} + +/// preprocesses lines so that we're less likely to hit issues when using the spade triangulation +fn preprocess_lines( + lines: Vec>, + snap_radius: T, +) -> (Vec>, Vec>) { + let mut known_coords: Vec> = vec![]; + let capacity = lines.len(); + let lines = lines + .into_iter() + .fold(Vec::with_capacity(capacity), |mut lines, mut line| { + // deduplicating: + + // 1. snap coords of lines to existing coords + line.start = snap_or_register_point(line.start, &mut known_coords, snap_radius); + line.end = snap_or_register_point(line.end, &mut known_coords, snap_radius); + if + // 2. make sure line isn't degenerate (no length when start == end) + line.start != line.end + // 3. make sure line or flipped line wasn't already added + && !lines.contains(&line) + && !lines.contains(&Line::new(line.end, line.start)) + { + lines.push(line) + } + + lines + }); + (known_coords, lines) +} + +/// converts Line to something somewhat similar in the spade world +fn to_spade_line(line: Line) -> [Point2; 2] { + [to_spade_point(line.start), to_spade_point(line.end)] +} + +/// converts Coord to something somewhat similar in the spade world +fn to_spade_point(coord: Coord) -> Point2 { + Point2::new(coord.x, coord.y) +} + +#[cfg(test)] +mod spade_triangulation { + use super::*; + use geo_types::*; + + fn assert_num_triangles( + triangulation: &TriangulationResult>, + num: usize, + ) { + assert_eq!( + triangulation + .as_ref() + .map(|tris| tris.len()) + .expect("triangulation success"), + num + ) + } + + #[test] + fn basic_triangle_triangulates() { + let triangulation = Triangle::new( + Coord { x: 0.0, y: 0.0 }, + Coord { x: 1.0, y: 0.0 }, + Coord { x: 0.0, y: 1.0 }, + ) + .unconstrained_triangulation(); + + assert_num_triangles(&triangulation, 1); + } + + #[test] + fn basic_rectangle_triangulates() { + let triangulation = Rect::new(Coord { x: 0.0, y: 0.0 }, Coord { x: 1.0, y: 1.0 }) + .unconstrained_triangulation(); + + assert_num_triangles(&triangulation, 2); + } + + #[test] + fn basic_polygon_triangulates() { + let triangulation = Polygon::new( + LineString::new(vec![ + Coord { x: 0.0, y: 1.0 }, + Coord { x: -1.0, y: 0.0 }, + Coord { x: -0.5, y: -1.0 }, + Coord { x: 0.5, y: -1.0 }, + Coord { x: 1.0, y: 0.0 }, + ]), + vec![], + ) + .unconstrained_triangulation(); + + assert_num_triangles(&triangulation, 3); + } + + #[test] + fn overlapping_triangles_triangulate_unconstrained() { + let triangles = vec![ + Triangle::new( + Coord { x: 0.0, y: 0.0 }, + Coord { x: 2.0, y: 0.0 }, + Coord { x: 0.0, y: 2.0 }, + ), + Triangle::new( + Coord { x: 1.0, y: 1.0 }, + Coord { x: -1.0, y: 1.0 }, + Coord { x: 1.0, y: -1.0 }, + ), + ]; + + let unconstrained_triangulation = triangles.unconstrained_triangulation(); + assert_num_triangles(&unconstrained_triangulation, 4); + } + + #[test] + fn overlapping_triangles_triangulate_constrained_outer() { + let triangles = vec![ + Triangle::new( + Coord { x: 0.0, y: 0.0 }, + Coord { x: 2.0, y: 0.0 }, + Coord { x: 0.0, y: 2.0 }, + ), + Triangle::new( + Coord { x: 1.0, y: 1.0 }, + Coord { x: -1.0, y: 1.0 }, + Coord { x: 1.0, y: -1.0 }, + ), + ]; + + let constrained_outer_triangulation = + triangles.constrained_outer_triangulation(Default::default()); + assert_num_triangles(&constrained_outer_triangulation, 8); + } + + #[test] + fn overlapping_triangles_triangulate_constrained() { + let triangles = vec![ + Triangle::new( + Coord { x: 0.0, y: 0.0 }, + Coord { x: 2.0, y: 0.0 }, + Coord { x: 0.0, y: 2.0 }, + ), + Triangle::new( + Coord { x: 1.0, y: 1.0 }, + Coord { x: -1.0, y: 1.0 }, + Coord { x: 1.0, y: -1.0 }, + ), + ]; + + let constrained_outer_triangulation = + triangles.constrained_triangulation(Default::default()); + assert_num_triangles(&constrained_outer_triangulation, 6); + } + + #[test] + fn u_shaped_polygon_triangulates_unconstrained() { + let u_shape = Polygon::new( + LineString::new(vec![ + Coord { x: 0.0, y: 0.0 }, + Coord { x: 1.0, y: 0.0 }, + Coord { x: 1.0, y: 1.0 }, + Coord { x: 2.0, y: 1.0 }, + Coord { x: 2.0, y: 0.0 }, + Coord { x: 3.0, y: 0.0 }, + Coord { x: 3.0, y: 3.0 }, + Coord { x: 0.0, y: 3.0 }, + ]), + vec![], + ); + + let unconstrained_triangulation = u_shape.unconstrained_triangulation(); + assert_num_triangles(&unconstrained_triangulation, 8); + } + + #[test] + fn u_shaped_polygon_triangulates_constrained_outer() { + let u_shape = Polygon::new( + LineString::new(vec![ + Coord { x: 0.0, y: 0.0 }, + Coord { x: 1.0, y: 0.0 }, + Coord { x: 1.0, y: 1.0 }, + Coord { x: 2.0, y: 1.0 }, + Coord { x: 2.0, y: 0.0 }, + Coord { x: 3.0, y: 0.0 }, + Coord { x: 3.0, y: 3.0 }, + Coord { x: 0.0, y: 3.0 }, + ]), + vec![], + ); + + let constrained_outer_triangulation = + u_shape.constrained_outer_triangulation(Default::default()); + assert_num_triangles(&constrained_outer_triangulation, 8); + } + + #[test] + fn u_shaped_polygon_triangulates_constrained_inner() { + let u_shape = Polygon::new( + LineString::new(vec![ + Coord { x: 0.0, y: 0.0 }, + Coord { x: 1.0, y: 0.0 }, + Coord { x: 1.0, y: 1.0 }, + Coord { x: 2.0, y: 1.0 }, + Coord { x: 2.0, y: 0.0 }, + Coord { x: 3.0, y: 0.0 }, + Coord { x: 3.0, y: 3.0 }, + Coord { x: 0.0, y: 3.0 }, + ]), + vec![], + ); + + let constrained_triangulation = u_shape.constrained_triangulation(Default::default()); + assert_num_triangles(&constrained_triangulation, 6); + } + + #[test] + fn various_snap_radius_works() { + let u_shape = Polygon::new( + LineString::new(vec![ + Coord { x: 0.0, y: 0.0 }, + Coord { x: 1.0, y: 0.0 }, + Coord { x: 1.0, y: 1.0 }, + Coord { x: 2.0, y: 1.0 }, + Coord { x: 2.0, y: 0.0 }, + Coord { x: 3.0, y: 0.0 }, + Coord { x: 3.0, y: 3.0 }, + Coord { x: 0.0, y: 3.0 }, + ]), + vec![], + ); + + for snap_with in (1..6).map(|pow| 0.1_f64.powi(pow)) { + let constrained_triangulation = + u_shape.constrained_triangulation(SpadeTriangulationConfig { + snap_radius: snap_with, + }); + assert_num_triangles(&constrained_triangulation, 6); + } + } +} diff --git a/geo/src/algorithm/vector_ops.rs b/geo/src/algorithm/vector_ops.rs new file mode 100644 index 0000000000..1a23c66e73 --- /dev/null +++ b/geo/src/algorithm/vector_ops.rs @@ -0,0 +1,389 @@ +//! This module defines the [Vector2DOps] trait and implements it for the +//! [Coord] struct. + +use crate::{Coord, CoordFloat, CoordNum}; + +/// Defines vector operations for 2D coordinate types which implement CoordFloat +/// +/// This trait is intended for internal use within the geo crate as a way to +/// bring together the various hand-crafted linear algebra operations used +/// throughout other algorithms and attached to various structs. +pub trait Vector2DOps +where + Self: Sized, +{ + type Scalar: CoordNum; + + /// The euclidean distance between this coordinate and the origin + /// + /// `sqrt(x² + y²)` + /// + fn magnitude(self) -> Self::Scalar; + + /// The squared distance between this coordinate and the origin. + /// (Avoids the square root calculation when it is not needed) + /// + /// `x² + y²` + /// + fn magnitude_squared(self) -> Self::Scalar; + + /// Rotate this coordinate around the origin by 90 degrees clockwise. + /// + /// `a.left() => (-a.y, a.x)` + /// + /// Assumes a coordinate system where positive `y` is up and positive `x` is + /// to the right. The described rotation direction is consistent with the + /// documentation for [crate::algorithm::rotate::Rotate]. + fn left(self) -> Self; + + /// Rotate this coordinate around the origin by 90 degrees anti-clockwise. + /// + /// `a.right() => (a.y, -a.x)` + /// + /// Assumes a coordinate system where positive `y` is up and positive `x` is + /// to the right. The described rotation direction is consistent with the + /// documentation for [crate::algorithm::rotate::Rotate]. + fn right(self) -> Self; + + /// The inner product of the coordinate components + /// + /// `a · b = a.x * b.x + a.y * b.y` + /// + fn dot_product(self, other: Rhs) -> Self::Scalar; + + /// The calculates the `wedge product` between two vectors. + /// + /// `a ∧ b = a.x * b.y - a.y * b.x` + /// + /// Also known as: + /// + /// - `exterior product` + /// - because the wedge product comes from 'Exterior Algebra' + /// - `perpendicular product` + /// - because it is equivalent to `a.dot(b.right())` + /// - `2D cross product` + /// - because it is equivalent to the signed magnitude of the + /// conventional 3D cross product assuming `z` ordinates are zero + /// - `determinant` + /// - because it is equivalent to the `determinant` of the 2x2 matrix + /// formed by the column-vector inputs. + /// + /// ## Examples + /// + /// The following list highlights some examples in geo which might be + /// brought together to use this function: + /// + /// 1. [geo_types::Point::cross_prod()] is already defined on + /// [geo_types::Point]... but that it seems to be some other + /// operation on 3 points?? + /// 2. [geo_types::Line] struct also has a [geo_types::Line::determinant()] + /// function which is the same as `line.start.wedge_product(line.end)` + /// 3. The [crate::algorithm::Kernel::orient2d()] trait default + /// implementation uses cross product to compute orientation. It returns + /// an enum, not the numeric value which is needed for line segment + /// intersection. + /// + /// ## Properties + /// + /// - The absolute value of the cross product is the area of the + /// parallelogram formed by the operands + /// - Anti-commutative: The sign of the output is reversed if the operands + /// are reversed + /// - If the operands are colinear with the origin, the value is zero + /// - The sign can be used to check if the operands are clockwise with + /// respect to the origin, or phrased differently: + /// "is a to the left of the line between the origin and b"? + /// - If this is what you are using it for, then please use + /// [crate::algorithm::Kernel::orient2d()] instead as this is more + /// explicit and has a `RobustKernel` option for extra precision. + fn wedge_product(self, other: Rhs) -> Self::Scalar; + + /// Try to find a vector of unit length in the same direction as this + /// vector. + /// + /// Returns `None` if the result is not finite. This can happen when + /// + /// - the vector is really small (or zero length) and the `.magnitude()` + /// calculation has rounded-down to `0.0` + /// - the vector is really large and the `.magnitude()` has rounded-up + /// or 'overflowed' to `f64::INFINITY` + /// - Either x or y are `f64::NAN` or `f64::INFINITY` + fn try_normalize(self) -> Option; + + /// Returns true if both the x and y components are finite + // Annotation to disable bad clippy lint; It is not good to use + // `&self` as clippy suggests since Coord is Copy + #[allow(clippy::wrong_self_convention)] + fn is_finite(self) -> bool; +} + +impl Vector2DOps for Coord +where + T: CoordFloat, +{ + type Scalar = T; + + fn wedge_product(self, other: Coord) -> Self::Scalar { + self.x * other.y - self.y * other.x + } + + fn dot_product(self, other: Self) -> Self::Scalar { + self.x * other.x + self.y * other.y + } + + fn magnitude(self) -> Self::Scalar { + // Note uses cmath::hypot which avoids 'undue overflow and underflow' + // This also increases the range of values for which `.try_normalize()` works + Self::Scalar::hypot(self.x, self.y) + } + + fn magnitude_squared(self) -> Self::Scalar { + self.x * self.x + self.y * self.y + } + + fn left(self) -> Self { + Self { + x: -self.y, + y: self.x, + } + } + + fn right(self) -> Self { + Self { + x: self.y, + y: -self.x, + } + } + + fn try_normalize(self) -> Option { + let magnitude = self.magnitude(); + let result = self / magnitude; + // Both the result AND the magnitude must be finite they are finite + // Otherwise very large vectors overflow magnitude to Infinity, + // and the after the division the result would be coord!{x:0.0,y:0.0} + // Note we don't need to check if magnitude is zero, because after the division + // that would have made result non-finite or NaN anyway. + if result.is_finite() && magnitude.is_finite() { + Some(result) + } else { + None + } + } + + fn is_finite(self) -> bool { + self.x.is_finite() && self.y.is_finite() + } +} + +#[cfg(test)] +mod test { + use super::Vector2DOps; + use crate::coord; + + #[test] + fn test_cross_product() { + // perpendicular unit length + let a = coord! { x: 1f64, y: 0f64 }; + let b = coord! { x: 0f64, y: 1f64 }; + + // expect the area of parallelogram + assert_eq!(a.wedge_product(b), 1f64); + // expect swapping will result in negative + assert_eq!(b.wedge_product(a), -1f64); + + // Add skew; expect results should be the same + let a = coord! { x: 1f64, y: 0f64 }; + let b = coord! { x: 1f64, y: 1f64 }; + + // expect the area of parallelogram + assert_eq!(a.wedge_product(b), 1f64); + // expect swapping will result in negative + assert_eq!(b.wedge_product(a), -1f64); + + // Make Colinear; expect zero + let a = coord! { x: 2f64, y: 2f64 }; + let b = coord! { x: 1f64, y: 1f64 }; + assert_eq!(a.wedge_product(b), 0f64); + } + + #[test] + fn test_dot_product() { + // perpendicular unit length + let a = coord! { x: 1f64, y: 0f64 }; + let b = coord! { x: 0f64, y: 1f64 }; + // expect zero for perpendicular + assert_eq!(a.dot_product(b), 0f64); + + // Parallel, same direction + let a = coord! { x: 1f64, y: 0f64 }; + let b = coord! { x: 2f64, y: 0f64 }; + // expect +ive product of magnitudes + assert_eq!(a.dot_product(b), 2f64); + // expect swapping will have same result + assert_eq!(b.dot_product(a), 2f64); + + // Parallel, opposite direction + let a = coord! { x: 3f64, y: 4f64 }; + let b = coord! { x: -3f64, y: -4f64 }; + // expect -ive product of magnitudes + assert_eq!(a.dot_product(b), -25f64); + // expect swapping will have same result + assert_eq!(b.dot_product(a), -25f64); + } + + #[test] + fn test_magnitude() { + let a = coord! { x: 1f64, y: 0f64 }; + assert_eq!(a.magnitude(), 1f64); + + let a = coord! { x: 0f64, y: 0f64 }; + assert_eq!(a.magnitude(), 0f64); + + let a = coord! { x: -3f64, y: 4f64 }; + assert_eq!(a.magnitude(), 5f64); + } + + #[test] + fn test_magnitude_squared() { + let a = coord! { x: 1f64, y: 0f64 }; + assert_eq!(a.magnitude_squared(), 1f64); + + let a = coord! { x: 0f64, y: 0f64 }; + assert_eq!(a.magnitude_squared(), 0f64); + + let a = coord! { x: -3f64, y: 4f64 }; + assert_eq!(a.magnitude_squared(), 25f64); + } + + #[test] + fn test_left_right() { + let a = coord! { x: 1f64, y: 0f64 }; + let a_left = coord! { x: 0f64, y: 1f64 }; + let a_right = coord! { x: 0f64, y: -1f64 }; + + assert_eq!(a.left(), a_left); + assert_eq!(a.right(), a_right); + assert_eq!(a.left(), -a.right()); + } + + #[test] + fn test_left_right_match_rotate() { + use crate::algorithm::rotate::Rotate; + use crate::Point; + // The aim of this test is to confirm that wording in documentation is + // consistent. + + // when the user is in a coordinate system where the y axis is flipped + // (eg screen coordinates in a HTML canvas), then rotation directions + // will be different to those described in the documentation. + + // The documentation for the Rotate trait says: 'Positive angles are + // counter-clockwise, and negative angles are clockwise rotations' + + let counter_clockwise_rotation_degrees = 90.0; + let clockwise_rotation_degrees = -counter_clockwise_rotation_degrees; + + let a: Point = coord! { x: 1.0, y: 0.0 }.into(); + let origin: Point = coord! { x: 0.0, y: 0.0 }.into(); + + // left is anti-clockwise + assert_relative_eq!( + Point::from(a.0.left()), + a.rotate_around_point(counter_clockwise_rotation_degrees, origin), + ); + // right is clockwise + assert_relative_eq!( + Point::from(a.0.right()), + a.rotate_around_point(clockwise_rotation_degrees, origin), + ); + } + + #[test] + fn test_try_normalize() { + // Already Normalized + let a = coord! { + x: 1.0, + y: 0.0 + }; + assert_relative_eq!(a.try_normalize().unwrap(), a); + + // Already Normalized + let a = coord! { + x: 1.0 / f64::sqrt(2.0), + y: -1.0 / f64::sqrt(2.0) + }; + assert_relative_eq!(a.try_normalize().unwrap(), a); + + // Non trivial example + let a = coord! { x: -10.0, y: 8.0 }; + assert_relative_eq!( + a.try_normalize().unwrap(), + coord! { x: -10.0, y: 8.0 } / f64::sqrt(10.0 * 10.0 + 8.0 * 8.0) + ); + } + + #[test] + /// Tests edge cases that were previously returning None + /// before switching to cmath::hypot + fn test_try_normalize_edge_cases_1() { + use float_next_after::NextAfter; + // Very Small Input still returns a value thanks to cmath::hypot + let a = coord! { + x: 0.0, + y: 1e-301_f64 + }; + assert_eq!( + a.try_normalize(), + Some(coord! { + x: 0.0, + y: 1.0, + }) + ); + + // A large vector where try_normalize returns Some + // Because the magnitude is f64::MAX (Just before overflow to f64::INFINITY) + let a = coord! { + x: f64::sqrt(f64::MAX/2.0), + y: f64::sqrt(f64::MAX/2.0) + }; + assert_relative_eq!( + a.try_normalize().unwrap(), + coord! { + x: 1.0 / f64::sqrt(2.0), + y: 1.0 / f64::sqrt(2.0), + } + ); + + // A large vector where try_normalize still returns Some because we are using cmath::hypot + // even though the magnitude would be just above f64::MAX + let a = coord! { + x: f64::sqrt(f64::MAX / 2.0), + y: f64::sqrt(f64::MAX / 2.0).next_after(f64::INFINITY) + }; + assert_relative_eq!( + a.try_normalize().unwrap(), + coord! { + x: 1.0 / f64::sqrt(2.0), + y: 1.0 / f64::sqrt(2.0), + } + ); + } + + #[test] + fn test_try_normalize_edge_cases_2() { + // The following tests demonstrate some of the floating point + // edge cases that can cause try_normalize to return None. + + // Zero vector - Normalize returns None + let a = coord! { x: 0.0, y: 0.0 }; + assert_eq!(a.try_normalize(), None); + + // Where one of the components is NaN try_normalize returns None + let a = coord! { x: f64::NAN, y: 0.0 }; + assert_eq!(a.try_normalize(), None); + + // Where one of the components is Infinite try_normalize returns None + let a = coord! { x: f64::INFINITY, y: 0.0 }; + assert_eq!(a.try_normalize(), None); + } +} diff --git a/geo/src/lib.rs b/geo/src/lib.rs index 5ee23830f9..6606c41346 100644 --- a/geo/src/lib.rs +++ b/geo/src/lib.rs @@ -33,70 +33,76 @@ //! //! ## Area //! -//! - **[`Area`](Area)**: Calculate the planar area of a geometry -//! - **[`ChamberlainDuquetteArea`](ChamberlainDuquetteArea)**: Calculate the geodesic area of a geometry on a sphere using the algorithm presented in _Some Algorithms for Polygons on a Sphere_ by Chamberlain and Duquette (2007) -//! - **[`GeodesicArea`](GeodesicArea)**: Calculate the geodesic area and perimeter of a geometry on an ellipsoid using the algorithm presented in _Algorithms for geodesics_ by Charles Karney (2013) +//! - **[`Area`]**: Calculate the planar area of a geometry +//! - **[`ChamberlainDuquetteArea`]**: Calculate the geodesic area of a geometry on a sphere using the algorithm presented in _Some Algorithms for Polygons on a Sphere_ by Chamberlain and Duquette (2007) +//! - **[`GeodesicArea`]**: Calculate the geodesic area and perimeter of a geometry on an ellipsoid using the algorithm presented in _Algorithms for geodesics_ by Charles Karney (2013) //! //! ## Boolean Operations //! -//! - **[`BooleanOps`](BooleanOps)**: combine or split (Multi)Polygons using intersecton, union, xor, or difference operations +//! - **[`BooleanOps`]**: combine or split (Multi)Polygons using intersecton, union, xor, or difference operations //! //! ## Distance //! -//! - **[`EuclideanDistance`](EuclideanDistance)**: Calculate the minimum euclidean distance between geometries -//! - **[`GeodesicDistance`](GeodesicDistance)**: Calculate the minimum geodesic distance between geometries using the algorithm presented in _Algorithms for geodesics_ by Charles Karney (2013) -//! - **[`HaversineDistance`](HaversineDistance)**: Calculate the minimum geodesic distance between geometries using the haversine formula -//! - **[`VincentyDistance`](VincentyDistance)**: Calculate the minimum geodesic distance between geometries using Vincenty’s formula +//! - **[`EuclideanDistance`]**: Calculate the minimum euclidean distance between geometries +//! - **[`GeodesicDistance`]**: Calculate the minimum geodesic distance between geometries using the algorithm presented in _Algorithms for geodesics_ by Charles Karney (2013) +//! - **[`HausdorffDistance`]**: Calculate "the maximum of the distances from a point in any of the sets to the nearest point in the other set." (Rote, 1991) +//! - **[`HaversineDistance`]**: Calculate the minimum geodesic distance between geometries using the haversine formula +//! - **[`RhumbDistance`]**: Calculate the length of a rhumb line connecting the two geometries +//! - **[`VincentyDistance`]**: Calculate the minimum geodesic distance between geometries using Vincenty’s formula //! //! ## Length //! -//! - **[`EuclideanLength`](EuclideanLength)**: Calculate the euclidean length of a geometry -//! - **[`GeodesicLength`](GeodesicLength)**: Calculate the geodesic length of a geometry using the algorithm presented in _Algorithms for geodesics_ by Charles Karney (2013) -//! - **[`HaversineLength`](HaversineLength)**: Calculate the geodesic length of a geometry using the haversine formula -//! - **[`VincentyLength`](VincentyLength)**: Calculate the geodesic length of a geometry using Vincenty’s formula +//! - **[`EuclideanLength`]**: Calculate the euclidean length of a geometry +//! - **[`GeodesicLength`]**: Calculate the geodesic length of a geometry using the algorithm presented in _Algorithms for geodesics_ by Charles Karney (2013) +//! - **[`HaversineLength`]**: Calculate the geodesic length of a geometry using the haversine formula +//! - **[`RhumbLength`]**: Calculate the length of a geometry assuming it's composed of rhumb lines +//! - **[`VincentyLength`]**: Calculate the geodesic length of a geometry using Vincenty’s formula //! //! ## Outlier Detection //! -//! - **[`OutlierDetection`](OutlierDetection)**: Detect outliers in a group of points using [LOF](https://en.wikipedia.org/wiki/Local_outlier_factor) +//! - **[`OutlierDetection`]**: Detect outliers in a group of points using [LOF](https://en.wikipedia.org/wiki/Local_outlier_factor) //! //! ## Simplification //! -//! - **[`Simplify`](Simplify)**: Simplify a geometry using the Ramer–Douglas–Peucker algorithm -//! - **[`SimplifyIdx`](SimplifyIdx)**: Calculate a simplified geometry using the Ramer–Douglas–Peucker algorithm, returning coordinate indices -//! - **[`SimplifyVw`](SimplifyVw)**: Simplify a geometry using the Visvalingam-Whyatt algorithm -//! - **[`SimplifyVwPreserve`](SimplifyVwPreserve)**: Simplify a geometry using a topology-preserving variant of the Visvalingam-Whyatt algorithm -//! - **[`SimplifyVwIdx`](SimplifyVwIdx)**: Calculate a simplified geometry using a topology-preserving variant of the Visvalingam-Whyatt algorithm, returning coordinate indices +//! - **[`Simplify`]**: Simplify a geometry using the Ramer–Douglas–Peucker algorithm +//! - **[`SimplifyIdx`]**: Calculate a simplified geometry using the Ramer–Douglas–Peucker algorithm, returning coordinate indices +//! - **[`SimplifyVw`]**: Simplify a geometry using the Visvalingam-Whyatt algorithm +//! - **[`SimplifyVwPreserve`]**: Simplify a geometry using a topology-preserving variant of the Visvalingam-Whyatt algorithm +//! - **[`SimplifyVwIdx`]**: Calculate a simplified geometry using the Visvalingam-Whyatt algorithm, returning coordinate indices //! //! ## Query //! //! - **[`HaversineBearing`]**: Calculate the bearing between points using great circle calculations. -//! - **[`GeodesicBearing`](GeodesicBearing)**: Calculate the bearing between points on a [geodesic](https://en.wikipedia.org/wiki/Geodesics_on_an_ellipsoid) -//! - **[`ClosestPoint`](ClosestPoint)**: Find the point on a geometry +//! - **[`GeodesicBearing`]**: Calculate the bearing between points on a [geodesic](https://en.wikipedia.org/wiki/Geodesics_on_an_ellipsoid) +//! - **[`RhumbBearing`]**: Calculate the angle from north of the rhumb line connecting two points. +//! - **[`ClosestPoint`]**: Find the point on a geometry //! closest to a given point -//! - **[`IsConvex`](IsConvex)**: Calculate the convexity of a +//! - **[`HaversineClosestPoint`]**: Find the point on a geometry +//! closest to a given point on a sphere using spherical coordinates and lines being great arcs. +//! - **[`IsConvex`]**: Calculate the convexity of a //! [`LineString`] -//! - **[`LineInterpolatePoint`](LineInterpolatePoint)**: +//! - **[`LineInterpolatePoint`]**: //! Generates a point that lies a given fraction along the line -//! - **[`LineLocatePoint`](LineLocatePoint)**: Calculate the +//! - **[`LineLocatePoint`]**: Calculate the //! fraction of a line’s total length representing the location of the closest point on the //! line to the given point //! //! ## Similarity //! -//! - **[`FrechetDistance`](FrechetDistance)**: Calculate the similarity between [`LineString`]s using the Fréchet distance +//! - **[`FrechetDistance`]**: Calculate the similarity between [`LineString`]s using the Fréchet distance //! //! ## Topology //! -//! - **[`Contains`](Contains)**: Calculate if a geometry contains another +//! - **[`Contains`]**: Calculate if a geometry contains another //! geometry -//! - **[`CoordinatePosition`](CoordinatePosition)**: Calculate +//! - **[`CoordinatePosition`]**: Calculate //! the position of a coordinate relative to a geometry -//! - **[`HasDimensions`](HasDimensions)**: Determine the dimensions of a geometry -//! - **[`Intersects`](Intersects)**: Calculate if a geometry intersects +//! - **[`HasDimensions`]**: Determine the dimensions of a geometry +//! - **[`Intersects`]**: Calculate if a geometry intersects //! another geometry -//! - **[`line_intersection`](line_intersection::line_intersection)**: Calculates the +//! - **[`line_intersection`]**: Calculates the //! intersection, if any, between two lines. -//! - **[`Relate`](Relate)**: Topologically relate two geometries based on +//! - **[`Relate`]**: Topologically relate two geometries based on //! [DE-9IM](https://en.wikipedia.org/wiki/DE-9IM) semantics. //! - **[`Within`]**: Calculate if a geometry lies completely within another geometry. //! @@ -106,8 +112,8 @@ //! //! ## Winding //! -//! - **[`Orient`](Orient)**: Apply a specified winding [`Direction`](orient::Direction) to a [`Polygon`]’s interior and exterior rings -//! - **[`Winding`](Winding)**: Calculate and manipulate the [`WindingOrder`](winding_order::WindingOrder) of a [`LineString`] +//! - **[`Orient`]**: Apply a specified winding [`Direction`](orient::Direction) to a [`Polygon`]’s interior and exterior rings +//! - **[`Winding`]**: Calculate and manipulate the [`WindingOrder`](winding_order::WindingOrder) of a [`LineString`] //! //! ## Iteration //! @@ -120,42 +126,48 @@ //! //! ## Boundary //! -//! - **[`BoundingRect`](BoundingRect)**: Calculate the axis-aligned +//! - **[`BoundingRect`]**: Calculate the axis-aligned //! bounding rectangle of a geometry -//! - **[`MinimumRotatedRect`](MinimumRotatedRect)**: Calculate the +//! - **[`MinimumRotatedRect`]**: Calculate the //! minimum bounding box of a geometry -//! - **[`ConcaveHull`](ConcaveHull)**: Calculate the concave hull of a +//! - **[`ConcaveHull`]**: Calculate the concave hull of a //! geometry -//! - **[`ConvexHull`](ConvexHull)**: Calculate the convex hull of a +//! - **[`ConvexHull`]**: Calculate the convex hull of a //! geometry -//! - **[`Extremes`](Extremes)**: Calculate the extreme coordinates and +//! - **[`Extremes`]**: Calculate the extreme coordinates and //! indices of a geometry //! //! ## Affine transformations //! -//! - **[`Rotate`](Rotate)**: Rotate a geometry around its centroid -//! - **[`Scale`](Scale)**: Scale a geometry up or down by a factor -//! - **[`Skew`](Skew)**: Skew a geometry by shearing angles along the `x` and `y` dimension -//! - **[`Translate`](Translate)**: Translate a geometry along its axis -//! - **[`AffineOps`](AffineOps)**: generalised composable affine operations +//! - **[`Rotate`]**: Rotate a geometry around its centroid +//! - **[`Scale`]**: Scale a geometry up or down by a factor +//! - **[`Skew`]**: Skew a geometry by shearing angles along the `x` and `y` dimension +//! - **[`Translate`]**: Translate a geometry along its axis +//! - **[`AffineOps`]**: generalised composable affine operations //! //! ## Conversion //! //! - **[`Convert`]**: Convert (infalliby) the type of a geometry’s coordinate value //! - **[`TryConvert`]**: Convert (falliby) the type of a geometry’s coordinate value +//! - **[`ToDegrees`]**: Radians to degrees coordinate transforms for a given geometry. +//! - **[`ToRadians`]**: Degrees to radians coordinate transforms for a given geometry. //! //! ## Miscellaneous //! -//! - **[`Centroid`](Centroid)**: Calculate the centroid of a geometry -//! - **[`GeodesicDestination`](GeodesicDestination)**: Given a start point, bearing, and distance, calculate the destination point on a [geodesic](https://en.wikipedia.org/wiki/Geodesics_on_an_ellipsoid) -//! - **[`GeodesicIntermediate`](GeodesicIntermediate)**: Calculate intermediate points on a [geodesic](https://en.wikipedia.org/wiki/Geodesics_on_an_ellipsoid) -//! - **[`HaversineDestination`]**: Given a start point, bearing, and distance, calculate the destination point on a sphere -//! - **[`HaversineIntermediate`](HaversineIntermediate)**: Calculate intermediate points on a sphere -//! - **[`proj`](proj)**: Project geometries with the `proj` crate (requires the `use-proj` feature) -//! - **[`ChaikinSmoothing`](ChaikinSmoothing)**: Smoothen `LineString`, `Polygon`, `MultiLineString` and `MultiPolygon` using Chaikins algorithm. -//! - **[`Densify`](Densify)**: Densify linear geometry components by interpolating points -//! - **[`Transform`](Transform)**: Transform a geometry using Proj. -//! - **[`RemoveRepeatedPoints`](RemoveRepeatedPoints)**: Remove repeated points from a geometry. +//! - **[`Centroid`]**: Calculate the centroid of a geometry +//! - **[`ChaikinSmoothing`]**: Smoothen `LineString`, `Polygon`, `MultiLineString` and `MultiPolygon` using Chaikin's algorithm. +//! - **[`Densify`]**: Densify linear geometry components by interpolating points +//! - **[`DensifyHaversine`]**: Densify spherical geometry by interpolating points on a sphere +//! - **[`GeodesicDestination`]**: Given a start point, bearing, and distance, calculate the destination point on a [geodesic](https://en.wikipedia.org/wiki/Geodesics_on_an_ellipsoid) +//! - **[`GeodesicIntermediate`]**: Calculate intermediate points on a [geodesic](https://en.wikipedia.org/wiki/Geodesics_on_an_ellipsoid) +//! - **[`HaversineDestination`]**: Given a start point, bearing, and distance, calculate the destination point on a sphere assuming travel on a great circle +//! - **[`HaversineIntermediate`]**: Calculate intermediate points on a sphere along a great-circle line +//! - **[`RhumbDestination`]**: Given a start point, bearing, and distance, calculate the destination point on a sphere assuming travel along a rhumb line +//! - **[`RhumbIntermediate`]**: Calculate intermediate points on a sphere along a rhumb line +//! - **[`proj`]**: Project geometries with the `proj` crate (requires the `use-proj` feature) +//! - **[`LineStringSegmentize`]**: Segment a LineString into `n` segments. +//! - **[`Transform`]**: Transform a geometry using Proj. +//! - **[`RemoveRepeatedPoints`]**: Remove repeated points from a geometry. //! //! # Features //! @@ -194,19 +206,14 @@ //! [proj crate file download]: https://docs.rs/proj/*/proj/#grid-file-download //! [Serde]: https://serde.rs/ -extern crate geo_types; -extern crate num_traits; #[cfg(feature = "use-serde")] #[macro_use] extern crate serde; -#[cfg(feature = "use-proj")] -extern crate proj; -extern crate rstar; pub use crate::algorithm::*; pub use crate::types::Closest; -pub use geo_types::{coord, line_string, point, polygon, CoordFloat, CoordNum}; +pub use geo_types::{coord, line_string, point, polygon, wkt, CoordFloat, CoordNum}; pub mod geometry; pub use geometry::*; diff --git a/geo/src/utils.rs b/geo/src/utils.rs index 18e5f77428..9db9a3cd42 100644 --- a/geo/src/utils.rs +++ b/geo/src/utils.rs @@ -1,6 +1,7 @@ //! Internal utility functions, types, and data structures. -use geo_types::{Coord, CoordNum}; +use geo_types::{Coord, CoordFloat, CoordNum}; +use num_traits::FromPrimitive; /// Partition a mutable slice in-place so that it contains all elements for /// which `predicate(e)` is `true`, followed by all elements for which @@ -155,6 +156,15 @@ pub fn least_and_greatest_index(pts: &[Coord]) -> (usize, usize) (min.unwrap().0, max.unwrap().0) } +/// Normalize a longitude to coordinate to ensure it's within [-180,180] +pub fn normalize_longitude(coord: T) -> T { + let one_eighty = T::from(180.0f64).unwrap(); + let three_sixty = T::from(360.0f64).unwrap(); + let five_forty = T::from(540.0f64).unwrap(); + + ((coord + five_forty) % three_sixty) - one_eighty +} + #[cfg(test)] mod test { use super::{partial_max, partial_min}; From fd19c5db7acd3ef93137b8f4decb75df5e0b8cc4 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Thu, 16 Nov 2023 20:54:24 -0500 Subject: [PATCH 2/4] Remove unneeded trait lifetime in geo-traits --- geo-traits/src/coord.rs | 40 +++++++---- geo-traits/src/geometry.rs | 99 +++++++++++++++++---------- geo-traits/src/geometry_collection.rs | 48 ++++++++----- geo-traits/src/lib.rs | 2 + geo-traits/src/line_string.rs | 40 ++++++----- geo-traits/src/multi_line_string.rs | 43 +++++++----- geo-traits/src/multi_point.rs | 39 ++++++----- geo-traits/src/multi_polygon.rs | 43 +++++++----- geo-traits/src/point.rs | 48 +++++-------- geo-traits/src/polygon.rs | 56 ++++++++------- geo-traits/src/rect.rs | 39 +++++++++++ 11 files changed, 306 insertions(+), 191 deletions(-) create mode 100644 geo-traits/src/rect.rs diff --git a/geo-traits/src/coord.rs b/geo-traits/src/coord.rs index 2036ef9f93..336b21fe2a 100644 --- a/geo-traits/src/coord.rs +++ b/geo-traits/src/coord.rs @@ -1,7 +1,7 @@ use geo_types::{Coord, CoordNum, Point}; -pub trait CoordTrait: Send + Sync { - type T: CoordNum + Send + Sync; +pub trait CoordTrait { + type T: CoordNum; /// x component of this coord fn x(&self) -> Self::T; @@ -15,50 +15,62 @@ pub trait CoordTrait: Send + Sync { } } -impl CoordTrait for Point { +impl CoordTrait for Point { type T = T; - fn x(&self) -> T { + fn x(&self) -> Self::T { self.0.x } - fn y(&self) -> T { + fn y(&self) -> Self::T { self.0.y } } -impl CoordTrait for &Point { +impl CoordTrait for &Point { type T = T; - fn x(&self) -> T { + fn x(&self) -> Self::T { self.0.x } - fn y(&self) -> T { + fn y(&self) -> Self::T { self.0.y } } -impl CoordTrait for Coord { +impl CoordTrait for Coord { type T = T; - fn x(&self) -> T { + fn x(&self) -> Self::T { self.x } - fn y(&self) -> T { + fn y(&self) -> Self::T { self.y } } -impl CoordTrait for &Coord { +impl CoordTrait for &Coord { type T = T; - fn x(&self) -> T { + fn x(&self) -> Self::T { self.x } - fn y(&self) -> T { + fn y(&self) -> Self::T { self.y } } + +impl CoordTrait for (T, T) { + type T = T; + + fn x(&self) -> Self::T { + self.0 + } + + fn y(&self) -> Self::T { + self.1 + } +} diff --git a/geo-traits/src/geometry.rs b/geo-traits/src/geometry.rs index 3fede9acb4..b01744bea0 100644 --- a/geo-traits/src/geometry.rs +++ b/geo-traits/src/geometry.rs @@ -1,47 +1,67 @@ use geo_types::{ CoordNum, Geometry, GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, - Point, Polygon, + Point, Polygon, Rect, }; use super::{ GeometryCollectionTrait, LineStringTrait, MultiLineStringTrait, MultiPointTrait, - MultiPolygonTrait, PointTrait, PolygonTrait, + MultiPolygonTrait, PointTrait, PolygonTrait, RectTrait, }; #[allow(clippy::type_complexity)] -pub trait GeometryTrait<'a>: Send + Sync { - type Point: 'a + PointTrait; - type LineString: 'a + LineStringTrait<'a>; - type Polygon: 'a + PolygonTrait<'a>; - type MultiPoint: 'a + MultiPointTrait<'a>; - type MultiLineString: 'a + MultiLineStringTrait<'a>; - type MultiPolygon: 'a + MultiPolygonTrait<'a>; - type GeometryCollection: 'a + GeometryCollectionTrait<'a>; +pub trait GeometryTrait { + type T: CoordNum; + type Point<'a>: 'a + PointTrait + where + Self: 'a; + type LineString<'a>: 'a + LineStringTrait + where + Self: 'a; + type Polygon<'a>: 'a + PolygonTrait + where + Self: 'a; + type MultiPoint<'a>: 'a + MultiPointTrait + where + Self: 'a; + type MultiLineString<'a>: 'a + MultiLineStringTrait + where + Self: 'a; + type MultiPolygon<'a>: 'a + MultiPolygonTrait + where + Self: 'a; + type GeometryCollection<'a>: 'a + GeometryCollectionTrait + where + Self: 'a; + type Rect<'a>: 'a + RectTrait + where + Self: 'a; fn as_type( - &'a self, + &self, ) -> GeometryType< - 'a, - Self::Point, - Self::LineString, - Self::Polygon, - Self::MultiPoint, - Self::MultiLineString, - Self::MultiPolygon, - Self::GeometryCollection, + '_, + Self::Point<'_>, + Self::LineString<'_>, + Self::Polygon<'_>, + Self::MultiPoint<'_>, + Self::MultiLineString<'_>, + Self::MultiPolygon<'_>, + Self::GeometryCollection<'_>, + Self::Rect<'_>, >; } #[derive(Debug)] -pub enum GeometryType<'a, P, L, Y, MP, ML, MY, GC> +pub enum GeometryType<'a, P, L, Y, MP, ML, MY, GC, R> where - P: 'a + PointTrait, - L: 'a + LineStringTrait<'a>, - Y: 'a + PolygonTrait<'a>, - MP: 'a + MultiPointTrait<'a>, - ML: 'a + MultiLineStringTrait<'a>, - MY: 'a + MultiPolygonTrait<'a>, - GC: 'a + GeometryCollectionTrait<'a>, + P: PointTrait, + L: LineStringTrait, + Y: PolygonTrait, + MP: MultiPointTrait, + ML: MultiLineStringTrait, + MY: MultiPolygonTrait, + GC: GeometryCollectionTrait, + R: RectTrait, { Point(&'a P), LineString(&'a L), @@ -50,21 +70,24 @@ where MultiLineString(&'a ML), MultiPolygon(&'a MY), GeometryCollection(&'a GC), + Rect(&'a R), } -impl<'a, T: CoordNum + Send + Sync + 'a> GeometryTrait<'a> for Geometry { - type Point = Point; - type LineString = LineString; - type Polygon = Polygon; - type MultiPoint = MultiPoint; - type MultiLineString = MultiLineString; - type MultiPolygon = MultiPolygon; - type GeometryCollection = GeometryCollection; +impl<'a, T: CoordNum + 'a> GeometryTrait for Geometry { + type T = T; + type Point<'b> = Point where Self: 'b; + type LineString<'b> = LineString where Self: 'b; + type Polygon<'b> = Polygon where Self: 'b; + type MultiPoint<'b> = MultiPoint where Self: 'b; + type MultiLineString<'b> = MultiLineString where Self: 'b; + type MultiPolygon<'b> = MultiPolygon where Self: 'b; + type GeometryCollection<'b> = GeometryCollection where Self: 'b; + type Rect<'b> = Rect where Self: 'b; fn as_type( - &'a self, + &self, ) -> GeometryType< - 'a, + '_, Point, LineString, Polygon, @@ -72,6 +95,7 @@ impl<'a, T: CoordNum + Send + Sync + 'a> GeometryTrait<'a> for Geometry { MultiLineString, MultiPolygon, GeometryCollection, + Rect, > { match self { Geometry::Point(p) => GeometryType::Point(p), @@ -81,6 +105,7 @@ impl<'a, T: CoordNum + Send + Sync + 'a> GeometryTrait<'a> for Geometry { Geometry::MultiLineString(p) => GeometryType::MultiLineString(p), Geometry::MultiPolygon(p) => GeometryType::MultiPolygon(p), Geometry::GeometryCollection(p) => GeometryType::GeometryCollection(p), + Geometry::Rect(p) => GeometryType::Rect(p), _ => todo!(), } } diff --git a/geo-traits/src/geometry_collection.rs b/geo-traits/src/geometry_collection.rs index 4343ad121d..17e6b1b501 100644 --- a/geo-traits/src/geometry_collection.rs +++ b/geo-traits/src/geometry_collection.rs @@ -3,51 +3,63 @@ use geo_types::{CoordNum, Geometry, GeometryCollection}; use std::iter::Cloned; use std::slice::Iter; -pub trait GeometryCollectionTrait<'a>: Send + Sync { - type ItemType: 'a + GeometryTrait<'a>; - type Iter: ExactSizeIterator; +pub trait GeometryCollectionTrait { + type T: CoordNum; + type ItemType<'a>: 'a + GeometryTrait + where + Self: 'a; + type Iter<'a>: ExactSizeIterator> + where + Self: 'a; /// An iterator over the geometries in this GeometryCollection - fn geometries(&'a self) -> Self::Iter; + fn geometries(&self) -> Self::Iter<'_>; /// The number of geometries in this GeometryCollection - fn num_geometries(&'a self) -> usize; + fn num_geometries(&self) -> usize; /// Access to a specified geometry in this GeometryCollection /// Will return None if the provided index is out of bounds - fn geometry(&'a self, i: usize) -> Option; + fn geometry(&self, i: usize) -> Option>; } -impl<'a, T: CoordNum + Send + Sync + 'a> GeometryCollectionTrait<'a> for GeometryCollection { - type ItemType = Geometry; - type Iter = Cloned>; +impl GeometryCollectionTrait for GeometryCollection { + type T = T; + type ItemType<'a> = Geometry + where + Self: 'a; + type Iter<'a> = Cloned>> + where T: 'a; - fn geometries(&'a self) -> Self::Iter { + fn geometries(&self) -> Self::Iter<'_> { self.0.iter().cloned() } - fn num_geometries(&'a self) -> usize { + fn num_geometries(&self) -> usize { self.0.len() } - fn geometry(&'a self, i: usize) -> Option { + fn geometry(&self, i: usize) -> Option> { self.0.get(i).cloned() } } -impl<'a, T: CoordNum + Send + Sync + 'a> GeometryCollectionTrait<'a> for &GeometryCollection { - type ItemType = Geometry; - type Iter = Cloned>; +impl<'a, T: CoordNum> GeometryCollectionTrait for &'a GeometryCollection { + type T = T; + type ItemType<'b> = Geometry where + Self: 'b; + type Iter<'b> = Cloned>> where + Self: 'b; - fn geometries(&'a self) -> Self::Iter { + fn geometries(&self) -> Self::Iter<'_> { self.0.iter().cloned() } - fn num_geometries(&'a self) -> usize { + fn num_geometries(&self) -> usize { self.0.len() } - fn geometry(&'a self, i: usize) -> Option { + fn geometry(&self, i: usize) -> Option> { self.0.get(i).cloned() } } diff --git a/geo-traits/src/lib.rs b/geo-traits/src/lib.rs index ce4bbe22cf..be26600563 100644 --- a/geo-traits/src/lib.rs +++ b/geo-traits/src/lib.rs @@ -7,6 +7,7 @@ pub use multi_point::MultiPointTrait; pub use multi_polygon::MultiPolygonTrait; pub use point::PointTrait; pub use polygon::PolygonTrait; +pub use rect::RectTrait; mod coord; mod geometry; @@ -17,3 +18,4 @@ mod multi_point; mod multi_polygon; mod point; mod polygon; +mod rect; diff --git a/geo-traits/src/line_string.rs b/geo-traits/src/line_string.rs index b4fa30bd3c..2124bcbba8 100644 --- a/geo-traits/src/line_string.rs +++ b/geo-traits/src/line_string.rs @@ -4,26 +4,33 @@ use super::CoordTrait; use std::iter::Cloned; use std::slice::Iter; -pub trait LineStringTrait<'a>: Send + Sync { - type ItemType: 'a + CoordTrait; - type Iter: ExactSizeIterator; +pub trait LineStringTrait { + type T: CoordNum; + type ItemType<'a>: 'a + CoordTrait + where + Self: 'a; + type Iter<'a>: ExactSizeIterator> + where + Self: 'a; /// An iterator over the coords in this LineString - fn coords(&'a self) -> Self::Iter; + fn coords(&self) -> Self::Iter<'_>; /// The number of coords in this LineString - fn num_coords(&'a self) -> usize; + fn num_coords(&self) -> usize; /// Access to a specified point in this LineString /// Will return None if the provided index is out of bounds - fn coord(&'a self, i: usize) -> Option; + fn coord(&self, i: usize) -> Option>; } -impl<'a, T: CoordNum + Send + Sync + 'a> LineStringTrait<'a> for LineString { - type ItemType = Coord; - type Iter = Cloned>; +impl LineStringTrait for LineString { + type T = T; + type ItemType<'a> = Coord where Self: 'a; + type Iter<'a> = Cloned>> where T: 'a; - fn coords(&'a self) -> Self::Iter { + fn coords(&self) -> Self::Iter<'_> { + // TODO: remove cloned self.0.iter().cloned() } @@ -31,16 +38,17 @@ impl<'a, T: CoordNum + Send + Sync + 'a> LineStringTrait<'a> for LineString { self.0.len() } - fn coord(&'a self, i: usize) -> Option { + fn coord(&self, i: usize) -> Option> { self.0.get(i).cloned() } } -impl<'a, T: CoordNum + Send + Sync + 'a> LineStringTrait<'a> for &LineString { - type ItemType = Coord; - type Iter = Cloned>; +impl<'a, T: CoordNum> LineStringTrait for &'a LineString { + type T = T; + type ItemType<'b> = Coord where Self: 'b; + type Iter<'b> = Cloned>> where Self: 'b; - fn coords(&'a self) -> Self::Iter { + fn coords(&self) -> Self::Iter<'_> { self.0.iter().cloned() } @@ -48,7 +56,7 @@ impl<'a, T: CoordNum + Send + Sync + 'a> LineStringTrait<'a> for &LineString self.0.len() } - fn coord(&'a self, i: usize) -> Option { + fn coord(&self, i: usize) -> Option> { self.0.get(i).cloned() } } diff --git a/geo-traits/src/multi_line_string.rs b/geo-traits/src/multi_line_string.rs index 27debbfe5e..8c81075ba3 100644 --- a/geo-traits/src/multi_line_string.rs +++ b/geo-traits/src/multi_line_string.rs @@ -3,51 +3,58 @@ use geo_types::{CoordNum, LineString, MultiLineString}; use std::iter::Cloned; use std::slice::Iter; -pub trait MultiLineStringTrait<'a>: Send + Sync { - type ItemType: 'a + LineStringTrait<'a>; - type Iter: ExactSizeIterator; +pub trait MultiLineStringTrait { + type T: CoordNum; + type ItemType<'a>: 'a + LineStringTrait + where + Self: 'a; + type Iter<'a>: ExactSizeIterator> + where + Self: 'a; /// An iterator over the LineStrings in this MultiLineString - fn lines(&'a self) -> Self::Iter; + fn lines(&self) -> Self::Iter<'_>; /// The number of lines in this MultiLineString - fn num_lines(&'a self) -> usize; + fn num_lines(&self) -> usize; /// Access to a specified line in this MultiLineString /// Will return None if the provided index is out of bounds - fn line(&'a self, i: usize) -> Option; + fn line(&self, i: usize) -> Option>; } -impl<'a, T: CoordNum + Send + Sync + 'a> MultiLineStringTrait<'a> for MultiLineString { - type ItemType = LineString; - type Iter = Cloned>; +impl MultiLineStringTrait for MultiLineString { + type T = T; + type ItemType<'a> = LineString where Self: 'a; + type Iter<'a> = Cloned>> where T: 'a; - fn lines(&'a self) -> Self::Iter { + fn lines(&self) -> Self::Iter<'_> { self.0.iter().cloned() } - fn num_lines(&'a self) -> usize { + fn num_lines(&self) -> usize { self.0.len() } - fn line(&'a self, i: usize) -> Option { + fn line(&self, i: usize) -> Option> { self.0.get(i).cloned() } } -impl<'a, T: CoordNum + Send + Sync + 'a> MultiLineStringTrait<'a> for &MultiLineString { - type ItemType = LineString; - type Iter = Cloned>; +impl<'a, T: CoordNum> MultiLineStringTrait for &'a MultiLineString { + type T = T; + type ItemType<'b> = LineString where Self: 'b; + type Iter<'b> = Cloned>> where Self: 'b; - fn lines(&'a self) -> Self::Iter { + fn lines(&self) -> Self::Iter<'_> { self.0.iter().cloned() } - fn num_lines(&'a self) -> usize { + fn num_lines(&self) -> usize { self.0.len() } - fn line(&'a self, i: usize) -> Option { + fn line(&self, i: usize) -> Option> { self.0.get(i).cloned() } } diff --git a/geo-traits/src/multi_point.rs b/geo-traits/src/multi_point.rs index 01fc858bc1..f00b6845ba 100644 --- a/geo-traits/src/multi_point.rs +++ b/geo-traits/src/multi_point.rs @@ -3,26 +3,32 @@ use geo_types::{CoordNum, MultiPoint, Point}; use std::iter::Cloned; use std::slice::Iter; -pub trait MultiPointTrait<'a>: Send + Sync { - type ItemType: 'a + PointTrait; - type Iter: ExactSizeIterator; +pub trait MultiPointTrait { + type T: CoordNum; + type ItemType<'a>: 'a + PointTrait + where + Self: 'a; + type Iter<'a>: ExactSizeIterator> + where + Self: 'a; /// An iterator over the points in this MultiPoint - fn points(&'a self) -> Self::Iter; + fn points(&self) -> Self::Iter<'_>; /// The number of points in this MultiPoint - fn num_points(&'a self) -> usize; + fn num_points(&self) -> usize; /// Access to a specified point in this MultiPoint /// Will return None if the provided index is out of bounds - fn point(&'a self, i: usize) -> Option; + fn point(&self, i: usize) -> Option>; } -impl<'a, T: CoordNum + Send + Sync + 'a> MultiPointTrait<'a> for MultiPoint { - type ItemType = Point; - type Iter = Cloned>; +impl MultiPointTrait for MultiPoint { + type T = T; + type ItemType<'a> = Point where Self: 'a; + type Iter<'a> = Cloned>> where T: 'a; - fn points(&'a self) -> Self::Iter { + fn points(&self) -> Self::Iter<'_> { self.0.iter().cloned() } @@ -30,16 +36,17 @@ impl<'a, T: CoordNum + Send + Sync + 'a> MultiPointTrait<'a> for MultiPoint { self.0.len() } - fn point(&'a self, i: usize) -> Option { + fn point(&self, i: usize) -> Option> { self.0.get(i).cloned() } } -impl<'a, T: CoordNum + Send + Sync + 'a> MultiPointTrait<'a> for &MultiPoint { - type ItemType = Point; - type Iter = Cloned>; +impl<'a, T: CoordNum> MultiPointTrait for &'a MultiPoint { + type T = T; + type ItemType<'b> = Point where Self: 'b; + type Iter<'b> = Cloned>> where Self: 'b; - fn points(&'a self) -> Self::Iter { + fn points(&self) -> Self::Iter<'_> { self.0.iter().cloned() } @@ -47,7 +54,7 @@ impl<'a, T: CoordNum + Send + Sync + 'a> MultiPointTrait<'a> for &MultiPoint self.0.len() } - fn point(&'a self, i: usize) -> Option { + fn point(&self, i: usize) -> Option> { self.0.get(i).cloned() } } diff --git a/geo-traits/src/multi_polygon.rs b/geo-traits/src/multi_polygon.rs index 4b2eeacfa3..27bb35b2a1 100644 --- a/geo-traits/src/multi_polygon.rs +++ b/geo-traits/src/multi_polygon.rs @@ -3,51 +3,58 @@ use geo_types::{CoordNum, MultiPolygon, Polygon}; use std::iter::Cloned; use std::slice::Iter; -pub trait MultiPolygonTrait<'a>: Send + Sync { - type ItemType: 'a + PolygonTrait<'a>; - type Iter: ExactSizeIterator; +pub trait MultiPolygonTrait { + type T: CoordNum; + type ItemType<'a>: 'a + PolygonTrait + where + Self: 'a; + type Iter<'a>: ExactSizeIterator> + where + Self: 'a; /// An iterator over the Polygons in this MultiPolygon - fn polygons(&'a self) -> Self::Iter; + fn polygons(&self) -> Self::Iter<'_>; /// The number of polygons in this MultiPolygon - fn num_polygons(&'a self) -> usize; + fn num_polygons(&self) -> usize; /// Access to a specified polygon in this MultiPolygon /// Will return None if the provided index is out of bounds - fn polygon(&'a self, i: usize) -> Option; + fn polygon(&self, i: usize) -> Option>; } -impl<'a, T: CoordNum + Send + Sync + 'a> MultiPolygonTrait<'a> for MultiPolygon { - type ItemType = Polygon; - type Iter = Cloned>; +impl MultiPolygonTrait for MultiPolygon { + type T = T; + type ItemType<'a> = Polygon where Self: 'a; + type Iter<'a> = Cloned>> where T: 'a; - fn polygons(&'a self) -> Self::Iter { + fn polygons(&self) -> Self::Iter<'_> { self.0.iter().cloned() } - fn num_polygons(&'a self) -> usize { + fn num_polygons(&self) -> usize { self.0.len() } - fn polygon(&'a self, i: usize) -> Option { + fn polygon(&self, i: usize) -> Option> { self.0.get(i).cloned() } } -impl<'a, T: CoordNum + Send + Sync + 'a> MultiPolygonTrait<'a> for &MultiPolygon { - type ItemType = Polygon; - type Iter = Cloned>; +impl<'a, T: CoordNum> MultiPolygonTrait for &'a MultiPolygon { + type T = T; + type ItemType<'b> = Polygon where Self: 'b; + type Iter<'b> = Cloned>> where Self: 'b; - fn polygons(&'a self) -> Self::Iter { + fn polygons(&self) -> Self::Iter<'_> { self.0.iter().cloned() } - fn num_polygons(&'a self) -> usize { + fn num_polygons(&self) -> usize { self.0.len() } - fn polygon(&'a self, i: usize) -> Option { + fn polygon(&self, i: usize) -> Option> { self.0.get(i).cloned() } } diff --git a/geo-traits/src/point.rs b/geo-traits/src/point.rs index 1f33ea8437..62216b026c 100644 --- a/geo-traits/src/point.rs +++ b/geo-traits/src/point.rs @@ -1,7 +1,7 @@ use geo_types::{Coord, CoordNum, Point}; -pub trait PointTrait: Send + Sync { - type T: CoordNum + Send + Sync; +pub trait PointTrait { + type T: CoordNum; /// x component of this coord fn x(&self) -> Self::T; @@ -10,69 +10,55 @@ pub trait PointTrait: Send + Sync { fn y(&self) -> Self::T; /// Returns a tuple that contains the x/horizontal & y/vertical component of the coord. - fn x_y(&self) -> (Self::T, Self::T); + fn x_y(&self) -> (Self::T, Self::T) { + (self.x(), self.y()) + } } -impl PointTrait for Point { +impl PointTrait for Point { type T = T; - fn x(&self) -> T { + fn x(&self) -> Self::T { self.0.x } - fn y(&self) -> T { + fn y(&self) -> Self::T { self.0.y } - - fn x_y(&self) -> (T, T) { - (self.0.x, self.0.y) - } } -impl PointTrait for &Point { +impl PointTrait for &Point { type T = T; - fn x(&self) -> T { + fn x(&self) -> Self::T { self.0.x } - fn y(&self) -> T { + fn y(&self) -> Self::T { self.0.y } - - fn x_y(&self) -> (T, T) { - (self.0.x, self.0.y) - } } -impl PointTrait for Coord { +impl PointTrait for Coord { type T = T; - fn x(&self) -> T { + fn x(&self) -> Self::T { self.x } - fn y(&self) -> T { + fn y(&self) -> Self::T { self.y } - - fn x_y(&self) -> (T, T) { - (self.x, self.y) - } } -impl PointTrait for &Coord { +impl PointTrait for &Coord { type T = T; - fn x(&self) -> T { + fn x(&self) -> Self::T { self.x } - fn y(&self) -> T { + fn y(&self) -> Self::T { self.y } - - fn x_y(&self) -> (T, T) { - (self.x, self.y) - } } diff --git a/geo-traits/src/polygon.rs b/geo-traits/src/polygon.rs index be8ad591b9..360462b5f7 100644 --- a/geo-traits/src/polygon.rs +++ b/geo-traits/src/polygon.rs @@ -3,62 +3,72 @@ use geo_types::{CoordNum, LineString, Polygon}; use std::iter::Cloned; use std::slice::Iter; -pub trait PolygonTrait<'a>: Send + Sync { - type ItemType: 'a + LineStringTrait<'a>; - type Iter: ExactSizeIterator; +pub trait PolygonTrait { + type T: CoordNum; + type ItemType<'a>: 'a + LineStringTrait + where + Self: 'a; + type Iter<'a>: ExactSizeIterator> + where + Self: 'a; /// The exterior ring of the polygon - fn exterior(&'a self) -> Self::ItemType; + fn exterior(&self) -> Option>; /// An iterator of the interior rings of this Polygon - fn interiors(&'a self) -> Self::Iter; + fn interiors(&self) -> Self::Iter<'_>; /// The number of interior rings in this Polygon - fn num_interiors(&'a self) -> usize; + fn num_interiors(&self) -> usize; /// Access to a specified interior ring in this Polygon /// Will return None if the provided index is out of bounds - fn interior(&'a self, i: usize) -> Option; + fn interior(&self, i: usize) -> Option>; } -impl<'a, T: CoordNum + Send + Sync + 'a> PolygonTrait<'a> for Polygon { - type ItemType = LineString; - type Iter = Cloned>; +impl PolygonTrait for Polygon { + type T = T; + type ItemType<'a> = LineString where Self: 'a; + type Iter<'a> = Cloned>> where T: 'a; - fn exterior(&'a self) -> Self::ItemType { - Polygon::exterior(self).clone() + fn exterior(&self) -> Option> { + // geo-types doesn't really have a way to describe an empty polygon + Some(Polygon::exterior(self).clone()) } - fn interiors(&'a self) -> Self::Iter { + fn interiors(&self) -> Self::Iter<'_> { Polygon::interiors(self).iter().cloned() } - fn num_interiors(&'a self) -> usize { + fn num_interiors(&self) -> usize { Polygon::interiors(self).len() } - fn interior(&'a self, i: usize) -> Option { + fn interior(&self, i: usize) -> Option> { Polygon::interiors(self).get(i).cloned() } } -impl<'a, T: CoordNum + Send + Sync + 'a> PolygonTrait<'a> for &Polygon { - type ItemType = LineString; - type Iter = Cloned>; +impl<'a, T: CoordNum> PolygonTrait for &'a Polygon { + type T = T; + type ItemType<'b> = LineString where + Self: 'b; + type Iter<'b> = Cloned>> where + Self: 'b; - fn exterior(&'a self) -> Self::ItemType { - Polygon::exterior(self).clone() + fn exterior(&self) -> Option> { + Some(Polygon::exterior(self).clone()) } - fn interiors(&'a self) -> Self::Iter { + fn interiors(&self) -> Self::Iter<'_> { Polygon::interiors(self).iter().cloned() } - fn num_interiors(&'a self) -> usize { + fn num_interiors(&self) -> usize { Polygon::interiors(self).len() } - fn interior(&'a self, i: usize) -> Option { + fn interior(&self, i: usize) -> Option> { Polygon::interiors(self).get(i).cloned() } } diff --git a/geo-traits/src/rect.rs b/geo-traits/src/rect.rs new file mode 100644 index 0000000000..ec071b44e0 --- /dev/null +++ b/geo-traits/src/rect.rs @@ -0,0 +1,39 @@ +use super::CoordTrait; +use geo_types::{Coord, CoordNum, Rect}; + +pub trait RectTrait { + type T: CoordNum; + type ItemType<'a>: 'a + CoordTrait + where + Self: 'a; + + fn lower(&self) -> Self::ItemType<'_>; + + fn upper(&self) -> Self::ItemType<'_>; +} + +impl<'a, T: CoordNum + 'a> RectTrait for Rect { + type T = T; + type ItemType<'b> = Coord where Self: 'b; + + fn lower(&self) -> Self::ItemType<'_> { + self.min() + } + + fn upper(&self) -> Self::ItemType<'_> { + self.max() + } +} + +impl<'a, T: CoordNum + 'a> RectTrait for &'a Rect { + type T = T; + type ItemType<'b> = Coord where Self: 'b; + + fn lower(&self) -> Self::ItemType<'_> { + self.min() + } + + fn upper(&self) -> Self::ItemType<'_> { + self.max() + } +} From 378b6d62aba89307e2e9629b83863e029f381c93 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Thu, 16 Nov 2023 23:24:09 -0500 Subject: [PATCH 3/4] Working area on traits, and passing tests! --- geo-bool-ops-benches/src/tests.rs | 1 + geo-traits/src/rect.rs | 37 +++++++ geo/Cargo.toml | 1 + geo/benches/area.rs | 1 + geo/src/algorithm/area.rs | 125 ++++++++++++++++------ geo/src/algorithm/centroid.rs | 2 +- geo/src/algorithm/minimum_rotated_rect.rs | 1 + 7 files changed, 135 insertions(+), 33 deletions(-) diff --git a/geo-bool-ops-benches/src/tests.rs b/geo-bool-ops-benches/src/tests.rs index 191340f225..a2b5350a28 100644 --- a/geo-bool-ops-benches/src/tests.rs +++ b/geo-bool-ops-benches/src/tests.rs @@ -1,4 +1,5 @@ use anyhow::{bail, Context, Result}; +use geo::area::AreaMultiPolygon; use geo::prelude::*; use geo_booleanop::boolean::BooleanOp as OtherBOp; use geo_types::*; diff --git a/geo-traits/src/rect.rs b/geo-traits/src/rect.rs index ec071b44e0..e9b5e232e6 100644 --- a/geo-traits/src/rect.rs +++ b/geo-traits/src/rect.rs @@ -10,6 +10,43 @@ pub trait RectTrait { fn lower(&self) -> Self::ItemType<'_>; fn upper(&self) -> Self::ItemType<'_>; + + /// Returns the width of the `Rect`. + /// + /// # Examples + /// + /// ```rust + /// use geo_types::{coord, Rect}; + /// + /// let rect = Rect::new( + /// coord! { x: 5., y: 5. }, + /// coord! { x: 15., y: 15. }, + /// ); + /// + /// assert_eq!(rect.width(), 10.); + /// ``` + fn width(&self) -> Self::T { + self.upper().x() - self.lower().x() + } + + /// Returns the height of the `Rect`. + /// + /// # Examples + /// + /// ```rust + /// use geo_types::{coord, Rect}; + /// + /// let rect = Rect::new( + /// coord! { x: 5., y: 5. }, + /// coord! { x: 15., y: 15. }, + /// ); + /// + /// assert_eq!(rect.height(), 10.); + /// ``` + fn height(&self) -> Self::T { + self.upper().y() - self.lower().y() + } + } impl<'a, T: CoordNum + 'a> RectTrait for Rect { diff --git a/geo/Cargo.toml b/geo/Cargo.toml index e587c1299e..0a0eb8916e 100644 --- a/geo/Cargo.toml +++ b/geo/Cargo.toml @@ -23,6 +23,7 @@ earcutr = { version = "0.4.2", optional = true } spade = { version = "2.2.0", optional = true } float_next_after = "1.0.0" geo-types = { version = "0.7.12", features = ["approx", "use-rstar_0_11"] } +geo-traits = { path = "../geo-traits" } geographiclib-rs = { version = "0.2.3", default-features = false } log = "0.4.11" num-traits = "0.2" diff --git a/geo/benches/area.rs b/geo/benches/area.rs index b295a89887..540cbd1b32 100644 --- a/geo/benches/area.rs +++ b/geo/benches/area.rs @@ -1,6 +1,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use geo::Area; use geo::Polygon; +use geo::area::AreaPolygon; fn criterion_benchmark(c: &mut Criterion) { c.bench_function("area", |bencher| { diff --git a/geo/src/algorithm/area.rs b/geo/src/algorithm/area.rs index 6aec9d21af..67328c1d0b 100644 --- a/geo/src/algorithm/area.rs +++ b/geo/src/algorithm/area.rs @@ -1,19 +1,24 @@ use crate::geometry::*; use crate::{CoordFloat, CoordNum}; +use geo_traits::*; -pub(crate) fn twice_signed_ring_area(linestring: &LineString) -> T +pub(crate) fn twice_signed_ring_area(linestring: &impl LineStringTrait) -> T where T: CoordNum, { // LineString with less than 3 points is empty, or a // single point, or is not closed. - if linestring.0.len() < 3 { + if linestring.num_coords() < 3 { return T::zero(); } // Above test ensures the vector has at least 2 elements. // We check if linestring is closed, and return 0 otherwise. - if linestring.0.first().unwrap() != linestring.0.last().unwrap() { + // TODO: should the trait require Eq? + let p1 = linestring.coord(0).unwrap(); + let p2 = linestring.coord(linestring.num_coords() - 1).unwrap(); + let closed = p1.x() == p2.x() && p1.y() == p2.y(); + if !closed { return T::zero(); } @@ -27,12 +32,25 @@ where // of the coordinates, but it is not fool-proof to // divide by the length of the linestring (eg. a long // line-string with T = u8) - let shift = linestring.0[0]; + let shift = linestring.coord(0).unwrap(); let mut tmp = T::zero(); - for line in linestring.lines() { - use crate::MapCoords; - let line = line.map_coords(|c| c - shift); + for i in 0..linestring.num_coords() - 1 { + let mut c1 = Coord { + x: linestring.coord(i).unwrap().x(), + y: linestring.coord(i).unwrap().y(), + }; + c1.x = c1.x - shift.x(); + c1.y = c1.y - shift.y(); + + let mut c2 = Coord { + x: linestring.coord(i + 1).unwrap().x(), + y: linestring.coord(i + 1).unwrap().y(), + }; + c2.x = c2.x - shift.x(); + c2.y = c2.y - shift.y(); + + let line = Line::new(c1, c2); tmp = tmp + line.determinant(); } @@ -46,6 +64,7 @@ where /// ``` /// use geo::polygon; /// use geo::Area; +/// use geo::area::AreaPolygon; /// /// let mut polygon = polygon![ /// (x: 0., y: 0.), @@ -75,14 +94,14 @@ where } // Calculation of simple (no interior holes) Polygon area -pub(crate) fn get_linestring_area(linestring: &LineString) -> T +pub(crate) fn get_linestring_area(linestring: &impl LineStringTrait) -> T where T: CoordFloat, { twice_signed_ring_area(linestring) / (T::one() + T::one()) } -impl Area for Point +pub trait AreaPoint: PointTrait where T: CoordNum, { @@ -95,7 +114,9 @@ where } } -impl Area for LineString +impl> AreaPoint for P {} + +pub trait AreaLineString: LineStringTrait where T: CoordNum, { @@ -108,6 +129,8 @@ where } } +impl> AreaLineString for P {} + impl Area for Line where T: CoordNum, @@ -124,19 +147,19 @@ where /// **Note.** The implementation handles polygons whose /// holes do not all have the same orientation. The sign of /// the output is the same as that of the exterior shell. -impl Area for Polygon +pub trait AreaPolygon: PolygonTrait where T: CoordFloat, { fn signed_area(&self) -> T { - let area = get_linestring_area(self.exterior()); + let area = get_linestring_area(&self.exterior().unwrap()); // We could use winding order here, but that would // result in computing the shoelace formula twice. - let is_negative = area < T::zero(); + let is_negative = area < Self::T::zero(); - let area = self.interiors().iter().fold(area.abs(), |total, next| { - total - get_linestring_area(next).abs() + let area = self.interiors().fold(area.abs(), |total, next| { + total - get_linestring_area(&next).abs() }); if is_negative { @@ -151,7 +174,9 @@ where } } -impl Area for MultiPoint +impl> AreaPolygon for P {} + +pub trait AreaMultiPoint: MultiPointTrait where T: CoordNum, { @@ -164,7 +189,9 @@ where } } -impl Area for MultiLineString +impl> AreaMultiPoint for P {} + +pub trait AreaMultiLineString: MultiLineStringTrait where T: CoordNum, { @@ -177,31 +204,33 @@ where } } +impl> AreaMultiLineString for P {} + /// **Note.** The implementation is a straight-forward /// summation of the signed areas of the individual /// polygons. In particular, `unsigned_area` is not /// necessarily the sum of the `unsigned_area` of the /// constituent polygons unless they are all oriented the /// same. -impl Area for MultiPolygon +pub trait AreaMultiPolygon: MultiPolygonTrait where T: CoordFloat, { fn signed_area(&self) -> T { - self.0 - .iter() + self.polygons() .fold(T::zero(), |total, next| total + next.signed_area()) } fn unsigned_area(&self) -> T { - self.0 - .iter() + self.polygons() .fold(T::zero(), |total, next| total + next.signed_area().abs()) } } +impl> AreaMultiPolygon for P {} + /// Because a `Rect` has no winding order, the area will always be positive. -impl Area for Rect +pub trait AreaRect: RectTrait where T: CoordNum, { @@ -214,6 +243,8 @@ where } } +impl> AreaRect for P {} + impl Area for Triangle where T: CoordFloat, @@ -230,38 +261,68 @@ where } } -impl Area for Geometry +pub trait AreaGeometry: GeometryTrait where T: CoordFloat, { - crate::geometry_delegate_impl! { - fn signed_area(&self) -> T; - fn unsigned_area(&self) -> T; + fn signed_area(&self) -> T { + use GeometryType::*; + match self.as_type() { + Point(g) => g.signed_area(), + LineString(g) => g.signed_area(), + Polygon(g) => g.signed_area(), + MultiPoint(g) => g.signed_area(), + MultiLineString(g) => g.signed_area(), + MultiPolygon(g) => g.signed_area(), + GeometryCollection(g) => g.signed_area(), + Rect(g) => g.signed_area(), + // TODO: Implement triangle and line traits + _ => todo!(), + } + } + + fn unsigned_area(&self) -> T { + use GeometryType::*; + match self.as_type() { + Point(g) => g.unsigned_area(), + LineString(g) => g.unsigned_area(), + Polygon(g) => g.unsigned_area(), + MultiPoint(g) => g.unsigned_area(), + MultiLineString(g) => g.unsigned_area(), + MultiPolygon(g) => g.unsigned_area(), + GeometryCollection(g) => g.unsigned_area(), + Rect(g) => g.unsigned_area(), + // TODO: Implement triangle and line traits + _ => todo!(), + } } } -impl Area for GeometryCollection +impl> AreaGeometry for P {} + +pub trait AreaGeometryCollection: GeometryCollectionTrait where T: CoordFloat, { fn signed_area(&self) -> T { - self.0 - .iter() + self.geometries() .map(|g| g.signed_area()) .fold(T::zero(), |acc, next| acc + next) } fn unsigned_area(&self) -> T { - self.0 - .iter() + self.geometries() .map(|g| g.unsigned_area()) .fold(T::zero(), |acc, next| acc + next) } } +impl> AreaGeometryCollection for P {} + #[cfg(test)] mod test { use crate::Area; + use crate::area::{AreaPolygon, AreaRect, AreaMultiPolygon}; use crate::{coord, polygon, wkt, Line, MultiPolygon, Polygon, Rect, Triangle}; // Area of the polygon diff --git a/geo/src/algorithm/centroid.rs b/geo/src/algorithm/centroid.rs index 8718295f55..dfeaf66ae7 100644 --- a/geo/src/algorithm/centroid.rs +++ b/geo/src/algorithm/centroid.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; -use crate::area::{get_linestring_area, Area}; +use crate::area::{get_linestring_area, Area, AreaRect}; use crate::dimensions::{Dimensions, Dimensions::*, HasDimensions}; use crate::geometry::*; use crate::EuclideanLength; diff --git a/geo/src/algorithm/minimum_rotated_rect.rs b/geo/src/algorithm/minimum_rotated_rect.rs index 73ecc3aba8..1faea43c7a 100644 --- a/geo/src/algorithm/minimum_rotated_rect.rs +++ b/geo/src/algorithm/minimum_rotated_rect.rs @@ -1,5 +1,6 @@ use num_traits::Float; +use crate::area::AreaPolygon; use crate::{ algorithm::{centroid::Centroid, rotate::Rotate, BoundingRect, CoordsIter}, Area, ConvexHull, CoordFloat, GeoFloat, GeoNum, LinesIter, Polygon, From 52d56275a66ed419159a15c5f7c75a2cff889aa0 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Sat, 18 Nov 2023 13:02:57 -0500 Subject: [PATCH 4/4] lint --- geo-traits/src/rect.rs | 1 - geo/benches/area.rs | 2 +- geo/src/algorithm/area.rs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/geo-traits/src/rect.rs b/geo-traits/src/rect.rs index e9b5e232e6..26b81dba68 100644 --- a/geo-traits/src/rect.rs +++ b/geo-traits/src/rect.rs @@ -46,7 +46,6 @@ pub trait RectTrait { fn height(&self) -> Self::T { self.upper().y() - self.lower().y() } - } impl<'a, T: CoordNum + 'a> RectTrait for Rect { diff --git a/geo/benches/area.rs b/geo/benches/area.rs index 540cbd1b32..c15c41631e 100644 --- a/geo/benches/area.rs +++ b/geo/benches/area.rs @@ -1,7 +1,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; +use geo::area::AreaPolygon; use geo::Area; use geo::Polygon; -use geo::area::AreaPolygon; fn criterion_benchmark(c: &mut Criterion) { c.bench_function("area", |bencher| { diff --git a/geo/src/algorithm/area.rs b/geo/src/algorithm/area.rs index 67328c1d0b..f1ac4e8aee 100644 --- a/geo/src/algorithm/area.rs +++ b/geo/src/algorithm/area.rs @@ -321,8 +321,8 @@ impl> AreaGeometryCollection #[cfg(test)] mod test { + use crate::area::{AreaMultiPolygon, AreaPolygon, AreaRect}; use crate::Area; - use crate::area::{AreaPolygon, AreaRect, AreaMultiPolygon}; use crate::{coord, polygon, wkt, Line, MultiPolygon, Polygon, Rect, Triangle}; // Area of the polygon