Skip to content

Commit 106caa6

Browse files
authored
Merge branch 'main' into release/geo-0.32.0
2 parents 7b56106 + 1d4d2b5 commit 106caa6

23 files changed

Lines changed: 1210 additions & 72 deletions

File tree

.github/workflows/test.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ jobs:
7070
image: "ghcr.io/georust/geo-ci:proj-${{ needs.set-matrix.outputs.proj-version }}-rust-${{ fromJson(needs.set-matrix.outputs.rust-versions)[2] }}"
7171
steps:
7272
- name: Checkout repository
73-
uses: actions/checkout@v5
73+
uses: actions/checkout@v6
7474
- uses: Swatinem/rust-cache@v2
7575
- run: rustup component add rustfmt clippy
7676
- run: cargo fmt --all -- --check
@@ -91,7 +91,7 @@ jobs:
9191
image: "ghcr.io/georust/geo-ci:proj-${{ needs.set-matrix.outputs.proj-version }}-rust-${{ matrix.rust_version }}"
9292
steps:
9393
- name: Checkout repository
94-
uses: actions/checkout@v5
94+
uses: actions/checkout@v6
9595
- uses: Swatinem/rust-cache@v2
9696
- run: rustup target add thumbv7em-none-eabihf
9797
- run: cargo check --all-targets --no-default-features
@@ -113,7 +113,7 @@ jobs:
113113
image: "ghcr.io/georust/geo-ci:proj-${{ needs.set-matrix.outputs.proj-version }}-rust-${{ matrix.rust_version }}"
114114
steps:
115115
- name: Checkout repository
116-
uses: actions/checkout@v5
116+
uses: actions/checkout@v6
117117
- uses: Swatinem/rust-cache@v2
118118
- run: cargo check --all-targets --no-default-features
119119
# we don't want to test `proj-network` because it only enables the `proj` feature
@@ -134,7 +134,7 @@ jobs:
134134
image: "ghcr.io/georust/geo-ci:proj-${{ needs.set-matrix.outputs.proj-version }}-rust-${{ matrix.rust_version }}"
135135
steps:
136136
- name: Checkout repository
137-
uses: actions/checkout@v5
137+
uses: actions/checkout@v6
138138
- uses: Swatinem/rust-cache@v2
139139
- run: cargo check --all-targets
140140
- run: cargo test
@@ -154,7 +154,7 @@ jobs:
154154
image: "ghcr.io/georust/geo-ci:proj-${{ needs.set-matrix.outputs.proj-version }}-rust-${{ matrix.rust_version }}"
155155
steps:
156156
- name: Checkout repository
157-
uses: actions/checkout@v5
157+
uses: actions/checkout@v6
158158
- uses: Swatinem/rust-cache@v2
159159
- run: cargo check --all-targets
160160

@@ -170,7 +170,7 @@ jobs:
170170
image: "ghcr.io/georust/geo-ci:proj-${{ needs.set-matrix.outputs.proj-version }}-rust-${{ fromJson(needs.set-matrix.outputs.rust-versions)[2] }}"
171171
steps:
172172
- name: Checkout repository
173-
uses: actions/checkout@v5
173+
uses: actions/checkout@v6
174174
- uses: Swatinem/rust-cache@v2
175175
- run: cargo build --bins
176176

@@ -183,6 +183,6 @@ jobs:
183183
image: "ghcr.io/georust/geo-ci:proj-${{ needs.set-matrix.outputs.proj-version }}-rust-${{ fromJson(needs.set-matrix.outputs.rust-versions)[2] }}"
184184
steps:
185185
- name: Checkout repository
186-
uses: actions/checkout@v5
186+
uses: actions/checkout@v6
187187
- uses: Swatinem/rust-cache@v2
188188
- run: RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps

geo-benches/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ name = "contains_properly"
5555
path = "src/contains_properly.rs"
5656
harness = false
5757

58+
[[bench]]
59+
name = "covers"
60+
path = "src/covers.rs"
61+
harness = false
62+
5863
[[bench]]
5964
name = "convex_hull"
6065
path = "src/convex_hull.rs"

geo-benches/src/contains_properly.rs

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use criterion::{Criterion, criterion_group, criterion_main};
1+
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
22
use geo::PreparedGeometry;
33
use geo::algorithm::{ContainsProperly, Convert, Relate};
4-
use geo::geometry::*;
54
use geo::wkt;
5+
use geo::{coord, geometry::*};
6+
use std::iter::once;
67

