diff --git a/.gitignore b/.gitignore index 7d28498..aeb594b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +build/ *.gpx .vscode/settings.json *.pyc diff --git a/bicycle_statistics/__main__.py b/bicycle_statistics/__main__.py index 54c62c2..f99f882 100644 --- a/bicycle_statistics/__main__.py +++ b/bicycle_statistics/__main__.py @@ -14,7 +14,7 @@ LOG_FORMAT = "%(asctime)s %(levelname)s %(message)s" UPDATE_INTERVAL = 60 logging.basicConfig(format=LOG_FORMAT, level=log_level, filename=LOG_FILE) -#logging.basicConfig(format=LOG_FORMAT, level=log_level) +# logging.basicConfig(format=LOG_FORMAT, level=log_level) log = logging.getLogger('bicycle-statistics') def parse_args(): diff --git a/gpx2html/__init__.py b/gpx2html/__init__.py index b1283d1..452c699 100755 --- a/gpx2html/__init__.py +++ b/gpx2html/__init__.py @@ -47,6 +47,46 @@ def plot_bar_chart(labels, ticklabels, values, title, xlabel, ylabel, filename, plt.savefig(filename) plt.close('all') + +def plot_line_chart(values, ticklabels, title, xlabel, ylabel, filename, xtick_rotation=0): + '''Plot a line chart. + + Args: + values (dict): key: line name + value (list): line values + ticklabels (list): Names for the tick labels (must be same length as value list). + title (str): Title of the chart. + + ''' + fig = plt.figure() + ax1 = fig.add_subplot(111) + ax1.grid(zorder=0) + ax1.spines["top"].set_visible(False) + ax1.spines["bottom"].set_visible(False) + ax1.spines["left"].set_visible(False) + ax1.spines["right"].set_visible(False) + + plt.title(title) + plt.xlabel(xlabel) + plt.ylabel(ylabel) + + for key in values.keys(): + if len(ticklabels) == len(values[key]): + plt.plot(ticklabels, values[key], label=key) + else: + short_ticklabels = list() + for i in range(0, len(values[key])): + short_ticklabels.append(ticklabels[i]) + plt.plot(short_ticklabels, values[key], label=key) + + x_base = numpy.arange(len(ticklabels)) + plt.xticks(x_base, ticklabels, rotation=xtick_rotation) + + plt.legend() + plt.savefig(filename) + plt.close('all') + + class Gpx2Html(object): def __init__(self, infolder, outfolder, logger): self.logger = logger @@ -67,9 +107,12 @@ class Gpx2Html(object): self.logger.info("Begin update of png's/html...") distances = list() avg_speeds = list() + distances_dict = dict() for year in self.tracks.years(): distances.append(self.tracks.distances(year)) + distances_dict[year] = self.tracks.distances(year) avg_speeds.append(self.tracks.avg_speeds(year)) + self.logger.info("{}: {}".format(year, self.tracks.distances)) plot_bar_chart(self.tracks.years(), MONTH_LABELS, distances, 'Distance', 'Month', 'km', @@ -79,6 +122,26 @@ class Gpx2Html(object): 'Average Speed', 'Month', 'km/h', os.path.join(self.outfolder, 'avg_spd.png')) + # Accumulated distance: + accumulated_distances = dict() + for year in distances_dict.keys(): + accumulated_distance = list() + accumulated_distance.append(distances_dict[year][0]) + for i in range(1, len(distances_dict[year])): + accumulated_distance.append(accumulated_distance[i - 1] + distances_dict[year][i]) + accumulated_distances[year] = accumulated_distance + + current_year = datetime.datetime.today().year + current_month = datetime.datetime.today().month + current_year_distance = list() + for i in range(0, current_month): + current_year_distance.append(accumulated_distances[current_year][i]) + accumulated_distances[current_year] = current_year_distance + + plot_line_chart(accumulated_distances, MONTH_LABELS, + "accumulated distance", 'Month', 'km', + os.path.join(self.outfolder, 'acc_dist.png')) + end_date = datetime.datetime.today() start_date = end_date - datetime.timedelta(days=14) last_n_tracks = self.tracks.tracks(start_date, end_date) @@ -182,6 +245,7 @@ class Gpx2Html(object): handle.write('

