use crate::crs::*;
use crate::datum::{Datum, HelmertParams};
use crate::ellipsoid::Ellipsoid;
static EPSG_DATA: &[u8] = include_bytes!("../data/epsg.bin");
const MAGIC: u32 = 0x45505347; const HEADER_SIZE: usize = 16;
const ELLIPSOID_RECORD_SIZE: usize = 20;
const DATUM_RECORD_SIZE: usize = 64;
const GEO_CRS_RECORD_SIZE: usize = 8;
const PROJ_CRS_RECORD_SIZE: usize = 80;
pub(crate) const METHOD_WEB_MERCATOR: u8 = 1;
pub(crate) const METHOD_TRANSVERSE_MERCATOR: u8 = 2;
pub(crate) const METHOD_MERCATOR: u8 = 3;
pub(crate) const METHOD_LCC: u8 = 4;
pub(crate) const METHOD_ALBERS: u8 = 5;
pub(crate) const METHOD_POLAR_STEREO: u8 = 6;
pub(crate) const METHOD_EQUIDISTANT_CYL: u8 = 7;
fn read_u16(data: &[u8], offset: usize) -> u16 {
u16::from_le_bytes([data[offset], data[offset + 1]])
}
fn read_u32(data: &[u8], offset: usize) -> u32 {
u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
])
}
fn read_f64(data: &[u8], offset: usize) -> f64 {
f64::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
])
}
struct DbLayout {
num_ellipsoids: usize,
num_datums: usize,
num_geo_crs: usize,
num_proj_crs: usize,
ellipsoid_offset: usize,
datum_offset: usize,
geo_crs_offset: usize,
proj_crs_offset: usize,
}
fn layout() -> DbLayout {
let data = EPSG_DATA;
assert!(data.len() >= HEADER_SIZE, "EPSG database too small");
assert_eq!(read_u32(data, 0), MAGIC, "invalid EPSG database magic");
assert_eq!(read_u16(data, 4), 2, "unsupported EPSG database version");
let num_ellipsoids = read_u16(data, 6) as usize;
let num_datums = read_u16(data, 8) as usize;
let num_geo_crs = read_u16(data, 10) as usize;
let num_proj_crs = read_u32(data, 12) as usize;
let ellipsoid_offset = HEADER_SIZE;
let datum_offset = ellipsoid_offset + num_ellipsoids * ELLIPSOID_RECORD_SIZE;
let geo_crs_offset = datum_offset + num_datums * DATUM_RECORD_SIZE;
let proj_crs_offset = geo_crs_offset + num_geo_crs * GEO_CRS_RECORD_SIZE;
DbLayout {
num_ellipsoids,
num_datums,
num_geo_crs,
num_proj_crs,
ellipsoid_offset,
datum_offset,
geo_crs_offset,
proj_crs_offset,
}
}
fn binary_search_table(
data: &[u8],
table_offset: usize,
record_size: usize,
count: usize,
target: u32,
) -> Option<usize> {
let mut lo = 0usize;
let mut hi = count;
while lo < hi {
let mid = lo + (hi - lo) / 2;
let offset = table_offset + mid * record_size;
let code = read_u32(data, offset);
if code < target {
lo = mid + 1;
} else if code > target {
hi = mid;
} else {
return Some(offset);
}
}
None
}
fn lookup_ellipsoid(db: &DbLayout, epsg: u32) -> Option<Ellipsoid> {
let offset = binary_search_table(
EPSG_DATA,
db.ellipsoid_offset,
ELLIPSOID_RECORD_SIZE,
db.num_ellipsoids,
epsg,
)?;
let a = read_f64(EPSG_DATA, offset + 4);
let inv_f = read_f64(EPSG_DATA, offset + 12);
if inv_f == 0.0 {
Some(Ellipsoid::sphere(a))
} else {
Some(Ellipsoid::from_a_rf(a, inv_f))
}
}
fn lookup_datum(db: &DbLayout, epsg: u32) -> Option<Datum> {
let offset = binary_search_table(
EPSG_DATA,
db.datum_offset,
DATUM_RECORD_SIZE,
db.num_datums,
epsg,
)?;
let ellipsoid_code = read_u32(EPSG_DATA, offset + 4);
let ellipsoid = lookup_ellipsoid(db, ellipsoid_code)?;
let dx = read_f64(EPSG_DATA, offset + 8);
let dy = read_f64(EPSG_DATA, offset + 16);
let dz = read_f64(EPSG_DATA, offset + 24);
let rx = read_f64(EPSG_DATA, offset + 32);
let ry = read_f64(EPSG_DATA, offset + 40);
let rz = read_f64(EPSG_DATA, offset + 48);
let ds = read_f64(EPSG_DATA, offset + 56);
let has_helmert =
dx != 0.0 || dy != 0.0 || dz != 0.0 || rx != 0.0 || ry != 0.0 || rz != 0.0 || ds != 0.0;
let to_wgs84 = if has_helmert {
Some(HelmertParams {
dx,
dy,
dz,
rx,
ry,
rz,
ds,
})
} else {
None
};
Some(Datum {
ellipsoid,
to_wgs84,
})
}
pub(crate) fn lookup_geographic(code: u32) -> Option<CrsDef> {
let db = layout();
let offset = binary_search_table(
EPSG_DATA,
db.geo_crs_offset,
GEO_CRS_RECORD_SIZE,
db.num_geo_crs,
code,
)?;
let datum_code = read_u32(EPSG_DATA, offset + 4);
let datum = lookup_datum(&db, datum_code)?;
Some(CrsDef::Geographic(GeographicCrsDef::new(code, datum, "")))
}
pub(crate) fn lookup_projected(code: u32) -> Option<CrsDef> {
let db = layout();
let offset = binary_search_table(
EPSG_DATA,
db.proj_crs_offset,
PROJ_CRS_RECORD_SIZE,
db.num_proj_crs,
code,
)?;
let datum_code = read_u32(EPSG_DATA, offset + 4);
let datum = lookup_datum(&db, datum_code)?;
let method_id = EPSG_DATA[offset + 8];
let linear_unit_to_meter = read_f64(EPSG_DATA, offset + 16);
let p0 = read_f64(EPSG_DATA, offset + 24);
let p1 = read_f64(EPSG_DATA, offset + 32);
let p2 = read_f64(EPSG_DATA, offset + 40);
let p3 = read_f64(EPSG_DATA, offset + 48);
let p4 = read_f64(EPSG_DATA, offset + 56);
let p5 = read_f64(EPSG_DATA, offset + 64);
let p6 = read_f64(EPSG_DATA, offset + 72);
let method = match method_id {
METHOD_WEB_MERCATOR => ProjectionMethod::WebMercator,
METHOD_TRANSVERSE_MERCATOR => ProjectionMethod::TransverseMercator {
lon0: p0,
lat0: p1,
k0: p2,
false_easting: p3,
false_northing: p4,
},
METHOD_MERCATOR => ProjectionMethod::Mercator {
lon0: p0,
lat_ts: p1,
k0: p2,
false_easting: p3,
false_northing: p4,
},
METHOD_LCC => ProjectionMethod::LambertConformalConic {
lon0: p0,
lat0: p1,
lat1: p2,
lat2: p5,
false_easting: p3,
false_northing: p6,
},
METHOD_ALBERS => ProjectionMethod::AlbersEqualArea {
lon0: p0,
lat0: p1,
lat1: p2,
lat2: p5,
false_easting: p3,
false_northing: p6,
},
METHOD_POLAR_STEREO => ProjectionMethod::PolarStereographic {
lon0: p0,
lat_ts: p1,
k0: p2,
false_easting: p3,
false_northing: p4,
},
METHOD_EQUIDISTANT_CYL => ProjectionMethod::EquidistantCylindrical {
lon0: p0,
lat_ts: p1,
false_easting: p3,
false_northing: p4,
},
_ => return None,
};
let linear_unit = LinearUnit::from_meters_per_unit(linear_unit_to_meter).ok()?;
Some(CrsDef::Projected(ProjectedCrsDef::new(
code,
datum,
method,
linear_unit,
"",
)))
}
pub(crate) fn lookup(code: u32) -> Option<CrsDef> {
lookup_geographic(code).or_else(|| lookup_projected(code))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn db_header_valid() {
let db = layout();
assert!(db.num_ellipsoids > 30, "ellipsoids: {}", db.num_ellipsoids);
assert!(db.num_datums > 100, "datums: {}", db.num_datums);
assert!(db.num_geo_crs > 400, "geo CRS: {}", db.num_geo_crs);
assert!(db.num_proj_crs > 4000, "proj CRS: {}", db.num_proj_crs);
}
#[test]
fn lookup_wgs84() {
let crs = lookup(4326).expect("4326");
assert!(crs.is_geographic());
}
#[test]
fn lookup_web_mercator() {
let crs = lookup(3857).expect("3857");
assert!(crs.is_projected());
}
#[test]
fn lookup_utm_18n() {
let crs = lookup(32618).expect("32618");
assert!(crs.is_projected());
if let CrsDef::Projected(p) = crs {
if let ProjectionMethod::TransverseMercator { lon0, k0, .. } = p.method() {
assert!((lon0 - (-75.0)).abs() < 0.01, "lon0 = {lon0}");
assert!((k0 - 0.9996).abs() < 0.0001, "k0 = {k0}");
} else {
panic!("expected TM");
}
}
}
#[test]
fn lookup_nz_tm() {
let crs = lookup(2193).expect("2193");
assert!(crs.is_projected());
}
#[test]
fn lookup_nc_state_plane() {
let crs = lookup(32119).expect("32119");
assert!(crs.is_projected());
}
#[test]
fn lookup_nc_state_plane_feet_unit() {
let crs = lookup(2264).expect("2264");
let CrsDef::Projected(projected) = crs else {
panic!("expected projected CRS");
};
assert!(
(projected.linear_unit_to_meter() - 0.3048006096012192).abs() < 1e-15,
"linear unit = {}",
projected.linear_unit_to_meter()
);
}
#[test]
fn lookup_unknown() {
assert!(lookup(99999).is_none());
}
}