78
fn compare_simple_in_complex(c: &mut Criterion) {
89
c.bench_function(
@@ -138,5 +139,60 @@ fn compare_poly_in_poly(c: &mut Criterion) {
138139
});
139140
}
140141

142+
fn polygon_polygon_scaling(c: &mut Criterion) {
143+
fn make_outer_poly(n: i32) -> Polygon<f64> {
144+
let pts: LineString<f64> = LineString::new(
145+
once(coord! {x:0,y:0})
146+
.chain((1..n).map(|i: i32| coord! {x:i*5,y:5*(1+(1+i%2))}))
147+
.chain(once(coord! {x:n,y:0}))
148+
.collect(),
149+
)
150+
.convert();
151+
Polygon::<f64>::new(pts, vec![])
152+
}
153+
fn make_inner_poly(n: i32) -> Polygon<f64> {
154+
let pts: LineString<f64> = LineString::new(
155+
once(coord! {x:1,y:1})
156+
.chain((1..n).map(|i: i32| coord! {x:i,y:(1+i%2)}))
157+
.chain(once(coord! {x:n-1,y:0}))
158+
.collect(),
159+
)
160+
.convert();
161+
Polygon::<f64>::new(pts, vec![])
162+
}
163+
164+
{
165+
// create two polygons, both of of n+2 sides and no holes
166+
let mut group = c.benchmark_group("contains_properly polygon polygon scaling");
167+
168+
// trait is faster for small polygons, but relate overtakes from around 700*700 boundary segment checks
169+
for i in [10, 100, 200, 300, 400, 500, 600, 700, 800] {
170+
group.throughput(Throughput::Elements(i as u64));
171+
172+
let inner_poly = make_inner_poly(i);
173+
let outer_poly = make_outer_poly(i);
174+
175+
group.bench_with_input(
176+
BenchmarkId::new("trait", i),
177+
&(&outer_poly, &inner_poly),
178+
|bencher, &(a, b)| {
179+
bencher.iter(|| a.contains_properly(b));
180+
},
181+
);
182+
183+
group.bench_with_input(
184+
BenchmarkId::new("relate", i),
185+
&(&outer_poly, &inner_poly),
186+
|bencher, &(a, b)| {
187+
bencher.iter(|| a.relate(b).is_contains_properly());
188+
},
189+
);
190+
}
191+
group.finish();
192+
}
193+
}
194+
141195
criterion_group!(benches, compare_simple_in_complex, compare_poly_in_poly,);
142-
criterion_main!(benches);
196+
criterion_group!(benches_scaling, polygon_polygon_scaling);
197+
198+
criterion_main!(benches, benches_scaling);

