├── .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 | [![Documentation Status](https://readthedocs.org/projects/gpxplotter/badge/?version=latest)](https://gpxplotter.readthedocs.io/en/latest/?badge=latest) 4 | [![Binder](https://mybinder.org/badge_logo.svg)](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 | [![map](examples/images/map001.png)](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 | "" 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 | "" 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'{key}', 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'\"{key}\"',\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 | [![Documentation Status](https://readthedocs.org/projects/gpxplotter/badge/?version=latest)](https://gpxplotter.readthedocs.io/en/latest/?badge=latest) 4 | [![Binder](https://mybinder.org/badge_logo.svg)](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 | } --------------------------------------------------------------------------------