\n') handle.write('Distance\n'.format('distance.png')) + handle.write('Distance\n'.format('acc_dist.png')) handle.write('Distance\n'.format('avg_spd.png')) handle.write('Distance\n'.format('last_14_days.png')) handle.write('

\n') diff --git a/gpx_parser/__init__.py b/gpx_parser/__init__.py index 254e4ab..5948b52 100644 --- a/gpx_parser/__init__.py +++ b/gpx_parser/__init__.py @@ -1,115 +1,255 @@ +#!/usr/bin/env python + +import argparse import datetime import glob +import sys +import gpx_parser +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +import numpy import os -import gpxpy -import gpxpy.gpx -from geopy import distance -from geopy import Point import pandas as pd +import collections +from gpx_parser import Tracks -class Segment(object): - start_time = None - end_time = None - distance = 0.0 # [m] - -class Track(object): - start_time = None - end_time = None - distance = 0.0 # [m] - avg_speed = 0.0 # [km/h] - duration = None - - def __init__(self, raw_track): - for segment in raw_track.segments: - seg = Segment() - for i in range(1, len(segment.points)): - if self.start_time is None: - self.start_time = segment.points[i - 1].time - if seg.start_time is None: - seg.start_time = segment.points[i - 1].time - seg.end_time = segment.points[i - 1].time - point1 = Point(str(segment.points[i - 1].latitude) + \ - ' ' + str(segment.points[i - 1].longitude)) - point2 = Point(str(segment.points[i].latitude) + \ - ' ' + str(segment.points[i].longitude)) - seg.distance += distance.distance(point1, point2).meters - - try: - if self.duration is None: - self.duration = seg.end_time - seg.start_time - else: - self.duration += seg.end_time - seg.start_time - except Exception: - # TODO: Add logging mechanism. - pass - self.end_time = seg.end_time - self.distance += seg.distance - self.avg_speed = self.distance / self.duration.total_seconds() * 3.6 +MONTH_LABELS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] -class Tracks(object): - __distance = dict() - __duration = dict() - __avg_speed = dict() - __tracks = list() - __files = list() +def plot_bar_chart(labels, ticklabels, values, title, xlabel, ylabel, filename, xtick_rotation=0): + fig = plt.figure() + ax1 = fig.add_subplot(111) + ax1.grid(zorder=0) + ax1.spines["top"].set_visible(False) + ax1.spines["bottom"].set_visible(False) + ax1.spines["left"].set_visible(False) + ax1.spines["right"].set_visible(False) - def __init__(self, logger): + plt.title(title) + plt.xlabel(xlabel) + plt.ylabel(ylabel) + + width = 1.0 / len(values) - 0.03 + x_base = numpy.arange(len(ticklabels)) + x_pos = list() + + for i in range(len(values)): + x_pos.append([x + (width / 2) + i * width for x in range(len(x_base))]) + plt.bar(x_pos[i], values[i], width=width, label=labels[i], zorder=2) + + plt.xticks(x_base, ticklabels, rotation=xtick_rotation) + + # Tweak spacing to prevent clipping of tick-labels + plt.subplots_adjust(bottom=0.2) + + plt.legend() + plt.savefig(filename) + plt.close('all') + + +def plot_line_chart(values, ticklabels, title, xlabel, ylabel, filename, xtick_rotation=0): + '''Plot a line chart. + + Args: + values (dict): key: line name + value (list): line values + ticklabels (list): Names for the tick labels (must be same length as value list). + title (str): Title of the chart. + + ''' + fig = plt.figure() + ax1 = fig.add_subplot(111) + ax1.grid(zorder=0) + ax1.spines["top"].set_visible(False) + ax1.spines["bottom"].set_visible(False) + ax1.spines["left"].set_visible(False) + ax1.spines["right"].set_visible(False) + + plt.title(title) + plt.xlabel(xlabel) + plt.ylabel(ylabel) + + for key in values.keys(): + if len(ticklabels) == len(values[key]): + plt.plot(ticklabels, values[key], label=key) + else: + short_ticklabels = list() + for i in range(0, len(values[key])): + short_ticklabels.append(ticklabels[i]) + plt.plot(short_ticklabels, values[key], label=key) + + x_base = numpy.arange(len(ticklabels)) + plt.xticks(x_base, ticklabels, rotation=xtick_rotation) + + plt.legend() + plt.savefig(filename) + plt.close('all') + + +class Gpx2Html(object): + def __init__(self, infolder, outfolder, logger): self.logger = logger + self.infolder = infolder + self.outfolder = os.path.abspath(outfolder) - def add(self, filename): - if filename not in self.__files: - self.logger.info("Adding file %s.", filename) - with open(filename, 'r') as f: - self.__files.append(filename) - gpx = gpxpy.parse(f) - for raw in gpx.tracks: - track = Track(raw) - self.__tracks.append(track) - trk_month = track.start_time.month - trk_year = track.start_time.year + if not os.path.exists(self.outfolder): + os.makedirs(self.outfolder) - if trk_year not in self.__distance: - self.__distance[trk_year] = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0} - self.__distance[trk_year][trk_month] += track.distance / 1000 + self.tracks = Tracks(logger) + self.update() - if trk_year not in self.__duration: - self.__duration[trk_year] = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0} - self.__duration[trk_year][trk_month] += track.duration.total_seconds() + def update(self): + infiles = glob.glob(os.path.join(self.infolder, '*.gpx')) + for filename in infiles: + self.tracks.add(filename) - if trk_year not in self.__avg_speed: - self.__avg_speed[trk_year] = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0} - self.__avg_speed[trk_year][trk_month] = self.__distance[trk_year][trk_month] / (self.__duration[trk_year][trk_month] / 3600) - self.logger.info("Adding done.") + self.logger.info("Begin update of png's/html...") + distances = list() + avg_speeds = list() + distances_dict = dict() + for year in self.tracks.years(): + distances.append(self.tracks.distances(year)) + distances_dict[year] = self.tracks.distances(year) + avg_speeds.append(self.tracks.avg_speeds(year)) + self.logger.info("{}: {}".format(year, self.tracks.distances)) - def years(self): - ret = None - try: - ret = sorted(self.__distance.keys()) - except Exception: - pass - return ret + plot_bar_chart(self.tracks.years(), MONTH_LABELS, distances, + 'Distance', 'Month', 'km', + os.path.join(self.outfolder, 'distance.png')) - def distances(self, year): - ret = 0 - try: - ret = self.__distance[year].values() - except Exception: - pass - return ret + plot_bar_chart(self.tracks.years(), MONTH_LABELS, avg_speeds, + 'Average Speed', 'Month', 'km/h', + os.path.join(self.outfolder, 'avg_spd.png')) - def avg_speeds(self, year): - ret = None - try: - ret = self.__avg_speed[year].values() - except Exception: - pass - return ret + # Accumulated distance: + accumulated_distances = dict() + for year in distances_dict.keys(): + accumulated_distance = list() + accumulated_distance.append(0) + for i in range(0, len(distances_dict[year])): + accumulated_distance.append(accumulated_distance[i] + distances_dict[year][i]) + accumulated_distances[year] = accumulated_distance - def tracks(self, start_date, end_date): - tracks = list() + current_year = datetime.datetime.today().year + current_month = datetime.datetime.today().month + current_year_distance = list() + for i in range(0, current_month): + current_year_distance.append(accumulated_distances[current_year][i]) + accumulated_distances[current_year] = current_year_distance + + plot_line_chart(accumulated_distances, [""] + MONTH_LABELS, + "accumulated distance", 'Month', 'km', + os.path.join(self.outfolder, 'acc_dist.png')) + + end_date = datetime.datetime.today() + start_date = end_date - datetime.timedelta(days=14) + last_n_tracks = self.tracks.tracks(start_date, end_date) + last_n_distances = dict() + last_n_durations = dict() dates = pd.date_range(start_date.date(), end_date.date()) - for track in self.__tracks: - if track.start_time.date() in dates: - tracks.append(track) - return tracks + for date in dates: + for track in last_n_tracks: + if date.date() == track.start_time.date(): + get = 0 + try: + get = last_n_distances[date.date()] + except KeyError: + pass + if get == 0: + last_n_distances[date.date()] = track.distance / 1000 + else: + last_n_distances[date.date()] += track.distance / 1000 + try: + get = last_n_durations[date.date()] + except KeyError: + pass + if get == 0: + last_n_durations[date.date()] = track.duration.total_seconds() + else: + last_n_durations[date.date()] += track.duration.total_seconds() + else: + try: + get = last_n_distances[date.date()] + except KeyError: + last_n_distances[date.date()] = 0 + try: + get = last_n_durations[date.date()] + except KeyError: + last_n_durations[date.date()] = 0 + last_n_dist = list() + last_n_dur = list() + last_n_avg = list() + last_n_dates = list() + for date in dates: + try: + last_n_dist.append(last_n_distances[date.date()]) + except KeyError: + last_n_dist.append(0) + try: + last_n_dur.append(last_n_durations[date.date()]) + except KeyError: + last_n_dur.append(0) + date_str = "{0:04d}-{1:02d}-{2:02d}".format(date.year, date.month, date.day) + last_n_dates.append(date_str) + try: + if last_n_durations[date.date()] == 0: + last_n_avg.append(0) + else: + last_n_avg.append(last_n_distances[date.date()] / + (last_n_durations[date.date()] / 3600)) + except KeyError: + last_n_avg.append(0) + + plot_bar_chart(["Distance", "Average speed"], last_n_dates, + [last_n_dist, last_n_avg], + 'Last 14 days', 'Date', 'km, km/h', + os.path.join(self.outfolder, 'last_14_days.png'), 90) + self.__write_html_file() + self.logger.info("End update of png's/html...") + + def __write_html_file(self): + with open(os.path.join(self.outfolder, 'index.html'), 'w') as handle: + handle.write('\n') + handle.write('\n') + handle.write('\n') + handle.write('\n') + handle.write(' Bicycle \n') + handle.write('\n') + handle.write('\n') + handle.write('
\n') + handle.write('

Bicycle

\n') + handle.write('

\n') + + handle.write('\n') + handle.write('\n') + for year in self.tracks.years(): + handle.write('\n'.format(year)) + handle.write('\n') + + handle.write('\n') + for year in self.tracks.years(): + handle.write('\n'.format(round(sum(self.tracks.distances(year)), 1))) + handle.write('\n') + handle.write('
{}
{} km
\n') + + handle.write('

\n') + + handle.write('

\n') + handle.write('Distance\n'.format('distance.png')) + handle.write('Distance\n'.format('acc_dist.png')) + handle.write('Distance\n'.format('avg_spd.png')) + handle.write('Distance\n'.format('last_14_days.png')) + handle.write('

\n') + + handle.write('\n') + handle.write('
\n') + handle.write('\n') diff --git a/input_observer/__init__.py b/input_observer/__init__.py deleted file mode 100644 index b645b41..0000000 --- a/input_observer/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -from watchdog.events import PatternMatchingEventHandler - -import threading - -class InputObserver(PatternMatchingEventHandler): - def __init__(self, patterns=None, ignore_patterns=None, - ignore_directories=False, case_sensitive=False): - super(InputObserver, self).__init__(patterns, ignore_patterns, - ignore_directories, case_sensitive) - - self.lock = threading.Lock() - self.lock.acquire() - self.new_filename = None - self.run_condition = True - - -# def on_created(self, event): - def on_any_event(self, event): - self.new_filename = event.src_path - self.lock.release() - - - def get_new_file(self): - self.lock.acquire() # don't release - will be released with next on_created - if self.run_condition == True: - return self.new_filename - else: - return None - - def stop(self): - self.run_condition = False - self.lock.release() diff --git a/setup.py b/setup.py index 792454b..7ff72e9 100755 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ NAME = 'bicycle-statistics' VERSION = '0.2.0' AUTHOR = 'Thomas Klaehn' EMAIL = 'tkl@blackfinn.de' -PACKAGES = ['bicycle_statistics', 'gpx_parser', 'gpx2html', 'input_observer'] +PACKAGES = ['bicycle_statistics', 'gpx_parser', 'gpx2html'] SCRIPTS = ['example-gpx-parser', 'bicycle-stat'] DAEMON_START_SCRIPT = os.path.join("/lib/systemd/system", "bicycle-stat.service")