✨ Added ability to view Columbus CSV GPS tracks
This commit is contained in:
parent
012353d016
commit
6921d28add
6 changed files with 248 additions and 6 deletions
106
Cargo.lock
generated
106
Cargo.lock
generated
|
@ -11,6 +11,21 @@ dependencies = [
|
||||||
"memchr",
|
"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]]
|
[[package]]
|
||||||
name = "any_spawner"
|
name = "any_spawner"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -137,6 +152,20 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
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]]
|
[[package]]
|
||||||
name = "codee"
|
name = "codee"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
@ -231,12 +260,39 @@ dependencies = [
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.21"
|
version = "0.8.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
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]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "6.1.0"
|
version = "6.1.0"
|
||||||
|
@ -552,6 +608,29 @@ dependencies = [
|
||||||
"throw_error",
|
"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]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
@ -1034,6 +1113,15 @@ dependencies = [
|
||||||
"minimal-lexical",
|
"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]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
|
@ -1070,13 +1158,16 @@ checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd"
|
||||||
name = "osm_track_importer"
|
name = "osm_track_importer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"console_log",
|
"console_log",
|
||||||
|
"csv",
|
||||||
"leptos",
|
"leptos",
|
||||||
"leptos-leaflet",
|
"leptos-leaflet",
|
||||||
"leptos_meta",
|
"leptos_meta",
|
||||||
"leptos_router",
|
"leptos_router",
|
||||||
"log",
|
"log",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-test",
|
"wasm-bindgen-test",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
@ -1989,6 +2080,21 @@ dependencies = [
|
||||||
"windows-sys",
|
"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]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.59.0"
|
version = "0.59.0"
|
||||||
|
|
|
@ -24,6 +24,9 @@ web-sys = { version = "0.3", features = [
|
||||||
"HtmlInputElement",
|
"HtmlInputElement",
|
||||||
"FileList",
|
"FileList",
|
||||||
] }
|
] }
|
||||||
|
serde = { version = "1.0.218", features = ["derive"] }
|
||||||
|
csv = "1.3.1"
|
||||||
|
chrono = "0.4.40"
|
||||||
|
|
||||||
# utils
|
# utils
|
||||||
# strum = { version = "0.25", features = ["derive", "strum_macros"] }
|
# strum = { version = "0.25", features = ["derive", "strum_macros"] }
|
||||||
|
|
112
src/gps/gps_track.rs
Normal file
112
src/gps/gps_track.rs
Normal file
|
@ -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<GpsTrack>) -> 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! { <Polyline positions=positions(&positions_vec) /> }.into_any()
|
||||||
|
} else {
|
||||||
|
view! {}.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GpsTrack {
|
||||||
|
pub points: Vec<GpsTrackPoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Utc>,
|
||||||
|
pub latitude: f64,
|
||||||
|
pub longitude: f64,
|
||||||
|
pub height: i32,
|
||||||
|
pub speed: f32,
|
||||||
|
pub heading: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for GpsTrackPoint {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
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<E>(coord: &str) -> Result<f64, E>
|
||||||
|
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::<D::Error>(&helper.latitude)?;
|
||||||
|
let longitude = parse_coord::<D::Error>(&helper.longitude)?;
|
||||||
|
|
||||||
|
Ok(GpsTrackPoint {
|
||||||
|
index: helper.index,
|
||||||
|
tag: helper.tag,
|
||||||
|
timestamp,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
height: helper.height,
|
||||||
|
speed: helper.speed,
|
||||||
|
heading: helper.heading,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
1
src/gps/mod.rs
Normal file
1
src/gps/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod gps_track;
|
|
@ -4,6 +4,7 @@ use leptos_router::{components::*, path};
|
||||||
|
|
||||||
// Modules
|
// Modules
|
||||||
mod components;
|
mod components;
|
||||||
|
mod gps;
|
||||||
mod pages;
|
mod pages;
|
||||||
|
|
||||||
// Top-Level pages
|
// Top-Level pages
|
||||||
|
|
|
@ -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::logging::log;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::wasm_bindgen::JsCast;
|
use leptos::wasm_bindgen::JsCast;
|
||||||
|
use leptos_leaflet::leaflet::*;
|
||||||
use leptos_leaflet::prelude::*;
|
use leptos_leaflet::prelude::*;
|
||||||
use wasm_bindgen::prelude::Closure;
|
use wasm_bindgen::prelude::Closure;
|
||||||
use web_sys::{File, FileReader};
|
use web_sys::{File, FileReader};
|
||||||
|
@ -9,14 +11,15 @@ use web_sys::{File, FileReader};
|
||||||
/// Default Home Page
|
/// Default Home Page
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Home() -> impl IntoView {
|
pub fn Home() -> impl IntoView {
|
||||||
let file_contents = RwSignal::new(None);
|
let track = RwSignal::new(None);
|
||||||
|
let map: RwSignal<Option<leptos_leaflet::leaflet::Map>, _> = RwSignal::new_local(None);
|
||||||
|
|
||||||
// Function to handle file selection
|
// Function to handle file selection
|
||||||
let on_file_select = move |file: File| {
|
let on_file_select = move |file: File| {
|
||||||
let reader = FileReader::new().expect("Failed to create FileReader");
|
let reader = FileReader::new().expect("Failed to create FileReader");
|
||||||
|
|
||||||
// Clone the signal to move into the closure
|
// 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
|
// Define the callback to handle the 'load' event
|
||||||
let onload = Closure::wrap(Box::new(move |event: web_sys::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 Ok(result) = reader.result() {
|
||||||
if let Some(text) = result.as_string() {
|
if let Some(text) = result.as_string() {
|
||||||
log!("File content: {}", text);
|
log!("File content: {}", text);
|
||||||
file_contents_clone.set(Some(text));
|
track_clone.set(Some(GpsTrack::from(text.as_str())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) as Box<dyn FnMut(_)>);
|
}) as Box<dyn FnMut(_)>);
|
||||||
|
@ -39,6 +42,20 @@ pub fn Home() -> impl IntoView {
|
||||||
onload.forget();
|
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! {
|
view! {
|
||||||
<ErrorBoundary fallback=|errors| {
|
<ErrorBoundary fallback=|errors| {
|
||||||
view! {
|
view! {
|
||||||
|
@ -69,15 +86,17 @@ pub fn Home() -> impl IntoView {
|
||||||
"Select Trace File"
|
"Select Trace File"
|
||||||
</FileSelect>
|
</FileSelect>
|
||||||
<MapContainer
|
<MapContainer
|
||||||
|
center=Position::new(0.0, 0.0)
|
||||||
|
zoom=3.0
|
||||||
class="flex-grow w-full"
|
class="flex-grow w-full"
|
||||||
center=Position::new(51.505, -0.09)
|
map=map.write_only()
|
||||||
zoom=13.0
|
|
||||||
set_view=true
|
set_view=true
|
||||||
>
|
>
|
||||||
<TileLayer
|
<TileLayer
|
||||||
url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
attribution="© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors"
|
attribution="© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors"
|
||||||
/>
|
/>
|
||||||
|
{{ move || to_polyline(&track.get()) }}
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue