[gnome-clocks/wip/geoinfo] Introduce geolocation support.
- From: Evgeny Bobkin <ebobkin src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-clocks/wip/geoinfo] Introduce geolocation support.
- Date: Tue, 10 Sep 2013 13:38:47 +0000 (UTC)
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]