├── .gitignore ├── LICENSE ├── README.md ├── gpx └── empty ├── requirements.txt ├── screenshot.png └── strava_local_heatmap_browser.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Remi Salmon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # strava_local_heatmap_browser.py 2 | Python script to reproduce the Strava Global Heatmap ([www.strava.com/heatmap](https://www.strava.com/heatmap)) with local GPX files 3 | 4 | ![screenshot.png](screenshot.png) 5 | 6 | ## Features 7 | 8 | * Minimal Python dependencies 9 | * Fast (3x faster than `gpxpy.parse()`) 10 | 11 | ## Usage 12 | 13 | * Download your GPX files from Strava to the `gpx` folder 14 | (see https://support.strava.com/hc/en-us/articles/216918437-Exporting-your-Data-and-Bulk-Export) 15 | * Run `python3 strava_local_heatmap_browser.py` 16 | 17 | ### Command-line options 18 | ``` 19 | usage: strava_local_heatmap_browser.py [-h] [--gpx-dir DIR] [--gpx-filter FILTER] [--skip-ratio N] [--light-map] 20 | [--output FILE] [--radius RADIUS] [--blur BLUR] [--min-opacity OPACITY] 21 | [--max-val VAL] [--quiet] 22 | 23 | optional arguments: 24 | -h, --help show this help message and exit 25 | --gpx-dir DIR directory containing the GPX files (default: gpx) 26 | --gpx-filters FILTERS glob filter(s) for the GPX files (default: *.gpx) 27 | --skip-ratio N read every other N point of each GPX file (default: 1) 28 | --light-map use light map background 29 | --output FILE output html file (default: strava_local_heatmap.html) 30 | --radius RADIUS radius of trackpoints in pixels (default: 2) 31 | --blur BLUR amount of blur in pixels (default: 2) 32 | --min-opacity OPACITY 33 | minimum opacity value (default: 0.3) 34 | --max-val VAL maximum point intensity (default: 1.0) 35 | --quiet quiet output 36 | ``` 37 | 38 | ## Python dependencies 39 | * [folium](https://github.com/python-visualization/folium) 40 | 41 | ## See also 42 | 43 | https://github.com/arichnad/heatmap-geojson to produce a heatmap in GeoJSON format from GPX files 44 | -------------------------------------------------------------------------------- /gpx/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remisalmon/Strava-local-heatmap-browser/0e390e0e508c821a739d84b7d3f238529dd948df/gpx/empty -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | folium==0.11.0 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remisalmon/Strava-local-heatmap-browser/0e390e0e508c821a739d84b7d3f238529dd948df/screenshot.png -------------------------------------------------------------------------------- /strava_local_heatmap_browser.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Remi Salmon 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | # imports 22 | import re 23 | import glob 24 | import argparse 25 | import webbrowser 26 | 27 | from folium import Map 28 | from folium.plugins import HeatMap 29 | 30 | # constants 31 | HEATMAP_MAXZOOM = 16 32 | 33 | HEATMAP_GRAD = {'dark':{0.0: '#000004', 34 | 0.1: '#160b39', 35 | 0.2: '#420a68', 36 | 0.3: '#6a176e', 37 | 0.4: '#932667', 38 | 0.5: '#bc3754', 39 | 0.6: '#dd513a', 40 | 0.7: '#f37819', 41 | 0.8: '#fca50a', 42 | 0.9: '#f6d746', 43 | 1.0: '#fcffa4'}, 44 | 'light':{0.0: '#3b4cc0', 45 | 0.1: '#5977e3', 46 | 0.2: '#7b9ff9', 47 | 0.3: '#9ebeff', 48 | 0.4: '#c0d4f5', 49 | 0.5: '#dddcdc', 50 | 0.6: '#f2cbb7', 51 | 0.7: '#f7ac8e', 52 | 0.8: '#ee8468', 53 | 0.9: '#d65244', 54 | 1.0: '#b40426'}} 55 | 56 | def get_gpx_files(args): 57 | gpx_filters = args.gpx_filters if args.gpx_filters else ['*.gpx'] 58 | gpx_files = [] 59 | 60 | for filter in gpx_filters: 61 | gpx_files += glob.glob('{}/{}'.format(args.gpx_dir, filter)) 62 | 63 | if not gpx_files: 64 | exit('ERROR No GPX files found') 65 | 66 | return gpx_files 67 | 68 | # functions 69 | def main(args): 70 | if not args.output[-5:] == '.html': 71 | exit('ERROR Output file must be .html') 72 | 73 | heatmap_data = [] 74 | 75 | for gpx_file in get_gpx_files(args): 76 | if not args.quiet: 77 | print('Reading {}'.format(gpx_file)) 78 | 79 | with open(gpx_file, 'r') as file: 80 | for line in file: 81 | if ' 1: 90 | heatmap_data = heatmap_data[::args.skip_ratio] 91 | 92 | print('Reduced to {} trackpoints'.format(len(heatmap_data))) 93 | 94 | fmap = Map(tiles = 'CartoDB positron' if args.light_map else 'CartoDB dark_matter', 95 | prefer_canvas = True, 96 | max_zoom = HEATMAP_MAXZOOM) 97 | 98 | HeatMap(heatmap_data, 99 | radius = args.radius, 100 | blur = args.blur, 101 | gradient = HEATMAP_GRAD['light' if args.light_map else 'dark'], 102 | min_opacity = args.min_opacity, 103 | max_val = args.max_val).add_to(fmap) 104 | 105 | fmap.fit_bounds(fmap.get_bounds()) 106 | 107 | fmap.save(args.output) 108 | 109 | if not args.quiet: 110 | print('Saved {}'.format(args.output)) 111 | 112 | webbrowser.open(args.output, new = 2, autoraise = True) 113 | 114 | if __name__ == '__main__': 115 | parser = argparse.ArgumentParser(description = 'Generate a local heatmap from Strava GPX files', epilog = 'Report issues to github.com/remisalmon/strava-local-heatmap-browser') 116 | 117 | parser.add_argument('--gpx-dir', metavar = 'DIR', default = 'gpx', help = 'directory containing the GPX files (default: gpx)') 118 | parser.add_argument('--gpx-filters', metavar = 'FILTERS', action = 'append', help = 'glob filter(s) for the GPX files (default: *.gpx)') 119 | parser.add_argument('--skip-ratio', metavar = 'N', type = int, default = 1, help = 'read every other N point of each GPX file (default: 1)') 120 | parser.add_argument('--light-map', action='store_true', help = 'use light map background') 121 | parser.add_argument('--output', metavar = 'FILE', default = 'strava_local_heatmap.html', help = 'output html file (default: strava_local_heatmap.html)') 122 | parser.add_argument('--radius', type = int, default = 2, help = 'radius of trackpoints in pixels (default: 2)') 123 | parser.add_argument('--blur', type = int, default = 2, help = 'amount of blur in pixels (default: 2)') 124 | parser.add_argument('--min-opacity', metavar = 'OPACITY', type = float, default = 0.3, help = 'minimum opacity value (default: 0.3)') 125 | parser.add_argument('--max-val', metavar = 'VAL', type = float, default = 1.0, help = 'maximum point intensity (default: 1.0)') 126 | parser.add_argument('--quiet', default = False, action = 'store_true', help = 'quiet output') 127 | 128 | args = parser.parse_args() 129 | 130 | main(args) 131 | --------------------------------------------------------------------------------