Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/runalyze' into runalyze-globalHe…
Browse files Browse the repository at this point in the history
…atmap
  • Loading branch information
laufhannes committed Jan 20, 2017
2 parents 687ea5b + 6c41ad9 commit 301b151
Show file tree
Hide file tree
Showing 21 changed files with 188 additions and 102 deletions.
3 changes: 1 addition & 2 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2016 Florian Pigorsch
Copyright (c) 2016-2017 Florian Pigorsch & Contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand All @@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[![Scrutinizer Score](https://scrutinizer-ci.com/g/flopp/GpxTrackPoster/badges/quality-score.png)](https://scrutinizer-ci.com/g/flopp/GpxTrackPoster/)
[![Scrutinizer Build](https://scrutinizer-ci.com/g/flopp/GpxTrackPoster/badges/build.png)](https://scrutinizer-ci.com/g/flopp/GpxTrackPoster/)
![License MIT](https://img.shields.io/badge/license-MIT-lightgrey.svg?style=flat)

# GpxTrackPoster
Create a visually appealing poster from your GPX tracks - heavily inspired by https://www.madewithsisu.com/

Expand All @@ -24,7 +28,7 @@ optional arguments:
--title TITLE Title to display (default: "My Tracks").
--athlete NAME Athlete name to display (default: "John Doe").
--special FILE Mark track file from the GPX directory as special; use multiple times to mark multiple tracks.
--type TYPE Type of poster to create (default: "grid", available: "calendar", "grid", "heatmap").
--type TYPE Type of poster to create (default: "grid", available: "calendar", "circular", "grid", "heatmap").
--stat-label LABEL Label for number of activities (default: "Runs").
--stat-num NUMBER Number of activities (default: automatically calculated).
--stat-total KM Total distance (default: automatically calculated).
Expand Down Expand Up @@ -65,6 +69,12 @@ The *Calendar Poster* draws one square for each day, each row of squares corresp
![Example Grid Poster](https://github.com/flopp/GpxTrackPoster/blob/master/examples/example_calendar.png)
[svg](https://github.com/flopp/GpxTrackPoster/blob/master/examples/example_calendar.svg)

### Circular Poster (`--type circular`)
The *Circular Poster* the year in a circle; each day corresponds to a circle segment. The length of each segment corresponds to the total track distance of that day.

![Example Circular Poster](https://github.com/flopp/GpxTrackPoster/blob/master/examples/example_circular.png)
[svg](https://github.com/flopp/GpxTrackPoster/blob/master/examples/example_circular.svg)

### Heatmap Poster (`--type heatmap`)
The *Heatmap Poster* displays all tracks within one "map". The more often a location has been "visited" on a track, the more colorful the corresponding location is on the map. *Special tracks* are drawn with the *special color*.

Expand All @@ -80,6 +90,10 @@ The *Heatmap Poster* displays all tracks within one "map". The more often a loca
6. Run `./create_poster.py` (see above)
7. Deactive virtualenv: `deactivate`

## Contributing
If you have found a bug or have a feature request, please create a new issue. I'm always happy improve the implementation!

Or even better: clone the repo, fix the bug/implement the feature yourself, and file a pull request. Contributions are always welcome!

## License
[MIT](https://github.com/flopp/GpxTrackPoster/blob/master/LICENSE) © 2016 florian Pigorsch
9 changes: 8 additions & 1 deletion create_poster.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
#!/usr/bin/env python

# Copyright 2016-2017 Florian Pigorsch & Contributors. All rights reserved.
#
# Use of this source code is governed by a MIT-style
# license that can be found in the LICENSE file.

import argparse
import datetime
import appdirs
Expand All @@ -8,6 +13,7 @@
from src import poster
from src import grid_drawer
from src import calendar_drawer
from src import circular_drawer
from src import heatmap_drawer


Expand All @@ -18,7 +24,8 @@
def main():
generators = {"grid": grid_drawer.TracksDrawer(),
"calendar": calendar_drawer.TracksDrawer(),
"heatmap": heatmap_drawer.TracksDrawer()}
"heatmap": heatmap_drawer.TracksDrawer(),
"circular": circular_drawer.TracksDrawer()}

args_parser = argparse.ArgumentParser()
args_parser.add_argument('--gpx-dir', dest='gpx_dir', metavar='DIR', type=str, default='.',
Expand Down
Binary file modified examples/example_calendar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion examples/example_calendar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/example_circular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions examples/example_circular.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/example_grid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion examples/example_grid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/example_heatmap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion examples/example_heatmap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions examples/examples.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash

for TYPE in grid calendar circular heatmap ; do
../create_poster.py --gpx-dir ../2016 --year 2016 \
--athlete "Florian Pigorsch" --title "My Runs 2016" \
--type $TYPE --output example_$TYPE.svg \
--special 20161231-123107-Run.gpx \
--special 20160916-171532-Run.gpx \
--special 20160911-093006-Run.gpx \
--special 20160710-075921-Run.gpx \
--special 20160508-080955-Run.gpx \
--special 20160403-091527-Run.gpx \
--special 20160313-130016-Run.gpx \
--special 20160117-101524-Run.gpx

# use headless inkscape to produce a png
inkscape --without-gui --export-width 500 \
--file example_$TYPE.svg --export-png example_$TYPE.png
done
7 changes: 6 additions & 1 deletion src/calendar_drawer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Copyright 2016-2017 Florian Pigorsch & Contributors. All rights reserved.
#
# Use of this source code is governed by a MIT-style
# license that can be found in the LICENSE file.

import calendar
import datetime

Expand All @@ -12,7 +17,7 @@ def draw(self, poster, d, w, h, offset_x, offset_y):
count_x = 31
for month in range(1, 13):
date = datetime.date(self.poster.year, month, 1)
(first_day, last_day) = calendar.monthrange(self.poster.year, month)
(_, last_day) = calendar.monthrange(self.poster.year, month)
count_x = max(count_x, date.weekday() + last_day)

size = min(w / count_x, h / 36)
Expand Down
80 changes: 80 additions & 0 deletions src/circular_drawer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Copyright 2016-2017 Florian Pigorsch & Contributors. All rights reserved.
#
# Use of this source code is governed by a MIT-style
# license that can be found in the LICENSE file.

import calendar
import datetime
import math
import svgwrite


class TracksDrawer:
def __init__(self):
self.poster = None

def draw(self, poster, d, w, h, offset_x, offset_y):
self.poster = poster
outer_radius = 0.5 * min(w, h) - 6
inner_radius = 0.25 * outer_radius
c_x = offset_x + 0.5 * w
c_y = offset_y + 0.5 * h
df = 360.0 / (366 if calendar.isleap(self.poster.year) else 365)

tracks_by_date = {}
for track in self.poster.tracks:
text_date = track.start_time.strftime("%Y-%m-%d")
if text_date in tracks_by_date:
tracks_by_date[text_date].append(track)
else:
tracks_by_date[text_date] = [track]
max_length = 0
for date, tracks in tracks_by_date.items():
length = sum([t.length for t in tracks])
if length > max_length:
max_length = length
if max_length == 0:
return

day = 0
date = datetime.date(self.poster.year, 1, 1)
while date.year == self.poster.year:
text_date = date.strftime("%Y-%m-%d")
a1 = math.radians(day * df)
a2 = math.radians((day + 1) * df)
if date.day == 1:
(_, last_day) = calendar.monthrange(date.year, date.month)
a3 = math.radians((day + last_day - 1) * df)
r1 = outer_radius + 1
r2 = outer_radius + 6
r3 = outer_radius + 2
d.add(d.line(
start=(c_x + r1 * math.sin(a1), c_y - r1 * math.cos(a1)),
end=(c_x + r2 * math.sin(a1), c_y - r2 * math.cos(a1)),
stroke=self.poster.colors['text'],
stroke_width=0.3))
path = d.path(d=('M', c_x + r3 * math.sin(a1), c_y - r3 * math.cos(a1)), fill='none', stroke='none')
path.push('a{},{} 0 0,1 {},{}'.format(
r3, r3,
r3 * (math.sin(a3) - math.sin(a1)),
r3 * (math.cos(a1) - math.cos(a3))))
d.add(path)
tpath = svgwrite.text.TextPath(path, date.strftime("%B"), startOffset=(0.5 * r3 * (a3 - a1)))
text = d.text("", fill=self.poster.colors['text'], text_anchor="middle", style="font-size:4px; font-family:Arial")
text.add(tpath)
d.add(text)
if text_date in tracks_by_date:
tracks = tracks_by_date[text_date]
special = [t for t in tracks if t.special]
length = sum([t.length for t in tracks])
color = self.poster.colors['special'] if special else self.poster.colors['track']
r1 = inner_radius
r2 = inner_radius + (outer_radius-inner_radius) * length / max_length
path = d.path(d=('M', c_x + r1 * math.sin(a1), c_y - r1 * math.cos(a1)), fill=color, stroke='none')
path.push('l', (r2 - r1) * math.sin(a1), (r1 - r2) * math.cos(a1))
path.push('a{},{} 0 0,0 {},{}'.format(r2, r2, r2 * (math.sin(a2) - math.sin(a1)), r2 * (math.cos(a1) - math.cos(a2))))
path.push('l', (r1 - r2) * math.sin(a2), (r2 - r1) * math.cos(a2))
d.add(path)

day += 1
date += datetime.timedelta(1)
5 changes: 5 additions & 0 deletions src/grid_drawer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Copyright 2016-2017 Florian Pigorsch & Contributors. All rights reserved.
#
# Use of this source code is governed by a MIT-style
# license that can be found in the LICENSE file.

from . import utils


Expand Down
72 changes: 0 additions & 72 deletions src/grid_poster.py

This file was deleted.

5 changes: 5 additions & 0 deletions src/heatmap_drawer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Copyright 2016-2017 Florian Pigorsch & Contributors. All rights reserved.
#
# Use of this source code is governed by a MIT-style
# license that can be found in the LICENSE file.

from . import utils


Expand Down
5 changes: 5 additions & 0 deletions src/poster.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Copyright 2016-2017 Florian Pigorsch & Contributors. All rights reserved.
#
# Use of this source code is governed by a MIT-style
# license that can be found in the LICENSE file.

import svgwrite


Expand Down
6 changes: 5 additions & 1 deletion src/track.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Copyright 2016-2017 Florian Pigorsch & Contributors. All rights reserved.
#
# Use of this source code is governed by a MIT-style
# license that can be found in the LICENSE file.

import datetime
import gpxpy
import json
Expand Down Expand Up @@ -65,4 +70,3 @@ def store_cache(self, cache_file_name):
"length": self.length,
"segments": lines_data},
json_file)

50 changes: 29 additions & 21 deletions src/track_loader.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Copyright 2016-2017 Florian Pigorsch & Contributors. All rights reserved.
#
# Use of this source code is governed by a MIT-style
# license that can be found in the LICENSE file.

import hashlib
import os
Expand Down Expand Up @@ -69,12 +73,21 @@ def load_tracks(self, base_dir, load_json = False):
# store non-cached tracks in cache
if loaded_tracks and self.cache_dir:
print("Storing {} track(s) in cache...".format(len(loaded_tracks)))
for (file_name, track) in loaded_tracks.items():
for (file_name, t) in loaded_tracks.items():
checksum = hashlib.sha256(open(file_name, 'rb').read()).hexdigest()
cache_file = os.path.join(self.cache_dir, checksum + ".json")
track.store_cache(cache_file)
t.store_cache(cache_file)
tracks.extend(loaded_tracks.values())

tracks = self.__filter_tracks(tracks)

# merge tracks that took place within one hour
#tracks = self.__merge_tracks(tracks)

# filter out tracks with length < min_length
return [t for t in tracks if t.length >= self.min_length]

def __filter_tracks(self, tracks):
filtered_tracks = []
for t in tracks:
file_name = t.file_names[0]
Expand All @@ -87,28 +100,25 @@ def load_tracks(self, base_dir, load_json = False):
else:
t.special = (file_name in self.special_file_names)
filtered_tracks.append(t)
return filtered_tracks

# sort tracks by start time
sorted_tracks = sorted(filtered_tracks, key=lambda t: t.start_time)

# merge tracks that took place within one hour
def __merge_tracks(self, tracks):
print("Merging tracks...")
tracks = sorted(tracks, key=lambda t: t.start_time)
merged_tracks = []
last_end_time = None
for t in sorted_tracks:
merged_tracks.append(t)
#if last_end_time is None:
# merged_tracks.append(t)
#else:
# dt = (t.start_time - last_end_time).total_seconds()
# if 0 < dt < 3600:
# merged_tracks[-1].append(t)
# else:
# merged_tracks.append(t)
for t in tracks:
if last_end_time is None:
merged_tracks.append(t)
else:
dt = (t.start_time - last_end_time).total_seconds()
if 0 < dt < 3600:
merged_tracks[-1].append(t)
else:
merged_tracks.append(t)
last_end_time = t.end_time
print("Merged {} track(s)".format(len(sorted_tracks) - len(merged_tracks)))
# filter out tracks with length < min_length
return [t for t in merged_tracks if t.length >= self.min_length]
print("Merged {} track(s)".format(len(tracks) - len(merged_tracks)))
return merged_tracks

@staticmethod
def __load_tracks(file_names):
Expand Down Expand Up @@ -161,5 +171,3 @@ def __list_json_files(base_dir):
path_name = os.path.join(base_dir, name)
if name.endswith(".json") and os.path.isfile(path_name):
yield path_name


Loading

0 comments on commit 301b151

Please sign in to comment.