use crate::crs::SpatialRef;
use crate::formats::{RasterDriver, VectorDriver, extension};
use crate::vfs;
use crate::{Error, Result};
use std::path::Path;
use wbraster::Raster;
use wbvector::Layer;
#[derive(Debug, Clone)]
pub struct RasterDataset {
pub raster: Raster,
pub driver: RasterDriver,
pub has_nodata: bool,
pub affine: Option<[f64; 6]>,
}
impl RasterDataset {
pub fn new(raster: Raster, has_nodata: bool) -> Self {
Self { raster, driver: RasterDriver::GTiff, has_nodata, affine: None }
}
fn with_geo_transform(mut self, gt: [f64; 6]) -> Raster {
self.set_geo_transform(gt);
self.raster
}
pub fn set_geo_transform(&mut self, gt: [f64; 6]) {
let r = &mut self.raster;
if is_north_up(>) {
r.x_min = gt[0];
r.cell_size_x = gt[1];
r.cell_size_y = -gt[5];
r.y_min = gt[3] + gt[5] * r.rows as f64;
self.affine = None;
return;
}
let (w, h) = (r.cols as f64, r.rows as f64);
let corners = [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)].map(|(c, l)| (gt[0] + c * gt[1] + l * gt[2], gt[3] + c * gt[4] + l * gt[5]));
let min_x = corners.iter().map(|p| p.0).fold(f64::INFINITY, f64::min);
let max_x = corners.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max);
let min_y = corners.iter().map(|p| p.1).fold(f64::INFINITY, f64::min);
let max_y = corners.iter().map(|p| p.1).fold(f64::NEG_INFINITY, f64::max);
r.x_min = min_x;
r.y_min = min_y;
r.cell_size_x = if r.cols > 0 { (max_x - min_x) / w } else { gt[1].hypot(gt[4]) };
r.cell_size_y = if r.rows > 0 { (max_y - min_y) / h } else { gt[2].hypot(gt[5]) };
self.affine = Some(gt);
}
pub fn crs(&self) -> SpatialRef {
SpatialRef::from_raster_crs(&self.raster.crs)
}
pub fn geo_transform(&self) -> [f64; 6] {
self.affine.unwrap_or_else(|| geo_transform(&self.raster))
}
pub fn nodata(&self) -> Option<f64> {
self.has_nodata.then_some(self.raster.nodata)
}
}
pub fn is_north_up(gt: &[f64; 6]) -> bool {
gt[2] == 0.0 && gt[4] == 0.0 && gt[1] > 0.0 && gt[5] < 0.0
}
pub fn geo_transform(r: &Raster) -> [f64; 6] {
[r.x_min, r.cell_size_x, 0.0, r.y_max(), 0.0, -r.cell_size_y]
}
#[derive(Debug, Clone)]
pub struct VectorDataset {
pub layers: Vec<Layer>,
pub driver: VectorDriver,
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum Dataset {
Raster(RasterDataset),
Vector(VectorDataset),
}
#[derive(Debug, Clone, Default)]
pub struct VectorOpenOptions {
pub layers: Vec<String>,
pub csv: crate::drivers::vector::csv::CsvOptions,
pub spatial_filter: Option<(f64, f64, f64, f64)>,
pub tiles: crate::tiles::TileOpenOptions,
}
pub fn open(path: &str) -> Result<Dataset> {
#[cfg(feature = "gdal")]
if let Some(rest) = crate::gdal_backend::strip_prefix(path) {
return crate::gdal_backend::open(rest);
}
let result = open_native(path);
#[cfg(feature = "gdal")]
let result = gdal_fallback(path, result, crate::gdal_backend::open);
result
}
fn open_native(path: &str) -> Result<Dataset> {
if let Some(result) = crate::stac::open(path) {
return result;
}
if let Some(result) = crate::drivers::open_subdataset(path) {
return result.map(Dataset::Raster);
}
if is_tile_archive(path) {
return crate::tiles::open_dataset(path, &VectorOpenOptions::default());
}
let local = vfs::resolve(path)?;
open_local(local.as_str(), &VectorOpenOptions::default())
}
pub fn open_raster(path: &str) -> Result<RasterDataset> {
#[cfg(feature = "gdal")]
if let Some(rest) = crate::gdal_backend::strip_prefix(path) {
return crate::gdal_backend::open_raster(rest);
}
let result = open_raster_native(path);
#[cfg(feature = "gdal")]
let result = gdal_fallback(path, result, crate::gdal_backend::open_raster);
result
}
fn open_raster_native(path: &str) -> Result<RasterDataset> {
if let Some(result) = crate::stac::open(path) {
return match result? {
Dataset::Raster(r) => Ok(r),
Dataset::Vector(_) => Err(Error::Unsupported(format!("'{path}' is a vector, not a raster dataset"))),
};
}
if let Some(result) = crate::drivers::open_subdataset(path) {
return result;
}
if is_tile_archive(path) {
return match crate::tiles::open_dataset(path, &VectorOpenOptions::default())? {
Dataset::Raster(r) => Ok(r),
Dataset::Vector(_) => Err(Error::Unsupported(format!("'{path}' holds vector tiles, not a raster"))),
};
}
let local = vfs::resolve(path)?;
read_raster_local(local.as_str())
}
pub fn open_vector(path: &str, options: &VectorOpenOptions) -> Result<VectorDataset> {
#[cfg(feature = "gdal")]
if let Some(rest) = crate::gdal_backend::strip_prefix(path) {
return crate::gdal_backend::open_vector(rest, options);
}
let result = open_vector_native(path, options);
#[cfg(feature = "gdal")]
let result = gdal_fallback(path, result, |p| crate::gdal_backend::open_vector(p, options));
result
}
fn open_vector_native(path: &str, options: &VectorOpenOptions) -> Result<VectorDataset> {
if let Some(result) = crate::stac::open(path) {
return match result? {
Dataset::Vector(v) => Ok(v),
Dataset::Raster(_) => Err(Error::Unsupported(format!("'{path}' is a raster, not a vector dataset"))),
};
}
if is_tile_archive(path) {
return match crate::tiles::open_dataset(path, options)? {
Dataset::Vector(v) => Ok(v),
Dataset::Raster(_) => Err(Error::Unsupported(format!("'{path}' holds raster tiles, not vector tiles"))),
};
}
let local = vfs::resolve(path)?;
match open_local(local.as_str(), options)? {
Dataset::Vector(v) => Ok(v),
Dataset::Raster(_) => Err(Error::Unsupported(format!("'{path}' is a raster, not a vector dataset"))),
}
}
pub fn list_layers(path: &str) -> Result<Vec<String>> {
#[cfg(feature = "gdal")]
if let Some(rest) = crate::gdal_backend::strip_prefix(path) {
return crate::gdal_backend::list_layers(rest);
}
let result = list_layers_native(path);
#[cfg(feature = "gdal")]
let result = gdal_fallback(path, result, crate::gdal_backend::list_layers);
result
}
fn list_layers_native(path: &str) -> Result<Vec<String>> {
if is_tile_archive(path) {
return crate::tiles::layer_names(path);
}
let local = vfs::resolve(path)?;
if VectorDriver::for_extension(local.as_str()) == Some(VectorDriver::GPKG) {
return crate::drivers::vector::gpkg_read::list_layers(local.as_str());
}
Ok(open_vector_native(local.as_str(), &VectorOpenOptions::default())?.layers.into_iter().map(|l| l.name).collect())
}
#[cfg(feature = "gdal")]
fn gdal_fallback<T>(path: &str, result: Result<T>, gdal: impl FnOnce(&str) -> Result<T>) -> Result<T> {
let Err(e) = result else { return result };
let retry = match &e {
Error::InvalidArgument(_) | Error::Http(_) => false,
Error::Io(io) if io.kind() == std::io::ErrorKind::NotFound => path.contains(':') || path.starts_with("/vsi"),
_ => true,
};
if !retry {
return Err(e);
}
gdal(path).map_err(|_| e)
}
fn is_tile_archive(path: &str) -> bool {
let loc = vfs::Location::parse(path);
!matches!(loc, vfs::Location::Zip { .. }) && crate::tiles::archive_kind(&loc.file_name()).is_some()
}
fn open_local(path: &str, options: &VectorOpenOptions) -> Result<Dataset> {
if crate::tiles::archive_kind(path).is_some() {
return crate::tiles::open_dataset(path, options);
}
if let Some(layers) = crate::tiles::open_mvt_local(path, options) {
return layers.map(|l| Dataset::Vector(finish_vector(l, path, VectorDriver::MVT)));
}
if !Path::new(path).exists() {
return Err(Error::Io(std::io::Error::new(std::io::ErrorKind::NotFound, format!("'{path}' does not exist"))));
}
let ext = extension(path).unwrap_or_default();
if ext == "gpkg" {
return crate::drivers::gpkg::open(path, options);
}
if let Some(driver) = VectorDriver::for_extension(path) {
return read_vector_local(path, driver, options).map(Dataset::Vector);
}
if RasterDriver::for_extension(path).is_some() || Path::new(path).is_dir() {
return read_raster_local(path).map(Dataset::Raster);
}
let raster = read_raster_local(path);
#[cfg(feature = "gdal")]
if raster.is_err()
&& let Ok(ds) = crate::gdal_backend::open_with(path, options)
{
return Ok(ds);
}
match raster {
Ok(r) => Ok(Dataset::Raster(r)),
Err(raster_err) => match wbvector::VectorFormat::detect(path) {
Ok(_) => {
let layer = wbvector::read(path)?;
Ok(Dataset::Vector(VectorDataset {
layers: vec![fix_layer(layer, path, VectorDriver::GeoJSON)],
driver: VectorDriver::GeoJSON,
}))
}
Err(_) => Err(Error::Unsupported(format!("'{path}': format not recognized ({raster_err})"))),
},
}
}
fn read_raster_local(path: &str) -> Result<RasterDataset> {
if RasterDriver::for_extension(path) == Some(RasterDriver::VRT) {
return crate::ops::vrt::read_vrt(path);
}
if let Some(result) = crate::drivers::read(path) {
return result;
}
if RasterDriver::for_extension(path) == Some(RasterDriver::GTiff) {
return crate::geotiff::read_geotiff(Path::new(path));
}
let (raster, driver) = match RasterDriver::for_extension(path) {
Some(driver) => (Raster::read_with_format(path, driver.to_wb())?, driver),
None => {
let fmt = wbraster::RasterFormat::detect(path)?;
(fmt.read(path)?, RasterDriver::from_wb(fmt))
}
};
let mut ds = crate::drivers::nodata::finish(path, driver, raster);
crate::drivers::nodata::apply_pam(path, &mut ds);
Ok(ds)
}
fn read_vector_local(path: &str, driver: VectorDriver, options: &VectorOpenOptions) -> Result<VectorDataset> {
let layers = if driver == VectorDriver::GPKG {
crate::drivers::vector::gpkg_read::read(path, &options.layers, options.spatial_filter)?
} else if let Some(layers) = crate::drivers::vector::read(path, driver, options)? {
select_layers(layers, path, options)?
} else {
let format = driver.to_wb().ok_or_else(|| Error::Unsupported(format!("the {} driver cannot read", driver.name())))?;
let layer = format.read(path)?;
if !options.layers.is_empty() && !options.layers.contains(&layer.name) {
let stem = Path::new(path).file_stem().and_then(|s| s.to_str()).unwrap_or_default();
if !options.layers.iter().any(|l| l == stem) {
return Err(Error::InvalidArgument(format!("no layer named {}", options.layers.join(", "))));
}
}
vec![layer]
};
Ok(finish_vector(layers, path, driver))
}
pub(crate) fn finish_vector(layers: Vec<Layer>, path: &str, driver: VectorDriver) -> VectorDataset {
let layers = layers.into_iter().map(|l| fix_layer(l, path, driver)).collect();
VectorDataset { layers, driver }
}
fn select_layers(layers: Vec<Layer>, path: &str, options: &VectorOpenOptions) -> Result<Vec<Layer>> {
if options.layers.is_empty() {
return Ok(layers);
}
let stem = Path::new(path).file_stem().and_then(|s| s.to_str()).unwrap_or_default();
let single = layers.len() == 1;
let names: Vec<String> = layers.iter().map(|l| l.name.clone()).collect();
let kept: Vec<Layer> =
layers.into_iter().filter(|l| options.layers.contains(&l.name) || single && options.layers.iter().any(|n| n == stem)).collect();
if kept.is_empty() {
return Err(Error::InvalidArgument(format!("no layer named {} (layers: {})", options.layers.join(", "), names.join(", "))));
}
Ok(kept)
}
pub(crate) fn fix_layer(mut layer: Layer, path: &str, driver: VectorDriver) -> Layer {
let stem = Path::new(path).file_stem().and_then(|s| s.to_str()).unwrap_or("layer");
if layer.name.is_empty() || (layer.name == "layer" && driver != VectorDriver::GPKG) {
layer.name = stem.to_string();
}
let registry = layer.crs_epsg().and_then(|c| wbprojection::to_ogc_wkt(c).ok());
if let Some(wkt) = layer.crs_wkt().map(str::to_string)
&& registry.as_deref() != Some(wkt.as_str())
&& let Some(code) = crate::crs::identify_epsg(&wkt)
{
layer.set_crs_epsg(Some(code));
}
let wgs84_by_definition =
matches!(driver, VectorDriver::GeoJSON | VectorDriver::TopoJSON | VectorDriver::KML | VectorDriver::KMZ | VectorDriver::GPX);
if wgs84_by_definition && layer.crs_epsg().is_none() && layer.crs_wkt().is_none() {
layer.set_crs_epsg(Some(4326));
}
layer
}
#[derive(Debug, Clone, Default)]
pub struct RasterWriteOptions {
pub driver: Option<RasterDriver>,
pub compression: Option<String>,
pub tile_size: Option<u32>,
pub bigtiff: bool,
pub creation_options: Vec<(String, String)>,
pub band_metadata: Vec<BandMetadata>,
pub color_table: Vec<[u8; 4]>,
pub overviews: Vec<usize>,
pub overview_resampling: Option<String>,
pub geo_transform: Option<[f64; 6]>,
pub gcps: Vec<Gcp>,
pub mask: Option<Vec<u8>>,
pub rpc: Vec<f64>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Gcp {
pub id: String,
pub info: String,
pub pixel: f64,
pub line: f64,
pub x: f64,
pub y: f64,
pub z: f64,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct BandMetadata {
pub description: Option<String>,
pub scale: Option<f64>,
pub offset: Option<f64>,
pub unit: Option<String>,
pub color_interp: Option<String>,
pub items: Vec<(String, String)>,
}
pub fn read_gcps(path: &str) -> Result<Vec<Gcp>> {
let local = vfs::resolve(path)?;
Ok(crate::geotiff::read_geo_tags(local.path()).map(|t| t.gcps).unwrap_or_default())
}
pub fn write_raster(raster: &Raster, path: &str, options: &RasterWriteOptions) -> Result<()> {
write_raster_with_nodata(raster, Some(raster.nodata), path, options)
}
pub fn write_raster_dataset(ds: &RasterDataset, path: &str, options: &RasterWriteOptions) -> Result<()> {
if let Some(gt) = ds.affine
&& options.geo_transform.is_none()
{
let options = RasterWriteOptions { geo_transform: Some(gt), ..options.clone() };
return write_raster_with_nodata(&ds.raster, ds.nodata(), path, &options);
}
write_raster_with_nodata(&ds.raster, ds.nodata(), path, options)
}
fn write_raster_with_nodata(raster: &Raster, nodata: Option<f64>, path: &str, options: &RasterWriteOptions) -> Result<()> {
ensure_writable_location(path)?;
let driver = match options.driver {
Some(d) => d,
None => RasterDriver::for_extension(path)
.ok_or_else(|| Error::InvalidArgument(format!("cannot tell the output format of '{path}': give it explicitly")))?,
};
if !driver.info().write {
return Err(Error::Unsupported(format!("the {} driver cannot write", driver.name())));
}
let tiff = matches!(driver, RasterDriver::GTiff | RasterDriver::Cog);
if tiff {
if let Some(opts) = crate::drivers::gtiff_write::TiffOptions::from_write_options(options, driver == RasterDriver::Cog, nodata)? {
return crate::drivers::gtiff_write::write(raster, path, &opts);
}
}
if options.geo_transform.is_some_and(|gt| !is_north_up(>))
|| !options.gcps.is_empty()
|| options.mask.is_some()
|| !options.rpc.is_empty()
{
return Err(Error::Unsupported(format!(
"the {} writer cannot store rotated or south-up geotransforms, GCPs, masks or RPCs (GeoTIFF can, with a lossless compression)",
driver.name()
)));
}
let north_up;
let raster = match options.geo_transform {
Some(gt) if gt != geo_transform(raster) => {
north_up = RasterDataset { raster: raster.clone(), driver, has_nodata: false, affine: None }.with_geo_transform(gt);
&north_up
}
_ => raster,
};
if let Some(result) = crate::drivers::write(raster, nodata, path, driver, options) {
return result;
}
#[cfg(feature = "gdal")]
if driver == RasterDriver::HFA {
let mut co = options.creation_options.clone();
if let Some(c) = &options.compression {
co.push(("COMPRESSED".into(), if c.eq_ignore_ascii_case("NONE") { "NO" } else { "YES" }.into()));
}
return crate::gdal_backend::write_raster_with(raster, nodata, path, "HFA", &co);
}
let compression = options.compression.as_deref().map(parse_compression).transpose()?;
match driver {
RasterDriver::Cog => {
let opts = wbraster::CogWriteOptions { compression, bigtiff: Some(options.bigtiff), tile_size: options.tile_size };
raster.write_cog_with_options(path, &opts)?;
}
RasterDriver::GTiff => {
let layout = options.tile_size.map(|t| wbraster::GeoTiffLayout::Tiled { tile_width: t, tile_height: t });
let opts = wbraster::GeoTiffWriteOptions { compression, bigtiff: Some(options.bigtiff), layout };
raster.write_geotiff_with_options(path, &opts)?;
}
RasterDriver::VRT => {
return Err(Error::Unsupported("a VRT references other files: build one with `terrio mosaic` (ops::mosaic::build_vrt)".into()));
}
d => raster.write(path, d.to_wb())?,
}
Ok(())
}
fn parse_compression(s: &str) -> Result<wbraster::GeoTiffCompression> {
use wbraster::GeoTiffCompression as C;
Ok(match s.to_ascii_uppercase().as_str() {
"JPEG" => C::Jpeg,
"WEBP" => C::WebP,
other => {
return Err(Error::InvalidArgument(format!("unknown compression '{other}' (NONE, LZW, DEFLATE, ZSTD, PACKBITS, JPEG, WEBP)")));
}
})
}
#[derive(Debug, Clone, Default)]
pub struct VectorWriteOptions {
pub driver: Option<VectorDriver>,
pub csv: crate::drivers::vector::csv::CsvOptions,
pub geojson: crate::drivers::vector::geojson::GeoJsonOptions,
pub tiles: crate::tiles::TileWriteOptions,
}
pub fn write_vector(layers: &[Layer], path: &str, options: &VectorWriteOptions) -> Result<()> {
ensure_writable_location(path)?;
let driver = match options.driver {
Some(d) => d,
None => VectorDriver::for_extension(path)
.ok_or_else(|| Error::InvalidArgument(format!("cannot tell the output format of '{path}': give it explicitly")))?,
};
if !driver.info().write {
return Err(Error::Unsupported(format!("the {} driver cannot write", driver.name())));
}
match (driver, layers) {
(_, []) => Err(Error::InvalidArgument("no layers to write".into())),
(VectorDriver::GPKG, _) => crate::drivers::vector::geopackage::write(layers, path),
(VectorDriver::GPX, _) => crate::drivers::vector::gpx::write_layers(layers, path),
(VectorDriver::KML, _) => crate::drivers::vector::kml::write(layers, path),
(VectorDriver::KMZ, _) => crate::drivers::vector::kml::write_kmz(layers, path),
(VectorDriver::TopoJSON, _) => crate::drivers::vector::topojson::write_layers(layers, path),
(VectorDriver::MBTiles | VectorDriver::PMTiles | VectorDriver::MVT, _) => {
crate::tiles::write_vector(layers, path, driver, &options.tiles)
}
(d, [layer]) => {
if !crate::drivers::vector::write(layer, path, d, options)? {
let format = d.to_wb().ok_or_else(|| Error::Unsupported(format!("the {} driver cannot write", d.name())))?;
wbvector::write(layer, path, format)?;
}
Ok(())
}
(d, _) => Err(Error::InvalidArgument(format!("the {} format holds one layer: choose a layer, or write a GeoPackage", d.name()))),
}
}
fn ensure_writable_location(path: &str) -> Result<()> {
if !vfs::Location::parse(path).is_local() {
return Err(Error::Unsupported(format!("cannot write to '{path}': only local outputs are supported")));
}
Ok(())
}