use crate::LazyRaster;
use crate::crs::{SpatialRef, Transformer};
use crate::dataset::{Dataset, RasterDataset, VectorDataset};
use serde::Serialize;
use std::fmt::Write as _;
pub use crate::histogram::Histogram;
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Info {
Raster(Box<RasterInfo>),
Vector(VectorInfo),
#[cfg(feature = "pointcloud")]
#[cfg_attr(docsrs, doc(cfg(feature = "pointcloud")))]
#[serde(rename = "pointcloud")]
PointCloud(Box<crate::pointcloud::info::PointCloudInfo>),
}
#[derive(Debug, Clone, Serialize)]
pub struct RasterInfo {
pub driver: String,
pub size: [usize; 2],
pub band_count: usize,
pub geo_transform: [f64; 6],
pub crs: CrsSummary,
pub extent: [f64; 4],
pub wgs84_extent: Option<[f64; 4]>,
pub bands: Vec<BandInfo>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub overviews: Vec<[usize; 2]>,
pub metadata: Vec<(String, String)>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub subdatasets: Vec<Subdataset>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub geolocation: Vec<(String, String)>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Subdataset {
pub name: String,
pub description: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct BandInfo {
pub band: usize,
pub data_type: String,
#[serde(serialize_with = "serialize_nodata")]
pub nodata: Option<f64>,
pub statistics: Option<Statistics>,
#[serde(skip_serializing_if = "Option::is_none")]
pub histogram: Option<Histogram>,
}
fn serialize_nodata<S: serde::Serializer>(v: &Option<f64>, s: S) -> Result<S::Ok, S::Error> {
match v {
None => s.serialize_none(),
Some(x) if x.is_nan() => s.serialize_str("nan"),
Some(x) if x.is_infinite() => s.serialize_str(if *x > 0.0 { "inf" } else { "-inf" }),
Some(x) => s.serialize_f64(*x),
}
}
#[derive(Debug, Clone, Copy, Serialize)]
pub struct Statistics {
pub min: f64,
pub max: f64,
pub mean: f64,
pub std_dev: f64,
pub valid_count: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct CrsSummary {
pub epsg: Option<u32>,
pub name: Option<String>,
pub wkt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub esri: Option<u32>,
}
impl CrsSummary {
pub(crate) fn new(s: &SpatialRef) -> Self {
Self { epsg: s.epsg, name: s.name(), wkt: s.to_wkt(), esri: s.esri }
}
pub fn code(&self) -> Option<String> {
match (self.epsg, self.esri) {
(Some(code), _) => Some(format!("EPSG:{code}")),
(None, Some(code)) => Some(format!("ESRI:{code}")),
(None, None) => None,
}
}
pub(crate) fn line(&self) -> String {
match (self.code(), &self.name) {
(Some(code), Some(name)) => format!("{code} ({name})"),
(Some(code), None) => code,
(None, Some(name)) => name.clone(),
(None, None) => "unknown".into(),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct VectorInfo {
pub driver: String,
pub layers: Vec<LayerInfo>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub subdatasets: Vec<Subdataset>,
}
#[derive(Debug, Clone, Serialize)]
pub struct LayerInfo {
pub name: String,
pub geometry_type: String,
pub feature_count: usize,
pub crs: CrsSummary,
pub extent: Option<[f64; 4]>,
pub fields: Vec<FieldInfo>,
}
#[derive(Debug, Clone, Serialize)]
pub struct FieldInfo {
pub name: String,
pub field_type: String,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct InfoOptions {
pub statistics: bool,
pub histogram: bool,
}
pub fn info(ds: &Dataset, options: &InfoOptions) -> Info {
match ds {
Dataset::Raster(r) => Info::Raster(Box::new(raster_info(r, options))),
Dataset::Vector(v) => Info::Vector(vector_info(v)),
}
}
pub fn raster_info(ds: &RasterDataset, options: &InfoOptions) -> RasterInfo {
raster_info_at(ds, options, None, None)
}
fn raster_info_at(ds: &RasterDataset, options: &InfoOptions, path: Option<&str>, mut lazy: Option<&mut LazyRaster>) -> RasterInfo {
let r = &ds.raster;
let crs = ds.crs();
let extent = [r.x_min, r.y_min, r.x_max(), r.y_max()];
let part = if ds.complex { 2 } else { 1 };
let bands = (0..ds.band_count())
.map(|b| {
let statistics = options.statistics.then(|| band_statistics(ds, b * part)).flatten();
let histogram = options
.histogram
.then(|| {
let ctx = histogram_context(ds, b * part, path, lazy.as_deref_mut());
crate::histogram::default_histogram(ds, b * part, &ctx, statistics.map(|s| (s.min, s.max)))
})
.flatten();
BandInfo { band: b + 1, data_type: ds.data_type_name(), nodata: ds.nodata(), statistics, histogram }
})
.collect();
RasterInfo {
driver: ds.driver.name().to_string(),
size: [r.cols, r.rows],
band_count: ds.band_count(),
geo_transform: ds.geo_transform(),
wgs84_extent: wgs84_extent(&crs, extent),
crs: CrsSummary::new(&crs),
extent,
bands,
overviews: Vec::new(),
metadata: r.metadata.iter().filter(|(k, _)| subdataset_index(k).is_none()).cloned().collect(),
subdatasets: subdatasets_from_metadata(&r.metadata),
geolocation: ds.geolocation.as_ref().map(|g| g.items()).unwrap_or_default(),
}
}
fn subdataset_index(key: &str) -> Option<(usize, bool)> {
let rest = key.strip_prefix("SUBDATASET_")?;
let (n, kind) = rest.split_once('_')?;
let is_name = match kind {
"NAME" => true,
"DESC" => false,
_ => return None,
};
Some((n.parse().ok()?, is_name))
}
fn subdatasets_from_metadata(metadata: &[(String, String)]) -> Vec<Subdataset> {
let mut out: Vec<Subdataset> = Vec::new();
for (k, v) in metadata {
let Some((n, is_name)) = subdataset_index(k) else { continue };
if n == 0 {
continue;
}
while out.len() < n {
out.push(Subdataset { name: String::new(), description: String::new() });
}
if is_name {
out[n - 1].name = v.clone();
} else {
out[n - 1].description = v.clone();
}
}
out.retain(|s| !s.name.is_empty());
out
}
fn histogram_context(ds: &RasterDataset, band: usize, path: Option<&str>, lazy: Option<&mut LazyRaster>) -> crate::histogram::BandContext {
let mut ctx = crate::histogram::BandContext::default();
let local = path.filter(|p| std::path::Path::new(p).is_file());
if let Some(p) = local {
ctx.stored_histogram = crate::histogram::pam_histogram(p, band + 1);
ctx.stored_range = crate::histogram::stored_range(ds, p, band + 1);
}
match lazy {
Some(l) if l.block_size().is_some() => {
ctx.block = l.block_size();
let overviews = l.overviews();
if ctx.stored_range.is_none()
&& let Some(i) = crate::histogram::sample_overview(l.width(), l.height(), &overviews)
{
let (w, h) = overviews[i];
let options = crate::WindowOptions { bands: vec![band + 1], overview: Some(i) };
ctx.overview = l.read_window(crate::Window::full(w, h), &options).ok().map(|o| crate::ops::band_f64(&o.raster, 0));
}
}
_ => ctx.block = crate::histogram::driver_block(ds),
}
ctx
}
pub fn lazy_raster_info(r: &mut LazyRaster, options: &InfoOptions) -> crate::Result<RasterInfo> {
lazy_raster_info_at(r, options, None)
}
fn lazy_raster_info_at(r: &mut LazyRaster, options: &InfoOptions, path: Option<&str>) -> crate::Result<RasterInfo> {
let overviews = r.overviews().into_iter().map(|(w, h)| [w, h]).collect();
if let Some(ds) = r.dataset() {
let mut info = raster_info_at(ds, options, path, None);
info.overviews = overviews;
return Ok(info);
}
if options.histogram {
let ds = r.read()?;
let mut info = raster_info_at(&ds, options, path, Some(r));
info.overviews = overviews;
return Ok(info);
}
if options.statistics {
let stats = lazy_band_statistics(r)?;
let header = r.header();
let mut info = raster_info_at(&header, &InfoOptions { statistics: false, ..*options }, path, None);
for (band, s) in info.bands.iter_mut().zip(stats) {
band.statistics = s;
}
info.overviews = overviews;
return Ok(info);
}
let crs = r.crs();
let gt = r.geo_transform();
let (w, h) = (r.width() as f64, r.height() as f64);
let extent = [gt[0], gt[3] + h * gt[5], gt[0] + w * gt[1], gt[3]];
let bands = (0..r.band_count())
.map(|b| BandInfo {
band: b + 1,
data_type: gdal_type_name(r.data_type()).to_string(),
nodata: r.nodata(),
statistics: None,
histogram: None,
})
.collect();
Ok(RasterInfo {
driver: r.driver().name().to_string(),
size: [r.width(), r.height()],
band_count: r.band_count(),
geo_transform: gt,
wgs84_extent: wgs84_extent(&crs, extent),
crs: CrsSummary::new(&crs),
extent,
bands,
overviews,
metadata: r.metadata(),
subdatasets: Vec::new(),
geolocation: Vec::new(),
})
}
pub fn info_path(path: &str, options: &InfoOptions) -> crate::Result<Info> {
if let Some(result) = crate::stac::info(path, options) {
return result;
}
if let Some(r) = crate::drivers::open_lazy(path) {
return Ok(Info::Raster(Box::new(lazy_raster_info(&mut r?, options)?)));
}
#[cfg(feature = "pointcloud")]
if crate::pointcloud::is_point_cloud_path(path) {
let mut r = crate::pointcloud::open_points(path)?;
return Ok(Info::PointCloud(Box::new(crate::pointcloud::info::point_cloud_info(&mut r, options.statistics)?)));
}
#[cfg(feature = "gdal")]
if crate::gdal_backend::strip_prefix(path).is_some() {
return Ok(info(&crate::open(path)?, options));
}
let loc = crate::vfs::Location::parse(path);
#[cfg(feature = "tiles")]
if !matches!(loc, crate::vfs::Location::Zip { .. }) && crate::tiles::archive_kind(&loc.file_name()).is_some() {
let archive = crate::tiles::TileArchive::open(path)?;
if archive.is_vector() {
return Ok(info(&crate::tiles::archive_dataset(archive, &loc.file_name(), &Default::default())?, options));
}
let mut r = LazyRaster::from_tiles(crate::tiles::raster::TileRaster::new(archive, None)?);
return Ok(Info::Raster(Box::new(lazy_raster_info(&mut r, options)?)));
}
if !matches!(loc, crate::vfs::Location::Zip { .. })
&& let Some(mut r) = LazyRaster::open_tiff(path)?
{
return Ok(Info::Raster(Box::new(lazy_raster_info_at(&mut r, options, Some(path))?)));
}
#[cfg(feature = "sqlite")]
if let Some(v) = gpkg_vector_info(path)? {
return Ok(Info::Vector(v));
}
if let Some(v) = osm_vector_info(path)? {
return Ok(Info::Vector(v));
}
#[cfg_attr(not(feature = "sqlite"), allow(unused_mut))]
let mut summary = match crate::open(path)? {
Dataset::Raster(r) => Info::Raster(Box::new(raster_info_at(&r, options, Some(path), None))),
Dataset::Vector(v) => Info::Vector(vector_info(&v)),
};
#[cfg(feature = "sqlite")]
if crate::formats::extension(path).as_deref() == Some("gpkg")
&& let Ok(local) = crate::vfs::resolve(path)
{
let subdatasets: Vec<Subdataset> = crate::drivers::gpkg::subdatasets(local.as_str())
.into_iter()
.map(|(name, description)| Subdataset { name, description })
.collect();
match &mut summary {
Info::Vector(v) => v.subdatasets = subdatasets,
Info::Raster(r) if subdatasets.len() > 1 => r.subdatasets = subdatasets,
#[cfg(feature = "pointcloud")]
Info::PointCloud(_) => {}
Info::Raster(_) => {}
}
}
Ok(summary)
}
pub fn band_statistics(ds: &RasterDataset, band: usize) -> Option<Statistics> {
let mut acc = StatsAcc::new(ds.nodata());
acc.add(ds.raster.band_view(band).as_slice());
acc.finish()
}
struct StatsAcc {
nodata: Option<f64>,
n: u64,
min: f64,
max: f64,
mean: f64,
m2: f64,
}
impl StatsAcc {
fn new(nodata: Option<f64>) -> Self {
Self { nodata, n: 0, min: f64::INFINITY, max: f64::NEG_INFINITY, mean: 0.0, m2: 0.0 }
}
fn add(&mut self, values: &[f64]) {
for &v in values {
if v.is_nan() || self.nodata.is_some_and(|nd| v == nd) {
continue;
}
self.n += 1;
self.min = self.min.min(v);
self.max = self.max.max(v);
let d = v - self.mean;
self.mean += d / self.n as f64;
self.m2 += d * (v - self.mean);
}
}
fn finish(&self) -> Option<Statistics> {
(self.n > 0).then(|| Statistics {
min: self.min,
max: self.max,
mean: self.mean,
std_dev: (self.m2 / self.n as f64).sqrt(),
valid_count: self.n,
})
}
}
fn lazy_band_statistics(r: &mut LazyRaster) -> crate::Result<Vec<Option<Statistics>>> {
let (w, h, bands) = (r.width(), r.height(), r.band_count());
let block = r.block_size().map_or(256, |(_, bh)| bh.max(1));
let rows = ((64 << 20) / (8 * w.max(1) * bands.max(1))).max(1).div_ceil(block) * block;
let mut acc: Vec<StatsAcc> = (0..bands).map(|_| StatsAcc::new(r.nodata())).collect();
let mut y = 0;
while y < h {
let n = rows.min(h - y);
let strip = r.read_window(crate::cog::Window::new(0, y, w, n), &crate::cog::WindowOptions::default())?;
for (b, a) in acc.iter_mut().enumerate() {
a.add(strip.raster.band_view(b).as_slice());
}
y += n;
}
Ok(acc.iter().map(StatsAcc::finish).collect())
}
pub fn gdal_type_name(t: wbraster::DataType) -> &'static str {
use wbraster::DataType as T;
match t {
T::U8 => "Byte",
T::I8 => "Int8",
T::U16 => "UInt16",
T::I16 => "Int16",
T::U32 => "UInt32",
T::I32 => "Int32",
T::U64 => "UInt64",
T::I64 => "Int64",
T::F32 => "Float32",
T::F64 => "Float64",
}
}
pub fn vector_info(ds: &VectorDataset) -> VectorInfo {
let kml = matches!(ds.driver, crate::formats::VectorDriver::KML | crate::formats::VectorDriver::KMZ);
let extent = |l: &wbvector::Layer| match (&l.extent, kml) {
(Some(b), true) => Some((b.min_x, b.min_y, b.max_x, b.max_y)),
_ => crate::vector::layer_extent(l),
};
let layers = ds.layers.iter().map(|l| layer_info(l, l.len(), crate::vector::geometry_type_name(l), extent(l))).collect();
VectorInfo { driver: ds.driver.name().to_string(), layers, subdatasets: Vec::new() }
}
fn layer_info(l: &wbvector::Layer, feature_count: usize, geometry_type: String, extent: Option<(f64, f64, f64, f64)>) -> LayerInfo {
let crs = SpatialRef::from_layer(l);
LayerInfo {
name: l.name.clone(),
geometry_type,
feature_count,
crs: CrsSummary::new(&crs),
extent: extent.map(|b| [b.0, b.1, b.2, b.3]),
fields: l
.schema
.fields()
.iter()
.map(|f| FieldInfo { name: f.name.clone(), field_type: f.field_type.as_str().to_string() })
.collect(),
}
}
#[cfg(feature = "sqlite")]
fn gpkg_vector_info(path: &str) -> crate::Result<Option<VectorInfo>> {
if crate::formats::extension(path).as_deref() != Some("gpkg")
|| !matches!(crate::vfs::Location::parse(path), crate::vfs::Location::Local(_))
|| !std::path::Path::new(path).is_file()
{
return Ok(None);
}
let Some((layers, rasters)) = crate::drivers::vector::gpkg_read::summarize(path)? else { return Ok(None) };
let layers = layers.into_iter().map(|s| layer_info(&s.layer, s.feature_count, s.geometry_type, s.extent)).collect();
let subdatasets = rasters
.into_iter()
.map(|c| {
let desc = if c.description.is_empty() { c.name.clone() } else { c.description.clone() };
Subdataset { name: format!("GPKG:{path}:{}", c.name), description: format!("{} - {desc}", c.name) }
})
.collect();
Ok(Some(VectorInfo { driver: crate::formats::VectorDriver::GPKG.name().to_string(), layers, subdatasets }))
}
#[cfg(all(feature = "osm", not(target_arch = "wasm32")))]
fn osm_vector_info(path: &str) -> crate::Result<Option<VectorInfo>> {
use crate::drivers::vector::osm;
if crate::formats::extension(path).as_deref() != Some("pbf")
|| crate::formats::VectorDriver::for_extension(path) != Some(crate::formats::VectorDriver::OSM)
|| !matches!(crate::vfs::Location::parse(path), crate::vfs::Location::Local(_))
|| !std::path::Path::new(path).is_file()
{
return Ok(None);
}
let mut file = std::fs::File::open(path)?;
if !osm::is_sorted_pbf(&mut file) {
return Ok(None);
}
let schemas = osm::layer_schemas();
#[derive(Clone, Default)]
struct Acc {
count: usize,
extent: Option<(f64, f64, f64, f64)>,
geometries: usize,
has_z: bool,
}
let mut acc = vec![Acc::default(); schemas.len()];
let read = osm::read_sorted_pbf(&mut file, [true; 5], &mut |l, features| {
let a = &mut acc[l];
a.count += features.len();
let boxes = features.iter().filter_map(|f| f.geometry.as_ref()?.bbox()).map(|b| (b.min_x, b.min_y, b.max_x, b.max_y));
a.extent = crate::vector::extent_of(a.extent.into_iter().chain(boxes));
for g in features.iter().filter_map(|f| f.geometry.as_ref()) {
if a.geometries < 100 {
a.has_z |= crate::vector::geometry_has_z(g);
}
a.geometries += 1;
}
Ok(())
});
match read {
Err(e) if osm::is_unsorted(&e) => return Ok(None),
r => r?,
};
let layers = schemas
.iter()
.zip(acc)
.map(|(l, a)| {
let types = l.geom_type.into_iter().take(usize::from(a.geometries > 0));
layer_info(l, a.count, crate::vector::geometry_type_name_of(l.geom_type, types, a.has_z), a.extent)
})
.collect();
Ok(Some(VectorInfo { driver: crate::formats::VectorDriver::OSM.name().to_string(), layers, subdatasets: Vec::new() }))
}
#[cfg(any(not(feature = "osm"), target_arch = "wasm32"))]
fn osm_vector_info(_path: &str) -> crate::Result<Option<VectorInfo>> {
Ok(None)
}
fn wgs84_extent(crs: &SpatialRef, e: [f64; 4]) -> Option<[f64; 4]> {
if crs.is_unknown() {
return None;
}
let wgs84 = SpatialRef::from_epsg(4326).ok()?;
let t = Transformer::new(crs, &wgs84).ok()?;
let b = t.transform_bounds((e[0], e[1], e[2], e[3])).ok()?;
Some([b.0, b.1, b.2, b.3])
}
impl Info {
pub fn to_text(&self) -> String {
let mut s = String::new();
match self {
Info::Raster(r) => {
let _ = writeln!(s, "Driver: {}", r.driver);
let _ = writeln!(s, "Size: {} x {}, {} band(s)", r.size[0], r.size[1], r.band_count);
let _ = writeln!(s, "CRS: {}", r.crs.line());
let g = r.geo_transform;
let _ = writeln!(s, "Origin: ({}, {})", g[0], g[3]);
let _ = writeln!(s, "Pixel size: ({}, {})", g[1], g[5]);
let _ = writeln!(s, "Extent: {}", fmt_box(&r.extent));
if let Some(w) = &r.wgs84_extent {
let _ = writeln!(s, "WGS 84 extent: {}", fmt_box(w));
}
if !r.overviews.is_empty() {
let sizes: Vec<String> = r.overviews.iter().map(|[w, h]| format!("{w}x{h}")).collect();
let _ = writeln!(s, "Overviews: {}", sizes.join(", "));
}
for (k, v) in &r.metadata {
let _ = writeln!(s, "Metadata: {k}={v}");
}
write_subdatasets(&mut s, &r.subdatasets);
if !r.geolocation.is_empty() {
let _ = writeln!(s, "Geolocation:");
for (k, v) in &r.geolocation {
let _ = writeln!(s, " {k}={v}");
}
}
for b in &r.bands {
let _ = write!(s, "Band {}: {}", b.band, b.data_type);
if let Some(nd) = b.nodata {
let _ = write!(s, ", nodata={nd}");
}
let _ = writeln!(s);
if let Some(st) = &b.statistics {
let _ = writeln!(
s,
" min={} max={} mean={:.6} std_dev={:.6} valid={}",
st.min, st.max, st.mean, st.std_dev, st.valid_count
);
}
if let Some(h) = &b.histogram {
s.push_str(&h.to_text());
}
}
}
#[cfg(feature = "pointcloud")]
Info::PointCloud(p) => s.push_str(&p.to_text()),
Info::Vector(v) => {
let _ = writeln!(s, "Driver: {}", v.driver);
for l in &v.layers {
let _ = writeln!(s, "\nLayer: {}", l.name);
let _ = writeln!(s, "Geometry: {}", l.geometry_type);
let _ = writeln!(s, "Features: {}", l.feature_count);
let _ = writeln!(s, "CRS: {}", l.crs.line());
if let Some(e) = &l.extent {
let _ = writeln!(s, "Extent: {}", fmt_box(e));
}
for f in &l.fields {
let _ = writeln!(s, " {}: {}", f.name, f.field_type);
}
}
if !v.subdatasets.is_empty() {
let _ = writeln!(s);
}
write_subdatasets(&mut s, &v.subdatasets);
}
}
s
}
}
fn write_subdatasets(s: &mut String, subdatasets: &[Subdataset]) {
if subdatasets.is_empty() {
return;
}
let _ = writeln!(s, "Subdatasets:");
for (i, d) in subdatasets.iter().enumerate() {
let _ = writeln!(s, " SUBDATASET_{}_NAME={}", i + 1, d.name);
let _ = writeln!(s, " SUBDATASET_{}_DESC={}", i + 1, d.description);
}
}
fn fmt_box(b: &[f64; 4]) -> String {
format!("({}, {}) - ({}, {})", b[0], b[1], b[2], b[3])
}