432 lines
15 KiB
Python
432 lines
15 KiB
Python
from plyer import gps
|
|
from kivy.app import App
|
|
from kivy.clock import Clock
|
|
from kivy.core.window import Window
|
|
from kivy.properties import DictProperty, NumericProperty, StringProperty
|
|
from kivy.logger import Logger
|
|
from kivy.clock import mainthread
|
|
from kivy.utils import platform
|
|
from kivy_garden.mapview import MapView
|
|
from kivy.uix.screenmanager import ScreenManager, Screen, RiseInTransition, SlideTransition
|
|
from kivy.uix.behaviors import ButtonBehavior
|
|
from kivy.uix.image import Image
|
|
from kivy.vector import Vector
|
|
from kivy.animation import Animation
|
|
from math import atan2, sin, cos, degrees, floor
|
|
import requests
|
|
from datetime import timedelta, datetime
|
|
from dateutil.parser import isoparse
|
|
|
|
from geopy.distance import geodesic
|
|
|
|
from os import path
|
|
import pickle
|
|
|
|
#FLOCK_SERVER="http://192.168.0.8:5000"
|
|
#FLOCK_SERVER="http://awelo.ath.cx:5000"
|
|
|
|
class IconButton(ButtonBehavior, Image):
|
|
pass
|
|
|
|
|
|
class MapScreen(Screen):
|
|
pass
|
|
|
|
|
|
class CompassScreen(Screen):
|
|
def __init__(self, **kwargs):
|
|
super(CompassScreen, self).__init__(**kwargs)
|
|
Window.bind(on_keyboard=self.android_back_click)
|
|
|
|
def android_back_click(self, window, key, *largs):
|
|
if key == 27 and self.parent is not None:
|
|
self.parent.current='map'
|
|
return True
|
|
|
|
|
|
|
|
class SettingsScreen(Screen):
|
|
def __init__(self, **kwargs):
|
|
super(SettingsScreen, self).__init__(**kwargs)
|
|
Window.bind(on_keyboard=self.android_back_click)
|
|
|
|
def android_back_click(self, window, key, *largs):
|
|
if key == 27 and self.parent is not None:
|
|
self.parent.transition = SlideTransition(direction='left')
|
|
self.parent.current='map'
|
|
return True
|
|
|
|
|
|
|
|
class FlockompassApp(App):
|
|
|
|
gps_data = DictProperty()
|
|
session_data = DictProperty()
|
|
needle_angle = NumericProperty(0)
|
|
fbearing = NumericProperty(0)
|
|
dbearing = NumericProperty(0)
|
|
|
|
dest_distance = StringProperty("")
|
|
dashboard_dest = StringProperty("")
|
|
dashboard_flock = StringProperty("")
|
|
|
|
theme = DictProperty(
|
|
{'needle': "assets/needle_dark.png",
|
|
'to_map': "assets/to_map_dark.png",
|
|
'bgcolor': "#336645"})
|
|
|
|
def read_settings(self):
|
|
app_folder = os.path.dirname(os.path.abspath(__file__))
|
|
pickle_path = path.join(app_folder, 'settings.pickle')
|
|
|
|
if path.isfile(pickle_path):
|
|
with open(pickle_path, 'rb') as f:
|
|
settings = pickle.load(f)
|
|
else:
|
|
settings = {'settings_smoothing': 0.88,
|
|
'settings_compass_update': 0.88,
|
|
'settings_theme': "dark",
|
|
'settings_flock_server': 'http://flock.apps.lancis.ecologia.unam.mx'
|
|
}
|
|
|
|
with open(pickle_path, 'wb') as f:
|
|
pickle.dump(settings, f)
|
|
|
|
self.session_data.update(settings)
|
|
|
|
if self.session_data['settings_theme'] == "dark":
|
|
self.set_dark_theme(True)
|
|
else:
|
|
self.set_dark_theme(False)
|
|
|
|
|
|
def save_settings(self):
|
|
app_folder = os.path.dirname(os.path.abspath(__file__))
|
|
pickle_path = path.join(app_folder, 'settings.pickle')
|
|
|
|
settings = {k: self.session_data[k]
|
|
for k in self.session_data
|
|
if k.startswith('settings_')}
|
|
|
|
with open(pickle_path, 'wb') as f:
|
|
pickle.dump(settings, f)
|
|
|
|
def set_smoothing(self):
|
|
self.session_data['settings_smoothing'] = self.ss.ids.slider_smoothing.value
|
|
self.save_settings()
|
|
|
|
def set_flock_server(self):
|
|
self.session_data['settings_flock_server'] = self.ss.ids.flock_server.value
|
|
self.save_settings()
|
|
|
|
def set_update_freq(self):
|
|
Logger.info('setting update freq to %s' % self.ss.ids.slider_update_freq.value)
|
|
self.session_data['settings_update_freq'] = self.ss.ids.slider_update_freq.value
|
|
self.save_settings()
|
|
Clock.unschedule(self.flock)
|
|
Clock.schedule_interval(self.flock,
|
|
self.session_data.get('settings_update_freq', 10.0))
|
|
|
|
def set_compass_update(self):
|
|
self.session_data['settings_compass_update'] = self.ss.ids.slider_compass_update.value
|
|
self.compass_disable()
|
|
self.compass_enable()
|
|
self.save_settings()
|
|
|
|
def set_destination(self):
|
|
self.compass_enable()
|
|
self.session_data['dest_lat'] = self.ms.ids.centermark.lat
|
|
self.session_data['dest_lon'] = self.ms.ids.centermark.lon
|
|
self.ms.ids.mapview.center_on(self.session_data['dest_lat'],
|
|
self.session_data['dest_lon'])
|
|
self.flock_server_register()
|
|
Clock.schedule_interval(self.flock,
|
|
self.session_data.get('settings_update_freq', 10.0))
|
|
|
|
|
|
def set_dark_theme(self, active):
|
|
if active:
|
|
self.theme = {'needle': "assets/needle_dark.png",
|
|
'to_map': "assets/to_map_dark.png",
|
|
'bgcolor': "#336645",
|
|
'fgcolor': "#f3e8d2"
|
|
}
|
|
self.session_data['settings_theme'] = 'dark'
|
|
else:
|
|
self.theme = {'needle': "assets/needle_bright.png",
|
|
'to_map': "assets/to_map_bright.png",
|
|
'bgcolor': "#f3e8d2",
|
|
'fgcolor': "#346645"
|
|
}
|
|
self.session_data['settings_theme'] = 'bright'
|
|
self.save_settings()
|
|
|
|
|
|
def request_android_permissions(self):
|
|
from android.permissions import request_permissions, Permission
|
|
|
|
def callback(permissions, results):
|
|
if all([res for res in results]):
|
|
Logger.info("All permissions granted.")
|
|
else:
|
|
Logger.info("Some permissions refused. %s" % results)
|
|
|
|
request_permissions([Permission.ACCESS_COARSE_LOCATION,
|
|
Permission.ACCESS_FINE_LOCATION],
|
|
callback)
|
|
|
|
def gps_start(self, minTime, minDistance):
|
|
gps.start(minTime, minDistance)
|
|
|
|
def gps_stop(self):
|
|
gps.stop()
|
|
|
|
@mainthread
|
|
def on_location(self, **kwargs):
|
|
self.gps_data = kwargs
|
|
|
|
if ('dest_lat' not in self.session_data and 'dest_lon' not in self.session_data):
|
|
|
|
self.session_data['dest_lat'] = self.gps_data['lat']
|
|
self.session_data['dest_lon'] = self.gps_data['lon']
|
|
|
|
self.ms.ids.mapview.center_on(self.session_data['dest_lat'],
|
|
self.session_data['dest_lon'])
|
|
else:
|
|
dest_distance = geodesic(
|
|
(self.gps_data['lat'], self.gps_data['lon']),
|
|
(self.session_data['dest_lat'], self.session_data['dest_lon'])).kilometers
|
|
if dest_distance < 1:
|
|
dest_distance *= 1000
|
|
dest_distance = "%i metros" % dest_distance
|
|
else:
|
|
dest_distance = "%0.1f km" % dest_distance
|
|
|
|
km_per_mile = 1.609344
|
|
self.dest_distance = "Destino: %s\nVel: %0.1f km/h" % (dest_distance,
|
|
self.gps_data['speed'] * km_per_mile)
|
|
|
|
|
|
if 'flock_size' in self.session_data:
|
|
flock_distance = geodesic(
|
|
(self.gps_data['lat'], self.gps_data['lon']),
|
|
(self.session_data['flock_lat'], self.session_data['flock_lon'])).kilometers
|
|
if flock_distance < 1:
|
|
flock_distance *= 1000
|
|
flock_distance = "%i metros" % flock_distance
|
|
else:
|
|
flock_distance = "%0.1f km" % flock_distance
|
|
|
|
self.dashboard_flock = "%s @ %s\nvel: %0.1f km/h" % (self.session_data['flock_size'],
|
|
flock_distance,
|
|
self.session_data['flock_avg_speed'] * 1.609344)
|
|
else:
|
|
self.dashboard_flock = "no flocks"
|
|
|
|
|
|
self.session_data.update({'speed': self.gps_data['speed'] * 1.609344,
|
|
'bearing': self.gps_data['bearing'],
|
|
'lat': self.gps_data['lat'],
|
|
'lon': self.gps_data['lon']})
|
|
|
|
|
|
def center_map_on_gps(self):
|
|
self.ms.ids.mapview.center_on(self.gps_data['lat'],
|
|
self.gps_data['lon'])
|
|
|
|
|
|
def get_field(self, dt):
|
|
"""
|
|
get magnetic field for compass
|
|
"""
|
|
|
|
needle_angle = 0
|
|
if self.cs.facade.field != (None, None, None):
|
|
x, y, z = self.cs.facade.field
|
|
|
|
smoothingFactor = self.session_data.get('settings_smoothing', 0.88)
|
|
angle = atan2(y, x)
|
|
self.last_angle = smoothingFactor * self.last_angle + (1 - smoothingFactor) * angle
|
|
|
|
needle_angle = degrees(self.last_angle) - 90
|
|
|
|
# fix animation transition around the unit circle
|
|
if (self.needle_angle % 360) - needle_angle > 180:
|
|
needle_angle += 360
|
|
elif (self.needle_angle % 360) - needle_angle < -180:
|
|
needle_angle -= 360
|
|
# add the number of revolutions to the result
|
|
needle_angle += 360 * floor(self.needle_angle / 360.)
|
|
|
|
# animate the needle
|
|
if self.cs._anim:
|
|
self.cs._anim.stop(self)
|
|
self.cs._anim = Animation(needle_angle=needle_angle,
|
|
d=0.1,
|
|
t='out_quad')
|
|
|
|
lat1 = self.gps_data.get('lat', 0)
|
|
lon1 = self.gps_data.get('lon', 0)
|
|
|
|
lat2 = self.session_data.get('dest_lat', 0)
|
|
lon2 = self.session_data.get('dest_lon', 0)
|
|
|
|
dbearing = atan2(sin(lon2 - lon1) * cos(lat2),
|
|
cos(lat1) * sin(lat2)
|
|
- sin(lat1) * cos(lat2) * cos(lon2-lon1))
|
|
dbearing = self.needle_angle - degrees(dbearing)
|
|
|
|
self.cs._anim &= Animation(dbearing=dbearing,
|
|
d=0.1,
|
|
t='out_quad')
|
|
|
|
if 'flock_lat' in self.session_data:
|
|
self.cs.ids.to_flock.source = 'assets/needle_to_flock.png'
|
|
lat2 = self.session_data['flock_lat']
|
|
lon2 = self.session_data['flock_lon']
|
|
|
|
fbearing = atan2(sin(lon2 - lon1) * cos(lat2),
|
|
cos(lat1) * sin(lat2)
|
|
- sin(lat1) * cos(lat2) * cos(lon2-lon1))
|
|
fbearing = self.needle_angle - degrees(fbearing)
|
|
|
|
self.cs._anim &= Animation(fbearing=fbearing,
|
|
d=0.1,
|
|
t='out_quad')
|
|
|
|
else:
|
|
self.cs.ids.to_flock.source = 'assets/pivot.png'
|
|
|
|
self.cs._anim.start(self)
|
|
|
|
def compass_enable(self):
|
|
if hasattr(self, 'cs'):
|
|
self.cs.facade.enable()
|
|
Clock.schedule_interval(self.get_field,
|
|
1.0 - self.session_data.get('settings_compass_update', 0.88))
|
|
|
|
def compass_disable(self):
|
|
if hasattr(self, 'cs'):
|
|
self.cs.facade.disable()
|
|
Clock.unschedule(self.get_field)
|
|
|
|
def on_pause(self):
|
|
self.compass_disable()
|
|
self.gps_stop()
|
|
return True
|
|
|
|
def on_resume(self):
|
|
self.compass_enable()
|
|
self.gps_start(1000, 0)
|
|
|
|
def flock_server_register(self):
|
|
try:
|
|
register_url = "{server}/register/?dest_lon={dest_lon}&dest_lat={dest_lat}"
|
|
|
|
r = requests.get(register_url.format(server=self.session_data['settings_flock_server'],
|
|
dest_lon=self.session_data['dest_lon'],
|
|
dest_lat=self.session_data['dest_lat']),
|
|
timeout=0.5)
|
|
self.session_data['bike_id'] = r.json()['bike_id']
|
|
Logger.info('registered! got bike_id: %s' % self.session_data['bike_id'])
|
|
self.flock_server_update()
|
|
except requests.exceptions.Timeout:
|
|
Logger.info('comm timeout while registering with flock server')
|
|
except requests.exceptions.ConnectionError:
|
|
Logger.info('unable to connect while registering with flock server')
|
|
# finally:
|
|
# self.session_data.pop('bike_id', None)
|
|
# self.session_data.pop('speed', None)
|
|
# self.session_data.pop('bearing', None)
|
|
|
|
|
|
def flock_server_update(self):
|
|
|
|
try:
|
|
update_url = "{server}/update/{bike_id}/?lat={lat}&lon={lon}&speed={speed}&bearing={bearing}"
|
|
req = requests.get(update_url.format(server=self.session_data['settings_flock_server'],
|
|
bike_id=self.session_data['bike_id'],
|
|
speed=self.session_data.get('speed', 0),
|
|
bearing=self.session_data.get('bearing', 0),
|
|
lat=self.session_data['lat'],
|
|
lon=self.session_data['lon']),
|
|
timeout=0.5)
|
|
resp = req.json()
|
|
if resp:
|
|
self.session_data['last_update'] = isoparse(resp.pop('last_update'))
|
|
|
|
if 'flock_size' not in resp:
|
|
self.session_data.pop('flock_avg_speed', None)
|
|
self.session_data.pop('flock_lat', None)
|
|
self.session_data.pop('flock_lon', None)
|
|
self.session_data.pop('flock_size', None)
|
|
|
|
pLogger.info(self.session_data)
|
|
|
|
except requests.exceptions.Timeout:
|
|
Logger.info('comm timeout while updating flock server')
|
|
|
|
|
|
def get_session_timeout(self):
|
|
min_step = 100 # update at least every 100 meters
|
|
speed = self.session_data.get('speed', None)
|
|
if speed:
|
|
seconds = min_step / speed
|
|
else:
|
|
seconds = 100
|
|
|
|
return timedelta(seconds=seconds)
|
|
|
|
|
|
def flock(self, dt):
|
|
if ('bike_id' in self.session_data
|
|
and (self.session_data.get('last_update') + self.get_session_timeout()) > datetime.now()):
|
|
self.flock_server_update()
|
|
else:
|
|
self.flock_server_register()
|
|
|
|
|
|
def build(self):
|
|
|
|
# start GPS
|
|
try:
|
|
gps.configure(on_location=self.on_location)
|
|
except NotImplementedError:
|
|
import traceback
|
|
traceback.print_exc()
|
|
Logger.info('GPS is not implemented for your platform')
|
|
|
|
if platform == "android":
|
|
self.request_android_permissions()
|
|
|
|
self.gps_start(1000, 0)
|
|
|
|
# setup app screens
|
|
screen_manager = ScreenManager(transition=RiseInTransition())
|
|
|
|
# map screen
|
|
self.ms = MapScreen(name='map')
|
|
screen_manager.add_widget(self.ms)
|
|
|
|
# settings screen
|
|
self.ss = SettingsScreen(name='settings')
|
|
screen_manager.add_widget(self.ss)
|
|
|
|
|
|
# compass screen
|
|
|
|
# smoothing vars for compass
|
|
self.last_angle = 0
|
|
|
|
self.cs = CompassScreen(name='compass')
|
|
self.cs._anim = None
|
|
self.cs._anim1 = None
|
|
screen_manager.add_widget(self.cs)
|
|
return screen_manager
|
|
|
|
|
|
if __name__ == '__main__':
|
|
app = FlockompassApp()
|
|
app.read_settings()
|
|
app.run()
|