├── .gitignore ├── Cargo.toml ├── src ├── borrowed.rs ├── owned.rs ├── borrowed_modify.rs └── test.geojson ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "geojson_example" 3 | version = "0.7.0" 4 | authors = ["Stephan Hügel "] 5 | license = "Blue Oak Model License 1.0.0" 6 | readme = "README.md" 7 | repository = "https://github.com/urschrei/geojson_example" 8 | keywords = ["geo", "geojson", "gis"] 9 | edition = "2021" 10 | 11 | [dependencies] 12 | geo = "0.23.1" 13 | geo-types = "0.7.8" 14 | geojson = { version = "0.24.0", features=["geo-types"] } 15 | serde_json = "1.0" 16 | rayon = "1.6" 17 | 18 | [[bin]] 19 | name = "borrowed" 20 | path = "src/borrowed.rs" 21 | 22 | [[bin]] 23 | name = "owned" 24 | path = "src/owned.rs" 25 | 26 | [[bin]] 27 | name = "borrowed_modify" 28 | path = "src/borrowed_modify.rs" 29 | -------------------------------------------------------------------------------- /src/borrowed.rs: -------------------------------------------------------------------------------- 1 | use geojson::{GeoJson, Geometry, Value}; 2 | use rayon::prelude::*; 3 | 4 | /// Process GeoJSON geometries 5 | fn match_geometry(geom: &Geometry) { 6 | match geom.value { 7 | Value::Polygon(_) => println!("Matched a Polygon"), 8 | Value::MultiPolygon(_) => println!("Matched a MultiPolygon"), 9 | Value::GeometryCollection(ref collection) => { 10 | println!("Matched a GeometryCollection"); 11 | // GeometryCollections contain other Geometry types, and can nest 12 | // we deal with this by recursively processing each geometry 13 | collection.par_iter().for_each(match_geometry) 14 | } 15 | // Point, LineString, and their Multi– counterparts 16 | _ => println!("Matched some other geometry"), 17 | } 18 | } 19 | 20 | /// Process top-level GeoJSON items 21 | fn process_geojson(gj: &GeoJson) { 22 | match *gj { 23 | GeoJson::FeatureCollection(ref collection) => collection 24 | .features 25 | // Iterate in parallel when appropriate 26 | .par_iter() 27 | // Only pass on non-empty geometries, doing so by reference 28 | .filter_map(|feature| feature.geometry.as_ref()) 29 | .for_each(match_geometry), 30 | GeoJson::Feature(ref feature) => { 31 | if let Some(ref geometry) = feature.geometry { 32 | match_geometry(geometry) 33 | } 34 | } 35 | GeoJson::Geometry(ref geometry) => match_geometry(geometry), 36 | } 37 | } 38 | 39 | fn main() { 40 | let geojson_str = include!("test.geojson"); 41 | let geojson = geojson_str.parse::().unwrap(); 42 | process_geojson(&geojson); 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2017 by Stephan Hügel 2 | 3 | # Blue Oak Model License 4 | 5 | Version 1.0.0 6 | 7 | ## Purpose 8 | 9 | This license gives everyone as much permission to work with 10 | this software as possible, while protecting contributors 11 | from liability. 12 | 13 | ## Acceptance 14 | 15 | In order to receive this license, you must agree to its 16 | rules. The rules of this license are both obligations 17 | under that agreement and conditions to your license. 18 | You must not do anything with this software that triggers 19 | a rule that you cannot or will not follow. 20 | 21 | ## Copyright 22 | 23 | Each contributor licenses you to do everything with this 24 | software that would otherwise infringe that contributor's 25 | copyright in it. 26 | 27 | ## Notices 28 | 29 | You must ensure that everyone who gets a copy of 30 | any part of this software from you, with or without 31 | changes, also gets the text of this license or a link to 32 | . 33 | 34 | ## Excuse 35 | 36 | If anyone notifies you in writing that you have not 37 | complied with [Notices](#notices), you can keep your 38 | license by taking all practical steps to comply within 30 39 | days after the notice. If you do not do so, your license 40 | ends immediately. 41 | 42 | ## Patent 43 | 44 | Each contributor licenses you to do everything with this 45 | software that would otherwise infringe any patent claims 46 | they can license or become able to license. 47 | 48 | ## Reliability 49 | 50 | No contributor can revoke this license. 51 | 52 | ## No Liability 53 | 54 | ***As far as the law allows, this software comes as is, 55 | without any warranty or condition, and no contributor 56 | will be liable to anyone for any damages related to this 57 | software or this license, under any kind of legal claim.*** 58 | -------------------------------------------------------------------------------- /src/owned.rs: -------------------------------------------------------------------------------- 1 | use geo::algorithm::centroid::Centroid; 2 | use geo_types::Polygon; 3 | use geojson::{GeoJson, Geometry, Value}; 4 | use rayon::prelude::*; 5 | use std::convert::TryInto; 6 | 7 | /// Process GeoJSON geometries 8 | fn match_geometry(geom: Geometry) { 9 | match geom.value { 10 | Value::Polygon(_) => { 11 | let poly: Polygon = geom.value.try_into().expect("Unable to convert Polygon"); 12 | let centroid = poly.centroid().unwrap(); 13 | println!( 14 | "Matched a Polygon with centroid ({}, {})", 15 | centroid.x(), 16 | centroid.y() 17 | ); 18 | } 19 | Value::MultiPolygon(_) => println!("Matched a MultiPolygon"), 20 | Value::GeometryCollection(collection) => { 21 | println!("Matched a GeometryCollection"); 22 | // GeometryCollections contain other Geometry types, and can nest 23 | // we deal with this by recursively processing each geometry 24 | collection.into_par_iter().for_each(match_geometry) 25 | } 26 | // Point, LineString, and their Multi– counterparts 27 | _ => println!("Matched some other geometry"), 28 | } 29 | } 30 | 31 | /// Process top-level GeoJSON items 32 | fn process_geojson(gj: GeoJson) { 33 | match gj { 34 | GeoJson::FeatureCollection(collection) => collection 35 | .features 36 | // Iterate in parallel where appropriate 37 | .into_par_iter() 38 | // Only pass on non-empty geometries 39 | .filter_map(|feature| feature.geometry) 40 | .for_each(match_geometry), 41 | GeoJson::Feature(feature) => { 42 | if let Some(geometry) = feature.geometry { 43 | match_geometry(geometry) 44 | } 45 | } 46 | GeoJson::Geometry(geometry) => match_geometry(geometry), 47 | } 48 | } 49 | 50 | fn main() { 51 | let geojson_str = include!("test.geojson"); 52 | let geojson = geojson_str.parse::().unwrap(); 53 | process_geojson(geojson); 54 | } 55 | -------------------------------------------------------------------------------- /src/borrowed_modify.rs: -------------------------------------------------------------------------------- 1 | use std::mem::replace; 2 | 3 | use geo::algorithm::convex_hull::ConvexHull; 4 | use geo_types::{LineString, Point, Polygon}; 5 | use geojson::{GeoJson, Geometry, Value}; 6 | use rayon::prelude::*; 7 | use serde_json::to_string_pretty; 8 | use std::convert::TryInto; 9 | 10 | /// Process top-level `GeoJSON` items 11 | fn process_geojson(gj: &mut GeoJson) { 12 | match *gj { 13 | GeoJson::FeatureCollection(ref mut collection) => collection 14 | .features 15 | .par_iter_mut() 16 | // Only pass on non-empty geometries 17 | .filter_map(|feature| feature.geometry.as_mut()) 18 | .for_each(process_geometry), 19 | GeoJson::Feature(ref mut feature) => { 20 | if let Some(ref mut geometry) = feature.geometry { 21 | process_geometry(geometry) 22 | } 23 | } 24 | GeoJson::Geometry(ref mut geometry) => process_geometry(geometry), 25 | } 26 | } 27 | 28 | /// Process `GeoJSON` Geometries 29 | fn process_geometry(geom: &mut Geometry) { 30 | match geom.value { 31 | // Only modify Polygon geometries 32 | Value::Polygon(_) => calculate_hull(Some(geom)), 33 | Value::GeometryCollection(ref mut collection) => { 34 | // GeometryCollections contain other Geometry types, and can nest 35 | // we deal with this by recursively processing each geometry 36 | collection.par_iter_mut().for_each(process_geometry) 37 | } 38 | // Point, LineString, and their Multi– counterparts 39 | _ => (), 40 | } 41 | } 42 | 43 | /// Modify a Polygon geometry by mutating its shell into its convex hull 44 | fn calculate_hull(geom: Option<&mut Geometry>) { 45 | if let Some(gmt) = geom { 46 | // construct a placeholder empty Polygon – this doesn't allocate 47 | let shell: Vec> = Vec::new(); 48 | let rings = Vec::new(); 49 | let fake_polygon: Polygon = Polygon::new(LineString::from(shell), rings); 50 | // convert it into a Value, and swap it for the actual Polygon 51 | let intermediate = replace(&mut gmt.value, Value::from(&fake_polygon)); 52 | let mut geo_type: Polygon = intermediate.try_into().unwrap(); 53 | // modify the borrowed, converted Value 54 | geo_type = geo_type.convex_hull(); 55 | // and put it back 56 | gmt.value = Value::from(&geo_type); 57 | } 58 | } 59 | 60 | fn main() { 61 | let geojson_str = include!("test.geojson"); 62 | let mut geojson = geojson_str.parse::().unwrap(); 63 | process_geojson(&mut geojson); 64 | println!("{}", to_string_pretty(&geojson).unwrap()); 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Parsing GeoJSON using Rust 2 | These are three minimal examples demonstrating GeoJSON parsing using Rust. In order to run them, you'll need at least Stable Rust 1.34. In most cases, the easiest way to do this is using [Rustup](https://rustup.rs). 3 | 4 | If you aren't familiar with it, it may be helpful to familiarise yourself with the [GeoJSON spec](https://tools.ietf.org/html/rfc7946), as this should make it obvious why e.g. `Feature` geometries in [`rust-geojson`](https://docs.rs/geojson/0.9.1/geojson/struct.Feature.html) are `Option`. 5 | 6 | The example GeoJSON used is deliberately baroque: `GeometryCollection` isn't in wide use, and the use of nested `GeometryCollection`s is discouraged by the spec, being all but unknown "in the wild". Nevertheless, if we need to e.g. extract all `Polygon` objects in an arbitrary GeoJSON file, we have to be able to process them – this turns out to be relatively painless using a recursive function. 7 | 8 | The example code could be more minimal, but this is an ideal use case for [Rayon](https://docs.rs/rayon/) in order to parallelise the processing, so iterators have been substituted for `for { … }` loops to faciliate its use, leading to the requirement for Rust 1.21 or later. If you'd prefer to use `for` loops and avoid iterators, the [`plain`](https://github.com/urschrei/geojson_example/tree/plain) branch is available. 9 | 10 | ## Approach 11 | Three different approaches to parsing GeoJSON are shown: 12 | 1. [`borrowed.rs`](src/borrowed.rs) shows parsing using only borrowed data, and does not consume the GeoJSON, clone any part of it, or allocate – you're free to use `geojson` again as soon as `process_geojson` returns. Run it using cargo: `cargo run --bin borrowed` 13 | 2. [`owned.rs`](src/owned.rs) shows parsing and conversion to [`Geo`](https://docs.rs/geo) types, which necessarily consumes the GeoJSON, as `Geo`'s primitives currently require owned data. To faciliate conversions of this kind,`rust-geojson` provides the `conversion::try_into` trait for this on its `Value` structs. This approach is the most flexible, especially if you need to add or remove data, as opposed to modifying it. Run it using cargo: `cargo run --bin owned` 14 | 3. [`borrowed_modify.rs`](src/borrowed_modify.rs) (Run it using cargo: `cargo run --bin borrowed_modify`) shows parsing and modification of geometries using only mutably borrowed data. The core idea can be seen in `calculate_hull()`: ordinarily, `take()` could be used to remove the `T` from an `Option` – in this case a `Geometry` – convert its `value` to a `Geo` type, and modify it before replacing it. However, this approach will only work for the `Feature` type; input which contains top-level `Geometry` and/or `GeometryCollection` types can't be modified in this way, due to the lack of `Option`. However, a more general approach involving [`std::mem::replace`](https://doc.rust-lang.org/std/mem/fn.replace.html), is possible: 15 | 1. We match against `&mut Geometry` (which all [`GeoJson` enum](https://docs.rs/geojson/0.9.1/geojson/enum.GeoJson.html) variants are guaranteed to have) on its `value`, wrapping *that* in an `Option` 16 | 2. This is passed to the conversion function 17 | 3. In the conversion function, a non-allocating `Geo` placeholder geometry is constructed, converted into a `Value`, and swapped for the `Value` we wish to modify, using `replace`. The original geometry can then be modified or freely used, before being swapped back into the borrowed `Geometry.value`, before the function returns. 18 | 19 | ## Further Work 20 | The [`polylabel_cmd`](https://github.com/urschrei/polylabel_cmd) crate contains more advanced parsing and conversion code which has the same structure as these examples. 21 | 22 | ## License 23 | [Blue Oak Model License 1.0.0](LICENSE.md) 24 | 25 | -------------------------------------------------------------------------------- /src/test.geojson: -------------------------------------------------------------------------------- 1 | r#"{ 2 | "geometries": [ 3 | { 4 | "coordinates": [ 5 | 0, 6 | 1 7 | ], 8 | "type": "Point" 9 | }, 10 | { 11 | "coordinates": [ 12 | [ 13 | -1, 14 | 0 15 | ], 16 | [ 17 | 1, 18 | 0 19 | ] 20 | ], 21 | "type": "MultiPoint" 22 | }, 23 | { 24 | "coordinates": [ 25 | [ 26 | -1, 27 | -1 28 | ], 29 | [ 30 | 1, 31 | -1 32 | ] 33 | ], 34 | "type": "LineString" 35 | }, 36 | { 37 | "coordinates": [ 38 | [ 39 | [ 40 | -2, 41 | -2 42 | ], 43 | [ 44 | 2, 45 | -2 46 | ] 47 | ], 48 | [ 49 | [ 50 | -3, 51 | -3 52 | ], 53 | [ 54 | 3, 55 | -3 56 | ] 57 | ] 58 | ], 59 | "type": "MultiLineString" 60 | }, 61 | { 62 | "coordinates": [ 63 | [ 64 | [ 65 | -5, 66 | -5 67 | ], 68 | [ 69 | 5, 70 | -5 71 | ], 72 | [ 73 | 0, 74 | 5 75 | ], 76 | [ 77 | -5, 78 | -5 79 | ] 80 | ], 81 | [ 82 | [ 83 | -4, 84 | -4 85 | ], 86 | [ 87 | 0, 88 | 4 89 | ], 90 | [ 91 | 4, 92 | -4 93 | ], 94 | [ 95 | -4, 96 | -4 97 | ] 98 | ] 99 | ], 100 | "type": "Polygon" 101 | }, 102 | { 103 | "coordinates": [ 104 | [ 105 | [ 106 | [ 107 | -7, 108 | -7 109 | ], 110 | [ 111 | 7, 112 | -7 113 | ], 114 | [ 115 | 0, 116 | 7 117 | ], 118 | [ 119 | -7, 120 | -7 121 | ] 122 | ], 123 | [ 124 | [ 125 | -6, 126 | -6 127 | ], 128 | [ 129 | 0, 130 | 6 131 | ], 132 | [ 133 | 6, 134 | -6 135 | ], 136 | [ 137 | -6, 138 | -6 139 | ] 140 | ] 141 | ], 142 | [ 143 | [ 144 | [ 145 | -9, 146 | -9 147 | ], 148 | [ 149 | 9, 150 | -9 151 | ], 152 | [ 153 | 0, 154 | 9 155 | ], 156 | [ 157 | -9, 158 | -9 159 | ] 160 | ], 161 | [ 162 | [ 163 | -8, 164 | -8 165 | ], 166 | [ 167 | 0, 168 | 8 169 | ], 170 | [ 171 | 8, 172 | -8 173 | ], 174 | [ 175 | -8, 176 | -8 177 | ] 178 | ] 179 | ] 180 | ], 181 | "type": "MultiPolygon" 182 | }, 183 | { 184 | "geometries": [ 185 | { 186 | "coordinates": [ 187 | [ 188 | [ 189 | -5.5, 190 | -5.5 191 | ], 192 | [ 193 | 5, 194 | -5 195 | ], 196 | [ 197 | 0, 198 | 5 199 | ], 200 | [ 201 | -5, 202 | -5 203 | ], 204 | [ 205 | -5.5, 206 | -5.5 207 | ] 208 | ], 209 | [ 210 | [ 211 | -4, 212 | -4 213 | ], 214 | [ 215 | -4.5, 216 | -4.5 217 | ], 218 | [ 219 | 0, 220 | 4 221 | ], 222 | [ 223 | 4, 224 | -4 225 | ], 226 | [ 227 | -4, 228 | -4 229 | ] 230 | ] 231 | ], 232 | "type": "Polygon" 233 | } 234 | ], 235 | "type": "GeometryCollection" 236 | } 237 | ], 238 | "type": "GeometryCollection" 239 | }"# 240 | --------------------------------------------------------------------------------