Compare commits
1 Commits
7d7ca2b48f
...
0a2bae9350
Author | SHA1 | Date | |
---|---|---|---|
|
0a2bae9350 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
build/
|
||||
*.gpx
|
||||
.vscode/settings.json
|
||||
*.pyc
|
||||
|
@ -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():
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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()
|
2
setup.py
2
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")
|
||||
|
Loading…
Reference in New Issue
Block a user