wthrr 1.1.0

Weather companion for the terminal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
use anyhow::Result;
use chrono::{Timelike, Utc};
use colored::{
	Color::{Blue, BrightBlack, Yellow},
	Colorize,
};
use std::fmt::Write as _;

use crate::modules::{
	localization::WeatherCodeLocales,
	params::Params,
	units::{Precipitation, Temperature, Time},
	weather::OptionalWeather,
};

use super::{
	border::{Border, BorderStyle, Separator},
	graph::Graph,
	gui_config::ColorOption,
	product::Product,
	utils::{lang_len_diff, style_number},
	weathercode::WeatherCode,
};

const DISPLAY_HOURS: [usize; 8] = [0, 3, 6, 9, 12, 15, 18, 21];
pub const WIDTH: usize = 72;

pub struct HourlyForecast {
	heading: String,
	temperatures: String,
	precipitation: String,
	graph: Graph,
	summary: Option<WeatherSummery>,
	time_indicator_col: Option<usize>,
}

struct WeatherSummery {
	temp_max_min: String,
	precipitation_probability_max: u8,
}

impl HourlyForecast {
	pub fn render(self, params: &Params) {
		let Self {
			heading,
			temperatures,
			precipitation,
			summary,
			graph,
			time_indicator_col,
		} = self;

		let (units, gui) = (&params.config.units, &params.config.gui);

		// Blank Line
		println!(
			"{}",
			&Separator::Blank.fmt(WIDTH, &gui.border).color_option(BrightBlack, &gui.color)
		);

		// Set Measurment Unit Symbols
		let temperature_unit = match units.temperature {
			Temperature::fahrenheit => "",
			Temperature::celsius => "",
		};
		let precipitation_unit = match units.precipitation {
			Precipitation::mm => "ₘₘ",
			Precipitation::inch => "ᵢₙ",
			Precipitation::probability => "󰖎 ",
		};

		// Hourly Forecast Heading
		println!(
			"{} {: <WIDTH$} {}",
			Border::L.fmt(&gui.border).color_option(BrightBlack, &gui.color),
			heading.bold(),
			Border::R.fmt(&gui.border).color_option(BrightBlack, &gui.color),
			WIDTH = WIDTH - 2 - lang_len_diff(&heading, &params.config.language)
		);

		// Day Max/Mix Temperatur + Max Precipitation
		if let Some(summary) = summary {
			println!(
				"{} {} ❲{}{}❳{: <WIDTH$} {}",
				Border::L.fmt(&gui.border).color_option(BrightBlack, &gui.color),
				summary.temp_max_min,
				summary.precipitation_probability_max,
				"󰖎".bold(),
				"",
				Border::R.fmt(&gui.border).color_option(BrightBlack, &gui.color),
				WIDTH =
					WIDTH - 5 - summary.temp_max_min.len() - summary.precipitation_probability_max.to_string().len()
			);
		}

		// Graph Border Top with Potential Time Indicator
		match time_indicator_col {
			Some(col) => {
				println!(
					"{}",
					Self::prepare_separator(col, &gui.border, WIDTH, '╤').color_option(BrightBlack, &gui.color),
				);
			}
			_ => {
				println!(
					"{}",
					Separator::Dashed.fmt(WIDTH, &gui.border).color_option(BrightBlack, &gui.color)
				);
			}
		}

		// Temperatures
		println!(
			"{} {: <WIDTH$}{} {}",
			Border::L.fmt(&gui.border).color_option(BrightBlack, &gui.color),
			temperatures.color_option(Yellow, &gui.color).bold(),
			temperature_unit.color_option(Yellow, &gui.color).bold(),
			Border::R.fmt(&gui.border).color_option(BrightBlack, &gui.color),
			WIDTH = WIDTH - 3
		);

		// Blank Line
		println!(
			"{}",
			&Separator::Blank.fmt(WIDTH, &gui.border).color_option(BrightBlack, &gui.color)
		);

		// Graph Row 1
		if graph.1.chars().count() > 0 {
			println!(
				"{}{}{}",
				Border::L.fmt(&gui.border).color_option(BrightBlack, &gui.color),
				graph.1.color_option(Yellow, &gui.color),
				Border::R.fmt(&gui.border).color_option(BrightBlack, &gui.color)
			);
		}
		// Graph Row 2
		println!(
			"{}{}{}",
			Border::L.fmt(&gui.border).color_option(BrightBlack, &gui.color),
			graph.0.color_option(Yellow, &gui.color),
			Border::R.fmt(&gui.border).color_option(BrightBlack, &gui.color)
		);

		// Precipitation
		println!(
			"{} {: <WIDTH$}{}{}",
			Border::L.fmt(&gui.border).color_option(BrightBlack, &gui.color),
			precipitation.color_option(Blue, &gui.color).bold(),
			if units.precipitation == Precipitation::probability {
				// to enlarge the water percent icon we use bold as a hack
				precipitation_unit.color_option(Blue, &gui.color).bold()
			} else {
				precipitation_unit.color_option(Blue, &gui.color)
			},
			Border::R.fmt(&gui.border).color_option(BrightBlack, &gui.color),
			WIDTH = WIDTH - 1 - precipitation_unit.chars().count()
		);

		// Graph Border Bottom with Potential Time Indicator
		match time_indicator_col {
			Some(col) => {
				println!(
					"{}",
					Self::prepare_separator(col, &gui.border, WIDTH, '╧').color_option(BrightBlack, &gui.color),
				);
			}
			_ => {
				println!(
					"{}",
					Separator::Dashed.fmt(WIDTH, &gui.border).color_option(BrightBlack, &gui.color)
				);
			}
		}

		// Graph Hours Row
		print!("{}", Border::L.fmt(&gui.border).color_option(BrightBlack, &gui.color));
		let hours = match units.time {
			Time::am_pm => ["¹²·⁰⁰ₐₘ", "³·⁰⁰ₐₘ", "⁶˙⁰⁰ₐₘ", "⁹˙⁰⁰ₐₘ", "¹²˙⁰⁰ₚₘ", "³˙⁰⁰ₚₘ", "⁶˙⁰⁰ₚₘ", "⁹˙⁰⁰ₚₘ"],
			Time::military => ["⁰⁰˙⁰⁰", "⁰³˙⁰⁰", "⁰⁶˙⁰⁰", "⁰⁹˙⁰⁰", "¹²˙⁰⁰", "¹⁵˙⁰⁰", "¹⁸˙⁰⁰", "²¹˙⁰⁰"],
		};
		for hour in hours {
			print!("{hour: <9}");
		}
		println!("{}", Border::R.fmt(&gui.border).color_option(BrightBlack, &gui.color));
	}

