Skip to content

Contour polygon creation in Rust (using marching squares algorithm)

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT
Notifications You must be signed in to change notification settings

mthh/contour-rs

Repository files navigation

contour-rs

Build status GitHub Actions Build status Appveyor Docs.rs version

Computes isolines, contour polygons and isobands by applying marching squares to a rectangular array of numeric values.
Outputs ring coordinates or geo-types geometries.

The results can also easily be serialised to GeoJSON.

Note : The core of the algorithm is ported from d3-contour.


Usage

Add this to your Cargo.toml:

[dependencies]
contour = "0.13.1"

and this to your crate root:

extern crate contour;

The API exposes:

  • a ContourBuilder struct, which computes isorings coordinates for a Vec of threshold values and transform them either :

    • in Contours (a type containing the threshold value and the geometry as a MultiPolygon), or,
    • in Lines (a type containing the threshold value and the geometry as a MultiLineString).
    • in Bands (a type containing a minimum value, a maximum value and the geometry as a MultiPolygon).
  • 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).

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).

Line, Contour and Band can be serialised to GeoJSON using the geojson feature.

Example:

Without defining origin and step:

let c = ContourBuilder::new(10, 10, false); // x dim., y dim., smoothing
let res = c.contours(&vec![
    0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
    0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
    0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
    0., 0., 0., 1., 1., 1., 0., 0., 0., 0.,
    0., 0., 0., 1., 1., 1., 0., 0., 0., 0.,
    0., 0., 0., 1., 1., 1., 0., 0., 0., 0.,
    0., 0., 0., 1., 1., 1., 0., 0., 0., 0.,
    0., 0., 0., 1., 1., 1., 0., 0., 0., 0.,
    0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
    0., 0., 0., 0., 0., 0., 0., 0., 0., 0.
], &[0.5])?; // values, thresholds

With origin and step

let c = ContourBuilder::new(10, 10, true) // x dim., y dim., smoothing
    .x_step(2) // The horizontal coordinate for the origin of the grid.
    .y_step(2) // The vertical coordinate for the origin of the grid.
    .x_origin(100) // The horizontal step for the grid
    .y_origin(200); // The vertical step for the grid

let res = c.contours(&[
0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 1., 1., 0., 1., 0., 0., 0.,
0., 0., 0., 1., 1., 0., 1., 0., 0., 0.,
0., 0., 0., 1., 1., 0., 1., 0., 0., 0.,
0., 0., 0., 1., 1., 0., 1., 0., 0., 0.,
0., 0., 0., 1., 1., 0., 1., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0.
], &[0.5]).unwrap(); // values, thresholds

Using the geojson feature

The geojson feature is not enabled by default, so you need to specify it in your Cargo.toml:

[dependencies]
contour = { version = "0.13.1", features = ["geojson"] }
let c = ContourBuilder::new(10, 10, true) // x dim., y dim., smoothing
    .x_step(2) // The horizontal coordinate for the origin of the grid.
    .y_step(2) // The vertical coordinate for the origin of the grid.
    .x_origin(100) // The horizontal step for the grid
    .y_origin(200); // The vertical step for the grid

let res = c.contours(&[
0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 1., 1., 0., 1., 0., 0., 0.,
0., 0., 0., 1., 1., 0., 1., 0., 0., 0.,
0., 0., 0., 1., 1., 0., 1., 0., 0., 0.,
0., 0., 0., 1., 1., 0., 1., 0., 0., 0.,
0., 0., 0., 1., 1., 0., 1., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0.
], &[0.5]).unwrap(); // values, thresholds
println!("{:?}", res[0].to_geojson()); // prints the GeoJSON representation of the first contour

Output:

Feature {
    bbox: None,
    geometry: Some(Geometry {
        bbox: None,
        value: MultiPolygon([
            [[
                [110.0, 215.0], [110.0, 213.0], [110.0, 211.0], [110.0, 209.0],
                [110.0, 207.0], [109.0, 206.0], [107.0, 206.0], [106.0, 207.0],
                [106.0, 209.0], [106.0, 211.0], [106.0, 213.0], [106.0, 215.0],
                [107.0, 216.0], [109.0, 216.0], [110.0, 215.0]
            ]],
            [[
                [114.0, 215.0], [114.0, 213.0], [114.0, 211.0], [114.0, 209.0],
                [114.0, 207.0], [113.0, 206.0], [112.0, 207.0], [112.0, 209.0],
                [112.0, 211.0], [112.0, 213.0], [112.0, 215.0], [113.0, 216.0],
                [114.0, 215.0]
            ]]
        ]),
        foreign_members: None
    }),
    id: None,
    properties: Some({"threshold": Number(0.5)}),
    foreign_members: None
}

Using the f32 feature

By default, this crate expects f64 values as input and uses f64 values for its computations. If you want to use f32 values instead, you need to specify the f32 feature in your Cargo.toml:

[dependencies]
contour = { version = "0.13.1", features = ["f32"] }

WASM demo

Demo of this crate compiled to WebAssembly and used from JavaScript : wasm_demo_contour.

Difference with the contour-isobands crate (from mthh/contour-isobands-rs repository)

While this crate computes isolines (cf. wikipedia:Marching_squares) from which it derives contour polygons (i.e. polygons that contain all points above the threshold defined for a given isoline) and isobands (i.e. polygons that contain all points between a minimum and a maximum bound), contour-isobands-rs is only dedicated to compute isobands and use a slightly different implementation of the marching squares algorithm for the disambiguation of saddle points (cf. wikipedia:Marching_squares).

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.