Skip to content

Commit 974c848

Browse files
committed
Make geojson optional, use geo-types for geometry representation.
geo-types: - is pretty stable, so this should avoid some downstream upgrade churn. - is a smaller dependency than geojson. - allows for interop with an ecosystem of other tools - is a more efficient way to represent geometry for example here are the benchmarks: before: ``` test bench_build_geojson_contour                                                ... bench:       3,337 ns/iter (+/- 182) test bench_build_geojson_contour_no_smoothing                                   ... bench:       3,252 ns/iter (+/- 328) test bench_build_geojson_contours_multiple_thresholds                           ... bench:      16,838 ns/iter (+/- 1,371) test bench_build_geojson_contours_multiple_thresholds_and_x_y_steps_and_origins ... bench:      16,934 ns/iter (+/- 1,325) test bench_build_isoring                                                        ... bench:       2,890 ns/iter (+/- 135) test bench_build_isoring_values2                                                ... bench:       5,103 ns/iter (+/- 395) ``` after: ``` test bench_build_geojson_contour                                                ... bench:       1,917 ns/iter (+/- 29) test bench_build_geojson_contour_no_smoothing                                   ... bench:       1,833 ns/iter (+/- 83) test bench_build_geojson_contours_multiple_thresholds                           ... bench:       9,414 ns/iter (+/- 206) test bench_build_geojson_contours_multiple_thresholds_and_x_y_steps_and_origins ... bench:       9,490 ns/iter (+/- 539) test bench_build_isoring                                                        ... bench:       1,639 ns/iter (+/- 72) test bench_build_isoring_values2                                                ... bench:       2,893 ns/iter (+/- 102) ```
1 parent f591ae8 commit 974c848

File tree

4 files changed

+436
-362
lines changed

4 files changed

+436
-362
lines changed

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ keywords = ["contour", "polygon", "isoring", "marching-squares", "geojson"]
1313
license = "MIT/Apache-2.0"
1414

1515
[dependencies]
16-
geojson = ">=0.16, <=0.24"
16+
geojson = { version = ">=0.16, <=0.24", optional = true }
17+
geo-types= { version = "0.7.0" }
1718
lazy_static = "1.0"
1819
serde_json = "^1.0"
1920
rustc-hash = "1.0"
2021
slab = "0.4"
22+
23+
[package.metadata.docs.rs]
24+
all-features = true
25+
rustdoc-args = ["--cfg", "docsrs"]

src/area.rs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ use crate::contour::Pt;
33
pub fn area(ring: &[Pt]) -> f64 {
44
let mut i = 0;
55
let n = ring.len() - 1;
6-
let mut area = ring[n - 1][1] * ring[0][0] - ring[n - 1][0] * ring[0][1];
6+
let mut area = ring[n - 1].y * ring[0].x - ring[n - 1].x * ring[0].y;
77
while i < n {
88
i += 1;
9-
area += ring[i - 1][1] * ring[i][0] - ring[i - 1][0] * ring[i][1];
9+
area += ring[i - 1].y * ring[i].x - ring[i - 1].x * ring[i].y;
1010
}
1111
area
1212
}
@@ -25,19 +25,19 @@ pub fn contains(ring: &[Pt], hole: &[Pt]) -> i32 {
2525
0
2626
}
2727

