Skip to main content

oxigdal_shapefile/
writer.rs

1//! Shapefile writer - coordinates writing to .shp, .dbf, and .shx files
2//!
3//! This module provides a high-level interface for writing Shapefiles,
4//! creating geometry in .shp, attributes in .dbf, and spatial index in .shx.
5
6use crate::dbf::{DbfRecord, DbfWriter, FieldDescriptor, FieldType, FieldValue};
7use crate::error::{Result, ShapefileError};
8use crate::reader::ShapefileFeature;
9use crate::shp::header::BoundingBox;
10use crate::shp::shapes::{Point, ShapeType};
11use crate::shp::{Shape, ShpWriter};
12use crate::shx::ShxWriter;
13use oxigdal_core::vector::{Feature, Geometry, PropertyValue};
14use std::fs::File;
15use std::path::{Path, PathBuf};
16
17/// Shapefile writer that coordinates .shp, .dbf, and .shx files
18pub struct ShapefileWriter {
19    /// Base path (without extension)
20    base_path: PathBuf,
21    /// Shape type for the Shapefile
22    shape_type: ShapeType,
23    /// Field descriptors for attributes
24    field_descriptors: Vec<FieldDescriptor>,
25    /// Bounding box (will be updated as features are added)
26    bbox: BoundingBox,
27}
28
29impl ShapefileWriter {
30    /// Creates a new Shapefile writer
31    pub fn new<P: AsRef<Path>>(
32        base_path: P,
33        shape_type: ShapeType,
34        field_descriptors: Vec<FieldDescriptor>,
35    ) -> Result<Self> {
36        // Initialize with empty bounding box (will be updated)
37        let bbox = BoundingBox::new_2d(0.0, 0.0, 0.0, 0.0)?;
38
39        Ok(Self {
40            base_path: base_path.as_ref().to_path_buf(),
41            shape_type,
42            field_descriptors,
43            bbox,
44        })
45    }
46
47    /// Writes features to the Shapefile
48    pub fn write_features(&mut self, features: &[ShapefileFeature]) -> Result<()> {
49        if features.is_empty() {
50            return Err(ShapefileError::invalid_geometry(
51                "cannot write empty feature collection",
52            ));
53        }
54
55        // Calculate bounding box from all features
56        self.bbox = Self::calculate_bbox(features)?;
57
58        // Open output files
59        let shp_path = Self::with_extension(&self.base_path, "shp");
60        let dbf_path = Self::with_extension(&self.base_path, "dbf");
61        let shx_path = Self::with_extension(&self.base_path, "shx");
62
63        let shp_file = File::create(&shp_path)?;
64        let mut shp_writer = ShpWriter::new(shp_file, self.shape_type, self.bbox.clone());
65
66        let dbf_file = File::create(&dbf_path)?;
67        let mut dbf_writer = DbfWriter::new(dbf_file, self.field_descriptors.clone())?;
68
69        let shx_file = File::create(&shx_path)?;
70        let mut shx_writer = ShxWriter::new(shx_file, self.shape_type, self.bbox.clone());
71
72        // Write headers
73        shp_writer.write_header()?;
74        dbf_writer.write_header()?;
75
76        // Write features
77        let mut current_offset = 50; // Header is 100 bytes = 50 words
78
79        for feature in features {
80            // Convert geometry to Shape
81            let shape = Self::geometry_to_shape(&feature.geometry)?;
82
83            // Calculate content length (includes shape type)
84            let content_length = 2 + shape.content_length(); // +2 for shape type
85
86            // Add to index (content_length should match .shp record header)
87            shx_writer.add_entry(current_offset, content_length);
88
89            // Write shape
90            shp_writer.write_record(shape)?;
91
92            // Convert attributes to DBF record
93            let dbf_record = Self::attributes_to_dbf(&feature.attributes, &self.field_descriptors)?;
94            dbf_writer.write_record(&dbf_record)?;
95
96            // Update offset (record header is 8 bytes = 4 words)
97            current_offset += 4 + content_length;
98        }
99
100        // Flush all writers before updating headers
101        shp_writer.flush()?;
102        dbf_writer.flush()?;
103
104        // Update file lengths in headers
105        shp_writer.update_file_length()?;
106        dbf_writer.update_record_count()?;
107
108        // Finalize and flush remaining files
109        shx_writer.write_all()?;
110        shx_writer.flush()?;
111
112        // DBF finalize consumes the writer
113        dbf_writer.finalize()?;
114
115        // Explicitly drop remaining writers to ensure all data is written
116        drop(shp_writer);
117        drop(shx_writer);
118
119        Ok(())
120    }
121
122    /// Writes OxiGDAL features to the Shapefile
123    pub fn write_oxigdal_features(&mut self, features: &[Feature]) -> Result<()> {
124        let shapefile_features: Vec<ShapefileFeature> = features
125            .iter()
126            .enumerate()
127            .map(|(i, feature)| {
128                let geometry = feature.geometry.clone();
129                let attributes: std::collections::HashMap<String, PropertyValue> = feature
130                    .properties
131                    .iter()
132                    .map(|(k, v)| (k.clone(), v.clone()))
133                    .collect();
134
135                ShapefileFeature::new((i + 1) as i32, geometry, attributes)
136            })
137            .collect();
138
139        self.write_features(&shapefile_features)
140    }
141
142    /// Calculates the bounding box from features
143    fn calculate_bbox(features: &[ShapefileFeature]) -> Result<BoundingBox> {
144        let mut x_min = f64::INFINITY;
145        let mut y_min = f64::INFINITY;
146        let mut x_max = f64::NEG_INFINITY;
147        let mut y_max = f64::NEG_INFINITY;
148
149        for feature in features {
150            if let Some(geometry) = &feature.geometry {
151                match geometry {
152                    Geometry::Point(point) => {
153                        x_min = x_min.min(point.coord.x);
154                        y_min = y_min.min(point.coord.y);
155                        x_max = x_max.max(point.coord.x);
156                        y_max = y_max.max(point.coord.y);
157                    }
158                    Geometry::LineString(linestring) => {
159                        for coord in &linestring.coords {
160                            x_min = x_min.min(coord.x);
161                            y_min = y_min.min(coord.y);
162                            x_max = x_max.max(coord.x);
163                            y_max = y_max.max(coord.y);
164                        }
165                    }
166                    Geometry::Polygon(polygon) => {
167                        for coord in &polygon.exterior.coords {
168                            x_min = x_min.min(coord.x);
169                            y_min = y_min.min(coord.y);
170                            x_max = x_max.max(coord.x);
171                            y_max = y_max.max(coord.y);
172                        }
173                        for interior in &polygon.interiors {
174                            for coord in &interior.coords {
175                                x_min = x_min.min(coord.x);
176                                y_min = y_min.min(coord.y);
177                                x_max = x_max.max(coord.x);
178                                y_max = y_max.max(coord.y);
179                            }
180                        }
181                    }
182                    Geometry::MultiPoint(multipoint) => {
183                        for point in &multipoint.points {
184                            x_min = x_min.min(point.coord.x);
185                            y_min = y_min.min(point.coord.y);
186                            x_max = x_max.max(point.coord.x);
187                            y_max = y_max.max(point.coord.y);
188                        }
189                    }
190                    Geometry::MultiLineString(multilinestring) => {
191                        for linestring in &multilinestring.line_strings {
192                            for coord in &linestring.coords {
193                                x_min = x_min.min(coord.x);
194                                y_min = y_min.min(coord.y);
195                                x_max = x_max.max(coord.x);
196                                y_max = y_max.max(coord.y);
197                            }
198                        }
199                    }
200                    Geometry::MultiPolygon(multipolygon) => {
201                        for polygon in &multipolygon.polygons {
202                            for coord in &polygon.exterior.coords {
203                                x_min = x_min.min(coord.x);
204                                y_min = y_min.min(coord.y);
205                                x_max = x_max.max(coord.x);
206                                y_max = y_max.max(coord.y);
207                            }
208                            for interior in &polygon.interiors {
209                                for coord in &interior.coords {
210                                    x_min = x_min.min(coord.x);
211                                    y_min = y_min.min(coord.y);
212                                    x_max = x_max.max(coord.x);
213                                    y_max = y_max.max(coord.y);
214                                }
215                            }
216                        }
217                    }
218                    Geometry::GeometryCollection(collection) => {
219                        for geom in &collection.geometries {
220                            if let Some((gx_min, gy_min, gx_max, gy_max)) = geom.bounds() {
221                                x_min = x_min.min(gx_min);
222                                y_min = y_min.min(gy_min);
223                                x_max = x_max.max(gx_max);
224                                y_max = y_max.max(gy_max);
225                            }
226                        }
227                    }
228                }
229            }
230        }
231
232        if x_min.is_infinite() {
233            return Err(ShapefileError::invalid_geometry(
234                "could not calculate bounding box",
235            ));
236        }
237
238        BoundingBox::new_2d(x_min, y_min, x_max, y_max)
239    }
240
241    /// Converts an OxiGDAL Geometry to a Shape
242    fn geometry_to_shape(geometry: &Option<Geometry>) -> Result<Shape> {
243        match geometry {
244            None => Ok(Shape::Null),
245            Some(Geometry::Point(point)) => {
246                let shp_point = Point::new(point.coord.x, point.coord.y);
247                Ok(Shape::Point(shp_point))
248            }
249            Some(Geometry::LineString(linestring)) => {
250                let points: Vec<Point> = linestring
251                    .coords
252                    .iter()
253                    .map(|coord| Point::new(coord.x, coord.y))
254                    .collect();
255
256                if points.is_empty() {
257                    return Err(ShapefileError::invalid_geometry(
258                        "LineString must have at least one point",
259                    ));
260                }
261
262                let parts = vec![0]; // Single part
263                let multi_part = crate::shp::shapes::MultiPartShape::new(parts, points)?;
264                Ok(Shape::PolyLine(multi_part))
265            }
266            Some(Geometry::Polygon(polygon)) => {
267                let mut all_points = Vec::new();
268                let mut parts = Vec::new();
269
270                // Add exterior ring
271                parts.push(all_points.len() as i32);
272                for coord in &polygon.exterior.coords {
273                    all_points.push(Point::new(coord.x, coord.y));
274                }
275
276                // Add interior rings (holes)
277                for interior in &polygon.interiors {
278                    parts.push(all_points.len() as i32);
279                    for coord in &interior.coords {
280                        all_points.push(Point::new(coord.x, coord.y));
281                    }
282                }
283
284                if all_points.is_empty() {
285                    return Err(ShapefileError::invalid_geometry(
286                        "Polygon must have at least one point",
287                    ));
288                }
289
290                let multi_part = crate::shp::shapes::MultiPartShape::new(parts, all_points)?;
291                Ok(Shape::Polygon(multi_part))
292            }
293            Some(Geometry::MultiPoint(multipoint)) => {
294                let points: Vec<Point> = multipoint
295                    .points
296                    .iter()
297                    .map(|pt| Point::new(pt.coord.x, pt.coord.y))
298                    .collect();
299
300                if points.is_empty() {
301                    return Err(ShapefileError::invalid_geometry(
302                        "MultiPoint must have at least one point",
303                    ));
304                }
305
306                let parts: Vec<i32> = (0..points.len() as i32).collect();
307                let multi_part = crate::shp::shapes::MultiPartShape::new(parts, points)?;
308                Ok(Shape::MultiPoint(multi_part))
309            }
310            Some(Geometry::MultiLineString(multilinestring)) => {
311                let mut all_points = Vec::new();
312                let mut parts = Vec::new();
313
314                for linestring in &multilinestring.line_strings {
315                    parts.push(all_points.len() as i32);
316                    for coord in &linestring.coords {
317                        all_points.push(Point::new(coord.x, coord.y));
318                    }
319                }
320
321                if all_points.is_empty() {
322                    return Err(ShapefileError::invalid_geometry(
323                        "MultiLineString must have at least one point",
324                    ));
325                }
326
327                let multi_part = crate::shp::shapes::MultiPartShape::new(parts, all_points)?;
328                Ok(Shape::PolyLine(multi_part))
329            }
330            Some(Geometry::MultiPolygon(multipolygon)) => {
331                let mut all_points = Vec::new();
332                let mut parts = Vec::new();
333
334                for polygon in &multipolygon.polygons {
335                    // Add exterior ring
336                    parts.push(all_points.len() as i32);
337                    for coord in &polygon.exterior.coords {
338                        all_points.push(Point::new(coord.x, coord.y));
339                    }
340
341                    // Add interior rings (holes)
342                    for interior in &polygon.interiors {
343                        parts.push(all_points.len() as i32);
344                        for coord in &interior.coords {
345                            all_points.push(Point::new(coord.x, coord.y));
346                        }
347                    }
348                }
349
350                if all_points.is_empty() {
351                    return Err(ShapefileError::invalid_geometry(
352                        "MultiPolygon must have at least one point",
353                    ));
354                }
355
356                let multi_part = crate::shp::shapes::MultiPartShape::new(parts, all_points)?;
357                Ok(Shape::Polygon(multi_part))
358            }
359            Some(Geometry::GeometryCollection(_)) => Err(ShapefileError::invalid_geometry(
360                "GeometryCollection is not supported in Shapefile format",
361            )),
362        }
363    }
364
365    /// Converts attributes to a DBF record
366    fn attributes_to_dbf(
367        attributes: &std::collections::HashMap<String, PropertyValue>,
368        field_descriptors: &[FieldDescriptor],
369    ) -> Result<DbfRecord> {
370        let mut values = Vec::with_capacity(field_descriptors.len());
371
372        for field in field_descriptors {
373            let value = attributes
374                .get(&field.name)
375                .cloned()
376                .unwrap_or(PropertyValue::Null);
377
378            let dbf_value = match value {
379                PropertyValue::String(s) => FieldValue::String(s),
380                PropertyValue::Integer(i) => FieldValue::Integer(i),
381                PropertyValue::Float(f) => FieldValue::Float(f),
382                PropertyValue::Bool(b) => FieldValue::Boolean(b),
383                PropertyValue::Null => FieldValue::Null,
384                PropertyValue::UInteger(u) => FieldValue::Integer(u as i64),
385                PropertyValue::Array(_) | PropertyValue::Object(_) => FieldValue::Null,
386            };
387
388            values.push(dbf_value);
389        }
390
391        Ok(DbfRecord::new(values))
392    }
393
394    /// Helper to add extension to base path
395    fn with_extension<P: AsRef<Path>>(base_path: P, ext: &str) -> PathBuf {
396        let base = base_path.as_ref();
397
398        // If base already has an extension, replace it
399        if base.extension().is_some() {
400            base.with_extension(ext)
401        } else {
402            // Otherwise, add the extension
403            let mut path = base.to_path_buf();
404            path.set_extension(ext);
405            path
406        }
407    }
408}
409
410/// Builder for creating Shapefile field descriptors
411pub struct ShapefileSchemaBuilder {
412    fields: Vec<FieldDescriptor>,
413}
414
415impl ShapefileSchemaBuilder {
416    /// Creates a new schema builder
417    pub fn new() -> Self {
418        Self { fields: Vec::new() }
419    }
420
421    /// Adds a character field
422    pub fn add_character_field(mut self, name: &str, length: u8) -> Result<Self> {
423        let field = FieldDescriptor::new(name.to_string(), FieldType::Character, length, 0)?;
424        self.fields.push(field);
425        Ok(self)
426    }
427
428    /// Adds a numeric field
429    pub fn add_numeric_field(mut self, name: &str, length: u8, decimals: u8) -> Result<Self> {
430        let field = FieldDescriptor::new(name.to_string(), FieldType::Number, length, decimals)?;
431        self.fields.push(field);
432        Ok(self)
433    }
434
435    /// Adds a logical field
436    pub fn add_logical_field(mut self, name: &str) -> Result<Self> {
437        let field = FieldDescriptor::new(name.to_string(), FieldType::Logical, 1, 0)?;
438        self.fields.push(field);
439        Ok(self)
440    }
441
442    /// Adds a date field
443    pub fn add_date_field(mut self, name: &str) -> Result<Self> {
444        let field = FieldDescriptor::new(name.to_string(), FieldType::Date, 8, 0)?;
445        self.fields.push(field);
446        Ok(self)
447    }
448
449    /// Builds the field descriptors
450    pub fn build(self) -> Vec<FieldDescriptor> {
451        self.fields
452    }
453}
454
455impl Default for ShapefileSchemaBuilder {
456    fn default() -> Self {
457        Self::new()
458    }
459}
460
461#[cfg(test)]
462#[allow(clippy::panic, clippy::assertions_on_constants)]
463mod tests {
464    use super::*;
465    use std::collections::HashMap;
466
467    #[test]
468    fn test_schema_builder() {
469        let schema = ShapefileSchemaBuilder::new()
470            .add_character_field("NAME", 50)
471            .expect("Failed to add NAME field")
472            .add_numeric_field("VALUE", 10, 2)
473            .expect("Failed to add VALUE field")
474            .add_logical_field("ACTIVE")
475            .expect("Failed to add ACTIVE field")
476            .build();
477
478        assert_eq!(schema.len(), 3);
479        assert_eq!(schema[0].name, "NAME");
480        assert_eq!(schema[0].field_type, FieldType::Character);
481        assert_eq!(schema[1].name, "VALUE");
482        assert_eq!(schema[2].name, "ACTIVE");
483    }
484
485    #[test]
486    fn test_bbox_calculation() {
487        let features = vec![
488            ShapefileFeature::new(
489                1,
490                Some(Geometry::Point(oxigdal_core::vector::Point::new(
491                    10.0, 20.0,
492                ))),
493                HashMap::new(),
494            ),
495            ShapefileFeature::new(
496                2,
497                Some(Geometry::Point(oxigdal_core::vector::Point::new(
498                    30.0, 40.0,
499                ))),
500                HashMap::new(),
501            ),
502            ShapefileFeature::new(
503                3,
504                Some(Geometry::Point(oxigdal_core::vector::Point::new(
505                    -5.0, 15.0,
506                ))),
507                HashMap::new(),
508            ),
509        ];
510
511        let bbox = ShapefileWriter::calculate_bbox(&features).expect("Failed to calculate bbox");
512        assert!((bbox.x_min - (-5.0)).abs() < f64::EPSILON);
513        assert!((bbox.y_min - 15.0).abs() < f64::EPSILON);
514        assert!((bbox.x_max - 30.0).abs() < f64::EPSILON);
515        assert!((bbox.y_max - 40.0).abs() < f64::EPSILON);
516    }
517
518    #[test]
519    fn test_geometry_to_shape() {
520        let geometry = Some(Geometry::Point(oxigdal_core::vector::Point::new(
521            10.5, 20.3,
522        )));
523        let shape =
524            ShapefileWriter::geometry_to_shape(&geometry).expect("Failed to convert geometry");
525
526        if let Shape::Point(point) = shape {
527            assert!((point.x - 10.5).abs() < f64::EPSILON);
528            assert!((point.y - 20.3).abs() < f64::EPSILON);
529        } else {
530            assert!(false, "Expected Point shape");
531        }
532    }
533}