1use anyhow::{Context, Result, anyhow};
7use std::fs::File;
8use std::io::{BufReader, BufWriter};
9use std::path::Path;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum VectorFormat {
16 GeoJson,
18 Shapefile,
20 FlatGeobuf,
22}
23
24impl VectorFormat {
25 pub fn from_path(path: &Path) -> Option<Self> {
27 path.extension()
28 .and_then(|e| e.to_str())
29 .and_then(|e| match e.to_lowercase().as_str() {
30 "geojson" | "json" => Some(Self::GeoJson),
31 "shp" => Some(Self::Shapefile),
32 "fgb" => Some(Self::FlatGeobuf),
33 _ => None,
34 })
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum FilterOp {
41 Eq,
43 Ne,
45 Contains,
47}
48
49#[derive(Debug, Clone)]
51pub struct AttributeFilter {
52 pub field: String,
54 pub op: FilterOp,
56 pub value: String,
58}
59
60impl AttributeFilter {
61 pub fn matches_json(&self, props: &serde_json::Map<String, serde_json::Value>) -> bool {
63 match props.get(&self.field) {
64 None => false,
65 Some(v) => {
66 let candidate = json_value_to_string(v).to_lowercase();
67 let target = self.value.to_lowercase();
68 match self.op {
69 FilterOp::Eq => candidate == target,
70 FilterOp::Ne => candidate != target,
71 FilterOp::Contains => candidate.contains(&target),
72 }
73 }
74 }
75 }
76
77 pub fn matches_field_map(
79 &self,
80 props: &std::collections::HashMap<String, oxigeo_core::vector::FieldValue>,
81 ) -> bool {
82 match props.get(&self.field) {
83 None => false,
84 Some(v) => {
85 let candidate = field_value_to_string(v).to_lowercase();
86 let target = self.value.to_lowercase();
87 match self.op {
88 FilterOp::Eq => candidate == target,
89 FilterOp::Ne => candidate != target,
90 FilterOp::Contains => candidate.contains(&target),
91 }
92 }
93 }
94 }
95}
96
97fn json_value_to_string(v: &serde_json::Value) -> String {
100 match v {
101 serde_json::Value::Null => String::new(),
102 serde_json::Value::Bool(b) => b.to_string(),
103 serde_json::Value::Number(n) => n.to_string(),
104 serde_json::Value::String(s) => s.clone(),
105 serde_json::Value::Array(a) => format!("{a:?}"),
106 serde_json::Value::Object(o) => format!("{o:?}"),
107 }
108}
109
110fn field_value_to_string(v: &oxigeo_core::vector::FieldValue) -> String {
111 use oxigeo_core::vector::FieldValue;
112 match v {
113 FieldValue::Null => String::new(),
114 FieldValue::Bool(b) => b.to_string(),
115 FieldValue::Integer(i) => i.to_string(),
116 FieldValue::UInteger(u) => u.to_string(),
117 FieldValue::Float(f) => f.to_string(),
118 FieldValue::String(s) => s.clone(),
119 FieldValue::Array(a) => format!("{a:?}"),
120 FieldValue::Object(o) => format!("{o:?}"),
121 FieldValue::Date(d) => d.to_string(),
122 FieldValue::Blob(b) => format!("{b:?}"),
123 }
124}
125
126pub fn geojson_geom_to_core(
131 geom: &oxigeo_geojson::Geometry,
132) -> Result<oxigeo_core::vector::Geometry> {
133 use oxigeo_core::vector::{
134 Geometry as CoreGeom, GeometryCollection as CoreGC, LineString as CoreLS,
135 MultiLineString as CoreMLS, MultiPoint as CoreMP, MultiPolygon as CoreMPoly,
136 Point as CorePoint, Polygon as CorePoly,
137 };
138 use oxigeo_geojson::Geometry as GjGeom;
139
140 match geom {
141 GjGeom::Point(p) => {
142 let coord = pos_to_coord(&p.coordinates)?;
143 Ok(CoreGeom::Point(CorePoint::from_coord(coord)))
144 }
145 GjGeom::LineString(ls) => {
146 let coords = positions_to_coords(&ls.coordinates)?;
147 let core_ls =
148 CoreLS::new(coords).map_err(|e| anyhow!("LineString conversion error: {e}"))?;
149 Ok(CoreGeom::LineString(core_ls))
150 }
151 GjGeom::Polygon(p) => {
152 let rings = p
153 .coordinates
154 .iter()
155 .map(|ring| positions_to_coords(ring))
156 .collect::<Result<Vec<_>>>()?;
157 let (exterior, interiors) = rings_to_exterior_interiors(rings)?;
158 let core_poly = CorePoly::new(exterior, interiors)
159 .map_err(|e| anyhow!("Polygon conversion error: {e}"))?;
160 Ok(CoreGeom::Polygon(core_poly))
161 }
162 GjGeom::MultiPoint(mp) => {
163 let points = mp
164 .coordinates
165 .iter()
166 .map(|pos| {
167 let coord = pos_to_coord(pos)?;
168 Ok(CorePoint::from_coord(coord))
169 })
170 .collect::<Result<Vec<_>>>()?;
171 Ok(CoreGeom::MultiPoint(CoreMP::new(points)))
172 }
173 GjGeom::MultiLineString(mls) => {
174 let lines = mls
175 .coordinates
176 .iter()
177 .map(|ring| {
178 let coords = positions_to_coords(ring)?;
179 CoreLS::new(coords).map_err(|e| anyhow!("MultiLineString line error: {e}"))
180 })
181 .collect::<Result<Vec<_>>>()?;
182 Ok(CoreGeom::MultiLineString(CoreMLS {
183 line_strings: lines,
184 }))
185 }
186 GjGeom::MultiPolygon(mpoly) => {
187 let polygons = mpoly
188 .coordinates
189 .iter()
190 .map(|rings| {
191 let coord_rings = rings
192 .iter()
193 .map(|ring| positions_to_coords(ring))
194 .collect::<Result<Vec<_>>>()?;
195 let (exterior, interiors) = rings_to_exterior_interiors(coord_rings)?;
196 CorePoly::new(exterior, interiors)
197 .map_err(|e| anyhow!("MultiPolygon polygon error: {e}"))
198 })
199 .collect::<Result<Vec<_>>>()?;
200 Ok(CoreGeom::MultiPolygon(CoreMPoly { polygons }))
201 }
202 GjGeom::GeometryCollection(gc) => {
203 let geoms = gc
204 .geometries
205 .iter()
206 .map(geojson_geom_to_core)
207 .collect::<Result<Vec<_>>>()?;
208 Ok(CoreGeom::GeometryCollection(CoreGC { geometries: geoms }))
209 }
210 }
211}
212
213pub fn core_geom_to_geojson(
215 geom: &oxigeo_core::vector::Geometry,
216) -> Result<oxigeo_geojson::Geometry> {
217 use oxigeo_core::vector::Geometry as CoreGeom;
218 use oxigeo_geojson::types::{
219 Geometry as GjGeom, GeometryCollection as GjGC, LineString as GjLS,
220 MultiLineString as GjMLS, MultiPoint as GjMP, MultiPolygon as GjMPoly, Point as GjPoint,
221 Polygon as GjPoly,
222 };
223
224 match geom {
225 CoreGeom::Point(p) => {
226 let pos = coord_to_pos(&p.coord);
227 let gj_point = GjPoint::new(pos).map_err(|e| anyhow!("Point conversion error: {e}"))?;
228 Ok(GjGeom::Point(gj_point))
229 }
230 CoreGeom::LineString(ls) => {
231 let coords = ls.coords.iter().map(coord_to_pos).collect();
232 let gj_ls =
233 GjLS::new(coords).map_err(|e| anyhow!("LineString conversion error: {e}"))?;
234 Ok(GjGeom::LineString(gj_ls))
235 }
236 CoreGeom::Polygon(p) => {
237 let exterior: Vec<Vec<f64>> = p.exterior.coords.iter().map(coord_to_pos).collect();
238 let mut rings = vec![exterior];
239 for interior in &p.interiors {
240 rings.push(interior.coords.iter().map(coord_to_pos).collect());
241 }
242 let gj_poly =
243 GjPoly::new(rings).map_err(|e| anyhow!("Polygon conversion error: {e}"))?;
244 Ok(GjGeom::Polygon(gj_poly))
245 }
246 CoreGeom::MultiPoint(mp) => {
247 let positions = mp.points.iter().map(|p| coord_to_pos(&p.coord)).collect();
248 let gj_mp = GjMP {
249 coordinates: positions,
250 bbox: None,
251 };
252 Ok(GjGeom::MultiPoint(gj_mp))
253 }
254 CoreGeom::MultiLineString(mls) => {
255 let lines = mls
256 .line_strings
257 .iter()
258 .map(|ls| ls.coords.iter().map(coord_to_pos).collect::<Vec<_>>())
259 .collect();
260 let gj_mls = GjMLS {
261 coordinates: lines,
262 bbox: None,
263 };
264 Ok(GjGeom::MultiLineString(gj_mls))
265 }
266 CoreGeom::MultiPolygon(mpoly) => {
267 let polys = mpoly
268 .polygons
269 .iter()
270 .map(|p| {
271 let ext: Vec<Vec<f64>> = p.exterior.coords.iter().map(coord_to_pos).collect();
272 let mut rings = vec![ext];
273 for interior in &p.interiors {
274 rings.push(interior.coords.iter().map(coord_to_pos).collect());
275 }
276 rings
277 })
278 .collect();
279 let gj_mpoly = GjMPoly {
280 coordinates: polys,
281 bbox: None,
282 };
283 Ok(GjGeom::MultiPolygon(gj_mpoly))
284 }
285 CoreGeom::GeometryCollection(gc) => {
286 let geoms = gc
287 .geometries
288 .iter()
289 .map(core_geom_to_geojson)
290 .collect::<Result<Vec<_>>>()?;
291 let gj_gc = GjGC {
292 geometries: geoms,
293 bbox: None,
294 };
295 Ok(GjGeom::GeometryCollection(gj_gc))
296 }
297 }
298}
299
300fn pos_to_coord(pos: &[f64]) -> Result<oxigeo_core::vector::Coordinate> {
303 if pos.len() < 2 {
304 return Err(anyhow!(
305 "position needs at least 2 elements, got {}",
306 pos.len()
307 ));
308 }
309 Ok(oxigeo_core::vector::Coordinate {
310 x: pos[0],
311 y: pos[1],
312 z: pos.get(2).copied(),
313 m: None,
314 })
315}
316
317fn positions_to_coords(positions: &[Vec<f64>]) -> Result<Vec<oxigeo_core::vector::Coordinate>> {
318 positions.iter().map(|p| pos_to_coord(p)).collect()
319}
320
321fn coord_to_pos(c: &oxigeo_core::vector::Coordinate) -> Vec<f64> {
322 match c.z {
323 Some(z) => vec![c.x, c.y, z],
324 None => vec![c.x, c.y],
325 }
326}
327
328fn rings_to_exterior_interiors(
331 mut rings: Vec<Vec<oxigeo_core::vector::Coordinate>>,
332) -> Result<(
333 oxigeo_core::vector::LineString,
334 Vec<oxigeo_core::vector::LineString>,
335)> {
336 use oxigeo_core::vector::LineString as CoreLS;
337
338 if rings.is_empty() {
339 return Err(anyhow!("polygon has no rings"));
340 }
341
342 let exterior_coords = rings.remove(0);
343 let exterior = CoreLS::new(exterior_coords).map_err(|e| anyhow!("exterior ring error: {e}"))?;
344
345 let interiors = rings
346 .into_iter()
347 .map(|ring| CoreLS::new(ring).map_err(|e| anyhow!("interior ring error: {e}")))
348 .collect::<Result<Vec<_>>>()?;
349
350 Ok((exterior, interiors))
351}
352
353pub fn infer_shapefile_schema_from_geojson(
358 features: &[oxigeo_geojson::Feature],
359) -> Result<(
360 oxigeo_shapefile::shp::shapes::ShapeType,
361 Vec<oxigeo_shapefile::dbf::FieldDescriptor>,
362)> {
363 use oxigeo_geojson::Geometry as GjGeom;
364 use oxigeo_shapefile::{
365 dbf::{FieldDescriptor, FieldType},
366 shp::shapes::ShapeType,
367 };
368
369 let shape_type = features
371 .iter()
372 .find_map(|f| f.geometry.as_ref())
373 .map(|g| match g {
374 GjGeom::Point(_) => ShapeType::Point,
375 GjGeom::LineString(_) | GjGeom::MultiLineString(_) => ShapeType::PolyLine,
376 GjGeom::Polygon(_) | GjGeom::MultiPolygon(_) => ShapeType::Polygon,
377 GjGeom::MultiPoint(_) => ShapeType::MultiPoint,
378 GjGeom::GeometryCollection(_) => ShapeType::Point, })
380 .unwrap_or(ShapeType::Point);
381
382 let mut field_widths: std::collections::HashMap<String, u8> = std::collections::HashMap::new();
384
385 for feature in features {
386 if let Some(props) = &feature.properties {
387 for (key, value) in props {
388 let key_short = truncate_field_name(key);
389 let width = json_value_str_width(value);
390 let entry = field_widths.entry(key_short).or_insert(0);
391 if width > *entry {
392 *entry = width;
393 }
394 }
395 }
396 }
397
398 let mut descriptors = Vec::new();
399 for (name, width) in &field_widths {
400 let length = width.clamp(&1, &254);
401 let desc = FieldDescriptor::new(name.clone(), FieldType::Character, *length, 0)
402 .with_context(|| format!("invalid field descriptor for '{name}'"))?;
403 descriptors.push(desc);
404 }
405
406 descriptors.sort_by(|a, b| a.name.cmp(&b.name));
408
409 Ok((shape_type, descriptors))
410}
411
412pub fn infer_shapefile_schema_from_shapefiles(
416 features: &[oxigeo_shapefile::reader::ShapefileFeature],
417) -> Result<(
418 oxigeo_shapefile::shp::shapes::ShapeType,
419 Vec<oxigeo_shapefile::dbf::FieldDescriptor>,
420)> {
421 use oxigeo_core::vector::Geometry as CoreGeom;
422 use oxigeo_shapefile::{
423 dbf::{FieldDescriptor, FieldType},
424 shp::shapes::ShapeType,
425 };
426
427 let shape_type = features
428 .iter()
429 .find_map(|f| f.geometry.as_ref())
430 .map(|g| match g {
431 CoreGeom::Point(_) => ShapeType::Point,
432 CoreGeom::LineString(_) | CoreGeom::MultiLineString(_) => ShapeType::PolyLine,
433 CoreGeom::Polygon(_) | CoreGeom::MultiPolygon(_) => ShapeType::Polygon,
434 CoreGeom::MultiPoint(_) => ShapeType::MultiPoint,
435 CoreGeom::GeometryCollection(_) => ShapeType::Point,
436 })
437 .unwrap_or(ShapeType::Point);
438
439 let mut field_widths: std::collections::HashMap<String, u8> = std::collections::HashMap::new();
440
441 for feature in features {
442 for (key, value) in &feature.attributes {
443 let key_short = truncate_field_name(key);
444 let width = field_value_str_width(value);
445 let entry = field_widths.entry(key_short).or_insert(0);
446 if width > *entry {
447 *entry = width;
448 }
449 }
450 }
451
452 let mut descriptors = Vec::new();
453 for (name, width) in &field_widths {
454 let length = width.clamp(&1, &254);
455 let desc = FieldDescriptor::new(name.clone(), FieldType::Character, *length, 0)
456 .with_context(|| format!("invalid field descriptor for '{name}'"))?;
457 descriptors.push(desc);
458 }
459
460 descriptors.sort_by(|a, b| a.name.cmp(&b.name));
461
462 Ok((shape_type, descriptors))
463}
464
465fn truncate_field_name(name: &str) -> String {
467 name.chars().take(10).collect()
468}
469
470fn json_value_str_width(v: &serde_json::Value) -> u8 {
471 let s = json_value_to_string(v);
472 s.len().min(254) as u8
473}
474
475fn field_value_str_width(v: &oxigeo_core::vector::FieldValue) -> u8 {
476 let s = field_value_to_string(v);
477 s.len().min(254) as u8
478}
479
480fn geojson_feature_to_shapefile(
483 feature: &oxigeo_geojson::Feature,
484 record_number: i32,
485 field_names: &[String],
486) -> Result<oxigeo_shapefile::reader::ShapefileFeature> {
487 use oxigeo_core::vector::FieldValue;
488 use oxigeo_shapefile::reader::ShapefileFeature;
489
490 let geometry = match &feature.geometry {
491 Some(gj_geom) => Some(geojson_geom_to_core(gj_geom)?),
492 None => None,
493 };
494
495 let mut attributes = std::collections::HashMap::new();
496
497 if let Some(props) = &feature.properties {
498 for name in field_names {
499 let original_key = props
501 .keys()
502 .find(|k| truncate_field_name(k) == *name)
503 .cloned();
504
505 if let Some(key) = original_key {
506 let json_val = props.get(&key).cloned().unwrap_or(serde_json::Value::Null);
507 let fv = json_to_field_value(&json_val);
508 attributes.insert(name.clone(), fv);
509 } else {
510 attributes.insert(name.clone(), FieldValue::Null);
511 }
512 }
513 } else {
514 for name in field_names {
515 attributes.insert(name.clone(), FieldValue::Null);
516 }
517 }
518
519 Ok(ShapefileFeature::new(record_number, geometry, attributes))
520}
521
522fn json_to_field_value(v: &serde_json::Value) -> oxigeo_core::vector::FieldValue {
523 use oxigeo_core::vector::FieldValue;
524 match v {
525 serde_json::Value::Null => FieldValue::Null,
526 serde_json::Value::Bool(b) => FieldValue::Bool(*b),
527 serde_json::Value::Number(n) => {
528 if let Some(i) = n.as_i64() {
529 FieldValue::Integer(i)
530 } else if let Some(u) = n.as_u64() {
531 FieldValue::UInteger(u)
532 } else {
533 FieldValue::Float(n.as_f64().unwrap_or(0.0))
534 }
535 }
536 serde_json::Value::String(s) => FieldValue::String(s.clone()),
537 other => FieldValue::String(other.to_string()),
538 }
539}
540
541fn shapefile_feature_to_geojson(
544 sf: &oxigeo_shapefile::reader::ShapefileFeature,
545) -> Result<oxigeo_geojson::Feature> {
546 use oxigeo_geojson::Feature as GjFeature;
547
548 let geometry = match &sf.geometry {
549 Some(core_geom) => Some(core_geom_to_geojson(core_geom)?),
550 None => None,
551 };
552
553 let mut props = serde_json::Map::new();
554 for (key, value) in &sf.attributes {
555 props.insert(key.clone(), value.to_json_value());
556 }
557
558 Ok(GjFeature::new(geometry, Some(props)))
559}
560
561pub fn convert_vector(
567 input: &Path,
568 output: &Path,
569 filter: Option<&AttributeFilter>,
570) -> Result<usize> {
571 let input_fmt = VectorFormat::from_path(input)
572 .ok_or_else(|| anyhow!("Unknown input vector format: {}", input.display()))?;
573 let output_fmt = VectorFormat::from_path(output)
574 .ok_or_else(|| anyhow!("Cannot determine output format from: {}", output.display()))?;
575
576 match (input_fmt, output_fmt) {
577 (VectorFormat::GeoJson, VectorFormat::GeoJson) => {
578 convert_geojson_to_geojson(input, output, filter)
579 }
580 (VectorFormat::GeoJson, VectorFormat::Shapefile) => {
581 convert_geojson_to_shapefile(input, output, filter)
582 }
583 (VectorFormat::Shapefile, VectorFormat::GeoJson) => {
584 convert_shapefile_to_geojson(input, output, filter)
585 }
586 (VectorFormat::Shapefile, VectorFormat::Shapefile) => {
587 convert_shapefile_to_shapefile(input, output, filter)
588 }
589 (VectorFormat::GeoJson, VectorFormat::FlatGeobuf) => {
590 convert_geojson_to_fgb(input, output, filter)
591 }
592 (VectorFormat::Shapefile, VectorFormat::FlatGeobuf) => {
593 convert_shapefile_to_fgb(input, output, filter)
594 }
595 (VectorFormat::FlatGeobuf, VectorFormat::GeoJson) => {
596 convert_fgb_to_geojson(input, output, filter)
597 }
598 (VectorFormat::FlatGeobuf, VectorFormat::Shapefile) => {
599 convert_fgb_to_shapefile(input, output, filter)
600 }
601 (VectorFormat::FlatGeobuf, VectorFormat::FlatGeobuf) => {
602 convert_fgb_to_fgb(input, output, filter)
603 }
604 }
605}
606
607fn convert_geojson_to_geojson(
610 input: &Path,
611 output: &Path,
612 filter: Option<&AttributeFilter>,
613) -> Result<usize> {
614 use oxigeo_geojson::{FeatureCollection, GeoJsonReader, GeoJsonWriter};
615
616 let file =
617 File::open(input).with_context(|| format!("Failed to open input: {}", input.display()))?;
618 let buf_reader = BufReader::new(file);
619 let mut reader = GeoJsonReader::new(buf_reader);
620
621 let fc = reader
622 .read_feature_collection()
623 .context("Failed to read GeoJSON feature collection")?;
624
625 let features: Vec<_> = fc
626 .features
627 .into_iter()
628 .filter(|f| match filter {
629 None => true,
630 Some(filt) => {
631 let props = f
632 .properties
633 .as_ref()
634 .map_or(serde_json::Map::new(), |p| p.clone());
635 filt.matches_json(&props)
636 }
637 })
638 .collect();
639
640 let count = features.len();
641
642 let out_fc = FeatureCollection::new(features);
643
644 let out_file = File::create(output)
645 .with_context(|| format!("Failed to create output: {}", output.display()))?;
646 let buf_writer = BufWriter::new(out_file);
647 let mut writer = GeoJsonWriter::pretty(buf_writer);
648
649 writer
650 .write_feature_collection(&out_fc)
651 .context("Failed to write GeoJSON")?;
652
653 Ok(count)
654}
655
656fn convert_geojson_to_shapefile(
659 input: &Path,
660 output: &Path,
661 filter: Option<&AttributeFilter>,
662) -> Result<usize> {
663 use oxigeo_geojson::GeoJsonReader;
664 use oxigeo_shapefile::ShapefileWriter;
665
666 let file =
667 File::open(input).with_context(|| format!("Failed to open input: {}", input.display()))?;
668 let buf_reader = BufReader::new(file);
669 let mut reader = GeoJsonReader::new(buf_reader);
670
671 let fc = reader
672 .read_feature_collection()
673 .context("Failed to read GeoJSON feature collection")?;
674
675 let features: Vec<_> = fc
676 .features
677 .into_iter()
678 .filter(|f| match filter {
679 None => true,
680 Some(filt) => {
681 let props = f
682 .properties
683 .as_ref()
684 .map_or(serde_json::Map::new(), |p| p.clone());
685 filt.matches_json(&props)
686 }
687 })
688 .collect();
689
690 if features.is_empty() {
691 anyhow::bail!("No features remain after filtering; cannot write empty Shapefile");
692 }
693
694 let count = features.len();
695
696 let (shape_type, field_descriptors) = infer_shapefile_schema_from_geojson(&features)?;
698
699 let field_names: Vec<String> = field_descriptors.iter().map(|d| d.name.clone()).collect();
700
701 let base_path = output.with_extension("");
703
704 let mut writer = ShapefileWriter::new(&base_path, shape_type, field_descriptors)
705 .context("Failed to create Shapefile writer")?;
706
707 let sf_features: Vec<_> = features
709 .iter()
710 .enumerate()
711 .map(|(i, f)| geojson_feature_to_shapefile(f, (i + 1) as i32, &field_names))
712 .collect::<Result<Vec<_>>>()?;
713
714 writer
715 .write_features(&sf_features)
716 .context("Failed to write Shapefile")?;
717
718 Ok(count)
719}
720
721fn convert_shapefile_to_geojson(
724 input: &Path,
725 output: &Path,
726 filter: Option<&AttributeFilter>,
727) -> Result<usize> {
728 use oxigeo_geojson::{FeatureCollection, GeoJsonWriter};
729 use oxigeo_shapefile::ShapefileReader;
730
731 let base_path = input.with_extension("");
733
734 let reader = ShapefileReader::open(&base_path)
735 .with_context(|| format!("Failed to open Shapefile: {}", input.display()))?;
736
737 let sf_features = reader
738 .read_features()
739 .context("Failed to read Shapefile features")?;
740
741 let filtered: Vec<_> = sf_features
742 .into_iter()
743 .filter(|f| match filter {
744 None => true,
745 Some(filt) => filt.matches_field_map(&f.attributes),
746 })
747 .collect();
748
749 let count = filtered.len();
750
751 let gj_features = filtered
753 .iter()
754 .map(shapefile_feature_to_geojson)
755 .collect::<Result<Vec<_>>>()?;
756
757 let out_fc = FeatureCollection::new(gj_features);
758
759 let out_file = File::create(output)
760 .with_context(|| format!("Failed to create output: {}", output.display()))?;
761 let buf_writer = BufWriter::new(out_file);
762 let mut writer = GeoJsonWriter::pretty(buf_writer);
763
764 writer
765 .write_feature_collection(&out_fc)
766 .context("Failed to write GeoJSON")?;
767
768 Ok(count)
769}
770
771fn convert_shapefile_to_shapefile(
774 input: &Path,
775 output: &Path,
776 filter: Option<&AttributeFilter>,
777) -> Result<usize> {
778 use oxigeo_shapefile::{ShapefileReader, ShapefileWriter};
779
780 let base_in = input.with_extension("");
781 let reader = ShapefileReader::open(&base_in)
782 .with_context(|| format!("Failed to open Shapefile: {}", input.display()))?;
783
784 let sf_features = reader
785 .read_features()
786 .context("Failed to read Shapefile features")?;
787
788 let filtered: Vec<_> = sf_features
789 .into_iter()
790 .filter(|f| match filter {
791 None => true,
792 Some(filt) => filt.matches_field_map(&f.attributes),
793 })
794 .collect();
795
796 if filtered.is_empty() {
797 anyhow::bail!("No features remain after filtering; cannot write empty Shapefile");
798 }
799
800 let count = filtered.len();
801
802 let (shape_type, field_descriptors) = infer_shapefile_schema_from_shapefiles(&filtered)?;
803
804 let base_out = output.with_extension("");
805 let mut writer = ShapefileWriter::new(&base_out, shape_type, field_descriptors)
806 .context("Failed to create Shapefile writer")?;
807
808 writer
809 .write_features(&filtered)
810 .context("Failed to write Shapefile")?;
811
812 Ok(count)
813}
814
815fn convert_geojson_to_fgb(
818 input: &Path,
819 output: &Path,
820 filter: Option<&AttributeFilter>,
821) -> Result<usize> {
822 use oxigeo_flatgeobuf::{FlatGeobufWriterBuilder, GeometryType};
823 use oxigeo_geojson::GeoJsonReader;
824
825 let file =
826 File::open(input).with_context(|| format!("Failed to open input: {}", input.display()))?;
827 let buf_reader = BufReader::new(file);
828 let mut reader = GeoJsonReader::new(buf_reader);
829
830 let fc = reader
831 .read_feature_collection()
832 .context("Failed to read GeoJSON feature collection")?;
833
834 let features: Vec<_> = fc
836 .features
837 .into_iter()
838 .filter(|f| match filter {
839 None => true,
840 Some(filt) => {
841 let props = f
842 .properties
843 .as_ref()
844 .map_or(serde_json::Map::new(), |p| p.clone());
845 filt.matches_json(&props)
846 }
847 })
848 .collect();
849
850 let count = features.len();
851
852 let core_features: Vec<oxigeo_core::vector::Feature> = features
854 .iter()
855 .map(geojson_driver_feature_to_core)
856 .collect::<Result<Vec<_>>>()?;
857
858 let fgb_geom_type = core_features
860 .iter()
861 .find_map(|f| f.geometry.as_ref())
862 .map(core_geom_to_fgb_geometry_type)
863 .unwrap_or(GeometryType::Unknown);
864
865 let columns = infer_fgb_columns(&core_features);
867
868 let mut builder = FlatGeobufWriterBuilder::new(fgb_geom_type).with_index();
870 for col in &columns {
871 builder = builder.with_column(col.clone());
872 }
873
874 let out_file = File::create(output)
875 .with_context(|| format!("Failed to create output: {}", output.display()))?;
876 let buf_writer = BufWriter::new(out_file);
877 let mut writer = builder
878 .build(buf_writer)
879 .context("Failed to create FlatGeobuf writer")?;
880
881 for feature in &core_features {
882 writer
883 .add_feature(feature)
884 .context("Failed to add feature to FlatGeobuf writer")?;
885 }
886
887 writer
888 .finish()
889 .context("Failed to finalise FlatGeobuf file")?;
890
891 Ok(count)
892}
893
894fn convert_shapefile_to_fgb(
897 input: &Path,
898 output: &Path,
899 filter: Option<&AttributeFilter>,
900) -> Result<usize> {
901 use oxigeo_flatgeobuf::{FlatGeobufWriterBuilder, GeometryType};
902 use oxigeo_shapefile::ShapefileReader;
903
904 let base_path = input.with_extension("");
905 let reader = ShapefileReader::open(&base_path)
906 .with_context(|| format!("Failed to open Shapefile: {}", input.display()))?;
907
908 let sf_features = reader
909 .read_features()
910 .context("Failed to read Shapefile features")?;
911
912 let filtered: Vec<_> = sf_features
913 .into_iter()
914 .filter(|f| match filter {
915 None => true,
916 Some(filt) => filt.matches_field_map(&f.attributes),
917 })
918 .collect();
919
920 let count = filtered.len();
921
922 let core_features: Vec<oxigeo_core::vector::Feature> = filtered
924 .iter()
925 .map(shapefile_feature_to_core)
926 .collect::<Result<Vec<_>>>()?;
927
928 let fgb_geom_type = core_features
929 .iter()
930 .find_map(|f| f.geometry.as_ref())
931 .map(core_geom_to_fgb_geometry_type)
932 .unwrap_or(GeometryType::Unknown);
933
934 let columns = infer_fgb_columns(&core_features);
935
936 let mut builder = FlatGeobufWriterBuilder::new(fgb_geom_type).with_index();
937 for col in &columns {
938 builder = builder.with_column(col.clone());
939 }
940
941 let out_file = File::create(output)
942 .with_context(|| format!("Failed to create output: {}", output.display()))?;
943 let buf_writer = BufWriter::new(out_file);
944 let mut writer = builder
945 .build(buf_writer)
946 .context("Failed to create FlatGeobuf writer")?;
947
948 for feature in &core_features {
949 writer
950 .add_feature(feature)
951 .context("Failed to add feature to FlatGeobuf writer")?;
952 }
953
954 writer
955 .finish()
956 .context("Failed to finalise FlatGeobuf file")?;
957
958 Ok(count)
959}
960
961fn convert_fgb_to_geojson(
964 input: &Path,
965 output: &Path,
966 filter: Option<&AttributeFilter>,
967) -> Result<usize> {
968 use oxigeo_flatgeobuf::FlatGeobufReader;
969 use oxigeo_geojson::{Feature as GjFeature, FeatureCollection, GeoJsonWriter};
970
971 let file =
972 File::open(input).with_context(|| format!("Failed to open input: {}", input.display()))?;
973 let mut reader = FlatGeobufReader::new(file)
974 .with_context(|| format!("Failed to parse FlatGeobuf header: {}", input.display()))?;
975
976 let mut gj_features: Vec<GjFeature> = Vec::new();
977
978 let iter = reader
979 .features()
980 .context("Failed to get FlatGeobuf feature iterator")?;
981
982 for result in iter {
983 let core_feat = result.context("Failed to read FlatGeobuf feature")?;
984
985 if let Some(filt) = filter
987 && !filt.matches_field_map(&core_feat.properties)
988 {
989 continue;
990 }
991
992 let gj_feat = core_feature_to_geojson_driver_feature(&core_feat)?;
993 gj_features.push(gj_feat);
994 }
995
996 let count = gj_features.len();
997
998 let out_fc = FeatureCollection::new(gj_features);
999
1000 let out_file = File::create(output)
1001 .with_context(|| format!("Failed to create output: {}", output.display()))?;
1002 let buf_writer = BufWriter::new(out_file);
1003 let mut writer = GeoJsonWriter::pretty(buf_writer);
1004
1005 writer
1006 .write_feature_collection(&out_fc)
1007 .context("Failed to write GeoJSON")?;
1008
1009 Ok(count)
1010}
1011
1012fn convert_fgb_to_shapefile(
1015 input: &Path,
1016 output: &Path,
1017 filter: Option<&AttributeFilter>,
1018) -> Result<usize> {
1019 use oxigeo_flatgeobuf::FlatGeobufReader;
1020 use oxigeo_shapefile::{ShapefileWriter, reader::ShapefileFeature};
1021
1022 let file =
1023 File::open(input).with_context(|| format!("Failed to open input: {}", input.display()))?;
1024 let mut reader = FlatGeobufReader::new(file)
1025 .with_context(|| format!("Failed to parse FlatGeobuf header: {}", input.display()))?;
1026
1027 let mut core_features: Vec<oxigeo_core::vector::Feature> = Vec::new();
1028
1029 let iter = reader
1030 .features()
1031 .context("Failed to get FlatGeobuf feature iterator")?;
1032
1033 for result in iter {
1034 let core_feat = result.context("Failed to read FlatGeobuf feature")?;
1035 if let Some(filt) = filter
1036 && !filt.matches_field_map(&core_feat.properties)
1037 {
1038 continue;
1039 }
1040 core_features.push(core_feat);
1041 }
1042
1043 if core_features.is_empty() {
1044 anyhow::bail!("No features remain after filtering; cannot write empty Shapefile");
1045 }
1046
1047 let count = core_features.len();
1048
1049 let sf_features: Vec<ShapefileFeature> = core_features
1051 .iter()
1052 .enumerate()
1053 .map(|(i, f)| {
1054 let attrs = f.properties.clone();
1055 ShapefileFeature::new((i + 1) as i32, f.geometry.clone(), attrs)
1056 })
1057 .collect();
1058
1059 let (shape_type, field_descriptors) = infer_shapefile_schema_from_shapefiles(&sf_features)?;
1060
1061 let base_out = output.with_extension("");
1062 let mut writer = ShapefileWriter::new(&base_out, shape_type, field_descriptors)
1063 .context("Failed to create Shapefile writer")?;
1064
1065 writer
1066 .write_features(&sf_features)
1067 .context("Failed to write Shapefile")?;
1068
1069 Ok(count)
1070}
1071
1072fn convert_fgb_to_fgb(
1075 input: &Path,
1076 output: &Path,
1077 filter: Option<&AttributeFilter>,
1078) -> Result<usize> {
1079 use oxigeo_flatgeobuf::{FlatGeobufReader, FlatGeobufWriterBuilder};
1080
1081 let file =
1082 File::open(input).with_context(|| format!("Failed to open input: {}", input.display()))?;
1083 let mut reader = FlatGeobufReader::new(file)
1084 .with_context(|| format!("Failed to parse FlatGeobuf header: {}", input.display()))?;
1085
1086 let source_header = reader.header().clone();
1087
1088 let mut core_features: Vec<oxigeo_core::vector::Feature> = Vec::new();
1089
1090 let iter = reader
1091 .features()
1092 .context("Failed to get FlatGeobuf feature iterator")?;
1093
1094 for result in iter {
1095 let core_feat = result.context("Failed to read FlatGeobuf feature")?;
1096 if let Some(filt) = filter
1097 && !filt.matches_field_map(&core_feat.properties)
1098 {
1099 continue;
1100 }
1101 core_features.push(core_feat);
1102 }
1103
1104 let count = core_features.len();
1105
1106 let mut builder = FlatGeobufWriterBuilder::new(source_header.geometry_type).with_index();
1107 for col in &source_header.columns {
1108 builder = builder.with_column(col.clone());
1109 }
1110
1111 let out_file = File::create(output)
1112 .with_context(|| format!("Failed to create output: {}", output.display()))?;
1113 let buf_writer = BufWriter::new(out_file);
1114 let mut writer = builder
1115 .build(buf_writer)
1116 .context("Failed to create FlatGeobuf writer")?;
1117
1118 for feature in &core_features {
1119 writer
1120 .add_feature(feature)
1121 .context("Failed to add feature to FlatGeobuf writer")?;
1122 }
1123
1124 writer
1125 .finish()
1126 .context("Failed to finalise FlatGeobuf file")?;
1127
1128 Ok(count)
1129}
1130
1131fn core_geom_to_fgb_geometry_type(
1135 geom: &oxigeo_core::vector::Geometry,
1136) -> oxigeo_flatgeobuf::GeometryType {
1137 use oxigeo_core::vector::Geometry as CoreGeom;
1138 use oxigeo_flatgeobuf::GeometryType as FgbGT;
1139 match geom {
1140 CoreGeom::Point(_) => FgbGT::Point,
1141 CoreGeom::LineString(_) => FgbGT::LineString,
1142 CoreGeom::Polygon(_) => FgbGT::Polygon,
1143 CoreGeom::MultiPoint(_) => FgbGT::MultiPoint,
1144 CoreGeom::MultiLineString(_) => FgbGT::MultiLineString,
1145 CoreGeom::MultiPolygon(_) => FgbGT::MultiPolygon,
1146 CoreGeom::GeometryCollection(_) => FgbGT::GeometryCollection,
1147 }
1148}
1149
1150fn infer_fgb_columns(features: &[oxigeo_core::vector::Feature]) -> Vec<oxigeo_flatgeobuf::Column> {
1156 use oxigeo_core::vector::FieldValue;
1157 use oxigeo_flatgeobuf::{Column, ColumnType};
1158
1159 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1161 let mut ordered: Vec<String> = Vec::new();
1162
1163 for feat in features {
1164 for key in feat.properties.keys() {
1165 if seen.insert(key.clone()) {
1166 ordered.push(key.clone());
1167 }
1168 }
1169 }
1170
1171 let mut col_types: std::collections::HashMap<String, ColumnType> =
1173 std::collections::HashMap::new();
1174
1175 for feat in features {
1176 for (key, val) in &feat.properties {
1177 if col_types.contains_key(key) {
1178 continue;
1179 }
1180 let ct = match val {
1181 FieldValue::Null => continue,
1182 FieldValue::Bool(_) => ColumnType::Bool,
1183 FieldValue::Integer(_) => ColumnType::Long,
1184 FieldValue::UInteger(_) => ColumnType::ULong,
1185 FieldValue::Float(_) => ColumnType::Double,
1186 FieldValue::String(_) | FieldValue::Array(_) | FieldValue::Object(_) => {
1187 ColumnType::String
1188 }
1189 FieldValue::Date(_) => ColumnType::DateTime,
1190 FieldValue::Blob(_) => ColumnType::Binary,
1191 };
1192 col_types.insert(key.clone(), ct);
1193 }
1194 }
1195
1196 ordered
1197 .into_iter()
1198 .map(|name| {
1199 let ct = col_types.get(&name).copied().unwrap_or(ColumnType::String);
1200 Column::new(name, ct)
1201 })
1202 .collect()
1203}
1204
1205fn geojson_driver_feature_to_core(
1207 f: &oxigeo_geojson::Feature,
1208) -> Result<oxigeo_core::vector::Feature> {
1209 use oxigeo_core::vector::{Feature as CoreFeature, FieldValue};
1210
1211 let geometry = match &f.geometry {
1212 Some(gj_geom) => Some(geojson_geom_to_core(gj_geom)?),
1213 None => None,
1214 };
1215
1216 let mut core_feat = match geometry {
1217 Some(g) => CoreFeature::new(g),
1218 None => CoreFeature::new_attribute_only(),
1219 };
1220
1221 if let Some(props) = &f.properties {
1222 for (k, v) in props {
1223 core_feat.set_property(k.clone(), FieldValue::from_json(v));
1224 }
1225 }
1226
1227 Ok(core_feat)
1228}
1229
1230fn core_feature_to_geojson_driver_feature(
1232 core_feat: &oxigeo_core::vector::Feature,
1233) -> Result<oxigeo_geojson::Feature> {
1234 use oxigeo_geojson::Feature as GjFeature;
1235
1236 let geometry = match &core_feat.geometry {
1237 Some(g) => Some(core_geom_to_geojson(g)?),
1238 None => None,
1239 };
1240
1241 let properties: oxigeo_geojson::types::Properties = core_feat
1242 .properties
1243 .iter()
1244 .map(|(k, v)| (k.clone(), v.to_json_value()))
1245 .collect();
1246
1247 let props_opt = if properties.is_empty() {
1248 None
1249 } else {
1250 Some(properties)
1251 };
1252
1253 Ok(GjFeature::new(geometry, props_opt))
1254}
1255
1256fn shapefile_feature_to_core(
1258 sf: &oxigeo_shapefile::reader::ShapefileFeature,
1259) -> Result<oxigeo_core::vector::Feature> {
1260 use oxigeo_core::vector::Feature as CoreFeature;
1261
1262 let mut core_feat = match &sf.geometry {
1263 Some(g) => CoreFeature::new(g.clone()),
1264 None => CoreFeature::new_attribute_only(),
1265 };
1266
1267 for (k, v) in &sf.attributes {
1268 core_feat.set_property(k.clone(), v.clone());
1269 }
1270
1271 Ok(core_feat)
1272}