Skip to main content

shapefile_wasm/
read.rs

1//! Reads shapefile components back into GeoJSON.
2//!
3//! Two things make this more than a mechanical transcription:
4//!
5//! * A shapefile polygon is a flat bag of rings with no nesting, so the holes
6//!   have to be matched back to the ring that contains them before GeoJSON can
7//!   describe them.
8//! * A .dbf carries no encoding of its own. The caller passes whatever the
9//!   companion .cpg said, and we fall back to UTF-8.
10
11use std::io::Cursor;
12
13use serde::Deserialize;
14use serde_json::{Map, Number, Value};
15
16use shapefile::record::traits::{HasM, HasXY, HasZ};
17use shapefile::{Multipatch, Patch, Point, PointM, PointZ, Shape, ShapeReader, NO_DATA};
18
19use crate::error::{Result, ShapefileError};
20
21/// Caller-tunable knobs for reading.
22/// `#[serde(default)]` fills absent fields from `Default`, so the derived impl
23/// and the deserialised defaults are the same thing by construction.
24#[derive(Debug, Default, Deserialize)]
25#[serde(default, rename_all = "camelCase")]
26pub struct ReadOptions {
27    /// Character set of the .dbf, normally taken from the companion .cpg file.
28    pub encoding: Option<String>,
29    /// Emit shapefile M (measure) values as a trailing ordinate. GeoJSON has no
30    /// notion of measures, so this is off by default.
31    pub include_m: bool,
32    /// Contents of the companion `.prj`, surfaced as `wkt` on the result.
33    /// Passed through by the TypeScript layer; nothing here parses it.
34    pub prj: Option<String>,
35}
36
37impl ReadOptions {
38    /// `DynEncoding` is not exported by `dbase`, so the encoding is applied to
39    /// the builder inside the match rather than returned from it.
40    fn reader_builder(&self) -> dbase::ReaderBuilder {
41        let builder = dbase::ReaderBuilder::new();
42
43        let requested = self
44            .encoding
45            .as_deref()
46            .unwrap_or("utf-8")
47            .trim()
48            .to_ascii_lowercase()
49            .replace(['-', '_', ' '], "");
50
51        // .cpg files are wildly inconsistent, so match generously.
52        match requested.as_str() {
53            "utf8" | "utf8bom" | "65001" | "" => builder.with_encoding(dbase::UnicodeLossy),
54            "cp1252" | "windows1252" | "iso88591" | "latin1" | "ansi" | "1252" => {
55                builder.with_encoding(yore::code_pages::CP1252)
56            }
57            "cp1250" | "windows1250" | "1250" => builder.with_encoding(yore::code_pages::CP1250),
58            "cp1251" | "windows1251" | "1251" => builder.with_encoding(yore::code_pages::CP1251),
59            "cp1253" | "windows1253" | "1253" => builder.with_encoding(yore::code_pages::CP1253),
60            "cp1254" | "windows1254" | "1254" => builder.with_encoding(yore::code_pages::CP1254),
61            "cp1255" | "windows1255" | "1255" => builder.with_encoding(yore::code_pages::CP1255),
62            "cp1256" | "windows1256" | "1256" => builder.with_encoding(yore::code_pages::CP1256),
63            "cp437" | "437" | "oem" => builder.with_encoding(yore::code_pages::CP437),
64            "cp850" | "850" => builder.with_encoding(yore::code_pages::CP850),
65            "cp852" | "852" => builder.with_encoding(yore::code_pages::CP852),
66            "cp865" | "865" => builder.with_encoding(yore::code_pages::CP865),
67            "cp866" | "866" => builder.with_encoding(yore::code_pages::CP866),
68            "cp874" | "874" => builder.with_encoding(yore::code_pages::CP874),
69            // An unrecognised label is far better handled as UTF-8 than as a
70            // hard failure; worst case a few characters come back replaced.
71            _ => builder.with_encoding(dbase::UnicodeLossy),
72        }
73    }
74}
75
76/// Reads a `.shp` (and optionally its `.dbf`) into a GeoJSON FeatureCollection.
77pub fn read(shp: &[u8], dbf: Option<&[u8]>, options: &ReadOptions) -> Result<Value> {
78    let reader = ShapeReader::new(Cursor::new(shp))?;
79    let shapes = reader.read()?;
80
81    // Shapes and records are paired positionally. Reading them separately (as
82    // opposed to `shapefile::Reader`) means a truncated .dbf degrades to missing
83    // attributes instead of failing the whole file.
84    let records = match dbf {
85        Some(bytes) => read_records(bytes, options)?,
86        None => Vec::new(),
87    };
88
89    let mut features = Vec::with_capacity(shapes.len());
90    for (index, shape) in shapes.iter().enumerate() {
91        let mut feature = Map::new();
92        feature.insert("type".into(), Value::String("Feature".into()));
93        feature.insert(
94            "geometry".into(),
95            shape_to_geometry(shape, options.include_m, index)?,
96        );
97        feature.insert(
98            "properties".into(),
99            records
100                .get(index)
101                .cloned()
102                .map(Value::Object)
103                .unwrap_or(Value::Object(Map::new())),
104        );
105        features.push(Value::Object(feature));
106    }
107
108    let mut collection = Map::new();
109    collection.insert("type".into(), Value::String("FeatureCollection".into()));
110    collection.insert("features".into(), Value::Array(features));
111    if let Some(prj) = &options.prj {
112        // Non-standard, but the alternative is silently losing the projection.
113        collection.insert("wkt".into(), Value::String(prj.clone()));
114    }
115
116    Ok(Value::Object(collection))
117}
118
119fn read_records(dbf: &[u8], options: &ReadOptions) -> Result<Vec<Map<String, Value>>> {
120    let mut reader = options.reader_builder().build(Cursor::new(dbf))?;
121
122    // `dbase::Record` is a hash map, so grab the declared field order first;
123    // otherwise the GeoJSON properties come out shuffled.
124    let field_names: Vec<String> = reader
125        .fields()
126        .iter()
127        .map(|field| field.name().to_string())
128        .filter(|name| name != "DeletionFlag")
129        .collect();
130
131    let records = reader.read()?;
132
133    Ok(records
134        .into_iter()
135        .map(|record| {
136            let mut properties = Map::new();
137            for name in &field_names {
138                let value = record.get(name).map(field_to_json).unwrap_or(Value::Null);
139                properties.insert(name.clone(), value);
140            }
141            properties
142        })
143        .collect())
144}
145
146/// dBase pads character fields out to their declared width; `dbase` strips that
147/// padding on read and offers no way to keep it, so values always arrive trimmed.
148fn field_to_json(value: &dbase::FieldValue) -> Value {
149    use dbase::FieldValue;
150
151    let text = |value: &String| Value::String(value.clone());
152
153    match value {
154        FieldValue::Character(Some(value)) => text(value),
155        FieldValue::Memo(value) => text(value),
156        FieldValue::Character(None) => Value::Null,
157        FieldValue::Numeric(Some(value)) => number(*value),
158        FieldValue::Numeric(None) => Value::Null,
159        FieldValue::Float(Some(value)) => number(*value as f64),
160        FieldValue::Float(None) => Value::Null,
161        FieldValue::Logical(Some(value)) => Value::Bool(*value),
162        FieldValue::Logical(None) => Value::Null,
163        FieldValue::Integer(value) => Value::Number(Number::from(*value)),
164        FieldValue::Currency(value) | FieldValue::Double(value) => number(*value),
165        FieldValue::Date(Some(date)) => Value::String(format!(
166            "{:04}-{:02}-{:02}",
167            date.year(),
168            date.month(),
169            date.day()
170        )),
171        FieldValue::Date(None) => Value::Null,
172        FieldValue::DateTime(stamp) => {
173            let date = stamp.date();
174            let time = stamp.time();
175            Value::String(format!(
176                "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
177                date.year(),
178                date.month(),
179                date.day(),
180                time.hours(),
181                time.minutes(),
182                time.seconds()
183            ))
184        }
185    }
186}
187
188fn number(value: f64) -> Value {
189    Number::from_f64(value)
190        .map(Value::Number)
191        .unwrap_or(Value::Null)
192}
193
194/// True when a measure ordinate carries the shapefile "no data" sentinel.
195fn has_measure(m: f64) -> bool {
196    m > NO_DATA && m.is_finite()
197}
198
199fn position_xy(point: &Point) -> Value {
200    Value::Array(vec![number(point.x), number(point.y)])
201}
202
203fn position_m(point: &PointM, include_m: bool) -> Value {
204    let mut ordinates = vec![number(point.x()), number(point.y())];
205    if include_m && has_measure(point.m()) {
206        ordinates.push(number(point.m()));
207    }
208    Value::Array(ordinates)
209}
210
211fn position_z(point: &PointZ, include_m: bool) -> Value {
212    let mut ordinates = vec![number(point.x()), number(point.y()), number(point.z())];
213    if include_m && has_measure(point.m()) {
214        ordinates.push(number(point.m()));
215    }
216    Value::Array(ordinates)
217}
218
219fn geometry(kind: &str, coordinates: Value) -> Value {
220    let mut object = Map::new();
221    object.insert("type".into(), Value::String(kind.into()));
222    object.insert("coordinates".into(), coordinates);
223    Value::Object(object)
224}
225
226/// Wraps a list of parts as a LineString when there is exactly one, and a
227/// MultiLineString otherwise — GeoJSON prefers the simpler type.
228fn lines(parts: Vec<Value>) -> Value {
229    if parts.len() == 1 {
230        geometry("LineString", parts.into_iter().next().unwrap())
231    } else {
232        geometry("MultiLineString", Value::Array(parts))
233    }
234}
235
236fn points(points: Vec<Value>) -> Value {
237    geometry("MultiPoint", Value::Array(points))
238}
239
240fn shape_to_geometry(shape: &Shape, include_m: bool, index: usize) -> Result<Value> {
241    let geometry = match shape {
242        Shape::NullShape => Value::Null,
243
244        Shape::Point(point) => geometry("Point", position_xy(point)),
245        Shape::PointM(point) => geometry("Point", position_m(point, include_m)),
246        Shape::PointZ(point) => geometry("Point", position_z(point, include_m)),
247
248        Shape::Multipoint(shape) => points(shape.points().iter().map(position_xy).collect()),
249        Shape::MultipointM(shape) => points(
250            shape
251                .points()
252                .iter()
253                .map(|point| position_m(point, include_m))
254                .collect(),
255        ),
256        Shape::MultipointZ(shape) => points(
257            shape
258                .points()
259                .iter()
260                .map(|point| position_z(point, include_m))
261                .collect(),
262        ),
263
264        Shape::Polyline(shape) => lines(
265            shape
266                .parts()
267                .iter()
268                .map(|part| Value::Array(part.iter().map(position_xy).collect()))
269                .collect(),
270        ),
271        Shape::PolylineM(shape) => lines(
272            shape
273                .parts()
274                .iter()
275                .map(|part| Value::Array(part.iter().map(|p| position_m(p, include_m)).collect()))
276                .collect(),
277        ),
278        Shape::PolylineZ(shape) => lines(
279            shape
280                .parts()
281                .iter()
282                .map(|part| Value::Array(part.iter().map(|p| position_z(p, include_m)).collect()))
283                .collect(),
284        ),
285
286        Shape::Polygon(shape) => rings_to_geometry(
287            shape
288                .rings()
289                .iter()
290                .map(|ring| Ring::from_points(ring, |point| (point.x, point.y), position_xy))
291                .collect(),
292        ),
293        Shape::PolygonM(shape) => rings_to_geometry(
294            shape
295                .rings()
296                .iter()
297                .map(|ring| {
298                    Ring::from_points(
299                        ring,
300                        |point| (point.x(), point.y()),
301                        |point| position_m(point, include_m),
302                    )
303                })
304                .collect(),
305        ),
306        Shape::PolygonZ(shape) => rings_to_geometry(
307            shape
308                .rings()
309                .iter()
310                .map(|ring| {
311                    Ring::from_points(
312                        ring,
313                        |point| (point.x(), point.y()),
314                        |point| position_z(point, include_m),
315                    )
316                })
317                .collect(),
318        ),
319
320        Shape::Multipatch(shape) => multipatch_to_geometry(shape, include_m, index)?,
321    };
322
323    Ok(geometry)
324}
325
326/// A polygon ring reduced to what the nesting logic needs: plain XY for the
327/// containment maths, plus the already-formatted GeoJSON positions.
328struct Ring {
329    outer: bool,
330    xy: Vec<(f64, f64)>,
331    positions: Vec<Value>,
332}
333
334impl Ring {
335    fn from_points<P, F, G>(ring: &shapefile::PolygonRing<P>, to_xy: F, to_position: G) -> Self
336    where
337        F: Fn(&P) -> (f64, f64),
338        G: Fn(&P) -> Value,
339    {
340        let (outer, points) = match ring {
341            shapefile::PolygonRing::Outer(points) => (true, points),
342            shapefile::PolygonRing::Inner(points) => (false, points),
343        };
344        Self {
345            outer,
346            xy: points.iter().map(&to_xy).collect(),
347            positions: points.iter().map(&to_position).collect(),
348        }
349    }
350
351    /// Twice the signed area. Positive means counter-clockwise.
352    fn signed_area(&self) -> f64 {
353        let mut total = 0.0;
354        for window in self.xy.windows(2) {
355            let (x1, y1) = window[0];
356            let (x2, y2) = window[1];
357            total += (x2 - x1) * (y2 + y1);
358        }
359        -total
360    }
361
362    fn contains(&self, point: (f64, f64)) -> bool {
363        // Standard ray casting; the ring is already closed.
364        let (px, py) = point;
365        let mut inside = false;
366        for window in self.xy.windows(2) {
367            let (x1, y1) = window[0];
368            let (x2, y2) = window[1];
369            if (y1 > py) != (y2 > py) {
370                let slope = (x2 - x1) / (y2 - y1);
371                if px < x1 + (py - y1) * slope {
372                    inside = !inside;
373                }
374            }
375        }
376        inside
377    }
378
379    /// GeoJSON (RFC 7946) wants exteriors counter-clockwise and holes clockwise;
380    /// shapefiles use the opposite convention.
381    fn oriented(mut self, counter_clockwise: bool) -> Vec<Value> {
382        let is_ccw = self.signed_area() > 0.0;
383        if is_ccw != counter_clockwise {
384            self.positions.reverse();
385        }
386        self.positions
387    }
388}
389
390/// Rebuilds GeoJSON polygon nesting from a shapefile's flat ring list.
391fn rings_to_geometry(rings: Vec<Ring>) -> Value {
392    let (outers, inners): (Vec<Ring>, Vec<Ring>) = rings.into_iter().partition(|ring| ring.outer);
393
394    if outers.is_empty() {
395        // Malformed input: no ring was marked as an exterior. Treat each one as
396        // its own polygon rather than dropping the geometry entirely.
397        let polygons: Vec<Value> = inners
398            .into_iter()
399            .map(|ring| Value::Array(vec![Value::Array(ring.oriented(true))]))
400            .collect();
401        return finish_polygons(polygons);
402    }
403
404    // Each hole belongs to the smallest exterior ring that contains it. Using
405    // the smallest matters when polygons are nested inside one another.
406    let mut assignments: Vec<Vec<Value>> = outers.iter().map(|_| Vec::new()).collect();
407    for inner in inners {
408        let probe = match inner.xy.first() {
409            Some(point) => *point,
410            None => continue,
411        };
412
413        let best = outers
414            .iter()
415            .enumerate()
416            .filter(|(_, outer)| outer.contains(probe))
417            .min_by(|(_, a), (_, b)| {
418                a.signed_area()
419                    .abs()
420                    .partial_cmp(&b.signed_area().abs())
421                    .unwrap_or(std::cmp::Ordering::Equal)
422            })
423            .map(|(index, _)| index);
424
425        // An unmatched hole is a data error; parking it on the first ring keeps
426        // the output valid GeoJSON.
427        let target = best.unwrap_or(0);
428        assignments[target].push(Value::Array(inner.oriented(false)));
429    }
430
431    let polygons: Vec<Value> = outers
432        .into_iter()
433        .zip(assignments)
434        .map(|(outer, holes)| {
435            let mut ring_list = vec![Value::Array(outer.oriented(true))];
436            ring_list.extend(holes);
437            Value::Array(ring_list)
438        })
439        .collect();
440
441    finish_polygons(polygons)
442}
443
444fn finish_polygons(polygons: Vec<Value>) -> Value {
445    if polygons.len() == 1 {
446        geometry("Polygon", polygons.into_iter().next().unwrap())
447    } else {
448        geometry("MultiPolygon", Value::Array(polygons))
449    }
450}
451
452/// Multipatch has no GeoJSON equivalent. Triangle strips and fans are expanded
453/// into individual triangles so the surface survives as a MultiPolygon; the
454/// alternative is discarding the geometry.
455fn multipatch_to_geometry(shape: &Multipatch, include_m: bool, index: usize) -> Result<Value> {
456    let mut polygons: Vec<Value> = Vec::new();
457    let mut pending_outer: Option<Vec<Value>> = None;
458
459    let close = |points: &[PointZ]| -> Vec<Value> {
460        let mut positions: Vec<Value> = points.iter().map(|p| position_z(p, include_m)).collect();
461        if positions.len() > 2 && positions.first() != positions.last() {
462            positions.push(positions[0].clone());
463        }
464        positions
465    };
466
467    for patch in shape.patches() {
468        match patch {
469            Patch::TriangleStrip(points) => {
470                for triangle in points.windows(3) {
471                    polygons.push(Value::Array(vec![Value::Array(close(triangle))]));
472                }
473            }
474            Patch::TriangleFan(points) => {
475                if points.len() >= 3 {
476                    for pair in points[1..].windows(2) {
477                        let triangle = [points[0], pair[0], pair[1]];
478                        polygons.push(Value::Array(vec![Value::Array(close(&triangle))]));
479                    }
480                }
481            }
482            Patch::OuterRing(points) => {
483                if let Some(previous) = pending_outer.take() {
484                    polygons.push(Value::Array(vec![Value::Array(previous)]));
485                }
486                pending_outer = Some(close(points));
487            }
488            Patch::InnerRing(points) => {
489                // An inner ring belongs to the outer ring that preceded it.
490                let hole = Value::Array(close(points));
491                match polygons.last_mut() {
492                    Some(Value::Array(rings)) if pending_outer.is_none() => rings.push(hole),
493                    _ => {
494                        if let Some(outer) = pending_outer.take() {
495                            polygons.push(Value::Array(vec![Value::Array(outer), hole]));
496                        }
497                    }
498                }
499            }
500            Patch::FirstRing(points) | Patch::Ring(points) => {
501                if let Some(previous) = pending_outer.take() {
502                    polygons.push(Value::Array(vec![Value::Array(previous)]));
503                }
504                polygons.push(Value::Array(vec![Value::Array(close(points))]));
505            }
506        }
507    }
508
509    if let Some(outer) = pending_outer.take() {
510        polygons.push(Value::Array(vec![Value::Array(outer)]));
511    }
512
513    if polygons.is_empty() {
514        return Err(ShapefileError::Feature {
515            index,
516            message: "multipatch contained no renderable patches".into(),
517        });
518    }
519
520    Ok(finish_polygons(polygons))
521}