Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to write a vector of polygons? #161

Closed
Robinlovelace opened this issue Mar 27, 2021 · 12 comments
Closed

How to write a vector of polygons? #161

Robinlovelace opened this issue Mar 27, 2021 · 12 comments

Comments

@Robinlovelace
Copy link

Apologies if I'm missing something obvious but I'm working on a project to try to learn Rust and cannot see how to save a list of polygons.

A single polygon works fine with the following code:

fn main() {
    let polygon_list = clockboard(Point::new(0.0, 0.0), Params::default(), None);
    let geojson_list = geojson::Value::from(&polygon_list[0]);
    println!("{}", geojson_list);
}

Which outputs

{"coordinates":[[[1.0,0.0],[0.866025,0.499999],[0.5,0.866025],[0.0,1.0],[-0.499999,0.866025],[-0.866025,0.5],[-1.0,0.0],[-0.866025,-0.499999],[-0.5,-0.866025],[-0.0,-1.0],[0.499999,-0.866025],[0.866025,-0.5],[1.0,0.0]]],"type":"Polygon"}

And so does the next feature when the relevant line is set to:

    let geojson_list = geojson::Value::from(&polygon_list[1]);

But the following fails:

    let geojson_list = geojson::Value::from(&polygon_list);

With the following error message:

cargo run 
...
error[E0277]: the trait bound `geojson::Value: std::convert::From<&std::vec::Vec<geo::Polygon<f64>>>` is not satisfied
 --> src/main.rs:7:24
  |
7 |     let geojson_list = geojson::Value::from(&polygon_list);
  |                        ^^^^^^^^^^^^^^^^^^^^ the trait `std::convert::From<&std::vec::Vec<geo::Polygon<f64>>>` is not implemented for `geojson::Value`

Context: zonebuilders/zonebuilder-rust#18

@Robinlovelace
Copy link
Author

Related question: how can the result be made to print with 'pretty' GeoJSON formatting? Sure it's possible but not quite sure how even after reading some of the docs here: https://docs.rs/geojson/0.22.0/geojson/#writing

@urschrei
Copy link
Member

