Skip to content

Commit 8877a27

Browse files
committed
Add support for building isolines as MultiLineString
1 parent 2b58431 commit 8877a27

File tree

4 files changed

+136
-5
lines changed

4 files changed

+136
-5
lines changed

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
21
/target/
32
**/*.rs.bk
43
Cargo.lock
4+
5+
# IntelliJ related
6+
.idea/
7+
8+
# VSCode related
9+
.vscode/

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
## Changelog
22

3+
### 0.9.0 (2023-xx-xx)
4+
5+
- Add support for building isolines as MultiLineStrings (instead of solely building contour polygons as MultiPolygons).
6+
7+
- Improve some minor details in the documentation.
8+
39
### 0.8.0 (2023-02-21)
410

511
- Be less restrictive about the geo-types version and use geo_types::Coord instead of deprecated geo_types::Coordinate.

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,15 @@ extern crate contour;
2828
```
2929

3030
The API exposes:
31-
- a `contour_rings` function, which computes isorings coordinates for one threshold value (*returns a `Vec` of rings coordinates*).
32-
- a `ContourBuilder` struct, which computes isorings coordinates for a `Vec` of threshold values and transform them in `Contour`s (a type containing the threshold value and the geometry as a MultiPolygon, easily serializable to GeoJSON).
31+
- a `ContourBuilder` struct, which computes isorings coordinates for a `Vec` of threshold values and transform them either :
32+
- in `Contour`s (a type containing the threshold value and the geometry as a `MultiPolygon`, easily serializable to GeoJSON), or,
33+
- in `Line`s (a type containing the threshold value and the geometry as a `MultiLineString`, easily serializable to GeoJSON).
3334

3435

36+
- a `contour_rings` function, which computes isorings coordinates for a single threshold value (*returns a `Vec` of rings coordinates* - this is what is used internally by the `ContourBuilder`).
37+
38+
`ContourBuilder` is the recommended way to use this crate, as it is more flexible and easier to use (it enables to specify the origin and the step of the grid, and to smooth the contours, while `contour_rings` only speak in grid coordinates and doesn't smooth the resulting rings).
39+
3540
### Example:
3641

3742
**Without defining origin and step:**

src/contour.rs

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::area::{area, contains};
22
use crate::error::{new_error, ErrorKind, Result};
3-
use geo_types::{LineString, MultiPolygon, Polygon};
3+
use geo_types::{LineString, MultiLineString, MultiPolygon, Polygon};
44
use lazy_static::lazy_static;
55
use rustc_hash::FxHashMap;
66
use slab::Slab;
@@ -140,8 +140,58 @@ impl ContourBuilder {
140140
.for_each(drop);
141141
}
142142

143+
/// Computes isolines according the given input `values` and the given `thresholds`.
144+
/// Returns a `Vec` of [`Line`] (that can easily be transformed
145+
/// to GeoJSON Features of MultiLineString).
146+
/// The threshold value of each Feature is stored in its `value` property.
147+
///
148+
/// # Arguments
149+
///
150+
/// * `values` - The slice of values to be used.
151+
/// * `thresholds` - The slice of thresholds values to be used.
152+
pub fn lines(&self, values: &[f64], thresholds: &[f64]) -> Result<Vec<Line>> {
153+
if values.len() as u32 != self.dx * self.dy {
154+
return Err(new_error(ErrorKind::BadDimension));
155+
}
156+
let mut isoring = IsoRingBuilder::new(self.dx, self.dy);
157+
thresholds
158+
.iter()
159+
.map(|threshold| self.line(values, *threshold, &mut isoring))
160+
.collect()
161+
}
162+
163+
fn line(&self, values: &[f64], threshold: f64, isoring: &mut IsoRingBuilder) -> Result<Line> {
164+
let mut result = isoring.compute(values, threshold)?;
165+
let mut linestrings = Vec::new();
166+
167+
result
168+
.drain(..)
169+
.map(|mut ring| {
170+
// Smooth the ring if needed
171+
if self.smooth {
172+
self.smoooth_linear(&mut ring, values, threshold);
173+
}
174+
// Compute the polygon coordinates according to the grid properties if needed
175+
if (self.x_origin, self.y_origin) != (0f64, 0f64)
176+
|| (self.x_step, self.y_step) != (1f64, 1f64)
177+
{
178+
ring.iter_mut()
179+
.map(|point| {
180+
point.x = point.x * self.x_step + self.x_origin;
181+
point.y = point.y * self.y_step + self.y_origin;
182+
})
183+
.for_each(drop);
184+
}
185+
linestrings.push(LineString(ring));
186+
}).for_each(drop);
187+
Ok(Line {
188+
geometry: MultiLineString(linestrings),
189+
threshold,
190+
})
191+
}
143192
/// Computes contours according the given input `values` and the given `thresholds`.
144-
/// Returns a `Vec` of GeoJSON Features of MultiPolygon.
193+
/// Returns a `Vec` of [`Contour`] (that can easily be transformed
194+
/// to GeoJSON Features of MultiPolygon).
145195
/// The threshold value of each Feature is stored in its `value` property.
146196
///
147197
/// # Arguments
@@ -213,6 +263,71 @@ impl ContourBuilder {
213263
}
214264
}
215265

266+
/// A line has the geometry and threshold of a contour ring, built by [`ContourBuilder`].
267+
#[derive(Debug, Clone)]
268+
pub struct Line {
269+
geometry: MultiLineString,
270+
threshold: f64,
271+
}
272+
273+
impl Line {
274+
/// Borrow the [`MultiPolygon`](geo_types::MultiPolygon) geometry of this contour.
275+
pub fn geometry(&self) -> &MultiLineString {
276+
&self.geometry
277+
}
278+
279+
/// Get the owned polygons and threshold of this countour.
280+
pub fn into_inner(self) -> (MultiLineString, f64) {
281+
(self.geometry, self.threshold)
282+
}
283+
284+
/// Get the threshold used to construct this contour.
285+
pub fn threshold(&self) -> f64 {
286+
self.threshold
287+
}
288+
289+
#[cfg(feature = "geojson")]
290+
/// Convert the line to a struct from the `geojson` crate.
291+
///
292+
/// To get a string representation, call to_geojson().to_string().
293+
/// ```
294+
/// use contour::ContourBuilder;
295+
///
296+
/// let builder = ContourBuilder::new(10, 10, false);
297+
/// # #[rustfmt::skip]
298+
/// let contours = builder.lines(&[
299+
/// // ...ellided for brevity
300+
/// # 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
301+
/// # 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
302+
/// # 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
303+
/// # 0., 0., 0., 2., 1., 2., 0., 0., 0., 0.,
304+
/// # 0., 0., 0., 2., 2., 2., 0., 0., 0., 0.,
305+
/// # 0., 0., 0., 1., 2., 1., 0., 0., 0., 0.,
306+
/// # 0., 0., 0., 2., 2., 2., 0., 0., 0., 0.,
307+
/// # 0., 0., 0., 2., 1., 2., 0., 0., 0., 0.,
308+
/// # 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
309+
/// # 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.
310+
/// ], &[0.5]).unwrap();
311+
///
312+
/// let geojson_string = contours[0].to_geojson().to_string();
313+
///
314+
/// assert_eq!(&geojson_string[0..27], r#"{"geometry":{"coordinates":"#);
315+
/// ```
316+
pub fn to_geojson(&self) -> geojson::Feature {
317+
let mut properties = geojson::JsonObject::with_capacity(1);
318+
properties.insert("threshold".to_string(), self.threshold.into());
319+
320+
geojson::Feature {
321+
bbox: None,
322+
geometry: Some(geojson::Geometry::from(self.geometry())),
323+
id: None,
324+
properties: Some(properties),
325+
foreign_members: None,
326+
}
327+
}
328+
329+
}
330+
216331
/// A contour has the geometry and threshold of a contour ring, built by [`ContourBuilder`].
217332
#[derive(Debug, Clone)]
218333
pub struct Contour {

0 commit comments

Comments
 (0)