[
  {
    "path": ".gitignore",
    "content": ".cache\n.eggs\n.idea\ndist\n*.egg-info\nvenv*\nREADME\n.ipynb_checkpoints\n*.zip\n*.gv\n*.gv.pdf\n*.xml\ntests/route.gpx\nexamples/Leuven\\ Stadswandeling*\n.git-old\n.pytest_cache\ndocs/_build\nbuild\ncache\nbaselines\n./examples\n*.pkl\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "version: 2\n\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n\nsphinx:\n  configuration: docs/conf.py\n\npython:\n   install:\n   - requirements: docs/requirements.txt\n"
  },
  {
    "path": "LICENSE",
    "content": "Leuven.MapMatching\n------------------\n\nCopyright 2015-2018 KU Leuven, DTAI Research Group\nCopyright 2017-2018 Sirris\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\n\n\nThis package/repository contains code from the dtaimapmatching project as well\nas some open-source works:\n\n\nLatitude/longitude spherical geodesy tools\n------------------------------------------\n\nLatitude/longitude spherical geodesy tools\n(c) Chris Veness 2002-2017, MIT Licence\nwww.movable-type.co.uk/scripts/latlong.html\nwww.movable-type.co.uk/scripts/geodesy/docs/module-latlon-spherical.html\n\n\nNvector\n-------\n\nGade, K. (2010). A Nonsingular Horizontal Position Representation, The Journal\nof Navigation, Volume 63, Issue 03, pp 395-417, July 2010.\n(www.navlab.net/Publications/A_Nonsingular_Horizontal_Position_Representation.pdf)\n\nThis paper should be cited in publications using this library.\n\nCopyright (c) 2015, Norwegian Defence Research Establishment (FFI)\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above publication\ninformation, copyright notice, this list of conditions and the following\ndisclaimer.\n\n2. Redistributions in binary form must reproduce the above publication\ninformation, copyright notice, this list of conditions and the following\ndisclaimer in the documentation and/or other materials provided with the\ndistribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED\nTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS\nBE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\nSUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\nINTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF\nTHE POSSIBILITY OF SUCH DAMAGE."
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.md\ninclude LICENSE"
  },
  {
    "path": "Makefile",
    "content": "\n.PHONY: test3\ntest3:\n\t@#export PYTHONPATH=.;./venv/bin/py.test --ignore=venv -vv\n\tpython3 setup.py test\n\n.PHONY: test2\ntest2:\n\tpython2 setup.py test\n\n.PHONY: test\ntest: test3 test2\n\n.PHONY: version\nversion:\n\t@python3 setup.py --version\n\n.PHONY: prepare_dist\nprepare_dist:\n\trm -rf dist/*\n\tpython3 setup.py sdist bdist_wheel\n\n.PHONY: prepare_tag\nprepare_tag:\n\t@echo \"Check whether repo is clean\"\n\tgit diff-index --quiet HEAD\n\t@echo \"Check correct branch\"\n\tif [[ \"$$(git rev-parse --abbrev-ref HEAD)\" != \"master\" ]]; then echo 'Not master branch'; exit 1; fi\n\t@echo \"Add tag\"\n\tgit tag \"v$$(python3 setup.py --version)\"\n\tgit push --tags\n\n.PHONY: deploy\ndeploy: prepare_dist prepare_tag\n\t@echo \"Check whether repo is clean\"\n\tgit diff-index --quiet HEAD\n\t@echo \"Start uploading\"\n\ttwine upload --repository leuvenmapmatching dist/*\n\n.PHONY: docs\ndocs:\n\texport PYTHONPATH=..; cd docs; make html\n\n.PHONY: docsclean\ndocsclean:\n\tcd docs; make clean\n\n.PHONY: clean\nclean: docsclean\n\n"
  },
  {
    "path": "README.md",
    "content": "# Leuven.MapMatching\n\n[![PyPi Version](https://img.shields.io/pypi/v/leuvenmapmatching.svg)](https://pypi.org/project/leuvenmapmatching/)\n[![Documentation Status](https://readthedocs.org/projects/leuvenmapmatching/badge/?version=latest)](https://leuvenmapmatching.readthedocs.io/en/latest/?badge=latest)\n\n\nAlign a trace of GPS measurements to a map or road segments.\n\nThe matching is based on a Hidden Markov Model (HMM) with non-emitting \nstates. The model can deal with missing data and you can plug in custom\ntransition and emission probability distributions.\n\n![example](http://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/example1.png?v=2)\n\nMain reference:\n\n> Meert Wannes, Mathias Verbeke, \"HMM with Non-Emitting States for Map Matching\",\n> European Conference on Data Analysis (ECDA), Paderborn, Germany, 2018.\n\nOther references:\n\n> Devos Laurens, Vandebril Raf (supervisor), Meert Wannes (supervisor),\n> \"Trafﬁc patterns revealed through matrix functions and map matching\",\n> Master thesis, Faculty of Engineering Science, KU Leuven, 2018\n\n## Installation and usage\n\n    $ pip install leuvenmapmatching\n\nMore information and examples:\n\n[leuvenmapmatching.readthedocs.io](https://leuvenmapmatching.readthedocs.io)\n\n## Dependencies\n\nRequired:\n\n- [numpy](http://www.numpy.org)\n- [scipy](https://www.scipy.org)\n\n\nOptional (only loaded when methods are called to rely on these packages):\n\n- [matplotlib](http://matplotlib.org):\n    For visualisation\n- [smopy](https://github.com/rossant/smopy):\n    For visualisation\n- [nvector](https://github.com/pbrod/Nvector):\n    For latitude-longitude computations\n- [gpxpy](https://github.com/tkrajina/gpxpy):\n    To import GPX files\n- [pykalman](https://pykalman.github.io):\n    So smooth paths using a Kalman filter\n- [pyproj](https://jswhit.github.io/pyproj/):\n    To project latitude-longitude coordinates to an XY-plane\n- [rtree](http://toblerity.org/rtree/):\n    To quickly search locations\n\n\n## Contact\n\nWannes Meert, DTAI, KU Leuven  \nwannes.meert@cs.kuleuven.be  \nhttps://dtai.cs.kuleuven.be\n\nMathias Verbeke, Sirris  \nmathias.verbeke@sirris.be  \nhttp://www.sirris.be/expertise/data-innovation\n\nDeveloped with the support of [Elucidata.be](http://www.elucidata.be).\n\n\n## License\n\nCopyright 2015-2022, KU Leuven - DTAI Research Group, Sirris - Elucidata Group  \nApache License, Version 2.0.\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = DTAIMap-Matching\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "docs/classes/map/BaseMap.rst",
    "content": "BaseMap\n=======\n\n\n.. autoclass:: leuvenmapmatching.map.base.BaseMap\n   :members:\n"
  },
  {
    "path": "docs/classes/map/InMemMap.rst",
    "content": "InMemMap\n========\n\n\n.. autoclass:: leuvenmapmatching.map.inmem.InMemMap\n   :members:\n"
  },
  {
    "path": "docs/classes/map/SqliteMap.rst",
    "content": "SqliteMap\n=========\n\n\n.. autoclass:: leuvenmapmatching.map.sqlite.SqliteMap\n   :members:\n"
  },
  {
    "path": "docs/classes/matcher/BaseMatcher.rst",
    "content": "BaseMatcher\n===========\n\nThis a generic base class to be used by matchers. This class itself\ndoes not implement a working matcher. Use a matcher such as\n``SimpleMatcher``, ``DistanceMatcher``, ...\n\n.. autoclass:: leuvenmapmatching.matcher.base.BaseMatcher\n   :members:\n"
  },
  {
    "path": "docs/classes/matcher/BaseMatching.rst",
    "content": "BaseMatching\n============\n\n\n.. autoclass:: leuvenmapmatching.matcher.base.BaseMatching\n   :members:\n"
  },
  {
    "path": "docs/classes/matcher/DistanceMatcher.rst",
    "content": "DistanceMatcher\n===============\n\n\n.. autoclass:: leuvenmapmatching.matcher.distance.DistanceMatcher\n   :members:\n"
  },
  {
    "path": "docs/classes/matcher/SimpleMatcher.rst",
    "content": "SimpleMatcher\n=============\n\n\n.. autoclass:: leuvenmapmatching.matcher.simple.SimpleMatcher\n   :members:\n"
  },
  {
    "path": "docs/classes/overview.rst",
    "content": "\n\nmatcher\n~~~~~~~\n\n.. toctree::\n   :caption: Matcher\n\n   matcher/BaseMatcher\n   matcher/SimpleMatcher\n   matcher/DistanceMatcher\n\n.. toctree::\n   :caption: Matching\n\n   matcher/BaseMatching\n\n\n\nmap\n~~~\n\n.. toctree::\n   :caption: Map\n\n   map/BaseMap\n   map/InMemMap\n   map/SqliteMap\n\n\n\nutil\n~~~~\n\n.. toctree::\n   :caption: Util\n\n   util/Segment\n"
  },
  {
    "path": "docs/classes/util/Segment.rst",
    "content": "Segment\n=======\n\n\n.. autoclass:: leuvenmapmatching.util.segment.Segment\n   :members:\n"
  },
  {
    "path": "docs/conf.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n#\n# Leuven.MapMatching documentation build configuration file, created by\n# sphinx-quickstart on Sat Apr 14 23:24:31 2018.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\nimport os\nimport sys\nsys.path.insert(0, os.path.abspath('..'))\n\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc',\n    'sphinx.ext.mathjax',\n    'sphinx.ext.viewcode']\n\nautoclass_content = 'both'\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = 'Leuven.MapMatching'\ncopyright = '2018-2022, Wannes Meert'\nauthor = 'Wannes Meert'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = '1.1.1'\n# The full version, including alpha/beta/rc tags.\nrelease = '1.1.1'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n# html_theme = 'alabaster'\nhtml_theme = \"sphinx_rtd_theme\"\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'LeuvenMapMatchingDoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'LeuvenMapMatching.tex', 'Leuven.MapMatching Documentation',\n     'Wannes Meert', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'leuvenmapmatching', 'Leuven.MapMatching Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'LeuvenMapMatching', 'Leuven.MapMatching Documentation',\n     author, 'LeuvenMapMatching', 'Map Matching',\n     'Miscellaneous'),\n]\n\n\n\n"
  },
  {
    "path": "docs/index.rst",
    "content": ".. Leuven.MapMatching documentation master file, created by\n   sphinx-quickstart on Sat Apr 14 23:24:31 2018.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nLeuven.MapMatching's documentation\n==================================\n\nAlign a trace of coordinates (e.g. GPS measurements) to a map of road segments.\n\nThe matching is based on a Hidden Markov Model (HMM) with non-emitting\nstates. The model can deal with missing data and you can plug in custom\ntransition and emission probability distributions.\n\nReference:\n\n   Meert Wannes, Mathias Verbeke, \"HMM with Non-Emitting States for Map Matching\",\n   European Conference on Data Analysis (ECDA), Paderborn, Germany, 2018.\n\n\n.. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/example1.png?v=2\n   :alt: example\n\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n\n.. toctree::\n   :caption: Usage\n\n\n   usage/installation\n   usage/introduction\n   usage/openstreetmap\n   usage/visualisation\n   usage/latitudelongitude\n   usage/customdistributions\n   usage/incremental\n   usage/debug\n\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Classes\n\n   classes/overview\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\nset SPHINXPROJ=DTAIMap-Matching\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.http://sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "docs/requirements.txt",
    "content": "numpy\nscipy\nsphinx_rtd_theme"
  },
  {
    "path": "docs/usage/customdistributions.rst",
    "content": "Custom probability distributions\n================================\n\nYou can use your own custom probability distributions for the transition and emission probabilities.\nThis is achieved by inheriting from the :class:`BaseMatcher` class.\n\nExamples are available in the :class:`SimpleMatching` class and :class:`DistanceMatching` class.\nThe latter implements a variation based on Newson and Krumm (2009).\n\nTransition probability distribution\n-----------------------------------\n\nOverwrite the :meth:`logprob_trans` method.\n\nFor example, if you want to use a uniform distribution over the possible road segments:\n\n.. code-block:: python\n\n   def logprob_trans(self, prev_m, edge_m, edge_o, is_prev_ne, is_next_ne):\n       return -math.log(len(self.matcher.map.nodes_nbrto(self.edge_m.last_point())))\n\nNote that ``prev_m.edge_m`` and ``edge_m`` are not necessarily connected. For example if the ``Map`` object\nreturns a neighbor state that is not connected in the roadmap. This functionality is used to allow switching lanes.\n\n\nEmission probability distribution\n---------------------------------\n\nOverwrite the :meth:`logprob_obs` method for non-emitting nodes.\nThese methods are given the closest distance as `dist`, the previous :class:`Matching` object\nin the lattice, the state as `edge_m`, and the observation as `edge_o`. The latter two are :class:`Segment` objects\nthat can represent either a segment or a point.\nEach segment also has a project point which is the point on the segment that is the closest point.\n\nFor example, a simple step function with more tolerance for non-emitting nodes:\n\n.. code-block:: python\n\n   def logprob_obs(self, dist, prev_m, new_edge_m, new_edge_o, is_ne):\n       if is_ne:\n           if dist < 50:\n               return -math.log(50)\n       else:\n           if dist < 10:\n               return -math.log(10)\n       return -np.inf\n\nNote that an emission probability can be given for a non-emitting node. This allows you to rank non-emitting nodes\neven when no observations are available. It will then insert pseudo-observations on the line between the previous\nand next observations.\nTo have a pure non-emitting node, the `logprob_obs` method should always return 0 if the\n``is_ne`` argument is true.\n\n\nCustom lattice objects\n----------------------\n\nIf you need to store additional information in the lattice, inherit from the :class:`Matching` class and\npass your custom object to the :class:`Matcher` object.\n\n.. code-block:: python\n\n   from leuvenmapmatching.map.base import BaseMatching\n\n   class MyMatching(BaseMatching):\n       ...\n\n   matcher = MyMatcher(mapdb, matching=MyMatching)\n\n"
  },
  {
    "path": "docs/usage/debug.rst",
    "content": "Debug\n=====\n\nIncreasing the verbosity level\n------------------------------\n\nTo inspect the intermediate steps that the algorithm take, you can increase\nthe verbosity level of the package. For example:\n\n.. code-block:: python\n\n    import sys\n    import logging\n    import leuvenmapmatching\n    logger = leuvenmapmatching.logger\n\n    logger.setLevel(logging.DEBUG)\n    logger.addHandler(logging.StreamHandler(sys.stdout))\n\n\nInspect the best matching\n-------------------------\n\nThe best match is available in ``matcher.lattice_best``. This is a list of\n``Matching`` objects. For example after running the first example in the introduction:\n\n.. code-block:: python\n\n    >>> matcher.lattice_best\n    [Matching<A-B-0-0>,\n     Matching<A-B-1-0>,\n     Matching<A-B-2-0>,\n    ...\n\nA matching object summarizes its information as a tuple with three values if\nthe best match is with a vertex: <label-observation-nonemitting>. And a tuple\nwith four values if the best match is with an edge: <labelstart-labelend-observation-nonemitting>.\n\nIn the example above, the first observation (with index 0) is matched to a point on the edge\nA-B. If you want to inspect the exact locations, you can query the ``Segment``\nobjects that express the observation and map: ``matching.edge_o`` and ``matching.edge_m``.\n\n.. code-block:: python\n\n   >>> match = matcher.lattice_best[0]\n   >>> match.edge_m.l1, match.edge_m.l2  # Edge start/end labels\n   ('A', 'B')\n   >>> match.edge_m.pi  # Best point on A-B edge\n   (1.0, 1.0)\n   >>> match.edge_m.p1, match.edge_m.p2  # Locations of A and B\n   ((1, 1), (1, 3))\n   >>> match.edge_o.l1, match.edge_o.l2  # Observation\n   ('O0', None)\n   >>> match.edge_o.pi  # Location of observation O0, because no second location\n   (0.8, 0.7)\n   >>> match.edge_o.p1  # Same as pi because no interpolation\n   (0.8, 0.7)\n\nInspect the matching lattice\n----------------------------\n\nAll paths through the lattice are available in ``matcher.lattice``.\nThe lattice is a dictionary with a ``LatticeColumn`` object for each observation\n(in case the full path of observations is matched).\n\nFor each observation, you can inspect the ``Matching`` objects with:\n\n.. code-block:: python\n\n    >>> matcher.lattice\n    {0: <leuvenmapmatching.matcher.base.LatticeColumn at 0x12369bf40>,\n     1: <leuvenmapmatching.matcher.base.LatticeColumn at 0x123639dc0>,\n     2: <leuvenmapmatching.matcher.base.LatticeColumn at 0x123603f40>,\n     ...\n    >>> matcher.lattice[0].values_all()\n    {Matching<A-B-0-0>,\n     Matching<A-B-0-1>,\n     Matching<A-C-0-0>,\n     ...\n\nTo start backtracking you can, for example, see which matching object\nfor the last element has the highest probability (thus the best match):\n\n.. code-block:: python\n\n    >>> m = max(matcher.lattice[len(path)-1].values_all(), key=lambda m: m.logprob)\n    >>> m.logprob\n    -0.6835815469734807\n\nThe previous matching objects can be queried with. These are only those\nmatches that are connected to this matchin the lattice (in this case\nnodes in the street graph with an edge to the current node):\n\n.. code-block:: python\n\n    >>> m.prev  # Best previous match with a connection (multiple if equal probability)\n    {Matching<E-F-20-0>}\n    >>> m.prev_other  # All previous matches in the lattice with a connection\n    {Matching<C-E-20-0>,\n     Matching<D-E-20-0>,\n     Matching<F-E-20-0>,\n     Matching<Y-E-20-0>}\n"
  },
  {
    "path": "docs/usage/incremental.rst",
    "content": "Incremental matching\n====================\n\nExample: Incremental matching\n-------------------------------\n\nIf the observations are collected in a streaming setting. The matching can also be invoked incrementally.\nThe lattice will be built further every time a new subsequence of the path is given.\n\n.. code-block:: python\n\n    from leuvenmapmatching.matcher.distance import DistanceMatcher\n    from leuvenmapmatching.map.inmemmap import InMemMap\n\n    map_con = InMemMap(\"mymap\", graph={\n        \"A\": ((1, 1), [\"B\", \"C\", \"X\"]),\n        \"B\": ((1, 3), [\"A\", \"C\", \"D\", \"K\"]),\n        \"C\": ((2, 2), [\"A\", \"B\", \"D\", \"E\", \"X\", \"Y\"]),\n        \"D\": ((2, 4), [\"B\", \"C\", \"D\", \"E\", \"K\", \"L\"]),\n        \"E\": ((3, 3), [\"C\", \"D\", \"F\", \"Y\"]),\n        \"F\": ((3, 5), [\"D\", \"E\", \"L\"]),\n        \"X\": ((2, 0), [\"A\", \"C\", \"Y\"]),\n        \"Y\": ((3, 1), [\"X\", \"C\", \"E\"]),\n        \"K\": ((1, 5), [\"B\", \"D\", \"L\"]),\n        \"L\": ((2, 6), [\"K\", \"D\", \"F\"])\n    }, use_latlon=False)\n\n    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),\n            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),\n            (2.3, 3.5), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2),\n            (3.1, 3.8), (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]\n\n    matcher = DistanceMatcher(map_con, max_dist=2, obs_noise=1, min_prob_norm=0.5)\n    states, _ = matcher.match(path[:5])\n    states, _ = matcher.match(path, expand=True)\n    nodes = matcher.path_pred_onlynodes\n\n    print(\"States\\n------\")\n    print(states)\n    print(\"Nodes\\n------\")\n    print(nodes)\n    print(\"\")\n    matcher.print_lattice_stats()\n\n\n"
  },
  {
    "path": "docs/usage/installation.rst",
    "content": "Installation\n============\n\nDependencies\n------------\n\nRequired:\n\n-  `numpy <http://www.numpy.org>`__\n-  `scipy <https://www.scipy.org>`__\n\nOptional (only loaded when methods are called that rely on these packages):\n\n-  `rtree <http://toblerity.org/rtree/>`__\n-  `nvector <https://github.com/pbrod/Nvector>`__\n-  `gpxpy <https://github.com/tkrajina/gpxpy>`__\n-  `pyproj <https://jswhit.github.io/pyproj/>`__\n-  `pykalman <https://pykalman.github.io>`__\n-  `matplotlib <http://matplotlib.org>`__\n-  `smopy <https://github.com/rossant/smopy>`__\n\n\nUsing pip\n---------\n\nIf you want to install the latest released version using pip:\n\n::\n\n    $ pip install leuvenmapmatching\n\nIf you want to install the latest non-released version (add ``@develop``) for the\nlatest development version:\n\n::\n\n    $ pip install git+https://github.com/wannesm/leuvenmapmatching\n\n\nFrom source\n-----------\n\nThe library can also be compiled and/or installed directly from source.\n\n* Download the source from https://github.com/wannesm/leuvenmapmatching\n* To compile and install in your site-package directory: ``python3 setup.py install``\n\n"
  },
  {
    "path": "docs/usage/introduction.rst",
    "content": "Examples\n========\n\nExample 1: Simple\n-----------------\n\nA first, simple example. Some parameters are given to tune the algorithm.\nThe ``max_dist`` and ``obs_noise`` are distances that indicate the maximal distance between observation and road\nsegment and the expected noise in the measurements, respectively.\nThe ``min_prob_norm`` prunes the lattice in that it drops paths that drop below 0.5 normalized probability.\nThe probability is normalized to allow for easier reasoning about the probability of a path.\nIt is computed as the exponential smoothed log probability components instead of the sum as would be the case\nfor log likelihood.\nBecause the number of possible paths quickly grows, it is recommended to set the\n``max_lattice_width`` argument to speed up the algorithm (available from version 1.0 onwards).\nIt will only continue the search with this number of possible paths at every step. If no solution is found,\nthis value can be incremented using the ``increase_max_lattice_width`` method.\n\n.. code-block:: python\n\n    from leuvenmapmatching.matcher.distance import DistanceMatcher\n    from leuvenmapmatching.map.inmem import InMemMap\n\n    map_con = InMemMap(\"mymap\", graph={\n        \"A\": ((1, 1), [\"B\", \"C\", \"X\"]),\n        \"B\": ((1, 3), [\"A\", \"C\", \"D\", \"K\"]),\n        \"C\": ((2, 2), [\"A\", \"B\", \"D\", \"E\", \"X\", \"Y\"]),\n        \"D\": ((2, 4), [\"B\", \"C\", \"F\", \"E\", \"K\", \"L\"]),\n        \"E\": ((3, 3), [\"C\", \"D\", \"F\", \"Y\"]),\n        \"F\": ((3, 5), [\"D\", \"E\", \"L\"]),\n        \"X\": ((2, 0), [\"A\", \"C\", \"Y\"]),\n        \"Y\": ((3, 1), [\"X\", \"C\", \"E\"]),\n        \"K\": ((1, 5), [\"B\", \"D\", \"L\"]),\n        \"L\": ((2, 6), [\"K\", \"D\", \"F\"])\n    }, use_latlon=False)\n\n    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),\n            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),\n            (2.3, 3.5), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2),\n            (3.1, 3.8), (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]\n\n    matcher = DistanceMatcher(map_con, max_dist=2, obs_noise=1, min_prob_norm=0.5, max_lattice_width=5)\n    states, _ = matcher.match(path)\n    nodes = matcher.path_pred_onlynodes\n\n    print(\"States\\n------\")\n    print(states)\n    print(\"Nodes\\n------\")\n    print(nodes)\n    print(\"\")\n    matcher.print_lattice_stats()\n\n\nExample 2: Non-emitting states\n------------------------------\n\nIn case there are less observations that states (an assumption of HMMs), non-emittings states allow you\nto deal with this. States will be inserted that are not associated with any of the given observations if\nthis improves the probability of the path.\n\nIt is possible to also associate a distribtion over the distance between observations and the non-emitting\nstates (`obs_noise_ne`). This allows the algorithm to prefer nearby road segments. This value should be\nlarger than `obs_noise` as it is mapped to the line between the previous and next observation, which does\nnot necessarily run over the relevant segment. Setting this to infinity is the same as using pure\nnon-emitting states that ignore observations completely.\n\n.. code-block:: python\n\n    from leuvenmapmatching.matcher.distance import DistanceMatcher\n    from leuvenmapmatching.map.inmem import InMemMap\n    from leuvenmapmatching import visualization as mmviz\n\n    path = [(1, 0), (7.5, 0.65), (10.1, 1.9)]\n    mapdb = InMemMap(\"mymap\", graph={\n        \"A\": ((1, 0.00), [\"B\"]),\n        \"B\": ((3, 0.00), [\"A\", \"C\"]),\n        \"C\": ((4, 0.70), [\"B\", \"D\"]),\n        \"D\": ((5, 1.00), [\"C\", \"E\"]),\n        \"E\": ((6, 1.00), [\"D\", \"F\"]),\n        \"F\": ((7, 0.70), [\"E\", \"G\"]),\n        \"G\": ((8, 0.00), [\"F\", \"H\"]),\n        \"H\": ((10, 0.0), [\"G\", \"I\"]),\n        \"I\": ((10, 2.0), [\"H\"])\n    }, use_latlon=False)\n    matcher = DistanceMatcher(mapdb, max_dist_init=0.2, obs_noise=1, obs_noise_ne=10,\n                              non_emitting_states=True, only_edges=True , max_lattice_width=5)\n    states, _ = matcher.match(path)\n    nodes = matcher.path_pred_onlynodes\n\n    print(\"States\\n------\")\n    print(states)\n    print(\"Nodes\\n------\")\n    print(nodes)\n    print(\"\")\n    matcher.print_lattice_stats()\n\n    mmviz.plot_map(mapdb, matcher=matcher,\n                  show_labels=True, show_matching=True\n                  filename=\"output.png\"))\n"
  },
  {
    "path": "docs/usage/latitudelongitude.rst",
    "content": "Dealing with Latitude-Longitude\n===============================\n\nThe toolbox can deal with latitude-longitude coordinates directly.\nMap matching, however, requires a lot of repeated computations between points and latitude-longitude\ncomputations will be more expensive than Euclidean distances.\n\nThere are three different options how you can handle latitude-longitude coordinates:\n\nOption 1: Use Latitude-Longitude directly\n-----------------------------------------\n\nSet the ``use_latlon`` flag in the :class:`Map` to true.\n\nFor example to read in an OpenStreetMap file directly to a :class:`InMemMap` object:\n\n.. code-block:: python\n\n    from leuvenmapmatching.map.inmem import InMemMap\n\n    map_con = InMemMap(\"myosm\", use_latlon=True)\n\n    for entity in osmread.parse_file(osm_fn):\n        if isinstance(entity, osmread.Way) and 'highway' in entity.tags:\n            for node_a, node_b in zip(entity.nodes, entity.nodes[1:]):\n                map_con.add_edge(node_a, node_b)\n                map_con.add_edge(node_b, node_a)\n        if isinstance(entity, osmread.Node):\n            map_con.add_node(entity.id, (entity.lat, entity.lon))\n    map_con.purge()\n\n\nOption 2: Project Latitude-Longitude to X-Y\n-------------------------------------------\n\nLatitude-Longitude coordinates can be transformed two a frame with two orthogonal axis.\n\n.. code-block:: python\n\n   from leuvenmapmatching.map.inmem import InMemMap\n\n   map_con_latlon = InMemMap(\"myosm\", use_latlon=True)\n   # Add edges/nodes\n   map_con_xy = map_con_latlon.to_xy()\n\n   route_latlon = []\n   # Add GPS locations\n   route_xy = [map_con_xy.latlon2yx(latlon) for latlon in route_latlon]\n\n\nThis can also be done directly using the `pyproj <https://github.com/jswhit/pyproj>`_ toolbox.\nFor example, using the Lambert Conformal projection to project the route GPS coordinates:\n\n.. code-block:: python\n\n   import pyproj\n\n   route = [(4.67878,50.864),(4.68054,50.86381),(4.68098,50.86332),(4.68129,50.86303),(4.6817,50.86284),\n            (4.68277,50.86371),(4.68894,50.86895),(4.69344,50.86987),(4.69354,50.86992),(4.69427,50.87157),\n            (4.69643,50.87315),(4.69768,50.87552),(4.6997,50.87828)]\n   lon_0, lat_0 = route[0]\n   proj = pyproj.Proj(f\"+proj=merc +ellps=GRS80 +units=m +lon_0={lon_0} +lat_0={lat_0} +lat_ts={lat_0} +no_defs\")\n   xs, ys = [], []\n   for lon, lat in route:\n       x, y = proj(lon, lat)\n       xs.append(x)\n       ys.append(y)\n\n\nNotice that the pyproj package uses the convention to express coordinates as x-y which is\nlongitude-latitude because it is defined this way in the CRS definitions while the Leuven.MapMatching\ntoolbox follows the ISO 6709 standard and expresses coordinates as latitude-longitude. If you\nwant ``pyproj`` to use latitude-longitude you can use set the\n`axisswap option <https://proj4.org/operations/conversions/axisswap.html>`_.\n\nIf you want to define both the from and to projections:\n\n.. code-block:: python\n\n   import pyproj\n\n   route = [(4.67878,50.864),(4.68054,50.86381),(4.68098,50.86332),(4.68129,50.86303),(4.6817,50.86284),\n            (4.68277,50.86371),(4.68894,50.86895),(4.69344,50.86987),(4.69354,50.86992),(4.69427,50.87157),\n            (4.69643,50.87315),(4.69768,50.87552),(4.6997,50.87828)]\n   p1 = pyproj.Proj(proj='latlon', datum='WGS84')\n   p2 = pyproj.Proj(proj='utm', datum='WGS84')\n   xs, ys = [], []\n   for lon, lat in route:\n       x, y = pyproj.transform(lon, lat)\n       xs.append(x)\n       ys.append(y)\n\n\nOption 3: Use Latitude-Longitude as if they are X-Y points\n----------------------------------------------------------\n\nA naive solution would be to use latitude-longitude coordinate pairs as if they are X-Y coordinates.\nFor small distances, far away from the poles and not crossing the dateline, this option might work.\nBut it is not adviced.\n\nFor example, for long distances the error is quite large. In the image beneath, the blue line is the computation\nof the intersection using latitude-longitude while the red line is the intersection using Eucludean distances.\n\n.. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/latlon_mismatch_1.png?v=1\n   :alt: Latitude-Longitude mismatch\n\n.. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/latlon_mismatch_2.png?v=1\n   :alt: Latitude-Longitude mismatch detail\n"
  },
  {
    "path": "docs/usage/openstreetmap.rst",
    "content": "Map from OpenStreetMap\n======================\n\nYou can download a graph for map-matching from the OpenStreetMap.org service.\nMultiple methods exists, we illustrate two.\n\nUsing requests, osmread and gpx\n-------------------------------\n\nYou can perform map matching on a OpenStreetMap database by combing ``leuvenmapmatching``\nwith the packages ``requests``, ``osmread`` and ``gpx``.\n\nDownload a map as XML\n~~~~~~~~~~~~~~~~~~~~~\n\nYou can use the overpass-api.de service:\n\n.. code-block:: python\n\n    from pathlib import Path\n    import requests\n    xml_file = Path(\".\") / \"osm.xml\"\n    url = 'http://overpass-api.de/api/map?bbox=4.694933,50.870047,4.709256000000001,50.879628'\n    r = requests.get(url, stream=True)\n    with xml_file.open('wb') as ofile:\n        for chunk in r.iter_content(chunk_size=1024):\n            if chunk:\n                ofile.write(chunk)\n\n\nCreate graph using osmread\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nOnce we have a file containing the region we are interested in, we can select the roads we want to use\nto create a graph from. In this case we focus on 'ways' with a 'highway' tag. Those represent a variety\nof roads. For a more detailed filtering look at the\n`possible values of the highway tag <https://wiki.openstreetmap.org/wiki/Key:highway>`_.\n\n.. code-block:: python\n\n    from leuvenmapmatching.map.inmem import InMemMap\n    import osmread\n\n    map_con = InMemMap(\"myosm\", use_latlon=True, use_rtree=True, index_edges=True)\n    for entity in osmread.parse_file(str(xml_file)):\n        if isinstance(entity, osmread.Way) and 'highway' in entity.tags:\n            for node_a, node_b in zip(entity.nodes, entity.nodes[1:]):\n                map_con.add_edge(node_a, node_b)\n                # Some roads are one-way. We'll add both directions.\n                map_con.add_edge(node_b, node_a)\n        if isinstance(entity, osmread.Node):\n            map_con.add_node(entity.id, (entity.lat, entity.lon))\n    map_con.purge()\n\n\nNote that ``InMemMap`` is a simple container for a map. It is recommended to use\nyour own optimized connecter to your map dataset.\n\nIf you want to allow transitions that are not following the exact road segments you can inherit from the ``Map``\nclass and define a new class with your own transitions.\nThe transitions are defined using the ``nodes_nbrto`` and ``edges_nbrt`` methods.\n\n\nPerform map matching on an OpenStreetMap database\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nYou can create a list of latitude-longitude coordinates manually. Or read a gpx file.\n\n.. code-block:: python\n\n    from leuvenmapmatching.util.gpx import gpx_to_path\n\n    track = gpx_to_path(\"mytrack.gpx\")\n    matcher = DistanceMatcher(map_con,\n                             max_dist=100, max_dist_init=25,  # meter\n                             min_prob_norm=0.001,\n                             non_emitting_length_factor=0.75,\n                             obs_noise=50, obs_noise_ne=75,  # meter\n                             dist_noise=50,  # meter\n                             non_emitting_states=True,\n                             max_lattice_width=5)\n    states, lastidx = matcher.match(track)\n\n\nUsing osmnx and geopandas\n-------------------------\n\nAnother great library to interact with OpenStreetMap data is the `osmnx <https://github.com/gboeing/osmnx>`_ package.\nThe osmnx package can retrieve relevant data automatically, for example when given a name of a region.\nThis package is build on top of the `geopandas <http://geopandas.org>`_ package.\n\n.. code-block:: python\n\n    import osmnx\n    graph = ox.graph_from_place('Leuven, Belgium', network_type='drive', simplify=False)\n    graph_proj = ox.project_graph(graph)\n    \n    # Create GeoDataFrames (gdfs)\n    # Approach 1\n    nodes_proj, edges_proj = ox.graph_to_gdfs(graph_proj, nodes=True, edges=True)\n    for nid, row in nodes_proj[['x', 'y']].iterrows():\n        map_con.add_node(nid, (row['x'], row['y']))\n    for eid, _ in edges_proj.iterrows():\n        map_con.add_edge(eid[0], eid[1])\n    \n    # Approach 2\n    nodes, edges = ox.graph_to_gdfs(graph_proj, nodes=True, edges=True)\n    nodes_proj = nodes.to_crs(\"EPSG:3395\")\n    edges_proj = edges.to_crs(\"EPSG:3395\")\n    for nid, row in nodes_proj.iterrows():\n        map_con.add_node(nid, (row['lat'], row['lon']))\n    # We can also extract edges also directly from networkx graph\n    for nid1, nid2, _ in graph.edges:\n        map_con.add_edge(nid1, nid2)\n\n\n"
  },
  {
    "path": "docs/usage/visualisation.rst",
    "content": "Visualisation\n=============\n\nTo inspect the results, a plotting function is included.\n\nSimple plotting\n---------------\n\nTo plot the graph in a matplotlib figure use:\n\n.. code-block:: python\n\n    from leuvenmapmatching import visualization as mmviz\n    mmviz.plot_map(map_con, matcher=matcher,\n                   show_labels=True, show_matching=True, show_graph=True,\n                   filename=\"my_plot.png\")\n\nThis will result in the following figure:\n\n.. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/plot1.png?v=1\n   :alt: Plot1\n\nYou can also define your own figure by passing a matplotlib axis object:\n\n.. code-block:: python\n\n    fig, ax = plt.subplots(1, 1)\n    mmviz.plot_map(map_con, matcher=matcher,\n                   ax=ax,\n                   show_labels=True, show_matching=True, show_graph=True,\n                   filename=\"my_plot.png\")\n\n\nPlotting with an OpenStreetMap background\n-----------------------------------------\n\nThe plotting function also supports a link with the ``smopy`` package.\nSet the ``use_osm`` argument to true and pass a map that is defined with\nlatitude-longitude (thus ``use_latlon=True``).\n\nYou can set ``zoom_path`` to true to only see the relevant part and not the\nentire map that is available in the map. Alternatively you can also set the\nbounding box manually using the ``bb`` argument.\n\n.. code-block:: python\n\n    mm_viz.plot_map(map_con, matcher=matcher,\n                    use_osm=True, zoom_path=True,\n                    show_labels=False, show_matching=True, show_graph=False,\n                    filename=\"my_osm_plot.png\")\n\n\nThis will result in the following figure:\n\n.. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/plot2.png?v=1\n   :alt: Plot2\n\nOr when some GPS points are missing in the track, the matching is more\nvisible as the matched route deviates from the straight line between two\nGPS points:\n\n.. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/plot3.png?v=1\n   :alt: Plot3\n"
  },
  {
    "path": "leuvenmapmatching/__init__.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching\n~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2022 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport logging\nfrom . import map, matcher, util\n# visualization is not loaded by default (avoid loading unnecessary dependencies such as matplotlib).\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n__version__ = '1.1.4'\n\n"
  },
  {
    "path": "leuvenmapmatching/map/__init__.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.map\n~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\n"
  },
  {
    "path": "leuvenmapmatching/map/base.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.map.base\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBase Map class.\n\nTo be used in a Matcher object, the following functions need to be defined:\n\n- ``edges_closeto``\n- ``nodes_closeto``\n- ``nodes_nbrto``\n- ``edges_nbrto``\n\nFor visualiation purposes the following methods need to be implemented:\n\n- ``bb``\n- ``labels``\n- ``size``\n- ``coordinates``\n- ``node_coordinates``\n- ``all_edges``\n- ``all_nodes``\n\n:author: Wannes Meert\n:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\n\nfrom abc import abstractmethod\nimport logging\nMYPY = False\nif MYPY:\n    from typing import Tuple, Union, List\n    LabelType = Union[int, str]\n    LocType = Tuple[float, float]\n\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n\nclass BaseMap(object):\n    \"\"\"Abstract class for a Map.\"\"\"\n\n    def __init__(self, name, use_latlon=True):\n        \"\"\"Simple database wrapper/stub.\"\"\"\n        self.name = name\n        self._use_latlon = None\n        self.distance = None\n        self.distance_point_to_segment = None\n        self.distance_segment_to_segment = None\n        self.box_around_point = None\n        self.use_latlon = use_latlon\n\n    @property\n    def use_latlon(self):\n        return self._use_latlon\n\n    @use_latlon.setter\n    def use_latlon(self, value):\n        self._use_latlon = value\n        if self._use_latlon:\n            from ..util import dist_latlon as dist_lib\n        else:\n            from ..util import dist_euclidean as dist_lib\n        self.distance = dist_lib.distance\n        self.distance_point_to_segment = dist_lib.distance_point_to_segment\n        self.distance_segment_to_segment = dist_lib.distance_segment_to_segment\n        self.box_around_point = dist_lib.box_around_point\n        self.lines_parallel = dist_lib.lines_parallel\n\n    @abstractmethod\n    def bb(self):\n        \"\"\"Bounding box.\n\n        :return: (lat_min, lon_min, lat_max, lon_max)\n        \"\"\"\n\n    @abstractmethod\n    def labels(self):\n        \"\"\"Labels of all nodes.\"\"\"\n\n    @abstractmethod\n    def size(self):\n        \"\"\"Number of nodes.\"\"\"\n\n    @abstractmethod\n    def node_coordinates(self, node_key):\n        \"\"\"Coordinates for given node key.\"\"\"\n\n    @abstractmethod\n    def edges_closeto(self, loc, max_dist=None, max_elmt=None):\n        \"\"\"Find edges close to a certain location.\n\n        :param loc: Latitude, Longitude\n        :param max_dist: Maximal distance that returned nodes can be from lat-lon\n        :param max_elmt: Maximal number of elements returned after sorting according to distance.\n        :return: list[tuple[dist, label, loc]]\n        \"\"\"\n        return None\n\n    @abstractmethod\n    def nodes_closeto(self, loc, max_dist=None, max_elmt=None):\n        \"\"\"Find nodes close to a certain location.\n\n        :param loc: Latitude, Longitude\n        :param max_dist: Maximal distance that returned nodes can be from lat-lon\n        :param max_elmt: Maximal number of elements returned after sorting according to distance.\n        :return: list[tuple[dist, label, loc]]\n        \"\"\"\n        return None\n\n    @abstractmethod\n    def nodes_nbrto(self, node):\n        # type: (BaseMap, LabelType) -> List[Tuple[LabelType, LocType]]\n        \"\"\"Return all nodes that are linked to ``node``.\n\n        :param node: Node identifier\n        :return: list[tuple[label, loc]]\n        \"\"\"\n        return []\n\n    def edges_nbrto(self, edge):\n        # type: (BaseMap, Tuple[LabelType, LabelType]) -> List[Tuple[LabelType, LocType, LabelType, LocType]]\n        \"\"\"Return all edges that are linked to ``edge``.\n\n        Defaults to ``nodes_nbrto``.\n\n        :param edge: Edge identifier\n        :return: list[tuple[label1, label2, loc1, loc2]]\n        \"\"\"\n        results = []\n        l1, l2 = edge\n        p2 = self.node_coordinates(l2)\n        for l3, p3 in self.nodes_nbrto(l2):\n            results.append((l2, p2, l3, p3))\n        return results\n\n    @abstractmethod\n    def all_nodes(self, bb=None):\n        \"\"\"All node keys and coordinates.\n\n        :return: [(key, (lat, lon))]\n        \"\"\"\n\n    @abstractmethod\n    def all_edges(self, bb=None):\n        \"\"\"All edges.\n\n        :return: [(key_a, loc_a, key_b, loc_b)]\n        \"\"\"\n"
  },
  {
    "path": "leuvenmapmatching/map/inmem.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.map.inmem\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nSimple in-memory map representation. Not suited for production purposes.\nWrite your own map class that connects to your map (e.g. a database instance).\n\n:author: Wannes Meert\n:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport logging\nimport time\nfrom pathlib import Path\nimport pickle\nfrom functools import partial\ntry:\n    import rtree\nexcept ImportError:\n    rtree = None\ntry:\n    import pyproj\nexcept ImportError:\n    pyproj = None\ntry:\n    import tqdm\nexcept ImportError:\n    tqdm = None\n\n\nfrom .base import BaseMap\n\n\nMYPY = False\nif MYPY:\n    from typing import Optional, Set, Tuple, Dict, Union\n    LabelType = Union[int, str]\n    LocType = Tuple[float, float]\n    EdgeType = Tuple[LabelType, LabelType]\n\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n\nclass InMemMap(BaseMap):\n    def __init__(self, name, use_latlon=True, use_rtree=False, index_edges=False,\n                 crs_lonlat=None, crs_xy=None, graph=None, linked_edges=None, dir=None, deserializing=False):\n        \"\"\"In-memory representation of a map.\n\n        This is a simple database-like object to perform experiments with map matching.\n        For production purposes it is recommended to use your own derived\n        class (e.g. to connect to your database instance).\n\n        This class supports:\n\n        - Indexing using rtrees to allow for fast searching of points on the map.\n          When using the rtree index, only integer numbers are allowed as node labels.\n        - Serializing to write and read from files.\n        - Projecting points to a different frame (e.g. GPS to Lambert)\n\n        :param name: Map name (mandatory)\n        :param use_latlon: The locations represent latitude-longitude pairs, otherwise y-x coordinates\n            are assumed.\n        :param use_rtree: Build an rtree index to quickly search for locations.\n        :param index_edges: Build an index for the edges in the map instead of the vertices.\n        :param crs_lonlat: Coordinate reference system for the latitude-longitude coordinates.\n        :param crs_xy: Coordiante reference system for the y-x coordinates.\n        :param graph: Initial graph of form Dict[label, Tuple[Tuple[y,x], List[neighbor]]]]\n        :param dir: Directory where to serialize to. If given, the rtree index structure will be written\n            to a file immediately.\n        :param deserializing: Internal variable to indicate that the object is being build from a file.\n        \"\"\"\n        super(InMemMap, self).__init__(name, use_latlon=use_latlon)\n        self.dir = None if dir is None else Path(dir)\n        self.index_edges = index_edges\n        self.graph = dict() if graph is None else graph\n        self.rtree = None\n        self.use_rtree = use_rtree\n        if self.use_rtree:\n            self.setup_index(deserializing=deserializing)\n\n        self.crs_lonlat = 'EPSG:4326' if crs_lonlat is None else crs_lonlat  # GPS\n        self.crs_xy = 'EPSG:3395' if crs_xy is None else crs_xy  # Mercator projection\n        if pyproj:\n            # proj_lonlat = pyproj.Proj(self.crs_lonlat, preserve_units=True)\n            # proj_xy = pyproj.Proj(self.crs_xy, preserve_units=True)\n            # self.lonlat2xy = partial(pyproj.transform, proj_lonlat, proj_xy)\n            # self.xy2lonlat = partial(pyproj.transform, proj_xy, proj_lonlat)\n            tr_lonlat2xy = pyproj.Transformer.from_crs(self.crs_lonlat, self.crs_xy)\n            self.lonlat2xy = tr_lonlat2xy.transform\n            tr_xy2lonlat = pyproj.Transformer.from_crs(self.crs_xy, self.crs_lonlat)\n            self.xy2lonlat = tr_xy2lonlat.transform\n        else:\n            def pyproj_notfound(*_args, **_kwargs):\n                raise Exception(\"pyproj package not found\")\n            self.lonlat2xy = pyproj_notfound\n            self.xy2lonlat = pyproj_notfound\n\n        self.linked_edges = linked_edges  # type: Optional[Dict[EdgeType, Set[Tuple[EdgeType]]]]\n        self.vertex_label_map = None\n\n    def vertex_label_to_int(self, label, create=False):\n        if type(label) is int:\n            return label\n        if self.vertex_label_map is None:\n            if not create:\n                return label\n            self.vertex_label_map = dict()\n        if label in self.vertex_label_map:\n            new_label = self.vertex_label_map[label]\n        else:\n            new_label = len(self.vertex_label_map)\n            self.vertex_label_map[label] = new_label\n        return new_label\n\n    def vertices_labels_to_int(self):\n        graph = dict()\n        for label, (loc, nbrs) in self.graph.items():\n            new_label = self.vertex_label_to_int(label, create=True)\n            new_nbrs = [self.vertex_label_to_int(nbr, create=True) for nbr in nbrs]\n            graph[new_label] = (loc, new_nbrs)\n        self.graph = graph\n\n    def serialize(self):\n        \"\"\"Create a serializable data structure.\"\"\"\n        data = {\n            \"name\": self.name,\n            \"graph\": self.graph,\n            \"use_latlon\": self.use_latlon,\n            \"use_rtree\": self.use_rtree,\n            \"index_edges\": self.index_edges,\n            \"crs_lonlat\": self.crs_lonlat,\n            \"crs_xy\": self.crs_xy,\n            \"linked_edges\": self.linked_edges\n        }\n        if self.dir is not None:\n            data[\"dir\"] = self.dir\n        return data\n\n    @classmethod\n    def deserialize(cls, data):\n        \"\"\"Create a new instance from a dictionary.\"\"\"\n        nmap = cls(data[\"name\"], dir=data.get(\"dir\", None),\n                   use_latlon=data[\"use_latlon\"], use_rtree=data[\"use_rtree\"],\n                   index_edges=data[\"index_edges\"],\n                   crs_lonlat=data.get(\"crs_lonlat\", None), crs_xy=data.get(\"crs_xy\", None),\n                   graph=data.get(\"graph\", None), linked_edges=data.get(\"linked_edges\", None),\n                   deserializing=True)\n        return nmap\n\n    def dump(self):\n        \"\"\"Serialize map using pickle.\n\n        All files will be saved to the `dir` directory using the `name` as filename.\n        \"\"\"\n        if self.dir is None:\n            logger.error(f\"No directory set where to save (see InMemMap.__init__)\")\n            return\n        filename = self.dir / (self.name + \".pkl\")\n        with filename.open(\"wb\") as ofile:\n            pickle.dump(self.serialize(), ofile)\n        logger.debug(f\"Saved map to {filename}\")\n        if self.rtree:\n            rtree_fn = self.rtree_fn()\n            if rtree_fn is not None:\n                self.rtree.close()\n                self.rtree = rtree.index.Index(str(rtree_fn))\n\n    @classmethod\n    def from_pickle(cls, filename):\n        \"\"\"Deserialize map using pickle to the given filename.\"\"\"\n        filename = Path(filename)\n        with filename.open(\"rb\") as ifile:\n            data = pickle.load(ifile)\n        nmap = cls.deserialize(data)\n        return nmap\n\n    def bb(self):\n        \"\"\"Bounding box.\n\n        :return: (lat_min, lon_min, lat_max, lon_max) or (y_min, x_min, y_max, x_max)\n        \"\"\"\n        if self.use_rtree:\n            lat_min, lon_min, lat_max, lon_max = self.rtree.bounds\n        else:\n            glat, glon = zip(*[t[0] for t in self.graph.values()])\n            lat_min, lat_max = min(glat), max(glat)\n            lon_min, lon_max = min(glon), max(glon)\n        return lat_min, lon_min, lat_max, lon_max\n\n    def labels(self):\n        \"\"\"All labels.\"\"\"\n        return self.graph.keys()\n\n    def size(self):\n        return len(self.graph)\n\n    def node_coordinates(self, node_key):\n        \"\"\"Get the coordinates of the given node.\n\n        :param node_key: Node label/key\n        :return: (lat, lon)\n        \"\"\"\n        return self.graph[node_key][0]\n\n    def add_node(self, node, loc):\n        \"\"\"Add new node to the map.\n\n        :param node: label\n        :param loc: (lat, lon) or (y, x)\n        \"\"\"\n        if node in self.graph:\n            if self.graph[node][0] is None:\n                self.graph[node] = (loc, self.graph[node][1])\n        else:\n            self.graph[node] = (loc, [])\n        if self.use_rtree and self.rtree is not None and not self.index_edges:\n            if type(node) is not int:\n                raise Exception(f\"Rtree index only supports integer keys for vertices\")\n            self.rtree.insert(node, (loc[0], loc[1], loc[0], loc[1]))\n\n    def del_node(self, node):\n        if node not in self.graph:\n            return\n        if self.rtree:\n            data = self.graph[node]\n            loc = data[0]\n            self.rtree.delete(node, (loc[0], loc[1], loc[0], loc[1]))\n        del self.graph[node]\n\n    def add_edge(self, node_a, node_b):\n        \"\"\"Add new edge to the map.\n\n        :param node_a: Label for the node that is the start of the edge\n        :param node_b: Label for the node that is the end of the edge\n        \"\"\"\n        if node_a not in self.graph:\n            raise ValueError(f\"Add {node_a} first as node\")\n        if node_b not in self.graph:\n            raise ValueError(f\"Add {node_b} first as node\")\n        if node_b not in self.graph[node_a][1]:\n            self.graph[node_a][1].append(node_b)\n        if self.use_rtree and self.rtree is not None and self.index_edges:\n            if type(node_a) is not int or type(node_b) is not int:\n                raise Exception(f\"Rtree index only supports integer keys for vertices\")\n            loc_a = self.graph[node_a][0]\n            loc_b = self.graph[node_b][0]\n            bb = (min(loc_a[0], loc_b[0]), min(loc_a[1], loc_b[1]),  # y_min, x_min\n                  max(loc_a[0], loc_b[0]), max(loc_a[1], loc_b[1]))  # y_max, x_max\n            self.rtree.insert(node_a, bb)\n            # self.rtree.insert(node_b, bb)\n\n    def _items_in_bb(self, bb):\n        if self.rtree is not None:\n            node_idxs = self.rtree.intersection(bb)\n            for key in node_idxs:\n                yield (key, self.graph[key])\n        else:\n            lat_min, lon_min, lat_max, lon_max = bb\n            for key, value in self.graph.items():\n                ((lat, lon), nbrs) = value\n                if lat_min <= lat <= lat_max and lon_min <= lon <= lon_max:\n                    yield (key, value)\n\n    def all_edges(self, bb=None):\n        \"\"\"Return all edges.\n\n        :param bb: Bounding box\n        :return: (key_a, loc_a, nbr, loc_b)\n        \"\"\"\n        if bb is None:\n            keyvals = self.graph.items()\n        else:\n            keyvals = self._items_in_bb(bb)\n        for key_a, (loc_a, nbrs) in keyvals:\n            if loc_a is not None:\n                for nbr in nbrs:\n                    try:\n                        loc_b, _ = self.graph[nbr]\n                        if loc_b is not None:\n                            yield (key_a, loc_a, nbr, loc_b)\n                    except KeyError:\n                        # print(\"Node not found: {}\".format(nbr))\n                        pass\n\n    def all_nodes(self, bb=None):\n        \"\"\"Return all nodes.\n\n        :param bb: Bounding box\n        :return:\n        \"\"\"\n        if bb is None:\n            keyvals = self.graph.items()\n        else:\n            keyvals = self._items_in_bb(bb)\n        for key_a, (loc_a, nbrs) in keyvals:\n            if loc_a is not None:\n                yield key_a, loc_a\n\n    def purge(self):\n        cnt_noloc = 0\n        cnt_noedges = 0\n        remove = []\n        for node in self.graph.keys():\n            if self.graph[node][0] is None:\n                cnt_noloc += 1\n                remove.append(node)\n                # print(\"No location for node {}\".format(node))\n            elif len(self.graph[node][1]) == 0:\n                cnt_noedges += 1\n                remove.append(node)\n        for node in remove:\n            self.del_node(node)\n        logger.debug(\"Removed {} nodes without location\".format(cnt_noloc))\n        logger.debug(\"Removed {} nodes without edges\".format(cnt_noedges))\n\n    def rtree_size(self):\n        bb = self.rtree.bounds\n        if bb[0] < bb[2] and bb[1] < bb[3]:\n            rtree_size = self.rtree.count(bb)\n        else:\n            rtree_size = 0\n        return rtree_size\n\n    def rtree_fn(self):\n        rtree_fn = None\n        if self.dir is not None:\n            rtree_fn = self.dir / self.name\n        return rtree_fn\n\n    def setup_index(self, force=False, deserializing=False):\n        if not self.use_rtree:\n            return\n        if self.rtree is not None and not force:\n            return\n        if rtree is None:\n            raise Exception(\"rtree package not found\")\n\n        rtree_fn = self.rtree_fn()\n        args = []\n        if deserializing and (rtree_fn is None or not rtree_fn.exists()):\n            deserializing = False\n\n        if self.graph and len(self.graph) > 0 and not deserializing:\n            if self.index_edges:\n                logger.debug(\"Generator to index edges\")\n\n                def generator_function():\n                    for label, data in self.graph.items():\n                        lat_min, lon_min = data[0]\n                        lat_max, lon_max = lat_min, lon_min\n                        for idx in data[1]:\n                            olat, olon = self.graph[idx][0]\n                            lat_min = min(lat_min, olat)\n                            lat_max = max(lat_max, olat)\n                            lon_min = min(lon_min, olon)\n                            lon_max = max(lon_max, olon)\n                        if type(label) is not int:\n                            raise Exception(f\"Rtree index only supports integer keys for vertices\")\n                        yield (label, (lat_min, lon_min, lat_max, lon_max), None)\n            else:\n                logger.debug(\"Generator to index nodes\")\n\n                def generator_function():\n                    for label, data in self.graph.items():\n                        lat, lon = data[0]\n                        if type(label) is not int:\n                            raise Exception(f\"Rtree index only supports integer keys for vertices\")\n                        yield (label, (lat, lon, lat, lon), None)\n            args.append(generator_function())\n\n        t_start = time.time()\n        if self.dir is not None:\n            # props = rtree.index.Property()\n            # if force:\n            #     props.overwrite = True\n            logger.debug(f\"Creating new file-based rtree index ({rtree_fn}) ...\")\n            args.insert(0, str(rtree_fn))\n        elif deserializing:\n            raise Exception(\"Cannot deserialize, no directory given\")\n        else:\n            logger.debug(f\"Creating new in-memory rtree index (args={args}) ...\")\n        self.rtree = rtree.index.Index(*args)\n        t_delta = time.time() - t_start\n        logger.debug(f\"... done: rtree size = {self.rtree_size()}, time = {t_delta} sec\")\n\n    def fill_index(self):\n        if not self.use_rtree or self.rtree is None:\n            return\n\n        for label, data in self.graph.items():\n            loc = data[0]\n            self.rtree.insert(label, (loc[1], loc[0], loc[1], loc[0]))\n        logger.debug(f\"After filling rtree, size = {self.rtree_size()}\")\n\n    def to_xy(self, name=None, use_rtree=None):\n        \"\"\"Create a map that uses a projected XY representation on which Euclidean distances\n        can be used.\n        \"\"\"\n        if not self.use_latlon:\n            return self\n        if name is None:\n            name = self.name + \"_xy\"\n        if use_rtree is None:\n            use_rtree = self.use_rtree\n\n        ngraph = dict()\n        for label, row in self.graph.items():\n            lat, lon = row[0]\n            x, y = self.lonlat2xy(lon, lat)\n            ngraph[label] = ((y, x), row[1])\n        nmap = self.__class__(name, dir=self.dir, graph=ngraph,\n                              use_latlon=False, use_rtree=use_rtree, index_edges=self.index_edges,\n                              crs_xy=self.crs_xy, crs_lonlat=self.crs_lonlat)\n\n        return nmap\n\n    def latlon2xy(self, lat, lon):\n        x, y = self.lonlat2xy(lon, lat)\n        return x, y\n\n    def latlon2yx(self, lat, lon):\n        x, y = self.lonlat2xy(lon, lat)\n        return y, x\n\n    def xy2latlon(self, x, y):\n        lon, lat = self.xy2lonlat(x, y)\n        return lat, lon\n\n    def yx2latlon(self, y, x):\n        lon, lat = self.xy2lonlat(x, y)\n        return lat, lon\n\n    def nodes_closeto(self, loc, max_dist=None, max_elmt=None):\n        \"\"\"Return all nodes close to the given location.\n\n        :param loc: Location\n        :param max_dist: Maximal distance from the location\n        :param max_elmt: Return only the most nearby nodes\n        \"\"\"\n        t_start = time.time()\n        lat, lon = loc[:2]\n        lat_b, lon_l, lat_t, lon_r = self.box_around_point((lat, lon), max_dist)\n        bb = (lat_b, lon_l,  # y_min, x_min\n              lat_t, lon_r)  # y_max, x_max\n        if self.rtree is not None and max_dist is not None:\n            logger.debug(f\"Search closeby nodes to {loc}, bb={bb}\")\n            nodes = self.rtree.intersection(bb)\n        else:\n            logger.warning(\"Searching closeby nodes with linear search, use an index and set max_dist\")\n            if max_dist is not None:\n                nodes = (key for key, _ in self._items_in_bb(self.box_around_point((lat, lon), max_dist)))\n            else:\n                nodes = self.graph.keys()\n        t_delta_search = time.time() - t_start\n        t_start = time.time()\n        results = []\n        for label in nodes:\n            oloc, nbrs = self.graph[label]\n            dist = self.distance(loc, oloc)\n            if dist < max_dist:\n                results.append((dist, label, oloc))\n        results.sort()\n        t_delta_dist = time.time() - t_start\n        logger.debug(f\"Found {len(results)} closeby nodes \"\n                     f\"in {t_delta_search} sec and computed distances in {t_delta_dist} sec\")\n        if max_elmt is not None:\n            results = results[:max_elmt]\n        return results\n\n    def edges_closeto(self, loc, max_dist=None, max_elmt=None):\n        \"\"\"Return all nodes that are on an edge that is close to the given location.\n\n        :param loc: Location\n        :param max_dist: Maximal distance from the location\n        :param max_elmt: Return only the most nearby nodes\n        \"\"\"\n        t_start = time.time()\n        lat, lon = loc[:2]\n        if self.rtree is not None and max_dist is not None and self.index_edges:\n            lat_b, lon_l, lat_t, lon_r = self.box_around_point((lat, lon), max_dist)\n            bb = (lat_b, lon_l,  # y_min, x_min\n                  lat_t, lon_r)  # y_max, x_max\n            logger.debug(f\"Search closeby edges to {loc}, bb={bb}\")\n            nodes = self.rtree.intersection(bb)\n        else:\n            if self.rtree is not None and max_dist is not None and not self.index_edges:\n                logger.warning(\"Index is built for nodes, not for edges, set the index_edges argument to true\")\n            logger.warning(\"Searching closeby nodes with linear search, use an index and set max_dist\")\n            if max_dist is not None:\n                bb = self.box_around_point((lat, lon), max_dist)\n                nodes = (key for key, _ in self._items_in_bb(bb))\n            else:\n                nodes = self.graph.keys()\n        t_delta_search = time.time() - t_start\n        t_start = time.time()\n        results = []\n        for label in nodes:\n            oloc, nbrs = self.graph[label]\n            for nbr in nbrs:\n                if label == nbr:\n                    continue\n                nbr_data = self.graph[nbr]\n                dist, pi, ti = self.distance_point_to_segment(loc, oloc, nbr_data[0])\n                # print(f\"label={label}/{oloc}, nbr={nbr}/{nbr_data[0]}   -- loc={loc}  -> {dist}, {pi}, {ti}\")\n                if dist < max_dist:\n                    results.append((dist, label, oloc, nbr, nbr_data[0], pi, ti))\n        results.sort()\n        t_delta_dist = time.time() - t_start\n        logger.debug(f\"Found {len(results)} closeby edges \"\n                     f\"in {t_delta_search} sec and computed distances in {t_delta_dist} sec\")\n        if max_elmt is not None:\n            results = results[:max_elmt]\n        return results\n\n    def nodes_nbrto(self, node):\n        results = []\n        if node not in self.graph:\n            return results\n        loc_node, nbrs = self.graph[node]\n        for nbr_label in nbrs + [node]:\n            try:\n                loc_nbr = self.graph[nbr_label][0]\n                if loc_nbr is not None:\n                    results.append((nbr_label, loc_nbr))\n            except KeyError:\n                pass\n        return results\n\n    def edges_nbrto(self, edge):\n        results = []\n        l1, l2 = edge\n        p1 = self.node_coordinates(l1)\n        p2 = self.node_coordinates(l2)\n        # Edges that connect at end of this edge\n        for l3, p3 in self.nodes_nbrto(l2):\n            results.append((l2, p2, l3, p3))\n        # Edges that are in parallel and close\n        if self.linked_edges:\n            for (l3, l4) in self.linked_edges.get(edge, []):\n                p3 = self.node_coordinates(l3)\n                p4 = self.node_coordinates(l4)\n                results.append((l3, p3, l4, p4))\n        return results\n\n    def find_duplicates(self, func=None):\n        \"\"\"Find entries with identical locations.\"\"\"\n        cnt = 0\n        for label, data in self.graph.items():\n            lat, lon = data[0]\n            idxs = list(self.rtree.nearest((lat, lon, lat, lon), num_results=1))\n            idxs.remove(label)\n            if len(idxs) > 0:\n                # logger.info(f\"Found doubles for {label}: {idxs}\")\n                if func:\n                    func(label, idxs)\n        logger.info(f\"Found {cnt} doubles\")\n\n    def connect_parallelroads(self, dist=0.5, bb=None):\n        if self.rtree is None or not self.index_edges:\n            logger.error(\"Finding parallel roads requires and edge-based index\")\n            return\n        self.linked_edges = {}\n        it = self.all_edges(bb=bb)\n        if tqdm:\n            it = tqdm.tqdm(list(it))\n        for key_a, loc_a, key_b, loc_b in it:\n            bb2 = [min(loc_a[0], loc_b[0]), min(loc_a[1], loc_b[1]),\n                   max(loc_a[0], loc_b[0]), max(loc_a[1], loc_b[1])]\n            for key_c, loc_c, key_d, loc_d in self.all_edges(bb=bb2):\n                if key_a == key_c or key_a == key_d or key_b == key_c or key_b == key_d:\n                    continue\n                # print(f\"Test: ({key_a},{key_b}) - ({key_c},{key_d})\")\n                if self.lines_parallel(loc_a, loc_b, loc_c, loc_d, d=dist):\n                    # print(f\"Parallel: ({key_a},{key_b}) - ({key_c},{key_d})\")\n                    key = (key_a, key_b)\n                    if key in self.linked_edges:\n                        self.linked_edges[key].add((key_c, key_d))\n                    else:\n                        self.linked_edges[key] = {(key_c, key_d)}\n        logger.debug(f\"Linked {len(self.linked_edges)} edges\")\n\n    def print_stats(self):\n        print(\"Graph\\n-----\")\n        print(\"Nodes: {}\".format(len(self.graph)))\n\n    def __str__(self):\n        # s = \"\"\n        # for label, (loc, nbrs, _) in self.graph.items():\n        #     s += f\"{label:<10} - ({loc[0]:10.4f}, {loc[1]:10.4f})\\n\"\n        # return s\n        return f\"InMemMap({self.name}, size={self.size()})\"\n"
  },
  {
    "path": "leuvenmapmatching/map/sqlite.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.map.sqlite\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nMap representation based on a sqlite database. Not optimized for production purposes.\n\n:author: Wannes Meert\n:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport sqlite3\nimport tempfile\nimport logging\nimport time\nfrom pathlib import Path\nimport pickle\nfrom functools import partial\ntry:\n    import pyproj\nexcept ImportError:\n    pyproj = None\ntry:\n    import tqdm\nexcept ImportError:\n    tqdm = None\n\n\nfrom .base import BaseMap\n\n\nMYPY = False\nif MYPY:\n    from typing import Optional, Set, Tuple, Dict, Union\n    LabelType = Union[int, str]\n    LocType = Tuple[float, float]\n    EdgeType = Tuple[LabelType, LabelType]\n\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n\nclass SqliteMap(BaseMap):\n    def __init__(self, name, use_latlon=True,\n                 crs_lonlat=None, crs_xy=None, dir=None, deserializing=False):\n        \"\"\"Store a map as a SQLite instance.\n\n        This class supports:\n\n        - Indexing using rtrees to allow for fast searching of points on the map.\n          When using the rtree index, only integer numbers are allowed as node labels.\n        - Serializing to write and read from files.\n        - Projecting points to a different frame (e.g. GPS to Lambert)\n\n        :param name: Name of database file\n        :param use_latlon: The locations represent latitude-longitude pairs, otherwise y-x coordinates\n            are assumed.\n        :param crs_lonlat: Coordinate reference system for the latitude-longitude coordinates.\n        :param crs_xy: Coordiante reference system for the y-x coordinates.\n        :param dir: Directory where to serialize to. If not given, a temporary location will be used.\n        :param deserializing: Internal variable to indicate that the object is being build from a file.\n        \"\"\"\n        super(SqliteMap, self).__init__(name, use_latlon=use_latlon)\n        self.dir = Path(tempfile.gettempdir()) if dir is None else Path(dir)\n        name = Path(name)\n        suffix = name.suffix\n        if suffix == '':\n            name = name.with_suffix('.sqlite')\n        self.db_fn = self.dir / name\n        if deserializing and not self.db_fn.exists():\n            raise Exception(f\"File not found: {self.db_fn}\")\n        logger.debug(f\"Opening database: {self.db_fn}\")\n        try:\n            self.db = sqlite3.connect(str(self.db_fn))\n        except Exception as exc:\n            raise Exception(f'Problem with database: {self.db_fn}') from exc\n        self.crs_lonlat = crs_lonlat\n        self.crs_xy = crs_xy\n        self.use_latlon = use_latlon\n\n        if deserializing:\n            self.read_properties()\n        else:\n            self.create_db()\n\n        if self.crs_lonlat is None:\n            self.crs_lonlat = 'EPSG:4326'  # GPS\n        if self.crs_xy is None:\n            self.crs_xy = 'EPSG:3395'  # Mercator projection\n\n        self.save_properties()\n\n        if pyproj:\n            proj_lonlat = pyproj.Proj(self.crs_lonlat, preserve_units=True)\n            proj_xy = pyproj.Proj(self.crs_xy, preserve_units=True)\n            self.lonlat2xy = partial(pyproj.transform, proj_lonlat, proj_xy)\n            self.xy2lonlat = partial(pyproj.transform, proj_xy, proj_lonlat)\n        else:\n            def pyproj_notfound(*_args, **_kwargs):\n                raise Exception(\"pyproj package not found\")\n            self.lonlat2xy = pyproj_notfound\n            self.xy2lonlat = pyproj_notfound\n\n    def read_properties(self):\n        c = self.db.cursor()\n        for row in c.execute(\"SELECT key, value FROM properties;\"):\n            key, value = row[0], pickle.loads(row[1])\n            self.__dict__[key] = value\n\n    def save_properties(self):\n        c = self.db.cursor()\n        q = \"INSERT INTO properties (key, value) VALUES (?, ?)\"\n        v = [('name', pickle.dumps(self.name)),\n             ('use_latlon', pickle.dumps(self.use_latlon)),\n             ('crs_lonlat', pickle.dumps(self.crs_lonlat)),\n             ('crs_xy', pickle.dumps(self.crs_xy))]\n        c.executemany(q, v)\n        self.db.commit()\n\n    def create_db(self):\n        logger.debug(\"Cleaning database file and creating new tables\")\n        c = self.db.cursor()\n        c.execute(\"DROP INDEX IF EXISTS edges_from_index\")\n        c.execute(\"DROP INDEX IF EXISTS close_edges_index\")\n        c.execute(\"DROP TABLE IF EXISTS nodes_index\")\n        c.execute(\"DROP TABLE IF EXISTS nodes\")\n        c.execute(\"DROP TABLE IF EXISTS edges_index\")\n        c.execute(\"DROP TABLE IF EXISTS edges\")\n        c.execute(\"DROP TABLE IF EXISTS close_edges\")\n        c.execute(\"DROP TABLE IF EXISTS properties\")\n        self.db.commit()\n\n        # Create tables\n        q = (\"CREATE VIRTUAL TABLE nodes_index USING rtree(\\n\"\n             \"id,              -- Integer primary key\\n\"\n             \"minX, maxX,      -- Minimum and maximum X coordinate\\n\"\n             \"minY, maxY       -- Minimum and maximum Y coordinate\\n\"\n             \")\")\n        c.execute(q)\n        q = (\"CREATE TABLE nodes(\\n\"\n             \"id INTEGER PRIMARY KEY,\\n\"\n             \"x REAL,\\n\"\n             \"y REAL\\n\"\n             \")\")\n        c.execute(q)\n        q = (\"CREATE VIRTUAL TABLE edges_index USING rtree(\\n\"\n             \"id,              -- Integer primary key\\n\"\n             \"minX, maxX,      -- Minimum and maximum X coordinate\\n\"\n             \"minY, maxY       -- Minimum and maximum Y coordinate\\n\"\n             \")\")\n        c.execute(q)\n        q = (\"CREATE TABLE edges(\\n\"\n             \"id INTEGER PRIMARY KEY,\\n\"\n             \"path INTEGER,\\n\"  # Not necessarily unique, a pathway id can consist of multiple edges\n             \"pathnum INTEGER,\\n\"\n             \"id1 INTEGER,\\n\"  # node 1\n             \"id2 INTEGER,\\n\"  # node 2\n             \"speed REAL,\\n\"  # speed m/s\n             \"type INTEGER\\n\"  # extra field\n             \")\")\n        c.execute(q)\n        q = (\"CREATE TABLE close_edges(\\n\"\n             \"id1 INTEGER,\\n\"  # edge 1\n             \"id2 INTEGER\\n\"  # edge 2\n             \")\")\n        c.execute(q)\n        q = (\"CREATE TABLE properties(\\n\"\n             \"key TEXT,\\n\"\n             \"value BLOB\\n\"\n             \")\")\n        c.execute(q)\n        q = \"CREATE INDEX edges_from_index ON edges(id1)\"\n        c.execute(q)\n        q = \"CREATE INDEX close_edges_index ON close_edges(id1)\"\n        c.execute(q)\n        self.db.commit()\n\n    @classmethod\n    def from_file(cls, filename):\n        \"\"\"Read from an existing file.\"\"\"\n        filename = Path(filename).with_suffix('')\n        nmap = cls(filename.name, dir=filename.parent, deserializing=True)\n        return nmap\n\n    def bb(self):\n        \"\"\"Bounding box.\n\n        :return: (lat_min, lon_min, lat_max, lon_max) or (y_min, x_min, y_max, x_max)\n        \"\"\"\n        c = self.db.cursor()\n        c.execute('SELECT min(minX), max(minX), min(maxX), max(maxX) FROM nodes_index;')\n        lon_min, lon_max, lat_min, lat_max = c.fetchone()\n        return lat_min, lon_min, lat_max, lon_max\n\n    def labels(self):\n        \"\"\"All labels.\"\"\"\n        c = self.db.cursor()\n        c.execute('SELECT id FROM nodes;')\n        result = [row[0] for row in c.fetchall()]\n        return result\n\n    def size(self):\n        c = self.db.cursor()\n        c.execute('SELECT count(*) FROM nodes')\n        result = c.fetchone()[0]\n        return result\n\n    def node_coordinates(self, node_key):\n        \"\"\"Get the coordinates of the given node.\n\n        :param node_key: Node label/key\n        :return: (lat, lon)\n        \"\"\"\n        c = self.db.cursor()\n        c.execute('SELECT y, x FROM nodes WHERE id = ?', (node_key, ))\n        result = c.fetchone()\n        if result is None:\n            raise Exception(f\"No coordinates found for node {node_key}\")\n        return result\n\n    def add_node(self, node, loc, ignore_doubles=False, no_index=False, no_commit=False):\n        \"\"\"Add new node to the map.\n\n        :param node: label\n        :param loc: (lat, lon) or (y, x)\n        :param ignore_doubles: When trying to add the same node, ignore it\n        :param no_commit: Do not commit to database (remember to commit later)\n        \"\"\"\n        c = self.db.cursor()\n        lat, lon = loc\n        # Nodes\n        q = \"INSERT INTO nodes VALUES(?, ?, ?)\"\n        try:\n            c.execute(q, (node, lon, lat))\n        except sqlite3.IntegrityError as exc:\n            if ignore_doubles and \"UNIQUE constraint failed: nodes.id\" in str(exc):\n                return\n            logger.error(f\"Problem with adding node {node} {loc}\")\n            raise exc\n        # Nodes index\n        if not no_index:\n            q = \"INSERT INTO nodes_index VALUES(?, ?, ?, ?, ?)\"\n            try:\n                c.execute(q, (node, lon, lon, lat, lat))\n            except sqlite3.IntegrityError as exc:\n                logger.error(f\"Problem with adding node to index {node} {loc}\")\n                raise exc\n        if not no_commit:\n            self.db.commit()\n\n    def reindex_nodes(self):\n        logger.debug(\"Reindexing nodes ...\")\n        t_start = time.time()\n        c = self.db.cursor()\n        c.execute('DELETE FROM nodes_index')\n        q = (\"INSERT INTO nodes_index \"\n             \"SELECT id, x, x, y, y FROM nodes\")\n        c.execute(q)\n        self.db.commit()\n        c.execute('SELECT count(*) FROM nodes_index')\n        cnt = c.fetchone()[0]\n        t_delta = time.time() - t_start\n        logger.debug(f\"... done, #rows = {cnt}, time = {t_delta} sec\")\n\n    def add_nodes(self, nodes):\n        \"\"\"Add list of nodes to database.\n\n        :param nodes: List[Tuple[node_key, Tuple[lat, lon]]]\n        \"\"\"\n        c = self.db.cursor()\n\n        def get_node_index():\n            for key, (lat, lon) in nodes:\n                yield key, lon, lon, lat, lat\n\n        q = \"INSERT INTO nodes_index VALUES(?, ?, ?, ?, ?)\"\n        c.executemany(q, get_node_index())\n\n        def get_node_vals():\n            for key, (lat, lon) in nodes:\n                yield key, lon, lat\n\n        q = \"INSERT INTO nodes VALUES(?, ?, ?)\"\n        c.executemany(q, get_node_vals())\n        self.db.commit()\n\n    def del_node(self, node):\n        raise Exception(\"TODO\")\n\n    def add_edge(self, node_a, node_b, loc_a=None, loc_b=None, speed=None, edge_type=None,\n                 path=None, pathnum=None,\n                 no_index=False, no_commit=False):\n        \"\"\"Add new edge to the map.\n\n        :param node_a: Label for the node that is the start of the edge\n        :param node_b: Label for the node that is the end of the edge\n        :param no_commit: Do not commit to database (remember to commit later)\n        \"\"\"\n        c = self.db.cursor()\n        eid = (node_a, node_b).__hash__()\n        c.execute('INSERT OR IGNORE INTO edges(id, path, pathnum, id1, id2, type, speed) VALUES (?, ?, ?, ?, ?, ?, ?)',\n                  (eid, path, pathnum, node_a, node_b, edge_type, speed))\n        # c.execute('SELECT last_insert_rowid();')\n        # eid = c.fetchone()[0]\n\n        if not no_index:\n            if loc_a is None:\n                c.execute('SELECT y, x FROM nodes WHERE id = ?;', (node_a, ))\n                loc_a = c.fetchone()\n            if loc_b is None:\n                c.execute('SELECT y, x FROM nodes WHERE id = ?;', (node_b, ))\n                loc_b = c.fetchone()\n            lat1, lon1 = loc_a\n            lat2, lon2 = loc_b\n            if lat1 > lat2:\n                lat1, lat2 = lat2, lat1\n            if lon1 > lon2:\n                lon1, lon2 = lon2, lon1\n            c.execute('INSERT OR IGNORE INTO edges_index(id, minX, maxX, minY, maxY) VALUES (?, ?, ?, ?, ?)',\n                      (eid, lon1, lon2, lat1, lat2))\n        if not no_commit:\n            self.db.commit()\n\n    def add_edges(self, edges, no_index=False):\n        \"\"\"Add list of nodes to database.\n\n        :param edges: List[Tuple[node_key, node_key]] or\n            List[Tuple[node_key, node_key, path_key, int]]\n        \"\"\"\n        c = self.db.cursor()\n\n        def get_edge():\n            for row in edges:\n                row = list(row) + ([None] * (6 - len(row)))\n                key_a, key_b, path, pathnum, edge_type, speed = row\n                eid = (key_a, key_b).__hash__()\n                yield eid, path, pathnum, key_a, key_b, edge_type, speed\n\n        q = \"INSERT INTO edges(id, path, pathnum, id1, id2, type, speed) VALUES(?, ?, ?, ?, ?, ?, ?);\"\n        c.executemany(q, get_edge())\n        self.db.commit()\n\n        if not no_index:\n            self.reindex_edges()\n\n    def reindex_edges(self):\n        logger.debug(\"Reindexing edges ...\")\n        t_start = time.time()\n        c = self.db.cursor()\n        # c2 = self.db.cursor()\n        c.execute('DELETE FROM edges_index')\n        q = ('INSERT INTO edges_index '\n             'SELECT e.id, MIN(n1.x,n2.x), MAX(n1.x,n2.x), '\n             '             MIN(n1.y,n2.y), MAX(n1.y,n2.y) '\n             'FROM edges e '\n             'INNER JOIN nodes n1 ON n1.id = e.id1 '\n             'INNER JOIN nodes n2 ON n2.id = e.id2')\n        c.execute(q)\n        # cnt = 0\n        # for row in c.execute(q):\n        #     # Contained in query\n        #     c2.execute('INSERT INTO edges_index(id, minX, maxX, minY, maxY) VALUES (?, ?, ?, ?, ?)', row)\n        #     cnt += 1\n        self.db.commit()\n        c.execute('SELECT count(*) FROM edges_index')\n        cnt = c.fetchone()[0]\n        t_delta = time.time() - t_start\n        logger.debug(f\"... done, #rows = {cnt}, time = {t_delta} sec\")\n\n    def all_edges(self, bb=None):\n        \"\"\"Return all edges.\n\n        :param bb: Bounding box\n        :return: (key_a, loc_a, nbr, loc_b)\n        \"\"\"\n        c = self.db.cursor()\n        q = 'SELECT e.id1, e.id2, n1.x AS n1x, n2.x AS n2x, n1.y AS n1y, n2.y AS n2y ' + \\\n            'FROM edges e, edges_index ei ' + \\\n            'LEFT JOIN nodes n1 ON n1.id = e.id1 ' + \\\n            'LEFT JOIN nodes n2 ON n2.id = e.id2 ' + \\\n            'WHERE ei.id == e.id'\n        if bb:\n            min_y, min_x, max_y, max_x = bb\n            # Intersecting with query\n            q += ' AND ei.maxX >= ? AND ei.minX <= ? AND ei.maxY >= ? AND ei.minY <= ?'\n            c.execute(q, (min_x, max_x, min_y, max_y))\n        else:\n            c.execute(q)\n\n        for row in c.fetchall():\n            key_a, key_b, lon_a, lon_b, lat_a, lat_b = row\n            yield key_a, (lat_a, lon_a), key_b, (lat_b, lon_b)\n\n    def all_nodes(self, bb=None):\n        \"\"\"Return all nodes.\n\n        :param bb: Bounding box (minY, minX, maxY, maxX)\n        :return:\n        \"\"\"\n        c = self.db.cursor()\n        q = ('SELECT n.id, n.x, n.y '\n             'FROM nodes n, nodes_index ni '\n             'WHERE n.id = ni.id ')\n        if bb:\n            minY, minX, maxY, maxX = bb\n            q += 'AND ni.minX >= ? AND ni.maxX <= ? AND ni.minY >= ? AND ni.maxY <= ?'\n            c.execute(q, (minX, maxX, minY, maxY))\n        else:\n            c.execute(q)\n\n        for row in c.fetchall():\n            key_a, lon_a, lat_a = row\n            yield key_a, (lat_a, lon_a)\n\n    def purge(self):\n        pass\n\n    def to_xy(self, name=None):\n        \"\"\"Create a map that uses a projected XY representation on which Euclidean distances\n        can be used.\n        \"\"\"\n        if not self.use_latlon:\n            return self\n        if name is None:\n            name = self.name + \"_xy\"\n\n        logger.debug(\"Start transformation ...\")\n        t_start = time.time()\n        nmap = self.__class__(name, dir=self.dir, use_latlon=self.use_latlon,\n                              crs_xy=self.crs_xy, crs_lonlat=self.crs_lonlat)\n\n        raise Exception(\"to implement\")\n\n        t_delta = time.time() - t_start\n        logger.debug(f\"... done: rtree size = {self.rtree_size()}, time = {t_delta} sec\")\n\n        return nmap\n\n    def latlon2xy(self, lat, lon):\n        x, y = self.lonlat2xy(lon, lat)\n        return x, y\n\n    def latlon2yx(self, lat, lon):\n        x, y = self.lonlat2xy(lon, lat)\n        return y, x\n\n    def xy2latlon(self, x, y):\n        lon, lat = self.xy2lonlat(x, y)\n        return lat, lon\n\n    def yx2latlon(self, y, x):\n        lon, lat = self.xy2lonlat(x, y)\n        return lat, lon\n\n    def nodes_closeto(self, loc, max_dist=None, max_elmt=None):\n        \"\"\"Return all nodes close to the given location.\n\n        :param loc: Location\n        :param max_dist: Maximal distance from the location\n        :param max_elmt: Return only the most nearby nodes\n        \"\"\"\n        t_start = time.time()\n        lat, lon = loc[:2]\n        lat_b, lon_l, lat_t, lon_r = self.box_around_point((lat, lon), max_dist)\n        bb = (lat_b, lon_l,  # y_min, x_min\n              lat_t, lon_r)  # y_max, x_max\n        nodes = self.all_nodes(bb=bb)\n        t_delta_search = time.time() - t_start\n        t_start = time.time()\n        results = []\n        for key_o, loc_o in nodes:\n            dist = self.distance(loc, loc_o)\n            if dist < max_dist:\n                results.append((dist, key_o, loc_o))\n        results.sort()\n        t_delta_dist = time.time() - t_start\n        logger.debug(f\"Found {len(results)} closeby nodes \"\n                     f\"in {t_delta_search} sec and computed distances in {t_delta_dist} sec\")\n        if max_elmt is not None:\n            results = results[:max_elmt]\n        return results\n\n    def edges_closeto(self, loc, max_dist=None, max_elmt=None):\n        \"\"\"Return all nodes that are on an edge that is close to the given location.\n\n        :param loc: Location\n        :param max_dist: Maximal distance from the location\n        :param max_elmt: Return only the most nearby nodes\n        \"\"\"\n        print(f\"edges_closeto({loc})\")\n        t_start = time.time()\n        lat, lon = loc[:2]\n        lat_b, lon_l, lat_t, lon_r = self.box_around_point((lat, lon), max_dist)\n        bb = (lat_b, lon_l,  # y_min, x_min\n              lat_t, lon_r)  # y_max, x_max\n        logger.debug(f\"Search in bounding box {bb}\")\n        nodes = self.all_edges(bb=bb)\n        t_delta_search = time.time() - t_start\n        t_start = time.time()\n        results = []\n        for key_a, loc_a, key_b, loc_b in nodes:\n            dist, pi, ti = self.distance_point_to_segment(loc, loc_a, loc_b)\n            if dist < max_dist:\n                results.append((dist, key_a, loc_a, key_b, loc_b, pi, ti))\n        results.sort()\n        t_delta_dist = time.time() - t_start\n        logger.debug(f\"Found {len(results)} closeby edges \"\n                     f\"in {t_delta_search} sec and computed distances in {t_delta_dist} sec\")\n        if max_elmt is not None:\n            results = results[:max_elmt]\n        return results\n\n    def nodes_nbrto(self, node):\n        c = self.db.cursor()\n        q = ('SELECT e.id2, n2.y, n2.x FROM edges e '\n             'INNER JOIN nodes n2 ON n2.id = e.id2 '\n             'WHERE e.id1 = ?')\n        results = []\n        for nbr_label, nbr_lat, nbr_lon in c.execute(q, (node, )):\n            results.append((nbr_label, (nbr_lat, nbr_lon)))\n        return results\n\n    def edges_nbrto(self, edge):\n        l1, l2 = edge\n        c = self.db.cursor()\n        c.execute('SELECT n.y, n.x FROM nodes n WHERE id = ?', (l2, ))\n        p2 = c.fetchone()\n        results = []\n        # Edges that connect at end of this edge\n        for l3, p3 in self.nodes_nbrto(l2):\n            results.append((l2, p2, l3, p3))\n        # Edges that are in parallel and close\n        edge_id = edge.__hash__()\n        q = ('SELECT e.id1, e.id2, n1.y, n1.x, n2.y, n2.x FROM close_edges ce '\n             'INNER JOIN edges e ON e.id = ce.id2 '\n             'INNER JOIN nodes n1 ON n1.id = e.id1 '\n             'INNER JOIN nodes n2 ON n2.id = e.id2 '\n             'WHERE ce.id1 = ?')\n        for l3, l4, p3lat, p3lon, p4lat, p4lon in c.execute(q, (edge_id,)):\n            results.append((l3, (p3lat, p3lon), l4, (p4lat, p4lon)))\n        return results\n\n    def find_duplicates(self, func=None):\n        \"\"\"Find entries with identical locations.\"\"\"\n        c = self.db.cursor()\n        logger.debug('Find duplicates ...')\n        t_start = time.time()\n        cnt = 0\n        q = ('select count(*)as qty, group_concat(id) '\n             'from nodes '\n             'group by y, x '\n             'having qty > 1 ')\n        for ncnt, idxs in c.execute(q):\n            func(int(idx) for idx in idxs.split(\",\"))\n        t_delta = time.time() - t_start\n        logger.info(f\"Found {cnt} doubles, time: {t_delta} seconds\")\n\n    def connect_parallelroads(self, dist=0.5, bb=None):\n        c = self.db.cursor()\n        it = self.all_edges(bb=bb)\n        if tqdm:\n            it = tqdm.tqdm(list(it))\n        cnt = 0\n        for key_a, loc_a, key_b, loc_b in it:\n            e_id1 = (key_a, key_b).__hash__()\n            bb2 = [min(loc_a[0], loc_b[0]), min(loc_a[1], loc_b[1]),\n                   max(loc_a[0], loc_b[0]), max(loc_a[1], loc_b[1])]\n            for key_c, loc_c, key_d, loc_d in self.all_edges(bb=bb2):\n                e_id2 = (key_c, key_d).__hash__()\n                if key_a == key_c or key_a == key_d or key_b == key_c or key_b == key_d:\n                    continue\n                # print(f\"Test: ({key_a},{key_b}) - ({key_c},{key_d})\")\n                if self.lines_parallel(loc_a, loc_b, loc_c, loc_d, d=dist):\n                    # print(f\"Parallel: ({key_a},{key_b}) - ({key_c},{key_d})\")\n                    c.execute('INSERT INTO close_edges(id1, id2) VALUES (?, ?)', (e_id1, e_id2))\n                    c.execute('INSERT INTO close_edges(id1, id2) VALUES (?, ?)', (e_id2, e_id1))\n                    cnt += 1\n        logger.debug(f\"Linked {cnt} edges\")\n        self.db.commit()\n\n    def nodes_to_paths(self, nodes, ignore_nopath=True):\n        c = self.db.cursor()\n        prev_path = None\n        paths = []\n        for begin, end in zip(nodes[:-1], nodes[1:]):\n            c.execute(\"SELECT path FROM edges WHERE id1=? AND id2=?\", (begin, end))\n            path = c.fetchone()[0]\n            if path is None and ignore_nopath:\n                continue\n            if path != prev_path:\n                paths.append(path)\n                prev_path = path\n        return paths\n\n    def path_dist(self, path):\n        c = self.db.cursor()\n        dist = 0\n        q = ('SELECT n1.y, n1.x, n2.y, n2.x FROM edges e '\n             'INNER JOIN nodes n1 ON n1.id = e.id1 '\n             'INNER JOIN nodes n2 ON n2.id = e.id2 '\n             'WHERE e.pathnum>0 AND e.path=?')\n        for lat1, lon1, lat2, lon2 in c.execute(q, (path,)):\n            dist += self.distance((lat1, lon1), (lat2, lon2))\n        return dist\n\n    def print_stats(self):\n        print(\"Graph\\n-----\")\n        print(\"Nodes: {}\".format(len(self.graph)))\n\n    def __str__(self):\n        # s = \"\"\n        # for label, (loc, nbrs, _) in self.graph.items():\n        #     s += f\"{label:<10} - ({loc[0]:10.4f}, {loc[1]:10.4f})\\n\"\n        # return s\n        c = self.db.cursor()\n        c.execute(\"select sqlite_version()\")\n        row = c.fetchone()\n        version = row[0]\n        return f\"SqliteMap({self.name}, size={self.size()}, version={version})\"\n"
  },
  {
    "path": "leuvenmapmatching/matcher/__init__.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.matcher\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\n"
  },
  {
    "path": "leuvenmapmatching/matcher/base.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.matcher.base\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBase Matcher and Matching classes.\n\nThis a generic base class to be used by matchers. This class itself\ndoes not implement a working matcher.\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2021 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nfrom __future__ import print_function\n\nimport math\nimport sys\nimport logging\nimport time\nfrom collections import OrderedDict, defaultdict, namedtuple\nfrom itertools import islice\nfrom typing import List, Tuple, Dict, Any, Optional, Set\n\nimport numpy as np\n\nfrom ..util.segment import Segment\nfrom ..util import approx_equal, approx_leq\n\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\napprox_value = 0.0000000001\nema_const = namedtuple('EMAConst', ['prev', 'cur'])(0.7, 0.3)\ndefault_label_width = 25\n\n\nclass BaseMatching(object):\n    \"\"\"Matching object that represents a node in the Viterbi lattice.\"\"\"\n    __slots__ = ['matcher', 'edge_m', 'edge_o',\n                 'logprob', 'logprobema', 'logprobe', 'logprobne',\n                 'obs', 'obs_ne', 'dist_obs',\n                 'prev', 'prev_other', 'stop', 'length', 'delayed']\n\n    def __init__(self, matcher: 'BaseMatcher', edge_m: Segment, edge_o: Segment,\n                 logprob=-np.inf, logprobema=-np.inf, logprobe=-np.inf, logprobne=-np.inf,\n                 dist_obs: float = 0.0, obs: int = 0, obs_ne: int = 0,\n                 prev: Optional[Set['BaseMatching']] = None, stop: bool = False, length: int = 1,\n                 delayed: int = 0, **_kwargs):\n        \"\"\"\n\n        :param matcher: Reference to the Matcher used to generate this matching object.\n        :param edge_m: Segment in the given graph (thus line between two nodes in the graph).\n        :param edge_o: Segment in the given observations (thus line in between two observations).\n        :param logprob: Log probability of this matching.\n        :param logprobema: Exponential Mean Average of Log probability.\n        :param logprobe: Emitting\n        :param logprobne: Non-emitting\n        :param dist_obs: Distance between map point and observation\n        :param obs: Reference to path entry index (observation)\n        :param obs_ne: Number of non-emitting states for this observation\n        :param prev: Previous best matching objects\n        :param stop: Stop after this matching (e.g. because probability is too low)\n        :param length: Lenght of current matching sequence through lattice.\n        :param delayed: This matching is temporarily stopped if >0 (e.g. to first explore better options).\n        :param dist_m: Distance over graph\n        :param dist_o: Distance over observations\n        :param _kwargs:\n        \"\"\"\n        self.edge_m: Segment = edge_m\n        self.edge_o: Segment = edge_o\n        self.logprob: float = logprob        # max probability\n        self.logprobe: float = logprobe      # Emitting\n        self.logprobne: float = logprobne    # Non-emitting\n        self.logprobema: float = logprobema  # exponential moving average log probability  # TODO: Not used anymore?\n        self.obs: int = obs  # reference to path entry index (observation)\n        self.obs_ne: int = obs_ne  # number of non-emitting states for this observation\n        self.dist_obs: float = dist_obs  # Distance between map point and observation\n        self.prev: Set[BaseMatching] = set() if prev is None else prev  # Previous best matching objects\n        self.prev_other: Set[BaseMatching] = set()  # Previous matching objects with lower logprob\n        self.stop: bool = stop\n        self.length: int = length\n        self.delayed: int = delayed\n        self.matcher: BaseMatcher = matcher\n\n    @property\n    def prune_value(self):\n        \"\"\"Pruning the lattice (e.g. to delay) is based on this key.\"\"\"\n        return self.logprob\n        # return self.logprobema\n\n    def next(self, edge_m: Segment, edge_o: Segment, obs: int = 0, obs_ne: int = 0):\n        \"\"\"Create a next lattice Matching object with this Matching object as the previous one in the lattice.\"\"\"\n        new_stop = False\n        if edge_m.is_point() and edge_o.is_point():\n            # node to node\n            dist = self.matcher.map.distance(edge_m.p1, edge_o.p1)\n            # proj_m = edge_m.p1\n            # proj_o = edge_o.pi\n        elif edge_m.is_point() and not edge_o.is_point():\n            # node to edge\n            dist, proj_o, t_o = self.matcher.map.distance_point_to_segment(edge_m.p1, edge_o.p1, edge_o.p2)\n            # proj_m = edge_m.p1\n            edge_o.pi = proj_o\n            edge_o.ti = t_o\n        elif not edge_m.is_point() and edge_o.is_point():\n            # edge to node\n            dist, proj_m, t_m = self.matcher.map.distance_point_to_segment(edge_o.p1, edge_m.p1, edge_m.p2)\n            if not self.matcher.only_edges and (approx_equal(t_m, 0.0) or approx_equal(t_m, 1.0)):\n                if __debug__ and logger.isEnabledFor(logging.DEBUG):\n                    logger.debug(f\"   | Stopped trace: Too close to end, {t_m}\")\n                    new_stop = True\n                else:\n                    return None\n            edge_m.pi = proj_m\n            edge_m.ti = t_m\n            # proj_o = edge_o.pi\n        elif not edge_m.is_point() and not edge_o.is_point():\n            # edge to edge\n            dist, proj_m, proj_o, t_m, t_o = self.matcher.map.distance_segment_to_segment(edge_m.p1, edge_m.p2,\n                                                                                          edge_o.p1, edge_o.p2)\n            edge_m.pi = proj_m\n            edge_m.ti = t_m\n            edge_o.pi = proj_o\n            edge_o.ti = t_o\n        else:\n            raise Exception(f\"Should not happen\")\n\n        logprob_trans, props_trans = self.matcher.logprob_trans(self, edge_m, edge_o,\n                                                                is_prev_ne=(self.obs_ne != 0),\n                                                                is_next_ne=(obs_ne != 0))\n        logprob_obs, props_obs = self.matcher.logprob_obs(dist, self, edge_m, edge_o,\n                                                          is_ne=(obs_ne != 0))\n        if __debug__ and logprob_trans > 0:\n            raise Exception(f\"logprob_trans = {logprob_trans} > 0\")\n        if __debug__ and logprob_obs > 0:\n            raise Exception(f\"logprob_obs = {logprob_obs} > 0\")\n        new_logprob_delta = logprob_trans + logprob_obs\n        if obs_ne == 0:\n            new_logprobe = self.logprob + new_logprob_delta\n            new_logprobne = 0\n            new_logprob = new_logprobe\n            new_length = self.length + 1\n        else:\n            # Non-emitting states require normalisation\n            # \"* e^(ne_length_factor_log)\" or \"- ne_length_factor_log\" for every step to a non-emitting\n            # state to prefer shorter paths\n            new_logprobe = self.logprobe + self.matcher.ne_length_factor_log\n            # The obvious choice would be average to compensate for that non-emitting states\n            # create different path lengths between emitting nodes.\n            # We use min() as it is a monotonic function, in contrast with an average\n            new_logprobne = min(self.logprobne, new_logprob_delta)\n            new_logprob = new_logprobe + new_logprobne\n            # Alternative approach with an average\n            # new_logprobne = self.logprobne + new_logprob_delta\n            # \"+ 1\" to punish non-emitting states a bit less. Otherwise it would be\n            # similar to (Pr_tr*Pr_obs)**2, which punishes just one non-emitting state too much.\n            # new_logprob = new_logprobe + new_logprobne / (obs_ne + 1)\n            new_length = self.length\n        new_logprobema = ema_const.cur * new_logprob_delta + ema_const.prev * self.logprobema\n        new_stop |= self.matcher.do_stop(new_logprob / new_length, dist, logprob_trans, logprob_obs)\n        if __debug__ and new_logprob > self.logprob:\n            raise Exception(f\"Expecting a monotonic probability, \"\n                            f\"new_logprob = {new_logprob} > logprob = {self.logprob}\")\n        if not new_stop or (__debug__ and logger.isEnabledFor(logging.DEBUG)):\n            m_next = self.__class__(self.matcher, edge_m, edge_o,\n                                    logprob=new_logprob, logprobne=new_logprobne,\n                                    logprobe=new_logprobe, logprobema=new_logprobema,\n                                    obs=obs, obs_ne=obs_ne, prev={self}, dist_obs=dist,\n                                    stop=new_stop, length=new_length, delayed=self.delayed,\n                                    **props_trans, **props_obs)\n            return m_next\n        else:\n            return None\n\n    @classmethod\n    def first(cls, logprob_init, edge_m, edge_o, matcher, dist_obs):\n        \"\"\"Create an initial lattice Matching object.\"\"\"\n        logprob_obs, props_obs = matcher.logprob_obs(dist_obs, None, edge_m, edge_o)\n        logprob = logprob_init + logprob_obs\n        new_stop = matcher.do_stop(logprob, dist_obs, logprob_init, logprob_obs)\n        if not new_stop or logger.isEnabledFor(logging.DEBUG):\n            m_next = cls(matcher, edge_m=edge_m, edge_o=edge_o,\n                         logprob=logprob, logprobema=logprob, logprobe=logprob, logprobne=0,\n                         dist_obs=dist_obs, obs=0, stop=new_stop, **props_obs)\n            return m_next\n        else:\n            return None\n\n    def update(self, m_next):\n        \"\"\"Update the current entry if the new matching object for this state is better.\n\n        :param m_next: The new matching object representing the same node in the lattice.\n        :return: True if the current object is replaced, False otherwise\n        \"\"\"\n        # if self.length != m_next.length:\n        #     slogprob_norm = self.logprob / self.length\n        #     nlogprob_norm = m_next.logprob / m_next.length\n        # else:\n        #     slogprob_norm = self.logprob\n        #     nlogprob_norm = m_next.logprob\n        # if (self.stop == m_next.stop and slogprob_norm < nlogprob_norm) or (self.stop and not m_next.stop):\n        #     self._update_inner(m_next)\n        #     return True\n        # elif abs(slogprob_norm - nlogprob_norm) < approx_value and self.stop == m_next.stop:\n        #     self.prev.update(m_next.prev)\n        #     self.stop = m_next.stop\n        #     return False\n        assert self.length == m_next.length\n        if (self.stop and not m_next.stop) \\\n                or (self.stop == m_next.stop and self.logprob < m_next.logprob):\n            self._update_inner(m_next)\n            return True\n        else:\n            self.prev_other.update(m_next.prev)\n            return False\n\n    def _update_inner(self, m_other: 'BaseMatching'):\n        self.edge_m = m_other.edge_m\n        self.edge_o = m_other.edge_o\n        self.logprob = m_other.logprob\n        self.logprobe = m_other.logprobe\n        self.logprobne = m_other.logprobne\n        self.logprobema = m_other.logprobema\n        self.dist_obs = m_other.dist_obs\n        self.obs = m_other.obs\n        self.obs_ne = m_other.obs_ne\n        self.prev_other.update(self.prev)  # Do we use this?\n        self.prev = m_other.prev\n        self.stop = m_other.stop\n        self.delayed = m_other.delayed\n        self.length = m_other.length\n\n    def is_nonemitting(self):\n        return self.obs_ne != 0\n\n    def is_emitting(self):\n        return self.obs_ne == 0\n\n    def last_emitting_logprob(self):\n        if self.is_emitting():\n            return self.logprob\n        elif self.prev is None or len(self.prev) == 0:\n            return 0\n        else:\n            return next(iter(self.prev)).last_emitting_logprob()\n\n    def __str__(self, label_width=None):\n        stop = ''\n        if self.stop:\n            stop = 'x'\n        else:\n            stop = f'{self.delayed}'\n        if label_width is None:\n            label_width = default_label_width\n        repr_tmpl = \"{:<2} | {:<\"+str(label_width)+\"} | {:10.5f} | {:10.5f} | {:10.5f} | {:10.5f} | \" +\\\n                    \"{:<3} | {:10.5f} | {:<\" + str(label_width) + \"} |\"\n        return repr_tmpl.format(stop, self.label, self.logprob, self.logprob / self.length,\n                                self.logprobema, self.logprobne, self.obs,\n                                self.dist_obs, \",\".join([str(prev.label) for prev in self.prev]))\n\n    def __repr__(self):\n        return \"Matching<\"+str(self.label)+\">\"\n\n    @staticmethod\n    def repr_header(label_width=None, stop=\"\"):\n        if label_width is None:\n            label_width = default_label_width\n        repr_tmpl = \"{:<2} | {:<\"+str(label_width)+\"} | {:<10} | {:<10} | {:<10} | {:<10} | \" + \\\n                    \"{:<3} | {:<10} | {:<\"+str(label_width)+\"} |\"\n        return repr_tmpl.format(stop, \"\", \"lg(Pr)\", \"nlg(Pr)\", \"slg(Pr)\", \"lg(Pr-ne)\", \"obs\", \"d(obs)\", \"prev\")\n\n    @staticmethod\n    def repr_static(fields, label_width=None):\n        if label_width is None:\n            label_width = default_label_width\n        default_fields = [\"\", \"\", float('nan'), float('nan'), float('nan'), float('nan'), \"\", float('nan'), \"\", \"\"]\n        repr_tmpl = \"{:<2} | {:<\" + str(label_width) + \"} | {:10.5f} | {:10.5f} | {:10.5f} | {:10.5f} | \" + \\\n                    \"{:<3} | {:10.5f} | {:<\" + str(label_width) + \"} |\"\n        if len(fields) < 8:\n            fields = list(fields) + default_fields[len(fields):]\n        return repr_tmpl.format(*fields)\n\n    @property\n    def label(self):\n        if self.edge_m.p2 is None:\n            return \"{}---{}-{}\".format(self.edge_m.l1, self.obs, self.obs_ne)\n        else:\n            return \"{}-{}-{}-{}\".format(self.edge_m.l1, self.edge_m.l2, self.obs, self.obs_ne)\n\n    @property\n    def cname(self):\n        if self.edge_m.l2 is None:\n            return \"{}_{}_{}\".format(self.edge_m.l1, self.obs, self.obs_ne)\n        else:\n            return \"{}_{}_{}_{}\".format(self.edge_m.l1, self.edge_m.l2, self.obs, self.obs_ne)\n\n    @property\n    def key(self):\n        \"\"\"Key that indicates the node or edge, observation and non-emitting step.\n        This is the unique key that is used in the lattice.\n        \"\"\"\n        if self.edge_m.l2 is None:\n            return tuple([self.edge_m.l1, self.obs, self.obs_ne])\n        else:\n            return tuple([self.edge_m.l1, self.edge_m.l2, self.obs, self.obs_ne])\n\n    @property\n    def shortkey(self):\n        \"\"\"Key that indicates the node or edge. Irrespective of the current observation.\"\"\"\n        if self.edge_m.l2 is None:\n            return self.edge_m.l1\n        else:\n            return tuple([self.edge_m.l1, self.edge_m.l2])\n\n    @property\n    def nodes(self):\n        if self.edge_m.l2 is None:\n            return [self.edge_m.l1]\n        else:\n            return [self.edge_m.l1, self.edge_m.l2]\n\n    def __hash__(self):\n        return self.cname.__hash__()\n\n    def __lt__(self, o):\n        return self.logprob < o.logprob\n\n    def __le__(self, o):\n        return self.logprob <= o.logprob\n\n    def __eq__(self, o):\n        return self.logprob == o.logprob\n\n    def __ne__(self, o):\n        return self.logprob != o.logprob\n\n    def __ge__(self, o):\n        return self.logprob >= o.logprob\n\n    def __gt__(self, o):\n        return self.logprob > o.logprob\n\n\nclass LatticeColumn:\n\n    def __init__(self, obs_idx):\n        # 0 = obs, >0 = non-emitting between this obs and next\n        self.obs_idx = obs_idx\n        self.o = []  # type list[dict[label,Matching]]\n\n    def __contains__(self, item):\n        for c in self.o:\n            if item in c:\n                return True\n        return False\n\n    def __len__(self):\n        return len(self.o)\n\n    def set_delayed(self, delayed):\n        \"\"\"Update all delayed values.\"\"\"\n        for c in self.o:\n            for m in c.values():\n                m.delayed = delayed\n\n    def dict(self, obs_ne=None):\n        if obs_ne is None:\n            raise AttributeError('obs_ne should be value')\n        while obs_ne >= len(self.o):\n            self.o.append({})\n        return self.o[obs_ne]\n\n    def values_all(self):\n        \"\"\"All matches for the emitting layer and all non-emitting layers.\"\"\"\n        values = set()\n        for o in self.o:\n            values.update(o.values())\n        return values\n\n    def values(self, obs_ne=None):\n        if obs_ne is None:\n            raise AttributeError('obs_ne should be value')\n        if len(self.o) <= obs_ne:\n            return []\n        return self.o[obs_ne].values()\n\n    def upsert(self, matching):\n        # type: (BaseMatching) -> None\n        if matching is None:\n            return None\n        while matching.obs_ne >= len(self.o):\n            self.o.append({})\n        c = self.o[matching.obs_ne]\n        if matching.key in c:\n            other_matching = c[matching.key]  # type: BaseMatching\n            other_matching.update(matching)\n        else:\n            c[matching.key] = matching\n        return c[matching.key]\n\n    def prune(self, obs_ne, max_lattice_width, expand_upto, prune_thr=None):\n        \"\"\"Prune given column in the lattice to fit in max_lattice_width.\n        Also ignore all matchings with a probability lower than prune_thr. These are\n        matchings that are worse than the matchings at the next observation that are\n        retained after pruning.\n\n        :param obs_ne:\n        :param max_lattice_width:\n        :param expand_upto: The current expand level\n        :return:\n        \"\"\"\n        cur_lattice = [m for m in self.values(obs_ne) if not m.stop]\n        if __debug__:\n            logger.debug('Prune lattice[{},{}] from {} to {}, with prune thr {}'\n                         .format(self.obs_idx, obs_ne,\n                                 len([m for m in cur_lattice if not m.stop and m.delayed == expand_upto]),\n                                 max_lattice_width, prune_thr))\n            cnt_pruned = 0\n        if max_lattice_width is not None and len(cur_lattice) > max_lattice_width:\n            ms = sorted(cur_lattice, key=lambda t: t.prune_value, reverse=True)\n            cur_width = max_lattice_width\n            m_last = ms[cur_width - 1]\n            # Extend current width if next pruned matching has same logprob as last kept matching\n            # This increases the lattice width but otherwise the algorithm depends on the\n            # order of edges/nodes and is not deterministic.\n            while cur_width < len(ms) and ms[cur_width].prune_value == m_last.prune_value:\n                m_last = ms[cur_width]\n                cur_width += 1\n            if prune_thr is not None:\n                while cur_width > 0 and ms[cur_width - 1].prune_value < prune_thr:\n                    cur_width -= 1\n            for m in ms[:cur_width]:  # type: BaseMatching\n                if m.delayed > expand_upto:\n                    m.delayed = expand_upto  # expand now\n            for m in ms[cur_width:]:\n                if m.delayed <= expand_upto:\n                    if __debug__:\n                        cnt_pruned += 1\n                    m.delayed = expand_upto + 1  # expand later\n            if cur_width > 0:\n                prune_thr = ms[cur_width - 1].prune_value\n        if __debug__:\n            logger.debug(f'Pruned {cnt_pruned} matchings, return {prune_thr}')\n        return prune_thr\n\n\nclass BaseMatcher:\n\n    def __init__(self, map_con, obs_noise=1, max_dist_init=None, max_dist=None, min_prob_norm=None,\n                 non_emitting_states=True, max_lattice_width=None,\n                 only_edges=True, obs_noise_ne=None, matching=BaseMatching,\n                 non_emitting_length_factor=0.75, **kwargs):\n        \"\"\"Initialize a matcher for map matching.\n\n        This a generic base class to be used by matchers. This class itself\n        does not implement a working matcher.\n\n        Distances are in meters when using latitude-longitude.\n\n        :param map_con: Map object to connect to map database\n        :param obs_noise: Standard deviation of noise\n        :param obs_noise_ne: Standard deviation of noise for non-emitting states (is set to obs_noise if not give)\n        :param max_dist_init: Maximum distance from start location (if not given, uses max_dist)\n        :param max_dist: Maximum distance from path (this is a hard cut, min_prob_norm should be better)\n        :param min_prob_norm: Minimum normalized probability of observations (ema)\n        :param non_emitting_states: Allow non-emitting states. A non-emitting state is a state that is\n            not associated with an observation. Here we assume it can be associated with a location in between\n            two observations to allow for pruning. It is advised to set min_prob_norm and/or max_dist to avoid\n            visiting all possible nodes in the graph.\n        :param max_lattice_width: Only continue from a limited number of states (thus locations) for a given observation.\n            This possibly speeds up the matching by a lot.\n            If there are more possible next states, the states with the best likelihood so far are selected.\n            The other states are 'delayed'. If the matching is continued later with a larger value using\n            `increase_max_lattice_width`, the algorithms continuous from these delayed states.\n        :param only_edges: Do not include nodes as states, only edges. This is the typical setting for HMM methods.\n        :param matching: Matching type\n        :param non_emitting_length_factor: Reduce the probability of a sequence of non-emitting states the longer it\n            is. This can be used to prefer shorter paths. This is separate from the transition probabilities because\n            transition probabilities are averaged for non-emitting states and thus the length is also averaged out.\n\n        To define a custom transition and/or emission probability distribtion, overwrite the following functions:\n\n        - :meth:`logprob_trans`\n        - :meth:`logprob_obs`\n\n        \"\"\"\n        self.map = map_con  # type: BaseMap\n        if max_dist:\n            self.max_dist = max_dist\n        else:\n            self.max_dist = np.inf\n        if max_dist_init:\n            self.max_dist_init = max_dist_init\n        else:\n            self.max_dist_init = self.max_dist\n        if min_prob_norm:\n            self.min_logprob_norm = math.log(min_prob_norm)\n        else:\n            self.min_logprob_norm = -np.inf\n        logger.debug(f\"Matcher.min_logprob_norm = {self.min_logprob_norm}, Matcher.max_dist = {self.max_dist}\")\n        self.obs_noise = obs_noise\n        if obs_noise_ne is None:\n            self.obs_noise_ne = obs_noise\n        else:\n            self.obs_noise_ne = obs_noise_ne\n\n        self.path = None\n        self.lattice = None  # type: Optional[dict[int,LatticeColumn]]\n        # Best path through lattice:\n        self.lattice_best = None  # type: Optional[list[BaseMatching]]\n        self.node_path = None  # type: Optional[list[str]]\n        self.matching = matching\n        self.non_emitting_states = non_emitting_states  # type: bool\n        self.non_emitting_states_maxnb = 100\n        self.max_lattice_width = max_lattice_width  # type: Optional[int]\n        self.only_edges = only_edges  # type: bool\n        self.expand_now = 0  # all m.delayed <= expand_upto will be expanded\n        self.early_stop_idx = None\n\n        # Penalties\n        self.ne_length_factor_log = math.log(non_emitting_length_factor)\n\n    def logprob_trans(self, prev_m, edge_m, edge_o,\n                      is_prev_ne=False, is_next_ne=False):\n        # type: (BaseMatcher, BaseMatching, Segment, Segment, bool, bool) -> Tuple[float, Dict[str, Any]]\n        \"\"\"Transition probability.\n\n        Note: In contrast with a regular HMM, this cannot be a probability density function, it needs\n              to be a proper probability (thus values between 0.0 and 1.0).\n\n        :return: probability, properties that are passed to the matching object\n        \"\"\"\n        return 0, {}  # All probabilities are 1 (thus technically not a distribution)\n\n    def logprob_obs(self, dist, prev_m, new_edge_m, new_edge_o, is_ne=False):\n        \"\"\"Emission probability.\n\n        Note: In contrast with a regular HMM, this cannot be a probability density function, it needs\n              to be a proper probability (thus values between 0.0 and 1.0).\n\n        :return: probability, properties that are passed to the matching object\n        \"\"\"\n        return 0, {}\n\n    def match_gpx(self, gpx_file, unique=True):\n        \"\"\"Map matching from a gpx file\"\"\"\n        from ..util.gpx import gpx_to_path\n        path = gpx_to_path(gpx_file)\n        return self.match(path, unique=unique)\n\n    def do_stop(self, logprob_norm, dist, logprob_trans, logprob_obs):\n        if logprob_norm < self.min_logprob_norm:\n            logger.debug(f\"   | Stopped trace: norm(log(Pr)) too small: {logprob_norm} < {self.min_logprob_norm}\"\n                         f\"  -- lPr_t = {logprob_trans:.3f}, lPr_o = {logprob_obs:.3f}\")\n            return True\n        if dist > self.max_dist:\n            logger.debug(f\"   | Stopped trace: distance too large: {dist} > {self.max_dist}\")\n            return True\n        return False\n\n    def _insert(self, m_next):\n        return self.lattice[m_next.obs].upsert(m_next)\n\n    def match(self, path, unique=False, tqdm=None, expand=False):\n        \"\"\"Dynamic Programming based (HMM-like) map matcher.\n\n        If the matcher fails to match the entire path, the last matched index is returned.\n        This index can be used to run the matcher again from that observation onwards.\n\n        :param path: list[Union[tuple[lat, lon], tuple[lat, lon, time]]\n        :param unique: Only retain unique nodes in the sequence (avoid repetitions)\n        :param tqdm: Use a tqdm progress reporter (default is None)\n        :param expand: Expand the current lattice (delayed matches)\n        :return: Tuple of (List of BaseMatching, index of last observation that was matched)\n        \"\"\"\n        if __debug__:\n            logger.debug(\"Start matching path of length {}\".format(len(path)))\n\n        # Initialisation\n        if expand:\n            self.expand_now += 1\n            if self.path != path:\n                is_path_extended = True\n                if len(path) > len(self.path):\n                    for pi, spi in zip(path, self.path):\n                        if pi != spi:\n                            is_path_extended = False\n                            break\n                else:\n                    is_path_extended = False\n                if is_path_extended:\n                    self.lattice[len(self.path) - 1].set_delayed(self.expand_now)\n                    for obs_idx in range(len(self.path), len(path)):\n                        if obs_idx not in self.lattice:\n                            self.lattice[obs_idx] = LatticeColumn(obs_idx)\n                    self.path = path\n                else:\n                    raise Exception(f'Cannot expand for a new path, should be the same path (or an extension).')\n        else:\n            self.path = path\n            self.expand_now = 0\n\n        nb_start_nodes = self._create_start_nodes(use_edges=self.only_edges)\n        if nb_start_nodes == 0:\n            self.lattice_best = []\n            return [], 0\n        if __debug__ and logger.isEnabledFor(logging.DEBUG):\n            self.print_lattice(obs_idx=0, label_width=default_label_width, debug=True)\n\n        # Start iterating over observations 1..end\n        t_start = time.time()\n        iterator = range(1, len(path))\n        if tqdm:\n            iterator = tqdm(iterator)\n        self.early_stop_idx = None\n        for obs_idx in iterator:\n            if __debug__:\n                logger.debug(\"--- obs {} --- {} ---\".format(obs_idx, self.path[obs_idx]))\n            # check if early stopping has occured\n            cnt_lat_size_not_zero = False\n            for m_tmp in self.lattice[obs_idx - 1].values(0):\n                if not m_tmp.stop:\n                    cnt_lat_size_not_zero = True\n                    break\n            # if len(self.lattice[obs_idx - 1]) == 0:\n            if not cnt_lat_size_not_zero:\n                if __debug__:\n                    logger.debug(\"No solutions found anymore\")\n                self.early_stop_idx = obs_idx - 1\n                logger.info(f'Stopped early at observation {self.early_stop_idx}')\n                break\n            # Expand matches\n            self._match_states(obs_idx)\n            if self.non_emitting_states:\n                # Fill in non-emitting states between previous and current observation\n                self._match_non_emitting_states(obs_idx - 1, expand=expand)\n            if self.max_lattice_width:\n                # Prune again if non_emitting_states reactives matches from match_states\n                self.lattice[obs_idx].prune(0, self.max_lattice_width, self.expand_now)\n            if __debug__ and logger.isEnabledFor(logging.DEBUG):\n                self.print_lattice(obs_idx=obs_idx, label_width=default_label_width, debug=True)\n                logger.debug(f\"--- end obs {obs_idx} ---\")\n\n        t_delta = time.time() - t_start\n        logger.info(\"--- end ---\")\n        logger.info(\"Build lattice in {} seconds\".format(t_delta))\n\n        # Backtrack to find best path\n        if not self.early_stop_idx:\n            one_no_stop = False\n            for m in self.lattice[len(path) - 1].values_all():  # todo: could be values(0) ?\n                if not m.stop:\n                    one_no_stop = True\n                    break\n            if not one_no_stop:\n                self.early_stop_idx = len(path) - 1\n        if self.early_stop_idx is not None:\n            if self.early_stop_idx == 0:\n                self.lattice_best = []\n                return [], 0\n            start_idx = self.early_stop_idx - 1\n        else:\n            start_idx = len(self.path) - 1\n        node_path = self._build_node_path(start_idx, unique)\n        return node_path, start_idx\n\n    def _skip_ne_states(self, prev_m):\n        # type: (BaseMatcher, BaseMatching) -> bool\n        return False\n\n    def _create_start_nodes(self, use_edges=True):\n        \"\"\"Find those nodes that are close to the first point in the path.\n\n        :return: Number of created start points.\n        \"\"\"\n        # Initialisation on first observation\n        if self.expand_now > 0:\n            # No need to search for new points, only activate delayed matches\n            self.lattice[0].prune(0, self.max_lattice_width, self.expand_now)\n            return len(self.lattice[0])\n\n        t_start = time.time()\n        self.lattice = dict()\n        for obs_idx in range(len(self.path)):\n            self.lattice[obs_idx] = LatticeColumn(obs_idx)\n\n        if use_edges:\n            nodes = self.map.edges_closeto(self.path[0], max_dist=self.max_dist_init)\n        else:\n            nodes = self.map.nodes_closeto(self.path[0], max_dist=self.max_dist_init)\n        if __debug__:\n            logger.debug(\"--- obs {} --- {} ---\".format(0, self.path[0]))\n        t_delta = time.time() - t_start\n        logger.info(\"Initialized lattice with {} starting points in {} seconds\".format(len(nodes), t_delta))\n        if len(nodes) == 0:\n            logger.info(f'Stopped early at observation 0'\n                        f', no starting points/edges x found for which '\n                        f'|x - ({self.path[0][0]:.2f},{self.path[0][1]:.2f})| < {self.max_dist_init}')\n            return 0\n        if __debug__:\n            logger.debug(self.matching.repr_header())\n        logprob_init = 0  # math.log(1.0/len(nodes))\n        if use_edges:\n            # Search for nearby edges\n            for dist_obs, label1, loc1, label2, loc2, pi, ti in nodes:\n                if label2 == label1:\n                    continue\n                edge_m = Segment(label1, loc1, label2, loc2, pi, ti)\n                edge_o = Segment(f\"O{0}\", self.path[0])\n                m_next = self.matching.first(logprob_init, edge_m, edge_o, self, dist_obs)\n                if m_next is not None:\n                    self.lattice[0].upsert(m_next)\n                    if __debug__:\n                        logger.debug(str(m_next))\n        else:\n            # Search for nearby nodes\n            for dist_obs, label, loc in nodes:\n                edge_m = Segment(label, loc)\n                edge_o = Segment(f\"O{0}\", self.path[0])\n                m_next = self.matching.first(logprob_init, edge_m, edge_o, self, dist_obs)\n                if m_next is not None:\n                    self.lattice[0].upsert(m_next)\n                    if __debug__:\n                        logger.debug(str(m_next))\n        if self.max_lattice_width:\n            self.lattice[0].prune(0, max_lattice_width=self.max_lattice_width, expand_upto=self.expand_now)\n            # if self.non_emitting_states:\n            #     self._match_non_emitting_states(0, path)\n        return len(self.lattice[0])\n\n    def increase_delayed(self, expand_from=None):\n        if expand_from is None:\n            expand_from = self.expand_now + 1\n        for col in self.lattice.values():\n            for colo in col.o:\n                for m in colo.values():\n                    if m.delayed >= expand_from:\n                        m.delayed += 1\n\n    def _match_states(self, obs_idx, prev_lattice=None, max_dist=None, inc_delayed=False):\n        \"\"\"Match states\n\n        :param obs_idx:\n        :param prev_lattice: Start from this list instead of the previous\n            column in the lattice\n        :param max_dist: Use map.*_closeto instead of map.*_nbrto\n        :param inc_delayed: Increase delayed property when new state is created\n        :return: True is new states have been found, False otherwise.\n        \"\"\"\n        if prev_lattice is None:\n            prev_lattice = [m for m in self.lattice[obs_idx - 1].values(0) if not m.stop and m.delayed == self.expand_now]\n        count = 0\n        for m in prev_lattice:  # type: BaseMatching\n            if m.stop:\n                assert False  # should not happen\n                continue\n            count += 1\n            if m.edge_m.is_point():\n                # == Move to neighbour from node ==\n                if max_dist is None:\n                    nbrs = self.map.nodes_nbrto(m.edge_m.l1)\n                else:\n                    nbrs = self.map.nodes_closeto(m.edge_m.p1, max_dist=max_dist)\n                # print(\"Neighbours for {}: {}\".format(m, nbrs))\n                if nbrs is None:\n                    if __debug__:\n                        logger.debug(\"No neighbours found for node {}\".format(m.edge_m.l1))\n                    continue\n                if __debug__:\n                    logger.debug(\"   + Move to {} neighbours from node {}\".format(len(nbrs), m.edge_m.l1))\n                    logger.debug(m.repr_header())\n                for nbr_label, nbr_loc in nbrs:\n                    # === Move from node to node (or stay on node) ===\n                    if not self.only_edges:\n                        edge_m = Segment(nbr_label, nbr_loc)\n                        edge_o = Segment(f\"O{obs_idx}\", self.path[obs_idx])\n                        m_next = m.next(edge_m, edge_o, obs=obs_idx)\n                        if m_next is not None:\n                            if inc_delayed:\n                                m_next.delayed += 1\n                            self._insert(m_next)\n                            if __debug__:\n                                logger.debug(str(m_next))\n\n                    # === Move from node to edge ===\n                    if m.edge_m.l1 != nbr_label:\n                        edge_m = Segment(m.edge_m.l1, m.edge_m.p1, nbr_label, nbr_loc)\n                        edge_o = Segment(f\"O{obs_idx}\", self.path[obs_idx])\n                        m_next = m.next(edge_m, edge_o, obs=obs_idx)\n                        if m_next is not None:\n                            if inc_delayed:\n                                m_next.delayed += 1\n                            self._insert(m_next)\n                            if __debug__:\n                                logger.debug(str(m_next))\n                    else:\n                        if __debug__:\n                            logger.debug(self.matching.repr_static(('x', f'{nbr_label}-{nbr_label} < self-loop')))\n\n            else:\n                # == Move to neighbour from edge ==\n                if __debug__:\n                    logger.debug(\"   + Move to neighbour from edge {}\".format(m.label))\n                    logger.debug(m.repr_header())\n\n                # === Stay on edge ===\n                edge_m = Segment(m.edge_m.l1, m.edge_m.p1, m.edge_m.l2, m.edge_m.p2)\n                edge_o = Segment(f\"O{obs_idx}\", self.path[obs_idx])\n                m_next = m.next(edge_m, edge_o, obs=obs_idx)\n                if m_next is not None:\n                    if inc_delayed:\n                        m_next.delayed += 1\n                    self._insert(m_next)\n                    if __debug__:\n                        logger.debug(str(m_next))\n\n                # === Move from edge to node ===\n                if not self.only_edges:\n                    edge_m = Segment(m.edge_m.l2, m.edge_m.p2)\n                    edge_o = Segment(f\"O{obs_idx}\", self.path[obs_idx])\n                    m_next = m.next(edge_m, edge_o, obs=obs_idx)\n                    if m_next is not None:\n                        if inc_delayed:\n                            m_next.delayed += 1\n                        self._insert(m_next)\n                        if __debug__:\n                            logger.debug(str(m_next))\n\n                else:\n                    # === Move from edge to next edge ===\n                    if max_dist is None:\n                        nbrs = self.map.edges_nbrto((m.edge_m.l1, m.edge_m.l2))  # type: list\n                    else:\n                        nbrs = [(l1, p1, l2, p2) for _, l1, p1, l2, p2, _, _\n                                in self.map.edges_closeto(m.edge_m.pi, max_dist=max_dist)]\n                    if nbrs is None or len(nbrs) == 0:\n                        if __debug__:\n                            logger.debug(f\"No neighbours found for edge {m.edge_m.label}\")\n                        continue\n                    for nbr_label1, nbr_loc1, nbr_label2, nbr_loc2 in nbrs:\n                        # same edge is different action, opposite edge should be allowed to return in a one-way street\n                        if m.edge_m.l2 != nbr_label2 and m.edge_m.l1 != nbr_label1:\n                            edge_m = Segment(nbr_label1, nbr_loc1, nbr_label2, nbr_loc2)\n                            edge_o = Segment(f\"O{obs_idx}\", self.path[obs_idx])\n                            m_next = m.next(edge_m, edge_o, obs=obs_idx)\n                            if m_next is not None:\n                                if inc_delayed:\n                                    m_next.delayed += 1\n                                self._insert(m_next)\n                                if __debug__:\n                                    mstr = str(m_next)\n                                    logger.debug(mstr)\n        if self.max_lattice_width:\n            self.lattice[obs_idx].prune(0, self.max_lattice_width, self.expand_now)\n        if count == 0:\n            if __debug__:\n                logger.debug(\"No active solution found anymore\")\n            return False\n        return True\n\n    def _match_non_emitting_states(self, obs_idx, expand=False):\n        \"\"\"Match sequences of nodes that all refer to the same observation at obs_idx.\n\n        Assumptions:\n        This method assumes that the lattice is filled up for both obs_idx and obs_idx + 1.\n\n        :param obs_idx: Index of the first observation used (the second will be obs_idx + 1)\n        :return: None\n        \"\"\"\n        obs = self.path[obs_idx]\n        if obs_idx < len(self.path) - 1:\n            obs_next = self.path[obs_idx + 1]\n        else:\n            obs_next = None\n        # The current states are the current observation's states\n        if expand:\n            cur_lattice = dict((m.key, m) for m in self.lattice[obs_idx].values(0) if not m.stop and m.delayed == self.expand_now)\n        else:\n            cur_lattice = dict((m.key, m) for m in self.lattice[obs_idx].values(0) if not (m.stop or m.delayed > 0))\n        lattice_toinsert = list()\n        # The current best states are the next observation's states if you would ignore non-emitting states\n        lattice_best = dict((m.shortkey, m)\n                            for m in self.lattice[obs_idx + 1].values(0) if not m.stop)\n        lattice_ne = set(m.shortkey\n                         for m in self.lattice[obs_idx + 1].values(0) if not m.stop and self._skip_ne_states(m))\n        # cur_lattice = set(self.lattice[obs_idx].values())\n        nb_ne = 0\n        prune_thr = None\n        while len(cur_lattice) > 0 and nb_ne < self.non_emitting_states_maxnb:\n            nb_ne += 1\n            if __debug__:\n                logger.debug(\"--- obs {}:{} --- {} - {} ---\".format(obs_idx, nb_ne, obs, obs_next))\n            cur_lattice = self._match_non_emitting_states_inner(cur_lattice, obs_idx, obs, obs_next, nb_ne,\n                                                                lattice_best, lattice_ne)\n            if self.max_lattice_width is not None:\n                self.lattice[obs_idx].prune(nb_ne, self.max_lattice_width, self.expand_now, prune_thr)\n            # Link to next observation\n            self._match_non_emitting_states_end(cur_lattice, obs_idx + 1, obs_next,\n                                                lattice_best, expand=expand)\n            if self.max_lattice_width is not None:\n                prune_thr = self.lattice[obs_idx + 1].prune(0, self.max_lattice_width, self.expand_now, None)\n        if self.max_lattice_width is not None:\n            self.lattice[obs_idx + 1].prune(0, self.max_lattice_width, self.expand_now, None)\n        # logger.info('Used {} levels of non-emitting states'.format(nb_ne))\n        # for m in lattice_toinsert:\n        #     self._insert(m)\n\n    def _node_in_prev_ne(self, m_next, label):\n        \"\"\"Is the given node already visited in the chain of non-emitting states.\n\n        :param m_next:\n        :param label: Node label\n        :return: True or False\n        \"\"\"\n        # for m in itertools.chain(m_next.prev, m_next.prev_other):\n        for m in m_next.prev:  # type: BaseMatching\n            if m.obs != m_next.obs:\n                return False\n            assert(m_next.obs_ne != m.obs_ne)\n            # print('prev', m.shortkey, 'checking for ', label)\n            # if label == m.shortkey:\n            if label in m.nodes:\n                return True\n            if m.obs_ne == 0:\n                return False\n            if self._node_in_prev_ne(m, label):\n                return True\n        return False\n\n    @staticmethod\n    def _insert_tmp(m_next, lattice):\n        if m_next.key in lattice:\n            return lattice[m_next.key].update(m_next)\n        else:\n            lattice[m_next.key] = m_next\n            return True\n\n    def _match_non_emitting_states_inner(self, cur_lattice, obs_idx, obs, obs_next, nb_ne,\n                                         lattice_best, lattice_ne):\n        # cur_lattice_new = dict()\n        cur_lattice_new = self.lattice[obs_idx].dict(nb_ne)\n        for m in cur_lattice.values():  # type: BaseMatching\n            if m.stop or m.delayed != self.expand_now:\n                continue\n            if m.shortkey in lattice_ne:\n                logger.debug(f\"Skip non-emitting states from {m.label}, already visited\")\n                continue\n            # == Move to neighbour edge from edge ==\n            if m.edge_m.l2 is not None and self.only_edges:\n                nbrs = self.map.edges_nbrto((m.edge_m.l1, m.edge_m.l2))\n                # print(\"Neighbours for {}: {}\".format(m, nbrs))\n                if nbrs is None or len(nbrs) == 0:\n                    if __debug__:\n                        logger.debug(f\"No neighbours found for edge {m.edge_m.label} ({m.label}, non-emitting)\")\n                    continue\n                for nbr_label1, nbr_loc1, nbr_label2, nbr_loc2 in nbrs:\n                    if self._node_in_prev_ne(m, nbr_label2):\n                        if __debug__:\n                            logger.debug(self.matching.repr_static(('x', '{} < node in prev ne'.format(nbr_label2))))\n                        continue\n                    # === Move to next edge ===\n                    if m.edge_m.l2 != nbr_label2 and m.edge_m.l1 != nbr_label2:\n                        edge_m = Segment(nbr_label1, nbr_loc1, nbr_label2, nbr_loc2)\n                        edge_o = Segment(f\"O{obs_idx}\", obs, f\"O{obs_idx+1}\", obs_next)\n                        m_next = m.next(edge_m, edge_o, obs=obs_idx, obs_ne=nb_ne)\n                        if m_next is not None:\n                            if m_next.key in cur_lattice_new:\n                                if m_next.shortkey in lattice_best:\n                                    if approx_leq(m_next.dist_obs, lattice_best[m_next.shortkey].dist_obs):\n                                        cur_lattice_new[m_next.key].update(m_next)\n                                    else:\n                                        m_next.stop = True\n                                        if __debug__ and logger.isEnabledFor(logging.DEBUG):\n                                            logger.debug(f\"   | Stopped trace: distance larger than best for key {m_next.shortkey}: \"\n                                                         f\"{m_next.dist_obs} > {lattice_best[m_next.shortkey].dist_obs}\")\n                                else:\n                                    cur_lattice_new[m_next.key].update(m_next)\n                            else:\n                                if m_next.shortkey in lattice_best:\n                                    # if m_next.logprob > lattice_best[m_next.shortkey].logprob:\n                                    if approx_leq(m_next.dist_obs, lattice_best[m_next.shortkey].dist_obs):\n                                        cur_lattice_new[m_next.key] = m_next\n                                        # lattice_best[m_next.shortkey] = m_next\n                                        # lattice_toinsert.append(m_next)\n                                    else:\n                                        if __debug__ and logger.isEnabledFor(logging.DEBUG):\n                                            logger.debug(f\"   | Stopped trace: distance larger than best for key {m_next.shortkey}: \"\n                                                         f\"{m_next.dist_obs} > {lattice_best[m_next.shortkey].dist_obs}\")\n                                        m_next.stop = True\n                                else:\n                                    cur_lattice_new[m_next.key] = m_next\n                                    # lattice_best[m_next.shortkey] = m_next\n                                    # lattice_toinsert.append(m_next)\n                            # cur_lattice_new.add(m_next)\n                            if __debug__:\n                                logger.debug(str(m_next))\n                    else:\n                        if __debug__:\n                            logger.debug(self.matching.repr_static(('x', f'{nbr_label1}-{nbr_label2} < goes back (ne)')))\n            # == Move to neighbour node from node==\n            if m.edge_m.l2 is None and not self.only_edges:\n                cur_node = m.edge_m.l1\n                nbrs = self.map.nodes_nbrto(cur_node)\n                if nbrs is None:\n                    if __debug__:\n                        logger.debug(\n                            f\"No neighbours found for node {cur_node} ({m.label}, non-emitting)\")\n                    continue\n                if __debug__:\n                    logger.debug(\n                        f\"   + Move to {len(nbrs)} neighbours from node {cur_node} ({m.label}, non-emitting)\")\n                    logger.debug(m.repr_header())\n                for nbr_label, nbr_loc in nbrs:\n                    # print(f\"self._node_in_prev_ne({m.label}, {nbr_label}) = {self._node_in_prev_ne(m, nbr_label)}\")\n                    if self._node_in_prev_ne(m, nbr_label):\n                        if __debug__:\n                            logger.debug(self.matching.repr_static(('x', '{} < node in prev ne'.format(nbr_label))))\n                        continue\n                    # === Move to next node ===\n                    if m.edge_m.l1 != nbr_label:\n                        edge_m = Segment(nbr_label, nbr_loc)\n                        edge_o = Segment(f\"O{obs_idx}\", obs, f\"O{obs_idx+1}\", obs_next)\n                        m_next = m.next(edge_m, edge_o, obs=obs_idx, obs_ne=nb_ne)\n                        if m_next is not None:\n                            if m_next.key in cur_lattice_new:\n                                cur_lattice_new[m_next.key].update(m_next)\n                            else:\n                                if m_next.shortkey in lattice_best:\n                                    # if m_next.logprob > lattice_best[m_next.shortkey].logprob:\n                                    if m_next.dist_obs < lattice_best[m_next.shortkey].dist_obs:\n                                        cur_lattice_new[m_next.key] = m_next\n                                        lattice_best[m_next.shortkey] = m_next\n                                        # lattice_toinsert.append(m_next)\n                                    elif __debug__ and logger.isEnabledFor(logging.DEBUG):\n                                        m_next.stop = True\n                                        cur_lattice_new[m_next.key] = m_next\n                                        # lattice_toinsert.append(m_next)\n                                else:\n                                    cur_lattice_new[m_next.key] = m_next\n                                    lattice_best[m_next.shortkey] = m_next\n                                    # lattice_toinsert.append(m_next)\n                            # cur_lattice_new.add(m_next)\n                            if __debug__:\n                                logger.debug(str(m_next))\n                    else:\n                        if __debug__:\n                            logger.debug(f\"x  | {m.edge_m.l1}-{nbr_label} < self-loop\")\n\n        return cur_lattice_new\n\n    def _match_non_emitting_states_end(self, cur_lattice, obs_idx, obs_next,\n                                       lattice_best, expand=False):\n        for m in cur_lattice.values():  # type: BaseMatching\n            if m.stop or m.delayed > self.expand_now:\n                continue\n            if m.edge_m.l2 is not None:\n                # Move to neighbour edge from edge\n                nbrs = self.map.edges_nbrto((m.edge_m.l1, m.edge_m.l2))\n                # print(\"Neighbours for {}: {}\".format(m, nbrs))\n                if nbrs is None or len(nbrs) == 0:\n                    if __debug__:\n                        logger.debug(\"No neighbours found for edge {} ({})\".format(m.edge_m.label, m.label))\n                    continue\n                if __debug__:\n                    logger.debug(f\"   + Move to {len(nbrs)} neighbours from edge {m.edge_m.label} \"\n                                 f\"({m.label}, non-emitting->emitting)\")\n                    logger.debug(m.repr_header())\n                for nbr_label1, nbr_loc1, nbr_label2, nbr_loc2 in nbrs:\n                    if self._node_in_prev_ne(m, nbr_label2):\n                        if __debug__:\n                            logger.debug(self.matching.repr_static(('x', '{} < node in prev ne'.format(nbr_label2))))\n                        continue\n                    # Move to next edge\n                    if m.edge_m.l1 != nbr_label2 and m.edge_m.l2 != nbr_label2:\n                        edge_m = Segment(nbr_label1, nbr_loc1, nbr_label2, nbr_loc2)\n                        edge_o = Segment(f\"O{obs_idx+1}\", obs_next)\n                        m_next = m.next(edge_m, edge_o, obs=obs_idx)\n                        if m_next is not None:\n                            if m_next.shortkey in lattice_best:\n                                # if m_next.dist_obs < lattice_best[m_next.shortkey].dist_obs:\n                                if m_next.logprob > lattice_best[m_next.shortkey].logprob:\n                                    lattice_best[m_next.shortkey] = m_next\n                                    # lattice_toinsert.append(m_next)\n                                    self.lattice[obs_idx].upsert(m_next)\n                                elif __debug__ and logger.isEnabledFor(logging.DEBUG):\n                                    m_next.stop = True\n                                    # lattice_toinsert.append(m_next)\n                                    self.lattice[obs_idx].upsert(m_next)\n                            else:\n                                lattice_best[m_next.shortkey] = m_next\n                                # lattice_toinsert.append(m_next)\n                                self.lattice[obs_idx].upsert(m_next)\n                            if __debug__:\n                                logger.debug(str(m_next))\n                    else:\n                        if __debug__:\n                            logger.debug(self.matching.repr_static(('x', '{} < going back'.format(nbr_label2))))\n            else:  # m.edge_m.l2 is None:\n                # Move to neighbour node from node\n                cur_node = m.edge_m.l1\n                nbrs = self.map.nodes_nbrto(cur_node)\n                # print(\"Neighbours for {}: {}\".format(m, nbrs))\n                if nbrs is None:\n                    if __debug__:\n                        logger.debug(\"No neighbours found for node {}\".format(cur_node, m.label))\n                    continue\n                if __debug__:\n                    logger.debug(f\"   + Move to {len(nbrs)} neighbours from node {cur_node} \"\n                                 f\"({m.label}, non-emitting->emitting)\")\n                    logger.debug(m.repr_header())\n                for nbr_label, nbr_loc in nbrs:\n                    if self._node_in_prev_ne(m, nbr_label):\n                        if __debug__:\n                            logger.debug(self.matching.repr_static(('x', '{} < node in prev ne'.format(nbr_label))))\n                        continue\n                    # Move to next node\n                    if m.edge_m.l1 != nbr_label:\n                        # edge_m = Segment(m.edge_m.l1, m.edge_m.p1, nbr_label, nbr_loc)\n                        edge_m = Segment(nbr_label, nbr_loc)\n                        edge_o = Segment(f\"O{obs_idx+1}\", obs_next)\n                        m_next = m.next(edge_m, edge_o, obs=obs_idx)\n                        if m_next is not None:\n                            if m_next.shortkey in lattice_best:\n                                # if m_next.dist_obs < lattice_best[m_next.shortkey].dist_obs:\n                                if m_next.logprob > lattice_best[m_next.shortkey].logprob:\n                                    lattice_best[m_next.shortkey] = m_next\n                                    # lattice_toinsert.append(m_next)\n                                    self.lattice[obs_idx].upsert(m_next)\n                                elif __debug__ and logger.isEnabledFor(logging.DEBUG):\n                                    m_next.stop = True\n                                    # lattice_toinsert.append(m_next)\n                                    self.lattice[obs_idx].upsert(m_next)\n                            else:\n                                lattice_best[m_next.shortkey] = m_next\n                                # lattice_toinsert.append(m_next)\n                                self.lattice[obs_idx].upsert(m_next)\n                            if __debug__:\n                                logger.debug(str(m_next))\n                    else:\n                        if __debug__:\n                            logger.debug(self.matching.repr_static(('x', '{} < self-loop'.format(nbr_label))))\n\n    def get_matching(self, identifier=None):\n        m = None  # type: Optional[BaseMatching]\n        if isinstance(identifier, BaseMatching):\n            m = identifier\n        elif identifier is None:\n            col = self.lattice[len(self.lattice) - 1]\n            for curm in col.values_all():\n                if m is None or curm.logprob > m.logprob:\n                    m = curm\n        elif type(identifier) is int:\n            # If integer, search for the best matching at this index in the lattice\n            for cur_m in self.lattice[identifier].values_all():  # type:BaseMatching\n                if not cur_m.stop and (m is None or cur_m.logprob > m.logprob):\n                    m = cur_m\n        elif type(identifier) is str:\n            # If string, try to parse identifier\n            parts = identifier.split('-')\n            idx, ne, key = None, None, None\n            if len(parts) == 4:\n                nodea, nodeb, idx, ne = [int(part) for part in parts]\n                key = (nodea, nodeb, idx, ne)\n                col = self.lattice[idx]  # type: LatticeColumn\n                col_ne = col.o[ne]\n                m = col_ne[key]\n            elif len(parts) == 3:\n                node, idx, ne = [int(part) for part in parts]\n                key = (node, idx, ne)\n                col = self.lattice[idx]  # type: LatticeColumn\n                col_ne = col.o[ne]\n                m = col_ne[key]\n            elif len(parts) == 1:\n                m = None\n                l1 = int(parts[0])\n                for l in self.lattice.values():  # type: LatticeColumn\n                    for curm in l.values_all():\n                        if (curm.edge_m.l1 == l1 or curm.edge_m.l2 == l1) and \\\n                                (m is None or curm.logprob > m.logprob):\n                            m = curm\n            else:\n                raise AttributeError(f'Unknown string format for matching. '\n                                     'Expects <node>-<idx>-<ne> or <node>-<node>-<idx>-<ne>.')\n\n        return m\n\n    def get_matching_path(self, start_m):\n        \"\"\"List of Matching objects that end in the given Matching object.\"\"\"\n        start_m = self.get_matching(start_m)\n        return self._build_matching_path(start_m)\n\n    def get_node_path(self, start_m, only_nodes=False):\n        \"\"\"List of node/edge names that end in the given Matching object.\"\"\"\n        path = self.get_matching_path(start_m)\n        node_path = [m.shortkey for m in path]\n        if only_nodes:\n            node_path = self.node_path_to_only_nodes(node_path)\n        return node_path\n\n    def get_path(self, only_nodes=True, allow_jumps=False, only_closest=True):\n        \"\"\"A list with all the nodes (no edges) the matched path passes through.\"\"\"\n        if only_nodes is False:\n            return self.node_path\n        if self.node_path is None or len(self.node_path) == 0:\n            return []\n        path = self.node_path_to_only_nodes(self.node_path, allow_jumps=allow_jumps)\n        if only_closest:\n            m = self.lattice_best[0]\n            if m.edge_m.ti > 0.5:\n                path.pop(0)\n        return path\n\n    def node_path_to_only_nodes(self, path, allow_jumps=False):\n        \"\"\"Path of nodes and edges to only nodes.\n\n        :param path: List of node names or edges as (node name, node name)\n        :param allow_jumps: Allow a path over edges that are not connected.\n            This occurs when matches are added without an edge, for example,\n            when searching for edges in the distance neighborhood instead in\n            the graph.\n        :return: List of node names\n        \"\"\"\n        nodes = []\n        prev_state = path[0]\n        if type(prev_state) is tuple:\n            nodes.append(prev_state[0])\n            nodes.append(prev_state[1])\n            prev_node = prev_state[1]\n        else:\n            nodes.append(prev_state)\n            prev_node = prev_state\n        for state in path[1:]:\n            if state == prev_state:\n                continue\n            if type(state) is not tuple:\n                if state != prev_node:\n                    nodes.append(state)\n                    prev_node = state\n            elif type(state) is tuple:\n                if state[0] == prev_node:\n                    if state[1] != prev_node:\n                        nodes.append(state[1])\n                        prev_node = state[1]\n                elif state[1] == prev_node:\n                    if state[0] != prev_node:\n                        nodes.append(state[0])\n                        prev_node = state[0]\n                elif not allow_jumps:\n                    raise Exception(f\"State {state} does not have as previous node {prev_node}\")\n                else:\n                    nodes.append(state[0])\n                    nodes.append(state[1])\n                    prev_node = state[1]\n            else:\n                raise Exception(f\"Unknown type of state: {state} ({type(state)})\")\n            prev_state = state\n        return nodes\n\n    def _build_matching_path(self, start_m, max_depth=None):\n        lattice_best = []\n        node_max = start_m\n        cur_depth = 0\n        if __debug__ and logger.isEnabledFor(logging.DEBUG):\n            logger.debug(self.matching.repr_header(stop=\"             \"))\n        logger.debug(\"Start ({}): {}\".format(node_max.obs, node_max))\n        lattice_best.append(node_max)\n        if node_max.is_emitting():\n            cur_depth += 1\n        # for obs_idx in reversed(range(start_idx)):\n        if max_depth is None:\n            max_depth = len(self.lattice) + 1\n        while cur_depth < max_depth and len(node_max.prev) > 0:\n            node_max_last = node_max\n            node_max: Optional[BaseMatching] = None\n            for prev_m in node_max_last.prev:\n                if prev_m is not None and (node_max is None or prev_m.logprob > node_max.logprob):\n                    node_max = prev_m\n            if node_max is None:\n                logger.error(\"Did not find a matching node for path point at index {}. \".format(node_max_last.obs) +\n                             \"Stopped building path.\")\n                break\n            logger.debug(\"Max   ({}): {}\".format(node_max.obs, node_max))\n            lattice_best.append(node_max)\n            if node_max.is_emitting():\n                cur_depth += 1\n        lattice_best = list(reversed(lattice_best))\n        return lattice_best\n\n    def _build_node_path(self, start_idx, unique=True, max_depth=None, last_is_e=False):\n        \"\"\"Build the path from the lattice.\n\n        :param start_idx:\n        :param unique:\n        :param max_depth:\n        :param last_is_e: Last matched lattice node should be an emitting state.\n            In case the matching stops early, the longest path can be in between two observations\n            and thus be a nonemitting state (which by definition has a lower probability than the\n            last emitting state). If this argument is set to true, the longer match is preferred.\n        :return:\n        \"\"\"\n        node_max = None\n        node_max_ne = 0\n        if last_is_e:\n            for m in self.lattice[start_idx].values_all():  # type:BaseMatching\n                if not m.stop and (node_max is None or m.logprob > node_max.logprob):\n                    node_max = m\n        else:\n            for m in self.lattice[start_idx].values_all():  # type:BaseMatching\n                if not m.stop and (node_max is None or m.obs_ne > node_max_ne or m.logprob > node_max.logprob):\n                    node_max_ne = m.obs_ne\n                    node_max = m\n        if node_max is None:\n            logger.error(\"Did not find a matching node for path point at index {}\".format(start_idx))\n            return None\n\n        self.lattice_best = self._build_matching_path(node_max, max_depth)\n\n        node_path = [m.shortkey for m in self.lattice_best]\n        if unique:\n            self.node_path = []\n            prev_node = None\n            for node in node_path:\n                if node != prev_node:\n                    self.node_path.append(node)\n                    prev_node = node\n        else:\n            self.node_path = node_path\n        return self.node_path\n\n    def increase_max_lattice_width(self, max_lattice_width, unique=False, tqdm=None):\n        \"\"\"Increase the value for max_lattice_width and continue the matching with all\n        paths that were ignored so far (up to the new max_lattice_width).\n\n        This is useful when the matcher is first run with a small max_lattice_width\n        to be fast, but when the true path is not obvious and excluded from the first\n        guesses. When the matcher stops early, this method allows to easily expand the\n        search space.\n\n        :param max_lattice_width: New maximal number of paths to consider\n        :param unique: See match method\n        :param tqdm: See match method\n        \"\"\"\n        self.max_lattice_width = max_lattice_width\n        return self.match(self.path, unique=unique, tqdm=tqdm, expand=True)\n\n    def continue_with_distance(self, from_matches=None, k=2, nb_obs=2, max_dist=None):\n        \"\"\"Continue the matcher but ignore edges and allow jumps\n        to nearby edged.\n\n        :param from_matches: Search in the neigborhood of these matches\n        :param k: If from_matches is not given, the k best matches are used\n            in the last nb_obs observations since last early_stop_idx\n        :praram nb_obs: If from_matches is not given, the k best matches are used\n            in the last nb_obs observations since last early_stop_idx\n        :param max_dist: Add edges that are maximally max_dist away from the previous\n            match. If none, self.max_dist * 3 is used.\n        \"\"\"\n        if from_matches is None:\n            from_matches = self.best_last_matches(k=k, nb_obs=nb_obs)\n        self.increase_delayed()\n        if max_dist is None:\n            max_dist = self.max_dist * 3\n        for obs_idx, cur_matches in from_matches.items():\n            self._match_states(obs_idx, prev_lattice=cur_matches, max_dist=max_dist, inc_delayed=True)\n\n    def path_bb(self):\n        \"\"\"Get boundig box of matched path (if it exists, otherwise return None).\"\"\"\n        path = self.path\n        plat, plon = islice(zip(*path), 2)\n        lat_min, lat_max = min(plat), max(plat)\n        lon_min, lon_max = min(plon), max(plon)\n        bb = lat_min, lon_min, lat_max, lon_max\n        return bb\n\n    def print_lattice(self, file=None, obs_idx=None, obs_ne=0, label_width=None, debug=False):\n        if debug:\n            xprint = logger.debug\n        else:\n            if file is None:\n                file = sys.stdout\n            xprint = lambda arg: print(arg, file=file)\n        # print(\"Lattice:\", file=file)\n        if obs_idx is not None:\n            idxs = [obs_idx]\n        else:\n            idxs = range(len(self.lattice))\n        for idx in idxs:\n            if len(self.lattice[idx]) > 0:\n                if label_width is None:\n                    label_width = 0\n                    for m in self.lattice[idx].values(obs_ne):\n                        label_width = max(label_width, len(str(m.label)))\n                xprint(\"--- obs {} ---\".format(idx))\n                xprint(self.matching.repr_header(label_width=label_width))\n                for m in sorted(self.lattice[idx].values(obs_ne), key=lambda t: str(t.label)):\n                    xprint(m.__str__(label_width=label_width))\n\n    def lattice_dot(self, file=None, precision=None, render=False):\n        \"\"\"Write the lattice as a Graphviz DOT file.\n\n        :param file: File object to print to. Prints to stdout if None.\n        :param precision: Precision of (log) probabilities.\n        :param render: Try to render the generated Graphviz file.\n        \"\"\"\n        if file is None:\n            file = sys.stdout\n        if precision is None:\n            prfmt = ''\n        else:\n            prfmt = f'.{precision}f'\n        print('digraph lattice {', file=file)\n        print('\\trankdir=LR;', file=file)\n        # Vertices\n        for idx_ob in range(len(self.lattice)):\n            col = self.lattice[idx_ob]\n            for idx_ne in range(len(col)):\n                ms = col.values(idx_ne)\n                if len(ms) == 0:\n                    continue\n                cnames = [(m.obs_ne, m.cname, m.stop, m.delayed) for m in ms]\n                cnames.sort()\n                cur_obs_ne = -1\n                print('\\t{\\n\\t\\trank=same; ', file=file)\n                for obs_ne, cname, stop, delayed in cnames:\n                    if obs_ne != cur_obs_ne:\n                        if cur_obs_ne != -1:\n                            print('\\t};\\n\\t{\\n\\t\\trank=same; ', file=file)\n                        cur_obs_ne = obs_ne\n                    if stop:\n                        options = 'label=\"{} x\",color=gray,fontcolor=gray'.format(cname)\n                    elif delayed > self.expand_now:\n                        options = 'label=\"{} d{}\",color=gray,fontcolor=gray'.format(cname, delayed)\n                    elif self.expand_now != 0:\n                        options = 'label=\"{} d{}\"'.format(cname, delayed)\n                    else:\n                        options = 'label=\"{}  \"'.format(cname)\n                    print('\\t\\t{} [{}];'.format(cname, options), file=file)\n                print('\\t};', file=file)\n        # Edges\n        for idx_ob in range(len(self.lattice)):\n            col = self.lattice[idx_ob]\n            for idx_ne in range(len(col)):\n                ms = col.values(idx_ne)\n                if len(ms) == 0:\n                    continue\n                for m in ms:\n                    for mp in m.prev:\n                        if m.stop or m.delayed > self.expand_now:\n                            options = ',color=gray,fontcolor=gray'\n                        else:\n                            options = ''\n                        print(f'\\t {mp.cname} -> {m.cname} [label=\"{m.logprob:{prfmt}}\"{options}];', file=file)\n                    for mp in m.prev_other:\n                        if m.stop or m.delayed > self.expand_now:\n                            options = ',color=gray,fontcolor=gray'\n                        else:\n                            options = ''\n                        print(f'\\t {mp.cname} -> {m.cname} [color=gray,label=\"{m.logprob:{prfmt}}\"{options}];', file=file)\n        print('}', file=file)\n        if render and file is not None:\n            import subprocess as sp\n            from pathlib import Path\n            from io import TextIOWrapper\n            if isinstance(file, Path):\n                fn = str(file.canonical())\n            elif isinstance(file, TextIOWrapper):\n                file.flush()\n                fn = file.name\n            else:\n                fn = str(file)\n            cmd = ['dot', '-Tpdf', '-O', fn]\n            logger.debug(' '.join(cmd))\n            sp.call(cmd)\n\n    def print_lattice_stats(self, file=None, verbose=False):\n        if file is None:\n            file = sys.stdout\n        print(\"Stats lattice\", file=file)\n        print(\"-------------\", file=file)\n        stats = OrderedDict()\n        stats[\"nbr levels\"] = len(self.lattice) if self.lattice else \"?\"\n        total_nodes = 0\n        max_nodes = 0\n        min_nodes = 9999999\n        if self.lattice:\n            sizes = []\n            for idx in range(len(self.lattice)):\n                level = self.lattice[idx].values(0)\n                # stats[\"#nodes[{}]\".format(idx)] = len(level)\n                sizes.append(len(level))\n                total_nodes += len(level)\n                if len(level) < min_nodes:\n                    min_nodes = len(level)\n                if len(level) > max_nodes:\n                    max_nodes = len(level)\n            stats[\"nbr lattice\"] = total_nodes\n            if verbose:\n                stats[\"nbr lattice[level]\"] = \", \".join([str(s) for s in sizes])\n            stats[\"avg lattice[level]\"] = total_nodes/len(self.lattice)\n            stats[\"min lattice[level]\"] = min_nodes\n            stats[\"max lattice[level]\"] = max_nodes\n        if self.lattice_best and len(self.lattice_best) > 0:\n            stats[\"avg obs distance\"] = np.mean([m.dist_obs for m in self.lattice_best])\n            stats[\"last logprob\"] = self.lattice_best[-1].logprob\n            stats[\"last length\"] = self.lattice_best[-1].length\n            stats[\"last norm logprob\"] = self.lattice_best[-1].logprob / self.lattice_best[-1].length\n            if verbose:\n                stats[\"best logprob\"] = \", \".join([\"{:.3f}\".format(m.logprob) for m in self.lattice_best])\n                stats[\"best norm logprob\"] = \\\n                    \", \".join([\"{:.3f}\".format(m.logprob/m.length) for i, m in enumerate(self.lattice_best)])\n                stats[\"best norm prob\"] = \\\n                    \", \".join([\"{:.3f}\".format(math.exp(m.logprob/m.length)) for i, m in enumerate(self.lattice_best)])\n        for key, val in stats.items():\n            print(\"{:<24} : {}\".format(key, val), file=file)\n\n    def node_counts(self):\n        if self.lattice is None:\n            return None\n        counts = defaultdict(lambda: 0)\n        for level in self.lattice.values():\n            for m in level.values_all():\n                counts[m.label] += 1\n        return counts\n\n    def inspect_early_stopping(self):\n        \"\"\"Analyze the lattice and try to find most plausible reason why the\n        matching stopped early and print to stdout.\"\"\"\n        if self.early_stop_idx is None:\n            print(\"No early stopping.\")\n            return\n        col = self.lattice[self.early_stop_idx - 1]\n        print(\"The last matched nodes or edges were:\")\n        first_row = True\n        ignore = set()\n        for ne_i in range(len(col.o) - 1, -1, -1):\n            for v in col.o[ne_i].values():\n                if v.key not in ignore:\n                    if first_row:\n                        print(v.repr_header())\n                        first_row = False\n                    print(v)\n                ignore.update(r.key for r in v.prev)\n\n    def best_last_matches(self, k=1, nb_obs=3):\n        \"\"\"Return the k best last matches.\n\n        :param k: Number of best matches to keep for an observation\n        :param nb_obs: How many last matched observations to consider\n        \"\"\"\n        import heapq\n        if self.early_stop_idx is None:\n            col_idx = len(self.lattice) - 1\n        else:\n            col_idx = self.early_stop_idx - 1\n        hh = []\n        obs_cnt = 0\n        while col_idx >= 0 and obs_cnt < nb_obs:\n            h = []\n            col = self.lattice[col_idx]\n            col_oneselected = False\n            for ne_i in range(len(col.o) - 1, -1, -1):\n                for v in col.o[ne_i].values():\n                    if v.stop:\n                        continue\n                    if len(h) < k:\n                        heapq.heappush(h, (v.logprob, v))\n                        col_oneselected = True\n                    elif v.logprob > h[0][0]:\n                        heapq.heappop(h)\n                        heapq.heappush(h, (v.logprob, v))\n                        col_oneselected = True\n            hh.extend(h)\n            if col_oneselected is False:\n                print(f'break in {col_idx=}')\n                break\n            col_idx -= 1\n            obs_cnt += 1\n        result = defaultdict(list)\n        for m in hh:\n            m = m[1]\n            result[m.obs + 1].append(m)\n        # return [m[1] for m in hh]\n        return result\n\n    def copy_lastinterface(self, nb_interfaces=1):\n        \"\"\"Copy the current matcher and keep the last interface as the start point.\n\n        This method allows you to perform incremental matching without keeping the entire\n        lattice in memory.\n\n        You need to run :meth:`match_incremental` on this object to continue from the existing\n        (partial) lattice. Otherwise, if you use :meth:`match`, it will be overwritten.\n\n        Open question, if there is no need to keep track of older lattices, it will probably\n        be more efficient to clear the older parts of the interface instead of copying the newer\n        parts.\n\n        :param nb_interfaces: Nb of interfaces (columns in lattice) to keep. Default is 1, the last one.\n        :return: new Matcher object\n        \"\"\"\n        matcher = self.__class__(self.map, obs_noise=self.obs_noise, max_dist_init=self.max_dist_init,\n                                 max_dist=self.max_dist, min_prob_norm=self.min_logprob_norm,\n                                 non_emitting_states=self.non_emitting_states,\n                                 max_lattice_width=self.max_lattice_width, only_edges=self.only_edges,\n                                 obs_noise_ne=self.obs_noise_ne, matching=self.matching,\n                                 avoid_goingback=self.avoid_goingback,\n                                 non_emitting_length_factor=math.exp(self.ne_length_factor_log))\n        matcher.lattice = []\n        matcher.path = []\n        for int_i in range(len(self.lattice) - nb_interfaces, len(self.lattice)):\n            matcher.lattice.append(self.lattice[int_i])\n            matcher.path.append(self.path[int_i])\n        return matcher\n\n    @property\n    def path_pred(self):\n        \"\"\"The matched path, both nodes and/or edges (depending on your settings).\"\"\"\n        return self.node_path\n\n    @property\n    def path_pred_onlynodes(self):\n        \"\"\"A list with all the nodes (no edges) the matched path passes through.\"\"\"\n        return self.get_path(only_nodes=True, allow_jumps=False)\n\n    @property\n    def path_pred_onlynodes_withjumps(self):\n        \"\"\"A list with all the nodes (no edges) the matched path passes through.\"\"\"\n        return self.get_path(only_nodes=True, allow_jumps=True)\n\n    def path_pred_distance(self):\n        \"\"\"Total distance of the matched path.\"\"\"\n        if self.lattice_best is None:\n            return None\n        if len(self.lattice_best) == 1:\n            return 0\n        dist = 0\n        m_prev = self.lattice_best[0]\n        for idx, m in enumerate(self.lattice_best[1:]):\n            if m_prev.edge_m.label != m.edge_m.label and m_prev.edge_m.l2 == m.edge_m.l1:\n                # Go over the connection between two edges to compute the distance\n                cdist = self.map.distance(m_prev.edge_m.pi, m_prev.edge_m.p2)\n                cdist += self.map.distance(m_prev.edge_m.p2, m.edge_m.pi)\n            else:\n                cdist = self.map.distance(m_prev.edge_m.pi, m.edge_m.pi)\n            dist += cdist\n            m_prev = m\n        return dist\n\n    def path_distance(self):\n        \"\"\"Total distance of the observations.\"\"\"\n        if self.lattice_best is None:\n            return None\n        if len(self.lattice_best) == 1:\n            return 0\n        dist = 0\n        m_prev = self.lattice_best[0]\n        for m in self.lattice_best[1:]:\n            dist += self.map.distance(m_prev.edge_o.pi, m.edge_o.pi)\n            m_prev = m\n        return dist\n\n    def path_all_distances(self):\n        \"\"\"Return a list of all distances between observed trace and map.\n\n        One entry for each point in the map and point in the trace that are mapped to each other.\n        In case non-emitting nodes are used, extra entries can be present where a point in the trace\n        or a point in the map is mapped to a segment.\n        \"\"\"\n        path = self.lattice_best\n        dists = [m.dist_obs for m in path]\n        return dists\n"
  },
  {
    "path": "leuvenmapmatching/matcher/distance.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.matcher.distance\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport logging\nimport math\n\nfrom .base import BaseMatching, BaseMatcher\nfrom ..util.segment import Segment\nfrom ..util.debug import printd\n\nMYPY = False\nif MYPY:\n    from typing import Tuple, Any, Dict\n\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n\nclass DistanceMatching(BaseMatching):\n    __slots__ = ['d_s', 'd_o', 'lpe', 'lpt']  # Additional fields\n\n    def __init__(self, *args, d_s=0.0, d_o=0.0, lpe=0.0, lpt=0.0, **kwargs):\n        \"\"\"\n\n        :param args: Arguments for BaseMatching\n        :param d_s: Distance between two (interpolated) states\n        :param d_o: Distance between two (interpolated) observations\n        :param lpe: Log probability of emission\n        :param lpt: Log probablity of transition\n        :param kwargs: Arguments for BaseMatching\n        \"\"\"\n        super().__init__(*args, **kwargs)\n        self.d_o: float = d_o\n        self.d_s: float = d_s\n        self.lpe: float = lpe\n        self.lpt: float = lpt\n\n    def _update_inner(self, m_other):\n        # type: (DistanceMatching, DistanceMatching) -> None\n        super()._update_inner(m_other)\n        self.d_s = m_other.d_s\n        self.d_o = m_other.d_o\n        self.lpe = m_other.lpe\n        self.lpt = m_other.lpt\n\n    @staticmethod\n    def repr_header(label_width=None, stop=\"\"):\n        res = BaseMatching.repr_header(label_width)\n        res += f\" {'dt(o)':<6} | {'dt(s)':<6} |\"\n        if logger.isEnabledFor(logging.DEBUG):\n            res += f\" {'lg(Pr-t)':<9} | {'lg(Pr-e)':<9} |\"\n        return res\n\n    def __str__(self, label_width=None):\n        res = super().__str__(label_width)\n        res += f\" {self.d_o:>6.2f} | {self.d_s:>6.2f} |\"\n        if logger.isEnabledFor(logging.DEBUG):\n            res += f\" {self.lpt:>9.2f} | {self.lpe:>9.2f} |\"\n        return res\n\n    def __repr__(self):\n        return self.label\n\n\nclass DistanceMatcher(BaseMatcher):\n    \"\"\"\n    Map Matching that takes into account the distance between matched locations on the map compared to\n    the distance between the observations (that are matched to these locations). It thus prefers matched\n    paths that have a similar distance than the observations.\n\n    Inspired on the method presented in:\n\n        P. Newson and J. Krumm. Hidden markov map matching through noise and sparseness.\n        In Proceedings of the 17th ACM SIGSPATIAL international conference on advances\n        in geographic information systems, pages 336–343. ACM, 2009.\n\n    The options available in :class:``BaseMatcher`` are inherited. Additionally, this class\n    offers:\n\n    - Transition probability is lower if the distance between observations and states is different\n    - Transition probability is lower if the the next match is going back on an edge or to a previous edge\n    - Transition probability is lower if two neighboring states represent not connected edges\n    - Skip non-emitting states if distance between states and observations is close to each other\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"Create a new object.\n\n        :param map_con: Map object to connect to map database\n        :param obs_noise: Standard deviation of noise\n        :param obs_noise_ne: Standard deviation of noise for non-emitting states (is set to obs_noise if not given)\n        :param max_dist_init: Maximum distance from start location (if not given, uses max_dist)\n        :param max_dist: Maximum distance from path (this is a hard cut, min_prob_norm should be better)\n        :param min_prob_norm: Minimum normalized probability of observations (ema)\n        :param non_emitting_states: Allow non-emitting states. A non-emitting state is a state that is\n            not associated with an observation. Here we assume it can be associated with a location in between\n            two observations to allow for pruning. It is advised to set min_prob_norm and/or max_dist to avoid\n            visiting all possible nodes in the graph.\n        :param non_emitting_length_factor: Reduce the probability of a sequence of non-emitting states the longer it\n            is. This can be used to prefer shorter paths. This is separate from the transition probabilities because\n            transition probabilities are averaged for non-emitting states and thus the length is also averaged out.\n        :param max_lattice_width: Restrict the lattice (or possible candidate states per observation) to this value.\n            If there are more possible next states, the states with the best likelihood so far are selected.\n\n        :param dist_noise: Standard deviation of difference between distance between states and distance\n            between observatoins. If not given, set to obs_noise\n        :param dist_noise_ne: If not given, set to dist_noise\n        :param restrained_ne: Avoid non-emitting states if the distance between states and between\n            observations is close to each other.\n        :param avoid_goingback: If true, the probability is lowered for a transition that returns back to a\n            previous edges or returns to a position on an edge.\n\n        :param args: Arguments for BaseMatcher\n        :param kwargs: Arguments for BaseMatcher\n        \"\"\"\n\n        if not kwargs.get(\"only_edges\", True):\n            logger.warning(\"The MatcherDistance method only works on edges as states. Nodes have been disabled.\")\n        kwargs[\"only_edges\"] = True\n        if \"matching\" not in kwargs:\n            kwargs[\"matching\"] = DistanceMatching\n        super().__init__(*args, **kwargs)\n        self.use_original = kwargs.get('use_original', False)\n\n        # if not use_original, the following value for beta gives a prob of 0.5 at dist=x_half:\n        # beta = np.sqrt(np.power(x_half, 2) / (np.log(2)*2))\n        self.dist_noise = kwargs.get('dist_noise', self.obs_noise)\n        self.dist_noise_ne = kwargs.get('dist_noise_ne', self.dist_noise)\n        self.beta = 2 * self.dist_noise ** 2\n        self.beta_ne = 2 * self.dist_noise_ne ** 2\n\n        self.sigma = 2 * self.obs_noise ** 2\n        self.sigma_ne = 2 * self.obs_noise_ne ** 2\n\n        self.restrained_ne = kwargs.get('restrained_ne', True)\n        self.restrained_ne_thr = 1.25  # Threshold\n        self.exact_dt_s = True  # Newson and Krumm is 'True'\n\n        self.avoid_goingback = kwargs.get('avoid_goingback', True)\n        self.gobackonedge_factor_log = math.log(0.5)\n        self.gobacktoedge_factor_log = math.log(0.5)\n        self.first_farend_penalty = math.log(0.75)  # should be > gobacktoedge_factor_log\n\n        self.notconnectededges_factor_log = math.log(0.5)\n\n    def logprob_trans(self, prev_m, edge_m, edge_o,\n                      is_prev_ne=False, is_next_ne=False):\n        # type: (DistanceMatcher, DistanceMatching, Segment, Segment, bool, bool) -> Tuple[float, Dict[str, Any]]\n        \"\"\"Transition probability.\n\n        The probability is defined with a formula from the exponential family.\n        :math:`P(dt) = exp(-d_t^2 / (2 * dist_{noise}^2))`\n\n        with :math:`d_t = |d_s - d_o|,\n        d_s = |loc_{prev\\_state} - loc_{cur\\_state}|,\n        d_o = |loc_{prev\\_obs} - loc_{cur\\_obs}|`\n\n        This function is more tolerant for low values. The intuition is that values under a certain\n        distance should all be close to probability 1.0.\n\n        Note: We should also smooth the distance between observations to handle outliers better.\n\n        :param prev_m: Previous matching / state\n        :param edge_m: Edge between matchings / states\n        :param edge_o: Edge between observations\n        :param is_prev_ne: Is previous state non-emitting\n        :param is_next_ne: Is the next state non-emitting\n        :param dist_o: First output of distance_progress\n        :param dist_m: Second output of distance_progress\n        :return:\n        \"\"\"\n        d_z = self.map.distance(prev_m.edge_o.pi, edge_o.pi)\n        is_same_edge = False\n        if (prev_m.edge_m.l1 == edge_m.l1 and prev_m.edge_m.l2 == edge_m.l2) or \\\n                (prev_m.edge_m.l1 == edge_m.l2 and prev_m.edge_m.l2 == edge_m.l1):\n            is_same_edge = True\n        if ((not self.exact_dt_s) or\n                is_same_edge or  # On same edge\n                prev_m.edge_m.l2 != edge_m.l1):  # Edges are not connected\n            d_x = self.map.distance(prev_m.edge_m.pi, edge_m.pi)\n        else:\n            # Take into account the curvature\n            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)\n\n        if is_next_ne:\n            # For non-emitting states, the distances are added\n            # Otherwise it can map to a sequence of short segments and stay at the same\n            # observation because the difference is then always small.\n            d_z += prev_m.d_o\n            d_x += prev_m.d_s\n\n        d_t = abs(d_z - d_x)\n        # p_dt = 1 / beta * math.exp(-d_t / beta)\n        if is_prev_ne or is_next_ne:\n            beta = self.beta_ne\n        else:\n            beta = self.beta\n        logprob = -d_t ** 2 / beta\n\n        # Penalties\n        if prev_m.edge_m.label == edge_m.label:\n            # Staying in same state\n            if self.avoid_goingback and edge_m.key == prev_m.edge_m.key and edge_m.ti < prev_m.edge_m.ti:\n                # Going back on edge (direction is from p1 to p2 of the segment)\n                logprob += self.gobackonedge_factor_log  # Prefer not going back\n        elif (prev_m.edge_m.l1, prev_m.edge_m.l2) == (edge_m.l2, edge_m.l1):\n            if self.avoid_goingback:\n                logprob += self.gobackonedge_factor_log\n        else:\n            # Moving states\n            if prev_m.edge_m.l2 != edge_m.l1:\n                # We are moving between states that represent edges that are not connected through a node\n                logprob += self.notconnectededges_factor_log\n            elif self.avoid_goingback:\n                # Goin back on state\n                going_back = False\n                for m in prev_m.prev:\n                    if edge_m.label == m.edge_m.label:\n                        going_back = True\n                        break\n                if going_back:\n                    logprob += self.gobacktoedge_factor_log  # prefer not going back\n\n        props = {\n            'd_o': d_z,\n            'd_s': d_x,\n            'lpt': logprob\n        }\n        return logprob, props\n\n    def logprob_obs(self, dist, prev_m=None, new_edge_m=None, new_edge_o=None, is_ne=False):\n        # type: (DistanceMatcher, float, DistanceMatching, Segment, Segment, bool) -> Tuple[float, Dict[str, Any]]\n        \"\"\"Emission probability for emitting states.\n\n        Exponential family:\n        :math:`P(dt) = exp(-d_o^2 / (2 * obs_{noise}^2))`\n\n        with :math:`d_o = |loc_{state} - loc_{obs}|`\n\n        \"\"\"\n        if is_ne:\n            sigma = self.sigma_ne\n        else:\n            sigma = self.sigma\n        result = -dist ** 2 / sigma\n        props = {\n            'lpe': result\n        }\n        return result, props\n\n    def _skip_ne_states(self, next_ne_m):\n        # type: (DistanceMatcher, DistanceMatching) -> bool\n        # Skip searching for non-emitting states when the distances between nodes\n        # on the map are similar to the distances between the observation\n        if not self.restrained_ne:\n            return False\n        if next_ne_m.d_s > 0:\n            factor = (next_ne_m.d_o + next_ne_m.dist_obs) / next_ne_m.d_s\n        else:\n            factor = 0\n        if factor < self.restrained_ne_thr:\n            logger.debug(f\"Skip non-emitting states to {next_ne_m.label}: {factor} < {self.restrained_ne_thr} \"\n                         \"(observations close enough to each other)\")\n            return True\n        return False\n"
  },
  {
    "path": "leuvenmapmatching/matcher/newsonkrumm.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.matcher.newsonkrumm\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nMethods similar to Newson Krumm 2009 for comparison purposes.\n\n    P. Newson and J. Krumm. Hidden markov map matching through noise and sparseness.\n    In Proceedings of the 17th ACM SIGSPATIAL international conference on advances\n    in geographic information systems, pages 336–343. ACM, 2009.\n\n\n\n:author: Wannes Meert\n:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nfrom scipy.stats import norm\nimport math\nimport logging\n\nfrom .base import BaseMatching, BaseMatcher\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n\nclass NewsonKrummMatching(BaseMatching):\n    __slots__ = ['d_s', 'd_o', 'lpe', 'lpt']  # Additional fields\n\n    def __init__(self, *args, d_s=1.0, d_o=1.0, lpe=0.0, lpt=0.0, **kwargs):\n        \"\"\"\n\n        :param args: Arguments for BaseMatching\n        :param d_s: Distance between two (interpolated) states\n        :param d_o: Distance between two (interpolated) observations\n        :param lpe: Log probability of emission\n        :param lpt: Log probablity of transition\n        :param kwargs: Arguments for BaseMatching\n        \"\"\"\n        super().__init__(*args, **kwargs)\n        self.d_o: float = d_o\n        self.d_s: float = d_s\n        self.lpe: float = lpe\n        self.lpt: float = lpt\n\n    def _update_inner(self, m_other):\n        # type: (NewsonKrummMatching, NewsonKrummMatching) -> None\n        super()._update_inner(m_other)\n        self.d_s = m_other.d_s\n        self.d_o = m_other.d_o\n        self.lpe = m_other.lpe\n        self.lpt = m_other.lpt\n\n    @staticmethod\n    def repr_header(label_width=None, stop=\"\"):\n        res = BaseMatching.repr_header(label_width)\n        res += f\" {'dt(o)':<6} | {'dt(s)':<6} |\"\n        if logger.isEnabledFor(logging.DEBUG):\n            res += f\" {'lg(Pr-t)':<9} | {'lg(Pr-e)':<9} |\"\n        return res\n\n    def __str__(self, label_width=None):\n        res = super().__str__(label_width)\n        res += f\" {self.d_o:>6.2f} | {self.d_s:>6.2f} |\"\n        if logger.isEnabledFor(logging.DEBUG):\n            res += f\" {self.lpt:>9.2f} | {self.lpe:>9.2f} |\"\n        return res\n\n\nclass NewsonKrummMatcher(BaseMatcher):\n    \"\"\"\n    Take distance between observations vs states into account. Based on the\n    method presented in:\n\n        P. Newson and J. Krumm. Hidden markov map matching through noise and sparseness.\n        In Proceedings of the 17th ACM SIGSPATIAL international conference on advances\n        in geographic information systems, pages 336–343. ACM, 2009.\n\n    Two important differences:\n\n    * Newson and Krumm use shortest path to handle situations where the distances between\n      observations are larger than distances between nodes in the graph. The LeuvenMapMatching\n      toolbox uses non-emitting states to handle this. We thus do not implement the shortest\n      path algorithm in this class.\n    * Transition and emission probability are transformed from densities to probababilities by\n      taking the 1 - CDF instead of the PDF.\n\n\n    Newson and Krumm defaults:\n\n    - max_dist = 200 m\n    - obs_noise = 4.07 m\n    - beta = 1/6\n    - only_edges = True\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"\n\n        :param beta: Default is 1/6\n        :param beta_ne: Default is beta\n        :param args: Arguments for BaseMatcher\n        :param kwargs: Arguments for BaseMatcher\n        \"\"\"\n\n        if not kwargs.get(\"only_edges\", True):\n            logger.warning(\"The MatcherDistance method only works on edges as states. Nodes have been disabled.\")\n        kwargs[\"only_edges\"] = True\n        if \"matching\" not in kwargs:\n            kwargs[\"matching\"] = NewsonKrummMatching\n        super().__init__(*args, **kwargs)\n\n        # if not use_original, the following value for beta gives a prob of 0.5 at dist=x_half:\n        # beta = np.sqrt(np.power(x_half, 2) / (np.log(2)*2))\n        self.beta = kwargs.get('beta', 1/6)\n        self.beta_ne = kwargs.get('beta_ne', self.beta)\n\n        self.obs_noise_dist = norm(scale=self.obs_noise)\n        self.obs_noise_dist_ne = norm(scale=self.obs_noise_ne)\n        self.ne_thr = 1.25\n\n    def logprob_trans(self, prev_m: NewsonKrummMatching, edge_m, edge_o,\n                      is_prev_ne=False, is_next_ne=False):\n        \"\"\"Transition probability.\n\n        Main difference with Newson and Krumm: we know all points are connected thus do not compute the\n        shortest path but the distance between two points.\n\n        Original PDF:\n        p(dt) = 1 / beta * e^(-dt / beta)\n        with beta = 1/6\n\n        Transformed to probability:\n        P(dt) = p(d > dt) = e^(-dt / beta)\n\n        :param prev_m:\n        :param edge_m:\n        :param edge_o:\n        :param is_prev_ne:\n        :param is_next_ne:\n        :return:\n        \"\"\"\n        d_z = self.map.distance(prev_m.edge_o.pi, edge_o.pi)\n        if prev_m.edge_m.label == edge_m.label:\n            d_x = self.map.distance(prev_m.edge_m.pi, edge_m.pi)\n        else:\n            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)\n        d_t = abs(d_z - d_x)\n        # p_dt = 1 / beta * math.exp(-d_t / beta)\n        if is_prev_ne or is_next_ne:\n            beta = self.beta_ne\n        else:\n            beta = self.beta\n        # icp_dt = math.exp(-d_t / beta)\n        # try:\n        #     licp_dt = math.log(icp_dt)\n        # except ValueError:\n        #     licp_dt = float('-inf')\n        licp_dt = -d_t / beta\n        props = {\n            'd_o': d_z,\n            'd_s': d_x,\n            'lpt': licp_dt\n        }\n        return licp_dt, props\n\n    def logprob_obs(self, dist, prev_m, new_edge_m, new_edge_o, is_ne=False):\n        \"\"\"Emission probability for emitting states.\n\n        Original pdf:\n        p(d) = N(0, sigma)\n        with sigma = 4.07m\n\n        Transformed to probability:\n        P(d) = 2 * (1 - p(d > D)) = 2 * (1 - cdf)\n\n        \"\"\"\n        if is_ne:\n            result = 2 * (1 - self.obs_noise_dist_ne.cdf(dist))\n        else:\n            result = 2 * (1 - self.obs_noise_dist.cdf(dist))\n        try:\n            result = math.log(result)\n        except ValueError:\n            result = -float(\"inf\")\n        props = {\n            'lpe': result\n        }\n        return result, props\n"
  },
  {
    "path": "leuvenmapmatching/matcher/simple.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.matcher.simple\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport math\nfrom scipy.stats import halfnorm, norm\n\nfrom .base import BaseMatcher, BaseMatching\nfrom ..util.segment import Segment\n\n\nclass SimpleMatching(BaseMatching):\n    pass\n\n\nclass SimpleMatcher(BaseMatcher):\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"A simple matcher that prefers paths where each matched location is as close as possible to the\n        observed position.\n\n        :param avoid_goingback: Change the transition probability to be lower for the direction the path is coming\n            from.\n        :param kwargs: Arguments passed to :class:`BaseMatcher`.\n        \"\"\"\n        if \"matching\" not in kwargs:\n            kwargs['matching'] = SimpleMatching\n        super().__init__(*args, **kwargs)\n\n        self.obs_noise_dist = halfnorm(scale=self.obs_noise)\n        self.obs_noise_dist_ne = halfnorm(scale=self.obs_noise_ne)\n        # normalize to max 1 to simulate a prob instead of density\n        self.obs_noise_logint = math.log(self.obs_noise * math.sqrt(2 * math.pi) / 2)\n        self.obs_noise_logint_ne = math.log(self.obs_noise_ne * math.sqrt(2 * math.pi) / 2)\n\n        # Transition probability is divided (in logprob_trans) by this factor if we move back on the\n        # current edge.\n        self.avoid_goingback = kwargs.get('avoid_goingback', True)\n        self.gobackonedge_factor_log = math.log(0.99)\n        # Transition probability is divided (in logprob_trans) by this factor if the next state is\n        # also the previous state, thus if we go back\n        self.gobacktoedge_factor_log = math.log(0.5)\n        # Transition probability is divided (in logprob_trans) by this factor if a transition is made\n        # This is to try to stay on the same node unless there is a good reason\n        self.transition_factor = math.log(0.9)\n\n    def logprob_trans(self, prev_m: BaseMatching, edge_m: Segment, edge_o: Segment,\n                      is_prev_ne=False, is_next_ne=False):\n        \"\"\"Transition probability.\n\n        Note: In contrast with a regular HMM, this is not a probability density function, it needs\n              to be a proper probability (thus values between 0.0 and 1.0).\n        \"\"\"\n        logprob = 0\n        if prev_m.edge_m.label == edge_m.label:\n            # Staying in same state\n            if self.avoid_goingback and edge_m.key == prev_m.edge_m.key and edge_m.ti < prev_m.edge_m.ti:\n                # Going back on edge\n                logprob += self.gobackonedge_factor_log  # prefer not going back\n        else:\n            # Moving states\n            logprob += self.transition_factor\n            if self.avoid_goingback:\n                # Goin back on state\n                going_back = False\n                for m in prev_m.prev:\n                    if edge_m.label == m.edge_m.label:\n                        going_back = True\n                        break\n                if going_back:\n                    logprob += self.gobacktoedge_factor_log  # prefer not going back\n        return logprob, {}  # All probabilities are 1 (thus technically not a distribution)\n\n    def logprob_obs(self, dist, prev_m, new_edge_m, new_edge_o, is_ne=False):\n        \"\"\"Emission probability.\n\n        Note: In contrast with a regular HMM, this is not a probability density function, it needs\n              to be a proper probability (thus values between 0.0 and 1.0).\n        \"\"\"\n        if is_ne:\n            result = self.obs_noise_dist_ne.logpdf(dist) + self.obs_noise_logint_ne\n        else:\n            result = self.obs_noise_dist.logpdf(dist) + self.obs_noise_logint\n        # print(\"logprob_obs: {} -> {:.5f} = {:.5f}\".format(dist, result, math.exp(result)))\n        return result, {}\n"
  },
  {
    "path": "leuvenmapmatching/util/__init__.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util\n~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\n\n# No automatic loading to avoid dependency on packages such as nvector and gpxpy if not used.\n\n\ndef approx_equal(a, b, rtol=0.0, atol=1e-08):\n    return abs(a - b) <= (atol + rtol * abs(b))\n\n\ndef approx_leq(a, b, rtol=0.0, atol=1e-08):\n    return (a - b) <= (atol + rtol * abs(b))\n"
  },
  {
    "path": "leuvenmapmatching/util/debug.py",
    "content": "import logging\n\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n\ndef printd(*args, **kwargs):\n    \"\"\"Print to debug output.\"\"\"\n    logger.debug(*args, **kwargs)\n"
  },
  {
    "path": "leuvenmapmatching/util/dist_euclidean.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.dist_euclidean\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport logging\nimport math\n\nimport numpy as np\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n\ndef distance(p1, p2):\n    result = math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)\n    # print(\"distance({}, {}) -> {}\".format(p1, p2, result))\n    return result\n\n\ndef distance_point_to_segment(p, s1, s2, delta=0.0):\n    p_int, ti = project(s1, s2, p, delta=delta)\n    return distance(p_int, p), p_int, ti\n    # l1a = np.array(s1)\n    # l2a = np.array(s2)\n    # pa = np.array(p)\n    # return np.linalg.norm(np.cross(l2a - l1a, l1a - pa)) / np.linalg.norm(l2a - l1a)\n\n\ndef distance_segment_to_segment(f1, f2, t1, t2):\n    \"\"\"Distance between segments..\n\n    :param f1: From\n    :param f2:\n    :param t1: To\n    :param t2:\n    :return: (distance, proj on f, proj on t, rel pos on f, rel pos on t)\n    \"\"\"\n    x1, y1 = f1\n    x2, y2 = f2\n    x3, y3 = t1\n    x4, y4 = t2\n    n = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1))\n    if np.allclose([n], [0], rtol=0):\n        # parallel\n        is_parallel = True\n        n = 0.0001  # TODO: simulates a point far away\n    else:\n        is_parallel = False\n    u_f = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / n\n    u_t = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / n\n    xi = x1 + u_f * (x2 - x1)\n    yi = y1 + u_f * (y2 - y1)\n    changed_f = False\n    changed_t = False\n    if u_t > 1:\n        u_t = 1\n        changed_t = True\n    elif u_t < 0:\n        u_t = 0\n        changed_t = True\n    if u_f > 1:\n        u_f = 1\n        changed_f = True\n    elif u_f < 0:\n        u_f = 0\n        changed_f = True\n    if not changed_t and not changed_f:\n        return 0, (xi, yi), (xi, yi), u_f, u_t\n    xf = x1 + u_f * (x2 - x1)\n    yf = y1 + u_f * (y2 - y1)\n    xt = x3 + u_t * (x4 - x3)\n    yt = y3 + u_t * (y4 - y3)\n    if changed_t and changed_f:\n        # Compare furthest point from intersection with segment\n        df = (xf - xi) ** 2 + (yf - yi) ** 2\n        dt = (xt - xi) ** 2 + (yt - yi) ** 2\n        if df > dt:\n            changed_t = False\n        else:\n            changed_f = False\n    if changed_t:\n        pt = (xt, yt)\n        pf, u_f = project(f1, f2, pt)\n    elif changed_f:\n        pf = (xf, yf)\n        pt, u_t = project(t1, t2, pf)\n    else:\n        raise Exception(f\"Should not happen\")\n    d = distance(pf, pt)\n    return d, pf, pt, u_f, u_t\n\n\ndef project(s1, s2, p, delta=0.0):\n    \"\"\"\n\n    :param s1: Segment start\n    :param s2: Segment end\n    :param p: Point\n    :param delta: Keep delta fraction away from ends\n    :return: Point of projection, Relative position on segment\n    \"\"\"\n    if np.isclose(s1[0], s2[0], rtol=0) and np.isclose(s1[1], s2[1], rtol=0):\n        return s1, 0.0\n\n    l2 = (s1[0]-s2[0])**2 + (s1[1]-s2[1])**2\n    t = max(delta, min(1-delta, ((p[0]-s1[0])*(s2[0]-s1[0]) + (p[1]-s1[1])*(s2[1]-s1[1])) / l2))\n    return (s1[0] + t * (s2[0]-s1[0]), s1[1] + t * (s2[1]-s1[1])), t\n\n\ndef interpolate_path(path, dd):\n    \"\"\"\n    TODO: interplate time as third term\n    :param path: (y, x)\n    :param dd: Distance difference (meter)\n    :return:\n    \"\"\"\n    path_new = [path[0]]\n    for p1, p2 in zip(path, path[1:]):\n        dist = distance(p1, p2)\n        if dist > dd:\n            dt = int(math.ceil(dist / dd))\n            dx = (p2[0] - p1[0]) / dt\n            dy = (p2[1] - p1[1]) / dt\n            px, py = p1[0], p1[1]\n            for _ in range(dt):\n                px += dx\n                py += dy\n                path_new.append((px, py))\n        path_new.append(p2)\n    return path_new\n\n\ndef box_around_point(p, dist):\n    lat, lon = p\n    lat_t, lon_r = lat + dist, lon + dist\n    lat_b, lon_l = lat - dist, lon - dist\n    return lat_b, lon_l, lat_t, lon_r\n\n\ndef lines_parallel(la, lb, lc, ld, d=None):\n    x1 = la[0] - lb[0]\n    y1 = la[1] - lb[1]\n    if x1 == 0:\n        if y1 == 0:\n            return False\n        s1 = 0\n    else:\n        s1 = math.atan(abs(y1 / x1))\n    x2 = lc[0] - ld[0]\n    y2 = lc[1] - ld[1]\n    if x2 == 0:\n        s2 = 0\n        if y2 == 0:\n            return False\n    else:\n        s2 = math.atan(abs(y2 / x2))\n    thr = math.pi / 180\n    if abs(s1 - s2) > thr:\n        return False\n    if d is not None:\n        dist, _, _, _, _ = distance_segment_to_segment(la, lb, lc, ld)\n        if dist > d:\n            return False\n    return True\n\n"
  },
  {
    "path": "leuvenmapmatching/util/dist_latlon.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.dist_latlon\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBased on:\nhttps://www.movable-type.co.uk/scripts/latlong.html\nhttps://www.movable-type.co.uk/scripts/latlong-vectors.html\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport logging\nimport math\nfrom math import radians, cos, sin, asin, acos, sqrt, atan2, fabs, degrees, ceil, copysign\n\nfrom . import dist_euclidean as diste\n\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\nearth_radius = 6371000\n\n\ndef distance(p1, p2):\n    \"\"\"Distance between two points.\n\n    :param p1: (Lat, Lon)\n    :param p2: (Lat, Lon)\n    :return: Distance in meters\n    \"\"\"\n    lat1, lon1 = p1[0], p1[1]\n    lat2, lon2 = p2[0], p2[1]\n    lat1, lon1 = radians(lat1), radians(lon1)\n    lat2, lon2 = radians(lat2), radians(lon2)\n    dist = distance_haversine_radians(lat1, lon1, lat2, lon2)\n    return dist\n\n\ndef distance_point_to_segment(p, s1, s2, delta=0.0, constrain=True):\n    \"\"\"Distance between point and segment.\n\n    Cross-track distance.\n\n    https://www.movable-type.co.uk/scripts/latlong.html#cross-track\n\n    :param s1: Segment start point\n    :param s2: Segment end point\n    :param p: Point to measure distance from path to\n    :param delta: Stay away from the endpoints with this factor\n    :return: (Distance in meters, projected location on segment, relative location on segment)\n    \"\"\"\n    lat1, lon1 = s1  # Start point\n    lat2, lon2 = s2  # End point\n    lat3, lon3 = p[0], p[1]\n    lat1, lon1 = radians(lat1), radians(lon1)\n    lat2, lon2 = radians(lat2), radians(lon2)\n    lat3, lon3 = radians(lat3), radians(lon3)\n\n    dist_hs = distance_haversine_radians(lat1, lon1, lat2, lon2)\n    if dist_hs == 0:\n        dist_ct, pi, ti = distance(p, s1), s1, 0\n        return dist_ct, pi, ti\n\n    d13 = distance_haversine_radians(lat1, lon1, lat3, lon3)\n    delta13 = d13 / earth_radius\n    b13 = bearing_radians(lat1, lon1, lat3, lon3)\n    b12 = bearing_radians(lat1, lon1, lat2, lon2)\n\n    dxt = asin(sin(delta13) * sin(b13 - b12))\n    # b13d12 = (b13 - b12) % (2 * math.pi)\n    # if b13d12 > math.pi:\n    #     b13d12 = 2 * math.pi - b13d12\n    dist_ct = fabs(dxt) * earth_radius\n    # Correct to negative value if point is before segment\n    # sgn = -1 if b13d12 > (math.pi / 2) else 1\n    sgn = copysign(1, cos(b12 - b13))\n    dat = sgn * acos(cos(delta13) / abs(cos(dxt))) * earth_radius\n    ti = dat / dist_hs\n\n    if not constrain:\n        lati, loni = destination_radians(lat1, lon1, b12, dat)\n    elif ti > 1.0:\n        ti = 1.0\n        lati, loni = lat2, lon2\n        dist_ct = distance_haversine_radians(lat3, lon3, lati, loni)\n    elif ti < 0.0:\n        ti = 0.0\n        lati, loni = lat1, lon1\n        dist_ct = distance_haversine_radians(lat3, lon3, lati, loni)\n    else:\n        lati, loni = destination_radians(lat1, lon1, b12, dat)\n    pi = (degrees(lati), degrees(loni))\n\n    return dist_ct, pi, ti\n\n\ndef distance_segment_to_segment(f1, f2, t1, t2):\n    \"\"\"Distance between segments. If no intersection within range, simplified to distance from f2 to [t1,t2].\n\n    :param f1: From\n    :param f2:\n    :param t1: To\n    :param t2:\n    :return: (distance, proj on f, proj on t, rel pos on t)\n    \"\"\"\n    # Translate lat-lon to x-y and apply the Euclidean function\n    latf1, lonf1 = f1\n    latf1, lonf1 = radians(latf1), radians(lonf1)\n    f1 = 0, 0  # Origin\n\n    latf2, lonf2 = f2\n    latf2, lonf2 = radians(latf2), radians(lonf2)\n    df1f2 = distance_haversine_radians(latf1, lonf1, latf2, lonf2)\n    bf1f2 = bearing_radians(latf1, lonf1, latf2, lonf2)\n    # print(f\"bf1f2 = {bf1f2} = {degrees(bf1f2)} degrees\")\n    f2 = (df1f2 * cos(bf1f2),  df1f2 * sin(bf1f2))\n\n    latt1, lont1 = t1[0], t1[1]\n    latt1, lont1 = radians(latt1), radians(lont1)\n    df1t1 = distance_haversine_radians(latf1, lonf1, latt1, lont1)\n    bf1t1 = bearing_radians(latf1, lonf1, latt1, lont1)\n    # print(f\"bf1t1 = {bf1t1} = {degrees(bf1t1)} degrees\")\n    t1 = (df1t1 * cos(bf1t1), df1t1 * sin(bf1t1))\n\n    latt2, lont2 = t2[0], t2[1]\n    latt2, lont2 = radians(latt2), radians(lont2)\n    dt1t2 = distance_haversine_radians(latt1, lont1, latt2, lont2)\n    # print(f\"dt1t2 = {dt1t2}\")\n    bt1t2 = bearing_radians(latt1, lont1, latt2, lont2)\n    # print(f\"bt1t2 = {bt1t2} = {degrees(bt1t2)} degrees\")\n    t2 = (t1[0] + dt1t2 * cos(bt1t2), t1[1] + dt1t2 * sin(bt1t2))\n\n    d, pf, pt, u_f, u_t = diste.distance_segment_to_segment(f1, f2, t1, t2)\n    pf = destination_radians(latf1, lonf1, bf1f2, u_f * df1f2)\n    pf = (degrees(pf[0]), degrees(pf[1]))\n    pt = destination_radians(latt1, lont1, bt1t2, u_t * dt1t2)\n    pt = (degrees(pt[0]), degrees(pt[1]))\n\n    return d, pf, pt, u_f, u_t\n\n\ndef project(s1, s2, p, delta=0.0):\n    _, pi, ti = distance_point_to_segment(p, s1, s2, delta)\n    return pi, ti\n\n\ndef box_around_point(p, dist):\n    lat, lon = p\n    latr, lonr = radians(lat), radians(lon)\n    # diag_dist = sqrt(2 * dist ** 2)\n    diag_dist = dist\n    lat_t, lon_r = destination_radians(latr, lonr, radians(45), diag_dist)\n    lat_b, lon_l = destination_radians(latr, lonr, radians(225), diag_dist)\n    lat_t, lon_r = degrees(lat_t), degrees(lon_r)\n    lat_b, lon_l = degrees(lat_b), degrees(lon_l)\n    return lat_b, lon_l, lat_t, lon_r\n\n\ndef interpolate_path(path, dd):\n    \"\"\"\n    :param path: (lat, lon)\n    :param dd: Distance difference (meter)\n    :return:\n    \"\"\"\n    path_new = [path[0]]\n    for p1, p2 in zip(path, path[1:]):\n        lat1, lon1 = p1[0], p1[1]\n        lat2, lon2 = p2[0], p2[1]\n        lat1, lon1 = radians(lat1), radians(lon1)\n        lat2, lon2 = radians(lat2), radians(lon2)\n        dist = distance_haversine_radians(lat1, lon1, lat2, lon2)\n        if dist > dd:\n            dt = int(ceil(dist / dd))\n            distd = dist/dt\n            disti = 0\n            brng = bearing_radians(lat1, lon1, lat2, lon2)\n            for _ in range(dt):\n                disti += distd\n                lati, loni = destination_radians(lat1, lon1, brng, disti)\n                path_new.append((degrees(lati), degrees(loni)))\n        path_new.append(p2)\n    return path_new\n\n\ndef bearing_radians(lat1, lon1, lat2, lon2):\n    \"\"\"Initial bearing\"\"\"\n    dlon = lon2 - lon1\n    y = sin(dlon) * cos(lat2)\n    x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlon)\n    return atan2(y, x)\n\n\ndef distance_haversine_radians(lat1, lon1, lat2, lon2, radius=earth_radius):\n    # type: (float, float, float, float, float) -> float\n    lat = lat2 - lat1\n    lon = lon2 - lon1\n    a = sin(lat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(lon / 2) ** 2\n    # dist = 2 * radius * asin(sqrt(a))\n    dist = 2 * radius * atan2(sqrt(a), sqrt(1 - a))\n    return dist\n\n\ndef destination_radians(lat1, lon1, bearing, dist):\n    d = dist / earth_radius\n    lat2 = asin(sin(lat1) * cos(d) + cos(lat1) * sin(d) * cos(bearing))\n    lon2 = lon1 + atan2(sin(bearing) * sin(d) * cos(lat1), cos(d) - sin(lat1) * sin(lat2))\n    return lat2, lon2\n\n\ndef lines_parallel(f1, f2, t1, t2, d=None):\n    latf1, lonf1 = f1\n    latf1, lonf1 = radians(latf1), radians(lonf1)\n    f1 = 0, 0  # Origin\n\n    latf2, lonf2 = f2\n    latf2, lonf2 = radians(latf2), radians(lonf2)\n    df1f2 = distance_haversine_radians(latf1, lonf1, latf2, lonf2)\n    bf1f2 = bearing_radians(latf1, lonf1, latf2, lonf2)\n    # print(f\"bf1f2 = {bf1f2} = {degrees(bf1f2)} degrees\")\n    f2 = (df1f2 * cos(bf1f2), df1f2 * sin(bf1f2))\n\n    latt1, lont1 = t1\n    latt1, lont1 = radians(latt1), radians(lont1)\n    df1t1 = distance_haversine_radians(latf1, lonf1, latt1, lont1)\n    bf1t1 = bearing_radians(latf1, lonf1, latt1, lont1)\n    # print(f\"bf1t1 = {bf1t1} = {degrees(bf1t1)} degrees\")\n    t1 = (df1t1 * cos(bf1t1), df1t1 * sin(bf1t1))\n\n    latt2, lont2 = t2\n    latt2, lont2 = radians(latt2), radians(lont2)\n    dt1t2 = distance_haversine_radians(latt1, lont1, latt2, lont2)\n    # print(f\"dt1t2 = {dt1t2}\")\n    bt1t2 = bearing_radians(latt1, lont1, latt2, lont2)\n    # print(f\"bt1t2 = {bt1t2} = {degrees(bt1t2)} degrees\")\n    t2 = (t1[0] + dt1t2 * cos(bt1t2), t1[1] + dt1t2 * sin(bt1t2))\n\n    return diste.lines_parallel(f1, f2, t1, t2, d=d)\n"
  },
  {
    "path": "leuvenmapmatching/util/dist_latlon_nvector.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.dist_latlon_nvector\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport logging\nimport math\n\nimport numpy as np\nfrom nvector._core import unit, n_E2lat_lon, great_circle_normal\nimport nvector as nv\n\n\nframe = nv.FrameE(a=6371e3, f=0)\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n\ndef distance(p1, p2):\n    \"\"\"\n\n    :param p1:\n    :param p2:\n    :return: Distance in meters\n    \"\"\"\n    p1 = frame.GeoPoint(p1[0], p1[1], degrees=True)\n    p2 = frame.GeoPoint(p2[0], p2[1], degrees=True)\n    d, _, _ = p1.distance_and_azimuth(p2)\n    # print(\"distance_latlon({}, {}) -> {}\".format(p1, p2, d))\n    return d\n\n\ndef distance_gp(p1, p2):\n    d, _, _ = p1.distance_and_azimuth(p2)\n    return d\n\n\ndef distance_point_to_segment(p, s1, s2, delta=0.0):\n    \"\"\"\n    TODO: A point exactly on the line gives an error.\n\n    :param s1: Segment start point\n    :param s2: Segment end point\n    :param p: Point to measure distance from path to\n    :param delta: Stay away from the endpoints with this factor\n    :return: (Distance in meters, projected location on segmegnt)\n    \"\"\"\n    # TODO: Initialize all points as GeoPoint when loading data\n    s1 = frame.GeoPoint(s1[0], s1[1], degrees=True)\n    s2 = frame.GeoPoint(s2[0], s2[1], degrees=True)\n    p = frame.GeoPoint(p[0], p[1], degrees=True)\n    p_int, ti = _project_nvector(s1, s2, p)\n    d, _, _ = p.distance_and_azimuth(p_int)\n    return d, (p_int.latitude_deg[0], p_int.longitude_deg[0]), ti\n\n\ndef distance_segment_to_segment(f1, f2, t1, t2):\n    \"\"\"Distance between segments. If no intersection within range, simplified to distance from f2 to [t1,t2].\n\n    :param f1: From\n    :param f2:\n    :param t1: To\n    :param t2:\n    :return: (distance, proj on f, proj on t, rel pos on t)\n    \"\"\"\n    # TODO: Should be improved\n    f1_gp = frame.GeoPoint(f1[0], f1[1], degrees=True)\n    f2_gp = frame.GeoPoint(f2[0], f2[1], degrees=True)\n    path_f = nv.GeoPath(f1_gp, f2_gp)\n    t1_gp = frame.GeoPoint(t1[0], t1[1], degrees=True)\n    t2_gp = frame.GeoPoint(t2[0], t2[1], degrees=True)\n    path_t = nv.GeoPath(t1_gp, t2_gp)\n    p_int = path_f.intersect(path_t)\n    p_int_gp = p_int.to_geo_point()\n    if path_f.on_path(p_int)[0] and path_t.on_path(p_int)[0]:\n        # Intersection point is on segments, between both begins and ends\n        loc = (p_int_gp.latitude_deg[0], p_int_gp.longitude_deg[0])\n        u_f = distance_gp(f1_gp, p_int_gp) / distance_gp(f1_gp, f2_gp)\n        u_t = distance_gp(t1_gp, p_int_gp) / distance_gp(t1_gp, t2_gp)\n        return 0, loc, loc, u_f, u_t\n    # No intersection, use last point of map segment (the assumption is the observations are far apart)\n    # TODO: decide which point to use (see distance_segment_to_segment)\n    p_int, u_t = _project_nvector(t1_gp, t2_gp, f2_gp)\n    u_f = 1\n    d, _, _ = f2_gp.distance_and_azimuth(p_int)\n    return d, (f1, f2), (p_int_gp.latitude_deg[0], p_int_gp.longitude_deg[0]), u_f, u_t\n\n\ndef project(s1, s2, p, delta=0.0):\n    s1 = frame.GeoPoint(s1[0], s1[1], degrees=True)\n    s2 = frame.GeoPoint(s2[0], s2[1], degrees=True)\n    p = frame.GeoPoint(p[0], p[1], degrees=True)\n    p_int, ti = _project_nvector(s1, s2, p, delta=delta)\n    return (p_int.latitude_deg[0], p_int.longitude_deg[0]), ti\n\n\ndef _project_nvector(s1, s2, p, delta=0.0):\n    path = nv.GeoPath(s1, s2)\n    p_intr = _cross_track_point(path, p)\n    pin = p_intr.to_nvector().normal\n    s1n = s1.to_nvector().normal\n    s2n = s2.to_nvector().normal\n    ti = np.linalg.norm(pin - s1n) / np.linalg.norm(s2n - s1n)\n    ti = max(delta, min(1 - delta, ti))\n    return path.interpolate(ti).to_geo_point(), ti\n\n\ndef _cross_track_point(path, point):\n    \"\"\"Extend nvector package to find the projection point.\n\n    The projection point is the closest point on path to the given point.\n    Based on the nvector.cross_track_distance function.\n    http://www.navlab.net/nvector/\n\n    :param path: GeoPath\n    :param point: GeoPoint\n    \"\"\"\n    c_E = great_circle_normal(*path.nvector_normals())\n    n_EB_E = point.to_nvector().normal  # type: np.array\n    c_EP_E = np.cross(c_E, n_EB_E, axis=0)\n\n    # Find intersection point C that is closest to point B\n    frame = path.positionA.frame\n    n_EA1_E = path.positionA.to_nvector().normal  # should also be ok to use  n_EB_C\n    n_EC_E_tmp = unit(np.cross(c_E, c_EP_E, axis=0), norm_zero_vector=np.nan)\n    n_EC_E = np.sign(np.dot(n_EC_E_tmp.T, n_EA1_E)) * n_EC_E_tmp\n    if np.any(np.isnan(n_EC_E)):\n        raise Exception('Paths are Equal. Intersection point undefined. NaN returned.')\n    lat_C, long_C = n_E2lat_lon(n_EC_E, frame.R_Ee)\n    return nv.GeoPoint(lat_C, long_C, frame=frame)\n\n\ndef interpolate_path(path, dd):\n    \"\"\"\n    TODO: interplate time as third term\n    :param path: (lat, lon)\n    :param dd: Distance difference (meter)\n    :return:\n    \"\"\"\n    path_new = [path[0]]\n    for p1, p2 in zip(path, path[1:]):\n        dist = distance(p1, p2)\n        if dist > dd:\n            s1 = frame.GeoPoint(p1[0], p1[1], degrees=True)\n            s2 = frame.GeoPoint(p2[0], p2[1], degrees=True)\n            segment = nv.GeoPath(s1, s2)\n            dt = int(math.floor(dist / dd))\n            for dti in range(1, dt):\n                p_new = segment.interpolate(dti/dt).to_geo_point()\n                path_new.append((p_new.latitude_deg[0], p_new.longitude_deg[0]))\n        path_new.append(p2)\n    return path_new\n"
  },
  {
    "path": "leuvenmapmatching/util/evaluation.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.evaluation\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nMethods to help set up and evaluate experiments.\n\n:author: Wannes Meert\n:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport logging\nfrom dtaidistance.alignment import needleman_wunsch, best_alignment\n\nfrom . import dist_latlon\nMYPY = False\nif MYPY:\n    from ..map.base import BaseMap\n    from typing import List, Tuple, Optional, Callable\n\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n\ndef route_mismatch_factor(map_con, path_pred, path_grnd, window=None, dist_fn=None, keep_mismatches=False):\n    # type: (BaseMap, List[int], List[int], Optional[int], Optional[Callable], bool) -> Tuple[float, float, float, float, List[Tuple[int, int]], float, float]\n    \"\"\"Evaluation method from Newson and Krumm (2009).\n\n    :math:`f = \\frac{d_{-} + d_{+}}{d_0}`\n\n    With :math:`d_{-}` the length that is erroneously subtracted,\n    :math:`d_{+}` the length that is erroneously added, and :math:`d_0` the\n    distance of the correct route.\n\n    This function only supports connected states (thus not switching between states\n    that are not connected (e.g. parallel roads).\n\n    Also computes the Accuracy by Number (AN) and Accuracy by Length (AL) metrics from\n    Zheng et al. (2009).\n    \"\"\"\n    if dist_fn is None:\n        dist_fn = dist_latlon.distance\n    _, matrix = needleman_wunsch(path_pred, path_grnd, window=window)\n    print(matrix[:10, :10])\n    algn, _, _ = best_alignment(matrix)\n    print(algn[:10])\n    d_plus = 0  # length erroneously added\n    d_min = 0  # length erroneously subtracted\n    d_zero = 0  # length of correct route\n    cnt_matches = 0  # number of perfect matches\n    cnt_mismatches = 0\n    mismatches = [] if keep_mismatches else None\n\n    prev_grnd_pi = None\n    for pred_pi, grnd_pi in algn:\n        pred_p = path_pred[pred_pi]\n        grnd_p = path_grnd[grnd_pi]\n        grnd_d = map_con.path_dist(grnd_p)\n        d_zero += grnd_d\n        if pred_p == grnd_p:\n            cnt_matches += 1\n        else:\n            # print(f\"Mismatch: {pred_p} != {grnd_p}\")\n            cnt_mismatches += 1\n            pred_d = map_con.path_dist(pred_p)\n            d_plus += pred_d\n            d_min += grnd_d\n            if keep_mismatches:\n                mismatches.append((pred_p, grnd_p))\n        prev_grnd_pi = grnd_pi\n\n    factor = (d_min + d_plus) / d_zero\n    an = cnt_matches / len(path_grnd)\n    al = (d_zero - d_min) / d_zero\n    return factor, cnt_matches, cnt_mismatches, d_zero, mismatches, an, al\n"
  },
  {
    "path": "leuvenmapmatching/util/gpx.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.gpx\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nSome additional functions to interact with the gpx library.\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport logging\n\nimport gpxpy\nimport gpxpy.gpx\n\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n\ndef gpx_to_path(gpx_file):\n    gpx_fh = open(gpx_file)\n    track = None\n    try:\n        gpx = gpxpy.parse(gpx_fh)\n        if len(gpx.tracks) == 0:\n            logger.error('No tracks found in GPX file (<trk> tag missing?): {}'.format(gpx_file))\n            return None\n        logger.info(\"Read gpx file: {} points, {} tracks, {} segments\".format(\n            gpx.get_points_no(), len(gpx.tracks), len(gpx.tracks[0].segments)))\n        track = [(p.latitude, p.longitude, p.time) for p in gpx.tracks[0].segments[0].points]\n    finally:\n        gpx_fh.close()\n    return track\n\n\ndef path_to_gpx(path, filename=None):\n    gpx = gpxpy.gpx.GPX()\n\n    # Create first track in our GPX:\n    gpx_track = gpxpy.gpx.GPXTrack()\n    gpx.tracks.append(gpx_track)\n\n    # Create first segment in our GPX track:\n    gpx_segment = gpxpy.gpx.GPXTrackSegment()\n    gpx_track.segments.append(gpx_segment)\n\n    gpx_segment.points = [(gpxpy.gpx.GPXTrackPoint(lat, lon, time=time)) for (lat, lon, time) in path]\n\n    if filename:\n        with open(filename, 'w') as gpx_fh:\n            gpx_fh.write(gpx.to_xml())\n\n    return gpx\n"
  },
  {
    "path": "leuvenmapmatching/util/kalman.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.kalman\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport logging\n\nfrom pykalman import KalmanFilter\nimport numpy as np\n\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n\ndef smooth_path(path, dt=1, obs_noise=1e-4, loc_var=1e-4, vel_var=1e-6, kf=None,\n                rm_outliers=False, use_euclidean=True, n_iter=1000):\n    \"\"\"Apply Kalman filtering. Assumes data with a constant sample rate.\n\n    Inspired by https://github.com/FlorianWilhelm/gps_data_with_python\n\n    :param path:\n    :param dt: Sample interval in seconds\n    :param obs_noise: Observation noise (default=1e-4, approx 10-30m)\n    :param loc_var: estimated location variance\n    :param vel_var: estimated velocity variance\n    :param kf: Trained Kalman filter\n    :param rm_outliers: Remove outliers based on Kalman prediction\n        True or 1 will be removal, 2 will also retrain after removal\n    :param use_euclidean:\n    :param n_iter: Kalman iterations\n    :return:\n    \"\"\"\n    path = np.array(path)\n    if kf is None:\n        # state is (x, y, v_x, v_y)\n        F = np.array([[1, 0, dt, 0],\n                      [0, 1, 0,  dt],\n                      [0, 0, 1,  0],\n                      [0, 0, 0,  1]])\n\n        # observations is (x, y)\n        H = np.array([[1, 0, 0, 0],\n                      [0, 1, 0, 0]])\n\n        R = np.diag([obs_noise, obs_noise]) ** 2\n\n        initial_state_mean = np.hstack([path[0, :2], 2 * [0.]])\n        initial_state_covariance = np.diag([loc_var, loc_var, vel_var, vel_var]) ** 2\n\n        kf = KalmanFilter(transition_matrices=F,\n                          observation_matrices=H,\n                          observation_covariance=R,\n                          initial_state_mean=initial_state_mean,\n                          initial_state_covariance=initial_state_covariance,\n                          em_vars=['transition_covariance'])\n\n        if n_iter > 0:\n            logger.debug(\"Start learning\")\n            kf = kf.em(path[:, :2], n_iter=n_iter)\n\n    state_means, state_vars = kf.smooth(path[:, :2])\n\n    if use_euclidean:\n        from .dist_euclidean import distance\n        distance_f = distance\n    else:\n        from .dist_latlon import distance\n        distance_f = distance\n    if rm_outliers:\n        path_ma = np.ma.asarray(path[:, :2])\n        for idx in range(path.shape[0]):\n            d = distance_f(path[idx, :2], state_means[idx, :2])\n            if d > obs_noise * 2:\n                logger.debug(\"Rm point {}\".format(idx))\n                path_ma[idx] = np.ma.masked\n        if rm_outliers == 2:\n            logger.debug(\"Retrain\")\n            kf = kf.em(path_ma, n_iter=n_iter)\n        state_means, state_vars = kf.smooth(path_ma)\n\n    return state_means, state_vars, kf\n"
  },
  {
    "path": "leuvenmapmatching/util/openstreetmap.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.openstreetmap\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport logging\nfrom pathlib import Path\nimport requests\nimport tempfile\nimport osmread\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n\ndef locations_to_map(locations, map_con, filename=None):\n    lats, lons = zip(*locations)\n    lon_min, lon_max = min(lons), max(lons)\n    lat_min, lat_max = min(lats), max(lats)\n    bb = [lon_min, lat_min, lon_max, lat_max]\n    return bb_to_map(bb, map_con, filename)\n\n\ndef bb_to_map(bb, map_con, filename=None):\n    \"\"\"Download map from overpass-api.de.\n\n    :param bb: [lon_min, lat_min, lon_max, lat_max]\n    :param map:\n    :param filename:\n    :return:\n    \"\"\"\n    if filename:\n        xml_file = Path(filename)\n    else:\n        xml_file = Path(tempfile.gettempdir()) / \"osm.xml\"\n    if not xml_file.exists():\n        bb_str = \",\".join(str(coord) for coord in bb)\n        url = 'http://overpass-api.de/api/map?bbox='+bb_str\n        logger.debug(\"Downloading {} from {} ...\".format(xml_file, url))\n        r = requests.get(url, stream=True)\n        with xml_file.open('wb') as ofile:\n            for chunk in r.iter_content(chunk_size=1024):\n                if chunk:\n                    ofile.write(chunk)\n        logger.debug(\"... done\")\n    else:\n        logger.debug(\"Reusing existing file: {}\".format(xml_file))\n    return file_to_map(xml_file, map_con)\n\n\ndef file_to_map(filename, map_con):\n    logger.debug(\"Parse OSM file ...\")\n    for entity in osmread.parse_file(str(filename)):\n        if isinstance(entity, osmread.Way) and 'highway' in entity.tags:\n            for node_a, node_b in zip(entity.nodes, entity.nodes[1:]):\n                map_con.add_edge(node_a, node_b)\n                # Some roads are one-way. We'll add both directions.\n                map_con.add_edge(node_b, node_a)\n        if isinstance(entity, osmread.Node):\n            map_con.add_node(entity.id, (entity.lat, entity.lon))\n    logger.debug(\"... done\")\n    logger.debug(\"Purging database ...\")\n    map_con.purge()\n    logger.debug(\"... done\")\n\n\ndef download_map_xml(fn, bbox, force=False, verbose=False):\n    \"\"\"Download map from overpass-api.de based on a given bbox\n\n    :param fn: Filename where to store the map as xml\n    :param bbox: String or array with [lon_min, lat_min, lon_max, lat_max]\n    :param force: Also download if file already exists\n    :param verbose: Verbose output\n    :return:\n    \"\"\"\n    fn = Path(fn)\n    if type(bbox) is list:\n        bb_str = \",\".join(str(coord) for coord in bbox)\n    elif type(bbox) is str:\n        bb_str = bbox\n    else:\n        raise AttributeError('Unknown type for bbox: {}'.format(type(bbox)))\n    if force or not fn.exists():\n        if verbose:\n            print(\"Downloading {}\".format(fn))\n        import requests\n        url = f'http://overpass-api.de/api/map?bbox={bb_str}'\n        r = requests.get(url, stream=True)\n        with fn.open('wb') as ofile:\n            for chunk in r.iter_content(chunk_size=1024):\n                if chunk:\n                    ofile.write(chunk)\n    else:\n        if verbose:\n            print(\"File already exists\")\n\n\ndef create_map_from_xml(fn, include_footways=False, include_parking=False,\n                        use_rtree=False, index_edges=False):\n    \"\"\"Create an InMemMap from an OpenStreetMap XML file.\n\n    Used for testing routes on OpenStreetMap.\n    \"\"\"\n    from ..map.inmem import InMemMap\n    map_con = InMemMap(\"map\", use_latlon=True, use_rtree=use_rtree, index_edges=index_edges)\n    cnt = 0\n    ways_filter = ['bridleway', 'bus_guideway', 'track']\n    if not include_footways:\n        ways_filter += ['footway', 'cycleway', 'path']\n    parking_filter = ['driveway']\n    if not include_parking:\n        parking_filter += ['parking_aisle']\n    for entity in osmread.parse_file(str(fn)):\n        if isinstance(entity, osmread.Way):\n            tags = entity.tags\n            if 'highway' in tags \\\n                and not (tags['highway'] in ways_filter) \\\n                and not ('access' in tags and tags['access'] == 'private') \\\n                and not ('landuse' in tags and tags['landuse'] == 'square') \\\n                and not ('amenity' in tags and tags['amenity'] == 'parking') \\\n                and not ('service' in tags and tags['service'] in parking_filter) \\\n                and not ('area' in tags and tags['area'] == 'yes'):\n                for node_a, node_b in zip(entity.nodes, entity.nodes[1:]):\n                    map_con.add_edge(node_a, node_b)\n                    # Some roads are one-way. We'll add both directions.\n                    map_con.add_edge(node_b, node_a)\n        if isinstance(entity, osmread.Node):\n            map_con.add_node(entity.id, (entity.lat, entity.lon))\n    map_con.purge()\n    return map_con\n"
  },
  {
    "path": "leuvenmapmatching/util/projections.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.projections\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport math\nimport logging\n\nimport pyproj\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n\ndef latlon2equirectangular(lat, lon, phi_er=0, lambda_er=0):\n    \"\"\"Naive equirectangular projection.  This is the same as considering (lat,lon) == (y,x).\n    This is a lot faster but only works if you are far enough from the poles and the dateline.\n\n    :param lat:\n    :param lon:\n    :param phi_er: The standard parallels (north and south of the equator) where the scale of the projection is true\n    :param lambda_er: The central meridian of the map\n    \"\"\"\n    x = (lon - lambda_er) * math.cos(phi_er)\n    y = lat - phi_er\n    return y, x\n\n\ndef equirectangular2latlon(y, x, phi_er=0, lambda_er=0):\n    \"\"\"Naive equirectangular projection. This is the same as considering (lat,lon) == (y,x).\n    This is a lot faster but only works if you are far enough from the poles and the dateline.\n\n    :param phi_er: The standard parallels (north and south of the equator) where the scale of the projection is true\n    :param lambda_er: The central meridian of the map\n    \"\"\"\n    lon = x / math.cos(phi_er) + lambda_er\n    lat = y + phi_er\n    return lat, lon\n\n\ndef latlon2grs80(coordinates, lon_0=0.0, lat_ts=0.0, y_0=0, x_0=0.0, zone=31, **kwargs):\n    \"\"\"Given a list of (lon, lat) coordinates, create x-y coordinates in meter.\n\n    :param coordinates: A list of lon-lat tuples\n    :param lon_0: Longitude of projection center.\n    :param lat_ts: Latitude of true scale. Defines the latitude where scale is not distorted.\n    :param y_0: False northing\n    :param x_0: False easting\n    :param zone: UTM zone to use for projection (Defaults to 31)\n    \"\"\"\n    if zone is None:\n        # https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system\n        zone = 31\n    other_options = \" \".join(f\"+{key}={val}\" for key, val in kwargs.items())\n    proj = pyproj.Proj(f\"+proj=utm +zone={zone} +ellps=GRS80 +units=m \"\n                       f\"+lon_0={lon_0} +lat_ts={lat_ts} +y_0={y_0} +x_0={x_0} \"\n                       f\"+no_defs {other_options}\")\n    for lon, lat in coordinates:\n        x, y = proj(lon, lat)\n        yield x, y\n"
  },
  {
    "path": "leuvenmapmatching/util/segment.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.segment\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport logging\n\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n\nclass Segment(object):\n    \"\"\"Segment in the graph and its interpolated point.\"\"\"\n    __slots__ = [\"l1\", \"p1\", \"l2\", \"p2\", \"_pi\", \"_ti\"]\n\n    def __init__(self, l1, p1, l2=None, p2=None, pi=None, ti=None):\n        \"\"\"Create a new segment.\n\n        :param l1: Label of the node that is the start of the segment.\n        :param p1: Point (coordinate) of the start node.\n        :param l2: Label of the node that is the end of the segment.\n        :param p2: Point (coordinate) of the end node.\n        :param pi: Interpolated point. The point that is the best match and\n            can be in between p1 and p2.\n        :param ti: Position of pi on the segment [p1,p2], thus pi = p1+t1*(p2-p1).\n        \"\"\"\n        self.l1 = l1  # Start of segment, label\n        self.p1 = p1  # point\n        self.l2 = l2  # End of segment, if None the segment is a point\n        self.p2 = p2\n        self.pi = pi  # Interpolated point\n        self.ti = ti  # Position on segment p1-p2\n\n    @property\n    def label(self):\n        if self.l2 is None:\n            return self.l1\n        return f\"{self.l1}-{self.l2}\"\n\n    @property\n    def rlabel(self):\n        if self.l2 is None:\n            return self.l1\n        return f\"{self.l2}-{self.l1}\"\n\n    @property\n    def key(self):\n        if self.l2 is None:\n            return self.l1\n        return f\"{self.l1}-{self.l2}\"\n\n    @property\n    def pi(self):\n        if self.p2 is None:\n            return self.p1\n        return self._pi\n\n    @pi.setter\n    def pi(self, value):\n        if value is not None and len(value) > 2:\n            self._pi = tuple(value[:2])\n        else:\n            self._pi = value\n\n    @property\n    def ti(self):\n        if self.p2 is None:\n            return 0\n        return self._ti\n\n    @ti.setter\n    def ti(self, value):\n        self._ti = value\n\n    def is_point(self):\n        return self.p2 is None\n\n    def last_point(self):\n        if self.p2 is None:\n            return self.p1\n        return self.p2\n\n    def loc_to_str(self):\n        if self.p2 is None:\n            return f\"{self.p1}\"\n        if self._pi is not None:\n            return f\"{self.p1}-{self.pi}/{self.ti}-{self.p2}\"\n        return f\"{self.p1}-{self.p2}\"\n\n    def __str__(self):\n        if self.p2 is None:\n            return f\"{self.l1}\"\n        if self._pi is not None:\n            return f\"{self.l1}-i-{self.l2}\"\n        return f\"{self.l1}-{self.l2}\"\n\n    def __repr__(self):\n        return \"Segment<\" + self.__str__() + \">\"\n"
  },
  {
    "path": "leuvenmapmatching/visualization.py",
    "content": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.visualization\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport math\nimport random\nimport logging\nfrom itertools import islice\n\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom matplotlib import colors as mcolors\nimport smopy\n\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\ngraph_color = mcolors.CSS4_COLORS['darkmagenta']\nmatch_color = mcolors.CSS4_COLORS['green']\nmatch_ne_color = mcolors.CSS4_COLORS['olive']\nlattice_color = mcolors.CSS4_COLORS['magenta']\nnodes_color = mcolors.CSS4_COLORS['cyan']\npath_color = mcolors.CSS4_COLORS['blue']\nfontsize = 11  # 7  # 11\n\n\ndef plot_map(map_con, path=None, nodes=None, counts=None, ax=None, use_osm=False, z=None, bb=None,\n             show_labels=False, matcher=None, show_graph=False, zoom_path=False, show_lattice=False,\n             show_matching=False, filename=None, linewidth=2, coord_trans=None,\n             figwidth=20, lattice_nodes=None):\n    \"\"\"Plot the db/graph and optionally include the observed path and inferred nodes.\n\n    :param map_con: Map\n    :param path: list[(lat, lon)]\n    :param nodes: list[str]\n    :param counts: Number of visits of a node in the lattice. dict[label, int]\n    :param ax: Matplotlib axis\n    :param use_osm: Use OpenStreetMap layer, the points should be latitude-longitude pairs.\n    :param matcher: Matcher object (overrules given path, nodes and counts)\n    :param filename: File to write image to\n    :param show_graph: Plot the vertices and edges in the graph\n    :return: None\n    \"\"\"\n    if matcher is not None:\n        path = matcher.path\n        counts = matcher.node_counts()\n        nodes = None\n        if lattice_nodes is None:\n            lat_nodes = matcher.lattice_best\n        else:\n            lat_nodes = lattice_nodes\n        if lat_nodes is None:\n            lat_nodes = []\n    else:\n        lat_nodes = []\n\n    if not bb:\n        bb = map_con.bb()\n    lat_min, lon_min, lat_max, lon_max = bb\n    if path:\n        plat, plon = islice(zip(*path), 2)\n        lat_min, lat_max = min(lat_min, min(plat)), max(lat_max, max(plat))\n        lon_min, lon_max = min(lon_min, min(plon)), max(lon_max, max(plon))\n        bb = [lat_min, lon_min, lat_max, lon_max]\n    logger.debug(\"bb = [{}, {}, {}, {}]\".format(*bb))\n\n    if zoom_path and path:\n        if type(zoom_path) is slice:\n            plat, plon = islice(zip(*path[zoom_path]), 2)\n            lat_min, lat_max = min(plat), max(plat)\n            lon_min, lon_max = min(plon), max(plon)\n        else:\n            plat, plon = islice(zip(*path), 2)\n            lat_min, lat_max = min(plat), max(plat)\n            lon_min, lon_max = min(plon), max(plon)\n        lat_d = lat_max - lat_min\n        lon_d = lon_max - lon_min\n        latlon_d = max(lat_d, lon_d)\n        lat_max += max(latlon_d * 0.01, lat_d * 0.2)\n        lon_min -= max(latlon_d * 0.01, lon_d * 0.2)\n        lat_min -= max(latlon_d * 0.01, lat_d * 0.2)\n        lon_max += max(latlon_d * 0.01, lon_d * 0.2)\n        logger.debug(\"Setting bounding box to path\")\n        bb = [lat_min, lon_min, lat_max, lon_max]\n        logger.debug(\"bb(zoom-path) = [{}, {}, {}, {}]\".format(*bb))\n\n    bb_o = bb\n    if coord_trans:\n        logger.debug(\"Converting bounding box coordinates\")\n        if path:\n            path = [coord_trans(lat, lon) for lat, lon in path]\n        lat_min, lon_min, lat_max, lon_max = bb\n        lat_min, lon_min = coord_trans(lat_min, lon_min)\n        lat_max, lon_max = coord_trans(lat_max, lon_max)\n        bb = [lat_min, lon_min, lat_max, lon_max]\n        logger.debug(\"bb = [{}, {}, {}, {}]\".format(*bb))\n\n    if use_osm:\n        from .util import dist_latlon\n        project = dist_latlon.project\n        if z is None:\n            z = 18\n        m = smopy.Map(bb, z=z, ax=ax)\n        to_pixels = m.to_pixels\n        x_max, y_max = to_pixels(lat_max, lon_max)\n        x_min, y_min = to_pixels(lat_min, lon_min)\n        height = figwidth / abs(x_max - x_min) * abs(y_max - y_min)\n        if ax is None:\n            ax = m.show_mpl(figsize=(figwidth, height))\n        else:\n            ax = m.show_mpl(ax=ax)\n        fig = None\n\n    else:\n        from .util import dist_euclidean\n        project = dist_euclidean.project\n\n        def to_pixels(lat, lon=None):\n            if lon is None:\n                lat, lon = lat[0], lat[1]\n            return lon, lat\n        x_max, y_max = to_pixels(lat_max, lon_max)\n        x_min, y_min = to_pixels(lat_min, lon_min)\n        height = figwidth / abs(lon_max - lon_min) * abs(lat_max - lat_min)\n        if ax is None:\n            fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(figwidth, height))\n        else:\n            fig = None\n        ax.set_xlim([x_min, x_max])\n        ax.set_ylim([y_min, y_max])\n\n    # if counts is None:\n    #     node_sizes = [10] * map_con.size()\n    # else:\n    #     node_sizes = [counts[label]*100+5 for label in map_con.labels()]\n\n    if show_graph:\n        logger.debug('Plot vertices ...')\n        cnt = 0\n        for key, coord in map_con.all_nodes(bb=bb_o):\n            if coord_trans:\n                coord = coord_trans(*coord)\n            coord = to_pixels(coord)\n            plt.plot(coord[0], coord[1], marker='o', markersize=2*linewidth, color=graph_color, alpha=0.75)\n            if show_labels:\n                key = str(key)\n                if type(show_labels) is int:\n                    key = key[-show_labels:]\n                xytext = ax.transLimits.transform(coord)\n                xytext = (xytext[0]+0.001, xytext[1]+0.0)\n                xytext = ax.transLimits.inverted().transform(xytext)\n                # key = str(key)[-3:]\n                # print(f'annotate: {key} {coord} {xytext}')\n                ann = ax.annotate(key, xy=coord, xytext=xytext,\n                            # textcoords=('axes fraction', 'axes fraction'),\n                            # arrowprops=dict(arrowstyle='->'),\n                            color=graph_color, fontsize=fontsize)\n                # ann.set_rotation(45)\n            cnt += 1\n        logger.debug(f'... done, {cnt} nodes')\n\n        logger.debug('Plot lines ...')\n        cnt = 0\n        for row in map_con.all_edges(bb=bb_o):\n            loc_a = row[1]\n            loc_b = row[3]\n            if coord_trans:\n                loc_a = coord_trans(*loc_a)\n                loc_b = coord_trans(*loc_b)\n            x_a, y_a = to_pixels(*loc_a)\n            x_b, y_b = to_pixels(*loc_b)\n            ax.plot([x_a, x_b], [y_a, y_b], color=graph_color, linewidth=linewidth, markersize=linewidth)\n            cnt += 1\n        logger.debug(f'... done, {cnt} edges')\n\n    if show_lattice:\n        if matcher is None:\n            logger.warning(\"Matcher needs to be passed to show lattice. Not showing lattice.\")\n        plot_lattice(ax, to_pixels, matcher)\n\n    if path:\n        logger.debug('Plot path ...')\n        if type(zoom_path) is slice:\n            path_startidx = zoom_path.start\n            path_slice = path[zoom_path]\n        else:\n            path_startidx = 0\n            path_slice = path\n        px, py = zip(*[to_pixels(p[:2]) for p in path_slice])\n        ax.plot(px, py, linewidth=linewidth, markersize=linewidth * 2, alpha=0.75,\n                linestyle=\"--\", marker='o', color=path_color)\n        if show_labels:\n            for li, (lx, ly) in enumerate(zip(px, py)):\n                # ax.text(lx, ly, f\"O{li}\", color=path_color)\n                ann = ax.annotate(f\"O{path_startidx + li}\", xy=(lx, ly), color=path_color, fontsize=fontsize)\n                ann.set_rotation(45)\n\n    if nodes or matcher:\n        logger.debug('Plot nodes ...')\n        xs, ys, ls = [], [], []\n        prev = None\n        node_locs = []\n        if nodes:\n            for node in nodes:\n                if type(node) == tuple:\n                    node = node[0]\n                lat, lon = map_con.node_coordinates(node)\n                node_locs.append((lat, lon, node))\n        elif lat_nodes is not None:\n            prev_m = None\n            for m in lat_nodes:\n                if prev_m is not None and prev_m.edge_m.l2 == m.edge_m.l1 \\\n                        and prev_m.edge_m.l1 != m.edge_m.l2:\n                    lat, lon = m.edge_m.p1\n                    node_locs.append((lat, lon, m.edge_m.l1))\n                lat, lon = m.edge_m.pi\n                node_locs.append((lat, lon, m.edge_m.label))\n                prev_m = m\n        for lat, lon, label in node_locs:\n            if coord_trans:\n                lat, lon = coord_trans(lat, lon)\n            if bb[0] <= lat <= bb[2] and bb[1] <= lon <= bb[3]:\n                if prev is not None:\n                    x, y = to_pixels(*prev)\n                    xs.append(x)\n                    ys.append(y)\n                    ls.append(label)\n                    prev = None\n                x, y = to_pixels(lat, lon)\n                xs.append(x)\n                ys.append(y)\n                ls.append(label)\n            else:\n                if prev is None:\n                    x, y = to_pixels(lat, lon)\n                    xs.append(x)\n                    ys.append(y)\n                    ls.append(label)\n                prev = lat, lon\n        ax.plot(xs, ys, 'o-', linewidth=linewidth * 3, markersize=linewidth * 3, alpha=0.75,\n                color=nodes_color)\n        # if show_labels:\n        #     for label, lx, ly in zip(ls, xs, ys):\n        #         ax.annotate(label, xy=(lx, ly), xytext=(lx - 30, ly), color=nodes_color)\n\n    if matcher and show_matching:\n        logger.debug('Plot matching path-nodes (using matcher) ...')\n        for idx, m in enumerate(lat_nodes):\n            lat, lon = m.edge_m.pi[:2]\n            lat2, lon2 = m.edge_o.pi[:2]\n            if coord_trans:\n                lat, lon = coord_trans(lat, lon)\n                lat2, lon2 = coord_trans(lat2, lon2)\n            x, y = to_pixels(lat, lon)\n            x2, y2 = to_pixels(lat2, lon2)\n            if m.edge_o.is_point():\n                plt.plot(x, y, marker='x', markersize=2 * linewidth, color=match_color, alpha=0.75)\n                plt.plot(x2, y2, marker='+', markersize=2 * linewidth, color=match_color, alpha=0.75)\n                ax.plot((x, x2), (y, y2), '-', color=match_color, linewidth=linewidth, alpha=0.75)\n            else:\n                plt.plot(x, y, marker='x', markersize=2 * linewidth, color=match_ne_color, alpha=0.75)\n                plt.plot(x2, y2, marker='+', markersize=2 * linewidth, color=match_ne_color, alpha=0.75)\n                ax.plot((x, x2), (y, y2), '-', color=match_ne_color, linewidth=linewidth, alpha=0.75)\n            # ax.plot((x, x2), (y, y2), '-', color=match_color, linewidth=10, alpha=0.1)\n            # if show_labels:\n            #     ax.annotate(f\"{m.obs}.{m.obs_ne}\", xy=(x, y))\n    elif path and nodes and len(path) == len(nodes) and show_matching:\n        logger.debug('Plot matching path-nodes (using sequence of nodes) ...')\n        for idx, (loc, node) in enumerate(zip(path, nodes)):\n            x, y = to_pixels(*loc)\n            if type(node) == tuple and (len(node) == 4 or len(node) == 2):\n                latlon2, latlon3 = map_con.node_coordinates(node[0]), map_con.node_coordinates(node[1])\n                if coord_trans:\n                    latlon2 = coord_trans(*latlon2)\n                    latlon3 = coord_trans(*latlon3)\n                latlon4, _ = project(latlon2, latlon3, loc)\n                x4, y4 = to_pixels(*latlon4)\n                ax.plot((x, x4), (y, y4), '-', color=match_color, linewidth=linewidth, alpha=0.75)\n            elif type(node) == tuple and len(node) == 3:\n                lat2, lon2 = map_con.node_coordinates(node[0])\n                if coord_trans:\n                    lat2, lon2 = coord_trans(lat2, lon2)\n                x2, y2 = to_pixels(lat2, lon2)\n                ax.plot((x, x2), (y, y2), '-', color=match_color, linewidth=linewidth, alpha=0.75)\n            elif type(node) == str or type(node) == int:\n                lat2, lon2 = map_con.node_coordinates(node[0])\n                if coord_trans:\n                    lat2, lon2 = coord_trans(lat2, lon2)\n                x2, y2 = to_pixels(lat2, lon2)\n                ax.plot((x, x2), (y, y2), '-', color=match_color, linewidth=linewidth, alpha=0.75)\n            else:\n                raise Exception('Unknown node type: {} ({})'.format(node, type(node)))\n            # if show_labels:\n            #     ax.annotate(str(idx), xy=(x, y))\n    if map_con.use_latlon:\n        ax.set_xlabel('Longitude')\n        ax.set_ylabel('Latitude')\n    else:\n        ax.set_xlabel('X')\n        ax.set_ylabel('Y')\n    ax.axis('equal')\n    ax.set_aspect('equal')\n    if filename is not None:\n        plt.savefig(filename)\n        if fig is not None:\n            plt.close(fig)\n            fig = None\n            ax = None\n    return fig, ax\n\n\ndef plot_lattice(ax, to_pixels, matcher):\n    for idx in range(len(matcher.lattice)):\n        if len(matcher.lattice[idx]) == 0:\n            continue\n        for m in matcher.lattice[idx].values_all():\n            for mp in m.prev:\n                if m.stop:\n                    alpha = 0.1\n                    linewidth = 1\n                else:\n                    alpha = 0.3\n                    linewidth = 3\n                if mp.edge_m.p2 is None:\n                    prv = mp.edge_m.p1\n                else:\n                    prv = mp.edge_m.p2\n                nxt = m.edge_m.p1\n                x1, y1 = to_pixels(*prv)\n                x2, y2 = to_pixels(*nxt)\n                ax.plot((x1, x2), (y1, y2), '.-', color=lattice_color, linewidth=linewidth, alpha=alpha)\n                if m.edge_m.p2 is not None:\n                    x1, y1 = to_pixels(*m.edge_m.p1)\n                    x2, y2 = to_pixels(*m.edge_m.p2)\n                    ax.plot((x1, x2), (y1, y2), '.-', color=lattice_color, linewidth=linewidth, alpha=alpha)\n\n\ndef plot_obs_noise_dist(obs_fn, obs_noise, min_dist=0, max_dist=10):\n    \"\"\"Plot the expected noise of an observation distribution.\n\n    :param matcher: Matcher\n    :return:\n    \"\"\"\n    x = np.linspace(min_dist, max_dist, 100)\n    y = [obs_fn(xi) for xi in x]\n    plt.plot(x, y)\n    plt.xlabel(\"Distance\")\n    plt.ylabel(\"Probability\")\n    plt.xlim((min_dist, max_dist))\n    plt.ylim((0, 1))\n    plt.axvline(x=obs_noise, color='red', alpha=0.7)\n    plt.annotate(\"Observation noise stddev\", xy=(obs_noise, 0))\n"
  },
  {
    "path": "setup.cfg",
    "content": "[metadata]\nname = leuvenmapmatching\nversion = attr: leuvenmapmatching.__version__\nauthor = Wannes Meert\ndescription = Match a trace of GPS positions to a locations and streets on a map\nlicense = Apache 2.0\nlong_description = file: README.md\nlong_description_content_type = text/markdown\nurl = https://github.com/wannesm/LeuvenMapMatching\nproject_urls =\n    Bug Tracker = https://github.com/wannesm/LeuvenMapMatching/issues\nclassifiers =\n    Programming Language :: Python :: 3\n    License :: OSI Approved :: Apache Software License\n    Operating System :: OS Independent\nkeywords = map, matching\n\n[options]\npackages = find:\npython_requires = >=3.6\ninstall_requires =\n    numpy\n    scipy\ntests_requires =\n    pytest-runner\n    pytest\n\n[options.extras_require]\nvis = smopy; matplotlib>=2.0.0\ndb = rtree; pyproj\nall = requests; smopy; matplotlib>=2.0.0; rtree; pyproj; nvector==0.5.2; gpxpy; pykalman; pytest; pytest-runner; osmread; osmnx\n# In case of problems with osmread, use: \"osmread @ git+https://github.com/dezhin/osmread\"\n\n[aliases]\ntest=pytest\n\n[tool:pytest]\nnorecursedirs = .git venv* .eggs\naddopts = --verbose\npython_files = tests/*.py tests/*/*.py\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\nsetup.py\n~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2017-2021 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nfrom setuptools import setup, find_packages\nimport re\nimport os\n\nhere = os.path.abspath(os.path.dirname(__file__))\n\n\nwith open(os.path.join('leuvenmapmatching', '__init__.py'), 'r') as fd:\n    version = re.search(r'^__version__\\s*=\\s*[\\'\"]([^\\'\"]*)[\\'\"]',\n                        fd.read(), re.MULTILINE).group(1)\nif not version:\n    raise RuntimeError('Cannot find version information')\n\n\nsetup(\n    name='leuvenmapmatching',\n    version=version,\n    packages=find_packages(),\n    author='Wannes Meert',\n    author_email='wannes.meert@cs.kuleuven.be',\n    url='https://github.com/wannesm/LeuvenMapMatching',\n    description='Match a trace of GPS positions to a locations and streets on a map',\n    python_requires='>=3.6',\n    license='Apache 2.0',\n    classifiers=[\n        'Intended Audience :: Developers',\n        'License :: OSI Approved :: Apache Software License',\n        'Programming Language :: Python :: 3'\n    ],\n    keywords='map matching',\n)\n"
  },
  {
    "path": "tests/examples/example_1_simple.py",
    "content": "from leuvenmapmatching.matcher.distance import DistanceMatcher\nfrom leuvenmapmatching.map.inmem import InMemMap\n\nmap_con = InMemMap(\"mymap\", graph={\n    \"A\": ((1, 1), [\"B\", \"C\", \"X\"]),\n    \"B\": ((1, 3), [\"A\", \"C\", \"D\", \"K\"]),\n    \"C\": ((2, 2), [\"A\", \"B\", \"D\", \"E\", \"X\", \"Y\"]),\n    \"D\": ((2, 4), [\"B\", \"C\", \"F\", \"E\", \"K\", \"L\"]),\n    \"E\": ((3, 3), [\"C\", \"D\", \"F\", \"Y\"]),\n    \"F\": ((3, 5), [\"D\", \"E\", \"L\"]),\n    \"X\": ((2, 0), [\"A\", \"C\", \"Y\"]),\n    \"Y\": ((3, 1), [\"X\", \"C\", \"E\"]),\n    \"K\": ((1, 5), [\"B\", \"D\", \"L\"]),\n    \"L\": ((2, 6), [\"K\", \"D\", \"F\"])\n}, use_latlon=False)\n\npath = [(0.8, 0.7), (0.9, 0.7), (1.1, 1.0), (1.2, 1.5), (1.2, 1.6), (1.1, 2.0),\n        (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),\n        (2.3, 3.5), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2),\n        (3.1, 3.8), (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]\n\nmatcher = DistanceMatcher(map_con, max_dist=2, obs_noise=1, min_prob_norm=0.5, max_lattice_width=5)\nstates, _ = matcher.match(path)\nnodes = matcher.path_pred_onlynodes\n\nprint(\"States\\n------\")\nprint(states)\nprint(\"Nodes\\n------\")\nprint(nodes)\nprint(\"\")\nmatcher.print_lattice_stats()\n"
  },
  {
    "path": "tests/examples/example_using_osmnx_and_geopandas.py",
    "content": "import os\nimport sys\nimport logging\nfrom pathlib import Path\n\nthis_path = Path(os.path.realpath(__file__)).parent.parent / \"rsrc\" / \"path_latlon\"\nassert(this_path.exists())\npath_to_mytrack_gpx = this_path / \"route.gpx\"\nassert(path_to_mytrack_gpx.exists())\n\nimport leuvenmapmatching as mm\nfrom leuvenmapmatching.map.inmem import InMemMap\n\n\ndef run():\n    # Start example\n    import osmnx as ox\n\n    # Select map (all, drive, walk, ...)\n    graph = ox.graph_from_place('Leuven, Belgium', network_type='all', simplify=False)\n    graph_proj = ox.project_graph(graph)\n\n    # Create GeoDataFrames\n    # Approach 1: translate map to graph\n    # DistanceMatcher uses edges, thus build index based on edges\n    map_con = InMemMap(\"myosm\", use_latlon=True, use_rtree=True, index_edges=True)\n    nodes_proj, edges_proj = ox.graph_to_gdfs(graph_proj, nodes=True, edges=True)\n    for nid, row in nodes_proj[['x', 'y']].iterrows():\n        map_con.add_node(nid, (row['x'], row['y']))\n    for eid, _ in edges_proj.iterrows():\n        map_con.add_edge(eid[0], eid[1])\n\n    # Approach 2: use a specific projection\n    map_con = InMemMap(\"myosm\", use_latlon=True, use_rtree=True, index_edges=True)\n    nodes_proj, edges_proj = ox.graph_to_gdfs(graph_proj, nodes=True, edges=True)\n    nodes_proj = nodes_proj.to_crs(\"EPSG:3395\")\n    # edges_proj = edges_proj.to_crs(\"EPSG:3395\")\n    for nid, row in nodes_proj.iterrows():\n        map_con.add_node(nid, (row['lat'], row['lon']))\n    # We can also extract edges also directly from networkx graph\n    for nid1, nid2, _ in graph.edges:\n        map_con.add_edge(nid1, nid2)\n\n    # Perform matching\n    from leuvenmapmatching.util.gpx import gpx_to_path\n    from leuvenmapmatching.matcher.distance import DistanceMatcher\n\n    track = gpx_to_path(path_to_mytrack_gpx)\n    matcher = DistanceMatcher(map_con,\n                             max_dist=100, max_dist_init=50,  # meter\n                             non_emitting_length_factor=0.75,\n                             obs_noise=50, obs_noise_ne=75,  # meter\n                             dist_noise=50,  # meter\n                             non_emitting_states=True,\n                             max_lattice_width=5)\n    states, lastidx = matcher.match(track)\n    print(states)\n\n    # End example\n\n    # import leuvenmapmatching.visualization as mm_viz\n    # import matplotlib as mpl\n    # mpl.use('MacOSX')\n    # mm_viz.plot_map(map_con, matcher=matcher, use_osm=True,\n    #                 zoom_path=True, show_graph=True,\n    #                 filename=Path(os.environ.get('TESTDIR', Path(__file__).parent)) / \"example.png\")\n\n\nif __name__ == \"__main__\":\n    mm.logger.setLevel(logging.INFO)\n    mm.logger.addHandler(logging.StreamHandler(sys.stdout))\n    run()\n"
  },
  {
    "path": "tests/rsrc/bug2/readme.md",
    "content": "Test data\n=========\n\nDownload from https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/leuvenmapmatching_testdata.zip\n\n- `edgesrl.csv`\n- `nodesrl.csv`\n- `path.csv`\n\n"
  },
  {
    "path": "tests/rsrc/newson_krumm_2009/readme.md",
    "content": "Newson Krum testdata\n====================\n\nFiles will be downloaded from https://www.microsoft.com/en-us/research/publication/hidden-markov-map-matching-noise-sparseness/\n\n- `gps_data.txt`\n- `road_network.txt`\n- `ground_truth_route.txt`\n\n\n"
  },
  {
    "path": "tests/rsrc/path_latlon/readme.md",
    "content": "Test data for path_latlon\n=========================\n\nDownload from https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/leuvenmapmatching_testdata2.zip\n\n- `route.gpx`\n- `route2.gpx`\n- `osm_downloaded.xml`\n- `osm_downloaded2.xml`\n\n"
  },
  {
    "path": "tests/rsrc/path_latlon/route.gpx",
    "content": "<?xml version=\"1.0\"?>\r\n<gpx\r\n  version=\"1.0\"\r\n  creator=\"VB Net GPS:  vermeiren-willy@pandora.be\"\r\n  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n  xmlns=\"http://www.topografix.com/GPX/1/0\"\r\n  xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd\">\r\n\r\n<bounds minlat=\"50.870247\" minlon=\"4.695133\" maxlat=\"50.879428\" maxlon=\"4.709056\"/>\r\n\r\n<wpt lat=\"50.879428\" lon=\"4.699974\">\r\n   <name>START</name>\r\n</wpt>\r\n\r\n<trk>\r\n  <name>Leuven Stadswandeling   5 km TR</name>\r\n  <trkseg>\r\n    <trkpt lat=\"50.879428\" lon=\"4.699974\">\r\n      <name>1</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.879428\" lon=\"4.700002\">\r\n      <name>2</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.879302\" lon=\"4.700429\">\r\n      <name>3</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.879105\" lon=\"4.700315\">\r\n      <name>4</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.879105\" lon=\"4.700344\">\r\n      <name>5</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.879015\" lon=\"4.700885\">\r\n      <name>6</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.877506\" lon=\"4.7006\">\r\n      <name>7</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.877326\" lon=\"4.70134\">\r\n      <name>8</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.87711\" lon=\"4.70134\">\r\n      <name>9</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.87711\" lon=\"4.701739\">\r\n      <name>10</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.877631\" lon=\"4.702309\">\r\n      <name>11</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.878045\" lon=\"4.702451\">\r\n      <name>12</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.877883\" lon=\"4.70359\">\r\n      <name>13</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.878063\" lon=\"4.70359\">\r\n      <name>14</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.878602\" lon=\"4.703106\">\r\n      <name>15</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.879141\" lon=\"4.70433\">\r\n      <name>16</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.878548\" lon=\"4.705469\">\r\n      <name>17</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.878548\" lon=\"4.705526\">\r\n      <name>18</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.879051\" lon=\"4.706266\">\r\n      <name>19</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.877506\" lon=\"4.709056\">\r\n      <name>20</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.876985\" lon=\"4.708373\">\r\n      <name>21</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.877649\" lon=\"4.707035\">\r\n      <name>22</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.876715\" lon=\"4.706038\">\r\n      <name>23</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.876248\" lon=\"4.70675\">\r\n      <name>24</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.876122\" lon=\"4.706807\">\r\n      <name>25</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.876122\" lon=\"4.705839\">\r\n      <name>26</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.875332\" lon=\"4.70433\">\r\n      <name>27</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.874649\" lon=\"4.70228\">\r\n      <name>28</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.875206\" lon=\"4.702365\">\r\n      <name>29</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.875278\" lon=\"4.702081\">\r\n      <name>30</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.875619\" lon=\"4.701824\">\r\n      <name>31</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.875871\" lon=\"4.701768\">\r\n      <name>32</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.875691\" lon=\"4.700287\">\r\n      <name>33</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.872745\" lon=\"4.699746\">\r\n      <name>34</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.872834\" lon=\"4.698095\">\r\n      <name>35</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.871002\" lon=\"4.697981\">\r\n      <name>36</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.870858\" lon=\"4.698408\">\r\n      <name>37</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.870589\" lon=\"4.698465\">\r\n      <name>38</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.870247\" lon=\"4.698465\">\r\n      <name>39</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.870265\" lon=\"4.698322\">\r\n      <name>40</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.870822\" lon=\"4.698322\">\r\n      <name>41</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.87102\" lon=\"4.69781\">\r\n      <name>42</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.871649\" lon=\"4.697924\">\r\n      <name>43</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.871631\" lon=\"4.697639\">\r\n      <name>44</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.872206\" lon=\"4.697611\">\r\n      <name>45</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.872295\" lon=\"4.697013\">\r\n      <name>46</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.871649\" lon=\"4.697013\">\r\n      <name>47</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.871774\" lon=\"4.695845\">\r\n      <name>48</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.87226\" lon=\"4.695931\">\r\n      <name>49</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.873032\" lon=\"4.696102\">\r\n      <name>50</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.874685\" lon=\"4.696187\">\r\n      <name>51</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.874811\" lon=\"4.696643\">\r\n      <name>52</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.875098\" lon=\"4.697098\">\r\n      <name>53</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.87526\" lon=\"4.697668\">\r\n      <name>54</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.876032\" lon=\"4.697753\">\r\n      <name>55</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.876571\" lon=\"4.695133\">\r\n      <name>56</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.876679\" lon=\"4.695162\">\r\n      <name>57</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.87614\" lon=\"4.697696\">\r\n      <name>58</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.875853\" lon=\"4.698237\">\r\n      <name>59</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.875727\" lon=\"4.700088\">\r\n      <name>60</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.875835\" lon=\"4.700315\">\r\n      <name>61</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.877057\" lon=\"4.700486\">\r\n      <name>62</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.877254\" lon=\"4.699177\">\r\n      <name>63</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.878674\" lon=\"4.700116\">\r\n      <name>64</name>\r\n    </trkpt>\r\n    <trkpt lat=\"50.878979\" lon=\"4.700258\">\r\n      <name>65</name>\r\n    </trkpt>\r\n  </trkseg>\r\n</trk>\r\n</gpx>\r\n"
  },
  {
    "path": "tests/rsrc/path_latlon/route2.gpx",
    "content": "<?xml version=\"1.0\"?>\r\n<gpx\r\n  version=\"1.0\"\r\n  creator=\"Wannes\"\r\n  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n  xmlns=\"http://www.topografix.com/GPX/1/0\"\r\n  xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd\">\r\n\r\n<bounds minlat=\"50.870247\" minlon=\"4.695133\" maxlat=\"50.879428\" maxlon=\"4.709056\"/>\r\n\r\n<wpt lat=\"50.879428\" lon=\"4.699974\">\r\n   <name>START</name>\r\n</wpt>\r\n\r\n<trk>\r\n  <name>Test route</name>\r\n  <trkseg>\r\n      <trkpt lon=\"4.6997666\" lat=\"50.8684188\">\r\n          <name>1</name>\r\n      </trkpt>\r\n      <trkpt lon=\"4.7008181\" lat=\"50.8692042\">\r\n          <name>2</name>\r\n      </trkpt>\r\n      <trkpt lon=\"4.7007751\" lat=\"50.8694344\">\r\n          <name>3</name>\r\n      </trkpt>\r\n      <trkpt lon=\"4.7015691\" lat=\"50.8699625\">\r\n          <name>4</name>\r\n      </trkpt>\r\n      <trkpt lon=\"4.7035003\" lat=\"50.8703282\">\r\n          <name>5</name>\r\n      </trkpt>\r\n      <trkpt lon=\"4.7038221\" lat=\"50.8714927\">\r\n          <name>6</name>\r\n      </trkpt>\r\n      <trkpt lon=\"4.7042942\" lat=\"50.8724812\">\r\n          <name>7</name>\r\n      </trkpt>\r\n      <trkpt lon=\"4.7052813\" lat=\"50.8731718\">\r\n          <name>8</name>\r\n      </trkpt>\r\n  </trkseg>\r\n</trk>\r\n</gpx>\r\n"
  },
  {
    "path": "tests/test_bugs.py",
    "content": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_bugs\n~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport os\nimport sys\nimport logging\nfrom pathlib import Path\nimport csv\n\nimport leuvenmapmatching as mm\nfrom leuvenmapmatching.map.inmem import InMemMap\nfrom leuvenmapmatching.map.sqlite import SqliteMap\nfrom leuvenmapmatching.matcher.simple import SimpleMatcher\nfrom leuvenmapmatching.matcher.distance import DistanceMatcher\nimport leuvenmapmatching.visualization as mm_viz\nMYPY = False\nif MYPY:\n    from typing import List, Tuple\n\n\nlogger = mm.logger\ndirectory = None\n\n\ndef test_bug1():\n    dist = 10\n    nb_steps = 20\n\n    map_con = InMemMap(\"map\", graph={\n        \"A\":  ((1, dist), [\"B\"]),\n        \"B\":  ((2, dist), [\"A\", \"C\", \"CC\"]),\n        \"C\":  ((3, 0), [\"B\", \"D\"]),\n        \"D\":  ((4 + dist, 0), [\"C\", \"E\"]),\n        \"CC\": ((3, 2 * dist), [\"B\", \"DD\"]),\n        \"DD\": ((4 + dist, 2 * dist), [\"CC\", \"E\"]),\n        \"E\":  ((5 + dist, dist), [\"F\", \"D\", \"DD\"]),\n        \"F\":  ((6 + dist, dist), [\"E\", ]),\n\n    }, use_latlon=False)\n\n    i = 10\n    path = [(1.1,      2*dist*i/nb_steps),\n            (2.1,      2*dist*i/nb_steps),\n            (5.1+dist, 2*dist*i/nb_steps),\n            (6.1+dist, 2*dist*i/nb_steps)\n            # (1, len*i/nb_steps),\n            # (2, len*i/nb_steps),\n            # (3, len*i/nb_steps)\n            ]\n\n    matcher = SimpleMatcher(map_con, max_dist=dist + 1, obs_noise=dist + 1, min_prob_norm=None,\n                                  non_emitting_states=True)\n\n    nodes = matcher.match(path, unique=False)\n    print(\"Solution: \", nodes)\n    if directory:\n        import leuvenmapmatching.visualization as mm_vis\n        matcher.print_lattice()\n        matcher.print_lattice_stats()\n        mm_vis.plot_map(map_con, path=path, nodes=nodes, counts=matcher.node_counts(),\n                        show_labels=True, filename=str(directory / \"test_bugs_1.png\"))\n\n\ndef test_bug2():\n    this_path = Path(os.path.realpath(__file__)).parent / \"rsrc\" / \"bug2\"\n    edges_fn = this_path / \"edgesrl.csv\"\n    nodes_fn = this_path / \"nodesrl.csv\"\n    path_fn = this_path / \"path.csv\"\n    zip_fn = this_path / \"leuvenmapmatching_testdata.zip\"\n\n    if not (edges_fn.exists() and nodes_fn.exists() and path_fn.exists()):\n        import requests\n        url = 'https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/leuvenmapmatching_testdata.zip'\n        logger.debug(\"Download testfiles from kuleuven.be\")\n        r = requests.get(url, stream=True)\n        with zip_fn.open('wb') as ofile:\n            for chunk in r.iter_content(chunk_size=1024):\n                if chunk:\n                    ofile.write(chunk)\n        import zipfile\n        logger.debug(\"Unzipping leuvenmapmatching_testdata.zip\")\n        with zipfile.ZipFile(str(zip_fn), \"r\") as zip_ref:\n            zip_ref.extractall(str(zip_fn.parent))\n\n    logger.debug(f\"Reading map ...\")\n    mmap = SqliteMap(\"road_network\", use_latlon=True, dir=this_path)\n\n    path = []\n    with path_fn.open(\"r\") as path_f:\n        reader = csv.reader(path_f, delimiter=',')\n        for row in reader:\n            lat, lon = [float(coord) for coord in row]\n            path.append((lat, lon))\n    node_cnt = 0\n    with nodes_fn.open(\"r\") as nodes_f:\n        reader = csv.reader(nodes_f, delimiter=',')\n        for row in reader:\n            nid, lonlat, _ = row\n            nid = int(nid)\n            lon, lat = [float(coord) for coord in lonlat[1:-1].split(\",\")]\n            mmap.add_node(nid, (lat, lon), ignore_doubles=True, no_index=True, no_commit=True)\n            node_cnt += 1\n    edge_cnt = 0\n    with edges_fn.open(\"r\") as edges_f:\n        reader = csv.reader(edges_f, delimiter=',')\n        for row in reader:\n            _eid, nid1, nid2, pid = [int(val) for val in row]\n            mmap.add_edge(nid1, nid2, edge_type=0, path=pid, no_index=True, no_commit=True)\n            edge_cnt += 1\n    logger.debug(f\"... done: {node_cnt} nodes and {edge_cnt} edges\")\n    logger.debug(\"Indexing ...\")\n    mmap.reindex_nodes()\n    mmap.reindex_edges()\n    logger.debug(\"... done\")\n\n    matcher = DistanceMatcher(mmap, min_prob_norm=0.001,\n                              max_dist=200, obs_noise=4.07,\n                              non_emitting_states=True)\n    # path = path[:2]\n    nodes, idx = matcher.match(path, unique=True)\n    path_pred = matcher.path_pred\n    if directory:\n        import matplotlib.pyplot as plt\n        matcher.print_lattice_stats()\n        logger.debug(\"Plotting post map ...\")\n        fig = plt.figure(figsize=(100, 100))\n        ax = fig.get_axes()\n        mm_viz.plot_map(mmap, matcher=matcher, use_osm=True, ax=ax,\n                        show_lattice=False, show_labels=True, show_graph=False, zoom_path=True,\n                        show_matching=True)\n        plt.savefig(str(directory / \"test_bug1.png\"))\n        plt.close(fig)\n        logger.debug(\"... done\")\n\n\nif __name__ == \"__main__\":\n    mm.logger.setLevel(logging.DEBUG)\n    mm.logger.addHandler(logging.StreamHandler(sys.stdout))\n    directory = Path(os.environ.get('TESTDIR', Path(__file__).parent))\n    print(f\"Saving files to {directory}\")\n    # test_bug1()\n    test_bug2()\n"
  },
  {
    "path": "tests/test_conversion.py",
    "content": "#!/usr/bin/env python3\n# encoding: utf-8\nimport sys\nimport logging\nfrom datetime import datetime\nimport pytest\nimport math\nimport os\nfrom pathlib import Path\nimport itertools\n\nimport leuvenmapmatching as mm\nfrom leuvenmapmatching.util import dist_euclidean as de\nfrom leuvenmapmatching.util import dist_latlon as dll\n\n\ndirectory = None\n\n\ndef test_path_to_gpx():\n    from leuvenmapmatching.util.gpx import path_to_gpx\n    path = [(i, i, datetime.fromtimestamp(i)) for i in range(0, 10)]\n    gpx = path_to_gpx(path)\n\n    assert len(path) == len(gpx.tracks[0].segments[0].points)\n    assert path[0][0] == pytest.approx(gpx.tracks[0].segments[0].points[0].latitude)\n    assert path[0][1] == pytest.approx(gpx.tracks[0].segments[0].points[0].longitude)\n    assert path[0][2] == gpx.tracks[0].segments[0].points[0].time\n\n\ndef test_grs80():\n    from leuvenmapmatching.util.projections import latlon2grs80\n    coordinates = [(4.67878, 50.864), (4.68054, 50.86381), (4.68098, 50.86332), (4.68129, 50.86303), (4.6817, 50.86284),\n                   (4.68277, 50.86371), (4.68894, 50.86895), (4.69344, 50.86987), (4.69354, 50.86992),\n                   (4.69427, 50.87157), (4.69643, 50.87315), (4.69768, 50.87552), (4.6997, 50.87828)]\n    points = latlon2grs80(coordinates, lon_0=coordinates[0][0], lat_ts=coordinates[0][1])\n    points = list(points)\n    point = points[0]\n    assert point[0] == pytest.approx(618139.9385518166)\n    assert point[1] == pytest.approx(5636043.991970774)\n\n\ndef test_distance1():\n    p1 = (38.898556, -77.037852)\n    p2 = (38.897147, -77.043934)\n    d = dll.distance(p1, p2)\n    assert d == pytest.approx(549.1557912048178), f\"Got: {d}\"\n\n\ndef test_distance2():\n    o_p1 = (6007539.987516373, -13607675.997610645)\n    m_p1 = (6007518.475594072, -13607641.049711559)\n    m_p2 = (6007576.295597112, -13607713.306589901)\n    dist, proj_m, t_m = de.distance_point_to_segment(o_p1, m_p1, m_p2)\n    assert dist == pytest.approx(5.038773480896327), f\"dist = {dist}\"\n    assert t_m == pytest.approx(0.4400926470800718), f\"t_m = {t_m}\"\n\n\ndef test_bearing1():\n    lat1, lon1 = math.radians(38.898556), math.radians(-77.037852)\n    lat2, lon2 = math.radians(38.897147), math.radians(-77.043934)\n    b = dll.bearing_radians(lat1, lon1, lat2, lon2)\n    b = math.degrees(b)\n    # assert b == pytest.approx(253.42138889), f\"Got: {b}\"\n    assert b == pytest.approx(-106.5748183426045), f\"Got: {b}\"\n\n\ndef test_destination1():\n    lat1, lon1 = math.radians(53.32055556), math.radians(1.72972222)\n    bearing = math.radians(96.02166667)\n    dist = 124800\n    lat2, lon2 = dll.destination_radians(lat1, lon1, bearing, dist)\n    lat2, lon2 = (math.degrees(lat2), math.degrees(lon2))\n    assert lat2 == pytest.approx(53.188269553709034), f\"Got: {lat2}\"\n    assert lon2 == pytest.approx(3.592721390871882), f\"Got: {lon2}\"\n\n\ndef test_distance_segment_to_segment1():\n    f1 = (50.900393, 4.728607)\n    f2 = (50.900389, 4.734047)\n    t1 = (50.898538, 4.726107)\n    t2 = (50.898176, 4.735463)\n    d, pf, pt, u_f, u_t = dll.distance_segment_to_segment(f1, f2, t1, t2)\n    if directory:\n        plot_distance_segment_to_segment_latlon(f1, f2, t1, t2, pf, pt, \"test_distance_segment_to_segment1\")\n    assert d == pytest.approx(216.60187728486514)\n    assert pf == pytest.approx((50.900392999999994, 4.728607))\n    assert pt == pytest.approx((50.898448650708666, 4.728418070396815))\n    assert u_f == pytest.approx(0)\n    assert u_t == pytest.approx(0.2470133466162735)\n\n\ndef test_distance_segment_to_segment2():\n    f1 = (0, 0)\n    f2 = (-0.43072496752146333, 381.4928613075559)\n    t1 = (-206.26362055248765, -175.32538004745732)\n    t2 = (-246.4746107556939, 480.8174213050763)\n    d, pf, pt, u_f, u_t = de.distance_segment_to_segment(f1, f2, t1, t2)\n    if directory:\n        plot_distance_segment_to_segment_euc(f1, f2, t1, t2, pf, pt, \"test_distance_segment_to_segment2\")\n    assert d == pytest.approx(216.60187728486514)\n    assert pf == pytest.approx((0.0, 0.0))\n    assert pt == pytest.approx((-216.1962718133358, -13.249350827191222))\n    assert u_f == pytest.approx(0)\n    assert u_t == pytest.approx(0.2470133466162735)\n\n\ndef test_distance_segment_to_segment3():\n    f1 = (50.87205, 4.66089)\n    f2 = (50.874550000000006, 4.672980000000001)\n    t1 = (50.8740376, 4.6705204)\n    t2 = (50.8741866999999, 4.67119980000001)\n    d, pf, pt, u_f, u_t = dll.distance_segment_to_segment(f1, f2, t1, t2)\n    if directory:\n        plot_distance_segment_to_segment_latlon(f1, f2, t1, t2, pf, pt, \"test_distance_segment_to_segment3\")\n    assert d == pytest.approx(0)\n    assert pf == pytest.approx((50.87410572908839, 4.670830969750696))\n    assert pt == pytest.approx((50.87410575464133, 4.670830955670548))\n    assert u_f == pytest.approx(0.8222551304652699)\n    assert u_t == pytest.approx(0.4571036354431931)\n\n\ndef test_distance_segment_to_segment4():\n    f1 = (0, 0)\n    f2 = (278.05674689789083, 848.3102386968303)\n    t1 = (221.055090540802, 675.7367042826397)\n    t2 = (237.6344733521503, 723.4080418578025)\n    d, pf, pt, u_f, u_t = de.distance_segment_to_segment(f1, f2, t1, t2)\n    if directory:\n        plot_distance_segment_to_segment_euc(f1, f2, t1, t2, pf, pt, \"test_distance_segment_to_segment4\")\n    assert d == pytest.approx(0)\n    assert pf == pytest.approx((228.63358669727376, 697.5274459946864))\n    assert pt == pytest.approx((228.63358669727376, 697.5274459946864))\n    assert u_f == pytest.approx(0.8222551304652699)\n    assert u_t == pytest.approx(0.4571036354431931)\n\n\ndef test_distance_point_to_segment1():\n    locs = [\n        (47.6373, -122.0950167),\n        (47.6369, -122.0950167),\n        (47.6369, -122.0959167),\n        (47.6369, -122.09422),\n        (47.6369, -122.09400),\n        (47.6375, -122.09505)\n    ]\n    loc_a = (47.6372498273849, -122.094900012016)\n    loc_b = (47.6368394494057, -122.094280421734)\n    segments = []\n    for lat_a, lat_b in itertools.product((loc_a[0], loc_b[0]), repeat=2):\n        for lon_a, lon_b in itertools.product((loc_a[1], loc_b[1]), repeat=2):\n            segments.append(((lat_a, lon_a), (lat_b, lon_b)))\n    # segments = [(loc_a, loc_b)]\n\n    for constrain in [True, False]:\n        for loc_idx, loc in enumerate(locs):\n            for seg_idx, (loc_a, loc_b) in enumerate(segments):\n                dist1, pi1, ti1 = dll.distance_point_to_segment(loc, loc_a, loc_b, constrain=constrain)\n                dist2, pi2, ti2 = dll.distance_point_to_segment(loc, loc_b, loc_a, constrain=constrain)\n                if directory:\n                    plot_distance_point_to_segment_latlon(loc, loc_a, loc_b, pi1,\n                                                          f\"point_to_segment_{loc_idx}_{seg_idx}_{constrain}.png\")\n                assert dist1 == pytest.approx(dist2), \\\n                    f\"Locs[{loc_idx},{seg_idx},{constrain}]: Distances different, {dist1} != {dist2}\"\n                assert pi1[0] == pytest.approx(pi2[0]), \\\n                    f\"Locs[{loc_idx},{seg_idx},{constrain}]: y coord different, {pi1[0]} != {pi2[0]}\"\n                assert pi1[1] == pytest.approx(pi2[1]), \\\n                    f\"Locs[{loc_idx},{seg_idx},{constrain}]: y coord different, {pi1[1]} != {pi2[1]}\"\n\n\ndef plot_distance_point_to_segment_latlon(f, t1, t2, pt, fn):\n    import smopy\n    import matplotlib.pyplot as plt\n    lat_min = min(f[0], t1[0], t2[0])\n    lat_max = max(f[0], t1[0], t2[0])\n    lon_min = min(f[1], t1[1], t2[1])\n    lon_max = max(f[1], t1[1], t2[1])\n    bb = [lat_min, lon_min, lat_max, lon_max]\n    m = smopy.Map(bb)\n    ax = m.show_mpl(figsize=(10, 10))\n    p1 = m.to_pixels(t1)\n    p2 = m.to_pixels(t2)\n    p3 = m.to_pixels(f)\n    p4 = m.to_pixels(pt)\n    ax.plot([p1[0], p2[0]], [p1[1], p2[1]], 'o-', color=\"black\")\n    ax.plot([p3[0]], [p3[1]], 'o-', color=\"black\")\n    ax.plot([p3[0], p4[0]], [p3[1], p4[1]], '--', color=\"red\")\n    plt.savefig(str(directory / fn))\n    plt.close(plt.gcf())\n\n\ndef plot_distance_segment_to_segment_latlon(f1, f2, t1, t2, pf, pt, fn):\n    import smopy\n    import matplotlib.pyplot as plt\n    lat_min = min(f1[0], f2[0], t1[0], t2[0])\n    lat_max = max(f1[0], f2[0], t1[0], t2[0])\n    lon_min = min(f1[1], f2[1], t1[1], t2[1])\n    lon_max = max(f1[1], f2[1], t1[1], t2[1])\n    bb = [lat_min, lon_min, lat_max, lon_max]\n    m = smopy.Map(bb)\n    ax = m.show_mpl(figsize=(10, 10))\n    p1 = m.to_pixels(f1)\n    p2 = m.to_pixels(f2)\n    p3 = m.to_pixels(t1)\n    p4 = m.to_pixels(t2)\n    p5 = m.to_pixels(pf)\n    p6 = m.to_pixels(pt)\n    ax.plot([p1[0], p2[0]], [p1[1], p2[1]], 'o-')\n    ax.plot([p3[0], p4[0]], [p3[1], p4[1]], 'o-')\n    ax.plot([p5[0], p6[0]], [p5[1], p6[1]], 'x-')\n    plt.savefig(str(directory / fn))\n    plt.close(plt.gcf())\n\n\ndef plot_distance_segment_to_segment_euc(f1, f2, t1, t2, pf, pt, fn):\n    import matplotlib.pyplot as plt\n    fig, ax = plt.subplots(1, 1, figsize=(10, 10))\n    ax.plot([f1[1], f2[1]], [f1[0], f2[0]], 'o-')\n    ax.plot([t1[1], t2[1]], [t1[0], t2[0]], 'o-')\n    ax.plot([pf[1], pt[1]], [pf[0], pt[0]], 'x-')\n    ax.axis('equal')\n    ax.set_aspect('equal')\n    plt.savefig(str(directory / fn))\n    plt.close(plt.gcf())\n\n\nif __name__ == \"__main__\":\n    # mm.matching.logger.setLevel(logging.INFO)\n    mm.logger.setLevel(logging.DEBUG)\n    mm.logger.addHandler(logging.StreamHandler(sys.stdout))\n    directory = Path(os.environ.get('TESTDIR', Path(__file__).parent))\n    print(f\"Saving files to {directory}\")\n    # test_path_to_gpx()\n    test_grs80()\n    # test_distance1()\n    # test_bearing1()\n    # test_destination1()\n    # test_distance_segment_to_segment1()\n    # test_distance_segment_to_segment2()\n    # test_distance_segment_to_segment3()\n    # test_distance_segment_to_segment4()\n    # test_distance_point_to_segment1()\n"
  },
  {
    "path": "tests/test_examples.py",
    "content": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_examples\n~~~~~~~~~~~~~~~~~~~\n\nRun standalone python files that are a complete examples.\nUsed to test the full examples in the documentation.\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2022 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport sys\nimport os\nimport logging\nfrom pathlib import Path\nimport subprocess as sp\nimport pytest\nimport leuvenmapmatching as mm\n\n\nlogger = mm.logger\nexamples_path = Path(os.path.realpath(__file__)).parent / \"examples\"\n\n\ndef test_examples():\n    example_fns = examples_path.glob(\"*.py\")\n    for example_fn in example_fns:\n        execute_file(example_fn.name)\n\n\ndef importrun_file(fn, cmp_with_previous=False):\n    import importlib\n    fn = f\"examples.{fn[:-3]}\"\n    print(f\"Importing: {fn}\")\n    o = importlib.import_module(fn)\n    o.run()\n\n\ndef execute_file(fn, cmp_with_previous=False):\n    print(f\"Testing: {fn}\")\n    fn = examples_path / fn\n    assert fn.exists()\n    try:\n        cmd = sp.run([\"python3\", fn], capture_output=True, check=True)\n    except sp.CalledProcessError as exc:\n        print(exc)\n        print(exc.stderr.decode())\n        print(exc.stdout.decode())\n        raise exc\n\n    if cmp_with_previous:\n        # Not ready to be used in general testing, output contains floats\n        result_data = cmd.stdout.decode()\n        correct_fn = fn.with_suffix(\".log\")\n        if correct_fn.exists():\n            with correct_fn.open(\"r\") as correct_fp:\n                correct_data = correct_fp.read()\n            print(correct_data)\n            print(result_data)\n            assert correct_data == result_data, f\"Logged output different for {fn}\"\n        else:\n            with correct_fn.open(\"w\") as correct_fp:\n                correct_fp.write(result_data)\n\n\nif __name__ == \"__main__\":\n    logger.setLevel(logging.WARNING)\n    logger.addHandler(logging.StreamHandler(sys.stdout))\n    directory = Path(os.environ.get('TESTDIR', Path(__file__).parent))\n    print(f\"Saving files to {directory}\")\n    # execute_file(\"example_1_simple.py\", cmp_with_previous=True)\n    execute_file(\"example_using_osmnx_and_geopandas.py\", cmp_with_previous=True)\n    # importrun_file(\"example_using_osmnx_and_geopandas.py\", cmp_with_previous=True)\n"
  },
  {
    "path": "tests/test_newsonkrumm2009.py",
    "content": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_path_newsonkrumm2009\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBased on the data available at:\nhttps://www.microsoft.com/en-us/research/publication/hidden-markov-map-matching-noise-sparseness/\n\nNotes:\n\n* There is a 'bug' in the map available from the website.\n  Multiple segments (streets) in the map are not connected but have overlapping, but\n  disconnected, nodes.\n  For example, the following nodes are on the same location and\n  should be connected because the given path runs over this road:\n  - 884147801204 and 884148400033\n  - 884148100260 and 884148001002\n* The path is missing a number of observations. For those parts non-emitting nodes are required.\n  This occurs at:\n  - 2770:2800 (index 2659 is start)\n  - 2910:2929\n\n:author: Wannes Meert\n:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport os\nimport sys\nimport logging\nimport pickle\nfrom pathlib import Path\nimport csv\nfrom datetime import datetime\nfrom itertools import product\nimport pytest\nimport leuvenmapmatching as mm\nfrom leuvenmapmatching.matcher import base\nfrom leuvenmapmatching.matcher.distance import DistanceMatcher\nfrom leuvenmapmatching.map.sqlite import SqliteMap\nimport leuvenmapmatching.visualization as mm_viz\nMYPY = False\nif MYPY:\n    from typing import List, Tuple\n\n\nlogger = mm.logger\nthis_path = Path(os.path.realpath(__file__)).parent / \"rsrc\" / \"newson_krumm_2009\"\ngps_data = this_path / \"gps_data.txt\"\ngps_data_pkl = gps_data.with_suffix(\".pkl\")\nground_truth_route = this_path / \"ground_truth_route.txt\"\nroad_network = this_path / \"road_network.txt\"\nroad_network_zip = this_path / \"road_network.zip\"\nroad_network_db = road_network.with_suffix(\".sqlite\")\n\ndirectory = None\nbase.default_label_width = 34\n\n\ndef read_gps(route_fn):\n    route = []\n    with route_fn.open(\"r\") as route_f:\n        reader = csv.reader(route_f, delimiter='\\t')\n        next(reader)\n        for row in reader:\n            date, time, lat, lon = row[:4]\n            date_str = date + \" \" + time\n            ts = datetime.strptime(date_str, '%d-%b-%Y %H:%M:%S')\n            lat = float(lat)\n            lon = float(lon)\n            route.append((lat, lon, ts))\n    logger.debug(f\"Read GPS trace of {len(route)} points\")\n    return route\n\n\ndef read_paths(paths_fn):\n    paths = []\n    with paths_fn.open(\"r\") as paths_f:\n        reader = csv.reader(paths_f, delimiter='\\t')\n        next(reader)\n        for row in reader:\n            pathid, trav = row[:2]\n            pathid = int(pathid)\n            trav = int(trav)\n            paths.append((pathid, trav))\n    logger.debug(f\"Read correct trace of {len(paths)} nodes\")\n    return paths\n\n\ndef parse_linestring(line):\n    # type: (str) -> List[Tuple[float, float]]\n    line = line[line.index(\"(\") + 1:line.index(\")\")]\n    latlons = []\n    for lonlat in line.split(\", \"):\n        lon, lat = lonlat.split(\" \")\n        latlons.append((float(lat), float(lon)))\n    return latlons\n\n\ndef read_map(map_fn):\n    logger.debug(f\"Reading map ...\")\n    mmap = SqliteMap(\"road_network\", use_latlon=True, dir=this_path)\n    node_cnt = 0\n    edge_cnt = 0\n    # new_node_id = 1000000000000\n    new_node_id = 1\n    with map_fn.open(\"r\") as map_f:\n        reader = csv.reader(map_f, delimiter='\\t')\n        next(reader)\n        for row in reader:\n            eid, nf, nt, twoway, speed, length, innernodes = row\n            eid = int(eid)\n            nf = int(nf)\n            nt = int(nt)\n            length = int(length)\n            twoway = int(twoway)\n            speed = float(speed)\n            if twoway == 0:\n                twoway = False\n            elif twoway == 1:\n                twoway = True\n            else:\n                raise Exception(f\"Unknown value for twoway: {twoway}\")\n            innernodes = parse_linestring(innernodes)\n            # Add nodes to map\n            mmap.add_node(nf, innernodes[0], ignore_doubles=True, no_index=True, no_commit=True)\n            mmap.add_node(nt, innernodes[-1], ignore_doubles=True, no_index=True, no_commit=True)\n            node_cnt += 2\n            prev_node = nf\n            assert(length < 1000)\n            idx = 1\n            for innernode in innernodes[1:-1]:\n                # innernode_id = nf * 1000 + idx\n                innernode_id = new_node_id\n                new_node_id += 1\n                mmap.add_node(innernode_id, innernode, no_index=True, no_commit=True)  # Should not be double\n                node_cnt += 1\n                mmap.add_edge(prev_node, innernode_id, speed=speed, edge_type=0,\n                              path=eid, pathnum=idx, no_index=True, no_commit=True)\n                edge_cnt += 1\n                if twoway:\n                    mmap.add_edge(innernode_id, prev_node, speed=speed, edge_type=0,\n                                  path=eid, pathnum=-idx, no_index=True, no_commit=True)\n                    edge_cnt += 1\n                prev_node = innernode_id\n                idx += 1\n            mmap.add_edge(prev_node, nt, speed=speed, edge_type=0,\n                          path=eid, pathnum=idx, no_index=True, no_commit=True)\n            edge_cnt += 1\n            if twoway:\n                mmap.add_edge(nt, prev_node, speed=speed, edge_type=0,\n                              path=eid, pathnum=-idx, no_index=True, no_commit=True)\n                edge_cnt += 1\n            if node_cnt % 100000 == 0:\n                mmap.db.commit()\n    logger.debug(f\"... done: {node_cnt} nodes and {edge_cnt} edges\")\n    mmap.reindex_nodes()\n    mmap.reindex_edges()\n    assert(new_node_id < 100000000000)\n    return mmap\n\n\ndef correct_map(mmap):\n    \"\"\"Add edges between nodes with degree > 2 that are on the exact same location.\n    This ignore that with bridges, the roads might not be connected. But we need a correct\n    because the dataset has a number of interrupted paths.\n    \"\"\"\n    def correct_edge(labels):\n        labels = [label for label in labels if label > 100000000000]\n        logger.info(f\"Add connections between {labels}\")\n        for l1, l2 in product(labels, repeat=2):\n            mmap.add_edge(l1, l2, edge_type=1)\n    mmap.find_duplicates(func=correct_edge)\n\n\ndef load_data():\n    max_route_length = None  # 200\n\n    # Paths\n    if not ground_truth_route.exists():\n        import requests\n        url = f'https://www.microsoft.com/en-us/research/uploads/prod/2017/07/ground_truth_route.txt'\n        logger.debug(\"Download gound_truth_route.txt from microsoft.com\")\n        r = requests.get(url, stream=True)\n        with ground_truth_route.open('wb') as ofile:\n            for chunk in r.iter_content(chunk_size=1024):\n                if chunk:\n                    ofile.write(chunk)\n    paths = read_paths(ground_truth_route)\n\n    # Map\n    if road_network_db.exists():\n        map_con = SqliteMap.from_file(road_network_db)\n        logger.debug(f\"Read road network from db file {road_network_db} ({map_con.size()} nodes)\")\n    else:\n        if not road_network.exists():\n            import requests\n            url = f'https://www.microsoft.com/en-us/research/uploads/prod/2017/07/road_network.zip'\n            logger.debug(\"Download road_network.zip from microsoft.com\")\n            r = requests.get(url, stream=True)\n            with road_network_zip.open('wb') as ofile:\n                for chunk in r.iter_content(chunk_size=1024):\n                    if chunk:\n                        ofile.write(chunk)\n            import zipfile\n            logger.debug(\"Unzipping road_network.zip\")\n            with zipfile.ZipFile(str(road_network_zip), \"r\") as zip_ref:\n                zip_ref.extractall(str(road_network_zip.parent))\n        map_con = read_map(road_network)\n        correct_map(map_con)\n        logger.debug(f\"Create road network to db file {map_con.db_fn} ({map_con.size()} nodes)\")\n\n    # Route\n    if gps_data_pkl.exists():\n        with gps_data_pkl.open(\"rb\") as ifile:\n            route = pickle.load(ifile)\n        logger.debug(f\"Read gps route from file ({len(route)} points)\")\n    else:\n        if not gps_data.exists():\n            import requests\n            url = f'https://www.microsoft.com/en-us/research/uploads/prod/2017/07/gps_data.txt'\n            logger.debug(\"Download gps_data.txt from microsoft.com\")\n            r = requests.get(url, stream=True)\n            with gps_data.open('wb') as ofile:\n                for chunk in r.iter_content(chunk_size=1024):\n                    if chunk:\n                        ofile.write(chunk)\n        route = read_gps(gps_data)\n        if max_route_length:\n            route = route[:max_route_length]\n        with gps_data_pkl.open(\"wb\") as ofile:\n            pickle.dump(route, ofile)\n    route = [(lat, lon) for lat, lon, _ in route]\n\n    return paths, map_con, route\n\n\ndef test_route_slice1():\n    if directory:\n        import matplotlib.pyplot as plt\n    nodes, map_con, route = load_data()\n    zoom_path = True\n\n    matcher = DistanceMatcher(map_con, min_prob_norm=0.001,\n                              max_dist=200,\n                              dist_noise=6, dist_noise_ne=12,\n                              obs_noise=30, obs_noise_ne=150,\n                              non_emitting_states=True)\n    route_slice = route[2657:2662]\n    matcher.match(route_slice)\n    path_pred = matcher.path_pred_onlynodes\n    path_sol = [172815, 172816, 172817, 172818, 172819, 172820, 172821, 172822, 172823, 172824,\n                172825, 172826, 172827, 172828, 172829, 172830, 884148100261, 172835, 172836,\n                172837, 884148100254, 172806, 884148100255, 172807]  # Can change when building db\n    assert len(path_pred) == len(path_sol)\n\n\ndef test_bug1():\n    map_con = SqliteMap(\"map\", use_latlon=True)\n    map_con.add_nodes([\n        (1, (47.590439915657, -122.238368690014)),\n        (2, (47.5910192728043, -122.239519357681)),\n        (3, (47.5913706421852, -122.240168452263))\n    ])\n    map_con.add_edges([\n        (1, 2),\n        (2, 3)\n    ])\n    path = [\n        # (47.59043333, -122.2384167),\n        (47.59058333, -122.2387),\n        (47.59071667, -122.2389833),\n        (47.59086667, -122.2392667),\n        (47.59101667, -122.23955),\n        (47.59115,    -122.2398333)\n    ]\n    path_sol = [(1, 2), (2, 3)]\n    matcher = DistanceMatcher(map_con, min_prob_norm=0.001,\n                              max_dist=200, obs_noise=4.07,\n                              non_emitting_states=True)\n    matcher.match(path, unique=True)\n    path_pred = matcher.path_pred\n    if directory:\n        import matplotlib.pyplot as plt\n        matcher.print_lattice_stats()\n        logger.debug(\"Plotting post map ...\")\n        fig = plt.figure(figsize=(100, 100))\n        ax = fig.get_axes()\n        mm_viz.plot_map(map_con, matcher=matcher, use_osm=True, ax=ax,\n                        show_lattice=False, show_labels=True, show_graph=True, zoom_path=True,\n                        show_matching=True)\n        plt.savefig(str(directory / \"test_newson_bug1.png\"))\n        plt.close(fig)\n        logger.debug(\"... done\")\n    assert path_pred == path_sol, f\"Edges not equal:\\n{path_pred}\\n{path_sol}\"\n\n\n@pytest.mark.skip(reason=\"Takes a long time\")\ndef test_route():\n    if directory:\n        import matplotlib.pyplot as plt\n    else:\n        plt = None\n    paths, map_con, route = load_data()\n    route = [(lat, lon) for lat, lon, _ in route]\n    zoom_path = True\n    # zoom_path = slice(2645, 2665)\n    slice_route = None\n    # slice_route = slice(650, 750)\n    # slice_route = slice(2657, 2662)  # First location where some observations are missing\n    # slice_route = slice(2770, 2800)  # Observations are missing\n    # slice_route = slice(2910, 2950)  # Interesting point\n    # slice_route = slice(2910, 2929)  # Interesting point\n    # slice_route = slice(6825, 6833)  # Outlier observation\n\n    # if directory is not None:\n    #     logger.debug(\"Plotting pre map ...\")\n    #     mm_viz.plot_map(map_con_latlon, path=route_latlon, use_osm=True,\n    #                     show_lattice=False, show_labels=False, show_graph=False, zoom_path=zoom_path,\n    #                     filename=str(directory / \"test_newson_route.png\"))\n    #     logger.debug(\"... done\")\n\n    matcher = DistanceMatcher(map_con, min_prob_norm=0.0001,\n                              max_dist=200,\n                              dist_noise=15, dist_noise_ne=30,\n                              obs_noise=30, obs_noise_ne=150,\n                              non_emitting_states=True)\n\n    if slice_route is None:\n        pkl_fn = this_path / \"nodes_pred.pkl\"\n        if pkl_fn.exists():\n            with pkl_fn.open(\"rb\") as pkl_file:\n                logger.debug(f\"Reading predicted nodes from pkl file\")\n                route_nodes = pickle.load(pkl_file)\n        else:\n            matcher.match(route)\n            route_nodes = matcher.path_pred_onlynodes\n            with pkl_fn.open(\"wb\") as pkl_file:\n                pickle.dump(route_nodes, pkl_file)\n        from leuvenmapmatching.util.evaluation import route_mismatch_factor\n        print(route_nodes[:10])\n        # route_edges = map_con.nodes_to_paths(route_nodes)\n        # print(route_edges[:10])\n        grnd_paths, _ = zip(*paths)\n        print(grnd_paths[:10])\n        route_paths = map_con.nodes_to_paths(route_nodes)\n        print(route_paths[:10])\n\n        logger.debug(f\"Compute route mismatch factor\")\n        factor, cnt_matches, cnt_mismatches, total_length, mismatches = \\\n            route_mismatch_factor(map_con, route_paths, grnd_paths,window=None, keep_mismatches=True)\n        logger.debug(f\"factor = {factor}, \"\n                     f\"cnt_matches = {cnt_matches}/{cnt_mismatches} of {len(grnd_paths)}/{len(route_paths)}, \"\n                     f\"total_length = {total_length}\\n\"\n                     f\"mismatches = \" + \" | \".join(str(v) for v in mismatches))\n    else:\n        _, last_idx = matcher.match(route[slice_route])\n        logger.debug(f\"Last index = {last_idx}\")\n\n    # matcher.match(route[2657:2662])  # First location where some observations are missing\n    # matcher.match(route[2770:2800])  # Observations are missing\n    # matcher.match(route[2910:2950])  # Interesting point\n    # matcher.match(route[2910:2929])  # Interesting point\n    # matcher.match(route[6000:])\n    path_pred = matcher.path_pred_onlynodes\n\n    if directory:\n        matcher.print_lattice_stats()\n        logger.debug(\"Plotting post map ...\")\n        fig = plt.figure(figsize=(200, 200))\n        ax = fig.get_axes()\n        mm_viz.plot_map(map_con, matcher=matcher, use_osm=True, ax=ax,\n                        show_lattice=False, show_labels=True, zoom_path=zoom_path,\n                        show_matching=True, show_graph=False)\n        plt.savefig(str(directory / \"test_newson_route_matched.png\"))\n        plt.close(fig)\n        logger.debug(\"... done\")\n        logger.debug(\"Best path:\")\n        for m in matcher.lattice_best:\n            logger.debug(m)\n\n    print(path_pred)\n\n\n@pytest.mark.skip(reason=\"Takes a too long\")\ndef test_bug2():\n    from leuvenmapmatching.util.openstreetmap import locations_to_map\n    map_con = SqliteMap(\"map\", use_latlon=True, dir=directory)\n    path = [\n        (50.87205, 4.66089), (50.874550000000006, 4.672980000000001), (50.87538000000001, 4.67698),\n        (50.875800000000005, 4.6787600000000005), (50.876520000000006, 4.6818), (50.87688000000001, 4.683280000000001),\n        (50.87814, 4.68733), (50.87832, 4.68778), (50.87879, 4.68851), (50.87903000000001, 4.68895),\n        (50.879560000000005, 4.689170000000001), (50.87946, 4.6900900000000005),\n        (50.879290000000005, 4.6909600000000005), (50.87906, 4.6921800000000005), (50.87935, 4.6924),\n        (50.879720000000006, 4.69275), (50.88002, 4.6930700000000005), (50.880430000000004, 4.693440000000001),\n        (50.880660000000006, 4.69357), (50.880660000000006, 4.6936100000000005), (50.88058, 4.694640000000001),\n        (50.88055000000001, 4.69491), (50.88036, 4.696160000000001), (50.88009, 4.697550000000001),\n        (50.87986, 4.6982800000000005), (50.879720000000006, 4.698790000000001), (50.87948, 4.699730000000001),\n        (50.87914000000001, 4.6996400000000005), (50.87894000000001, 4.6995000000000005),\n        (50.878800000000005, 4.699350000000001), (50.8785, 4.6991000000000005), (50.87841, 4.6990300000000005)\n    ]\n    locations_to_map(path, map_con, filename=directory / \"osm.xml\")\n    path_sol = [(5777282112, 2633552218), (2633552218, 5777282111), (5777282111, 5777282110), (5777282110, 1642021707),\n                (1642021707, 71361087), (71361087, 71364203), (71364203, 1151697757), (1151697757, 1647339017),\n                (1647339017, 1647339030), (1647339030, 2058510349), (2058510349, 2633552212), (2633552212, 1380538577),\n                (1380538577, 1439572271), (1439572271, 836434313), (836434313, 2633771041), (2633771041, 5042874484),\n                (5042874484, 5042874485), (5042874485, 2518922583), (2518922583, 2659762546), (2659762546, 5777282063),\n                (5777282063, 2633771037), (2633771037, 2633771035), (2633771035, 2633771033), (2633771033, 1151668705),\n                (1151668705, 2633771094), (2633771094, 1151668722), (1151668722, 1151668724), (1151668724, 5543948222),\n                (5543948222, 2058481517), (2058481517, 16933576), (16933576, 5543948221), (5543948221, 2518923620),\n                (2518923620, 5543948020), (5543948020, 5543948019), (5543948019, 18635886), (18635886, 18635887),\n                (18635887, 1036909153), (1036909153, 2658942230), (2658942230, 1001099975), (1001099975, 16933574),\n                (16933574, 1125604152), (1125604152, 5543948238), (5543948238, 1125604150), (1125604150, 1125604148),\n                (1125604148, 2634195334), (2634195334, 2087854243), (2087854243, 5543948237), (5543948237, 160226603),\n                (160226603, 180130266), (180130266, 5543948227), (5543948227, 5543948226), (5543948226, 1195681902),\n                (1195681902, 101135392), (101135392, 2606704673), (2606704673, 18635977), (18635977, 1026111708),\n                (1026111708, 1026111631), (1026111631, 16571375), (16571375, 2000680621), (2000680621, 999580042),\n                (999580042, 16571370), (16571370, 2000680620), (2000680620, 5078692402), (5078692402, 5543948008),\n                (5543948008, 16571371), (16571371, 999579936), (999579936, 2639836143), (2639836143, 5543948014),\n                (5543948014, 5222992316), (5222992316, 30251323), (30251323, 159701080), (159701080, 3173217124),\n                (3173217124, 1165209673), (1165209673, 1380538689), (1380538689, 2878334668), (2878334668, 2871137399),\n                (2871137399, 2876902981), (2876902981, 2873624508), (2873624508, 2873624509), (2873624509, 2899666507),\n                (2899666507, 2899666518), (2899666518, 2899666513), (2899666513, 2903073945), (2903073945, 2903073951),\n                (2903073951, 1380538681), (1380538681, 2914810627), (2914810627, 2914810618), (2914810618, 2914810607),\n                (2914810607, 2914810604), (2914810604, 2914810483), (2914810483, 2914810462), (2914810462, 2914810464),\n                (2914810464, 1312433523), (1312433523, 20918594), (20918594, 2634267817), (2634267817, 2967425445),\n                (2967425445, 3201523879), (3201523879, 157217466), (157217466, 2963305939), (2963305939, 3201523877),\n                (3201523877, 3889275909), (3889275909, 3889275897), (3889275897, 157255077), (157255077, 30251882),\n                (30251882, 157245624), (157245624, 1150903673), (1150903673, 4504936404)]\n    matcher = DistanceMatcher(map_con, min_prob_norm=0.001,\n                              max_dist=200, obs_noise=4.07,\n                              non_emitting_states=True)\n    nodes, idx = matcher.match(path, unique=True)\n    path_pred = matcher.path_pred\n    if directory:\n        import matplotlib.pyplot as plt\n        matcher.print_lattice_stats()\n        logger.debug(\"Plotting post map ...\")\n        fig = plt.figure(figsize=(100, 100))\n        ax = fig.get_axes()\n        mm_viz.plot_map(map_con, matcher=matcher, use_osm=True, ax=ax,\n                        show_lattice=False, show_labels=True, show_graph=False, zoom_path=True,\n                        show_matching=True)\n        plt.savefig(str(directory / \"test_newson_bug1.png\"))\n        plt.close(fig)\n        logger.debug(\"... done\")\n    assert path_pred == path_sol, f\"Edges not equal:\\n{path_pred}\\n{path_sol}\"\n\n\nif __name__ == \"__main__\":\n    logger.setLevel(logging.DEBUG)\n    logger.addHandler(logging.StreamHandler(sys.stdout))\n    directory = Path(os.environ.get('TESTDIR', Path(__file__).parent))\n    print(f\"Saving files to {directory}\")\n    # test_route()\n    test_route_slice1()\n    # test_bug1()\n    # test_bug2()\n"
  },
  {
    "path": "tests/test_nonemitting.py",
    "content": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_nonemitting\n~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2017-2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport sys\nimport os\nimport logging\nfrom pathlib import Path\n\ntry:\n    import leuvenmapmatching as mm\nexcept ImportError:\n    sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)))\n    import leuvenmapmatching as mm\nfrom leuvenmapmatching.matcher.distance import DistanceMatcher, DistanceMatching\nfrom leuvenmapmatching.matcher.simple import SimpleMatcher\nfrom leuvenmapmatching.map.inmem import InMemMap\n\nMYPY = False\nif MYPY:\n    from typing import Tuple\n\n\nlogger = mm.logger\ndirectory = None\n\n\ndef setup_map():\n    path1 = [(1.8, 0.1), (1.8, 3.5), (3.0, 4.9)]  # More nodes than observations\n    path2 = [(1.8, 0.1), (1.8, 2.0), (1.8, 3.5), (3.0, 4.9)]\n    path_sol = ['X', 'C', 'D', 'F']\n    mapdb = InMemMap(\"map\", graph={\n        \"A\": ((1, 1), [\"B\", \"C\", \"X\"]),\n        \"B\": ((1, 3), [\"A\", \"C\", \"D\", \"K\"]),\n        \"C\": ((2, 2), [\"A\", \"B\", \"D\", \"E\", \"X\", \"Y\"]),\n        \"D\": ((2, 4), [\"B\", \"C\", \"E\", \"K\", \"L\", \"F\"]),\n        \"E\": ((3, 3), [\"C\", \"D\", \"F\", \"Y\"]),\n        \"F\": ((3, 5), [\"D\", \"E\", \"L\"]),\n        \"X\": ((2, 0), [\"A\", \"C\", \"Y\"]),\n        \"Y\": ((3, 1), [\"X\", \"C\", \"E\"]),\n        \"K\": ((1, 5), [\"B\", \"D\", \"L\"]),\n        \"L\": ((2, 6), [\"K\", \"D\", \"F\"])\n    }, use_latlon=False)\n    return mapdb, path1, path2, path_sol\n\n\ndef visualize_map(pathnb=1):\n    mapdb, path1, path2, path_sol = setup_map()\n    import leuvenmapmatching.visualization as mm_vis\n    if pathnb == 2:\n        path = path2\n    else:\n        path = path1\n    mm_vis.plot_map(mapdb, path=path, show_labels=True,\n                    filename=(directory / \"test_nonemitting_map.png\"))\n\n\ndef test_path1():\n    mapdb, path1, path2, path_sol = setup_map()\n\n    matcher = SimpleMatcher(mapdb, max_dist_init=1,\n                                  min_prob_norm=0.5,\n                                  obs_noise=0.5,\n                                  non_emitting_states=True, only_edges=False)\n    matcher.match(path1, unique=True)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        with (directory / 'lattice_path1.gv').open('w') as ofile:\n            matcher.lattice_dot(file=ofile)\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       show_graph=True,\n                       filename=str(directory / \"test_nonemitting_test_path1.png\"))\n    assert path_pred == path_sol, f\"Nodes not equal:\\n{path_pred}\\n{path_sol}\"\n\n\ndef test_path1_inc():\n    mapdb, path1, path2, path_sol = setup_map()\n\n    matcher = SimpleMatcher(mapdb, max_dist_init=1,\n                            in_prob_norm=0.5, obs_noise=0.5,\n                            non_emitting_states=True, only_edges=False,\n                            max_lattice_width=1)\n\n    print('## PHASE 1 ##')\n    matcher.match(path1, unique=True)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        with (directory / 'lattice_path1_inc1.gv').open('w') as ofile:\n            matcher.lattice_dot(file=ofile, precision=2, render=True)\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       show_graph=True,\n                       filename=str(directory / \"test_nonemitting_test_path1_inc1.png\"))\n\n    print('## PHASE 2 ##')\n    matcher.increase_max_lattice_width(3, unique=True)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        with (directory / 'lattice_path1_inc2.gv').open('w') as ofile:\n            matcher.lattice_dot(file=ofile, precision=2, render=True)\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       show_graph=True,\n                       filename=str(directory / \"test_nonemitting_test_path1_inc2.png\"))\n\n\n    assert path_pred == path_sol, f\"Nodes not equal:\\n{path_pred}\\n{path_sol}\"\n\n\ndef test_path1_dist():\n    mapdb, path1, path2, path_sol = setup_map()\n\n    matcher = DistanceMatcher(mapdb, max_dist_init=1,\n                              min_prob_norm=0.5,\n                              obs_noise=0.5,\n                              non_emitting_states=True, only_edges=True)\n    matcher.match(path1, unique=True)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        print(\"LATTICE BEST\")\n        for m in matcher.lattice_best:\n            print(m)\n        with (directory / 'lattice_path1.gv').open('w') as ofile:\n            matcher.lattice_dot(file=ofile)\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, show_graph=True,\n                       filename=str(directory / \"test_nonemitting_test_path1_dist.png\"))\n    assert path_pred == path_sol, f\"Nodes not equal:\\n{path_pred}\\n{path_sol}\"\n\n\ndef test_path2():\n    mapdb, path1, path2, path_sol = setup_map()\n\n    matcher = SimpleMatcher(mapdb, max_dist_init=1, min_prob_norm=0.5, obs_noise=0.5,\n                                  non_emitting_states=True, only_edges=False)\n    matcher.match(path2, unique=True)\n    path_pred = matcher.path_pred_onlynodes\n\n    dists = matcher.path_all_distances()\n\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        with (directory / 'lattice_path2.gv').open('w') as ofile:\n            matcher.lattice_dot(file=ofile)\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       filename=str(directory / \"test_nonemitting_test_path2.png\"))\n    assert path_pred == path_sol, \"Nodes not equal:\\n{}\\n{}\".format(path_pred, path_sol)\n\n\ndef test_path2_dist():\n    mapdb, path1, path2, path_sol = setup_map()\n\n    matcher = DistanceMatcher(mapdb, max_dist_init=1, min_prob_norm=0.5,\n                              obs_noise=0.5, dist_noise=0.5,\n                              non_emitting_states=True)\n    matcher.match(path2, unique=True)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        # with (directory / 'lattice_path2.gv').open('w') as ofile:\n        #     matcher.lattice_dot(file=ofile)\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       filename=str(directory / \"test_nonemitting_test_path2_dist.png\"))\n    assert path_pred == path_sol, \"Nodes not equal:\\n{}\\n{}\".format(path_pred, path_sol)\n\n\ndef test_path2_incremental():\n    mapdb, path1, path2, path_sol = setup_map()\n\n    matcher = SimpleMatcher(mapdb, max_dist_init=1, min_prob_norm=0.5, obs_noise=0.5,\n                                  non_emitting_states=True, only_edges=False)\n    matcher.match(path2[:2])\n    path_pred_1 = matcher.path_pred_onlynodes\n    matcher.match(path2, expand=True)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        with (directory / 'lattice_path2.gv').open('w') as ofile:\n            matcher.lattice_dot(file=ofile)\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       filename=str(directory / \"test_nonemitting_test_path2.png\"))\n    assert path_pred_1 == path_sol[:2], \"Nodes not equal:\\n{}\\n{}\".format(path_pred, path_sol)\n    assert path_pred == path_sol, \"Nodes not equal:\\n{}\\n{}\".format(path_pred, path_sol)\n\n\ndef test_path_duplicate():\n    from datetime import datetime\n    # A path with two identical points\n    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),\n            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),\n            (2.1, 3.3), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2), (3.1, 3.8),\n            (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]\n\n    mapdb = InMemMap(\"map\", graph={\n        \"A\": ((1, 1), [\"B\", \"C\"]),\n        \"B\": ((1, 3), [\"A\", \"C\", \"D\"]),\n        \"C\": ((2, 2), [\"A\", \"B\", \"D\", \"E\"]),\n        \"D\": ((2, 4), [\"B\", \"C\", \"D\", \"E\"]),\n        \"E\": ((3, 3), [\"C\", \"D\", \"F\"]),\n        \"F\": ((3, 5), [\"D\", \"E\"])\n    }, use_latlon=False)\n\n    matcher = SimpleMatcher(mapdb, max_dist=None, min_prob_norm=None,\n                                  non_emitting_states = True, only_edges=False)\n\n    #Matching with and without timestamps signed to the points\n    path_pred = matcher.match(path, unique=False)\n\n    path = [(p1, p2, datetime.fromtimestamp(i)) for i, (p1, p2) in enumerate(path)]\n    path_pred_time = matcher.match(path, unique=False)\n\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       filename=str(directory / \"test_nonemitting_test_path_duplicate.png\"))\n\n    # The path should be identical regardless of the timestamps\n    assert path_pred == path_pred_time, f\"Nodes not equal:\\n{path_pred}\\n{path_pred_time}\"\n\n\ndef test_path3_many_obs():\n    path = [(1, 0), (3, -0.1), (3.7, 0.6), (4.5, 0.7),\n            (5.5, 1.2), (6.5, 0.88), (7.5, 0.65), (8.5, -0.1),\n            (9.8, 0.1),(10.1, 1.9)]\n    path_sol = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']\n    mapdb = InMemMap(\"map\", graph={\n        \"A\": ((1, 0.00), [\"B\"]),\n        \"B\": ((3, 0.00), [\"A\", \"C\"]),\n        \"C\": ((4, 0.70), [\"B\", \"D\"]),\n        \"D\": ((5, 1.00), [\"C\", \"E\"]),\n        \"E\": ((6, 1.00), [\"D\", \"F\"]),\n        \"F\": ((7, 0.70), [\"E\", \"G\"]),\n        \"G\": ((8, 0.00), [\"F\", \"H\"]),\n        \"H\": ((10, 0.0), [\"G\", \"I\"]),\n        \"I\": ((10, 2.0), [\"H\"])\n    }, use_latlon=False)\n    matcher = SimpleMatcher(mapdb, max_dist_init=0.2, obs_noise=1, obs_noise_ne=10,\n                                  non_emitting_states=True)\n    matcher.match(path)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, linewidth=10,\n                       show_graph=True, show_lattice=True,\n                       filename=str(directory / \"test_test_path_ne_3_mo.png\"))\n    assert path_pred == path_sol, f\"Nodes not equal:\\n{path_pred}\\n{path_sol}\"\n\n\ndef test_path3_few_obs_en():\n    path = [(1, 0), (7.5, 0.65), (10.1, 1.9)]\n    path_sol = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']\n    mapdb = InMemMap(\"map\", graph={\n        \"A\": ((1, 0.00), [\"B\"]),\n        \"B\": ((3, 0.00), [\"A\", \"C\"]),\n        \"C\": ((4, 0.70), [\"B\", \"D\"]),\n        \"D\": ((5, 1.00), [\"C\", \"E\"]),\n        \"E\": ((6, 1.00), [\"D\", \"F\"]),\n        \"F\": ((7, 0.70), [\"E\", \"G\"]),\n        \"G\": ((8, 0.00), [\"F\", \"H\"]),\n        \"H\": ((10, 0.0), [\"G\", \"I\"]),\n        \"I\": ((10, 2.0), [\"H\"])\n    }, use_latlon=False)\n    matcher = SimpleMatcher(mapdb, max_dist_init=0.2, obs_noise=1, obs_noise_ne=10,\n                                  non_emitting_states=True, only_edges=False)\n    matcher.match(path)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, linewidth=10,\n                       filename=str(directory / \"test_test_path_ne_3_fo.png\"))\n    assert path_pred == path_sol, f\"Nodes not equal:\\n{path_pred}\\n{path_sol}\"\n\n\ndef test_path3_few_obs_e():\n    path = [(1, 0), (7.5, 0.65), (10.1, 1.9)]\n    path_sol = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']\n    mapdb = InMemMap(\"map\", graph={\n        \"A\": ((1, 0.00), [\"B\"]),\n        \"B\": ((3, 0.00), [\"A\", \"C\"]),\n        \"C\": ((4, 0.70), [\"B\", \"D\"]),\n        \"D\": ((5, 1.00), [\"C\", \"E\"]),\n        \"E\": ((6, 1.00), [\"D\", \"F\"]),\n        \"F\": ((7, 0.70), [\"E\", \"G\"]),\n        \"G\": ((8, 0.00), [\"F\", \"H\"]),\n        \"H\": ((10, 0.0), [\"G\", \"I\"]),\n        \"I\": ((10, 2.0), [\"H\"])\n    }, use_latlon=False)\n    matcher = SimpleMatcher(mapdb, max_dist_init=0.2, obs_noise=1, obs_noise_ne=10,\n                                  non_emitting_states=True, only_edges=True)\n    matcher.match(path)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, linewidth=10,\n                       filename=str(directory / \"test_test_path_e_3_fo.png\"))\n    assert path_pred == path_sol, f\"Nodes not equal:\\n{path_pred}\\n{path_sol}\"\n\n\ndef test_path3_dist():\n    path = [(0, 1), (0.65, 7.5), (1.9, 10.1)]\n    path_sol = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']\n    mapdb = InMemMap(\"map\", graph={\n        \"A\": ((0.00, 1), [\"B\"]),\n        \"B\": ((0.00, 3), [\"A\", \"C\"]),\n        \"C\": ((0.70, 3), [\"B\", \"D\"]),\n        \"D\": ((1.00, 5), [\"C\", \"E\"]),\n        \"E\": ((1.00, 6), [\"D\", \"F\"]),\n        \"F\": ((0.70, 7), [\"E\", \"G\"]),\n        \"G\": ((0.00, 8), [\"F\", \"H\"]),\n        \"H\": ((0.0, 10), [\"G\", \"I\"]),\n        \"I\": ((2.0, 10), [\"H\"])\n    }, use_latlon=False)\n    matcher = DistanceMatcher(mapdb, max_dist_init=0.2,\n                              obs_noise=0.5, obs_noise_ne=2, dist_noise=0.5,\n                              non_emitting_states=True)\n    states, lastidx = matcher.match(path)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, linewidth=2,\n                       filename=str(directory / \"test_path_3_dist.png\"))\n    assert path_pred == path_sol, f\"Nodes not equal:\\n{path_pred}\\n{path_sol}\"\n\n    for obs_idx, m in enumerate(matcher.lattice_best):  # type: Tuple[int, DistanceMatching]\n        state = m.shortkey  # tuple indicating edge\n        ne_str = \"e\" if m.is_emitting() else \"ne\"  # state is emitting or not\n        p1_str = \"{:>5.2f}-{:<5.2f}\".format(*m.edge_m.pi)  # best matching location on graph\n        p2_str = \"{:>5.2f}-{:<5.2f}\".format(*m.edge_o.pi)  # best matching location on track\n        print(f\"{obs_idx:<2} | {state} | {ne_str:<2} | {p1_str} | {p2_str}\")\n\n\nif __name__ == \"__main__\":\n    # mm.matching.logger.setLevel(logging.INFO)\n    logger.setLevel(logging.DEBUG)\n    logger.addHandler(logging.StreamHandler(sys.stdout))\n    directory = Path(os.environ.get('TESTDIR', Path(__file__).parent))\n    print(f\"Saving files to {directory}\")\n    # visualize_map(pathnb=1)\n    # test_path1()\n    # test_path1_inc()\n    # test_path1_dist()\n    test_path2()\n    # test_path2_dist()\n    # test_path2_incremental()\n    # test_path_duplicate()\n    # test_path3_many_obs()\n    # test_path3_few_obs_en()\n    # test_path3_few_obs_e()\n    # test_path3_dist()\n"
  },
  {
    "path": "tests/test_nonemitting_circle.py",
    "content": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_nonemitting_circle\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport sys, os\nimport logging\nimport math\nfrom pathlib import Path\nimport numpy as np\n\ntry:\n    import leuvenmapmatching as mm\nexcept ImportError:\n    sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)))\n    import leuvenmapmatching as mm\nfrom leuvenmapmatching.matcher.simple import SimpleMatcher\nfrom leuvenmapmatching.matcher.distance import DistanceMatcher\nfrom leuvenmapmatching.map.inmem import InMemMap\n\n\ndirectory = None\n\n\ndef setup_map(disconnect=True):\n    theta = np.linspace(0, 2 * math.pi, 4 * 5 + 1)[:-1]\n\n    ox = 0.1 + np.cos(theta * 0.95)\n    oy = np.sin(theta * 1)\n    path1 = list(zip(ox, oy))  # all observations\n    path2 = [(x, y) for x, y in zip(ox, oy) if x > -0.60]\n\n    nx = np.cos(theta)\n    ny = np.sin(theta)\n    nl = [f\"N{i}\" for i in range(len(nx))]\n    graph = {}\n    for i, (x, y, l) in enumerate(zip(nx, ny, nl)):\n        if disconnect:\n            edges = []\n            if i != len(nx) - 1:\n                edges.append(nl[(i + 1) % len(nl)])\n            if i != 0:\n                edges.append(nl[(i - 1) % len(nl)])\n        else:\n            edges = [nl[(i - 1) % len(nl)], nl[(i + 1) % len(nl)]]\n        graph[l] = ((x, y), edges)\n    graph[\"M\"] = ((0, 0), [\"N5\", \"N15\"])\n    graph[\"N5\"][1].append(\"M\")\n    graph[\"N15\"][1].append(\"M\")\n    print(graph)\n\n    path_sol = nl\n    if not disconnect:\n        path_sol += [\"N0\"]\n\n    mapdb = InMemMap(\"map\", graph=graph, use_latlon=False)\n    return mapdb, path1, path2, path_sol\n\n\ndef visualize_map():\n    if directory is None:\n        return\n    import matplotlib.pyplot as plt\n    import leuvenmapmatching.visualization as mm_vis\n\n    mapdb, path1, path2, path_sol = setup_map()\n\n    fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(10, 5))\n    mm_vis.plot_map(mapdb, path=path1, ax=ax, show_labels=True)\n    fig.savefig(str(directory / 'test_nonemitting_circle_map_path1.png'))\n    plt.close(fig)\n\n    fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(10, 10))\n    mm_vis.plot_map(mapdb, path=path2, ax=ax, show_labels=True)\n    fig.savefig(str(directory / 'test_nonemitting_circle_map_path2.png'))\n    plt.close(fig)\n\n\ndef visualize_path(matcher, mapdb, name=\"test\"):\n    import matplotlib.pyplot as plt\n    from leuvenmapmatching import visualization as mmviz\n    fig, ax = plt.subplots(1, 1, figsize=(10, 10))\n    mmviz.plot_map(mapdb, matcher=matcher, ax=ax,\n                   show_labels=True, show_matching=True, show_graph=True,\n                   linewidth=2)\n    fn = directory / f\"test_nonemitting_circle_{name}_map.png\"\n    fig.savefig(str(fn))\n    plt.close(fig)\n    print(f\"saved to {fn}\")\n\n\ndef test_path1():\n    mapdb, path1, path2, path_sol = setup_map()\n    matcher = SimpleMatcher(mapdb, max_dist_init=1, min_prob_norm=0.5, obs_noise=0.5,\n                            non_emitting_states=True)\n    matcher.match(path1, unique=True)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        visualize_path(matcher, mapdb, name=\"testpath1\")\n    assert path_pred == path_sol, f\"Nodes not equal:\\n{path_pred}\\n{path_sol}\"\n\n\ndef test_path1_dist():\n    mapdb, path1, path2, path_sol = setup_map()\n    matcher = DistanceMatcher(mapdb, max_dist_init=1, min_prob_norm=0.8, obs_noise=0.5,\n                              non_emitting_states=True)\n    matcher.match(path1, unique=True)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        visualize_path(matcher, mapdb, name=\"test_path1_dist\")\n    assert path_pred == path_sol, f\"Nodes not equal:\\n{path_pred}\\n{path_sol}\"\n\n\ndef test_path2():\n    mapdb, path1, path2, _ = setup_map()\n    path_sol = [f\"N{i}\" for i in range(20)]\n    matcher = SimpleMatcher(mapdb, max_dist_init=0.2, min_prob_norm=0.1,\n                                  obs_noise=0.1, obs_noise_ne=1,\n                                  non_emitting_states=True, only_edges=True)\n    path_pred = matcher.match(path2, unique=True)\n    print(path_pred)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        matcher.print_lattice_stats(verbose=True)\n        matcher.print_lattice()\n        print(\"Best path through lattice:\")\n        for m in matcher.lattice_best:\n            print(m)\n        visualize_path(matcher, mapdb, name=\"testpath2\")\n    assert path_pred == path_sol, f\"Nodes not equal:\\n{path_pred}\\n{path_sol}\"\n\n\ndef test_path2_dist():\n    mapdb, path1, path2, _ = setup_map()\n    path_sol = [f\"N{i}\" for i in range(20)]\n    matcher = DistanceMatcher(mapdb, max_dist_init=0.2, min_prob_norm=0.1,\n                              obs_noise=0.1, obs_noise_ne=1,\n                              non_emitting_states=True)\n    matcher.match(path2)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        visualize_path(matcher, mapdb, name=\"test_path2_dist\")\n    assert path_pred == path_sol, f\"Nodes not equal:\\n{path_pred}\\n{path_sol}\"\n\n\nif __name__ == \"__main__\":\n    # mm.matching.logger.setLevel(logging.INFO)\n    mm.logger.setLevel(logging.DEBUG)\n    mm.logger.addHandler(logging.StreamHandler(sys.stdout))\n    directory = Path(os.environ.get('TESTDIR', Path(__file__).parent))\n    print(f\"Saving files to {directory}\")\n    # visualize_map()\n    test_path1()\n    # test_path1_dist()\n    # test_path2()\n    # test_path2_dist()\n"
  },
  {
    "path": "tests/test_parallelroads.py",
    "content": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_parallelroads\n~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport sys\nimport os\nimport logging\nfrom pathlib import Path\nimport leuvenmapmatching as mm\nfrom leuvenmapmatching.map.sqlite import SqliteMap\nfrom leuvenmapmatching.matcher.distance import DistanceMatcher\nfrom leuvenmapmatching.util.dist_euclidean import lines_parallel\n\n\nlogger = mm.logger\ndirectory = None\n\n\ndef create_map1():\n    db = SqliteMap(\"map\", use_latlon=False, dir=directory)\n    logger.debug(f\"Initialized db: {db}\")\n    db.add_nodes([\n        (1, (1, 1)),\n        (2, (1, 2.9)),\n        (22, (1, 3.0)),\n        (3, (2, 2)),\n        (33, (2, 2.1)),\n        (4, (2, 4)),\n        (5, (3, 3)),\n        (6, (3, 5))\n    ])\n    db.add_edges([\n        (1, 2), (1, 3),\n        (2, 22), (2, 1),\n        (22, 2), (22, 33), (22, 4),\n        (3, 33), (3, 1), (3, 2), (3, 5),\n        (33, 3),\n        (4, 22), (4, 33), (4, 5), (4, 6),\n        (5, 3), (5, 4), (5, 6),\n        (6, 4), (6, 5)\n    ])\n    logger.debug(f\"Filled db: {db}\")\n    return db\n\n\ndef create_path1():\n    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)]\n\n\ndef test_parallel():\n    result = lines_parallel((1, 2.9), (2, 2), (1, 3.0), (2, 2.1), d=0.1)\n    assert result is True\n\n\ndef test_bb1():\n    mapdb = create_map1()\n\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(mapdb,\n                       show_labels=True, show_graph=True,\n                       filename=str(directory / \"test_bb.png\"))\n\n    mapdb.connect_parallelroads()\n\n    assert mapdb.size() == 8\n    coord = mapdb.node_coordinates(2)\n    assert coord == (1, 2.9)\n\n    nodes = mapdb.all_nodes(bb=[0.5, 2.5, 1.5, 3.5])\n    node_ids = set([nid for nid, _ in nodes])\n    assert node_ids == {2, 22}\n\n    edges = mapdb.all_edges(bb=[0.5, 2.5, 1.5, 3.5])\n    edge_tuples = set((nid1, nid2) for nid1, _, nid2, _ in edges)\n    assert edge_tuples == {(1, 2), (2, 1), (3, 2), (2, 22), (22, 2), (22, 33), (22, 4), (4, 22)}\n\n    nodes = mapdb.nodes_nbrto(2)\n    node_ids = set([nid for nid, _ in nodes])\n    assert node_ids == {1, 22}\n\n    edges = mapdb.edges_nbrto((1, 2))\n    edge_tuples = set((nid1, nid2) for nid1, _, nid2, _ in edges)\n    assert edge_tuples == {(2, 22), (2, 1)}\n\n    edges = mapdb.edges_nbrto((3, 2))\n    edge_tuples = set((nid1, nid2) for nid1, _, nid2, _ in edges)\n    assert edge_tuples == {(22, 33), (2, 22), (2, 1)}\n\n\ndef test_merge1():\n    mapdb = create_map1()\n    mapdb.connect_parallelroads()\n\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(mapdb,\n                       show_labels=True, show_graph=True,\n                       filename=str(directory / \"test_parallel_merge.png\"))\n\n\ndef test_path1():\n    mapdb = create_map1()\n    mapdb.connect_parallelroads()\n    path = create_path1()\n    states_sol = [(1, 2), (2, 22), (22, 33), (22, 33), (22, 33), (3, 2), (3, 2), (3, 2), (2, 22), (22, 4)]\n\n    matcher = DistanceMatcher(mapdb, max_dist_init=0.2,\n                              obs_noise=0.5, obs_noise_ne=2, dist_noise=0.5,\n                              non_emitting_states=True)\n    states, _ = matcher.match(path)\n\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(mapdb, matcher=matcher,\n                       show_labels=True, show_graph=True, show_matching=True,\n                       filename=str(directory / \"test_parallel_merge.png\"))\n    assert states == states_sol, f\"Unexpected states: {states}\"\n\n\nif __name__ == \"__main__\":\n    logger.setLevel(logging.DEBUG)\n    logger.addHandler(logging.StreamHandler(sys.stdout))\n    directory = Path(os.environ.get('TESTDIR', Path(__file__).parent))\n    print(f\"Saving files to {directory}\")\n    # test_parallel()\n    # test_merge1()\n    # test_path1()\n    test_bb1()\n"
  },
  {
    "path": "tests/test_path.py",
    "content": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_path\n~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2017-2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport sys\nimport os\nimport logging\nfrom pathlib import Path\n\nimport leuvenmapmatching as mm\nfrom leuvenmapmatching.map.inmem import InMemMap\nfrom leuvenmapmatching.matcher.simple import SimpleMatcher\nfrom leuvenmapmatching.matcher.distance import DistanceMatcher\n\n\nlogger = mm.logger\ndirectory = None\n\n\ndef test_path1():\n    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),\n            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),\n            (2.1, 3.3), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2), (3.1, 3.8),\n            (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]\n    # path_sol = ['A', ('A', 'B'), 'B', ('B', 'D'), 'D', ('D', 'E'), 'E', ('E', 'F')]\n    path_sol_nodes = ['A', 'B', 'D', 'E', 'F']\n    mapdb = InMemMap(\"map\", graph={\n        \"A\": ((1, 1), [\"B\", \"C\"]),\n        \"B\": ((1, 3), [\"A\", \"C\", \"D\"]),\n        \"C\": ((2, 2), [\"A\", \"B\", \"D\", \"E\"]),\n        \"D\": ((2, 4), [\"B\", \"C\", \"D\", \"E\"]),\n        \"E\": ((3, 3), [\"C\", \"D\", \"F\"]),\n        \"F\": ((3, 5), [\"D\", \"E\"])\n    }, use_latlon=False)\n\n    matcher = SimpleMatcher(mapdb, max_dist=None, min_prob_norm=None,\n                            non_emitting_states=False, only_edges=False)\n    path_pred, _ = matcher.match(path, unique=True)\n    if directory:\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       show_graph=True, show_lattice=True,\n                       filename=str(directory / \"test_path1.png\"))\n    # assert path_pred == path_sol, f\"Paths not equal:\\n{path_pred}\\n{path_sol}\"\n    nodes_pred = matcher.path_pred_onlynodes\n    assert nodes_pred == path_sol_nodes, f\"Nodes not equal:\\n{nodes_pred}\\n{path_sol_nodes}\"\n\n\ndef test_path1_dist():\n    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),\n            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),\n            (2.1, 3.3), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2), (3.1, 3.8),\n            (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]\n    # path_sol = ['A', ('A', 'B'), 'B', ('B', 'D'), 'D', ('D', 'E'), 'E', ('E', 'F')]\n    path_sol_nodes = ['A', 'B', 'D', 'E', 'F']\n    mapdb = InMemMap(\"map\", graph={\n        \"A\": ((1, 1), [\"B\", \"C\"]),\n        \"B\": ((1, 3), [\"A\", \"C\", \"D\"]),\n        \"C\": ((2, 2), [\"A\", \"B\", \"D\", \"E\"]),\n        \"D\": ((2, 4), [\"B\", \"C\", \"D\", \"E\"]),\n        \"E\": ((3, 3), [\"C\", \"D\", \"F\"]),\n        \"F\": ((3, 5), [\"D\", \"E\"])\n    }, use_latlon=False)\n\n    matcher = DistanceMatcher(mapdb, max_dist=None, min_prob_norm=None,\n                              obs_noise=0.5,\n                              non_emitting_states=False)\n    matcher.match(path)\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True, show_graph=True,\n                       filename=str(directory / \"test_path1_dist.png\"))\n    nodes_pred = matcher.path_pred_onlynodes\n    assert nodes_pred == path_sol_nodes, f\"Nodes not equal:\\n{nodes_pred}\\n{path_sol_nodes}\"\n\n\ndef test_path2():\n    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),\n            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),\n            (2.1, 3.3), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2), (3.1, 3.8),\n            (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]\n    # path_sol = ['A', ('A', 'B'), 'B', ('B', 'D'), 'D', ('D', 'E'), 'E', ('E', 'F')]\n    path_sol_nodes = ['A', 'B', 'D', 'E', 'F']\n    mapdb = InMemMap(\"map\", graph={\n        \"A\": ((1, 1), [\"B\", \"C\", \"X\"]),\n        \"B\": ((1, 3), [\"A\", \"C\", \"D\", \"K\"]),\n        \"C\": ((2, 2), [\"A\", \"B\", \"D\", \"E\", \"X\", \"Y\"]),\n        \"D\": ((2, 4), [\"B\", \"C\", \"F\", \"E\", \"K\", \"L\"]),\n        \"E\": ((3, 3), [\"C\", \"D\", \"F\", \"Y\"]),\n        \"F\": ((3, 5), [\"D\", \"E\", \"L\"]),\n        \"X\": ((2, 0), [\"A\", \"C\", \"Y\"]),\n        \"Y\": ((3, 1), [\"X\", \"C\", \"E\"]),\n        \"K\": ((1, 5), [\"B\", \"D\", \"L\"]),\n        \"L\": ((2, 6), [\"K\", \"D\", \"F\"])\n    }, use_latlon=False)\n\n    matcher = SimpleMatcher(mapdb, max_dist=None, min_prob_norm=0.001,\n                            non_emitting_states=False, only_edges=False,\n                            max_lattice_width=3)\n    path_pred, _ = matcher.match(path, unique=True)\n    if directory:\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       show_lattice=True, show_graph=True,\n                       filename=str(directory / \"test_path2.png\"))\n    # assert path_pred == path_sol, \"Nodes not equal:\\n{}\\n{}\".format(path_pred, path_sol)\n    nodes_pred = matcher.path_pred_onlynodes\n    assert nodes_pred == path_sol_nodes, f\"Nodes not equal:\\n{nodes_pred}\\n{path_sol_nodes}\"\n\n\ndef test_path2_inc():\n    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),\n            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),\n            (2.1, 3.3), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2), (3.1, 3.8),\n            (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]\n    # path_sol = ['A', ('A', 'B'), 'B', ('B', 'D'), 'D', ('D', 'E'), 'E', ('E', 'F')]\n    path_sol_nodes = ['A', 'B', 'D', 'E', 'F']\n    mapdb = InMemMap(\"map\", graph={\n        \"A\": ((1, 1), [\"B\", \"C\", \"X\"]),\n        \"B\": ((1, 3), [\"A\", \"C\", \"D\", \"K\"]),\n        \"C\": ((2, 2), [\"A\", \"B\", \"D\", \"E\", \"X\", \"Y\"]),\n        \"D\": ((2, 4), [\"B\", \"C\", \"F\", \"E\", \"K\", \"L\"]),\n        \"E\": ((3, 3), [\"C\", \"D\", \"F\", \"Y\"]),\n        \"F\": ((3, 5), [\"D\", \"E\", \"L\"]),\n        \"X\": ((2, 0), [\"A\", \"C\", \"Y\"]),\n        \"Y\": ((3, 1), [\"X\", \"C\", \"E\"]),\n        \"K\": ((1, 5), [\"B\", \"D\", \"L\"]),\n        \"L\": ((2, 6), [\"K\", \"D\", \"F\"])\n    }, use_latlon=False)\n\n    ## Phase 1\n    print('=== PHASE 1 ===')\n    matcher = SimpleMatcher(mapdb, max_dist=None, min_prob_norm=0.001,\n                            non_emitting_states=False, only_edges=False,\n                            max_lattice_width=1)\n    path_pred, _ = matcher.match(path, unique=True)\n    if directory:\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        from leuvenmapmatching import visualization as mmviz\n        with (directory / 'test_path2_inc_1.gv').open('w') as ofile:\n            matcher.lattice_dot(file=ofile, precision=2, render=True)\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       show_lattice=True, show_graph=True,\n                       filename=str(directory / \"test_path2_inc_1.png\"))\n\n    ## Next phases\n    for phase_nb, phase_width in enumerate([2, 3]):\n        print(f'=== PHASE {phase_nb + 2} ===')\n        path_pred, _ = matcher.increase_max_lattice_width(phase_width, unique=True)\n        if directory:\n            matcher.print_lattice_stats()\n            matcher.print_lattice()\n            from leuvenmapmatching import visualization as mmviz\n            with (directory / f'test_path2_inc_{phase_nb + 2}.gv').open('w') as ofile:\n                matcher.lattice_dot(file=ofile, precision=2, render=True)\n            mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                           show_lattice=True, show_graph=True,\n                           filename=str(directory / f\"test_path2_inc_{phase_nb + 2}.png\"))\n\n    # assert path_pred == path_sol, \"Nodes not equal:\\n{}\\n{}\".format(path_pred, path_sol)\n    nodes_pred = matcher.path_pred_onlynodes\n    assert nodes_pred == path_sol_nodes, f\"Nodes not equal:\\n{nodes_pred}\\n{path_sol_nodes}\"\n\n\ndef test_path2_dist():\n    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),\n            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),\n            (2.1, 3.3), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2), (3.1, 3.8),\n            (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]\n    path_sol_nodes = ['X', 'A', 'B', 'D', 'E', 'F']\n    mapdb = InMemMap(\"map\", graph={\n        \"A\": ((1, 1), [\"B\", \"C\", \"X\"]),\n        \"B\": ((1, 3), [\"A\", \"C\", \"D\", \"K\"]),\n        \"C\": ((2, 2), [\"A\", \"B\", \"D\", \"E\", \"X\", \"Y\"]),\n        \"D\": ((2, 4), [\"B\", \"C\", \"F\", \"E\", \"K\", \"L\"]),\n        \"E\": ((3, 3), [\"C\", \"D\", \"F\", \"Y\"]),\n        \"F\": ((3, 5), [\"D\", \"E\", \"L\"]),\n        \"X\": ((2, 0), [\"A\", \"C\", \"Y\"]),\n        \"Y\": ((3, 1), [\"X\", \"C\", \"E\"]),\n        \"K\": ((1, 5), [\"B\", \"D\", \"L\"]),\n        \"L\": ((2, 6), [\"K\", \"D\", \"F\"])\n    }, use_latlon=False)\n\n    matcher = DistanceMatcher(mapdb, max_dist=None, min_prob_norm=0.001,\n                              obs_noise=0.5,\n                              non_emitting_states=False)\n    matcher.match(path, unique=True)\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(mapdb, matcher=matcher,\n                       show_labels=True, show_matching=True, show_graph=True,\n                       filename=str(directory / \"test_path2_dist.png\"))\n    nodes_pred = matcher.path_pred_onlynodes\n    assert nodes_pred == path_sol_nodes, f\"Nodes not equal:\\n{nodes_pred}\\n{path_sol_nodes}\"\n\n\ndef test_path_outlier():\n    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),\n            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),\n            (2.1, 3.3), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2), (3.1, 3.8),\n            (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]\n    path_sol = ['A', 'B', 'D', 'C', 'D', 'E', 'F']\n    path.insert(13, (2.3, 1.8))\n    mapdb = InMemMap(\"map\", graph={\n        \"A\": ((1, 1), [\"B\", \"C\", \"X\"]),\n        \"B\": ((1, 3), [\"A\", \"C\", \"D\", \"K\"]),\n        \"C\": ((2, 2), [\"A\", \"B\", \"D\", \"E\", \"X\", \"Y\"]),\n        \"D\": ((2, 4), [\"B\", \"C\", \"F\", \"E\", \"K\", \"L\"]),\n        \"E\": ((3, 3), [\"C\", \"D\", \"F\", \"Y\"]),\n        \"F\": ((3, 5), [\"D\", \"E\", \"L\"]),\n        \"X\": ((2, 0), [\"A\", \"C\", \"Y\"]),\n        \"Y\": ((3, 1), [\"X\", \"C\", \"E\"]),\n        \"K\": ((1, 5), [\"B\", \"D\", \"L\"]),\n        \"L\": ((2, 6), [\"K\", \"D\", \"F\"])\n    }, use_latlon=False)\n\n    matcher = SimpleMatcher(mapdb, max_dist=None, min_prob_norm=0.0001,\n                            max_dist_init=1, obs_noise=0.5, obs_noise_ne=10,\n                            non_emitting_states=True)\n    _, last_idx = matcher.match(path, unique=True)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        from leuvenmapmatching import visualization as mmviz\n        with (directory / 'lattice.gv').open('w') as ofile:\n            matcher.lattice_dot(file=ofile)\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       filename=str(directory / \"test_path_outlier.png\"))\n        print(\"Path through lattice:\\n\" + \"\\n\".join(m.label for m in matcher.lattice_best))\n    assert last_idx == len(path) - 1\n    assert path_pred == path_sol, \"Nodes not equal:\\n{}\\n{}\".format(path_pred, path_sol)\n\n\ndef test_path_outlier2():\n    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),\n            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),\n            (2.1, 3.3), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2), (3.1, 3.8),\n            (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]\n    path.insert(13, (2.3, -3.0))\n    mapdb = InMemMap(\"map\", graph={\n        \"A\": ((1, 1), [\"B\", \"C\", \"X\"]),\n        \"B\": ((1, 3), [\"A\", \"C\", \"D\", \"K\"]),\n        \"C\": ((2, 2), [\"A\", \"B\", \"D\", \"E\", \"X\", \"Y\"]),\n        \"D\": ((2, 4), [\"B\", \"C\", \"F\", \"E\", \"K\", \"L\"]),\n        \"E\": ((3, 3), [\"C\", \"D\", \"F\", \"Y\"]),\n        \"F\": ((3, 5), [\"D\", \"E\", \"L\"]),\n        \"X\": ((2, 0), [\"A\", \"C\", \"Y\"]),\n        \"Y\": ((3, 1), [\"X\", \"C\", \"E\"]),\n        \"K\": ((1, 5), [\"B\", \"D\", \"L\"]),\n        \"L\": ((2, 6), [\"K\", \"D\", \"F\"])\n    }, use_latlon=False)\n\n    matcher = DistanceMatcher(mapdb, max_dist=None, min_prob_norm=0.1,\n                            max_dist_init=1, obs_noise=0.25, obs_noise_ne=1,\n                            non_emitting_states=True)\n    _, last_idx = matcher.match(path, unique=True)\n    if directory:\n        # matcher.print_lattice_stats()\n        # matcher.print_lattice()\n        from leuvenmapmatching import visualization as mmviz\n        # with (directory / 'lattice.gv').open('w') as ofile:\n        #     matcher.lattice_dot(file=ofile)\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       filename=str(directory / \"test_path_outlier2.png\"))\n    assert last_idx == 12\n\n\ndef test_path_outlier_dist():\n    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),\n            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),\n            (2.1, 3.3), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2), (3.1, 3.8),\n            (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]\n    path_sol = ['A', 'B', 'D', 'C', 'E', 'F']\n    path.insert(13, (2.3, 1.8))\n    mapdb = InMemMap(\"map\", graph={\n        \"A\": ((1, 1), [\"B\", \"C\", \"X\"]),\n        \"B\": ((1, 3), [\"A\", \"C\", \"D\", \"K\"]),\n        \"C\": ((2, 2), [\"A\", \"B\", \"D\", \"E\", \"X\", \"Y\"]),\n        \"D\": ((2, 4), [\"B\", \"C\", \"F\", \"E\", \"K\", \"L\"]),\n        \"E\": ((3, 3), [\"C\", \"D\", \"F\", \"Y\"]),\n        \"F\": ((3, 5), [\"D\", \"E\", \"L\"]),\n        \"X\": ((2, 0), [\"A\", \"C\", \"Y\"]),\n        \"Y\": ((3, 1), [\"X\", \"C\", \"E\"]),\n        \"K\": ((1, 5), [\"B\", \"D\", \"L\"]),\n        \"L\": ((2, 6), [\"K\", \"D\", \"F\"])\n    }, use_latlon=False)\n\n    matcher = DistanceMatcher(mapdb, max_dist=None, min_prob_norm=0.0001,\n                              max_dist_init=1, obs_noise=0.5, obs_noise_ne=10,\n                              non_emitting_states=True)\n    matcher.match(path)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(mapdb, matcher=matcher,\n                       show_labels=True, show_matching=True, show_graph=True,\n                       filename=str(directory / \"test_path_outlier_dist.png\"))\n    # TODO: Smoothing the observation distances could eliminate the outlier\n    assert path_pred == path_sol, \"Nodes not equal:\\n{}\\n{}\".format(path_pred, path_sol)\n\n\ndef test_path3():\n    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)]\n    path_sol = ['E', 'F']\n    mapdb = InMemMap(\"map\", graph={\n        \"E\": ((3, 3), [\"F\"]),\n        \"F\": ((3, 5), [\"E\"]),\n    }, use_latlon=False)\n\n    matcher = SimpleMatcher(mapdb, max_dist=None, min_prob_norm=0.0001,\n                            max_dist_init=1, obs_noise=0.25, obs_noise_ne=10,\n                            non_emitting_states=True)\n    matcher.match(path, unique=True)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        from leuvenmapmatching import visualization as mmviz\n        with (directory / 'lattice.gv').open('w') as ofile:\n            matcher.lattice_dot(file=ofile)\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       filename=str(directory / \"test_path3.png\"))\n        print(\"Path through lattice:\\n\" + \"\\n\".join(m.label for m in matcher.lattice_best))\n    assert path_pred == path_sol, \"Nodes not equal:\\n{}\\n{}\".format(path_pred, path_sol)\n\n\ndef test_path3_dist():\n    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)]\n    path_sol = ['E', 'F']\n    mapdb = InMemMap(\"map\", graph={\n        \"E\": ((3, 3), [\"F\"]),\n        \"F\": ((3, 5), [\"E\"]),\n    }, use_latlon=False)\n\n    matcher = DistanceMatcher(mapdb, max_dist=None, min_prob_norm=0.0001,\n                              max_dist_init=1, obs_noise=0.25, obs_noise_ne=10,\n                              non_emitting_states=True)\n    matcher.match(path, unique=True)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       filename=str(directory / \"test_path3_dist.png\"))\n        print(\"Path through lattice:\\n\" + \"\\n\".join(m.label for m in matcher.lattice_best))\n    assert path_pred == path_sol, \"Nodes not equal:\\n{}\\n{}\".format(path_pred, path_sol)\n\n\ndef test_path4_dist_inc():\n    map_con = InMemMap(\"mymap\", graph={\n        \"A\": ((1, 1), [\"B\", \"C\", \"X\"]),\n        \"B\": ((1, 3), [\"A\", \"C\", \"D\", \"K\"]),\n        \"C\": ((2, 2), [\"A\", \"B\", \"D\", \"E\", \"X\", \"Y\"]),\n        \"D\": ((2, 4), [\"B\", \"C\", \"D\", \"E\", \"K\", \"L\"]),\n        \"E\": ((3, 3), [\"C\", \"D\", \"F\", \"Y\"]),\n        \"F\": ((3, 5), [\"D\", \"E\", \"L\"]),\n        \"X\": ((2, 0), [\"A\", \"C\", \"Y\"]),\n        \"Y\": ((3, 1), [\"X\", \"C\", \"E\"]),\n        \"K\": ((1, 5), [\"B\", \"D\", \"L\"]),\n        \"L\": ((2, 6), [\"K\", \"D\", \"F\"])\n    }, use_latlon=False)\n\n    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),\n            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),\n            (2.3, 3.5), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2),\n            (3.1, 3.8), (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]\n\n    matcher = DistanceMatcher(map_con, max_dist=2, obs_noise=1, min_prob_norm=0.5)\n    matcher.match(path[:5])\n    if directory:\n        import matplotlib\n        # matplotlib.use('macosx')\n        # print(matplotlib.matplotlib_fname())\n        # import matplotlib.pyplot as plt\n        # print(plt.get_backend())\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(map_con, matcher=matcher,\n                       show_labels=True, show_matching=True, show_graph=True,\n                       filename=str(directory / \"test_path4_dist_inc_1.png\"))\n\n    matcher.match(path, expand=True)\n    nodes = matcher.path_pred_onlynodes\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(map_con, matcher=matcher,\n                       show_labels=True, show_matching=True, show_graph=True,\n                       filename=str(directory / \"test_path4_dist_inc_2.png\"))\n    nodes_sol = ['X', 'A', 'B', 'D', 'E', 'F']\n    assert nodes == nodes_sol, \"Nodes not equal:\\n{}\\n{}\".format(nodes, nodes_sol)\n\n\ndef test_path4_dist_inc_missing():\n    map_con = InMemMap(\"mymap\", graph={\n        \"A\": ((1, 1), [\"B\", \"C\", \"X\"]),\n        \"B\": ((1, 3), [\"A\", \"C\"]),\n        \"C\": ((2, 2), [\"A\", \"B\", \"X\", \"Y\"]),\n        \"D\": ((2, 4), [\"E\", \"K\", \"L\"]),\n        \"E\": ((3, 3), [\"D\", \"F\"]),\n        \"F\": ((3, 5), [\"D\", \"E\", \"L\"]),\n        \"X\": ((2, 0), [\"A\", \"C\", \"Y\"]),\n        \"Y\": ((3, 1), [\"X\", \"C\"]),\n        \"K\": ((1, 5), [\"D\", \"L\"]),\n        \"L\": ((2, 6), [\"K\", \"D\", \"F\"])\n    }, use_latlon=False)\n\n    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),\n            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),\n            (2.3, 3.5), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2),\n            (3.1, 3.8), (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]\n\n    matcher = DistanceMatcher(map_con, max_dist=0.5, obs_noise=1, min_prob_norm=0.5,\n                              max_lattice_width=3)\n    matcher.match(path)\n    if directory:\n        import matplotlib\n        # matplotlib.use('macosx')\n        # print(matplotlib.matplotlib_fname())\n        # import matplotlib.pyplot as plt\n        # print(plt.get_backend())\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(map_con, matcher=matcher,\n                       show_labels=True, show_matching=True, show_graph=True,\n                       filename=str(directory / \"test_path4_dist_inc_missing_1.png\"))\n\n    matcher.continue_with_distance()\n\n    # return\n    matcher.match(path, expand=True)\n    nodes = matcher.path_pred_onlynodes_withjumps\n    if directory:\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(map_con, matcher=matcher,\n                       show_labels=True, show_matching=True, show_graph=True,\n                       filename=str(directory / \"test_path4_dist_inc_missing_2.png\"))\n    nodes_sol = ['A', 'B', 'C', 'D', 'E', 'F']\n    assert nodes == nodes_sol, \"Nodes not equal:\\n{}\\n{}\".format(nodes, nodes_sol)\n\n\nif __name__ == \"__main__\":\n    logger.setLevel(logging.INFO)\n    logger.addHandler(logging.StreamHandler(sys.stdout))\n    directory = Path(os.environ.get('TESTDIR', Path(__file__).parent))\n    print(f\"Saving files to {directory}\")\n    # test_path1()\n    test_path1_dist()\n    # test_path2()\n    # test_path2_inc()\n    # test_path2_dist()\n    # test_path_outlier()\n    # test_path_outlier2()\n    # test_path_outlier_dist()\n    # test_path3()\n    # test_path3_dist()\n    # test_path4_dist_inc()\n    # test_path4_dist_inc_missing()\n"
  },
  {
    "path": "tests/test_path_latlon.py",
    "content": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_path_latlon\n~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport sys\nimport os\nimport pickle\nimport logging\nfrom pathlib import Path\nimport pytest\nimport leuvenmapmatching as mm\nimport leuvenmapmatching.visualization as mm_viz\nfrom leuvenmapmatching.util.gpx import gpx_to_path\nfrom leuvenmapmatching.util.dist_latlon import interpolate_path\nfrom leuvenmapmatching.util.openstreetmap import create_map_from_xml, download_map_xml\nfrom leuvenmapmatching.matcher.distance import DistanceMatcher\nfrom leuvenmapmatching.map.inmem import InMemMap\n\nlogger = mm.logger\nthis_path = Path(os.path.realpath(__file__)).parent / \"rsrc\" / \"path_latlon\"\nosm_fn = this_path / \"osm_downloaded.xml\"\nosm2_fn = this_path / \"osm_downloaded2.xml\"\nosm3_fn = this_path / \"osm_downloaded3.xml\"\ntrack_fn = this_path / \"route.gpx\"  # http://users.telenet.be/meirenwi/Leuven%20Stadswandeling%20-%205%20km%20RT.zip\ntrack2_fn = this_path / \"route2.gpx\"\ntrack3_fn = this_path / \"route3.pgx\"\nzip_fn = this_path / \"leuvenmapmatching_testdata2.zip\"\ndirectory = None\n\n\ndef prepare_files(verbose=False, force=False, download_from_osm=False):\n    if download_from_osm:\n        download_map_xml(osm_fn, '4.694933,50.870047,4.709256000000001,50.879628', force=force, verbose=verbose)\n        download_map_xml(osm2_fn, '4.6997666,50.8684188,4.7052813,50.8731718', force=force, verbose=verbose)\n        download_map_xml(osm3_fn, '4.69049,50.86784,4.71604,50.88784', force=force, verbose=verbose)\n    else:\n        if not (osm_fn.exists() and osm2_fn.exists() and osm3_fn.exists() and\n                track_fn.exists() and track2_fn.exists()):\n            import requests\n            url = 'https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/leuvenmapmatching_testdata2.zip'\n            logger.debug(\"Download road_network.zip from kuleuven.be\")\n            r = requests.get(url, stream=True)\n            with zip_fn.open('wb') as ofile:\n                for chunk in r.iter_content(chunk_size=1024):\n                    if chunk:\n                        ofile.write(chunk)\n            import zipfile\n            logger.debug(\"Unzipping road_network.zip\")\n            with zipfile.ZipFile(str(zip_fn), \"r\") as zip_ref:\n                zip_ref.extractall(str(zip_fn.parent))\n\n\ndef test_path1(use_rtree=False):\n    prepare_files()\n    track = gpx_to_path(track_fn)\n    track = [loc[:2] for loc in track]\n    track = track[:5]\n    track_int = interpolate_path(track, 5)\n    map_con = create_map_from_xml(osm_fn, use_rtree=use_rtree, index_edges=True)\n\n    matcher = DistanceMatcher(map_con, max_dist=50, obs_noise=50, min_prob_norm=0.1)\n    states, last_idx = matcher.match(track_int)\n\n    if directory:\n        # matcher.print_lattice_stats()\n        mm_viz.plot_map(map_con, matcher=matcher, use_osm=True,\n                        zoom_path=True, show_graph=True,\n                        filename=str(directory / \"test_path_latlon_path1.png\"))\n    assert len(states) == len(track_int), f\"Path ({len(track_int)}) not fully matched by best path ({len(states)}), \" + \\\n                                          f\"last index = {last_idx}\"\n    states_sol = [(2963305939, 249348325), (2963305939, 249348325), (2963305939, 249348325), (2963305939, 249348325),\n                  (2963305939, 249348325), (2963305939, 249348325), (249348325, 1545679243), (249348325, 1545679243),\n                  (1545679243, 3663115134), (1545679243, 3663115134), (1545679243, 3663115134),\n                  (3663115134, 1545679251), (1545679251, 20910628), (1545679251, 20910628), (1545679251, 20910628),\n                  (1545679251, 20910628), (20910628, 3663115130)]\n    assert states == states_sol, f\"Got states: {states}\"\n\n\ndef test_path1_serialization(use_rtree=False):\n    prepare_files()\n    track = gpx_to_path(track_fn)\n    track = [loc[:2] for loc in track]\n    track = track[:5]\n    track_int = interpolate_path(track, 5)\n    map_con = create_map_from_xml(osm_fn, use_rtree=use_rtree, index_edges=True)\n\n    to_serialize = map_con.serialize()\n    map_con.dir = this_path\n    map_con.dump()\n\n    map_con2 = InMemMap.from_pickle(filename = map_con.dir / (map_con.name + \".pkl\"))\n\n    matcher = DistanceMatcher(map_con2, max_dist=50, obs_noise=50, min_prob_norm=0.1)\n    states, last_idx = matcher.match(track_int)\n\n    if directory:\n        # matcher.print_lattice_stats()\n        mm_viz.plot_map(map_con2, matcher=matcher, use_osm=True,\n                        zoom_path=True, show_graph=True,\n                        filename=str(directory / \"test_path_latlon_path1.png\"))\n    assert len(states) == len(track_int), f\"Path ({len(track_int)}) not fully matched by best path ({len(states)}), \" + \\\n                                          f\"last index = {last_idx}\"\n    states_sol = [(2963305939, 249348325), (2963305939, 249348325), (2963305939, 249348325), (2963305939, 249348325),\n                  (2963305939, 249348325), (2963305939, 249348325), (249348325, 1545679243), (249348325, 1545679243),\n                  (1545679243, 3663115134), (1545679243, 3663115134), (1545679243, 3663115134),\n                  (3663115134, 1545679251), (1545679251, 20910628), (1545679251, 20910628), (1545679251, 20910628),\n                  (1545679251, 20910628), (20910628, 3663115130)]\n    assert states == states_sol, f\"Got states: {states}\"\n\n\n@pytest.mark.skip(reason=\"Takes a long time\")\ndef test_path1_full():\n    prepare_files()\n    track = gpx_to_path(track_fn)\n    track = [loc[:2] for loc in track]\n    track_int = interpolate_path(track, 5)\n    map_con = create_map_from_xml(osm_fn, include_footways=True, include_parking=True)\n\n    matcher = DistanceMatcher(map_con, max_dist=50, obs_noise=50, min_prob_norm=0.1)\n    states, last_idx = matcher.match(track_int)\n\n    if directory:\n        # matcher.print_lattice_stats()\n        mm_viz.plot_map(map_con, matcher=matcher, use_osm=True,\n                        zoom_path=True, show_graph=True,\n                        filename=str(directory / \"test_path_latlon_path1.png\"))\n    assert len(states) == len(track_int), f\"Path ({len(track_int)}) not fully matched by best path ({len(states)}), \" + \\\n                                          f\"last index = {last_idx}\"\n\n\ndef test_path2_proj():\n    prepare_files()\n    map_con_latlon = create_map_from_xml(osm2_fn)\n    map_con = map_con_latlon.to_xy()\n    track = [map_con.latlon2yx(p[0], p[1]) for p in gpx_to_path(track2_fn)]\n    matcher = DistanceMatcher(map_con, max_dist=300, max_dist_init=25, min_prob_norm=0.0001,\n                              non_emitting_length_factor=0.95,\n                              obs_noise=50, obs_noise_ne=50,\n                              dist_noise=50,\n                              max_lattice_width=5,\n                              non_emitting_states=True)\n    states, last_idx = matcher.match(track, unique=False)\n    nodes = matcher.path_pred_onlynodes\n    if directory:\n        matcher.print_lattice_stats()\n        mm_viz.plot_map(map_con, matcher=matcher, path=track, use_osm=False,\n                        show_graph=True, show_matching=True, show_labels=5,\n                        filename=str(directory / \"test_path_latlon_path2_proj.png\"))\n    nodes_sol = [2634474831, 1096512242, 3051083902, 1096512239, 1096512241, 1096512240, 1096508366, 1096508372,\n                 16483861, 1096508360, 159656075, 1096508382, 16483862, 3051083898, 16526535, 3060597381, 3060515059,\n                 16526534, 16526532, 1274158119, 16526540, 3060597377, 16526541, 16424220, 1233373340, 613125597,\n                 1076057753]\n    nodes_sol2 = [1096512242, 3051083902, 1096512239, 1096512241, 1096512240, 159654664, 1096508373, 1096508381,\n                  16483859, 1096508369, 159654663, 1096508363, 16483862, 3051083898, 16526535, 3060597381, 3060515059,\n                  16526534, 16526532, 611867918, 3060725817, 16483866, 3060725817, 611867918, 16526532, 1274158119,\n                  16526540, 3060597377, 16526541, 16424220, 1233373340, 613125597, 1076057753]\n    assert (nodes == nodes_sol) or (nodes == nodes_sol2), f\"Nodes do not match: {nodes}\"\n\n\ndef test_path2():\n    prepare_files()\n    map_con = create_map_from_xml(osm2_fn)\n    track = [(p[0], p[1]) for p in gpx_to_path(track2_fn)]\n    matcher = DistanceMatcher(map_con, max_dist=300, max_dist_init=25, min_prob_norm=0.0001,\n                              non_emitting_length_factor=0.95,\n                              obs_noise=50, obs_noise_ne=50,\n                              dist_noise=50,\n                              max_lattice_width=5,\n                              non_emitting_states=True)\n    states, last_idx = matcher.match(track, unique=False)\n    nodes = matcher.path_pred_onlynodes\n    if directory:\n        mm_viz.plot_map(map_con, matcher=matcher, nodes=nodes, path=track, z=17, use_osm=True,\n                        show_graph=True, show_matching=True,\n                        filename=str(directory / \"test_path_latlon_path2.png\"))\n    nodes_sol = [2634474831, 1096512242, 3051083902, 1096512239, 1096512241, 1096512240, 1096508366, 1096508372,\n                 16483861, 3051083900, 16483864, 16483865, 3060515058, 16526534, 16526532, 1274158119, 16526540,\n                 3060597377, 16526541, 16424220, 1233373340, 613125597, 1076057753]\n    nodes_sol2 = [2634474831, 1096512242, 3051083902, 1096512239, 1096512241, 1096512240, 159654664, 1096508373,\n                  1096508381, 16483859, 1096508369, 159654663, 1096508363, 16483862, 3051083898, 16526535, 3060597381,\n                  3060515059, 16526534, 16526532, 1274158119, 16526540, 3060597377, 16526541, 16424220, 1233373340,\n                  613125597, 1076057753]\n\n    assert (nodes == nodes_sol) or (nodes == nodes_sol2), f\"Nodes do not match: {nodes}\"\n\n\ndef test_path3():\n    prepare_files()\n    track = [(50.87881, 4.698930000000001), (50.87899, 4.69836), (50.87905000000001, 4.698110000000001),\n             (50.879000000000005, 4.69793), (50.87903000000001, 4.69766), (50.87906, 4.697500000000001),\n             (50.87908, 4.6973), (50.879110000000004, 4.69665), (50.87854, 4.696420000000001),\n             (50.878440000000005, 4.696330000000001), (50.878370000000004, 4.696140000000001),\n             (50.8783, 4.69578), (50.87832, 4.69543), (50.87767, 4.695530000000001),\n             (50.87763, 4.695080000000001), (50.87758, 4.6948300000000005), (50.877480000000006, 4.69395),\n             (50.877500000000005, 4.693700000000001), (50.877520000000004, 4.69343),\n             (50.877610000000004, 4.692670000000001), (50.87776, 4.6917800000000005),\n             (50.87783, 4.69141), (50.87744000000001, 4.6908900000000004), (50.87736, 4.690790000000001),\n             (50.877300000000005, 4.69078), (50.876650000000005, 4.6907000000000005),\n             (50.87597, 4.69066), (50.875820000000004, 4.69068), (50.87561, 4.6907700000000006),\n             (50.874430000000004, 4.69136), (50.874210000000005, 4.691490000000001), (50.87413, 4.69151),\n             (50.87406000000001, 4.69151), (50.87397000000001, 4.69148), (50.87346, 4.6913800000000005),\n             (50.87279, 4.691260000000001), (50.872490000000006, 4.69115), (50.87259, 4.6908900000000004),\n             (50.87225, 4.690650000000001), (50.872080000000004, 4.6904900000000005),\n             (50.871550000000006, 4.69125), (50.87097000000001, 4.69216), (50.87033, 4.69324),\n             (50.87017, 4.6935400000000005), (50.87012000000001, 4.69373), (50.86997, 4.69406),\n             (50.86981, 4.694520000000001), (50.86943, 4.69585), (50.868970000000004, 4.697500000000001),\n             (50.868770000000005, 4.698130000000001), (50.86863, 4.6985), (50.86844000000001, 4.69899),\n             (50.868140000000004, 4.69977), (50.86802, 4.70023), (50.867920000000005, 4.70078),\n             (50.86787, 4.701180000000001), (50.86784, 4.70195), (50.86786000000001, 4.702310000000001),\n             (50.86791, 4.702870000000001), (50.86836, 4.7052700000000005), (50.86863, 4.7064900000000005),\n             (50.86880000000001, 4.707210000000001), (50.869220000000006, 4.708410000000001),\n             (50.869400000000006, 4.70891), (50.86959, 4.709350000000001), (50.86995, 4.71004), (50.87006, 4.71021),\n             (50.870900000000006, 4.7112300000000005), (50.872260000000004, 4.712890000000001), (50.87308, 4.71389),\n             (50.873430000000006, 4.714300000000001), (50.873560000000005, 4.71441),\n             (50.873740000000005, 4.714530000000001), (50.874280000000006, 4.714740000000001),\n             (50.876250000000006, 4.71544), (50.876490000000004, 4.7155700000000005),\n             (50.876900000000006, 4.7158500000000005), (50.87709, 4.71598), (50.877190000000006, 4.716010000000001),\n             (50.87751, 4.7160400000000005), (50.87782000000001, 4.7160400000000005), (50.87832, 4.71591),\n             (50.87894000000001, 4.71567), (50.87975, 4.71536), (50.88004, 4.71525), (50.8804, 4.715070000000001),\n             (50.88163, 4.71452), (50.881750000000004, 4.71447), (50.8819, 4.714390000000001),\n             (50.882200000000005, 4.71415), (50.882470000000005, 4.7138800000000005),\n             (50.883480000000006, 4.7127300000000005), (50.88552000000001, 4.710470000000001),\n             (50.88624, 4.70966), (50.88635000000001, 4.7096100000000005), (50.886520000000004, 4.709580000000001),\n             (50.88664000000001, 4.7095400000000005), (50.886750000000006, 4.709280000000001),\n             (50.88684000000001, 4.70906), (50.886970000000005, 4.70898), (50.88705, 4.70887), (50.88714, 4.70868),\n             (50.88743, 4.7079), (50.887840000000004, 4.7069), (50.88776000000001, 4.70687),\n             (50.88765, 4.706790000000001), (50.887100000000004, 4.70627), (50.88702000000001, 4.70619),\n             (50.886950000000006, 4.706040000000001), (50.886950000000006, 4.7058800000000005),\n             (50.886970000000005, 4.705620000000001), (50.88711000000001, 4.70417), (50.88720000000001, 4.70324),\n             (50.88723, 4.7027600000000005), (50.88709000000001, 4.70253), (50.886480000000006, 4.70148),\n             (50.88636, 4.70131), (50.886050000000004, 4.70101), (50.88593, 4.70092),\n             (50.885810000000006, 4.700880000000001), (50.88539, 4.7008600000000005), (50.88497, 4.70082),\n             (50.88436, 4.70089), (50.88398, 4.70094), (50.883250000000004, 4.7010700000000005),\n             (50.88271, 4.701160000000001), (50.88136, 4.70159), (50.881130000000006, 4.701790000000001),\n             (50.880930000000006, 4.7020100000000005), (50.88078, 4.70223), (50.88046000000001, 4.70146),\n             (50.88015000000001, 4.70101), (50.880030000000005, 4.700880000000001), (50.87997000000001, 4.70078),\n             (50.879900000000006, 4.70061), (50.87984, 4.70052), (50.879960000000004, 4.70026)]\n    track = track[:30]\n    map_con = create_map_from_xml(osm3_fn)\n\n    matcher = DistanceMatcher(map_con,\n                              max_dist_init=30, max_dist=50, min_prob_norm=0.1,\n                              obs_noise=10, obs_noise_ne=20, dist_noise=10,\n                              non_emitting_states=True)\n    states, last_idx = matcher.match(track)\n\n    if directory:\n        # matcher.print_lattice_stats()\n        mm_viz.plot_map(map_con, matcher=matcher, use_osm=True,\n                        zoom_path=True, show_graph=False, show_matching=True,\n                        filename=str(directory / \"test_path_latlon_path3.png\"))\n    nodes = matcher.path_pred_onlynodes\n    nodes_sol = [3906576303, 1150903750, 4506996820, 4506996819, 4506996798, 3906576457, 130147477, 3906576346,\n                 231974072, 231974123, 1180606706, 19792164, 19792172, 1180606683, 1180606709, 5236409057,\n                 19792169, 5236409056, 180241961, 180241975, 4506996259, 19792156, 5236409048, 180241625,\n                 180241638, 231953030, 241928030, 241928031, 83796665, 231953028, 1125556965, 1380538625,\n                 1824115892, 4909655515, 16571387, 16737662, 16571388, 179425214, 3705540990, 4567021046]\n    assert nodes == nodes_sol, f\"Nodes do not match: {nodes}\"\n\n\nif __name__ == \"__main__\":\n    logger.setLevel(logging.INFO)\n    logger.addHandler(logging.StreamHandler(sys.stdout))\n    directory = Path(os.environ.get('TESTDIR', Path(__file__).parent))\n    print(f\"Saving files to {directory}\")\n    import matplotlib as mpl\n    mpl.use('MacOSX')\n    # test_path1(use_rtree=True)\n    test_path1_serialization(use_rtree=True)\n    # test_path1_full()\n    # test_path2_proj()\n    # test_path2()\n    # test_path3()\n"
  },
  {
    "path": "tests/test_path_onlyedges.py",
    "content": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_path_onlyedges\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.\n:license: Apache License, Version 2.0, see LICENSE for details.\n\"\"\"\nimport sys\nimport os\nimport logging\nfrom pathlib import Path\nimport leuvenmapmatching as mm\nfrom leuvenmapmatching.map.inmem import InMemMap\nfrom leuvenmapmatching.matcher.simple import SimpleMatcher\n\n\nlogger = mm.logger\ndirectory = None\n\n\ndef test_path1():\n    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),\n            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),\n            (2.1, 3.3), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2), (3.1, 3.8),\n            (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]\n    path_sol = ['A', 'B', 'D', 'E', 'F']\n    mapdb = InMemMap(\"map\", graph={\n        \"A\": ((1, 1), [\"B\", \"C\"]),\n        \"B\": ((1, 3), [\"A\", \"C\", \"D\"]),\n        \"C\": ((2, 2), [\"A\", \"B\", \"D\", \"E\"]),\n        \"D\": ((2, 4), [\"B\", \"C\", \"D\", \"E\"]),\n        \"E\": ((3, 3), [\"C\", \"D\", \"F\"]),\n        \"F\": ((3, 5), [\"D\", \"E\"])\n    }, use_latlon=False)\n\n    matcher = SimpleMatcher(mapdb, max_dist=None, min_prob_norm=None,\n                            only_edges=True, non_emitting_states=False)\n    matcher.match(path, unique=True)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        print(\"Lattice best:\")\n        for m in matcher.lattice_best:\n            print(m)\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        from leuvenmapmatching import visualization as mmviz\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       filename=str(directory / \"test_onlyedges_path1.png\"))\n    assert path_pred == path_sol, f\"Paths not equal:\\n{path_pred}\\n{path_sol}\"\n\n\ndef test_path3():\n    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)]\n    path_sol = ['E', 'F']\n    mapdb = InMemMap(\"map\", graph={\n        \"E\": ((3, 3), [\"F\"]),\n        \"F\": ((3, 5), [\"E\"]),\n    }, use_latlon=False)\n\n    matcher = SimpleMatcher(mapdb, max_dist=None, min_prob_norm=0.0001,\n                            max_dist_init=1, obs_noise=0.25, obs_noise_ne=10,\n                            non_emitting_states=True,\n                            only_edges=True)\n    matcher.match(path, unique=True)\n    path_pred = matcher.path_pred_onlynodes\n    if directory:\n        matcher.print_lattice_stats()\n        matcher.print_lattice()\n        from leuvenmapmatching import visualization as mmviz\n        with (directory / 'lattice.gv').open('w') as ofile:\n            matcher.lattice_dot(file=ofile)\n        mmviz.plot_map(mapdb, matcher=matcher, show_labels=True, show_matching=True,\n                       filename=str(directory / \"test_onlyedges_path3.png\"))\n        print(\"Path through lattice:\\n\" + \"\\n\".join(m.label for m in matcher.lattice_best))\n    assert path_pred == path_sol, \"Nodes not equal:\\n{}\\n{}\".format(path_pred, path_sol)\n\n\nif __name__ == \"__main__\":\n    logger.setLevel(logging.DEBUG)\n    logger.addHandler(logging.StreamHandler(sys.stdout))\n    directory = Path(os.environ.get('TESTDIR', Path(__file__).parent))\n    print(f\"Saving files to {directory}\")\n    test_path1()\n    # test_path3()\n"
  }
]