1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: MIT
3
4use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel, Weak};
5use std::rc::Rc;
6
7use crate::ui;
8use ui::{
9 AppWindow, BusyLayerController, CityWeather, CityWeatherInfo, GeoLocation, GeoLocationEntry,
10 IconType, TemperatureInfo, WeatherForecastInfo, WeatherInfo,
11};
12
13use crate::weather::weathercontroller::{
14 CityData, CityWeatherData, DayWeatherData, ForecastWeatherData, GeoLocationData,
15 WeatherCondition, WeatherControllerSharedPointer,
16};
17
18#[cfg(not(target_arch = "wasm32"))]
19use async_std::task::spawn as spawn_task;
20
21#[cfg(target_arch = "wasm32")]
22use wasm_bindgen_futures::spawn_local as spawn_task;
23
24pub struct WeatherDisplayController {
25 data_controller: WeatherControllerSharedPointer,
26}
27
28fn forecast_graph_command(
29 model: ModelRc<WeatherForecastInfo>,
30 days_count: i32,
31 width: f32,
32 height: f32,
33) -> SharedString {
34 if days_count == 0 || width == 0.0 || height == 0.0 {
35 return SharedString::new();
36 }
37
38 let temperatures: Vec<f32> = model
39 .clone()
40 .iter()
41 .take(days_count as usize)
42 .map(|info| info.weather_info.detailed_temp.day)
43 .collect();
44
45 const MIN_MAX_MARGIN: f32 = 5.0;
46 let min_temperature = match temperatures.iter().min_by(|a, b| a.total_cmp(b)) {
47 Some(min) => min - MIN_MAX_MARGIN,
48 None => 0.0,
49 };
50 let max_temperature = match temperatures.iter().max_by(|a, b| a.total_cmp(b)) {
51 Some(max) => max + MIN_MAX_MARGIN,
52 None => 50.0,
53 };
54
55 let max_temperature_value = max_temperature - min_temperature;
56 let temperature_ratio = height / max_temperature_value;
57
58 let day_width = width / days_count as f32;
59 let max_day_shift = days_count as f32 * day_width;
60
61 let border_command =
62 format!(
63 "M 0 0 M {max_width} 0 M {max_width} {max_temperature_value} M 0 {max_temperature_value} ",
64 max_width=max_day_shift, max_temperature_value=max_temperature_value * temperature_ratio);
65
66 let mut command = border_command;
67
68 let day_shift = |index: f32| -> f32 { index * day_width + 0.5 * day_width };
69 let day_temperature =
70 |temperature: f32| -> f32 { (max_temperature - temperature) * temperature_ratio };
71
72 for (index, &temperature) in temperatures.iter().enumerate() {
73 if index == 0 {
74 command += format!(
75 "M {x} {y} ",
76 x = day_shift(index as f32),
77 y = day_temperature(temperature)
78 )
79 .as_str();
80 }
81
82 if let Some(next_temperature) = temperatures.get(index + 1) {
83 let next_temperature = *next_temperature;
84
85 let day1 = day_shift(index as f32);
86 let day2 = day_shift(index as f32 + 1.0);
87 let temp1 = day_temperature(temperature);
88 let temp2 = day_temperature(next_temperature);
89
90 let day_mid = (day1 + day2) / 2.0;
91 let temp_mid = (temp1 + temp2) / 2.0;
92
93 let cp_day1 = (day_mid + day1) / 2.0;
94 let cp_day2 = (day_mid + day2) / 2.0;
95
96 // Q {x1} {y1} {cx1} {cy1} Q {x2} {y2} {cx2} {cy2}
97 command += format!(
98 "Q {cp_day1} {temp1} {day_mid} {temp_mid} Q {cp_day2} {temp2} {day2} {temp2} "
99 )
100 .as_str();
101 }
102 }
103
104 SharedString::from(command)
105}
106
107impl WeatherDisplayController {
108 pub fn new(data_controller: &WeatherControllerSharedPointer) -> Self {
109 Self { data_controller: data_controller.clone() }
110 }
111
112 pub fn initialize_ui(&self, window: &AppWindow, support_add_city: bool) {
113 let city_weather = window.global::<CityWeather>();
114 let geo_location = window.global::<GeoLocation>();
115
116 // initialized models
117 city_weather
118 .set_city_weather(ModelRc::from(Rc::new(VecModel::<CityWeatherInfo>::from(vec![]))));
119 geo_location
120 .set_result_list(ModelRc::from(Rc::new(VecModel::<GeoLocationEntry>::from(vec![]))));
121
122 // initialize state
123 city_weather.set_can_add_city(support_add_city);
124
125 // handle callbacks
126 city_weather.on_get_forecast_graph_command(forecast_graph_command);
127
128 city_weather.on_refresh_all({
129 let window_weak = window.as_weak();
130 let data_controller = self.data_controller.clone();
131
132 move || Self::refresh_cities(&window_weak, &data_controller)
133 });
134
135 city_weather.on_reorder({
136 let window_weak = window.as_weak();
137 let data_controller = self.data_controller.clone();
138
139 move |index, new_index| {
140 if let Err(e) =
141 Self::reorder_cities(&window_weak, &data_controller, index, new_index)
142 {
143 log::warn!("Failed to reorder city from {} to {}: {}", index, new_index, e);
144 }
145 }
146 });
147
148 city_weather.on_delete({
149 let window_weak = window.as_weak();
150 let data_controller = self.data_controller.clone();
151
152 move |index| {
153 if let Err(e) = Self::remove_city(&window_weak, &data_controller, index) {
154 log::warn!("Failed to remove city from {}: {}", index, e);
155 }
156 }
157 });
158
159 geo_location.on_search_location({
160 let window_weak = window.as_weak();
161 let data_controller = self.data_controller.clone();
162
163 move |location| Self::search_location(&window_weak, &data_controller, location)
164 });
165
166 geo_location.on_add_location({
167 let window_weak = window.as_weak();
168 let data_controller = self.data_controller.clone();
169
170 move |location| {
171 Self::add_city(&window_weak, &data_controller, location);
172 }
173 });
174 }
175
176 #[cfg_attr(not(target_os = "android"), allow(dead_code))]
177 pub fn refresh(&self, window: &AppWindow) {
178 Self::set_busy(window);
179
180 let window_weak = window.as_weak();
181 Self::refresh_cities(&window_weak, &self.data_controller);
182 }
183
184 pub fn load(&self, window: &AppWindow) {
185 Self::set_busy(window);
186
187 let window_weak = window.as_weak();
188 let data_controller = self.data_controller.clone();
189
190 spawn_task(async move {
191 let city_data_res = async {
192 let mut data_controller = data_controller.lock().unwrap();
193 data_controller.load()?;
194 data_controller.refresh_cities()
195 }
196 .await;
197
198 let city_data = match city_data_res {
199 Ok(city_data) => Some(city_data),
200 Err(e) => {
201 log::warn!("Failed to load cities: {}.", e);
202 None
203 }
204 };
205
206 Self::check_update_error(window_weak.upgrade_in_event_loop(move |window| {
207 if let Some(city_data) = city_data {
208 WeatherDisplayController::update_displayed_cities(&window, city_data);
209 }
210 Self::unset_busy(&window);
211 }));
212 });
213 }
214
215 fn refresh_cities(
216 window_weak: &Weak<AppWindow>,
217 data_controller: &WeatherControllerSharedPointer,
218 ) {
219 let window_weak = window_weak.clone();
220 let data_controller = data_controller.clone();
221
222 spawn_task(async move {
223 let city_data_res = async { data_controller.lock().unwrap().refresh_cities() }.await;
224
225 let city_data = match city_data_res {
226 Ok(city_data) => Some(city_data),
227 Err(e) => {
228 log::warn!("Failed to update cities: {}.", e);
229 None
230 }
231 };
232
233 Self::check_update_error(window_weak.upgrade_in_event_loop(move |window| {
234 if let Some(city_data) = city_data {
235 WeatherDisplayController::update_displayed_cities(&window, city_data);
236 }
237 Self::unset_busy(&window);
238 }));
239 });
240 }
241
242 fn add_city(
243 window_weak: &Weak<AppWindow>,
244 data_controller: &WeatherControllerSharedPointer,
245 location: GeoLocationEntry,
246 ) {
247 let city = CityData {
248 lat: location.lat as f64,
249 lon: location.lon as f64,
250 city_name: String::from(&location.name),
251 };
252 let city_data_res = data_controller.lock().unwrap().add_city(city);
253
254 // update ui
255 let window = window_weak.upgrade().unwrap();
256 match city_data_res {
257 Ok(city_data) => {
258 if let Some(city_data) = city_data {
259 let city_weather = window.global::<CityWeather>();
260 let city_weather_list = city_weather.get_city_weather();
261
262 let city_weather = Self::city_weather_info_from_data(&city_data);
263 city_weather_list
264 .as_any()
265 .downcast_ref::<slint::VecModel<CityWeatherInfo>>()
266 .unwrap()
267 .push(city_weather);
268 }
269 }
270 Err(e) => {
271 log::warn!("Failed to add city: {}.", e);
272 }
273 }
274
275 Self::unset_busy(&window);
276 }
277
278 fn reorder_cities(
279 window_weak: &Weak<AppWindow>,
280 data_controller: &WeatherControllerSharedPointer,
281 index: i32,
282 new_index: i32,
283 ) -> Result<(), Box<dyn std::error::Error>> {
284 let pos: usize = index.try_into()?;
285 let new_pos: usize = new_index.try_into()?;
286
287 data_controller.lock().unwrap().reorder_cities(pos, new_pos)?;
288
289 // update ui
290 let window = window_weak.upgrade().unwrap();
291 let city_weather = window.global::<CityWeather>();
292 let city_weather_list = city_weather.get_city_weather();
293
294 let pos_data = city_weather_list.row_data(pos).ok_or(Box::new(std::io::Error::new(
295 std::io::ErrorKind::InvalidInput,
296 "Index out of bounds",
297 )))?;
298 let new_pos_data = city_weather_list.row_data(new_pos).ok_or(Box::new(
299 std::io::Error::new(std::io::ErrorKind::InvalidInput, "Index out of bounds"),
300 ))?;
301
302 city_weather_list.set_row_data(pos, new_pos_data);
303 city_weather_list.set_row_data(new_pos, pos_data);
304 Ok(())
305 }
306
307 fn remove_city(
308 window_weak: &Weak<AppWindow>,
309 data_controller: &WeatherControllerSharedPointer,
310 index: i32,
311 ) -> Result<(), Box<dyn std::error::Error>> {
312 let pos: usize = index.try_into()?;
313
314 data_controller.lock().unwrap().remove_city(pos)?;
315
316 // update ui
317 let window = window_weak.upgrade().unwrap();
318 let city_weather = window.global::<CityWeather>();
319 let city_weather_list = city_weather.get_city_weather();
320
321 let model = city_weather_list
322 .as_any()
323 .downcast_ref::<slint::VecModel<CityWeatherInfo>>()
324 .expect("CityWeatherInfo model is not provided!");
325
326 model.remove(pos);
327 Ok(())
328 }
329
330 fn search_location(
331 window_weak: &Weak<AppWindow>,
332 data_controller: &WeatherControllerSharedPointer,
333 query: slint::SharedString,
334 ) {
335 let window_weak = window_weak.clone();
336 let data_controller = data_controller.clone();
337 let query = query.to_string();
338
339 spawn_task(async move {
340 let locations_res =
341 async { data_controller.lock().unwrap().search_location(query) }.await;
342
343 let locations = match locations_res {
344 Ok(locations) => Some(locations),
345 Err(e) => {
346 log::warn!("Failed to search for location: {}.", e);
347 None
348 }
349 };
350
351 Self::check_update_error(window_weak.upgrade_in_event_loop(move |window| {
352 if let Some(locations) = locations {
353 WeatherDisplayController::update_location_search_results(&window, locations);
354 }
355 }));
356 });
357 }
358
359 fn update_displayed_cities(window: &AppWindow, data: Vec<CityWeatherData>) {
360 let display_vector: Vec<CityWeatherInfo> =
361 data.iter().map(Self::city_weather_info_from_data).collect();
362
363 let city_weather = window.global::<CityWeather>().get_city_weather();
364 let model = city_weather
365 .as_any()
366 .downcast_ref::<VecModel<CityWeatherInfo>>()
367 .expect("City weather model not set.");
368
369 model.set_vec(display_vector);
370 }
371
372 fn update_location_search_results(window: &AppWindow, result: Vec<GeoLocationData>) {
373 let display_vector: Vec<GeoLocationEntry> =
374 result.iter().map(Self::geo_location_entry_from_data).collect();
375
376 let geo_location = window.global::<GeoLocation>().get_result_list();
377 let model = geo_location
378 .as_any()
379 .downcast_ref::<VecModel<GeoLocationEntry>>()
380 .expect("Geo location entry model not set.");
381
382 model.set_vec(display_vector);
383 }
384
385 fn set_busy(window: &AppWindow) {
386 window.global::<BusyLayerController>().invoke_set_busy();
387 }
388
389 fn unset_busy(window: &AppWindow) {
390 window.global::<BusyLayerController>().invoke_unset_busy();
391 }
392
393 fn check_update_error<E: std::fmt::Display>(result: Result<(), E>) {
394 if let Err(e) = result {
395 log::error!("Error while updating UI: {}", e);
396 }
397 }
398
399 fn icon_type_from_condition(condition: &WeatherCondition) -> IconType {
400 match condition {
401 WeatherCondition::Sunny => IconType::Sunny,
402 WeatherCondition::PartiallyCloudy => IconType::PartiallyCloudy,
403 WeatherCondition::MostlyCloudy => IconType::MostlyCloudy,
404 WeatherCondition::Cloudy => IconType::Cloudy,
405 WeatherCondition::SunnyRainy => IconType::SunnyRainy,
406 WeatherCondition::Rainy => IconType::Rainy,
407 WeatherCondition::Stormy => IconType::Stormy,
408 WeatherCondition::Snowy => IconType::Snowy,
409 WeatherCondition::Foggy => IconType::Foggy,
410 _ => IconType::Unknown,
411 }
412 }
413
414 fn weather_info_from_data(data: &DayWeatherData) -> WeatherInfo {
415 WeatherInfo {
416 description: SharedString::from(&data.description),
417 icon_type: Self::icon_type_from_condition(&data.condition),
418 current_temp: data.current_temperature as f32,
419 detailed_temp: TemperatureInfo {
420 min: data.detailed_temperature.min as f32,
421 max: data.detailed_temperature.max as f32,
422
423 morning: data.detailed_temperature.morning as f32,
424 day: data.detailed_temperature.day as f32,
425 evening: data.detailed_temperature.evening as f32,
426 night: data.detailed_temperature.night as f32,
427 },
428 uv: data.uv_index as i32,
429 precipitation_prob: data.precipitation.probability as f32,
430 rain: data.precipitation.rain_volume as f32,
431 snow: data.precipitation.snow_volume as f32,
432 }
433 }
434
435 fn forecast_weather_info_from_data(data: &[ForecastWeatherData]) -> Vec<WeatherForecastInfo> {
436 data.iter()
437 .map(|forecast_data| WeatherForecastInfo {
438 day_name: SharedString::from(&forecast_data.day_name),
439 weather_info: Self::weather_info_from_data(&forecast_data.weather_data),
440 })
441 .collect()
442 }
443
444 fn city_weather_info_from_data(data: &CityWeatherData) -> CityWeatherInfo {
445 let current_weather_info = Self::weather_info_from_data(&data.weather_data.current_data);
446 let forecast_weather_info =
447 Self::forecast_weather_info_from_data(&data.weather_data.forecast_data);
448
449 CityWeatherInfo {
450 city_name: SharedString::from(&data.city_data.city_name),
451 current_weather: current_weather_info,
452 forecast_weather: Rc::new(slint::VecModel::from(forecast_weather_info)).into(),
453 }
454 }
455
456 fn geo_location_entry_from_data(data: &GeoLocationData) -> GeoLocationEntry {
457 GeoLocationEntry {
458 name: SharedString::from(&data.name),
459 state: SharedString::from(data.state.as_deref().unwrap_or_default()),
460 country: SharedString::from(&data.country),
461 lat: data.lat as f32,
462 lon: data.lon as f32,
463 }
464 }
465}
466