1use 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
17pub struct ShapefileWriter {
19 base_path: PathBuf,
21 shape_type: ShapeType,
23 field_descriptors: Vec<FieldDescriptor>,
25 bbox: BoundingBox,
27}
28
29impl ShapefileWriter {
30 pub fn new<P: AsRef<Path>>(
32 base_path: P,
33 shape_type: ShapeType,
34 field_descriptors: Vec<FieldDescriptor>,
35 ) -> Result<Self> {
36 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 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 self.bbox = Self::calculate_bbox(features)?;
57
58 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 shp_writer.write_header()?;
74 dbf_writer.write_header()?;
75
76 let mut current_offset = 50; for feature in features {
80 let shape = Self::geometry_to_shape(&feature.geometry)?;
82
83 let content_length = 2 + shape.content_length(); shx_writer.add_entry(current_offset, content_length);
88
89 shp_writer.write_record(shape)?;
91
92 let dbf_record = Self::attributes_to_dbf(&feature.attributes, &self.field_descriptors)?;
94 dbf_writer.write_record(&dbf_record)?;
95
96 current_offset += 4 + content_length;
98 }
99
100 shp_writer.flush()?;
102 dbf_writer.flush()?;
103
104 shp_writer.update_file_length()?;
106 dbf_writer.update_record_count()?;
107
108 shx_writer.write_all()?;
110 shx_writer.flush()?;
111
112 dbf_writer.finalize()?;
114
115 drop(shp_writer);
117 drop(shx_writer);
118
119 Ok(())
120 }
121
122 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 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 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]; 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 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 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 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 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 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 fn with_extension<P: AsRef<Path>>(base_path: P, ext: &str) -> PathBuf {
396 let base = base_path.as_ref();
397
398 if base.extension().is_some() {
400 base.with_extension(ext)
401 } else {
402 let mut path = base.to_path_buf();
404 path.set_extension(ext);
405 path
406 }
407 }
408}
409
410pub struct ShapefileSchemaBuilder {
412 fields: Vec<FieldDescriptor>,
413}
414
415impl ShapefileSchemaBuilder {
416 pub fn new() -> Self {
418 Self { fields: Vec::new() }
419 }
420
421 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 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 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 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 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}