1use 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, FieldValue, Geometry, LineString as CoreLineString,
12 MultiLineString as CoreMultiLineString, MultiPoint as CoreMultiPoint, Point as CorePoint,
13 Polygon as CorePolygon,
14};
15use std::collections::HashMap;
16use std::fs::File;
17use std::io::BufReader;
18use std::path::{Path, PathBuf};
19
20#[derive(Debug, Clone)]
22pub struct ShapefileFeature {
23 pub record_number: i32,
25 pub geometry: Option<Geometry>,
27 pub attributes: HashMap<String, FieldValue>,
29}
30
31impl ShapefileFeature {
32 pub fn new(
34 record_number: i32,
35 geometry: Option<Geometry>,
36 attributes: HashMap<String, FieldValue>,
37 ) -> Self {
38 Self {
39 record_number,
40 geometry,
41 attributes,
42 }
43 }
44
45 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 for (key, value) in &self.attributes {
56 feature.set_property(key, value.clone());
57 }
58
59 Ok(feature)
60 }
61}
62
63pub struct ShapefileReader {
65 base_path: PathBuf,
67 header: ShapefileHeader,
69 field_descriptors: Vec<FieldDescriptor>,
71 index_entries: Option<Vec<IndexEntry>>,
73 pub crs: Option<String>,
75 pub encoding: Option<String>,
77}
78
79impl ShapefileReader {
80 pub fn open<P: AsRef<Path>>(base_path: P) -> Result<Self> {
84 let base_path = base_path.as_ref();
85
86 let shp_path = Self::with_extension(base_path, "shp");
88 let dbf_path = Self::with_extension(base_path, "dbf");
89 let shx_path = Self::with_extension(base_path, "shx");
90 let prj_path = Self::with_extension(base_path, "prj");
91 let cpg_path = Self::with_extension(base_path, "cpg");
92
93 let shp_file = File::open(&shp_path).map_err(|_| ShapefileError::MissingFile {
95 file_type: ".shp".to_string(),
96 })?;
97 let shp_reader = BufReader::new(shp_file);
98 let shp_reader = ShpReader::new(shp_reader)?;
99 let header = shp_reader.header().clone();
100
101 let dbf_file = File::open(&dbf_path).map_err(|_| ShapefileError::MissingFile {
103 file_type: ".dbf".to_string(),
104 })?;
105 let dbf_reader = BufReader::new(dbf_file);
106 let dbf_reader = DbfReader::new(dbf_reader)?;
107 let field_descriptors = dbf_reader.field_descriptors().to_vec();
108
109 let index_entries = if shx_path.exists() {
111 let shx_file = File::open(&shx_path).ok();
112 if let Some(file) = shx_file {
113 let shx_reader = BufReader::new(file);
114 let mut shx_reader = ShxReader::new(shx_reader)?;
115 Some(shx_reader.read_all_entries()?)
116 } else {
117 None
118 }
119 } else {
120 None
121 };
122
123 let crs = if prj_path.exists() {
125 std::fs::read_to_string(&prj_path)
126 .ok()
127 .map(|s| s.trim().to_string())
128 .filter(|s| !s.is_empty())
129 } else {
130 None
131 };
132
133 let encoding = if cpg_path.exists() {
138 std::fs::read_to_string(&cpg_path)
139 .ok()
140 .map(|s| s.trim().to_string())
141 .filter(|s| !s.is_empty())
142 } else {
143 None
144 };
145
146 Ok(Self {
147 base_path: base_path.to_path_buf(),
148 header,
149 field_descriptors,
150 index_entries,
151 crs,
152 encoding,
153 })
154 }
155
156 pub fn header(&self) -> &ShapefileHeader {
158 &self.header
159 }
160
161 pub fn field_descriptors(&self) -> &[FieldDescriptor] {
163 &self.field_descriptors
164 }
165
166 pub fn index_entries(&self) -> Option<&[IndexEntry]> {
168 self.index_entries.as_deref()
169 }
170
171 pub fn crs(&self) -> Option<&str> {
173 self.crs.as_deref()
174 }
175
176 pub fn encoding(&self) -> Option<&str> {
182 self.encoding.as_deref()
183 }
184
185 pub fn features_in_bbox(
195 &mut self,
196 min_x: f64,
197 min_y: f64,
198 max_x: f64,
199 max_y: f64,
200 ) -> Result<Vec<ShapefileFeature>> {
201 let all_features = self.read_features()?;
202
203 let filtered = all_features
204 .into_iter()
205 .filter(|feature| {
206 let Some(ref geom) = feature.geometry else {
207 return false;
208 };
209 if let Some((fx_min, fy_min, fx_max, fy_max)) = Self::geometry_bbox(geom) {
210 !(fx_max < min_x || fx_min > max_x || fy_max < min_y || fy_min > max_y)
212 } else {
213 false
214 }
215 })
216 .collect();
217
218 Ok(filtered)
219 }
220
221 fn geometry_bbox(geom: &Geometry) -> Option<(f64, f64, f64, f64)> {
226 geom.bounds()
227 }
228
229 pub fn iter_features(&self) -> Result<FeatureIter<'_>> {
244 let shp_path = Self::with_extension(&self.base_path, "shp");
245 let dbf_path = Self::with_extension(&self.base_path, "dbf");
246
247 let shp_file = File::open(&shp_path)?;
248 let shp_reader = BufReader::new(shp_file);
249 let shp_reader = ShpReader::new(shp_reader)?;
250
251 let dbf_file = File::open(&dbf_path)?;
252 let dbf_reader = BufReader::new(dbf_file);
253 let dbf_reader = DbfReader::new(dbf_reader)?;
254
255 Ok(FeatureIter {
256 shp_reader,
257 dbf_reader,
258 field_descriptors: &self.field_descriptors,
259 done: false,
260 })
261 }
262
263 pub fn read_features_where<F>(&self, predicate: F) -> Result<Vec<ShapefileFeature>>
278 where
279 F: Fn(&ShapefileFeature) -> bool,
280 {
281 let all = self.read_features()?;
282 Ok(all.into_iter().filter(|f| predicate(f)).collect())
283 }
284
285 pub fn read_features_filtered(
295 &self,
296 filter: &crate::filter::FieldFilter,
297 ) -> Result<Vec<ShapefileFeature>> {
298 self.read_features_where(|f| filter.matches(f))
299 }
300
301 pub fn read_features(&self) -> Result<Vec<ShapefileFeature>> {
303 let shp_path = Self::with_extension(&self.base_path, "shp");
305 let dbf_path = Self::with_extension(&self.base_path, "dbf");
306
307 let shp_file = File::open(&shp_path)?;
308 let shp_reader = BufReader::new(shp_file);
309 let mut shp_reader = ShpReader::new(shp_reader)?;
310
311 let dbf_file = File::open(&dbf_path)?;
312 let dbf_reader = BufReader::new(dbf_file);
313 let mut dbf_reader = DbfReader::new(dbf_reader)?;
314
315 let shape_records = shp_reader.read_all_records()?;
317
318 let dbf_records = dbf_reader.read_all_records()?;
320
321 if shape_records.len() != dbf_records.len() {
323 return Err(ShapefileError::RecordMismatch {
324 shp_count: shape_records.len(),
325 dbf_count: dbf_records.len(),
326 });
327 }
328
329 let mut features = Vec::with_capacity(shape_records.len());
331 for (shape_record, dbf_record) in shape_records.iter().zip(dbf_records.iter()) {
332 let geometry = Self::shape_to_geometry(&shape_record.shape)?;
333
334 let attributes = Self::dbf_to_attributes(dbf_record, &self.field_descriptors);
336
337 features.push(ShapefileFeature::new(
338 shape_record.record_number,
339 geometry,
340 attributes,
341 ));
342 }
343
344 Ok(features)
345 }
346
347 fn shape_to_geometry(shape: &Shape) -> Result<Option<Geometry>> {
349 match shape {
350 Shape::Null => Ok(None),
351 Shape::Point(point) => {
352 let oxigdal_point = CorePoint::new(point.x, point.y);
353 Ok(Some(Geometry::Point(oxigdal_point)))
354 }
355 Shape::PointZ(point) => {
356 use oxigdal_core::vector::Coordinate;
357 let coord = if let Some(m) = point.m {
358 Coordinate::new_3dm(point.x, point.y, point.z, m)
359 } else {
360 Coordinate::new_3d(point.x, point.y, point.z)
361 };
362 Ok(Some(Geometry::Point(CorePoint::from_coord(coord))))
363 }
364 Shape::PointM(point) => {
365 use oxigdal_core::vector::Coordinate;
366 let coord = Coordinate::new_2dm(point.x, point.y, point.m);
367 Ok(Some(Geometry::Point(CorePoint::from_coord(coord))))
368 }
369 Shape::PolyLine(multi_part) => {
370 if multi_part.parts.len() == 1 {
371 let coords: Vec<Coordinate> = multi_part
373 .points
374 .iter()
375 .map(|p| Coordinate::new_2d(p.x, p.y))
376 .collect();
377
378 if coords.len() < 2 {
379 return Ok(None);
380 }
381
382 let linestring = CoreLineString::new(coords).map_err(|e| {
383 ShapefileError::invalid_geometry(format!("Invalid LineString: {}", e))
384 })?;
385 Ok(Some(Geometry::LineString(linestring)))
386 } else {
387 let mut linestrings = Vec::new();
389
390 for i in 0..multi_part.parts.len() {
391 let start_idx = multi_part.parts[i] as usize;
392 let end_idx = if i + 1 < multi_part.parts.len() {
393 multi_part.parts[i + 1] as usize
394 } else {
395 multi_part.points.len()
396 };
397
398 let coords: Vec<Coordinate> = multi_part.points[start_idx..end_idx]
399 .iter()
400 .map(|p| Coordinate::new_2d(p.x, p.y))
401 .collect();
402
403 if coords.len() >= 2 {
404 if let Ok(linestring) = CoreLineString::new(coords) {
405 linestrings.push(linestring);
406 }
407 }
408 }
409
410 if linestrings.is_empty() {
411 Ok(None)
412 } else {
413 Ok(Some(Geometry::MultiLineString(CoreMultiLineString::new(
414 linestrings,
415 ))))
416 }
417 }
418 }
419 Shape::Polygon(multi_part) => {
420 if multi_part.parts.is_empty() {
421 return Ok(None);
422 }
423
424 let exterior_start = multi_part.parts[0] as usize;
426 let exterior_end = if multi_part.parts.len() > 1 {
427 multi_part.parts[1] as usize
428 } else {
429 multi_part.points.len()
430 };
431
432 let exterior_coords: Vec<Coordinate> = multi_part.points
433 [exterior_start..exterior_end]
434 .iter()
435 .map(|p| Coordinate::new_2d(p.x, p.y))
436 .collect();
437
438 if exterior_coords.len() < 4 {
439 return Ok(None);
440 }
441
442 let exterior = CoreLineString::new(exterior_coords).map_err(|e| {
443 ShapefileError::invalid_geometry(format!("Invalid exterior ring: {}", e))
444 })?;
445
446 let mut interiors = Vec::new();
448 for i in 1..multi_part.parts.len() {
449 let start_idx = multi_part.parts[i] as usize;
450 let end_idx = if i + 1 < multi_part.parts.len() {
451 multi_part.parts[i + 1] as usize
452 } else {
453 multi_part.points.len()
454 };
455
456 let interior_coords: Vec<Coordinate> = multi_part.points[start_idx..end_idx]
457 .iter()
458 .map(|p| Coordinate::new_2d(p.x, p.y))
459 .collect();
460
461 if interior_coords.len() >= 4 {
462 if let Ok(interior) = CoreLineString::new(interior_coords) {
463 interiors.push(interior);
464 }
465 }
466 }
467
468 let polygon = CorePolygon::new(exterior, interiors).map_err(|e| {
469 ShapefileError::invalid_geometry(format!("Invalid polygon: {}", e))
470 })?;
471
472 Ok(Some(Geometry::Polygon(polygon)))
473 }
474 Shape::MultiPoint(multi_part) => {
475 let points: Vec<CorePoint> = multi_part
476 .points
477 .iter()
478 .map(|p| CorePoint::new(p.x, p.y))
479 .collect();
480
481 if points.is_empty() {
482 Ok(None)
483 } else {
484 Ok(Some(Geometry::MultiPoint(CoreMultiPoint::new(points))))
485 }
486 }
487 Shape::PolyLineZ(shape_z) => Self::multipart_z_to_linestring_geometry(
489 &shape_z.base,
490 &shape_z.z_values,
491 shape_z.m_values.as_deref(),
492 ),
493 Shape::PolygonZ(shape_z) => Self::multipart_z_to_polygon_geometry(
494 &shape_z.base,
495 &shape_z.z_values,
496 shape_z.m_values.as_deref(),
497 ),
498 Shape::MultiPointZ(shape_z) => Self::multipart_z_to_multipoint_geometry(
499 &shape_z.base,
500 &shape_z.z_values,
501 shape_z.m_values.as_deref(),
502 ),
503 Shape::PolyLineM(shape_m) => {
505 Self::multipart_m_to_linestring_geometry(&shape_m.base, &shape_m.m_values)
506 }
507 Shape::PolygonM(shape_m) => {
508 Self::multipart_m_to_polygon_geometry(&shape_m.base, &shape_m.m_values)
509 }
510 Shape::MultiPointM(shape_m) => {
511 Self::multipart_m_to_multipoint_geometry(&shape_m.base, &shape_m.m_values)
512 }
513 Shape::MultiPatch(mp_shape) => {
516 use oxigdal_core::vector::Coordinate;
518 let points: Vec<CorePoint> = mp_shape
519 .base
520 .points
521 .iter()
522 .zip(mp_shape.z_values.iter())
523 .map(|(p, z)| CorePoint::from_coord(Coordinate::new_3d(p.x, p.y, *z)))
524 .collect();
525
526 if points.is_empty() {
527 Ok(None)
528 } else {
529 Ok(Some(Geometry::MultiPoint(CoreMultiPoint::new(points))))
530 }
531 }
532 }
533 }
534
535 fn multipart_z_to_linestring_geometry(
538 base: &crate::shp::MultiPartShape,
539 z_values: &[f64],
540 m_values: Option<&[f64]>,
541 ) -> Result<Option<Geometry>> {
542 use oxigdal_core::vector::Coordinate;
543
544 let make_coord = |i: usize, p: &crate::shp::shapes::Point| -> Coordinate {
545 let z = z_values.get(i).copied().unwrap_or(0.0);
546 if let Some(mv) = m_values {
547 Coordinate::new_3dm(p.x, p.y, z, mv.get(i).copied().unwrap_or(0.0))
548 } else {
549 Coordinate::new_3d(p.x, p.y, z)
550 }
551 };
552
553 if base.parts.len() == 1 {
554 let coords: Vec<Coordinate> = base
555 .points
556 .iter()
557 .enumerate()
558 .map(|(i, p)| make_coord(i, p))
559 .collect();
560 if coords.len() < 2 {
561 return Ok(None);
562 }
563 let linestring = CoreLineString::new(coords).map_err(|e| {
564 ShapefileError::invalid_geometry(format!("Invalid LineString: {}", e))
565 })?;
566 Ok(Some(Geometry::LineString(linestring)))
567 } else {
568 let mut linestrings = Vec::new();
569 for i in 0..base.parts.len() {
570 let start = base.parts[i] as usize;
571 let end = if i + 1 < base.parts.len() {
572 base.parts[i + 1] as usize
573 } else {
574 base.points.len()
575 };
576 let coords: Vec<Coordinate> = base.points[start..end]
577 .iter()
578 .enumerate()
579 .map(|(j, p)| make_coord(start + j, p))
580 .collect();
581 if coords.len() >= 2 {
582 if let Ok(ls) = CoreLineString::new(coords) {
583 linestrings.push(ls);
584 }
585 }
586 }
587 if linestrings.is_empty() {
588 Ok(None)
589 } else {
590 Ok(Some(Geometry::MultiLineString(CoreMultiLineString::new(
591 linestrings,
592 ))))
593 }
594 }
595 }
596
597 fn multipart_z_to_polygon_geometry(
599 base: &crate::shp::MultiPartShape,
600 z_values: &[f64],
601 m_values: Option<&[f64]>,
602 ) -> Result<Option<Geometry>> {
603 use oxigdal_core::vector::Coordinate;
604
605 if base.parts.is_empty() {
606 return Ok(None);
607 }
608
609 let make_coord = |i: usize, p: &crate::shp::shapes::Point| -> Coordinate {
610 let z = z_values.get(i).copied().unwrap_or(0.0);
611 if let Some(mv) = m_values {
612 Coordinate::new_3dm(p.x, p.y, z, mv.get(i).copied().unwrap_or(0.0))
613 } else {
614 Coordinate::new_3d(p.x, p.y, z)
615 }
616 };
617
618 let ext_start = base.parts[0] as usize;
619 let ext_end = if base.parts.len() > 1 {
620 base.parts[1] as usize
621 } else {
622 base.points.len()
623 };
624 let ext_coords: Vec<Coordinate> = base.points[ext_start..ext_end]
625 .iter()
626 .enumerate()
627 .map(|(j, p)| make_coord(ext_start + j, p))
628 .collect();
629 if ext_coords.len() < 4 {
630 return Ok(None);
631 }
632 let exterior = CoreLineString::new(ext_coords).map_err(|e| {
633 ShapefileError::invalid_geometry(format!("Invalid exterior Z ring: {}", e))
634 })?;
635
636 let mut interiors = Vec::new();
637 for i in 1..base.parts.len() {
638 let start = base.parts[i] as usize;
639 let end = if i + 1 < base.parts.len() {
640 base.parts[i + 1] as usize
641 } else {
642 base.points.len()
643 };
644 let coords: Vec<Coordinate> = base.points[start..end]
645 .iter()
646 .enumerate()
647 .map(|(j, p)| make_coord(start + j, p))
648 .collect();
649 if coords.len() >= 4 {
650 if let Ok(ring) = CoreLineString::new(coords) {
651 interiors.push(ring);
652 }
653 }
654 }
655 let polygon = CorePolygon::new(exterior, interiors)
656 .map_err(|e| ShapefileError::invalid_geometry(format!("Invalid polygon Z: {}", e)))?;
657 Ok(Some(Geometry::Polygon(polygon)))
658 }
659
660 fn multipart_z_to_multipoint_geometry(
662 base: &crate::shp::MultiPartShape,
663 z_values: &[f64],
664 m_values: Option<&[f64]>,
665 ) -> Result<Option<Geometry>> {
666 use oxigdal_core::vector::Coordinate;
667 let points: Vec<CorePoint> = base
668 .points
669 .iter()
670 .enumerate()
671 .map(|(i, p)| {
672 let z = z_values.get(i).copied().unwrap_or(0.0);
673 let coord = if let Some(mv) = m_values {
674 Coordinate::new_3dm(p.x, p.y, z, mv.get(i).copied().unwrap_or(0.0))
675 } else {
676 Coordinate::new_3d(p.x, p.y, z)
677 };
678 CorePoint::from_coord(coord)
679 })
680 .collect();
681 if points.is_empty() {
682 Ok(None)
683 } else {
684 Ok(Some(Geometry::MultiPoint(CoreMultiPoint::new(points))))
685 }
686 }
687
688 fn multipart_m_to_linestring_geometry(
690 base: &crate::shp::MultiPartShape,
691 m_values: &[f64],
692 ) -> Result<Option<Geometry>> {
693 use oxigdal_core::vector::Coordinate;
694
695 let make_coord = |i: usize, p: &crate::shp::shapes::Point| -> Coordinate {
696 Coordinate::new_2dm(p.x, p.y, m_values.get(i).copied().unwrap_or(0.0))
697 };
698
699 if base.parts.len() == 1 {
700 let coords: Vec<Coordinate> = base
701 .points
702 .iter()
703 .enumerate()
704 .map(|(i, p)| make_coord(i, p))
705 .collect();
706 if coords.len() < 2 {
707 return Ok(None);
708 }
709 let linestring = CoreLineString::new(coords).map_err(|e| {
710 ShapefileError::invalid_geometry(format!("Invalid LineStringM: {}", e))
711 })?;
712 Ok(Some(Geometry::LineString(linestring)))
713 } else {
714 let mut linestrings = Vec::new();
715 for i in 0..base.parts.len() {
716 let start = base.parts[i] as usize;
717 let end = if i + 1 < base.parts.len() {
718 base.parts[i + 1] as usize
719 } else {
720 base.points.len()
721 };
722 let coords: Vec<Coordinate> = base.points[start..end]
723 .iter()
724 .enumerate()
725 .map(|(j, p)| make_coord(start + j, p))
726 .collect();
727 if coords.len() >= 2 {
728 if let Ok(ls) = CoreLineString::new(coords) {
729 linestrings.push(ls);
730 }
731 }
732 }
733 if linestrings.is_empty() {
734 Ok(None)
735 } else {
736 Ok(Some(Geometry::MultiLineString(CoreMultiLineString::new(
737 linestrings,
738 ))))
739 }
740 }
741 }
742
743 fn multipart_m_to_polygon_geometry(
745 base: &crate::shp::MultiPartShape,
746 m_values: &[f64],
747 ) -> Result<Option<Geometry>> {
748 use oxigdal_core::vector::Coordinate;
749
750 if base.parts.is_empty() {
751 return Ok(None);
752 }
753
754 let make_coord = |i: usize, p: &crate::shp::shapes::Point| -> Coordinate {
755 Coordinate::new_2dm(p.x, p.y, m_values.get(i).copied().unwrap_or(0.0))
756 };
757
758 let ext_start = base.parts[0] as usize;
759 let ext_end = if base.parts.len() > 1 {
760 base.parts[1] as usize
761 } else {
762 base.points.len()
763 };
764 let ext_coords: Vec<Coordinate> = base.points[ext_start..ext_end]
765 .iter()
766 .enumerate()
767 .map(|(j, p)| make_coord(ext_start + j, p))
768 .collect();
769 if ext_coords.len() < 4 {
770 return Ok(None);
771 }
772 let exterior = CoreLineString::new(ext_coords).map_err(|e| {
773 ShapefileError::invalid_geometry(format!("Invalid exterior M ring: {}", e))
774 })?;
775
776 let mut interiors = Vec::new();
777 for i in 1..base.parts.len() {
778 let start = base.parts[i] as usize;
779 let end = if i + 1 < base.parts.len() {
780 base.parts[i + 1] as usize
781 } else {
782 base.points.len()
783 };
784 let coords: Vec<Coordinate> = base.points[start..end]
785 .iter()
786 .enumerate()
787 .map(|(j, p)| make_coord(start + j, p))
788 .collect();
789 if coords.len() >= 4 {
790 if let Ok(ring) = CoreLineString::new(coords) {
791 interiors.push(ring);
792 }
793 }
794 }
795 let polygon = CorePolygon::new(exterior, interiors)
796 .map_err(|e| ShapefileError::invalid_geometry(format!("Invalid polygon M: {}", e)))?;
797 Ok(Some(Geometry::Polygon(polygon)))
798 }
799
800 fn multipart_m_to_multipoint_geometry(
802 base: &crate::shp::MultiPartShape,
803 m_values: &[f64],
804 ) -> Result<Option<Geometry>> {
805 use oxigdal_core::vector::Coordinate;
806 let points: Vec<CorePoint> = base
807 .points
808 .iter()
809 .enumerate()
810 .map(|(i, p)| {
811 CorePoint::from_coord(Coordinate::new_2dm(
812 p.x,
813 p.y,
814 m_values.get(i).copied().unwrap_or(0.0),
815 ))
816 })
817 .collect();
818 if points.is_empty() {
819 Ok(None)
820 } else {
821 Ok(Some(Geometry::MultiPoint(CoreMultiPoint::new(points))))
822 }
823 }
824
825 fn dbf_to_attributes(
827 dbf_record: &crate::dbf::DbfRecord,
828 field_descriptors: &[FieldDescriptor],
829 ) -> HashMap<String, FieldValue> {
830 let mut attributes = HashMap::new();
831
832 for (field, value) in field_descriptors.iter().zip(&dbf_record.values) {
833 let property_value = match value {
834 crate::dbf::FieldValue::String(s) => FieldValue::String(s.clone()),
835 crate::dbf::FieldValue::Integer(i) => FieldValue::Integer(*i),
836 crate::dbf::FieldValue::Float(f) => FieldValue::Float(*f),
837 crate::dbf::FieldValue::Boolean(b) => FieldValue::Bool(*b),
838 crate::dbf::FieldValue::Date(d) => FieldValue::String(d.clone()),
839 crate::dbf::FieldValue::Null => FieldValue::Null,
840 };
841
842 attributes.insert(field.name.clone(), property_value);
843 }
844
845 attributes
846 }
847
848 pub(crate) fn dbf_to_attributes_pub(
850 dbf_record: &crate::dbf::DbfRecord,
851 field_descriptors: &[FieldDescriptor],
852 ) -> HashMap<String, FieldValue> {
853 Self::dbf_to_attributes(dbf_record, field_descriptors)
854 }
855
856 pub(crate) fn shape_to_geometry_pub(shape: &Shape) -> Result<Option<Geometry>> {
858 Self::shape_to_geometry(shape)
859 }
860
861 fn with_extension<P: AsRef<Path>>(base_path: P, ext: &str) -> PathBuf {
863 let base = base_path.as_ref();
864
865 if base.extension().is_some() {
867 base.with_extension(ext)
868 } else {
869 let mut path = base.to_path_buf();
871 path.set_extension(ext);
872 path
873 }
874 }
875}
876
877pub struct FeatureIter<'a> {
889 shp_reader: ShpReader<BufReader<File>>,
890 dbf_reader: DbfReader<BufReader<File>>,
891 field_descriptors: &'a [FieldDescriptor],
893 done: bool,
895}
896
897impl<'a> Iterator for FeatureIter<'a> {
898 type Item = Result<ShapefileFeature>;
899
900 fn next(&mut self) -> Option<Self::Item> {
901 if self.done {
902 return None;
903 }
904
905 let shp_record = match self.shp_reader.read_record() {
907 Ok(Some(r)) => r,
908 Ok(None) => {
909 self.done = true;
910 return None;
911 }
912 Err(e) => {
913 self.done = true;
914 return Some(Err(e));
915 }
916 };
917
918 let dbf_record = match self.dbf_reader.read_record() {
920 Ok(Some(r)) => r,
921 Ok(None) => {
922 self.done = true;
923 return None;
924 }
925 Err(e) => {
926 self.done = true;
927 return Some(Err(e));
928 }
929 };
930
931 let geometry = match ShapefileReader::shape_to_geometry_pub(&shp_record.shape) {
933 Ok(g) => g,
934 Err(e) => {
935 self.done = true;
936 return Some(Err(e));
937 }
938 };
939
940 let attributes =
942 ShapefileReader::dbf_to_attributes_pub(&dbf_record, self.field_descriptors);
943
944 Some(Ok(ShapefileFeature::new(
945 shp_record.record_number,
946 geometry,
947 attributes,
948 )))
949 }
950}
951
952#[cfg(test)]
953mod tests {
954 use super::*;
955
956 #[test]
957 fn test_path_extension_helper() {
958 let base = std::env::temp_dir().join("oxigdal_shapefile_test");
959 let expected_shp = std::env::temp_dir().join("oxigdal_shapefile_test.shp");
960 assert_eq!(ShapefileReader::with_extension(&base, "shp"), expected_shp);
961
962 let base_shp = std::env::temp_dir().join("oxigdal_shapefile_test.shp");
963 let expected_dbf = std::env::temp_dir().join("oxigdal_shapefile_test.dbf");
964 assert_eq!(
965 ShapefileReader::with_extension(&base_shp, "dbf"),
966 expected_dbf
967 );
968 }
969
970 #[test]
971 fn test_shapefile_feature_creation() {
972 let mut attributes = HashMap::new();
973 attributes.insert("name".to_string(), FieldValue::String("Test".to_string()));
974 attributes.insert("value".to_string(), FieldValue::Integer(42));
975
976 let geometry = Some(Geometry::Point(CorePoint::new(10.0, 20.0)));
977
978 let feature = ShapefileFeature::new(1, geometry, attributes);
979 assert_eq!(feature.record_number, 1);
980 assert!(feature.geometry.is_some());
981 assert_eq!(feature.attributes.len(), 2);
982 }
983}