use super::datum::{Op, parse_steps};
use super::route::Leg;
use std::collections::HashMap;
use std::io::Read;
use std::sync::{Arc, Mutex, OnceLock};
static DATA: &[u8] = include_bytes!("../../data/epsg.tsv.zst");
static ESRI_DATA: &[u8] = include_bytes!("../../data/esri.tsv.zst");
static DATUM_NAMES: &[u8] = include_bytes!("../../data/datums.tsv.zst");
#[derive(Debug)]
pub(crate) struct Entry {
pub name: String,
pub datum: u32,
pub geog: u32,
pub ops: Vec<Arc<Op>>,
pub grid_ops: Vec<Arc<Op>>,
pub dynamic: bool,
pub three_d: bool,
pub area: Option<[f64; 4]>,
pub proj: String,
pub wkt: String,
}
#[derive(Debug)]
pub(crate) struct Vertical {
pub name: String,
pub to_meter: f64,
pub down: bool,
pub wkt: String,
}
#[derive(Debug)]
pub(crate) struct Compound {
pub name: String,
pub horizontal: u32,
pub vertical: u32,
pub wkt: String,
}
type OpLists = (Vec<Arc<Op>>, Vec<Arc<Op>>);
struct PairLists {
lists: OpLists,
forward: Vec<Arc<Op>>,
backward: Vec<Arc<Op>>,
}
struct Table {
text: String,
crs: HashMap<u32, Lazy<Option<Entry>>>,
ops: HashMap<u32, Lazy<Option<Arc<Op>>>>,
pairs: HashMap<(u32, u32), Lazy<PairLists>>,
verticals: HashMap<u32, Lazy<Option<Vertical>>>,
compounds: HashMap<u32, Lazy<Option<Compound>>>,
geoids: HashMap<u32, Vec<(u32, Lazy<OpLists>)>>,
alternatives: HashMap<String, Vec<String>>,
dates: HashMap<u32, String>,
}
struct Lazy<T> {
line: (u32, u32),
value: OnceLock<T>,
}
impl<T> Lazy<T> {
fn new(start: usize, end: usize) -> Self {
Self { line: (start as u32, end as u32), value: OnceLock::new() }
}
fn get(&self, text: &str, parse: impl FnOnce(&str) -> T) -> &T {
self.value.get_or_init(|| parse(&text[self.line.0 as usize..self.line.1 as usize]))
}
}
fn decompress(data: &[u8]) -> String {
#[cfg(not(target_arch = "wasm32"))]
if let Ok(bytes) = zstd::stream::decode_all(data)
&& let Ok(text) = String::from_utf8(bytes)
{
return text;
}
let mut text = String::new();
if let Ok(mut d) = ruzstd::decoding::StreamingDecoder::new(&mut &data[..]) {
let _ = d.read_to_string(&mut text);
}
text
}
fn table() -> &'static Table {
static TABLE: OnceLock<Table> = OnceLock::new();
TABLE.get_or_init(|| load(DATA))
}
fn esri_table() -> &'static Table {
static TABLE: OnceLock<Table> = OnceLock::new();
TABLE.get_or_init(|| load(ESRI_DATA))
}
fn load(data: &[u8]) -> Table {
let text = decompress(data);
let (mut crs, mut ops, mut pairs) = (HashMap::new(), HashMap::new(), HashMap::new());
let (mut verticals, mut compounds, mut geoids) = (HashMap::new(), HashMap::new(), HashMap::<u32, Vec<_>>::new());
let mut alternatives = HashMap::<String, Vec<String>>::new();
let mut dates = HashMap::new();
let mut pos = 0;
for line in text.split_inclusive('\n') {
let start = pos;
pos += line.len();
let line = line.trim_end_matches(['\n', '\r']);
let end = start + line.len();
let mut f = line.splitn(3, '\t');
let (Some(kind), Some(key)) = (f.next(), f.next()) else { continue };
let rest = (start + kind.len() + key.len() + 2).min(end);
let after_second = || (rest + line[rest - start..].find('\t').map_or(end - rest, |i| i + 1)).min(end);
let second = || line[rest - start..].split('\t').next().and_then(|v| v.parse::<u32>().ok());
match kind {
"O" => {
if let Ok(id) = key.parse::<u32>() {
ops.insert(id, Lazy::new(rest, end));
}
}
"C" => {
if let Ok(code) = key.parse::<u32>() {
crs.insert(code, Lazy::new(rest, end));
}
}
"P" => {
if let (Ok(a), Some(b)) = (key.parse::<u32>(), second()) {
pairs.insert((a, b), Lazy::new(after_second(), end));
}
}
"V" => {
if let Ok(code) = key.parse::<u32>() {
verticals.insert(code, Lazy::new(rest, end));
}
}
"K" => {
if let Ok(code) = key.parse::<u32>() {
compounds.insert(code, Lazy::new(rest, end));
}
}
"G" => {
if let (Ok(g), Some(v)) = (key.parse::<u32>(), second()) {
geoids.entry(v).or_default().push((g, Lazy::new(after_second(), end)));
}
}
"A" => {
alternatives.entry(key.to_string()).or_default().push(line[rest - start..].to_string());
}
"D" => {
if let Ok(code) = key.parse::<u32>() {
dates.insert(code, line[rest - start..].to_string());
}
}
_ => {}
}
}
Table { text, crs, ops, pairs, verticals, compounds, geoids, alternatives, dates }
}
impl Table {
fn op(&self, id: u32) -> Option<Arc<Op>> {
self.ops
.get(&id)?
.get(&self.text, |line| {
let mut f = line.split('\t');
let (Some(bounds), Some(accuracy), Some(flags)) = (f.next(), f.next(), f.next()) else { return None };
let b: Vec<f64> = bounds.split(',').filter_map(|v| v.parse().ok()).collect();
let (true, Some(steps)) = (b.len() == 4, parse_steps(f.next().unwrap_or(""))) else { return None };
let name = f.next().unwrap_or("").to_string();
let accuracy = accuracy.parse().unwrap_or(-1.0);
let bounds = [b[0], b[1], b[2], b[3]];
Some(Arc::new(Op {
bounds,
accuracy,
chain_accuracy: f.next().and_then(|a| a.parse().ok()).unwrap_or(accuracy),
chain_bounds: f
.next()
.map(|v| v.split(',').filter_map(|v| v.parse().ok()).collect::<Vec<f64>>())
.and_then(|v| <[f64; 4]>::try_from(v).ok())
.unwrap_or(bounds),
offshore: flags.contains('o'),
unknown_area: flags.contains('u'),
ballpark: flags.contains('b'),
concatenated: flags.contains('c'),
between_3d: flags.contains('h'),
geocentric: flags.contains('g'),
priority: flags.contains('p'),
keeps_height: flags.contains('z'),
steps,
name,
}))
})
.clone()
}
fn resolve(&self, ids: &str) -> Vec<Arc<Op>> {
ids.split(',').filter_map(|i| i.parse().ok()).filter_map(|i| self.op(i)).collect()
}
fn entry(&self, code: u32) -> Option<&Entry> {
self.crs
.get(&code)?
.get(&self.text, |line| {
let fields: Vec<&str> = line.split('\t').collect();
let [name, datum, geog, op_ids, grid_ids, flags, area, proj, wkt] = fields[..] else { return None };
let area: Vec<f64> = area.split(',').filter_map(|v| v.parse().ok()).collect();
Some(Entry {
name: name.to_string(),
datum: datum.parse().unwrap_or(0),
geog: geog.parse().unwrap_or(0),
ops: self.resolve(op_ids),
grid_ops: self.resolve(grid_ids),
dynamic: flags.contains('d'),
three_d: flags.contains('3'),
area: (area.len() == 4).then(|| [area[0], area[1], area[2], area[3]]),
proj: proj.to_string(),
wkt: wkt.to_string(),
})
})
.as_ref()
}
fn lists(&self, ids: &str) -> OpLists {
let (a, b) = ids.split_once('\t').unwrap_or((ids, ""));
(self.resolve(a), self.resolve(b))
}
fn pair(&self, a: u32, b: u32) -> Option<&PairLists> {
Some(self.pairs.get(&(a, b))?.get(&self.text, |ids| {
let mut f = ids.split('\t');
let mut next = || self.resolve(f.next().unwrap_or(""));
PairLists { lists: (next(), next()), forward: next(), backward: next() }
}))
}
}
pub(crate) fn usable(ops: &[Arc<Op>], grid_ops: &[Arc<Op>]) -> Vec<Arc<Op>> {
if grid_ops.is_empty() {
return ops.to_vec();
}
let kept: Vec<Arc<Op>> = grid_ops.iter().filter(|o| o.available()).cloned().collect();
if kept.iter().any(|o| o.needs_grids() && !o.ballpark) { kept } else { ops.to_vec() }
}
fn chainable(ops: &[Arc<Op>], grid_ops: &[Arc<Op>]) -> Vec<Arc<Op>> {
let mut out: Vec<Arc<Op>> = grid_ops.iter().filter(|o| o.available()).cloned().collect();
for op in ops {
if !out.iter().any(|o| Arc::ptr_eq(o, op)) {
out.push(op.clone());
}
}
out
}
pub(crate) fn prefetch() {
#[cfg(not(target_arch = "wasm32"))]
rayon::spawn(|| {
table();
});
}
pub(crate) fn get(code: u32) -> Option<&'static Entry> {
table().entry(code)
}
pub(crate) fn esri(code: u32) -> Option<&'static Entry> {
esri_table().entry(code)
}
fn name_index(t: &Table, key: impl Fn(&str) -> String) -> HashMap<String, Vec<u32>> {
let mut names = HashMap::<String, Vec<u32>>::new();
for (code, l) in &t.crs {
if let Some(n) = t.text[l.line.0 as usize..l.line.1 as usize].split('\t').next() {
names.entry(key(n)).or_default().push(*code);
}
}
names.values_mut().for_each(|c| c.sort_unstable());
names
}
pub(crate) fn named(name: &str) -> Vec<u32> {
static NAMES: OnceLock<HashMap<String, Vec<u32>>> = OnceLock::new();
let names = NAMES.get_or_init(|| name_index(table(), str::to_ascii_lowercase));
names.get(&name.to_ascii_lowercase()).cloned().unwrap_or_default()
}
pub(crate) fn datum_key(name: &str) -> String {
let name = name.strip_prefix("D_").unwrap_or(name);
name.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '+').map(|c| c.to_ascii_lowercase()).collect()
}
const ESRI_WRITTEN: u8 = 5;
type DatumNames = (HashMap<String, (u8, Vec<u32>)>, HashMap<String, Vec<u32>>);
fn datum_names() -> &'static DatumNames {
static NAMES: OnceLock<DatumNames> = OnceLock::new();
NAMES.get_or_init(|| {
let (mut names, mut written) = (HashMap::<String, (u8, Vec<u32>)>::new(), HashMap::<String, Vec<u32>>::new());
for line in decompress(DATUM_NAMES).lines().filter(|l| !l.starts_with('#')) {
let mut f = line.splitn(3, '\t');
let (Some(Ok(code)), Some(Ok(rank)), Some(name)) = (f.next().map(str::parse::<u32>), f.next().map(str::parse::<u8>), f.next())
else {
continue;
};
if rank == ESRI_WRITTEN {
written.entry(datum_key(name)).or_default().push(code);
continue;
}
let e = names.entry(datum_key(name)).or_insert((rank, Vec::new()));
if rank < e.0 {
*e = (rank, Vec::new());
}
if rank == e.0 && !e.1.contains(&code) {
e.1.push(code);
}
}
names.values_mut().for_each(|(_, c)| c.sort_unstable());
written.values_mut().for_each(|c| c.sort_unstable());
debug_assert!(!names.is_empty(), "the datum name table did not decompress");
(names, written)
})
}
pub(crate) fn datums_named(name: &str) -> Vec<u32> {
datum_names().0.get(&datum_key(name)).map(|(_, c)| c.clone()).unwrap_or_default()
}
pub(crate) fn datums_written_as(name: &str) -> &'static [u32] {
datum_names().1.get(&datum_key(name)).map_or(&[], Vec::as_slice)
}
pub(crate) fn geographic_on(datum: u32) -> &'static [u32] {
static INDEX: OnceLock<HashMap<u32, Vec<u32>>> = OnceLock::new();
let index = INDEX.get_or_init(|| {
let t = table();
let mut index = HashMap::<u32, Vec<u32>>::new();
for (code, l) in &t.crs {
let f: Vec<&str> = t.text[l.line.0 as usize..l.line.1 as usize].splitn(9, '\t').collect();
let [_, datum_field, geog, _, _, flags, _, proj, ..] = f[..] else { continue };
if geog.parse() == Ok(*code)
&& !flags.contains('3')
&& proj.starts_with("+proj=longlat")
&& let Ok(d) = datum_field.parse::<u32>()
{
index.entry(d).or_default().push(*code);
}
}
index.values_mut().for_each(|c| c.sort_unstable());
index
});
index.get(&datum).map_or(&[], Vec::as_slice)
}
pub(crate) fn projected_on(geog: u32) -> &'static [(u32, &'static str)] {
static INDEX: OnceLock<HashMap<u32, Vec<(u32, &'static str)>>> = OnceLock::new();
let index = INDEX.get_or_init(|| {
let t = table();
let mut index = HashMap::<u32, Vec<(u32, &'static str)>>::new();
for (code, l) in &t.crs {
let f: Vec<&'static str> = t.text[l.line.0 as usize..l.line.1 as usize].splitn(9, '\t').collect();
let [_, _, geog, _, _, flags, _, proj, wkt] = f[..] else { continue };
let current = super::declared_epsg(wkt) == Some(*code);
let method = proj.split_whitespace().next().unwrap_or_default();
if current
&& !flags.contains('3')
&& method != "+proj=longlat"
&& let Ok(g) = geog.parse::<u32>()
{
index.entry(g).or_default().push((*code, method));
}
}
index.values_mut().for_each(|c| c.sort_unstable());
index
});
index.get(&geog).map_or(&[], Vec::as_slice)
}
pub(crate) fn esri_named(name: &str) -> Vec<u32> {
static NAMES: OnceLock<HashMap<String, Vec<u32>>> = OnceLock::new();
let key = |n: &str| n.to_ascii_lowercase().replace(' ', "_");
let names = NAMES.get_or_init(|| name_index(esri_table(), key));
names.get(&key(name)).cloned().unwrap_or_default()
}
pub(crate) type Adjacency = HashMap<u32, Vec<(u32, Vec<Leg>)>>;
pub(crate) fn adjacency() -> Arc<Adjacency> {
static ADJ: Mutex<Option<(u64, Arc<Adjacency>)>> = Mutex::new(None);
let generation = super::grid::generation();
let mut cached = ADJ.lock().unwrap_or_else(|e| e.into_inner());
if let Some((g, adj)) = cached.as_ref()
&& *g == generation
{
return adj.clone();
}
let adj = Arc::new({
let t = table();
let single = |ops: &[Arc<Op>], reverse: bool| -> Vec<Leg> {
ops.iter().filter(|o| !o.ballpark && !o.concatenated).map(|op| Leg { op: op.clone(), reverse }).collect()
};
let mut adj = Adjacency::new();
let mut add = |a: u32, b: u32, legs: Vec<Leg>| {
if !legs.is_empty() {
adj.entry(a).or_default().push((b, legs));
}
};
let mut pair_keys: Vec<(u32, u32)> = t.pairs.keys().copied().collect();
pair_keys.sort_unstable();
for (a, b) in pair_keys {
let Some(p) = t.pair(a, b) else { continue };
let ops = chainable(&p.lists.0, &p.lists.1);
let found = |ops: &[Arc<Op>], reverse: bool| -> Vec<Leg> {
ops.iter().filter(|o| o.available()).map(|op| Leg { op: op.clone(), reverse }).collect()
};
let mut there = single(&ops, false);
there.extend(found(&p.forward, false).into_iter().chain(found(&p.backward, true)));
let mut back = single(&ops, true);
back.extend(found(&p.forward, true).into_iter().chain(found(&p.backward, false)));
add(a, b, there);
add(b, a, back);
}
let mut codes: Vec<u32> = t.crs.keys().copied().collect();
codes.sort_unstable();
for code in codes {
let Some(e) = t.entry(code) else { continue };
if e.geog == code && code != 4326 && e.datum != 6326 && !e.three_d {
let ops = chainable(&e.ops, &e.grid_ops);
add(code, 4326, single(&ops, false));
add(4326, code, single(&ops, true));
}
}
adj
});
*cached = Some((generation, adj.clone()));
adj
}
pub(crate) fn pair(from: u32, to: u32) -> Option<(Vec<Arc<Op>>, bool)> {
let t = table();
let (p, reverse) = t.pair(from, to).map(|o| (o, false)).or_else(|| t.pair(to, from).map(|o| (o, true)))?;
Some((usable(&p.lists.0, &p.lists.1), reverse))
}
pub(crate) fn vertical(code: u32) -> Option<&'static Vertical> {
let t = table();
t.verticals
.get(&code)?
.get(&t.text, |line| {
let f: Vec<&str> = line.split('\t').collect();
let [name, _datum, unit, direction, wkt] = f[..] else { return None };
Some(Vertical {
name: name.to_string(),
to_meter: unit.parse().unwrap_or(1.0),
down: direction == "down",
wkt: wkt.to_string(),
})
})
.as_ref()
}
pub(crate) fn compound(code: u32) -> Option<&'static Compound> {
let t = table();
t.compounds
.get(&code)?
.get(&t.text, |line| {
let f: Vec<&str> = line.split('\t').collect();
let [name, h, v, wkt] = f[..] else { return None };
Some(Compound { name: name.to_string(), horizontal: h.parse().ok()?, vertical: v.parse().ok()?, wkt: wkt.to_string() })
})
.as_ref()
}
pub(crate) fn geoid_models(vertical: u32) -> Vec<(u32, Vec<Arc<Op>>)> {
let t = table();
let Some(list) = t.geoids.get(&vertical) else { return Vec::new() };
list.iter()
.map(|(g, lazy)| {
let (ops, grid_ops) = lazy.get(&t.text, |ids| t.lists(ids));
(*g, usable(ops, grid_ops))
})
.filter(|(_, ops)| !ops.is_empty())
.collect()
}
pub(crate) fn datum_date(datum: u32) -> Option<&'static str> {
table().dates.get(&datum).map(String::as_str)
}
pub(crate) fn grid_alternatives(name: &str) -> Vec<String> {
table().alternatives.get(name).cloned().unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn table_loads() {
let e = get(2263).unwrap();
assert_eq!(e.name, "NAD83 / New York Long Island (ftUS)");
assert!(e.proj.contains("+units=us-ft"));
assert!(e.wkt.starts_with("PROJCS[\"NAD83 / New York Long Island (ftUS)\""));
assert_eq!(e.datum, 6269);
assert!(get(23030).unwrap().ops.len() > 3);
assert_eq!(get(31467).unwrap().geog, 4314);
assert!(pair(4314, 4258).is_some());
assert_eq!(vertical(5773).unwrap().name, "EGM96 height");
let c = compound(5498).unwrap();
assert_eq!((c.horizontal, c.vertical), (4269, 5703));
assert!(get(4979).unwrap().three_d && get(4979).unwrap().geog == 4326);
assert!(get(9000).unwrap().dynamic);
assert_eq!(grid_alternatives("us_noaa_conus.tif"), vec!["conus".to_string()]);
assert_eq!(datum_date(6759), Some("2007-01-01"));
assert_eq!(datum_date(6326), None);
let p = table().pair(9000, 4267).unwrap();
assert!(p.backward.iter().any(|o| o.name.starts_with("NAD27 to NAD83 (7) + ") && o.accuracy == 0.2));
}
#[test]
fn esri_table_loads() {
let e = esri(102003).unwrap();
assert_eq!(e.name, "USA_Contiguous_Albers_Equal_Area_Conic");
assert_eq!((e.datum, e.geog), (6269, 4269));
assert!(e.proj.starts_with("+proj=aea") && !e.ops.is_empty());
assert!(esri(4326).is_none() && get(54009).is_none());
assert_eq!(esri_named("World Mollweide"), vec![54009]);
}
}