[libgweather] Add serialization capabilities to GWeatherLocation



commit c522bea3e49c37a0878d654ded454b182afb1144
Author: Giovanni Campagna <gcampagna src gnome org>
Date:   Sun Dec 2 03:55:31 2012 +0100

    Add serialization capabilities to GWeatherLocation
    
    Applications using libgweather want to store locations in GSettings
    or data files, and then be able to retrieve them, in a way that minimizes
    the difference between acquiring the location from a GWeatherLocationEntry
    or from storage.
    To do so, a new location level is introduced, GWEATHER_LOCATION_DETACHED,
    which is used as a fallback in case we cannot recover the right location
    from the database.

 libgweather/gweather-location.c           |  278 ++++++++++++++++++++++++++++-
 libgweather/gweather-location.h           |   12 +-
 libgweather/gweather-xml.c                |    3 +
 libgweather/location-entry.c              |   58 +++----
 libgweather/parser.c                      |    8 +-
 libgweather/weather-priv.h                |    6 +
 schemas/org.gnome.GWeather.gschema.xml.in |    2 +-
 7 files changed, 325 insertions(+), 42 deletions(-)
---
diff --git a/libgweather/gweather-location.c b/libgweather/gweather-location.c
index 0729ed9..2929b66 100644
--- a/libgweather/gweather-location.c
+++ b/libgweather/gweather-location.c
@@ -34,6 +34,9 @@
 #include "parser.h"
 #include "weather-priv.h"
 
+/* This is the precision of coordinates in the database */
+#define EPSILON 0.000001
+
 /**
  * SECTION:gweather-location
  * @Title: GWeatherLocation
@@ -60,6 +63,10 @@
  * @GWEATHER_LOCATION_CITY: A location representing a city
  * @GWEATHER_LOCATION_WEATHER_STATION: A location representing a
  * weather station.
+ * @GWEATHER_LOCATION_DETACHED: A location that is detached from the
+ * database, for example because it was loaded from external storage
+ * and could not be fully recovered. The parent of this location is
+ * the nearest weather station.
  *
  * The size/scope of a particular #GWeatherLocation.
  *
@@ -69,6 +76,9 @@
  * or may not be divided into "adm2"s. A city will have at least one,
  * and possibly several, weather stations inside it. Weather stations
  * will never appear outside of cities.
+ *
+ * Building a database with gweather_location_new_world() will never
+ * create detached instances, but deserializing might.
  **/
 
 static int
