From e6268fd1caf243decfa902f5f7331f03ca9bf5a1 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Wed, 27 Dec 2023 10:22:02 -0800 Subject: [PATCH 1/7] docstrings --- geozero/src/feature_processor.rs | 28 +++++++++++ geozero/src/geometry_processor.rs | 84 ++++++++++++++++++++++++++++--- geozero/src/property_processor.rs | 6 +++ 3 files changed, 111 insertions(+), 7 deletions(-) diff --git a/geozero/src/feature_processor.rs b/geozero/src/feature_processor.rs index 2b8554a9..02aff8dc 100644 --- a/geozero/src/feature_processor.rs +++ b/geozero/src/feature_processor.rs @@ -6,22 +6,46 @@ use crate::property_processor::PropertyProcessor; #[allow(unused_variables)] pub trait FeatureProcessor: GeomProcessor + PropertyProcessor { /// Begin of dataset processing + /// + /// ## Invariants + /// + /// - `dataset_begin` is called _only once_ for an entire dataset. + /// - `dataset_begin` is called before all other methods, including `feature_begin`, + /// `properties_begin`, `geometry_begin`, and all methods from [`GeomProcessor`] and + /// [`PropertyProcessor`] fn dataset_begin(&mut self, name: Option<&str>) -> Result<()> { Ok(()) } /// End of dataset processing + /// + /// ## Invariants + /// + /// - `dataset_end` is called _only once_ for an entire dataset. + /// - No other methods may be called after `dataset_end`. fn dataset_end(&mut self) -> Result<()> { Ok(()) } /// Begin of feature processing + /// + /// - `idx` refers to the positional row index in the dataset. For the `n`th row, `idx` will be + /// `n`. + /// - `feature_begin` will be called before both `properties_begin` and `geometry_begin`. fn feature_begin(&mut self, idx: u64) -> Result<()> { Ok(()) } /// End of feature processing + /// + /// - `idx` refers to the positional row index in the dataset. For the `n`th row, `idx` will be + /// `n`. + /// - `feature_end` will be called after both `properties_end` and `geometry_end`. fn feature_end(&mut self, idx: u64) -> Result<()> { Ok(()) } /// Begin of feature property processing + /// + /// ## Invariants + /// + /// - `properties_begin` will not be called a second time before `properties_end` is called. fn properties_begin(&mut self) -> Result<()> { Ok(()) } @@ -30,6 +54,10 @@ pub trait FeatureProcessor: GeomProcessor + PropertyProcessor { Ok(()) } /// Begin of feature geometry processing + /// + /// ## Following events + /// + /// - Relevant methods from [`GeomProcessor`] will be called for each geometry. fn geometry_begin(&mut self) -> Result<()> { Ok(()) } diff --git a/geozero/src/geometry_processor.rs b/geozero/src/geometry_processor.rs index 56873716..4e8afefa 100644 --- a/geozero/src/geometry_processor.rs +++ b/geozero/src/geometry_processor.rs @@ -103,6 +103,9 @@ pub trait GeomProcessor { } /// Process empty coordinates, like WKT's `POINT EMPTY` + /// + /// - `idx` is the positional index inside this geometry. `idx` will usually be 0 except in the + /// case of a MultiPoint or GeometryCollection. fn empty_point(&mut self, idx: usize) -> Result<()> { Err(GeozeroError::Geometry( "The input was an empty Point, but the output doesn't support empty Points".to_string(), @@ -123,21 +126,46 @@ pub trait GeomProcessor { /// Begin of MultiPoint processing /// - /// Next: size * xy/coordinate + /// Next: `size` calls to [`xy()`][`Self::xy()`] or [`coordinate()`][`Self::coordinate()`] + /// + /// ## Parameters + /// + /// - `size`: the number of Points in this MultiPoint + /// - `idx`: the positional index of this MultiPoint. This will be 0 except in the case of a + /// GeometryCollection. + /// + /// ## Following events + /// + /// - [`point_begin`][`Self::point_begin()`] for each contained point (NOTE: seems expected but seems not implemented) + /// - `size` calls to [`xy()`][`Self::xy()`] or [`coordinate()`][`Self::coordinate()`] for each point. + /// - [`point_end`][`Self::point_end()`] for each contained point + /// - [`multipoint_end`][Self::multipoint_end()] to end this MultiPoint fn multipoint_begin(&mut self, size: usize, idx: usize) -> Result<()> { Ok(()) } /// End of MultiPoint processing + /// + /// - `idx`: the positional index of this MultiPoint. This will be 0 except in the case of a + /// GeometryCollection. fn multipoint_end(&mut self, idx: usize) -> Result<()> { Ok(()) } /// Begin of `LineString` processing /// - /// An untagged `LineString` is either a Polygon ring or part of a `MultiLineString` + /// ## Parameters /// - /// Next: size * xy/coordinate + /// - `tagged`: if `false`, this `LineString` is either a Polygon ring or part of a `MultiLineString` + /// - `size`: the number of coordinates in this LineString + /// - `idx`: the positional index of this LineString. This will be 0 for a tagged LineString + /// except in the case of a GeometryCollection. This can be non-zero for an untagged + /// LineString for MultiLineStrings or Polygons with multiple interiors + /// + /// ## Following events + /// + /// - `size` calls to [`xy()`][`Self::xy()`] or [`coordinate()`][`Self::coordinate()`] for each coordinate. + /// - [`linestring_end`][Self::linestring_end()] to end this LineString fn linestring_begin(&mut self, tagged: bool, size: usize, idx: usize) -> Result<()> { Ok(()) } @@ -150,6 +178,14 @@ pub trait GeomProcessor { /// Begin of `MultiLineString` processing /// /// Next: size * LineString (untagged) + /// + /// ## Following events + /// + /// - `size` calls to: + /// - [`linestring_begin`][Self::linestring_begin] (with `tagged` set to `false`). + /// - one or more calls to [`xy()`][`Self::xy()`] or [`coordinate()`][`Self::coordinate()`] for each coordinate in the LineString. + /// - [`linestring_end`][Self::linestring_end] + /// - [`multilinestring_end`][Self::multilinestring_end()] to end this MultiLineString fn multilinestring_begin(&mut self, size: usize, idx: usize) -> Result<()> { Ok(()) } @@ -159,11 +195,23 @@ pub trait GeomProcessor { Ok(()) } - /// Begin of Polygon processing + /// Begin of `Polygon` processing /// - /// An untagged Polygon is part of a `MultiPolygon` + /// ## Parameters /// - /// Next: size * LineString (untagged) = rings + /// - `tagged`: if `false`, this `Polygon` is part of a `MultiPolygon`. + /// - `size`: the number of rings in this Polygon, _including_ the exterior ring. + /// - `idx`: the positional index of this Polygon. This will be 0 for a tagged Polygon + /// except in the case of a GeometryCollection. This can be non-zero for an untagged + /// Polygon for a MultiPolygon with multiple interiors + /// + /// ## Following events + /// + /// - `size` calls to: + /// - [`linestring_begin`][Self::linestring_begin] (with `tagged` set to `false`). + /// - one or more calls to [`xy()`][`Self::xy()`] or [`coordinate()`][`Self::coordinate()`] for each coordinate in the ring. + /// - [`linestring_end`][Self::linestring_end] + /// - [`polygon_end`][Self::polygon_end()] to end this Polygon fn polygon_begin(&mut self, tagged: bool, size: usize, idx: usize) -> Result<()> { Ok(()) } @@ -175,7 +223,19 @@ pub trait GeomProcessor { /// Begin of `MultiPolygon` processing /// - /// Next: size * Polygon (untagged) + /// ## Parameters + /// + /// - `size`: the number of Polygons in this MultiPolygon. + /// - `idx`: the positional index of this MultiPolygon. This will be 0 except in the case of a + /// GeometryCollection. + /// + /// ## Following events + /// + /// - `size` calls to: + /// - [`polygon_begin`][Self::polygon_begin] (with `tagged` set to `false`). + /// - See [`polygon_begin`][Self::polygon_begin] for its internal calls. + /// - [`polygon_end`][Self::polygon_end] + /// - [`multipolygon_end`][Self::multipolygon_end()] to end this MultiPolygon fn multipolygon_begin(&mut self, size: usize, idx: usize) -> Result<()> { Ok(()) } @@ -186,6 +246,16 @@ pub trait GeomProcessor { } /// Begin of `GeometryCollection` processing + /// + /// ## Parameters + /// + /// - `size`: the number of geometries in this GeometryCollection. + /// - `idx`: the positional index of this GeometryCollection. Unsure when this is non-zero?? Nested geometry collections? + /// + /// ## Following events + /// + /// - `size` calls to one of the internal geometry `begin` and `end` methods, called in pairs. + /// - [`geometrycollection_end`][Self::geometrycollection_end()] to end this GeometryCollection fn geometrycollection_begin(&mut self, size: usize, idx: usize) -> Result<()> { Ok(()) } diff --git a/geozero/src/property_processor.rs b/geozero/src/property_processor.rs index e0cba015..f981adf0 100644 --- a/geozero/src/property_processor.rs +++ b/geozero/src/property_processor.rs @@ -18,7 +18,9 @@ pub enum ColumnValue<'a> { Float(f32), Double(f64), String(&'a str), + /// A JSON-formatted string Json(&'a str), + /// A datetime stored as an ISO8601-formatted string DateTime(&'a str), Binary(&'a [u8]), } @@ -42,6 +44,10 @@ pub enum ColumnValue<'a> { #[allow(unused_variables)] pub trait PropertyProcessor { /// Process property value. Abort processing, if return value is true. + /// + /// - `idx` is the positional index of the column??? The row of the dataset? Unknown + /// - `name` is the name of the column + /// - `value` is the value of this field fn property(&mut self, idx: usize, name: &str, value: &ColumnValue) -> Result { Ok(true) } From 3bbbf60f2062a2f7e5bfe7973d8b9357e22cd2b6 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Wed, 27 Dec 2023 10:26:57 -0800 Subject: [PATCH 2/7] property notes --- geozero/src/property_processor.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/geozero/src/property_processor.rs b/geozero/src/property_processor.rs index f981adf0..78c27fbf 100644 --- a/geozero/src/property_processor.rs +++ b/geozero/src/property_processor.rs @@ -48,6 +48,14 @@ pub trait PropertyProcessor { /// - `idx` is the positional index of the column??? The row of the dataset? Unknown /// - `name` is the name of the column /// - `value` is the value of this field + /// + /// ## Notes: + /// + /// - It is not guaranteed that `name` is consistent across rows for the same `idx`, nor is it + /// guaranteed that the set of names in each row is the same. Some input formats, like + /// GeoJSON, are schema-less and properties may change in every row. + /// - It is not guaranteed that the data type of `name` is consistent across rows. For a given + /// `name`, it may be numeric in one row and string in the next. fn property(&mut self, idx: usize, name: &str, value: &ColumnValue) -> Result { Ok(true) } From 8d806d2809343a3fb2c001446066c388e9c62821 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Tue, 16 Jan 2024 19:16:57 -0500 Subject: [PATCH 3/7] Update geozero/src/geometry_processor.rs Co-authored-by: Michael Kirk --- geozero/src/geometry_processor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geozero/src/geometry_processor.rs b/geozero/src/geometry_processor.rs index 4e8afefa..8c0aad18 100644 --- a/geozero/src/geometry_processor.rs +++ b/geozero/src/geometry_processor.rs @@ -160,7 +160,7 @@ pub trait GeomProcessor { /// - `size`: the number of coordinates in this LineString /// - `idx`: the positional index of this LineString. This will be 0 for a tagged LineString /// except in the case of a GeometryCollection. This can be non-zero for an untagged - /// LineString for MultiLineStrings or Polygons with multiple interiors + /// LineString for MultiLineStrings or Polygons with multiple interiors. /// /// ## Following events /// From eec4574052c9ccf756459f9949eb15ffa4e6e871 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Tue, 16 Jan 2024 19:20:55 -0500 Subject: [PATCH 4/7] address comments --- geozero/src/feature_processor.rs | 4 ++-- geozero/src/geometry_processor.rs | 5 +++-- geozero/src/property_processor.rs | 7 ++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/geozero/src/feature_processor.rs b/geozero/src/feature_processor.rs index 02aff8dc..ba82c02a 100644 --- a/geozero/src/feature_processor.rs +++ b/geozero/src/feature_processor.rs @@ -27,7 +27,7 @@ pub trait FeatureProcessor: GeomProcessor + PropertyProcessor { } /// Begin of feature processing /// - /// - `idx` refers to the positional row index in the dataset. For the `n`th row, `idx` will be + /// - `idx`: the positional row index in the dataset. For the `n`th row, `idx` will be /// `n`. /// - `feature_begin` will be called before both `properties_begin` and `geometry_begin`. fn feature_begin(&mut self, idx: u64) -> Result<()> { @@ -35,7 +35,7 @@ pub trait FeatureProcessor: GeomProcessor + PropertyProcessor { } /// End of feature processing /// - /// - `idx` refers to the positional row index in the dataset. For the `n`th row, `idx` will be + /// - `idx`: the positional row index in the dataset. For the `n`th row, `idx` will be /// `n`. /// - `feature_end` will be called after both `properties_end` and `geometry_end`. fn feature_end(&mut self, idx: u64) -> Result<()> { diff --git a/geozero/src/geometry_processor.rs b/geozero/src/geometry_processor.rs index 4e8afefa..f42e8dbc 100644 --- a/geozero/src/geometry_processor.rs +++ b/geozero/src/geometry_processor.rs @@ -136,10 +136,11 @@ pub trait GeomProcessor { /// /// ## Following events /// - /// - [`point_begin`][`Self::point_begin()`] for each contained point (NOTE: seems expected but seems not implemented) /// - `size` calls to [`xy()`][`Self::xy()`] or [`coordinate()`][`Self::coordinate()`] for each point. - /// - [`point_end`][`Self::point_end()`] for each contained point /// - [`multipoint_end`][Self::multipoint_end()] to end this MultiPoint + /// + /// As of v0.12, `point_begin` and `point_end` are **not** called for each point in a + /// MultiPoint. See also discussion in [#184](https://github.com/georust/geozero/issues/184). fn multipoint_begin(&mut self, size: usize, idx: usize) -> Result<()> { Ok(()) } diff --git a/geozero/src/property_processor.rs b/geozero/src/property_processor.rs index 78c27fbf..3b32fc67 100644 --- a/geozero/src/property_processor.rs +++ b/geozero/src/property_processor.rs @@ -45,15 +45,16 @@ pub enum ColumnValue<'a> { pub trait PropertyProcessor { /// Process property value. Abort processing, if return value is true. /// - /// - `idx` is the positional index of the column??? The row of the dataset? Unknown + /// - `idx`: the positional index of the property. /// - `name` is the name of the column /// - `value` is the value of this field /// /// ## Notes: /// - /// - It is not guaranteed that `name` is consistent across rows for the same `idx`, nor is it + /// - It is not guaranteed that `idx` is consistent across rows, nor is it /// guaranteed that the set of names in each row is the same. Some input formats, like - /// GeoJSON, are schema-less and properties may change in every row. + /// GeoJSON, are schema-less and properties may change in every row. For this reason, it is + /// suggested to use the `name` parameter for matching across rows. /// - It is not guaranteed that the data type of `name` is consistent across rows. For a given /// `name`, it may be numeric in one row and string in the next. fn property(&mut self, idx: usize, name: &str, value: &ColumnValue) -> Result { From dc0f254defa07f4908d3145f385c36cc53677937 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Wed, 7 Feb 2024 23:05:23 -0500 Subject: [PATCH 5/7] reword --- geozero/src/geometry_processor.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/geozero/src/geometry_processor.rs b/geozero/src/geometry_processor.rs index f221d46c..c7a736a7 100644 --- a/geozero/src/geometry_processor.rs +++ b/geozero/src/geometry_processor.rs @@ -251,7 +251,10 @@ pub trait GeomProcessor { /// ## Parameters /// /// - `size`: the number of geometries in this GeometryCollection. - /// - `idx`: the positional index of this GeometryCollection. Unsure when this is non-zero?? Nested geometry collections? + /// - `idx`: the positional index of this GeometryCollection. This can be greater than 0 for + /// nested geometry collections but also when using `GeometryProcessor` to process a + /// `Feature` whose geometry is a `GeometryCollection`. For an example of this see [this + /// comment](https://github.com/georust/geozero/pull/183#discussion_r1454319662). /// /// ## Following events /// From 0fc71baba8c94d05b9e8f88ee9680cddaa2ac34a Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Fri, 9 Feb 2024 13:57:47 -0500 Subject: [PATCH 6/7] geojson lines writer (#193) --- geozero/src/geojson/geojson_line_writer.rs | 272 +++++++++++++++++++++ geozero/src/geojson/geojson_writer.rs | 2 +- geozero/src/geojson/mod.rs | 2 + geozero/src/lib.rs | 28 +-- 4 files changed, 289 insertions(+), 15 deletions(-) create mode 100644 geozero/src/geojson/geojson_line_writer.rs diff --git a/geozero/src/geojson/geojson_line_writer.rs b/geozero/src/geojson/geojson_line_writer.rs new file mode 100644 index 00000000..b602f46e --- /dev/null +++ b/geozero/src/geojson/geojson_line_writer.rs @@ -0,0 +1,272 @@ +use std::io::Write; + +use crate::error::Result; +use crate::geojson::GeoJsonWriter; +use crate::{FeatureProcessor, GeomProcessor, PropertyProcessor}; + +/// Line Delimited GeoJSON Writer: One feature per line. +/// +/// See +pub struct GeoJsonLineWriter { + /// We use a count of the number of contexts entered to decide when to add a newline character + /// and finish a line. The [newline-delimited GeoJSON + /// spec](https://datatracker.ietf.org/doc/html/rfc8142) defines that any type of GeoJSON + /// objects can be written as an object on a single line. Therefore, we can't solely add + /// newlines in `feature_end`. If the object on this line is a Point geometry, then we need to + /// add a newline character in `point_end`, because `feature_end` will never be called. + /// + /// Note that this approach is not resilient to malformed input. If the number of begin and end + /// calls do not match, newline characters will not be correctly added. + open_contexts: usize, + line_writer: GeoJsonWriter, +} + +impl GeoJsonLineWriter { + pub fn new(out: W) -> Self { + Self { + open_contexts: 0, + line_writer: GeoJsonWriter::new(out), + } + } + + fn write_newline(&mut self) -> Result<()> { + self.line_writer.out.write_all(b"\n")?; + Ok(()) + } + + fn begin_context(&mut self) { + self.open_contexts += 1; + } + + fn end_context(&mut self) -> Result<()> { + self.open_contexts -= 1; + if self.open_contexts == 0 { + self.write_newline()?; + } + Ok(()) + } + + /// Manually add a comma to the writer. + fn comma(&mut self) -> Result<()> { + self.line_writer.out.write_all(b",")?; + Ok(()) + } +} + +impl FeatureProcessor for GeoJsonLineWriter { + fn feature_begin(&mut self, _idx: u64) -> Result<()> { + self.begin_context(); + // We always pass `0` for `idx` because we want to avoid a preceding comma on this line. + self.line_writer.feature_begin(0)?; + Ok(()) + } + + fn feature_end(&mut self, idx: u64) -> Result<()> { + self.line_writer.feature_end(idx)?; + self.end_context()?; + Ok(()) + } + + fn properties_begin(&mut self) -> Result<()> { + self.line_writer.properties_begin() + } + + fn properties_end(&mut self) -> Result<()> { + self.line_writer.properties_end() + } + + fn geometry_begin(&mut self) -> Result<()> { + self.line_writer.geometry_begin() + } + + fn geometry_end(&mut self) -> Result<()> { + self.line_writer.geometry_end() + } +} + +impl GeomProcessor for GeoJsonLineWriter { + fn dimensions(&self) -> crate::CoordDimensions { + self.line_writer.dimensions() + } + + fn xy(&mut self, x: f64, y: f64, idx: usize) -> Result<()> { + self.line_writer.xy(x, y, idx) + } + + fn coordinate( + &mut self, + x: f64, + y: f64, + z: Option, + m: Option, + t: Option, + tm: Option, + idx: usize, + ) -> Result<()> { + self.line_writer.coordinate(x, y, z, m, t, tm, idx) + } + + // Whenever the idx is > 0, the underlying GeoJsonWriter will automatically add a comma prefix. + // _Almost always_ we don't want the prefixed comma because each feature or geometry will be on + // a new line. _However_ we need to distinguish between geometries that are _part of a feature_ + // and which need a preceding comma, and those that are standalone and don't need a preceding + // comma. + // + // When `self.open_contexts > 0` it means that we are inside a top-level feature and when `idx + // > 0` it means that this is not the first geometry in this feature. In that case we manually + // add a comma. Then we always pass `0` for `idx` to the underlying GeoJsonWriter. + fn empty_point(&mut self, idx: usize) -> Result<()> { + if self.open_contexts > 0 && idx > 0 { + self.comma()?; + } + + self.begin_context(); + self.line_writer.empty_point(0)?; + self.end_context() + } + + fn point_begin(&mut self, idx: usize) -> Result<()> { + if self.open_contexts > 0 && idx > 0 { + self.comma()?; + } + + self.begin_context(); + self.line_writer.point_begin(0) + } + + fn point_end(&mut self, idx: usize) -> Result<()> { + self.line_writer.point_end(idx)?; + self.end_context() + } + + fn multipoint_begin(&mut self, size: usize, idx: usize) -> Result<()> { + if self.open_contexts > 0 && idx > 0 { + self.comma()?; + } + + self.begin_context(); + self.line_writer.multipoint_begin(size, 0) + } + + fn multipoint_end(&mut self, idx: usize) -> Result<()> { + self.line_writer.multipoint_end(idx)?; + self.end_context() + } + + fn linestring_begin(&mut self, tagged: bool, size: usize, idx: usize) -> Result<()> { + if self.open_contexts > 0 && idx > 0 { + self.comma()?; + } + + self.begin_context(); + self.line_writer.linestring_begin(tagged, size, 0) + } + + fn linestring_end(&mut self, tagged: bool, idx: usize) -> Result<()> { + self.line_writer.linestring_end(tagged, idx)?; + self.end_context() + } + + fn multilinestring_begin(&mut self, size: usize, idx: usize) -> Result<()> { + if self.open_contexts > 0 && idx > 0 { + self.comma()?; + } + + self.begin_context(); + self.line_writer.multilinestring_begin(size, 0) + } + + fn multilinestring_end(&mut self, idx: usize) -> Result<()> { + self.line_writer.multilinestring_end(idx)?; + self.end_context() + } + + fn polygon_begin(&mut self, tagged: bool, size: usize, idx: usize) -> Result<()> { + if self.open_contexts > 0 && idx > 0 { + self.comma()?; + } + + self.begin_context(); + self.line_writer.polygon_begin(tagged, size, 0) + } + + fn polygon_end(&mut self, tagged: bool, idx: usize) -> Result<()> { + self.line_writer.polygon_end(tagged, idx)?; + self.end_context() + } + + fn multipolygon_begin(&mut self, size: usize, idx: usize) -> Result<()> { + if self.open_contexts > 0 && idx > 0 { + self.comma()?; + } + + self.begin_context(); + self.line_writer.multipolygon_begin(size, 0) + } + + fn multipolygon_end(&mut self, idx: usize) -> Result<()> { + self.line_writer.multipolygon_end(idx)?; + self.end_context() + } + + fn geometrycollection_begin(&mut self, size: usize, idx: usize) -> Result<()> { + if self.open_contexts > 0 && idx > 0 { + self.comma()?; + } + + self.begin_context(); + self.line_writer.geometrycollection_begin(size, 0) + } + + fn geometrycollection_end(&mut self, idx: usize) -> Result<()> { + self.line_writer.geometrycollection_end(idx)?; + self.end_context() + } +} + +impl PropertyProcessor for GeoJsonLineWriter { + fn property(&mut self, idx: usize, name: &str, value: &crate::ColumnValue) -> Result { + self.line_writer.property(idx, name, value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::geojson::read_geojson_lines; + + #[test] + fn good_geometries() { + let input = r#"{ "type": "Point", "coordinates": [1.1, 1.2] } +{ "type": "Point", "coordinates": [2.1, 2.2] } +{ "type": "Point", "coordinates": [3.1, 3.2] } +"#; + let mut out: Vec = Vec::new(); + assert!( + read_geojson_lines(input.as_bytes(), &mut GeoJsonLineWriter::new(&mut out)).is_ok() + ); + assert_json_lines_eq(&out, input); + } + + #[test] + fn good_features() { + let input = r#"{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [1.1, 1.2] }, "properties": { "name": "first" } } +{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [2.1, 2.2] }, "properties": { "name": "second" } } +{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [3.1, 3.3] }, "properties": { "name": "third" } } +"#; + let mut out: Vec = Vec::new(); + assert!( + read_geojson_lines(input.as_bytes(), &mut GeoJsonLineWriter::new(&mut out)).is_ok() + ); + assert_json_lines_eq(&out, input); + } + + fn assert_json_lines_eq(a: &[u8], b: &str) { + let a = std::str::from_utf8(a).unwrap(); + a.lines().zip(b.lines()).for_each(|(a_line, b_line)| { + let a_val: serde_json::Value = serde_json::from_str(a_line).unwrap(); + let b_val: serde_json::Value = serde_json::from_str(b_line).unwrap(); + assert_eq!(a_val, b_val); + }) + } +} diff --git a/geozero/src/geojson/geojson_writer.rs b/geozero/src/geojson/geojson_writer.rs index 2b18b218..8f943366 100644 --- a/geozero/src/geojson/geojson_writer.rs +++ b/geozero/src/geojson/geojson_writer.rs @@ -6,7 +6,7 @@ use std::io::Write; /// GeoJSON writer. pub struct GeoJsonWriter { dims: CoordDimensions, - out: W, + pub(crate) out: W, } impl GeoJsonWriter { diff --git a/geozero/src/geojson/mod.rs b/geozero/src/geojson/mod.rs index 8fe359d6..75b5cb85 100644 --- a/geozero/src/geojson/mod.rs +++ b/geozero/src/geojson/mod.rs @@ -1,9 +1,11 @@ //! GeoJSON conversions. pub(crate) mod geojson_line_reader; +pub(crate) mod geojson_line_writer; pub(crate) mod geojson_reader; pub(crate) mod geojson_writer; pub use geojson_line_reader::*; +pub use geojson_line_writer::*; pub use geojson_reader::*; pub use geojson_writer::*; diff --git a/geozero/src/lib.rs b/geozero/src/lib.rs index c9be30e9..aa9a32ec 100644 --- a/geozero/src/lib.rs +++ b/geozero/src/lib.rs @@ -17,20 +17,20 @@ //! //! ## Format conversion overview //! -//! | | [`GeozeroGeometry`] | Dimensions | [`GeozeroDatasource`] | Geometry Conversion | [`GeomProcessor`] | -//! |---------------|--------------------------------------------------------------------------------------------------------------------------|------------|--------------------------------------------------------------------------------------|---------------------|-----------------------------------------| -//! | CSV | [csv::Csv], [csv::CsvString] | XY | - | [ProcessToCsv] | [CsvWriter](csv::CsvWriter) | -//! | GDAL | `gdal::vector::Geometry` | XYZ | - | [ToGdal] | [GdalWriter](gdal::GdalWriter) | -//! | geo-types | `geo_types::Geometry` | XY | - | [ToGeo] | [GeoWriter](geo_types::GeoWriter) | -//! | GeoArrow | `arrow2::array::BinaryArray` | XY | - | - | - | -//! | GeoJSON | [GeoJson](geojson::GeoJson), [GeoJsonString](geojson::GeoJsonString) | XYZ | [GeoJsonReader](geojson::GeoJsonReader), [GeoJson](geojson::GeoJson) | [ToJson] | [GeoJsonWriter](geojson::GeoJsonWriter) | -//! | GeoJSON Lines | | XYZ | [GeoJsonLineReader](geojson::GeoJsonLineReader) | | | -//! | GEOS | `geos::Geometry` | XYZ | - | [ToGeos] | [GeosWriter](geos::GeosWriter) | -//! | GPX | | XY | [GpxReader](gpx::GpxReader) | | | -//! | MVT | [mvt::tile::Feature] | XY | [mvt::tile::Layer] | [ToMvt] | [MvtWriter](mvt::MvtWriter) | -//! | SVG | - | XY | - | [ToSvg] | [SvgWriter](svg::SvgWriter) | -//! | WKB | [Wkb](wkb::Wkb), [Ewkb](wkb::Ewkb), [GpkgWkb](wkb::GpkgWkb), [SpatiaLiteWkb](wkb::SpatiaLiteWkb), [MySQL](wkb::MySQLWkb) | XYZM | - | [ToWkb] | [WkbWriter](wkb::WkbWriter) | -//! | WKT | [wkt::WktStr], [wkt::WktString], [wkt::EwktStr], [wkt::EwktString] | XYZM | [wkt::WktReader], [wkt::WktStr], [wkt::WktString], [wkt::EwktStr], [wkt::EwktString] | [ToWkt] | [WktWriter](wkt::WktWriter) | +//! | | [`GeozeroGeometry`] | Dimensions | [`GeozeroDatasource`] | Geometry Conversion | [`GeomProcessor`] | +//! |---------------|--------------------------------------------------------------------------------------------------------------------------|------------|--------------------------------------------------------------------------------------|---------------------|-------------------------------------------------| +//! | CSV | [csv::Csv], [csv::CsvString] | XY | - | [ProcessToCsv] | [CsvWriter](csv::CsvWriter) | +//! | GDAL | `gdal::vector::Geometry` | XYZ | - | [ToGdal] | [GdalWriter](gdal::GdalWriter) | +//! | geo-types | `geo_types::Geometry` | XY | - | [ToGeo] | [GeoWriter](geo_types::GeoWriter) | +//! | GeoArrow | `arrow2::array::BinaryArray` | XY | - | - | - | +//! | GeoJSON | [GeoJson](geojson::GeoJson), [GeoJsonString](geojson::GeoJsonString) | XYZ | [GeoJsonReader](geojson::GeoJsonReader), [GeoJson](geojson::GeoJson) | [ToJson] | [GeoJsonWriter](geojson::GeoJsonWriter) | +//! | GeoJSON Lines | | XYZ | [GeoJsonLineReader](geojson::GeoJsonLineReader) | | [GeoJsonLineWriter](geojson::GeoJsonLineWriter) | +//! | GEOS | `geos::Geometry` | XYZ | - | [ToGeos] | [GeosWriter](geos::GeosWriter) | +//! | GPX | | XY | [GpxReader](gpx::GpxReader) | | | +//! | MVT | [mvt::tile::Feature] | XY | [mvt::tile::Layer] | [ToMvt] | [MvtWriter](mvt::MvtWriter) | +//! | SVG | - | XY | - | [ToSvg] | [SvgWriter](svg::SvgWriter) | +//! | WKB | [Wkb](wkb::Wkb), [Ewkb](wkb::Ewkb), [GpkgWkb](wkb::GpkgWkb), [SpatiaLiteWkb](wkb::SpatiaLiteWkb), [MySQL](wkb::MySQLWkb) | XYZM | - | [ToWkb] | [WkbWriter](wkb::WkbWriter) | +//! | WKT | [wkt::WktStr], [wkt::WktString], [wkt::EwktStr], [wkt::EwktString] | XYZM | [wkt::WktReader], [wkt::WktStr], [wkt::WktString], [wkt::EwktStr], [wkt::EwktString] | [ToWkt] | [WktWriter](wkt::WktWriter) | #![warn(clippy::uninlined_format_args)] #![allow( From 404c2da8fa2668835bd42de1e09fd202f342198d Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 9 Feb 2024 12:04:41 -0800 Subject: [PATCH 7/7] Configure for merge queue (#194) Currently we rely on maintainers to manually verify that CI has passed and that branches are up to date with main. I'd like to enable GH merge queue (like we have in georust/geo, georust/proj, and a few others). This entails enabling branch protection for main. All together, these changes will ensure that a PR won't merge until it is confirmed to pass tests when up-to-date with main. --- .github/workflows/linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 3a6f1502..bb12bef3 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -1,6 +1,6 @@ name: CI-Linux -on: [push, pull_request] +on: [push, pull_request, merge_group] env: CARGO_TERM_COLOR: always