Skip to main content

rustial_engine/
shapefile_parser.rs

1//! Shapefile parser.
2//!
3//! Converts ESRI Shapefiles into the engine's internal geometry types.
4//! Gated behind the `shapefile` Cargo feature flag.
5
6use crate::geometry::*;
7use rustial_math::GeoCoord;
8use thiserror::Error;
9
10/// Error type for Shapefile parsing.
11#[derive(Debug, Error)]
12pub enum ShapefileError {
13    /// Failed to read or parse the Shapefile.
14    #[error("shapefile read error: {0}")]
15    Read(String),
16    /// Unsupported shape type encountered.
17    #[error("unsupported shape type: {0}")]
18    UnsupportedShape(String),
19}
20
21/// Parse Shapefile bytes (`.shp` content) into a [`FeatureCollection`].
22///
23/// This reads the `.shp` binary data directly via [`shapefile::ShapeReader`].
24/// DBF attribute data is not parsed -- all features will have empty
25/// `properties`.
26///
27/// Supported shape types: Point, PolyLine, Polygon, MultiPoint,
28/// PointZ, PolyLineZ, PolygonZ, MultiPointZ.
29///
30/// # Errors
31///
32/// Returns [`ShapefileError::Read`] if the binary data is malformed,
33/// or [`ShapefileError::UnsupportedShape`] for shape types that cannot
34/// be mapped to the engine's geometry model.
35#[cfg(feature = "shapefile")]
36pub fn parse_shapefile(shp_bytes: &[u8]) -> Result<FeatureCollection, ShapefileError> {
37    use std::io::Cursor;
38
39    let mut reader = shapefile::ShapeReader::new(Cursor::new(shp_bytes))
40        .map_err(|e| ShapefileError::Read(e.to_string()))?;
41
42    let mut features = Vec::new();
43
44    for shape_result in reader.iter_shapes_as::<shapefile::Shape>() {
45        let shape = shape_result.map_err(|e| ShapefileError::Read(e.to_string()))?;
46        let geometry = shape_to_geometry(shape)?;
47        features.push(Feature {
48            geometry,
49            properties: std::collections::HashMap::new(),
50        });
51    }
52
53    Ok(FeatureCollection { features })
54}
55
56#[cfg(feature = "shapefile")]
57fn shape_to_geometry(shape: shapefile::Shape) -> Result<Geometry, ShapefileError> {
58    match shape {
59        shapefile::Shape::Point(p) => Ok(Geometry::Point(Point {
60            coord: GeoCoord::new(p.y, p.x, 0.0),
61        })),
62        shapefile::Shape::PointZ(p) => Ok(Geometry::Point(Point {
63            coord: GeoCoord::new(p.y, p.x, p.z),
64        })),
65        shapefile::Shape::Polyline(pl) => {
66            let lines: Vec<LineString> = pl
67                .parts()
68                .iter()
69                .map(|part| LineString {
70                    coords: part
71                        .iter()
72                        .map(|p| GeoCoord::new(p.y, p.x, 0.0))
73                        .collect(),
74                })
75                .collect();
76            if lines.len() == 1 {
77                Ok(Geometry::LineString(lines.into_iter().next().unwrap()))
78            } else {
79                Ok(Geometry::MultiLineString(MultiLineString { lines }))
80            }
81        }
82        shapefile::Shape::PolylineZ(pl) => {
83            let lines: Vec<LineString> = pl
84                .parts()
85                .iter()
86                .map(|part| LineString {
87                    coords: part
88                        .iter()
89                        .map(|p| GeoCoord::new(p.y, p.x, p.z))
90                        .collect(),
91                })
92                .collect();
93            if lines.len() == 1 {
94                Ok(Geometry::LineString(lines.into_iter().next().unwrap()))
95            } else {
96                Ok(Geometry::MultiLineString(MultiLineString { lines }))
97            }
98        }
99        shapefile::Shape::Polygon(pg) => {
100            let rings: Vec<Vec<GeoCoord>> = pg
101                .rings()
102                .iter()
103                .map(|ring| match ring {
104                    shapefile::PolygonRing::Outer(pts)
105                    | shapefile::PolygonRing::Inner(pts) => {
106                        pts.iter().map(|p| GeoCoord::new(p.y, p.x, 0.0)).collect()
107                    }
108                })
109                .collect();
110            rings_to_polygon(rings)
111        }
112        shapefile::Shape::PolygonZ(pg) => {
113            let rings: Vec<Vec<GeoCoord>> = pg
114                .rings()
115                .iter()
116                .map(|ring| match ring {
117                    shapefile::PolygonRing::Outer(pts)
118                    | shapefile::PolygonRing::Inner(pts) => pts
119                        .iter()
120                        .map(|p| GeoCoord::new(p.y, p.x, p.z))
121                        .collect(),
122                })
123                .collect();
124            rings_to_polygon(rings)
125        }
126        shapefile::Shape::Multipoint(mp) => {
127            let points = mp
128                .points()
129                .iter()
130                .map(|p| Point {
131                    coord: GeoCoord::new(p.y, p.x, 0.0),
132                })
133                .collect();
134            Ok(Geometry::MultiPoint(MultiPoint { points }))
135        }
136        shapefile::Shape::MultipointZ(mp) => {
137            let points = mp
138                .points()
139                .iter()
140                .map(|p| Point {
141                    coord: GeoCoord::new(p.y, p.x, p.z),
142                })
143                .collect();
144            Ok(Geometry::MultiPoint(MultiPoint { points }))
145        }
146        shapefile::Shape::NullShape => Ok(Geometry::GeometryCollection(Vec::new())),
147        _ => Err(ShapefileError::UnsupportedShape(
148            "unsupported shape variant".into(),
149        )),
150    }
151}
152
153#[cfg(feature = "shapefile")]
154fn rings_to_polygon(rings: Vec<Vec<GeoCoord>>) -> Result<Geometry, ShapefileError> {
155    if rings.is_empty() {
156        return Ok(Geometry::Polygon(crate::geometry::Polygon {
157            exterior: Vec::new(),
158            interiors: Vec::new(),
159        }));
160    }
161    let exterior = rings[0].clone();
162    let interiors = rings[1..].to_vec();
163    Ok(Geometry::Polygon(crate::geometry::Polygon {
164        exterior,
165        interiors,
166    }))
167}
168
169// ---------------------------------------------------------------------------
170// Tests
171// ---------------------------------------------------------------------------
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn error_display() {
179        let e = ShapefileError::Read("bad".into());
180        assert!(e.to_string().contains("bad"));
181
182        let e = ShapefileError::UnsupportedShape("Multipatch".into());
183        assert!(e.to_string().contains("Multipatch"));
184    }
185
186    #[cfg(feature = "shapefile")]
187    #[test]
188    fn parse_invalid_bytes() {
189        let result = parse_shapefile(b"not a shapefile");
190        assert!(result.is_err());
191    }
192}