Skip to main content

howler_core/
export.rs

1use crate::models::Sighting;
2use anyhow::Result;
3use std::fs::File;
4use std::io::Write;
5
6/// Export format
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum ExportFormat {
9    Csv,
10    GeoJson,
11    Kml,
12}
13
14/// Export sightings to specified format
15pub fn export_sightings(
16    sightings: &[Sighting],
17    format: ExportFormat,
18    output_path: &str,
19) -> Result<()> {
20    match format {
21        ExportFormat::Csv => export_csv(sightings, output_path),
22        ExportFormat::GeoJson => export_geojson(sightings, output_path),
23        ExportFormat::Kml => export_kml(sightings, output_path),
24    }
25}
26
27/// Export sightings to CSV format
28pub fn export_csv(sightings: &[Sighting], output_path: &str) -> Result<()> {
29    let mut file = File::create(output_path)?;
30
31    // Write header
32    writeln!(
33        file,
34        "id,species,scientific_name,latitude,longitude,observed_on,source,source_id,details"
35    )?;
36
37    // Write data rows
38    for sighting in sightings {
39        let id = sighting.id.map(|i| i.to_string()).unwrap_or_default();
40        let scientific_name = sighting.scientific_name.as_deref().unwrap_or("");
41        let observed_on = sighting.observed_on.to_rfc3339();
42        let details = sighting.details.as_deref().unwrap_or("");
43
44        writeln!(
45            file,
46            "{},{},{},{},{},{},{},{},{}",
47            id,
48            sighting.species,
49            scientific_name,
50            sighting.latitude,
51            sighting.longitude,
52            observed_on,
53            sighting.source,
54            sighting.source_id,
55            details
56        )?;
57    }
58
59    Ok(())
60}
61
62/// Export sightings to GeoJSON format
63pub fn export_geojson(sightings: &[Sighting], output_path: &str) -> Result<()> {
64    use geojson::{Feature, FeatureCollection, Geometry, Value};
65
66    let mut features = Vec::new();
67
68    for sighting in sightings {
69        let coords = vec![sighting.longitude, sighting.latitude];
70        let geometry = Geometry::new(Value::Point(coords));
71
72        let mut props = serde_json::Map::new();
73        props.insert("id".to_string(), serde_json::json!(sighting.id));
74        props.insert("species".to_string(), serde_json::json!(sighting.species));
75        props.insert(
76            "scientific_name".to_string(),
77            serde_json::json!(sighting.scientific_name),
78        );
79        props.insert(
80            "observed_on".to_string(),
81            serde_json::json!(sighting.observed_on.to_rfc3339()),
82        );
83        props.insert(
84            "source".to_string(),
85            serde_json::json!(sighting.source.to_string()),
86        );
87        props.insert(
88            "source_id".to_string(),
89            serde_json::json!(sighting.source_id),
90        );
91        props.insert("details".to_string(), serde_json::json!(sighting.details));
92
93        let feature = Feature {
94            bbox: None,
95            geometry: Some(geometry),
96            id: None,
97            properties: Some(props),
98            foreign_members: None,
99        };
100
101        features.push(feature);
102    }
103
104    let feature_collection = FeatureCollection {
105        bbox: None,
106        features,
107        foreign_members: None,
108    };
109
110    let json_string = serde_json::to_string_pretty(&feature_collection)?;
111    std::fs::write(output_path, json_string)?;
112
113    Ok(())
114}
115
116/// Export sightings to KML format (Google Earth)
117pub fn export_kml(sightings: &[Sighting], output_path: &str) -> Result<()> {
118    let mut file = File::create(output_path)?;
119
120    writeln!(file, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
121    writeln!(file, r#"<kml xmlns="http://www.opengis.net/kml/2.2">"#)?;
122    writeln!(file, r#"  <Document>"#)?;
123    writeln!(file, r#"    <name>Wolf Sightings</name>"#)?;
124    writeln!(
125        file,
126        r#"    <description>Exported from Howler</description>"#
127    )?;
128
129    for sighting in sightings {
130        writeln!(file, r#"    <Placemark>"#)?;
131        writeln!(file, r#"      <name>{}</name>"#, sighting.source_id)?;
132        writeln!(file, r#"      <description>"#)?;
133        writeln!(file, r#"        <![CDATA["#)?;
134        let _ = writeln!(
135            file,
136            r#"          <strong>Species:</strong> {}<br/>"#,
137            sighting.species
138        );
139        if let Some(scientific_name) = &sighting.scientific_name {
140            let _ = writeln!(
141                file,
142                r#"          <strong>Scientific Name:</strong> {}<br/>"#,
143                scientific_name
144            );
145        }
146        let _ = writeln!(
147            file,
148            r#"          <strong>Source:</strong> {}<br/>"#,
149            sighting.source
150        );
151        let _ = writeln!(
152            file,
153            r#"          <strong>Date:</strong> {}<br/>"#,
154            sighting.observed_on.format("%Y-%m-%d %H:%M:%S")
155        );
156        if let Some(details) = &sighting.details {
157            let _ = writeln!(file, r#"          <strong>Details:</strong> {}"#, details);
158        }
159        writeln!(file, r#"        ]]>"#)?;
160        writeln!(file, r#"      </description>"#)?;
161        writeln!(file, r#"      <Point>"#)?;
162        writeln!(
163            file,
164            r#"        <coordinates>{},{},0</coordinates>"#,
165            sighting.longitude, sighting.latitude
166        )?;
167        writeln!(file, r#"      </Point>"#)?;
168        writeln!(file, r#"    </Placemark>"#)?;
169    }
170
171    writeln!(file, r#"  </Document>"#)?;
172    writeln!(file, r#"</kml>"#)?;
173
174    Ok(())
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use crate::models::Source;
181    use chrono::Utc;
182    use tempfile::NamedTempFile;
183
184    fn create_test_sighting(lat: f64, lon: f64, id: i64) -> Sighting {
185        Sighting {
186            id: Some(id),
187            species: "Canis lupus".to_string(),
188            scientific_name: Some("Canis lupus".to_string()),
189            latitude: lat,
190            longitude: lon,
191            observed_on: Utc::now(),
192            source: Source::GBIF,
193            source_id: format!("test_{}", id),
194            details: Some("Test sighting".to_string()),
195        }
196    }
197
198    #[test]
199    fn test_export_csv() {
200        let sightings = vec![
201            create_test_sighting(45.0, -122.0, 1),
202            create_test_sighting(46.0, -123.0, 2),
203        ];
204
205        let temp_file = NamedTempFile::new().unwrap();
206        export_csv(&sightings, temp_file.path().to_str().unwrap()).unwrap();
207
208        let content = std::fs::read_to_string(temp_file.path()).unwrap();
209        assert!(content.contains("id,species,scientific_name"));
210        assert!(content.contains("Canis lupus"));
211    }
212
213    #[test]
214    fn test_export_geojson() {
215        let sightings = vec![create_test_sighting(45.0, -122.0, 1)];
216
217        let temp_file = NamedTempFile::new().unwrap();
218        export_geojson(&sightings, temp_file.path().to_str().unwrap()).unwrap();
219
220        let content = std::fs::read_to_string(temp_file.path()).unwrap();
221        assert!(content.contains("FeatureCollection"));
222        assert!(content.contains("Point"));
223    }
224
225    #[test]
226    fn test_export_kml() {
227        let sightings = vec![create_test_sighting(45.0, -122.0, 1)];
228
229        let temp_file = NamedTempFile::new().unwrap();
230        export_kml(&sightings, temp_file.path().to_str().unwrap()).unwrap();
231
232        let content = std::fs::read_to_string(temp_file.path()).unwrap();
233        assert!(content.contains("<kml"));
234        assert!(content.contains("<Placemark>"));
235        assert!(content.contains("<Point>"));
236    }
237}