Skip to main content

shapefile_wasm/
input.rs

1//! Parsing of the GeoJSON that comes in from JavaScript.
2//!
3//! We deliberately walk `serde_json::Value` by hand instead of pulling in the
4//! `geojson` crate: it keeps the wasm binary small and lets us accept the
5//! slightly-off shapes that real-world data tends to have (a bare geometry, a
6//! bare feature, a plain array of features).
7
8use serde_json::{Map, Value};
9
10use crate::error::{Result, ShapefileError};
11
12/// A single coordinate, normalised to four ordinates so downstream code never
13/// has to worry about how many were actually present.
14#[derive(Debug, Clone, Copy)]
15pub struct Position {
16    pub x: f64,
17    pub y: f64,
18    /// Third ordinate as written in the source, if any.
19    pub third: Option<f64>,
20    /// Fourth ordinate as written in the source, if any.
21    pub fourth: Option<f64>,
22}
23
24impl Position {
25    fn parse(value: &Value, index: usize) -> Result<Self> {
26        let array = value.as_array().ok_or_else(|| ShapefileError::Feature {
27            index,
28            message: "a coordinate must be an array of numbers".into(),
29        })?;
30
31        if array.len() < 2 {
32            return Err(ShapefileError::Feature {
33                index,
34                message: format!("a coordinate needs at least x and y, got {}", array.len()),
35            });
36        }
37
38        let ordinate = |slot: usize| -> Result<f64> {
39            array[slot].as_f64().ok_or_else(|| ShapefileError::Feature {
40                index,
41                message: format!("coordinate ordinate {slot} is not a number"),
42            })
43        };
44
45        Ok(Self {
46            x: ordinate(0)?,
47            y: ordinate(1)?,
48            third: if array.len() > 2 {
49                Some(ordinate(2)?)
50            } else {
51                None
52            },
53            fourth: if array.len() > 3 {
54                Some(ordinate(3)?)
55            } else {
56                None
57            },
58        })
59    }
60
61    /// How many ordinates the source actually carried (2, 3 or 4).
62    pub fn arity(&self) -> usize {
63        2 + usize::from(self.third.is_some()) + usize::from(self.fourth.is_some())
64    }
65}
66
67/// A GeoJSON geometry, reduced to the six types a shapefile can represent.
68#[derive(Debug, Clone)]
69pub enum Geometry {
70    Point(Position),
71    MultiPoint(Vec<Position>),
72    LineString(Vec<Position>),
73    MultiLineString(Vec<Vec<Position>>),
74    Polygon(Vec<Vec<Position>>),
75    MultiPolygon(Vec<Vec<Vec<Position>>>),
76}
77
78impl Geometry {
79    /// The shapefile geometry family this GeoJSON type has to be written as.
80    pub fn family(&self) -> Family {
81        match self {
82            Geometry::Point(_) => Family::Point,
83            Geometry::MultiPoint(_) => Family::Multipoint,
84            Geometry::LineString(_) | Geometry::MultiLineString(_) => Family::Polyline,
85            Geometry::Polygon(_) | Geometry::MultiPolygon(_) => Family::Polygon,
86        }
87    }
88
89    /// Visits every position in the geometry, for bounds and dimension probing.
90    pub fn for_each_position(&self, mut visit: impl FnMut(&Position)) {
91        match self {
92            Geometry::Point(p) => visit(p),
93            Geometry::MultiPoint(ps) | Geometry::LineString(ps) => ps.iter().for_each(visit),
94            Geometry::MultiLineString(parts) | Geometry::Polygon(parts) => {
95                parts.iter().flatten().for_each(visit)
96            }
97            Geometry::MultiPolygon(polys) => polys.iter().flatten().flatten().for_each(visit),
98        }
99    }
100
101    fn parse(value: &Value, index: usize) -> Result<Self> {
102        let object = value.as_object().ok_or_else(|| ShapefileError::Feature {
103            index,
104            message: "geometry must be an object".into(),
105        })?;
106
107        let kind =
108            object
109                .get("type")
110                .and_then(Value::as_str)
111                .ok_or_else(|| ShapefileError::Feature {
112                    index,
113                    message: "geometry is missing a `type`".into(),
114                })?;
115
116        if kind == "GeometryCollection" {
117            return Err(ShapefileError::GeometryCollection { index });
118        }
119
120        let coordinates = object
121            .get("coordinates")
122            .ok_or_else(|| ShapefileError::Feature {
123                index,
124                message: format!("{kind} geometry is missing `coordinates`"),
125            })?;
126
127        match kind {
128            "Point" => Ok(Geometry::Point(Position::parse(coordinates, index)?)),
129            "MultiPoint" => Ok(Geometry::MultiPoint(parse_line(coordinates, index)?)),
130            "LineString" => Ok(Geometry::LineString(parse_line(coordinates, index)?)),
131            "MultiLineString" => Ok(Geometry::MultiLineString(parse_lines(coordinates, index)?)),
132            "Polygon" => Ok(Geometry::Polygon(parse_lines(coordinates, index)?)),
133            "MultiPolygon" => {
134                let outer = coordinates
135                    .as_array()
136                    .ok_or_else(|| ShapefileError::Feature {
137                        index,
138                        message: "MultiPolygon coordinates must be an array".into(),
139                    })?;
140                outer
141                    .iter()
142                    .map(|poly| parse_lines(poly, index))
143                    .collect::<Result<Vec<_>>>()
144                    .map(Geometry::MultiPolygon)
145            }
146            other => Err(ShapefileError::UnsupportedGeometry(other.to_string())),
147        }
148    }
149}
150
151fn parse_line(value: &Value, index: usize) -> Result<Vec<Position>> {
152    let array = value.as_array().ok_or_else(|| ShapefileError::Feature {
153        index,
154        message: "expected an array of coordinates".into(),
155    })?;
156    array
157        .iter()
158        .map(|pos| Position::parse(pos, index))
159        .collect()
160}
161
162fn parse_lines(value: &Value, index: usize) -> Result<Vec<Vec<Position>>> {
163    let array = value.as_array().ok_or_else(|| ShapefileError::Feature {
164        index,
165        message: "expected an array of coordinate arrays".into(),
166    })?;
167    array.iter().map(|line| parse_line(line, index)).collect()
168}
169
170/// The four shapefile geometry families we can emit.
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum Family {
173    Point,
174    Multipoint,
175    Polyline,
176    Polygon,
177}
178
179impl Family {
180    pub fn label(self) -> &'static str {
181        match self {
182            Family::Point => "Point",
183            Family::Multipoint => "Multipoint",
184            Family::Polyline => "Polyline",
185            Family::Polygon => "Polygon",
186        }
187    }
188
189    pub fn parse(name: &str) -> Option<Self> {
190        match name.to_ascii_lowercase().as_str() {
191            "point" => Some(Family::Point),
192            "multipoint" => Some(Family::Multipoint),
193            "polyline" | "linestring" | "line" => Some(Family::Polyline),
194            "polygon" => Some(Family::Polygon),
195            _ => None,
196        }
197    }
198
199    /// Resolves the single family a whole dataset will be written as.
200    ///
201    /// Point and MultiPoint coexist happily by promoting everything to
202    /// Multipoint; anything else genuinely cannot share one file.
203    pub fn reconcile(self, other: Family, index: usize) -> Result<Family> {
204        if self == other {
205            return Ok(self);
206        }
207        match (self, other) {
208            (Family::Point, Family::Multipoint) | (Family::Multipoint, Family::Point) => {
209                Ok(Family::Multipoint)
210            }
211            _ => Err(ShapefileError::MixedGeometry {
212                first: self.label().to_string(),
213                other: other.label().to_string(),
214                index,
215            }),
216        }
217    }
218}
219
220/// One row of the eventual shapefile: a geometry plus its attribute bag.
221pub struct Feature {
222    pub geometry: Option<Geometry>,
223    pub properties: Map<String, Value>,
224}
225
226/// Accepts a FeatureCollection, a lone Feature, a lone geometry, or an array of
227/// any of those, and flattens it to a feature list.
228pub fn normalize(root: &Value) -> Result<Vec<Feature>> {
229    match root {
230        Value::Array(items) => {
231            let mut features = Vec::with_capacity(items.len());
232            for (index, item) in items.iter().enumerate() {
233                features.push(parse_feature(item, index)?);
234            }
235            Ok(features)
236        }
237        Value::Object(object) => match object.get("type").and_then(Value::as_str) {
238            Some("FeatureCollection") => {
239                let items = object
240                    .get("features")
241                    .and_then(Value::as_array)
242                    .ok_or_else(|| {
243                        ShapefileError::Input("FeatureCollection has no `features` array".into())
244                    })?;
245                let mut features = Vec::with_capacity(items.len());
246                for (index, item) in items.iter().enumerate() {
247                    features.push(parse_feature(item, index)?);
248                }
249                Ok(features)
250            }
251            Some(_) => Ok(vec![parse_feature(root, 0)?]),
252            None => Err(ShapefileError::Input(
253                "expected a GeoJSON object with a `type` member".into(),
254            )),
255        },
256        _ => Err(ShapefileError::Input(
257            "expected a GeoJSON object or an array of features".into(),
258        )),
259    }
260}
261
262fn parse_feature(value: &Value, index: usize) -> Result<Feature> {
263    let object = value.as_object().ok_or_else(|| ShapefileError::Feature {
264        index,
265        message: "expected an object".into(),
266    })?;
267
268    // A bare geometry is a perfectly reasonable thing to hand us.
269    if object.get("type").and_then(Value::as_str) != Some("Feature") {
270        return Ok(Feature {
271            geometry: Some(Geometry::parse(value, index)?),
272            properties: Map::new(),
273        });
274    }
275
276    let geometry = match object.get("geometry") {
277        None | Some(Value::Null) => None,
278        Some(geometry) => Some(Geometry::parse(geometry, index)?),
279    };
280
281    let properties = match object.get("properties") {
282        Some(Value::Object(map)) => map.clone(),
283        _ => Map::new(),
284    };
285
286    Ok(Feature {
287        geometry,
288        properties,
289    })
290}