1use crate::artifact_bytes::ArtifactBytes;
15use std::collections::{HashMap, HashSet};
16use std::fs;
17use std::path::{Path, PathBuf};
18
19use crate::geoid::{egm96_undulation, GeoidError, GeoidGrid};
20use crate::terrain::{
21 self, terrain_grid_candidates, validate_lookup_coordinates, DtedInterpolation,
22 DtedLookupOptions, DtedTile,
23};
24use crate::{Error, Result};
25
26const STORE_MAGIC: &[u8; 8] = b"TMMAP001";
27const STORE_VERSION: u16 = 1;
28const STORE_ALIGNMENT: usize = 4096;
29const STORE_HEADER_LEN: usize = 64;
30const STORE_INDEX_RECORD_LEN: usize = 80;
31const HEADER_VERSION_OFFSET: usize = 8;
32const HEADER_DATUM_OFFSET: usize = 10;
33const HEADER_TILE_COUNT_OFFSET: usize = 12;
34const HEADER_INDEX_OFFSET_OFFSET: usize = 16;
35const HEADER_DATA_OFFSET_OFFSET: usize = 24;
36const HEADER_TOTAL_LEN_OFFSET: usize = 32;
37const INDEX_LAT_OFFSET: usize = 0;
38const INDEX_LON_OFFSET: usize = 4;
39const INDEX_LON_COUNT_OFFSET: usize = 8;
40const INDEX_LAT_COUNT_OFFSET: usize = 12;
41const INDEX_DATA_OFFSET_OFFSET: usize = 16;
42const INDEX_DATA_LEN_OFFSET: usize = 24;
43const INDEX_CHECKSUM_OFFSET: usize = 32;
44const INDEX_MIN_LAT_OFFSET: usize = 40;
45const INDEX_MIN_LON_OFFSET: usize = 48;
46const INDEX_MAX_LAT_OFFSET: usize = 56;
47const INDEX_MAX_LON_OFFSET: usize = 64;
48const INDEX_DATUM_OFFSET: usize = 72;
49const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
50const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
51const EGM96_DAC_REMEDIATION: &str =
52 "obtain the public NGA EGM96 15-arcminute WW15MGH.DAC file and load it with Egm96FifteenMinuteGeoid::from_ww15mgh_dac_path or Egm96FifteenMinuteGeoid::from_ww15mgh_dac_bytes";
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum VerticalDatum {
57 Egm96MslOrthometric,
59}
60
61impl VerticalDatum {
62 fn tag(self) -> u8 {
63 match self {
64 Self::Egm96MslOrthometric => 1,
65 }
66 }
67
68 fn from_tag(tag: u8) -> core::result::Result<Self, TerrainStoreError> {
69 match tag {
70 1 => Ok(Self::Egm96MslOrthometric),
71 other => Err(TerrainStoreError::UnsupportedDatum { tag: other }),
72 }
73 }
74}
75
76#[derive(Clone, Copy, Debug, PartialEq)]
82pub struct OrthometricHeightM {
83 pub value_m: f64,
85}
86
87impl OrthometricHeightM {
88 #[must_use]
90 pub const fn new(value_m: f64) -> Self {
91 Self { value_m }
92 }
93
94 #[must_use]
96 pub const fn metres(self) -> f64 {
97 self.value_m
98 }
99
100 pub fn to_ellipsoidal_height_deg(
111 self,
112 latitude_deg: f64,
113 longitude_deg: f64,
114 geoid: TerrainGeoidModel<'_>,
115 ) -> core::result::Result<EllipsoidalHeightM, TerrainDatumError> {
116 Ok(EllipsoidalHeightM::new(
117 self.value_m + geoid.undulation_deg(latitude_deg, longitude_deg),
118 ))
119 }
120
121 pub fn to_ellipsoidal_height_rad(
129 self,
130 latitude_rad: f64,
131 longitude_rad: f64,
132 geoid: TerrainGeoidModel<'_>,
133 ) -> core::result::Result<EllipsoidalHeightM, TerrainDatumError> {
134 Ok(EllipsoidalHeightM::new(
135 self.value_m + geoid.undulation_rad(latitude_rad, longitude_rad),
136 ))
137 }
138}
139
140#[derive(Clone, Copy, Debug, PartialEq)]
142pub struct EllipsoidalHeightM {
143 pub value_m: f64,
145}
146
147impl EllipsoidalHeightM {
148 #[must_use]
150 pub const fn new(value_m: f64) -> Self {
151 Self { value_m }
152 }
153
154 #[must_use]
156 pub const fn metres(self) -> f64 {
157 self.value_m
158 }
159}
160
161#[derive(Clone, Debug, PartialEq)]
166pub struct Egm96FifteenMinuteGeoid {
167 grid: GeoidGrid,
168}
169
170impl Egm96FifteenMinuteGeoid {
171 pub fn from_ww15mgh_dac_bytes(bytes: &[u8]) -> core::result::Result<Self, TerrainDatumError> {
173 let grid = GeoidGrid::from_egm96_dac(bytes).map_err(TerrainDatumError::Geoid)?;
174 Ok(Self { grid })
175 }
176
177 pub fn from_ww15mgh_dac_path(
185 path: impl AsRef<Path>,
186 ) -> core::result::Result<Self, TerrainDatumError> {
187 let path = path.as_ref();
188 let bytes = match fs::read(path) {
189 Ok(bytes) => bytes,
190 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
191 return Err(TerrainDatumError::MissingEgm96Dac {
192 path: path.to_path_buf(),
193 remediation: EGM96_DAC_REMEDIATION,
194 });
195 }
196 Err(err) => {
197 return Err(TerrainDatumError::Io {
198 path: path.to_path_buf(),
199 message: err.to_string(),
200 });
201 }
202 };
203 Self::from_ww15mgh_dac_bytes(&bytes)
204 }
205
206 #[must_use]
208 pub const fn grid(&self) -> &GeoidGrid {
209 &self.grid
210 }
211}
212
213#[derive(Clone, Copy, Debug)]
216pub enum TerrainGeoidModel<'a> {
217 Egm96OneDegree,
222 Egm96FifteenMinute(&'a Egm96FifteenMinuteGeoid),
228}
229
230impl TerrainGeoidModel<'_> {
231 fn undulation_deg(self, latitude_deg: f64, longitude_deg: f64) -> f64 {
232 match self {
233 Self::Egm96OneDegree => {
234 egm96_undulation(latitude_deg.to_radians(), longitude_deg.to_radians())
235 }
236 Self::Egm96FifteenMinute(grid) => grid.grid.undulation_deg(latitude_deg, longitude_deg),
237 }
238 }
239
240 fn undulation_rad(self, latitude_rad: f64, longitude_rad: f64) -> f64 {
241 match self {
242 Self::Egm96OneDegree => egm96_undulation(latitude_rad, longitude_rad),
243 Self::Egm96FifteenMinute(grid) => grid.grid.undulation_rad(latitude_rad, longitude_rad),
244 }
245 }
246}
247
248#[derive(Debug, Clone, PartialEq)]
250pub enum TerrainDatumError {
251 Terrain(Error),
253 Geoid(GeoidError),
255 Io {
257 path: PathBuf,
259 message: String,
261 },
262 MissingEgm96Dac {
264 path: PathBuf,
266 remediation: &'static str,
268 },
269}
270
271impl core::fmt::Display for TerrainDatumError {
272 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
273 match self {
274 Self::Terrain(err) => write!(f, "terrain lookup failed: {err}"),
275 Self::Geoid(err) => write!(f, "geoid grid failed: {err}"),
276 Self::Io { path, message } => {
277 write!(f, "{} could not be read: {message}", path.display())
278 }
279 Self::MissingEgm96Dac { path, remediation } => {
280 write!(f, "{} is missing; {remediation}", path.display())
281 }
282 }
283 }
284}
285
286impl std::error::Error for TerrainDatumError {}
287
288impl From<Error> for TerrainDatumError {
289 fn from(value: Error) -> Self {
290 Self::Terrain(value)
291 }
292}
293
294#[derive(Clone, Copy, Debug, PartialEq)]
296pub struct TerrainStoreTileIndex {
297 pub lat_index: i32,
299 pub lon_index: i32,
302 pub min_longitude_deg: f64,
304 pub min_latitude_deg: f64,
306 pub max_longitude_deg: f64,
308 pub max_latitude_deg: f64,
310 pub lon_count: u32,
312 pub lat_count: u32,
314 pub data_offset: u64,
316 pub data_len: u64,
318 pub checksum64: u64,
320 pub vertical_datum: VerticalDatum,
322}
323
324#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
326pub struct TerrainTileId {
327 pub lat_index: i32,
329 pub lon_index: i32,
332}
333
334impl TerrainTileId {
335 #[must_use]
337 pub const fn new(lat_index: i32, lon_index: i32) -> Self {
338 Self {
339 lat_index,
340 lon_index,
341 }
342 }
343}
344
345#[derive(Clone, Debug, PartialEq, Eq)]
347pub struct DtedTileListEntry {
348 pub tile_id: TerrainTileId,
350 pub path: PathBuf,
352}
353
354impl DtedTileListEntry {
355 #[must_use]
357 pub fn new(tile_id: TerrainTileId, path: impl Into<PathBuf>) -> Self {
358 Self {
359 tile_id,
360 path: path.into(),
361 }
362 }
363
364 #[must_use]
366 pub fn from_indices(lat_index: i32, lon_index: i32, path: impl Into<PathBuf>) -> Self {
367 Self::new(TerrainTileId::new(lat_index, lon_index), path)
368 }
369}
370
371#[derive(Debug, Clone, PartialEq, Eq)]
373pub enum TerrainStoreError {
374 Io {
376 path: PathBuf,
378 message: String,
380 },
381 Parse {
383 reason: String,
385 },
386 UnsupportedVersion {
388 version: u16,
390 },
391 UnsupportedDatum {
393 tag: u8,
395 },
396 DuplicateTile {
398 lat_index: i32,
400 lon_index: i32,
402 },
403 TileIdMismatch {
405 path: PathBuf,
407 expected: TerrainTileId,
409 found: TerrainTileId,
411 },
412 Checksum {
414 lat_index: i32,
416 lon_index: i32,
418 expected: u64,
420 found: u64,
422 },
423}
424
425impl core::fmt::Display for TerrainStoreError {
426 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
427 match self {
428 Self::Io { path, message } => write!(f, "{} failed: {message}", path.display()),
429 Self::Parse { reason } => write!(f, "terrain store parse error: {reason}"),
430 Self::UnsupportedVersion { version } => {
431 write!(f, "terrain store version {version} is not supported")
432 }
433 Self::UnsupportedDatum { tag } => {
434 write!(f, "terrain store vertical datum tag {tag} is not supported")
435 }
436 Self::DuplicateTile {
437 lat_index,
438 lon_index,
439 } => write!(f, "duplicate terrain tile ({lat_index},{lon_index})"),
440 Self::TileIdMismatch {
441 path,
442 expected,
443 found,
444 } => write!(
445 f,
446 "{} tile id expected ({},{}) but DTED origin is ({},{})",
447 path.display(),
448 expected.lat_index,
449 expected.lon_index,
450 found.lat_index,
451 found.lon_index
452 ),
453 Self::Checksum {
454 lat_index,
455 lon_index,
456 expected,
457 found,
458 } => write!(
459 f,
460 "terrain tile ({lat_index},{lon_index}) checksum expected {expected:#x} but found {found:#x}"
461 ),
462 }
463 }
464}
465
466impl std::error::Error for TerrainStoreError {}
467
468#[derive(Clone, Debug)]
469struct MmapTile {
470 index: TerrainStoreTileIndex,
471}
472
473impl MmapTile {
474 fn contains(&self, longitude_deg: f64, latitude_deg: f64) -> bool {
475 latitude_deg >= self.index.min_latitude_deg
476 && latitude_deg <= self.index.max_latitude_deg
477 && longitude_deg >= self.index.min_longitude_deg
478 && longitude_deg <= self.index.max_longitude_deg
479 }
480
481 fn get_elevation(&self, bytes: &[u8], longitude_deg: f64, latitude_deg: f64) -> Result<i16> {
482 if !self.contains(longitude_deg, latitude_deg) {
483 return Err(Error::Parse(format!(
484 "point ({longitude_deg},{latitude_deg}) is outside terrain store tile ({},{})",
485 self.index.min_longitude_deg, self.index.min_latitude_deg
486 )));
487 }
488
489 let lat_count = self.index.lat_count as usize;
490 let lon_count = self.index.lon_count as usize;
491 let latitude_index = terrain::py_round_to_usize(
492 (latitude_deg - self.index.min_latitude_deg) * (lat_count - 1) as f64,
493 )
494 .map_err(Error::Parse)?;
495 let longitude_index = terrain::py_round_to_usize(
496 (longitude_deg - self.index.min_longitude_deg) * (lon_count - 1) as f64,
497 )
498 .map_err(Error::Parse)?;
499 if latitude_index >= lat_count || longitude_index >= lon_count {
500 return Err(Error::Parse(format!(
501 "posting index out of bounds lon={longitude_index} lat={latitude_index}"
502 )));
503 }
504
505 let sample_start =
506 self.index.data_offset as usize + 2 * (longitude_index * lat_count + latitude_index);
507 Ok(i16::from_le_bytes([
508 bytes[sample_start],
509 bytes[sample_start + 1],
510 ]))
511 }
512}
513
514#[derive(Clone, Debug)]
521pub struct MmapTerrain<'a> {
522 bytes: ArtifactBytes<'a>,
523 tiles: Vec<MmapTile>,
524 by_grid: HashMap<(i32, i32), usize>,
525 tile_index: Vec<TerrainStoreTileIndex>,
526 tile_ids: Vec<TerrainTileId>,
527 vertical_datum: VerticalDatum,
528}
529
530impl MmapTerrain<'static> {
531 pub fn from_vec(bytes: Vec<u8>) -> core::result::Result<Self, TerrainStoreError> {
533 Self::from_backing(ArtifactBytes::Owned(bytes))
534 }
535
536 pub fn from_path(path: impl AsRef<Path>) -> core::result::Result<Self, TerrainStoreError> {
548 let path = path.as_ref();
549
550 #[cfg(feature = "mmap")]
551 {
552 let bytes = crate::artifact_bytes::map_file_read_only(path).map_err(|err| {
553 TerrainStoreError::Io {
554 path: path.to_path_buf(),
555 message: err.to_string(),
556 }
557 })?;
558 Self::from_backing(bytes)
559 }
560
561 #[cfg(not(feature = "mmap"))]
562 {
563 let bytes = fs::read(path).map_err(|err| TerrainStoreError::Io {
564 path: path.to_path_buf(),
565 message: err.to_string(),
566 })?;
567 Self::from_vec(bytes)
568 }
569 }
570}
571
572impl<'a> MmapTerrain<'a> {
573 pub fn from_bytes(bytes: &'a [u8]) -> core::result::Result<Self, TerrainStoreError> {
578 Self::from_backing(ArtifactBytes::Borrowed(bytes))
579 }
580
581 fn from_backing(bytes: ArtifactBytes<'a>) -> core::result::Result<Self, TerrainStoreError> {
582 let parsed = parse_store(bytes.as_slice())?;
583 Ok(Self {
584 bytes,
585 tiles: parsed.tiles,
586 by_grid: parsed.by_grid,
587 tile_index: parsed.tile_index,
588 tile_ids: parsed.tile_ids,
589 vertical_datum: parsed.vertical_datum,
590 })
591 }
592
593 #[must_use]
595 pub fn as_bytes(&self) -> &[u8] {
596 self.bytes.as_slice()
597 }
598
599 #[must_use]
602 pub fn is_memory_mapped(&self) -> bool {
603 self.bytes.is_memory_mapped()
604 }
605
606 #[must_use]
608 pub const fn vertical_datum(&self) -> VerticalDatum {
609 self.vertical_datum
610 }
611
612 #[must_use]
614 pub fn tile_index(&self) -> &[TerrainStoreTileIndex] {
615 &self.tile_index
616 }
617
618 #[must_use]
620 pub fn tile_count(&self) -> usize {
621 self.tile_ids.len()
622 }
623
624 #[must_use]
626 pub fn tile_ids(&self) -> &[TerrainTileId] {
627 &self.tile_ids
628 }
629
630 #[must_use]
632 pub fn checksum64(&self) -> u64 {
633 terrain_store_checksum64(self.bytes.as_ref())
634 }
635
636 #[must_use]
641 pub fn to_bytes(&self) -> Vec<u8> {
642 let pending = self
643 .tiles
644 .iter()
645 .map(|tile| PendingTile {
646 lat_index: tile.index.lat_index,
647 lon_index: tile.index.lon_index,
648 min_latitude_deg: tile.index.min_latitude_deg,
649 min_longitude_deg: tile.index.min_longitude_deg,
650 max_latitude_deg: tile.index.max_latitude_deg,
651 max_longitude_deg: tile.index.max_longitude_deg,
652 lon_count: tile.index.lon_count,
653 lat_count: tile.index.lat_count,
654 data: self.tile_payload(tile).to_vec(),
655 vertical_datum: tile.index.vertical_datum,
656 })
657 .collect();
658 build_store(pending).expect("parsed terrain store can be reserialized")
659 }
660
661 pub fn height_m(&mut self, longitude_deg: f64, latitude_deg: f64) -> Result<f64> {
664 self.height_m_with_options(longitude_deg, latitude_deg, DtedLookupOptions::default())
665 }
666
667 pub fn height_m_with_options(
670 &mut self,
671 longitude_deg: f64,
672 latitude_deg: f64,
673 options: DtedLookupOptions,
674 ) -> Result<f64> {
675 self.orthometric_height_m_with_options(longitude_deg, latitude_deg, options)
676 .map(OrthometricHeightM::metres)
677 }
678
679 pub fn orthometric_height_m(
682 &self,
683 longitude_deg: f64,
684 latitude_deg: f64,
685 ) -> Result<OrthometricHeightM> {
686 self.orthometric_height_m_with_options(
687 longitude_deg,
688 latitude_deg,
689 DtedLookupOptions::default(),
690 )
691 }
692
693 pub fn orthometric_height_m_with_options(
697 &self,
698 longitude_deg: f64,
699 latitude_deg: f64,
700 options: DtedLookupOptions,
701 ) -> Result<OrthometricHeightM> {
702 validate_lookup_coordinates(longitude_deg, latitude_deg)?;
703 let Some(tile_idx) = self.resolve_grid(longitude_deg, latitude_deg) else {
704 return Err(missing_terrain_tile(longitude_deg, latitude_deg));
705 };
706 height_from_tile(
707 self.bytes.as_ref(),
708 &self.tiles[tile_idx],
709 longitude_deg,
710 latitude_deg,
711 options,
712 )
713 .map(OrthometricHeightM::new)
714 }
715
716 pub fn height_batch(
723 &mut self,
724 points: &[(f64, f64)],
725 options: DtedLookupOptions,
726 ) -> Vec<Result<f64>> {
727 self.orthometric_height_batch(points, options)
728 .into_iter()
729 .map(|result| result.map(OrthometricHeightM::metres))
730 .collect()
731 }
732
733 pub fn orthometric_height_batch(
739 &self,
740 points: &[(f64, f64)],
741 options: DtedLookupOptions,
742 ) -> Vec<Result<OrthometricHeightM>> {
743 let mut out = Vec::with_capacity(points.len());
744 let mut current = None;
745
746 for &(longitude_deg, latitude_deg) in points {
747 if let Err(err) = validate_lookup_coordinates(longitude_deg, latitude_deg) {
748 out.push(Err(err));
749 continue;
750 }
751
752 let primary_grid = terrain::terrain_grid(longitude_deg, latitude_deg);
753 if current == Some(primary_grid) {
754 if let Some(&tile_idx) = self.by_grid.get(&primary_grid) {
755 let tile = &self.tiles[tile_idx];
756 if tile.contains(longitude_deg, latitude_deg) {
757 out.push(
758 height_from_tile(
759 self.bytes.as_ref(),
760 tile,
761 longitude_deg,
762 latitude_deg,
763 options,
764 )
765 .map(OrthometricHeightM::new),
766 );
767 continue;
768 }
769 }
770 }
771
772 match self.resolve_grid(longitude_deg, latitude_deg) {
773 Some(tile_idx) => {
774 let tile = &self.tiles[tile_idx];
775 current = Some((tile.index.lat_index, tile.index.lon_index));
776 out.push(
777 height_from_tile(
778 self.bytes.as_ref(),
779 tile,
780 longitude_deg,
781 latitude_deg,
782 options,
783 )
784 .map(OrthometricHeightM::new),
785 );
786 }
787 None => {
788 current = None;
789 out.push(Err(missing_terrain_tile(longitude_deg, latitude_deg)));
790 }
791 }
792 }
793
794 out
795 }
796
797 pub fn ellipsoidal_height_m(
805 &self,
806 longitude_deg: f64,
807 latitude_deg: f64,
808 ) -> core::result::Result<EllipsoidalHeightM, TerrainDatumError> {
809 self.ellipsoidal_height_m_with_options(
810 longitude_deg,
811 latitude_deg,
812 DtedLookupOptions::default(),
813 )
814 }
815
816 pub fn ellipsoidal_height_m_with_options(
822 &self,
823 longitude_deg: f64,
824 latitude_deg: f64,
825 options: DtedLookupOptions,
826 ) -> core::result::Result<EllipsoidalHeightM, TerrainDatumError> {
827 self.ellipsoidal_height_m_with_model(
828 longitude_deg,
829 latitude_deg,
830 options,
831 TerrainGeoidModel::Egm96OneDegree,
832 )
833 }
834
835 pub fn ellipsoidal_height_m_with_model(
844 &self,
845 longitude_deg: f64,
846 latitude_deg: f64,
847 options: DtedLookupOptions,
848 geoid: TerrainGeoidModel<'_>,
849 ) -> core::result::Result<EllipsoidalHeightM, TerrainDatumError> {
850 let orthometric = self
851 .orthometric_height_m_with_options(longitude_deg, latitude_deg, options)
852 .map_err(TerrainDatumError::Terrain)?;
853 orthometric.to_ellipsoidal_height_deg(latitude_deg, longitude_deg, geoid)
854 }
855
856 fn resolve_grid(&self, longitude_deg: f64, latitude_deg: f64) -> Option<usize> {
857 for grid_idx in terrain_grid_candidates(longitude_deg, latitude_deg) {
858 if let Some(&tile_idx) = self.by_grid.get(&grid_idx) {
859 if self.tiles[tile_idx].contains(longitude_deg, latitude_deg) {
860 return Some(tile_idx);
861 }
862 }
863 }
864 None
865 }
866
867 fn tile_payload(&self, tile: &MmapTile) -> &[u8] {
868 let start = tile.index.data_offset as usize;
869 let end = start + tile.index.data_len as usize;
870 &self.bytes.as_slice()[start..end]
871 }
872}
873
874pub fn dted_tree_to_mmap_store(
884 root: impl AsRef<Path>,
885) -> core::result::Result<Vec<u8>, TerrainStoreError> {
886 let root = root.as_ref();
887 let mut paths = Vec::new();
888 collect_dted_tile_paths(root, &mut paths)?;
889 dted_paths_to_mmap_store(paths)
890}
891
892pub fn dted_tile_list_to_mmap_store(
900 entries: &[DtedTileListEntry],
901) -> core::result::Result<Vec<u8>, TerrainStoreError> {
902 let mut pending = Vec::with_capacity(entries.len());
903 for entry in entries {
904 pending.push(pending_tile_from_dted_path(
905 &entry.path,
906 Some(entry.tile_id),
907 )?);
908 }
909 build_store(pending)
910}
911
912pub fn write_dted_tree_to_mmap_store(
919 root: impl AsRef<Path>,
920 output_path: impl AsRef<Path>,
921) -> core::result::Result<(), TerrainStoreError> {
922 let bytes = dted_tree_to_mmap_store(root)?;
923 let output_path = output_path.as_ref();
924 fs::write(output_path, &bytes).map_err(|err| TerrainStoreError::Io {
925 path: output_path.to_path_buf(),
926 message: err.to_string(),
927 })
928}
929
930pub fn write_dted_tile_list_to_mmap_store(
933 entries: &[DtedTileListEntry],
934 output_path: impl AsRef<Path>,
935) -> core::result::Result<(), TerrainStoreError> {
936 let bytes = dted_tile_list_to_mmap_store(entries)?;
937 let output_path = output_path.as_ref();
938 fs::write(output_path, &bytes).map_err(|err| TerrainStoreError::Io {
939 path: output_path.to_path_buf(),
940 message: err.to_string(),
941 })
942}
943
944#[must_use]
949pub fn terrain_store_checksum64(bytes: &[u8]) -> u64 {
950 fnv1a64(bytes)
951}
952
953#[derive(Debug)]
954struct PendingTile {
955 lat_index: i32,
956 lon_index: i32,
957 min_latitude_deg: f64,
958 min_longitude_deg: f64,
959 max_latitude_deg: f64,
960 max_longitude_deg: f64,
961 lon_count: u32,
962 lat_count: u32,
963 data: Vec<u8>,
964 vertical_datum: VerticalDatum,
965}
966
967#[derive(Debug)]
968struct ParsedStore {
969 vertical_datum: VerticalDatum,
970 tiles: Vec<MmapTile>,
971 by_grid: HashMap<(i32, i32), usize>,
972 tile_index: Vec<TerrainStoreTileIndex>,
973 tile_ids: Vec<TerrainTileId>,
974}
975
976fn height_from_tile(
977 bytes: &[u8],
978 tile: &MmapTile,
979 longitude_deg: f64,
980 latitude_deg: f64,
981 options: DtedLookupOptions,
982) -> Result<f64> {
983 if options.interpolation == DtedInterpolation::NearestPosting {
984 return tile
985 .get_elevation(bytes, longitude_deg, latitude_deg)
986 .map(|v| v as f64);
987 }
988
989 let postings_per_deg_lon = tile.index.lon_count as usize - 1;
990 let postings_per_deg_lat = tile.index.lat_count as usize - 1;
991
992 let lon_idx = (longitude_deg - tile.index.min_longitude_deg) * postings_per_deg_lon as f64;
993 let lat_idx = (latitude_deg - tile.index.min_latitude_deg) * postings_per_deg_lat as f64;
994 let lon_lo = lon_idx.floor() as i64;
995 let lat_lo = lat_idx.floor() as i64;
996 let fx = lon_idx - lon_lo as f64;
997 let fy = lat_idx - lat_lo as f64;
998
999 let mut z = 0.0;
1000 for (di, wx) in [(0i64, 1.0 - fx), (1i64, fx)] {
1001 for (dj, wy) in [(0i64, 1.0 - fy), (1i64, fy)] {
1002 let w = wx * wy;
1003 if w == 0.0 {
1004 continue;
1005 }
1006 let posting_lon =
1007 tile.index.min_longitude_deg + (lon_lo + di) as f64 / postings_per_deg_lon as f64;
1008 let posting_lat =
1009 tile.index.min_latitude_deg + (lat_lo + dj) as f64 / postings_per_deg_lat as f64;
1010 z += w * f64::from(tile.get_elevation(bytes, posting_lon, posting_lat)?);
1011 }
1012 }
1013 Ok(z)
1014}
1015
1016fn dted_paths_to_mmap_store(
1017 mut paths: Vec<PathBuf>,
1018) -> core::result::Result<Vec<u8>, TerrainStoreError> {
1019 paths.sort();
1020
1021 let mut pending = Vec::with_capacity(paths.len());
1022 for path in paths {
1023 pending.push(pending_tile_from_dted_path(&path, None)?);
1024 }
1025 build_store(pending)
1026}
1027
1028fn pending_tile_from_dted_path(
1029 path: &Path,
1030 expected_id: Option<TerrainTileId>,
1031) -> core::result::Result<PendingTile, TerrainStoreError> {
1032 let tile = DtedTile::from_path(path).map_err(|reason| TerrainStoreError::Parse {
1033 reason: format!("{}: {reason}", path.display()),
1034 })?;
1035 let decoded = tile
1036 .decoded_postings_lon_major()
1037 .map_err(|reason| TerrainStoreError::Parse {
1038 reason: format!("{}: {reason}", path.display()),
1039 })?;
1040 let mut data = Vec::with_capacity(decoded.len() * 2);
1041 for posting in decoded {
1042 data.extend_from_slice(&posting.to_le_bytes());
1043 }
1044 let lat_index = tile.origin_latitude().floor() as i32;
1045 let lon_index = tile.origin_longitude().floor() as i32;
1046 let found = TerrainTileId::new(lat_index, lon_index);
1047 if let Some(expected) = expected_id {
1048 if expected != found {
1049 return Err(TerrainStoreError::TileIdMismatch {
1050 path: path.to_path_buf(),
1051 expected,
1052 found,
1053 });
1054 }
1055 }
1056 Ok(PendingTile {
1057 lat_index,
1058 lon_index,
1059 min_latitude_deg: tile.origin_latitude(),
1060 min_longitude_deg: tile.origin_longitude(),
1061 max_latitude_deg: tile.origin_latitude() + 1.0,
1062 max_longitude_deg: tile.origin_longitude() + 1.0,
1063 lon_count: u32::try_from(tile.lon_count()).map_err(|_| TerrainStoreError::Parse {
1064 reason: format!("{} longitude count exceeds u32", path.display()),
1065 })?,
1066 lat_count: u32::try_from(tile.lat_count()).map_err(|_| TerrainStoreError::Parse {
1067 reason: format!("{} latitude count exceeds u32", path.display()),
1068 })?,
1069 data,
1070 vertical_datum: VerticalDatum::Egm96MslOrthometric,
1071 })
1072}
1073
1074fn collect_dted_tile_paths(
1075 root: &Path,
1076 out: &mut Vec<PathBuf>,
1077) -> core::result::Result<(), TerrainStoreError> {
1078 let mut visited_dirs = HashSet::new();
1079 collect_dted_tile_paths_inner(root, out, &mut visited_dirs)
1080}
1081
1082fn collect_dted_tile_paths_inner(
1083 path: &Path,
1084 out: &mut Vec<PathBuf>,
1085 visited_dirs: &mut HashSet<PathBuf>,
1086) -> core::result::Result<(), TerrainStoreError> {
1087 let metadata = fs::metadata(path).map_err(|err| TerrainStoreError::Io {
1088 path: path.to_path_buf(),
1089 message: err.to_string(),
1090 })?;
1091
1092 if metadata.is_dir() {
1093 let canonical = fs::canonicalize(path).map_err(|err| TerrainStoreError::Io {
1094 path: path.to_path_buf(),
1095 message: err.to_string(),
1096 })?;
1097 if !visited_dirs.insert(canonical) {
1098 return Ok(());
1099 }
1100 let entries = fs::read_dir(path).map_err(|err| TerrainStoreError::Io {
1101 path: path.to_path_buf(),
1102 message: err.to_string(),
1103 })?;
1104 for entry in entries {
1105 let entry = entry.map_err(|err| TerrainStoreError::Io {
1106 path: path.to_path_buf(),
1107 message: err.to_string(),
1108 })?;
1109 collect_dted_tile_paths_inner(&entry.path(), out, visited_dirs)?;
1110 }
1111 } else if metadata.is_file() && is_dted_tile_source(path)? {
1112 out.push(path.to_path_buf());
1113 }
1114 Ok(())
1115}
1116
1117fn is_dted_tile_source(path: &Path) -> core::result::Result<bool, TerrainStoreError> {
1118 if is_dted_tile_path(path) {
1119 return Ok(true);
1120 }
1121
1122 let canonical = fs::canonicalize(path).map_err(|err| TerrainStoreError::Io {
1123 path: path.to_path_buf(),
1124 message: err.to_string(),
1125 })?;
1126 Ok(is_dted_tile_path(&canonical))
1127}
1128
1129fn is_dted_tile_path(path: &Path) -> bool {
1130 path.file_name()
1131 .and_then(|name| name.to_str())
1132 .is_some_and(|name| name.ends_with(".dt2"))
1133}
1134
1135fn parse_store(bytes: &[u8]) -> core::result::Result<ParsedStore, TerrainStoreError> {
1136 if bytes.len() < STORE_HEADER_LEN {
1137 return Err(TerrainStoreError::Parse {
1138 reason: format!(
1139 "store has {} bytes but needs at least {STORE_HEADER_LEN}",
1140 bytes.len()
1141 ),
1142 });
1143 }
1144 if &bytes[..STORE_MAGIC.len()] != STORE_MAGIC {
1145 return Err(TerrainStoreError::Parse {
1146 reason: "missing terrain store magic".to_string(),
1147 });
1148 }
1149 let version = read_u16(bytes, HEADER_VERSION_OFFSET)?;
1150 if version != STORE_VERSION {
1151 return Err(TerrainStoreError::UnsupportedVersion { version });
1152 }
1153 ensure_zero(bytes, 11, 12, "header reserved byte")?;
1154 ensure_zero(bytes, 40, STORE_HEADER_LEN, "header reserved bytes")?;
1155
1156 let vertical_datum = VerticalDatum::from_tag(bytes[HEADER_DATUM_OFFSET])?;
1157 let tile_count = read_u32(bytes, HEADER_TILE_COUNT_OFFSET)? as usize;
1158 let index_offset = read_u64(bytes, HEADER_INDEX_OFFSET_OFFSET)? as usize;
1159 let data_offset = read_u64(bytes, HEADER_DATA_OFFSET_OFFSET)? as usize;
1160 let total_len = read_u64(bytes, HEADER_TOTAL_LEN_OFFSET)? as usize;
1161 if total_len != bytes.len() {
1162 return Err(TerrainStoreError::Parse {
1163 reason: format!(
1164 "header total length {total_len} does not match {}",
1165 bytes.len()
1166 ),
1167 });
1168 }
1169 if index_offset != STORE_HEADER_LEN {
1170 return Err(TerrainStoreError::Parse {
1171 reason: format!("index offset must be {STORE_HEADER_LEN}, got {index_offset}"),
1172 });
1173 }
1174
1175 let index_len = tile_count
1176 .checked_mul(STORE_INDEX_RECORD_LEN)
1177 .ok_or_else(|| TerrainStoreError::Parse {
1178 reason: "tile index length overflows usize".to_string(),
1179 })?;
1180 let index_end =
1181 index_offset
1182 .checked_add(index_len)
1183 .ok_or_else(|| TerrainStoreError::Parse {
1184 reason: "tile index end overflows usize".to_string(),
1185 })?;
1186 if index_end > bytes.len() {
1187 return Err(TerrainStoreError::Parse {
1188 reason: "tile index extends past store length".to_string(),
1189 });
1190 }
1191 let expected_data_offset = align_up(index_end, STORE_ALIGNMENT)?;
1192 if data_offset != expected_data_offset {
1193 return Err(TerrainStoreError::Parse {
1194 reason: format!("data offset must be {expected_data_offset}, got {data_offset}"),
1195 });
1196 }
1197 ensure_zero(bytes, index_end, data_offset, "index padding")?;
1198
1199 let mut tiles = Vec::with_capacity(tile_count);
1200 let mut tile_index = Vec::with_capacity(tile_count);
1201 let mut tile_ids = Vec::with_capacity(tile_count);
1202 let mut by_grid = HashMap::with_capacity(tile_count);
1203 let mut previous_id = None;
1204 let mut expected_next = data_offset;
1205
1206 for idx in 0..tile_count {
1207 let record_offset = index_offset + idx * STORE_INDEX_RECORD_LEN;
1208 let record = &bytes[record_offset..record_offset + STORE_INDEX_RECORD_LEN];
1209 let lat_index = read_i32(record, INDEX_LAT_OFFSET)?;
1210 let lon_index = read_i32(record, INDEX_LON_OFFSET)?;
1211 let tile_id = (lat_index, lon_index);
1212 if previous_id.is_some_and(|previous| tile_id <= previous) {
1213 return Err(TerrainStoreError::Parse {
1214 reason: "tile index records are not strictly sorted".to_string(),
1215 });
1216 }
1217 previous_id = Some(tile_id);
1218
1219 let lon_count = read_u32(record, INDEX_LON_COUNT_OFFSET)?;
1220 let lat_count = read_u32(record, INDEX_LAT_COUNT_OFFSET)?;
1221 if lon_count < 2 || lat_count < 2 {
1222 return Err(TerrainStoreError::Parse {
1223 reason: format!(
1224 "tile ({lat_index},{lon_index}) has invalid dimensions lon_count={lon_count} lat_count={lat_count}"
1225 ),
1226 });
1227 }
1228 let offset = read_u64(record, INDEX_DATA_OFFSET_OFFSET)? as usize;
1229 let data_len = read_u64(record, INDEX_DATA_LEN_OFFSET)? as usize;
1230 let expected_len = (lon_count as usize)
1231 .checked_mul(lat_count as usize)
1232 .and_then(|count| count.checked_mul(2))
1233 .ok_or_else(|| TerrainStoreError::Parse {
1234 reason: format!("tile ({lat_index},{lon_index}) data length overflows usize"),
1235 })?;
1236 if data_len != expected_len {
1237 return Err(TerrainStoreError::Parse {
1238 reason: format!(
1239 "tile ({lat_index},{lon_index}) data length must be {expected_len}, got {data_len}"
1240 ),
1241 });
1242 }
1243
1244 let expected_offset = align_up(expected_next, STORE_ALIGNMENT)?;
1245 ensure_zero(bytes, expected_next, expected_offset, "tile padding")?;
1246 if offset != expected_offset {
1247 return Err(TerrainStoreError::Parse {
1248 reason: format!(
1249 "tile ({lat_index},{lon_index}) data offset must be {expected_offset}, got {offset}"
1250 ),
1251 });
1252 }
1253 let end = offset
1254 .checked_add(data_len)
1255 .ok_or_else(|| TerrainStoreError::Parse {
1256 reason: format!("tile ({lat_index},{lon_index}) data end overflows usize"),
1257 })?;
1258 if end > bytes.len() {
1259 return Err(TerrainStoreError::Parse {
1260 reason: format!("tile ({lat_index},{lon_index}) data extends past store length"),
1261 });
1262 }
1263
1264 let checksum64 = read_u64(record, INDEX_CHECKSUM_OFFSET)?;
1265 let found = fnv1a64(&bytes[offset..end]);
1266 if found != checksum64 {
1267 return Err(TerrainStoreError::Checksum {
1268 lat_index,
1269 lon_index,
1270 expected: checksum64,
1271 found,
1272 });
1273 }
1274
1275 let min_latitude_deg = read_f64(record, INDEX_MIN_LAT_OFFSET)?;
1276 let min_longitude_deg = read_f64(record, INDEX_MIN_LON_OFFSET)?;
1277 let max_latitude_deg = read_f64(record, INDEX_MAX_LAT_OFFSET)?;
1278 let max_longitude_deg = read_f64(record, INDEX_MAX_LON_OFFSET)?;
1279 for (field, value) in [
1280 ("min_latitude_deg", min_latitude_deg),
1281 ("min_longitude_deg", min_longitude_deg),
1282 ("max_latitude_deg", max_latitude_deg),
1283 ("max_longitude_deg", max_longitude_deg),
1284 ] {
1285 if !value.is_finite() {
1286 return Err(TerrainStoreError::Parse {
1287 reason: format!("tile ({lat_index},{lon_index}) {field} is not finite"),
1288 });
1289 }
1290 }
1291 let tile_datum = VerticalDatum::from_tag(record[INDEX_DATUM_OFFSET])?;
1292 if tile_datum != vertical_datum {
1293 return Err(TerrainStoreError::Parse {
1294 reason: format!("tile ({lat_index},{lon_index}) datum differs from header"),
1295 });
1296 }
1297 ensure_zero(
1298 record,
1299 INDEX_DATUM_OFFSET + 1,
1300 STORE_INDEX_RECORD_LEN,
1301 "tile index reserved bytes",
1302 )?;
1303
1304 let index = TerrainStoreTileIndex {
1305 lat_index,
1306 lon_index,
1307 min_longitude_deg,
1308 min_latitude_deg,
1309 max_longitude_deg,
1310 max_latitude_deg,
1311 lon_count,
1312 lat_count,
1313 data_offset: offset as u64,
1314 data_len: data_len as u64,
1315 checksum64,
1316 vertical_datum: tile_datum,
1317 };
1318 by_grid.insert(tile_id, tiles.len());
1319 tiles.push(MmapTile { index });
1320 tile_index.push(index);
1321 tile_ids.push(TerrainTileId::new(lat_index, lon_index));
1322 expected_next = end;
1323 }
1324
1325 if expected_next != bytes.len() {
1326 return Err(TerrainStoreError::Parse {
1327 reason: format!(
1328 "store has trailing bytes: expected length {expected_next}, got {}",
1329 bytes.len()
1330 ),
1331 });
1332 }
1333
1334 Ok(ParsedStore {
1335 vertical_datum,
1336 tiles,
1337 by_grid,
1338 tile_index,
1339 tile_ids,
1340 })
1341}
1342
1343fn missing_terrain_tile(longitude_deg: f64, latitude_deg: f64) -> Error {
1344 let (lat_index, lon_index) = terrain::terrain_grid(longitude_deg, latitude_deg);
1345 Error::MissingTerrainTile {
1346 lat_index,
1347 lon_index,
1348 }
1349}
1350
1351fn build_store(mut tiles: Vec<PendingTile>) -> core::result::Result<Vec<u8>, TerrainStoreError> {
1352 tiles.sort_by_key(|tile| (tile.lat_index, tile.lon_index));
1353 for pair in tiles.windows(2) {
1354 if (pair[0].lat_index, pair[0].lon_index) == (pair[1].lat_index, pair[1].lon_index) {
1355 return Err(TerrainStoreError::DuplicateTile {
1356 lat_index: pair[0].lat_index,
1357 lon_index: pair[0].lon_index,
1358 });
1359 }
1360 }
1361
1362 let index_end = STORE_HEADER_LEN
1363 .checked_add(
1364 tiles
1365 .len()
1366 .checked_mul(STORE_INDEX_RECORD_LEN)
1367 .ok_or_else(|| TerrainStoreError::Parse {
1368 reason: "tile index length overflows usize".to_string(),
1369 })?,
1370 )
1371 .ok_or_else(|| TerrainStoreError::Parse {
1372 reason: "tile index end overflows usize".to_string(),
1373 })?;
1374 let data_offset = align_up(index_end, STORE_ALIGNMENT)?;
1375 let mut offsets = Vec::with_capacity(tiles.len());
1376 let mut cursor = data_offset;
1377 for tile in &tiles {
1378 cursor = align_up(cursor, STORE_ALIGNMENT)?;
1379 offsets.push(cursor);
1380 cursor = cursor
1381 .checked_add(tile.data.len())
1382 .ok_or_else(|| TerrainStoreError::Parse {
1383 reason: "store length overflows usize".to_string(),
1384 })?;
1385 }
1386
1387 let mut out = vec![0u8; cursor];
1388 out[..STORE_MAGIC.len()].copy_from_slice(STORE_MAGIC);
1389 write_u16(&mut out, HEADER_VERSION_OFFSET, STORE_VERSION);
1390 out[HEADER_DATUM_OFFSET] = VerticalDatum::Egm96MslOrthometric.tag();
1391 write_u32(
1392 &mut out,
1393 HEADER_TILE_COUNT_OFFSET,
1394 u32::try_from(tiles.len()).map_err(|_| TerrainStoreError::Parse {
1395 reason: "tile count exceeds u32".to_string(),
1396 })?,
1397 );
1398 write_u64(
1399 &mut out,
1400 HEADER_INDEX_OFFSET_OFFSET,
1401 STORE_HEADER_LEN as u64,
1402 );
1403 write_u64(&mut out, HEADER_DATA_OFFSET_OFFSET, data_offset as u64);
1404 write_u64(&mut out, HEADER_TOTAL_LEN_OFFSET, cursor as u64);
1405
1406 for (idx, tile) in tiles.iter().enumerate() {
1407 let record_offset = STORE_HEADER_LEN + idx * STORE_INDEX_RECORD_LEN;
1408 let offset = offsets[idx];
1409 let data_len = tile.data.len();
1410 let expected_len = (tile.lon_count as usize)
1411 .checked_mul(tile.lat_count as usize)
1412 .and_then(|count| count.checked_mul(2))
1413 .ok_or_else(|| TerrainStoreError::Parse {
1414 reason: format!(
1415 "tile ({},{}) data length overflows usize",
1416 tile.lat_index, tile.lon_index
1417 ),
1418 })?;
1419 if data_len != expected_len {
1420 return Err(TerrainStoreError::Parse {
1421 reason: format!(
1422 "tile ({},{}) data length must be {expected_len}, got {data_len}",
1423 tile.lat_index, tile.lon_index
1424 ),
1425 });
1426 }
1427
1428 let record = &mut out[record_offset..record_offset + STORE_INDEX_RECORD_LEN];
1429 write_i32(record, INDEX_LAT_OFFSET, tile.lat_index);
1430 write_i32(record, INDEX_LON_OFFSET, tile.lon_index);
1431 write_u32(record, INDEX_LON_COUNT_OFFSET, tile.lon_count);
1432 write_u32(record, INDEX_LAT_COUNT_OFFSET, tile.lat_count);
1433 write_u64(record, INDEX_DATA_OFFSET_OFFSET, offset as u64);
1434 write_u64(record, INDEX_DATA_LEN_OFFSET, data_len as u64);
1435 write_u64(record, INDEX_CHECKSUM_OFFSET, fnv1a64(&tile.data));
1436 write_f64(record, INDEX_MIN_LAT_OFFSET, tile.min_latitude_deg);
1437 write_f64(record, INDEX_MIN_LON_OFFSET, tile.min_longitude_deg);
1438 write_f64(record, INDEX_MAX_LAT_OFFSET, tile.max_latitude_deg);
1439 write_f64(record, INDEX_MAX_LON_OFFSET, tile.max_longitude_deg);
1440 record[INDEX_DATUM_OFFSET] = tile.vertical_datum.tag();
1441 out[offset..offset + data_len].copy_from_slice(&tile.data);
1442 }
1443
1444 Ok(out)
1445}
1446
1447fn align_up(value: usize, alignment: usize) -> core::result::Result<usize, TerrainStoreError> {
1448 let rem = value % alignment;
1449 if rem == 0 {
1450 Ok(value)
1451 } else {
1452 value
1453 .checked_add(alignment - rem)
1454 .ok_or_else(|| TerrainStoreError::Parse {
1455 reason: "aligned offset overflows usize".to_string(),
1456 })
1457 }
1458}
1459
1460fn ensure_zero(
1461 bytes: &[u8],
1462 start: usize,
1463 end: usize,
1464 context: &str,
1465) -> core::result::Result<(), TerrainStoreError> {
1466 if start > end || end > bytes.len() {
1467 return Err(TerrainStoreError::Parse {
1468 reason: format!("{context} range is out of bounds"),
1469 });
1470 }
1471 if bytes[start..end].iter().any(|&byte| byte != 0) {
1472 return Err(TerrainStoreError::Parse {
1473 reason: format!("{context} must be zero-filled"),
1474 });
1475 }
1476 Ok(())
1477}
1478
1479fn fnv1a64(bytes: &[u8]) -> u64 {
1480 bytes.iter().fold(FNV_OFFSET_BASIS, |hash, byte| {
1481 (hash ^ u64::from(*byte)).wrapping_mul(FNV_PRIME)
1482 })
1483}
1484
1485fn read_u16(bytes: &[u8], offset: usize) -> core::result::Result<u16, TerrainStoreError> {
1486 Ok(u16::from_le_bytes(read_array(bytes, offset)?))
1487}
1488
1489fn read_u32(bytes: &[u8], offset: usize) -> core::result::Result<u32, TerrainStoreError> {
1490 Ok(u32::from_le_bytes(read_array(bytes, offset)?))
1491}
1492
1493fn read_i32(bytes: &[u8], offset: usize) -> core::result::Result<i32, TerrainStoreError> {
1494 Ok(i32::from_le_bytes(read_array(bytes, offset)?))
1495}
1496
1497fn read_u64(bytes: &[u8], offset: usize) -> core::result::Result<u64, TerrainStoreError> {
1498 Ok(u64::from_le_bytes(read_array(bytes, offset)?))
1499}
1500
1501fn read_f64(bytes: &[u8], offset: usize) -> core::result::Result<f64, TerrainStoreError> {
1502 Ok(f64::from_le_bytes(read_array(bytes, offset)?))
1503}
1504
1505fn read_array<const N: usize>(
1506 bytes: &[u8],
1507 offset: usize,
1508) -> core::result::Result<[u8; N], TerrainStoreError> {
1509 let end = offset
1510 .checked_add(N)
1511 .ok_or_else(|| TerrainStoreError::Parse {
1512 reason: "numeric field offset overflows usize".to_string(),
1513 })?;
1514 let slice = bytes
1515 .get(offset..end)
1516 .ok_or_else(|| TerrainStoreError::Parse {
1517 reason: "numeric field extends past record".to_string(),
1518 })?;
1519 slice.try_into().map_err(|_| TerrainStoreError::Parse {
1520 reason: "numeric field has wrong length".to_string(),
1521 })
1522}
1523
1524fn write_u16(bytes: &mut [u8], offset: usize, value: u16) {
1525 bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
1526}
1527
1528fn write_u32(bytes: &mut [u8], offset: usize, value: u32) {
1529 bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
1530}
1531
1532fn write_i32(bytes: &mut [u8], offset: usize, value: i32) {
1533 bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
1534}
1535
1536fn write_u64(bytes: &mut [u8], offset: usize, value: u64) {
1537 bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1538}
1539
1540fn write_f64(bytes: &mut [u8], offset: usize, value: f64) {
1541 bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1542}