From 492c67bec3b8e8f523bf489b2e12f92019b20b41 Mon Sep 17 00:00:00 2001 From: Thomas Klaehn Date: Thu, 26 Jul 2018 09:53:44 +0200 Subject: [PATCH] bicycle-statistics: Restructure Signed-off-by: Thomas Klaehn --- .gitignore | 2 +- bicycle-stat | 3 + bicycle-stat.service | 10 ++ bicycle_statistics/__init__.py | 0 bicycle_statistics/__main__.py | 18 ++++ gpx2html | 191 --------------------------------- gpx2html/__init__.py | 184 +++++++++++++++++++++++++++++++ setup.py | 18 +++- 8 files changed, 230 insertions(+), 196 deletions(-) create mode 100755 bicycle-stat create mode 100644 bicycle-stat.service create mode 100644 bicycle_statistics/__init__.py create mode 100644 bicycle_statistics/__main__.py delete mode 100755 gpx2html create mode 100755 gpx2html/__init__.py diff --git a/.gitignore b/.gitignore index a51e862..e68c680 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ data/ .vscode/settings.json -gpx_parser/__init__.pyc +*.pyc diff --git a/bicycle-stat b/bicycle-stat new file mode 100755 index 0000000..f8b05fe --- /dev/null +++ b/bicycle-stat @@ -0,0 +1,3 @@ +#!/bin/bash + +python -m bicycle_statistics $@ diff --git a/bicycle-stat.service b/bicycle-stat.service new file mode 100644 index 0000000..7bae109 --- /dev/null +++ b/bicycle-stat.service @@ -0,0 +1,10 @@ +[Unit] +Description=Bicucle statistics +After=multi-user.target + +[Service] +Type=idle +ExecStart=/usr/local/bin/bicycle-stat + +[Install] +WantedBy=multi-user.target diff --git a/bicycle_statistics/__init__.py b/bicycle_statistics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bicycle_statistics/__main__.py b/bicycle_statistics/__main__.py new file mode 100644 index 0000000..44b5ca5 --- /dev/null +++ b/bicycle_statistics/__main__.py @@ -0,0 +1,18 @@ +import argparse +import sys +from gpx2html import Gpx2Html + +def parse_args(): + '''Shell argument parser.''' + parser = argparse.ArgumentParser() + parser.add_argument('infolder', help='Specify the in folder.') + parser.add_argument('outfolder', help='Specify the out folder.') + return parser.parse_args() + +def main(): + args = parse_args() + gpx2html = Gpx2Html(args.infolder, args.outfolder) + gpx2html.process() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/gpx2html b/gpx2html deleted file mode 100755 index 700bcb4..0000000 --- a/gpx2html +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env python - -import argparse -import datetime -import sys -import gpx_parser -import matplotlib -matplotlib.use('Agg') -import matplotlib.pyplot as plt -import numpy -import os -import pandas as pd -import collections - -MONTH_LABELS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] - -def parse_args(): - '''Shell argument parser.''' - parser = argparse.ArgumentParser() - parser.add_argument('infolder', help='Specify the in folder.') - parser.add_argument('outfolder', help='Specify the out folder.') - return parser.parse_args() - - -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) - - 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) - - -def main(): - args = parse_args() - tracks = gpx_parser.Tracks(args.infolder) - years_distance = list() - years_avg_spd = list() - years = list() - for year in range(2017, datetime.datetime.now().year + 1): - month_avg_spd = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0} - month_distance = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0} - month_duration = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0} - for month in range(1, 13): - if month == 12: - max_day = (datetime.date(year + 1, 1, 1) - datetime.timedelta(days=1)).day - else: - max_day = (datetime.date(year, month + 1, 1) - datetime.timedelta(days=1)).day - for day in range(1, max_day + 1): - date_tracks = tracks.get(year, month, day) - for track in date_tracks: - month_distance[month] += (track.distance / 1000) # km - month_duration[month] += track.duration.total_seconds() / 3600 # h - for i in range(1, 13): - if month_duration[i] > 0: - month_avg_spd[i] = month_distance[i] / month_duration[i] - years_distance.append(month_distance.values()) - years_avg_spd.append(month_avg_spd.values()) - years.append(str(year)) - - out_folder = os.path.abspath(args.outfolder) - - dst_file_name = 'distance.png' - plot_bar_chart(years, MONTH_LABELS, years_distance, 'Distance', 'Month', 'km', os.path.join(out_folder, dst_file_name)) - - avg_file_name = 'avg_spd.png' - plot_bar_chart(years, MONTH_LABELS, years_avg_spd, 'Average Speed', 'Month', 'km/h', os.path.join(out_folder, avg_file_name)) - - # last n days - n = 14 - end_date = datetime.date.today() - start_date = end_date - datetime.timedelta(days=n) - dates = pd.date_range(start_date, end_date) - - date_distance = dict() - date_duration = dict() - date_avg_spd = dict() - - for date in dates: - date_str = "{0:04d}-{1:02d}-{2:02d}".format(date.year, date.month, date.day) - date_tracks = tracks.get(date.year, date.month, date.day) - for track in date_tracks: - try: - current_dist = date_distance[date_str] - current_duration = date_duration[date_str] - except KeyError: - current_dist = 0 - current_duration = 0 - current_dist += track.distance / 1000 - date_distance.update({date_str:current_dist}) - current_duration += track.duration.total_seconds() / 3600 - date_duration.update({date_str:current_duration}) - # check for empty dates - try: - current_dist = date_distance[date_str] - current_duration = date_duration[date_str] - except KeyError: - date_distance.update({date_str:0}) - date_duration.update({date_str:0}) - - date_duration = collections.OrderedDict(sorted(date_duration.items())) - - for key, value in date_duration.items(): - if value == 0: - date_avg_spd.update({key:0}) - else: - avg_spd = date_distance[key] / value - date_avg_spd.update({key:avg_spd}) - - date_avg_spd = collections.OrderedDict(sorted(date_avg_spd.items())) - date_distance = collections.OrderedDict(sorted(date_distance.items())) - - dst_n_file_name = "distance_last_{}_days.png".format(n) - plot_bar_chart(["Distance", "Average speed"], - date_distance.keys(), - [date_distance.values(), date_avg_spd.values()], - 'Last {} days'.format(n), - 'Date', - 'km, km/h', - os.path.join(out_folder, dst_n_file_name), - 90) - - html_file = os.path.join(out_folder, 'index.html') - with open(html_file, '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 years: - handle.write('\n'.format(year)) - handle.write('\n') - - handle.write('\n') - for i in range(len(years_distance)): - handle.write('\n'.format(round(sum(years_distance[i]), 1))) - handle.write('\n') - handle.write('
{}
{} km
\n') - - handle.write('

