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 name: String,
|
||||||
pub desc: Option<String>,
|
pub desc: Option<String>,
|
||||||
pub author: Option<Author>,
|
pub author: Option<Author>,
|
||||||
pub link: Option<Link>,
|
pub links: Vec<Link>,
|
||||||
pub time: Option<i64>,
|
pub time: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq)]
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
pub struct Author {
|
pub struct Author {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
/// `id@domain`, which the file gives as the two attributes of an `email` element.
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
|
/// The author has a single link, unlike the other elements.
|
||||||
pub link: Option<Link>,
|
pub link: Option<Link>,
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ pub struct TrackInfo {
|
|||||||
pub cmt: Option<String>,
|
pub cmt: Option<String>,
|
||||||
pub desc: Option<String>,
|
pub desc: Option<String>,
|
||||||
pub src: Option<String>,
|
pub src: Option<String>,
|
||||||
pub link: Option<Link>,
|
pub links: Vec<Link>,
|
||||||
pub type_: Option<String>,
|
pub type_: Option<String>,
|
||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
pub opacity: Option<f64>,
|
pub opacity: Option<f64>,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ pub struct Waypoint {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub cmt: Option<String>,
|
pub cmt: Option<String>,
|
||||||
pub desc: Option<String>,
|
pub desc: Option<String>,
|
||||||
pub link: Option<Link>,
|
pub links: Vec<Link>,
|
||||||
pub sym: Option<String>,
|
pub sym: Option<String>,
|
||||||
pub type_: Option<String>,
|
pub type_: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ impl Waypoint {
|
|||||||
&& self.name == other.name
|
&& self.name == other.name
|
||||||
&& self.cmt == other.cmt
|
&& self.cmt == other.cmt
|
||||||
&& self.desc == other.desc
|
&& self.desc == other.desc
|
||||||
&& self.link == other.link
|
&& self.links == other.links
|
||||||
&& self.sym == other.sym
|
&& self.sym == other.sym
|
||||||
&& self.type_ == other.type_
|
&& 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
|
/// 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.
|
/// `categories`, which learns the values it does not know yet.
|
||||||
pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File, Error> {
|
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),
|
"desc" => stack.push(GPXElement::Description),
|
||||||
"src" => stack.push(GPXElement::Source),
|
"src" => stack.push(GPXElement::Source),
|
||||||
"author" => stack.push(GPXElement::Author(Author::default())),
|
"author" => stack.push(GPXElement::Author(Author::default())),
|
||||||
"link" => {
|
"link" => stack.push(GPXElement::Link(parse_link(e.attributes()))),
|
||||||
let mut link = Link::default();
|
"email" => set_email(&mut stack, e.attributes()),
|
||||||
for attr in e.attributes().flatten() {
|
|
||||||
if attr.key.as_ref() == "href" {
|
|
||||||
link.href = attr.value.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stack.push(GPXElement::Link(link));
|
|
||||||
}
|
|
||||||
"text" => stack.push(GPXElement::Text),
|
"text" => stack.push(GPXElement::Text),
|
||||||
"trk" | "rte" => stack.push(GPXElement::Track(Track::default())),
|
"trk" | "rte" => stack.push(GPXElement::Track(Track::default())),
|
||||||
"trkseg" => {
|
"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
|
// Self-closing points (`<trkpt lat=".." lon=".."/>`), which have no children
|
||||||
Ok(Event::Empty(e)) => match e.name().as_ref() {
|
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" => {
|
"trkpt" => {
|
||||||
if let Some(GPXElement::Segment(trkseg)) = stack.last_mut() {
|
if let Some(GPXElement::Segment(trkseg)) = stack.last_mut() {
|
||||||
trkpt_chunk.trkpt.push(Trackpoint {
|
trkpt_chunk.trkpt.push(Trackpoint {
|
||||||
@@ -398,18 +434,7 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
|
|||||||
}
|
}
|
||||||
"link" => {
|
"link" => {
|
||||||
if let Some(GPXElement::Link(link)) = stack.pop() {
|
if let Some(GPXElement::Link(link)) = stack.pop() {
|
||||||
match stack.last_mut() {
|
attach_link(&mut stack, &mut gpx, link);
|
||||||
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);
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"trk" => {
|
"trk" => {
|
||||||
@@ -594,8 +619,8 @@ mod tests {
|
|||||||
.is_some_and(|d| d == "track description")
|
.is_some_and(|d| d == "track description")
|
||||||
);
|
);
|
||||||
assert!(trk.info.src.as_ref().is_some_and(|s| s == "track source"));
|
assert!(trk.info.src.as_ref().is_some_and(|s| s == "track source"));
|
||||||
assert!(trk.info.link.is_some());
|
assert_eq!(trk.info.links.len(), 1);
|
||||||
let link = trk.info.link.as_ref().unwrap();
|
let link = &trk.info.links[0];
|
||||||
assert_eq!(link.href, "https://gpx.studio");
|
assert_eq!(link.href, "https://gpx.studio");
|
||||||
assert!(link.text.as_ref().is_some_and(|t| t == "track link text"));
|
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"));
|
assert!(trk.info.type_.as_ref().is_some_and(|c| c == "Cycling"));
|
||||||
@@ -685,6 +710,86 @@ mod tests {
|
|||||||
assert!(points[2].time.is_some());
|
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]
|
#[test]
|
||||||
fn test_parse_segments() {
|
fn test_parse_segments() {
|
||||||
let gpx = parse_data("with_segments");
|
let gpx = parse_data("with_segments");
|
||||||
@@ -763,8 +868,8 @@ mod tests {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|d| d == "waypoint description")
|
.is_some_and(|d| d == "waypoint description")
|
||||||
);
|
);
|
||||||
assert!(wpt.link.is_some());
|
assert_eq!(wpt.links.len(), 1);
|
||||||
let link = wpt.link.as_ref().unwrap();
|
let link = &wpt.links[0];
|
||||||
assert_eq!(link.href, "https://gpx.studio");
|
assert_eq!(link.href, "https://gpx.studio");
|
||||||
assert!(
|
assert!(
|
||||||
link.text
|
link.text
|
||||||
|
|||||||
@@ -165,8 +165,8 @@ fn time(w: &mut XmlWriter, millis: Option<i64>) -> io::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn link(w: &mut XmlWriter, link: &Option<Link>) -> io::Result<()> {
|
fn link(w: &mut XmlWriter, link: &Link) -> io::Result<()> {
|
||||||
if let Some(link) = link.as_ref().filter(|link| !link.href.is_empty()) {
|
if !link.href.is_empty() {
|
||||||
w.create_element("link")
|
w.create_element("link")
|
||||||
.with_attribute(("href", link.href.as_str()))
|
.with_attribute(("href", link.href.as_str()))
|
||||||
.write_inner_content(|w| optional_text(w, "text", &link.text))?;
|
.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(())
|
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<()> {
|
fn write_metadata(w: &mut XmlWriter, file: &File, options: ExportOptions) -> io::Result<()> {
|
||||||
let info = &file.info;
|
let info = &file.info;
|
||||||
let time = options.time.then_some(info.time).flatten();
|
let time = options.time.then_some(info.time).flatten();
|
||||||
if info.name.is_empty()
|
if info.name.is_empty()
|
||||||
&& info.desc.is_none()
|
&& info.desc.is_none()
|
||||||
&& info.author.is_none()
|
&& info.author.is_none()
|
||||||
&& info.link.is_none()
|
&& info.links.is_empty()
|
||||||
&& time.is_none()
|
&& time.is_none()
|
||||||
{
|
{
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -197,10 +201,10 @@ fn write_metadata(w: &mut XmlWriter, file: &File, options: ExportOptions) -> io:
|
|||||||
.with_attributes([("id", id), ("domain", domain)])
|
.with_attributes([("id", id), ("domain", domain)])
|
||||||
.write_empty()?;
|
.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)
|
self::time(w, time)
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -226,7 +230,7 @@ fn write_waypoint(w: &mut XmlWriter, wpt: &Waypoint, options: ExportOptions) ->
|
|||||||
optional_text(w, "name", &wpt.name)?;
|
optional_text(w, "name", &wpt.name)?;
|
||||||
optional_text(w, "cmt", &wpt.cmt)?;
|
optional_text(w, "cmt", &wpt.cmt)?;
|
||||||
optional_text(w, "desc", &wpt.desc)?;
|
optional_text(w, "desc", &wpt.desc)?;
|
||||||
link(w, &wpt.link)?;
|
links(w, &wpt.links)?;
|
||||||
optional_text(w, "sym", &wpt.sym)?;
|
optional_text(w, "sym", &wpt.sym)?;
|
||||||
optional_text(w, "type", &wpt.type_)
|
optional_text(w, "type", &wpt.type_)
|
||||||
})?;
|
})?;
|
||||||
@@ -246,7 +250,7 @@ fn write_track(
|
|||||||
optional_text(w, "cmt", &info.cmt)?;
|
optional_text(w, "cmt", &info.cmt)?;
|
||||||
optional_text(w, "desc", &info.desc)?;
|
optional_text(w, "desc", &info.desc)?;
|
||||||
optional_text(w, "src", &info.src)?;
|
optional_text(w, "src", &info.src)?;
|
||||||
link(w, &info.link)?;
|
links(w, &info.links)?;
|
||||||
optional_text(w, "type", &info.type_)?;
|
optional_text(w, "type", &info.type_)?;
|
||||||
if info.color.is_some() || info.opacity.is_some() || info.width.is_some() {
|
if info.color.is_some() || info.opacity.is_some() || info.width.is_some() {
|
||||||
w.create_element("extensions").write_inner_content(|w| {
|
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]
|
#[test]
|
||||||
fn test_files_survive_a_round_trip() {
|
fn test_files_survive_a_round_trip() {
|
||||||
for name in [
|
for name in [
|
||||||
"simple",
|
"simple",
|
||||||
|
"with_links",
|
||||||
"with_time",
|
"with_time",
|
||||||
"with_hr",
|
"with_hr",
|
||||||
"with_cad",
|
"with_cad",
|
||||||
|
|||||||
@@ -52,10 +52,16 @@ mod tests {
|
|||||||
desc: Some("old description".into()),
|
desc: Some("old description".into()),
|
||||||
cmt: Some("old comment".into()),
|
cmt: Some("old comment".into()),
|
||||||
sym: Some("Flag".into()),
|
sym: Some("Flag".into()),
|
||||||
link: Some(Link {
|
links: vec![
|
||||||
|
Link {
|
||||||
href: "https://old".into(),
|
href: "https://old".into(),
|
||||||
text: None,
|
text: None,
|
||||||
}),
|
},
|
||||||
|
Link {
|
||||||
|
href: "https://second".into(),
|
||||||
|
text: Some("second".into()),
|
||||||
|
},
|
||||||
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Waypoint {
|
Waypoint {
|
||||||
@@ -90,8 +96,9 @@ mod tests {
|
|||||||
assert_eq!(edited.desc.as_deref(), Some("new description"));
|
assert_eq!(edited.desc.as_deref(), Some("new description"));
|
||||||
assert_eq!(edited.cmt.as_deref(), Some("new description"));
|
assert_eq!(edited.cmt.as_deref(), Some("new description"));
|
||||||
assert_eq!(edited.sym.as_deref(), Some("Summit"));
|
assert_eq!(edited.sym.as_deref(), Some("Summit"));
|
||||||
// an empty link removes it
|
// an empty link removes the first one, the others are not the form's business
|
||||||
assert!(edited.link.is_none());
|
assert_eq!(edited.links.len(), 1);
|
||||||
|
assert_eq!(edited.links[0].href, "https://second");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(edited.coordinates.lng, edited.coordinates.lat, edited.ele),
|
(edited.coordinates.lng, edited.coordinates.lat, edited.ele),
|
||||||
(4.5, 50.5, 12.0)
|
(4.5, 50.5, 12.0)
|
||||||
@@ -99,4 +106,57 @@ mod tests {
|
|||||||
// the other waypoint is as it was
|
// the other waypoint is as it was
|
||||||
assert_eq!(wpts[1].name.as_deref(), Some("other"));
|
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
|
// the description is also the comment, as it is what the other applications show
|
||||||
waypoint.cmt = non_empty(desc);
|
waypoint.cmt = non_empty(desc);
|
||||||
waypoint.sym = non_empty(icon);
|
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<'_> {
|
impl Apply for NewWaypoint<'_> {
|
||||||
@@ -137,7 +145,7 @@ mod tests {
|
|||||||
assert_eq!(wpt.cmt.as_deref(), Some("a description"));
|
assert_eq!(wpt.cmt.as_deref(), Some("a description"));
|
||||||
assert_eq!(wpt.sym.as_deref(), Some("Summit"));
|
assert_eq!(wpt.sym.as_deref(), Some("Summit"));
|
||||||
assert_eq!(
|
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")
|
Some("https://example.com")
|
||||||
);
|
);
|
||||||
assert_ne!(fx.files[&b].wpt.rev_id, rev);
|
assert_ne!(fx.files[&b].wpt.rev_id, rev);
|
||||||
@@ -178,7 +186,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let wpt = &waypoints(&fx, a)[0];
|
let wpt = &waypoints(&fx, a)[0];
|
||||||
assert!(wpt.name.is_none() && wpt.desc.is_none() && wpt.cmt.is_none());
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -266,7 +266,10 @@ mod tests {
|
|||||||
name: Some("summit".into()),
|
name: Some("summit".into()),
|
||||||
cmt: Some("comment".into()),
|
cmt: Some("comment".into()),
|
||||||
desc: Some("description".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()),
|
sym: Some("Summit".into()),
|
||||||
type_: Some("peak".into()),
|
type_: Some("peak".into()),
|
||||||
}
|
}
|
||||||
@@ -284,7 +287,10 @@ mod tests {
|
|||||||
email: Some("someone@example.com".into()),
|
email: Some("someone@example.com".into()),
|
||||||
link: Some(link("https://example.com/someone")),
|
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),
|
time: Some(1_700_000_000_000),
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -304,7 +310,10 @@ mod tests {
|
|||||||
cmt: Some("cmt".into()),
|
cmt: Some("cmt".into()),
|
||||||
desc: Some("desc".into()),
|
desc: Some("desc".into()),
|
||||||
src: Some("src".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()),
|
type_: Some("Cycling".into()),
|
||||||
color: Some("ff0000".into()),
|
color: Some("ff0000".into()),
|
||||||
opacity: Some(0.5),
|
opacity: Some(0.5),
|
||||||
@@ -508,13 +517,16 @@ mod tests {
|
|||||||
const FILE_V1: &str = concat!(
|
const FILE_V1: &str = concat!(
|
||||||
"010673616d706c65010561626f7574010107736f6d656f6e650113736f6d656f6e65406578616d706c652e63",
|
"010673616d706c65010561626f7574010107736f6d656f6e650113736f6d656f6e65406578616d706c652e63",
|
||||||
"6f6d011b68747470733a2f2f6578616d706c652e636f6d2f736f6d656f6e65012374657874206f6620687474",
|
"6f6d011b68747470733a2f2f6578616d706c652e636f6d2f736f6d656f6e65012374657874206f6620687474",
|
||||||
"70733a2f2f6578616d706c652e636f6d2f736f6d656f6e65011868747470733a2f2f6578616d706c652e636f",
|
"70733a2f2f6578616d706c652e636f6d2f736f6d656f6e65021868747470733a2f2f6578616d706c652e636f",
|
||||||
"6d2f66696c65012074657874206f662068747470733a2f2f6578616d706c652e636f6d2f66696c650180a0ab",
|
"6d2f66696c65012074657874206f662068747470733a2f2f6578616d706c652e636f6d2f66696c651a687474",
|
||||||
"fef9620110000000000000000000000000000000020105747261636b0103636d740104646573630103737263",
|
"70733a2f2f6578616d706c652e636f6d2f66696c652d32012274657874206f662068747470733a2f2f657861",
|
||||||
"011968747470733a2f2f6578616d706c652e636f6d2f747261636b012174657874206f662068747470733a2f",
|
"6d706c652e636f6d2f66696c652d320180a0abfef96201100000000000000000000000000000000201057472",
|
||||||
"2f6578616d706c652e636f6d2f747261636b01074379636c696e67010666663030303001000000000000e03f",
|
"61636b0103636d740104646573630103737263021968747470733a2f2f6578616d706c652e636f6d2f747261",
|
||||||
"0100000000000018400210000000000000000000000000000000030110000000000000000000000000000000",
|
"636b012174657874206f662068747470733a2f2f6578616d706c652e636f6d2f747261636b1b68747470733a",
|
||||||
"04100000000000000000000000000000000600011000000000000000000000000000000005",
|
"2f2f6578616d706c652e636f6d2f747261636b2d32012374657874206f662068747470733a2f2f6578616d70",
|
||||||
|
"6c652e636f6d2f747261636b2d3201074379636c696e67010666663030303001000000000000e03f01000000",
|
||||||
|
"0000001840021000000000000000000000000000000003011000000000000000000000000000000004100000",
|
||||||
|
"000000000000000000000000000600011000000000000000000000000000000005",
|
||||||
);
|
);
|
||||||
const TRACKPOINTS_V1: &str = concat!(
|
const TRACKPOINTS_V1: &str = concat!(
|
||||||
"01030000000000001040000000000000494000000000002059400180a0abfef9620105018c01015001fa0101",
|
"01030000000000001040000000000000494000000000002059400180a0abfef9620105018c01015001fa0101",
|
||||||
@@ -524,10 +536,12 @@ mod tests {
|
|||||||
);
|
);
|
||||||
const WAYPOINTS_V1: &str = concat!(
|
const WAYPOINTS_V1: &str = concat!(
|
||||||
"0102100000000000000000000000000000000700000000000016400000000000c04940000000000010744001",
|
"0102100000000000000000000000000000000700000000000016400000000000c04940000000000010744001",
|
||||||
"80a0abfef962010673756d6d69740107636f6d6d656e74010b6465736372697074696f6e011a68747470733a",
|
"80a0abfef962010673756d6d69740107636f6d6d656e74010b6465736372697074696f6e021a68747470733a",
|
||||||
"2f2f6578616d706c652e636f6d2f73756d6d6974012274657874206f662068747470733a2f2f6578616d706c",
|
"2f2f6578616d706c652e636f6d2f73756d6d6974012274657874206f662068747470733a2f2f6578616d706c",
|
||||||
"652e636f6d2f73756d6d6974010653756d6d697401047065616b100000000000000000000000000000000800",
|
"652e636f6d2f73756d6d69741c68747470733a2f2f6578616d706c652e636f6d2f73756d6d69742d32012474",
|
||||||
"000000000000000000000000000000000000000000000000000000000000",
|
"657874206f662068747470733a2f2f6578616d706c652e636f6d2f73756d6d69742d32010653756d6d697401",
|
||||||
|
"047065616b100000000000000000000000000000000800000000000000000000000000000000000000000000",
|
||||||
|
"000000000000000000",
|
||||||
);
|
);
|
||||||
const ORDER_V1: &str =
|
const ORDER_V1: &str =
|
||||||
"010210000000000000000000000000000000091000000000000000000000000000000008";
|
"010210000000000000000000000000000000091000000000000000000000000000000008";
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ pub struct FileInfo1 {
|
|||||||
name: String,
|
name: String,
|
||||||
desc: Option<String>,
|
desc: Option<String>,
|
||||||
author: Option<Author1>,
|
author: Option<Author1>,
|
||||||
link: Option<Link1>,
|
links: Vec<Link1>,
|
||||||
time: Option<i64>,
|
time: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ impl From<&FileInfo> for FileInfo1 {
|
|||||||
email: author.email.clone(),
|
email: author.email.clone(),
|
||||||
link: author.link.as_ref().map(Into::into),
|
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,
|
time: info.time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ impl From<FileInfo1> for FileInfo {
|
|||||||
email: author.email,
|
email: author.email,
|
||||||
link: author.link.map(Into::into),
|
link: author.link.map(Into::into),
|
||||||
}),
|
}),
|
||||||
link: info.link.map(Into::into),
|
links: info.links.into_iter().map(Into::into).collect(),
|
||||||
time: info.time,
|
time: info.time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ pub struct TrackInfo1 {
|
|||||||
cmt: Option<String>,
|
cmt: Option<String>,
|
||||||
desc: Option<String>,
|
desc: Option<String>,
|
||||||
src: Option<String>,
|
src: Option<String>,
|
||||||
link: Option<Link1>,
|
links: Vec<Link1>,
|
||||||
type_: Option<String>,
|
type_: Option<String>,
|
||||||
color: Option<String>,
|
color: Option<String>,
|
||||||
opacity: Option<f64>,
|
opacity: Option<f64>,
|
||||||
@@ -102,7 +102,7 @@ impl From<&TrackInfo> for TrackInfo1 {
|
|||||||
cmt: info.cmt.clone(),
|
cmt: info.cmt.clone(),
|
||||||
desc: info.desc.clone(),
|
desc: info.desc.clone(),
|
||||||
src: info.src.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(),
|
type_: info.type_.clone(),
|
||||||
color: info.color.clone(),
|
color: info.color.clone(),
|
||||||
opacity: info.opacity,
|
opacity: info.opacity,
|
||||||
@@ -118,7 +118,7 @@ impl From<TrackInfo1> for TrackInfo {
|
|||||||
cmt: info.cmt,
|
cmt: info.cmt,
|
||||||
desc: info.desc,
|
desc: info.desc,
|
||||||
src: info.src,
|
src: info.src,
|
||||||
link: info.link.map(Into::into),
|
links: info.links.into_iter().map(Into::into).collect(),
|
||||||
type_: info.type_,
|
type_: info.type_,
|
||||||
color: info.color,
|
color: info.color,
|
||||||
opacity: info.opacity,
|
opacity: info.opacity,
|
||||||
@@ -242,7 +242,7 @@ pub struct Waypoint1 {
|
|||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
cmt: Option<String>,
|
cmt: Option<String>,
|
||||||
desc: Option<String>,
|
desc: Option<String>,
|
||||||
link: Option<Link1>,
|
links: Vec<Link1>,
|
||||||
sym: Option<String>,
|
sym: Option<String>,
|
||||||
type_: Option<String>,
|
type_: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -258,7 +258,7 @@ impl From<&Waypoint> for Waypoint1 {
|
|||||||
name: waypoint.name.clone(),
|
name: waypoint.name.clone(),
|
||||||
cmt: waypoint.cmt.clone(),
|
cmt: waypoint.cmt.clone(),
|
||||||
desc: waypoint.desc.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(),
|
sym: waypoint.sym.clone(),
|
||||||
type_: waypoint.type_.clone(),
|
type_: waypoint.type_.clone(),
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ impl From<Waypoint1> for Waypoint {
|
|||||||
name: waypoint.name,
|
name: waypoint.name,
|
||||||
cmt: waypoint.cmt,
|
cmt: waypoint.cmt,
|
||||||
desc: waypoint.desc,
|
desc: waypoint.desc,
|
||||||
link: waypoint.link.map(Into::into),
|
links: waypoint.links.into_iter().map(Into::into).collect(),
|
||||||
sym: waypoint.sym,
|
sym: waypoint.sym,
|
||||||
type_: waypoint.type_,
|
type_: waypoint.type_,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,13 +143,19 @@ pub fn waypoint(file_id: &str, waypoint_id: &str) -> WaypointDetails {
|
|||||||
set(&object, key, value.as_str());
|
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();
|
let link_object = Object::new();
|
||||||
set(&link_object, "href", link.href.as_str());
|
set(&link_object, "href", link.href.as_str());
|
||||||
if let Some(text) = &link.text {
|
if let Some(text) = &link.text {
|
||||||
set(&link_object, "text", text.as_str());
|
set(&link_object, "text", text.as_str());
|
||||||
}
|
}
|
||||||
set(&object, "link", link_object);
|
link_object.into()
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
JsValue::from(object)
|
JsValue::from(object)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ export interface WaypointDetails {
|
|||||||
name?: string;
|
name?: string;
|
||||||
desc?: string;
|
desc?: string;
|
||||||
cmt?: 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;
|
sym?: string;
|
||||||
}
|
}
|
||||||
/** The position, elevation and time of a trackpoint. */
|
/** 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.Root class="border-none shadow-md text-base p-2 max-w-[50dvw] gap-0">
|
||||||
<Card.Header class="p-0 gap-0">
|
<Card.Header class="p-0 gap-0">
|
||||||
<Card.Title class="text-md">
|
<Card.Title class="text-md">
|
||||||
{#if waypoint.item.link?.href}
|
{@const link = waypoint.item.links?.[0]}
|
||||||
<a href={waypoint.item.link.href} target="_blank">
|
{#if link?.href}
|
||||||
{waypoint.item.name ?? waypoint.item.link.href}
|
<a href={link.href} target="_blank">
|
||||||
|
{waypoint.item.name ?? link.href}
|
||||||
<ExternalLink size="12" class="inline-block mb-1.5" />
|
<ExternalLink size="12" class="inline-block mb-1.5" />
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
if (wpt.cmt !== undefined && wpt.cmt !== wpt.desc) {
|
if (wpt.cmt !== undefined && wpt.cmt !== wpt.desc) {
|
||||||
description += '\n\n' + wpt.cmt;
|
description += '\n\n' + wpt.cmt;
|
||||||
}
|
}
|
||||||
link = wpt.link?.href ?? '';
|
link = wpt.links?.[0]?.href ?? '';
|
||||||
sym = wpt.sym ?? '';
|
sym = wpt.sym ?? '';
|
||||||
longitude = parseFloat(wpt.lng.toFixed(6));
|
longitude = parseFloat(wpt.lng.toFixed(6));
|
||||||
latitude = parseFloat(wpt.lat.toFixed(6));
|
latitude = parseFloat(wpt.lat.toFixed(6));
|
||||||
|
|||||||
Reference in new issue
Block a user