diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 84f26f58..00b18df4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,6 +20,7 @@ repos: hooks: - id: check-yaml - id: end-of-file-fixer + exclude: "^rust/geo-test-fixtures/fixtures/.*" - id: trailing-whitespace - repo: https://github.com/codespell-project/codespell diff --git a/Cargo.lock b/Cargo.lock index 56a50589..551f08be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,9 +52,21 @@ dependencies = [ [[package]] name = "adbc_core" -version = "0.18.0" -source = "git+https://github.com/apache/arrow-adbc?rev=1ba248290cd299c4969b679463bcd54c217cf2e4#1ba248290cd299c4969b679463bcd54c217cf2e4" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b891479797b5588e320f7fd3caf15faba311cf8f8a76911195b6a3d55304eb" +dependencies = [ + "arrow-array", + "arrow-schema", +] + +[[package]] +name = "adbc_ffi" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c30e0f28a8363a76a8ec802934223ea5811ea658308c7403c8beb9aa86fa808f" dependencies = [ + "adbc_core", "arrow-array", "arrow-schema", ] @@ -525,6 +537,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -2320,6 +2343,19 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime 1.3.0", + "log", + "regex", + "termcolor", +] + [[package]] name = "env_logger" version = "0.11.8" @@ -2566,31 +2602,12 @@ dependencies = [ [[package]] name = "geo" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4416397671d8997e9a3e7ad99714f4f00a22e9eaa9b966a5985d2194fc9e02e1" -dependencies = [ - "earcutr", - "float_next_after", - "geo-types", - "geographiclib-rs", - "i_overlay", - "log", - "num-traits", - "robust", - "rstar", - "spade", -] - -[[package]] -name = "geo-generic-alg" -version = "0.1.0" -source = "git+https://github.com/wherobots/geo.git?branch=generic-alg#66ff85949a82549b0d28fb2d4fae01e3ea19ca83" +checksum = "2fc1a1678e54befc9b4bcab6cd43b8e7f834ae8ea121118b0fd8c42747675b4a" dependencies = [ "earcutr", "float_next_after", - "geo-traits 0.2.0", - "geo-traits-ext", "geo-types", "geographiclib-rs", "i_overlay", @@ -2604,12 +2621,12 @@ dependencies = [ [[package]] name = "geo-index" version = "0.3.1" -source = "git+https://github.com/wherobots/geo-index.git?branch=main#f7d5bef2044831e78b2deb095f1af932128d74e4" +source = "git+https://github.com/wherobots/geo-index.git?branch=geo-0.31.0#bb50e17c9bf921e147de72e826d4d924c4c298cf" dependencies = [ "bytemuck", "float_next_after", "geo", - "geo-traits 0.3.0", + "geo-traits", "geo-types", "num-traits", "thiserror 1.0.69", @@ -2617,14 +2634,6 @@ dependencies = [ "wkt 0.14.0", ] -[[package]] -name = "geo-traits" -version = "0.2.0" -source = "git+https://github.com/wherobots/geo.git?branch=generic-alg#66ff85949a82549b0d28fb2d4fae01e3ea19ca83" -dependencies = [ - "geo-types", -] - [[package]] name = "geo-traits" version = "0.3.0" @@ -2634,22 +2643,11 @@ dependencies = [ "geo-types", ] -[[package]] -name = "geo-traits-ext" -version = "0.1.0" -source = "git+https://github.com/wherobots/geo.git?branch=generic-alg#66ff85949a82549b0d28fb2d4fae01e3ea19ca83" -dependencies = [ - "approx", - "geo-traits 0.2.0", - "geo-types", - "num-traits", - "serde", -] - [[package]] name = "geo-types" -version = "0.7.16" -source = "git+https://github.com/wherobots/geo.git?branch=generic-alg#66ff85949a82549b0d28fb2d4fae01e3ea19ca83" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a4dcd69d35b2c87a7c83bce9af69fd65c9d68d3833a0ded568983928f3fc99" dependencies = [ "approx", "num-traits", @@ -2813,6 +2811,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -2905,6 +2912,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + [[package]] name = "humantime" version = "2.3.0" @@ -2976,24 +2992,24 @@ dependencies = [ [[package]] name = "i_float" -version = "1.7.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85df3a416829bb955fdc2416c7b73680c8dcea8d731f2c7aa23e1042fe1b8343" +checksum = "010025c2c532c8d82e42d0b8bb5184afa449fa6f06c709ea9adcb16c49ae405b" dependencies = [ - "serde", + "libm", ] [[package]] name = "i_key_sort" -version = "0.2.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "347c253b4748a1a28baf94c9ce133b6b166f08573157e05afe718812bc599fcd" +checksum = "9190f86706ca38ac8add223b2aed8b1330002b5cdbbce28fb58b10914d38fc27" [[package]] name = "i_overlay" -version = "2.0.5" +version = "4.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0542dfef184afdd42174a03dcc0625b6147fb73e1b974b1a08a2a42ac35cee49" +checksum = "0fcccbd4e4274e0f80697f5fbc6540fdac533cce02f2081b328e68629cce24f9" dependencies = [ "i_float", "i_key_sort", @@ -3004,19 +3020,18 @@ dependencies = [ [[package]] name = "i_shape" -version = "1.7.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a38f5a42678726718ff924f6d4a0e79b129776aeed298f71de4ceedbd091bce" +checksum = "1ea154b742f7d43dae2897fcd5ead86bc7b5eefcedd305a7ebf9f69d44d61082" dependencies = [ "i_float", - "serde", ] [[package]] name = "i_tree" -version = "0.8.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "155181bc97d770181cf9477da51218a19ee92a8e5be642e796661aee2b601139" +checksum = "35e6d558e6d4c7b82bc51d9c771e7a927862a161a7d87bf2b0541450e0e20915" [[package]] name = "iana-time-zone" @@ -3210,7 +3225,7 @@ version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi", + "hermit-abi 0.5.2", "libc", "windows-sys 0.59.0", ] @@ -3761,7 +3776,7 @@ dependencies = [ "futures", "http 1.3.1", "http-body-util", - "humantime", + "humantime 2.3.0", "hyper", "itertools 0.14.0", "md-5", @@ -4016,6 +4031,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger 0.7.1", + "log", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -4158,6 +4183,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40" +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quick-xml" version = "0.38.3" @@ -4307,6 +4338,16 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "rayon" version = "1.11.0" @@ -4757,7 +4798,7 @@ dependencies = [ "datafusion-ffi", "dirs", "futures", - "geo-traits 0.2.0", + "geo-traits", "geo-types", "object_store", "parking_lot", @@ -4787,6 +4828,7 @@ name = "sedona-adbc" version = "0.2.0" dependencies = [ "adbc_core", + "adbc_ffi", "arrow-array", "arrow-schema", "datafusion", @@ -4803,7 +4845,7 @@ dependencies = [ "async-trait", "clap", "datafusion", - "env_logger", + "env_logger 0.11.8", "futures", "libmimalloc-sys", "mimalloc", @@ -4832,7 +4874,7 @@ dependencies = [ "datafusion-common", "datafusion-expr", "datafusion-physical-expr", - "geo-traits 0.2.0", + "geo-traits", "rstest", "sedona-common", "sedona-geometry", @@ -4853,7 +4895,7 @@ dependencies = [ "datafusion", "datafusion-common", "datafusion-expr", - "geo-traits 0.2.0", + "geo-traits", "rstest", "sedona-common", "sedona-expr", @@ -4863,7 +4905,7 @@ dependencies = [ "serde_json", "tokio", "wkb", - "wkt 0.13.0", + "wkt 0.14.0", ] [[package]] @@ -4876,17 +4918,53 @@ dependencies = [ "datafusion-common", "datafusion-expr", "geo", - "geo-generic-alg", - "geo-traits 0.2.0", + "geo-traits", "geo-types", "rstest", "sedona-expr", "sedona-functions", + "sedona-geo-generic-alg", "sedona-geometry", "sedona-schema", "sedona-testing", "wkb", - "wkt 0.13.0", + "wkt 0.14.0", +] + +[[package]] +name = "sedona-geo-generic-alg" +version = "0.2.0" +dependencies = [ + "approx", + "criterion", + "float_next_after", + "geo", + "geo-traits", + "geo-types", + "i_overlay", + "log", + "num-traits", + "pretty_env_logger", + "rand 0.8.5", + "rand_distr", + "robust", + "rstar", + "sedona-geo-traits-ext", + "sedona-testing", + "serde", + "wkb", + "wkt 0.14.0", +] + +[[package]] +name = "sedona-geo-traits-ext" +version = "0.2.0" +dependencies = [ + "byteorder", + "geo-traits", + "geo-types", + "num-traits", + "wkb", ] [[package]] @@ -4914,7 +4992,7 @@ dependencies = [ name = "sedona-geometry" version = "0.2.0" dependencies = [ - "geo-traits 0.2.0", + "geo-traits", "geo-types", "lru", "rstest", @@ -4923,7 +5001,7 @@ dependencies = [ "serde_with", "thiserror 2.0.16", "wkb", - "wkt 0.13.0", + "wkt 0.14.0", ] [[package]] @@ -4943,7 +5021,7 @@ dependencies = [ "datafusion-physical-expr", "datafusion-physical-plan", "futures", - "geo-traits 0.2.0", + "geo-traits", "object_store", "parquet", "rstest", @@ -4966,9 +5044,12 @@ version = "0.2.0" dependencies = [ "arrow-array", "arrow-schema", + "byteorder", "criterion", "datafusion-common", "datafusion-expr", + "geo-traits", + "geo-types", "geos", "rstest", "sedona", @@ -4991,7 +5072,7 @@ dependencies = [ "criterion", "datafusion-common", "datafusion-expr", - "geo-traits 0.2.0", + "geo-traits", "geo-types", "proj-sys", "rstest", @@ -5056,10 +5137,9 @@ dependencies = [ "datafusion-physical-plan", "float_next_after", "futures", - "geo-generic-alg", + "geo", "geo-index", - "geo-traits 0.2.0", - "geo-traits-ext", + "geo-traits", "geo-types", "geos", "parking_lot", @@ -5069,6 +5149,8 @@ dependencies = [ "sedona-expr", "sedona-functions", "sedona-geo", + "sedona-geo-generic-alg", + "sedona-geo-traits-ext", "sedona-geometry", "sedona-geos", "sedona-schema", @@ -5076,7 +5158,7 @@ dependencies = [ "sedona-tg", "tokio", "wkb", - "wkt 0.13.0", + "wkt 0.14.0", ] [[package]] @@ -5090,7 +5172,8 @@ dependencies = [ "datafusion-common", "datafusion-expr", "datafusion-physical-expr", - "geo-traits 0.2.0", + "geo", + "geo-traits", "geo-types", "parquet", "rand 0.8.5", @@ -5100,7 +5183,7 @@ dependencies = [ "sedona-geometry", "sedona-schema", "wkb", - "wkt 0.13.0", + "wkt 0.14.0", ] [[package]] @@ -5497,6 +5580,15 @@ dependencies = [ "windows-sys 0.61.1", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -6357,14 +6449,11 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "wkb" -version = "0.8.0" -source = "git+https://github.com/wherobots/wkb.git?branch=generic-alg#5496c33919e9193edcde6ccf7dd51a9093782277" +version = "0.9.1" +source = "git+https://github.com/georust/wkb.git?rev=130eb0c2b343bc9299aeafba6d34c2a6e53f3b6a#130eb0c2b343bc9299aeafba6d34c2a6e53f3b6a" dependencies = [ "byteorder", - "geo-traits 0.2.0", - "geo-traits-ext", - "geo-types", - "geos", + "geo-traits", "num_enum", "thiserror 1.0.69", ] @@ -6381,25 +6470,13 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "wkt" -version = "0.13.0" -source = "git+https://github.com/wherobots/wkt.git?branch=generic-alg#ec26b050ec1718ee08e4d8a911e99f1039b60c8b" -dependencies = [ - "geo-traits 0.2.0", - "geo-types", - "log", - "num-traits", - "thiserror 1.0.69", -] - [[package]] name = "wkt" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb2b923ccc882312e559ffaa832a055ba9d1ac0cc8e86b3e25453247e4b81d7" dependencies = [ - "geo-traits 0.3.0", + "geo-traits", "geo-types", "log", "num-traits", diff --git a/Cargo.toml b/Cargo.toml index bdce5422..2bb997e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ members = [ "c/sedona-s2geography", "c/sedona-tg", "r/sedonadb/src/rust", + "rust/sedona-geo-traits-ext", + "rust/sedona-geo-generic-alg", "rust/sedona-adbc", "rust/sedona-expr", "rust/sedona-functions", @@ -54,7 +56,8 @@ rust-version = "1.82" [workspace.dependencies] approx = "0.5" abi_stable = "0.11.3" -adbc_core = { git = "https://github.com/apache/arrow-adbc", rev = "1ba248290cd299c4969b679463bcd54c217cf2e4" } +adbc_core = "0.20.0" +adbc_ffi = "0.20.0" lru = "0.12" arrow = { version = "55.1.0", features = ["prettyprint", "ffi", "chrono-tz"] } arrow-array = { version = "55.1.0" } @@ -64,6 +67,7 @@ arrow-json = { version = "55.1.0" } arrow-schema = { version = "55.1.0" } async-trait = { version = "0.1.87" } bytes = "1.10" +byteorder = "1" chrono = { version = "0.4.38", default-features = false } comfy-table = { version = "7.0" } criterion = { version = "0.5", features = ["html_reports"] } @@ -81,22 +85,20 @@ env_logger = "0.11" futures = { version = "0.3" } object_store = { version = "0.12.0", default-features = false } float_next_after = "1" +num-traits = { version = "0.2", default-features = false, features = ["libm"] } mimalloc = { version = "0.1", default-features = false } libmimalloc-sys = { version = "0.1", default-features = false } -geos = { version = "10.0.0", features = ["geo"] } +geos = { version = "10.0.0", features = ["geo", "v3_10_0"] } -# Use our own fork of georust/geo, which implements generic computational geometry algorithms for geo-traits -geo-generic-alg = { git = "https://github.com/wherobots/geo.git", branch = "generic-alg", package = "geo-generic-alg" } -geo-types = "0.7.16" -geo-traits = "0.2.0" -geo-traits-ext = "0.1.0" -geo = { version = "0.30.0" } +geo-types = "0.7.17" +geo-traits = "0.3.0" +geo = "0.31.0" geo-index = { version = "0.3.1" } -wkb = { version = "0.8.0", features = ["geos"] } -wkt = "0.13.0" +wkb = "0.9.1" +wkt = "0.14.0" parking_lot = "0.12" parquet = { version = "55.1.0", default-features = false, features = [ @@ -127,9 +129,6 @@ datafusion-ffi = { git = "https://github.com/paleolimbot/datafusion.git", branch datafusion-physical-expr = { git = "https://github.com/paleolimbot/datafusion.git", branch = "local-49-with-patch", package = "datafusion-physical-expr" } datafusion-physical-plan = { git = "https://github.com/paleolimbot/datafusion.git", branch = "local-49-with-patch", package = "datafusion-physical-plan" } -geo-types = { git = "https://github.com/wherobots/geo.git", branch = "generic-alg", package = "geo-types" } -geo-traits = { git = "https://github.com/wherobots/geo.git", branch = "generic-alg", package = "geo-traits" } -geo-traits-ext = { git = "https://github.com/wherobots/geo.git", branch = "generic-alg", package = "geo-traits-ext" } -geo-index = { git = "https://github.com/wherobots/geo-index.git", branch = "main" } -wkb = { git = "https://github.com/wherobots/wkb.git", branch = "generic-alg" } -wkt = { git = "https://github.com/wherobots/wkt.git", branch = "generic-alg" } +# TODO: remove them once changes we made to geo-index and wkb crates are merged to upstream and released +geo-index = { git = "https://github.com/wherobots/geo-index.git", branch = "geo-0.31.0" } +wkb = { git = "https://github.com/georust/wkb.git", rev = "130eb0c2b343bc9299aeafba6d34c2a6e53f3b6a" } diff --git a/c/sedona-geos/Cargo.toml b/c/sedona-geos/Cargo.toml index f7de7ee9..d92f076f 100644 --- a/c/sedona-geos/Cargo.toml +++ b/c/sedona-geos/Cargo.toml @@ -31,6 +31,7 @@ criterion = { workspace = true } sedona = { path = "../../rust/sedona" } sedona-testing = { path = "../../rust/sedona-testing", features = ["criterion"] } rstest = { workspace = true } +geo-types = { workspace = true } [dependencies] arrow-schema = { workspace = true } @@ -42,8 +43,14 @@ sedona-expr = { path = "../../rust/sedona-expr" } sedona-functions = { path = "../../rust/sedona-functions" } sedona-geometry = { path = "../../rust/sedona-geometry" } sedona-schema = { path = "../../rust/sedona-schema" } -wkb = { workspace = true, features = ["geos"] } +geo-traits = { workspace = true } +wkb = { workspace = true } +byteorder = { workspace = true } [[bench]] harness = false name = "geos-functions" + +[[bench]] +harness = false +name = "wkb_to_geos" diff --git a/c/sedona-geos/benches/wkb_to_geos.rs b/c/sedona-geos/benches/wkb_to_geos.rs new file mode 100644 index 00000000..cd2b9dbb --- /dev/null +++ b/c/sedona-geos/benches/wkb_to_geos.rs @@ -0,0 +1,77 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use criterion::{criterion_group, criterion_main}; +use geo_types::{LineString, Point}; +use sedona_geos::wkb_to_geos::GEOSWkbFactory; +use wkb::Endianness; + +fn generate_wkb_linestring(num_points: usize, endianness: Endianness) -> Vec { + let mut points = Vec::new(); + for i in 0..num_points { + points.push(Point::new(i as f64, i as f64)); + } + let linestring = LineString::from(points); + let mut buffer = Vec::new(); + wkb::writer::write_geometry( + &mut buffer, + &linestring, + &wkb::writer::WriteOptions { endianness }, + ) + .unwrap(); + buffer +} + +fn bench_parse(c: &mut criterion::Criterion) { + for num_points in [4, 10, 100, 500, 1000] { + for endianness in [Endianness::BigEndian, Endianness::LittleEndian] { + let wkb_buf = generate_wkb_linestring(num_points, endianness); + let wkb = wkb::reader::read_wkb(&wkb_buf).unwrap(); + let endianness_name: &str = match endianness { + Endianness::BigEndian => "big endian", + Endianness::LittleEndian => "little endian", + }; + + c.bench_function( + &format!( + "convert linestring containing {num_points} points using to_geos ({endianness_name})" + ), + |b| { + let factory = GEOSWkbFactory::new(); + b.iter(|| { + let g = factory.create(&wkb).unwrap(); + criterion::black_box(g); + }); + }, + ); + + c.bench_function( + &format!( + "convert linestring containing {num_points} points using geos wkb parser ({endianness_name})" + ), + |b| { + b.iter(|| { + let g = geos::Geometry::new_from_wkb(wkb.buf()).unwrap(); + criterion::black_box(g); + }); + }, + ); + } + } +} + +criterion_group!(benches, bench_parse); +criterion_main!(benches); diff --git a/c/sedona-geos/src/executor.rs b/c/sedona-geos/src/executor.rs index 3d806cc3..5e8a021d 100644 --- a/c/sedona-geos/src/executor.rs +++ b/c/sedona-geos/src/executor.rs @@ -17,13 +17,15 @@ use datafusion_common::{DataFusionError, Result}; use sedona_functions::executor::{GenericExecutor, GeometryFactory}; +use crate::wkb_to_geos::GEOSWkbFactory; + /// A [GenericExecutor] that iterates over [geos::Geometry] pub type GeosExecutor<'a, 'b> = GenericExecutor<'a, 'b, GeosGeometryFactory, GeosGeometryFactory>; /// [GeometryFactory] implementation for iterating over [geos::Geometry] #[derive(Default)] pub struct GeosGeometryFactory { - inner: wkb::reader::to_geos::GEOSWkbFactory, + inner: GEOSWkbFactory, } impl GeometryFactory for GeosGeometryFactory { diff --git a/c/sedona-geos/src/lib.rs b/c/sedona-geos/src/lib.rs index 667bc823..74d57b94 100644 --- a/c/sedona-geos/src/lib.rs +++ b/c/sedona-geos/src/lib.rs @@ -27,3 +27,4 @@ mod st_convexhull; mod st_dwithin; mod st_length; mod st_perimeter; +pub mod wkb_to_geos; diff --git a/c/sedona-geos/src/wkb_to_geos.rs b/c/sedona-geos/src/wkb_to_geos.rs new file mode 100644 index 00000000..9bd83507 --- /dev/null +++ b/c/sedona-geos/src/wkb_to_geos.rs @@ -0,0 +1,1335 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use std::cell::RefCell; + +use byteorder::{BigEndian, ByteOrder, LittleEndian}; +use geo_traits::*; +use geos::GResult; +use wkb::{reader::*, Endianness}; + +/// A factory for converting WKB to GEOS geometries. +/// +/// This factory uses a scratch buffer to store intermediate coordinate data. +/// The scratch buffer is reused for each conversion, which reduces memory allocation +/// overhead. +pub struct GEOSWkbFactory { + scratch: RefCell>, +} + +impl Default for GEOSWkbFactory { + fn default() -> Self { + Self::new() + } +} + +impl GEOSWkbFactory { + /// Create a new GEOSWkbFactory. + pub fn new() -> Self { + Self { + scratch: RefCell::new(Vec::new()), + } + } + + /// Create a GEOS geometry from a WKB. + pub fn create(&self, wkb: &Wkb) -> GResult { + let scratch = &mut self.scratch.borrow_mut(); + geometry_to_geos(scratch, wkb) + } +} + +fn geometry_to_geos(scratch: &mut Vec, wkb: &Wkb) -> GResult { + let geom = wkb.as_type(); + match geom { + geo_traits::GeometryType::Point(p) => point_to_geos(scratch, p), + geo_traits::GeometryType::LineString(ls) => line_string_to_geos(scratch, ls), + geo_traits::GeometryType::Polygon(poly) => polygon_to_geos(scratch, poly), + geo_traits::GeometryType::MultiPoint(mp) => multi_point_to_geos(scratch, mp), + geo_traits::GeometryType::MultiLineString(mls) => multi_line_string_to_geos(scratch, mls), + geo_traits::GeometryType::MultiPolygon(mpoly) => multi_polygon_to_geos(scratch, mpoly), + geo_traits::GeometryType::GeometryCollection(gc) => { + geometry_collection_to_geos(scratch, gc) + } + _ => Err(geos::Error::ConversionError( + "Unsupported geometry type".to_string(), + )), + } +} + +fn point_to_geos(scratch: &mut Vec, p: &Point) -> GResult { + if p.is_empty() { + geos::Geometry::create_empty_point() + } else { + let coord_seq = create_coord_sequence_from_raw_parts( + p.coord_slice(), + p.dimension(), + p.byte_order(), + 1, + scratch, + )?; + let point = geos::Geometry::create_point(coord_seq)?; + Ok(point) + } +} + +fn line_string_to_geos(scratch: &mut Vec, ls: &LineString) -> GResult { + let num_points = ls.num_coords(); + if num_points == 0 { + geos::Geometry::create_empty_line_string() + } else { + let coord_seq = create_coord_sequence_from_raw_parts( + ls.coords_slice(), + ls.dimension(), + ls.byte_order(), + num_points, + scratch, + )?; + geos::Geometry::create_line_string(coord_seq) + } +} + +fn polygon_to_geos(scratch: &mut Vec, poly: &Polygon) -> GResult { + // Create exterior ring + let exterior = if let Some(ring) = poly.exterior() { + let coord_seq = create_coord_sequence_from_raw_parts( + ring.coords_slice(), + ring.dimension(), + ring.byte_order(), + ring.num_coords(), + scratch, + )?; + geos::Geometry::create_linear_ring(coord_seq)? + } else { + return geos::Geometry::create_empty_polygon(); + }; + + // Create interior rings + let num_interiors = poly.num_interiors(); + let mut interior_rings = Vec::with_capacity(num_interiors); + for i in 0..num_interiors { + let ring = poly.interior(i).unwrap(); + let coord_seq = create_coord_sequence_from_raw_parts( + ring.coords_slice(), + ring.dimension(), + ring.byte_order(), + ring.num_coords(), + scratch, + )?; + let interior_ring = geos::Geometry::create_linear_ring(coord_seq)?; + interior_rings.push(interior_ring); + } + + geos::Geometry::create_polygon(exterior, interior_rings) +} + +fn multi_point_to_geos(scratch: &mut Vec, mp: &MultiPoint) -> GResult { + let num_points = mp.num_points(); + if num_points == 0 { + // Create an empty multi-point by creating a geometry collection with no geometries + geos::Geometry::create_empty_collection(geos::GeometryTypes::MultiPoint) + } else { + let mut points = Vec::with_capacity(num_points); + for i in 0..num_points { + let point = unsafe { mp.point_unchecked(i) }; + let geos_point = point_to_geos(scratch, &point)?; + points.push(geos_point); + } + geos::Geometry::create_multipoint(points) + } +} + +fn multi_line_string_to_geos( + scratch: &mut Vec, + mls: &MultiLineString, +) -> GResult { + let num_line_strings = mls.num_line_strings(); + if num_line_strings == 0 { + geos::Geometry::create_empty_collection(geos::GeometryTypes::MultiLineString) + } else { + let mut line_strings = Vec::with_capacity(num_line_strings); + for i in 0..num_line_strings { + let ls = unsafe { mls.line_string_unchecked(i) }; + let geos_line_string = line_string_to_geos(scratch, ls)?; + line_strings.push(geos_line_string); + } + geos::Geometry::create_multiline_string(line_strings) + } +} + +fn multi_polygon_to_geos(scratch: &mut Vec, mpoly: &MultiPolygon) -> GResult { + let num_polygons = mpoly.num_polygons(); + if num_polygons == 0 { + geos::Geometry::create_empty_collection(geos::GeometryTypes::MultiPolygon) + } else { + let mut polygons = Vec::with_capacity(num_polygons); + for i in 0..num_polygons { + let poly = unsafe { mpoly.polygon_unchecked(i) }; + let geos_polygon = polygon_to_geos(scratch, poly)?; + polygons.push(geos_polygon); + } + geos::Geometry::create_multipolygon(polygons) + } +} + +fn geometry_collection_to_geos( + scratch: &mut Vec, + gc: &GeometryCollection, +) -> GResult { + if gc.num_geometries() == 0 { + geos::Geometry::create_empty_collection(geos::GeometryTypes::GeometryCollection) + } else { + let num_geometries = gc.num_geometries(); + let mut geometries = Vec::with_capacity(num_geometries); + for i in 0..num_geometries { + let geom = gc.geometry(i).unwrap(); + let geos_geom = geometry_to_geos(scratch, geom)?; + geometries.push(geos_geom); + } + geos::Geometry::create_geometry_collection(geometries) + } +} + +const NATIVE_ENDIANNESS: Endianness = if cfg!(target_endian = "big") { + Endianness::BigEndian +} else { + Endianness::LittleEndian +}; + +fn create_coord_sequence_from_raw_parts( + buf: &[u8], + dim: Dimension, + byte_order: Endianness, + num_coords: usize, + scratch: &mut Vec, +) -> GResult { + let (has_z, has_m, dim_size) = match dim { + Dimension::Xy => (false, false, 2), + Dimension::Xyz => (true, false, 3), + Dimension::Xym => (false, true, 3), + Dimension::Xyzm => (true, true, 4), + }; + let num_ordinates = dim_size * num_coords; + + // If the byte order matches native endianness, we can potentially use zero-copy + if byte_order == NATIVE_ENDIANNESS { + let ptr = buf.as_ptr(); + + // On platforms with unaligned memory access support, we can construct the coord seq + // directly from the raw parts without copying to the scratch buffer. + #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] + { + let coords_f64 = + unsafe { &*core::ptr::slice_from_raw_parts(ptr as *const f64, num_ordinates) }; + geos::CoordSeq::new_from_buffer(coords_f64, num_coords, has_z, has_m) + } + + // On platforms without unaligned memory access support, we need to copy the data to the + // scratch buffer to make sure the data is aligned. + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + { + unsafe { + scratch.clear(); + scratch.reserve(num_ordinates); + scratch.set_len(num_ordinates); + std::ptr::copy_nonoverlapping( + ptr, + scratch.as_mut_ptr() as *mut u8, + num_ordinates * std::mem::size_of::(), + ); + geos::CoordSeq::new_from_buffer(scratch.as_slice(), num_coords, has_z, has_m) + } + } + } else { + // Need to convert byte order + match byte_order { + Endianness::BigEndian => { + save_f64_to_scratch::(scratch, buf, num_ordinates); + } + Endianness::LittleEndian => { + save_f64_to_scratch::(scratch, buf, num_ordinates); + } + } + geos::CoordSeq::new_from_buffer(scratch.as_slice(), num_coords, has_z, has_m) + } +} + +fn save_f64_to_scratch(scratch: &mut Vec, buf: &[u8], num_ordinates: usize) { + scratch.clear(); + scratch.reserve(num_ordinates); + // Safety: we have already reserved the capacity, so we can set the length safely. + // Justification: rewriting the loop to not use Vec::push makes it many times faster, + // since it eliminates several memory loads and stores for vector's length and capacity, + // and it enables the compiler to generate vectorized code. + #[allow(clippy::uninit_vec)] + unsafe { + scratch.set_len(num_ordinates); + } + assert!(num_ordinates * 8 <= buf.len()); + for (i, tgt) in scratch.iter_mut().enumerate().take(num_ordinates) { + let offset = i * 8; + let value = B::read_f64(&buf[offset..]); + *tgt = value; + } +} + +#[cfg(test)] +mod test { + use super::*; + use geo_types::{ + line_string, point, polygon, Geometry, GeometryCollection, LineString, MultiLineString, + MultiPoint, MultiPolygon, Point, Polygon, + }; + use geos::Geom; + use wkb::{ + writer::{ + write_geometry_collection, write_line_string, write_multi_line_string, + write_multi_point, write_multi_polygon, write_point, write_polygon, WriteOptions, + }, + Endianness, + }; + + pub(super) fn point_2d() -> Point { + point!( + x: 0., y: 1. + ) + } + + pub(super) fn linestring_2d() -> LineString { + line_string![ + (x: 0., y: 1.), + (x: 1., y: 2.) + ] + } + + pub(super) fn polygon_2d() -> Polygon { + polygon![ + (x: -111., y: 45.), + (x: -111., y: 41.), + (x: -104., y: 41.), + (x: -104., y: 45.), + ] + } + + pub(super) fn polygon_2d_with_interior() -> Polygon { + polygon!( + exterior: [ + (x: -111., y: 45.), + (x: -111., y: 41.), + (x: -104., y: 41.), + (x: -104., y: 45.), + ], + interiors: [ + [ + (x: -110., y: 44.), + (x: -110., y: 42.), + (x: -105., y: 42.), + (x: -105., y: 44.), + ], + ], + ) + } + + pub(super) fn multi_point_2d() -> MultiPoint { + MultiPoint::new(vec![ + point!( + x: 0., y: 1. + ), + point!( + x: 1., y: 2. + ), + ]) + } + + pub(super) fn multi_line_string_2d() -> MultiLineString { + MultiLineString::new(vec![ + line_string![ + (x: -111., y: 45.), + (x: -111., y: 41.), + (x: -104., y: 41.), + (x: -104., y: 45.), + ], + line_string![ + (x: -110., y: 44.), + (x: -110., y: 42.), + (x: -105., y: 42.), + (x: -105., y: 44.), + ], + ]) + } + + pub(super) fn multi_polygon_2d() -> MultiPolygon { + MultiPolygon::new(vec![ + polygon![ + (x: -111., y: 45.), + (x: -111., y: 41.), + (x: -104., y: 41.), + (x: -104., y: 45.), + ], + polygon!( + exterior: [ + (x: -111., y: 45.), + (x: -111., y: 41.), + (x: -104., y: 41.), + (x: -104., y: 45.), + ], + interiors: [ + [ + (x: -110., y: 44.), + (x: -110., y: 42.), + (x: -105., y: 42.), + (x: -105., y: 44.), + ], + ], + ), + ]) + } + + pub(super) fn geometry_collection_2d() -> GeometryCollection { + GeometryCollection::new_from(vec![ + Geometry::Point(point_2d()), + Geometry::LineString(linestring_2d()), + Geometry::Polygon(polygon_2d()), + Geometry::Polygon(polygon_2d_with_interior()), + Geometry::MultiPoint(multi_point_2d()), + Geometry::MultiLineString(multi_line_string_2d()), + Geometry::MultiPolygon(multi_polygon_2d()), + ]) + } + + fn test_geometry_conversion(geo_geom: &Geometry, endianness: Endianness) { + // Convert geo geometry to WKB + let mut buf = Vec::new(); + let write_options = WriteOptions { endianness }; + match geo_geom { + Geometry::Point(p) => write_point(&mut buf, p, &write_options).unwrap(), + Geometry::LineString(ls) => write_line_string(&mut buf, ls, &write_options).unwrap(), + Geometry::Polygon(p) => write_polygon(&mut buf, p, &write_options).unwrap(), + Geometry::MultiPoint(mp) => write_multi_point(&mut buf, mp, &write_options).unwrap(), + Geometry::MultiLineString(mls) => { + write_multi_line_string(&mut buf, mls, &write_options).unwrap() + } + Geometry::MultiPolygon(mp) => { + write_multi_polygon(&mut buf, mp, &write_options).unwrap() + } + Geometry::GeometryCollection(gc) => { + write_geometry_collection(&mut buf, gc, &write_options).unwrap() + } + Geometry::Line(_) => panic!("Line geometry not supported in tests"), + Geometry::Rect(_) => panic!("Rect geometry not supported in tests"), + Geometry::Triangle(_) => panic!("Triangle geometry not supported in tests"), + } + + // Read WKB back + let wkb = wkb::reader::read_wkb(&buf).unwrap(); + + // Convert to GEOS using our ToGeos converter + let geos_geom = GEOSWkbFactory::new().create(&wkb).unwrap(); + + // Convert back to geo for comparison + let geo_from_geos: Geometry = geos_geom.try_into().unwrap(); + + // Compare the geometries + assert_eq!(*geo_geom, geo_from_geos); + } + + #[test] + fn test_point_conversion() { + let point = point_2d(); + let geo_geom = Geometry::Point(point); + + test_geometry_conversion(&geo_geom, Endianness::LittleEndian); + test_geometry_conversion(&geo_geom, Endianness::BigEndian); + } + + #[test] + fn test_empty_point_conversion() { + // Create an empty point by writing NaN coordinates + let mut buf = Vec::new(); + buf.push(1); // Little endian + buf.extend_from_slice(&1u32.to_le_bytes()); // Point type + buf.extend_from_slice(&f64::NAN.to_le_bytes()); // x = NaN + buf.extend_from_slice(&f64::NAN.to_le_bytes()); // y = NaN + + let wkb = read_wkb(&buf).unwrap(); + let geos_geom = GEOSWkbFactory::new().create(&wkb).unwrap(); + + assert!(geos_geom.is_empty().unwrap()); + } + + #[test] + fn test_line_string_conversion() { + let line_string = linestring_2d(); + let geo_geom = Geometry::LineString(line_string); + + test_geometry_conversion(&geo_geom, Endianness::LittleEndian); + test_geometry_conversion(&geo_geom, Endianness::BigEndian); + } + + #[test] + fn test_empty_line_string_conversion() { + let mut buf = Vec::new(); + write_line_string( + &mut buf, + &LineString::new(vec![]), + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); + + let wkb = read_wkb(&buf).unwrap(); + let geos_geom = GEOSWkbFactory::new().create(&wkb).unwrap(); + + assert!(geos_geom.is_empty().unwrap()); + } + + #[test] + fn test_polygon_conversion() { + let polygon = polygon_2d(); + let geo_geom = Geometry::Polygon(polygon); + + test_geometry_conversion(&geo_geom, Endianness::LittleEndian); + test_geometry_conversion(&geo_geom, Endianness::BigEndian); + } + + #[test] + fn test_polygon_with_interior_conversion() { + let polygon = polygon_2d_with_interior(); + let geo_geom = Geometry::Polygon(polygon); + + test_geometry_conversion(&geo_geom, Endianness::LittleEndian); + test_geometry_conversion(&geo_geom, Endianness::BigEndian); + } + + #[test] + fn test_multi_point_conversion() { + let multi_point = multi_point_2d(); + let geo_geom = Geometry::MultiPoint(multi_point); + + test_geometry_conversion(&geo_geom, Endianness::LittleEndian); + test_geometry_conversion(&geo_geom, Endianness::BigEndian); + } + + #[test] + fn test_empty_multi_point_conversion() { + let mut buf = Vec::new(); + write_multi_point( + &mut buf, + &MultiPoint::new(vec![]), + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); + + let wkb = read_wkb(&buf).unwrap(); + let geos_geom = GEOSWkbFactory::new().create(&wkb).unwrap(); + + assert!(geos_geom.is_empty().unwrap()); + } + + #[test] + fn test_multi_line_string_conversion() { + let multi_line_string = multi_line_string_2d(); + let geo_geom = Geometry::MultiLineString(multi_line_string); + + test_geometry_conversion(&geo_geom, Endianness::LittleEndian); + test_geometry_conversion(&geo_geom, Endianness::BigEndian); + } + + #[test] + fn test_empty_multi_line_string_conversion() { + let mut buf = Vec::new(); + write_multi_line_string( + &mut buf, + &MultiLineString::new(vec![]), + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); + + let wkb = read_wkb(&buf).unwrap(); + let geos_geom = GEOSWkbFactory::new().create(&wkb).unwrap(); + + assert!(geos_geom.is_empty().unwrap()); + } + + #[test] + fn test_multi_polygon_conversion() { + let multi_polygon = multi_polygon_2d(); + let geo_geom = Geometry::MultiPolygon(multi_polygon); + + test_geometry_conversion(&geo_geom, Endianness::LittleEndian); + test_geometry_conversion(&geo_geom, Endianness::BigEndian); + } + + #[test] + fn test_empty_multi_polygon_conversion() { + let mut buf = Vec::new(); + write_multi_polygon( + &mut buf, + &MultiPolygon::new(vec![]), + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); + + let wkb = read_wkb(&buf).unwrap(); + let geos_geom = GEOSWkbFactory::new().create(&wkb).unwrap(); + + assert!(geos_geom.is_empty().unwrap()); + } + + #[test] + fn test_geometry_collection_conversion() { + let geometry_collection = geometry_collection_2d(); + let geo_geom = Geometry::GeometryCollection(geometry_collection); + + test_geometry_conversion(&geo_geom, Endianness::LittleEndian); + test_geometry_conversion(&geo_geom, Endianness::BigEndian); + } + + #[test] + fn test_empty_geometry_collection_conversion() { + let mut buf = Vec::new(); + write_geometry_collection( + &mut buf, + &GeometryCollection::new_from(vec![]), + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); + + let wkb = read_wkb(&buf).unwrap(); + let geos_geom = GEOSWkbFactory::new().create(&wkb).unwrap(); + + assert!(geos_geom.is_empty().unwrap()); + } + + #[test] + fn test_nested_geometry_collection() { + // Create a geometry collection containing other geometry collections + let inner_gc1 = GeometryCollection::new_from(vec![ + Geometry::Point(point_2d()), + Geometry::LineString(linestring_2d()), + ]); + + let inner_gc2 = GeometryCollection::new_from(vec![ + Geometry::Polygon(polygon_2d()), + Geometry::MultiPoint(multi_point_2d()), + ]); + + let outer_gc = GeometryCollection::new_from(vec![ + Geometry::GeometryCollection(inner_gc1), + Geometry::GeometryCollection(inner_gc2), + Geometry::MultiLineString(multi_line_string_2d()), + ]); + + let geo_geom = Geometry::GeometryCollection(outer_gc); + + test_geometry_conversion(&geo_geom, Endianness::LittleEndian); + test_geometry_conversion(&geo_geom, Endianness::BigEndian); + } + + #[test] + fn test_coordinate_precision() { + // Test with high precision coordinates + let high_precision_point = Point::new(123.456789012345, -98.765432109876); + let geo_geom = Geometry::Point(high_precision_point); + + test_geometry_conversion(&geo_geom, Endianness::LittleEndian); + test_geometry_conversion(&geo_geom, Endianness::BigEndian); + } + + #[test] + fn test_large_coordinates() { + // Test with very large coordinate values + let large_point = Point::new(1e10, -1e10); + let geo_geom = Geometry::Point(large_point); + + test_geometry_conversion(&geo_geom, Endianness::LittleEndian); + test_geometry_conversion(&geo_geom, Endianness::BigEndian); + } + + #[test] + fn test_negative_coordinates() { + // Test with negative coordinates + let negative_point = Point::new(-180.0, -90.0); + let geo_geom = Geometry::Point(negative_point); + + test_geometry_conversion(&geo_geom, Endianness::LittleEndian); + test_geometry_conversion(&geo_geom, Endianness::BigEndian); + } + + #[test] + fn test_zero_coordinates() { + // Test with zero coordinates + let zero_point = Point::new(0.0, 0.0); + let geo_geom = Geometry::Point(zero_point); + + test_geometry_conversion(&geo_geom, Endianness::LittleEndian); + test_geometry_conversion(&geo_geom, Endianness::BigEndian); + } + + #[test] + fn test_endianness_handling() { + let factory = GEOSWkbFactory::new(); + // Test that both endianness variants work correctly + let point = point_2d(); + let geo_geom = Geometry::Point(point); + + // Test little endian + let mut buf_le = Vec::new(); + write_point( + &mut buf_le, + &point_2d(), + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); + let wkb_le = read_wkb(&buf_le).unwrap(); + let geos_geom_le = factory.create(&wkb_le).unwrap(); + let geo_from_geos_le: Geometry = geos_geom_le.try_into().unwrap(); + + // Test big endian + let mut buf_be = Vec::new(); + write_point( + &mut buf_be, + &point_2d(), + &WriteOptions { + endianness: Endianness::BigEndian, + }, + ) + .unwrap(); + let wkb_be = read_wkb(&buf_be).unwrap(); + let geos_geom_be = factory.create(&wkb_be).unwrap(); + let geo_from_geos_be: Geometry = geos_geom_be.try_into().unwrap(); + + // Both should produce the same result + assert_eq!(geo_from_geos_le, geo_from_geos_be); + assert_eq!(geo_geom, geo_from_geos_le); + } + + #[test] + fn test_xyz_dimension_handling() { + // Test XYZ dimension handling by manually creating WKB with XYZ coordinates + let mut buf = Vec::new(); + + // Write WKB header for LineString XYZ (type 1002) + buf.push(1); // Little endian + buf.extend_from_slice(&1002u32.to_le_bytes()); // LineString XYZ + buf.extend_from_slice(&2u32.to_le_bytes()); // 2 points + + // Write XYZ coordinates: (0.0, 1.0, 10.0), (1.0, 2.0, 20.0) + buf.extend_from_slice(&0.0f64.to_le_bytes()); + buf.extend_from_slice(&1.0f64.to_le_bytes()); + buf.extend_from_slice(&10.0f64.to_le_bytes()); + buf.extend_from_slice(&1.0f64.to_le_bytes()); + buf.extend_from_slice(&2.0f64.to_le_bytes()); + buf.extend_from_slice(&20.0f64.to_le_bytes()); + + let wkb = read_wkb(&buf).unwrap(); + let geos_geom = GEOSWkbFactory::new().create(&wkb).unwrap(); + + // Verify the geometry was created successfully + assert!(!geos_geom.is_empty().unwrap()); + + // Verify coordinates by checking the WKT representation + let wkt = geos_geom.to_wkt().unwrap(); + // Expected WKT for LineString with XYZ coordinates (0.0, 1.0, 10.0), (1.0, 2.0, 20.0) + let expected_wkt = "LINESTRING Z (0 1 10, 1 2 20)"; + assert_eq!(wkt, expected_wkt); + } + + #[test] + fn test_xym_dimension_handling() { + // Test XYM dimension handling by manually creating WKB with XYM coordinates + let mut buf = Vec::new(); + + // Write WKB header for LineString XYM (type 2002) + buf.push(1); // Little endian + buf.extend_from_slice(&2002u32.to_le_bytes()); // LineString XYM + buf.extend_from_slice(&2u32.to_le_bytes()); // 2 points + + // Write XYM coordinates: (0.0, 1.0, 100.0), (1.0, 2.0, 200.0) + buf.extend_from_slice(&0.0f64.to_le_bytes()); + buf.extend_from_slice(&1.0f64.to_le_bytes()); + buf.extend_from_slice(&100.0f64.to_le_bytes()); + buf.extend_from_slice(&1.0f64.to_le_bytes()); + buf.extend_from_slice(&2.0f64.to_le_bytes()); + buf.extend_from_slice(&200.0f64.to_le_bytes()); + + let wkb = read_wkb(&buf).unwrap(); + let geos_geom = GEOSWkbFactory::new().create(&wkb).unwrap(); + + // Verify the geometry was created successfully + assert!(!geos_geom.is_empty().unwrap()); + + // Verify coordinates by checking the WKT representation + let wkt = geos_geom.to_wkt().unwrap(); + // Expected WKT for LineString with XYM coordinates (0.0, 1.0, 100.0), (1.0, 2.0, 200.0) + let expected_wkt = "LINESTRING M (0 1 100, 1 2 200)"; + assert_eq!(wkt, expected_wkt); + } + + #[test] + fn test_xyzm_dimension_handling() { + // Test XYZM dimension handling by manually creating WKB with XYZM coordinates + let mut buf = Vec::new(); + + // Write WKB header for LineString XYZM (type 3002) + buf.push(1); // Little endian + buf.extend_from_slice(&3002u32.to_le_bytes()); // LineString XYZM + buf.extend_from_slice(&2u32.to_le_bytes()); // 2 points + + // Write XYZM coordinates: (0.0, 1.0, 10.0, 100.0), (1.0, 2.0, 20.0, 200.0) + buf.extend_from_slice(&0.0f64.to_le_bytes()); + buf.extend_from_slice(&1.0f64.to_le_bytes()); + buf.extend_from_slice(&10.0f64.to_le_bytes()); + buf.extend_from_slice(&100.0f64.to_le_bytes()); + buf.extend_from_slice(&1.0f64.to_le_bytes()); + buf.extend_from_slice(&2.0f64.to_le_bytes()); + buf.extend_from_slice(&20.0f64.to_le_bytes()); + buf.extend_from_slice(&200.0f64.to_le_bytes()); + + let wkb = read_wkb(&buf).unwrap(); + let geos_geom = GEOSWkbFactory::new().create(&wkb).unwrap(); + + // Verify the geometry was created successfully + assert!(!geos_geom.is_empty().unwrap()); + + // Verify coordinates by checking the WKT representation + let wkt = geos_geom.to_wkt().unwrap(); + // Expected WKT for LineString with XYZM coordinates (0.0, 1.0, 10.0, 100.0), (1.0, 2.0, 20.0, 200.0) + let expected_wkt = "LINESTRING ZM (0 1 10 100, 1 2 20 200)"; + assert_eq!(wkt, expected_wkt); + } + + #[test] + fn test_big_endian_xyz_dimension_handling() { + // Test XYZ dimension handling with big endian byte order + let mut buf = Vec::new(); + + // Write WKB header for LineString XYZ (type 1002) in big endian + buf.push(0); // Big endian + buf.extend_from_slice(&1002u32.to_be_bytes()); // LineString XYZ + buf.extend_from_slice(&2u32.to_be_bytes()); // 2 points + + // Write XYZ coordinates in big endian: (0.0, 1.0, 10.0), (1.0, 2.0, 20.0) + buf.extend_from_slice(&0.0f64.to_be_bytes()); + buf.extend_from_slice(&1.0f64.to_be_bytes()); + buf.extend_from_slice(&10.0f64.to_be_bytes()); + buf.extend_from_slice(&1.0f64.to_be_bytes()); + buf.extend_from_slice(&2.0f64.to_be_bytes()); + buf.extend_from_slice(&20.0f64.to_be_bytes()); + + let wkb = read_wkb(&buf).unwrap(); + let geos_geom = GEOSWkbFactory::new().create(&wkb).unwrap(); + + // Verify the geometry was created successfully + assert!(!geos_geom.is_empty().unwrap()); + + // Verify coordinates by checking the WKT representation + let wkt = geos_geom.to_wkt().unwrap(); + // Expected WKT for LineString with XYZ coordinates (0.0, 1.0, 10.0), (1.0, 2.0, 20.0) + let expected_wkt = "LINESTRING Z (0 1 10, 1 2 20)"; + assert_eq!(wkt, expected_wkt); + } + + /// Represents a single WKB test case, holding the expected geometry type, Dimension, + /// the raw WKB bytes, and the WKT string. + /// This is the direct Rust equivalent of your C++ `WKBTestCase` struct, with WKT added. + #[derive(Debug, PartialEq, Clone)] + pub struct WkbTestCase { + pub dimension: Dimension, + pub wkb_bytes: Vec, + pub wkt_string: String, // Added WKT field + } + + // You can then define your test cases as a `Vec` + pub fn get_wkb_test_cases() -> Vec { + vec![ + // POINT EMPTY + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![ + 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x7f, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x7f, + ], + wkt_string: "POINT EMPTY".to_string(), + }, + // POINT (30 10) + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![ + 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, + ], + wkt_string: "POINT (30 10)".to_string(), + }, + // POINT Z (30 10 40) + WkbTestCase { + dimension: Dimension::Xyz, + wkb_bytes: vec![ + 0x01, 0xe9, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x44, 0x40, + ], + wkt_string: "POINT Z (30 10 40)".to_string(), + }, + // POINT M (30 10 300) + WkbTestCase { + dimension: Dimension::Xym, + wkb_bytes: vec![ + 0x01, 0xd1, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xc0, 0x72, 0x40, + ], + wkt_string: "POINT M (30 10 300)".to_string(), + }, + // POINT ZM (30 10 40 300) + WkbTestCase { + dimension: Dimension::Xyzm, + wkb_bytes: vec![ + 0x01, 0xb9, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x72, 0x40, + ], + wkt_string: "POINT ZM (30 10 40 300)".to_string(), + }, + // POINT (30 10) (big endian) + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![ + 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + wkt_string: "POINT (30 10)".to_string(), // WKT is endian-agnostic + }, + // LINESTRING EMPTY + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + wkt_string: "LINESTRING EMPTY".to_string(), + }, + // LINESTRING (30 10, 10 30, 40 40) + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![ + 0x01, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x44, 0x40, + ], + wkt_string: "LINESTRING (30 10, 10 30, 40 40)".to_string(), + }, + // LINESTRING Z (30 10 40, 10 30 40, 40 40 80) + WkbTestCase { + dimension: Dimension::Xyz, + wkb_bytes: vec![ + 0x01, 0xea, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x54, 0x40 + ], + wkt_string: "LINESTRING Z (30 10 40, 10 30 40, 40 40 80)".to_string(), + }, + // LINESTRING M (30 10 300, 10 30 300, 40 40 1600) + WkbTestCase { + dimension: Dimension::Xym, + wkb_bytes: vec![ + 0x01, 0xd2, 0x07, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc0, 0x72, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xc0, 0x72, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x99, 0x40, + ], + wkt_string: "LINESTRING M (30 10 300, 10 30 300, 40 40 1600)".to_string(), + }, + // LINESTRING ZM (30 10 40 300, 10 30 40 300, 40 40 80 1600) + WkbTestCase { + dimension: Dimension::Xyzm, + wkb_bytes: vec![ + 0x01, 0xba, 0x0b, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x72, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc0, 0x72, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x99, 0x40, + ], + wkt_string: "LINESTRING ZM (30 10 40 300, 10 30 40 300, 40 40 80 1600)".to_string(), + }, + // LINESTRING (30 10, 10 30, 40 40) (big endian) + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![ + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x40, 0x3e, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x40, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, + 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x3e, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x40, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x44, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + ], + wkt_string: "LINESTRING (30 10, 10 30, 40 40)".to_string(), + }, + // POLYGON EMPTY + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + wkt_string: "POLYGON EMPTY".to_string(), + }, + // POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10)) + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![ + 0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x24, 0x40, + ], + wkt_string: "POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))".to_string(), + }, + // POLYGON Z ((30 10 40, 40 40 80, 20 40 60, 10 20 30, 30 10 40)) + WkbTestCase { + dimension: Dimension::Xyz, + wkb_bytes: vec![ + 0x01, 0xeb, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x34, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x4e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x44, 0x40, + ], + wkt_string: "POLYGON Z ((30 10 40, 40 40 80, 20 40 60, 10 20 30, 30 10 40))".to_string(), + }, + // POLYGON M ((30 10 300, 40 40 1600, 20 40 800, 10 20 200, 30 10 300)) + WkbTestCase { + dimension: Dimension::Xym, + wkb_bytes: vec![ + 0x01, 0xd3, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x72, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x99, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x34, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x89, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x69, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xc0, 0x72, 0x40, + ], + wkt_string: "POLYGON M ((30 10 300, 40 40 1600, 20 40 800, 10 20 200, 30 10 300))".to_string(), + }, + // POLYGON ZM ((30 10 40 300, 40 40 80 1600, 20 40 60 800, 10 20 30 200, 30 + // 10 40 300)) + WkbTestCase { + dimension: Dimension::Xyzm, + wkb_bytes: vec![ + 0x01, 0xbb, 0x0b, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xc0, 0x72, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x54, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x99, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4e, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x89, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xc0, 0x72, 0x40, + ], + wkt_string: "POLYGON ZM ((30 10 40 300, 40 40 80 1600, 20 40 60 800, 10 20 30 200, 30 10 40 300))".to_string(), + }, + // MULTIPOINT EMPTY + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + wkt_string: "MULTIPOINT EMPTY".to_string(), + }, + // MULTIPOINT ((30 10)) + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![ + 0x01, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x40, + ], + wkt_string: "MULTIPOINT ((30 10))".to_string(), + }, + // MULTIPOINT Z ((30 10 40)) + WkbTestCase { + dimension: Dimension::Xyz, + wkb_bytes: vec![ + 0x01, 0xec, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xe9, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, + ], + wkt_string: "MULTIPOINT Z ((30 10 40))".to_string(), + }, + // MULTIPOINT M ((30 10 300)) + WkbTestCase { + dimension: Dimension::Xym, + wkb_bytes: vec![ + 0x01, 0xd4, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xd1, 0x07, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x72, 0x40, + ], + wkt_string: "MULTIPOINT M ((30 10 300))".to_string(), + }, + // MULTIPOINT ZM ((30 10 40 300)) + WkbTestCase { + dimension: Dimension::Xyzm, + wkb_bytes: vec![ + 0x01, 0xbc, 0x0b, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xb9, 0x0b, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc0, 0x72, 0x40, + ], + wkt_string: "MULTIPOINT ZM ((30 10 40 300))".to_string(), + }, + // MULTILINESTRING EMPTY + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + wkt_string: "MULTILINESTRING EMPTY".to_string(), + }, + // MULTILINESTRING ((30 10, 10 30, 40 40)) + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![ + 0x01, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, + 0x40, + ], + wkt_string: "MULTILINESTRING ((30 10, 10 30, 40 40))".to_string(), + }, + // MULTILINESTRING Z ((30 10 40, 10 30 40, 40 40 80)) + WkbTestCase { + dimension: Dimension::Xyz, + wkb_bytes: vec![ + 0x01, 0xed, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xea, 0x03, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x40, + ], + wkt_string: "MULTILINESTRING Z ((30 10 40, 10 30 40, 40 40 80))".to_string(), + }, + // MULTILINESTRING M ((30 10 300, 10 30 300, 40 40 1600)) + WkbTestCase { + dimension: Dimension::Xym, + wkb_bytes: vec![ + 0x01, 0xd5, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xd2, 0x07, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xc0, 0x72, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x72, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x99, 0x40, + ], + wkt_string: "MULTILINESTRING M ((30 10 300, 10 30 300, 40 40 1600))".to_string(), + }, + // MULTILINESTRING ZM ((30 10 40 300, 10 30 40 300, 40 40 80 1600)) + WkbTestCase { + dimension: Dimension::Xyzm, + wkb_bytes: vec![ + 0x01, 0xbd, 0x0b, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xba, 0x0b, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x72, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xc0, 0x72, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x54, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x99, 0x40 + ], + wkt_string: "MULTILINESTRING ZM ((30 10 40 300, 10 30 40 300, 40 40 80 1600))".to_string(), + }, + // MULTIPOLYGON EMPTY + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![0x01, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + wkt_string: "MULTIPOLYGON EMPTY".to_string(), + }, + // MULTIPOLYGON (((30 10, 40 40, 20 40, 10 20, 30 10))) + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![ + 0x01, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, + ], + wkt_string: "MULTIPOLYGON (((30 10, 40 40, 20 40, 10 20, 30 10)))".to_string(), + }, + // MULTIPOLYGON Z (((30 10 40, 40 40 80, 20 40 60, 10 20 30, 30 10 40))) + WkbTestCase { + dimension: Dimension::Xyz, + wkb_bytes: vec![ + 0x01, 0xee, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xeb, 0x03, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x54, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x4e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, + ], + wkt_string: "MULTIPOLYGON Z (((30 10 40, 40 40 80, 20 40 60, 10 20 30, 30 10 40)))".to_string(), + }, + // MULTIPOLYGON M (((30 10 300, 40 40 1600, 20 40 800, 10 20 200, 30 10 300))) + WkbTestCase { + dimension: Dimension::Xym, + wkb_bytes: vec![ + 0x01, 0xd6, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xd3, 0x07, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc0, 0x72, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x99, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x89, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x72, 0x40, + ], + wkt_string: "MULTIPOLYGON M (((30 10 300, 40 40 1600, 20 40 800, 10 20 200, 30 10 300)))".to_string(), + }, + // MULTIPOLYGON ZM (((30 10 40 300, 40 40 80 1600, 20 40 60 800, 10 20 30 200, 30 + // 10 40 300))) + WkbTestCase { + dimension: Dimension::Xyzm, + wkb_bytes: vec![ + 0x01, 0xbe, 0x0b, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xbb, 0x0b, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, + 0x72, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x99, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x34, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x4e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x89, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x34, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x69, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x72, 0x40, + ], + wkt_string: "MULTIPOLYGON ZM (((30 10 40 300, 40 40 80 1600, 20 40 60 800, 10 20 30 200, 30 10 40 300)))".to_string(), + }, + // GEOMETRYCOLLECTION EMPTY + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![0x01, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + wkt_string: "GEOMETRYCOLLECTION EMPTY".to_string(), + }, + // GEOMETRYCOLLECTION (POINT (30 10)) + WkbTestCase { + dimension: Dimension::Xy, + wkb_bytes: vec![ + 0x01, 0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x40, + ], + wkt_string: "GEOMETRYCOLLECTION (POINT (30 10))".to_string(), + }, + // GEOMETRYCOLLECTION Z (POINT Z (30 10 40)) + WkbTestCase { + dimension: Dimension::Xyz, + wkb_bytes: vec![ + 0x01, 0xef, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xe9, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, + ], + wkt_string: "GEOMETRYCOLLECTION Z (POINT Z (30 10 40))".to_string(), + }, + // GEOMETRYCOLLECTION M (POINT M (30 10 300)) + WkbTestCase { + dimension: Dimension::Xym, + wkb_bytes: vec![ + 0x01, 0xd7, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xd1, 0x07, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x72, 0x40, + ], + wkt_string: "GEOMETRYCOLLECTION M (POINT M (30 10 300))".to_string(), + }, + // GEOMETRYCOLLECTION ZM (POINT ZM (30 10 40 300)) + WkbTestCase { + dimension: Dimension::Xyzm, + wkb_bytes: vec![ + 0x01, 0xbf, 0x0b, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xb9, 0x0b, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc0, 0x72, 0x40, + ], + wkt_string: "GEOMETRYCOLLECTION ZM (POINT ZM (30 10 40 300))".to_string(), + }, + ] + } + + #[test] + fn test_using_comprehensive_cases() { + let factory = GEOSWkbFactory::new(); + let test_cases = get_wkb_test_cases(); + for test_case in test_cases { + let wkb = read_wkb(&test_case.wkb_bytes).unwrap(); + let geos_geom = factory.create(&wkb).unwrap(); + let wkt_from_geos = geos_geom.to_wkt().unwrap(); + assert_eq!( + wkt_from_geos, test_case.wkt_string, + "Failed for test case {}", + test_case.wkt_string + ); + } + } +} diff --git a/c/sedona-tg/benches/parse-wkb.rs b/c/sedona-tg/benches/parse-wkb.rs index 9632afec..79a920d9 100644 --- a/c/sedona-tg/benches/parse-wkb.rs +++ b/c/sedona-tg/benches/parse-wkb.rs @@ -27,14 +27,18 @@ fn criterion_benchmark(c: &mut Criterion) { wkb::writer::write_geometry( &mut large_geom_wkb_big_endian, &large_geom, - wkb::Endianness::BigEndian, + &wkb::writer::WriteOptions { + endianness: wkb::Endianness::BigEndian, + }, ) .unwrap(); let mut large_geom_wkb_little_endian = Vec::new(); wkb::writer::write_geometry( &mut large_geom_wkb_little_endian, &large_geom, - wkb::Endianness::LittleEndian, + &wkb::writer::WriteOptions { + endianness: wkb::Endianness::LittleEndian, + }, ) .unwrap(); diff --git a/dev/release/rat_exclude_files.txt b/dev/release/rat_exclude_files.txt index ab4302c9..8dd58fbf 100644 --- a/dev/release/rat_exclude_files.txt +++ b/dev/release/rat_exclude_files.txt @@ -18,3 +18,4 @@ r/sedonadb/DESCRIPTION r/sedonadb/NAMESPACE r/sedonadb/src/sedonadb-win.def submodules/geoarrow-data/* +rust/sedona-geo-test-fixtures/fixtures/*.wkt diff --git a/rust/sedona-adbc/Cargo.toml b/rust/sedona-adbc/Cargo.toml index 7a557a79..f7796b20 100644 --- a/rust/sedona-adbc/Cargo.toml +++ b/rust/sedona-adbc/Cargo.toml @@ -29,6 +29,7 @@ result_large_err = "allow" [dependencies] adbc_core = { workspace = true } +adbc_ffi = { workspace = true } arrow-array = { workspace = true } arrow-schema = { workspace = true } datafusion = { workspace = true } diff --git a/rust/sedona-adbc/src/database.rs b/rust/sedona-adbc/src/database.rs index e8697da7..248d51c0 100644 --- a/rust/sedona-adbc/src/database.rs +++ b/rust/sedona-adbc/src/database.rs @@ -51,12 +51,12 @@ impl Optionable for SedonaDatabase { impl Database for SedonaDatabase { type ConnectionType = SedonaConnection; - fn new_connection(&mut self) -> Result { + fn new_connection(&self) -> Result { self.new_connection_with_opts([]) } fn new_connection_with_opts( - &mut self, + &self, opts: impl IntoIterator, ) -> Result { SedonaConnection::try_new(opts) diff --git a/rust/sedona-adbc/src/lib.rs b/rust/sedona-adbc/src/lib.rs index 7e7da284..ae88f480 100644 --- a/rust/sedona-adbc/src/lib.rs +++ b/rust/sedona-adbc/src/lib.rs @@ -21,7 +21,6 @@ pub mod database; pub mod driver; pub mod statement; -use adbc_core::error::{Error, Status}; use driver::SedonaDriver; -adbc_core::export_driver!(AdbcSedonadbDriverInit, SedonaDriver); +adbc_ffi::export_driver!(AdbcSedonadbDriverInit, SedonaDriver); diff --git a/rust/sedona-functions/src/st_geomfromwkt.rs b/rust/sedona-functions/src/st_geomfromwkt.rs index b35d628b..558fbec3 100644 --- a/rust/sedona-functions/src/st_geomfromwkt.rs +++ b/rust/sedona-functions/src/st_geomfromwkt.rs @@ -29,7 +29,8 @@ use sedona_schema::{ datatypes::{SedonaType, WKB_GEOGRAPHY, WKB_GEOMETRY}, matchers::ArgMatcher, }; -use wkb::writer::write_geometry; +use wkb::writer::{write_geometry, WriteOptions}; +use wkb::Endianness; use wkt::Wkt; use crate::executor::WkbExecutor; @@ -128,8 +129,14 @@ fn invoke_scalar(wkt_bytes: &str, builder: &mut BinaryBuilder) -> Result<()> { let geometry: Wkt = Wkt::from_str(wkt_bytes) .map_err(|err| DataFusionError::Internal(format!("WKT parse error: {err}")))?; - write_geometry(builder, &geometry, wkb::Endianness::LittleEndian) - .map_err(|err| DataFusionError::Internal(format!("WKB write error: {err}"))) + write_geometry( + builder, + &geometry, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .map_err(|err| DataFusionError::Internal(format!("WKB write error: {err}"))) } #[cfg(test)] diff --git a/rust/sedona-geo-generic-alg/Cargo.toml b/rust/sedona-geo-generic-alg/Cargo.toml new file mode 100644 index 00000000..2da128ea --- /dev/null +++ b/rust/sedona-geo-generic-alg/Cargo.toml @@ -0,0 +1,77 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +[package] +name = "sedona-geo-generic-alg" +version.workspace = true +homepage.workspace = true +repository.workspace = true +description.workspace = true +readme.workspace = true +edition.workspace = true +rust-version.workspace = true + +[features] +default = ["multithreading"] +use-serde = ["serde", "geo-types/serde"] +multithreading = ["i_overlay/allow_multithreading", "geo-types/multithreading"] + +[dependencies] +float_next_after = { workspace = true } +geo-traits = { workspace = true } +geo-types = { workspace = true, features = ["approx", "use-rstar_0_12"] } +sedona-geo-traits-ext = { path = "../sedona-geo-traits-ext" } +log = "0.4.11" +num-traits = { workspace = true } +robust = "1.1.0" +rstar = "0.12.0" +serde = { workspace = true, features = ["derive"], optional = true } +i_overlay = { version = "4.0.0, < 4.1.0", default-features = false } + +[dev-dependencies] +sedona-testing = { path = "../sedona-testing" } +approx = { workspace = true } +criterion = { workspace = true } +pretty_env_logger = "0.4" +rand = { workspace = true } +rand_distr = "0.4.3" +geo = { workspace = true } +wkb = { workspace = true } +wkt = { workspace = true } + +[[bench]] +name = "area" +harness = false + +[[bench]] +name = "intersection" +harness = false + +[[bench]] +name = "centroid" +harness = false + +[[bench]] +name = "length" +harness = false + +[[bench]] +name = "distance" +harness = false + +[[bench]] +name = "perimeter" +harness = false diff --git a/rust/sedona-geo-generic-alg/README.md b/rust/sedona-geo-generic-alg/README.md new file mode 100644 index 00000000..1ba86778 --- /dev/null +++ b/rust/sedona-geo-generic-alg/README.md @@ -0,0 +1,22 @@ + + +# Generic Algorithms for Geo-Traits + +This crate contains algorithms ported from the [`geo` crate](https://github.com/georust/geo), +but works with traits defined in `geo-traits-ext` instead of concrete geometry types defined in +`geo-types`. diff --git a/rust/sedona-geo-generic-alg/benches/area.rs b/rust/sedona-geo-generic-alg/benches/area.rs new file mode 100644 index 00000000..ab9e5da8 --- /dev/null +++ b/rust/sedona-geo-generic-alg/benches/area.rs @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use criterion::{criterion_group, criterion_main, Criterion}; +use geo_traits::to_geo::ToGeoGeometry; +use sedona_geo_generic_alg::Area; +use sedona_geo_generic_alg::Polygon; + +#[path = "utils/wkb_util.rs"] +mod wkb_util; + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("area_generic_f32", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&polygon).signed_area()); + }); + }); + + c.bench_function("area_generic", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&polygon).signed_area()); + }); + }); + + c.bench_function("area_geo_f32", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(geo::Area::signed_area(criterion::black_box(&polygon))); + }); + }); + + c.bench_function("area_geo", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(geo::Area::signed_area(criterion::black_box(&polygon))); + }); + }); + + c.bench_function("area_wkb", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + let wkb_bytes = wkb_util::geo_to_wkb(polygon); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + criterion::black_box(wkb_geom.signed_area()); + }); + }); + + c.bench_function("area_wkb_convert", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + let wkb_bytes = wkb_util::geo_to_wkb(polygon); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + let geom = wkb_geom.to_geometry(); + criterion::black_box(geom.signed_area()); + }); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/rust/sedona-geo-generic-alg/benches/centroid.rs b/rust/sedona-geo-generic-alg/benches/centroid.rs new file mode 100644 index 00000000..62d7c033 --- /dev/null +++ b/rust/sedona-geo-generic-alg/benches/centroid.rs @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use criterion::{criterion_group, criterion_main, Criterion}; +use geo_traits::to_geo::ToGeoGeometry; +use sedona_geo_generic_alg::Centroid; +use sedona_geo_generic_alg::Polygon; + +#[path = "utils/wkb_util.rs"] +mod wkb_util; + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("centroid_generic_f32", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&polygon).centroid()); + }); + }); + + c.bench_function("centroid_generic", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&polygon).centroid()); + }); + }); + + c.bench_function("centroid_geo_f32", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(geo::Centroid::centroid(criterion::black_box(&polygon))); + }); + }); + + c.bench_function("centroid_geo", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(geo::Centroid::centroid(criterion::black_box(&polygon))); + }); + }); + + c.bench_function("centroid_wkb", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + let wkb_bytes = wkb_util::geo_to_wkb(polygon); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + criterion::black_box(wkb_geom.centroid()); + }); + }); + + c.bench_function("centroid_wkb_convert", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + let wkb_bytes = wkb_util::geo_to_wkb(polygon); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + let geom = wkb_geom.to_geometry(); + criterion::black_box(geom.centroid()); + }); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/rust/sedona-geo-generic-alg/benches/distance.rs b/rust/sedona-geo-generic-alg/benches/distance.rs new file mode 100644 index 00000000..313e4cf7 --- /dev/null +++ b/rust/sedona-geo-generic-alg/benches/distance.rs @@ -0,0 +1,511 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use criterion::{criterion_group, criterion_main, Criterion}; +use geo::{Distance as GeoDistance, Euclidean}; +use sedona_geo_generic_alg::algorithm::line_measures::DistanceExt; +use sedona_geo_generic_alg::{coord, LineString, MultiPolygon, Point, Polygon}; + +#[path = "utils/wkb_util.rs"] +mod wkb_util; + +// Helper function to create complex polygons with many vertices for stress testing +fn create_complex_polygon( + center_x: f64, + center_y: f64, + radius: f64, + num_vertices: usize, +) -> Polygon { + let mut vertices = Vec::with_capacity(num_vertices + 1); + + for i in 0..num_vertices { + let angle = 2.0 * std::f64::consts::PI * i as f64 / num_vertices as f64; + // Add some variation to make it non-regular + let r = radius * (1.0 + 0.1 * (i as f64 * 0.3).sin()); + let x = center_x + r * angle.cos(); + let y = center_y + r * angle.sin(); + vertices.push(coord!(x: x, y: y)); + } + + // Close the polygon + vertices.push(vertices[0]); + + Polygon::new(LineString::from(vertices), vec![]) +} + +// Helper function to create multipolygons for testing iteration overhead +fn create_multipolygon(num_polygons: usize) -> MultiPolygon { + let mut polygons = Vec::with_capacity(num_polygons); + + for i in 0..num_polygons { + let offset = i as f64 * 50.0; + let poly = Polygon::new( + LineString::from(vec![ + coord!(x: offset, y: offset), + coord!(x: offset + 30.0, y: offset), + coord!(x: offset + 30.0, y: offset + 30.0), + coord!(x: offset, y: offset + 30.0), + coord!(x: offset, y: offset), + ]), + vec![], + ); + polygons.push(poly); + } + + MultiPolygon::new(polygons) +} + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("distance_point_to_point", |bencher| { + let p1 = Point::new(0.0, 0.0); + let p2 = Point::new(100.0, 100.0); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&p1).distance_ext(criterion::black_box(&p2))); + }); + }); + + c.bench_function("distance_linestring_to_linestring", |bencher| { + let ls1 = sedona_testing::fixtures::norway_main::(); + let ls2 = LineString::from(vec![ + coord!(x: 100.0, y: 100.0), + coord!(x: 200.0, y: 200.0), + coord!(x: 300.0, y: 300.0), + ]); + + bencher.iter(|| { + criterion::black_box( + criterion::black_box(&ls1).distance_ext(criterion::black_box(&ls2)), + ); + }); + }); + + c.bench_function("distance_polygon_to_polygon", |bencher| { + let poly1 = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 100.0, y: 0.0), + coord!(x: 100.0, y: 100.0), + coord!(x: 0.0, y: 100.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + let poly2 = Polygon::new( + LineString::from(vec![ + coord!(x: 200.0, y: 200.0), + coord!(x: 300.0, y: 200.0), + coord!(x: 300.0, y: 300.0), + coord!(x: 200.0, y: 300.0), + coord!(x: 200.0, y: 200.0), + ]), + vec![], + ); + + bencher.iter(|| { + criterion::black_box( + criterion::black_box(&poly1).distance_ext(criterion::black_box(&poly2)), + ); + }); + }); + + c.bench_function("distance_wkb_point_to_point", |bencher| { + let p1 = Point::new(0.0, 0.0); + let p2 = Point::new(100.0, 100.0); + let wkb_bytes1 = wkb_util::geo_to_wkb(p1); + let wkb_bytes2 = wkb_util::geo_to_wkb(p2); + + bencher.iter(|| { + let wkb_geom1 = wkb::reader::read_wkb(&wkb_bytes1).unwrap(); + let wkb_geom2 = wkb::reader::read_wkb(&wkb_bytes2).unwrap(); + criterion::black_box(wkb_geom1.distance_ext(&wkb_geom2)); + }); + }); + + c.bench_function("distance_wkb_linestring_to_linestring", |bencher| { + let ls1 = sedona_testing::fixtures::norway_main::(); + let ls2 = LineString::from(vec![ + coord!(x: 100.0, y: 100.0), + coord!(x: 200.0, y: 200.0), + coord!(x: 300.0, y: 300.0), + ]); + let wkb_bytes1 = wkb_util::geo_to_wkb(ls1); + let wkb_bytes2 = wkb_util::geo_to_wkb(ls2); + + bencher.iter(|| { + let wkb_geom1 = wkb::reader::read_wkb(&wkb_bytes1).unwrap(); + let wkb_geom2 = wkb::reader::read_wkb(&wkb_bytes2).unwrap(); + criterion::black_box(wkb_geom1.distance_ext(&wkb_geom2)); + }); + }); + + c.bench_function("distance_multipolygon_to_multipolygon", |bencher| { + let poly1 = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 50.0, y: 0.0), + coord!(x: 50.0, y: 50.0), + coord!(x: 0.0, y: 50.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + let poly2 = Polygon::new( + LineString::from(vec![ + coord!(x: 60.0, y: 60.0), + coord!(x: 110.0, y: 60.0), + coord!(x: 110.0, y: 110.0), + coord!(x: 60.0, y: 110.0), + coord!(x: 60.0, y: 60.0), + ]), + vec![], + ); + let mp1 = MultiPolygon::new(vec![poly1.clone(), poly1]); + let mp2 = MultiPolygon::new(vec![poly2.clone(), poly2]); + + bencher.iter(|| { + criterion::black_box( + criterion::black_box(&mp1).distance_ext(criterion::black_box(&mp2)), + ); + }); + }); + + c.bench_function("distance_concrete_point_to_point", |bencher| { + let p1 = Point::new(0.0, 0.0); + let p2 = Point::new(100.0, 100.0); + + bencher.iter(|| { + criterion::black_box( + Euclidean.distance(criterion::black_box(p1), criterion::black_box(p2)), + ); + }); + }); + + c.bench_function("distance_concrete_linestring_to_linestring", |bencher| { + let ls1 = sedona_testing::fixtures::norway_main::(); + let ls2 = LineString::from(vec![ + coord!(x: 100.0, y: 100.0), + coord!(x: 200.0, y: 200.0), + coord!(x: 300.0, y: 300.0), + ]); + + bencher.iter(|| { + criterion::black_box( + geo::Euclidean.distance(criterion::black_box(&ls1), criterion::black_box(&ls2)), + ); + }); + }); + + c.bench_function("distance_cross_type_point_to_linestring", |bencher| { + let point = Point::new(50.0, 50.0); + let linestring = LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 100.0, y: 100.0), + coord!(x: 200.0, y: 0.0), + ]); + + bencher.iter(|| { + criterion::black_box(Euclidean.distance( + criterion::black_box(&point), + criterion::black_box(&linestring), + )); + }); + }); + + c.bench_function("distance_cross_type_linestring_to_polygon", |bencher| { + let linestring = + LineString::from(vec![coord!(x: -50.0, y: 50.0), coord!(x: 150.0, y: 50.0)]); + let polygon = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 100.0, y: 0.0), + coord!(x: 100.0, y: 100.0), + coord!(x: 0.0, y: 100.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + + bencher.iter(|| { + criterion::black_box(Euclidean.distance( + criterion::black_box(&linestring), + criterion::black_box(&polygon), + )); + }); + }); + + c.bench_function("distance_cross_type_point_to_polygon", |bencher| { + let point = Point::new(150.0, 50.0); + let polygon = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 100.0, y: 0.0), + coord!(x: 100.0, y: 100.0), + coord!(x: 0.0, y: 100.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + + bencher.iter(|| { + criterion::black_box( + Euclidean.distance(criterion::black_box(&point), criterion::black_box(&polygon)), + ); + }); + }); + + // ┌────────────────────────────────────────────────────────────┐ + // │ Targeted Performance Benchmarks: Generic vs Concrete │ + // └────────────────────────────────────────────────────────────┘ + + c.bench_function( + "generic_vs_concrete_polygon_containment_simple", + |bencher| { + // Simple polygon-to-polygon distance (no holes, no containment) + let poly1 = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 50.0, y: 0.0), + coord!(x: 50.0, y: 50.0), + coord!(x: 0.0, y: 50.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + let poly2 = Polygon::new( + LineString::from(vec![ + coord!(x: 100.0, y: 100.0), + coord!(x: 150.0, y: 100.0), + coord!(x: 150.0, y: 150.0), + coord!(x: 100.0, y: 150.0), + coord!(x: 100.0, y: 100.0), + ]), + vec![], + ); + + bencher.iter(|| { + // Generic implementation + criterion::black_box( + criterion::black_box(&poly1).distance_ext(criterion::black_box(&poly2)), + ); + }); + }, + ); + + c.bench_function( + "concrete_vs_generic_polygon_containment_simple", + |bencher| { + let poly1 = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 50.0, y: 0.0), + coord!(x: 50.0, y: 50.0), + coord!(x: 0.0, y: 50.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + let poly2 = Polygon::new( + LineString::from(vec![ + coord!(x: 100.0, y: 100.0), + coord!(x: 150.0, y: 100.0), + coord!(x: 150.0, y: 150.0), + coord!(x: 100.0, y: 150.0), + coord!(x: 100.0, y: 100.0), + ]), + vec![], + ); + + bencher.iter(|| { + // Concrete implementation + criterion::black_box( + Euclidean.distance(criterion::black_box(&poly1), criterion::black_box(&poly2)), + ); + }); + }, + ); + + c.bench_function("generic_polygon_with_holes_distance", |bencher| { + // Polygon with holes - this triggers the containment check and temporary object creation + let outer = LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 200.0, y: 0.0), + coord!(x: 200.0, y: 200.0), + coord!(x: 0.0, y: 200.0), + coord!(x: 0.0, y: 0.0), + ]); + let hole = LineString::from(vec![ + coord!(x: 50.0, y: 50.0), + coord!(x: 150.0, y: 50.0), + coord!(x: 150.0, y: 150.0), + coord!(x: 50.0, y: 150.0), + coord!(x: 50.0, y: 50.0), + ]); + let poly_with_hole = Polygon::new(outer, vec![hole]); + + // Small polygon that might be inside the hole (triggers containment logic) + let small_poly = Polygon::new( + LineString::from(vec![ + coord!(x: 75.0, y: 75.0), + coord!(x: 125.0, y: 75.0), + coord!(x: 125.0, y: 125.0), + coord!(x: 75.0, y: 125.0), + coord!(x: 75.0, y: 75.0), + ]), + vec![], + ); + + bencher.iter(|| { + criterion::black_box( + criterion::black_box(&poly_with_hole) + .distance_ext(criterion::black_box(&small_poly)), + ); + }); + }); + + c.bench_function("concrete_polygon_with_holes_distance", |bencher| { + let outer = LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 200.0, y: 0.0), + coord!(x: 200.0, y: 200.0), + coord!(x: 0.0, y: 200.0), + coord!(x: 0.0, y: 0.0), + ]); + let hole = LineString::from(vec![ + coord!(x: 50.0, y: 50.0), + coord!(x: 150.0, y: 50.0), + coord!(x: 150.0, y: 150.0), + coord!(x: 50.0, y: 150.0), + coord!(x: 50.0, y: 50.0), + ]); + let poly_with_hole = Polygon::new(outer, vec![hole]); + let small_poly = Polygon::new( + LineString::from(vec![ + coord!(x: 75.0, y: 75.0), + coord!(x: 125.0, y: 75.0), + coord!(x: 125.0, y: 125.0), + coord!(x: 75.0, y: 125.0), + coord!(x: 75.0, y: 75.0), + ]), + vec![], + ); + + bencher.iter(|| { + criterion::black_box(Euclidean.distance( + criterion::black_box(&poly_with_hole), + criterion::black_box(&small_poly), + )); + }); + }); + + c.bench_function("generic_complex_polygon_distance", |bencher| { + // Complex polygons with many vertices - stress test for temporary object creation + let complex_poly1 = create_complex_polygon(0.0, 0.0, 50.0, 20); // 20 vertices + let complex_poly2 = create_complex_polygon(100.0, 100.0, 30.0, 15); // 15 vertices + + bencher.iter(|| { + criterion::black_box( + criterion::black_box(&complex_poly1) + .distance_ext(criterion::black_box(&complex_poly2)), + ); + }); + }); + + c.bench_function("concrete_complex_polygon_distance", |bencher| { + let complex_poly1 = create_complex_polygon(0.0, 0.0, 50.0, 20); + let complex_poly2 = create_complex_polygon(100.0, 100.0, 30.0, 15); + + bencher.iter(|| { + criterion::black_box(Euclidean.distance( + criterion::black_box(&complex_poly1), + criterion::black_box(&complex_poly2), + )); + }); + }); + + c.bench_function("generic_linestring_to_polygon_intersecting", |bencher| { + // LineString that intersects polygon - tests early exit performance + let polygon = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 100.0, y: 0.0), + coord!(x: 100.0, y: 100.0), + coord!(x: 0.0, y: 100.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + let intersecting_linestring = LineString::from(vec![ + coord!(x: -50.0, y: 50.0), + coord!(x: 150.0, y: 50.0), // Crosses through the polygon + ]); + + bencher.iter(|| { + criterion::black_box( + criterion::black_box(&intersecting_linestring) + .distance_ext(criterion::black_box(&polygon)), + ); + }); + }); + + c.bench_function("concrete_linestring_to_polygon_intersecting", |bencher| { + let polygon = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 100.0, y: 0.0), + coord!(x: 100.0, y: 100.0), + coord!(x: 0.0, y: 100.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + let intersecting_linestring = + LineString::from(vec![coord!(x: -50.0, y: 50.0), coord!(x: 150.0, y: 50.0)]); + + bencher.iter(|| { + criterion::black_box(Euclidean.distance( + criterion::black_box(&intersecting_linestring), + criterion::black_box(&polygon), + )); + }); + }); + + c.bench_function("generic_multipolygon_distance_overhead", |bencher| { + // Test multipolygon distance to measure iterator and temporary object overhead + let mp1 = create_multipolygon(5); // 5 polygons + let mp2 = create_multipolygon(3); // 3 polygons + + bencher.iter(|| { + criterion::black_box( + criterion::black_box(&mp1).distance_ext(criterion::black_box(&mp2)), + ); + }); + }); + + c.bench_function("concrete_multipolygon_distance_overhead", |bencher| { + let mp1 = create_multipolygon(5); + let mp2 = create_multipolygon(3); + + bencher.iter(|| { + criterion::black_box( + Euclidean.distance(criterion::black_box(&mp1), criterion::black_box(&mp2)), + ); + }); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/rust/sedona-geo-generic-alg/benches/intersection.rs b/rust/sedona-geo-generic-alg/benches/intersection.rs new file mode 100644 index 00000000..a5891e0b --- /dev/null +++ b/rust/sedona-geo-generic-alg/benches/intersection.rs @@ -0,0 +1,458 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use criterion::{criterion_group, criterion_main, Criterion}; +use geo_traits::to_geo::ToGeoGeometry; +use geo_types::Geometry; +use sedona_geo_generic_alg::MultiPolygon; +use sedona_geo_generic_alg::{intersects::Intersects, Centroid}; + +#[path = "utils/wkb_util.rs"] +mod wkb_util; + +fn multi_polygon_intersection(c: &mut Criterion) { + let plot_polygons: MultiPolygon = sedona_testing::fixtures::nl_plots_wgs84(); + let zone_polygons: MultiPolygon = sedona_testing::fixtures::nl_zones(); + let plot_geoms: Vec = plot_polygons.into_iter().map(|p| p.into()).collect(); + let zone_geoms: Vec = zone_polygons.into_iter().map(|p| p.into()).collect(); + + c.bench_function("MultiPolygon intersects", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_geoms { + for b in &zone_geoms { + if criterion::black_box(b.intersects(a)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 974); + assert_eq!(non_intersects, 27782); + }); + }); + + c.bench_function("MultiPolygon intersects geo", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_geoms { + for b in &zone_geoms { + if criterion::black_box(geo::Intersects::intersects(b, a)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 974); + assert_eq!(non_intersects, 27782); + }); + }); +} + +fn multi_polygon_intersection_wkb(c: &mut Criterion) { + let plot_polygons: MultiPolygon = sedona_testing::fixtures::nl_plots_wgs84(); + let zone_polygons: MultiPolygon = sedona_testing::fixtures::nl_zones(); + + // Convert intersected polygons to WKB + let mut plot_polygon_wkbs = Vec::new(); + let mut zone_polygon_wkbs = Vec::new(); + for plot_polygon in plot_polygons { + plot_polygon_wkbs.push(wkb_util::geo_to_wkb(plot_polygon)); + } + for zone_polygon in zone_polygons { + zone_polygon_wkbs.push(wkb_util::geo_to_wkb(zone_polygon)); + } + + c.bench_function("MultiPolygon intersects wkb", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_polygon_wkbs { + for b in &zone_polygon_wkbs { + let a_geom = wkb::reader::read_wkb(a).unwrap(); // Skip padding + let b_geom = wkb::reader::read_wkb(b).unwrap(); // Skip padding + if criterion::black_box(b_geom.intersects(&a_geom)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 974); + assert_eq!(non_intersects, 27782); + }); + }); +} + +fn multi_polygon_intersection_wkb_aligned(c: &mut Criterion) { + let plot_polygons: MultiPolygon = sedona_testing::fixtures::nl_plots_wgs84(); + let zone_polygons: MultiPolygon = sedona_testing::fixtures::nl_zones(); + + // Convert intersected polygons to WKB + let mut plot_polygon_wkbs = Vec::new(); + let mut zone_polygon_wkbs = Vec::new(); + for plot_polygon in plot_polygons { + let mut wkb = vec![0, 0, 0]; // Add 3-byte padding + wkb.extend_from_slice(&wkb_util::geo_to_wkb(plot_polygon)); + plot_polygon_wkbs.push(wkb); + } + for zone_polygon in zone_polygons { + let mut wkb = vec![0, 0, 0]; // Add 3-byte padding + wkb.extend_from_slice(&wkb_util::geo_to_wkb(zone_polygon)); + zone_polygon_wkbs.push(wkb); + } + + c.bench_function("MultiPolygon intersects wkb aligned", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_polygon_wkbs { + for b in &zone_polygon_wkbs { + let a_geom = wkb::reader::read_wkb(&a[3..]).unwrap(); // Skip padding + let b_geom = wkb::reader::read_wkb(&b[3..]).unwrap(); // Skip padding + if criterion::black_box(b_geom.intersects(&a_geom)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 974); + assert_eq!(non_intersects, 27782); + }); + }); +} + +fn multi_polygon_intersection_wkb_conv(c: &mut Criterion) { + let plot_polygons: MultiPolygon = sedona_testing::fixtures::nl_plots_wgs84(); + let zone_polygons: MultiPolygon = sedona_testing::fixtures::nl_zones(); + + // Convert intersected polygons to WKB + let mut plot_polygon_wkbs = Vec::new(); + let mut zone_polygon_wkbs = Vec::new(); + for plot_polygon in plot_polygons { + plot_polygon_wkbs.push(wkb_util::geo_to_wkb(plot_polygon)); + } + for zone_polygon in zone_polygons { + zone_polygon_wkbs.push(wkb_util::geo_to_wkb(zone_polygon)); + } + + c.bench_function("MultiPolygon intersects wkb conv", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_polygon_wkbs { + for b in &zone_polygon_wkbs { + let a_geom = wkb::reader::read_wkb(a).unwrap(); + let b_geom = wkb::reader::read_wkb(b).unwrap(); + let a_geom = a_geom.to_geometry(); + let b_geom = b_geom.to_geometry(); + if criterion::black_box(b_geom.intersects(&a_geom)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 974); + assert_eq!(non_intersects, 27782); + }); + }); +} + +fn point_polygon_intersection(c: &mut Criterion) { + let plot_polygons: MultiPolygon = sedona_testing::fixtures::nl_plots_wgs84(); + let zone_polygons: MultiPolygon = sedona_testing::fixtures::nl_zones(); + let plot_geoms: Vec = plot_polygons + .into_iter() + .map(|p| { + let centroid = p.centroid().unwrap(); + centroid.into() + }) + .collect(); + let zone_geoms: Vec = zone_polygons.into_iter().map(|p| p.into()).collect(); + + c.bench_function("Point polygon intersects", |bencher| { + bencher.iter(|| { + for a in &plot_geoms { + for b in &zone_geoms { + criterion::black_box(b.intersects(a)); + } + } + }); + }); + + c.bench_function("Point polygon intersects geo", |bencher| { + bencher.iter(|| { + for a in &plot_geoms { + for b in &zone_geoms { + criterion::black_box(geo::Intersects::intersects(b, a)); + } + } + }); + }); +} + +fn point_polygon_intersection_wkb(c: &mut Criterion) { + let plot_polygons: MultiPolygon = sedona_testing::fixtures::nl_plots_wgs84(); + let zone_polygons: MultiPolygon = sedona_testing::fixtures::nl_zones(); + + // Convert intersected polygons to WKB + let mut plot_centroid_wkbs = Vec::new(); + let mut zone_polygon_wkbs = Vec::new(); + for plot_polygon in plot_polygons { + let centroid = plot_polygon.centroid().unwrap(); + plot_centroid_wkbs.push(wkb_util::geo_to_wkb(centroid)); + } + for zone_polygon in zone_polygons { + zone_polygon_wkbs.push(wkb_util::geo_to_wkb(zone_polygon)); + } + + c.bench_function("Point polygon intersects wkb", |bencher| { + bencher.iter(|| { + for a in &plot_centroid_wkbs { + for b in &zone_polygon_wkbs { + let a_geom = wkb::reader::read_wkb(a).unwrap(); + let b_geom = wkb::reader::read_wkb(b).unwrap(); + criterion::black_box(b_geom.intersects(&a_geom)); + } + } + }); + }); +} + +fn point_polygon_intersection_wkb_conv(c: &mut Criterion) { + let plot_polygons: MultiPolygon = sedona_testing::fixtures::nl_plots_wgs84(); + let zone_polygons: MultiPolygon = sedona_testing::fixtures::nl_zones(); + + // Convert intersected polygons to WKB + let mut plot_centroid_wkbs = Vec::new(); + let mut zone_polygon_wkbs = Vec::new(); + for plot_polygon in plot_polygons { + let centroid = plot_polygon.centroid().unwrap(); + plot_centroid_wkbs.push(wkb_util::geo_to_wkb(centroid)); + } + for zone_polygon in zone_polygons { + zone_polygon_wkbs.push(wkb_util::geo_to_wkb(zone_polygon)); + } + + c.bench_function("Point polygon intersects wkb conv", |bencher| { + bencher.iter(|| { + for a in &plot_centroid_wkbs { + for b in &zone_polygon_wkbs { + let a_geom = wkb::reader::read_wkb(a).unwrap(); + let b_geom = wkb::reader::read_wkb(b).unwrap(); + let a_geom = a_geom.to_geometry(); + let b_geom = b_geom.to_geometry(); + criterion::black_box(b_geom.intersects(&a_geom)); + } + } + }); + }); +} + +fn rect_intersection(c: &mut Criterion) { + use sedona_geo_generic_alg::algorithm::BoundingRect; + use sedona_geo_generic_alg::Rect; + let plot_bbox: Vec = sedona_testing::fixtures::nl_plots_wgs84() + .iter() + .map(|plot| plot.bounding_rect().unwrap()) + .collect(); + let zone_bbox: Vec = sedona_testing::fixtures::nl_zones() + .iter() + .map(|plot| plot.bounding_rect().unwrap()) + .collect(); + + c.bench_function("Rect intersects", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_bbox { + for b in &zone_bbox { + if criterion::black_box(a.intersects(b)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 3054); + assert_eq!(non_intersects, 25702); + }); + }); +} + +fn point_rect_intersection(c: &mut Criterion) { + use sedona_geo_generic_alg::algorithm::{BoundingRect, Centroid}; + use sedona_geo_generic_alg::geometry::{Point, Rect}; + let plot_centroids: Vec = sedona_testing::fixtures::nl_plots_wgs84() + .iter() + .map(|plot| plot.centroid().unwrap()) + .collect(); + let zone_bbox: Vec = sedona_testing::fixtures::nl_zones() + .iter() + .map(|plot| plot.bounding_rect().unwrap()) + .collect(); + + c.bench_function("Point intersects rect", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_centroids { + for b in &zone_bbox { + if criterion::black_box(a.intersects(b)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 2246); + assert_eq!(non_intersects, 26510); + }); + }); +} + +fn point_triangle_intersection(c: &mut Criterion) { + use geo::algorithm::TriangulateEarcut; + use sedona_geo_generic_alg::{Point, Triangle}; + let plot_centroids: Vec = sedona_testing::fixtures::nl_plots_wgs84() + .iter() + .map(|plot| plot.centroid().unwrap()) + .collect(); + let zone_triangles: Vec = sedona_testing::fixtures::nl_zones() + .iter() + .flat_map(|plot| plot.earcut_triangles_iter()) + .collect(); + + c.bench_function("Point intersects triangle", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_centroids { + for b in &zone_triangles { + if criterion::black_box(a.intersects(b)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 533); + assert_eq!(non_intersects, 5450151); + }); + }); + + c.bench_function("Triangle intersects point", |bencher| { + let triangle = Triangle::from([(0., 0.), (10., 0.), (5., 10.)]); + let point = Point::new(5., 5.); + + bencher.iter(|| { + assert!(criterion::black_box(&triangle).intersects(criterion::black_box(&point))); + }); + }); + + c.bench_function("Triangle intersects point on edge", |bencher| { + let triangle = Triangle::from([(0., 0.), (10., 0.), (6., 10.)]); + let point = Point::new(3., 5.); + + bencher.iter(|| { + assert!(criterion::black_box(&triangle).intersects(criterion::black_box(&point))); + }); + }); +} + +criterion_group! { + name = bench_multi_polygons; + config = Criterion::default().sample_size(10); + targets = multi_polygon_intersection +} +criterion_group! { + name = bench_multi_polygons_wkb; + config = Criterion::default().sample_size(10); + targets = multi_polygon_intersection_wkb +} +criterion_group! { + name = bench_multi_polygons_wkb_aligned; + config = Criterion::default().sample_size(10); + targets = multi_polygon_intersection_wkb_aligned +} +criterion_group! { + name = bench_multi_polygons_wkb_conv; + config = Criterion::default().sample_size(10); + targets = multi_polygon_intersection_wkb_conv +} + +criterion_group!(bench_rects, rect_intersection); +criterion_group! { + name = bench_point_rect; + config = Criterion::default().sample_size(50); + targets = point_rect_intersection +} +criterion_group! { + name = bench_point_triangle; + config = Criterion::default().sample_size(50); + targets = point_triangle_intersection +} + +criterion_group! { + name = bench_point_polygon; + config = Criterion::default().sample_size(50); + targets = point_polygon_intersection +} +criterion_group! { + name = bench_point_polygon_wkb; + config = Criterion::default().sample_size(50); + targets = point_polygon_intersection_wkb +} +criterion_group! { + name = bench_point_polygon_wkb_conv; + config = Criterion::default().sample_size(50); + targets = point_polygon_intersection_wkb_conv +} + +criterion_main!( + bench_multi_polygons, + bench_multi_polygons_wkb, + bench_multi_polygons_wkb_aligned, + bench_multi_polygons_wkb_conv, + bench_rects, + bench_point_rect, + bench_point_triangle, + bench_point_polygon, + bench_point_polygon_wkb, + bench_point_polygon_wkb_conv +); diff --git a/rust/sedona-geo-generic-alg/benches/length.rs b/rust/sedona-geo-generic-alg/benches/length.rs new file mode 100644 index 00000000..24970d06 --- /dev/null +++ b/rust/sedona-geo-generic-alg/benches/length.rs @@ -0,0 +1,64 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use criterion::{criterion_group, criterion_main, Criterion}; +use geo_traits::to_geo::ToGeoGeometry; +use sedona_geo_generic_alg::algorithm::line_measures::{Euclidean, LengthMeasurableExt}; + +#[path = "utils/wkb_util.rs"] +mod wkb_util; + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("length_f32", |bencher| { + let linestring = sedona_testing::fixtures::norway_main::(); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&linestring).length_ext(&Euclidean)); + }); + }); + + c.bench_function("length", |bencher| { + let linestring = sedona_testing::fixtures::norway_main::(); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&linestring).length_ext(&Euclidean)); + }); + }); + + c.bench_function("length_wkb", |bencher| { + let linestring = sedona_testing::fixtures::norway_main::(); + let wkb_bytes = wkb_util::geo_to_wkb(linestring); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + criterion::black_box(wkb_geom.length_ext(&Euclidean)); + }); + }); + + c.bench_function("length_wkb_convert", |bencher| { + let linestring = sedona_testing::fixtures::norway_main::(); + let wkb_bytes = wkb_util::geo_to_wkb(linestring); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + let geom = wkb_geom.to_geometry(); + criterion::black_box(geom.length_ext(&Euclidean)); + }); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/rust/sedona-geo-generic-alg/benches/perimeter.rs b/rust/sedona-geo-generic-alg/benches/perimeter.rs new file mode 100644 index 00000000..95d9e0a3 --- /dev/null +++ b/rust/sedona-geo-generic-alg/benches/perimeter.rs @@ -0,0 +1,69 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use criterion::{criterion_group, criterion_main, Criterion}; +use geo_traits::to_geo::ToGeoGeometry; +use sedona_geo_generic_alg::algorithm::line_measures::{Euclidean, LengthMeasurableExt}; +use sedona_geo_generic_alg::Polygon; + +#[path = "utils/wkb_util.rs"] +mod wkb_util; + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("perimeter_f32", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&polygon).perimeter_ext(&Euclidean)); + }); + }); + + c.bench_function("perimeter", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&polygon).perimeter_ext(&Euclidean)); + }); + }); + + c.bench_function("perimeter_wkb", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + let wkb_bytes = wkb_util::geo_to_wkb(polygon); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + criterion::black_box(wkb_geom.perimeter_ext(&Euclidean)); + }); + }); + + c.bench_function("perimeter_wkb_convert", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + let wkb_bytes = wkb_util::geo_to_wkb(polygon); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + let geom = wkb_geom.to_geometry(); + criterion::black_box(geom.perimeter_ext(&Euclidean)); + }); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/rust/sedona-geo-generic-alg/benches/utils/wkb_util.rs b/rust/sedona-geo-generic-alg/benches/utils/wkb_util.rs new file mode 100644 index 00000000..b76a9403 --- /dev/null +++ b/rust/sedona-geo-generic-alg/benches/utils/wkb_util.rs @@ -0,0 +1,25 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +pub fn geo_to_wkb(geo: G) -> Vec +where + G: Into, +{ + let geom = geo.into(); + let mut out: Vec = vec![]; + wkb::writer::write_geometry(&mut out, &geom, &wkb::writer::WriteOptions::default()).unwrap(); + out +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/affine_ops.rs b/rust/sedona-geo-generic-alg/src/algorithm/affine_ops.rs new file mode 100644 index 00000000..6ca937a6 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/affine_ops.rs @@ -0,0 +1,680 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use num_traits::ToPrimitive; + +use crate::{Coord, CoordFloat, CoordNum, MapCoords, MapCoordsInPlace}; +use std::{fmt, ops::Mul, ops::Neg}; + +/// Apply an [`AffineTransform`] like [`scale`](AffineTransform::scale), +/// [`skew`](AffineTransform::skew), or [`rotate`](AffineTransform::rotate) to a +/// [`Geometry`](crate::geometry::Geometry). +/// +/// Multiple transformations can be composed in order to be efficiently applied in a single +/// operation. See [`AffineTransform`] for more on how to build up a transformation. +/// +/// If you are not composing operations, traits that leverage this same machinery exist which might +/// be more readable. See: [`Scale`](crate::algorithm::Scale), +/// [`Translate`](crate::algorithm::Translate), [`Rotate`](crate::algorithm::Rotate), +/// and [`Skew`](crate::algorithm::Skew). +/// +/// # Examples +/// ## Build up transforms by beginning with a constructor, then chaining mutation operations +/// ``` +/// use sedona_geo_generic_alg::{AffineOps, AffineTransform}; +/// use sedona_geo_generic_alg::{point, line_string, BoundingRect}; +/// use approx::assert_relative_eq; +/// +/// let line_string = line_string![(x: 0.0, y: 0.0),(x: 1.0, y: 1.0)]; +/// +/// let transform = AffineTransform::translate(1.0, 1.0).scaled(2.0, 2.0, point!(x: 0.0, y: 0.0)); +/// +/// let transformed_line_string = line_string.affine_transform(&transform); +/// +/// assert_relative_eq!( +/// transformed_line_string, +/// line_string![(x: 2.0, y: 2.0),(x: 4.0, y: 4.0)] +/// ); +/// ``` +pub trait AffineOps { + type Output; + + /// Apply `transform` immutably, outputting a new geometry. + #[must_use] + fn affine_transform(&self, transform: &AffineTransform) -> Self::Output; +} + +/// Apply an [`AffineTransform`] like [`scale`](AffineTransform::scale), +/// [`skew`](AffineTransform::skew), or [`rotate`](AffineTransform::rotate) to a +/// [`Geometry`](crate::geometry::Geometry) in place. +/// +/// Multiple transformations can be composed in order to be efficiently applied in a single +/// operation. See [`AffineTransform`] for more on how to build up a transformation. +/// +/// If you are not composing operations, traits that leverage this same machinery exist which might +/// be more readable. See: [`Scale`](crate::algorithm::Scale), +/// [`Translate`](crate::algorithm::Translate), [`Rotate`](crate::algorithm::Rotate), +/// and [`Skew`](crate::algorithm::Skew). +pub trait AffineOpsMut { + /// Apply `transform` to mutate `self`. + fn affine_transform_mut(&mut self, transform: &AffineTransform); +} + +impl> AffineOps for M { + type Output = M::Output; + + fn affine_transform(&self, transform: &AffineTransform) -> Self::Output { + self.map_coords(|c| transform.apply(c)) + } +} + +impl> AffineOpsMut for M { + fn affine_transform_mut(&mut self, transform: &AffineTransform) { + self.map_coords_in_place(|c| transform.apply(c)) + } +} + +/// A general affine transformation matrix, and associated operations. +/// +/// 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`]) 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. +/// +/// `AffineTransform` is a row-major matrix. +/// 2D affine transforms require six matrix parameters: +/// +/// `[a, b, xoff, d, e, yoff]` +/// +/// these map onto the `AffineTransform` rows as follows: +/// ```ignore +/// [[a, b, xoff], +/// [d, e, yoff], +/// [0, 0, 1]] +/// ``` +/// The equations for transforming coordinates `(x, y) -> (x', y')` are given as follows: +/// +/// `x' = ax + by + xoff` +/// +/// `y' = dx + ey + yoff` +/// +/// # Usage +/// +/// Two types of operation are provided: construction and mutation. **Construction** functions create a *new* transform +/// and are denoted by the use of the **present tense**: `scale()`, `translate()`, `rotate()`, and `skew()`. +/// +/// **Mutation** methods *add* a transform to the existing `AffineTransform`, and are denoted by the use of the past participle: +/// `scaled()`, `translated()`, `rotated()`, and `skewed()`. +/// +/// # Examples +/// ## Build up transforms by beginning with a constructor, then chaining mutation operations +/// ``` +/// use sedona_geo_generic_alg::{AffineOps, AffineTransform}; +/// use sedona_geo_generic_alg::{point, line_string, BoundingRect}; +/// use approx::assert_relative_eq; +/// +/// let line_string = line_string![(x: 0.0, y: 0.0),(x: 1.0, y: 1.0)]; +/// +/// let transform = AffineTransform::translate(1.0, 1.0).scaled(2.0, 2.0, point!(x: 0.0, y: 0.0)); +/// +/// let transformed_line_string = line_string.affine_transform(&transform); +/// +/// assert_relative_eq!( +/// transformed_line_string, +/// line_string![(x: 2.0, y: 2.0),(x: 4.0, y: 4.0)] +/// ); +/// ``` +/// +/// ## Create affine transform manually, and access elements using getter methods +/// ``` +/// use sedona_geo_generic_alg::AffineTransform; +/// +/// let transform = AffineTransform::new(10.0, 0.0, 400_000.0, 0.0, -10.0, 500_000.0); +/// +/// let a: f64 = transform.a(); +/// let b: f64 = transform.b(); +/// let xoff: f64 = transform.xoff(); +/// let d: f64 = transform.d(); +/// let e: f64 = transform.e(); +/// let yoff: f64 = transform.yoff(); +/// assert_eq!(transform, AffineTransform::new(a, b, xoff, d, e, yoff)) +/// ``` + +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct AffineTransform([[T; 3]; 3]); + +impl Default for AffineTransform { + fn default() -> Self { + // identity matrix + Self::identity() + } +} + +impl AffineTransform { + /// Create a new affine transformation by composing two `AffineTransform`s. + /// + /// This is a **cumulative** operation; the new transform is *added* to the existing transform. + #[must_use] + pub fn compose(&self, other: &Self) -> Self { + // lol + Self([ + [ + (other.0[0][0] * self.0[0][0]) + + (other.0[0][1] * self.0[1][0]) + + (other.0[0][2] * self.0[2][0]), + (other.0[0][0] * self.0[0][1]) + + (other.0[0][1] * self.0[1][1]) + + (other.0[0][2] * self.0[2][1]), + (other.0[0][0] * self.0[0][2]) + + (other.0[0][1] * self.0[1][2]) + + (other.0[0][2] * self.0[2][2]), + ], + [ + (other.0[1][0] * self.0[0][0]) + + (other.0[1][1] * self.0[1][0]) + + (other.0[1][2] * self.0[2][0]), + (other.0[1][0] * self.0[0][1]) + + (other.0[1][1] * self.0[1][1]) + + (other.0[1][2] * self.0[2][1]), + (other.0[1][0] * self.0[0][2]) + + (other.0[1][1] * self.0[1][2]) + + (other.0[1][2] * self.0[2][2]), + ], + [ + // this section isn't technically necessary since the last row is invariant: [0, 0, 1] + (other.0[2][0] * self.0[0][0]) + + (other.0[2][1] * self.0[1][0]) + + (other.0[2][2] * self.0[2][0]), + (other.0[2][0] * self.0[0][1]) + + (other.0[2][1] * self.0[1][1]) + + (other.0[2][2] * self.0[2][1]), + (other.0[2][0] * self.0[0][2]) + + (other.0[2][1] * self.0[1][2]) + + (other.0[2][2] * self.0[2][2]), + ], + ]) + } + + /// Create a new affine transformation by composing an arbitrary number of `AffineTransform`s. + /// + /// This is a **cumulative** operation; the new transform is *added* to the existing transform. + /// ``` + /// use sedona_geo_generic_alg::AffineTransform; + /// let mut transform = AffineTransform::identity(); + /// + /// // create two transforms that cancel each other + /// let transform1 = AffineTransform::translate(1.0, 2.0); + /// let transform2 = AffineTransform::translate(-1.0, -2.0); + /// let transforms = vec![transform1, transform2]; + /// + /// // apply them + /// let outcome = transform.compose_many(&transforms); + /// // we should be back to square one + /// assert!(outcome.is_identity()); + /// ``` + #[must_use] + pub fn compose_many(&self, transforms: &[Self]) -> Self { + self.compose(&transforms.iter().fold( + AffineTransform::default(), + |acc: AffineTransform, transform| acc.compose(transform), + )) + } + + /// Create the identity matrix + /// + /// The matrix is: + /// ```ignore + /// [[1, 0, 0], + /// [0, 1, 0], + /// [0, 0, 1]] + /// ``` + pub fn identity() -> Self { + Self::new( + T::one(), + T::zero(), + T::zero(), + T::zero(), + T::one(), + T::zero(), + ) + } + + /// Whether the transformation is equivalent to the [identity matrix](Self::identity), + /// that is, whether it's application will be a a no-op. + /// + /// ``` + /// use sedona_geo_generic_alg::AffineTransform; + /// let mut transform = AffineTransform::identity(); + /// assert!(transform.is_identity()); + /// + /// // mutate the transform a bit + /// transform = transform.translated(1.0, 2.0); + /// assert!(!transform.is_identity()); + /// + /// // put it back + /// transform = transform.translated(-1.0, -2.0); + /// assert!(transform.is_identity()); + /// ``` + pub fn is_identity(&self) -> bool { + self == &Self::identity() + } + + /// **Create** a new affine transform for scaling, scaled by factors along the `x` and `y` dimensions. + /// The point of origin is *usually* given as the 2D bounding box centre of the geometry, but + /// any coordinate may be specified. + /// Negative scale factors will mirror or reflect coordinates. + /// + /// The matrix is: + /// ```ignore + /// [[xfact, 0, xoff], + /// [0, yfact, yoff], + /// [0, 0, 1]] + /// + /// xoff = origin.x - (origin.x * xfact) + /// yoff = origin.y - (origin.y * yfact) + /// ``` + pub fn scale(xfact: T, yfact: T, origin: impl Into>) -> Self { + let (x0, y0) = origin.into().x_y(); + let xoff = x0 - (x0 * xfact); + let yoff = y0 - (y0 * yfact); + Self::new(xfact, T::zero(), xoff, T::zero(), yfact, yoff) + } + + /// **Add** an affine transform for scaling, scaled by factors along the `x` and `y` dimensions. + /// The point of origin is *usually* given as the 2D bounding box centre of the geometry, but + /// any coordinate may be specified. + /// Negative scale factors will mirror or reflect coordinates. + /// This is a **cumulative** operation; the new transform is *added* to the existing transform. + #[must_use] + pub fn scaled(mut self, xfact: T, yfact: T, origin: impl Into>) -> Self { + self.0 = self.compose(&Self::scale(xfact, yfact, origin)).0; + self + } + + /// **Create** an affine transform for translation, shifted by offsets along the `x` and `y` dimensions. + /// + /// The matrix is: + /// ```ignore + /// [[1, 0, xoff], + /// [0, 1, yoff], + /// [0, 0, 1]] + /// ``` + pub fn translate(xoff: T, yoff: T) -> Self { + Self::new(T::one(), T::zero(), xoff, T::zero(), T::one(), yoff) + } + + /// **Add** an affine transform for translation, shifted by offsets along the `x` and `y` dimensions + /// + /// This is a **cumulative** operation; the new transform is *added* to the existing transform. + #[must_use] + pub fn translated(mut self, xoff: T, yoff: T) -> Self { + self.0 = self.compose(&Self::translate(xoff, yoff)).0; + self + } + + /// Apply the current transform to a coordinate + pub fn apply(&self, coord: Coord) -> Coord { + Coord { + x: (self.0[0][0] * coord.x + self.0[0][1] * coord.y + self.0[0][2]), + y: (self.0[1][0] * coord.x + self.0[1][1] * coord.y + self.0[1][2]), + } + } + + /// Create a new custom transform matrix + /// + /// The argument order matches that of the affine transform matrix: + ///```ignore + /// [[a, b, xoff], + /// [d, e, yoff], + /// [0, 0, 1]] <-- not part of the input arguments + /// ``` + pub fn new(a: T, b: T, xoff: T, d: T, e: T, yoff: T) -> Self { + Self([[a, b, xoff], [d, e, yoff], [T::zero(), T::zero(), T::one()]]) + } + + /// See [AffineTransform::new] for this value's role in the affine transformation. + pub fn a(&self) -> T { + self.0[0][0] + } + /// See [AffineTransform::new] for this value's role in the affine transformation. + pub fn b(&self) -> T { + self.0[0][1] + } + /// See [AffineTransform::new] for this value's role in the affine transformation. + pub fn xoff(&self) -> T { + self.0[0][2] + } + /// See [AffineTransform::new] for this value's role in the affine transformation. + pub fn d(&self) -> T { + self.0[1][0] + } + /// See [AffineTransform::new] for this value's role in the affine transformation. + pub fn e(&self) -> T { + self.0[1][1] + } + /// See [AffineTransform::new] for this value's role in the affine transformation. + pub fn yoff(&self) -> T { + self.0[1][2] + } +} + +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") + .field("a", &self.0[0][0]) + .field("b", &self.0[0][1]) + .field("xoff", &self.0[0][2]) + .field("d", &self.0[1][0]) + .field("e", &self.0[1][1]) + .field("yoff", &self.0[1][2]) + .finish() + } +} + +impl From<[T; 6]> for AffineTransform { + fn from(arr: [T; 6]) -> Self { + Self::new(arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]) + } +} + +impl From<(T, T, T, T, T, T)> for AffineTransform { + fn from(tup: (T, T, T, T, T, T)) -> Self { + Self::new(tup.0, tup.1, tup.2, tup.3, tup.4, tup.5) + } +} + +impl AffineTransform { + /// **Create** an affine transform for rotation, using an arbitrary point as its centre. + /// + /// Note that this operation is only available for geometries with floating point coordinates. + /// + /// `angle` is given in **degrees**. + /// + /// The matrix (angle denoted as theta) is: + /// ```ignore + /// [[cos_theta, -sin_theta, xoff], + /// [sin_theta, cos_theta, yoff], + /// [0, 0, 1]] + /// + /// xoff = origin.x - (origin.x * cos(theta)) + (origin.y * sin(theta)) + /// yoff = origin.y - (origin.x * sin(theta)) + (origin.y * cos(theta)) + /// ``` + pub fn rotate(degrees: U, origin: impl Into>) -> Self { + let (sin_theta, cos_theta) = degrees.to_radians().sin_cos(); + let (x0, y0) = origin.into().x_y(); + let xoff = x0 - (x0 * cos_theta) + (y0 * sin_theta); + let yoff = y0 - (x0 * sin_theta) - (y0 * cos_theta); + Self::new(cos_theta, -sin_theta, xoff, sin_theta, cos_theta, yoff) + } + + /// **Add** an affine transform for rotation, using an arbitrary point as its centre. + /// + /// Note that this operation is only available for geometries with floating point coordinates. + /// + /// `angle` is given in **degrees**. + /// + /// This is a **cumulative** operation; the new transform is *added* to the existing transform. + #[must_use] + pub fn rotated(mut self, angle: U, origin: impl Into>) -> Self { + self.0 = self.compose(&Self::rotate(angle, origin)).0; + self + } + + /// **Create** an affine transform for skewing. + /// + /// Note that this operation is only available for geometries with floating point coordinates. + /// + /// Geometries are sheared by angles along x (`xs`) and y (`ys`) dimensions. + /// The point of origin is *usually* given as the 2D bounding box centre of the geometry, but + /// any coordinate may be specified. Angles are given in **degrees**. + /// The matrix is: + /// ```ignore + /// [[1, tan(x), xoff], + /// [tan(y), 1, yoff], + /// [0, 0, 1]] + /// + /// xoff = -origin.y * tan(xs) + /// yoff = -origin.x * tan(ys) + /// ``` + pub fn skew(xs: U, ys: U, origin: impl Into>) -> Self { + let Coord { x: x0, y: y0 } = origin.into(); + let mut tanx = xs.to_radians().tan(); + let mut tany = ys.to_radians().tan(); + // These checks are stolen from Shapely's implementation -- may not be necessary + if tanx.abs() < U::from::(2.5e-16).unwrap() { + tanx = U::zero(); + } + if tany.abs() < U::from::(2.5e-16).unwrap() { + tany = U::zero(); + } + let xoff = -y0 * tanx; + let yoff = -x0 * tany; + Self::new(U::one(), tanx, xoff, tany, U::one(), yoff) + } + + /// **Add** an affine transform for skewing. + /// + /// Note that this operation is only available for geometries with floating point coordinates. + /// + /// Geometries are sheared by angles along x (`xs`) and y (`ys`) dimensions. + /// The point of origin is *usually* given as the 2D bounding box centre of the geometry, but + /// any coordinate may be specified. Angles are given in **degrees**. + /// + /// This is a **cumulative** operation; the new transform is *added* to the existing transform. + #[must_use] + pub fn skewed(mut self, xs: U, ys: U, origin: impl Into>) -> Self { + self.0 = self.compose(&Self::skew(xs, ys, origin)).0; + self + } +} + +#[cfg(test)] +mod tests { + use approx::{AbsDiffEq, RelativeEq}; + + 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)) + } + } + + 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)) + } + } + + use super::*; + use crate::{wkt, Point}; + + // given a matrix with the shape + // [[a, b, xoff], + // [d, e, yoff], + // [0, 0, 1]] + #[test] + fn matrix_multiply() { + let a = AffineTransform::new(1, 2, 5, 3, 4, 6); + let b = AffineTransform::new(7, 8, 11, 9, 10, 12); + let composed = a.compose(&b); + assert_eq!(composed.0[0][0], 31); + assert_eq!(composed.0[0][1], 46); + assert_eq!(composed.0[0][2], 94); + assert_eq!(composed.0[1][0], 39); + assert_eq!(composed.0[1][1], 58); + assert_eq!(composed.0[1][2], 117); + } + #[test] + fn test_transform_composition() { + let p0 = Point::new(0.0f64, 0.0); + // scale once + let mut scale_a = AffineTransform::default().scaled(2.0, 2.0, p0); + // rotate + scale_a = scale_a.rotated(45.0, p0); + // rotate back + scale_a = scale_a.rotated(-45.0, p0); + // scale up again, doubling + scale_a = scale_a.scaled(2.0, 2.0, p0); + // scaled once + let scale_b = AffineTransform::default().scaled(2.0, 2.0, p0); + // 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_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 = wkt! { POLYGON((0.0 0.0,0.0 2.0,1.0 2.0)) }; + poly.affine_transform_mut(&transform); + + let expected = wkt! { POLYGON((2.0 2.0,2.0 6.0,4.0 6.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); + } + #[test] + fn test_affine_transform_getters() { + let transform = AffineTransform::new(10.0, 0.0, 400_000.0, 0.0, -10.0, 500_000.0); + assert_eq!(transform.a(), 10.0); + assert_eq!(transform.b(), 0.0); + assert_eq!(transform.xoff(), 400_000.0); + assert_eq!(transform.d(), 0.0); + assert_eq!(transform.e(), -10.0); + assert_eq!(transform.yoff(), 500_000.0); + } + #[test] + fn test_compose() { + let point = Point::new(1., 0.); + + let translate = AffineTransform::translate(1., 0.); + let scale = AffineTransform::scale(4., 1., [0., 0.]); + let composed = translate.compose(&scale); + + assert_eq!(point.affine_transform(&translate), Point::new(2., 0.)); + assert_eq!(point.affine_transform(&scale), Point::new(4., 0.)); + assert_eq!( + point.affine_transform(&translate).affine_transform(&scale), + Point::new(8., 0.) + ); + + assert_eq!(point.affine_transform(&composed), Point::new(8., 0.)); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/area.rs b/rust/sedona-geo-generic-alg/src/algorithm/area.rs new file mode 100644 index 00000000..6e0f14bc --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/area.rs @@ -0,0 +1,621 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use sedona_geo_traits_ext::*; + +use crate::{CoordFloat, CoordNum}; +use core::borrow::Borrow; + +pub(crate) fn twice_signed_ring_area>( + linestring: &LS, +) -> T { + // LineString with less than 3 points is empty, or a + // single point, or is not closed. + let num_coords = linestring.num_coords(); + if num_coords < 3 { + return T::zero(); + } + + unsafe { + // Above test ensures the vector has at least 2 elements. + // We check if linestring is closed, and return 0 otherwise. + if linestring.geo_coord_unchecked(0) != linestring.geo_coord_unchecked(num_coords - 1) { + return T::zero(); + } + + // Use a reasonable shift for the line-string coords + // to avoid numerical-errors when summing the + // determinants. + // + // Note: we can't use the `Centroid` trait as it + // requires `T: Float` and in fact computes area in the + // implementation. Another option is to use the average + // 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.geo_coord_unchecked(0); + + let mut tmp = T::zero(); + for line in linestring.lines() { + use crate::MapCoords; + let line = line.map_coords(|c| c - shift); + tmp = tmp + line.determinant(); + } + + tmp + } +} + +/// Signed and unsigned planar area of a geometry. +/// +/// # Examples +/// +/// ``` +/// use sedona_geo_generic_alg::polygon; +/// use sedona_geo_generic_alg::Area; +/// +/// let mut polygon = polygon![ +/// (x: 0., y: 0.), +/// (x: 5., y: 0.), +/// (x: 5., y: 6.), +/// (x: 0., y: 6.), +/// (x: 0., y: 0.), +/// ]; +/// +/// assert_eq!(polygon.signed_area(), 30.); +/// assert_eq!(polygon.unsigned_area(), 30.); +/// +/// polygon.exterior_mut(|line_string| { +/// line_string.0.reverse(); +/// }); +/// +/// assert_eq!(polygon.signed_area(), -30.); +/// assert_eq!(polygon.unsigned_area(), 30.); +/// ``` +pub trait Area +where + T: CoordNum, +{ + fn signed_area(&self) -> T; + + fn unsigned_area(&self) -> T; +} + +impl Area for G +where + T: CoordNum, + G: GeoTraitExtWithTypeTag + AreaTrait, +{ + fn signed_area(&self) -> T { + self.signed_area_trait() + } + + fn unsigned_area(&self) -> T { + self.unsigned_area_trait() + } +} + +trait AreaTrait +where + T: CoordNum, +{ + fn signed_area_trait(&self) -> T; + + fn unsigned_area_trait(&self) -> T; +} + +// Calculation of simple (no interior holes) Polygon area +pub(crate) fn get_linestring_area>(linestring: &LS) -> T +where + T: CoordFloat, +{ + twice_signed_ring_area(linestring) / (T::one() + T::one()) +} + +impl> AreaTrait for P +where + T: CoordNum, +{ + fn signed_area_trait(&self) -> T { + T::zero() + } + + fn unsigned_area_trait(&self) -> T { + T::zero() + } +} + +impl> AreaTrait for LS +where + T: CoordNum, +{ + fn signed_area_trait(&self) -> T { + T::zero() + } + + fn unsigned_area_trait(&self) -> T { + T::zero() + } +} + +impl> AreaTrait for L +where + T: CoordNum, +{ + fn signed_area_trait(&self) -> T { + T::zero() + } + + fn unsigned_area_trait(&self) -> T { + T::zero() + } +} + +/// **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> AreaTrait for P +where + T: CoordFloat, +{ + fn signed_area_trait(&self) -> T { + match self.exterior_ext() { + Some(exterior) => { + let area = get_linestring_area(&exterior); + + // We could use winding order here, but that would + // result in computing the shoelace formula twice. + let is_negative = area < T::zero(); + + let area = self.interiors_ext().fold(area.abs(), |total, next| { + total - get_linestring_area(&next).abs() + }); + + if is_negative { + -area + } else { + area + } + } + None => T::zero(), + } + } + + fn unsigned_area_trait(&self) -> T { + self.signed_area_trait().abs() + } +} + +impl> AreaTrait for MP +where + T: CoordNum, +{ + fn signed_area_trait(&self) -> T { + T::zero() + } + + fn unsigned_area_trait(&self) -> T { + T::zero() + } +} + +impl> AreaTrait for MLS +where + T: CoordNum, +{ + fn signed_area_trait(&self) -> T { + T::zero() + } + + fn unsigned_area_trait(&self) -> T { + T::zero() + } +} + +/// **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> AreaTrait for MP +where + T: CoordFloat, +{ + fn signed_area_trait(&self) -> T { + self.polygons_ext() + .fold(T::zero(), |total, next| total + next.signed_area_trait()) + } + + fn unsigned_area_trait(&self) -> T { + self.polygons_ext().fold(T::zero(), |total, next| { + total + next.signed_area_trait().abs() + }) + } +} + +/// Because a `Rect` has no winding order, the area will always be positive. +impl> AreaTrait for R +where + T: CoordNum, +{ + fn signed_area_trait(&self) -> T { + self.width() * self.height() + } + + fn unsigned_area_trait(&self) -> T { + self.width() * self.height() + } +} + +impl> AreaTrait for TT +where + T: CoordFloat, +{ + fn signed_area_trait(&self) -> T { + self.to_lines() + .iter() + .fold(T::zero(), |total, line| total + line.determinant()) + / (T::one() + T::one()) + } + + fn unsigned_area_trait(&self) -> T { + self.signed_area_trait().abs() + } +} + +impl> AreaTrait for GC +where + T: CoordFloat, +{ + fn signed_area_trait(&self) -> T { + self.geometries_ext() + .map(|g| g.signed_area_trait()) + .fold(T::zero(), |acc, next| acc + next) + } + + fn unsigned_area_trait(&self) -> T { + self.geometries_ext() + .map(|g| g.unsigned_area_trait()) + .fold(T::zero(), |acc, next| acc + next) + } +} + +impl> AreaTrait for G +where + T: CoordFloat, +{ + fn signed_area_trait(&self) -> T { + if self.is_collection() { + self.geometries_ext() + .map(|g_inner| g_inner.borrow().signed_area_trait()) + .fold(T::zero(), |acc, next| acc + next) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => g.signed_area_trait(), + GeometryTypeExt::Line(g) => g.signed_area_trait(), + GeometryTypeExt::LineString(g) => g.signed_area_trait(), + GeometryTypeExt::Polygon(g) => g.signed_area_trait(), + GeometryTypeExt::MultiPoint(g) => g.signed_area_trait(), + GeometryTypeExt::MultiLineString(g) => g.signed_area_trait(), + GeometryTypeExt::MultiPolygon(g) => g.signed_area_trait(), + GeometryTypeExt::Rect(g) => g.signed_area_trait(), + GeometryTypeExt::Triangle(g) => g.signed_area_trait(), + } + } + } + + fn unsigned_area_trait(&self) -> T { + if self.is_collection() { + self.geometries_ext() + .map(|g_inner| g_inner.borrow().unsigned_area_trait()) + .fold(T::zero(), |acc, next| acc + next) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => g.unsigned_area_trait(), + GeometryTypeExt::Line(g) => g.unsigned_area_trait(), + GeometryTypeExt::LineString(g) => g.unsigned_area_trait(), + GeometryTypeExt::Polygon(g) => g.unsigned_area_trait(), + GeometryTypeExt::MultiPoint(g) => g.unsigned_area_trait(), + GeometryTypeExt::MultiLineString(g) => g.unsigned_area_trait(), + GeometryTypeExt::MultiPolygon(g) => g.unsigned_area_trait(), + GeometryTypeExt::Rect(g) => g.unsigned_area_trait(), + GeometryTypeExt::Triangle(g) => g.unsigned_area_trait(), + } + } + } +} + +#[cfg(test)] +mod test { + use crate::Area; + use crate::{coord, polygon, wkt, Line, MultiPolygon, Polygon, Rect, Triangle}; + + // Area of the polygon + #[test] + fn area_empty_polygon_test() { + let poly: Polygon = polygon![]; + assert_relative_eq!(poly.signed_area(), 0.); + } + + #[test] + fn area_one_point_polygon_test() { + let poly = wkt! { POLYGON((1. 0.)) }; + assert_relative_eq!(poly.signed_area(), 0.); + } + #[test] + fn area_polygon_test() { + let polygon = wkt! { POLYGON((0. 0.,5. 0.,5. 6.,0. 6.,0. 0.)) }; + assert_relative_eq!(polygon.signed_area(), 30.); + } + #[test] + fn area_polygon_numerical_stability() { + let polygon = { + use std::f64::consts::PI; + const NUM_VERTICES: usize = 10; + const ANGLE_INC: f64 = 2. * PI / NUM_VERTICES as f64; + + Polygon::new( + (0..NUM_VERTICES) + .map(|i| { + let angle = i as f64 * ANGLE_INC; + coord! { + x: angle.cos(), + y: angle.sin(), + } + }) + .collect::>() + .into(), + vec![], + ) + }; + + let area = polygon.signed_area(); + + let shift = coord! { x: 1.5e8, y: 1.5e8 }; + + use crate::map_coords::MapCoords; + let polygon = polygon.map_coords(|c| c + shift); + + let new_area = polygon.signed_area(); + let err = (area - new_area).abs() / area; + + assert!(err < 1e-2); + } + #[test] + fn rectangle_test() { + let rect1: Rect = Rect::new(coord! { x: 10., y: 30. }, coord! { x: 20., y: 40. }); + assert_relative_eq!(rect1.signed_area(), 100.); + + let rect2: Rect = Rect::new(coord! { x: 10, y: 30 }, coord! { x: 20, y: 40 }); + assert_eq!(rect2.signed_area(), 100); + } + #[test] + fn area_polygon_inner_test() { + let poly = polygon![ + exterior: [ + (x: 0., y: 0.), + (x: 10., y: 0.), + (x: 10., y: 10.), + (x: 0., y: 10.), + (x: 0., y: 0.) + ], + interiors: [ + [ + (x: 1., y: 1.), + (x: 2., y: 1.), + (x: 2., y: 2.), + (x: 1., y: 2.), + (x: 1., y: 1.), + ], + [ + (x: 5., y: 5.), + (x: 6., y: 5.), + (x: 6., y: 6.), + (x: 5., y: 6.), + (x: 5., y: 5.) + ], + ], + ]; + assert_relative_eq!(poly.signed_area(), 98.); + } + #[test] + fn area_multipolygon_test() { + let poly0 = polygon![ + (x: 0., y: 0.), + (x: 10., y: 0.), + (x: 10., y: 10.), + (x: 0., y: 10.), + (x: 0., y: 0.) + ]; + let poly1 = polygon![ + (x: 1., y: 1.), + (x: 2., y: 1.), + (x: 2., y: 2.), + (x: 1., y: 2.), + (x: 1., y: 1.) + ]; + let poly2 = polygon![ + (x: 5., y: 5.), + (x: 6., y: 5.), + (x: 6., y: 6.), + (x: 5., y: 6.), + (x: 5., y: 5.) + ]; + let mpoly = MultiPolygon::new(vec![poly0, poly1, poly2]); + assert_relative_eq!(mpoly.signed_area(), 102.); + assert_relative_eq!(mpoly.signed_area(), 102.); + } + #[test] + fn area_line_test() { + let line1 = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 1.0, y: 1.0 }); + assert_relative_eq!(line1.signed_area(), 0.); + } + + #[test] + fn area_triangle_test() { + let triangle = Triangle::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 0.0, y: 1.0 }, + ); + assert_relative_eq!(triangle.signed_area(), 0.5); + + let triangle = Triangle::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 0.0, y: 1.0 }, + coord! { x: 1.0, y: 0.0 }, + ); + // triangles are always ccw, thus positive + assert_relative_eq!(triangle.signed_area(), 0.5); + } + + #[test] + fn area_geometry_test() { + let geom = wkt! { + MULTIPOLYGON( + ((0. 0.,5. 0.,5. 6.,0. 6.,0. 0.)), + ((1. 1.,2. 1.,2. 2.,1. 2.,1. 1.)) + ) + }; + assert_relative_eq!(geom.signed_area(), 31.0); + } + + #[test] + fn area_multi_polygon_area_reversed() { + let polygon_cw: Polygon = polygon![ + coord! { x: 0.0, y: 0.0 }, + coord! { x: 0.0, y: 1.0 }, + coord! { x: 1.0, y: 1.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 0.0, y: 0.0 }, + ]; + let polygon_ccw: Polygon = polygon![ + coord! { x: 0.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 1.0, y: 1.0 }, + coord! { x: 0.0, y: 1.0 }, + coord! { x: 0.0, y: 0.0 }, + ]; + let polygon_area = polygon_cw.unsigned_area(); + + let multi_polygon = MultiPolygon::new(vec![polygon_cw, polygon_ccw]); + + assert_eq!(polygon_area * 2., multi_polygon.unsigned_area()); + } + + #[test] + fn area_north_america_cutout() { + let poly = polygon![ + exterior: [ + (x: -102.902861858977, y: 31.6943450891131), + (x: -102.917375513247, y: 31.6990175356827), + (x: -102.917887344527, y: 31.7044889522597), + (x: -102.938892711173, y: 31.7032871894594), + (x: -102.939919687305, y: 31.7142296141915), + (x: -102.946922353444, y: 31.713828170995), + (x: -102.954642979004, y: 31.7210594956594), + (x: -102.960927457803, y: 31.7130240707676), + (x: -102.967929895872, y: 31.7126214137469), + (x: -102.966383373178, y: 31.6962079209847), + (x: -102.973384192133, y: 31.6958049292994), + (x: -102.97390013779, y: 31.701276160078), + (x: -102.980901394769, y: 31.7008727405409), + (x: -102.987902575456, y: 31.7004689164622), + (x: -102.986878877087, y: 31.7127206248263), + (x: -102.976474089689, y: 31.7054378797983), + (x: -102.975448432121, y: 31.7176893134691), + (x: -102.96619351228, y: 31.7237224912303), + (x: -102.976481009643, y: 31.7286309669534), + (x: -102.976997412845, y: 31.7341016591658), + (x: -102.978030448215, y: 31.7450427747035), + (x: -102.985035821671, y: 31.7446391683265), + (x: -102.985552968771, y: 31.7501095683386), + (x: -102.992558780682, y: 31.7497055338313), + (x: -102.993594334215, y: 31.7606460184322), + (x: -102.973746840657, y: 31.7546100958509), + (x: -102.966082339116, y: 31.767730116605), + (x: -102.959074676589, y: 31.768132602064), + (x: -102.95206693787, y: 31.7685346826851), + (x: -102.953096767614, y: 31.7794749110023), + (x: -102.953611796704, y: 31.7849448911322), + (x: -102.952629078076, y: 31.7996518517642), + (x: -102.948661251495, y: 31.8072257578725), + (x: -102.934638176282, y: 31.8080282207231), + (x: -102.927626524626, y: 31.8084288446215), + (x: -102.927113253813, y: 31.8029591283411), + (x: -102.920102042027, y: 31.8033593239799), + (x: -102.919076759513, y: 31.792419577395), + (x: -102.912066503301, y: 31.7928193216213), + (x: -102.911554491357, y: 31.7873492912889), + (x: -102.904544675025, y: 31.7877486073783), + (x: -102.904033254331, y: 31.7822784646103), + (x: -102.903521909259, y: 31.7768082325431), + (x: -102.895800463718, y: 31.7695748336589), + (x: -102.889504111843, y: 31.7776055573633), + (x: -102.882495099915, y: 31.7780036124077), + (x: -102.868476849997, y: 31.7787985077398), + (x: -102.866950998738, y: 31.7623869292283), + (x: -102.873958615171, y: 31.7619897531194), + (x: -102.87888647278, y: 31.7688910039026), + (x: -102.879947237315, y: 31.750650764952), + (x: -102.886953672823, y: 31.750252825268), + (x: -102.89396003296, y: 31.7498544807869), + (x: -102.892939355062, y: 31.7389128078806), + (x: -102.913954892669, y: 31.7377154844276), + (x: -102.913443122277, y: 31.7322445829725), + (x: -102.912931427507, y: 31.7267735918962), + (x: -102.911908264767, y: 31.7158313407426), + (x: -102.904905220014, y: 31.7162307607961), + (x: -102.904394266551, y: 31.7107594775392), + (x: -102.903372586049, y: 31.6998166417321), + (x: -102.902861858977, y: 31.6943450891131), + ], + interiors: [ + [ + (x: -102.916514879554, y: 31.7650686485918), + (x: -102.921022256876, y: 31.7770831833398), + (x: -102.93367363719, y: 31.771184865332), + (x: -102.916514879554, y: 31.7650686485918), + ], + [ + (x: -102.935483140202, y: 31.7419852607081), + (x: -102.932452314332, y: 31.7328567234689), + (x: -102.918345099146, y: 31.7326099897391), + (x: -102.925566322952, y: 31.7552505533503), + (x: -102.928990700436, y: 31.747856686604), + (x: -102.935996606762, y: 31.7474559134477), + (x: -102.939021176592, y: 31.7539885279379), + (x: -102.944714388971, y: 31.7488395547293), + (x: -102.935996606762, y: 31.7474559134477), + (x: -102.935483140202, y: 31.7419852607081), + ], + [ + (x: -102.956498858767, y: 31.7407805824758), + (x: -102.960959476367, y: 31.7475080456347), + (x: -102.972817445204, y: 31.742072061889), + (x: -102.956498858767, y: 31.7407805824758), + ] + ], + ]; + // Value from shapely + assert_relative_eq!( + poly.unsigned_area(), + 0.006547948219252177, + max_relative = 0.0001 + ); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/bounding_rect.rs b/rust/sedona-geo-generic-alg/src/algorithm/bounding_rect.rs new file mode 100644 index 00000000..ede5c84d --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/bounding_rect.rs @@ -0,0 +1,419 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use crate::utils::{partial_max, partial_min}; +use crate::{coord, geometry::*, CoordNum}; +use core::borrow::Borrow; +use geo_types::private_utils::get_bounding_rect; +use sedona_geo_traits_ext::*; + +/// Calculation of the bounding rectangle of a geometry. +pub trait BoundingRect { + type Output: Into>>; + + /// Return the bounding rectangle of a geometry + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::BoundingRect; + /// use sedona_geo_generic_alg::line_string; + /// + /// let line_string = line_string![ + /// (x: 40.02f64, y: 116.34), + /// (x: 42.02f64, y: 116.34), + /// (x: 42.02f64, y: 118.34), + /// ]; + /// + /// let bounding_rect = line_string.bounding_rect().unwrap(); + /// + /// assert_eq!(40.02f64, bounding_rect.min().x); + /// assert_eq!(42.02f64, bounding_rect.max().x); + /// assert_eq!(116.34, bounding_rect.min().y); + /// assert_eq!(118.34, bounding_rect.max().y); + /// ``` + fn bounding_rect(&self) -> Self::Output; +} + +impl BoundingRect for G +where + T: CoordNum, + G: GeoTraitExtWithTypeTag + BoundingRectTrait, +{ + type Output = G::Output; + + fn bounding_rect(&self) -> Self::Output { + self.bounding_rect_trait() + } +} + +pub trait BoundingRectTrait +where + T: CoordNum, +{ + type Output: Into>>; + + fn bounding_rect_trait(&self) -> Self::Output; +} + +impl> BoundingRectTrait for C +where + T: CoordNum, +{ + type Output = Rect; + + /// Return the bounding rectangle for a `Coord`. It will have zero width + /// and zero height. + fn bounding_rect_trait(&self) -> Self::Output { + Rect::new(self.geo_coord(), self.geo_coord()) + } +} + +impl> BoundingRectTrait for P +where + T: CoordNum, +{ + type Output = Rect; + + /// Return the bounding rectangle for a `Point`. It will have zero width + /// and zero height. + fn bounding_rect_trait(&self) -> Self::Output { + match self.geo_coord() { + Some(coord) => Rect::new(coord, coord), + None => { + let zero = Coord::::zero(); + Rect::new(zero, zero) + } + } + } +} + +impl> BoundingRectTrait for MP +where + T: CoordNum, +{ + type Output = Option>; + + /// + /// Return the BoundingRect for a MultiPoint + fn bounding_rect_trait(&self) -> Self::Output { + get_bounding_rect(self.coord_iter()) + } +} + +impl> BoundingRectTrait for L +where + T: CoordNum, +{ + type Output = Rect; + + fn bounding_rect_trait(&self) -> Self::Output { + Rect::new(self.start_coord(), self.end_coord()) + } +} + +impl> BoundingRectTrait for LS +where + T: CoordNum, +{ + type Output = Option>; + + /// + /// Return the BoundingRect for a LineString + fn bounding_rect_trait(&self) -> Self::Output { + get_bounding_rect(self.coord_iter()) + } +} + +impl> BoundingRectTrait for MLS +where + T: CoordNum, +{ + type Output = Option>; + + /// + /// Return the BoundingRect for a MultiLineString + fn bounding_rect_trait(&self) -> Self::Output { + self.line_strings_ext().fold(None, |acc, p| { + let rect = p.bounding_rect_trait(); + match (acc, rect) { + (None, None) => None, + (Some(r), None) | (None, Some(r)) => Some(r), + (Some(r1), Some(r2)) => Some(bounding_rect_merge(r1, r2)), + } + }) + } +} + +impl> BoundingRectTrait for P +where + T: CoordNum, +{ + type Output = Option>; + + /// + /// Return the BoundingRect for a Polygon + fn bounding_rect_trait(&self) -> Self::Output { + let exterior = self.exterior_ext(); + exterior.and_then(|e| get_bounding_rect(e.coord_iter())) + } +} + +impl> BoundingRectTrait for MP +where + T: CoordNum, +{ + type Output = Option>; + + /// + /// Return the BoundingRect for a MultiPolygon + fn bounding_rect_trait(&self) -> Self::Output { + self.polygons_ext().fold(None, |acc, p| { + let rect = p + .exterior_ext() + .and_then(|e| get_bounding_rect(e.coord_iter())); + match (acc, rect) { + (None, None) => None, + (Some(r), None) | (None, Some(r)) => Some(r), + (Some(r1), Some(r2)) => Some(bounding_rect_merge(r1, r2)), + } + }) + } +} + +impl> BoundingRectTrait for TT +where + T: CoordNum, +{ + type Output = Rect; + + fn bounding_rect_trait(&self) -> Self::Output { + get_bounding_rect(self.coord_iter()).unwrap() + } +} + +impl> BoundingRectTrait for R +where + T: CoordNum, +{ + type Output = Rect; + + fn bounding_rect_trait(&self) -> Self::Output { + self.geo_rect() + } +} + +impl> BoundingRectTrait for GC +where + T: CoordNum, +{ + type Output = Option>; + + fn bounding_rect_trait(&self) -> Self::Output { + self.geometries_ext().fold(None, |acc, next| { + let next_bounding_rect = next.bounding_rect_trait(); + + match (acc, next_bounding_rect) { + (None, None) => None, + (Some(r), None) | (None, Some(r)) => Some(r), + (Some(r1), Some(r2)) => Some(bounding_rect_merge(r1, r2)), + } + }) + } +} + +impl> BoundingRectTrait for G +where + T: CoordNum, +{ + type Output = Option>; + + fn bounding_rect_trait(&self) -> Self::Output { + if self.is_collection() { + self.geometries_ext().fold(None, |acc, next| { + let next_bounding_rect = next.borrow().bounding_rect_trait(); + + match (acc, next_bounding_rect) { + (None, None) => None, + (Some(r), None) | (None, Some(r)) => Some(r), + (Some(r1), Some(r2)) => Some(bounding_rect_merge(r1, r2)), + } + }) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => g.bounding_rect_trait().into(), + GeometryTypeExt::Line(g) => g.bounding_rect_trait().into(), + GeometryTypeExt::LineString(g) => g.bounding_rect_trait(), + GeometryTypeExt::Polygon(g) => g.bounding_rect_trait(), + GeometryTypeExt::MultiPoint(g) => g.bounding_rect_trait(), + GeometryTypeExt::MultiLineString(g) => g.bounding_rect_trait(), + GeometryTypeExt::MultiPolygon(g) => g.bounding_rect_trait(), + GeometryTypeExt::Rect(g) => g.bounding_rect_trait().into(), + GeometryTypeExt::Triangle(g) => g.bounding_rect_trait().into(), + } + } + } +} + +// Return a new rectangle that encompasses the provided rectangles +fn bounding_rect_merge(a: Rect, b: Rect) -> Rect { + Rect::new( + coord! { + x: partial_min(a.min().x, b.min().x), + y: partial_min(a.min().y, b.min().y), + }, + coord! { + x: partial_max(a.max().x, b.max().x), + y: partial_max(a.max().y, b.max().y), + }, + ) +} + +#[cfg(test)] +mod test { + use super::bounding_rect_merge; + use crate::line_string; + use crate::BoundingRect; + use crate::{ + coord, point, polygon, Geometry, GeometryCollection, Line, LineString, MultiLineString, + MultiPoint, MultiPolygon, Polygon, Rect, + }; + + #[test] + fn empty_linestring_test() { + let linestring: LineString = line_string![]; + let bounding_rect = linestring.bounding_rect(); + assert!(bounding_rect.is_none()); + } + #[test] + fn linestring_one_point_test() { + let linestring = line_string![(x: 40.02f64, y: 116.34)]; + let bounding_rect = Rect::new( + coord! { + x: 40.02f64, + y: 116.34, + }, + coord! { + x: 40.02, + y: 116.34, + }, + ); + assert_eq!(bounding_rect, linestring.bounding_rect().unwrap()); + } + #[test] + fn linestring_test() { + let linestring = line_string![ + (x: 1., y: 1.), + (x: 2., y: -2.), + (x: -3., y: -3.), + (x: -4., y: 4.) + ]; + let bounding_rect = Rect::new(coord! { x: -4., y: -3. }, coord! { x: 2., y: 4. }); + assert_eq!(bounding_rect, linestring.bounding_rect().unwrap()); + } + #[test] + fn multilinestring_test() { + let multiline = MultiLineString::new(vec![ + line_string![(x: 1., y: 1.), (x: -40., y: 1.)], + line_string![(x: 1., y: 1.), (x: 50., y: 1.)], + line_string![(x: 1., y: 1.), (x: 1., y: -60.)], + line_string![(x: 1., y: 1.), (x: 1., y: 70.)], + ]); + let bounding_rect = Rect::new(coord! { x: -40., y: -60. }, coord! { x: 50., y: 70. }); + assert_eq!(bounding_rect, multiline.bounding_rect().unwrap()); + } + #[test] + fn multipoint_test() { + let multipoint = MultiPoint::from(vec![(1., 1.), (2., -2.), (-3., -3.), (-4., 4.)]); + let bounding_rect = Rect::new(coord! { x: -4., y: -3. }, coord! { x: 2., y: 4. }); + assert_eq!(bounding_rect, multipoint.bounding_rect().unwrap()); + } + #[test] + fn polygon_test() { + let linestring = line_string![ + (x: 0., y: 0.), + (x: 5., y: 0.), + (x: 5., y: 6.), + (x: 0., y: 6.), + (x: 0., y: 0.), + ]; + let line_bounding_rect = linestring.bounding_rect().unwrap(); + let poly = Polygon::new(linestring, Vec::new()); + assert_eq!(line_bounding_rect, poly.bounding_rect().unwrap()); + } + #[test] + fn multipolygon_test() { + let mpoly = MultiPolygon::new(vec![ + polygon![(x: 0., y: 0.), (x: 50., y: 0.), (x: 0., y: -70.), (x: 0., y: 0.)], + polygon![(x: 0., y: 0.), (x: 5., y: 0.), (x: 0., y: 80.), (x: 0., y: 0.)], + polygon![(x: 0., y: 0.), (x: -60., y: 0.), (x: 0., y: 6.), (x: 0., y: 0.)], + ]); + let bounding_rect = Rect::new(coord! { x: -60., y: -70. }, coord! { x: 50., y: 80. }); + assert_eq!(bounding_rect, mpoly.bounding_rect().unwrap()); + } + #[test] + fn line_test() { + let line1 = Line::new(coord! { x: 0., y: 1. }, coord! { x: 2., y: 3. }); + let line2 = Line::new(coord! { x: 2., y: 3. }, coord! { x: 0., y: 1. }); + assert_eq!( + line1.bounding_rect(), + Rect::new(coord! { x: 0., y: 1. }, coord! { x: 2., y: 3. },) + ); + assert_eq!( + line2.bounding_rect(), + Rect::new(coord! { x: 0., y: 1. }, coord! { x: 2., y: 3. },) + ); + } + + #[test] + fn bounding_rect_merge_test() { + assert_eq!( + bounding_rect_merge( + Rect::new(coord! { x: 0., y: 0. }, coord! { x: 1., y: 1. }), + Rect::new(coord! { x: 1., y: 1. }, coord! { x: 2., y: 2. }), + ), + Rect::new(coord! { x: 0., y: 0. }, coord! { x: 2., y: 2. }), + ); + } + + #[test] + fn point_bounding_rect_test() { + assert_eq!( + Rect::new(coord! { x: 1., y: 2. }, coord! { x: 1., y: 2. }), + point! { x: 1., y: 2. }.bounding_rect(), + ); + } + + #[test] + fn geometry_collection_bounding_rect_test() { + assert_eq!( + Some(Rect::new(coord! { x: 0., y: 0. }, coord! { x: 1., y: 2. })), + GeometryCollection::new_from(vec![ + Geometry::Point(point! { x: 0., y: 0. }), + Geometry::Point(point! { x: 1., y: 2. }), + ]) + .bounding_rect(), + ); + assert_eq!( + Some(Rect::new(coord! { x: 0., y: 0. }, coord! { x: 1., y: 2. })), + Geometry::GeometryCollection(GeometryCollection::new_from(vec![ + Geometry::Point(point! { x: 0., y: 0. }), + Geometry::Point(point! { x: 1., y: 2. }), + ])) + .bounding_rect(), + ); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/centroid.rs b/rust/sedona-geo-generic-alg/src/algorithm/centroid.rs new file mode 100644 index 00000000..45f5d63d --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/centroid.rs @@ -0,0 +1,1233 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use core::borrow::Borrow; +use std::cmp::Ordering; + +use sedona_geo_traits_ext::*; + +use crate::area::{get_linestring_area, Area}; +use crate::dimensions::{Dimensions, Dimensions::*, HasDimensions}; +use crate::geometry::*; +use crate::line_measures::metric_spaces::euclidean::Euclidean; +use crate::line_measures::LengthMeasurableExt; +use crate::GeoFloat; + +/// Calculation of the centroid. +/// The centroid is the arithmetic mean position of all points in the shape. +/// Informally, it is the point at which a cutout of the shape could be perfectly +/// balanced on the tip of a pin. +/// The geometric centroid of a convex object always lies in the object. +/// A non-convex object might have a centroid that _is outside the object itself_. +/// +/// # Examples +/// +/// ``` +/// use sedona_geo_generic_alg::Centroid; +/// use sedona_geo_generic_alg::{point, polygon}; +/// +/// // rhombus shaped polygon +/// let polygon = polygon![ +/// (x: -2., y: 1.), +/// (x: 1., y: 3.), +/// (x: 4., y: 1.), +/// (x: 1., y: -1.), +/// (x: -2., y: 1.), +/// ]; +/// +/// assert_eq!( +/// Some(point!(x: 1., y: 1.)), +/// polygon.centroid(), +/// ); +/// ``` +pub trait Centroid { + type Output; + + /// See: + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{line_string, point}; + /// + /// let line_string = line_string![ + /// (x: 40.02f64, y: 116.34), + /// (x: 40.02f64, y: 118.23), + /// ]; + /// + /// assert_eq!( + /// Some(point!(x: 40.02, y: 117.285)), + /// line_string.centroid(), + /// ); + /// ``` + fn centroid(&self) -> Self::Output; +} + +impl Centroid for G +where + G: GeoTraitExtWithTypeTag + CentroidTrait, +{ + type Output = G::Output; + + fn centroid(&self) -> Self::Output { + self.centroid_trait() + } +} + +pub trait CentroidTrait { + type Output; + + fn centroid_trait(&self) -> Self::Output; +} + +impl CentroidTrait for L +where + L: LineTraitExt, + T: GeoFloat, +{ + type Output = Point; + + /// The Centroid of a [`Line`] is its middle point + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{Line, point}; + /// + /// let line = Line::new( + /// point!(x: 1.0f64, y: 3.0), + /// point!(x: 2.0f64, y: 4.0), + /// ); + /// + /// assert_eq!( + /// point!(x: 1.5, y: 3.5), + /// line.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let two = T::one() + T::one(); + let start = self.start_coord(); + let end = self.end_coord(); + let center = (start + end) / two; + center.into() + } +} + +impl CentroidTrait for LS +where + LS: LineStringTraitExt, + T: GeoFloat, +{ + type Output = Option>; + + // The Centroid of a [`LineString`] is the mean of the middle of the segment + // weighted by the length of the segments. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{line_string, point}; + /// + /// let line_string = line_string![ + /// (x: 1.0f32, y: 1.0), + /// (x: 2.0, y: 2.0), + /// (x: 4.0, y: 4.0) + /// ]; + /// + /// assert_eq!( + /// // (1.0 * (1.5, 1.5) + 2.0 * (3.0, 3.0)) / 3.0 + /// Some(point!(x: 2.5, y: 2.5)), + /// line_string.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let mut operation = CentroidOperation::new(); + operation.add_line_string(self); + operation.centroid() + } +} + +impl CentroidTrait for MLS +where + MLS: MultiLineStringTraitExt, + T: GeoFloat, +{ + type Output = Option>; + + /// The Centroid of a [`MultiLineString`] is the mean of the centroids of all the constituent linestrings, + /// weighted by the length of each linestring + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{MultiLineString, line_string, point}; + /// + /// let multi_line_string = MultiLineString::new(vec![ + /// // centroid: (2.5, 2.5) + /// line_string![(x: 1.0f32, y: 1.0), (x: 2.0, y: 2.0), (x: 4.0, y: 4.0)], + /// // centroid: (4.0, 4.0) + /// line_string![(x: 1.0, y: 1.0), (x: 3.0, y: 3.0), (x: 7.0, y: 7.0)], + /// ]); + /// + /// assert_eq!( + /// // ( 3.0 * (2.5, 2.5) + 6.0 * (4.0, 4.0) ) / 9.0 + /// Some(point!(x: 3.5, y: 3.5)), + /// multi_line_string.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let mut operation = CentroidOperation::new(); + operation.add_multi_line_string(self); + operation.centroid() + } +} + +impl CentroidTrait for P +where + P: PolygonTraitExt, + T: GeoFloat, +{ + type Output = Option>; + + /// The Centroid of a [`Polygon`] is the mean of its points + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{polygon, point}; + /// + /// let polygon = 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), + /// ]; + /// + /// assert_eq!( + /// Some(point!(x: 1.0, y: 0.5)), + /// polygon.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let mut operation = CentroidOperation::new(); + operation.add_polygon(self); + operation.centroid() + } +} + +impl CentroidTrait for MP +where + MP: MultiPolygonTraitExt, + T: GeoFloat, +{ + type Output = Option>; + + /// The Centroid of a [`MultiPolygon`] is the mean of the centroids of its polygons, weighted + /// by the area of the polygons + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{MultiPolygon, polygon, point}; + /// + /// let multi_polygon = MultiPolygon::new(vec![ + /// // centroid (1.0, 0.5) + /// 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), + /// ], + /// // centroid (-0.5, 0.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_eq!( + /// // ( 2.0 * (1.0, 0.5) + 6.0 * (-0.5, 0.0) ) / 8.0 + /// Some(point!(x: -0.125, y: 0.125)), + /// multi_polygon.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let mut operation = CentroidOperation::new(); + operation.add_multi_polygon(self); + operation.centroid() + } +} + +impl CentroidTrait for R +where + R: RectTraitExt, + T: GeoFloat, +{ + type Output = Point; + + /// The Centroid of a [`Rect`] is the mean of its [`Point`]s + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{Rect, point}; + /// + /// let rect = Rect::new( + /// point!(x: 0.0f32, y: 0.0), + /// point!(x: 1.0, y: 1.0), + /// ); + /// + /// assert_eq!( + /// point!(x: 0.5, y: 0.5), + /// rect.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + self.center().into() + } +} + +impl CentroidTrait for TT +where + T: GeoFloat, + TT: TriangleTraitExt, +{ + type Output = Point; + + /// The Centroid of a [`Triangle`] is the mean of its [`Point`]s + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{Triangle, coord, point}; + /// + /// let triangle = Triangle::new( + /// coord!(x: 0.0f32, y: -1.0), + /// coord!(x: 3.0, y: 0.0), + /// coord!(x: 0.0, y: 1.0), + /// ); + /// + /// assert_eq!( + /// point!(x: 1.0, y: 0.0), + /// triangle.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let mut operation = CentroidOperation::new(); + operation.add_triangle(self); + operation + .centroid() + .expect("triangle cannot have an empty centroid") + } +} + +impl CentroidTrait for P +where + T: GeoFloat, + P: PointTraitExt, +{ + type Output = Point; + + /// The Centroid of a [`Point`] is the point itself + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::point; + /// + /// let point = point!(x: 1.0f32, y: 2.0); + /// + /// assert_eq!( + /// point!(x: 1.0f32, y: 2.0), + /// point.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + self.geo_point() + .unwrap_or_else(|| Point::new(T::zero(), T::zero())) + } +} + +impl CentroidTrait for MP +where + T: GeoFloat, + MP: MultiPointTraitExt, +{ + type Output = Option>; + + /// The Centroid of a [`MultiPoint`] is the mean of all [`Point`]s + /// + /// # Example + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{MultiPoint, Point}; + /// + /// let empty: Vec = Vec::new(); + /// let empty_multi_points: MultiPoint<_> = empty.into(); + /// assert_eq!(empty_multi_points.centroid(), None); + /// + /// let points: MultiPoint<_> = vec![(5., 1.), (1., 3.), (3., 2.)].into(); + /// assert_eq!(points.centroid(), Some(Point::new(3., 2.))); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let mut operation = CentroidOperation::new(); + operation.add_multi_point(self); + operation.centroid() + } +} + +impl CentroidTrait for G +where + T: GeoFloat, + G: GeometryTraitExt, +{ + type Output = Option>; + + /// The Centroid of a [`Geometry`] is the centroid of its enum variant + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{Geometry, Rect, point}; + /// + /// let rect = Rect::new( + /// point!(x: 0.0f32, y: 0.0), + /// point!(x: 1.0, y: 1.0), + /// ); + /// let geometry = Geometry::from(rect.clone()); + /// + /// assert_eq!( + /// Some(rect.centroid()), + /// geometry.centroid(), + /// ); + /// + /// assert_eq!( + /// Some(point!(x: 0.5, y: 0.5)), + /// geometry.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + if self.is_collection() { + // Handle geometry collection by computing weighted centroid + let mut operation = CentroidOperation::new(); + for g_inner in self.geometries_ext() { + operation.add_geometry(g_inner.borrow()); + } + operation.centroid() + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => Some(g.centroid_trait()), + GeometryTypeExt::Line(g) => Some(g.centroid_trait()), + GeometryTypeExt::LineString(g) => g.centroid_trait(), + GeometryTypeExt::Polygon(g) => g.centroid_trait(), + GeometryTypeExt::MultiPoint(g) => g.centroid_trait(), + GeometryTypeExt::MultiLineString(g) => g.centroid_trait(), + GeometryTypeExt::MultiPolygon(g) => g.centroid_trait(), + GeometryTypeExt::Rect(g) => Some(g.centroid_trait()), + GeometryTypeExt::Triangle(g) => Some(g.centroid_trait()), + } + } + } +} + +impl CentroidTrait for GC +where + T: GeoFloat, + GC: GeometryCollectionTraitExt, +{ + type Output = Option>; + + /// The Centroid of a [`GeometryCollection`] is the mean of the centroids of elements, weighted + /// by the area of its elements. + /// + /// Note that this means, that elements which have no area are not considered when calculating + /// the centroid. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{Geometry, GeometryCollection, Rect, Triangle, point, coord}; + /// + /// let rect_geometry = Geometry::from(Rect::new( + /// point!(x: 0.0f32, y: 0.0), + /// point!(x: 1.0, y: 1.0), + /// )); + /// + /// let triangle_geometry = Geometry::from(Triangle::new( + /// coord!(x: 0.0f32, y: -1.0), + /// coord!(x: 3.0, y: 0.0), + /// coord!(x: 0.0, y: 1.0), + /// )); + /// + /// let point_geometry = Geometry::from( + /// point!(x: 12351.0, y: 129815.0) + /// ); + /// + /// let geometry_collection = GeometryCollection::new_from( + /// vec![ + /// rect_geometry, + /// triangle_geometry, + /// point_geometry + /// ] + /// ); + /// + /// assert_eq!( + /// Some(point!(x: 0.875, y: 0.125)), + /// geometry_collection.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let mut operation = CentroidOperation::new(); + operation.add_geometry_collection(self); + operation.centroid() + } +} + +struct CentroidOperation(Option>); +impl CentroidOperation { + fn new() -> Self { + CentroidOperation(None) + } + + fn centroid(&self) -> Option> { + self.0.as_ref().map(|weighted_centroid| { + Point::from(weighted_centroid.accumulated / weighted_centroid.weight) + }) + } + + fn centroid_dimensions(&self) -> Dimensions { + self.0 + .as_ref() + .map(|weighted_centroid| weighted_centroid.dimensions) + .unwrap_or(Empty) + } + + fn add_coord(&mut self, coord: Coord) { + self.add_centroid(ZeroDimensional, coord, T::one()); + } + + fn add_line(&mut self, line: &L) + where + L: LineTraitExt, + { + match line.dimensions() { + ZeroDimensional => self.add_coord(line.start_coord()), + OneDimensional => { + let weight = line.length_ext(&Euclidean); + self.add_centroid(OneDimensional, line.centroid().0, weight) + } + _ => unreachable!("Line must be zero or one dimensional"), + } + } + + fn add_line_string(&mut self, line_string: &LS) + where + LS: LineStringTraitExt, + { + if self.centroid_dimensions() > OneDimensional { + return; + } + + if line_string.num_coords() == 1 { + unsafe { self.add_coord(line_string.geo_coord_unchecked(0)) }; + return; + } + + for line in line_string.lines() { + self.add_line(&line); + } + } + + fn add_multi_line_string(&mut self, multi_line_string: &MLS) + where + MLS: MultiLineStringTraitExt, + { + if self.centroid_dimensions() > OneDimensional { + return; + } + + for element in multi_line_string.line_strings_ext() { + self.add_line_string(&element); + } + } + + fn add_polygon

(&mut self, polygon: &P) + where + P: PolygonTraitExt, + { + // Polygons which are completely covered by their interior rings have zero area, and + // represent a unique degeneracy into a line_string which cannot be handled by accumulating + // directly into `self`. Instead, we perform a sub-operation, inspect the result, and only + // then incorporate the result into `self. + + let mut exterior_operation = CentroidOperation::new(); + if let Some(exterior) = polygon.exterior_ext() { + exterior_operation.add_ring(&exterior); + } + + let mut interior_operation = CentroidOperation::new(); + for interior in polygon.interiors_ext() { + interior_operation.add_ring(&interior); + } + + if let Some(exterior_weighted_centroid) = exterior_operation.0 { + let mut poly_weighted_centroid = exterior_weighted_centroid; + if let Some(interior_weighted_centroid) = interior_operation.0 { + poly_weighted_centroid.sub_assign(interior_weighted_centroid); + if poly_weighted_centroid.weight.is_zero() { + // A polygon with no area `interiors` completely covers `exterior`, degenerating to a linestring + polygon.exterior_ext().iter().for_each(|exterior| { + self.add_line_string(exterior); + }); + return; + } + } + self.add_weighted_centroid(poly_weighted_centroid); + } + } + + fn add_multi_point(&mut self, multi_point: &MP) + where + MP: MultiPointTraitExt, + { + if self.centroid_dimensions() > ZeroDimensional { + return; + } + + for element in multi_point.coord_iter() { + self.add_coord(element); + } + } + + fn add_multi_polygon(&mut self, multi_polygon: &MP) + where + MP: MultiPolygonTraitExt, + { + for element in multi_polygon.polygons_ext() { + self.add_polygon(&element); + } + } + + fn add_geometry_collection(&mut self, geometry_collection: &GC) + where + GC: GeometryCollectionTraitExt, + { + for element in geometry_collection.geometries_ext() { + self.add_geometry(&element); + } + } + + fn add_rect(&mut self, rect: &R) + where + R: RectTraitExt, + { + match rect.dimensions() { + ZeroDimensional => self.add_coord(rect.min_coord()), + OneDimensional => { + // Degenerate rect is a line, treat it the same way we treat flat polygons + self.add_line(&Line::new(rect.min_coord(), rect.min_coord())); + self.add_line(&Line::new(rect.min_coord(), rect.max_coord())); + self.add_line(&Line::new(rect.max_coord(), rect.max_coord())); + self.add_line(&Line::new(rect.max_coord(), rect.min_coord())); + } + TwoDimensional => { + self.add_centroid(TwoDimensional, rect.centroid().0, rect.unsigned_area()) + } + Empty => unreachable!("Rect dimensions cannot be empty"), + } + } + + fn add_triangle(&mut self, triangle: &TT) + where + TT: TriangleTraitExt, + { + match triangle.dimensions() { + ZeroDimensional => self.add_coord(triangle.first_coord()), + OneDimensional => { + // Degenerate triangle is a line, treat it the same way we treat flat + // polygons + let l0_1 = Line::new(triangle.first_coord(), triangle.second_coord()); + let l1_2 = Line::new(triangle.second_coord(), triangle.third_coord()); + let l2_0 = Line::new(triangle.third_coord(), triangle.first_coord()); + self.add_line(&l0_1); + self.add_line(&l1_2); + self.add_line(&l2_0); + } + TwoDimensional => { + let centroid = + (triangle.first_coord() + triangle.second_coord() + triangle.third_coord()) + / T::from(3).unwrap(); + self.add_centroid(TwoDimensional, centroid, triangle.unsigned_area()); + } + Empty => unreachable!("Rect dimensions cannot be empty"), + } + } + + fn add_geometry(&mut self, geometry: &G) + where + G: GeometryTraitExt, + { + if geometry.is_collection() { + for g_inner in geometry.geometries_ext() { + self.add_geometry(g_inner.borrow()); + } + } else { + match geometry.as_type_ext() { + GeometryTypeExt::Point(g) => { + if let Some(coord) = g.geo_coord() { + self.add_coord(coord) + } + } + GeometryTypeExt::Line(g) => self.add_line(g), + GeometryTypeExt::LineString(g) => self.add_line_string(g), + GeometryTypeExt::Polygon(g) => self.add_polygon(g), + GeometryTypeExt::MultiPoint(g) => self.add_multi_point(g), + GeometryTypeExt::MultiLineString(g) => self.add_multi_line_string(g), + GeometryTypeExt::MultiPolygon(g) => self.add_multi_polygon(g), + GeometryTypeExt::Rect(g) => self.add_rect(g), + GeometryTypeExt::Triangle(g) => self.add_triangle(g), + } + } + } + + fn add_ring(&mut self, ring: &LS) + where + LS: LineStringTraitExt, + { + debug_assert!(ring.is_closed()); + + let area = get_linestring_area(ring); + if area == T::zero() { + match ring.dimensions() { + // empty ring doesn't contribute to centroid + Empty => {} + // degenerate ring is a point + ZeroDimensional => unsafe { self.add_coord(ring.geo_coord_unchecked(0)) }, + // zero-area ring is a line string + _ => self.add_line_string(ring), + } + return; + } + + // Since area is non-zero, we know the ring has at least one point + let shift = unsafe { ring.geo_coord_unchecked(0) }; + + let accumulated_coord = ring.lines().fold(Coord::zero(), |accum, line| { + use crate::MapCoords; + let line = line.map_coords(|c| c - shift); + let tmp = line.determinant(); + accum + (line.end + line.start) * tmp + }); + let six = T::from(6).unwrap(); + let centroid = accumulated_coord / (six * area) + shift; + let weight = area.abs(); + self.add_centroid(TwoDimensional, centroid, weight); + } + + fn add_centroid(&mut self, dimensions: Dimensions, centroid: Coord, weight: T) { + let weighted_centroid = WeightedCentroid { + dimensions, + weight, + accumulated: centroid * weight, + }; + self.add_weighted_centroid(weighted_centroid); + } + + fn add_weighted_centroid(&mut self, other: WeightedCentroid) { + match self.0.as_mut() { + Some(centroid) => centroid.add_assign(other), + None => self.0 = Some(other), + } + } +} + +// Aggregated state for accumulating the centroid of a geometry or collection of geometries. +struct WeightedCentroid { + weight: T, + accumulated: Coord, + /// Collections of Geometries can have different dimensionality. Centroids must be considered + /// separately by dimensionality. + /// + /// e.g. If I have several Points, adding a new `Point` will affect their centroid. + /// + /// However, because a Point is zero dimensional, it is infinitely small when compared to + /// any 2-D Polygon. Thus a Point will not affect the centroid of any GeometryCollection + /// containing a 2-D Polygon. + /// + /// So, when accumulating a centroid, we must track the dimensionality of the centroid + dimensions: Dimensions, +} + +impl WeightedCentroid { + fn add_assign(&mut self, b: WeightedCentroid) { + match self.dimensions.cmp(&b.dimensions) { + Ordering::Less => *self = b, + Ordering::Greater => {} + Ordering::Equal => { + self.accumulated = self.accumulated + b.accumulated; + self.weight = self.weight + b.weight; + } + } + } + + fn sub_assign(&mut self, b: WeightedCentroid) { + match self.dimensions.cmp(&b.dimensions) { + Ordering::Less => *self = b, + Ordering::Greater => {} + Ordering::Equal => { + self.accumulated = self.accumulated - b.accumulated; + self.weight = self.weight - b.weight; + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{coord, line_string, point, polygon, wkt}; + + /// small helper to create a coordinate + fn c(x: T, y: T) -> Coord { + coord! { x: x, y: y } + } + + /// small helper to create a point + fn p(x: T, y: T) -> Point { + point! { x: x, y: y } + } + + // Tests: Centroid of LineString + #[test] + fn empty_linestring_test() { + let linestring: LineString = line_string![]; + let centroid = linestring.centroid(); + assert!(centroid.is_none()); + } + #[test] + fn linestring_one_point_test() { + let coord = coord! { + x: 40.02f64, + y: 116.34, + }; + let linestring = line_string![coord]; + let centroid = linestring.centroid(); + assert_eq!(centroid, Some(Point::from(coord))); + } + #[test] + fn linestring_test() { + let linestring = line_string![ + (x: 1., y: 1.), + (x: 7., y: 1.), + (x: 8., y: 1.), + (x: 9., y: 1.), + (x: 10., y: 1.), + (x: 11., y: 1.) + ]; + assert_eq!(linestring.centroid(), Some(point!(x: 6., y: 1. ))); + } + #[test] + fn linestring_with_repeated_point_test() { + let l1 = LineString::from(vec![p(1., 1.), p(1., 1.), p(1., 1.)]); + assert_eq!(l1.centroid(), Some(p(1., 1.))); + + let l2 = LineString::from(vec![p(2., 2.), p(2., 2.), p(2., 2.)]); + let mls = MultiLineString::new(vec![l1, l2]); + assert_eq!(mls.centroid(), Some(p(1.5, 1.5))); + } + // Tests: Centroid of MultiLineString + #[test] + fn empty_multilinestring_test() { + let mls: MultiLineString = MultiLineString::new(vec![]); + let centroid = mls.centroid(); + assert!(centroid.is_none()); + } + #[test] + fn multilinestring_with_empty_line_test() { + let mls: MultiLineString = MultiLineString::new(vec![line_string![]]); + let centroid = mls.centroid(); + assert!(centroid.is_none()); + } + #[test] + fn multilinestring_length_0_test() { + let coord = coord! { + x: 40.02f64, + y: 116.34, + }; + let mls: MultiLineString = MultiLineString::new(vec![ + line_string![coord], + line_string![coord], + line_string![coord], + ]); + assert_relative_eq!(mls.centroid().unwrap(), Point::from(coord)); + } + #[test] + fn multilinestring_one_line_test() { + let linestring = line_string![ + (x: 1., y: 1.), + (x: 7., y: 1.), + (x: 8., y: 1.), + (x: 9., y: 1.), + (x: 10., y: 1.), + (x: 11., y: 1.) + ]; + let mls: MultiLineString = MultiLineString::new(vec![linestring]); + assert_relative_eq!(mls.centroid().unwrap(), point! { x: 6., y: 1. }); + } + #[test] + fn multilinestring_test() { + 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] + ); + } + // Tests: Centroid of Polygon + #[test] + fn empty_polygon_test() { + let poly: Polygon = polygon![]; + assert!(poly.centroid().is_none()); + } + #[test] + fn polygon_one_point_test() { + let p = point![ x: 2., y: 1. ]; + let poly = polygon![p.0]; + assert_relative_eq!(poly.centroid().unwrap(), p); + } + + #[test] + fn centroid_polygon_numerical_stability() { + let polygon = { + use std::f64::consts::PI; + const NUM_VERTICES: usize = 10; + const ANGLE_INC: f64 = 2. * PI / NUM_VERTICES as f64; + + Polygon::new( + (0..NUM_VERTICES) + .map(|i| { + let angle = i as f64 * ANGLE_INC; + coord! { + x: angle.cos(), + y: angle.sin(), + } + }) + .collect::>() + .into(), + vec![], + ) + }; + + let centroid = polygon.centroid().unwrap(); + + let shift = coord! { x: 1.5e8, y: 1.5e8 }; + + use crate::map_coords::MapCoords; + let polygon = polygon.map_coords(|c| c + shift); + + let new_centroid = polygon.centroid().unwrap().map_coords(|c| c - shift); + debug!("centroid {:?}", centroid.0); + debug!("new_centroid {:?}", new_centroid.0); + assert_relative_eq!(centroid.0.x, new_centroid.0.x, max_relative = 0.0001); + assert_relative_eq!(centroid.0.y, new_centroid.0.y, max_relative = 0.0001); + } + + #[test] + fn polygon_test() { + let poly = polygon![ + (x: 0., y: 0.), + (x: 2., y: 0.), + (x: 2., y: 2.), + (x: 0., y: 2.), + (x: 0., y: 0.) + ]; + assert_relative_eq!(poly.centroid().unwrap(), point![x:1., y:1.]); + } + #[test] + fn polygon_hole_test() { + // hexagon + 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 = 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 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 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 = wkt! { POLYGON((1. 1.,1. 1.,1. 1.)) }; + assert_eq!(p1.centroid(), Some(p(1., 1.))); + + 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] + fn multi_poly_with_one_ring_and_one_real_poly() { + // if the multipolygon is composed of a 'normal' polygon (with an area not null) + // and a ring (a polygon with a null area) + // the centroid of the multipolygon is the centroid of the 'normal' polygon + let normal = Polygon::new( + LineString::from(vec![p(1., 1.), p(1., 3.), p(3., 1.), p(1., 1.)]), + vec![], + ); + let flat = Polygon::new( + LineString::from(vec![p(2., 2.), p(6., 2.), p(2., 2.)]), + vec![], + ); + let multipoly = MultiPolygon::new(vec![normal.clone(), flat]); + assert_eq!(multipoly.centroid(), normal.centroid()); + } + #[test] + fn polygon_flat_interior_test() { + let poly = Polygon::new( + LineString::from(vec![p(0., 0.), p(0., 1.), p(1., 1.), p(1., 0.), p(0., 0.)]), + vec![LineString::from(vec![p(0., 0.), p(0., 1.), p(0., 0.)])], + ); + assert_eq!(poly.centroid(), Some(p(0.5, 0.5))); + } + #[test] + fn empty_interior_polygon_test() { + let poly = Polygon::new( + LineString::from(vec![p(0., 0.), p(0., 1.), p(1., 1.), p(1., 0.), p(0., 0.)]), + vec![LineString::new(vec![])], + ); + assert_eq!(poly.centroid(), Some(p(0.5, 0.5))); + } + #[test] + fn polygon_ring_test() { + let square = LineString::from(vec![p(0., 0.), p(0., 1.), p(1., 1.), p(1., 0.), p(0., 0.)]); + let poly = Polygon::new(square.clone(), vec![square]); + assert_eq!(poly.centroid(), Some(p(0.5, 0.5))); + } + #[test] + fn polygon_cell_test() { + // test the centroid of polygon with a null area + // this one a polygon with 2 interior polygon that makes a partition of the exterior + let square = LineString::from(vec![p(0., 0.), p(0., 2.), p(2., 2.), p(2., 0.), p(0., 0.)]); + let bottom = LineString::from(vec![p(0., 0.), p(2., 0.), p(2., 1.), p(0., 1.), p(0., 0.)]); + let top = LineString::from(vec![p(0., 1.), p(2., 1.), p(2., 2.), p(0., 2.), p(0., 1.)]); + let poly = Polygon::new(square, vec![top, bottom]); + assert_eq!(poly.centroid(), Some(p(1., 1.))); + } + // Tests: Centroid of MultiPolygon + #[test] + fn empty_multipolygon_polygon_test() { + assert!(MultiPolygon::::new(Vec::new()).centroid().is_none()); + } + + #[test] + fn multipolygon_one_polygon_test() { + let linestring = + LineString::from(vec![p(0., 0.), p(2., 0.), p(2., 2.), p(0., 2.), p(0., 0.)]); + let poly = Polygon::new(linestring, Vec::new()); + assert_eq!(MultiPolygon::new(vec![poly]).centroid(), Some(p(1., 1.))); + } + #[test] + fn multipolygon_two_polygons_test() { + let linestring = + LineString::from(vec![p(2., 1.), p(5., 1.), p(5., 3.), p(2., 3.), p(2., 1.)]); + let poly1 = Polygon::new(linestring, Vec::new()); + let linestring = + LineString::from(vec![p(7., 1.), p(8., 1.), p(8., 2.), p(7., 2.), p(7., 1.)]); + let poly2 = Polygon::new(linestring, Vec::new()); + let centroid = MultiPolygon::new(vec![poly1, poly2]).centroid().unwrap(); + assert_relative_eq!( + centroid, + point![x: 4.071428571428571, y: 1.9285714285714286] + ); + } + #[test] + fn multipolygon_two_polygons_of_opposite_clockwise_test() { + let linestring = LineString::from(vec![(0., 0.), (2., 0.), (2., 2.), (0., 2.), (0., 0.)]); + let poly1 = Polygon::new(linestring, Vec::new()); + let linestring = LineString::from(vec![(0., 0.), (-2., 0.), (-2., 2.), (0., 2.), (0., 0.)]); + let poly2 = Polygon::new(linestring, Vec::new()); + assert_relative_eq!( + MultiPolygon::new(vec![poly1, poly2]).centroid().unwrap(), + point![x: 0., y: 1.] + ); + } + #[test] + fn bounding_rect_test() { + let bounding_rect = Rect::new(coord! { x: 0., y: 50. }, coord! { x: 4., y: 100. }); + let point = point![x: 2., y: 75.]; + assert_eq!(point, bounding_rect.centroid()); + } + #[test] + fn line_test() { + let line1 = Line::new(c(0., 1.), c(1., 3.)); + assert_eq!(line1.centroid(), point![x: 0.5, y: 2.]); + } + #[test] + fn collection_weighting() { + let p0 = point!(x: 0.0, y: 0.0); + let p1 = point!(x: 2.0, y: 0.0); + let p2 = point!(x: 2.0, y: 2.0); + let p3 = point!(x: 0.0, y: 2.0); + + let multi_point = MultiPoint::new(vec![p0, p1, p2, p3]); + assert_eq!(multi_point.centroid().unwrap(), point!(x: 1.0, y: 1.0)); + + let collection = + GeometryCollection::new_from(vec![MultiPoint::new(vec![p1, p2, p3]).into(), p0.into()]); + + assert_eq!(collection.centroid().unwrap(), point!(x: 1.0, y: 1.0)); + } + #[test] + fn triangles() { + // boring triangle + assert_eq!( + Triangle::new(c(0., 0.), c(3., 0.), c(1.5, 3.)).centroid(), + point!(x: 1.5, y: 1.0) + ); + + // flat triangle + assert_eq!( + Triangle::new(c(0., 0.), c(3., 0.), c(1., 0.)).centroid(), + point!(x: 1.5, y: 0.0) + ); + + // flat triangle that's not axis-aligned + assert_eq!( + Triangle::new(c(0., 0.), c(3., 3.), c(1., 1.)).centroid(), + point!(x: 1.5, y: 1.5) + ); + + // triangle with some repeated points + assert_eq!( + Triangle::new(c(0., 0.), c(0., 0.), c(1., 0.)).centroid(), + point!(x: 0.5, y: 0.0) + ); + + // triangle with all repeated points + assert_eq!( + Triangle::new(c(0., 0.5), c(0., 0.5), c(0., 0.5)).centroid(), + point!(x: 0., y: 0.5) + ) + } + + #[test] + fn degenerate_triangle_like_ring() { + let triangle = Triangle::new(c(0., 0.), c(1., 1.), c(2., 2.)); + let poly: Polygon<_> = triangle.into(); + + let line = Line::new(c(0., 1.), c(1., 3.)); + + let g1 = GeometryCollection::new_from(vec![triangle.into(), line.into()]); + let g2 = GeometryCollection::new_from(vec![poly.into(), line.into()]); + assert_eq!(g1.centroid(), g2.centroid()); + } + + #[test] + fn degenerate_rect_like_ring() { + let rect = Rect::new(c(0., 0.), c(0., 4.)); + let poly: Polygon<_> = rect.into(); + + let line = Line::new(c(0., 1.), c(1., 3.)); + + let g1 = GeometryCollection::new_from(vec![rect.into(), line.into()]); + let g2 = GeometryCollection::new_from(vec![poly.into(), line.into()]); + assert_eq!(g1.centroid(), g2.centroid()); + } + + #[test] + fn rectangles() { + // boring rect + assert_eq!( + Rect::new(c(0., 0.), c(4., 4.)).centroid(), + point!(x: 2.0, y: 2.0) + ); + + // flat rect + assert_eq!( + Rect::new(c(0., 0.), c(4., 0.)).centroid(), + point!(x: 2.0, y: 0.0) + ); + + // rect with all repeated points + assert_eq!( + Rect::new(c(4., 4.), c(4., 4.)).centroid(), + point!(x: 4., y: 4.) + ); + + // collection with rect + let mut collection = GeometryCollection::new_from(vec![ + p(0., 0.).into(), + p(6., 0.).into(), + p(6., 6.).into(), + ]); + // sanity check + assert_eq!(collection.centroid().unwrap(), point!(x: 4., y: 2.)); + + // 0-d rect treated like point + collection.0.push(Rect::new(c(0., 6.), c(0., 6.)).into()); + assert_eq!(collection.centroid().unwrap(), point!(x: 3., y: 3.)); + + // 1-d rect treated like line. Since a line has higher dimensions than the rest of the + // collection, its centroid clobbers everything else in the collection. + collection.0.push(Rect::new(c(0., 0.), c(0., 2.)).into()); + assert_eq!(collection.centroid().unwrap(), point!(x: 0., y: 1.)); + + // 2-d has higher dimensions than the rest of the collection, so its centroid clobbers + // everything else in the collection. + collection + .0 + .push(Rect::new(c(10., 10.), c(11., 11.)).into()); + assert_eq!(collection.centroid().unwrap(), point!(x: 10.5, y: 10.5)); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/convert.rs b/rust/sedona-geo-generic-alg/src/algorithm/convert.rs new file mode 100644 index 00000000..d6385f6c --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/convert.rs @@ -0,0 +1,91 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use crate::{Coord, CoordNum, MapCoords}; + +/// Convert (infalliby) the type of a geometry’s coordinate value. +/// +/// # Examples +/// +/// ```rust +/// use sedona_geo_generic_alg::{Convert, LineString, line_string}; +/// +/// let line_string_32: LineString = line_string![ +/// (x: 5., y: 10.), +/// (x: 3., y: 1.), +/// (x: 8., y: 9.), +/// ]; +/// +/// let line_string_64: LineString = line_string_32.convert(); +/// ``` +/// +pub trait Convert { + type Output; + + fn convert(&self) -> Self::Output; +} +impl Convert for G +where + G: MapCoords, + U: From, +{ + type Output = >::Output; + + fn convert(&self) -> Self::Output { + self.map_coords(|Coord { x, y }| Coord { + x: x.into(), + y: y.into(), + }) + } +} + +/// Convert (fallibly) the type of a geometry’s coordinate value. +/// +/// # Examples +/// +/// ```rust +/// use sedona_geo_generic_alg::{TryConvert, LineString, line_string}; +/// +/// let line_string_64: LineString = line_string![ +/// (x: 5, y: 10), +/// (x: 3, y: 1), +/// (x: 8, y: 9), +/// ]; +/// +/// let line_string_32: Result, _> = line_string_64.try_convert(); +/// ``` +/// +pub trait TryConvert { + type Output; + + fn try_convert(&self) -> Self::Output; +} +impl TryConvert for G +where + G: MapCoords, + U: TryFrom, +{ + type Output = Result<>::Output, >::Error>; + + fn try_convert(&self) -> Self::Output { + self.try_map_coords(|Coord { x, y }| { + Ok(Coord { + x: x.try_into()?, + y: y.try_into()?, + }) + }) + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/convert_angle_unit.rs b/rust/sedona-geo-generic-alg/src/algorithm/convert_angle_unit.rs new file mode 100644 index 00000000..9f871fec --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/convert_angle_unit.rs @@ -0,0 +1,100 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +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/rust/sedona-geo-generic-alg/src/algorithm/coordinate_position.rs b/rust/sedona-geo-generic-alg/src/algorithm/coordinate_position.rs new file mode 100644 index 00000000..15211ccb --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/coordinate_position.rs @@ -0,0 +1,896 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use core::borrow::Borrow; +use std::cmp::Ordering; + +use crate::geometry::*; +use crate::intersects::{point_in_rect, value_in_between}; +use crate::kernels::*; +use crate::GeoNum; +use crate::{BoundingRect, HasDimensions, Intersects}; +use sedona_geo_traits_ext::*; + +/// The position of a `Coord` relative to a `Geometry` +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub enum CoordPos { + OnBoundary, + Inside, + Outside, +} + +/// Determine whether a `Coord` lies inside, outside, or on the boundary of a geometry. +/// +/// # Examples +/// +/// ```rust +/// use sedona_geo_generic_alg::{polygon, coord}; +/// use sedona_geo_generic_alg::coordinate_position::{CoordinatePosition, CoordPos}; +/// +/// let square_poly = polygon![(x: 0.0, y: 0.0), (x: 2.0, y: 0.0), (x: 2.0, y: 2.0), (x: 0.0, y: 2.0), (x: 0.0, y: 0.0)]; +/// +/// let inside_coord = coord! { x: 1.0, y: 1.0 }; +/// assert_eq!(square_poly.coordinate_position(&inside_coord), CoordPos::Inside); +/// +/// let boundary_coord = coord! { x: 0.0, y: 1.0 }; +/// assert_eq!(square_poly.coordinate_position(&boundary_coord), CoordPos::OnBoundary); +/// +/// let outside_coord = coord! { x: 5.0, y: 5.0 }; +/// assert_eq!(square_poly.coordinate_position(&outside_coord), CoordPos::Outside); +/// ``` +pub trait CoordinatePosition { + type Scalar: GeoNum; + fn coordinate_position(&self, coord: &Coord) -> CoordPos { + let mut is_inside = false; + let mut boundary_count = 0; + + self.calculate_coordinate_position(coord, &mut is_inside, &mut boundary_count); + + // “The boundary of an arbitrary collection of geometries whose interiors are disjoint + // consists of geometries drawn from the boundaries of the element geometries by + // application of the ‘mod 2’ union rule” + // + // ― OpenGIS Simple Feature Access § 6.1.15.1 + if boundary_count % 2 == 1 { + CoordPos::OnBoundary + } else if is_inside { + CoordPos::Inside + } else { + CoordPos::Outside + } + } + + // impls of this trait must: + // 1. set `is_inside = true` if `coord` is contained within the Interior of any component. + // 2. increment `boundary_count` for each component whose Boundary contains `coord`. + fn calculate_coordinate_position( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ); +} + +impl CoordinatePosition for G +where + G: GeoTraitExtWithTypeTag, + G: CoordinatePositionTrait, +{ + type Scalar = G::T; + + fn calculate_coordinate_position( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + self.calculate_coordinate_position_trait(coord, is_inside, boundary_count); + } +} + +pub trait CoordinatePositionTrait { + type T: GeoNum; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ); +} + +impl CoordinatePositionTrait for C +where + T: GeoNum, + C: CoordTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + _boundary_count: &mut usize, + ) { + if &self.geo_coord() == coord { + *is_inside = true; + } + } +} + +impl CoordinatePositionTrait for P +where + T: GeoNum, + P: PointTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + _boundary_count: &mut usize, + ) { + if let Some(point_coord) = self.geo_coord() { + if &point_coord == coord { + *is_inside = true; + } + } + } +} + +impl CoordinatePositionTrait for L +where + T: GeoNum, + L: LineTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + let start = self.start_coord(); + let end = self.end_coord(); + + // degenerate line is a point + if start == end { + self.start_ext() + .calculate_coordinate_position(coord, is_inside, boundary_count); + return; + } + + if coord == &start || coord == &end { + *boundary_count += 1; + } else if self.intersects(coord) { + *is_inside = true; + } + } +} + +impl CoordinatePositionTrait for LS +where + T: GeoNum, + LS: LineStringTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + let num_coords = self.num_coords(); + if num_coords < 2 { + debug_assert!(false, "invalid line string with less than 2 coords"); + return; + } + + if num_coords == 2 { + // line string with two coords is just a line + unsafe { + let start = self.geo_coord_unchecked(0); + let end = self.geo_coord_unchecked(1); + Line::new(start, end).calculate_coordinate_position( + coord, + is_inside, + boundary_count, + ); + } + return; + } + + // optimization: return early if there's no chance of an intersection + // since bounding rect is not empty, we can safely `unwrap`. + if !self.bounding_rect().unwrap().intersects(coord) { + return; + } + + // A closed linestring has no boundary, per SFS + if !self.is_closed() { + // since we have at least two coords, first and last will exist + unsafe { + let first = self.geo_coord_unchecked(0); + let last = self.geo_coord_unchecked(num_coords - 1); + if coord == &first || coord == &last { + *boundary_count += 1; + return; + } + } + } + + if self.intersects(coord) { + // We've already checked for "Boundary" condition, so if there's an intersection at + // this point, coord must be on the interior + *is_inside = true + } + } +} + +impl CoordinatePositionTrait for TT +where + T: GeoNum, + TT: TriangleTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + *is_inside = self + .to_lines() + .map(|l| { + let orientation = T::Ker::orient2d(l.start, l.end, *coord); + if orientation == Orientation::Collinear + && point_in_rect(*coord, l.start, l.end) + && coord.x != l.end.x + { + *boundary_count += 1; + } + orientation + }) + .windows(2) + .all(|win| win[0] == win[1] && win[0] != Orientation::Collinear); + } +} + +impl CoordinatePositionTrait for R +where + T: GeoNum, + R: RectTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + let mut boundary = false; + let min = self.min_coord(); + + match coord.x.partial_cmp(&min.x).unwrap() { + Ordering::Less => return, + Ordering::Equal => boundary = true, + Ordering::Greater => {} + } + match coord.y.partial_cmp(&min.y).unwrap() { + Ordering::Less => return, + Ordering::Equal => boundary = true, + Ordering::Greater => {} + } + + let max = self.max_coord(); + + match max.x.partial_cmp(&coord.x).unwrap() { + Ordering::Less => return, + Ordering::Equal => boundary = true, + Ordering::Greater => {} + } + match max.y.partial_cmp(&coord.y).unwrap() { + Ordering::Less => return, + Ordering::Equal => boundary = true, + Ordering::Greater => {} + } + + if boundary { + *boundary_count += 1; + } else { + *is_inside = true; + } + } +} + +impl CoordinatePositionTrait for MP +where + T: GeoNum, + MP: MultiPointTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + _boundary_count: &mut usize, + ) { + if self + .points_ext() + .any(|p| p.geo_coord().is_some_and(|c| &c == coord)) + { + *is_inside = true; + } + } +} + +impl CoordinatePositionTrait for P +where + T: GeoNum, + P: PolygonTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + let Some(exterior) = self.exterior_ext() else { + return; + }; + + if self.is_empty() { + return; + } + + match coord_pos_relative_to_ring(*coord, &exterior) { + CoordPos::Outside => {} + CoordPos::OnBoundary => { + *boundary_count += 1; + } + CoordPos::Inside => { + for hole in self.interiors_ext() { + match coord_pos_relative_to_ring(*coord, &hole) { + CoordPos::Outside => {} + CoordPos::OnBoundary => { + *boundary_count += 1; + return; + } + CoordPos::Inside => { + return; + } + } + } + // the coord is *outside* the interior holes, so it's *inside* the polygon + *is_inside = true; + } + } + } +} + +impl CoordinatePositionTrait for MLS +where + T: GeoNum, + MLS: MultiLineStringTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + for line_string in self.line_strings_ext() { + line_string.calculate_coordinate_position_trait(coord, is_inside, boundary_count); + } + } +} + +impl CoordinatePositionTrait for MP +where + T: GeoNum, + MP: MultiPolygonTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + for polygon in self.polygons_ext() { + polygon.calculate_coordinate_position_trait(coord, is_inside, boundary_count); + } + } +} + +impl CoordinatePositionTrait for GC +where + T: GeoNum, + GC: GeometryCollectionTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + for geometry in self.geometries_ext() { + geometry.calculate_coordinate_position_trait(coord, is_inside, boundary_count); + } + } +} + +fn geometry_calculate_coordinate_position( + g: &G, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, +) where + T: GeoNum, + G: GeometryTraitExt, +{ + if g.is_collection() { + for g_inner in g.geometries_ext() { + geometry_calculate_coordinate_position( + g_inner.borrow(), + coord, + is_inside, + boundary_count, + ); + } + } else { + match g.as_type_ext() { + GeometryTypeExt::Point(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::Line(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::LineString(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::Polygon(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::MultiPoint(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::MultiLineString(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::MultiPolygon(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::Rect(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::Triangle(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + } + } +} + +impl CoordinatePositionTrait for G +where + T: GeoNum, + G: GeometryTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + geometry_calculate_coordinate_position(self, coord, is_inside, boundary_count); + } +} + +/// Calculate the position of a `Coord` relative to a +/// closed `LineString`. +pub fn coord_pos_relative_to_ring(coord: Coord, linestring: &LS) -> CoordPos +where + T: GeoNum, + LS: LineStringTraitExt, +{ + debug_assert!(linestring.is_closed()); + + // LineString without points + if linestring.num_coords() == 0 { + return CoordPos::Outside; + } + if linestring.num_coords() == 1 { + // If LineString has one point, it will not generate + // any lines. So, we handle this edge case separately. + return if coord == unsafe { linestring.geo_coord_unchecked(0) } { + CoordPos::OnBoundary + } else { + CoordPos::Outside + }; + } + + // Use winding number algorithm with on boundary short-cicuit + // See: https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm + let mut winding_number = 0; + for line in linestring.lines() { + // Edge Crossing Rules: + // 1. an upward edge includes its starting endpoint, and excludes its final endpoint; + // 2. a downward edge excludes its starting endpoint, and includes its final endpoint; + // 3. horizontal edges are excluded + // 4. the edge-ray intersection point must be strictly right of the coord. + if line.start.y <= coord.y { + if line.end.y >= coord.y { + let o = T::Ker::orient2d(line.start, line.end, coord); + if o == Orientation::CounterClockwise && line.end.y != coord.y { + winding_number += 1 + } else if o == Orientation::Collinear + && value_in_between(coord.x, line.start.x, line.end.x) + { + return CoordPos::OnBoundary; + } + }; + } else if line.end.y <= coord.y { + let o = T::Ker::orient2d(line.start, line.end, coord); + if o == Orientation::Clockwise { + winding_number -= 1 + } else if o == Orientation::Collinear + && value_in_between(coord.x, line.start.x, line.end.x) + { + return CoordPos::OnBoundary; + } + } + } + if winding_number == 0 { + CoordPos::Outside + } else { + CoordPos::Inside + } +} + +#[cfg(test)] +mod test { + use geo_types::coord; + + use super::*; + use crate::{line_string, point, polygon}; + + #[test] + fn test_empty_poly() { + let square_poly: Polygon = Polygon::new(LineString::new(vec![]), vec![]); + assert_eq!( + square_poly.coordinate_position(&Coord::zero()), + CoordPos::Outside + ); + } + + #[test] + fn test_simple_poly() { + let square_poly = polygon![(x: 0.0, y: 0.0), (x: 2.0, y: 0.0), (x: 2.0, y: 2.0), (x: 0.0, y: 2.0), (x: 0.0, y: 0.0)]; + + let inside_coord = coord! { x: 1.0, y: 1.0 }; + assert_eq!( + square_poly.coordinate_position(&inside_coord), + CoordPos::Inside + ); + + let vertex_coord = coord! { x: 0.0, y: 0.0 }; + assert_eq!( + square_poly.coordinate_position(&vertex_coord), + CoordPos::OnBoundary + ); + + let boundary_coord = coord! { x: 0.0, y: 1.0 }; + assert_eq!( + square_poly.coordinate_position(&boundary_coord), + CoordPos::OnBoundary + ); + + let outside_coord = coord! { x: 5.0, y: 5.0 }; + assert_eq!( + square_poly.coordinate_position(&outside_coord), + CoordPos::Outside + ); + } + + #[test] + fn test_poly_interior() { + let poly = polygon![ + exterior: [ + (x: 11., y: 11.), + (x: 20., y: 11.), + (x: 20., y: 20.), + (x: 11., y: 20.), + (x: 11., y: 11.), + ], + interiors: [ + [ + (x: 13., y: 13.), + (x: 13., y: 17.), + (x: 17., y: 17.), + (x: 17., y: 13.), + (x: 13., y: 13.), + ] + ], + ]; + + let inside_hole = coord! { x: 14.0, y: 14.0 }; + assert_eq!(poly.coordinate_position(&inside_hole), CoordPos::Outside); + + let outside_poly = coord! { x: 30.0, y: 30.0 }; + assert_eq!(poly.coordinate_position(&outside_poly), CoordPos::Outside); + + let on_outside_border = coord! { x: 20.0, y: 15.0 }; + assert_eq!( + poly.coordinate_position(&on_outside_border), + CoordPos::OnBoundary + ); + + let on_inside_border = coord! { x: 13.0, y: 15.0 }; + assert_eq!( + poly.coordinate_position(&on_inside_border), + CoordPos::OnBoundary + ); + + let inside_coord = coord! { x: 12.0, y: 12.0 }; + assert_eq!(poly.coordinate_position(&inside_coord), CoordPos::Inside); + } + + #[test] + fn test_simple_line() { + use crate::point; + let line = Line::new(point![x: 0.0, y: 0.0], point![x: 10.0, y: 10.0]); + + let start = coord! { x: 0.0, y: 0.0 }; + assert_eq!(line.coordinate_position(&start), CoordPos::OnBoundary); + + let end = coord! { x: 10.0, y: 10.0 }; + assert_eq!(line.coordinate_position(&end), CoordPos::OnBoundary); + + let interior = coord! { x: 5.0, y: 5.0 }; + assert_eq!(line.coordinate_position(&interior), CoordPos::Inside); + + let outside = coord! { x: 6.0, y: 5.0 }; + assert_eq!(line.coordinate_position(&outside), CoordPos::Outside); + } + + #[test] + fn test_degenerate_line() { + let line = Line::new(point![x: 0.0, y: 0.0], point![x: 0.0, y: 0.0]); + + let start = coord! { x: 0.0, y: 0.0 }; + assert_eq!(line.coordinate_position(&start), CoordPos::Inside); + + let outside = coord! { x: 10.0, y: 10.0 }; + assert_eq!(line.coordinate_position(&outside), CoordPos::Outside); + } + + #[test] + fn test_point() { + let p1 = point![x: 2.0, y: 0.0]; + + let c1 = coord! { x: 2.0, y: 0.0 }; + let c2 = coord! { x: 3.0, y: 3.0 }; + + assert_eq!(p1.coordinate_position(&c1), CoordPos::Inside); + assert_eq!(p1.coordinate_position(&c2), CoordPos::Outside); + + assert_eq!(c1.coordinate_position(&c1), CoordPos::Inside); + assert_eq!(c1.coordinate_position(&c2), CoordPos::Outside); + } + + #[test] + fn test_simple_line_string() { + let line_string = + line_string![(x: 0.0, y: 0.0), (x: 1.0, y: 1.0), (x: 2.0, y: 0.0), (x: 3.0, y: 0.0)]; + + let start = Coord::zero(); + assert_eq!( + line_string.coordinate_position(&start), + CoordPos::OnBoundary + ); + + let midpoint = coord! { x: 0.5, y: 0.5 }; + assert_eq!(line_string.coordinate_position(&midpoint), CoordPos::Inside); + + let vertex = coord! { x: 2.0, y: 0.0 }; + assert_eq!(line_string.coordinate_position(&vertex), CoordPos::Inside); + + let end = coord! { x: 3.0, y: 0.0 }; + assert_eq!(line_string.coordinate_position(&end), CoordPos::OnBoundary); + + let outside = coord! { x: 3.0, y: 1.0 }; + assert_eq!(line_string.coordinate_position(&outside), CoordPos::Outside); + } + + #[test] + fn test_degenerate_line_strings() { + let line_string = line_string![(x: 0.0, y: 0.0), (x: 0.0, y: 0.0)]; + + let start = Coord::zero(); + assert_eq!(line_string.coordinate_position(&start), CoordPos::Inside); + + let line_string = line_string![(x: 0.0, y: 0.0), (x: 2.0, y: 0.0)]; + + let start = Coord::zero(); + assert_eq!( + line_string.coordinate_position(&start), + CoordPos::OnBoundary + ); + } + + #[test] + fn test_closed_line_string() { + let line_string = line_string![(x: 0.0, y: 0.0), (x: 1.0, y: 1.0), (x: 2.0, y: 0.0), (x: 3.0, y: 2.0), (x: 0.0, y: 2.0), (x: 0.0, y: 0.0)]; + + // sanity check + assert!(line_string.is_closed()); + + // closed line strings have no boundary + let start = Coord::zero(); + assert_eq!(line_string.coordinate_position(&start), CoordPos::Inside); + + let midpoint = coord! { x: 0.5, y: 0.5 }; + assert_eq!(line_string.coordinate_position(&midpoint), CoordPos::Inside); + + let outside = coord! { x: 3.0, y: 1.0 }; + assert_eq!(line_string.coordinate_position(&outside), CoordPos::Outside); + } + + #[test] + fn test_boundary_rule() { + let multi_line_string = MultiLineString::new(vec![ + // first two lines have same start point but different end point + line_string![(x: 0.0, y: 0.0), (x: 1.0, y: 1.0)], + line_string![(x: 0.0, y: 0.0), (x: -1.0, y: -1.0)], + // third line has its own start point, but it's end touches the middle of first line + line_string![(x: 0.0, y: 1.0), (x: 0.5, y: 0.5)], + // fourth and fifth have independent start points, but both end at the middle of the + // second line + line_string![(x: 0.0, y: -1.0), (x: -0.5, y: -0.5)], + line_string![(x: 0.0, y: -2.0), (x: -0.5, y: -0.5)], + ]); + + let outside_of_all = coord! { x: 123.0, y: 123.0 }; + assert_eq!( + multi_line_string.coordinate_position(&outside_of_all), + CoordPos::Outside + ); + + let end_of_one_line = coord! { x: -1.0, y: -1.0 }; + assert_eq!( + multi_line_string.coordinate_position(&end_of_one_line), + CoordPos::OnBoundary + ); + + // in boundary of first and second, so considered *not* in the boundary by mod 2 rule + let shared_start = Coord::zero(); + assert_eq!( + multi_line_string.coordinate_position(&shared_start), + CoordPos::Outside + ); + + // *in* the first line, on the boundary of the third line + let one_end_plus_midpoint = coord! { x: 0.5, y: 0.5 }; + assert_eq!( + multi_line_string.coordinate_position(&one_end_plus_midpoint), + CoordPos::OnBoundary + ); + + // *in* the first line, on the *boundary* of the fourth and fifth line + let two_ends_plus_midpoint = coord! { x: -0.5, y: -0.5 }; + assert_eq!( + multi_line_string.coordinate_position(&two_ends_plus_midpoint), + CoordPos::Inside + ); + } + + #[test] + fn test_rect() { + let rect = Rect::new((0.0, 0.0), (10.0, 10.0)); + assert_eq!( + rect.coordinate_position(&coord! { x: 5.0, y: 5.0 }), + CoordPos::Inside + ); + assert_eq!( + rect.coordinate_position(&coord! { x: 0.0, y: 5.0 }), + CoordPos::OnBoundary + ); + assert_eq!( + rect.coordinate_position(&coord! { x: 15.0, y: 15.0 }), + CoordPos::Outside + ); + } + + #[test] + fn test_triangle() { + let triangle = Triangle::new((0.0, 0.0).into(), (5.0, 10.0).into(), (10.0, 0.0).into()); + assert_eq!( + triangle.coordinate_position(&coord! { x: 5.0, y: 5.0 }), + CoordPos::Inside + ); + assert_eq!( + triangle.coordinate_position(&coord! { x: 2.5, y: 5.0 }), + CoordPos::OnBoundary + ); + assert_eq!( + triangle.coordinate_position(&coord! { x: 2.49, y: 5.0 }), + CoordPos::Outside + ); + } + + #[test] + fn test_collection() { + let triangle = Triangle::new((0.0, 0.0).into(), (5.0, 10.0).into(), (10.0, 0.0).into()); + let rect = Rect::new((0.0, 0.0), (10.0, 10.0)); + let collection = GeometryCollection::new_from(vec![triangle.into(), rect.into()]); + let geom = Geometry::GeometryCollection(collection.clone()); + + // outside of both + assert_eq!( + collection.coordinate_position(&coord! { x: 15.0, y: 15.0 }), + CoordPos::Outside + ); + assert_eq!( + geom.coordinate_position(&coord! { x: 15.0, y: 15.0 }), + CoordPos::Outside + ); + + // inside both + assert_eq!( + collection.coordinate_position(&coord! { x: 5.0, y: 5.0 }), + CoordPos::Inside + ); + assert_eq!( + geom.coordinate_position(&coord! { x: 5.0, y: 5.0 }), + CoordPos::Inside + ); + + // inside one, boundary of other + assert_eq!( + collection.coordinate_position(&coord! { x: 2.5, y: 5.0 }), + CoordPos::OnBoundary + ); + assert_eq!( + geom.coordinate_position(&coord! { x: 2.5, y: 5.0 }), + CoordPos::OnBoundary + ); + + // boundary of both + assert_eq!( + collection.coordinate_position(&coord! { x: 5.0, y: 10.0 }), + CoordPos::Outside + ); + assert_eq!( + geom.coordinate_position(&coord! { x: 5.0, y: 10.0 }), + CoordPos::Outside + ); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/coords_iter.rs b/rust/sedona-geo-generic-alg/src/algorithm/coords_iter.rs new file mode 100644 index 00000000..2d4b14b9 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/coords_iter.rs @@ -0,0 +1,1547 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use std::borrow::Borrow; +use std::option; +use std::slice; + +use geo_traits::*; +use sedona_geo_traits_ext::*; + +use crate::geometry::*; +use crate::{coord, CoordNum}; + +use std::{iter, marker}; + +type CoordinateChainOnce = iter::Chain>, iter::Once>>; + +/// Iterate over geometry coordinates. +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. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::coords_iter::CoordsIter; + /// + /// let multi_point = geo::MultiPoint::new(vec![ + /// geo::point!(x: -10., y: 0.), + /// geo::point!(x: 20., y: 20.), + /// geo::point!(x: 30., y: 40.), + /// ]); + /// + /// let mut iter = multi_point.coords_iter(); + /// assert_eq!(Some(geo::coord! { x: -10., y: 0. }), iter.next()); + /// assert_eq!(Some(geo::coord! { x: 20., y: 20. }), iter.next()); + /// assert_eq!(Some(geo::coord! { x: 30., y: 40. }), iter.next()); + /// assert_eq!(None, iter.next()); + /// ``` + fn coords_iter(&self) -> Self::Iter<'_>; + + /// Return the number of coordinates in a geometry. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::coords_iter::CoordsIter; + /// use sedona_geo_generic_alg::line_string; + /// + /// let ls = line_string![ + /// (x: 1., y: 2.), + /// (x: 23., y: 82.), + /// (x: -1., y: 0.), + /// ]; + /// + /// assert_eq!(3, ls.coords_count()); + /// ``` + fn coords_count(&self) -> usize; + + /// Iterate over all exterior coordinates of a geometry. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::coords_iter::CoordsIter; + /// use sedona_geo_generic_alg::polygon; + /// + /// // a diamond shape + /// let polygon = polygon![ + /// exterior: [ + /// (x: 1.0, y: 0.0), + /// (x: 2.0, y: 1.0), + /// (x: 1.0, y: 2.0), + /// (x: 0.0, y: 1.0), + /// (x: 1.0, y: 0.0), + /// ], + /// interiors: [ + /// [ + /// (x: 1.0, y: 0.5), + /// (x: 0.5, y: 1.0), + /// (x: 1.0, y: 1.5), + /// (x: 1.5, y: 1.0), + /// (x: 1.0, y: 0.5), + /// ], + /// ], + /// ]; + /// + /// let mut iter = polygon.exterior_coords_iter(); + /// assert_eq!(Some(geo::coord! { x: 1., y: 0. }), iter.next()); + /// assert_eq!(Some(geo::coord! { x: 2., y: 1. }), iter.next()); + /// assert_eq!(Some(geo::coord! { x: 1., y: 2. }), iter.next()); + /// assert_eq!(Some(geo::coord! { x: 0., y: 1. }), iter.next()); + /// assert_eq!(Some(geo::coord! { x: 1., y: 0. }), iter.next()); + /// assert_eq!(None, iter.next()); + /// ``` + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_>; +} + +impl CoordsIter for G +where + G: GeoTraitExtWithTypeTag + CoordsIterTrait, +{ + type Iter<'a> + = G::Iter<'a> + where + G: 'a; + type ExteriorIter<'a> + = G::ExteriorIter<'a> + where + G: 'a; + type Scalar = G::Scalar; + + fn coords_iter(&self) -> Self::Iter<'_> { + self.coords_iter_trait() + } + + fn coords_count(&self) -> usize { + self.coords_count_trait() + } + + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_> { + self.exterior_coords_iter_trait() + } +} + +pub trait CoordsIterTrait { + type Scalar: CoordNum; + + type Iter<'a>: Iterator> + where + Self: 'a; + + type ExteriorIter<'a>: Iterator> + where + Self: 'a; + + fn coords_iter_trait(&self) -> Self::Iter<'_>; + + fn coords_count_trait(&self) -> usize; + + fn exterior_coords_iter_trait(&self) -> Self::ExteriorIter<'_>; +} + +// ┌──────────────────────────┐ +// │ Implementation for Coord │ +// └──────────────────────────┘ + +impl CoordsIterTrait for C +where + T: CoordNum, + C: CoordTraitExt, +{ + type Iter<'a> + = iter::Once> + where + Self: 'a; + + type ExteriorIter<'a> + = iter::Once> + where + Self: 'a; + + type Scalar = T; + + fn coords_iter_trait(&self) -> Self::Iter<'_> { + iter::once(self.geo_coord()) + } + + fn coords_count_trait(&self) -> usize { + 1 + } + + fn exterior_coords_iter_trait(&self) -> Self::ExteriorIter<'_> { + self.coords_iter_trait() + } +} + +// ┌──────────────────────────┐ +// │ Implementation for Point │ +// └──────────────────────────┘ + +impl CoordsIterTrait for P +where + T: CoordNum, + P: PointTraitExt, +{ + type Iter<'a> + = option::IntoIter> + where + Self: 'a; + + type ExteriorIter<'a> + = Self::Iter<'a> + where + Self: 'a; + + type Scalar = T; + + fn coords_iter_trait(&self) -> Self::Iter<'_> { + self.geo_coord().into_iter() + } + + fn coords_count_trait(&self) -> usize { + self.coord().map_or(0, |_| 1) + } + + fn exterior_coords_iter_trait(&self) -> Self::ExteriorIter<'_> { + self.geo_coord().into_iter() + } +} + +// ┌─────────────────────────┐ +// │ Implementation for Line │ +// └─────────────────────────┘ + +impl CoordsIterTrait for L +where + T: CoordNum, + L: LineTraitExt, +{ + type Iter<'a> + = iter::Chain>, iter::Once>> + where + Self: 'a; + + type ExteriorIter<'a> + = Self::Iter<'a> + where + Self: 'a; + + type Scalar = T; + + fn coords_iter_trait(&self) -> Self::Iter<'_> { + iter::once(self.start_coord()).chain(iter::once(self.end_coord())) + } + + fn coords_count_trait(&self) -> usize { + 2 + } + + fn exterior_coords_iter_trait(&self) -> Self::ExteriorIter<'_> { + self.coords_iter_trait() + } +} + +// ┌───────────────────────────────┐ +// │ Implementation for LineString │ +// └───────────────────────────────┘ + +pub struct LineStringCoordIter +where + LS: LineStringTraitExt, + LSB: Borrow, +{ + ls: Option, + idx: usize, + limit: usize, + _marker: marker::PhantomData, +} + +impl LineStringCoordIter +where + LS: LineStringTraitExt, + LSB: Borrow, +{ + fn new(ls_opt: Option) -> Self { + match &ls_opt { + Some(ls) => { + let limit = ls.borrow().num_coords(); + Self { + ls: ls_opt, + idx: 0, + limit, + _marker: marker::PhantomData, + } + } + None => Self { + ls: None, + idx: 0, + limit: 0, + _marker: marker::PhantomData, + }, + } + } +} + +impl Iterator for LineStringCoordIter +where + LS: LineStringTraitExt, + LSB: Borrow, +{ + type Item = Coord; + + fn next(&mut self) -> Option { + if self.idx >= self.limit { + None + } else { + let coord = unsafe { + // unwrap should be safe here. If ls is None, limit is 0, and we would not reach here. + // We also have self.idx < self.limit, so we are not accessing out of bounds. + self.ls + .as_ref() + .unwrap() + .borrow() + .geo_coord_unchecked(self.idx) + }; + self.idx += 1; + Some(coord) + } + } +} + +impl CoordsIterTrait for LS +where + T: CoordNum, + LS: LineStringTraitExt, +{ + type Iter<'a> + = LineStringCoordIter + where + Self: 'a; + + type ExteriorIter<'a> + = Self::Iter<'a> + where + Self: 'a; + + type Scalar = T; + + fn coords_iter_trait(&self) -> Self::Iter<'_> { + LineStringCoordIter::new(Some(self)) + } + + fn coords_count_trait(&self) -> usize { + self.num_coords() + } + + fn exterior_coords_iter_trait(&self) -> Self::ExteriorIter<'_> { + self.coords_iter_trait() + } +} + +// ┌────────────────────────────┐ +// │ Implementation for Polygon │ +// └────────────────────────────┘ + +/// State for the PolygonIter +enum PolygonIterState { + Exterior, + Interior(usize), // Holds the current interior ring index + Done, +} + +/// Helper iterator for Polygon coordinates (exterior + interiors) +pub struct PolygonCoordIter<'a, P, BP> +where + P: PolygonTraitExt, + BP: Borrow

, +{ + polygon: BP, + state: PolygonIterState, + ring_idx: usize, + /// Current coordinate index within the current ring + coord_index: usize, + ring_size: usize, + marker: marker::PhantomData<&'a P>, +} + +impl PolygonCoordIter<'_, P, BP> +where + P: PolygonTraitExt, + BP: Borrow

, +{ + fn new(polygon: BP) -> Self { + let ring_size; + let ring_idx; + let initial_state = if let Some(exterior) = polygon.borrow().exterior_ext() { + ring_size = exterior.num_coords(); + ring_idx = 0; + PolygonIterState::Exterior + } else if let Some(interior) = polygon.borrow().interior_ext(0) { + ring_size = interior.num_coords(); + ring_idx = 1; + PolygonIterState::Interior(0) + } else { + ring_size = 0; + ring_idx = 0; + PolygonIterState::Done + }; + + Self { + polygon, + state: initial_state, + ring_idx, + coord_index: 0, + ring_size, + marker: marker::PhantomData, + } + } + + fn start_interior_ring(&mut self, ring_idx: usize, num_coords: usize) { + self.state = PolygonIterState::Interior(ring_idx); + self.ring_idx = ring_idx; + self.coord_index = 0; + self.ring_size = num_coords; + } +} + +impl Iterator for PolygonCoordIter<'_, P, BP> +where + P: PolygonTraitExt, + BP: Borrow

, +{ + type Item = Coord; + + fn next(&mut self) -> Option { + let (ring_idx, ring_size) = { + let ring_opt = if self.ring_idx == 0 { + self.polygon.borrow().exterior_ext() + } else { + self.polygon.borrow().interior_ext(self.ring_idx - 1) + }; + if let Some(ring) = ring_opt { + if self.coord_index < self.ring_size { + let coord = unsafe { ring.geo_coord_unchecked(self.coord_index) }; + self.coord_index += 1; + return Some(coord); + } else { + // Finished this ring, move to next + match self.state { + PolygonIterState::Exterior => { + let interior_opt = self.polygon.borrow().interior_ext(0); + match interior_opt { + Some(interior) => (1, interior.num_coords()), + None => return None, + } + } + PolygonIterState::Interior(ring_idx) => { + let interior_opt = self.polygon.borrow().interior_ext(ring_idx + 1); + match interior_opt { + Some(interior) => (ring_idx + 2, interior.num_coords()), + None => return None, + } + } + PolygonIterState::Done => return None, + } + } + } else { + // No more rings + return None; + } + }; + + self.start_interior_ring(ring_idx, ring_size); + self.next() + } +} + +impl CoordsIterTrait for P +where + T: CoordNum, + P: PolygonTraitExt, +{ + type Iter<'a> + = PolygonCoordIter<'a, P, &'a P> + where + Self: 'a; + + type ExteriorIter<'a> + = LineStringCoordIter, P::RingTypeExt<'a>> + where + Self: 'a; + + type Scalar = T; + + fn coords_iter_trait(&self) -> Self::Iter<'_> { + PolygonCoordIter::new(self) + } + + // Return the number of coordinates in the `Polygon`. + fn coords_count_trait(&self) -> usize { + self.exterior_ext() + .map_or(0, |exterior| exterior.num_coords()) + + self.interiors_ext().map(|i| i.num_coords()).sum::() + } + + fn exterior_coords_iter_trait(&self) -> Self::ExteriorIter<'_> { + let exterior_opt = self.exterior_ext(); + LineStringCoordIter::new(exterior_opt) + } +} + +// ┌───────────────────────────────┐ +// │ Implementation for MultiPoint │ +// └───────────────────────────────┘ + +pub struct MultiPointCoordIter<'a, MP> +where + MP: MultiPointTraitExt, +{ + mp: &'a MP, + idx: usize, + limit: usize, +} + +impl<'a, MP> MultiPointCoordIter<'a, MP> +where + MP: MultiPointTraitExt, +{ + fn new(mp: &'a MP) -> Self { + let limit = mp.num_points(); + Self { mp, idx: 0, limit } + } +} + +impl Iterator for MultiPointCoordIter<'_, MP> +where + MP: MultiPointTraitExt, +{ + type Item = Coord; + + fn next(&mut self) -> Option { + loop { + if self.idx >= self.limit { + return None; + } + let coord = unsafe { self.mp.geo_coord_unchecked(self.idx) }; + self.idx += 1; + if coord.is_some() { + return coord; + } + } + } +} + +impl CoordsIterTrait for MP +where + T: CoordNum, + MP: MultiPointTraitExt, +{ + type Iter<'a> + = MultiPointCoordIter<'a, MP> + where + Self: 'a; + + type ExteriorIter<'a> + = Self::Iter<'a> + where + Self: 'a; + + type Scalar = T; + + fn coords_iter_trait(&self) -> Self::Iter<'_> { + MultiPointCoordIter::new(self) + } + + fn coords_count_trait(&self) -> usize { + self.points_ext() + .filter_map(|p| p.coord_ext().map(|_c| 1)) + .count() + } + + fn exterior_coords_iter_trait(&self) -> Self::ExteriorIter<'_> { + self.coords_iter_trait() + } +} + +// ┌────────────────────────────────────┐ +// │ Implementation for MultiLineString │ +// └────────────────────────────────────┘ + +pub struct MultiLineStringCoordIter<'a, MLS> +where + MLS: MultiLineStringTraitExt, +{ + ml: &'a MLS, + idx_ls: usize, + ls_opt: Option>, + idx: usize, + limit: usize, +} + +impl<'a, T, MLS> MultiLineStringCoordIter<'a, MLS> +where + T: CoordNum, + MLS: MultiLineStringTraitExt, +{ + fn new(ml: &'a MLS) -> Self { + match ml.line_string_ext(0) { + Some(ls) => { + let limit = ls.num_coords(); + Self { + ml, + idx_ls: 0, + ls_opt: Some(ls), + idx: 0, + limit, + } + } + None => Self { + ml, + idx_ls: 0, + ls_opt: None, + idx: 0, + limit: 0, + }, + } + } +} + +impl Iterator for MultiLineStringCoordIter<'_, MLS> +where + T: CoordNum, + MLS: MultiLineStringTraitExt, +{ + type Item = Coord; + + fn next(&mut self) -> Option { + loop { + if self.idx < self.limit { + // When idx < limit, ls_opt is guaranteed to exist. limit is the number of coordinates + // in ls_opt and we have idx < limit, so the geo_coord_unchecked is guaranteed to be safe. + let coord = unsafe { self.ls_opt.as_ref().unwrap().geo_coord_unchecked(self.idx) }; + self.idx += 1; + return Some(coord); + } else { + // Head to the next line string + let ls_opt = self.ml.line_string_ext(self.idx_ls + 1); + match &ls_opt { + Some(ls) => { + self.idx = 0; + self.limit = ls.num_coords(); + self.ls_opt = ls_opt; + self.idx_ls += 1; + } + None => return None, + } + } + } + } +} + +impl CoordsIterTrait for MLS +where + T: CoordNum, + MLS: MultiLineStringTraitExt, +{ + type Iter<'a> + = MultiLineStringCoordIter<'a, MLS> + where + Self: 'a; + + type ExteriorIter<'a> + = Self::Iter<'a> + where + Self: 'a; + + type Scalar = T; + + fn coords_iter_trait(&self) -> Self::Iter<'_> { + MultiLineStringCoordIter::new(self) + } + + fn coords_count_trait(&self) -> usize { + self.line_strings_ext().map(|ls| ls.num_coords()).sum() + } + + fn exterior_coords_iter_trait(&self) -> Self::ExteriorIter<'_> { + self.coords_iter_trait() + } +} + +// ┌─────────────────────────────────┐ +// │ Implementation for MultiPolygon │ +// └─────────────────────────────────┘ + +pub struct MultiPolygonCoordIter<'a, MP> +where + MP: MultiPolygonTraitExt, +{ + mp: &'a MP, + idx_poly: usize, + poly_iter: Option, MP::PolygonTypeExt<'a>>>, +} + +impl<'a, T, MP> MultiPolygonCoordIter<'a, MP> +where + T: CoordNum, + MP: MultiPolygonTraitExt, +{ + fn new(mp: &'a MP) -> Self { + match mp.polygon_ext(0) { + Some(poly) => Self { + mp, + idx_poly: 0, + poly_iter: Some(PolygonCoordIter::new(poly)), + }, + None => Self { + mp, + idx_poly: 0, + poly_iter: None, + }, + } + } +} + +impl Iterator for MultiPolygonCoordIter<'_, MP> +where + T: CoordNum, + MP: MultiPolygonTraitExt, +{ + type Item = Coord; + + fn next(&mut self) -> Option { + match self.poly_iter.as_mut() { + Some(iter) => { + let coord = iter.next(); + if coord.is_some() { + coord + } else { + self.idx_poly += 1; + match self.mp.polygon_ext(self.idx_poly) { + Some(poly) => { + self.poly_iter = Some(PolygonCoordIter::new(poly)); + self.next() + } + None => None, + } + } + } + None => None, + } + } +} + +pub struct MultiPolygonExteriorCoordIter<'a, MP> +where + MP: MultiPolygonTraitExt, +{ + mp: &'a MP, + current_poly: Option>, + idx_poly: usize, + idx: usize, + limit: usize, +} + +impl<'a, T, MP> MultiPolygonExteriorCoordIter<'a, MP> +where + T: CoordNum, + MP: MultiPolygonTraitExt, +{ + fn new(mp: &'a MP) -> Self { + match mp.polygon_ext(0) { + Some(poly) => { + // limit will be zero if the exterior ring doesn't exist. + let limit = poly.exterior_ext().map_or(0, |ring| ring.num_coords()); + Self { + mp, + idx_poly: 0, + idx: 0, + limit, + current_poly: Some(poly), + } + } + None => Self { + mp, + idx_poly: 0, + idx: 0, + limit: 0, + current_poly: None, + }, + } + } +} + +impl Iterator for MultiPolygonExteriorCoordIter<'_, MP> +where + T: CoordNum, + MP: MultiPolygonTraitExt, +{ + type Item = Coord; + + fn next(&mut self) -> Option { + if self.idx < self.limit { + let coord = unsafe { + // When idx < limit, current_poly and the exterior ring are guaranteed to exist. + // This is because if either of them doesn't exist, limit would be 0, and we won't + // reach here in this case. + self.current_poly + .as_ref() + .unwrap() + .exterior_ext() + .unwrap() + .geo_coord_unchecked(self.idx) + }; + self.idx += 1; + Some(coord) + } else { + self.idx_poly += 1; + match self.mp.polygon_ext(self.idx_poly) { + Some(poly) => { + self.idx = 0; + // limit will be zero if the exterior ring doesn't exist. + self.limit = poly.exterior_ext().map_or(0, |ring| ring.num_coords()); + self.current_poly = Some(poly); + self.next() + } + None => None, + } + } + } +} + +impl CoordsIterTrait for MP +where + T: CoordNum, + MP: MultiPolygonTraitExt, +{ + type Iter<'a> + = MultiPolygonCoordIter<'a, MP> + where + Self: 'a; + + type ExteriorIter<'a> + = MultiPolygonExteriorCoordIter<'a, MP> + where + Self: 'a; + + type Scalar = T; + + fn coords_iter_trait(&self) -> Self::Iter<'_> { + MultiPolygonCoordIter::new(self) + } + + fn coords_count_trait(&self) -> usize { + // self.0.iter().map(|polygon| polygon.coords_count()).sum() + self.polygons_ext().map(|p| p.coords_count_trait()).sum() + } + + fn exterior_coords_iter_trait(&self) -> Self::ExteriorIter<'_> { + MultiPolygonExteriorCoordIter::new(self) + } +} + +// ┌───────────────────────────────────────┐ +// │ Implementation for GeometryCollection │ +// └───────────────────────────────────────┘ + +impl CoordsIterTrait for GC +where + GC: GeometryCollectionTraitExt, +{ + type Iter<'a> + = std::vec::IntoIter> + where + Self: 'a; + + type ExteriorIter<'a> + = std::vec::IntoIter> + where + Self: 'a; + + type Scalar = GC::T; + + fn coords_iter_trait(&self) -> Self::Iter<'_> { + // Boxing is likely necessary here due to heterogeneous nature + // and complexity of tracking state across different geometry types + // without significant code complexity or allocations anyway. + let mut all_coords: Vec> = Vec::new(); + for g in self.geometries_ext() { + all_coords.extend(g.coords_iter_trait()); + } + all_coords.into_iter() + } + + /// Return the number of coordinates in the `GeometryCollection`. + fn coords_count_trait(&self) -> usize { + self.geometries_ext().map(|g| g.coords_count_trait()).sum() + } + + fn exterior_coords_iter_trait(&self) -> Self::ExteriorIter<'_> { + let mut all_coords: Vec> = Vec::new(); + for g in self.geometries_ext() { + all_coords.extend(g.exterior_coords_iter_trait()); + } + all_coords.into_iter() + } +} + +// ┌─────────────────────────┐ +// │ Implementation for Rect │ +// └─────────────────────────┘ + +type RectIter = + iter::Chain, iter::Once>>, iter::Once>>; + +impl CoordsIterTrait for TT +where + T: CoordNum, + TT: RectTraitExt, +{ + type Iter<'a> + = RectIter + where + Self: 'a; + type ExteriorIter<'a> + = Self::Iter<'a> + where + Self: 'a; + type Scalar = T; + + /// Iterates over the coordinates in CCW order + fn coords_iter_trait(&self) -> Self::Iter<'_> { + let max = self.max_coord(); + let min = self.min_coord(); + iter::once(coord! { + x: max.x, + y: min.y, + }) + .chain(iter::once(coord! { + x: max.x, + y: max.y, + })) + .chain(iter::once(coord! { + x: min.x, + y: max.y, + })) + .chain(iter::once(coord! { + x: min.x, + y: min.y, + })) + } + + /// Return the number of coordinates in the `Rect`. + /// + /// Note: Although a `Rect` is represented by two coordinates, it is + /// spatially represented by four, so this method returns `4`. + fn coords_count_trait(&self) -> usize { + 4 + } + + fn exterior_coords_iter_trait(&self) -> Self::ExteriorIter<'_> { + self.coords_iter_trait() + } +} + +// ┌─────────────────────────────┐ +// │ Implementation for Triangle │ +// └─────────────────────────────┘ + +impl CoordsIterTrait for TT +where + T: CoordNum, + TT: TriangleTraitExt, +{ + type Iter<'a> + = iter::Chain, iter::Once>> + where + Self: 'a; + type ExteriorIter<'a> + = Self::Iter<'a> + where + Self: 'a; + type Scalar = T; + + fn coords_iter_trait(&self) -> Self::Iter<'_> { + iter::once(self.first_coord()) + .chain(iter::once(self.second_coord())) + .chain(iter::once(self.third_coord())) + } + + /// Return the number of coordinates in the `Triangle`. + fn coords_count_trait(&self) -> usize { + 3 + } + + fn exterior_coords_iter_trait(&self) -> Self::ExteriorIter<'_> { + self.coords_iter_trait() + } +} + +// ┌─────────────────────────────┐ +// │ Implementation for Geometry │ +// └─────────────────────────────┘ + +impl CoordsIterTrait for G +where + T: CoordNum, + G: GeometryTraitExt, +{ + type Iter<'a> + = GeometryTraitCoordsIter<'a, G> + where + Self: 'a; + type ExteriorIter<'a> + = GeometryTraitExteriorCoordsIter<'a, G> + where + Self: 'a; + type Scalar = T; + + fn coords_iter_trait(&self) -> Self::Iter<'_> { + if self.is_collection() { + // Boxing is likely necessary here due to heterogeneous nature + // and complexity of tracking state across different geometry types + // without significant code complexity or allocations anyway. + let mut all_coords: Vec> = Vec::new(); + for g in self.geometries_ext() { + all_coords.extend(g.borrow().coords_iter_trait()); + } + let iter = all_coords.into_iter(); + GeometryTraitCoordsIter::GeometryCollection(iter) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => GeometryTraitCoordsIter::Point(g.coords_iter_trait()), + GeometryTypeExt::Line(g) => GeometryTraitCoordsIter::Line(g.coords_iter_trait()), + GeometryTypeExt::LineString(g) => { + GeometryTraitCoordsIter::LineString(g.coords_iter_trait()) + } + GeometryTypeExt::Polygon(g) => { + GeometryTraitCoordsIter::Polygon(g.coords_iter_trait()) + } + GeometryTypeExt::MultiPoint(g) => { + GeometryTraitCoordsIter::MultiPoint(g.coords_iter_trait()) + } + GeometryTypeExt::MultiLineString(g) => { + GeometryTraitCoordsIter::MultiLineString(g.coords_iter_trait()) + } + GeometryTypeExt::MultiPolygon(g) => { + GeometryTraitCoordsIter::MultiPolygon(g.coords_iter_trait()) + } + GeometryTypeExt::Rect(g) => GeometryTraitCoordsIter::Rect(g.coords_iter_trait()), + GeometryTypeExt::Triangle(g) => { + GeometryTraitCoordsIter::Triangle(g.coords_iter_trait()) + } + } + } + } + + /// Return the number of coordinates in the `Geometry`. + fn coords_count_trait(&self) -> usize { + if self.is_collection() { + self.geometries_ext() + .map(|g_inner| g_inner.borrow().coords_count_trait()) + .sum() + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => g.coords_count_trait(), + GeometryTypeExt::Line(g) => g.coords_count_trait(), + GeometryTypeExt::LineString(g) => g.coords_count_trait(), + GeometryTypeExt::Polygon(g) => g.coords_count_trait(), + GeometryTypeExt::MultiPoint(g) => g.coords_count_trait(), + GeometryTypeExt::MultiLineString(g) => g.coords_count_trait(), + GeometryTypeExt::MultiPolygon(g) => g.coords_count_trait(), + GeometryTypeExt::Rect(g) => g.coords_count_trait(), + GeometryTypeExt::Triangle(g) => g.coords_count_trait(), + } + } + } + + fn exterior_coords_iter_trait(&self) -> Self::ExteriorIter<'_> { + if self.is_collection() { + let mut all_coords: Vec> = Vec::new(); + for g in self.geometries_ext() { + all_coords.extend(g.borrow().exterior_coords_iter_trait()); + } + let iter = all_coords.into_iter(); + GeometryTraitExteriorCoordsIter::GeometryCollection(iter) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => { + GeometryTraitExteriorCoordsIter::Point(g.exterior_coords_iter_trait()) + } + GeometryTypeExt::Line(g) => { + GeometryTraitExteriorCoordsIter::Line(g.exterior_coords_iter_trait()) + } + GeometryTypeExt::LineString(g) => { + GeometryTraitExteriorCoordsIter::LineString(g.exterior_coords_iter_trait()) + } + GeometryTypeExt::Polygon(g) => { + GeometryTraitExteriorCoordsIter::Polygon(g.exterior_coords_iter_trait()) + } + GeometryTypeExt::MultiPoint(g) => { + GeometryTraitExteriorCoordsIter::MultiPoint(g.exterior_coords_iter_trait()) + } + GeometryTypeExt::MultiLineString(g) => { + GeometryTraitExteriorCoordsIter::MultiLineString(g.exterior_coords_iter_trait()) + } + GeometryTypeExt::MultiPolygon(g) => { + GeometryTraitExteriorCoordsIter::MultiPolygon(g.exterior_coords_iter_trait()) + } + GeometryTypeExt::Rect(g) => { + GeometryTraitExteriorCoordsIter::Rect(g.exterior_coords_iter_trait()) + } + GeometryTypeExt::Triangle(g) => { + GeometryTraitExteriorCoordsIter::Triangle(g.exterior_coords_iter_trait()) + } + } + } + } +} + +// ┌──────────────────────────┐ +// │ Implementation for Array │ +// └──────────────────────────┘ + +pub trait CoordsSeqIter { + 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. + fn coords_iter(&self) -> Self::Iter<'_>; + + /// Return the number of coordinates in a geometry. + fn coords_count(&self) -> usize; + + /// Iterate over all exterior coordinates of a geometry. + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_>; +} + +impl CoordsSeqIter for [Coord; N] { + type Iter<'a> + = iter::Copied>> + where + T: 'a; + type ExteriorIter<'a> + = Self::Iter<'a> + where + T: 'a; + type Scalar = T; + + fn coords_iter(&self) -> Self::Iter<'_> { + self.iter().copied() + } + + fn coords_count(&self) -> usize { + N + } + + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_> { + self.coords_iter() + } +} + +// ┌──────────────────────────┐ +// │ Implementation for Slice │ +// └──────────────────────────┘ + +impl<'a, T: CoordNum> CoordsSeqIter for &'a [Coord] { + type Iter<'b> + = iter::Copied>> + where + T: 'b, + 'a: 'b; + type ExteriorIter<'b> + = Self::Iter<'b> + where + T: 'b, + 'a: 'b; + type Scalar = T; + + fn coords_iter(&self) -> Self::Iter<'_> { + self.iter().copied() + } + + fn coords_count(&self) -> usize { + self.len() + } + + fn exterior_coords_iter(&self) -> Self::ExteriorIter<'_> { + self.coords_iter() + } +} + +// Utility to transform Geometry into Iterator +#[doc(hidden)] +pub enum GeometryTraitCoordsIter<'a, G> +where + G: GeometryTraitExt + 'a, +{ + Point( as CoordsIterTrait>::Iter<'a>), + Line( as CoordsIterTrait>::Iter<'a>), + LineString( as CoordsIterTrait>::Iter<'a>), + Polygon( as CoordsIterTrait>::Iter<'a>), + MultiPoint( as CoordsIterTrait>::Iter<'a>), + MultiLineString( + as CoordsIterTrait>::Iter<'a>, + ), + MultiPolygon( as CoordsIterTrait>::Iter<'a>), + GeometryCollection(std::vec::IntoIter>), + Rect( as CoordsIterTrait>::Iter<'a>), + Triangle( as CoordsIterTrait>::Iter<'a>), +} + +impl<'a, G> Iterator for GeometryTraitCoordsIter<'a, G> +where + G: GeometryTraitExt + 'a, +{ + type Item = Coord; + + fn next(&mut self) -> Option { + match self { + GeometryTraitCoordsIter::Point(g) => g.next(), + GeometryTraitCoordsIter::Line(g) => g.next(), + GeometryTraitCoordsIter::LineString(g) => g.next(), + GeometryTraitCoordsIter::Polygon(g) => g.next(), + GeometryTraitCoordsIter::MultiPoint(g) => g.next(), + GeometryTraitCoordsIter::MultiLineString(g) => g.next(), + GeometryTraitCoordsIter::MultiPolygon(g) => g.next(), + GeometryTraitCoordsIter::GeometryCollection(g) => g.next(), + GeometryTraitCoordsIter::Rect(g) => g.next(), + GeometryTraitCoordsIter::Triangle(g) => g.next(), + } + } + + fn size_hint(&self) -> (usize, Option) { + match self { + GeometryTraitCoordsIter::Point(g) => g.size_hint(), + GeometryTraitCoordsIter::Line(g) => g.size_hint(), + GeometryTraitCoordsIter::LineString(g) => g.size_hint(), + GeometryTraitCoordsIter::Polygon(g) => g.size_hint(), + GeometryTraitCoordsIter::MultiPoint(g) => g.size_hint(), + GeometryTraitCoordsIter::MultiLineString(g) => g.size_hint(), + GeometryTraitCoordsIter::MultiPolygon(g) => g.size_hint(), + GeometryTraitCoordsIter::GeometryCollection(g) => g.size_hint(), + GeometryTraitCoordsIter::Rect(g) => g.size_hint(), + GeometryTraitCoordsIter::Triangle(g) => g.size_hint(), + } + } +} + +// Utility to transform Geometry into Iterator +#[doc(hidden)] +pub enum GeometryTraitExteriorCoordsIter<'a, G> +where + G: GeometryTraitExt + 'a, +{ + Point( as CoordsIterTrait>::ExteriorIter<'a>), + Line( as CoordsIterTrait>::ExteriorIter<'a>), + LineString( as CoordsIterTrait>::ExteriorIter<'a>), + Polygon( as CoordsIterTrait>::ExteriorIter<'a>), + MultiPoint( as CoordsIterTrait>::ExteriorIter<'a>), + MultiLineString( + as CoordsIterTrait>::ExteriorIter<'a>, + ), + MultiPolygon( + as CoordsIterTrait>::ExteriorIter<'a>, + ), + GeometryCollection(std::vec::IntoIter>), + Rect( as CoordsIterTrait>::ExteriorIter<'a>), + Triangle( as CoordsIterTrait>::ExteriorIter<'a>), +} + +impl<'a, G> Iterator for GeometryTraitExteriorCoordsIter<'a, G> +where + G: GeometryTraitExt + 'a, +{ + type Item = Coord; + + fn next(&mut self) -> Option { + match self { + GeometryTraitExteriorCoordsIter::Point(g) => g.next(), + GeometryTraitExteriorCoordsIter::Line(g) => g.next(), + GeometryTraitExteriorCoordsIter::LineString(g) => g.next(), + GeometryTraitExteriorCoordsIter::Polygon(g) => g.next(), + GeometryTraitExteriorCoordsIter::MultiPoint(g) => g.next(), + GeometryTraitExteriorCoordsIter::MultiLineString(g) => g.next(), + GeometryTraitExteriorCoordsIter::MultiPolygon(g) => g.next(), + GeometryTraitExteriorCoordsIter::GeometryCollection(g) => g.next(), + GeometryTraitExteriorCoordsIter::Rect(g) => g.next(), + GeometryTraitExteriorCoordsIter::Triangle(g) => g.next(), + } + } + + fn size_hint(&self) -> (usize, Option) { + match self { + GeometryTraitExteriorCoordsIter::Point(g) => g.size_hint(), + GeometryTraitExteriorCoordsIter::Line(g) => g.size_hint(), + GeometryTraitExteriorCoordsIter::LineString(g) => g.size_hint(), + GeometryTraitExteriorCoordsIter::Polygon(g) => g.size_hint(), + GeometryTraitExteriorCoordsIter::MultiPoint(g) => g.size_hint(), + GeometryTraitExteriorCoordsIter::MultiLineString(g) => g.size_hint(), + GeometryTraitExteriorCoordsIter::MultiPolygon(g) => g.size_hint(), + GeometryTraitExteriorCoordsIter::GeometryCollection(g) => g.size_hint(), + GeometryTraitExteriorCoordsIter::Rect(g) => g.size_hint(), + GeometryTraitExteriorCoordsIter::Triangle(g) => g.size_hint(), + } + } +} + +#[cfg(test)] +mod test { + use super::CoordsIter; + use super::CoordsSeqIter; + use crate::{ + coord, line_string, point, polygon, Coord, Geometry, GeometryCollection, Line, LineString, + MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, Rect, Triangle, + }; + + #[test] + fn test_point() { + let (point, expected_coords) = create_point(); + + let actual_coords = point.coords_iter().collect::>(); + + assert_eq!(expected_coords, actual_coords); + } + + #[test] + fn test_line() { + let line = Line::new(coord! { x: 1., y: 2. }, coord! { x: 2., y: 3. }); + + let coords = line.coords_iter().collect::>(); + + assert_eq!( + vec![coord! { x: 1., y: 2. }, coord! { x: 2., y: 3. },], + coords + ); + } + + #[test] + fn test_line_string() { + let (line_string, expected_coords) = create_line_string(); + + let actual_coords = line_string.coords_iter().collect::>(); + + assert_eq!(expected_coords, actual_coords); + } + + #[test] + fn test_polygon() { + let (polygon, expected_coords) = create_polygon(); + + let actual_coords = polygon.coords_iter().collect::>(); + + assert_eq!(expected_coords, actual_coords); + } + + #[test] + fn test_multi_point() { + let mut expected_coords = vec![]; + let (point, mut coords) = create_point(); + expected_coords.append(&mut coords.clone()); + expected_coords.append(&mut coords); + + let actual_coords = MultiPoint::new(vec![point, point]) + .coords_iter() + .collect::>(); + + assert_eq!(expected_coords, actual_coords); + } + + #[test] + fn test_multi_line_string() { + let mut expected_coords = vec![]; + let (line_string, mut coords) = create_line_string(); + expected_coords.append(&mut coords.clone()); + expected_coords.append(&mut coords); + + let actual_coords = MultiLineString::new(vec![line_string.clone(), line_string]) + .coords_iter() + .collect::>(); + + assert_eq!(expected_coords, actual_coords); + } + + #[test] + fn test_multi_polygon() { + let mut expected_coords = vec![]; + let (polygon, mut coords) = create_polygon(); + expected_coords.append(&mut coords.clone()); + expected_coords.append(&mut coords); + + let actual_coords = MultiPolygon::new(vec![polygon.clone(), polygon]) + .coords_iter() + .collect::>(); + + assert_eq!(expected_coords, actual_coords); + } + + #[test] + fn test_geometry() { + let (line_string, expected_coords) = create_line_string(); + + let actual_coords = Geometry::LineString(line_string) + .coords_iter() + .collect::>(); + + assert_eq!(expected_coords, actual_coords); + } + + #[test] + fn test_rect() { + let (rect, expected_coords) = create_rect(); + + let actual_coords = rect.coords_iter().collect::>(); + + assert_eq!(expected_coords, actual_coords); + } + + #[test] + fn test_triangle() { + let (triangle, expected_coords) = create_triangle(); + + let actual_coords = triangle.coords_iter().collect::>(); + + assert_eq!(expected_coords, actual_coords); + } + + #[test] + fn test_geometry_collection() { + let mut expected_coords = vec![]; + let (line_string, mut coords) = create_line_string(); + expected_coords.append(&mut coords); + let (polygon, mut coords) = create_polygon(); + expected_coords.append(&mut coords); + + let collection = GeometryCollection::new_from(vec![ + Geometry::LineString(line_string), + Geometry::Polygon(polygon), + ]); + let geom = Geometry::GeometryCollection(collection.clone()); + + let actual_coords = collection.coords_iter().collect::>(); + assert_eq!(expected_coords, actual_coords); + + let actual_coords = geom.coords_iter().collect::>(); + assert_eq!(expected_coords, actual_coords); + } + + #[test] + fn test_array() { + let coords = [ + coord! { x: 1., y: 2. }, + coord! { x: 3., y: 4. }, + coord! { x: 5., y: 6. }, + ]; + + let actual_coords = coords.coords_iter().collect::>(); + + assert_eq!(coords.to_vec(), actual_coords); + } + + #[test] + fn test_slice() { + let coords = &[ + coord! { x: 1., y: 2. }, + coord! { x: 3., y: 4. }, + coord! { x: 5., y: 6. }, + ]; + + let actual_coords = coords.coords_iter().collect::>(); + + assert_eq!(coords.to_vec(), actual_coords); + } + + #[test] + fn test_coord() { + let c = coord! { x: 1., y: 2. }; + let actual_coords = c.coords_iter().collect::>(); + assert_eq!(vec![c], actual_coords); + } + + fn create_point() -> (Point, Vec) { + (point!(x: 1., y: 2.), vec![coord! { x: 1., y: 2. }]) + } + + fn create_triangle() -> (Triangle, Vec) { + ( + Triangle::new( + coord! { x: 1., y: 2. }, + coord! { x: 3., y: 4. }, + coord! { x: 5., y: 6. }, + ), + vec![ + coord! { x: 1., y: 2. }, + coord! { x: 3., y: 4. }, + coord! { x: 5., y: 6. }, + ], + ) + } + + fn create_rect() -> (Rect, Vec) { + ( + Rect::new(coord! { x: 1., y: 2. }, coord! { x: 3., y: 4. }), + vec![ + coord! { x: 3., y: 2. }, + coord! { x: 3., y: 4. }, + coord! { x: 1., y: 4. }, + coord! { x: 1., y: 2. }, + ], + ) + } + + fn create_line_string() -> (LineString, Vec) { + ( + line_string![ + (x: 1., y: 2.), + (x: 2., y: 3.), + ], + vec![coord! { x: 1., y: 2. }, coord! { x: 2., y: 3. }], + ) + } + + fn create_polygon() -> (Polygon, Vec) { + ( + polygon!( + exterior: [(x: 0., y: 0.), (x: 5., y: 10.), (x: 10., y: 0.), (x: 0., y: 0.)], + interiors: [[(x: 1., y: 1.), (x: 9., y: 1.), (x: 5., y: 9.), (x: 1., y: 1.)]], + ), + vec![ + coord! { x: 0.0, y: 0.0 }, + coord! { x: 5.0, y: 10.0 }, + coord! { x: 10.0, y: 0.0 }, + coord! { x: 0.0, y: 0.0 }, + coord! { x: 1.0, y: 1.0 }, + coord! { x: 9.0, y: 1.0 }, + coord! { x: 5.0, y: 9.0 }, + coord! { x: 1.0, y: 1.0 }, + ], + ) + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/dimensions.rs b/rust/sedona-geo-generic-alg/src/algorithm/dimensions.rs new file mode 100644 index 00000000..bf3d8b4e --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/dimensions.rs @@ -0,0 +1,781 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use core::borrow::Borrow; +use sedona_geo_traits_ext::*; + +use crate::Orientation::Collinear; +use crate::{CoordNum, GeoNum}; + +/// Geometries can have 0, 1, or two dimensions. Or, in the case of an [`empty`](#is_empty) +/// geometry, a special `Empty` dimensionality. +/// +/// # Examples +/// +/// ``` +/// use geo_types::{Point, Rect, line_string}; +/// use sedona_geo_generic_alg::dimensions::{HasDimensions, Dimensions}; +/// +/// let point = Point::new(0.0, 5.0); +/// let line_string = line_string![(x: 0.0, y: 0.0), (x: 5.0, y: 5.0), (x: 0.0, y: 5.0)]; +/// let rect = Rect::new((0.0, 0.0), (10.0, 10.0)); +/// assert_eq!(Dimensions::ZeroDimensional, point.dimensions()); +/// assert_eq!(Dimensions::OneDimensional, line_string.dimensions()); +/// assert_eq!(Dimensions::TwoDimensional, rect.dimensions()); +/// +/// assert!(point.dimensions() < line_string.dimensions()); +/// assert!(rect.dimensions() > line_string.dimensions()); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +pub enum Dimensions { + /// Some geometries, like a `MultiPoint` or `GeometryCollection` may have no elements - thus no + /// dimensions. Note that this is distinct from being `ZeroDimensional`, like a `Point`. + Empty, + /// Dimension of a point + ZeroDimensional, + /// Dimension of a line or curve + OneDimensional, + /// Dimension of a surface + TwoDimensional, +} + +/// Operate on the dimensionality of geometries. +pub trait HasDimensions { + /// Some geometries, like a `MultiPoint`, can have zero coordinates - we call these `empty`. + /// + /// Types like `Point` and `Rect`, which have at least one coordinate by construction, can + /// never be considered empty. + /// ``` + /// use geo_types::{Point, coord, LineString}; + /// use sedona_geo_generic_alg::HasDimensions; + /// + /// let line_string = LineString::new(vec![ + /// coord! { x: 0., y: 0. }, + /// coord! { x: 10., y: 0. }, + /// ]); + /// assert!(!line_string.is_empty()); + /// + /// let empty_line_string: LineString = LineString::new(vec![]); + /// assert!(empty_line_string.is_empty()); + /// + /// let point = Point::new(0.0, 0.0); + /// assert!(!point.is_empty()); + /// ``` + fn is_empty(&self) -> bool; + + /// The dimensions of some geometries are fixed, e.g. a Point always has 0 dimensions. However + /// for others, the dimensionality depends on the specific geometry instance - for example + /// typical `Rect`s are 2-dimensional, but it's possible to create degenerate `Rect`s which + /// have either 1 or 0 dimensions. + /// + /// ## Examples + /// + /// ``` + /// use geo_types::{GeometryCollection, Rect, Point}; + /// use sedona_geo_generic_alg::dimensions::{Dimensions, HasDimensions}; + /// + /// // normal rectangle + /// let rect = Rect::new((0.0, 0.0), (10.0, 10.0)); + /// assert_eq!(Dimensions::TwoDimensional, rect.dimensions()); + /// + /// // "rectangle" with zero height degenerates to a line + /// let degenerate_line_rect = Rect::new((0.0, 10.0), (10.0, 10.0)); + /// assert_eq!(Dimensions::OneDimensional, degenerate_line_rect.dimensions()); + /// + /// // "rectangle" with zero height and zero width degenerates to a point + /// let degenerate_point_rect = Rect::new((10.0, 10.0), (10.0, 10.0)); + /// assert_eq!(Dimensions::ZeroDimensional, degenerate_point_rect.dimensions()); + /// + /// // collections inherit the greatest dimensionality of their elements + /// let geometry_collection = GeometryCollection::new_from(vec![degenerate_line_rect.into(), degenerate_point_rect.into()]); + /// assert_eq!(Dimensions::OneDimensional, geometry_collection.dimensions()); + /// + /// let point = Point::new(10.0, 10.0); + /// assert_eq!(Dimensions::ZeroDimensional, point.dimensions()); + /// + /// // An `Empty` dimensionality is distinct from, and less than, being 0-dimensional + /// let empty_collection = GeometryCollection::::new_from(vec![]); + /// assert_eq!(Dimensions::Empty, empty_collection.dimensions()); + /// assert!(empty_collection.dimensions() < point.dimensions()); + /// ``` + fn dimensions(&self) -> Dimensions; + + /// The dimensions of the `Geometry`'s boundary, as used by OGC-SFA. + /// + /// ## Examples + /// + /// ``` + /// use geo_types::{GeometryCollection, Rect, Point}; + /// use sedona_geo_generic_alg::dimensions::{Dimensions, HasDimensions}; + /// + /// // a point has no boundary + /// let point = Point::new(10.0, 10.0); + /// assert_eq!(Dimensions::Empty, point.boundary_dimensions()); + /// + /// // a typical rectangle has a *line* (one dimensional) boundary + /// let rect = Rect::new((0.0, 0.0), (10.0, 10.0)); + /// assert_eq!(Dimensions::OneDimensional, rect.boundary_dimensions()); + /// + /// // a "rectangle" with zero height degenerates to a line, whose boundary is two points + /// let degenerate_line_rect = Rect::new((0.0, 10.0), (10.0, 10.0)); + /// assert_eq!(Dimensions::ZeroDimensional, degenerate_line_rect.boundary_dimensions()); + /// + /// // a "rectangle" with zero height and zero width degenerates to a point, + /// // and points have no boundary + /// let degenerate_point_rect = Rect::new((10.0, 10.0), (10.0, 10.0)); + /// assert_eq!(Dimensions::Empty, degenerate_point_rect.boundary_dimensions()); + /// + /// // collections inherit the greatest dimensionality of their elements + /// let geometry_collection = GeometryCollection::new_from(vec![degenerate_line_rect.into(), degenerate_point_rect.into()]); + /// assert_eq!(Dimensions::ZeroDimensional, geometry_collection.boundary_dimensions()); + /// + /// let geometry_collection = GeometryCollection::::new_from(vec![]); + /// assert_eq!(Dimensions::Empty, geometry_collection.boundary_dimensions()); + /// ``` + fn boundary_dimensions(&self) -> Dimensions; +} + +impl HasDimensions for G +where + G: GeoTraitExtWithTypeTag + HasDimensionsTrait, +{ + fn is_empty(&self) -> bool { + self.is_empty_trait() + } + + fn dimensions(&self) -> Dimensions { + self.dimensions_trait() + } + + fn boundary_dimensions(&self) -> Dimensions { + self.boundary_dimensions_trait() + } +} + +trait HasDimensionsTrait { + fn is_empty_trait(&self) -> bool; + fn dimensions_trait(&self) -> Dimensions; + fn boundary_dimensions_trait(&self) -> Dimensions; +} + +impl HasDimensionsTrait for G +where + G: GeometryTraitExt, +{ + fn is_empty_trait(&self) -> bool { + if self.is_collection() { + if self.num_geometries_ext() == 0 { + true + } else { + self.geometries_ext() + .all(|g_inner| g_inner.borrow().is_empty_trait()) + } + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => g.is_empty_trait(), + GeometryTypeExt::Line(g) => g.is_empty_trait(), + GeometryTypeExt::LineString(g) => g.is_empty_trait(), + GeometryTypeExt::Polygon(g) => g.is_empty_trait(), + GeometryTypeExt::MultiPoint(g) => g.is_empty_trait(), + GeometryTypeExt::MultiLineString(g) => g.is_empty_trait(), + GeometryTypeExt::MultiPolygon(g) => g.is_empty_trait(), + GeometryTypeExt::Rect(g) => g.is_empty_trait(), + GeometryTypeExt::Triangle(g) => g.is_empty_trait(), + } + } + } + + fn dimensions_trait(&self) -> Dimensions { + if self.is_collection() { + let mut max = Dimensions::Empty; + for geom in self.geometries_ext() { + let dimensions = geom.borrow().dimensions_trait(); + if dimensions == Dimensions::TwoDimensional { + // short-circuit since we know none can be larger + return Dimensions::TwoDimensional; + } + max = max.max(dimensions); + } + max + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => g.dimensions_trait(), + GeometryTypeExt::Line(g) => g.dimensions_trait(), + GeometryTypeExt::LineString(g) => g.dimensions_trait(), + GeometryTypeExt::Polygon(g) => g.dimensions_trait(), + GeometryTypeExt::MultiPoint(g) => g.dimensions_trait(), + GeometryTypeExt::MultiLineString(g) => g.dimensions_trait(), + GeometryTypeExt::MultiPolygon(g) => g.dimensions_trait(), + GeometryTypeExt::Rect(g) => g.dimensions_trait(), + GeometryTypeExt::Triangle(g) => g.dimensions_trait(), + } + } + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + if self.is_collection() { + let mut max = Dimensions::Empty; + for geom in self.geometries_ext() { + let d = geom.borrow().boundary_dimensions_trait(); + + if d == Dimensions::OneDimensional { + return Dimensions::OneDimensional; + } + + max = max.max(d); + } + max + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::Line(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::LineString(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::Polygon(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::MultiPoint(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::MultiLineString(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::MultiPolygon(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::Rect(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::Triangle(g) => g.boundary_dimensions_trait(), + } + } + } +} + +impl HasDimensionsTrait for P +where + P: PointTraitExt, +{ + fn is_empty_trait(&self) -> bool { + false + } + + fn dimensions_trait(&self) -> Dimensions { + Dimensions::ZeroDimensional + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + Dimensions::Empty + } +} + +impl HasDimensionsTrait for L +where + L: LineTraitExt, +{ + fn is_empty_trait(&self) -> bool { + false + } + + fn dimensions_trait(&self) -> Dimensions { + if self.start_coord() == self.end_coord() { + // degenerate line is a point + Dimensions::ZeroDimensional + } else { + Dimensions::OneDimensional + } + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + if self.start_coord() == self.end_coord() { + // degenerate line is a point, which has no boundary + Dimensions::Empty + } else { + Dimensions::ZeroDimensional + } + } +} + +impl HasDimensionsTrait for LS +where + LS: LineStringTraitExt, +{ + fn is_empty_trait(&self) -> bool { + self.num_coords() == 0 + } + + fn dimensions_trait(&self) -> Dimensions { + if self.num_coords() == 0 { + return Dimensions::Empty; + } + + // There should be at least 1 coordinate since num_coords is not 0. + let first = unsafe { self.geo_coord_unchecked(0) }; + if self.coord_iter().any(|coord| first != coord) { + Dimensions::OneDimensional + } else { + // all coords are the same - i.e. a point + Dimensions::ZeroDimensional + } + } + + /// ``` + /// use geo_types::line_string; + /// use sedona_geo_generic_alg::dimensions::{HasDimensions, Dimensions}; + /// + /// let ls = line_string![(x: 0., y: 0.), (x: 0., y: 1.), (x: 1., y: 1.)]; + /// assert_eq!(Dimensions::ZeroDimensional, ls.boundary_dimensions()); + /// + /// let ls = line_string![(x: 0., y: 0.), (x: 0., y: 1.), (x: 1., y: 1.), (x: 0., y: 0.)]; + /// assert_eq!(Dimensions::Empty, ls.boundary_dimensions()); + ///``` + fn boundary_dimensions_trait(&self) -> Dimensions { + if self.is_closed() { + return Dimensions::Empty; + } + + match self.dimensions_trait() { + Dimensions::Empty | Dimensions::ZeroDimensional => Dimensions::Empty, + Dimensions::OneDimensional => Dimensions::ZeroDimensional, + Dimensions::TwoDimensional => unreachable!("line_string cannot be 2 dimensional"), + } + } +} + +impl HasDimensionsTrait for P +where + P: PolygonTraitExt, +{ + fn is_empty_trait(&self) -> bool { + self.exterior_ext() + .is_none_or(|exterior| exterior.is_empty_trait()) + } + + fn dimensions_trait(&self) -> Dimensions { + if let Some(exterior) = self.exterior_ext() { + let mut coords = exterior.coord_iter(); + + let Some(first) = coords.next() else { + // No coordinates - the polygon is empty + return Dimensions::Empty; + }; + + let Some(second) = coords.find(|next| *next != first) else { + // All coordinates in the polygon are the same point + return Dimensions::ZeroDimensional; + }; + + let Some(_third) = coords.find(|next| *next != first && *next != second) else { + // There are only two distinct coordinates in the Polygon - it's collapsed to a line + return Dimensions::OneDimensional; + }; + + Dimensions::TwoDimensional + } else { + Dimensions::Empty + } + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + match self.dimensions_trait() { + Dimensions::Empty | Dimensions::ZeroDimensional => Dimensions::Empty, + Dimensions::OneDimensional => Dimensions::ZeroDimensional, + Dimensions::TwoDimensional => Dimensions::OneDimensional, + } + } +} + +impl HasDimensionsTrait for MP +where + MP: MultiPointTraitExt, +{ + fn is_empty_trait(&self) -> bool { + self.num_points() == 0 + } + + fn dimensions_trait(&self) -> Dimensions { + if self.num_points() == 0 { + return Dimensions::Empty; + } + + Dimensions::ZeroDimensional + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + Dimensions::Empty + } +} + +impl HasDimensionsTrait for MLS +where + MLS: MultiLineStringTraitExt, +{ + fn is_empty_trait(&self) -> bool { + self.line_strings_ext().all(|ls| ls.is_empty_trait()) + } + + fn dimensions_trait(&self) -> Dimensions { + let mut max = Dimensions::Empty; + for line in self.line_strings_ext() { + match line.dimensions_trait() { + Dimensions::Empty => {} + Dimensions::ZeroDimensional => max = Dimensions::ZeroDimensional, + Dimensions::OneDimensional => { + // return early since we know multi line string dimensionality cannot exceed + // 1-d + return Dimensions::OneDimensional; + } + Dimensions::TwoDimensional => unreachable!("MultiLineString cannot be 2d"), + } + } + max + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + if self.is_closed() { + return Dimensions::Empty; + } + + match self.dimensions_trait() { + Dimensions::Empty | Dimensions::ZeroDimensional => Dimensions::Empty, + Dimensions::OneDimensional => Dimensions::ZeroDimensional, + Dimensions::TwoDimensional => unreachable!("line_string cannot be 2 dimensional"), + } + } +} + +impl HasDimensionsTrait for MP +where + MP: MultiPolygonTraitExt, +{ + fn is_empty_trait(&self) -> bool { + self.polygons_ext().all(|p| p.is_empty_trait()) + } + + fn dimensions_trait(&self) -> Dimensions { + let mut max = Dimensions::Empty; + for geom in self.polygons_ext() { + let dimensions = geom.dimensions_trait(); + if dimensions == Dimensions::TwoDimensional { + // short-circuit since we know none can be larger + return Dimensions::TwoDimensional; + } + max = max.max(dimensions) + } + max + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + match self.dimensions_trait() { + Dimensions::Empty | Dimensions::ZeroDimensional => Dimensions::Empty, + Dimensions::OneDimensional => Dimensions::ZeroDimensional, + Dimensions::TwoDimensional => Dimensions::OneDimensional, + } + } +} + +impl HasDimensionsTrait for GC +where + GC: GeometryCollectionTraitExt, +{ + fn is_empty_trait(&self) -> bool { + if self.num_geometries() == 0 { + true + } else { + self.geometries_ext().all(|g| g.is_empty_trait()) + } + } + + fn dimensions_trait(&self) -> Dimensions { + let mut max = Dimensions::Empty; + for geom in self.geometries_ext() { + let dimensions = geom.dimensions_trait(); + if dimensions == Dimensions::TwoDimensional { + // short-circuit since we know none can be larger + return Dimensions::TwoDimensional; + } + max = max.max(dimensions); + } + max + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + let mut max = Dimensions::Empty; + for geom in self.geometries_ext() { + let d = geom.boundary_dimensions_trait(); + + if d == Dimensions::OneDimensional { + return Dimensions::OneDimensional; + } + + max = max.max(d); + } + max + } +} + +impl HasDimensionsTrait for R +where + R: RectTraitExt, +{ + fn is_empty_trait(&self) -> bool { + false + } + + fn dimensions_trait(&self) -> Dimensions { + if self.min_coord() == self.max_coord() { + // degenerate rectangle is a point + Dimensions::ZeroDimensional + } else if self.min_coord().x == self.max_coord().x + || self.min_coord().y == self.max_coord().y + { + // degenerate rectangle is a line + Dimensions::OneDimensional + } else { + Dimensions::TwoDimensional + } + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + match self.dimensions_trait() { + Dimensions::Empty => { + unreachable!("even a degenerate rect should be at least 0-Dimensional") + } + Dimensions::ZeroDimensional => Dimensions::Empty, + Dimensions::OneDimensional => Dimensions::ZeroDimensional, + Dimensions::TwoDimensional => Dimensions::OneDimensional, + } + } +} + +impl HasDimensionsTrait for T +where + T: TriangleTraitExt, +{ + fn is_empty_trait(&self) -> bool { + false + } + + fn dimensions_trait(&self) -> Dimensions { + use crate::Kernel; + let (c0, c1, c2) = (self.first_coord(), self.second_coord(), self.third_coord()); + if Collinear == C::Ker::orient2d(c0, c1, c2) { + if c0 == c1 && c1 == c2 { + // degenerate triangle is a point + Dimensions::ZeroDimensional + } else { + // degenerate triangle is a line + Dimensions::OneDimensional + } + } else { + Dimensions::TwoDimensional + } + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + match self.dimensions_trait() { + Dimensions::Empty => { + unreachable!("even a degenerate triangle should be at least 0-dimensional") + } + Dimensions::ZeroDimensional => Dimensions::Empty, + Dimensions::OneDimensional => Dimensions::ZeroDimensional, + Dimensions::TwoDimensional => Dimensions::OneDimensional, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use geo_types::*; + + const ONE: Coord = crate::coord!(x: 1.0, y: 1.0); + use crate::wkt; + + #[test] + fn point() { + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(POINT(1.0 1.0)).dimensions_trait() + ); + } + + #[test] + fn line_string() { + assert_eq!( + Dimensions::OneDimensional, + wkt!(LINESTRING(1.0 1.0,2.0 2.0,3.0 3.0)).dimensions_trait() + ); + } + + #[test] + fn polygon() { + assert_eq!( + Dimensions::TwoDimensional, + wkt!(POLYGON((1.0 1.0,2.0 2.0,3.0 3.0,1.0 1.0))).dimensions_trait() + ); + } + + #[test] + fn multi_point() { + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(MULTIPOINT(1.0 1.0)).dimensions_trait() + ); + } + + #[test] + fn multi_line_string() { + assert_eq!( + Dimensions::OneDimensional, + wkt!(MULTILINESTRING((1.0 1.0,2.0 2.0,3.0 3.0))).dimensions_trait() + ); + } + + #[test] + fn multi_polygon() { + assert_eq!( + Dimensions::TwoDimensional, + wkt!(MULTIPOLYGON(((1.0 1.0,2.0 2.0,3.0 3.0,1.0 1.0)))).dimensions_trait() + ); + } + + mod empty { + use super::*; + #[test] + fn empty_line_string() { + assert_eq!( + Dimensions::Empty, + (wkt!(LINESTRING EMPTY) as LineString).dimensions_trait() + ); + } + + #[test] + fn empty_polygon() { + assert_eq!( + Dimensions::Empty, + (wkt!(POLYGON EMPTY) as Polygon).dimensions_trait() + ); + } + + #[test] + fn empty_multi_point() { + assert_eq!( + Dimensions::Empty, + (wkt!(MULTIPOINT EMPTY) as MultiPoint).dimensions_trait() + ); + } + + #[test] + fn empty_multi_line_string() { + assert_eq!( + Dimensions::Empty, + (wkt!(MULTILINESTRING EMPTY) as MultiLineString).dimensions_trait() + ); + } + + #[test] + fn multi_line_string_with_empty_line_string() { + let empty_line_string = wkt!(LINESTRING EMPTY) as LineString; + let multi_line_string = MultiLineString::new(vec![empty_line_string]); + assert_eq!(Dimensions::Empty, multi_line_string.dimensions_trait()); + } + + #[test] + fn empty_multi_polygon() { + assert_eq!( + Dimensions::Empty, + (wkt!(MULTIPOLYGON EMPTY) as MultiPolygon).dimensions_trait() + ); + } + + #[test] + fn multi_polygon_with_empty_polygon() { + let empty_polygon = (wkt!(POLYGON EMPTY) as Polygon); + let multi_polygon = MultiPolygon::new(vec![empty_polygon]); + assert_eq!(Dimensions::Empty, multi_polygon.dimensions_trait()); + } + } + + mod dimensional_collapse { + use super::*; + + #[test] + fn line_collapsed_to_point() { + assert_eq!( + Dimensions::ZeroDimensional, + Line::new(ONE, ONE).dimensions_trait() + ); + } + + #[test] + fn line_string_collapsed_to_point() { + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(LINESTRING(1.0 1.0)).dimensions_trait() + ); + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(LINESTRING(1.0 1.0,1.0 1.0)).dimensions_trait() + ); + } + + #[test] + fn polygon_collapsed_to_point() { + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(POLYGON((1.0 1.0))).dimensions_trait() + ); + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(POLYGON((1.0 1.0,1.0 1.0))).dimensions_trait() + ); + } + + #[test] + fn polygon_collapsed_to_line() { + assert_eq!( + Dimensions::OneDimensional, + wkt!(POLYGON((1.0 1.0,2.0 2.0))).dimensions_trait() + ); + } + + #[test] + fn multi_line_string_with_line_string_collapsed_to_point() { + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(MULTILINESTRING((1.0 1.0))).dimensions_trait() + ); + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(MULTILINESTRING((1.0 1.0,1.0 1.0))).dimensions_trait() + ); + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(MULTILINESTRING((1.0 1.0),(1.0 1.0))).dimensions_trait() + ); + } + + #[test] + fn multi_polygon_with_polygon_collapsed_to_point() { + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(MULTIPOLYGON(((1.0 1.0)))).dimensions_trait() + ); + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(MULTIPOLYGON(((1.0 1.0,1.0 1.0)))).dimensions_trait() + ); + } + + #[test] + fn multi_polygon_with_polygon_collapsed_to_line() { + assert_eq!( + Dimensions::OneDimensional, + wkt!(MULTIPOLYGON(((1.0 1.0,2.0 2.0)))).dimensions_trait() + ); + } + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/euclidean_length.rs b/rust/sedona-geo-generic-alg/src/algorithm/euclidean_length.rs new file mode 100644 index 00000000..6aa09a73 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/euclidean_length.rs @@ -0,0 +1,566 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use core::borrow::Borrow; +use std::iter::Sum; + +use crate::CoordFloat; +use sedona_geo_traits_ext::*; + +/// Calculation of the length +#[deprecated( + since = "0.29.0", + note = "Please use the `Euclidean.length(&line)` via the `Length` trait instead." +)] +pub trait EuclideanLength { + /// Calculation of the length of a Line + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::EuclideanLength; + /// use sedona_geo_generic_alg::line_string; + /// + /// let line_string = line_string![ + /// (x: 40.02f64, y: 116.34), + /// (x: 42.02f64, y: 116.34), + /// ]; + /// + /// assert_eq!( + /// 2., + /// line_string.euclidean_length(), + /// ) + /// ``` + fn euclidean_length(&self) -> T; +} + +#[allow(deprecated)] +impl EuclideanLength for G +where + T: CoordFloat + Sum, + G: GeoTraitExtWithTypeTag + EuclideanLengthTrait, +{ + fn euclidean_length(&self) -> T { + self.euclidean_length_trait() + } +} + +trait EuclideanLengthTrait +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T; +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for L +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + let start_coord = self.start_coord(); + let end_coord = self.end_coord(); + let delta = start_coord - end_coord; + delta.x.hypot(delta.y) + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for LS +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + let mut length = T::zero(); + for line in self.lines() { + let start_coord = line.start_coord(); + let end_coord = line.end_coord(); + let delta = start_coord - end_coord; + length = length + delta.x.hypot(delta.y); + } + length + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for MLS +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + let mut length = T::zero(); + for line_string in self.line_strings_ext() { + length = length + line_string.euclidean_length_trait(); + } + length + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for P +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + // Length is a 1D concept, doesn't apply to 2D polygons + // Return zero, similar to how Area returns zero for linear geometries + T::zero() + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for P +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + // A point has no length dimension + T::zero() + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for MP +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + // Points have no length dimension + T::zero() + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for MPG +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + // Length is a 1D concept, doesn't apply to 2D polygons + T::zero() + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for R +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + // Length is a 1D concept, doesn't apply to 2D rectangles + T::zero() + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for TR +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + // Length is a 1D concept, doesn't apply to 2D triangles + T::zero() + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for GC +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + // Sum the lengths of all geometries in the collection + // Linear geometries (lines, linestrings) will contribute their actual length + // Non-linear geometries (points, polygons) will contribute zero + self.geometries_ext() + .map(|g| g.euclidean_length_trait()) + .fold(T::zero(), |acc, next| acc + next) + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for G +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + if self.is_collection() { + self.geometries_ext() + .map(|g_inner| g_inner.borrow().euclidean_length_trait()) + .fold(T::zero(), |acc, next| acc + next) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(_) => T::zero(), + GeometryTypeExt::Line(line) => line.euclidean_length_trait(), + GeometryTypeExt::LineString(ls) => ls.euclidean_length_trait(), + GeometryTypeExt::Polygon(_) => T::zero(), + GeometryTypeExt::MultiPoint(_) => T::zero(), + GeometryTypeExt::MultiLineString(mls) => mls.euclidean_length_trait(), + GeometryTypeExt::MultiPolygon(_) => T::zero(), + GeometryTypeExt::Rect(_) => T::zero(), + GeometryTypeExt::Triangle(_) => T::zero(), + } + } + } +} + +#[cfg(test)] +mod test { + use crate::line_string; + #[allow(deprecated)] + use crate::EuclideanLength; + use crate::{coord, Line, MultiLineString}; + + #[allow(deprecated)] + #[test] + fn empty_linestring_test() { + let linestring = line_string![]; + assert_relative_eq!(0.0_f64, linestring.euclidean_length()); + } + #[allow(deprecated)] + #[test] + fn linestring_one_point_test() { + let linestring = line_string![(x: 0., y: 0.)]; + assert_relative_eq!(0.0_f64, linestring.euclidean_length()); + } + #[allow(deprecated)] + #[test] + fn linestring_test() { + let linestring = line_string![ + (x: 1., y: 1.), + (x: 7., y: 1.), + (x: 8., y: 1.), + (x: 9., y: 1.), + (x: 10., y: 1.), + (x: 11., y: 1.) + ]; + assert_relative_eq!(10.0_f64, linestring.euclidean_length()); + } + #[allow(deprecated)] + #[test] + fn multilinestring_test() { + let mline = MultiLineString::new(vec![ + line_string![ + (x: 1., y: 0.), + (x: 7., y: 0.), + (x: 8., y: 0.), + (x: 9., y: 0.), + (x: 10., y: 0.), + (x: 11., y: 0.) + ], + line_string![ + (x: 0., y: 0.), + (x: 0., y: 5.) + ], + ]); + assert_relative_eq!(15.0_f64, mline.euclidean_length()); + } + #[allow(deprecated)] + #[test] + fn line_test() { + let line0 = Line::new(coord! { x: 0., y: 0. }, coord! { x: 0., y: 1. }); + let line1 = Line::new(coord! { x: 0., y: 0. }, coord! { x: 3., y: 4. }); + assert_relative_eq!(line0.euclidean_length(), 1.); + assert_relative_eq!(line1.euclidean_length(), 5.); + } + + #[allow(deprecated)] + #[test] + fn polygon_returns_zero_test() { + use crate::{polygon, Polygon}; + let polygon: Polygon = polygon![ + (x: 0., y: 0.), + (x: 4., y: 0.), + (x: 4., y: 4.), + (x: 0., y: 4.), + (x: 0., y: 0.), + ]; + // Length doesn't apply to 2D polygons, should return zero + assert_relative_eq!(polygon.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn point_returns_zero_test() { + use crate::Point; + let point = Point::new(3.0, 4.0); + // Points have no length dimension + assert_relative_eq!(point.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn comprehensive_test_scenarios() { + use crate::{line_string, polygon}; + use crate::{ + Geometry, GeometryCollection, MultiLineString, MultiPoint, MultiPolygon, Point, + }; + + // Test cases matching the Python pytest scenarios + + // POINT EMPTY - represented as Point with NaN coordinates + // Note: In Rust we can't easily create "empty" points, so we test regular point + + // LINESTRING EMPTY + let empty_linestring: crate::LineString = line_string![]; + assert_relative_eq!(empty_linestring.euclidean_length(), 0.0); + + // POINT (0 0) + let point = Point::new(0.0, 0.0); + assert_relative_eq!(point.euclidean_length(), 0.0); + + // LINESTRING (0 0, 0 1) - length should be 1 + let linestring = line_string![(x: 0., y: 0.), (x: 0., y: 1.)]; + assert_relative_eq!(linestring.euclidean_length(), 1.0); + + // MULTIPOINT ((0 0), (1 1)) - should be 0 + let multipoint = MultiPoint::new(vec![Point::new(0.0, 0.0), Point::new(1.0, 1.0)]); + assert_relative_eq!(multipoint.euclidean_length(), 0.0); + + // MULTILINESTRING ((0 0, 1 1), (1 1, 2 2)) - should be ~2.828427 + // Distance from (0,0) to (1,1) = sqrt(2) ≈ 1.4142135623730951 + // Distance from (1,1) to (2,2) = sqrt(2) ≈ 1.4142135623730951 + // Total ≈ 2.8284271247461903 + let multilinestring = MultiLineString::new(vec![ + line_string![(x: 0., y: 0.), (x: 1., y: 1.)], + line_string![(x: 1., y: 1.), (x: 2., y: 2.)], + ]); + assert_relative_eq!( + multilinestring.euclidean_length(), + 2.8284271247461903, + epsilon = 1e-10 + ); + + // POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)) - should be 0 (perimeter not included) + let polygon = polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]; + assert_relative_eq!(polygon.euclidean_length(), 0.0); + + // MULTIPOLYGON - should be 0 + let multipolygon = MultiPolygon::new(vec![ + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + ]); + assert_relative_eq!(multipolygon.euclidean_length(), 0.0); + + // GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1), POLYGON (...), LINESTRING (0 0, 1 1)) + // Should sum only the linestrings: 2 * sqrt(2) ≈ 2.8284271247461903 + let collection = GeometryCollection::new_from(vec![ + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // sqrt(2) + Geometry::Polygon(polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]), // contributes 0 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // sqrt(2) + ]); + // Now correctly sums only the linear geometries: 2 * sqrt(2) ≈ 2.8284271247461903 + // The polygon contributes 0 to the total + assert_relative_eq!( + collection.euclidean_length(), + 2.8284271247461903, + epsilon = 1e-10 + ); + assert_relative_eq!( + Geometry::GeometryCollection(collection).euclidean_length(), + 2.8284271247461903, + epsilon = 1e-10 + ); + } + + // Individual test functions matching pytest parametrized scenarios + + #[allow(deprecated)] + #[test] + fn test_point_empty() { + use crate::Point; + // POINT EMPTY -> 0 (represented as empty coordinates or NaN in Rust context) + let point = Point::new(f64::NAN, f64::NAN); + // NaN coordinates still result in zero length for points + assert_relative_eq!(point.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn test_linestring_empty() { + // LINESTRING EMPTY -> 0 + let empty_linestring: crate::LineString = line_string![]; + assert_relative_eq!(empty_linestring.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn test_point_0_0() { + use crate::Point; + // POINT (0 0) -> 0 + let point = Point::new(0.0, 0.0); + assert_relative_eq!(point.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn test_linestring_0_0_to_0_1() { + // LINESTRING (0 0, 0 1) -> 1 + let linestring = line_string![(x: 0., y: 0.), (x: 0., y: 1.)]; + assert_relative_eq!(linestring.euclidean_length(), 1.0); + } + + #[allow(deprecated)] + #[test] + fn test_multipoint() { + // MULTIPOINT ((0 0), (1 1)) -> 0 + use crate::{MultiPoint, Point}; + let multipoint = MultiPoint::new(vec![Point::new(0.0, 0.0), Point::new(1.0, 1.0)]); + assert_relative_eq!(multipoint.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn test_multilinestring_diagonal() { + // MULTILINESTRING ((0 0, 1 1), (1 1, 2 2)) -> 2.8284271247461903 + use crate::MultiLineString; + let multilinestring = MultiLineString::new(vec![ + line_string![(x: 0., y: 0.), (x: 1., y: 1.)], // sqrt(2) + line_string![(x: 1., y: 1.), (x: 2., y: 2.)], // sqrt(2) + ]); + assert_relative_eq!( + multilinestring.euclidean_length(), + 2.8284271247461903, + epsilon = 1e-10 + ); + } + + #[allow(deprecated)] + #[test] + fn test_polygon_unit_square() { + // POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)) -> 0 (perimeters aren't included) + use crate::polygon; + let polygon = polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]; + assert_relative_eq!(polygon.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn test_multipolygon_double_unit_squares() { + // MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((0 0, 1 0, 1 1, 0 1, 0 0))) -> 0 + use crate::{polygon, MultiPolygon}; + let multipolygon = MultiPolygon::new(vec![ + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + ]); + assert_relative_eq!(multipolygon.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn test_geometrycollection_mixed() { + // GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1)) + // Expected: 2.8284271247461903 (only linestrings contribute) + use crate::{polygon, Geometry, GeometryCollection}; + let collection = GeometryCollection::new_from(vec![ + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // sqrt(2) ≈ 1.4142135623730951 + Geometry::Polygon(polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]), // contributes 0 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // sqrt(2) ≈ 1.4142135623730951 + ]); + // Now correctly returns the expected sum of only the linear geometries + // Expected: 2.8284271247461903 (sum of the two linestring lengths, polygon contributes 0) + assert_relative_eq!( + collection.euclidean_length(), + 2.8284271247461903, + epsilon = 1e-10 + ); + + // For now, let's test that individual geometries work correctly + let linestring1 = line_string![(x: 0., y: 0.), (x: 1., y: 1.)]; + let linestring2 = line_string![(x: 0., y: 0.), (x: 1., y: 1.)]; + let expected_total = linestring1.euclidean_length() + linestring2.euclidean_length(); + assert_relative_eq!(expected_total, 2.8284271247461903, epsilon = 1e-10); + } + + #[allow(deprecated)] + #[test] + fn test_geometrycollection_pytest_exact_scenario() { + // Exact match for the Python pytest scenario: + // GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1)) + // Expected: 2.8284271247461903 + use crate::{polygon, Geometry, GeometryCollection}; + + let collection = GeometryCollection::new_from(vec![ + // LINESTRING (0 0, 1 1) - length = sqrt(2) ≈ 1.4142135623730951 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), + // POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)) - contributes 0 (perimeter not included) + Geometry::Polygon(polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]), + // LINESTRING (0 0, 1 1) - length = sqrt(2) ≈ 1.4142135623730951 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), + ]); + + // Total length = sqrt(2) + 0 + sqrt(2) = 2 * sqrt(2) ≈ 2.8284271247461903 + assert_relative_eq!( + collection.euclidean_length(), + 2.8284271247461903, + epsilon = 1e-10 + ); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/extremes.rs b/rust/sedona-geo-generic-alg/src/algorithm/extremes.rs new file mode 100644 index 00000000..a16272de --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/extremes.rs @@ -0,0 +1,147 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use crate::CoordsIter; +use crate::{Coord, CoordNum}; + +/// Find the extreme coordinates and indices of a geometry. +/// +/// # Examples +/// +/// ``` +/// use sedona_geo_generic_alg::extremes::Extremes; +/// use sedona_geo_generic_alg::polygon; +/// +/// // a diamond shape +/// let polygon = polygon![ +/// (x: 1.0, y: 0.0), +/// (x: 2.0, y: 1.0), +/// (x: 1.0, y: 2.0), +/// (x: 0.0, y: 1.0), +/// (x: 1.0, y: 0.0), +/// ]; +/// +/// let extremes = polygon.extremes().unwrap(); +/// +/// assert_eq!(extremes.y_max.index, 2); +/// assert_eq!(extremes.y_max.coord.x, 1.); +/// assert_eq!(extremes.y_max.coord.y, 2.); +/// ``` +pub trait Extremes<'a, T: CoordNum> { + fn extremes(&'a self) -> Option>; +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Extreme { + pub index: usize, + pub coord: Coord, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Outcome { + pub x_min: Extreme, + pub y_min: Extreme, + pub x_max: Extreme, + pub y_max: Extreme, +} + +impl<'a, T, G> Extremes<'a, T> for G +where + G: CoordsIter, + T: CoordNum, +{ + fn extremes(&'a self) -> Option> { + let mut iter = self.exterior_coords_iter().enumerate(); + + let mut outcome = iter.next().map(|(index, coord)| Outcome { + x_min: Extreme { index, coord }, + y_min: Extreme { index, coord }, + x_max: Extreme { index, coord }, + y_max: Extreme { index, coord }, + })?; + + for (index, coord) in iter { + if coord.x < outcome.x_min.coord.x { + outcome.x_min = Extreme { coord, index }; + } + + if coord.y < outcome.y_min.coord.y { + outcome.y_min = Extreme { coord, index }; + } + + if coord.x > outcome.x_max.coord.x { + outcome.x_max = Extreme { coord, index }; + } + + if coord.y > outcome.y_max.coord.y { + outcome.y_max = Extreme { coord, index }; + } + } + + Some(outcome) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{coord, polygon, MultiPoint}; + + #[test] + fn polygon() { + // a diamond shape + let polygon = polygon![ + (x: 1.0, y: 0.0), + (x: 2.0, y: 1.0), + (x: 1.0, y: 2.0), + (x: 0.0, y: 1.0), + (x: 1.0, y: 0.0), + ]; + + let actual = polygon.extremes(); + + assert_eq!( + Some(Outcome { + x_min: Extreme { + index: 3, + coord: coord! { x: 0.0, y: 1.0 } + }, + y_min: Extreme { + index: 0, + coord: coord! { x: 1.0, y: 0.0 } + }, + x_max: Extreme { + index: 1, + coord: coord! { x: 2.0, y: 1.0 } + }, + y_max: Extreme { + index: 2, + coord: coord! { x: 1.0, y: 2.0 } + } + }), + actual + ); + } + + #[test] + fn empty() { + let multi_point: MultiPoint = MultiPoint::new(vec![]); + + let actual = multi_point.extremes(); + + assert!(actual.is_none()); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/collections.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/collections.rs new file mode 100644 index 00000000..da807285 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/collections.rs @@ -0,0 +1,160 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use core::borrow::Borrow; +use sedona_geo_traits_ext::*; + +use super::has_disjoint_bboxes; +use super::IntersectsTrait; +use crate::GeoNum; + +macro_rules! impl_intersects_geometry { + ($rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait for LHS + where + T: GeoNum, + LHS: GeometryTraitExt, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + if self.is_collection() { + self.geometries_ext() + .any(|lhs_inner| lhs_inner.borrow().intersects_trait(rhs)) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => g.intersects_trait(rhs), + GeometryTypeExt::Line(g) => g.intersects_trait(rhs), + GeometryTypeExt::LineString(g) => g.intersects_trait(rhs), + GeometryTypeExt::Polygon(g) => g.intersects_trait(rhs), + GeometryTypeExt::MultiPoint(g) => g.intersects_trait(rhs), + GeometryTypeExt::MultiLineString(g) => g.intersects_trait(rhs), + GeometryTypeExt::MultiPolygon(g) => g.intersects_trait(rhs), + GeometryTypeExt::Rect(g) => g.intersects_trait(rhs), + GeometryTypeExt::Triangle(g) => g.intersects_trait(rhs), + } + } + } + } + }; +} + +impl_intersects_geometry!(CoordTraitExt, CoordTag); +impl_intersects_geometry!(PointTraitExt, PointTag); +impl_intersects_geometry!(LineStringTraitExt, LineStringTag); +impl_intersects_geometry!(PolygonTraitExt, PolygonTag); +impl_intersects_geometry!(MultiPointTraitExt, MultiPointTag); +impl_intersects_geometry!(MultiLineStringTraitExt, MultiLineStringTag); +impl_intersects_geometry!(MultiPolygonTraitExt, MultiPolygonTag); +impl_intersects_geometry!(GeometryTraitExt, GeometryTag); +impl_intersects_geometry!(GeometryCollectionTraitExt, GeometryCollectionTag); +impl_intersects_geometry!(LineTraitExt, LineTag); +impl_intersects_geometry!(RectTraitExt, RectTag); +impl_intersects_geometry!(TriangleTraitExt, TriangleTag); + +symmetric_intersects_trait_impl!( + GeoNum, + CoordTraitExt, + CoordTag, + GeometryTraitExt, + GeometryTag +); +symmetric_intersects_trait_impl!(GeoNum, LineTraitExt, LineTag, GeometryTraitExt, GeometryTag); +symmetric_intersects_trait_impl!(GeoNum, RectTraitExt, RectTag, GeometryTraitExt, GeometryTag); +symmetric_intersects_trait_impl!( + GeoNum, + TriangleTraitExt, + TriangleTag, + GeometryTraitExt, + GeometryTag +); +symmetric_intersects_trait_impl!( + GeoNum, + PolygonTraitExt, + PolygonTag, + GeometryTraitExt, + GeometryTag +); + +// Generate implementations for GeometryCollection by delegating to the Geometry implementation +macro_rules! impl_intersects_geometry_collection_from_geometry { + ($rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait for LHS + where + T: GeoNum, + LHS: GeometryCollectionTraitExt, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + if has_disjoint_bboxes(self, rhs) { + return false; + } + self.geometries_ext().any(|geom| geom.intersects_trait(rhs)) + } + } + }; +} + +impl_intersects_geometry_collection_from_geometry!(CoordTraitExt, CoordTag); +impl_intersects_geometry_collection_from_geometry!(PointTraitExt, PointTag); +impl_intersects_geometry_collection_from_geometry!(LineStringTraitExt, LineStringTag); +impl_intersects_geometry_collection_from_geometry!(PolygonTraitExt, PolygonTag); +impl_intersects_geometry_collection_from_geometry!(MultiPointTraitExt, MultiPointTag); +impl_intersects_geometry_collection_from_geometry!(MultiLineStringTraitExt, MultiLineStringTag); +impl_intersects_geometry_collection_from_geometry!(MultiPolygonTraitExt, MultiPolygonTag); +impl_intersects_geometry_collection_from_geometry!(GeometryTraitExt, GeometryTag); +impl_intersects_geometry_collection_from_geometry!( + GeometryCollectionTraitExt, + GeometryCollectionTag +); +impl_intersects_geometry_collection_from_geometry!(LineTraitExt, LineTag); +impl_intersects_geometry_collection_from_geometry!(RectTraitExt, RectTag); +impl_intersects_geometry_collection_from_geometry!(TriangleTraitExt, TriangleTag); + +symmetric_intersects_trait_impl!( + GeoNum, + CoordTraitExt, + CoordTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_intersects_trait_impl!( + GeoNum, + LineTraitExt, + LineTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_intersects_trait_impl!( + GeoNum, + RectTraitExt, + RectTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_intersects_trait_impl!( + GeoNum, + TriangleTraitExt, + TriangleTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_intersects_trait_impl!( + GeoNum, + PolygonTraitExt, + PolygonTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/coordinate.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/coordinate.rs new file mode 100644 index 00000000..29931ba3 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/coordinate.rs @@ -0,0 +1,43 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use sedona_geo_traits_ext::{CoordTag, CoordTraitExt, PointTag, PointTraitExt}; + +use super::IntersectsTrait; +use crate::*; + +impl IntersectsTrait for LHS +where + T: CoordNum, + LHS: CoordTraitExt, + RHS: CoordTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + self.geo_coord() == rhs.geo_coord() + } +} + +// The other side of this is handled via a blanket impl. +impl IntersectsTrait for LHS +where + T: CoordNum, + LHS: CoordTraitExt, + RHS: PointTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + rhs.geo_coord().is_some_and(|c| self.geo_coord() == c) + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/line.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/line.rs new file mode 100644 index 00000000..96f8762d --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/line.rs @@ -0,0 +1,115 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use sedona_geo_traits_ext::*; + +use super::{point_in_rect, IntersectsTrait}; +use crate::*; + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: LineTraitExt, + RHS: CoordTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + let start = self.start_coord(); + let end = self.end_coord(); + let rhs = rhs.geo_coord(); + + // First we check if the point is collinear with the line. + T::Ker::orient2d(start, end, rhs) == Orientation::Collinear + // In addition, the point must have _both_ coordinates + // within the start and end bounds. + && point_in_rect(rhs, start, end) + } +} + +symmetric_intersects_trait_impl!(GeoNum, CoordTraitExt, CoordTag, LineTraitExt, LineTag); +symmetric_intersects_trait_impl!(GeoNum, LineTraitExt, LineTag, PointTraitExt, PointTag); + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: LineTraitExt, + RHS: LineTraitExt, +{ + fn intersects_trait(&self, line: &RHS) -> bool { + let start_ext = self.start_ext(); + let self_start = self.start_coord(); + let self_end = self.end_coord(); + let line_start = line.start_coord(); + let line_end = line.end_coord(); + + // Special case: self is equiv. to a point. + if self_start == self_end { + return line.intersects_trait(&start_ext); + } + + // Precondition: start and end are distinct. + + // Check if orientation of rhs.{start,end} are different + // with respect to self.{start,end}. + let check_1_1 = T::Ker::orient2d(self_start, self_end, line_start); + let check_1_2 = T::Ker::orient2d(self_start, self_end, line_end); + + if check_1_1 != check_1_2 { + // Since the checks are different, + // rhs.{start,end} are distinct, and rhs is not + // collinear with self. Thus, there is exactly + // one point on the infinite extensions of rhs, + // that is collinear with self. + + // By continuity, this point is not on the + // exterior of rhs. Now, check the same with + // self, rhs swapped. + + let check_2_1 = T::Ker::orient2d(line_start, line_end, self_start); + let check_2_2 = T::Ker::orient2d(line_start, line_end, self_end); + + // By similar argument, there is (exactly) one + // point on self, collinear with rhs. Thus, + // those two have to be same, and lies (interior + // or boundary, but not exterior) on both lines. + check_2_1 != check_2_2 + } else if check_1_1 == Orientation::Collinear { + // Special case: collinear line segments. + + // Equivalent to 4 point-line intersection + // checks, but removes the calls to the kernel + // predicates. + point_in_rect(line_start, self_start, self_end) + || point_in_rect(line_end, self_start, self_end) + || point_in_rect(self_end, line_start, line_end) + || point_in_rect(self_end, line_start, line_end) + } else { + false + } + } +} + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: LineTraitExt, + RHS: TriangleTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + self.intersects_trait(&rhs.to_polygon()) + } +} + +symmetric_intersects_trait_impl!(GeoNum, TriangleTraitExt, TriangleTag, LineTraitExt, LineTag); diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/line_string.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/line_string.rs new file mode 100644 index 00000000..d291d86d --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/line_string.rs @@ -0,0 +1,145 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use sedona_geo_traits_ext::*; + +use super::{has_disjoint_bboxes, IntersectsTrait}; +use crate::*; + +// Generate implementations for LineString by delegating to Line +macro_rules! impl_intersects_line_string_from_line { + ($rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait for LHS + where + T: GeoNum, + LHS: LineStringTraitExt, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + if has_disjoint_bboxes(self, rhs) { + return false; + } + self.lines().any(|l| l.intersects_trait(rhs)) + } + } + }; +} + +impl_intersects_line_string_from_line!(CoordTraitExt, CoordTag); +impl_intersects_line_string_from_line!(PointTraitExt, PointTag); +impl_intersects_line_string_from_line!(LineStringTraitExt, LineStringTag); +impl_intersects_line_string_from_line!(PolygonTraitExt, PolygonTag); +impl_intersects_line_string_from_line!(MultiPointTraitExt, MultiPointTag); +impl_intersects_line_string_from_line!(MultiLineStringTraitExt, MultiLineStringTag); +impl_intersects_line_string_from_line!(MultiPolygonTraitExt, MultiPolygonTag); +impl_intersects_line_string_from_line!(GeometryTraitExt, GeometryTag); +impl_intersects_line_string_from_line!(GeometryCollectionTraitExt, GeometryCollectionTag); +impl_intersects_line_string_from_line!(LineTraitExt, LineTag); +impl_intersects_line_string_from_line!(RectTraitExt, RectTag); +impl_intersects_line_string_from_line!(TriangleTraitExt, TriangleTag); + +symmetric_intersects_trait_impl!( + GeoNum, + CoordTraitExt, + CoordTag, + LineStringTraitExt, + LineStringTag +); +symmetric_intersects_trait_impl!( + GeoNum, + LineTraitExt, + LineTag, + LineStringTraitExt, + LineStringTag +); +symmetric_intersects_trait_impl!( + GeoNum, + RectTraitExt, + RectTag, + LineStringTraitExt, + LineStringTag +); +symmetric_intersects_trait_impl!( + GeoNum, + TriangleTraitExt, + TriangleTag, + LineStringTraitExt, + LineStringTag +); + +// Generate implementations for MultiLineString by delegating to LineString +macro_rules! impl_intersects_multi_line_string_from_line_string { + ($rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait for LHS + where + T: GeoNum, + LHS: MultiLineStringTraitExt, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + if has_disjoint_bboxes(self, rhs) { + return false; + } + self.line_strings_ext().any(|ls| ls.intersects_trait(rhs)) + } + } + }; +} + +impl_intersects_multi_line_string_from_line_string!(CoordTraitExt, CoordTag); +impl_intersects_multi_line_string_from_line_string!(PointTraitExt, PointTag); +impl_intersects_multi_line_string_from_line_string!(LineStringTraitExt, LineStringTag); +impl_intersects_multi_line_string_from_line_string!(PolygonTraitExt, PolygonTag); +impl_intersects_multi_line_string_from_line_string!(MultiPointTraitExt, MultiPointTag); +impl_intersects_multi_line_string_from_line_string!(MultiLineStringTraitExt, MultiLineStringTag); +impl_intersects_multi_line_string_from_line_string!(MultiPolygonTraitExt, MultiPolygonTag); +impl_intersects_multi_line_string_from_line_string!(GeometryTraitExt, GeometryTag); +impl_intersects_multi_line_string_from_line_string!( + GeometryCollectionTraitExt, + GeometryCollectionTag +); +impl_intersects_multi_line_string_from_line_string!(LineTraitExt, LineTag); +impl_intersects_multi_line_string_from_line_string!(RectTraitExt, RectTag); +impl_intersects_multi_line_string_from_line_string!(TriangleTraitExt, TriangleTag); + +symmetric_intersects_trait_impl!( + GeoNum, + CoordTraitExt, + CoordTag, + MultiLineStringTraitExt, + MultiLineStringTag +); +symmetric_intersects_trait_impl!( + GeoNum, + LineTraitExt, + LineTag, + MultiLineStringTraitExt, + MultiLineStringTag +); +symmetric_intersects_trait_impl!( + GeoNum, + RectTraitExt, + RectTag, + MultiLineStringTraitExt, + MultiLineStringTag +); +symmetric_intersects_trait_impl!( + GeoNum, + TriangleTraitExt, + TriangleTag, + MultiLineStringTraitExt, + MultiLineStringTag +); diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/mod.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/mod.rs new file mode 100644 index 00000000..aa5c41b4 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/mod.rs @@ -0,0 +1,750 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use sedona_geo_traits_ext::GeoTraitExtWithTypeTag; + +use crate::BoundingRect; +use crate::*; + +/// Checks if the geometry Self intersects the geometry Rhs. +/// More formally, either boundary or interior of Self has +/// non-empty (set-theoretic) intersection with the boundary +/// or interior of Rhs. In other words, the [DE-9IM] +/// intersection matrix for (Self, Rhs) is _not_ `FF*FF****`. +/// +/// This predicate is symmetric: `a.intersects(b)` iff +/// `b.intersects(a)`. +/// +/// [DE-9IM]: https://en.wikipedia.org/wiki/DE-9IM +/// +/// # Examples +/// +/// ``` +/// use sedona_geo_generic_alg::Intersects; +/// use sedona_geo_generic_alg::line_string; +/// +/// let line_string_a = line_string![ +/// (x: 3., y: 2.), +/// (x: 7., y: 6.), +/// ]; +/// +/// let line_string_b = line_string![ +/// (x: 3., y: 4.), +/// (x: 8., y: 4.), +/// ]; +/// +/// let line_string_c = line_string![ +/// (x: 9., y: 2.), +/// (x: 11., y: 5.), +/// ]; +/// +/// assert!(line_string_a.intersects(&line_string_b)); +/// assert!(!line_string_a.intersects(&line_string_c)); +/// ``` +pub trait Intersects { + fn intersects(&self, rhs: &Rhs) -> bool; +} + +pub trait IntersectsTrait { + fn intersects_trait(&self, rhs: &Rhs) -> bool; +} + +impl Intersects for LHS +where + LHS: GeoTraitExtWithTypeTag, + RHS: GeoTraitExtWithTypeTag, + LHS: IntersectsTrait, +{ + fn intersects(&self, rhs: &RHS) -> bool { + self.intersects_trait(rhs) + } +} + +macro_rules! symmetric_intersects_trait_impl { + ($num_type:ident, $lhs_type:ident, $lhs_tag:ident, $rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait<$lhs_tag, $rhs_tag, RHS> for LHS + where + T: $num_type, + LHS: $lhs_type, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + rhs.intersects_trait(self) + } + } + }; +} + +mod collections; +mod coordinate; +mod line; +mod line_string; +mod point; +mod polygon; +mod rect; +mod triangle; + +// Helper function to check value lies between min and max. +// Only makes sense if min <= max (or always false) +#[inline] +fn value_in_range(value: T, min: T, max: T) -> bool +where + T: std::cmp::PartialOrd, +{ + value >= min && value <= max +} + +// Helper function to check value lies between two bounds, +// where the ordering of the bounds is not known +#[inline] +pub(crate) fn value_in_between(value: T, bound_1: T, bound_2: T) -> bool +where + T: std::cmp::PartialOrd, +{ + if bound_1 < bound_2 { + value_in_range(value, bound_1, bound_2) + } else { + value_in_range(value, bound_2, bound_1) + } +} + +// Helper function to check point lies inside rect given by +// bounds. The first bound need not be min. +#[inline] +pub(crate) fn point_in_rect(value: Coord, bound_1: Coord, bound_2: Coord) -> bool +where + T: CoordNum, +{ + value_in_between(value.x, bound_1.x, bound_2.x) + && value_in_between(value.y, bound_1.y, bound_2.y) +} + +// A cheap bbox check to see if we can skip the more expensive intersection computation +fn has_disjoint_bboxes(a: &A, b: &B) -> bool +where + T: CoordNum, + A: BoundingRect, + B: BoundingRect, +{ + let mut disjoint_bbox = false; + if let Some(a_bbox) = a.bounding_rect().into() { + if let Some(b_bbox) = b.bounding_rect().into() { + if !a_bbox.intersects(&b_bbox) { + disjoint_bbox = true; + } + } + } + disjoint_bbox +} + +#[cfg(test)] +mod test { + use geo_types::{Coord, GeometryCollection}; + + use crate::Intersects; + use crate::{ + coord, line_string, polygon, Geometry, Line, LineString, MultiLineString, MultiPoint, + MultiPolygon, Point, Polygon, Rect, + }; + + /// Tests: intersection LineString and LineString + #[test] + fn empty_linestring1_test() { + let linestring = line_string![(x: 3., y: 2.), (x: 7., y: 6.)]; + assert!(!line_string![].intersects(&linestring)); + } + #[test] + fn empty_linestring2_test() { + let linestring = line_string![(x: 3., y: 2.), (x: 7., y: 6.)]; + assert!(!linestring.intersects(&LineString::new(Vec::new()))); + } + #[test] + fn empty_all_linestring_test() { + let empty: LineString = line_string![]; + assert!(!empty.intersects(&empty)); + } + #[test] + fn intersect_linestring_test() { + let ls1 = line_string![(x: 3., y: 2.), (x: 7., y: 6.)]; + let ls2 = line_string![(x: 3., y: 4.), (x: 8., y: 4.)]; + assert!(ls1.intersects(&ls2)); + } + #[test] + fn parallel_linestrings_test() { + let ls1 = line_string![(x: 3., y: 2.), (x: 7., y: 6.)]; + let ls2 = line_string![(x: 3., y: 1.), (x: 7., y: 5.)]; + assert!(!ls1.intersects(&ls2)); + } + /// Tests: intersection LineString and Polygon + #[test] + fn linestring_in_polygon_test() { + let poly = polygon![ + (x: 0., y: 0.), + (x: 5., y: 0.), + (x: 5., y: 6.), + (x: 0., y: 6.), + (x: 0., y: 0.), + ]; + let ls = line_string![(x: 2., y: 2.), (x: 3., y: 3.)]; + assert!(poly.intersects(&ls)); + } + #[test] + fn linestring_on_boundary_polygon_test() { + let poly = Polygon::new( + LineString::from(vec![(0., 0.), (5., 0.), (5., 6.), (0., 6.), (0., 0.)]), + Vec::new(), + ); + assert!(poly.intersects(&LineString::from(vec![(0., 0.), (5., 0.)]))); + assert!(poly.intersects(&LineString::from(vec![(5., 0.), (5., 6.)]))); + assert!(poly.intersects(&LineString::from(vec![(5., 6.), (0., 6.)]))); + assert!(poly.intersects(&LineString::from(vec![(0., 6.), (0., 0.)]))); + } + #[test] + fn intersect_linestring_polygon_test() { + let poly = Polygon::new( + LineString::from(vec![(0., 0.), (5., 0.), (5., 6.), (0., 6.), (0., 0.)]), + Vec::new(), + ); + assert!(poly.intersects(&LineString::from(vec![(2., 2.), (6., 6.)]))); + } + #[test] + fn linestring_outside_polygon_test() { + let poly = Polygon::new( + LineString::from(vec![(0., 0.), (5., 0.), (5., 6.), (0., 6.), (0., 0.)]), + Vec::new(), + ); + assert!(!poly.intersects(&LineString::from(vec![(7., 2.), (9., 4.)]))); + } + #[test] + fn linestring_in_inner_polygon_test() { + let e = LineString::from(vec![(0., 0.), (5., 0.), (5., 6.), (0., 6.), (0., 0.)]); + let v = vec![LineString::from(vec![ + (1., 1.), + (4., 1.), + (4., 4.), + (1., 4.), + (1., 1.), + ])]; + let poly = Polygon::new(e, v); + assert!(!poly.intersects(&LineString::from(vec![(2., 2.), (3., 3.)]))); + assert!(poly.intersects(&LineString::from(vec![(2., 2.), (4., 4.)]))); + } + #[test] + fn linestring_traverse_polygon_test() { + let e = LineString::from(vec![(0., 0.), (5., 0.), (5., 6.), (0., 6.), (0., 0.)]); + let v = vec![LineString::from(vec![ + (1., 1.), + (4., 1.), + (4., 4.), + (1., 4.), + (1., 1.), + ])]; + let poly = Polygon::new(e, v); + assert!(poly.intersects(&LineString::from(vec![(2., 0.5), (2., 5.)]))); + } + #[test] + fn linestring_in_inner_with_2_inner_polygon_test() { + // (8,9) + // (2,8) | (14,8) + // ------------------------------------|------------------------------------------ + // | | | + // | (4,7) (6,7) | | + // | ------------------ | (11,7) | + // | | | | + // | (4,6) (7,6) | (9,6) | (12,6) | + // | ---------------------- | ----------------|--------- | + // | | | | | | | | + // | | (6,5) | | | | | | + // | | / | | | | | | + // | | / | | | | | | + // | | (5,4) | | | | | | + // | | | | | | | | + // | ---------------------- | ----------------|--------- | + // | (4,3) (7,3) | (9,3) | (12,3) | + // | | (11,2.5) | + // | | | + // ------------------------------------|------------------------------------------ + // (2,2) | (14,2) + // (8,1) + // + let e = LineString::from(vec![(2., 2.), (14., 2.), (14., 8.), (2., 8.), (2., 2.)]); + let v = vec![ + LineString::from(vec![(4., 3.), (7., 3.), (7., 6.), (4., 6.), (4., 3.)]), + LineString::from(vec![(9., 3.), (12., 3.), (12., 6.), (9., 6.), (9., 3.)]), + ]; + let poly = Polygon::new(e, v); + assert!(!poly.intersects(&LineString::from(vec![(5., 4.), (6., 5.)]))); + assert!(poly.intersects(&LineString::from(vec![(11., 2.5), (11., 7.)]))); + assert!(poly.intersects(&LineString::from(vec![(4., 7.), (6., 7.)]))); + assert!(poly.intersects(&LineString::from(vec![(8., 1.), (8., 9.)]))); + } + #[test] + fn polygons_do_not_intersect() { + let p1 = Polygon::new( + LineString::from(vec![(1., 3.), (3., 3.), (3., 5.), (1., 5.), (1., 3.)]), + Vec::new(), + ); + let p2 = Polygon::new( + LineString::from(vec![ + (10., 30.), + (30., 30.), + (30., 50.), + (10., 50.), + (10., 30.), + ]), + Vec::new(), + ); + + assert!(!p1.intersects(&p2)); + assert!(!p2.intersects(&p1)); + } + #[test] + fn polygons_overlap() { + let p1 = Polygon::new( + LineString::from(vec![(1., 3.), (3., 3.), (3., 5.), (1., 5.), (1., 3.)]), + Vec::new(), + ); + let p2 = Polygon::new( + LineString::from(vec![(2., 3.), (4., 3.), (4., 7.), (2., 7.), (2., 3.)]), + Vec::new(), + ); + + assert!(p1.intersects(&p2)); + assert!(p2.intersects(&p1)); + } + #[test] + fn polygon_contained() { + let p1 = Polygon::new( + LineString::from(vec![(1., 3.), (4., 3.), (4., 6.), (1., 6.), (1., 3.)]), + Vec::new(), + ); + let p2 = Polygon::new( + LineString::from(vec![(2., 4.), (3., 4.), (3., 5.), (2., 5.), (2., 4.)]), + Vec::new(), + ); + + assert!(p1.intersects(&p2)); + assert!(p2.intersects(&p1)); + } + #[test] + fn polygons_conincident() { + let p1 = Polygon::new( + LineString::from(vec![(1., 3.), (4., 3.), (4., 6.), (1., 6.), (1., 3.)]), + Vec::new(), + ); + let p2 = Polygon::new( + LineString::from(vec![(1., 3.), (4., 3.), (4., 6.), (1., 6.), (1., 3.)]), + Vec::new(), + ); + + assert!(p1.intersects(&p2)); + assert!(p2.intersects(&p1)); + } + #[test] + fn polygon_intersects_bounding_rect_test() { + // Polygon poly = + // + // (0,8) (12,8) + // ┌──────────────────────┐ + // │ (7,7) (11,7) │ + // │ ┌──────┐ │ + // │ │ │ │ + // │ │(hole)│ │ + // │ │ │ │ + // │ │ │ │ + // │ └──────┘ │ + // │ (7,4) (11,4) │ + // │ │ + // │ │ + // │ │ + // │ │ + // │ │ + // └──────────────────────┘ + // (0,0) (12,0) + let poly = Polygon::new( + LineString::from(vec![(0., 0.), (12., 0.), (12., 8.), (0., 8.), (0., 0.)]), + vec![LineString::from(vec![ + (7., 4.), + (11., 4.), + (11., 7.), + (7., 7.), + (7., 4.), + ])], + ); + let b1 = Rect::new(coord! { x: 11., y: 1. }, coord! { x: 13., y: 2. }); + let b2 = Rect::new(coord! { x: 2., y: 2. }, coord! { x: 8., y: 5. }); + let b3 = Rect::new(coord! { x: 8., y: 5. }, coord! { x: 10., y: 6. }); + let b4 = Rect::new(coord! { x: 1., y: 1. }, coord! { x: 3., y: 3. }); + // overlaps + assert!(poly.intersects(&b1)); + // contained in exterior, overlaps with hole + assert!(poly.intersects(&b2)); + // completely contained in the hole + assert!(!poly.intersects(&b3)); + // completely contained in the polygon + assert!(poly.intersects(&b4)); + // conversely, + assert!(b1.intersects(&poly)); + assert!(b2.intersects(&poly)); + assert!(!b3.intersects(&poly)); + assert!(b4.intersects(&poly)); + } + #[test] + fn bounding_rect_test() { + let bounding_rect_xl = + Rect::new(coord! { x: -100., y: -200. }, coord! { x: 100., y: 200. }); + let bounding_rect_sm = Rect::new(coord! { x: -10., y: -20. }, coord! { x: 10., y: 20. }); + let bounding_rect_s2 = Rect::new(coord! { x: 0., y: 0. }, coord! { x: 20., y: 30. }); + // confirmed using GEOS + assert!(bounding_rect_xl.intersects(&bounding_rect_sm)); + assert!(bounding_rect_sm.intersects(&bounding_rect_xl)); + assert!(bounding_rect_sm.intersects(&bounding_rect_s2)); + assert!(bounding_rect_s2.intersects(&bounding_rect_sm)); + } + #[test] + fn rect_intersection_consistent_with_poly_intersection_test() { + let bounding_rect_xl = + Rect::new(coord! { x: -100., y: -200. }, coord! { x: 100., y: 200. }); + let bounding_rect_sm = Rect::new(coord! { x: -10., y: -20. }, coord! { x: 10., y: 20. }); + let bounding_rect_s2 = Rect::new(coord! { x: 0., y: 0. }, coord! { x: 20., y: 30. }); + + assert!(bounding_rect_xl.to_polygon().intersects(&bounding_rect_sm)); + assert!(bounding_rect_xl.intersects(&bounding_rect_sm.to_polygon())); + assert!(bounding_rect_xl + .to_polygon() + .intersects(&bounding_rect_sm.to_polygon())); + + assert!(bounding_rect_sm.to_polygon().intersects(&bounding_rect_xl)); + assert!(bounding_rect_sm.intersects(&bounding_rect_xl.to_polygon())); + assert!(bounding_rect_sm + .to_polygon() + .intersects(&bounding_rect_xl.to_polygon())); + + assert!(bounding_rect_sm.to_polygon().intersects(&bounding_rect_s2)); + assert!(bounding_rect_sm.intersects(&bounding_rect_s2.to_polygon())); + assert!(bounding_rect_sm + .to_polygon() + .intersects(&bounding_rect_s2.to_polygon())); + + assert!(bounding_rect_s2.to_polygon().intersects(&bounding_rect_sm)); + assert!(bounding_rect_s2.intersects(&bounding_rect_sm.to_polygon())); + assert!(bounding_rect_s2 + .to_polygon() + .intersects(&bounding_rect_sm.to_polygon())); + } + #[test] + fn point_intersects_line_test() { + let p0 = Point::new(2., 4.); + // vertical line + let line1 = Line::from([(2., 0.), (2., 5.)]); + // point on line, but outside line segment + let line2 = Line::from([(0., 6.), (1.5, 4.5)]); + // point on line + let line3 = Line::from([(0., 6.), (3., 3.)]); + // point above line with positive slope + let line4 = Line::from([(1., 2.), (5., 3.)]); + // point below line with positive slope + let line5 = Line::from([(1., 5.), (5., 6.)]); + // point above line with negative slope + let line6 = Line::from([(1., 2.), (5., -3.)]); + // point below line with negative slope + let line7 = Line::from([(1., 6.), (5., 5.)]); + assert!(line1.intersects(&p0)); + assert!(p0.intersects(&line1)); + assert!(!line2.intersects(&p0)); + assert!(!p0.intersects(&line2)); + assert!(line3.intersects(&p0)); + assert!(p0.intersects(&line3)); + assert!(!line4.intersects(&p0)); + assert!(!p0.intersects(&line4)); + assert!(!line5.intersects(&p0)); + assert!(!p0.intersects(&line5)); + assert!(!line6.intersects(&p0)); + assert!(!p0.intersects(&line6)); + assert!(!line7.intersects(&p0)); + assert!(!p0.intersects(&line7)); + } + #[test] + fn line_intersects_line_test() { + let line0 = Line::from([(0., 0.), (3., 4.)]); + let line1 = Line::from([(2., 0.), (2., 5.)]); + let line2 = Line::from([(0., 7.), (5., 4.)]); + let line3 = Line::from([(0., 0.), (-3., -4.)]); + assert!(line0.intersects(&line0)); + assert!(line0.intersects(&line1)); + assert!(!line0.intersects(&line2)); + assert!(line0.intersects(&line3)); + + assert!(line1.intersects(&line0)); + assert!(line1.intersects(&line1)); + assert!(!line1.intersects(&line2)); + assert!(!line1.intersects(&line3)); + + assert!(!line2.intersects(&line0)); + assert!(!line2.intersects(&line1)); + assert!(line2.intersects(&line2)); + assert!(!line1.intersects(&line3)); + } + #[test] + fn line_intersects_linestring_test() { + let line0 = Line::from([(0., 0.), (3., 4.)]); + let linestring0 = LineString::from(vec![(0., 1.), (1., 0.), (2., 0.)]); + let linestring1 = LineString::from(vec![(0.5, 0.2), (1., 0.), (2., 0.)]); + assert!(line0.intersects(&linestring0)); + assert!(!line0.intersects(&linestring1)); + assert!(linestring0.intersects(&line0)); + assert!(!linestring1.intersects(&line0)); + } + #[test] + fn line_intersects_polygon_test() { + let line0 = Line::from([(0.5, 0.5), (2., 1.)]); + let poly0 = Polygon::new( + LineString::from(vec![(0., 0.), (1., 2.), (1., 0.), (0., 0.)]), + vec![], + ); + let poly1 = Polygon::new( + LineString::from(vec![(1., -1.), (2., -1.), (2., -2.), (1., -1.)]), + vec![], + ); + // line contained in the hole + let poly2 = Polygon::new( + LineString::from(vec![(-1., -1.), (-1., 10.), (10., -1.), (-1., -1.)]), + vec![LineString::from(vec![ + (0., 0.), + (3., 4.), + (3., 0.), + (0., 0.), + ])], + ); + assert!(line0.intersects(&poly0)); + assert!(poly0.intersects(&line0)); + + assert!(!line0.intersects(&poly1)); + assert!(!poly1.intersects(&line0)); + + assert!(!line0.intersects(&poly2)); + assert!(!poly2.intersects(&line0)); + } + #[test] + // See https://github.com/georust/geo/issues/419 + fn rect_test_419() { + let a = Rect::new( + coord! { + x: 9.228515625, + y: 46.83013364044739, + }, + coord! { + x: 9.2724609375, + y: 46.86019101567026, + }, + ); + let b = Rect::new( + coord! { + x: 9.17953, + y: 46.82018, + }, + coord! { + x: 9.26309, + y: 46.88099, + }, + ); + assert!(a.intersects(&b)); + assert!(b.intersects(&a)); + } + + #[test] + fn test_geom_collection_collection() { + let collection0 = Geometry::GeometryCollection(GeometryCollection::new_from(vec![ + Geometry::Point(Point::new(0., 0.)), + Geometry::Point(Point::new(1., 1.)), + ])); + let collection1 = Geometry::GeometryCollection(GeometryCollection::new_from(vec![ + Geometry::Point(Point::new(0., 0.)), + Geometry::Point(Point::new(2., 2.)), + ])); + let collection2 = Geometry::GeometryCollection(GeometryCollection::new_from(vec![ + Geometry::Point(Point::new(3., 3.)), + Geometry::Point(Point::new(4., 4.)), + ])); + assert!(collection0.intersects(&collection1)); + assert!(collection1.intersects(&collection0)); + assert!(!collection0.intersects(&collection2)); + assert!(!collection2.intersects(&collection0)); + } + + #[test] + fn compile_test_geom_geom() { + // This test should check existence of all + // combinations of geometry types. + let geom: Geometry<_> = Line::from([(0.5, 0.5), (2., 1.)]).into(); + assert!(geom.intersects(&geom)); + } + + #[test] + fn exhaustive_compile_test() { + use geo_types::{GeometryCollection, Triangle}; + let c: Coord = coord! { x: 0., y: 0. }; + let pt: Point = Point::new(0., 0.); + let ln: Line = Line::new((0., 0.), (1., 1.)); + let ls = line_string![(0., 0.).into(), (1., 1.).into()]; + let poly = Polygon::new(LineString::from(vec![(0., 0.), (1., 1.), (1., 0.)]), vec![]); + let rect = Rect::new(coord! { x: 10., y: 20. }, coord! { x: 30., y: 10. }); + let tri = Triangle::new( + coord! { x: 0., y: 0. }, + coord! { x: 10., y: 20. }, + coord! { x: 20., y: -10. }, + ); + let geom = Geometry::Point(pt); + let gc = GeometryCollection::new_from(vec![geom.clone()]); + let multi_point = MultiPoint::new(vec![pt]); + let multi_ls = MultiLineString::new(vec![ls.clone()]); + let multi_poly = MultiPolygon::new(vec![poly.clone()]); + + let _ = c.intersects(&pt); + let _ = c.intersects(&ln); + let _ = c.intersects(&ls); + let _ = c.intersects(&poly); + let _ = c.intersects(&rect); + let _ = c.intersects(&tri); + let _ = c.intersects(&geom); + let _ = c.intersects(&gc); + let _ = c.intersects(&multi_point); + let _ = c.intersects(&multi_ls); + let _ = c.intersects(&multi_poly); + + let _ = pt.intersects(&pt); + let _ = pt.intersects(&ln); + let _ = pt.intersects(&ls); + let _ = pt.intersects(&poly); + let _ = pt.intersects(&rect); + let _ = pt.intersects(&tri); + let _ = pt.intersects(&geom); + let _ = pt.intersects(&gc); + let _ = pt.intersects(&multi_point); + let _ = pt.intersects(&multi_ls); + let _ = pt.intersects(&multi_poly); + let _ = ln.intersects(&pt); + let _ = ln.intersects(&ln); + let _ = ln.intersects(&ls); + let _ = ln.intersects(&poly); + let _ = ln.intersects(&rect); + let _ = ln.intersects(&tri); + let _ = ln.intersects(&geom); + let _ = ln.intersects(&gc); + let _ = ln.intersects(&multi_point); + let _ = ln.intersects(&multi_ls); + let _ = ln.intersects(&multi_poly); + let _ = ls.intersects(&pt); + let _ = ls.intersects(&ln); + let _ = ls.intersects(&ls); + let _ = ls.intersects(&poly); + let _ = ls.intersects(&rect); + let _ = ls.intersects(&tri); + let _ = ls.intersects(&geom); + let _ = ls.intersects(&gc); + let _ = ls.intersects(&multi_point); + let _ = ls.intersects(&multi_ls); + let _ = ls.intersects(&multi_poly); + let _ = poly.intersects(&pt); + let _ = poly.intersects(&ln); + let _ = poly.intersects(&ls); + let _ = poly.intersects(&poly); + let _ = poly.intersects(&rect); + let _ = poly.intersects(&tri); + let _ = poly.intersects(&geom); + let _ = poly.intersects(&gc); + let _ = poly.intersects(&multi_point); + let _ = poly.intersects(&multi_ls); + let _ = poly.intersects(&multi_poly); + let _ = rect.intersects(&pt); + let _ = rect.intersects(&ln); + let _ = rect.intersects(&ls); + let _ = rect.intersects(&poly); + let _ = rect.intersects(&rect); + let _ = rect.intersects(&tri); + let _ = rect.intersects(&geom); + let _ = rect.intersects(&gc); + let _ = rect.intersects(&multi_point); + let _ = rect.intersects(&multi_ls); + let _ = rect.intersects(&multi_poly); + let _ = tri.intersects(&pt); + let _ = tri.intersects(&ln); + let _ = tri.intersects(&ls); + let _ = tri.intersects(&poly); + let _ = tri.intersects(&rect); + let _ = tri.intersects(&tri); + let _ = tri.intersects(&geom); + let _ = tri.intersects(&gc); + let _ = tri.intersects(&multi_point); + let _ = tri.intersects(&multi_ls); + let _ = tri.intersects(&multi_poly); + let _ = geom.intersects(&pt); + let _ = geom.intersects(&ln); + let _ = geom.intersects(&ls); + let _ = geom.intersects(&poly); + let _ = geom.intersects(&rect); + let _ = geom.intersects(&tri); + let _ = geom.intersects(&geom); + let _ = geom.intersects(&gc); + let _ = geom.intersects(&multi_point); + let _ = geom.intersects(&multi_ls); + let _ = geom.intersects(&multi_poly); + let _ = gc.intersects(&pt); + let _ = gc.intersects(&ln); + let _ = gc.intersects(&ls); + let _ = gc.intersects(&poly); + let _ = gc.intersects(&rect); + let _ = gc.intersects(&tri); + let _ = gc.intersects(&geom); + let _ = gc.intersects(&gc); + let _ = gc.intersects(&multi_point); + let _ = gc.intersects(&multi_ls); + let _ = gc.intersects(&multi_poly); + let _ = multi_point.intersects(&pt); + let _ = multi_point.intersects(&ln); + let _ = multi_point.intersects(&ls); + let _ = multi_point.intersects(&poly); + let _ = multi_point.intersects(&rect); + let _ = multi_point.intersects(&tri); + let _ = multi_point.intersects(&geom); + let _ = multi_point.intersects(&gc); + let _ = multi_point.intersects(&multi_point); + let _ = multi_point.intersects(&multi_ls); + let _ = multi_point.intersects(&multi_poly); + let _ = multi_ls.intersects(&pt); + let _ = multi_ls.intersects(&ln); + let _ = multi_ls.intersects(&ls); + let _ = multi_ls.intersects(&poly); + let _ = multi_ls.intersects(&rect); + let _ = multi_ls.intersects(&tri); + let _ = multi_ls.intersects(&geom); + let _ = multi_ls.intersects(&gc); + let _ = multi_ls.intersects(&multi_point); + let _ = multi_ls.intersects(&multi_ls); + let _ = multi_ls.intersects(&multi_poly); + let _ = multi_poly.intersects(&pt); + let _ = multi_poly.intersects(&ln); + let _ = multi_poly.intersects(&ls); + let _ = multi_poly.intersects(&poly); + let _ = multi_poly.intersects(&rect); + let _ = multi_poly.intersects(&tri); + let _ = multi_poly.intersects(&geom); + let _ = multi_poly.intersects(&gc); + let _ = multi_poly.intersects(&multi_point); + let _ = multi_poly.intersects(&multi_ls); + let _ = multi_poly.intersects(&multi_poly); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/point.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/point.rs new file mode 100644 index 00000000..4fec2da5 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/point.rs @@ -0,0 +1,107 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use sedona_geo_traits_ext::*; + +use super::IntersectsTrait; +use crate::*; + +// Generate implementations for Point by delegating to Coord +macro_rules! impl_intersects_point_from_coord { + ($num_type:ident, $rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait for LHS + where + T: $num_type, + LHS: PointTraitExt, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + self.coord_ext().is_some_and(|c| c.intersects_trait(rhs)) + } + } + }; +} + +impl_intersects_point_from_coord!(CoordNum, CoordTraitExt, CoordTag); +impl_intersects_point_from_coord!(CoordNum, PointTraitExt, PointTag); +impl_intersects_point_from_coord!(GeoNum, LineStringTraitExt, LineStringTag); +impl_intersects_point_from_coord!(GeoNum, PolygonTraitExt, PolygonTag); +impl_intersects_point_from_coord!(CoordNum, MultiPointTraitExt, MultiPointTag); +impl_intersects_point_from_coord!(GeoNum, MultiLineStringTraitExt, MultiLineStringTag); +impl_intersects_point_from_coord!(GeoNum, MultiPolygonTraitExt, MultiPolygonTag); +impl_intersects_point_from_coord!(GeoNum, GeometryTraitExt, GeometryTag); +impl_intersects_point_from_coord!(GeoNum, GeometryCollectionTraitExt, GeometryCollectionTag); +impl_intersects_point_from_coord!(GeoNum, LineTraitExt, LineTag); +impl_intersects_point_from_coord!(CoordNum, RectTraitExt, RectTag); +impl_intersects_point_from_coord!(GeoNum, TriangleTraitExt, TriangleTag); + +// Generate implementations for MultiPoint by delegating to Point +macro_rules! impl_intersects_multipoint_from_point { + ($num_type:ident, $rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait for LHS + where + T: $num_type, + LHS: MultiPointTraitExt, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + self.points_ext().any(|p| p.intersects_trait(rhs)) + } + } + }; +} + +impl_intersects_multipoint_from_point!(CoordNum, CoordTraitExt, CoordTag); +impl_intersects_multipoint_from_point!(CoordNum, PointTraitExt, PointTag); +impl_intersects_multipoint_from_point!(GeoNum, LineStringTraitExt, LineStringTag); +impl_intersects_multipoint_from_point!(GeoNum, PolygonTraitExt, PolygonTag); +impl_intersects_multipoint_from_point!(CoordNum, MultiPointTraitExt, MultiPointTag); +impl_intersects_multipoint_from_point!(GeoNum, MultiLineStringTraitExt, MultiLineStringTag); +impl_intersects_multipoint_from_point!(GeoNum, MultiPolygonTraitExt, MultiPolygonTag); +impl_intersects_multipoint_from_point!(GeoNum, GeometryTraitExt, GeometryTag); +impl_intersects_multipoint_from_point!(GeoNum, GeometryCollectionTraitExt, GeometryCollectionTag); +impl_intersects_multipoint_from_point!(GeoNum, LineTraitExt, LineTag); +impl_intersects_multipoint_from_point!(CoordNum, RectTraitExt, RectTag); +impl_intersects_multipoint_from_point!(GeoNum, TriangleTraitExt, TriangleTag); + +symmetric_intersects_trait_impl!( + CoordNum, + CoordTraitExt, + CoordTag, + MultiPointTraitExt, + MultiPointTag +); +symmetric_intersects_trait_impl!( + GeoNum, + LineTraitExt, + LineTag, + MultiPointTraitExt, + MultiPointTag +); +symmetric_intersects_trait_impl!( + GeoNum, + TriangleTraitExt, + TriangleTag, + MultiPointTraitExt, + MultiPointTag +); +symmetric_intersects_trait_impl!( + GeoNum, + PolygonTraitExt, + PolygonTag, + MultiPointTraitExt, + MultiPointTag +); diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/polygon.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/polygon.rs new file mode 100644 index 00000000..01142c9f --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/polygon.rs @@ -0,0 +1,209 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use super::{has_disjoint_bboxes, IntersectsTrait}; +use crate::coordinate_position::CoordPos; +use crate::CoordinatePosition; +use crate::GeoNum; +use sedona_geo_traits_ext::*; + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: PolygonTraitExt, + RHS: CoordTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + self.coordinate_position(&rhs.geo_coord()) != CoordPos::Outside + } +} + +symmetric_intersects_trait_impl!(GeoNum, CoordTraitExt, CoordTag, PolygonTraitExt, PolygonTag); +symmetric_intersects_trait_impl!(GeoNum, PolygonTraitExt, PolygonTag, PointTraitExt, PointTag); + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: PolygonTraitExt, + RHS: LineTraitExt, +{ + fn intersects_trait(&self, line: &RHS) -> bool { + // Check if line intersects any part of the polygon + if let Some(exterior) = self.exterior_ext() { + exterior.intersects_trait(line) + || self + .interiors_ext() + .any(|inner| inner.intersects_trait(line)) + || self.intersects_trait(&line.start_ext()) + || self.intersects_trait(&line.end_ext()) + } else { + false + } + } +} + +symmetric_intersects_trait_impl!(GeoNum, LineTraitExt, LineTag, PolygonTraitExt, PolygonTag); +symmetric_intersects_trait_impl!( + GeoNum, + PolygonTraitExt, + PolygonTag, + LineStringTraitExt, + LineStringTag +); +symmetric_intersects_trait_impl!( + GeoNum, + PolygonTraitExt, + PolygonTag, + MultiLineStringTraitExt, + MultiLineStringTag +); + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: PolygonTraitExt, + RHS: RectTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + self.intersects_trait(&rhs.to_polygon()) + } +} + +symmetric_intersects_trait_impl!(GeoNum, RectTraitExt, RectTag, PolygonTraitExt, PolygonTag); + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: PolygonTraitExt, + RHS: TriangleTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + self.intersects_trait(&rhs.to_polygon()) + } +} + +symmetric_intersects_trait_impl!( + GeoNum, + TriangleTraitExt, + TriangleTag, + PolygonTraitExt, + PolygonTag +); + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: PolygonTraitExt, + RHS: PolygonTraitExt, +{ + fn intersects_trait(&self, polygon: &RHS) -> bool { + if has_disjoint_bboxes(self, polygon) { + return false; + } + + if let (Some(self_exterior), Some(polygon_exterior)) = + (self.exterior_ext(), polygon.exterior_ext()) + { + // self intersects (or contains) any line in polygon + self.intersects_trait(&polygon_exterior) || + polygon.interiors_ext().any(|inner_line_string| self.intersects_trait(&inner_line_string)) || + // self is contained inside polygon + polygon.intersects_trait(&self_exterior) + } else { + false + } + } +} + +// Generate implementations for MultiPolygon by delegating to the Polygon implementation + +macro_rules! impl_intersects_multi_polygon_from_polygon { + ($rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait for LHS + where + T: GeoNum, + LHS: MultiPolygonTraitExt, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + if has_disjoint_bboxes(self, rhs) { + return false; + } + self.polygons_ext().any(|p| p.intersects_trait(rhs)) + } + } + }; +} + +impl_intersects_multi_polygon_from_polygon!(CoordTraitExt, CoordTag); +impl_intersects_multi_polygon_from_polygon!(PointTraitExt, PointTag); +impl_intersects_multi_polygon_from_polygon!(LineStringTraitExt, LineStringTag); +impl_intersects_multi_polygon_from_polygon!(PolygonTraitExt, PolygonTag); +impl_intersects_multi_polygon_from_polygon!(MultiPointTraitExt, MultiPointTag); +impl_intersects_multi_polygon_from_polygon!(MultiLineStringTraitExt, MultiLineStringTag); +impl_intersects_multi_polygon_from_polygon!(MultiPolygonTraitExt, MultiPolygonTag); +impl_intersects_multi_polygon_from_polygon!(GeometryTraitExt, GeometryTag); +impl_intersects_multi_polygon_from_polygon!(GeometryCollectionTraitExt, GeometryCollectionTag); +impl_intersects_multi_polygon_from_polygon!(LineTraitExt, LineTag); +impl_intersects_multi_polygon_from_polygon!(RectTraitExt, RectTag); +impl_intersects_multi_polygon_from_polygon!(TriangleTraitExt, TriangleTag); + +symmetric_intersects_trait_impl!( + GeoNum, + CoordTraitExt, + CoordTag, + MultiPolygonTraitExt, + MultiPolygonTag +); +symmetric_intersects_trait_impl!( + GeoNum, + LineTraitExt, + LineTag, + MultiPolygonTraitExt, + MultiPolygonTag +); +symmetric_intersects_trait_impl!( + GeoNum, + RectTraitExt, + RectTag, + MultiPolygonTraitExt, + MultiPolygonTag +); +symmetric_intersects_trait_impl!( + GeoNum, + TriangleTraitExt, + TriangleTag, + MultiPolygonTraitExt, + MultiPolygonTag +); +symmetric_intersects_trait_impl!( + GeoNum, + PolygonTraitExt, + PolygonTag, + MultiPolygonTraitExt, + MultiPolygonTag +); + +#[cfg(test)] +mod tests { + use crate::*; + #[test] + fn geom_intersects_geom() { + let a = Geometry::::from(polygon![]); + let b = Geometry::from(polygon![]); + assert!(!a.intersects(&b)); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/rect.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/rect.rs new file mode 100644 index 00000000..a588c3e8 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/rect.rs @@ -0,0 +1,113 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use geo_traits::CoordTrait; +use sedona_geo_traits_ext::*; + +use super::IntersectsTrait; +use crate::*; + +impl IntersectsTrait for LHS +where + T: CoordNum, + LHS: RectTraitExt, + RHS: CoordTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + let lhs_x = rhs.x(); + let lhs_y = rhs.y(); + + lhs_x >= self.min().x() + && lhs_y >= self.min().y() + && lhs_x <= self.max().x() + && lhs_y <= self.max().y() + } +} + +symmetric_intersects_trait_impl!(CoordNum, CoordTraitExt, CoordTag, RectTraitExt, RectTag); +symmetric_intersects_trait_impl!(CoordNum, RectTraitExt, RectTag, PointTraitExt, PointTag); +symmetric_intersects_trait_impl!( + CoordNum, + RectTraitExt, + RectTag, + MultiPointTraitExt, + MultiPointTag +); + +impl IntersectsTrait for LHS +where + T: CoordNum, + LHS: RectTraitExt, + RHS: RectTraitExt, +{ + fn intersects_trait(&self, other: &RHS) -> bool { + if self.max().x() < other.min().x() { + return false; + } + + if self.max().y() < other.min().y() { + return false; + } + + if self.min().x() > other.max().x() { + return false; + } + + if self.min().y() > other.max().y() { + return false; + } + + true + } +} + +// Same logic as polygon x line, but avoid an allocation. +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: RectTraitExt, + RHS: LineTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + let lt = self.min_coord(); + let rb = self.max_coord(); + let lb = Coord::from((lt.x, rb.y)); + let rt = Coord::from((rb.x, lt.y)); + + // If either rhs.{start,end} lies inside Rect, then true + self.intersects_trait(&rhs.start_ext()) + || self.intersects_trait(&rhs.end_ext()) + || Line::new(lt, rt).intersects_trait(rhs) + || Line::new(rt, rb).intersects_trait(rhs) + || Line::new(lb, rb).intersects_trait(rhs) + || Line::new(lt, lb).intersects_trait(rhs) + } +} + +symmetric_intersects_trait_impl!(GeoNum, LineTraitExt, LineTag, RectTraitExt, RectTag); + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: RectTraitExt, + RHS: TriangleTraitExt, +{ + fn intersects_trait(&self, other: &RHS) -> bool { + self.intersects_trait(&other.to_polygon()) + } +} + +symmetric_intersects_trait_impl!(GeoNum, TriangleTraitExt, TriangleTag, RectTraitExt, RectTag); diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/triangle.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/triangle.rs new file mode 100644 index 00000000..33d8c233 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/triangle.rs @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use super::IntersectsTrait; +use crate::*; +use sedona_geo_traits_ext::*; + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: TriangleTraitExt, + RHS: CoordTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + let rhs = rhs.geo_coord(); + + let mut orientations = self + .to_lines() + .map(|l| T::Ker::orient2d(l.start, l.end, rhs)); + + orientations.sort(); + + !orientations + .windows(2) + .any(|win| win[0] != win[1] && win[1] != Orientation::Collinear) + + // // neglecting robust predicates, hence faster + // let p0x = self.0.x.to_f64().unwrap(); + // let p0y = self.0.y.to_f64().unwrap(); + // let p1x = self.1.x.to_f64().unwrap(); + // let p1y = self.1.y.to_f64().unwrap(); + // let p2x = self.2.x.to_f64().unwrap(); + // let p2y = self.2.y.to_f64().unwrap(); + + // let px = rhs.x.to_f64().unwrap(); + // let py = rhs.y.to_f64().unwrap(); + + // let s = (p0x - p2x) * (py - p2y) - (p0y - p2y) * (px - p2x); + // let t = (p1x - p0x) * (py - p0y) - (p1y - p0y) * (px - p0x); + + // if (s < 0.) != (t < 0.) && s != 0. && t != 0. { + // return false; + // } + + // let d = (p2x - p1x) * (py - p1y) - (p2y - p1y) * (px - p1x); + // d == 0. || (d < 0.) == (s + t <= 0.) + } +} + +symmetric_intersects_trait_impl!( + GeoNum, + CoordTraitExt, + CoordTag, + TriangleTraitExt, + TriangleTag +); +symmetric_intersects_trait_impl!( + GeoNum, + TriangleTraitExt, + TriangleTag, + PointTraitExt, + PointTag +); + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: TriangleTraitExt, + RHS: TriangleTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + self.to_polygon().intersects_trait(&rhs.to_polygon()) + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/kernels/mod.rs b/rust/sedona-geo-generic-alg/src/algorithm/kernels/mod.rs new file mode 100644 index 00000000..c6ab0453 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/kernels/mod.rs @@ -0,0 +1,66 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use num_traits::Zero; + +use crate::{coord, Coord, CoordNum}; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +pub enum Orientation { + CounterClockwise, + Clockwise, + Collinear, +} + +/// Kernel trait to provide predicates to operate on +/// different scalar types. +pub trait Kernel { + /// Gives the orientation of 3 2-dimensional points: + /// ccw, cw or collinear (None) + fn orient2d(p: Coord, q: Coord, r: Coord) -> Orientation { + let res = (q.x - p.x) * (r.y - q.y) - (q.y - p.y) * (r.x - q.x); + if res > Zero::zero() { + Orientation::CounterClockwise + } else if res < Zero::zero() { + Orientation::Clockwise + } else { + Orientation::Collinear + } + } + + fn square_euclidean_distance(p: Coord, q: Coord) -> T { + (p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y) + } + + /// Compute the sign of the dot product of `u` and `v` using + /// robust predicates. The output is `CounterClockwise` if + /// the sign is positive, `Clockwise` if negative, and + /// `Collinear` if zero. + fn dot_product_sign(u: Coord, v: Coord) -> Orientation { + let zero = Coord::zero(); + let vdash = coord! { + x: T::zero() - v.y, + y: v.x, + }; + Self::orient2d(zero, u, vdash) + } +} + +pub mod robust; +pub use self::robust::RobustKernel; + +pub mod simple; +pub use self::simple::SimpleKernel; diff --git a/rust/sedona-geo-generic-alg/src/algorithm/kernels/robust.rs b/rust/sedona-geo-generic-alg/src/algorithm/kernels/robust.rs new file mode 100644 index 00000000..f77f9dc5 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/kernels/robust.rs @@ -0,0 +1,60 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use super::{CoordNum, Kernel, Orientation}; +use crate::Coord; + +use num_traits::{Float, NumCast}; + +/// Robust kernel that uses [fast robust +/// predicates](//www.cs.cmu.edu/~quake/robust.html) to +/// provide robust floating point predicates. Should only be +/// used with types that can _always_ be casted to `f64` +/// _without loss in precision_. +#[derive(Default, Debug)] +pub struct RobustKernel; + +impl Kernel for RobustKernel +where + T: CoordNum + Float, +{ + fn orient2d(p: Coord, q: Coord, r: Coord) -> Orientation { + use robust::{orient2d, Coord}; + + let orientation = orient2d( + Coord { + x: ::from(p.x).unwrap(), + y: ::from(p.y).unwrap(), + }, + Coord { + x: ::from(q.x).unwrap(), + y: ::from(q.y).unwrap(), + }, + Coord { + x: ::from(r.x).unwrap(), + y: ::from(r.y).unwrap(), + }, + ); + + if orientation < 0. { + Orientation::Clockwise + } else if orientation > 0. { + Orientation::CounterClockwise + } else { + Orientation::Collinear + } + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/kernels/simple.rs b/rust/sedona-geo-generic-alg/src/algorithm/kernels/simple.rs new file mode 100644 index 00000000..4c297426 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/kernels/simple.rs @@ -0,0 +1,26 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use super::Kernel; +use crate::CoordNum; + +/// Simple kernel provides the direct implementation of the +/// predicates. These are meant to be used with exact +/// arithmetic signed types (eg. i32, i64). +#[derive(Default, Debug)] +pub struct SimpleKernel; + +impl Kernel for SimpleKernel {} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/line_measures/distance.rs b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/distance.rs new file mode 100644 index 00000000..3be861c1 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/distance.rs @@ -0,0 +1,31 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +/// Calculate the minimum distance between two geometries. +pub trait Distance { + /// Note that not all implementations support all geometry combinations, but at least `Point` to `Point` + /// is supported. + /// See [specific implementations](#implementers) for details. + /// + /// # Units + /// + /// - `origin`, `destination`: geometry where the units of x/y depend on the trait implementation. + /// - returns: depends on the trait implementation. + fn distance(&self, origin: Origin, destination: Destination) -> F; +} + +// Re-export the DistanceExt trait from the refactored euclidean metric space +pub use super::metric_spaces::euclidean::DistanceExt; diff --git a/rust/sedona-geo-generic-alg/src/algorithm/line_measures/length.rs b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/length.rs new file mode 100644 index 00000000..1b511a4f --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/length.rs @@ -0,0 +1,827 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use super::Distance; +use crate::{CoordFloat, Point}; +use geo_traits::{CoordTrait, PolygonTrait}; +use sedona_geo_traits_ext::*; +use std::borrow::Borrow; + +/// Extension trait that enables the modern Length and Perimeter API for WKB and other generic geometry types. +/// +/// This provides the same API as the concrete `LengthMeasurable` implementations but works with +/// any geometry type that implements the geo-traits-ext pattern. +/// +/// # Examples +/// ``` +/// use sedona_geo_generic_alg::algorithm::line_measures::{LengthMeasurableExt, Euclidean}; +/// use geo_types::{LineString, coord}; +/// let ls = LineString::new(vec![ +/// coord! { x: 0., y: 0. }, +/// coord! { x: 3., y: 4. }, +/// coord! { x: 3., y: 5. }, +/// ]); +/// let length = ls.length_ext(&Euclidean); +/// assert_eq!(length, 6.0); +/// ``` +pub trait LengthMeasurableExt { + /// Calculate the length using the given metric space. + /// + /// For 1D geometries (Line, LineString, MultiLineString), returns the actual length. + /// For 0D and 2D geometries, returns zero. + fn length_ext(&self, metric_space: &impl Distance, Point>) -> F; + + /// Calculate the perimeter using the given metric space. + /// + /// For 2D geometries (Polygon, MultiPolygon, Rect, Triangle), returns the perimeter. + /// For 1D geometries (Line, LineString, MultiLineString), returns zero. + /// For 0D geometries (Point, MultiPoint), returns zero. + fn perimeter_ext(&self, metric_space: &impl Distance, Point>) -> F; +} + +// Implementation for WKB and other generic geometries using the type-tag pattern +impl LengthMeasurableExt for G +where + F: CoordFloat, + G: GeoTraitExtWithTypeTag + LengthMeasurableTrait, +{ + fn length_ext(&self, metric_space: &impl Distance, Point>) -> F { + self.length_trait(metric_space) + } + + fn perimeter_ext(&self, metric_space: &impl Distance, Point>) -> F { + self.perimeter_trait(metric_space) + } +} + +// Internal trait that handles the actual length and perimeter computation for different geometry types +trait LengthMeasurableTrait +where + F: CoordFloat, +{ + fn length_trait(&self, metric_space: &impl Distance, Point>) -> F; + fn perimeter_trait(&self, metric_space: &impl Distance, Point>) -> F; +} + +// Implementation for Line geometries +impl> LengthMeasurableTrait for L +where + F: CoordFloat, +{ + fn length_trait(&self, metric_space: &impl Distance, Point>) -> F { + let start = Point::new(self.start_coord().x, self.start_coord().y); + let end = Point::new(self.end_coord().x, self.end_coord().y); + metric_space.distance(start, end) + } + + fn perimeter_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // For 1D geometries like lines, perimeter should be 0 according to PostGIS/OGC standards + F::zero() + } +} + +// Implementation for LineString geometries +impl> LengthMeasurableTrait for LS +where + F: CoordFloat, +{ + fn length_trait(&self, metric_space: &impl Distance, Point>) -> F { + let mut length = F::zero(); + for line in self.lines() { + let start = Point::new(line.start_coord().x, line.start_coord().y); + let end = Point::new(line.end_coord().x, line.end_coord().y); + length = length + metric_space.distance(start, end); + } + length + } + + fn perimeter_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // For 1D geometries like linestrings, perimeter should be 0 according to PostGIS/OGC standards + F::zero() + } +} + +// Implementation for MultiLineString geometries +impl> LengthMeasurableTrait for MLS +where + F: CoordFloat, +{ + fn length_trait(&self, metric_space: &impl Distance, Point>) -> F { + let mut length = F::zero(); + for line_string in self.line_strings_ext() { + length = length + line_string.length_trait(metric_space); + } + length + } + + fn perimeter_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // For 1D geometries like multilinestrings, perimeter should be 0 according to PostGIS/OGC standards + F::zero() + } +} + +// For geometry types that don't have a meaningful length (return zero) +impl> LengthMeasurableTrait for P +where + F: CoordFloat, +{ + fn length_trait(&self, _metric_space: &impl Distance, Point>) -> F { + F::zero() + } + + fn perimeter_trait(&self, _metric_space: &impl Distance, Point>) -> F { + F::zero() + } +} + +impl> LengthMeasurableTrait for MP +where + F: CoordFloat, +{ + fn length_trait(&self, _metric_space: &impl Distance, Point>) -> F { + F::zero() + } + + fn perimeter_trait(&self, _metric_space: &impl Distance, Point>) -> F { + F::zero() + } +} + +// Helper function to calculate the perimeter of a linestring using a metric space +fn linestring_perimeter_with_metric>( + linestring: &LS, + metric_space: &impl Distance, Point>, +) -> F +where + F: CoordFloat, +{ + let mut perimeter = F::zero(); + for line in linestring.lines() { + let start_coord = line.start_coord(); + let end_coord = line.end_coord(); + let start_point = Point::new(start_coord.x(), start_coord.y()); + let end_point = Point::new(end_coord.x(), end_coord.y()); + perimeter = perimeter + metric_space.distance(start_point, end_point); + } + perimeter +} + +// Helper function to calculate the perimeter of a ring using the basic LineStringTrait +fn ring_perimeter_with_metric( + ring: &LS, + metric_space: &impl Distance, Point>, +) -> F +where + F: CoordFloat, + LS: geo_traits::LineStringTrait, +{ + let mut perimeter = F::zero(); + let num_coords = ring.num_coords(); + if num_coords > 1 { + for i in 0..(num_coords - 1) { + let start_coord = ring.coord(i).unwrap(); + let end_coord = ring.coord(i + 1).unwrap(); + let start_point = Point::new(start_coord.x(), start_coord.y()); + let end_point = Point::new(end_coord.x(), end_coord.y()); + perimeter = perimeter + metric_space.distance(start_point, end_point); + } + } + perimeter +} + +impl> LengthMeasurableTrait for P +where + F: CoordFloat, +{ + fn length_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // Length is a 1D concept, doesn't apply to 2D polygons + F::zero() + } + + fn perimeter_trait(&self, metric_space: &impl Distance, Point>) -> F { + // For polygons, return the perimeter (length of the boundary) + let mut total_perimeter = match self.exterior_ext() { + Some(exterior) => linestring_perimeter_with_metric(&exterior, metric_space), + None => F::zero(), + }; + + // Add interior rings perimeter + for interior in self.interiors_ext() { + total_perimeter = + total_perimeter + linestring_perimeter_with_metric(&interior, metric_space); + } + + total_perimeter + } +} + +impl> LengthMeasurableTrait for MP +where + F: CoordFloat, +{ + fn length_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // Length is a 1D concept, doesn't apply to 2D multipolygons + F::zero() + } + + fn perimeter_trait(&self, metric_space: &impl Distance, Point>) -> F { + // For multipolygons, return the sum of all polygon perimeters + let mut total_perimeter = F::zero(); + for polygon in self.polygons() { + // Calculate perimeter for each polygon + let mut polygon_perimeter = match polygon.exterior() { + Some(exterior) => ring_perimeter_with_metric(&exterior, metric_space), + None => F::zero(), + }; + + // Add interior rings perimeter + for interior in polygon.interiors() { + polygon_perimeter = + polygon_perimeter + ring_perimeter_with_metric(&interior, metric_space); + } + + total_perimeter = total_perimeter + polygon_perimeter; + } + total_perimeter + } +} + +impl> LengthMeasurableTrait for R +where + F: CoordFloat, +{ + fn length_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // Length is a 1D concept, doesn't apply to 2D rectangles + F::zero() + } + + fn perimeter_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // For rectangles, return the perimeter + let width = self.width(); + let height = self.height(); + let two = F::one() + F::one(); + two * (width + height) + } +} + +impl> LengthMeasurableTrait for T +where + F: CoordFloat, +{ + fn length_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // Length is a 1D concept, doesn't apply to 2D triangles + F::zero() + } + + fn perimeter_trait(&self, metric_space: &impl Distance, Point>) -> F { + // For triangles, return the perimeter (sum of all three sides) + let coord0 = self.first_coord(); + let coord1 = self.second_coord(); + let coord2 = self.third_coord(); + + let p0 = Point::new(coord0.x, coord0.y); + let p1 = Point::new(coord1.x, coord1.y); + let p2 = Point::new(coord2.x, coord2.y); + + let side1 = metric_space.distance(p0, p1); + let side2 = metric_space.distance(p1, p2); + let side3 = metric_space.distance(p2, p0); + + side1 + side2 + side3 + } +} + +// Implementation for GeometryCollection with runtime type dispatch +impl> LengthMeasurableTrait + for GC +where + F: CoordFloat, +{ + fn length_trait(&self, metric_space: &impl Distance, Point>) -> F { + self.geometries_ext() + .map(|g| g.borrow().length_trait(metric_space)) + .fold(F::zero(), |acc, next| acc + next) + } + + fn perimeter_trait(&self, metric_space: &impl Distance, Point>) -> F { + self.geometries_ext() + .map(|g| g.borrow().perimeter_trait(metric_space)) + .fold(F::zero(), |acc, next| acc + next) + } +} + +impl> LengthMeasurableTrait for G +where + F: CoordFloat, +{ + fn length_trait(&self, metric_space: &impl Distance, Point>) -> F { + if self.is_collection() { + self.geometries_ext() + .map(|g_inner| g_inner.borrow().length_trait(metric_space)) + .fold(F::zero(), |acc, next| acc + next) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(_) => F::zero(), + GeometryTypeExt::Line(line) => line.length_trait(metric_space), + GeometryTypeExt::LineString(ls) => ls.length_trait(metric_space), + GeometryTypeExt::Polygon(_) => F::zero(), + GeometryTypeExt::MultiPoint(_) => F::zero(), + GeometryTypeExt::MultiLineString(mls) => mls.length_trait(metric_space), + GeometryTypeExt::MultiPolygon(_) => F::zero(), + GeometryTypeExt::Rect(_) => F::zero(), + GeometryTypeExt::Triangle(_) => F::zero(), + } + } + } + + fn perimeter_trait(&self, metric_space: &impl Distance, Point>) -> F { + if self.is_collection() { + self.geometries_ext() + .map(|g_inner| g_inner.borrow().perimeter_trait(metric_space)) + .fold(F::zero(), |acc, next| acc + next) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(_) => F::zero(), + GeometryTypeExt::Line(_) => F::zero(), // 1D geometry - no perimeter + GeometryTypeExt::LineString(_) => F::zero(), // 1D geometry - no perimeter + GeometryTypeExt::Polygon(polygon) => polygon.perimeter_trait(metric_space), + GeometryTypeExt::MultiPoint(_) => F::zero(), + GeometryTypeExt::MultiLineString(_) => F::zero(), // 1D geometry - no perimeter + GeometryTypeExt::MultiPolygon(mp) => mp.perimeter_trait(metric_space), + GeometryTypeExt::Rect(rect) => rect.perimeter_trait(metric_space), + GeometryTypeExt::Triangle(triangle) => triangle.perimeter_trait(metric_space), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Euclidean; + + // Tests for LengthMeasurableExt - adapted from euclidean_length.rs + mod length_measurable_ext_tests { + use geo::LineString; + + use super::*; + use crate::{ + coord, line_string, polygon, Geometry, GeometryCollection, Line, MultiLineString, + MultiPoint, MultiPolygon, Point, Polygon, + }; + + #[test] + fn empty_linestring_test() { + let linestring = line_string![]; + assert_relative_eq!(0.0_f64, linestring.length_ext(&Euclidean)); + } + + #[test] + fn linestring_one_point_test() { + let linestring = line_string![(x: 0., y: 0.)]; + assert_relative_eq!(0.0_f64, linestring.length_ext(&Euclidean)); + } + + #[test] + fn linestring_test() { + let linestring = line_string![ + (x: 1., y: 1.), + (x: 7., y: 1.), + (x: 8., y: 1.), + (x: 9., y: 1.), + (x: 10., y: 1.), + (x: 11., y: 1.) + ]; + assert_relative_eq!(10.0_f64, linestring.length_ext(&Euclidean)); + } + + #[test] + fn multilinestring_test() { + let mline = MultiLineString::new(vec![ + line_string![ + (x: 1., y: 0.), + (x: 7., y: 0.), + (x: 8., y: 0.), + (x: 9., y: 0.), + (x: 10., y: 0.), + (x: 11., y: 0.) + ], + line_string![ + (x: 0., y: 0.), + (x: 0., y: 5.) + ], + ]); + assert_relative_eq!(15.0_f64, mline.length_ext(&Euclidean)); + } + + #[test] + fn line_test() { + let line0 = Line::new(coord! { x: 0., y: 0. }, coord! { x: 0., y: 1. }); + let line1 = Line::new(coord! { x: 0., y: 0. }, coord! { x: 3., y: 4. }); + assert_relative_eq!(line0.length_ext(&Euclidean), 1.); + assert_relative_eq!(line1.length_ext(&Euclidean), 5.); + } + + #[test] + fn polygon_length_and_perimeter_test() { + let polygon: Polygon = polygon![ + (x: 0., y: 0.), + (x: 4., y: 0.), + (x: 4., y: 4.), + (x: 0., y: 4.), + (x: 0., y: 0.), + ]; + // For polygons, length_ext returns zero (length is a 1D concept) + assert_relative_eq!(polygon.length_ext(&Euclidean), 0.0); + // For polygons, perimeter_ext returns the perimeter: 4 + 4 + 4 + 4 = 16 + assert_relative_eq!(polygon.perimeter_ext(&Euclidean), 16.0); + } + + #[test] + fn point_returns_zero_test() { + let point = Point::new(3.0, 4.0); + // Points have no length dimension + assert_relative_eq!(point.length_ext(&Euclidean), 0.0); + } + + #[test] + fn comprehensive_length_test_scenarios() { + // Test cases for length calculations - should return actual length only for 1D geometries + + // LINESTRING EMPTY + let empty_linestring: crate::LineString = line_string![]; + assert_relative_eq!(empty_linestring.length_ext(&Euclidean), 0.0); + + // POINT (0 0) - 0D geometry + let point = Point::new(0.0, 0.0); + assert_relative_eq!(point.length_ext(&Euclidean), 0.0); + + // LINESTRING (0 0, 0 1) - 1D geometry, length should be 1 + let linestring = line_string![(x: 0., y: 0.), (x: 0., y: 1.)]; + assert_relative_eq!(linestring.length_ext(&Euclidean), 1.0); + + // MULTIPOINT ((0 0), (1 1)) - 0D geometry, should be 0 + let multipoint = MultiPoint::new(vec![Point::new(0.0, 0.0), Point::new(1.0, 1.0)]); + assert_relative_eq!(multipoint.length_ext(&Euclidean), 0.0); + + // MULTILINESTRING ((0 0, 1 1), (1 1, 2 2)) - 1D geometry, should be ~2.828427 + let multilinestring = MultiLineString::new(vec![ + line_string![(x: 0., y: 0.), (x: 1., y: 1.)], + line_string![(x: 1., y: 1.), (x: 2., y: 2.)], + ]); + assert_relative_eq!( + multilinestring.length_ext(&Euclidean), + 2.8284271247461903, + epsilon = 1e-10 + ); + + // POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)) - 2D geometry, length should be 0 + let polygon = polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]; + assert_relative_eq!(polygon.length_ext(&Euclidean), 0.0); + + // MULTIPOLYGON - 2D geometry, length should be 0 + let multipolygon = MultiPolygon::new(vec![ + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + polygon![ + (x: 2., y: 2.), + (x: 3., y: 2.), + (x: 3., y: 3.), + (x: 2., y: 3.), + (x: 2., y: 2.), + ], + ]); + assert_relative_eq!(multipolygon.length_ext(&Euclidean), 0.0); + + // RECT - 2D geometry, length should be 0 + let rect = crate::Rect::new(coord! { x: 0., y: 0. }, coord! { x: 3., y: 4. }); + assert_relative_eq!(rect.length_ext(&Euclidean), 0.0); + + // TRIANGLE - 2D geometry, length should be 0 + let triangle = crate::Triangle::new( + coord! { x: 0., y: 0. }, + coord! { x: 3., y: 0. }, + coord! { x: 0., y: 4. }, + ); + assert_relative_eq!(triangle.length_ext(&Euclidean), 0.0); + + // GEOMETRYCOLLECTION - should sum only the 1D geometries (linestrings) + let collection = GeometryCollection::new_from(vec![ + Geometry::Point(Point::new(0.0, 0.0)), // contributes 0 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // sqrt(2) ≈ 1.414 + Geometry::Polygon(polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]), // contributes 0 to length + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // sqrt(2) ≈ 1.414 + ]); + assert_relative_eq!( + collection.length_ext(&Euclidean), + 2.8284271247461903, // 2*sqrt(2) only from linestrings + epsilon = 1e-10 + ); + + // GEOMETRY representation of GEOMETRYCOLLECTION + assert_relative_eq!( + Geometry::GeometryCollection(collection.clone()).length_ext(&Euclidean), + 2.8284271247461903, // 2*sqrt(2) only from linestrings + epsilon = 1e-10 + ); + } + + #[test] + fn comprehensive_perimeter_test_scenarios() { + // Test cases for perimeter calculations + + // LINESTRING EMPTY - no perimeter + let empty_linestring: crate::LineString = line_string![]; + assert_relative_eq!(empty_linestring.perimeter_ext(&Euclidean), 0.0); + + // POINT (0 0) - 0D geometry, no perimeter + let point = Point::new(0.0, 0.0); + assert_relative_eq!(point.perimeter_ext(&Euclidean), 0.0); + + // LINESTRING (0 0, 0 1) - 1D geometry, perimeter should be 0 + let linestring = line_string![(x: 0., y: 0.), (x: 0., y: 1.)]; + assert_relative_eq!(linestring.perimeter_ext(&Euclidean), 0.0); + + // MULTIPOINT ((0 0), (1 1)) - 0D geometry, no perimeter + let multipoint = MultiPoint::new(vec![Point::new(0.0, 0.0), Point::new(1.0, 1.0)]); + assert_relative_eq!(multipoint.perimeter_ext(&Euclidean), 0.0); + + // MULTILINESTRING ((0 0, 1 1), (1 1, 2 2)) - 1D geometry, perimeter should be 0 + let multilinestring = MultiLineString::new(vec![ + line_string![(x: 0., y: 0.), (x: 1., y: 1.)], + line_string![(x: 1., y: 1.), (x: 2., y: 2.)], + ]); + assert_relative_eq!(multilinestring.perimeter_ext(&Euclidean), 0.0); + + // POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)) - 2D geometry, actual perimeter + let polygon = polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]; + assert_relative_eq!(polygon.perimeter_ext(&Euclidean), 4.0); + + // MULTIPOLYGON - 2D geometry, sum of all polygon perimeters + let multipolygon = MultiPolygon::new(vec![ + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + polygon![ + (x: 2., y: 2.), + (x: 3., y: 2.), + (x: 3., y: 3.), + (x: 2., y: 3.), + (x: 2., y: 2.), + ], + ]); + assert_relative_eq!(multipolygon.perimeter_ext(&Euclidean), 8.0); + + // RECT - 2D geometry, perimeter = 2*(width + height) + let rect = crate::Rect::new(coord! { x: 0., y: 0. }, coord! { x: 3., y: 4. }); + assert_relative_eq!(rect.perimeter_ext(&Euclidean), 14.0); // 2*(3+4) = 14 + + // TRIANGLE - 2D geometry, sum of all three sides + let triangle = crate::Triangle::new( + coord! { x: 0., y: 0. }, + coord! { x: 3., y: 0. }, + coord! { x: 0., y: 4. }, + ); + assert_relative_eq!(triangle.perimeter_ext(&Euclidean), 12.0); // 3 + 4 + 5 = 12 + + // GEOMETRYCOLLECTION - should sum perimeters from all geometries + let collection = GeometryCollection::new_from(vec![ + Geometry::Point(Point::new(0.0, 0.0)), // contributes 0 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // contributes 0 (1D geometry) + Geometry::Polygon(polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]), // perimeter = 4.0 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // contributes 0 (1D geometry) + ]); + assert_relative_eq!( + collection.perimeter_ext(&Euclidean), + 4.0, // only polygon perimeter counts + epsilon = 1e-10 + ); + + // GEOMETRY representation of GEOMETRYCOLLECTION + assert_relative_eq!( + Geometry::GeometryCollection(collection).perimeter_ext(&Euclidean), + 4.0, // only polygon perimeter counts + epsilon = 1e-10 + ); + } + + #[test] + fn test_polygon_with_holes() { + // Test polygon with interior rings (holes) + let polygon = Polygon::new( + LineString::new(vec![ + coord! { x: 0., y: 0. }, + coord! { x: 10., y: 0. }, + coord! { x: 10., y: 10. }, + coord! { x: 0., y: 10. }, + coord! { x: 0., y: 0. }, + ]), + vec![LineString::new(vec![ + coord! { x: 2., y: 2. }, + coord! { x: 8., y: 2. }, + coord! { x: 8., y: 8. }, + coord! { x: 2., y: 8. }, + coord! { x: 2., y: 2. }, + ])], + ); + // Length should be 0 (2D geometry) + assert_relative_eq!(polygon.length_ext(&Euclidean), 0.0); + // Exterior perimeter: 40 (10+10+10+10), Interior perimeter: 24 (6+6+6+6) + assert_relative_eq!(polygon.perimeter_ext(&Euclidean), 64.0); + } + + #[test] + fn test_triangle_perimeter() { + use crate::Triangle; + // Right triangle with sides 3, 4, 5 + let triangle = Triangle::new( + coord! { x: 0., y: 0. }, + coord! { x: 3., y: 0. }, + coord! { x: 0., y: 4. }, + ); + // Length should be 0 (2D geometry) + assert_relative_eq!(triangle.length_ext(&Euclidean), 0.0); + // Perimeter should be 3 + 4 + 5 = 12 + assert_relative_eq!(triangle.perimeter_ext(&Euclidean), 12.0); + } + + #[test] + fn test_rect_perimeter() { + use crate::Rect; + // Rectangle 3x4 + let rect = Rect::new(coord! { x: 0., y: 0. }, coord! { x: 3., y: 4. }); + // Length should be 0 (2D geometry) + assert_relative_eq!(rect.length_ext(&Euclidean), 0.0); + // Perimeter should be 2*(3+4) = 14 + assert_relative_eq!(rect.perimeter_ext(&Euclidean), 14.0); + } + + #[test] + fn test_postgis_compliance_perimeter_scenarios() { + // Test cases based on PostGIS ST_Perimeter behavior to ensure compliance + // These test cases mirror the pytest.mark.parametrize scenarios + + // POINT EMPTY - should return 0 + // Note: We can't easily test empty point, so we test a regular point + let point = Point::new(0.0, 0.0); + assert_relative_eq!(point.perimeter_ext(&Euclidean), 0.0); + + // LINESTRING EMPTY - should return 0 + let empty_linestring: crate::LineString = line_string![]; + assert_relative_eq!(empty_linestring.perimeter_ext(&Euclidean), 0.0); + + // POINT (0 0) - should return 0 + let point_origin = Point::new(0.0, 0.0); + assert_relative_eq!(point_origin.perimeter_ext(&Euclidean), 0.0); + + // LINESTRING (0 0, 0 1) - should return 0 (1D geometry has no perimeter) + let linestring_simple = line_string![(x: 0., y: 0.), (x: 0., y: 1.)]; + assert_relative_eq!(linestring_simple.perimeter_ext(&Euclidean), 0.0); + + // MULTIPOINT ((0 0), (1 1)) - should return 0 + let multipoint = MultiPoint::new(vec![Point::new(0.0, 0.0), Point::new(1.0, 1.0)]); + assert_relative_eq!(multipoint.perimeter_ext(&Euclidean), 0.0); + + // MULTILINESTRING ((0 0, 1 1), (1 1, 2 2)) - should return 0 (1D geometry has no perimeter) + let multilinestring = MultiLineString::new(vec![ + line_string![(x: 0., y: 0.), (x: 1., y: 1.)], + line_string![(x: 1., y: 1.), (x: 2., y: 2.)], + ]); + assert_relative_eq!(multilinestring.perimeter_ext(&Euclidean), 0.0); + + // POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)) - should return 4 (perimeter of unit square) + let polygon_unit_square = polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]; + assert_relative_eq!(polygon_unit_square.perimeter_ext(&Euclidean), 4.0); + + // MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((0 0, 1 0, 1 1, 0 1, 0 0))) - should return 8 (two unit squares) + let multipolygon_two_unit_squares = MultiPolygon::new(vec![ + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + ]); + assert_relative_eq!(multipolygon_two_unit_squares.perimeter_ext(&Euclidean), 8.0); + + // GEOMETRYCOLLECTION (POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))) + // Should return 8 (only polygons contribute to perimeter: 4 + 0 + 4 = 8) + let geometry_collection_mixed = GeometryCollection::new_from(vec![ + Geometry::Polygon(polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]), // contributes 4 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // contributes 0 (1D geometry) + Geometry::Polygon(polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]), // contributes 4 + ]); + assert_relative_eq!(geometry_collection_mixed.perimeter_ext(&Euclidean), 8.0); + } + + #[test] + fn test_perimeter_vs_length_distinction() { + // This test ensures we correctly distinguish between length and perimeter + // according to PostGIS/OGC standards + + let linestring = line_string![(x: 0., y: 0.), (x: 3., y: 4.)]; // length = 5.0 + let polygon = polygon![(x: 0., y: 0.), (x: 3., y: 0.), (x: 3., y: 4.), (x: 0., y: 4.), (x: 0., y: 0.)]; // perimeter = 14.0 + + // For 1D geometries: length > 0, perimeter = 0 + assert_relative_eq!(linestring.length_ext(&Euclidean), 5.0); + assert_relative_eq!(linestring.perimeter_ext(&Euclidean), 0.0); + + // For 2D geometries: length = 0, perimeter > 0 + assert_relative_eq!(polygon.length_ext(&Euclidean), 0.0); + assert_relative_eq!(polygon.perimeter_ext(&Euclidean), 14.0); + } + + #[test] + fn test_empty_geometry_perimeter() { + // Test empty geometries return 0 perimeter + + // Empty LineString + let empty_ls: crate::LineString = line_string![]; + assert_relative_eq!(empty_ls.perimeter_ext(&Euclidean), 0.0); + + // Empty MultiLineString + let empty_mls = MultiLineString::::new(vec![]); + assert_relative_eq!(empty_mls.perimeter_ext(&Euclidean), 0.0); + + // Empty MultiPoint + let empty_mp = MultiPoint::::new(vec![]); + assert_relative_eq!(empty_mp.perimeter_ext(&Euclidean), 0.0); + + // Empty GeometryCollection + let empty_gc = GeometryCollection::::new_from(vec![]); + assert_relative_eq!(empty_gc.perimeter_ext(&Euclidean), 0.0); + } + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/distance.rs b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/distance.rs new file mode 100644 index 00000000..6444588f --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/distance.rs @@ -0,0 +1,2271 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use crate::{CoordFloat, GeoFloat, Point}; +use num_traits::{Bounded, Float}; +use std::borrow::Borrow; + +// Import all the utility functions from utils module +use super::utils::{ + distance_coord_to_line_generic, + // Symmetric distance functions generated by macro + distance_line_to_line_generic, + distance_line_to_linestring_generic, + distance_line_to_polygon_generic, + distance_linestring_to_polygon_generic, + distance_point_to_linestring_generic, + distance_point_to_point_generic, + distance_point_to_polygon_generic, + distance_polygon_to_polygon_generic, +}; + +// ┌──────────────────────────────────────────────────────────┐ +// │ Generic Trait Distance Extension │ +// └──────────────────────────────────────────────────────────┘ + +use sedona_geo_traits_ext::*; + +/// Extension trait for generic geometry types to calculate distances directly +/// using Euclidean metric space without conversion overhead +/// Supports both same-type and cross-type distance calculations +pub trait DistanceExt { + /// Calculate Euclidean distance using generic traits without conversion overhead + fn distance_ext(&self, other: &Rhs) -> F; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ Generic trait macro implementations │ +// └──────────────────────────────────────────────────────────┘ + +/// Generic trait version of polygon-like geometry distance implementation +/// Follows the same pattern as impl_euclidean_distance_for_polygonlike_geometry! +macro_rules! impl_distance_ext_for_polygonlike_geometry_trait { + ($polygonlike_trait:ident, $polygonlike_tag:ident, [$(($geometry_trait:ident, $geometry_tag:ident)),*]) => { + // Self-to-self distance implementation + impl GenericDistanceTrait for LHS + where + F: GeoFloat, + LHS: $polygonlike_trait, + RHS: $polygonlike_trait, + { + fn generic_distance_trait(&self, rhs: &RHS) -> F { + let poly1 = self.to_polygon(); + let poly2 = rhs.to_polygon(); + distance_polygon_to_polygon_generic(&poly1, &poly2) + } + } + }; +} + +// Separate macro to generate individual implementations for each geometry type +macro_rules! impl_polygonlike_to_geometry_distance { + ($polygonlike_trait:ident, $polygonlike_tag:ident, $geometry_trait:ident, $geometry_tag:ident) => { + impl GenericDistanceTrait for PL + where + F: GeoFloat, + PL: $polygonlike_trait, + G: $geometry_trait, + { + fn generic_distance_trait(&self, rhs: &G) -> F { + let poly = self.to_polygon(); + impl_polygonlike_to_geometry_distance!(@call_distance poly, rhs, $geometry_tag) + } + } + }; + + (@call_distance $poly:expr, $rhs:expr, PointTag) => { + distance_point_to_polygon_generic($rhs, &$poly) + }; + (@call_distance $poly:expr, $rhs:expr, LineTag) => { + distance_line_to_polygon_generic($rhs, &$poly) + }; + (@call_distance $poly:expr, $rhs:expr, LineStringTag) => { + distance_linestring_to_polygon_generic($rhs, &$poly) + }; + (@call_distance $poly:expr, $rhs:expr, PolygonTag) => { + distance_polygon_to_polygon_generic(&$poly, $rhs) + }; + (@call_distance $poly:expr, $rhs:expr, RectTag) => { + { + let poly2 = $rhs.to_polygon(); + distance_polygon_to_polygon_generic(&$poly, &poly2) + } + }; + (@call_distance $poly:expr, $rhs:expr, TriangleTag) => { + { + let poly2 = $rhs.to_polygon(); + distance_polygon_to_polygon_generic(&$poly, &poly2) + } + }; + (@call_distance $poly:expr, $rhs:expr, MultiPointTag) => { + { + let mut min_dist: F = Bounded::max_value(); + for coord in $rhs.coord_iter() { + let point = Point::from(coord); + let dist = distance_point_to_polygon_generic(&point, &$poly); + min_dist = min_dist.min(dist); + } + if min_dist == Bounded::max_value() { + F::zero() + } else { + min_dist + } + } + }; + (@call_distance $poly:expr, $rhs:expr, MultiLineStringTag) => { + { + let mut min_dist: F = Bounded::max_value(); + for line_string in $rhs.line_strings_ext() { + let dist = distance_linestring_to_polygon_generic(&line_string, &$poly); + min_dist = min_dist.min(dist); + } + if min_dist == Bounded::max_value() { + F::zero() + } else { + min_dist + } + } + }; + (@call_distance $poly:expr, $rhs:expr, MultiPolygonTag) => { + { + let mut min_dist: F = Bounded::max_value(); + for polygon in $rhs.polygons_ext() { + let dist = distance_polygon_to_polygon_generic(&$poly, &polygon); + min_dist = min_dist.min(dist); + } + if min_dist == Bounded::max_value() { + F::zero() + } else { + min_dist + } + } + }; +} + +/// Generic trait version of multi-geometry distance implementation +/// Follows the same pattern as impl_euclidean_distance_for_iter_geometry! +macro_rules! impl_distance_ext_for_iter_geometry_trait { + ($iter_trait:ident, $iter_tag:ident, $member_method:ident) => { + impl GenericDistanceTrait for LHS + where + F: GeoFloat, + LHS: $iter_trait, + RHS: $iter_trait, + { + fn generic_distance_trait(&self, rhs: &RHS) -> F { + let mut min_dist: F = Float::max_value(); + for member1 in self.$member_method() { + for member2 in rhs.$member_method() { + let dist = member1.distance_ext(&member2); + min_dist = min_dist.min(dist); + } + } + if min_dist == Float::max_value() { + F::zero() + } else { + min_dist + } + } + } + }; +} + +// Array-based macro for systematic implementation generation +macro_rules! impl_cross_type_array { + // Generate multi-geometry self-implementations + (self_multi_geometries: [$(($trait:ident, $tag:ident, $method:ident)),+]) => { + $( + impl_distance_ext_for_iter_geometry_trait!($trait, $tag, $method); + )+ + }; + + // Generate single-to-multi implementations with Point to MultiPoint skip + (single_to_multi: $single_trait:ident, $single_tag:ident => [$(($multi_trait:ident, $multi_tag:ident, $method:ident)),+]) => { + $( + impl_cross_type_array!(@single_to_multi_check $single_trait, $single_tag, $multi_trait, $multi_tag, $method); + )+ + }; + + // Skip Point to MultiPoint (special implementation exists) + (@single_to_multi_check PointTraitExt, PointTag, MultiPointTraitExt, MultiPointTag, $method:ident) => {}; + + // Generate for all other combinations + (@single_to_multi_check $single_trait:ident, $single_tag:ident, $multi_trait:ident, $multi_tag:ident, $method:ident) => { + impl_single_to_multi_geometry_distance!($single_trait, $single_tag, $multi_trait, $multi_tag, $method); + }; + + // Generate symmetric implementations for single-to-multi + (symmetric_single_to_multi: $single_trait:ident, $single_tag:ident => [$(($multi_trait:ident, $multi_tag:ident)),+]) => { + $( + symmetric_distance_ext_trait_impl!(GeoFloat, $multi_trait, $multi_tag, $single_trait, $single_tag); + )+ + }; +} + +/// Macro for implementing cross-type distance calculations from single geometry to multi-geometry types +macro_rules! impl_single_to_multi_geometry_distance { + ($single_trait:ident, $single_tag:ident, $multi_trait:ident, $multi_tag:ident, $member_method:ident) => { + impl GenericDistanceTrait for S + where + F: GeoFloat, + S: $single_trait, + M: $multi_trait, + { + fn generic_distance_trait(&self, rhs: &M) -> F { + let mut min_dist: F = Bounded::max_value(); + for member in rhs.$member_method() { + let dist = self.distance_ext(&member); + min_dist = min_dist.min(dist); + } + if min_dist == Bounded::max_value() { + F::zero() + } else { + min_dist + } + } + } + }; +} + +// Implementation of DistanceExt for cross-type generic trait geometries using the two type-tag pattern +impl DistanceExt for LHS +where + F: GeoFloat, + LHS: GeoTraitExtWithTypeTag, + RHS: GeoTraitExtWithTypeTag, + LHS: GenericDistanceTrait, +{ + fn distance_ext(&self, other: &RHS) -> F { + self.generic_distance_trait(other) + } +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Internal trait for cross-type distance calculations │ +// └────────────────────────────────────────────────────────────┘ + +// Internal trait for cross-type distance calculations without conversion +trait GenericDistanceTrait +where + F: GeoFloat, +{ + fn generic_distance_trait(&self, rhs: &Rhs) -> F; +} +macro_rules! symmetric_distance_ext_trait_impl { + ($num_type:ident, $lhs_type:ident, $lhs_tag:ident, $rhs_type:ident, $rhs_tag:ident) => { + impl GenericDistanceTrait for LHS + where + F: $num_type, + LHS: $lhs_type, + RHS: $rhs_type, + { + fn generic_distance_trait(&self, rhs: &RHS) -> F { + rhs.generic_distance_trait(self) + } + } + }; +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementations for Coord (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Coord-to-Coord direct distance implementation +impl GenericDistanceTrait for LHS +where + F: GeoFloat, + LHS: CoordTraitExt, + RHS: CoordTraitExt, +{ + fn generic_distance_trait(&self, rhs: &RHS) -> F { + let delta = self.geo_coord() - rhs.geo_coord(); + delta.x.hypot(delta.y) + } +} + +// Coord-to-Point distance implementation +// The other side (Point-to-Coord) is handled via a symmetric impl or blanket impl +impl GenericDistanceTrait for LHS +where + F: GeoFloat, + LHS: CoordTraitExt, + RHS: PointTraitExt, +{ + fn generic_distance_trait(&self, rhs: &RHS) -> F { + if let Some(rhs_coord) = rhs.coord_ext() { + let delta = self.geo_coord() - rhs_coord.geo_coord(); + delta.x.hypot(delta.y) + } else { + F::zero() + } + } +} + +// Coord-to-Line distance implementation +impl GenericDistanceTrait for LHS +where + F: GeoFloat, + LHS: CoordTraitExt, + RHS: LineTraitExt, +{ + fn generic_distance_trait(&self, rhs: &RHS) -> F { + distance_coord_to_line_generic(self, rhs) + } +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementations for Point (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Point-to-Point direct distance implementation +impl GenericDistanceTrait for LHS +where + F: GeoFloat, + LHS: PointTraitExt, + RHS: PointTraitExt, +{ + fn generic_distance_trait(&self, rhs: &RHS) -> F { + distance_point_to_point_generic(self, rhs) + } +} + +// Point-to-Line distance implementation +impl GenericDistanceTrait for P +where + F: GeoFloat, + P: PointTraitExt, + L: LineTraitExt, +{ + fn generic_distance_trait(&self, rhs: &L) -> F { + if let Some(coord) = self.coord_ext() { + distance_coord_to_line_generic(&coord, rhs) + } else { + F::zero() + } + } +} + +// Point-to-LineString distance implementation +impl GenericDistanceTrait for P +where + F: GeoFloat, + P: PointTraitExt, + LS: LineStringTraitExt, +{ + fn generic_distance_trait(&self, rhs: &LS) -> F { + distance_point_to_linestring_generic(self, rhs) + } +} + +// Point-to-Polygon distance implementation +impl GenericDistanceTrait for P +where + F: GeoFloat, + P: PointTraitExt, + Poly: PolygonTraitExt, +{ + fn generic_distance_trait(&self, rhs: &Poly) -> F { + distance_point_to_polygon_generic(self, rhs) + } +} + +// Point to MultiPoint distance implementation +impl GenericDistanceTrait for P +where + F: GeoFloat, + P: PointTraitExt, + MP: MultiPointTraitExt, +{ + fn generic_distance_trait(&self, rhs: &MP) -> F { + if let Some(point_coord) = self.geo_coord() { + let mut min_dist: F = Bounded::max_value(); + for coord in rhs.coord_iter() { + let dist = point_coord.distance_ext(&coord); + min_dist = min_dist.min(dist); + } + if min_dist == Bounded::max_value() { + F::zero() + } else { + min_dist + } + } else { + F::zero() + } + } +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementations for Line (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Symmetric Line distance implementations +symmetric_distance_ext_trait_impl!(GeoFloat, LineTraitExt, LineTag, CoordTraitExt, CoordTag); +symmetric_distance_ext_trait_impl!(GeoFloat, LineTraitExt, LineTag, PointTraitExt, PointTag); + +// Line-to-Line direct distance implementation +impl GenericDistanceTrait for LHS +where + F: GeoFloat, + LHS: LineTraitExt, + RHS: LineTraitExt, +{ + fn generic_distance_trait(&self, rhs: &RHS) -> F { + distance_line_to_line_generic(self, rhs) + } +} + +// Line-to-LineString distance implementation +impl GenericDistanceTrait for L +where + F: GeoFloat, + L: LineTraitExt, + LS: LineStringTraitExt, +{ + fn generic_distance_trait(&self, rhs: &LS) -> F { + distance_line_to_linestring_generic(self, rhs) + } +} + +// Line-to-Polygon distance implementation +impl GenericDistanceTrait for L +where + F: GeoFloat, + L: LineTraitExt, + Poly: PolygonTraitExt, +{ + fn generic_distance_trait(&self, rhs: &Poly) -> F { + distance_line_to_polygon_generic(self, rhs) + } +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementations for LineString (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Symmetric LineString distance implementations +// LineString-to-Point (symmetric to Point-to-LineString) +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineStringTraitExt, + LineStringTag, + PointTraitExt, + PointTag +); + +// LineString-to-Line (symmetric to Line-to-LineString) +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineStringTraitExt, + LineStringTag, + LineTraitExt, + LineTag +); + +// LineString-to-LineString distance implementation +// This general implementation supports both same-type (LS to LS) and different-type (LS1 to LS2) +impl GenericDistanceTrait for LS1 +where + F: GeoFloat, + LS1: LineStringTraitExt, + LS2: LineStringTraitExt, +{ + fn generic_distance_trait(&self, rhs: &LS2) -> F { + let mut min_dist: F = Float::max_value(); + for line1 in self.lines() { + for line2 in rhs.lines() { + // Line-to-line distance using endpoints + let d1 = distance_coord_to_line_generic(&line1.start_coord(), &line2); + let d2 = distance_coord_to_line_generic(&line1.end_coord(), &line2); + let d3 = distance_coord_to_line_generic(&line2.start_coord(), &line1); + let d4 = distance_coord_to_line_generic(&line2.end_coord(), &line1); + let line_dist = d1.min(d2).min(d3).min(d4); + min_dist = min_dist.min(line_dist); + } + } + if min_dist == Float::max_value() { + F::zero() + } else { + min_dist + } + } +} + +// LineString-to-Polygon distance implementation +impl GenericDistanceTrait for LS +where + F: GeoFloat, + LS: LineStringTraitExt, + Poly: PolygonTraitExt, +{ + fn generic_distance_trait(&self, rhs: &Poly) -> F { + distance_linestring_to_polygon_generic(self, rhs) + } +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementations for Polygon (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Symmetric Polygon distance implementations +// Polygon-to-Point (symmetric to Point-to-Polygon) +symmetric_distance_ext_trait_impl!( + GeoFloat, + PolygonTraitExt, + PolygonTag, + PointTraitExt, + PointTag +); + +// Polygon-to-Line (symmetric to Line-to-Polygon) +symmetric_distance_ext_trait_impl!(GeoFloat, PolygonTraitExt, PolygonTag, LineTraitExt, LineTag); + +// Polygon-to-LineString (symmetric to LineString-to-Polygon) +symmetric_distance_ext_trait_impl!( + GeoFloat, + PolygonTraitExt, + PolygonTag, + LineStringTraitExt, + LineStringTag +); + +// Polygon-to-Polygon distance implementation +// This general implementation supports both same-type (P to P) and different-type (P1 to P2) +impl GenericDistanceTrait for P1 +where + F: GeoFloat, + P1: PolygonTraitExt, + P2: PolygonTraitExt, +{ + fn generic_distance_trait(&self, rhs: &P2) -> F { + distance_polygon_to_polygon_generic(self, rhs) + } +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementations for Rect and Triangle (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Triangle implementations +impl_distance_ext_for_polygonlike_geometry_trait!(TriangleTraitExt, TriangleTag, []); +impl_polygonlike_to_geometry_distance!(TriangleTraitExt, TriangleTag, PointTraitExt, PointTag); +impl_polygonlike_to_geometry_distance!(TriangleTraitExt, TriangleTag, LineTraitExt, LineTag); +impl_polygonlike_to_geometry_distance!( + TriangleTraitExt, + TriangleTag, + LineStringTraitExt, + LineStringTag +); +impl_polygonlike_to_geometry_distance!(TriangleTraitExt, TriangleTag, PolygonTraitExt, PolygonTag); +impl_polygonlike_to_geometry_distance!(TriangleTraitExt, TriangleTag, RectTraitExt, RectTag); +impl_polygonlike_to_geometry_distance!( + TriangleTraitExt, + TriangleTag, + MultiPointTraitExt, + MultiPointTag +); +impl_polygonlike_to_geometry_distance!( + TriangleTraitExt, + TriangleTag, + MultiLineStringTraitExt, + MultiLineStringTag +); +impl_polygonlike_to_geometry_distance!( + TriangleTraitExt, + TriangleTag, + MultiPolygonTraitExt, + MultiPolygonTag +); + +// Symmetric implementations for Triangle +symmetric_distance_ext_trait_impl!( + GeoFloat, + PointTraitExt, + PointTag, + TriangleTraitExt, + TriangleTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineTraitExt, + LineTag, + TriangleTraitExt, + TriangleTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineStringTraitExt, + LineStringTag, + TriangleTraitExt, + TriangleTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + PolygonTraitExt, + PolygonTag, + TriangleTraitExt, + TriangleTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + RectTraitExt, + RectTag, + TriangleTraitExt, + TriangleTag +); + +// Rect implementations +impl_distance_ext_for_polygonlike_geometry_trait!(RectTraitExt, RectTag, []); +impl_polygonlike_to_geometry_distance!(RectTraitExt, RectTag, PointTraitExt, PointTag); +impl_polygonlike_to_geometry_distance!(RectTraitExt, RectTag, LineTraitExt, LineTag); +impl_polygonlike_to_geometry_distance!(RectTraitExt, RectTag, LineStringTraitExt, LineStringTag); +impl_polygonlike_to_geometry_distance!(RectTraitExt, RectTag, PolygonTraitExt, PolygonTag); +impl_polygonlike_to_geometry_distance!(RectTraitExt, RectTag, MultiPointTraitExt, MultiPointTag); +impl_polygonlike_to_geometry_distance!( + RectTraitExt, + RectTag, + MultiLineStringTraitExt, + MultiLineStringTag +); +impl_polygonlike_to_geometry_distance!( + RectTraitExt, + RectTag, + MultiPolygonTraitExt, + MultiPolygonTag +); + +// Symmetric implementations for Rect (excluding Triangle which is already handled above) +symmetric_distance_ext_trait_impl!(GeoFloat, PointTraitExt, PointTag, RectTraitExt, RectTag); +symmetric_distance_ext_trait_impl!(GeoFloat, LineTraitExt, LineTag, RectTraitExt, RectTag); +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineStringTraitExt, + LineStringTag, + RectTraitExt, + RectTag +); +symmetric_distance_ext_trait_impl!(GeoFloat, PolygonTraitExt, PolygonTag, RectTraitExt, RectTag); + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementations for multi-geometry types (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Multi-geometry self-implementations +impl_cross_type_array!(self_multi_geometries: [ + (MultiPointTraitExt, MultiPointTag, points_ext), + (MultiLineStringTraitExt, MultiLineStringTag, line_strings_ext), + (MultiPolygonTraitExt, MultiPolygonTag, polygons_ext) +]); + +// Single-geometry to multi-geometry implementations +impl_cross_type_array!(single_to_multi: PointTraitExt, PointTag => [ + (MultiPointTraitExt, MultiPointTag, points_ext), + (MultiLineStringTraitExt, MultiLineStringTag, line_strings_ext), + (MultiPolygonTraitExt, MultiPolygonTag, polygons_ext) +]); + +impl_cross_type_array!(single_to_multi: LineTraitExt, LineTag => [ + (MultiPointTraitExt, MultiPointTag, points_ext), + (MultiLineStringTraitExt, MultiLineStringTag, line_strings_ext), + (MultiPolygonTraitExt, MultiPolygonTag, polygons_ext) +]); + +impl_cross_type_array!(single_to_multi: LineStringTraitExt, LineStringTag => [ + (MultiPointTraitExt, MultiPointTag, points_ext), + (MultiLineStringTraitExt, MultiLineStringTag, line_strings_ext), + (MultiPolygonTraitExt, MultiPolygonTag, polygons_ext) +]); + +impl_cross_type_array!(single_to_multi: PolygonTraitExt, PolygonTag => [ + (MultiPointTraitExt, MultiPointTag, points_ext), + (MultiLineStringTraitExt, MultiLineStringTag, line_strings_ext), + (MultiPolygonTraitExt, MultiPolygonTag, polygons_ext) +]); + +// Multi-geometry to multi-geometry implementations +impl_single_to_multi_geometry_distance!( + MultiPointTraitExt, + MultiPointTag, + MultiLineStringTraitExt, + MultiLineStringTag, + line_strings_ext +); +impl_single_to_multi_geometry_distance!( + MultiPointTraitExt, + MultiPointTag, + MultiPolygonTraitExt, + MultiPolygonTag, + polygons_ext +); +impl_single_to_multi_geometry_distance!( + MultiLineStringTraitExt, + MultiLineStringTag, + MultiPointTraitExt, + MultiPointTag, + points_ext +); +impl_single_to_multi_geometry_distance!( + MultiLineStringTraitExt, + MultiLineStringTag, + MultiPolygonTraitExt, + MultiPolygonTag, + polygons_ext +); +impl_single_to_multi_geometry_distance!( + MultiPolygonTraitExt, + MultiPolygonTag, + MultiPointTraitExt, + MultiPointTag, + points_ext +); +impl_single_to_multi_geometry_distance!( + MultiPolygonTraitExt, + MultiPolygonTag, + MultiLineStringTraitExt, + MultiLineStringTag, + line_strings_ext +); + +// Symmetric implementations +impl_cross_type_array!(symmetric_single_to_multi: PointTraitExt, PointTag => [ + (MultiPointTraitExt, MultiPointTag), + (MultiLineStringTraitExt, MultiLineStringTag), + (MultiPolygonTraitExt, MultiPolygonTag) +]); + +impl_cross_type_array!(symmetric_single_to_multi: LineTraitExt, LineTag => [ + (MultiPointTraitExt, MultiPointTag), + (MultiLineStringTraitExt, MultiLineStringTag), + (MultiPolygonTraitExt, MultiPolygonTag) +]); + +impl_cross_type_array!(symmetric_single_to_multi: LineStringTraitExt, LineStringTag => [ + (MultiPointTraitExt, MultiPointTag), + (MultiLineStringTraitExt, MultiLineStringTag), + (MultiPolygonTraitExt, MultiPolygonTag) +]); + +impl_cross_type_array!(symmetric_single_to_multi: PolygonTraitExt, PolygonTag => [ + (MultiPointTraitExt, MultiPointTag), + (MultiLineStringTraitExt, MultiLineStringTag), + (MultiPolygonTraitExt, MultiPolygonTag) +]); + +impl_cross_type_array!(symmetric_single_to_multi: RectTraitExt, RectTag => [ + (MultiPointTraitExt, MultiPointTag), + (MultiLineStringTraitExt, MultiLineStringTag), + (MultiPolygonTraitExt, MultiPolygonTag) +]); + +impl_cross_type_array!(symmetric_single_to_multi: TriangleTraitExt, TriangleTag => [ + (MultiPointTraitExt, MultiPointTag), + (MultiLineStringTraitExt, MultiLineStringTag), + (MultiPolygonTraitExt, MultiPolygonTag) +]); + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementation for GeometryCollection (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Generate implementations for GeometryCollection by delegating to the Geometry implementation +macro_rules! impl_distance_geometry_collection_from_geometry { + ($rhs_type:ident, $rhs_tag:ident) => { + impl GenericDistanceTrait for LHS + where + F: GeoFloat, + LHS: GeometryCollectionTraitExt, + RHS: $rhs_type, + { + fn generic_distance_trait(&self, rhs: &RHS) -> F { + use num_traits::Bounded; + + // Use distance_ext which will route through the appropriate implementations + // The key insight is that this works for all geometry types except GeometryCollection, + // where we need special handling to avoid infinite recursion + self.geometries_ext() + .map(|geom| geom.distance_ext(rhs)) + .fold(Bounded::max_value(), |acc, dist| acc.min(dist)) + } + } + }; +} + +impl_distance_geometry_collection_from_geometry!(PointTraitExt, PointTag); +impl_distance_geometry_collection_from_geometry!(LineTraitExt, LineTag); +impl_distance_geometry_collection_from_geometry!(LineStringTraitExt, LineStringTag); +impl_distance_geometry_collection_from_geometry!(PolygonTraitExt, PolygonTag); +impl_distance_geometry_collection_from_geometry!(MultiPointTraitExt, MultiPointTag); +impl_distance_geometry_collection_from_geometry!(MultiLineStringTraitExt, MultiLineStringTag); +impl_distance_geometry_collection_from_geometry!(MultiPolygonTraitExt, MultiPolygonTag); +impl_distance_geometry_collection_from_geometry!(RectTraitExt, RectTag); +impl_distance_geometry_collection_from_geometry!(TriangleTraitExt, TriangleTag); + +impl GenericDistanceTrait for LHS +where + F: GeoFloat, + LHS: GeometryCollectionTraitExt, + RHS: GeometryCollectionTraitExt, +{ + fn generic_distance_trait(&self, rhs: &RHS) -> F { + let mut min_distance = ::max_value(); + for lhs_geom in self.geometries_ext() { + for rhs_geom in rhs.geometries_ext() { + let distance = lhs_geom.distance_ext(&rhs_geom); + min_distance = min_distance.min(distance); + + // Early exit optimization + if distance == F::zero() { + return F::zero(); + } + } + } + + min_distance + } +} + +symmetric_distance_ext_trait_impl!( + GeoFloat, + PointTraitExt, + PointTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineTraitExt, + LineTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineStringTraitExt, + LineStringTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + PolygonTraitExt, + PolygonTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + MultiPointTraitExt, + MultiPointTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + MultiLineStringTraitExt, + MultiLineStringTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + MultiPolygonTraitExt, + MultiPolygonTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + RectTraitExt, + RectTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + TriangleTraitExt, + TriangleTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementation for Geometry (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Generate implementations for Geometry with other types using conversion +macro_rules! impl_distance_geometry_to_type { + ($rhs_type:ident, $rhs_tag:ident) => { + impl GenericDistanceTrait for LHS + where + F: GeoFloat, + LHS: GeometryTraitExt, + RHS: $rhs_type, + { + fn generic_distance_trait(&self, rhs: &RHS) -> F { + if self.is_collection() { + let mut min_distance = ::max_value(); + for lhs_geom in self.geometries_ext() { + let lhs_geom = lhs_geom.borrow(); + let distance = lhs_geom.generic_distance_trait(rhs); + min_distance = min_distance.min(distance); + + // Early exit optimization + if distance == F::zero() { + return F::zero(); + } + } + min_distance + } else { + match self.as_type_ext() { + sedona_geo_traits_ext::GeometryTypeExt::Point(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::Line(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::LineString(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::Polygon(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::MultiPoint(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::MultiLineString(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::MultiPolygon(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::Rect(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::Triangle(g) => { + g.generic_distance_trait(rhs) + } + } + } + } + } + }; +} + +impl_distance_geometry_to_type!(PointTraitExt, PointTag); +impl_distance_geometry_to_type!(LineTraitExt, LineTag); +impl_distance_geometry_to_type!(LineStringTraitExt, LineStringTag); +impl_distance_geometry_to_type!(PolygonTraitExt, PolygonTag); +impl_distance_geometry_to_type!(MultiPointTraitExt, MultiPointTag); +impl_distance_geometry_to_type!(MultiLineStringTraitExt, MultiLineStringTag); +impl_distance_geometry_to_type!(MultiPolygonTraitExt, MultiPolygonTag); +impl_distance_geometry_to_type!(RectTraitExt, RectTag); +impl_distance_geometry_to_type!(TriangleTraitExt, TriangleTag); +impl_distance_geometry_to_type!(GeometryCollectionTraitExt, GeometryCollectionTag); + +symmetric_distance_ext_trait_impl!( + GeoFloat, + PointTraitExt, + PointTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineTraitExt, + LineTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineStringTraitExt, + LineStringTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + PolygonTraitExt, + PolygonTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + MultiPointTraitExt, + MultiPointTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + MultiLineStringTraitExt, + MultiLineStringTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + MultiPolygonTraitExt, + MultiPolygonTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + RectTraitExt, + RectTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + TriangleTraitExt, + TriangleTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + GeometryCollectionTraitExt, + GeometryCollectionTag, + GeometryTraitExt, + GeometryTag +); + +impl GenericDistanceTrait for LHS +where + F: GeoFloat, + LHS: GeometryTraitExt, + RHS: GeometryTraitExt, +{ + fn generic_distance_trait(&self, rhs: &RHS) -> F { + if self.is_collection() { + let mut min_distance = ::max_value(); + for lhs_geom in self.geometries_ext() { + let lhs_geom = lhs_geom.borrow(); + let distance = lhs_geom.generic_distance_trait(rhs); + min_distance = min_distance.min(distance); + + // Early exit optimization + if distance == F::zero() { + return F::zero(); + } + } + min_distance + } else { + match self.as_type_ext() { + sedona_geo_traits_ext::GeometryTypeExt::Point(g) => g.generic_distance_trait(rhs), + sedona_geo_traits_ext::GeometryTypeExt::Line(g) => g.generic_distance_trait(rhs), + sedona_geo_traits_ext::GeometryTypeExt::LineString(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::Polygon(g) => g.generic_distance_trait(rhs), + sedona_geo_traits_ext::GeometryTypeExt::MultiPoint(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::MultiLineString(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::MultiPolygon(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::Rect(g) => g.generic_distance_trait(rhs), + sedona_geo_traits_ext::GeometryTypeExt::Triangle(g) => { + g.generic_distance_trait(rhs) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Line, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon}; + use geo::orient::{Direction, Orient}; + use geo_types::{coord, polygon, private_utils::line_segment_distance}; + + mod distance_cross_validation_tests { + use geo::{Coord, Distance, Euclidean, Geometry, GeometryCollection, Rect, Triangle}; + + use super::*; + + #[test] + fn line_segment_distance_test() { + let o1 = Point::new(8.0, 0.0); + let o2 = Point::new(5.5, 0.0); + let o3 = Point::new(5.0, 0.0); + let o4 = Point::new(4.5, 1.5); + + let p1 = Point::new(7.2, 2.0); + let p2 = Point::new(6.0, 1.0); + + // Test original implementation + let dist = line_segment_distance(o1, p1, p2); + let dist2 = line_segment_distance(o2, p1, p2); + let dist3 = line_segment_distance(o3, p1, p2); + let dist4 = line_segment_distance(o4, p1, p2); + // Results agree with Shapely + assert_relative_eq!(dist, 2.0485900789263356); + assert_relative_eq!(dist2, 1.118033988749895); + assert_relative_eq!(dist3, std::f64::consts::SQRT_2); // workaround clippy::correctness error approx_constant (1.4142135623730951) + assert_relative_eq!(dist4, 1.5811388300841898); + // Point is on the line + let zero_dist = line_segment_distance(p1, p1, p2); + assert_relative_eq!(zero_dist, 0.0); + + // Test generic implementation + if let (Some(p1_coord), Some(p2_coord)) = (p1.coord_ext(), p2.coord_ext()) { + let line_seg = Line::new(p1_coord, p2_coord); + + if let Some(o1_coord) = o1.coord_ext() { + let generic_dist = distance_coord_to_line_generic(&o1_coord, &line_seg); + assert_relative_eq!(generic_dist, 2.0485900789263356); + assert_relative_eq!(dist, generic_dist); + } + if let Some(o2_coord) = o2.coord_ext() { + let generic_dist2 = distance_coord_to_line_generic(&o2_coord, &line_seg); + assert_relative_eq!(generic_dist2, 1.118033988749895); + assert_relative_eq!(dist2, generic_dist2); + } + if let Some(o3_coord) = o3.coord_ext() { + let generic_dist3 = distance_coord_to_line_generic(&o3_coord, &line_seg); + assert_relative_eq!(generic_dist3, std::f64::consts::SQRT_2); + assert_relative_eq!(dist3, generic_dist3); + } + if let Some(o4_coord) = o4.coord_ext() { + let generic_dist4 = distance_coord_to_line_generic(&o4_coord, &line_seg); + assert_relative_eq!(generic_dist4, 1.5811388300841898); + assert_relative_eq!(dist4, generic_dist4); + } + if let Some(p1_coord_zero) = p1.coord_ext() { + let generic_zero_dist = + distance_coord_to_line_generic(&p1_coord_zero, &line_seg); + assert_relative_eq!(generic_zero_dist, 0.0); + assert_relative_eq!(zero_dist, generic_zero_dist); + } + } + } + #[test] + // Point to Polygon, outside point + fn point_polygon_distance_outside_test() { + // an octagon + let points = vec![ + (5., 1.), + (4., 2.), + (4., 3.), + (5., 4.), + (6., 4.), + (7., 3.), + (7., 2.), + (6., 1.), + (5., 1.), + ]; + let ls = LineString::from(points); + let poly = Polygon::new(ls, vec![]); + // A Random point outside the octagon + let p = Point::new(2.5, 0.5); + + // Test original implementation + let dist = Euclidean.distance(&p, &poly); + assert_relative_eq!(dist, 2.1213203435596424); + + // Test generic implementation + let generic_dist = p.distance_ext(&poly); + assert_relative_eq!(generic_dist, 2.1213203435596424); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + // Point to Polygon, inside point + fn point_polygon_distance_inside_test() { + // an octagon + let points = vec![ + (5., 1.), + (4., 2.), + (4., 3.), + (5., 4.), + (6., 4.), + (7., 3.), + (7., 2.), + (6., 1.), + (5., 1.), + ]; + let ls = LineString::from(points); + let poly = Polygon::new(ls, vec![]); + // A Random point inside the octagon + let p = Point::new(5.5, 2.1); + + // Test original implementation + let dist = Euclidean.distance(&p, &poly); + assert_relative_eq!(dist, 0.0); + + // Test generic implementation + let generic_dist = p.distance_ext(&poly); + assert_relative_eq!(generic_dist, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + // Point to Polygon, on boundary + fn point_polygon_distance_boundary_test() { + // an octagon + let points = vec![ + (5., 1.), + (4., 2.), + (4., 3.), + (5., 4.), + (6., 4.), + (7., 3.), + (7., 2.), + (6., 1.), + (5., 1.), + ]; + let ls = LineString::from(points); + let poly = Polygon::new(ls, vec![]); + // A point on the octagon + let p = Point::new(5.0, 1.0); + + // Test original implementation + let dist = Euclidean.distance(&p, &poly); + assert_relative_eq!(dist, 0.0); + + // Test generic implementation + let generic_dist = p.distance_ext(&poly); + assert_relative_eq!(generic_dist, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + // Point to Polygon, on boundary + fn point_polygon_boundary_test2() { + let exterior = LineString::from(vec![ + (0., 0.), + (0., 0.0004), + (0.0004, 0.0004), + (0.0004, 0.), + (0., 0.), + ]); + + let poly = Polygon::new(exterior, vec![]); + let bugged_point = Point::new(0.0001, 0.); + + // Test original implementation + let distance = Euclidean.distance(&poly, &bugged_point); + assert_relative_eq!(distance, 0.); + + // Test generic implementation + let generic_distance = poly.distance_ext(&bugged_point); + assert_relative_eq!(generic_distance, 0.); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // Point to Polygon, empty Polygon + fn point_polygon_empty_test() { + // an empty Polygon + let points = vec![]; + let ls = LineString::new(points); + let poly = Polygon::new(ls, vec![]); + // A point on the octagon + let p = Point::new(2.5, 0.5); + + // Test original implementation + let dist = Euclidean.distance(&p, &poly); + assert_relative_eq!(dist, 0.0); + + // Test generic implementation + let generic_dist = p.distance_ext(&poly); + assert_relative_eq!(generic_dist, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + // Point to Polygon with an interior ring + fn point_polygon_interior_cutout_test() { + // an octagon + let ext_points = vec![ + (4., 1.), + (5., 2.), + (5., 3.), + (4., 4.), + (3., 4.), + (2., 3.), + (2., 2.), + (3., 1.), + (4., 1.), + ]; + // cut out a triangle inside octagon + let int_points = vec![(3.5, 3.5), (4.4, 1.5), (2.6, 1.5), (3.5, 3.5)]; + let ls_ext = LineString::from(ext_points); + let ls_int = LineString::from(int_points); + let poly = Polygon::new(ls_ext, vec![ls_int]); + // A point inside the cutout triangle + let p = Point::new(3.5, 2.5); + + // Test original implementation + let dist = Euclidean.distance(&p, &poly); + // 0.41036467732879783 <-- Shapely + assert_relative_eq!(dist, 0.41036467732879767); + + // Test generic implementation + let generic_dist = p.distance_ext(&poly); + assert_relative_eq!(generic_dist, 0.41036467732879767); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + + #[test] + fn line_distance_multipolygon_do_not_intersect_test() { + // checks that the distance from the multipolygon + // is equal to the distance from the closest polygon + // taken in isolation, whatever that distance is + let ls1 = LineString::from(vec![ + (0.0, 0.0), + (10.0, 0.0), + (10.0, 10.0), + (5.0, 15.0), + (0.0, 10.0), + (0.0, 0.0), + ]); + let ls2 = LineString::from(vec![ + (0.0, 30.0), + (0.0, 25.0), + (10.0, 25.0), + (10.0, 30.0), + (0.0, 30.0), + ]); + let ls3 = LineString::from(vec![ + (15.0, 30.0), + (15.0, 25.0), + (20.0, 25.0), + (20.0, 30.0), + (15.0, 30.0), + ]); + let pol1 = Polygon::new(ls1, vec![]); + let pol2 = Polygon::new(ls2, vec![]); + let pol3 = Polygon::new(ls3, vec![]); + let mp = MultiPolygon::new(vec![pol1.clone(), pol2, pol3]); + let pnt1 = Point::new(0.0, 15.0); + let pnt2 = Point::new(10.0, 20.0); + let ln = Line::new(pnt1.0, pnt2.0); + + // Test original implementation + let dist_mp_ln = Euclidean.distance(&ln, &mp); + let dist_pol1_ln = Euclidean.distance(&ln, &pol1); + assert_relative_eq!(dist_mp_ln, dist_pol1_ln); + + // Test generic implementation - compare line to polygon + let generic_dist_pol1_ln = ln.distance_ext(&pol1); + assert_relative_eq!(generic_dist_pol1_ln, dist_pol1_ln); + + // Ensure both implementations agree for the single polygon case + assert_relative_eq!(dist_pol1_ln, generic_dist_pol1_ln); + } + + #[test] + fn point_distance_multipolygon_test() { + 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 mp = MultiPolygon::new(vec![p1.clone(), p2.clone()]); + let p = Point::new(50.0, 50.0); + + // Test original implementation + let distance = Euclidean.distance(&p, &mp); + assert_relative_eq!(distance, 60.959002616512684); + + // Test generic implementation + let generic_dist = mp.distance_ext(&p); + assert_relative_eq!(generic_dist, 60.959002616512684); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_dist); + } + #[test] + // Point to LineString + fn point_linestring_distance_test() { + // like an octagon, but missing the lowest horizontal segment + let points = vec![ + (5., 1.), + (4., 2.), + (4., 3.), + (5., 4.), + (6., 4.), + (7., 3.), + (7., 2.), + (6., 1.), + ]; + let ls = LineString::from(points); + // A Random point "inside" the LineString + let p = Point::new(5.5, 2.1); + + // Test original implementation + let dist = Euclidean.distance(&p, &ls); + assert_relative_eq!(dist, 1.1313708498984762); + + // Test generic implementation + let generic_dist = p.distance_ext(&ls); + assert_relative_eq!(generic_dist, 1.1313708498984762); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + // Point to LineString, point lies on the LineString + fn point_linestring_contains_test() { + // like an octagon, but missing the lowest horizontal segment + let points = vec![ + (5., 1.), + (4., 2.), + (4., 3.), + (5., 4.), + (6., 4.), + (7., 3.), + (7., 2.), + (6., 1.), + ]; + let ls = LineString::from(points); + // A point which lies on the LineString + let p = Point::new(5.0, 4.0); + + // Test original implementation + let dist = Euclidean.distance(&p, &ls); + assert_relative_eq!(dist, 0.0); + + // Test generic implementation + let generic_dist = p.distance_ext(&ls); + assert_relative_eq!(generic_dist, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + // Point to LineString, closed triangle + fn point_linestring_triangle_test() { + let points = vec![(3.5, 3.5), (4.4, 2.0), (2.6, 2.0), (3.5, 3.5)]; + let ls = LineString::from(points); + let p = Point::new(3.5, 2.5); + + // Test original implementation + let dist = Euclidean.distance(&p, &ls); + assert_relative_eq!(dist, 0.5); + + // Test generic implementation + let generic_dist = p.distance_ext(&ls); + assert_relative_eq!(generic_dist, 0.5); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + // Point to LineString, empty LineString + fn point_linestring_empty_test() { + let points = vec![]; + let ls = LineString::new(points); + let p = Point::new(5.0, 4.0); + + // Test original implementation + let dist = Euclidean.distance(&p, &ls); + assert_relative_eq!(dist, 0.0); + + // Test generic implementation + let generic_dist = p.distance_ext(&ls); + assert_relative_eq!(generic_dist, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + fn distance_multilinestring_test() { + 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.clone(), v2.clone()]); + let p = Point::new(50.0, 50.0); + + // Test original implementation + let distance = Euclidean.distance(&p, &mls); + assert_relative_eq!(distance, 63.25345840347388); + + // Test generic implementation + let generic_dist = p.distance_ext(&mls); + assert_relative_eq!(generic_dist, 63.25345840347388); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_dist); + } + #[test] + fn distance1_test() { + let p1 = Point::new(0., 0.); + let p2 = Point::new(1., 0.); + + // Test original implementation + let distance = Euclidean.distance(&p1, &p2); + assert_relative_eq!(distance, 1.); + + // Test generic implementation + let generic_distance = distance_point_to_point_generic(&p1, &p2); + assert_relative_eq!(generic_distance, 1.); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + fn distance2_test() { + let p1 = Point::new(-72.1235, 42.3521); + let p2 = Point::new(72.1260, 70.612); + + // Test original implementation + let dist = Euclidean.distance(&p1, &p2); + assert_relative_eq!(dist, 146.99163308930207); + + // Test generic implementation + let generic_dist = distance_point_to_point_generic(&p1, &p2); + assert_relative_eq!(generic_dist, 146.99163308930207); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + fn distance_multipoint_test() { + 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 mp = MultiPoint::new(v.clone()); + let p = Point::new(50.0, 50.0); + + // Test original implementation + let distance = Euclidean.distance(&p, &mp); + assert_relative_eq!(distance, 64.03124237432849); + + let generic_dist = mp.distance_ext(&p); + // Ensure both implementations agree + assert_relative_eq!(distance, generic_dist); + } + #[test] + fn distance_line_test() { + let line0 = Line::from([(0., 0.), (5., 0.)]); + let p0 = Point::new(2., 3.); + let p1 = Point::new(3., 0.); + let p2 = Point::new(6., 0.); + + // Test original implementation + let dist_line_p0 = Euclidean.distance(&line0, &p0); + let dist_p0_line = Euclidean.distance(&p0, &line0); + assert_relative_eq!(dist_line_p0, 3.); + assert_relative_eq!(dist_p0_line, 3.); + + let dist_line_p1 = Euclidean.distance(&line0, &p1); + let dist_p1_line = Euclidean.distance(&p1, &line0); + assert_relative_eq!(dist_line_p1, 0.); + assert_relative_eq!(dist_p1_line, 0.); + + let dist_line_p2 = Euclidean.distance(&line0, &p2); + let dist_p2_line = Euclidean.distance(&p2, &line0); + assert_relative_eq!(dist_line_p2, 1.); + assert_relative_eq!(dist_p2_line, 1.); + + // Test generic implementation + let generic_dist_p0 = if let Some(coord) = p0.coord_ext() { + distance_coord_to_line_generic(&coord, &line0) + } else { + 0.0 + }; + let generic_dist_p1 = if let Some(coord) = p1.coord_ext() { + distance_coord_to_line_generic(&coord, &line0) + } else { + 0.0 + }; + let generic_dist_p2 = if let Some(coord) = p2.coord_ext() { + distance_coord_to_line_generic(&coord, &line0) + } else { + 0.0 + }; + + assert_relative_eq!(generic_dist_p0, 3.); + assert_relative_eq!(generic_dist_p1, 0.); + assert_relative_eq!(generic_dist_p2, 1.); + + // Ensure both implementations agree + assert_relative_eq!(dist_line_p0, generic_dist_p0); + assert_relative_eq!(dist_p0_line, generic_dist_p0); + assert_relative_eq!(dist_line_p1, generic_dist_p1); + assert_relative_eq!(dist_p1_line, generic_dist_p1); + assert_relative_eq!(dist_line_p2, generic_dist_p2); + assert_relative_eq!(dist_p2_line, generic_dist_p2); + } + #[test] + fn distance_line_line_test() { + let line0 = Line::from([(0., 0.), (5., 0.)]); + let line1 = Line::from([(2., 1.), (7., 2.)]); + + // Test original implementation + let distance01 = Euclidean.distance(&line0, &line1); + let distance10 = Euclidean.distance(&line1, &line0); + assert_relative_eq!(distance01, 1.); + assert_relative_eq!(distance10, 1.); + + // Test generic implementation + let generic_distance01 = line0.distance_ext(&line1); + let generic_distance10 = line1.distance_ext(&line0); + assert_relative_eq!(generic_distance01, 1.); + assert_relative_eq!(generic_distance10, 1.); + + // Ensure both implementations agree + assert_relative_eq!(distance01, generic_distance01); + assert_relative_eq!(distance10, generic_distance10); + } + #[test] + // See https://github.com/georust/geo/issues/476 + fn distance_line_polygon_test() { + let line = Line::new( + coord! { + x: -0.17084137691985102, + y: 0.8748085493016657, + }, + coord! { + x: -0.17084137691985102, + y: 0.09858870312437906, + }, + ); + let poly: Polygon = polygon![ + coord! { + x: -0.10781391405721802, + y: -0.15433610862574643, + }, + coord! { + x: -0.7855276236615211, + y: 0.23694208404779793, + }, + coord! { + x: -0.7855276236615214, + y: -0.5456143012992907, + }, + coord! { + x: -0.10781391405721802, + y: -0.15433610862574643, + }, + ]; + + // Test original implementation + let distance = Euclidean.distance(&line, &poly); + assert_eq!(distance, 0.18752558079168907); + + // Test generic implementation + let generic_distance = line.distance_ext(&poly); + assert_relative_eq!(generic_distance, 0.18752558079168907); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // test edge-vertex minimum distance + fn test_minimum_polygon_distance() { + let points_raw = [ + (126., 232.), + (126., 212.), + (112., 202.), + (97., 204.), + (87., 215.), + (87., 232.), + (100., 246.), + (118., 247.), + ]; + let points = points_raw + .iter() + .map(|e| Point::new(e.0, e.1)) + .collect::>(); + let poly1 = Polygon::new(LineString::from(points), vec![]); + + let points_raw_2 = [ + (188., 231.), + (189., 207.), + (174., 196.), + (164., 196.), + (147., 220.), + (158., 242.), + (177., 242.), + ]; + let points2 = points_raw_2 + .iter() + .map(|e| Point::new(e.0, e.1)) + .collect::>(); + let poly2 = Polygon::new(LineString::from(points2), vec![]); + + // Test generic implementation + let generic_dist = poly1.exterior().distance_ext(poly2.exterior()); + assert_relative_eq!(generic_dist, 21.0); + } + #[test] + // test vertex-vertex minimum distance + fn test_minimum_polygon_distance_2() { + let points_raw = [ + (118., 200.), + (153., 179.), + (106., 155.), + (88., 190.), + (118., 200.), + ]; + let points = points_raw + .iter() + .map(|e| Point::new(e.0, e.1)) + .collect::>(); + let poly1 = Polygon::new(LineString::from(points), vec![]); + + let points_raw_2 = [ + (242., 186.), + (260., 146.), + (182., 175.), + (216., 193.), + (242., 186.), + ]; + let points2 = points_raw_2 + .iter() + .map(|e| Point::new(e.0, e.1)) + .collect::>(); + let poly2 = Polygon::new(LineString::from(points2), vec![]); + + // Test generic implementation + let generic_dist = poly1.exterior().distance_ext(poly2.exterior()); + assert_relative_eq!(generic_dist, 29.274562336608895); + } + #[test] + // test edge-edge minimum distance + fn test_minimum_polygon_distance_3() { + let points_raw = [ + (182., 182.), + (182., 168.), + (138., 160.), + (136., 193.), + (182., 182.), + ]; + let points = points_raw + .iter() + .map(|e| Point::new(e.0, e.1)) + .collect::>(); + let poly1 = Polygon::new(LineString::from(points), vec![]); + + let points_raw_2 = [ + (232., 196.), + (234., 150.), + (194., 165.), + (194., 191.), + (232., 196.), + ]; + let points2 = points_raw_2 + .iter() + .map(|e| Point::new(e.0, e.1)) + .collect::>(); + let poly2 = Polygon::new(LineString::from(points2), vec![]); + + // Test generic implementation + let generic_dist = poly1.exterior().distance_ext(poly2.exterior()); + assert_relative_eq!(generic_dist, 12.0); + } + #[test] + fn test_large_polygon_distance() { + let ls = sedona_testing::fixtures::norway_main::(); + let poly1 = Polygon::new(ls, vec![]); + let vec2 = vec![ + (4.921875, 66.33750501996518), + (3.69140625, 65.21989393613207), + (6.15234375, 65.07213008560697), + (4.921875, 66.33750501996518), + ]; + let poly2 = Polygon::new(vec2.into(), vec![]); + + // Test original implementation + let distance = Euclidean.distance(&poly1, &poly2); + // GEOS says 2.2864896295566055 + assert_relative_eq!(distance, 2.2864896295566055); + + // Test generic implementation + let generic_distance = poly1.distance_ext(&poly2); + assert_relative_eq!(generic_distance, 2.2864896295566055); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // A polygon inside another polygon's ring; they're disjoint in the DE-9IM sense: + // FF2FF1212 + fn test_poly_in_ring() { + let shell = sedona_testing::fixtures::shell::(); + let ring = sedona_testing::fixtures::ring::(); + let poly_in_ring = sedona_testing::fixtures::poly_in_ring::(); + // inside is "inside" outside's ring, but they are disjoint + let outside = Polygon::new(shell, vec![ring]); + let inside = Polygon::new(poly_in_ring, vec![]); + + // Test original implementation + let distance = Euclidean.distance(&outside, &inside); + assert_relative_eq!(distance, 5.992772737231033); + + // Test generic implementation + let generic_distance = outside.distance_ext(&inside); + assert_relative_eq!(generic_distance, 5.992772737231033); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // two ring LineStrings; one encloses the other but they neither touch nor intersect + fn test_linestring_distance() { + let ring = sedona_testing::fixtures::ring::(); + let poly_in_ring = sedona_testing::fixtures::poly_in_ring::(); + + // Test original implementation + let distance = Euclidean.distance(&ring, &poly_in_ring); + assert_relative_eq!(distance, 5.992772737231033); + + // Test generic implementation + let generic_distance = ring.distance_ext(&poly_in_ring); + assert_relative_eq!(generic_distance, 5.992772737231033); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // Line-Polygon test: closest point on Polygon is NOT nearest to a Line end-point + fn test_line_polygon_simple() { + let line = Line::from([(0.0, 0.0), (0.0, 3.0)]); + let v = vec![(5.0, 1.0), (5.0, 2.0), (0.25, 1.5), (5.0, 1.0)]; + let poly = Polygon::new(v.into(), vec![]); + + // Test original implementation + let distance = Euclidean.distance(&line, &poly); + assert_relative_eq!(distance, 0.25); + + // Test generic implementation + let generic_distance = line.distance_ext(&poly); + assert_relative_eq!(generic_distance, 0.25); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // Line-Polygon test: Line intersects Polygon + fn test_line_polygon_intersects() { + let line = Line::from([(0.5, 0.0), (0.0, 3.0)]); + let v = vec![(5.0, 1.0), (5.0, 2.0), (0.25, 1.5), (5.0, 1.0)]; + let poly = Polygon::new(v.into(), vec![]); + + // Test original implementation + let distance = Euclidean.distance(&line, &poly); + assert_relative_eq!(distance, 0.0); + + // Test generic implementation + let generic_distance = line.distance_ext(&poly); + assert_relative_eq!(generic_distance, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // Line-Polygon test: Line contained by interior ring + fn test_line_polygon_inside_ring() { + let line = Line::from([(4.4, 1.5), (4.45, 1.5)]); + let v = vec![(5.0, 1.0), (5.0, 2.0), (0.25, 1.0), (5.0, 1.0)]; + let v2 = vec![(4.5, 1.2), (4.5, 1.8), (3.5, 1.2), (4.5, 1.2)]; + let poly = Polygon::new(v.into(), vec![v2.into()]); + + // Test original implementation + let distance = Euclidean.distance(&line, &poly); + assert_relative_eq!(distance, 0.04999999999999982); + + // Test generic implementation + let generic_distance = line.distance_ext(&poly); + assert_relative_eq!(generic_distance, 0.04999999999999982); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // LineString-Line test + fn test_linestring_line_distance() { + let line = Line::from([(0.0, 0.0), (0.0, 2.0)]); + let ls: LineString<_> = vec![(3.0, 0.0), (1.0, 1.0), (3.0, 2.0)].into(); + + // Test original implementation + let distance = Euclidean.distance(&ls, &line); + assert_relative_eq!(distance, 1.0); + + // Test generic implementation + let generic_distance = ls.distance_ext(&line); + assert_relative_eq!(generic_distance, 1.0); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + + #[test] + // Triangle-Point test: point on vertex + fn test_triangle_point_on_vertex_distance() { + let triangle = Triangle::from([(0.0, 0.0), (2.0, 0.0), (2.0, 2.0)]); + let point = Point::new(0.0, 0.0); + + // Test original implementation + let distance = Euclidean.distance(&triangle, &point); + assert_relative_eq!(distance, 0.0); + + // Test generic implementation + let generic_distance = triangle.distance_ext(&point); + assert_relative_eq!(generic_distance, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + + #[test] + // Triangle-Point test: point on edge + fn test_triangle_point_on_edge_distance() { + let triangle = Triangle::from([(0.0, 0.0), (2.0, 0.0), (2.0, 2.0)]); + let point = Point::new(1.5, 0.0); + + // Test original implementation + let distance = Euclidean.distance(&triangle, &point); + assert_relative_eq!(distance, 0.0); + + // Test generic implementation + let generic_distance = triangle.distance_ext(&point); + assert_relative_eq!(generic_distance, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + + #[test] + // Triangle-Point test + fn test_triangle_point_distance() { + let triangle = Triangle::from([(0.0, 0.0), (2.0, 0.0), (2.0, 2.0)]); + let point = Point::new(2.0, 3.0); + + // Test original implementation + let distance = Euclidean.distance(&triangle, &point); + assert_relative_eq!(distance, 1.0); + + // Test generic implementation + let generic_distance = triangle.distance_ext(&point); + assert_relative_eq!(generic_distance, 1.0); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + + #[test] + // Triangle-Point test: point within triangle + fn test_triangle_point_inside_distance() { + let triangle = Triangle::from([(0.0, 0.0), (2.0, 0.0), (2.0, 2.0)]); + let point = Point::new(1.0, 0.5); + + // Test original implementation + let distance = Euclidean.distance(&triangle, &point); + assert_relative_eq!(distance, 0.0); + + // Test generic implementation + let generic_distance = triangle.distance_ext(&point); + assert_relative_eq!(generic_distance, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + + #[test] + fn convex_and_nearest_neighbour_comparison() { + let ls1: LineString = vec![ + Coord::from((57.39453770777941, 307.60533608924663)), + Coord::from((67.1800355576469, 309.6654408997451)), + Coord::from((84.89693692793338, 225.5101593908847)), + Coord::from((75.1114390780659, 223.45005458038628)), + Coord::from((57.39453770777941, 307.60533608924663)), + ] + .into(); + let first_polygon: Polygon = Polygon::new(ls1, vec![]); + let ls2: LineString = vec![ + Coord::from((138.11769866645008, -45.75134112915392)), + Coord::from((130.50230476949187, -39.270154833870336)), + Coord::from((184.94426964987397, 24.699153900578573)), + Coord::from((192.55966354683218, 18.217967605294987)), + Coord::from((138.11769866645008, -45.75134112915392)), + ] + .into(); + let second_polygon = Polygon::new(ls2, vec![]); + + // Test original implementation + let distance = Euclidean.distance(&first_polygon, &second_polygon); + assert_relative_eq!(distance, 224.35357967013238); + + // Test generic implementation + let generic_distance = first_polygon.distance_ext(&second_polygon); + assert_relative_eq!(generic_distance, 224.35357967013238); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + fn fast_path_regression() { + // this test will fail if the fast path algorithm is reintroduced without being fixed + let p1 = polygon!( + (x: 0_f64, y: 0_f64), + (x: 300_f64, y: 0_f64), + (x: 300_f64, y: 100_f64), + (x: 0_f64, y: 100_f64), + ) + .orient(Direction::Default); + let p2 = polygon!( + (x: 100_f64, y: 150_f64), + (x: 150_f64, y: 200_f64), + (x: 50_f64, y: 200_f64), + ) + .orient(Direction::Default); + let p3 = polygon!( + (x: 0_f64, y: 0_f64), + (x: 300_f64, y: 0_f64), + (x: 300_f64, y: 100_f64), + (x: 0_f64, y: 100_f64), + ) + .orient(Direction::Reversed); + let p4 = polygon!( + (x: 100_f64, y: 150_f64), + (x: 150_f64, y: 200_f64), + (x: 50_f64, y: 200_f64), + ) + .orient(Direction::Reversed); + + // Test original implementation + let distance_p1_p2 = Euclidean.distance(&p1, &p2); + let distance_p3_p4 = Euclidean.distance(&p3, &p4); + let distance_p1_p4 = Euclidean.distance(&p1, &p4); + let distance_p2_p3 = Euclidean.distance(&p2, &p3); + assert_eq!(distance_p1_p2, 50.0f64); + assert_eq!(distance_p3_p4, 50.0f64); + assert_eq!(distance_p1_p4, 50.0f64); + assert_eq!(distance_p2_p3, 50.0f64); + + // Test generic implementation + let generic_distance_p1_p2 = p1.distance_ext(&p2); + let generic_distance_p3_p4 = p3.distance_ext(&p4); + let generic_distance_p1_p4 = p1.distance_ext(&p4); + let generic_distance_p2_p3 = p2.distance_ext(&p3); + assert_relative_eq!(generic_distance_p1_p2, 50.0f64); + assert_relative_eq!(generic_distance_p3_p4, 50.0f64); + assert_relative_eq!(generic_distance_p1_p4, 50.0f64); + assert_relative_eq!(generic_distance_p2_p3, 50.0f64); + + // Ensure both implementations agree + assert_relative_eq!(distance_p1_p2, generic_distance_p1_p2); + assert_relative_eq!(distance_p3_p4, generic_distance_p3_p4); + assert_relative_eq!(distance_p1_p4, generic_distance_p1_p4); + assert_relative_eq!(distance_p2_p3, generic_distance_p2_p3); + } + #[test] + fn rect_to_polygon_distance_test() { + // Test that Rect to Polygon distance works + let rect = Rect::new((0.0, 0.0), (2.0, 2.0)); + let poly_points = vec![(3., 0.), (5., 0.), (5., 2.), (3., 2.), (3., 0.)]; + let poly = Polygon::new(LineString::from(poly_points), vec![]); + + // Test original implementation (both directions) + let dist1 = Euclidean.distance(&rect, &poly); + let dist2 = Euclidean.distance(&poly, &rect); + assert_relative_eq!(dist1, 1.0); + assert_relative_eq!(dist2, 1.0); + assert_relative_eq!(dist1, dist2); // Verify symmetry + + // Test generic implementation + let rect_as_poly = rect.to_polygon(); + let generic_dist1 = rect_as_poly.distance_ext(&poly); + let generic_dist2 = poly.distance_ext(&rect_as_poly); + assert_relative_eq!(generic_dist1, 1.0); + assert_relative_eq!(generic_dist2, 1.0); + + // Ensure both implementations agree + assert_relative_eq!(dist1, generic_dist1); + assert_relative_eq!(dist2, generic_dist2); + } + + #[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), + ]); + + // Test original implementations + let test_p = Point::new(50., 50.); + let distance_p_gc = Euclidean.distance(&test_p, &gc); + assert_relative_eq!(distance_p_gc, 60.959002616512684); + + let test_multipoint = MultiPoint::new(vec![test_p]); + let distance_mp_gc = Euclidean.distance(&test_multipoint, &gc); + assert_relative_eq!(distance_mp_gc, 60.959002616512684); + + let test_line = Line::from([(50., 50.), (60., 60.)]); + let distance_line_gc = Euclidean.distance(&test_line, &gc); + assert_relative_eq!(distance_line_gc, 60.959002616512684); + + let test_ls = LineString::from(vec![(50., 50.), (60., 60.), (70., 70.)]); + let distance_ls_gc = Euclidean.distance(&test_ls, &gc); + assert_relative_eq!(distance_ls_gc, 60.959002616512684); + + let test_mls = MultiLineString::new(vec![test_ls]); + let distance_mls_gc = Euclidean.distance(&test_mls, &gc); + assert_relative_eq!(distance_mls_gc, 60.959002616512684); + + let test_poly = Polygon::new( + LineString::from(vec![ + (50., 50.), + (60., 50.), + (60., 60.), + (55., 55.), + (50., 50.), + ]), + vec![], + ); + let distance_poly_gc = Euclidean.distance(&test_poly, &gc); + assert_relative_eq!(distance_poly_gc, 60.959002616512684); + + let test_multipoly = MultiPolygon::new(vec![test_poly]); + let distance_multipoly_gc = Euclidean.distance(&test_multipoly, &gc); + assert_relative_eq!(distance_multipoly_gc, 60.959002616512684); + + let test_tri = Triangle::from([(50., 50.), (60., 50.), (55., 55.)]); + let distance_tri_gc = Euclidean.distance(&test_tri, &gc); + assert_relative_eq!(distance_tri_gc, 60.959002616512684); + + let test_rect = Rect::new(coord! { x: 50., y: 50. }, coord! { x: 60., y: 60. }); + let distance_rect_gc = Euclidean.distance(&test_rect, &gc); + assert_relative_eq!(distance_rect_gc, 60.959002616512684); + + let test_gc = GeometryCollection(vec![Geometry::Rect(test_rect)]); + let distance_gc_gc = Euclidean.distance(&test_gc, &gc); + assert_relative_eq!(distance_gc_gc, 60.959002616512684); + } + + #[test] + fn test_original_issue_verification() { + let point = Point::new(0.0, 0.0); + let linestring = LineString::from(vec![(0.0, 0.0), (1.0, 1.0)]); + + let gc1 = GeometryCollection(vec![ + Geometry::Point(point), + Geometry::LineString(linestring.clone()), + ]); + + let gc2 = GeometryCollection(vec![ + Geometry::Point(point), + Geometry::LineString(linestring), + ]); + + // Test the concrete Distance API + let distance = Euclidean.distance(&gc1, &gc2); + assert_eq!( + distance, 0.0, + "Distance between identical GeometryCollections should be 0" + ); + + // Test the generic distance_ext API directly + use crate::line_measures::DistanceExt; + let distance_ext = gc1.distance_ext(&gc2); + assert_eq!(distance_ext, 0.0, "Generic distance should also be 0"); + } + + #[test] + fn test_force_generic_trait_recursion() { + let point = Point::new(0.0, 0.0); + let linestring = LineString::from(vec![(0.0, 0.0), (1.0, 1.0)]); + + let gc1 = GeometryCollection(vec![ + Geometry::Point(point), + Geometry::LineString(linestring.clone()), + ]); + + let gc2 = GeometryCollection(vec![ + Geometry::Point(point), + Geometry::LineString(linestring), + ]); + + let distance_result = gc1.distance_ext(&gc2); + assert_eq!(distance_result, 0.0); + + let geom_gc1 = Geometry::GeometryCollection(gc1.clone()); + let geom_gc2 = Geometry::GeometryCollection(gc2.clone()); + let distance_result = geom_gc1.distance_ext(&geom_gc2); + assert_eq!(distance_result, 0.0); + + let distance_result = geom_gc1.distance_ext(&gc2); + assert_eq!(distance_result, 0.0); + + let distance_result = gc1.distance_ext(&geom_gc2); + assert_eq!(distance_result, 0.0); + } + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/mod.rs b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/mod.rs new file mode 100644 index 00000000..0c1656f7 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/mod.rs @@ -0,0 +1,94 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +mod distance; +mod utils; +pub use distance::DistanceExt; +use geo_types::{Coord, CoordFloat, Point}; + +use crate::line_measures::distance::Distance; + +/// Operations on the [Euclidean plane] measure distance with the pythagorean formula - +/// what you'd measure with a ruler. +/// +/// If you have lon/lat points, use the [`Haversine`], [`Geodesic`], or other [metric spaces] - +/// Euclidean methods will give nonsense results. +/// +/// If you wish to use Euclidean operations with lon/lat, the coordinates must first be transformed +/// using the [`Transform::transform`](crate::Transform::transform) / [`Transform::transform_crs_to_crs`](crate::Transform::transform_crs_to_crs) methods or their +/// immutable variants. Use of these requires the proj feature +/// +/// [Euclidean plane]: https://en.wikipedia.org/wiki/Euclidean_plane +/// [`Transform`]: crate::Transform +/// [`Haversine`]: super::Haversine +/// [`Geodesic`]: super::Geodesic +/// [metric spaces]: super +pub struct Euclidean; + +// ┌───────────────────────────┐ +// │ Implementations for Coord │ +// └───────────────────────────┘ + +impl Distance, Coord> for Euclidean { + fn distance(&self, origin: Coord, destination: Coord) -> F { + let delta = origin - destination; + delta.x.hypot(delta.y) + } +} + +// ┌───────────────────────────┐ +// │ Implementations for Point │ +// └───────────────────────────┘ + +/// Calculate the Euclidean distance (a.k.a. pythagorean distance) between two Points +impl Distance, Point> for Euclidean { + /// Calculate the Euclidean distance (a.k.a. pythagorean distance) between two Points + /// + /// # Units + /// - `origin`, `destination`: Point where the units of x/y represent non-angular units + /// — e.g. meters or miles, not lon/lat. For lon/lat points, use the + /// [`Haversine`] or [`Geodesic`] [metric spaces]. + /// - returns: distance in the same units as the `origin` and `destination` points + /// + /// # Example + /// ``` + /// use sedona_geo_generic_alg::{Euclidean, Distance}; + /// use sedona_geo_generic_alg::Point; + /// // web mercator + /// let new_york_city = Point::new(-8238310.24, 4942194.78); + /// // web mercator + /// let london = Point::new(-14226.63, 6678077.70); + /// let distance: f64 = Euclidean.distance(new_york_city, london); + /// + /// assert_eq!( + /// 8_405_286., // meters in web mercator + /// distance.round() + /// ); + /// ``` + /// + /// [`Haversine`]: crate::line_measures::metric_spaces::Haversine + /// [`Geodesic`]: crate::line_measures::metric_spaces::Geodesic + /// [metric spaces]: crate::line_measures::metric_spaces + fn distance(&self, origin: Point, destination: Point) -> F { + self.distance(origin.0, destination.0) + } +} + +impl Distance, &Point> for Euclidean { + fn distance(&self, origin: &Point, destination: &Point) -> F { + self.distance(*origin, *destination) + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/utils.rs b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/utils.rs new file mode 100644 index 00000000..6c79ef8f --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/utils.rs @@ -0,0 +1,2380 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use crate::algorithm::Intersects; +use crate::coordinate_position::{coord_pos_relative_to_ring, CoordPos}; +use crate::geometry::*; +use crate::{CoordFloat, GeoFloat, GeoNum}; +use geo_traits::{CoordTrait, LineStringTrait}; +use num_traits::{Bounded, Float}; +use sedona_geo_traits_ext::{ + LineStringTraitExt, LineTraitExt, PointTraitExt, PolygonTraitExt, TriangleTraitExt, +}; + +// ┌────────────────────────────────────────────────────────────┐ +// │ Helper functions for generic distance calculations │ +// └────────────────────────────────────────────────────────────┘ + +pub fn nearest_neighbour_distance(geom1: &LineString, geom2: &LineString) -> F { + let mut min_distance: F = Bounded::max_value(); + + // Primary computation: line-to-line distances + for line1 in geom1.lines() { + for line2 in geom2.lines() { + let line_distance = distance_line_to_line_generic(&line1, &line2); + min_distance = min_distance.min(line_distance); + + // Early exit if we found an intersection + if line_distance == F::zero() { + return F::zero(); + } + } + } + + // Check points of geom2 against lines of geom1 + for point in geom2.points() { + if let Some(coord) = point.coord_ext() { + for line1 in geom1.lines() { + let dist = distance_coord_to_line_generic(&coord, &line1); + min_distance = min_distance.min(dist); + } + } + } + + // Check points of geom1 against lines of geom2 + for point in geom1.points() { + if let Some(coord) = point.coord_ext() { + for line2 in geom2.lines() { + let dist = distance_coord_to_line_generic(&coord, &line2); + min_distance = min_distance.min(dist); + } + } + } + + min_distance +} + +pub fn ring_contains_coord(ring: &LineString, c: Coord) -> bool { + match coord_pos_relative_to_ring(c, ring) { + CoordPos::Inside => true, + CoordPos::OnBoundary | CoordPos::Outside => false, + } +} + +/// Generic point-to-point Euclidean distance calculation. +/// +/// # Algorithm Equivalence to Concrete Implementation +/// +/// This function is algorithmically identical to the concrete `Distance, Coord>` implementation. +/// +/// **Equivalence Details:** +/// - **Same mathematical formula**: Both use Euclidean distance `sqrt(Δx² + Δy²)` via `hypot()` +/// - **Same calculation steps**: Extract coordinates, compute deltas, apply `hypot()` +/// - **Same edge case handling**: Both return 0 for invalid/empty points +/// - **Same numerical precision**: Both use identical `hypot()` implementation +/// +/// The only difference is the abstraction layer - this generic version works with any +/// type implementing `PointTraitExt`, while concrete works with `Coord` directly. +pub fn distance_point_to_point_generic(p1: &P1, p2: &P2) -> F +where + F: CoordFloat, + P1: PointTraitExt, + P2: PointTraitExt, +{ + if let (Some(c1), Some(c2)) = (p1.coord(), p2.coord()) { + let delta_x = c1.x() - c2.x(); + let delta_y = c1.y() - c2.y(); + delta_x.hypot(delta_y) + } else { + F::zero() + } +} + +/// Generic coordinate-to-line-segment distance calculation. +/// +/// # Algorithm Equivalence to Concrete Implementation +/// +/// This function is algorithmically identical to the concrete `line_segment_distance` function +/// in `geo-types/src/private_utils.rs`. +/// +/// **Equivalence Details:** +/// - **Same parametric approach**: Both compute parameter `r` to find the closest point on the line +/// - **Same boundary handling**: Both check if `r <= 0` (closest to start) or `r >= 1` (closest to end) +/// - **Same degenerate case**: Both handle zero-length lines by computing direct point distance +/// - **Same perpendicular distance formula**: Both use cross product formula `s.abs() * dx.hypot(dy)` for interior points +/// - **Same numerical precision**: Both use identical calculations and `hypot()` calls +/// +/// The concrete implementation uses `line_euclidean_length()` helper for endpoint distances, +/// while this uses inline `delta.hypot()` - both compute the same Euclidean distance. +pub fn distance_coord_to_line_generic(coord: &C, line: &L) -> F +where + F: CoordFloat, + C: CoordTrait, + L: LineTraitExt, +{ + let point_x = coord.x(); + let point_y = coord.y(); + let start = line.start_coord(); + let end = line.end_coord(); + + // Handle degenerate case: line segment is a point + if start.x == end.x && start.y == end.y { + let delta_x = point_x - start.x; + let delta_y = point_y - start.y; + return delta_x.hypot(delta_y); + } + + let dx = end.x - start.x; + let dy = end.y - start.y; + let d_squared = dx * dx + dy * dy; + let r = ((point_x - start.x) * dx + (point_y - start.y) * dy) / d_squared; + + if r <= F::zero() { + // Closest point is the start point + let delta_x = point_x - start.x; + let delta_y = point_y - start.y; + return delta_x.hypot(delta_y); + } + if r >= F::one() { + // Closest point is the end point + let delta_x = point_x - end.x; + let delta_y = point_y - end.y; + return delta_x.hypot(delta_y); + } + + // Closest point is on the line segment - use perpendicular distance + let s = ((start.y - point_y) * dx - (start.x - point_x) * dy) / d_squared; + s.abs() * dx.hypot(dy) +} + +/// Generic point-to-linestring distance calculation. +/// +/// # Algorithm Equivalence to Concrete Implementation +/// +/// This function is algorithmically identical to the concrete `point_line_string_euclidean_distance` function +/// in `geo-types/src/private_utils.rs`. +/// +/// **Equivalence Details:** +/// - **Same containment check optimization**: Both check if point intersects/is contained in the linestring first +/// - **Same early exit**: Both return 0 immediately if point is on the linestring +/// - **Same iteration approach**: Both iterate through all line segments to find minimum distance +/// - **Same distance calculation**: Both use point-to-line-segment distance for each segment +/// - **Same empty handling**: Both return 0 for empty linestrings +/// +/// The concrete implementation uses `line_string_contains_point()` while this uses `intersects()` trait method, +/// but both perform the same containment check. The iteration pattern and minimum distance logic are identical. +pub fn distance_point_to_linestring_generic(point: &P, linestring: &LS) -> F +where + F: GeoFloat, + P: PointTraitExt, + LS: LineStringTraitExt, +{ + if let Some(coord) = point.coord() { + // Early exit optimization: if point is on the linestring, distance is 0 + // Check if the point is contained in the linestring using intersects + if linestring.intersects(point) { + return F::zero(); + } + + let mut lines = linestring.lines(); + if let Some(first_line) = lines.next() { + let mut min_distance = distance_coord_to_line_generic(&coord, &first_line); + for line in lines { + min_distance = min_distance.min(distance_coord_to_line_generic(&coord, &line)); + } + min_distance + } else { + F::zero() + } + } else { + F::zero() + } +} + +/// Point to Polygon distance +/// +/// # Algorithm Equivalence +/// +/// This generic implementation is algorithmically identical to the concrete +/// `Distance, &Polygon>` implementation: +/// +/// 1. **Intersection Check**: First checks if the point intersects the polygon +/// using the same `Intersects` trait, returning zero for any intersection +/// (boundary or interior). +/// +/// 2. **Ring Distance Calculation**: If no intersection, computes minimum distance +/// by iterating through all polygon rings (exterior and all interior holes). +/// +/// 3. **Minimum Selection**: Uses the same fold pattern to find the minimum +/// distance across all rings, starting with F::max_value(). +/// +/// The only difference is the generic trait-based interface for accessing +/// polygon components, while the core distance logic remains identical. +pub fn distance_point_to_polygon_generic(point: &P, polygon: &Poly) -> F +where + F: GeoFloat, + P: PointTraitExt, + Poly: PolygonTraitExt, +{ + // Check if the polygon is empty + if polygon.exterior_ext().is_none() { + return F::zero(); + } + + // If the point intersects the polygon (is inside or on boundary), distance is 0 + if polygon.intersects(point) { + return F::zero(); + } + + // Point is outside the polygon, calculate minimum distance to edges + if let (Some(coord), Some(exterior)) = (point.coord_ext(), polygon.exterior_ext()) { + let mut min_dist: F = Float::max_value(); + + // Calculate minimum distance to exterior ring - single loop + for line in exterior.lines() { + let dist = distance_coord_to_line_generic(&coord, &line); + min_dist = min_dist.min(dist); + } + + // Only check interior rings if they exist + if polygon.interiors_ext().next().is_some() { + for interior in polygon.interiors_ext() { + for line in interior.lines() { + let dist = distance_coord_to_line_generic(&coord, &line); + min_dist = min_dist.min(dist); + } + } + } + + min_dist + } else { + F::zero() + } +} + +/// Line to Line distance +/// +/// # Algorithm Equivalence +/// +/// This generic implementation is algorithmically identical to the concrete +/// `Distance, &Line>` implementation: +/// +/// 1. **Intersection Check**: First uses the `Intersects` trait to check if the +/// lines intersect, returning zero if they do. +/// +/// 2. **Four-Point Distance**: If no intersection, computes the minimum distance +/// by checking all four possible point-to-line segment distances: +/// +/// 3. **Minimum Selection**: Uses the same chained min() operations to find +/// the shortest distance among all four calculations. +/// +/// The generic trait interface provides the same coordinate access while +/// maintaining identical distance computation logic. +pub fn distance_line_to_line_generic(line1: &L1, line2: &L2) -> F +where + F: GeoFloat, + L1: LineTraitExt, + L2: LineTraitExt, +{ + let start1 = line1.start_coord(); + let end1 = line1.end_coord(); + let start2 = line2.start_coord(); + let end2 = line2.end_coord(); + + // Check if lines intersect using generic intersects + if line1.intersects(line2) { + return F::zero(); + } + + // Find minimum distance between all endpoint combinations + let dist1 = distance_coord_to_line_generic(&start1, line2); + let dist2 = distance_coord_to_line_generic(&end1, line2); + let dist3 = distance_coord_to_line_generic(&start2, line1); + let dist4 = distance_coord_to_line_generic(&end2, line1); + + dist1.min(dist2).min(dist3).min(dist4) +} + +/// Line to LineString distance +/// +/// # Algorithm Equivalence +/// +/// This generic implementation is algorithmically identical to the concrete +/// `Distance, &LineString>` implementation: +/// +/// 1. **Segment Iteration**: Maps over each line segment in the LineString +/// using the same `lines()` iterator approach. +/// +/// 2. **Line-to-Line Distance**: For each segment, calls the same line-to-line +/// distance function that handles intersection detection and four-point +/// distance calculations. +/// +/// 3. **Minimum Selection**: Uses the same fold pattern with F::max_value() +/// as the starting accumulator and min() reduction to find the shortest +/// distance across all segments. +/// +/// The generic trait interface provides equivalent LineString iteration while +/// maintaining identical distance computation logic. +pub fn distance_line_to_linestring_generic(line: &L, linestring: &LS) -> F +where + F: GeoFloat, + L: LineTraitExt, + LS: LineStringTraitExt, +{ + linestring + .lines() + .map(|ls_line| distance_line_to_line_generic(line, &ls_line)) + .fold(Float::max_value(), |acc, dist| acc.min(dist)) +} + +/// Line to Polygon distance +/// +/// # Algorithm Equivalence +/// +/// This generic implementation is algorithmically identical to the concrete +/// `Distance, &Polygon>` implementation: +/// +/// 1. **Line-to-LineString Conversion**: Converts the line segment into a +/// two-point LineString containing the start and end coordinates. +/// +/// 2. **Delegation to LineString-Polygon**: Uses the same delegation pattern +/// as the concrete implementation by calling the LineString-to-Polygon +/// distance function. +/// +/// 3. **Identical Logic Path**: This ensures the same containment checks, +/// intersection detection, and ring distance calculations are applied +/// as in the concrete implementation. +/// +/// The conversion approach maintains algorithmic equivalence while leveraging +/// the more comprehensive LineString-to-Polygon distance logic. +pub fn distance_line_to_polygon_generic(line: &L, polygon: &Poly) -> F +where + F: GeoFloat, + L: LineTraitExt, + Poly: PolygonTraitExt, +{ + // Convert line to linestring and use existing linestring-to-polygon function + let line_coords = vec![line.start_coord(), line.end_coord()]; + let line_as_ls = LineString::from(line_coords); + distance_linestring_to_polygon_generic(&line_as_ls, polygon) +} + +/// LineString to LineString distance +/// +/// # Algorithm Equivalence +/// +/// This generic implementation is algorithmically identical to the concrete +/// `Distance, &LineString>` implementation: +/// +/// 1. **Cartesian Product**: Uses flat_map to create all possible combinations +/// of line segments between the two LineStrings, matching the nested +/// iteration pattern of the concrete implementation. +/// +/// 2. **Line-to-Line Distance**: For each segment pair, applies the same +/// line-to-line distance function with intersection detection and +/// four-point distance calculations. +/// +/// 3. **Minimum Selection**: Uses the same fold pattern with F::max_value() +/// as the starting accumulator and min() reduction to find the shortest +/// distance across all segment combinations. +/// +/// The generic trait interface provides equivalent segment iteration while +/// maintaining identical pairwise distance computation logic. +pub fn distance_linestring_to_linestring_generic(ls1: &LS1, ls2: &LS2) -> F +where + F: GeoFloat, + LS1: LineStringTraitExt, + LS2: LineStringTraitExt, +{ + ls1.lines() + .flat_map(|line1| { + ls2.lines() + .map(move |line2| distance_line_to_line_generic(&line1, &line2)) + }) + .fold(Float::max_value(), |acc, dist| acc.min(dist)) +} + +/// LineString to Polygon distance +/// +/// # Algorithm Equivalence +/// +/// This generic implementation is algorithmically identical to the concrete +/// `Distance, &Polygon>` implementation: +/// +/// 1. **Intersection Check**: First uses the `Intersects` trait to check if +/// the LineString intersects the polygon, returning zero if they do. +/// +/// 2. **Containment-Based Logic**: Implements the same containment logic as +/// the concrete implementation: +/// - If polygon has holes AND first point of LineString is inside exterior +/// ring, only check distance to interior rings (holes) +/// - Otherwise, check distance to exterior ring only +/// +/// 3. **Ray Casting Algorithm**: Uses identical ray casting point-in-polygon +/// test to determine if the first LineString point is inside the exterior. +/// +/// 4. **Direct Nested Loop Approach**: Unlike simpler functions that use +/// `nearest_neighbour_distance`, this function implements the distance +/// calculation directly with nested loops over LineString and polygon +/// ring segments. This matches the concrete implementation's approach +/// which requires the specialized containment logic for polygons with holes. +/// +/// 5. **Early Exit**: Includes the same zero-distance early exit optimization +/// when any line segments intersect during the nested iteration. +/// +/// Note: +/// The direct nested loop approach (rather than delegating to helper functions) +/// is necessary to maintain the exact containment-based ring selection logic +/// that the concrete implementation uses for polygons with holes. +/// We have seen sufficient performance improvements in benchmarks by avoiding +/// the overhead of additional function calls and iterator abstractions. +/// +pub fn distance_linestring_to_polygon_generic(linestring: &LS, polygon: &Poly) -> F +where + F: GeoFloat, + LS: LineStringTraitExt, + Poly: PolygonTraitExt, +{ + // Early intersect check + if polygon.intersects(linestring) { + return F::zero(); + } + + if let Some(exterior) = polygon.exterior_ext() { + // Check containment logic: if polygon has holes AND first point of LineString is inside exterior ring, + // then only consider distance to holes (interior rings). Otherwise, consider distance to exterior. + let has_holes = polygon.interiors_ext().next().is_some(); + + let first_point_inside = if has_holes { + // Check if first point of LineString is inside the exterior ring + if let Some(first_coord) = linestring.coords().next() { + // Simple point-in-polygon test using ray casting + let point_x = first_coord.x(); + let point_y = first_coord.y(); + let mut inside = false; + let ring_coords: Vec<_> = exterior.coords().collect(); + let n = ring_coords.len(); + + if n > 0 { + let mut j = n - 1; + for i in 0..n { + let xi = ring_coords[i].x(); + let yi = ring_coords[i].y(); + let xj = ring_coords[j].x(); + let yj = ring_coords[j].y(); + + if ((yi > point_y) != (yj > point_y)) + && (point_x < (xj - xi) * (point_y - yi) / (yj - yi) + xi) + { + inside = !inside; + } + j = i; + } + } + inside + } else { + false // Empty LineString + } + } else { + false + }; + + if has_holes && first_point_inside { + // LineString is inside polygon: only check distance to interior rings (holes) + let mut min_dist: F = Float::max_value(); + for interior in polygon.interiors_ext() { + for line1 in linestring.lines() { + for line2 in interior.lines() { + let line_dist = distance_line_to_line_generic(&line1, &line2); + min_dist = min_dist.min(line_dist); + + if line_dist == F::zero() { + return F::zero(); + } + } + } + } + min_dist + } else { + // LineString is outside polygon or polygon has no holes: check distance to exterior ring only + let mut min_dist: F = Float::max_value(); + for line1 in linestring.lines() { + for line2 in exterior.lines() { + let line_dist = distance_line_to_line_generic(&line1, &line2); + min_dist = min_dist.min(line_dist); + + if line_dist == F::zero() { + return F::zero(); + } + } + } + min_dist + } + } else { + F::zero() + } +} + +/// Polygon to Polygon distance +/// +/// # Algorithm Equivalence +/// +/// This generic implementation is algorithmically identical to the concrete +/// `Distance, &Polygon>` implementation: +/// +/// 1. **Intersection Check**: First uses the `Intersects` trait to check if +/// the polygons intersect, returning zero if they do. +/// +/// 2. **Fast Path Optimization**: If neither polygon has holes, directly +/// delegates to LineString-to-LineString distance between exterior rings. +/// +/// 3. **Symmetric Containment Logic**: Implements the same bidirectional +/// containment checks as the concrete implementation: +/// - If polygon1 has holes AND polygon2's first point is inside polygon1's +/// exterior, check distance from polygon2's exterior to polygon1's holes +/// - If polygon2 has holes AND polygon1's first point is inside polygon2's +/// exterior, check distance from polygon1's exterior to polygon2's holes +/// +/// 4. **Mixed Approach**: Uses `nearest_neighbour_distance` for the contained +/// polygon cases (for efficiency when checking against multiple holes), +/// but delegates to `distance_linestring_to_linestring_generic` for the +/// default exterior-to-exterior case. +/// +/// 5. **Point-in-Polygon Test**: Uses the same `ring_contains_coord` helper +/// function for containment detection as the concrete implementation. +/// +/// The mixed approach (using both helper functions and direct delegation) +/// matches the concrete implementation's optimization strategy for different +/// geometric configurations. +pub fn distance_polygon_to_polygon_generic(polygon1: &P1, polygon2: &P2) -> F +where + F: GeoFloat, + P1: PolygonTraitExt, + P2: PolygonTraitExt, +{ + // Check if polygons intersect using generic intersects + if polygon1.intersects(polygon2) { + return F::zero(); + } + + if let (Some(ext1), Some(ext2)) = (polygon1.exterior_ext(), polygon2.exterior_ext()) { + let has_interiors1 = polygon1.interiors_ext().next().is_some(); + let has_interiors2 = polygon2.interiors_ext().next().is_some(); + + // Fast path: if no interiors in either polygon, skip containment logic entirely + if !has_interiors1 && !has_interiors2 { + return distance_linestring_to_linestring_generic(&ext1, &ext2); + } + + // Symmetric containment logic matching concrete implementation exactly + // Check if polygon_b is contained within polygon_a (has holes) + if has_interiors1 { + if let Some(first_coord_b) = ext2.coords_ext().next() { + let ext1_ls = LineString::from( + ext1.coords_ext() + .map(|c| (c.x(), c.y())) + .collect::>(), + ); + + let coord_b = Coord::from((first_coord_b.x(), first_coord_b.y())); + if ring_contains_coord(&ext1_ls, coord_b) { + // polygon_b is inside polygon_a: check distance to polygon_a's holes + let ext2_concrete = LineString::from( + ext2.coords_ext() + .map(|c| (c.x(), c.y())) + .collect::>(), + ); + + let mut mindist: F = Float::max_value(); + for ring in polygon1.interiors_ext() { + let ring_concrete = LineString::from( + ring.coords_ext() + .map(|c| (c.x(), c.y())) + .collect::>(), + ); + mindist = + mindist.min(nearest_neighbour_distance(&ext2_concrete, &ring_concrete)); + } + return mindist; + } + } + } + + // Check if polygon_a is contained within polygon_b (has holes) + if has_interiors2 { + if let Some(first_coord_a) = ext1.coords_ext().next() { + let ext2_ls = LineString::from( + ext2.coords_ext() + .map(|c| (c.x(), c.y())) + .collect::>(), + ); + + let coord_a = Coord::from((first_coord_a.x(), first_coord_a.y())); + if ring_contains_coord(&ext2_ls, coord_a) { + // polygon_a is inside polygon_b: check distance to polygon_b's holes + let ext1_concrete = LineString::from( + ext1.coords_ext() + .map(|c| (c.x(), c.y())) + .collect::>(), + ); + + let mut mindist: F = Float::max_value(); + for ring in polygon2.interiors_ext() { + let ring_concrete = LineString::from( + ring.coords_ext() + .map(|c| (c.x(), c.y())) + .collect::>(), + ); + mindist = + mindist.min(nearest_neighbour_distance(&ext1_concrete, &ring_concrete)); + } + return mindist; + } + } + } + + // Default case - distance between exterior rings + distance_linestring_to_linestring_generic(&ext1, &ext2) + } else { + F::zero() + } +} + +/// Triangle to Point distance +pub fn distance_triangle_to_point_generic(triangle: &T, point: &P) -> F +where + F: GeoFloat, + T: TriangleTraitExt, + P: PointTraitExt, +{ + // Convert triangle to polygon and use existing point-to-polygon function + let tri_poly = triangle.to_polygon(); + distance_point_to_polygon_generic(point, &tri_poly) +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Symmetric Distance Function Generator Macro │ +// └────────────────────────────────────────────────────────────┘ + +/// Macro to generate symmetric distance functions +/// For distance operations that are symmetric (distance(a, b) == distance(b, a)), +/// this macro generates the reverse function that calls the primary implementation +macro_rules! symmetric_distance_generic_impl { + ($func_name_ab:ident, $func_name_ba:ident, $trait_a:ident, $trait_b:ident) => { + #[allow(dead_code)] + pub fn $func_name_ba(b: &B, a: &A) -> F + where + F: GeoFloat, + A: $trait_a, + B: $trait_b, + { + $func_name_ab(a, b) + } + }; +} + +// Generate symmetric distance functions +symmetric_distance_generic_impl!( + distance_point_to_linestring_generic, + distance_linestring_to_point_generic, + PointTraitExt, + LineStringTraitExt +); + +symmetric_distance_generic_impl!( + distance_point_to_polygon_generic, + distance_polygon_to_point_generic, + PointTraitExt, + PolygonTraitExt +); + +symmetric_distance_generic_impl!( + distance_linestring_to_polygon_generic, + distance_polygon_to_linestring_generic, + LineStringTraitExt, + PolygonTraitExt +); + +symmetric_distance_generic_impl!( + distance_line_to_linestring_generic, + distance_linestring_to_line_generic, + LineTraitExt, + LineStringTraitExt +); + +symmetric_distance_generic_impl!( + distance_line_to_polygon_generic, + distance_polygon_to_line_generic, + LineTraitExt, + PolygonTraitExt +); + +symmetric_distance_generic_impl!( + distance_triangle_to_point_generic, + distance_point_to_triangle_generic, + TriangleTraitExt, + PointTraitExt +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::{coord, Line, LineString, Point, Polygon, Triangle}; + use approx::assert_relative_eq; + use geo::algorithm::line_measures::{Distance, Euclidean}; + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for point_distance_generic function │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_point_distance_generic_basic() { + let p1 = Point::new(0.0, 0.0); + let p2 = Point::new(3.0, 4.0); + + let distance = distance_point_to_point_generic(&p1, &p2); + assert_relative_eq!(distance, 5.0); // 3-4-5 triangle + + // Test symmetry + let distance_reverse = distance_point_to_point_generic(&p2, &p1); + assert_relative_eq!(distance, distance_reverse); + } + + #[test] + fn test_point_distance_generic_same_point() { + let p = Point::new(2.5, -1.5); + let distance = distance_point_to_point_generic(&p, &p); + assert_relative_eq!(distance, 0.0); + } + + #[test] + fn test_point_distance_generic_negative_coordinates() { + let p1 = Point::new(-2.0, -3.0); + let p2 = Point::new(1.0, 1.0); + + let distance = distance_point_to_point_generic(&p1, &p2); + assert_relative_eq!(distance, 5.0); // sqrt((1-(-2))^2 + (1-(-3))^2) = sqrt(9+16) = 5 + } + + #[test] + fn test_point_distance_generic_empty_points() { + // Test with empty points (no coordinates) + let empty_point: Point = Point::new(f64::NAN, f64::NAN); + let regular_point = Point::new(1.0, 1.0); + + // When either point has no valid coordinates, distance should be 0 + let distance = distance_point_to_point_generic(&empty_point, ®ular_point); + assert!(distance.is_nan() || distance == 0.0); // Implementation dependent + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for line_segment_distance_generic function │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_line_segment_distance_generic_point_on_line() { + let coord = coord! { x: 2.0, y: 0.0 }; + let line = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 4.0, y: 0.0 }); + + let distance = distance_coord_to_line_generic(&coord, &line); + assert_relative_eq!(distance, 0.0); + } + + #[test] + fn test_line_segment_distance_generic_perpendicular() { + let coord = coord! { x: 2.0, y: 3.0 }; + let line = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 4.0, y: 0.0 }); + + let distance = distance_coord_to_line_generic(&coord, &line); + assert_relative_eq!(distance, 3.0); + } + + #[test] + fn test_line_segment_distance_generic_beyond_endpoint() { + let coord = coord! { x: 6.0, y: 0.0 }; + let line = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 4.0, y: 0.0 }); + + let distance = distance_coord_to_line_generic(&coord, &line); + assert_relative_eq!(distance, 2.0); // Distance to closest endpoint (4,0) + } + + #[test] + fn test_line_segment_distance_generic_before_startpoint() { + let coord = coord! { x: -2.0, y: 0.0 }; + let line = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 4.0, y: 0.0 }); + + let distance = distance_coord_to_line_generic(&coord, &line); + assert_relative_eq!(distance, 2.0); // Distance to start point (0,0) + } + + #[test] + fn test_line_segment_distance_generic_zero_length_line() { + let coord = coord! { x: 2.0, y: 3.0 }; + let line = Line::new(coord! { x: 1.0, y: 1.0 }, coord! { x: 1.0, y: 1.0 }); + + let distance = distance_coord_to_line_generic(&coord, &line); + let expected = ((2.0 - 1.0).powi(2) + (3.0 - 1.0).powi(2)).sqrt(); + assert_relative_eq!(distance, expected); + } + + #[test] + fn test_line_segment_distance_generic_diagonal_line() { + let coord = coord! { x: 0.0, y: 2.0 }; + let line = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 2.0, y: 2.0 }); + + let distance = distance_coord_to_line_generic(&coord, &line); + // Point (0,2) to line from (0,0) to (2,2) - should be sqrt(2) + assert_relative_eq!(distance, std::f64::consts::SQRT_2, epsilon = 1e-10); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for nearest_neighbour_distance function │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_nearest_neighbour_distance_basic() { + let ls1 = LineString::from(vec![(0.0, 0.0), (2.0, 0.0), (2.0, 2.0)]); + let ls2 = LineString::from(vec![(3.0, 0.0), (5.0, 0.0), (5.0, 2.0)]); + + let distance = nearest_neighbour_distance(&ls1, &ls2); + assert_relative_eq!(distance, 1.0); // Distance between (2,0)-(2,2) and (3,0)-(5,0) + } + + #[test] + fn test_nearest_neighbour_distance_intersecting() { + let ls1 = LineString::from(vec![(0.0, 0.0), (4.0, 0.0)]); + let ls2 = LineString::from(vec![(2.0, -1.0), (2.0, 1.0)]); + + let distance = nearest_neighbour_distance(&ls1, &ls2); + // The linestrings intersect at (2,0), so distance should be 0.0 + assert_relative_eq!(distance, 0.0); + } + + #[test] + fn test_nearest_neighbour_distance_parallel_lines() { + let ls1 = LineString::from(vec![(0.0, 0.0), (4.0, 0.0)]); + let ls2 = LineString::from(vec![(0.0, 2.0), (4.0, 2.0)]); + + let distance = nearest_neighbour_distance(&ls1, &ls2); + assert_relative_eq!(distance, 2.0); // Perpendicular distance between parallel lines + } + + #[test] + fn test_nearest_neighbour_distance_single_segment_each() { + let ls1 = LineString::from(vec![(0.0, 0.0), (1.0, 0.0)]); + let ls2 = LineString::from(vec![(2.0, 1.0), (3.0, 1.0)]); + + let distance = nearest_neighbour_distance(&ls1, &ls2); + let expected = ((2.0 - 1.0).powi(2) + (1.0 - 0.0).powi(2)).sqrt(); + assert_relative_eq!(distance, expected); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for ring_contains_coord function │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_ring_contains_coord_inside() { + let ring = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let coord = coord! { x: 2.0, y: 2.0 }; + + assert!(ring_contains_coord(&ring, coord)); + } + + #[test] + fn test_ring_contains_coord_outside() { + let ring = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let coord = coord! { x: 5.0, y: 2.0 }; + + assert!(!ring_contains_coord(&ring, coord)); + } + + #[test] + fn test_ring_contains_coord_on_boundary() { + let ring = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let coord = coord! { x: 2.0, y: 0.0 }; + + assert!(!ring_contains_coord(&ring, coord)); // On boundary = false + } + + #[test] + fn test_ring_contains_coord_triangle() { + let ring = LineString::from(vec![(0.0, 0.0), (3.0, 0.0), (1.5, 2.0), (0.0, 0.0)]); + let inside_coord = coord! { x: 1.5, y: 0.5 }; + let outside_coord = coord! { x: 3.0, y: 3.0 }; + + assert!(ring_contains_coord(&ring, inside_coord)); + assert!(!ring_contains_coord(&ring, outside_coord)); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for distance_point_to_linestring_generic │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_distance_point_to_linestring_generic_basic() { + let point = Point::new(1.0, 2.0); + let linestring = LineString::from(vec![(0.0, 0.0), (2.0, 0.0), (2.0, 2.0)]); + + let distance = distance_point_to_linestring_generic(&point, &linestring); + assert_relative_eq!(distance, 1.0); // Distance to closest segment + } + + #[test] + fn test_distance_point_to_linestring_generic_empty() { + let point = Point::new(1.0, 2.0); + let linestring = LineString::::new(vec![]); + + let distance = distance_point_to_linestring_generic(&point, &linestring); + assert_relative_eq!(distance, 0.0); + } + + #[test] + fn test_distance_point_to_linestring_generic_single_point() { + let point = Point::new(1.0, 2.0); + let linestring = LineString::from(vec![(0.0, 0.0)]); + + let distance = distance_point_to_linestring_generic(&point, &linestring); + assert_relative_eq!(distance, 0.0); // Single point linestring + } + + #[test] + fn test_distance_point_to_linestring_generic_on_linestring() { + let point = Point::new(1.0, 0.0); + let linestring = LineString::from(vec![(0.0, 0.0), (2.0, 0.0)]); + + let distance = distance_point_to_linestring_generic(&point, &linestring); + assert_relative_eq!(distance, 0.0); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for distance_point_to_polygon_generic │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_distance_point_to_polygon_generic_outside() { + let exterior = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + let point = Point::new(6.0, 2.0); + + let distance = distance_point_to_polygon_generic(&point, &polygon); + assert_relative_eq!(distance, 2.0); // Distance to right edge + } + + #[test] + fn test_distance_point_to_polygon_generic_inside() { + let exterior = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + let point = Point::new(2.0, 2.0); + + let distance = distance_point_to_polygon_generic(&point, &polygon); + assert_relative_eq!(distance, 0.0); // Inside polygon + } + + #[test] + fn test_distance_point_to_polygon_generic_on_boundary() { + let exterior = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + let point = Point::new(2.0, 0.0); + + let distance = distance_point_to_polygon_generic(&point, &polygon); + assert_relative_eq!(distance, 0.0); // On boundary + } + + #[test] + fn test_distance_point_to_polygon_generic_with_hole() { + let exterior = LineString::from(vec![ + (0.0, 0.0), + (6.0, 0.0), + (6.0, 6.0), + (0.0, 6.0), + (0.0, 0.0), + ]); + let interior = LineString::from(vec![ + (2.0, 2.0), + (4.0, 2.0), + (4.0, 4.0), + (2.0, 4.0), + (2.0, 2.0), + ]); + let polygon = Polygon::new(exterior, vec![interior]); + let point = Point::new(3.0, 3.0); // Inside the hole + + let distance = distance_point_to_polygon_generic(&point, &polygon); + assert_relative_eq!(distance, 1.0); // Distance to closest hole edge + } + + #[test] + fn test_distance_point_to_polygon_generic_empty() { + let empty_polygon = Polygon::new(LineString::::new(vec![]), vec![]); + let point = Point::new(1.0, 1.0); + + let distance = distance_point_to_polygon_generic(&point, &empty_polygon); + assert_relative_eq!(distance, 0.0); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for distance_line_to_line_generic │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_distance_line_to_line_generic_parallel() { + let line1 = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 2.0, y: 0.0 }); + let line2 = Line::new(coord! { x: 0.0, y: 3.0 }, coord! { x: 2.0, y: 3.0 }); + + let distance = distance_line_to_line_generic(&line1, &line2); + assert_relative_eq!(distance, 3.0); + } + + #[test] + fn test_distance_line_to_line_generic_intersecting() { + let line1 = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 2.0, y: 0.0 }); + let line2 = Line::new(coord! { x: 1.0, y: -1.0 }, coord! { x: 1.0, y: 1.0 }); + + let distance = distance_line_to_line_generic(&line1, &line2); + assert_relative_eq!(distance, 0.0, epsilon = 1e-10); + } + + #[test] + fn test_distance_line_to_line_generic_skew() { + let line1 = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 1.0, y: 0.0 }); + let line2 = Line::new(coord! { x: 2.0, y: 1.0 }, coord! { x: 3.0, y: 1.0 }); + + let distance = distance_line_to_line_generic(&line1, &line2); + let expected = ((2.0 - 1.0).powi(2) + (1.0 - 0.0).powi(2)).sqrt(); + assert_relative_eq!(distance, expected); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for distance_linestring_to_polygon_generic │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_distance_linestring_to_polygon_generic_outside() { + let exterior = LineString::from(vec![ + (0.0, 0.0), + (2.0, 0.0), + (2.0, 2.0), + (0.0, 2.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + let linestring = LineString::from(vec![(3.0, 0.0), (4.0, 1.0)]); + + let distance = distance_linestring_to_polygon_generic(&linestring, &polygon); + assert_relative_eq!(distance, 1.0); // Distance to right edge of polygon + } + + #[test] + fn test_distance_linestring_to_polygon_generic_intersecting() { + let exterior = LineString::from(vec![ + (0.0, 0.0), + (2.0, 0.0), + (2.0, 2.0), + (0.0, 2.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + let linestring = LineString::from(vec![(-1.0, 1.0), (3.0, 1.0)]); + + let distance = distance_linestring_to_polygon_generic(&linestring, &polygon); + // The linestring intersects the polygon, so distance should be 0.0 + assert_relative_eq!(distance, 0.0); + } + + #[test] + fn test_distance_linestring_to_polygon_generic_empty_polygon() { + let empty_polygon = Polygon::new(LineString::::new(vec![]), vec![]); + let linestring = LineString::from(vec![(0.0, 0.0), (1.0, 1.0)]); + + let distance = distance_linestring_to_polygon_generic(&linestring, &empty_polygon); + assert_relative_eq!(distance, 0.0); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for distance_polygon_to_polygon_generic │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_distance_polygon_to_polygon_generic_separate() { + let exterior1 = LineString::from(vec![ + (0.0, 0.0), + (2.0, 0.0), + (2.0, 2.0), + (0.0, 2.0), + (0.0, 0.0), + ]); + let polygon1 = Polygon::new(exterior1, vec![]); + + let exterior2 = LineString::from(vec![ + (4.0, 0.0), + (6.0, 0.0), + (6.0, 2.0), + (4.0, 2.0), + (4.0, 0.0), + ]); + let polygon2 = Polygon::new(exterior2, vec![]); + + let distance = distance_polygon_to_polygon_generic(&polygon1, &polygon2); + assert_relative_eq!(distance, 2.0); // Distance between closest edges + } + + #[test] + fn test_distance_polygon_to_polygon_generic_intersecting() { + let exterior1 = LineString::from(vec![ + (0.0, 0.0), + (3.0, 0.0), + (3.0, 3.0), + (0.0, 3.0), + (0.0, 0.0), + ]); + let polygon1 = Polygon::new(exterior1, vec![]); + + let exterior2 = LineString::from(vec![ + (1.0, 1.0), + (4.0, 1.0), + (4.0, 4.0), + (1.0, 4.0), + (1.0, 1.0), + ]); + let polygon2 = Polygon::new(exterior2, vec![]); + + let distance = distance_polygon_to_polygon_generic(&polygon1, &polygon2); + assert_relative_eq!(distance, 0.0, epsilon = 1e-10); // Polygons intersect + } + + #[test] + fn test_distance_polygon_to_polygon_generic_one_in_others_hole() { + let exterior = LineString::from(vec![ + (0.0, 0.0), + (10.0, 0.0), + (10.0, 10.0), + (0.0, 10.0), + (0.0, 0.0), + ]); + let interior = LineString::from(vec![ + (2.0, 2.0), + (8.0, 2.0), + (8.0, 8.0), + (2.0, 8.0), + (2.0, 2.0), + ]); + let polygon_with_hole = Polygon::new(exterior, vec![interior]); + + let small_exterior = LineString::from(vec![ + (4.0, 4.0), + (6.0, 4.0), + (6.0, 6.0), + (4.0, 6.0), + (4.0, 4.0), + ]); + let small_polygon = Polygon::new(small_exterior, vec![]); + + let distance = distance_polygon_to_polygon_generic(&polygon_with_hole, &small_polygon); + assert_relative_eq!(distance, 2.0); // Distance to hole boundary + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for symmetric distance functions │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_symmetric_distance_point_linestring() { + let point = Point::new(1.0, 2.0); + let linestring = LineString::from(vec![(0.0, 0.0), (2.0, 0.0)]); + + let dist1 = distance_point_to_linestring_generic(&point, &linestring); + let dist2 = distance_linestring_to_point_generic(&linestring, &point); + + assert_relative_eq!(dist1, dist2); + assert_relative_eq!(dist1, 2.0); + } + + #[test] + fn test_symmetric_distance_point_polygon() { + let point = Point::new(5.0, 2.0); + let exterior = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + + let dist1 = distance_point_to_polygon_generic(&point, &polygon); + let dist2 = distance_polygon_to_point_generic(&polygon, &point); + + assert_relative_eq!(dist1, dist2); + assert_relative_eq!(dist1, 1.0); + } + + #[test] + fn test_symmetric_distance_linestring_polygon() { + let linestring = LineString::from(vec![(5.0, 1.0), (6.0, 2.0)]); + let exterior = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + + let dist1 = distance_linestring_to_polygon_generic(&linestring, &polygon); + let dist2 = distance_polygon_to_linestring_generic(&polygon, &linestring); + + assert_relative_eq!(dist1, dist2); + assert_relative_eq!(dist1, 1.0); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for line-to-linestring and line-to-polygon functions │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_distance_line_to_linestring_generic() { + let line = Line::new(coord! { x: 0.0, y: 3.0 }, coord! { x: 2.0, y: 3.0 }); + let linestring = LineString::from(vec![(0.0, 0.0), (2.0, 0.0), (2.0, 2.0)]); + + let distance = distance_line_to_linestring_generic(&line, &linestring); + assert_relative_eq!(distance, 1.0); // Distance to closest segment + } + + #[test] + fn test_distance_line_to_polygon_generic() { + let line = Line::new(coord! { x: 5.0, y: 1.0 }, coord! { x: 6.0, y: 2.0 }); + let exterior = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + + let distance = distance_line_to_polygon_generic(&line, &polygon); + assert_relative_eq!(distance, 1.0); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for distance_triangle_to_point_generic │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_distance_triangle_to_point_generic() { + let triangle = Triangle::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + coord! { x: 1.5, y: 3.0 }, + ); + let point = Point::new(1.5, 1.0); // Inside triangle + + let distance = distance_triangle_to_point_generic(&triangle, &point); + assert_relative_eq!(distance, 0.0); + } + + #[test] + fn test_distance_triangle_to_point_generic_outside() { + let triangle = Triangle::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + coord! { x: 1.5, y: 3.0 }, + ); + let point = Point::new(5.0, 0.0); // Outside triangle + + let distance = distance_triangle_to_point_generic(&triangle, &point); + assert_relative_eq!(distance, 2.0); // Distance to right vertex + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Edge case tests │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_empty_geometries_edge_cases() { + // Empty LineString + let empty_ls = LineString::::new(vec![]); + let point = Point::new(1.0, 1.0); + + let dist = distance_point_to_linestring_generic(&point, &empty_ls); + assert_relative_eq!(dist, 0.0); + + // Empty Polygon + let empty_poly = Polygon::new(LineString::::new(vec![]), vec![]); + let dist2 = distance_point_to_polygon_generic(&point, &empty_poly); + assert_relative_eq!(dist2, 0.0); + } + + #[test] + fn test_degenerate_geometries() { + // Single point LineString + let single_point_ls = LineString::from(vec![(1.0, 1.0)]); + let point = Point::new(2.0, 2.0); + + let dist = distance_point_to_linestring_generic(&point, &single_point_ls); + assert_relative_eq!(dist, 0.0); // Should handle gracefully + + // Two identical points in LineString + let two_same_points_ls = LineString::from(vec![(1.0, 1.0), (1.0, 1.0)]); + let dist2 = distance_point_to_linestring_generic(&point, &two_same_points_ls); + assert_relative_eq!(dist2, std::f64::consts::SQRT_2); // Distance to the point + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Performance comparison tests (basic) │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_generic_vs_concrete_point_distance() { + let p1 = Point::new(-72.1235, 42.3521); + let p2 = Point::new(72.1260, 70.612); + + // Test generic implementation + let generic_dist = distance_point_to_point_generic(&p1, &p2); + + // Test concrete implementation via Euclidean trait + let concrete_dist = Euclidean.distance(&p1, &p2); + + // Both should give the same result + assert_relative_eq!(generic_dist, concrete_dist, epsilon = 1e-10); + assert_relative_eq!(generic_dist, 146.99163308930207); + } + + #[test] + fn test_cross_validation_with_existing_tests() { + // Test cases from existing distance.rs tests to ensure compatibility + let o1 = Point::new(8.0, 0.0); + let p1 = Point::new(7.2, 2.0); + let p2 = Point::new(6.0, 1.0); + + // Create line from p1 to p2 + let line_seg = Line::new( + coord! { x: p1.x(), y: p1.y() }, + coord! { x: p2.x(), y: p2.y() }, + ); + + if let Some(o1_coord) = o1.coord_ext() { + let generic_dist = distance_coord_to_line_generic(&o1_coord, &line_seg); + + // This should match the expected value from the original test + assert_relative_eq!(generic_dist, 2.0485900789263356, epsilon = 1e-10); + } + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Property-based tests with random inputs │ + // └────────────────────────────────────────────────────────────┘ + + fn generate_random_point(seed: u64) -> Point { + // Simple LCG for deterministic "random" numbers + let mut rng = seed; + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let x = ((rng >> 16) as i16) as f64 * 0.001; + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let y = ((rng >> 16) as i16) as f64 * 0.001; + Point::new(x, y) + } + + fn generate_random_line(seed: u64) -> Line { + let mut rng = seed; + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let x1 = ((rng >> 16) as i16) as f64 * 0.001; + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let y1 = ((rng >> 16) as i16) as f64 * 0.001; + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let x2 = ((rng >> 16) as i16) as f64 * 0.001; + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let y2 = ((rng >> 16) as i16) as f64 * 0.001; + Line::new(coord! { x: x1, y: y1 }, coord! { x: x2, y: y2 }) + } + + fn generate_random_linestring(seed: u64, num_points: usize) -> LineString { + let mut rng = seed; + let mut points = Vec::new(); + for _ in 0..num_points { + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let x = ((rng >> 16) as i16) as f64 * 0.001; + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let y = ((rng >> 16) as i16) as f64 * 0.001; + points.push((x, y)); + } + LineString::from(points) + } + + fn generate_random_polygon(seed: u64, num_exterior_points: usize) -> Polygon { + let mut rng = seed; + let mut points = Vec::new(); + + // Generate points around a circle to ensure a valid polygon + let center_x = 0.0; + let center_y = 0.0; + let radius = 10.0; + + for i in 0..num_exterior_points { + let angle = 2.0 * std::f64::consts::PI * i as f64 / num_exterior_points as f64; + // Add some random noise + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let noise = ((rng >> 16) as i16) as f64 * 0.0001; + let x = center_x + (radius + noise) * angle.cos(); + let y = center_y + (radius + noise) * angle.sin(); + points.push((x, y)); + } + + // Close the polygon + if !points.is_empty() { + points.push(points[0]); + } + + Polygon::new(LineString::from(points), vec![]) + } + + #[test] + fn test_random_point_to_point_distance() { + // Test point-to-point distance with random inputs + for i in 0..100 { + let seed1 = 12345 + i * 17; + let seed2 = 54321 + i * 23; + + let p1 = generate_random_point(seed1); + let p2 = generate_random_point(seed2); + + let concrete_dist = Euclidean.distance(&p1, &p2); + let generic_dist = distance_point_to_point_generic(&p1, &p2); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-12, + max_relative = 1e-12 + ); + } + } + + #[test] + fn test_random_point_to_linestring_distance() { + // Test point-to-linestring distance with random inputs + for i in 0..100 { + let seed1 = 11111 + i * 31; + let seed2 = 22222 + i * 37; + + let point = generate_random_point(seed1); + let linestring = generate_random_linestring(seed2, 3 + (i % 5) as usize); // 3-7 points + + let concrete_dist = Euclidean.distance(&point, &linestring); + let generic_dist = distance_point_to_linestring_generic(&point, &linestring); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-12, + max_relative = 1e-12 + ); + } + } + + #[test] + fn test_random_point_to_polygon_distance() { + // Test point-to-polygon distance with random inputs + for i in 0..100 { + let seed1 = 33333 + i * 41; + let seed2 = 44444 + i * 43; + + let point = generate_random_point(seed1); + let polygon = generate_random_polygon(seed2, 4 + (i % 4) as usize); // 4-7 sides + + let concrete_dist = Euclidean.distance(&point, &polygon); + let generic_dist = distance_point_to_polygon_generic(&point, &polygon); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-10, + max_relative = 1e-10 + ); + } + } + + #[test] + fn test_random_line_to_line_distance() { + // Test line-to-line distance with random inputs + for i in 0..100 { + let seed1 = 55555 + i * 47; + let seed2 = 66666 + i * 53; + + let line1 = generate_random_line(seed1); + let line2 = generate_random_line(seed2); + + let concrete_dist = Euclidean.distance(&line1, &line2); + let generic_dist = distance_line_to_line_generic(&line1, &line2); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-12, + max_relative = 1e-12 + ); + } + } + + #[test] + fn test_random_linestring_to_linestring_distance() { + // Test linestring-to-linestring distance with random inputs + for i in 0..100 { + let seed1 = 77777 + i * 59; + let seed2 = 88888 + i * 61; + + let ls1 = generate_random_linestring(seed1, 3 + (i % 3) as usize); // 3-5 points + let ls2 = generate_random_linestring(seed2, 3 + ((i + 1) % 3) as usize); // 3-5 points + + let concrete_dist = Euclidean.distance(&ls1, &ls2); + // Use our actual generic implementation via nearest_neighbour_distance + let generic_dist = nearest_neighbour_distance(&ls1, &ls2); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-10, + max_relative = 1e-10 + ); + } + } + + #[test] + fn test_random_polygon_to_polygon_distance() { + // Test polygon-to-polygon distance with random inputs + for i in 0..100 { + let seed1 = 99999 + i * 67; + let seed2 = 10101 + i * 71; + + let poly1 = generate_random_polygon(seed1, 4 + (i % 3) as usize); // 4-6 sides + let poly2 = generate_random_polygon(seed2, 4 + ((i + 1) % 3) as usize); // 4-6 sides + + let concrete_dist = Euclidean.distance(&poly1, &poly2); + let generic_dist = distance_polygon_to_polygon_generic(&poly1, &poly2); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-8, + max_relative = 1e-8 + ); + } + } + + #[test] + fn test_random_line_to_polygon_distance() { + // Test line-to-polygon distance with random inputs + for i in 0..100 { + let seed1 = 12121 + i * 73; + let seed2 = 13131 + i * 79; + + let line = generate_random_line(seed1); + let polygon = generate_random_polygon(seed2, 4 + (i % 3) as usize); // 4-6 sides + + let concrete_dist = Euclidean.distance(&line, &polygon); + let generic_dist = distance_line_to_polygon_generic(&line, &polygon); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-10, + max_relative = 1e-10 + ); + } + } + + #[test] + fn test_random_linestring_to_polygon_distance() { + // Test linestring-to-polygon distance with random inputs + for i in 0..100 { + let seed1 = 14141 + i * 83; + let seed2 = 15151 + i * 89; + + let linestring = generate_random_linestring(seed1, 3 + (i % 3) as usize); // 3-5 points + let polygon = generate_random_polygon(seed2, 4 + (i % 3) as usize); // 4-6 sides + + let concrete_dist = Euclidean.distance(&linestring, &polygon); + let generic_dist = distance_linestring_to_polygon_generic(&linestring, &polygon); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-8, + max_relative = 1e-8 + ); + } + } + + #[test] + fn test_random_symmetry_properties() { + // Test symmetry properties with random inputs + for i in 0..100 { + let seed1 = 16161 + i * 97; + let seed2 = 17171 + i * 101; + + let point = generate_random_point(seed1); + let linestring = generate_random_linestring(seed2, 4); + + // Test point-linestring symmetry + let dist1 = distance_point_to_linestring_generic(&point, &linestring); + let dist2 = distance_linestring_to_point_generic(&linestring, &point); + assert_relative_eq!(dist1, dist2, epsilon = 1e-12); + + // Test with polygon + if i % 2 == 0 { + let polygon = generate_random_polygon(seed1 + seed2, 5); + let dist3 = distance_point_to_polygon_generic(&point, &polygon); + let dist4 = distance_polygon_to_point_generic(&polygon, &point); + assert_relative_eq!(dist3, dist4, epsilon = 1e-10); + } + } + } + + #[test] + fn test_random_edge_cases_and_boundaries() { + // Test edge cases with specific patterns + for i in 0..100 { + // Same point distance should be zero + let point = generate_random_point(12345 + i); + let same_point_dist = distance_point_to_point_generic(&point, &point); + assert_relative_eq!(same_point_dist, 0.0); + + // Zero-length line segment + let coord = coord! { x: point.x(), y: point.y() }; + let zero_line = Line::new(coord, coord); + let dist_to_zero_line = distance_coord_to_line_generic(&coord, &zero_line); + assert_relative_eq!(dist_to_zero_line, 0.0); + + // Point on line segment should have zero distance + let seed = 54321 + i * 13; + let line = generate_random_line(seed); + let start_coord = line.start_coord(); + let dist_to_start = distance_coord_to_line_generic(&start_coord, &line); + assert_relative_eq!(dist_to_start, 0.0, epsilon = 1e-12); + } + } + + #[test] + fn test_random_large_coordinates() { + // Test with large coordinate values to check numerical stability + for i in 0..100 { + let mut rng: u64 = 98765 + i * 107; + + // Generate large coordinates + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let scale = 1e6 + (rng % 1000000) as f64; + + let p1 = Point::new(scale, scale * 0.5); + let p2 = Point::new(scale * 1.1, scale * 0.7); + + let concrete_dist = Euclidean.distance(&p1, &p2); + let generic_dist = distance_point_to_point_generic(&p1, &p2); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-10, + max_relative = 1e-10 + ); + } + } + + #[test] + fn test_random_small_coordinates() { + // Test with very small coordinate values to check numerical precision + for i in 0..100 { + let mut rng: u64 = 13579 + i * 109; + + // Generate small coordinates + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let scale = 1e-6 * (1.0 + (rng % 100) as f64 * 0.01); + + let p1 = Point::new(scale, scale * 0.5); + let p2 = Point::new(scale * 1.1, scale * 0.7); + + let concrete_dist = Euclidean.distance(&p1, &p2); + let generic_dist = distance_point_to_point_generic(&p1, &p2); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-15, + max_relative = 1e-12 + ); + } + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Geometric Edge Cases Tests │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_collinear_linestring_geometries() { + // Test linestrings where all points are collinear + let collinear_ls1 = LineString::from(vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0)]); + let collinear_ls2 = LineString::from(vec![(0.0, 1.0), (1.0, 2.0), (2.0, 3.0)]); + + let concrete_dist = Euclidean.distance(&collinear_ls1, &collinear_ls2); + let generic_dist = nearest_neighbour_distance(&collinear_ls1, &collinear_ls2); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-10); + // Distance should be sqrt(2)/2 (perpendicular distance between parallel lines) + assert_relative_eq!( + concrete_dist, + std::f64::consts::SQRT_2 / 2.0, + epsilon = 1e-10 + ); + } + + #[test] + fn test_degenerate_triangle_as_line() { + // Triangle where all three points are collinear (degenerate triangle) + let degenerate_triangle = Triangle::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 1.0, y: 1.0 }, + coord! { x: 2.0, y: 2.0 }, + ); + let point = Point::new(0.0, 1.0); + + let concrete_dist = Euclidean.distance(°enerate_triangle, &point); + let generic_dist = distance_triangle_to_point_generic(°enerate_triangle, &point); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-10); + // Distance should be sqrt(2)/2 (distance from point to line y=x) + assert_relative_eq!( + concrete_dist, + std::f64::consts::SQRT_2 / 2.0, + epsilon = 1e-10 + ); + } + + #[test] + fn test_self_intersecting_polygon() { + // Create a bowtie/figure-8 shaped self-intersecting polygon + let self_intersecting = LineString::from(vec![ + (0.0, 0.0), + (2.0, 2.0), + (2.0, 0.0), + (0.0, 2.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(self_intersecting, vec![]); + let point = Point::new(3.0, 1.0); // Outside the polygon + + let concrete_dist = Euclidean.distance(&point, &polygon); + let generic_dist = distance_point_to_polygon_generic(&point, &polygon); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-10); + assert_relative_eq!(concrete_dist, 1.0, epsilon = 1e-10); // Distance to closest edge + } + + #[test] + fn test_nearly_touching_geometries() { + // Test geometries separated by very small distances + let epsilon_dist = 1e-12; + + let line1 = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 1.0, y: 0.0 }); + let line2 = Line::new( + coord! { x: 0.0, y: epsilon_dist }, + coord! { x: 1.0, y: epsilon_dist }, + ); + + let concrete_dist = Euclidean.distance(&line1, &line2); + let generic_dist = distance_line_to_line_generic(&line1, &line2); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-15); + assert_relative_eq!(concrete_dist, epsilon_dist, epsilon = 1e-15); + } + + #[test] + fn test_very_close_but_separate_polygons() { + // Two polygons separated by extremely small distance + let tiny_gap = 1e-14; + + let poly1_exterior = LineString::from(vec![ + (0.0, 0.0), + (1.0, 0.0), + (1.0, 1.0), + (0.0, 1.0), + (0.0, 0.0), + ]); + let poly1 = Polygon::new(poly1_exterior, vec![]); + + let poly2_exterior = LineString::from(vec![ + (1.0 + tiny_gap, 0.0), + (2.0 + tiny_gap, 0.0), + (2.0 + tiny_gap, 1.0), + (1.0 + tiny_gap, 1.0), + (1.0 + tiny_gap, 0.0), + ]); + let poly2 = Polygon::new(poly2_exterior, vec![]); + + let concrete_dist = Euclidean.distance(&poly1, &poly2); + let generic_dist = distance_polygon_to_polygon_generic(&poly1, &poly2); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-15); + assert_relative_eq!(concrete_dist, tiny_gap, epsilon = 1e-16); + } + + #[test] + fn test_overlapping_but_not_intersecting_linestrings() { + // LineStrings that overlap in projection but are at different heights + let ls1 = LineString::from(vec![(0.0, 0.0), (2.0, 0.0)]); + let ls2 = LineString::from(vec![(1.0, 1e-13), (3.0, 1e-13)]); + + let concrete_dist = Euclidean.distance(&ls1, &ls2); + let generic_dist = nearest_neighbour_distance(&ls1, &ls2); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-15); + assert_relative_eq!(concrete_dist, 1e-13, epsilon = 1e-16); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Numerical Precision Tests │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_very_close_but_non_zero_distances() { + // Test extremely small but non-zero distances to check floating-point precision + let test_cases = [1e-15, 1e-14, 1e-13, 1e-12, 1e-11, 1e-10]; + + for &tiny_dist in &test_cases { + let p1 = Point::new(0.0, 0.0); + let p2 = Point::new(tiny_dist, 0.0); + + let concrete_dist = Euclidean.distance(&p1, &p2); + let generic_dist = distance_point_to_point_generic(&p1, &p2); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-16); + assert_relative_eq!(concrete_dist, tiny_dist, epsilon = 1e-16); + assert!( + concrete_dist > 0.0, + "Distance should be positive for tiny_dist = {tiny_dist}" + ); + } + } + + #[test] + fn test_numerical_precision_near_floating_point_limits() { + // Test with coordinates that produce distances near floating-point precision limits + let base = 1.0; + let tiny_offset = f64::EPSILON * 10.0; // Slightly above machine epsilon + + let p1 = Point::new(base, base); + let p2 = Point::new(base + tiny_offset, base); + + let concrete_dist = Euclidean.distance(&p1, &p2); + let generic_dist = distance_point_to_point_generic(&p1, &p2); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-15); + assert!(concrete_dist > 0.0); + assert!(concrete_dist < 1e-14); // Should be very small but measurable + } + + #[test] + fn test_precision_with_large_coordinate_differences() { + // Test with one geometry having small coordinates and another having large coordinates + let small_point = Point::new(1e-10, 1e-10); + let large_polygon = Polygon::new( + LineString::from(vec![ + (1e8, 1e8), + (1e8 + 1.0, 1e8), + (1e8 + 1.0, 1e8 + 1.0), + (1e8, 1e8 + 1.0), + (1e8, 1e8), + ]), + vec![], + ); + + let concrete_dist = Euclidean.distance(&small_point, &large_polygon); + let generic_dist = distance_point_to_polygon_generic(&small_point, &large_polygon); + + assert_relative_eq!(concrete_dist, generic_dist, max_relative = 1e-10); + assert!(concrete_dist > 1e7); // Should be very large distance + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Robustness Tests │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_nan_coordinate_handling() { + // Test behavior with NaN coordinates + let nan_point = Point::new(f64::NAN, 0.0); + let normal_point = Point::new(1.0, 1.0); + + let distance = distance_point_to_point_generic(&nan_point, &normal_point); + + // Distance involving NaN should be NaN + assert!( + distance.is_nan(), + "Distance with NaN coordinate should be NaN" + ); + } + + #[test] + fn test_infinity_coordinate_handling() { + // Test behavior with infinite coordinates + let inf_point = Point::new(f64::INFINITY, 0.0); + let normal_point = Point::new(1.0, 1.0); + + let distance = distance_point_to_point_generic(&inf_point, &normal_point); + + // Distance involving infinity should be infinity + assert!( + distance.is_infinite(), + "Distance with infinite coordinate should be infinite" + ); + } + + #[test] + fn test_negative_infinity_coordinate_handling() { + // Test behavior with negative infinite coordinates + let neg_inf_point = Point::new(f64::NEG_INFINITY, 0.0); + let normal_point = Point::new(1.0, 1.0); + + let distance = distance_point_to_point_generic(&neg_inf_point, &normal_point); + + // Distance involving negative infinity should be infinity + assert!( + distance.is_infinite(), + "Distance with negative infinite coordinate should be infinite" + ); + } + + #[test] + fn test_mixed_special_values() { + // Test combinations of NaN and infinity + let nan_point = Point::new(f64::NAN, f64::INFINITY); + let inf_point = Point::new(f64::INFINITY, f64::NEG_INFINITY); + + let distance = distance_point_to_point_generic(&nan_point, &inf_point); + + // Any operation involving NaN should result in NaN or Infinity depending on the math + // Since we're using hypot which can handle NaN differently, let's test that it's either NaN or infinite + assert!( + distance.is_nan() || distance.is_infinite(), + "Distance involving NaN and Infinity should be NaN or Infinite, got: {distance}" + ); + } + + #[test] + fn test_subnormal_number_handling() { + // Test with subnormal (denormalized) numbers + let subnormal = f64::MIN_POSITIVE / 2.0; // This creates a subnormal number + assert!(subnormal > 0.0 && subnormal < f64::MIN_POSITIVE); + + let p1 = Point::new(0.0, 0.0); + let p2 = Point::new(subnormal, 0.0); + + let concrete_dist = Euclidean.distance(&p1, &p2); + let generic_dist = distance_point_to_point_generic(&p1, &p2); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-16); + assert_relative_eq!(concrete_dist, subnormal, epsilon = 1e-16); + assert!(concrete_dist > 0.0); + } + + #[test] + fn test_zero_vs_negative_zero() { + // Test behavior with positive zero vs negative zero + let p1 = Point::new(0.0, 0.0); + let p2 = Point::new(-0.0, -0.0); // Negative zero + + let distance = distance_point_to_point_generic(&p1, &p2); + + // Distance between +0 and -0 should be exactly 0 + assert_eq!( + distance, 0.0, + "Distance between +0 and -0 should be exactly 0" + ); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Algorithmic Correctness Validation Tests │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_linestring_inside_polygon_with_holes_correctness() { + // This test exposes the algorithmic difference between generic and concrete implementations + + // Create a polygon with a hole + let outer = LineString::from(vec![ + (0.0, 0.0), + (10.0, 0.0), + (10.0, 10.0), + (0.0, 10.0), + (0.0, 0.0), + ]); + let hole = LineString::from(vec![ + (3.0, 3.0), + (7.0, 3.0), + (7.0, 7.0), + (3.0, 7.0), + (3.0, 3.0), + ]); + let polygon = Polygon::new(outer, vec![hole]); + + // LineString that is INSIDE the polygon but OUTSIDE the hole + let linestring_inside = LineString::from(vec![(1.0, 1.0), (2.0, 2.0)]); + + let concrete_dist = Euclidean.distance(&linestring_inside, &polygon); + let generic_dist = distance_linestring_to_polygon_generic(&linestring_inside, &polygon); + + // The results should be identical + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-10); + } + + #[test] + fn test_linestring_outside_polygon_with_holes_correctness() { + // Test case where LineString is completely outside the polygon + + let outer = LineString::from(vec![ + (0.0, 0.0), + (10.0, 0.0), + (10.0, 10.0), + (0.0, 10.0), + (0.0, 0.0), + ]); + let hole = LineString::from(vec![ + (3.0, 3.0), + (7.0, 3.0), + (7.0, 7.0), + (3.0, 7.0), + (3.0, 3.0), + ]); + let polygon = Polygon::new(outer, vec![hole]); + + // LineString that is OUTSIDE the polygon entirely + let linestring_outside = LineString::from(vec![(12.0, 12.0), (13.0, 13.0)]); + + let concrete_dist = Euclidean.distance(&linestring_outside, &polygon); + let generic_dist = distance_linestring_to_polygon_generic(&linestring_outside, &polygon); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-10); + } + + #[test] + fn test_linestring_crossing_polygon_boundary_correctness() { + // Test case where LineString crosses the polygon boundary + + let outer = LineString::from(vec![ + (0.0, 0.0), + (10.0, 0.0), + (10.0, 10.0), + (0.0, 10.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(outer, vec![]); + + // LineString that crosses the polygon boundary (should intersect) + let linestring_crossing = LineString::from(vec![(-1.0, 5.0), (11.0, 5.0)]); + + let concrete_dist = Euclidean.distance(&linestring_crossing, &polygon); + let generic_dist = distance_linestring_to_polygon_generic(&linestring_crossing, &polygon); + + // Both should be 0.0 since they intersect + assert_eq!( + concrete_dist, 0.0, + "Concrete should return 0 for intersecting geometries" + ); + assert_eq!( + generic_dist, 0.0, + "Generic should return 0 for intersecting geometries" + ); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-10); + } + + #[test] + fn test_containment_logic_specific() { + // This test specifically checks the containment logic for polygons with holes + use geo_types::{LineString, Polygon}; + + // Create a larger polygon with a hole + let exterior = LineString::from(vec![ + (0.0, 0.0), + (20.0, 0.0), + (20.0, 20.0), + (0.0, 20.0), + (0.0, 0.0), + ]); + let hole = LineString::from(vec![ + (8.0, 8.0), + (12.0, 8.0), + (12.0, 12.0), + (8.0, 12.0), + (8.0, 8.0), + ]); + let polygon = Polygon::new(exterior, vec![hole]); + + // LineString that is INSIDE the polygon but OUTSIDE the hole, + // Create a small LineString very close to itself to avoid intersection + let inside_linestring = LineString::from(vec![(5.0, 5.0), (5.1, 5.1)]); + + let concrete_distance = Euclidean.distance(&inside_linestring, &polygon); + let generic_distance = distance_linestring_to_polygon_generic(&inside_linestring, &polygon); + + // Check if LineString actually intersects with the polygon + use crate::algorithm::Intersects; + let _does_intersect = inside_linestring.intersects(&polygon); + + assert_relative_eq!(concrete_distance, generic_distance, epsilon = 1e-10); + } + + #[test] + fn test_polygon_to_polygon_symmetric_containment_correctness() { + // Test that both A contains B and B contains A cases work correctly + use geo_types::{LineString, Polygon}; + + // Case 1: Large polygon with hole contains small polygon + let large_exterior = LineString::from(vec![ + (0.0, 0.0), + (20.0, 0.0), + (20.0, 20.0), + (0.0, 20.0), + (0.0, 0.0), + ]); + let large_hole = LineString::from(vec![ + (8.0, 8.0), + (12.0, 8.0), + (12.0, 12.0), + (8.0, 12.0), + (8.0, 8.0), + ]); + let large_polygon = Polygon::new(large_exterior, vec![large_hole]); + + // Small polygon inside the large polygon (but outside the hole) + let small_exterior = LineString::from(vec![ + (2.0, 2.0), + (6.0, 2.0), + (6.0, 6.0), + (2.0, 6.0), + (2.0, 2.0), + ]); + let small_polygon = Polygon::new(small_exterior, vec![]); + + // Test A contains B: large polygon with hole contains small polygon + let concrete_dist_ab = Euclidean.distance(&small_polygon, &large_polygon); + let generic_dist_ab = distance_polygon_to_polygon_generic(&small_polygon, &large_polygon); + + // Test B contains A: small polygon contains large polygon (should be distance between exteriors) + let concrete_dist_ba = Euclidean.distance(&large_polygon, &small_polygon); + let generic_dist_ba = distance_polygon_to_polygon_generic(&large_polygon, &small_polygon); + + // Both directions should match between concrete and generic + assert_relative_eq!(concrete_dist_ab, generic_dist_ab, epsilon = 1e-10); + assert_relative_eq!(concrete_dist_ba, generic_dist_ba, epsilon = 1e-10); + + // The distances should be the same due to symmetry + assert_relative_eq!(concrete_dist_ab, concrete_dist_ba, epsilon = 1e-10); + assert_relative_eq!(generic_dist_ab, generic_dist_ba, epsilon = 1e-10); + } + + #[test] + fn test_polygon_to_polygon_both_have_holes_correctness() { + // Test case where both polygons have holes + use geo_types::{LineString, Polygon}; + + // Polygon A with hole + let exterior_a = LineString::from(vec![ + (0.0, 0.0), + (10.0, 0.0), + (10.0, 10.0), + (0.0, 10.0), + (0.0, 0.0), + ]); + let hole_a = LineString::from(vec![ + (3.0, 3.0), + (7.0, 3.0), + (7.0, 7.0), + (3.0, 7.0), + (3.0, 3.0), + ]); + let polygon_a = Polygon::new(exterior_a, vec![hole_a]); + + // Polygon B with hole (separate from A) + let exterior_b = LineString::from(vec![ + (15.0, 0.0), + (25.0, 0.0), + (25.0, 10.0), + (15.0, 10.0), + (15.0, 0.0), + ]); + let hole_b = LineString::from(vec![ + (18.0, 3.0), + (22.0, 3.0), + (22.0, 7.0), + (18.0, 7.0), + (18.0, 3.0), + ]); + let polygon_b = Polygon::new(exterior_b, vec![hole_b]); + + // Neither polygon contains the other, so should calculate distance between exteriors + let concrete_dist = Euclidean.distance(&polygon_a, &polygon_b); + let generic_dist = distance_polygon_to_polygon_generic(&polygon_a, &polygon_b); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-10); + + // Test symmetry + let concrete_dist_reverse = Euclidean.distance(&polygon_b, &polygon_a); + let generic_dist_reverse = distance_polygon_to_polygon_generic(&polygon_b, &polygon_a); + + assert_relative_eq!(concrete_dist_reverse, generic_dist_reverse, epsilon = 1e-10); + assert_relative_eq!(concrete_dist, concrete_dist_reverse, epsilon = 1e-10); + } + + #[test] + fn test_point_to_linestring_containment_optimization() { + // Test that the containment check optimization works correctly + use geo_types::{LineString, Point}; + + // Create a LineString + let linestring = LineString::from(vec![(0.0, 0.0), (5.0, 0.0), (5.0, 5.0), (10.0, 5.0)]); + + // Point ON the LineString (should return 0 due to containment check) + let point_on_line = Point::new(2.5, 0.0); // On first segment + let concrete_dist_on = Euclidean.distance(&point_on_line, &linestring); + let generic_dist_on = distance_point_to_linestring_generic(&point_on_line, &linestring); + + // Both should be exactly 0 due to containment + assert_eq!(concrete_dist_on, 0.0); + assert_eq!(generic_dist_on, 0.0); + assert_relative_eq!(concrete_dist_on, generic_dist_on, epsilon = 1e-10); + + // Point ON a vertex (should return 0) + let point_on_vertex = Point::new(5.0, 0.0); + let concrete_dist_vertex = Euclidean.distance(&point_on_vertex, &linestring); + let generic_dist_vertex = + distance_point_to_linestring_generic(&point_on_vertex, &linestring); + + assert_eq!(concrete_dist_vertex, 0.0); + assert_eq!(generic_dist_vertex, 0.0); + assert_relative_eq!(concrete_dist_vertex, generic_dist_vertex, epsilon = 1e-10); + + // Point NOT on the LineString (should calculate actual distance) + let point_off_line = Point::new(2.5, 3.0); + let concrete_dist_off = Euclidean.distance(&point_off_line, &linestring); + let generic_dist_off = distance_point_to_linestring_generic(&point_off_line, &linestring); + + // Should be greater than 0 and both implementations should match + assert!(concrete_dist_off > 0.0); + assert!(generic_dist_off > 0.0); + assert_relative_eq!(concrete_dist_off, generic_dist_off, epsilon = 1e-10); + } + + #[test] + fn test_line_segment_distance_algorithm_equivalence() { + // Test that the updated generic algorithm produces identical results to concrete + use geo_types::{coord, Line, Point}; + + // Test cases covering different scenarios + let test_cases = vec![ + // Point, Line start, Line end + ( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + ), // Before start + ( + coord! { x: 2.0, y: 1.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + ), // Perpendicular + ( + coord! { x: 4.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + ), // Beyond end + ( + coord! { x: 2.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + ), // On line + ( + coord! { x: 1.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + ), // On start point + ( + coord! { x: 3.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + ), // On end point + ( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 1.0, y: 1.0 }, + coord! { x: 1.0, y: 1.0 }, + ), // Degenerate line + ( + coord! { x: 2.5, y: 3.0 }, + coord! { x: 0.0, y: 0.0 }, + coord! { x: 5.0, y: 5.0 }, + ), // Diagonal line + ]; + + for (point_coord, start_coord, end_coord) in test_cases { + let point = Point::from(point_coord); + let line = Line::new(start_coord, end_coord); + + // Test concrete implementation + let concrete_distance = Euclidean.distance(&point, &line); + + // Test generic implementation + let generic_distance = distance_coord_to_line_generic(&point_coord, &line); + + // They should be identical now + assert_relative_eq!(concrete_distance, generic_distance, epsilon = 1e-15); + } + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/mod.rs b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/mod.rs new file mode 100644 index 00000000..c935da8c --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/mod.rs @@ -0,0 +1,19 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +pub mod euclidean; +pub use euclidean::DistanceExt; +pub use euclidean::Euclidean; diff --git a/rust/sedona-geo-generic-alg/src/algorithm/line_measures/mod.rs b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/mod.rs new file mode 100644 index 00000000..c098604d --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/mod.rs @@ -0,0 +1,24 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +mod distance; +pub use distance::{Distance, DistanceExt}; + +mod length; +pub use length::LengthMeasurableExt; + +pub mod metric_spaces; +pub use metric_spaces::Euclidean; diff --git a/rust/sedona-geo-generic-alg/src/algorithm/map_coords.rs b/rust/sedona-geo-generic-alg/src/algorithm/map_coords.rs new file mode 100644 index 00000000..b70c53b1 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/map_coords.rs @@ -0,0 +1,1096 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +pub(crate) use crate::geometry::*; +pub(crate) use crate::CoordNum; + +use core::borrow::Borrow; +use sedona_geo_traits_ext::*; +use Coord; + +/// Map a function over all the coordinates in an object, returning a new one +pub trait MapCoords { + type Output; + + /// Apply a function to all the coordinates in a geometric object, returning a new object. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::MapCoords; + /// use sedona_geo_generic_alg::{Coord, Point}; + /// use approx::assert_relative_eq; + /// + /// let p1 = Point::new(10., 20.); + /// let p2 = p1.map_coords(|Coord { x, y }| Coord { x: x + 1000., y: y * 2. }); + /// + /// assert_relative_eq!(p2, Point::new(1010., 40.), epsilon = 1e-6); + /// ``` + /// + /// Note that the input and output numeric types need not match. + /// + /// For example, consider OpenStreetMap's coordinate encoding scheme, which, to save space, + /// encodes latitude/longitude as 32bit signed integers from the floating point values + /// to six decimal places (eg. lat/lon * 1000000). + /// + /// ``` + /// # use geo::{Coord, Point}; + /// # use geo::MapCoords; + /// # use approx::assert_relative_eq; + /// + /// let SCALE_FACTOR: f64 = 1000000.0; + /// let floating_point_geom: Point = Point::new(10.15f64, 20.05f64); + /// let fixed_point_geom: Point = floating_point_geom.map_coords(|Coord { x, y }| { + /// Coord { x: (x * SCALE_FACTOR) as i32, y: (y * SCALE_FACTOR) as i32 } + /// }); + /// + /// assert_eq!(fixed_point_geom.x(), 10150000); + /// ``` + /// + /// If you want *only* to convert between numeric types (i32 -> f64) without further + /// transformation, consider using [`Convert`](crate::Convert). + fn map_coords(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output + where + T: CoordNum, + NT: CoordNum; + + /// Map a fallible function over all the coordinates in a geometry, returning a Result + /// + /// # Examples + /// + /// ``` + /// use approx::assert_relative_eq; + /// use sedona_geo_generic_alg::MapCoords; + /// use sedona_geo_generic_alg::{Coord, Point}; + /// + /// let p1 = Point::new(10., 20.); + /// let p2 = p1 + /// .try_map_coords(|Coord { x, y }| -> Result<_, std::convert::Infallible> { + /// Ok(Coord { x: x + 1000., y: y * 2. }) + /// }).unwrap(); + /// + /// assert_relative_eq!(p2, Point::new(1010., 40.), epsilon = 1e-6); + /// ``` + fn try_map_coords( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result + where + T: CoordNum, + NT: CoordNum; +} + +pub trait MapCoordsInPlace { + /// Apply a function to all the coordinates in a geometric object, in place + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::MapCoordsInPlace; + /// use sedona_geo_generic_alg::{Coord, Point}; + /// use approx::assert_relative_eq; + /// + /// let mut p = Point::new(10., 20.); + /// p.map_coords_in_place(|Coord { x, y }| Coord { x: x + 1000., y: y * 2. }); + /// + /// assert_relative_eq!(p, Point::new(1010., 40.), epsilon = 1e-6); + /// ``` + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord + Copy) + where + T: CoordNum; + + /// Map a fallible function over all the coordinates in a geometry, in place, returning a `Result`. + /// + /// Upon encountering an `Err` from the function, `try_map_coords_in_place` immediately returns + /// and the geometry is potentially left in a partially mapped state. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::MapCoordsInPlace; + /// use sedona_geo_generic_alg::Coord; + /// + /// let mut p1 = geo::point!{x: 10u32, y: 20u32}; + /// + /// p1.try_map_coords_in_place(|Coord { x, y }| -> Result<_, &str> { + /// Ok(Coord { + /// x: x.checked_add(1000).ok_or("Overflow")?, + /// y: y.checked_mul(2).ok_or("Overflow")?, + /// }) + /// })?; + /// + /// assert_eq!( + /// p1, + /// geo::point!{x: 1010u32, y: 40u32}, + /// ); + /// # Ok::<(), &str>(()) + /// ``` + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> + where + T: CoordNum; +} + +// Generic implementation using trait-based approach +impl MapCoords for G +where + T: CoordNum, + NT: CoordNum, + G: GeoTraitExtWithTypeTag + MapCoordsTrait, +{ + type Output = >::Output; + + fn map_coords(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + self.map_coords_trait(func) + } + + fn try_map_coords( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + self.try_map_coords_trait(func) + } +} + +pub trait MapCoordsTrait +where + T: CoordNum, + NT: CoordNum, +{ + type Output; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output; + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result; +} + +//-----------------------// +// Point implementations // +//-----------------------// + +impl MapCoordsTrait for P +where + T: CoordNum, + NT: CoordNum, + P: PointTraitExt, +{ + type Output = Point; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + if let Some(coord) = self.geo_coord() { + Point(func(coord)) + } else { + Point::new(NT::zero(), NT::zero()) + } + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + if let Some(coord) = self.geo_coord() { + Ok(Point(func(coord)?)) + } else { + Ok(Point::new(NT::zero(), NT::zero())) + } + } +} + +impl MapCoordsInPlace for Point { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord) { + self.0 = func(self.0); + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + self.0 = func(self.0)?; + Ok(()) + } +} + +//----------------------// +// Line implementations // +//----------------------// + +impl MapCoordsTrait for L +where + T: CoordNum, + NT: CoordNum, + L: LineTraitExt, +{ + type Output = Line; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + Line::new(func(self.start_coord()), func(self.end_coord())) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + Ok(Line::new( + func(self.start_coord())?, + func(self.end_coord())?, + )) + } +} + +impl MapCoordsInPlace for Line { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord) { + self.start = func(self.start); + self.end = func(self.end); + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + self.start = func(self.start)?; + self.end = func(self.end)?; + + Ok(()) + } +} + +//----------------------------// +// LineString implementations // +//----------------------------// + +impl MapCoordsTrait for LS +where + T: CoordNum, + NT: CoordNum, + LS: LineStringTraitExt, +{ + type Output = LineString; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + let coords = self.coord_iter().map(func).collect::>(); + LineString::new(coords) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + let coords = self.coord_iter().map(func).collect::, E>>()?; + Ok(LineString::new(coords)) + } +} + +impl MapCoordsInPlace for LineString { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord) { + for p in &mut self.0 { + *p = func(*p); + } + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + for p in &mut self.0 { + *p = func(*p)?; + } + Ok(()) + } +} + +//-------------------------// +// Polygon implementations // +//-------------------------// + +impl MapCoordsTrait for P +where + T: CoordNum, + NT: CoordNum, + P: PolygonTraitExt, +{ + type Output = Polygon; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + let exterior = match self.exterior_ext() { + Some(ext) => ext.map_coords(func), + None => LineString::new(vec![]), + }; + + let interiors = self + .interiors_ext() + .map(|line| line.map_coords(func)) + .collect(); + + Polygon::new(exterior, interiors) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + let exterior = match self.exterior_ext() { + Some(ext) => ext.try_map_coords(func)?, + None => LineString::new(vec![]), + }; + + let interiors = self + .interiors_ext() + .map(|line| line.try_map_coords(func)) + .collect::, E>>()?; + + Ok(Polygon::new(exterior, interiors)) + } +} + +impl MapCoordsInPlace for Polygon { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord + Copy) { + self.exterior_mut(|line_string| { + line_string.map_coords_in_place(func); + }); + + self.interiors_mut(|line_strings| { + for line_string in line_strings { + line_string.map_coords_in_place(func); + } + }); + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + let mut result = Ok(()); + + self.exterior_mut(|line_string| { + if let Err(e) = line_string.try_map_coords_in_place(&func) { + result = Err(e); + } + }); + + if result.is_ok() { + self.interiors_mut(|line_strings| { + for line_string in line_strings { + if let Err(e) = line_string.try_map_coords_in_place(&func) { + result = Err(e); + break; + } + } + }); + } + + result + } +} + +//----------------------------// +// MultiPoint implementations // +//----------------------------// + +impl MapCoordsTrait for MP +where + T: CoordNum, + NT: CoordNum, + MP: MultiPointTraitExt, +{ + type Output = MultiPoint; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + MultiPoint::new(self.points_ext().map(|p| p.map_coords(func)).collect()) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + Ok(MultiPoint::new( + self.points_ext() + .map(|p| p.try_map_coords(func)) + .collect::, E>>()?, + )) + } +} + +impl MapCoordsInPlace for MultiPoint { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord + Copy) { + for p in &mut self.0 { + p.map_coords_in_place(func); + } + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + for p in &mut self.0 { + p.try_map_coords_in_place(&func)?; + } + Ok(()) + } +} + +//---------------------------------// +// MultiLineString implementations // +//---------------------------------// + +impl MapCoordsTrait for MLS +where + T: CoordNum, + NT: CoordNum, + MLS: MultiLineStringTraitExt, +{ + type Output = MultiLineString; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + MultiLineString::new( + self.line_strings_ext() + .map(|l| l.map_coords(func)) + .collect(), + ) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + Ok(MultiLineString::new( + self.line_strings_ext() + .map(|l| l.try_map_coords(func)) + .collect::, E>>()?, + )) + } +} + +impl MapCoordsInPlace for MultiLineString { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord + Copy) { + for p in &mut self.0 { + p.map_coords_in_place(func); + } + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + for p in &mut self.0 { + p.try_map_coords_in_place(&func)?; + } + Ok(()) + } +} + +//------------------------------// +// MultiPolygon implementations // +//------------------------------// + +impl MapCoordsTrait for MP +where + T: CoordNum, + NT: CoordNum, + MP: MultiPolygonTraitExt, +{ + type Output = MultiPolygon; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + MultiPolygon::new(self.polygons_ext().map(|p| p.map_coords(func)).collect()) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + Ok(MultiPolygon::new( + self.polygons_ext() + .map(|p| p.try_map_coords(func)) + .collect::, E>>()?, + )) + } +} + +impl MapCoordsInPlace for MultiPolygon { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord + Copy) { + for p in &mut self.0 { + p.map_coords_in_place(func); + } + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + for p in &mut self.0 { + p.try_map_coords_in_place(&func)?; + } + Ok(()) + } +} + +//--------------------------// +// Geometry implementations // +//--------------------------// + +impl MapCoordsTrait for G +where + T: CoordNum, + NT: CoordNum, + G: GeometryTraitExt, +{ + type Output = Geometry; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + if self.is_collection() { + let collection = GeometryCollection::new_from( + self.geometries_ext() + .map(|g| g.borrow().map_coords(func)) + .collect(), + ); + Geometry::GeometryCollection(collection) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(x) => Geometry::Point(x.map_coords_trait(func)), + GeometryTypeExt::Line(x) => Geometry::Line(x.map_coords_trait(func)), + GeometryTypeExt::LineString(x) => Geometry::LineString(x.map_coords_trait(func)), + GeometryTypeExt::Polygon(x) => Geometry::Polygon(x.map_coords_trait(func)), + GeometryTypeExt::MultiPoint(x) => Geometry::MultiPoint(x.map_coords_trait(func)), + GeometryTypeExt::MultiLineString(x) => { + Geometry::MultiLineString(x.map_coords_trait(func)) + } + GeometryTypeExt::MultiPolygon(x) => { + Geometry::MultiPolygon(x.map_coords_trait(func)) + } + GeometryTypeExt::Rect(x) => Geometry::Rect(x.map_coords_trait(func)), + GeometryTypeExt::Triangle(x) => Geometry::Triangle(x.map_coords_trait(func)), + } + } + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + if self.is_collection() { + let geoms = self + .geometries_ext() + .map(|g| g.borrow().try_map_coords(func)) + .collect::, E>>()?; + let collection = GeometryCollection::new_from(geoms); + Ok(Geometry::GeometryCollection(collection)) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(x) => Ok(Geometry::Point(x.try_map_coords_trait(func)?)), + GeometryTypeExt::Line(x) => Ok(Geometry::Line(x.try_map_coords_trait(func)?)), + GeometryTypeExt::LineString(x) => { + Ok(Geometry::LineString(x.try_map_coords_trait(func)?)) + } + GeometryTypeExt::Polygon(x) => Ok(Geometry::Polygon(x.try_map_coords_trait(func)?)), + GeometryTypeExt::MultiPoint(x) => { + Ok(Geometry::MultiPoint(x.try_map_coords_trait(func)?)) + } + GeometryTypeExt::MultiLineString(x) => { + Ok(Geometry::MultiLineString(x.try_map_coords_trait(func)?)) + } + GeometryTypeExt::MultiPolygon(x) => { + Ok(Geometry::MultiPolygon(x.try_map_coords_trait(func)?)) + } + GeometryTypeExt::Rect(x) => Ok(Geometry::Rect(x.try_map_coords_trait(func)?)), + GeometryTypeExt::Triangle(x) => { + Ok(Geometry::Triangle(x.try_map_coords_trait(func)?)) + } + } + } + } +} + +impl MapCoordsInPlace for Geometry { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord + Copy) { + match *self { + Geometry::Point(ref mut x) => x.map_coords_in_place(func), + Geometry::Line(ref mut x) => x.map_coords_in_place(func), + Geometry::LineString(ref mut x) => x.map_coords_in_place(func), + Geometry::Polygon(ref mut x) => x.map_coords_in_place(func), + Geometry::MultiPoint(ref mut x) => x.map_coords_in_place(func), + Geometry::MultiLineString(ref mut x) => x.map_coords_in_place(func), + Geometry::MultiPolygon(ref mut x) => x.map_coords_in_place(func), + Geometry::GeometryCollection(ref mut x) => x.map_coords_in_place(func), + Geometry::Rect(ref mut x) => x.map_coords_in_place(func), + Geometry::Triangle(ref mut x) => x.map_coords_in_place(func), + } + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + match *self { + Geometry::Point(ref mut x) => x.try_map_coords_in_place(func), + Geometry::Line(ref mut x) => x.try_map_coords_in_place(func), + Geometry::LineString(ref mut x) => x.try_map_coords_in_place(func), + Geometry::Polygon(ref mut x) => x.try_map_coords_in_place(func), + Geometry::MultiPoint(ref mut x) => x.try_map_coords_in_place(func), + Geometry::MultiLineString(ref mut x) => x.try_map_coords_in_place(func), + Geometry::MultiPolygon(ref mut x) => x.try_map_coords_in_place(func), + Geometry::GeometryCollection(ref mut x) => x.try_map_coords_in_place(func), + Geometry::Rect(ref mut x) => x.try_map_coords_in_place(func), + Geometry::Triangle(ref mut x) => x.try_map_coords_in_place(func), + } + } +} + +//------------------------------------// +// GeometryCollection implementations // +//------------------------------------// + +impl MapCoordsTrait for GC +where + T: CoordNum, + NT: CoordNum, + GC: GeometryCollectionTraitExt, +{ + type Output = GeometryCollection; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + GeometryCollection::new_from(self.geometries_ext().map(|g| g.map_coords(func)).collect()) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + Ok(GeometryCollection::new_from( + self.geometries_ext() + .map(|g| g.try_map_coords(func)) + .collect::, E>>()?, + )) + } +} + +impl MapCoordsInPlace for GeometryCollection { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord + Copy) { + for p in &mut self.0 { + p.map_coords_in_place(func); + } + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + for p in &mut self.0 { + p.try_map_coords_in_place(&func)?; + } + Ok(()) + } +} + +//----------------------// +// Rect implementations // +//----------------------// + +impl MapCoordsTrait for R +where + T: CoordNum, + NT: CoordNum, + R: RectTraitExt, +{ + type Output = Rect; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + Rect::new(func(self.min_coord()), func(self.max_coord())) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result { + Ok(Rect::new(func(self.min_coord())?, func(self.max_coord())?)) + } +} + +impl MapCoordsInPlace for Rect { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord) { + let mut new_rect = Rect::new(func(self.min()), func(self.max())); + ::std::mem::swap(self, &mut new_rect); + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + let mut new_rect = Rect::new(func(self.min())?, func(self.max())?); + ::std::mem::swap(self, &mut new_rect); + Ok(()) + } +} + +//--------------------------// +// Triangle implementations // +//--------------------------// + +impl MapCoordsTrait for TT +where + T: CoordNum, + NT: CoordNum, + TT: TriangleTraitExt, +{ + type Output = Triangle; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + Triangle::new( + func(self.first_coord()), + func(self.second_coord()), + func(self.third_coord()), + ) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result { + Ok(Triangle::new( + func(self.first_coord())?, + func(self.second_coord())?, + func(self.third_coord())?, + )) + } +} + +impl MapCoordsInPlace for Triangle { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord) { + let mut new_triangle = Triangle::new(func(self.0), func(self.1), func(self.2)); + + ::std::mem::swap(self, &mut new_triangle); + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + let mut new_triangle = Triangle::new(func(self.0)?, func(self.1)?, func(self.2)?); + + ::std::mem::swap(self, &mut new_triangle); + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::{MapCoords, MapCoordsInPlace}; + use crate::{ + coord, polygon, Coord, Geometry, GeometryCollection, Line, LineString, MultiLineString, + MultiPoint, MultiPolygon, Point, Polygon, Rect, + }; + + #[test] + fn point() { + let p = Point::new(10., 10.); + let new_p = p.map_coords(|Coord { x, y }| (x + 10., y + 100.).into()); + assert_relative_eq!(new_p.x(), 20.); + assert_relative_eq!(new_p.y(), 110.); + } + + #[test] + fn point_inplace() { + let mut p2 = Point::new(10f32, 10f32); + p2.map_coords_in_place(|Coord { x, y }| (x + 10., y + 100.).into()); + assert_relative_eq!(p2.x(), 20.); + assert_relative_eq!(p2.y(), 110.); + } + + #[test] + fn rect_inplace() { + let mut rect = Rect::new((10, 10), (20, 20)); + rect.map_coords_in_place(|Coord { x, y }| (x + 10, y + 20).into()); + assert_eq!(rect.min(), coord! { x: 20, y: 30 }); + assert_eq!(rect.max(), coord! { x: 30, y: 40 }); + } + + #[test] + fn rect_inplace_normalized() { + let mut rect = Rect::new((2, 2), (3, 3)); + // Rect's enforce that rect.min is up and left of p2. Here we test that the points are + // normalized into a valid rect, regardless of the order they are mapped. + rect.map_coords_in_place(|pt| { + match pt.x_y() { + // old min point maps to new max point + (2, 2) => (4, 4).into(), + // old max point maps to new min point + (3, 3) => (1, 1).into(), + _ => panic!("unexpected point"), + } + }); + + assert_eq!(rect.min(), coord! { x: 1, y: 1 }); + assert_eq!(rect.max(), coord! { x: 4, y: 4 }); + } + + #[test] + fn rect_map_coords() { + let rect = Rect::new((10, 10), (20, 20)); + let another_rect = rect.map_coords(|Coord { x, y }| (x + 10, y + 20).into()); + assert_eq!(another_rect.min(), coord! { x: 20, y: 30 }); + assert_eq!(another_rect.max(), coord! { x: 30, y: 40 }); + } + + #[test] + fn rect_try_map_coords() { + let rect = Rect::new((10i32, 10), (20, 20)); + let result = rect.try_map_coords(|Coord { x, y }| -> Result<_, &'static str> { + Ok(( + x.checked_add(10).ok_or("overflow")?, + y.checked_add(20).ok_or("overflow")?, + ) + .into()) + }); + assert!(result.is_ok()); + } + + #[test] + fn rect_try_map_coords_normalized() { + let rect = Rect::new((2, 2), (3, 3)); + // Rect's enforce that rect.min is up and left of p2. Here we test that the points are + // normalized into a valid rect, regardless of the order they are mapped. + let result: Result<_, std::convert::Infallible> = rect.try_map_coords(|pt| { + match pt.x_y() { + // old min point maps to new max point + (2, 2) => Ok((4, 4).into()), + // old max point maps to new min point + (3, 3) => Ok((1, 1).into()), + _ => panic!("unexpected point"), + } + }); + let new_rect = result.unwrap(); + assert_eq!(new_rect.min(), coord! { x: 1, y: 1 }); + assert_eq!(new_rect.max(), coord! { x: 4, y: 4 }); + } + + #[test] + fn line() { + let line = Line::from([(0., 0.), (1., 2.)]); + assert_relative_eq!( + line.map_coords(|Coord { x, y }| (x * 2., y).into()), + Line::from([(0., 0.), (2., 2.)]), + epsilon = 1e-6 + ); + } + + #[test] + fn linestring() { + let line1: LineString = LineString::from(vec![(0., 0.), (1., 2.)]); + let line2 = line1.map_coords(|Coord { x, y }| (x + 10., y - 100.).into()); + assert_relative_eq!(line2.0[0], Coord::from((10., -100.)), epsilon = 1e-6); + assert_relative_eq!(line2.0[1], Coord::from((11., -98.)), epsilon = 1e-6); + } + + #[test] + fn polygon() { + let exterior = LineString::from(vec![(0., 0.), (1., 1.), (1., 0.), (0., 0.)]); + let interiors = vec![LineString::from(vec![ + (0.1, 0.1), + (0.9, 0.9), + (0.9, 0.1), + (0.1, 0.1), + ])]; + let p = Polygon::new(exterior, interiors); + + let p2 = p.map_coords(|Coord { x, y }| (x + 10., y - 100.).into()); + + let exterior2 = + LineString::from(vec![(10., -100.), (11., -99.), (11., -100.), (10., -100.)]); + let interiors2 = vec![LineString::from(vec![ + (10.1, -99.9), + (10.9, -99.1), + (10.9, -99.9), + (10.1, -99.9), + ])]; + let expected_p2 = Polygon::new(exterior2, interiors2); + + assert_relative_eq!(p2, expected_p2, epsilon = 1e-6); + } + + #[test] + fn multipoint() { + let p1 = Point::new(10., 10.); + let p2 = Point::new(0., -100.); + let mp = MultiPoint::new(vec![p1, p2]); + + assert_eq!( + mp.map_coords(|Coord { x, y }| (x + 10., y + 100.).into()), + MultiPoint::new(vec![Point::new(20., 110.), Point::new(10., 0.)]) + ); + } + + #[test] + fn multilinestring() { + let line1: LineString = LineString::from(vec![(0., 0.), (1., 2.)]); + let line2: LineString = LineString::from(vec![(-1., 0.), (0., 0.), (1., 2.)]); + let mline = MultiLineString::new(vec![line1, line2]); + let mline2 = mline.map_coords(|Coord { x, y }| (x + 10., y - 100.).into()); + assert_relative_eq!( + mline2, + MultiLineString::new(vec![ + LineString::from(vec![(10., -100.), (11., -98.)]), + LineString::from(vec![(9., -100.), (10., -100.), (11., -98.)]), + ]), + epsilon = 1e-6 + ); + } + + #[test] + fn multipolygon() { + let poly1 = polygon![ + (x: 0., y: 0.), + (x: 10., y: 0.), + (x: 10., y: 10.), + (x: 0., y: 10.), + (x: 0., y: 0.), + ]; + let poly2 = polygon![ + exterior: [ + (x: 11., y: 11.), + (x: 20., y: 11.), + (x: 20., y: 20.), + (x: 11., y: 20.), + (x: 11., y: 11.), + ], + interiors: [ + [ + (x: 13., y: 13.), + (x: 13., y: 17.), + (x: 17., y: 17.), + (x: 17., y: 13.), + (x: 13., y: 13.), + ] + ], + ]; + + let mp = MultiPolygon::new(vec![poly1, poly2]); + let mp2 = mp.map_coords(|Coord { x, y }| (x * 2., y + 100.).into()); + assert_eq!(mp2.0.len(), 2); + assert_relative_eq!( + mp2.0[0], + polygon![ + (x: 0., y: 100.), + (x: 20., y: 100.), + (x: 20., y: 110.), + (x: 0., y: 110.), + (x: 0., y: 100.), + ], + ); + assert_relative_eq!( + mp2.0[1], + polygon![ + exterior: [ + (x: 22., y: 111.), + (x: 40., y: 111.), + (x: 40., y: 120.), + (x: 22., y: 120.), + (x: 22., y: 111.), + ], + interiors: [ + [ + (x: 26., y: 113.), + (x: 26., y: 117.), + (x: 34., y: 117.), + (x: 34., y: 113.), + (x: 26., y: 113.), + ], + ], + ], + ); + } + + #[test] + fn geometrycollection() { + let p1 = Geometry::Point(Point::new(10., 10.)); + let line1 = Geometry::LineString(LineString::from(vec![(0., 0.), (1., 2.)])); + + let gc = GeometryCollection::new_from(vec![p1, line1]); + let expected = GeometryCollection::new_from(vec![ + Geometry::Point(Point::new(20., 110.)), + Geometry::LineString(LineString::from(vec![(10., 100.), (11., 102.)])), + ]); + + assert_eq!( + gc.map_coords(|Coord { x, y }| (x + 10., y + 100.).into()), + expected + ); + assert_eq!( + Geometry::GeometryCollection(gc) + .map_coords(|Coord { x, y }| (x + 10., y + 100.).into()), + Geometry::GeometryCollection(expected) + ); + } + + #[test] + fn convert_type() { + let p1: Point = Point::new(1., 2.); + let p2: Point = p1.map_coords(|Coord { x, y }| (x as f32, y as f32).into()); + assert_relative_eq!(p2.x(), 1f32); + assert_relative_eq!(p2.y(), 2f32); + } + + #[test] + fn test_fallible() { + let f = |Coord { x, y }| -> Result<_, &'static str> { + if relative_ne!(x, 2.0) { + Ok((x * 2., y + 100.).into()) + } else { + Err("Ugh") + } + }; + // this should produce an error + let bad_ls: LineString<_> = vec![ + Point::new(1.0, 1.0), + Point::new(2.0, 2.0), + Point::new(3.0, 3.0), + ] + .into(); + // this should be fine + let good_ls: LineString<_> = vec![ + Point::new(1.0, 1.0), + Point::new(2.1, 2.0), + Point::new(3.0, 3.0), + ] + .into(); + let bad = bad_ls.try_map_coords(f); + assert!(bad.is_err()); + let good = good_ls.try_map_coords(f); + assert!(good.is_ok()); + assert_relative_eq!( + good.unwrap(), + vec![ + Point::new(2., 101.), + Point::new(4.2, 102.), + Point::new(6.0, 103.), + ] + .into() + ); + } + + #[test] + fn rect_map_invert_coords() { + let rect = Rect::new(coord! { x: 0., y: 0. }, coord! { x: 1., y: 1. }); + + // This call should not panic even though Rect::new + // constructor panics if min coords > max coords + rect.map_coords(|Coord { x, y }| (-x, -y).into()); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/mod.rs b/rust/sedona-geo-generic-alg/src/algorithm/mod.rs new file mode 100644 index 00000000..e208a98f --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/mod.rs @@ -0,0 +1,98 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +/// Kernels to compute various predicates +pub mod kernels; +pub use kernels::{Kernel, Orientation}; + +/// Calculate the area of the surface of a `Geometry`. +pub mod area; +pub use area::Area; + +/// Calculate the bounding rectangle of a `Geometry`. +pub mod bounding_rect; +pub use bounding_rect::BoundingRect; + +/// Calculate the centroid of a `Geometry`. +pub mod centroid; +pub use centroid::Centroid; + +/// Convert the type of a geometry’s coordinate value. +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}; + +/// Determine whether a `Coord` lies inside, outside, or on the boundary of a geometry. +pub mod coordinate_position; +pub use coordinate_position::CoordinatePosition; + +/// Iterate over geometry coordinates. +pub mod coords_iter; +pub use coords_iter::CoordsIter; +pub use coords_iter::CoordsSeqIter; + +/// Dimensionality of a geometry and its boundary, based on OGC-SFA. +pub mod dimensions; +pub use dimensions::HasDimensions; + +/// Calculate the length of a planar line between two `Geometries`. +pub mod euclidean_length; +#[allow(deprecated)] +pub use euclidean_length::EuclideanLength; + +/// Calculate the extreme coordinates and indices of a geometry. +pub mod extremes; +pub use extremes::Extremes; + +/// Determine whether `Geometry` `A` intersects `Geometry` `B`. +pub mod intersects; +pub use intersects::Intersects; + +pub mod line_measures; +pub use line_measures::metric_spaces::Euclidean; + +pub use line_measures::{Distance, DistanceExt, LengthMeasurableExt}; + +/// Apply a function to all `Coord`s of a `Geometry`. +pub mod map_coords; +pub use map_coords::{MapCoords, MapCoordsInPlace}; + +/// Rotate a `Geometry` by an angle given in degrees. +pub mod rotate; +pub use rotate::{Rotate, RotateMut}; + +/// Scale a `Geometry` up or down by a factor +pub mod scale; +pub use scale::{Scale, ScaleMut}; + +/// Skew a `Geometry` by shearing it at angles along the x and y dimensions +pub mod skew; +pub use skew::{Skew, SkewMut}; + +/// Composable affine operations such as rotate, scale, skew, and translate +pub mod affine_ops; +pub use affine_ops::{AffineOps, AffineOpsMut, AffineTransform}; + +/// Simplify `Geometries` using the Ramer-Douglas-Peucker algorithm. +pub mod simplify; +pub use simplify::{Simplify, SimplifyIdx}; + +/// Translate a `Geometry` along the given offsets. +pub mod translate; +pub use translate::{Translate, TranslateMut}; diff --git a/rust/sedona-geo-generic-alg/src/algorithm/rotate.rs b/rust/sedona-geo-generic-alg/src/algorithm/rotate.rs new file mode 100644 index 00000000..9906b0bf --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/rotate.rs @@ -0,0 +1,619 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use crate::algorithm::affine_ops::AffineOpsMut; +use crate::algorithm::{AffineOps, AffineTransform, BoundingRect, Centroid}; +use crate::geometry::*; +use crate::CoordFloat; + +/// Rotate a geometry around a point by an angle, in degrees. +/// +/// Positive angles are counter-clockwise, and negative angles are clockwise rotations. +/// +/// ## Performance +/// +/// If you will be performing multiple transformations, like [`Scale`](crate::Scale), +/// [`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`] trait. +pub trait Rotate { + /// The output type of the rotation operations + type Output; + + /// Rotate a geometry around its [centroid](Centroid) by an angle, in degrees + /// + /// Positive angles are counter-clockwise, and negative angles are clockwise rotations. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Rotate; + /// use sedona_geo_generic_alg::line_string; + /// use approx::assert_relative_eq; + /// + /// let line_string = line_string![ + /// (x: 0.0, y: 0.0), + /// (x: 5.0, y: 5.0), + /// (x: 10.0, y: 10.0), + /// ]; + /// + /// let rotated = line_string.rotate_around_centroid(-45.0); + /// + /// let expected = line_string![ + /// (x: -2.071067811865475, y: 5.0), + /// (x: 5.0, y: 5.0), + /// (x: 12.071067811865476, y: 5.0), + /// ]; + /// + /// assert_relative_eq!(expected, rotated); + /// ``` + #[must_use] + fn rotate_around_centroid(&self, degrees: T) -> Self::Output; + + /// Rotate a geometry around the center of its [bounding box](BoundingRect) by an angle, in + /// degrees. + /// + /// Positive angles are counter-clockwise, and negative angles are clockwise rotations. + /// + #[must_use] + fn rotate_around_center(&self, degrees: T) -> Self::Output; + + /// Rotate a Geometry around an arbitrary point by an angle, given in degrees + /// + /// Positive angles are counter-clockwise, and negative angles are clockwise rotations. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Rotate; + /// use sedona_geo_generic_alg::{line_string, point}; + /// + /// let ls = line_string![ + /// (x: 0.0, y: 0.0), + /// (x: 5.0, y: 5.0), + /// (x: 10.0, y: 10.0) + /// ]; + /// + /// let rotated = ls.rotate_around_point( + /// -45.0, + /// point!(x: 10.0, y: 0.0), + /// ); + /// + /// 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) + /// ]); + /// ``` + #[must_use] + fn rotate_around_point(&self, degrees: T, point: Point) -> Self::Output; +} + +/// Mutable version of the [`Rotate`] trait that applies rotations in place. +/// +/// Positive angles are counter-clockwise, and negative angles are clockwise rotations. +/// +/// ## Performance +/// +/// If you will be performing multiple transformations, like [`Scale`](crate::Scale), +/// [`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 +/// [`AffineOpsMut`] trait. +pub trait RotateMut { + /// Mutable version of [`Rotate::rotate_around_centroid`] + fn rotate_around_centroid_mut(&mut self, degrees: T); + + /// Mutable version of [`Rotate::rotate_around_center`] + fn rotate_around_center_mut(&mut self, degrees: T); + + /// Mutable version of [`Rotate::rotate_around_point`] + fn rotate_around_point_mut(&mut self, degrees: T, point: Point); +} + +impl Rotate for G +where + T: CoordFloat, + IP: Into>>, + IR: Into>>, + G: Clone + Centroid + BoundingRect + AffineOps, +{ + type Output = >::Output; + + fn rotate_around_centroid(&self, degrees: T) -> Self::Output { + let point = match self.centroid().into() { + Some(coord) => coord, + // geometry was empty, so there's nothing to rotate + None => return self.affine_transform(&AffineTransform::identity()), + }; + self.rotate_around_point(degrees, point) + } + + fn rotate_around_center(&self, degrees: T) -> Self::Output { + let point = match self.bounding_rect().into() { + Some(rect) => Point(rect.center()), + // geometry was empty, so there's nothing to rotate + None => return self.affine_transform(&AffineTransform::identity()), + }; + self.rotate_around_point(degrees, point) + } + + fn rotate_around_point(&self, degrees: T, point: Point) -> Self::Output { + let transform = AffineTransform::rotate(degrees, point); + self.affine_transform(&transform) + } +} + +impl RotateMut for G +where + T: CoordFloat, + IP: Into>>, + IR: Into>>, + G: Clone + Centroid + BoundingRect + AffineOpsMut, +{ + fn rotate_around_centroid_mut(&mut self, degrees: T) { + let point = match self.centroid().into() { + Some(coord) => coord, + // geometry was empty, so there's nothing to rotate + None => return, + }; + self.rotate_around_point_mut(degrees, point) + } + + fn rotate_around_center_mut(&mut self, degrees: T) { + let point = match self.bounding_rect().into() { + Some(rect) => Point(rect.center()), + // geometry was empty, so there's nothing to rotate + None => return, + }; + self.rotate_around_point_mut(degrees, point) + } + + fn rotate_around_point_mut(&mut self, degrees: T, point: Point) { + let transform = AffineTransform::rotate(degrees, point); + self.affine_transform_mut(&transform) + } +} + +#[cfg(test)] +mod test { + use crate::algorithm::Rotate; + use crate::geometry::*; + use crate::{line_string, point, polygon}; + use approx::assert_relative_eq; + + #[test] + fn test_rotate_around_point() { + let p = point!(x: 1.0, y: 5.0); + let rotated = p.rotate_around_centroid(30.0); + // results agree with Shapely / GEOS + assert_eq!(rotated, Point::new(1.0, 5.0)); + } + + #[test] + fn test_rotate_points() { + let point = point!(x: 1.0, y: 5.0); + let rotated_center = point.rotate_around_center(30.); + let rotated_centroid = point.rotate_around_centroid(30.); + + // results agree with Shapely / GEOS + // a rotated point should always equal itself + assert_eq!(point, rotated_center); + assert_eq!(point, rotated_centroid); + } + + #[test] + fn test_rotate_multipoints() { + let multi_points = MultiPoint::new(vec![ + point!(x: 0., y: 0.), + point!(x: 1., y: 1.), + point!(x: 2., y: 1.), + ]); + + // Results match shapely for `centroid` + let expected_for_centroid = MultiPoint::new(vec![ + point!(x: 0.7642977396044841, y: -0.5118446353109125), + point!(x: 0.7642977396044842, y: 0.9023689270621824), + point!(x: 1.471404520791032, y: 1.60947570824873), + ]); + assert_relative_eq!( + multi_points.rotate_around_centroid(45.), + expected_for_centroid + ); + + // Results match shapely for `center` + let expected_for_center = MultiPoint::new(vec![ + point!(x: 0.6464466094067262, y: -0.5606601717798212), + 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, + epsilon = 1e-15 + ); + } + + #[test] + fn test_rotate_linestring() { + let linestring = line_string![ + (x: 0.0, y: 0.0), + (x: 5.0, y: 5.0), + (x: 5.0, y: 10.0) + ]; + + // results agree with Shapely / GEOS for `centroid` + let rotated_around_centroid = linestring.rotate_around_centroid(-45.0); + assert_relative_eq!( + rotated_around_centroid, + line_string![ + (x: -2.196699141100894, y: 3.838834764831844), + (x: 4.874368670764582, y: 3.838834764831844), + (x: 8.40990257669732, y: 7.374368670764582) + ] + ); + + // results agree with Shapely / GEOS for `center` + let rotated_around_center = linestring.rotate_around_center(-45.0); + assert_relative_eq!( + rotated_around_center, + line_string![ + (x: -2.803300858899106, y: 3.232233047033631), + (x: 4.267766952966369, y: 3.232233047033632), + (x: 7.803300858899107, y: 6.767766952966369) + ], + epsilon = 1e-12 + ); + } + #[test] + fn test_rotate_polygon() { + let poly1 = polygon![ + (x: 5., y: 1.), + (x: 4., y: 2.), + (x: 4., y: 3.), + (x: 5., y: 4.), + (x: 6., y: 4.), + (x: 7., y: 3.), + (x: 7., y: 2.), + (x: 6., y: 1.), + (x: 5., y: 1.) + ]; + let rotated = poly1.rotate_around_centroid(-15.0); + let correct = polygon![ + (x: 4.6288085192016855, y: 1.1805207831176578), + (x: 3.921701738015137, y: 2.405265654509247), + (x: 4.180520783117659, y: 3.3711914807983154), + (x: 5.405265654509247, y: 4.0782982619848624), + (x: 6.371191480798316, y: 3.819479216882342), + (x: 7.0782982619848624, y: 2.594734345490753), + (x: 6.819479216882343, y: 1.6288085192016848), + (x: 5.594734345490753, y: 0.9217017380151372), + (x: 4.6288085192016855, y: 1.1805207831176578) + ]; + // results agree with Shapely / GEOS + assert_eq!(rotated, correct); + } + #[test] + fn test_rotate_polygon_holes() { + let poly1 = polygon![ + exterior: [ + (x: 5.0, y: 1.0), + (x: 4.0, y: 2.0), + (x: 4.0, y: 3.0), + (x: 5.0, y: 4.0), + (x: 6.0, y: 4.0), + (x: 7.0, y: 3.0), + (x: 7.0, y: 2.0), + (x: 6.0, y: 1.0), + (x: 5.0, y: 1.0) + ], + interiors: [ + [ + (x: 5.0, y: 1.3), + (x: 5.5, y: 2.0), + (x: 6.0, y: 1.3), + (x: 5.0, y: 1.3), + ], + [ + (x: 5., y: 2.3), + (x: 5.5, y: 3.0), + (x: 6., y: 2.3), + (x: 5., y: 2.3), + ], + ], + ]; + + // now rotate around center + let center_expected = polygon![ + exterior: [ + (x: 4.628808519201685, y: 1.180520783117658), + (x: 3.921701738015137, y: 2.405265654509247), + (x: 4.180520783117659, y: 3.371191480798315), + (x: 5.405265654509247, y: 4.078298261984862), + (x: 6.371191480798316, y: 3.819479216882342), + (x: 7.078298261984862, y: 2.594734345490753), + (x: 6.819479216882343, y: 1.628808519201685), + (x: 5.594734345490753, y: 0.9217017380151372), + (x: 4.628808519201685, y: 1.180520783117658), + ], + interiors: [ + [ + (x: 4.706454232732442, y: 1.470298531004379), + (x: 5.37059047744874, y: 2.017037086855466), + (x: 5.67238005902151, y: 1.211479485901858), + (x: 4.706454232732442, y: 1.470298531004379), + ], + [ + (x: 4.965273277834962, y: 2.436224357293447), + (x: 5.62940952255126, y: 2.982962913144534), + (x: 5.931199104124032, y: 2.177405312190926), + (x: 4.965273277834962, y: 2.436224357293447), + ], + ], + ]; + + let rotated_around_center = poly1.rotate_around_center(-15.); + + assert_relative_eq!(rotated_around_center, center_expected, epsilon = 1e-12); + + // now rotate around centroid + let centroid_expected = polygon![ + exterior: [ + (x: 4.615388272418591, y: 1.182287592124891), + (x: 3.908281491232044, y: 2.40703246351648), + (x: 4.167100536334565, y: 3.372958289805549), + (x: 5.391845407726153, y: 4.080065070992097), + (x: 6.357771234015222, y: 3.821246025889576), + (x: 7.064878015201769, y: 2.596501154497987), + (x: 6.806058970099248, y: 1.630575328208918), + (x: 5.58131409870766, y: 0.9234685470223708), + (x: 4.615388272418591, y: 1.182287592124891), + ], + interiors: [ + [ + (x: 4.693033985949348, y: 1.472065340011612), + (x: 5.357170230665646, y: 2.0188038958627), + (x: 5.658959812238415, y: 1.213246294909091), + (x: 4.693033985949348, y: 1.472065340011612), + ], + [ + (x: 4.951853031051868, y: 2.43799116630068), + (x: 5.615989275768166, y: 2.984729722151768), + (x: 5.917778857340937, y: 2.179172121198159), + (x: 4.951853031051868, y: 2.43799116630068), + ], + ], + ]; + let rotated_around_centroid = poly1.rotate_around_centroid(-15.); + assert_relative_eq!(rotated_around_centroid, centroid_expected, epsilon = 1e-12); + } + #[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_relative_eq!( + rotated, + Point::new(-10.506096654409877, 20.564971157455595), + epsilon = 1e-14 + ); + } + #[test] + fn test_rotate_line() { + let line0 = Line::from([(0., 0.), (0., 2.)]); + let line1 = Line::from([(1., 1.), (-1., 1.)]); + assert_relative_eq!(line0.rotate_around_centroid(90.0), line1); + assert_relative_eq!(line0.rotate_around_center(90.0), line1); + } + + #[test] + fn test_rotate_multi_line_string() { + let ls1 = line_string![ + (x: 0., y: 0.), + (x: 1., y: 1.), + (x: 4., y: 1.), + ]; + let ls2 = line_string![ + (x: 10., y: 10.), + (x: 20., y: 20.), + (x: 40., y: 20.) + ]; + let multi_line_string: MultiLineString = MultiLineString::new(vec![ls1, ls2]); + + // Results match with Shapely for `centroid` + let expected_around_centroid = MultiLineString::new(vec![ + line_string![ + (x: -5.062519283392216, y: 19.72288595632566), + (x: -3.648305721019121, y: 19.72288595632566), + (x: -1.526985377459479, y: 17.60156561276602) + ], + line_string![ + (x: 9.079616340338735, y: 19.72288595632566), + (x: 23.22175196406969, y: 19.72288595632566), + (x: 37.36388758780063, y: 5.580750332594715) + ], + ]); + assert_relative_eq!( + multi_line_string.rotate_around_centroid(-45.), + expected_around_centroid, + epsilon = 1e-12 + ); + + // Results match with Shapely for `center` + let expected_around_center: MultiLineString = MultiLineString::new(vec![ + line_string![ + (x: -1.213203435596426, y: 17.07106781186548), + (x: 0.2010101267766693, y: 17.07106781186548), + (x: 2.322330470336312, y: 14.94974746830583), + ], + line_string![ + (x: 12.92893218813452, y: 17.07106781186548), + (x: 27.07106781186548, y: 17.07106781186548), + (x: 41.21320343559643, y: 2.928932188134528), + + ], + ]); + assert_relative_eq!( + multi_line_string.rotate_around_center(-45.), + expected_around_center, + epsilon = 1e-12 + ); + } + + #[test] + fn test_rotate_line_around_point() { + let line0 = Line::new(Point::new(0., 0.), Point::new(0., 2.)); + let line1 = Line::new(Point::new(0., 0.), Point::new(-2., 0.)); + assert_relative_eq!(line0.rotate_around_point(90., Point::new(0., 0.)), line1); + } + + #[test] + fn test_rotate_multipolygon_around_centroid() { + let multipolygon: MultiPolygon = vec![ + polygon![ + (x: 0., y: 0.), + (x: 10., y: 0.), + (x: 10., y: 10.), + (x: 0., y: 10.), + (x: 0., y: 0.), + ], + polygon![ + (x: 0., y: 0.), + (x: -10., y: 0.), + (x: -10., y: -10.), + (x: 0., y: -10.), + (x: 0., y: 0.), + ], + ] + .into(); + + let expected_centroid: MultiPolygon = vec![ + polygon![ + (x: 0., y: 0.), + (x: 7.0710678118654755, y: 7.071067811865475), + (x: 0., y: 14.142135623730951), + (x: -7.071067811865475, y: 7.0710678118654755), + (x: 0., y: 0.), + ], + polygon![ + (x: 0., y: 0.), + (x: -7.0710678118654755, y: -7.071067811865475), + (x: 0., y: -14.142135623730951), + (x: 7.071067811865475, y: -7.0710678118654755), + (x: 0., y: 0.), + ], + ] + .into(); + + // results agree with Shapely / GEOS + // (relaxing the epsilon a bit) + assert_relative_eq!( + multipolygon.rotate_around_centroid(45.), + expected_centroid, + epsilon = 1e-12 + ); + } + + #[test] + fn test_rotate_multipolygons() { + let multipolygon: MultiPolygon = vec![ + polygon![ + (x: 1., y: 1. ), + (x: 2., y: 1. ), + (x: 2., y: 10.), + (x: 1., y: 10.), + (x: 1., y: 1. ), + ], + polygon![ + (x: 10., y: 1.), + (x: 12., y: 1.), + (x: 12., y: 12.), + (x: 10., y: 12.), + (x: 10., y: 1.), + ], + ] + .into(); + + let expected_center: MultiPolygon = vec![ + polygon![ + (x: -0.2360967926537398, y: 2.610912703473988), + (x: 0.7298290336353284, y: 2.352093658371467), + (x: 3.059200439558015, y: 11.04542609497308), + (x: 2.093274613268947, y: 11.3042451400756), + (x: -0.2360967926537398, y: 2.610912703473988), + ], + polygon![ + (x: 8.457235643947875, y: 0.2815412975513012), + (x: 10.38908729652601, y: -0.2360967926537403), + (x: 13.23609679265374, y: 10.38908729652601), + (x: 11.3042451400756, y: 10.90672538673105), + (x: 8.457235643947875, y: 0.2815412975513012), + ], + ] + .into(); + + let expected_centroid: MultiPolygon = vec![ + polygon![ + (x: -0.1016007672888048, y: 3.05186627999456), + (x: 0.8643250590002634, y: 2.793047234892039), + (x: 3.19369646492295, y: 11.48637967149365), + (x: 2.227770638633882, y: 11.74519871659617), + (x: -0.1016007672888048, y: 3.05186627999456), + ], + polygon![ + (x: 8.59173166931281, y: 0.7224948740718733), + (x: 10.52358332189095, y: 0.2048567838668318), + (x: 13.37059281801868, y: 10.83004087304658), + (x: 11.43874116544054, y: 11.34767896325162), + (x: 8.59173166931281, y: 0.7224948740718733), + ], + ] + .into(); + + // results agree with Shapely / GEOS + assert_relative_eq!( + multipolygon.rotate_around_center(-15.), + expected_center, + epsilon = 1e-12 + ); + assert_relative_eq!( + multipolygon.rotate_around_centroid(-15.), + expected_centroid, + epsilon = 1e-12 + ); + } + + #[test] + fn test_rotate_empty_geometries_error_gracefully() { + // line string + let empty_linestring: LineString = line_string![]; + let rotated_empty_linestring = empty_linestring.rotate_around_centroid(90.); + assert_eq!(empty_linestring, rotated_empty_linestring); + + // multi line string + let empty_multilinestring: MultiLineString = MultiLineString::new(vec![]); + let rotated_empty_multilinestring = empty_multilinestring.rotate_around_centroid(90.); + assert_eq!(empty_multilinestring, rotated_empty_multilinestring); + + // polygon + let empty_polygon: Polygon = polygon![]; + let rotated_empty_polygon = empty_polygon.rotate_around_centroid(90.); + assert_eq!(empty_polygon, rotated_empty_polygon); + + // multi polygon + let empty_multipolygon: MultiPolygon = Vec::>::new().into(); + let rotated_empty_multipolygon = empty_multipolygon.rotate_around_centroid(90.); + assert_eq!(empty_multipolygon, rotated_empty_multipolygon); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/scale.rs b/rust/sedona-geo-generic-alg/src/algorithm/scale.rs new file mode 100644 index 00000000..a9389722 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/scale.rs @@ -0,0 +1,179 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use crate::algorithm::affine_ops::AffineOpsMut; +use crate::{AffineOps, AffineTransform, BoundingRect, Coord, CoordFloat, CoordNum, Rect}; + +/// An affine transformation which scales a geometry up or down by a factor. +/// +/// ## Performance +/// +/// 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`] trait. +pub trait Scale { + /// The output type of the scaling operations + type Output; + + /// Scale a geometry from it's bounding box center. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Scale; + /// use sedona_geo_generic_alg::{LineString, line_string}; + /// + /// let ls: LineString = line_string![(x: 0., y: 0.), (x: 10., y: 10.)]; + /// + /// let scaled = ls.scale(2.); + /// + /// assert_eq!(scaled, line_string![ + /// (x: -5., y: -5.), + /// (x: 15., y: 15.) + /// ]); + /// ``` + #[must_use] + fn scale(&self, scale_factor: T) -> Self::Output; + + /// Scale a geometry from it's bounding box center, using different values for `x_factor` and + /// `y_factor` to distort the geometry's [aspect ratio](https://en.wikipedia.org/wiki/Aspect_ratio). + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Scale; + /// use sedona_geo_generic_alg::{LineString, line_string}; + /// + /// let ls: LineString = line_string![(x: 0., y: 0.), (x: 10., y: 10.)]; + /// + /// let scaled = ls.scale_xy(2., 4.); + /// + /// assert_eq!(scaled, line_string![ + /// (x: -5., y: -15.), + /// (x: 15., y: 25.) + /// ]); + /// ``` + #[must_use] + fn scale_xy(&self, x_factor: T, y_factor: T) -> Self::Output; + + /// Scale a geometry around a point of `origin`. + /// + /// The point of origin is *usually* given as the 2D bounding box centre of the geometry, in + /// which case you can just use [`scale`](Self::scale) or [`scale_xy`](Self::scale_xy), but + /// this method allows you to specify any point. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Scale; + /// use sedona_geo_generic_alg::{LineString, line_string, Coord}; + /// + /// let ls: LineString = line_string![(x: 0., y: 0.), (x: 10., y: 10.)]; + /// + /// let scaled = ls.scale_around_point(2., 4., Coord { x: 100., y: 100. }); + /// + /// assert_eq!(scaled, line_string![ + /// (x: -100., y: -300.), + /// (x: -80., y: -260.) + /// ]); + /// ``` + #[must_use] + fn scale_around_point( + &self, + x_factor: T, + y_factor: T, + origin: impl Into>, + ) -> Self::Output; +} + +/// Mutable version of the [`Scale`] trait that applies scaling in place. +/// +/// ## Performance +/// +/// 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 +/// [`AffineOpsMut`] trait. +pub trait ScaleMut { + /// Mutable version of [`Scale::scale`]. + fn scale_mut(&mut self, scale_factor: T); + + /// Mutable version of [`Scale::scale_xy`]. + fn scale_xy_mut(&mut self, x_factor: T, y_factor: T); + + /// Mutable version of [`Scale::scale_around_point`]. + fn scale_around_point_mut(&mut self, x_factor: T, y_factor: T, origin: impl Into>); +} + +impl Scale for G +where + T: CoordFloat, + IR: Into>>, + G: Clone + AffineOps + BoundingRect, +{ + type Output = >::Output; + + fn scale(&self, scale_factor: T) -> Self::Output { + self.scale_xy(scale_factor, scale_factor) + } + + fn scale_xy(&self, x_factor: T, y_factor: T) -> Self::Output { + let origin = match self.bounding_rect().into() { + Some(rect) => rect.center(), + // Empty geometries have no bounding rect, but in that case + // transforming is a no-op anyway. + None => return self.affine_transform(&AffineTransform::identity()), + }; + self.scale_around_point(x_factor, y_factor, origin) + } + + fn scale_around_point( + &self, + x_factor: T, + y_factor: T, + origin: impl Into>, + ) -> Self::Output { + let affineop = AffineTransform::scale(x_factor, y_factor, origin); + self.affine_transform(&affineop) + } +} + +impl ScaleMut for G +where + T: CoordFloat, + IR: Into>>, + G: Clone + AffineOpsMut + BoundingRect, +{ + fn scale_mut(&mut self, scale_factor: T) { + self.scale_xy_mut(scale_factor, scale_factor); + } + + fn scale_xy_mut(&mut self, x_factor: T, y_factor: T) { + let origin = match self.bounding_rect().into() { + Some(rect) => rect.center(), + // Empty geometries have no bounding rect, but in that case + // transforming is a no-op anyway. + None => return, + }; + self.scale_around_point_mut(x_factor, y_factor, origin); + } + + fn scale_around_point_mut(&mut self, x_factor: T, y_factor: T, origin: impl Into>) { + let affineop = AffineTransform::scale(x_factor, y_factor, origin); + self.affine_transform_mut(&affineop) + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/simplify.rs b/rust/sedona-geo-generic-alg/src/algorithm/simplify.rs new file mode 100644 index 00000000..52439901 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/simplify.rs @@ -0,0 +1,598 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use crate::geometry::{Coord, Line, LineString, MultiLineString, MultiPolygon, Polygon}; +use crate::{DistanceExt, GeoFloat}; +use sedona_geo_traits_ext::*; + +const LINE_STRING_INITIAL_MIN: usize = 2; +const POLYGON_INITIAL_MIN: usize = 4; + +// Because the RDP algorithm is recursive, we can't assign an index to a point inside the loop +// instead, we wrap a simple struct around index and point in a wrapper function, +// passing that around instead, extracting either points or indices on the way back out +#[derive(Copy, Clone)] +struct RdpIndex +where + T: GeoFloat, +{ + index: usize, + coord: Coord, +} + +// Wrapper for the RDP algorithm, returning simplified points +fn rdp>, const INITIAL_MIN: usize>( + coords: I, + epsilon: T, +) -> Vec> +where + T: GeoFloat, +{ + // Epsilon must be greater than zero for any meaningful simplification to happen + if epsilon <= T::zero() { + return coords.collect::>>(); + } + let rdp_indices = &coords + .enumerate() + .map(|(idx, coord)| RdpIndex { index: idx, coord }) + .collect::>>(); + let mut simplified_len = rdp_indices.len(); + let simplified_coords: Vec<_> = + compute_rdp::(rdp_indices, &mut simplified_len, epsilon) + .into_iter() + .map(|rdpindex| rdpindex.coord) + .collect(); + debug_assert_eq!(simplified_coords.len(), simplified_len); + simplified_coords +} + +// Wrapper for the RDP algorithm, returning simplified point indices +fn calculate_rdp_indices( + rdp_indices: &[RdpIndex], + epsilon: T, +) -> Vec +where + T: GeoFloat, +{ + if epsilon <= T::zero() { + return rdp_indices + .iter() + .map(|rdp_index| rdp_index.index) + .collect(); + } + + let mut simplified_len = rdp_indices.len(); + let simplified_coords = + compute_rdp::(rdp_indices, &mut simplified_len, epsilon) + .into_iter() + .map(|rdpindex| rdpindex.index) + .collect::>(); + debug_assert_eq!(simplified_len, simplified_coords.len()); + simplified_coords +} + +// Ramer–Douglas-Peucker line simplification algorithm +// This function returns both the retained points, and their indices in the original geometry, +// for more flexible use by FFI implementers +fn compute_rdp( + rdp_indices: &[RdpIndex], + simplified_len: &mut usize, + epsilon: T, +) -> Vec> +where + T: GeoFloat, +{ + if rdp_indices.is_empty() { + return vec![]; + } + + let first = rdp_indices[0]; + let last = rdp_indices[rdp_indices.len() - 1]; + if rdp_indices.len() == 2 { + return vec![first, last]; + } + + let first_last_line = Line::new(first.coord, last.coord); + + // Find the farthest `RdpIndex` from `first_last_line` + let (farthest_index, farthest_distance) = rdp_indices + .iter() + .enumerate() + .take(rdp_indices.len() - 1) // Don't include the last index + .skip(1) // Don't include the first index + .map(|(index, rdp_index)| { + let dist = rdp_index.coord.distance_ext(&first_last_line); + (index, dist) + }) + .fold( + (0usize, T::zero()), + |(farthest_index, farthest_distance), (index, distance)| { + if distance >= farthest_distance { + (index, distance) + } else { + (farthest_index, farthest_distance) + } + }, + ); + debug_assert_ne!(farthest_index, 0); + + if farthest_distance > epsilon { + // The farthest index was larger than epsilon, so we will recursively simplify subsegments + // split by the farthest index. + let mut intermediate = + compute_rdp::(&rdp_indices[..=farthest_index], simplified_len, epsilon); + + intermediate.pop(); // Don't include the farthest index twice + + intermediate.extend_from_slice(&compute_rdp::( + &rdp_indices[farthest_index..], + simplified_len, + epsilon, + )); + return intermediate; + } + + // The farthest index was less than or equal to epsilon, so we will retain only the first + // and last indices, resulting in the indices in between getting culled. + + // Update `simplified_len` to reflect the new number of indices by subtracting the number + // of indices we're culling. + let number_culled = rdp_indices.len() - 2; + let new_length = *simplified_len - number_culled; + + // If `simplified_len` is now lower than the minimum number of indices needed, then don't + // perform the culling and return the original input. + if new_length < INITIAL_MIN { + return rdp_indices.to_owned(); + } + *simplified_len = new_length; + + // Cull indices between `first` and `last`. + vec![first, last] +} + +/// Simplifies a geometry. +/// +/// The [Ramer–Douglas–Peucker +/// algorithm](https://en.wikipedia.org/wiki/Ramer–Douglas–Peucker_algorithm) simplifies a +/// linestring. Polygons are simplified by running the RDP 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. +/// +/// A larger `epsilon` means being more aggressive about removing points with less concern for +/// maintaining the existing shape. +/// +/// Specifically, points closer than `epsilon` distance from the simplified output may be +/// discarded. +/// +/// An `epsilon` less than or equal to zero will return an unaltered version of the geometry. +pub trait Simplify { + type Output; + + /// Returns the simplified representation of a geometry, using the [Ramer–Douglas–Peucker](https://en.wikipedia.org/wiki/Ramer–Douglas–Peucker_algorithm) algorithm + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Simplify; + /// use sedona_geo_generic_alg::line_string; + /// + /// let line_string = line_string![ + /// (x: 0.0, y: 0.0), + /// (x: 5.0, y: 4.0), + /// (x: 11.0, y: 5.5), + /// (x: 17.3, y: 3.2), + /// (x: 27.8, y: 0.1), + /// ]; + /// + /// let simplified = line_string.simplify(1.0); + /// + /// let expected = line_string![ + /// (x: 0.0, y: 0.0), + /// (x: 5.0, y: 4.0), + /// (x: 11.0, y: 5.5), + /// (x: 27.8, y: 0.1), + /// ]; + /// + /// assert_eq!(expected, simplified) + /// ``` + fn simplify(&self, epsilon: T) -> Self::Output + where + T: GeoFloat; +} + +/// Simplifies a geometry, returning the retained _indices_ of the input. +/// +/// This operation uses the [Ramer–Douglas–Peucker algorithm](https://en.wikipedia.org/wiki/Ramer–Douglas–Peucker_algorithm) +/// and does not guarantee that the returned geometry is valid. +/// +/// A larger `epsilon` means being more aggressive about removing points with less concern for +/// maintaining the existing shape. +/// +/// Specifically, points closer than `epsilon` distance from the simplified output may be +/// discarded. +/// +/// An `epsilon` less than or equal to zero will return an unaltered version of the geometry. +pub trait SimplifyIdx { + /// Returns the simplified indices of a geometry, using the [Ramer–Douglas–Peucker](https://en.wikipedia.org/wiki/Ramer–Douglas–Peucker_algorithm) algorithm + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::SimplifyIdx; + /// use sedona_geo_generic_alg::line_string; + /// + /// let line_string = line_string![ + /// (x: 0.0, y: 0.0), + /// (x: 5.0, y: 4.0), + /// (x: 11.0, y: 5.5), + /// (x: 17.3, y: 3.2), + /// (x: 27.8, y: 0.1), + /// ]; + /// + /// let simplified = line_string.simplify_idx(1.0); + /// + /// let expected = vec![ + /// 0_usize, + /// 1_usize, + /// 2_usize, + /// 4_usize, + /// ]; + /// + /// assert_eq!(expected, simplified); + /// ``` + fn simplify_idx(&self, epsilon: T) -> Vec + where + T: GeoFloat; +} + +impl Simplify for G +where + T: GeoFloat, + G: GeoTraitExtWithTypeTag + SimplifyTrait, +{ + type Output = >::Output; + + fn simplify(&self, epsilon: T) -> Self::Output { + self.simplify_trait(epsilon) + } +} + +impl SimplifyIdx for G +where + T: GeoFloat, + G: GeoTraitExtWithTypeTag + SimplifyIdxTrait, +{ + fn simplify_idx(&self, epsilon: T) -> Vec { + self.simplify_idx_trait(epsilon) + } +} + +pub trait SimplifyTrait +where + T: GeoFloat, +{ + type Output; + fn simplify_trait(&self, epsilon: T) -> Self::Output; +} + +pub trait SimplifyIdxTrait +where + T: GeoFloat, +{ + fn simplify_idx_trait(&self, epsilon: T) -> Vec; +} + +impl SimplifyTrait for LS +where + T: GeoFloat, + LS: LineStringTraitExt, +{ + type Output = LineString; + + fn simplify_trait(&self, epsilon: T) -> Self::Output { + LineString::from(rdp::<_, _, LINE_STRING_INITIAL_MIN>( + self.coord_iter(), + epsilon, + )) + } +} + +impl SimplifyIdxTrait for LS +where + T: GeoFloat, + LS: LineStringTraitExt, +{ + fn simplify_idx_trait(&self, epsilon: T) -> Vec { + calculate_rdp_indices::<_, LINE_STRING_INITIAL_MIN>( + &self + .coord_iter() + .enumerate() + .map(|(idx, coord)| RdpIndex { index: idx, coord }) + .collect::>>(), + epsilon, + ) + } +} + +impl SimplifyTrait for MLS +where + T: GeoFloat, + MLS: MultiLineStringTraitExt, +{ + type Output = MultiLineString; + + fn simplify_trait(&self, epsilon: T) -> Self::Output { + let simplified_lines: Vec> = self + .line_strings_ext() + .map(|l| l.simplify_trait(epsilon)) + .collect(); + + MultiLineString::new(simplified_lines) + } +} + +impl SimplifyTrait for P +where + T: GeoFloat, + P: PolygonTraitExt, +{ + type Output = Polygon; + + fn simplify_trait(&self, epsilon: T) -> Self::Output { + if let Some(exterior) = self.exterior_ext() { + let simplified_exterior = LineString::from(rdp::<_, _, POLYGON_INITIAL_MIN>( + exterior.coord_iter(), + epsilon, + )); + + let simplified_interiors: Vec> = self + .interiors_ext() + .map(|interior| { + LineString::from(rdp::<_, _, POLYGON_INITIAL_MIN>( + interior.coord_iter(), + epsilon, + )) + }) + .collect(); + + Polygon::new(simplified_exterior, simplified_interiors) + } else { + // Create an empty polygon + Polygon::new(LineString::new(vec![]), vec![]) + } + } +} + +impl SimplifyTrait for MP +where + T: GeoFloat, + MP: MultiPolygonTraitExt, +{ + type Output = MultiPolygon; + + fn simplify_trait(&self, epsilon: T) -> Self::Output { + MultiPolygon::new( + self.polygons_ext() + .map(|p| p.simplify_trait(epsilon)) + .collect(), + ) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{coord, line_string, polygon}; + + #[test] + fn recursion_test() { + let input = [ + coord! { x: 8.0, y: 100.0 }, + coord! { x: 9.0, y: 100.0 }, + coord! { x: 12.0, y: 100.0 }, + ]; + let actual = rdp::<_, _, 2>(input.into_iter(), 1.0); + let expected = [coord! { x: 8.0, y: 100.0 }, coord! { x: 12.0, y: 100.0 }]; + assert_eq!(actual, expected); + } + + #[test] + fn rdp_test() { + let vec = vec![ + coord! { x: 0.0, y: 0.0 }, + coord! { x: 5.0, y: 4.0 }, + coord! { x: 11.0, y: 5.5 }, + coord! { x: 17.3, y: 3.2 }, + coord! { x: 27.8, y: 0.1 }, + ]; + let compare = vec![ + coord! { x: 0.0, y: 0.0 }, + coord! { x: 5.0, y: 4.0 }, + coord! { x: 11.0, y: 5.5 }, + coord! { x: 27.8, y: 0.1 }, + ]; + let simplified = rdp::<_, _, 2>(vec.into_iter(), 1.0); + assert_eq!(simplified, compare); + } + #[test] + fn rdp_test_empty_linestring() { + let vec = Vec::new(); + let compare = Vec::new(); + let simplified = rdp::<_, _, 2>(vec.into_iter(), 1.0); + assert_eq!(simplified, compare); + } + #[test] + fn rdp_test_two_point_linestring() { + let vec = vec![coord! { x: 0.0, y: 0.0 }, coord! { x: 27.8, y: 0.1 }]; + let compare = vec![coord! { x: 0.0, y: 0.0 }, coord! { x: 27.8, y: 0.1 }]; + let simplified = rdp::<_, _, 2>(vec.into_iter(), 1.0); + assert_eq!(simplified, compare); + } + + #[test] + fn multilinestring() { + let mline = MultiLineString::new(vec![LineString::from(vec![ + (0.0, 0.0), + (5.0, 4.0), + (11.0, 5.5), + (17.3, 3.2), + (27.8, 0.1), + ])]); + + let mline2 = mline.simplify(1.0); + + assert_eq!( + mline2, + MultiLineString::new(vec![LineString::from(vec![ + (0.0, 0.0), + (5.0, 4.0), + (11.0, 5.5), + (27.8, 0.1), + ])]) + ); + } + + #[test] + fn polygon() { + let poly = polygon![ + (x: 0., y: 0.), + (x: 0., y: 10.), + (x: 5., y: 11.), + (x: 10., y: 10.), + (x: 10., y: 0.), + (x: 0., y: 0.), + ]; + + let poly2 = poly.simplify(2.); + + assert_eq!( + poly2, + polygon![ + (x: 0., y: 0.), + (x: 0., y: 10.), + (x: 10., y: 10.), + (x: 10., y: 0.), + (x: 0., y: 0.), + ], + ); + } + + #[test] + fn multipolygon() { + let mpoly = MultiPolygon::new(vec![polygon![ + (x: 0., y: 0.), + (x: 0., y: 10.), + (x: 5., y: 11.), + (x: 10., y: 10.), + (x: 10., y: 0.), + (x: 0., y: 0.), + ]]); + + let mpoly2 = mpoly.simplify(2.); + + assert_eq!( + mpoly2, + MultiPolygon::new(vec![polygon![ + (x: 0., y: 0.), + (x: 0., y: 10.), + (x: 10., y: 10.), + (x: 10., y: 0.), + (x: 0., y: 0.) + ]]), + ); + } + + #[test] + fn simplify_negative_epsilon() { + let ls = line_string![ + (x: 0., y: 0.), + (x: 0., y: 10.), + (x: 5., y: 11.), + (x: 10., y: 10.), + (x: 10., y: 0.), + ]; + let simplified = ls.simplify(-1.0); + assert_eq!(ls, simplified); + } + + #[test] + fn simplify_idx_negative_epsilon() { + let ls = line_string![ + (x: 0., y: 0.), + (x: 0., y: 10.), + (x: 5., y: 11.), + (x: 10., y: 10.), + (x: 10., y: 0.), + ]; + let indices = ls.simplify_idx(-1.0); + assert_eq!(vec![0usize, 1, 2, 3, 4], indices); + } + + // https://github.com/georust/geo/issues/142 + #[test] + fn simplify_line_string_polygon_initial_min() { + let ls = line_string![ + ( x: 1.4324054e-16, y: 1.4324054e-16 ), + ( x: 1.4324054e-16, y: 1.4324054e-16 ), + ( x: -5.9730447e26, y: 1.5590374e-27 ), + ( x: 1.4324054e-16, y: 1.4324054e-16 ), + ]; + let epsilon: f64 = 3.46e-43; + + // LineString result should be three coordinates + let result = ls.simplify(epsilon); + assert_eq!( + line_string![ + ( x: 1.4324054e-16, y: 1.4324054e-16 ), + ( x: -5.9730447e26, y: 1.5590374e-27 ), + ( x: 1.4324054e-16, y: 1.4324054e-16 ), + ], + result + ); + + // Polygon result should be five coordinates + let result = Polygon::new(ls, vec![]).simplify(epsilon); + assert_eq!( + polygon![ + ( x: 1.4324054e-16, y: 1.4324054e-16 ), + ( x: 1.4324054e-16, y: 1.4324054e-16 ), + ( x: -5.9730447e26, y: 1.5590374e-27 ), + ( x: 1.4324054e-16, y: 1.4324054e-16 ), + ], + result, + ); + } + + // https://github.com/georust/geo/issues/995 + #[test] + fn dont_oversimplify() { + let unsimplified = line_string![ + (x: 0.0, y: 0.0), + (x: 5.0, y: 4.0), + (x: 11.0, y: 5.5), + (x: 17.3, y: 3.2), + (x: 27.8, y: 0.1) + ]; + let actual = unsimplified.simplify(30.0); + let expected = line_string![ + (x: 0.0, y: 0.0), + (x: 27.8, y: 0.1) + ]; + assert_eq!(actual, expected); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/skew.rs b/rust/sedona-geo-generic-alg/src/algorithm/skew.rs new file mode 100644 index 00000000..d7881608 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/skew.rs @@ -0,0 +1,224 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use crate::algorithm::affine_ops::AffineOpsMut; +use crate::{AffineOps, AffineTransform, BoundingRect, Coord, CoordFloat, CoordNum, Rect}; + +/// An affine transformation which skews a geometry, sheared by angles along x and y dimensions. +/// +/// ## Performance +/// +/// If you will be performing multiple transformations, like [`Scale`](crate::Scale), +/// [`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`] trait. +/// +pub trait Skew { + /// The output type of the skewing operations + type Output; + + /// An affine transformation which skews a geometry, sheared by a uniform angle along the x and + /// y dimensions. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Skew; + /// use sedona_geo_generic_alg::{Polygon, polygon}; + /// + /// let square: Polygon = polygon![ + /// (x: 0., y: 0.), + /// (x: 10., y: 0.), + /// (x: 10., y: 10.), + /// (x: 0., y: 10.) + /// ]; + /// + /// let skewed = square.skew(30.); + /// + /// let expected_output: Polygon = polygon![ + /// (x: -2.89, y: -2.89), + /// (x: 7.11, y: 2.89), + /// (x: 12.89, y: 12.89), + /// (x: 2.89, y: 7.11) + /// ]; + /// approx::assert_relative_eq!(skewed, expected_output, epsilon = 1e-2); + /// ``` + #[must_use] + fn skew(&self, degrees: T) -> Self::Output; + + /// An affine transformation which skews a geometry, sheared by an angle along the x and y dimensions. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Skew; + /// use sedona_geo_generic_alg::{Polygon, polygon}; + /// + /// let square: Polygon = polygon![ + /// (x: 0., y: 0.), + /// (x: 10., y: 0.), + /// (x: 10., y: 10.), + /// (x: 0., y: 10.) + /// ]; + /// + /// let skewed = square.skew_xy(30., 12.); + /// + /// let expected_output: Polygon = polygon![ + /// (x: -2.89, y: -1.06), + /// (x: 7.11, y: 1.06), + /// (x: 12.89, y: 11.06), + /// (x: 2.89, y: 8.94) + /// ]; + /// approx::assert_relative_eq!(skewed, expected_output, epsilon = 1e-2); + /// ``` + #[must_use] + fn skew_xy(&self, degrees_x: T, degrees_y: T) -> Self::Output; + + /// An affine transformation which skews a geometry around a point of `origin`, sheared by an + /// angle along the x and y dimensions. + /// + /// The point of origin is *usually* given as the 2D bounding box centre of the geometry, in + /// which case you can just use [`skew`](Self::skew) or [`skew_xy`](Self::skew_xy), but this method allows you + /// to specify any point. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Skew; + /// use sedona_geo_generic_alg::{Polygon, polygon, point}; + /// + /// let square: Polygon = polygon![ + /// (x: 0., y: 0.), + /// (x: 10., y: 0.), + /// (x: 10., y: 10.), + /// (x: 0., y: 10.) + /// ]; + /// + /// let origin = point! { x: 2., y: 2. }; + /// let skewed = square.skew_around_point(45.0, 10.0, origin); + /// + /// let expected_output: Polygon = polygon![ + /// (x: -2., y: -0.353), + /// (x: 8., y: 1.410), + /// (x: 18., y: 11.41), + /// (x: 8., y: 9.647) + /// ]; + /// approx::assert_relative_eq!(skewed, expected_output, epsilon = 1e-2); + /// ``` + #[must_use] + fn skew_around_point( + &self, + degrees_x: T, + degrees_y: T, + origin: impl Into>, + ) -> Self::Output; +} + +/// Mutable version of the [`Skew`] trait that applies skewing in place. +/// +/// ## Performance +/// +/// If you will be performing multiple transformations, like [`Scale`](crate::Scale), +/// [`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 +/// [`AffineOpsMut`] trait. +pub trait SkewMut { + /// Mutable version of [`Skew::skew`]. + fn skew_mut(&mut self, degrees: T); + + /// Mutable version of [`Skew::skew_xy`]. + fn skew_xy_mut(&mut self, degrees_x: T, degrees_y: T); + + /// Mutable version of [`Skew::skew_around_point`]. + fn skew_around_point_mut(&mut self, degrees_x: T, degrees_y: T, origin: impl Into>); +} + +impl Skew for G +where + T: CoordFloat, + IR: Into>>, + G: Clone + AffineOps + BoundingRect, +{ + type Output = >::Output; + + fn skew(&self, degrees: T) -> Self::Output { + self.skew_xy(degrees, degrees) + } + + fn skew_xy(&self, degrees_x: T, degrees_y: T) -> Self::Output { + let origin = match self.bounding_rect().into() { + Some(rect) => rect.center(), + // Empty geometries have no bounding rect, but in that case + // transforming is a no-op anyway. + None => return self.affine_transform(&AffineTransform::identity()), + }; + self.skew_around_point(degrees_x, degrees_y, origin) + } + + fn skew_around_point(&self, xs: T, ys: T, origin: impl Into>) -> Self::Output { + let transform = AffineTransform::skew(xs, ys, origin); + self.affine_transform(&transform) + } +} + +impl SkewMut for G +where + T: CoordFloat, + IR: Into>>, + G: Clone + AffineOpsMut + BoundingRect, +{ + fn skew_mut(&mut self, degrees: T) { + self.skew_xy_mut(degrees, degrees); + } + + fn skew_xy_mut(&mut self, degrees_x: T, degrees_y: T) { + let origin = match self.bounding_rect().into() { + Some(rect) => rect.center(), + // Empty geometries have no bounding rect, but in that case + // transforming is a no-op anyway. + None => return, + }; + self.skew_around_point_mut(degrees_x, degrees_y, origin); + } + + fn skew_around_point_mut(&mut self, xs: T, ys: T, origin: impl Into>) { + let transform = AffineTransform::skew(xs, ys, origin); + AffineOpsMut::affine_transform_mut(self, &transform); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{line_string, BoundingRect, Centroid, LineString}; + + #[test] + fn skew_linestring() { + let ls: LineString = line_string![ + (x: 3.0, y: 0.0), + (x: 3.0, y: 10.0), + ]; + let origin = ls.bounding_rect().unwrap().centroid(); + let sheared = ls.skew_around_point(45.0, 45.0, origin); + assert_eq!( + sheared, + line_string![ + (x: -1.9999999999999991, y: 0.0), + (x: 7.999999999999999, y: 10.0) + ] + ); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/translate.rs b/rust/sedona-geo-generic-alg/src/algorithm/translate.rs new file mode 100644 index 00000000..9d56aab2 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/translate.rs @@ -0,0 +1,194 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use crate::algorithm::affine_ops::AffineOpsMut; +use crate::{AffineOps, AffineTransform, CoordNum}; + +pub trait Translate { + /// The output type of the translation operations + type Output; + + /// Translate a Geometry along its axes by the given offsets + /// + /// ## Performance + /// + /// If you will be performing multiple transformations, like [`Scale`](crate::Scale), + /// [`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`] trait. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Translate; + /// use sedona_geo_generic_alg::line_string; + /// + /// let ls = line_string![ + /// (x: 0.0, y: 0.0), + /// (x: 5.0, y: 5.0), + /// (x: 10.0, y: 10.0), + /// ]; + /// + /// let translated = ls.translate(1.5, 3.5); + /// + /// assert_eq!(translated, line_string![ + /// (x: 1.5, y: 3.5), + /// (x: 6.5, y: 8.5), + /// (x: 11.5, y: 13.5), + /// ]); + /// ``` + #[must_use] + fn translate(&self, x_offset: T, y_offset: T) -> Self::Output; +} + +/// Mutable version of the [`Translate`] trait that applies translations in place. +/// +/// ## Performance +/// +/// If you will be performing multiple transformations, like [`Scale`](crate::Scale), +/// [`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 +/// [`AffineOpsMut`] trait. +pub trait TranslateMut { + /// Translate a Geometry along its axes, but in place. + fn translate_mut(&mut self, x_offset: T, y_offset: T); +} + +impl Translate for G +where + T: CoordNum, + G: AffineOps, +{ + type Output = >::Output; + + fn translate(&self, x_offset: T, y_offset: T) -> Self::Output { + let transform = AffineTransform::translate(x_offset, y_offset); + self.affine_transform(&transform) + } +} + +impl TranslateMut for G +where + T: CoordNum, + G: AffineOpsMut, +{ + fn translate_mut(&mut self, x_offset: T, y_offset: T) { + let transform = AffineTransform::translate(x_offset, y_offset); + self.affine_transform_mut(&transform) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{line_string, point, polygon, Coord, LineString, Polygon}; + + #[test] + fn test_translate_point() { + let p = point!(x: 1.0, y: 5.0); + let translated = p.translate(30.0, 20.0); + assert_eq!(translated, point!(x: 31.0, y: 25.0)); + } + #[test] + fn test_translate_point_in_place() { + let mut p = point!(x: 1.0, y: 5.0); + p.translate_mut(30.0, 20.0); + assert_eq!(p, point!(x: 31.0, y: 25.0)); + } + #[test] + fn test_translate_linestring() { + let linestring = line_string![ + (x: 0.0, y: 0.0), + (x: 5.0, y: 1.0), + (x: 10.0, y: 0.0), + ]; + let translated = linestring.translate(17.0, 18.0); + assert_eq!( + translated, + line_string![ + (x: 17.0, y: 18.0), + (x: 22.0, y: 19.0), + (x: 27., y: 18.), + ] + ); + } + #[test] + fn test_translate_polygon() { + let poly1 = polygon![ + (x: 5., y: 1.), + (x: 4., y: 2.), + (x: 4., y: 3.), + (x: 5., y: 4.), + (x: 6., y: 4.), + (x: 7., y: 3.), + (x: 7., y: 2.), + (x: 6., y: 1.), + (x: 5., y: 1.), + ]; + let translated = poly1.translate(17.0, 18.0); + let correct = polygon![ + (x: 22.0, y: 19.0), + (x: 21.0, y: 20.0), + (x: 21.0, y: 21.0), + (x: 22.0, y: 22.0), + (x: 23.0, y: 22.0), + (x: 24.0, y: 21.0), + (x: 24.0, y: 20.0), + (x: 23.0, y: 19.0), + (x: 22.0, y: 19.0), + ]; + // results agree with Shapely / GEOS + assert_eq!(translated, correct); + } + #[test] + fn test_rotate_polygon_holes() { + 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 poly1 = Polygon::new(ls1, vec![ls2]); + let rotated = poly1.translate(17.0, 18.0); + let correct_outside = vec![ + Coord::from((22.0, 19.0)), + Coord::from((21.0, 20.0)), + Coord::from((21.0, 21.0)), + Coord::from((22.0, 22.0)), + Coord::from((23.0, 22.0)), + Coord::from((24.0, 21.0)), + Coord::from((24.0, 20.0)), + Coord::from((23.0, 19.0)), + Coord::from((22.0, 19.0)), + ]; + let correct_inside = vec![ + Coord::from((22.0, 19.3)), + Coord::from((22.5, 20.0)), + Coord::from((23.0, 19.3)), + Coord::from((22.0, 19.3)), + ]; + assert_eq!(rotated.exterior().0, correct_outside); + assert_eq!(rotated.interiors()[0].0, correct_inside); + } +} diff --git a/rust/sedona-geo-generic-alg/src/geometry.rs b/rust/sedona-geo-generic-alg/src/geometry.rs new file mode 100644 index 00000000..b9a97e53 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/geometry.rs @@ -0,0 +1,18 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! This module makes all geometry types available +pub use geo_types::geometry::*; diff --git a/rust/sedona-geo-generic-alg/src/lib.rs b/rust/sedona-geo-generic-alg/src/lib.rs new file mode 100644 index 00000000..c4968ad8 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/lib.rs @@ -0,0 +1,146 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +#[cfg(feature = "use-serde")] +#[macro_use] +extern crate serde; + +pub use crate::algorithm::*; +pub use crate::types::Closest; +use std::cmp::Ordering; + +pub use geo_types::{coord, line_string, point, polygon, wkt, CoordFloat, CoordNum}; + +pub mod geometry; +pub use geometry::*; + +/// This module includes all the functions of geometric calculations +pub mod algorithm; +mod types; +mod utils; +use crate::kernels::{RobustKernel, SimpleKernel}; + +#[cfg(test)] +#[macro_use] +extern crate approx; + +#[cfg(test)] +#[macro_use] +extern crate log; + +/// A prelude which re-exports the traits for manipulating objects in this +/// crate. Typically imported with `use geo::prelude::*`. +pub mod prelude { + pub use crate::algorithm::*; +} + +/// A common numeric trait used for geo algorithms +/// +/// Different numeric types have different tradeoffs. `geo` strives to utilize generics to allow +/// users to choose their numeric types. If you are writing a function which you'd like to be +/// generic over all the numeric types supported by geo, you probably want to constrain +/// your function input to `GeoFloat`. For methods which work for integers, and not just floating +/// point, see [`GeoNum`]. +pub trait GeoFloat: + GeoNum + num_traits::Float + num_traits::Signed + num_traits::Bounded + float_next_after::NextAfter +{ +} +impl GeoFloat for T where + T: GeoNum + + num_traits::Float + + num_traits::Signed + + num_traits::Bounded + + float_next_after::NextAfter +{ +} + +/// A trait for methods which work for both integers **and** floating point +pub trait GeoNum: CoordNum { + type Ker: Kernel; + + /// Return the ordering between self and other. + /// + /// For integers, this should behave just like [`Ord`]. + /// + /// For floating point numbers, unlike the standard partial comparison between floating point numbers, this comparison + /// always produces an ordering. + /// + /// See [f64::total_cmp](https://doc.rust-lang.org/src/core/num/f64.rs.html#1432) for details. + fn total_cmp(&self, other: &Self) -> Ordering; +} + +macro_rules! impl_geo_num_for_float { + ($t: ident) => { + impl GeoNum for $t { + type Ker = RobustKernel; + fn total_cmp(&self, other: &Self) -> Ordering { + self.total_cmp(other) + } + } + }; +} +macro_rules! impl_geo_num_for_int { + ($t: ident) => { + impl GeoNum for $t { + type Ker = SimpleKernel; + fn total_cmp(&self, other: &Self) -> Ordering { + self.cmp(other) + } + } + }; +} + +// This is the list of primitives that we support. +impl_geo_num_for_float!(f32); +impl_geo_num_for_float!(f64); +impl_geo_num_for_int!(i16); +impl_geo_num_for_int!(i32); +impl_geo_num_for_int!(i64); +impl_geo_num_for_int!(i128); +impl_geo_num_for_int!(isize); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn total_ord_float() { + assert_eq!(GeoNum::total_cmp(&3.0f64, &2.0f64), Ordering::Greater); + assert_eq!(GeoNum::total_cmp(&2.0f64, &2.0f64), Ordering::Equal); + assert_eq!(GeoNum::total_cmp(&1.0f64, &2.0f64), Ordering::Less); + assert_eq!(GeoNum::total_cmp(&1.0f64, &f64::NAN), Ordering::Less); + assert_eq!(GeoNum::total_cmp(&f64::NAN, &f64::NAN), Ordering::Equal); + assert_eq!(GeoNum::total_cmp(&f64::INFINITY, &f64::NAN), Ordering::Less); + } + + #[test] + fn total_ord_int() { + assert_eq!(GeoNum::total_cmp(&3i32, &2i32), Ordering::Greater); + assert_eq!(GeoNum::total_cmp(&2i32, &2i32), Ordering::Equal); + assert_eq!(GeoNum::total_cmp(&1i32, &2i32), Ordering::Less); + } + + #[test] + fn numeric_types() { + let _n_i16 = Point::new(1i16, 2i16); + let _n_i32 = Point::new(1i32, 2i32); + let _n_i64 = Point::new(1i64, 2i64); + let _n_i128 = Point::new(1i128, 2i128); + let _n_isize = Point::new(1isize, 2isize); + let _n_f32 = Point::new(1.0f32, 2.0f32); + let _n_f64 = Point::new(1.0f64, 2.0f64); + } +} diff --git a/rust/sedona-geo-generic-alg/src/types.rs b/rust/sedona-geo-generic-alg/src/types.rs new file mode 100644 index 00000000..26fe59e4 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/types.rs @@ -0,0 +1,174 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use crate::{GeoFloat, Point}; + +/// The result of trying to find the closest spot on an object to a point. +#[cfg_attr(feature = "use-serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Closest { + /// The point actually intersects with the object. + Intersection(Point), + /// There is exactly one place on this object which is closest to the point. + SinglePoint(Point), + /// There are two or more (possibly infinite or undefined) possible points. + Indeterminate, +} + +impl Closest { + /// Compare two `Closest`s relative to `p` and return a copy of the best + /// one. + pub fn best_of_two(&self, other: &Self, p: Point) -> Self { + use crate::{Distance, Euclidean}; + + let left = match *self { + Closest::Indeterminate => return *other, + Closest::Intersection(_) => return *self, + Closest::SinglePoint(l) => l, + }; + let right = match *other { + Closest::Indeterminate => return *self, + Closest::Intersection(_) => return *other, + Closest::SinglePoint(r) => r, + }; + + if Euclidean.distance(left, p) <= Euclidean.distance(right, p) { + *self + } else { + *other + } + } +} + +/// Implements the common pattern where a Geometry enum simply delegates its trait impl to it's inner type. +/// +/// ``` +/// # use geo::{GeoNum, Coord, Point, Line, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection, Rect, Triangle, Geometry}; +/// +/// trait Foo { +/// fn foo_1(&self, coord: Coord) -> bool; +/// fn foo_2(&self) -> i32; +/// } +/// +/// // Assuming we have an impl for all the inner types like this: +/// impl Foo for Point { +/// fn foo_1(&self, coord: Coord) -> bool { true } +/// fn foo_2(&self) -> i32 { 1 } +/// } +/// impl Foo for Line { +/// fn foo_1(&self, coord: Coord) -> bool { false } +/// fn foo_2(&self) -> i32 { 2 } +/// } +/// impl Foo for LineString { +/// fn foo_1(&self, coord: Coord) -> bool { true } +/// fn foo_2(&self) -> i32 { 3 } +/// } +/// impl Foo for Polygon { +/// fn foo_1(&self, coord: Coord) -> bool { false } +/// fn foo_2(&self) -> i32 { 4 } +/// } +/// impl Foo for MultiPoint { +/// fn foo_1(&self, coord: Coord) -> bool { true } +/// fn foo_2(&self) -> i32 { 5 } +/// } +/// impl Foo for MultiLineString { +/// fn foo_1(&self, coord: Coord) -> bool { false } +/// fn foo_2(&self) -> i32 { 6 } +/// } +/// impl Foo for MultiPolygon { +/// fn foo_1(&self, coord: Coord) -> bool { true } +/// fn foo_2(&self) -> i32 { 7 } +/// } +/// impl Foo for GeometryCollection { +/// fn foo_1(&self, coord: Coord) -> bool { false } +/// fn foo_2(&self) -> i32 { 8 } +/// } +/// impl Foo for Rect { +/// fn foo_1(&self, coord: Coord) -> bool { true } +/// fn foo_2(&self) -> i32 { 9 } +/// } +/// impl Foo for Triangle { +/// fn foo_1(&self, coord: Coord) -> bool { true } +/// fn foo_2(&self) -> i32 { 10 } +/// } +/// +/// // If we want the impl for Geometry to simply delegate to it's +/// // inner case... +/// impl Foo for Geometry { +/// // Instead of writing out this trivial enum delegation... +/// // fn foo_1(&self, coord: Coord) -> bool { +/// // match self { +/// // Geometry::Point(g) => g.foo_1(coord), +/// // Geometry::LineString(g) => g.foo_1(coord), +/// // _ => unimplemented!("...etc for other cases") +/// // } +/// // } +/// // +/// // fn foo_2(&self) -> i32 { +/// // match self { +/// // Geometry::Point(g) => g.foo_2(), +/// // Geometry::LineString(g) => g.foo_2(), +/// // _ => unimplemented!("...etc for other cases") +/// // } +/// // } +/// +/// // we can equivalently write: +/// geo::geometry_delegate_impl! { +/// fn foo_1(&self, coord: Coord) -> bool; +/// fn foo_2(&self) -> i32; +/// } +/// } +/// ``` +#[macro_export] +macro_rules! geometry_delegate_impl { + ($($a:tt)*) => { $crate::__geometry_delegate_impl_helper!{ Geometry, $($a)* } } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! geometry_cow_delegate_impl { + ($($a:tt)*) => { $crate::__geometry_delegate_impl_helper!{ GeometryCow, $($a)* } } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __geometry_delegate_impl_helper { + ( + $enum:ident, + $( + $(#[$outer:meta])* + fn $func_name: ident(&$($self_life:lifetime)?self $(, $arg_name: ident: $arg_type: ty)*) -> $return: ty; + )+ + ) => { + $( + $(#[$outer])* + fn $func_name(&$($self_life)? self, $($arg_name: $arg_type),*) -> $return { + match self { + $enum::Point(g) => g.$func_name($($arg_name),*).into(), + $enum::Line(g) => g.$func_name($($arg_name),*).into(), + $enum::LineString(g) => g.$func_name($($arg_name),*).into(), + $enum::Polygon(g) => g.$func_name($($arg_name),*).into(), + $enum::MultiPoint(g) => g.$func_name($($arg_name),*).into(), + $enum::MultiLineString(g) => g.$func_name($($arg_name),*).into(), + $enum::MultiPolygon(g) => g.$func_name($($arg_name),*).into(), + $enum::GeometryCollection(g) => g.$func_name($($arg_name),*).into(), + $enum::Rect(g) => g.$func_name($($arg_name),*).into(), + $enum::Triangle(g) => g.$func_name($($arg_name),*).into(), + } + } + )+ + }; +} diff --git a/rust/sedona-geo-generic-alg/src/utils.rs b/rust/sedona-geo-generic-alg/src/utils.rs new file mode 100644 index 00000000..c15bcfa6 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/utils.rs @@ -0,0 +1,52 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Internal utility functions, types, and data structures. + +// The Rust standard library has `max` for `Ord`, but not for `PartialOrd` +pub fn partial_max(a: T, b: T) -> T { + if a > b { + a + } else { + b + } +} + +// The Rust standard library has `min` for `Ord`, but not for `PartialOrd` +pub fn partial_min(a: T, b: T) -> T { + if a < b { + a + } else { + b + } +} + +#[cfg(test)] +mod test { + use super::{partial_max, partial_min}; + + #[test] + fn test_partial_max() { + assert_eq!(5, partial_max(5, 4)); + assert_eq!(5, partial_max(5, 5)); + } + + #[test] + fn test_partial_min() { + assert_eq!(4, partial_min(5, 4)); + assert_eq!(4, partial_min(4, 4)); + } +} diff --git a/rust/sedona-geo-traits-ext/Cargo.toml b/rust/sedona-geo-traits-ext/Cargo.toml new file mode 100644 index 00000000..20001dda --- /dev/null +++ b/rust/sedona-geo-traits-ext/Cargo.toml @@ -0,0 +1,32 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +[package] +name = "sedona-geo-traits-ext" +version.workspace = true +homepage.workspace = true +repository.workspace = true +description.workspace = true +readme.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +geo-traits = { workspace = true } +geo-types = { workspace = true } +num-traits = { workspace = true } +wkb = { workspace = true } +byteorder ={ workspace = true } diff --git a/rust/sedona-geo-traits-ext/README.md b/rust/sedona-geo-traits-ext/README.md new file mode 100644 index 00000000..73463218 --- /dev/null +++ b/rust/sedona-geo-traits-ext/README.md @@ -0,0 +1,37 @@ + + +# Geo-Traits Extended + +This crate extends the `geo-traits` crate with additional traits and +implementations. The goal is to provide a set of traits that are useful for +implementing algorithms in `geo-generic-alg` crate. Most of the methods are +inspired by the `geo-types` crate, but are implemented as traits for the +`geo-traits` types. Some methods returns concrete types defined in `geo-types`, +these methods are only for computing tiny, intermediate results during +algorithm execution. By adding methods in `geo-types` to `geo-traits-ext`, +we can port algorithms in `geo` crate for concrete `geo-types` types to generic +`geo-traits-ext` types more easily. + +`geo-traits-ext` traits also has an associated `Tag` type to workaround the +single orphan rule in Rust. For instance, we cannot write blanket `AreaTrait` +implementations for both `LineStringTrait` and `PolygonTrait` because we +cannot show that there would be no type implementing both `LineStringTrait` and +`PolygonTrait`. By adding an associated `Tag` type, we can write blanket +implementations for `AreaTrait` and `AreaTrait`, since +`AreaTrait` and `AreaTrait` are different types. +Please refer to the source code of `geo-generic-alg` for more details. diff --git a/rust/sedona-geo-traits-ext/src/coord.rs b/rust/sedona-geo-traits-ext/src/coord.rs new file mode 100644 index 00000000..2a1922da --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/coord.rs @@ -0,0 +1,67 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Extend CoordTrait traits for the `geo-traits` crate + +use geo_traits::{CoordTrait, UnimplementedCoord}; +use geo_types::{Coord, CoordNum}; + +use crate::{CoordTag, GeoTraitExtWithTypeTag}; + +pub trait CoordTraitExt: CoordTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + #[inline] + fn geo_coord(&self) -> Coord { + Coord { + x: self.x(), + y: self.y(), + } + } +} + +impl CoordTraitExt for Coord +where + T: CoordNum, +{ + fn geo_coord(&self) -> Coord { + *self + } +} + +impl GeoTraitExtWithTypeTag for Coord { + type Tag = CoordTag; +} + +impl CoordTraitExt for &Coord +where + T: CoordNum, +{ + fn geo_coord(&self) -> Coord { + **self + } +} + +impl GeoTraitExtWithTypeTag for &Coord { + type Tag = CoordTag; +} + +impl CoordTraitExt for UnimplementedCoord where T: CoordNum {} + +impl GeoTraitExtWithTypeTag for UnimplementedCoord { + type Tag = CoordTag; +} diff --git a/rust/sedona-geo-traits-ext/src/geometry.rs b/rust/sedona-geo-traits-ext/src/geometry.rs new file mode 100644 index 00000000..d8fb3548 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/geometry.rs @@ -0,0 +1,344 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// Extend GeometryTrait traits for the `geo-traits` crate + +use core::{borrow::Borrow, panic}; + +use geo_traits::*; +use geo_types::*; + +use crate::*; + +#[allow(clippy::type_complexity)] +pub trait GeometryTraitExt: GeometryTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + type PointTypeExt<'a>: 'a + PointTraitExt::T> + where + Self: 'a; + + type LineStringTypeExt<'a>: 'a + LineStringTraitExt::T> + where + Self: 'a; + + type PolygonTypeExt<'a>: 'a + PolygonTraitExt::T> + where + Self: 'a; + + type MultiPointTypeExt<'a>: 'a + MultiPointTraitExt::T> + where + Self: 'a; + + type MultiLineStringTypeExt<'a>: 'a + MultiLineStringTraitExt::T> + where + Self: 'a; + + type MultiPolygonTypeExt<'a>: 'a + MultiPolygonTraitExt::T> + where + Self: 'a; + + type TriangleTypeExt<'a>: 'a + TriangleTraitExt::T> + where + Self: 'a; + + type RectTypeExt<'a>: 'a + RectTraitExt::T> + where + Self: 'a; + + type LineTypeExt<'a>: 'a + LineTraitExt::T> + where + Self: 'a; + + // Note that we don't have a GeometryCollectionTypeExt here, because it would introduce recursive GATs + // such as G::GeometryCollectionTypeExt::GeometryTypeExt::GeometryCollectionTypeExt::... and easily + // trigger a Rust compiler bug: https://github.com/rust-lang/rust/issues/128887 and https://github.com/rust-lang/rust/issues/131960. + // See also https://github.com/geoarrow/geoarrow-rs/issues/1339. + // + // Although this could be worked around by not implementing generic functions using trait-based approach and use + // function-based approach instead, see https://github.com/geoarrow/geoarrow-rs/pull/956 and https://github.com/georust/wkb/pull/77, + // we are not certain if there will be other issues caused by recursive GATs in the future. So we decided to completely get rid + // of recursive GATs. + + type InnerGeometryRef<'a>: 'a + Borrow + where + Self: 'a; + + /// Returns true if this geometry is a GeometryCollection + #[inline] + fn is_collection(&self) -> bool { + matches!(self.as_type(), GeometryType::GeometryCollection(_)) + } + + /// Returns the number of geometries inside this GeometryCollection + #[inline] + fn num_geometries_ext(&self) -> usize { + let GeometryType::GeometryCollection(gc) = self.as_type() else { + panic!("Not a GeometryCollection"); + }; + gc.num_geometries() + } + + /// Cast this geometry to a [`GeometryTypeExt`] enum, which allows for downcasting to a specific + /// type. This does not work when the geometry is a GeometryCollection. Please use `is_collection` + /// to check if the geometry is NOT a GeometryCollection first before calling this method. + fn as_type_ext( + &self, + ) -> GeometryTypeExt< + '_, + Self::PointTypeExt<'_>, + Self::LineStringTypeExt<'_>, + Self::PolygonTypeExt<'_>, + Self::MultiPointTypeExt<'_>, + Self::MultiLineStringTypeExt<'_>, + Self::MultiPolygonTypeExt<'_>, + Self::RectTypeExt<'_>, + Self::TriangleTypeExt<'_>, + Self::LineTypeExt<'_>, + >; + + /// Returns a geometry by index, or None if the index is out of bounds. This method only works with + /// GeometryCollection. Please use `is_collection` to check if the geometry is a GeometryCollection first before + /// calling this method. + fn geometry_ext(&self, i: usize) -> Option>; + + /// Returns a geometry by index without bounds checking. This method only works with GeometryCollection. + /// Please use `is_collection` to check if the geometry is a GeometryCollection first before calling this method. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of geometries. + /// Otherwise, this function may cause undefined behavior. + unsafe fn geometry_unchecked_ext(&self, i: usize) -> Self::InnerGeometryRef<'_>; + + /// Returns an iterator over the geometries in this GeometryCollection. This method only works with + /// GeometryCollection. Please use `is_collection` to check if the geometry is a GeometryCollection first before + /// calling this method. + fn geometries_ext(&self) -> impl Iterator>; +} + +#[derive(Debug)] +pub enum GeometryTypeExt<'a, P, LS, Y, MP, ML, MY, R, TT, L> +where + P: PointTraitExt, + LS: LineStringTraitExt, + Y: PolygonTraitExt, + MP: MultiPointTraitExt, + ML: MultiLineStringTraitExt, + MY: MultiPolygonTraitExt, + R: RectTraitExt, + TT: TriangleTraitExt, + L: LineTraitExt, +

::T: CoordNum, + ::T: CoordNum, + ::T: CoordNum, + ::T: CoordNum, + ::T: CoordNum, + ::T: CoordNum, + ::T: CoordNum, + ::T: CoordNum, + ::T: CoordNum, +{ + Point(&'a P), + LineString(&'a LS), + Polygon(&'a Y), + MultiPoint(&'a MP), + MultiLineString(&'a ML), + MultiPolygon(&'a MY), + Rect(&'a R), + Triangle(&'a TT), + Line(&'a L), +} + +#[macro_export] +macro_rules! forward_geometry_trait_ext_funcs { + ($t:ty) => { + type PointTypeExt<'__g_inner> + = ::PointType<'__g_inner> + where + Self: '__g_inner; + + type LineStringTypeExt<'__g_inner> + = ::LineStringType<'__g_inner> + where + Self: '__g_inner; + + type PolygonTypeExt<'__g_inner> + = ::PolygonType<'__g_inner> + where + Self: '__g_inner; + + type MultiPointTypeExt<'__g_inner> + = ::MultiPointType<'__g_inner> + where + Self: '__g_inner; + + type MultiLineStringTypeExt<'__g_inner> + = ::MultiLineStringType<'__g_inner> + where + Self: '__g_inner; + + type MultiPolygonTypeExt<'__g_inner> + = ::MultiPolygonType<'__g_inner> + where + Self: '__g_inner; + + type RectTypeExt<'__g_inner> + = ::RectType<'__g_inner> + where + Self: '__g_inner; + + type TriangleTypeExt<'__g_inner> + = ::TriangleType<'__g_inner> + where + Self: '__g_inner; + + type LineTypeExt<'__g_inner> + = ::LineType<'__g_inner> + where + Self: '__g_inner; + + fn as_type_ext( + &self, + ) -> GeometryTypeExt< + '_, + Self::PointTypeExt<'_>, + Self::LineStringTypeExt<'_>, + Self::PolygonTypeExt<'_>, + Self::MultiPointTypeExt<'_>, + Self::MultiLineStringTypeExt<'_>, + Self::MultiPolygonTypeExt<'_>, + Self::RectTypeExt<'_>, + Self::TriangleTypeExt<'_>, + Self::LineTypeExt<'_>, + > { + match self.as_type() { + GeometryType::Point(p) => GeometryTypeExt::Point(p), + GeometryType::LineString(ls) => GeometryTypeExt::LineString(ls), + GeometryType::Polygon(p) => GeometryTypeExt::Polygon(p), + GeometryType::MultiPoint(mp) => GeometryTypeExt::MultiPoint(mp), + GeometryType::MultiLineString(mls) => GeometryTypeExt::MultiLineString(mls), + GeometryType::MultiPolygon(mp) => GeometryTypeExt::MultiPolygon(mp), + GeometryType::GeometryCollection(_) => { + panic!("GeometryCollection is not supported in GeometryTraitExt::as_type_ext") + } + GeometryType::Rect(r) => GeometryTypeExt::Rect(r), + GeometryType::Triangle(t) => GeometryTypeExt::Triangle(t), + GeometryType::Line(l) => GeometryTypeExt::Line(l), + } + } + }; +} + +impl GeometryTraitExt for Geometry +where + T: CoordNum, +{ + forward_geometry_trait_ext_funcs!(T); + + type InnerGeometryRef<'a> + = &'a Geometry + where + Self: 'a; + + fn geometry_ext(&self, i: usize) -> Option<&Geometry> { + let GeometryType::GeometryCollection(gc) = self.as_type() else { + panic!("Not a GeometryCollection"); + }; + gc.geometry(i) + } + + unsafe fn geometry_unchecked_ext(&self, i: usize) -> &Geometry { + let GeometryType::GeometryCollection(gc) = self.as_type() else { + panic!("Not a GeometryCollection"); + }; + gc.geometry_unchecked(i) + } + + fn geometries_ext(&self) -> impl Iterator> { + let GeometryType::GeometryCollection(gc) = self.as_type() else { + panic!("Not a GeometryCollection"); + }; + gc.geometries() + } +} + +impl GeoTraitExtWithTypeTag for Geometry { + type Tag = GeometryTag; +} + +impl<'a, T> GeometryTraitExt for &'a Geometry +where + T: CoordNum, +{ + forward_geometry_trait_ext_funcs!(T); + + type InnerGeometryRef<'b> + = &'a Geometry + where + Self: 'b; + + fn geometry_ext(&self, i: usize) -> Option<&'a Geometry> { + let g = *self; + g.geometry_ext(i) + } + + unsafe fn geometry_unchecked_ext(&self, i: usize) -> &'a Geometry { + let g = *self; + g.geometry_unchecked_ext(i) + } + + fn geometries_ext(&self) -> impl Iterator> { + let g = *self; + g.geometries_ext() + } +} + +impl GeoTraitExtWithTypeTag for &Geometry { + type Tag = GeometryTag; +} + +impl GeometryTraitExt for UnimplementedGeometry +where + T: CoordNum, +{ + forward_geometry_trait_ext_funcs!(T); + + type InnerGeometryRef<'a> + = &'a UnimplementedGeometry + where + Self: 'a; + + fn geometry_ext(&self, _i: usize) -> Option> { + unimplemented!() + } + + unsafe fn geometry_unchecked_ext(&self, _i: usize) -> Self::InnerGeometryRef<'_> { + unimplemented!() + } + + fn geometries_ext(&self) -> impl Iterator> { + unimplemented!(); + + // For making the type checker happy + #[allow(unreachable_code)] + core::iter::empty() + } +} + +impl GeoTraitExtWithTypeTag for UnimplementedGeometry { + type Tag = GeometryTag; +} diff --git a/rust/sedona-geo-traits-ext/src/geometry_collection.rs b/rust/sedona-geo-traits-ext/src/geometry_collection.rs new file mode 100644 index 00000000..582e2e75 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/geometry_collection.rs @@ -0,0 +1,101 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// Extend GeometryCollectionTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryCollectionTrait, GeometryTrait, UnimplementedGeometryCollection}; +use geo_types::{CoordNum, GeometryCollection}; + +use crate::{GeoTraitExtWithTypeTag, GeometryCollectionTag, GeometryTraitExt}; + +pub trait GeometryCollectionTraitExt: + GeometryCollectionTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + type GeometryTypeExt<'a>: 'a + GeometryTraitExt::T> + where + Self: 'a; + + fn geometry_ext(&self, i: usize) -> Option>; + + /// Returns a geometry by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of geometries. + /// Otherwise, this function may cause undefined behavior. + unsafe fn geometry_unchecked_ext(&self, i: usize) -> Self::GeometryTypeExt<'_>; + + fn geometries_ext(&self) -> impl Iterator>; +} + +#[macro_export] +macro_rules! forward_geometry_collection_trait_ext_funcs { + () => { + type GeometryTypeExt<'__gc_inner> + = ::GeometryType<'__gc_inner> + where + Self: '__gc_inner; + + #[inline] + fn geometry_ext(&self, i: usize) -> Option> { + ::geometry(self, i) + } + + #[inline] + unsafe fn geometry_unchecked_ext(&self, i: usize) -> Self::GeometryTypeExt<'_> { + unsafe { ::geometry_unchecked(self, i) } + } + + #[inline] + fn geometries_ext(&self) -> impl Iterator> { + ::geometries(self) + } + }; +} + +impl GeometryCollectionTraitExt for GeometryCollection +where + T: CoordNum, +{ + forward_geometry_collection_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for GeometryCollection { + type Tag = GeometryCollectionTag; +} + +impl GeometryCollectionTraitExt for &GeometryCollection +where + T: CoordNum, +{ + forward_geometry_collection_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for &GeometryCollection { + type Tag = GeometryCollectionTag; +} + +impl GeometryCollectionTraitExt for UnimplementedGeometryCollection +where + T: CoordNum, +{ + forward_geometry_collection_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedGeometryCollection { + type Tag = GeometryCollectionTag; +} diff --git a/rust/sedona-geo-traits-ext/src/lib.rs b/rust/sedona-geo-traits-ext/src/lib.rs new file mode 100644 index 00000000..12729e79 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/lib.rs @@ -0,0 +1,64 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Extended traits for the `geo-traits` crate +//! +//! This crate extends the `geo-traits` crate with additional traits and +//! implementations. The goal is to provide a set of traits that are useful for +//! implementing algorithms on top of the `geo` crate. Most of the methods are +//! inspired by the `geo-types` crate, but are implemented as traits on the +//! `geo-traits` types. Some methods returns concrete types defined in `geo-types`, +//! these methods are only for computing tiny, intermediate results during +//! algorithm execution. +//! +//! The crate is designed to support migration of the `geo` crate to use the +//! traits defined in `geo-traits` by providing generic implementations of the +//! geospatial algorithms, rather than implementing algorithms on concrete types +//! defined in `geo-types`. +//! +//! The crate is currently under active development and the API is subject to +//! change. + +pub use coord::CoordTraitExt; +pub use geometry::{GeometryTraitExt, GeometryTypeExt}; +pub use geometry_collection::GeometryCollectionTraitExt; +pub use line::LineTraitExt; +pub use line_string::LineStringTraitExt; +pub use multi_line_string::MultiLineStringTraitExt; +pub use multi_point::MultiPointTraitExt; +pub use multi_polygon::MultiPolygonTraitExt; +pub use point::PointTraitExt; +pub use polygon::PolygonTraitExt; +pub use rect::RectTraitExt; +pub use triangle::TriangleTraitExt; + +mod coord; +mod geometry; +mod geometry_collection; +mod line; +mod line_string; +mod multi_line_string; +mod multi_point; +mod multi_polygon; +mod point; +mod polygon; +mod rect; +mod triangle; + +pub use type_tag::*; +mod type_tag; + +pub mod wkb_ext; diff --git a/rust/sedona-geo-traits-ext/src/line.rs b/rust/sedona-geo-traits-ext/src/line.rs new file mode 100644 index 00000000..1ad57361 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/line.rs @@ -0,0 +1,132 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// Extend LineTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryTrait, LineTrait, UnimplementedLine}; +use geo_types::{CoordNum, Line}; + +use crate::{CoordTraitExt, GeoTraitExtWithTypeTag, LineTag}; + +pub trait LineTraitExt: LineTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + type CoordTypeExt<'a>: 'a + CoordTraitExt::T> + where + Self: 'a; + + fn start_ext(&self) -> Self::CoordTypeExt<'_>; + fn end_ext(&self) -> Self::CoordTypeExt<'_>; + fn coords_ext(&self) -> [Self::CoordTypeExt<'_>; 2]; + + #[inline] + fn start_coord(&self) -> geo_types::Coord<::T> { + self.start_ext().geo_coord() + } + + #[inline] + fn end_coord(&self) -> geo_types::Coord<::T> { + self.end_ext().geo_coord() + } + + #[inline] + fn geo_line(&self) -> Line<::T> { + Line::new(self.start_coord(), self.end_coord()) + } +} + +#[macro_export] +macro_rules! forward_line_trait_ext_funcs { + () => { + type CoordTypeExt<'__l_inner> + = ::CoordType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn start_ext(&self) -> Self::CoordTypeExt<'_> { + ::start(self) + } + + #[inline] + fn end_ext(&self) -> Self::CoordTypeExt<'_> { + ::end(self) + } + + #[inline] + fn coords_ext(&self) -> [Self::CoordTypeExt<'_>; 2] { + [self.start_ext(), self.end_ext()] + } + }; +} + +impl LineTraitExt for Line +where + T: CoordNum, +{ + forward_line_trait_ext_funcs!(); + + fn start_coord(&self) -> geo_types::Coord { + self.start + } + + fn end_coord(&self) -> geo_types::Coord { + self.end + } + + fn geo_line(&self) -> geo_types::Line { + *self + } +} + +impl GeoTraitExtWithTypeTag for Line { + type Tag = LineTag; +} + +impl LineTraitExt for &Line +where + T: CoordNum, +{ + forward_line_trait_ext_funcs!(); + + fn start_coord(&self) -> geo_types::Coord { + self.start + } + + fn end_coord(&self) -> geo_types::Coord { + self.end + } + + fn geo_line(&self) -> geo_types::Line { + **self + } +} + +impl GeoTraitExtWithTypeTag for &Line { + type Tag = LineTag; +} + +impl LineTraitExt for UnimplementedLine +where + T: CoordNum, +{ + forward_line_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedLine { + type Tag = LineTag; +} diff --git a/rust/sedona-geo-traits-ext/src/line_string.rs b/rust/sedona-geo-traits-ext/src/line_string.rs new file mode 100644 index 00000000..0d5a70bf --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/line_string.rs @@ -0,0 +1,193 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// Extend LineStringTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryTrait, LineStringTrait, UnimplementedLineString}; +use geo_types::{Coord, CoordNum, Line, LineString, Triangle}; + +use crate::{CoordTraitExt, GeoTraitExtWithTypeTag, LineStringTag}; + +pub trait LineStringTraitExt: + LineStringTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + type CoordTypeExt<'a>: 'a + CoordTraitExt::T> + where + Self: 'a; + + fn coord_ext(&self, i: usize) -> Option>; + + /// Returns a coordinate by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of coordinates. + /// Otherwise, this function may cause undefined behavior. + unsafe fn coord_unchecked_ext(&self, i: usize) -> Self::CoordTypeExt<'_>; + + fn coords_ext(&self) -> impl Iterator>; + + /// Returns a coordinate by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of coordinates. + /// Otherwise, this function may cause undefined behavior. + #[inline] + unsafe fn geo_coord_unchecked(&self, i: usize) -> Coord { + self.coord_unchecked_ext(i).geo_coord() + } + + /// Return an iterator yielding one [`Line`] for each line segment + /// in the [`LineString`][`geo_types::LineString`]. + #[inline] + fn lines(&'_ self) -> impl ExactSizeIterator::T>> + '_ { + let num_coords = self.num_coords(); + (0..num_coords.saturating_sub(1)).map(|i| unsafe { + let coord1 = self.coord_unchecked_ext(i); + let coord2 = self.coord_unchecked_ext(i + 1); + Line::new(coord1.geo_coord(), coord2.geo_coord()) + }) + } + + /// Return an iterator yielding one [`Line`] for each line segment in the [`LineString`][`geo_types::LineString`], + /// starting from the **end** point of the LineString, working towards the start. + /// + /// Note: This is like [`Self::lines`], but the sequence **and** the orientation of + /// segments are reversed. + #[inline] + fn rev_lines(&'_ self) -> impl ExactSizeIterator::T>> + '_ { + let num_coords = self.num_coords(); + (num_coords - 1..0).map(|i| unsafe { + let coord1 = self.coord_unchecked_ext(i); + let coord2 = self.coord_unchecked_ext(i - 1); + Line::new(coord2.geo_coord(), coord1.geo_coord()) + }) + } + + /// An iterator which yields the coordinates of a [`LineString`][`geo_types::LineString`] as [Triangle]s + #[inline] + fn triangles( + &'_ self, + ) -> impl ExactSizeIterator::T>> + '_ { + let num_coords = self.num_coords(); + (0..num_coords - 2).map(|i| unsafe { + let coord1 = self.coord_unchecked_ext(i); + let coord2 = self.coord_unchecked_ext(i + 1); + let coord3 = self.coord_unchecked_ext(i + 2); + Triangle::new(coord1.geo_coord(), coord2.geo_coord(), coord3.geo_coord()) + }) + } + + // Returns an iterator yielding the coordinates of this line string as `geo_types::Coord`s. + #[inline] + fn coord_iter(&self) -> impl Iterator::T>> { + self.coords_ext().map(|c| c.geo_coord()) + } + + #[inline] + fn is_closed(&self) -> bool { + match (self.coords_ext().next(), self.coords_ext().last()) { + (Some(first), Some(last)) => first.geo_coord() == last.geo_coord(), + (None, None) => true, + _ => false, + } + } +} + +#[macro_export] +macro_rules! forward_line_string_trait_ext_funcs { + () => { + type CoordTypeExt<'__l_inner> + = ::CoordType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn coord_ext(&self, i: usize) -> Option> { + ::coord(self, i) + } + + #[inline] + unsafe fn coord_unchecked_ext(&self, i: usize) -> Self::CoordTypeExt<'_> { + ::coord_unchecked(self, i) + } + + #[inline] + fn coords_ext(&self) -> impl Iterator> { + ::coords(self) + } + }; +} + +impl LineStringTraitExt for LineString +where + T: CoordNum, +{ + forward_line_string_trait_ext_funcs!(); + + unsafe fn geo_coord_unchecked(&self, i: usize) -> Coord { + *self.0.get_unchecked(i) + } + + // Delegate to the `geo-types` implementation for less performance overhead + fn lines(&'_ self) -> impl ExactSizeIterator::T>> + '_ { + self.lines() + } + + fn coord_iter(&self) -> impl Iterator::T>> { + self.0.iter().copied() + } +} + +impl GeoTraitExtWithTypeTag for LineString { + type Tag = LineStringTag; +} + +impl LineStringTraitExt for &LineString +where + T: CoordNum, +{ + forward_line_string_trait_ext_funcs!(); + + unsafe fn geo_coord_unchecked(&self, i: usize) -> Coord { + *self.0.get_unchecked(i) + } + + // Delegate to the `geo-types` implementation for less performance overhead + fn lines(&'_ self) -> impl ExactSizeIterator::T>> + '_ { + (*self).lines() + } + + fn coord_iter(&self) -> impl Iterator::T>> { + self.0.iter().copied() + } +} + +impl GeoTraitExtWithTypeTag for &LineString { + type Tag = LineStringTag; +} + +impl LineStringTraitExt for UnimplementedLineString +where + T: CoordNum, +{ + forward_line_string_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedLineString { + type Tag = LineStringTag; +} diff --git a/rust/sedona-geo-traits-ext/src/multi_line_string.rs b/rust/sedona-geo-traits-ext/src/multi_line_string.rs new file mode 100644 index 00000000..1b673fa4 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/multi_line_string.rs @@ -0,0 +1,108 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// Extend MultiLineStringTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryTrait, MultiLineStringTrait, UnimplementedMultiLineString}; +use geo_types::{CoordNum, MultiLineString}; + +use crate::{GeoTraitExtWithTypeTag, LineStringTraitExt, MultiLineStringTag}; + +pub trait MultiLineStringTraitExt: + MultiLineStringTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + type LineStringTypeExt<'a>: 'a + LineStringTraitExt::T> + where + Self: 'a; + + fn line_string_ext(&self, i: usize) -> Option>; + + /// Returns a line string by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of line strings. + /// Otherwise, this function may cause undefined behavior. + unsafe fn line_string_unchecked_ext(&self, i: usize) -> Self::LineStringTypeExt<'_>; + + fn line_strings_ext(&self) -> impl Iterator>; + + /// True if the MultiLineString is empty or if all of its LineStrings are closed + #[inline] + fn is_closed(&self) -> bool { + // Note: Unlike JTS et al, we consider an empty MultiLineString as closed. + self.line_strings_ext().all(|ls| ls.is_closed()) + } +} + +#[macro_export] +macro_rules! forward_multi_line_string_trait_ext_funcs { + () => { + type LineStringTypeExt<'__l_inner> + = ::InnerLineStringType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn line_string_ext(&self, i: usize) -> Option> { + ::line_string(self, i) + } + + #[inline] + unsafe fn line_string_unchecked_ext(&self, i: usize) -> Self::LineStringTypeExt<'_> { + ::line_string_unchecked(self, i) + } + + #[inline] + fn line_strings_ext(&self) -> impl Iterator> { + ::line_strings(self) + } + }; +} + +impl MultiLineStringTraitExt for MultiLineString +where + T: CoordNum, +{ + forward_multi_line_string_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for MultiLineString { + type Tag = MultiLineStringTag; +} + +impl MultiLineStringTraitExt for &MultiLineString +where + T: CoordNum, +{ + forward_multi_line_string_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for &MultiLineString { + type Tag = MultiLineStringTag; +} + +impl MultiLineStringTraitExt for UnimplementedMultiLineString +where + T: CoordNum, +{ + forward_multi_line_string_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedMultiLineString { + type Tag = MultiLineStringTag; +} diff --git a/rust/sedona-geo-traits-ext/src/multi_point.rs b/rust/sedona-geo-traits-ext/src/multi_point.rs new file mode 100644 index 00000000..425f7991 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/multi_point.rs @@ -0,0 +1,135 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// Extend MultiPointTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryTrait, MultiPointTrait, UnimplementedMultiPoint}; +use geo_types::{Coord, CoordNum, MultiPoint}; + +use crate::{CoordTraitExt, GeoTraitExtWithTypeTag, MultiPointTag, PointTraitExt}; + +pub trait MultiPointTraitExt: + MultiPointTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + type PointTypeExt<'a>: 'a + PointTraitExt::T> + where + Self: 'a; + + fn point_ext(&self, i: usize) -> Option>; + + /// Returns a point by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of points. + /// Otherwise, this function may cause undefined behavior. + unsafe fn point_unchecked_ext(&self, i: usize) -> Self::PointTypeExt<'_>; + + /// Returns a coordinate by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of points. + /// Otherwise, this function may cause undefined behavior. + #[inline] + unsafe fn geo_coord_unchecked(&self, i: usize) -> Option::T>> { + let point = unsafe { self.point_unchecked_ext(i) }; + point.coord_ext().map(|c| c.geo_coord()) + } + + fn points_ext(&self) -> impl DoubleEndedIterator>; + + #[inline] + fn coord_iter(&self) -> impl DoubleEndedIterator::T>> { + self.points_ext().flat_map(|p| p.geo_coord()) + } +} + +#[macro_export] +macro_rules! forward_multi_point_trait_ext_funcs { + () => { + type PointTypeExt<'__l_inner> + = ::InnerPointType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn point_ext(&self, i: usize) -> Option> { + ::point(self, i) + } + + #[inline] + unsafe fn point_unchecked_ext(&self, i: usize) -> Self::PointTypeExt<'_> { + ::point_unchecked(self, i) + } + + #[inline] + fn points_ext(&self) -> impl DoubleEndedIterator> { + ::points(self) + } + }; +} + +impl MultiPointTraitExt for MultiPoint +where + T: CoordNum, +{ + forward_multi_point_trait_ext_funcs!(); + + unsafe fn geo_coord_unchecked(&self, i: usize) -> Option> { + Some(self.0.get_unchecked(i).0) + } + + // Specialized implementation for geo_types::MultiPoint to reduce performance overhead + fn coord_iter(&self) -> impl DoubleEndedIterator::T>> { + self.0.iter().map(|p| p.0) + } +} + +impl GeoTraitExtWithTypeTag for MultiPoint { + type Tag = MultiPointTag; +} + +impl MultiPointTraitExt for &MultiPoint +where + T: CoordNum, +{ + forward_multi_point_trait_ext_funcs!(); + + unsafe fn geo_coord_unchecked(&self, i: usize) -> Option> { + Some(self.0.get_unchecked(i).0) + } + + // Specialized implementation for geo_types::MultiPoint to reduce performance overhead + fn coord_iter(&self) -> impl DoubleEndedIterator::T>> { + self.0.iter().map(|p| p.0) + } +} + +impl GeoTraitExtWithTypeTag for &MultiPoint { + type Tag = MultiPointTag; +} + +impl MultiPointTraitExt for UnimplementedMultiPoint +where + T: CoordNum, +{ + forward_multi_point_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedMultiPoint { + type Tag = MultiPointTag; +} diff --git a/rust/sedona-geo-traits-ext/src/multi_polygon.rs b/rust/sedona-geo-traits-ext/src/multi_polygon.rs new file mode 100644 index 00000000..cb9cfed2 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/multi_polygon.rs @@ -0,0 +1,101 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// Extend MultiPolygonTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryTrait, MultiPolygonTrait, UnimplementedMultiPolygon}; +use geo_types::{CoordNum, MultiPolygon}; + +use crate::{GeoTraitExtWithTypeTag, MultiPolygonTag, PolygonTraitExt}; + +pub trait MultiPolygonTraitExt: + MultiPolygonTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + type PolygonTypeExt<'a>: 'a + PolygonTraitExt::T> + where + Self: 'a; + + fn polygon_ext(&self, i: usize) -> Option>; + + /// Returns a polygon by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of polygons. + /// Otherwise, this function may cause undefined behavior. + unsafe fn polygon_unchecked_ext(&self, i: usize) -> Self::PolygonTypeExt<'_>; + + fn polygons_ext(&self) -> impl Iterator>; +} + +#[macro_export] +macro_rules! forward_multi_polygon_trait_ext_funcs { + () => { + type PolygonTypeExt<'__l_inner> + = ::InnerPolygonType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn polygon_ext(&self, i: usize) -> Option> { + ::polygon(self, i) + } + + #[inline] + unsafe fn polygon_unchecked_ext(&self, i: usize) -> Self::PolygonTypeExt<'_> { + ::polygon_unchecked(self, i) + } + + #[inline] + fn polygons_ext(&self) -> impl Iterator> { + ::polygons(self) + } + }; +} + +impl MultiPolygonTraitExt for MultiPolygon +where + T: CoordNum, +{ + forward_multi_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for MultiPolygon { + type Tag = MultiPolygonTag; +} + +impl MultiPolygonTraitExt for &MultiPolygon +where + T: CoordNum, +{ + forward_multi_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for &MultiPolygon { + type Tag = MultiPolygonTag; +} + +impl MultiPolygonTraitExt for UnimplementedMultiPolygon +where + T: CoordNum, +{ + forward_multi_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedMultiPolygon { + type Tag = MultiPolygonTag; +} diff --git a/rust/sedona-geo-traits-ext/src/point.rs b/rust/sedona-geo-traits-ext/src/point.rs new file mode 100644 index 00000000..30d23cc1 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/point.rs @@ -0,0 +1,108 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// Extend PointTrait traits for the `geo-traits` crate + +use geo_traits::{CoordTrait, GeometryTrait, PointTrait, UnimplementedPoint}; +use geo_types::{Coord, CoordNum, Point}; + +use crate::{CoordTraitExt, GeoTraitExtWithTypeTag, PointTag}; + +pub trait PointTraitExt: PointTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + type CoordTypeExt<'a>: 'a + CoordTraitExt::T> + where + Self: 'a; + + fn coord_ext(&self) -> Option>; + + #[inline] + fn geo_point(&self) -> Option::T>> { + self.coord_ext() + .map(|coord| Point::new(coord.x(), coord.y())) + } + + #[inline] + fn geo_coord(&self) -> Option::T>> { + self.coord_ext().map(|coord| coord.geo_coord()) + } +} + +#[macro_export] +macro_rules! forward_point_trait_ext_funcs { + () => { + type CoordTypeExt<'__l_inner> + = ::CoordType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn coord_ext(&self) -> Option> { + ::coord(self) + } + }; +} + +impl PointTraitExt for Point +where + T: CoordNum, +{ + forward_point_trait_ext_funcs!(); + + fn geo_point(&self) -> Option> { + Some(*self) + } + + fn geo_coord(&self) -> Option> { + Some(self.0) + } +} + +impl GeoTraitExtWithTypeTag for Point { + type Tag = PointTag; +} + +impl PointTraitExt for &Point +where + T: CoordNum, +{ + forward_point_trait_ext_funcs!(); + + fn geo_point(&self) -> Option> { + Some(**self) + } + + fn geo_coord(&self) -> Option> { + Some(self.0) + } +} + +impl GeoTraitExtWithTypeTag for &Point { + type Tag = PointTag; +} + +impl PointTraitExt for UnimplementedPoint +where + T: CoordNum, +{ + forward_point_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedPoint { + type Tag = PointTag; +} diff --git a/rust/sedona-geo-traits-ext/src/polygon.rs b/rust/sedona-geo-traits-ext/src/polygon.rs new file mode 100644 index 00000000..f34eaacc --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/polygon.rs @@ -0,0 +1,109 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// Extend PolygonTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryTrait, PolygonTrait, UnimplementedPolygon}; +use geo_types::{CoordNum, Polygon}; + +use crate::{GeoTraitExtWithTypeTag, LineStringTraitExt, PolygonTag}; + +pub trait PolygonTraitExt: PolygonTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + type RingTypeExt<'a>: 'a + LineStringTraitExt::T> + where + Self: 'a; + + fn exterior_ext(&self) -> Option>; + fn interiors_ext( + &self, + ) -> impl DoubleEndedIterator + ExactSizeIterator>; + fn interior_ext(&self, i: usize) -> Option>; + + /// Returns an interior ring by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of interior rings. + /// Otherwise, this function may cause undefined behavior. + unsafe fn interior_unchecked_ext(&self, i: usize) -> Self::RingTypeExt<'_>; +} + +#[macro_export] +macro_rules! forward_polygon_trait_ext_funcs { + () => { + type RingTypeExt<'__l_inner> + = ::RingType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn exterior_ext(&self) -> Option> { + ::exterior(self) + } + + #[inline] + fn interiors_ext( + &self, + ) -> impl DoubleEndedIterator + ExactSizeIterator> { + ::interiors(self) + } + + #[inline] + fn interior_ext(&self, i: usize) -> Option> { + ::interior(self, i) + } + + #[inline] + unsafe fn interior_unchecked_ext(&self, i: usize) -> Self::RingTypeExt<'_> { + ::interior_unchecked(self, i) + } + }; +} + +impl PolygonTraitExt for Polygon +where + T: CoordNum, +{ + forward_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for Polygon { + type Tag = PolygonTag; +} + +impl PolygonTraitExt for &Polygon +where + T: CoordNum, +{ + forward_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for &Polygon { + type Tag = PolygonTag; +} + +impl PolygonTraitExt for UnimplementedPolygon +where + T: CoordNum, +{ + forward_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedPolygon { + type Tag = PolygonTag; +} diff --git a/rust/sedona-geo-traits-ext/src/rect.rs b/rust/sedona-geo-traits-ext/src/rect.rs new file mode 100644 index 00000000..2124ce00 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/rect.rs @@ -0,0 +1,309 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// Extend RectTrait traits for the `geo-traits` crate + +use geo_traits::{CoordTrait, GeometryTrait, RectTrait, UnimplementedRect}; +use geo_types::{coord, Coord, CoordFloat, CoordNum, Line, LineString, Polygon, Rect}; +use num_traits::One; + +use crate::{CoordTraitExt, GeoTraitExtWithTypeTag, RectTag}; + +static RECT_INVALID_BOUNDS_ERROR: &str = "Failed to create Rect: 'min' coordinate's x/y value must be smaller or equal to the 'max' x/y value"; + +pub trait RectTraitExt: RectTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + type CoordTypeExt<'a>: 'a + CoordTraitExt::T> + where + Self: 'a; + + fn min_ext(&self) -> Self::CoordTypeExt<'_>; + fn max_ext(&self) -> Self::CoordTypeExt<'_>; + + #[inline] + fn min_coord(&self) -> Coord<::T> { + self.min_ext().geo_coord() + } + + #[inline] + fn max_coord(&self) -> Coord<::T> { + self.max_ext().geo_coord() + } + + #[inline] + fn geo_rect(&self) -> Rect<::T> { + Rect::new(self.min_coord(), self.max_coord()) + } + + #[inline] + fn width(&self) -> ::T { + self.max().x() - self.min().x() + } + + #[inline] + fn height(&self) -> ::T { + self.max().y() - self.min().y() + } + + fn to_polygon(&self) -> Polygon<::T> + where + ::T: Clone, + { + let min_coord = self.min_coord(); + let max_coord = self.max_coord(); + + let min_x = min_coord.x; + let min_y = min_coord.y; + let max_x = max_coord.x; + let max_y = max_coord.y; + + let line_string = LineString::new(vec![ + Coord { x: min_x, y: min_y }, + Coord { x: min_x, y: max_y }, + Coord { x: max_x, y: max_y }, + Coord { x: max_x, y: min_y }, + Coord { x: min_x, y: min_y }, + ]); + + Polygon::new(line_string, vec![]) + } + + fn to_lines(&self) -> [Line<::T>; 4] { + let min_coord = self.min_coord(); + let max_coord = self.max_coord(); + [ + Line::new( + coord! { + x: max_coord.x, + y: min_coord.y, + }, + coord! { + x: max_coord.x, + y: max_coord.y, + }, + ), + Line::new( + coord! { + x: max_coord.x, + y: min_coord.y, + }, + coord! { + x: min_coord.x, + y: max_coord.y, + }, + ), + Line::new( + coord! { + x: min_coord.x, + y: max_coord.y, + }, + coord! { + x: min_coord.x, + y: min_coord.y, + }, + ), + Line::new( + coord! { + x: min_coord.x, + y: min_coord.y, + }, + coord! { + x: max_coord.x, + y: min_coord.y, + }, + ), + ] + } + + fn to_line_string(&self) -> LineString<::T> + where + ::T: Clone, + { + let min_coord = self.min_coord(); + let max_coord = self.max_coord(); + + let min_x = min_coord.x; + let min_y = min_coord.y; + let max_x = max_coord.x; + let max_y = max_coord.y; + + LineString::new(vec![ + Coord { x: min_x, y: min_y }, + Coord { x: min_x, y: max_y }, + Coord { x: max_x, y: max_y }, + Coord { x: max_x, y: min_y }, + Coord { x: min_x, y: min_y }, + ]) + } + + #[inline] + fn has_valid_bounds(&self) -> bool { + let min_coord = self.min_coord(); + let max_coord = self.max_coord(); + min_coord.x <= max_coord.x && min_coord.y <= max_coord.y + } + + #[inline] + fn assert_valid_bounds(&self) { + if !self.has_valid_bounds() { + panic!("{}", RECT_INVALID_BOUNDS_ERROR); + } + } + + #[inline] + fn contains_point(&self, coord: &Coord<::T>) -> bool + where + ::T: PartialOrd, + { + let min_coord = self.min_coord(); + let max_coord = self.max_coord(); + + let min_x = min_coord.x; + let min_y = min_coord.y; + let max_x = max_coord.x; + let max_y = max_coord.y; + + (min_x <= coord.x && coord.x <= max_x) && (min_y <= coord.y && coord.y <= max_y) + } + + #[inline] + fn contains_rect(&self, rect: &Self) -> bool + where + ::T: PartialOrd, + { + let self_min = self.min_coord(); + let self_max = self.max_coord(); + let other_min = rect.min_coord(); + let other_max = rect.max_coord(); + + let self_min_x = self_min.x; + let self_min_y = self_min.y; + let self_max_x = self_max.x; + let self_max_y = self_max.y; + + let other_min_x = other_min.x; + let other_min_y = other_min.y; + let other_max_x = other_max.x; + let other_max_y = other_max.y; + + (self_min_x <= other_min_x && other_max_x <= self_max_x) + && (self_min_y <= other_min_y && other_max_y <= self_max_y) + } + + #[inline] + fn center(&self) -> Coord<::T> + where + ::T: CoordFloat, + { + let two = ::T::one() + ::T::one(); + coord! { + x: (self.max_coord().x + self.min_coord().x) / two, + y: (self.max_coord().y + self.min_coord().y) / two, + } + } +} + +#[macro_export] +macro_rules! forward_rect_trait_ext_funcs { + () => { + type CoordTypeExt<'__l_inner> + = ::CoordType<'__l_inner> + where + Self: '__l_inner; + + fn min_ext(&self) -> Self::CoordTypeExt<'_> { + ::min(self) + } + + fn max_ext(&self) -> Self::CoordTypeExt<'_> { + ::max(self) + } + }; +} + +impl RectTraitExt for Rect +where + T: CoordNum, +{ + forward_rect_trait_ext_funcs!(); + + fn min_coord(&self) -> Coord { + Rect::min(*self) + } + + fn max_coord(&self) -> Coord { + Rect::max(*self) + } + + fn geo_rect(&self) -> Rect { + *self + } + + fn to_lines(&self) -> [Line<::T>; 4] { + self.to_lines() + } +} + +impl GeoTraitExtWithTypeTag for Rect { + type Tag = RectTag; +} + +impl RectTraitExt for &Rect +where + T: CoordNum, +{ + forward_rect_trait_ext_funcs!(); + + fn min_coord(&self) -> Coord { + Rect::min(**self) + } + + fn max_coord(&self) -> Coord { + Rect::max(**self) + } + + fn geo_rect(&self) -> Rect { + **self + } + + fn to_polygon(&self) -> Polygon<::T> + where + ::T: Clone, + { + (*self).to_polygon() + } + + fn to_lines(&self) -> [Line<::T>; 4] { + (*self).to_lines() + } +} + +impl GeoTraitExtWithTypeTag for &Rect { + type Tag = RectTag; +} + +impl RectTraitExt for UnimplementedRect +where + T: CoordNum, +{ + forward_rect_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedRect { + type Tag = RectTag; +} diff --git a/rust/sedona-geo-traits-ext/src/triangle.rs b/rust/sedona-geo-traits-ext/src/triangle.rs new file mode 100644 index 00000000..c12b8986 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/triangle.rs @@ -0,0 +1,167 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// Extend TriangleTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryTrait, TriangleTrait, UnimplementedTriangle}; +use geo_types::{polygon, Coord, CoordNum, Line, Polygon, Triangle}; + +use crate::{CoordTraitExt, GeoTraitExtWithTypeTag, TriangleTag}; + +pub trait TriangleTraitExt: TriangleTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + type CoordTypeExt<'a>: 'a + CoordTraitExt::T> + where + Self: 'a; + + fn first_ext(&self) -> Self::CoordTypeExt<'_>; + fn second_ext(&self) -> Self::CoordTypeExt<'_>; + fn third_ext(&self) -> Self::CoordTypeExt<'_>; + fn coords_ext(&self) -> [Self::CoordTypeExt<'_>; 3]; + + #[inline] + fn first_coord(&self) -> Coord<::T> { + self.first_ext().geo_coord() + } + + #[inline] + fn second_coord(&self) -> Coord<::T> { + self.second_ext().geo_coord() + } + + #[inline] + fn third_coord(&self) -> Coord<::T> { + self.third_ext().geo_coord() + } + + #[inline] + fn to_array(&self) -> [Coord<::T>; 3] { + [self.first_coord(), self.second_coord(), self.third_coord()] + } + + #[inline] + fn to_lines(&self) -> [Line<::T>; 3] { + [ + Line::new(self.first_coord(), self.second_coord()), + Line::new(self.second_coord(), self.third_coord()), + Line::new(self.third_coord(), self.first_coord()), + ] + } + + #[inline] + fn to_polygon(&self) -> Polygon<::T> { + polygon![ + self.first_coord(), + self.second_coord(), + self.third_coord(), + self.first_coord(), + ] + } + + #[inline] + fn coord_iter(&self) -> impl Iterator::T>> { + [self.first_coord(), self.second_coord(), self.third_coord()].into_iter() + } +} + +#[macro_export] +macro_rules! forward_triangle_trait_ext_funcs { + () => { + type CoordTypeExt<'__l_inner> + = ::CoordType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn first_ext(&self) -> Self::CoordTypeExt<'_> { + ::first(self) + } + + #[inline] + fn second_ext(&self) -> Self::CoordTypeExt<'_> { + ::second(self) + } + + #[inline] + fn third_ext(&self) -> Self::CoordTypeExt<'_> { + ::third(self) + } + + #[inline] + fn coords_ext(&self) -> [Self::CoordTypeExt<'_>; 3] { + [self.first_ext(), self.second_ext(), self.third_ext()] + } + }; +} + +impl TriangleTraitExt for Triangle +where + T: CoordNum, +{ + forward_triangle_trait_ext_funcs!(); + + fn first_coord(&self) -> Coord<::T> { + self.0 + } + + fn second_coord(&self) -> Coord<::T> { + self.1 + } + + fn third_coord(&self) -> Coord<::T> { + self.2 + } +} + +impl GeoTraitExtWithTypeTag for Triangle { + type Tag = TriangleTag; +} + +impl TriangleTraitExt for &Triangle +where + T: CoordNum, +{ + forward_triangle_trait_ext_funcs!(); + + fn first_coord(&self) -> Coord<::T> { + self.0 + } + + fn second_coord(&self) -> Coord<::T> { + self.1 + } + + fn third_coord(&self) -> Coord<::T> { + self.2 + } +} + +impl GeoTraitExtWithTypeTag for &Triangle { + type Tag = TriangleTag; +} + +impl TriangleTraitExt for UnimplementedTriangle +where + T: CoordNum, +{ + forward_triangle_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedTriangle { + type Tag = TriangleTag; +} diff --git a/rust/sedona-geo-traits-ext/src/type_tag.rs b/rust/sedona-geo-traits-ext/src/type_tag.rs new file mode 100644 index 00000000..9ec9c792 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/type_tag.rs @@ -0,0 +1,49 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// Geometry type tags for dispatching algorithm traits to the corresponding implementation + +pub trait GeoTypeTag {} + +pub struct CoordTag; +pub struct PointTag; +pub struct LineStringTag; +pub struct PolygonTag; +pub struct MultiPointTag; +pub struct MultiLineStringTag; +pub struct MultiPolygonTag; +pub struct GeometryCollectionTag; +pub struct GeometryTag; +pub struct LineTag; +pub struct RectTag; +pub struct TriangleTag; + +impl GeoTypeTag for CoordTag {} +impl GeoTypeTag for PointTag {} +impl GeoTypeTag for LineStringTag {} +impl GeoTypeTag for PolygonTag {} +impl GeoTypeTag for MultiPointTag {} +impl GeoTypeTag for MultiLineStringTag {} +impl GeoTypeTag for MultiPolygonTag {} +impl GeoTypeTag for GeometryCollectionTag {} +impl GeoTypeTag for GeometryTag {} +impl GeoTypeTag for LineTag {} +impl GeoTypeTag for RectTag {} +impl GeoTypeTag for TriangleTag {} + +pub trait GeoTraitExtWithTypeTag { + type Tag: GeoTypeTag; +} diff --git a/rust/sedona-geo-traits-ext/src/wkb_ext.rs b/rust/sedona-geo-traits-ext/src/wkb_ext.rs new file mode 100644 index 00000000..df0f5443 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/wkb_ext.rs @@ -0,0 +1,551 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use std::marker::PhantomData; + +use crate::*; +use byteorder::{BigEndian, ByteOrder, LittleEndian}; +use geo_traits::{ + GeometryCollectionTrait, GeometryTrait, GeometryType, LineStringTrait, MultiLineStringTrait, + MultiPointTrait, MultiPolygonTrait, PointTrait, PolygonTrait, +}; +use geo_types::{Coord as GeoCoord, Line}; +use wkb::reader::{ + Coord, Dimension, GeometryCollection, LineString, LinearRing, MultiLineString, MultiPoint, + MultiPolygon, Point, Polygon, Wkb, +}; +use wkb::Endianness; + +// ┌──────────────────────────────────────────────────────────┐ +// │ Coord │ +// └──────────────────────────────────────────────────────────┘ +impl CoordTraitExt for Coord<'_> { + #[inline] + fn geo_coord(&self) -> geo_types::Coord { + let coord_slice = self.coord_slice(); + unsafe { + let x_bytes = std::slice::from_raw_parts(coord_slice.as_ptr(), 8); + let y_bytes = std::slice::from_raw_parts(coord_slice.as_ptr().add(8), 8); + match self.byte_order() { + Endianness::BigEndian => { + let x = BigEndian::read_f64(x_bytes); + let y = BigEndian::read_f64(y_bytes); + geo_types::Coord { x, y } + } + Endianness::LittleEndian => { + let x = LittleEndian::read_f64(x_bytes); + let y = LittleEndian::read_f64(y_bytes); + geo_types::Coord { x, y } + } + } + } + } +} + +impl GeoTraitExtWithTypeTag for Coord<'_> { + type Tag = CoordTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ Point │ +// └──────────────────────────────────────────────────────────┘ + +impl PointTraitExt for Point<'_> { + forward_point_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for Point<'_> { + type Tag = PointTag; +} + +impl PointTraitExt for &Point<'_> { + forward_point_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for &Point<'_> { + type Tag = PointTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ LineString │ +// └──────────────────────────────────────────────────────────┘ + +impl LineStringTraitExt for LineString<'_> { + forward_line_string_trait_ext_funcs!(); + + #[inline(always)] + fn lines(&'_ self) -> impl ExactSizeIterator> + '_ { + let buf = self.coords_slice(); + let dim_size = dimension_size(self.dimension()); + let num_coords = self.num_coords(); + match self.byte_order() { + Endianness::LittleEndian => { + EndianLineIter::LE(LineIter::new(buf, num_coords, dim_size)) + } + Endianness::BigEndian => EndianLineIter::BE(LineIter::new(buf, num_coords, dim_size)), + } + } + + #[inline(always)] + fn coord_iter(&self) -> impl Iterator> { + let buf = self.coords_slice(); + let dim_size = dimension_size(self.dimension()); + let num_coords = self.num_coords(); + match self.byte_order() { + Endianness::LittleEndian => { + EndianCoordIter::LE(CoordIter::new(buf, num_coords, dim_size)) + } + Endianness::BigEndian => EndianCoordIter::BE(CoordIter::new(buf, num_coords, dim_size)), + } + } +} + +impl GeoTraitExtWithTypeTag for LineString<'_> { + type Tag = LineStringTag; +} + +impl LineStringTraitExt for &LineString<'_> { + forward_line_string_trait_ext_funcs!(); + + #[inline(always)] + fn lines(&'_ self) -> impl ExactSizeIterator> + '_ { + (*self).lines() + } + + #[inline(always)] + fn coord_iter(&self) -> impl Iterator> { + (*self).coord_iter() + } +} + +impl GeoTraitExtWithTypeTag for &LineString<'_> { + type Tag = LineStringTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ LinearRing │ +// └──────────────────────────────────────────────────────────┘ + +impl LineStringTraitExt for LinearRing<'_> { + forward_line_string_trait_ext_funcs!(); + + #[inline(always)] + fn lines(&'_ self) -> impl ExactSizeIterator> + '_ { + let buf = self.coords_slice(); + let dim_size = dimension_size(self.dimension()); + let num_coords = self.num_coords(); + match self.byte_order() { + Endianness::LittleEndian => { + EndianLineIter::LE(LineIter::new(buf, num_coords, dim_size)) + } + Endianness::BigEndian => EndianLineIter::BE(LineIter::new(buf, num_coords, dim_size)), + } + } + + #[inline(always)] + fn coord_iter(&self) -> impl Iterator> { + let buf = self.coords_slice(); + let dim_size = dimension_size(self.dimension()); + let num_coords = self.num_coords(); + match self.byte_order() { + Endianness::LittleEndian => { + EndianCoordIter::LE(CoordIter::new(buf, num_coords, dim_size)) + } + Endianness::BigEndian => EndianCoordIter::BE(CoordIter::new(buf, num_coords, dim_size)), + } + } +} + +impl GeoTraitExtWithTypeTag for LinearRing<'_> { + type Tag = LineStringTag; +} + +impl LineStringTraitExt for &LinearRing<'_> { + forward_line_string_trait_ext_funcs!(); + + #[inline(always)] + fn lines(&'_ self) -> impl ExactSizeIterator> + '_ { + (*self).lines() + } + + #[inline(always)] + fn coord_iter(&self) -> impl Iterator> { + (*self).coord_iter() + } +} + +impl GeoTraitExtWithTypeTag for &LinearRing<'_> { + type Tag = LineStringTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ Polygon │ +// └──────────────────────────────────────────────────────────┘ + +impl PolygonTraitExt for Polygon<'_> { + forward_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for Polygon<'_> { + type Tag = PolygonTag; +} + +impl PolygonTraitExt for &Polygon<'_> { + forward_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for &Polygon<'_> { + type Tag = PolygonTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ MultiPoint │ +// └──────────────────────────────────────────────────────────┘ + +impl MultiPointTraitExt for MultiPoint<'_> { + forward_multi_point_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for MultiPoint<'_> { + type Tag = MultiPointTag; +} + +impl<'a, 'b> MultiPointTraitExt for &'b MultiPoint<'a> +where + 'a: 'b, +{ + forward_multi_point_trait_ext_funcs!(); +} + +impl<'a, 'b> GeoTraitExtWithTypeTag for &'b MultiPoint<'a> +where + 'a: 'b, +{ + type Tag = MultiPointTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ MultiLineString │ +// └──────────────────────────────────────────────────────────┘ + +impl MultiLineStringTraitExt for MultiLineString<'_> { + forward_multi_line_string_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for MultiLineString<'_> { + type Tag = MultiLineStringTag; +} + +impl<'a, 'b> MultiLineStringTraitExt for &'b MultiLineString<'a> +where + 'a: 'b, +{ + forward_multi_line_string_trait_ext_funcs!(); +} + +impl<'a, 'b> GeoTraitExtWithTypeTag for &'b MultiLineString<'a> +where + 'a: 'b, +{ + type Tag = MultiLineStringTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ MultiPolygon │ +// └──────────────────────────────────────────────────────────┘ + +impl MultiPolygonTraitExt for MultiPolygon<'_> { + forward_multi_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for MultiPolygon<'_> { + type Tag = MultiPolygonTag; +} + +impl<'a, 'b> MultiPolygonTraitExt for &'b MultiPolygon<'a> +where + 'a: 'b, +{ + forward_multi_polygon_trait_ext_funcs!(); +} + +impl<'a, 'b> GeoTraitExtWithTypeTag for &'b MultiPolygon<'a> +where + 'a: 'b, +{ + type Tag = MultiPolygonTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ GeometryCollection │ +// └──────────────────────────────────────────────────────────┘ + +impl GeometryCollectionTraitExt for GeometryCollection<'_> { + forward_geometry_collection_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for GeometryCollection<'_> { + type Tag = GeometryCollectionTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ Wkb/Geometry │ +// └──────────────────────────────────────────────────────────┘ + +impl<'a> GeometryTraitExt for Wkb<'a> { + forward_geometry_trait_ext_funcs!(f64); + + type InnerGeometryRef<'b> + = &'b Wkb<'a> + where + Self: 'b; + + #[inline] + fn geometry_ext(&self, i: usize) -> Option> { + let GeometryType::GeometryCollection(gc) = self.as_type() else { + return None; + }; + gc.geometry(i) + } + + #[inline] + unsafe fn geometry_unchecked_ext(&self, i: usize) -> Self::InnerGeometryRef<'_> { + let GeometryType::GeometryCollection(gc) = self.as_type() else { + panic!("Called geometry_unchecked_ext on a non-GeometryCollection geometry"); + }; + gc.geometry_unchecked(i) + } + + #[inline] + fn geometries_ext(&self) -> impl Iterator> { + let GeometryType::GeometryCollection(gc) = self.as_type() else { + panic!("Called geometries_ext on a non-GeometryCollection geometry"); + }; + gc.geometries() + } +} + +impl<'a, 'b> GeometryTraitExt for &'b Wkb<'a> +where + 'a: 'b, +{ + forward_geometry_trait_ext_funcs!(f64); + + type InnerGeometryRef<'c> + = &'b Wkb<'a> + where + Self: 'c; + + #[inline] + fn geometry_ext(&self, i: usize) -> Option> { + (*self).geometry_ext(i) + } + + #[inline] + unsafe fn geometry_unchecked_ext(&self, i: usize) -> Self::InnerGeometryRef<'_> { + (*self).geometry_unchecked_ext(i) + } + + #[inline] + fn geometries_ext(&self) -> impl Iterator> { + (*self).geometries_ext() + } +} + +impl GeoTraitExtWithTypeTag for Wkb<'_> { + type Tag = GeometryTag; +} + +impl<'a, 'b> GeoTraitExtWithTypeTag for &'b Wkb<'a> +where + 'a: 'b, +{ + type Tag = GeometryTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ Iterators │ +// └──────────────────────────────────────────────────────────┘ + +// Coordinate iterator with compile-time endianness +pub struct CoordIter<'a, B: ByteOrder> { + buf: &'a [u8], + current_offset: usize, + remaining: usize, + dim_size: usize, + _marker: PhantomData, +} + +impl<'a, B: ByteOrder> CoordIter<'a, B> { + #[inline] + pub fn new(buf: &'a [u8], num_coords: usize, dim_size: usize) -> Self { + Self { + buf, + current_offset: 0, + remaining: num_coords, + dim_size, + _marker: PhantomData, + } + } +} + +impl Iterator for CoordIter<'_, B> { + type Item = GeoCoord; + + #[inline] + fn next(&mut self) -> Option { + if self.remaining == 0 { + return None; + } + + // SAFETY: We're reading raw memory from the buffer at calculated offsets. + // This assumes the buffer contains valid data and offsets are within bounds. + let coord = unsafe { + let x_bytes = std::slice::from_raw_parts(self.buf.as_ptr().add(self.current_offset), 8); + let y_bytes = + std::slice::from_raw_parts(self.buf.as_ptr().add(self.current_offset + 8), 8); + let x = B::read_f64(x_bytes); + let y = B::read_f64(y_bytes); + GeoCoord { x, y } + }; + + self.current_offset += self.dim_size * 8; + self.remaining -= 1; + Some(coord) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) + } +} + +impl ExactSizeIterator for CoordIter<'_, B> {} + +// Line iterator with compile-time endianness +pub struct LineIter<'a, B: ByteOrder> { + coord_iter: CoordIter<'a, B>, + prev_coord: Option>, +} + +impl<'a, B: ByteOrder> LineIter<'a, B> { + #[inline] + pub fn new(buf: &'a [u8], num_coords: usize, dim_size: usize) -> Self { + Self { + coord_iter: CoordIter::new(buf, num_coords, dim_size), + prev_coord: None, + } + } +} + +impl Iterator for LineIter<'_, B> { + type Item = Line; + + #[inline] + fn next(&mut self) -> Option { + let current_coord = self.coord_iter.next()?; + + match self.prev_coord { + Some(prev_coord) => { + let line = Line::new(prev_coord, current_coord); + self.prev_coord = Some(current_coord); + Some(line) + } + None => { + // Grab the next coordinate to form the first line segment + let next_coord = self.coord_iter.next()?; + let line = Line::new(current_coord, next_coord); + self.prev_coord = Some(next_coord); + Some(line) + } + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let (min, max) = self.coord_iter.size_hint(); + (min.saturating_sub(1), max.map(|m| m.saturating_sub(1))) + } +} + +impl ExactSizeIterator for LineIter<'_, B> {} + +// Enum-based wrappers to handle the different endianness types without boxing. +// The dispatch in the iterator methods is static and can be inlined by the compiler. +pub enum EndianCoordIter<'a> { + LE(CoordIter<'a, LittleEndian>), + BE(CoordIter<'a, BigEndian>), +} + +impl Iterator for EndianCoordIter<'_> { + type Item = GeoCoord; + + #[inline] + fn next(&mut self) -> Option { + // We rely on compiler optimization to hoist the match out of the loop, so that + // there's no performance overhead of checking the endianness inside the loop. + match self { + EndianCoordIter::LE(iter) => iter.next(), + EndianCoordIter::BE(iter) => iter.next(), + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + match self { + EndianCoordIter::LE(iter) => iter.size_hint(), + EndianCoordIter::BE(iter) => iter.size_hint(), + } + } +} + +pub enum EndianLineIter<'a> { + LE(LineIter<'a, LittleEndian>), + BE(LineIter<'a, BigEndian>), +} + +impl Iterator for EndianLineIter<'_> { + type Item = Line; + + #[inline] + fn next(&mut self) -> Option { + match self { + EndianLineIter::LE(iter) => iter.next(), + EndianLineIter::BE(iter) => iter.next(), + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + match self { + EndianLineIter::LE(iter) => iter.size_hint(), + EndianLineIter::BE(iter) => iter.size_hint(), + } + } +} + +impl ExactSizeIterator for EndianLineIter<'_> {} + +// ┌──────────────────────────────────────────────────────────┐ +// │ Utils. │ +// └──────────────────────────────────────────────────────────┘ +fn dimension_size(dim: Dimension) -> usize { + match dim { + Dimension::Xy => 2, + Dimension::Xyz | Dimension::Xym => 3, + Dimension::Xyzm => 4, + } +} diff --git a/rust/sedona-geo/Cargo.toml b/rust/sedona-geo/Cargo.toml index f9c947a5..df439182 100644 --- a/rust/sedona-geo/Cargo.toml +++ b/rust/sedona-geo/Cargo.toml @@ -32,7 +32,7 @@ criterion = { workspace = true } rstest = { workspace = true } sedona-geometry = { path = "../sedona-geometry" } sedona-schema = { path = "../sedona-schema" } -sedona-testing = { path = "../sedona-testing", features = ["criterion"] } +sedona-testing = { path = "../sedona-testing", features = ["criterion", "geo"] } wkt = { workspace = true } [dependencies] @@ -40,7 +40,7 @@ arrow-schema = { workspace = true } arrow-array = { workspace = true } datafusion-common = { workspace = true } datafusion-expr = { workspace = true } -geo-generic-alg = { workspace = true } +sedona-geo-generic-alg = { path = "../sedona-geo-generic-alg" } geo-traits = { workspace = true, features = ["geo-types"] } geo-types = { workspace = true } geo = { workspace = true } diff --git a/rust/sedona-geo/src/centroid.rs b/rust/sedona-geo/src/centroid.rs index e7ab25f5..ee74df13 100644 --- a/rust/sedona-geo/src/centroid.rs +++ b/rust/sedona-geo/src/centroid.rs @@ -17,11 +17,11 @@ //! Centroid extraction functionality for WKB geometries use datafusion_common::{error::DataFusionError, Result}; -use geo_generic_alg::Centroid; -use geo_generic_alg::HasDimensions; use geo_traits::CoordTrait; use geo_traits::GeometryTrait; use geo_traits::PointTrait; +use sedona_geo_generic_alg::Centroid; +use sedona_geo_generic_alg::HasDimensions; use crate::to_geo::item_to_geometry; diff --git a/rust/sedona-geo/src/st_area.rs b/rust/sedona-geo/src/st_area.rs index 8a274d56..5efd5f07 100644 --- a/rust/sedona-geo/src/st_area.rs +++ b/rust/sedona-geo/src/st_area.rs @@ -20,9 +20,9 @@ use arrow_array::builder::Float64Builder; use arrow_schema::DataType; use datafusion_common::error::Result; use datafusion_expr::ColumnarValue; -use geo_generic_alg::Area; use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel}; use sedona_functions::executor::WkbExecutor; +use sedona_geo_generic_alg::Area; use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher}; use wkb::reader::Wkb; diff --git a/rust/sedona-geo/src/st_centroid.rs b/rust/sedona-geo/src/st_centroid.rs index 31c3dd70..bc6b8188 100644 --- a/rust/sedona-geo/src/st_centroid.rs +++ b/rust/sedona-geo/src/st_centroid.rs @@ -20,9 +20,9 @@ use std::sync::Arc; use arrow_array::builder::BinaryBuilder; use datafusion_common::{error::Result, exec_err}; use datafusion_expr::ColumnarValue; -use geo_generic_alg::Centroid; use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel}; use sedona_functions::executor::WkbExecutor; +use sedona_geo_generic_alg::Centroid; use sedona_geometry::is_empty::is_geometry_empty; use sedona_schema::{ datatypes::{SedonaType, WKB_GEOMETRY}, diff --git a/rust/sedona-geo/src/st_distance.rs b/rust/sedona-geo/src/st_distance.rs index 4900690a..e2f48a15 100644 --- a/rust/sedona-geo/src/st_distance.rs +++ b/rust/sedona-geo/src/st_distance.rs @@ -20,9 +20,9 @@ use arrow_array::builder::Float64Builder; use arrow_schema::DataType; use datafusion_common::error::Result; use datafusion_expr::ColumnarValue; -use geo_generic_alg::line_measures::DistanceExt; use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel}; use sedona_functions::executor::WkbExecutor; +use sedona_geo_generic_alg::line_measures::DistanceExt; use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher}; use wkb::reader::Wkb; diff --git a/rust/sedona-geo/src/st_dwithin.rs b/rust/sedona-geo/src/st_dwithin.rs index 2ba3f5de..25e8bf26 100644 --- a/rust/sedona-geo/src/st_dwithin.rs +++ b/rust/sedona-geo/src/st_dwithin.rs @@ -20,9 +20,9 @@ use arrow_array::builder::BooleanBuilder; use arrow_schema::DataType; use datafusion_common::{cast::as_float64_array, error::Result}; use datafusion_expr::ColumnarValue; -use geo_generic_alg::line_measures::DistanceExt; use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel}; use sedona_functions::executor::WkbExecutor; +use sedona_geo_generic_alg::line_measures::DistanceExt; use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher}; use wkb::reader::Wkb; diff --git a/rust/sedona-geo/src/st_intersection_aggr.rs b/rust/sedona-geo/src/st_intersection_aggr.rs index 446290f8..b9cad44b 100644 --- a/rust/sedona-geo/src/st_intersection_aggr.rs +++ b/rust/sedona-geo/src/st_intersection_aggr.rs @@ -31,9 +31,9 @@ use sedona_schema::{ datatypes::{SedonaType, WKB_GEOMETRY}, matchers::ArgMatcher, }; -use wkb::reader::Wkb; use wkb::writer::write_geometry; use wkb::Endianness; +use wkb::{reader::Wkb, writer::WriteOptions}; /// ST_Intersection_Aggr() implementation pub fn st_intersection_aggr_impl() -> SedonaAccumulatorRef { @@ -133,7 +133,13 @@ impl IntersectionAccumulator { fn geometry_to_wkb(&self, geom: &geo::Geometry) -> Option> { let mut wkb_bytes = Vec::new(); - match write_geometry(&mut wkb_bytes, geom, Endianness::LittleEndian) { + match write_geometry( + &mut wkb_bytes, + geom, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) { Ok(_) => Some(wkb_bytes), Err(_) => None, } @@ -223,7 +229,9 @@ mod test { use rstest::rstest; use sedona_functions::st_intersection_aggr::st_intersection_aggr_udf; use sedona_schema::datatypes::WKB_VIEW_GEOMETRY; - use sedona_testing::{compare::assert_scalar_equal_wkb_geometry, testers::AggregateUdfTester}; + use sedona_testing::{ + compare::assert_scalar_equal_wkb_geometry_topologically, testers::AggregateUdfTester, + }; #[rstest] fn polygon_polygon_cases(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType) { @@ -238,16 +246,19 @@ mod test { vec![Some("POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))")], vec![Some("POLYGON((1 1, 3 1, 3 3, 1 3, 1 1))")], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(batches).unwrap(), Some("MULTIPOLYGON(((1 1, 2 1, 2 2, 1 2, 1 1)))"), ); // Empty input - assert_scalar_equal_wkb_geometry(&tester.aggregate_wkt(vec![]).unwrap(), None); + assert_scalar_equal_wkb_geometry_topologically( + &tester.aggregate_wkt(vec![]).unwrap(), + None, + ); // Single polygon input - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester .aggregate_wkt(vec![vec![Some("POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))")]]) .unwrap(), @@ -259,14 +270,17 @@ mod test { vec![Some("POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))")], vec![Some("POLYGON((2 2, 3 2, 3 3, 2 3, 2 2))")], ]; - assert_scalar_equal_wkb_geometry(&tester.aggregate_wkt(non_intersecting).unwrap(), None); + assert_scalar_equal_wkb_geometry_topologically( + &tester.aggregate_wkt(non_intersecting).unwrap(), + None, + ); // Input with nulls let nulls_input = vec![ vec![Some("POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))"), None], vec![Some("POLYGON((1 1, 3 1, 3 3, 1 3, 1 1))"), None], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(nulls_input).unwrap(), Some("MULTIPOLYGON(((1 1, 2 1, 2 2, 1 2, 1 1)))"), ); @@ -276,7 +290,7 @@ mod test { vec![Some("POLYGON((0 0, 3 0, 3 3, 0 3, 0 0))")], vec![Some("POLYGON((1 1, 2 1, 2 2, 1 2, 1 1))")], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(contained).unwrap(), Some("MULTIPOLYGON(((1 1, 2 1, 2 2, 1 2, 1 1)))"), ); @@ -298,7 +312,7 @@ mod test { "MULTIPOLYGON(((1 1, 2 1, 2 2, 1 2, 1 1)), ((4 4, 5 4, 5 5, 4 5, 4 4)))", )], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(poly_and_multi).unwrap(), Some("MULTIPOLYGON(((1 1, 2 1, 2 2, 1 2, 1 1)))"), ); @@ -310,7 +324,7 @@ mod test { "MULTIPOLYGON(((2 2, 3 2, 3 3, 2 3, 2 2)), ((4 4, 5 4, 5 5, 4 5, 4 4)))", )], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(poly_and_nonoverlap_multi).unwrap(), None, ); @@ -324,7 +338,7 @@ mod test { "MULTIPOLYGON(((1 1, 2 1, 2 2, 1 2, 1 1)), ((11 11, 12 11, 12 12, 11 12, 11 11)))", )], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(multi_and_multi).unwrap(), Some("MULTIPOLYGON(((1 1,2 1,2 2,1 2,1 1)),((11 11,12 11,12 12,11 12,11 11)))"), ); @@ -348,7 +362,7 @@ mod test { "MULTIPOLYGON(((2 2, 5 2, 5 5, 2 5, 2 2)), ((9 9, 12 9, 12 12, 9 12, 9 9)))", )], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(multi_multi_case1).unwrap(), Some("MULTIPOLYGON(((2 2, 3 2, 3 3, 2 3, 2 2)))"), ); @@ -362,7 +376,10 @@ mod test { "MULTIPOLYGON(((2 2, 3 2, 3 3, 2 3, 2 2)), ((7 7, 8 7, 8 8, 7 8, 7 7)))", )], ]; - assert_scalar_equal_wkb_geometry(&tester.aggregate_wkt(multi_multi_case2).unwrap(), None); + assert_scalar_equal_wkb_geometry_topologically( + &tester.aggregate_wkt(multi_multi_case2).unwrap(), + None, + ); // Test case 3: Three MultiPolygons intersection let multi_multi_case3 = vec![ @@ -376,7 +393,7 @@ mod test { "MULTIPOLYGON(((3 3, 5 3, 5 5, 3 5, 3 3)), ((13 13, 15 13, 15 15, 13 15, 13 13)))", )], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(multi_multi_case3).unwrap(), Some("MULTIPOLYGON(((3 3,4 3,4 4,3 4,3 3)),((13 13,14 13,14 14,13 14,13 13)))"), ); diff --git a/rust/sedona-geo/src/st_intersects.rs b/rust/sedona-geo/src/st_intersects.rs index 9fac6ed8..8c624fd1 100644 --- a/rust/sedona-geo/src/st_intersects.rs +++ b/rust/sedona-geo/src/st_intersects.rs @@ -20,9 +20,9 @@ use arrow_array::builder::BooleanBuilder; use arrow_schema::DataType; use datafusion_common::error::Result; use datafusion_expr::ColumnarValue; -use geo_generic_alg::Intersects; use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel}; use sedona_functions::executor::WkbExecutor; +use sedona_geo_generic_alg::Intersects; use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher}; use wkb::reader::Wkb; diff --git a/rust/sedona-geo/src/st_length.rs b/rust/sedona-geo/src/st_length.rs index 0ab4461e..21b2be1d 100644 --- a/rust/sedona-geo/src/st_length.rs +++ b/rust/sedona-geo/src/st_length.rs @@ -21,9 +21,9 @@ use arrow_array::builder::Float64Builder; use arrow_schema::DataType; use datafusion_common::error::Result; use datafusion_expr::ColumnarValue; -use geo_generic_alg::algorithm::{line_measures::Euclidean, LengthMeasurableExt}; use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel}; use sedona_functions::executor::WkbExecutor; +use sedona_geo_generic_alg::algorithm::{line_measures::Euclidean, LengthMeasurableExt}; use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher}; use wkb::reader::Wkb; diff --git a/rust/sedona-geo/src/st_perimeter.rs b/rust/sedona-geo/src/st_perimeter.rs index ad5ffbca..11111b66 100644 --- a/rust/sedona-geo/src/st_perimeter.rs +++ b/rust/sedona-geo/src/st_perimeter.rs @@ -21,9 +21,9 @@ use arrow_array::builder::Float64Builder; use arrow_schema::DataType; use datafusion_common::error::Result; use datafusion_expr::ColumnarValue; -use geo_generic_alg::algorithm::{line_measures::Euclidean, LengthMeasurableExt}; use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel}; use sedona_functions::executor::WkbExecutor; +use sedona_geo_generic_alg::algorithm::{line_measures::Euclidean, LengthMeasurableExt}; use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher}; use wkb::reader::Wkb; diff --git a/rust/sedona-geo/src/st_union_aggr.rs b/rust/sedona-geo/src/st_union_aggr.rs index 1260de58..2462e48c 100644 --- a/rust/sedona-geo/src/st_union_aggr.rs +++ b/rust/sedona-geo/src/st_union_aggr.rs @@ -31,9 +31,9 @@ use sedona_schema::{ datatypes::{SedonaType, WKB_GEOMETRY}, matchers::ArgMatcher, }; -use wkb::reader::Wkb; use wkb::writer::write_geometry; use wkb::Endianness; +use wkb::{reader::Wkb, writer::WriteOptions}; /// ST_Union_Aggr() implementation pub fn st_union_aggr_impl() -> SedonaAccumulatorRef { @@ -127,7 +127,13 @@ impl UnionAccumulator { fn geometry_to_wkb(&self, geom: &geo::Geometry) -> Option> { let mut wkb_bytes = Vec::new(); - match write_geometry(&mut wkb_bytes, geom, Endianness::LittleEndian) { + match write_geometry( + &mut wkb_bytes, + geom, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) { Ok(_) => Some(wkb_bytes), Err(_) => None, } @@ -217,7 +223,9 @@ mod test { use rstest::rstest; use sedona_functions::st_union_aggr::st_union_aggr_udf; use sedona_schema::datatypes::WKB_VIEW_GEOMETRY; - use sedona_testing::{compare::assert_scalar_equal_wkb_geometry, testers::AggregateUdfTester}; + use sedona_testing::{ + compare::assert_scalar_equal_wkb_geometry_topologically, testers::AggregateUdfTester, + }; #[rstest] fn polygon_polygon_cases(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType) { @@ -233,16 +241,16 @@ mod test { vec![Some("POLYGON((1 1, 3 1, 3 3, 1 3, 1 1))")], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(batches).unwrap(), Some("MULTIPOLYGON(((0 0, 2 0, 2 1, 3 1, 3 3, 1 3, 1 2, 0 2, 0 0)))"), ); // Empty input - assert_scalar_equal_wkb_geometry(&tester.aggregate(&vec![]).unwrap(), None); + assert_scalar_equal_wkb_geometry_topologically(&tester.aggregate(&vec![]).unwrap(), None); // Single polygon input - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester .aggregate_wkt(vec![vec![Some("POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))")]]) .unwrap(), @@ -254,7 +262,7 @@ mod test { vec![Some("POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))")], vec![Some("POLYGON((2 2, 3 2, 3 3, 2 3, 2 2))")], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(non_intersecting).unwrap(), Some("MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0)),((2 2, 3 2, 3 3, 2 3, 2 2)))"), ); @@ -264,7 +272,7 @@ mod test { vec![Some("POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))"), None], vec![Some("POLYGON((1 1, 3 1, 3 3, 1 3, 1 1))"), None], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(nulls_input).unwrap(), Some("MULTIPOLYGON(((0 0, 2 0, 2 1, 3 1, 3 3, 1 3, 1 2, 0 2, 0 0)))"), ); @@ -274,7 +282,7 @@ mod test { vec![Some("POLYGON((0 0, 3 0, 3 3, 0 3, 0 0))")], vec![Some("POLYGON((1 1, 2 1, 2 2, 1 2, 1 1))")], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(contained).unwrap(), Some("MULTIPOLYGON(((0 0, 3 0, 3 3, 0 3, 0 0)))"), ); @@ -296,7 +304,7 @@ mod test { "MULTIPOLYGON(((1 1, 2 1, 2 2, 1 2, 1 1)), ((4 4, 5 4, 5 5, 4 5, 4 4)))", )], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(poly_and_multi).unwrap(), Some("MULTIPOLYGON(((0 0, 3 0, 3 3, 0 3, 0 0)),((4 4, 5 4, 5 5, 4 5, 4 4)))"), ); @@ -308,7 +316,7 @@ mod test { "MULTIPOLYGON(((2 2, 3 2, 3 3, 2 3, 2 2)), ((4 4, 5 4, 5 5, 4 5, 4 4)))", )], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(poly_and_nonoverlap_multi).unwrap(), Some("MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0)),((2 2, 3 2, 3 3, 2 3, 2 2)),((4 4, 5 4, 5 5, 4 5, 4 4)))"), ); @@ -322,7 +330,7 @@ mod test { "MULTIPOLYGON(((1 1, 2 1, 2 2, 1 2, 1 1)), ((11 11, 13 11, 13 13, 11 13, 11 11)))", )], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(multi_and_multi).unwrap(), Some("MULTIPOLYGON(((0 0, 3 0, 3 3, 0 3, 0 0)),((10 10, 12 10, 12 11, 13 11, 13 13, 11 13, 11 12, 10 12, 10 10)))"), ); @@ -346,7 +354,7 @@ mod test { "MULTIPOLYGON(((2 2, 5 2, 5 5, 2 5, 2 2)), ((7 7, 10 7, 10 10, 7 10, 7 7)))", )], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(multi_multi_case1).unwrap(), Some("MULTIPOLYGON(((0 0, 3 0, 3 2, 5 2, 5 5, 2 5, 2 3, 0 3, 0 0)),((5 5, 8 5, 8 7, 10 7, 10 10, 7 10, 7 8, 5 8, 5 5)))"), ); @@ -360,7 +368,7 @@ mod test { "MULTIPOLYGON(((2 2, 3 2, 3 3, 2 3, 2 2)), ((7 7, 8 7, 8 8, 7 8, 7 7)))", )], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(multi_multi_case2).unwrap(), Some("MULTIPOLYGON(((0 0,1 0,1 1,0 1,0 0)),((2 2,3 2,3 3,2 3,2 2)),((5 5,6 5,6 6,5 6,5 5)),((7 7,8 7,8 8,7 8,7 7)))"), ); @@ -371,7 +379,7 @@ mod test { vec![Some("MULTIPOLYGON(((3 3, 7 3, 7 7, 3 7, 3 3)), ((13 13, 17 13, 17 17, 13 17, 13 13)))")], vec![Some("MULTIPOLYGON(((6 6, 10 6, 10 10, 6 10, 6 6)), ((16 16, 20 16, 20 20, 16 20, 16 16)))")], ]; - assert_scalar_equal_wkb_geometry( + assert_scalar_equal_wkb_geometry_topologically( &tester.aggregate_wkt(multi_multi_case3).unwrap(), Some("MULTIPOLYGON(((0 0, 4 0, 4 3, 7 3, 7 6, 10 6, 10 10, 6 10, 6 7, 3 7, 3 4, 0 4, 0 0)),((10 10, 14 10, 14 13, 17 13, 17 16, 20 16, 20 20, 16 20, 16 17, 13 17, 13 14, 10 14, 10 10)))"), ); diff --git a/rust/sedona-geo/src/to_geo.rs b/rust/sedona-geo/src/to_geo.rs index 56a92178..a4401125 100644 --- a/rust/sedona-geo/src/to_geo.rs +++ b/rust/sedona-geo/src/to_geo.rs @@ -69,8 +69,10 @@ pub fn item_to_geometry(geo: impl GeometryTrait) -> Result { } // GeometryCollection causes issues because it has a recursive definition and won't work -// with cargo run --release. Thus, we need our own version of this that limits the -// recursion supported in a GeometryCollection. +// with cargo run --release. Thus, we need our own version of this that works around this +// problem by processing GeometryCollection using a free function instead of relying +// on trait resolver. +// See also https://github.com/geoarrow/geoarrow-rs/pull/956. fn to_geometry(item: impl GeometryTrait) -> Option { match item.as_type() { Point(geom) => geom.try_to_point().map(Geometry::Point), @@ -79,35 +81,38 @@ fn to_geometry(item: impl GeometryTrait) -> Option { MultiPoint(geom) => geom.try_to_multi_point().map(Geometry::MultiPoint), MultiLineString(geom) => Some(Geometry::MultiLineString(geom.to_multi_line_string())), MultiPolygon(geom) => Some(Geometry::MultiPolygon(geom.to_multi_polygon())), - GeometryCollection(geom) => { - let geometries = geom - .geometries() - .filter_map(|child| match child.as_type() { - Point(geom) => geom.try_to_point().map(Geometry::Point), - LineString(geom) => Some(Geometry::LineString(geom.to_line_string())), - Polygon(geom) => Some(Geometry::Polygon(geom.to_polygon())), - MultiPoint(geom) => geom.try_to_multi_point().map(Geometry::MultiPoint), - MultiLineString(geom) => { - Some(Geometry::MultiLineString(geom.to_multi_line_string())) - } - MultiPolygon(geom) => Some(Geometry::MultiPolygon(geom.to_multi_polygon())), - _ => None, - }) - .collect::>(); - - // If any child conversions failed, also return None - if geometries.len() != geom.num_geometries() { - return None; - } - - Some(Geometry::GeometryCollection(geo_types::GeometryCollection( - geometries, - ))) - } + GeometryCollection(geom) => geometry_collection_to_geometry(geom), _ => None, } } +fn geometry_collection_to_geometry>( + geom: &GC, +) -> Option { + let geometries = geom + .geometries() + .filter_map(|child| match child.as_type() { + Point(geom) => geom.try_to_point().map(Geometry::Point), + LineString(geom) => Some(Geometry::LineString(geom.to_line_string())), + Polygon(geom) => Some(Geometry::Polygon(geom.to_polygon())), + MultiPoint(geom) => geom.try_to_multi_point().map(Geometry::MultiPoint), + MultiLineString(geom) => Some(Geometry::MultiLineString(geom.to_multi_line_string())), + MultiPolygon(geom) => Some(Geometry::MultiPolygon(geom.to_multi_polygon())), + GeometryCollection(geom) => geometry_collection_to_geometry(geom), + _ => None, + }) + .collect::>(); + + // If any child conversions failed, also return None + if geometries.len() != geom.num_geometries() { + return None; + } + + Some(Geometry::GeometryCollection(geo_types::GeometryCollection( + geometries, + ))) +} + #[cfg(test)] mod tests { use datafusion_expr::ColumnarValue; @@ -126,11 +131,6 @@ mod tests { let err = item_to_geometry(unsupported).unwrap_err(); assert!(err.message().starts_with("geo kernel implementation")); - let unsupported = - Wkt::from_str("GEOMETRYCOLLECTION (GEOMETRYCOLLECTION(POINT (1 2)))").unwrap(); - let err = item_to_geometry(unsupported).unwrap_err(); - assert!(err.message().starts_with("geo kernel implementation")); - let unsupported = Wkt::from_str("GEOMETRYCOLLECTION (POINT EMPTY)").unwrap(); let err = item_to_geometry(unsupported).unwrap_err(); assert!(err.message().starts_with("geo kernel implementation")); @@ -145,7 +145,8 @@ mod tests { "MULTIPOINT (1 2, 3 4)", "MULTILINESTRING ((1 2, 3 4))", "MULTIPOLYGON (((0 0, 1 0, 0 1, 0 0)))", - "GEOMETRYCOLLECTION(POINT (1 2))" + "GEOMETRYCOLLECTION(POINT (1 2))", + "GEOMETRYCOLLECTION (GEOMETRYCOLLECTION(POINT (1 2)))" )] wkt_value: &str, ) { @@ -163,6 +164,7 @@ mod tests { Some("MULTILINESTRING ((1 2, 3 4))"), Some("MULTIPOLYGON (((0 0, 1 0, 0 1, 0 0)))"), Some("GEOMETRYCOLLECTION(POINT (1 2))"), + Some("GEOMETRYCOLLECTION (GEOMETRYCOLLECTION(POINT (1 2)))"), None, ]; let args = vec![ColumnarValue::Array(create_array_storage( diff --git a/rust/sedona-geometry/src/bounds.rs b/rust/sedona-geometry/src/bounds.rs index 0f50040b..e918d448 100644 --- a/rust/sedona-geometry/src/bounds.rs +++ b/rust/sedona-geometry/src/bounds.rs @@ -223,6 +223,7 @@ mod test { use super::*; use rstest::rstest; use std::{iter::zip, str::FromStr}; + use wkb::{writer::WriteOptions, Endianness}; use wkt::Wkt; pub fn wkt_bounds_xy(wkt_value: &str) -> Result { @@ -441,7 +442,14 @@ mod test { fn test_wkb_bounds_xy() { let wkt: Wkt = Wkt::from_str("POINT (0 1)").unwrap(); let mut out = Vec::new(); - wkb::writer::write_geometry(&mut out, &wkt, wkb::Endianness::LittleEndian).unwrap(); + wkb::writer::write_geometry( + &mut out, + &wkt, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); assert_eq!( wkb_bounds_xy(&out).unwrap(), BoundingBox::xy((0, 0), (1, 1)) diff --git a/rust/sedona-geometry/src/is_empty.rs b/rust/sedona-geometry/src/is_empty.rs index ad3f7d50..444b3d9f 100644 --- a/rust/sedona-geometry/src/is_empty.rs +++ b/rust/sedona-geometry/src/is_empty.rs @@ -51,13 +51,21 @@ mod tests { use super::*; use std::str::FromStr; use wkb::reader::read_wkb; - use wkb::writer::write_geometry; + use wkb::writer::{write_geometry, WriteOptions}; + use wkb::Endianness; use wkt::Wkt; fn create_wkb_bytes_from_wkt(wkt_str: &str) -> Vec { let wkt: Wkt = Wkt::from_str(wkt_str).unwrap(); let mut wkb_bytes = vec![]; - write_geometry(&mut wkb_bytes, &wkt, wkb::Endianness::LittleEndian).unwrap(); + write_geometry( + &mut wkb_bytes, + &wkt, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); wkb_bytes } diff --git a/rust/sedona-geometry/src/wkb_factory.rs b/rust/sedona-geometry/src/wkb_factory.rs index efa9f09f..000788f0 100644 --- a/rust/sedona-geometry/src/wkb_factory.rs +++ b/rust/sedona-geometry/src/wkb_factory.rs @@ -469,7 +469,8 @@ fn count_to_u32(count: usize) -> Result { mod test { use std::str::FromStr; use wkb::reader::read_wkb; - use wkb::writer::write_geometry; + use wkb::writer::{write_geometry, WriteOptions}; + use wkb::Endianness; use wkt::Wkt; use super::*; @@ -478,7 +479,14 @@ mod test { fn test_wkb_point() { let wkt: Wkt = Wkt::from_str("POINT (0 1)").unwrap(); let mut wkb = vec![]; - write_geometry(&mut wkb, &wkt, wkb::Endianness::LittleEndian).unwrap(); + write_geometry( + &mut wkb, + &wkt, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); assert_eq!(wkb_point((0.0, 1.0)).unwrap(), wkb); } @@ -533,12 +541,26 @@ mod test { fn test_wkb_linestring() { let wkt: Wkt = Wkt::from_str("LINESTRING EMPTY").unwrap(); let mut wkb = vec![]; - write_geometry(&mut wkb, &wkt, wkb::Endianness::LittleEndian).unwrap(); + write_geometry( + &mut wkb, + &wkt, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); assert_eq!(wkb_linestring([].into_iter()).unwrap(), wkb); let wkt: Wkt = Wkt::from_str("LINESTRING (0 1, 2 3)").unwrap(); let mut wkb = vec![]; - write_geometry(&mut wkb, &wkt, wkb::Endianness::LittleEndian).unwrap(); + write_geometry( + &mut wkb, + &wkt, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); assert_eq!( wkb_linestring([(0.0, 1.0), (2.0, 3.0)].into_iter()).unwrap(), wkb @@ -584,12 +606,26 @@ mod test { fn test_wkb_multilinestring() { let wkt: Wkt = Wkt::from_str("MULTILINESTRING EMPTY").unwrap(); let mut wkb = vec![]; - write_geometry(&mut wkb, &wkt, wkb::Endianness::LittleEndian).unwrap(); + write_geometry( + &mut wkb, + &wkt, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); assert_eq!(wkb_multilinestring([].into_iter()).unwrap(), wkb); let wkt: Wkt = Wkt::from_str("MULTILINESTRING ((0 0, 1 1, 2 2), (3 3, 4 4))").unwrap(); let mut wkb = vec![]; - write_geometry(&mut wkb, &wkt, wkb::Endianness::LittleEndian).unwrap(); + write_geometry( + &mut wkb, + &wkt, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); let linestrings = vec![ vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)], @@ -603,12 +639,26 @@ mod test { fn test_wkb_polygon() { let wkt: Wkt = Wkt::from_str("POLYGON EMPTY").unwrap(); let mut wkb = vec![]; - write_geometry(&mut wkb, &wkt, wkb::Endianness::LittleEndian).unwrap(); + write_geometry( + &mut wkb, + &wkt, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); assert_eq!(wkb_polygon([].into_iter()).unwrap(), wkb); let wkt: Wkt = Wkt::from_str("POLYGON ((0 0, 1 0, 0 1, 0 0))").unwrap(); let mut wkb = vec![]; - write_geometry(&mut wkb, &wkt, wkb::Endianness::LittleEndian).unwrap(); + write_geometry( + &mut wkb, + &wkt, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); assert_eq!( wkb_polygon([(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0)].into_iter()).unwrap(), wkb @@ -697,13 +747,27 @@ mod test { fn test_wkb_multipolygon() { let wkt: Wkt = Wkt::from_str("MULTIPOLYGON EMPTY").unwrap(); let mut wkb = vec![]; - write_geometry(&mut wkb, &wkt, wkb::Endianness::LittleEndian).unwrap(); + write_geometry( + &mut wkb, + &wkt, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); assert_eq!(wkb_multipolygon([].into_iter()).unwrap(), wkb); let wkt: Wkt = Wkt::from_str("MULTIPOLYGON (((0 0, 1 0, 0 1, 0 0)), ((2 2, 3 2, 2 3, 2 2)))").unwrap(); let mut wkb = vec![]; - write_geometry(&mut wkb, &wkt, wkb::Endianness::LittleEndian).unwrap(); + write_geometry( + &mut wkb, + &wkt, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); let polygons = vec![ vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0)], @@ -717,12 +781,26 @@ mod test { fn test_wkb_multipoint() { let wkt: Wkt = Wkt::from_str("MULTIPOINT EMPTY").unwrap(); let mut wkb = vec![]; - write_geometry(&mut wkb, &wkt, wkb::Endianness::LittleEndian).unwrap(); + write_geometry( + &mut wkb, + &wkt, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); assert_eq!(wkb_multipoint([].into_iter()).unwrap(), wkb); let wkt: Wkt = Wkt::from_str("MULTIPOINT ((0 0), (1 1))").unwrap(); let mut wkb = vec![]; - write_geometry(&mut wkb, &wkt, wkb::Endianness::LittleEndian).unwrap(); + write_geometry( + &mut wkb, + &wkt, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); let points = vec![(0.0, 0.0), (1.0, 1.0)]; assert_eq!(wkb_multipoint(points.into_iter()).unwrap(), wkb); diff --git a/rust/sedona-spatial-join/Cargo.toml b/rust/sedona-spatial-join/Cargo.toml index 0b2029f2..ee45190d 100644 --- a/rust/sedona-spatial-join/Cargo.toml +++ b/rust/sedona-spatial-join/Cargo.toml @@ -43,9 +43,10 @@ datafusion-execution = { workspace = true } datafusion-common-runtime = { workspace = true } futures = { workspace = true } parking_lot = { workspace = true } -geo-generic-alg = { workspace = true } +geo = { workspace = true } +sedona-geo-generic-alg = { path = "../sedona-geo-generic-alg" } geo-traits = { workspace = true, features = ["geo-types"] } -geo-traits-ext = { workspace = true } +sedona-geo-traits-ext = { path = "../sedona-geo-traits-ext" } geo-types = { workspace = true } sedona-common = { path = "../sedona-common" } sedona-expr = { path = "../sedona-expr" } @@ -54,6 +55,7 @@ sedona-geo = { path = "../sedona-geo" } sedona-geometry = { path = "../sedona-geometry" } sedona-schema = { path = "../sedona-schema" } sedona-tg = { path = "../../c/sedona-tg" } +sedona-geos = { path = "../../c/sedona-geos" } wkb = { workspace = true } geo-index = { workspace = true } geos = { workspace = true } @@ -65,5 +67,4 @@ rstest = { workspace = true } sedona-testing = { path = "../sedona-testing" } wkt = { workspace = true } tokio = { workspace = true, features = ["macros"] } -sedona-geos = { path = "../../c/sedona-geos" } rand = { workspace = true } diff --git a/rust/sedona-spatial-join/src/index.rs b/rust/sedona-spatial-join/src/index.rs index 59335f9c..d0f422a3 100644 --- a/rust/sedona-spatial-join/src/index.rs +++ b/rust/sedona-spatial-join/src/index.rs @@ -624,7 +624,7 @@ impl SpatialIndex { let max_distance = distances_with_indices[k_idx].0; // For tie-breakers, create spatial envelope around probe centroid and use rtree.search() - use geo_generic_alg::algorithm::Centroid; + use sedona_geo_generic_alg::algorithm::Centroid; let probe_centroid = probe_geom.centroid().unwrap_or(Point::new(0.0, 0.0)); let probe_x = probe_centroid.x() as f32; let probe_y = probe_centroid.y() as f32; diff --git a/rust/sedona-spatial-join/src/operand_evaluator.rs b/rust/sedona-spatial-join/src/operand_evaluator.rs index 56dca647..114d4309 100644 --- a/rust/sedona-spatial-join/src/operand_evaluator.rs +++ b/rust/sedona-spatial-join/src/operand_evaluator.rs @@ -25,10 +25,10 @@ use datafusion_common::{ use datafusion_expr::ColumnarValue; use datafusion_physical_expr::PhysicalExpr; use float_next_after::NextAfter; -use geo_generic_alg::BoundingRect; use geo_index::rtree::util::f64_box_to_f32; use geo_types::{coord, Rect}; use sedona_functions::executor::IterGeo; +use sedona_geo_generic_alg::BoundingRect; use sedona_schema::datatypes::SedonaType; use wkb::reader::Wkb; diff --git a/rust/sedona-spatial-join/src/refine/geo.rs b/rust/sedona-spatial-join/src/refine/geo.rs index 3b555e74..5d13b5e4 100644 --- a/rust/sedona-spatial-join/src/refine/geo.rs +++ b/rust/sedona-spatial-join/src/refine/geo.rs @@ -17,12 +17,11 @@ use std::sync::{Arc, OnceLock}; use datafusion_common::Result; -use geo_generic_alg::{ - line_measures::DistanceExt, Contains, Distance, Euclidean, Intersects, Relate, Within, -}; +use geo::{Contains, Relate, Within}; use sedona_common::{sedona_internal_err, ExecutionMode, SpatialJoinOptions}; use sedona_expr::statistics::GeoStatistics; use sedona_geo::to_geo::item_to_geometry; +use sedona_geo_generic_alg::{line_measures::DistanceExt, Intersects}; use wkb::reader::Wkb; use crate::{ @@ -136,7 +135,7 @@ impl GeoRefiner { Ok(geom) => geom, Err(_) => return Ok(Vec::new()), }; - let probe_geom = geo_generic_alg::PreparedGeometry::from(probe_geom); + let probe_geom = geo::PreparedGeometry::from(probe_geom); for index_result in index_query_results { if self.evaluator.evaluate_prepare_probe( @@ -204,7 +203,7 @@ trait GeoPredicateEvaluator: Send + Sync { fn evaluate_prepare_probe( &self, build: &Wkb, - probe: &geo_generic_alg::PreparedGeometry<'static, geo_types::Geometry>, + probe: &geo::PreparedGeometry<'static, geo_types::Geometry>, distance: Option, ) -> Result; } @@ -237,7 +236,7 @@ impl GeoPredicateEvaluator for GeoIntersects { fn evaluate_prepare_probe( &self, build: &Wkb, - probe: &geo_generic_alg::PreparedGeometry<'static, geo_types::Geometry>, + probe: &geo::PreparedGeometry<'static, geo_types::Geometry>, _distance: Option, ) -> Result { let build_geom = match item_to_geometry(build) { @@ -266,7 +265,7 @@ impl GeoPredicateEvaluator for GeoContains { fn evaluate_prepare_probe( &self, build: &Wkb, - probe: &geo_generic_alg::PreparedGeometry<'static, geo_types::Geometry>, + probe: &geo::PreparedGeometry<'static, geo_types::Geometry>, _distance: Option, ) -> Result { let build_geom = match item_to_geometry(build) { @@ -295,7 +294,7 @@ impl GeoPredicateEvaluator for GeoWithin { fn evaluate_prepare_probe( &self, build: &Wkb, - probe: &geo_generic_alg::PreparedGeometry<'static, geo_types::Geometry>, + probe: &geo::PreparedGeometry<'static, geo_types::Geometry>, _distance: Option, ) -> Result { let build_geom = match item_to_geometry(build) { @@ -320,18 +319,13 @@ impl GeoPredicateEvaluator for GeoDistance { fn evaluate_prepare_probe( &self, build: &Wkb, - probe: &geo_generic_alg::PreparedGeometry<'static, geo_types::Geometry>, + probe: &geo::PreparedGeometry<'static, geo_types::Geometry>, distance: Option, ) -> Result { let Some(distance) = distance else { return Ok(false); }; - let build_geom = match item_to_geometry(build) { - Ok(geom) => geom, - Err(_) => return Ok(false), - }; - let euc = Euclidean; - let dist = euc.distance(&build_geom, probe.geometry()); + let dist = build.distance_ext(probe.geometry()); Ok(dist <= distance) } } @@ -358,7 +352,7 @@ macro_rules! impl_relate_evaluator { fn evaluate_prepare_probe( &self, build: &Wkb, - probe: &geo_generic_alg::PreparedGeometry<'static, geo_types::Geometry>, + probe: &geo::PreparedGeometry<'static, geo_types::Geometry>, _distance: Option, ) -> Result { let build_geom = match item_to_geometry(build) { diff --git a/rust/sedona-spatial-join/src/refine/geos.rs b/rust/sedona-spatial-join/src/refine/geos.rs index b6570b70..9b4bc298 100644 --- a/rust/sedona-spatial-join/src/refine/geos.rs +++ b/rust/sedona-spatial-join/src/refine/geos.rs @@ -24,7 +24,8 @@ use geos::{Geom, PreparedGeometry}; use parking_lot::Mutex; use sedona_common::{sedona_internal_err, ExecutionMode, SpatialJoinOptions}; use sedona_expr::statistics::GeoStatistics; -use wkb::reader::{to_geos::GEOSWkbFactory, Wkb}; +use sedona_geos::wkb_to_geos::GEOSWkbFactory; +use wkb::reader::Wkb; use crate::{ index::IndexQueryResult, diff --git a/rust/sedona-testing/Cargo.toml b/rust/sedona-testing/Cargo.toml index c7e07836..d0c058fc 100644 --- a/rust/sedona-testing/Cargo.toml +++ b/rust/sedona-testing/Cargo.toml @@ -32,6 +32,7 @@ rstest = { workspace = true } [features] default = [] +geo = ["dep:geo"] criterion = ["dep:criterion"] [dependencies] @@ -42,7 +43,7 @@ criterion = { workspace = true, optional = true } datafusion-common = { workspace = true } datafusion-expr = { workspace = true } datafusion-physical-expr = { workspace = true } -geo-traits = { workspace = true } +geo-traits = { workspace = true, features = ["geo-types"] } geo-types = { workspace = true } parquet = { workspace = true, features = ["arrow", "snap", "zstd"] } rand = { workspace = true } @@ -52,3 +53,4 @@ sedona-expr = { path = "../sedona-expr" } sedona-schema = { path = "../sedona-schema" } wkb = { workspace = true } wkt = { workspace = true } +geo = { workspace = true, optional = true } diff --git a/rust/sedona-testing/src/compare.rs b/rust/sedona-testing/src/compare.rs index dac27896..780b2ff9 100644 --- a/rust/sedona-testing/src/compare.rs +++ b/rust/sedona-testing/src/compare.rs @@ -104,7 +104,25 @@ pub fn assert_array_equal(actual: &ArrayRef, expected: &ArrayRef) { /// Panics if the values' are not equal, generating reasonable failure messages for geometry /// arrays where the default failure message would otherwise be uninformative. pub fn assert_scalar_equal_wkb_geometry(actual: &ScalarValue, expected_wkt: Option<&str>) { - assert_scalar_equal(actual, &create_scalar(expected_wkt, &WKB_GEOMETRY)); + let expected = create_scalar(expected_wkt, &WKB_GEOMETRY); + assert_eq!(actual.data_type(), DataType::Binary); + assert_wkb_scalar_equal(actual, &expected, false); +} + +/// Assert a [`ScalarValue`] is a WKB_GEOMETRY scalar corresponding to the given WKT. This function +/// compares the geometries topologically, so two geometries that are not byte-wise equal but are +/// topologically equal will be considered equal. +/// +/// Panics if the values' are not topologically equal, generating reasonable failure messages for geometry +/// arrays where the default failure message would otherwise be uninformative. +#[cfg(feature = "geo")] +pub fn assert_scalar_equal_wkb_geometry_topologically( + actual: &ScalarValue, + expected_wkt: Option<&str>, +) { + let expected = create_scalar(expected_wkt, &WKB_GEOMETRY); + assert_eq!(actual.data_type(), DataType::Binary); + assert_wkb_scalar_equal(actual, &expected, true); } /// Assert two [`ScalarValue`]s are equal @@ -123,7 +141,7 @@ pub fn assert_scalar_equal(actual: &ScalarValue, expected: &ScalarValue) { (SedonaType::Arrow(_), SedonaType::Arrow(_)) => assert_arrow_scalar_equal(actual, expected), (SedonaType::Wkb(_, _), SedonaType::Wkb(_, _)) | (SedonaType::WkbView(_, _), SedonaType::WkbView(_, _)) => { - assert_wkb_scalar_equal(actual, expected); + assert_wkb_scalar_equal(actual, expected, false); } (_, _) => unreachable!(), } @@ -160,11 +178,21 @@ where for (i, (actual_item, expected_item)) in zip(actual, expected).enumerate() { let actual_label = format!("actual Array element #{i}"); let expected_label = format!("expected Array element #{i}"); - assert_wkb_value_equal(actual_item, expected_item, &actual_label, &expected_label); + assert_wkb_value_equal( + actual_item, + expected_item, + &actual_label, + &expected_label, + false, + ); } } -fn assert_wkb_scalar_equal(actual: &ScalarValue, expected: &ScalarValue) { +fn assert_wkb_scalar_equal( + actual: &ScalarValue, + expected: &ScalarValue, + compare_topologically: bool, +) { match (actual, expected) { (ScalarValue::Binary(maybe_actual_wkb), ScalarValue::Binary(maybe_expected_wkb)) | ( @@ -176,6 +204,7 @@ fn assert_wkb_scalar_equal(actual: &ScalarValue, expected: &ScalarValue) { maybe_expected_wkb.as_deref(), "actual WKB scalar", "expected WKB scalar", + compare_topologically, ); } (_, _) => { @@ -189,6 +218,7 @@ fn assert_wkb_value_equal( expected: Option<&[u8]>, actual_label: &str, expected_label: &str, + compare_topologically: bool, ) { match (actual, expected) { (None, None) => {} @@ -205,12 +235,56 @@ fn assert_wkb_value_equal( ) } (Some(actual_wkb), Some(expected_wkb)) => { + // Quick test: if the binary of the WKB is the same, they are equal if actual_wkb != expected_wkb { - let (actual_wkt, expected_wkt) = (format_wkb(actual_wkb), format_wkb(expected_wkb)); - panic!("{actual_label} != {expected_label}\n{actual_label}:\n {actual_wkt}\n{expected_label}:\n {expected_wkt}") + let is_equals = if compare_topologically { + compare_wkb_topologically(expected_wkb, actual_wkb) + } else { + false + }; + + if !is_equals { + let (actual_wkt, expected_wkt) = + (format_wkb(actual_wkb), format_wkb(expected_wkb)); + panic!("{actual_label} != {expected_label}\n{actual_label}:\n {actual_wkt}\n{expected_label}:\n {expected_wkt}") + } + } + } + } +} + +fn compare_wkb_topologically( + #[allow(unused)] expected_wkb: &[u8], + #[allow(unused)] actual_wkb: &[u8], +) -> bool { + #[cfg(feature = "geo")] + { + use geo::Relate; + use geo_traits::to_geo::ToGeoGeometry; + use geo_traits::Dimensions; + use geo_traits::GeometryTrait; + + let expected = wkb::reader::read_wkb(expected_wkb); + let actual = wkb::reader::read_wkb(actual_wkb); + match (expected, actual) { + (Ok(expected_geom), Ok(actual_geom)) => { + if expected_geom.dim() == Dimensions::Xy && actual_geom.dim() == Dimensions::Xy { + let expected_geom = expected_geom.to_geometry(); + let actual_geom = actual_geom.to_geometry(); + expected_geom.relate(&actual_geom).is_equal_topo() + } else { + // geo crate does not support 3D/4D geometry operations, so we fall back to using the result + // of byte-wise comparison + false + } } + _ => false, } } + #[cfg(not(feature = "geo"))] + { + panic!("Topological comparison requires the 'geo' feature to be enabled"); + } } fn format_wkb(value: &[u8]) -> String { @@ -357,20 +431,20 @@ actual Array element #0 is POINT(1 2), expected Array element #0 is null")] #[test] fn wkb_value_equal() { - assert_wkb_value_equal(None, None, "lhs", "rhs"); - assert_wkb_value_equal(Some(&[]), Some(&[]), "lhs", "rhs"); + assert_wkb_value_equal(None, None, "lhs", "rhs", false); + assert_wkb_value_equal(Some(&[]), Some(&[]), "lhs", "rhs", false); } #[test] #[should_panic(expected = "lhs != rhs:\nlhs is POINT(1 2), rhs is null")] fn wkb_value_expected_null() { - assert_wkb_value_equal(Some(&POINT), None, "lhs", "rhs"); + assert_wkb_value_equal(Some(&POINT), None, "lhs", "rhs", false); } #[test] #[should_panic(expected = "lhs != rhs:\nlhs is null, rhs is POINT(1 2)")] fn wkb_value_actual_null() { - assert_wkb_value_equal(None, Some(&POINT), "lhs", "rhs"); + assert_wkb_value_equal(None, Some(&POINT), "lhs", "rhs", false); } #[test] @@ -380,7 +454,31 @@ lhs: rhs: POINT(1 2)")] fn wkb_value_values_not_equal() { - assert_wkb_value_equal(Some(&[]), Some(&POINT), "lhs", "rhs"); + assert_wkb_value_equal(Some(&[]), Some(&POINT), "lhs", "rhs", false); + } + + #[cfg(feature = "geo")] + #[test] + fn wkb_value_equal_topologically() { + use crate::create::make_wkb; + assert_wkb_value_equal(Some(&POINT), Some(&POINT), "lhs", "rhs", true); + let lhs = make_wkb("POLYGON ((0 0, 1 0, 0 1, 0 0))"); + let rhs = make_wkb("POLYGON ((0 0, 0 1, 1 0, 0 0))"); + assert_wkb_value_equal(Some(&lhs), Some(&rhs), "lhs", "rhs", true); + } + + #[cfg(feature = "geo")] + #[test] + #[should_panic(expected = "lhs != rhs +lhs: + POLYGON((0 0,1 0,0 1,0 0)) +rhs: + POLYGON((0 0,1 0,0 0))")] + fn wkb_value_not_equal_topologically() { + use crate::create::make_wkb; + let lhs = make_wkb("POLYGON ((0 0, 1 0, 0 1, 0 0))"); + let rhs = make_wkb("POLYGON ((0 0, 1 0, 0 0))"); + assert_wkb_value_equal(Some(&lhs), Some(&rhs), "lhs", "rhs", true); } #[test] diff --git a/rust/sedona-testing/src/create.rs b/rust/sedona-testing/src/create.rs index fd5410ac..9d785d69 100644 --- a/rust/sedona-testing/src/create.rs +++ b/rust/sedona-testing/src/create.rs @@ -20,6 +20,7 @@ use arrow_array::{ArrayRef, BinaryArray, BinaryViewArray}; use datafusion_common::ScalarValue; use datafusion_expr::ColumnarValue; use sedona_schema::datatypes::SedonaType; +use wkb::{writer::WriteOptions, Endianness}; use wkt::Wkt; /// Create a [`ColumnarValue`] array from a sequence of WKT literals @@ -86,7 +87,14 @@ where pub fn make_wkb(wkt_value: &str) -> Vec { let geom = Wkt::::from_str(wkt_value).unwrap(); let mut out: Vec = vec![]; - wkb::writer::write_geometry(&mut out, &geom, Default::default()).unwrap(); + wkb::writer::write_geometry( + &mut out, + &geom, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); out } diff --git a/rust/sedona-testing/src/datagen.rs b/rust/sedona-testing/src/datagen.rs index 4fb73c21..44818603 100644 --- a/rust/sedona-testing/src/datagen.rs +++ b/rust/sedona-testing/src/datagen.rs @@ -40,6 +40,8 @@ use sedona_geometry::types::GeometryTypeId; use sedona_schema::datatypes::{SedonaType, WKB_GEOMETRY}; use std::f64::consts::PI; use std::sync::Arc; +use wkb::writer::WriteOptions; +use wkb::Endianness; /// Builder for generating test data partitions with random geometries. /// @@ -502,7 +504,14 @@ fn generate_random_wkb(rng: &mut R, options: &RandomGeometryOption // Convert geometry to WKB let mut out: Vec = vec![]; - wkb::writer::write_geometry(&mut out, &geometry, Default::default()).unwrap(); + wkb::writer::write_geometry( + &mut out, + &geometry, + &WriteOptions { + endianness: Endianness::LittleEndian, + }, + ) + .unwrap(); out } diff --git a/rust/sedona-testing/src/fixtures.rs b/rust/sedona-testing/src/fixtures.rs index 502bfaec..13011bf8 100644 --- a/rust/sedona-testing/src/fixtures.rs +++ b/rust/sedona-testing/src/fixtures.rs @@ -14,6 +14,11 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. +use std::{fs::File, path::PathBuf, str::FromStr}; + +use geo_types::{LineString, MultiPolygon, Point, Polygon}; +use wkt::{TryFromWkt, WktFloat}; + /// A well-known binary blob of MULTIPOINT (EMPTY) /// /// The wkt crate's parser rejects this; however, it's a corner case that may show @@ -22,3 +27,208 @@ pub const MULTIPOINT_WITH_EMPTY_CHILD_WKB: [u8; 30] = [ 0x01, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x7f, ]; + +pub fn louisiana() -> LineString +where + T: WktFloat + Default + FromStr, +{ + line_string("louisiana.wkt") +} + +pub fn baton_rouge() -> Point +where + T: WktFloat + Default + FromStr, +{ + let x = T::from(-91.147385).unwrap(); + let y = T::from(30.471165).unwrap(); + Point::new(x, y) +} + +pub fn east_baton_rouge() -> Polygon +where + T: WktFloat + Default + FromStr, +{ + polygon("east_baton_rouge.wkt") +} + +pub fn norway_main() -> LineString +where + T: WktFloat + Default + FromStr, +{ + line_string("norway_main.wkt") +} + +pub fn norway_concave_hull() -> LineString +where + T: WktFloat + Default + FromStr, +{ + line_string("norway_concave_hull.wkt") +} + +pub fn norway_convex_hull() -> LineString +where + T: WktFloat + Default + FromStr, +{ + line_string("norway_convex_hull.wkt") +} + +pub fn norway_nonconvex_hull() -> LineString +where + T: WktFloat + Default + FromStr, +{ + line_string("norway_nonconvex_hull.wkt") +} + +pub fn vw_orig() -> LineString +where + T: WktFloat + Default + FromStr, +{ + line_string("vw_orig.wkt") +} + +pub fn vw_simplified() -> LineString +where + T: WktFloat + Default + FromStr, +{ + line_string("vw_simplified.wkt") +} + +pub fn poly1() -> LineString +where + T: WktFloat + Default + FromStr, +{ + line_string("poly1.wkt") +} + +pub fn poly1_hull() -> LineString +where + T: WktFloat + Default + FromStr, +{ + line_string("poly1_hull.wkt") +} + +pub fn poly2() -> LineString +where + T: WktFloat + Default + FromStr, +{ + line_string("poly2.wkt") +} + +pub fn poly2_hull() -> LineString +where + T: WktFloat + Default + FromStr, +{ + line_string("poly2_hull.wkt") +} + +pub fn poly_in_ring() -> LineString +where + T: WktFloat + Default + FromStr, +{ + line_string("poly_in_ring.wkt") +} + +pub fn ring() -> LineString +where + T: WktFloat + Default + FromStr, +{ + line_string("ring.wkt") +} + +pub fn shell() -> LineString +where + T: WktFloat + Default + FromStr, +{ + line_string("shell.wkt") +} + +// From https://geodata.nationaalgeoregister.nl/kadastralekaart/wfs/v4_0?request=GetFeature&service=WFS&srsName=EPSG:4326&typeName=kadastralekaartv4:perceel&version=2.0.0&outputFormat=json&bbox=165593,480993,166125,481552 +pub fn nl_zones() -> MultiPolygon +where + T: WktFloat + Default + FromStr, +{ + multi_polygon("nl_zones.wkt") +} + +// From https://afnemers.ruimtelijkeplannen.nl/afnemers/services?request=GetFeature&service=WFS&srsName=EPSG:4326&typeName=Enkelbestemming&version=2.0.0&bbox=165618,480983,166149,481542"; +pub fn nl_plots_wgs84() -> MultiPolygon +where + T: WktFloat + Default + FromStr, +{ + multi_polygon("nl_plots.wkt") +} + +pub fn nl_plots_epsg_28992() -> MultiPolygon +where + T: WktFloat + Default + FromStr, +{ + // https://epsg.io/28992 + multi_polygon("nl_plots_epsg_28992.wkt") +} + +fn line_string(name: &str) -> LineString +where + T: WktFloat + Default + FromStr, +{ + LineString::try_from_wkt_reader(file(name)).unwrap() +} + +pub fn polygon(name: &str) -> Polygon +where + T: WktFloat + Default + FromStr, +{ + Polygon::try_from_wkt_reader(file(name)).unwrap() +} + +pub fn multi_polygon(name: &str) -> MultiPolygon +where + T: WktFloat + Default + FromStr, +{ + MultiPolygon::try_from_wkt_reader(file(name)).unwrap() +} + +pub fn file(name: &str) -> File { + let base = crate::data::sedona_testing_dir() + .expect("sedona-testing directory should resolve when accessing fixtures"); + + let mut path = PathBuf::from(base); + path.push("data"); + path.push("wkts"); + path.push("geo-test-fixtures"); + path.push(name); + + File::open(&path).unwrap_or_else(|_| panic!("Can't open file: {path:?}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn norway_main_linestring_has_vertices() { + let ls = norway_main::(); + assert!( + !ls.0.is_empty(), + "LineString loaded from norway_main.wkt should have vertices" + ); + + let first = ls.0.first().expect("expected at least one coordinate"); + assert!(first.x.is_finite(), "first coordinate x should be finite"); + assert!(first.y.is_finite(), "first coordinate y should be finite"); + } + + #[test] + fn nl_zones_multipolygon_not_empty() { + let mp = nl_zones::(); + assert!( + !mp.0.is_empty(), + "MultiPolygon from nl_zones.wkt should contain polygons" + ); + + let polygon = mp.0.first().expect("expected at least one polygon"); + assert!( + !polygon.exterior().0.is_empty(), + "polygon exterior ring should contain coordinates" + ); + } +} diff --git a/submodules/sedona-testing b/submodules/sedona-testing index 5073f740..c7bc17d7 160000 --- a/submodules/sedona-testing +++ b/submodules/sedona-testing @@ -1 +1 @@ -Subproject commit 5073f7405a6aa2b8eb326da94356802dca956a6a +Subproject commit c7bc17d7109fc628959eb2850d4cfce3d483b1ee