geo-benches/src/covers.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
2+
use geo::algorithm::{Contains, Covers};
3+
use geo::{Convert, wkt};
4+
use geo::{coord, geometry::*};
5+
6+
fn rect_covers(c: &mut Criterion) {
7+
c.bench_function("rect covers line", |bencher| {
8+
let rect = Rect::new(coord! { x: 0., y: 0. }, coord! { x: 10., y: 10. });
9+
let line = Line::new(coord! { x: 5., y: 5. }, coord! { x: 7., y: 7. });
10+
bencher.iter(|| {
11+
assert!(criterion::black_box(&rect).covers(criterion::black_box(&line)));
12+
});
13+
});
14+
15+
c.bench_function("rect contains line", |bencher| {
16+
let rect = Rect::new(coord! { x: 0., y: 0. }, coord! { x: 10., y: 10. });
17+
let line = Line::new(coord! { x: 5., y: 5. }, coord! { x: 7., y: 7. });
18+
bencher.iter(|| {
19+
assert!(criterion::black_box(&rect).contains(criterion::black_box(&line)));
20+
});
21+
});
22+
}
23+
24+
fn linestring_covers_point(c: &mut Criterion) {
25+
c.bench_function("linestring covers point", |bencher| {
26+
let ls: LineString<f64> = wkt! {LINESTRING(0 0, 10 0, 10 10, 0 10)}.convert();
27+
let pt: Point<f64> = wkt! {POINT(5 10)}.convert();
28+
bencher.iter(|| {
29+
assert!(criterion::black_box(&ls).covers(criterion::black_box(&pt)));
30+
});
31+
});
32+
33+
c.bench_function("linestring contains point", |bencher| {
34+
let ls: LineString<f64> = wkt! {LINESTRING(0 0, 10 0, 10 10, 0 10)}.convert();
35+
let pt: Point<f64> = wkt! {POINT(5 10)}.convert();
36+
bencher.iter(|| {
37+
assert!(criterion::black_box(&ls).contains(criterion::black_box(&pt)));
38+
});
39+
});
40+
}
41+
42+
// bench a method derived from intersects
43+
fn rect_linestring_scaling(c: &mut Criterion) {
44+
fn make_outer_rect(n: i32) -> Rect<f64> {
45+
Rect::new(coord! {x:0,y:0}, coord! {x:n,y:3}).convert()
46+
}
47+
fn make_inner_ls(n: i32) -> LineString<f64> {
48+
LineString::new((1..n).map(|i: i32| coord! {x:i,y:(1+i%2)}).collect()).convert()
49+
}
50+
51+
{
52+
// create two polygons, both of of n+2 sides and no holes
53+
let mut group = c.benchmark_group("covers rect linestring scaling");
54+
55+
// trait is faster for small polygons, but relate overtakes from around 700*700 boundary segment checks
56+
for i in [10, 1_000, 100_000] {
57+
group.throughput(Throughput::Elements(i as u64));
58+
59+
let inner_poly = make_inner_ls(i);
60+
let outer_poly = make_outer_rect(i);
61+
62+
group.bench_with_input(
63+
BenchmarkId::new("covers trait", i),
64+
&(&outer_poly, &inner_poly),
65+
|bencher, &(a, b)| {
66+
bencher.iter(|| assert!(a.covers(b)));
67+
},
68+
);
69+
70+
// contains delegates this to relate
71+
group.bench_with_input(
72+
BenchmarkId::new("contains trait", i),
73+
&(&outer_poly, &inner_poly),
74+
|bencher, &(a, b)| {
75+
bencher.iter(|| assert!(a.contains(b)));
76+
},
77+
);
78+
}
79+
group.finish();
80+
}
81+
}
82+
83+
criterion_group!(
84+
benches,
85+
rect_covers,
86+
linestring_covers_point,
87+
rect_linestring_scaling
88+
);
89+
criterion_main!(benches);

geo/CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
- Add `distance_within` method with default impl for any geometry that implements `Distance`, with similar semantics to the PostGIS [ST_DWithin](https://postgis.net/docs/ST_DWithin.html) function
1010
- Add fast minimum 1D and 2D Euclidean distance algorithm for linearly separable geometries (#1424)
1111
- Add `ContainsProperly` trait to relate and as a standalone operation
12+
- `ContainsProperly` is faster for `Polygon` and `MultiPolygon` when inputs are smaller than about 650 vertices, otherwise use `Relate.is_contains_properly`
13+
- <https://github.com/georust/geo/pull/1457>
1214
- Add k-means clustering algorithm
1315
- POSSIBLY BREAKING: `minimum_rotated_rect` is about 1.3-2x faster, but might return slightly different results.
1416
- <https://github.com/georust/geo/pull/1446>
@@ -19,6 +21,9 @@
1921
- BREAKING: The `concave_hull` method now has no `concavity` parameter.
2022
- Add `concave_hull_with_options` method which requires `ConcaveHullOptions` as a parameter with `concavity` and `length_threshold` options.
2123
- <https://github.com/georust/geo/pull/1442>
24+
- Add `Covers` trait to relate and as a standalone operation
25+
- custom implementations for checking Geometries covered by `Rect`, `Triangle`, `Line`, `Point`, `Coord`
26+
- custom implementations for checking Geometries covering `Point` and `MultiPoint`
2227

2328
## 0.31.0 - 2025-09-01
2429

geo/src/algorithm/contains/line_string.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use super::{Contains, impl_contains_from_relate, impl_contains_geometry_for};
22
use crate::algorithm::kernels::Kernel;
3+
use crate::dimensions::Dimensions;
34
use crate::geometry::*;
45
use crate::{CoordNum, GeoFloat, GeoNum, HasDimensions, Intersects, Orientation};
56

@@ -124,7 +125,18 @@ where
124125
if self.is_empty() || rhs.is_empty() {
125126
return false;
126127
}
127-
rhs.lines().all(|l| self.contains(&l))
128+
129+
// handle degenerate linestring case
130+
if rhs.dimensions() == Dimensions::ZeroDimensional {
131+
return self.contains(&rhs.0[0]);
132+
}
133+
134+
// filter out zero-length segments
135+
// it is known from != Dimensions::ZeroDimensional && !self.is_empty()
136+
// that there must be at least one segment with non-zero length
137+
rhs.lines()
138+
.filter(|l| l.start != l.end)
139+
.all(|l| self.contains(&l))
128140
}
129141
}
130142

