use crate::result::{Error, Result};
use nexrad_model::data::{GateStatus, SweepField, VerticalField};
const EFFECTIVE_EARTH_RADIUS_KM: f64 = 6371.0 * 4.0 / 3.0;
pub struct VerticalCrossSection {
azimuth_degrees: f32,
max_range_km: f64,
max_altitude_m: f64,
width: usize,
height: usize,
}
impl VerticalCrossSection {
pub fn new(
azimuth_degrees: f32,
max_range_km: f64,
max_altitude_m: f64,
width: usize,
height: usize,
) -> Result<Self> {
if max_range_km <= 0.0 {
return Err(Error::InvalidParameter(
"max_range_km must be positive".to_string(),
));
}
if max_altitude_m <= 0.0 {
return Err(Error::InvalidParameter(
"max_altitude_m must be positive".to_string(),
));
}
if width == 0 || height == 0 {
return Err(Error::InvalidParameter(
"output dimensions must be > 0".to_string(),
));
}
Ok(Self {
azimuth_degrees,
max_range_km,
max_altitude_m,
width,
height,
})
}
pub fn compute(&self, fields: &[SweepField]) -> Result<VerticalField> {
if fields.is_empty() {
return Err(Error::MissingData("no sweep fields provided".to_string()));
}
let label = format!("{} RHI", fields[0].label());
let unit = fields[0].unit().to_string();
let mut output = VerticalField::new(
label,
unit,
(0.0, self.max_range_km),
(0.0, self.max_altitude_m),
self.width,
self.height,
);
let re = EFFECTIVE_EARTH_RADIUS_KM;
for field in fields {
let elev_rad = (field.elevation_degrees() as f64).to_radians();
for col in 0..self.width {
let range_km = (col as f64 + 0.5) / self.width as f64 * self.max_range_km;
let altitude_km = range_km * elev_rad.sin() + (range_km * range_km) / (2.0 * re);
let altitude_m = altitude_km * 1000.0;
if altitude_m < 0.0 || altitude_m > self.max_altitude_m {
continue;
}
let row = ((self.max_altitude_m - altitude_m) / self.max_altitude_m
* self.height as f64) as usize;
if row >= self.height {
continue;
}
if let Some((val, status)) = field.value_at_polar(self.azimuth_degrees, range_km) {
if status == GateStatus::Valid {
let (existing_val, existing_status) = output.get(row, col);
if existing_status != GateStatus::Valid || val > existing_val {
output.set(row, col, val, GateStatus::Valid);
}
}
}
}
}
Ok(output)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_uniform_field(elevation: f32, value: f32) -> SweepField {
let mut azimuths = Vec::new();
for i in 0..360 {
azimuths.push(i as f32);
}
let gate_count = 100;
let mut field = SweepField::new_empty(
"Reflectivity",
"dBZ",
elevation,
azimuths,
1.0,
2.0,
0.25,
gate_count,
);
for az in 0..360 {
for gate in 0..gate_count {
field.set(az, gate, value, GateStatus::Valid);
}
}
field
}
#[test]
fn test_vertical_basic() {
let fields = vec![make_uniform_field(0.5, 30.0), make_uniform_field(3.5, 40.0)];
let vcs = VerticalCrossSection::new(180.0, 25.0, 5000.0, 50, 25).unwrap();
let result = vcs.compute(&fields).unwrap();
assert_eq!(result.width(), 50);
assert_eq!(result.height(), 25);
let mut found_valid = false;
for row in 0..result.height() {
for col in 0..result.width() {
let (_, status) = result.get(row, col);
if status == GateStatus::Valid {
found_valid = true;
}
}
}
assert!(found_valid, "expected at least some valid data");
}
#[test]
fn test_vertical_takes_max() {
let fields = vec![make_uniform_field(0.5, 20.0), make_uniform_field(0.5, 40.0)];
let vcs = VerticalCrossSection::new(180.0, 25.0, 5000.0, 50, 25).unwrap();
let result = vcs.compute(&fields).unwrap();
for row in 0..result.height() {
for col in 0..result.width() {
let (val, status) = result.get(row, col);
if status == GateStatus::Valid {
assert_eq!(val, 40.0);
}
}
}
}
#[test]
fn test_vertical_empty_fields_error() {
let vcs = VerticalCrossSection::new(180.0, 25.0, 5000.0, 50, 25).unwrap();
assert!(vcs.compute(&[]).is_err());
}
#[test]
fn test_vertical_invalid_params() {
assert!(VerticalCrossSection::new(180.0, 0.0, 5000.0, 50, 25).is_err());
assert!(VerticalCrossSection::new(180.0, 25.0, 0.0, 50, 25).is_err());
assert!(VerticalCrossSection::new(180.0, 25.0, 5000.0, 0, 25).is_err());
assert!(VerticalCrossSection::new(180.0, 25.0, 5000.0, 50, 0).is_err());
}
}