├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── docs ├── Makefile ├── classes │ ├── map │ │ ├── BaseMap.rst │ │ ├── InMemMap.rst │ │ └── SqliteMap.rst │ ├── matcher │ │ ├── BaseMatcher.rst │ │ ├── BaseMatching.rst │ │ ├── DistanceMatcher.rst │ │ └── SimpleMatcher.rst │ ├── overview.rst │ └── util │ │ └── Segment.rst ├── conf.py ├── index.rst ├── make.bat ├── requirements.txt └── usage │ ├── customdistributions.rst │ ├── debug.rst │ ├── incremental.rst │ ├── installation.rst │ ├── introduction.rst │ ├── latitudelongitude.rst │ ├── openstreetmap.rst │ └── visualisation.rst ├── leuvenmapmatching ├── __init__.py ├── map │ ├── __init__.py │ ├── base.py │ ├── inmem.py │ └── sqlite.py ├── matcher │ ├── __init__.py │ ├── base.py │ ├── distance.py │ ├── newsonkrumm.py │ └── simple.py ├── util │ ├── __init__.py │ ├── debug.py │ ├── dist_euclidean.py │ ├── dist_latlon.py │ ├── dist_latlon_nvector.py │ ├── evaluation.py │ ├── gpx.py │ ├── kalman.py │ ├── openstreetmap.py │ ├── projections.py │ └── segment.py └── visualization.py ├── setup.cfg ├── setup.py └── tests ├── examples ├── example_1_simple.py └── example_using_osmnx_and_geopandas.py ├── rsrc ├── bug2 │ └── readme.md ├── newson_krumm_2009 │ └── readme.md └── path_latlon │ ├── readme.md │ ├── route.gpx │ └── route2.gpx ├── test_bugs.py ├── test_conversion.py ├── test_examples.py ├── test_newsonkrumm2009.py ├── test_nonemitting.py ├── test_nonemitting_circle.py ├── test_parallelroads.py ├── test_path.py ├── test_path_latlon.py └── test_path_onlyedges.py /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .eggs 3 | .idea 4 | dist 5 | *.egg-info 6 | venv* 7 | README 8 | .ipynb_checkpoints 9 | *.zip 10 | *.gv 11 | *.gv.pdf 12 | *.xml 13 | tests/route.gpx 14 | examples/Leuven\ Stadswandeling* 15 | .git-old 16 | .pytest_cache 17 | docs/_build 18 | build 19 | cache 20 | baselines 21 | ./examples 22 | *.pkl 23 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Leuven.MapMatching 2 | ------------------ 3 | 4 | Copyright 2015-2018 KU Leuven, DTAI Research Group 5 | Copyright 2017-2018 Sirris 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | 19 | 20 | 21 | This package/repository contains code from the dtaimapmatching project as well 22 | as some open-source works: 23 | 24 | 25 | Latitude/longitude spherical geodesy tools 26 | ------------------------------------------ 27 | 28 | Latitude/longitude spherical geodesy tools 29 | (c) Chris Veness 2002-2017, MIT Licence 30 | www.movable-type.co.uk/scripts/latlong.html 31 | www.movable-type.co.uk/scripts/geodesy/docs/module-latlon-spherical.html 32 | 33 | 34 | Nvector 35 | ------- 36 | 37 | Gade, K. (2010). A Nonsingular Horizontal Position Representation, The Journal 38 | of Navigation, Volume 63, Issue 03, pp 395-417, July 2010. 39 | (www.navlab.net/Publications/A_Nonsingular_Horizontal_Position_Representation.pdf) 40 | 41 | This paper should be cited in publications using this library. 42 | 43 | Copyright (c) 2015, Norwegian Defence Research Establishment (FFI) 44 | All rights reserved. 45 | 46 | Redistribution and use in source and binary forms, with or without 47 | modification, are permitted provided that the following conditions are met: 48 | 49 | 1. Redistributions of source code must retain the above publication 50 | information, copyright notice, this list of conditions and the following 51 | disclaimer. 52 | 53 | 2. Redistributions in binary form must reproduce the above publication 54 | information, copyright notice, this list of conditions and the following 55 | disclaimer in the documentation and/or other materials provided with the 56 | distribution. 57 | 58 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 59 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 60 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 61 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 62 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 63 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 64 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 65 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 66 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 67 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 68 | THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: test3 3 | test3: 4 | @#export PYTHONPATH=.;./venv/bin/py.test --ignore=venv -vv 5 | python3 setup.py test 6 | 7 | .PHONY: test2 8 | test2: 9 | python2 setup.py test 10 | 11 | .PHONY: test 12 | test: test3 test2 13 | 14 | .PHONY: version 15 | version: 16 | @python3 setup.py --version 17 | 18 | .PHONY: prepare_dist 19 | prepare_dist: 20 | rm -rf dist/* 21 | python3 setup.py sdist bdist_wheel 22 | 23 | .PHONY: prepare_tag 24 | prepare_tag: 25 | @echo "Check whether repo is clean" 26 | git diff-index --quiet HEAD 27 | @echo "Check correct branch" 28 | if [[ "$$(git rev-parse --abbrev-ref HEAD)" != "master" ]]; then echo 'Not master branch'; exit 1; fi 29 | @echo "Add tag" 30 | git tag "v$$(python3 setup.py --version)" 31 | git push --tags 32 | 33 | .PHONY: deploy 34 | deploy: prepare_dist prepare_tag 35 | @echo "Check whether repo is clean" 36 | git diff-index --quiet HEAD 37 | @echo "Start uploading" 38 | twine upload --repository leuvenmapmatching dist/* 39 | 40 | .PHONY: docs 41 | docs: 42 | export PYTHONPATH=..; cd docs; make html 43 | 44 | .PHONY: docsclean 45 | docsclean: 46 | cd docs; make clean 47 | 48 | .PHONY: clean 49 | clean: docsclean 50 | 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Leuven.MapMatching 2 | 3 | [![PyPi Version](https://img.shields.io/pypi/v/leuvenmapmatching.svg)](https://pypi.org/project/leuvenmapmatching/) 4 | [![Documentation Status](https://readthedocs.org/projects/leuvenmapmatching/badge/?version=latest)](https://leuvenmapmatching.readthedocs.io/en/latest/?badge=latest) 5 | 6 | 7 | Align a trace of GPS measurements to a map or road segments. 8 | 9 | The matching is based on a Hidden Markov Model (HMM) with non-emitting 10 | states. The model can deal with missing data and you can plug in custom 11 | transition and emission probability distributions. 12 | 13 | ![example](http://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/example1.png?v=2) 14 | 15 | Main reference: 16 | 17 | > Meert Wannes, Mathias Verbeke, "HMM with Non-Emitting States for Map Matching", 18 | > European Conference on Data Analysis (ECDA), Paderborn, Germany, 2018. 19 | 20 | Other references: 21 | 22 | > Devos Laurens, Vandebril Raf (supervisor), Meert Wannes (supervisor), 23 | > "Traffic patterns revealed through matrix functions and map matching", 24 | > Master thesis, Faculty of Engineering Science, KU Leuven, 2018 25 | 26 | ## Installation and usage 27 | 28 | $ pip install leuvenmapmatching 29 | 30 | More information and examples: 31 | 32 | [leuvenmapmatching.readthedocs.io](https://leuvenmapmatching.readthedocs.io) 33 | 34 | ## Dependencies 35 | 36 | Required: 37 | 38 | - [numpy](http://www.numpy.org) 39 | - [scipy](https://www.scipy.org) 40 | 41 | 42 | Optional (only loaded when methods are called to rely on these packages): 43 | 44 | - [matplotlib](http://matplotlib.org): 45 | For visualisation 46 | - [smopy](https://github.com/rossant/smopy): 47 | For visualisation 48 | - [nvector](https://github.com/pbrod/Nvector): 49 | For latitude-longitude computations 50 | - [gpxpy](https://github.com/tkrajina/gpxpy): 51 | To import GPX files 52 | - [pykalman](https://pykalman.github.io): 53 | So smooth paths using a Kalman filter 54 | - [pyproj](https://jswhit.github.io/pyproj/): 55 | To project latitude-longitude coordinates to an XY-plane 56 | - [rtree](http://toblerity.org/rtree/): 57 | To quickly search locations 58 | 59 | 60 | ## Contact 61 | 62 | Wannes Meert, DTAI, KU Leuven 63 | wannes.meert@cs.kuleuven.be 64 | https://dtai.cs.kuleuven.be 65 | 66 | Mathias Verbeke, Sirris 67 | mathias.verbeke@sirris.be 68 | http://www.sirris.be/expertise/data-innovation 69 | 70 | Developed with the support of [Elucidata.be](http://www.elucidata.be). 71 | 72 | 73 | ## License 74 | 75 | Copyright 2015-2022, KU Leuven - DTAI Research Group, Sirris - Elucidata Group 76 | Apache License, Version 2.0. 77 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = DTAIMap-Matching 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/classes/map/BaseMap.rst: -------------------------------------------------------------------------------- 1 | BaseMap 2 | ======= 3 | 4 | 5 | .. autoclass:: leuvenmapmatching.map.base.BaseMap 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/classes/map/InMemMap.rst: -------------------------------------------------------------------------------- 1 | InMemMap 2 | ======== 3 | 4 | 5 | .. autoclass:: leuvenmapmatching.map.inmem.InMemMap 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/classes/map/SqliteMap.rst: -------------------------------------------------------------------------------- 1 | SqliteMap 2 | ========= 3 | 4 | 5 | .. autoclass:: leuvenmapmatching.map.sqlite.SqliteMap 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/classes/matcher/BaseMatcher.rst: -------------------------------------------------------------------------------- 1 | BaseMatcher 2 | =========== 3 | 4 | This a generic base class to be used by matchers. This class itself 5 | does not implement a working matcher. Use a matcher such as 6 | ``SimpleMatcher``, ``DistanceMatcher``, ... 7 | 8 | .. autoclass:: leuvenmapmatching.matcher.base.BaseMatcher 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/classes/matcher/BaseMatching.rst: -------------------------------------------------------------------------------- 1 | BaseMatching 2 | ============ 3 | 4 | 5 | .. autoclass:: leuvenmapmatching.matcher.base.BaseMatching 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/classes/matcher/DistanceMatcher.rst: -------------------------------------------------------------------------------- 1 | DistanceMatcher 2 | =============== 3 | 4 | 5 | .. autoclass:: leuvenmapmatching.matcher.distance.DistanceMatcher 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/classes/matcher/SimpleMatcher.rst: -------------------------------------------------------------------------------- 1 | SimpleMatcher 2 | ============= 3 | 4 | 5 | .. autoclass:: leuvenmapmatching.matcher.simple.SimpleMatcher 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/classes/overview.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | matcher 4 | ~~~~~~~ 5 | 6 | .. toctree:: 7 | :caption: Matcher 8 | 9 | matcher/BaseMatcher 10 | matcher/SimpleMatcher 11 | matcher/DistanceMatcher 12 | 13 | .. toctree:: 14 | :caption: Matching 15 | 16 | matcher/BaseMatching 17 | 18 | 19 | 20 | map 21 | ~~~ 22 | 23 | .. toctree:: 24 | :caption: Map 25 | 26 | map/BaseMap 27 | map/InMemMap 28 | map/SqliteMap 29 | 30 | 31 | 32 | util 33 | ~~~~ 34 | 35 | .. toctree:: 36 | :caption: Util 37 | 38 | util/Segment 39 | -------------------------------------------------------------------------------- /docs/classes/util/Segment.rst: -------------------------------------------------------------------------------- 1 | Segment 2 | ======= 3 | 4 | 5 | .. autoclass:: leuvenmapmatching.util.segment.Segment 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Leuven.MapMatching documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Apr 14 23:24:31 2018. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ['sphinx.ext.autodoc', 35 | 'sphinx.ext.mathjax', 36 | 'sphinx.ext.viewcode'] 37 | 38 | autoclass_content = 'both' 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix(es) of source filenames. 44 | # You can specify multiple suffix as a list of string: 45 | # 46 | # source_suffix = ['.rst', '.md'] 47 | source_suffix = '.rst' 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = 'Leuven.MapMatching' 54 | copyright = '2018-2022, Wannes Meert' 55 | author = 'Wannes Meert' 56 | 57 | # The version info for the project you're documenting, acts as replacement for 58 | # |version| and |release|, also used in various other places throughout the 59 | # built documents. 60 | # 61 | # The short X.Y version. 62 | version = '1.1.1' 63 | # The full version, including alpha/beta/rc tags. 64 | release = '1.1.1' 65 | 66 | # The language for content autogenerated by Sphinx. Refer to documentation 67 | # for a list of supported languages. 68 | # 69 | # This is also used if you do content translation via gettext catalogs. 70 | # Usually you set "language" from the command line for these cases. 71 | language = 'en' 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | # This patterns also effect to html_static_path and html_extra_path 76 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 77 | 78 | # The name of the Pygments (syntax highlighting) style to use. 79 | pygments_style = 'sphinx' 80 | 81 | # If true, `todo` and `todoList` produce output, else they produce nothing. 82 | todo_include_todos = False 83 | 84 | 85 | # -- Options for HTML output ---------------------------------------------- 86 | 87 | # The theme to use for HTML and HTML Help pages. See the documentation for 88 | # a list of builtin themes. 89 | # 90 | # html_theme = 'alabaster' 91 | html_theme = "sphinx_rtd_theme" 92 | 93 | # Theme options are theme-specific and customize the look and feel of a theme 94 | # further. For a list of options available for each theme, see the 95 | # documentation. 96 | # 97 | # html_theme_options = {} 98 | 99 | # Add any paths that contain custom static files (such as style sheets) here, 100 | # relative to this directory. They are copied after the builtin static files, 101 | # so a file named "default.css" will overwrite the builtin "default.css". 102 | html_static_path = ['_static'] 103 | 104 | 105 | # -- Options for HTMLHelp output ------------------------------------------ 106 | 107 | # Output file base name for HTML help builder. 108 | htmlhelp_basename = 'LeuvenMapMatchingDoc' 109 | 110 | 111 | # -- Options for LaTeX output --------------------------------------------- 112 | 113 | latex_elements = { 114 | # The paper size ('letterpaper' or 'a4paper'). 115 | # 116 | # 'papersize': 'letterpaper', 117 | 118 | # The font size ('10pt', '11pt' or '12pt'). 119 | # 120 | # 'pointsize': '10pt', 121 | 122 | # Additional stuff for the LaTeX preamble. 123 | # 124 | # 'preamble': '', 125 | 126 | # Latex figure (float) alignment 127 | # 128 | # 'figure_align': 'htbp', 129 | } 130 | 131 | # Grouping the document tree into LaTeX files. List of tuples 132 | # (source start file, target name, title, 133 | # author, documentclass [howto, manual, or own class]). 134 | latex_documents = [ 135 | (master_doc, 'LeuvenMapMatching.tex', 'Leuven.MapMatching Documentation', 136 | 'Wannes Meert', 'manual'), 137 | ] 138 | 139 | 140 | # -- Options for manual page output --------------------------------------- 141 | 142 | # One entry per manual page. List of tuples 143 | # (source start file, name, description, authors, manual section). 144 | man_pages = [ 145 | (master_doc, 'leuvenmapmatching', 'Leuven.MapMatching Documentation', 146 | [author], 1) 147 | ] 148 | 149 | 150 | # -- Options for Texinfo output ------------------------------------------- 151 | 152 | # Grouping the document tree into Texinfo files. List of tuples 153 | # (source start file, target name, title, author, 154 | # dir menu entry, description, category) 155 | texinfo_documents = [ 156 | (master_doc, 'LeuvenMapMatching', 'Leuven.MapMatching Documentation', 157 | author, 'LeuvenMapMatching', 'Map Matching', 158 | 'Miscellaneous'), 159 | ] 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Leuven.MapMatching documentation master file, created by 2 | sphinx-quickstart on Sat Apr 14 23:24:31 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Leuven.MapMatching's documentation 7 | ================================== 8 | 9 | Align a trace of coordinates (e.g. GPS measurements) to a map of road segments. 10 | 11 | The matching is based on a Hidden Markov Model (HMM) with non-emitting 12 | states. The model can deal with missing data and you can plug in custom 13 | transition and emission probability distributions. 14 | 15 | Reference: 16 | 17 | Meert Wannes, Mathias Verbeke, "HMM with Non-Emitting States for Map Matching", 18 | European Conference on Data Analysis (ECDA), Paderborn, Germany, 2018. 19 | 20 | 21 | .. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/example1.png?v=2 22 | :alt: example 23 | 24 | 25 | .. toctree:: 26 | :maxdepth: 2 27 | :caption: Contents: 28 | 29 | 30 | .. toctree:: 31 | :caption: Usage 32 | 33 | 34 | usage/installation 35 | usage/introduction 36 | usage/openstreetmap 37 | usage/visualisation 38 | usage/latitudelongitude 39 | usage/customdistributions 40 | usage/incremental 41 | usage/debug 42 | 43 | 44 | .. toctree:: 45 | :maxdepth: 2 46 | :caption: Classes 47 | 48 | classes/overview 49 | 50 | 51 | Indices and tables 52 | ================== 53 | 54 | * :ref:`genindex` 55 | * :ref:`modindex` 56 | * :ref:`search` 57 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=DTAIMap-Matching 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | sphinx_rtd_theme -------------------------------------------------------------------------------- /docs/usage/customdistributions.rst: -------------------------------------------------------------------------------- 1 | Custom probability distributions 2 | ================================ 3 | 4 | You can use your own custom probability distributions for the transition and emission probabilities. 5 | This is achieved by inheriting from the :class:`BaseMatcher` class. 6 | 7 | Examples are available in the :class:`SimpleMatching` class and :class:`DistanceMatching` class. 8 | The latter implements a variation based on Newson and Krumm (2009). 9 | 10 | Transition probability distribution 11 | ----------------------------------- 12 | 13 | Overwrite the :meth:`logprob_trans` method. 14 | 15 | For example, if you want to use a uniform distribution over the possible road segments: 16 | 17 | .. code-block:: python 18 | 19 | def logprob_trans(self, prev_m, edge_m, edge_o, is_prev_ne, is_next_ne): 20 | return -math.log(len(self.matcher.map.nodes_nbrto(self.edge_m.last_point()))) 21 | 22 | Note that ``prev_m.edge_m`` and ``edge_m`` are not necessarily connected. For example if the ``Map`` object 23 | returns a neighbor state that is not connected in the roadmap. This functionality is used to allow switching lanes. 24 | 25 | 26 | Emission probability distribution 27 | --------------------------------- 28 | 29 | Overwrite the :meth:`logprob_obs` method for non-emitting nodes. 30 | These methods are given the closest distance as `dist`, the previous :class:`Matching` object 31 | in the lattice, the state as `edge_m`, and the observation as `edge_o`. The latter two are :class:`Segment` objects 32 | that can represent either a segment or a point. 33 | Each segment also has a project point which is the point on the segment that is the closest point. 34 | 35 | For example, a simple step function with more tolerance for non-emitting nodes: 36 | 37 | .. code-block:: python 38 | 39 | def logprob_obs(self, dist, prev_m, new_edge_m, new_edge_o, is_ne): 40 | if is_ne: 41 | if dist < 50: 42 | return -math.log(50) 43 | else: 44 | if dist < 10: 45 | return -math.log(10) 46 | return -np.inf 47 | 48 | Note that an emission probability can be given for a non-emitting node. This allows you to rank non-emitting nodes 49 | even when no observations are available. It will then insert pseudo-observations on the line between the previous 50 | and next observations. 51 | To have a pure non-emitting node, the `logprob_obs` method should always return 0 if the 52 | ``is_ne`` argument is true. 53 | 54 | 55 | Custom lattice objects 56 | ---------------------- 57 | 58 | If you need to store additional information in the lattice, inherit from the :class:`Matching` class and 59 | pass your custom object to the :class:`Matcher` object. 60 | 61 | .. code-block:: python 62 | 63 | from leuvenmapmatching.map.base import BaseMatching 64 | 65 | class MyMatching(BaseMatching): 66 | ... 67 | 68 | matcher = MyMatcher(mapdb, matching=MyMatching) 69 | 70 | -------------------------------------------------------------------------------- /docs/usage/debug.rst: -------------------------------------------------------------------------------- 1 | Debug 2 | ===== 3 | 4 | Increasing the verbosity level 5 | ------------------------------ 6 | 7 | To inspect the intermediate steps that the algorithm take, you can increase 8 | the verbosity level of the package. For example: 9 | 10 | .. code-block:: python 11 | 12 | import sys 13 | import logging 14 | import leuvenmapmatching 15 | logger = leuvenmapmatching.logger 16 | 17 | logger.setLevel(logging.DEBUG) 18 | logger.addHandler(logging.StreamHandler(sys.stdout)) 19 | 20 | 21 | Inspect the best matching 22 | ------------------------- 23 | 24 | The best match is available in ``matcher.lattice_best``. This is a list of 25 | ``Matching`` objects. For example after running the first example in the introduction: 26 | 27 | .. code-block:: python 28 | 29 | >>> matcher.lattice_best 30 | [Matching, 31 | Matching, 32 | Matching, 33 | ... 34 | 35 | A matching object summarizes its information as a tuple with three values if 36 | the best match is with a vertex: . And a tuple 37 | with four values if the best match is with an edge: . 38 | 39 | In the example above, the first observation (with index 0) is matched to a point on the edge 40 | A-B. If you want to inspect the exact locations, you can query the ``Segment`` 41 | objects that express the observation and map: ``matching.edge_o`` and ``matching.edge_m``. 42 | 43 | .. code-block:: python 44 | 45 | >>> match = matcher.lattice_best[0] 46 | >>> match.edge_m.l1, match.edge_m.l2 # Edge start/end labels 47 | ('A', 'B') 48 | >>> match.edge_m.pi # Best point on A-B edge 49 | (1.0, 1.0) 50 | >>> match.edge_m.p1, match.edge_m.p2 # Locations of A and B 51 | ((1, 1), (1, 3)) 52 | >>> match.edge_o.l1, match.edge_o.l2 # Observation 53 | ('O0', None) 54 | >>> match.edge_o.pi # Location of observation O0, because no second location 55 | (0.8, 0.7) 56 | >>> match.edge_o.p1 # Same as pi because no interpolation 57 | (0.8, 0.7) 58 | 59 | Inspect the matching lattice 60 | ---------------------------- 61 | 62 | All paths through the lattice are available in ``matcher.lattice``. 63 | The lattice is a dictionary with a ``LatticeColumn`` object for each observation 64 | (in case the full path of observations is matched). 65 | 66 | For each observation, you can inspect the ``Matching`` objects with: 67 | 68 | .. code-block:: python 69 | 70 | >>> matcher.lattice 71 | {0: , 72 | 1: , 73 | 2: , 74 | ... 75 | >>> matcher.lattice[0].values_all() 76 | {Matching, 77 | Matching, 78 | Matching, 79 | ... 80 | 81 | To start backtracking you can, for example, see which matching object 82 | for the last element has the highest probability (thus the best match): 83 | 84 | .. code-block:: python 85 | 86 | >>> m = max(matcher.lattice[len(path)-1].values_all(), key=lambda m: m.logprob) 87 | >>> m.logprob 88 | -0.6835815469734807 89 | 90 | The previous matching objects can be queried with. These are only those 91 | matches that are connected to this matchin the lattice (in this case 92 | nodes in the street graph with an edge to the current node): 93 | 94 | .. code-block:: python 95 | 96 | >>> m.prev # Best previous match with a connection (multiple if equal probability) 97 | {Matching} 98 | >>> m.prev_other # All previous matches in the lattice with a connection 99 | {Matching, 100 | Matching, 101 | Matching, 102 | Matching} 103 | -------------------------------------------------------------------------------- /docs/usage/incremental.rst: -------------------------------------------------------------------------------- 1 | Incremental matching 2 | ==================== 3 | 4 | Example: Incremental matching 5 | ------------------------------- 6 | 7 | If the observations are collected in a streaming setting. The matching can also be invoked incrementally. 8 | The lattice will be built further every time a new subsequence of the path is given. 9 | 10 | .. code-block:: python 11 | 12 | from leuvenmapmatching.matcher.distance import DistanceMatcher 13 | from leuvenmapmatching.map.inmemmap import InMemMap 14 | 15 | map_con = InMemMap("mymap", graph={ 16 | "A": ((1, 1), ["B", "C", "X"]), 17 | "B": ((1, 3), ["A", "C", "D", "K"]), 18 | "C": ((2, 2), ["A", "B", "D", "E", "X", "Y"]), 19 | "D": ((2, 4), ["B", "C", "D", "E", "K", "L"]), 20 | "E": ((3, 3), ["C", "D", "F", "Y"]), 21 | "F": ((3, 5), ["D", "E", "L"]), 22 | "X": ((2, 0), ["A", "C", "Y"]), 23 | "Y": ((3, 1), ["X", "C", "E"]), 24 | "K": ((1, 5), ["B", "D", "L"]), 25 | "L": ((2, 6), ["K", "D", "F"]) 26 | }, use_latlon=False) 27 | 28 | path = [(0.8, 0.7), (0.9, 0.7), (1.1, 1.0), (1.2, 1.5), (1.2, 1.6), (1.1, 2.0), 29 | (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7), 30 | (2.3, 3.5), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2), 31 | (3.1, 3.8), (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)] 32 | 33 | matcher = DistanceMatcher(map_con, max_dist=2, obs_noise=1, min_prob_norm=0.5) 34 | states, _ = matcher.match(path[:5]) 35 | states, _ = matcher.match(path, expand=True) 36 | nodes = matcher.path_pred_onlynodes 37 | 38 | print("States\n------") 39 | print(states) 40 | print("Nodes\n------") 41 | print(nodes) 42 | print("") 43 | matcher.print_lattice_stats() 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/usage/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Dependencies 5 | ------------ 6 | 7 | Required: 8 | 9 | - `numpy `__ 10 | - `scipy `__ 11 | 12 | Optional (only loaded when methods are called that rely on these packages): 13 | 14 | - `rtree `__ 15 | - `nvector `__ 16 | - `gpxpy `__ 17 | - `pyproj `__ 18 | - `pykalman `__ 19 | - `matplotlib `__ 20 | - `smopy `__ 21 | 22 | 23 | Using pip 24 | --------- 25 | 26 | If you want to install the latest released version using pip: 27 | 28 | :: 29 | 30 | $ pip install leuvenmapmatching 31 | 32 | If you want to install the latest non-released version (add ``@develop``) for the 33 | latest development version: 34 | 35 | :: 36 | 37 | $ pip install git+https://github.com/wannesm/leuvenmapmatching 38 | 39 | 40 | From source 41 | ----------- 42 | 43 | The library can also be compiled and/or installed directly from source. 44 | 45 | * Download the source from https://github.com/wannesm/leuvenmapmatching 46 | * To compile and install in your site-package directory: ``python3 setup.py install`` 47 | 48 | -------------------------------------------------------------------------------- /docs/usage/introduction.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | Example 1: Simple 5 | ----------------- 6 | 7 | A first, simple example. Some parameters are given to tune the algorithm. 8 | The ``max_dist`` and ``obs_noise`` are distances that indicate the maximal distance between observation and road 9 | segment and the expected noise in the measurements, respectively. 10 | The ``min_prob_norm`` prunes the lattice in that it drops paths that drop below 0.5 normalized probability. 11 | The probability is normalized to allow for easier reasoning about the probability of a path. 12 | It is computed as the exponential smoothed log probability components instead of the sum as would be the case 13 | for log likelihood. 14 | Because the number of possible paths quickly grows, it is recommended to set the 15 | ``max_lattice_width`` argument to speed up the algorithm (available from version 1.0 onwards). 16 | It will only continue the search with this number of possible paths at every step. If no solution is found, 17 | this value can be incremented using the ``increase_max_lattice_width`` method. 18 | 19 | .. code-block:: python 20 | 21 | from leuvenmapmatching.matcher.distance import DistanceMatcher 22 | from leuvenmapmatching.map.inmem import InMemMap 23 | 24 | map_con = InMemMap("mymap", graph={ 25 | "A": ((1, 1), ["B", "C", "X"]), 26 | "B": ((1, 3), ["A", "C", "D", "K"]), 27 | "C": ((2, 2), ["A", "B", "D", "E", "X", "Y"]), 28 | "D": ((2, 4), ["B", "C", "F", "E", "K", "L"]), 29 | "E": ((3, 3), ["C", "D", "F", "Y"]), 30 | "F": ((3, 5), ["D", "E", "L"]), 31 | "X": ((2, 0), ["A", "C", "Y"]), 32 | "Y": ((3, 1), ["X", "C", "E"]), 33 | "K": ((1, 5), ["B", "D", "L"]), 34 | "L": ((2, 6), ["K", "D", "F"]) 35 | }, use_latlon=False) 36 | 37 | path = [(0.8, 0.7), (0.9, 0.7), (1.1, 1.0), (1.2, 1.5), (1.2, 1.6), (1.1, 2.0), 38 | (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7), 39 | (2.3, 3.5), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2), 40 | (3.1, 3.8), (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)] 41 | 42 | matcher = DistanceMatcher(map_con, max_dist=2, obs_noise=1, min_prob_norm=0.5, max_lattice_width=5) 43 | states, _ = matcher.match(path) 44 | nodes = matcher.path_pred_onlynodes 45 | 46 | print("States\n------") 47 | print(states) 48 | print("Nodes\n------") 49 | print(nodes) 50 | print("") 51 | matcher.print_lattice_stats() 52 | 53 | 54 | Example 2: Non-emitting states 55 | ------------------------------ 56 | 57 | In case there are less observations that states (an assumption of HMMs), non-emittings states allow you 58 | to deal with this. States will be inserted that are not associated with any of the given observations if 59 | this improves the probability of the path. 60 | 61 | It is possible to also associate a distribtion over the distance between observations and the non-emitting 62 | states (`obs_noise_ne`). This allows the algorithm to prefer nearby road segments. This value should be 63 | larger than `obs_noise` as it is mapped to the line between the previous and next observation, which does 64 | not necessarily run over the relevant segment. Setting this to infinity is the same as using pure 65 | non-emitting states that ignore observations completely. 66 | 67 | .. code-block:: python 68 | 69 | from leuvenmapmatching.matcher.distance import DistanceMatcher 70 | from leuvenmapmatching.map.inmem import InMemMap 71 | from leuvenmapmatching import visualization as mmviz 72 | 73 | path = [(1, 0), (7.5, 0.65), (10.1, 1.9)] 74 | mapdb = InMemMap("mymap", graph={ 75 | "A": ((1, 0.00), ["B"]), 76 | "B": ((3, 0.00), ["A", "C"]), 77 | "C": ((4, 0.70), ["B", "D"]), 78 | "D": ((5, 1.00), ["C", "E"]), 79 | "E": ((6, 1.00), ["D", "F"]), 80 | "F": ((7, 0.70), ["E", "G"]), 81 | "G": ((8, 0.00), ["F", "H"]), 82 | "H": ((10, 0.0), ["G", "I"]), 83 | "I": ((10, 2.0), ["H"]) 84 | }, use_latlon=False) 85 | matcher = DistanceMatcher(mapdb, max_dist_init=0.2, obs_noise=1, obs_noise_ne=10, 86 | non_emitting_states=True, only_edges=True , max_lattice_width=5) 87 | states, _ = matcher.match(path) 88 | nodes = matcher.path_pred_onlynodes 89 | 90 | print("States\n------") 91 | print(states) 92 | print("Nodes\n------") 93 | print(nodes) 94 | print("") 95 | matcher.print_lattice_stats() 96 | 97 | mmviz.plot_map(mapdb, matcher=matcher, 98 | show_labels=True, show_matching=True 99 | filename="output.png")) 100 | -------------------------------------------------------------------------------- /docs/usage/latitudelongitude.rst: -------------------------------------------------------------------------------- 1 | Dealing with Latitude-Longitude 2 | =============================== 3 | 4 | The toolbox can deal with latitude-longitude coordinates directly. 5 | Map matching, however, requires a lot of repeated computations between points and latitude-longitude 6 | computations will be more expensive than Euclidean distances. 7 | 8 | There are three different options how you can handle latitude-longitude coordinates: 9 | 10 | Option 1: Use Latitude-Longitude directly 11 | ----------------------------------------- 12 | 13 | Set the ``use_latlon`` flag in the :class:`Map` to true. 14 | 15 | For example to read in an OpenStreetMap file directly to a :class:`InMemMap` object: 16 | 17 | .. code-block:: python 18 | 19 | from leuvenmapmatching.map.inmem import InMemMap 20 | 21 | map_con = InMemMap("myosm", use_latlon=True) 22 | 23 | for entity in osmread.parse_file(osm_fn): 24 | if isinstance(entity, osmread.Way) and 'highway' in entity.tags: 25 | for node_a, node_b in zip(entity.nodes, entity.nodes[1:]): 26 | map_con.add_edge(node_a, node_b) 27 | map_con.add_edge(node_b, node_a) 28 | if isinstance(entity, osmread.Node): 29 | map_con.add_node(entity.id, (entity.lat, entity.lon)) 30 | map_con.purge() 31 | 32 | 33 | Option 2: Project Latitude-Longitude to X-Y 34 | ------------------------------------------- 35 | 36 | Latitude-Longitude coordinates can be transformed two a frame with two orthogonal axis. 37 | 38 | .. code-block:: python 39 | 40 | from leuvenmapmatching.map.inmem import InMemMap 41 | 42 | map_con_latlon = InMemMap("myosm", use_latlon=True) 43 | # Add edges/nodes 44 | map_con_xy = map_con_latlon.to_xy() 45 | 46 | route_latlon = [] 47 | # Add GPS locations 48 | route_xy = [map_con_xy.latlon2yx(latlon) for latlon in route_latlon] 49 | 50 | 51 | This can also be done directly using the `pyproj `_ toolbox. 52 | For example, using the Lambert Conformal projection to project the route GPS coordinates: 53 | 54 | .. code-block:: python 55 | 56 | import pyproj 57 | 58 | route = [(4.67878,50.864),(4.68054,50.86381),(4.68098,50.86332),(4.68129,50.86303),(4.6817,50.86284), 59 | (4.68277,50.86371),(4.68894,50.86895),(4.69344,50.86987),(4.69354,50.86992),(4.69427,50.87157), 60 | (4.69643,50.87315),(4.69768,50.87552),(4.6997,50.87828)] 61 | lon_0, lat_0 = route[0] 62 | proj = pyproj.Proj(f"+proj=merc +ellps=GRS80 +units=m +lon_0={lon_0} +lat_0={lat_0} +lat_ts={lat_0} +no_defs") 63 | xs, ys = [], [] 64 | for lon, lat in route: 65 | x, y = proj(lon, lat) 66 | xs.append(x) 67 | ys.append(y) 68 | 69 | 70 | Notice that the pyproj package uses the convention to express coordinates as x-y which is 71 | longitude-latitude because it is defined this way in the CRS definitions while the Leuven.MapMatching 72 | toolbox follows the ISO 6709 standard and expresses coordinates as latitude-longitude. If you 73 | want ``pyproj`` to use latitude-longitude you can use set the 74 | `axisswap option `_. 75 | 76 | If you want to define both the from and to projections: 77 | 78 | .. code-block:: python 79 | 80 | import pyproj 81 | 82 | route = [(4.67878,50.864),(4.68054,50.86381),(4.68098,50.86332),(4.68129,50.86303),(4.6817,50.86284), 83 | (4.68277,50.86371),(4.68894,50.86895),(4.69344,50.86987),(4.69354,50.86992),(4.69427,50.87157), 84 | (4.69643,50.87315),(4.69768,50.87552),(4.6997,50.87828)] 85 | p1 = pyproj.Proj(proj='latlon', datum='WGS84') 86 | p2 = pyproj.Proj(proj='utm', datum='WGS84') 87 | xs, ys = [], [] 88 | for lon, lat in route: 89 | x, y = pyproj.transform(lon, lat) 90 | xs.append(x) 91 | ys.append(y) 92 | 93 | 94 | Option 3: Use Latitude-Longitude as if they are X-Y points 95 | ---------------------------------------------------------- 96 | 97 | A naive solution would be to use latitude-longitude coordinate pairs as if they are X-Y coordinates. 98 | For small distances, far away from the poles and not crossing the dateline, this option might work. 99 | But it is not adviced. 100 | 101 | For example, for long distances the error is quite large. In the image beneath, the blue line is the computation 102 | of the intersection using latitude-longitude while the red line is the intersection using Eucludean distances. 103 | 104 | .. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/latlon_mismatch_1.png?v=1 105 | :alt: Latitude-Longitude mismatch 106 | 107 | .. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/latlon_mismatch_2.png?v=1 108 | :alt: Latitude-Longitude mismatch detail 109 | -------------------------------------------------------------------------------- /docs/usage/openstreetmap.rst: -------------------------------------------------------------------------------- 1 | Map from OpenStreetMap 2 | ====================== 3 | 4 | You can download a graph for map-matching from the OpenStreetMap.org service. 5 | Multiple methods exists, we illustrate two. 6 | 7 | Using requests, osmread and gpx 8 | ------------------------------- 9 | 10 | You can perform map matching on a OpenStreetMap database by combing ``leuvenmapmatching`` 11 | with the packages ``requests``, ``osmread`` and ``gpx``. 12 | 13 | Download a map as XML 14 | ~~~~~~~~~~~~~~~~~~~~~ 15 | 16 | You can use the overpass-api.de service: 17 | 18 | .. code-block:: python 19 | 20 | from pathlib import Path 21 | import requests 22 | xml_file = Path(".") / "osm.xml" 23 | url = 'http://overpass-api.de/api/map?bbox=4.694933,50.870047,4.709256000000001,50.879628' 24 | r = requests.get(url, stream=True) 25 | with xml_file.open('wb') as ofile: 26 | for chunk in r.iter_content(chunk_size=1024): 27 | if chunk: 28 | ofile.write(chunk) 29 | 30 | 31 | Create graph using osmread 32 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 33 | 34 | Once we have a file containing the region we are interested in, we can select the roads we want to use 35 | to create a graph from. In this case we focus on 'ways' with a 'highway' tag. Those represent a variety 36 | of roads. For a more detailed filtering look at the 37 | `possible values of the highway tag `_. 38 | 39 | .. code-block:: python 40 | 41 | from leuvenmapmatching.map.inmem import InMemMap 42 | import osmread 43 | 44 | map_con = InMemMap("myosm", use_latlon=True, use_rtree=True, index_edges=True) 45 | for entity in osmread.parse_file(str(xml_file)): 46 | if isinstance(entity, osmread.Way) and 'highway' in entity.tags: 47 | for node_a, node_b in zip(entity.nodes, entity.nodes[1:]): 48 | map_con.add_edge(node_a, node_b) 49 | # Some roads are one-way. We'll add both directions. 50 | map_con.add_edge(node_b, node_a) 51 | if isinstance(entity, osmread.Node): 52 | map_con.add_node(entity.id, (entity.lat, entity.lon)) 53 | map_con.purge() 54 | 55 | 56 | Note that ``InMemMap`` is a simple container for a map. It is recommended to use 57 | your own optimized connecter to your map dataset. 58 | 59 | If you want to allow transitions that are not following the exact road segments you can inherit from the ``Map`` 60 | class and define a new class with your own transitions. 61 | The transitions are defined using the ``nodes_nbrto`` and ``edges_nbrt`` methods. 62 | 63 | 64 | Perform map matching on an OpenStreetMap database 65 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 66 | 67 | You can create a list of latitude-longitude coordinates manually. Or read a gpx file. 68 | 69 | .. code-block:: python 70 | 71 | from leuvenmapmatching.util.gpx import gpx_to_path 72 | 73 | track = gpx_to_path("mytrack.gpx") 74 | matcher = DistanceMatcher(map_con, 75 | max_dist=100, max_dist_init=25, # meter 76 | min_prob_norm=0.001, 77 | non_emitting_length_factor=0.75, 78 | obs_noise=50, obs_noise_ne=75, # meter 79 | dist_noise=50, # meter 80 | non_emitting_states=True, 81 | max_lattice_width=5) 82 | states, lastidx = matcher.match(track) 83 | 84 | 85 | Using osmnx and geopandas 86 | ------------------------- 87 | 88 | Another great library to interact with OpenStreetMap data is the `osmnx `_ package. 89 | The osmnx package can retrieve relevant data automatically, for example when given a name of a region. 90 | This package is build on top of the `geopandas `_ package. 91 | 92 | .. code-block:: python 93 | 94 | import osmnx 95 | graph = ox.graph_from_place('Leuven, Belgium', network_type='drive', simplify=False) 96 | graph_proj = ox.project_graph(graph) 97 | 98 | # Create GeoDataFrames (gdfs) 99 | # Approach 1 100 | nodes_proj, edges_proj = ox.graph_to_gdfs(graph_proj, nodes=True, edges=True) 101 | for nid, row in nodes_proj[['x', 'y']].iterrows(): 102 | map_con.add_node(nid, (row['x'], row['y'])) 103 | for eid, _ in edges_proj.iterrows(): 104 | map_con.add_edge(eid[0], eid[1]) 105 | 106 | # Approach 2 107 | nodes, edges = ox.graph_to_gdfs(graph_proj, nodes=True, edges=True) 108 | nodes_proj = nodes.to_crs("EPSG:3395") 109 | edges_proj = edges.to_crs("EPSG:3395") 110 | for nid, row in nodes_proj.iterrows(): 111 | map_con.add_node(nid, (row['lat'], row['lon'])) 112 | # We can also extract edges also directly from networkx graph 113 | for nid1, nid2, _ in graph.edges: 114 | map_con.add_edge(nid1, nid2) 115 | 116 | 117 | -------------------------------------------------------------------------------- /docs/usage/visualisation.rst: -------------------------------------------------------------------------------- 1 | Visualisation 2 | ============= 3 | 4 | To inspect the results, a plotting function is included. 5 | 6 | Simple plotting 7 | --------------- 8 | 9 | To plot the graph in a matplotlib figure use: 10 | 11 | .. code-block:: python 12 | 13 | from leuvenmapmatching import visualization as mmviz 14 | mmviz.plot_map(map_con, matcher=matcher, 15 | show_labels=True, show_matching=True, show_graph=True, 16 | filename="my_plot.png") 17 | 18 | This will result in the following figure: 19 | 20 | .. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/plot1.png?v=1 21 | :alt: Plot1 22 | 23 | You can also define your own figure by passing a matplotlib axis object: 24 | 25 | .. code-block:: python 26 | 27 | fig, ax = plt.subplots(1, 1) 28 | mmviz.plot_map(map_con, matcher=matcher, 29 | ax=ax, 30 | show_labels=True, show_matching=True, show_graph=True, 31 | filename="my_plot.png") 32 | 33 | 34 | Plotting with an OpenStreetMap background 35 | ----------------------------------------- 36 | 37 | The plotting function also supports a link with the ``smopy`` package. 38 | Set the ``use_osm`` argument to true and pass a map that is defined with 39 | latitude-longitude (thus ``use_latlon=True``). 40 | 41 | You can set ``zoom_path`` to true to only see the relevant part and not the 42 | entire map that is available in the map. Alternatively you can also set the 43 | bounding box manually using the ``bb`` argument. 44 | 45 | .. code-block:: python 46 | 47 | mm_viz.plot_map(map_con, matcher=matcher, 48 | use_osm=True, zoom_path=True, 49 | show_labels=False, show_matching=True, show_graph=False, 50 | filename="my_osm_plot.png") 51 | 52 | 53 | This will result in the following figure: 54 | 55 | .. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/plot2.png?v=1 56 | :alt: Plot2 57 | 58 | Or when some GPS points are missing in the track, the matching is more 59 | visible as the matched route deviates from the straight line between two 60 | GPS points: 61 | 62 | .. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/plot3.png?v=1 63 | :alt: Plot3 64 | -------------------------------------------------------------------------------- /leuvenmapmatching/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching 4 | ~~~~~~~~~~~~~~~~~ 5 | 6 | :author: Wannes Meert 7 | :copyright: Copyright 2015-2022 DTAI, KU Leuven and Sirris. 8 | :license: Apache License, Version 2.0, see LICENSE for details. 9 | """ 10 | import logging 11 | from . import map, matcher, util 12 | # visualization is not loaded by default (avoid loading unnecessary dependencies such as matplotlib). 13 | 14 | logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching") 15 | 16 | __version__ = '1.1.4' 17 | 18 | -------------------------------------------------------------------------------- /leuvenmapmatching/map/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.map 4 | ~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: Wannes Meert 7 | :copyright: Copyright 2018 DTAI, KU Leuven and Sirris. 8 | :license: Apache License, Version 2.0, see LICENSE for details. 9 | """ 10 | -------------------------------------------------------------------------------- /leuvenmapmatching/map/base.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.map.base 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Base Map class. 7 | 8 | To be used in a Matcher object, the following functions need to be defined: 9 | 10 | - ``edges_closeto`` 11 | - ``nodes_closeto`` 12 | - ``nodes_nbrto`` 13 | - ``edges_nbrto`` 14 | 15 | For visualiation purposes the following methods need to be implemented: 16 | 17 | - ``bb`` 18 | - ``labels`` 19 | - ``size`` 20 | - ``coordinates`` 21 | - ``node_coordinates`` 22 | - ``all_edges`` 23 | - ``all_nodes`` 24 | 25 | :author: Wannes Meert 26 | :copyright: Copyright 2018 DTAI, KU Leuven and Sirris. 27 | :license: Apache License, Version 2.0, see LICENSE for details. 28 | """ 29 | 30 | from abc import abstractmethod 31 | import logging 32 | MYPY = False 33 | if MYPY: 34 | from typing import Tuple, Union, List 35 | LabelType = Union[int, str] 36 | LocType = Tuple[float, float] 37 | 38 | 39 | logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching") 40 | 41 | 42 | class BaseMap(object): 43 | """Abstract class for a Map.""" 44 | 45 | def __init__(self, name, use_latlon=True): 46 | """Simple database wrapper/stub.""" 47 | self.name = name 48 | self._use_latlon = None 49 | self.distance = None 50 | self.distance_point_to_segment = None 51 | self.distance_segment_to_segment = None 52 | self.box_around_point = None 53 | self.use_latlon = use_latlon 54 | 55 | @property 56 | def use_latlon(self): 57 | return self._use_latlon 58 | 59 | @use_latlon.setter 60 | def use_latlon(self, value): 61 | self._use_latlon = value 62 | if self._use_latlon: 63 | from ..util import dist_latlon as dist_lib 64 | else: 65 | from ..util import dist_euclidean as dist_lib 66 | self.distance = dist_lib.distance 67 | self.distance_point_to_segment = dist_lib.distance_point_to_segment 68 | self.distance_segment_to_segment = dist_lib.distance_segment_to_segment 69 | self.box_around_point = dist_lib.box_around_point 70 | self.lines_parallel = dist_lib.lines_parallel 71 | 72 | @abstractmethod 73 | def bb(self): 74 | """Bounding box. 75 | 76 | :return: (lat_min, lon_min, lat_max, lon_max) 77 | """ 78 | 79 | @abstractmethod 80 | def labels(self): 81 | """Labels of all nodes.""" 82 | 83 | @abstractmethod 84 | def size(self): 85 | """Number of nodes.""" 86 | 87 | @abstractmethod 88 | def node_coordinates(self, node_key): 89 | """Coordinates for given node key.""" 90 | 91 | @abstractmethod 92 | def edges_closeto(self, loc, max_dist=None, max_elmt=None): 93 | """Find edges close to a certain location. 94 | 95 | :param loc: Latitude, Longitude 96 | :param max_dist: Maximal distance that returned nodes can be from lat-lon 97 | :param max_elmt: Maximal number of elements returned after sorting according to distance. 98 | :return: list[tuple[dist, label, loc]] 99 | """ 100 | return None 101 | 102 | @abstractmethod 103 | def nodes_closeto(self, loc, max_dist=None, max_elmt=None): 104 | """Find nodes close to a certain location. 105 | 106 | :param loc: Latitude, Longitude 107 | :param max_dist: Maximal distance that returned nodes can be from lat-lon 108 | :param max_elmt: Maximal number of elements returned after sorting according to distance. 109 | :return: list[tuple[dist, label, loc]] 110 | """ 111 | return None 112 | 113 | @abstractmethod 114 | def nodes_nbrto(self, node): 115 | # type: (BaseMap, LabelType) -> List[Tuple[LabelType, LocType]] 116 | """Return all nodes that are linked to ``node``. 117 | 118 | :param node: Node identifier 119 | :return: list[tuple[label, loc]] 120 | """ 121 | return [] 122 | 123 | def edges_nbrto(self, edge): 124 | # type: (BaseMap, Tuple[LabelType, LabelType]) -> List[Tuple[LabelType, LocType, LabelType, LocType]] 125 | """Return all edges that are linked to ``edge``. 126 | 127 | Defaults to ``nodes_nbrto``. 128 | 129 | :param edge: Edge identifier 130 | :return: list[tuple[label1, label2, loc1, loc2]] 131 | """ 132 | results = [] 133 | l1, l2 = edge 134 | p2 = self.node_coordinates(l2) 135 | for l3, p3 in self.nodes_nbrto(l2): 136 | results.append((l2, p2, l3, p3)) 137 | return results 138 | 139 | @abstractmethod 140 | def all_nodes(self, bb=None): 141 | """All node keys and coordinates. 142 | 143 | :return: [(key, (lat, lon))] 144 | """ 145 | 146 | @abstractmethod 147 | def all_edges(self, bb=None): 148 | """All edges. 149 | 150 | :return: [(key_a, loc_a, key_b, loc_b)] 151 | """ 152 | -------------------------------------------------------------------------------- /leuvenmapmatching/matcher/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.matcher 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: Wannes Meert 7 | :copyright: Copyright 2018 DTAI, KU Leuven and Sirris. 8 | :license: Apache License, Version 2.0, see LICENSE for details. 9 | """ 10 | -------------------------------------------------------------------------------- /leuvenmapmatching/matcher/distance.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.matcher.distance 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: Wannes Meert 7 | :copyright: Copyright 2018 DTAI, KU Leuven and Sirris. 8 | :license: Apache License, Version 2.0, see LICENSE for details. 9 | """ 10 | import logging 11 | import math 12 | 13 | from .base import BaseMatching, BaseMatcher 14 | from ..util.segment import Segment 15 | from ..util.debug import printd 16 | 17 | MYPY = False 18 | if MYPY: 19 | from typing import Tuple, Any, Dict 20 | 21 | 22 | logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching") 23 | 24 | 25 | class DistanceMatching(BaseMatching): 26 | __slots__ = ['d_s', 'd_o', 'lpe', 'lpt'] # Additional fields 27 | 28 | def __init__(self, *args, d_s=0.0, d_o=0.0, lpe=0.0, lpt=0.0, **kwargs): 29 | """ 30 | 31 | :param args: Arguments for BaseMatching 32 | :param d_s: Distance between two (interpolated) states 33 | :param d_o: Distance between two (interpolated) observations 34 | :param lpe: Log probability of emission 35 | :param lpt: Log probablity of transition 36 | :param kwargs: Arguments for BaseMatching 37 | """ 38 | super().__init__(*args, **kwargs) 39 | self.d_o: float = d_o 40 | self.d_s: float = d_s 41 | self.lpe: float = lpe 42 | self.lpt: float = lpt 43 | 44 | def _update_inner(self, m_other): 45 | # type: (DistanceMatching, DistanceMatching) -> None 46 | super()._update_inner(m_other) 47 | self.d_s = m_other.d_s 48 | self.d_o = m_other.d_o 49 | self.lpe = m_other.lpe 50 | self.lpt = m_other.lpt 51 | 52 | @staticmethod 53 | def repr_header(label_width=None, stop=""): 54 | res = BaseMatching.repr_header(label_width) 55 | res += f" {'dt(o)':<6} | {'dt(s)':<6} |" 56 | if logger.isEnabledFor(logging.DEBUG): 57 | res += f" {'lg(Pr-t)':<9} | {'lg(Pr-e)':<9} |" 58 | return res 59 | 60 | def __str__(self, label_width=None): 61 | res = super().__str__(label_width) 62 | res += f" {self.d_o:>6.2f} | {self.d_s:>6.2f} |" 63 | if logger.isEnabledFor(logging.DEBUG): 64 | res += f" {self.lpt:>9.2f} | {self.lpe:>9.2f} |" 65 | return res 66 | 67 | def __repr__(self): 68 | return self.label 69 | 70 | 71 | class DistanceMatcher(BaseMatcher): 72 | """ 73 | Map Matching that takes into account the distance between matched locations on the map compared to 74 | the distance between the observations (that are matched to these locations). It thus prefers matched 75 | paths that have a similar distance than the observations. 76 | 77 | Inspired on the method presented in: 78 | 79 | P. Newson and J. Krumm. Hidden markov map matching through noise and sparseness. 80 | In Proceedings of the 17th ACM SIGSPATIAL international conference on advances 81 | in geographic information systems, pages 336–343. ACM, 2009. 82 | 83 | The options available in :class:``BaseMatcher`` are inherited. Additionally, this class 84 | offers: 85 | 86 | - Transition probability is lower if the distance between observations and states is different 87 | - Transition probability is lower if the the next match is going back on an edge or to a previous edge 88 | - Transition probability is lower if two neighboring states represent not connected edges 89 | - Skip non-emitting states if distance between states and observations is close to each other 90 | """ 91 | 92 | def __init__(self, *args, **kwargs): 93 | """Create a new object. 94 | 95 | :param map_con: Map object to connect to map database 96 | :param obs_noise: Standard deviation of noise 97 | :param obs_noise_ne: Standard deviation of noise for non-emitting states (is set to obs_noise if not given) 98 | :param max_dist_init: Maximum distance from start location (if not given, uses max_dist) 99 | :param max_dist: Maximum distance from path (this is a hard cut, min_prob_norm should be better) 100 | :param min_prob_norm: Minimum normalized probability of observations (ema) 101 | :param non_emitting_states: Allow non-emitting states. A non-emitting state is a state that is 102 | not associated with an observation. Here we assume it can be associated with a location in between 103 | two observations to allow for pruning. It is advised to set min_prob_norm and/or max_dist to avoid 104 | visiting all possible nodes in the graph. 105 | :param non_emitting_length_factor: Reduce the probability of a sequence of non-emitting states the longer it 106 | is. This can be used to prefer shorter paths. This is separate from the transition probabilities because 107 | transition probabilities are averaged for non-emitting states and thus the length is also averaged out. 108 | :param max_lattice_width: Restrict the lattice (or possible candidate states per observation) to this value. 109 | If there are more possible next states, the states with the best likelihood so far are selected. 110 | 111 | :param dist_noise: Standard deviation of difference between distance between states and distance 112 | between observatoins. If not given, set to obs_noise 113 | :param dist_noise_ne: If not given, set to dist_noise 114 | :param restrained_ne: Avoid non-emitting states if the distance between states and between 115 | observations is close to each other. 116 | :param avoid_goingback: If true, the probability is lowered for a transition that returns back to a 117 | previous edges or returns to a position on an edge. 118 | 119 | :param args: Arguments for BaseMatcher 120 | :param kwargs: Arguments for BaseMatcher 121 | """ 122 | 123 | if not kwargs.get("only_edges", True): 124 | logger.warning("The MatcherDistance method only works on edges as states. Nodes have been disabled.") 125 | kwargs["only_edges"] = True 126 | if "matching" not in kwargs: 127 | kwargs["matching"] = DistanceMatching 128 | super().__init__(*args, **kwargs) 129 | self.use_original = kwargs.get('use_original', False) 130 | 131 | # if not use_original, the following value for beta gives a prob of 0.5 at dist=x_half: 132 | # beta = np.sqrt(np.power(x_half, 2) / (np.log(2)*2)) 133 | self.dist_noise = kwargs.get('dist_noise', self.obs_noise) 134 | self.dist_noise_ne = kwargs.get('dist_noise_ne', self.dist_noise) 135 | self.beta = 2 * self.dist_noise ** 2 136 | self.beta_ne = 2 * self.dist_noise_ne ** 2 137 | 138 | self.sigma = 2 * self.obs_noise ** 2 139 | self.sigma_ne = 2 * self.obs_noise_ne ** 2 140 | 141 | self.restrained_ne = kwargs.get('restrained_ne', True) 142 | self.restrained_ne_thr = 1.25 # Threshold 143 | self.exact_dt_s = True # Newson and Krumm is 'True' 144 | 145 | self.avoid_goingback = kwargs.get('avoid_goingback', True) 146 | self.gobackonedge_factor_log = math.log(0.5) 147 | self.gobacktoedge_factor_log = math.log(0.5) 148 | self.first_farend_penalty = math.log(0.75) # should be > gobacktoedge_factor_log 149 | 150 | self.notconnectededges_factor_log = math.log(0.5) 151 | 152 | def logprob_trans(self, prev_m, edge_m, edge_o, 153 | is_prev_ne=False, is_next_ne=False): 154 | # type: (DistanceMatcher, DistanceMatching, Segment, Segment, bool, bool) -> Tuple[float, Dict[str, Any]] 155 | """Transition probability. 156 | 157 | The probability is defined with a formula from the exponential family. 158 | :math:`P(dt) = exp(-d_t^2 / (2 * dist_{noise}^2))` 159 | 160 | with :math:`d_t = |d_s - d_o|, 161 | d_s = |loc_{prev\_state} - loc_{cur\_state}|, 162 | d_o = |loc_{prev\_obs} - loc_{cur\_obs}|` 163 | 164 | This function is more tolerant for low values. The intuition is that values under a certain 165 | distance should all be close to probability 1.0. 166 | 167 | Note: We should also smooth the distance between observations to handle outliers better. 168 | 169 | :param prev_m: Previous matching / state 170 | :param edge_m: Edge between matchings / states 171 | :param edge_o: Edge between observations 172 | :param is_prev_ne: Is previous state non-emitting 173 | :param is_next_ne: Is the next state non-emitting 174 | :param dist_o: First output of distance_progress 175 | :param dist_m: Second output of distance_progress 176 | :return: 177 | """ 178 | d_z = self.map.distance(prev_m.edge_o.pi, edge_o.pi) 179 | is_same_edge = False 180 | if (prev_m.edge_m.l1 == edge_m.l1 and prev_m.edge_m.l2 == edge_m.l2) or \ 181 | (prev_m.edge_m.l1 == edge_m.l2 and prev_m.edge_m.l2 == edge_m.l1): 182 | is_same_edge = True 183 | if ((not self.exact_dt_s) or 184 | is_same_edge or # On same edge 185 | prev_m.edge_m.l2 != edge_m.l1): # Edges are not connected 186 | d_x = self.map.distance(prev_m.edge_m.pi, edge_m.pi) 187 | else: 188 | # Take into account the curvature 189 | d_x = self.map.distance(prev_m.edge_m.pi, prev_m.edge_m.p2) + self.map.distance(prev_m.edge_m.p2, edge_m.pi) 190 | 191 | if is_next_ne: 192 | # For non-emitting states, the distances are added 193 | # Otherwise it can map to a sequence of short segments and stay at the same 194 | # observation because the difference is then always small. 195 | d_z += prev_m.d_o 196 | d_x += prev_m.d_s 197 | 198 | d_t = abs(d_z - d_x) 199 | # p_dt = 1 / beta * math.exp(-d_t / beta) 200 | if is_prev_ne or is_next_ne: 201 | beta = self.beta_ne 202 | else: 203 | beta = self.beta 204 | logprob = -d_t ** 2 / beta 205 | 206 | # Penalties 207 | if prev_m.edge_m.label == edge_m.label: 208 | # Staying in same state 209 | if self.avoid_goingback and edge_m.key == prev_m.edge_m.key and edge_m.ti < prev_m.edge_m.ti: 210 | # Going back on edge (direction is from p1 to p2 of the segment) 211 | logprob += self.gobackonedge_factor_log # Prefer not going back 212 | elif (prev_m.edge_m.l1, prev_m.edge_m.l2) == (edge_m.l2, edge_m.l1): 213 | if self.avoid_goingback: 214 | logprob += self.gobackonedge_factor_log 215 | else: 216 | # Moving states 217 | if prev_m.edge_m.l2 != edge_m.l1: 218 | # We are moving between states that represent edges that are not connected through a node 219 | logprob += self.notconnectededges_factor_log 220 | elif self.avoid_goingback: 221 | # Goin back on state 222 | going_back = False 223 | for m in prev_m.prev: 224 | if edge_m.label == m.edge_m.label: 225 | going_back = True 226 | break 227 | if going_back: 228 | logprob += self.gobacktoedge_factor_log # prefer not going back 229 | 230 | props = { 231 | 'd_o': d_z, 232 | 'd_s': d_x, 233 | 'lpt': logprob 234 | } 235 | return logprob, props 236 | 237 | def logprob_obs(self, dist, prev_m=None, new_edge_m=None, new_edge_o=None, is_ne=False): 238 | # type: (DistanceMatcher, float, DistanceMatching, Segment, Segment, bool) -> Tuple[float, Dict[str, Any]] 239 | """Emission probability for emitting states. 240 | 241 | Exponential family: 242 | :math:`P(dt) = exp(-d_o^2 / (2 * obs_{noise}^2))` 243 | 244 | with :math:`d_o = |loc_{state} - loc_{obs}|` 245 | 246 | """ 247 | if is_ne: 248 | sigma = self.sigma_ne 249 | else: 250 | sigma = self.sigma 251 | result = -dist ** 2 / sigma 252 | props = { 253 | 'lpe': result 254 | } 255 | return result, props 256 | 257 | def _skip_ne_states(self, next_ne_m): 258 | # type: (DistanceMatcher, DistanceMatching) -> bool 259 | # Skip searching for non-emitting states when the distances between nodes 260 | # on the map are similar to the distances between the observation 261 | if not self.restrained_ne: 262 | return False 263 | if next_ne_m.d_s > 0: 264 | factor = (next_ne_m.d_o + next_ne_m.dist_obs) / next_ne_m.d_s 265 | else: 266 | factor = 0 267 | if factor < self.restrained_ne_thr: 268 | logger.debug(f"Skip non-emitting states to {next_ne_m.label}: {factor} < {self.restrained_ne_thr} " 269 | "(observations close enough to each other)") 270 | return True 271 | return False 272 | -------------------------------------------------------------------------------- /leuvenmapmatching/matcher/newsonkrumm.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.matcher.newsonkrumm 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Methods similar to Newson Krumm 2009 for comparison purposes. 7 | 8 | P. Newson and J. Krumm. Hidden markov map matching through noise and sparseness. 9 | In Proceedings of the 17th ACM SIGSPATIAL international conference on advances 10 | in geographic information systems, pages 336–343. ACM, 2009. 11 | 12 | 13 | 14 | :author: Wannes Meert 15 | :copyright: Copyright 2018 DTAI, KU Leuven and Sirris. 16 | :license: Apache License, Version 2.0, see LICENSE for details. 17 | """ 18 | from scipy.stats import norm 19 | import math 20 | import logging 21 | 22 | from .base import BaseMatching, BaseMatcher 23 | 24 | logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching") 25 | 26 | 27 | class NewsonKrummMatching(BaseMatching): 28 | __slots__ = ['d_s', 'd_o', 'lpe', 'lpt'] # Additional fields 29 | 30 | def __init__(self, *args, d_s=1.0, d_o=1.0, lpe=0.0, lpt=0.0, **kwargs): 31 | """ 32 | 33 | :param args: Arguments for BaseMatching 34 | :param d_s: Distance between two (interpolated) states 35 | :param d_o: Distance between two (interpolated) observations 36 | :param lpe: Log probability of emission 37 | :param lpt: Log probablity of transition 38 | :param kwargs: Arguments for BaseMatching 39 | """ 40 | super().__init__(*args, **kwargs) 41 | self.d_o: float = d_o 42 | self.d_s: float = d_s 43 | self.lpe: float = lpe 44 | self.lpt: float = lpt 45 | 46 | def _update_inner(self, m_other): 47 | # type: (NewsonKrummMatching, NewsonKrummMatching) -> None 48 | super()._update_inner(m_other) 49 | self.d_s = m_other.d_s 50 | self.d_o = m_other.d_o 51 | self.lpe = m_other.lpe 52 | self.lpt = m_other.lpt 53 | 54 | @staticmethod 55 | def repr_header(label_width=None, stop=""): 56 | res = BaseMatching.repr_header(label_width) 57 | res += f" {'dt(o)':<6} | {'dt(s)':<6} |" 58 | if logger.isEnabledFor(logging.DEBUG): 59 | res += f" {'lg(Pr-t)':<9} | {'lg(Pr-e)':<9} |" 60 | return res 61 | 62 | def __str__(self, label_width=None): 63 | res = super().__str__(label_width) 64 | res += f" {self.d_o:>6.2f} | {self.d_s:>6.2f} |" 65 | if logger.isEnabledFor(logging.DEBUG): 66 | res += f" {self.lpt:>9.2f} | {self.lpe:>9.2f} |" 67 | return res 68 | 69 | 70 | class NewsonKrummMatcher(BaseMatcher): 71 | """ 72 | Take distance between observations vs states into account. Based on the 73 | method presented in: 74 | 75 | P. Newson and J. Krumm. Hidden markov map matching through noise and sparseness. 76 | In Proceedings of the 17th ACM SIGSPATIAL international conference on advances 77 | in geographic information systems, pages 336–343. ACM, 2009. 78 | 79 | Two important differences: 80 | 81 | * Newson and Krumm use shortest path to handle situations where the distances between 82 | observations are larger than distances between nodes in the graph. The LeuvenMapMatching 83 | toolbox uses non-emitting states to handle this. We thus do not implement the shortest 84 | path algorithm in this class. 85 | * Transition and emission probability are transformed from densities to probababilities by 86 | taking the 1 - CDF instead of the PDF. 87 | 88 | 89 | Newson and Krumm defaults: 90 | 91 | - max_dist = 200 m 92 | - obs_noise = 4.07 m 93 | - beta = 1/6 94 | - only_edges = True 95 | """ 96 | 97 | def __init__(self, *args, **kwargs): 98 | """ 99 | 100 | :param beta: Default is 1/6 101 | :param beta_ne: Default is beta 102 | :param args: Arguments for BaseMatcher 103 | :param kwargs: Arguments for BaseMatcher 104 | """ 105 | 106 | if not kwargs.get("only_edges", True): 107 | logger.warning("The MatcherDistance method only works on edges as states. Nodes have been disabled.") 108 | kwargs["only_edges"] = True 109 | if "matching" not in kwargs: 110 | kwargs["matching"] = NewsonKrummMatching 111 | super().__init__(*args, **kwargs) 112 | 113 | # if not use_original, the following value for beta gives a prob of 0.5 at dist=x_half: 114 | # beta = np.sqrt(np.power(x_half, 2) / (np.log(2)*2)) 115 | self.beta = kwargs.get('beta', 1/6) 116 | self.beta_ne = kwargs.get('beta_ne', self.beta) 117 | 118 | self.obs_noise_dist = norm(scale=self.obs_noise) 119 | self.obs_noise_dist_ne = norm(scale=self.obs_noise_ne) 120 | self.ne_thr = 1.25 121 | 122 | def logprob_trans(self, prev_m: NewsonKrummMatching, edge_m, edge_o, 123 | is_prev_ne=False, is_next_ne=False): 124 | """Transition probability. 125 | 126 | Main difference with Newson and Krumm: we know all points are connected thus do not compute the 127 | shortest path but the distance between two points. 128 | 129 | Original PDF: 130 | p(dt) = 1 / beta * e^(-dt / beta) 131 | with beta = 1/6 132 | 133 | Transformed to probability: 134 | P(dt) = p(d > dt) = e^(-dt / beta) 135 | 136 | :param prev_m: 137 | :param edge_m: 138 | :param edge_o: 139 | :param is_prev_ne: 140 | :param is_next_ne: 141 | :return: 142 | """ 143 | d_z = self.map.distance(prev_m.edge_o.pi, edge_o.pi) 144 | if prev_m.edge_m.label == edge_m.label: 145 | d_x = self.map.distance(prev_m.edge_m.pi, edge_m.pi) 146 | else: 147 | d_x = self.map.distance(prev_m.edge_m.pi, prev_m.edge_m.p2) + self.map.distance(prev_m.edge_m.p2, edge_m.pi) 148 | d_t = abs(d_z - d_x) 149 | # p_dt = 1 / beta * math.exp(-d_t / beta) 150 | if is_prev_ne or is_next_ne: 151 | beta = self.beta_ne 152 | else: 153 | beta = self.beta 154 | # icp_dt = math.exp(-d_t / beta) 155 | # try: 156 | # licp_dt = math.log(icp_dt) 157 | # except ValueError: 158 | # licp_dt = float('-inf') 159 | licp_dt = -d_t / beta 160 | props = { 161 | 'd_o': d_z, 162 | 'd_s': d_x, 163 | 'lpt': licp_dt 164 | } 165 | return licp_dt, props 166 | 167 | def logprob_obs(self, dist, prev_m, new_edge_m, new_edge_o, is_ne=False): 168 | """Emission probability for emitting states. 169 | 170 | Original pdf: 171 | p(d) = N(0, sigma) 172 | with sigma = 4.07m 173 | 174 | Transformed to probability: 175 | P(d) = 2 * (1 - p(d > D)) = 2 * (1 - cdf) 176 | 177 | """ 178 | if is_ne: 179 | result = 2 * (1 - self.obs_noise_dist_ne.cdf(dist)) 180 | else: 181 | result = 2 * (1 - self.obs_noise_dist.cdf(dist)) 182 | try: 183 | result = math.log(result) 184 | except ValueError: 185 | result = -float("inf") 186 | props = { 187 | 'lpe': result 188 | } 189 | return result, props 190 | -------------------------------------------------------------------------------- /leuvenmapmatching/matcher/simple.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.matcher.simple 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: Wannes Meert 7 | :copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris. 8 | :license: Apache License, Version 2.0, see LICENSE for details. 9 | """ 10 | import math 11 | from scipy.stats import halfnorm, norm 12 | 13 | from .base import BaseMatcher, BaseMatching 14 | from ..util.segment import Segment 15 | 16 | 17 | class SimpleMatching(BaseMatching): 18 | pass 19 | 20 | 21 | class SimpleMatcher(BaseMatcher): 22 | 23 | def __init__(self, *args, **kwargs): 24 | """A simple matcher that prefers paths where each matched location is as close as possible to the 25 | observed position. 26 | 27 | :param avoid_goingback: Change the transition probability to be lower for the direction the path is coming 28 | from. 29 | :param kwargs: Arguments passed to :class:`BaseMatcher`. 30 | """ 31 | if "matching" not in kwargs: 32 | kwargs['matching'] = SimpleMatching 33 | super().__init__(*args, **kwargs) 34 | 35 | self.obs_noise_dist = halfnorm(scale=self.obs_noise) 36 | self.obs_noise_dist_ne = halfnorm(scale=self.obs_noise_ne) 37 | # normalize to max 1 to simulate a prob instead of density 38 | self.obs_noise_logint = math.log(self.obs_noise * math.sqrt(2 * math.pi) / 2) 39 | self.obs_noise_logint_ne = math.log(self.obs_noise_ne * math.sqrt(2 * math.pi) / 2) 40 | 41 | # Transition probability is divided (in logprob_trans) by this factor if we move back on the 42 | # current edge. 43 | self.avoid_goingback = kwargs.get('avoid_goingback', True) 44 | self.gobackonedge_factor_log = math.log(0.99) 45 | # Transition probability is divided (in logprob_trans) by this factor if the next state is 46 | # also the previous state, thus if we go back 47 | self.gobacktoedge_factor_log = math.log(0.5) 48 | # Transition probability is divided (in logprob_trans) by this factor if a transition is made 49 | # This is to try to stay on the same node unless there is a good reason 50 | self.transition_factor = math.log(0.9) 51 | 52 | def logprob_trans(self, prev_m: BaseMatching, edge_m: Segment, edge_o: Segment, 53 | is_prev_ne=False, is_next_ne=False): 54 | """Transition probability. 55 | 56 | Note: In contrast with a regular HMM, this is not a probability density function, it needs 57 | to be a proper probability (thus values between 0.0 and 1.0). 58 | """ 59 | logprob = 0 60 | if prev_m.edge_m.label == edge_m.label: 61 | # Staying in same state 62 | if self.avoid_goingback and edge_m.key == prev_m.edge_m.key and edge_m.ti < prev_m.edge_m.ti: 63 | # Going back on edge 64 | logprob += self.gobackonedge_factor_log # prefer not going back 65 | else: 66 | # Moving states 67 | logprob += self.transition_factor 68 | if self.avoid_goingback: 69 | # Goin back on state 70 | going_back = False 71 | for m in prev_m.prev: 72 | if edge_m.label == m.edge_m.label: 73 | going_back = True 74 | break 75 | if going_back: 76 | logprob += self.gobacktoedge_factor_log # prefer not going back 77 | return logprob, {} # All probabilities are 1 (thus technically not a distribution) 78 | 79 | def logprob_obs(self, dist, prev_m, new_edge_m, new_edge_o, is_ne=False): 80 | """Emission probability. 81 | 82 | Note: In contrast with a regular HMM, this is not a probability density function, it needs 83 | to be a proper probability (thus values between 0.0 and 1.0). 84 | """ 85 | if is_ne: 86 | result = self.obs_noise_dist_ne.logpdf(dist) + self.obs_noise_logint_ne 87 | else: 88 | result = self.obs_noise_dist.logpdf(dist) + self.obs_noise_logint 89 | # print("logprob_obs: {} -> {:.5f} = {:.5f}".format(dist, result, math.exp(result))) 90 | return result, {} 91 | -------------------------------------------------------------------------------- /leuvenmapmatching/util/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.util 4 | ~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: Wannes Meert 7 | :copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris. 8 | :license: Apache License, Version 2.0, see LICENSE for details. 9 | """ 10 | 11 | # No automatic loading to avoid dependency on packages such as nvector and gpxpy if not used. 12 | 13 | 14 | def approx_equal(a, b, rtol=0.0, atol=1e-08): 15 | return abs(a - b) <= (atol + rtol * abs(b)) 16 | 17 | 18 | def approx_leq(a, b, rtol=0.0, atol=1e-08): 19 | return (a - b) <= (atol + rtol * abs(b)) 20 | -------------------------------------------------------------------------------- /leuvenmapmatching/util/debug.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching") 5 | 6 | 7 | def printd(*args, **kwargs): 8 | """Print to debug output.""" 9 | logger.debug(*args, **kwargs) 10 | -------------------------------------------------------------------------------- /leuvenmapmatching/util/dist_euclidean.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.util.dist_euclidean 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: Wannes Meert 7 | :copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris. 8 | :license: Apache License, Version 2.0, see LICENSE for details. 9 | """ 10 | import logging 11 | import math 12 | 13 | import numpy as np 14 | 15 | logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching") 16 | 17 | 18 | def distance(p1, p2): 19 | result = math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) 20 | # print("distance({}, {}) -> {}".format(p1, p2, result)) 21 | return result 22 | 23 | 24 | def distance_point_to_segment(p, s1, s2, delta=0.0): 25 | p_int, ti = project(s1, s2, p, delta=delta) 26 | return distance(p_int, p), p_int, ti 27 | # l1a = np.array(s1) 28 | # l2a = np.array(s2) 29 | # pa = np.array(p) 30 | # return np.linalg.norm(np.cross(l2a - l1a, l1a - pa)) / np.linalg.norm(l2a - l1a) 31 | 32 | 33 | def distance_segment_to_segment(f1, f2, t1, t2): 34 | """Distance between segments.. 35 | 36 | :param f1: From 37 | :param f2: 38 | :param t1: To 39 | :param t2: 40 | :return: (distance, proj on f, proj on t, rel pos on f, rel pos on t) 41 | """ 42 | x1, y1 = f1 43 | x2, y2 = f2 44 | x3, y3 = t1 45 | x4, y4 = t2 46 | n = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)) 47 | if np.allclose([n], [0], rtol=0): 48 | # parallel 49 | is_parallel = True 50 | n = 0.0001 # TODO: simulates a point far away 51 | else: 52 | is_parallel = False 53 | u_f = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / n 54 | u_t = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / n 55 | xi = x1 + u_f * (x2 - x1) 56 | yi = y1 + u_f * (y2 - y1) 57 | changed_f = False 58 | changed_t = False 59 | if u_t > 1: 60 | u_t = 1 61 | changed_t = True 62 | elif u_t < 0: 63 | u_t = 0 64 | changed_t = True 65 | if u_f > 1: 66 | u_f = 1 67 | changed_f = True 68 | elif u_f < 0: 69 | u_f = 0 70 | changed_f = True 71 | if not changed_t and not changed_f: 72 | return 0, (xi, yi), (xi, yi), u_f, u_t 73 | xf = x1 + u_f * (x2 - x1) 74 | yf = y1 + u_f * (y2 - y1) 75 | xt = x3 + u_t * (x4 - x3) 76 | yt = y3 + u_t * (y4 - y3) 77 | if changed_t and changed_f: 78 | # Compare furthest point from intersection with segment 79 | df = (xf - xi) ** 2 + (yf - yi) ** 2 80 | dt = (xt - xi) ** 2 + (yt - yi) ** 2 81 | if df > dt: 82 | changed_t = False 83 | else: 84 | changed_f = False 85 | if changed_t: 86 | pt = (xt, yt) 87 | pf, u_f = project(f1, f2, pt) 88 | elif changed_f: 89 | pf = (xf, yf) 90 | pt, u_t = project(t1, t2, pf) 91 | else: 92 | raise Exception(f"Should not happen") 93 | d = distance(pf, pt) 94 | return d, pf, pt, u_f, u_t 95 | 96 | 97 | def project(s1, s2, p, delta=0.0): 98 | """ 99 | 100 | :param s1: Segment start 101 | :param s2: Segment end 102 | :param p: Point 103 | :param delta: Keep delta fraction away from ends 104 | :return: Point of projection, Relative position on segment 105 | """ 106 | if np.isclose(s1[0], s2[0], rtol=0) and np.isclose(s1[1], s2[1], rtol=0): 107 | return s1, 0.0 108 | 109 | l2 = (s1[0]-s2[0])**2 + (s1[1]-s2[1])**2 110 | t = max(delta, min(1-delta, ((p[0]-s1[0])*(s2[0]-s1[0]) + (p[1]-s1[1])*(s2[1]-s1[1])) / l2)) 111 | return (s1[0] + t * (s2[0]-s1[0]), s1[1] + t * (s2[1]-s1[1])), t 112 | 113 | 114 | def interpolate_path(path, dd): 115 | """ 116 | TODO: interplate time as third term 117 | :param path: (y, x) 118 | :param dd: Distance difference (meter) 119 | :return: 120 | """ 121 | path_new = [path[0]] 122 | for p1, p2 in zip(path, path[1:]): 123 | dist = distance(p1, p2) 124 | if dist > dd: 125 | dt = int(math.ceil(dist / dd)) 126 | dx = (p2[0] - p1[0]) / dt 127 | dy = (p2[1] - p1[1]) / dt 128 | px, py = p1[0], p1[1] 129 | for _ in range(dt): 130 | px += dx 131 | py += dy 132 | path_new.append((px, py)) 133 | path_new.append(p2) 134 | return path_new 135 | 136 | 137 | def box_around_point(p, dist): 138 | lat, lon = p 139 | lat_t, lon_r = lat + dist, lon + dist 140 | lat_b, lon_l = lat - dist, lon - dist 141 | return lat_b, lon_l, lat_t, lon_r 142 | 143 | 144 | def lines_parallel(la, lb, lc, ld, d=None): 145 | x1 = la[0] - lb[0] 146 | y1 = la[1] - lb[1] 147 | if x1 == 0: 148 | if y1 == 0: 149 | return False 150 | s1 = 0 151 | else: 152 | s1 = math.atan(abs(y1 / x1)) 153 | x2 = lc[0] - ld[0] 154 | y2 = lc[1] - ld[1] 155 | if x2 == 0: 156 | s2 = 0 157 | if y2 == 0: 158 | return False 159 | else: 160 | s2 = math.atan(abs(y2 / x2)) 161 | thr = math.pi / 180 162 | if abs(s1 - s2) > thr: 163 | return False 164 | if d is not None: 165 | dist, _, _, _, _ = distance_segment_to_segment(la, lb, lc, ld) 166 | if dist > d: 167 | return False 168 | return True 169 | 170 | -------------------------------------------------------------------------------- /leuvenmapmatching/util/dist_latlon.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.util.dist_latlon 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Based on: 7 | https://www.movable-type.co.uk/scripts/latlong.html 8 | https://www.movable-type.co.uk/scripts/latlong-vectors.html 9 | 10 | :author: Wannes Meert 11 | :copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris. 12 | :license: Apache License, Version 2.0, see LICENSE for details. 13 | """ 14 | import logging 15 | import math 16 | from math import radians, cos, sin, asin, acos, sqrt, atan2, fabs, degrees, ceil, copysign 17 | 18 | from . import dist_euclidean as diste 19 | 20 | 21 | logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching") 22 | earth_radius = 6371000 23 | 24 | 25 | def distance(p1, p2): 26 | """Distance between two points. 27 | 28 | :param p1: (Lat, Lon) 29 | :param p2: (Lat, Lon) 30 | :return: Distance in meters 31 | """ 32 | lat1, lon1 = p1[0], p1[1] 33 | lat2, lon2 = p2[0], p2[1] 34 | lat1, lon1 = radians(lat1), radians(lon1) 35 | lat2, lon2 = radians(lat2), radians(lon2) 36 | dist = distance_haversine_radians(lat1, lon1, lat2, lon2) 37 | return dist 38 | 39 | 40 | def distance_point_to_segment(p, s1, s2, delta=0.0, constrain=True): 41 | """Distance between point and segment. 42 | 43 | Cross-track distance. 44 | 45 | https://www.movable-type.co.uk/scripts/latlong.html#cross-track 46 | 47 | :param s1: Segment start point 48 | :param s2: Segment end point 49 | :param p: Point to measure distance from path to 50 | :param delta: Stay away from the endpoints with this factor 51 | :return: (Distance in meters, projected location on segment, relative location on segment) 52 | """ 53 | lat1, lon1 = s1 # Start point 54 | lat2, lon2 = s2 # End point 55 | lat3, lon3 = p[0], p[1] 56 | lat1, lon1 = radians(lat1), radians(lon1) 57 | lat2, lon2 = radians(lat2), radians(lon2) 58 | lat3, lon3 = radians(lat3), radians(lon3) 59 | 60 | dist_hs = distance_haversine_radians(lat1, lon1, lat2, lon2) 61 | if dist_hs == 0: 62 | dist_ct, pi, ti = distance(p, s1), s1, 0 63 | return dist_ct, pi, ti 64 | 65 | d13 = distance_haversine_radians(lat1, lon1, lat3, lon3) 66 | delta13 = d13 / earth_radius 67 | b13 = bearing_radians(lat1, lon1, lat3, lon3) 68 | b12 = bearing_radians(lat1, lon1, lat2, lon2) 69 | 70 | dxt = asin(sin(delta13) * sin(b13 - b12)) 71 | # b13d12 = (b13 - b12) % (2 * math.pi) 72 | # if b13d12 > math.pi: 73 | # b13d12 = 2 * math.pi - b13d12 74 | dist_ct = fabs(dxt) * earth_radius 75 | # Correct to negative value if point is before segment 76 | # sgn = -1 if b13d12 > (math.pi / 2) else 1 77 | sgn = copysign(1, cos(b12 - b13)) 78 | dat = sgn * acos(cos(delta13) / abs(cos(dxt))) * earth_radius 79 | ti = dat / dist_hs 80 | 81 | if not constrain: 82 | lati, loni = destination_radians(lat1, lon1, b12, dat) 83 | elif ti > 1.0: 84 | ti = 1.0 85 | lati, loni = lat2, lon2 86 | dist_ct = distance_haversine_radians(lat3, lon3, lati, loni) 87 | elif ti < 0.0: 88 | ti = 0.0 89 | lati, loni = lat1, lon1 90 | dist_ct = distance_haversine_radians(lat3, lon3, lati, loni) 91 | else: 92 | lati, loni = destination_radians(lat1, lon1, b12, dat) 93 | pi = (degrees(lati), degrees(loni)) 94 | 95 | return dist_ct, pi, ti 96 | 97 | 98 | def distance_segment_to_segment(f1, f2, t1, t2): 99 | """Distance between segments. If no intersection within range, simplified to distance from f2 to [t1,t2]. 100 | 101 | :param f1: From 102 | :param f2: 103 | :param t1: To 104 | :param t2: 105 | :return: (distance, proj on f, proj on t, rel pos on t) 106 | """ 107 | # Translate lat-lon to x-y and apply the Euclidean function 108 | latf1, lonf1 = f1 109 | latf1, lonf1 = radians(latf1), radians(lonf1) 110 | f1 = 0, 0 # Origin 111 | 112 | latf2, lonf2 = f2 113 | latf2, lonf2 = radians(latf2), radians(lonf2) 114 | df1f2 = distance_haversine_radians(latf1, lonf1, latf2, lonf2) 115 | bf1f2 = bearing_radians(latf1, lonf1, latf2, lonf2) 116 | # print(f"bf1f2 = {bf1f2} = {degrees(bf1f2)} degrees") 117 | f2 = (df1f2 * cos(bf1f2), df1f2 * sin(bf1f2)) 118 | 119 | latt1, lont1 = t1[0], t1[1] 120 | latt1, lont1 = radians(latt1), radians(lont1) 121 | df1t1 = distance_haversine_radians(latf1, lonf1, latt1, lont1) 122 | bf1t1 = bearing_radians(latf1, lonf1, latt1, lont1) 123 | # print(f"bf1t1 = {bf1t1} = {degrees(bf1t1)} degrees") 124 | t1 = (df1t1 * cos(bf1t1), df1t1 * sin(bf1t1)) 125 | 126 | latt2, lont2 = t2[0], t2[1] 127 | latt2, lont2 = radians(latt2), radians(lont2) 128 | dt1t2 = distance_haversine_radians(latt1, lont1, latt2, lont2) 129 | # print(f"dt1t2 = {dt1t2}") 130 | bt1t2 = bearing_radians(latt1, lont1, latt2, lont2) 131 | # print(f"bt1t2 = {bt1t2} = {degrees(bt1t2)} degrees") 132 | t2 = (t1[0] + dt1t2 * cos(bt1t2), t1[1] + dt1t2 * sin(bt1t2)) 133 | 134 | d, pf, pt, u_f, u_t = diste.distance_segment_to_segment(f1, f2, t1, t2) 135 | pf = destination_radians(latf1, lonf1, bf1f2, u_f * df1f2) 136 | pf = (degrees(pf[0]), degrees(pf[1])) 137 | pt = destination_radians(latt1, lont1, bt1t2, u_t * dt1t2) 138 | pt = (degrees(pt[0]), degrees(pt[1])) 139 | 140 | return d, pf, pt, u_f, u_t 141 | 142 | 143 | def project(s1, s2, p, delta=0.0): 144 | _, pi, ti = distance_point_to_segment(p, s1, s2, delta) 145 | return pi, ti 146 | 147 | 148 | def box_around_point(p, dist): 149 | lat, lon = p 150 | latr, lonr = radians(lat), radians(lon) 151 | # diag_dist = sqrt(2 * dist ** 2) 152 | diag_dist = dist 153 | lat_t, lon_r = destination_radians(latr, lonr, radians(45), diag_dist) 154 | lat_b, lon_l = destination_radians(latr, lonr, radians(225), diag_dist) 155 | lat_t, lon_r = degrees(lat_t), degrees(lon_r) 156 | lat_b, lon_l = degrees(lat_b), degrees(lon_l) 157 | return lat_b, lon_l, lat_t, lon_r 158 | 159 | 160 | def interpolate_path(path, dd): 161 | """ 162 | :param path: (lat, lon) 163 | :param dd: Distance difference (meter) 164 | :return: 165 | """ 166 | path_new = [path[0]] 167 | for p1, p2 in zip(path, path[1:]): 168 | lat1, lon1 = p1[0], p1[1] 169 | lat2, lon2 = p2[0], p2[1] 170 | lat1, lon1 = radians(lat1), radians(lon1) 171 | lat2, lon2 = radians(lat2), radians(lon2) 172 | dist = distance_haversine_radians(lat1, lon1, lat2, lon2) 173 | if dist > dd: 174 | dt = int(ceil(dist / dd)) 175 | distd = dist/dt 176 | disti = 0 177 | brng = bearing_radians(lat1, lon1, lat2, lon2) 178 | for _ in range(dt): 179 | disti += distd 180 | lati, loni = destination_radians(lat1, lon1, brng, disti) 181 | path_new.append((degrees(lati), degrees(loni))) 182 | path_new.append(p2) 183 | return path_new 184 | 185 | 186 | def bearing_radians(lat1, lon1, lat2, lon2): 187 | """Initial bearing""" 188 | dlon = lon2 - lon1 189 | y = sin(dlon) * cos(lat2) 190 | x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlon) 191 | return atan2(y, x) 192 | 193 | 194 | def distance_haversine_radians(lat1, lon1, lat2, lon2, radius=earth_radius): 195 | # type: (float, float, float, float, float) -> float 196 | lat = lat2 - lat1 197 | lon = lon2 - lon1 198 | a = sin(lat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(lon / 2) ** 2 199 | # dist = 2 * radius * asin(sqrt(a)) 200 | dist = 2 * radius * atan2(sqrt(a), sqrt(1 - a)) 201 | return dist 202 | 203 | 204 | def destination_radians(lat1, lon1, bearing, dist): 205 | d = dist / earth_radius 206 | lat2 = asin(sin(lat1) * cos(d) + cos(lat1) * sin(d) * cos(bearing)) 207 | lon2 = lon1 + atan2(sin(bearing) * sin(d) * cos(lat1), cos(d) - sin(lat1) * sin(lat2)) 208 | return lat2, lon2 209 | 210 | 211 | def lines_parallel(f1, f2, t1, t2, d=None): 212 | latf1, lonf1 = f1 213 | latf1, lonf1 = radians(latf1), radians(lonf1) 214 | f1 = 0, 0 # Origin 215 | 216 | latf2, lonf2 = f2 217 | latf2, lonf2 = radians(latf2), radians(lonf2) 218 | df1f2 = distance_haversine_radians(latf1, lonf1, latf2, lonf2) 219 | bf1f2 = bearing_radians(latf1, lonf1, latf2, lonf2) 220 | # print(f"bf1f2 = {bf1f2} = {degrees(bf1f2)} degrees") 221 | f2 = (df1f2 * cos(bf1f2), df1f2 * sin(bf1f2)) 222 | 223 | latt1, lont1 = t1 224 | latt1, lont1 = radians(latt1), radians(lont1) 225 | df1t1 = distance_haversine_radians(latf1, lonf1, latt1, lont1) 226 | bf1t1 = bearing_radians(latf1, lonf1, latt1, lont1) 227 | # print(f"bf1t1 = {bf1t1} = {degrees(bf1t1)} degrees") 228 | t1 = (df1t1 * cos(bf1t1), df1t1 * sin(bf1t1)) 229 | 230 | latt2, lont2 = t2 231 | latt2, lont2 = radians(latt2), radians(lont2) 232 | dt1t2 = distance_haversine_radians(latt1, lont1, latt2, lont2) 233 | # print(f"dt1t2 = {dt1t2}") 234 | bt1t2 = bearing_radians(latt1, lont1, latt2, lont2) 235 | # print(f"bt1t2 = {bt1t2} = {degrees(bt1t2)} degrees") 236 | t2 = (t1[0] + dt1t2 * cos(bt1t2), t1[1] + dt1t2 * sin(bt1t2)) 237 | 238 | return diste.lines_parallel(f1, f2, t1, t2, d=d) 239 | -------------------------------------------------------------------------------- /leuvenmapmatching/util/dist_latlon_nvector.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.util.dist_latlon_nvector 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: Wannes Meert 7 | :copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris. 8 | :license: Apache License, Version 2.0, see LICENSE for details. 9 | """ 10 | import logging 11 | import math 12 | 13 | import numpy as np 14 | from nvector._core import unit, n_E2lat_lon, great_circle_normal 15 | import nvector as nv 16 | 17 | 18 | frame = nv.FrameE(a=6371e3, f=0) 19 | logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching") 20 | 21 | 22 | def distance(p1, p2): 23 | """ 24 | 25 | :param p1: 26 | :param p2: 27 | :return: Distance in meters 28 | """ 29 | p1 = frame.GeoPoint(p1[0], p1[1], degrees=True) 30 | p2 = frame.GeoPoint(p2[0], p2[1], degrees=True) 31 | d, _, _ = p1.distance_and_azimuth(p2) 32 | # print("distance_latlon({}, {}) -> {}".format(p1, p2, d)) 33 | return d 34 | 35 | 36 | def distance_gp(p1, p2): 37 | d, _, _ = p1.distance_and_azimuth(p2) 38 | return d 39 | 40 | 41 | def distance_point_to_segment(p, s1, s2, delta=0.0): 42 | """ 43 | TODO: A point exactly on the line gives an error. 44 | 45 | :param s1: Segment start point 46 | :param s2: Segment end point 47 | :param p: Point to measure distance from path to 48 | :param delta: Stay away from the endpoints with this factor 49 | :return: (Distance in meters, projected location on segmegnt) 50 | """ 51 | # TODO: Initialize all points as GeoPoint when loading data 52 | s1 = frame.GeoPoint(s1[0], s1[1], degrees=True) 53 | s2 = frame.GeoPoint(s2[0], s2[1], degrees=True) 54 | p = frame.GeoPoint(p[0], p[1], degrees=True) 55 | p_int, ti = _project_nvector(s1, s2, p) 56 | d, _, _ = p.distance_and_azimuth(p_int) 57 | return d, (p_int.latitude_deg[0], p_int.longitude_deg[0]), ti 58 | 59 | 60 | def distance_segment_to_segment(f1, f2, t1, t2): 61 | """Distance between segments. If no intersection within range, simplified to distance from f2 to [t1,t2]. 62 | 63 | :param f1: From 64 | :param f2: 65 | :param t1: To 66 | :param t2: 67 | :return: (distance, proj on f, proj on t, rel pos on t) 68 | """ 69 | # TODO: Should be improved 70 | f1_gp = frame.GeoPoint(f1[0], f1[1], degrees=True) 71 | f2_gp = frame.GeoPoint(f2[0], f2[1], degrees=True) 72 | path_f = nv.GeoPath(f1_gp, f2_gp) 73 | t1_gp = frame.GeoPoint(t1[0], t1[1], degrees=True) 74 | t2_gp = frame.GeoPoint(t2[0], t2[1], degrees=True) 75 | path_t = nv.GeoPath(t1_gp, t2_gp) 76 | p_int = path_f.intersect(path_t) 77 | p_int_gp = p_int.to_geo_point() 78 | if path_f.on_path(p_int)[0] and path_t.on_path(p_int)[0]: 79 | # Intersection point is on segments, between both begins and ends 80 | loc = (p_int_gp.latitude_deg[0], p_int_gp.longitude_deg[0]) 81 | u_f = distance_gp(f1_gp, p_int_gp) / distance_gp(f1_gp, f2_gp) 82 | u_t = distance_gp(t1_gp, p_int_gp) / distance_gp(t1_gp, t2_gp) 83 | return 0, loc, loc, u_f, u_t 84 | # No intersection, use last point of map segment (the assumption is the observations are far apart) 85 | # TODO: decide which point to use (see distance_segment_to_segment) 86 | p_int, u_t = _project_nvector(t1_gp, t2_gp, f2_gp) 87 | u_f = 1 88 | d, _, _ = f2_gp.distance_and_azimuth(p_int) 89 | return d, (f1, f2), (p_int_gp.latitude_deg[0], p_int_gp.longitude_deg[0]), u_f, u_t 90 | 91 | 92 | def project(s1, s2, p, delta=0.0): 93 | s1 = frame.GeoPoint(s1[0], s1[1], degrees=True) 94 | s2 = frame.GeoPoint(s2[0], s2[1], degrees=True) 95 | p = frame.GeoPoint(p[0], p[1], degrees=True) 96 | p_int, ti = _project_nvector(s1, s2, p, delta=delta) 97 | return (p_int.latitude_deg[0], p_int.longitude_deg[0]), ti 98 | 99 | 100 | def _project_nvector(s1, s2, p, delta=0.0): 101 | path = nv.GeoPath(s1, s2) 102 | p_intr = _cross_track_point(path, p) 103 | pin = p_intr.to_nvector().normal 104 | s1n = s1.to_nvector().normal 105 | s2n = s2.to_nvector().normal 106 | ti = np.linalg.norm(pin - s1n) / np.linalg.norm(s2n - s1n) 107 | ti = max(delta, min(1 - delta, ti)) 108 | return path.interpolate(ti).to_geo_point(), ti 109 | 110 | 111 | def _cross_track_point(path, point): 112 | """Extend nvector package to find the projection point. 113 | 114 | The projection point is the closest point on path to the given point. 115 | Based on the nvector.cross_track_distance function. 116 | http://www.navlab.net/nvector/ 117 | 118 | :param path: GeoPath 119 | :param point: GeoPoint 120 | """ 121 | c_E = great_circle_normal(*path.nvector_normals()) 122 | n_EB_E = point.to_nvector().normal # type: np.array 123 | c_EP_E = np.cross(c_E, n_EB_E, axis=0) 124 | 125 | # Find intersection point C that is closest to point B 126 | frame = path.positionA.frame 127 | n_EA1_E = path.positionA.to_nvector().normal # should also be ok to use n_EB_C 128 | n_EC_E_tmp = unit(np.cross(c_E, c_EP_E, axis=0), norm_zero_vector=np.nan) 129 | n_EC_E = np.sign(np.dot(n_EC_E_tmp.T, n_EA1_E)) * n_EC_E_tmp 130 | if np.any(np.isnan(n_EC_E)): 131 | raise Exception('Paths are Equal. Intersection point undefined. NaN returned.') 132 | lat_C, long_C = n_E2lat_lon(n_EC_E, frame.R_Ee) 133 | return nv.GeoPoint(lat_C, long_C, frame=frame) 134 | 135 | 136 | def interpolate_path(path, dd): 137 | """ 138 | TODO: interplate time as third term 139 | :param path: (lat, lon) 140 | :param dd: Distance difference (meter) 141 | :return: 142 | """ 143 | path_new = [path[0]] 144 | for p1, p2 in zip(path, path[1:]): 145 | dist = distance(p1, p2) 146 | if dist > dd: 147 | s1 = frame.GeoPoint(p1[0], p1[1], degrees=True) 148 | s2 = frame.GeoPoint(p2[0], p2[1], degrees=True) 149 | segment = nv.GeoPath(s1, s2) 150 | dt = int(math.floor(dist / dd)) 151 | for dti in range(1, dt): 152 | p_new = segment.interpolate(dti/dt).to_geo_point() 153 | path_new.append((p_new.latitude_deg[0], p_new.longitude_deg[0])) 154 | path_new.append(p2) 155 | return path_new 156 | -------------------------------------------------------------------------------- /leuvenmapmatching/util/evaluation.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.util.evaluation 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Methods to help set up and evaluate experiments. 7 | 8 | :author: Wannes Meert 9 | :copyright: Copyright 2018 DTAI, KU Leuven and Sirris. 10 | :license: Apache License, Version 2.0, see LICENSE for details. 11 | """ 12 | import logging 13 | from dtaidistance.alignment import needleman_wunsch, best_alignment 14 | 15 | from . import dist_latlon 16 | MYPY = False 17 | if MYPY: 18 | from ..map.base import BaseMap 19 | from typing import List, Tuple, Optional, Callable 20 | 21 | 22 | logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching") 23 | 24 | 25 | def route_mismatch_factor(map_con, path_pred, path_grnd, window=None, dist_fn=None, keep_mismatches=False): 26 | # type: (BaseMap, List[int], List[int], Optional[int], Optional[Callable], bool) -> Tuple[float, float, float, float, List[Tuple[int, int]], float, float] 27 | """Evaluation method from Newson and Krumm (2009). 28 | 29 | :math:`f = \frac{d_{-} + d_{+}}{d_0}` 30 | 31 | With :math:`d_{-}` the length that is erroneously subtracted, 32 | :math:`d_{+}` the length that is erroneously added, and :math:`d_0` the 33 | distance of the correct route. 34 | 35 | This function only supports connected states (thus not switching between states 36 | that are not connected (e.g. parallel roads). 37 | 38 | Also computes the Accuracy by Number (AN) and Accuracy by Length (AL) metrics from 39 | Zheng et al. (2009). 40 | """ 41 | if dist_fn is None: 42 | dist_fn = dist_latlon.distance 43 | _, matrix = needleman_wunsch(path_pred, path_grnd, window=window) 44 | print(matrix[:10, :10]) 45 | algn, _, _ = best_alignment(matrix) 46 | print(algn[:10]) 47 | d_plus = 0 # length erroneously added 48 | d_min = 0 # length erroneously subtracted 49 | d_zero = 0 # length of correct route 50 | cnt_matches = 0 # number of perfect matches 51 | cnt_mismatches = 0 52 | mismatches = [] if keep_mismatches else None 53 | 54 | prev_grnd_pi = None 55 | for pred_pi, grnd_pi in algn: 56 | pred_p = path_pred[pred_pi] 57 | grnd_p = path_grnd[grnd_pi] 58 | grnd_d = map_con.path_dist(grnd_p) 59 | d_zero += grnd_d 60 | if pred_p == grnd_p: 61 | cnt_matches += 1 62 | else: 63 | # print(f"Mismatch: {pred_p} != {grnd_p}") 64 | cnt_mismatches += 1 65 | pred_d = map_con.path_dist(pred_p) 66 | d_plus += pred_d 67 | d_min += grnd_d 68 | if keep_mismatches: 69 | mismatches.append((pred_p, grnd_p)) 70 | prev_grnd_pi = grnd_pi 71 | 72 | factor = (d_min + d_plus) / d_zero 73 | an = cnt_matches / len(path_grnd) 74 | al = (d_zero - d_min) / d_zero 75 | return factor, cnt_matches, cnt_mismatches, d_zero, mismatches, an, al 76 | -------------------------------------------------------------------------------- /leuvenmapmatching/util/gpx.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.util.gpx 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Some additional functions to interact with the gpx library. 7 | 8 | :author: Wannes Meert 9 | :copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris. 10 | :license: Apache License, Version 2.0, see LICENSE for details. 11 | """ 12 | import logging 13 | 14 | import gpxpy 15 | import gpxpy.gpx 16 | 17 | 18 | logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching") 19 | 20 | 21 | def gpx_to_path(gpx_file): 22 | gpx_fh = open(gpx_file) 23 | track = None 24 | try: 25 | gpx = gpxpy.parse(gpx_fh) 26 | if len(gpx.tracks) == 0: 27 | logger.error('No tracks found in GPX file ( tag missing?): {}'.format(gpx_file)) 28 | return None 29 | logger.info("Read gpx file: {} points, {} tracks, {} segments".format( 30 | gpx.get_points_no(), len(gpx.tracks), len(gpx.tracks[0].segments))) 31 | track = [(p.latitude, p.longitude, p.time) for p in gpx.tracks[0].segments[0].points] 32 | finally: 33 | gpx_fh.close() 34 | return track 35 | 36 | 37 | def path_to_gpx(path, filename=None): 38 | gpx = gpxpy.gpx.GPX() 39 | 40 | # Create first track in our GPX: 41 | gpx_track = gpxpy.gpx.GPXTrack() 42 | gpx.tracks.append(gpx_track) 43 | 44 | # Create first segment in our GPX track: 45 | gpx_segment = gpxpy.gpx.GPXTrackSegment() 46 | gpx_track.segments.append(gpx_segment) 47 | 48 | gpx_segment.points = [(gpxpy.gpx.GPXTrackPoint(lat, lon, time=time)) for (lat, lon, time) in path] 49 | 50 | if filename: 51 | with open(filename, 'w') as gpx_fh: 52 | gpx_fh.write(gpx.to_xml()) 53 | 54 | return gpx 55 | -------------------------------------------------------------------------------- /leuvenmapmatching/util/kalman.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.util.kalman 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: Wannes Meert 7 | :copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris. 8 | :license: Apache License, Version 2.0, see LICENSE for details. 9 | """ 10 | import logging 11 | 12 | from pykalman import KalmanFilter 13 | import numpy as np 14 | 15 | 16 | logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching") 17 | 18 | 19 | def smooth_path(path, dt=1, obs_noise=1e-4, loc_var=1e-4, vel_var=1e-6, kf=None, 20 | rm_outliers=False, use_euclidean=True, n_iter=1000): 21 | """Apply Kalman filtering. Assumes data with a constant sample rate. 22 | 23 | Inspired by https://github.com/FlorianWilhelm/gps_data_with_python 24 | 25 | :param path: 26 | :param dt: Sample interval in seconds 27 | :param obs_noise: Observation noise (default=1e-4, approx 10-30m) 28 | :param loc_var: estimated location variance 29 | :param vel_var: estimated velocity variance 30 | :param kf: Trained Kalman filter 31 | :param rm_outliers: Remove outliers based on Kalman prediction 32 | True or 1 will be removal, 2 will also retrain after removal 33 | :param use_euclidean: 34 | :param n_iter: Kalman iterations 35 | :return: 36 | """ 37 | path = np.array(path) 38 | if kf is None: 39 | # state is (x, y, v_x, v_y) 40 | F = np.array([[1, 0, dt, 0], 41 | [0, 1, 0, dt], 42 | [0, 0, 1, 0], 43 | [0, 0, 0, 1]]) 44 | 45 | # observations is (x, y) 46 | H = np.array([[1, 0, 0, 0], 47 | [0, 1, 0, 0]]) 48 | 49 | R = np.diag([obs_noise, obs_noise]) ** 2 50 | 51 | initial_state_mean = np.hstack([path[0, :2], 2 * [0.]]) 52 | initial_state_covariance = np.diag([loc_var, loc_var, vel_var, vel_var]) ** 2 53 | 54 | kf = KalmanFilter(transition_matrices=F, 55 | observation_matrices=H, 56 | observation_covariance=R, 57 | initial_state_mean=initial_state_mean, 58 | initial_state_covariance=initial_state_covariance, 59 | em_vars=['transition_covariance']) 60 | 61 | if n_iter > 0: 62 | logger.debug("Start learning") 63 | kf = kf.em(path[:, :2], n_iter=n_iter) 64 | 65 | state_means, state_vars = kf.smooth(path[:, :2]) 66 | 67 | if use_euclidean: 68 | from .dist_euclidean import distance 69 | distance_f = distance 70 | else: 71 | from .dist_latlon import distance 72 | distance_f = distance 73 | if rm_outliers: 74 | path_ma = np.ma.asarray(path[:, :2]) 75 | for idx in range(path.shape[0]): 76 | d = distance_f(path[idx, :2], state_means[idx, :2]) 77 | if d > obs_noise * 2: 78 | logger.debug("Rm point {}".format(idx)) 79 | path_ma[idx] = np.ma.masked 80 | if rm_outliers == 2: 81 | logger.debug("Retrain") 82 | kf = kf.em(path_ma, n_iter=n_iter) 83 | state_means, state_vars = kf.smooth(path_ma) 84 | 85 | return state_means, state_vars, kf 86 | -------------------------------------------------------------------------------- /leuvenmapmatching/util/openstreetmap.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.util.openstreetmap 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: Wannes Meert 7 | :copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris. 8 | :license: Apache License, Version 2.0, see LICENSE for details. 9 | """ 10 | import logging 11 | from pathlib import Path 12 | import requests 13 | import tempfile 14 | import osmread 15 | 16 | logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching") 17 | 18 | 19 | def locations_to_map(locations, map_con, filename=None): 20 | lats, lons = zip(*locations) 21 | lon_min, lon_max = min(lons), max(lons) 22 | lat_min, lat_max = min(lats), max(lats) 23 | bb = [lon_min, lat_min, lon_max, lat_max] 24 | return bb_to_map(bb, map_con, filename) 25 | 26 | 27 | def bb_to_map(bb, map_con, filename=None): 28 | """Download map from overpass-api.de. 29 | 30 | :param bb: [lon_min, lat_min, lon_max, lat_max] 31 | :param map: 32 | :param filename: 33 | :return: 34 | """ 35 | if filename: 36 | xml_file = Path(filename) 37 | else: 38 | xml_file = Path(tempfile.gettempdir()) / "osm.xml" 39 | if not xml_file.exists(): 40 | bb_str = ",".join(str(coord) for coord in bb) 41 | url = 'http://overpass-api.de/api/map?bbox='+bb_str 42 | logger.debug("Downloading {} from {} ...".format(xml_file, url)) 43 | r = requests.get(url, stream=True) 44 | with xml_file.open('wb') as ofile: 45 | for chunk in r.iter_content(chunk_size=1024): 46 | if chunk: 47 | ofile.write(chunk) 48 | logger.debug("... done") 49 | else: 50 | logger.debug("Reusing existing file: {}".format(xml_file)) 51 | return file_to_map(xml_file, map_con) 52 | 53 | 54 | def file_to_map(filename, map_con): 55 | logger.debug("Parse OSM file ...") 56 | for entity in osmread.parse_file(str(filename)): 57 | if isinstance(entity, osmread.Way) and 'highway' in entity.tags: 58 | for node_a, node_b in zip(entity.nodes, entity.nodes[1:]): 59 | map_con.add_edge(node_a, node_b) 60 | # Some roads are one-way. We'll add both directions. 61 | map_con.add_edge(node_b, node_a) 62 | if isinstance(entity, osmread.Node): 63 | map_con.add_node(entity.id, (entity.lat, entity.lon)) 64 | logger.debug("... done") 65 | logger.debug("Purging database ...") 66 | map_con.purge() 67 | logger.debug("... done") 68 | 69 | 70 | def download_map_xml(fn, bbox, force=False, verbose=False): 71 | """Download map from overpass-api.de based on a given bbox 72 | 73 | :param fn: Filename where to store the map as xml 74 | :param bbox: String or array with [lon_min, lat_min, lon_max, lat_max] 75 | :param force: Also download if file already exists 76 | :param verbose: Verbose output 77 | :return: 78 | """ 79 | fn = Path(fn) 80 | if type(bbox) is list: 81 | bb_str = ",".join(str(coord) for coord in bbox) 82 | elif type(bbox) is str: 83 | bb_str = bbox 84 | else: 85 | raise AttributeError('Unknown type for bbox: {}'.format(type(bbox))) 86 | if force or not fn.exists(): 87 | if verbose: 88 | print("Downloading {}".format(fn)) 89 | import requests 90 | url = f'http://overpass-api.de/api/map?bbox={bb_str}' 91 | r = requests.get(url, stream=True) 92 | with fn.open('wb') as ofile: 93 | for chunk in r.iter_content(chunk_size=1024): 94 | if chunk: 95 | ofile.write(chunk) 96 | else: 97 | if verbose: 98 | print("File already exists") 99 | 100 | 101 | def create_map_from_xml(fn, include_footways=False, include_parking=False, 102 | use_rtree=False, index_edges=False): 103 | """Create an InMemMap from an OpenStreetMap XML file. 104 | 105 | Used for testing routes on OpenStreetMap. 106 | """ 107 | from ..map.inmem import InMemMap 108 | map_con = InMemMap("map", use_latlon=True, use_rtree=use_rtree, index_edges=index_edges) 109 | cnt = 0 110 | ways_filter = ['bridleway', 'bus_guideway', 'track'] 111 | if not include_footways: 112 | ways_filter += ['footway', 'cycleway', 'path'] 113 | parking_filter = ['driveway'] 114 | if not include_parking: 115 | parking_filter += ['parking_aisle'] 116 | for entity in osmread.parse_file(str(fn)): 117 | if isinstance(entity, osmread.Way): 118 | tags = entity.tags 119 | if 'highway' in tags \ 120 | and not (tags['highway'] in ways_filter) \ 121 | and not ('access' in tags and tags['access'] == 'private') \ 122 | and not ('landuse' in tags and tags['landuse'] == 'square') \ 123 | and not ('amenity' in tags and tags['amenity'] == 'parking') \ 124 | and not ('service' in tags and tags['service'] in parking_filter) \ 125 | and not ('area' in tags and tags['area'] == 'yes'): 126 | for node_a, node_b in zip(entity.nodes, entity.nodes[1:]): 127 | map_con.add_edge(node_a, node_b) 128 | # Some roads are one-way. We'll add both directions. 129 | map_con.add_edge(node_b, node_a) 130 | if isinstance(entity, osmread.Node): 131 | map_con.add_node(entity.id, (entity.lat, entity.lon)) 132 | map_con.purge() 133 | return map_con 134 | -------------------------------------------------------------------------------- /leuvenmapmatching/util/projections.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.util.projections 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: Wannes Meert 7 | :copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris. 8 | :license: Apache License, Version 2.0, see LICENSE for details. 9 | """ 10 | import math 11 | import logging 12 | 13 | import pyproj 14 | 15 | logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching") 16 | 17 | 18 | def latlon2equirectangular(lat, lon, phi_er=0, lambda_er=0): 19 | """Naive equirectangular projection. This is the same as considering (lat,lon) == (y,x). 20 | This is a lot faster but only works if you are far enough from the poles and the dateline. 21 | 22 | :param lat: 23 | :param lon: 24 | :param phi_er: The standard parallels (north and south of the equator) where the scale of the projection is true 25 | :param lambda_er: The central meridian of the map 26 | """ 27 | x = (lon - lambda_er) * math.cos(phi_er) 28 | y = lat - phi_er 29 | return y, x 30 | 31 | 32 | def equirectangular2latlon(y, x, phi_er=0, lambda_er=0): 33 | """Naive equirectangular projection. This is the same as considering (lat,lon) == (y,x). 34 | This is a lot faster but only works if you are far enough from the poles and the dateline. 35 | 36 | :param phi_er: The standard parallels (north and south of the equator) where the scale of the projection is true 37 | :param lambda_er: The central meridian of the map 38 | """ 39 | lon = x / math.cos(phi_er) + lambda_er 40 | lat = y + phi_er 41 | return lat, lon 42 | 43 | 44 | def latlon2grs80(coordinates, lon_0=0.0, lat_ts=0.0, y_0=0, x_0=0.0, zone=31, **kwargs): 45 | """Given a list of (lon, lat) coordinates, create x-y coordinates in meter. 46 | 47 | :param coordinates: A list of lon-lat tuples 48 | :param lon_0: Longitude of projection center. 49 | :param lat_ts: Latitude of true scale. Defines the latitude where scale is not distorted. 50 | :param y_0: False northing 51 | :param x_0: False easting 52 | :param zone: UTM zone to use for projection (Defaults to 31) 53 | """ 54 | if zone is None: 55 | # https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system 56 | zone = 31 57 | other_options = " ".join(f"+{key}={val}" for key, val in kwargs.items()) 58 | proj = pyproj.Proj(f"+proj=utm +zone={zone} +ellps=GRS80 +units=m " 59 | f"+lon_0={lon_0} +lat_ts={lat_ts} +y_0={y_0} +x_0={x_0} " 60 | f"+no_defs {other_options}") 61 | for lon, lat in coordinates: 62 | x, y = proj(lon, lat) 63 | yield x, y 64 | -------------------------------------------------------------------------------- /leuvenmapmatching/util/segment.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.util.segment 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: Wannes Meert 7 | :copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris. 8 | :license: Apache License, Version 2.0, see LICENSE for details. 9 | """ 10 | import logging 11 | 12 | 13 | logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching") 14 | 15 | 16 | class Segment(object): 17 | """Segment in the graph and its interpolated point.""" 18 | __slots__ = ["l1", "p1", "l2", "p2", "_pi", "_ti"] 19 | 20 | def __init__(self, l1, p1, l2=None, p2=None, pi=None, ti=None): 21 | """Create a new segment. 22 | 23 | :param l1: Label of the node that is the start of the segment. 24 | :param p1: Point (coordinate) of the start node. 25 | :param l2: Label of the node that is the end of the segment. 26 | :param p2: Point (coordinate) of the end node. 27 | :param pi: Interpolated point. The point that is the best match and 28 | can be in between p1 and p2. 29 | :param ti: Position of pi on the segment [p1,p2], thus pi = p1+t1*(p2-p1). 30 | """ 31 | self.l1 = l1 # Start of segment, label 32 | self.p1 = p1 # point 33 | self.l2 = l2 # End of segment, if None the segment is a point 34 | self.p2 = p2 35 | self.pi = pi # Interpolated point 36 | self.ti = ti # Position on segment p1-p2 37 | 38 | @property 39 | def label(self): 40 | if self.l2 is None: 41 | return self.l1 42 | return f"{self.l1}-{self.l2}" 43 | 44 | @property 45 | def rlabel(self): 46 | if self.l2 is None: 47 | return self.l1 48 | return f"{self.l2}-{self.l1}" 49 | 50 | @property 51 | def key(self): 52 | if self.l2 is None: 53 | return self.l1 54 | return f"{self.l1}-{self.l2}" 55 | 56 | @property 57 | def pi(self): 58 | if self.p2 is None: 59 | return self.p1 60 | return self._pi 61 | 62 | @pi.setter 63 | def pi(self, value): 64 | if value is not None and len(value) > 2: 65 | self._pi = tuple(value[:2]) 66 | else: 67 | self._pi = value 68 | 69 | @property 70 | def ti(self): 71 | if self.p2 is None: 72 | return 0 73 | return self._ti 74 | 75 | @ti.setter 76 | def ti(self, value): 77 | self._ti = value 78 | 79 | def is_point(self): 80 | return self.p2 is None 81 | 82 | def last_point(self): 83 | if self.p2 is None: 84 | return self.p1 85 | return self.p2 86 | 87 | def loc_to_str(self): 88 | if self.p2 is None: 89 | return f"{self.p1}" 90 | if self._pi is not None: 91 | return f"{self.p1}-{self.pi}/{self.ti}-{self.p2}" 92 | return f"{self.p1}-{self.p2}" 93 | 94 | def __str__(self): 95 | if self.p2 is None: 96 | return f"{self.l1}" 97 | if self._pi is not None: 98 | return f"{self.l1}-i-{self.l2}" 99 | return f"{self.l1}-{self.l2}" 100 | 101 | def __repr__(self): 102 | return "Segment<" + self.__str__() + ">" 103 | -------------------------------------------------------------------------------- /leuvenmapmatching/visualization.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | leuvenmapmatching.visualization 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: Wannes Meert 7 | :copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris. 8 | :license: Apache License, Version 2.0, see LICENSE for details. 9 | """ 10 | import math 11 | import random 12 | import logging 13 | from itertools import islice 14 | 15 | import numpy as np 16 | import matplotlib.pyplot as plt 17 | from matplotlib import colors as mcolors 18 | import smopy 19 | 20 | 21 | logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching") 22 | graph_color = mcolors.CSS4_COLORS['darkmagenta'] 23 | match_color = mcolors.CSS4_COLORS['green'] 24 | match_ne_color = mcolors.CSS4_COLORS['olive'] 25 | lattice_color = mcolors.CSS4_COLORS['magenta'] 26 | nodes_color = mcolors.CSS4_COLORS['cyan'] 27 | path_color = mcolors.CSS4_COLORS['blue'] 28 | fontsize = 11 # 7 # 11 29 | 30 | 31 | def plot_map(map_con, path=None, nodes=None, counts=None, ax=None, use_osm=False, z=None, bb=None, 32 | show_labels=False, matcher=None, show_graph=False, zoom_path=False, show_lattice=False, 33 | show_matching=False, filename=None, linewidth=2, coord_trans=None, 34 | figwidth=20, lattice_nodes=None): 35 | """Plot the db/graph and optionally include the observed path and inferred nodes. 36 | 37 | :param map_con: Map 38 | :param path: list[(lat, lon)] 39 | :param nodes: list[str] 40 | :param counts: Number of visits of a node in the lattice. dict[label, int] 41 | :param ax: Matplotlib axis 42 | :param use_osm: Use OpenStreetMap layer, the points should be latitude-longitude pairs. 43 | :param matcher: Matcher object (overrules given path, nodes and counts) 44 | :param filename: File to write image to 45 | :param show_graph: Plot the vertices and edges in the graph 46 | :return: None 47 | """ 48 | if matcher is not None: 49 | path = matcher.path 50 | counts = matcher.node_counts() 51 | nodes = None 52 | if lattice_nodes is None: 53 | lat_nodes = matcher.lattice_best 54 | else: 55 | lat_nodes = lattice_nodes 56 | if lat_nodes is None: 57 | lat_nodes = [] 58 | else: 59 | lat_nodes = [] 60 | 61 | if not bb: 62 | bb = map_con.bb() 63 | lat_min, lon_min, lat_max, lon_max = bb 64 | if path: 65 | plat, plon = islice(zip(*path), 2) 66 | lat_min, lat_max = min(lat_min, min(plat)), max(lat_max, max(plat)) 67 | lon_min, lon_max = min(lon_min, min(plon)), max(lon_max, max(plon)) 68 | bb = [lat_min, lon_min, lat_max, lon_max] 69 | logger.debug("bb = [{}, {}, {}, {}]".format(*bb)) 70 | 71 | if zoom_path and path: 72 | if type(zoom_path) is slice: 73 | plat, plon = islice(zip(*path[zoom_path]), 2) 74 | lat_min, lat_max = min(plat), max(plat) 75 | lon_min, lon_max = min(plon), max(plon) 76 | else: 77 | plat, plon = islice(zip(*path), 2) 78 | lat_min, lat_max = min(plat), max(plat) 79 | lon_min, lon_max = min(plon), max(plon) 80 | lat_d = lat_max - lat_min 81 | lon_d = lon_max - lon_min 82 | latlon_d = max(lat_d, lon_d) 83 | lat_max += max(latlon_d * 0.01, lat_d * 0.2) 84 | lon_min -= max(latlon_d * 0.01, lon_d * 0.2) 85 | lat_min -= max(latlon_d * 0.01, lat_d * 0.2) 86 | lon_max += max(latlon_d * 0.01, lon_d * 0.2) 87 | logger.debug("Setting bounding box to path") 88 | bb = [lat_min, lon_min, lat_max, lon_max] 89 | logger.debug("bb(zoom-path) = [{}, {}, {}, {}]".format(*bb)) 90 | 91 | bb_o = bb 92 | if coord_trans: 93 | logger.debug("Converting bounding box coordinates") 94 | if path: 95 | path = [coord_trans(lat, lon) for lat, lon in path] 96 | lat_min, lon_min, lat_max, lon_max = bb 97 | lat_min, lon_min = coord_trans(lat_min, lon_min) 98 | lat_max, lon_max = coord_trans(lat_max, lon_max) 99 | bb = [lat_min, lon_min, lat_max, lon_max] 100 | logger.debug("bb = [{}, {}, {}, {}]".format(*bb)) 101 | 102 | if use_osm: 103 | from .util import dist_latlon 104 | project = dist_latlon.project 105 | if z is None: 106 | z = 18 107 | m = smopy.Map(bb, z=z, ax=ax) 108 | to_pixels = m.to_pixels 109 | x_max, y_max = to_pixels(lat_max, lon_max) 110 | x_min, y_min = to_pixels(lat_min, lon_min) 111 | height = figwidth / abs(x_max - x_min) * abs(y_max - y_min) 112 | if ax is None: 113 | ax = m.show_mpl(figsize=(figwidth, height)) 114 | else: 115 | ax = m.show_mpl(ax=ax) 116 | fig = None 117 | 118 | else: 119 | from .util import dist_euclidean 120 | project = dist_euclidean.project 121 | 122 | def to_pixels(lat, lon=None): 123 | if lon is None: 124 | lat, lon = lat[0], lat[1] 125 | return lon, lat 126 | x_max, y_max = to_pixels(lat_max, lon_max) 127 | x_min, y_min = to_pixels(lat_min, lon_min) 128 | height = figwidth / abs(lon_max - lon_min) * abs(lat_max - lat_min) 129 | if ax is None: 130 | fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(figwidth, height)) 131 | else: 132 | fig = None 133 | ax.set_xlim([x_min, x_max]) 134 | ax.set_ylim([y_min, y_max]) 135 | 136 | # if counts is None: 137 | # node_sizes = [10] * map_con.size() 138 | # else: 139 | # node_sizes = [counts[label]*100+5 for label in map_con.labels()] 140 | 141 | if show_graph: 142 | logger.debug('Plot vertices ...') 143 | cnt = 0 144 | for key, coord in map_con.all_nodes(bb=bb_o): 145 | if coord_trans: 146 | coord = coord_trans(*coord) 147 | coord = to_pixels(coord) 148 | plt.plot(coord[0], coord[1], marker='o', markersize=2*linewidth, color=graph_color, alpha=0.75) 149 | if show_labels: 150 | key = str(key) 151 | if type(show_labels) is int: 152 | key = key[-show_labels:] 153 | xytext = ax.transLimits.transform(coord) 154 | xytext = (xytext[0]+0.001, xytext[1]+0.0) 155 | xytext = ax.transLimits.inverted().transform(xytext) 156 | # key = str(key)[-3:] 157 | # print(f'annotate: {key} {coord} {xytext}') 158 | ann = ax.annotate(key, xy=coord, xytext=xytext, 159 | # textcoords=('axes fraction', 'axes fraction'), 160 | # arrowprops=dict(arrowstyle='->'), 161 | color=graph_color, fontsize=fontsize) 162 | # ann.set_rotation(45) 163 | cnt += 1 164 | logger.debug(f'... done, {cnt} nodes') 165 | 166 | logger.debug('Plot lines ...') 167 | cnt = 0 168 | for row in map_con.all_edges(bb=bb_o): 169 | loc_a = row[1] 170 | loc_b = row[3] 171 | if coord_trans: 172 | loc_a = coord_trans(*loc_a) 173 | loc_b = coord_trans(*loc_b) 174 | x_a, y_a = to_pixels(*loc_a) 175 | x_b, y_b = to_pixels(*loc_b) 176 | ax.plot([x_a, x_b], [y_a, y_b], color=graph_color, linewidth=linewidth, markersize=linewidth) 177 | cnt += 1 178 | logger.debug(f'... done, {cnt} edges') 179 | 180 | if show_lattice: 181 | if matcher is None: 182 | logger.warning("Matcher needs to be passed to show lattice. Not showing lattice.") 183 | plot_lattice(ax, to_pixels, matcher) 184 | 185 | if path: 186 | logger.debug('Plot path ...') 187 | if type(zoom_path) is slice: 188 | path_startidx = zoom_path.start 189 | path_slice = path[zoom_path] 190 | else: 191 | path_startidx = 0 192 | path_slice = path 193 | px, py = zip(*[to_pixels(p[:2]) for p in path_slice]) 194 | ax.plot(px, py, linewidth=linewidth, markersize=linewidth * 2, alpha=0.75, 195 | linestyle="--", marker='o', color=path_color) 196 | if show_labels: 197 | for li, (lx, ly) in enumerate(zip(px, py)): 198 | # ax.text(lx, ly, f"O{li}", color=path_color) 199 | ann = ax.annotate(f"O{path_startidx + li}", xy=(lx, ly), color=path_color, fontsize=fontsize) 200 | ann.set_rotation(45) 201 | 202 | if nodes or matcher: 203 | logger.debug('Plot nodes ...') 204 | xs, ys, ls = [], [], [] 205 | prev = None 206 | node_locs = [] 207 | if nodes: 208 | for node in nodes: 209 | if type(node) == tuple: 210 | node = node[0] 211 | lat, lon = map_con.node_coordinates(node) 212 | node_locs.append((lat, lon, node)) 213 | elif lat_nodes is not None: 214 | prev_m = None 215 | for m in lat_nodes: 216 | if prev_m is not None and prev_m.edge_m.l2 == m.edge_m.l1 \ 217 | and prev_m.edge_m.l1 != m.edge_m.l2: 218 | lat, lon = m.edge_m.p1 219 | node_locs.append((lat, lon, m.edge_m.l1)) 220 | lat, lon = m.edge_m.pi 221 | node_locs.append((lat, lon, m.edge_m.label)) 222 | prev_m = m 223 | for lat, lon, label in node_locs: 224 | if coord_trans: 225 | lat, lon = coord_trans(lat, lon) 226 | if bb[0] <= lat <= bb[2] and bb[1] <= lon <= bb[3]: 227 | if prev is not None: 228 | x, y = to_pixels(*prev) 229 | xs.append(x) 230 | ys.append(y) 231 | ls.append(label) 232 | prev = None 233 | x, y = to_pixels(lat, lon) 234 | xs.append(x) 235 | ys.append(y) 236 | ls.append(label) 237 | else: 238 | if prev is None: 239 | x, y = to_pixels(lat, lon) 240 | xs.append(x) 241 | ys.append(y) 242 | ls.append(label) 243 | prev = lat, lon 244 | ax.plot(xs, ys, 'o-', linewidth=linewidth * 3, markersize=linewidth * 3, alpha=0.75, 245 | color=nodes_color) 246 | # if show_labels: 247 | # for label, lx, ly in zip(ls, xs, ys): 248 | # ax.annotate(label, xy=(lx, ly), xytext=(lx - 30, ly), color=nodes_color) 249 | 250 | if matcher and show_matching: 251 | logger.debug('Plot matching path-nodes (using matcher) ...') 252 | for idx, m in enumerate(lat_nodes): 253 | lat, lon = m.edge_m.pi[:2] 254 | lat2, lon2 = m.edge_o.pi[:2] 255 | if coord_trans: 256 | lat, lon = coord_trans(lat, lon) 257 | lat2, lon2 = coord_trans(lat2, lon2) 258 | x, y = to_pixels(lat, lon) 259 | x2, y2 = to_pixels(lat2, lon2) 260 | if m.edge_o.is_point(): 261 | plt.plot(x, y, marker='x', markersize=2 * linewidth, color=match_color, alpha=0.75) 262 | plt.plot(x2, y2, marker='+', markersize=2 * linewidth, color=match_color, alpha=0.75) 263 | ax.plot((x, x2), (y, y2), '-', color=match_color, linewidth=linewidth, alpha=0.75) 264 | else: 265 | plt.plot(x, y, marker='x', markersize=2 * linewidth, color=match_ne_color, alpha=0.75) 266 | plt.plot(x2, y2, marker='+', markersize=2 * linewidth, color=match_ne_color, alpha=0.75) 267 | ax.plot((x, x2), (y, y2), '-', color=match_ne_color, linewidth=linewidth, alpha=0.75) 268 | # ax.plot((x, x2), (y, y2), '-', color=match_color, linewidth=10, alpha=0.1) 269 | # if show_labels: 270 | # ax.annotate(f"{m.obs}.{m.obs_ne}", xy=(x, y)) 271 | elif path and nodes and len(path) == len(nodes) and show_matching: 272 | logger.debug('Plot matching path-nodes (using sequence of nodes) ...') 273 | for idx, (loc, node) in enumerate(zip(path, nodes)): 274 | x, y = to_pixels(*loc) 275 | if type(node) == tuple and (len(node) == 4 or len(node) == 2): 276 | latlon2, latlon3 = map_con.node_coordinates(node[0]), map_con.node_coordinates(node[1]) 277 | if coord_trans: 278 | latlon2 = coord_trans(*latlon2) 279 | latlon3 = coord_trans(*latlon3) 280 | latlon4, _ = project(latlon2, latlon3, loc) 281 | x4, y4 = to_pixels(*latlon4) 282 | ax.plot((x, x4), (y, y4), '-', color=match_color, linewidth=linewidth, alpha=0.75) 283 | elif type(node) == tuple and len(node) == 3: 284 | lat2, lon2 = map_con.node_coordinates(node[0]) 285 | if coord_trans: 286 | lat2, lon2 = coord_trans(lat2, lon2) 287 | x2, y2 = to_pixels(lat2, lon2) 288 | ax.plot((x, x2), (y, y2), '-', color=match_color, linewidth=linewidth, alpha=0.75) 289 | elif type(node) == str or type(node) == int: 290 | lat2, lon2 = map_con.node_coordinates(node[0]) 291 | if coord_trans: 292 | lat2, lon2 = coord_trans(lat2, lon2) 293 | x2, y2 = to_pixels(lat2, lon2) 294 | ax.plot((x, x2), (y, y2), '-', color=match_color, linewidth=linewidth, alpha=0.75) 295 | else: 296 | raise Exception('Unknown node type: {} ({})'.format(node, type(node))) 297 | # if show_labels: 298 | # ax.annotate(str(idx), xy=(x, y)) 299 | if map_con.use_latlon: 300 | ax.set_xlabel('Longitude') 301 | ax.set_ylabel('Latitude') 302 | else: 303 | ax.set_xlabel('X') 304 | ax.set_ylabel('Y') 305 | ax.axis('equal') 306 | ax.set_aspect('equal') 307 | if filename is not None: 308 | plt.savefig(filename) 309 | if fig is not None: 310 | plt.close(fig) 311 | fig = None 312 | ax = None 313 | return fig, ax 314 | 315 | 316 | def plot_lattice(ax, to_pixels, matcher): 317 | for idx in range(len(matcher.lattice)): 318 | if len(matcher.lattice[idx]) == 0: 319 | continue 320 | for m in matcher.lattice[idx].values_all(): 321 | for mp in m.prev: 322 | if m.stop: 323 | alpha = 0.1 324 | linewidth = 1 325 | else: 326 | alpha = 0.3 327 | linewidth = 3 328 | if mp.edge_m.p2 is None: 329 | prv = mp.edge_m.p1 330 | else: 331 | prv = mp.edge_m.p2 332 | nxt = m.edge_m.p1 333 | x1, y1 = to_pixels(*prv) 334 | x2, y2 = to_pixels(*nxt) 335 | ax.plot((x1, x2), (y1, y2), '.-', color=lattice_color, linewidth=linewidth, alpha=alpha) 336 | if m.edge_m.p2 is not None: 337 | x1, y1 = to_pixels(*m.edge_m.p1) 338 | x2, y2 = to_pixels(*m.edge_m.p2) 339 | ax.plot((x1, x2), (y1, y2), '.-', color=lattice_color, linewidth=linewidth, alpha=alpha) 340 | 341 | 342 | def plot_obs_noise_dist(obs_fn, obs_noise, min_dist=0, max_dist=10): 343 | """Plot the expected noise of an observation distribution. 344 | 345 | :param matcher: Matcher 346 | :return: 347 | """ 348 | x = np.linspace(min_dist, max_dist, 100) 349 | y = [obs_fn(xi) for xi in x] 350 | plt.plot(x, y) 351 | plt.xlabel("Distance") 352 | plt.ylabel("Probability") 353 | plt.xlim((min_dist, max_dist)) 354 | plt.ylim((0, 1)) 355 | plt.axvline(x=obs_noise, color='red', alpha=0.7) 356 | plt.annotate("Observation noise stddev", xy=(obs_noise, 0)) 357 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = leuvenmapmatching 3 | version = attr: leuvenmapmatching.__version__ 4 | author = Wannes Meert 5 | description = Match a trace of GPS positions to a locations and streets on a map 6 | license = Apache 2.0 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = https://github.com/wannesm/LeuvenMapMatching 10 | project_urls = 11 | Bug Tracker = https://github.com/wannesm/LeuvenMapMatching/issues 12 | classifiers = 13 | Programming Language :: Python :: 3 14 | License :: OSI Approved :: Apache Software License 15 | Operating System :: OS Independent 16 | keywords = map, matching 17 | 18 | [options] 19 | packages = find: 20 | python_requires = >=3.6 21 | install_requires = 22 | numpy 23 | scipy 24 | tests_requires = 25 | pytest-runner 26 | pytest 27 | 28 | [options.extras_require] 29 | vis = smopy; matplotlib>=2.0.0 30 | db = rtree; pyproj 31 | all = requests; smopy; matplotlib>=2.0.0; rtree; pyproj; nvector==0.5.2; gpxpy; pykalman; pytest; pytest-runner; osmread; osmnx 32 | # In case of problems with osmread, use: "osmread @ git+https://github.com/dezhin/osmread" 33 | 34 | [aliases] 35 | test=pytest 36 | 37 | [tool:pytest] 38 | norecursedirs = .git venv* .eggs 39 | addopts = --verbose 40 | python_files = tests/*.py tests/*/*.py 41 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf-8 3 | """ 4 | setup.py 5 | ~~~~~~~~ 6 | 7 | :author: Wannes Meert 8 | :copyright: Copyright 2017-2021 DTAI, KU Leuven and Sirris. 9 | :license: Apache License, Version 2.0, see LICENSE for details. 10 | """ 11 | from setuptools import setup, find_packages 12 | import re 13 | import os 14 | 15 | here = os.path.abspath(os.path.dirname(__file__)) 16 | 17 | 18 | with open(os.path.join('leuvenmapmatching', '__init__.py'), 'r') as fd: 19 | version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', 20 | fd.read(), re.MULTILINE).group(1) 21 | if not version: 22 | raise RuntimeError('Cannot find version information') 23 | 24 | 25 | setup( 26 | name='leuvenmapmatching', 27 | version=version, 28 | packages=find_packages(), 29 | author='Wannes Meert', 30 | author_email='wannes.meert@cs.kuleuven.be', 31 | url='https://github.com/wannesm/LeuvenMapMatching', 32 | description='Match a trace of GPS positions to a locations and streets on a map', 33 | python_requires='>=3.6', 34 | license='Apache 2.0', 35 | classifiers=[ 36 | 'Intended Audience :: Developers', 37 | 'License :: OSI Approved :: Apache Software License', 38 | 'Programming Language :: Python :: 3' 39 | ], 40 | keywords='map matching', 41 | ) 42 | -------------------------------------------------------------------------------- /tests/examples/example_1_simple.py: -------------------------------------------------------------------------------- 1 | from leuvenmapmatching.matcher.distance import DistanceMatcher 2 | from leuvenmapmatching.map.inmem import InMemMap 3 | 4 | map_con = InMemMap("mymap", graph={ 5 | "A": ((1, 1), ["B", "C", "X"]), 6 | "B": ((1, 3), ["A", "C", "D", "K"]), 7 | "C": ((2, 2), ["A", "B", "D", "E", "X", "Y"]), 8 | "D": ((2, 4), ["B", "C", "F", "E", "K", "L"]), 9 | "E": ((3, 3), ["C", "D", "F", "Y"]), 10 | "F": ((3, 5), ["D", "E", "L"]), 11 | "X": ((2, 0), ["A", "C", "Y"]), 12 | "Y": ((3, 1), ["X", "C", "E"]), 13 | "K": ((1, 5), ["B", "D", "L"]), 14 | "L": ((2, 6), ["K", "D", "F"]) 15 | }, use_latlon=False) 16 | 17 | path = [(0.8, 0.7), (0.9, 0.7), (1.1, 1.0), (1.2, 1.5), (1.2, 1.6), (1.1, 2.0), 18 | (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7), 19 | (2.3, 3.5), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2), 20 | (3.1, 3.8), (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)] 21 | 22 | matcher = DistanceMatcher(map_con, max_dist=2, obs_noise=1, min_prob_norm=0.5, max_lattice_width=5) 23 | states, _ = matcher.match(path) 24 | nodes = matcher.path_pred_onlynodes 25 | 26 | print("States\n------") 27 | print(states) 28 | print("Nodes\n------") 29 | print(nodes) 30 | print("") 31 | matcher.print_lattice_stats() 32 | -------------------------------------------------------------------------------- /tests/examples/example_using_osmnx_and_geopandas.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | from pathlib import Path 5 | 6 | this_path = Path(os.path.realpath(__file__)).parent.parent / "rsrc" / "path_latlon" 7 | assert(this_path.exists()) 8 | path_to_mytrack_gpx = this_path / "route.gpx" 9 | assert(path_to_mytrack_gpx.exists()) 10 | 11 | import leuvenmapmatching as mm 12 | from leuvenmapmatching.map.inmem import InMemMap 13 | 14 | 15 | def run(): 16 | # Start example 17 | import osmnx as ox 18 | 19 | # Select map (all, drive, walk, ...) 20 | graph = ox.graph_from_place('Leuven, Belgium', network_type='all', simplify=False) 21 | graph_proj = ox.project_graph(graph) 22 | 23 | # Create GeoDataFrames 24 | # Approach 1: translate map to graph 25 | # DistanceMatcher uses edges, thus build index based on edges 26 | map_con = InMemMap("myosm", use_latlon=True, use_rtree=True, index_edges=True) 27 | nodes_proj, edges_proj = ox.graph_to_gdfs(graph_proj, nodes=True, edges=True) 28 | for nid, row in nodes_proj[['x', 'y']].iterrows(): 29 | map_con.add_node(nid, (row['x'], row['y'])) 30 | for eid, _ in edges_proj.iterrows(): 31 | map_con.add_edge(eid[0], eid[1]) 32 | 33 | # Approach 2: use a specific projection 34 | map_con = InMemMap("myosm", use_latlon=True, use_rtree=True, index_edges=True) 35 | nodes_proj, edges_proj = ox.graph_to_gdfs(graph_proj, nodes=True, edges=True) 36 | nodes_proj = nodes_proj.to_crs("EPSG:3395") 37 | # edges_proj = edges_proj.to_crs("EPSG:3395") 38 | for nid, row in nodes_proj.iterrows(): 39 | map_con.add_node(nid, (row['lat'], row['lon'])) 40 | # We can also extract edges also directly from networkx graph 41 | for nid1, nid2, _ in graph.edges: 42 | map_con.add_edge(nid1, nid2) 43 | 44 | # Perform matching 45 | from leuvenmapmatching.util.gpx import gpx_to_path 46 | from leuvenmapmatching.matcher.distance import DistanceMatcher 47 | 48 | track = gpx_to_path(path_to_mytrack_gpx) 49 | matcher = DistanceMatcher(map_con, 50 | max_dist=100, max_dist_init=50, # meter 51 | non_emitting_length_factor=0.75, 52 | obs_noise=50, obs_noise_ne=75, # meter 53 | dist_noise=50, # meter 54 | non_emitting_states=True, 55 | max_lattice_width=5) 56 | states, lastidx = matcher.match(track) 57 | print(states) 58 | 59 | # End example 60 | 61 | # import leuvenmapmatching.visualization as mm_viz 62 | # import matplotlib as mpl 63 | # mpl.use('MacOSX') 64 | # mm_viz.plot_map(map_con, matcher=matcher, use_osm=True, 65 | # zoom_path=True, show_graph=True, 66 | # filename=Path(os.environ.get('TESTDIR', Path(__file__).parent)) / "example.png") 67 | 68 | 69 | if __name__ == "__main__": 70 | mm.logger.setLevel(logging.INFO) 71 | mm.logger.addHandler(logging.StreamHandler(sys.stdout)) 72 | run() 73 | -------------------------------------------------------------------------------- /tests/rsrc/bug2/readme.md: -------------------------------------------------------------------------------- 1 | Test data 2 | ========= 3 | 4 | Download from https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/leuvenmapmatching_testdata.zip 5 | 6 | - `edgesrl.csv` 7 | - `nodesrl.csv` 8 | - `path.csv` 9 | 10 | -------------------------------------------------------------------------------- /tests/rsrc/newson_krumm_2009/readme.md: -------------------------------------------------------------------------------- 1 | Newson Krum testdata 2 | ==================== 3 | 4 | Files will be downloaded from https://www.microsoft.com/en-us/research/publication/hidden-markov-map-matching-noise-sparseness/ 5 | 6 | - `gps_data.txt` 7 | - `road_network.txt` 8 | - `ground_truth_route.txt` 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/rsrc/path_latlon/readme.md: -------------------------------------------------------------------------------- 1 | Test data for path_latlon 2 | ========================= 3 | 4 | Download from https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/leuvenmapmatching_testdata2.zip 5 | 6 | - `route.gpx` 7 | - `route2.gpx` 8 | - `osm_downloaded.xml` 9 | - `osm_downloaded2.xml` 10 | 11 | -------------------------------------------------------------------------------- /tests/rsrc/path_latlon/route.gpx: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | START 13 | 14 | 15 | 16 | Leuven Stadswandeling 5 km TR 17 | 18 | 19 | 1 20 | 21 | 22 | 2 23 | 24 | 25 | 3 26 | 27 | 28 | 4 29 | 30 | 31 | 5 32 | 33 | 34 | 6 35 | 36 | 37 | 7 38 | 39 | 40 | 8 41 | 42 | 43 | 9 44 | 45 | 46 | 10 47 | 48 | 49 | 11 50 | 51 | 52 | 12 53 | 54 | 55 | 13 56 | 57 | 58 | 14 59 | 60 | 61 | 15 62 | 63 | 64 | 16 65 | 66 | 67 | 17 68 | 69 | 70 | 18 71 | 72 | 73 | 19 74 | 75 | 76 | 20 77 | 78 | 79 | 21 80 | 81 | 82 | 22 83 | 84 | 85 | 23 86 | 87 | 88 | 24 89 | 90 | 91 | 25 92 | 93 | 94 | 26 95 | 96 | 97 | 27 98 | 99 | 100 | 28 101 | 102 | 103 | 29 104 | 105 | 106 | 30 107 | 108 | 109 | 31 110 | 111 | 112 | 32 113 | 114 | 115 | 33 116 | 117 | 118 | 34 119 | 120 | 121 | 35 122 | 123 | 124 | 36 125 | 126 | 127 | 37 128 | 129 | 130 | 38 131 | 132 | 133 | 39 134 | 135 | 136 | 40 137 | 138 | 139 | 41 140 | 141 | 142 | 42 143 | 144 | 145 | 43 146 | 147 | 148 | 44 149 | 150 | 151 | 45 152 | 153 | 154 | 46 155 | 156 | 157 | 47 158 | 159 | 160 | 48 161 | 162 | 163 | 49 164 | 165 | 166 | 50 167 | 168 | 169 | 51 170 | 171 | 172 | 52 173 | 174 | 175 | 53 176 | 177 | 178 | 54 179 | 180 | 181 | 55 182 | 183 | 184 | 56 185 | 186 | 187 | 57 188 | 189 | 190 | 58 191 | 192 | 193 | 59 194 | 195 | 196 | 60 197 | 198 | 199 | 61 200 | 201 | 202 | 62 203 | 204 | 205 | 63 206 | 207 | 208 | 64 209 | 210 | 211 | 65 212 | 213 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /tests/rsrc/path_latlon/route2.gpx: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | START 13 | 14 | 15 | 16 | Test route 17 | 18 | 19 | 1 20 | 21 | 22 | 2 23 | 24 | 25 | 3 26 | 27 | 28 | 4 29 | 30 | 31 | 5 32 | 33 | 34 | 6 35 | 36 | 37 | 7 38 | 39 | 40 | 8 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/test_bugs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf-8 3 | """ 4 | tests.test_bugs 5 | ~~~~~~~~~~~~~~~ 6 | 7 | :author: Wannes Meert 8 | :copyright: Copyright 2018 DTAI, KU Leuven and Sirris. 9 | :license: Apache License, Version 2.0, see LICENSE for details. 10 | """ 11 | import os 12 | import sys 13 | import logging 14 | from pathlib import Path 15 | import csv 16 | 17 | import leuvenmapmatching as mm 18 | from leuvenmapmatching.map.inmem import InMemMap 19 | from leuvenmapmatching.map.sqlite import SqliteMap 20 | from leuvenmapmatching.matcher.simple import SimpleMatcher 21 | from leuvenmapmatching.matcher.distance import DistanceMatcher 22 | import leuvenmapmatching.visualization as mm_viz 23 | MYPY = False 24 | if MYPY: 25 | from typing import List, Tuple 26 | 27 | 28 | logger = mm.logger 29 | directory = None 30 | 31 | 32 | def test_bug1(): 33 | dist = 10 34 | nb_steps = 20 35 | 36 | map_con = InMemMap("map", graph={ 37 | "A": ((1, dist), ["B"]), 38 | "B": ((2, dist), ["A", "C", "CC"]), 39 | "C": ((3, 0), ["B", "D"]), 40 | "D": ((4 + dist, 0), ["C", "E"]), 41 | "CC": ((3, 2 * dist), ["B", "DD"]), 42 | "DD": ((4 + dist, 2 * dist), ["CC", "E"]), 43 | "E": ((5 + dist, dist), ["F", "D", "DD"]), 44 | "F": ((6 + dist, dist), ["E", ]), 45 | 46 | }, use_latlon=False) 47 | 48 | i = 10 49 | path = [(1.1, 2*dist*i/nb_steps), 50 | (2.1, 2*dist*i/nb_steps), 51 | (5.1+dist, 2*dist*i/nb_steps), 52 | (6.1+dist, 2*dist*i/nb_steps) 53 | # (1, len*i/nb_steps), 54 | # (2, len*i/nb_steps), 55 | # (3, len*i/nb_steps) 56 | ] 57 | 58 | matcher = SimpleMatcher(map_con, max_dist=dist + 1, obs_noise=dist + 1, min_prob_norm=None, 59 | non_emitting_states=True) 60 | 61 | nodes = matcher.match(path, unique=False) 62 | print("Solution: ", nodes) 63 | if directory: 64 | import leuvenmapmatching.visualization as mm_vis 65 | matcher.print_lattice() 66 | matcher.print_lattice_stats() 67 | mm_vis.plot_map(map_con, path=path, nodes=nodes, counts=matcher.node_counts(), 68 | show_labels=True, filename=str(directory / "test_bugs_1.png")) 69 | 70 | 71 | def test_bug2(): 72 | this_path = Path(os.path.realpath(__file__)).parent / "rsrc" / "bug2" 73 | edges_fn = this_path / "edgesrl.csv" 74 | nodes_fn = this_path / "nodesrl.csv" 75 | path_fn = this_path / "path.csv" 76 | zip_fn = this_path / "leuvenmapmatching_testdata.zip" 77 | 78 | if not (edges_fn.exists() and nodes_fn.exists() and path_fn.exists()): 79 | import requests 80 | url = 'https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/leuvenmapmatching_testdata.zip' 81 | logger.debug("Download testfiles from kuleuven.be") 82 | r = requests.get(url, stream=True) 83 | with zip_fn.open('wb') as ofile: 84 | for chunk in r.iter_content(chunk_size=1024): 85 | if chunk: 86 | ofile.write(chunk) 87 | import zipfile 88 | logger.debug("Unzipping leuvenmapmatching_testdata.zip") 89 | with zipfile.ZipFile(str(zip_fn), "r") as zip_ref: 90 | zip_ref.extractall(str(zip_fn.parent)) 91 | 92 | logger.debug(f"Reading map ...") 93 | mmap = SqliteMap("road_network", use_latlon=True, dir=this_path) 94 | 95 | path = [] 96 | with path_fn.open("r") as path_f: 97 | reader = csv.reader(path_f, delimiter=',') 98 | for row in reader: 99 | lat, lon = [float(coord) for coord in row] 100 | path.append((lat, lon)) 101 | node_cnt = 0 102 | with nodes_fn.open("r") as nodes_f: 103 | reader = csv.reader(nodes_f, delimiter=',') 104 | for row in reader: 105 | nid, lonlat, _ = row 106 | nid = int(nid) 107 | lon, lat = [float(coord) for coord in lonlat[1:-1].split(",")] 108 | mmap.add_node(nid, (lat, lon), ignore_doubles=True, no_index=True, no_commit=True) 109 | node_cnt += 1 110 | edge_cnt = 0 111 | with edges_fn.open("r") as edges_f: 112 | reader = csv.reader(edges_f, delimiter=',') 113 | for row in reader: 114 | _eid, nid1, nid2, pid = [int(val) for val in row] 115 | mmap.add_edge(nid1, nid2, edge_type=0, path=pid, no_index=True, no_commit=True) 116 | edge_cnt += 1 117 | logger.debug(f"... done: {node_cnt} nodes and {edge_cnt} edges") 118 | logger.debug("Indexing ...") 119 | mmap.reindex_nodes() 120 | mmap.reindex_edges() 121 | logger.debug("... done") 122 | 123 | matcher = DistanceMatcher(mmap, min_prob_norm=0.001, 124 | max_dist=200, obs_noise=4.07, 125 | non_emitting_states=True) 126 | # path = path[:2] 127 | nodes, idx = matcher.match(path, unique=True) 128 | path_pred = matcher.path_pred 129 | if directory: 130 | import matplotlib.pyplot as plt 131 | matcher.print_lattice_stats() 132 | logger.debug("Plotting post map ...") 133 | fig = plt.figure(figsize=(100, 100)) 134 | ax = fig.get_axes() 135 | mm_viz.plot_map(mmap, matcher=matcher, use_osm=True, ax=ax, 136 | show_lattice=False, show_labels=True, show_graph=False, zoom_path=True, 137 | show_matching=True) 138 | plt.savefig(str(directory / "test_bug1.png")) 139 | plt.close(fig) 140 | logger.debug("... done") 141 | 142 | 143 | if __name__ == "__main__": 144 | mm.logger.setLevel(logging.DEBUG) 145 | mm.logger.addHandler(logging.StreamHandler(sys.stdout)) 146 | directory = Path(os.environ.get('TESTDIR', Path(__file__).parent)) 147 | print(f"Saving files to {directory}") 148 | # test_bug1() 149 | test_bug2() 150 | -------------------------------------------------------------------------------- /tests/test_conversion.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf-8 3 | import sys 4 | import logging 5 | from datetime import datetime 6 | import pytest 7 | import math 8 | import os 9 | from pathlib import Path 10 | import itertools 11 | 12 | import leuvenmapmatching as mm 13 | from leuvenmapmatching.util import dist_euclidean as de 14 | from leuvenmapmatching.util import dist_latlon as dll 15 | 16 | 17 | directory = None 18 | 19 | 20 | def test_path_to_gpx(): 21 | from leuvenmapmatching.util.gpx import path_to_gpx 22 | path = [(i, i, datetime.fromtimestamp(i)) for i in range(0, 10)] 23 | gpx = path_to_gpx(path) 24 | 25 | assert len(path) == len(gpx.tracks[0].segments[0].points) 26 | assert path[0][0] == pytest.approx(gpx.tracks[0].segments[0].points[0].latitude) 27 | assert path[0][1] == pytest.approx(gpx.tracks[0].segments[0].points[0].longitude) 28 | assert path[0][2] == gpx.tracks[0].segments[0].points[0].time 29 | 30 | 31 | def test_grs80(): 32 | from leuvenmapmatching.util.projections import latlon2grs80 33 | coordinates = [(4.67878, 50.864), (4.68054, 50.86381), (4.68098, 50.86332), (4.68129, 50.86303), (4.6817, 50.86284), 34 | (4.68277, 50.86371), (4.68894, 50.86895), (4.69344, 50.86987), (4.69354, 50.86992), 35 | (4.69427, 50.87157), (4.69643, 50.87315), (4.69768, 50.87552), (4.6997, 50.87828)] 36 | points = latlon2grs80(coordinates, lon_0=coordinates[0][0], lat_ts=coordinates[0][1]) 37 | points = list(points) 38 | point = points[0] 39 | assert point[0] == pytest.approx(618139.9385518166) 40 | assert point[1] == pytest.approx(5636043.991970774) 41 | 42 | 43 | def test_distance1(): 44 | p1 = (38.898556, -77.037852) 45 | p2 = (38.897147, -77.043934) 46 | d = dll.distance(p1, p2) 47 | assert d == pytest.approx(549.1557912048178), f"Got: {d}" 48 | 49 | 50 | def test_distance2(): 51 | o_p1 = (6007539.987516373, -13607675.997610645) 52 | m_p1 = (6007518.475594072, -13607641.049711559) 53 | m_p2 = (6007576.295597112, -13607713.306589901) 54 | dist, proj_m, t_m = de.distance_point_to_segment(o_p1, m_p1, m_p2) 55 | assert dist == pytest.approx(5.038773480896327), f"dist = {dist}" 56 | assert t_m == pytest.approx(0.4400926470800718), f"t_m = {t_m}" 57 | 58 | 59 | def test_bearing1(): 60 | lat1, lon1 = math.radians(38.898556), math.radians(-77.037852) 61 | lat2, lon2 = math.radians(38.897147), math.radians(-77.043934) 62 | b = dll.bearing_radians(lat1, lon1, lat2, lon2) 63 | b = math.degrees(b) 64 | # assert b == pytest.approx(253.42138889), f"Got: {b}" 65 | assert b == pytest.approx(-106.5748183426045), f"Got: {b}" 66 | 67 | 68 | def test_destination1(): 69 | lat1, lon1 = math.radians(53.32055556), math.radians(1.72972222) 70 | bearing = math.radians(96.02166667) 71 | dist = 124800 72 | lat2, lon2 = dll.destination_radians(lat1, lon1, bearing, dist) 73 | lat2, lon2 = (math.degrees(lat2), math.degrees(lon2)) 74 | assert lat2 == pytest.approx(53.188269553709034), f"Got: {lat2}" 75 | assert lon2 == pytest.approx(3.592721390871882), f"Got: {lon2}" 76 | 77 | 78 | def test_distance_segment_to_segment1(): 79 | f1 = (50.900393, 4.728607) 80 | f2 = (50.900389, 4.734047) 81 | t1 = (50.898538, 4.726107) 82 | t2 = (50.898176, 4.735463) 83 | d, pf, pt, u_f, u_t = dll.distance_segment_to_segment(f1, f2, t1, t2) 84 | if directory: 85 | plot_distance_segment_to_segment_latlon(f1, f2, t1, t2, pf, pt, "test_distance_segment_to_segment1") 86 | assert d == pytest.approx(216.60187728486514) 87 | assert pf == pytest.approx((50.900392999999994, 4.728607)) 88 | assert pt == pytest.approx((50.898448650708666, 4.728418070396815)) 89 | assert u_f == pytest.approx(0) 90 | assert u_t == pytest.approx(0.2470133466162735) 91 | 92 | 93 | def test_distance_segment_to_segment2(): 94 | f1 = (0, 0) 95 | f2 = (-0.43072496752146333, 381.4928613075559) 96 | t1 = (-206.26362055248765, -175.32538004745732) 97 | t2 = (-246.4746107556939, 480.8174213050763) 98 | d, pf, pt, u_f, u_t = de.distance_segment_to_segment(f1, f2, t1, t2) 99 | if directory: 100 | plot_distance_segment_to_segment_euc(f1, f2, t1, t2, pf, pt, "test_distance_segment_to_segment2") 101 | assert d == pytest.approx(216.60187728486514) 102 | assert pf == pytest.approx((0.0, 0.0)) 103 | assert pt == pytest.approx((-216.1962718133358, -13.249350827191222)) 104 | assert u_f == pytest.approx(0) 105 | assert u_t == pytest.approx(0.2470133466162735) 106 | 107 | 108 | def test_distance_segment_to_segment3(): 109 | f1 = (50.87205, 4.66089) 110 | f2 = (50.874550000000006, 4.672980000000001) 111 | t1 = (50.8740376, 4.6705204) 112 | t2 = (50.8741866999999, 4.67119980000001) 113 | d, pf, pt, u_f, u_t = dll.distance_segment_to_segment(f1, f2, t1, t2) 114 | if directory: 115 | plot_distance_segment_to_segment_latlon(f1, f2, t1, t2, pf, pt, "test_distance_segment_to_segment3") 116 | assert d == pytest.approx(0) 117 | assert pf == pytest.approx((50.87410572908839, 4.670830969750696)) 118 | assert pt == pytest.approx((50.87410575464133, 4.670830955670548)) 119 | assert u_f == pytest.approx(0.8222551304652699) 120 | assert u_t == pytest.approx(0.4571036354431931) 121 | 122 | 123 | def test_distance_segment_to_segment4(): 124 | f1 = (0, 0) 125 | f2 = (278.05674689789083, 848.3102386968303) 126 | t1 = (221.055090540802, 675.7367042826397) 127 | t2 = (237.6344733521503, 723.4080418578025) 128 | d, pf, pt, u_f, u_t = de.distance_segment_to_segment(f1, f2, t1, t2) 129 | if directory: 130 | plot_distance_segment_to_segment_euc(f1, f2, t1, t2, pf, pt, "test_distance_segment_to_segment4") 131 | assert d == pytest.approx(0) 132 | assert pf == pytest.approx((228.63358669727376, 697.5274459946864)) 133 | assert pt == pytest.approx((228.63358669727376, 697.5274459946864)) 134 | assert u_f == pytest.approx(0.8222551304652699) 135 | assert u_t == pytest.approx(0.4571036354431931) 136 | 137 | 138 | def test_distance_point_to_segment1(): 139 | locs = [ 140 | (47.6373, -122.0950167), 141 | (47.6369, -122.0950167), 142 | (47.6369, -122.0959167), 143 | (47.6369, -122.09422), 144 | (47.6369, -122.09400), 145 | (47.6375, -122.09505) 146 | ] 147 | loc_a = (47.6372498273849, -122.094900012016) 148 | loc_b = (47.6368394494057, -122.094280421734) 149 | segments = [] 150 | for lat_a, lat_b in itertools.product((loc_a[0], loc_b[0]), repeat=2): 151 | for lon_a, lon_b in itertools.product((loc_a[1], loc_b[1]), repeat=2): 152 | segments.append(((lat_a, lon_a), (lat_b, lon_b))) 153 | # segments = [(loc_a, loc_b)] 154 | 155 | for constrain in [True, False]: 156 | for loc_idx, loc in enumerate(locs): 157 | for seg_idx, (loc_a, loc_b) in enumerate(segments): 158 | dist1, pi1, ti1 = dll.distance_point_to_segment(loc, loc_a, loc_b, constrain=constrain) 159 | dist2, pi2, ti2 = dll.distance_point_to_segment(loc, loc_b, loc_a, constrain=constrain) 160 | if directory: 161 | plot_distance_point_to_segment_latlon(loc, loc_a, loc_b, pi1, 162 | f"point_to_segment_{loc_idx}_{seg_idx}_{constrain}.png") 163 | assert dist1 == pytest.approx(dist2), \ 164 | f"Locs[{loc_idx},{seg_idx},{constrain}]: Distances different, {dist1} != {dist2}" 165 | assert pi1[0] == pytest.approx(pi2[0]), \ 166 | f"Locs[{loc_idx},{seg_idx},{constrain}]: y coord different, {pi1[0]} != {pi2[0]}" 167 | assert pi1[1] == pytest.approx(pi2[1]), \ 168 | f"Locs[{loc_idx},{seg_idx},{constrain}]: y coord different, {pi1[1]} != {pi2[1]}" 169 | 170 | 171 | def plot_distance_point_to_segment_latlon(f, t1, t2, pt, fn): 172 | import smopy 173 | import matplotlib.pyplot as plt 174 | lat_min = min(f[0], t1[0], t2[0]) 175 | lat_max = max(f[0], t1[0], t2[0]) 176 | lon_min = min(f[1], t1[1], t2[1]) 177 | lon_max = max(f[1], t1[1], t2[1]) 178 | bb = [lat_min, lon_min, lat_max, lon_max] 179 | m = smopy.Map(bb) 180 | ax = m.show_mpl(figsize=(10, 10)) 181 | p1 = m.to_pixels(t1) 182 | p2 = m.to_pixels(t2) 183 | p3 = m.to_pixels(f) 184 | p4 = m.to_pixels(pt) 185 | ax.plot([p1[0], p2[0]], [p1[1], p2[1]], 'o-', color="black") 186 | ax.plot([p3[0]], [p3[1]], 'o-', color="black") 187 | ax.plot([p3[0], p4[0]], [p3[1], p4[1]], '--', color="red") 188 | plt.savefig(str(directory / fn)) 189 | plt.close(plt.gcf()) 190 | 191 | 192 | def plot_distance_segment_to_segment_latlon(f1, f2, t1, t2, pf, pt, fn): 193 | import smopy 194 | import matplotlib.pyplot as plt 195 | lat_min = min(f1[0], f2[0], t1[0], t2[0]) 196 | lat_max = max(f1[0], f2[0], t1[0], t2[0]) 197 | lon_min = min(f1[1], f2[1], t1[1], t2[1]) 198 | lon_max = max(f1[1], f2[1], t1[1], t2[1]) 199 | bb = [lat_min, lon_min, lat_max, lon_max] 200 | m = smopy.Map(bb) 201 | ax = m.show_mpl(figsize=(10, 10)) 202 | p1 = m.to_pixels(f1) 203 | p2 = m.to_pixels(f2) 204 | p3 = m.to_pixels(t1) 205 | p4 = m.to_pixels(t2) 206 | p5 = m.to_pixels(pf) 207 | p6 = m.to_pixels(pt) 208 | ax.plot([p1[0], p2[0]], [p1[1], p2[1]], 'o-') 209 | ax.plot([p3[0], p4[0]], [p3[1], p4[1]], 'o-') 210 | ax.plot([p5[0], p6[0]], [p5[1], p6[1]], 'x-') 211 | plt.savefig(str(directory / fn)) 212 | plt.close(plt.gcf()) 213 | 214 | 215 | def plot_distance_segment_to_segment_euc(f1, f2, t1, t2, pf, pt, fn): 216 | import matplotlib.pyplot as plt 217 | fig, ax = plt.subplots(1, 1, figsize=(10, 10)) 218 | ax.plot([f1[1], f2[1]], [f1[0], f2[0]], 'o-') 219 | ax.plot([t1[1], t2[1]], [t1[0], t2[0]], 'o-') 220 | ax.plot([pf[1], pt[1]], [pf[0], pt[0]], 'x-') 221 | ax.axis('equal') 222 | ax.set_aspect('equal') 223 | plt.savefig(str(directory / fn)) 224 | plt.close(plt.gcf()) 225 | 226 | 227 | if __name__ == "__main__": 228 | # mm.matching.logger.setLevel(logging.INFO) 229 | mm.logger.setLevel(logging.DEBUG) 230 | mm.logger.addHandler(logging.StreamHandler(sys.stdout)) 231 | directory = Path(os.environ.get('TESTDIR', Path(__file__).parent)) 232 | print(f"Saving files to {directory}") 233 | # test_path_to_gpx() 234 | test_grs80() 235 | # test_distance1() 236 | # test_bearing1() 237 | # test_destination1() 238 | # test_distance_segment_to_segment1() 239 | # test_distance_segment_to_segment2() 240 | # test_distance_segment_to_segment3() 241 | # test_distance_segment_to_segment4() 242 | # test_distance_point_to_segment1() 243 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf-8 3 | """ 4 | tests.test_examples 5 | ~~~~~~~~~~~~~~~~~~~ 6 | 7 | Run standalone python files that are a complete examples. 8 | Used to test the full examples in the documentation. 9 | 10 | :author: Wannes Meert 11 | :copyright: Copyright 2015-2022 DTAI, KU Leuven and Sirris. 12 | :license: Apache License, Version 2.0, see LICENSE for details. 13 | """ 14 | import sys 15 | import os 16 | import logging 17 | from pathlib import Path 18 | import subprocess as sp 19 | import pytest 20 | import leuvenmapmatching as mm 21 | 22 | 23 | logger = mm.logger 24 | examples_path = Path(os.path.realpath(__file__)).parent / "examples" 25 | 26 | 27 | def test_examples(): 28 | example_fns = examples_path.glob("*.py") 29 | for example_fn in example_fns: 30 | execute_file(example_fn.name) 31 | 32 | 33 | def importrun_file(fn, cmp_with_previous=False): 34 | import importlib 35 | fn = f"examples.{fn[:-3]}" 36 | print(f"Importing: {fn}") 37 | o = importlib.import_module(fn) 38 | o.run() 39 | 40 | 41 | def execute_file(fn, cmp_with_previous=False): 42 | print(f"Testing: {fn}") 43 | fn = examples_path / fn 44 | assert fn.exists() 45 | try: 46 | cmd = sp.run(["python3", fn], capture_output=True, check=True) 47 | except sp.CalledProcessError as exc: 48 | print(exc) 49 | print(exc.stderr.decode()) 50 | print(exc.stdout.decode()) 51 | raise exc 52 | 53 | if cmp_with_previous: 54 | # Not ready to be used in general testing, output contains floats 55 | result_data = cmd.stdout.decode() 56 | correct_fn = fn.with_suffix(".log") 57 | if correct_fn.exists(): 58 | with correct_fn.open("r") as correct_fp: 59 | correct_data = correct_fp.read() 60 | print(correct_data) 61 | print(result_data) 62 | assert correct_data == result_data, f"Logged output different for {fn}" 63 | else: 64 | with correct_fn.open("w") as correct_fp: 65 | correct_fp.write(result_data) 66 | 67 | 68 | if __name__ == "__main__": 69 | logger.setLevel(logging.WARNING) 70 | logger.addHandler(logging.StreamHandler(sys.stdout)) 71 | directory = Path(os.environ.get('TESTDIR', Path(__file__).parent)) 72 | print(f"Saving files to {directory}") 73 | # execute_file("example_1_simple.py", cmp_with_previous=True) 74 | execute_file("example_using_osmnx_and_geopandas.py", cmp_with_previous=True) 75 | # importrun_file("example_using_osmnx_and_geopandas.py", cmp_with_previous=True) 76 | -------------------------------------------------------------------------------- /tests/test_nonemitting.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf-8 3 | """ 4 | tests.test_nonemitting 5 | ~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | :author: Wannes Meert 8 | :copyright: Copyright 2017-2018 DTAI, KU Leuven and Sirris. 9 | :license: Apache License, Version 2.0, see LICENSE for details. 10 | """ 11 | import sys 12 | import os 13 | import logging 14 | from pathlib import Path 15 | 16 | try: 17 | import leuvenmapmatching as mm 18 | except ImportError: 19 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) 20 | import leuvenmapmatching as mm 21 | from leuvenmapmatching.matcher.distance import DistanceMatcher, DistanceMatching 22 | from leuvenmapmatching.matcher.simple import SimpleMatcher 23 | from leuvenmapmatching.map.inmem import InMemMap 24 | 25 | MYPY = False 26 | if MYPY: 27 | from typing import Tuple 28 | 29 | 30 | logger = mm.logger 31 | directory = None 32 | 33 | 34 | def setup_map(): 35 | path1 = [(1.8, 0.1), (1.8, 3.5), (3.0, 4.9)] # More nodes than observations 36 | path2 = [(1.8, 0.1), (1.8, 2.0), (1.8, 3.5), (3.0, 4.9)] 37 | path_sol = ['X', 'C', 'D', 'F'] 38 | mapdb = InMemMap("map", graph={ 39 | "A": ((1, 1), ["B", "C", "X"]), 40 | "B": ((1, 3), ["A", "C", "D", "K"]), 41 | "C": ((2, 2), ["A", "B", "D", "E", "X", "Y"]), 42 | "D": ((2, 4), ["B", "C", "E", "K", "L", "F"]), 43 | "E": ((3, 3), ["C", "D", "F", "Y"]), 44 | "F": ((3, 5), ["D", "E", "L"]), 45 | "X": ((2, 0), ["A", "C", "Y"]), 46 | "Y": ((3, 1), ["X", "C", "E"]), 47 | "K": ((1, 5), ["B", "D", "L"]), 48 | "L": ((2, 6), ["K", "D", "F"]) 49 | }, use_latlon=False) 50 | return mapdb, path1, path2, path_sol 51 | 52 | 53 | def visualize_map(pathnb=1): 54 | mapdb, path1, path2, path_sol = setup_map() 55 | import leuvenmapmatching.visualization as mm_vis 56 | if pathnb == 2: 57 | path = path2 58 | else: 59 | path = path1 60 | mm_vis.plot_map(mapdb, path=path, show_labels=True, 61 | filename=(directory / "test_nonemitting_map.png")) 62 | 63 | 64 | def test_path1(): 65 | mapdb, path1, path2, path_sol = setup_map() 66 | 67 | matcher = SimpleMatcher(mapdb, max_dist_init=1, 68 | min_prob_norm=0.5, 69 | obs_noise=0.5, 70 | non_emitting_states=True, only_edges=False) 71 | matcher.match(path1, unique=True) 72 | path_pred = matcher.path_pred_onlynodes 73 | if directory: 74 | from leuvenmapmatching import visualization as mmviz 75 | matcher.print_lattice_stats() 76 | matcher.print_lattice() 77 | with (directory / 'lattice_path1.gv').open('w') as ofile: 78 | matcher.lattice_dot(file=ofile) 79 | mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, 80 | show_graph=True, 81 | filename=str(directory / "test_nonemitting_test_path1.png")) 82 | assert path_pred == path_sol, f"Nodes not equal:\n{path_pred}\n{path_sol}" 83 | 84 | 85 | def test_path1_inc(): 86 | mapdb, path1, path2, path_sol = setup_map() 87 | 88 | matcher = SimpleMatcher(mapdb, max_dist_init=1, 89 | in_prob_norm=0.5, obs_noise=0.5, 90 | non_emitting_states=True, only_edges=False, 91 | max_lattice_width=1) 92 | 93 | print('## PHASE 1 ##') 94 | matcher.match(path1, unique=True) 95 | path_pred = matcher.path_pred_onlynodes 96 | if directory: 97 | from leuvenmapmatching import visualization as mmviz 98 | matcher.print_lattice_stats() 99 | matcher.print_lattice() 100 | with (directory / 'lattice_path1_inc1.gv').open('w') as ofile: 101 | matcher.lattice_dot(file=ofile, precision=2, render=True) 102 | mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, 103 | show_graph=True, 104 | filename=str(directory / "test_nonemitting_test_path1_inc1.png")) 105 | 106 | print('## PHASE 2 ##') 107 | matcher.increase_max_lattice_width(3, unique=True) 108 | path_pred = matcher.path_pred_onlynodes 109 | if directory: 110 | from leuvenmapmatching import visualization as mmviz 111 | matcher.print_lattice_stats() 112 | matcher.print_lattice() 113 | with (directory / 'lattice_path1_inc2.gv').open('w') as ofile: 114 | matcher.lattice_dot(file=ofile, precision=2, render=True) 115 | mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, 116 | show_graph=True, 117 | filename=str(directory / "test_nonemitting_test_path1_inc2.png")) 118 | 119 | 120 | assert path_pred == path_sol, f"Nodes not equal:\n{path_pred}\n{path_sol}" 121 | 122 | 123 | def test_path1_dist(): 124 | mapdb, path1, path2, path_sol = setup_map() 125 | 126 | matcher = DistanceMatcher(mapdb, max_dist_init=1, 127 | min_prob_norm=0.5, 128 | obs_noise=0.5, 129 | non_emitting_states=True, only_edges=True) 130 | matcher.match(path1, unique=True) 131 | path_pred = matcher.path_pred_onlynodes 132 | if directory: 133 | from leuvenmapmatching import visualization as mmviz 134 | matcher.print_lattice_stats() 135 | matcher.print_lattice() 136 | print("LATTICE BEST") 137 | for m in matcher.lattice_best: 138 | print(m) 139 | with (directory / 'lattice_path1.gv').open('w') as ofile: 140 | matcher.lattice_dot(file=ofile) 141 | mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, show_graph=True, 142 | filename=str(directory / "test_nonemitting_test_path1_dist.png")) 143 | assert path_pred == path_sol, f"Nodes not equal:\n{path_pred}\n{path_sol}" 144 | 145 | 146 | def test_path2(): 147 | mapdb, path1, path2, path_sol = setup_map() 148 | 149 | matcher = SimpleMatcher(mapdb, max_dist_init=1, min_prob_norm=0.5, obs_noise=0.5, 150 | non_emitting_states=True, only_edges=False) 151 | matcher.match(path2, unique=True) 152 | path_pred = matcher.path_pred_onlynodes 153 | 154 | dists = matcher.path_all_distances() 155 | 156 | if directory: 157 | from leuvenmapmatching import visualization as mmviz 158 | matcher.print_lattice_stats() 159 | matcher.print_lattice() 160 | with (directory / 'lattice_path2.gv').open('w') as ofile: 161 | matcher.lattice_dot(file=ofile) 162 | mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, 163 | filename=str(directory / "test_nonemitting_test_path2.png")) 164 | assert path_pred == path_sol, "Nodes not equal:\n{}\n{}".format(path_pred, path_sol) 165 | 166 | 167 | def test_path2_dist(): 168 | mapdb, path1, path2, path_sol = setup_map() 169 | 170 | matcher = DistanceMatcher(mapdb, max_dist_init=1, min_prob_norm=0.5, 171 | obs_noise=0.5, dist_noise=0.5, 172 | non_emitting_states=True) 173 | matcher.match(path2, unique=True) 174 | path_pred = matcher.path_pred_onlynodes 175 | if directory: 176 | from leuvenmapmatching import visualization as mmviz 177 | matcher.print_lattice_stats() 178 | matcher.print_lattice() 179 | # with (directory / 'lattice_path2.gv').open('w') as ofile: 180 | # matcher.lattice_dot(file=ofile) 181 | mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, 182 | filename=str(directory / "test_nonemitting_test_path2_dist.png")) 183 | assert path_pred == path_sol, "Nodes not equal:\n{}\n{}".format(path_pred, path_sol) 184 | 185 | 186 | def test_path2_incremental(): 187 | mapdb, path1, path2, path_sol = setup_map() 188 | 189 | matcher = SimpleMatcher(mapdb, max_dist_init=1, min_prob_norm=0.5, obs_noise=0.5, 190 | non_emitting_states=True, only_edges=False) 191 | matcher.match(path2[:2]) 192 | path_pred_1 = matcher.path_pred_onlynodes 193 | matcher.match(path2, expand=True) 194 | path_pred = matcher.path_pred_onlynodes 195 | if directory: 196 | from leuvenmapmatching import visualization as mmviz 197 | matcher.print_lattice_stats() 198 | matcher.print_lattice() 199 | with (directory / 'lattice_path2.gv').open('w') as ofile: 200 | matcher.lattice_dot(file=ofile) 201 | mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, 202 | filename=str(directory / "test_nonemitting_test_path2.png")) 203 | assert path_pred_1 == path_sol[:2], "Nodes not equal:\n{}\n{}".format(path_pred, path_sol) 204 | assert path_pred == path_sol, "Nodes not equal:\n{}\n{}".format(path_pred, path_sol) 205 | 206 | 207 | def test_path_duplicate(): 208 | from datetime import datetime 209 | # A path with two identical points 210 | path = [(0.8, 0.7), (0.9, 0.7), (1.1, 1.0), (1.2, 1.5), (1.2, 1.5), (1.2, 1.6), (1.1, 2.0), 211 | (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7), 212 | (2.1, 3.3), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2), (3.1, 3.8), 213 | (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)] 214 | 215 | mapdb = InMemMap("map", graph={ 216 | "A": ((1, 1), ["B", "C"]), 217 | "B": ((1, 3), ["A", "C", "D"]), 218 | "C": ((2, 2), ["A", "B", "D", "E"]), 219 | "D": ((2, 4), ["B", "C", "D", "E"]), 220 | "E": ((3, 3), ["C", "D", "F"]), 221 | "F": ((3, 5), ["D", "E"]) 222 | }, use_latlon=False) 223 | 224 | matcher = SimpleMatcher(mapdb, max_dist=None, min_prob_norm=None, 225 | non_emitting_states = True, only_edges=False) 226 | 227 | #Matching with and without timestamps signed to the points 228 | path_pred = matcher.match(path, unique=False) 229 | 230 | path = [(p1, p2, datetime.fromtimestamp(i)) for i, (p1, p2) in enumerate(path)] 231 | path_pred_time = matcher.match(path, unique=False) 232 | 233 | if directory: 234 | from leuvenmapmatching import visualization as mmviz 235 | matcher.print_lattice_stats() 236 | matcher.print_lattice() 237 | mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, 238 | filename=str(directory / "test_nonemitting_test_path_duplicate.png")) 239 | 240 | # The path should be identical regardless of the timestamps 241 | assert path_pred == path_pred_time, f"Nodes not equal:\n{path_pred}\n{path_pred_time}" 242 | 243 | 244 | def test_path3_many_obs(): 245 | path = [(1, 0), (3, -0.1), (3.7, 0.6), (4.5, 0.7), 246 | (5.5, 1.2), (6.5, 0.88), (7.5, 0.65), (8.5, -0.1), 247 | (9.8, 0.1),(10.1, 1.9)] 248 | path_sol = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] 249 | mapdb = InMemMap("map", graph={ 250 | "A": ((1, 0.00), ["B"]), 251 | "B": ((3, 0.00), ["A", "C"]), 252 | "C": ((4, 0.70), ["B", "D"]), 253 | "D": ((5, 1.00), ["C", "E"]), 254 | "E": ((6, 1.00), ["D", "F"]), 255 | "F": ((7, 0.70), ["E", "G"]), 256 | "G": ((8, 0.00), ["F", "H"]), 257 | "H": ((10, 0.0), ["G", "I"]), 258 | "I": ((10, 2.0), ["H"]) 259 | }, use_latlon=False) 260 | matcher = SimpleMatcher(mapdb, max_dist_init=0.2, obs_noise=1, obs_noise_ne=10, 261 | non_emitting_states=True) 262 | matcher.match(path) 263 | path_pred = matcher.path_pred_onlynodes 264 | if directory: 265 | matcher.print_lattice_stats() 266 | matcher.print_lattice() 267 | from leuvenmapmatching import visualization as mmviz 268 | mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, linewidth=10, 269 | show_graph=True, show_lattice=True, 270 | filename=str(directory / "test_test_path_ne_3_mo.png")) 271 | assert path_pred == path_sol, f"Nodes not equal:\n{path_pred}\n{path_sol}" 272 | 273 | 274 | def test_path3_few_obs_en(): 275 | path = [(1, 0), (7.5, 0.65), (10.1, 1.9)] 276 | path_sol = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] 277 | mapdb = InMemMap("map", graph={ 278 | "A": ((1, 0.00), ["B"]), 279 | "B": ((3, 0.00), ["A", "C"]), 280 | "C": ((4, 0.70), ["B", "D"]), 281 | "D": ((5, 1.00), ["C", "E"]), 282 | "E": ((6, 1.00), ["D", "F"]), 283 | "F": ((7, 0.70), ["E", "G"]), 284 | "G": ((8, 0.00), ["F", "H"]), 285 | "H": ((10, 0.0), ["G", "I"]), 286 | "I": ((10, 2.0), ["H"]) 287 | }, use_latlon=False) 288 | matcher = SimpleMatcher(mapdb, max_dist_init=0.2, obs_noise=1, obs_noise_ne=10, 289 | non_emitting_states=True, only_edges=False) 290 | matcher.match(path) 291 | path_pred = matcher.path_pred_onlynodes 292 | if directory: 293 | matcher.print_lattice_stats() 294 | matcher.print_lattice() 295 | from leuvenmapmatching import visualization as mmviz 296 | mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, linewidth=10, 297 | filename=str(directory / "test_test_path_ne_3_fo.png")) 298 | assert path_pred == path_sol, f"Nodes not equal:\n{path_pred}\n{path_sol}" 299 | 300 | 301 | def test_path3_few_obs_e(): 302 | path = [(1, 0), (7.5, 0.65), (10.1, 1.9)] 303 | path_sol = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] 304 | mapdb = InMemMap("map", graph={ 305 | "A": ((1, 0.00), ["B"]), 306 | "B": ((3, 0.00), ["A", "C"]), 307 | "C": ((4, 0.70), ["B", "D"]), 308 | "D": ((5, 1.00), ["C", "E"]), 309 | "E": ((6, 1.00), ["D", "F"]), 310 | "F": ((7, 0.70), ["E", "G"]), 311 | "G": ((8, 0.00), ["F", "H"]), 312 | "H": ((10, 0.0), ["G", "I"]), 313 | "I": ((10, 2.0), ["H"]) 314 | }, use_latlon=False) 315 | matcher = SimpleMatcher(mapdb, max_dist_init=0.2, obs_noise=1, obs_noise_ne=10, 316 | non_emitting_states=True, only_edges=True) 317 | matcher.match(path) 318 | path_pred = matcher.path_pred_onlynodes 319 | if directory: 320 | matcher.print_lattice_stats() 321 | matcher.print_lattice() 322 | from leuvenmapmatching import visualization as mmviz 323 | mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, linewidth=10, 324 | filename=str(directory / "test_test_path_e_3_fo.png")) 325 | assert path_pred == path_sol, f"Nodes not equal:\n{path_pred}\n{path_sol}" 326 | 327 | 328 | def test_path3_dist(): 329 | path = [(0, 1), (0.65, 7.5), (1.9, 10.1)] 330 | path_sol = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] 331 | mapdb = InMemMap("map", graph={ 332 | "A": ((0.00, 1), ["B"]), 333 | "B": ((0.00, 3), ["A", "C"]), 334 | "C": ((0.70, 3), ["B", "D"]), 335 | "D": ((1.00, 5), ["C", "E"]), 336 | "E": ((1.00, 6), ["D", "F"]), 337 | "F": ((0.70, 7), ["E", "G"]), 338 | "G": ((0.00, 8), ["F", "H"]), 339 | "H": ((0.0, 10), ["G", "I"]), 340 | "I": ((2.0, 10), ["H"]) 341 | }, use_latlon=False) 342 | matcher = DistanceMatcher(mapdb, max_dist_init=0.2, 343 | obs_noise=0.5, obs_noise_ne=2, dist_noise=0.5, 344 | non_emitting_states=True) 345 | states, lastidx = matcher.match(path) 346 | path_pred = matcher.path_pred_onlynodes 347 | if directory: 348 | from leuvenmapmatching import visualization as mmviz 349 | mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, linewidth=2, 350 | filename=str(directory / "test_path_3_dist.png")) 351 | assert path_pred == path_sol, f"Nodes not equal:\n{path_pred}\n{path_sol}" 352 | 353 | for obs_idx, m in enumerate(matcher.lattice_best): # type: Tuple[int, DistanceMatching] 354 | state = m.shortkey # tuple indicating edge 355 | ne_str = "e" if m.is_emitting() else "ne" # state is emitting or not 356 | p1_str = "{:>5.2f}-{:<5.2f}".format(*m.edge_m.pi) # best matching location on graph 357 | p2_str = "{:>5.2f}-{:<5.2f}".format(*m.edge_o.pi) # best matching location on track 358 | print(f"{obs_idx:<2} | {state} | {ne_str:<2} | {p1_str} | {p2_str}") 359 | 360 | 361 | if __name__ == "__main__": 362 | # mm.matching.logger.setLevel(logging.INFO) 363 | logger.setLevel(logging.DEBUG) 364 | logger.addHandler(logging.StreamHandler(sys.stdout)) 365 | directory = Path(os.environ.get('TESTDIR', Path(__file__).parent)) 366 | print(f"Saving files to {directory}") 367 | # visualize_map(pathnb=1) 368 | # test_path1() 369 | # test_path1_inc() 370 | # test_path1_dist() 371 | test_path2() 372 | # test_path2_dist() 373 | # test_path2_incremental() 374 | # test_path_duplicate() 375 | # test_path3_many_obs() 376 | # test_path3_few_obs_en() 377 | # test_path3_few_obs_e() 378 | # test_path3_dist() 379 | -------------------------------------------------------------------------------- /tests/test_nonemitting_circle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf-8 3 | """ 4 | tests.test_nonemitting_circle 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | :author: Wannes Meert 8 | :copyright: Copyright 2018 DTAI, KU Leuven and Sirris. 9 | :license: Apache License, Version 2.0, see LICENSE for details. 10 | """ 11 | import sys, os 12 | import logging 13 | import math 14 | from pathlib import Path 15 | import numpy as np 16 | 17 | try: 18 | import leuvenmapmatching as mm 19 | except ImportError: 20 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) 21 | import leuvenmapmatching as mm 22 | from leuvenmapmatching.matcher.simple import SimpleMatcher 23 | from leuvenmapmatching.matcher.distance import DistanceMatcher 24 | from leuvenmapmatching.map.inmem import InMemMap 25 | 26 | 27 | directory = None 28 | 29 | 30 | def setup_map(disconnect=True): 31 | theta = np.linspace(0, 2 * math.pi, 4 * 5 + 1)[:-1] 32 | 33 | ox = 0.1 + np.cos(theta * 0.95) 34 | oy = np.sin(theta * 1) 35 | path1 = list(zip(ox, oy)) # all observations 36 | path2 = [(x, y) for x, y in zip(ox, oy) if x > -0.60] 37 | 38 | nx = np.cos(theta) 39 | ny = np.sin(theta) 40 | nl = [f"N{i}" for i in range(len(nx))] 41 | graph = {} 42 | for i, (x, y, l) in enumerate(zip(nx, ny, nl)): 43 | if disconnect: 44 | edges = [] 45 | if i != len(nx) - 1: 46 | edges.append(nl[(i + 1) % len(nl)]) 47 | if i != 0: 48 | edges.append(nl[(i - 1) % len(nl)]) 49 | else: 50 | edges = [nl[(i - 1) % len(nl)], nl[(i + 1) % len(nl)]] 51 | graph[l] = ((x, y), edges) 52 | graph["M"] = ((0, 0), ["N5", "N15"]) 53 | graph["N5"][1].append("M") 54 | graph["N15"][1].append("M") 55 | print(graph) 56 | 57 | path_sol = nl 58 | if not disconnect: 59 | path_sol += ["N0"] 60 | 61 | mapdb = InMemMap("map", graph=graph, use_latlon=False) 62 | return mapdb, path1, path2, path_sol 63 | 64 | 65 | def visualize_map(): 66 | if directory is None: 67 | return 68 | import matplotlib.pyplot as plt 69 | import leuvenmapmatching.visualization as mm_vis 70 | 71 | mapdb, path1, path2, path_sol = setup_map() 72 | 73 | fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(10, 5)) 74 | mm_vis.plot_map(mapdb, path=path1, ax=ax, show_labels=True) 75 | fig.savefig(str(directory / 'test_nonemitting_circle_map_path1.png')) 76 | plt.close(fig) 77 | 78 | fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(10, 10)) 79 | mm_vis.plot_map(mapdb, path=path2, ax=ax, show_labels=True) 80 | fig.savefig(str(directory / 'test_nonemitting_circle_map_path2.png')) 81 | plt.close(fig) 82 | 83 | 84 | def visualize_path(matcher, mapdb, name="test"): 85 | import matplotlib.pyplot as plt 86 | from leuvenmapmatching import visualization as mmviz 87 | fig, ax = plt.subplots(1, 1, figsize=(10, 10)) 88 | mmviz.plot_map(mapdb, matcher=matcher, ax=ax, 89 | show_labels=True, show_matching=True, show_graph=True, 90 | linewidth=2) 91 | fn = directory / f"test_nonemitting_circle_{name}_map.png" 92 | fig.savefig(str(fn)) 93 | plt.close(fig) 94 | print(f"saved to {fn}") 95 | 96 | 97 | def test_path1(): 98 | mapdb, path1, path2, path_sol = setup_map() 99 | matcher = SimpleMatcher(mapdb, max_dist_init=1, min_prob_norm=0.5, obs_noise=0.5, 100 | non_emitting_states=True) 101 | matcher.match(path1, unique=True) 102 | path_pred = matcher.path_pred_onlynodes 103 | if directory: 104 | visualize_path(matcher, mapdb, name="testpath1") 105 | assert path_pred == path_sol, f"Nodes not equal:\n{path_pred}\n{path_sol}" 106 | 107 | 108 | def test_path1_dist(): 109 | mapdb, path1, path2, path_sol = setup_map() 110 | matcher = DistanceMatcher(mapdb, max_dist_init=1, min_prob_norm=0.8, obs_noise=0.5, 111 | non_emitting_states=True) 112 | matcher.match(path1, unique=True) 113 | path_pred = matcher.path_pred_onlynodes 114 | if directory: 115 | visualize_path(matcher, mapdb, name="test_path1_dist") 116 | assert path_pred == path_sol, f"Nodes not equal:\n{path_pred}\n{path_sol}" 117 | 118 | 119 | def test_path2(): 120 | mapdb, path1, path2, _ = setup_map() 121 | path_sol = [f"N{i}" for i in range(20)] 122 | matcher = SimpleMatcher(mapdb, max_dist_init=0.2, min_prob_norm=0.1, 123 | obs_noise=0.1, obs_noise_ne=1, 124 | non_emitting_states=True, only_edges=True) 125 | path_pred = matcher.match(path2, unique=True) 126 | print(path_pred) 127 | path_pred = matcher.path_pred_onlynodes 128 | if directory: 129 | matcher.print_lattice_stats(verbose=True) 130 | matcher.print_lattice() 131 | print("Best path through lattice:") 132 | for m in matcher.lattice_best: 133 | print(m) 134 | visualize_path(matcher, mapdb, name="testpath2") 135 | assert path_pred == path_sol, f"Nodes not equal:\n{path_pred}\n{path_sol}" 136 | 137 | 138 | def test_path2_dist(): 139 | mapdb, path1, path2, _ = setup_map() 140 | path_sol = [f"N{i}" for i in range(20)] 141 | matcher = DistanceMatcher(mapdb, max_dist_init=0.2, min_prob_norm=0.1, 142 | obs_noise=0.1, obs_noise_ne=1, 143 | non_emitting_states=True) 144 | matcher.match(path2) 145 | path_pred = matcher.path_pred_onlynodes 146 | if directory: 147 | visualize_path(matcher, mapdb, name="test_path2_dist") 148 | assert path_pred == path_sol, f"Nodes not equal:\n{path_pred}\n{path_sol}" 149 | 150 | 151 | if __name__ == "__main__": 152 | # mm.matching.logger.setLevel(logging.INFO) 153 | mm.logger.setLevel(logging.DEBUG) 154 | mm.logger.addHandler(logging.StreamHandler(sys.stdout)) 155 | directory = Path(os.environ.get('TESTDIR', Path(__file__).parent)) 156 | print(f"Saving files to {directory}") 157 | # visualize_map() 158 | test_path1() 159 | # test_path1_dist() 160 | # test_path2() 161 | # test_path2_dist() 162 | -------------------------------------------------------------------------------- /tests/test_parallelroads.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf-8 3 | """ 4 | tests.test_parallelroads 5 | ~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | :author: Wannes Meert 8 | :copyright: Copyright 2018 DTAI, KU Leuven and Sirris. 9 | :license: Apache License, Version 2.0, see LICENSE for details. 10 | """ 11 | import sys 12 | import os 13 | import logging 14 | from pathlib import Path 15 | import leuvenmapmatching as mm 16 | from leuvenmapmatching.map.sqlite import SqliteMap 17 | from leuvenmapmatching.matcher.distance import DistanceMatcher 18 | from leuvenmapmatching.util.dist_euclidean import lines_parallel 19 | 20 | 21 | logger = mm.logger 22 | directory = None 23 | 24 | 25 | def create_map1(): 26 | db = SqliteMap("map", use_latlon=False, dir=directory) 27 | logger.debug(f"Initialized db: {db}") 28 | db.add_nodes([ 29 | (1, (1, 1)), 30 | (2, (1, 2.9)), 31 | (22, (1, 3.0)), 32 | (3, (2, 2)), 33 | (33, (2, 2.1)), 34 | (4, (2, 4)), 35 | (5, (3, 3)), 36 | (6, (3, 5)) 37 | ]) 38 | db.add_edges([ 39 | (1, 2), (1, 3), 40 | (2, 22), (2, 1), 41 | (22, 2), (22, 33), (22, 4), 42 | (3, 33), (3, 1), (3, 2), (3, 5), 43 | (33, 3), 44 | (4, 22), (4, 33), (4, 5), (4, 6), 45 | (5, 3), (5, 4), (5, 6), 46 | (6, 4), (6, 5) 47 | ]) 48 | logger.debug(f"Filled db: {db}") 49 | return db 50 | 51 | 52 | def create_path1(): 53 | return [(0.9, 2.5), (1.1, 2.75), (1.25, 2.6), (1.4, 2.5), (1.5, 2.4), (1.6, 2.5), (1.4, 2.7), (1.2, 2.9), (1.1, 3.0), (1.3, 3.2)] 54 | 55 | 56 | def test_parallel(): 57 | result = lines_parallel((1, 2.9), (2, 2), (1, 3.0), (2, 2.1), d=0.1) 58 | assert result is True 59 | 60 | 61 | def test_bb1(): 62 | mapdb = create_map1() 63 | 64 | if directory: 65 | from leuvenmapmatching import visualization as mmviz 66 | mmviz.plot_map(mapdb, 67 | show_labels=True, show_graph=True, 68 | filename=str(directory / "test_bb.png")) 69 | 70 | mapdb.connect_parallelroads() 71 | 72 | assert mapdb.size() == 8 73 | coord = mapdb.node_coordinates(2) 74 | assert coord == (1, 2.9) 75 | 76 | nodes = mapdb.all_nodes(bb=[0.5, 2.5, 1.5, 3.5]) 77 | node_ids = set([nid for nid, _ in nodes]) 78 | assert node_ids == {2, 22} 79 | 80 | edges = mapdb.all_edges(bb=[0.5, 2.5, 1.5, 3.5]) 81 | edge_tuples = set((nid1, nid2) for nid1, _, nid2, _ in edges) 82 | assert edge_tuples == {(1, 2), (2, 1), (3, 2), (2, 22), (22, 2), (22, 33), (22, 4), (4, 22)} 83 | 84 | nodes = mapdb.nodes_nbrto(2) 85 | node_ids = set([nid for nid, _ in nodes]) 86 | assert node_ids == {1, 22} 87 | 88 | edges = mapdb.edges_nbrto((1, 2)) 89 | edge_tuples = set((nid1, nid2) for nid1, _, nid2, _ in edges) 90 | assert edge_tuples == {(2, 22), (2, 1)} 91 | 92 | edges = mapdb.edges_nbrto((3, 2)) 93 | edge_tuples = set((nid1, nid2) for nid1, _, nid2, _ in edges) 94 | assert edge_tuples == {(22, 33), (2, 22), (2, 1)} 95 | 96 | 97 | def test_merge1(): 98 | mapdb = create_map1() 99 | mapdb.connect_parallelroads() 100 | 101 | if directory: 102 | from leuvenmapmatching import visualization as mmviz 103 | mmviz.plot_map(mapdb, 104 | show_labels=True, show_graph=True, 105 | filename=str(directory / "test_parallel_merge.png")) 106 | 107 | 108 | def test_path1(): 109 | mapdb = create_map1() 110 | mapdb.connect_parallelroads() 111 | path = create_path1() 112 | states_sol = [(1, 2), (2, 22), (22, 33), (22, 33), (22, 33), (3, 2), (3, 2), (3, 2), (2, 22), (22, 4)] 113 | 114 | matcher = DistanceMatcher(mapdb, max_dist_init=0.2, 115 | obs_noise=0.5, obs_noise_ne=2, dist_noise=0.5, 116 | non_emitting_states=True) 117 | states, _ = matcher.match(path) 118 | 119 | if directory: 120 | from leuvenmapmatching import visualization as mmviz 121 | mmviz.plot_map(mapdb, matcher=matcher, 122 | show_labels=True, show_graph=True, show_matching=True, 123 | filename=str(directory / "test_parallel_merge.png")) 124 | assert states == states_sol, f"Unexpected states: {states}" 125 | 126 | 127 | if __name__ == "__main__": 128 | logger.setLevel(logging.DEBUG) 129 | logger.addHandler(logging.StreamHandler(sys.stdout)) 130 | directory = Path(os.environ.get('TESTDIR', Path(__file__).parent)) 131 | print(f"Saving files to {directory}") 132 | # test_parallel() 133 | # test_merge1() 134 | # test_path1() 135 | test_bb1() 136 | -------------------------------------------------------------------------------- /tests/test_path_latlon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf-8 3 | """ 4 | tests.test_path_latlon 5 | ~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | :author: Wannes Meert 8 | :copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris. 9 | :license: Apache License, Version 2.0, see LICENSE for details. 10 | """ 11 | import sys 12 | import os 13 | import pickle 14 | import logging 15 | from pathlib import Path 16 | import pytest 17 | import leuvenmapmatching as mm 18 | import leuvenmapmatching.visualization as mm_viz 19 | from leuvenmapmatching.util.gpx import gpx_to_path 20 | from leuvenmapmatching.util.dist_latlon import interpolate_path 21 | from leuvenmapmatching.util.openstreetmap import create_map_from_xml, download_map_xml 22 | from leuvenmapmatching.matcher.distance import DistanceMatcher 23 | from leuvenmapmatching.map.inmem import InMemMap 24 | 25 | logger = mm.logger 26 | this_path = Path(os.path.realpath(__file__)).parent / "rsrc" / "path_latlon" 27 | osm_fn = this_path / "osm_downloaded.xml" 28 | osm2_fn = this_path / "osm_downloaded2.xml" 29 | osm3_fn = this_path / "osm_downloaded3.xml" 30 | track_fn = this_path / "route.gpx" # http://users.telenet.be/meirenwi/Leuven%20Stadswandeling%20-%205%20km%20RT.zip 31 | track2_fn = this_path / "route2.gpx" 32 | track3_fn = this_path / "route3.pgx" 33 | zip_fn = this_path / "leuvenmapmatching_testdata2.zip" 34 | directory = None 35 | 36 | 37 | def prepare_files(verbose=False, force=False, download_from_osm=False): 38 | if download_from_osm: 39 | download_map_xml(osm_fn, '4.694933,50.870047,4.709256000000001,50.879628', force=force, verbose=verbose) 40 | download_map_xml(osm2_fn, '4.6997666,50.8684188,4.7052813,50.8731718', force=force, verbose=verbose) 41 | download_map_xml(osm3_fn, '4.69049,50.86784,4.71604,50.88784', force=force, verbose=verbose) 42 | else: 43 | if not (osm_fn.exists() and osm2_fn.exists() and osm3_fn.exists() and 44 | track_fn.exists() and track2_fn.exists()): 45 | import requests 46 | url = 'https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/leuvenmapmatching_testdata2.zip' 47 | logger.debug("Download road_network.zip from kuleuven.be") 48 | r = requests.get(url, stream=True) 49 | with zip_fn.open('wb') as ofile: 50 | for chunk in r.iter_content(chunk_size=1024): 51 | if chunk: 52 | ofile.write(chunk) 53 | import zipfile 54 | logger.debug("Unzipping road_network.zip") 55 | with zipfile.ZipFile(str(zip_fn), "r") as zip_ref: 56 | zip_ref.extractall(str(zip_fn.parent)) 57 | 58 | 59 | def test_path1(use_rtree=False): 60 | prepare_files() 61 | track = gpx_to_path(track_fn) 62 | track = [loc[:2] for loc in track] 63 | track = track[:5] 64 | track_int = interpolate_path(track, 5) 65 | map_con = create_map_from_xml(osm_fn, use_rtree=use_rtree, index_edges=True) 66 | 67 | matcher = DistanceMatcher(map_con, max_dist=50, obs_noise=50, min_prob_norm=0.1) 68 | states, last_idx = matcher.match(track_int) 69 | 70 | if directory: 71 | # matcher.print_lattice_stats() 72 | mm_viz.plot_map(map_con, matcher=matcher, use_osm=True, 73 | zoom_path=True, show_graph=True, 74 | filename=str(directory / "test_path_latlon_path1.png")) 75 | assert len(states) == len(track_int), f"Path ({len(track_int)}) not fully matched by best path ({len(states)}), " + \ 76 | f"last index = {last_idx}" 77 | states_sol = [(2963305939, 249348325), (2963305939, 249348325), (2963305939, 249348325), (2963305939, 249348325), 78 | (2963305939, 249348325), (2963305939, 249348325), (249348325, 1545679243), (249348325, 1545679243), 79 | (1545679243, 3663115134), (1545679243, 3663115134), (1545679243, 3663115134), 80 | (3663115134, 1545679251), (1545679251, 20910628), (1545679251, 20910628), (1545679251, 20910628), 81 | (1545679251, 20910628), (20910628, 3663115130)] 82 | assert states == states_sol, f"Got states: {states}" 83 | 84 | 85 | def test_path1_serialization(use_rtree=False): 86 | prepare_files() 87 | track = gpx_to_path(track_fn) 88 | track = [loc[:2] for loc in track] 89 | track = track[:5] 90 | track_int = interpolate_path(track, 5) 91 | map_con = create_map_from_xml(osm_fn, use_rtree=use_rtree, index_edges=True) 92 | 93 | to_serialize = map_con.serialize() 94 | map_con.dir = this_path 95 | map_con.dump() 96 | 97 | map_con2 = InMemMap.from_pickle(filename = map_con.dir / (map_con.name + ".pkl")) 98 | 99 | matcher = DistanceMatcher(map_con2, max_dist=50, obs_noise=50, min_prob_norm=0.1) 100 | states, last_idx = matcher.match(track_int) 101 | 102 | if directory: 103 | # matcher.print_lattice_stats() 104 | mm_viz.plot_map(map_con2, matcher=matcher, use_osm=True, 105 | zoom_path=True, show_graph=True, 106 | filename=str(directory / "test_path_latlon_path1.png")) 107 | assert len(states) == len(track_int), f"Path ({len(track_int)}) not fully matched by best path ({len(states)}), " + \ 108 | f"last index = {last_idx}" 109 | states_sol = [(2963305939, 249348325), (2963305939, 249348325), (2963305939, 249348325), (2963305939, 249348325), 110 | (2963305939, 249348325), (2963305939, 249348325), (249348325, 1545679243), (249348325, 1545679243), 111 | (1545679243, 3663115134), (1545679243, 3663115134), (1545679243, 3663115134), 112 | (3663115134, 1545679251), (1545679251, 20910628), (1545679251, 20910628), (1545679251, 20910628), 113 | (1545679251, 20910628), (20910628, 3663115130)] 114 | assert states == states_sol, f"Got states: {states}" 115 | 116 | 117 | @pytest.mark.skip(reason="Takes a long time") 118 | def test_path1_full(): 119 | prepare_files() 120 | track = gpx_to_path(track_fn) 121 | track = [loc[:2] for loc in track] 122 | track_int = interpolate_path(track, 5) 123 | map_con = create_map_from_xml(osm_fn, include_footways=True, include_parking=True) 124 | 125 | matcher = DistanceMatcher(map_con, max_dist=50, obs_noise=50, min_prob_norm=0.1) 126 | states, last_idx = matcher.match(track_int) 127 | 128 | if directory: 129 | # matcher.print_lattice_stats() 130 | mm_viz.plot_map(map_con, matcher=matcher, use_osm=True, 131 | zoom_path=True, show_graph=True, 132 | filename=str(directory / "test_path_latlon_path1.png")) 133 | assert len(states) == len(track_int), f"Path ({len(track_int)}) not fully matched by best path ({len(states)}), " + \ 134 | f"last index = {last_idx}" 135 | 136 | 137 | def test_path2_proj(): 138 | prepare_files() 139 | map_con_latlon = create_map_from_xml(osm2_fn) 140 | map_con = map_con_latlon.to_xy() 141 | track = [map_con.latlon2yx(p[0], p[1]) for p in gpx_to_path(track2_fn)] 142 | matcher = DistanceMatcher(map_con, max_dist=300, max_dist_init=25, min_prob_norm=0.0001, 143 | non_emitting_length_factor=0.95, 144 | obs_noise=50, obs_noise_ne=50, 145 | dist_noise=50, 146 | max_lattice_width=5, 147 | non_emitting_states=True) 148 | states, last_idx = matcher.match(track, unique=False) 149 | nodes = matcher.path_pred_onlynodes 150 | if directory: 151 | matcher.print_lattice_stats() 152 | mm_viz.plot_map(map_con, matcher=matcher, path=track, use_osm=False, 153 | show_graph=True, show_matching=True, show_labels=5, 154 | filename=str(directory / "test_path_latlon_path2_proj.png")) 155 | nodes_sol = [2634474831, 1096512242, 3051083902, 1096512239, 1096512241, 1096512240, 1096508366, 1096508372, 156 | 16483861, 1096508360, 159656075, 1096508382, 16483862, 3051083898, 16526535, 3060597381, 3060515059, 157 | 16526534, 16526532, 1274158119, 16526540, 3060597377, 16526541, 16424220, 1233373340, 613125597, 158 | 1076057753] 159 | nodes_sol2 = [1096512242, 3051083902, 1096512239, 1096512241, 1096512240, 159654664, 1096508373, 1096508381, 160 | 16483859, 1096508369, 159654663, 1096508363, 16483862, 3051083898, 16526535, 3060597381, 3060515059, 161 | 16526534, 16526532, 611867918, 3060725817, 16483866, 3060725817, 611867918, 16526532, 1274158119, 162 | 16526540, 3060597377, 16526541, 16424220, 1233373340, 613125597, 1076057753] 163 | assert (nodes == nodes_sol) or (nodes == nodes_sol2), f"Nodes do not match: {nodes}" 164 | 165 | 166 | def test_path2(): 167 | prepare_files() 168 | map_con = create_map_from_xml(osm2_fn) 169 | track = [(p[0], p[1]) for p in gpx_to_path(track2_fn)] 170 | matcher = DistanceMatcher(map_con, max_dist=300, max_dist_init=25, min_prob_norm=0.0001, 171 | non_emitting_length_factor=0.95, 172 | obs_noise=50, obs_noise_ne=50, 173 | dist_noise=50, 174 | max_lattice_width=5, 175 | non_emitting_states=True) 176 | states, last_idx = matcher.match(track, unique=False) 177 | nodes = matcher.path_pred_onlynodes 178 | if directory: 179 | mm_viz.plot_map(map_con, matcher=matcher, nodes=nodes, path=track, z=17, use_osm=True, 180 | show_graph=True, show_matching=True, 181 | filename=str(directory / "test_path_latlon_path2.png")) 182 | nodes_sol = [2634474831, 1096512242, 3051083902, 1096512239, 1096512241, 1096512240, 1096508366, 1096508372, 183 | 16483861, 3051083900, 16483864, 16483865, 3060515058, 16526534, 16526532, 1274158119, 16526540, 184 | 3060597377, 16526541, 16424220, 1233373340, 613125597, 1076057753] 185 | nodes_sol2 = [2634474831, 1096512242, 3051083902, 1096512239, 1096512241, 1096512240, 159654664, 1096508373, 186 | 1096508381, 16483859, 1096508369, 159654663, 1096508363, 16483862, 3051083898, 16526535, 3060597381, 187 | 3060515059, 16526534, 16526532, 1274158119, 16526540, 3060597377, 16526541, 16424220, 1233373340, 188 | 613125597, 1076057753] 189 | 190 | assert (nodes == nodes_sol) or (nodes == nodes_sol2), f"Nodes do not match: {nodes}" 191 | 192 | 193 | def test_path3(): 194 | prepare_files() 195 | track = [(50.87881, 4.698930000000001), (50.87899, 4.69836), (50.87905000000001, 4.698110000000001), 196 | (50.879000000000005, 4.69793), (50.87903000000001, 4.69766), (50.87906, 4.697500000000001), 197 | (50.87908, 4.6973), (50.879110000000004, 4.69665), (50.87854, 4.696420000000001), 198 | (50.878440000000005, 4.696330000000001), (50.878370000000004, 4.696140000000001), 199 | (50.8783, 4.69578), (50.87832, 4.69543), (50.87767, 4.695530000000001), 200 | (50.87763, 4.695080000000001), (50.87758, 4.6948300000000005), (50.877480000000006, 4.69395), 201 | (50.877500000000005, 4.693700000000001), (50.877520000000004, 4.69343), 202 | (50.877610000000004, 4.692670000000001), (50.87776, 4.6917800000000005), 203 | (50.87783, 4.69141), (50.87744000000001, 4.6908900000000004), (50.87736, 4.690790000000001), 204 | (50.877300000000005, 4.69078), (50.876650000000005, 4.6907000000000005), 205 | (50.87597, 4.69066), (50.875820000000004, 4.69068), (50.87561, 4.6907700000000006), 206 | (50.874430000000004, 4.69136), (50.874210000000005, 4.691490000000001), (50.87413, 4.69151), 207 | (50.87406000000001, 4.69151), (50.87397000000001, 4.69148), (50.87346, 4.6913800000000005), 208 | (50.87279, 4.691260000000001), (50.872490000000006, 4.69115), (50.87259, 4.6908900000000004), 209 | (50.87225, 4.690650000000001), (50.872080000000004, 4.6904900000000005), 210 | (50.871550000000006, 4.69125), (50.87097000000001, 4.69216), (50.87033, 4.69324), 211 | (50.87017, 4.6935400000000005), (50.87012000000001, 4.69373), (50.86997, 4.69406), 212 | (50.86981, 4.694520000000001), (50.86943, 4.69585), (50.868970000000004, 4.697500000000001), 213 | (50.868770000000005, 4.698130000000001), (50.86863, 4.6985), (50.86844000000001, 4.69899), 214 | (50.868140000000004, 4.69977), (50.86802, 4.70023), (50.867920000000005, 4.70078), 215 | (50.86787, 4.701180000000001), (50.86784, 4.70195), (50.86786000000001, 4.702310000000001), 216 | (50.86791, 4.702870000000001), (50.86836, 4.7052700000000005), (50.86863, 4.7064900000000005), 217 | (50.86880000000001, 4.707210000000001), (50.869220000000006, 4.708410000000001), 218 | (50.869400000000006, 4.70891), (50.86959, 4.709350000000001), (50.86995, 4.71004), (50.87006, 4.71021), 219 | (50.870900000000006, 4.7112300000000005), (50.872260000000004, 4.712890000000001), (50.87308, 4.71389), 220 | (50.873430000000006, 4.714300000000001), (50.873560000000005, 4.71441), 221 | (50.873740000000005, 4.714530000000001), (50.874280000000006, 4.714740000000001), 222 | (50.876250000000006, 4.71544), (50.876490000000004, 4.7155700000000005), 223 | (50.876900000000006, 4.7158500000000005), (50.87709, 4.71598), (50.877190000000006, 4.716010000000001), 224 | (50.87751, 4.7160400000000005), (50.87782000000001, 4.7160400000000005), (50.87832, 4.71591), 225 | (50.87894000000001, 4.71567), (50.87975, 4.71536), (50.88004, 4.71525), (50.8804, 4.715070000000001), 226 | (50.88163, 4.71452), (50.881750000000004, 4.71447), (50.8819, 4.714390000000001), 227 | (50.882200000000005, 4.71415), (50.882470000000005, 4.7138800000000005), 228 | (50.883480000000006, 4.7127300000000005), (50.88552000000001, 4.710470000000001), 229 | (50.88624, 4.70966), (50.88635000000001, 4.7096100000000005), (50.886520000000004, 4.709580000000001), 230 | (50.88664000000001, 4.7095400000000005), (50.886750000000006, 4.709280000000001), 231 | (50.88684000000001, 4.70906), (50.886970000000005, 4.70898), (50.88705, 4.70887), (50.88714, 4.70868), 232 | (50.88743, 4.7079), (50.887840000000004, 4.7069), (50.88776000000001, 4.70687), 233 | (50.88765, 4.706790000000001), (50.887100000000004, 4.70627), (50.88702000000001, 4.70619), 234 | (50.886950000000006, 4.706040000000001), (50.886950000000006, 4.7058800000000005), 235 | (50.886970000000005, 4.705620000000001), (50.88711000000001, 4.70417), (50.88720000000001, 4.70324), 236 | (50.88723, 4.7027600000000005), (50.88709000000001, 4.70253), (50.886480000000006, 4.70148), 237 | (50.88636, 4.70131), (50.886050000000004, 4.70101), (50.88593, 4.70092), 238 | (50.885810000000006, 4.700880000000001), (50.88539, 4.7008600000000005), (50.88497, 4.70082), 239 | (50.88436, 4.70089), (50.88398, 4.70094), (50.883250000000004, 4.7010700000000005), 240 | (50.88271, 4.701160000000001), (50.88136, 4.70159), (50.881130000000006, 4.701790000000001), 241 | (50.880930000000006, 4.7020100000000005), (50.88078, 4.70223), (50.88046000000001, 4.70146), 242 | (50.88015000000001, 4.70101), (50.880030000000005, 4.700880000000001), (50.87997000000001, 4.70078), 243 | (50.879900000000006, 4.70061), (50.87984, 4.70052), (50.879960000000004, 4.70026)] 244 | track = track[:30] 245 | map_con = create_map_from_xml(osm3_fn) 246 | 247 | matcher = DistanceMatcher(map_con, 248 | max_dist_init=30, max_dist=50, min_prob_norm=0.1, 249 | obs_noise=10, obs_noise_ne=20, dist_noise=10, 250 | non_emitting_states=True) 251 | states, last_idx = matcher.match(track) 252 | 253 | if directory: 254 | # matcher.print_lattice_stats() 255 | mm_viz.plot_map(map_con, matcher=matcher, use_osm=True, 256 | zoom_path=True, show_graph=False, show_matching=True, 257 | filename=str(directory / "test_path_latlon_path3.png")) 258 | nodes = matcher.path_pred_onlynodes 259 | nodes_sol = [3906576303, 1150903750, 4506996820, 4506996819, 4506996798, 3906576457, 130147477, 3906576346, 260 | 231974072, 231974123, 1180606706, 19792164, 19792172, 1180606683, 1180606709, 5236409057, 261 | 19792169, 5236409056, 180241961, 180241975, 4506996259, 19792156, 5236409048, 180241625, 262 | 180241638, 231953030, 241928030, 241928031, 83796665, 231953028, 1125556965, 1380538625, 263 | 1824115892, 4909655515, 16571387, 16737662, 16571388, 179425214, 3705540990, 4567021046] 264 | assert nodes == nodes_sol, f"Nodes do not match: {nodes}" 265 | 266 | 267 | if __name__ == "__main__": 268 | logger.setLevel(logging.INFO) 269 | logger.addHandler(logging.StreamHandler(sys.stdout)) 270 | directory = Path(os.environ.get('TESTDIR', Path(__file__).parent)) 271 | print(f"Saving files to {directory}") 272 | import matplotlib as mpl 273 | mpl.use('MacOSX') 274 | # test_path1(use_rtree=True) 275 | test_path1_serialization(use_rtree=True) 276 | # test_path1_full() 277 | # test_path2_proj() 278 | # test_path2() 279 | # test_path3() 280 | -------------------------------------------------------------------------------- /tests/test_path_onlyedges.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf-8 3 | """ 4 | tests.test_path_onlyedges 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | :author: Wannes Meert 8 | :copyright: Copyright 2018 DTAI, KU Leuven and Sirris. 9 | :license: Apache License, Version 2.0, see LICENSE for details. 10 | """ 11 | import sys 12 | import os 13 | import logging 14 | from pathlib import Path 15 | import leuvenmapmatching as mm 16 | from leuvenmapmatching.map.inmem import InMemMap 17 | from leuvenmapmatching.matcher.simple import SimpleMatcher 18 | 19 | 20 | logger = mm.logger 21 | directory = None 22 | 23 | 24 | def test_path1(): 25 | path = [(0.8, 0.7), (0.9, 0.7), (1.1, 1.0), (1.2, 1.5), (1.2, 1.6), (1.1, 2.0), 26 | (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7), 27 | (2.1, 3.3), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2), (3.1, 3.8), 28 | (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)] 29 | path_sol = ['A', 'B', 'D', 'E', 'F'] 30 | mapdb = InMemMap("map", graph={ 31 | "A": ((1, 1), ["B", "C"]), 32 | "B": ((1, 3), ["A", "C", "D"]), 33 | "C": ((2, 2), ["A", "B", "D", "E"]), 34 | "D": ((2, 4), ["B", "C", "D", "E"]), 35 | "E": ((3, 3), ["C", "D", "F"]), 36 | "F": ((3, 5), ["D", "E"]) 37 | }, use_latlon=False) 38 | 39 | matcher = SimpleMatcher(mapdb, max_dist=None, min_prob_norm=None, 40 | only_edges=True, non_emitting_states=False) 41 | matcher.match(path, unique=True) 42 | path_pred = matcher.path_pred_onlynodes 43 | if directory: 44 | print("Lattice best:") 45 | for m in matcher.lattice_best: 46 | print(m) 47 | matcher.print_lattice_stats() 48 | matcher.print_lattice() 49 | from leuvenmapmatching import visualization as mmviz 50 | mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, 51 | filename=str(directory / "test_onlyedges_path1.png")) 52 | assert path_pred == path_sol, f"Paths not equal:\n{path_pred}\n{path_sol}" 53 | 54 | 55 | def test_path3(): 56 | path = [(3.0, 3.2), (3.1, 3.8), (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)] 57 | path_sol = ['E', 'F'] 58 | mapdb = InMemMap("map", graph={ 59 | "E": ((3, 3), ["F"]), 60 | "F": ((3, 5), ["E"]), 61 | }, use_latlon=False) 62 | 63 | matcher = SimpleMatcher(mapdb, max_dist=None, min_prob_norm=0.0001, 64 | max_dist_init=1, obs_noise=0.25, obs_noise_ne=10, 65 | non_emitting_states=True, 66 | only_edges=True) 67 | matcher.match(path, unique=True) 68 | path_pred = matcher.path_pred_onlynodes 69 | if directory: 70 | matcher.print_lattice_stats() 71 | matcher.print_lattice() 72 | from leuvenmapmatching import visualization as mmviz 73 | with (directory / 'lattice.gv').open('w') as ofile: 74 | matcher.lattice_dot(file=ofile) 75 | mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, 76 | filename=str(directory / "test_onlyedges_path3.png")) 77 | print("Path through lattice:\n" + "\n".join(m.label for m in matcher.lattice_best)) 78 | assert path_pred == path_sol, "Nodes not equal:\n{}\n{}".format(path_pred, path_sol) 79 | 80 | 81 | if __name__ == "__main__": 82 | logger.setLevel(logging.DEBUG) 83 | logger.addHandler(logging.StreamHandler(sys.stdout)) 84 | directory = Path(os.environ.get('TESTDIR', Path(__file__).parent)) 85 | print(f"Saving files to {directory}") 86 | test_path1() 87 | # test_path3() 88 | --------------------------------------------------------------------------------