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
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#[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}