├── MANIFEST.in ├── TODO.txt ├── test_files ├── default_schema_locations.gpx ├── unicode2.gpx ├── gpx1.1_with_extensions_without_namespaces.gpx ├── unicode_with_bom_noencoding.gpx ├── gpx1.1_with_extensions.gpx ├── custom_schema_locations.gpx ├── unicode_with_bom.gpx ├── gpx-with-node-with-comments.gpx ├── track_with_dilution_errors.gpx ├── track-with-small-floats.gpx ├── track_with_speed.gpx ├── first_and_last_elevation.gpx ├── unicode.gpx ├── track-with-empty-segment.gpx ├── track-with-extremes.gpx ├── gpx1.0_with_all_fields.gpx ├── route.gpx ├── gpx1.1_with_all_fields.gpx ├── Mojstrovka.gpx ├── cerknicko-without-times.gpx └── cerknicko-jezero-without-elevations.gpx ├── .gitignore ├── .travis.yml ├── makefile ├── NOTICE.txt ├── gpxpy ├── __init__.py ├── gpxxml.py ├── utils.py ├── parser.py ├── geo.py └── gpxfield.py ├── setup.py ├── CHANGELOG.md ├── xsd ├── gpx1.0.txt ├── pretty_print_schemas.py └── gpx1.1.txt ├── CONTRIBUTING.md ├── gpxinfo ├── README.md └── LICENSE.txt /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE.txt NOTICE.txt 2 | include test.py 3 | recursive-include test_files *.gpx 4 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | 1.0 and 1.1 field name ambiguities: 2 | - url/urlame vs ... 3 | - author, author_name, name 4 | + gpx extensions and metadata extensions 5 | - Check generated GPX filex with SAXParser as part of "make test" task 6 | - position_dilution -> positional_dilution 7 | -------------------------------------------------------------------------------- /test_files/default_schema_locations.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test_files/unicode2.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test_files/gpx1.1_with_extensions_without_namespaces.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bbbhhh 5 | 6 | eee 7 | gggiii 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test_files/unicode_with_bom_noencoding.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0.0 5 | bom noencoding ő 6 | bom desc 7 | 8 | 9 | -------------------------------------------------------------------------------- /test_files/gpx1.1_with_extensions.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bbbhhh 5 | 6 | eee 7 | gggiii 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test_files/custom_schema_locations.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test_files/unicode_with_bom.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -14.6 5 | 6 | 0 7 | test 8 | 69.26474016 9 | test desc 10 | 11 | 4 12 | 16.2 13 | 9.3 14 | 18.7 15 | 16 | Open 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test_files/gpx-with-node-with-comments.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 81.000000 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | *.swp 3 | *.swo 4 | tmp/ 5 | tmp_*.py 6 | q.py 7 | tags 8 | xsd/*.xsd 9 | .cache/* 10 | 11 | *.py[cod] 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Packages 17 | *.egg 18 | *.egg-info 19 | dist 20 | build 21 | eggs 22 | parts 23 | bin 24 | var 25 | sdist 26 | develop-eggs 27 | .installed.cfg 28 | lib 29 | lib64 30 | 31 | # Installer logs 32 | pip-log.txt 33 | 34 | # Unit test / coverage reports 35 | .coverage 36 | .tox 37 | nosetests.xml 38 | htmlcov/ 39 | 40 | # Translations 41 | *.mo 42 | 43 | # Mr Developer 44 | .mr.developer.cfg 45 | .project 46 | .pydevproject 47 | 48 | # PyCharm 49 | .idea 50 | 51 | # vscode 52 | .vscode 53 | 54 | pyenv/* 55 | validation_gpx10.gpx 56 | validation_gpx11.gpx 57 | -------------------------------------------------------------------------------- /test_files/track_with_dilution_errors.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 100.1 5 | 101.1 6 | 102.1 7 | 8 | 9 | 200.1 10 | 201.1 11 | 202.1 12 | 13 | 14 | 15 | 16 | 17 | 300.1 18 | 301.1 19 | 302.1 20 | -------------------------------------------------------------------------------- /test_files/track-with-small-floats.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | Untitled Path 11 | 12 | 13 | 10.000000 14 | 15 | 16 | 12.000000 17 | 18 | 19 | 0.000005 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | # Use container-based infrastructure 4 | sudo: false 5 | 6 | # Those without lxml wheels first because building lxml is slow 7 | python: 8 | - 2.7 9 | - 3.4 10 | - 3.5 11 | - 3.6 12 | 13 | env: 14 | - XMLPARSER=LXML 15 | - XMLPARSER=STDLIB 16 | 17 | 18 | install: 19 | # Check whether to install lxml 20 | - if [ "$XMLPARSER" == "LXML" ]; then pip install lxml; fi 21 | 22 | - travis_retry pip install coverage 23 | 24 | 25 | script: 26 | - coverage run --source=gpxpy ./test.py 27 | 28 | after_success: 29 | - pip install coveralls 30 | - coveralls 31 | 32 | after_script: 33 | - coverage report 34 | - pip install pyflakes pycodestyle 35 | - pyflakes . | tee >(wc -l) 36 | - pycodestyle --statistics --count . 37 | 38 | matrix: 39 | fast_finish: true 40 | -------------------------------------------------------------------------------- /test_files/track_with_speed.gpx: -------------------------------------------------------------------------------- 1 | 2 | Serial=1443 3 | 4 | 5 | 6 | 7 | 48.962041 8 | 9 | 1.2 10 | 11 | 12 | 58.166437 13 | 14 | 2.2 15 | 16 | 17 | 53.381747 18 | 19 | 3.2 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | GIT_PORCELAIN_STATUS=$(shell git status --porcelain) 2 | 3 | test: test-py2 test-py3 4 | echo 'OK' 5 | test-py3: 6 | python3 -m unittest test 7 | test-py2: 8 | python -m unittest test 9 | check-all-commited: 10 | if [ -n "$(GIT_PORCELAIN_STATUS)" ]; \ 11 | then \ 12 | echo 'YOU HAVE UNCOMMITED CHANGES'; \ 13 | git status; \ 14 | exit 1; \ 15 | fi 16 | pypi-upload: check-all-commited test 17 | rm -Rf dist/* 18 | python setup.py sdist 19 | twine upload dist/* 20 | ctags: 21 | ctags -R . 22 | clean: 23 | rm -Rf build 24 | rm -v -- $(shell find . -name "*.pyc") 25 | rm -Rf xsd 26 | analyze-xsd: 27 | mkdir -p xsd 28 | test -f xsd/gpx1.1.xsd || wget http://www.topografix.com/gpx/1/1/gpx.xsd -O xsd/gpx1.1.xsd 29 | test -f xsd/gpx1.0.xsd || wget http://www.topografix.com/gpx/1/0/gpx.xsd -O xsd/gpx1.0.xsd 30 | cd xsd && python pretty_print_schemas.py 31 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | GPX.py 2 | Copyright 2010-2012 Tomo Krajina 3 | 4 | This product includes software developed by 5 | Tomo Krajina (https://tkrajina.blogspot.com/). 6 | 7 | =============================================================================== 8 | 9 | Dilution of precision stuff based on Son "Sean" Pham's fork (https://github.com/famanson) 10 | 11 | =============================================================================== 12 | 13 | Time and geo bounds methods based on Yakov Istomin fork (https://github.com/Valerich) 14 | 15 | =============================================================================== 16 | 17 | Lxml library based on Isaev Igor fork (https://github.com/itoldya) 18 | 19 | =============================================================================== 20 | 21 | Python3 compatibility from Robert Smallshire (https://github.com/rob-smallshire) 22 | 23 | =============================================================================== 24 | 25 | __repr__, pep8, better docs, timedelta fix from Bryce Jasmer (https://github.com/bj1) 26 | 27 | =============================================================================== 28 | 29 | Better docs by George Titsworth (https://github.com/titsworth) 30 | 31 | =============================================================================== 32 | -------------------------------------------------------------------------------- /gpxpy/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2011 Tomo Krajina 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | def parse(xml_or_file, version = None): 19 | """ 20 | Parse xml (string) or file object. This is just an wrapper for 21 | GPXParser.parse() function. 22 | 23 | parser may be 'lxml', 'minidom' or None (then it will be automatically 24 | detected, lxml if possible). 25 | 26 | xml_or_file must be the xml to parse or a file-object with the XML. 27 | 28 | version may be '1.0', '1.1' or None (then it will be read from the gpx 29 | xml node if possible, if not then version 1.0 will be used). 30 | """ 31 | 32 | from . import parser as mod_parser 33 | 34 | parser = mod_parser.GPXParser(xml_or_file) 35 | 36 | return parser.parse(version) 37 | -------------------------------------------------------------------------------- /test_files/first_and_last_elevation.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 140.0 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 140.0 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2011 Tomo Krajina 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from setuptools import setup 18 | 19 | 20 | with open('README.md') as f: 21 | long_description = f.read() 22 | 23 | setup( 24 | name='gpxpy', 25 | version='1.3.4', 26 | description='GPX file parser and GPS track manipulation library', 27 | long_description=long_description, 28 | long_description_content_type="text/markdown", 29 | license='Apache License, Version 2.0', 30 | author='Tomo Krajina', 31 | author_email='tkrajina@gmail.com', 32 | url='http://www.trackprofiler.com/gpxpy/index.html', 33 | packages=['gpxpy', ], 34 | python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", 35 | classifiers=[ 36 | "Programming Language :: Python", 37 | "Programming Language :: Python :: 2", 38 | "Programming Language :: Python :: 2.7", 39 | "Programming Language :: Python :: 3", 40 | "Programming Language :: Python :: 3.4", 41 | "Programming Language :: Python :: 3.5", 42 | "Programming Language :: Python :: 3.6", 43 | ], 44 | scripts=['gpxinfo'] 45 | ) 46 | -------------------------------------------------------------------------------- /test_files/unicode.gpx: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | -0.114380 12 | šđčćž 13 | ŠĐČĆŽ 14 | BACK TO THE ROOTS 15 | City (Small) 16 | 17 | 18 | -0.114380 19 | BIRDS NEST 20 | BIRDS NEST 21 | BIRDS NEST 22 | City (Small) 23 | 24 | 25 | -0.114380 26 | FAGGIO 27 | FAGGIO 28 | FAGGIO 29 | City (Small) 30 | 31 | 32 | -0.114380 33 | RAKOV12 34 | RAKOV12 35 | RAKOV12 36 | City (Small) 37 | 38 | 39 | -0.114380 40 | RAKV SKCJN 41 | RAKOV SKOCJAN 42 | RAKOV SKOCJAN 43 | City (Small) 44 | 45 | 46 | -0.114380 47 | VANSHNG LK 48 | VANISHING LAKE 49 | VANISHING LAKE 50 | City (Small) 51 | 52 | 53 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.3.4 4 | 5 | * Added custom schemaLocation support 6 | * Division by zero in gpxinfo 7 | * Missing tag(s) during parsing 8 | * to_xml() fails with an empty extension element 9 | * Setup.py: update classifiers, add python_requires and long_description 10 | 11 | ## v1.3.3 12 | 13 | * Added avg time to gpxpnfo 14 | * gpx.adjust_times for waypoints and routes 15 | * added gpx.fill_time_data_with_regular_intervals 16 | 17 | ## v1.3.2 18 | 19 | * Fix #123 by using Earth semi-major axis with 6378.137 km (WGS84) 20 | * No assert error if can't calculate missing elevations 21 | 22 | ## v1.3.1 23 | 24 | * Prefix format reserved for internal use 25 | 26 | ## v.1.3.0 27 | 28 | * Logging exception fix 29 | * Extension handling 30 | * simplify polyline 31 | 32 | ## v1.2.0 33 | 34 | * Remove timezone from timestam string 35 | * gpxinfo: output times in seconds 36 | * gpxinfo: `-m` for miles/feet 37 | * Minor `get_speed` fix 38 | * Lat/Lon must not have scientific notation 39 | * Simplify polyline fix 40 | * Fix unicode BOM behavior 41 | * Named logger 42 | * Remove minidom 43 | 44 | ## v.1.1.0 45 | 46 | ... 47 | -------------------------------------------------------------------------------- /xsd/gpx1.0.txt: -------------------------------------------------------------------------------- 1 | gpx 2 | - attr: version (xsd:string) required 3 | - attr: creator (xsd:string) required 4 | name 5 | desc 6 | author 7 | email 8 | url 9 | urlname 10 | time 11 | keywords 12 | bounds 13 | wpt 14 | - attr: lat (gpx:latitudeType) required 15 | - attr: lon (gpx:longitudeType) required 16 | ele 17 | time 18 | magvar 19 | geoidheight 20 | name 21 | cmt 22 | desc 23 | src 24 | url 25 | urlname 26 | sym 27 | type 28 | fix 29 | sat 30 | hdop 31 | vdop 32 | pdop 33 | ageofdgpsdata 34 | dgpsid 35 | rte 36 | name 37 | cmt 38 | desc 39 | src 40 | url 41 | urlname 42 | number 43 | rtept 44 | - attr: lat (gpx:latitudeType) required 45 | - attr: lon (gpx:longitudeType) required 46 | ele 47 | time 48 | magvar 49 | geoidheight 50 | name 51 | cmt 52 | desc 53 | src 54 | url 55 | urlname 56 | sym 57 | type 58 | fix 59 | sat 60 | hdop 61 | vdop 62 | pdop 63 | ageofdgpsdata 64 | dgpsid 65 | trk 66 | name 67 | cmt 68 | desc 69 | src 70 | url 71 | urlname 72 | number 73 | trkseg 74 | trkpt 75 | - attr: lat (gpx:latitudeType) required 76 | - attr: lon (gpx:longitudeType) required 77 | ele 78 | time 79 | course 80 | speed 81 | magvar 82 | geoidheight 83 | name 84 | cmt 85 | desc 86 | src 87 | url 88 | urlname 89 | sym 90 | type 91 | fix 92 | sat 93 | hdop 94 | vdop 95 | pdop 96 | ageofdgpsdata 97 | dgpsid 98 | -------------------------------------------------------------------------------- /gpxpy/gpxxml.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import xml.dom.minidom as mod_minidom 4 | 5 | def split_gpxs(xml): 6 | """ 7 | Split single tracks from this one, without parsing with gpxpy 8 | """ 9 | dom = mod_minidom.parseString(xml) 10 | gpx_node = _find_gpx_node(dom) 11 | gpx_track_nodes = [] 12 | if gpx_node: 13 | for child_node in gpx_node.childNodes: 14 | if child_node.nodeName == 'trk': 15 | gpx_track_nodes.append(child_node) 16 | gpx_node.removeChild(child_node) 17 | 18 | for gpx_track_node in gpx_track_nodes: 19 | gpx_node.appendChild(gpx_track_node) 20 | yield dom.toxml() 21 | gpx_node.removeChild(gpx_track_node) 22 | 23 | def join_gpxs(xmls): 24 | """ 25 | Utility to join GPX files without parsing them with gpxpy 26 | """ 27 | result = None 28 | 29 | wpt_elements = [] 30 | rte_elements = [] 31 | trk_elements = [] 32 | 33 | for xml in xmls: 34 | dom = mod_minidom.parseString(xml) 35 | if not result: 36 | result = dom 37 | 38 | gpx_node = _find_gpx_node(dom) 39 | if gpx_node: 40 | for child_node in gpx_node.childNodes: 41 | if child_node.nodeName == 'wpt': 42 | wpt_elements.append(child_node) 43 | gpx_node.removeChild(child_node) 44 | elif child_node.nodeName == 'rte': 45 | rte_elements.append(child_node) 46 | gpx_node.removeChild(child_node) 47 | elif child_node.nodeName == 'trk': 48 | trk_elements.append(child_node) 49 | gpx_node.removeChild(child_node) 50 | 51 | gpx_node = _find_gpx_node(result) 52 | if gpx_node: 53 | for wpt_element in wpt_elements: 54 | gpx_node.appendChild(wpt_element) 55 | for rte_element in rte_elements: 56 | gpx_node.appendChild(rte_element) 57 | for trk_element in trk_elements: 58 | gpx_node.appendChild(trk_element) 59 | 60 | return result.toxml() 61 | 62 | def _find_gpx_node(dom): 63 | for gpx_candidate_node in dom.childNodes: 64 | if gpx_candidate_node.nodeName == 'gpx': 65 | return gpx_candidate_node 66 | return None 67 | -------------------------------------------------------------------------------- /test_files/track-with-empty-segment.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2013-07-06T14:59:00Z 5 | 6 | 7 | 191.5999756 8 | 9 | 10 | 11 | 191.5999756 12 | 13 | 14 | 15 | 191.5999756 16 | 17 | 18 | 19 | 191.5999756 20 | 21 | 22 | 23 | 191.5999756 24 | 25 | 26 | 27 | 191.5999756 28 | 29 | 30 | 31 | 191.5999756 32 | 33 | 34 | 35 | 191.5999756 36 | 37 | 38 | 39 | 191.5999756 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to gpxpy 2 | 3 | gpxpy aims to be a full featured library for handling gpx files defined by the GPX 1.0 and 1.1 schemas. Specifically: 4 | 5 | - Be able to lossless read any well-formed and valid gpx (1.0 or 1.1) 6 | - Be able to manipulate all gpx fields defined by the schema 7 | - Provide convenience functions for common computations and manipulations 8 | - Be able to lossless write out any well-formed and valid gpx 9 | 10 | 11 | Bug fixes, feature additions, tests, documentation and more can be contributed via [issues](https://github.com/tkrajina/gpxpy/issues) and/or [pull requests](https://github.com/tkrajina/gpxpy/pulls). All contributions are welcome. 12 | 13 | ## Bug fixes, feature additions, etc. 14 | 15 | Please send a pull requests for new features or bugfixes to the `dev` branch. Minor changes or urgent hotfixes can be sent to `master`. 16 | Please include tests for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/tkrajina/gpxpy/issues/new). 17 | 18 | - Fork the gpxpy repository. 19 | - Create a branch from master. 20 | - Develop bug fixes, features, tests, etc. 21 | - Run the test suite on both Python 2.x and 3.x. You can enable [Travis CI](https://travis-ci.org/profile/) on your repo to catch test failures prior to the pull request, and [Coveralls](https://coveralls.io) to see if the changed code is covered by tests. 22 | - Create a pull request to pull the changes from your branch to the gpxpy master. 23 | 24 | If you plan a big refactory, open an inssue for discussion before starting it. 25 | 26 | ### Guidelines 27 | 28 | Library code is read far more than it is written. Keep your code clean and understandable. 29 | - Provide tests for any newly added code. 30 | - Follow PEP8 and use pycodestyle for new code. 31 | - Follow PEP257 and use pydocstyle. Additionally, docstrings should be styled like Google's [Python Style Guide](https://google.github.io/styleguide/pyguide.html?showone=Comments#Comments) 32 | - New code should pass flake8 33 | - Avoid decreases in Coverage 34 | 35 | ## Reporting Issues 36 | 37 | When reporting issues, please include code that reproduces the issue and whenever possible, a gpx that demonstrates the issue. The best reproductions are self-contained scripts with minimal dependencies. 38 | 39 | ### Provide details 40 | 41 | - What did you do? 42 | - What did you expect to happen? 43 | - What actually happened? 44 | - What versions of gpxpy, lxml and Python are you using? 45 | -------------------------------------------------------------------------------- /test_files/track-with-extremes.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | Untitled Path 13 | 14 | 15 | 10.000000 16 | 17 | 18 | 12.000000 19 | 20 | 21 | 30.000000 22 | 23 | 24 | 13.000000 25 | 26 | 27 | 12.000000 28 | 29 | 30 | 14.000000 31 | 32 | 33 | 15.000000 34 | 35 | 36 | 16.000000 37 | 38 | 39 | 15.000000 40 | 41 | 42 | 18.000000 43 | 44 | 45 | 15.000000 46 | 47 | 48 | 15.000000 49 | 50 | 51 | 14.000000 52 | 53 | 54 | 15.000000 55 | 56 | 57 | 17.000000 58 | 59 | 60 | 19.000000 61 | 62 | 63 | 20.000000 64 | 65 | 66 | 22.000000 67 | 68 | 69 | 20.000000 70 | 71 | 72 | 21.000000 73 | 74 | 75 | 22.000000 76 | 77 | 78 | 22.000000 79 | 80 | 81 | 21.000000 82 | 83 | 84 | 21.000000 85 | 86 | 87 | 22.000000 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /gpxpy/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2011 Tomo Krajina 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import sys as mod_sys 18 | import math as mod_math 19 | import xml.sax.saxutils as mod_saxutils 20 | 21 | PYTHON_VERSION = mod_sys.version.split(' ')[0] 22 | 23 | 24 | def to_xml(tag, attributes=None, content=None, default=None, escape=False, prettyprint=True, indent=''): 25 | if not prettyprint: 26 | indent = '' 27 | attributes = attributes or {} 28 | result = [] 29 | result.append('\n' + indent + '<{0}'.format(tag)) 30 | 31 | if content is None and default: 32 | content = default 33 | 34 | if attributes: 35 | for attribute in attributes.keys(): 36 | result.append(make_str(' %s="%s"' % (attribute, attributes[attribute]))) 37 | 38 | if content is None: 39 | result.append('/>') 40 | else: 41 | if escape: 42 | result.append(make_str('>%s' % (mod_saxutils.escape(content), tag))) 43 | else: 44 | result.append(make_str('>%s' % (content, tag))) 45 | 46 | result = make_str(''.join(result)) 47 | 48 | return result 49 | 50 | 51 | def is_numeric(object): 52 | try: 53 | float(object) 54 | return True 55 | except TypeError: 56 | return False 57 | except ValueError: 58 | return False 59 | 60 | 61 | def to_number(s, default=0, nan_value=None): 62 | try: 63 | result = float(s) 64 | if mod_math.isnan(result): 65 | return nan_value 66 | return result 67 | except TypeError: 68 | pass 69 | except ValueError: 70 | pass 71 | return default 72 | 73 | 74 | def total_seconds(timedelta): 75 | """ Some versions of python dont have timedelta.total_seconds() method. """ 76 | if timedelta is None: 77 | return None 78 | return (timedelta.days * 86400) + timedelta.seconds 79 | 80 | 81 | def make_str(s): 82 | """ Convert a str or unicode or float object into a str type. """ 83 | if isinstance(s, float): 84 | result = str(s) 85 | if not 'e' in result: 86 | return result 87 | # scientific notation is illegal in GPX 1/1 88 | return format(s, '.10f').rstrip('0.') 89 | if PYTHON_VERSION[0] == '2': 90 | if isinstance(s, unicode): 91 | return s.encode("utf-8") 92 | return str(s) 93 | -------------------------------------------------------------------------------- /test_files/gpx1.0_with_all_fields.gpx: -------------------------------------------------------------------------------- 1 | 2 | example name 3 | example description 4 | example author 5 | example@email.com 6 | http://example.url 7 | example urlname 8 | 9 | example keywords 10 | 11 | 12 | 75.1 13 | 14 | 1.1 15 | 2.0 16 | example name 17 | example cmt 18 | example desc 19 | example src 20 | example url 21 | example urlname 22 | example sym 23 | example type 24 | 2d 25 | 5 26 | 6 27 | 7 28 | 8 29 | 9 30 | 45 31 | 32 | 33 | 34 | 35 | example name 36 | example cmt 37 | example desc 38 | example src 39 | example url 40 | example urlname 41 | 7 42 | 43 | 75.1 44 | 45 | 1.2 46 | 2.1 47 | example name r 48 | example cmt r 49 | example desc r 50 | example src r 51 | example url r 52 | example urlname r 53 | example sym r 54 | example type r 55 | 3d 56 | 6 57 | 7 58 | 8 59 | 9 60 | 10 61 | 99 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | second route 70 | example desc 2 71 | 72 | 73 | 74 | 75 | 76 | 77 | example name t 78 | example cmt t 79 | example desc t 80 | example src t 81 | example url t 82 | example urlname t 83 | 1 84 | 85 | 86 | 11.1 87 | 88 | 12 89 | 13 90 | example name t 91 | example cmt t 92 | example desc t 93 | example src t 94 | example url t 95 | example urlname t 96 | example sym t 97 | example type t 98 | 3d 99 | 100 100 | 101 101 | 102 102 | 103 103 | 104 104 | 99 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /xsd/pretty_print_schemas.py: -------------------------------------------------------------------------------- 1 | import xml.dom.minidom as mod_minidom 2 | 3 | def get_children_by_tag_name(node, tag_name): 4 | result = [] 5 | for child in node.childNodes: 6 | if child.nodeName == tag_name: 7 | result.append(child) 8 | return result 9 | 10 | def get_indented(indentation, string): 11 | result = '' 12 | for i in range(indentation): 13 | result += ' ' 14 | return result + str(string) + '\n' 15 | 16 | def parse_1_1(dom): 17 | root_node = dom.childNodes[0] 18 | complex_type_nodes = parse_1_1_find_complex_type(root_node) 19 | gpx_element_nodes = parse_1_1_find_elements(root_node) 20 | result = '' 21 | for gpx_element_node in gpx_element_nodes: 22 | result += parse_1_1_element_node(gpx_element_node, complex_type_nodes) 23 | return result 24 | 25 | def parse_1_1_element_node(gpx_element_node, complex_type_nodes, depth=0): 26 | result = '' 27 | node_type = gpx_element_node.attributes['type'].value 28 | node_name = gpx_element_node.attributes['name'].value 29 | result += get_indented(depth, '%s (%s)' % (node_name, node_type)) 30 | if node_type in complex_type_nodes: 31 | # Complex node => parse: 32 | attribute_nodes = get_children_by_tag_name(complex_type_nodes[node_type], 'xsd:attribute') 33 | for attribute_node in attribute_nodes: 34 | attribute_node_name = attribute_node.attributes['name'].value 35 | attribute_node_type = attribute_node.attributes['type'].value 36 | attribute_node_required = attribute_node.attributes['required'].value if attribute_node.attributes.has_key('required') else None 37 | result += get_indented(depth + 1, '- attr: %s (%s) %s' % (attribute_node_name, attribute_node_type, attribute_node_required)) 38 | sequence_nodes = get_children_by_tag_name(complex_type_nodes[node_type], 'xsd:sequence') 39 | if len(sequence_nodes) > 0: 40 | sequence_node = sequence_nodes[0] 41 | for element_node in get_children_by_tag_name(sequence_node, 'xsd:element'): 42 | result += parse_1_1_element_node(element_node, complex_type_nodes, depth=depth+1) 43 | return result 44 | 45 | def parse_1_1_find_elements(node): 46 | result = [] 47 | for node in node.childNodes: 48 | if node.nodeName == 'xsd:element': 49 | #import ipdb;ipdb.set_trace() 50 | result.append(node) 51 | return result 52 | 53 | def parse_1_1_find_complex_type(root_node): 54 | result = {} 55 | for node in root_node.childNodes: 56 | if node.nodeName == 'xsd:complexType': 57 | node_name = node.attributes['name'].value 58 | result[node_name] = node 59 | return result 60 | 61 | def parse_1_0(dom): 62 | root_node = dom.getElementsByTagName('xsd:element')[0] 63 | return parse_1_0_elements(root_node) 64 | 65 | def parse_1_0_elements(element, depth=0): 66 | result = '' 67 | result += get_indented(depth, element.attributes['name'].value) 68 | complex_types = get_children_by_tag_name(element, 'xsd:complexType') 69 | if len(complex_types) > 0: 70 | print(complex_types) 71 | complex_type = complex_types[0] 72 | for attribute_node in get_children_by_tag_name(complex_type, 'xsd:attribute'): 73 | attribute_node_name = attribute_node.attributes['name'].value 74 | attribute_node_type = attribute_node.attributes['type'].value 75 | attribute_node_use = attribute_node.attributes['use'].value if attribute_node.attributes.has_key('use') else None 76 | result += get_indented(depth + 1, '- attr: %s (%s) %s' % (attribute_node_name, attribute_node_type, attribute_node_use)) 77 | sequences = get_children_by_tag_name(complex_type, 'xsd:sequence') 78 | if len(sequences) > 0: 79 | sequence = sequences[0] 80 | for sub_element in get_children_by_tag_name(sequence, 'xsd:element'): 81 | result += parse_1_0_elements(sub_element, depth=depth+1) 82 | return result 83 | 84 | dom = mod_minidom.parseString(open('gpx1.0.xsd').read()) 85 | prety_print_1_0 = parse_1_0(dom) 86 | with open('gpx1.0.txt', 'w') as f: 87 | f.write(prety_print_1_0) 88 | print '1.0 written' 89 | 90 | dom = mod_minidom.parseString(open('gpx1.1.xsd').read()) 91 | prety_print_1_1 = parse_1_1(dom) 92 | with open('gpx1.1.txt', 'w') as f: 93 | f.write(prety_print_1_1) 94 | print '1.1 written' 95 | -------------------------------------------------------------------------------- /test_files/route.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | #001 6 | 7 | #002 8 | 9 | #003 10 | 11 | #004 12 | 13 | #005 14 | 15 | #006 16 | 17 | #007 18 | 19 | #008 20 | 21 | #009 22 | 23 | #010 24 | 25 | #011 26 | 27 | #012 28 | 29 | #013 30 | 31 | #014 32 | 33 | #015 34 | 35 | #016 36 | 37 | #017 38 | 39 | #018 40 | 41 | #019 42 | 43 | #020 44 | 45 | #021 46 | 47 | #022 48 | 49 | #023 50 | 51 | #024 52 | 53 | #025 54 | 55 | #026 56 | 57 | #027 58 | 59 | #028 60 | 61 | #029 62 | 63 | #030 64 | 65 | #031 66 | 67 | #032 68 | 69 | #033 70 | 71 | #034 72 | 73 | #035 74 | 75 | #036 76 | 77 | #037 78 | 79 | #038 80 | 81 | #039 82 | 83 | #040 84 | 85 | #041 86 | 87 | #042 88 | 89 | #043 90 | 91 | #044 92 | 93 | #045 94 | 95 | #046 96 | 97 | #047 98 | 99 | #048 100 | 101 | #049 102 | 103 | #050 104 | 105 | #051 106 | 107 | #052 108 | 109 | #053 110 | 111 | #054 112 | 113 | #055 -------------------------------------------------------------------------------- /xsd/gpx1.1.txt: -------------------------------------------------------------------------------- 1 | gpx (gpxType) 2 | - attr: version (xsd:string) None 3 | - attr: creator (xsd:string) None 4 | metadata (metadataType) 5 | name (xsd:string) 6 | desc (xsd:string) 7 | author (personType) 8 | name (xsd:string) 9 | email (emailType) 10 | - attr: id (xsd:string) None 11 | - attr: domain (xsd:string) None 12 | link (linkType) 13 | - attr: href (xsd:anyURI) None 14 | text (xsd:string) 15 | type (xsd:string) 16 | copyright (copyrightType) 17 | - attr: author (xsd:string) None 18 | year (xsd:gYear) 19 | license (xsd:anyURI) 20 | link (linkType) 21 | - attr: href (xsd:anyURI) None 22 | text (xsd:string) 23 | type (xsd:string) 24 | time (xsd:dateTime) 25 | keywords (xsd:string) 26 | bounds (boundsType) 27 | - attr: minlat (latitudeType) None 28 | - attr: minlon (longitudeType) None 29 | - attr: maxlat (latitudeType) None 30 | - attr: maxlon (longitudeType) None 31 | extensions (extensionsType) 32 | wpt (wptType) 33 | - attr: lat (latitudeType) None 34 | - attr: lon (longitudeType) None 35 | ele (xsd:decimal) 36 | time (xsd:dateTime) 37 | magvar (degreesType) 38 | geoidheight (xsd:decimal) 39 | name (xsd:string) 40 | cmt (xsd:string) 41 | desc (xsd:string) 42 | src (xsd:string) 43 | link (linkType) 44 | - attr: href (xsd:anyURI) None 45 | text (xsd:string) 46 | type (xsd:string) 47 | sym (xsd:string) 48 | type (xsd:string) 49 | fix (fixType) 50 | sat (xsd:nonNegativeInteger) 51 | hdop (xsd:decimal) 52 | vdop (xsd:decimal) 53 | pdop (xsd:decimal) 54 | ageofdgpsdata (xsd:decimal) 55 | dgpsid (dgpsStationType) 56 | extensions (extensionsType) 57 | rte (rteType) 58 | name (xsd:string) 59 | cmt (xsd:string) 60 | desc (xsd:string) 61 | src (xsd:string) 62 | link (linkType) 63 | - attr: href (xsd:anyURI) None 64 | text (xsd:string) 65 | type (xsd:string) 66 | number (xsd:nonNegativeInteger) 67 | type (xsd:string) 68 | extensions (extensionsType) 69 | rtept (wptType) 70 | - attr: lat (latitudeType) None 71 | - attr: lon (longitudeType) None 72 | ele (xsd:decimal) 73 | time (xsd:dateTime) 74 | magvar (degreesType) 75 | geoidheight (xsd:decimal) 76 | name (xsd:string) 77 | cmt (xsd:string) 78 | desc (xsd:string) 79 | src (xsd:string) 80 | link (linkType) 81 | - attr: href (xsd:anyURI) None 82 | text (xsd:string) 83 | type (xsd:string) 84 | sym (xsd:string) 85 | type (xsd:string) 86 | fix (fixType) 87 | sat (xsd:nonNegativeInteger) 88 | hdop (xsd:decimal) 89 | vdop (xsd:decimal) 90 | pdop (xsd:decimal) 91 | ageofdgpsdata (xsd:decimal) 92 | dgpsid (dgpsStationType) 93 | extensions (extensionsType) 94 | trk (trkType) 95 | name (xsd:string) 96 | cmt (xsd:string) 97 | desc (xsd:string) 98 | src (xsd:string) 99 | link (linkType) 100 | - attr: href (xsd:anyURI) None 101 | text (xsd:string) 102 | type (xsd:string) 103 | number (xsd:nonNegativeInteger) 104 | type (xsd:string) 105 | extensions (extensionsType) 106 | trkseg (trksegType) 107 | trkpt (wptType) 108 | - attr: lat (latitudeType) None 109 | - attr: lon (longitudeType) None 110 | ele (xsd:decimal) 111 | time (xsd:dateTime) 112 | magvar (degreesType) 113 | geoidheight (xsd:decimal) 114 | name (xsd:string) 115 | cmt (xsd:string) 116 | desc (xsd:string) 117 | src (xsd:string) 118 | link (linkType) 119 | - attr: href (xsd:anyURI) None 120 | text (xsd:string) 121 | type (xsd:string) 122 | sym (xsd:string) 123 | type (xsd:string) 124 | fix (fixType) 125 | sat (xsd:nonNegativeInteger) 126 | hdop (xsd:decimal) 127 | vdop (xsd:decimal) 128 | pdop (xsd:decimal) 129 | ageofdgpsdata (xsd:decimal) 130 | dgpsid (dgpsStationType) 131 | extensions (extensionsType) 132 | extensions (extensionsType) 133 | extensions (extensionsType) 134 | -------------------------------------------------------------------------------- /gpxinfo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Command line utility to extract basic statistics from gpx file(s) 5 | """ 6 | 7 | import pdb 8 | 9 | import sys as mod_sys 10 | import logging as mod_logging 11 | import math as mod_math 12 | import argparse as mod_argparse 13 | 14 | import gpxpy as mod_gpxpy 15 | 16 | 17 | KM_TO_MILES = 0.621371 18 | M_TO_FEET = 3.28084 19 | 20 | 21 | def format_time(time_s): 22 | if not time_s: 23 | return 'n/a' 24 | elif args.seconds: 25 | return str(int(time_s)) 26 | else: 27 | minutes = mod_math.floor(time_s / 60.) 28 | hours = mod_math.floor(minutes / 60.) 29 | return '%s:%s:%s' % (str(int(hours)).zfill(2), str(int(minutes % 60)).zfill(2), str(int(time_s % 60)).zfill(2)) 30 | 31 | 32 | def format_long_length(length): 33 | if args.miles: 34 | return '{:.3f}miles'.format(length / 1000. * KM_TO_MILES) 35 | else: 36 | return '{:.3f}km'.format(length / 1000.) 37 | 38 | 39 | def format_short_length(length): 40 | if args.miles: 41 | return '{:.2f}ft'.format(length * M_TO_FEET) 42 | else: 43 | return '{:.2f}m'.format(length) 44 | 45 | 46 | def format_speed(speed): 47 | if not speed: 48 | speed = 0 49 | if args.miles: 50 | return '{:.2f}mph'.format(speed * KM_TO_MILES * 3600. / 1000.) 51 | else: 52 | return '{:.2f}m/s = {:.2f}km/h'.format(speed, speed * 3600. / 1000.) 53 | 54 | 55 | def print_gpx_part_info(gpx_part, indentation=' '): 56 | """ 57 | gpx_part may be a track or segment. 58 | """ 59 | length_2d = gpx_part.length_2d() 60 | length_3d = gpx_part.length_3d() 61 | print('%sLength 2D: %s' % (indentation, format_long_length(length_2d))) 62 | print('%sLength 3D: %s' % (indentation, format_long_length(length_3d))) 63 | 64 | moving_time, stopped_time, moving_distance, stopped_distance, max_speed = gpx_part.get_moving_data() 65 | print('%sMoving time: %s' % (indentation, format_time(moving_time))) 66 | print('%sStopped time: %s' % (indentation, format_time(stopped_time))) 67 | #print('%sStopped distance: %s' % (indentation, format_short_length(stopped_distance))) 68 | print('%sMax speed: %s' % (indentation, format_speed(max_speed))) 69 | print('%sAvg speed: %s' % (indentation, format_speed(moving_distance / moving_time) if moving_time > 0 else "?")) 70 | 71 | uphill, downhill = gpx_part.get_uphill_downhill() 72 | print('%sTotal uphill: %s' % (indentation, format_short_length(uphill))) 73 | print('%sTotal downhill: %s' % (indentation, format_short_length(downhill))) 74 | 75 | start_time, end_time = gpx_part.get_time_bounds() 76 | print('%sStarted: %s' % (indentation, start_time)) 77 | print('%sEnded: %s' % (indentation, end_time)) 78 | 79 | points_no = len(list(gpx_part.walk(only_points=True))) 80 | print('%sPoints: %s' % (indentation, points_no)) 81 | 82 | if points_no > 0: 83 | distances = [] 84 | previous_point = None 85 | for point in gpx_part.walk(only_points=True): 86 | if previous_point: 87 | distance = point.distance_2d(previous_point) 88 | distances.append(distance) 89 | previous_point = point 90 | print('%sAvg distance between points: %s' % (indentation, format_short_length(sum(distances) / len(list(gpx_part.walk()))))) 91 | 92 | print('') 93 | 94 | 95 | def print_gpx_info(gpx, gpx_file): 96 | print('File: %s' % gpx_file) 97 | 98 | if gpx.name: 99 | print(' GPX name: %s' % gpx.name) 100 | if gpx.description: 101 | print(' GPX description: %s' % gpx.description) 102 | if gpx.author_name: 103 | print(' Author: %s' % gpx.author_name) 104 | if gpx.author_email: 105 | print(' Email: %s' % gpx.author_email) 106 | 107 | print_gpx_part_info(gpx) 108 | 109 | for track_no, track in enumerate(gpx.tracks): 110 | for segment_no, segment in enumerate(track.segments): 111 | print(' Track #%s, Segment #%s' % (track_no, segment_no)) 112 | print_gpx_part_info(segment, indentation=' ') 113 | 114 | 115 | def run(gpx_files): 116 | if not gpx_files: 117 | print('No GPX files given') 118 | mod_sys.exit(1) 119 | 120 | for gpx_file in gpx_files: 121 | try: 122 | gpx = mod_gpxpy.parse(open(gpx_file)) 123 | print_gpx_info(gpx, gpx_file) 124 | except Exception as e: 125 | mod_logging.exception(e) 126 | print('Error processing %s' % gpx_file) 127 | mod_sys.exit(1) 128 | 129 | 130 | def make_parser(): 131 | parser = mod_argparse.ArgumentParser(usage='%(prog)s [-s] [-m] [-d] [file ...]', 132 | description='Command line utility to extract basic statistics from gpx file(s)') 133 | parser.add_argument('-s', '--seconds', action='store_true', 134 | help='print times as N seconds, rather than HH:MM:SS') 135 | parser.add_argument('-m', '--miles', action='store_true', 136 | help='print distances and speeds using miles and feet') 137 | parser.add_argument('-d', '--debug', action='store_true', 138 | help='show detailed logging') 139 | return parser 140 | 141 | if __name__ == '__main__': 142 | args, gpx_files = make_parser().parse_known_args() 143 | if args.debug: 144 | mod_logging.basicConfig(level=mod_logging.DEBUG, 145 | format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s') 146 | run(gpx_files) 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/tkrajina/gpxpy.svg?branch=master)](https://travis-ci.org/tkrajina/gpxpy) 2 | [![Coverage Status](https://coveralls.io/repos/github/tkrajina/gpxpy/badge.svg?branch=master)](https://coveralls.io/github/tkrajina/gpxpy?branch=master) 3 | 4 | # gpxpy -- GPX file parser 5 | 6 | This is a simple Python library for parsing and manipulating GPX files. GPX is an XML based format for GPS tracks. 7 | 8 | You can see it in action on [my online GPS track editor and organizer](http://www.trackprofiler.com). 9 | 10 | There is also a Golang port of gpxpy: [gpxgo](http://github.com/tkrajina/gpxgo). 11 | 12 | See also [srtm.py](https://github.com/tkrajina/srtm.py) if your track lacks elevation data. 13 | 14 | ## Usage 15 | 16 | ```python 17 | import gpxpy 18 | import gpxpy.gpx 19 | 20 | # Parsing an existing file: 21 | # ------------------------- 22 | 23 | gpx_file = open('test_files/cerknicko-jezero.gpx', 'r') 24 | 25 | gpx = gpxpy.parse(gpx_file) 26 | 27 | for track in gpx.tracks: 28 | for segment in track.segments: 29 | for point in segment.points: 30 | print('Point at ({0},{1}) -> {2}'.format(point.latitude, point.longitude, point.elevation)) 31 | 32 | for waypoint in gpx.waypoints: 33 | print('waypoint {0} -> ({1},{2})'.format(waypoint.name, waypoint.latitude, waypoint.longitude)) 34 | 35 | for route in gpx.routes: 36 | print('Route:') 37 | for point in route.points: 38 | print('Point at ({0},{1}) -> {2}'.format(point.latitude, point.longitude, point.elevation)) 39 | 40 | # There are many more utility methods and functions: 41 | # You can manipulate/add/remove tracks, segments, points, waypoints and routes and 42 | # get the GPX XML file from the resulting object: 43 | 44 | print 'GPX:', gpx.to_xml() 45 | 46 | # Creating a new file: 47 | # -------------------- 48 | 49 | gpx = gpxpy.gpx.GPX() 50 | 51 | # Create first track in our GPX: 52 | gpx_track = gpxpy.gpx.GPXTrack() 53 | gpx.tracks.append(gpx_track) 54 | 55 | # Create first segment in our GPX track: 56 | gpx_segment = gpxpy.gpx.GPXTrackSegment() 57 | gpx_track.segments.append(gpx_segment) 58 | 59 | # Create points: 60 | gpx_segment.points.append(gpxpy.gpx.GPXTrackPoint(2.1234, 5.1234, elevation=1234)) 61 | gpx_segment.points.append(gpxpy.gpx.GPXTrackPoint(2.1235, 5.1235, elevation=1235)) 62 | gpx_segment.points.append(gpxpy.gpx.GPXTrackPoint(2.1236, 5.1236, elevation=1236)) 63 | 64 | # You can add routes and waypoints, too... 65 | 66 | print 'Created GPX:', gpx.to_xml() 67 | ``` 68 | 69 | ## GPX Version: 70 | 71 | gpx.py can parse and generate GPX 1.0 and 1.1 files. Note that the generated file will always be a valid XML document, but it may not be (strictly speaking) a valid GPX document. For example, if you set gpx.email to "my.email AT mail.com" the generated GPX tag won't confirm to the regex pattern. And the file won't be valid. Most applications will ignore such errors, but... Be aware of this! 72 | 73 | Be aware that the gpxpy object model *is not 100% equivalent* with the underlying GPX XML file schema. That's because the library object model works with both GPX 1.0 and 1.1. 74 | 75 | For example, GPX 1.0 specified a `speed` attribute for every track point, but that was removed in GPX 1.1. If you parse GPX 1.0 and serialize back with `gpx.to_xml()` everything will work fine. But if you have a GPX 1.1 object, changes in the `speed` attribute will be lost after `gpx.to_xml()`. If you want to force using 1.0, you can `gpx.to_xml(version="1.0")`. Another possibility is to use `extensions` to save the speed in GPX 1.1. 76 | 77 | ## GPX extensions 78 | 79 | gpx.py preserves GPX extensions. They are stored as [ElementTree](https://docs.python.org/2/library/xml.etree.elementtree.html#module-xml.etree.ElementTree) DOM objects. Extensions are part of GPX 1.1, and will be ignored when serializing a GPX object in a GPX 1.0 file. 80 | 81 | ## XML parsing 82 | 83 | If lxml is available, then it will be used for XML parsing. 84 | Otherwise minidom is used. 85 | Note that lxml is 2-3 times faster so, if you can choose -- use it :) 86 | 87 | The GPX version is automatically determined when parsing by reading the version attribute in the gpx node. If this attribute is not present then the version is assumed to be 1.0. A specific version can be forced by setting the `version` parameter in the parse function. Possible values for the 'version' parameter are `1.0`, `1.1` and `None`. 88 | 89 | ## Pull requests 90 | 91 | OK, so you found a bug and fixed it. Before sending a pull request -- check that all tests are OK with Python 2.7 and Python 3.4+. 92 | 93 | Run all tests with: 94 | 95 | $ python -m unittest test 96 | $ python3 -m unittest test 97 | 98 | Run a single test with: 99 | 100 | $ python -m unittest test.GPXTests.test_haversine_and_nonhaversine 101 | $ python3 -m unittest test.GPXTests.test_haversine_and_nonhaversine 102 | 103 | ## GPXInfo 104 | 105 | The repository contains a little command line utility to extract basic statistics from a file. 106 | Example usage: 107 | 108 | $ gpxinfo voznjica.gpx 109 | File: voznjica.gpx 110 | Length 2D: 63.6441229018 111 | Length 3D: 63.8391428454 112 | Moving time: 02:56:03 113 | Stopped time: 00:21:38 114 | Max speed: 14.187909492m/s = 51.0764741713km/h 115 | Total uphill: 1103.1626183m 116 | Total downhill: 1087.7812703m 117 | Started: 2013-06-01 06:46:53 118 | Ended: 2013-06-01 10:23:45 119 | 120 | ## License 121 | 122 | GPX.py is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) 123 | 124 | -------------------------------------------------------------------------------- /test_files/gpx1.1_with_all_fields.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | example name 4 | example description 5 | 6 | author name 7 | 8 | 9 | link text 10 | link type 11 | 12 | 13 | 14 | 15 | 2013 16 | lic 17 | 18 | 19 | link text2 20 | link type2 21 | 22 | 23 | example keywords 24 | 25 | 26 | bbb 27 | ccc 28 | ddd 29 | 30 | 31 | 32 | 75.1 33 | 34 | 1.1 35 | 2.0 36 | example name 37 | example cmt 38 | example desc 39 | example src 40 | 41 | link text3 42 | link type3 43 | 44 | example sym 45 | example type 46 | 2d 47 | 5 48 | 6 49 | 7 50 | 8 51 | 9 52 | 45 53 | 54 | bbb 55 | ddd 56 | 57 | 58 | 59 | 60 | 61 | example name 62 | example cmt 63 | example desc 64 | example src 65 | 66 | link text3 67 | link type3 68 | 69 | 7 70 | rte type 71 | 72 | 1 73 | 2 74 | 75 | 76 | 75.1 77 | 78 | 1.2 79 | 2.1 80 | example name r 81 | example cmt r 82 | example desc r 83 | example src r 84 | 85 | rtept link 86 | rtept link type 87 | 88 | example sym r 89 | example type r 90 | 3d 91 | 6 92 | 7 93 | 8 94 | 9 95 | 10 96 | 99 97 | 98 | rtept 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | second route 108 | example desc 2 109 | 110 | 111 | 112 | 113 | 114 | 115 | example name t 116 | example cmt t 117 | example desc t 118 | example src t 119 | 120 | trk link 121 | trk link type 122 | 123 | 1 124 | t 125 | 126 | 2 127 | 128 | 129 | 130 | 11.1 131 | 132 | 12 133 | 13 134 | example name t 135 | example cmt t 136 | example desc t 137 | example src t 138 | 139 | trkpt link 140 | trkpt link type 141 | 142 | example sym t 143 | example type t 144 | 3d 145 | 100 146 | 101 147 | 102 148 | 103 149 | 104 150 | 99 151 | 152 | true 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | ... 163 | 164 | 165 | -------------------------------------------------------------------------------- /gpxpy/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2011 Tomo Krajina 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import logging as mod_logging 18 | import re as mod_re 19 | 20 | try: 21 | import lxml.etree as mod_etree # Load LXML or fallback to cET or ET 22 | except ImportError: 23 | try: 24 | import xml.etree.cElementTree as mod_etree 25 | except ImportError: 26 | import xml.etree.ElementTree as mod_etree 27 | 28 | from . import gpx as mod_gpx 29 | from . import utils as mod_utils 30 | from . import gpxfield as mod_gpxfield 31 | 32 | log = mod_logging.getLogger(__name__) 33 | 34 | class GPXParser: 35 | """ 36 | Parse the XML and provide new GPX instance. 37 | 38 | Methods: 39 | __init__: initialize new instance 40 | init: format XML 41 | parse: parse XML, build tree, build GPX 42 | 43 | Attributes: 44 | gpx: GPX instance of the most recently parsed XML 45 | xml: string containing the XML text 46 | 47 | """ 48 | 49 | def __init__(self, xml_or_file=None): 50 | """ 51 | Initialize new GPXParser instance. 52 | 53 | Arguments: 54 | xml_or_file: string or file object containing the gpx 55 | formatted xml 56 | 57 | """ 58 | self.init(xml_or_file) 59 | self.gpx = mod_gpx.GPX() 60 | 61 | def init(self, xml_or_file): 62 | """ 63 | Store the XML and remove utf-8 Byte Order Mark if present. 64 | 65 | Args: 66 | xml_or_file: string or file object containing the gpx 67 | formatted xml 68 | 69 | """ 70 | text = xml_or_file.read() if hasattr(xml_or_file, 'read') else xml_or_file 71 | self.xml = mod_utils.make_str(text) 72 | 73 | def parse(self, version=None): 74 | """ 75 | Parse the XML and return a GPX object. 76 | 77 | Args: 78 | version: str or None indicating the GPX Schema to use. 79 | Options are '1.0', '1.1' and None. When version is None 80 | the version is read from the file or falls back on 1.0. 81 | 82 | Returns: 83 | A GPX object loaded from the xml 84 | 85 | Raises: 86 | GPXXMLSyntaxException: XML file is invalid 87 | GPXException: XML is valid but GPX data contains errors 88 | 89 | """ 90 | # Build prefix map for reserialization and extension handlings 91 | for namespace in mod_re.findall(r'\sxmlns:?[^=]*="[^"]+"', self.xml): 92 | prefix, _, URI = namespace[6:].partition('=') 93 | prefix = prefix.lstrip(':') 94 | if prefix == '': 95 | prefix = 'defaultns' # alias default for easier handling 96 | else: 97 | if prefix.startswith("ns"): 98 | mod_etree.register_namespace("noglobal_" + prefix, URI.strip('"')) 99 | else: 100 | mod_etree.register_namespace(prefix, URI.strip('"')) 101 | self.gpx.nsmap[prefix] = URI.strip('"') 102 | 103 | schema_loc = mod_re.search(r'\sxsi:schemaLocation="[^"]+"', self.xml) 104 | if schema_loc: 105 | _, _, value = schema_loc.group(0).partition('=') 106 | self.gpx.schema_locations = value.strip('"').split() 107 | 108 | # Remove default namespace to simplify processing later 109 | self.xml = mod_re.sub(r"""\sxmlns=(['"])[^'"]+\1""", '', self.xml, count=1) 110 | 111 | # Build tree 112 | try: 113 | if GPXParser.__library() == "LXML": 114 | # lxml does not like unicode strings when it's expecting 115 | # UTF-8. Also, XML comments result in a callable .tag(). 116 | # Strip them out to avoid handling them later. 117 | if mod_utils.PYTHON_VERSION[0] >= '3': 118 | self.xml = self.xml.encode('utf-8') 119 | root = mod_etree.XML(self.xml, 120 | mod_etree.XMLParser(remove_comments=True)) 121 | else: 122 | root = mod_etree.XML(self.xml) 123 | 124 | except Exception as e: 125 | # The exception here can be a lxml or ElementTree exception. 126 | log.debug('Error in:\n%s\n-----------\n' % self.xml, exc_info=True) 127 | 128 | # The library should work in the same way regardless of the 129 | # underlying XML parser that's why the exception thrown 130 | # here is GPXXMLSyntaxException (instead of simply throwing the 131 | # original ElementTree or lxml exception e). 132 | # 133 | # But, if the user needs the original exception (lxml or ElementTree) 134 | # it is available with GPXXMLSyntaxException.original_exception: 135 | raise mod_gpx.GPXXMLSyntaxException('Error parsing XML: %s' % str(e), e) 136 | 137 | if root is None: 138 | raise mod_gpx.GPXException('Document must have a `gpx` root node.') 139 | 140 | if version is None: 141 | version = root.get('version') 142 | 143 | mod_gpxfield.gpx_fields_from_xml(self.gpx, root, version) 144 | return self.gpx 145 | 146 | @staticmethod 147 | def __library(): 148 | """ 149 | Return the underlying ETree. 150 | 151 | Provided for convenient unittests. 152 | """ 153 | if "lxml" in str(mod_etree): 154 | return "LXML" 155 | return "STDLIB" 156 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /gpxpy/geo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2011 Tomo Krajina 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import logging as mod_logging 18 | import math as mod_math 19 | 20 | from . import utils as mod_utils 21 | 22 | log = mod_logging.getLogger(__name__) 23 | 24 | # Generic geo related function and class(es) 25 | 26 | # latitude/longitude in GPX files is always in WGS84 datum 27 | # WGS84 defined the Earth semi-major axis with 6378.137 km 28 | EARTH_RADIUS = 6378.137 * 1000 29 | 30 | # One degree in meters: 31 | ONE_DEGREE = (2*mod_math.pi*EARTH_RADIUS) / 360 # ==> 111.319 km 32 | 33 | 34 | def to_rad(x): 35 | return x / 180. * mod_math.pi 36 | 37 | 38 | def haversine_distance(latitude_1, longitude_1, latitude_2, longitude_2): 39 | """ 40 | Haversine distance between two points, expressed in meters. 41 | 42 | Implemented from http://www.movable-type.co.uk/scripts/latlong.html 43 | """ 44 | d_lat = to_rad(latitude_1 - latitude_2) 45 | d_lon = to_rad(longitude_1 - longitude_2) 46 | lat1 = to_rad(latitude_1) 47 | lat2 = to_rad(latitude_2) 48 | 49 | a = mod_math.sin(d_lat/2) * mod_math.sin(d_lat/2) + \ 50 | mod_math.sin(d_lon/2) * mod_math.sin(d_lon/2) * mod_math.cos(lat1) * mod_math.cos(lat2) 51 | c = 2 * mod_math.atan2(mod_math.sqrt(a), mod_math.sqrt(1-a)) 52 | d = EARTH_RADIUS * c 53 | 54 | return d 55 | 56 | 57 | def length(locations=None, _3d=None): 58 | locations = locations or [] 59 | if not locations: 60 | return 0 61 | length = 0 62 | for i in range(len(locations)): 63 | if i > 0: 64 | previous_location = locations[i - 1] 65 | location = locations[i] 66 | 67 | if _3d: 68 | d = location.distance_3d(previous_location) 69 | else: 70 | d = location.distance_2d(previous_location) 71 | if d: 72 | length += d 73 | return length 74 | 75 | 76 | def length_2d(locations=None): 77 | """ 2-dimensional length (meters) of locations (only latitude and longitude, no elevation). """ 78 | locations = locations or [] 79 | return length(locations, False) 80 | 81 | 82 | def length_3d(locations=None): 83 | """ 3-dimensional length (meters) of locations (it uses latitude, longitude, and elevation). """ 84 | locations = locations or [] 85 | return length(locations, True) 86 | 87 | 88 | def calculate_max_speed(speeds_and_distances): 89 | """ 90 | Compute average distance and standard deviation for distance. Extremes 91 | in distances are usually extremes in speeds, so we will ignore them, 92 | here. 93 | 94 | speeds_and_distances must be a list containing pairs of (speed, distance) 95 | for every point in a track segment. 96 | """ 97 | assert speeds_and_distances 98 | if len(speeds_and_distances) > 0: 99 | assert len(speeds_and_distances[0]) == 2 100 | # ... 101 | assert len(speeds_and_distances[-1]) == 2 102 | 103 | size = float(len(speeds_and_distances)) 104 | 105 | if size < 20: 106 | log.debug('Segment too small to compute speed, size=%s', size) 107 | return None 108 | 109 | distances = list(map(lambda x: x[1], speeds_and_distances)) 110 | average_distance = sum(distances) / float(size) 111 | standard_distance_deviation = mod_math.sqrt(sum(map(lambda distance: (distance-average_distance)**2, distances))/size) 112 | 113 | # Ignore items where the distance is too big: 114 | filtered_speeds_and_distances = filter(lambda speed_and_distance: abs(speed_and_distance[1] - average_distance) <= standard_distance_deviation * 1.5, speeds_and_distances) 115 | 116 | # sort by speed: 117 | speeds = list(map(lambda speed_and_distance: speed_and_distance[0], filtered_speeds_and_distances)) 118 | if not isinstance(speeds, list): # python3 119 | speeds = list(speeds) 120 | if not speeds: 121 | return None 122 | speeds.sort() 123 | 124 | # Even here there may be some extremes => ignore the last 5%: 125 | index = int(len(speeds) * 0.95) 126 | if index >= len(speeds): 127 | index = -1 128 | 129 | return speeds[index] 130 | 131 | 132 | def calculate_uphill_downhill(elevations): 133 | if not elevations: 134 | return 0, 0 135 | 136 | size = len(elevations) 137 | 138 | def __filter(n): 139 | current_ele = elevations[n] 140 | if current_ele is None: 141 | return False 142 | if 0 < n < size - 1: 143 | previous_ele = elevations[n-1] 144 | next_ele = elevations[n+1] 145 | if previous_ele is not None and current_ele is not None and next_ele is not None: 146 | return previous_ele*.3 + current_ele*.4 + next_ele*.3 147 | return current_ele 148 | 149 | smoothed_elevations = list(map(__filter, range(size))) 150 | 151 | uphill, downhill = 0., 0. 152 | 153 | for n, elevation in enumerate(smoothed_elevations): 154 | if n > 0 and elevation is not None and smoothed_elevations is not None: 155 | d = elevation - smoothed_elevations[n-1] 156 | if d > 0: 157 | uphill += d 158 | else: 159 | downhill -= d 160 | 161 | return uphill, downhill 162 | 163 | 164 | def distance(latitude_1, longitude_1, elevation_1, latitude_2, longitude_2, elevation_2, 165 | haversine=None): 166 | """ 167 | Distance between two points. If elevation is None compute a 2d distance 168 | 169 | if haversine==True -- haversine will be used for every computations, 170 | otherwise... 171 | 172 | Haversine distance will be used for distant points where elevation makes a 173 | small difference, so it is ignored. That's because haversine is 5-6 times 174 | slower than the dummy distance algorithm (which is OK for most GPS tracks). 175 | """ 176 | 177 | # If points too distant -- compute haversine distance: 178 | if haversine or (abs(latitude_1 - latitude_2) > .2 or abs(longitude_1 - longitude_2) > .2): 179 | return haversine_distance(latitude_1, longitude_1, latitude_2, longitude_2) 180 | 181 | coef = mod_math.cos(latitude_1 / 180. * mod_math.pi) 182 | x = latitude_1 - latitude_2 183 | y = (longitude_1 - longitude_2) * coef 184 | 185 | distance_2d = mod_math.sqrt(x * x + y * y) * ONE_DEGREE 186 | 187 | if elevation_1 is None or elevation_2 is None or elevation_1 == elevation_2: 188 | return distance_2d 189 | 190 | return mod_math.sqrt(distance_2d ** 2 + (elevation_1 - elevation_2) ** 2) 191 | 192 | 193 | def elevation_angle(location1, location2, radians=False): 194 | """ Uphill/downhill angle between two locations. """ 195 | if location1.elevation is None or location2.elevation is None: 196 | return None 197 | 198 | b = float(location2.elevation - location1.elevation) 199 | a = location2.distance_2d(location1) 200 | 201 | if a == 0: 202 | return 0 203 | 204 | angle = mod_math.atan(b / a) 205 | 206 | if radians: 207 | return angle 208 | 209 | return 180 * angle / mod_math.pi 210 | 211 | 212 | def distance_from_line(point, line_point_1, line_point_2): 213 | """ Distance of point from a line given with two points. """ 214 | assert point, point 215 | assert line_point_1, line_point_1 216 | assert line_point_2, line_point_2 217 | 218 | a = line_point_1.distance_2d(line_point_2) 219 | 220 | if a == 0: 221 | return line_point_1.distance_2d(point) 222 | 223 | b = line_point_1.distance_2d(point) 224 | c = line_point_2.distance_2d(point) 225 | 226 | s = (a + b + c) / 2. 227 | 228 | return 2. * mod_math.sqrt(abs(s * (s - a) * (s - b) * (s - c))) / a 229 | 230 | 231 | def get_line_equation_coefficients(location1, location2): 232 | """ 233 | Get line equation coefficients for: 234 | latitude * a + longitude * b + c = 0 235 | 236 | This is a normal cartesian line (not spherical!) 237 | """ 238 | if location1.longitude == location2.longitude: 239 | # Vertical line: 240 | return float(0), float(1), float(-location1.longitude) 241 | else: 242 | a = float(location1.latitude - location2.latitude) / (location1.longitude - location2.longitude) 243 | b = location1.latitude - location1.longitude * a 244 | return float(1), float(-a), float(-b) 245 | 246 | 247 | def simplify_polyline(points, max_distance): 248 | """Does Ramer-Douglas-Peucker algorithm for simplification of polyline """ 249 | 250 | if len(points) < 3: 251 | return points 252 | 253 | begin, end = points[0], points[-1] 254 | 255 | # Use a "normal" line just to detect the most distant point (not its real distance) 256 | # this is because this is faster to compute than calling distance_from_line() for 257 | # every point. 258 | # 259 | # This is an approximation and may have some errors near the poles and if 260 | # the points are too distant, but it should be good enough for most use 261 | # cases... 262 | a, b, c = get_line_equation_coefficients(begin, end) 263 | 264 | # Initialize to safe values 265 | tmp_max_distance = 0 266 | tmp_max_distance_position = 1 267 | 268 | # Check distance of all points between begin and end, exclusive 269 | for point_no in range(1,len(points)-1): 270 | point = points[point_no] 271 | d = abs(a * point.latitude + b * point.longitude + c) 272 | if d > tmp_max_distance: 273 | tmp_max_distance = d 274 | tmp_max_distance_position = point_no 275 | 276 | # Now that we have the most distance point, compute its real distance: 277 | real_max_distance = distance_from_line(points[tmp_max_distance_position], begin, end) 278 | 279 | # If furthest point is less than max_distance, remove all points between begin and end 280 | if real_max_distance < max_distance: 281 | return [begin, end] 282 | 283 | # If furthest point is more than max_distance, use it as anchor and run 284 | # function again using (begin to anchor) and (anchor to end), remove extra anchor 285 | return (simplify_polyline(points[:tmp_max_distance_position + 1], max_distance) + 286 | simplify_polyline(points[tmp_max_distance_position:], max_distance)[1:]) 287 | 288 | 289 | class Location: 290 | """ Generic geographical location """ 291 | 292 | latitude = None 293 | longitude = None 294 | elevation = None 295 | 296 | def __init__(self, latitude, longitude, elevation=None): 297 | self.latitude = latitude 298 | self.longitude = longitude 299 | self.elevation = elevation 300 | 301 | def has_elevation(self): 302 | return self.elevation or self.elevation == 0 303 | 304 | def remove_elevation(self): 305 | self.elevation = None 306 | 307 | def distance_2d(self, location): 308 | if not location: 309 | return None 310 | 311 | return distance(self.latitude, self.longitude, None, location.latitude, location.longitude, None) 312 | 313 | def distance_3d(self, location): 314 | if not location: 315 | return None 316 | 317 | return distance(self.latitude, self.longitude, self.elevation, location.latitude, location.longitude, location.elevation) 318 | 319 | def elevation_angle(self, location, radians=False): 320 | return elevation_angle(self, location, radians) 321 | 322 | def move(self, location_delta): 323 | self.latitude, self.longitude = location_delta.move(self) 324 | 325 | def __add__(self, location_delta): 326 | latitude, longitude = location_delta.move(self) 327 | return Location(latitude, longitude) 328 | 329 | def __str__(self): 330 | return '[loc:%s,%s@%s]' % (self.latitude, self.longitude, self.elevation) 331 | 332 | def __repr__(self): 333 | if self.elevation is None: 334 | return 'Location(%s, %s)' % (self.latitude, self.longitude) 335 | else: 336 | return 'Location(%s, %s, %s)' % (self.latitude, self.longitude, self.elevation) 337 | 338 | def __hash__(self): 339 | return mod_utils.hash_object(self, ('latitude', 'longitude', 'elevation')) 340 | 341 | 342 | class LocationDelta: 343 | """ 344 | Intended to use similar to timestamp.timedelta, but for Locations. 345 | """ 346 | 347 | NORTH = 0 348 | EAST = 90 349 | SOUTH = 180 350 | WEST = 270 351 | 352 | def __init__(self, distance=None, angle=None, latitude_diff=None, longitude_diff=None): 353 | """ 354 | Version 1: 355 | Distance (in meters). 356 | angle_from_north *clockwise*. 357 | ...must be given 358 | Version 2: 359 | latitude_diff and longitude_diff 360 | ...must be given 361 | """ 362 | if (distance is not None) and (angle is not None): 363 | if (latitude_diff is not None) or (longitude_diff is not None): 364 | raise Exception('No lat/lon diff if using distance and angle!') 365 | self.distance = distance 366 | self.angle_from_north = angle 367 | self.move_function = self.move_by_angle_and_distance 368 | elif (latitude_diff is not None) and (longitude_diff is not None): 369 | if (distance is not None) or (angle is not None): 370 | raise Exception('No distance/angle if using lat/lon diff!') 371 | self.latitude_diff = latitude_diff 372 | self.longitude_diff = longitude_diff 373 | self.move_function = self.move_by_lat_lon_diff 374 | 375 | def move(self, location): 376 | """ 377 | Move location by this timedelta. 378 | """ 379 | return self.move_function(location) 380 | 381 | def move_by_angle_and_distance(self, location): 382 | coef = mod_math.cos(location.latitude / 180. * mod_math.pi) 383 | vertical_distance_diff = mod_math.sin((90 - self.angle_from_north) / 180. * mod_math.pi) / ONE_DEGREE 384 | horizontal_distance_diff = mod_math.cos((90 - self.angle_from_north) / 180. * mod_math.pi) / ONE_DEGREE 385 | lat_diff = self.distance * vertical_distance_diff 386 | lon_diff = self.distance * horizontal_distance_diff / coef 387 | return location.latitude + lat_diff, location.longitude + lon_diff 388 | 389 | def move_by_lat_lon_diff(self, location): 390 | return location.latitude + self.latitude_diff, location.longitude + self.longitude_diff 391 | -------------------------------------------------------------------------------- /gpxpy/gpxfield.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2014 Tomo Krajina 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import inspect as mod_inspect 18 | import datetime as mod_datetime 19 | import re as mod_re 20 | import copy as mod_copy 21 | 22 | from . import utils as mod_utils 23 | 24 | 25 | class GPXFieldTypeConverter: 26 | def __init__(self, from_string, to_string): 27 | self.from_string = from_string 28 | self.to_string = to_string 29 | 30 | 31 | def parse_time(string): 32 | from . import gpx as mod_gpx 33 | if not string: 34 | return None 35 | if 'T' in string: 36 | string = string.replace('T', ' ') 37 | if 'Z' in string: 38 | string = string.replace('Z', '') 39 | if '.' in string: 40 | string = string.split('.')[0] 41 | if len(string) > 19: 42 | # remove the timezone part 43 | d = max(string.rfind('+'), string.rfind('-')) 44 | string = string[0:d] 45 | if len(string) < 19: 46 | # string has some single digits 47 | p = '^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}).*$' 48 | s = mod_re.findall(p, string) 49 | if len(s) > 0: 50 | string = '{0}-{1:02d}-{2:02d} {3:02d}:{4:02d}:{5:02d}'\ 51 | .format(*[int(x) for x in s[0]]) 52 | for date_format in mod_gpx.DATE_FORMATS: 53 | try: 54 | return mod_datetime.datetime.strptime(string, date_format) 55 | except ValueError: 56 | pass 57 | raise mod_gpx.GPXException('Invalid time: {0}'.format(string)) 58 | 59 | 60 | # ---------------------------------------------------------------------------------------------------- 61 | # Type converters used to convert from/to the string in the XML: 62 | # ---------------------------------------------------------------------------------------------------- 63 | 64 | 65 | class FloatConverter: 66 | def __init__(self): 67 | self.from_string = lambda string : None if string is None else float(string.strip()) 68 | self.to_string = lambda flt : mod_utils.make_str(flt) 69 | 70 | 71 | class IntConverter: 72 | def __init__(self): 73 | self.from_string = lambda string: None if string is None else int(string.strip()) 74 | self.to_string = lambda flt: str(flt) 75 | 76 | 77 | class TimeConverter: 78 | def from_string(self, string): 79 | try: 80 | return parse_time(string) 81 | except: 82 | return None 83 | 84 | def to_string(self, time): 85 | from . import gpx as mod_gpx 86 | return time.strftime(mod_gpx.DATE_FORMAT) if time else None 87 | 88 | 89 | INT_TYPE = IntConverter() 90 | FLOAT_TYPE = FloatConverter() 91 | TIME_TYPE = TimeConverter() 92 | 93 | 94 | # ---------------------------------------------------------------------------------------------------- 95 | # Field converters: 96 | # ---------------------------------------------------------------------------------------------------- 97 | 98 | 99 | class AbstractGPXField: 100 | def __init__(self, attribute_field=None, is_list=None): 101 | self.attribute_field = attribute_field 102 | self.is_list = is_list 103 | self.attribute = False 104 | 105 | def from_xml(self, node, version): 106 | raise Exception('Not implemented') 107 | 108 | def to_xml(self, value, version, nsmap): 109 | raise Exception('Not implemented') 110 | 111 | 112 | class GPXField(AbstractGPXField): 113 | """ 114 | Used for to (de)serialize fields with simple field<->xml_tag mapping. 115 | """ 116 | def __init__(self, name, tag=None, attribute=None, type=None, 117 | possible=None, mandatory=None): 118 | AbstractGPXField.__init__(self) 119 | self.name = name 120 | if tag and attribute: 121 | from . import gpx as mod_gpx 122 | raise mod_gpx.GPXException('Only tag *or* attribute may be given!') 123 | if attribute: 124 | self.tag = None 125 | self.attribute = name if attribute is True else attribute 126 | elif tag: 127 | self.tag = name if tag is True else tag 128 | self.attribute = None 129 | else: 130 | self.tag = name 131 | self.attribute = None 132 | self.type_converter = type 133 | self.possible = possible 134 | self.mandatory = mandatory 135 | 136 | def from_xml(self, node, version): 137 | if self.attribute: 138 | if node is not None: 139 | result = node.get(self.attribute) 140 | else: 141 | __node = node.find(self.tag) 142 | if __node is not None: 143 | result = __node.text 144 | else: 145 | result = None 146 | if result is None: 147 | if self.mandatory: 148 | from . import gpx as mod_gpx 149 | raise mod_gpx.GPXException('{0} is mandatory in {1} (got {2})'.format(self.name, self.tag, result)) 150 | return None 151 | 152 | if self.type_converter: 153 | try: 154 | result = self.type_converter.from_string(result) 155 | except Exception as e: 156 | from . import gpx as mod_gpx 157 | raise mod_gpx.GPXException('Invalid value for <{0}>... {1} ({2})'.format(self.tag, result, e)) 158 | 159 | if self.possible: 160 | if not (result in self.possible): 161 | from . import gpx as mod_gpx 162 | raise mod_gpx.GPXException('Invalid value "{0}", possible: {1}'.format(result, self.possible)) 163 | 164 | return result 165 | 166 | def to_xml(self, value, version, nsmap=None, prettyprint=True, indent=''): 167 | if value is None: 168 | return '' 169 | if not prettyprint: 170 | indent = '' 171 | if self.attribute: 172 | return '{0}="{1}"'.format(self.attribute, mod_utils.make_str(value)) 173 | elif self.type_converter: 174 | value = self.type_converter.to_string(value) 175 | return mod_utils.to_xml(self.tag, content=value, escape=True, 176 | prettyprint=prettyprint, indent=indent) 177 | 178 | 179 | class GPXComplexField(AbstractGPXField): 180 | def __init__(self, name, classs, tag=None, is_list=None): 181 | AbstractGPXField.__init__(self, is_list=is_list) 182 | self.name = name 183 | self.tag = tag or name 184 | self.classs = classs 185 | 186 | def from_xml(self, node, version): 187 | if self.is_list: 188 | result = [] 189 | for child in node: 190 | if child.tag == self.tag: 191 | result.append(gpx_fields_from_xml(self.classs, child, 192 | version)) 193 | return result 194 | else: 195 | field_node = node.find(self.tag) 196 | if field_node is None: 197 | return None 198 | return gpx_fields_from_xml(self.classs, field_node, version) 199 | 200 | def to_xml(self, value, version, nsmap=None, prettyprint=True, indent=''): 201 | if not prettyprint: 202 | indent = '' 203 | if self.is_list: 204 | result = [] 205 | for obj in value: 206 | result.append(gpx_fields_to_xml(obj, self.tag, version, 207 | nsmap=nsmap, 208 | prettyprint=prettyprint, 209 | indent=indent)) 210 | return ''.join(result) 211 | else: 212 | return gpx_fields_to_xml(value, self.tag, version, 213 | prettyprint=prettyprint, indent=indent) 214 | 215 | 216 | class GPXEmailField(AbstractGPXField): 217 | """ 218 | Converts GPX1.1 email tag group from/to string. 219 | """ 220 | def __init__(self, name, tag=None): 221 | AbstractGPXField.__init__(self, is_list=False) 222 | self.name = name 223 | self.tag = tag or name 224 | 225 | def from_xml(self, node, version): 226 | """ 227 | Extract email address. 228 | 229 | Args: 230 | node: ETree node with child node containing self.tag 231 | version: str of the gpx output version "1.0" or "1.1" 232 | 233 | Returns: 234 | A string containing the email address. 235 | """ 236 | email_node = node.find(self.tag) 237 | if email_node is None: 238 | return '' 239 | 240 | email_id = email_node.get('id') 241 | email_domain = email_node.get('domain') 242 | return '{0}@{1}'.format(email_id, email_domain) 243 | 244 | def to_xml(self, value, version, nsmap=None, prettyprint=True, indent=''): 245 | """ 246 | Write email address to XML 247 | 248 | Args: 249 | value: str representing an email address 250 | version: str of the gpx output version "1.0" or "1.1" 251 | 252 | Returns: 253 | None if value is empty or str of XML representation of the 254 | address. Representation starts with a \n. 255 | """ 256 | if not value: 257 | return '' 258 | 259 | if not prettyprint: 260 | indent = '' 261 | 262 | if '@' in value: 263 | pos = value.find('@') 264 | email_id = value[:pos] 265 | email_domain = value[pos+1:] 266 | else: 267 | email_id = value 268 | email_domain = 'unknown' 269 | 270 | return ('\n' + indent + 271 | '<{0} id="{1}" domain="{2}" />'.format(self.tag, 272 | email_id, email_domain)) 273 | 274 | 275 | class GPXExtensionsField(AbstractGPXField): 276 | """ 277 | GPX1.1 extensions ... key-value type. 278 | """ 279 | def __init__(self, name, tag=None, is_list=True): 280 | AbstractGPXField.__init__(self, is_list=is_list) 281 | self.name = name 282 | self.tag = tag or 'extensions' 283 | 284 | def from_xml(self, node, version): 285 | """ 286 | Build a list of extension Elements. 287 | 288 | Args: 289 | node: Element at the root of the extensions 290 | version: unused, only 1.1 supports extensions 291 | 292 | Returns: 293 | a list of Element objects 294 | """ 295 | result = [] 296 | extensions_node = node.find(self.tag) 297 | if extensions_node is None: 298 | return result 299 | for child in extensions_node: 300 | result.append(mod_copy.deepcopy(child)) 301 | return result 302 | 303 | def _resolve_prefix(self, qname, nsmap): 304 | """ 305 | Convert a tag from Clark notation into prefix notation. 306 | 307 | Convert a tag from Clark notation using the nsmap into a 308 | prefixed tag. If the tag isn't in Clark notation, return the 309 | qname back. Converts {namespace}tag -> prefix:tag 310 | 311 | Args: 312 | qname: string with the fully qualified name in Clark notation 313 | nsmap: a dict of prefix, namespace pairs 314 | 315 | Returns: 316 | string of the tag ready to be serialized. 317 | """ 318 | if nsmap is not None and '}' in qname: 319 | uri, _, localname = qname.partition("}") 320 | uri = uri.lstrip("{") 321 | qname = uri + ':' + localname 322 | for prefix, namespace in nsmap.items(): 323 | if uri == namespace: 324 | qname = prefix + ':' + localname 325 | break 326 | return qname 327 | 328 | def _ETree_to_xml(self, node, nsmap=None, prettyprint=True, indent=''): 329 | """ 330 | Serialize ETree element and all subelements. 331 | 332 | Creates a string of the ETree and all children. The prefixes are 333 | resolved through the nsmap for easier to read XML. 334 | 335 | Args: 336 | node: ETree with the extension data 337 | version: string of GPX version, must be 1.1 338 | nsmap: dict of prefixes and URIs 339 | prettyprint: boolean, when true, indent line 340 | indent: string prepended to tag, usually 2 spaces per level 341 | 342 | Returns: 343 | string with all the prefixed tags and data for the node 344 | and its children as XML. 345 | 346 | """ 347 | if not prettyprint: 348 | indent = '' 349 | 350 | # Build element tag and text 351 | result = [] 352 | prefixedname = self._resolve_prefix(node.tag, nsmap) 353 | result.append('\n' + indent + '<' + prefixedname) 354 | for attrib, value in node.attrib.items(): 355 | attrib = self._resolve_prefix(attrib, nsmap) 356 | result.append(' {0}="{1}"'.format(attrib, value)) 357 | result.append('>') 358 | if node.text is not None: 359 | result.append(node.text.strip()) 360 | 361 | 362 | # Build subelement nodes 363 | for child in node: 364 | result.append(self._ETree_to_xml(child, nsmap, 365 | prettyprint=prettyprint, 366 | indent=indent+' ')) 367 | 368 | # Add tail and close tag 369 | tail = node.tail 370 | if tail is not None: 371 | tail = tail.strip() 372 | else: 373 | tail = '' 374 | if len(node) > 0: 375 | result.append('\n' + indent) 376 | result.append('' + tail) 377 | 378 | return ''.join(result) 379 | 380 | def to_xml(self, value, version, nsmap=None, prettyprint=True, indent=''): 381 | """ 382 | Serialize list of ETree. 383 | 384 | Creates a string of all the ETrees in the list. The prefixes are 385 | resolved through the nsmap for easier to read XML. 386 | 387 | Args: 388 | value: list of ETrees with the extension data 389 | version: string of GPX version, must be 1.1 390 | nsmap: dict of prefixes and URIs 391 | prettyprint: boolean, when true, indent line 392 | indent: string prepended to tag, usually 2 spaces per level 393 | 394 | Returns: 395 | string with all the prefixed tags and data for each node 396 | as XML. 397 | 398 | """ 399 | if not prettyprint: 400 | indent = '' 401 | if not value or version != "1.1": 402 | return '' 403 | result = [] 404 | result.append('\n' + indent + '<' + self.tag + '>') 405 | for extension in value: 406 | result.append(self._ETree_to_xml(extension, nsmap, 407 | prettyprint=prettyprint, 408 | indent=indent+' ')) 409 | result.append('\n' + indent + '') 410 | return ''.join(result) 411 | 412 | # ---------------------------------------------------------------------------------------------------- 413 | # Utility methods: 414 | # ---------------------------------------------------------------------------------------------------- 415 | 416 | def _check_dependents(gpx_object, fieldname): 417 | """ 418 | Check for data in subelements. 419 | 420 | Fieldname takes the form of 'tag:dep1:dep2:dep3' for an arbitrary 421 | number of dependents. If all the gpx_object.dep attributes are 422 | empty, return a sentinel value to suppress serialization of all 423 | subelements. 424 | 425 | Args: 426 | gpx_object: GPXField object to check for data 427 | fieldname: string with tag and dependents delimited with ':' 428 | 429 | Returns: 430 | Two strings. The first is a sentinel value, '/' + tag, if all 431 | the subelements are empty and an empty string otherwise. The 432 | second is the bare tag name. 433 | """ 434 | if ':' in fieldname: 435 | children = fieldname.split(':') 436 | field = children.pop(0) 437 | for child in children: 438 | if getattr(gpx_object, child.lstrip('@')): 439 | return '', field # Child has data 440 | return '/' + field, field # No child has data 441 | return '', fieldname # No children 442 | 443 | def gpx_fields_to_xml(instance, tag, version, custom_attributes=None, 444 | nsmap=None, prettyprint=True, indent=''): 445 | if not prettyprint: 446 | indent = '' 447 | fields = instance.gpx_10_fields 448 | if version == '1.1': 449 | fields = instance.gpx_11_fields 450 | 451 | tag_open = bool(tag) 452 | body = [] 453 | if tag: 454 | body.append('\n' + indent + '<' + tag) 455 | if tag == 'gpx': # write nsmap in root node 456 | body.append(' xmlns="{0}"'.format(nsmap['defaultns'])) 457 | namespaces = set(nsmap.keys()) 458 | namespaces.remove('defaultns') 459 | for prefix in sorted(namespaces): 460 | body.append( 461 | ' xmlns:{0}="{1}"'.format(prefix, nsmap[prefix]) 462 | ) 463 | if custom_attributes: 464 | # Make sure to_xml() always return attributes in the same order: 465 | for key in sorted(custom_attributes.keys()): 466 | body.append(' {0}="{1}"'.format(key, mod_utils.make_str(custom_attributes[key]))) 467 | suppressuntil = '' 468 | for gpx_field in fields: 469 | # strings indicate non-data container tags with subelements 470 | if isinstance(gpx_field, str): 471 | # Suppress empty tags 472 | if suppressuntil: 473 | if suppressuntil == gpx_field: 474 | suppressuntil = '' 475 | else: 476 | suppressuntil, gpx_field = _check_dependents(instance, 477 | gpx_field) 478 | if not suppressuntil: 479 | if tag_open: 480 | body.append('>') 481 | tag_open = False 482 | if gpx_field[0] == '/': 483 | body.append('\n' + indent + '<{0}>'.format(gpx_field)) 484 | if prettyprint and len(indent) > 1: 485 | indent = indent[:-2] 486 | else: 487 | if prettyprint: 488 | indent += ' ' 489 | body.append('\n' + indent + '<{0}'.format(gpx_field)) 490 | tag_open = True 491 | elif not suppressuntil: 492 | value = getattr(instance, gpx_field.name) 493 | if gpx_field.attribute: 494 | body.append(' ' + gpx_field.to_xml(value, version, nsmap, 495 | prettyprint=prettyprint, 496 | indent=indent + ' ')) 497 | elif value is not None: 498 | if tag_open: 499 | body.append('>') 500 | tag_open = False 501 | xml_value = gpx_field.to_xml(value, version, nsmap, 502 | prettyprint=prettyprint, 503 | indent=indent + ' ') 504 | if xml_value: 505 | body.append(xml_value) 506 | 507 | if tag: 508 | if tag_open: 509 | body.append('>') 510 | body.append('\n' + indent + '') 511 | 512 | return ''.join(body) 513 | 514 | 515 | def gpx_fields_from_xml(class_or_instance, node, version): 516 | if mod_inspect.isclass(class_or_instance): 517 | result = class_or_instance() 518 | else: 519 | result = class_or_instance 520 | 521 | fields = result.gpx_10_fields 522 | if version == '1.1': 523 | fields = result.gpx_11_fields 524 | 525 | node_path = [node] 526 | 527 | for gpx_field in fields: 528 | current_node = node_path[-1] 529 | if isinstance(gpx_field, str): 530 | gpx_field = gpx_field.partition(':')[0] 531 | if gpx_field.startswith('/'): 532 | node_path.pop() 533 | else: 534 | if current_node is None: 535 | node_path.append(None) 536 | else: 537 | node_path.append(current_node.find(gpx_field)) 538 | else: 539 | if current_node is not None: 540 | value = gpx_field.from_xml(current_node, version) 541 | setattr(result, gpx_field.name, value) 542 | elif gpx_field.attribute: 543 | value = gpx_field.from_xml(node, version) 544 | setattr(result, gpx_field.name, value) 545 | 546 | return result 547 | 548 | def gpx_check_slots_and_default_values(classs): 549 | """ 550 | Will fill the default values for this class. Instances will inherit those 551 | values so we don't need to fill default values for every instance. 552 | This method will also fill the attribute gpx_field_names with a list of 553 | gpx field names. This can be used 554 | """ 555 | fields = classs.gpx_10_fields + classs.gpx_11_fields 556 | 557 | gpx_field_names = [] 558 | 559 | instance = classs() 560 | 561 | try: 562 | attributes = list(filter(lambda x : x[0] != '_', dir(instance))) 563 | attributes = list(filter(lambda x : not callable(getattr(instance, x)), attributes)) 564 | attributes = list(filter(lambda x : not x.startswith('gpx_'), attributes)) 565 | except Exception as e: 566 | raise Exception('Error reading attributes for %s: %s' % (classs.__name__, e)) 567 | 568 | attributes.sort() 569 | slots = list(classs.__slots__) 570 | slots.sort() 571 | 572 | if attributes != slots: 573 | raise Exception('Attributes for %s is\n%s but should be\n%s' % (classs.__name__, attributes, slots)) 574 | 575 | for field in fields: 576 | if not isinstance(field, str): 577 | if field.is_list: 578 | value = [] 579 | else: 580 | value = None 581 | try: 582 | actual_value = getattr(instance, field.name) 583 | except: 584 | raise Exception('%s has no attribute %s' % (classs.__name__, field.name)) 585 | if value != actual_value: 586 | raise Exception('Invalid default value %s.%s is %s but should be %s' 587 | % (classs.__name__, field.name, actual_value, value)) 588 | #print('%s.%s -> %s' % (classs, field.name, value)) 589 | if not field.name in gpx_field_names: 590 | gpx_field_names.append(field.name) 591 | 592 | gpx_field_names = tuple(gpx_field_names) 593 | ## if not hasattr(classs, '__slots__') or not classs.__slots__ or classs.__slots__ != gpx_field_names: 594 | ## try: slots = classs.__slots__ 595 | ## except Exception as e: slots = '[Unknown:%s]' % e 596 | ## raise Exception('%s __slots__ invalid, found %s, but should be %s' % (classs, slots, gpx_field_names)) 597 | -------------------------------------------------------------------------------- /test_files/Mojstrovka.gpx: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 1614.678000 14 | 15 | 16 | 17 | 1636.776000 18 | 19 | 20 | 21 | 1632.935520 22 | 23 | 24 | 25 | 1631.502960 26 | 27 | 28 | 29 | 1629.095040 30 | 31 | 32 | 33 | 1628.119680 34 | 35 | 36 | 37 | 1626.199440 38 | 39 | 40 | 41 | 1627.174800 42 | 43 | 44 | 45 | 1652.168400 46 | 47 | 48 | 49 | 1684.842960 50 | 51 | 52 | 53 | 1688.226240 54 | 55 | 56 | 57 | 1688.226240 58 | 59 | 60 | 61 | 1694.931840 62 | 63 | 64 | 65 | 1699.747680 66 | 67 | 68 | 69 | 1701.180240 70 | 71 | 72 | 73 | 1702.155600 74 | 75 | 76 | 77 | 1716.084960 78 | 79 | 80 | 81 | 1714.652400 82 | 83 | 84 | 85 | 1718.980560 86 | 87 | 88 | 89 | 1725.716640 90 | 91 | 92 | 93 | 1726.173840 94 | 95 | 96 | 97 | 1727.149200 98 | 99 | 100 | 101 | 1728.094080 102 | 103 | 104 | 105 | 1738.670640 106 | 107 | 108 | 109 | 1874.215200 110 | 111 | 112 | 113 | 1883.359200 114 | 115 | 116 | 117 | 1887.687360 118 | 119 | 120 | 121 | 1886.254800 122 | 123 | 124 | 125 | 1921.337280 126 | 127 | 128 | 129 | 1959.315360 130 | 131 | 132 | 133 | 1945.843200 134 | 135 | 136 | 137 | 1946.330880 138 | 139 | 140 | 141 | 1951.603920 142 | 143 | 144 | 145 | 1954.987200 146 | 147 | 148 | 149 | 1956.419760 150 | 151 | 152 | 153 | 1961.235600 154 | 155 | 156 | 157 | 1968.428880 158 | 159 | 160 | 161 | 1972.757040 162 | 163 | 164 | 165 | 1973.244720 166 | 167 | 168 | 169 | 1975.652640 170 | 171 | 172 | 173 | 1981.413360 174 | 175 | 176 | 177 | 1992.965280 178 | 179 | 180 | 181 | 1999.670880 182 | 183 | 184 | 185 | 2000.646240 186 | 187 | 188 | 189 | 2015.063280 190 | 191 | 192 | 193 | 2019.391440 194 | 195 | 196 | 197 | 2014.575600 198 | 199 | 200 | 201 | 2019.879120 202 | 203 | 204 | 205 | 2024.207280 206 | 207 | 208 | 209 | 2025.152160 210 | 211 | 212 | 213 | 2023.719600 214 | 215 | 216 | 217 | 2028.992640 218 | 219 | 220 | 221 | 2032.375920 222 | 223 | 224 | 225 | 2057.369520 226 | 227 | 228 | 229 | 2050.145760 230 | 231 | 232 | 233 | 2048.713200 234 | 235 | 236 | 237 | 2046.792960 238 | 239 | 240 | 241 | 2042.464800 242 | 243 | 244 | 245 | 2038.624320 246 | 247 | 248 | 249 | 2034.753360 250 | 251 | 252 | 253 | 2033.320800 254 | 255 | 256 | 257 | 2029.968000 258 | 259 | 260 | 261 | 2027.072400 262 | 263 | 264 | 265 | 2022.744240 266 | 267 | 268 | 269 | 2021.799360 270 | 271 | 272 | 273 | 2018.416080 274 | 275 | 276 | 277 | 2016.983520 278 | 279 | 280 | 281 | 2015.550960 282 | 283 | 284 | 285 | 2008.814880 286 | 287 | 288 | 289 | 2003.054160 290 | 291 | 292 | 293 | 1997.750640 294 | 295 | 296 | 297 | 1999.213680 298 | 299 | 300 | 301 | 1991.014560 302 | 303 | 304 | 305 | 1983.333600 306 | 307 | 308 | 309 | 1981.901040 310 | 311 | 312 | 313 | 1982.845920 314 | 315 | 316 | 317 | 1979.005440 318 | 319 | 320 | 321 | 1976.140320 322 | 323 | 324 | 325 | 1972.757040 326 | 327 | 328 | 329 | 1970.836800 330 | 331 | 332 | 333 | 1963.643520 334 | 335 | 336 | 337 | 1959.772560 338 | 339 | 340 | 341 | 1956.907440 342 | 343 | 344 | 345 | 1955.444400 346 | 347 | 348 | 349 | 1954.011840 350 | 351 | 352 | 353 | 1948.738800 354 | 355 | 356 | 357 | 1946.818560 358 | 359 | 360 | 361 | 1943.922960 362 | 363 | 364 | 365 | 1942.490400 366 | 367 | 368 | 369 | 1942.490400 370 | 371 | 372 | 373 | 1942.490400 374 | 375 | 376 | 377 | 1942.490400 378 | 379 | 380 | 381 | 1942.490400 382 | 383 | 384 | 385 | 1942.490400 386 | 387 | 388 | 389 | 1942.490400 390 | 391 | 392 | 393 | 1873.270320 394 | 395 | 396 | 397 | 1867.021920 398 | 399 | 400 | 401 | 1869.429840 402 | 403 | 404 | 405 | 1863.181440 406 | 407 | 408 | 409 | 1864.614000 410 | 411 | 412 | 413 | 1864.614000 414 | 415 | 416 | 417 | 1864.614000 418 | 419 | 420 | 421 | 1864.614000 422 | 423 | 424 | 425 | 1864.614000 426 | 427 | 428 | 429 | 1852.604880 430 | 431 | 432 | 433 | 1846.356480 434 | 435 | 436 | 437 | 1837.700160 438 | 439 | 440 | 441 | 1833.859680 442 | 443 | 444 | 445 | 1829.531520 446 | 447 | 448 | 449 | 1827.123600 450 | 451 | 452 | 453 | 1822.307760 454 | 455 | 456 | 457 | 1819.899840 458 | 459 | 460 | 461 | 1816.547040 462 | 463 | 464 | 465 | 1813.651440 466 | 467 | 468 | 469 | 1813.651440 470 | 471 | 472 | 473 | 1811.274000 474 | 475 | 476 | 477 | 1809.810960 478 | 479 | 480 | 481 | 1807.890720 482 | 483 | 484 | 485 | 1805.482800 486 | 487 | 488 | 489 | 1800.697440 490 | 491 | 492 | 493 | 1794.906240 494 | 495 | 496 | 497 | 1792.041120 498 | 499 | 500 | 501 | 1788.200640 502 | 503 | 504 | 505 | 1786.737600 506 | 507 | 508 | 509 | 1782.897120 510 | 511 | 512 | 513 | 1779.056640 514 | 515 | 516 | 517 | 1779.544320 518 | 519 | 520 | 521 | 1772.320560 522 | 523 | 524 | 525 | 1767.535200 526 | 527 | 528 | 529 | 1760.799120 530 | 531 | 532 | 533 | 1759.823760 534 | 535 | 536 | 537 | 1759.823760 538 | 539 | 540 | 541 | 1757.903520 542 | 543 | 544 | 545 | 1757.415840 546 | 547 | 548 | 549 | 1755.495600 550 | 551 | 552 | 553 | 1753.087680 554 | 555 | 556 | 557 | 1751.655120 558 | 559 | 560 | 561 | 1748.302320 562 | 563 | 564 | 565 | 1747.326960 566 | 567 | 568 | 569 | 1742.053920 570 | 571 | 572 | 573 | 1737.238080 574 | 575 | 576 | 577 | 1736.293200 578 | 579 | 580 | 581 | 1734.342480 582 | 583 | 584 | 585 | 1732.909920 586 | 587 | 588 | 589 | 1730.989680 590 | 591 | 592 | 593 | 1726.173840 594 | 595 | 596 | 597 | 1723.765920 598 | 599 | 600 | 601 | 1719.468240 602 | 603 | 604 | 605 | 1715.140080 606 | 607 | 608 | 609 | 1714.164720 610 | 611 | 612 | 613 | 1711.756800 614 | 615 | 616 | 617 | 1710.811920 618 | 619 | 620 | 621 | 1708.891680 622 | 623 | 624 | 625 | 1708.404000 626 | 627 | 628 | 629 | 1704.563520 630 | 631 | 632 | 633 | 1702.643280 634 | 635 | 636 | 637 | 1700.235360 638 | 639 | 640 | 641 | 1696.852080 642 | 643 | 644 | 645 | 1695.907200 646 | 647 | 648 | 649 | 1693.499280 650 | 651 | 652 | 653 | 1692.523920 654 | 655 | 656 | 657 | 1689.658800 658 | 659 | 660 | 661 | 1686.763200 662 | 663 | 664 | 665 | 1686.763200 666 | 667 | 668 | 669 | 1686.763200 670 | 671 | 672 | 673 | 1686.763200 674 | 675 | 676 | 677 | 1686.275520 678 | 679 | 680 | 681 | 1686.275520 682 | 683 | 684 | 685 | 1686.275520 686 | 687 | 688 | 689 | 1685.818320 690 | 691 | 692 | 693 | 1685.818320 694 | 695 | 696 | 697 | 1685.330640 698 | 699 | 700 | 701 | 1685.330640 702 | 703 | 704 | 705 | 1685.330640 706 | 707 | 708 | 709 | 1685.330640 710 | 711 | 712 | 713 | 1684.842960 714 | 715 | 716 | 717 | 1677.162000 718 | 719 | 720 | 721 | 1677.162000 722 | 723 | 724 | 725 | 1677.162000 726 | 727 | 728 | 729 | 1659.849360 730 | 731 | 732 | 733 | 1652.168400 734 | 735 | 736 | 737 | 1649.272800 738 | 739 | 740 | 741 | 1644.944640 742 | 743 | 744 | 745 | 1643.512080 746 | 747 | 748 | 749 | 750 | 751 | -------------------------------------------------------------------------------- /test_files/cerknicko-without-times.gpx: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 001 12 | 05-AUG-10 16:58:37 13 | 05-AUG-10 16:58:37 14 | Flag, Blue 15 | 16 | 17 | -0.114380 18 | BACK T TH 19 | BACK TO THE ROOTS 20 | BACK TO THE ROOTS 21 | City (Small) 22 | 23 | 24 | -0.114380 25 | BIRDS NEST 26 | BIRDS NEST 27 | BIRDS NEST 28 | City (Small) 29 | 30 | 31 | -0.114380 32 | FAGGIO 33 | FAGGIO 34 | FAGGIO 35 | City (Small) 36 | 37 | 38 | -0.114380 39 | RAKOV12 40 | RAKOV12 41 | RAKOV12 42 | City (Small) 43 | 44 | 45 | -0.114380 46 | RAKV SKCJN 47 | RAKOV SKOCJAN 48 | RAKOV SKOCJAN 49 | City (Small) 50 | 51 | 52 | -0.114380 53 | VANSHNG LK 54 | VANISHING LAKE 55 | VANISHING LAKE 56 | City (Small) 57 | 58 | 59 | ACTIVE LOG 60 | 61 | 62 | 63 | 64 | ACTIVE LOG #2 65 | 1 66 | 67 | 68 | 542.320923 69 | 70 | 71 | 550.972656 72 | 73 | 74 | 553.856689 75 | 76 | 77 | 555.779297 78 | 79 | 80 | 555.779297 81 | 82 | 83 | 555.298584 84 | 85 | 86 | 553.856689 87 | 88 | 89 | 552.414551 90 | 91 | 92 | 551.934082 93 | 94 | 95 | 552.895264 96 | 97 | 98 | 552.414551 99 | 100 | 101 | 552.895264 102 | 103 | 104 | 552.895264 105 | 106 | 107 | 552.895264 108 | 109 | 110 | 552.414551 111 | 112 | 113 | 552.414551 114 | 115 | 116 | 551.453369 117 | 118 | 119 | 550.972656 120 | 121 | 122 | 551.453369 123 | 124 | 125 | 551.934082 126 | 127 | 128 | 551.934082 129 | 130 | 131 | 550.492188 132 | 133 | 134 | 550.011475 135 | 136 | 137 | 550.011475 138 | 139 | 140 | 549.530762 141 | 142 | 143 | 548.088867 144 | 145 | 146 | 547.608154 147 | 148 | 149 | 547.608154 150 | 151 | 152 | 548.088867 153 | 154 | 155 | 548.569336 156 | 157 | 158 | 548.569336 159 | 160 | 161 | 548.088867 162 | 163 | 164 | 547.608154 165 | 166 | 167 | 548.088867 168 | 169 | 170 | 549.530762 171 | 172 | 173 | 550.972656 174 | 175 | 176 | 550.972656 177 | 178 | 179 | 551.453369 180 | 181 | 182 | 550.972656 183 | 184 | 185 | 551.453369 186 | 187 | 188 | 551.453369 189 | 190 | 191 | 551.453369 192 | 193 | 194 | 551.453369 195 | 196 | 197 | 551.453369 198 | 199 | 200 | 549.050049 201 | 202 | 203 | 546.646851 204 | 205 | 206 | 548.088867 207 | 208 | 209 | 546.166260 210 | 211 | 212 | 548.569336 213 | 214 | 215 | 550.011475 216 | 217 | 218 | 552.895264 219 | 220 | 221 | 553.856689 222 | 223 | 224 | 553.375977 225 | 226 | 227 | 552.895264 228 | 229 | 230 | 553.375977 231 | 232 | 233 | 553.856689 234 | 235 | 236 | 553.856689 237 | 238 | 239 | 551.453369 240 | 241 | 242 | 552.895264 243 | 244 | 245 | 553.856689 246 | 247 | 248 | 553.375977 249 | 250 | 251 | 553.375977 252 | 253 | 254 | 553.856689 255 | 256 | 257 | 553.856689 258 | 259 | 260 | 554.337402 261 | 262 | 263 | 554.337402 264 | 265 | 266 | 553.856689 267 | 268 | 269 | 552.895264 270 | 271 | 272 | 553.375977 273 | 274 | 275 | 552.895264 276 | 277 | 278 | 553.375977 279 | 280 | 281 | 553.375977 282 | 283 | 284 | 553.375977 285 | 286 | 287 | 553.375977 288 | 289 | 290 | 553.375977 291 | 292 | 293 | 553.856689 294 | 295 | 296 | 554.337402 297 | 298 | 299 | 553.856689 300 | 301 | 302 | 553.375977 303 | 304 | 305 | 552.895264 306 | 307 | 308 | 552.895264 309 | 310 | 311 | 552.414551 312 | 313 | 314 | 551.453369 315 | 316 | 317 | 550.492188 318 | 319 | 320 | 550.492188 321 | 322 | 323 | 550.972656 324 | 325 | 326 | 550.492188 327 | 328 | 329 | 550.492188 330 | 331 | 332 | 549.530762 333 | 334 | 335 | 549.050049 336 | 337 | 338 | 550.011475 339 | 340 | 341 | 550.972656 342 | 343 | 344 | 551.934082 345 | 346 | 347 | 550.972656 348 | 349 | 350 | 549.530762 351 | 352 | 353 | 548.088867 354 | 355 | 356 | 546.646851 357 | 358 | 359 | 546.166260 360 | 361 | 362 | 545.685547 363 | 364 | 365 | 545.685547 366 | 367 | 368 | 547.127441 369 | 370 | 371 | 548.088867 372 | 373 | 374 | 548.088867 375 | 376 | 377 | 548.569336 378 | 379 | 380 | 547.127441 381 | 382 | 383 | 546.646851 384 | 385 | 386 | 546.166260 387 | 388 | 389 | 546.166260 390 | 391 | 392 | 545.685547 393 | 394 | 395 | 542.801514 396 | 397 | 398 | 543.762817 399 | 400 | 401 | 543.762817 402 | 403 | 404 | 543.762817 405 | 406 | 407 | 544.724243 408 | 409 | 410 | 544.724243 411 | 412 | 413 | 545.204834 414 | 415 | 416 | 545.685547 417 | 418 | 419 | 545.204834 420 | 421 | 422 | 545.685547 423 | 424 | 425 | 545.685547 426 | 427 | 428 | 545.204834 429 | 430 | 431 | 544.243652 432 | 433 | 434 | 543.282104 435 | 436 | 437 | 544.243652 438 | 439 | 440 | 545.685547 441 | 442 | 443 | 546.646851 444 | 445 | 446 | 546.646851 447 | 448 | 449 | 550.011475 450 | 451 | 452 | 550.492188 453 | 454 | 455 | 550.492188 456 | 457 | 458 | 550.972656 459 | 460 | 461 | 550.972656 462 | 463 | 464 | 550.972656 465 | 466 | 467 | 550.492188 468 | 469 | 470 | 550.492188 471 | 472 | 473 | 550.972656 474 | 475 | 476 | 551.934082 477 | 478 | 479 | 551.934082 480 | 481 | 482 | 551.934082 483 | 484 | 485 | 551.934082 486 | 487 | 488 | 552.414551 489 | 490 | 491 | 552.895264 492 | 493 | 494 | 551.453369 495 | 496 | 497 | 550.972656 498 | 499 | 500 | 550.492188 501 | 502 | 503 | 550.972656 504 | 505 | 506 | 550.492188 507 | 508 | 509 | 550.492188 510 | 511 | 512 | 550.011475 513 | 514 | 515 | 549.530762 516 | 517 | 518 | 550.492188 519 | 520 | 521 | 550.972656 522 | 523 | 524 | 550.492188 525 | 526 | 527 | 551.453369 528 | 529 | 530 | 556.260010 531 | 532 | 533 | 553.856689 534 | 535 | 536 | 554.337402 537 | 538 | 539 | 553.856689 540 | 541 | 542 | 552.414551 543 | 544 | 545 | 551.453369 546 | 547 | 548 | 550.492188 549 | 550 | 551 | 549.530762 552 | 553 | 554 | 546.166260 555 | 556 | 557 | 545.685547 558 | 559 | 560 | 543.282104 561 | 562 | 563 | 544.243652 564 | 565 | 566 | 546.166260 567 | 568 | 569 | 546.646851 570 | 571 | 572 | 545.685547 573 | 574 | 575 | 546.646851 576 | 577 | 578 | 545.685547 579 | 580 | 581 | 544.243652 582 | 583 | 584 | 543.282104 585 | 586 | 587 | 588 | 589 | ACTIVE LOG #3 590 | 2 591 | 592 | 593 | 546.646851 594 | 595 | 596 | 549.050049 597 | 598 | 599 | 548.088867 600 | 601 | 602 | 548.569336 603 | 604 | 605 | 548.569336 606 | 607 | 608 | 548.569336 609 | 610 | 611 | 548.569336 612 | 613 | 614 | 549.050049 615 | 616 | 617 | 549.530762 618 | 619 | 620 | 549.530762 621 | 622 | 623 | 549.530762 624 | 625 | 626 | 549.530762 627 | 628 | 629 | 549.530762 630 | 631 | 632 | 549.530762 633 | 634 | 635 | 549.530762 636 | 637 | 638 | 549.530762 639 | 640 | 641 | 550.011475 642 | 643 | 644 | 550.492188 645 | 646 | 647 | 550.972656 648 | 649 | 650 | 551.453369 651 | 652 | 653 | 550.972656 654 | 655 | 656 | 550.492188 657 | 658 | 659 | 550.011475 660 | 661 | 662 | 550.011475 663 | 664 | 665 | 550.011475 666 | 667 | 668 | 550.011475 669 | 670 | 671 | 550.011475 672 | 673 | 674 | 550.492188 675 | 676 | 677 | 550.492188 678 | 679 | 680 | 550.492188 681 | 682 | 683 | 550.972656 684 | 685 | 686 | 550.492188 687 | 688 | 689 | 550.972656 690 | 691 | 692 | 550.972656 693 | 694 | 695 | 550.972656 696 | 697 | 698 | 551.453369 699 | 700 | 701 | 551.453369 702 | 703 | 704 | 551.453369 705 | 706 | 707 | 551.934082 708 | 709 | 710 | 551.453369 711 | 712 | 713 | 551.934082 714 | 715 | 716 | 551.934082 717 | 718 | 719 | 551.934082 720 | 721 | 722 | 551.453369 723 | 724 | 725 | 551.934082 726 | 727 | 728 | 551.934082 729 | 730 | 731 | 551.934082 732 | 733 | 734 | 551.453369 735 | 736 | 737 | 551.453369 738 | 739 | 740 | 551.453369 741 | 742 | 743 | 551.453369 744 | 745 | 746 | 550.972656 747 | 748 | 749 | 750 | 751 | ACTIVE LOG #4 752 | 3 753 | 754 | 755 | 506.752075 756 | 757 | 758 | 544.243652 759 | 760 | 761 | 762 | 763 | ACTIVE LOG #5 764 | 4 765 | 766 | 767 | 545.685547 768 | 769 | 770 | 551.934082 771 | 772 | 773 | 552.414551 774 | 775 | 776 | 553.375977 777 | 778 | 779 | 553.375977 780 | 781 | 782 | 552.414551 783 | 784 | 785 | 552.895264 786 | 787 | 788 | 553.375977 789 | 790 | 791 | 552.414551 792 | 793 | 794 | 552.895264 795 | 796 | 797 | 553.375977 798 | 799 | 800 | 553.375977 801 | 802 | 803 | 553.375977 804 | 805 | 806 | 553.375977 807 | 808 | 809 | 553.375977 810 | 811 | 812 | 553.375977 813 | 814 | 815 | 553.375977 816 | 817 | 818 | 553.375977 819 | 820 | 821 | 553.375977 822 | 823 | 824 | 553.375977 825 | 826 | 827 | 553.856689 828 | 829 | 830 | 554.337402 831 | 832 | 833 | 553.856689 834 | 835 | 836 | 553.856689 837 | 838 | 839 | 553.375977 840 | 841 | 842 | 552.895264 843 | 844 | 845 | 552.895264 846 | 847 | 848 | 552.895264 849 | 850 | 851 | 552.895264 852 | 853 | 854 | 552.414551 855 | 856 | 857 | 552.895264 858 | 859 | 860 | 552.895264 861 | 862 | 863 | 551.934082 864 | 865 | 866 | 551.934082 867 | 868 | 869 | 551.934082 870 | 871 | 872 | 551.453369 873 | 874 | 875 | 551.934082 876 | 877 | 878 | 551.934082 879 | 880 | 881 | 552.414551 882 | 883 | 884 | 552.414551 885 | 886 | 887 | 552.414551 888 | 889 | 890 | 552.414551 891 | 892 | 893 | 552.414551 894 | 895 | 896 | 555.779297 897 | 898 | 899 | 900 | 901 | ACTIVE LOG #6 902 | 5 903 | 904 | 905 | 511.558716 906 | 907 | 908 | 542.320923 909 | 910 | 911 | 912 | 913 | ACTIVE LOG #7 914 | 6 915 | 916 | 917 | 544.243652 918 | 919 | 920 | 543.282104 921 | 922 | 923 | 924 | 925 | ACTIVE LOG #8 926 | 7 927 | 928 | 929 | 511.078003 930 | 931 | 932 | 556.260010 933 | 934 | 935 | 555.298584 936 | 937 | 938 | 556.740723 939 | 940 | 941 | 561.066650 942 | 943 | 944 | 579.331543 945 | 946 | 947 | 578.370361 948 | 949 | 950 | 565.392578 951 | 952 | 953 | 549.530762 954 | 955 | 956 | 540.398315 957 | 958 | 959 | 540.398315 960 | 961 | 962 | 540.878906 963 | 964 | 965 | 546.646851 966 | 967 | 968 | 547.608154 969 | 970 | 971 | 551.934082 972 | 973 | 974 | 554.337402 975 | 976 | 977 | 556.740723 978 | 979 | 980 | 981 | 559.144043 982 | 983 | 984 | 985 | 561.066650 986 | 987 | 988 | 989 | 561.066650 990 | 991 | 992 | 993 | 562.508545 994 | 995 | 996 | 997 | 998 | 999 | -------------------------------------------------------------------------------- /test_files/cerknicko-jezero-without-elevations.gpx: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 001 12 | 05-AUG-10 16:58:37 13 | 05-AUG-10 16:58:37 14 | Flag, Blue 15 | 16 | 17 | BACK T TH 18 | BACK TO THE ROOTS 19 | BACK TO THE ROOTS 20 | City (Small) 21 | 22 | 23 | BIRDS NEST 24 | BIRDS NEST 25 | BIRDS NEST 26 | City (Small) 27 | 28 | 29 | FAGGIO 30 | FAGGIO 31 | FAGGIO 32 | City (Small) 33 | 34 | 35 | RAKOV12 36 | RAKOV12 37 | RAKOV12 38 | City (Small) 39 | 40 | 41 | RAKV SKCJN 42 | RAKOV SKOCJAN 43 | RAKOV SKOCJAN 44 | City (Small) 45 | 46 | 47 | VANSHNG LK 48 | VANISHING LAKE 49 | VANISHING LAKE 50 | City (Small) 51 | 52 | 53 | ACTIVE LOG 54 | 55 | 56 | 57 | 58 | ACTIVE LOG #2 59 | 1 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | ACTIVE LOG #3 584 | 2 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | ACTIVE LOG #4 746 | 3 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | ACTIVE LOG #5 758 | 4 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | ACTIVE LOG #6 896 | 5 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | ACTIVE LOG #7 908 | 6 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | ACTIVE LOG #8 920 | 7 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | 955 | 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | 977 | 978 | 979 | 980 | 981 | 982 | 983 | 984 | 985 | 986 | 987 | --------------------------------------------------------------------------------