[gnome-clocks/wip/geoinfo] Introduce geolocation support.



commit 2bcd2f80493516941d714309b8a314e5003f02b0
Author: Evgeny Bobkin <evgen ibqn gmail com>
Date:   Sun Sep 8 10:21:36 2013 +0200

    Introduce geolocation support.
    
    Use geoclue service to obtain longitude and latitude data. Reverse
    geocoding with geocode-glib provides country code. With this info we
    query a city location in the libgweather database. The found location
    appears automatically in the main view if no other locations within the
    same country and timezone are present.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=702403

 Makefile.am        |    3 +
 configure.ac       |    4 +-
 src/alarm.vala     |    2 +
 src/geocoding.vala |  213 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/widgets.vala   |  101 ++++++++++++++++++++++---
 src/world.vala     |   34 ++++++++-
 6 files changed, 345 insertions(+), 12 deletions(-)
---
diff --git a/Makefile.am b/Makefile.am
index 3ce38e4..e939fbc 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -93,6 +93,8 @@ AM_VALAFLAGS = \
        --pkg gweather-3.0 \
        --pkg libcanberra \
        --pkg libnotify \
+       --pkg gio-2.0 \
+       --pkg geocode-glib-1.0 \
        --gresources  $(top_srcdir)/data/gnome-clocks.gresource.xml
 
 bin_PROGRAMS = gnome-clocks
@@ -114,6 +116,7 @@ VALA_SOURCES = \
        src/timer.vala \
        src/utils.vala \
        src/widgets.vala \
+       src/geocoding.vala \
        src/main.vala
 
 gnome_clocks_SOURCES = \
