Added ability to view Columbus CSV GPS tracks

This commit is contained in:
LeMoonStar 2025-03-01 19:17:42 +01:00
parent 012353d016
commit 6921d28add
6 changed files with 248 additions and 6 deletions

106
Cargo.lock generated
View file

@ -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"

View file

@ -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
View 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
View file

@ -0,0 +1 @@
pub mod gps_track;

View file

@ -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

View file

@ -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="&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors" attribution="&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors"
/> />
{{ move || to_polyline(&track.get()) }}
</MapContainer> </MapContainer>
</div> </div>
</ErrorBoundary> </ErrorBoundary>