	pub fn prepare(product: &Product, params: &Params, day_index: usize) -> Result<Self> {
		let weather = &product.weather;
		let current_hour = weather.current_weather.time[11..13].parse::<usize>().unwrap_or_default();

		// The graph splits one hour into three "levels": last, current and next.
		// We slice 25 items to use the 25th in the last "next"-level of a graph.
		let day_start_idx = day_index * 24;
		let day_end_idx = day_start_idx + 24;
		let next_day_start_idx = day_end_idx + 1;
		let next_day_end_idx = next_day_start_idx + 24;

		let (temperatures, weather_codes, precipitation): (&[f32], &[u8], Vec<u8>);
		let (mut temps, mut codes, mut prec);
		// If it's the last possible requested day, the last index(start_index of the 7th day) is not available.
		// Therefore we'll extend the values by 1. For this we simply use the last value of the array twice.
		if day_index == 6 {
			temperatures = {
				temps = weather.hourly.temperature_2m[day_start_idx..].to_vec();
				temps.push(weather.hourly.temperature_2m[weather.hourly.temperature_2m.len() - 1]);
				&temps
			};
			weather_codes = {
				codes = weather.hourly.weathercode[day_start_idx..].to_vec();
				codes.push(weather.hourly.weathercode[weather.hourly.weathercode.len() - 1]);
				&codes
			};
			precipitation = match params.config.units.precipitation {
				Precipitation::probability => {
					prec = weather.hourly.precipitation_probability[day_start_idx..].to_vec();
					prec.push(
						weather.hourly.precipitation_probability[weather.hourly.precipitation_probability.len() - 1],
					);
					prec
				}
				_ => weather.hourly.precipitation[day_start_idx..]
					.iter()
					.map(|x| x.ceil() as u8)
					.collect::<Vec<u8>>(),
			};
		// If it's the end of one day we show the weather of the next day
		} else if current_hour == day_end_idx - 1 {
			temperatures = &weather.hourly.temperature_2m[next_day_start_idx..=next_day_end_idx];
			weather_codes = &weather.hourly.weathercode[next_day_start_idx..=next_day_end_idx];
			precipitation = match params.config.units.precipitation {
				Precipitation::probability => {
					weather.hourly.precipitation_probability[next_day_start_idx..=next_day_end_idx].to_vec()
				}
				_ => weather.hourly.precipitation[next_day_start_idx..=next_day_end_idx]
					.iter()
					.map(|x| x.ceil() as u8)
					.collect::<Vec<u8>>(),
			};
		} else {
			temperatures = &weather.hourly.temperature_2m[day_start_idx..=day_end_idx];
			weather_codes = &weather.hourly.weathercode[day_start_idx..=day_end_idx];
			precipitation = match params.config.units.precipitation {
				Precipitation::probability => {
					weather.hourly.precipitation_probability[day_start_idx..=day_end_idx].to_vec()
				}
				_ => weather.hourly.precipitation[day_start_idx..=day_end_idx]
					.iter()
					.map(|x| x.ceil() as u8)
					.collect::<Vec<u8>>(),
			};
		};

		let time_indicator_col = if day_index == 0 && params.config.gui.graph.time_indicator {
			let col_adjustment = if current_hour == day_end_idx - 1 {
				// if it's the last hour of the day, the time idicator will be placed at the beginning of the graph
				1
			} else {
				// add 3 cols to adjust to the multiple chars used to display the current hour below the chart
				(current_hour * 3) + 3
			};
			Some(col_adjustment + (Timelike::minute(&Utc::now()) / 20) as usize)
		} else {
			None
		};

		let sunrise_sunset = (
			weather.daily.sunrise[day_index][11..13].parse::<usize>().unwrap_or_default(),
			weather.daily.sunset[day_index][11..13].parse::<usize>().unwrap_or_default(),
		);

		// Future or historical forecast already include a weather Max/Min summary in the top part of the display.
		let summary = match day_index {
			0 => Some(WeatherSummery {
				temp_max_min: format!(
					"{:.1}/{:.1}{}",
					weather.daily.temperature_2m_max[day_index],
					weather.daily.temperature_2m_min[day_index],
					weather.daily_units.temperature_2m_max,
				),
				precipitation_probability_max: weather.daily.precipitation_probability_max[day_index],
			}),
			_ => None,
		};

		Ok(Self {
			heading: params.texts.weather.hourly_forecast.to_string(),
			temperatures: Self::prepare_temperatures(
				temperatures,
				weather_codes,
				sunrise_sunset,
				&params.texts.weather.weather_code,
			)?,
			precipitation: Self::prepare_precipitation(&precipitation),
			summary,
			graph: Graph::prepare_graph(temperatures, &params.config.gui.graph),
			time_indicator_col,
		})
	}