@@ -212,6 +224,33 @@ mod test {
212224
use crate::{Convert, wkt};
213225
use crate::{Line, LineString, Validation};
214226

227+
#[test]
228+
fn linestring_component_with_zero_length() {
229+
let ls: LineString<f64> = wkt! {LINESTRING(0 0, 2 2)}.convert();
230+
231+
// these are valid geometries
232+
let ls_start: LineString<f64> = wkt! {LINESTRING(0 0, 0 0,0 0, 1 1)}.convert();
233+
let ls_end: LineString<f64> = wkt! {LINESTRING(0 0, 1 1,1 1,1 1)}.convert();
234+
assert!(ls_start.is_valid());
235+
assert!(ls_end.is_valid());
236+
237+
// these are invalid geometries but we handle degenerate geometries as points
238+
let degen_start: LineString<f64> = wkt! {LINESTRING(0 0, 0 0)}.convert();
239+
let degen_end: LineString<f64> = wkt! {LINESTRING(2 2, 2 2)}.convert();
240+
assert!(!degen_start.is_valid());
241+
assert!(!degen_end.is_valid());
242+
243+
assert!(ls.relate(&ls_start).is_contains());
244+
assert!(ls.contains(&ls_start));
245+
assert!(ls.relate(&ls_end).is_contains());
246+
assert!(ls.contains(&ls_end));
247+
248+
assert!(!ls.contains(&degen_start));
249+
assert!(!ls.relate(&degen_start).is_contains());
250+
assert!(!ls.contains(&degen_end));
251+
assert!(!ls.relate(&degen_end).is_contains());
252+
}
253+
215254
#[test]
216255
fn triangles() {
217256
let ln: Line<f64> = wkt! {LINE(0 0, 10 0)}.convert();

geo/src/algorithm/contains_properly/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
///
4040
/// Much of this trait is currently implemented by delegating to the [`Relate`](crate::algorithm::Relate) trait - see
4141
/// [`IntersectionMatrix::is_contains_properly`](crate::algorithm::relate::IntersectionMatrix::is_contains_properly);
42-
/// However, [`ContainsProperly`] may be faster for checking some `Polygon`s.
42+
/// `ContainsProperly` is faster when checking between `Polygon` and `MultiPolygon` when inputs are smaller than about 650 vertices, otherwise use [`Relate::relate().is_contains_properly`](crate::algorithm::relate::IntersectionMatrix::is_contains_properly).
4343
pub trait ContainsProperly<Rhs = Self> {
4444
fn contains_properly(&self, rhs: &Rhs) -> bool;
4545
}

geo/src/algorithm/covers/coord.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use super::{Covers, impl_covers_from_intersects};
2+
use crate::GeoNum;
3+
use crate::Intersects;
4+
use crate::geometry::*;
5+
6+
impl<T, G> Covers<Coord<T>> for G
7+
where
8+
T: GeoNum,
9+
G: Intersects<Coord<T>>,
10+
{
11+
fn covers(&self, rhs: &Coord<T>) -> bool {
12+
self.intersects(rhs)
13+
}
14+
}
15+
16+
// valid because self is convex geometry
17+
// all exterior pts of RHS intersecting self means self covers RHS
18+
impl_covers_from_intersects!(Coord<T>, [
19+
Point<T>, MultiPoint<T>,
20+
Line<T>,
21+
LineString<T>, MultiLineString<T>,
22+
Rect<T>, Triangle<T>,
23+
Polygon<T>, MultiPolygon<T>,
24+
Geometry<T>, GeometryCollection<T>
25+
]);
26+
27+
#[cfg(test)]
28+
mod tests {
29+
use super::*;
30+
31+
#[test]
32+
fn test_rhs_empty() {
33+
let s: Coord<f64> = Coord::zero();
34+
assert!(!s.covers(&LineString::empty()));
35+
assert!(!s.covers(&Polygon::empty()));
36+
assert!(!s.covers(&MultiPoint::empty()));
37+
assert!(!s.covers(&MultiLineString::empty()));
38+
assert!(!s.covers(&MultiPolygon::empty()));
39+
assert!(!s.covers(&GeometryCollection::empty()));
40+
}
41+
}

0 commit comments

Comments
 (0)