serde_json has a to_string_pretty() method (https://docs.serde.rs/serde_json/fn.to_string_pretty.html) so you should be able to do to_string_pretty(&geojson). See here for indentation options: https://stackoverflow.com/a/49087292

@Robinlovelace
Copy link
Author

Thanks for the comments and links, very useful. Happy to say that the pretty json printing is working 🎉

Unfortunately when it does print, the geojson specific elements like the "coordinates" bit seem to get lost. Here's the code I'm running:

use geo::Point;
use serde_json::to_string_pretty;
use zonebuilder::clockboard;
use zonebuilder::Params;

fn main() {
    let polygon_list = clockboard(Point::new(0.0, 0.0), Params::default());

    // This works but only restults in 1 polygon:
    let geojson_list = geojson::Value::from(&polygon_list[0]);
    println!("{}", geojson_list);

    // This fails - cannot handle more than 1 polygon it seems and there's no unwrap method I can see:
    // let geojson_list = geojson::Value::from(&polygon_list);
    // println!("{}", geojson_list);
    
    // serde json version gives:     [  [   [   1.0,  0.0   ], (no 'geo')
    let result = serde_json::to_string_pretty(&geojson_list);
    println!("{}", result.unwrap());

    // Trying to get it working with json  to_string_pretty:
    // let geojson_unwrapped = polygon_list.unwrap()
    // println!("{}", geojson_unwrapped);

}

And here's the result showing the 2 types of output:

cargo run
   Compiling zonebuilder v0.1.0 (/home/robin/github-orgs/zonebuilders/zonebuilder-rust)
warning: unused import: `serde_json::to_string_pretty`
 --> src/main.rs:2:5
  |
2 | use serde_json::to_string_pretty;
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: 1 warning emitted

    Finished dev [unoptimized + debuginfo] target(s) in 0.50s
     Running `target/debug/zonebuilder`
{"coordinates":[[[1.0,0.0],[0.866025,0.499999],[0.5,0.866025],[0.0,1.0],[-0.499999,0.866025],[-0.866025,0.5],[-1.0,0.0],[-0.866025,-0.499999],[-0.5,-0.866025],[-0.0,-1.0],[0.499999,-0.866025],[0.866025,-0.5],[1.0,0.0]]],"type":"Polygon"}
[
  [
    [
      1.0,
      0.0
    ],
    [
      0.866025,
      0.499999
    ],
...

@Robinlovelace
Copy link
Author

Update, the following produces a better result, but this is not valid GeoJSON:

fn main() {
    let polygon_list = clockboard(Point::new(0.0, 0.0), Params::default());
    // See https://github.com/georust/geojson/issues/161 for details
    let result = serde_json::to_string_pretty(&polygon_list);
    println!("{}", result.unwrap());
}
cargo run
   Compiling zonebuilder v0.1.0 (/home/robin/github-orgs/zonebuilders/zonebuilder-rust)
warning: unused import: `serde_json::to_string_pretty`
 --> src/main.rs:2:5
  |
2 | use serde_json::to_string_pretty;
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: 1 warning emitted

    Finished dev [unoptimized + debuginfo] target(s) in 0.40s
     Running `target/debug/zonebuilder`
[
  {
    "exterior": [
      {
        "x": 1.0,
        "y": 0.0
      },
      {
        "x": 0.866025,
        "y": 0.499999
      },
  ...

@Robinlovelace
Copy link
Author

And trying to convert it into a geojson object first also fails:

fn main() {
    let polygon_list = clockboard(Point::new(0.0, 0.0), Params::default());
    // See https://github.com/georust/geojson/issues/161 for details
    let geojson_list = geojson::Value::from(&polygon_list);
    let result = serde_json::to_string_pretty(&geojson_list);
    println!("{}", result.unwrap());
}

With the following message:

error[E0277]: the trait bound `geojson::Value: std::convert::From<&std::vec::Vec<geo::Polygon<f64>>>` is not satisfied
 --> src/main.rs:9:24
  |
9 |     let geojson_list = geojson::Value::from(&polygon_list);
  |                        ^^^^^^^^^^^^^^^^^^^^ the trait `std::convert::From<&std::vec::Vec<geo::Polygon<f64>>>` is not implemented for `geojson::Value`

Guess: I need to iterate over each polygon, e.g. with a for loop. Wonder if there is another solution. Apologies for all the comments/questions and thanks in advance for the support!

@Robinlovelace
Copy link
Author

Good news: I've succeeded in writing a vector polygons, but using the brute force approach of a for loop and no prettified GeoJSON output.

fn main() {
    let polygon_list = clockboard(Point::new(0.0, 0.0), Params::default());

    // Attempt to print pretty json - not outputting valid json currently
    // See https://github.com/georust/geojson/issues/161 for details
    // let geojson_list = geojson::Value::from(&polygon_list[0]);
    // let result = serde_json::to_string_pretty(&geojson_list);
    // println!("{}", result.unwrap());

    // Brute force approach: for loop
    for polygon in polygon_list {
        let result = geojson::Value::from(&polygon);
        println!("{}", result);
    }
}

@urschrei
Copy link
Member

The reason that this is somewhat convoluted is because GeoJSON uses nested values: a FeatureCollection (which is what you ultimately want to produce most of the time) is made up of Features and their associated properties. The Features in turn are made up of Geometry objects and their associated properties.

There's a relatively close correspondence between geo geometries (LineString, Polygon etc.) and geojson Geometry objects, and you can convert between them. In more abstract terms, the idea is that you figure out which geojson object most closely maps onto the geo objects you have. You convert those geo objects into geojson objects, and use the result as a building block for the next geojson object "up" the chain:

Geometry -> Feature -> FeatureCollection

In our case, we have Vec of geo geometries.

First, we need to convert those into geojson Geometry objects, then to Features. We can do this in a single step using an iterator. Note that you'll have to enable the geo_types feature in the geojson crate in your Cargo.toml, in order for the Geometry::from() call to work:

[dependencies]
geojson = { version = "0.22.0", features = ["geo-types"] }

Anyway, on to the code:

use geojson::{GeoJson, Feature, FeatureCollection, Geometry};
use serde_json;
use serde_json::{to_string_pretty};

let mut features: Vec<Feature> = polygon_list
    .iter()
    .map(|poly| Feature {
        bbox: None,
        geometry: Some(Geometry::from(poly)),
        id: None,
        properties: None,
        foreign_members: None,
    })
    .collect();

The iterator is a bit like a for loop, but with some advantages. It traverses our vec of geo polygons, mapping the Geometry::from() method over each borrowed polygon, collecting the results into a new Vec containing geojson Feature objects. Note the bbox, id, properties, and foreign_members fields which are all optional (as per the GeoJSON spec). In most cases, the only change you might wish to make is populating the properties or id fields, but we won't worry about that for now – if we did need to add properties, it would be a two-step process.

Now that we have a Vec of features, we can use that to build a FeatureCollection:

let fc = FeatureCollection {
    bbox: None,
    features,
    foreign_members: None,
};

Again, note that there are some fields we're leaving blank here by using None.

We're now (finally!) ready to build a GeoJson object:

let gj = GeoJson::from(fc);

…which we can borrow to produce a pretty-printed string:

let gjstring = to_string_pretty(&gj);

Which we can print / dump / etc:

println!("{}", &gjstring);

Robinlovelace added a commit to zonebuilders/zonebuilder-rust that referenced this issue Mar 28, 2021
@Robinlovelace
Copy link
Author

Fantastic, many thanks @urschrei. I was just about to write another comment explaining that it almost worked but failed at the last hurdle but, remembering something about unwrap gave this updated final line a go and am stoked to say it works 🎉

@Robinlovelace
Copy link
Author

Issue is closed! As a quick follow-up and as an exercise in reproducibility please try to reproduce the following to check it works.

git clone https://github.com/zonebuilders/zonebuilder-rust.git
cd zonebuilder-rust
git checkout circles
## Cloning into 'zonebuilder-rust'...
## Switched to a new branch 'circles'
## Branch 'circles' set up to track remote branch 'circles' from 'origin'.

Run the CLI:

cargo run > circle.geojson
## warning: variable does not need to be mutable
##   --> src/lib.rs:55:9
##    |
## 55 |     let mut features: Vec<Feature> = polygons
##    |         ----^^^^^^^^
##    |         |
##    |         help: remove this `mut`
##    |
##    = note: `#[warn(unused_mut)]` on by default
## 
## warning: 1 warning emitted
## 
##    Compiling zonebuilder v0.1.0 (/home/robin/github-orgs/zonebuilders/zonebuilder-rust)
##     Finished dev [unoptimized + debuginfo] target(s) in 0.37s
##      Running `target/debug/zonebuilder`

Take a look at the output:

head -c 80 circle.geojson
## {
##   "features": [
##     {
##       "geometry": {
##         "coordinates": [
##           [

Then read in the GeoJSON file with another tool, e.g. R:

circle = sf::read_sf("circle.geojson")
plot(circle)

image

bors bot added a commit that referenced this issue Apr 1, 2021
162: Provide additional conversions from `Geometry` and `Value` to `Feature` r=michaelkirk a=urschrei

While addressing #161, I once again had the feeling that this crate can be made of papercuts sometimes. Based on the assumption that one of the primary uses of GeoJSON as a format is the production of `FeatureCollection` objects, this PR provides some additional conversions from `Value` enum members and `Geometry` objects to `Feature` objects.
<s>I'm also considering an additional `From` impl to construct a `FeatureCollection` from a `geo_types::GeometryCollection`</s> Done!

Co-authored-by: Stephan Hügel <shugel@tcd.ie>
@Robinlovelace
Copy link
Author

Heads-up @urschrei and @michaelkirk you'll be pleased to see, this fix is incorporated in the zonebuilder repo (in a branch at least): https://github.com/zonebuilders/zonebuilder-rust/pull/22/files

@michaelkirk
Copy link
Member

Thanks @Robinlovelace!

We've just now published these changes to crates.io, so you could update your Cargo.toml to use geojson = "0.22.2" rather than the Github version.

Robinlovelace added a commit to zonebuilders/zonebuilder-rust that referenced this issue Apr 20, 2021
@Robinlovelace
Copy link
Author

Thanks Michael, zonebuilder-rust is now updated!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants