From 23a9de1108a0a7a0f833188230fa671c50705231 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Mon, 11 Dec 2017 23:10:55 -0200 Subject: [PATCH] weather-service: major reorganization and cleanup To make it more like the actual code style of GNOME Calendar. Hope Florian doesn't get (too) mad of me removing and modifying so much on his code. I'm sorry dude, my OCD didn't allow me to just flood the gate. --- src/gcal-weather-service.c | 2181 +++++++++++++++--------------------- 1 file changed, 912 insertions(+), 1269 deletions(-) diff --git a/src/gcal-weather-service.c b/src/gcal-weather-service.c index aa39598c..28429f3e 100644 --- a/src/gcal-weather-service.c +++ b/src/gcal-weather-service.c @@ -16,7 +16,8 @@ * along with this program. If not, see . */ -#define G_LOG_DOMAIN "Weather" +#define G_LOG_DOMAIN "GcalWeatherService" + #define DESKTOP_FILE_NAME "org.gnome.Calendar" #include @@ -28,8 +29,7 @@ #include "gcal-timer.h" -G_BEGIN_DECLS - +#define DAY_SECONDS (24 * 60 * 60) /** * Internal structure used to manage known @@ -75,10 +75,7 @@ struct _GcalWeatherService { GObjectClass parent; - /* */ - - /* */ - GTimeZone *time_zone; /* owned, nullable */ + GTimeZone *timezone; /* owned, nullable */ /* timer: */ guint check_interval_new; @@ -103,101 +100,23 @@ struct _GcalWeatherService gboolean weather_service_running; }; - -/* Auxiliary methods: */ -static void on_network_change (GNetworkMonitor *monitor, - gboolean available, +static void on_gweather_update_cb (GWeatherInfo *info, GcalWeatherService *self); -static void on_gclue_simple_creation (GClueSimple *source, - GAsyncResult *result, - GcalWeatherService *data); - -static void on_gclue_location_changed (GClueLocation *location, - GcalWeatherService *self); - -static void on_gclue_client_activity_changed (GClueClient *client, - GcalWeatherService *self); - -static void on_gclue_client_stop (GClueClient *client, - GAsyncResult *res, - GClueSimple *simple); - -static void on_gweather_update (GWeatherInfo *info, - GcalWeatherService *self); - -static void on_duration_timer_timeout (GcalTimer *timer, - GcalWeatherService *self); - -static void on_midnight_timer_timeout (GcalTimer *timer, - GcalWeatherService *self); - - -/* Timer Helpers: */ -static void update_timeout_interval (GcalWeatherService *self); - static void schedule_midnight (GcalWeatherService *self); static void start_timer (GcalWeatherService *self); static void stop_timer (GcalWeatherService *self); - -/* Internal location API and callbacks: */ - -static void update_location (GcalWeatherService *self, - GWeatherLocation *location); - -static void update_gclue_location (GcalWeatherService *self, - GClueLocation *location); - - -/* Internal Weather API */ -static void set_max_days (GcalWeatherService *self, - guint days); - -static void set_valid_timespan (GcalWeatherService *self, - gint64 timespan); - static gboolean has_valid_weather_infos (GcalWeatherService *self); static void update_weather (GcalWeatherService *self, GWeatherInfo *info, gboolean reuse_old_on_error); - -/* Internal weather update API and callbacks */ -static void set_check_interval_new (GcalWeatherService *self, - guint check_interval); - -static void set_check_interval_renew (GcalWeatherService *self, - guint check_interval); - static gssize get_normalized_icon_name_len (const gchar *str); -static gchar* get_normalized_icon_name (GWeatherInfo *gwi, - gboolean is_night_icon); - -static gint get_icon_name_sortkey (const gchar *icon_name, - gboolean *supports_night_icon); - -static gboolean get_time_day_start (GcalWeatherService *self, - GDate *ret_date, - gint64 *ret_unix, - gint64 *ret_unix_exact); - -static inline gboolean get_gweather_temperature (GWeatherInfo *gwi, - gdouble *temp); - -static gboolean compute_weather_info_data (GSList *samples, - gboolean is_today, - gchar **icon_name, - gchar **temperature); - -static GSList* preprocess_gweather_reports (GcalWeatherService *self, - GSList *samples); - - G_DEFINE_TYPE (GcalWeatherService, gcal_weather_service, G_TYPE_OBJECT) enum @@ -214,55 +133,844 @@ enum enum { SIG_WEATHER_CHANGED, - SIG_NUM, + N_SIGNALS, }; -static guint gcal_weather_service_signals[SIG_NUM] = { 0 }; - -G_END_DECLS +static guint signals[N_SIGNALS] = { 0 }; -/******************** - * < gobject setup > - *******************/ +/* + * Auxiliary methods + */ -static void -gcal_weather_service_finalize (GObject *object) +static gchar* +get_normalized_icon_name (GWeatherInfo* wi, + gboolean is_night_icon) { - GcalWeatherService *self; /* unowned */ + const gchar night_pfx[] = "-night"; + const gsize night_pfx_size = G_N_ELEMENTS (night_pfx) - 1; - self = (GcalWeatherService *) object; + const gchar sym_pfx[] = "-symbolic"; + const gsize sym_pfx_size = G_N_ELEMENTS (sym_pfx) - 1; - gcal_timer_free (self->duration_timer); - self->duration_timer = NULL; + const gchar *str; /* unowned */ + gssize normalized_size; + gchar *buffer = NULL; /* owned */ + gchar *bufpos = NULL; /* unowned */ + gsize buffer_size; - gcal_timer_free (self->midnight_timer); - self->midnight_timer = NULL; + g_return_val_if_fail (wi != NULL, NULL); - if (self->time_zone != NULL) + str = gweather_info_get_icon_name (wi); + if (!str) + return NULL; + + normalized_size = get_normalized_icon_name_len (str); + g_return_val_if_fail (normalized_size >= 0, NULL); + + if (is_night_icon) + buffer_size = normalized_size + night_pfx_size + sym_pfx_size + 1; + else + buffer_size = normalized_size + sym_pfx_size + 1; + + buffer = g_malloc (buffer_size); + bufpos = buffer; + + memcpy (bufpos, str, normalized_size); + bufpos = bufpos + normalized_size; + + if (is_night_icon) { - g_time_zone_unref (self->time_zone); - self->time_zone = NULL; + memcpy (bufpos, night_pfx, night_pfx_size); + bufpos = bufpos + night_pfx_size; } - if (self->location_service != NULL) - g_clear_object (&self->location_service); + memcpy (bufpos, sym_pfx, sym_pfx_size); + buffer[buffer_size - 1] = '\0'; - g_cancellable_cancel (self->location_cancellable); - g_clear_object (&self->location_cancellable); + return buffer; +} - g_slist_free_full (self->weather_infos, g_object_unref); +static void +update_timeout_interval (GcalWeatherService *self) +{ + guint interval; + + if (has_valid_weather_infos (self)) + interval = self->check_interval_renew; + else + interval = self->check_interval_new; + + gcal_timer_set_default_duration (self->duration_timer, interval); +} + +static void +schedule_midnight (GcalWeatherService *self) +{ + g_autoptr (GTimeZone) zone = NULL; + g_autoptr (GDateTime) now = NULL; + g_autoptr (GDateTime) tom = NULL; + g_autoptr (GDateTime) mid = NULL; + gint64 real_now; + gint64 real_mid; + + g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); + + zone = !self->timezone ? g_time_zone_new_local () : g_time_zone_ref (self->timezone); + + now = g_date_time_new_now (zone); + tom = g_date_time_add_days (now, 1); + mid = g_date_time_new (zone, + g_date_time_get_year (tom), + g_date_time_get_month (tom), + g_date_time_get_day_of_month (tom), + 0, 0, 0); + + real_mid = g_date_time_to_unix (mid); + real_now = g_date_time_to_unix (now); + + gcal_timer_set_default_duration (self->midnight_timer, + real_mid - real_now); +} + +static void +start_timer (GcalWeatherService *self) +{ + GNetworkMonitor *monitor; /* unowned */ + + monitor = g_network_monitor_get_default (); + if (g_network_monitor_get_network_available (monitor)) + { + update_timeout_interval (self); + gcal_timer_start (self->duration_timer); + + schedule_midnight (self); + gcal_timer_start (self->midnight_timer); + } +} + +static void +stop_timer (GcalWeatherService *self) +{ + gcal_timer_stop (self->duration_timer); + gcal_timer_stop (self->midnight_timer); +} + +static void +set_check_interval_new (GcalWeatherService *self, + guint interval) +{ + self->check_interval_new = interval; + update_timeout_interval (self); + + g_object_notify ((GObject*) self, "check-interval-new"); +} + +static void +set_check_interval_renew (GcalWeatherService *self, + guint interval) +{ + self->check_interval_renew = interval; + update_timeout_interval (self); + + g_object_notify ((GObject*) self, "check-interval-renew"); +} + +static gssize +get_normalized_icon_name_len (const gchar *str) +{ + const gchar *suffix1 = "-symbolic"; + const gssize suffix1_len = strlen (suffix1); + + const gchar *suffix2 = "-night"; + const gssize suffix2_len = strlen (suffix2); + + gssize clean_len; + gssize str_len; + + str_len = strlen (str); + + clean_len = str_len - suffix1_len; + if (clean_len >= 0 && memcmp (suffix1, str + clean_len, suffix1_len) == 0) + str_len = clean_len; + + clean_len = str_len - suffix2_len; + if (clean_len >= 0 && memcmp (suffix2, str + clean_len, suffix2_len) == 0) + str_len = clean_len; + + return str_len; +} + +static gint +get_icon_name_sortkey (const gchar *icon_name, + gboolean *supports_night_icon) +{ + /* Note that we can't use gweathers condition + * field as it is not necessarily holds valid values. + * libgweather uses its own heuristic to determine + * the icon to use. String matching is still better + * than copying their algorithm, I guess. + */ + + gssize normalized_name_len; + + const GcalWeatherIconInfo icons[] = + { {"weather-clear", TRUE}, + {"weather-few-clouds", TRUE}, + {"weather-overcast", FALSE}, + {"weather-fog", FALSE}, + {"weather-showers-scattered", FALSE}, + {"weather-showers", FALSE}, + {"weather-snow", FALSE}, + {"weather-storm", FALSE}, + {"weather-severe-alert", FALSE} + }; + + g_return_val_if_fail (icon_name != NULL, -1); + g_return_val_if_fail (supports_night_icon != NULL, -1); + + *supports_night_icon = FALSE; + + normalized_name_len = get_normalized_icon_name_len (icon_name); + g_return_val_if_fail (normalized_name_len >= 0, -1); + + for (int i = 0; i < G_N_ELEMENTS (icons); i++) + { + if (icons[i].name[normalized_name_len] == '\0' && strncmp (icon_name, icons[i].name, normalized_name_len) == 0) + { + *supports_night_icon = icons[i].night_support; + return i; + } + } + + g_warning ("Unknown weather icon '%s'", icon_name); + + return -1; +} + +static gboolean +get_time_day_start (GcalWeatherService *self, + GDate *ret_date, + gint64 *ret_unix, + gint64 *ret_unix_exact) +{ + g_autoptr (GTimeZone) zone = NULL; + g_autoptr (GDateTime) now = NULL; + g_autoptr (GDateTime) day = NULL; + + g_return_val_if_fail (self != NULL, FALSE); + g_return_val_if_fail (ret_date != NULL, FALSE); + g_return_val_if_fail (ret_unix != NULL, FALSE); + g_return_val_if_fail (ret_unix_exact != NULL, FALSE); + + zone = !self->timezone ? g_time_zone_new_local () : g_time_zone_ref (self->timezone); + + now = g_date_time_new_now (zone); + day = g_date_time_new (zone, + g_date_time_get_year (now), + g_date_time_get_month (now), + g_date_time_get_day_of_month (now), + 0, 0, 0); + + g_date_set_dmy (ret_date, + g_date_time_get_day_of_month (day), + g_date_time_get_month (day), + g_date_time_get_year (day)); + + *ret_unix = g_date_time_to_unix (day); + *ret_unix_exact = g_date_time_to_unix (now); + return TRUE; +} + +static inline gboolean +get_gweather_temperature (GWeatherInfo *gwi, + gdouble *temp) +{ + gboolean valid; + gdouble value; + + *temp = NAN; + + g_return_val_if_fail (gwi != NULL, FALSE); + g_return_val_if_fail (temp != NULL, FALSE); + + valid = gweather_info_get_value_temp (gwi, + GWEATHER_TEMP_UNIT_DEFAULT, + &value); + + /* TODO: Extract temperatures in Celsius and catch implausible cases */ + if (valid) + { + *temp = value; + return TRUE; + } + else + { + *temp = NAN; + return FALSE; + } +} + +static gboolean +compute_weather_info_data (GSList *samples, + gboolean is_today, + gchar **icon_name, + gchar **temperature) +{ + GWeatherInfo *phenomenon_gwi = NULL; + GWeatherInfo *temp_gwi = NULL; + GSList *iter; + gboolean phenomenon_supports_night_icon; + gboolean has_daytime; + gdouble temp_val; + gint phenomenon_val; + + temp_val = NAN; + has_daytime = FALSE; + phenomenon_val = -1; + phenomenon_supports_night_icon = FALSE; + + /* Note: I checked three different gweather consumers + * and they all pick different values. So here is my + * take: I pick up the worst weather for icons and + * the highest temperature. I basically want to know + * whether I need my umbrella for my appointment. + * Not sure about the right temperature. It is probably + * better to pick-up the median of all predictions + * during daytime. + */ + + for (iter = samples; iter; iter = iter->next) + { + GWeatherInfo *gwi; /* unowned */ + const gchar *icon_name; /* unowned */ + gboolean supports_night_icon; + gboolean valid_temp; + gdouble temp; + gint phenomenon; + + gwi = GWEATHER_INFO (iter->data); + phenomenon = -1; + + icon_name = gweather_info_get_icon_name (gwi); + if (icon_name) + phenomenon = get_icon_name_sortkey (icon_name, &supports_night_icon); + + valid_temp = get_gweather_temperature (gwi, &temp); + + if (phenomenon >= 0 && (phenomenon_gwi == NULL || phenomenon > phenomenon_val)) + { + phenomenon_supports_night_icon = supports_night_icon; + phenomenon_val = phenomenon; + phenomenon_gwi = gwi; + } + + if (valid_temp && (!temp_gwi || temp > temp_val)) + { + temp_val = temp; + temp_gwi = gwi; + } + + if (gweather_info_is_daytime (gwi)) + has_daytime = TRUE; + } + + if (phenomenon_gwi && temp_gwi) + { + *icon_name = get_normalized_icon_name (phenomenon_gwi, is_today && !has_daytime && phenomenon_supports_night_icon); + *temperature = gweather_info_get_temp (temp_gwi); + return TRUE; + } + else + { + /* empty list */ + *icon_name = NULL; + *temperature = NULL; + return FALSE; + } +} + + +static void +update_location (GcalWeatherService *self, + GWeatherLocation *location) +{ + g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); + + if (gcal_timer_is_running (self->duration_timer)) + stop_timer (self); if (self->gweather_info != NULL) g_clear_object (&self->gweather_info); - if (self->network_changed_sid > 0) + if (location == NULL) { - GNetworkMonitor *monitor; /* unowned */ - - monitor = g_network_monitor_get_default (); - g_signal_handler_disconnect (monitor, self->network_changed_sid); + g_debug ("Could not retrieve current location"); + update_weather (self, NULL, FALSE); } + else + { + g_debug ("Got new weather service location: '%s'", + (location == NULL)? "" : gweather_location_get_name (location)); + + self->gweather_info = gweather_info_new (location, GWEATHER_FORECAST_ZONE | GWEATHER_FORECAST_LIST); + + /* NOTE: We do not get detailed infos for GWEATHER_PROVIDER_ALL. + * This combination works fine, though. We should open a bug / investigate + * what is going on. + */ + gweather_info_set_enabled_providers (self->gweather_info, GWEATHER_PROVIDER_METAR | GWEATHER_PROVIDER_OWM | GWEATHER_PROVIDER_YR_NO); + g_signal_connect (self->gweather_info, "updated", (GCallback) on_gweather_update_cb, self); + + /* gweather_info_update might or might not trigger a + * GWeatherInfo::update() signal. Therefore, we have to + * remove weather information before querying new one. + * This might result in icon flickering on screen. + * We probably want to introduce a "unknown" or "loading" + * state in gweather-info to soften the effect. + */ + update_weather (self, NULL, FALSE); + gweather_info_update (self->gweather_info); + + start_timer (self); + } +} + +static void +update_gclue_location (GcalWeatherService *self, + GClueLocation *location) +{ + GWeatherLocation *wlocation = NULL; /* owned */ + + if (location) + { + GWeatherLocation *wworld; /* unowned */ + gdouble latitude; + gdouble longitude; + + latitude = gclue_location_get_latitude (location); + longitude = gclue_location_get_longitude (location); + + /* nearest-city works more closely to gnome weather. */ + wworld = gweather_location_get_world (); + wlocation = gweather_location_find_nearest_city (wworld, latitude, longitude); + } + + update_location (self, wlocation); + + g_clear_pointer (&wlocation, gweather_location_unref); +} + +static GSList* +preprocess_gweather_reports (GcalWeatherService *self, + GSList *samples) +{ + GWeatherInfo *first_tomorrow = NULL; /* unowned */ + GSList **days = NULL; /* owned[owned[unowned]] */ + GSList *result = NULL; /* owned[owned] */ + GSList *iter = NULL; /* unowned */ + GDate cur_gdate; + glong first_tomorrow_dtime = -1; + glong today_unix; + glong unix_now; + + /* This function basically separates samples by date and calls compute_weather_info_data + * for every bucket to build weather infos. Each bucket represents a single day. + * + * All gweather consumers I reviewed presume sorted samples. However, there is no documented + * order. Lets assume the worst. + */ + + if (self->max_days <= 0) + return NULL; + + if (!get_time_day_start (self, &cur_gdate, &today_unix, &unix_now)) + return NULL; + + days = g_malloc0 (sizeof (GSList*) * self->max_days); + + /* Split samples to max_days buckets: */ + for (iter = samples; iter != NULL; iter = iter->next) + { + GWeatherInfo *gwi; /* unowned */ + gboolean valid_date; + glong gwi_dtime; + gsize bucket; + + gwi = GWEATHER_INFO (iter->data); + valid_date = gweather_info_get_value_update (gwi, &gwi_dtime); + if (!valid_date) + continue; + + #if PRINT_WEATHER_DATA + { + g_autofree gchar* dbg_str = gwc2str (gwi); + g_message ("WEATHER READING POINT: %s", dbg_str); + } + #endif + + if (gwi_dtime >= 0 && gwi_dtime >= today_unix) + { + bucket = (gwi_dtime - today_unix) / DAY_SECONDS; + if (bucket < self->max_days) + days[bucket] = g_slist_prepend (days[bucket], gwi); + + if (bucket == 1 && (first_tomorrow == NULL || first_tomorrow_dtime > gwi_dtime)) + { + first_tomorrow_dtime = gwi_dtime; + first_tomorrow = gwi; + } + } + else + { + g_debug ("Encountered historic weather information"); + } + } + + if (!days[0] && first_tomorrow) + { + + glong secs_left_today; + glong secs_between; + + /* There is no data point left for today. Lets borrow one. */ + secs_left_today = DAY_SECONDS - (unix_now - today_unix); + secs_between = first_tomorrow_dtime - unix_now; + + if (secs_left_today < 90*60 && secs_between <= 180*60) + days[0] = g_slist_prepend (days[0], first_tomorrow); + } + + /* Produce GcalWeatherInfo for each bucket: */ + for (int i = 0; i < self->max_days; i++) + { + g_autofree gchar *icon_name; + g_autofree gchar *temperature; + + if (compute_weather_info_data (days[i], i == 0, &icon_name, &temperature)) + { + GcalWeatherInfo* gcwi; + + gcwi = gcal_weather_info_new (&cur_gdate, icon_name, temperature); + result = g_slist_prepend (result, g_steal_pointer (&gcwi)); + } + + g_date_add_days (&cur_gdate, 1); + } + + /* Cleanup */ + for (int i = 0; i < self->max_days; i++) + g_slist_free (days[i]); + g_free (days); + + return result; +} + + +/* + * Callbacks + */ + +static void +on_network_changed_cb (GNetworkMonitor *monitor, + gboolean available, + GcalWeatherService *self) +{ + gboolean is_running; + + is_running = gcal_timer_is_running (self->duration_timer); + + if (available && !is_running) + { + if (self->gweather_info != NULL) + gweather_info_update (self->gweather_info); + + start_timer (self); + } + else if (!available && is_running) + { + stop_timer (self); + } +} + +static void +on_gclue_location_changed_cb (GClueLocation *location, + GcalWeatherService *self) +{ + update_gclue_location (self, location); +} + +static void +on_gclue_client_activity_changed_cb (GClueClient *client, + GcalWeatherService *self) +{ + /* Notify listeners about unknown locations: */ + update_location (self, NULL); +} + +static void +on_gclue_simple_creation_cb (GClueSimple *_source, + GAsyncResult *result, + GcalWeatherService *self) +{ + g_autoptr (GError) error = NULL; + GClueLocation *location; /* unowned */ + GClueSimple *location_service; /* owned */ + GClueClient *client; /* unowned */ + + /* make sure we do not touch self->location_service + * if the current operation was cancelled. + */ + location_service = gclue_simple_new_finish (result, &error); + + if (error) + { + g_assert_null (location_service); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + /* Cancelled during creation. Silently fail. */; + else + g_warning ("Could not create GCLueSimple: %s", error->message); + + g_object_unref (self); + return; + } + + self->location_service = g_steal_pointer (&location_service); + + location = gclue_simple_get_location (self->location_service); + client = gclue_simple_get_client (self->location_service); + + if (location) + { + update_gclue_location (self, location); + + g_signal_connect_object (location, + "notify::location", + G_CALLBACK (on_gclue_location_changed_cb), + self, + 0); + } + + g_signal_connect_object (client, + "notify::active", + G_CALLBACK (on_gclue_client_activity_changed_cb), + self, + 0); + + g_object_unref (self); +} + +/* on_gclue_client_stop: + * @source_object: A #GClueClient. + * @res: Result of gclue_client_call_stop(). + * @simple: (transfer full): A #GClueSimple. + * + * Helper-callback used in gcal_weather_service_stop(). + */ +static void +on_gclue_client_stop (GClueClient *client, + GAsyncResult *res, + GClueSimple *simple) +{ + g_autoptr(GError) error = NULL; /* owned */ + gboolean stopped; + + stopped = gclue_client_call_stop_finish (client, + res, + &error); + if (error != NULL) + g_warning ("Could not stop location service: %s", error->message); + else if (!stopped) + g_warning ("Could not stop location service"); + + g_object_unref (simple); +} + +static void +on_gweather_update_cb (GWeatherInfo *info, + GcalWeatherService *self) +{ + update_weather (self, info, TRUE); +} + +/* on_duration_timer_timeout + * @self: A #GcalWeatherService. + * + * Handles scheduled weather report updates. + */ +static void +on_duration_timer_timeout (GcalTimer *timer, + GcalWeatherService *self) +{ + if (self->gweather_info) + gweather_info_update (self->gweather_info); +} + +/* on_midnight_timer_timeout + * @self: A #GcalWeatherService. + * + * Handles scheduled weather report updates. + */ +static void +on_midnight_timer_timeout (GcalTimer *timer, + GcalWeatherService *self) +{ + if (self->gweather_info) + gweather_info_update (self->gweather_info); + + if (gcal_timer_is_running (self->duration_timer)) + gcal_timer_reset (self->duration_timer); + + schedule_midnight (self); +} + +#if PRINT_WEATHER_DATA +static gchar* +gwc2str (GWeatherInfo *gwi) +{ + g_autoptr (GDateTime) date = NULL; + g_autofree gchar *date_str = NULL; + glong update; + + gchar *icon_name; /* unowned */ + gdouble temp; + + if (!gweather_info_get_value_update (gwi, &update)) + return g_strdup (""); + + date = g_date_time_new_from_unix_local (update); + date_str = g_date_time_format (date, "%F %T"), + + get_gweather_temperature (gwi, &temp); + icon_name = gweather_info_get_symbolic_icon_name (gwi); + + return g_strdup_printf ("(%s: t:%f, w:%s)", + date_str, + temp, + icon_name); +} +#endif + +static void +set_max_days (GcalWeatherService *self, + guint days) +{ + g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); + g_return_if_fail (days >= 1); + + self->max_days = days; + + g_object_notify ((GObject*) self, "max-days"); +} + +static void +set_valid_timespan (GcalWeatherService *self, + gint64 timespan) +{ + g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); + g_return_if_fail (timespan >= 0); + + self->valid_timespan = timespan; + + g_object_notify ((GObject*) self, "valid-timespan"); +} + +static gboolean +has_valid_weather_infos (GcalWeatherService *self) +{ + gint64 now; + + g_return_val_if_fail (GCAL_IS_WEATHER_SERVICE (self), FALSE); + + if (self->gweather_info == NULL || self->weather_infos_upated < 0) + return FALSE; + + now = g_get_monotonic_time (); + return (now - self->weather_infos_upated) / 1000000 <= self->valid_timespan; +} + +/* update_weather: + * @self: A #GcalWeatherService instance. + * @info: (nullable): Newly received weather information or %NULL. + * @reuse_old_on_error: Whether to re-use old but not outdated weather + * information in case we could not fetch new data. + * + * Retrieves weather information for @location and triggers + * #GcalWeatherService::weather-changed. + */ +static void +update_weather (GcalWeatherService *self, + GWeatherInfo *info, + gboolean reuse_old_on_error) +{ + GSList *gwforecast = NULL; /* unowned */ + + g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); + g_return_if_fail (info == NULL || GWEATHER_IS_INFO (info)); + + + /* Compute a list of newly received weather infos. */ + if (!info) + { + g_debug ("Could not retrieve valid weather"); + } + else if (gweather_info_is_valid (info)) + { + g_debug ("Received valid weather information"); + gwforecast = gweather_info_get_forecast_list (info); + } + else + { + g_autofree gchar* location_name = gweather_info_get_location_name (info); + g_debug ("Could not retrieve valid weather for location '%s'", location_name); + } + + if (!gwforecast && self->weather_infos_upated >= 0) + { + if (!reuse_old_on_error || !has_valid_weather_infos (self)) + { + g_slist_free_full (self->weather_infos, g_object_unref); + self->weather_infos = NULL; + self->weather_infos_upated = -1; + + g_signal_emit (self, signals[SIG_WEATHER_CHANGED], 0); + } + } + else if (gwforecast) + { + g_slist_free_full (self->weather_infos, g_object_unref); + self->weather_infos = preprocess_gweather_reports (self, gwforecast); + self->weather_infos_upated = g_get_monotonic_time (); + + g_signal_emit (self, signals[SIG_WEATHER_CHANGED], 0); + } +} + + +/* + * GObject overrides + */ + +static void +gcal_weather_service_finalize (GObject *object) +{ + GcalWeatherService *self = (GcalWeatherService *) object; + + g_cancellable_cancel (self->location_cancellable); + + g_clear_pointer (&self->duration_timer, gcal_timer_free); + g_clear_pointer (&self->midnight_timer, gcal_timer_free); + g_clear_pointer (&self->timezone, g_time_zone_unref); + + g_clear_object (&self->gweather_info); + g_clear_object (&self->location_service); + g_clear_object (&self->location_cancellable); + + g_slist_free_full (self->weather_infos, g_object_unref); + + if (self->network_changed_sid > 0) + g_signal_handler_disconnect (g_network_monitor_get_default (), self->network_changed_sid); G_OBJECT_CLASS (gcal_weather_service_parent_class)->finalize (object); } @@ -273,30 +981,34 @@ gcal_weather_service_get_property (GObject *object, GValue *value, GParamSpec *pspec) { - GcalWeatherService* self; /* unowned */ + GcalWeatherService *self = (GcalWeatherService *) object; - self = GCAL_WEATHER_SERVICE (object); switch (prop_id) - { - case PROP_MAX_DAYS: - g_value_set_uint (value, gcal_weather_service_get_max_days (self)); - break; - case PROP_TIME_ZONE: - g_value_set_pointer (value, gcal_weather_service_get_time_zone (self)); - break; - case PROP_CHECK_INTERVAL_NEW: - g_value_set_uint (value, gcal_weather_service_get_check_interval_new (self)); - break; - case PROP_CHECK_INTERVAL_RENEW: - g_value_set_uint (value, gcal_weather_service_get_check_interval_renew (self)); - break; - case PROP_VALID_TIMESPAN: - g_value_set_int64 (value, gcal_weather_service_get_valid_timespan (self)); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } + { + case PROP_MAX_DAYS: + g_value_set_uint (value, gcal_weather_service_get_max_days (self)); + break; + + case PROP_TIME_ZONE: + g_value_set_pointer (value, gcal_weather_service_get_time_zone (self)); + break; + + case PROP_CHECK_INTERVAL_NEW: + g_value_set_uint (value, gcal_weather_service_get_check_interval_new (self)); + break; + + case PROP_CHECK_INTERVAL_RENEW: + g_value_set_uint (value, gcal_weather_service_get_check_interval_renew (self)); + break; + + case PROP_VALID_TIMESPAN: + g_value_set_int64 (value, gcal_weather_service_get_valid_timespan (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } } static void @@ -305,36 +1017,40 @@ gcal_weather_service_set_property (GObject *object, const GValue *value, GParamSpec *pspec) { - GcalWeatherService* self; /* unowned */ + GcalWeatherService *self = (GcalWeatherService *) object; - self = GCAL_WEATHER_SERVICE (object); switch (prop_id) - { - case PROP_MAX_DAYS: - set_max_days (self, g_value_get_uint (value)); - break; - case PROP_TIME_ZONE: - gcal_weather_service_set_time_zone (self, g_value_get_pointer (value)); - break; - case PROP_CHECK_INTERVAL_NEW: - set_check_interval_new (self, g_value_get_uint (value)); - break; - case PROP_CHECK_INTERVAL_RENEW: - set_check_interval_renew (self, g_value_get_uint (value)); - break; - case PROP_VALID_TIMESPAN: - set_valid_timespan (self, g_value_get_int64 (value)); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } + { + case PROP_MAX_DAYS: + set_max_days (self, g_value_get_uint (value)); + break; + + case PROP_TIME_ZONE: + gcal_weather_service_set_time_zone (self, g_value_get_pointer (value)); + break; + + case PROP_CHECK_INTERVAL_NEW: + set_check_interval_new (self, g_value_get_uint (value)); + break; + + case PROP_CHECK_INTERVAL_RENEW: + set_check_interval_renew (self, g_value_get_uint (value)); + break; + + case PROP_VALID_TIMESPAN: + set_valid_timespan (self, g_value_get_int64 (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } } static void gcal_weather_service_class_init (GcalWeatherServiceClass *klass) { - GObjectClass *object_class; /* unowned */ + GObjectClass *object_class; object_class = G_OBJECT_CLASS (klass); object_class->finalize = gcal_weather_service_finalize; @@ -412,1105 +1128,38 @@ gcal_weather_service_class_init (GcalWeatherServiceClass *klass) * gcal_weather_service_get_weather_infos() to * retrieve predictions. */ - gcal_weather_service_signals[SIG_WEATHER_CHANGED] - = g_signal_new ("weather-changed", - GCAL_TYPE_WEATHER_SERVICE, - G_SIGNAL_RUN_LAST, - 0, - NULL, - NULL, - g_cclosure_marshal_VOID__VOID, - G_TYPE_NONE, - 0); + signals[SIG_WEATHER_CHANGED] = g_signal_new ("weather-changed", + GCAL_TYPE_WEATHER_SERVICE, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); } static void gcal_weather_service_init (GcalWeatherService *self) { - GNetworkMonitor *monitor; /* unowned */ - - self->duration_timer = gcal_timer_new (GCAL_WEATHER_CHECK_INTERVAL_NEW_DFLT); - gcal_timer_set_callback (self->duration_timer, (GCalTimerFunc) on_duration_timer_timeout, self, NULL); - self->midnight_timer = gcal_timer_new (24*60*60); - gcal_timer_set_callback (self->midnight_timer, (GCalTimerFunc) on_midnight_timer_timeout, self, NULL); - self->time_zone = NULL; self->check_interval_new = GCAL_WEATHER_CHECK_INTERVAL_NEW_DFLT; self->check_interval_renew = GCAL_WEATHER_CHECK_INTERVAL_RENEW_DFLT; self->location_cancellable = g_cancellable_new (); - self->location_service_running = FALSE; - self->location_service = NULL; - self->weather_infos = NULL; self->weather_infos_upated = -1; self->valid_timespan = -1; - self->gweather_info = NULL; - self->weather_service_running = FALSE; - self->max_days = 0; - monitor = g_network_monitor_get_default (); - self->network_changed_sid = g_signal_connect (monitor, + self->duration_timer = gcal_timer_new (GCAL_WEATHER_CHECK_INTERVAL_NEW_DFLT); + gcal_timer_set_callback (self->duration_timer, (GCalTimerFunc) on_duration_timer_timeout, self, NULL); + + self->midnight_timer = gcal_timer_new (24 * 60 * 60); + gcal_timer_set_callback (self->midnight_timer, (GCalTimerFunc) on_midnight_timer_timeout, self, NULL); + + self->network_changed_sid = g_signal_connect (g_network_monitor_get_default (), "network-changed", - (GCallback) on_network_change, + G_CALLBACK (on_network_changed_cb), self); } -/* get_normalized_icon_name: - * @str: A icon name to normalize. - * - * Translates a given weather icon name to the - * one to display for folded weather reports. - * - * Returns: (transfer full): A normalized icon name. - */ -static gchar* -get_normalized_icon_name (GWeatherInfo* wi, - gboolean is_night_icon) -{ - const gchar night_pfx[] = "-night"; - const gsize night_pfx_size = G_N_ELEMENTS (night_pfx) - 1; - - const gchar sym_pfx[] = "-symbolic"; - const gsize sym_pfx_size = G_N_ELEMENTS (sym_pfx) - 1; - - const gchar *str; /* unowned */ - gssize normalized_size; - gchar *buffer = NULL; /* owned */ - gchar *bufpos = NULL; /* unowned */ - gsize buffer_size; - - g_return_val_if_fail (wi != NULL, NULL); - - str = gweather_info_get_icon_name (wi); - if (str == NULL) - return NULL; - - normalized_size = get_normalized_icon_name_len (str); - g_return_val_if_fail (normalized_size >= 0, NULL); - - if (is_night_icon) - buffer_size = normalized_size + night_pfx_size + sym_pfx_size + 1; - else - buffer_size = normalized_size + sym_pfx_size + 1; - - buffer = g_malloc (buffer_size); - bufpos = buffer; - - memcpy (bufpos, str, normalized_size); - bufpos = bufpos + normalized_size; - - if (is_night_icon) - { - memcpy (bufpos, night_pfx, night_pfx_size); - bufpos = bufpos + night_pfx_size; - } - - memcpy (bufpos, sym_pfx, sym_pfx_size); - buffer[buffer_size - 1] = '\0'; - - return buffer; -} - - -/************** - * < private > - **************/ - -/* - * Auxiliary methods: - */ - -/* on_network_change: - * @monitor: The emitting #GNetworkMonitor - * @available: The current value of “network-available” - * @self: The #GcalWeatherService instance. - * - * Starts and stops timer based on monitored network - * changes. - */ -static void -on_network_change (GNetworkMonitor *monitor, - gboolean available, - GcalWeatherService *self) -{ - gboolean is_running; - - g_return_if_fail (G_IS_NETWORK_MONITOR (monitor)); - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - - g_debug ("network changed, available = %d", available); - - is_running = gcal_timer_is_running (self->duration_timer); - if (available && !is_running) - { - if (self->gweather_info != NULL) - gweather_info_update (self->gweather_info); - - start_timer (self); - } - else if (!available && is_running) - { - stop_timer (self); - } -} - -/* on_gclue_simple_creation: - * @source: - * @result: Result of gclue_simple_new(). - * @self: (transfer full): A GcalWeatherService reference. - * - * Callback used in gcal_weather_service_run(). - */ -static void -on_gclue_simple_creation (GClueSimple *_source, - GAsyncResult *result, - GcalWeatherService *self) -{ - GClueSimple *location_service; /* owned */ - GClueLocation *location; /* unowned */ - GClueClient *client; /* unowned */ - g_autoptr (GError) error = NULL; - - g_return_if_fail (G_IS_ASYNC_RESULT (result)); - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - - /* make sure we do not touch self->location_service - * if the current operation was cancelled. - */ - location_service = gclue_simple_new_finish (result, &error); - if (error != NULL) - { - g_assert (location_service == NULL); - - if (error->domain == G_IO_ERROR && error->code == G_IO_ERROR_CANCELLED) - /* Cancelled during creation. Silently fail. */; - else - g_warning ("Could not create GCLueSimple: %s", error->message); - - g_object_unref (self); - return; - } - - g_assert (self->location_service == NULL); - g_assert (location_service != NULL); - - self->location_service = g_steal_pointer (&location_service); - - location = gclue_simple_get_location (self->location_service); - client = gclue_simple_get_client (self->location_service); - - if (location != NULL) - { - update_gclue_location (self, location); - - g_signal_connect_object (location, - "notify::location", - G_CALLBACK (on_gclue_location_changed), - self, - 0); - } - - g_signal_connect_object (client, - "notify::active", - G_CALLBACK (on_gclue_client_activity_changed), - self, - 0); - - g_object_unref (self); -} - -/* on_gclue_location_changed: - * @location: #GClueLocation owned by @self - * @self: The #GcalWeatherService - * - * Handles location changes. - */ -static void -on_gclue_location_changed (GClueLocation *location, - GcalWeatherService *self) -{ - g_return_if_fail (GCLUE_IS_LOCATION (location)); - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - - update_gclue_location (self, location); -} - -/* on_gclue_client_activity_changed: - * @client: The #GClueclient ownd by @self - * @self: The #GcalWeatherService - * - * Handles location client activity changes. - */ -static void -on_gclue_client_activity_changed (GClueClient *client, - GcalWeatherService *self) -{ - g_return_if_fail (GCLUE_IS_CLIENT (client)); - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - - /* Notify listeners about unknown locations: */ - update_location (self, NULL); -} - -/* on_gclue_client_stop: - * @source_object: A #GClueClient. - * @res: Result of gclue_client_call_stop(). - * @simple: (transfer full): A #GClueSimple. - * - * Helper-callback used in gcal_weather_service_stop(). - */ -static void -on_gclue_client_stop (GClueClient *client, - GAsyncResult *res, - GClueSimple *simple) -{ - g_autoptr(GError) error = NULL; /* owned */ - gboolean stopped; - - g_return_if_fail (GCLUE_IS_CLIENT (client)); - g_return_if_fail (G_IS_ASYNC_RESULT (res)); - g_return_if_fail (GCLUE_IS_SIMPLE (simple)); - - stopped = gclue_client_call_stop_finish (client, - res, - &error); - if (error != NULL) - g_warning ("Could not stop location service: %s", error->message); - else if (!stopped) - g_warning ("Could not stop location service"); - - g_object_unref (simple); -} - -/* on_gweather_update: - * @self: A #GcalWeatherService instance. - * @timespan: Amount of seconds we consider weather information as valid. - * - * Triggered on weather updates with previously handled or no - * location changes. - */ -static void -on_gweather_update (GWeatherInfo *info, - GcalWeatherService *self) -{ - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - g_return_if_fail (info == NULL || GWEATHER_IS_INFO (info)); - - update_weather (self, info, TRUE); -} - -/* on_duration_timer_timeout - * @self: A #GcalWeatherService. - * - * Handles scheduled weather report updates. - */ -static void -on_duration_timer_timeout (GcalTimer *timer, - GcalWeatherService *self) -{ - g_return_if_fail (timer != NULL); - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - - if (self->gweather_info != NULL) - gweather_info_update (self->gweather_info); -} - -/* on_midnight_timer_timeout - * @self: A #GcalWeatherService. - * - * Handles scheduled weather report updates. - */ -static void -on_midnight_timer_timeout (GcalTimer *timer, - GcalWeatherService *self) -{ - g_return_if_fail (timer != NULL); - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - - if (self->gweather_info != NULL) - gweather_info_update (self->gweather_info); - - if (gcal_timer_is_running (self->duration_timer)) - gcal_timer_reset (self->duration_timer); - - schedule_midnight (self); -} - - - -/* - * Intenral timer Helpers: - */ - -/* update_timeout_interval: - * @self: The #GcalWeatherService instance. - * - * Selects the right duration timer timeout based - * on locally-stored weather information. - */ -static void -update_timeout_interval (GcalWeatherService *self) -{ - guint interval; - - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - - if (has_valid_weather_infos (self)) - interval = self->check_interval_renew; - else - interval = self->check_interval_new; - - gcal_timer_set_default_duration (self->duration_timer, interval); -} - -/* schedule_midnight: - * @self: The #GcalWeatherService instance. - * - * Sets the midnight timer timeout to midnight. - * The timer needs to be reset when it - * emits. - */ -static void -schedule_midnight (GcalWeatherService *self) -{ - g_autoptr (GTimeZone) zone = NULL; - g_autoptr (GDateTime) now = NULL; - g_autoptr (GDateTime) tom = NULL; - g_autoptr (GDateTime) mid = NULL; - gint64 real_now; - gint64 real_mid; - - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - - zone = (self->time_zone == NULL) - ? g_time_zone_new_local () - : g_time_zone_ref (self->time_zone); - - now = g_date_time_new_now (zone); - tom = g_date_time_add_days (now, 1); - mid = g_date_time_new (zone, - g_date_time_get_year (tom), - g_date_time_get_month (tom), - g_date_time_get_day_of_month (tom), - 0, 0, 0); - - real_mid = g_date_time_to_unix (mid); - real_now = g_date_time_to_unix (now); - - gcal_timer_set_default_duration (self->midnight_timer, - real_mid - real_now); -} - -/* start_timer: - * @self: The #GcalWeatherService instance. - * - * Starts weather timer in case it makes sense. - */ -static void -start_timer (GcalWeatherService *self) -{ - GNetworkMonitor *monitor; /* unowned */ - - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - - monitor = g_network_monitor_get_default (); - if (g_network_monitor_get_network_available (monitor)) - { - update_timeout_interval (self); - gcal_timer_start (self->duration_timer); - - schedule_midnight (self); - gcal_timer_start (self->midnight_timer); - } -} - -/* stop_timer: - * @self: The #GcalWeatherService instance. - * - * Stops the timer. - */ -static void -stop_timer (GcalWeatherService *self) -{ - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - - gcal_timer_stop (self->duration_timer); - gcal_timer_stop (self->midnight_timer); -} - - -/* - * Internal location API: - */ - -/** - * update_location: - * @self: The #GcalWeatherService instance. - * @location: (nullable): The location we want weather information for. - * - * Registers the location to retrieve weather information from. - */ -static void -update_location (GcalWeatherService *self, - GWeatherLocation *location) -{ - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - - if (gcal_timer_is_running (self->duration_timer)) - stop_timer (self); - - if (self->gweather_info != NULL) - g_clear_object (&self->gweather_info); - - if (location == NULL) - { - g_debug ("Could not retrieve current location"); - update_weather (self, NULL, FALSE); - } - else - { - g_debug ("Got new weather service location: '%s'", - (location == NULL)? "" : gweather_location_get_name (location)); - - self->gweather_info = gweather_info_new (location, GWEATHER_FORECAST_ZONE | GWEATHER_FORECAST_LIST); - - /* NOTE: We do not get detailed infos for GWEATHER_PROVIDER_ALL. - * This combination works fine, though. We should open a bug / investigate - * what is going on. - */ - gweather_info_set_enabled_providers (self->gweather_info, GWEATHER_PROVIDER_METAR | GWEATHER_PROVIDER_OWM | GWEATHER_PROVIDER_YR_NO); - g_signal_connect (self->gweather_info, "updated", (GCallback) on_gweather_update, self); - - /* gweather_info_update might or might not trigger a - * GWeatherInfo::update() signal. Therefore, we have to - * remove weather information before querying new one. - * This might result in icon flickering on screen. - * We probably want to introduce a "unknown" or "loading" - * state in gweather-info to soften the effect. - */ - update_weather (self, NULL, FALSE); - gweather_info_update (self->gweather_info); - - start_timer (self); - } -} - -/** - * update_gclue_location: - * @self: The #GcalWeatherService instance. - * @location: (nullable): The location we want weather information for. - * - * Registers the location to retrieve weather information from. - */ -static void -update_gclue_location (GcalWeatherService *self, - GClueLocation *location) -{ - GWeatherLocation *wlocation = NULL; /* owned */ - - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - g_return_if_fail (location == NULL || GCLUE_IS_LOCATION (location)); - - if (location != NULL) - { - GWeatherLocation *wworld; /* unowned */ - gdouble latitude; - gdouble longitude; - - latitude = gclue_location_get_latitude (location); - longitude = gclue_location_get_longitude (location); - - /* nearest-city works more closely to gnome weather. */ - wworld = gweather_location_get_world (); - wlocation = gweather_location_find_nearest_city (wworld, latitude, longitude); - } - - - update_location (self, wlocation); - - if (wlocation != NULL) - gweather_location_unref (wlocation); -} - - - -/* - * Internal Weather API - */ - -#if PRINT_WEATHER_DATA -static gchar* -gwc2str (GWeatherInfo *gwi) -{ - g_autoptr (GDateTime) date = NULL; - g_autofree gchar *date_str = NULL; - glong update; - - gchar *icon_name; /* unowned */ - gdouble temp; - - if (!gweather_info_get_value_update (gwi, &update)) - return g_strdup (""); - - date = g_date_time_new_from_unix_local (update); - date_str = g_date_time_format (date, "%F %T"), - - get_gweather_temperature (gwi, &temp); - icon_name = gweather_info_get_symbolic_icon_name (gwi); - - return g_strdup_printf ("(%s: t:%f, w:%s)", - date_str, - temp, - icon_name); -} -#endif - -/* set_max_days: - * @self: The #GcalWeatherService instance. - * @days: Number of days. - * - * Setter for #GcalWeatherInfos:max-days. - */ -static void -set_max_days (GcalWeatherService *self, - guint days) -{ - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - g_return_if_fail (days >= 1); - - self->max_days = days; - - g_object_notify ((GObject*) self, "max-days"); -} - -/* set_valid_timespan: - * @self: A #GcalWeatherService instance. - * @timespan: Amount of seconds we consider weather information as valid. - */ -static void -set_valid_timespan (GcalWeatherService *self, - gint64 timespan) -{ - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - g_return_if_fail (timespan >= 0); - - self->valid_timespan = timespan; - - g_object_notify ((GObject*) self, "valid-timespan"); -} - -/* has_valid_weather_infos - * @self: A #GcalWeatherService instance. - * - * Checks whether weather information are available - * and up-to-date. - */ -static gboolean -has_valid_weather_infos (GcalWeatherService *self) -{ - gint64 now; - - g_return_val_if_fail (GCAL_IS_WEATHER_SERVICE (self), FALSE); - - if (self->gweather_info == NULL || self->weather_infos_upated < 0) - return FALSE; - - now = g_get_monotonic_time (); - return (now - self->weather_infos_upated) / 1000000 <= self->valid_timespan; -} - -/* update_weather: - * @self: A #GcalWeatherService instance. - * @info: (nullable): Newly received weather information or %NULL. - * @reuse_old_on_error: Whether to re-use old but not outdated weather - * information in case we could not fetch new data. - * - * Retrieves weather information for @location and triggers - * #GcalWeatherService::weather-changed. - */ -static void -update_weather (GcalWeatherService *self, - GWeatherInfo *info, - gboolean reuse_old_on_error) -{ - GSList *gwforecast = NULL; /* unowned */ - - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - g_return_if_fail (info == NULL || GWEATHER_IS_INFO (info)); - - - /* Compute a list of newly received weather infos. */ - if (info == NULL) - { - g_debug ("Could not retrieve valid weather"); - } - else if (gweather_info_is_valid (info)) - { - g_debug ("Received valid weather information"); - gwforecast = gweather_info_get_forecast_list (info); - } - else - { - g_autofree gchar* location_name = gweather_info_get_location_name (info); - g_debug ("Could not retrieve valid weather for location '%s'", location_name); - } - - if (gwforecast == NULL && self->weather_infos_upated >= 0) - { - if (!reuse_old_on_error || !has_valid_weather_infos (self)) - { - g_slist_free_full (self->weather_infos, g_object_unref); - self->weather_infos = NULL; - self->weather_infos_upated = -1; - - g_signal_emit (self, gcal_weather_service_signals[SIG_WEATHER_CHANGED], 0); - } - } - else if (gwforecast != NULL) - { - g_slist_free_full (self->weather_infos, g_object_unref); - self->weather_infos = preprocess_gweather_reports (self, gwforecast); - self->weather_infos_upated = g_get_monotonic_time (); - - g_signal_emit (self, gcal_weather_service_signals[SIG_WEATHER_CHANGED], 0); - } -} - - - -/* - * Internal weather update API and callbacks - */ - -/* set_check_interval_new: - * @self: The #GcalWeatherService instance. - * @days: Number of days. - * - * Setter for GcalWeatherInfos:check-interval-new. - */ -static void -set_check_interval_new (GcalWeatherService *self, - guint interval) -{ - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - g_return_if_fail (interval > 0); - - self->check_interval_new = interval; - update_timeout_interval (self); - - g_object_notify ((GObject*) self, "check-interval-new"); -} - -/* set_check_interval_renew: - * @self: The #GcalWeatherService instance. - * @days: Number of days. - * - * Setter for GcalWeatherInfos:check-interval-renew. - */ -static void -set_check_interval_renew (GcalWeatherService *self, - guint interval) -{ - g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - g_return_if_fail (interval > 0); - - self->check_interval_renew = interval; - update_timeout_interval (self); - - g_object_notify ((GObject*) self, "check-interval-renew"); -} - -/* drop_suffix: - * @str: A icon name to normalize. - * - * Translates a given weather icon name to the - * one to display for folded weather reports. - * - * Returns: Number of initial characters that - * belong to the normalized name. - */ -static gssize -get_normalized_icon_name_len (const gchar *str) -{ - const gchar suffix1[] = "-symbolic"; - const gssize suffix1_len = G_N_ELEMENTS (suffix1) - 1; - - const gchar suffix2[] = "-night"; - const gssize suffix2_len = G_N_ELEMENTS (suffix2) - 1; - - gssize clean_len; - gssize str_len; - - g_return_val_if_fail (str != NULL, -1); - - str_len = strlen (str); - - clean_len = str_len - suffix1_len; - if (clean_len >= 0 && memcmp (suffix1, str + clean_len, suffix1_len) == 0) - str_len = clean_len; - - clean_len = str_len - suffix2_len; - if (clean_len >= 0 && memcmp (suffix2, str + clean_len, suffix2_len) == 0) - str_len = clean_len; - - return str_len; -} - -/* get_icon_name_sortkey: - * - * Returns a sort key for a given weather - * icon name and -1 for unknown ones. - * - * The lower the key, the better the weather. - */ -static gint -get_icon_name_sortkey (const gchar *icon_name, - gboolean *supports_night_icon) -{ - /* Note that we can't use gweathers condition - * field as it is not necessarily holds valid values. - * libgweather uses its own heuristic to determine - * the icon to use. String matching is still better - * than copying their algorithm, I guess. - */ - - gssize normalized_name_len; - - const GcalWeatherIconInfo icons[] = - { {"weather-clear", TRUE}, - {"weather-few-clouds", TRUE}, - {"weather-overcast", FALSE}, - {"weather-fog", FALSE}, - {"weather-showers-scattered", FALSE}, - {"weather-showers", FALSE}, - {"weather-snow", FALSE}, - {"weather-storm", FALSE}, - {"weather-severe-alert", FALSE} - }; - - g_return_val_if_fail (icon_name != NULL, -1); - g_return_val_if_fail (supports_night_icon != NULL, -1); - - *supports_night_icon = FALSE; - - normalized_name_len = get_normalized_icon_name_len (icon_name); - g_return_val_if_fail (normalized_name_len >= 0, -1); - - for (int i = 0; i < G_N_ELEMENTS (icons); i++) - { - if (icons[i].name[normalized_name_len] == '\0' && strncmp (icon_name, icons[i].name, normalized_name_len) == 0) - { - *supports_night_icon = icons[i].night_support; - return i; - } - } - - g_warning ("Unknown weather icon '%s'", icon_name); - - return -1; -} - -/* get_time_day_start: - * @self: The #GcalWeatherService instance. - * @date: (out) (not nullable): A #GDate that should be set to today. - * @unix: (out) (not nullable): A UNIX time stamp that should be set to today. - * @ret_unix_exact (out) (not nullable): The exact date time this data point predicts. - * - * Provides current date in two different forms. - * - * Returns: %TRUE on success. - */ -static gboolean -get_time_day_start (GcalWeatherService *self, - GDate *ret_date, - gint64 *ret_unix, - gint64 *ret_unix_exact) -{ - g_autoptr (GTimeZone) zone = NULL; - g_autoptr (GDateTime) now = NULL; - g_autoptr (GDateTime) day = NULL; - - g_return_val_if_fail (self != NULL, FALSE); - g_return_val_if_fail (ret_date != NULL, FALSE); - g_return_val_if_fail (ret_unix != NULL, FALSE); - g_return_val_if_fail (ret_unix_exact != NULL, FALSE); - - zone = (self->time_zone == NULL) - ? g_time_zone_new_local () - : g_time_zone_ref (self->time_zone); - - now = g_date_time_new_now (zone); - day = g_date_time_new (zone, - g_date_time_get_year (now), - g_date_time_get_month (now), - g_date_time_get_day_of_month (now), - 0, 0, 0); - - g_date_set_dmy (ret_date, - g_date_time_get_day_of_month (day), - g_date_time_get_month (day), - g_date_time_get_year (day)); - - *ret_unix = g_date_time_to_unix (day); - *ret_unix_exact = g_date_time_to_unix (now); - return TRUE; -} - -/* get_gweather_temperature: - * @gwi: #GWeatherInfo to extract temperatures from. - * @temp: (out): extracted temperature or %NAN. - * - * Returns sanitized temperatures. Returned values should only - * be used as sort key. - * - * Returns: %TRUE for valid temperatures. - */ -static inline gboolean -get_gweather_temperature (GWeatherInfo *gwi, - gdouble *temp) -{ - gboolean valid; - gdouble value; - - *temp = NAN; - - g_return_val_if_fail (gwi != NULL, FALSE); - g_return_val_if_fail (temp != NULL, FALSE); - - valid = gweather_info_get_value_temp (gwi, - GWEATHER_TEMP_UNIT_DEFAULT, - &value); - - /* TODO: Extract temperatures in Celsius and catch - * implausible cases. - */ - if (valid) - { - *temp = value; - return TRUE; - } - else - { - *temp = NAN; - return FALSE; - } -} - -/* compute_weather_info_data: - * @samples: List of received #GWeatherInfos. - * @icon_name: (out): (transfer full): weather icon name or %NULL. - * @temperature: (out): (transfer full): temperature and unit or %NULL. - * - * Computes a icon name and temperature representing @samples. - * - * Returns: %TRUE if there is a valid icon name and temperature. - */ -static gboolean -compute_weather_info_data (GSList *samples, - gboolean is_today, - gchar **icon_name, - gchar **temperature) -{ - gboolean phenomenon_supports_night_icon = FALSE; - gint phenomenon_val = -1; - GWeatherInfo *phenomenon_gwi = NULL; /* unowned */ - gdouble temp_val = NAN; - GWeatherInfo *temp_gwi = NULL; /* unowned */ - gboolean has_daytime = FALSE; - GSList *iter; /* unowned */ - - /* Note: I checked three different gweather consumers - * and they all pick different values. So here is my - * take: I pick up the worst weather for icons and - * the highest temperature. I basically want to know - * whether I need my umbrella for my appointment. - * Not sure about the right temperature. It is probably - * better to pick-up the median of all predictions - * during daytime. - */ - - g_return_val_if_fail (icon_name != NULL, FALSE); - g_return_val_if_fail (temperature != NULL, FALSE); - - for (iter = samples; iter != NULL; iter = iter->next) - { - GWeatherInfo *gwi; /* unowned */ - const gchar *icon_name; /* unowned */ - gint phenomenon = -1; - gboolean supports_night_icon; - gdouble temp; - gboolean valid_temp; - - gwi = GWEATHER_INFO (iter->data); - - icon_name = gweather_info_get_icon_name (gwi); - if (icon_name != NULL) - phenomenon = get_icon_name_sortkey (icon_name, &supports_night_icon); - - valid_temp = get_gweather_temperature (gwi, &temp); - - if (phenomenon >= 0 && (phenomenon_gwi == NULL || phenomenon > phenomenon_val)) - { - phenomenon_supports_night_icon = supports_night_icon; - phenomenon_val = phenomenon; - phenomenon_gwi = gwi; - } - - if (valid_temp && (temp_gwi == NULL || temp > temp_val)) - { - temp_val = temp; - temp_gwi = gwi; - } - - if (gweather_info_is_daytime (gwi)) - has_daytime = TRUE; - } - - if (phenomenon_gwi != NULL && temp_gwi != NULL) - { - *icon_name = get_normalized_icon_name (phenomenon_gwi, is_today && !has_daytime && phenomenon_supports_night_icon); - *temperature = gweather_info_get_temp (temp_gwi); - return TRUE; - } - else - { - /* empty list */ - *icon_name = NULL; - *temperature = NULL; - return FALSE; - } -} - -/* preprocess_gweather_reports: - * @self: The #GcalWeatherService instance. - * @samples: Received list of #GWeatherInfos - * - * Computes weather info objects representing specific days - * by combining given #GWeatherInfos. - * - * Returns: (transfer full): List of up to $self->max_days #GcalWeatherInfos. - */ -static GSList* -preprocess_gweather_reports (GcalWeatherService *self, - GSList *samples) -{ - const glong DAY_SECONDS = 24*60*60; - GSList *result = NULL; /* owned[owned] */ - GSList **days; /* owned[owned[unowned]] */ - GSList *iter; /* unowned */ - GDate cur_gdate; - glong today_unix; - glong unix_now; - - GWeatherInfo *first_tomorrow = NULL; /* unowned */ - glong first_tomorrow_dtime = -1; - - g_return_val_if_fail (GCAL_IS_WEATHER_SERVICE (self), NULL); - - /* This function basically separates samples by - * date and calls compute_weather_info_data for - * every bucket to build weather infos. - * Each bucket represents a single day. - */ - - /* All gweather consumers I reviewed presume - * sorted samples. However, there is no documented - * order. Lets assume the worst. - */ - - if (self->max_days <= 0) - return NULL; - - if (!get_time_day_start (self, &cur_gdate, &today_unix, &unix_now)) - return NULL; - - days = g_malloc0 (sizeof (GSList*) * self->max_days); - - /* Split samples to max_days buckets: */ - for (iter = samples; iter != NULL; iter = iter->next) - { - GWeatherInfo *gwi; /* unowned */ - gboolean valid_date; - glong gwi_dtime; - gsize bucket; - - gwi = GWEATHER_INFO (iter->data); - valid_date = gweather_info_get_value_update (gwi, &gwi_dtime); - if (!valid_date) - continue; - - #if PRINT_WEATHER_DATA - { - g_autofree gchar* dbg_str = gwc2str (gwi); - g_message ("WEATHER READING POINT: %s", dbg_str); - } - #endif - - if (gwi_dtime >= 0 && gwi_dtime >= today_unix) - { - bucket = (gwi_dtime - today_unix) / DAY_SECONDS; - if (bucket >= 0 && bucket < self->max_days) - days[bucket] = g_slist_prepend (days[bucket], gwi); - - if (bucket == 1 && (first_tomorrow == NULL || first_tomorrow_dtime > gwi_dtime)) - { - first_tomorrow_dtime = gwi_dtime; - first_tomorrow = gwi; - } - } - else - { - g_debug ("Encountered historic weather information"); - } - } - - if (days[0] == NULL && first_tomorrow != NULL) - { - /* There is no data point left for today. - * Lets borrow one. - */ - glong secs_left_today; - glong secs_between; - - secs_left_today = DAY_SECONDS - (unix_now - today_unix); - secs_between = first_tomorrow_dtime - unix_now; - - if (secs_left_today < 90*60 && secs_between <= 180*60) - days[0] = g_slist_prepend (days[0], first_tomorrow); - } - - /* Produce GcalWeatherInfo for each bucket: */ - for (int i = 0; i < self->max_days; i++) - { - g_autofree gchar *icon_name; - g_autofree gchar *temperature; - - if (compute_weather_info_data (days[i], i == 0, &icon_name, &temperature)) - { - GcalWeatherInfo* gcwi; /* owned */ - - gcwi = gcal_weather_info_new (&cur_gdate, - icon_name, - temperature); - - result = g_slist_prepend (result, - g_steal_pointer (&gcwi)); - } - - g_date_add_days (&cur_gdate, 1); - } - - /* Cleanup: */ - for (int i = 0; i < self->max_days; i++) - g_slist_free (days[i]); - g_free (days); - - return result; -} - - -/************* - * < public > - *************/ - /** * gcal_weather_service_new: * @max_days: mumber of days to fetch forecasts for. @@ -1548,7 +1197,8 @@ GTimeZone* gcal_weather_service_get_time_zone (GcalWeatherService *self) { g_return_val_if_fail (self != NULL, NULL); - return self->time_zone; + + return self->timezone; } /** @@ -1564,25 +1214,19 @@ gcal_weather_service_set_time_zone (GcalWeatherService *self, { g_return_if_fail (self != NULL); - if (self->time_zone != value) - { - if (self->time_zone != NULL) - { - g_time_zone_unref (self->time_zone); - self->time_zone = NULL; - } + if (self->timezone == value) + return; - if (value != NULL) - self->time_zone = g_time_zone_ref (value); + g_clear_pointer (&self->timezone, g_time_zone_unref); + self->timezone = value ? g_time_zone_ref (value) : NULL; - /* make sure we provide correct weather infos */ - gweather_info_update (self->gweather_info); + /* make sure we provide correct weather infos */ + gweather_info_update (self->gweather_info); - /* make sure midnight is timed correctly: */ - schedule_midnight (self); + /* make sure midnight is timed correctly: */ + schedule_midnight (self); - g_object_notify ((GObject*) self, "time-zone"); - } + g_object_notify ((GObject*) self, "time-zone"); } /** @@ -1688,6 +1332,7 @@ gcal_weather_service_update (GcalWeatherService *self) { gweather_info_update (self->gweather_info); update_timeout_interval (self); + if (gcal_timer_is_running (self->duration_timer)) gcal_timer_reset (self->duration_timer); } @@ -1707,22 +1352,24 @@ gcal_weather_service_run (GcalWeatherService *self, { g_return_if_fail (GCAL_IS_WEATHER_SERVICE (self)); - g_debug ("Start weather service"); + g_debug ("Starting weather service"); if (self->location_service_running || self->weather_service_running) gcal_weather_service_stop (self); - if (location == NULL) + if (!location) { /* Start location and weather service: */ self->location_service_running = TRUE; self->weather_service_running = TRUE; + g_cancellable_cancel (self->location_cancellable); g_cancellable_reset (self->location_cancellable); + gclue_simple_new (DESKTOP_FILE_NAME, GCLUE_ACCURACY_LEVEL_EXACT, self->location_cancellable, - (GAsyncReadyCallback) on_gclue_simple_creation, + (GAsyncReadyCallback) on_gclue_simple_creation_cb, g_object_ref (self)); } else @@ -1756,21 +1403,17 @@ gcal_weather_service_stop (GcalWeatherService *self) self->location_service_running = FALSE; self->weather_service_running = FALSE; - /* Notify all listeners about unknown location: */ + /* Notify all listeners about unknown location */ update_location (self, NULL); - if (self->location_service == NULL) + if (!self->location_service) { /* location service is under construction. Cancel creation. */ g_cancellable_cancel (self->location_cancellable); } else { - GClueClient *client; /* unowned */ - - client = gclue_simple_get_client (self->location_service); - - gclue_client_call_stop (client, + gclue_client_call_stop (gclue_simple_get_client (self->location_service), NULL, (GAsyncReadyCallback) on_gclue_client_stop, g_steal_pointer (&self->location_service));