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.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 geopy.distance import geodesic from os import path import pickle FLOCK_SERVER="http://192.168.0.8: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 dump(self, dt): print(dt, self.gps_data, self.session_data) 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.777, 'settings_theme': "dark" } 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_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']) 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]): print("All permissions granted.") else: print("Some permissions refused.", 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 self.dump(1) 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 self.dest_distance = "Destino: %s\nVel: %0.1f km/h" % (dest_distance, self.gps_data['speed'] * 1.609344) 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['lat'] lon1 = self.gps_data['lon'] lat2 = self.session_data['dest_lat'] lon2 = self.session_data['dest_lon'] 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') self.cs._anim.start(self) def compass_enable(self): self.cs.facade.enable() Clock.schedule_interval(self.get_field, 1.0 - self.session_data.get('settings_compass_update', 0.777)) def compass_disable(self): 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): register_url = "{server}/register/?dest_lon={dest_lon}&dest_lat={dest_lat}" r = requests.get(register_url.format(server=FLOCK_SERVER, dest_lon=self.session_data['dest_lon'], dest_lat=self.session_data['dest_lat'])) out = r.json() self.session_data['bike_id'] = out['bike_id'] def flock_server_update(self): update_url = "{server}/update/{bike_id}/?lat={lat}&lon={lon}&speed={speed}&bearing={bearing}" req = requests.get(update_url.format(server=FLOCK_SERVER, bike_id=self.session_data['bike_id'], speed=self.session_data['speed'], bearing=self.session_data['bearing'], lat=self.session_data['lat'], lon=self.session_data['lon'])) resp = req.json() self.session_data.update(resp) def flock(self, dt): if 'bike_id' in self.session_data: 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() print('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) Clock.schedule_interval(self.flock, 20.0) return screen_manager if __name__ == '__main__': app = FlockompassApp() app.read_settings() app.run()