use geo::BoundingRect;
use geo::Contains;
use geo_types::{Geometry, Point, Polygon};
use crate::error::{Error, Result};
use crate::raster::{GeoTransform, Raster};
use super::{AttributeValue, FeatureCollection};
pub fn rasterize_polygons(
features: &FeatureCollection,
transform: &GeoTransform,
rows: usize,
cols: usize,
attribute: Option<&str>,
) -> Result<Raster<f64>> {
let mut output = Raster::filled(rows, cols, f64::NAN);
output.set_transform(*transform);
for (idx, feature) in features.iter().enumerate() {
let value = match attribute {
Some(attr_name) => match feature.get_property(attr_name) {
Some(AttributeValue::Int(v)) => *v as f64,
Some(AttributeValue::Float(v)) => *v,
Some(_) => {
return Err(Error::Other(format!(
"Property '{attr_name}' is not numeric for feature {idx}"
)));
}
None => {
return Err(Error::Other(format!(
"Property '{attr_name}' not found in feature {idx}"
)));
}
},
None => (idx + 1) as f64,
};
let geom = match &feature.geometry {
Some(g) => g,
None => continue,
};
match geom {
Geometry::Polygon(poly) => {
rasterize_single_polygon(poly, transform, rows, cols, value, &mut output);
}
Geometry::MultiPolygon(mp) => {
for poly in &mp.0 {
rasterize_single_polygon(poly, transform, rows, cols, value, &mut output);
}
}
_ => {
}
}
}
Ok(output)
}
fn rasterize_single_polygon(
polygon: &Polygon<f64>,
transform: &GeoTransform,
rows: usize,
cols: usize,
value: f64,
output: &mut Raster<f64>,
) {
let bbox = match polygon.bounding_rect() {
Some(r) => r,
None => return,
};
let (min_col_f, min_row_f) = transform.geo_to_pixel(bbox.min().x, bbox.max().y);
let (max_col_f, max_row_f) = transform.geo_to_pixel(bbox.max().x, bbox.min().y);
let r_start = (min_row_f.floor().max(0.0) as usize).min(rows);
let r_end = ((max_row_f.ceil() as usize) + 1).min(rows);
let c_start = (min_col_f.floor().max(0.0) as usize).min(cols);
let c_end = ((max_col_f.ceil() as usize) + 1).min(cols);
for row in r_start..r_end {
for col in c_start..c_end {
let (x, y) = transform.pixel_to_geo(col, row);
let pt = Point::new(x, y);
if polygon.contains(&pt) {
let _ = output.set(row, col, value);
}
}
}
}
pub fn clip_raster_by_polygon(raster: &Raster<f64>, polygon: &Polygon<f64>) -> Result<Raster<f64>> {
let (rows, cols) = raster.shape();
let transform = raster.transform();
let mut output = raster.clone();
let bbox = polygon.bounding_rect();
for row in 0..rows {
for col in 0..cols {
let (x, y) = transform.pixel_to_geo(col, row);
let pt = Point::new(x, y);
if let Some(ref rect) = bbox {
if x < rect.min().x || x > rect.max().x || y < rect.min().y || y > rect.max().y {
let _ = output.set(row, col, f64::NAN);
continue;
}
}
if !polygon.contains(&pt) {
let _ = output.set(row, col, f64::NAN);
}
}
}
Ok(output)
}
pub fn clip_raster(raster: &Raster<f64>, features: &FeatureCollection) -> Result<Raster<f64>> {
if features.is_empty() {
return Err(Error::Other(
"FeatureCollection is empty, nothing to clip by".into(),
));
}
let mut polygons: Vec<&Polygon<f64>> = Vec::new();
for feature in features.iter() {
match &feature.geometry {
Some(Geometry::Polygon(p)) => polygons.push(p),
Some(Geometry::MultiPolygon(mp)) => {
for p in &mp.0 {
polygons.push(p);
}
}
_ => {}
}
}
if polygons.is_empty() {
return Err(Error::Other(
"No polygon geometries found in FeatureCollection".into(),
));
}
let mut combined_min_x = f64::INFINITY;
let mut combined_min_y = f64::INFINITY;
let mut combined_max_x = f64::NEG_INFINITY;
let mut combined_max_y = f64::NEG_INFINITY;
for poly in &polygons {
if let Some(rect) = poly.bounding_rect() {
combined_min_x = combined_min_x.min(rect.min().x);
combined_min_y = combined_min_y.min(rect.min().y);
combined_max_x = combined_max_x.max(rect.max().x);
combined_max_y = combined_max_y.max(rect.max().y);
}
}
let (rows, cols) = raster.shape();
let transform = raster.transform();
let mut output = raster.clone();
for row in 0..rows {
for col in 0..cols {
let (x, y) = transform.pixel_to_geo(col, row);
if x < combined_min_x || x > combined_max_x || y < combined_min_y || y > combined_max_y
{
let _ = output.set(row, col, f64::NAN);
continue;
}
let pt = Point::new(x, y);
let inside = polygons.iter().any(|poly| poly.contains(&pt));
if !inside {
let _ = output.set(row, col, f64::NAN);
}
}
}
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vector::Feature;
use geo_types::Coord;
fn rect_polygon(x_min: f64, y_min: f64, x_max: f64, y_max: f64) -> Polygon<f64> {
Polygon::new(
geo_types::LineString::new(vec![
Coord { x: x_min, y: y_min },
Coord { x: x_max, y: y_min },
Coord { x: x_max, y: y_max },
Coord { x: x_min, y: y_max },
Coord { x: x_min, y: y_min },
]),
vec![],
)
}
#[test]
fn test_clip_raster_by_polygon() {
let mut raster = Raster::filled(10, 10, 1.0_f64);
raster.set_transform(GeoTransform::new(0.0, 10.0, 1.0, -1.0));
let poly = rect_polygon(2.0, 3.0, 8.0, 7.0);
let clipped = clip_raster_by_polygon(&raster, &poly).unwrap();
let (rows, cols) = clipped.shape();
let mut valid_count = 0;
for r in 0..rows {
for c in 0..cols {
let v = clipped.get(r, c).unwrap();
if v.is_finite() {
valid_count += 1;
}
}
}
assert!(valid_count > 0, "Some cells must survive clipping");
assert!(valid_count < 100, "Not all cells should survive");
assert_eq!(valid_count, 24);
}
#[test]
fn test_rasterize_single_polygon() {
let transform = GeoTransform::new(0.0, 10.0, 1.0, -1.0);
let poly = rect_polygon(2.0, 3.0, 8.0, 7.0);
let mut fc = FeatureCollection::new();
fc.push(Feature::new(Geometry::Polygon(poly)));
let result = rasterize_polygons(&fc, &transform, 10, 10, None).unwrap();
let mut count_ones = 0;
let mut count_nan = 0;
for r in 0..10 {
for c in 0..10 {
let v = result.get(r, c).unwrap();
if v == 1.0 {
count_ones += 1;
} else if v.is_nan() {
count_nan += 1;
}
}
}
assert_eq!(count_ones, 24);
assert_eq!(count_nan, 76);
}
#[test]
fn test_rasterize_with_attribute() {
let transform = GeoTransform::new(0.0, 5.0, 1.0, -1.0);
let poly = rect_polygon(1.0, 1.0, 4.0, 4.0);
let mut feat = Feature::new(Geometry::Polygon(poly));
feat.set_property("class", AttributeValue::Float(42.0));
let mut fc = FeatureCollection::new();
fc.push(feat);
let result = rasterize_polygons(&fc, &transform, 5, 5, Some("class")).unwrap();
let mut found_42 = false;
for r in 0..5 {
for c in 0..5 {
let v = result.get(r, c).unwrap();
if v == 42.0 {
found_42 = true;
}
}
}
assert!(found_42);
}
#[test]
fn test_clip_raster_union() {
let mut raster = Raster::filled(10, 10, 5.0_f64);
raster.set_transform(GeoTransform::new(0.0, 10.0, 1.0, -1.0));
let poly1 = rect_polygon(0.0, 8.0, 3.0, 10.0); let poly2 = rect_polygon(7.0, 0.0, 10.0, 3.0);
let mut fc = FeatureCollection::new();
fc.push(Feature::new(Geometry::Polygon(poly1)));
fc.push(Feature::new(Geometry::Polygon(poly2)));
let clipped = clip_raster(&raster, &fc).unwrap();
let mut valid = 0;
for r in 0..10 {
for c in 0..10 {
if clipped.get(r, c).unwrap().is_finite() {
valid += 1;
}
}
}
assert!(valid > 0);
assert!(valid < 100);
}
#[test]
fn test_empty_feature_collection_errors() {
let raster = Raster::filled(5, 5, 1.0_f64);
let fc = FeatureCollection::new();
assert!(clip_raster(&raster, &fc).is_err());
}
}