1use wasm_bindgen::prelude::*;
2use gpx::read;
3use std::io::Cursor;
4use serde::Serialize;
5use std::collections::HashMap;
6
7#[derive(Serialize)]
9pub struct HeatmapTrack {
10 coordinates: Vec<[f64; 2]>,
11 frequency: u32,
12}
13
14#[derive(Serialize)]
15pub struct HeatmapResult {
16 tracks: Vec<HeatmapTrack>,
17 max_frequency: u32,
18}
19
20#[wasm_bindgen]
22extern "C" {
23 #[wasm_bindgen(js_namespace = console)]
24 fn log(s: &str);
25}
26
27pub fn decode_polyline(encoded: &str) -> Vec<[f64; 2]> {
29 let mut coords = Vec::new();
30 let mut lat = 0i32;
31 let mut lng = 0i32;
32 let mut index = 0;
33 let bytes = encoded.as_bytes();
34
35 while index < bytes.len() {
36 let mut shift = 0;
38 let mut result = 0i32;
39 loop {
40 if index >= bytes.len() {
41 break;
42 }
43 let b = bytes[index] as i32 - 63;
44 index += 1;
45 result |= (b & 0x1f) << shift;
46 shift += 5;
47 if b < 0x20 {
48 break;
49 }
50 }
51 let dlat = if (result & 1) != 0 { !(result >> 1) } else { result >> 1 };
52 lat += dlat;
53
54 shift = 0;
56 result = 0;
57 loop {
58 if index >= bytes.len() {
59 break;
60 }
61 let b = bytes[index] as i32 - 63;
62 index += 1;
63 result |= (b & 0x1f) << shift;
64 shift += 5;
65 if b < 0x20 {
66 break;
67 }
68 }
69 let dlng = if (result & 1) != 0 { !(result >> 1) } else { result >> 1 };
70 lng += dlng;
71
72 let lat_f64 = lat as f64 * 1e-5;
74 let lng_f64 = lng as f64 * 1e-5;
75
76 if is_valid_coordinate(lat_f64, lng_f64) {
77 coords.push([lat_f64, lng_f64]);
78 }
79 }
80
81 coords
82}
83
84#[wasm_bindgen]
86pub fn decode_polyline_string(encoded: &str) -> JsValue {
87 let coords = decode_polyline(encoded);
88 serde_wasm_bindgen::to_value(&coords).unwrap()
89}
90
91fn process_polyline(polyline_str: &str) -> Vec<[f64; 2]> {
93 if let Ok(json_coords) = serde_json::from_str::<Vec<[f64; 2]>>(polyline_str) {
95 return if !json_coords.is_empty() {
97 filter_unrealistic_jumps(&json_coords)
98 } else {
99 Vec::new()
100 };
101 }
102
103 let coords = decode_polyline(polyline_str);
105 if !coords.is_empty() {
106 filter_unrealistic_jumps(&coords)
107 } else {
108 Vec::new()
109 }
110}
111
112#[wasm_bindgen]
114pub fn process_polylines(polylines: js_sys::Array) -> JsValue {
115 let mut all_tracks: Vec<Vec<[f64; 2]>> = Vec::new();
116
117 for i in 0..polylines.length() {
119 if let Some(polyline_str) = polylines.get(i).as_string() {
120 let coords = process_polyline(&polyline_str);
121 if coords.len() > 1 {
122 let simplified = simplify_track(&coords, 0.00005);
123 if simplified.len() > 1 {
124 all_tracks.push(simplified);
125 }
126 }
127 }
128 }
129
130 let result = create_heatmap_from_tracks(all_tracks);
132
133 serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
134}
135
136fn create_heatmap_from_tracks(all_tracks: Vec<Vec<[f64; 2]>>) -> HeatmapResult {
138 let mut segment_usage: HashMap<String, u32> = HashMap::new();
140
141 for track in &all_tracks {
143 for window in track.windows(2) {
144 if let [start, end] = window {
145 let segment_key = create_segment_key(*start, *end);
146 *segment_usage.entry(segment_key).or_insert(0) += 1;
147 }
148 }
149 }
150
151 let mut heatmap_tracks = Vec::new();
153
154 for track in all_tracks {
155 if track.len() < 2 {
156 continue;
157 }
158
159 let mut total_usage = 0;
161 let mut segment_count = 0;
162
163 for window in track.windows(2) {
164 if let [start, end] = window {
165 let segment_key = create_segment_key(*start, *end);
166 if let Some(&usage) = segment_usage.get(&segment_key) {
167 total_usage += usage;
168 segment_count += 1;
169 }
170 }
171 }
172
173 let track_frequency = if segment_count > 0 {
175 (total_usage as f64 / segment_count as f64).round() as u32
176 } else {
177 1
178 };
179
180 heatmap_tracks.push(HeatmapTrack {
181 coordinates: track,
182 frequency: track_frequency,
183 });
184 }
185
186 let max_frequency = heatmap_tracks.iter()
188 .map(|track| track.frequency)
189 .max()
190 .unwrap_or(1);
191
192 HeatmapResult {
193 tracks: heatmap_tracks,
194 max_frequency,
195 }
196}
197
198fn round(value: f64) -> f64 {
199 (value * 100000.0).round() / 100000.0
200}
201
202#[wasm_bindgen]
203pub fn process_gpx_files(files: js_sys::Array) -> JsValue {
204 let mut all_tracks: Vec<Vec<[f64; 2]>> = Vec::new();
205
206 for file_bytes in files.iter() {
208 let array = js_sys::Uint8Array::new(&file_bytes);
209 let bytes = array.to_vec();
210
211 if let Ok(gpx) = read(Cursor::new(&bytes)) {
213 for track in gpx.tracks {
214 for segment in track.segments {
215 let mut track_coords = Vec::new();
216
217 for point in segment.points {
218 let lat = round(point.point().y());
219 let lon = round(point.point().x());
220
221 if is_valid_coordinate(lat, lon) {
223 track_coords.push([lat, lon]);
224 }
225 }
226
227 if track_coords.len() > 1 {
228 let filtered_coords = filter_unrealistic_jumps(&track_coords);
230
231 if filtered_coords.len() > 1 {
232 let simplified = simplify_track(&filtered_coords, 0.00005);
234 if simplified.len() > 1 {
235 all_tracks.push(simplified);
236 }
237 }
238 }
239 }
240 }
241 }
242 else if is_fit_file(&bytes) {
244 let mut fit_parser = FitParser::new(bytes);
246 let fit_coordinates = fit_parser.parse_gps_coordinates();
247
248 if fit_coordinates.len() > 1 {
250 let filtered_coords = filter_unrealistic_jumps(&fit_coordinates);
251
252 if filtered_coords.len() > 1 {
253 let simplified = simplify_track(&filtered_coords, 0.00005);
254 if simplified.len() > 1 {
255 all_tracks.push(simplified);
256 }
257 }
258 }
259 }
260 else {
262 continue;
263 }
264 }
265
266 let mut segment_usage: HashMap<String, u32> = HashMap::new();
268
269 for track in &all_tracks {
271 for window in track.windows(2) {
272 if let [start, end] = window {
273 let segment_key = create_segment_key(*start, *end);
274 *segment_usage.entry(segment_key).or_insert(0) += 1;
275 }
276 }
277 }
278
279 let mut heatmap_tracks = Vec::new();
281 let mut max_frequency = 0;
282
283 for track in all_tracks {
284 if track.len() < 2 {
285 continue;
286 }
287
288 let mut total_usage = 0;
290 let mut segment_count = 0;
291
292 for window in track.windows(2) {
293 if let [start, end] = window {
294 let segment_key = create_segment_key(*start, *end);
295 if let Some(&usage) = segment_usage.get(&segment_key) {
296 total_usage += usage;
297 segment_count += 1;
298 }
299 }
300 }
301
302 let track_frequency = if segment_count > 0 {
304 (total_usage as f64 / segment_count as f64).round() as u32
305 } else {
306 1
307 };
308
309 max_frequency = max_frequency.max(track_frequency);
310
311 heatmap_tracks.push(HeatmapTrack {
312 coordinates: track,
313 frequency: track_frequency,
314 });
315 }
316
317 let result = HeatmapResult {
318 tracks: heatmap_tracks,
319 max_frequency,
320 };
321
322 serde_wasm_bindgen::to_value(&result).unwrap()
323}
324
325fn create_segment_key(start: [f64; 2], end: [f64; 2]) -> String {
326 let tolerance = 0.001; let snap_start = snap_to_grid(start, tolerance);
329 let snap_end = snap_to_grid(end, tolerance);
330
331 let (p1, p2) = if (snap_start[0], snap_start[1]) < (snap_end[0], snap_end[1]) {
333 (snap_start, snap_end)
334 } else {
335 (snap_end, snap_start)
336 };
337
338 format!("{:.4},{:.4}-{:.4},{:.4}", p1[0], p1[1], p2[0], p2[1])
339}
340
341fn snap_to_grid(point: [f64; 2], tolerance: f64) -> [f64; 2] {
342 [
343 (point[0] / tolerance).round() * tolerance,
344 (point[1] / tolerance).round() * tolerance,
345 ]
346}
347
348fn simplify_track(points: &[[f64; 2]], tolerance: f64) -> Vec<[f64; 2]> {
349 if points.len() <= 2 {
350 return points.to_vec();
351 }
352
353 let mut result = vec![points[0]];
354 let mut last_added = 0;
355
356 for i in 1..points.len() {
357 let distance = distance(points[last_added], points[i]);
358
359 if distance > tolerance || i == points.len() - 1 {
362 result.push(points[i]);
363 last_added = i;
364 }
365 }
366
367 result
368}
369
370fn distance(p1: [f64; 2], p2: [f64; 2]) -> f64 {
371 let dx = p1[0] - p2[0];
372 let dy = p1[1] - p2[1];
373 (dx * dx + dy * dy).sqrt()
374}
375
376fn is_valid_coordinate(lat: f64, lon: f64) -> bool {
377 if lat < -90.0 || lat > 90.0 || lon < -180.0 || lon > 180.0 {
379 return false;
380 }
381
382 if (lat == 0.0 && lon == 0.0) || lat.is_nan() || lon.is_nan() || lat.is_infinite() || lon.is_infinite() {
384 return false;
385 }
386
387 true
388}
389
390fn filter_unrealistic_jumps(coords: &[[f64; 2]]) -> Vec<[f64; 2]> {
391 if coords.len() <= 1 {
392 return coords.to_vec();
393 }
394
395 let mut filtered = vec![coords[0]];
396 let max_jump_km = 100.0; let mut consecutive_bad_points = 0;
398 const MAX_CONSECUTIVE_BAD: usize = 10; for i in 1..coords.len() {
401 let prev = filtered.last().unwrap();
402 let curr = coords[i];
403
404 let distance_km = haversine_distance(prev[0], prev[1], curr[0], curr[1]);
406
407 if distance_km <= max_jump_km {
409 filtered.push(curr);
410 consecutive_bad_points = 0; } else {
412 consecutive_bad_points += 1;
413
414 if consecutive_bad_points <= MAX_CONSECUTIVE_BAD {
416 let mut found_good_continuation = false;
418 for j in (i + 1)..(i + 21).min(coords.len()) {
419 let future_point = coords[j];
420 let future_distance = haversine_distance(prev[0], prev[1], future_point[0], future_point[1]);
421
422 if future_distance <= max_jump_km * 1.5 { found_good_continuation = true;
425 break;
426 }
427 }
428
429 if !found_good_continuation {
431 for k in (i + 1)..coords.len() {
433 let remaining_point = coords[k];
434 let remaining_distance = haversine_distance(prev[0], prev[1], remaining_point[0], remaining_point[1]);
435
436 if remaining_distance <= max_jump_km {
438 filtered.push(remaining_point);
439 for m in (k + 1)..coords.len() {
441 let next_prev = filtered.last().unwrap();
442 let next_curr = coords[m];
443 let next_distance = haversine_distance(next_prev[0], next_prev[1], next_curr[0], next_curr[1]);
444
445 if next_distance <= max_jump_km {
446 filtered.push(next_curr);
447 }
448 }
450 break; }
452 }
453 break; }
455 } else {
456 break;
458 }
459 }
461 }
462
463 filtered
464}
465
466fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
467 let r = 6371.0; let d_lat = (lat2 - lat1).to_radians();
469 let d_lon = (lon2 - lon1).to_radians();
470 let lat1_rad = lat1.to_radians();
471 let lat2_rad = lat2.to_radians();
472
473 let a = (d_lat / 2.0).sin().powi(2) + lat1_rad.cos() * lat2_rad.cos() * (d_lon / 2.0).sin().powi(2);
474 let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
475
476 r * c
477}
478
479struct FitParser {
483 data: Vec<u8>,
484 pos: usize,
485 message_definitions: HashMap<u8, MessageDefinition>,
486}
487
488#[derive(Clone)]
489struct MessageDefinition {
490 global_message_number: u16,
491 fields: Vec<FieldDefinition>,
492}
493
494#[derive(Clone)]
495struct FieldDefinition {
496 field_def_num: u8,
497 size: u8,
498 _base_type: u8,
499}
500
501impl FitParser {
502 fn new(data: Vec<u8>) -> Self {
503 Self {
504 data,
505 pos: 0,
506 message_definitions: HashMap::new(),
507 }
508 }
509
510 fn read_u8(&mut self) -> Option<u8> {
511 if self.pos < self.data.len() {
512 let val = self.data[self.pos];
513 self.pos += 1;
514 Some(val)
515 } else {
516 None
517 }
518 }
519
520 fn read_u16_le(&mut self) -> Option<u16> {
521 if self.pos + 1 < self.data.len() {
522 let val = u16::from_le_bytes([self.data[self.pos], self.data[self.pos + 1]]);
523 self.pos += 2;
524 Some(val)
525 } else {
526 None
527 }
528 }
529
530 fn read_u32_le(&mut self) -> Option<u32> {
531 if self.pos + 3 < self.data.len() {
532 let val = u32::from_le_bytes([
533 self.data[self.pos],
534 self.data[self.pos + 1],
535 self.data[self.pos + 2],
536 self.data[self.pos + 3],
537 ]);
538 self.pos += 4;
539 Some(val)
540 } else {
541 None
542 }
543 }
544
545 fn read_i32_le(&mut self) -> Option<i32> {
546 if self.pos + 3 < self.data.len() {
547 let val = i32::from_le_bytes([
548 self.data[self.pos],
549 self.data[self.pos + 1],
550 self.data[self.pos + 2],
551 self.data[self.pos + 3],
552 ]);
553 self.pos += 4;
554 Some(val)
555 } else {
556 None
557 }
558 }
559
560 fn skip(&mut self, bytes: usize) {
561 self.pos = (self.pos + bytes).min(self.data.len());
562 }
563
564 fn parse_gps_coordinates(&mut self) -> Vec<[f64; 2]> {
565 let mut coordinates = Vec::new();
566
567 if self.data.len() < 14 {
569 return coordinates;
570 }
571
572 let header_size = self.read_u8().unwrap_or(0);
574 if header_size < 12 {
575 return coordinates;
576 }
577
578 let _protocol_version = self.read_u8().unwrap_or(0);
579 let _profile_version = self.read_u16_le().unwrap_or(0);
580 let data_size = self.read_u32_le().unwrap_or(0);
581
582 let signature = [
584 self.read_u8().unwrap_or(0),
585 self.read_u8().unwrap_or(0),
586 self.read_u8().unwrap_or(0),
587 self.read_u8().unwrap_or(0),
588 ];
589 if signature != [b'.', b'F', b'I', b'T'] {
590 return coordinates;
591 }
592
593 if header_size == 14 {
595 self.skip(2);
596 }
597
598 let header_data_end = (self.pos + data_size as usize).min(self.data.len());
602 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;
606 const MAX_CONSECUTIVE_ERRORS: usize = 100; let mut processed_bytes = 0;
608 let mut last_progress_pos = self.pos;
609
610 while self.pos < data_end && self.pos < self.data.len() && self.pos + 1 < self.data.len() {
612 let start_pos = self.pos;
613
614 if self.pos - last_progress_pos > 10000 {
616 processed_bytes += self.pos - last_progress_pos;
617 last_progress_pos = self.pos;
618
619 if coordinates.len() > 100 && processed_bytes > 50000 {
621 consecutive_errors = 0; }
623 }
624
625 if self.pos >= self.data.len() {
627 break;
628 }
629
630 let record_header = match self.read_u8() {
631 Some(header) => header,
632 None => break, };
634
635 let is_definition = (record_header & 0x40) != 0;
636 let local_message_type = record_header & 0x0F;
637
638 let parse_success = if is_definition {
639 match self.parse_definition_message() {
641 Some(definition) => {
642 self.message_definitions.insert(local_message_type, definition);
643 true
644 }
645 None => {
646 false
648 }
649 }
650 } else {
651 if let Some(definition) = self.message_definitions.get(&local_message_type).cloned() {
653 let total_size: usize = definition.fields.iter().map(|f| f.size as usize).sum();
655 if self.pos + total_size > self.data.len() {
656 if total_size < 1000 { self.skip(self.data.len() - self.pos); }
660 break;
661 }
662
663 match definition.global_message_number {
665 20 => {
666 if let Some(coord) = self.parse_record_message(&definition) {
668 if is_valid_coordinate(coord[0], coord[1]) {
669 coordinates.push(coord);
670 }
671 }
672 true
673 }
674 19 => {
675 if let Some(coord) = self.parse_flexible_gps_message(&definition) {
677 if is_valid_coordinate(coord[0], coord[1]) {
678 coordinates.push(coord);
679 }
680 }
681 true
682 }
683 18 => {
684 if let Some(coord) = self.parse_flexible_gps_message(&definition) {
686 if is_valid_coordinate(coord[0], coord[1]) {
687 coordinates.push(coord);
688 }
689 }
690 true
691 }
692 _ => {
693 let total_size: usize = definition.fields.iter().map(|f| f.size as usize).sum();
695 if total_size < 1000 && self.pos + total_size <= self.data.len() {
696 self.skip(total_size);
697 } else {
698 self.skip(self.data.len() - self.pos);
700 break;
701 }
702 true
703 }
704 }
705 } else {
706 false
708 }
709 };
710
711 if parse_success {
712 consecutive_errors = 0; } else {
714 consecutive_errors += 1;
715
716 if self.pos == start_pos {
718 self.skip(1);
720 }
721
722 if consecutive_errors >= MAX_CONSECUTIVE_ERRORS {
724 if coordinates.len() < 100 {
726 break; } else {
728 consecutive_errors = MAX_CONSECUTIVE_ERRORS / 2; }
731 }
732 }
733 }
734
735 coordinates
736 }
737
738 fn parse_definition_message(&mut self) -> Option<MessageDefinition> {
739 let _start_pos = self.pos;
740
741 if self.pos + 5 > self.data.len() {
743 return None;
744 }
745
746 self.skip(1); self.skip(1); let global_message_number = self.read_u16_le()?;
749 let num_fields = self.read_u8()?;
750
751 if num_fields > 100 {
753 return None;
755 }
756
757 if self.pos + (num_fields as usize * 3) > self.data.len() {
759 return None;
760 }
761
762 let mut fields = Vec::new();
763 for _ in 0..num_fields {
764 if self.pos + 3 > self.data.len() {
766 return None;
768 }
769
770 let field_def_num = self.read_u8()?;
771 let size = self.read_u8()?;
772 let base_type = self.read_u8()?;
773
774 if size > 100 {
776 return None;
778 }
779
780 fields.push(FieldDefinition {
781 field_def_num,
782 size,
783 _base_type: base_type,
784 });
785 }
786
787 Some(MessageDefinition {
788 global_message_number,
789 fields,
790 })
791 }
792
793 fn parse_record_message(&mut self, definition: &MessageDefinition) -> Option<[f64; 2]> {
794 let mut lat: Option<f64> = None;
795 let mut lon: Option<f64> = None;
796
797 for field in &definition.fields {
798 if field.size == 0 || self.pos >= self.data.len() || self.pos + field.size as usize > self.data.len() {
800 let safe_skip = (self.data.len() - self.pos).min(field.size as usize);
802 self.skip(safe_skip);
803 continue;
804 }
805
806 match field.field_def_num {
807 0 => {
808 if field.size == 4 {
810 if let Some(lat_raw) = self.read_i32_le() {
811 if lat_raw != 0x7FFFFFFF && lat_raw != 0 {
812 let lat_degrees = lat_raw as f64 * (180.0 / 2147483648.0);
813 if lat_degrees.abs() <= 90.0 {
814 lat = Some(lat_degrees);
815 }
816 }
817 }
818 } else {
819 self.skip(field.size as usize);
820 }
821 }
822 1 => {
823 if field.size == 4 {
825 if let Some(lon_raw) = self.read_i32_le() {
826 if lon_raw != 0x7FFFFFFF && lon_raw != 0 {
827 let lon_degrees = lon_raw as f64 * (180.0 / 2147483648.0);
828 if lon_degrees.abs() <= 180.0 {
829 lon = Some(lon_degrees);
830 }
831 }
832 }
833 } else {
834 self.skip(field.size as usize);
835 }
836 }
837 _ => {
838 self.skip(field.size as usize);
840 }
841 }
842 }
843
844 if let (Some(lat_val), Some(lon_val)) = (lat, lon) {
845 Some([round(lat_val), round(lon_val)])
846 } else {
847 None
848 }
849 }
850
851 fn parse_flexible_gps_message(&mut self, definition: &MessageDefinition) -> Option<[f64; 2]> {
852 let mut lat: Option<f64> = None;
853 let mut lon: Option<f64> = None;
854 let mut potential_coords = Vec::new();
855
856 for field in &definition.fields {
858 if field.size == 0 || self.pos >= self.data.len() || self.pos + field.size as usize > self.data.len() {
860 let safe_skip = (self.data.len() - self.pos).min(field.size as usize);
862 self.skip(safe_skip);
863 continue;
864 }
865
866 if field.size == 4 {
867 if let Some(value) = self.read_i32_le() {
868 if value != 0x7FFFFFFF && value != 0 {
869 let degrees = value as f64 * (180.0 / 2147483648.0);
870 if degrees.abs() <= 180.0 {
872 potential_coords.push(degrees);
873 }
874 }
875 }
876 } else {
877 self.skip(field.size as usize);
878 }
879 }
880
881 for coord in &potential_coords {
883 if coord.abs() <= 90.0 && lat.is_none() {
884 lat = Some(*coord);
885 } else if coord.abs() <= 180.0 && lon.is_none() && Some(*coord) != lat {
886 lon = Some(*coord);
887 }
888 }
889
890 if let (Some(lat_val), Some(lon_val)) = (lat, lon) {
891 Some([round(lat_val), round(lon_val)])
892 } else {
893 None
894 }
895 }
896}
897
898fn is_fit_file(data: &[u8]) -> bool {
899 if data.len() < 12 {
900 return false;
901 }
902
903 data[8] == b'.' && data[9] == b'F' && data[10] == b'I' && data[11] == b'T'
905}