From 0a2bae9350a0031a33d705cdcee318b39f937c23 Mon Sep 17 00:00:00 2001
From: Thomas Klaehn
Date: Sun, 16 Jun 2019 08:34:37 +0200
Subject: [PATCH] bicycle-statistics: add accumulated distances view
---
.gitignore | 1 +
bicycle_statistics/__main__.py | 2 +-
gpx2html/__init__.py | 64 +++++++
gpx_parser/__init__.py | 334 +++++++++++++++++++++++----------
input_observer/__init__.py | 32 ----
setup.py | 2 +-
6 files changed, 304 insertions(+), 131 deletions(-)
delete mode 100644 input_observer/__init__.py
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('\n'.format('distance.png'))
+ handle.write('\n'.format('acc_dist.png'))
handle.write('\n'.format('avg_spd.png'))
handle.write('\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('{} km | \n'.format(round(sum(self.tracks.distances(year)), 1)))
+ handle.write('
\n')
+ handle.write('
\n')
+
+ handle.write('
\n')
+
+ handle.write('\n')
+ handle.write('\n'.format('distance.png'))
+ handle.write('\n'.format('acc_dist.png'))
+ handle.write('\n'.format('avg_spd.png'))
+ handle.write('\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")