Skip to main content

geonative_shapefile/
shape.rs

1//! Decode an individual shape record into a `geonative_core::Geometry`.
2//!
3//! ## Per-type layouts (2D only — Z/M deferred to v0.2)
4//!
5//! All offsets are within the record's **content** (i.e. after the 8-byte
6//! record header). The first 4 bytes are always the LE shape-type code.
7//!
8//! - **Point (type 1)** — `type(4) + x(8) + y(8)` = 20 bytes
9//! - **MultiPoint (type 8)** — `type(4) + bbox(32) + num_points(4) +
10//!   points(16*N)`
11//! - **PolyLine (type 3) / Polygon (type 5)** — `type(4) + bbox(32) +
12//!   num_parts(4) + num_points(4) + parts[num_parts](4*P) +
13//!   points[num_points](16*N)`. Parts are 0-based indices into the points
14//!   array marking the start of each part/ring.
15//!
16//! ## Polygon ring orientation
17//!
18//! Shapefile uses **CW = exterior, CCW = hole** — the opposite of the OGC
19//! convention used by geonative-core. We compute each ring's signed area to
20//! determine its role, then reverse every ring so the emitted polygon ends
21//! up CCW-exterior / CW-hole (OGC) as the IR expects.
22
23use geonative_core::{Coord, Geometry, GeometryType, LineString, Polygon};
24
25use crate::bytes::Cursor;
26use crate::error::{Result, ShpError};
27use crate::header::ShapeType;
28
29pub fn decode_record_content(content: &[u8]) -> Result<Geometry> {
30    let mut c = Cursor::new(content);
31    let stype = ShapeType::from_i32(c.read_i32_le()?)?;
32    match stype {
33        ShapeType::Null => Ok(Geometry::Empty(GeometryType::Point)),
34        ShapeType::Point => decode_point(&mut c),
35        ShapeType::Multipoint => decode_multipoint(&mut c),
36        ShapeType::Polyline => decode_polyline_or_polygon(&mut c, /* polygon */ false),
37        ShapeType::Polygon => decode_polyline_or_polygon(&mut c, /* polygon */ true),
38        other => Err(ShpError::unsupported(format!(
39            "shape type {other:?} (v0.1 supports 2D Null/Point/Polyline/Polygon/MultiPoint)"
40        ))),
41    }
42}
43
44fn decode_point(c: &mut Cursor<'_>) -> Result<Geometry> {
45    let x = c.read_f64_le()?;
46    let y = c.read_f64_le()?;
47    Ok(Geometry::Point(Coord {
48        x,
49        y,
50        z: None,
51        m: None,
52    }))
53}
54
55fn decode_multipoint(c: &mut Cursor<'_>) -> Result<Geometry> {
56    // Skip 32-byte bbox.
57    c.read_bytes(32)?;
58    let n = c.read_i32_le()? as usize;
59    let mut coords = Vec::with_capacity(n);
60    for _ in 0..n {
61        let x = c.read_f64_le()?;
62        let y = c.read_f64_le()?;
63        coords.push(Coord {
64            x,
65            y,
66            z: None,
67            m: None,
68        });
69    }
70    if coords.is_empty() {
71        Ok(Geometry::Empty(GeometryType::MultiPoint))
72    } else {
73        Ok(Geometry::MultiPoint(coords))
74    }
75}
76
77fn decode_polyline_or_polygon(c: &mut Cursor<'_>, is_polygon: bool) -> Result<Geometry> {
78    // Skip 32-byte bbox.
79    c.read_bytes(32)?;
80    let n_parts = c.read_i32_le()? as usize;
81    let n_points = c.read_i32_le()? as usize;
82    if n_parts == 0 || n_points == 0 {
83        return Ok(if is_polygon {
84            Geometry::Empty(GeometryType::Polygon)
85        } else {
86            Geometry::Empty(GeometryType::LineString)
87        });
88    }
89    let mut parts: Vec<usize> = Vec::with_capacity(n_parts + 1);
90    for _ in 0..n_parts {
91        parts.push(c.read_i32_le()? as usize);
92    }
93    parts.push(n_points);
94
95    let mut all_coords = Vec::with_capacity(n_points);
96    for _ in 0..n_points {
97        let x = c.read_f64_le()?;
98        let y = c.read_f64_le()?;
99        all_coords.push(Coord {
100            x,
101            y,
102            z: None,
103            m: None,
104        });
105    }
106
107    let mut part_rings: Vec<LineString> = Vec::with_capacity(n_parts);
108    for w in parts.windows(2) {
109        let (start, end) = (w[0], w[1]);
110        if start > end || end > n_points {
111            return Err(ShpError::malformed(format!(
112                "part indices out of range: {start}..{end} (total {n_points})"
113            )));
114        }
115        part_rings.push(LineString::new(all_coords[start..end].to_vec()));
116    }
117
118    if is_polygon {
119        let polygons = group_rings_esri_to_ogc(part_rings);
120        if polygons.len() == 1 {
121            Ok(Geometry::Polygon(polygons.into_iter().next().unwrap()))
122        } else {
123            Ok(Geometry::MultiPolygon(polygons))
124        }
125    } else if part_rings.len() == 1 {
126        Ok(Geometry::LineString(part_rings.into_iter().next().unwrap()))
127    } else {
128        Ok(Geometry::MultiLineString(part_rings))
129    }
130}
131
132/// Group Shapefile rings (CW outer, CCW hole) into OGC polygons (CCW outer,
133/// CW hole) by reversing every ring after classifying. Same logic we use in
134/// `geonative-filegdb`.
135fn group_rings_esri_to_ogc(rings: Vec<LineString>) -> Vec<Polygon> {
136    let mut polygons: Vec<Polygon> = Vec::new();
137    for ring in rings {
138        let is_outer_esri = signed_area(&ring.coords) < 0.0;
139        let mut reversed = ring;
140        reversed.coords.reverse();
141        if is_outer_esri {
142            polygons.push(Polygon::new(reversed, Vec::new()));
143        } else if let Some(last) = polygons.last_mut() {
144            last.holes.push(reversed);
145        } else {
146            polygons.push(Polygon::new(reversed, Vec::new()));
147        }
148    }
149    polygons
150}
151
152fn signed_area(coords: &[Coord]) -> f64 {
153    if coords.len() < 3 {
154        return 0.0;
155    }
156    let mut sum = 0.0;
157    let n = coords.len();
158    for i in 0..n {
159        let j = (i + 1) % n;
160        sum += coords[i].x * coords[j].y - coords[j].x * coords[i].y;
161    }
162    sum * 0.5
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    fn point_bytes(x: f64, y: f64) -> Vec<u8> {
170        let mut b = Vec::new();
171        b.extend_from_slice(&1i32.to_le_bytes()); // type
172        b.extend_from_slice(&x.to_le_bytes());
173        b.extend_from_slice(&y.to_le_bytes());
174        b
175    }
176
177    fn polyline_bytes(parts: &[Vec<(f64, f64)>]) -> Vec<u8> {
178        let mut b = Vec::new();
179        b.extend_from_slice(&3i32.to_le_bytes()); // type Polyline
180        for _ in 0..4 {
181            b.extend_from_slice(&0.0f64.to_le_bytes()); // bbox
182        }
183        b.extend_from_slice(&(parts.len() as i32).to_le_bytes());
184        let n_points: i32 = parts.iter().map(|p| p.len() as i32).sum();
185        b.extend_from_slice(&n_points.to_le_bytes());
186        let mut idx = 0i32;
187        for part in parts {
188            b.extend_from_slice(&idx.to_le_bytes());
189            idx += part.len() as i32;
190        }
191        for part in parts {
192            for &(x, y) in part {
193                b.extend_from_slice(&x.to_le_bytes());
194                b.extend_from_slice(&y.to_le_bytes());
195            }
196        }
197        b
198    }
199
200    #[test]
201    fn decode_simple_point() {
202        let g = decode_record_content(&point_bytes(1.5, -3.2)).unwrap();
203        match g {
204            Geometry::Point(c) => {
205                assert_eq!(c.x, 1.5);
206                assert_eq!(c.y, -3.2);
207            }
208            _ => panic!("expected Point"),
209        }
210    }
211
212    #[test]
213    fn decode_single_part_polyline_is_linestring() {
214        let pts = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)];
215        let g = decode_record_content(&polyline_bytes(&[pts])).unwrap();
216        match g {
217            Geometry::LineString(ls) => assert_eq!(ls.coords.len(), 3),
218            _ => panic!("expected LineString"),
219        }
220    }
221
222    #[test]
223    fn decode_two_part_polyline_is_multilinestring() {
224        let g = decode_record_content(&polyline_bytes(&[
225            vec![(0.0, 0.0), (1.0, 1.0)],
226            vec![(10.0, 10.0), (11.0, 11.0), (12.0, 12.0)],
227        ]))
228        .unwrap();
229        match g {
230            Geometry::MultiLineString(parts) => {
231                assert_eq!(parts.len(), 2);
232                assert_eq!(parts[0].coords.len(), 2);
233                assert_eq!(parts[1].coords.len(), 3);
234            }
235            _ => panic!("expected MultiLineString"),
236        }
237    }
238
239    #[test]
240    fn null_shape_returns_empty() {
241        let b = 0i32.to_le_bytes();
242        let g = decode_record_content(&b).unwrap();
243        assert!(matches!(g, Geometry::Empty(GeometryType::Point)));
244    }
245
246    #[test]
247    fn polygon_ring_orientation_flipped_to_ogc() {
248        // Two CW rings (Esri outer) → expect 2 OGC polygons (CCW exterior each).
249        let cw_outer: Vec<(f64, f64)> = vec![
250            (0.0, 0.0),
251            (0.0, 10.0),
252            (10.0, 10.0),
253            (10.0, 0.0),
254            (0.0, 0.0),
255        ];
256        // Build polygon bytes with type=5
257        let mut b = Vec::new();
258        b.extend_from_slice(&5i32.to_le_bytes()); // Polygon
259        for _ in 0..4 {
260            b.extend_from_slice(&0.0f64.to_le_bytes());
261        }
262        b.extend_from_slice(&1i32.to_le_bytes()); // 1 part
263        b.extend_from_slice(&(cw_outer.len() as i32).to_le_bytes());
264        b.extend_from_slice(&0i32.to_le_bytes()); // part 0 starts at point 0
265        for &(x, y) in &cw_outer {
266            b.extend_from_slice(&x.to_le_bytes());
267            b.extend_from_slice(&y.to_le_bytes());
268        }
269
270        let g = decode_record_content(&b).unwrap();
271        let poly = match g {
272            Geometry::Polygon(p) => p,
273            other => panic!("expected Polygon, got {:?}", other),
274        };
275        // After OGC reorientation, the exterior must be CCW (signed area > 0).
276        assert!(signed_area(&poly.exterior.coords) > 0.0);
277    }
278}