rustial_engine/
shapefile_parser.rs1use crate::geometry::*;
7use rustial_math::GeoCoord;
8use thiserror::Error;
9
10#[derive(Debug, Error)]
12pub enum ShapefileError {
13 #[error("shapefile read error: {0}")]
15 Read(String),
16 #[error("unsupported shape type: {0}")]
18 UnsupportedShape(String),
19}
20
21#[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#[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}