#[cfg(target_arch = "wasm32")]
use gpx::read;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[cfg(target_arch = "wasm32")]
use std::io::Cursor;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[derive(Serialize)]
pub struct HeatmapTrack {
pub coordinates: Vec<[f64; 2]>,
pub frequency: u32,
}
#[derive(Serialize)]
pub struct HeatmapResult {
pub tracks: Vec<HeatmapTrack>,
pub max_frequency: u32,
}
#[derive(Serialize, Deserialize)]
pub struct ValidationResult {
valid_count: u32,
total_count: u32,
issues: Vec<String>,
}
#[derive(Serialize, Deserialize)]
pub struct TrackStatistics {
distance_km: f64,
point_count: u32,
bounding_box: [f64; 4], elevation_gain: Option<f64>,
average_speed: Option<f64>,
}
#[derive(Serialize, Deserialize)]
pub struct FileInfo {
format: String,
track_count: u32,
point_count: u32,
valid: bool,
file_size: u32,
}
#[derive(Serialize, Deserialize)]
pub struct IntersectionPoint {
coordinate: [f64; 2],
track_indices: Vec<u32>,
}
#[derive(Serialize, Deserialize)]
pub struct TrackCluster {
representative_track: Vec<[f64; 2]>,
member_indices: Vec<u32>,
similarity_score: f64,
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
pub fn decode_polyline(encoded: &str) -> Vec<[f64; 2]> {
let mut coords = Vec::new();
let mut lat = 0i32;
let mut lng = 0i32;
let mut index = 0;
let bytes = encoded.as_bytes();
while index < bytes.len() {
let mut shift = 0;
let mut result = 0i32;
loop {
if index >= bytes.len() || shift >= 30 {
break;
}
let b = bytes[index] as i32 - 63;
index += 1;
result |= (b & 0x1f) << shift;
shift += 5;
if b < 0x20 {
break;
}
}
let dlat = if (result & 1) != 0 {
!(result >> 1)
} else {
result >> 1
};
lat += dlat;
shift = 0;
result = 0;
loop {
if index >= bytes.len() || shift >= 30 {
break;
}
let b = bytes[index] as i32 - 63;
index += 1;
result |= (b & 0x1f) << shift;
shift += 5;
if b < 0x20 {
break;
}
}
let dlng = if (result & 1) != 0 {
!(result >> 1)
} else {
result >> 1
};
lng += dlng;
let lat_f64 = lat as f64 * 1e-5;
let lng_f64 = lng as f64 * 1e-5;
if is_valid_coordinate(lat_f64, lng_f64) {
coords.push([lat_f64, lng_f64]);
}
}
coords
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn decode_polyline_string(encoded: &str) -> JsValue {
let coords = decode_polyline(encoded);
serde_wasm_bindgen::to_value(&coords).unwrap()
}
pub fn process_polyline(polyline_str: &str) -> Vec<[f64; 2]> {
if let Ok(json_coords) = serde_json::from_str::<Vec<[f64; 2]>>(polyline_str) {
return if !json_coords.is_empty() {
filter_unrealistic_jumps(&json_coords)
} else {
Vec::new()
};
}
let coords = decode_polyline(polyline_str);
if !coords.is_empty() {
filter_unrealistic_jumps(&coords)
} else {
Vec::new()
}
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn process_polylines(polylines: js_sys::Array) -> JsValue {
let mut all_tracks: Vec<Vec<[f64; 2]>> = Vec::new();
for i in 0..polylines.length() {
if let Some(polyline_str) = polylines.get(i).as_string() {
let coords = process_polyline(&polyline_str);
if coords.len() > 1 {
let simplified = simplify_track(&coords, 0.00005);
if simplified.len() > 1 {
all_tracks.push(simplified);
}
}
}
}
let result = create_heatmap_from_tracks(all_tracks);
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
}
pub fn create_heatmap_from_tracks(all_tracks: Vec<Vec<[f64; 2]>>) -> HeatmapResult {
let mut segment_usage: HashMap<String, u32> = HashMap::new();
for track in &all_tracks {
for window in track.windows(2) {
if let [start, end] = window {
let segment_key = create_segment_key(*start, *end);
*segment_usage.entry(segment_key).or_insert(0) += 1;
}
}
}
let mut heatmap_tracks = Vec::new();
for track in all_tracks {
if track.len() < 2 {
continue;
}
let mut total_usage = 0;
let mut segment_count = 0;
for window in track.windows(2) {
if let [start, end] = window {
let segment_key = create_segment_key(*start, *end);
if let Some(&usage) = segment_usage.get(&segment_key) {
total_usage += usage;
segment_count += 1;
}
}
}
let track_frequency = if segment_count > 0 {
(total_usage as f64 / segment_count as f64).round() as u32
} else {
1
};
heatmap_tracks.push(HeatmapTrack {
coordinates: track,
frequency: track_frequency,
});
}
let max_frequency = heatmap_tracks
.iter()
.map(|track| track.frequency)
.max()
.unwrap_or(1);
HeatmapResult {
tracks: heatmap_tracks,
max_frequency,
}
}
pub fn round(value: f64) -> f64 {
(value * 100000.0).round() / 100000.0
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn process_gpx_files(files: js_sys::Array) -> JsValue {
let mut all_tracks: Vec<Vec<[f64; 2]>> = Vec::new();
for file_bytes in files.iter() {
let array = js_sys::Uint8Array::new(&file_bytes);
let bytes = array.to_vec();
if let Ok(gpx) = read(Cursor::new(&bytes)) {
for track in gpx.tracks {
for segment in track.segments {
let mut track_coords = Vec::new();
for point in segment.points {
let lat = round(point.point().y());
let lon = round(point.point().x());
if is_valid_coordinate(lat, lon) {
track_coords.push([lat, lon]);
}
}
if track_coords.len() > 1 {
let filtered_coords = filter_unrealistic_jumps(&track_coords);
if filtered_coords.len() > 1 {
let simplified = simplify_track(&filtered_coords, 0.00005);
if simplified.len() > 1 {
all_tracks.push(simplified);
}
}
}
}
}
}
else if is_fit_file(&bytes) {
let mut fit_parser = FitParser::new(bytes);
let fit_coordinates = fit_parser.parse_gps_coordinates();
if fit_coordinates.len() > 1 {
let filtered_coords = filter_unrealistic_jumps(&fit_coordinates);
if filtered_coords.len() > 1 {
let simplified = simplify_track(&filtered_coords, 0.00005);
if simplified.len() > 1 {
all_tracks.push(simplified);
}
}
}
}
else {
continue;
}
}
let mut segment_usage: HashMap<String, u32> = HashMap::new();
for track in &all_tracks {
for window in track.windows(2) {
if let [start, end] = window {
let segment_key = create_segment_key(*start, *end);
*segment_usage.entry(segment_key).or_insert(0) += 1;
}
}
}
let mut heatmap_tracks = Vec::new();
let mut max_frequency = 0;
for track in all_tracks {
if track.len() < 2 {
continue;
}
let mut total_usage = 0;
let mut segment_count = 0;
for window in track.windows(2) {
if let [start, end] = window {
let segment_key = create_segment_key(*start, *end);
if let Some(&usage) = segment_usage.get(&segment_key) {
total_usage += usage;
segment_count += 1;
}
}
}
let track_frequency = if segment_count > 0 {
(total_usage as f64 / segment_count as f64).round() as u32
} else {
1
};
max_frequency = max_frequency.max(track_frequency);
heatmap_tracks.push(HeatmapTrack {
coordinates: track,
frequency: track_frequency,
});
}
let result = HeatmapResult {
tracks: heatmap_tracks,
max_frequency,
};
serde_wasm_bindgen::to_value(&result).unwrap()
}
pub fn create_segment_key(start: [f64; 2], end: [f64; 2]) -> String {
let tolerance = 0.001; let snap_start = snap_to_grid(start, tolerance);
let snap_end = snap_to_grid(end, tolerance);
let (p1, p2) = if (snap_start[0], snap_start[1]) < (snap_end[0], snap_end[1]) {
(snap_start, snap_end)
} else {
(snap_end, snap_start)
};
format!("{:.4},{:.4}-{:.4},{:.4}", p1[0], p1[1], p2[0], p2[1])
}
pub fn snap_to_grid(point: [f64; 2], tolerance: f64) -> [f64; 2] {
[
(point[0] / tolerance).round() * tolerance,
(point[1] / tolerance).round() * tolerance,
]
}
fn simplify_track(points: &[[f64; 2]], tolerance: f64) -> Vec<[f64; 2]> {
if points.len() <= 2 {
return points.to_vec();
}
let mut result = vec![points[0]];
let mut last_added = 0;
for i in 1..points.len() {
let distance = distance(points[last_added], points[i]);
if distance > tolerance || i == points.len() - 1 {
result.push(points[i]);
last_added = i;
}
}
result
}
fn distance(p1: [f64; 2], p2: [f64; 2]) -> f64 {
let dx = p1[0] - p2[0];
let dy = p1[1] - p2[1];
(dx * dx + dy * dy).sqrt()
}
pub fn is_valid_coordinate(lat: f64, lon: f64) -> bool {
if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
return false;
}
if (lat == 0.0 && lon == 0.0)
|| lat.is_nan()
|| lon.is_nan()
|| lat.is_infinite()
|| lon.is_infinite()
{
return false;
}
true
}
pub fn filter_unrealistic_jumps(coords: &[[f64; 2]]) -> Vec<[f64; 2]> {
if coords.len() <= 1 {
return coords.to_vec();
}
let mut filtered = vec![coords[0]];
let max_jump_km = 100.0;
let mut consecutive_bad_points = 0;
const MAX_CONSECUTIVE_BAD: usize = 10;
for i in 1..coords.len() {
let prev = filtered.last().unwrap();
let curr = coords[i];
let distance_km = haversine_distance(prev[0], prev[1], curr[0], curr[1]);
if distance_km <= max_jump_km {
filtered.push(curr);
consecutive_bad_points = 0; } else {
consecutive_bad_points += 1;
if consecutive_bad_points <= MAX_CONSECUTIVE_BAD {
let mut found_good_continuation = false;
#[allow(clippy::needless_range_loop)]
for j in (i + 1)..(i + 21).min(coords.len()) {
let future_point = coords[j];
let future_distance =
haversine_distance(prev[0], prev[1], future_point[0], future_point[1]);
if future_distance <= max_jump_km * 1.5 {
found_good_continuation = true;
break;
}
}
if !found_good_continuation {
for k in (i + 1)..coords.len() {
let remaining_point = coords[k];
let remaining_distance = haversine_distance(
prev[0],
prev[1],
remaining_point[0],
remaining_point[1],
);
if remaining_distance <= max_jump_km {
filtered.push(remaining_point);
#[allow(clippy::needless_range_loop)]
for m in (k + 1)..coords.len() {
let next_prev = filtered.last().unwrap();
let next_curr = coords[m];
let next_distance = haversine_distance(
next_prev[0],
next_prev[1],
next_curr[0],
next_curr[1],
);
if next_distance <= max_jump_km {
filtered.push(next_curr);
}
}
break; }
}
break; }
} else {
break;
}
}
}
filtered
}
fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let r = 6371.0; let d_lat = (lat2 - lat1).to_radians();
let d_lon = (lon2 - lon1).to_radians();
let lat1_rad = lat1.to_radians();
let lat2_rad = lat2.to_radians();
let a =
(d_lat / 2.0).sin().powi(2) + lat1_rad.cos() * lat2_rad.cos() * (d_lon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
r * c
}
pub struct FitParser {
pub data: Vec<u8>,
pub pos: usize,
pub message_definitions: HashMap<u8, MessageDefinition>,
}
#[derive(Clone)]
pub struct MessageDefinition {
pub global_message_number: u16,
pub fields: Vec<FieldDefinition>,
}
#[derive(Clone)]
pub struct FieldDefinition {
pub field_def_num: u8,
pub size: u8,
pub _base_type: u8,
}
impl FitParser {
pub fn new(data: Vec<u8>) -> Self {
Self {
data,
pos: 0,
message_definitions: HashMap::new(),
}
}
pub fn read_u8(&mut self) -> Option<u8> {
if self.pos < self.data.len() {
let val = self.data[self.pos];
self.pos += 1;
Some(val)
} else {
None
}
}
pub fn read_u16_le(&mut self) -> Option<u16> {
if self.pos + 1 < self.data.len() {
let val = u16::from_le_bytes([self.data[self.pos], self.data[self.pos + 1]]);
self.pos += 2;
Some(val)
} else {
None
}
}
pub fn read_u32_le(&mut self) -> Option<u32> {
if self.pos + 3 < self.data.len() {
let val = u32::from_le_bytes([
self.data[self.pos],
self.data[self.pos + 1],
self.data[self.pos + 2],
self.data[self.pos + 3],
]);
self.pos += 4;
Some(val)
} else {
None
}
}
pub fn read_i32_le(&mut self) -> Option<i32> {
if self.pos + 3 < self.data.len() {
let val = i32::from_le_bytes([
self.data[self.pos],
self.data[self.pos + 1],
self.data[self.pos + 2],
self.data[self.pos + 3],
]);
self.pos += 4;
Some(val)
} else {
None
}
}
pub fn skip(&mut self, bytes: usize) {
self.pos = (self.pos + bytes).min(self.data.len());
}
pub fn parse_gps_coordinates(&mut self) -> Vec<[f64; 2]> {
let mut coordinates = Vec::new();
if self.data.len() < 14 {
return coordinates;
}
let header_size = self.read_u8().unwrap_or(0);
if header_size < 12 {
return coordinates;
}
let _protocol_version = self.read_u8().unwrap_or(0);
let _profile_version = self.read_u16_le().unwrap_or(0);
let data_size = self.read_u32_le().unwrap_or(0);
let signature = [
self.read_u8().unwrap_or(0),
self.read_u8().unwrap_or(0),
self.read_u8().unwrap_or(0),
self.read_u8().unwrap_or(0),
];
if signature != [b'.', b'F', b'I', b'T'] {
return coordinates;
}
if header_size == 14 {
self.skip(2);
}
let header_data_end = (self.pos + data_size as usize).min(self.data.len());
let file_data_end = self.data.len().saturating_sub(2); let data_end = header_data_end.max(file_data_end);
let mut consecutive_errors = 0;
const MAX_CONSECUTIVE_ERRORS: usize = 100; let mut processed_bytes = 0;
let mut last_progress_pos = self.pos;
while self.pos < data_end && self.pos < self.data.len() && self.pos + 1 < self.data.len() {
let start_pos = self.pos;
if self.pos - last_progress_pos > 10000 {
processed_bytes += self.pos - last_progress_pos;
last_progress_pos = self.pos;
if coordinates.len() > 100 && processed_bytes > 50000 {
consecutive_errors = 0; }
}
if self.pos >= self.data.len() {
break;
}
let record_header = match self.read_u8() {
Some(header) => header,
None => break, };
let is_definition = (record_header & 0x40) != 0;
let local_message_type = record_header & 0x0F;
let parse_success = if is_definition {
match self.parse_definition_message() {
Some(definition) => {
self.message_definitions
.insert(local_message_type, definition);
true
}
None => {
false
}
}
} else {
if let Some(definition) = self.message_definitions.get(&local_message_type).cloned()
{
let total_size: usize = definition.fields.iter().map(|f| f.size as usize).sum();
if self.pos + total_size > self.data.len() {
if total_size < 1000 {
self.skip(self.data.len() - self.pos); }
break;
}
match definition.global_message_number {
20 => {
if let Some(coord) = self.parse_record_message(&definition) {
if is_valid_coordinate(coord[0], coord[1]) {
coordinates.push(coord);
}
}
true
}
19 => {
if let Some(coord) = self.parse_flexible_gps_message(&definition) {
if is_valid_coordinate(coord[0], coord[1]) {
coordinates.push(coord);
}
}
true
}
18 => {
if let Some(coord) = self.parse_flexible_gps_message(&definition) {
if is_valid_coordinate(coord[0], coord[1]) {
coordinates.push(coord);
}
}
true
}
_ => {
let total_size: usize =
definition.fields.iter().map(|f| f.size as usize).sum();
if total_size < 1000 && self.pos + total_size <= self.data.len() {
self.skip(total_size);
} else {
self.skip(self.data.len() - self.pos);
break;
}
true
}
}
} else {
false
}
};
if parse_success {
consecutive_errors = 0; } else {
consecutive_errors += 1;
if self.pos == start_pos {
self.skip(1);
}
if consecutive_errors >= MAX_CONSECUTIVE_ERRORS {
if coordinates.len() < 100 {
break; } else {
consecutive_errors = MAX_CONSECUTIVE_ERRORS / 2; }
}
}
}
coordinates
}
pub fn parse_definition_message(&mut self) -> Option<MessageDefinition> {
let _start_pos = self.pos;
if self.pos + 5 > self.data.len() {
return None;
}
self.skip(1); self.skip(1); let global_message_number = self.read_u16_le()?;
let num_fields = self.read_u8()?;
if num_fields > 100 {
return None;
}
if self.pos + (num_fields as usize * 3) > self.data.len() {
return None;
}
let mut fields = Vec::new();
for _ in 0..num_fields {
if self.pos + 3 > self.data.len() {
return None;
}
let field_def_num = self.read_u8()?;
let size = self.read_u8()?;
let base_type = self.read_u8()?;
if size > 100 {
return None;
}
fields.push(FieldDefinition {
field_def_num,
size,
_base_type: base_type,
});
}
Some(MessageDefinition {
global_message_number,
fields,
})
}
pub fn parse_record_message(&mut self, definition: &MessageDefinition) -> Option<[f64; 2]> {
let mut lat: Option<f64> = None;
let mut lon: Option<f64> = None;
for field in &definition.fields {
if field.size == 0
|| self.pos >= self.data.len()
|| self.pos + field.size as usize > self.data.len()
{
let safe_skip = (self.data.len() - self.pos).min(field.size as usize);
self.skip(safe_skip);
continue;
}
match field.field_def_num {
0 => {
if field.size == 4 {
if let Some(lat_raw) = self.read_i32_le() {
if lat_raw != 0x7FFFFFFF && lat_raw != 0 {
let lat_degrees = lat_raw as f64 * (180.0 / 2147483648.0);
if lat_degrees.abs() <= 90.0 {
lat = Some(lat_degrees);
}
}
}
} else {
self.skip(field.size as usize);
}
}
1 => {
if field.size == 4 {
if let Some(lon_raw) = self.read_i32_le() {
if lon_raw != 0x7FFFFFFF && lon_raw != 0 {
let lon_degrees = lon_raw as f64 * (180.0 / 2147483648.0);
if lon_degrees.abs() <= 180.0 {
lon = Some(lon_degrees);
}
}
}
} else {
self.skip(field.size as usize);
}
}
_ => {
self.skip(field.size as usize);
}
}
}
if let (Some(lat_val), Some(lon_val)) = (lat, lon) {
Some([round(lat_val), round(lon_val)])
} else {
None
}
}
pub fn parse_flexible_gps_message(
&mut self,
definition: &MessageDefinition,
) -> Option<[f64; 2]> {
let mut lat: Option<f64> = None;
let mut lon: Option<f64> = None;
let mut potential_coords = Vec::new();
for field in &definition.fields {
if field.size == 0
|| self.pos >= self.data.len()
|| self.pos + field.size as usize > self.data.len()
{
let safe_skip = (self.data.len() - self.pos).min(field.size as usize);
self.skip(safe_skip);
continue;
}
if field.size == 4 {
if let Some(value) = self.read_i32_le() {
if value != 0x7FFFFFFF && value != 0 {
let degrees = value as f64 * (180.0 / 2147483648.0);
if degrees.abs() <= 180.0 {
potential_coords.push(degrees);
}
}
}
} else {
self.skip(field.size as usize);
}
}
for coord in &potential_coords {
if coord.abs() <= 90.0 && lat.is_none() {
lat = Some(*coord);
} else if coord.abs() <= 180.0 && lon.is_none() && Some(*coord) != lat {
lon = Some(*coord);
}
}
if let (Some(lat_val), Some(lon_val)) = (lat, lon) {
Some([round(lat_val), round(lon_val)])
} else {
None
}
}
}
pub fn is_fit_file(data: &[u8]) -> bool {
if data.len() < 12 {
return false;
}
data[8] == b'.' && data[9] == b'F' && data[10] == b'I' && data[11] == b'T'
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn validate_coordinates(coords: js_sys::Array) -> JsValue {
let mut issues = Vec::new();
let mut valid_count = 0;
for i in 0..coords.length() {
if let Ok(coord_array) = serde_wasm_bindgen::from_value::<[f64; 2]>(coords.get(i)) {
let [lat, lon] = coord_array;
if !is_valid_coordinate(lat, lon) {
issues.push(format!(
"invalid coordinate at index {}: [{}, {}]",
i, lat, lon
));
} else {
valid_count += 1;
}
}
}
let result = ValidationResult {
valid_count,
total_count: coords.length(),
issues,
};
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
}
pub fn validate_coordinates_rust(coords: &[[f64; 2]]) -> (u32, Vec<String>) {
let mut issues = Vec::new();
let mut valid_count = 0;
for (i, &[lat, lon]) in coords.iter().enumerate() {
if !is_valid_coordinate(lat, lon) {
issues.push(format!("invalid coordinate at index {i}: [{lat}, {lon}]"));
} else {
valid_count += 1;
}
}
(valid_count, issues)
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn filter_coordinates_by_bounds(coords: js_sys::Array, bounds: js_sys::Array) -> JsValue {
if bounds.length() != 4 {
return JsValue::NULL;
}
let min_lat = bounds.get(0).as_f64().unwrap_or(-90.0);
let min_lon = bounds.get(1).as_f64().unwrap_or(-180.0);
let max_lat = bounds.get(2).as_f64().unwrap_or(90.0);
let max_lon = bounds.get(3).as_f64().unwrap_or(180.0);
let mut filtered = Vec::new();
for i in 0..coords.length() {
if let Ok(coord_array) = serde_wasm_bindgen::from_value::<[f64; 2]>(coords.get(i)) {
let [lat, lon] = coord_array;
if lat >= min_lat && lat <= max_lat && lon >= min_lon && lon <= max_lon {
filtered.push([lat, lon]);
}
}
}
serde_wasm_bindgen::to_value(&filtered).unwrap_or(JsValue::NULL)
}
pub fn filter_coordinates_by_bounds_rust(coords: &[[f64; 2]], bounds: [f64; 4]) -> Vec<[f64; 2]> {
let [min_lat, min_lon, max_lat, max_lon] = bounds;
coords
.iter()
.filter(|&&[lat, lon]| lat >= min_lat && lat <= max_lat && lon >= min_lon && lon <= max_lon)
.copied()
.collect()
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn calculate_track_statistics(coords: js_sys::Array) -> JsValue {
if coords.length() == 0 {
return JsValue::NULL;
}
let mut coordinates = Vec::new();
let mut min_lat = f64::MAX;
let mut max_lat = f64::MIN;
let mut min_lng = f64::MAX;
let mut max_lng = f64::MIN;
for i in 0..coords.length() {
if let Ok(coord_array) = serde_wasm_bindgen::from_value::<[f64; 2]>(coords.get(i)) {
let [lat, lon] = coord_array;
coordinates.push([lat, lon]);
min_lat = min_lat.min(lat);
max_lat = max_lat.max(lat);
min_lng = min_lng.min(lon);
max_lng = max_lng.max(lon);
}
}
let mut total_distance = 0.0;
for window in coordinates.windows(2) {
if let [start, end] = window {
total_distance += haversine_distance(start[0], start[1], end[0], end[1]);
}
}
let result = TrackStatistics {
distance_km: total_distance,
point_count: coordinates.len() as u32,
bounding_box: [min_lat, min_lng, max_lat, max_lng],
elevation_gain: None, average_speed: None, };
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
}
pub fn calculate_track_statistics_rust(coords: &[[f64; 2]]) -> Option<(f64, u32, [f64; 4])> {
if coords.is_empty() {
return None;
}
let bbox = calculate_bounding_box(coords);
let mut total_distance = 0.0;
for window in coords.windows(2) {
if let [start, end] = window {
total_distance += haversine_distance(start[0], start[1], end[0], end[1]);
}
}
Some((total_distance, coords.len() as u32, bbox))
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn simplify_coordinates(coords: js_sys::Array, tolerance: f64) -> JsValue {
let mut coordinates = Vec::new();
for i in 0..coords.length() {
if let Ok(coord_array) = serde_wasm_bindgen::from_value::<[f64; 2]>(coords.get(i)) {
coordinates.push(coord_array);
}
}
let simplified = simplify_track(&coordinates, tolerance);
serde_wasm_bindgen::to_value(&simplified).unwrap_or(JsValue::NULL)
}
pub fn simplify_coordinates_rust(coords: &[[f64; 2]], tolerance: f64) -> Vec<[f64; 2]> {
simplify_track(coords, tolerance)
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn merge_nearby_tracks(tracks: js_sys::Array, distance_threshold: f64) -> JsValue {
let mut track_list = Vec::new();
for i in 0..tracks.length() {
if let Ok(track) = serde_wasm_bindgen::from_value::<Vec<[f64; 2]>>(tracks.get(i)) {
track_list.push(track);
}
}
let merged = merge_similar_tracks(&track_list, distance_threshold);
serde_wasm_bindgen::to_value(&merged).unwrap_or(JsValue::NULL)
}
pub fn merge_nearby_tracks_rust(
tracks: &[Vec<[f64; 2]>],
distance_threshold: f64,
) -> Vec<Vec<[f64; 2]>> {
merge_similar_tracks(tracks, distance_threshold)
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn split_track_by_gaps(
coords: js_sys::Array,
max_gap_km: f64,
_max_time_gap_seconds: u32,
) -> JsValue {
let mut coordinates = Vec::new();
for i in 0..coords.length() {
if let Ok(coord_array) = serde_wasm_bindgen::from_value::<[f64; 2]>(coords.get(i)) {
coordinates.push(coord_array);
}
}
let split_tracks = split_by_spatial_gaps(&coordinates, max_gap_km);
serde_wasm_bindgen::to_value(&split_tracks).unwrap_or(JsValue::NULL)
}
pub fn split_track_by_gaps_rust(coords: &[[f64; 2]], max_gap_km: f64) -> Vec<Vec<[f64; 2]>> {
split_by_spatial_gaps(coords, max_gap_km)
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn coordinates_to_polyline(coords: js_sys::Array) -> String {
let mut coordinates = Vec::new();
for i in 0..coords.length() {
if let Ok(coord_array) = serde_wasm_bindgen::from_value::<[f64; 2]>(coords.get(i)) {
coordinates.push(coord_array);
}
}
encode_polyline(&coordinates)
}
pub fn coordinates_to_polyline_rust(coords: &[[f64; 2]]) -> String {
encode_polyline(coords)
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn coordinates_to_geojson(coords: js_sys::Array, properties: JsValue) -> JsValue {
let mut coordinates = Vec::new();
for i in 0..coords.length() {
if let Ok(coord_array) = serde_wasm_bindgen::from_value::<[f64; 2]>(coords.get(i)) {
coordinates.push([coord_array[1], coord_array[0]]);
}
}
let props = if properties.is_null() || properties.is_undefined() {
serde_json::json!({})
} else {
serde_wasm_bindgen::from_value(properties).unwrap_or(serde_json::json!({}))
};
let geojson = serde_json::json!({
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": coordinates
},
"properties": props
});
serde_wasm_bindgen::to_value(&geojson).unwrap_or(JsValue::NULL)
}
pub fn coordinates_to_geojson_rust(
coords: &[[f64; 2]],
properties: serde_json::Value,
) -> serde_json::Value {
let coordinates: Vec<[f64; 2]> = coords
.iter()
.map(|&[lat, lon]| [lon, lat]) .collect();
serde_json::json!({
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": coordinates
},
"properties": properties
})
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn export_to_gpx(tracks: js_sys::Array, _metadata: JsValue) -> String {
let mut gpx_content = String::from(
r#"<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="fastgeotoolkit">
"#,
);
for i in 0..tracks.length() {
if let Ok(track) = serde_wasm_bindgen::from_value::<Vec<[f64; 2]>>(tracks.get(i)) {
gpx_content.push_str(&format!(
" <trk>\n <name>Track {}</name>\n <trkseg>\n",
i + 1
));
for coord in track {
gpx_content.push_str(&format!(
" <trkpt lat=\"{:.6}\" lon=\"{:.6}\"></trkpt>\n",
coord[0], coord[1]
));
}
gpx_content.push_str(" </trkseg>\n </trk>\n");
}
}
gpx_content.push_str("</gpx>");
gpx_content
}
pub fn export_to_gpx_rust(tracks: &[Vec<[f64; 2]>]) -> String {
let mut gpx_content = String::from(
r#"<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="fastgeotoolkit">
"#,
);
for (i, track) in tracks.iter().enumerate() {
gpx_content.push_str(&format!(
" <trk>\n <name>Track {}</name>\n <trkseg>\n",
i + 1
));
for coord in track {
gpx_content.push_str(&format!(
" <trkpt lat=\"{:.6}\" lon=\"{:.6}\"></trkpt>\n",
coord[0], coord[1]
));
}
gpx_content.push_str(" </trkseg>\n </trk>\n");
}
gpx_content.push_str("</gpx>");
gpx_content
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn find_track_intersections(tracks: js_sys::Array, tolerance: f64) -> JsValue {
let mut track_list = Vec::new();
for i in 0..tracks.length() {
if let Ok(track) = serde_wasm_bindgen::from_value::<Vec<[f64; 2]>>(tracks.get(i)) {
track_list.push(track);
}
}
let intersections = find_intersections(&track_list, tolerance);
serde_wasm_bindgen::to_value(&intersections).unwrap_or(JsValue::NULL)
}
pub fn find_track_intersections_rust(
tracks: &[Vec<[f64; 2]>],
tolerance: f64,
) -> Vec<([f64; 2], Vec<u32>)> {
let intersections = find_intersections(tracks, tolerance);
intersections
.into_iter()
.map(|ip| (ip.coordinate, ip.track_indices))
.collect()
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn calculate_coverage_area(tracks: js_sys::Array) -> JsValue {
let mut all_points = Vec::new();
for i in 0..tracks.length() {
if let Ok(track) = serde_wasm_bindgen::from_value::<Vec<[f64; 2]>>(tracks.get(i)) {
all_points.extend(track);
}
}
if all_points.is_empty() {
return JsValue::NULL;
}
let bbox = calculate_bounding_box(&all_points);
let area_km2 = calculate_area_km2(&bbox);
let result = serde_json::json!({
"bounding_box": bbox,
"area_km2": area_km2,
"point_count": all_points.len()
});
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
}
pub fn calculate_coverage_area_rust(tracks: &[Vec<[f64; 2]>]) -> Option<([f64; 4], f64, usize)> {
let all_points: Vec<[f64; 2]> = tracks.iter().flatten().copied().collect();
if all_points.is_empty() {
return None;
}
let bbox = calculate_bounding_box(&all_points);
let area_km2 = calculate_area_km2(&bbox);
Some((bbox, area_km2, all_points.len()))
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn cluster_tracks_by_similarity(tracks: js_sys::Array, similarity_threshold: f64) -> JsValue {
let mut track_list = Vec::new();
for i in 0..tracks.length() {
if let Ok(track) = serde_wasm_bindgen::from_value::<Vec<[f64; 2]>>(tracks.get(i)) {
track_list.push(track);
}
}
let clusters = cluster_similar_tracks(&track_list, similarity_threshold);
serde_wasm_bindgen::to_value(&clusters).unwrap_or(JsValue::NULL)
}
pub fn cluster_tracks_by_similarity_rust(
tracks: &[Vec<[f64; 2]>],
similarity_threshold: f64,
) -> Vec<(Vec<[f64; 2]>, Vec<u32>, f64)> {
let clusters = cluster_similar_tracks(tracks, similarity_threshold);
clusters
.into_iter()
.map(|tc| {
(
tc.representative_track,
tc.member_indices,
tc.similarity_score,
)
})
.collect()
}
pub fn calculate_distance_between_points(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
haversine_distance(lat1, lon1, lat2, lon2)
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn get_bounding_box(coords: js_sys::Array) -> JsValue {
let mut coordinates = Vec::new();
for i in 0..coords.length() {
if let Ok(coord_array) = serde_wasm_bindgen::from_value::<[f64; 2]>(coords.get(i)) {
coordinates.push(coord_array);
}
}
if coordinates.is_empty() {
return JsValue::NULL;
}
let bbox = calculate_bounding_box(&coordinates);
serde_wasm_bindgen::to_value(&bbox).unwrap_or(JsValue::NULL)
}
pub fn get_bounding_box_rust(coords: &[[f64; 2]]) -> [f64; 4] {
calculate_bounding_box(coords)
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn resample_track(coords: js_sys::Array, target_point_count: u32) -> JsValue {
let mut coordinates = Vec::new();
for i in 0..coords.length() {
if let Ok(coord_array) = serde_wasm_bindgen::from_value::<[f64; 2]>(coords.get(i)) {
coordinates.push(coord_array);
}
}
let resampled = resample_coordinates(&coordinates, target_point_count as usize);
serde_wasm_bindgen::to_value(&resampled).unwrap_or(JsValue::NULL)
}
pub fn resample_track_rust(coords: &[[f64; 2]], target_point_count: usize) -> Vec<[f64; 2]> {
resample_coordinates(coords, target_point_count)
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn get_file_info(file_bytes: js_sys::Uint8Array) -> JsValue {
let bytes = file_bytes.to_vec();
let mut info = FileInfo {
format: "unknown".to_string(),
track_count: 0,
point_count: 0,
valid: false,
file_size: bytes.len() as u32,
};
if let Ok(gpx) = read(Cursor::new(&bytes)) {
info.format = "gpx".to_string();
info.valid = true;
info.track_count = gpx.tracks.len() as u32;
for track in gpx.tracks {
for segment in track.segments {
info.point_count += segment.points.len() as u32;
}
}
}
else if is_fit_file(&bytes) {
info.format = "fit".to_string();
info.valid = true;
info.track_count = 1;
let mut parser = FitParser::new(bytes);
let coords = parser.parse_gps_coordinates();
info.point_count = coords.len() as u32;
}
serde_wasm_bindgen::to_value(&info).unwrap_or(JsValue::NULL)
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn extract_file_metadata(file_bytes: js_sys::Uint8Array) -> JsValue {
let bytes = file_bytes.to_vec();
if let Ok(gpx) = read(Cursor::new(&bytes)) {
let metadata = serde_json::json!({
"format": "gpx",
"creator": gpx.creator,
"version": format!("{:?}", gpx.version),
"tracks": gpx.tracks.iter().map(|t| serde_json::json!({
"name": t.name,
"description": t.description,
"segment_count": t.segments.len()
})).collect::<Vec<_>>()
});
return serde_wasm_bindgen::to_value(&metadata).unwrap_or(JsValue::NULL);
}
if is_fit_file(&bytes) {
let metadata = serde_json::json!({
"format": "fit",
"file_size": bytes.len()
});
return serde_wasm_bindgen::to_value(&metadata).unwrap_or(JsValue::NULL);
}
JsValue::NULL
}
fn encode_polyline(coordinates: &[[f64; 2]]) -> String {
let mut encoded = String::new();
let mut prev_lat = 0i32;
let mut prev_lng = 0i32;
for coord in coordinates {
let lat = (coord[0] * 1e5).round() as i32;
let lng = (coord[1] * 1e5).round() as i32;
let d_lat = lat - prev_lat;
let d_lng = lng - prev_lng;
encoded.push_str(&encode_number(d_lat));
encoded.push_str(&encode_number(d_lng));
prev_lat = lat;
prev_lng = lng;
}
encoded
}
fn encode_number(num: i32) -> String {
let mut value = if num < 0 { (!num) << 1 | 1 } else { num << 1 };
let mut encoded = String::new();
while value >= 0x20 {
encoded.push(((0x20 | (value & 0x1f)) + 63) as u8 as char);
value >>= 5;
}
encoded.push((value + 63) as u8 as char);
encoded
}
fn merge_similar_tracks(tracks: &[Vec<[f64; 2]>], distance_threshold: f64) -> Vec<Vec<[f64; 2]>> {
let mut merged = Vec::new();
let mut used = vec![false; tracks.len()];
for i in 0..tracks.len() {
if used[i] {
continue;
}
let mut current_track = tracks[i].clone();
used[i] = true;
for j in (i + 1)..tracks.len() {
if used[j] {
continue;
}
if tracks_are_similar(¤t_track, &tracks[j], distance_threshold) {
current_track = merge_two_tracks(¤t_track, &tracks[j]);
used[j] = true;
}
}
merged.push(current_track);
}
merged
}
fn tracks_are_similar(track1: &[[f64; 2]], track2: &[[f64; 2]], threshold: f64) -> bool {
if track1.len() < 2 || track2.len() < 2 {
return false;
}
let start_dist = haversine_distance(track1[0][0], track1[0][1], track2[0][0], track2[0][1]);
let end_dist = haversine_distance(
track1[track1.len() - 1][0],
track1[track1.len() - 1][1],
track2[track2.len() - 1][0],
track2[track2.len() - 1][1],
);
start_dist <= threshold && end_dist <= threshold
}
fn merge_two_tracks(track1: &[[f64; 2]], track2: &[[f64; 2]]) -> Vec<[f64; 2]> {
if track1.len() >= track2.len() {
track1.to_vec()
} else {
track2.to_vec()
}
}
fn split_by_spatial_gaps(coordinates: &[[f64; 2]], max_gap_km: f64) -> Vec<Vec<[f64; 2]>> {
if coordinates.len() < 2 {
return vec![coordinates.to_vec()];
}
let mut tracks = Vec::new();
let mut current_track = vec![coordinates[0]];
for window in coordinates.windows(2) {
if let [current, next] = window {
let distance = haversine_distance(current[0], current[1], next[0], next[1]);
if distance <= max_gap_km {
current_track.push(*next);
} else {
if current_track.len() > 1 {
tracks.push(current_track);
}
current_track = vec![*next];
}
}
}
if current_track.len() > 1 {
tracks.push(current_track);
}
tracks
}
fn find_intersections(tracks: &[Vec<[f64; 2]>], tolerance: f64) -> Vec<IntersectionPoint> {
let mut intersections = Vec::new();
let mut intersection_map: HashMap<String, Vec<u32>> = HashMap::new();
for (i, track1) in tracks.iter().enumerate() {
for point1 in track1 {
let grid_key = format!(
"{:.4},{:.4}",
(point1[0] / tolerance).round() * tolerance,
(point1[1] / tolerance).round() * tolerance
);
intersection_map.entry(grid_key).or_default().push(i as u32);
}
}
for (key_str, track_indices) in intersection_map {
if track_indices.len() > 1 {
let coords: Vec<f64> = key_str
.split(',')
.map(|s| s.parse().unwrap_or(0.0))
.collect();
if coords.len() == 2 {
let mut unique_tracks = track_indices;
unique_tracks.sort();
unique_tracks.dedup();
if unique_tracks.len() > 1 {
intersections.push(IntersectionPoint {
coordinate: [coords[0], coords[1]],
track_indices: unique_tracks,
});
}
}
}
}
intersections
}
fn calculate_bounding_box(coordinates: &[[f64; 2]]) -> [f64; 4] {
let mut min_lat = f64::MAX;
let mut max_lat = f64::MIN;
let mut min_lng = f64::MAX;
let mut max_lng = f64::MIN;
for coord in coordinates {
min_lat = min_lat.min(coord[0]);
max_lat = max_lat.max(coord[0]);
min_lng = min_lng.min(coord[1]);
max_lng = max_lng.max(coord[1]);
}
[min_lat, min_lng, max_lat, max_lng]
}
fn calculate_area_km2(bbox: &[f64; 4]) -> f64 {
let [min_lat, min_lng, max_lat, max_lng] = *bbox;
let width_km = haversine_distance(min_lat, min_lng, min_lat, max_lng);
let height_km = haversine_distance(min_lat, min_lng, max_lat, min_lng);
width_km * height_km
}
fn cluster_similar_tracks(
tracks: &[Vec<[f64; 2]>],
similarity_threshold: f64,
) -> Vec<TrackCluster> {
let mut clusters = Vec::new();
let mut assigned = vec![false; tracks.len()];
for i in 0..tracks.len() {
if assigned[i] {
continue;
}
let mut cluster_members = vec![i as u32];
assigned[i] = true;
for j in (i + 1)..tracks.len() {
if assigned[j] {
continue;
}
let similarity = calculate_track_similarity(&tracks[i], &tracks[j]);
if similarity >= similarity_threshold {
cluster_members.push(j as u32);
assigned[j] = true;
}
}
clusters.push(TrackCluster {
representative_track: tracks[i].clone(),
member_indices: cluster_members,
similarity_score: 1.0, });
}
clusters
}
fn calculate_track_similarity(track1: &[[f64; 2]], track2: &[[f64; 2]]) -> f64 {
if track1.len() < 2 || track2.len() < 2 {
return 0.0;
}
let start_dist = haversine_distance(track1[0][0], track1[0][1], track2[0][0], track2[0][1]);
let end_dist = haversine_distance(
track1[track1.len() - 1][0],
track1[track1.len() - 1][1],
track2[track2.len() - 1][0],
track2[track2.len() - 1][1],
);
let max_reasonable_distance = 10.0; let start_similarity = (max_reasonable_distance - start_dist.min(max_reasonable_distance))
/ max_reasonable_distance;
let end_similarity =
(max_reasonable_distance - end_dist.min(max_reasonable_distance)) / max_reasonable_distance;
(start_similarity + end_similarity) / 2.0
}
fn resample_coordinates(coordinates: &[[f64; 2]], target_count: usize) -> Vec<[f64; 2]> {
if coordinates.len() <= target_count {
return coordinates.to_vec();
}
let mut resampled = Vec::new();
let step = coordinates.len() as f64 / target_count as f64;
for i in 0..target_count {
let index = (i as f64 * step) as usize;
if index < coordinates.len() {
resampled.push(coordinates[index]);
}
}
if let Some(last) = coordinates.last() {
if resampled.last() != Some(last) {
resampled.push(*last);
}
}
resampled
}