use std::str::FromStr;
use crate::error::GpkgError;
use crate::gpkg::{GeoPackage, cell_to_i64};
#[derive(Debug, Clone, PartialEq)]
pub enum CoverageDatatype {
Integer,
Float,
}
impl CoverageDatatype {
pub fn as_str(&self) -> &'static str {
match self {
Self::Integer => "integer",
Self::Float => "float",
}
}
}
impl FromStr for CoverageDatatype {
type Err = GpkgError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"integer" => Ok(Self::Integer),
"float" => Ok(Self::Float),
other => Err(GpkgError::InvalidCoverageDatatype(other.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum GridCellEncoding {
Grid,
PixelIsArea,
PixelIsPoint,
}
impl GridCellEncoding {
pub fn as_str(&self) -> &'static str {
match self {
Self::Grid => "grid-value-is-center",
Self::PixelIsArea => "grid-value-is-area",
Self::PixelIsPoint => "grid-value-is-corner",
}
}
}
impl FromStr for GridCellEncoding {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"grid-value-is-center" => Self::Grid,
"grid-value-is-area" => Self::PixelIsArea,
"grid-value-is-corner" => Self::PixelIsPoint,
_ => Self::Grid,
})
}
}
#[derive(Debug, Clone)]
pub struct GriddedCoverage {
pub table_name: String,
pub datatype: CoverageDatatype,
pub scale: f64,
pub offset: f64,
pub precision: f64,
pub data_null: Option<f64>,
pub grid_cell_encoding: GridCellEncoding,
pub uom: Option<String>,
pub field_name: String,
pub quantity_definition: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TileGriddedAncillary {
pub id: i64,
pub tpudt_id: i64,
pub scale: f64,
pub offset: f64,
pub min: Option<f64>,
pub max: Option<f64>,
pub mean: Option<f64>,
pub std_dev: Option<f64>,
}
use crate::btree::CellValue;
fn cell_to_f64(v: &CellValue) -> f64 {
match v {
CellValue::Float(f) => *f,
CellValue::Integer(i) => *i as f64,
_ => 0.0,
}
}
fn cell_to_optional_f64(v: &CellValue) -> Option<f64> {
match v {
CellValue::Null => None,
CellValue::Float(f) => Some(*f),
CellValue::Integer(i) => Some(*i as f64),
_ => None,
}
}
fn cell_to_string(v: &CellValue) -> String {
match v {
CellValue::Text(s) => s.clone(),
CellValue::Integer(i) => i.to_string(),
CellValue::Float(f) => f.to_string(),
CellValue::Blob(b) => String::from_utf8_lossy(b).into_owned(),
CellValue::Null => String::new(),
}
}
fn cell_to_optional_string(v: &CellValue) -> Option<String> {
match v {
CellValue::Null => None,
CellValue::Text(s) if s.is_empty() => None,
other => Some(cell_to_string(other)),
}
}
pub fn load_gridded_coverages(reader: &GeoPackage) -> Result<Vec<GriddedCoverage>, GpkgError> {
let table_name = "gpkg_2d_gridded_coverage_ancillary";
let rows = match reader.scan_table_by_name(table_name)? {
Some(r) => r,
None => return Ok(Vec::new()),
};
let mut out = Vec::with_capacity(rows.len());
for (_rowid, values) in rows {
if values.len() < 11 {
continue;
}
let tbl = cell_to_string(&values[1]);
let datatype_str = cell_to_string(&values[2]);
let datatype = datatype_str.parse::<CoverageDatatype>()?;
let scale = cell_to_f64(&values[3]);
let offset = cell_to_f64(&values[4]);
let precision = cell_to_f64(&values[5]);
let data_null = cell_to_optional_f64(&values[6]);
let encoding_str = cell_to_string(&values[7]);
let grid_cell_encoding = encoding_str
.parse::<GridCellEncoding>()
.unwrap_or(GridCellEncoding::Grid);
let uom = cell_to_optional_string(&values[8]);
let field_name = cell_to_string(&values[9]);
let quantity_definition = cell_to_optional_string(&values[10]);
out.push(GriddedCoverage {
table_name: tbl,
datatype,
scale,
offset,
precision,
data_null,
grid_cell_encoding,
uom,
field_name,
quantity_definition,
});
}
Ok(out)
}
pub fn load_gridded_tile_ancillary(
reader: &GeoPackage,
table_name: &str,
) -> Result<Vec<TileGriddedAncillary>, GpkgError> {
let sys_table = "gpkg_2d_gridded_tile_ancillary";
let rows = match reader.scan_table_by_name(sys_table)? {
Some(r) => r,
None => return Ok(Vec::new()),
};
let mut out = Vec::new();
for (_rowid, values) in rows {
if values.len() < 9 {
continue;
}
let tpudt_name = cell_to_string(&values[1]);
if tpudt_name != table_name {
continue;
}
let id = cell_to_i64(&values[0]);
let tpudt_id = cell_to_i64(&values[2]);
let scale = cell_to_f64(&values[3]);
let offset = cell_to_f64(&values[4]);
let min = cell_to_optional_f64(&values[5]);
let max = cell_to_optional_f64(&values[6]);
let mean = cell_to_optional_f64(&values[7]);
let std_dev = cell_to_optional_f64(&values[8]);
out.push(TileGriddedAncillary {
id,
tpudt_id,
scale,
offset,
min,
max,
mean,
std_dev,
});
}
Ok(out)
}
pub fn unscale_value(
raw: f64,
coverage: &GriddedCoverage,
tile_ancillary: Option<&TileGriddedAncillary>,
) -> f64 {
if let Some(null_val) = coverage.data_null {
#[allow(clippy::float_cmp)]
if raw == null_val {
return f64::NAN;
}
}
let (effective_scale, effective_offset) = match tile_ancillary {
Some(ta) => (ta.scale, ta.offset),
None => (coverage.scale, coverage.offset),
};
raw * effective_scale + effective_offset
}
pub fn unscale_tile_buffer_u16(
raw_buf: &[u16],
coverage: &GriddedCoverage,
tile_ancillary: Option<&TileGriddedAncillary>,
) -> Vec<f64> {
raw_buf
.iter()
.map(|&sample| unscale_value(sample as f64, coverage, tile_ancillary))
.collect()
}
pub fn unscale_tile_buffer_i16(
raw_buf: &[i16],
coverage: &GriddedCoverage,
tile_ancillary: Option<&TileGriddedAncillary>,
) -> Vec<f64> {
raw_buf
.iter()
.map(|&sample| unscale_value(sample as f64, coverage, tile_ancillary))
.collect()
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
fn make_coverage(scale: f64, offset: f64, data_null: Option<f64>) -> GriddedCoverage {
GriddedCoverage {
table_name: "dem".to_string(),
datatype: CoverageDatatype::Integer,
scale,
offset,
precision: 1.0,
data_null,
grid_cell_encoding: GridCellEncoding::Grid,
uom: None,
field_name: "Height".to_string(),
quantity_definition: None,
}
}
fn make_tile_ancillary(scale: f64, offset: f64) -> TileGriddedAncillary {
TileGriddedAncillary {
id: 1,
tpudt_id: 42,
scale,
offset,
min: None,
max: None,
mean: None,
std_dev: None,
}
}
#[test]
fn datatype_from_str_integer() {
assert_eq!(
"integer".parse::<CoverageDatatype>().unwrap(),
CoverageDatatype::Integer
);
}
#[test]
fn datatype_from_str_float() {
assert_eq!(
"float".parse::<CoverageDatatype>().unwrap(),
CoverageDatatype::Float
);
}
#[test]
fn datatype_from_str_invalid() {
let err = "raster".parse::<CoverageDatatype>().unwrap_err();
assert!(
matches!(err, GpkgError::InvalidCoverageDatatype(ref s) if s == "raster"),
"unexpected error: {err:?}"
);
}
#[test]
fn datatype_as_str_roundtrip() {
assert_eq!(CoverageDatatype::Integer.as_str(), "integer");
assert_eq!(CoverageDatatype::Float.as_str(), "float");
}
#[test]
fn grid_cell_encoding_parses_all_three() {
assert_eq!(
"grid-value-is-center".parse::<GridCellEncoding>().unwrap(),
GridCellEncoding::Grid
);
assert_eq!(
"grid-value-is-area".parse::<GridCellEncoding>().unwrap(),
GridCellEncoding::PixelIsArea
);
assert_eq!(
"grid-value-is-corner".parse::<GridCellEncoding>().unwrap(),
GridCellEncoding::PixelIsPoint
);
}
#[test]
fn grid_cell_encoding_unknown_defaults_to_grid() {
assert_eq!(
"unknown-encoding".parse::<GridCellEncoding>().unwrap(),
GridCellEncoding::Grid
);
}
#[test]
fn unscale_value_identity_passthrough() {
let cov = make_coverage(1.0, 0.0, None);
assert_eq!(unscale_value(42.0, &cov, None), 42.0);
}
#[test]
fn unscale_value_scale_and_offset_applied() {
let cov = make_coverage(0.1, -100.0, None);
let phys = unscale_value(1000.0, &cov, None);
assert!((phys - 0.0).abs() < 1e-10, "expected 0.0, got {phys}");
}
#[test]
fn unscale_value_tile_ancillary_overrides_coverage() {
let cov = make_coverage(1.0, 0.0, None);
let ta = make_tile_ancillary(2.0, 5.0);
let phys = unscale_value(10.0, &cov, Some(&ta));
assert!((phys - 25.0).abs() < 1e-10, "expected 25.0, got {phys}");
}
#[test]
fn unscale_value_data_null_returns_nan() {
let cov = make_coverage(1.0, 0.0, Some(0.0));
let result = unscale_value(0.0, &cov, None);
assert!(result.is_nan(), "expected NAN, got {result}");
}
#[test]
fn unscale_value_non_null_not_nan() {
let cov = make_coverage(1.0, 0.0, Some(0.0));
let result = unscale_value(1.0, &cov, None);
assert!(!result.is_nan(), "should not be NAN for non-null value");
assert!((result - 1.0).abs() < 1e-10);
}
#[test]
fn unscale_buffer_u16_identity() {
let cov = make_coverage(1.0, 0.0, None);
let raw = [0u16, 1, 65535];
let out = unscale_tile_buffer_u16(&raw, &cov, None);
assert_eq!(out.len(), 3);
assert!((out[0] - 0.0).abs() < 1e-10);
assert!((out[1] - 1.0).abs() < 1e-10);
assert!((out[2] - 65535.0).abs() < 1e-10);
}
#[test]
fn unscale_buffer_u16_with_scale() {
let cov = make_coverage(0.01, 0.0, None);
let raw = [100u16, 200u16];
let out = unscale_tile_buffer_u16(&raw, &cov, None);
assert!((out[0] - 1.0).abs() < 1e-10, "got {}", out[0]);
assert!((out[1] - 2.0).abs() < 1e-10, "got {}", out[1]);
}
#[test]
fn unscale_buffer_i16_negative_elevations() {
let cov = make_coverage(1.0, 0.0, None);
let raw = [-100i16, 0i16, 100i16];
let out = unscale_tile_buffer_i16(&raw, &cov, None);
assert_eq!(out.len(), 3);
assert!((out[0] - (-100.0)).abs() < 1e-10, "got {}", out[0]);
assert!((out[1] - 0.0).abs() < 1e-10, "got {}", out[1]);
assert!((out[2] - 100.0).abs() < 1e-10, "got {}", out[2]);
}
#[test]
fn unscale_buffer_i16_with_scale_and_offset() {
let cov = make_coverage(0.5, 10.0, None);
let raw = [-2i16, 0i16, 4i16];
let out = unscale_tile_buffer_i16(&raw, &cov, None);
assert!((out[0] - 9.0).abs() < 1e-10, "got {}", out[0]);
assert!((out[1] - 10.0).abs() < 1e-10, "got {}", out[1]);
assert!((out[2] - 12.0).abs() < 1e-10, "got {}", out[2]);
}
}