diff --git a/configure.ac b/configure.ac
index 982b0b4..ba55509 100644
--- a/configure.ac
+++ b/configure.ac
@@ -49,13 +49,15 @@ LT_INIT([disable-static])
 PKG_PROG_PKG_CONFIG([0.22])
 
 PKG_CHECK_MODULES(CLOCKS, [
-    gio-2.0 >= 2.30.0
+    gio-2.0 >= 2.36
     glib-2.0 >= 2.36
     gtk+-3.0 >= 3.9.11
     libcanberra >= 0.30
     gweather-3.0 >= 3.9.91
     gnome-desktop-3.0 >= 3.7.90
     libnotify >= 0.7.0
+    geocode-glib-1.0 >= 0.99.3
+    geoclue-2.0 >= 1.99.3
 ])
 
 AC_CONFIG_FILES([
diff --git a/src/alarm.vala b/src/alarm.vala
index 0e24233..af55ad8 100644
--- a/src/alarm.vala
+++ b/src/alarm.vala
@@ -31,6 +31,8 @@ private class Item : Object, ContentItem {
         SNOOZING
     }
 
+    public string title_icon { get; set; default = null; }
+
     public string name {
         get {
             return _name;
diff --git a/src/geocoding.vala b/src/geocoding.vala
new file mode 100644
index 0000000..ffec2e7
--- /dev/null
+++ b/src/geocoding.vala
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2013  Evgeny Bobkin <evgen ibqn gmail com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+namespace Clocks {
+namespace Geo {
+
+[DBus (name = "org.freedesktop.GeoClue2.Manager")]
+private interface Manager : Object {
+    public abstract async void get_client (out string client_path) throws IOError;
+}
+
+[DBus (name = "org.freedesktop.GeoClue2.Client")]
+private interface Client : Object {
+    public abstract string location { owned get; }
+    public abstract uint distance_threshold { get; set; }
+
+    public signal void location_updated (string old_path, string new_path);
+
+    public abstract async void start () throws IOError;
+
+    // This function belongs to the Geoclue interface, however it is not used here
+    // public abstract async void stop () throws IOError;
+}
+
+[DBus (name = "org.freedesktop.GeoClue2.Location")]
+public interface Location : Object {
+    public abstract double latitude { get; }
+    public abstract double longitude { get; }
+    public abstract double accuracy { get; }
+    public abstract string description { owned get; }
+}
+
+public class Info : Object {
+    public Geo.Location? geo_location { get; private set; default = null; }
+
+    private GWeather.Location? found_location;
+    private string? country_code;
+    private Geo.Manager manager;
+    private Geo.Client client;
+    private double minimal_distance;
+
+    public signal void location_changed (GWeather.Location location);
+
+    public Info () {
+        country_code = null;
+        found_location = null;
+        minimal_distance = 1000.0d;
+    }
+
+    public async void seek () {
+        string? client_path = null;
+
+        try {
+            manager = yield Bus.get_proxy (GLib.BusType.SYSTEM,
+                                           "org.freedesktop.GeoClue2",
+                                           "/org/freedesktop/GeoClue2/Manager");
+        } catch (IOError e) {
+            warning ("Failed to connect to GeoClue2 Manager service: %s", e.message);
+            return;
+        }
+
+        try {
+            yield manager.get_client (out client_path);
+        } catch (IOError e) {
+            warning ("Failed to connect to GeoClue2 Manager service: %s", e.message);
+            return;
+        }
+
+        if (client_path == null) {
+            warning ("The client path is not set");
+            return;
+        }
+
+        try {
+            client = yield Bus.get_proxy (GLib.BusType.SYSTEM,
+                                          "org.freedesktop.GeoClue2",
+                                          client_path);
+        } catch (IOError e) {
+            warning ("Failed to connect to GeoClue2 Client service: %s", e.message);
+            return;
+        }
+
+        client.location_updated.connect (on_location_updated);
+
+        try {
+            yield client.start ();
+        } catch (IOError e) {
+            warning ("Failed to start client: %s", e.message);
+            return;
+        }
+    }
+
+    public async void on_location_updated (string old_path, string new_path) {
+        try {
+            geo_location = yield Bus.get_proxy (GLib.BusType.SYSTEM,
+                                                "org.freedesktop.GeoClue2",
+                                                new_path);
+        } catch (IOError e) {
+            warning ("Failed to connect to GeoClue2 Location service: %s", e.message);
+            return;
+        }
+
+        yield seek_country_code ();
+
+        yield search_locations (GWeather.Location.get_world ());
+
+        if (found_location != null) {
+            location_changed (found_location);
+        }
+    }
+
+    private async void seek_country_code () {
+        Geocode.Location location = new Geocode.Location (geo_location.latitude, geo_location.longitude);
+        Geocode.Reverse reverse = new Geocode.Reverse.for_location (location);
+
+        try {
+            Geocode.Place place = yield reverse.resolve_async ();
+
+            country_code = place.get_country_code ();
+
+            // Reverse geocoding returns country code which is not uppercased
+            country_code = country_code.up ();
+        } catch (Error e) {
+            warning ("Failed to obtain country code: %s", e.message);
+        }
+    }
+
+    private double deg_to_rad (double deg) {
+        return Math.PI / 180.0d * deg;
+    }
+
+    private double get_distance (double latitude1, double longitude1, double latitude2, double longitude2) {
+        const double earth_radius = 6372.795;
+
+        double lat1 = deg_to_rad (latitude1);
+        double lat2 = deg_to_rad (latitude2);
+        double lon1 = deg_to_rad (longitude1);
+        double lon2 = deg_to_rad (longitude2);
+
+        return Math.acos (Math.cos (lat1) * Math.cos (lat2) * Math.cos (lon1 - lon2) + Math.sin (lat1) * 
Math.sin (lat2)) * earth_radius;
+    }
+
+    private async void search_locations (GWeather.Location location) {
+        if (this.country_code != null) {
+            string? loc_country_code = location.get_country ();
+            if (loc_country_code != null) {
+                if (loc_country_code != this.country_code) {
+                    return;
+                }
+            }
+        }
+
+        GWeather.Location? [] locations = location.get_children ();
+        if (locations != null) {
+            for (int i = 0; i < locations.length; i++) {
+                if (locations[i].get_level () == GWeather.LocationLevel.CITY) {
+                    if (locations[i].has_coords ()) {
+                        double latitude, longitude, distance;
+
+                        locations[i].get_coords (out latitude, out longitude);
+                        distance = get_distance (geo_location.latitude, geo_location.longitude, latitude, 
longitude);
+
+                        if (distance < minimal_distance) {
+                            found_location = locations[i];
+                            minimal_distance = distance;
+                        }
+                    }
+                }
+
+                yield search_locations (locations[i]);
+            }
+        }
+    }
+
+    public bool is_location_similar (GWeather.Location location) {
+        if (this.found_location != null) {
+            string? country_code = location.get_country ();
+            string? found_country_code = found_location.get_country ();
+            if (country_code != null && country_code == found_country_code) {
+                GWeather.Timezone? timezone = location.get_timezone();
+                GWeather.Timezone? found_timezone = found_location.get_timezone();
+
+                if (timezone != null && found_timezone != null) {
+                    string? tzid = timezone.get_tzid ();
+                    string? found_tzid = found_timezone.get_tzid ();
+                    if (tzid == found_tzid) {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+}
+
+} // Geo
+} // Clocks
diff --git a/src/widgets.vala b/src/widgets.vala
index 0ec36b4..0efa254 100644
--- a/src/widgets.vala
+++ b/src/widgets.vala
@@ -56,6 +56,73 @@ public class HeaderBar : Gtk.HeaderBar {
     }
 }
 
+private class TitleRenderer : Gtk.CellRendererText {
+    private int ICON_XOFF;
+    private int ICON_YOFF;
+    private int ICON_SIZE;
+
+    public string title {
+        get {
+            return _title;
+        }
+        set {
+            markup = _title = value;
+        }
+    }
+
+    public string title_icon { get; set; default = null; }
+
+    private string _title;
+
+    public TitleRenderer () {
+        ICON_YOFF = 5;
+        ICON_XOFF = 25;
+        ICON_SIZE = 18;
+    }
+
+    public override void render (Cairo.Context cr, Gtk.Widget widget, Gdk.Rectangle background_area, 
Gdk.Rectangle cell_area, Gtk.CellRendererState flags) {
+        base.render (cr, widget, cell_area, cell_area, flags);
+
+        if (title_icon != null) {
+            var context = widget.get_style_context ();
+            context.save ();
+
+            cr.save ();
+            Gdk.cairo_rectangle (cr, cell_area);
+            cr.clip ();
+
+            cr.translate (cell_area.x, cell_area.y);
+
+            // create the layouts so that we can measure them
+            var layout = widget.create_pango_layout ("");
+            layout.set_markup (title, -1);
+            layout.set_alignment (Pango.Alignment.CENTER);
+            int text_w, text_h;
+            layout.get_pixel_size (out text_w, out text_h);
+
+            int x = (cell_area.width - text_w) / 2 - ICON_XOFF, y = ICON_YOFF;
+
+            if (widget.get_direction () == Gtk.TextDirection.RTL) {
+                x = (cell_area.width + text_w) / 2 + ICON_XOFF - ICON_SIZE;
+            }
+
+            Gtk.IconTheme icon_theme = Gtk.IconTheme.get_for_screen (Gdk.Screen.get_default ());
+            try {
+                Gtk.IconInfo? icon_info = icon_theme.lookup_icon (title_icon, ICON_SIZE, 0);
+                assert (icon_info != null);
+
+                Gdk.Pixbuf pixbuf = icon_info.load_icon ();
+                context.render_icon (cr, pixbuf, x, y);
+            } catch (Error e) {
+                warning (e.message);
+            }
+
+            context.restore ();
+            cr.restore ();
+        }
+    }
+}
+
 private class DigitalClockRenderer : Gtk.CellRendererPixbuf {
     public const int TILE_SIZE = 256;
     public const int CHECK_ICON_SIZE = 40;
@@ -175,6 +242,8 @@ private class DigitalClockRenderer : Gtk.CellRendererPixbuf {
 
 public interface ContentItem : GLib.Object {
     public abstract string name { get; set; }
+    public abstract string title_icon { get; set; default = null; }
+
     public abstract void get_thumb_properties (out string text, out string subtext, out Gdk.Pixbuf? pixbuf, 
out string css_class);
 }
 
@@ -244,18 +313,19 @@ private class IconView : Gtk.IconView {
             renderer.css_class = css_class;
         });
 
-        var text_renderer = new Gtk.CellRendererText ();
-        text_renderer.set_alignment (0.5f, 0.5f);
-        text_renderer.set_fixed_size (tile_width, -1);
-        text_renderer.alignment = Pango.Alignment.CENTER;
-        text_renderer.wrap_width = 220;
-        text_renderer.wrap_mode = Pango.WrapMode.WORD_CHAR;
-        pack_start (text_renderer, true);
-        set_cell_data_func (text_renderer, (column, cell, model, iter) => {
+        var title_renderer = new TitleRenderer ();
+        title_renderer.set_alignment (0.5f, 0.5f);
+        title_renderer.set_fixed_size (tile_width, -1);
+        title_renderer.alignment = Pango.Alignment.CENTER;
+        title_renderer.wrap_width = 220;
+        title_renderer.wrap_mode = Pango.WrapMode.WORD_CHAR;
+        pack_start (title_renderer, true);
+        set_cell_data_func (title_renderer, (column, cell, model, iter) => {
             ContentItem item;
             model.get (iter, IconView.Column.ITEM, out item);
-            var renderer = (Gtk.CellRendererText) cell;
-            renderer.markup = GLib.Markup.escape_text (item.name);
+            var renderer = (TitleRenderer) cell;
+            renderer.title = GLib.Markup.escape_text (item.name);
+            renderer.title_icon = item.title_icon;
         });
     }
 
@@ -291,6 +361,13 @@ private class IconView : Gtk.IconView {
         store.set (i, Column.SELECTED, false, Column.ITEM, item);
     }
 
+    public void prepend (Object item) {
+        var store = (Gtk.ListStore) model;
+        Gtk.TreeIter i;
+        store.insert (out i, 0);
+        store.set (i, Column.SELECTED, false, Column.ITEM, item);
+    }
+
     // Redefine selection handling methods since we handle selection manually
 
     public new List<Gtk.TreePath> get_selected_items () {
@@ -522,6 +599,10 @@ public class ContentView : Gtk.Bin {
         icon_view.add_item (item);
     }
 
+    public void prepend (ContentItem item) {
+        icon_view.prepend (item);
+    }
+
     // Note: this is not efficient: we first walk the model to collect
     // a list then the caller has to walk this list and then it has to
     // delete the items from the view, which walks the model again...
diff --git a/src/world.vala b/src/world.vala
index 80e73ee..b47f87f 100644
--- a/src/world.vala
+++ b/src/world.vala
@@ -24,6 +24,11 @@ private class Item : Object, ContentItem {
     private static Gdk.Pixbuf? night_pixbuf = Utils.load_image ("night.png");
 
     public GWeather.Location location { get; set; }
+
+    public bool automatic { get; set; default = false; }
+
+    public string title_icon { get; set; default = null; }
+
     public string name {
         get {
             // We store it in a _name member even if we overwrite it every time
@@ -314,6 +319,10 @@ public class MainPanel : Gtk.Stack, Clocks.Clock {
 
         load ();
 
+        use_geolocation.begin ((obj, res) => {
+            use_geolocation.end (res);
+        });
+
         notify["visible-child"].connect (() => {
             if (visible_child == content_view) {
                 header_bar.mode = HeaderBar.Mode.NORMAL;
@@ -349,11 +358,34 @@ public class MainPanel : Gtk.Stack, Clocks.Clock {
     private void save () {
         var builder = new GLib.VariantBuilder (new VariantType ("aa{sv}"));
         foreach (Item i in locations) {
-            i.serialize (builder);
+            if (!i.automatic) {
+                i.serialize (builder);
+            }
         }
         settings.set_value ("world-clocks", builder.end ());
     }
 
+    private async void use_geolocation () {
+        Geo.Info geo_info = new Geo.Info ();
+
+        geo_info.location_changed.connect ((found_location) => {
+            foreach (Item i in locations) {
+                if (geo_info.is_location_similar (i.location)) {
+                    return;
+                }
+            }
+
+            var item = new Item (found_location);
+
+            item.automatic = true;
+            item.title_icon = "find-location-symbolic";
+            locations.append (item);
+            content_view.prepend (item);
+        });
+
+        yield geo_info.seek ();
+    }
+
     public void activate_new () {
         var dialog = new LocationDialog ((Gtk.Window) get_toplevel ());
 


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