bicycle-statistics: add accumulated distances view

This commit is contained in:
Thomas Klaehn 2019-06-16 08:34:37 +02:00
parent 06ecfdf0fe
commit 0a2bae9350
6 changed files with 304 additions and 131 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
build/
*.gpx *.gpx
.vscode/settings.json .vscode/settings.json
*.pyc *.pyc

View File

@ -14,7 +14,7 @@ LOG_FORMAT = "%(asctime)s %(levelname)s %(message)s"
UPDATE_INTERVAL = 60 UPDATE_INTERVAL = 60
logging.basicConfig(format=LOG_FORMAT, level=log_level, filename=LOG_FILE) 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') log = logging.getLogger('bicycle-statistics')
def parse_args(): def parse_args():

View File

@ -47,6 +47,46 @@ def plot_bar_chart(labels, ticklabels, values, title, xlabel, ylabel, filename,
plt.savefig(filename) plt.savefig(filename)
plt.close('all') 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): class Gpx2Html(object):
def __init__(self, infolder, outfolder, logger): def __init__(self, infolder, outfolder, logger):
self.logger = logger self.logger = logger
@ -67,9 +107,12 @@ class Gpx2Html(object):
self.logger.info("Begin update of png's/html...") self.logger.info("Begin update of png's/html...")
distances = list() distances = list()
avg_speeds = list() avg_speeds = list()
distances_dict = dict()
for year in self.tracks.years(): for year in self.tracks.years():
distances.append(self.tracks.distances(year)) distances.append(self.tracks.distances(year))
distances_dict[year] = self.tracks.distances(year)
avg_speeds.append(self.tracks.avg_speeds(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, plot_bar_chart(self.tracks.years(), MONTH_LABELS, distances,
'Distance', 'Month', 'km', 'Distance', 'Month', 'km',
@ -79,6 +122,26 @@ class Gpx2Html(object):
'Average Speed', 'Month', 'km/h', 'Average Speed', 'Month', 'km/h',
os.path.join(self.outfolder, 'avg_spd.png')) 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() end_date = datetime.datetime.today()
start_date = end_date - datetime.timedelta(days=14) start_date = end_date - datetime.timedelta(days=14)
last_n_tracks = self.tracks.tracks(start_date, end_date) last_n_tracks = self.tracks.tracks(start_date, end_date)
@ -182,6 +245,7 @@ class Gpx2Html(object):
handle.write('<p>\n') handle.write('<p>\n')
handle.write('<IMG SRC="{}" ALT="Distance">\n'.format('distance.png')) handle.write('<IMG SRC="{}" ALT="Distance">\n'.format('distance.png'))
handle.write('<IMG SRC="{}" ALT="Distance">\n'.format('acc_dist.png'))
handle.write('<IMG SRC="{}" ALT="Distance">\n'.format('avg_spd.png')) handle.write('<IMG SRC="{}" ALT="Distance">\n'.format('avg_spd.png'))
handle.write('<IMG SRC="{}" ALT="Distance">\n'.format('last_14_days.png')) handle.write('<IMG SRC="{}" ALT="Distance">\n'.format('last_14_days.png'))
handle.write('</p>\n') handle.write('</p>\n')

View File

@ -1,115 +1,255 @@
#!/usr/bin/env python
import argparse
import datetime import datetime
import glob import glob
import sys
import gpx_parser
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy
import os import os
import gpxpy
import gpxpy.gpx
from geopy import distance
from geopy import Point
import pandas as pd import pandas as pd
import collections
from gpx_parser import Tracks
class Segment(object): MONTH_LABELS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
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
class Tracks(object): def plot_bar_chart(labels, ticklabels, values, title, xlabel, ylabel, filename, xtick_rotation=0):
__distance = dict() fig = plt.figure()
__duration = dict() ax1 = fig.add_subplot(111)
__avg_speed = dict() ax1.grid(zorder=0)
__tracks = list() ax1.spines["top"].set_visible(False)
__files = list() 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.logger = logger
self.infolder = infolder
self.outfolder = os.path.abspath(outfolder)
def add(self, filename): if not os.path.exists(self.outfolder):
if filename not in self.__files: os.makedirs(self.outfolder)
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 trk_year not in self.__distance: self.tracks = Tracks(logger)
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.update()
self.__distance[trk_year][trk_month] += track.distance / 1000
if trk_year not in self.__duration: def update(self):
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} infiles = glob.glob(os.path.join(self.infolder, '*.gpx'))
self.__duration[trk_year][trk_month] += track.duration.total_seconds() for filename in infiles:
self.tracks.add(filename)
if trk_year not in self.__avg_speed: self.logger.info("Begin update of png's/html...")
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} distances = list()
self.__avg_speed[trk_year][trk_month] = self.__distance[trk_year][trk_month] / (self.__duration[trk_year][trk_month] / 3600) avg_speeds = list()
self.logger.info("Adding done.") 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): plot_bar_chart(self.tracks.years(), MONTH_LABELS, distances,
ret = None 'Distance', 'Month', 'km',
try: os.path.join(self.outfolder, 'distance.png'))
ret = sorted(self.__distance.keys())
except Exception:
pass
return ret
def distances(self, year): plot_bar_chart(self.tracks.years(), MONTH_LABELS, avg_speeds,
ret = 0 'Average Speed', 'Month', 'km/h',
try: os.path.join(self.outfolder, 'avg_spd.png'))
ret = self.__distance[year].values()
except Exception:
pass
return ret
def avg_speeds(self, year): # Accumulated distance:
ret = None accumulated_distances = dict()
try: for year in distances_dict.keys():
ret = self.__avg_speed[year].values() accumulated_distance = list()
except Exception: accumulated_distance.append(0)
pass for i in range(0, len(distances_dict[year])):
return ret accumulated_distance.append(accumulated_distance[i] + distances_dict[year][i])
accumulated_distances[year] = accumulated_distance
def tracks(self, start_date, end_date): current_year = datetime.datetime.today().year
tracks = list() 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()) dates = pd.date_range(start_date.date(), end_date.date())
for track in self.__tracks: for date in dates:
if track.start_time.date() in dates: for track in last_n_tracks:
tracks.append(track) if date.date() == track.start_time.date():
return tracks 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('<!DOCTYPE html>\n')
handle.write('<html>\n')
handle.write('<head>\n')
handle.write('<style>\n')
handle.write('table {\n')
handle.write(' border-collapse: separate;\n')
handle.write(' border-spacing: 20px 0;\n')
handle.write('}\n')
handle.write('th {\n')
handle.write(' text-align: left;\n')
handle.write('}\n')
handle.write('</style>\n')
handle.write('<title> Bicycle </title>\n')
handle.write('</head>\n')
handle.write('<body>\n')
handle.write('<center>\n')
handle.write('<h1> Bicycle </h1>\n')
handle.write('<p>\n')
handle.write('<table>\n')
handle.write('<tr>\n')
for year in self.tracks.years():
handle.write('<th>{}</th>\n'.format(year))
handle.write('</tr>\n')
handle.write('<tr>\n')
for year in self.tracks.years():
handle.write('<td>{} km</td>\n'.format(round(sum(self.tracks.distances(year)), 1)))
handle.write('</tr>\n')
handle.write('</table>\n')
handle.write('</p>\n')
handle.write('<p>\n')
handle.write('<IMG SRC="{}" ALT="Distance">\n'.format('distance.png'))
handle.write('<IMG SRC="{}" ALT="Distance">\n'.format('acc_dist.png'))
handle.write('<IMG SRC="{}" ALT="Distance">\n'.format('avg_spd.png'))
handle.write('<IMG SRC="{}" ALT="Distance">\n'.format('last_14_days.png'))
handle.write('</p>\n')
handle.write('</body>\n')
handle.write('<center>\n')
handle.write('</html>\n')

View File

@ -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()

View File

@ -10,7 +10,7 @@ NAME = 'bicycle-statistics'
VERSION = '0.2.0' VERSION = '0.2.0'
AUTHOR = 'Thomas Klaehn' AUTHOR = 'Thomas Klaehn'
EMAIL = 'tkl@blackfinn.de' EMAIL = 'tkl@blackfinn.de'
PACKAGES = ['bicycle_statistics', 'gpx_parser', 'gpx2html', 'input_observer'] PACKAGES = ['bicycle_statistics', 'gpx_parser', 'gpx2html']
SCRIPTS = ['example-gpx-parser', 'bicycle-stat'] SCRIPTS = ['example-gpx-parser', 'bicycle-stat']
DAEMON_START_SCRIPT = os.path.join("/lib/systemd/system", "bicycle-stat.service") DAEMON_START_SCRIPT = os.path.join("/lib/systemd/system", "bicycle-stat.service")