nexrad_render/
render_result.rs1use crate::RgbaImage;
7use nexrad_model::data::{GateStatus, SweepField};
8use nexrad_model::geo::{GeoExtent, GeoPoint, GeoPoint3D, PolarPoint, RadarCoordinateSystem};
9use std::path::Path;
10
11#[derive(Debug)]
17pub struct RenderResult {
18 image: RgbaImage,
19 metadata: RenderMetadata,
20}
21
22#[derive(Debug, Clone)]
26pub struct RenderMetadata {
27 pub(crate) width: u32,
28 pub(crate) height: u32,
29 pub(crate) center_pixel: (f64, f64),
30 pub(crate) pixels_per_km: f64,
31 pub(crate) max_range_km: f64,
32 pub(crate) elevation_degrees: Option<f32>,
33 pub(crate) geo_extent: Option<GeoExtent>,
34 pub(crate) coord_system: Option<RadarCoordinateSystem>,
35}
36
37#[derive(Debug, Clone)]
39pub struct PointQuery {
40 pub polar: PolarPoint,
42 pub geo: Option<GeoPoint3D>,
44 pub value: f32,
46 pub status: GateStatus,
48}
49
50impl RenderResult {
51 pub fn new(image: RgbaImage, metadata: RenderMetadata) -> Self {
53 Self { image, metadata }
54 }
55
56 pub fn image(&self) -> &RgbaImage {
58 &self.image
59 }
60
61 pub fn into_image(self) -> RgbaImage {
63 self.image
64 }
65
66 pub fn metadata(&self) -> &RenderMetadata {
68 &self.metadata
69 }
70
71 pub fn save<P: AsRef<Path>>(&self, path: P) -> crate::result::Result<()> {
80 self.image
81 .save(path)
82 .map_err(|e| crate::result::Error::ImageSave(e.to_string()))
83 }
84
85 pub fn query_pixel(&self, field: &SweepField, x: f64, y: f64) -> Option<PointQuery> {
90 let polar = self.metadata.pixel_to_polar(x, y)?;
91 self.build_query(field, polar)
92 }
93
94 pub fn query_polar(
96 &self,
97 field: &SweepField,
98 azimuth_degrees: f32,
99 range_km: f64,
100 ) -> Option<PointQuery> {
101 let polar = PolarPoint {
102 azimuth_degrees,
103 range_km,
104 elevation_degrees: self.metadata.elevation_degrees.unwrap_or(0.0),
105 };
106 self.build_query(field, polar)
107 }
108
109 pub fn query_geo(&self, field: &SweepField, point: &GeoPoint) -> Option<PointQuery> {
114 let coord_system = self.metadata.coord_system.as_ref()?;
115 let elevation = self.metadata.elevation_degrees.unwrap_or(0.0);
116 let polar = coord_system.geo_to_polar(*point, elevation);
117 self.build_query(field, polar)
118 }
119
120 fn build_query(&self, field: &SweepField, polar: PolarPoint) -> Option<PointQuery> {
121 let (value, status) = field.value_at_polar(polar.azimuth_degrees, polar.range_km)?;
122
123 let geo = self
124 .metadata
125 .coord_system
126 .as_ref()
127 .map(|cs| cs.polar_to_geo(polar));
128
129 Some(PointQuery {
130 polar,
131 geo,
132 value,
133 status,
134 })
135 }
136}
137
138impl RenderMetadata {
139 pub fn width(&self) -> u32 {
141 self.width
142 }
143
144 pub fn height(&self) -> u32 {
146 self.height
147 }
148
149 pub fn center_pixel(&self) -> (f64, f64) {
151 self.center_pixel
152 }
153
154 pub fn pixels_per_km(&self) -> f64 {
156 self.pixels_per_km
157 }
158
159 pub fn max_range_km(&self) -> f64 {
161 self.max_range_km
162 }
163
164 pub fn elevation_degrees(&self) -> Option<f32> {
166 self.elevation_degrees
167 }
168
169 pub fn geo_extent(&self) -> Option<&GeoExtent> {
171 self.geo_extent.as_ref()
172 }
173
174 pub fn coord_system(&self) -> Option<&RadarCoordinateSystem> {
177 self.coord_system.as_ref()
178 }
179
180 pub fn pixel_to_polar(&self, x: f64, y: f64) -> Option<PolarPoint> {
184 let dx = x - self.center_pixel.0;
185 let dy = y - self.center_pixel.1;
186 let distance_px = (dx * dx + dy * dy).sqrt();
187 let range_km = distance_px / self.pixels_per_km;
188
189 if range_km > self.max_range_km {
190 return None;
191 }
192
193 let azimuth_rad = dx.atan2(-dy);
194 let azimuth_degrees = (azimuth_rad.to_degrees() + 360.0) % 360.0;
195
196 Some(PolarPoint {
197 azimuth_degrees: azimuth_degrees as f32,
198 range_km,
199 elevation_degrees: self.elevation_degrees.unwrap_or(0.0),
200 })
201 }
202
203 pub fn polar_to_pixel(&self, azimuth_degrees: f32, range_km: f64) -> (f64, f64) {
205 let az_rad = (azimuth_degrees as f64).to_radians();
206 let distance_px = range_km * self.pixels_per_km;
207 let x = self.center_pixel.0 + distance_px * az_rad.sin();
208 let y = self.center_pixel.1 - distance_px * az_rad.cos();
209 (x, y)
210 }
211
212 pub fn pixel_to_geo(&self, x: f64, y: f64) -> Option<GeoPoint3D> {
217 let polar = self.pixel_to_polar(x, y)?;
218 let coord_system = self.coord_system.as_ref()?;
219 Some(coord_system.polar_to_geo(polar))
220 }
221
222 pub fn geo_to_pixel(&self, point: &GeoPoint) -> Option<(f64, f64)> {
226 let coord_system = self.coord_system.as_ref()?;
227 let elevation = self.elevation_degrees.unwrap_or(0.0);
228 let polar = coord_system.geo_to_polar(*point, elevation);
229 Some(self.polar_to_pixel(polar.azimuth_degrees, polar.range_km))
230 }
231
232 pub fn pixel_distance_to_km(&self, pixels: f64) -> f64 {
234 pixels / self.pixels_per_km
235 }
236
237 pub fn km_to_pixel_distance(&self, km: f64) -> f64 {
239 km * self.pixels_per_km
240 }
241}