Another important part of many mobile apps is location tracking – there is, fortunately, there is a built in api for most Linux systems called Geoclue that should work… There is even a Python-geoclue package, but after some digging I found that this package does not work in Python3. In fact it’s hard to find examples or documentation, if you look at the files of the package you can see there are some basic docs:
but they seem very sparse… reading this “Geoclue.geoclue”, we can know that it is imported with:
from Geocode import geocode
Unfortunately this is not working in Python3… as it states in the documentation. So with Python2 discontinued this is not so useful.
Finding the right library for Python3
Searching in Synaptic, a more promising package is gir1.2-geoclue-2.0. If you read the previous post about importing the openstreetmap library, this name may look familiar! In fact if you look at the properties and installed files in Synaptic you’ll see it installs one non-/doc/ file, that is /usr/lib/x86_64-linux-gnu/girepository-1.0/Geoclue-2.0.typelib
Within this directory you will see many other libraries that you can also interact with in Python. These are binary files, but like most you can grab the contents with “strings” command and see what text is within, which in this case seems to reveal the name in the fourth line:
/usr/lib/x86_64-linux-gnu/girepository-1.0$ strings ./Geoclue-2.0.typelib | head
GOBJ
METADATA
Gio-2.0
Geoclue
libgeoclue-2.so.0
GClue
AccuracyLevel
none
country
city
Note that you can include “Geoclue” in a similar manner as OSM library. In fact, let’s explore the api in the Python3 terminal, it appears this exposes the simple Geoclue geolocation api:
>>> from gi.repository import Geoclue
__main__:1: PyGIWarning: Geoclue was imported without specifying a version first. Use gi.require_version('Geoclue', '1.0') before import to ensure that the right version gets loaded.
>>> dir(Geoclue);
['AccuracyLevel', 'Client', 'ClientIface', 'ClientProxy', 'ClientProxyClass', 'ClientProxyPrivate', 'ClientSkeleton', 'ClientSkeletonClass', 'ClientSkeletonPrivate', 'Location', 'LocationIface', 'LocationProxy', 'LocationProxyClass', 'LocationProxyPrivate', 'LocationSkeleton', 'LocationSkeletonClass', 'LocationSkeletonPrivate', 'Manager', 'ManagerIface', 'ManagerProxy', 'ManagerProxyClass', 'ManagerProxyPrivate', 'ManagerSkeleton', 'ManagerSkeletonClass', 'ManagerSkeletonPrivate', 'Simple', 'SimpleClass', 'SimplePrivate', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__file__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__loader__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__package__', '__path__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__spec__', '__str__', '__subclasshook__', '__weakref__', '_namespace', '_version', 'client_interface_info', 'client_override_properties', 'location_interface_info', 'location_override_properties', 'manager_interface_info', 'manager_override_properties']
>>> dir(Geoclue.Simple)
['__class__', '__copy__', '__deepcopy__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__gdoc__', '__ge__', '__getattribute__', '__gpointer__', '__grefcount__', '__gsignals__', '__gt__', '__gtype__', '__hash__', '__info__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_force_floating', '_ref', '_ref_sink', '_unref', '_unsupported_data_method', '_unsupported_method', 'bind_property', 'bind_property_full', 'chain', 'compat_control', 'connect', 'connect_after', 'connect_data', 'connect_object', 'connect_object_after', 'disconnect', 'disconnect_by_func', 'emit', 'emit_stop_by_name', 'find_property', 'force_floating', 'freeze_notify', 'g_type_instance', 'get_client', 'get_data', 'get_location', 'get_properties', 'get_property', 'get_qdata', 'handler_block', 'handler_block_by_func', 'handler_disconnect', 'handler_is_connected', 'handler_unblock', 'handler_unblock_by_func', 'init_async', 'init_finish', 'install_properties', 'install_property', 'interface_find_property', 'interface_install_property', 'interface_list_properties', 'is_floating', 'list_properties', 'new', 'new_finish', 'new_sync', 'newv_async', 'notify', 'notify_by_pspec', 'override_property', 'parent', 'priv', 'props', 'qdata', 'ref', 'ref_count', 'ref_sink', 'replace_data', 'replace_qdata', 'run_dispose', 'set_data', 'set_properties', 'set_property', 'steal_data', 'steal_qdata', 'stop_emission', 'stop_emission_by_name', 'thaw_notify', 'unref', 'watch_closure', 'weak_ref']
Other files in the directory have similar interfaces you can import and use in Python… for example if you wanted to dig in to NetworkManager api, let’s see what that would be:
/usr/lib/x86_64-linux-gnu/girepository-1.0$ strings ./NetworkManager-1.0.typelib | head
GOBJ
METADATA
GObject-2.0|DBusGLib-1.0
NetworkManager
libnm-util.so.2
80211ApFlags
none
privacy
80211ApSecurityFlags
pair_wep40
And in similar manner, “from gi.repository import NetworkManager” makes the NetworkManager object available.
Exploring the Geoclue API for Python
Now let’s see what the Python Geoclue api has for us. The tab key helps to find some commands and their parameters which are similar to gclue_simple_new_sync(), gclue_simple_new() in the c docs.:
>>> Geoclue.Simple.new_sync.__doc__
'new_sync(desktop_id:str, accuracy_level:Geoclue.AccuracyLevel, cancellable:Gio.Cancellable=None) -> Geoclue.Simple'
>>> Geoclue.Simple.new.__doc__
'new(desktop_id:str, accuracy_level:Geoclue.AccuracyLevel, cancellable:Gio.Cancellable=None, callback:Gio.AsyncReadyCallback=None, user_data=None)'
So let’s see if we can get an approximate location using Geoclue and the get_location as in their docs:
>>> c = Geoclue.Simple.new_sync('something',Geoclue.AccuracyLevel.NEIGHBORHOOD,None)
>>> c
<Geoclue.Simple object at 0x7fce98294948 (GClueSimple at 0x2783fa0)>
>>> c.get_location.__doc__
'get_location(self) -> Geoclue.LocationProxy'
>>> loc = c.get_location()
>>> loc.
Display all 134 possibilities? (y or n)
loc.__class__( loc.call_with_unix_fd_list( loc.install_properties(
loc.__copy__( loc.call_with_unix_fd_list_finish( loc.install_property(
loc.__deepcopy__( loc.call_with_unix_fd_list_sync( loc.interface_find_property(
loc.__delattr__( loc.chain( loc.interface_info(
loc.__dict__ loc.compat_control( loc.interface_install_property(
loc.__dir__( loc.connect( loc.interface_list_properties(
loc.__doc__ loc.connect_after( loc.is_floating(
loc.__eq__( loc.connect_data( loc.list_properties(
loc.__format__( loc.connect_object( loc.new(
loc.__gdoc__ loc.connect_object_after( loc.new_finish(
loc.__ge__( loc.disconnect( loc.new_for_bus(
loc.__getattr__( loc.disconnect_by_func( loc.new_for_bus_finish(
loc.__getattribute__( loc.do_g_properties_changed( loc.new_for_bus_sync(
loc.__gpointer__ loc.do_g_signal( loc.new_sync(
loc.__grefcount__ loc.emit( loc.newv(
loc.__gsignals__ loc.emit_stop_by_name( loc.newv_async(
loc.__gt__( loc.find_property( loc.notify(
loc.__gtype__ loc.force_floating( loc.notify_by_pspec(
loc.__hash__( loc.freeze_notify( loc.override_properties(
loc.__info__ loc.g_type_instance loc.override_property(
loc.__init__( loc.get_cached_property( loc.priv
loc.__le__( loc.get_cached_property_names( loc.props
loc.__lt__( loc.get_connection( loc.qdata
loc.__module__ loc.get_data( loc.ref(
loc.__ne__( loc.get_default_timeout( loc.ref_count
loc.__new__( loc.get_flags( loc.ref_sink(
loc.__reduce__( loc.get_info( loc.replace_data(
loc.__reduce_ex__( loc.get_interface_info( loc.replace_qdata(
loc.__repr__( loc.get_interface_name( loc.run_dispose(
loc.__setattr__( loc.get_name( loc.set_cached_property(
loc.__sizeof__( loc.get_name_owner( loc.set_data(
loc.__str__( loc.get_object( loc.set_default_timeout(
loc.__subclasshook__( loc.get_object_path( loc.set_interface_info(
loc.__weakref__ loc.get_properties( loc.set_object(
loc._force_floating( loc.get_property( loc.set_properties(
loc._ref( loc.get_qdata( loc.set_property(
loc._ref_sink( loc.handler_block( loc.steal_data(
loc._unref( loc.handler_block_by_func( loc.steal_qdata(
loc._unsupported_data_method( loc.handler_disconnect( loc.stop_emission(
loc._unsupported_method( loc.handler_is_connected( loc.stop_emission_by_name(
loc.bind_property( loc.handler_unblock( loc.thaw_notify(
loc.bind_property_full( loc.handler_unblock_by_func( loc.unref(
loc.call( loc.init( loc.watch_closure(
loc.call_finish( loc.init_async( loc.weak_ref(
loc.call_sync( loc.init_finish(
>>> loc.get_data()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3/dist-packages/gi/overrides/GObject.py", line 550, in _unsupported_data_method
raise RuntimeError('Data access methods are unsupported. '
RuntimeError: Data access methods are unsupported. Use normal Python attributes instead
This is odd, unlike the c api for the object returned by get_location, there is no get_latitude(), get_longitude(). After some guess and trying I found it is “loc.get_property(‘latitude’)”, “loc.get_property(‘longitude’)”, or “loc.props.longitude”, “loc.props.latitude” for the loc location object you get from that. So, putting it all together, to get an approximate location for your laptop or device you can enter the following at the python3 terminal:
from gi.repository import Geoclue
clue = Geoclue.Simple.new_sync('something',Geoclue.AccuracyLevel.NEIGHBORHOOD,None)
location = clue.get_location()
print(location.get_property('latitude'), location.get_property('longitude'))
Not only that, there are other attributes you can get from the location object:
>>> for i in l.props:
... print(i)
...
<GParamDouble 'accuracy'>
<GParamDouble 'altitude'>
<GParamString 'description'>
<GParamDouble 'heading'>
<GParamDouble 'latitude'>
<GParamDouble 'longitude'>
<GParamDouble 'speed'>
<GParamObject 'g-connection'>
<GParamEnum 'g-bus-type'>
<GParamString 'g-name'>
<GParamString 'g-name-owner'>
<GParamFlags 'g-flags'>
<GParamString 'g-object-path'>
<GParamString 'g-interface-name'>
<GParamInt 'g-default-timeout'>
<GParamBoxed 'g-interface-info'>
Note that the Geoclue.AccuracyLevel.NEIGHBORHOOD can be EXACT instead, and it can give a more exact location – I think it is using wifi MAC addresses and Mozilla’s database for geolocation? Another thing you might wonder looking at the above code is, what is the first argument “desktop id” supposed to be? Apparently no one on their mailing list knows either unfortunately 🙁 Perhaps it should be just the name of your application.
Building the app GUI
Let’s add a button with the circle/crosshair icon that’s common for the my-location button for gps, and connect this… There are the stock icons for whatever GTK theme on your system but that doesn’t seem to be one of them. So let’s add one using a svg file I quickly made in Inkscape – pretty quickly using rectangle, circle, move layer to bottom, and object align and distribute options:
I change the “home” button because we don’t want it like the example sending us to some spot in europe but rather where you are. Let’s change that button:
GoImg = Gtk.Image.new_from_file('locateme.svg')
home_button.set_image(GoImg)
Yikes, that is too big on the screen. Like the documentation says let’s make a pixmap like before at set dimensions.
home_button = Gtk.Button()
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale('locateme.svg',width=21,height=21,preserve_aspect_ratio=True)
GoImg = Gtk.Image.new_from_pixbuf(pixbuf)
home_button.set_image(GoImg)
home_button.set_tooltip_text('Find my location')
home_button.connect('clicked', self.home_clicked)
This revised code looks more like a navigation app! Now plugging in the location code this should make it easy to find yourself on the map and map nearby repeaters:
def home_clicked(self, button):
self.osm.set_center_and_zoom(-44.39, 171.25, 12)
#self.getlocation() #Freezes up, odd.
GObject.timeout_add(1, self.getlocation)
def getlocation(self):
clue = Geoclue.Simple.new_sync('repeaterstart',Geoclue.AccuracyLevel.EXACT,None)
location = clue.get_location()
self.osm.set_center_and_zoom(location.get_property('latitude'), location.get_property('longitude'), 12)
I noted something odd, that the code locked up the whole program if it runs on the UI thread – that is, if you call the function directly in a click handler. Since that is blocking that is not a great thing to run without a thread but I thought it would work. Now, with the revised code there is an easy map center to location button 🙂
(See note below if you are running newer Ubuntu/GNOME)
Since all the data is IRLP for now, it should be good to switch between an area you browse and your current location. So let’s add a back button too:
home_button.connect('clicked', self.home_clicked)
back_button = Gtk.Button(stock=Gtk.STOCK_GO_BACK)
back_button.connect('clicked', self.back_clicked)
cache_button = Gtk.Button('Cache')
cache_button.connect('clicked', self.cache_clicked)
self.vbox.pack_start(self.osm, True, True, 0)
hbox = Gtk.HBox(False, 0)
hbox.pack_start(home_button, False, True, 0)
hbox.pack_start(back_button, False, True, 0)
hbox.pack_start(cache_button, False, True, 0)
and function to go back, and save the place you were when you center to current location:
def back_clicked(self, button):
self.osm.set_center_and_zoom(self.lastLat, self.lastLon, 12)
def getlocation(self):
self.lastLat = self.osm.props.latitude
self.lastLon = self.osm.props.longitude
clue = Geoclue.Simple.new_sync('repeaterstart',Geoclue.AccuracyLevel.EXACT,None)
location = clue.get_location()
self.osm.set_center_and_zoom(location.get_property('latitude'), location.get_property('longitude'), 12)
def on_query_tooltip(self, widget, x, y, keyboard_tip, tooltip, data=None):
and now you can easily switch between your current location and where you were browsing to IRLP to!
Update: After upgrading to Ubuntu 18.04 or newer you may notice that it fails with a message:
GLib.Error: g-dbus-error-quark: GDBus.Error:org.freedesktop.DBus.Error.AccessDenied: Agent rejected 'repeaterstart' for user '1000'. Please ensure that 'repeaterstart' has installed a valid repeaterstart.desktop file. (9)
You need to have user approve use of location, which you can add an alert with:
def privacySettingsOpen(self):
Gdk.threads_enter()
dlg = Gtk.MessageDialog(self,
0,Gtk.MessageType.WARNING,
Gtk.ButtonsType.OK,
'Please allow geolocation to use this feature.')
response = dlg.run()
dlg.destroy()
subprocess.Popen(['gnome-control-center','privacy'])
Gdk.threads_leave()
From the thread I call this with GObject.idle_add(self.privacySettingsOpen) in the try-except case:
try:
clue = Geoclue.Simple.new_sync('repeaterstart',Geoclue.AccuracyLevel.EXACT,None)
location = clue.get_location()
self.osm.set_center_and_zoom(location.get_property('latitude'), location.get_property('longitude'), 12)
except GLib.Error as err:
print(err)
GObject.idle_add(self.privacySettingsOpen)
In the next post, see how to set a subprocess on a thread.