use crate::crs::{SpatialRef, Transformer};
use crate::{Error, FieldDef, FieldType, FieldValue, Geometry, Layer, RasterDataset, Result};
pub struct SampleInput<'a> {
pub field_name: String,
pub raster: &'a RasterDataset,
pub band: usize,
}
pub fn sample(points: &Layer, inputs: &[SampleInput<'_>]) -> Result<Layer> {
if inputs.is_empty() {
return Err(Error::InvalidArgument("sampling needs at least one raster band".into()));
}
for f in &points.features {
match &f.geometry {
Some(Geometry::Point(c)) if c.x.is_finite() && c.y.is_finite() => {}
None => {}
Some(Geometry::Point(_)) => return Err(Error::InvalidArgument(format!("feature {} has non-finite XY coordinates", f.fid))),
_ => {
return Err(Error::InvalidArgument(format!(
"feature {} is not a point; split multipart points with singlepart first",
f.fid
)));
}
}
}
let src = SpatialRef::from_layer(points);
let mut out = points.clone();
for input in inputs {
let ds = input.raster;
check_raster(ds)?;
check_band(ds, input.band)?;
if input.field_name.trim().is_empty() {
return Err(Error::InvalidArgument("sample field names cannot be empty".into()));
}
let dst = ds.crs();
let ct = match (src.is_unknown(), dst.is_unknown()) {
(true, true) => None,
(true, false) | (false, true) => {
return Err(Error::Crs("points and raster must both declare a CRS, or both have no CRS".into()));
}
_ if src.same_as(&dst) => None,
_ => Some(Transformer::new(&src, &dst)?),
};
let gt = ds.geo_transform();
let det = gt[1] * gt[5] - gt[2] * gt[4];
let name = super::geometry::unique_field(&out, &input.field_name);
let index = out.schema.len();
out.schema.add_field(FieldDef::new(name.clone(), FieldType::Float));
let mut projection_failures = 0usize;
for f in &mut out.features {
let value = match &f.geometry {
Some(Geometry::Point(c)) => {
let coords = match &ct {
Some(t) => t.transform(c.x, c.y),
None => Ok((c.x, c.y)),
};
let (x, y) = match coords {
Ok(xy) => xy,
Err(_) => {
projection_failures += 1;
f.set_by_index(index, FieldValue::Null);
continue;
}
};
let (dx, dy) = (x - gt[0], y - gt[3]);
let col = ((gt[5] * dx - gt[2] * dy) / det).floor();
let row = ((gt[1] * dy - gt[4] * dx) / det).floor();
if col >= 0.0 && row >= 0.0 && col < ds.raster.cols as f64 && row < ds.raster.rows as f64 {
let v = ds
.raster
.data
.get_f64((input.band - 1) * ds.raster.rows * ds.raster.cols + row as usize * ds.raster.cols + col as usize);
if v.is_finite() && crate::histogram::is_valid(v, ds.nodata(), ds.raster.data_type) {
FieldValue::Float(v)
} else {
FieldValue::Null
}
} else {
FieldValue::Null
}
}
_ => FieldValue::Null,
};
f.attributes.resize(index, FieldValue::Null);
f.attributes.push(value);
}
if projection_failures > 0 {
let point_label = if projection_failures == 1 { "point" } else { "points" };
crate::warnings::warn(format!(
"sample field '{name}' in layer '{}': could not transform {projection_failures} {point_label} into the raster CRS; samples set to null",
points.name
));
}
}
Ok(out)
}
pub(crate) fn check_band(ds: &RasterDataset, band: usize) -> Result<()> {
if band == 0 || band > ds.band_count() {
return Err(Error::InvalidArgument(format!(
"band {band} does not exist (bands are numbered from 1; the raster has {})",
ds.band_count()
)));
}
Ok(())
}
pub(crate) fn check_raster(ds: &RasterDataset) -> Result<()> {
if ds.complex || ds.geolocation.is_some() {
return Err(Error::Unsupported("this operation needs real-valued raster bands with affine georeferencing".into()));
}
let gt = ds.geo_transform();
let det = gt[1] * gt[5] - gt[2] * gt[4];
if gt.iter().any(|v| !v.is_finite()) || det == 0.0 || !det.is_finite() {
return Err(Error::InvalidArgument("the raster geotransform must be finite and invertible".into()));
}
if ds.raster.cols == 0 || ds.raster.rows == 0 || ds.band_count() == 0 {
return Err(Error::InvalidArgument("the raster must have pixels and at least one band".into()));
}
Ok(())
}