1mod error;
45
46pub use error::*;
47
48use serde::Deserialize;
49
50#[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 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#[derive(Copy, Clone)]
101pub struct RequestArguments {
102 inner: RequestArgumentsInner,
103}
104
105impl RequestArguments {
106 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 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 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 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 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 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 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#[derive(Debug, Clone)]
261pub struct Frame {
262 pub time: chrono::NaiveDateTime,
264
265 pub path: String,
267}
268
269#[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#[derive(Deserialize)]
283#[allow(dead_code)]
284struct RawAvailableData {
285 pub version: String,
287 pub generated: u64,
289
290 pub host: String,
292
293 pub radar: Radar,
295
296 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 pub time: u64,
315
316 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 assert_eq!(&png[0..4], &[0x89, 0x50, 0x4e, 0x47]);
356 }
357}