1use std::{fmt::Display, io};
12
13use chrono::{DateTime, Utc};
14use geo::{CoordsIter, Point as GeoPoint, Polygon as GeoPolygon, polygon};
15use geojson::{Feature, JsonObject, JsonValue};
16use product_description::{OperationalMode, ProductDescription};
17use product_symbology::ProductSymbology;
18use shapefile::{
19 Point as ShapefilePoint, Polygon as ShapefilePolygon, PolygonRing,
20 dbase::{self, FieldValue},
21 record::polygon::GenericPolygon,
22};
23use uom::si::{
24 angle::radian,
25 f32::{Length, Velocity},
26 length::meter,
27};
28
29#[macro_use]
30extern crate uom;
31
32mod error;
33mod product_description;
34mod product_symbology;
35mod radials;
36mod utils;
37
38pub use error::DiprError;
39use product_description::product_description;
40use product_symbology::product_symbology;
41pub use radials::Radial;
42use utils::*;
43
44pub type ParseResult<'a, T> = Result<(T, &'a [u8]), DiprError>;
52
53#[derive(Clone, Debug, PartialEq)]
54pub struct PrecipRate {
58 pub station_code: String,
64 pub capture_time: DateTime<Utc>,
66 pub scan_number: u8,
70 pub location: GeoPoint<f32>,
77 pub operational_mode: OperationalMode,
79 pub precip_detected: bool,
81 pub max_precip_rate: Velocity,
83 pub bin_size: Length,
85 pub range_to_first_bin: Length,
87 pub radials: Vec<Radial>,
89}
90
91unit! {
92 system: uom::si;
93 quantity: uom::si::velocity;
94
95 @inch_per_hour: 0.09144; "in/hr", "inch per hour", "inches per hour";
96}
97
98fn destination(
99 origin_rad: GeoPoint<f32>,
100 origin_lat_sin: f32,
101 origin_lat_cos: f32,
102 bearing_rad: f32,
103 meters: f32,
104) -> GeoPoint<f32> {
105 const EARTH_RADIUS_METERS: f32 = 6371008.8;
106
107 let origin_lng = origin_rad.x();
108
109 let rad = meters / EARTH_RADIUS_METERS;
110
111 let lat =
112 { origin_lat_sin * rad.cos() + origin_lat_cos * rad.sin() * bearing_rad.cos() }.asin();
113 let y = bearing_rad.sin() * rad.sin() * origin_lat_cos;
114 let x = rad.cos() - origin_lat_sin * lat.sin();
115 let y_div_x = y / x;
116 let lng = if y_div_x < 0.017322 {
118 y_div_x
119 } else {
120 y.atan2(x)
121 } + origin_lng;
122
123 let lng = if lng > 180. {
125 lng - 180.
126 } else if lng < -180. {
127 lng + 180.
128 } else {
129 lng
130 };
131
132 GeoPoint::new(lng.to_degrees(), lat.to_degrees())
133}
134
135impl PrecipRate {
136 pub fn into_bins_iter(
150 self,
151 skip_zeros: bool,
152 ) -> impl Iterator<Item = (GeoPolygon<f32>, Velocity)> {
153 let PrecipRate {
154 location,
155 bin_size,
156 range_to_first_bin,
157 radials,
158 ..
159 } = self;
160 let origin = location;
161 radials.into_iter().flat_map(move |radial| {
162 let Radial {
163 azimuth,
164 width,
165 precip_rates,
166 ..
167 } = radial;
168 let origin_rad = origin.to_radians();
169 let origin_lat_sin = origin_rad.y().sin();
170 let origin_lat_cos = origin_rad.y().cos();
171 let center_azimuth = azimuth;
172 let left_azimuth = center_azimuth - width / 2.;
173 let right_azimuth = center_azimuth + width / 2.;
174 precip_rates
175 .into_iter()
176 .enumerate()
177 .flat_map(move |(bin_idx, precip_rate)| {
178 if skip_zeros && precip_rate.get::<inch_per_hour>() == 0. {
179 return None;
180 }
181
182 let distance_inner_meters = range_to_first_bin.get::<meter>()
183 + bin_size.get::<meter>() * (bin_idx as f32 - 0.5);
184 let distance_outer_meters = range_to_first_bin.get::<meter>()
185 + bin_size.get::<meter>() * (bin_idx as f32 + 0.5);
186
187 let center_inner = destination(
188 origin_rad,
189 origin_lat_sin,
190 origin_lat_cos,
191 center_azimuth.get::<radian>(),
192 distance_inner_meters,
193 );
194 let center_outer = destination(
195 origin_rad,
196 origin_lat_sin,
197 origin_lat_cos,
198 center_azimuth.get::<radian>(),
199 distance_outer_meters,
200 );
201
202 let left_inner = destination(
203 origin_rad,
204 origin_lat_sin,
205 origin_lat_cos,
206 left_azimuth.get::<radian>(),
207 distance_inner_meters,
208 );
209 let left_outer = destination(
210 origin_rad,
211 origin_lat_sin,
212 origin_lat_cos,
213 left_azimuth.get::<radian>(),
214 distance_outer_meters,
215 );
216
217 let right_inner = destination(
218 origin_rad,
219 origin_lat_sin,
220 origin_lat_cos,
221 right_azimuth.get::<radian>(),
222 distance_inner_meters,
223 );
224 let right_outer = destination(
225 origin_rad,
226 origin_lat_sin,
227 origin_lat_cos,
228 right_azimuth.get::<radian>(),
229 distance_outer_meters,
230 );
231
232 let bin_shape = if center_inner == right_inner || center_inner == left_inner {
233 polygon!(center_inner.into(), right_outer.into(), left_outer.into(),)
234 } else {
235 polygon!(
236 center_inner.into(),
237 right_inner.into(),
238 right_outer.into(),
239 center_outer.into(),
240 left_outer.into(),
241 left_inner.into()
242 )
243 };
244 Some((bin_shape, precip_rate))
245 })
246 })
247 }
248 pub fn into_shapefile_iter(
251 self,
252 skip_zeros: bool,
253 ) -> impl Iterator<Item = (GenericPolygon<ShapefilePoint>, FieldValue)> {
254 self.into_bins_iter(skip_zeros)
255 .map(|(polygon, precip_rate)| {
256 (
257 ShapefilePolygon::new(PolygonRing::Outer(
258 polygon
259 .coords_iter()
260 .map(|c| ShapefilePoint::new(c.x.into(), c.y.into()))
261 .collect::<Vec<ShapefilePoint>>(),
262 )),
263 dbase::FieldValue::Float(Some(precip_rate.get::<inch_per_hour>())),
264 )
265 })
266 }
267 pub fn into_geojson_iter(self, skip_zeros: bool) -> impl Iterator<Item = Feature> {
270 self.into_bins_iter(skip_zeros)
271 .map(|(polygon, precip_rate)| {
272 let mut properties = JsonObject::new();
273 properties.insert(
274 "precipRate".to_string(),
275 JsonValue::from(precip_rate.get::<inch_per_hour>()),
276 );
277 Feature {
278 geometry: Some((&polygon).into()),
279 properties: Some(properties),
280 ..Default::default()
281 }
282 })
283 }
284}
285
286impl Display for PrecipRate {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 writeln!(f, "Station Code: {}", self.station_code)?;
289 writeln!(f, "Capture Time: {}", self.capture_time)?;
290 writeln!(f, "Operational Mode: {}", self.operational_mode)?;
291 writeln!(
292 f,
293 "Precip Detected: {}",
294 if self.precip_detected { "Yes" } else { "No" }
295 )?;
296 writeln!(f, "Scan Number: {}", self.scan_number)?;
297 writeln!(
298 f,
299 "Max Precip Rate: {:.3} in/hr",
300 self.max_precip_rate.get::<inch_per_hour>()
301 )?;
302 writeln!(
303 f,
304 "Bin Size: {: >3} m",
305 self.bin_size.get::<meter>()
306 )?;
307 writeln!(f, "Number of Radials: {: >4}", self.radials.len())?;
308 write!(
309 f,
310 "Range to First Bin: {: >3} m",
311 self.range_to_first_bin.get::<meter>()
312 )
313 }
314}
315
316fn text_header(input: &[u8]) -> ParseResult<String> {
317 let (_, tail) = take_bytes(input, 7)?;
318 let (station_code, tail) = take_bytes(tail, 4)?;
319 let (_, tail) = take_bytes(tail, 19)?;
320 match String::from_utf8(station_code.to_vec()) {
321 Ok(s) => Ok((s, tail)),
322 Err(e) => Err(e.into()),
323 }
324}
325
326fn message_header(input: &[u8]) -> ParseResult<()> {
327 let (_, tail) = take_bytes(input, 18)?;
328 Ok(((), tail))
329}
330
331pub fn parse_dipr(input: &[u8]) -> Result<PrecipRate, DiprError> {
333 let (station_code, tail) = text_header(input)?;
334 let (_, tail) = message_header(tail)?;
335
336 let (
337 ProductDescription {
338 location,
339 operational_mode,
340 precip_detected,
341 max_precip_rate,
342 uncompressed_size,
343 },
344 tail,
345 ) = product_description(tail)?;
346
347 let mut uncompressed_payload = Vec::with_capacity(uncompressed_size as usize);
349 let mut reader = bzip2_rs::DecoderReader::new(tail);
350 io::copy(&mut reader, &mut uncompressed_payload)?;
351
352 let (
353 ProductSymbology {
354 range_to_first_bin,
355 bin_size,
356 scan_number,
357 capture_time,
358 radials,
359 },
360 _,
361 ) = product_symbology(&uncompressed_payload)?;
362
363 Ok(PrecipRate {
364 station_code,
365 capture_time,
366 scan_number,
367 location,
368 operational_mode,
369 precip_detected,
370 max_precip_rate,
371 bin_size,
372 range_to_first_bin,
373 radials,
374 })
375}