├── .github
└── workflows
│ └── python-publish.yml
├── .gitignore
├── .readthedocs.yaml
├── COPYING
├── COPYING.LESSER
├── LICENSE
├── MANIFEST.in
├── README.md
├── docs
├── Makefile
├── make.bat
└── source
│ ├── _static
│ ├── example1.gpx
│ ├── example2.gpx
│ ├── example3.gpx
│ ├── example4.gpx
│ ├── image1.jpg
│ ├── image2.jpg
│ ├── image3.jpg
│ ├── map_001.html
│ └── map_002.html
│ ├── conf.py
│ ├── extensions
│ └── thumbnail_updater.py
│ ├── gallery
│ ├── maps
│ │ ├── README.txt
│ │ ├── example1.gpx
│ │ ├── example2.gpx
│ │ ├── example3.gpx
│ │ ├── example4.gpx
│ │ ├── plot_000_segment.py
│ │ ├── plot_001_heart_rate.py
│ │ ├── plot_002_annotate.py
│ │ ├── plot_003_velocity.py
│ │ ├── plot_004_zones.py
│ │ ├── plot_005_images.py
│ │ ├── plot_006_chart1.py
│ │ ├── plot_006_chart2.py
│ │ ├── plot_007_custom_tiles.py
│ │ ├── plot_008_overlay.py
│ │ ├── plot_009_adding_markers.py
│ │ ├── plot_010_heat_map.py
│ │ └── plot_011_custom_color_map.py
│ └── plots
│ │ ├── README.txt
│ │ ├── example1.gpx
│ │ ├── example3.gpx
│ │ ├── example4.gpx
│ │ ├── plot_001_simple1.py
│ │ ├── plot_001_simple2.py
│ │ ├── plot_002_elevation.py
│ │ ├── plot_003_elevation.py
│ │ ├── plot_004_velocity.py
│ │ ├── plot_005_velocity.py
│ │ ├── plot_006_heart_rate.py
│ │ └── plot_006_heart_rate_bar.py
│ ├── gallery_thumbs
│ ├── make_snap.py
│ ├── sphx_glr_plot_000_segment_thumb.png
│ ├── sphx_glr_plot_001_heart_rate_thumb.png
│ ├── sphx_glr_plot_002_annotate_thumb.png
│ ├── sphx_glr_plot_003_velocity_thumb.png
│ ├── sphx_glr_plot_004_zones_thumb.png
│ ├── sphx_glr_plot_005_images_thumb.png
│ ├── sphx_glr_plot_006_chart1_thumb.png
│ ├── sphx_glr_plot_006_chart2_thumb.png
│ ├── sphx_glr_plot_007_custom_tiles_thumb.png
│ ├── sphx_glr_plot_008_overlay_thumb.png
│ ├── sphx_glr_plot_009_adding_markers_thumb.png
│ ├── sphx_glr_plot_010_heat_map_thumb.png
│ └── sphx_glr_plot_011_custom_color_map_thumb.png
│ ├── index.rst
│ └── source
│ ├── gpxplotter.common.rst
│ ├── gpxplotter.folium_map.rst
│ ├── gpxplotter.gpxread.rst
│ ├── gpxplotter.mplplotting.rst
│ ├── gpxplotter.rst
│ └── gpxplotter.version.rst
├── examples
├── html
│ └── map001.html
├── images
│ ├── map001.png
│ └── plot1.png
└── jupyter
│ ├── example1.gpx
│ ├── example3.gpx
│ ├── examples.ipynb
│ ├── examples_chart.ipynb
│ ├── examples_image.ipynb
│ ├── image1.jpg
│ ├── image2.jpg
│ └── image3.jpg
├── gpxplotter
├── __init__.py
├── common.py
├── folium_map.py
├── gpxread.py
├── mplplotting.py
└── version.py
├── make_package.sh
├── pypireadme.md
├── requirements.txt
├── setup.py
├── setup_version.py
└── version.json
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will upload a Python Package using Twine when a release is created
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Upload Python Package
10 |
11 | on:
12 | release:
13 | types: [published]
14 |
15 | permissions:
16 | contents: read
17 |
18 | jobs:
19 | deploy:
20 |
21 | runs-on: ubuntu-latest
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 | - name: Set up Python
26 | uses: actions/setup-python@v3
27 | with:
28 | python-version: '3.x'
29 | - name: Install dependencies
30 | run: |
31 | python -m pip install --upgrade pip
32 | pip install build
33 | - name: Build package
34 | run: python -m build
35 | - name: Publish package
36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
37 | with:
38 | user: __token__
39 | password: ${{ secrets.PYPI_API_TOKEN }}
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 | docs/source/auto_examples/
74 | docs/build
75 |
76 | # PyBuilder
77 | .pybuilder/
78 | target/
79 |
80 | # Jupyter Notebook
81 | .ipynb_checkpoints
82 |
83 | # IPython
84 | profile_default/
85 | ipython_config.py
86 |
87 | # pyenv
88 | # For a library or package, you might want to ignore these files since the code is
89 | # intended to run in multiple environments; otherwise, check them in:
90 | # .python-version
91 |
92 | # pipenv
93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
96 | # install all needed dependencies.
97 | #Pipfile.lock
98 |
99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
100 | __pypackages__/
101 |
102 | # Celery stuff
103 | celerybeat-schedule
104 | celerybeat.pid
105 |
106 | # SageMath parsed files
107 | *.sage.py
108 |
109 | # Environments
110 | .env
111 | .venv
112 | env/
113 | venv/
114 | ENV/
115 | env.bak/
116 | venv.bak/
117 |
118 | # Spyder project settings
119 | .spyderproject
120 | .spyproject
121 |
122 | # Rope project settings
123 | .ropeproject
124 |
125 | # mkdocs documentation
126 | /site
127 |
128 | # mypy
129 | .mypy_cache/
130 | .dmypy.json
131 | dmypy.json
132 |
133 | # Pyre type checker
134 | .pyre/
135 |
136 | # pytype static type analyzer
137 | .pytype/
138 |
139 | # Cython debug symbols
140 | cython_debug/
141 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # Read the Docs configuration file for Sphinx projects
2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3 |
4 | # Required
5 | version: 2
6 |
7 | # Set the OS, Python version and other tools you might need
8 | build:
9 | os: ubuntu-22.04
10 | tools:
11 | python: "3.12"
12 | # You can also specify other tool versions:
13 | # nodejs: "20"
14 | # rust: "1.70"
15 | # golang: "1.20"
16 |
17 | # Build documentation in the "docs/" directory with Sphinx
18 | sphinx:
19 | configuration: docs/source/conf.py
20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
21 | # builder: "dirhtml"
22 | # Fail on all warnings to avoid broken references
23 | # fail_on_warning: true
24 |
25 | # Optionally build your docs in additional formats such as PDF and ePub
26 | # formats:
27 | # - pdf
28 | # - epub
29 |
30 | # Optional but recommended, declare the Python requirements required
31 | # to build your documentation
32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
33 | python:
34 | install:
35 | - requirements: requirements.txt
36 | - method: pip
37 | path: .
38 |
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 2.1, February 1999
3 |
4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc.
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | [This is the first released version of the Lesser GPL. It also counts
10 | as the successor of the GNU Library Public License, version 2, hence
11 | the version number 2.1.]
12 |
13 | Preamble
14 |
15 | The licenses for most software are designed to take away your
16 | freedom to share and change it. By contrast, the GNU General Public
17 | Licenses are intended to guarantee your freedom to share and change
18 | free software--to make sure the software is free for all its users.
19 |
20 | This license, the Lesser General Public License, applies to some
21 | specially designated software packages--typically libraries--of the
22 | Free Software Foundation and other authors who decide to use it. You
23 | can use it too, but we suggest you first think carefully about whether
24 | this license or the ordinary General Public License is the better
25 | strategy to use in any particular case, based on the explanations below.
26 |
27 | When we speak of free software, we are referring to freedom of use,
28 | not price. Our General Public Licenses are designed to make sure that
29 | you have the freedom to distribute copies of free software (and charge
30 | for this service if you wish); that you receive source code or can get
31 | it if you want it; that you can change the software and use pieces of
32 | it in new free programs; and that you are informed that you can do
33 | these things.
34 |
35 | To protect your rights, we need to make restrictions that forbid
36 | distributors to deny you these rights or to ask you to surrender these
37 | rights. These restrictions translate to certain responsibilities for
38 | you if you distribute copies of the library or if you modify it.
39 |
40 | For example, if you distribute copies of the library, whether gratis
41 | or for a fee, you must give the recipients all the rights that we gave
42 | you. You must make sure that they, too, receive or can get the source
43 | code. If you link other code with the library, you must provide
44 | complete object files to the recipients, so that they can relink them
45 | with the library after making changes to the library and recompiling
46 | it. And you must show them these terms so they know their rights.
47 |
48 | We protect your rights with a two-step method: (1) we copyright the
49 | library, and (2) we offer you this license, which gives you legal
50 | permission to copy, distribute and/or modify the library.
51 |
52 | To protect each distributor, we want to make it very clear that
53 | there is no warranty for the free library. Also, if the library is
54 | modified by someone else and passed on, the recipients should know
55 | that what they have is not the original version, so that the original
56 | author's reputation will not be affected by problems that might be
57 | introduced by others.
58 |
59 | Finally, software patents pose a constant threat to the existence of
60 | any free program. We wish to make sure that a company cannot
61 | effectively restrict the users of a free program by obtaining a
62 | restrictive license from a patent holder. Therefore, we insist that
63 | any patent license obtained for a version of the library must be
64 | consistent with the full freedom of use specified in this license.
65 |
66 | Most GNU software, including some libraries, is covered by the
67 | ordinary GNU General Public License. This license, the GNU Lesser
68 | General Public License, applies to certain designated libraries, and
69 | is quite different from the ordinary General Public License. We use
70 | this license for certain libraries in order to permit linking those
71 | libraries into non-free programs.
72 |
73 | When a program is linked with a library, whether statically or using
74 | a shared library, the combination of the two is legally speaking a
75 | combined work, a derivative of the original library. The ordinary
76 | General Public License therefore permits such linking only if the
77 | entire combination fits its criteria of freedom. The Lesser General
78 | Public License permits more lax criteria for linking other code with
79 | the library.
80 |
81 | We call this license the "Lesser" General Public License because it
82 | does Less to protect the user's freedom than the ordinary General
83 | Public License. It also provides other free software developers Less
84 | of an advantage over competing non-free programs. These disadvantages
85 | are the reason we use the ordinary General Public License for many
86 | libraries. However, the Lesser license provides advantages in certain
87 | special circumstances.
88 |
89 | For example, on rare occasions, there may be a special need to
90 | encourage the widest possible use of a certain library, so that it becomes
91 | a de-facto standard. To achieve this, non-free programs must be
92 | allowed to use the library. A more frequent case is that a free
93 | library does the same job as widely used non-free libraries. In this
94 | case, there is little to gain by limiting the free library to free
95 | software only, so we use the Lesser General Public License.
96 |
97 | In other cases, permission to use a particular library in non-free
98 | programs enables a greater number of people to use a large body of
99 | free software. For example, permission to use the GNU C Library in
100 | non-free programs enables many more people to use the whole GNU
101 | operating system, as well as its variant, the GNU/Linux operating
102 | system.
103 |
104 | Although the Lesser General Public License is Less protective of the
105 | users' freedom, it does ensure that the user of a program that is
106 | linked with the Library has the freedom and the wherewithal to run
107 | that program using a modified version of the Library.
108 |
109 | The precise terms and conditions for copying, distribution and
110 | modification follow. Pay close attention to the difference between a
111 | "work based on the library" and a "work that uses the library". The
112 | former contains code derived from the library, whereas the latter must
113 | be combined with the library in order to run.
114 |
115 | GNU LESSER GENERAL PUBLIC LICENSE
116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
117 |
118 | 0. This License Agreement applies to any software library or other
119 | program which contains a notice placed by the copyright holder or
120 | other authorized party saying it may be distributed under the terms of
121 | this Lesser General Public License (also called "this License").
122 | Each licensee is addressed as "you".
123 |
124 | A "library" means a collection of software functions and/or data
125 | prepared so as to be conveniently linked with application programs
126 | (which use some of those functions and data) to form executables.
127 |
128 | The "Library", below, refers to any such software library or work
129 | which has been distributed under these terms. A "work based on the
130 | Library" means either the Library or any derivative work under
131 | copyright law: that is to say, a work containing the Library or a
132 | portion of it, either verbatim or with modifications and/or translated
133 | straightforwardly into another language. (Hereinafter, translation is
134 | included without limitation in the term "modification".)
135 |
136 | "Source code" for a work means the preferred form of the work for
137 | making modifications to it. For a library, complete source code means
138 | all the source code for all modules it contains, plus any associated
139 | interface definition files, plus the scripts used to control compilation
140 | and installation of the library.
141 |
142 | Activities other than copying, distribution and modification are not
143 | covered by this License; they are outside its scope. The act of
144 | running a program using the Library is not restricted, and output from
145 | such a program is covered only if its contents constitute a work based
146 | on the Library (independent of the use of the Library in a tool for
147 | writing it). Whether that is true depends on what the Library does
148 | and what the program that uses the Library does.
149 |
150 | 1. You may copy and distribute verbatim copies of the Library's
151 | complete source code as you receive it, in any medium, provided that
152 | you conspicuously and appropriately publish on each copy an
153 | appropriate copyright notice and disclaimer of warranty; keep intact
154 | all the notices that refer to this License and to the absence of any
155 | warranty; and distribute a copy of this License along with the
156 | Library.
157 |
158 | You may charge a fee for the physical act of transferring a copy,
159 | and you may at your option offer warranty protection in exchange for a
160 | fee.
161 |
162 | 2. You may modify your copy or copies of the Library or any portion
163 | of it, thus forming a work based on the Library, and copy and
164 | distribute such modifications or work under the terms of Section 1
165 | above, provided that you also meet all of these conditions:
166 |
167 | a) The modified work must itself be a software library.
168 |
169 | b) You must cause the files modified to carry prominent notices
170 | stating that you changed the files and the date of any change.
171 |
172 | c) You must cause the whole of the work to be licensed at no
173 | charge to all third parties under the terms of this License.
174 |
175 | d) If a facility in the modified Library refers to a function or a
176 | table of data to be supplied by an application program that uses
177 | the facility, other than as an argument passed when the facility
178 | is invoked, then you must make a good faith effort to ensure that,
179 | in the event an application does not supply such function or
180 | table, the facility still operates, and performs whatever part of
181 | its purpose remains meaningful.
182 |
183 | (For example, a function in a library to compute square roots has
184 | a purpose that is entirely well-defined independent of the
185 | application. Therefore, Subsection 2d requires that any
186 | application-supplied function or table used by this function must
187 | be optional: if the application does not supply it, the square
188 | root function must still compute square roots.)
189 |
190 | These requirements apply to the modified work as a whole. If
191 | identifiable sections of that work are not derived from the Library,
192 | and can be reasonably considered independent and separate works in
193 | themselves, then this License, and its terms, do not apply to those
194 | sections when you distribute them as separate works. But when you
195 | distribute the same sections as part of a whole which is a work based
196 | on the Library, the distribution of the whole must be on the terms of
197 | this License, whose permissions for other licensees extend to the
198 | entire whole, and thus to each and every part regardless of who wrote
199 | it.
200 |
201 | Thus, it is not the intent of this section to claim rights or contest
202 | your rights to work written entirely by you; rather, the intent is to
203 | exercise the right to control the distribution of derivative or
204 | collective works based on the Library.
205 |
206 | In addition, mere aggregation of another work not based on the Library
207 | with the Library (or with a work based on the Library) on a volume of
208 | a storage or distribution medium does not bring the other work under
209 | the scope of this License.
210 |
211 | 3. You may opt to apply the terms of the ordinary GNU General Public
212 | License instead of this License to a given copy of the Library. To do
213 | this, you must alter all the notices that refer to this License, so
214 | that they refer to the ordinary GNU General Public License, version 2,
215 | instead of to this License. (If a newer version than version 2 of the
216 | ordinary GNU General Public License has appeared, then you can specify
217 | that version instead if you wish.) Do not make any other change in
218 | these notices.
219 |
220 | Once this change is made in a given copy, it is irreversible for
221 | that copy, so the ordinary GNU General Public License applies to all
222 | subsequent copies and derivative works made from that copy.
223 |
224 | This option is useful when you wish to copy part of the code of
225 | the Library into a program that is not a library.
226 |
227 | 4. You may copy and distribute the Library (or a portion or
228 | derivative of it, under Section 2) in object code or executable form
229 | under the terms of Sections 1 and 2 above provided that you accompany
230 | it with the complete corresponding machine-readable source code, which
231 | must be distributed under the terms of Sections 1 and 2 above on a
232 | medium customarily used for software interchange.
233 |
234 | If distribution of object code is made by offering access to copy
235 | from a designated place, then offering equivalent access to copy the
236 | source code from the same place satisfies the requirement to
237 | distribute the source code, even though third parties are not
238 | compelled to copy the source along with the object code.
239 |
240 | 5. A program that contains no derivative of any portion of the
241 | Library, but is designed to work with the Library by being compiled or
242 | linked with it, is called a "work that uses the Library". Such a
243 | work, in isolation, is not a derivative work of the Library, and
244 | therefore falls outside the scope of this License.
245 |
246 | However, linking a "work that uses the Library" with the Library
247 | creates an executable that is a derivative of the Library (because it
248 | contains portions of the Library), rather than a "work that uses the
249 | library". The executable is therefore covered by this License.
250 | Section 6 states terms for distribution of such executables.
251 |
252 | When a "work that uses the Library" uses material from a header file
253 | that is part of the Library, the object code for the work may be a
254 | derivative work of the Library even though the source code is not.
255 | Whether this is true is especially significant if the work can be
256 | linked without the Library, or if the work is itself a library. The
257 | threshold for this to be true is not precisely defined by law.
258 |
259 | If such an object file uses only numerical parameters, data
260 | structure layouts and accessors, and small macros and small inline
261 | functions (ten lines or less in length), then the use of the object
262 | file is unrestricted, regardless of whether it is legally a derivative
263 | work. (Executables containing this object code plus portions of the
264 | Library will still fall under Section 6.)
265 |
266 | Otherwise, if the work is a derivative of the Library, you may
267 | distribute the object code for the work under the terms of Section 6.
268 | Any executables containing that work also fall under Section 6,
269 | whether or not they are linked directly with the Library itself.
270 |
271 | 6. As an exception to the Sections above, you may also combine or
272 | link a "work that uses the Library" with the Library to produce a
273 | work containing portions of the Library, and distribute that work
274 | under terms of your choice, provided that the terms permit
275 | modification of the work for the customer's own use and reverse
276 | engineering for debugging such modifications.
277 |
278 | You must give prominent notice with each copy of the work that the
279 | Library is used in it and that the Library and its use are covered by
280 | this License. You must supply a copy of this License. If the work
281 | during execution displays copyright notices, you must include the
282 | copyright notice for the Library among them, as well as a reference
283 | directing the user to the copy of this License. Also, you must do one
284 | of these things:
285 |
286 | a) Accompany the work with the complete corresponding
287 | machine-readable source code for the Library including whatever
288 | changes were used in the work (which must be distributed under
289 | Sections 1 and 2 above); and, if the work is an executable linked
290 | with the Library, with the complete machine-readable "work that
291 | uses the Library", as object code and/or source code, so that the
292 | user can modify the Library and then relink to produce a modified
293 | executable containing the modified Library. (It is understood
294 | that the user who changes the contents of definitions files in the
295 | Library will not necessarily be able to recompile the application
296 | to use the modified definitions.)
297 |
298 | b) Use a suitable shared library mechanism for linking with the
299 | Library. A suitable mechanism is one that (1) uses at run time a
300 | copy of the library already present on the user's computer system,
301 | rather than copying library functions into the executable, and (2)
302 | will operate properly with a modified version of the library, if
303 | the user installs one, as long as the modified version is
304 | interface-compatible with the version that the work was made with.
305 |
306 | c) Accompany the work with a written offer, valid for at
307 | least three years, to give the same user the materials
308 | specified in Subsection 6a, above, for a charge no more
309 | than the cost of performing this distribution.
310 |
311 | d) If distribution of the work is made by offering access to copy
312 | from a designated place, offer equivalent access to copy the above
313 | specified materials from the same place.
314 |
315 | e) Verify that the user has already received a copy of these
316 | materials or that you have already sent this user a copy.
317 |
318 | For an executable, the required form of the "work that uses the
319 | Library" must include any data and utility programs needed for
320 | reproducing the executable from it. However, as a special exception,
321 | the materials to be distributed need not include anything that is
322 | normally distributed (in either source or binary form) with the major
323 | components (compiler, kernel, and so on) of the operating system on
324 | which the executable runs, unless that component itself accompanies
325 | the executable.
326 |
327 | It may happen that this requirement contradicts the license
328 | restrictions of other proprietary libraries that do not normally
329 | accompany the operating system. Such a contradiction means you cannot
330 | use both them and the Library together in an executable that you
331 | distribute.
332 |
333 | 7. You may place library facilities that are a work based on the
334 | Library side-by-side in a single library together with other library
335 | facilities not covered by this License, and distribute such a combined
336 | library, provided that the separate distribution of the work based on
337 | the Library and of the other library facilities is otherwise
338 | permitted, and provided that you do these two things:
339 |
340 | a) Accompany the combined library with a copy of the same work
341 | based on the Library, uncombined with any other library
342 | facilities. This must be distributed under the terms of the
343 | Sections above.
344 |
345 | b) Give prominent notice with the combined library of the fact
346 | that part of it is a work based on the Library, and explaining
347 | where to find the accompanying uncombined form of the same work.
348 |
349 | 8. You may not copy, modify, sublicense, link with, or distribute
350 | the Library except as expressly provided under this License. Any
351 | attempt otherwise to copy, modify, sublicense, link with, or
352 | distribute the Library is void, and will automatically terminate your
353 | rights under this License. However, parties who have received copies,
354 | or rights, from you under this License will not have their licenses
355 | terminated so long as such parties remain in full compliance.
356 |
357 | 9. You are not required to accept this License, since you have not
358 | signed it. However, nothing else grants you permission to modify or
359 | distribute the Library or its derivative works. These actions are
360 | prohibited by law if you do not accept this License. Therefore, by
361 | modifying or distributing the Library (or any work based on the
362 | Library), you indicate your acceptance of this License to do so, and
363 | all its terms and conditions for copying, distributing or modifying
364 | the Library or works based on it.
365 |
366 | 10. Each time you redistribute the Library (or any work based on the
367 | Library), the recipient automatically receives a license from the
368 | original licensor to copy, distribute, link with or modify the Library
369 | subject to these terms and conditions. You may not impose any further
370 | restrictions on the recipients' exercise of the rights granted herein.
371 | You are not responsible for enforcing compliance by third parties with
372 | this License.
373 |
374 | 11. If, as a consequence of a court judgment or allegation of patent
375 | infringement or for any other reason (not limited to patent issues),
376 | conditions are imposed on you (whether by court order, agreement or
377 | otherwise) that contradict the conditions of this License, they do not
378 | excuse you from the conditions of this License. If you cannot
379 | distribute so as to satisfy simultaneously your obligations under this
380 | License and any other pertinent obligations, then as a consequence you
381 | may not distribute the Library at all. For example, if a patent
382 | license would not permit royalty-free redistribution of the Library by
383 | all those who receive copies directly or indirectly through you, then
384 | the only way you could satisfy both it and this License would be to
385 | refrain entirely from distribution of the Library.
386 |
387 | If any portion of this section is held invalid or unenforceable under any
388 | particular circumstance, the balance of the section is intended to apply,
389 | and the section as a whole is intended to apply in other circumstances.
390 |
391 | It is not the purpose of this section to induce you to infringe any
392 | patents or other property right claims or to contest validity of any
393 | such claims; this section has the sole purpose of protecting the
394 | integrity of the free software distribution system which is
395 | implemented by public license practices. Many people have made
396 | generous contributions to the wide range of software distributed
397 | through that system in reliance on consistent application of that
398 | system; it is up to the author/donor to decide if he or she is willing
399 | to distribute software through any other system and a licensee cannot
400 | impose that choice.
401 |
402 | This section is intended to make thoroughly clear what is believed to
403 | be a consequence of the rest of this License.
404 |
405 | 12. If the distribution and/or use of the Library is restricted in
406 | certain countries either by patents or by copyrighted interfaces, the
407 | original copyright holder who places the Library under this License may add
408 | an explicit geographical distribution limitation excluding those countries,
409 | so that distribution is permitted only in or among countries not thus
410 | excluded. In such case, this License incorporates the limitation as if
411 | written in the body of this License.
412 |
413 | 13. The Free Software Foundation may publish revised and/or new
414 | versions of the Lesser General Public License from time to time.
415 | Such new versions will be similar in spirit to the present version,
416 | but may differ in detail to address new problems or concerns.
417 |
418 | Each version is given a distinguishing version number. If the Library
419 | specifies a version number of this License which applies to it and
420 | "any later version", you have the option of following the terms and
421 | conditions either of that version or of any later version published by
422 | the Free Software Foundation. If the Library does not specify a
423 | license version number, you may choose any version ever published by
424 | the Free Software Foundation.
425 |
426 | 14. If you wish to incorporate parts of the Library into other free
427 | programs whose distribution conditions are incompatible with these,
428 | write to the author to ask for permission. For software which is
429 | copyrighted by the Free Software Foundation, write to the Free
430 | Software Foundation; we sometimes make exceptions for this. Our
431 | decision will be guided by the two goals of preserving the free status
432 | of all derivatives of our free software and of promoting the sharing
433 | and reuse of software generally.
434 |
435 | NO WARRANTY
436 |
437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
446 |
447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
456 | DAMAGES.
457 |
458 | END OF TERMS AND CONDITIONS
459 |
--------------------------------------------------------------------------------
/COPYING.LESSER:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 2.1, February 1999
3 |
4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc.
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | [This is the first released version of the Lesser GPL. It also counts
10 | as the successor of the GNU Library Public License, version 2, hence
11 | the version number 2.1.]
12 |
13 | Preamble
14 |
15 | The licenses for most software are designed to take away your
16 | freedom to share and change it. By contrast, the GNU General Public
17 | Licenses are intended to guarantee your freedom to share and change
18 | free software--to make sure the software is free for all its users.
19 |
20 | This license, the Lesser General Public License, applies to some
21 | specially designated software packages--typically libraries--of the
22 | Free Software Foundation and other authors who decide to use it. You
23 | can use it too, but we suggest you first think carefully about whether
24 | this license or the ordinary General Public License is the better
25 | strategy to use in any particular case, based on the explanations below.
26 |
27 | When we speak of free software, we are referring to freedom of use,
28 | not price. Our General Public Licenses are designed to make sure that
29 | you have the freedom to distribute copies of free software (and charge
30 | for this service if you wish); that you receive source code or can get
31 | it if you want it; that you can change the software and use pieces of
32 | it in new free programs; and that you are informed that you can do
33 | these things.
34 |
35 | To protect your rights, we need to make restrictions that forbid
36 | distributors to deny you these rights or to ask you to surrender these
37 | rights. These restrictions translate to certain responsibilities for
38 | you if you distribute copies of the library or if you modify it.
39 |
40 | For example, if you distribute copies of the library, whether gratis
41 | or for a fee, you must give the recipients all the rights that we gave
42 | you. You must make sure that they, too, receive or can get the source
43 | code. If you link other code with the library, you must provide
44 | complete object files to the recipients, so that they can relink them
45 | with the library after making changes to the library and recompiling
46 | it. And you must show them these terms so they know their rights.
47 |
48 | We protect your rights with a two-step method: (1) we copyright the
49 | library, and (2) we offer you this license, which gives you legal
50 | permission to copy, distribute and/or modify the library.
51 |
52 | To protect each distributor, we want to make it very clear that
53 | there is no warranty for the free library. Also, if the library is
54 | modified by someone else and passed on, the recipients should know
55 | that what they have is not the original version, so that the original
56 | author's reputation will not be affected by problems that might be
57 | introduced by others.
58 |
59 | Finally, software patents pose a constant threat to the existence of
60 | any free program. We wish to make sure that a company cannot
61 | effectively restrict the users of a free program by obtaining a
62 | restrictive license from a patent holder. Therefore, we insist that
63 | any patent license obtained for a version of the library must be
64 | consistent with the full freedom of use specified in this license.
65 |
66 | Most GNU software, including some libraries, is covered by the
67 | ordinary GNU General Public License. This license, the GNU Lesser
68 | General Public License, applies to certain designated libraries, and
69 | is quite different from the ordinary General Public License. We use
70 | this license for certain libraries in order to permit linking those
71 | libraries into non-free programs.
72 |
73 | When a program is linked with a library, whether statically or using
74 | a shared library, the combination of the two is legally speaking a
75 | combined work, a derivative of the original library. The ordinary
76 | General Public License therefore permits such linking only if the
77 | entire combination fits its criteria of freedom. The Lesser General
78 | Public License permits more lax criteria for linking other code with
79 | the library.
80 |
81 | We call this license the "Lesser" General Public License because it
82 | does Less to protect the user's freedom than the ordinary General
83 | Public License. It also provides other free software developers Less
84 | of an advantage over competing non-free programs. These disadvantages
85 | are the reason we use the ordinary General Public License for many
86 | libraries. However, the Lesser license provides advantages in certain
87 | special circumstances.
88 |
89 | For example, on rare occasions, there may be a special need to
90 | encourage the widest possible use of a certain library, so that it becomes
91 | a de-facto standard. To achieve this, non-free programs must be
92 | allowed to use the library. A more frequent case is that a free
93 | library does the same job as widely used non-free libraries. In this
94 | case, there is little to gain by limiting the free library to free
95 | software only, so we use the Lesser General Public License.
96 |
97 | In other cases, permission to use a particular library in non-free
98 | programs enables a greater number of people to use a large body of
99 | free software. For example, permission to use the GNU C Library in
100 | non-free programs enables many more people to use the whole GNU
101 | operating system, as well as its variant, the GNU/Linux operating
102 | system.
103 |
104 | Although the Lesser General Public License is Less protective of the
105 | users' freedom, it does ensure that the user of a program that is
106 | linked with the Library has the freedom and the wherewithal to run
107 | that program using a modified version of the Library.
108 |
109 | The precise terms and conditions for copying, distribution and
110 | modification follow. Pay close attention to the difference between a
111 | "work based on the library" and a "work that uses the library". The
112 | former contains code derived from the library, whereas the latter must
113 | be combined with the library in order to run.
114 |
115 | GNU LESSER GENERAL PUBLIC LICENSE
116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
117 |
118 | 0. This License Agreement applies to any software library or other
119 | program which contains a notice placed by the copyright holder or
120 | other authorized party saying it may be distributed under the terms of
121 | this Lesser General Public License (also called "this License").
122 | Each licensee is addressed as "you".
123 |
124 | A "library" means a collection of software functions and/or data
125 | prepared so as to be conveniently linked with application programs
126 | (which use some of those functions and data) to form executables.
127 |
128 | The "Library", below, refers to any such software library or work
129 | which has been distributed under these terms. A "work based on the
130 | Library" means either the Library or any derivative work under
131 | copyright law: that is to say, a work containing the Library or a
132 | portion of it, either verbatim or with modifications and/or translated
133 | straightforwardly into another language. (Hereinafter, translation is
134 | included without limitation in the term "modification".)
135 |
136 | "Source code" for a work means the preferred form of the work for
137 | making modifications to it. For a library, complete source code means
138 | all the source code for all modules it contains, plus any associated
139 | interface definition files, plus the scripts used to control compilation
140 | and installation of the library.
141 |
142 | Activities other than copying, distribution and modification are not
143 | covered by this License; they are outside its scope. The act of
144 | running a program using the Library is not restricted, and output from
145 | such a program is covered only if its contents constitute a work based
146 | on the Library (independent of the use of the Library in a tool for
147 | writing it). Whether that is true depends on what the Library does
148 | and what the program that uses the Library does.
149 |
150 | 1. You may copy and distribute verbatim copies of the Library's
151 | complete source code as you receive it, in any medium, provided that
152 | you conspicuously and appropriately publish on each copy an
153 | appropriate copyright notice and disclaimer of warranty; keep intact
154 | all the notices that refer to this License and to the absence of any
155 | warranty; and distribute a copy of this License along with the
156 | Library.
157 |
158 | You may charge a fee for the physical act of transferring a copy,
159 | and you may at your option offer warranty protection in exchange for a
160 | fee.
161 |
162 | 2. You may modify your copy or copies of the Library or any portion
163 | of it, thus forming a work based on the Library, and copy and
164 | distribute such modifications or work under the terms of Section 1
165 | above, provided that you also meet all of these conditions:
166 |
167 | a) The modified work must itself be a software library.
168 |
169 | b) You must cause the files modified to carry prominent notices
170 | stating that you changed the files and the date of any change.
171 |
172 | c) You must cause the whole of the work to be licensed at no
173 | charge to all third parties under the terms of this License.
174 |
175 | d) If a facility in the modified Library refers to a function or a
176 | table of data to be supplied by an application program that uses
177 | the facility, other than as an argument passed when the facility
178 | is invoked, then you must make a good faith effort to ensure that,
179 | in the event an application does not supply such function or
180 | table, the facility still operates, and performs whatever part of
181 | its purpose remains meaningful.
182 |
183 | (For example, a function in a library to compute square roots has
184 | a purpose that is entirely well-defined independent of the
185 | application. Therefore, Subsection 2d requires that any
186 | application-supplied function or table used by this function must
187 | be optional: if the application does not supply it, the square
188 | root function must still compute square roots.)
189 |
190 | These requirements apply to the modified work as a whole. If
191 | identifiable sections of that work are not derived from the Library,
192 | and can be reasonably considered independent and separate works in
193 | themselves, then this License, and its terms, do not apply to those
194 | sections when you distribute them as separate works. But when you
195 | distribute the same sections as part of a whole which is a work based
196 | on the Library, the distribution of the whole must be on the terms of
197 | this License, whose permissions for other licensees extend to the
198 | entire whole, and thus to each and every part regardless of who wrote
199 | it.
200 |
201 | Thus, it is not the intent of this section to claim rights or contest
202 | your rights to work written entirely by you; rather, the intent is to
203 | exercise the right to control the distribution of derivative or
204 | collective works based on the Library.
205 |
206 | In addition, mere aggregation of another work not based on the Library
207 | with the Library (or with a work based on the Library) on a volume of
208 | a storage or distribution medium does not bring the other work under
209 | the scope of this License.
210 |
211 | 3. You may opt to apply the terms of the ordinary GNU General Public
212 | License instead of this License to a given copy of the Library. To do
213 | this, you must alter all the notices that refer to this License, so
214 | that they refer to the ordinary GNU General Public License, version 2,
215 | instead of to this License. (If a newer version than version 2 of the
216 | ordinary GNU General Public License has appeared, then you can specify
217 | that version instead if you wish.) Do not make any other change in
218 | these notices.
219 |
220 | Once this change is made in a given copy, it is irreversible for
221 | that copy, so the ordinary GNU General Public License applies to all
222 | subsequent copies and derivative works made from that copy.
223 |
224 | This option is useful when you wish to copy part of the code of
225 | the Library into a program that is not a library.
226 |
227 | 4. You may copy and distribute the Library (or a portion or
228 | derivative of it, under Section 2) in object code or executable form
229 | under the terms of Sections 1 and 2 above provided that you accompany
230 | it with the complete corresponding machine-readable source code, which
231 | must be distributed under the terms of Sections 1 and 2 above on a
232 | medium customarily used for software interchange.
233 |
234 | If distribution of object code is made by offering access to copy
235 | from a designated place, then offering equivalent access to copy the
236 | source code from the same place satisfies the requirement to
237 | distribute the source code, even though third parties are not
238 | compelled to copy the source along with the object code.
239 |
240 | 5. A program that contains no derivative of any portion of the
241 | Library, but is designed to work with the Library by being compiled or
242 | linked with it, is called a "work that uses the Library". Such a
243 | work, in isolation, is not a derivative work of the Library, and
244 | therefore falls outside the scope of this License.
245 |
246 | However, linking a "work that uses the Library" with the Library
247 | creates an executable that is a derivative of the Library (because it
248 | contains portions of the Library), rather than a "work that uses the
249 | library". The executable is therefore covered by this License.
250 | Section 6 states terms for distribution of such executables.
251 |
252 | When a "work that uses the Library" uses material from a header file
253 | that is part of the Library, the object code for the work may be a
254 | derivative work of the Library even though the source code is not.
255 | Whether this is true is especially significant if the work can be
256 | linked without the Library, or if the work is itself a library. The
257 | threshold for this to be true is not precisely defined by law.
258 |
259 | If such an object file uses only numerical parameters, data
260 | structure layouts and accessors, and small macros and small inline
261 | functions (ten lines or less in length), then the use of the object
262 | file is unrestricted, regardless of whether it is legally a derivative
263 | work. (Executables containing this object code plus portions of the
264 | Library will still fall under Section 6.)
265 |
266 | Otherwise, if the work is a derivative of the Library, you may
267 | distribute the object code for the work under the terms of Section 6.
268 | Any executables containing that work also fall under Section 6,
269 | whether or not they are linked directly with the Library itself.
270 |
271 | 6. As an exception to the Sections above, you may also combine or
272 | link a "work that uses the Library" with the Library to produce a
273 | work containing portions of the Library, and distribute that work
274 | under terms of your choice, provided that the terms permit
275 | modification of the work for the customer's own use and reverse
276 | engineering for debugging such modifications.
277 |
278 | You must give prominent notice with each copy of the work that the
279 | Library is used in it and that the Library and its use are covered by
280 | this License. You must supply a copy of this License. If the work
281 | during execution displays copyright notices, you must include the
282 | copyright notice for the Library among them, as well as a reference
283 | directing the user to the copy of this License. Also, you must do one
284 | of these things:
285 |
286 | a) Accompany the work with the complete corresponding
287 | machine-readable source code for the Library including whatever
288 | changes were used in the work (which must be distributed under
289 | Sections 1 and 2 above); and, if the work is an executable linked
290 | with the Library, with the complete machine-readable "work that
291 | uses the Library", as object code and/or source code, so that the
292 | user can modify the Library and then relink to produce a modified
293 | executable containing the modified Library. (It is understood
294 | that the user who changes the contents of definitions files in the
295 | Library will not necessarily be able to recompile the application
296 | to use the modified definitions.)
297 |
298 | b) Use a suitable shared library mechanism for linking with the
299 | Library. A suitable mechanism is one that (1) uses at run time a
300 | copy of the library already present on the user's computer system,
301 | rather than copying library functions into the executable, and (2)
302 | will operate properly with a modified version of the library, if
303 | the user installs one, as long as the modified version is
304 | interface-compatible with the version that the work was made with.
305 |
306 | c) Accompany the work with a written offer, valid for at
307 | least three years, to give the same user the materials
308 | specified in Subsection 6a, above, for a charge no more
309 | than the cost of performing this distribution.
310 |
311 | d) If distribution of the work is made by offering access to copy
312 | from a designated place, offer equivalent access to copy the above
313 | specified materials from the same place.
314 |
315 | e) Verify that the user has already received a copy of these
316 | materials or that you have already sent this user a copy.
317 |
318 | For an executable, the required form of the "work that uses the
319 | Library" must include any data and utility programs needed for
320 | reproducing the executable from it. However, as a special exception,
321 | the materials to be distributed need not include anything that is
322 | normally distributed (in either source or binary form) with the major
323 | components (compiler, kernel, and so on) of the operating system on
324 | which the executable runs, unless that component itself accompanies
325 | the executable.
326 |
327 | It may happen that this requirement contradicts the license
328 | restrictions of other proprietary libraries that do not normally
329 | accompany the operating system. Such a contradiction means you cannot
330 | use both them and the Library together in an executable that you
331 | distribute.
332 |
333 | 7. You may place library facilities that are a work based on the
334 | Library side-by-side in a single library together with other library
335 | facilities not covered by this License, and distribute such a combined
336 | library, provided that the separate distribution of the work based on
337 | the Library and of the other library facilities is otherwise
338 | permitted, and provided that you do these two things:
339 |
340 | a) Accompany the combined library with a copy of the same work
341 | based on the Library, uncombined with any other library
342 | facilities. This must be distributed under the terms of the
343 | Sections above.
344 |
345 | b) Give prominent notice with the combined library of the fact
346 | that part of it is a work based on the Library, and explaining
347 | where to find the accompanying uncombined form of the same work.
348 |
349 | 8. You may not copy, modify, sublicense, link with, or distribute
350 | the Library except as expressly provided under this License. Any
351 | attempt otherwise to copy, modify, sublicense, link with, or
352 | distribute the Library is void, and will automatically terminate your
353 | rights under this License. However, parties who have received copies,
354 | or rights, from you under this License will not have their licenses
355 | terminated so long as such parties remain in full compliance.
356 |
357 | 9. You are not required to accept this License, since you have not
358 | signed it. However, nothing else grants you permission to modify or
359 | distribute the Library or its derivative works. These actions are
360 | prohibited by law if you do not accept this License. Therefore, by
361 | modifying or distributing the Library (or any work based on the
362 | Library), you indicate your acceptance of this License to do so, and
363 | all its terms and conditions for copying, distributing or modifying
364 | the Library or works based on it.
365 |
366 | 10. Each time you redistribute the Library (or any work based on the
367 | Library), the recipient automatically receives a license from the
368 | original licensor to copy, distribute, link with or modify the Library
369 | subject to these terms and conditions. You may not impose any further
370 | restrictions on the recipients' exercise of the rights granted herein.
371 | You are not responsible for enforcing compliance by third parties with
372 | this License.
373 |
374 | 11. If, as a consequence of a court judgment or allegation of patent
375 | infringement or for any other reason (not limited to patent issues),
376 | conditions are imposed on you (whether by court order, agreement or
377 | otherwise) that contradict the conditions of this License, they do not
378 | excuse you from the conditions of this License. If you cannot
379 | distribute so as to satisfy simultaneously your obligations under this
380 | License and any other pertinent obligations, then as a consequence you
381 | may not distribute the Library at all. For example, if a patent
382 | license would not permit royalty-free redistribution of the Library by
383 | all those who receive copies directly or indirectly through you, then
384 | the only way you could satisfy both it and this License would be to
385 | refrain entirely from distribution of the Library.
386 |
387 | If any portion of this section is held invalid or unenforceable under any
388 | particular circumstance, the balance of the section is intended to apply,
389 | and the section as a whole is intended to apply in other circumstances.
390 |
391 | It is not the purpose of this section to induce you to infringe any
392 | patents or other property right claims or to contest validity of any
393 | such claims; this section has the sole purpose of protecting the
394 | integrity of the free software distribution system which is
395 | implemented by public license practices. Many people have made
396 | generous contributions to the wide range of software distributed
397 | through that system in reliance on consistent application of that
398 | system; it is up to the author/donor to decide if he or she is willing
399 | to distribute software through any other system and a licensee cannot
400 | impose that choice.
401 |
402 | This section is intended to make thoroughly clear what is believed to
403 | be a consequence of the rest of this License.
404 |
405 | 12. If the distribution and/or use of the Library is restricted in
406 | certain countries either by patents or by copyrighted interfaces, the
407 | original copyright holder who places the Library under this License may add
408 | an explicit geographical distribution limitation excluding those countries,
409 | so that distribution is permitted only in or among countries not thus
410 | excluded. In such case, this License incorporates the limitation as if
411 | written in the body of this License.
412 |
413 | 13. The Free Software Foundation may publish revised and/or new
414 | versions of the Lesser General Public License from time to time.
415 | Such new versions will be similar in spirit to the present version,
416 | but may differ in detail to address new problems or concerns.
417 |
418 | Each version is given a distinguishing version number. If the Library
419 | specifies a version number of this License which applies to it and
420 | "any later version", you have the option of following the terms and
421 | conditions either of that version or of any later version published by
422 | the Free Software Foundation. If the Library does not specify a
423 | license version number, you may choose any version ever published by
424 | the Free Software Foundation.
425 |
426 | 14. If you wish to incorporate parts of the Library into other free
427 | programs whose distribution conditions are incompatible with these,
428 | write to the author to ask for permission. For software which is
429 | copyrighted by the Free Software Foundation, write to the Free
430 | Software Foundation; we sometimes make exceptions for this. Our
431 | decision will be guided by the two goals of preserving the free status
432 | of all derivatives of our free software and of promoting the sharing
433 | and reuse of software generally.
434 |
435 | NO WARRANTY
436 |
437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
446 |
447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
456 | DAMAGES.
457 |
458 | END OF TERMS AND CONDITIONS
459 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | gpxplotter - A package for some simple plotting of GPX files.
2 | Copyright (C) 2021, Anders Lervik
3 |
4 | gpxplotter is released under the GNU Lesser General Public License v2.1+.
5 | Please see the files: COPYING and COPYING.LESSER included in the source
6 | directory.
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy
9 | of this software and associated documentation files (the "Software"), to deal
10 | in the Software without restriction, including without limitation the rights
11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | copies of the Software, and to permit persons to whom the Software is
13 | furnished to do so, subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in all
16 | copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | SOFTWARE.
25 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include COPYING
2 | include COPYING.LESSER
3 | include LICENSE
4 | include README.md
5 | include requirements.txt
6 | include pypireadme.md
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gpxplotter
2 |
3 | [](https://gpxplotter.readthedocs.io/en/latest/?badge=latest)
4 | [](https://mybinder.org/v2/gh/andersle/gpxplotter/main?filepath=examples%2Fjupyter%2F)
5 |
6 | **gpxplotter** is a Python package for reading
7 | [gpx](https://en.wikipedia.org/wiki/GPS_Exchange_Format)
8 | files and creating simple predefined plots using
9 | [matplotlib](http://matplotlib.org/)
10 | and maps using
11 | [folium](https://python-visualization.github.io/folium/).
12 |
13 | Please see
14 | [https://gpxplotter.readthedocs.io/en/latest/](https://gpxplotter.readthedocs.io/en/latest/)
15 | for the latest documentation and the
16 | [Binder notebooks](https://mybinder.org/v2/gh/andersle/gpxplotter/main?filepath=examples%2Fjupyter%2F) for examples.
17 |
18 | ## Installation
19 |
20 | ```
21 | pip install gpxplotter
22 | ```
23 |
24 | ## Examples
25 |
26 | Interactive examples can be explored
27 | via [Binder](https://mybinder.org/v2/gh/andersle/gpxplotter/main?filepath=examples%2Fjupyter%2F).
28 |
29 |
30 | #### Simple example for showing a track in a map, colored by heart rate
31 |
32 | ```python
33 |
34 | from gpxplotter import read_gpx_file, create_folium_map, add_segment_to_map
35 |
36 | the_map = create_folium_map()
37 | for track in read_gpx_file('ruten.gpx'):
38 | for i, segment in enumerate(track['segments']):
39 | add_segment_to_map(the_map, segment, color_by='hr')
40 |
41 | # To display the map in a Jupyter notebook:
42 | the_map
43 | ```
44 |
45 | [](examples/html/map001.html)
46 |
47 | ### Further examples
48 |
49 | Please see the [gallery in the documentation](https://gpxplotter.readthedocs.io/en/latest/auto_examples_maps/index.html)
50 | for further examples.
51 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/source/_static/image1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/_static/image1.jpg
--------------------------------------------------------------------------------
/docs/source/_static/image2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/_static/image2.jpg
--------------------------------------------------------------------------------
/docs/source/_static/image3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/_static/image3.jpg
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | import os
14 | import sys
15 | sys.path.insert(0, os.path.abspath('./extensions'))
16 | from datetime import date
17 | import warnings
18 | import gpxplotter
19 | from sphinx_gallery.sorting import FileNameSortKey
20 |
21 |
22 | # Disable matplotlib warnings for generation of gallery:
23 | warnings.filterwarnings(
24 | 'ignore',
25 | category=UserWarning,
26 | message='Matplotlib is currently using agg, which is a'
27 | ' non-GUI backend, so cannot show the figure.'
28 | )
29 |
30 | # -- Project information -----------------------------------------------------
31 | master_doc = 'index'
32 | project = 'gpxplotter'
33 |
34 | year = date.today().year
35 | if year > 2020:
36 | copyright = '2020-{}, Anders Lervik'.format(year)
37 | else:
38 | copyright = '2020, Anders Lervik'
39 | author = 'Anders Lervik'
40 |
41 | rst_prolog = '.. |gpxplotter| replace:: gpxplotter'
42 | # The full version, including alpha/beta/rc tags
43 | version = gpxplotter.version.SHORT_VERSION
44 | release = gpxplotter.version.VERSION
45 |
46 | # -- General configuration ---------------------------------------------------
47 |
48 | # Add any Sphinx extension module names here, as strings. They can be
49 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
50 | # ones.
51 | extensions = [
52 | 'sphinx.ext.autodoc',
53 | 'sphinx.ext.viewcode',
54 | 'sphinx.ext.napoleon',
55 | 'sphinx.ext.intersphinx',
56 | 'sphinx_gallery.gen_gallery',
57 | 'thumbnail_updater',
58 | ]
59 |
60 | # Napoleon settings
61 | napoleon_numpy_docstring = True
62 | napoleon_include_init_with_doc = True
63 | napoleon_include_private_with_doc = True
64 | napoleon_include_special_with_doc = True
65 | napoleon_use_ivar = False
66 | napoleon_use_keyword = False
67 | napoleon_use_param = False
68 | napoleon_use_rtype = False
69 |
70 | # Add any paths that contain templates here, relative to this directory.
71 | templates_path = ['_templates']
72 |
73 | # List of patterns, relative to source directory, that match files and
74 | # directories to ignore when looking for source files.
75 | # This pattern also affects html_static_path and html_extra_path.
76 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
77 |
78 | # -- Options for HTML output -------------------------------------------------
79 |
80 | # The theme to use for HTML and HTML Help pages. See the documentation for
81 | # a list of builtin themes.
82 | #
83 | html_theme = 'sphinx_rtd_theme'
84 |
85 | # Add any paths that contain custom static files (such as style sheets) here,
86 | # relative to this directory. They are copied after the builtin static files,
87 | # so a file named "default.css" will overwrite the builtin "default.css".
88 | html_static_path = ['_static']
89 |
90 | intersphinx_mapping = {
91 | 'python': (
92 | 'https://docs.python.org/{.major}'.format(sys.version_info), None
93 | ),
94 | 'numpy': ('https://numpy.org/doc/stable/', None),
95 | 'scipy': ('https://docs.scipy.org/doc/scipy/reference', None),
96 | 'matplotlib': ('https://matplotlib.org/', None),
97 | 'sphinx': ('https://www.sphinx-doc.org/en/master/', None),
98 | 'pandas': ('https://pandas.pydata.org/pandas-docs/stable', None),
99 | }
100 |
101 | # Settings for gallery:
102 | sphinx_gallery_conf = {
103 | 'examples_dirs': ['gallery/maps', 'gallery/plots'],
104 | 'gallery_dirs': ['auto_examples_maps', 'auto_examples_plots'],
105 | 'download_all_examples': False,
106 | 'within_subsection_order': FileNameSortKey,
107 | }
108 |
109 | # Settings for thumbnail-updater:
110 | thumbnail_updater_conf = {
111 | 'thumbnail_dir': 'gallery_thumbs'
112 | }
113 |
--------------------------------------------------------------------------------
/docs/source/extensions/thumbnail_updater.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """This module defines a class for replacing thumbnails
4 |
5 | This module defines a class that will do some post-processing
6 | for sphinx. It will replace some thumbnails that sphinx-gallery
7 | generate automatically. The use case here is to add thumbnails
8 | of HTML-pages that are not too easy to generate fully automatically.
9 |
10 | """
11 | import pathlib
12 | from shutil import copyfile
13 | from docutils import nodes
14 | from sphinx.transforms.post_transforms import SphinxPostTransform
15 |
16 |
17 | DEFAULTS = {
18 | 'thumbnail_dir': 'gallery_thumbs',
19 | }
20 |
21 |
22 | class ThumnailUpdater(SphinxPostTransform):
23 | """Replace thumbnails generated by sphinx-gallery."""
24 | default_priority = 100
25 |
26 | def run(self, **kwargs):
27 | # Check if we can get the config
28 | gallery_conf = getattr(self.config, 'sphinx_gallery_conf')
29 | gallery_dirs = gallery_conf['gallery_dirs']
30 |
31 | config = DEFAULTS.copy()
32 | config_set = getattr(self.config, 'thumbnail_updater_conf', {})
33 | config.update(config_set)
34 | thumbs_dir = pathlib.Path(self.app.srcdir).joinpath(
35 | config['thumbnail_dir']
36 | )
37 | thumbs_files = [i for i in thumbs_dir.rglob('*') if i.is_file()]
38 |
39 | for node in self.document.traverse(nodes.image):
40 | path = pathlib.Path(node['uri'])
41 | if not check_file_path(path, gallery_dirs):
42 | continue
43 | source = None
44 | for i in thumbs_files:
45 | if i.name == path.name:
46 | source = i
47 | break
48 | if source:
49 | target = pathlib.Path(self.app.srcdir).joinpath(path)
50 | copyfile(source, target)
51 |
52 |
53 | def check_file_path(path, gallery_dirs):
54 | """Check if a filename seems to be a thumbnail."""
55 | filename = path.name
56 | if '_thumb' not in filename:
57 | return False
58 | if 'sphx_glr' not in filename:
59 | return False
60 | parents = [i.name for i in path.parents]
61 | if 'thumb' not in parents:
62 | return False
63 | if not any((i in parents for i in gallery_dirs)):
64 | return False
65 | return True
66 |
67 |
68 | def setup(app):
69 | """Register the Thumbnail updater."""
70 | app.add_post_transform(ThumnailUpdater)
71 |
72 | return {
73 | 'version': '0.1',
74 | 'parallel_read_safe': True,
75 | 'parallel_write_safe': True,
76 | }
77 |
--------------------------------------------------------------------------------
/docs/source/gallery/maps/README.txt:
--------------------------------------------------------------------------------
1 | .. _examples-maps:
2 |
3 | Creating maps
4 | -------------
5 |
6 | Here are some examples that add tracks/segments and also
7 | additional information (markers, colors, images, etc.) to maps.
8 |
--------------------------------------------------------------------------------
/docs/source/gallery/maps/example1.gpx:
--------------------------------------------------------------------------------
1 | ../../_static/example1.gpx
--------------------------------------------------------------------------------
/docs/source/gallery/maps/example2.gpx:
--------------------------------------------------------------------------------
1 | ../../_static/example2.gpx
--------------------------------------------------------------------------------
/docs/source/gallery/maps/example3.gpx:
--------------------------------------------------------------------------------
1 | ../../_static/example3.gpx
--------------------------------------------------------------------------------
/docs/source/gallery/maps/example4.gpx:
--------------------------------------------------------------------------------
1 | ../../_static/example4.gpx
--------------------------------------------------------------------------------
/docs/source/gallery/maps/plot_000_segment.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Display a segment in a map
5 | ==========================
6 |
7 | This example will create a map and add a track to it.
8 | """
9 | from gpxplotter import add_segment_to_map, create_folium_map, read_gpx_file
10 |
11 | # Define some properties for drawing the line:
12 | line_options = {"color": "red", "weight": 8, "opacity": 0.5}
13 |
14 | the_map = create_folium_map(tiles="kartverket_topo4")
15 | for track in read_gpx_file("example1.gpx"):
16 | for i, segment in enumerate(track["segments"]):
17 | add_segment_to_map(the_map, segment, line_options=line_options)
18 |
19 | # To store the map as a HTML page:
20 | # the_map.save('map_000.html')
21 |
22 | # To display the map in a Jupyter notebook:
23 | the_map
24 |
--------------------------------------------------------------------------------
/docs/source/gallery/maps/plot_001_heart_rate.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Track colored by heart rate
5 | ===========================
6 |
7 | This example will create a map and color the track according
8 | to the measured heart rate.
9 | """
10 | import folium
11 |
12 | from gpxplotter import (
13 | add_all_tiles,
14 | add_segment_to_map,
15 | create_folium_map,
16 | read_gpx_file,
17 | )
18 |
19 | the_map = create_folium_map(tiles="kartverket_topo4")
20 | # Add pre-defined tiles:
21 | add_all_tiles(the_map)
22 |
23 | for track in read_gpx_file("example1.gpx"):
24 | for i, segment in enumerate(track["segments"]):
25 | add_segment_to_map(the_map, segment, color_by="hr")
26 |
27 | # Add layer control to change tiles:
28 | folium.LayerControl(sortLayers=True).add_to(the_map)
29 |
30 | # To store the map as a HTML page:
31 | # the_map.save('map_001.html')
32 |
33 | # To display the map in a Jupyter notebook:
34 | the_map
35 |
--------------------------------------------------------------------------------
/docs/source/gallery/maps/plot_002_annotate.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Annotate a map with folium
5 | ==========================
6 |
7 | This example will create a map and color the track according
8 | to the elevation. It will then add two folium markers to show
9 | the location of the highest elevation and the highest heart rate.
10 | """
11 | import folium
12 | import numpy as np
13 |
14 | from gpxplotter import add_segment_to_map, create_folium_map, read_gpx_file
15 |
16 | the_map = create_folium_map(tiles="kartverket_topo4")
17 | for track in read_gpx_file("example1.gpx"):
18 | for i, segment in enumerate(track["segments"]):
19 | # Add track to the map:
20 | add_segment_to_map(the_map, segment, color_by="elevation")
21 |
22 | # This is sufficient to add the segment to the map.
23 | # Here we will add some extra markers using folium:
24 | # 1) Add marker at highest elevation:
25 | idx = np.argmax(segment["elevation"])
26 | value = segment["elevation"][idx]
27 | time = segment["time"][idx].strftime("%A %B %d, %Y, %H:%M:%S")
28 | hrate = segment["heart rate"][idx]
29 | distance = segment["distance"][idx] / 1000.0
30 | txt = (
31 | f"The highest elevation was {value:g} m:"
32 | "
"
33 | f"- Time: {time}"
34 | f"
- Distance: {distance:.2f} km"
35 | f"
- Heart rate: {hrate:g} bpm"
36 | "
"
37 | )
38 | high = folium.Marker(
39 | location=segment["latlon"][idx],
40 | tooltip=f"Highest elevation:{value:g} m",
41 | popup=folium.Popup(txt, max_width=300),
42 | icon=folium.Icon(icon="star", color="green"),
43 | )
44 | high.add_to(the_map)
45 | # 2) Add marker at highest heart rate:
46 | idx = np.argmax(segment["heart rate"])
47 | value = segment["heart rate"][idx]
48 | time = segment["time"][idx].strftime("%A %B %d, %Y, %H:%M:%S")
49 | distance = segment["distance"][idx] / 1000.0
50 | elevation = segment["elevation"][idx]
51 | txt = (
52 | f"The highest heart rate was {value:g} bpm:"
53 | ""
54 | f"- Time: {time}"
55 | f"
- Distance: {distance:.2f} km"
56 | f"
- Elevation: {elevation:.2f} m"
57 | "
"
58 | )
59 | high_hr = folium.Marker(
60 | location=segment["latlon"][idx],
61 | tooltip=f"Highest heart rate:{value:g} bmp",
62 | popup=folium.Popup(txt, max_width=300),
63 | icon=folium.Icon(icon="heart", color="red"),
64 | )
65 | high_hr.add_to(the_map)
66 |
67 |
68 | # To store the map as a HTML page:
69 | # the_map.save('map_002.html')
70 |
71 | # To display the map in a Jupyter notebook:
72 | the_map
73 |
--------------------------------------------------------------------------------
/docs/source/gallery/maps/plot_003_velocity.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Track colored by velocity
5 | ==========================
6 |
7 | This example will create a map and color the track according
8 | to the velocity.
9 |
10 | .. note:: The velocities are calculated from the distance
11 | so it is a bit noisy.
12 | """
13 | from gpxplotter import add_segment_to_map, create_folium_map, read_gpx_file
14 |
15 | line_options = {"weight": 8}
16 |
17 | the_map = create_folium_map(tiles="openstreetmap")
18 | for track in read_gpx_file("example2.gpx"):
19 | for i, segment in enumerate(track["segments"]):
20 | add_segment_to_map(
21 | the_map,
22 | segment,
23 | color_by="velocity-level",
24 | cmap="RdPu_09",
25 | line_options=line_options,
26 | )
27 |
28 | # To store the map as a HTML page:
29 | # the_map.save('map_003.html')
30 |
31 | # To display the map in a Jupyter notebook:
32 | the_map
33 |
--------------------------------------------------------------------------------
/docs/source/gallery/maps/plot_004_zones.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Track colored by heart rate zones
5 | =================================
6 |
7 | This example will create a map and color the track according
8 | to heart rate zones.
9 | """
10 | import folium
11 |
12 | from gpxplotter import (
13 | add_segment_to_map,
14 | add_tiles_to_map,
15 | create_folium_map,
16 | read_gpx_file,
17 | )
18 |
19 | line_options = {"weight": 8}
20 |
21 | the_map = create_folium_map()
22 | add_tiles_to_map(the_map, "kartverket_topo4", "kartverket_topo4graatone")
23 | folium.LayerControl(sortLayers=True).add_to(the_map)
24 |
25 | for track in read_gpx_file("example3.gpx"):
26 | for i, segment in enumerate(track["segments"]):
27 | add_segment_to_map(
28 | the_map,
29 | segment,
30 | color_by="hr-zone-float",
31 | cmap="viridis",
32 | line_options=line_options,
33 | )
34 |
35 | # To store the map as a HTML page:
36 | # the_map.save('map_004.html')
37 |
38 | # To display the map in a Jupyter notebook:
39 | the_map
40 |
--------------------------------------------------------------------------------
/docs/source/gallery/maps/plot_005_images.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Adding images to a map
5 | ======================
6 |
7 | In this example we will add some images to the map, these
8 | are placed in the map by matching time and location information
9 | from the image.
10 | """
11 | import datetime
12 |
13 | import folium
14 | import numpy as np
15 | import PIL
16 | from PIL.ExifTags import GPSTAGS, TAGS
17 |
18 | from gpxplotter import add_segment_to_map, create_folium_map, read_gpx_file
19 |
20 | line_options = {"weight": 8}
21 |
22 | the_map = create_folium_map()
23 | for track in read_gpx_file("example3.gpx"):
24 | for i, segment in enumerate(track["segments"]):
25 | add_segment_to_map(
26 | the_map,
27 | segment,
28 | color_by="hr-zone-float",
29 | cmap="viridis",
30 | line_options=line_options,
31 | )
32 |
33 | # Display initial map:
34 | the_map
35 |
36 |
37 | # Create a method to get coordinates from an image:
38 | def get_lat_lon(imagefile):
39 | image = PIL.Image.open(imagefile)
40 | exif = image.getexif()
41 | exif_info = {TAGS.get(key, key): val for key, val in exif.items()}
42 | # Get the GPSInfo:
43 | for key, value in TAGS.items():
44 | if value == "GPSInfo":
45 | exif_info[value] = exif.get_ifd(key)
46 | break
47 | gps_info = {}
48 | for key, val in exif_info["GPSInfo"].items():
49 | gps_info[GPSTAGS.get(key, key)] = val
50 | # Convert to decimal latitude/longitude:
51 | deg, minutes, seconds = gps_info["GPSLatitude"]
52 | latitude = deg + minutes / 60.0 + seconds / 3600.0
53 | if "GPSLatitudeRef" == "S":
54 | latitude *= -1
55 | deg, minutes, seconds = gps_info["GPSLongitude"]
56 | longitude = deg + minutes / 60.0 + seconds / 3600.0
57 | if "GPSLongitudeRef" == "W":
58 | longitude *= -1
59 | # Turn time into datetime:
60 | time = datetime.datetime.strptime(
61 | exif_info["DateTime"], "%Y:%m:%d %H:%M:%S"
62 | )
63 | return latitude, longitude, time
64 |
65 |
66 | info = {}
67 | for filename in ("image1.jpg", "image2.jpg", "image3.jpg"):
68 | lat, lon, time = get_lat_lon(f"../../_static/{filename}")
69 | info[filename] = {"latlon": (lat, lon), "time": time}
70 |
71 |
72 | # Add, markers to the gps-locations we read from the images
73 | for key, val in info.items():
74 | marker = folium.Marker(
75 | location=val["latlon"],
76 | tooltip=f"You took a picture here? {key} says so...",
77 | icon=folium.Icon(icon="camera", color="red"),
78 | )
79 | marker.add_to(the_map)
80 | boundary = the_map.get_bounds()
81 | the_map.fit_bounds(boundary, padding=(3, 3))
82 |
83 | # To store the map as a HTML page:
84 | # the_map.save('map_005_v1.html')
85 |
86 | # Display updated map:
87 | the_map
88 |
89 | # As can be seen in the map above, the GPS locations in the
90 | # image files may be a bit off.
91 | # Let's try to see if we can use the time information to place them better.
92 | # Note: The best approach is probably to make your GPS devise show its
93 | # current time, and then take a photo of it with your phone. This can
94 | # be used to "align" time-stamps from the two devices better.
95 |
96 | # Time read from the images does not contain time zone information, so it's
97 | # difficult to compare with the timestamps from the GPS device.
98 | # Here, I remember when I started so I will use that.
99 |
100 | time_offset = datetime.timedelta(seconds=2 * 3600) # time_offset is 2 hours
101 | for key, val in info.items():
102 | time = val["time"]
103 | times = [i.replace(tzinfo=None) + time_offset for i in segment["time"]]
104 | time_diff = []
105 | for i in times:
106 | if i < time:
107 | time_diff.append((time - i).seconds)
108 | else:
109 | time_diff.append((i - time).seconds)
110 | minidx = np.argmin(time_diff)
111 | info[key]["latlon_time"] = segment["latlon"][minidx]
112 |
113 | # Mark the gps-locations we interpolated using time:
114 | the_map = create_folium_map(
115 | zoom_start=18, location=info["image1.jpg"]["latlon_time"]
116 | )
117 | add_segment_to_map(
118 | the_map,
119 | segment,
120 | line_options=line_options,
121 | fit_bounds=False,
122 | color_by="hr-zone-float",
123 | )
124 | colors = ["blue", "red", "green"]
125 | for i, (key, val) in enumerate(info.items()):
126 | show = i == 0 # Open the first one.
127 | popup = folium.Popup(
128 | f'
', show=show
129 | )
130 | marker = folium.Marker(
131 | location=val["latlon_time"],
132 | popup=popup,
133 | icon=folium.Icon(icon="camera", color=colors[i]),
134 | )
135 | marker.add_to(the_map)
136 |
137 | # To store the map as a HTML page:
138 | # the_map.save('map_005_v2.html')
139 |
140 | # To display the map in a Jupyter notebook:
141 | the_map
142 |
--------------------------------------------------------------------------------
/docs/source/gallery/maps/plot_006_chart1.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Adding a chart to a map (vincent)
5 | =================================
6 |
7 | In this example we will add the elevation profile as a chart
8 | to the map. The chart is placed inside a popup.
9 | """
10 | import json
11 |
12 | import folium
13 | import numpy as np
14 | import vincent
15 |
16 | from gpxplotter import add_segment_to_map, create_folium_map, read_gpx_file
17 |
18 | line_options = {"weight": 8}
19 |
20 | the_map = create_folium_map(tiles="kartverket_topo4")
21 | for track in read_gpx_file("example3.gpx"):
22 | for i, segment in enumerate(track["segments"]):
23 | add_segment_to_map(
24 | the_map,
25 | segment,
26 | color_by="hr-zone-float",
27 | cmap="viridis",
28 | line_options=line_options,
29 | )
30 |
31 | # Create a chart using vincent
32 | idx = np.argmax(segment["elevation"])
33 |
34 | data = {
35 | "x": segment["Distance / km"],
36 | "y": segment["elevation"],
37 | }
38 |
39 | WIDTH = 400
40 | HEIGHT = 200
41 |
42 |
43 | line = vincent.Line(data, iter_idx="x", width=WIDTH, height=HEIGHT)
44 | line.axis_titles(x="Distance / km", y="Elevation / m")
45 | line.x_axis_properties(title_offset=2)
46 | line.y_axis_properties(title_offset=-10)
47 | line_json = line.to_json()
48 | line_dict = json.loads(line_json)
49 |
50 |
51 | popup = folium.Popup(max_width=WIDTH + 50, show=True)
52 | chart = folium.Vega(line_dict, width=WIDTH + 50, height=HEIGHT + 50)
53 | chart.add_to(popup)
54 |
55 | marker = folium.Marker(
56 | location=segment["latlon"][idx],
57 | popup=popup,
58 | icon=folium.Icon(icon="star"),
59 | )
60 | marker.add_to(the_map)
61 |
62 | # To store the map as a HTML page:
63 | # the_map.save('map_006_chart1.html')
64 |
65 | # To display the map in a Jupyter notebook:
66 | the_map
67 |
--------------------------------------------------------------------------------
/docs/source/gallery/maps/plot_006_chart2.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Adding a chart to a map (altair)
5 | ================================
6 |
7 | In this example we will add the elevation profile as a chart
8 | to the map. The chart is placed inside a popup.
9 | """
10 | import json
11 |
12 | import altair
13 | import folium
14 | import numpy as np
15 | import pandas as pd
16 |
17 | from gpxplotter import add_segment_to_map, create_folium_map, read_gpx_file
18 |
19 | line_options = {"weight": 8}
20 |
21 | the_map = create_folium_map(tiles="kartverket_topo4")
22 | for track in read_gpx_file("example3.gpx"):
23 | for i, segment in enumerate(track["segments"]):
24 | add_segment_to_map(
25 | the_map,
26 | segment,
27 | color_by="hr-zone-float",
28 | cmap="viridis",
29 | line_options=line_options,
30 | )
31 |
32 |
33 | idx = np.argmax(segment["elevation"])
34 |
35 | WIDTH = 400
36 | HEIGHT = 200
37 |
38 |
39 | def smooth(signal, points):
40 | """Smooth the given signal using a rectangular window."""
41 | window = np.ones(points) / points
42 | return np.convolve(signal, window, mode="same")
43 |
44 |
45 | data = pd.DataFrame(
46 | {
47 | "dist": segment["Distance / km"],
48 | "elevation": segment["elevation"],
49 | "heart": smooth(segment["hr"], 51),
50 | }
51 | )
52 |
53 | area1 = (
54 | altair.Chart(data)
55 | .mark_area(fillOpacity=0.4, strokeWidth=5, line=True)
56 | .encode(
57 | x=altair.X("dist", title="Distance / km"),
58 | y=altair.Y("elevation", title="Elevation / m"),
59 | )
60 | )
61 |
62 | line1 = (
63 | altair.Chart(data)
64 | .mark_line(strokeWidth=5)
65 | .encode(
66 | x=altair.X("dist", title="Distance / km"),
67 | y=altair.Y("heart", title="Heart rate / bpm"),
68 | color=altair.value("#1b9e77"),
69 | )
70 | )
71 | chart = altair.layer(
72 | area1,
73 | line1,
74 | width=WIDTH,
75 | height=HEIGHT,
76 | ).resolve_scale(y="independent")
77 |
78 | chart.title = "Elevation & heart rate profile (altair)"
79 |
80 | chart_dict = json.loads(chart.to_json())
81 | popup = folium.Popup(max_width=WIDTH + 100, show=True)
82 | chart_vega = folium.features.VegaLite(
83 | chart_dict, width=WIDTH + 100, height=HEIGHT + 50
84 | )
85 | chart_vega.add_to(popup)
86 | marker = folium.Marker(
87 | location=segment["latlon"][idx],
88 | popup=popup,
89 | icon=folium.Icon(icon="star"),
90 | )
91 | marker.add_to(the_map)
92 |
93 | # To store the map as a HTML page:
94 | # the_map.save('map_006_chart2.html')
95 |
96 | # To display the map in a Jupyter notebook:
97 | the_map
98 |
--------------------------------------------------------------------------------
/docs/source/gallery/maps/plot_007_custom_tiles.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Adding custom tiles to a map
5 | ============================
6 |
7 | In this example we will add custom tiles to a map.
8 | Specifically, we will add tiles from
9 | `Thunderforest `_.
10 |
11 | To use these maps, you will have to sign up at
12 | `Thunderforest `_
13 | and obtain an API key to use for retrieving the
14 | tiles.
15 | """
16 | import folium
17 |
18 | from gpxplotter import add_segment_to_map, create_folium_map, read_gpx_file
19 |
20 | line_options = {"weight": 10}
21 |
22 | API_KEY = ""
23 | the_map = create_folium_map()
24 |
25 | # Make use of the Thunderforest Outdoors map:
26 | tile_settings1 = {
27 | "tiles": (
28 | "https://tile.thunderforest.com/outdoors/{z}/{x}/{y}.png?apikey="
29 | + API_KEY
30 | ),
31 | "name": "Thunderforest Outdoors",
32 | "attr": (
33 | '© Thunderforest,'
34 | ' © '
35 | "OpenStreetMap contributors"
36 | ),
37 | }
38 | # Create a tile layer and add it to the map:
39 | tile_layer1 = folium.TileLayer(**tile_settings1)
40 | the_map.add_child(tile_layer1, name=tile_layer1.tile_name)
41 |
42 | # Make use of the Thunderforest Landscape map:
43 | tile_settings2 = {
44 | "tiles": (
45 | "https://tile.thunderforest.com/landscape/{z}/{x}/{y}.png?apikey="
46 | + API_KEY
47 | ),
48 | "name": "Thunderforest Landscape",
49 | "attr": (
50 | '© Thunderforest,'
51 | ' © '
52 | "OpenStreetMap contributors"
53 | ),
54 | }
55 | # Create a tile layer and add it to the map:
56 | tile_layer2 = folium.TileLayer(**tile_settings2)
57 | the_map.add_child(tile_layer2, name=tile_layer2.tile_name)
58 |
59 | folium.LayerControl(sortLayers=True).add_to(the_map)
60 |
61 | # Add a track:
62 | for track in read_gpx_file("example3.gpx"):
63 | for i, segment in enumerate(track["segments"]):
64 | add_segment_to_map(the_map, segment, line_options=line_options)
65 |
66 | # To store the map as a HTML page:
67 | # the_map.save('map_007.html')
68 |
69 | # To display the map in a Jupyter notebook:
70 | the_map
71 |
--------------------------------------------------------------------------------
/docs/source/gallery/maps/plot_008_overlay.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Adding overlays
5 | ===============
6 |
7 | This example will create a map, add a track and some
8 | overlays that will display the steepness.
9 | """
10 | import folium
11 | from branca.element import MacroElement
12 | from jinja2 import Template
13 |
14 | from gpxplotter import (
15 | add_segment_to_map,
16 | create_folium_map,
17 | read_gpx_file,
18 | )
19 |
20 | line_options = {"weight": 8}
21 |
22 | the_map = create_folium_map(tiles="kartverket_topo4")
23 |
24 | for track in read_gpx_file("example3.gpx"):
25 | for i, segment in enumerate(track["segments"]):
26 | add_segment_to_map(the_map, segment, line_options=line_options)
27 |
28 | steepness = folium.WmsTileLayer(
29 | url="https://nve.geodataonline.no/arcgis/services/Bratthet/MapServer/WmsServer?",
30 | layers="Bratthet_snoskred",
31 | fmt="image/png",
32 | opacity=0.7,
33 | transparent=True,
34 | name="Steepness",
35 | )
36 | steepness.add_to(the_map)
37 |
38 |
39 | # Add some custom code to show a legend:
40 | class Legend(MacroElement):
41 | """Just add a hard-coded legend template."""
42 |
43 | _template = Template(
44 | """
45 | {% macro header(this,kwargs) %}
46 |
71 | {% endmacro %}
72 |
73 | {% macro script(this,kwargs) %}
74 | var legend = L.control({position: 'bottomright'});
75 | legend.onAdd = function (map) {
76 |
77 | var div = L.DomUtil.create('div', 'info legend'),
78 | steep = [27, 30, 35, 40, 45, 50, 90];
79 | colors = ['#13711d', '#fff93e', '#ffa228', '#ff4e11',
80 | '#f50000', '#7c0000'];
81 | labels = ['Degrees'];
82 | for (var i = 0; i < steep.length - 1; i++) {
83 | labels.push(
84 | ' ' +
85 | steep[i] + '–' + steep[i + 1]);
86 | }
87 | div.innerHTML += labels.join('
');
88 | return div;
89 | };
90 | legend.addTo({{ this._parent.get_name() }});
91 | {% endmacro %}
92 | """
93 | )
94 |
95 |
96 | the_map.add_child(Legend())
97 |
98 | # Add layer control:
99 | folium.LayerControl(sortLayers=True).add_to(the_map)
100 |
101 | # To store the map as a HTML page:
102 | # the_map.save('map_008.html')
103 |
104 | # To display the map in a Jupyter notebook:
105 | the_map
106 |
--------------------------------------------------------------------------------
/docs/source/gallery/maps/plot_009_adding_markers.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Adding extra markers
5 | ====================
6 |
7 | This example will create a map, add a track
8 | and display markers every km along the track.
9 | """
10 | import folium
11 | import numpy as np
12 | from folium.plugins import BeautifyIcon
13 |
14 | from gpxplotter import (
15 | add_segment_to_map,
16 | create_folium_map,
17 | read_gpx_file,
18 | )
19 | from gpxplotter.common import cluster_velocities
20 |
21 | line_options1 = {"weight": 13, "color": "#006d2c"}
22 | line_options2 = {
23 | "weight": 8,
24 | }
25 |
26 | the_map = create_folium_map(tiles="kartverket_topo4")
27 |
28 | for track in read_gpx_file("example3.gpx"):
29 | for i, segment in enumerate(track["segments"]):
30 | segment["velocity-level"] = cluster_velocities(
31 | segment["velocity"],
32 | n_clusters=9,
33 | )
34 | add_segment_to_map(
35 | the_map, segment, line_options=line_options1, add_start_end=False
36 | )
37 | add_segment_to_map(
38 | the_map,
39 | segment,
40 | color_by="velocity-level",
41 | cmap="RdYlGn_09",
42 | line_options=line_options2,
43 | add_start_end=False,
44 | )
45 | # Find 1 km locations:
46 | maxd = max(segment["Distance / km"])
47 | locations = np.arange(1, maxd, 1)
48 | location_idx = []
49 | for j in locations:
50 | diff = abs(j - segment["Distance / km"])
51 | idx = np.argmin(diff)
52 | location_idx.append(idx)
53 | for dist, j in zip(locations, location_idx):
54 | icon = BeautifyIcon(
55 | text_color="#262626",
56 | border_color="#006d2c",
57 | background_color="#ffffff",
58 | number=dist,
59 | icon_shape="marker",
60 | border_width=3,
61 | )
62 | marker = folium.Marker(
63 | location=segment["latlon"][j],
64 | icon=icon,
65 | tooltip=f"{dist} km",
66 | popup=f'{segment["time"][j].strftime("%H:%M:%S")}',
67 | )
68 | marker.add_to(the_map)
69 | # Add end marker:
70 | marker = folium.Marker(
71 | location=segment["latlon"][-1],
72 | icon=folium.Icon(icon="home"),
73 | tooltip=f'{segment["Distance / km"][-1]:.2f} km',
74 | popup=f'{segment["time"][i].strftime("%H:%M:%S")}',
75 | )
76 | marker.add_to(the_map)
77 |
78 | # To store the map as a HTML page:
79 | # the_map.save('map_009.html')
80 |
81 | # To display the map in a Jupyter notebook:
82 | the_map
83 |
--------------------------------------------------------------------------------
/docs/source/gallery/maps/plot_010_heat_map.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Heart rate heat map
5 | ===================
6 |
7 | This example will create a map and add a heart rate heat map along
8 | a track.
9 | """
10 | from folium.plugins import HeatMap
11 |
12 | from gpxplotter import create_folium_map, read_gpx_file
13 |
14 | the_map = create_folium_map(tiles="openstreetmap")
15 | for track in read_gpx_file("example4.gpx"):
16 | for i, segment in enumerate(track["segments"]):
17 | data = []
18 | for lat, lon, hr in zip(segment["lat"], segment["lon"], segment["hr"]):
19 | data.append([lat, lon, float(hr)])
20 | HeatMap(data, name="Heart rate", radius=20).add_to(the_map)
21 | boundary = the_map.get_bounds()
22 | the_map.fit_bounds(boundary, padding=(3, 3))
23 | break
24 | # To store the map as a HTML page:
25 | # the_map.save('map_010.html')
26 |
27 | # To display the map in a Jupyter notebook:
28 | the_map
29 |
--------------------------------------------------------------------------------
/docs/source/gallery/maps/plot_011_custom_color_map.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Track colored with a custom color map
5 | =====================================
6 |
7 | This example will create a map and color the track according
8 | to the velocity using a custom color map, instead of a pre-defined one.
9 |
10 | .. note:: The velocities are calculated from the distance
11 | so it is a bit noisy.
12 | """
13 | from branca.colormap import LinearColormap
14 |
15 | from gpxplotter import add_segment_to_map, create_folium_map, read_gpx_file
16 |
17 | line_options = {"weight": 8}
18 |
19 | color_map = LinearColormap(["#00ff00", "#ff0000"])
20 |
21 | the_map = create_folium_map(tiles="openstreetmap")
22 | for track in read_gpx_file("example2.gpx"):
23 | for i, segment in enumerate(track["segments"]):
24 | add_segment_to_map(
25 | the_map,
26 | segment,
27 | color_by="velocity-level",
28 | cmap=color_map,
29 | line_options=line_options,
30 | )
31 |
32 | # To store the map as a HTML page:
33 | # the_map.save('map_011.html')
34 |
35 | # To display the map in a Jupyter notebook:
36 | the_map
37 |
--------------------------------------------------------------------------------
/docs/source/gallery/plots/README.txt:
--------------------------------------------------------------------------------
1 | .. _examples-plots:
2 |
3 | Creating simple plots
4 | ---------------------
5 |
6 | The data from the `gpx` files can also be plotted with, for
7 | instance, `matplotlib `_. Here are some examples:
8 |
--------------------------------------------------------------------------------
/docs/source/gallery/plots/example1.gpx:
--------------------------------------------------------------------------------
1 | ../../_static/example1.gpx
--------------------------------------------------------------------------------
/docs/source/gallery/plots/example3.gpx:
--------------------------------------------------------------------------------
1 | ../../_static/example3.gpx
--------------------------------------------------------------------------------
/docs/source/gallery/plots/example4.gpx:
--------------------------------------------------------------------------------
1 | ../../_static/example4.gpx
--------------------------------------------------------------------------------
/docs/source/gallery/plots/plot_001_simple1.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Simple plot with matplotlib
5 | ===========================
6 |
7 | This example will just plot the distance as a function
8 | of time.
9 | """
10 | import matplotlib.dates as mdates
11 | import seaborn as sns
12 | from matplotlib import pyplot as plt
13 |
14 | from gpxplotter import read_gpx_file
15 |
16 | sns.set_context("notebook")
17 |
18 | for track in read_gpx_file("example1.gpx"):
19 | for i, segment in enumerate(track["segments"]):
20 | fig, ax1 = plt.subplots(constrained_layout=True)
21 | ax1.plot(segment["time"], segment["distance"] / 1000.0, lw=5)
22 | ax1.set(xlabel="Time", ylabel="Distance / km")
23 | ax1.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M:%S"))
24 | sns.despine(fig=fig)
25 | plt.show()
26 |
--------------------------------------------------------------------------------
/docs/source/gallery/plots/plot_001_simple2.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Simple plot with matplotlib (2)
5 | ===============================
6 |
7 | This example will combine some information into a single
8 | plot with matplotlib.
9 | """
10 | import numpy as np
11 | import seaborn as sns
12 | from matplotlib import pyplot as plt
13 |
14 | from gpxplotter import read_gpx_file
15 |
16 | sns.set_context("notebook")
17 |
18 |
19 | def smooth(signal, points):
20 | """Smooth the given signal using a rectangular window."""
21 | window = np.ones(points) / points
22 | return np.convolve(signal, window, mode="same")
23 |
24 |
25 | colors = sns.color_palette()
26 |
27 |
28 | for track in read_gpx_file("example3.gpx"):
29 | for i, segment in enumerate(track["segments"]):
30 | fig, ax1 = plt.subplots(constrained_layout=True, figsize=(8, 4))
31 | x = segment["Distance / km"]
32 | y = segment["elevation"]
33 | (line1,) = ax1.plot(x, y, lw=3, color=colors[0])
34 | # Fill the area:
35 | ax1.fill_between(x, y, y2=y.min(), alpha=0.1)
36 | ax1.set(xlabel="Distance / km", ylabel="Elevation")
37 | # Add heart rate:
38 | ax2 = ax1.twinx()
39 | # Smooth the heart rate for plotting:
40 | heart = smooth(segment["hr"], 51)
41 | (line2,) = ax2.plot(x, heart, color=colors[1], alpha=0.8, lw=3)
42 | ax2.set_ylim(0, 200)
43 | ax2.set(ylabel="Heart rate / bpm")
44 | # Add velocity:
45 | ax3 = ax1.twinx()
46 | ax3.spines["right"].set_position(("axes", 1.2))
47 | # Smooth the velocity for plotting:
48 | vel = 3.6 * smooth(segment["velocity"], 51)
49 | (line3,) = ax3.plot(x, vel, alpha=0.8, color=colors[2], lw=3)
50 | ax3.set(ylabel="Velocity / km/h")
51 | ax3.set_ylim(0, 20)
52 |
53 | # Style plot:
54 | axes = (ax1, ax2, ax3)
55 | lines = (line1, line2, line3)
56 | for axi, linei in zip(axes, lines):
57 | axi.yaxis.label.set_color(linei.get_color())
58 | axi.tick_params(axis="y", colors=linei.get_color())
59 | key = "right" if axi != ax1 else "left"
60 | axi.spines[key].set_edgecolor(linei.get_color())
61 | axi.spines[key].set_linewidth(2)
62 |
63 | ax1.spines["top"].set_visible(False)
64 |
65 | for axi in (ax2, ax3):
66 | for key in axi.spines:
67 | axi.spines[key].set_visible(False)
68 | axi.spines["right"].set_visible(True)
69 |
70 | # Add legend:
71 | ax1.legend(
72 | (line1, line2, line3),
73 | ("Elevation", "Heart rate", "Velocity"),
74 | loc="upper left",
75 | frameon=True,
76 | )
77 | plt.show()
78 |
--------------------------------------------------------------------------------
/docs/source/gallery/plots/plot_002_elevation.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Elevation profiles - line plots
5 | ===============================
6 |
7 | This example will use the plotting methods of
8 | gpxplotter to plot the elevation as a
9 | function of distance and elapsed time.
10 | """
11 | import seaborn as sns
12 | from matplotlib import pyplot as plt
13 |
14 | from gpxplotter import plot_line, read_gpx_file
15 |
16 | sns.set_context("notebook")
17 |
18 | for track in read_gpx_file("example1.gpx"):
19 | for i, segment in enumerate(track["segments"]):
20 | # Plot elevation as function of distance:
21 | plot_line(track, segment, xvar="Distance / km", yvar="elevation")
22 | # Plot elevation as function of elapsed time:
23 | plot_line(track, segment, xvar="elapsed-time", yvar="elevation")
24 | # Repeat plots, but color by heart rate:
25 | plot_line(
26 | track,
27 | segment,
28 | xvar="Distance / km",
29 | yvar="elevation",
30 | zvar="hr",
31 | lw=10,
32 | )
33 | plot_line(
34 | track,
35 | segment,
36 | xvar="elapsed-time",
37 | yvar="elevation",
38 | zvar="hr",
39 | lw=10,
40 | )
41 | plt.show()
42 |
--------------------------------------------------------------------------------
/docs/source/gallery/plots/plot_003_elevation.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Elevation profiles - filled plots
5 | =================================
6 |
7 | This example will use the plotting methods of gpxplotter
8 | to plot the elevation as a
9 | function of distance and elapsed time.
10 | """
11 | import seaborn as sns
12 | from matplotlib import pyplot as plt
13 |
14 | from gpxplotter import plot_filled, read_gpx_file
15 |
16 | sns.set_context("notebook")
17 |
18 | for track in read_gpx_file("example1.gpx"):
19 | for i, segment in enumerate(track["segments"]):
20 | # Plot elevation as function of distance:
21 | fig1, _ = plot_filled(
22 | track, segment, xvar="Distance / km", yvar="elevation", zvar="hr"
23 | )
24 | sns.despine(fig=fig1)
25 | # Plot elevation as function of elapsed time:
26 | fig2, _ = plot_filled(
27 | track,
28 | segment,
29 | xvar="elapsed-time",
30 | yvar="elevation",
31 | zvar="hr",
32 | cmap="vlag",
33 | )
34 | sns.despine(fig=fig2)
35 | plt.show()
36 |
--------------------------------------------------------------------------------
/docs/source/gallery/plots/plot_004_velocity.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Adding velocity
5 | ===============
6 |
7 | This example will plot the elevation as a
8 | function of distance and color the plot
9 | according to the velocity.
10 |
11 | .. note:: The velocities are calculated from the distance
12 | so it is a bit noisy.
13 | """
14 | import seaborn as sns
15 | from matplotlib import pyplot as plt
16 |
17 | from gpxplotter import plot_filled, read_gpx_file
18 |
19 | sns.set_context("notebook")
20 |
21 | for track in read_gpx_file("example1.gpx"):
22 | for i, segment in enumerate(track["segments"]):
23 | # Plot elevation as function of distance:
24 | fig, _ = plot_filled(
25 | track,
26 | segment,
27 | xvar="Distance / km",
28 | yvar="elevation",
29 | zvar="Velocity / km/h",
30 | )
31 | sns.despine(fig=fig)
32 | plt.show()
33 |
--------------------------------------------------------------------------------
/docs/source/gallery/plots/plot_005_velocity.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Inspecting velocities
5 | =====================
6 |
7 | This example will inspect the calculated velocities.
8 |
9 | .. note:: The velocities are calculated from the distance
10 | so it is a bit noisy.
11 | """
12 | import matplotlib.dates as mdates
13 | import seaborn as sns
14 | from matplotlib import pyplot as plt
15 |
16 | from gpxplotter import plot_filled, read_gpx_file
17 | from gpxplotter.common import cluster_velocities
18 |
19 | sns.set_context("notebook")
20 |
21 | for track in read_gpx_file("example1.gpx"):
22 | for i, segment in enumerate(track["segments"]):
23 | fig, (axi, axj) = plt.subplots(
24 | constrained_layout=True, ncols=2, figsize=(10, 5)
25 | )
26 | # First draw a histogram:
27 | sns.histplot(
28 | x=segment["velocity"],
29 | ax=axi,
30 | stat="density",
31 | kde=True,
32 | line_kws={"label": "kde", "lw": 3},
33 | )
34 | axi.set_title("Histogram of velocities", loc="left")
35 | axi.set(xlabel="Velocity / m/s", ylabel="Frequency")
36 | axi.legend(frameon=False)
37 | # Add clustering of velocities for grouping them into levels:
38 | level = cluster_velocities(segment["velocity"], n_clusters=6)
39 | scatter = axj.scatter(
40 | segment["time"], segment["velocity"], c=level, cmap="viridis"
41 | )
42 | axj.legend(
43 | *scatter.legend_elements(num=len(set(level))),
44 | title="Velocity level",
45 | )
46 | axj.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M:%S"))
47 | axj.tick_params(axis="x", rotation=25)
48 | axj.set(xlabel="Time", ylabel="Velocity")
49 | sns.despine(fig=fig)
50 | # The velocity levels is also calculated when loading the gpx-file:
51 | fig2, _ = plot_filled(
52 | track,
53 | segment,
54 | xvar="Distance / km",
55 | yvar="elevation",
56 | zvar="velocity-level",
57 | )
58 | sns.despine(fig=fig2)
59 | # The number of levels can be changed by updating the
60 | # velocity-level:
61 | levels = cluster_velocities(segment["velocity"], n_clusters=6)
62 | if levels is not None:
63 | segment["velocity-level"] = levels
64 | fig3, _ = plot_filled(
65 | track,
66 | segment,
67 | xvar="Distance / km",
68 | yvar="elevation",
69 | zvar="velocity-level",
70 | cmap="viridis",
71 | color="k",
72 | )
73 | sns.despine(fig=fig3)
74 | plt.show()
75 |
--------------------------------------------------------------------------------
/docs/source/gallery/plots/plot_006_heart_rate.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Pie chart of heart rate zones
5 | =============================
6 |
7 | This example will use the calculated heart rate zones from
8 | gpxplotter to show the fraction of time spent in the different
9 | zones.
10 | """
11 | import numpy as np
12 | import seaborn as sns
13 | from matplotlib import pyplot as plt
14 |
15 | from gpxplotter import read_gpx_file
16 | from gpxplotter.common import format_time_delta
17 |
18 | sns.set_context("notebook")
19 |
20 |
21 | MAX_HEART_RATE = 189
22 |
23 |
24 | for track in read_gpx_file("example4.gpx", max_heart_rate=MAX_HEART_RATE):
25 | for i, segment in enumerate(track["segments"]):
26 | time = segment["time"]
27 | time_in_zones = {}
28 | for start, stop, value in segment["hr-regions"]:
29 | seconds = (time[stop] - time[start]).seconds
30 | if value not in time_in_zones:
31 | time_in_zones[value] = 0
32 | time_in_zones[value] += seconds
33 | sum_time = sum([val for _, val in time_in_zones.items()])
34 | # Check consistency:
35 | print("Times are equal?", sum_time == (time[-1] - time[0]).seconds)
36 |
37 | zones = sorted(list(time_in_zones.keys()))
38 | zone_txt = segment["zone_txt"]
39 |
40 | percent = {
41 | key: 100 * val / sum_time for key, val in time_in_zones.items()
42 | }
43 | values = [time_in_zones[j] for j in zones]
44 | times = format_time_delta(values)
45 | labels = []
46 | for j in zones:
47 | labels.append(
48 | f"Zone {j} ({zone_txt[j]})\n"
49 | f"({times[j][3:]}, {percent[j]:.1f}%)"
50 | )
51 | cmap = plt.colormaps.get_cmap("Reds")
52 | colors = cmap(np.linspace(0, 1, len(zones) + 1))
53 | colors = colors[1:] # Skip the first color
54 | fig, ax1 = plt.subplots(constrained_layout=True)
55 | patches, _ = ax1.pie(
56 | values,
57 | colors=colors,
58 | # labels=labels, # Labels may overlap, so this is commented here
59 | # textprops={'fontsize': 'x-large', 'ha': 'center'},
60 | labeldistance=1.15,
61 | wedgeprops={"width": 0.45, "linewidth": 3, "edgecolor": "w"},
62 | normalize=True,
63 | startangle=90,
64 | counterclock=False,
65 | )
66 | ax1.set(aspect="equal")
67 |
68 | legend = ax1.legend(
69 | patches,
70 | labels,
71 | loc="upper left",
72 | bbox_to_anchor=(-0.25, 1.0),
73 | handlelength=3,
74 | ) # , fontsize='x-large')
75 | # Make patches thicker for the legend:
76 | for patch in legend.get_patches():
77 | patch.set_height(20)
78 | ax1.text(
79 | 0,
80 | 0,
81 | "Time in\nheart rate zones",
82 | fontdict={"fontsize": "large", "ha": "center", "va": "center"},
83 | )
84 | plt.show()
85 |
--------------------------------------------------------------------------------
/docs/source/gallery/plots/plot_006_heart_rate_bar.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | Bar chart of heart rate zones
5 | =============================
6 |
7 | This example will use the calculated heart rate zones from
8 | gpxplotter to show the fraction of time spent in the different
9 | zones.
10 | """
11 | import numpy as np
12 | import seaborn as sns
13 | from matplotlib import pyplot as plt
14 |
15 | from gpxplotter import read_gpx_file
16 | from gpxplotter.common import format_time_delta
17 |
18 | sns.set_context("notebook")
19 |
20 |
21 | MAX_HEART_RATE = 189
22 |
23 |
24 | for track in read_gpx_file("example4.gpx", max_heart_rate=MAX_HEART_RATE):
25 | for j, segment in enumerate(track["segments"]):
26 | time = segment["time"]
27 | time_in_zones = {}
28 | for start, stop, value in segment["hr-regions"]:
29 | seconds = (time[stop] - time[start]).seconds
30 | if value not in time_in_zones:
31 | time_in_zones[value] = 0
32 | time_in_zones[value] += seconds
33 | sum_time = sum([val for _, val in time_in_zones.items()])
34 | # Check consistency:
35 | print("Times are equal?", sum_time == (time[-1] - time[0]).seconds)
36 |
37 | zone_txt = segment["zone_txt"]
38 |
39 | zones = sorted(list(time_in_zones.keys()))
40 | percent = {
41 | key: 100 * val / sum_time for key, val in time_in_zones.items()
42 | }
43 | labels = [
44 | f"Zone {i} ({zone_txt[i]})\n({percent[i]:.1f}%)" for i in zones
45 | ]
46 | values = [time_in_zones[i] for i in zones]
47 | times = format_time_delta(values)
48 | cmap = plt.colormaps.get_cmap("Reds")
49 | colors = cmap(np.linspace(0, 1, len(zones) + 1))
50 | colors = colors[1:] # Skip the first color
51 | fig, ax1 = plt.subplots(constrained_layout=True)
52 | rects = ax1.barh(zones, values, align="center", tick_label=labels)
53 | for i, recti in enumerate(rects):
54 | recti.set_facecolor(colors[i])
55 | width = int(recti.get_width())
56 | yloc = recti.get_y() + recti.get_height() / 2
57 | ax1.annotate(
58 | times[i],
59 | xy=(width, yloc),
60 | xytext=(3, 0),
61 | textcoords="offset points",
62 | ha="left",
63 | va="center",
64 | )
65 | ax1.spines["top"].set_visible(False)
66 | ax1.spines["right"].set_visible(False)
67 | ax1.spines["bottom"].set_visible(False)
68 | ax1.tick_params(bottom=False)
69 | ax1.tick_params(labelbottom=False)
70 | plt.show()
71 |
--------------------------------------------------------------------------------
/docs/source/gallery_thumbs/make_snap.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """Read a generated map as HTML and take a screenshot."""
4 | import io
5 | import os
6 | import pathlib
7 | import runpy
8 | import sys
9 | import time
10 | import tempfile
11 | from selenium import webdriver
12 | from selenium.webdriver.firefox.service import (
13 | Service as FirefoxService,
14 | )
15 | from webdriver_manager.firefox import GeckoDriverManager
16 | from PIL import Image
17 |
18 |
19 | def convert(html, thumb, pngfile=None):
20 | """Convert HTML to a PNG image."""
21 | dirname = pathlib.Path(__file__).parent
22 | with tempfile.NamedTemporaryFile(
23 | mode="w", dir=dirname
24 | ) as temp, tempfile.TemporaryDirectory(
25 | suffix=".profile", dir=dirname
26 | ) as profile:
27 | temp.write(html)
28 | temp.flush()
29 | url = f"file://{temp.name}"
30 |
31 | options = webdriver.FirefoxOptions()
32 | options.add_argument("--headless")
33 | options.add_argument("--profile")
34 | options.add_argument(f"{profile}")
35 |
36 | driver = webdriver.Firefox(
37 | service=FirefoxService(GeckoDriverManager().install()),
38 | options=options,
39 | )
40 | print(f"Loading: {url}")
41 | driver.get(url)
42 | print("\t Move/resize...")
43 | driver.set_window_position(0, 0)
44 | driver.set_window_size(400 * 3, 280 * 3)
45 | print("\t Sleep...")
46 | time.sleep(5)
47 | print("\t Store png...")
48 | result = driver.get_screenshot_as_png()
49 | image = Image.open(io.BytesIO(result))
50 | thumbnail = image.resize((400, 280), Image.Resampling.LANCZOS)
51 | with open(thumb, "wb") as output:
52 | thumbnail.save(output, format="PNG")
53 | if pngfile is not None:
54 | driver.save_screenshot(pngfile)
55 | driver.quit()
56 |
57 |
58 | def main(pyfiles):
59 | """Read the given input files and generate pngs."""
60 | for pyfile in pyfiles:
61 | path = pathlib.Path(pyfile).resolve()
62 | cwd = os.getcwd()
63 | try:
64 | print(f"{path.stem}")
65 | os.chdir(path.parent)
66 | mod = runpy.run_path(path)
67 | html = mod["the_map"]._repr_html_()
68 | convert(html, f"sphx_glr_{path.stem}_thumb.png")
69 | finally:
70 | os.chdir(cwd)
71 |
72 |
73 | if __name__ == "__main__":
74 | main(sys.argv[1:])
75 |
--------------------------------------------------------------------------------
/docs/source/gallery_thumbs/sphx_glr_plot_000_segment_thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/gallery_thumbs/sphx_glr_plot_000_segment_thumb.png
--------------------------------------------------------------------------------
/docs/source/gallery_thumbs/sphx_glr_plot_001_heart_rate_thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/gallery_thumbs/sphx_glr_plot_001_heart_rate_thumb.png
--------------------------------------------------------------------------------
/docs/source/gallery_thumbs/sphx_glr_plot_002_annotate_thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/gallery_thumbs/sphx_glr_plot_002_annotate_thumb.png
--------------------------------------------------------------------------------
/docs/source/gallery_thumbs/sphx_glr_plot_003_velocity_thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/gallery_thumbs/sphx_glr_plot_003_velocity_thumb.png
--------------------------------------------------------------------------------
/docs/source/gallery_thumbs/sphx_glr_plot_004_zones_thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/gallery_thumbs/sphx_glr_plot_004_zones_thumb.png
--------------------------------------------------------------------------------
/docs/source/gallery_thumbs/sphx_glr_plot_005_images_thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/gallery_thumbs/sphx_glr_plot_005_images_thumb.png
--------------------------------------------------------------------------------
/docs/source/gallery_thumbs/sphx_glr_plot_006_chart1_thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/gallery_thumbs/sphx_glr_plot_006_chart1_thumb.png
--------------------------------------------------------------------------------
/docs/source/gallery_thumbs/sphx_glr_plot_006_chart2_thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/gallery_thumbs/sphx_glr_plot_006_chart2_thumb.png
--------------------------------------------------------------------------------
/docs/source/gallery_thumbs/sphx_glr_plot_007_custom_tiles_thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/gallery_thumbs/sphx_glr_plot_007_custom_tiles_thumb.png
--------------------------------------------------------------------------------
/docs/source/gallery_thumbs/sphx_glr_plot_008_overlay_thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/gallery_thumbs/sphx_glr_plot_008_overlay_thumb.png
--------------------------------------------------------------------------------
/docs/source/gallery_thumbs/sphx_glr_plot_009_adding_markers_thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/gallery_thumbs/sphx_glr_plot_009_adding_markers_thumb.png
--------------------------------------------------------------------------------
/docs/source/gallery_thumbs/sphx_glr_plot_010_heat_map_thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/gallery_thumbs/sphx_glr_plot_010_heat_map_thumb.png
--------------------------------------------------------------------------------
/docs/source/gallery_thumbs/sphx_glr_plot_011_custom_color_map_thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/docs/source/gallery_thumbs/sphx_glr_plot_011_custom_color_map_thumb.png
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | ========================================
2 | Welcome to |gpxplotter|'s documentation!
3 | ========================================
4 |
5 | |gpxplotter| is a small package for reading `.gpx` files and
6 | generating plots and maps using `matplotlib `_
7 | and `folium `_.
8 | The source code for |gpxplotter| can be found
9 | at Github: https://github.com/andersle/gpxplotter.
10 |
11 | Here is
12 | a short example of the usage of |gpxplotter|
13 | (please see :ref:`examples-maps` and :ref:`examples-plots` for more examples):
14 |
15 |
16 | .. literalinclude:: gallery/maps/plot_001_heart_rate.py
17 | :lines: 3-
18 |
19 | .. raw:: html
20 |
21 |
22 |
23 |
24 |
25 | Installing |gpxplotter|
26 | =======================
27 |
28 | |gpxplotter| can be installed via `pip `_:
29 |
30 | .. code-block:: bash
31 |
32 | pip install gpxplotter
33 |
34 |
35 | .. toctree::
36 | :maxdepth: 2
37 | :caption: Documentation:
38 |
39 | auto_examples_maps/index
40 | auto_examples_plots/index
41 | source/gpxplotter
42 |
--------------------------------------------------------------------------------
/docs/source/source/gpxplotter.common.rst:
--------------------------------------------------------------------------------
1 | .. _api-common:
2 |
3 | gpxplotter.common
4 | -----------------
5 |
6 | .. automodule:: gpxplotter.common
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/source/gpxplotter.folium_map.rst:
--------------------------------------------------------------------------------
1 | .. _api-folium:
2 |
3 | gpxplotter.folium\_map
4 | ----------------------
5 |
6 | .. automodule:: gpxplotter.folium_map
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/source/gpxplotter.gpxread.rst:
--------------------------------------------------------------------------------
1 | .. _api-gpxread:
2 |
3 | gpxplotter.gpxread
4 | ------------------
5 |
6 | .. automodule:: gpxplotter.gpxread
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/source/gpxplotter.mplplotting.rst:
--------------------------------------------------------------------------------
1 | .. _api-mplplotting:
2 |
3 | gpxplotter.mplplotting
4 | ----------------------
5 |
6 | .. automodule:: gpxplotter.mplplotting
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/source/gpxplotter.rst:
--------------------------------------------------------------------------------
1 | .. _gpxplotter-doc:
2 |
3 | The |gpxplotter| package
4 | ========================
5 |
6 | This is the documentation for the source code of
7 | the |gpxplotter| library, version |version|.
8 |
9 | |gpxplotter| modules
10 | --------------------
11 |
12 | .. toctree::
13 | :hidden:
14 | :maxdepth: 2
15 |
16 | gpxplotter.common
17 | gpxplotter.folium_map
18 | gpxplotter.gpxread
19 | gpxplotter.mplplotting
20 | gpxplotter.version
21 |
22 | The |gpxplotter| package is structured into modules as follow:
23 |
24 | * :ref:`gpxplotter.common ` with common methods
25 | for the plotting and gpx-reading methods.
26 |
27 | * :ref:`gpxplotter.folium\_map ` with methods
28 | for generating HTML maps
29 | with `folium `_.
30 |
31 | * :ref:`gpxplotter.gpxread ` with method for
32 | reading information from gpx-files.
33 |
34 | * :ref:`gpxplotter.mplplotting ` with methods
35 | for generating pre-defined plots
36 | with `matplotlib `_.
37 |
38 | * :ref:`gpxplotter.version ` with version
39 | information about |gpxplotter|.
40 |
--------------------------------------------------------------------------------
/docs/source/source/gpxplotter.version.rst:
--------------------------------------------------------------------------------
1 | .. _api-version:
2 |
3 | gpxplotter.version
4 | ------------------
5 |
6 | .. automodule:: gpxplotter.version
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/examples/images/map001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/examples/images/map001.png
--------------------------------------------------------------------------------
/examples/images/plot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/examples/images/plot1.png
--------------------------------------------------------------------------------
/examples/jupyter/examples.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# gpxplotter examples\n",
8 | "This notebook contains a few examples, showing the basic usage of gpxplotter."
9 | ]
10 | },
11 | {
12 | "cell_type": "code",
13 | "execution_count": null,
14 | "metadata": {},
15 | "outputs": [],
16 | "source": [
17 | "from matplotlib import pyplot as plt\n",
18 | "import seaborn as sns\n",
19 | "from gpxplotter import read_gpx_file, create_folium_map, add_segment_to_map\n",
20 | "from gpxplotter import plot_filled, plot_line\n",
21 | "\n",
22 | "\n",
23 | "sns.set_context(\"notebook\")"
24 | ]
25 | },
26 | {
27 | "cell_type": "code",
28 | "execution_count": null,
29 | "metadata": {},
30 | "outputs": [],
31 | "source": [
32 | "for track in read_gpx_file(\"example1.gpx\"):\n",
33 | " for i, segment in enumerate(track[\"segments\"]):\n",
34 | " # Plot elevation as function of distance, color by heart rate:\n",
35 | " fig, ax = plot_filled(\n",
36 | " track, segment, xvar=\"Distance / km\", yvar=\"elevation\", zvar=\"hr\"\n",
37 | " )\n",
38 | " sns.despine(fig=fig)\n",
39 | " break # Stop after the first segment"
40 | ]
41 | },
42 | {
43 | "cell_type": "code",
44 | "execution_count": null,
45 | "metadata": {},
46 | "outputs": [],
47 | "source": [
48 | "# Print out available keys for plotting\n",
49 | "print(segment.keys())\n",
50 | "print(f\"Mean heart rate: {segment['average-hr']}\")\n",
51 | "print(f\"Elevation gain: {segment['elevation-up']}\")\n",
52 | "print(f\"Elevation drop: {segment['elevation-down']}\")"
53 | ]
54 | },
55 | {
56 | "cell_type": "code",
57 | "execution_count": null,
58 | "metadata": {},
59 | "outputs": [],
60 | "source": [
61 | "# Plot elevation as function of time, color by heart rate zone:\n",
62 | "fig, ax = plot_filled(\n",
63 | " track,\n",
64 | " segment,\n",
65 | " xvar=\"elapsed-time\",\n",
66 | " yvar=\"elevation\",\n",
67 | " zvar=\"hr-zone-float\",\n",
68 | " color=\"k\",\n",
69 | ")\n",
70 | "sns.despine(fig=fig)"
71 | ]
72 | },
73 | {
74 | "cell_type": "code",
75 | "execution_count": null,
76 | "metadata": {},
77 | "outputs": [],
78 | "source": [
79 | "# Plot elevation as function of time, color by velocity\n",
80 | "fig, ax = plot_line(\n",
81 | " track,\n",
82 | " segment,\n",
83 | " xvar=\"time\",\n",
84 | " yvar=\"elevation\",\n",
85 | " zvar=\"velocity-level\",\n",
86 | " lw=5,\n",
87 | " cmap=\"vlag\",\n",
88 | ")\n",
89 | "sns.despine(fig=fig)"
90 | ]
91 | },
92 | {
93 | "cell_type": "code",
94 | "execution_count": null,
95 | "metadata": {},
96 | "outputs": [],
97 | "source": [
98 | "# Create a map, color the line by the heart rate:\n",
99 | "map1 = create_folium_map()\n",
100 | "add_segment_to_map(map1, segment, color_by=\"hr\")\n",
101 | "map1"
102 | ]
103 | },
104 | {
105 | "cell_type": "code",
106 | "execution_count": null,
107 | "metadata": {},
108 | "outputs": [],
109 | "source": [
110 | "# Just add a track:\n",
111 | "line_options = {\"color\": \"red\", \"weight\": 8, \"opacity\": 0.6}\n",
112 | "map2 = create_folium_map()\n",
113 | "add_segment_to_map(map2, segment, line_options=line_options)\n",
114 | "map2"
115 | ]
116 | },
117 | {
118 | "cell_type": "code",
119 | "execution_count": null,
120 | "metadata": {},
121 | "outputs": [],
122 | "source": [
123 | "# Add some folium markers to the map:\n",
124 | "import folium\n",
125 | "import numpy as np\n",
126 | "\n",
127 | "map3 = create_folium_map()\n",
128 | "# Add segment, color by elevation:\n",
129 | "add_segment_to_map(map3, segment, color_by=\"elevation\")\n",
130 | "# Add marker at the highest elevation:\n",
131 | "idx = np.argmax(segment[\"elevation\"])\n",
132 | "high = folium.Marker(\n",
133 | " location=segment[\"latlon\"][idx],\n",
134 | " tooltip=\"Highest elevation\",\n",
135 | " popup=folium.Popup(f'{segment[\"elevation\"][idx]:g} m', max_width=250),\n",
136 | " icon=folium.Icon(icon=\"star\", color=\"blue\"),\n",
137 | ")\n",
138 | "high.add_to(map3)\n",
139 | "# Add marker at highest heart rate:\n",
140 | "idx = np.argmax(segment[\"heart rate\"])\n",
141 | "high_hr = folium.Marker(\n",
142 | " location=segment[\"latlon\"][idx],\n",
143 | " tooltip=\"Highest heart rate\",\n",
144 | " popup=folium.Popup(f'{segment[\"heart rate\"][idx]:g} bpm', max_width=250),\n",
145 | " icon=folium.Icon(icon=\"heart\", color=\"red\"),\n",
146 | ")\n",
147 | "high_hr.add_to(map3)\n",
148 | "\n",
149 | "map3"
150 | ]
151 | },
152 | {
153 | "cell_type": "code",
154 | "execution_count": null,
155 | "metadata": {},
156 | "outputs": [],
157 | "source": [
158 | "map4 = create_folium_map()\n",
159 | "add_segment_to_map(map4, segment, color_by=\"Velocity / km/h\")\n",
160 | "map4"
161 | ]
162 | },
163 | {
164 | "cell_type": "code",
165 | "execution_count": null,
166 | "metadata": {},
167 | "outputs": [],
168 | "source": [
169 | "map5 = create_folium_map()\n",
170 | "add_segment_to_map(map5, segment, color_by=\"velocity-level\")\n",
171 | "map5"
172 | ]
173 | }
174 | ],
175 | "metadata": {
176 | "kernelspec": {
177 | "display_name": "Python 3 (ipykernel)",
178 | "language": "python",
179 | "name": "python3"
180 | },
181 | "language_info": {
182 | "codemirror_mode": {
183 | "name": "ipython",
184 | "version": 3
185 | },
186 | "file_extension": ".py",
187 | "mimetype": "text/x-python",
188 | "name": "python",
189 | "nbconvert_exporter": "python",
190 | "pygments_lexer": "ipython3",
191 | "version": "3.10.6"
192 | }
193 | },
194 | "nbformat": 4,
195 | "nbformat_minor": 4
196 | }
197 |
--------------------------------------------------------------------------------
/examples/jupyter/examples_chart.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Displaying charts\n",
8 | "This notebook will add a elevation chart as a popup."
9 | ]
10 | },
11 | {
12 | "cell_type": "code",
13 | "execution_count": null,
14 | "metadata": {},
15 | "outputs": [],
16 | "source": [
17 | "from gpxplotter import create_folium_map, read_gpx_file, add_segment_to_map\n",
18 | "import folium\n",
19 | "\n",
20 | "line_options = {\"weight\": 8}\n",
21 | "\n",
22 | "the_map = create_folium_map(tiles=\"openstreetmap\")\n",
23 | "for track in read_gpx_file(\"example3.gpx\"):\n",
24 | " for i, segment in enumerate(track[\"segments\"]):\n",
25 | " add_segment_to_map(\n",
26 | " the_map,\n",
27 | " segment,\n",
28 | " color_by=\"hr-zone-float\",\n",
29 | " cmap=\"viridis\",\n",
30 | " line_options=line_options,\n",
31 | " )\n",
32 | "\n",
33 | "# To display the map in a Jupyter notebook:\n",
34 | "the_map"
35 | ]
36 | },
37 | {
38 | "cell_type": "code",
39 | "execution_count": null,
40 | "metadata": {},
41 | "outputs": [],
42 | "source": [
43 | "# Create a chart:\n",
44 | "import vincent\n",
45 | "import json\n",
46 | "import numpy as np\n",
47 | "\n",
48 | "idx = np.argmax(segment[\"elevation\"])\n",
49 | "\n",
50 | "data = {\n",
51 | " \"x\": segment[\"Distance / km\"],\n",
52 | " \"y\": segment[\"elevation\"],\n",
53 | "}\n",
54 | "\n",
55 | "WIDTH = 400\n",
56 | "HEIGHT = 200\n",
57 | "\n",
58 | "\n",
59 | "line = vincent.Line(data, iter_idx=\"x\", width=WIDTH, height=HEIGHT)\n",
60 | "line.axis_titles(x=\"Distance / km\", y=\"Elevation / m\")\n",
61 | "line.x_axis_properties(title_offset=2)\n",
62 | "line.y_axis_properties(title_offset=-10)\n",
63 | "line_json = line.to_json()\n",
64 | "line_dict = json.loads(line_json)\n",
65 | "\n",
66 | "\n",
67 | "popup = folium.Popup(max_width=WIDTH + 50, show=True)\n",
68 | "chart = folium.Vega(line_dict, width=WIDTH + 50, height=HEIGHT + 50)\n",
69 | "chart.add_to(popup)\n",
70 | "\n",
71 | "marker = folium.Marker(\n",
72 | " location=segment[\"latlon\"][idx],\n",
73 | " popup=popup,\n",
74 | " icon=folium.Icon(icon=\"star\"),\n",
75 | ")\n",
76 | "marker.add_to(the_map)\n",
77 | "the_map"
78 | ]
79 | },
80 | {
81 | "cell_type": "code",
82 | "execution_count": null,
83 | "metadata": {},
84 | "outputs": [],
85 | "source": [
86 | "# Create a chart using altair:\n",
87 | "import altair as alt\n",
88 | "import pandas as pd"
89 | ]
90 | },
91 | {
92 | "cell_type": "code",
93 | "execution_count": null,
94 | "metadata": {},
95 | "outputs": [],
96 | "source": [
97 | "def smooth(signal, points):\n",
98 | " \"\"\"Smooth the given signal using a rectangular window.\"\"\"\n",
99 | " window = np.ones(points) / points\n",
100 | " return np.convolve(signal, window, mode=\"same\")\n",
101 | "\n",
102 | "\n",
103 | "data2 = pd.DataFrame(\n",
104 | " {\n",
105 | " \"dist\": segment[\"Distance / km\"],\n",
106 | " \"elevation\": segment[\"elevation\"],\n",
107 | " \"heart\": smooth(segment[\"hr\"], 51),\n",
108 | " }\n",
109 | ")"
110 | ]
111 | },
112 | {
113 | "cell_type": "code",
114 | "execution_count": null,
115 | "metadata": {},
116 | "outputs": [],
117 | "source": [
118 | "area1 = (\n",
119 | " alt.Chart(data2)\n",
120 | " .mark_area(fillOpacity=0.4, strokeWidth=5, line=True)\n",
121 | " .encode(\n",
122 | " x=alt.X(\"dist\", title=\"Distance / km\"),\n",
123 | " y=alt.Y(\"elevation\", title=\"Elevation / m\"),\n",
124 | " )\n",
125 | ")\n",
126 | "\n",
127 | "line2 = (\n",
128 | " alt.Chart(data2)\n",
129 | " .mark_line(strokeWidth=5)\n",
130 | " .encode(\n",
131 | " x=alt.X(\"dist\", title=\"Distance / km\"),\n",
132 | " y=alt.Y(\n",
133 | " \"heart\",\n",
134 | " axis=alt.Axis(title=\"Heart rate / bpm\", titleColor=\"#1b9e77\"),\n",
135 | " ),\n",
136 | " color=alt.value(\"#1b9e77\"),\n",
137 | " )\n",
138 | ")\n",
139 | "chart = alt.layer(\n",
140 | " area1,\n",
141 | " line2,\n",
142 | " width=WIDTH,\n",
143 | " height=HEIGHT,\n",
144 | ").resolve_scale(y=\"independent\")\n",
145 | "\n",
146 | "chart.title = \"Elevation & heart rate profile\"\n",
147 | "\n",
148 | "\n",
149 | "the_map = create_folium_map(tiles=\"openstreetmap\")\n",
150 | "add_segment_to_map(\n",
151 | " the_map,\n",
152 | " segment,\n",
153 | " color_by=\"hr-zone-float\",\n",
154 | " cmap=\"viridis\",\n",
155 | " line_options=line_options,\n",
156 | ")\n",
157 | "\n",
158 | "\n",
159 | "chart_dict = json.loads(chart.to_json())\n",
160 | "popup = folium.Popup(max_width=WIDTH + 100, show=True)\n",
161 | "chart_vega = folium.features.VegaLite(\n",
162 | " chart_dict, width=WIDTH + 100, height=HEIGHT + 50\n",
163 | ")\n",
164 | "chart_vega.add_to(popup)\n",
165 | "marker = folium.Marker(\n",
166 | " location=segment[\"latlon\"][idx],\n",
167 | " popup=popup,\n",
168 | " icon=folium.Icon(icon=\"star\"),\n",
169 | ")\n",
170 | "marker.add_to(the_map)\n",
171 | "the_map"
172 | ]
173 | }
174 | ],
175 | "metadata": {
176 | "kernelspec": {
177 | "display_name": "Python 3 (ipykernel)",
178 | "language": "python",
179 | "name": "python3"
180 | },
181 | "language_info": {
182 | "codemirror_mode": {
183 | "name": "ipython",
184 | "version": 3
185 | },
186 | "file_extension": ".py",
187 | "mimetype": "text/x-python",
188 | "name": "python",
189 | "nbconvert_exporter": "python",
190 | "pygments_lexer": "ipython3",
191 | "version": "3.10.6"
192 | }
193 | },
194 | "nbformat": 4,
195 | "nbformat_minor": 4
196 | }
197 |
--------------------------------------------------------------------------------
/examples/jupyter/examples_image.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Adding some images as popups to a map\n",
8 | "This example will add some images taken during an activity\n",
9 | "as popups in the map."
10 | ]
11 | },
12 | {
13 | "cell_type": "code",
14 | "execution_count": null,
15 | "metadata": {},
16 | "outputs": [],
17 | "source": [
18 | "from gpxplotter import create_folium_map, read_gpx_file, add_segment_to_map\n",
19 | "import folium\n",
20 | "\n",
21 | "line_options = {\"weight\": 8}\n",
22 | "\n",
23 | "the_map = create_folium_map(tiles=\"kartverket_topo4\")\n",
24 | "for track in read_gpx_file(\"example3.gpx\"):\n",
25 | " for i, segment in enumerate(track[\"segments\"]):\n",
26 | " add_segment_to_map(\n",
27 | " the_map,\n",
28 | " segment,\n",
29 | " color_by=\"hr-zone-float\",\n",
30 | " cmap=\"viridis\",\n",
31 | " line_options=line_options,\n",
32 | " )\n",
33 | "\n",
34 | "# To display the map in a Jupyter notebook:\n",
35 | "the_map"
36 | ]
37 | },
38 | {
39 | "cell_type": "markdown",
40 | "metadata": {},
41 | "source": [
42 | "First, attempt to read location information from the images:"
43 | ]
44 | },
45 | {
46 | "cell_type": "code",
47 | "execution_count": null,
48 | "metadata": {},
49 | "outputs": [],
50 | "source": [
51 | "import PIL\n",
52 | "from PIL.ExifTags import TAGS, GPSTAGS\n",
53 | "import datetime\n",
54 | "\n",
55 | "\n",
56 | "# Create a method to get coordinates from an image:\n",
57 | "def get_lat_lon(imagefile):\n",
58 | " image = PIL.Image.open(imagefile)\n",
59 | " exif = image.getexif()\n",
60 | " exif_info = {TAGS.get(key, key): val for key, val in exif.items()}\n",
61 | " # Get the GPSInfo:\n",
62 | " for key, value in TAGS.items():\n",
63 | " if value == \"GPSInfo\":\n",
64 | " exif_info[value] = exif.get_ifd(key)\n",
65 | " break\n",
66 | " gps_info = {}\n",
67 | " for key, val in exif_info[\"GPSInfo\"].items():\n",
68 | " gps_info[GPSTAGS.get(key, key)] = val\n",
69 | " # Convert to decimal latitude/longitude:\n",
70 | " deg, minutes, seconds = gps_info[\"GPSLatitude\"]\n",
71 | " latitude = deg + minutes / 60.0 + seconds / 3600.0\n",
72 | " if \"GPSLatitudeRef\" == \"S\":\n",
73 | " latitude *= -1\n",
74 | " deg, minutes, seconds = gps_info[\"GPSLongitude\"]\n",
75 | " longitude = deg + minutes / 60.0 + seconds / 3600.0\n",
76 | " if \"GPSLongitudeRef\" == \"W\":\n",
77 | " longitude *= -1\n",
78 | " # Turn time into datetime:\n",
79 | " time = datetime.datetime.strptime(\n",
80 | " exif_info[\"DateTime\"], \"%Y:%m:%d %H:%M:%S\"\n",
81 | " )\n",
82 | " return latitude, longitude, time"
83 | ]
84 | },
85 | {
86 | "cell_type": "code",
87 | "execution_count": null,
88 | "metadata": {},
89 | "outputs": [],
90 | "source": [
91 | "info = {}\n",
92 | "for filename in (\"image1.jpg\", \"image2.jpg\", \"image3.jpg\"):\n",
93 | " lat, lon, time = get_lat_lon(filename)\n",
94 | " info[filename] = {\"latlon\": (lat, lon), \"time\": time}"
95 | ]
96 | },
97 | {
98 | "cell_type": "code",
99 | "execution_count": null,
100 | "metadata": {},
101 | "outputs": [],
102 | "source": [
103 | "# Add, markers to the gps-locations we read from the images\n",
104 | "for key, val in info.items():\n",
105 | " marker = folium.Marker(\n",
106 | " location=val[\"latlon\"],\n",
107 | " tooltip=f\"You took a picture here? {key} says so...\",\n",
108 | " icon=folium.Icon(icon=\"camera\", color=\"red\"),\n",
109 | " )\n",
110 | " marker.add_to(the_map)\n",
111 | "boundary = the_map.get_bounds()\n",
112 | "the_map.fit_bounds(boundary, padding=(3, 3))\n",
113 | "the_map"
114 | ]
115 | },
116 | {
117 | "cell_type": "markdown",
118 | "metadata": {},
119 | "source": [
120 | "As can be seen in the map above, the GPS locations in the image files may be a bit off.\n",
121 | "Let's try to see if we can use the time information to place them better.\n",
122 | "\n",
123 | "**Note**: The best approach is probably to make your GPS devise show its\n",
124 | "current time, and then take a photo of it with your phone. This can\n",
125 | "be used to \"align\" time-stamps from the two devices better."
126 | ]
127 | },
128 | {
129 | "cell_type": "code",
130 | "execution_count": null,
131 | "metadata": {},
132 | "outputs": [],
133 | "source": [
134 | "# Time read from the images does not contain time zone information, so it's\n",
135 | "# difficult to compare with the timestamps from the GPS device.\n",
136 | "# Here, I remember when I started so I will use that.\n",
137 | "time_offset = datetime.timedelta(seconds=2 * 3600) # time_offset is 2 hours\n",
138 | "import numpy as np\n",
139 | "\n",
140 | "for key, val in info.items():\n",
141 | " time = val[\"time\"]\n",
142 | " times = [i.replace(tzinfo=None) + time_offset for i in segment[\"time\"]]\n",
143 | " time_diff = []\n",
144 | " for i in times:\n",
145 | " if i < time:\n",
146 | " time_diff.append((time - i).seconds)\n",
147 | " else:\n",
148 | " time_diff.append((i - time).seconds)\n",
149 | " minidx = np.argmin(time_diff)\n",
150 | " info[key][\"latlon_time\"] = segment[\"latlon\"][minidx]"
151 | ]
152 | },
153 | {
154 | "cell_type": "code",
155 | "execution_count": null,
156 | "metadata": {},
157 | "outputs": [],
158 | "source": [
159 | "# Mark the gps-locations we interpolated using time:\n",
160 | "the_map = create_folium_map(\n",
161 | " tiles=\"kartverket_topo4\",\n",
162 | " zoom_start=18,\n",
163 | " location=info[\"image1.jpg\"][\"latlon_time\"],\n",
164 | ")\n",
165 | "add_segment_to_map(\n",
166 | " the_map,\n",
167 | " segment,\n",
168 | " line_options=line_options,\n",
169 | " fit_bounds=False,\n",
170 | " color_by=\"hr-zone-float\",\n",
171 | ")\n",
172 | "colors = [\"blue\", \"red\", \"green\"]\n",
173 | "for i, (key, val) in enumerate(info.items()):\n",
174 | " marker = folium.Marker(\n",
175 | " location=val[\"latlon_time\"],\n",
176 | " popup=f'
',\n",
177 | " icon=folium.Icon(icon=\"camera\", color=colors[i]),\n",
178 | " )\n",
179 | " marker.add_to(the_map)\n",
180 | "the_map"
181 | ]
182 | }
183 | ],
184 | "metadata": {
185 | "kernelspec": {
186 | "display_name": "Python 3 (ipykernel)",
187 | "language": "python",
188 | "name": "python3"
189 | },
190 | "language_info": {
191 | "codemirror_mode": {
192 | "name": "ipython",
193 | "version": 3
194 | },
195 | "file_extension": ".py",
196 | "mimetype": "text/x-python",
197 | "name": "python",
198 | "nbconvert_exporter": "python",
199 | "pygments_lexer": "ipython3",
200 | "version": "3.10.6"
201 | }
202 | },
203 | "nbformat": 4,
204 | "nbformat_minor": 4
205 | }
206 |
--------------------------------------------------------------------------------
/examples/jupyter/image1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/examples/jupyter/image1.jpg
--------------------------------------------------------------------------------
/examples/jupyter/image2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/examples/jupyter/image2.jpg
--------------------------------------------------------------------------------
/examples/jupyter/image3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andersle/gpxplotter/6c08e89fa794efe5d5a84a503e680a155b522b5e/examples/jupyter/image3.jpg
--------------------------------------------------------------------------------
/gpxplotter/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | ##########
5 | gpxplotter
6 | ##########
7 |
8 | This is gpxplotter - A package for reading GPX files and make some
9 | simple plots.
10 |
11 | Copyright (C) 2021, Anders Lervik.
12 |
13 | Permission is hereby granted, free of charge, to any person obtaining a copy
14 | of this software and associated documentation files (the "Software"), to deal
15 | in the Software without restriction, including without limitation the rights
16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 | copies of the Software, and to permit persons to whom the Software is
18 | furnished to do so, subject to the following conditions:
19 |
20 | The above copyright notice and this permission notice shall be included in all
21 | copies or substantial portions of the Software.
22 |
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29 | SOFTWARE.
30 |
31 | """
32 | from .folium_map import (
33 | add_all_tiles,
34 | add_segment_to_map,
35 | add_tiles_to_map,
36 | create_folium_map,
37 | )
38 | from .gpxread import read_gpx_file
39 | from .mplplotting import plot_filled, plot_line
40 | from .version import VERSION as __version__
41 |
42 | __all__ = [
43 | "read_gpx_file",
44 | "create_folium_map",
45 | "add_segment_to_map",
46 | "add_tiles_to_map",
47 | "add_all_tiles",
48 | "plot_line",
49 | "plot_filled",
50 | ]
51 |
--------------------------------------------------------------------------------
/gpxplotter/common.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """This module defines common methods for gpxplotter."""
4 | import warnings
5 |
6 | import numpy as np
7 | from sklearn.cluster import KMeans
8 |
9 | # Define heart-rate limits:
10 | HR_LIMITS = [(0.5, 0.6), (0.6, 0.7), (0.7, 0.8), (0.8, 0.9), (0.9, 1.0)]
11 |
12 | # For adding text:
13 | RELABEL = {
14 | "hr": "Heart rate / bpm",
15 | "distance": "Distance / m",
16 | "time": "Time",
17 | "elapsed-time": "Elapsed time",
18 | "elevation": "Elevation / m",
19 | "hr-zone-frac": "Fraction of maximum heart rate",
20 | "hr-zone-float": "Heart rate zone",
21 | "hr-zone": "Heart rate zone",
22 | "velocity-level": "Velocity (slower -> faster)",
23 | }
24 |
25 |
26 | def heart_rate_zone_limits(max_heart_rate=187, limits=None):
27 | """Return the limits for the heart rate zones.
28 |
29 | Parameters
30 | ----------
31 | max_heart_rate : int (or float)
32 | The maximum heart rate
33 | limits : list
34 | A list of heart rate zone limits as fractions of the
35 | maximum heart rate. This list is on the
36 | form `[[min_zone_1, max_zone_1],]`. The default zones
37 | are:
38 | `[(0.5, 0.6), (0.6, 0.7), (0.7, 0.8), (0.8, 0.9), (0.9, 1.0)]`
39 |
40 | Returns
41 | -------
42 | out : list of list of numbers
43 | The heart rates defining the different zones.
44 |
45 | """
46 | if limits is None:
47 | limits = HR_LIMITS
48 | return [(max_heart_rate * i[0], max_heart_rate * i[1]) for i in limits]
49 |
50 |
51 | def format_time_delta(time_delta):
52 | """Format time deltas as strings on the form hh:mm:ss.
53 |
54 | Parameters
55 | ----------
56 | time_delta : array_like
57 | A time in seconds.
58 |
59 | Returns
60 | -------
61 | timel : list of strings
62 | The ``time_delta`` formatted as hh:mm:ss
63 |
64 | """
65 | timel = []
66 | for i in time_delta:
67 | hours, res = divmod(i, 3600)
68 | minutes, seconds = divmod(res, 60)
69 | timel.append(f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}")
70 | return timel
71 |
72 |
73 | def find_regions(yval):
74 | """Find borders for regions with equal values.
75 |
76 | Parameters
77 | ----------
78 | yval : array_like
79 | The values we are to locate regions for.
80 |
81 | Returns
82 | -------
83 | new_regions : list of lists of numbers
84 | The regions where yval is constant. These are on the form
85 | ``[start_index, end_index, constant_y]`` with the
86 | interpretation that ``yval=constant-y`` for the index
87 | range ``[start_index, end_index]``
88 |
89 | """
90 | regions = []
91 | region_y = None
92 | i = None
93 | for i, ypos in enumerate(yval):
94 | if region_y is None:
95 | region_y = ypos
96 | if ypos != region_y:
97 | regions.append([i, region_y])
98 | region_y = ypos
99 | # for adding the last region
100 | if i is not None:
101 | regions.append([i, region_y])
102 | new_regions = []
103 | for i, region in enumerate(regions):
104 | if i == 0:
105 | reg = [0, region[0], region[1]]
106 | else:
107 | reg = [regions[i - 1][0], region[0], region[1]]
108 | new_regions.append(reg)
109 | return new_regions
110 |
111 |
112 | def update_hr_zones(segment, max_heart_rate=187):
113 | """Find and update heart rate zones for a segment.
114 |
115 | Parameters
116 | ----------
117 | segment : dict
118 | The segment to add zones for.
119 | max_heart_rate : int (or float)
120 | The maximum heart rate, used for the calculation of zones.
121 |
122 | """
123 | if "hr" in segment:
124 | limits = heart_rate_zone_limits(max_heart_rate=max_heart_rate)
125 | bins = [i[0] for i in limits]
126 | # bins[i-1] <= x < bins[i]
127 | segment["hr-zone"] = np.digitize(segment["hr"], bins, right=False)
128 | # Add fractional part:
129 | zone_float = []
130 | for hrate, zone in zip(segment["hr"], segment["hr-zone"]):
131 | if zone == 0:
132 | left = 0
133 | right = bins[0]
134 | elif zone == len(bins):
135 | left = bins[-1]
136 | right = max_heart_rate
137 | else:
138 | left = bins[zone - 1]
139 | right = bins[zone]
140 | frac = (hrate - left) / (right - left)
141 | zone_float.append(zone + frac)
142 | segment["hr-zone-float"] = np.array(zone_float)
143 | segment["hr-zone-frac"] = segment["hr"] / max_heart_rate
144 | segment["hr-regions"] = find_regions(segment["hr-zone"])
145 | segment["zone_txt"] = get_limits_txt(limits)
146 |
147 |
148 | def get_limits_txt(limits):
149 | """Return heart rate limits as text.
150 |
151 | Parameters
152 | ----------
153 | limits : list of list of numbers
154 |
155 | Returns
156 | -------
157 | txt : dict
158 | Text representing the heart rate zones.
159 |
160 | """
161 | txt = {
162 | 0: f"$<${int(limits[0][0])} bpm",
163 | 1: f"{int(limits[0][0])}‒{int(limits[0][1])} bpm",
164 | 2: f"{int(limits[1][0])}‒{int(limits[1][1])} bpm",
165 | 3: f"{int(limits[2][0])}‒{int(limits[2][1])} bpm",
166 | 4: f"{int(limits[3][0])}‒{int(limits[3][1])} bpm",
167 | 5: f"$>${int(limits[3][1])} bpm",
168 | }
169 | return txt
170 |
171 |
172 | def cluster_velocities(velocities, n_clusters=5):
173 | """Group the velocities into a predefined set of clusters.
174 |
175 | This is used to label velocities as `faster`, `slower`, etc.
176 |
177 | Parameters
178 | ----------
179 | velocities : array_like
180 | The velocities to cluster.
181 | n_clusters : int, optional
182 | The number of clusters to look for.
183 |
184 | Returns
185 | -------
186 | levels : array_like
187 | The velocity level (cluster) each velocity is assigned to.
188 |
189 | """
190 | if np.isnan(velocities).any():
191 | warnings.warn("Some velocities are NaN, skipping clustering")
192 | return None
193 | vel = np.array(velocities).reshape(-1, 1)
194 | clu = KMeans(n_clusters=n_clusters, init="k-means++", n_init=10)
195 | labels = clu.fit_predict(vel)
196 | # Sort labels according to cluster centers so that a lower label
197 | # is a lower velocity:
198 | centers = clu.cluster_centers_.flatten()
199 | idx = list(np.argsort(centers))
200 | levels = np.array([idx.index(i) for i in labels], dtype=np.int_)
201 | return levels
202 |
--------------------------------------------------------------------------------
/gpxplotter/folium_map.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """This module defines methods for interfacing with folium."""
4 | import branca.colormap
5 | import folium
6 |
7 | from gpxplotter.common import RELABEL
8 |
9 | TILES = {
10 | "kartverket_topo4": {
11 | "name": "Kartverket (topo4)",
12 | "tiles": (
13 | "http://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?"
14 | "layers=topo4&zoom={z}&x={x}&y={y}"
15 | ),
16 | "attr": 'Kartverket',
17 | },
18 | "kartverket_topo4graatone": {
19 | "name": "Kartverket (topo4graatone)",
20 | "tiles": (
21 | "http://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?"
22 | "layers=topo4graatone&zoom={z}&x={x}&y={y}"
23 | ),
24 | "attr": 'Kartverket',
25 | },
26 | "opentopomap": {
27 | "name": "OpenTopoMap",
28 | "tiles": "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png",
29 | "attr": (
30 | "Map data: © "
31 | 'OpenStreetMap'
32 | ' contributors, '
33 | 'SRTM | Map style: © OpenTopoMap (CC-BY-SA)'
36 | ),
37 | "min_zoom": 0,
38 | "max_zoom": 18,
39 | },
40 | "ersi.worldtopomap": {
41 | "name": "Esri (WorldTopoMap)",
42 | "tiles": (
43 | "https://server.arcgisonline.com/ArcGIS/rest/services/"
44 | "World_Topo_Map/MapServer/tile/{z}/{y}/{x}"
45 | ),
46 | "attr": (
47 | "Tiles © Esri — Esri, DeLorme, NAVTEQ, "
48 | "TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase,"
49 | " Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri"
50 | " China (Hong Kong), and the GIS User Community"
51 | ),
52 | },
53 | "esri_worldimagery": {
54 | "name": "Esri (WorldImagery)",
55 | "tiles": (
56 | "https://server.arcgisonline.com/ArcGIS/rest/services/"
57 | "World_Imagery/MapServer/tile/{z}/{y}/{x}"
58 | ),
59 | "attr": (
60 | "Tiles © Esri — Source: Esri, i-cubed, USDA, "
61 | "USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP"
62 | ", and the GIS User Community"
63 | ),
64 | },
65 | "openstreetmap_humanitarian": {
66 | "name": "Humanitarian",
67 | "tiles": "https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png",
68 | "attr": (
69 | '© '
70 | 'OpenStreetMap contributors, Tiles style by Humanitarian '
72 | "OpenStreetMap Team hosted by OpenStreetMap'
74 | " France"
75 | ),
76 | },
77 | "cyclosm": {
78 | "name": "CyclOSM",
79 | "tiles": (
80 | "https://{s}.tile-cyclosm.openstreetmap.fr"
81 | "/cyclosm/{z}/{x}/{y}.png"
82 | ),
83 | "attr": (
84 | 'CyclOSM'
86 | ' | Map data: © OpenStreetMap contributors'
88 | ),
89 | "min_zoom": 0,
90 | "max_zoom": 20,
91 | },
92 | }
93 |
94 |
95 | _FOLIUM_TILES = (
96 | "openstreetmap",
97 | "cartodb positron",
98 | "cartodb voyager",
99 | )
100 |
101 |
102 | def create_folium_map(**kwargs):
103 | """Create a folium map.
104 |
105 | This method is essentially the same as calling
106 | ``folium.Map(**kwargs)``, with a few differences:
107 |
108 | * ``control_scale = True`` by default.
109 | * ``tiles`` can be ``"openstreetmap"``,
110 | ``"cartodb positron"``, ``"cartodb voyager"``, or
111 | any of the tiles defined in :py:const:`.TILES`.
112 |
113 | Parameters
114 | ----------
115 | kwargs : optional
116 | Arguments passed to the method generating the map,
117 | see :py:func:`folium.Map`.
118 |
119 | Returns
120 | -------
121 | the_map : object like :py:class:`folium.folium.Map`
122 | The map created here.
123 |
124 | """
125 | # Add a few defaults:
126 | kwargs["control_scale"] = kwargs.get("control_scale", True)
127 | tiles = kwargs.get("tiles", None)
128 | if tiles is None:
129 | the_map = folium.Map(**kwargs)
130 | else:
131 | if tiles in _FOLIUM_TILES:
132 | the_map = folium.Map(**kwargs)
133 | else:
134 | if tiles in TILES:
135 | tile_layer = folium.TileLayer(**TILES[tiles])
136 | kwargs["tiles"] = None
137 | the_map = folium.Map(**kwargs)
138 | the_map.add_child(tile_layer, name=tile_layer.tile_name)
139 | return the_map
140 |
141 |
142 | def add_tiles_to_map(the_map, *tiles):
143 | """Add pre-defined tiles to the given map.
144 |
145 | Parameters
146 | ----------
147 | the_map : object like :py:class:`folium.folium.Map`
148 | The map to add tiles to.
149 | tiles : list of strings
150 | The name of the tiles to add.
151 |
152 | """
153 | for tile in tiles:
154 | if tile in _FOLIUM_TILES:
155 | folium.TileLayer(tile).add_to(the_map)
156 | else:
157 | if tile in TILES:
158 | tile_layer = folium.TileLayer(**TILES[tile])
159 | the_map.add_child(tile_layer, name=tile_layer.tile_name)
160 |
161 |
162 | def add_all_tiles(the_map):
163 | """Add all pre-defined tiles to the given map."""
164 | tiles = list(_FOLIUM_TILES) + list(TILES.keys())
165 | add_tiles_to_map(the_map, *tiles)
166 |
167 |
168 | def add_marker_at(the_map, segment, index, tooltip, **kwargs):
169 | """Add start marker at the given index.
170 |
171 | Parameters
172 | ----------
173 | the_map : object like :py:class:`folium.folium.Map`
174 | The map to add the markers to.
175 | segment : dict
176 | The segment to add marker for.
177 | index : int
178 | The point in the segment to add marker to.
179 | tooltip : string
180 | The tooltip to be added to the marker.
181 | kwargs : optional
182 | Arguments passed on to :py:class:`folim.Icon`.
183 |
184 | """
185 | time = segment["time"][index].strftime("%A %B %d, %Y: %H:%M:%S")
186 | if index == 0:
187 | txt = f"Start: {time}"
188 | elif index == -1:
189 | dist = segment["distance"][index] / 1000.0
190 | txt = f"End: {time}Distance: {dist:.2f} km"
191 | else:
192 | dist = segment["distance"][index] / 1000.0
193 | txt = f"Time:{time}Distance: {dist:.2f} km"
194 | marker = folium.Marker(
195 | location=segment["latlon"][index],
196 | tooltip=tooltip,
197 | popup=folium.Popup(txt, max_width=250),
198 | icon=folium.Icon(**kwargs),
199 | )
200 | marker.add_to(the_map)
201 |
202 |
203 | def add_start_top_markers(the_map, segment):
204 | """Add markers for the start and end of the segment.
205 |
206 | Parameters
207 | ----------
208 | the_map : object like :py:class:`folium.folium.Map`
209 | The map to add the markers to.
210 | segment : dict
211 | The segment to use for finding the start and end points.
212 |
213 | """
214 | add_marker_at(the_map, segment, 0, "Start", icon="ok", color="green")
215 | add_marker_at(the_map, segment, -1, "End", icon="home", color="lightgray")
216 |
217 |
218 | def add_segment_to_map(
219 | the_map,
220 | segment,
221 | color_by=None,
222 | cmap="viridis",
223 | line_options=None,
224 | fit_bounds=True,
225 | add_start_end=True,
226 | min_value=None,
227 | max_value=None,
228 | ):
229 | """Add a segment as a line to a map.
230 |
231 | This method will add a segment as a line to the given map. The line
232 | can be colored according to values selected by the parameter
233 | ``color_by``.
234 |
235 | Parameters
236 | ----------
237 | the_map : object like :py:class:`folium.folium.Map`
238 | The map to add the segment to.
239 | segment : dict
240 | The segment to add.
241 | color_by : string, optional
242 | This string selects what property we will color the segment
243 | according to. If this is None, the segment will be displayed
244 | with a single color.
245 | cmap : string
246 | The colormap to use if ``color_by != None``.
247 | line_options : dict
248 | Extra control options for drawing the line.
249 | fit_bounds : boolean, optional
250 | Determines if we try to fit the map so the whole segment
251 | is shown.
252 | add_start_end : boolean, optional
253 | If True, this method will add markers at the start/end of the
254 | segment.
255 | min_value : the minimum value for the colormap.
256 | If None it will be determined from the segment data.
257 | max_value : the maximum value for the colormap.
258 | If None it will be determined from the segment data.
259 |
260 | """
261 | if color_by is None:
262 | if line_options is None:
263 | line_options = {}
264 | line = folium.features.PolyLine(segment["latlon"], **line_options)
265 | line.add_to(the_map)
266 | else:
267 | add_colored_line(
268 | the_map,
269 | segment,
270 | color_by,
271 | cmap=cmap,
272 | line_options=line_options,
273 | min_value=min_value,
274 | max_value=max_value,
275 | )
276 | if add_start_end:
277 | add_start_top_markers(the_map, segment)
278 | if fit_bounds:
279 | boundary = the_map.get_bounds()
280 | the_map.fit_bounds(boundary, padding=(3, 3))
281 |
282 |
283 | def add_colored_line(
284 | the_map,
285 | segment,
286 | color_by,
287 | cmap="viridis",
288 | line_options=None,
289 | min_value=None,
290 | max_value=None,
291 | ):
292 | """Add segment as a colored line to a map.
293 |
294 | Add a line colored by some value to the given map.
295 |
296 | Parameters
297 | ----------
298 | the_map : object like :py:class:`folium.folium.Map`
299 | The map to add the segment to.
300 | segment : dict
301 | The segment to add.
302 | color_by : string
303 | This string selects what property we will color the segment
304 | according to.
305 | cmap : string
306 | The colormap to use for coloring.
307 | line_options : dict
308 | Extra control options for drawing the line.
309 | min_value : the minimum value for the colormap.
310 | If None it will be determined from the segment data.
311 | max_value : the maximum value for the colormap.
312 | If None it will be determined from the segment data.
313 |
314 | """
315 | zdata = segment[color_by]
316 | avg = 0.5 * (zdata[1:] + zdata[:-1])
317 | minz = min_value if min_value else min(avg)
318 | maxz = max_value if max_value else max(avg)
319 | uniq = len(set(zdata))
320 | if uniq < 10:
321 | levels = uniq + 1
322 | else:
323 | levels = 10
324 | linmap = None
325 | if isinstance(cmap, str):
326 | linmap = getattr(branca.colormap.linear, cmap)
327 | elif isinstance(cmap, branca.colormap.ColorMap):
328 | linmap = cmap
329 | else:
330 | raise Exception(
331 | "Color map can be either a name of a linear map from "
332 | "branca.coloramp package, or a branca.colormap.ColorMap "
333 | "instance."
334 | )
335 |
336 | colormap = linmap.scale(minz, maxz).to_step(levels)
337 | colormap.caption = RELABEL.get(color_by, color_by)
338 |
339 | if line_options is None:
340 | line_options = {"weight": 6}
341 | line_options["weight"] = line_options.get("weight", 6)
342 | line = folium.ColorLine(
343 | positions=segment["latlon"],
344 | colormap=colormap,
345 | colors=avg,
346 | control=False,
347 | **line_options,
348 | )
349 | line.add_to(the_map)
350 | the_map.add_child(colormap)
351 |
--------------------------------------------------------------------------------
/gpxplotter/gpxread.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """This module defines methods for reading data from GPX files."""
4 | import warnings
5 | from math import atan, atan2, cos, radians, sin, sqrt, tan
6 | from xml.dom import minidom
7 |
8 | import dateutil.parser
9 | import numpy as np
10 | from scipy.interpolate import UnivariateSpline
11 |
12 | from gpxplotter.common import cluster_velocities, update_hr_zones
13 |
14 | EXTRACT = {
15 | "time": {"key": "time", "formatter": dateutil.parser.parse},
16 | "temp": {"key": "ns3:atemp", "formatter": float},
17 | "hr": {"key": "ns3:hr", "formatter": int},
18 | "cad": {"key": "ns3:cad", "formatter": float},
19 | }
20 |
21 |
22 | def vincenty(point1, point2, tol=10**-12, maxitr=1000):
23 | """Calculate distance between two lat/lon coordinates.
24 |
25 | This calculation is based on the formula available from
26 | Wikipedia [1]_.
27 |
28 | Parameters
29 | ----------
30 | point1 : tuple of floats
31 | This is the first coordinate on the form: ``(lat, lon)``.
32 | point2 : tuple of floats
33 | This is the second coordinate on the form: ``(lat, lon)``.
34 | tol : float, optional
35 | The tolerance from convergence. The default value is
36 | taken from the wikipedia article.
37 | maxitr : integer, optional
38 | The maximum number of iterations to perform.
39 |
40 | References
41 | ----------
42 | .. [1] https://en.wikipedia.org/wiki/Vincenty's_formulae
43 |
44 | """
45 | # Some WGS-84 parameters:
46 | major_axis = 6378137.0
47 | flattening = 1 / 298.257223563
48 | minor_axis = 6356752.314245
49 |
50 | lat1, lon1 = point1
51 | lat2, lon2 = point2
52 |
53 | rlat1 = atan((1 - flattening) * tan(radians(lat1)))
54 | rlat2 = atan((1 - flattening) * tan(radians(lat2)))
55 |
56 | diff_long = radians(lon2) - radians(lon1)
57 |
58 | # Iteratively, according to Wikipedia
59 | lambd = diff_long
60 |
61 | sin1 = sin(rlat1)
62 | cos1 = cos(rlat1)
63 |
64 | sin2 = sin(rlat2)
65 | cos2 = cos(rlat2)
66 |
67 | converged = False
68 | for _ in range(maxitr):
69 | sin_sigma = sqrt(
70 | (cos2 * sin(lambd)) ** 2
71 | + (cos1 * sin2 - sin1 * cos2 * cos(lambd)) ** 2
72 | )
73 | if sin_sigma == 0.0:
74 | return 0.0
75 | cos_sigma = sin1 * sin2 + cos1 * cos2 * cos(lambd)
76 | sigma = atan2(sin_sigma, cos_sigma)
77 | sin_alpha = (cos1 * cos2 * sin(lambd)) / sin_sigma
78 | cos_alpha_sq = 1.0 - sin_alpha**2
79 | if cos_alpha_sq == 0.0:
80 | cos_sigma2 = 0.0
81 | else:
82 | cos_sigma2 = cos_sigma - ((2.0 * sin1 * sin2) / cos_alpha_sq)
83 | cvar = (
84 | (flattening / 16.0)
85 | * cos_alpha_sq
86 | * (4.0 + flattening * (4.0 - 3.0 * cos_alpha_sq))
87 | )
88 | lambd0 = lambd
89 | lambd = diff_long + (1.0 - cvar) * flattening * sin_alpha * (
90 | sigma
91 | + cvar
92 | * sin_sigma
93 | * (
94 | cos_sigma2
95 | + cvar * (cos_sigma * (-1.0 + 2.0 * cos_sigma2**2))
96 | )
97 | )
98 | diff = abs(lambd0 - lambd)
99 | if diff <= tol:
100 | converged = True
101 | break
102 | if not converged:
103 | raise ValueError("Vincenty's formulae did not converge!")
104 | uvar_sq = cos_alpha_sq * (
105 | (major_axis**2 - minor_axis**2) / minor_axis**2
106 | )
107 | avar = 1.0 + (uvar_sq / 16384.0) * (
108 | 4096.0 + uvar_sq * (-768.0 + uvar_sq * (320.0 - 175.0 * uvar_sq))
109 | )
110 | bvar = (uvar_sq / 1024.0) * (
111 | 256.0 + uvar_sq * (-128.0 + uvar_sq * (74.0 - 47.0 * uvar_sq))
112 | )
113 | delta_sigma = (
114 | bvar
115 | * sin_sigma
116 | * (
117 | cos_sigma2
118 | + 0.25
119 | * bvar
120 | * (
121 | cos_sigma * (-1.0 + 2.0 * cos_sigma2**2)
122 | - (bvar / 6.0)
123 | * (
124 | cos_sigma2
125 | * (
126 | (-3.0 + 4.0 * sin_sigma**2)
127 | * (-3.0 + 4.0 * cos_sigma2**2)
128 | )
129 | )
130 | )
131 | )
132 | )
133 | dist = minor_axis * avar * (sigma - delta_sigma)
134 | return dist
135 |
136 |
137 | def extract_data(point, key, formatter):
138 | """Extract data from a point.
139 |
140 | Parameters
141 | ----------
142 | key : string
143 | The xml field we are extracting data from.
144 | formatter : callable
145 | A method we use to format/convert the data we extract.
146 |
147 | """
148 | data = point.getElementsByTagName(key)
149 | for i in data:
150 | return [formatter(child.data) for child in i.childNodes]
151 |
152 |
153 | def get_point_data(point):
154 | """Get basic information from a track point.
155 |
156 | Parameters
157 | ----------
158 | point : object like :py:class:`xml.dom.minidom.Element`
159 | The point on the track we are extracting information from.
160 |
161 | """
162 | data = {
163 | "lat": float(point.getAttribute("lat")),
164 | "lon": float(point.getAttribute("lon")),
165 | }
166 |
167 | ele = extract_data(point, "ele", float)
168 | if ele is not None:
169 | data["elevation"] = ele
170 |
171 | for key, val in EXTRACT.items():
172 | value = extract_data(point, val["key"], val["formatter"])
173 | if value is not None:
174 | data[key] = value
175 |
176 | return data
177 |
178 |
179 | def read_segment(segment):
180 | """Read raw gpx-data for a segment.
181 |
182 | Parameters
183 | ----------
184 | segment : object like :py:class:`xml.dom.minidom.Element`
185 | The segment we are about to read data from.
186 |
187 | Returns
188 | -------
189 | out : dict
190 | The data read from the segment.
191 |
192 | """
193 | points = segment.getElementsByTagName("trkpt")
194 |
195 | data = {}
196 | for point in points:
197 | point_data = get_point_data(point)
198 | if any([i is None for i in data]):
199 | continue
200 | for key, val in point_data.items():
201 | if key not in data:
202 | data[key] = []
203 | try:
204 | data[key].extend(val)
205 | except TypeError:
206 | data[key].append(val)
207 | for key, val in data.items():
208 | if key not in ("time", "hr"):
209 | data[key] = np.array(val)
210 | if "hr" in data:
211 | data["hr"] = np.array(data["hr"], dtype=np.int_)
212 | return data
213 |
214 |
215 | def get_distances(lat, lon):
216 | """Get the spatial distance between time-ordered points.
217 |
218 | Parameters
219 | ----------
220 | lat : list of floats
221 | The latitudes.
222 | lon : list of floats
223 | The longitudes.
224 |
225 | Returns
226 | -------
227 | dist : list of floats
228 | The distances in meters.
229 |
230 | """
231 | dist = []
232 | for i, (lati, loni) in enumerate(zip(lat, lon)):
233 | if i == 0:
234 | dist.append(0)
235 | else:
236 | dist.append(
237 | dist[-1] + vincenty((lati, loni), (lat[i - 1], lon[i - 1]))
238 | )
239 | return dist
240 |
241 |
242 | def _get_gpx_text(track, tagname):
243 | """Grab text from a given track."""
244 | tag_txt = []
245 | tag = track.getElementsByTagName(tagname)
246 | for i in tag:
247 | tag_txt.append(
248 | "".join(
249 | child.data
250 | for child in i.childNodes
251 | if (child.nodeType == child.TEXT_NODE)
252 | )
253 | )
254 | return tag_txt
255 |
256 |
257 | def approximate_velocity(distance, time):
258 | """Calculate approximate velocities.
259 |
260 | This method will calculate approximate velocities by
261 | finding a spline and its derivative.
262 |
263 | Parameters
264 | ----------
265 | distance : array_like
266 | Distances, measured as a function of time.
267 | time : array_like
268 | The accompanying time stamps for the velocities.
269 |
270 | """
271 | try:
272 | spline = UnivariateSpline(time, distance, k=3)
273 | vel = spline.derivative()(time)
274 | idx = np.where(vel < 0)[0]
275 | vel[idx] = 0.0
276 | return vel
277 | except Exception as error:
278 | warnings.warn(f'Estimating velocities failed: "{error.args}"')
279 | return None
280 |
281 |
282 | def get_velocity(segment):
283 | """Attempt to estimate the velocity.
284 |
285 | Parameters
286 | ----------
287 | segment : dict
288 | The raw data from the gpx file.
289 |
290 | """
291 | # Velocity i m / s
292 | velocity = approximate_velocity(
293 | segment["distance"], segment["elapsed-time"]
294 | )
295 | if velocity is not None:
296 | segment["velocity"] = velocity
297 | segment["Velocity / km/h"] = 3.6 * segment["velocity"]
298 | # Pace in min / km, as float
299 | idx = np.where(segment["velocity"] > 0)[0]
300 | segment["pace"] = np.zeros_like(segment["velocity"])
301 | segment["pace"][idx] = 1.0 / ((60.0 / 1000) * segment["velocity"][idx])
302 | # Add velocity levels:
303 | levels = cluster_velocities(segment["velocity"])
304 | if levels is not None:
305 | segment["velocity-level"] = levels
306 |
307 |
308 | def process_segment(segment, max_heart_rate=187):
309 | """Add derived properties to the given segment.
310 |
311 | Parameters
312 | ----------
313 | segment : dict
314 | The raw data from the gpx file.
315 | max_heart_rate : float, optional
316 | The maximum heart rate, used for the calculation of
317 | heart rate zones.
318 |
319 | """
320 | segment["latlon"] = list(zip(segment["lat"], segment["lon"]))
321 | # Process time information:
322 | if "time" in segment:
323 | time_zero = segment["time"][0]
324 | time_delta = [i - time_zero for i in segment["time"]]
325 | segment["elapsed-time"] = np.array(
326 | [i.total_seconds() for i in time_delta],
327 | )
328 | # Calculate distance:
329 | segment["distance"] = np.array(
330 | get_distances(segment["lat"], segment["lon"])
331 | )
332 | segment["Distance / km"] = segment["distance"] / 1000.0
333 | # Estimate velocity:
334 | if "distance" in segment and "elapsed-time" in segment:
335 | get_velocity(segment)
336 | # Add hr metrics:
337 | if "hr" in segment:
338 | update_hr_zones(segment, max_heart_rate=max_heart_rate)
339 | if "elapsed-time" in segment:
340 | delta_time = (
341 | segment["elapsed-time"][-1] - segment["elapsed-time"][0]
342 | )
343 | segment["average-hr"] = (
344 | np.trapz(segment["hr"], segment["elapsed-time"]) / delta_time
345 | )
346 | # Add elevation metrics:
347 | if "elevation" in segment:
348 | ele_diff = np.diff(segment["elevation"])
349 | segment["elevation-up"] = sum(ele_diff[np.where(ele_diff > 0)[0]])
350 | segment["elevation-down"] = sum(ele_diff[np.where(ele_diff < 0)[0]])
351 | # Add alias:
352 | if "hr" in segment:
353 | segment["heart rate"] = segment["hr"]
354 |
355 |
356 | def read_gpx_file(gpxfile, max_heart_rate=187):
357 | """Read data from a given gpx file.
358 |
359 | Parameters
360 | ----------
361 | gpxfile : string
362 | The file to open and read.
363 | max_heart_rate : integer (or float)
364 | The heart rate, used in calculation of heart rate zones.
365 |
366 | Yields
367 | ------
368 | out : dict
369 | A dictionary containing the data read from each track in the
370 | file.
371 |
372 | """
373 | gpx = minidom.parse(gpxfile)
374 | tracks = gpx.getElementsByTagName("trk")
375 | for track in tracks:
376 | raw_segments = track.getElementsByTagName("trkseg")
377 | track_data = {
378 | "name": _get_gpx_text(track, "name"),
379 | "type": _get_gpx_text(track, "type"),
380 | "segments": [read_segment(i) for i in raw_segments],
381 | }
382 | # Add some more processed data for segments
383 | for segment in track_data["segments"]:
384 | process_segment(segment, max_heart_rate=max_heart_rate)
385 | yield track_data
386 |
--------------------------------------------------------------------------------
/gpxplotter/mplplotting.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """This module defines methods for plotting GPX data using matplotlib."""
4 | import datetime
5 | import warnings
6 | from math import ceil, floor
7 |
8 | import matplotlib.dates as mdates
9 | import matplotlib.patches as mpatches
10 | import numpy as np
11 | from matplotlib import pyplot as plt
12 | from matplotlib.collections import LineCollection, PolyCollection
13 | from matplotlib.colors import BoundaryNorm, LinearSegmentedColormap, Normalize
14 |
15 | from gpxplotter.common import RELABEL, format_time_delta
16 |
17 | ZONE_COLORS_0 = {
18 | 1: "#ffffcc",
19 | 2: "#a1dab4",
20 | 3: "#41b6c4",
21 | 4: "#2c7fb8",
22 | 5: "#253494",
23 | }
24 | ZONE_COLORS_1 = {
25 | 1: "#e41a1c",
26 | 2: "#377eb8",
27 | 3: "#4daf4a",
28 | 4: "#984ea3",
29 | 5: "#ff7f00",
30 | }
31 | ZONE_COLORS = {
32 | 0: "#bcbddc",
33 | 1: "#9e9ac8",
34 | }
35 |
36 |
37 | def _select_cmap(zdata, cmap_name):
38 | """Select a colormap and determine the number of colors.
39 |
40 | Parameters
41 | ----------
42 | zdata ; array_like
43 | The data used for creating the color map.
44 | cmap_name : string
45 | The name of the color map to use.
46 |
47 | Returns
48 | -------
49 | cmap : object like :py:class:`matplotlib.colors.ListedColormap`
50 | The created color map.
51 | norm : object like :py:class:`matplotlib.colors.Normalize`
52 | The created normalization for the data.
53 |
54 | """
55 | uniqz = len(set(zdata))
56 | if uniqz > 10:
57 | cmap = plt.colormaps.get_cmap(cmap_name)
58 | norm = Normalize(vmin=floor(min(zdata)), vmax=ceil(max(zdata)))
59 | else:
60 | base_cmap = plt.colormaps.get_cmap(cmap_name)
61 | new_colors = base_cmap(np.linspace(0, 1, uniqz))
62 | cmap = LinearSegmentedColormap.from_list(
63 | f"{cmap_name}_{uniqz}", new_colors, N=uniqz
64 | )
65 | boundaries = list(sorted(set(zdata)))
66 | boundaries = boundaries + [max(boundaries) + 1]
67 | norm = BoundaryNorm(boundaries, uniqz, clip=True)
68 | return cmap, norm
69 |
70 |
71 | def make_patches(xdata, ydata, zdata, cmap_name="viridis"):
72 | """Make some patches for multi-coloring the area under a curve.
73 |
74 | Parameters
75 | ----------
76 | xdata : list or array_like
77 | The x positions for the curve.
78 | ydata : list or array_like
79 | The y positions for the curve.
80 | zdata : list or array_like
81 | A list of values associated with each point, used for
82 | coloring.
83 | cmap_name : string, optional
84 | The name of the color map to use.
85 |
86 | Returns
87 | -------
88 | poly : object like :py:class:`matplotlib.collections.PolyCollection`
89 | The polygons created here, with individual colors.
90 | cmap : object like :py:class:`matplotlib.colors.ListedColormap`
91 | The created color map.
92 | norm : object like :py:class:`matplotlib.colors.Normalize`
93 | The created normalization for the data.
94 |
95 | """
96 | cmap, norm = _select_cmap(zdata, cmap_name)
97 | verts = []
98 | for i, (xval, yval) in enumerate(zip(xdata, ydata)):
99 | if i == 0:
100 | xnext = 0.5 * (xdata[i + 1] + xval)
101 | ynext = 0.5 * (ydata[i + 1] + yval)
102 | verts.append([[xval, 0], [xval, yval], [xnext, ynext], [xnext, 0]])
103 | elif i == len(xdata) - 1:
104 | xprev = 0.5 * (xval + xdata[i - 1])
105 | yprev = 0.5 * (yval + ydata[i - 1])
106 | verts.append([[xprev, 0], [xprev, yprev], [xval, yval], [xval, 0]])
107 | else:
108 | xnext = 0.5 * (xdata[i + 1] + xval)
109 | ynext = 0.5 * (ydata[i + 1] + yval)
110 | xprev = 0.5 * (xval + xdata[i - 1])
111 | yprev = 0.5 * (yval + ydata[i - 1])
112 | verts.append(
113 | [
114 | [xprev, 0],
115 | [xprev, yprev],
116 | [xval, yval],
117 | [xnext, ynext],
118 | [xnext, 0],
119 | ]
120 | )
121 | poly = PolyCollection(verts, cmap=cmap, norm=norm)
122 | poly.set_array(zdata)
123 | return poly, cmap, norm
124 |
125 |
126 | def _make_time_labels(delta_seconds, nlab=5):
127 | """Make n time-formatted labels for data in seconds."""
128 | label_pos = np.linspace(
129 | min(delta_seconds), max(delta_seconds), nlab, dtype=np.int_
130 | )
131 | label_lab = format_time_delta(label_pos)
132 | return label_pos, label_lab
133 |
134 |
135 | def set_up_figure(track):
136 | """Help with creating a figure.
137 |
138 | This method will just create the figure and axis and
139 | set the title.
140 |
141 | Parameters
142 | ----------
143 | track : dict
144 | The track we are creating a figure for.
145 |
146 |
147 | Returns
148 | -------
149 | fig: object like :py:class:`matplotlib.figure.Figure`
150 | The figure created here.
151 | axi : object like :py:class:`matplotlib.axes.Axes`
152 | The axis created here
153 |
154 | """
155 | fig, ax1 = plt.subplots(constrained_layout=True)
156 | track_name, track_type = None, None
157 | try:
158 | track_name = track["name"][0]
159 | except (IndexError, KeyError):
160 | track_name = None
161 |
162 | try:
163 | track_type = track["type"][0]
164 | except (IndexError, KeyError):
165 | track_type = None
166 |
167 | if track_name is not None:
168 | if track_type is None:
169 | ax1.set_title(f"{track_name}")
170 | else:
171 | ax1.set_title(f"{track_name}: {track_type}")
172 | return fig, ax1
173 |
174 |
175 | def add_regions(axi, xdata, ydata, regions, cut):
176 | """Add heart rate patches to axis.
177 |
178 | Parameters
179 | ----------
180 | axi : object like :py:class:`matplotlib.axes.Axes`
181 | The axes to add regions to.
182 | xdata : array_like
183 | The x-values we are plotting for.
184 | ydata : array_like
185 | The y-values we are plotting for.
186 | regions : list of lists
187 | regions[i] defines a heart rate region as [start, stop, hr-region].
188 | cut : integer, optional
189 | If given, the zones will be divided into smaller (inclusive) or
190 | larger than the given cut.
191 |
192 | """
193 | legends, handles = [], []
194 | if cut is None:
195 | for i in regions:
196 | xpos = xdata[i[0] : i[1] + 1]
197 | ypos = ydata[i[0] : i[1] + 1]
198 | axi.fill_between(
199 | xpos, min(ydata), ypos, alpha=1.0, color=ZONE_COLORS_0[i[2]]
200 | )
201 | for i in range(1, 6):
202 | patch = mpatches.Patch(color=ZONE_COLORS_0[i])
203 | legend = f"Zone = {i}"
204 | handles.append(patch)
205 | legends.append(legend)
206 | else:
207 | for i in regions:
208 | xpos = xdata[i[0] : i[1] + 1]
209 | ypos = ydata[i[0] : i[1] + 1]
210 | idx = 0 if i[2] <= cut else 1
211 | axi.fill_between(
212 | xpos, min(ydata), ypos, alpha=1.0, color=ZONE_COLORS[idx]
213 | )
214 | handles.append(mpatches.Patch(color=ZONE_COLORS[0]))
215 | legends.append(rf"Zone $\leq$ {cut}")
216 | handles.append(mpatches.Patch(color=ZONE_COLORS[1]))
217 | legends.append(f"Zone > {cut}")
218 | axi.legend(handles, legends)
219 |
220 |
221 | def _get_data(data, key):
222 | """Attempt to read a key from a dictionary.
223 |
224 | This method is here to give some more instructive error messages.
225 |
226 | Parameters
227 | ----------
228 | data : dict
229 | The dictionary to read from.
230 | key : string
231 | The key we attempt to read from the dictionary.
232 |
233 | Returns
234 | -------
235 | out : any
236 | The values returned by ``data[key]``.
237 | """
238 | kdata = None
239 | try:
240 | kdata = data[key]
241 | except KeyError as error:
242 | msg = f'Requested "{key}" not found in data!' f"\nValid: {data.keys()}"
243 | raise Exception(msg) from error
244 | return kdata
245 |
246 |
247 | def _keys_are_present(data, *keys):
248 | all_good = True
249 | for key in keys:
250 | if key is None:
251 | continue
252 | if key not in data:
253 | warnings.warn(f'"{key}" not found in the segment. Ending plot.')
254 | all_good = False
255 | return all_good
256 |
257 |
258 | def add_segmented_line(xdata, ydata, zdata, cmap_name="viridis"):
259 | """Create multicolored line.
260 |
261 | Create a multicolored line, colored according to the provided
262 | ``zdata``-values.
263 |
264 | Parameters
265 | ----------
266 | xdata : array_like
267 | x-positions to use.
268 | ydata : array_like
269 | y-positions to use.
270 | zdata : array_like
271 | Values to use for coloring the line segments.
272 | cmap_name : string, optional
273 | Colormap to use for the colors.
274 |
275 | Returns
276 | -------
277 | out : object like :py:class:`matplotlib.collections.LineCollection`
278 | The multicolored lines.
279 |
280 | Note
281 | ----
282 | https://matplotlib.org/stable/gallery/lines_bars_and_markers/multicolored_line.html
283 |
284 | """
285 | cmap, norm = _select_cmap(zdata, cmap_name)
286 | points = np.array([xdata, ydata]).T.reshape(-1, 1, 2)
287 | segments = np.concatenate([points[:-1], points[1:]], axis=1)
288 | lines = LineCollection(segments, cmap=cmap, norm=norm)
289 | lines.set_array(zdata)
290 | return lines
291 |
292 |
293 | def _update_limits(axi, data, which="x", factor=0.025):
294 | """Update limits for axes (x or y).
295 |
296 | This method will lengthen the given axes.
297 |
298 | Parameters
299 | ----------
300 | axi : object like :py:class:`matplotlib.axes.Axes`
301 | The axes to update for.
302 | data : array_like
303 | The data we are plotting on the given axes.
304 | which : string, optional
305 | Determines if we are updating the x or y-axes.
306 | factor : float, optional
307 | Half of the factor of the current length to add.
308 |
309 | """
310 | length = abs(data.max() - data.min())
311 | if which == "x":
312 | axi.set_xlim(
313 | data.min() - length * factor, data.max() + length * factor
314 | )
315 | elif which == "y":
316 | axi.set_ylim(
317 | data.min() - length * factor, data.max() + length * factor
318 | )
319 | else:
320 | pass
321 |
322 |
323 | def _add_elapsed_labels(axi, data, which="x"):
324 | """Add nicer labels for time-difference.
325 |
326 | Convert elapsed time in seconds to hours:minutes:seconds.
327 |
328 | Parameters
329 | ----------
330 | axi : object like :py:class:`matplotlib.axes.Axes`
331 | The axes to add ticks for.
332 | data : array_like
333 | The data we are updating.
334 | which : string, optional
335 | Selects the axes (x or y) we are updating.
336 |
337 | """
338 | label_pos, label_lab = _make_time_labels(data, 5)
339 | if which == "x":
340 | axi.set_xticks(label_pos)
341 | axi.set_xticklabels(label_lab, rotation=25)
342 | axi.set_xlabel("Time")
343 | elif which == "y":
344 | axi.set_yticks(label_pos)
345 | axi.set_yticklabels(label_lab)
346 | axi.set_ylabel("Time")
347 |
348 |
349 | def _shift_elapsed_labels(axi, start_time, which="x"):
350 | """Shift elapsed labels with a given time origin.
351 |
352 | Make a time difference start at a given time.
353 |
354 | Parameters
355 | ----------
356 | axi : object like :py:class:`matplotlib.axes.Axes`
357 | The axes to add ticks for.
358 | start_time : object like :py:class:`datetime.datetime`
359 | The starting time to use for shifting.
360 | which : string, optional
361 | Selects the axes (x or y) we are updating.
362 |
363 | """
364 | if which == "x":
365 | ticks = axi.get_xticks()
366 | elif which == "y":
367 | ticks = axi.get_yticks()
368 | else:
369 | ticks = []
370 | seconds = [datetime.timedelta(seconds=int(i)) for i in ticks]
371 | time = [start_time + i for i in seconds]
372 | time_lab = [i.strftime("%H:%M:%S") for i in time]
373 | if which == "x":
374 | axi.set_xticklabels(time_lab)
375 | elif which == "y":
376 | axi.set_yticklabels(time_lab)
377 |
378 |
379 | def _update_time_ticklabels(axi, xvar, yvar, xdata, ydata):
380 | """Update time tick labels for time data.
381 |
382 | Parameters
383 | ----------
384 | axi : object like :py:class:`matplotlib.axes.Axes`
385 | The axes to add ticks for.
386 | xvar : string
387 | The variable used for the x-axis.
388 | yvar : string
389 | The variable used for the y-axis.
390 | xdata : array_like
391 | The data used for the x-axis.
392 | ydata : array_like
393 | The data used for the y-axis.
394 |
395 | """
396 | fmt = mdates.DateFormatter("%H:%M:%S")
397 | if xvar == "elapsed-time":
398 | _add_elapsed_labels(axi, xdata, which="x")
399 | elif xvar in ("time",):
400 | axi.xaxis.set_major_formatter(fmt)
401 | axi.tick_params(axis="x", rotation=25)
402 | if yvar == "elapsed-time":
403 | _add_elapsed_labels(axi, ydata, which="y")
404 | elif yvar in ("time",):
405 | axi.yaxis.set_major_formatter(fmt)
406 |
407 |
408 | def fix_elapsed_time(axi, var, data_axes, data_plot, which="x"):
409 | """For labels for time when elapsed time is used in plotting.
410 |
411 | For coloring plots, the elapsed time data is used for making lines
412 | or polygons. This method will shift the labels back to the original
413 | variable.
414 |
415 | Parameters
416 | ----------
417 | axi : object like :py:class:`matplotlib.axes.Axes`
418 | The axes to add ticks for.
419 | var : string
420 | The variable used for the axis.
421 | data_axes : array_like
422 | The data we are to use for making labels.
423 | data_plot : array_like
424 | The actual data used for plotting.
425 | which : string, optional
426 | Selects the axes (x or y) we are updating.
427 |
428 | """
429 | if var in ("time", "elapsed-time"):
430 | _add_elapsed_labels(axi, data_plot, which=which)
431 | if var == "time":
432 | _shift_elapsed_labels(axi, data_axes[0], which=which)
433 |
434 |
435 | def plot_line(
436 | track,
437 | data,
438 | xvar="distance",
439 | yvar="elevation",
440 | zvar=None,
441 | cmap="viridis",
442 | **kwargs,
443 | ):
444 | """Plot line data from a segment.
445 |
446 | Plot a given segment from a track as a line. The line
447 | can be colored according to a given value.
448 |
449 | Parameters
450 | ----------
451 | track : dict
452 | The track we are plotting for.
453 | data : dict
454 | The segment we are plotting.
455 | xvar : string, optional
456 | Selects the variable to use for the x-axes.
457 | yvar : string, optional
458 | Selects the variable to use for the y-axes.
459 | zvar : string, optional
460 | Selects the variable to use for coloring the line.
461 | cmap : string, optional
462 | Color map to use for the coloring
463 | **kwargs : :py:class:`matplotlib.lines.Line2D` properties, optional
464 | Extra properties for the plotting passed to the ``axi.plot``
465 | method.
466 |
467 | Returns
468 | -------
469 | fig: object like :py:class:`matplotlib.figure.Figure`
470 | The figure created here.
471 | ax1 : object like :py:class:`matplotlib.axes.Axes`
472 | The axes to add ticks for.
473 |
474 | """
475 | if not _keys_are_present(data, xvar, yvar, zvar):
476 | return None, None
477 | fig, ax1 = set_up_figure(track)
478 | xdata = _get_data(data, xvar)
479 | ydata = _get_data(data, yvar)
480 | ax1.set(xlabel=RELABEL.get(xvar, xvar), ylabel=RELABEL.get(yvar, yvar))
481 | if zvar is None:
482 | ax1.plot(xdata, ydata, **kwargs)
483 | _update_time_ticklabels(ax1, xvar, yvar, xdata, ydata)
484 | else:
485 | zdata = _get_data(data, zvar)
486 | # For time, use the elapsed-time for making the segmented line
487 | if xvar in ("time",):
488 | xdata = _get_data(data, "elapsed-time")
489 | if yvar in ("time",):
490 | ydata = _get_data(data, "elapsed-time")
491 | lines = add_segmented_line(xdata, ydata, zdata, cmap_name=cmap)
492 | lines.set_linewidth(kwargs.get("lw", 3))
493 | line = ax1.add_collection(lines)
494 | _update_limits(ax1, xdata, which="x")
495 | _update_limits(ax1, ydata, which="y")
496 | cbar = fig.colorbar(line, ax=ax1)
497 | cbar.set_label(RELABEL.get(zvar, zvar))
498 | # Shift back for time:
499 | fix_elapsed_time(ax1, xvar, _get_data(data, xvar), xdata, which="x")
500 | fix_elapsed_time(ax1, yvar, _get_data(data, yvar), ydata, which="y")
501 | return fig, ax1
502 |
503 |
504 | def plot_filled(
505 | track,
506 | data,
507 | xvar="distance",
508 | yvar="elevation",
509 | zvar="hr",
510 | cmap="viridis",
511 | cut=None,
512 | **kwargs,
513 | ):
514 | """Plot a filled graph (line with colored area).
515 |
516 | Plot a line and fill the area under it, given a specified variable.
517 |
518 | Parameters
519 | ----------
520 | track : dict
521 | The track we are plotting for.
522 | data : dict
523 | The segment we are plotting.
524 | xvar : string, optional
525 | Selects the variable to use for the x-axes.
526 | yvar : string, optional
527 | Selects the variable to use for the y-axes.
528 | zvar : string, optional
529 | Selects the variable to use for coloring the area.
530 | cmap : string, optional
531 | Color map to use for the coloring
532 | cut : integer, optional
533 | If given and if we are plotting hr-regions, this will divide
534 | the coloring into two different groups (see `.add_regions`).
535 | **kwargs : :py:class:`matplotlib.lines.Line2D` properties, optional
536 | Extra properties for the plotting passed to the ``axi.plot``
537 | method.
538 |
539 | Returns
540 | -------
541 | fig: object like :py:class:`matplotlib.figure.Figure`
542 | The figure created here.
543 | ax1 : object like :py:class:`matplotlib.axes.Axes`
544 | The axes to add ticks for.
545 |
546 | """
547 | if not _keys_are_present(data, xvar, yvar, zvar):
548 | return None, None
549 | fig, ax1 = set_up_figure(track)
550 | xdata = _get_data(data, xvar)
551 | ydata = _get_data(data, yvar)
552 | zdata = _get_data(data, zvar)
553 | ax1.set(xlabel=RELABEL.get(xvar, xvar), ylabel=RELABEL.get(yvar, yvar))
554 | ax1.plot(xdata, ydata, **kwargs)
555 |
556 | if zvar == "hr-regions":
557 | add_regions(ax1, xdata, ydata, data[zvar], cut)
558 | _update_time_ticklabels(ax1, xvar, yvar, xdata, ydata)
559 | else:
560 | # For time, use the elapsed-time for making the filled plot
561 | if xvar in ("time",):
562 | xdata = _get_data(data, "elapsed-time")
563 | if yvar in ("time",):
564 | ydata = _get_data(data, "elapsed-time")
565 | poly, _, _ = make_patches(
566 | xdata,
567 | ydata,
568 | zdata,
569 | cmap_name=cmap,
570 | )
571 | col = ax1.add_collection(poly)
572 | _update_limits(ax1, xdata, which="x")
573 | _update_limits(ax1, ydata, which="y")
574 | cbar = fig.colorbar(col, ax=ax1)
575 | cbar.set_label(RELABEL.get(zvar, zvar))
576 | # Shift labels for time:
577 | fix_elapsed_time(ax1, xvar, _get_data(data, xvar), xdata, which="x")
578 | fix_elapsed_time(ax1, yvar, _get_data(data, yvar), ydata, which="y")
579 | return fig, ax1
580 |
--------------------------------------------------------------------------------
/gpxplotter/version.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2020, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """Version information for gpxplotter.
4 |
5 | This file is generated by gpxplotter (``setup_version.py``).
6 | """
7 | SHORT_VERSION = '0.2.12'
8 | VERSION = '0.2.12'
9 | FULL_VERSION = '0.2.12'
10 | GIT_REVISION = '90beb74d813c24f1074d26c6e78f25806a823429'
11 | GIT_VERSION = '0.2.12'
12 | RELEASE = True
13 |
14 | if not RELEASE:
15 | VERSION = GIT_VERSION
16 |
--------------------------------------------------------------------------------
/make_package.sh:
--------------------------------------------------------------------------------
1 | rm dist/*
2 | python setup_version.py
3 | python setup.py sdist bdist_wheel
4 | twine upload dist/*
5 |
--------------------------------------------------------------------------------
/pypireadme.md:
--------------------------------------------------------------------------------
1 | # gpxplotter
2 |
3 | [](https://gpxplotter.readthedocs.io/en/latest/?badge=latest)
4 | [](https://mybinder.org/v2/gh/andersle/gpxplotter/main?filepath=examples%2Fjupyter%2F)
5 |
6 | **gpxplotter** is a Python package for reading
7 | [gpx](https://en.wikipedia.org/wiki/GPS_Exchange_Format)
8 | files and creating simple predefined plots using
9 | [matplotlib](http://matplotlib.org/)
10 | and maps using
11 | [folium](https://python-visualization.github.io/folium/).
12 |
13 | Please see
14 | [https://gpxplotter.readthedocs.io/en/latest/](https://gpxplotter.readthedocs.io/en/latest/)
15 | for the latest documentation and the
16 | [Binder notebooks](https://mybinder.org/v2/gh/andersle/gpxplotter/main?filepath=examples%2Fjupyter%2F) for examples.
17 |
18 | ## Installation
19 |
20 | ```
21 | pip install gpxplotter
22 | ```
23 |
24 | ## Examples
25 |
26 | Interactive examples can be explored
27 | via [Binder](https://mybinder.org/v2/gh/andersle/gpxplotter/main?filepath=examples%2Fjupyter%2F).
28 |
29 |
30 | #### Simple example for showing a track in a map, colored by heart rate
31 |
32 | ```python
33 |
34 | from gpxplotter import read_gpx_file, create_folium_map, add_segment_to_map
35 |
36 | the_map = create_folium_map()
37 | for track in read_gpx_file('ruten.gpx'):
38 | for i, segment in enumerate(track['segments']):
39 | add_segment_to_map(the_map, segment, color_by='hr')
40 |
41 | # To display the map in a Jupyter notebook:
42 | the_map
43 | ```
44 |
45 | ### Further examples
46 |
47 | Please see the [gallery in the documentation](https://gpxplotter.readthedocs.io/en/latest/auto_examples/index.html)
48 | for further examples.
49 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | altair
2 | branca
3 | folium
4 | jinja2
5 | matplotlib
6 | numpy
7 | pandas
8 | Pillow
9 | python-dateutil
10 | seaborn
11 | scipy
12 | scikit-learn
13 | sphinx
14 | sphinx_rtd_theme
15 | sphinx-gallery
16 | vincent
17 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | gpxplotter - A library for reading gpx files and making some simple plots.
5 | Copyright (C) 2021, Anders Lervik.
6 |
7 | This file is part of gpxplotter.
8 |
9 | gpxplotter is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU Lesser General Public License as published by
11 | the Free Software Foundation, either version 2.1 of the License, or
12 | (at your option) any later version.
13 |
14 | gpxplotter is distributed in the hope that it will be useful,
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | GNU Lesser General Public License for more details.
18 |
19 | You should have received a copy of the GNU Lesser General Public License
20 | along with gpxplotter. If not, see
21 | """
22 | import ast
23 | import pathlib
24 | from setuptools import setup, find_packages
25 |
26 |
27 | GITHUB = 'https://github.com/andersle/gpxplotter'
28 | DOCS = 'https://gpxplotter.readthedocs.io/en/latest'
29 |
30 | FULL_VERSION = '0.2.12' # Automatically set by setup_version.py
31 |
32 |
33 | def get_long_description():
34 | """Return the contents of the README.md file."""
35 | # Get the long description from the README file
36 | long_description = ''
37 | here = pathlib.Path(__file__).absolute().parent
38 | readme = here.joinpath('pypireadme.md')
39 | with open(readme, 'r') as fileh:
40 | long_description = fileh.read()
41 | return long_description
42 |
43 |
44 | def get_version():
45 | """Return the version from version.py as a string."""
46 | here = pathlib.Path(__file__).absolute().parent
47 | filename = here.joinpath('gpxplotter', 'version.py')
48 | with open(filename, 'r') as fileh:
49 | for lines in fileh:
50 | if lines.startswith('FULL_VERSION ='):
51 | version = ast.literal_eval(lines.split('=')[1].strip())
52 | return version
53 | return FULL_VERSION
54 |
55 |
56 | def get_requirements():
57 | """Read requirements.txt and return a list of requirements."""
58 | here = pathlib.Path(__file__).absolute().parent
59 | requirements = []
60 | filename = here.joinpath('requirements.txt')
61 | with open(filename, 'r') as fileh:
62 | for lines in fileh:
63 | requirements.append(lines.strip())
64 | return requirements
65 |
66 |
67 | setup(
68 | name='gpxplotter',
69 | version=get_version(),
70 | description='A package for reading gpx files and creating simple plots',
71 | long_description=get_long_description(),
72 | long_description_content_type='text/markdown',
73 | url=GITHUB,
74 | author='Anders Lervik',
75 | author_email='andersle@gmail.com',
76 | license='LGPLv2.1+',
77 | classifiers=[
78 | 'Development Status :: 3 - Alpha',
79 | 'Environment :: Console',
80 | 'Intended Audience :: End Users/Desktop',
81 | ('License :: OSI Approved :: '
82 | 'GNU Lesser General Public License v2 or later (LGPLv2+)'),
83 | 'Natural Language :: English',
84 | 'Operating System :: MacOS :: MacOS X',
85 | 'Operating System :: POSIX',
86 | 'Programming Language :: Python :: 3',
87 | 'Programming Language :: Python :: 3.8',
88 | 'Programming Language :: Python :: 3.9',
89 | 'Programming Language :: Python :: 3.10',
90 | 'Topic :: Other/Nonlisted Topic',
91 | ],
92 | keywords='gpx gps',
93 | packages=find_packages(),
94 | install_requires=get_requirements(),
95 | )
96 |
--------------------------------------------------------------------------------
/setup_version.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2021, Anders Lervik.
2 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
3 | """
4 | gpxplotter - A library for reading GPX files and make some simple plots.
5 | Copyright (C) 2021, Anders Lervik.
6 |
7 | This file generates the version info.
8 | """
9 | import argparse
10 | import json
11 | import os
12 | import pathlib
13 | import subprocess
14 |
15 |
16 | # Path to file containing current version info:
17 | CURRENT_VERSION_FILE = pathlib.Path('version.json')
18 | # Path to file with version info accessible by gpxplotter:
19 | VERSION_FILE = pathlib.Path('gpxplotter').joinpath('version.py')
20 | # Path to the setup.py file:
21 | SETUP_PY = pathlib.Path('setup.py')
22 | # Format for versions:
23 | VERSION_DEV_FMT = '{major:d}.{minor:d}.{micro:d}.dev{dev:d}'
24 | VERSION_FMT = '{major:d}.{minor:d}.{micro:d}'
25 | # Format for version.py
26 | VERSION_TXT = '''# Copyright (c) 2020, Anders Lervik.
27 | # Distributed under the LGPLv2.1+ License. See LICENSE for more info.
28 | """Version information for gpxplotter.
29 |
30 | This file is generated by gpxplotter (``setup_version.py``).
31 | """
32 | SHORT_VERSION = '{version:s}'
33 | VERSION = '{version:s}'
34 | FULL_VERSION = '{full_version:s}'
35 | GIT_REVISION = '{git_revision:s}'
36 | GIT_VERSION = '{git_version:s}'
37 | RELEASE = {release:}
38 |
39 | if not RELEASE:
40 | VERSION = GIT_VERSION
41 | '''
42 |
43 |
44 | def generate_version_string(version):
45 | """Generate a string with the current gpxplotter version.
46 |
47 | Parameters
48 | ----------
49 | version : dict
50 | A dict containing the current gpxplotter version.
51 |
52 | Returns
53 | -------
54 | version_txt : string
55 | A string with the current gpxplotter version.
56 |
57 | """
58 | version_fmt = VERSION_FMT if version['release'] else VERSION_DEV_FMT
59 | return version_fmt.format(**version)
60 |
61 |
62 | def get_git_version():
63 | """Obtain the git revision as a string.
64 |
65 | This method is adapted from Numpy's setup.py
66 |
67 | Returns
68 | -------
69 | git_revision : string
70 | The git revision, it the git revision could not be determined,
71 | a 'Unknown' will be returned.
72 |
73 | """
74 | git_revision = 'Unknown'
75 | try:
76 | env = {}
77 | for key in ('SYSTEMROOT', 'PATH'):
78 | val = os.environ.get(key)
79 | if val is not None:
80 | env[key] = val
81 | # LANGUAGE is used on win32
82 | env['LANGUAGE'] = 'C'
83 | env['LANG'] = 'C'
84 | env['LC_ALL'] = 'C'
85 | out = subprocess.Popen(['git', 'rev-parse', 'HEAD'],
86 | stdout=subprocess.PIPE,
87 | env=env).communicate()[0]
88 | git_revision = out.strip().decode('ascii')
89 | except OSError:
90 | git_revision = 'Unknown'
91 | return git_revision
92 |
93 |
94 | def get_version_info(version):
95 | """Return the version number for gpxplotter.
96 |
97 | This method is adapted from Numpy's setup.py.
98 |
99 | Parameters
100 | ----------
101 | version : dict
102 | The current version information.
103 |
104 | Returns
105 | -------
106 | full_version : string
107 | The full version string for this release.
108 | git_revision : string
109 | The git revision number.
110 |
111 | """
112 | version_txt = generate_version_string(version)
113 | if pathlib.Path('.git').is_dir():
114 | git_revision = get_git_version()
115 | elif pathlib.Path(VERSION_FILE).is_file():
116 | try:
117 | from gpxplotter.version import git_revision
118 | except ImportError as error:
119 | msg = (
120 | 'Unable to import git_revision. Try removing '
121 | 'gpxplotter/version.py and the build directory '
122 | 'before building.'
123 | )
124 | raise Exception(msg) from error
125 | else:
126 | git_revision = 'Unknown'
127 | if not version['release']:
128 | git_version = ''.join(
129 | [
130 | version_txt.split('dev')[0],
131 | 'dev{:d}+'.format(version['dev']),
132 | git_revision[:7]
133 | ]
134 | )
135 | else:
136 | git_version = version_txt
137 | full_version = version_txt
138 | return full_version, git_revision, git_version
139 |
140 |
141 | def write_version_py(version):
142 | """Create a file with the version info for gpxplotter.
143 |
144 | This method is adapted from Numpy's setup.py.
145 |
146 | Parameters
147 | ----------
148 | version : dict
149 | The dict containing the current version information.
150 |
151 | Returns
152 | -------
153 | full_version : string
154 | The current full version for gpxplotter.
155 |
156 | """
157 | full_version, git_revision, git_version = get_version_info(version)
158 | version_txt = VERSION_TXT.format(
159 | version=full_version,
160 | full_version=full_version,
161 | git_revision=git_revision,
162 | git_version=git_version,
163 | release=version['release'],
164 | )
165 | with open(VERSION_FILE, 'wt') as vfile:
166 | vfile.write(version_txt)
167 | return full_version
168 |
169 |
170 | def write_version_in_setup_py(version):
171 | """Update version for setup.py."""
172 | tmp = []
173 | comment = '# Automatically set by setup_version.py'
174 | with open(SETUP_PY, 'r') as sfile:
175 | for lines in sfile:
176 | if lines.startswith('FULL_VERSION ='):
177 | tmp.append(
178 | ("FULL_VERSION = '{}' {}\n".format(version, comment))
179 | )
180 | else:
181 | tmp.append(lines)
182 | with open(SETUP_PY, 'wt') as sfile:
183 | for lines in tmp:
184 | sfile.write(lines)
185 |
186 |
187 | def bump_version(args, version):
188 | """Increment the version number if requested.
189 |
190 | Parameters
191 | ----------
192 | args : object like argparse.Namespace
193 | The arguments determining if we are to bump the version number.
194 | version : dict
195 | The current version.
196 |
197 | Returns
198 | -------
199 | new_version : dict
200 | The updated version (if an update is requested). Otherwise it
201 | is just a copy of the input version.
202 |
203 | """
204 | new_version = version.copy()
205 | if args.bump_dev:
206 | new_version['dev'] += 1
207 | if args.bump_micro:
208 | new_version['micro'] += 1
209 | new_version['dev'] = 0
210 | if args.bump_minor:
211 | new_version['minor'] += 1
212 | new_version['micro'] = 0
213 | new_version['dev'] = 0
214 | if args.bump_major:
215 | new_version['major'] += 1
216 | new_version['minor'] = 0
217 | new_version['micro'] = 0
218 | new_version['dev'] = 0
219 | return new_version
220 |
221 |
222 | def main(args):
223 | """Generate version information and update the relevant files."""
224 | version = {}
225 | with open(CURRENT_VERSION_FILE, 'r') as json_file:
226 | version = json.load(json_file)
227 | version = bump_version(args, version)
228 | full_version = write_version_py(version)
229 | print('Setting version to: {}'.format(full_version))
230 | write_version_in_setup_py(full_version)
231 | with open(CURRENT_VERSION_FILE, 'w') as json_file:
232 | json.dump(version, json_file, indent=4)
233 |
234 |
235 | def get_argument_parser():
236 | """Return a parser for arguments."""
237 | parser = argparse.ArgumentParser()
238 | parser.add_argument(
239 | '--bump_major',
240 | action='store_true',
241 | help='Increment the major version.'
242 | )
243 | parser.add_argument(
244 | '--bump_minor',
245 | action='store_true',
246 | help='Increment the minor version.'
247 | )
248 | parser.add_argument(
249 | '--bump_micro',
250 | action='store_true',
251 | help='Increment the micro version.'
252 | )
253 | parser.add_argument(
254 | '--bump_dev',
255 | action='store_true',
256 | help='Increment the development version.'
257 | )
258 | return parser
259 |
260 |
261 | if __name__ == '__main__':
262 | PARSER = get_argument_parser()
263 | main(PARSER.parse_args())
264 |
--------------------------------------------------------------------------------
/version.json:
--------------------------------------------------------------------------------
1 | {
2 | "major": 0,
3 | "minor": 2,
4 | "micro": 12,
5 | "dev": 0,
6 | "release": true
7 | }
--------------------------------------------------------------------------------