├── 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%s>' % (mod_saxutils.escape(content), tag)))
43 | else:
44 | result.append(make_str('>%s%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 | [](https://travis-ci.org/tkrajina/gpxpy)
2 | [](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('' + prefixedname + '>' + 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 + '' + self.tag + '>')
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 + '' + tag + '>')
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 |
--------------------------------------------------------------------------------