mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-09 23:26:31 +00:00
fix link fields
This commit is contained in:
1 parent
42a55e6759
commit
83b76bb838
14 files changed
+338
-72
No files matched your search
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1" creator="test">
|
||||
<metadata>
|
||||
<name>with links</name>
|
||||
<author>
|
||||
<name>someone</name>
|
||||
<email id="someone" domain="example.com"/>
|
||||
<link href="https://example.com/author"><text>author</text></link>
|
||||
</author>
|
||||
<link href="https://example.com/file-1"><text>first</text></link>
|
||||
<link href="https://example.com/file-2"/>
|
||||
</metadata>
|
||||
<wpt lat="50" lon="4">
|
||||
<name>summit</name>
|
||||
<link href="https://example.com/wpt-1"><text>one</text></link>
|
||||
<link href="https://example.com/wpt-2"/>
|
||||
<link href="https://example.com/wpt-3"><text>three</text></link>
|
||||
</wpt>
|
||||
<trk>
|
||||
<name>track</name>
|
||||
<link href="https://example.com/trk-1"/>
|
||||
<link href="https://example.com/trk-2"><text>two</text></link>
|
||||
<trkseg>
|
||||
<trkpt lat="50" lon="4"/>
|
||||
<trkpt lat="50.1" lon="4.1"/>
|
||||
</trkseg>
|
||||
</trk>
|
||||
<rte>
|
||||
<name>route</name>
|
||||
<link href="https://example.com/rte-1"/>
|
||||
<link href="https://example.com/rte-2"/>
|
||||
<rtept lat="51" lon="5"/>
|
||||
<rtept lat="51.1" lon="5.1"/>
|
||||
</rte>
|
||||
<wpt lat="51" lon="5">
|
||||
<name>email variants</name>
|
||||
</wpt>
|
||||
</gpx>
|
||||
@@ -17,13 +17,15 @@ pub struct FileInfo {
|
||||
pub name: String,
|
||||
pub desc: Option<String>,
|
||||
pub author: Option<Author>,
|
||||
pub link: Option<Link>,
|
||||
pub links: Vec<Link>,
|
||||
pub time: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Author {
|
||||
pub name: Option<String>,
|
||||
/// `id@domain`, which the file gives as the two attributes of an `email` element.
|
||||
pub email: Option<String>,
|
||||
/// The author has a single link, unlike the other elements.
|
||||
pub link: Option<Link>,
|
||||
}
|
||||
@@ -17,7 +17,7 @@ pub struct TrackInfo {
|
||||
pub cmt: Option<String>,
|
||||
pub desc: Option<String>,
|
||||
pub src: Option<String>,
|
||||
pub link: Option<Link>,
|
||||
pub links: Vec<Link>,
|
||||
pub type_: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub opacity: Option<f64>,
|
||||
|
||||
@@ -13,7 +13,7 @@ pub struct Waypoint {
|
||||
pub name: Option<String>,
|
||||
pub cmt: Option<String>,
|
||||
pub desc: Option<String>,
|
||||
pub link: Option<Link>,
|
||||
pub links: Vec<Link>,
|
||||
pub sym: Option<String>,
|
||||
pub type_: Option<String>,
|
||||
}
|
||||
@@ -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_
|
||||
}
|
||||
|
||||
@@ -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<File, Error> {
|
||||
@@ -283,15 +324,8 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
|
||||
"desc" => 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<File,
|
||||
},
|
||||
// Self-closing points (`<trkpt lat=".." lon=".."/>`), 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<File,
|
||||
}
|
||||
"link" => {
|
||||
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::<Vec<_>>();
|
||||
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!("<gpx><metadata><author>{xml}</author></metadata></gpx>");
|
||||
parse(data.as_bytes(), &mut Default::default())
|
||||
.unwrap()
|
||||
.info
|
||||
.author
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(
|
||||
author(r#"<email id="a" domain="b.c"></email>"#)
|
||||
.email
|
||||
.as_deref(),
|
||||
Some("a@b.c")
|
||||
);
|
||||
assert_eq!(
|
||||
author(r#"<name>n</name><email id="a" domain="b.c"/>"#)
|
||||
.email
|
||||
.as_deref(),
|
||||
Some("a@b.c")
|
||||
);
|
||||
// an email that is not complete is not one
|
||||
assert_eq!(author(r#"<email id="a"/>"#).email, None);
|
||||
assert_eq!(author("<name>n</name>").email, None);
|
||||
// outside of an author it means nothing
|
||||
let gpx = parse(
|
||||
br#"<gpx><metadata><name>n</name><email id="a" domain="b.c"/></metadata></gpx>"#,
|
||||
&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
|
||||
|
||||
@@ -165,8 +165,8 @@ fn time(w: &mut XmlWriter, millis: Option<i64>) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn link(w: &mut XmlWriter, link: &Option<Link>) -> 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<Link>) -> 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::<Vec<_>>();
|
||||
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",
|
||||
|
||||
@@ -52,10 +52,16 @@ mod tests {
|
||||
desc: Some("old description".into()),
|
||||
cmt: Some("old comment".into()),
|
||||
sym: Some("Flag".into()),
|
||||
link: Some(Link {
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -46,7 +46,7 @@ pub struct FileInfo1 {
|
||||
name: String,
|
||||
desc: Option<String>,
|
||||
author: Option<Author1>,
|
||||
link: Option<Link1>,
|
||||
links: Vec<Link1>,
|
||||
time: Option<i64>,
|
||||
}
|
||||
|
||||
@@ -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<FileInfo1> 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<String>,
|
||||
desc: Option<String>,
|
||||
src: Option<String>,
|
||||
link: Option<Link1>,
|
||||
links: Vec<Link1>,
|
||||
type_: Option<String>,
|
||||
color: Option<String>,
|
||||
opacity: Option<f64>,
|
||||
@@ -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<TrackInfo1> 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<String>,
|
||||
cmt: Option<String>,
|
||||
desc: Option<String>,
|
||||
link: Option<Link1>,
|
||||
links: Vec<Link1>,
|
||||
sym: Option<String>,
|
||||
type_: Option<String>,
|
||||
}
|
||||
@@ -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<Waypoint1> 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_,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
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());
|
||||
}
|
||||
set(&object, "link", link_object);
|
||||
link_object.into()
|
||||
}),
|
||||
);
|
||||
}
|
||||
JsValue::from(object)
|
||||
})
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -46,9 +46,10 @@
|
||||
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw] gap-0">
|
||||
<Card.Header class="p-0 gap-0">
|
||||
<Card.Title class="text-md">
|
||||
{#if waypoint.item.link?.href}
|
||||
<a href={waypoint.item.link.href} target="_blank">
|
||||
{waypoint.item.name ?? waypoint.item.link.href}
|
||||
{@const link = waypoint.item.links?.[0]}
|
||||
{#if link?.href}
|
||||
<a href={link.href} target="_blank">
|
||||
{waypoint.item.name ?? link.href}
|
||||
<ExternalLink size="12" class="inline-block mb-1.5" />
|
||||
</a>
|
||||
{:else}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user