use crate::RgbaImage;
use nexrad_model::data::{GateStatus, SweepField};
use nexrad_model::geo::{GeoExtent, GeoPoint, GeoPoint3D, PolarPoint, RadarCoordinateSystem};
use std::path::Path;
#[derive(Debug)]
pub struct RenderResult {
image: RgbaImage,
metadata: RenderMetadata,
}
#[derive(Debug, Clone)]
pub struct RenderMetadata {
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) center_pixel: (f64, f64),
pub(crate) pixels_per_km: f64,
pub(crate) max_range_km: f64,
pub(crate) elevation_degrees: Option<f32>,
pub(crate) geo_extent: Option<GeoExtent>,
pub(crate) coord_system: Option<RadarCoordinateSystem>,
}
#[derive(Debug, Clone)]
pub struct PointQuery {
pub polar: PolarPoint,
pub geo: Option<GeoPoint3D>,
pub value: f32,
pub status: GateStatus,
}
impl RenderResult {
pub fn new(image: RgbaImage, metadata: RenderMetadata) -> Self {
Self { image, metadata }
}
pub fn image(&self) -> &RgbaImage {
&self.image
}
pub fn into_image(self) -> RgbaImage {
self.image
}
pub fn metadata(&self) -> &RenderMetadata {
&self.metadata
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> crate::result::Result<()> {
self.image
.save(path)
.map_err(|e| crate::result::Error::ImageSave(e.to_string()))
}
pub fn query_pixel(&self, field: &SweepField, x: f64, y: f64) -> Option<PointQuery> {
let polar = self.metadata.pixel_to_polar(x, y)?;
self.build_query(field, polar)
}
pub fn query_polar(
&self,
field: &SweepField,
azimuth_degrees: f32,
range_km: f64,
) -> Option<PointQuery> {
let polar = PolarPoint {
azimuth_degrees,
range_km,
elevation_degrees: self.metadata.elevation_degrees.unwrap_or(0.0),
};
self.build_query(field, polar)
}
pub fn query_geo(&self, field: &SweepField, point: &GeoPoint) -> Option<PointQuery> {
let coord_system = self.metadata.coord_system.as_ref()?;
let elevation = self.metadata.elevation_degrees.unwrap_or(0.0);
let polar = coord_system.geo_to_polar(*point, elevation);
self.build_query(field, polar)
}
fn build_query(&self, field: &SweepField, polar: PolarPoint) -> Option<PointQuery> {
let (value, status) = field.value_at_polar(polar.azimuth_degrees, polar.range_km)?;
let geo = self
.metadata
.coord_system
.as_ref()
.map(|cs| cs.polar_to_geo(polar));
Some(PointQuery {
polar,
geo,
value,
status,
})
}
}
impl RenderMetadata {
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn center_pixel(&self) -> (f64, f64) {
self.center_pixel
}
pub fn pixels_per_km(&self) -> f64 {
self.pixels_per_km
}
pub fn max_range_km(&self) -> f64 {
self.max_range_km
}
pub fn elevation_degrees(&self) -> Option<f32> {
self.elevation_degrees
}
pub fn geo_extent(&self) -> Option<&GeoExtent> {
self.geo_extent.as_ref()
}
pub fn coord_system(&self) -> Option<&RadarCoordinateSystem> {
self.coord_system.as_ref()
}
pub fn pixel_to_polar(&self, x: f64, y: f64) -> Option<PolarPoint> {
let dx = x - self.center_pixel.0;
let dy = y - self.center_pixel.1;
let distance_px = (dx * dx + dy * dy).sqrt();
let range_km = distance_px / self.pixels_per_km;
if range_km > self.max_range_km {
return None;
}
let azimuth_rad = dx.atan2(-dy);
let azimuth_degrees = (azimuth_rad.to_degrees() + 360.0) % 360.0;
Some(PolarPoint {
azimuth_degrees: azimuth_degrees as f32,
range_km,
elevation_degrees: self.elevation_degrees.unwrap_or(0.0),
})
}
pub fn polar_to_pixel(&self, azimuth_degrees: f32, range_km: f64) -> (f64, f64) {
let az_rad = (azimuth_degrees as f64).to_radians();
let distance_px = range_km * self.pixels_per_km;
let x = self.center_pixel.0 + distance_px * az_rad.sin();
let y = self.center_pixel.1 - distance_px * az_rad.cos();
(x, y)
}
pub fn pixel_to_geo(&self, x: f64, y: f64) -> Option<GeoPoint3D> {
let polar = self.pixel_to_polar(x, y)?;
let coord_system = self.coord_system.as_ref()?;
Some(coord_system.polar_to_geo(polar))
}
pub fn geo_to_pixel(&self, point: &GeoPoint) -> Option<(f64, f64)> {
let coord_system = self.coord_system.as_ref()?;
let elevation = self.elevation_degrees.unwrap_or(0.0);
let polar = coord_system.geo_to_polar(*point, elevation);
Some(self.polar_to_pixel(polar.azimuth_degrees, polar.range_km))
}
pub fn pixel_distance_to_km(&self, pixels: f64) -> f64 {
pixels / self.pixels_per_km
}
pub fn km_to_pixel_distance(&self, km: f64) -> f64 {
km * self.pixels_per_km
}
}