1use crate::errors::ProjectionError;
4use crate::models::{GnssPosition, NetRelation, Netelement, ProjectedPosition};
5use chrono::DateTime;
6use geo::{Coord, LineString};
7use geojson::{Feature, GeoJson, Value};
8use std::fs;
9
10const DEFAULT_CRS: &str = "EPSG:4326";
12
13fn parse_crs_from_feature_collection(feature_collection: &geojson::FeatureCollection) -> String {
17 let Some(crs_obj) = &feature_collection.foreign_members else {
18 return DEFAULT_CRS.to_string();
19 };
20
21 let Some(crs_value) = crs_obj.get("crs") else {
22 return DEFAULT_CRS.to_string();
23 };
24
25 crs_value
27 .get("properties")
28 .and_then(|props| props.get("name"))
29 .and_then(|name| name.as_str())
30 .and_then(|name_str| {
31 if name_str.contains("EPSG") {
33 name_str
34 .split("::")
35 .last()
36 .map(|code| format!("EPSG:{}", code))
37 } else {
38 None
39 }
40 })
41 .unwrap_or_else(|| DEFAULT_CRS.to_string())
42}
43
44pub fn parse_network_geojson(
67 path: &str,
68) -> Result<(Vec<Netelement>, Vec<NetRelation>), ProjectionError> {
69 let geojson_str = fs::read_to_string(path)?;
71
72 let geojson = geojson_str
74 .parse::<GeoJson>()
75 .map_err(|e| ProjectionError::InvalidGeometry(format!("Failed to parse GeoJSON: {}", e)))?;
76
77 let feature_collection = match geojson {
79 GeoJson::FeatureCollection(fc) => fc,
80 _ => {
81 return Err(ProjectionError::InvalidGeometry(
82 "GeoJSON must be a FeatureCollection".to_string(),
83 ))
84 }
85 };
86
87 let crs = parse_crs_from_feature_collection(&feature_collection);
89
90 if !crs.contains("4326") && !crs.contains("WGS84") {
92 return Err(ProjectionError::InvalidCrs(format!(
93 "GeoJSON CRS must be WGS84 (EPSG:4326) per RFC 7946, got: {}",
94 crs
95 )));
96 }
97
98 let mut netelements = Vec::new();
100 let mut netrelations = Vec::new();
101
102 for (idx, feature) in feature_collection.features.iter().enumerate() {
103 if let Some(props) = &feature.properties {
105 if let Some(feature_type) = props.get("type") {
106 if feature_type.as_str() == Some("netrelation") {
107 let netrelation = parse_netrelation_feature(feature, idx)?;
108 netrelations.push(netrelation);
109 continue;
110 }
111 }
112 }
113
114 let netelement = parse_feature(feature, &crs, idx)?;
116 netelements.push(netelement);
117 }
118
119 if netelements.is_empty() {
120 return Err(ProjectionError::EmptyNetwork);
121 }
122
123 Ok((netelements, netrelations))
124}
125
126pub fn parse_gnss_geojson(path: &str, crs: &str) -> Result<Vec<GnssPosition>, ProjectionError> {
162 let geojson_str = fs::read_to_string(path)?;
164
165 let geojson = geojson_str
167 .parse::<GeoJson>()
168 .map_err(|e| ProjectionError::GeoJsonError(format!("Failed to parse GeoJSON: {}", e)))?;
169
170 let feature_collection = match geojson {
172 GeoJson::FeatureCollection(fc) => fc,
173 _ => {
174 return Err(ProjectionError::GeoJsonError(
175 "GeoJSON must be a FeatureCollection".to_string(),
176 ))
177 }
178 };
179
180 let mut positions = Vec::new();
182
183 for (idx, feature) in feature_collection.features.iter().enumerate() {
184 let position = parse_gnss_feature(feature, crs, idx)?;
185 positions.push(position);
186 }
187
188 if positions.is_empty() {
189 return Err(ProjectionError::GeoJsonError(
190 "GeoJSON contains no valid GNSS positions".to_string(),
191 ));
192 }
193
194 Ok(positions)
195}
196
197fn parse_gnss_feature(
199 feature: &Feature,
200 crs: &str,
201 idx: usize,
202) -> Result<GnssPosition, ProjectionError> {
203 let geometry = feature.geometry.as_ref().ok_or_else(|| {
205 ProjectionError::GeoJsonError(format!("Feature {} missing geometry", idx))
206 })?;
207
208 let (longitude, latitude) = match &geometry.value {
210 Value::Point(coords) => {
211 if coords.len() < 2 {
212 return Err(ProjectionError::InvalidCoordinate(format!(
213 "Feature {} Point must have at least 2 coordinates",
214 idx
215 )));
216 }
217 (coords[0], coords[1])
218 }
219 _ => {
220 return Err(ProjectionError::GeoJsonError(format!(
221 "Feature {} must have Point geometry for GNSS position",
222 idx
223 )))
224 }
225 };
226
227 if !(-90.0..=90.0).contains(&latitude) {
229 return Err(ProjectionError::InvalidCoordinate(format!(
230 "Feature {}: latitude {} out of range [-90, 90]",
231 idx, latitude
232 )));
233 }
234 if !(-180.0..=180.0).contains(&longitude) {
235 return Err(ProjectionError::InvalidCoordinate(format!(
236 "Feature {}: longitude {} out of range [-180, 180]",
237 idx, longitude
238 )));
239 }
240
241 let properties = feature.properties.as_ref().ok_or_else(|| {
243 ProjectionError::GeoJsonError(format!(
244 "Feature {} missing properties (timestamp required)",
245 idx
246 ))
247 })?;
248
249 let timestamp_str = properties
251 .get("timestamp")
252 .and_then(|v| v.as_str())
253 .ok_or_else(|| {
254 ProjectionError::MissingTimezone(format!(
255 "Feature {} missing 'timestamp' property",
256 idx
257 ))
258 })?;
259
260 let timestamp = DateTime::parse_from_rfc3339(timestamp_str).map_err(|e| {
262 ProjectionError::InvalidTimestamp(format!(
263 "Feature {}: invalid timestamp '{}': {}",
264 idx, timestamp_str, e
265 ))
266 })?;
267
268 let mut metadata = std::collections::HashMap::new();
270 let mut heading: Option<f64> = None;
271 let mut distance: Option<f64> = None;
272
273 for (key, value) in properties {
274 match key.as_str() {
275 "timestamp" => {} "heading" => {
277 if let Some(h) = value.as_f64() {
279 if (0.0..=360.0).contains(&h) {
280 heading = Some(h);
281 } else {
282 return Err(ProjectionError::InvalidGeometry(format!(
283 "Feature {}: heading {} not in [0, 360]",
284 idx, h
285 )));
286 }
287 }
288 }
289 "distance" => {
290 if let Some(d) = value.as_f64() {
292 if d >= 0.0 {
293 distance = Some(d);
294 } else {
295 return Err(ProjectionError::InvalidGeometry(format!(
296 "Feature {}: distance {} must be >= 0",
297 idx, d
298 )));
299 }
300 }
301 }
302 _ => {
303 if let Some(str_value) = value.as_str() {
305 metadata.insert(key.clone(), str_value.to_string());
306 } else {
307 metadata.insert(key.clone(), value.to_string());
308 }
309 }
310 }
311 }
312
313 Ok(GnssPosition {
314 latitude,
315 longitude,
316 timestamp,
317 crs: crs.to_string(),
318 metadata,
319 heading,
320 distance,
321 })
322}
323
324fn parse_feature(feature: &Feature, crs: &str, idx: usize) -> Result<Netelement, ProjectionError> {
326 let geometry = feature.geometry.as_ref().ok_or_else(|| {
328 ProjectionError::InvalidGeometry(format!("Feature {} missing geometry", idx))
329 })?;
330
331 let linestring = match &geometry.value {
333 Value::LineString(coords) => {
334 let geo_coords: Vec<Coord<f64>> = coords
335 .iter()
336 .map(|pos| Coord {
337 x: pos[0],
338 y: pos[1],
339 })
340 .collect();
341 LineString::from(geo_coords)
342 }
343 Value::MultiLineString(lines) => {
344 if lines.is_empty() {
346 return Err(ProjectionError::InvalidGeometry(format!(
347 "Feature {} has empty MultiLineString",
348 idx
349 )));
350 }
351 let geo_coords: Vec<Coord<f64>> = lines[0]
352 .iter()
353 .map(|pos| Coord {
354 x: pos[0],
355 y: pos[1],
356 })
357 .collect();
358 LineString::from(geo_coords)
359 }
360 _ => {
361 return Err(ProjectionError::InvalidGeometry(format!(
362 "Feature {} must have LineString or MultiLineString geometry",
363 idx
364 )))
365 }
366 };
367
368 let id = if let Some(props) = &feature.properties {
370 if let Some(id_value) = props.get("id") {
371 id_value
372 .as_str()
373 .map(|s| s.to_string())
374 .or_else(|| id_value.as_i64().map(|i| i.to_string()))
375 .ok_or_else(|| {
376 ProjectionError::GeoJsonError(format!(
377 "Feature {} has invalid 'id' property type",
378 idx
379 ))
380 })?
381 } else {
382 return Err(ProjectionError::GeoJsonError(format!(
383 "Feature {} missing required 'id' property",
384 idx
385 )));
386 }
387 } else {
388 return Err(ProjectionError::GeoJsonError(format!(
389 "Feature {} missing properties object",
390 idx
391 )));
392 };
393
394 Netelement::new(id, linestring, crs.to_string())
395}
396
397pub fn parse_netrelations_geojson(path: &str) -> Result<Vec<NetRelation>, ProjectionError> {
446 let geojson_str = fs::read_to_string(path)?;
448
449 let geojson = geojson_str
451 .parse::<GeoJson>()
452 .map_err(|e| ProjectionError::GeoJsonError(format!("Failed to parse GeoJSON: {}", e)))?;
453
454 let feature_collection = match geojson {
456 GeoJson::FeatureCollection(fc) => fc,
457 _ => {
458 return Err(ProjectionError::GeoJsonError(
459 "GeoJSON must be a FeatureCollection".to_string(),
460 ))
461 }
462 };
463
464 let mut netrelations = Vec::new();
466
467 for (idx, feature) in feature_collection.features.iter().enumerate() {
468 if let Some(props) = &feature.properties {
470 if let Some(feature_type) = props.get("type") {
471 if feature_type.as_str() == Some("netrelation") {
472 let netrelation = parse_netrelation_feature(feature, idx)?;
473 netrelations.push(netrelation);
474 }
475 }
476 }
477 }
478
479 Ok(netrelations)
480}
481
482fn parse_netrelation_feature(
484 feature: &Feature,
485 idx: usize,
486) -> Result<NetRelation, ProjectionError> {
487 let properties = feature.properties.as_ref().ok_or_else(|| {
489 ProjectionError::GeoJsonError(format!("Netrelation feature {} missing properties", idx))
490 })?;
491
492 let id = properties
494 .get("id")
495 .and_then(|v| v.as_str())
496 .ok_or_else(|| {
497 ProjectionError::GeoJsonError(format!(
498 "Netrelation feature {} missing 'id' property",
499 idx
500 ))
501 })?
502 .to_string();
503
504 let netelement_a = properties
506 .get("netelementA")
507 .or_else(|| properties.get("from"))
508 .and_then(|v| v.as_str())
509 .ok_or_else(|| {
510 ProjectionError::GeoJsonError(format!(
511 "Netrelation feature {} missing 'netelementA' or 'from' property",
512 idx
513 ))
514 })?
515 .to_string();
516
517 let netelement_b = properties
519 .get("netelementB")
520 .or_else(|| properties.get("to"))
521 .and_then(|v| v.as_str())
522 .ok_or_else(|| {
523 ProjectionError::GeoJsonError(format!(
524 "Netrelation feature {} missing 'netelementB' or 'to' property",
525 idx
526 ))
527 })?
528 .to_string();
529
530 let position_on_a = properties
532 .get("positionOnA")
533 .and_then(|v| v.as_u64().or_else(|| v.as_f64().map(|f| f as u64)))
534 .ok_or_else(|| {
535 ProjectionError::GeoJsonError(format!(
536 "Netrelation feature {} missing or invalid 'positionOnA' property",
537 idx
538 ))
539 })? as u8;
540
541 let position_on_b = properties
543 .get("positionOnB")
544 .and_then(|v| v.as_u64().or_else(|| v.as_f64().map(|f| f as u64)))
545 .ok_or_else(|| {
546 ProjectionError::GeoJsonError(format!(
547 "Netrelation feature {} missing or invalid 'positionOnB' property",
548 idx
549 ))
550 })? as u8;
551
552 let navigability_str = properties
554 .get("navigability")
555 .and_then(|v| v.as_str())
556 .ok_or_else(|| {
557 ProjectionError::GeoJsonError(format!(
558 "Netrelation feature {} missing 'navigability' property",
559 idx
560 ))
561 })?;
562
563 let (navigable_forward, navigable_backward) = match navigability_str.to_lowercase().as_str() {
564 "both" => (true, true),
565 "ab" => (true, false),
566 "ba" => (false, true),
567 "none" => (false, false),
568 _ => return Err(ProjectionError::GeoJsonError(
569 format!("Netrelation feature {}: invalid navigability value '{}' (expected: both, AB, BA, or none)", idx, navigability_str)
570 )),
571 };
572
573 let netrelation = NetRelation::new(
575 id,
576 netelement_a,
577 netelement_b,
578 position_on_a,
579 position_on_b,
580 navigable_forward,
581 navigable_backward,
582 )?;
583
584 Ok(netrelation)
585}
586
587pub fn write_geojson(
589 positions: &[ProjectedPosition],
590 writer: &mut impl std::io::Write,
591) -> Result<(), ProjectionError> {
592 use geojson::{Feature, FeatureCollection, Geometry, Value};
593 use serde_json::{Map, Value as JsonValue};
594
595 let mut features = Vec::new();
596
597 for pos in positions {
598 let geometry = Geometry::new(Value::Point(vec![
600 pos.projected_coords.x(),
601 pos.projected_coords.y(),
602 ]));
603
604 let mut properties = Map::new();
606 properties.insert(
607 "original_lat".to_string(),
608 JsonValue::from(pos.original.latitude),
609 );
610 properties.insert(
611 "original_lon".to_string(),
612 JsonValue::from(pos.original.longitude),
613 );
614 properties.insert(
615 "original_time".to_string(),
616 JsonValue::from(pos.original.timestamp.to_rfc3339()),
617 );
618 properties.insert(
619 "netelement_id".to_string(),
620 JsonValue::from(pos.netelement_id.clone()),
621 );
622 properties.insert(
623 "measure_meters".to_string(),
624 JsonValue::from(pos.measure_meters),
625 );
626 properties.insert(
627 "projection_distance_meters".to_string(),
628 JsonValue::from(pos.projection_distance_meters),
629 );
630 properties.insert("crs".to_string(), JsonValue::from(pos.crs.clone()));
631
632 for (key, value) in &pos.original.metadata {
634 properties.insert(format!("original_{}", key), JsonValue::from(value.clone()));
635 }
636
637 let feature = Feature {
638 bbox: None,
639 geometry: Some(geometry),
640 id: None,
641 properties: Some(properties),
642 foreign_members: None,
643 };
644
645 features.push(feature);
646 }
647
648 let feature_collection = FeatureCollection {
649 bbox: None,
650 features,
651 foreign_members: None,
652 };
653
654 let json = serde_json::to_string_pretty(&feature_collection).map_err(|e| {
655 ProjectionError::GeoJsonError(format!("Failed to serialize GeoJSON: {}", e))
656 })?;
657
658 writer.write_all(json.as_bytes())?;
659 Ok(())
660}
661
662pub fn parse_trainpath_geojson(path: &str) -> Result<crate::models::TrainPath, ProjectionError> {
675 use crate::models::AssociatedNetElement;
676
677 let geojson_str = fs::read_to_string(path)?;
678 let geojson = geojson_str.parse::<GeoJson>().map_err(|e| {
679 ProjectionError::GeoJsonError(format!("Failed to parse TrainPath GeoJSON: {}", e))
680 })?;
681
682 let fc = match geojson {
683 GeoJson::FeatureCollection(fc) => fc,
684 _ => {
685 return Err(ProjectionError::GeoJsonError(
686 "TrainPath GeoJSON must be a FeatureCollection".to_string(),
687 ))
688 }
689 };
690
691 let (overall_probability, calculated_at) = fc
694 .foreign_members
695 .as_ref()
696 .and_then(|fm| fm.get("properties"))
697 .and_then(|v| v.as_object())
698 .map(|props| {
699 let prob = props.get("overall_probability").and_then(|v| v.as_f64());
700 let calc_at = props
701 .get("calculated_at")
702 .and_then(|v| v.as_str())
703 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
704 .map(|dt| dt.with_timezone(&chrono::Utc));
705 (prob, calc_at)
706 })
707 .unwrap_or((None, None));
708
709 let mut segments = Vec::new();
710
711 for (idx, feature) in fc.features.iter().enumerate() {
712 let props = feature.properties.as_ref().ok_or_else(|| {
713 ProjectionError::GeoJsonError(format!("TrainPath feature {} missing properties", idx))
714 })?;
715
716 macro_rules! get_str {
717 ($key:expr) => {
718 props
719 .get($key)
720 .and_then(|v| v.as_str())
721 .map(|s| s.to_string())
722 .ok_or_else(|| {
723 ProjectionError::GeoJsonError(format!(
724 "TrainPath feature {} missing or invalid '{}' property",
725 idx, $key
726 ))
727 })
728 };
729 }
730 macro_rules! get_f64 {
731 ($key:expr) => {
732 props.get($key).and_then(|v| v.as_f64()).ok_or_else(|| {
733 ProjectionError::GeoJsonError(format!(
734 "TrainPath feature {} missing or invalid '{}' property",
735 idx, $key
736 ))
737 })
738 };
739 }
740 macro_rules! get_usize {
741 ($key:expr) => {
742 props
743 .get($key)
744 .and_then(|v| v.as_u64())
745 .map(|v| v as usize)
746 .ok_or_else(|| {
747 ProjectionError::GeoJsonError(format!(
748 "TrainPath feature {} missing or invalid '{}' property",
749 idx, $key
750 ))
751 })
752 };
753 }
754
755 let segment = AssociatedNetElement::new(
756 get_str!("netelement_id")?,
757 get_f64!("probability")?,
758 get_f64!("start_intrinsic")?,
759 get_f64!("end_intrinsic")?,
760 get_usize!("gnss_start_index")?,
761 get_usize!("gnss_end_index")?,
762 )?;
763
764 segments.push(segment);
765 }
766
767 let overall_prob = overall_probability.unwrap_or_else(|| {
768 if segments.is_empty() {
769 1.0
770 } else {
771 let sum: f64 = segments.iter().map(|s| s.probability).sum();
772 sum / segments.len() as f64
773 }
774 });
775
776 crate::models::TrainPath::new(segments, overall_prob, calculated_at, None)
777}
778
779pub fn write_trainpath_geojson(
818 train_path: &crate::models::TrainPath,
819 netelements: &std::collections::HashMap<String, Netelement>,
820 writer: &mut impl std::io::Write,
821) -> Result<(), ProjectionError> {
822 use geojson::{Feature, FeatureCollection, Geometry, Value};
823 use serde_json::{Map, Value as JsonValue};
824
825 let mut features = Vec::new();
826
827 for segment in &train_path.segments {
829 let netelement = netelements.get(&segment.netelement_id).ok_or_else(|| {
831 ProjectionError::InvalidGeometry(format!(
832 "Netelement {} not found in provided map",
833 segment.netelement_id
834 ))
835 })?;
836
837 let coords: Vec<Vec<f64>> = netelement
841 .geometry
842 .points()
843 .map(|point| vec![point.x(), point.y()])
844 .collect();
845
846 let geometry = Geometry::new(Value::LineString(coords));
847
848 let mut properties = Map::new();
850 properties.insert(
851 "netelement_id".to_string(),
852 JsonValue::from(segment.netelement_id.clone()),
853 );
854 properties.insert(
855 "probability".to_string(),
856 JsonValue::from(segment.probability),
857 );
858 properties.insert(
859 "start_intrinsic".to_string(),
860 JsonValue::from(segment.start_intrinsic),
861 );
862 properties.insert(
863 "end_intrinsic".to_string(),
864 JsonValue::from(segment.end_intrinsic),
865 );
866 properties.insert(
867 "gnss_start_index".to_string(),
868 JsonValue::from(segment.gnss_start_index as i64),
869 );
870 properties.insert(
871 "gnss_end_index".to_string(),
872 JsonValue::from(segment.gnss_end_index as i64),
873 );
874
875 let feature = Feature {
876 bbox: None,
877 geometry: Some(geometry),
878 id: None,
879 properties: Some(properties),
880 foreign_members: None,
881 };
882
883 features.push(feature);
884 }
885
886 let mut fc_properties = Map::new();
888 fc_properties.insert(
889 "overall_probability".to_string(),
890 JsonValue::from(train_path.overall_probability),
891 );
892
893 if let Some(calculated_at) = &train_path.calculated_at {
894 fc_properties.insert(
895 "calculated_at".to_string(),
896 JsonValue::from(calculated_at.to_rfc3339()),
897 );
898 }
899
900 if let Some(metadata) = &train_path.metadata {
902 fc_properties.insert(
903 "distance_scale".to_string(),
904 JsonValue::from(metadata.distance_scale),
905 );
906 fc_properties.insert(
907 "heading_scale".to_string(),
908 JsonValue::from(metadata.heading_scale),
909 );
910 fc_properties.insert(
911 "cutoff_distance".to_string(),
912 JsonValue::from(metadata.cutoff_distance),
913 );
914 fc_properties.insert(
915 "heading_cutoff".to_string(),
916 JsonValue::from(metadata.heading_cutoff),
917 );
918 fc_properties.insert(
919 "probability_threshold".to_string(),
920 JsonValue::from(metadata.probability_threshold),
921 );
922 if let Some(resampling_dist) = metadata.resampling_distance {
923 fc_properties.insert(
924 "resampling_distance".to_string(),
925 JsonValue::from(resampling_dist),
926 );
927 }
928 fc_properties.insert(
929 "fallback_mode".to_string(),
930 JsonValue::from(metadata.fallback_mode),
931 );
932 fc_properties.insert(
933 "candidate_paths_evaluated".to_string(),
934 JsonValue::from(metadata.candidate_paths_evaluated as i64),
935 );
936 fc_properties.insert(
937 "bidirectional_path".to_string(),
938 JsonValue::from(metadata.bidirectional_path),
939 );
940 }
941
942 let mut foreign_members = Map::new();
943 foreign_members.insert("properties".to_string(), JsonValue::Object(fc_properties));
944
945 let feature_collection = FeatureCollection {
946 bbox: None,
947 features,
948 foreign_members: Some(foreign_members),
949 };
950
951 let json = serde_json::to_string_pretty(&feature_collection).map_err(|e| {
952 ProjectionError::GeoJsonError(format!("Failed to serialize TrainPath GeoJSON: {}", e))
953 })?;
954
955 writer.write_all(json.as_bytes())?;
956 Ok(())
957}
958
959#[cfg(test)]
960mod tests;