diff --git a/Cargo.lock b/Cargo.lock index 5d701d7..42d3fb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "any_spawner" version = "0.2.0" @@ -137,6 +152,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "codee" version = "0.3.0" @@ -231,12 +260,39 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +dependencies = [ + "memchr", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -552,6 +608,29 @@ dependencies = [ "throw_error", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1034,6 +1113,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -1070,13 +1158,16 @@ checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd" name = "osm_track_importer" version = "0.1.0" dependencies = [ + "chrono", "console_error_panic_hook", "console_log", + "csv", "leptos", "leptos-leaflet", "leptos_meta", "leptos_router", "log", + "serde", "wasm-bindgen", "wasm-bindgen-test", "web-sys", @@ -1989,6 +2080,21 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index 6931a13..1157e20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,9 @@ web-sys = { version = "0.3", features = [ "HtmlInputElement", "FileList", ] } +serde = { version = "1.0.218", features = ["derive"] } +csv = "1.3.1" +chrono = "0.4.40" # utils # strum = { version = "0.25", features = ["derive", "strum_macros"] } diff --git a/src/gps/gps_track.rs b/src/gps/gps_track.rs new file mode 100644 index 0000000..27fba21 --- /dev/null +++ b/src/gps/gps_track.rs @@ -0,0 +1,112 @@ +use chrono::{DateTime, NaiveDateTime, TimeZone, Utc}; +use csv::ReaderBuilder; +use leptos::prelude::*; +use leptos_leaflet::prelude::*; +use serde::Deserialize; + +pub fn to_polyline(track: &Option) -> impl IntoView { + if let Some(track) = track { + // Extract a vector of (latitude, longitude) tuples from the track points. + let positions_vec: Vec<(f64, f64)> = track + .points + .iter() + .map(|point| (point.latitude, point.longitude)) + .collect(); + + // Return a Polyline view using the positions helper. + view! { }.into_any() + } else { + view! {}.into_any() + } +} + +#[derive(Debug, Clone)] +pub struct GpsTrack { + pub points: Vec, +} + +impl From<&str> for GpsTrack { + fn from(value: &str) -> Self { + let mut reader = ReaderBuilder::new() + .delimiter(b',') + .from_reader(value.as_bytes()); + + Self { + points: reader.deserialize().map(|v| v.unwrap()).collect(), + } + } +} + +#[derive(Debug, Clone)] +pub struct GpsTrackPoint { + pub index: u32, + pub tag: char, + pub timestamp: DateTime, + pub latitude: f64, + pub longitude: f64, + pub height: i32, + pub speed: f32, + pub heading: u32, +} + +impl<'de> Deserialize<'de> for GpsTrackPoint { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(rename_all = "UPPERCASE")] + struct Helper { + index: u32, + tag: char, + date: String, // e.g., "250301" (ddMMyy) + time: String, // e.g., "101102" (HHMMSS) + #[serde(rename = "LATITUDE N/S")] + latitude: String, // e.g., "51.9787868N" + #[serde(rename = "LONGITUDE E/W")] + longitude: String, // e.g., "9.2337406E" + height: i32, + speed: f32, + heading: u32, + } + + let helper = Helper::deserialize(deserializer)?; + + // Combine DATE and TIME into one string, then parse. + let datetime_str = format!("{} {}", helper.date, helper.time); + let naive = NaiveDateTime::parse_from_str(&datetime_str, "%y%m%d %H%M%S") + .map_err(serde::de::Error::custom)?; + // Use the TimeZone trait's method from_utc_datetime. + let timestamp = Utc.from_utc_datetime(&naive); + + fn parse_coord(coord: &str) -> Result + where + E: serde::de::Error, + { + if coord.len() < 2 { + return Err(serde::de::Error::custom("Coordinate string too short")); + } + let (number_part, direction) = coord.split_at(coord.len() - 1); + let value: f64 = number_part.parse().map_err(serde::de::Error::custom)?; + match direction { + "N" | "E" => Ok(value), + "S" | "W" => Ok(-value), + _ => Err(serde::de::Error::custom("Invalid coordinate direction")), + } + } + + let latitude = parse_coord::(&helper.latitude)?; + let longitude = parse_coord::(&helper.longitude)?; + + Ok(GpsTrackPoint { + index: helper.index, + tag: helper.tag, + timestamp, + latitude, + longitude, + height: helper.height, + speed: helper.speed, + heading: helper.heading, + }) + } +} diff --git a/src/gps/mod.rs b/src/gps/mod.rs new file mode 100644 index 0000000..2aec738 --- /dev/null +++ b/src/gps/mod.rs @@ -0,0 +1 @@ +pub mod gps_track; diff --git a/src/lib.rs b/src/lib.rs index 4cca9f2..8d209aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ use leptos_router::{components::*, path}; // Modules mod components; +mod gps; mod pages; // Top-Level pages diff --git a/src/pages/home.rs b/src/pages/home.rs index 5d36d27..75ba0b1 100644 --- a/src/pages/home.rs +++ b/src/pages/home.rs @@ -1,7 +1,9 @@ -use crate::components::file_select::FileSelect; +use crate::gps::gps_track::to_polyline; +use crate::{components::file_select::FileSelect, gps::gps_track::GpsTrack}; use leptos::logging::log; use leptos::prelude::*; use leptos::wasm_bindgen::JsCast; +use leptos_leaflet::leaflet::*; use leptos_leaflet::prelude::*; use wasm_bindgen::prelude::Closure; use web_sys::{File, FileReader}; @@ -9,14 +11,15 @@ use web_sys::{File, FileReader}; /// Default Home Page #[component] pub fn Home() -> impl IntoView { - let file_contents = RwSignal::new(None); + let track = RwSignal::new(None); + let map: RwSignal, _> = RwSignal::new_local(None); // Function to handle file selection let on_file_select = move |file: File| { let reader = FileReader::new().expect("Failed to create FileReader"); // Clone the signal to move into the closure - let file_contents_clone = file_contents.clone(); + let track_clone = track.clone(); // Define the callback to handle the 'load' event let onload = Closure::wrap(Box::new(move |event: web_sys::Event| { @@ -24,7 +27,7 @@ pub fn Home() -> impl IntoView { if let Ok(result) = reader.result() { if let Some(text) = result.as_string() { log!("File content: {}", text); - file_contents_clone.set(Some(text)); + track_clone.set(Some(GpsTrack::from(text.as_str()))); } } }) as Box); @@ -39,6 +42,20 @@ pub fn Home() -> impl IntoView { onload.forget(); }; + Effect::watch( + move || track.get(), + move |track, _, _| { + map.get().as_ref().unwrap().set_view( + &LatLng::new( + track.as_ref().unwrap().points[0].latitude, + track.as_ref().unwrap().points[0].longitude, + ), + 35.0, + ) + }, + false, + ); + view! { impl IntoView { "Select Trace File" OpenStreetMap contributors" /> + {{ move || to_polyline(&track.get()) }}