use super::{BaseGrid, GridHeader, GridSource};
use crate::Error;
const HEADER_SIZE: usize = 11 * 16;
const HEAD_NUM_RECORDS: usize = 8;
const NAME: usize = 8;
const PARENT: usize = 24;
const NLAT: usize = 88;
const SLAT: usize = 72;
const ELON: usize = 104;
const WLON: usize = 120;
const DLAT: usize = 136;
const DLON: usize = 152;
const GSCOUNT: usize = 168;
const NODE_LAT_CORRECTION: usize = 0;
const NODE_LON_CORRECTION: usize = 4;
const NODE_SIZE: usize = 16;
pub(crate) fn ntv2_grid(buf: &[u8]) -> Result<BaseGrid, Error> {
let parser = NTv2Parser::new(buf);
if !parser.cmp_str(0, "NUM_OREC") {
return Err(Error::Unsupported("Not a NTv2 file".to_string()));
}
let num_overview_records = parser.get_u32(8) as usize;
if num_overview_records != 11 {
return Err(Error::Unsupported("Bad header".to_string()));
}
if !parser.cmp_str(56, "SECONDS") {
return Err(Error::Invalid("Not in seconds".to_string()));
}
let num_sub_grids = parser.get_u32(40) as usize;
let mut grids = Vec::new();
let mut offset = HEADER_SIZE;
for _ in 0..num_sub_grids {
let (name, parent, grid) = parse_ntv2_grid(&parser, offset)?;
let GridSource::Internal { values } = &grid.grid else {
return Err(Error::Invalid("Bad subgrid".to_string()));
};
offset += HEADER_SIZE + values.len() / 2 * NODE_SIZE;
grids.push((name, parent, grid));
}
let mut top;
let topname;
if let Some(index) = grids.iter().position(|(_, parent, _)| parent == "NONE") {
(topname, _, top) = grids.swap_remove(index);
} else {
return Err(Error::Invalid(
"Invalid NTv2 file: Missing top level grid".into(),
));
}
if grids.is_empty() {
return Ok(top);
}
if grids.iter().any(|(_, parent, _)| parent == "NONE") {
return Err(Error::Invalid(
"Unsupported NTv2 file: More than one top level grid".into(),
));
}
if let Some(index) = grids.iter().position(|(_, parent, _)| *parent == topname) {
grids.swap(0, index);
} else {
return Err(Error::Invalid("Invalid NTv2 grid: Missing subgrid".into()));
}
for i in 0..grids.len() {
for j in i..grids.len() {
if grids[i].0 == grids[j].1 {
grids.swap(i, j);
}
}
}
grids.reverse();
for grid in grids {
top.subgrids.push(grid.2);
}
Ok(top)
}
fn parse_ntv2_header(
parser: &NTv2Parser,
offset: usize,
) -> Result<(String, String, GridHeader), Error> {
let lat_n = parser.get_f64(offset + NLAT);
let lat_s = parser.get_f64(offset + SLAT);
let lon_w = -parser.get_f64(offset + WLON);
let lon_e = -parser.get_f64(offset + ELON);
let dlat = parser.get_f64(offset + DLAT);
let dlon = parser.get_f64(offset + DLON);
let rows = (((lat_n - lat_s) / dlat).abs() + 1.5).floor() as usize;
let cols = (((lon_e - lon_w) / dlon).abs() + 1.5).floor() as usize;
let num_nodes = parser.get_u32(offset + GSCOUNT) as usize;
if num_nodes != rows * cols {
return Err(Error::Invalid(
"Number of nodes does not match the grid size".to_string(),
));
}
let dlat = dlat.copysign(lat_s - lat_n);
let dlon = dlon.copysign(lon_e - lon_w);
let radians_from_arcsec = 1f64.to_radians() / 3600f64;
let header = GridHeader {
lat_n: lat_n * radians_from_arcsec,
lat_s: lat_s * radians_from_arcsec,
lon_w: lon_w * radians_from_arcsec,
lon_e: lon_e * radians_from_arcsec,
dlat: dlat * radians_from_arcsec,
dlon: dlon * radians_from_arcsec,
rows,
cols,
bands: 2,
};
let name = parser.get_str(offset + NAME, 8)?.trim().to_string();
let parent = parser.get_str(offset + PARENT, 8)?.trim().to_string();
Ok((name, parent, header))
}
fn parse_ntv2_grid(
parser: &NTv2Parser,
head_offset: usize,
) -> Result<(String, String, BaseGrid), Error> {
let (name, parent, header) = parse_ntv2_header(parser, head_offset)?;
let grid_start = head_offset + HEADER_SIZE;
let grid = parse_ntv2_grid_nodes(parser, grid_start, header.rows * header.cols)?;
let base_grid = BaseGrid::new(
&name,
header,
crate::grid::GridSource::Internal { values: grid },
)?;
Ok((name, parent, base_grid))
}
fn parse_ntv2_grid_nodes(
parser: &NTv2Parser,
grid_start: usize,
num_nodes: usize,
) -> Result<Vec<f32>, Error> {
let grid_end_offset = grid_start + num_nodes * NODE_SIZE;
if grid_end_offset > parser.buffer().len() {
return Err(Error::Invalid("Grid Too Short".to_string()));
}
let mut grid = Vec::with_capacity(2 * num_nodes);
for i in (0..num_nodes).rev() {
let offset = grid_start + i * NODE_SIZE;
let lat_offset = offset + NODE_LAT_CORRECTION;
let lon_offset = offset + NODE_LON_CORRECTION;
let lat_corr = parser.get_f32(lat_offset) as f64;
let lon_corr = -parser.get_f32(lon_offset) as f64;
grid.push(lon_corr as f32);
grid.push(lat_corr as f32);
}
Ok(grid)
}
struct NTv2Parser<'a> {
buf: &'a [u8],
is_big_endian: bool,
}
impl<'a> NTv2Parser<'a> {
pub fn new(buf: &'a [u8]) -> Self {
let is_big_endian = buf[HEAD_NUM_RECORDS] != 11;
Self { buf, is_big_endian }
}
pub fn get_f64(&self, offset: usize) -> f64 {
match self.is_big_endian {
true => f64::from_be_bytes(self.buf[offset..offset + 8].try_into().unwrap()),
false => f64::from_le_bytes(self.buf[offset..offset + 8].try_into().unwrap()),
}
}
pub fn get_f32(&self, offset: usize) -> f32 {
match self.is_big_endian {
true => f32::from_be_bytes(self.buf[offset..offset + 4].try_into().unwrap()),
false => f32::from_le_bytes(self.buf[offset..offset + 4].try_into().unwrap()),
}
}
pub fn get_u32(&self, offset: usize) -> u32 {
match self.is_big_endian {
true => u32::from_be_bytes(self.buf[offset..offset + 4].try_into().unwrap()),
false => u32::from_le_bytes(self.buf[offset..offset + 4].try_into().unwrap()),
}
}
pub fn get_str(&self, offset: usize, len: usize) -> Result<&str, Error> {
std::str::from_utf8(&self.buf[offset..offset + len]).map_err(Error::from)
}
pub fn cmp_str(&self, offset: usize, s: &str) -> bool {
self.get_str(offset, s.len())
.map(|x| x == s)
.unwrap_or(false)
}
pub fn buffer(&self) -> &[u8] {
self.buf
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coord::{Coor4D, CoordinateTuple};
use crate::grid::Grid;
use float_eq::assert_float_eq;
#[test]
fn basic_ntv2_grid() -> Result<(), Error> {
let grid_buff = std::fs::read("geodesy/gsb/100800401.gsb").unwrap();
let basegrid = ntv2_grid(&grid_buff)?;
let barc = Coor4D::geo(41.3874, 2.1686, 0.0, 0.0);
let ldn = Coor4D::geo(51.505, -0.09, 0., 0.);
let first = Coor4D::geo(40.0, 3.5, 0., 0.);
let next = Coor4D::geo(40.0, 0., 0., 0.);
assert_eq!(basegrid.subgrids.len(), 0);
let GridSource::Internal { values } = &basegrid.grid else {
return Err(Error::Invalid(
"Missing internal values in NTv2 file".to_string(),
));
};
assert_eq!(values.len(), 1591 * 2);
assert_eq!(basegrid.bands(), 2);
assert!(basegrid.contains(barc, 0.5, true));
assert!(!basegrid.contains(ldn, 0.5, true));
let (dlon, dlat) = basegrid.at(None, next, 0.0).unwrap().xy();
assert_float_eq!(dlat, -4.2328200340, abs_all <= 1e-6);
assert_float_eq!(dlon, -4.3312602043, abs_all <= 1e-6);
let (dlon, dlat) = basegrid.at(None, first, 1.0).unwrap().xy();
assert_float_eq!(dlat, -4.1843700409, abs_all <= 1e-6);
assert_float_eq!(dlon, -3.9602699280, abs_all <= 1e-6);
Ok(())
}
#[test]
fn ntv2_multi_subgrid() -> Result<(), Error> {
let grid_buff = std::fs::read("geodesy/gsb/5458_with_subgrid.gsb").unwrap();
let basegrid = ntv2_grid(&grid_buff)?;
assert!(basegrid.subgrids.len() == 1);
Ok(())
}
#[test]
fn ntv2_multi_subgrid_find_grid() -> Result<(), Error> {
let grid_buff = std::fs::read("geodesy/gsb/5458_with_subgrid.gsb").unwrap();
let basegrid = ntv2_grid(&grid_buff)?;
let within_densified_grid = Coor4D::geo(55.5, 13.0, 0.0, 0.0);
let grid_id = basegrid
.which_subgrid_contains(within_densified_grid, 0.)
.unwrap();
assert_eq!(grid_id, "5556");
let on_densified_upper_lat = Coor4D::geo(56.0, 13.0, 0.0, 0.0);
let grid_id = basegrid
.which_subgrid_contains(on_densified_upper_lat, 0.0)
.unwrap();
assert_eq!(grid_id, "5458");
let on_densified_upper_lon = Coor4D::geo(55.5, 14.0, 0.0, 0.0);
let grid_id = basegrid
.which_subgrid_contains(on_densified_upper_lon, 0.0)
.unwrap();
assert_eq!(grid_id, "5458");
let on_densified_lower_lat = Coor4D::geo(55.0, 13.0, 0.0, 0.0);
let grid_id = basegrid
.which_subgrid_contains(on_densified_lower_lat, 0.0)
.unwrap();
assert_eq!(grid_id, "5556");
let on_densified_lower_lon = Coor4D::geo(55.5, 12.0, 0.0, 0.0);
let grid_id = basegrid
.which_subgrid_contains(on_densified_lower_lon, 0.0)
.unwrap();
assert_eq!(grid_id, "5556");
let on_root_upper_lat = Coor4D::geo(58.0, 12.0, 0.0, 0.0);
let grid_id = basegrid
.which_subgrid_contains(on_root_upper_lat, 0.0)
.unwrap();
assert_eq!(grid_id, "5458");
let on_root_upper_lon = Coor4D::geo(55.5, 16.0, 0.0, 0.0);
let grid_id = basegrid
.which_subgrid_contains(on_root_upper_lon, 0.0)
.unwrap();
assert_eq!(grid_id, "5458");
let on_root_lower_lat = Coor4D::geo(54.0, 12.0, 0.0, 0.0);
let grid_id = basegrid
.which_subgrid_contains(on_root_lower_lat, 0.0)
.unwrap();
assert_eq!(grid_id, "5458");
let on_root_lower_lon = Coor4D::geo(55.5, 8.0, 0.0, 0.0);
let grid_id = basegrid
.which_subgrid_contains(on_root_lower_lon, 0.0)
.unwrap();
assert_eq!(grid_id, "5458");
let on_root_upper_lat = Coor4D::geo(58.25, 12.0, 0.0, 0.0);
let grid_id = basegrid
.which_subgrid_contains(on_root_upper_lat, 0.5)
.unwrap();
assert_eq!(grid_id, "5458");
let on_root_upper_lon = Coor4D::geo(55.5, 16.25, 0.0, 0.0);
let grid_id = basegrid
.which_subgrid_contains(on_root_upper_lon, 0.5)
.unwrap();
assert_eq!(grid_id, "5458");
let on_root_lower_lat = Coor4D::geo(53.75, 12.0, 0.0, 0.0);
let grid_id = basegrid
.which_subgrid_contains(on_root_lower_lat, 0.5)
.unwrap();
assert_eq!(grid_id, "5458");
let on_root_lower_lon = Coor4D::geo(55.5, 7.75, 0.0, 0.0);
let grid_id = basegrid
.which_subgrid_contains(on_root_lower_lon, 0.5)
.unwrap();
assert_eq!(grid_id, "5458");
Ok(())
}
}