Skip to main content

oxigdal_shapefile/shp/
header.rs

1//! Shapefile header parsing
2//!
3//! This module handles parsing and writing the Shapefile main file header (100 bytes).
4//! The header contains metadata about the file including file code, version, shape type,
5//! and bounding box information.
6//!
7//! # Shapefile Header Structure (100 bytes)
8//!
9//! | Bytes | Type | Endian | Description |
10//! |-------|------|--------|-------------|
11//! | 0-3   | i32  | Big    | File Code (9994) |
12//! | 4-23  | -    | -      | Unused (zeros) |
13//! | 24-27 | i32  | Big    | File Length (in 16-bit words) |
14//! | 28-31 | i32  | Little | Version (1000) |
15//! | 32-35 | i32  | Little | Shape Type |
16//! | 36-67 | f64  | Little | Bounding Box (Xmin, Ymin, Xmax, Ymax) |
17//! | 68-99 | f64  | Little | Z/M ranges (Zmin, Zmax, Mmin, Mmax) |
18
19use crate::error::{Result, ShapefileError};
20use crate::shp::shapes::ShapeType;
21use byteorder::{BigEndian, LittleEndian, ReadBytesExt, WriteBytesExt};
22use std::io::{Read, Write};
23
24/// Shapefile file code (magic number)
25pub const FILE_CODE: i32 = 9994;
26
27/// Shapefile version
28pub const VERSION: i32 = 1000;
29
30/// Shapefile header size in bytes
31pub const HEADER_SIZE: usize = 100;
32
33/// Shapefile header
34#[derive(Debug, Clone, PartialEq)]
35pub struct ShapefileHeader {
36    /// File code (should be 9994)
37    pub file_code: i32,
38    /// File length in 16-bit words (including header)
39    pub file_length: i32,
40    /// Version (should be 1000)
41    pub version: i32,
42    /// Shape type
43    pub shape_type: ShapeType,
44    /// Bounding box
45    pub bbox: BoundingBox,
46}
47
48/// Bounding box with optional Z and M ranges
49#[derive(Debug, Clone, PartialEq)]
50pub struct BoundingBox {
51    /// Minimum X
52    pub x_min: f64,
53    /// Minimum Y
54    pub y_min: f64,
55    /// Maximum X
56    pub x_max: f64,
57    /// Maximum Y
58    pub y_max: f64,
59    /// Minimum Z (if present)
60    pub z_min: Option<f64>,
61    /// Maximum Z (if present)
62    pub z_max: Option<f64>,
63    /// Minimum M (if present)
64    pub m_min: Option<f64>,
65    /// Maximum M (if present)
66    pub m_max: Option<f64>,
67}
68
69impl BoundingBox {
70    /// Creates a new 2D bounding box
71    pub fn new_2d(x_min: f64, y_min: f64, x_max: f64, y_max: f64) -> Result<Self> {
72        if x_min > x_max {
73            return Err(ShapefileError::InvalidBbox {
74                message: format!("x_min ({}) > x_max ({})", x_min, x_max),
75            });
76        }
77        if y_min > y_max {
78            return Err(ShapefileError::InvalidBbox {
79                message: format!("y_min ({}) > y_max ({})", y_min, y_max),
80            });
81        }
82
83        Ok(Self {
84            x_min,
85            y_min,
86            x_max,
87            y_max,
88            z_min: None,
89            z_max: None,
90            m_min: None,
91            m_max: None,
92        })
93    }
94
95    /// Creates a new 3D bounding box with Z range
96    pub fn new_3d(
97        x_min: f64,
98        y_min: f64,
99        x_max: f64,
100        y_max: f64,
101        z_min: f64,
102        z_max: f64,
103    ) -> Result<Self> {
104        let mut bbox = Self::new_2d(x_min, y_min, x_max, y_max)?;
105        if z_min > z_max {
106            return Err(ShapefileError::InvalidBbox {
107                message: format!("z_min ({}) > z_max ({})", z_min, z_max),
108            });
109        }
110        bbox.z_min = Some(z_min);
111        bbox.z_max = Some(z_max);
112        Ok(bbox)
113    }
114
115    /// Checks if Z values are valid (not NaN/Inf for optional Z values)
116    #[allow(dead_code)]
117    fn has_valid_z(&self) -> bool {
118        match (self.z_min, self.z_max) {
119            (Some(z_min), Some(z_max)) => z_min.is_finite() && z_max.is_finite(),
120            (None, None) => true,
121            _ => false,
122        }
123    }
124
125    /// Checks if M values are valid (not NaN/Inf for optional M values)
126    #[allow(dead_code)]
127    fn has_valid_m(&self) -> bool {
128        match (self.m_min, self.m_max) {
129            (Some(m_min), Some(m_max)) => m_min.is_finite() && m_max.is_finite(),
130            (None, None) => true,
131            _ => false,
132        }
133    }
134}
135
136impl ShapefileHeader {
137    /// Creates a new Shapefile header
138    pub fn new(shape_type: ShapeType, bbox: BoundingBox) -> Self {
139        Self {
140            file_code: FILE_CODE,
141            file_length: 50, // Will be updated when writing
142            version: VERSION,
143            shape_type,
144            bbox,
145        }
146    }
147
148    /// Reads a Shapefile header from a reader
149    pub fn read<R: Read>(reader: &mut R) -> Result<Self> {
150        // Read file code (big endian)
151        let file_code = reader
152            .read_i32::<BigEndian>()
153            .map_err(|_| ShapefileError::unexpected_eof("reading file code"))?;
154
155        if file_code != FILE_CODE {
156            return Err(ShapefileError::InvalidFileCode { actual: file_code });
157        }
158
159        // Skip unused bytes (20 bytes)
160        let mut unused = [0u8; 20];
161        reader
162            .read_exact(&mut unused)
163            .map_err(|_| ShapefileError::unexpected_eof("reading unused header bytes"))?;
164
165        // Read file length (big endian, in 16-bit words)
166        let file_length = reader
167            .read_i32::<BigEndian>()
168            .map_err(|_| ShapefileError::unexpected_eof("reading file length"))?;
169
170        // Read version (little endian)
171        let version = reader
172            .read_i32::<LittleEndian>()
173            .map_err(|_| ShapefileError::unexpected_eof("reading version"))?;
174
175        if version != VERSION {
176            return Err(ShapefileError::InvalidVersion { version });
177        }
178
179        // Read shape type (little endian)
180        let shape_type_code = reader
181            .read_i32::<LittleEndian>()
182            .map_err(|_| ShapefileError::unexpected_eof("reading shape type"))?;
183
184        let shape_type = ShapeType::from_code(shape_type_code)?;
185
186        // Read bounding box (little endian)
187        let x_min = reader
188            .read_f64::<LittleEndian>()
189            .map_err(|_| ShapefileError::unexpected_eof("reading x_min"))?;
190        let y_min = reader
191            .read_f64::<LittleEndian>()
192            .map_err(|_| ShapefileError::unexpected_eof("reading y_min"))?;
193        let x_max = reader
194            .read_f64::<LittleEndian>()
195            .map_err(|_| ShapefileError::unexpected_eof("reading x_max"))?;
196        let y_max = reader
197            .read_f64::<LittleEndian>()
198            .map_err(|_| ShapefileError::unexpected_eof("reading y_max"))?;
199
200        // Read Z range (little endian)
201        let z_min = reader
202            .read_f64::<LittleEndian>()
203            .map_err(|_| ShapefileError::unexpected_eof("reading z_min"))?;
204        let z_max = reader
205            .read_f64::<LittleEndian>()
206            .map_err(|_| ShapefileError::unexpected_eof("reading z_max"))?;
207
208        // Read M range (little endian)
209        let m_min = reader
210            .read_f64::<LittleEndian>()
211            .map_err(|_| ShapefileError::unexpected_eof("reading m_min"))?;
212        let m_max = reader
213            .read_f64::<LittleEndian>()
214            .map_err(|_| ShapefileError::unexpected_eof("reading m_max"))?;
215
216        // Build bounding box
217        let mut bbox = BoundingBox::new_2d(x_min, y_min, x_max, y_max)?;
218
219        // Add Z range if valid (not NaN or very large negative number used as "no data")
220        if z_min.is_finite() && z_max.is_finite() && z_min > -1e38 {
221            bbox.z_min = Some(z_min);
222            bbox.z_max = Some(z_max);
223        }
224
225        // Add M range if valid
226        if m_min.is_finite() && m_max.is_finite() && m_min > -1e38 {
227            bbox.m_min = Some(m_min);
228            bbox.m_max = Some(m_max);
229        }
230
231        Ok(Self {
232            file_code,
233            file_length,
234            version,
235            shape_type,
236            bbox,
237        })
238    }
239
240    /// Writes a Shapefile header to a writer
241    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
242        // Write file code (big endian)
243        writer
244            .write_i32::<BigEndian>(self.file_code)
245            .map_err(ShapefileError::Io)?;
246
247        // Write unused bytes (20 bytes of zeros)
248        writer.write_all(&[0u8; 20]).map_err(ShapefileError::Io)?;
249
250        // Write file length (big endian, in 16-bit words)
251        writer
252            .write_i32::<BigEndian>(self.file_length)
253            .map_err(ShapefileError::Io)?;
254
255        // Write version (little endian)
256        writer
257            .write_i32::<LittleEndian>(self.version)
258            .map_err(ShapefileError::Io)?;
259
260        // Write shape type (little endian)
261        writer
262            .write_i32::<LittleEndian>(self.shape_type.to_code())
263            .map_err(ShapefileError::Io)?;
264
265        // Write bounding box (little endian)
266        writer
267            .write_f64::<LittleEndian>(self.bbox.x_min)
268            .map_err(ShapefileError::Io)?;
269        writer
270            .write_f64::<LittleEndian>(self.bbox.y_min)
271            .map_err(ShapefileError::Io)?;
272        writer
273            .write_f64::<LittleEndian>(self.bbox.x_max)
274            .map_err(ShapefileError::Io)?;
275        writer
276            .write_f64::<LittleEndian>(self.bbox.y_max)
277            .map_err(ShapefileError::Io)?;
278
279        // Write Z range (little endian)
280        let z_min = self.bbox.z_min.unwrap_or(0.0);
281        let z_max = self.bbox.z_max.unwrap_or(0.0);
282        writer
283            .write_f64::<LittleEndian>(z_min)
284            .map_err(ShapefileError::Io)?;
285        writer
286            .write_f64::<LittleEndian>(z_max)
287            .map_err(ShapefileError::Io)?;
288
289        // Write M range (little endian)
290        let m_min = self.bbox.m_min.unwrap_or(0.0);
291        let m_max = self.bbox.m_max.unwrap_or(0.0);
292        writer
293            .write_f64::<LittleEndian>(m_min)
294            .map_err(ShapefileError::Io)?;
295        writer
296            .write_f64::<LittleEndian>(m_max)
297            .map_err(ShapefileError::Io)?;
298
299        Ok(())
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306    use std::io::Cursor;
307
308    #[test]
309    fn test_bounding_box_2d() {
310        let bbox = BoundingBox::new_2d(-180.0, -90.0, 180.0, 90.0);
311        assert!(bbox.is_ok());
312        let bbox = bbox.expect("valid 2d bounding box");
313        assert_eq!(bbox.x_min, -180.0);
314        assert_eq!(bbox.y_max, 90.0);
315        assert!(bbox.z_min.is_none());
316    }
317
318    #[test]
319    fn test_bounding_box_invalid() {
320        let bbox = BoundingBox::new_2d(180.0, -90.0, -180.0, 90.0);
321        assert!(bbox.is_err());
322    }
323
324    #[test]
325    fn test_header_round_trip() {
326        let bbox = BoundingBox::new_2d(-180.0, -90.0, 180.0, 90.0).expect("valid bounding box");
327        let header = ShapefileHeader::new(ShapeType::Point, bbox);
328
329        let mut buffer = Vec::new();
330        header.write(&mut buffer).expect("write header to buffer");
331
332        assert_eq!(buffer.len(), HEADER_SIZE);
333
334        let mut cursor = Cursor::new(buffer);
335        let read_header = ShapefileHeader::read(&mut cursor).expect("read header from cursor");
336
337        assert_eq!(read_header.file_code, FILE_CODE);
338        assert_eq!(read_header.version, VERSION);
339        assert_eq!(read_header.shape_type, ShapeType::Point);
340        assert_eq!(read_header.bbox.x_min, -180.0);
341    }
342
343    #[test]
344    fn test_invalid_file_code() {
345        let mut buffer = vec![0u8; HEADER_SIZE];
346        // Write wrong file code
347        let mut cursor = Cursor::new(&mut buffer);
348        cursor
349            .write_i32::<BigEndian>(1234)
350            .expect("write invalid file code to cursor");
351
352        let mut cursor = Cursor::new(&buffer[..]);
353        let result = ShapefileHeader::read(&mut cursor);
354        assert!(result.is_err());
355    }
356}