@@ -255,7 +265,12 @@ location_new_from_xml (GWeatherParser *parser, GWeatherLocationLevel level,
 
     if (level == GWEATHER_LOCATION_WEATHER_STATION) {
 	/* Cache weather stations by METAR code */
-	g_hash_table_replace (parser->metar_code_cache, loc->station_code, gweather_location_ref (loc));
+	GList *a, *b;
+
+	a = g_hash_table_lookup (parser->metar_code_cache, loc->station_code);
+	b = g_list_append (a, gweather_location_ref (loc));
+	if (b != a)
+	    g_hash_table_replace (parser->metar_code_cache, loc->station_code, b);
     }
 
     if (children->len) {
@@ -706,9 +721,9 @@ gweather_location_get_code (GWeatherLocation *loc)
  * gweather_location_get_city_name:
  * @loc: a #GWeatherLocation
  *
- * For a %GWEATHER_LOCATION_CITY location, this is equivalent to
- * gweather_location_get_name(). For a
- * %GWEATHER_LOCATION_WEATHER_STATION location, it is equivalent to
+ * For a %GWEATHER_LOCATION_CITY or %GWEATHER_LOCATION_DETACHED location,
+ * this is equivalent to gweather_location_get_name().
+ * For a %GWEATHER_LOCATION_WEATHER_STATION location, it is equivalent to
  * calling gweather_location_get_name() on the location's parent. For
  * other locations it will return %NULL.
  *
@@ -719,7 +734,8 @@ gweather_location_get_city_name (GWeatherLocation *loc)
 {
     g_return_val_if_fail (loc != NULL, NULL);
 
-    if (loc->level == GWEATHER_LOCATION_CITY)
+    if (loc->level == GWEATHER_LOCATION_CITY ||
+	loc->level == GWEATHER_LOCATION_DETACHED)
 	return g_strdup (loc->name);
     else if (loc->level == GWEATHER_LOCATION_WEATHER_STATION &&
 	     loc->parent &&
@@ -772,11 +788,29 @@ _weather_location_from_gweather_location (GWeatherLocation *gloc, const gchar *n
     return wloc;
 }
 
+/**
+ * gweather_location_find_by_station_code:
+ * @world: a #GWeatherLocation at the world level
+ * @station_code: a 4 letter METAR code
+ *
+ * Retrieves the weather station identifier by @station_code.
+ * Note that multiple instances of the same weather station can exist
+ * in the database, and this function will return any of them, so this
+ * not usually what you want.
+ *
+ * See gweather_location_deserialize() to recover a stored #GWeatherLocation.
+ *
+ * Returns: a weather station level #GWeatherLocation for @station_code,
+ *          or %NULL if none exists in the database.
+ */
 GWeatherLocation *
 gweather_location_find_by_station_code (GWeatherLocation *world,
 					const gchar      *station_code)
 {
-    return g_hash_table_lookup (world->metar_code_cache, station_code);
+    GList *l;
+
+    l = g_hash_table_lookup (world->metar_code_cache, station_code);
+    return l ? l->data : NULL;
 }
 
 GWeatherLocation *
@@ -790,3 +824,235 @@ gweather_location_ref_world (GWeatherLocation *loc)
 	gweather_location_ref (loc);
     return loc;
 }
+
+/**
+ * gweather_location_equals:
+ * @one: a #GWeatherLocation
+ * @two: another #GWeatherLocation
+ *
+ * Compares two #GWeatherLocation and sees if they represent the same
+ * place.
+ * It is only legal to call this for cities, weather stations or
+ * detached locations.
+ * Note that this function only checks for geographical characteristics,
+ * such as coordinates and METAR code. It is still possible that the two
+ * locations belong to different worlds (in which case care must be
+ * taken when passing them GWeatherLocationEntry and GWeatherInfo), or
+ * if one is them is detached it could have a custom name.
+ *
+ * Returns: %TRUE if the two locations represent the same place as
+ *          far as libgweather can tell, and %FALSE otherwise.
+ */
+gboolean
+gweather_location_equal (GWeatherLocation *one,
+			 GWeatherLocation *two)
+{
+    int level;
+
+    if (one == two)
+	return TRUE;
+
+    if (one->level != two->level &&
+	one->level != GWEATHER_LOCATION_DETACHED &&
+	two->level != GWEATHER_LOCATION_DETACHED)
+	return FALSE;
+
+    level = one->level;
+    if (level == GWEATHER_LOCATION_DETACHED)
+	level = two->level;
+
+    if (level == GWEATHER_LOCATION_COUNTRY)
+	return g_strcmp0 (one->country_code, two->country_code);
+
+    if (level == GWEATHER_LOCATION_ADM1 ||
+	level == GWEATHER_LOCATION_ADM2) {
+	if (g_strcmp0 (one->sort_name, two->sort_name) != 0)
+	    return FALSE;
+
+	return one->parent && two->parent &&
+	    gweather_location_equal (one->parent, two->parent);
+    }
+
+    if (g_strcmp0 (one->station_code, two->station_code) != 0)
+	return FALSE;
+
+    if (one->level != GWEATHER_LOCATION_DETACHED &&
+	two->level != GWEATHER_LOCATION_DETACHED &&
+	!gweather_location_equal (one->parent, two->parent))
+	return FALSE;
+
+    return ABS(one->latitude - two->latitude) < EPSILON &&
+	ABS(one->longitude - two->longitude) < EPSILON;
+}
+
+/* ------------------- serialization ------------------------------- */
+
+#define FORMAT 1
+
+static GVariant *
+gweather_location_format_one_serialize (GWeatherLocation *location)
+{
+    const char *name;
+    gboolean is_city;
+
+    name = location->name;
+
+    /* Normalize location to be a weather station or detached */
+    if (location->level == GWEATHER_LOCATION_CITY) {
+	location = location->children[0];
+	is_city = TRUE;
+    } else {
+	is_city = FALSE;
+    }
+
+    return g_variant_new ("(ssbm(dd)m(dd))",
+			  name, location->station_code, is_city,
+			  location->latlon_valid,
+			  location->latitude,
+			  location->longitude,
+			  location->parent && location->parent->latlon_valid,
+			  location->parent ? location->parent->latitude : 0.0d,
+			  location->parent ? location->parent->longitude : 0.0d);
+}
+
+GWeatherLocation *
+_gweather_location_new_detached (GWeatherLocation *nearest_station,
+				 const char       *name,
+				 gboolean          latlon_valid,
+				 gdouble           latitude,
+				 gdouble           longitude)
+{
+    GWeatherLocation *self;
+    char *normalized;
+
+    self = g_slice_new0 (GWeatherLocation);
+    self->level = GWEATHER_LOCATION_DETACHED;
+    self->name = g_strdup (name);
+
+    normalized = g_utf8_normalize (name, -1, G_NORMALIZE_ALL);
+    self->sort_name = g_utf8_casefold (normalized, -1);
+    g_free (normalized);
+
+    self->parent = nearest_station;
+    self->children = NULL;
+
+    if (nearest_station)
+	self->station_code = g_strdup (nearest_station->station_code);
+
+    g_assert (nearest_station || latlon_valid);
+
+    if (latlon_valid) {
+	self->latlon_valid = TRUE;
+	self->latitude = latitude;
+	self->longitude = longitude;
+    } else {
+	self->latlon_valid = nearest_station->latlon_valid;
+	self->latitude = nearest_station->latitude;
+	self->longitude = nearest_station->longitude;
+    }
+
+    return self;
+}
+
+static GWeatherLocation *
+gweather_location_format_one_deserialize (GWeatherLocation *world,
+					  GVariant         *variant)
+{
+    const char *name;
+    const char *station_code;
+    gboolean is_city, latlon_valid, parent_latlon_valid;
+    gdouble latitude, longitude, parent_latitude, parent_longitude;
+    GList *candidates, *l;
+    GWeatherLocation *found;
+
+    g_variant_get (variant, "(&s&sbm(dd)m(dd))", &name, &station_code, &is_city,
+		   &latlon_valid, &latitude, &longitude,
+		   &parent_latlon_valid, &parent_latitude, &parent_longitude);
+
+    /* First find the list of candidate locations */
+    candidates = g_hash_table_lookup (world->metar_code_cache, station_code);
+
+    /* If we don't have coordinates, fallback immediately to making up
+     * a location
+     */
+    if (!latlon_valid)
+	return candidates ? _gweather_location_new_detached (candidates->data, name, FALSE, 0, 0) : NULL;
+
+    found = NULL;
+
+    /* First try weather stations directly. */
+    for (l = candidates; l; l = l->next) {
+	GWeatherLocation *ws, *city;
+
+	ws = l->data;
+
+	if (!ws->latlon_valid ||
+	    ABS(ws->latitude - latitude) >= EPSILON ||
+	    ABS(ws->longitude - longitude) >= EPSILON)
+	    /* Not what we're looking for... */
+	    continue;
+
+	/* If we can't check for the latitude and longitude
+	   of the parent, we just assume we found what we needed
+	*/
+	if ((!parent_latlon_valid || !ws->parent || !ws->parent->latlon_valid) ||
+	    (ABS(parent_latitude - ws->parent->latitude) < EPSILON &&
+	     ABS(parent_longitude - ws->parent->longitude) < EPSILON)) {
+
+	    /* Found! Now check which one we want (ws or city) and the name... */
+	    if (is_city)
+		city = ws->parent;
+	    else
+		city = ws;
+
+	    if (city == NULL) {
+		/* Oops! There is no city for this weather station! */
+		continue;
+	    }
+
+	    if (g_strcmp0 (name, city->name) == 0)
+		found = gweather_location_ref (city);
+	    else
+		found = _gweather_location_new_detached (city, name, FALSE, 0, 0);
+
+	    break;
+	}
+    }
+
+    if (found)
+	return found;
+
+    /* No weather station matches the serialized data, let's pick
+       one at random from the station code list */
+    if (candidates)
+	return _gweather_location_new_detached (candidates->data,
+						name, TRUE, latitude, longitude);
+    else
+	return NULL;
+}
+
+GVariant *
+gweather_location_serialize (GWeatherLocation *location)
+{
+    return g_variant_new ("(uv)", FORMAT,
+			  gweather_location_format_one_serialize (location));
+}
+
+GWeatherLocation *
+gweather_location_deserialize (GWeatherLocation *world,
+			       GVariant         *serialized)
+{
+    GVariant *v;
+    GWeatherLocation *loc;
+    int format;
+
+    g_variant_get (serialized, "(uv)", &format, &v);
+
+    if (format == FORMAT)
+	loc = gweather_location_format_one_deserialize (world, v);
+    else
+	loc = NULL;
+
+    g_variant_unref (v);
+    return loc;
+}
diff --git a/libgweather/gweather-location.h b/libgweather/gweather-location.h
index fb3b7ef..7efcd94 100644
--- a/libgweather/gweather-location.h
+++ b/libgweather/gweather-location.h
@@ -41,7 +41,8 @@ typedef enum { /*< underscore_name=gweather_location_level >*/
     /* ADM2 = second-order division = county, etc */
     GWEATHER_LOCATION_ADM2,
     GWEATHER_LOCATION_CITY,
-    GWEATHER_LOCATION_WEATHER_STATION
+    GWEATHER_LOCATION_WEATHER_STATION,
+    GWEATHER_LOCATION_DETACHED
 } GWeatherLocationLevel;
 
 GType gweather_location_get_type (void);
@@ -80,10 +81,17 @@ const char            *gweather_location_get_code       (GWeatherLocation  *loc)
 char                  *gweather_location_get_city_name  (GWeatherLocation  *loc);
 
 GWeatherLocation      *gweather_location_find_by_station_code (GWeatherLocation *world,
-							       const gchar *code);
+							       const gchar      *station_code);
 
 GWeatherLocation      *gweather_location_ref_world      (GWeatherLocation  *loc);
 
+gboolean               gweather_location_equal          (GWeatherLocation  *one,
+							 GWeatherLocation  *two);
+
+GVariant              *gweather_location_serialize      (GWeatherLocation  *loc);
+GWeatherLocation      *gweather_location_deserialize    (GWeatherLocation  *world,
+							 GVariant          *serialized);
+
 G_END_DECLS
 
 #endif /* __GWEATHER_LOCATIONS_H__ */
diff --git a/libgweather/gweather-xml.c b/libgweather/gweather-xml.c
index 3e72d5e..fa39618 100644
--- a/libgweather/gweather-xml.c
+++ b/libgweather/gweather-xml.c
@@ -114,6 +114,9 @@ gweather_xml_parse_node (GWeatherLocation *gloc,
 			    -1);
 	_weather_location_free (wloc);
 
+    case GWEATHER_LOCATION_DETACHED:
+	g_assert_not_reached ();
+
 	break;
     }
 
diff --git a/libgweather/location-entry.c b/libgweather/location-entry.c
index 132fa58..864ccdf 100644
--- a/libgweather/location-entry.c
+++ b/libgweather/location-entry.c
@@ -22,10 +22,11 @@
 #include <config.h>
 #endif
 
+#include <string.h>
+
 #define GWEATHER_I_KNOW_THIS_IS_UNSTABLE
 #include "location-entry.h"
-
-#include <string.h>
+#include "weather-priv.h"
 
 /**
  * SECTION:location-entry
@@ -175,14 +176,16 @@ entry_changed (GWeatherLocationEntry *entry)
 static void
 set_location_internal (GWeatherLocationEntry *entry,
 		       GtkTreeModel          *model,
-		       GtkTreeIter           *iter)
+		       GtkTreeIter           *iter,
+		       GWeatherLocation      *loc)
 {
-    GWeatherLocation *loc;
     char *name;
 
     if (entry->location)
 	gweather_location_unref (entry->location);
 
+    g_assert (iter == NULL || loc == NULL);
+
     if (iter) {
 	gtk_tree_model_get (model, iter,
 			    GWEATHER_LOCATION_ENTRY_COL_DISPLAY_NAME, &name,
@@ -192,6 +195,10 @@ set_location_internal (GWeatherLocationEntry *entry,
 	gtk_entry_set_text (GTK_ENTRY (entry), name);
 	entry->custom_text = FALSE;
 	g_free (name);
+    } else if (loc) {
+	entry->location = gweather_location_ref (loc);
+	gtk_entry_set_text (GTK_ENTRY (entry), loc->name);
+	entry->custom_text = TRUE;
     } else {
 	entry->location = NULL;
 	gtk_entry_set_text (GTK_ENTRY (entry), "");
@@ -210,6 +217,8 @@ set_location_internal (GWeatherLocationEntry *entry,
  *
  * Sets @entry's location to @loc, and updates the text of the
  * entry accordingly.
+ * Note that if the database contains a location that compares
+ * equal to @loc, that will be chosen in place of @loc.
  **/
 void
 gweather_location_entry_set_location (GWeatherLocationEntry *entry,
@@ -230,13 +239,13 @@ gweather_location_entry_set_location (GWeatherLocationEntry *entry,
 	gtk_tree_model_get (model, &iter,
 			    GWEATHER_LOCATION_ENTRY_COL_LOCATION, &cmploc,
 			    -1);
-	if (loc == cmploc) {
-	    set_location_internal (entry, model, &iter);
+	if (gweather_location_equal (loc, cmploc)) {
+	    set_location_internal (entry, model, &iter, NULL);
 	    return;
 	}
     } while (gtk_tree_model_iter_next (model, &iter));
 
-    set_location_internal (entry, model, NULL);
+    set_location_internal (entry, model, NULL, loc);
 }
 
 /**
@@ -330,11 +339,11 @@ gweather_location_entry_set_city (GWeatherLocationEntry *entry,
 	    g_free (cmpname);
 	}
 
-	set_location_internal (entry, model, &iter);
+	set_location_internal (entry, model, &iter, NULL);
 	return TRUE;
     } while (gtk_tree_model_iter_next (model, &iter));
 
-    set_location_internal (entry, model, NULL);
+    set_location_internal (entry, model, NULL, NULL);
 
     return FALSE;
 }
@@ -413,29 +422,11 @@ fill_location_entry_model (GtkTreeStore *store, GWeatherLocation *loc,
 		g_free (display_name);
 		g_free (compare_name);
 	    }
-	} else if (children[0]) {
-	    /* Else there's only one location. This is a mix of the
-	     * city-with-multiple-location case above and the
-	     * location-with-no-city case below.
-	     */
-	    display_name = g_strdup_printf ("%s, %s",
-					    gweather_location_get_name (loc),
-					    parent_display_name);
-	    compare_name = g_strdup_printf ("%s, %s",
-					    gweather_location_get_sort_name (loc),
-					    parent_compare_name);
-
-	    gtk_tree_store_append (store, &iter, NULL);
-	    gtk_tree_store_set (store, &iter,
-				GWEATHER_LOCATION_ENTRY_COL_LOCATION, children[0],
-				GWEATHER_LOCATION_ENTRY_COL_DISPLAY_NAME, display_name,
-				GWEATHER_LOCATION_ENTRY_COL_COMPARE_NAME, compare_name,
-				-1);
-
-	    g_free (display_name);
-	    g_free (compare_name);
+
+	    break;
 	}
-	break;
+
+	/* fall through */
 
     case GWEATHER_LOCATION_WEATHER_STATION:
 	/* <location> with no parent <city>, or <city> with a single
@@ -458,6 +449,9 @@ fill_location_entry_model (GtkTreeStore *store, GWeatherLocation *loc,
 	g_free (display_name);
 	g_free (compare_name);
 	break;
+
+    case GWEATHER_LOCATION_DETACHED:
+	g_assert_not_reached ();
     }
 
     gweather_location_free_children (loc, children);
@@ -569,7 +563,7 @@ match_selected (GtkEntryCompletion *completion,
 		GtkTreeIter        *iter,
 		gpointer            entry)
 {
-    set_location_internal (entry, model, iter);
+    set_location_internal (entry, model, iter, NULL);
     return TRUE;
 }
 
diff --git a/libgweather/parser.c b/libgweather/parser.c
index ffefcab..837444b 100644
--- a/libgweather/parser.c
+++ b/libgweather/parser.c
@@ -153,6 +153,12 @@ gweather_parser_get_localized_value (GWeatherParser *parser)
     return name;
 }
 
+static void
+gweather_location_list_free (gpointer list)
+{
+    g_list_free_full (list, (GDestroyNotify) gweather_location_unref);
+}
+
 GWeatherParser *
 gweather_parser_new (gboolean use_regions)
 {
@@ -247,7 +253,7 @@ gweather_parser_new (gboolean use_regions)
     tm.tm_year++;
     parser->year_end = mktime (&tm);
 
-    parser->metar_code_cache = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify) gweather_location_unref);
+    parser->metar_code_cache = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, gweather_location_list_free);
 
     return parser;
 
diff --git a/libgweather/weather-priv.h b/libgweather/weather-priv.h
index e0cd51e..985114c 100644
--- a/libgweather/weather-priv.h
+++ b/libgweather/weather-priv.h
@@ -218,5 +218,11 @@ void		free_forecast_list	(GWeatherInfo *info);
 
 GWeatherInfo   *_gweather_info_new_clone (GWeatherInfo *info);
 
+GWeatherLocation *_gweather_location_new_detached (GWeatherLocation *nearest_station,
+						   const char       *name,
+						   gboolean          latlon_valid,
+						   gdouble           latitude,
+						   gdouble           longitude);
+
 #endif /* __WEATHER_PRIV_H_ */
 
diff --git a/schemas/org.gnome.GWeather.gschema.xml.in b/schemas/org.gnome.GWeather.gschema.xml.in
index c1972d7..113854b 100644
--- a/schemas/org.gnome.GWeather.gschema.xml.in
+++ b/schemas/org.gnome.GWeather.gschema.xml.in
@@ -52,7 +52,7 @@
       </_description>
     </key>
     <key name="default-location" type="(ssm(dd))">
-      <!-- TRANSLATORS: pick a _default location to use in the weather applet. This should
+      <!-- TRANSLATORS: pick a default location to use in the weather applet. This should
       usually be the largest city or the capital of your country. If you're not picking
       a <location> in the database, don't forget to set name and coordinates. -->
       <_default>('', 'KNYC', nothing)</_default>



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]