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