This commit is contained in:
Matthew Ryan Dillon 2021-12-04 16:29:16 -07:00
parent f1e8d88add
commit fff4fdea81
8 changed files with 317 additions and 214 deletions

46
src/app.rs Normal file
View file

@ -0,0 +1,46 @@
use super::loader::Loader;
use yew::{html, Component, Context, Html};
pub struct App;
impl Component for App {
type Message = ();
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, _ctx: &Context<Self>) -> Html {
html! {
<>
<h1>
<a href="/">{"gpx.thermokar.st"}</a>
</h1>
<p>
{"This client-side tool is for merging "}
<a href="https://www.topografix.com/gpx.asp">{"GPX files"}</a>
{". Please note, this has only been tested on GPX files produced by "}
<a href="https://www.garmin.com">{"Garmin"}</a>
{" and "}
<a href="https://www.strava.com">{"Strava"}</a>
{" - your mileage may vary."}
</p>
<Loader />
<hr/>
<p>
<small>
<a href="https://github.com/thermokarst/gpx-web-utils">
{"https://github.com/thermokarst/gpx-web-utils"}
</a>
</small>
</p>
</>
}
}
}

View file

@ -1,17 +1,17 @@
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())
}
// 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())
// }

115
src/loader.rs Normal file
View file

@ -0,0 +1,115 @@
use std::collections::HashMap;
use gloo_file::callbacks::FileReader;
use gloo_file::File;
use web_sys::{Event, HtmlInputElement, Url, MouseEvent};
use yew::{html, html::TargetCast, Component, Context, Html};
use super::utils;
pub enum Msg {
FileLoaded(String, String),
Files(Vec<File>),
FilesLoaded,
Reset,
}
pub struct Loader {
readers: HashMap<String, FileReader>,
files: Vec<String>,
count: usize,
}
impl Component for Loader {
type Message = Msg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
Self {
readers: HashMap::default(),
files: vec![],
count: 0,
}
}
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::Files(files) => {
self.count = files.len();
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 merged = utils::merge(&self.files);
let window = web_sys::window().expect("no global `window` exists");
let document = window.document().expect("should have a document on window");
let anchor_element = document.create_element("a").unwrap();
let url = Url::create_object_url_with_blob(&merged).unwrap();
anchor_element.set_attribute("href", &url).unwrap();
anchor_element.set_attribute("download", "merged.gpx").unwrap();
let event = MouseEvent::new("click").unwrap();
anchor_element.dispatch_event(&event).unwrap();
true
}
Msg::Reset => {
self.readers = HashMap::default();
self.files = vec![];
self.count = 0;
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<>
<input type="file" multiple=true onchange={link.callback(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::Files(result)
})}
/>
</>
}
}
}

View file

@ -1,78 +1,7 @@
use yew::prelude::*;
enum Msg {
AddOne,
SubOne,
}
struct Model {
link: ComponentLink<Self>,
value: i64,
}
impl Component for Model {
type Message = Msg;
type Properties = ();
fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
link,
value: 0,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::AddOne => {
self.value += 1;
true
}
Msg::SubOne => {
self.value -= 1;
true
}
}
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! {
<>
<h1>
<a href="/">{"gpx.thermokar.st"}</a>
</h1>
<p>
{"This client-side tool is for merging "}
<a href="https://www.topografix.com/gpx.asp">{"GPX files"}</a>
{". Please note, this has only been tested on GPX files produced by "}
<a href="https://www.garmin.com">{"Garmin"}</a>
{" and "}
<a href="https://www.strava.com">{"Strava"}</a>
{" - your mileage may vary."}
</p>
<button onclick=self.link.callback(|_| Msg::SubOne)>{ "-1" }</button>
<button onclick=self.link.callback(|_| Msg::AddOne)>{ "+1" }</button>
<p>{ self.value }</p>
<hr/>
<p>
<small>
<a href="https://github.com/thermokarst/gpx-web-utils">
{"https://github.com/thermokarst/gpx-web-utils"}
</a>
</small>
</p>
</>
}
}
}
mod app;
mod loader;
mod utils;
fn main() {
yew::start_app::<Model>();
yew::start_app::<app::App>();
}

View file

@ -1,14 +1,6 @@
pub fn set_panic_hook() {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}
use web_sys::Blob;
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 {
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();
@ -46,10 +38,20 @@ pub fn join_gpx_files(files: Vec<String>) -> gpx::Gpx {
merged_gpx
}
pub fn write_gpx_to_buffer(gpx: gpx::Gpx) -> Vec<u8> {
fn write_gpx_to_buffer(gpx: gpx::Gpx) -> js_sys::Array {
let mut buffer = Vec::new();
gpx::write(&gpx, &mut buffer).unwrap();
buffer
let uint8arr = js_sys::Uint8Array::new(&unsafe { js_sys::Uint8Array::view(&buffer) }.into());
let array = js_sys::Array::new();
array.push(&uint8arr.buffer());
array
}
pub fn merge(files: &Vec<String>) -> Blob {
let merged: gpx::Gpx = join_gpx_files(files);
let out_vec = write_gpx_to_buffer(merged);
Blob::new_with_u8_array_sequence(&out_vec).unwrap()
}