use crate::geometry::*;
use rustial_math::GeoCoord;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ShapefileError {
#[error("shapefile read error: {0}")]
Read(String),
#[error("unsupported shape type: {0}")]
UnsupportedShape(String),
}
#[cfg(feature = "shapefile")]
pub fn parse_shapefile(shp_bytes: &[u8]) -> Result<FeatureCollection, ShapefileError> {
use std::io::Cursor;
let mut reader = shapefile::ShapeReader::new(Cursor::new(shp_bytes))
.map_err(|e| ShapefileError::Read(e.to_string()))?;
let mut features = Vec::new();
for shape_result in reader.iter_shapes_as::<shapefile::Shape>() {
let shape = shape_result.map_err(|e| ShapefileError::Read(e.to_string()))?;
let geometry = shape_to_geometry(shape)?;
features.push(Feature {
geometry,
properties: std::collections::HashMap::new(),
});
}
Ok(FeatureCollection { features })
}
#[cfg(feature = "shapefile")]
fn shape_to_geometry(shape: shapefile::Shape) -> Result<Geometry, ShapefileError> {
match shape {
shapefile::Shape::Point(p) => Ok(Geometry::Point(Point {
coord: GeoCoord::new(p.y, p.x, 0.0),
})),
shapefile::Shape::PointZ(p) => Ok(Geometry::Point(Point {
coord: GeoCoord::new(p.y, p.x, p.z),
})),
shapefile::Shape::Polyline(pl) => {
let lines: Vec<LineString> = pl
.parts()
.iter()
.map(|part| LineString {
coords: part.iter().map(|p| GeoCoord::new(p.y, p.x, 0.0)).collect(),
})
.collect();
if lines.len() == 1 {
#[allow(clippy::unwrap_used)]
Ok(Geometry::LineString(lines.into_iter().next().unwrap()))
} else {
Ok(Geometry::MultiLineString(MultiLineString { lines }))
}
}
shapefile::Shape::PolylineZ(pl) => {
let lines: Vec<LineString> = pl
.parts()
.iter()
.map(|part| LineString {
coords: part.iter().map(|p| GeoCoord::new(p.y, p.x, p.z)).collect(),
})
.collect();
if lines.len() == 1 {
#[allow(clippy::unwrap_used)]
Ok(Geometry::LineString(lines.into_iter().next().unwrap()))
} else {
Ok(Geometry::MultiLineString(MultiLineString { lines }))
}
}
shapefile::Shape::Polygon(pg) => {
let rings: Vec<Vec<GeoCoord>> = pg
.rings()
.iter()
.map(|ring| match ring {
shapefile::PolygonRing::Outer(pts) | shapefile::PolygonRing::Inner(pts) => {
pts.iter().map(|p| GeoCoord::new(p.y, p.x, 0.0)).collect()
}
})
.collect();
rings_to_polygon(rings)
}
shapefile::Shape::PolygonZ(pg) => {
let rings: Vec<Vec<GeoCoord>> = pg
.rings()
.iter()
.map(|ring| match ring {
shapefile::PolygonRing::Outer(pts) | shapefile::PolygonRing::Inner(pts) => {
pts.iter().map(|p| GeoCoord::new(p.y, p.x, p.z)).collect()
}
})
.collect();
rings_to_polygon(rings)
}
shapefile::Shape::Multipoint(mp) => {
let points = mp
.points()
.iter()
.map(|p| Point {
coord: GeoCoord::new(p.y, p.x, 0.0),
})
.collect();
Ok(Geometry::MultiPoint(MultiPoint { points }))
}
shapefile::Shape::MultipointZ(mp) => {
let points = mp
.points()
.iter()
.map(|p| Point {
coord: GeoCoord::new(p.y, p.x, p.z),
})
.collect();
Ok(Geometry::MultiPoint(MultiPoint { points }))
}
shapefile::Shape::NullShape => Ok(Geometry::GeometryCollection(Vec::new())),
_ => Err(ShapefileError::UnsupportedShape(
"unsupported shape variant".into(),
)),
}
}
#[cfg(feature = "shapefile")]
fn rings_to_polygon(rings: Vec<Vec<GeoCoord>>) -> Result<Geometry, ShapefileError> {
if rings.is_empty() {
return Ok(Geometry::Polygon(crate::geometry::Polygon {
exterior: Vec::new(),
interiors: Vec::new(),
}));
}
let exterior = rings[0].clone();
let interiors = rings[1..].to_vec();
Ok(Geometry::Polygon(crate::geometry::Polygon {
exterior,
interiors,
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_display() {
let e = ShapefileError::Read("bad".into());
assert!(e.to_string().contains("bad"));
let e = ShapefileError::UnsupportedShape("Multipatch".into());
assert!(e.to_string().contains("Multipatch"));
}
#[cfg(feature = "shapefile")]
#[test]
fn parse_invalid_bytes() {
let result = parse_shapefile(b"not a shapefile");
assert!(result.is_err());
}
}