1use shapefile::record::multipoint::GenericMultipoint;
8use shapefile::record::polygon::GenericPolygon;
9use shapefile::record::polyline::GenericPolyline;
10use shapefile::record::traits::{GrowablePoint, HasXY, ShrinkablePoint};
11use shapefile::{Point, PointM, PointZ, PolygonRing, NO_DATA};
12
13use crate::error::{Result, ShapefileError};
14use crate::input::{Geometry, Position};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Dimension {
19 Xy,
21 Xym,
23 Xyz,
25 Xyzm,
27}
28
29impl Dimension {
30 pub fn label(self) -> &'static str {
31 match self {
32 Dimension::Xy => "xy",
33 Dimension::Xym => "xym",
34 Dimension::Xyz => "xyz",
35 Dimension::Xyzm => "xyzm",
36 }
37 }
38
39 pub fn parse(name: &str) -> Option<Self> {
40 match name.to_ascii_lowercase().as_str() {
41 "xy" | "2d" => Some(Dimension::Xy),
42 "xym" => Some(Dimension::Xym),
43 "xyz" | "3d" => Some(Dimension::Xyz),
44 "xyzm" | "xyzm4" | "4d" => Some(Dimension::Xyzm),
45 _ => None,
46 }
47 }
48
49 pub fn from_arity(max_arity: usize) -> Self {
51 match max_arity {
52 0..=2 => Dimension::Xy,
53 3 => Dimension::Xyz,
54 _ => Dimension::Xyzm,
55 }
56 }
57
58 pub fn suffix(self) -> &'static str {
60 match self {
61 Dimension::Xy => "",
62 Dimension::Xym => "M",
63 Dimension::Xyz | Dimension::Xyzm => "Z",
64 }
65 }
66}
67
68pub trait FromPosition: Copy {
70 fn from_position(position: &Position, dimension: Dimension) -> Self;
71}
72
73impl FromPosition for Point {
74 fn from_position(position: &Position, _dimension: Dimension) -> Self {
75 Point::new(position.x, position.y)
76 }
77}
78
79impl FromPosition for PointM {
80 fn from_position(position: &Position, _dimension: Dimension) -> Self {
81 PointM::new(position.x, position.y, position.third.unwrap_or(NO_DATA))
83 }
84}
85
86impl FromPosition for PointZ {
87 fn from_position(position: &Position, dimension: Dimension) -> Self {
88 let z = position.third.unwrap_or(0.0);
89 let m = match dimension {
90 Dimension::Xyzm => position.fourth.unwrap_or(NO_DATA),
91 _ => NO_DATA,
92 };
93 PointZ::new(position.x, position.y, z, m)
94 }
95}
96
97pub trait ShapePoint:
99 FromPosition + Copy + PartialEq + HasXY + GrowablePoint + ShrinkablePoint
100{
101}
102
103impl<T> ShapePoint for T where
104 T: FromPosition + Copy + PartialEq + HasXY + GrowablePoint + ShrinkablePoint
105{
106}
107
108fn convert<P: ShapePoint>(positions: &[Position], dimension: Dimension) -> Vec<P> {
109 positions
110 .iter()
111 .map(|position| P::from_position(position, dimension))
112 .collect()
113}
114
115pub fn to_point<P: ShapePoint>(
117 geometry: &Geometry,
118 dimension: Dimension,
119 index: usize,
120) -> Result<P> {
121 match geometry {
122 Geometry::Point(position) => Ok(P::from_position(position, dimension)),
123 other => Err(mismatch(other, "Point", index)),
124 }
125}
126
127pub fn to_multipoint<P: ShapePoint>(
128 geometry: &Geometry,
129 dimension: Dimension,
130 index: usize,
131) -> Result<GenericMultipoint<P>> {
132 let positions: Vec<Position> = match geometry {
135 Geometry::MultiPoint(positions) => positions.clone(),
136 Geometry::Point(position) => vec![*position],
137 other => return Err(mismatch(other, "Multipoint", index)),
138 };
139
140 if positions.is_empty() {
141 return Err(ShapefileError::Feature {
142 index,
143 message: "a multipoint needs at least one coordinate".into(),
144 });
145 }
146
147 Ok(GenericMultipoint::new(convert::<P>(&positions, dimension)))
148}
149
150pub fn to_polyline<P: ShapePoint>(
151 geometry: &Geometry,
152 dimension: Dimension,
153 index: usize,
154) -> Result<GenericPolyline<P>> {
155 let parts: Vec<Vec<Position>> = match geometry {
156 Geometry::LineString(positions) => vec![positions.clone()],
157 Geometry::MultiLineString(parts) => parts.clone(),
158 other => return Err(mismatch(other, "Polyline", index)),
159 };
160
161 if parts.is_empty() {
162 return Err(ShapefileError::Feature {
163 index,
164 message: "a polyline needs at least one part".into(),
165 });
166 }
167
168 if let Some(short) = parts.iter().find(|part| part.len() < 2) {
171 return Err(ShapefileError::Feature {
172 index,
173 message: format!(
174 "every line needs at least 2 coordinates, found one with {}",
175 short.len()
176 ),
177 });
178 }
179
180 let converted = parts
181 .iter()
182 .map(|part| convert::<P>(part, dimension))
183 .collect();
184
185 Ok(GenericPolyline::with_parts(converted))
186}
187
188pub fn to_polygon<P: ShapePoint>(
189 geometry: &Geometry,
190 dimension: Dimension,
191 index: usize,
192) -> Result<GenericPolygon<P>> {
193 let polygons: Vec<Vec<Vec<Position>>> = match geometry {
196 Geometry::Polygon(rings) => vec![rings.clone()],
197 Geometry::MultiPolygon(polygons) => polygons.clone(),
198 other => return Err(mismatch(other, "Polygon", index)),
199 };
200
201 let mut rings: Vec<PolygonRing<P>> = Vec::new();
202 for polygon in &polygons {
203 for (ring_index, ring) in polygon.iter().enumerate() {
204 if ring.len() < 3 {
205 return Err(ShapefileError::Feature {
206 index,
207 message: format!(
208 "every polygon ring needs at least 3 coordinates, found one with {}",
209 ring.len()
210 ),
211 });
212 }
213 let points = convert::<P>(ring, dimension);
214 rings.push(if ring_index == 0 {
217 PolygonRing::Outer(points)
218 } else {
219 PolygonRing::Inner(points)
220 });
221 }
222 }
223
224 if rings.is_empty() {
225 return Err(ShapefileError::Feature {
226 index,
227 message: "a polygon needs at least one ring".into(),
228 });
229 }
230
231 Ok(GenericPolygon::with_rings(rings))
232}
233
234fn mismatch(geometry: &Geometry, expected: &str, index: usize) -> ShapefileError {
235 ShapefileError::MixedGeometry {
236 first: expected.to_string(),
237 other: geometry.family().label().to_string(),
238 index,
239 }
240}