1use crate::models::Sighting;
2use anyhow::Result;
3use std::fs::File;
4use std::io::Write;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum ExportFormat {
9 Csv,
10 GeoJson,
11 Kml,
12}
13
14pub 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
27pub fn export_csv(sightings: &[Sighting], output_path: &str) -> Result<()> {
29 let mut file = File::create(output_path)?;
30
31 writeln!(
33 file,
34 "id,species,scientific_name,latitude,longitude,observed_on,source,source_id,details"
35 )?;
36
37 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
62pub 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
116pub 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}