	pub fn prepare_historical(weather: &OptionalWeather, params: &Params) -> Result<Self> {
		// If it's the last possible requested day, the last index(start_index of the 7th day) is not available.
		// Therefore we'll extend the values by 1. For this we simply use the last value of the array twice.
		let (mut temps, mut codes, mut prec);
		let temperatures = {
			temps = weather.hourly.as_ref().unwrap().temperature_2m.as_ref().unwrap()[0..].to_vec();
			temps.push(temps[temps.len() - 1]);
			&temps
		};
		let weather_codes = {
			codes = weather.hourly.as_ref().unwrap().weathercode.as_ref().unwrap()[0..].to_vec();
			codes.push(codes[codes.len() - 1]);
			&codes
		};
		let sunrise_sunset = (
			weather.daily.as_ref().unwrap().sunrise.as_ref().unwrap()[0][11..13]
				.parse::<usize>()
				.unwrap_or_default(),
			weather.daily.as_ref().unwrap().sunset.as_ref().unwrap()[0][11..13]
				.parse::<usize>()
				.unwrap_or_default(),
		);
		let precipitation = {
			prec = weather.hourly.as_ref().unwrap().precipitation.as_ref().unwrap()[0..].to_vec();
			prec.push(prec[prec.len() - 1]);
			prec.iter().map(|x| x.ceil() as u8).collect::<Vec<u8>>()
		};

		Ok(Self {
			heading: params.texts.weather.daily_overview.to_string(),
			temperatures: Self::prepare_temperatures(
				temperatures,
				weather_codes,
				sunrise_sunset,
				&params.texts.weather.weather_code,
			)?,
			precipitation: Self::prepare_precipitation(&precipitation),
			summary: None,
			graph: Graph::prepare_graph(temperatures, &params.config.gui.graph),
			time_indicator_col: None,
		})
	}

	fn prepare_temperatures(
		temperatures: &[f32],
		weather_codes: &[u8],
		sunrise_sunset: (usize, usize),
		t: &WeatherCodeLocales,
	) -> Result<String> {
		let mut result = String::new();

		for hour in DISPLAY_HOURS {
			let temp = temperatures[hour].round() as i32;
			let temp_sub = style_number(temp, true);
			let night = hour < sunrise_sunset.0 || hour > sunrise_sunset.1;
			let wmo_code = WeatherCode::resolve(weather_codes[hour], night, t)?;
			let colspan = if hour == 0 { 2 } else { 8 };
			let _ = write!(result, "{: >colspan$}{}", temp_sub, wmo_code.icon);
		}

		Ok(result)
	}

	fn prepare_precipitation(precipitation: &[u8]) -> String {
		let mut result = String::new();

		for hour in DISPLAY_HOURS {
			let precipitation_sup = style_number(precipitation[hour].into(), true);
			let colspan = if hour == 0 { 2 } else { 8 };
			let _ = write!(result, "{precipitation_sup: >colspan$} ");
		}

		result
	}

	fn prepare_separator(
		time_indicator_col: usize,
		border_variant: &BorderStyle,
		width: usize,
		time_indicator_glyph: char,
	) -> String {
		match border_variant {
			BorderStyle::double => format!(
				"╟{:─>time_indicator_col$}{:─>width$}╢",
				time_indicator_glyph,
				"",
				width = width - time_indicator_col
			),
			BorderStyle::solid => format!(
				"┠{:─>time_indicator_col$}{:─>width$}┨",
				time_indicator_glyph,
				"",
				width = width - time_indicator_col
			),
			_ => format!(
				"├{:┈>time_indicator_col$}{:┈>width$}┤",
				time_indicator_glyph,
				"",
				width = width - time_indicator_col
			),
		}
	}
}