1use crate::tag::{Tag, TagGroup, TagId};
8use crate::value::Value;
9
10const KNOTS_TO_KPH: f64 = 1.852;
12const MPS_TO_KPH: f64 = 3.6;
13const MPH_TO_KPH: f64 = 1.60934;
14
15#[derive(Debug, Clone, Default)]
19pub struct TrackInfo {
20 pub handler_type: [u8; 4],
22 pub meta_format: Option<String>,
24 pub media_timescale: u32,
26 pub stco: Vec<u64>,
28 pub stsc: Vec<(u32, u32, u32)>,
30 pub stsz: Vec<u32>,
32 pub stts: Vec<(u32, u32)>,
34}
35
36#[derive(Debug, Clone, Default)]
38pub struct StreamState {
39 pub tracks: Vec<TrackInfo>,
40 pub current: TrackInfo,
42 pub in_stbl: bool,
44}
45
46pub fn extract_stream_tags(
51 data: &[u8],
52 tracks: &[TrackInfo],
53 _extract_embedded: u8,
54) -> Vec<Tag> {
55 let mut tags = Vec::new();
56 let mut doc_count: u32 = 0;
57
58 for track in tracks {
59 let handler = &track.handler_type;
60 if handler == b"soun" || handler == b"vide" {
62 continue;
63 }
64
65 let samples = compute_samples(track);
67 if samples.is_empty() {
68 continue;
69 }
70
71 let meta_format = track.meta_format.as_deref().unwrap_or("");
72
73 for s in &samples {
74 if s.offset as usize + s.size as usize > data.len() || s.size == 0 {
75 continue;
76 }
77 let sample_data =
78 &data[s.offset as usize..(s.offset as usize + s.size as usize)];
79
80 let mut sample_tags = Vec::new();
81
82 let dispatched = dispatch_sample(
84 sample_data,
85 handler,
86 meta_format,
87 s.time,
88 s.duration,
89 &mut sample_tags,
90 );
91
92 if dispatched && !sample_tags.is_empty() {
93 doc_count += 1;
94 if let Some(t) = s.time {
96 sample_tags.insert(
97 0,
98 mk_stream(
99 "SampleTime",
100 "Sample Time",
101 Value::String(format!("{:.6}", t)),
102 ),
103 );
104 }
105 if let Some(d) = s.duration {
106 sample_tags.insert(
107 1,
108 mk_stream(
109 "SampleDuration",
110 "Sample Duration",
111 Value::String(format!("{:.6}", d)),
112 ),
113 );
114 }
115 for t in &mut sample_tags {
117 t.description = format!("{} (Doc{})", t.description, doc_count);
118 }
119 tags.extend(sample_tags);
120 }
121 }
122 }
123
124 if doc_count == 0 {
126 scan_mdat_for_freegps(data, &mut tags, &mut doc_count);
127 }
128
129 tags
130}
131
132struct SampleInfo {
135 offset: u64,
136 size: u32,
137 time: Option<f64>,
138 duration: Option<f64>,
139}
140
141fn compute_samples(track: &TrackInfo) -> Vec<SampleInfo> {
142 let mut result = Vec::new();
143 if track.stsz.is_empty() || track.stco.is_empty() || track.stsc.is_empty() {
144 return result;
145 }
146
147 let ts = if track.media_timescale > 0 {
148 track.media_timescale as f64
149 } else {
150 1.0
151 };
152
153 let mut stts_flat: Vec<(u32, u32)> = Vec::new();
155 for &(count, delta) in &track.stts {
156 stts_flat.push((count, delta));
157 }
158 let mut stts_idx = 0;
159 let mut stts_remaining: u32 = if !stts_flat.is_empty() {
160 stts_flat[0].0
161 } else {
162 0
163 };
164 let mut stts_delta: u32 = if !stts_flat.is_empty() {
165 stts_flat[0].1
166 } else {
167 0
168 };
169 let mut time_acc: u64 = 0;
170 let has_time = !stts_flat.is_empty();
171
172 let mut stsc_idx = 0;
174 let mut samples_per_chunk = track.stsc[0].1;
175 let mut next_first_chunk: Option<u32> = if track.stsc.len() > 1 {
176 Some(track.stsc[1].0)
177 } else {
178 None
179 };
180
181 let mut sample_idx: usize = 0;
182
183 for (chunk_idx_0, &chunk_offset) in track.stco.iter().enumerate() {
184 let chunk_num = chunk_idx_0 as u32 + 1; if let Some(nfc) = next_first_chunk {
188 if chunk_num >= nfc {
189 stsc_idx += 1;
190 if stsc_idx < track.stsc.len() {
191 samples_per_chunk = track.stsc[stsc_idx].1;
192 next_first_chunk = if stsc_idx + 1 < track.stsc.len() {
193 Some(track.stsc[stsc_idx + 1].0)
194 } else {
195 None
196 };
197 }
198 }
199 }
200
201 let mut offset_in_chunk: u64 = 0;
202 for _ in 0..samples_per_chunk {
203 if sample_idx >= track.stsz.len() {
204 break;
205 }
206 let sz = track.stsz[sample_idx];
207 let sample_time = if has_time {
208 Some(time_acc as f64 / ts)
209 } else {
210 None
211 };
212 let sample_dur = if has_time {
213 Some(stts_delta as f64 / ts)
214 } else {
215 None
216 };
217
218 result.push(SampleInfo {
219 offset: chunk_offset + offset_in_chunk,
220 size: sz,
221 time: sample_time,
222 duration: sample_dur,
223 });
224
225 offset_in_chunk += sz as u64;
226 sample_idx += 1;
227
228 if has_time {
230 time_acc += stts_delta as u64;
231 if stts_remaining > 0 {
232 stts_remaining -= 1;
233 }
234 if stts_remaining == 0 {
235 stts_idx += 1;
236 if stts_idx < stts_flat.len() {
237 stts_remaining = stts_flat[stts_idx].0;
238 stts_delta = stts_flat[stts_idx].1;
239 }
240 }
241 }
242 }
243 }
244
245 result
246}
247
248fn dispatch_sample(
251 sample: &[u8],
252 handler: &[u8; 4],
253 meta_format: &str,
254 _time: Option<f64>,
255 _dur: Option<f64>,
256 tags: &mut Vec<Tag>,
257) -> bool {
258 match meta_format {
260 "camm" => return process_camm(sample, tags),
261 "gpmd" => return process_gpmd(sample, tags),
262 "mebx" => return process_mebx(sample, tags),
263 "tx3g" => return process_tx3g(sample, tags),
264 _ => {}
265 }
266
267 match handler {
269 b"text" | b"sbtl" => {
270 if meta_format == "tx3g" {
272 return process_tx3g(sample, tags);
273 }
274 return process_text(sample, tags);
275 }
276 b"gps " => {
277 if sample.len() >= 12 && &sample[4..12] == b"freeGPS " {
279 return process_freegps(sample, tags);
280 }
281 return process_nmea(sample, tags);
283 }
284 b"meta" | b"data" => {
285 match meta_format {
287 "RVMI" => return process_rvmi(sample, tags),
288 _ => {
289 if sample.len() >= 12 && &sample[4..12] == b"freeGPS " {
290 return process_freegps(sample, tags);
291 }
292 }
293 }
294 }
295 _ => {
296 if sample.starts_with(b"VIDEO") && sample.windows(2).any(|w| w == b"\xfe\xfe") {
298 return process_kenwood(sample, tags);
299 }
300 }
301 }
302 false
303}
304
305fn process_freegps(data: &[u8], tags: &mut Vec<Tag>) -> bool {
308 if data.len() < 82 {
309 return false;
310 }
311
312 if data.len() > 26 && &data[18..26] == b"\xaa\xaa\xf2\xe1\xf0\xee\x54\x54" {
314 return process_freegps_type1_encrypted(data, tags);
315 }
316
317 if data.len() > 64 {
319 if let Some(dt) = try_ascii_digits(&data[52..], 14) {
320 if dt.len() == 14 {
321 return process_freegps_type2_nmea(data, tags);
322 }
323 }
324 }
325
326 if data.len() > 75 && data[72] == b'A' && is_ns(data[73]) && is_ew(data[74]) && data[75] == 0
328 {
329 return process_freegps_novatek(data, tags);
330 }
331
332 if data.len() > 44 && &data[37..41] == b"\0\0\0A" && is_ns(data[41]) && is_ew(data[42]) {
334 return process_freegps_viofo(data, 0, tags);
335 }
336 if data.len() > 92 && &data[85..89] == b"\0\0\0A" && is_ns(data[89]) && is_ew(data[90]) {
337 return process_freegps_viofo(data, 48, tags);
339 }
340
341 if data.len() > 96
343 && data[60] == b'A'
344 && data[61] == 0
345 && data[62] == 0
346 && data[63] == 0
347 && is_ns(data[68])
348 && is_ew(data[76])
349 {
350 return process_freegps_akaso(data, tags);
351 }
352
353 if data.len() > 100 && data[64] == b'A' && is_ns(data[65]) && is_ew(data[66]) && data[67] == 0
355 {
356 return process_freegps_vantrue_s1(data, tags);
357 }
358
359 if data.len() >= 0x88
361 && data[60] == b'A'
362 && data[61] == 0
363 && is_ns(data[72])
364 && data[73] == 0
365 && is_ew(data[88])
366 && data[89] == 0
367 {
368 return process_freegps_type12(data, tags);
369 }
370
371 if data.len() > 48 && data[16] == b'A' && is_ns(data[17]) && is_ew(data[18]) && data[19] == 0
373 {
374 return process_freegps_innovv(data, tags);
375 }
376
377 if data.len() > 80
379 && data[28] == b'A'
380 && is_ns(data[40])
381 && is_ew(data[56])
382 {
383 return process_freegps_vantrue_n4(data, tags);
384 }
385
386 if data.len() > 0x50 {
388 return process_freegps_nextbase_binary(data, tags);
389 }
390
391 false
392}
393
394fn process_freegps_type1_encrypted(data: &[u8], tags: &mut Vec<Tag>) -> bool {
397 let n = (data.len() - 18).min(0x101);
398 let decrypted: Vec<u8> = data[18..18 + n].iter().map(|b| b ^ 0xaa).collect();
399
400 if decrypted.len() < 66 {
401 return false;
402 }
403
404 let dt_bytes = &decrypted[8..22];
406 let dt_str = match std::str::from_utf8(dt_bytes) {
407 Ok(s) if s.chars().all(|c| c.is_ascii_digit()) => s,
408 _ => return false,
409 };
410 if dt_str.len() < 14 {
411 return false;
412 }
413 let yr = &dt_str[0..4];
414 let mo = &dt_str[4..6];
415 let dy = &dt_str[6..8];
416 let hr = &dt_str[8..10];
417 let mi = &dt_str[10..12];
418 let se = &dt_str[12..14];
419
420 if decrypted.len() < 57 {
422 return false;
423 }
424 let lat_ref = decrypted[37];
425 if lat_ref != b'N' && lat_ref != b'S' {
426 return false;
427 }
428 let lon_ref = decrypted[46];
429 if lon_ref != b'E' && lon_ref != b'W' {
430 return false;
431 }
432 let lat_str = match std::str::from_utf8(&decrypted[38..46]) {
433 Ok(s) if s.chars().all(|c| c.is_ascii_digit()) => s,
434 _ => return false,
435 };
436 let lon_str = match std::str::from_utf8(&decrypted[47..56]) {
437 Ok(s) if s.chars().all(|c| c.is_ascii_digit()) => s,
438 _ => return false,
439 };
440 let lat: f64 = lat_str.parse::<f64>().unwrap_or(0.0) / 1e4;
441 let lon: f64 = lon_str.parse::<f64>().unwrap_or(0.0) / 1e4;
442 let (lat_dd, lon_dd) = convert_lat_lon(lat, lon);
443 let lat_final = lat_dd * if lat_ref == b'S' { -1.0 } else { 1.0 };
444 let lon_final = lon_dd * if lon_ref == b'W' { -1.0 } else { 1.0 };
445
446 tags.push(mk_gps_dt(&format!(
447 "{}:{}:{} {}:{}:{}Z",
448 yr, mo, dy, hr, mi, se
449 )));
450 tags.push(mk_gps_lat(lat_final));
451 tags.push(mk_gps_lon(lon_final));
452
453 if decrypted.len() >= 65 {
455 if let Ok(s) = std::str::from_utf8(&decrypted[56..64]) {
456 if let Ok(spd) = s.trim_start_matches('0').parse::<f64>() {
457 tags.push(mk_gps_spd(spd));
458 }
459 }
460 }
461
462 true
463}
464
465fn process_freegps_type2_nmea(data: &[u8], tags: &mut Vec<Tag>) -> bool {
468 if let Some(dt) = try_ascii_digits(&data[52..], 14) {
470 if dt.len() >= 14 {
471 let cam_dt = format!(
472 "{}:{}:{} {}:{}:{}",
473 &dt[0..4],
474 &dt[4..6],
475 &dt[6..8],
476 &dt[8..10],
477 &dt[10..12],
478 &dt[12..14]
479 );
480 tags.push(mk_stream(
481 "CameraDateTime",
482 "Camera Date/Time",
483 Value::String(cam_dt),
484 ));
485 }
486 }
487
488 let text = String::from_utf8_lossy(data);
490 if parse_nmea_rmc(&text, tags) {
491 return true;
492 }
493 if parse_nmea_gga(&text, tags) {
494 return true;
495 }
496 false
497}
498
499fn process_freegps_novatek(data: &[u8], tags: &mut Vec<Tag>) -> bool {
502 if data.len() < 0x5c {
507 return false;
508 }
509 let hr = get_u32_le(data, 0x30);
510 let min = get_u32_le(data, 0x34);
511 let sec = get_u32_le(data, 0x38);
512 let yr = get_u32_le(data, 0x3c);
513 let mon = get_u32_le(data, 0x40);
514 let day = get_u32_le(data, 0x44);
515 let lat_ref = data[0x49];
516 let lon_ref = data[0x4a];
517
518 if mon < 1 || mon > 12 || day < 1 || day > 31 {
519 return false;
520 }
521
522 let full_yr = if yr < 2000 { yr + 2000 } else { yr };
523
524 let lat = get_f32_le(data, 0x4c) as f64;
525 let lon = get_f32_le(data, 0x50) as f64;
526 let spd = get_f32_le(data, 0x54) as f64 * KNOTS_TO_KPH;
527 let trk = get_f32_le(data, 0x58) as f64;
528
529 let (lat_dd, lon_dd) = convert_lat_lon(lat, lon);
530 let lat_final = lat_dd * if lat_ref == b'S' { -1.0 } else { 1.0 };
531 let lon_final = lon_dd * if lon_ref == b'W' { -1.0 } else { 1.0 };
532
533 tags.push(mk_gps_dt(&format!(
534 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}Z",
535 full_yr, mon, day, hr, min, sec
536 )));
537 tags.push(mk_gps_lat(lat_final));
538 tags.push(mk_gps_lon(lon_final));
539 tags.push(mk_gps_spd(spd));
540 tags.push(mk_gps_trk(trk));
541
542 true
543}
544
545fn process_freegps_viofo(data: &[u8], extra_offset: usize, tags: &mut Vec<Tag>) -> bool {
548 let d = if extra_offset > 0 && data.len() > extra_offset {
549 &data[extra_offset..]
550 } else {
551 data
552 };
553 if d.len() < 0x3c {
554 return false;
555 }
556
557 let hr = get_u32_le(d, 0x10);
558 let min = get_u32_le(d, 0x14);
559 let sec = get_u32_le(d, 0x18);
560 let yr = get_u32_le(d, 0x1c);
561 let mon = get_u32_le(d, 0x20);
562 let day = get_u32_le(d, 0x24);
563
564 let lat_ref = d[0x29]; let lon_ref = d[0x2a]; if mon < 1 || mon > 12 || day < 1 || day > 31 {
568 return false;
569 }
570
571 let full_yr = if yr < 2000 { yr + 2000 } else { yr };
572
573 let lat = get_f32_le(d, 0x2c) as f64;
574 let lon = get_f32_le(d, 0x30) as f64;
575 let spd = get_f32_le(d, 0x34) as f64 * KNOTS_TO_KPH;
576 let trk = get_f32_le(d, 0x38) as f64;
577
578 tags.push(mk_gps_dt(&format!(
579 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}Z",
580 full_yr, mon, day, hr, min, sec
581 )));
582 tags.push(mk_gps_lat(lat * if lat_ref == b'S' { -1.0 } else { 1.0 }));
583 tags.push(mk_gps_lon(lon * if lon_ref == b'W' { -1.0 } else { 1.0 }));
584 tags.push(mk_gps_spd(spd));
585 tags.push(mk_gps_trk(trk));
586
587 true
588}
589
590fn process_freegps_akaso(data: &[u8], tags: &mut Vec<Tag>) -> bool {
593 if data.len() < 0x58 {
594 return false;
595 }
596 let lat_ref = data[68];
597 let lon_ref = data[76];
598 let hr = get_u32_le(data, 48);
599 let min = get_u32_le(data, 52);
600 let sec = get_u32_le(data, 56);
601 let yr = get_u32_le(data, 84);
602 let mon = get_u32_le(data, 88);
603 let day = get_u32_le(data, 92);
604
605 if mon < 1 || mon > 12 {
606 return false;
607 }
608
609 let lat = get_f32_le(data, 0x40) as f64;
610 let lon = get_f32_le(data, 0x48) as f64;
611 let spd = get_f32_le(data, 0x50) as f64;
612 let trk = get_f32_le(data, 0x54) as f64;
613
614 tags.push(mk_gps_dt(&format!(
615 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}Z",
616 yr, mon, day, hr, min, sec
617 )));
618 tags.push(mk_gps_lat(lat * if lat_ref == b'S' { -1.0 } else { 1.0 }));
619 tags.push(mk_gps_lon(lon * if lon_ref == b'W' { -1.0 } else { 1.0 }));
620 tags.push(mk_gps_spd(spd));
621 tags.push(mk_gps_trk(trk));
622
623 true
624}
625
626fn process_freegps_vantrue_s1(data: &[u8], tags: &mut Vec<Tag>) -> bool {
629 if data.len() < 0x70 {
630 return false;
631 }
632 let lat_ref = data[65];
633 let lon_ref = data[66];
634
635 let yr = get_u32_le(data, 68);
636 let mon = get_u32_le(data, 72);
637 let day = get_u32_le(data, 76);
638 let hr = get_u32_le(data, 80);
639 let min = get_u32_le(data, 84);
640 let sec = get_u32_le(data, 88);
641
642 if mon < 1 || mon > 12 || day < 1 || day > 31 {
643 return false;
644 }
645
646 let lon = get_f32_le(data, 0x5c) as f64;
647 let lat = get_f32_le(data, 0x60) as f64;
648 let spd = get_f32_le(data, 0x64) as f64 * KNOTS_TO_KPH;
649 let trk = get_f32_le(data, 0x68) as f64;
650 let alt = get_f32_le(data, 0x6c) as f64;
651
652 tags.push(mk_gps_dt(&format!(
653 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}Z",
654 yr, mon, day, hr, min, sec
655 )));
656 tags.push(mk_gps_lat(lat * if lat_ref == b'S' { -1.0 } else { 1.0 }));
657 tags.push(mk_gps_lon(lon * if lon_ref == b'W' { -1.0 } else { 1.0 }));
658 tags.push(mk_gps_spd(spd));
659 tags.push(mk_gps_trk(trk));
660 tags.push(mk_gps_alt(alt));
661
662 true
663}
664
665fn process_freegps_type12(data: &[u8], tags: &mut Vec<Tag>) -> bool {
668 if data.len() < 0x88 {
669 return false;
670 }
671 let lat_ref = data[72];
672 let lon_ref = data[88];
673
674 let hr = get_u32_le(data, 48);
675 let min = get_u32_le(data, 52);
676 let sec = get_u32_le(data, 56);
677 let yr = get_u32_le(data, 0x70);
678 let mon = get_u32_le(data, 0x74);
679 let day = get_u32_le(data, 0x78);
680
681 if mon < 1 || mon > 12 {
682 return false;
683 }
684
685 let full_yr = if yr < 2000 { yr + 2000 } else { yr };
686
687 let lat = get_f64_le(data, 0x40);
688 let lon = get_f64_le(data, 0x50);
689 let spd = get_f64_le(data, 0x60) * KNOTS_TO_KPH;
690 let trk = get_f64_le(data, 0x68);
691
692 let (lat_dd, lon_dd) = convert_lat_lon(lat, lon);
693
694 tags.push(mk_gps_dt(&format!(
695 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}Z",
696 full_yr, mon, day, hr, min, sec
697 )));
698 tags.push(mk_gps_lat(lat_dd * if lat_ref == b'S' { -1.0 } else { 1.0 }));
699 tags.push(mk_gps_lon(lon_dd * if lon_ref == b'W' { -1.0 } else { 1.0 }));
700 tags.push(mk_gps_spd(spd));
701 tags.push(mk_gps_trk(trk));
702
703 true
704}
705
706fn process_freegps_innovv(data: &[u8], tags: &mut Vec<Tag>) -> bool {
709 let mut pos = 16;
711 let mut found = false;
712 while pos + 32 <= data.len() {
713 if data[pos] != b'A' || !is_ns(data[pos + 1]) || !is_ew(data[pos + 2]) || data[pos + 3] != 0
714 {
715 break;
716 }
717 let lat_ref = data[pos + 1];
718 let lon_ref = data[pos + 2];
719 let lat = get_f32_le(data, pos + 4).abs() as f64;
720 let lon = get_f32_le(data, pos + 8).abs() as f64;
721 let spd = get_f32_le(data, pos + 12) as f64 * KNOTS_TO_KPH;
722 let trk = get_f32_le(data, pos + 16) as f64;
723
724 let (lat_dd, lon_dd) = convert_lat_lon(lat, lon);
725 tags.push(mk_gps_lat(lat_dd * if lat_ref == b'S' { -1.0 } else { 1.0 }));
726 tags.push(mk_gps_lon(lon_dd * if lon_ref == b'W' { -1.0 } else { 1.0 }));
727 tags.push(mk_gps_spd(spd));
728 tags.push(mk_gps_trk(trk));
729 found = true;
730 pos += 32;
731 }
732 found
733}
734
735fn process_freegps_vantrue_n4(data: &[u8], tags: &mut Vec<Tag>) -> bool {
738 if data.len() < 80 {
739 return false;
740 }
741 let lat_ref = data[40];
742 let lon_ref = data[56];
743
744 let hr = get_u32_le(data, 16);
745 let min = get_u32_le(data, 20);
746 let sec = get_u32_le(data, 24);
747
748 if data.len() < 92 {
750 return false;
751 }
752 let yr = get_u32_le(data, 80);
753 let mon = get_u32_le(data, 84);
754 let day = get_u32_le(data, 88);
755
756 if mon < 1 || mon > 12 {
757 return false;
758 }
759
760 let lat = get_f64_le(data, 32).abs();
761 let lon = get_f64_le(data, 48).abs();
762 let spd = get_f64_le(data, 64) * KNOTS_TO_KPH;
763 let trk = get_f64_le(data, 72);
764
765 tags.push(mk_gps_dt(&format!(
766 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}Z",
767 yr, mon, day, hr, min, sec
768 )));
769 tags.push(mk_gps_lat(lat * if lat_ref == b'S' { -1.0 } else { 1.0 }));
770 tags.push(mk_gps_lon(lon * if lon_ref == b'W' { -1.0 } else { 1.0 }));
771 tags.push(mk_gps_spd(spd));
772 tags.push(mk_gps_trk(trk));
773
774 true
775}
776
777fn process_freegps_nextbase_binary(data: &[u8], tags: &mut Vec<Tag>) -> bool {
780 let mut pos = 0x32usize;
783 let mut found = false;
784 while pos + 0x1e <= data.len() {
785 let spd_raw = get_u16_be(data, pos);
786 let trk_raw = get_u16_be(data, pos + 2) as i16;
787 let yr = get_u16_be(data, pos + 4);
788 let mon = data[pos + 6];
789 let day = data[pos + 7];
790 let hr = data[pos + 8];
791 let min = data[pos + 9];
792 let sec10 = get_u16_be(data, pos + 10);
793
794 if yr < 2000 || yr > 2200 || mon < 1 || mon > 12 || day < 1 || day > 31 || hr > 59 || min > 59 || sec10 > 600
795 {
796 break;
797 }
798
799 let lat_raw = get_u32_be(data, pos + 13);
800 let lon_raw = get_u32_be(data, pos + 17);
801 let lat = signed_u32(lat_raw) as f64 / 1e7;
802 let lon = signed_u32(lon_raw) as f64 / 1e7;
803 let mut trk = trk_raw as f64 / 100.0;
804 if trk < 0.0 {
805 trk += 360.0;
806 }
807
808 let time = format!(
809 "{:04}:{:02}:{:02} {:02}:{:02}:{:04.1}Z",
810 yr,
811 mon,
812 day,
813 hr,
814 min,
815 sec10 as f64 / 10.0
816 );
817 tags.push(mk_gps_dt(&time));
818 tags.push(mk_gps_lat(lat));
819 tags.push(mk_gps_lon(lon));
820 tags.push(mk_gps_spd(spd_raw as f64 / 100.0 * MPS_TO_KPH));
821 tags.push(mk_gps_trk(trk));
822 found = true;
823
824 pos += 0x20;
825 }
826 found
827}
828
829fn process_camm(data: &[u8], tags: &mut Vec<Tag>) -> bool {
832 if data.len() < 4 {
833 return false;
834 }
835 let camm_type = get_u16_le(data, 2);
836
837 match camm_type {
838 0 => {
839 if data.len() >= 16 {
841 let x = get_f32_le(data, 4);
842 let y = get_f32_le(data, 8);
843 let z = get_f32_le(data, 12);
844 tags.push(mk_stream(
845 "AngleAxis",
846 "Angle Axis",
847 Value::String(format!("{} {} {}", x, y, z)),
848 ));
849 return true;
850 }
851 }
852 2 => {
853 if data.len() >= 16 {
855 let x = get_f32_le(data, 4);
856 let y = get_f32_le(data, 8);
857 let z = get_f32_le(data, 12);
858 tags.push(mk_stream(
859 "AngularVelocity",
860 "Angular Velocity",
861 Value::String(format!("{} {} {}", x, y, z)),
862 ));
863 return true;
864 }
865 }
866 3 => {
867 if data.len() >= 16 {
869 let x = get_f32_le(data, 4);
870 let y = get_f32_le(data, 8);
871 let z = get_f32_le(data, 12);
872 tags.push(mk_stream(
873 "Accelerometer",
874 "Accelerometer",
875 Value::String(format!("{} {} {}", x, y, z)),
876 ));
877 return true;
878 }
879 }
880 5 => {
881 if data.len() >= 28 {
883 let lat = get_f64_le(data, 4);
884 let lon = get_f64_le(data, 12);
885 let alt = get_f64_le(data, 20);
886 tags.push(mk_gps_lat(lat));
887 tags.push(mk_gps_lon(lon));
888 tags.push(mk_gps_alt(alt));
889 return true;
890 }
891 }
892 6 => {
893 if data.len() >= 60 {
895 let _timestamp = get_f64_le(data, 4);
896 let lat = get_f64_le(data, 0x10);
897 let lon = get_f64_le(data, 0x18);
898 let alt = get_f32_le(data, 0x20) as f64;
899
900 tags.push(mk_gps_lat(lat));
901 tags.push(mk_gps_lon(lon));
902 tags.push(mk_gps_alt(alt));
903
904 if data.len() >= 0x38 {
905 let vel_east = get_f32_le(data, 0x2c);
906 let vel_north = get_f32_le(data, 0x30);
907 let speed = ((vel_east * vel_east + vel_north * vel_north) as f64).sqrt()
908 * MPS_TO_KPH;
909 tags.push(mk_gps_spd(speed));
910 }
911 return true;
912 }
913 }
914 7 => {
915 if data.len() >= 16 {
917 let x = get_f32_le(data, 4);
918 let y = get_f32_le(data, 8);
919 let z = get_f32_le(data, 12);
920 tags.push(mk_stream(
921 "MagneticField",
922 "Magnetic Field",
923 Value::String(format!("{} {} {}", x, y, z)),
924 ));
925 return true;
926 }
927 }
928 _ => {}
929 }
930 false
931}
932
933fn process_gpmd(data: &[u8], tags: &mut Vec<Tag>) -> bool {
936 process_gpmf_klv(data, 0, data.len(), tags)
939}
940
941fn process_gpmf_klv(data: &[u8], start: usize, end: usize, tags: &mut Vec<Tag>) -> bool {
942 let mut pos = start;
943 let mut found = false;
944
945 while pos + 8 <= end {
946 let fourcc = &data[pos..pos + 4];
947 let type_byte = data[pos + 4];
948 let size_byte = data[pos + 5];
949 let repeat = get_u16_be(data, pos + 6) as usize;
950
951 let struct_size = size_byte as usize;
952 let total_data = struct_size * repeat;
953 let padded = (total_data + 3) & !3;
955 let data_start = pos + 8;
956
957 if data_start + padded > end {
958 break;
959 }
960
961 if type_byte == 0 && struct_size == 4 {
962 if process_gpmf_klv(data, data_start, data_start + total_data, tags) {
964 found = true;
965 }
966 } else if fourcc == b"GPS5" && struct_size >= 20 && type_byte == b'l' {
967 for i in 0..repeat {
969 let off = data_start + i * struct_size;
970 if off + 20 > end {
971 break;
972 }
973 let lat = get_i32_be(data, off) as f64 / 1e7;
974 let lon = get_i32_be(data, off + 4) as f64 / 1e7;
975 let alt = get_i32_be(data, off + 8) as f64 / 100.0;
976 let speed2d = get_i32_be(data, off + 12) as f64 / 100.0 * MPS_TO_KPH;
977
978 tags.push(mk_gps_lat(lat));
979 tags.push(mk_gps_lon(lon));
980 tags.push(mk_gps_alt(alt));
981 tags.push(mk_gps_spd(speed2d));
982 found = true;
983 }
984 } else if fourcc == b"GPSU" && type_byte == b'U' && total_data >= 16 {
985 if let Ok(s) = std::str::from_utf8(&data[data_start..data_start + total_data.min(16)])
987 {
988 let s = s.trim_end_matches('\0');
989 if s.len() >= 12 {
990 let dt = format!(
991 "20{}:{}:{} {}:{}:{}Z",
992 &s[0..2],
993 &s[2..4],
994 &s[4..6],
995 &s[6..8],
996 &s[8..10],
997 &s[10..]
998 );
999 tags.push(mk_gps_dt(&dt));
1000 found = true;
1001 }
1002 }
1003 } else if fourcc == b"ACCL" && type_byte == b's' && struct_size >= 6 {
1004 for i in 0..repeat.min(1) {
1006 let off = data_start + i * struct_size;
1007 if off + 6 > end {
1008 break;
1009 }
1010 let x = get_i16_be(data, off) as f64 / 100.0;
1011 let y = get_i16_be(data, off + 2) as f64 / 100.0;
1012 let z = get_i16_be(data, off + 4) as f64 / 100.0;
1013 tags.push(mk_stream(
1014 "Accelerometer",
1015 "Accelerometer",
1016 Value::String(format!("{:.4} {:.4} {:.4}", x, y, z)),
1017 ));
1018 found = true;
1019 }
1020 } else if fourcc == b"GYRO" && type_byte == b's' && struct_size >= 6 {
1021 for i in 0..repeat.min(1) {
1023 let off = data_start + i * struct_size;
1024 if off + 6 > end {
1025 break;
1026 }
1027 let x = get_i16_be(data, off) as f64 / 100.0;
1028 let y = get_i16_be(data, off + 2) as f64 / 100.0;
1029 let z = get_i16_be(data, off + 4) as f64 / 100.0;
1030 tags.push(mk_stream(
1031 "AngularVelocity",
1032 "Angular Velocity",
1033 Value::String(format!("{:.4} {:.4} {:.4}", x, y, z)),
1034 ));
1035 found = true;
1036 }
1037 }
1038
1039 pos = data_start + padded;
1040 }
1041
1042 found
1043}
1044
1045fn process_mebx(data: &[u8], tags: &mut Vec<Tag>) -> bool {
1048 let mut pos = 0;
1050 let mut found = false;
1051 while pos + 8 < data.len() {
1052 let len = get_u32_be(data, pos) as usize;
1053 if len < 8 || pos + len > data.len() {
1054 break;
1055 }
1056 let key = &data[pos + 4..pos + 8];
1057 let val_data = &data[pos + 8..pos + len];
1058
1059 if let Ok(s) = std::str::from_utf8(val_data) {
1061 let key_str = String::from_utf8_lossy(key).to_string();
1062 let name = key_str.trim().to_string();
1063 if !name.is_empty() {
1064 tags.push(mk_stream(&name, &name, Value::String(s.trim().to_string())));
1065 found = true;
1066 }
1067 }
1068 pos += len;
1069 }
1070 found
1071}
1072
1073fn process_tx3g(data: &[u8], tags: &mut Vec<Tag>) -> bool {
1076 if data.len() < 2 {
1077 return false;
1078 }
1079 let text = String::from_utf8_lossy(&data[2..]); let text = text.trim();
1081 if text.is_empty() {
1082 return false;
1083 }
1084
1085 if text.starts_with("HOME(") {
1087 return process_tx3g_autel(&text, tags);
1088 }
1089
1090 let mut found = false;
1092 for line in text.lines() {
1094 let line = line.trim();
1095 for cap in line.split_whitespace() {
1097 if let Some((k, v)) = cap.split_once(':') {
1098 match k {
1099 "Lat" => {
1100 if let Ok(val) = v.parse::<f64>() {
1101 tags.push(mk_gps_lat(val));
1102 found = true;
1103 }
1104 }
1105 "Lon" => {
1106 if let Ok(val) = v.parse::<f64>() {
1107 tags.push(mk_gps_lon(val));
1108 found = true;
1109 }
1110 }
1111 "Alt" => {
1112 if let Ok(val) = v.trim_end_matches('m').trim().parse::<f64>() {
1113 tags.push(mk_gps_alt(val));
1114 found = true;
1115 }
1116 }
1117 _ => {}
1118 }
1119 }
1120 }
1121 }
1122
1123 if !found {
1124 tags.push(mk_stream("Text", "Text", Value::String(text.to_string())));
1126 let _ = parse_nmea_rmc(text, tags) || parse_nmea_gga(text, tags);
1128 found = true;
1129 }
1130 found
1131}
1132
1133fn process_tx3g_autel(text: &str, tags: &mut Vec<Tag>) -> bool {
1134 let mut found = false;
1135 for line in text.lines() {
1136 let line = line.trim();
1137 if line.starts_with("HOME(") {
1139 if let Some(rest) = line.strip_prefix("HOME(") {
1141 if let Some(paren_end) = rest.find(')') {
1142 let coords = &rest[..paren_end];
1143 let after = rest[paren_end + 1..].trim();
1144 let parts: Vec<&str> = coords.split(',').collect();
1146 if parts.len() == 2 {
1147 for part in &parts {
1148 let part = part.trim();
1149 if let Some((dir, val_s)) = part.split_once(':') {
1150 let dir = dir.trim();
1151 let val_s = val_s.trim();
1152 if let Ok(val) = val_s.parse::<f64>() {
1153 match dir {
1154 "N" | "S" => {
1155 let v = if dir == "S" { -val } else { val };
1156 tags.push(mk_stream(
1157 "GPSHomeLatitude",
1158 "GPS Home Latitude",
1159 Value::String(format!("{:.6}", v)),
1160 ));
1161 found = true;
1162 }
1163 "E" | "W" => {
1164 let v = if dir == "W" { -val } else { val };
1165 tags.push(mk_stream(
1166 "GPSHomeLongitude",
1167 "GPS Home Longitude",
1168 Value::String(format!("{:.6}", v)),
1169 ));
1170 found = true;
1171 }
1172 _ => {}
1173 }
1174 }
1175 }
1176 }
1177 }
1178 if !after.is_empty() {
1180 let dt = after.replace('-', ":");
1181 tags.push(mk_gps_dt(&dt));
1182 found = true;
1183 }
1184 }
1185 }
1186 } else if line.starts_with("GPS(") {
1187 if let Some(rest) = line.strip_prefix("GPS(") {
1189 if let Some(paren_end) = rest.find(')') {
1190 let inner = &rest[..paren_end];
1191 let parts: Vec<&str> = inner.split(',').collect();
1192 for part in &parts {
1193 let part = part.trim();
1194 if let Some((dir, val_s)) = part.split_once(':') {
1195 let dir = dir.trim();
1196 let val_s = val_s.trim();
1197 if let Ok(val) = val_s.parse::<f64>() {
1198 match dir {
1199 "N" | "S" => {
1200 let v = if dir == "S" { -val } else { val };
1201 tags.push(mk_gps_lat(v));
1202 found = true;
1203 }
1204 "E" | "W" => {
1205 let v = if dir == "W" { -val } else { val };
1206 tags.push(mk_gps_lon(v));
1207 found = true;
1208 }
1209 _ => {}
1210 }
1211 }
1212 } else if part.ends_with('m') {
1213 if let Ok(alt) = part.trim_end_matches('m').trim().parse::<f64>() {
1214 tags.push(mk_gps_alt(alt));
1215 found = true;
1216 }
1217 }
1218 }
1219 }
1220 }
1221 }
1222 }
1223 found
1224}
1225
1226fn process_nmea(data: &[u8], tags: &mut Vec<Tag>) -> bool {
1229 let text = String::from_utf8_lossy(data);
1230 parse_nmea_rmc(&text, tags) || parse_nmea_gga(&text, tags)
1231}
1232
1233fn parse_nmea_rmc(text: &str, tags: &mut Vec<Tag>) -> bool {
1234 let rmc_patterns = ["$GPRMC,", "$GNRMC,", "$GBRMC,"];
1237 for pat in &rmc_patterns {
1238 if let Some(start) = text.find(pat) {
1239 let rest = &text[start + pat.len()..];
1240 return parse_rmc_fields(rest, tags);
1241 }
1242 }
1243 false
1244}
1245
1246fn parse_rmc_fields(rest: &str, tags: &mut Vec<Tag>) -> bool {
1247 let fields: Vec<&str> = rest.split(',').collect();
1248 if fields.len() < 12 {
1249 return false;
1250 }
1251
1252 let time_str = fields[0];
1255 let status = fields[1];
1256 if status != "A" && !status.is_empty() {
1257 }
1259 let lat_str = fields[2];
1260 let lat_ref = fields[3];
1261 let lon_str = fields[4];
1262 let lon_ref = fields[5];
1263 let spd_str = fields[6];
1264 let trk_str = fields[7];
1265 let date_str = fields[8];
1266
1267 let lat = match parse_nmea_coord(lat_str) {
1269 Some(v) => v * if lat_ref == "S" { -1.0 } else { 1.0 },
1270 None => return false,
1271 };
1272 let lon = match parse_nmea_coord(lon_str) {
1273 Some(v) => v * if lon_ref == "W" { -1.0 } else { 1.0 },
1274 None => return false,
1275 };
1276
1277 if date_str.len() >= 6 && time_str.len() >= 6 {
1279 let dd = &date_str[0..2];
1280 let mm = &date_str[2..4];
1281 let yy = &date_str[4..6];
1282 let yr: u32 = yy.parse().unwrap_or(0);
1283 let full_yr = if yr >= 70 { 1900 + yr } else { 2000 + yr };
1284 let time_part = if time_str.len() > 6 {
1285 &time_str[..6]
1286 } else {
1287 time_str
1288 };
1289 let dt = format!(
1290 "{:04}:{:02}:{:02} {}:{}:{}Z",
1291 full_yr,
1292 mm,
1293 dd,
1294 &time_part[0..2],
1295 &time_part[2..4],
1296 &time_part[4..6]
1297 );
1298 tags.push(mk_gps_dt(&dt));
1299 }
1300
1301 tags.push(mk_gps_lat(lat));
1302 tags.push(mk_gps_lon(lon));
1303
1304 if let Ok(spd) = spd_str.parse::<f64>() {
1305 tags.push(mk_gps_spd(spd * KNOTS_TO_KPH));
1306 }
1307 if let Ok(trk) = trk_str.parse::<f64>() {
1308 tags.push(mk_gps_trk(trk));
1309 }
1310
1311 true
1312}
1313
1314fn parse_nmea_gga(text: &str, tags: &mut Vec<Tag>) -> bool {
1315 let patterns = ["$GPGGA,", "$GNGGA,"];
1316 for pat in &patterns {
1317 if let Some(start) = text.find(pat) {
1318 let rest = &text[start + pat.len()..];
1319 let fields: Vec<&str> = rest.split(',').collect();
1320 if fields.len() < 10 {
1321 continue;
1322 }
1323
1324 let lat_str = fields[1];
1325 let lat_ref = fields[2];
1326 let lon_str = fields[3];
1327 let lon_ref = fields[4];
1328
1329 let lat = match parse_nmea_coord(lat_str) {
1330 Some(v) => v * if lat_ref == "S" { -1.0 } else { 1.0 },
1331 None => continue,
1332 };
1333 let lon = match parse_nmea_coord(lon_str) {
1334 Some(v) => v * if lon_ref == "W" { -1.0 } else { 1.0 },
1335 None => continue,
1336 };
1337
1338 tags.push(mk_gps_lat(lat));
1339 tags.push(mk_gps_lon(lon));
1340
1341 if fields.len() > 8 {
1343 if let Ok(alt) = fields[8].parse::<f64>() {
1344 tags.push(mk_gps_alt(alt));
1345 }
1346 }
1347 if let Ok(sats) = fields[6].parse::<u32>() {
1349 tags.push(mk_stream(
1350 "GPSSatellites",
1351 "GPS Satellites",
1352 Value::String(sats.to_string()),
1353 ));
1354 }
1355 return true;
1356 }
1357 }
1358 false
1359}
1360
1361fn parse_nmea_coord(s: &str) -> Option<f64> {
1362 if s.is_empty() {
1364 return None;
1365 }
1366 let val: f64 = s.parse().ok()?;
1367 let deg = (val / 100.0).floor();
1368 let min = val - deg * 100.0;
1369 Some(deg + min / 60.0)
1370}
1371
1372fn process_text(data: &[u8], tags: &mut Vec<Tag>) -> bool {
1375 let text = String::from_utf8_lossy(data);
1376 let text = text.trim();
1377 if text.is_empty() {
1378 return false;
1379 }
1380
1381 if parse_nmea_rmc(text, tags) || parse_nmea_gga(text, tags) {
1383 return true;
1384 }
1385
1386 if text.contains("GPS (") || text.contains("GPS(") {
1388 return process_dji_text(text, tags);
1389 }
1390
1391 if data.len() >= 20 && (data.starts_with(b"PNDM") || (data.len() > 4 && &data[4..8.min(data.len())] == b"PNDM"))
1393 {
1394 return process_garmin_pndm(data, tags);
1395 }
1396
1397 false
1398}
1399
1400fn process_dji_text(text: &str, tags: &mut Vec<Tag>) -> bool {
1401 let gps_start = text.find("GPS (").or_else(|| text.find("GPS("));
1403 if let Some(idx) = gps_start {
1404 let rest = &text[idx..];
1405 if let Some(paren_start) = rest.find('(') {
1406 if let Some(paren_end) = rest.find(')') {
1407 let inner = &rest[paren_start + 1..paren_end];
1408 let parts: Vec<&str> = inner.split(',').collect();
1409 if parts.len() >= 2 {
1410 if let (Ok(lon), Ok(lat)) = (
1411 parts[0].trim().parse::<f64>(),
1412 parts[1].trim().parse::<f64>(),
1413 ) {
1414 tags.push(mk_gps_lat(lat));
1415 tags.push(mk_gps_lon(lon));
1416 if parts.len() >= 3 {
1417 if let Ok(alt) = parts[2].trim().parse::<f64>() {
1418 tags.push(mk_gps_alt(alt));
1419 }
1420 }
1421 }
1422 }
1423 }
1424 }
1425 }
1426
1427 if let Some(idx) = text.find("H.S ") {
1429 let rest = &text[idx + 4..];
1430 if let Some(end) = rest.find("m/s") {
1431 if let Ok(spd) = rest[..end].trim().parse::<f64>() {
1432 tags.push(mk_gps_spd(spd * MPS_TO_KPH));
1433 }
1434 }
1435 }
1436
1437 if let Some(idx) = text.find("ISO ") {
1439 let rest = &text[idx + 4..];
1440 let val: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
1441 if !val.is_empty() {
1442 tags.push(mk_stream("ISO", "ISO", Value::String(val)));
1443 }
1444 }
1445
1446 true
1447}
1448
1449fn process_garmin_pndm(data: &[u8], tags: &mut Vec<Tag>) -> bool {
1450 let offset = if data.starts_with(b"PNDM") { 0 } else { 4 };
1451 if data.len() < offset + 20 {
1452 return false;
1453 }
1454 let lat = get_i32_be(data, offset + 12) as f64 * 180.0 / 0x80000000u32 as f64;
1455 let lon = get_i32_be(data, offset + 16) as f64 * 180.0 / 0x80000000u32 as f64;
1456 let spd = get_u16_be(data, offset + 8) as f64 * MPH_TO_KPH;
1457
1458 tags.push(mk_gps_lat(lat));
1459 tags.push(mk_gps_lon(lon));
1460 tags.push(mk_gps_spd(spd));
1461 true
1462}
1463
1464fn process_rvmi(data: &[u8], tags: &mut Vec<Tag>) -> bool {
1467 if data.len() < 20 {
1468 return false;
1469 }
1470 if &data[0..4] == b"gReV" {
1471 let lat = get_i32_le(data, 4) as f64 / 1e6;
1473 let lon = get_i32_le(data, 8) as f64 / 1e6;
1474 let spd = get_i16_le(data, 16) as f64 / 10.0;
1475 let trk = get_u16_le(data, 18) as f64 * 2.0;
1476 tags.push(mk_gps_lat(lat));
1477 tags.push(mk_gps_lon(lon));
1478 tags.push(mk_gps_spd(spd));
1479 tags.push(mk_gps_trk(trk));
1480 return true;
1481 }
1482 if &data[0..4] == b"sReV" {
1483 if data.len() >= 10 {
1485 let x = get_i16_le(data, 4) as f64 / 1000.0;
1486 let y = get_i16_le(data, 6) as f64 / 1000.0;
1487 let z = get_i16_le(data, 8) as f64 / 1000.0;
1488 tags.push(mk_stream(
1489 "GSensor",
1490 "G Sensor",
1491 Value::String(format!("{} {} {}", x, y, z)),
1492 ));
1493 return true;
1494 }
1495 }
1496 false
1497}
1498
1499fn process_kenwood(data: &[u8], tags: &mut Vec<Tag>) -> bool {
1502 let mut found = false;
1504 let mut pos = 0;
1505 while pos + 2 < data.len() {
1506 if let Some(idx) = data[pos..].windows(2).position(|w| w == b"\xfe\xfe") {
1508 let start = pos + idx + 2;
1509 if start + 40 > data.len() {
1510 break;
1511 }
1512 let dat = &data[start..];
1513 if let Some(dt) = try_ascii_digits(dat, 14) {
1515 if dt.len() == 14 {
1516 let time = format!(
1517 "{}:{}:{} {}:{}:{}",
1518 &dt[0..4],
1519 &dt[4..6],
1520 &dt[6..8],
1521 &dt[8..10],
1522 &dt[10..12],
1523 &dt[12..14]
1524 );
1525
1526 let after = &dat[15..]; if after.len() < 20 {
1529 pos = start + 14;
1530 continue;
1531 }
1532 let after2 = if after.len() > 15 { &after[15..] } else { after };
1534
1535 if !after2.is_empty() && is_ns(after2[0]) {
1537 let lat_ref = after2[0];
1538 let mut ew_pos = 1;
1540 while ew_pos < after2.len() && !is_ew(after2[ew_pos]) {
1541 ew_pos += 1;
1542 }
1543 if ew_pos < after2.len() {
1544 let lon_ref = after2[ew_pos];
1545 let lat_digits = &after2[1..ew_pos];
1546 let lon_start = ew_pos + 1;
1548 let mut lon_end = lon_start;
1549 while lon_end < after2.len() && after2[lon_end].is_ascii_digit() {
1550 lon_end += 1;
1551 }
1552 let lon_digits = &after2[lon_start..lon_end];
1553
1554 if let (Ok(lat_s), Ok(lon_s)) = (
1555 std::str::from_utf8(lat_digits),
1556 std::str::from_utf8(lon_digits),
1557 ) {
1558 if let (Ok(lat_raw), Ok(lon_raw)) =
1559 (lat_s.parse::<f64>(), lon_s.parse::<f64>())
1560 {
1561 let lat = lat_raw / 1e4;
1562 let lon = lon_raw / 1e4;
1563 let (lat_dd, lon_dd) = convert_lat_lon(lat, lon);
1564
1565 tags.push(mk_gps_dt(&time));
1566 tags.push(mk_gps_lat(
1567 lat_dd * if lat_ref == b'S' { -1.0 } else { 1.0 },
1568 ));
1569 tags.push(mk_gps_lon(
1570 lon_dd * if lon_ref == b'W' { -1.0 } else { 1.0 },
1571 ));
1572 found = true;
1573
1574 if lon_end + 9 <= after2.len() {
1576 if let Ok(rest) =
1577 std::str::from_utf8(&after2[lon_end..lon_end + 9])
1578 {
1579 if rest.starts_with('+') || rest.starts_with('-') {
1581 if let Ok(alt) = rest[0..5].parse::<f64>() {
1582 tags.push(mk_gps_alt(alt));
1583 }
1584 if let Ok(spd) = rest[5..].parse::<f64>() {
1585 tags.push(mk_gps_spd(spd));
1586 }
1587 }
1588 }
1589 }
1590 }
1591 }
1592 }
1593 }
1594 }
1595 }
1596 pos = start + 40;
1597 } else {
1598 break;
1599 }
1600 }
1601 found
1602}
1603
1604fn scan_mdat_for_freegps(data: &[u8], tags: &mut Vec<Tag>, doc_count: &mut u32) {
1607 let pattern = b"freeGPS ";
1609 let mut pos = 0;
1610 let limit = data.len().min(20_000_000); while pos + 12 < limit {
1613 if let Some(idx) = data[pos..limit].windows(8).position(|w| w == pattern) {
1614 let abs_pos = pos + idx;
1615 if abs_pos >= 4 {
1617 let atom_start = abs_pos - 4;
1618 let atom_size =
1619 u32::from_be_bytes([data[atom_start], data[atom_start + 1], data[atom_start + 2], data[atom_start + 3]])
1620 as usize;
1621 let atom_size = if atom_size < 12 { 12 } else { atom_size };
1622 let end = (atom_start + atom_size).min(data.len());
1623 let block = &data[atom_start..end];
1624
1625 let mut sample_tags = Vec::new();
1626 if process_freegps(block, &mut sample_tags) && !sample_tags.is_empty() {
1627 *doc_count += 1;
1628 for t in &mut sample_tags {
1629 t.description = format!("{} (Doc{})", t.description, doc_count);
1630 }
1631 tags.extend(sample_tags);
1632 }
1633 pos = end;
1634 } else {
1635 pos = abs_pos + 8;
1636 }
1637 } else {
1638 break;
1639 }
1640 }
1641}
1642
1643fn is_ns(b: u8) -> bool {
1646 b == b'N' || b == b'S'
1647}
1648fn is_ew(b: u8) -> bool {
1649 b == b'E' || b == b'W'
1650}
1651
1652fn convert_lat_lon(lat: f64, lon: f64) -> (f64, f64) {
1654 let lat_deg = (lat / 100.0).floor();
1655 let lat_dd = lat_deg + (lat - lat_deg * 100.0) / 60.0;
1656 let lon_deg = (lon / 100.0).floor();
1657 let lon_dd = lon_deg + (lon - lon_deg * 100.0) / 60.0;
1658 (lat_dd, lon_dd)
1659}
1660
1661fn signed_u32(v: u32) -> i32 {
1662 if v < 0x80000000 {
1663 v as i32
1664 } else {
1665 v as i32 }
1667}
1668
1669fn get_u16_be(data: &[u8], off: usize) -> u16 {
1670 u16::from_be_bytes([data[off], data[off + 1]])
1671}
1672
1673fn get_u16_le(data: &[u8], off: usize) -> u16 {
1674 u16::from_le_bytes([data[off], data[off + 1]])
1675}
1676
1677fn get_u32_be(data: &[u8], off: usize) -> u32 {
1678 u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
1679}
1680
1681fn get_u32_le(data: &[u8], off: usize) -> u32 {
1682 u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
1683}
1684
1685fn get_i32_be(data: &[u8], off: usize) -> i32 {
1686 i32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
1687}
1688
1689fn get_i32_le(data: &[u8], off: usize) -> i32 {
1690 i32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
1691}
1692
1693fn get_i16_be(data: &[u8], off: usize) -> i16 {
1694 i16::from_be_bytes([data[off], data[off + 1]])
1695}
1696
1697fn get_i16_le(data: &[u8], off: usize) -> i16 {
1698 i16::from_le_bytes([data[off], data[off + 1]])
1699}
1700
1701fn get_f32_le(data: &[u8], off: usize) -> f32 {
1702 f32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
1703}
1704
1705fn get_f64_le(data: &[u8], off: usize) -> f64 {
1706 f64::from_le_bytes([
1707 data[off],
1708 data[off + 1],
1709 data[off + 2],
1710 data[off + 3],
1711 data[off + 4],
1712 data[off + 5],
1713 data[off + 6],
1714 data[off + 7],
1715 ])
1716}
1717
1718fn try_ascii_digits(data: &[u8], max_len: usize) -> Option<String> {
1719 let end = data.len().min(max_len);
1720 let slice = &data[..end];
1721 if slice.iter().all(|b| b.is_ascii_digit()) {
1722 Some(String::from_utf8_lossy(slice).to_string())
1723 } else {
1724 None
1725 }
1726}
1727
1728fn mk_stream(name: &str, description: &str, value: Value) -> Tag {
1731 let print_value = value.to_display_string();
1732 Tag {
1733 id: TagId::Text(name.to_string()),
1734 name: name.to_string(),
1735 description: description.to_string(),
1736 group: TagGroup {
1737 family0: "QuickTime".into(),
1738 family1: "QuickTime".into(),
1739 family2: "Location".into(),
1740 },
1741 raw_value: value,
1742 print_value,
1743 priority: 0,
1744 }
1745}
1746
1747fn mk_gps_dt(dt: &str) -> Tag {
1748 Tag {
1749 id: TagId::Text("GPSDateTime".into()),
1750 name: "GPSDateTime".into(),
1751 description: "GPS Date/Time".into(),
1752 group: TagGroup {
1753 family0: "QuickTime".into(),
1754 family1: "QuickTime".into(),
1755 family2: "Time".into(),
1756 },
1757 raw_value: Value::String(dt.to_string()),
1758 print_value: dt.to_string(),
1759 priority: 0,
1760 }
1761}
1762
1763fn mk_gps_lat(val: f64) -> Tag {
1764 let abs_val = val.abs();
1765 let d = abs_val.floor() as u32;
1766 let m_total = (abs_val - d as f64) * 60.0;
1767 let m = m_total.floor() as u32;
1768 let s = (m_total - m as f64) * 60.0;
1769 let ref_c = if val >= 0.0 { "N" } else { "S" };
1770 let print = format!("{} deg {}' {:.2}\" {}", d, m, s, ref_c);
1771 Tag {
1772 id: TagId::Text("GPSLatitude".into()),
1773 name: "GPSLatitude".into(),
1774 description: "GPS Latitude".into(),
1775 group: TagGroup {
1776 family0: "QuickTime".into(),
1777 family1: "QuickTime".into(),
1778 family2: "Location".into(),
1779 },
1780 raw_value: Value::F64(val),
1781 print_value: print,
1782 priority: 0,
1783 }
1784}
1785
1786fn mk_gps_lon(val: f64) -> Tag {
1787 let abs_val = val.abs();
1788 let d = abs_val.floor() as u32;
1789 let m_total = (abs_val - d as f64) * 60.0;
1790 let m = m_total.floor() as u32;
1791 let s = (m_total - m as f64) * 60.0;
1792 let ref_c = if val >= 0.0 { "E" } else { "W" };
1793 let print = format!("{} deg {}' {:.2}\" {}", d, m, s, ref_c);
1794 Tag {
1795 id: TagId::Text("GPSLongitude".into()),
1796 name: "GPSLongitude".into(),
1797 description: "GPS Longitude".into(),
1798 group: TagGroup {
1799 family0: "QuickTime".into(),
1800 family1: "QuickTime".into(),
1801 family2: "Location".into(),
1802 },
1803 raw_value: Value::F64(val),
1804 print_value: print,
1805 priority: 0,
1806 }
1807}
1808
1809fn mk_gps_alt(val: f64) -> Tag {
1810 Tag {
1811 id: TagId::Text("GPSAltitude".into()),
1812 name: "GPSAltitude".into(),
1813 description: "GPS Altitude".into(),
1814 group: TagGroup {
1815 family0: "QuickTime".into(),
1816 family1: "QuickTime".into(),
1817 family2: "Location".into(),
1818 },
1819 raw_value: Value::F64(val),
1820 print_value: format!("{:.4} m", val),
1821 priority: 0,
1822 }
1823}
1824
1825fn mk_gps_spd(val: f64) -> Tag {
1826 Tag {
1827 id: TagId::Text("GPSSpeed".into()),
1828 name: "GPSSpeed".into(),
1829 description: "GPS Speed".into(),
1830 group: TagGroup {
1831 family0: "QuickTime".into(),
1832 family1: "QuickTime".into(),
1833 family2: "Location".into(),
1834 },
1835 raw_value: Value::F64(val),
1836 print_value: format!("{:.4}", val),
1837 priority: 0,
1838 }
1839}
1840
1841fn mk_gps_trk(val: f64) -> Tag {
1842 Tag {
1843 id: TagId::Text("GPSTrack".into()),
1844 name: "GPSTrack".into(),
1845 description: "GPS Track".into(),
1846 group: TagGroup {
1847 family0: "QuickTime".into(),
1848 family1: "QuickTime".into(),
1849 family2: "Location".into(),
1850 },
1851 raw_value: Value::F64(val),
1852 print_value: format!("{:.4}", val),
1853 priority: 0,
1854 }
1855}