Skip to main content

oxigdal_shapefile/
reader.rs

1//! Shapefile reader - coordinates reading from .shp, .dbf, and .shx files
2//!
3//! This module provides a high-level interface for reading Shapefiles,
4//! combining geometry from .shp, attributes from .dbf, and spatial index from .shx.
5
6use crate::dbf::{DbfReader, FieldDescriptor};
7use crate::error::{Result, ShapefileError};
8use crate::shp::{Shape, ShapefileHeader, ShpReader};
9use crate::shx::{IndexEntry, ShxReader};
10use oxigdal_core::vector::{
11    Coordinate, Feature, Geometry, LineString as CoreLineString,
12    MultiLineString as CoreMultiLineString, MultiPoint as CoreMultiPoint, Point as CorePoint,
13    Polygon as CorePolygon, PropertyValue,
14};
15use std::collections::HashMap;
16use std::fs::File;
17use std::io::BufReader;
18use std::path::{Path, PathBuf};
19
20/// A complete Shapefile feature (geometry + attributes)
21#[derive(Debug, Clone)]
22pub struct ShapefileFeature {
23    /// Record number (1-based)
24    pub record_number: i32,
25    /// Geometry
26    pub geometry: Option<Geometry>,
27    /// Attributes (field name -> value)
28    pub attributes: HashMap<String, PropertyValue>,
29}
30
31impl ShapefileFeature {
32    /// Creates a new Shapefile feature
33    pub fn new(
34        record_number: i32,
35        geometry: Option<Geometry>,
36        attributes: HashMap<String, PropertyValue>,
37    ) -> Self {
38        Self {
39            record_number,
40            geometry,
41            attributes,
42        }
43    }
44
45    /// Converts to an OxiGDAL Feature
46    pub fn to_oxigdal_feature(&self) -> Result<Feature> {
47        let geometry = self
48            .geometry
49            .clone()
50            .ok_or_else(|| ShapefileError::invalid_geometry("feature has no geometry"))?;
51
52        let mut feature = Feature::new(geometry);
53
54        // Convert attributes
55        for (key, value) in &self.attributes {
56            feature.set_property(key, value.clone());
57        }
58
59        Ok(feature)
60    }
61}
62
63/// Shapefile reader that coordinates .shp, .dbf, and optionally .shx files
64pub struct ShapefileReader {
65    /// Base path (without extension)
66    base_path: PathBuf,
67    /// .shp file header
68    header: ShapefileHeader,
69    /// Field descriptors from .dbf
70    field_descriptors: Vec<FieldDescriptor>,
71    /// Index entries from .shx (if available)
72    index_entries: Option<Vec<IndexEntry>>,
73}
74
75impl ShapefileReader {
76    /// Opens a Shapefile from a base path (without extension)
77    ///
78    /// Reads the .shp, .dbf, and optionally .shx files.
79    pub fn open<P: AsRef<Path>>(base_path: P) -> Result<Self> {
80        let base_path = base_path.as_ref();
81
82        // Construct file paths
83        let shp_path = Self::with_extension(base_path, "shp");
84        let dbf_path = Self::with_extension(base_path, "dbf");
85        let shx_path = Self::with_extension(base_path, "shx");
86
87        // Open .shp file
88        let shp_file = File::open(&shp_path).map_err(|_| ShapefileError::MissingFile {
89            file_type: ".shp".to_string(),
90        })?;
91        let shp_reader = BufReader::new(shp_file);
92        let shp_reader = ShpReader::new(shp_reader)?;
93        let header = shp_reader.header().clone();
94
95        // Open .dbf file
96        let dbf_file = File::open(&dbf_path).map_err(|_| ShapefileError::MissingFile {
97            file_type: ".dbf".to_string(),
98        })?;
99        let dbf_reader = BufReader::new(dbf_file);
100        let dbf_reader = DbfReader::new(dbf_reader)?;
101        let field_descriptors = dbf_reader.field_descriptors().to_vec();
102
103        // Open .shx file (optional)
104        let index_entries = if shx_path.exists() {
105            let shx_file = File::open(&shx_path).ok();
106            if let Some(file) = shx_file {
107                let shx_reader = BufReader::new(file);
108                let mut shx_reader = ShxReader::new(shx_reader)?;
109                Some(shx_reader.read_all_entries()?)
110            } else {
111                None
112            }
113        } else {
114            None
115        };
116
117        Ok(Self {
118            base_path: base_path.to_path_buf(),
119            header,
120            field_descriptors,
121            index_entries,
122        })
123    }
124
125    /// Returns the Shapefile header
126    pub fn header(&self) -> &ShapefileHeader {
127        &self.header
128    }
129
130    /// Returns the field descriptors
131    pub fn field_descriptors(&self) -> &[FieldDescriptor] {
132        &self.field_descriptors
133    }
134
135    /// Returns the index entries (if .shx was loaded)
136    pub fn index_entries(&self) -> Option<&[IndexEntry]> {
137        self.index_entries.as_deref()
138    }
139
140    /// Reads all features from the Shapefile
141    pub fn read_features(&self) -> Result<Vec<ShapefileFeature>> {
142        // Open files
143        let shp_path = Self::with_extension(&self.base_path, "shp");
144        let dbf_path = Self::with_extension(&self.base_path, "dbf");
145
146        let shp_file = File::open(&shp_path)?;
147        let shp_reader = BufReader::new(shp_file);
148        let mut shp_reader = ShpReader::new(shp_reader)?;
149
150        let dbf_file = File::open(&dbf_path)?;
151        let dbf_reader = BufReader::new(dbf_file);
152        let mut dbf_reader = DbfReader::new(dbf_reader)?;
153
154        // Read all shape records
155        let shape_records = shp_reader.read_all_records()?;
156
157        // Read all DBF records
158        let dbf_records = dbf_reader.read_all_records()?;
159
160        // Verify record counts match
161        if shape_records.len() != dbf_records.len() {
162            return Err(ShapefileError::RecordMismatch {
163                shp_count: shape_records.len(),
164                dbf_count: dbf_records.len(),
165            });
166        }
167
168        // Combine into features
169        let mut features = Vec::with_capacity(shape_records.len());
170        for (shape_record, dbf_record) in shape_records.iter().zip(dbf_records.iter()) {
171            let geometry = Self::shape_to_geometry(&shape_record.shape)?;
172
173            // Convert DBF record to attributes
174            let attributes = Self::dbf_to_attributes(dbf_record, &self.field_descriptors);
175
176            features.push(ShapefileFeature::new(
177                shape_record.record_number,
178                geometry,
179                attributes,
180            ));
181        }
182
183        Ok(features)
184    }
185
186    /// Converts a Shape to an OxiGDAL Geometry
187    fn shape_to_geometry(shape: &Shape) -> Result<Option<Geometry>> {
188        match shape {
189            Shape::Null => Ok(None),
190            Shape::Point(point) => {
191                let oxigdal_point = CorePoint::new(point.x, point.y);
192                Ok(Some(Geometry::Point(oxigdal_point)))
193            }
194            Shape::PointZ(point) => {
195                // For now, just use X/Y (could extend OxiGDAL to support Z)
196                let oxigdal_point = CorePoint::new(point.x, point.y);
197                Ok(Some(Geometry::Point(oxigdal_point)))
198            }
199            Shape::PointM(point) => {
200                let oxigdal_point = CorePoint::new(point.x, point.y);
201                Ok(Some(Geometry::Point(oxigdal_point)))
202            }
203            Shape::PolyLine(multi_part) => {
204                if multi_part.parts.len() == 1 {
205                    // Single part - convert to LineString
206                    let coords: Vec<Coordinate> = multi_part
207                        .points
208                        .iter()
209                        .map(|p| Coordinate::new_2d(p.x, p.y))
210                        .collect();
211
212                    if coords.len() < 2 {
213                        return Ok(None);
214                    }
215
216                    let linestring = CoreLineString::new(coords).map_err(|e| {
217                        ShapefileError::invalid_geometry(format!("Invalid LineString: {}", e))
218                    })?;
219                    Ok(Some(Geometry::LineString(linestring)))
220                } else {
221                    // Multiple parts - convert to MultiLineString
222                    let mut linestrings = Vec::new();
223
224                    for i in 0..multi_part.parts.len() {
225                        let start_idx = multi_part.parts[i] as usize;
226                        let end_idx = if i + 1 < multi_part.parts.len() {
227                            multi_part.parts[i + 1] as usize
228                        } else {
229                            multi_part.points.len()
230                        };
231
232                        let coords: Vec<Coordinate> = multi_part.points[start_idx..end_idx]
233                            .iter()
234                            .map(|p| Coordinate::new_2d(p.x, p.y))
235                            .collect();
236
237                        if coords.len() >= 2 {
238                            if let Ok(linestring) = CoreLineString::new(coords) {
239                                linestrings.push(linestring);
240                            }
241                        }
242                    }
243
244                    if linestrings.is_empty() {
245                        Ok(None)
246                    } else {
247                        Ok(Some(Geometry::MultiLineString(CoreMultiLineString::new(
248                            linestrings,
249                        ))))
250                    }
251                }
252            }
253            Shape::Polygon(multi_part) => {
254                if multi_part.parts.is_empty() {
255                    return Ok(None);
256                }
257
258                // First part is exterior ring
259                let exterior_start = multi_part.parts[0] as usize;
260                let exterior_end = if multi_part.parts.len() > 1 {
261                    multi_part.parts[1] as usize
262                } else {
263                    multi_part.points.len()
264                };
265
266                let exterior_coords: Vec<Coordinate> = multi_part.points
267                    [exterior_start..exterior_end]
268                    .iter()
269                    .map(|p| Coordinate::new_2d(p.x, p.y))
270                    .collect();
271
272                if exterior_coords.len() < 4 {
273                    return Ok(None);
274                }
275
276                let exterior = CoreLineString::new(exterior_coords).map_err(|e| {
277                    ShapefileError::invalid_geometry(format!("Invalid exterior ring: {}", e))
278                })?;
279
280                // Remaining parts are interior rings (holes)
281                let mut interiors = Vec::new();
282                for i in 1..multi_part.parts.len() {
283                    let start_idx = multi_part.parts[i] as usize;
284                    let end_idx = if i + 1 < multi_part.parts.len() {
285                        multi_part.parts[i + 1] as usize
286                    } else {
287                        multi_part.points.len()
288                    };
289
290                    let interior_coords: Vec<Coordinate> = multi_part.points[start_idx..end_idx]
291                        .iter()
292                        .map(|p| Coordinate::new_2d(p.x, p.y))
293                        .collect();
294
295                    if interior_coords.len() >= 4 {
296                        if let Ok(interior) = CoreLineString::new(interior_coords) {
297                            interiors.push(interior);
298                        }
299                    }
300                }
301
302                let polygon = CorePolygon::new(exterior, interiors).map_err(|e| {
303                    ShapefileError::invalid_geometry(format!("Invalid polygon: {}", e))
304                })?;
305
306                Ok(Some(Geometry::Polygon(polygon)))
307            }
308            Shape::MultiPoint(multi_part) => {
309                let points: Vec<CorePoint> = multi_part
310                    .points
311                    .iter()
312                    .map(|p| CorePoint::new(p.x, p.y))
313                    .collect();
314
315                if points.is_empty() {
316                    Ok(None)
317                } else {
318                    Ok(Some(Geometry::MultiPoint(CoreMultiPoint::new(points))))
319                }
320            }
321            // Z variants: use the base 2D shape data (Z values are not
322            // directly representable in the current OxiGDAL Geometry model)
323            Shape::PolyLineZ(shape_z) => {
324                Self::shape_to_geometry(&Shape::PolyLine(shape_z.base.clone()))
325            }
326            Shape::PolygonZ(shape_z) => {
327                Self::shape_to_geometry(&Shape::Polygon(shape_z.base.clone()))
328            }
329            Shape::MultiPointZ(shape_z) => {
330                Self::shape_to_geometry(&Shape::MultiPoint(shape_z.base.clone()))
331            }
332            // M variants: use the base 2D shape data (M values are not
333            // directly representable in the current OxiGDAL Geometry model)
334            Shape::PolyLineM(shape_m) => {
335                Self::shape_to_geometry(&Shape::PolyLine(shape_m.base.clone()))
336            }
337            Shape::PolygonM(shape_m) => {
338                Self::shape_to_geometry(&Shape::Polygon(shape_m.base.clone()))
339            }
340            Shape::MultiPointM(shape_m) => {
341                Self::shape_to_geometry(&Shape::MultiPoint(shape_m.base.clone()))
342            }
343        }
344    }
345
346    /// Converts a DBF record to PropertyValue attributes
347    fn dbf_to_attributes(
348        dbf_record: &crate::dbf::DbfRecord,
349        field_descriptors: &[FieldDescriptor],
350    ) -> HashMap<String, PropertyValue> {
351        let mut attributes = HashMap::new();
352
353        for (field, value) in field_descriptors.iter().zip(&dbf_record.values) {
354            let property_value = match value {
355                crate::dbf::FieldValue::String(s) => PropertyValue::String(s.clone()),
356                crate::dbf::FieldValue::Integer(i) => PropertyValue::Integer(*i),
357                crate::dbf::FieldValue::Float(f) => PropertyValue::Float(*f),
358                crate::dbf::FieldValue::Boolean(b) => PropertyValue::Bool(*b),
359                crate::dbf::FieldValue::Date(d) => PropertyValue::String(d.clone()),
360                crate::dbf::FieldValue::Null => PropertyValue::Null,
361            };
362
363            attributes.insert(field.name.clone(), property_value);
364        }
365
366        attributes
367    }
368
369    /// Helper to add extension to base path
370    fn with_extension<P: AsRef<Path>>(base_path: P, ext: &str) -> PathBuf {
371        let base = base_path.as_ref();
372
373        // If base already has an extension, replace it
374        if base.extension().is_some() {
375            base.with_extension(ext)
376        } else {
377            // Otherwise, add the extension
378            let mut path = base.to_path_buf();
379            path.set_extension(ext);
380            path
381        }
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388
389    #[test]
390    fn test_path_extension_helper() {
391        let base = PathBuf::from("/tmp/test");
392        assert_eq!(
393            ShapefileReader::with_extension(&base, "shp"),
394            PathBuf::from("/tmp/test.shp")
395        );
396
397        let base = PathBuf::from("/tmp/test.shp");
398        assert_eq!(
399            ShapefileReader::with_extension(&base, "dbf"),
400            PathBuf::from("/tmp/test.dbf")
401        );
402    }
403
404    #[test]
405    fn test_shapefile_feature_creation() {
406        let mut attributes = HashMap::new();
407        attributes.insert(
408            "name".to_string(),
409            PropertyValue::String("Test".to_string()),
410        );
411        attributes.insert("value".to_string(), PropertyValue::Integer(42));
412
413        let geometry = Some(Geometry::Point(CorePoint::new(10.0, 20.0)));
414
415        let feature = ShapefileFeature::new(1, geometry, attributes);
416        assert_eq!(feature.record_number, 1);
417        assert!(feature.geometry.is_some());
418        assert_eq!(feature.attributes.len(), 2);
419    }
420}