diff --git a/gpx-rs/engine/data/with_links.gpx b/gpx-rs/engine/data/with_links.gpx
new file mode 100644
index 000000000..a650cfd1d
--- /dev/null
+++ b/gpx-rs/engine/data/with_links.gpx
@@ -0,0 +1,38 @@
+
+
+
+ with links
+
+ someone
+
+ author
+
+ first
+
+
+
+ summit
+ one
+
+ three
+
+
+ track
+
+ two
+
+
+
+
+
+
+ route
+
+
+
+
+
+
+ email variants
+
+
diff --git a/gpx-rs/engine/src/core/gpx/file.rs b/gpx-rs/engine/src/core/gpx/file.rs
index f6baad9d4..fe458238c 100644
--- a/gpx-rs/engine/src/core/gpx/file.rs
+++ b/gpx-rs/engine/src/core/gpx/file.rs
@@ -17,13 +17,15 @@ pub struct FileInfo {
pub name: String,
pub desc: Option,
pub author: Option,
- pub link: Option,
+ pub links: Vec,
pub time: Option,
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Author {
pub name: Option,
+ /// `id@domain`, which the file gives as the two attributes of an `email` element.
pub email: Option,
+ /// The author has a single link, unlike the other elements.
pub link: Option,
}
diff --git a/gpx-rs/engine/src/core/gpx/track.rs b/gpx-rs/engine/src/core/gpx/track.rs
index fd3d4a4b0..1fc7dd612 100644
--- a/gpx-rs/engine/src/core/gpx/track.rs
+++ b/gpx-rs/engine/src/core/gpx/track.rs
@@ -17,7 +17,7 @@ pub struct TrackInfo {
pub cmt: Option,
pub desc: Option,
pub src: Option,
- pub link: Option,
+ pub links: Vec,
pub type_: Option,
pub color: Option,
pub opacity: Option,
diff --git a/gpx-rs/engine/src/core/gpx/waypoint.rs b/gpx-rs/engine/src/core/gpx/waypoint.rs
index ea7d67b32..1de687d82 100644
--- a/gpx-rs/engine/src/core/gpx/waypoint.rs
+++ b/gpx-rs/engine/src/core/gpx/waypoint.rs
@@ -13,7 +13,7 @@ pub struct Waypoint {
pub name: Option,
pub cmt: Option,
pub desc: Option,
- pub link: Option,
+ pub links: Vec,
pub sym: Option,
pub type_: Option,
}
@@ -28,7 +28,7 @@ impl Waypoint {
&& self.name == other.name
&& self.cmt == other.cmt
&& self.desc == other.desc
- && self.link == other.link
+ && self.links == other.links
&& self.sym == other.sym
&& self.type_ == other.type_
}
diff --git a/gpx-rs/engine/src/core/io/parse.rs b/gpx-rs/engine/src/core/io/parse.rs
index 5838a6b41..c5818056e 100644
--- a/gpx-rs/engine/src/core/io/parse.rs
+++ b/gpx-rs/engine/src/core/io/parse.rs
@@ -259,6 +259,47 @@ fn apply_text(
}
}
+/// The email of the author, given as the `id` and `domain` attributes of the element.
+fn set_email(stack: &mut [GPXElement], attributes: Attributes<'_>) {
+ let Some(GPXElement::Author(author)) = stack.last_mut() else {
+ return;
+ };
+ let (mut id, mut domain) = (None, None);
+ for attr in attributes.flatten() {
+ match attr.key.as_ref() {
+ "id" => id = Some(attr.value.to_string()),
+ "domain" => domain = Some(attr.value.to_string()),
+ _ => (),
+ }
+ }
+ if let (Some(id), Some(domain)) = (id, domain) {
+ author.email = Some(format!("{id}@{domain}"));
+ }
+}
+
+/// The link with the given `href`, if it is an attribute of the element.
+fn parse_link(attributes: Attributes<'_>) -> Link {
+ let mut link = Link::default();
+ for attr in attributes.flatten() {
+ if attr.key.as_ref() == "href" {
+ link.href = attr.value.to_string();
+ }
+ }
+ link
+}
+
+/// Gives a link to what holds it, which is at the top of the stack.
+fn attach_link(stack: &mut [GPXElement], gpx: &mut File, link: Link) {
+ match stack.last_mut() {
+ // the author has a single link, the other elements have as many as they want
+ Some(GPXElement::Author(author)) => author.link = Some(link),
+ Some(GPXElement::Track(trk)) => trk.info.links.push(link),
+ Some(GPXElement::Waypoint(wpt)) => wpt.links.push(link),
+ Some(GPXElement::Metadata) => gpx.info.links.push(link),
+ _ => (),
+ }
+}
+
/// Parses a GPX file. The surface, highway, SAC scale and MTB scale of the trackpoints are stored as codes of
/// `categories`, which learns the values it does not know yet.
pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result {
@@ -283,15 +324,8 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result stack.push(GPXElement::Description),
"src" => stack.push(GPXElement::Source),
"author" => stack.push(GPXElement::Author(Author::default())),
- "link" => {
- let mut link = Link::default();
- for attr in e.attributes().flatten() {
- if attr.key.as_ref() == "href" {
- link.href = attr.value.to_string();
- }
- }
- stack.push(GPXElement::Link(link));
- }
+ "link" => stack.push(GPXElement::Link(parse_link(e.attributes()))),
+ "email" => set_email(&mut stack, e.attributes()),
"text" => stack.push(GPXElement::Text),
"trk" | "rte" => stack.push(GPXElement::Track(Track::default())),
"trkseg" => {
@@ -332,6 +366,8 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result`), which have no children
Ok(Event::Empty(e)) => match e.name().as_ref() {
+ "link" => attach_link(&mut stack, &mut gpx, parse_link(e.attributes())),
+ "email" => set_email(&mut stack, e.attributes()),
"trkpt" => {
if let Some(GPXElement::Segment(trkseg)) = stack.last_mut() {
trkpt_chunk.trkpt.push(Trackpoint {
@@ -398,18 +434,7 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result {
if let Some(GPXElement::Link(link)) = stack.pop() {
- match stack.last_mut() {
- Some(GPXElement::Author(author)) => {
- author.link = Some(link);
- }
- Some(GPXElement::Track(trk)) => {
- trk.info.link = Some(link);
- }
- Some(GPXElement::Waypoint(wpt)) => {
- wpt.link = Some(link);
- }
- _ => (),
- }
+ attach_link(&mut stack, &mut gpx, link);
}
}
"trk" => {
@@ -594,8 +619,8 @@ mod tests {
.is_some_and(|d| d == "track description")
);
assert!(trk.info.src.as_ref().is_some_and(|s| s == "track source"));
- assert!(trk.info.link.is_some());
- let link = trk.info.link.as_ref().unwrap();
+ assert_eq!(trk.info.links.len(), 1);
+ let link = &trk.info.links[0];
assert_eq!(link.href, "https://gpx.studio");
assert!(link.text.as_ref().is_some_and(|t| t == "track link text"));
assert!(trk.info.type_.as_ref().is_some_and(|c| c == "Cycling"));
@@ -685,6 +710,86 @@ mod tests {
assert!(points[2].time.is_some());
}
+ #[test]
+ fn test_parse_every_link_and_the_email() {
+ let gpx = parse_data("with_links");
+
+ // the file has as many links as it says, self-closing or not
+ let hrefs = |links: &[Link]| links.iter().map(|l| l.href.clone()).collect::>();
+ assert_eq!(
+ hrefs(&gpx.info.links),
+ ["https://example.com/file-1", "https://example.com/file-2"]
+ );
+ assert_eq!(gpx.info.links[0].text.as_deref(), Some("first"));
+ assert_eq!(gpx.info.links[1].text, None);
+
+ // the author has one link and an email, which is given by two attributes
+ let author = gpx.info.author.as_ref().unwrap();
+ assert_eq!(author.name.as_deref(), Some("someone"));
+ assert_eq!(author.email.as_deref(), Some("someone@example.com"));
+ assert_eq!(
+ author.link.as_ref().unwrap().href,
+ "https://example.com/author"
+ );
+
+ let waypoints: Vec<_> = gpx.wpt.iter().collect();
+ assert_eq!(
+ hrefs(&waypoints[0].links),
+ [
+ "https://example.com/wpt-1",
+ "https://example.com/wpt-2",
+ "https://example.com/wpt-3"
+ ]
+ );
+ assert_eq!(waypoints[0].links[2].text.as_deref(), Some("three"));
+ assert!(waypoints[1].links.is_empty());
+
+ // tracks, and routes which are read as tracks
+ assert_eq!(gpx.trk.len(), 2);
+ assert_eq!(
+ hrefs(&gpx.trk[0].info.links),
+ ["https://example.com/trk-1", "https://example.com/trk-2"]
+ );
+ assert_eq!(
+ hrefs(&gpx.trk[1].info.links),
+ ["https://example.com/rte-1", "https://example.com/rte-2"]
+ );
+ }
+
+ #[test]
+ fn test_parse_email_variants() {
+ let author = |xml: &str| {
+ let data = format!("{xml}");
+ parse(data.as_bytes(), &mut Default::default())
+ .unwrap()
+ .info
+ .author
+ .unwrap()
+ };
+ assert_eq!(
+ author(r#""#)
+ .email
+ .as_deref(),
+ Some("a@b.c")
+ );
+ assert_eq!(
+ author(r#"n"#)
+ .email
+ .as_deref(),
+ Some("a@b.c")
+ );
+ // an email that is not complete is not one
+ assert_eq!(author(r#""#).email, None);
+ assert_eq!(author("n").email, None);
+ // outside of an author it means nothing
+ let gpx = parse(
+ br#"n"#,
+ &mut Default::default(),
+ )
+ .unwrap();
+ assert!(gpx.info.author.is_none());
+ }
+
#[test]
fn test_parse_segments() {
let gpx = parse_data("with_segments");
@@ -763,8 +868,8 @@ mod tests {
.as_ref()
.is_some_and(|d| d == "waypoint description")
);
- assert!(wpt.link.is_some());
- let link = wpt.link.as_ref().unwrap();
+ assert_eq!(wpt.links.len(), 1);
+ let link = &wpt.links[0];
assert_eq!(link.href, "https://gpx.studio");
assert!(
link.text
diff --git a/gpx-rs/engine/src/core/io/write.rs b/gpx-rs/engine/src/core/io/write.rs
index ec9d1cfee..7e21a5d86 100644
--- a/gpx-rs/engine/src/core/io/write.rs
+++ b/gpx-rs/engine/src/core/io/write.rs
@@ -165,8 +165,8 @@ fn time(w: &mut XmlWriter, millis: Option) -> io::Result<()> {
Ok(())
}
-fn link(w: &mut XmlWriter, link: &Option) -> io::Result<()> {
- if let Some(link) = link.as_ref().filter(|link| !link.href.is_empty()) {
+fn link(w: &mut XmlWriter, link: &Link) -> io::Result<()> {
+ if !link.href.is_empty() {
w.create_element("link")
.with_attribute(("href", link.href.as_str()))
.write_inner_content(|w| optional_text(w, "text", &link.text))?;
@@ -174,13 +174,17 @@ fn link(w: &mut XmlWriter, link: &Option) -> io::Result<()> {
Ok(())
}
+fn links(w: &mut XmlWriter, links: &[Link]) -> io::Result<()> {
+ links.iter().try_for_each(|l| link(w, l))
+}
+
fn write_metadata(w: &mut XmlWriter, file: &File, options: ExportOptions) -> io::Result<()> {
let info = &file.info;
let time = options.time.then_some(info.time).flatten();
if info.name.is_empty()
&& info.desc.is_none()
&& info.author.is_none()
- && info.link.is_none()
+ && info.links.is_empty()
&& time.is_none()
{
return Ok(());
@@ -197,10 +201,10 @@ fn write_metadata(w: &mut XmlWriter, file: &File, options: ExportOptions) -> io:
.with_attributes([("id", id), ("domain", domain)])
.write_empty()?;
}
- link(w, &author.link)
+ author.link.iter().try_for_each(|l| link(w, l))
})?;
}
- link(w, &info.link)?;
+ links(w, &info.links)?;
self::time(w, time)
})?;
Ok(())
@@ -226,7 +230,7 @@ fn write_waypoint(w: &mut XmlWriter, wpt: &Waypoint, options: ExportOptions) ->
optional_text(w, "name", &wpt.name)?;
optional_text(w, "cmt", &wpt.cmt)?;
optional_text(w, "desc", &wpt.desc)?;
- link(w, &wpt.link)?;
+ links(w, &wpt.links)?;
optional_text(w, "sym", &wpt.sym)?;
optional_text(w, "type", &wpt.type_)
})?;
@@ -246,7 +250,7 @@ fn write_track(
optional_text(w, "cmt", &info.cmt)?;
optional_text(w, "desc", &info.desc)?;
optional_text(w, "src", &info.src)?;
- link(w, &info.link)?;
+ links(w, &info.links)?;
optional_text(w, "type", &info.type_)?;
if info.color.is_some() || info.opacity.is_some() || info.width.is_some() {
w.create_element("extensions").write_inner_content(|w| {
@@ -400,10 +404,37 @@ mod tests {
}
}
+ #[test]
+ fn test_links_and_the_email_are_written() {
+ let data = std::fs::read("data/with_links.gpx").unwrap();
+ let file = parse(&data, &mut Default::default()).unwrap();
+ let again = parse(
+ &write(&file, &Default::default(), ExportOptions::ALL),
+ &mut Default::default(),
+ )
+ .unwrap();
+
+ assert_eq!(again.info.links, file.info.links);
+ assert_eq!(again.info.links.len(), 2);
+ assert_eq!(again.info.author, file.info.author);
+ assert_eq!(
+ again.info.author.as_ref().unwrap().email.as_deref(),
+ Some("someone@example.com")
+ );
+ let waypoints = |file: &File| file.wpt.iter().map(|w| w.links.clone()).collect::>();
+ assert_eq!(waypoints(&again), waypoints(&file));
+ assert_eq!(again.wpt.iter().next().unwrap().links.len(), 3);
+ for (a, b) in again.trk.iter().zip(&file.trk) {
+ assert_eq!(a.info.links, b.info.links);
+ assert_eq!(a.info.links.len(), 2);
+ }
+ }
+
#[test]
fn test_files_survive_a_round_trip() {
for name in [
"simple",
+ "with_links",
"with_time",
"with_hr",
"with_cad",
diff --git a/gpx-rs/engine/src/engine/command/tools/edit_waypoint.rs b/gpx-rs/engine/src/engine/command/tools/edit_waypoint.rs
index 7828907bc..9a08a3d67 100644
--- a/gpx-rs/engine/src/engine/command/tools/edit_waypoint.rs
+++ b/gpx-rs/engine/src/engine/command/tools/edit_waypoint.rs
@@ -52,10 +52,16 @@ mod tests {
desc: Some("old description".into()),
cmt: Some("old comment".into()),
sym: Some("Flag".into()),
- link: Some(Link {
- href: "https://old".into(),
- text: None,
- }),
+ links: vec![
+ Link {
+ href: "https://old".into(),
+ text: None,
+ },
+ Link {
+ href: "https://second".into(),
+ text: Some("second".into()),
+ },
+ ],
..Default::default()
},
Waypoint {
@@ -90,8 +96,9 @@ mod tests {
assert_eq!(edited.desc.as_deref(), Some("new description"));
assert_eq!(edited.cmt.as_deref(), Some("new description"));
assert_eq!(edited.sym.as_deref(), Some("Summit"));
- // an empty link removes it
- assert!(edited.link.is_none());
+ // an empty link removes the first one, the others are not the form's business
+ assert_eq!(edited.links.len(), 1);
+ assert_eq!(edited.links[0].href, "https://second");
assert_eq!(
(edited.coordinates.lng, edited.coordinates.lat, edited.ele),
(4.5, 50.5, 12.0)
@@ -99,4 +106,57 @@ mod tests {
// the other waypoint is as it was
assert_eq!(wpts[1].name.as_deref(), Some("other"));
}
+
+ #[test]
+ fn test_the_link_of_the_form_is_the_first_one() {
+ let mut fx = Fixture::default();
+ let mut file = File::default();
+ file.wpt.push(WaypointChunk {
+ wpt: vec![Waypoint {
+ links: vec![
+ Link {
+ href: "https://first".into(),
+ text: Some("first".into()),
+ },
+ Link {
+ href: "https://second".into(),
+ text: None,
+ },
+ ],
+ ..Default::default()
+ }],
+ ..Default::default()
+ });
+ let (id, wpt_id) = (file.id, waypoint_ids(&file).next().unwrap());
+ fx.files.insert(id, Rc::new(file));
+ let edit = |fx: &mut Fixture, link: &str| {
+ EditWaypoint {
+ file_id: id,
+ waypoint_id: wpt_id,
+ lng: 0.0,
+ lat: 0.0,
+ ele: 0.0,
+ name: "",
+ desc: "",
+ icon: "",
+ link,
+ }
+ .apply(&mut fx.state())
+ .unwrap();
+ fx.files[&id].wpt.iter().next().unwrap().links.clone()
+ };
+
+ // the same link keeps its text
+ let links = edit(&mut fx, "https://first");
+ assert_eq!(links.len(), 2);
+ assert_eq!(links[0].text.as_deref(), Some("first"));
+ // another one replaces the first, and the text that described the old one
+ let links = edit(&mut fx, "https://other");
+ assert_eq!(links.len(), 2);
+ assert_eq!(
+ (links[0].href.as_str(), links[0].text.clone()),
+ ("https://other", None)
+ );
+ assert_eq!(links[1].href, "https://second");
+ }
}
diff --git a/gpx-rs/engine/src/engine/command/tools/new_waypoint.rs b/gpx-rs/engine/src/engine/command/tools/new_waypoint.rs
index 599347e01..586747c9f 100644
--- a/gpx-rs/engine/src/engine/command/tools/new_waypoint.rs
+++ b/gpx-rs/engine/src/engine/command/tools/new_waypoint.rs
@@ -36,7 +36,15 @@ pub fn set_waypoint_fields(
// the description is also the comment, as it is what the other applications show
waypoint.cmt = non_empty(desc);
waypoint.sym = non_empty(icon);
- waypoint.link = non_empty(link).map(|href| Link { href, text: None });
+ // the form has a single link: it is the first one, the others are left as they are
+ match (non_empty(link), waypoint.links.first_mut()) {
+ (Some(href), Some(first)) if first.href != href => *first = Link { href, text: None },
+ (Some(href), None) => waypoint.links.push(Link { href, text: None }),
+ (None, Some(_)) => {
+ waypoint.links.remove(0);
+ }
+ _ => (),
+ }
}
impl Apply for NewWaypoint<'_> {
@@ -137,7 +145,7 @@ mod tests {
assert_eq!(wpt.cmt.as_deref(), Some("a description"));
assert_eq!(wpt.sym.as_deref(), Some("Summit"));
assert_eq!(
- wpt.link.as_ref().map(|l| l.href.as_str()),
+ wpt.links.first().map(|l| l.href.as_str()),
Some("https://example.com")
);
assert_ne!(fx.files[&b].wpt.rev_id, rev);
@@ -178,7 +186,7 @@ mod tests {
.unwrap();
let wpt = &waypoints(&fx, a)[0];
assert!(wpt.name.is_none() && wpt.desc.is_none() && wpt.cmt.is_none());
- assert!(wpt.sym.is_none() && wpt.link.is_none());
+ assert!(wpt.sym.is_none() && wpt.links.is_empty());
}
#[test]
diff --git a/gpx-rs/engine/src/storage/codec/mod.rs b/gpx-rs/engine/src/storage/codec/mod.rs
index 187c68cd0..3a3797344 100644
--- a/gpx-rs/engine/src/storage/codec/mod.rs
+++ b/gpx-rs/engine/src/storage/codec/mod.rs
@@ -266,7 +266,10 @@ mod tests {
name: Some("summit".into()),
cmt: Some("comment".into()),
desc: Some("description".into()),
- link: Some(link("https://example.com/summit")),
+ links: vec![
+ link("https://example.com/summit"),
+ link("https://example.com/summit-2"),
+ ],
sym: Some("Summit".into()),
type_: Some("peak".into()),
}
@@ -284,7 +287,10 @@ mod tests {
email: Some("someone@example.com".into()),
link: Some(link("https://example.com/someone")),
}),
- link: Some(link("https://example.com/file")),
+ links: vec![
+ link("https://example.com/file"),
+ link("https://example.com/file-2"),
+ ],
time: Some(1_700_000_000_000),
},
..Default::default()
@@ -304,7 +310,10 @@ mod tests {
cmt: Some("cmt".into()),
desc: Some("desc".into()),
src: Some("src".into()),
- link: Some(link("https://example.com/track")),
+ links: vec![
+ link("https://example.com/track"),
+ link("https://example.com/track-2"),
+ ],
type_: Some("Cycling".into()),
color: Some("ff0000".into()),
opacity: Some(0.5),
@@ -508,13 +517,16 @@ mod tests {
const FILE_V1: &str = concat!(
"010673616d706c65010561626f7574010107736f6d656f6e650113736f6d656f6e65406578616d706c652e63",
"6f6d011b68747470733a2f2f6578616d706c652e636f6d2f736f6d656f6e65012374657874206f6620687474",
- "70733a2f2f6578616d706c652e636f6d2f736f6d656f6e65011868747470733a2f2f6578616d706c652e636f",
- "6d2f66696c65012074657874206f662068747470733a2f2f6578616d706c652e636f6d2f66696c650180a0ab",
- "fef9620110000000000000000000000000000000020105747261636b0103636d740104646573630103737263",
- "011968747470733a2f2f6578616d706c652e636f6d2f747261636b012174657874206f662068747470733a2f",
- "2f6578616d706c652e636f6d2f747261636b01074379636c696e67010666663030303001000000000000e03f",
- "0100000000000018400210000000000000000000000000000000030110000000000000000000000000000000",
- "04100000000000000000000000000000000600011000000000000000000000000000000005",
+ "70733a2f2f6578616d706c652e636f6d2f736f6d656f6e65021868747470733a2f2f6578616d706c652e636f",
+ "6d2f66696c65012074657874206f662068747470733a2f2f6578616d706c652e636f6d2f66696c651a687474",
+ "70733a2f2f6578616d706c652e636f6d2f66696c652d32012274657874206f662068747470733a2f2f657861",
+ "6d706c652e636f6d2f66696c652d320180a0abfef96201100000000000000000000000000000000201057472",
+ "61636b0103636d740104646573630103737263021968747470733a2f2f6578616d706c652e636f6d2f747261",
+ "636b012174657874206f662068747470733a2f2f6578616d706c652e636f6d2f747261636b1b68747470733a",
+ "2f2f6578616d706c652e636f6d2f747261636b2d32012374657874206f662068747470733a2f2f6578616d70",
+ "6c652e636f6d2f747261636b2d3201074379636c696e67010666663030303001000000000000e03f01000000",
+ "0000001840021000000000000000000000000000000003011000000000000000000000000000000004100000",
+ "000000000000000000000000000600011000000000000000000000000000000005",
);
const TRACKPOINTS_V1: &str = concat!(
"01030000000000001040000000000000494000000000002059400180a0abfef9620105018c01015001fa0101",
@@ -524,10 +536,12 @@ mod tests {
);
const WAYPOINTS_V1: &str = concat!(
"0102100000000000000000000000000000000700000000000016400000000000c04940000000000010744001",
- "80a0abfef962010673756d6d69740107636f6d6d656e74010b6465736372697074696f6e011a68747470733a",
+ "80a0abfef962010673756d6d69740107636f6d6d656e74010b6465736372697074696f6e021a68747470733a",
"2f2f6578616d706c652e636f6d2f73756d6d6974012274657874206f662068747470733a2f2f6578616d706c",
- "652e636f6d2f73756d6d6974010653756d6d697401047065616b100000000000000000000000000000000800",
- "000000000000000000000000000000000000000000000000000000000000",
+ "652e636f6d2f73756d6d69741c68747470733a2f2f6578616d706c652e636f6d2f73756d6d69742d32012474",
+ "657874206f662068747470733a2f2f6578616d706c652e636f6d2f73756d6d69742d32010653756d6d697401",
+ "047065616b100000000000000000000000000000000800000000000000000000000000000000000000000000",
+ "000000000000000000",
);
const ORDER_V1: &str =
"010210000000000000000000000000000000091000000000000000000000000000000008";
diff --git a/gpx-rs/engine/src/storage/codec/v1.rs b/gpx-rs/engine/src/storage/codec/v1.rs
index 31a1541b4..82a72e202 100644
--- a/gpx-rs/engine/src/storage/codec/v1.rs
+++ b/gpx-rs/engine/src/storage/codec/v1.rs
@@ -46,7 +46,7 @@ pub struct FileInfo1 {
name: String,
desc: Option,
author: Option,
- link: Option,
+ links: Vec,
time: Option,
}
@@ -60,7 +60,7 @@ impl From<&FileInfo> for FileInfo1 {
email: author.email.clone(),
link: author.link.as_ref().map(Into::into),
}),
- link: info.link.as_ref().map(Into::into),
+ links: info.links.iter().map(Into::into).collect(),
time: info.time,
}
}
@@ -76,7 +76,7 @@ impl From for FileInfo {
email: author.email,
link: author.link.map(Into::into),
}),
- link: info.link.map(Into::into),
+ links: info.links.into_iter().map(Into::into).collect(),
time: info.time,
}
}
@@ -88,7 +88,7 @@ pub struct TrackInfo1 {
cmt: Option,
desc: Option,
src: Option,
- link: Option,
+ links: Vec,
type_: Option,
color: Option,
opacity: Option,
@@ -102,7 +102,7 @@ impl From<&TrackInfo> for TrackInfo1 {
cmt: info.cmt.clone(),
desc: info.desc.clone(),
src: info.src.clone(),
- link: info.link.as_ref().map(Into::into),
+ links: info.links.iter().map(Into::into).collect(),
type_: info.type_.clone(),
color: info.color.clone(),
opacity: info.opacity,
@@ -118,7 +118,7 @@ impl From for TrackInfo {
cmt: info.cmt,
desc: info.desc,
src: info.src,
- link: info.link.map(Into::into),
+ links: info.links.into_iter().map(Into::into).collect(),
type_: info.type_,
color: info.color,
opacity: info.opacity,
@@ -242,7 +242,7 @@ pub struct Waypoint1 {
name: Option,
cmt: Option,
desc: Option,
- link: Option,
+ links: Vec,
sym: Option,
type_: Option,
}
@@ -258,7 +258,7 @@ impl From<&Waypoint> for Waypoint1 {
name: waypoint.name.clone(),
cmt: waypoint.cmt.clone(),
desc: waypoint.desc.clone(),
- link: waypoint.link.as_ref().map(Into::into),
+ links: waypoint.links.iter().map(Into::into).collect(),
sym: waypoint.sym.clone(),
type_: waypoint.type_.clone(),
}
@@ -278,7 +278,7 @@ impl From for Waypoint {
name: waypoint.name,
cmt: waypoint.cmt,
desc: waypoint.desc,
- link: waypoint.link.map(Into::into),
+ links: waypoint.links.into_iter().map(Into::into).collect(),
sym: waypoint.sym,
type_: waypoint.type_,
}
diff --git a/gpx-rs/wasm/src/queries/files.rs b/gpx-rs/wasm/src/queries/files.rs
index bc26113ff..7214d0966 100644
--- a/gpx-rs/wasm/src/queries/files.rs
+++ b/gpx-rs/wasm/src/queries/files.rs
@@ -143,13 +143,19 @@ pub fn waypoint(file_id: &str, waypoint_id: &str) -> WaypointDetails {
set(&object, key, value.as_str());
}
}
- if let Some(link) = &wpt.link {
- let link_object = Object::new();
- set(&link_object, "href", link.href.as_str());
- if let Some(text) = &link.text {
- set(&link_object, "text", text.as_str());
- }
- set(&object, "link", link_object);
+ if !wpt.links.is_empty() {
+ set(
+ &object,
+ "links",
+ array(&wpt.links, |link| {
+ let link_object = Object::new();
+ set(&link_object, "href", link.href.as_str());
+ if let Some(text) = &link.text {
+ set(&link_object, "text", text.as_str());
+ }
+ link_object.into()
+ }),
+ );
}
JsValue::from(object)
})
diff --git a/gpx-rs/wasm/src/ts.rs b/gpx-rs/wasm/src/ts.rs
index aa00552ea..e05816f47 100644
--- a/gpx-rs/wasm/src/ts.rs
+++ b/gpx-rs/wasm/src/ts.rs
@@ -52,7 +52,8 @@ export interface WaypointDetails {
name?: string;
desc?: string;
cmt?: string;
- link?: { href: string; text?: string };
+ /** The links of the waypoint, absent when it has none. */
+ links?: { href: string; text?: string }[];
sym?: string;
}
/** The position, elevation and time of a trackpoint. */
diff --git a/website/src/lib/components/map/gpx-layer/WaypointPopup.svelte b/website/src/lib/components/map/gpx-layer/WaypointPopup.svelte
index 02cee6692..1319fafab 100644
--- a/website/src/lib/components/map/gpx-layer/WaypointPopup.svelte
+++ b/website/src/lib/components/map/gpx-layer/WaypointPopup.svelte
@@ -46,9 +46,10 @@
- {#if waypoint.item.link?.href}
-
- {waypoint.item.name ?? waypoint.item.link.href}
+ {@const link = waypoint.item.links?.[0]}
+ {#if link?.href}
+
+ {waypoint.item.name ?? link.href}
{:else}
diff --git a/website/src/lib/components/toolbar/tools/waypoint/Waypoint.svelte b/website/src/lib/components/toolbar/tools/waypoint/Waypoint.svelte
index c20457a6a..8b94adb69 100644
--- a/website/src/lib/components/toolbar/tools/waypoint/Waypoint.svelte
+++ b/website/src/lib/components/toolbar/tools/waypoint/Waypoint.svelte
@@ -67,7 +67,7 @@
if (wpt.cmt !== undefined && wpt.cmt !== wpt.desc) {
description += '\n\n' + wpt.cmt;
}
- link = wpt.link?.href ?? '';
+ link = wpt.links?.[0]?.href ?? '';
sym = wpt.sym ?? '';
longitude = parseFloat(wpt.lng.toFixed(6));
latitude = parseFloat(wpt.lat.toFixed(6));