28-
fn ring_contains(ring: &[Pt], point: &[f64]) -> i32 {
29-
let x = point[0];
30-
let y = point[1];
28+
fn ring_contains(ring: &[Pt], point: &Pt) -> i32 {
29+
let x = point.x;
30+
let y = point.y;
3131
let n = ring.len();
3232
let mut contains = -1;
3333
let mut j = n - 1;
3434
for i in 0..n {
3535
let pi = &ring[i];
36-
let xi = pi[0];
37-
let yi = pi[1];
36+
let xi = pi.x;
37+
let yi = pi.y;
3838
let pj = &ring[j];
39-
let xj = pj[0];
40-
let yj = pj[1];
39+
let xj = pj.x;
40+
let yj = pj.y;
4141
if segment_contains(pi, pj, point) {
4242
return 0;
4343
}
@@ -49,20 +49,20 @@ fn ring_contains(ring: &[Pt], point: &[f64]) -> i32 {
4949
contains
5050
}
5151

52-
fn segment_contains(a: &[f64], b: &[f64], c: &[f64]) -> bool {
52+
fn segment_contains(a: &Pt, b: &Pt, c: &Pt) -> bool {
5353
if collinear(a, b, c) {
54-
if (a[0] - b[0]).abs() < std::f64::EPSILON {
55-
within(a[1], c[1], b[1])
54+
if (a.x - b.x).abs() < std::f64::EPSILON {
55+
within(a.y, c.y, b.y)
5656
} else {
57-
within(a[0], c[0], b[0])
57+
within(a.x, c.x, b.x)
5858
}
5959
} else {
6060
false
6161
}
6262
}
6363

64-
fn collinear(a: &[f64], b: &[f64], c: &[f64]) -> bool {
65-
((b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])).abs() < std::f64::EPSILON
64+
fn collinear(a: &Pt, b: &Pt, c: &Pt) -> bool {
65+
((b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y)).abs() < std::f64::EPSILON
6666
}
6767

6868
fn within(p: f64, q: f64, r: f64) -> bool {

src/contour.rs

Lines changed: 145 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
use crate::area::{area, contains};
22
use crate::error::{new_error, ErrorKind, Result};
3-
use geojson::Value::MultiPolygon;
4-
use geojson::{Feature, Geometry};
3+
use geo_types::{LineString, MultiPolygon, Polygon};
54
use lazy_static::lazy_static;
65
use rustc_hash::FxHashMap;
7-
use serde_json::map::Map;
8-
use serde_json::to_value;
96
use slab::Slab;
107

11-
pub type Pt = Vec<f64>;
8+
pub type Pt = geo_types::Coordinate;
129
pub type Ring = Vec<Pt>;
1310

1411
lazy_static! {
@@ -48,7 +45,7 @@ struct Fragment {
4845

4946
/// Contours generator, using builder pattern, to
5047
/// be used on a rectangular `Slice` of values to
51-
/// get a `Vec` of Features of MultiPolygon (use [`contour_rings`] internally).
48+
/// get a `Vec` of [`Contour`] (uses [`contour_rings`] internally).
5249
///
5350
/// [`contour_rings`]: fn.contour_rings.html
5451
pub struct ContourBuilder {
@@ -122,8 +119,8 @@ impl ContourBuilder {
122119

123120
ring.iter_mut()
124121
.map(|point| {
125-
let x = point[0];
126-
let y = point[1];
122+
let x = point.x;
123+
let y = point.y;
127124
let xt = x.trunc() as u32;
128125
let yt = y.trunc() as u32;
129126
let mut v0;
@@ -132,11 +129,11 @@ impl ContourBuilder {
132129
let v1 = values[ix];
133130
if x > 0.0 && x < (dx as f64) && (xt as f64 - x).abs() < std::f64::EPSILON {
134131
v0 = values[(yt * dx + xt - 1) as usize];
135-
point[0] = x + (value - v0) / (v1 - v0) - 0.5;
132+
point.x = x + (value - v0) / (v1 - v0) - 0.5;
136133
}
137134
if y > 0.0 && y < (dy as f64) && (yt as f64 - y).abs() < std::f64::EPSILON {
138135
v0 = values[((yt - 1) * dx + xt) as usize];
139-
point[1] = y + (value - v0) / (v1 - v0) - 0.5;
136+
point.y = y + (value - v0) / (v1 - v0) - 0.5;
140137
}
141138
}
142139
})
@@ -151,23 +148,23 @@ impl ContourBuilder {
151148
///
152149
/// * `values` - The slice of values to be used.
153150
/// * `thresholds` - The slice of thresholds values to be used.
154-
pub fn contours(&self, values: &[f64], thresholds: &[f64]) -> Result<Vec<Feature>> {
151+
pub fn contours(&self, values: &[f64], thresholds: &[f64]) -> Result<Vec<Contour>> {
155152
if values.len() as u32 != self.dx * self.dy {
156153
return Err(new_error(ErrorKind::BadDimension));
157154
}
158155
let mut isoring = IsoRingBuilder::new(self.dx, self.dy);
159156
thresholds
160157
.iter()
161-
.map(|value| self.contour(values, *value, &mut isoring))
162-
.collect::<Result<Vec<Feature>>>()
158+
.map(|threshold| self.contour(values, *threshold, &mut isoring))
159+
.collect()
163160
}
164161

165162
fn contour(
166163
&self,
167164
values: &[f64],
168165
threshold: f64,
169166
isoring: &mut IsoRingBuilder,
170-
) -> Result<Feature> {
167+
) -> Result<Contour> {
171168
let (mut polygons, mut holes) = (Vec::new(), Vec::new());
172169
let mut result = isoring.compute(values, threshold)?;
173170

@@ -184,15 +181,15 @@ impl ContourBuilder {
184181
{
185182
ring.iter_mut()
186183
.map(|point| {
187-
point[0] = point[0] * self.x_step + self.x_origin;
188-
point[1] = point[1] * self.y_step + self.y_origin;
184+
point.x = point.x * self.x_step + self.x_origin;
185+
point.y = point.y * self.y_step + self.y_origin;
189186
})
190187
.for_each(drop);
191188
}
192189
if area(&ring) > 0.0 {
193-
polygons.push(vec![ring]);
190+
polygons.push(Polygon::new(LineString::new(ring), vec![]))
194191
} else {
195-
holes.push(ring);
192+
holes.push(LineString::new(ring));
196193
}
197194
})
198195
.for_each(drop);
@@ -201,27 +198,84 @@ impl ContourBuilder {
201198
.drain(..)
202199
.map(|hole| {
203200
for polygon in &mut polygons {
204-
if contains(&polygon[0], &hole) != -1 {
205-
polygon.push(hole);
201+
if contains(&polygon.exterior().0, &hole.0) != -1 {
202+
polygon.interiors_push(hole);
206203
return;
207204
}
208205
}
209206
})
210207
.for_each(drop);
211208

212-
let mut properties = Map::with_capacity(1);
213-
properties.insert(String::from("value"), to_value(threshold)?);
214-
Ok(Feature {
215-
geometry: Some(Geometry {
216-
value: MultiPolygon(polygons),
217-
bbox: None,
218-
foreign_members: None,
219-
}),
220-
properties: Some(properties),
209+
Ok(Contour {
210+
geometry: MultiPolygon(polygons),
211+
threshold,
212+
})
213+
}
214+
}
215+
216+
/// A contour has the geometry and threshold of a contour ring, built by [`ContourBuilder`].
217+
#[derive(Debug, Clone)]
218+
pub struct Contour {
219+
geometry: MultiPolygon,
220+
threshold: f64,
221+
}
222+
223+
impl Contour {
224+
/// Borrow the [`MultiPolygon`](geo_types::MultiPolygon) geometry of this contour.
225+
pub fn geometry(&self) -> &MultiPolygon {
226+
&self.geometry
227+
}
228+
229+
/// Get the owned polygons and threshold of this countour.
230+
pub fn into_inner(self) -> (MultiPolygon, f64) {
231+
(self.geometry, self.threshold)
232+
}
233+
234+
/// Get the threshold used to construct this contour.
235+
pub fn threshold(&self) -> f64 {
236+
self.threshold
237+
}
238+
239+
#[cfg(feature = "geojson")]
240+
/// Convert the contour to a struct from the `geojson` crate.
241+
///
242+
/// To get a string reresentation, call to_geojson().to_string().
243+
/// ```
244+
/// use contour::ContourBuilder;
245+
///
246+
/// let builder = ContourBuilder::new(10, 10, false);
247+
/// # #[rustfmt::skip]
248+
/// let contours = builder.contours(&[
249+
/// // ...ellided for brevity
250+
/// # 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
251+
/// # 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
252+
/// # 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
253+
/// # 0., 0., 0., 2., 1., 2., 0., 0., 0., 0.,
254+
/// # 0., 0., 0., 2., 2., 2., 0., 0., 0., 0.,
255+
/// # 0., 0., 0., 1., 2., 1., 0., 0., 0., 0.,
256+
/// # 0., 0., 0., 2., 2., 2., 0., 0., 0., 0.,
257+
/// # 0., 0., 0., 2., 1., 2., 0., 0., 0., 0.,
258+
/// # 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
259+
/// # 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.
260+
/// ], &[0.5]).unwrap();
261+
///
262+
/// let geojson_string = contours[0].to_geojson().to_string();
263+
///
264+
/// assert_eq!(&geojson_string[0..27], r#"{"geometry":{"coordinates":"#);
265+
/// ```
266+
pub fn to_geojson(&self) -> geojson::Feature {
267+
let mut properties = geojson::JsonObject::with_capacity(1);
268+
// REVIEW: This maintains existing behavior, but should we rename
269+
// `value` to something more explicable, like `threshold`?
270+
properties.insert("value".to_string(), self.threshold.into());
271+
272+
geojson::Feature {
221273
bbox: None,
274+
geometry: Some(geojson::Geometry::from(self.geometry())),
222275
id: None,
276+
properties: Some(properties),
223277
foreign_members: None,
224-
})
278+
}
225279
}
226280
}
227281

@@ -351,14 +405,20 @@ impl IsoRingBuilder {
351405
Ok(result)
352406
}
353407

354-
fn index(&self, point: &[f64]) -> usize {
355-
(point[0] * 2.0 + point[1] * (self.dx as f64 + 1.) * 4.) as usize
408+
fn index(&self, point: &Pt) -> usize {
409+
(point.x * 2.0 + point.y * (self.dx as f64 + 1.) * 4.) as usize
356410
}
357411

358412
// Stitchs segments to rings.
359413
fn stitch(&mut self, line: &[Vec<f64>], x: i32, y: i32, result: &mut Vec<Ring>) -> Result<()> {
360-
let start = vec![line[0][0] + x as f64, line[0][1] + y as f64];
361-
let end = vec![line[1][0] + x as f64, line[1][1] + y as f64];
414+
let start = Pt {
415+
x: line[0][0] + x as f64,
416+
y: line[0][1] + y as f64,
417+
};
418+
let end = Pt {
419+
x: line[1][0] + x as f64,
420+
y: line[1][1] + y as f64,
421+
};
362422
let start_index = self.index(&start);
363423
let end_index = self.index(&end);
364424
if self.fragment_by_end.contains_key(&start_index) {
@@ -458,3 +518,54 @@ impl IsoRingBuilder {
458518
self.is_empty = true;
459519
}
460520
}
521+
522+
#[cfg(test)]
523+
mod tests {
524+
525+
#[cfg(feature = "geojson")]
526+
#[test]
527+
fn test_simple_polygon_no_smoothing_geojson() {
528+
use super::*;
529+
let c = ContourBuilder::new(10, 10, false);
530+
#[rustfmt::skip]
531+
let res = c.contours(&[
532+
0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
533+
0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
534+
0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
535+
0., 0., 0., 2., 1., 2., 0., 0., 0., 0.,
536+
0., 0., 0., 2., 2., 2., 0., 0., 0., 0.,
537+
0., 0., 0., 1., 2., 1., 0., 0., 0., 0.,
538+
0., 0., 0., 2., 2., 2., 0., 0., 0., 0.,
539+
0., 0., 0., 2., 1., 2., 0., 0., 0., 0.,
540+
0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
541+
0., 0., 0., 0., 0., 0., 0., 0., 0., 0.
542+
], &[0.5]).unwrap();
543+
match res[0].to_geojson().geometry.unwrap().value {
544+
geojson::Value::MultiPolygon(p) => {
545+
assert_eq!(
546+
p,
547+
vec![vec![vec![
548+
vec![6., 7.5],
549+
vec![6., 6.5],
550+
vec![6., 5.5],
551+
vec![6., 4.5],
552+
vec![6., 3.5],
553+
vec![5.5, 3.],
554+
vec![4.5, 3.],
555+
vec![3.5, 3.],
556+
vec![3., 3.5],
557+
vec![3., 4.5],
558+
vec![3., 5.5],
559+
vec![3., 6.5],
560+
vec![3., 7.5],
561+
vec![3.5, 8.],
562+
vec![4.5, 8.],
563+
vec![5.5, 8.],
564+
vec![6., 7.5],
565+
]]]
566+
);
567+
}
568+
_ => panic!(""),
569+
};
570+
}
571+
}

0 commit comments

Comments
 (0)