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.iter().map(|p| GeoCoord::new(p.y, p.x, 0.0)).collect(),
71                })
72                .collect();
73            if lines.len() == 1 {
74                #[allow(clippy::unwrap_used)]
75                Ok(Geometry::LineString(lines.into_iter().next().unwrap()))
76            } else {
77                Ok(Geometry::MultiLineString(MultiLineString { lines }))
78            }
79        }
80        shapefile::Shape::PolylineZ(pl) => {
81            let lines: Vec<LineString> = pl
82                .parts()
83                .iter()
84                .map(|part| LineString {
85                    coords: part.iter().map(|p| GeoCoord::new(p.y, p.x, p.z)).collect(),
86                })
87                .collect();
88            if lines.len() == 1 {
89                #[allow(clippy::unwrap_used)]
90                Ok(Geometry::LineString(lines.into_iter().next().unwrap()))
91            } else {
92                Ok(Geometry::MultiLineString(MultiLineString { lines }))
93            }
94        }
95        shapefile::Shape::Polygon(pg) => {
96            let rings: Vec<Vec<GeoCoord>> = pg
97                .rings()
98                .iter()
99                .map(|ring| match ring {
100                    shapefile::PolygonRing::Outer(pts) | shapefile::PolygonRing::Inner(pts) => {
101                        pts.iter().map(|p| GeoCoord::new(p.y, p.x, 0.0)).collect()
102                    }
103                })
104                .collect();
105            rings_to_polygon(rings)
106        }
107        shapefile::Shape::PolygonZ(pg) => {
108            let rings: Vec<Vec<GeoCoord>> = pg
109                .rings()
110                .iter()
111                .map(|ring| match ring {
112                    shapefile::PolygonRing::Outer(pts) | shapefile::PolygonRing::Inner(pts) => {
113                        pts.iter().map(|p| GeoCoord::new(p.y, p.x, p.z)).collect()
114                    }
115                })
116                .collect();
117            rings_to_polygon(rings)
118        }
119        shapefile::Shape::Multipoint(mp) => {
120            let points = mp
121                .points()
122                .iter()
123                .map(|p| Point {
124                    coord: GeoCoord::new(p.y, p.x, 0.0),
125                })
126                .collect();
127            Ok(Geometry::MultiPoint(MultiPoint { points }))
128        }
129        shapefile::Shape::MultipointZ(mp) => {
130            let points = mp
131                .points()
132                .iter()
133                .map(|p| Point {
134                    coord: GeoCoord::new(p.y, p.x, p.z),
135                })
136                .collect();
137            Ok(Geometry::MultiPoint(MultiPoint { points }))
138        }
139        shapefile::Shape::NullShape => Ok(Geometry::GeometryCollection(Vec::new())),
140        _ => Err(ShapefileError::UnsupportedShape(
141            "unsupported shape variant".into(),
142        )),
143    }
144}
145
146#[cfg(feature = "shapefile")]
147fn rings_to_polygon(rings: Vec<Vec<GeoCoord>>) -> Result<Geometry, ShapefileError> {
148    if rings.is_empty() {
149        return Ok(Geometry::Polygon(crate::geometry::Polygon {
150            exterior: Vec::new(),
151            interiors: Vec::new(),
152        }));
153    }
154    let exterior = rings[0].clone();
155    let interiors = rings[1..].to_vec();
156    Ok(Geometry::Polygon(crate::geometry::Polygon {
157        exterior,
158        interiors,
159    }))
160}
161
162// ---------------------------------------------------------------------------
163// Tests
164// ---------------------------------------------------------------------------
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn error_display() {
172        let e = ShapefileError::Read("bad".into());
173        assert!(e.to_string().contains("bad"));
174
175        let e = ShapefileError::UnsupportedShape("Multipatch".into());
176        assert!(e.to_string().contains("Multipatch"));
177    }
178
179    #[cfg(feature = "shapefile")]
180    #[test]
181    fn parse_invalid_bytes() {
182        let result = parse_shapefile(b"not a shapefile");
183        assert!(result.is_err());
184    }
185}