rain_viewer/
lib.rs

1//! Rust bindings to the free Rain Viewer API <https://www.rainviewer.com/weather-radar-map-live.html>
2//!
3//! Provides easy access to satellite-imagery-style precipitation radar imagery
4//! for the entire world.
5//!
6//! # Example
7//!
8//! ```
9//! #[tokio::main]
10//! async fn main() {
11//!     //Create requester for issuing requests
12//!     let req = rain_viewer::WeatherRequester::new();
13//!     // Query what data is available
14//!     let maps = req.available().await.unwrap();
15//!
16//!     // Pick the first past entry in the past to sample
17//!     let frame = &maps.past_radar[0];
18//!
19//!     // Setup the arguments for the tile we want to access
20//!     // Parameters are x, y and zoom following the satellite imagery style
21//!     let mut args = rain_viewer::RequestArguments::new_tile(4, 7, 6).unwrap();
22//!     // Use this pretty color scheme
23//!     args.set_color(rain_viewer::ColorKind::Titan);
24//!     // Enable showing snow in addition to rain
25//!     args.set_snow(true);
26//!     // Smooth out the tile image (looks nicer from tile to tile)
27//!     args.set_smooth(false);
28//!
29//!     // Make an API call to get the time image data using our parameters
30//!     let png = req.get_tile(&maps, frame, args)
31//!         .await
32//!         .unwrap();
33//!
34//!     //Check for PNG magic to make sure we got an image
35//!     assert_eq!(&png[0..4], &[0x89, 0x50, 0x4e, 0x47]);
36//! }
37//! ```
38//!
39//! [`available`] is the entry point to obtaining radar imagery. This returns
40//! historical data and forecast data that is available.
41//!
42//! From there, most users call [`get_tile`] to download a PNG of a specific satellite tile.
43
44mod error;
45
46pub use error::*;
47
48use serde::Deserialize;
49
50/// The kinds of colors supported by rainviewer
51/// All have different visual attributes. See <https://www.rainviewer.com/api/color-schemes.html>
52/// for more information
53#[derive(Copy, Clone, Debug)]
54pub enum ColorKind {
55    BlackAndWhite,
56    Original,
57    UniversalBlue,
58    Titan,
59    TheWeatherChannel,
60    Meteored,
61    NexradLevelIII,
62    RainbowSelexIS,
63    DarkSky,
64}
65
66impl From<ColorKind> for u32 {
67    fn from(color: ColorKind) -> Self {
68        // Values obtained from: https://www.rainviewer.com/api/color-schemes.html
69        match color {
70            ColorKind::BlackAndWhite => 0,
71            ColorKind::Original => 1,
72            ColorKind::UniversalBlue => 2,
73            ColorKind::Titan => 3,
74            ColorKind::TheWeatherChannel => 4,
75            ColorKind::Meteored => 5,
76            ColorKind::NexradLevelIII => 6,
77            ColorKind::RainbowSelexIS => 7,
78            ColorKind::DarkSky => 8,
79        }
80    }
81}
82
83#[derive(Copy, Clone, Debug)]
84struct TileArguments {
85    size: u32,
86    x: u32,
87    y: u32,
88    zoom: u32,
89    color: ColorKind,
90    smooth: bool,
91    snow: bool,
92}
93
94#[derive(Copy, Clone, Debug)]
95enum RequestArgumentsInner {
96    Tile(TileArguments),
97}
98
99/// Arguments needed to pull a rain tile from rainviewer
100#[derive(Copy, Clone)]
101pub struct RequestArguments {
102    inner: RequestArgumentsInner,
103}
104
105impl RequestArguments {
106    /// Creates arguments struct suitable for making a radar image request for a single tile
107    ///
108    /// `x` and `x` must be less than `2^zoom`, or Err(...) is returned
109    pub fn new_tile(x: u32, y: u32, zoom: u32) -> Result<Self, error::ParameterError> {
110        let max_coord = 2u32.pow(zoom);
111        if x >= max_coord {
112            Err(ParameterError::XOutOfRange(
113                x,
114                format!(
115                    "With a zoom of {}, the max value for x is {}",
116                    zoom,
117                    max_coord - 1
118                ),
119            ))
120        } else if y >= max_coord {
121            Err(ParameterError::YOutOfRange(
122                y,
123                format!(
124                    "With a zoom of {}, the max value for y is {}",
125                    zoom,
126                    max_coord - 1
127                ),
128            ))
129        } else {
130            Ok(Self {
131                inner: RequestArgumentsInner::Tile(TileArguments {
132                    size: 256,
133                    x,
134                    y,
135                    zoom,
136                    color: ColorKind::UniversalBlue,
137                    smooth: true,
138                    snow: true,
139                }),
140            })
141        }
142    }
143
144    /// Sets the size of the resulting image when the API call is made.
145    ///
146    /// `size` must be 256 or 512 else Err(...) is returned
147    pub fn set_size(&mut self, size: u32) -> Result<&mut Self, error::ParameterError> {
148        if size == 256 || size == 512 {
149            match &mut self.inner {
150                RequestArgumentsInner::Tile(tile) => {
151                    tile.size = size;
152                }
153            };
154            Ok(self)
155        } else {
156            Err(ParameterError::InvalidSize(
157                size,
158                "Image size must be either 256 or 512".to_owned(),
159            ))
160        }
161    }
162
163    /// Sets the size of the resulting tile image when the API call is made
164    pub fn set_smooth(&mut self, smooth: bool) -> &mut Self {
165        match &mut self.inner {
166            RequestArgumentsInner::Tile(tile) => {
167                tile.smooth = smooth;
168            }
169        };
170        self
171    }
172
173    /// Sets weather or not the resulting tile should show snow
174    pub fn set_snow(&mut self, snow: bool) -> &mut Self {
175        match &mut self.inner {
176            RequestArgumentsInner::Tile(tile) => {
177                tile.snow = snow;
178            }
179        };
180        self
181    }
182
183    /// Sets the color scheme for the tile
184    pub fn set_color(&mut self, color: ColorKind) -> &mut Self {
185        match &mut self.inner {
186            RequestArgumentsInner::Tile(tile) => {
187                tile.color = color;
188            }
189        };
190        self
191    }
192}
193
194pub struct WeatherRequester {
195    client: reqwest::Client,
196}
197
198impl WeatherRequester {
199    pub fn new() -> Self {
200        Self {
201            client: reqwest::Client::new(),
202        }
203    }
204    /// Queries the Rain Viewer API for what current and historical data is available.
205    /// This function should serve as the entry point so that the caller has the correct path and time
206    /// information to call [`get_tile`]
207    pub async fn available(&self) -> Result<AvailableData, error::Error> {
208        let res = self
209            .client
210            .get("https://api.rainviewer.com/public/weather-maps.json")
211            .send()
212            .await?;
213        let raw: RawAvailableData = serde_json::from_str(res.text().await?.as_str())?;
214
215        Ok(AvailableData {
216            host: raw.host,
217            past_radar: raw.radar.past.into_iter().map(|r| r.into()).collect(),
218            nowcast_radar: raw.radar.nowcast.into_iter().map(|r| r.into()).collect(),
219            infrared_satellite: raw
220                .satellite
221                .infrared
222                .into_iter()
223                .map(|r| r.into())
224                .collect(),
225        })
226    }
227
228    /// Hits the Rain Viewer API to obtain a single tile of rain for the world
229    ///
230    /// `maps` is the struct returned from [`available`]
231    ///
232    /// `frame` is the data frame indicating the moment in time to pull from
233    ///
234    /// See <https://www.rainviewer.com/api/weather-maps-api.html> for more details
235    pub async fn get_tile(
236        &self,
237        maps: &AvailableData,
238        frame: &Frame,
239        args: RequestArguments,
240    ) -> Result<Vec<u8>, error::Error> {
241        match args.inner {
242            RequestArgumentsInner::Tile(args) => {
243                let options = format!("{}_{}", args.smooth as u8, args.snow as u8);
244                let color_val: u32 = args.color.into();
245                let url = format!(
246                    "{}/{}/{}/{}/{}/{}/{}/{}.png",
247                    maps.host, frame.path, args.size, args.zoom, args.x, args.y, color_val, options,
248                );
249                let res = self.client.get(url).send().await?;
250                match res.status() {
251                    reqwest::StatusCode::OK => Ok(res.bytes().await?.to_vec()),
252                    status => Err(Error::Http(status)),
253                }
254            }
255        }
256    }
257}
258
259/// Indicates that radar or satellite data is available for the time given at path [`path`]
260#[derive(Debug, Clone)]
261pub struct Frame {
262    /// The timestamp when this data was generated
263    pub time: chrono::NaiveDateTime,
264
265    /// The path where this data can be accessed
266    pub path: String,
267}
268
269/// Contains the kinds of imagery that are available
270#[derive(Debug, Clone)]
271pub struct AvailableData {
272    host: String,
273    pub past_radar: Vec<Frame>,
274    pub nowcast_radar: Vec<Frame>,
275    pub infrared_satellite: Vec<Frame>,
276}
277
278/// Base API information returned by [`available`]
279///
280/// `radar` and `satellite` contain frame objects that can be used in conjunction with [`get_tile`]
281/// to obtain a tile of imagery.
282#[derive(Deserialize)]
283#[allow(dead_code)]
284struct RawAvailableData {
285    /// The version of Rain Viewer
286    pub version: String,
287    /// The unix timestamp when this response was generated
288    pub generated: u64,
289
290    /// The tile host. Pass this value to [`get_tile`] so that it contacts the correct mirror
291    pub host: String,
292
293    /// What radar information is available
294    pub radar: Radar,
295
296    /// What satellite information is available
297    pub satellite: Satellite,
298}
299
300#[derive(Deserialize)]
301struct Radar {
302    past: Vec<RawFrame>,
303    nowcast: Vec<RawFrame>,
304}
305
306#[derive(Deserialize)]
307struct Satellite {
308    infrared: Vec<RawFrame>,
309}
310
311#[derive(Deserialize, Debug, Clone)]
312struct RawFrame {
313    /// The unix timestamp when this data was generated
314    pub time: u64,
315
316    /// The path where this data can be accessed
317    pub path: String,
318}
319
320impl From<RawFrame> for Frame {
321    fn from(raw: RawFrame) -> Self {
322        use chrono::TimeZone;
323
324        Self {
325            time: chrono::Utc.timestamp(raw.time as i64, 0).naive_utc(),
326            path: raw.path,
327        }
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[tokio::test]
336    async fn test() {
337        let req = WeatherRequester::new();
338        let maps = req.available().await.unwrap();
339        let frame = &maps.past_radar[0];
340        let args = RequestArgumentsInner::Tile(TileArguments {
341            size: 256,
342            x: 26,
343            y: 12,
344            zoom: 6,
345            color: ColorKind::UniversalBlue,
346            smooth: true,
347            snow: true,
348        });
349        let png = req
350            .get_tile(&maps, frame, RequestArguments { inner: args })
351            .await
352            .unwrap();
353
354        //Check for PNG magic
355        assert_eq!(&png[0..4], &[0x89, 0x50, 0x4e, 0x47]);
356    }
357}