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
.vscode/settings.json
*.pyc

View File

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

View File

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

View File

@ -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('<!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'
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")