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));