new: yew-based front-end
This commit is contained in:
parent
abadb2ec7d
commit
d64938e8e5
20 changed files with 680 additions and 6263 deletions
81
src/app.rs
Normal file
81
src/app.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
use super::loader::Loader;
|
||||
|
||||
use web_sys::Node;
|
||||
use yew::virtual_dom::VNode;
|
||||
use yew::{function_component, html, Html};
|
||||
|
||||
#[function_component(App)]
|
||||
pub fn app() -> Html {
|
||||
html! {
|
||||
<>
|
||||
<h1>
|
||||
<a href="/">
|
||||
{ "gpx.thermokar.st" }
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
{ "a client-side tool for merging " }
|
||||
<a href="https://www.topografix.com/gpx.asp">
|
||||
{ "gpx files" }
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<Loader />
|
||||
|
||||
<hr/>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component(Footer)]
|
||||
fn footer() -> Html {
|
||||
let notes = Vec::from([
|
||||
// note 1
|
||||
"this has only been tested on GPX files produced by \
|
||||
<a href=\"https://strava.com\" target=\"_blank\">strava</a> and \
|
||||
<a href=\"https://garmin.com\" target=\"_blank\">garmin</a>",
|
||||
// note 2
|
||||
"all third-party extension info \
|
||||
<a href=\"https://github.com/georust/gpx/issues/8\" target=\"_blank\">\
|
||||
is stripped</a>",
|
||||
// note 3
|
||||
"if the app breaks, try refreshing the page",
|
||||
"source (public access): git://pingo.thermokar.st/gpx-web-utils",
|
||||
// note 4
|
||||
"source (mirror): \
|
||||
<a href=\"https://github.com/thermokarst/gpx-web-utils\" target=\"_blank\">\
|
||||
https://github.com/thermokarst/gpx-web-utils</a>",
|
||||
]);
|
||||
|
||||
html! {
|
||||
<div>
|
||||
<ul>
|
||||
{ for notes.iter().map(|n| inner_html_enabled_li(n)) }
|
||||
</ul>
|
||||
|
||||
<span>
|
||||
<small>
|
||||
{ "\u{000A9} matthew ryan dillon, 2021" }
|
||||
</small>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_html_enabled_li(data: &str) -> Html {
|
||||
let li = web_sys::window()
|
||||
.unwrap()
|
||||
.document()
|
||||
.unwrap()
|
||||
.create_element("li")
|
||||
.unwrap();
|
||||
|
||||
li.set_inner_html(data);
|
||||
|
||||
let node = Node::from(li);
|
||||
|
||||
VNode::VRef(node)
|
||||
}
|
88
src/gpx.rs
Normal file
88
src/gpx.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
use std::error::Error;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::{Blob, MouseEvent, Url};
|
||||
|
||||
fn join_gpx_files(files: &[String]) -> Result<gpx::Gpx, Box<dyn Error>> {
|
||||
let mut merged_gpx: gpx::Gpx = Default::default();
|
||||
let mut merged_track: gpx::Track = gpx::Track::new();
|
||||
|
||||
for file in files.iter() {
|
||||
let buffer = std::io::BufReader::new(file.as_bytes());
|
||||
let mut parsed_gpx: gpx::Gpx = gpx::read(buffer)?;
|
||||
|
||||
// consolidate all track segements into one single track.
|
||||
for track in parsed_gpx.tracks {
|
||||
for segment in track.segments {
|
||||
merged_track.segments.push(segment);
|
||||
}
|
||||
}
|
||||
|
||||
merged_gpx.waypoints.append(&mut parsed_gpx.waypoints);
|
||||
}
|
||||
|
||||
merged_gpx.tracks.push(merged_track);
|
||||
|
||||
let link = gpx::Link {
|
||||
href: String::from("https://gpx.thermokar.st"),
|
||||
..Default::default()
|
||||
};
|
||||
let author = gpx::Person {
|
||||
link: Some(link),
|
||||
..Default::default()
|
||||
};
|
||||
let metadata = gpx::Metadata {
|
||||
name: Some(String::from("merged")),
|
||||
author: Some(author),
|
||||
..Default::default()
|
||||
};
|
||||
merged_gpx.metadata = Some(metadata);
|
||||
merged_gpx.version = gpx::GpxVersion::Gpx11;
|
||||
|
||||
Ok(merged_gpx)
|
||||
}
|
||||
fn write_gpx_to_buffer(gpx: gpx::Gpx) -> Result<js_sys::Array, Box<dyn Error>> {
|
||||
let mut buffer = Vec::new();
|
||||
gpx::write(&gpx, &mut buffer)?;
|
||||
|
||||
let uint8arr = js_sys::Uint8Array::new(&unsafe { js_sys::Uint8Array::view(&buffer) }.into());
|
||||
let array = js_sys::Array::new();
|
||||
array.push(&uint8arr.buffer());
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
pub fn merge(files: &[String]) -> Result<Blob, Box<dyn Error>> {
|
||||
let merged: gpx::Gpx = join_gpx_files(files)?;
|
||||
let out_vec = write_gpx_to_buffer(merged)?;
|
||||
let result = Blob::new_with_u8_array_sequence(&out_vec).map_err(|e| e.as_string().unwrap())?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn download(merged: Blob) -> Result<(), Box<dyn Error>> {
|
||||
let window = web_sys::window().ok_or("no global `window` exists")?;
|
||||
let document = window
|
||||
.document()
|
||||
.ok_or("should have a document on window")?;
|
||||
|
||||
let err_handler = |e: JsValue| e.as_string().unwrap();
|
||||
let anchor_element = document.create_element("a").map_err(err_handler)?;
|
||||
let url = Url::create_object_url_with_blob(&merged).map_err(err_handler)?;
|
||||
|
||||
anchor_element
|
||||
.set_attribute("href", &url)
|
||||
.map_err(err_handler)?;
|
||||
anchor_element
|
||||
.set_attribute("download", "merged.gpx")
|
||||
.map_err(err_handler)?;
|
||||
|
||||
let event = MouseEvent::new("click").map_err(err_handler)?;
|
||||
anchor_element.dispatch_event(&event).map_err(err_handler)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
pub fn alert(s: &str);
|
||||
}
|
17
src/lib.rs
17
src/lib.rs
|
@ -1,17 +0,0 @@
|
|||
mod utils;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(feature = "wee_alloc")]
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn merge(files: js_sys::Array) -> wasm_bindgen::JsValue {
|
||||
utils::set_panic_hook();
|
||||
let files: Vec<String> = utils::translate_js_to_rust(files);
|
||||
let merged: gpx::Gpx = utils::join_gpx_files(files);
|
||||
let out_vec: Vec<u8> = utils::write_gpx_to_buffer(merged);
|
||||
|
||||
JsValue::from_str(&String::from_utf8(out_vec).unwrap())
|
||||
}
|
155
src/loader.rs
Normal file
155
src/loader.rs
Normal file
|
@ -0,0 +1,155 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use gloo_file::callbacks::FileReader;
|
||||
use gloo_file::File;
|
||||
use web_sys::{Blob, Event, HtmlInputElement};
|
||||
use yew::{html, html::TargetCast, Component, Context, Html};
|
||||
|
||||
use super::gpx;
|
||||
|
||||
pub enum Msg {
|
||||
FileLoaded(String, String),
|
||||
StartLoad(Vec<File>),
|
||||
FilesLoaded,
|
||||
Download(Blob),
|
||||
Reset,
|
||||
}
|
||||
|
||||
pub struct Loader {
|
||||
readers: HashMap<String, FileReader>,
|
||||
files: Vec<String>,
|
||||
count: usize,
|
||||
is_loading: bool,
|
||||
// This field is to handle resetting the native HTML widget's state on error
|
||||
field_value: &'static str,
|
||||
}
|
||||
|
||||
impl Component for Loader {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
readers: HashMap::default(),
|
||||
files: vec![],
|
||||
count: 0,
|
||||
is_loading: false,
|
||||
field_value: "",
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::FileLoaded(filename, data) => {
|
||||
self.files.push(data);
|
||||
self.readers.remove(&filename);
|
||||
if self.files.len() == self.count {
|
||||
ctx.link().send_message(Msg::FilesLoaded);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
Msg::StartLoad(files) => {
|
||||
self.count = files.len();
|
||||
if self.count < 2 {
|
||||
gpx::alert("must load two or more files");
|
||||
ctx.link().send_message(Msg::Reset);
|
||||
return true;
|
||||
}
|
||||
self.is_loading = true;
|
||||
|
||||
for file in files.into_iter() {
|
||||
let file_name = file.name();
|
||||
let task = {
|
||||
let file_name = file_name.clone();
|
||||
let link = ctx.link().clone();
|
||||
|
||||
gloo_file::callbacks::read_as_text(&file, move |res| {
|
||||
link.send_message(Msg::FileLoaded(
|
||||
file_name,
|
||||
res.unwrap_or_else(|e| e.to_string()),
|
||||
))
|
||||
})
|
||||
};
|
||||
self.readers.insert(file_name, task);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
Msg::FilesLoaded => {
|
||||
let link = ctx.link();
|
||||
|
||||
let merged = match gpx::merge(&self.files) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
gpx::alert(&err.to_string());
|
||||
link.send_message(Msg::Reset);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
link.send_message(Msg::Download(merged));
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
Msg::Download(merged) => {
|
||||
let link = ctx.link();
|
||||
|
||||
match gpx::download(merged) {
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
gpx::alert(&err.to_string());
|
||||
link.send_message(Msg::Reset);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
link.send_message(Msg::Reset);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
Msg::Reset => {
|
||||
self.readers = HashMap::default();
|
||||
self.files = vec![];
|
||||
self.count = 0;
|
||||
self.is_loading = false;
|
||||
self.field_value = "";
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let cb = move |e: Event| {
|
||||
let mut result = Vec::new();
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
|
||||
if let Some(files) = input.files() {
|
||||
let files = js_sys::try_iter(&files)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.map(|v| web_sys::File::from(v.unwrap()))
|
||||
.map(File::from);
|
||||
result.extend(files);
|
||||
}
|
||||
Msg::StartLoad(result)
|
||||
};
|
||||
|
||||
html! {
|
||||
if self.is_loading {
|
||||
<span><strong>{"processing..."}</strong></span>
|
||||
} else {
|
||||
<input
|
||||
type="file"
|
||||
value={self.field_value}
|
||||
multiple=true
|
||||
onchange={ctx.link().callback(cb)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
src/main.rs
Normal file
13
src/main.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
extern crate wee_alloc;
|
||||
|
||||
mod app;
|
||||
mod gpx;
|
||||
mod loader;
|
||||
|
||||
// Use `wee_alloc` as the global allocator.
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<app::App>();
|
||||
}
|
55
src/utils.rs
55
src/utils.rs
|
@ -1,55 +0,0 @@
|
|||
pub fn set_panic_hook() {
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
|
||||
pub fn translate_js_to_rust(files: js_sys::Array) -> Vec<String> {
|
||||
// https://github.com/rustwasm/wasm-bindgen/issues/111
|
||||
files.iter().map(|f| f.as_string().unwrap()).collect()
|
||||
}
|
||||
|
||||
pub fn join_gpx_files(files: Vec<String>) -> gpx::Gpx {
|
||||
let mut merged_gpx: gpx::Gpx = Default::default();
|
||||
let mut merged_track: gpx::Track = gpx::Track::new();
|
||||
|
||||
for file in files.iter() {
|
||||
let buffer = std::io::BufReader::new(file.as_bytes());
|
||||
let mut parsed_gpx: gpx::Gpx = gpx::read(buffer).expect("invalid gpx");
|
||||
|
||||
// consolidate all track segements into one single track.
|
||||
for track in parsed_gpx.tracks {
|
||||
for segment in track.segments {
|
||||
merged_track.segments.push(segment);
|
||||
}
|
||||
}
|
||||
|
||||
merged_gpx.waypoints.append(&mut parsed_gpx.waypoints);
|
||||
}
|
||||
|
||||
merged_gpx.tracks.push(merged_track);
|
||||
|
||||
let link = gpx::Link {
|
||||
href: String::from("https://gpx.thermokar.st"),
|
||||
..Default::default()
|
||||
};
|
||||
let author = gpx::Person {
|
||||
link: Some(link),
|
||||
..Default::default()
|
||||
};
|
||||
let metadata = gpx::Metadata {
|
||||
name: Some(String::from("merged")),
|
||||
author: Some(author),
|
||||
..Default::default()
|
||||
};
|
||||
merged_gpx.metadata = Some(metadata);
|
||||
merged_gpx.version = gpx::GpxVersion::Gpx11;
|
||||
|
||||
merged_gpx
|
||||
}
|
||||
|
||||
pub fn write_gpx_to_buffer(gpx: gpx::Gpx) -> Vec<u8> {
|
||||
let mut buffer = Vec::new();
|
||||
gpx::write(&gpx, &mut buffer).unwrap();
|
||||
|
||||
buffer
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue