1use std::fmt;
2use std::fs;
3use std::io;
4use std::path::Path;
5
6use quick_xml::events::Event;
7use quick_xml::Reader;
8use serde_json::{json, Value};
9
10use crate::gpx::error::ParseError;
11use crate::gpx::serialize;
12use crate::gpx::types::{Gpx, Route, Track, TrackSegment, Waypoint};
13
14const GPX_FORMAT: &str = "gpx";
15const GEOJSON_FORMAT: &str = "geojson";
16const JSON_FORMAT: &str = "json";
17const KML_FORMAT: &str = "kml";
18
19const KML_NS: &str = "http://www.opengis.net/kml/2.2";
20
21#[derive(Debug)]
23pub enum ConvertError {
24 Io(io::Error),
25 Parse(ParseError),
26 Json(serde_json::Error),
27 Xml(String),
28 UnsupportedFormat(String),
29 UnsupportedGeometry(String),
30 InvalidCoordinates(String),
31 MissingFormat,
32}
33
34impl fmt::Display for ConvertError {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 Self::Io(err) => write!(f, "I/O error: {err}"),
38 Self::Parse(err) => write!(f, "{err}"),
39 Self::Json(err) => write!(f, "JSON error: {err}"),
40 Self::Xml(msg) => write!(f, "XML error: {msg}"),
41 Self::UnsupportedFormat(format) => write!(f, "unsupported format: {format}"),
42 Self::UnsupportedGeometry(kind) => write!(f, "unsupported geometry type: {kind}"),
43 Self::InvalidCoordinates(msg) => write!(f, "invalid coordinates: {msg}"),
44 Self::MissingFormat => write!(f, "could not detect file format from extension"),
45 }
46 }
47}
48
49impl std::error::Error for ConvertError {
50 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
51 match self {
52 Self::Io(err) => Some(err),
53 Self::Parse(err) => Some(err),
54 Self::Json(err) => Some(err),
55 _ => None,
56 }
57 }
58}
59
60impl From<io::Error> for ConvertError {
61 fn from(err: io::Error) -> Self {
62 Self::Io(err)
63 }
64}
65
66impl From<ParseError> for ConvertError {
67 fn from(err: ParseError) -> Self {
68 Self::Parse(err)
69 }
70}
71
72impl From<serde_json::Error> for ConvertError {
73 fn from(err: serde_json::Error) -> Self {
74 Self::Json(err)
75 }
76}
77
78pub fn detect_format(path: &Path) -> Option<&'static str> {
80 match path.extension()?.to_str()?.to_ascii_lowercase().as_str() {
81 "gpx" => Some(GPX_FORMAT),
82 "geojson" => Some(GEOJSON_FORMAT),
83 "json" => Some(JSON_FORMAT),
84 "kml" => Some(KML_FORMAT),
85 _ => None,
86 }
87}
88
89pub fn read_gpx(path: &Path) -> Result<Gpx, ConvertError> {
91 Ok(Gpx::parse_file(path)?)
92}
93
94pub fn read_geojson(path: &Path) -> Result<Gpx, ConvertError> {
96 let data = fs::read_to_string(path)?;
97 parse_geojson(&data)
98}
99
100pub fn read_kml(path: &Path) -> Result<Gpx, ConvertError> {
102 let data = fs::read_to_string(path)?;
103 parse_kml(&data)
104}
105
106pub fn write_geojson(gpx: &Gpx, path: &Path) -> Result<(), ConvertError> {
108 let data = geojson_string(gpx)?;
109 fs::write(path, data)?;
110 Ok(())
111}
112
113pub fn write_kml(gpx: &Gpx, path: &Path, pretty: bool) -> Result<(), ConvertError> {
115 fs::write(path, kml_string(gpx, pretty))?;
116 Ok(())
117}
118
119pub fn convert_file(
124 input: &Path,
125 output: &Path,
126 input_format: Option<&str>,
127 output_format: Option<&str>,
128) -> Result<(String, String), ConvertError> {
129 let in_fmt = resolve_format(input_format, input)?;
130 let out_fmt = resolve_format(output_format, output)?;
131
132 let gpx = read_by_format(input, &in_fmt)?;
133 write_by_format(&gpx, output, &out_fmt, true)?;
134
135 Ok((in_fmt, out_fmt))
136}
137
138fn resolve_format(explicit: Option<&str>, path: &Path) -> Result<String, ConvertError> {
139 if let Some(format) = explicit {
140 return Ok(normalize_format(format)?.to_string());
141 }
142 detect_format(path)
143 .map(str::to_string)
144 .ok_or(ConvertError::MissingFormat)
145}
146
147fn normalize_format(format: &str) -> Result<&'static str, ConvertError> {
148 match format.to_ascii_lowercase().as_str() {
149 GPX_FORMAT => Ok(GPX_FORMAT),
150 GEOJSON_FORMAT | JSON_FORMAT => Ok(GEOJSON_FORMAT),
151 KML_FORMAT => Ok(KML_FORMAT),
152 other => Err(ConvertError::UnsupportedFormat(other.to_string())),
153 }
154}
155
156fn read_by_format(path: &Path, format: &str) -> Result<Gpx, ConvertError> {
157 match format {
158 GPX_FORMAT => read_gpx(path),
159 GEOJSON_FORMAT | JSON_FORMAT => read_geojson(path),
160 KML_FORMAT => read_kml(path),
161 other => Err(ConvertError::UnsupportedFormat(other.to_string())),
162 }
163}
164
165fn write_by_format(gpx: &Gpx, path: &Path, format: &str, pretty: bool) -> Result<(), ConvertError> {
166 match format {
167 GPX_FORMAT => serialize::write_file(gpx, path, pretty).map_err(ConvertError::from),
168 GEOJSON_FORMAT | JSON_FORMAT => write_geojson(gpx, path),
169 KML_FORMAT => write_kml(gpx, path, pretty),
170 other => Err(ConvertError::UnsupportedFormat(other.to_string())),
171 }
172}
173
174fn parse_geojson(data: &str) -> Result<Gpx, ConvertError> {
175 let value: Value = serde_json::from_str(data)?;
176 let mut gpx = Gpx::default();
177
178 match value.get("type").and_then(Value::as_str) {
179 Some("FeatureCollection") => {
180 if let Some(features) = value.get("features").and_then(Value::as_array) {
181 for feature in features {
182 ingest_feature(feature, &mut gpx)?;
183 }
184 }
185 }
186 Some("Feature") => ingest_feature(&value, &mut gpx)?,
187 Some("Point" | "LineString" | "MultiPoint" | "MultiLineString") => {
188 ingest_geometry(&value, None, None, &mut gpx)?;
189 }
190 Some(other) => return Err(ConvertError::UnsupportedGeometry(other.to_string())),
191 None => return Err(ConvertError::UnsupportedGeometry("missing type".to_string())),
192 }
193
194 Ok(gpx)
195}
196
197fn ingest_feature(feature: &Value, gpx: &mut Gpx) -> Result<(), ConvertError> {
198 let properties = feature.get("properties");
199 let name = properties
200 .and_then(|p| p.get("name"))
201 .or_else(|| properties.and_then(|p| p.get("title")))
202 .and_then(Value::as_str)
203 .map(str::to_string);
204 let desc = properties
205 .and_then(|p| p.get("description"))
206 .or_else(|| properties.and_then(|p| p.get("desc")))
207 .and_then(Value::as_str)
208 .map(str::to_string);
209 let gpx_type = properties
210 .and_then(|p| p.get("gpx_type"))
211 .and_then(Value::as_str);
212
213 if let Some(geometry) = feature.get("geometry") {
214 ingest_geometry_with_hint(geometry, name, desc, gpx, gpx_type)?;
215 }
216
217 Ok(())
218}
219
220fn ingest_geometry(
221 geometry: &Value,
222 name: Option<String>,
223 desc: Option<String>,
224 gpx: &mut Gpx,
225) -> Result<(), ConvertError> {
226 ingest_geometry_with_hint(geometry, name, desc, gpx, None)
227}
228
229fn ingest_geometry_with_hint(
230 geometry: &Value,
231 name: Option<String>,
232 desc: Option<String>,
233 gpx: &mut Gpx,
234 gpx_type: Option<&str>,
235) -> Result<(), ConvertError> {
236 let geo_type = geometry
237 .get("type")
238 .and_then(Value::as_str)
239 .ok_or_else(|| ConvertError::UnsupportedGeometry("missing type".to_string()))?;
240
241 match geo_type {
242 "Point" => {
243 let waypoint = parse_geojson_point(geometry.get("coordinates").ok_or_else(|| {
244 ConvertError::InvalidCoordinates("Point missing coordinates".to_string())
245 })?)?;
246 gpx.waypoints.push(apply_waypoint_meta(waypoint, name, desc));
247 }
248 "MultiPoint" => {
249 let coords = geometry
250 .get("coordinates")
251 .and_then(Value::as_array)
252 .ok_or_else(|| {
253 ConvertError::InvalidCoordinates("MultiPoint missing coordinates".to_string())
254 })?;
255 for coord in coords {
256 let waypoint = parse_geojson_point(coord)?;
257 gpx.waypoints.push(apply_waypoint_meta(waypoint, name.clone(), desc.clone()));
258 }
259 }
260 "LineString" => {
261 let points = parse_geojson_line(geometry.get("coordinates").ok_or_else(|| {
262 ConvertError::InvalidCoordinates("LineString missing coordinates".to_string())
263 })?)?;
264 if matches!(gpx_type, Some("track" | "trk")) {
265 gpx.tracks.push(Track {
266 name,
267 desc,
268 segments: vec![TrackSegment { points, ..Default::default() }],
269 ..Default::default()
270 });
271 } else {
272 gpx.routes.push(Route {
273 name,
274 desc,
275 points,
276 ..Default::default()
277 });
278 }
279 }
280 "MultiLineString" => {
281 let lines = geometry
282 .get("coordinates")
283 .and_then(Value::as_array)
284 .ok_or_else(|| {
285 ConvertError::InvalidCoordinates(
286 "MultiLineString missing coordinates".to_string(),
287 )
288 })?;
289 let mut segments = Vec::new();
290 for line in lines {
291 segments.push(TrackSegment {
292 points: parse_geojson_line(line)?,
293 ..Default::default()
294 });
295 }
296 gpx.tracks.push(Track {
297 name,
298 desc,
299 segments,
300 ..Default::default()
301 });
302 }
303 other => return Err(ConvertError::UnsupportedGeometry(other.to_string())),
304 }
305
306 Ok(())
307}
308
309fn apply_waypoint_meta(mut waypoint: Waypoint, name: Option<String>, desc: Option<String>) -> Waypoint {
310 if name.is_some() {
311 waypoint.name = name;
312 }
313 if desc.is_some() {
314 waypoint.desc = desc;
315 }
316 waypoint
317}
318
319fn parse_geojson_point(value: &Value) -> Result<Waypoint, ConvertError> {
320 let coords = value
321 .as_array()
322 .ok_or_else(|| ConvertError::InvalidCoordinates("expected coordinate array".to_string()))?;
323 if coords.len() < 2 {
324 return Err(ConvertError::InvalidCoordinates(
325 "coordinate array must contain at least lon and lat".to_string(),
326 ));
327 }
328 let lon = coord_component(&coords[0])?;
329 let lat = coord_component(&coords[1])?;
330 let ele = if coords.len() > 2 {
331 Some(coord_component(&coords[2])?)
332 } else {
333 None
334 };
335 Ok(waypoint(lat, lon, ele))
336}
337
338fn parse_geojson_line(value: &Value) -> Result<Vec<Waypoint>, ConvertError> {
339 let coords = value
340 .as_array()
341 .ok_or_else(|| ConvertError::InvalidCoordinates("expected coordinate array".to_string()))?;
342 coords.iter().map(parse_geojson_point).collect()
343}
344
345fn coord_component(value: &Value) -> Result<f64, ConvertError> {
346 value
347 .as_f64()
348 .ok_or_else(|| ConvertError::InvalidCoordinates(format!("expected number, got {value}")))
349}
350
351fn waypoint(lat: f64, lon: f64, ele: Option<f64>) -> Waypoint {
352 Waypoint {
353 lat,
354 lon,
355 ele,
356 time: None,
357 magvar: None,
358 geoidheight: None,
359 name: None,
360 cmt: None,
361 desc: None,
362 src: None,
363 links: vec![],
364 sym: None,
365 waypoint_type: None,
366 fix: None,
367 sat: None,
368 hdop: None,
369 vdop: None,
370 pdop: None,
371 ageofdgpsdata: None,
372 dgpsid: None,
373 extensions: None,
374 }
375}
376
377fn geojson_string(gpx: &Gpx) -> Result<String, ConvertError> {
378 let mut features = Vec::new();
379
380 for waypoint in &gpx.waypoints {
381 features.push(waypoint_feature(waypoint));
382 }
383 for route in &gpx.routes {
384 features.push(route_feature(route));
385 }
386 for track in &gpx.tracks {
387 features.extend(track_features(track));
388 }
389
390 let collection = json!({
391 "type": "FeatureCollection",
392 "features": features,
393 });
394 Ok(serde_json::to_string_pretty(&collection)?)
395}
396
397fn waypoint_feature(waypoint: &Waypoint) -> Value {
398 let mut properties = json!({ "gpx_type": "waypoint" });
399 if let Some(name) = &waypoint.name {
400 properties["name"] = json!(name);
401 }
402 if let Some(desc) = &waypoint.desc {
403 properties["description"] = json!(desc);
404 }
405
406 json!({
407 "type": "Feature",
408 "properties": properties,
409 "geometry": {
410 "type": "Point",
411 "coordinates": waypoint_coordinates(waypoint),
412 },
413 })
414}
415
416fn route_feature(route: &Route) -> Value {
417 let mut properties = json!({ "gpx_type": "route" });
418 if let Some(name) = &route.name {
419 properties["name"] = json!(name);
420 }
421 if let Some(desc) = &route.desc {
422 properties["description"] = json!(desc);
423 }
424
425 json!({
426 "type": "Feature",
427 "properties": properties,
428 "geometry": {
429 "type": "LineString",
430 "coordinates": route.points.iter().map(waypoint_coordinates).collect::<Vec<_>>(),
431 },
432 })
433}
434
435fn track_features(track: &Track) -> Vec<Value> {
436 track
437 .segments
438 .iter()
439 .enumerate()
440 .map(|(index, segment)| {
441 let mut properties = json!({ "gpx_type": "track" });
442 if let Some(name) = &track.name {
443 properties["name"] = json!(name);
444 }
445 if let Some(desc) = &track.desc {
446 properties["description"] = json!(desc);
447 }
448 if track.segments.len() > 1 {
449 properties["segment"] = json!(index);
450 }
451
452 json!({
453 "type": "Feature",
454 "properties": properties,
455 "geometry": {
456 "type": "LineString",
457 "coordinates": segment.points.iter().map(waypoint_coordinates).collect::<Vec<_>>(),
458 },
459 })
460 })
461 .collect()
462}
463
464fn waypoint_coordinates(waypoint: &Waypoint) -> Value {
465 match waypoint.ele {
466 Some(ele) => json!([waypoint.lon, waypoint.lat, ele]),
467 None => json!([waypoint.lon, waypoint.lat]),
468 }
469}
470
471fn parse_kml(data: &str) -> Result<Gpx, ConvertError> {
472 let data = strip_kml_namespace(data);
473 let placemarks = parse_kml_placemarks(&data)?;
474 let mut gpx = Gpx::default();
475
476 for placemark in placemarks {
477 if let Some(point) = placemark.point {
478 let mut waypoint = parse_kml_coordinates(&point)?.into_iter().next().ok_or_else(|| {
479 ConvertError::InvalidCoordinates("Point placemark has no coordinates".to_string())
480 })?;
481 waypoint.name = placemark.name.clone();
482 waypoint.desc = placemark.description.clone();
483 gpx.waypoints.push(waypoint);
484 } else if let Some(line) = placemark.line_string {
485 let points = parse_kml_coordinates(&line)?;
486 gpx.routes.push(Route {
487 name: placemark.name.clone(),
488 desc: placemark.description.clone(),
489 points,
490 ..Default::default()
491 });
492 } else if let Some(lines) = placemark.multi_geometry {
493 let mut segments = Vec::new();
494 for line in lines {
495 segments.push(TrackSegment {
496 points: parse_kml_coordinates(&line)?,
497 ..Default::default()
498 });
499 }
500 gpx.tracks.push(Track {
501 name: placemark.name.clone(),
502 desc: placemark.description.clone(),
503 segments,
504 ..Default::default()
505 });
506 }
507 }
508
509 Ok(gpx)
510}
511
512fn strip_kml_namespace(xml: &str) -> String {
513 xml.replace(&format!(r#" xmlns="{KML_NS}""#), "")
514 .replace(&format!(" xmlns='{KML_NS}'"), "")
515}
516
517#[derive(Debug, Default)]
518struct KmlPlacemarkData {
519 name: Option<String>,
520 description: Option<String>,
521 point: Option<String>,
522 line_string: Option<String>,
523 multi_geometry: Option<Vec<String>>,
524}
525
526fn parse_kml_placemarks(data: &str) -> Result<Vec<KmlPlacemarkData>, ConvertError> {
527 let mut reader = Reader::from_str(data);
528 reader.config_mut().trim_text(true);
529
530 let mut placemarks = Vec::new();
531 let mut current: Option<KmlPlacemarkData> = None;
532 let mut element_stack: Vec<String> = Vec::new();
533 let mut text = String::new();
534
535 loop {
536 match reader.read_event() {
537 Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
538 let name = local_name_start(&e);
539 if name == "Placemark" {
540 current = Some(KmlPlacemarkData::default());
541 }
542 if let Some(placemark) = current.as_mut() {
543 match name.as_str() {
544 "Point" if placemark.point.is_none() && placemark.line_string.is_none() => {}
545 "LineString" if placemark.line_string.is_none() && placemark.multi_geometry.is_none() => {}
546 "MultiGeometry" if placemark.multi_geometry.is_none() => {
547 placemark.multi_geometry = Some(Vec::new());
548 }
549 "coordinates" => text.clear(),
550 _ => {}
551 }
552 }
553 element_stack.push(name);
554 }
555 Ok(Event::Text(e)) => {
556 if element_stack.last().is_some_and(|tag| tag == "coordinates") {
557 text.push_str(&e.unescape().map_err(|err| ConvertError::Xml(err.to_string()))?);
558 } else if element_stack.last().is_some_and(|tag| tag == "name" || tag == "description") {
559 if let Some(placemark) = current.as_mut() {
560 let value = e
561 .unescape()
562 .map_err(|err| ConvertError::Xml(err.to_string()))?
563 .into_owned();
564 match element_stack.last().map(String::as_str) {
565 Some("name") => placemark.name = Some(value),
566 Some("description") => placemark.description = Some(value),
567 _ => {}
568 }
569 }
570 }
571 }
572 Ok(Event::End(e)) => {
573 let name = local_name_end(&e);
574 if name == "coordinates" {
575 if let Some(placemark) = current.as_mut() {
576 let coords = text.trim().to_string();
577 if element_stack.iter().any(|tag| tag == "MultiGeometry") {
578 if let Some(lines) = placemark.multi_geometry.as_mut() {
579 lines.push(coords);
580 }
581 } else if element_stack.iter().any(|tag| tag == "LineString") {
582 placemark.line_string = Some(coords);
583 } else if element_stack.iter().any(|tag| tag == "Point") {
584 placemark.point = Some(coords);
585 }
586 }
587 text.clear();
588 }
589 if name == "Placemark" {
590 if let Some(placemark) = current.take() {
591 placemarks.push(placemark);
592 }
593 }
594 element_stack.pop();
595 }
596 Ok(Event::Eof) => break,
597 Ok(_) => {}
598 Err(err) => return Err(ConvertError::Xml(err.to_string())),
599 }
600 }
601
602 Ok(placemarks)
603}
604
605fn local_name_start(event: &quick_xml::events::BytesStart<'_>) -> String {
606 String::from_utf8_lossy(event.local_name().as_ref()).into_owned()
607}
608
609fn local_name_end(event: &quick_xml::events::BytesEnd<'_>) -> String {
610 String::from_utf8_lossy(event.local_name().as_ref()).into_owned()
611}
612
613fn parse_kml_coordinates(raw: &str) -> Result<Vec<Waypoint>, ConvertError> {
614 let mut points = Vec::new();
615 for token in raw.split_whitespace() {
616 let parts: Vec<&str> = token.split(',').collect();
617 if parts.len() < 2 {
618 return Err(ConvertError::InvalidCoordinates(format!(
619 "expected lon,lat[,ele], got `{token}`"
620 )));
621 }
622 let lon = parts[0]
623 .trim()
624 .parse::<f64>()
625 .map_err(|_| ConvertError::InvalidCoordinates(format!("invalid longitude `{token}`")))?;
626 let lat = parts[1]
627 .trim()
628 .parse::<f64>()
629 .map_err(|_| ConvertError::InvalidCoordinates(format!("invalid latitude `{token}`")))?;
630 let ele = if parts.len() > 2 && !parts[2].trim().is_empty() {
631 Some(
632 parts[2]
633 .trim()
634 .parse::<f64>()
635 .map_err(|_| ConvertError::InvalidCoordinates(format!("invalid elevation `{token}`")))?,
636 )
637 } else {
638 None
639 };
640 points.push(waypoint(lat, lon, ele));
641 }
642 Ok(points)
643}
644
645fn kml_string(gpx: &Gpx, pretty: bool) -> String {
646 let mut out = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
647 if pretty {
648 out.push_str("<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n <Document>\n");
649 } else {
650 out.push_str("<kml xmlns=\"http://www.opengis.net/kml/2.2\"><Document>");
651 }
652
653 if let Some(metadata) = &gpx.metadata {
654 if let Some(name) = &metadata.name {
655 write_kml_text("name", name, &mut out, pretty, 2);
656 }
657 if let Some(desc) = &metadata.desc {
658 write_kml_text("description", desc, &mut out, pretty, 2);
659 }
660 }
661
662 for waypoint in &gpx.waypoints {
663 write_kml_waypoint_placemark(waypoint, &mut out, pretty);
664 }
665 for route in &gpx.routes {
666 write_kml_route_placemark(route, &mut out, pretty);
667 }
668 for track in &gpx.tracks {
669 write_kml_track_placemark(track, &mut out, pretty);
670 }
671
672 if pretty {
673 out.push_str(" </Document>\n</kml>\n");
674 } else {
675 out.push_str("</Document></kml>");
676 }
677 out
678}
679
680fn write_kml_waypoint_placemark(waypoint: &Waypoint, out: &mut String, pretty: bool) {
681 write_kml_placemark_start(out, pretty, 2);
682 write_kml_optional_text("name", waypoint.name.as_deref(), out, pretty, 3);
683 write_kml_optional_text("description", waypoint.desc.as_deref(), out, pretty, 3);
684 write_kml_tag_open("Point", out, pretty, 3);
685 write_kml_coordinates(std::slice::from_ref(waypoint), out, pretty, 4);
686 write_kml_tag_close("Point", out, pretty, 3);
687 write_kml_placemark_end(out, pretty, 2);
688}
689
690fn write_kml_route_placemark(route: &Route, out: &mut String, pretty: bool) {
691 write_kml_placemark_start(out, pretty, 2);
692 write_kml_optional_text("name", route.name.as_deref(), out, pretty, 3);
693 write_kml_optional_text("description", route.desc.as_deref(), out, pretty, 3);
694 write_kml_tag_open("LineString", out, pretty, 3);
695 write_kml_coordinates(&route.points, out, pretty, 4);
696 write_kml_tag_close("LineString", out, pretty, 3);
697 write_kml_placemark_end(out, pretty, 2);
698}
699
700fn write_kml_track_placemark(track: &Track, out: &mut String, pretty: bool) {
701 write_kml_placemark_start(out, pretty, 2);
702 write_kml_optional_text("name", track.name.as_deref(), out, pretty, 3);
703 write_kml_optional_text("description", track.desc.as_deref(), out, pretty, 3);
704 if track.segments.len() == 1 {
705 write_kml_tag_open("LineString", out, pretty, 3);
706 write_kml_coordinates(&track.segments[0].points, out, pretty, 4);
707 write_kml_tag_close("LineString", out, pretty, 3);
708 } else {
709 write_kml_tag_open("MultiGeometry", out, pretty, 3);
710 for segment in &track.segments {
711 write_kml_tag_open("LineString", out, pretty, 4);
712 write_kml_coordinates(&segment.points, out, pretty, 5);
713 write_kml_tag_close("LineString", out, pretty, 4);
714 }
715 write_kml_tag_close("MultiGeometry", out, pretty, 3);
716 }
717 write_kml_placemark_end(out, pretty, 2);
718}
719
720fn write_kml_placemark_start(out: &mut String, pretty: bool, depth: usize) {
721 write_kml_tag_open("Placemark", out, pretty, depth);
722}
723
724fn write_kml_placemark_end(out: &mut String, pretty: bool, depth: usize) {
725 write_kml_tag_close("Placemark", out, pretty, depth);
726}
727
728fn write_kml_coordinates(points: &[Waypoint], out: &mut String, pretty: bool, depth: usize) {
729 let coords = points
730 .iter()
731 .map(|point| match point.ele {
732 Some(ele) => format!("{},{},{}", point.lon, point.lat, ele),
733 None => format!("{},{}", point.lon, point.lat),
734 })
735 .collect::<Vec<_>>()
736 .join(if pretty { "\n" } else { " " });
737 write_kml_text("coordinates", &coords, out, pretty, depth);
738}
739
740fn write_kml_optional_text(tag: &str, value: Option<&str>, out: &mut String, pretty: bool, depth: usize) {
741 if let Some(value) = value {
742 write_kml_text(tag, value, out, pretty, depth);
743 }
744}
745
746fn write_kml_text(tag: &str, value: &str, out: &mut String, pretty: bool, depth: usize) {
747 if pretty {
748 out.push_str(&" ".repeat(depth));
749 }
750 out.push('<');
751 out.push_str(tag);
752 out.push('>');
753 out.push_str(&escape_xml(value));
754 out.push_str("</");
755 out.push_str(tag);
756 out.push('>');
757 if pretty {
758 out.push('\n');
759 }
760}
761
762fn write_kml_tag_open(tag: &str, out: &mut String, pretty: bool, depth: usize) {
763 if pretty {
764 out.push_str(&" ".repeat(depth));
765 }
766 out.push('<');
767 out.push_str(tag);
768 out.push('>');
769 if pretty {
770 out.push('\n');
771 }
772}
773
774fn write_kml_tag_close(tag: &str, out: &mut String, pretty: bool, depth: usize) {
775 if pretty {
776 out.push_str(&" ".repeat(depth));
777 }
778 out.push_str("</");
779 out.push_str(tag);
780 out.push('>');
781 if pretty {
782 out.push('\n');
783 }
784}
785
786fn escape_xml(value: &str) -> String {
787 value
788 .replace('&', "&")
789 .replace('<', "<")
790 .replace('>', ">")
791}
792
793#[cfg(test)]
794mod tests {
795 use super::*;
796
797 #[test]
798 fn detect_format_extensions() {
799 assert_eq!(detect_format(Path::new("a.gpx")), Some("gpx"));
800 assert_eq!(detect_format(Path::new("a.geojson")), Some("geojson"));
801 assert_eq!(detect_format(Path::new("a.json")), Some("json"));
802 assert_eq!(detect_format(Path::new("a.kml")), Some("kml"));
803 assert_eq!(detect_format(Path::new("a.txt")), None);
804 }
805
806 #[test]
807 fn geojson_point_to_waypoint() {
808 let gpx = parse_geojson(
809 r#"{"type":"Feature","geometry":{"type":"Point","coordinates":[-122.3,47.6,12.0]},"properties":{"name":"Test"}}"#,
810 )
811 .unwrap();
812 assert_eq!(gpx.waypoints.len(), 1);
813 assert!((gpx.waypoints[0].lon - (-122.3)).abs() < f64::EPSILON);
814 assert!((gpx.waypoints[0].lat - 47.6).abs() < f64::EPSILON);
815 assert_eq!(gpx.waypoints[0].ele, Some(12.0));
816 assert_eq!(gpx.waypoints[0].name.as_deref(), Some("Test"));
817 }
818
819 #[test]
820 fn geojson_linestring_to_route() {
821 let gpx = parse_geojson(
822 r#"{"type":"Feature","geometry":{"type":"LineString","coordinates":[[-122.3,47.6],[-122.4,47.7]]}}"#,
823 )
824 .unwrap();
825 assert_eq!(gpx.routes.len(), 1);
826 assert_eq!(gpx.routes[0].points.len(), 2);
827 }
828
829 #[test]
830 fn geojson_multilinestring_to_track() {
831 let gpx = parse_geojson(
832 r#"{"type":"Feature","geometry":{"type":"MultiLineString","coordinates":[[[-122.3,47.6],[-122.4,47.7]],[[-122.5,47.8],[-122.6,47.9]]]}}"#,
833 )
834 .unwrap();
835 assert_eq!(gpx.tracks.len(), 1);
836 assert_eq!(gpx.tracks[0].segments.len(), 2);
837 }
838
839 #[test]
840 fn kml_point_and_linestring() {
841 let kml = r#"<?xml version="1.0" encoding="UTF-8"?>
842<kml xmlns="http://www.opengis.net/kml/2.2">
843 <Document>
844 <Placemark>
845 <name>Point A</name>
846 <Point><coordinates>-122.3,47.6,10</coordinates></Point>
847 </Placemark>
848 <Placemark>
849 <name>Route B</name>
850 <LineString><coordinates>-122.3,47.6 -122.4,47.7</coordinates></LineString>
851 </Placemark>
852 <Placemark>
853 <name>Track C</name>
854 <MultiGeometry>
855 <LineString><coordinates>-122.3,47.6 -122.4,47.7</coordinates></LineString>
856 <LineString><coordinates>-122.5,47.8 -122.6,47.9</coordinates></LineString>
857 </MultiGeometry>
858 </Placemark>
859 </Document>
860</kml>"#;
861 let gpx = parse_kml(kml).unwrap();
862 assert_eq!(gpx.waypoints.len(), 1);
863 assert_eq!(gpx.routes.len(), 1);
864 assert_eq!(gpx.tracks.len(), 1);
865 assert_eq!(gpx.tracks[0].segments.len(), 2);
866 }
867}