Skip to main content

oxigdal_shapefile/shp/
shapes.rs

1//! Shapefile shape type definitions
2//!
3//! This module defines all shape types supported by the Shapefile format,
4//! including 2D, Z (3D), and M (measured) variants.
5
6use crate::error::{Result, ShapefileError};
7use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
8use std::io::{Read, Write};
9
10/// Shapefile shape types
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum ShapeType {
13    /// Null shape (empty)
14    Null,
15    /// Point (2D)
16    Point,
17    /// PolyLine (2D)
18    PolyLine,
19    /// Polygon (2D)
20    Polygon,
21    /// MultiPoint (2D)
22    MultiPoint,
23    /// Point with Z coordinate
24    PointZ,
25    /// PolyLine with Z coordinates
26    PolyLineZ,
27    /// Polygon with Z coordinates
28    PolygonZ,
29    /// MultiPoint with Z coordinates
30    MultiPointZ,
31    /// Point with M value (measure)
32    PointM,
33    /// PolyLine with M values
34    PolyLineM,
35    /// Polygon with M values
36    PolygonM,
37    /// MultiPoint with M values
38    MultiPointM,
39    /// MultiPatch (3D surface)
40    MultiPatch,
41}
42
43impl ShapeType {
44    /// Converts a shape type code to a `ShapeType`
45    pub fn from_code(code: i32) -> Result<Self> {
46        match code {
47            0 => Ok(Self::Null),
48            1 => Ok(Self::Point),
49            3 => Ok(Self::PolyLine),
50            5 => Ok(Self::Polygon),
51            8 => Ok(Self::MultiPoint),
52            11 => Ok(Self::PointZ),
53            13 => Ok(Self::PolyLineZ),
54            15 => Ok(Self::PolygonZ),
55            18 => Ok(Self::MultiPointZ),
56            21 => Ok(Self::PointM),
57            23 => Ok(Self::PolyLineM),
58            25 => Ok(Self::PolygonM),
59            28 => Ok(Self::MultiPointM),
60            31 => Ok(Self::MultiPatch),
61            _ => Err(ShapefileError::InvalidShapeType { shape_type: code }),
62        }
63    }
64
65    /// Converts a `ShapeType` to its code
66    pub fn to_code(self) -> i32 {
67        match self {
68            Self::Null => 0,
69            Self::Point => 1,
70            Self::PolyLine => 3,
71            Self::Polygon => 5,
72            Self::MultiPoint => 8,
73            Self::PointZ => 11,
74            Self::PolyLineZ => 13,
75            Self::PolygonZ => 15,
76            Self::MultiPointZ => 18,
77            Self::PointM => 21,
78            Self::PolyLineM => 23,
79            Self::PolygonM => 25,
80            Self::MultiPointM => 28,
81            Self::MultiPatch => 31,
82        }
83    }
84
85    /// Returns true if this shape type has Z coordinates
86    pub fn has_z(self) -> bool {
87        matches!(
88            self,
89            Self::PointZ | Self::PolyLineZ | Self::PolygonZ | Self::MultiPointZ | Self::MultiPatch
90        )
91    }
92
93    /// Returns true if this shape type has M values
94    pub fn has_m(self) -> bool {
95        matches!(
96            self,
97            Self::PointM
98                | Self::PolyLineM
99                | Self::PolygonM
100                | Self::MultiPointM
101                | Self::PointZ
102                | Self::PolyLineZ
103                | Self::PolygonZ
104                | Self::MultiPointZ
105                | Self::MultiPatch
106        )
107    }
108
109    /// Returns the name of the shape type
110    pub fn name(self) -> &'static str {
111        match self {
112            Self::Null => "Null",
113            Self::Point => "Point",
114            Self::PolyLine => "PolyLine",
115            Self::Polygon => "Polygon",
116            Self::MultiPoint => "MultiPoint",
117            Self::PointZ => "PointZ",
118            Self::PolyLineZ => "PolyLineZ",
119            Self::PolygonZ => "PolygonZ",
120            Self::MultiPointZ => "MultiPointZ",
121            Self::PointM => "PointM",
122            Self::PolyLineM => "PolyLineM",
123            Self::PolygonM => "PolygonM",
124            Self::MultiPointM => "MultiPointM",
125            Self::MultiPatch => "MultiPatch",
126        }
127    }
128}
129
130/// A 2D point
131#[derive(Debug, Clone, PartialEq)]
132pub struct Point {
133    /// X coordinate
134    pub x: f64,
135    /// Y coordinate
136    pub y: f64,
137}
138
139impl Point {
140    /// Creates a new point
141    pub fn new(x: f64, y: f64) -> Self {
142        Self { x, y }
143    }
144
145    /// Reads a point from a reader
146    pub fn read<R: Read>(reader: &mut R) -> Result<Self> {
147        let x = reader
148            .read_f64::<LittleEndian>()
149            .map_err(|_| ShapefileError::unexpected_eof("reading point x"))?;
150        let y = reader
151            .read_f64::<LittleEndian>()
152            .map_err(|_| ShapefileError::unexpected_eof("reading point y"))?;
153
154        if !x.is_finite() || !y.is_finite() {
155            return Err(ShapefileError::invalid_coordinates(
156                "point coordinates must be finite",
157            ));
158        }
159
160        Ok(Self { x, y })
161    }
162
163    /// Writes a point to a writer
164    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
165        writer
166            .write_f64::<LittleEndian>(self.x)
167            .map_err(ShapefileError::Io)?;
168        writer
169            .write_f64::<LittleEndian>(self.y)
170            .map_err(ShapefileError::Io)?;
171        Ok(())
172    }
173}
174
175/// A 3D point with Z coordinate
176#[derive(Debug, Clone, PartialEq)]
177pub struct PointZ {
178    /// X coordinate
179    pub x: f64,
180    /// Y coordinate
181    pub y: f64,
182    /// Z coordinate
183    pub z: f64,
184    /// M value (optional measure)
185    pub m: Option<f64>,
186}
187
188impl PointZ {
189    /// Creates a new 3D point
190    pub fn new(x: f64, y: f64, z: f64) -> Self {
191        Self { x, y, z, m: None }
192    }
193
194    /// Creates a new 3D point with M value
195    pub fn new_with_m(x: f64, y: f64, z: f64, m: f64) -> Self {
196        Self {
197            x,
198            y,
199            z,
200            m: Some(m),
201        }
202    }
203
204    /// Reads a PointZ from a reader
205    pub fn read<R: Read>(reader: &mut R) -> Result<Self> {
206        let x = reader
207            .read_f64::<LittleEndian>()
208            .map_err(|_| ShapefileError::unexpected_eof("reading pointz x"))?;
209        let y = reader
210            .read_f64::<LittleEndian>()
211            .map_err(|_| ShapefileError::unexpected_eof("reading pointz y"))?;
212        let z = reader
213            .read_f64::<LittleEndian>()
214            .map_err(|_| ShapefileError::unexpected_eof("reading pointz z"))?;
215
216        if !x.is_finite() || !y.is_finite() || !z.is_finite() {
217            return Err(ShapefileError::invalid_coordinates(
218                "pointz coordinates must be finite",
219            ));
220        }
221
222        // M value is optional
223        let m = match reader.read_f64::<LittleEndian>() {
224            Ok(m_val) if m_val.is_finite() => Some(m_val),
225            _ => None,
226        };
227
228        Ok(Self { x, y, z, m })
229    }
230
231    /// Writes a PointZ to a writer
232    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
233        writer
234            .write_f64::<LittleEndian>(self.x)
235            .map_err(ShapefileError::Io)?;
236        writer
237            .write_f64::<LittleEndian>(self.y)
238            .map_err(ShapefileError::Io)?;
239        writer
240            .write_f64::<LittleEndian>(self.z)
241            .map_err(ShapefileError::Io)?;
242        writer
243            .write_f64::<LittleEndian>(self.m.unwrap_or(0.0))
244            .map_err(ShapefileError::Io)?;
245        Ok(())
246    }
247}
248
249/// A point with M value (measure)
250#[derive(Debug, Clone, PartialEq)]
251pub struct PointM {
252    /// X coordinate
253    pub x: f64,
254    /// Y coordinate
255    pub y: f64,
256    /// M value (measure)
257    pub m: f64,
258}
259
260impl PointM {
261    /// Creates a new point with M value
262    pub fn new(x: f64, y: f64, m: f64) -> Self {
263        Self { x, y, m }
264    }
265
266    /// Reads a PointM from a reader
267    pub fn read<R: Read>(reader: &mut R) -> Result<Self> {
268        let x = reader
269            .read_f64::<LittleEndian>()
270            .map_err(|_| ShapefileError::unexpected_eof("reading pointm x"))?;
271        let y = reader
272            .read_f64::<LittleEndian>()
273            .map_err(|_| ShapefileError::unexpected_eof("reading pointm y"))?;
274        let m = reader
275            .read_f64::<LittleEndian>()
276            .map_err(|_| ShapefileError::unexpected_eof("reading pointm m"))?;
277
278        if !x.is_finite() || !y.is_finite() {
279            return Err(ShapefileError::invalid_coordinates(
280                "pointm coordinates must be finite",
281            ));
282        }
283
284        Ok(Self { x, y, m })
285    }
286
287    /// Writes a PointM to a writer
288    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
289        writer
290            .write_f64::<LittleEndian>(self.x)
291            .map_err(ShapefileError::Io)?;
292        writer
293            .write_f64::<LittleEndian>(self.y)
294            .map_err(ShapefileError::Io)?;
295        writer
296            .write_f64::<LittleEndian>(self.m)
297            .map_err(ShapefileError::Io)?;
298        Ok(())
299    }
300}
301
302/// Bounding box for shapes
303#[derive(Debug, Clone, PartialEq)]
304pub struct Box2D {
305    /// Minimum X
306    pub x_min: f64,
307    /// Minimum Y
308    pub y_min: f64,
309    /// Maximum X
310    pub x_max: f64,
311    /// Maximum Y
312    pub y_max: f64,
313}
314
315impl Box2D {
316    /// Creates a new 2D bounding box
317    pub fn new(x_min: f64, y_min: f64, x_max: f64, y_max: f64) -> Result<Self> {
318        if x_min > x_max {
319            return Err(ShapefileError::InvalidBbox {
320                message: format!("x_min ({}) > x_max ({})", x_min, x_max),
321            });
322        }
323        if y_min > y_max {
324            return Err(ShapefileError::InvalidBbox {
325                message: format!("y_min ({}) > y_max ({})", y_min, y_max),
326            });
327        }
328        Ok(Self {
329            x_min,
330            y_min,
331            x_max,
332            y_max,
333        })
334    }
335
336    /// Reads a 2D bounding box from a reader
337    pub fn read<R: Read>(reader: &mut R) -> Result<Self> {
338        let x_min = reader
339            .read_f64::<LittleEndian>()
340            .map_err(|_| ShapefileError::unexpected_eof("reading bbox x_min"))?;
341        let y_min = reader
342            .read_f64::<LittleEndian>()
343            .map_err(|_| ShapefileError::unexpected_eof("reading bbox y_min"))?;
344        let x_max = reader
345            .read_f64::<LittleEndian>()
346            .map_err(|_| ShapefileError::unexpected_eof("reading bbox x_max"))?;
347        let y_max = reader
348            .read_f64::<LittleEndian>()
349            .map_err(|_| ShapefileError::unexpected_eof("reading bbox y_max"))?;
350
351        Self::new(x_min, y_min, x_max, y_max)
352    }
353
354    /// Writes a 2D bounding box to a writer
355    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
356        writer
357            .write_f64::<LittleEndian>(self.x_min)
358            .map_err(ShapefileError::Io)?;
359        writer
360            .write_f64::<LittleEndian>(self.y_min)
361            .map_err(ShapefileError::Io)?;
362        writer
363            .write_f64::<LittleEndian>(self.x_max)
364            .map_err(ShapefileError::Io)?;
365        writer
366            .write_f64::<LittleEndian>(self.y_max)
367            .map_err(ShapefileError::Io)?;
368        Ok(())
369    }
370
371    /// Computes the bounding box from a list of points
372    pub fn from_points(points: &[Point]) -> Result<Self> {
373        if points.is_empty() {
374            return Err(ShapefileError::invalid_geometry(
375                "cannot compute bbox from empty points",
376            ));
377        }
378
379        let mut x_min = points[0].x;
380        let mut y_min = points[0].y;
381        let mut x_max = points[0].x;
382        let mut y_max = points[0].y;
383
384        for point in &points[1..] {
385            x_min = x_min.min(point.x);
386            y_min = y_min.min(point.y);
387            x_max = x_max.max(point.x);
388            y_max = y_max.max(point.y);
389        }
390
391        Self::new(x_min, y_min, x_max, y_max)
392    }
393}
394
395/// A multi-part shape (PolyLine or Polygon)
396#[derive(Debug, Clone, PartialEq)]
397pub struct MultiPartShape {
398    /// Bounding box
399    pub bbox: Box2D,
400    /// Number of parts
401    pub num_parts: i32,
402    /// Number of points
403    pub num_points: i32,
404    /// Part start indices
405    pub parts: Vec<i32>,
406    /// Points
407    pub points: Vec<Point>,
408}
409
410impl MultiPartShape {
411    /// Creates a new multi-part shape
412    pub fn new(parts: Vec<i32>, points: Vec<Point>) -> Result<Self> {
413        if parts.is_empty() {
414            return Err(ShapefileError::invalid_geometry("parts cannot be empty"));
415        }
416        if points.is_empty() {
417            return Err(ShapefileError::invalid_geometry("points cannot be empty"));
418        }
419
420        let bbox = Box2D::from_points(&points)?;
421
422        Ok(Self {
423            bbox,
424            num_parts: parts.len() as i32,
425            num_points: points.len() as i32,
426            parts,
427            points,
428        })
429    }
430
431    /// Reads a multi-part shape from a reader
432    pub fn read<R: Read>(reader: &mut R) -> Result<Self> {
433        let bbox = Box2D::read(reader)?;
434
435        let num_parts = reader
436            .read_i32::<LittleEndian>()
437            .map_err(|_| ShapefileError::unexpected_eof("reading num_parts"))?;
438        let num_points = reader
439            .read_i32::<LittleEndian>()
440            .map_err(|_| ShapefileError::unexpected_eof("reading num_points"))?;
441
442        if !(0..=1_000_000).contains(&num_parts) {
443            return Err(ShapefileError::limit_exceeded(
444                "num_parts out of range",
445                1_000_000,
446                num_parts as usize,
447            ));
448        }
449
450        if !(0..=100_000_000).contains(&num_points) {
451            return Err(ShapefileError::limit_exceeded(
452                "num_points out of range",
453                100_000_000,
454                num_points as usize,
455            ));
456        }
457
458        let mut parts = Vec::with_capacity(num_parts as usize);
459        for _ in 0..num_parts {
460            let part = reader
461                .read_i32::<LittleEndian>()
462                .map_err(|_| ShapefileError::unexpected_eof("reading part index"))?;
463            parts.push(part);
464        }
465
466        let mut points = Vec::with_capacity(num_points as usize);
467        for _ in 0..num_points {
468            let point = Point::read(reader)?;
469            points.push(point);
470        }
471
472        Ok(Self {
473            bbox,
474            num_parts,
475            num_points,
476            parts,
477            points,
478        })
479    }
480
481    /// Writes a multi-part shape to a writer
482    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
483        self.bbox.write(writer)?;
484
485        writer
486            .write_i32::<LittleEndian>(self.num_parts)
487            .map_err(ShapefileError::Io)?;
488        writer
489            .write_i32::<LittleEndian>(self.num_points)
490            .map_err(ShapefileError::Io)?;
491
492        for part in &self.parts {
493            writer
494                .write_i32::<LittleEndian>(*part)
495                .map_err(ShapefileError::Io)?;
496        }
497
498        for point in &self.points {
499            point.write(writer)?;
500        }
501
502        Ok(())
503    }
504}
505
506/// A multi-part shape with Z coordinates (PolyLineZ, PolygonZ, or MultiPointZ)
507///
508/// Binary layout (after shape type):
509/// - Box2D (32 bytes: x_min, y_min, x_max, y_max)
510/// - num_parts (4 bytes)
511/// - num_points (4 bytes)
512/// - parts array (num_parts * 4 bytes)
513/// - points array (num_points * 16 bytes: x, y pairs)
514/// - z_range (16 bytes: z_min, z_max)
515/// - z_values array (num_points * 8 bytes)
516/// - m_range (16 bytes: m_min, m_max) \[optional\]
517/// - m_values array (num_points * 8 bytes) \[optional\]
518#[derive(Debug, Clone, PartialEq)]
519pub struct MultiPartShapeZ {
520    /// Base 2D shape data (bbox, parts, points)
521    pub base: MultiPartShape,
522    /// Z coordinate range (min, max)
523    pub z_range: (f64, f64),
524    /// Z coordinate values for each point
525    pub z_values: Vec<f64>,
526    /// M value range (min, max), optional
527    pub m_range: Option<(f64, f64)>,
528    /// M values for each point, optional
529    pub m_values: Option<Vec<f64>>,
530}
531
532impl MultiPartShapeZ {
533    /// Creates a new multi-part shape with Z coordinates
534    pub fn new(
535        parts: Vec<i32>,
536        points: Vec<Point>,
537        z_values: Vec<f64>,
538        m_values: Option<Vec<f64>>,
539    ) -> Result<Self> {
540        if z_values.len() != points.len() {
541            return Err(ShapefileError::invalid_geometry(format!(
542                "z_values length ({}) must match points length ({})",
543                z_values.len(),
544                points.len()
545            )));
546        }
547        if let Some(ref mv) = m_values {
548            if mv.len() != points.len() {
549                return Err(ShapefileError::invalid_geometry(format!(
550                    "m_values length ({}) must match points length ({})",
551                    mv.len(),
552                    points.len()
553                )));
554            }
555        }
556
557        let base = MultiPartShape::new(parts, points)?;
558
559        let z_min = z_values.iter().copied().fold(f64::INFINITY, f64::min);
560        let z_max = z_values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
561
562        let m_range = m_values.as_ref().map(|mv| {
563            let m_min = mv.iter().copied().fold(f64::INFINITY, f64::min);
564            let m_max = mv.iter().copied().fold(f64::NEG_INFINITY, f64::max);
565            (m_min, m_max)
566        });
567
568        Ok(Self {
569            base,
570            z_range: (z_min, z_max),
571            z_values,
572            m_range,
573            m_values,
574        })
575    }
576
577    /// Reads a multi-part shape with Z from a reader
578    pub fn read<R: Read>(reader: &mut R) -> Result<Self> {
579        // Read the base 2D multi-part shape
580        let base = MultiPartShape::read(reader)?;
581
582        // Read Z range
583        let z_min = reader
584            .read_f64::<LittleEndian>()
585            .map_err(|_| ShapefileError::unexpected_eof("reading z range min"))?;
586        let z_max = reader
587            .read_f64::<LittleEndian>()
588            .map_err(|_| ShapefileError::unexpected_eof("reading z range max"))?;
589
590        // Read Z values
591        let num_points = base.num_points as usize;
592        let mut z_values = Vec::with_capacity(num_points);
593        for _ in 0..num_points {
594            let z = reader
595                .read_f64::<LittleEndian>()
596                .map_err(|_| ShapefileError::unexpected_eof("reading z value"))?;
597            z_values.push(z);
598        }
599
600        // Try to read optional M range and values
601        let (m_range, m_values) = match reader.read_f64::<LittleEndian>() {
602            Ok(m_min) => {
603                let m_max = reader
604                    .read_f64::<LittleEndian>()
605                    .map_err(|_| ShapefileError::unexpected_eof("reading m range max"))?;
606
607                let mut mv = Vec::with_capacity(num_points);
608                for _ in 0..num_points {
609                    let m = reader
610                        .read_f64::<LittleEndian>()
611                        .map_err(|_| ShapefileError::unexpected_eof("reading m value"))?;
612                    mv.push(m);
613                }
614
615                // Check if M values are "no data" (less than -1e38)
616                if m_min < -1e38 {
617                    (None, None)
618                } else {
619                    (Some((m_min, m_max)), Some(mv))
620                }
621            }
622            Err(_) => (None, None),
623        };
624
625        Ok(Self {
626            base,
627            z_range: (z_min, z_max),
628            z_values,
629            m_range,
630            m_values,
631        })
632    }
633
634    /// Writes a multi-part shape with Z to a writer
635    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
636        // Write base 2D shape
637        self.base.write(writer)?;
638
639        // Write Z range
640        writer
641            .write_f64::<LittleEndian>(self.z_range.0)
642            .map_err(ShapefileError::Io)?;
643        writer
644            .write_f64::<LittleEndian>(self.z_range.1)
645            .map_err(ShapefileError::Io)?;
646
647        // Write Z values
648        for z in &self.z_values {
649            writer
650                .write_f64::<LittleEndian>(*z)
651                .map_err(ShapefileError::Io)?;
652        }
653
654        // Write M range and values (if present)
655        if let (Some((m_min, m_max)), Some(m_values)) = (self.m_range, &self.m_values) {
656            writer
657                .write_f64::<LittleEndian>(m_min)
658                .map_err(ShapefileError::Io)?;
659            writer
660                .write_f64::<LittleEndian>(m_max)
661                .map_err(ShapefileError::Io)?;
662            for m in m_values {
663                writer
664                    .write_f64::<LittleEndian>(*m)
665                    .map_err(ShapefileError::Io)?;
666            }
667        }
668
669        Ok(())
670    }
671
672    /// Returns the content length in 16-bit words (excluding shape type)
673    pub fn content_length_words(&self) -> i32 {
674        // Base content: bbox(32) + num_parts(4) + num_points(4) + parts + points
675        let base_bytes = 32 + 4 + 4 + (self.base.num_parts * 4) + (self.base.num_points * 16);
676        // Z data: z_range(16) + z_values
677        let z_bytes = 16 + (self.base.num_points * 8);
678        // M data (optional): m_range(16) + m_values
679        let m_bytes = if self.m_values.is_some() {
680            16 + (self.base.num_points * 8)
681        } else {
682            0
683        };
684        (base_bytes + z_bytes + m_bytes) / 2
685    }
686}
687
688/// A multi-part shape with M (measure) values (PolyLineM, PolygonM, or MultiPointM)
689///
690/// Binary layout (after shape type):
691/// - Box2D (32 bytes: x_min, y_min, x_max, y_max)
692/// - num_parts (4 bytes)
693/// - num_points (4 bytes)
694/// - parts array (num_parts * 4 bytes)
695/// - points array (num_points * 16 bytes: x, y pairs)
696/// - m_range (16 bytes: m_min, m_max)
697/// - m_values array (num_points * 8 bytes)
698#[derive(Debug, Clone, PartialEq)]
699pub struct MultiPartShapeM {
700    /// Base 2D shape data (bbox, parts, points)
701    pub base: MultiPartShape,
702    /// M value range (min, max)
703    pub m_range: (f64, f64),
704    /// M values for each point
705    pub m_values: Vec<f64>,
706}
707
708impl MultiPartShapeM {
709    /// Creates a new multi-part shape with M values
710    pub fn new(parts: Vec<i32>, points: Vec<Point>, m_values: Vec<f64>) -> Result<Self> {
711        if m_values.len() != points.len() {
712            return Err(ShapefileError::invalid_geometry(format!(
713                "m_values length ({}) must match points length ({})",
714                m_values.len(),
715                points.len()
716            )));
717        }
718
719        let base = MultiPartShape::new(parts, points)?;
720
721        let m_min = m_values.iter().copied().fold(f64::INFINITY, f64::min);
722        let m_max = m_values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
723
724        Ok(Self {
725            base,
726            m_range: (m_min, m_max),
727            m_values,
728        })
729    }
730
731    /// Reads a multi-part shape with M from a reader
732    pub fn read<R: Read>(reader: &mut R) -> Result<Self> {
733        // Read the base 2D multi-part shape
734        let base = MultiPartShape::read(reader)?;
735
736        // Read M range
737        let m_min = reader
738            .read_f64::<LittleEndian>()
739            .map_err(|_| ShapefileError::unexpected_eof("reading m range min"))?;
740        let m_max = reader
741            .read_f64::<LittleEndian>()
742            .map_err(|_| ShapefileError::unexpected_eof("reading m range max"))?;
743
744        // Read M values
745        let num_points = base.num_points as usize;
746        let mut m_values = Vec::with_capacity(num_points);
747        for _ in 0..num_points {
748            let m = reader
749                .read_f64::<LittleEndian>()
750                .map_err(|_| ShapefileError::unexpected_eof("reading m value"))?;
751            m_values.push(m);
752        }
753
754        Ok(Self {
755            base,
756            m_range: (m_min, m_max),
757            m_values,
758        })
759    }
760
761    /// Writes a multi-part shape with M to a writer
762    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
763        // Write base 2D shape
764        self.base.write(writer)?;
765
766        // Write M range
767        writer
768            .write_f64::<LittleEndian>(self.m_range.0)
769            .map_err(ShapefileError::Io)?;
770        writer
771            .write_f64::<LittleEndian>(self.m_range.1)
772            .map_err(ShapefileError::Io)?;
773
774        // Write M values
775        for m in &self.m_values {
776            writer
777                .write_f64::<LittleEndian>(*m)
778                .map_err(ShapefileError::Io)?;
779        }
780
781        Ok(())
782    }
783
784    /// Returns the content length in 16-bit words (excluding shape type)
785    pub fn content_length_words(&self) -> i32 {
786        // Base content: bbox(32) + num_parts(4) + num_points(4) + parts + points
787        let base_bytes = 32 + 4 + 4 + (self.base.num_parts * 4) + (self.base.num_points * 16);
788        // M data: m_range(16) + m_values
789        let m_bytes = 16 + (self.base.num_points * 8);
790        (base_bytes + m_bytes) / 2
791    }
792}
793
794#[cfg(test)]
795mod tests {
796    use super::*;
797    use std::io::Cursor;
798
799    #[test]
800    fn test_shape_type_conversion() {
801        assert_eq!(
802            ShapeType::from_code(1).expect("valid shape type code 1"),
803            ShapeType::Point
804        );
805        assert_eq!(ShapeType::Point.to_code(), 1);
806        assert_eq!(
807            ShapeType::from_code(11).expect("valid shape type code 11"),
808            ShapeType::PointZ
809        );
810        assert!(ShapeType::from_code(999).is_err());
811    }
812
813    #[test]
814    fn test_shape_type_properties() {
815        assert!(ShapeType::PointZ.has_z());
816        assert!(!ShapeType::Point.has_z());
817        assert!(ShapeType::PointZ.has_m());
818        assert!(ShapeType::PointM.has_m());
819        assert!(!ShapeType::Point.has_m());
820    }
821
822    #[test]
823    fn test_point_round_trip() {
824        let point = Point::new(10.5, 20.3);
825        let mut buffer = Vec::new();
826        point.write(&mut buffer).expect("write point");
827
828        let mut cursor = Cursor::new(buffer);
829        let read_point = Point::read(&mut cursor).expect("read point");
830
831        assert_eq!(read_point, point);
832    }
833
834    #[test]
835    fn test_pointz_round_trip() {
836        let point = PointZ::new_with_m(10.5, 20.3, 30.7, 100.0);
837        let mut buffer = Vec::new();
838        point.write(&mut buffer).expect("write pointz");
839
840        let mut cursor = Cursor::new(buffer);
841        let read_point = PointZ::read(&mut cursor).expect("read pointz");
842
843        assert_eq!(read_point, point);
844    }
845
846    #[test]
847    fn test_box2d_from_points() {
848        let points = vec![
849            Point::new(0.0, 0.0),
850            Point::new(10.0, 20.0),
851            Point::new(-5.0, 15.0),
852        ];
853
854        let bbox = Box2D::from_points(&points).expect("compute bbox from points");
855        assert_eq!(bbox.x_min, -5.0);
856        assert_eq!(bbox.y_min, 0.0);
857        assert_eq!(bbox.x_max, 10.0);
858        assert_eq!(bbox.y_max, 20.0);
859    }
860
861    #[test]
862    fn test_invalid_coordinates() {
863        let mut buffer = Vec::new();
864        buffer
865            .write_f64::<LittleEndian>(f64::NAN)
866            .expect("write NAN coordinate");
867        buffer
868            .write_f64::<LittleEndian>(10.0)
869            .expect("write valid coordinate");
870
871        let mut cursor = Cursor::new(buffer);
872        let result = Point::read(&mut cursor);
873        assert!(result.is_err());
874    }
875}