gistools/readers/gpx/
spec.rs

1use super::GPXProperties;
2use crate::{
3    data_structures::HasLayer,
4    parsers::{XMLTagItem, xml_find_tag_by_name, xml_find_tags_by_name, xml_get_attribute},
5};
6use alloc::{
7    string::{String, ToString},
8    vec::Vec,
9};
10use core::{fmt, str::FromStr};
11use s2json::{
12    MValueCompatible, PrimitiveValue, ValuePrimitive, ValueType, VectorFeature, VectorFeatureType,
13    VectorGeometry, VectorLineString, VectorMultiLineString, VectorPoint,
14};
15use serde::{Deserialize, Serialize};
16
17/// Represents the root GPX document.
18#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
19pub struct GPX {
20    /// Fixed GPX version
21    pub version: String, // "1.1",
22    /// Name or URL of the software that created the GPX document
23    pub creator: String,
24    /// Optional metadata about the file
25    pub metadata: Option<GPXMetadata>,
26    /// Array of waypoints
27    pub wpt: Option<Vec<GPXWaypoint>>,
28    /// Array of routes
29    pub rte: Option<Vec<GPXRoute>>,
30    /// Array of tracks
31    pub trk: Option<Vec<GPXTrack>>,
32    // /// Custom extensions for additional data
33    // pub extensions: Option<MValue>,
34}
35impl GPX {
36    /// Creates a new GPX from an XML string
37    pub fn new(gpx_xml: &str) -> GPX {
38        let root_tag = xml_find_tag_by_name(gpx_xml, "gpx", None);
39
40        if let Some(root) = root_tag {
41            let version =
42                xml_get_attribute(&XMLTagItem::XMLTag(root.clone()), "version").unwrap_or_default();
43            let creator =
44                xml_get_attribute(&XMLTagItem::XMLTag(root.clone()), "creator").unwrap_or_default();
45
46            let metadata_tag = xml_find_tag_by_name(&root.outer, "metadata", None);
47            let metadata = metadata_tag.map(|tag| GPXMetadata::new(XMLTagItem::XMLTag(tag)));
48
49            let wpt = {
50                let wpt_tags = xml_find_tags_by_name(&root.outer, "wpt", None);
51                if !wpt_tags.is_empty() {
52                    Some(
53                        wpt_tags
54                            .into_iter()
55                            .map(|wpt| GPXWaypoint::new(XMLTagItem::XMLTag(wpt)))
56                            .collect(),
57                    )
58                } else {
59                    None
60                }
61            };
62
63            let rte = {
64                let rte_tags = xml_find_tags_by_name(&root.outer, "rte", None);
65                if !rte_tags.is_empty() {
66                    Some(
67                        rte_tags
68                            .into_iter()
69                            .map(|rte| GPXRoute::new(XMLTagItem::XMLTag(rte)))
70                            .collect(),
71                    )
72                } else {
73                    None
74                }
75            };
76
77            let trk = {
78                let trk_tags = xml_find_tags_by_name(&root.outer, "trk", None);
79                if !trk_tags.is_empty() {
80                    Some(
81                        trk_tags
82                            .into_iter()
83                            .map(|trk| GPXTrack::new(XMLTagItem::XMLTag(trk)))
84                            .collect(),
85                    )
86                } else {
87                    None
88                }
89            };
90
91            GPX { version, creator, metadata, wpt, rte, trk }
92        } else {
93            GPX::default()
94        }
95    }
96}
97
98/// Contains metadata information about the GPX file.
99#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
100pub struct GPXMetadata {
101    /// Name of the GPX file
102    pub name: Option<String>,
103    /// Description of the file's contents
104    pub desc: Option<String>,
105    /// Person or organization responsible for the file
106    pub author: Option<GPXPerson>,
107    /// Copyright and license information
108    pub copyright: Option<GPXCopyright>,
109    /// URLs associated with the GPX file
110    pub link: Option<GPXLink>,
111    /// Creation timestamp in ISO 8601 format
112    pub time: Option<String>,
113    /// Keywords for classification
114    pub keywords: Option<String>,
115    /// Bounding box of the data
116    pub bounds: Option<GPXBounds>,
117    // /// Custom extensions
118    // pub extensions: Option<MValue>,
119}
120impl GPXMetadata {
121    /// Creates a new GPXMetadata from an XMLTagItem
122    pub fn new(metadata_xml: XMLTagItem) -> Self {
123        let inner = match &metadata_xml {
124            XMLTagItem::XMLTag(tag) => tag.inner.clone().unwrap_or_default(),
125            XMLTagItem::String(s) => s.clone(),
126        };
127
128        let name = xml_find_tag_by_name(&inner, "name", None).and_then(|tag| tag.inner);
129        let desc = xml_find_tag_by_name(&inner, "desc", None).and_then(|tag| tag.inner);
130        let author = xml_find_tag_by_name(&inner, "author", None)
131            .map(|tag| GPXPerson::new(XMLTagItem::XMLTag(tag)));
132        let copyright = xml_find_tag_by_name(&inner, "copyright", None)
133            .map(|tag| GPXCopyright::new(XMLTagItem::XMLTag(tag)));
134        let link = xml_find_tag_by_name(&inner, "link", None)
135            .map(|tag| GPXLink::new(XMLTagItem::XMLTag(tag)));
136        let time = xml_find_tag_by_name(&inner, "time", None).and_then(|tag| tag.inner);
137        let keywords = xml_find_tag_by_name(&inner, "keywords", None).and_then(|tag| tag.inner);
138        let bounds = xml_find_tag_by_name(&inner, "bounds", None)
139            .map(|tag| GPXBounds::new(XMLTagItem::XMLTag(tag)));
140
141        GPXMetadata { name, desc, author, copyright, link, time, keywords, bounds }
142    }
143}
144impl HasLayer for GPXMetadata {
145    fn get_layer(&self) -> Option<String> {
146        None
147    }
148}
149
150/// Represents a route, which is an ordered list of waypoints leading to a destination.
151#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
152pub struct GPXRoute {
153    /// Route name
154    pub name: Option<String>,
155    /// Route comment
156    pub cmt: Option<String>,
157    /// Route description
158    pub desc: Option<String>,
159    /// Source of data
160    pub src: Option<String>,
161    /// Links to external information
162    pub link: Option<Vec<GPXLink>>,
163    /// Route number
164    pub number: Option<usize>,
165    /// Classification type of the route
166    pub r#type: Option<String>,
167    /// Ordered list of route waypoints
168    pub rtept: Option<Vec<GPXWaypoint>>,
169    // /// Custom extensions
170    // pub extensions: Option<MValue>,
171}
172impl GPXRoute {
173    /// Creates a new GPXRoute from an XMLTagItem
174    pub fn new(route_xml: XMLTagItem) -> Self {
175        let inner = match &route_xml {
176            XMLTagItem::XMLTag(tag) => tag.inner.clone().unwrap_or_default(),
177            XMLTagItem::String(s) => s.clone(),
178        };
179
180        let name = xml_find_tag_by_name(&inner, "name", None).and_then(|tag| tag.inner);
181        let cmt = xml_find_tag_by_name(&inner, "cmt", None).and_then(|tag| tag.inner);
182        let desc = xml_find_tag_by_name(&inner, "desc", None).and_then(|tag| tag.inner);
183        let src = xml_find_tag_by_name(&inner, "src", None).and_then(|tag| tag.inner);
184        let link = {
185            let link_tags = xml_find_tags_by_name(&inner, "link", None);
186            if !link_tags.is_empty() {
187                Some(
188                    link_tags
189                        .into_iter()
190                        .map(|tag| GPXLink::new(XMLTagItem::XMLTag(tag)))
191                        .collect(),
192                )
193            } else {
194                None
195            }
196        };
197        let number = xml_find_tag_by_name(&inner, "number", None)
198            .and_then(|tag| tag.inner.and_then(|s| s.parse::<usize>().ok()));
199        let r#type = xml_find_tag_by_name(&inner, "type", None).and_then(|tag| tag.inner);
200        let rtept = {
201            let rtept_tags = xml_find_tags_by_name(&inner, "rtept", None);
202            if !rtept_tags.is_empty() {
203                Some(
204                    rtept_tags
205                        .into_iter()
206                        .map(|tag| GPXWaypoint::new(XMLTagItem::XMLTag(tag)))
207                        .collect(),
208                )
209            } else {
210                None
211            }
212        };
213
214        GPXRoute { name, cmt, desc, src, link, number, r#type, rtept }
215    }
216
217    /// Create a linestring of waypoints
218    pub fn line(&self) -> VectorLineString<GPXWaypoint> {
219        self.rtept.as_ref().map(|r| r.iter().map(|w| w.point()).collect()).unwrap_or_default()
220    }
221
222    /// Create a Vector Feature from the Route
223    pub fn feature(&self) -> VectorFeature<(), GPXProperties, GPXWaypoint> {
224        VectorFeature {
225            _type: VectorFeatureType::VectorFeature,
226            properties: self.into(),
227            geometry: VectorGeometry::new_linestring(self.line(), None),
228            ..Default::default()
229        }
230    }
231}
232impl From<&GPXRoute> for GPXProperties {
233    fn from(route: &GPXRoute) -> Self {
234        GPXProperties {
235            name: route.name.clone(),
236            cmt: route.cmt.clone(),
237            desc: route.desc.clone(),
238            src: route.src.clone(),
239            link: route.link.clone(),
240            number: route.number,
241            track_type: None,
242            route_type: route.r#type.clone(),
243        }
244    }
245}
246
247/// Represents a track, which is an ordered list of points describing a path.
248#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
249pub struct GPXTrack {
250    /// Track name
251    pub name: Option<String>,
252    /// Track comment
253    pub cmt: Option<String>,
254    /// Track description
255    pub desc: Option<String>,
256    /// Source of data
257    pub src: Option<String>,
258    /// Links to external information
259    pub link: Option<Vec<GPXLink>>,
260    /// Track number
261    pub number: Option<usize>,
262    /// Classification type of the track
263    pub r#type: Option<String>,
264    /// Ordered list of track segments
265    pub trkseg: Option<Vec<GPXTrackSegment>>,
266    // /// Custom extensions
267    // pub extensions: Option<MValue>,
268}
269impl GPXTrack {
270    /// Creates a new GPXTrack from an XMLTagItem
271    pub fn new(track_xml: XMLTagItem) -> Self {
272        let inner = match &track_xml {
273            XMLTagItem::XMLTag(tag) => tag.inner.clone().unwrap_or_default(),
274            XMLTagItem::String(s) => s.clone(),
275        };
276
277        let name = xml_find_tag_by_name(&inner, "name", None).and_then(|tag| tag.inner);
278        let cmt = xml_find_tag_by_name(&inner, "cmt", None).and_then(|tag| tag.inner);
279        let desc = xml_find_tag_by_name(&inner, "desc", None).and_then(|tag| tag.inner);
280        let src = xml_find_tag_by_name(&inner, "src", None).and_then(|tag| tag.inner);
281        let link = {
282            let link_tags = xml_find_tags_by_name(&inner, "link", None);
283            if !link_tags.is_empty() {
284                Some(
285                    link_tags
286                        .into_iter()
287                        .map(|tag| GPXLink::new(XMLTagItem::XMLTag(tag)))
288                        .collect(),
289                )
290            } else {
291                None
292            }
293        };
294        let number = xml_find_tag_by_name(&inner, "number", None)
295            .and_then(|tag| tag.inner.and_then(|s| s.parse::<usize>().ok()));
296        let r#type = xml_find_tag_by_name(&inner, "type", None).and_then(|tag| tag.inner);
297        let trkseg = {
298            let trkseg_tags = xml_find_tags_by_name(&inner, "trkseg", None);
299            if !trkseg_tags.is_empty() {
300                Some(
301                    trkseg_tags
302                        .into_iter()
303                        .map(|tag| GPXTrackSegment::new(XMLTagItem::XMLTag(tag)))
304                        .collect(),
305                )
306            } else {
307                None
308            }
309        };
310
311        GPXTrack { name, cmt, desc, src, link, number, r#type, trkseg }
312    }
313
314    /// create a multi-linestring
315    pub fn multiline(&self) -> VectorMultiLineString<GPXWaypoint> {
316        self.trkseg
317            .as_ref()
318            .map(|r| {
319                r.iter()
320                    .map(|s| {
321                        s.trkpt
322                            .as_ref()
323                            .map(|t| t.iter().map(|w| w.point()).collect())
324                            .unwrap_or_default()
325                    })
326                    .collect()
327            })
328            .unwrap_or_default()
329    }
330
331    /// Create a Vector Feature from the Route
332    pub fn feature(&self) -> VectorFeature<(), GPXProperties, GPXWaypoint> {
333        VectorFeature {
334            _type: VectorFeatureType::VectorFeature,
335            properties: self.into(),
336            geometry: VectorGeometry::new_multilinestring(self.multiline(), None),
337            ..Default::default()
338        }
339    }
340}
341impl From<&GPXTrack> for GPXProperties {
342    fn from(track: &GPXTrack) -> Self {
343        GPXProperties {
344            name: track.name.clone(),
345            cmt: track.cmt.clone(),
346            desc: track.desc.clone(),
347            src: track.src.clone(),
348            link: track.link.clone(),
349            number: track.number,
350            track_type: track.r#type.clone(),
351            route_type: None,
352        }
353    }
354}
355
356/// Represents a track segment, which holds a list of track points logically connected in order.
357#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
358pub struct GPXTrackSegment {
359    /// Ordered list of track points
360    pub trkpt: Option<Vec<GPXWaypoint>>,
361    // /// Custom extensions
362    // pub extensions: Option<MValue>,
363}
364impl GPXTrackSegment {
365    /// Creates a new GPXTrackSegment from an XMLTagItem
366    pub fn new(trkseg_xml: XMLTagItem) -> Self {
367        let inner = match &trkseg_xml {
368            XMLTagItem::XMLTag(tag) => tag.inner.clone().unwrap_or_default(),
369            XMLTagItem::String(s) => s.clone(),
370        };
371
372        let trkpt = {
373            let trkpt_tags = xml_find_tags_by_name(&inner, "trkpt", None);
374            if !trkpt_tags.is_empty() {
375                Some(
376                    trkpt_tags
377                        .into_iter()
378                        .map(|tag| GPXWaypoint::new(XMLTagItem::XMLTag(tag)))
379                        .collect(),
380                )
381            } else {
382                None
383            }
384        };
385
386        GPXTrackSegment { trkpt }
387    }
388}
389
390/// Represents a waypoint, point of interest, or named feature on a map.
391#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, MValueCompatible)]
392pub struct GPXWaypoint {
393    /// Latitude in decimal degrees (WGS84)
394    pub lat: f64,
395    /// Longitude in decimal degrees (WGS84)
396    pub lon: f64,
397    /// Optional elevation in meters
398    pub ele: Option<f64>,
399    /// Optional timestamp in ISO 8601 format
400    pub time: Option<String>,
401    /// Optional magnetic variation in degrees
402    pub magvar: Option<f64>,
403    /// Height of geoid above WGS84 ellipsoid
404    pub geoidheight: Option<f64>,
405    /// Waypoint name
406    pub name: Option<String>,
407    /// Waypoint comment
408    pub cmt: Option<String>,
409    /// Description of the waypoint
410    pub desc: Option<String>,
411    /// Source of data
412    pub src: Option<String>,
413    /// Links to additional information
414    pub link: Option<Vec<GPXLink>>,
415    /// Symbol name for the waypoint
416    pub sym: Option<String>,
417    /// Classification type of the waypoint
418    pub r#type: Option<String>,
419    /// Type of GPS fix
420    pub fix: Option<GPXFixType>,
421    /// Number of satellites used for the fix
422    pub sat: Option<usize>,
423    /// Horizontal dilution of precision
424    pub hdop: Option<f64>,
425    /// Vertical dilution of precision
426    pub vdop: Option<f64>,
427    /// Position dilution of precision
428    pub pdop: Option<f64>,
429    /// Time since last DGPS update in seconds
430    pub ageofdgpsdata: Option<f64>,
431    /// ID of DGPS station used
432    pub dgpsid: Option<f64>,
433    // /// Custom extensions
434    // pub extensions: Option<MValue>,
435}
436impl GPXWaypoint {
437    /// Creates a new GPXWaypoint from an XMLTagItem
438    pub fn new(waypoint_xml: XMLTagItem) -> Self {
439        let lat = xml_get_attribute(&waypoint_xml, "lat")
440            .and_then(|s| s.parse::<f64>().ok())
441            .unwrap_or(0.0);
442        let lon = xml_get_attribute(&waypoint_xml, "lon")
443            .and_then(|s| s.parse::<f64>().ok())
444            .unwrap_or(0.0);
445        let fix =
446            xml_get_attribute(&waypoint_xml, "fix").and_then(|s| s.parse::<GPXFixType>().ok());
447
448        let inner = match waypoint_xml {
449            XMLTagItem::XMLTag(tag) => tag.inner.unwrap_or_default(),
450            XMLTagItem::String(s) => s,
451        };
452
453        let ele = xml_find_tag_by_name(&inner, "ele", None)
454            .and_then(|tag| tag.inner.and_then(|s| s.parse::<f64>().ok()));
455        let time = xml_find_tag_by_name(&inner, "time", None).and_then(|tag| tag.inner);
456        let magvar = xml_find_tag_by_name(&inner, "magvar", None)
457            .and_then(|tag| tag.inner.and_then(|s| s.parse::<f64>().ok()));
458        let geoidheight = xml_find_tag_by_name(&inner, "geoidheight", None)
459            .and_then(|tag| tag.inner.and_then(|s| s.parse::<f64>().ok()));
460        let name = xml_find_tag_by_name(&inner, "name", None).and_then(|tag| tag.inner);
461        let cmt = xml_find_tag_by_name(&inner, "cmt", None).and_then(|tag| tag.inner);
462        let desc = xml_find_tag_by_name(&inner, "desc", None).and_then(|tag| tag.inner);
463        let src = xml_find_tag_by_name(&inner, "src", None).and_then(|tag| tag.inner);
464        let link = {
465            let link_tags = xml_find_tags_by_name(&inner, "link", None);
466            if !link_tags.is_empty() {
467                Some(
468                    link_tags
469                        .into_iter()
470                        .map(|tag| GPXLink::new(XMLTagItem::XMLTag(tag)))
471                        .collect(),
472                )
473            } else {
474                None
475            }
476        };
477        let sym = xml_find_tag_by_name(&inner, "sym", None).and_then(|tag| tag.inner);
478        let r#type = xml_find_tag_by_name(&inner, "type", None).and_then(|tag| tag.inner);
479        let sat = xml_find_tag_by_name(&inner, "sat", None)
480            .and_then(|tag| tag.inner.and_then(|s| s.parse::<usize>().ok()));
481        let hdop = xml_find_tag_by_name(&inner, "hdop", None)
482            .and_then(|tag| tag.inner.and_then(|s| s.parse::<f64>().ok()));
483        let vdop = xml_find_tag_by_name(&inner, "vdop", None)
484            .and_then(|tag| tag.inner.and_then(|s| s.parse::<f64>().ok()));
485        let pdop = xml_find_tag_by_name(&inner, "pdop", None)
486            .and_then(|tag| tag.inner.and_then(|s| s.parse::<f64>().ok()));
487        let ageofdgpsdata = xml_find_tag_by_name(&inner, "ageofdgpsdata", None)
488            .and_then(|tag| tag.inner.and_then(|s| s.parse::<f64>().ok()));
489        let dgpsid = xml_find_tag_by_name(&inner, "dgpsid", None)
490            .and_then(|tag| tag.inner.and_then(|s| s.parse::<f64>().ok()));
491
492        GPXWaypoint {
493            lat,
494            lon,
495            ele,
496            time,
497            magvar,
498            geoidheight,
499            name,
500            cmt,
501            desc,
502            src,
503            link,
504            sym,
505            r#type,
506            fix,
507            sat,
508            hdop,
509            vdop,
510            pdop,
511            ageofdgpsdata,
512            dgpsid,
513        }
514    }
515
516    /// Create a vector point from the Waypoint
517    pub fn point(&self) -> VectorPoint<GPXWaypoint> {
518        VectorPoint { x: self.lon, y: self.lat, z: self.ele, m: Some(self.clone()), t: None }
519    }
520
521    /// Create a vector point from the Waypoint
522    pub fn feature(&self) -> VectorFeature<(), GPXProperties, GPXWaypoint> {
523        VectorFeature {
524            _type: VectorFeatureType::VectorFeature,
525            geometry: VectorGeometry::new_point(self.point(), None),
526            ..Default::default()
527        }
528    }
529}
530
531/// Defines copyright and license information.
532#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
533pub struct GPXCopyright {
534    /// Copyright holder
535    pub author: String,
536    /// Year of copyright
537    pub year: Option<String>,
538    /// License URL
539    pub license: Option<String>,
540}
541impl GPXCopyright {
542    /// Creates a new GPXCopyright from an XMLTagItem
543    pub fn new(copyright_xml: XMLTagItem) -> Self {
544        let inner = match copyright_xml {
545            XMLTagItem::XMLTag(tag) => tag.inner.unwrap_or_default(),
546            XMLTagItem::String(s) => s,
547        };
548
549        let author = xml_find_tag_by_name(&inner, "author", None)
550            .and_then(|tag| tag.inner)
551            .unwrap_or_default();
552        let year = xml_find_tag_by_name(&inner, "year", None).and_then(|tag| tag.inner);
553        let license = xml_find_tag_by_name(&inner, "license", None).and_then(|tag| tag.inner);
554
555        GPXCopyright { author, year, license }
556    }
557}
558
559/// Represents a hyperlink with optional text and MIME type.
560#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, ValuePrimitive)]
561pub struct GPXLink {
562    /// URL of the link
563    pub href: String,
564    /// Optional hyperlink text
565    pub text: Option<String>,
566    /// MIME type of the linked content
567    pub r#type: Option<String>,
568}
569impl GPXLink {
570    /// Creates a new GPXLink from an XMLTagItem
571    pub fn new(link_xml: XMLTagItem) -> Self {
572        let href = xml_get_attribute(&link_xml, "href").unwrap_or_default();
573
574        let inner = match link_xml {
575            XMLTagItem::XMLTag(tag) => tag.inner.unwrap_or_default(),
576            XMLTagItem::String(s) => s,
577        };
578
579        let text = xml_find_tag_by_name(&inner, "text", None).and_then(|tag| tag.inner);
580        let r#type = xml_find_tag_by_name(&inner, "type", None).and_then(|tag| tag.inner);
581
582        GPXLink { href, text, r#type }
583    }
584}
585
586/// Defines a person or organization associated with the GPX file.
587#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
588pub struct GPXPerson {
589    /// Name of the person or organization
590    pub name: Option<String>,
591    /// Email address (split into ID and domain)
592    pub email: Option<GPXEmail>,
593    /// Link to external information about the person
594    pub link: Option<GPXLink>,
595}
596impl GPXPerson {
597    /// Creates a new GPXPerson from an XMLTagItem
598    pub fn new(person_xml: XMLTagItem) -> Self {
599        let inner = match person_xml {
600            XMLTagItem::XMLTag(tag) => tag.inner.unwrap_or_default(),
601            XMLTagItem::String(s) => s,
602        };
603
604        let name = xml_find_tag_by_name(&inner, "name", None).and_then(|tag| tag.inner);
605        let email = xml_find_tag_by_name(&inner, "email", None)
606            .map(|tag| GPXEmail::new(XMLTagItem::XMLTag(tag)));
607        let link = xml_find_tag_by_name(&inner, "link", None)
608            .map(|tag| GPXLink::new(XMLTagItem::XMLTag(tag)));
609
610        GPXPerson { name, email, link }
611    }
612}
613
614/// Represents an email address, split into ID and domain parts.
615#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
616pub struct GPXEmail {
617    /// Local part of the email address
618    pub id: String,
619    /// Domain part of the email address
620    pub domain: String,
621}
622impl GPXEmail {
623    /// Creates a new GPXEmail from an XMLTagItem
624    pub fn new(email_xml: XMLTagItem) -> Self {
625        let id = xml_get_attribute(&email_xml, "id").unwrap_or_default();
626        let domain = xml_get_attribute(&email_xml, "domain").unwrap_or_default();
627
628        GPXEmail { id, domain }
629    }
630}
631
632/// Defines the bounding box of the GPX data.
633#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
634pub struct GPXBounds {
635    /// Minimum latitude
636    pub minlat: f64,
637    /// Minimum longitude
638    pub minlon: f64,
639    /// Maximum latitude
640    pub maxlat: f64,
641    /// Maximum longitude
642    pub maxlon: f64,
643}
644impl GPXBounds {
645    /// Creates a new GPXBounds from an XMLTagItem
646    pub fn new(bounds_xml: XMLTagItem) -> Self {
647        let minlat = xml_get_attribute(&bounds_xml, "minlat")
648            .and_then(|s| s.parse::<f64>().ok())
649            .unwrap_or(0.0);
650        let minlon = xml_get_attribute(&bounds_xml, "minlon")
651            .and_then(|s| s.parse::<f64>().ok())
652            .unwrap_or(0.0);
653        let maxlat = xml_get_attribute(&bounds_xml, "maxlat")
654            .and_then(|s| s.parse::<f64>().ok())
655            .unwrap_or(0.0);
656        let maxlon = xml_get_attribute(&bounds_xml, "maxlon")
657            .and_then(|s| s.parse::<f64>().ok())
658            .unwrap_or(0.0);
659
660        GPXBounds { minlat, minlon, maxlat, maxlon }
661    }
662}
663
664/// Enumeration of GPS fix types.
665#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
666#[serde(rename_all = "lowercase")]
667pub enum GPXFixType {
668    /// No fix
669    #[default]
670    None,
671    /// 2D fix
672    #[serde(rename = "2d")]
673    D2,
674    /// 3D fix
675    #[serde(rename = "3d")]
676    D3,
677    /// Differential GPS
678    Dgps,
679    /// Precise Positioning System
680    Pps,
681}
682impl fmt::Display for GPXFixType {
683    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
684        let s = match self {
685            GPXFixType::None => "none",
686            GPXFixType::D2 => "2d",
687            GPXFixType::D3 => "3d",
688            GPXFixType::Dgps => "dgps",
689            GPXFixType::Pps => "pps",
690        };
691        write!(f, "{s}")
692    }
693}
694impl From<&str> for GPXFixType {
695    fn from(s: &str) -> Self {
696        match s {
697            "none" => GPXFixType::None,
698            "2d" => GPXFixType::D2,
699            "3d" => GPXFixType::D3,
700            "dgps" => GPXFixType::Dgps,
701            "pps" => GPXFixType::Pps,
702            _ => GPXFixType::None, // Default case for unrecognized strings
703        }
704    }
705}
706impl FromStr for GPXFixType {
707    type Err = ();
708
709    fn from_str(s: &str) -> Result<Self, Self::Err> {
710        Ok(GPXFixType::from(s))
711    }
712}
713impl TryFrom<String> for GPXFixType {
714    type Error = ();
715
716    fn try_from(s: String) -> Result<Self, Self::Error> {
717        GPXFixType::from_str(&s)
718    }
719}
720impl From<GPXFixType> for ValueType {
721    fn from(v: GPXFixType) -> Self {
722        ValueType::Primitive(PrimitiveValue::String(v.to_string()))
723    }
724}
725impl From<&ValueType> for GPXFixType {
726    fn from(v: &ValueType) -> Self {
727        match v {
728            ValueType::Primitive(PrimitiveValue::String(s)) => match s.to_lowercase().as_str() {
729                "none" => GPXFixType::None,
730                "2d" => GPXFixType::D2,
731                "3d" => GPXFixType::D3,
732                "dgps" => GPXFixType::Dgps,
733                "pps" => GPXFixType::Pps,
734                _ => GPXFixType::None,
735            },
736            _ => GPXFixType::None,
737        }
738    }
739}