\n') - - handle.write('

\n') - handle.write('Distance\n'.format(dst_file_name)) - handle.write('Distance\n'.format(avg_file_name)) - handle.write('Distance\n'.format(dst_n_file_name)) - handle.write('

\n') - - handle.write('\n') - handle.write('
\n') - handle.write('\n') - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/gpx2html/__init__.py b/gpx2html/__init__.py new file mode 100755 index 0000000..a0524f2 --- /dev/null +++ b/gpx2html/__init__.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python + +import argparse +import datetime +import sys +import gpx_parser +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +import numpy +import os +import pandas as pd +import collections + +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) + + 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) + + +class Gpx2Html(object): + MONTH_LABELS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] + + def __init__(self, infolder, outfolder): + self.infolder = infolder + self.outfolder = outfolder + + + def process(self): + self.tracks = gpx_parser.Tracks(self.infolder) + years_distance = list() + years_avg_spd = list() + years = list() + for year in range(2017, datetime.datetime.now().year + 1): + month_avg_spd = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0} + month_distance = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0} + month_duration = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0} + for month in range(1, 13): + if month == 12: + max_day = (datetime.date(year + 1, 1, 1) - datetime.timedelta(days=1)).day + else: + max_day = (datetime.date(year, month + 1, 1) - datetime.timedelta(days=1)).day + for day in range(1, max_day + 1): + date_tracks = self.tracks.get(year, month, day) + for track in date_tracks: + month_distance[month] += (track.distance / 1000) # km + month_duration[month] += track.duration.total_seconds() / 3600 # h + for i in range(1, 13): + if month_duration[i] > 0: + month_avg_spd[i] = month_distance[i] / month_duration[i] + years_distance.append(month_distance.values()) + years_avg_spd.append(month_avg_spd.values()) + years.append(str(year)) + + out_folder = os.path.abspath(self.outfolder) + + dst_file_name = 'distance.png' + plot_bar_chart(years, self.MONTH_LABELS, years_distance, 'Distance', 'Month', 'km', os.path.join(out_folder, dst_file_name)) + + avg_file_name = 'avg_spd.png' + plot_bar_chart(years, self.MONTH_LABELS, years_avg_spd, 'Average Speed', 'Month', 'km/h', os.path.join(out_folder, avg_file_name)) + + # last n days + n = 14 + end_date = datetime.date.today() + start_date = end_date - datetime.timedelta(days=n) + dates = pd.date_range(start_date, end_date) + + date_distance = dict() + date_duration = dict() + date_avg_spd = dict() + + for date in dates: + date_str = "{0:04d}-{1:02d}-{2:02d}".format(date.year, date.month, date.day) + date_tracks = self.tracks.get(date.year, date.month, date.day) + for track in date_tracks: + try: + current_dist = date_distance[date_str] + current_duration = date_duration[date_str] + except KeyError: + current_dist = 0 + current_duration = 0 + current_dist += track.distance / 1000 + date_distance.update({date_str:current_dist}) + current_duration += track.duration.total_seconds() / 3600 + date_duration.update({date_str:current_duration}) + # check for empty dates + try: + current_dist = date_distance[date_str] + current_duration = date_duration[date_str] + except KeyError: + date_distance.update({date_str:0}) + date_duration.update({date_str:0}) + + date_duration = collections.OrderedDict(sorted(date_duration.items())) + + for key, value in date_duration.items(): + if value == 0: + date_avg_spd.update({key:0}) + else: + avg_spd = date_distance[key] / value + date_avg_spd.update({key:avg_spd}) + + date_avg_spd = collections.OrderedDict(sorted(date_avg_spd.items())) + date_distance = collections.OrderedDict(sorted(date_distance.items())) + + dst_n_file_name = "distance_last_{}_days.png".format(n) + plot_bar_chart(["Distance", "Average speed"], + date_distance.keys(), + [date_distance.values(), date_avg_spd.values()], + 'Last {} days'.format(n), + 'Date', + 'km, km/h', + os.path.join(out_folder, dst_n_file_name), + 90) + + html_file = os.path.join(out_folder, 'index.html') + with open(html_file, '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 years: + handle.write('\n'.format(year)) + handle.write('\n') + + handle.write('\n') + for i in range(len(years_distance)): + handle.write('\n'.format(round(sum(years_distance[i]), 1))) + handle.write('\n') + handle.write('
{}
{} km
\n') + + handle.write('

\n') + + handle.write('

\n') + handle.write('Distance\n'.format(dst_file_name)) + handle.write('Distance\n'.format(avg_file_name)) + handle.write('Distance\n'.format(dst_n_file_name)) + handle.write('

\n') + + handle.write('\n') + handle.write('
\n') + handle.write('\n') diff --git a/setup.py b/setup.py index ecd4b9d..fb543b7 100755 --- a/setup.py +++ b/setup.py @@ -1,12 +1,22 @@ #!/usr/bin/env python from distutils.core import setup +import sys +import os +import shutil +import stat -NAME = 'gpx_parser' -VERSION = '0.2.1' +NAME = 'bicycle-statistics' +VERSION = '0.1.0' AUTHOR = 'Thomas Klaehn' EMAIL = 'tkl@blackfinn.de' -PACKAGES = [NAME] -SCRIPTS = ['example-gpx-parser', 'gpx2html'] +PACKAGES = [NAME, 'gpx_parser', 'gpx2html'] +SCRIPTS = ['example-gpx-parser', 'bicycle-stat'] + +DAEMON_START_SCRIPT = os.path.join("/lib/systemd/system", "bicycle-stat.service") + +if sys.argv[1] == 'install': + shutil.copyfile("bicycle-stat.service", DAEMON_START_SCRIPT) + os.chmod(DAEMON_START_SCRIPT, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) setup(name=NAME, version=VERSION, author=AUTHOR, author_email=EMAIL, packages=PACKAGES, scripts=SCRIPTS)