def calculate_slice_totals(self, sl, list_store): """ Calculate the totals for activities taking place during the given timeslice, and add the appropriate column values to the provided list store. """ q = self.session.query(Activity) q = q.filter(Activity.start_time >= sl.start_date) q = q.filter(Activity.start_time <= sl.end_date) q = q.filter(Activity.sport == self.metrics_sport) activities = q.all() log.debug("Found %s activities for slice: %s" % (len(activities), sl.season.name)) total_distance = Decimal("0") total_duration = Decimal("0") for acti in activities: log.debug(" %s - %s" % (acti.start_time, acti.sport.name)) total_distance += acti.distance total_duration += acti.duration speed = calculate_speed(self.session, total_distance, total_duration) pace = calculate_pace(self.session, total_distance, total_duration) list_store.append([ "%s %s" % (sl.season.name, sl.start_date.year), "%.2f" % (total_distance / 1000), format_time_str(total_duration), "%.2f" % (speed), "%.2f" % (pace / 60), "hr", ])
def init_metrics_tab(self): """ On startup initialization of the Metrics tab. """ # Setup metrics treeview columns: log.debug("Initializing metrics tab.") period_column = gtk.TreeViewColumn("Period") distance_column = gtk.TreeViewColumn("Distance (km)") time_column = gtk.TreeViewColumn("Time") avg_speed_column = gtk.TreeViewColumn("Speed (km/hr)") pace_column = gtk.TreeViewColumn("Pace (min/km)") avg_hr_column = gtk.TreeViewColumn("Heart Rate") self.metrics_tv.append_column(period_column) self.metrics_tv.append_column(distance_column) self.metrics_tv.append_column(time_column) self.metrics_tv.append_column(avg_speed_column) self.metrics_tv.append_column(pace_column) self.metrics_tv.append_column(avg_hr_column) cell = gtk.CellRendererText() period_column.pack_start(cell, expand=False) distance_column.pack_start(cell, expand=False) time_column.pack_start(cell, expand=False) avg_speed_column.pack_start(cell, expand=False) pace_column.pack_start(cell, expand=False) avg_hr_column.pack_start(cell, expand=False) period_column.set_attributes(cell, text=0) distance_column.set_attributes(cell, text=1) time_column.set_attributes(cell, text=2) avg_speed_column.set_attributes(cell, text=3) pace_column.set_attributes(cell, text=4) avg_hr_column.set_attributes(cell, text=5) self.populate_metrics_timeslice_combo()
def activity_showmap_cb(self, widget): """ Callback for when user selected Show Map from the activity popup menu. """ activity = self.get_selected_activity() log.debug("Opening map window for: %s" % activity) activity_details_window = BrowserWindow(activity) activity_details_window.show_all()
def generate_html(self): """ Generate HTML to render a Google Map for the configured activity. """ filepath = "/tmp/granola-%s-%s.html" % (self.activity.id, self.activity.sport.name) f = open(filepath, "w") if len(self.activity.laps) == 0: f.write("<html><body>No trackpoints</body></html>") f.close() return filepath elif len(self.activity.laps[0].tracks) == 0: f.write("<html><body>No trackpoints</body></html>") f.close() return filepath elif len(self.activity.laps[0].tracks[0].trackpoints) == 0: f.write("<html><body>No trackpoints</body></html>") f.close() return filepath (maxLat, maxLon, minLat, minLon, centerLat, centerLon) = \ self._calculate_center_coords(self.activity) span_km = distance_between_coords(maxLat, maxLon, minLat, minLon) log.debug("Distance between coordinates: %s" % span_km) zoom_level = get_zoom_level(span_km) log.debug("Zoom level: %s" % zoom_level) title = "Granola Activity Map: %s (%s)" % (self.activity.start_time, self.activity.sport.name) center_coords = self.activity.laps[0].tracks[0].trackpoints[0] f.write(HTML_HEADER % (title, centerLat, centerLon, zoom_level)) # TODO: iterating trackpoints a second time but probably faster # than doing the string concatenation we'd have to do otherwise. for lap in self.activity.laps: for track in lap.tracks: for trackpoint in track.trackpoints: if trackpoint.latitude is None: # TODO: Empty data in a trackpoint indicates a pause, # could display this easily. continue f.write("new GLatLng(%s, %s)," % (trackpoint.latitude, trackpoint.longitude)) f.write(""" ], "#0000ff", 3);""") #f.write("""map.addOverlay(new GMarker(new GLatLng(%s, %s)));""" % # (maxLat, maxLon)) #f.write("""map.addOverlay(new GMarker(new GLatLng(%s, %s)));""" % # (minLat, minLon)) f.write(HTML_FOOTER % (self.map_width, self.map_height)) f.close() return filepath
def apply_prefs(self, widget): """ Callback when apply button is pressed. Write settings to disk and close the window. """ log.debug("Applying preferences.") import_folder = self.import_folder_chooser.get_filename() log.debug(" import_folder = %s" % import_folder) self.config.set("import", "import_folder", import_folder) write_config(self.config) self.preferences_dialog.destroy()
def populate_metrics(self): """ Populate metrics. Call this whenever we need to update the metrics displayed based on some action by the user. """ log.debug("Populating metrics.") metrics_liststore = self.build_metrics_liststore() self.metrics_tv.set_model(metrics_liststore)
def connect_to_db(): """ Open a connection to our database. Should be called only once when this module is first imported. """ db_str = "sqlite:///%s" % SQLITE_DB log.debug("Connecting to database: %s" % db_str) # Set echo True to see lots of sqlalchemy output: db = create_engine(db_str, echo=False) return db
def scan_dir(self, directory): """ Scan a directory for new data files to import. """ if not os.path.exists(directory): raise Exception("No such directory: %s" % directory) log.debug("Scanning %s for new data." % directory) session = Session() for root, dirs, files in os.walk(directory): for file in files: if file.endswith(".tcx"): # try: self.import_file(session, os.path.join(root, file)) session.commit()
def show_activity(self, activity): """ Display the given activity on the map. """ # If we're already displaying this activity, don't do anything. # Useful check for some scenarios where the use changes a combo. if activity == self.current_activity: log.debug("Already displaying activity: %s" % activity) return if self.temp_file: log.info("Removing: %s" % self.temp_file) commands.getstatusoutput("rm %s" % self.temp_file) self.current_activity = activity generator = HtmlGenerator(activity, self.map_width, self.map_height) self.temp_file = generator.generate_html() log.debug("Wrote activity HTML to: %s" % self.temp_file) self._browser.open("file://%s" % self.temp_file)
def build_metrics_liststore(self): """ Return a ListStore with totals for the metrics tab. Results depend heavily on the values currently selected in the timeslice and sport comboboxes. """ list_store = gtk.ListStore( str, # period str, # distance str, #time str, # avg speed str, # pace str, # avg heart rate ) log.debug("Calculating metrics:") # Now things get interesting. Start with the earliest activity, # determine which season it falls into, then calculate the actual # season start/end dates for each season that follows it up until # we cross the date of our last activity. Then construct queries. # Grab the first activity date: q = self.session.query(Activity).order_by(Activity.start_time) q = q.filter(Activity.sport == self.metrics_sport).limit(1) first_activity = q.first() if first_activity is None: # No activies for the current type, not much we can do here: return log.debug("First %s activity: %s" % (self.metrics_sport, first_activity.start_time)) q = self.session.query(Activity).order_by(Activity.start_time.desc()) q = q.filter(Activity.sport == self.metrics_sport).limit(1) last_activity = q.first() log.debug("Last %s activity: %s" % (self.metrics_sport, last_activity.start_time)) iter = self.metrics_timeslice_combo.get_active_iter() timeslice = self.metrics_timeslice_combo.get_model().get_value(iter, 0) log.debug("Current metrics timeslice: %s" % timeslice) seasons = None if timeslice == "monthly": seasons = MONTHLY_SEASONS elif timeslice == "yearly": seasons = YEARLY_SEASONS slices = build_season_slices(seasons, first_activity.start_time, last_activity.start_time) for s in slices: self.calculate_slice_totals(s, list_store) return list_store
def populate_metrics_timeslice_combo(self): """ Populate the metrics timeslice dropdown. """ log.debug("Populating metrics timeslice dropdown.") timeslice_liststore = gtk.ListStore(str) self.metrics_timeslice_combo.set_model(timeslice_liststore) cell = gtk.CellRendererText() self.metrics_timeslice_combo.pack_start(cell, True) self.metrics_timeslice_combo.add_attribute(cell, 'text', 0) # TODO: I18N problem here: self.metrics_timeslice_combo.append_text("monthly") self.metrics_timeslice_combo.append_text("yearly") #self.metrics_timeslice_combo.append_text("my seasons") # Activate the first item: iter = timeslice_liststore.get_iter_first() self.metrics_timeslice_combo.set_active_iter(iter)
def __init__(self, config): log.debug("Opening Preferences dialog.") self.config = config glade_file = 'granola/glade/prefs-dialog.glade' self.glade_xml = gtk.Builder() self.glade_xml.add_from_file(find_file_on_path(glade_file)) self.preferences_dialog = self.glade_xml.get_object("prefs_dialog") signals = { 'on_apply_button_clicked': self.apply_prefs, 'on_cancel_button_clicked': self.cancel, } self.glade_xml.connect_signals(signals) self.import_folder_chooser = self.glade_xml.get_object( "import_folder_filechooserbutton") self.import_folder_chooser.set_filename( self.config.get("import", "import_folder")) self.preferences_dialog.show_all()
def activity_delete_cb(self, widget): """ Confirm dialog if the user actually wishes to delete an activity. """ dialog = gtk.Dialog(title="Are you sure?", flags=gtk.DIALOG_MODAL, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) result = dialog.run() dialog.destroy() if result == gtk.RESPONSE_ACCEPT: activity = self.get_selected_activity() log.debug("Deleting! %s" % activity) self.session.delete(activity) self.session.commit() # TODO: More expensive than it needs to be, could just delete row from # model? self.populate_activities() self.populate_metrics() else: log.debug("NOT deleting activity.")
def populate_activities(self): """ Populate activity list. Called not just when we initialize the UI but also when the user changes the Sport combobox and we need to change the list of activities displayed. """ # TODO: Is it possible to keep the model here, and "filter" it # with GTK? Would be much more efficient. # Grab current selection if there is one, we'll preserve it if that # activity is present in the new list. tree_selection = self.activity_tv.get_selection() (model, iter) = tree_selection.get_selected() preserve_activity_id = None if (iter != None): preserve_activity_id = model.get_value(iter, 0) model = self.build_activity_liststore() self.activity_tv.set_model(model) iter = model.get_iter_first() # Points to the row we'll select if (preserve_activity_id is not None): log.debug("Searching for activity to preserve.") search_iter = model.get_iter_first() while search_iter is not None: log.debug("Examining: %s" % model.get_value(search_iter, 0)) if model.get_value(search_iter, 0) == preserve_activity_id: log.debug("Re-selecting activity: %s" % preserve_activity_id) iter = search_iter break search_iter = model.iter_next(search_iter) else: log.debug("No activity selected, auto-selecting first row.") if iter is not None: tree_selection.select_iter(iter) self.display_activity(self.get_selected_activity())
def create_first_slice(seasons, activity_date): """ Create the appropriate season slice for the given activity date. Uses the given list of seasons to identify which season this activity falls into. """ the_season = None i = 0 season_index = None for s in seasons: if s.month <= activity_date.month and s.day <= activity_date.day: the_season = s season_index = i i = i + 1 # If we couldn't find a season that starts before our activity date, # we want the last one. (which must wrap around over the year) if the_season is None: the_season = seasons[-1] season_index = len(seasons) - 1 # Should have found the season with the start date closest to our # activity by now, so we create the slice for it: log.debug("found season %s: %s - %s" % (the_season.name, the_season.month, the_season.day)) # Previous year if the season month is greater than our activity's: start_year = activity_date.year if the_season.month > activity_date.month or (the_season.month == activity_date.month and the_season.day > activity_date.day): start_year = start_year - 1 start_date = datetime(year=start_year, month=the_season.month, day=the_season.day) log.debug("start_date = %s" % start_date) # Now for the end date: next_season = seasons[(season_index + 1) % len(seasons)] log.debug("next season %s: %s - %s" % (next_season.name, next_season.month, next_season.day)) return SeasonSlice(the_season, start_date, next_season)
def cancel(self, widget): """ Don't apply any settings and close the window. """ log.debug("Cancel button pressed, closing preferences dialog.") self.preferences_dialog.destroy()
def __init__(self, config): log.debug("Starting GTK UI.") gtk.gdk.threads_init() self.config = config self.session = Session() glade_file = 'granola/glade/mainwindow.glade' self.glade_xml = gtk.Builder() self.glade_xml.add_from_file(find_file_on_path(glade_file)) main_window = self.glade_xml.get_object('main_window') self.activity_hbox = self.glade_xml.get_object( 'activity_hbox') # Filter main list of activities based on this, None = show all. # Storing just the string name here. self.filter_sport = None self.metrics_sport = None # References to various widgets used throughout the class: self.activity_popup_menu = self.glade_xml.get_object( 'activity_popup_menu') self.activity_tv = self.glade_xml.get_object('activity_treeview') self.metrics_tv = self.glade_xml.get_object('metrics_treeview') self.sport_filter_combobox = self.glade_xml.get_object( 'sport_filter_combobox') self.metrics_sport_combo = self.glade_xml.get_object( 'metrics_sport_combo') self.metrics_timeslice_combo = self.glade_xml.get_object( 'metrics_timeslice_combo') self.init_ui() signals = { 'on_quit_menu_item_activate': self.shutdown, 'on_main_window_destroy': self.shutdown, 'on_prefs_menu_item_activate': self.open_prefs_dialog_cb, 'on_activity_treeview_button_press_event': self.activity_tv_click_cb, 'on_activity_treeview_row_activated': self.activity_tv_doubleclick_cb, 'on_sport_filter_combobox_changed': self.activities_sport_combo_cb, 'on_metrics_sport_combo_changed': self.metrics_sport_combo_cb, 'on_metrics_timeslice_combo_changed': self.metrics_timeslice_combo_cb, 'on_activity_popup_delete_activate': self.activity_delete_cb, 'on_activity_popup_showmap_activate': self.activity_showmap_cb, } self.glade_xml.connect_signals(signals) self.running = self.session.query(Sport).filter( Sport.name == RUNNING).one() self.biking = self.session.query(Sport).filter( Sport.name == BIKING).one() self.browser_widget = BrowserWidget() self.activity_hbox.pack_start(self.browser_widget) self.populate_activities() self.populate_metrics() main_window.show_all()
def build_season_slices(seasons, first_activity_date, last_activity_date): """ Return a list of all season slices using the configured season boundaries, first activity date, and last activity date. """ starting_slice = create_first_slice(seasons, first_activity_date) log.debug("Building all season slices:") log.debug(" starting slice: %s" % starting_slice) log.debug(" last activity: %s" % last_activity_date) # What season does the starting slice point to? season_index = 0 for s in seasons: if s.month == starting_slice.start_date.month and \ s.day == starting_slice.start_date.day: break else: season_index += 1 log.debug("season_index = %s" % season_index) # Keep building season slices until one ends beyond the date of # our last activity: all_slices = [starting_slice] while all_slices[-1].end_date <= last_activity_date: log.debug("Building new slice:") season_index = (season_index + 1) % len(seasons) next_season = seasons[season_index] log.debug(" next_season = %s" % next_season) start_date = all_slices[-1].end_date + timedelta(seconds=1) log.debug(" start_date = %s" % start_date) new_slice = SeasonSlice(next_season, start_date, seasons[(season_index + 1) % len(seasons)]) log.debug(" new_slice = %s" % new_slice) all_slices.append(new_slice) log.debug("Seasons:") for s in all_slices: log.debug(s) return all_slices
def debug_activity(activity): """ Log debug info on this activity. """ log.debug("Activity: %s" % activity.start_time) i = 1 for lap in activity.laps: log.debug(" Lap %s:" % i) log.debug(" Start Time: %s" % lap.start_time) log.debug(" Duration: %s seconds" % lap.duration) log.debug(" Distance: %s meters" % lap.distance) log.debug(" Max speed: %s meters/second" % lap.speed_max) log.debug(" Calories: %s" % lap.calories) log.debug(" Max heart rate: %s bpm" % lap.heart_rate_max) log.debug(" Avg heart rate: %s bpm" % lap.heart_rate_avg) i += 1
def init_activities_tab(self): """ On startup initialization of the Activities tab. """ # Setup activity treeview columns: log.debug("Initializing activities tab.") sport_column = gtk.TreeViewColumn("Sport") date_column = gtk.TreeViewColumn("Date") distance_column = gtk.TreeViewColumn("Distance (km)") time_column = gtk.TreeViewColumn("Time") avg_speed_column = gtk.TreeViewColumn("Speed (km/hr)") pace_column = gtk.TreeViewColumn("Pace (min/km)") heart_rate_column = gtk.TreeViewColumn("Heart Rate") self.activity_tv.append_column(sport_column) self.activity_tv.append_column(date_column) self.activity_tv.append_column(distance_column) self.activity_tv.append_column(time_column) self.activity_tv.append_column(avg_speed_column) self.activity_tv.append_column(pace_column) self.activity_tv.append_column(heart_rate_column) cell = gtk.CellRendererText() sport_column.pack_start(cell, expand=False) date_column.pack_start(cell, expand=False) distance_column.pack_start(cell, expand=False) time_column.pack_start(cell, expand=False) avg_speed_column.pack_start(cell, expand=False) pace_column.pack_start(cell, expand=False) heart_rate_column.pack_start(cell, expand=False) sport_column.set_attributes(cell, text=5) date_column.set_attributes(cell, text=1) distance_column.set_attributes(cell, text=2) time_column.set_attributes(cell, text=3) avg_speed_column.set_attributes(cell, text=4) pace_column.set_attributes(cell, text=6) heart_rate_column.set_attributes(cell, text=7) self.lap_tv = self.glade_xml.get_object('lap_treeview') # Setup lap treeview columns: number_column = gtk.TreeViewColumn("Lap") distance_column = gtk.TreeViewColumn("Distance (km)") time_column = gtk.TreeViewColumn("Time") avg_speed_column = gtk.TreeViewColumn("Speed (km/hr)") avg_hr_column = gtk.TreeViewColumn("Avg HR") max_hr_column = gtk.TreeViewColumn("Max HR") self.lap_tv.append_column(number_column) self.lap_tv.append_column(distance_column) self.lap_tv.append_column(time_column) self.lap_tv.append_column(avg_speed_column) self.lap_tv.append_column(avg_hr_column) self.lap_tv.append_column(max_hr_column) cell = gtk.CellRendererText() number_column.pack_start(cell, expand=False) distance_column.pack_start(cell, expand=False) time_column.pack_start(cell, expand=False) avg_speed_column.pack_start(cell, expand=False) avg_hr_column.pack_start(cell, expand=False) max_hr_column.pack_start(cell, expand=False) number_column.set_attributes(cell, text=0) distance_column.set_attributes(cell, text=1) time_column.set_attributes(cell, text=2) avg_speed_column.set_attributes(cell, text=3) avg_hr_column.set_attributes(cell, text=4) max_hr_column.set_attributes(cell, text=5) self.populate_sport_combos()