Full Code of wannesm/LeuvenMapMatching for AI

master 9ca9f0b73665 cached
69 files
352.0 KB
100.9k tokens
320 symbols
1 requests
Download .txt
Showing preview only (373K chars total). Download the full file or copy to clipboard to get everything.
Repository: wannesm/LeuvenMapMatching
Branch: master
Commit: 9ca9f0b73665
Files: 69
Total size: 352.0 KB

Directory structure:
gitextract_6vcq1ios/

├── .gitignore
├── .readthedocs.yaml
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── docs/
│   ├── Makefile
│   ├── classes/
│   │   ├── map/
│   │   │   ├── BaseMap.rst
│   │   │   ├── InMemMap.rst
│   │   │   └── SqliteMap.rst
│   │   ├── matcher/
│   │   │   ├── BaseMatcher.rst
│   │   │   ├── BaseMatching.rst
│   │   │   ├── DistanceMatcher.rst
│   │   │   └── SimpleMatcher.rst
│   │   ├── overview.rst
│   │   └── util/
│   │       └── Segment.rst
│   ├── conf.py
│   ├── index.rst
│   ├── make.bat
│   ├── requirements.txt
│   └── usage/
│       ├── customdistributions.rst
│       ├── debug.rst
│       ├── incremental.rst
│       ├── installation.rst
│       ├── introduction.rst
│       ├── latitudelongitude.rst
│       ├── openstreetmap.rst
│       └── visualisation.rst
├── leuvenmapmatching/
│   ├── __init__.py
│   ├── map/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── inmem.py
│   │   └── sqlite.py
│   ├── matcher/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── distance.py
│   │   ├── newsonkrumm.py
│   │   └── simple.py
│   ├── util/
│   │   ├── __init__.py
│   │   ├── debug.py
│   │   ├── dist_euclidean.py
│   │   ├── dist_latlon.py
│   │   ├── dist_latlon_nvector.py
│   │   ├── evaluation.py
│   │   ├── gpx.py
│   │   ├── kalman.py
│   │   ├── openstreetmap.py
│   │   ├── projections.py
│   │   └── segment.py
│   └── visualization.py
├── setup.cfg
├── setup.py
└── tests/
    ├── examples/
    │   ├── example_1_simple.py
    │   └── example_using_osmnx_and_geopandas.py
    ├── rsrc/
    │   ├── bug2/
    │   │   └── readme.md
    │   ├── newson_krumm_2009/
    │   │   └── readme.md
    │   └── path_latlon/
    │       ├── readme.md
    │       ├── route.gpx
    │       └── route2.gpx
    ├── test_bugs.py
    ├── test_conversion.py
    ├── test_examples.py
    ├── test_newsonkrumm2009.py
    ├── test_nonemitting.py
    ├── test_nonemitting_circle.py
    ├── test_parallelroads.py
    ├── test_path.py
    ├── test_path_latlon.py
    └── test_path_onlyedges.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.cache
.eggs
.idea
dist
*.egg-info
venv*
README
.ipynb_checkpoints
*.zip
*.gv
*.gv.pdf
*.xml
tests/route.gpx
examples/Leuven\ Stadswandeling*
.git-old
.pytest_cache
docs/_build
build
cache
baselines
./examples
*.pkl


================================================
FILE: .readthedocs.yaml
================================================
version: 2

build:
  os: ubuntu-22.04
  tools:
    python: "3.11"

sphinx:
  configuration: docs/conf.py

python:
   install:
   - requirements: docs/requirements.txt


================================================
FILE: LICENSE
================================================
Leuven.MapMatching
------------------

Copyright 2015-2018 KU Leuven, DTAI Research Group
Copyright 2017-2018 Sirris

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.



This package/repository contains code from the dtaimapmatching project as well
as some open-source works:


Latitude/longitude spherical geodesy tools
------------------------------------------

Latitude/longitude spherical geodesy tools
(c) Chris Veness 2002-2017, MIT Licence
www.movable-type.co.uk/scripts/latlong.html
www.movable-type.co.uk/scripts/geodesy/docs/module-latlon-spherical.html


Nvector
-------

Gade, K. (2010). A Nonsingular Horizontal Position Representation, The Journal
of Navigation, Volume 63, Issue 03, pp 395-417, July 2010.
(www.navlab.net/Publications/A_Nonsingular_Horizontal_Position_Representation.pdf)

This paper should be cited in publications using this library.

Copyright (c) 2015, Norwegian Defence Research Establishment (FFI)
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above publication
information, copyright notice, this list of conditions and the following
disclaimer.

2. Redistributions in binary form must reproduce the above publication
information, copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided with the
distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.

================================================
FILE: MANIFEST.in
================================================
include README.md
include LICENSE

================================================
FILE: Makefile
================================================

.PHONY: test3
test3:
	@#export PYTHONPATH=.;./venv/bin/py.test --ignore=venv -vv
	python3 setup.py test

.PHONY: test2
test2:
	python2 setup.py test

.PHONY: test
test: test3 test2

.PHONY: version
version:
	@python3 setup.py --version

.PHONY: prepare_dist
prepare_dist:
	rm -rf dist/*
	python3 setup.py sdist bdist_wheel

.PHONY: prepare_tag
prepare_tag:
	@echo "Check whether repo is clean"
	git diff-index --quiet HEAD
	@echo "Check correct branch"
	if [[ "$$(git rev-parse --abbrev-ref HEAD)" != "master" ]]; then echo 'Not master branch'; exit 1; fi
	@echo "Add tag"
	git tag "v$$(python3 setup.py --version)"
	git push --tags

.PHONY: deploy
deploy: prepare_dist prepare_tag
	@echo "Check whether repo is clean"
	git diff-index --quiet HEAD
	@echo "Start uploading"
	twine upload --repository leuvenmapmatching dist/*

.PHONY: docs
docs:
	export PYTHONPATH=..; cd docs; make html

.PHONY: docsclean
docsclean:
	cd docs; make clean

.PHONY: clean
clean: docsclean



================================================
FILE: README.md
================================================
# Leuven.MapMatching

[![PyPi Version](https://img.shields.io/pypi/v/leuvenmapmatching.svg)](https://pypi.org/project/leuvenmapmatching/)
[![Documentation Status](https://readthedocs.org/projects/leuvenmapmatching/badge/?version=latest)](https://leuvenmapmatching.readthedocs.io/en/latest/?badge=latest)


Align a trace of GPS measurements to a map or road segments.

The matching is based on a Hidden Markov Model (HMM) with non-emitting 
states. The model can deal with missing data and you can plug in custom
transition and emission probability distributions.

![example](http://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/example1.png?v=2)

Main reference:

> Meert Wannes, Mathias Verbeke, "HMM with Non-Emitting States for Map Matching",
> European Conference on Data Analysis (ECDA), Paderborn, Germany, 2018.

Other references:

> Devos Laurens, Vandebril Raf (supervisor), Meert Wannes (supervisor),
> "Traffic patterns revealed through matrix functions and map matching",
> Master thesis, Faculty of Engineering Science, KU Leuven, 2018

## Installation and usage

    $ pip install leuvenmapmatching

More information and examples:

[leuvenmapmatching.readthedocs.io](https://leuvenmapmatching.readthedocs.io)

## Dependencies

Required:

- [numpy](http://www.numpy.org)
- [scipy](https://www.scipy.org)


Optional (only loaded when methods are called to rely on these packages):

- [matplotlib](http://matplotlib.org):
    For visualisation
- [smopy](https://github.com/rossant/smopy):
    For visualisation
- [nvector](https://github.com/pbrod/Nvector):
    For latitude-longitude computations
- [gpxpy](https://github.com/tkrajina/gpxpy):
    To import GPX files
- [pykalman](https://pykalman.github.io):
    So smooth paths using a Kalman filter
- [pyproj](https://jswhit.github.io/pyproj/):
    To project latitude-longitude coordinates to an XY-plane
- [rtree](http://toblerity.org/rtree/):
    To quickly search locations


## Contact

Wannes Meert, DTAI, KU Leuven  
wannes.meert@cs.kuleuven.be  
https://dtai.cs.kuleuven.be

Mathias Verbeke, Sirris  
mathias.verbeke@sirris.be  
http://www.sirris.be/expertise/data-innovation

Developed with the support of [Elucidata.be](http://www.elucidata.be).


## License

Copyright 2015-2022, KU Leuven - DTAI Research Group, Sirris - Elucidata Group  
Apache License, Version 2.0.


================================================
FILE: docs/Makefile
================================================
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line.
SPHINXOPTS    =
SPHINXBUILD   = sphinx-build
SPHINXPROJ    = DTAIMap-Matching
SOURCEDIR     = .
BUILDDIR      = _build

# Put it first so that "make" without argument is like "make help".
help:
	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

================================================
FILE: docs/classes/map/BaseMap.rst
================================================
BaseMap
=======


.. autoclass:: leuvenmapmatching.map.base.BaseMap
   :members:


================================================
FILE: docs/classes/map/InMemMap.rst
================================================
InMemMap
========


.. autoclass:: leuvenmapmatching.map.inmem.InMemMap
   :members:


================================================
FILE: docs/classes/map/SqliteMap.rst
================================================
SqliteMap
=========


.. autoclass:: leuvenmapmatching.map.sqlite.SqliteMap
   :members:


================================================
FILE: docs/classes/matcher/BaseMatcher.rst
================================================
BaseMatcher
===========

This a generic base class to be used by matchers. This class itself
does not implement a working matcher. Use a matcher such as
``SimpleMatcher``, ``DistanceMatcher``, ...

.. autoclass:: leuvenmapmatching.matcher.base.BaseMatcher
   :members:


================================================
FILE: docs/classes/matcher/BaseMatching.rst
================================================
BaseMatching
============


.. autoclass:: leuvenmapmatching.matcher.base.BaseMatching
   :members:


================================================
FILE: docs/classes/matcher/DistanceMatcher.rst
================================================
DistanceMatcher
===============


.. autoclass:: leuvenmapmatching.matcher.distance.DistanceMatcher
   :members:


================================================
FILE: docs/classes/matcher/SimpleMatcher.rst
================================================
SimpleMatcher
=============


.. autoclass:: leuvenmapmatching.matcher.simple.SimpleMatcher
   :members:


================================================
FILE: docs/classes/overview.rst
================================================


matcher
~~~~~~~

.. toctree::
   :caption: Matcher

   matcher/BaseMatcher
   matcher/SimpleMatcher
   matcher/DistanceMatcher

.. toctree::
   :caption: Matching

   matcher/BaseMatching



map
~~~

.. toctree::
   :caption: Map

   map/BaseMap
   map/InMemMap
   map/SqliteMap



util
~~~~

.. toctree::
   :caption: Util

   util/Segment


================================================
FILE: docs/classes/util/Segment.rst
================================================
Segment
=======


.. autoclass:: leuvenmapmatching.util.segment.Segment
   :members:


================================================
FILE: docs/conf.py
================================================
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Leuven.MapMatching documentation build configuration file, created by
# sphinx-quickstart on Sat Apr 14 23:24:31 2018.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('..'))


# -- General configuration ------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.autodoc',
    'sphinx.ext.mathjax',
    'sphinx.ext.viewcode']

autoclass_content = 'both'

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'

# The master toctree document.
master_doc = 'index'

# General information about the project.
project = 'Leuven.MapMatching'
copyright = '2018-2022, Wannes Meert'
author = 'Wannes Meert'

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.1.1'
# The full version, including alpha/beta/rc tags.
release = '1.1.1'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'en'

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'

# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False


# -- Options for HTML output ----------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
# html_theme = 'alabaster'
html_theme = "sphinx_rtd_theme"

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']


# -- Options for HTMLHelp output ------------------------------------------

# Output file base name for HTML help builder.
htmlhelp_basename = 'LeuvenMapMatchingDoc'


# -- Options for LaTeX output ---------------------------------------------

latex_elements = {
    # The paper size ('letterpaper' or 'a4paper').
    #
    # 'papersize': 'letterpaper',

    # The font size ('10pt', '11pt' or '12pt').
    #
    # 'pointsize': '10pt',

    # Additional stuff for the LaTeX preamble.
    #
    # 'preamble': '',

    # Latex figure (float) alignment
    #
    # 'figure_align': 'htbp',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
#  author, documentclass [howto, manual, or own class]).
latex_documents = [
    (master_doc, 'LeuvenMapMatching.tex', 'Leuven.MapMatching Documentation',
     'Wannes Meert', 'manual'),
]


# -- Options for manual page output ---------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
    (master_doc, 'leuvenmapmatching', 'Leuven.MapMatching Documentation',
     [author], 1)
]


# -- Options for Texinfo output -------------------------------------------

# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
#  dir menu entry, description, category)
texinfo_documents = [
    (master_doc, 'LeuvenMapMatching', 'Leuven.MapMatching Documentation',
     author, 'LeuvenMapMatching', 'Map Matching',
     'Miscellaneous'),
]





================================================
FILE: docs/index.rst
================================================
.. Leuven.MapMatching documentation master file, created by
   sphinx-quickstart on Sat Apr 14 23:24:31 2018.
   You can adapt this file completely to your liking, but it should at least
   contain the root `toctree` directive.

Leuven.MapMatching's documentation
==================================

Align a trace of coordinates (e.g. GPS measurements) to a map of road segments.

The matching is based on a Hidden Markov Model (HMM) with non-emitting
states. The model can deal with missing data and you can plug in custom
transition and emission probability distributions.

Reference:

   Meert Wannes, Mathias Verbeke, "HMM with Non-Emitting States for Map Matching",
   European Conference on Data Analysis (ECDA), Paderborn, Germany, 2018.


.. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/example1.png?v=2
   :alt: example


.. toctree::
   :maxdepth: 2
   :caption: Contents:


.. toctree::
   :caption: Usage


   usage/installation
   usage/introduction
   usage/openstreetmap
   usage/visualisation
   usage/latitudelongitude
   usage/customdistributions
   usage/incremental
   usage/debug


.. toctree::
   :maxdepth: 2
   :caption: Classes

   classes/overview


Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`


================================================
FILE: docs/make.bat
================================================
@ECHO OFF

pushd %~dp0

REM Command file for Sphinx documentation

if "%SPHINXBUILD%" == "" (
	set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set SPHINXPROJ=DTAIMap-Matching

if "%1" == "" goto help

%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
	echo.
	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
	echo.installed, then set the SPHINXBUILD environment variable to point
	echo.to the full path of the 'sphinx-build' executable. Alternatively you
	echo.may add the Sphinx directory to PATH.
	echo.
	echo.If you don't have Sphinx installed, grab it from
	echo.http://sphinx-doc.org/
	exit /b 1
)

%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end

:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%

:end
popd


================================================
FILE: docs/requirements.txt
================================================
numpy
scipy
sphinx_rtd_theme

================================================
FILE: docs/usage/customdistributions.rst
================================================
Custom probability distributions
================================

You can use your own custom probability distributions for the transition and emission probabilities.
This is achieved by inheriting from the :class:`BaseMatcher` class.

Examples are available in the :class:`SimpleMatching` class and :class:`DistanceMatching` class.
The latter implements a variation based on Newson and Krumm (2009).

Transition probability distribution
-----------------------------------

Overwrite the :meth:`logprob_trans` method.

For example, if you want to use a uniform distribution over the possible road segments:

.. code-block:: python

   def logprob_trans(self, prev_m, edge_m, edge_o, is_prev_ne, is_next_ne):
       return -math.log(len(self.matcher.map.nodes_nbrto(self.edge_m.last_point())))

Note that ``prev_m.edge_m`` and ``edge_m`` are not necessarily connected. For example if the ``Map`` object
returns a neighbor state that is not connected in the roadmap. This functionality is used to allow switching lanes.


Emission probability distribution
---------------------------------

Overwrite the :meth:`logprob_obs` method for non-emitting nodes.
These methods are given the closest distance as `dist`, the previous :class:`Matching` object
in the lattice, the state as `edge_m`, and the observation as `edge_o`. The latter two are :class:`Segment` objects
that can represent either a segment or a point.
Each segment also has a project point which is the point on the segment that is the closest point.

For example, a simple step function with more tolerance for non-emitting nodes:

.. code-block:: python

   def logprob_obs(self, dist, prev_m, new_edge_m, new_edge_o, is_ne):
       if is_ne:
           if dist < 50:
               return -math.log(50)
       else:
           if dist < 10:
               return -math.log(10)
       return -np.inf

Note that an emission probability can be given for a non-emitting node. This allows you to rank non-emitting nodes
even when no observations are available. It will then insert pseudo-observations on the line between the previous
and next observations.
To have a pure non-emitting node, the `logprob_obs` method should always return 0 if the
``is_ne`` argument is true.


Custom lattice objects
----------------------

If you need to store additional information in the lattice, inherit from the :class:`Matching` class and
pass your custom object to the :class:`Matcher` object.

.. code-block:: python

   from leuvenmapmatching.map.base import BaseMatching

   class MyMatching(BaseMatching):
       ...

   matcher = MyMatcher(mapdb, matching=MyMatching)



================================================
FILE: docs/usage/debug.rst
================================================
Debug
=====

Increasing the verbosity level
------------------------------

To inspect the intermediate steps that the algorithm take, you can increase
the verbosity level of the package. For example:

.. code-block:: python

    import sys
    import logging
    import leuvenmapmatching
    logger = leuvenmapmatching.logger

    logger.setLevel(logging.DEBUG)
    logger.addHandler(logging.StreamHandler(sys.stdout))


Inspect the best matching
-------------------------

The best match is available in ``matcher.lattice_best``. This is a list of
``Matching`` objects. For example after running the first example in the introduction:

.. code-block:: python

    >>> matcher.lattice_best
    [Matching<A-B-0-0>,
     Matching<A-B-1-0>,
     Matching<A-B-2-0>,
    ...

A matching object summarizes its information as a tuple with three values if
the best match is with a vertex: <label-observation-nonemitting>. And a tuple
with four values if the best match is with an edge: <labelstart-labelend-observation-nonemitting>.

In the example above, the first observation (with index 0) is matched to a point on the edge
A-B. If you want to inspect the exact locations, you can query the ``Segment``
objects that express the observation and map: ``matching.edge_o`` and ``matching.edge_m``.

.. code-block:: python

   >>> match = matcher.lattice_best[0]
   >>> match.edge_m.l1, match.edge_m.l2  # Edge start/end labels
   ('A', 'B')
   >>> match.edge_m.pi  # Best point on A-B edge
   (1.0, 1.0)
   >>> match.edge_m.p1, match.edge_m.p2  # Locations of A and B
   ((1, 1), (1, 3))
   >>> match.edge_o.l1, match.edge_o.l2  # Observation
   ('O0', None)
   >>> match.edge_o.pi  # Location of observation O0, because no second location
   (0.8, 0.7)
   >>> match.edge_o.p1  # Same as pi because no interpolation
   (0.8, 0.7)

Inspect the matching lattice
----------------------------

All paths through the lattice are available in ``matcher.lattice``.
The lattice is a dictionary with a ``LatticeColumn`` object for each observation
(in case the full path of observations is matched).

For each observation, you can inspect the ``Matching`` objects with:

.. code-block:: python

    >>> matcher.lattice
    {0: <leuvenmapmatching.matcher.base.LatticeColumn at 0x12369bf40>,
     1: <leuvenmapmatching.matcher.base.LatticeColumn at 0x123639dc0>,
     2: <leuvenmapmatching.matcher.base.LatticeColumn at 0x123603f40>,
     ...
    >>> matcher.lattice[0].values_all()
    {Matching<A-B-0-0>,
     Matching<A-B-0-1>,
     Matching<A-C-0-0>,
     ...

To start backtracking you can, for example, see which matching object
for the last element has the highest probability (thus the best match):

.. code-block:: python

    >>> m = max(matcher.lattice[len(path)-1].values_all(), key=lambda m: m.logprob)
    >>> m.logprob
    -0.6835815469734807

The previous matching objects can be queried with. These are only those
matches that are connected to this matchin the lattice (in this case
nodes in the street graph with an edge to the current node):

.. code-block:: python

    >>> m.prev  # Best previous match with a connection (multiple if equal probability)
    {Matching<E-F-20-0>}
    >>> m.prev_other  # All previous matches in the lattice with a connection
    {Matching<C-E-20-0>,
     Matching<D-E-20-0>,
     Matching<F-E-20-0>,
     Matching<Y-E-20-0>}


================================================
FILE: docs/usage/incremental.rst
================================================
Incremental matching
====================

Example: Incremental matching
-------------------------------

If the observations are collected in a streaming setting. The matching can also be invoked incrementally.
The lattice will be built further every time a new subsequence of the path is given.

.. code-block:: python

    from leuvenmapmatching.matcher.distance import DistanceMatcher
    from leuvenmapmatching.map.inmemmap import InMemMap

    map_con = InMemMap("mymap", graph={
        "A": ((1, 1), ["B", "C", "X"]),
        "B": ((1, 3), ["A", "C", "D", "K"]),
        "C": ((2, 2), ["A", "B", "D", "E", "X", "Y"]),
        "D": ((2, 4), ["B", "C", "D", "E", "K", "L"]),
        "E": ((3, 3), ["C", "D", "F", "Y"]),
        "F": ((3, 5), ["D", "E", "L"]),
        "X": ((2, 0), ["A", "C", "Y"]),
        "Y": ((3, 1), ["X", "C", "E"]),
        "K": ((1, 5), ["B", "D", "L"]),
        "L": ((2, 6), ["K", "D", "F"])
    }, use_latlon=False)

    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),
            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),
            (2.3, 3.5), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2),
            (3.1, 3.8), (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]

    matcher = DistanceMatcher(map_con, max_dist=2, obs_noise=1, min_prob_norm=0.5)
    states, _ = matcher.match(path[:5])
    states, _ = matcher.match(path, expand=True)
    nodes = matcher.path_pred_onlynodes

    print("States\n------")
    print(states)
    print("Nodes\n------")
    print(nodes)
    print("")
    matcher.print_lattice_stats()




================================================
FILE: docs/usage/installation.rst
================================================
Installation
============

Dependencies
------------

Required:

-  `numpy <http://www.numpy.org>`__
-  `scipy <https://www.scipy.org>`__

Optional (only loaded when methods are called that rely on these packages):

-  `rtree <http://toblerity.org/rtree/>`__
-  `nvector <https://github.com/pbrod/Nvector>`__
-  `gpxpy <https://github.com/tkrajina/gpxpy>`__
-  `pyproj <https://jswhit.github.io/pyproj/>`__
-  `pykalman <https://pykalman.github.io>`__
-  `matplotlib <http://matplotlib.org>`__
-  `smopy <https://github.com/rossant/smopy>`__


Using pip
---------

If you want to install the latest released version using pip:

::

    $ pip install leuvenmapmatching

If you want to install the latest non-released version (add ``@develop``) for the
latest development version:

::

    $ pip install git+https://github.com/wannesm/leuvenmapmatching


From source
-----------

The library can also be compiled and/or installed directly from source.

* Download the source from https://github.com/wannesm/leuvenmapmatching
* To compile and install in your site-package directory: ``python3 setup.py install``



================================================
FILE: docs/usage/introduction.rst
================================================
Examples
========

Example 1: Simple
-----------------

A first, simple example. Some parameters are given to tune the algorithm.
The ``max_dist`` and ``obs_noise`` are distances that indicate the maximal distance between observation and road
segment and the expected noise in the measurements, respectively.
The ``min_prob_norm`` prunes the lattice in that it drops paths that drop below 0.5 normalized probability.
The probability is normalized to allow for easier reasoning about the probability of a path.
It is computed as the exponential smoothed log probability components instead of the sum as would be the case
for log likelihood.
Because the number of possible paths quickly grows, it is recommended to set the
``max_lattice_width`` argument to speed up the algorithm (available from version 1.0 onwards).
It will only continue the search with this number of possible paths at every step. If no solution is found,
this value can be incremented using the ``increase_max_lattice_width`` method.

.. code-block:: python

    from leuvenmapmatching.matcher.distance import DistanceMatcher
    from leuvenmapmatching.map.inmem import InMemMap

    map_con = InMemMap("mymap", graph={
        "A": ((1, 1), ["B", "C", "X"]),
        "B": ((1, 3), ["A", "C", "D", "K"]),
        "C": ((2, 2), ["A", "B", "D", "E", "X", "Y"]),
        "D": ((2, 4), ["B", "C", "F", "E", "K", "L"]),
        "E": ((3, 3), ["C", "D", "F", "Y"]),
        "F": ((3, 5), ["D", "E", "L"]),
        "X": ((2, 0), ["A", "C", "Y"]),
        "Y": ((3, 1), ["X", "C", "E"]),
        "K": ((1, 5), ["B", "D", "L"]),
        "L": ((2, 6), ["K", "D", "F"])
    }, use_latlon=False)

    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),
            (1.1, 2.3), (1.3, 2.9), (1.2, 3.1), (1.5, 3.2), (1.8, 3.5), (2.0, 3.7),
            (2.3, 3.5), (2.4, 3.2), (2.6, 3.1), (2.9, 3.1), (3.0, 3.2),
            (3.1, 3.8), (3.0, 4.0), (3.1, 4.3), (3.1, 4.6), (3.0, 4.9)]

    matcher = DistanceMatcher(map_con, max_dist=2, obs_noise=1, min_prob_norm=0.5, max_lattice_width=5)
    states, _ = matcher.match(path)
    nodes = matcher.path_pred_onlynodes

    print("States\n------")
    print(states)
    print("Nodes\n------")
    print(nodes)
    print("")
    matcher.print_lattice_stats()


Example 2: Non-emitting states
------------------------------

In case there are less observations that states (an assumption of HMMs), non-emittings states allow you
to deal with this. States will be inserted that are not associated with any of the given observations if
this improves the probability of the path.

It is possible to also associate a distribtion over the distance between observations and the non-emitting
states (`obs_noise_ne`). This allows the algorithm to prefer nearby road segments. This value should be
larger than `obs_noise` as it is mapped to the line between the previous and next observation, which does
not necessarily run over the relevant segment. Setting this to infinity is the same as using pure
non-emitting states that ignore observations completely.

.. code-block:: python

    from leuvenmapmatching.matcher.distance import DistanceMatcher
    from leuvenmapmatching.map.inmem import InMemMap
    from leuvenmapmatching import visualization as mmviz

    path = [(1, 0), (7.5, 0.65), (10.1, 1.9)]
    mapdb = InMemMap("mymap", graph={
        "A": ((1, 0.00), ["B"]),
        "B": ((3, 0.00), ["A", "C"]),
        "C": ((4, 0.70), ["B", "D"]),
        "D": ((5, 1.00), ["C", "E"]),
        "E": ((6, 1.00), ["D", "F"]),
        "F": ((7, 0.70), ["E", "G"]),
        "G": ((8, 0.00), ["F", "H"]),
        "H": ((10, 0.0), ["G", "I"]),
        "I": ((10, 2.0), ["H"])
    }, use_latlon=False)
    matcher = DistanceMatcher(mapdb, max_dist_init=0.2, obs_noise=1, obs_noise_ne=10,
                              non_emitting_states=True, only_edges=True , max_lattice_width=5)
    states, _ = matcher.match(path)
    nodes = matcher.path_pred_onlynodes

    print("States\n------")
    print(states)
    print("Nodes\n------")
    print(nodes)
    print("")
    matcher.print_lattice_stats()

    mmviz.plot_map(mapdb, matcher=matcher,
                  show_labels=True, show_matching=True
                  filename="output.png"))


================================================
FILE: docs/usage/latitudelongitude.rst
================================================
Dealing with Latitude-Longitude
===============================

The toolbox can deal with latitude-longitude coordinates directly.
Map matching, however, requires a lot of repeated computations between points and latitude-longitude
computations will be more expensive than Euclidean distances.

There are three different options how you can handle latitude-longitude coordinates:

Option 1: Use Latitude-Longitude directly
-----------------------------------------

Set the ``use_latlon`` flag in the :class:`Map` to true.

For example to read in an OpenStreetMap file directly to a :class:`InMemMap` object:

.. code-block:: python

    from leuvenmapmatching.map.inmem import InMemMap

    map_con = InMemMap("myosm", use_latlon=True)

    for entity in osmread.parse_file(osm_fn):
        if isinstance(entity, osmread.Way) and 'highway' in entity.tags:
            for node_a, node_b in zip(entity.nodes, entity.nodes[1:]):
                map_con.add_edge(node_a, node_b)
                map_con.add_edge(node_b, node_a)
        if isinstance(entity, osmread.Node):
            map_con.add_node(entity.id, (entity.lat, entity.lon))
    map_con.purge()


Option 2: Project Latitude-Longitude to X-Y
-------------------------------------------

Latitude-Longitude coordinates can be transformed two a frame with two orthogonal axis.

.. code-block:: python

   from leuvenmapmatching.map.inmem import InMemMap

   map_con_latlon = InMemMap("myosm", use_latlon=True)
   # Add edges/nodes
   map_con_xy = map_con_latlon.to_xy()

   route_latlon = []
   # Add GPS locations
   route_xy = [map_con_xy.latlon2yx(latlon) for latlon in route_latlon]


This can also be done directly using the `pyproj <https://github.com/jswhit/pyproj>`_ toolbox.
For example, using the Lambert Conformal projection to project the route GPS coordinates:

.. code-block:: python

   import pyproj

   route = [(4.67878,50.864),(4.68054,50.86381),(4.68098,50.86332),(4.68129,50.86303),(4.6817,50.86284),
            (4.68277,50.86371),(4.68894,50.86895),(4.69344,50.86987),(4.69354,50.86992),(4.69427,50.87157),
            (4.69643,50.87315),(4.69768,50.87552),(4.6997,50.87828)]
   lon_0, lat_0 = route[0]
   proj = pyproj.Proj(f"+proj=merc +ellps=GRS80 +units=m +lon_0={lon_0} +lat_0={lat_0} +lat_ts={lat_0} +no_defs")
   xs, ys = [], []
   for lon, lat in route:
       x, y = proj(lon, lat)
       xs.append(x)
       ys.append(y)


Notice that the pyproj package uses the convention to express coordinates as x-y which is
longitude-latitude because it is defined this way in the CRS definitions while the Leuven.MapMatching
toolbox follows the ISO 6709 standard and expresses coordinates as latitude-longitude. If you
want ``pyproj`` to use latitude-longitude you can use set the
`axisswap option <https://proj4.org/operations/conversions/axisswap.html>`_.

If you want to define both the from and to projections:

.. code-block:: python

   import pyproj

   route = [(4.67878,50.864),(4.68054,50.86381),(4.68098,50.86332),(4.68129,50.86303),(4.6817,50.86284),
            (4.68277,50.86371),(4.68894,50.86895),(4.69344,50.86987),(4.69354,50.86992),(4.69427,50.87157),
            (4.69643,50.87315),(4.69768,50.87552),(4.6997,50.87828)]
   p1 = pyproj.Proj(proj='latlon', datum='WGS84')
   p2 = pyproj.Proj(proj='utm', datum='WGS84')
   xs, ys = [], []
   for lon, lat in route:
       x, y = pyproj.transform(lon, lat)
       xs.append(x)
       ys.append(y)


Option 3: Use Latitude-Longitude as if they are X-Y points
----------------------------------------------------------

A naive solution would be to use latitude-longitude coordinate pairs as if they are X-Y coordinates.
For small distances, far away from the poles and not crossing the dateline, this option might work.
But it is not adviced.

For example, for long distances the error is quite large. In the image beneath, the blue line is the computation
of the intersection using latitude-longitude while the red line is the intersection using Eucludean distances.

.. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/latlon_mismatch_1.png?v=1
   :alt: Latitude-Longitude mismatch

.. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/latlon_mismatch_2.png?v=1
   :alt: Latitude-Longitude mismatch detail


================================================
FILE: docs/usage/openstreetmap.rst
================================================
Map from OpenStreetMap
======================

You can download a graph for map-matching from the OpenStreetMap.org service.
Multiple methods exists, we illustrate two.

Using requests, osmread and gpx
-------------------------------

You can perform map matching on a OpenStreetMap database by combing ``leuvenmapmatching``
with the packages ``requests``, ``osmread`` and ``gpx``.

Download a map as XML
~~~~~~~~~~~~~~~~~~~~~

You can use the overpass-api.de service:

.. code-block:: python

    from pathlib import Path
    import requests
    xml_file = Path(".") / "osm.xml"
    url = 'http://overpass-api.de/api/map?bbox=4.694933,50.870047,4.709256000000001,50.879628'
    r = requests.get(url, stream=True)
    with xml_file.open('wb') as ofile:
        for chunk in r.iter_content(chunk_size=1024):
            if chunk:
                ofile.write(chunk)


Create graph using osmread
~~~~~~~~~~~~~~~~~~~~~~~~~~

Once we have a file containing the region we are interested in, we can select the roads we want to use
to create a graph from. In this case we focus on 'ways' with a 'highway' tag. Those represent a variety
of roads. For a more detailed filtering look at the
`possible values of the highway tag <https://wiki.openstreetmap.org/wiki/Key:highway>`_.

.. code-block:: python

    from leuvenmapmatching.map.inmem import InMemMap
    import osmread

    map_con = InMemMap("myosm", use_latlon=True, use_rtree=True, index_edges=True)
    for entity in osmread.parse_file(str(xml_file)):
        if isinstance(entity, osmread.Way) and 'highway' in entity.tags:
            for node_a, node_b in zip(entity.nodes, entity.nodes[1:]):
                map_con.add_edge(node_a, node_b)
                # Some roads are one-way. We'll add both directions.
                map_con.add_edge(node_b, node_a)
        if isinstance(entity, osmread.Node):
            map_con.add_node(entity.id, (entity.lat, entity.lon))
    map_con.purge()


Note that ``InMemMap`` is a simple container for a map. It is recommended to use
your own optimized connecter to your map dataset.

If you want to allow transitions that are not following the exact road segments you can inherit from the ``Map``
class and define a new class with your own transitions.
The transitions are defined using the ``nodes_nbrto`` and ``edges_nbrt`` methods.


Perform map matching on an OpenStreetMap database
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You can create a list of latitude-longitude coordinates manually. Or read a gpx file.

.. code-block:: python

    from leuvenmapmatching.util.gpx import gpx_to_path

    track = gpx_to_path("mytrack.gpx")
    matcher = DistanceMatcher(map_con,
                             max_dist=100, max_dist_init=25,  # meter
                             min_prob_norm=0.001,
                             non_emitting_length_factor=0.75,
                             obs_noise=50, obs_noise_ne=75,  # meter
                             dist_noise=50,  # meter
                             non_emitting_states=True,
                             max_lattice_width=5)
    states, lastidx = matcher.match(track)


Using osmnx and geopandas
-------------------------

Another great library to interact with OpenStreetMap data is the `osmnx <https://github.com/gboeing/osmnx>`_ package.
The osmnx package can retrieve relevant data automatically, for example when given a name of a region.
This package is build on top of the `geopandas <http://geopandas.org>`_ package.

.. code-block:: python

    import osmnx
    graph = ox.graph_from_place('Leuven, Belgium', network_type='drive', simplify=False)
    graph_proj = ox.project_graph(graph)
    
    # Create GeoDataFrames (gdfs)
    # Approach 1
    nodes_proj, edges_proj = ox.graph_to_gdfs(graph_proj, nodes=True, edges=True)
    for nid, row in nodes_proj[['x', 'y']].iterrows():
        map_con.add_node(nid, (row['x'], row['y']))
    for eid, _ in edges_proj.iterrows():
        map_con.add_edge(eid[0], eid[1])
    
    # Approach 2
    nodes, edges = ox.graph_to_gdfs(graph_proj, nodes=True, edges=True)
    nodes_proj = nodes.to_crs("EPSG:3395")
    edges_proj = edges.to_crs("EPSG:3395")
    for nid, row in nodes_proj.iterrows():
        map_con.add_node(nid, (row['lat'], row['lon']))
    # We can also extract edges also directly from networkx graph
    for nid1, nid2, _ in graph.edges:
        map_con.add_edge(nid1, nid2)




================================================
FILE: docs/usage/visualisation.rst
================================================
Visualisation
=============

To inspect the results, a plotting function is included.

Simple plotting
---------------

To plot the graph in a matplotlib figure use:

.. code-block:: python

    from leuvenmapmatching import visualization as mmviz
    mmviz.plot_map(map_con, matcher=matcher,
                   show_labels=True, show_matching=True, show_graph=True,
                   filename="my_plot.png")

This will result in the following figure:

.. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/plot1.png?v=1
   :alt: Plot1

You can also define your own figure by passing a matplotlib axis object:

.. code-block:: python

    fig, ax = plt.subplots(1, 1)
    mmviz.plot_map(map_con, matcher=matcher,
                   ax=ax,
                   show_labels=True, show_matching=True, show_graph=True,
                   filename="my_plot.png")


Plotting with an OpenStreetMap background
-----------------------------------------

The plotting function also supports a link with the ``smopy`` package.
Set the ``use_osm`` argument to true and pass a map that is defined with
latitude-longitude (thus ``use_latlon=True``).

You can set ``zoom_path`` to true to only see the relevant part and not the
entire map that is available in the map. Alternatively you can also set the
bounding box manually using the ``bb`` argument.

.. code-block:: python

    mm_viz.plot_map(map_con, matcher=matcher,
                    use_osm=True, zoom_path=True,
                    show_labels=False, show_matching=True, show_graph=False,
                    filename="my_osm_plot.png")


This will result in the following figure:

.. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/plot2.png?v=1
   :alt: Plot2

Or when some GPS points are missing in the track, the matching is more
visible as the matched route deviates from the straight line between two
GPS points:

.. figure:: https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/plot3.png?v=1
   :alt: Plot3


================================================
FILE: leuvenmapmatching/__init__.py
================================================
# encoding: utf-8
"""
leuvenmapmatching
~~~~~~~~~~~~~~~~~

:author: Wannes Meert
:copyright: Copyright 2015-2022 DTAI, KU Leuven and Sirris.
:license: Apache License, Version 2.0, see LICENSE for details.
"""
import logging
from . import map, matcher, util
# visualization is not loaded by default (avoid loading unnecessary dependencies such as matplotlib).

logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching")

__version__ = '1.1.4'



================================================
FILE: leuvenmapmatching/map/__init__.py
================================================
# encoding: utf-8
"""
leuvenmapmatching.map
~~~~~~~~~~~~~~~~~~~~~

:author: Wannes Meert
:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.
:license: Apache License, Version 2.0, see LICENSE for details.
"""


================================================
FILE: leuvenmapmatching/map/base.py
================================================
# encoding: utf-8
"""
leuvenmapmatching.map.base
~~~~~~~~~~~~~~~~~~~~~~~~~~

Base Map class.

To be used in a Matcher object, the following functions need to be defined:

- ``edges_closeto``
- ``nodes_closeto``
- ``nodes_nbrto``
- ``edges_nbrto``

For visualiation purposes the following methods need to be implemented:

- ``bb``
- ``labels``
- ``size``
- ``coordinates``
- ``node_coordinates``
- ``all_edges``
- ``all_nodes``

:author: Wannes Meert
:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.
:license: Apache License, Version 2.0, see LICENSE for details.
"""

from abc import abstractmethod
import logging
MYPY = False
if MYPY:
    from typing import Tuple, Union, List
    LabelType = Union[int, str]
    LocType = Tuple[float, float]


logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching")


class BaseMap(object):
    """Abstract class for a Map."""

    def __init__(self, name, use_latlon=True):
        """Simple database wrapper/stub."""
        self.name = name
        self._use_latlon = None
        self.distance = None
        self.distance_point_to_segment = None
        self.distance_segment_to_segment = None
        self.box_around_point = None
        self.use_latlon = use_latlon

    @property
    def use_latlon(self):
        return self._use_latlon

    @use_latlon.setter
    def use_latlon(self, value):
        self._use_latlon = value
        if self._use_latlon:
            from ..util import dist_latlon as dist_lib
        else:
            from ..util import dist_euclidean as dist_lib
        self.distance = dist_lib.distance
        self.distance_point_to_segment = dist_lib.distance_point_to_segment
        self.distance_segment_to_segment = dist_lib.distance_segment_to_segment
        self.box_around_point = dist_lib.box_around_point
        self.lines_parallel = dist_lib.lines_parallel

    @abstractmethod
    def bb(self):
        """Bounding box.

        :return: (lat_min, lon_min, lat_max, lon_max)
        """

    @abstractmethod
    def labels(self):
        """Labels of all nodes."""

    @abstractmethod
    def size(self):
        """Number of nodes."""

    @abstractmethod
    def node_coordinates(self, node_key):
        """Coordinates for given node key."""

    @abstractmethod
    def edges_closeto(self, loc, max_dist=None, max_elmt=None):
        """Find edges close to a certain location.

        :param loc: Latitude, Longitude
        :param max_dist: Maximal distance that returned nodes can be from lat-lon
        :param max_elmt: Maximal number of elements returned after sorting according to distance.
        :return: list[tuple[dist, label, loc]]
        """
        return None

    @abstractmethod
    def nodes_closeto(self, loc, max_dist=None, max_elmt=None):
        """Find nodes close to a certain location.

        :param loc: Latitude, Longitude
        :param max_dist: Maximal distance that returned nodes can be from lat-lon
        :param max_elmt: Maximal number of elements returned after sorting according to distance.
        :return: list[tuple[dist, label, loc]]
        """
        return None

    @abstractmethod
    def nodes_nbrto(self, node):
        # type: (BaseMap, LabelType) -> List[Tuple[LabelType, LocType]]
        """Return all nodes that are linked to ``node``.

        :param node: Node identifier
        :return: list[tuple[label, loc]]
        """
        return []

    def edges_nbrto(self, edge):
        # type: (BaseMap, Tuple[LabelType, LabelType]) -> List[Tuple[LabelType, LocType, LabelType, LocType]]
        """Return all edges that are linked to ``edge``.

        Defaults to ``nodes_nbrto``.

        :param edge: Edge identifier
        :return: list[tuple[label1, label2, loc1, loc2]]
        """
        results = []
        l1, l2 = edge
        p2 = self.node_coordinates(l2)
        for l3, p3 in self.nodes_nbrto(l2):
            results.append((l2, p2, l3, p3))
        return results

    @abstractmethod
    def all_nodes(self, bb=None):
        """All node keys and coordinates.

        :return: [(key, (lat, lon))]
        """

    @abstractmethod
    def all_edges(self, bb=None):
        """All edges.

        :return: [(key_a, loc_a, key_b, loc_b)]
        """


================================================
FILE: leuvenmapmatching/map/inmem.py
================================================
# encoding: utf-8
"""
leuvenmapmatching.map.inmem
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Simple in-memory map representation. Not suited for production purposes.
Write your own map class that connects to your map (e.g. a database instance).

:author: Wannes Meert
:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.
:license: Apache License, Version 2.0, see LICENSE for details.
"""
import logging
import time
from pathlib import Path
import pickle
from functools import partial
try:
    import rtree
except ImportError:
    rtree = None
try:
    import pyproj
except ImportError:
    pyproj = None
try:
    import tqdm
except ImportError:
    tqdm = None


from .base import BaseMap


MYPY = False
if MYPY:
    from typing import Optional, Set, Tuple, Dict, Union
    LabelType = Union[int, str]
    LocType = Tuple[float, float]
    EdgeType = Tuple[LabelType, LabelType]


logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching")


class InMemMap(BaseMap):
    def __init__(self, name, use_latlon=True, use_rtree=False, index_edges=False,
                 crs_lonlat=None, crs_xy=None, graph=None, linked_edges=None, dir=None, deserializing=False):
        """In-memory representation of a map.

        This is a simple database-like object to perform experiments with map matching.
        For production purposes it is recommended to use your own derived
        class (e.g. to connect to your database instance).

        This class supports:

        - Indexing using rtrees to allow for fast searching of points on the map.
          When using the rtree index, only integer numbers are allowed as node labels.
        - Serializing to write and read from files.
        - Projecting points to a different frame (e.g. GPS to Lambert)

        :param name: Map name (mandatory)
        :param use_latlon: The locations represent latitude-longitude pairs, otherwise y-x coordinates
            are assumed.
        :param use_rtree: Build an rtree index to quickly search for locations.
        :param index_edges: Build an index for the edges in the map instead of the vertices.
        :param crs_lonlat: Coordinate reference system for the latitude-longitude coordinates.
        :param crs_xy: Coordiante reference system for the y-x coordinates.
        :param graph: Initial graph of form Dict[label, Tuple[Tuple[y,x], List[neighbor]]]]
        :param dir: Directory where to serialize to. If given, the rtree index structure will be written
            to a file immediately.
        :param deserializing: Internal variable to indicate that the object is being build from a file.
        """
        super(InMemMap, self).__init__(name, use_latlon=use_latlon)
        self.dir = None if dir is None else Path(dir)
        self.index_edges = index_edges
        self.graph = dict() if graph is None else graph
        self.rtree = None
        self.use_rtree = use_rtree
        if self.use_rtree:
            self.setup_index(deserializing=deserializing)

        self.crs_lonlat = 'EPSG:4326' if crs_lonlat is None else crs_lonlat  # GPS
        self.crs_xy = 'EPSG:3395' if crs_xy is None else crs_xy  # Mercator projection
        if pyproj:
            # proj_lonlat = pyproj.Proj(self.crs_lonlat, preserve_units=True)
            # proj_xy = pyproj.Proj(self.crs_xy, preserve_units=True)
            # self.lonlat2xy = partial(pyproj.transform, proj_lonlat, proj_xy)
            # self.xy2lonlat = partial(pyproj.transform, proj_xy, proj_lonlat)
            tr_lonlat2xy = pyproj.Transformer.from_crs(self.crs_lonlat, self.crs_xy)
            self.lonlat2xy = tr_lonlat2xy.transform
            tr_xy2lonlat = pyproj.Transformer.from_crs(self.crs_xy, self.crs_lonlat)
            self.xy2lonlat = tr_xy2lonlat.transform
        else:
            def pyproj_notfound(*_args, **_kwargs):
                raise Exception("pyproj package not found")
            self.lonlat2xy = pyproj_notfound
            self.xy2lonlat = pyproj_notfound

        self.linked_edges = linked_edges  # type: Optional[Dict[EdgeType, Set[Tuple[EdgeType]]]]
        self.vertex_label_map = None

    def vertex_label_to_int(self, label, create=False):
        if type(label) is int:
            return label
        if self.vertex_label_map is None:
            if not create:
                return label
            self.vertex_label_map = dict()
        if label in self.vertex_label_map:
            new_label = self.vertex_label_map[label]
        else:
            new_label = len(self.vertex_label_map)
            self.vertex_label_map[label] = new_label
        return new_label

    def vertices_labels_to_int(self):
        graph = dict()
        for label, (loc, nbrs) in self.graph.items():
            new_label = self.vertex_label_to_int(label, create=True)
            new_nbrs = [self.vertex_label_to_int(nbr, create=True) for nbr in nbrs]
            graph[new_label] = (loc, new_nbrs)
        self.graph = graph

    def serialize(self):
        """Create a serializable data structure."""
        data = {
            "name": self.name,
            "graph": self.graph,
            "use_latlon": self.use_latlon,
            "use_rtree": self.use_rtree,
            "index_edges": self.index_edges,
            "crs_lonlat": self.crs_lonlat,
            "crs_xy": self.crs_xy,
            "linked_edges": self.linked_edges
        }
        if self.dir is not None:
            data["dir"] = self.dir
        return data

    @classmethod
    def deserialize(cls, data):
        """Create a new instance from a dictionary."""
        nmap = cls(data["name"], dir=data.get("dir", None),
                   use_latlon=data["use_latlon"], use_rtree=data["use_rtree"],
                   index_edges=data["index_edges"],
                   crs_lonlat=data.get("crs_lonlat", None), crs_xy=data.get("crs_xy", None),
                   graph=data.get("graph", None), linked_edges=data.get("linked_edges", None),
                   deserializing=True)
        return nmap

    def dump(self):
        """Serialize map using pickle.

        All files will be saved to the `dir` directory using the `name` as filename.
        """
        if self.dir is None:
            logger.error(f"No directory set where to save (see InMemMap.__init__)")
            return
        filename = self.dir / (self.name + ".pkl")
        with filename.open("wb") as ofile:
            pickle.dump(self.serialize(), ofile)
        logger.debug(f"Saved map to {filename}")
        if self.rtree:
            rtree_fn = self.rtree_fn()
            if rtree_fn is not None:
                self.rtree.close()
                self.rtree = rtree.index.Index(str(rtree_fn))

    @classmethod
    def from_pickle(cls, filename):
        """Deserialize map using pickle to the given filename."""
        filename = Path(filename)
        with filename.open("rb") as ifile:
            data = pickle.load(ifile)
        nmap = cls.deserialize(data)
        return nmap

    def bb(self):
        """Bounding box.

        :return: (lat_min, lon_min, lat_max, lon_max) or (y_min, x_min, y_max, x_max)
        """
        if self.use_rtree:
            lat_min, lon_min, lat_max, lon_max = self.rtree.bounds
        else:
            glat, glon = zip(*[t[0] for t in self.graph.values()])
            lat_min, lat_max = min(glat), max(glat)
            lon_min, lon_max = min(glon), max(glon)
        return lat_min, lon_min, lat_max, lon_max

    def labels(self):
        """All labels."""
        return self.graph.keys()

    def size(self):
        return len(self.graph)

    def node_coordinates(self, node_key):
        """Get the coordinates of the given node.

        :param node_key: Node label/key
        :return: (lat, lon)
        """
        return self.graph[node_key][0]

    def add_node(self, node, loc):
        """Add new node to the map.

        :param node: label
        :param loc: (lat, lon) or (y, x)
        """
        if node in self.graph:
            if self.graph[node][0] is None:
                self.graph[node] = (loc, self.graph[node][1])
        else:
            self.graph[node] = (loc, [])
        if self.use_rtree and self.rtree is not None and not self.index_edges:
            if type(node) is not int:
                raise Exception(f"Rtree index only supports integer keys for vertices")
            self.rtree.insert(node, (loc[0], loc[1], loc[0], loc[1]))

    def del_node(self, node):
        if node not in self.graph:
            return
        if self.rtree:
            data = self.graph[node]
            loc = data[0]
            self.rtree.delete(node, (loc[0], loc[1], loc[0], loc[1]))
        del self.graph[node]

    def add_edge(self, node_a, node_b):
        """Add new edge to the map.

        :param node_a: Label for the node that is the start of the edge
        :param node_b: Label for the node that is the end of the edge
        """
        if node_a not in self.graph:
            raise ValueError(f"Add {node_a} first as node")
        if node_b not in self.graph:
            raise ValueError(f"Add {node_b} first as node")
        if node_b not in self.graph[node_a][1]:
            self.graph[node_a][1].append(node_b)
        if self.use_rtree and self.rtree is not None and self.index_edges:
            if type(node_a) is not int or type(node_b) is not int:
                raise Exception(f"Rtree index only supports integer keys for vertices")
            loc_a = self.graph[node_a][0]
            loc_b = self.graph[node_b][0]
            bb = (min(loc_a[0], loc_b[0]), min(loc_a[1], loc_b[1]),  # y_min, x_min
                  max(loc_a[0], loc_b[0]), max(loc_a[1], loc_b[1]))  # y_max, x_max
            self.rtree.insert(node_a, bb)
            # self.rtree.insert(node_b, bb)

    def _items_in_bb(self, bb):
        if self.rtree is not None:
            node_idxs = self.rtree.intersection(bb)
            for key in node_idxs:
                yield (key, self.graph[key])
        else:
            lat_min, lon_min, lat_max, lon_max = bb
            for key, value in self.graph.items():
                ((lat, lon), nbrs) = value
                if lat_min <= lat <= lat_max and lon_min <= lon <= lon_max:
                    yield (key, value)

    def all_edges(self, bb=None):
        """Return all edges.

        :param bb: Bounding box
        :return: (key_a, loc_a, nbr, loc_b)
        """
        if bb is None:
            keyvals = self.graph.items()
        else:
            keyvals = self._items_in_bb(bb)
        for key_a, (loc_a, nbrs) in keyvals:
            if loc_a is not None:
                for nbr in nbrs:
                    try:
                        loc_b, _ = self.graph[nbr]
                        if loc_b is not None:
                            yield (key_a, loc_a, nbr, loc_b)
                    except KeyError:
                        # print("Node not found: {}".format(nbr))
                        pass

    def all_nodes(self, bb=None):
        """Return all nodes.

        :param bb: Bounding box
        :return:
        """
        if bb is None:
            keyvals = self.graph.items()
        else:
            keyvals = self._items_in_bb(bb)
        for key_a, (loc_a, nbrs) in keyvals:
            if loc_a is not None:
                yield key_a, loc_a

    def purge(self):
        cnt_noloc = 0
        cnt_noedges = 0
        remove = []
        for node in self.graph.keys():
            if self.graph[node][0] is None:
                cnt_noloc += 1
                remove.append(node)
                # print("No location for node {}".format(node))
            elif len(self.graph[node][1]) == 0:
                cnt_noedges += 1
                remove.append(node)
        for node in remove:
            self.del_node(node)
        logger.debug("Removed {} nodes without location".format(cnt_noloc))
        logger.debug("Removed {} nodes without edges".format(cnt_noedges))

    def rtree_size(self):
        bb = self.rtree.bounds
        if bb[0] < bb[2] and bb[1] < bb[3]:
            rtree_size = self.rtree.count(bb)
        else:
            rtree_size = 0
        return rtree_size

    def rtree_fn(self):
        rtree_fn = None
        if self.dir is not None:
            rtree_fn = self.dir / self.name
        return rtree_fn

    def setup_index(self, force=False, deserializing=False):
        if not self.use_rtree:
            return
        if self.rtree is not None and not force:
            return
        if rtree is None:
            raise Exception("rtree package not found")

        rtree_fn = self.rtree_fn()
        args = []
        if deserializing and (rtree_fn is None or not rtree_fn.exists()):
            deserializing = False

        if self.graph and len(self.graph) > 0 and not deserializing:
            if self.index_edges:
                logger.debug("Generator to index edges")

                def generator_function():
                    for label, data in self.graph.items():
                        lat_min, lon_min = data[0]
                        lat_max, lon_max = lat_min, lon_min
                        for idx in data[1]:
                            olat, olon = self.graph[idx][0]
                            lat_min = min(lat_min, olat)
                            lat_max = max(lat_max, olat)
                            lon_min = min(lon_min, olon)
                            lon_max = max(lon_max, olon)
                        if type(label) is not int:
                            raise Exception(f"Rtree index only supports integer keys for vertices")
                        yield (label, (lat_min, lon_min, lat_max, lon_max), None)
            else:
                logger.debug("Generator to index nodes")

                def generator_function():
                    for label, data in self.graph.items():
                        lat, lon = data[0]
                        if type(label) is not int:
                            raise Exception(f"Rtree index only supports integer keys for vertices")
                        yield (label, (lat, lon, lat, lon), None)
            args.append(generator_function())

        t_start = time.time()
        if self.dir is not None:
            # props = rtree.index.Property()
            # if force:
            #     props.overwrite = True
            logger.debug(f"Creating new file-based rtree index ({rtree_fn}) ...")
            args.insert(0, str(rtree_fn))
        elif deserializing:
            raise Exception("Cannot deserialize, no directory given")
        else:
            logger.debug(f"Creating new in-memory rtree index (args={args}) ...")
        self.rtree = rtree.index.Index(*args)
        t_delta = time.time() - t_start
        logger.debug(f"... done: rtree size = {self.rtree_size()}, time = {t_delta} sec")

    def fill_index(self):
        if not self.use_rtree or self.rtree is None:
            return

        for label, data in self.graph.items():
            loc = data[0]
            self.rtree.insert(label, (loc[1], loc[0], loc[1], loc[0]))
        logger.debug(f"After filling rtree, size = {self.rtree_size()}")

    def to_xy(self, name=None, use_rtree=None):
        """Create a map that uses a projected XY representation on which Euclidean distances
        can be used.
        """
        if not self.use_latlon:
            return self
        if name is None:
            name = self.name + "_xy"
        if use_rtree is None:
            use_rtree = self.use_rtree

        ngraph = dict()
        for label, row in self.graph.items():
            lat, lon = row[0]
            x, y = self.lonlat2xy(lon, lat)
            ngraph[label] = ((y, x), row[1])
        nmap = self.__class__(name, dir=self.dir, graph=ngraph,
                              use_latlon=False, use_rtree=use_rtree, index_edges=self.index_edges,
                              crs_xy=self.crs_xy, crs_lonlat=self.crs_lonlat)

        return nmap

    def latlon2xy(self, lat, lon):
        x, y = self.lonlat2xy(lon, lat)
        return x, y

    def latlon2yx(self, lat, lon):
        x, y = self.lonlat2xy(lon, lat)
        return y, x

    def xy2latlon(self, x, y):
        lon, lat = self.xy2lonlat(x, y)
        return lat, lon

    def yx2latlon(self, y, x):
        lon, lat = self.xy2lonlat(x, y)
        return lat, lon

    def nodes_closeto(self, loc, max_dist=None, max_elmt=None):
        """Return all nodes close to the given location.

        :param loc: Location
        :param max_dist: Maximal distance from the location
        :param max_elmt: Return only the most nearby nodes
        """
        t_start = time.time()
        lat, lon = loc[:2]
        lat_b, lon_l, lat_t, lon_r = self.box_around_point((lat, lon), max_dist)
        bb = (lat_b, lon_l,  # y_min, x_min
              lat_t, lon_r)  # y_max, x_max
        if self.rtree is not None and max_dist is not None:
            logger.debug(f"Search closeby nodes to {loc}, bb={bb}")
            nodes = self.rtree.intersection(bb)
        else:
            logger.warning("Searching closeby nodes with linear search, use an index and set max_dist")
            if max_dist is not None:
                nodes = (key for key, _ in self._items_in_bb(self.box_around_point((lat, lon), max_dist)))
            else:
                nodes = self.graph.keys()
        t_delta_search = time.time() - t_start
        t_start = time.time()
        results = []
        for label in nodes:
            oloc, nbrs = self.graph[label]
            dist = self.distance(loc, oloc)
            if dist < max_dist:
                results.append((dist, label, oloc))
        results.sort()
        t_delta_dist = time.time() - t_start
        logger.debug(f"Found {len(results)} closeby nodes "
                     f"in {t_delta_search} sec and computed distances in {t_delta_dist} sec")
        if max_elmt is not None:
            results = results[:max_elmt]
        return results

    def edges_closeto(self, loc, max_dist=None, max_elmt=None):
        """Return all nodes that are on an edge that is close to the given location.

        :param loc: Location
        :param max_dist: Maximal distance from the location
        :param max_elmt: Return only the most nearby nodes
        """
        t_start = time.time()
        lat, lon = loc[:2]
        if self.rtree is not None and max_dist is not None and self.index_edges:
            lat_b, lon_l, lat_t, lon_r = self.box_around_point((lat, lon), max_dist)
            bb = (lat_b, lon_l,  # y_min, x_min
                  lat_t, lon_r)  # y_max, x_max
            logger.debug(f"Search closeby edges to {loc}, bb={bb}")
            nodes = self.rtree.intersection(bb)
        else:
            if self.rtree is not None and max_dist is not None and not self.index_edges:
                logger.warning("Index is built for nodes, not for edges, set the index_edges argument to true")
            logger.warning("Searching closeby nodes with linear search, use an index and set max_dist")
            if max_dist is not None:
                bb = self.box_around_point((lat, lon), max_dist)
                nodes = (key for key, _ in self._items_in_bb(bb))
            else:
                nodes = self.graph.keys()
        t_delta_search = time.time() - t_start
        t_start = time.time()
        results = []
        for label in nodes:
            oloc, nbrs = self.graph[label]
            for nbr in nbrs:
                if label == nbr:
                    continue
                nbr_data = self.graph[nbr]
                dist, pi, ti = self.distance_point_to_segment(loc, oloc, nbr_data[0])
                # print(f"label={label}/{oloc}, nbr={nbr}/{nbr_data[0]}   -- loc={loc}  -> {dist}, {pi}, {ti}")
                if dist < max_dist:
                    results.append((dist, label, oloc, nbr, nbr_data[0], pi, ti))
        results.sort()
        t_delta_dist = time.time() - t_start
        logger.debug(f"Found {len(results)} closeby edges "
                     f"in {t_delta_search} sec and computed distances in {t_delta_dist} sec")
        if max_elmt is not None:
            results = results[:max_elmt]
        return results

    def nodes_nbrto(self, node):
        results = []
        if node not in self.graph:
            return results
        loc_node, nbrs = self.graph[node]
        for nbr_label in nbrs + [node]:
            try:
                loc_nbr = self.graph[nbr_label][0]
                if loc_nbr is not None:
                    results.append((nbr_label, loc_nbr))
            except KeyError:
                pass
        return results

    def edges_nbrto(self, edge):
        results = []
        l1, l2 = edge
        p1 = self.node_coordinates(l1)
        p2 = self.node_coordinates(l2)
        # Edges that connect at end of this edge
        for l3, p3 in self.nodes_nbrto(l2):
            results.append((l2, p2, l3, p3))
        # Edges that are in parallel and close
        if self.linked_edges:
            for (l3, l4) in self.linked_edges.get(edge, []):
                p3 = self.node_coordinates(l3)
                p4 = self.node_coordinates(l4)
                results.append((l3, p3, l4, p4))
        return results

    def find_duplicates(self, func=None):
        """Find entries with identical locations."""
        cnt = 0
        for label, data in self.graph.items():
            lat, lon = data[0]
            idxs = list(self.rtree.nearest((lat, lon, lat, lon), num_results=1))
            idxs.remove(label)
            if len(idxs) > 0:
                # logger.info(f"Found doubles for {label}: {idxs}")
                if func:
                    func(label, idxs)
        logger.info(f"Found {cnt} doubles")

    def connect_parallelroads(self, dist=0.5, bb=None):
        if self.rtree is None or not self.index_edges:
            logger.error("Finding parallel roads requires and edge-based index")
            return
        self.linked_edges = {}
        it = self.all_edges(bb=bb)
        if tqdm:
            it = tqdm.tqdm(list(it))
        for key_a, loc_a, key_b, loc_b in it:
            bb2 = [min(loc_a[0], loc_b[0]), min(loc_a[1], loc_b[1]),
                   max(loc_a[0], loc_b[0]), max(loc_a[1], loc_b[1])]
            for key_c, loc_c, key_d, loc_d in self.all_edges(bb=bb2):
                if key_a == key_c or key_a == key_d or key_b == key_c or key_b == key_d:
                    continue
                # print(f"Test: ({key_a},{key_b}) - ({key_c},{key_d})")
                if self.lines_parallel(loc_a, loc_b, loc_c, loc_d, d=dist):
                    # print(f"Parallel: ({key_a},{key_b}) - ({key_c},{key_d})")
                    key = (key_a, key_b)
                    if key in self.linked_edges:
                        self.linked_edges[key].add((key_c, key_d))
                    else:
                        self.linked_edges[key] = {(key_c, key_d)}
        logger.debug(f"Linked {len(self.linked_edges)} edges")

    def print_stats(self):
        print("Graph\n-----")
        print("Nodes: {}".format(len(self.graph)))

    def __str__(self):
        # s = ""
        # for label, (loc, nbrs, _) in self.graph.items():
        #     s += f"{label:<10} - ({loc[0]:10.4f}, {loc[1]:10.4f})\n"
        # return s
        return f"InMemMap({self.name}, size={self.size()})"


================================================
FILE: leuvenmapmatching/map/sqlite.py
================================================
# encoding: utf-8
"""
leuvenmapmatching.map.sqlite
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Map representation based on a sqlite database. Not optimized for production purposes.

:author: Wannes Meert
:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.
:license: Apache License, Version 2.0, see LICENSE for details.
"""
import sqlite3
import tempfile
import logging
import time
from pathlib import Path
import pickle
from functools import partial
try:
    import pyproj
except ImportError:
    pyproj = None
try:
    import tqdm
except ImportError:
    tqdm = None


from .base import BaseMap


MYPY = False
if MYPY:
    from typing import Optional, Set, Tuple, Dict, Union
    LabelType = Union[int, str]
    LocType = Tuple[float, float]
    EdgeType = Tuple[LabelType, LabelType]


logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching")


class SqliteMap(BaseMap):
    def __init__(self, name, use_latlon=True,
                 crs_lonlat=None, crs_xy=None, dir=None, deserializing=False):
        """Store a map as a SQLite instance.

        This class supports:

        - Indexing using rtrees to allow for fast searching of points on the map.
          When using the rtree index, only integer numbers are allowed as node labels.
        - Serializing to write and read from files.
        - Projecting points to a different frame (e.g. GPS to Lambert)

        :param name: Name of database file
        :param use_latlon: The locations represent latitude-longitude pairs, otherwise y-x coordinates
            are assumed.
        :param crs_lonlat: Coordinate reference system for the latitude-longitude coordinates.
        :param crs_xy: Coordiante reference system for the y-x coordinates.
        :param dir: Directory where to serialize to. If not given, a temporary location will be used.
        :param deserializing: Internal variable to indicate that the object is being build from a file.
        """
        super(SqliteMap, self).__init__(name, use_latlon=use_latlon)
        self.dir = Path(tempfile.gettempdir()) if dir is None else Path(dir)
        name = Path(name)
        suffix = name.suffix
        if suffix == '':
            name = name.with_suffix('.sqlite')
        self.db_fn = self.dir / name
        if deserializing and not self.db_fn.exists():
            raise Exception(f"File not found: {self.db_fn}")
        logger.debug(f"Opening database: {self.db_fn}")
        try:
            self.db = sqlite3.connect(str(self.db_fn))
        except Exception as exc:
            raise Exception(f'Problem with database: {self.db_fn}') from exc
        self.crs_lonlat = crs_lonlat
        self.crs_xy = crs_xy
        self.use_latlon = use_latlon

        if deserializing:
            self.read_properties()
        else:
            self.create_db()

        if self.crs_lonlat is None:
            self.crs_lonlat = 'EPSG:4326'  # GPS
        if self.crs_xy is None:
            self.crs_xy = 'EPSG:3395'  # Mercator projection

        self.save_properties()

        if pyproj:
            proj_lonlat = pyproj.Proj(self.crs_lonlat, preserve_units=True)
            proj_xy = pyproj.Proj(self.crs_xy, preserve_units=True)
            self.lonlat2xy = partial(pyproj.transform, proj_lonlat, proj_xy)
            self.xy2lonlat = partial(pyproj.transform, proj_xy, proj_lonlat)
        else:
            def pyproj_notfound(*_args, **_kwargs):
                raise Exception("pyproj package not found")
            self.lonlat2xy = pyproj_notfound
            self.xy2lonlat = pyproj_notfound

    def read_properties(self):
        c = self.db.cursor()
        for row in c.execute("SELECT key, value FROM properties;"):
            key, value = row[0], pickle.loads(row[1])
            self.__dict__[key] = value

    def save_properties(self):
        c = self.db.cursor()
        q = "INSERT INTO properties (key, value) VALUES (?, ?)"
        v = [('name', pickle.dumps(self.name)),
             ('use_latlon', pickle.dumps(self.use_latlon)),
             ('crs_lonlat', pickle.dumps(self.crs_lonlat)),
             ('crs_xy', pickle.dumps(self.crs_xy))]
        c.executemany(q, v)
        self.db.commit()

    def create_db(self):
        logger.debug("Cleaning database file and creating new tables")
        c = self.db.cursor()
        c.execute("DROP INDEX IF EXISTS edges_from_index")
        c.execute("DROP INDEX IF EXISTS close_edges_index")
        c.execute("DROP TABLE IF EXISTS nodes_index")
        c.execute("DROP TABLE IF EXISTS nodes")
        c.execute("DROP TABLE IF EXISTS edges_index")
        c.execute("DROP TABLE IF EXISTS edges")
        c.execute("DROP TABLE IF EXISTS close_edges")
        c.execute("DROP TABLE IF EXISTS properties")
        self.db.commit()

        # Create tables
        q = ("CREATE VIRTUAL TABLE nodes_index USING rtree(\n"
             "id,              -- Integer primary key\n"
             "minX, maxX,      -- Minimum and maximum X coordinate\n"
             "minY, maxY       -- Minimum and maximum Y coordinate\n"
             ")")
        c.execute(q)
        q = ("CREATE TABLE nodes(\n"
             "id INTEGER PRIMARY KEY,\n"
             "x REAL,\n"
             "y REAL\n"
             ")")
        c.execute(q)
        q = ("CREATE VIRTUAL TABLE edges_index USING rtree(\n"
             "id,              -- Integer primary key\n"
             "minX, maxX,      -- Minimum and maximum X coordinate\n"
             "minY, maxY       -- Minimum and maximum Y coordinate\n"
             ")")
        c.execute(q)
        q = ("CREATE TABLE edges(\n"
             "id INTEGER PRIMARY KEY,\n"
             "path INTEGER,\n"  # Not necessarily unique, a pathway id can consist of multiple edges
             "pathnum INTEGER,\n"
             "id1 INTEGER,\n"  # node 1
             "id2 INTEGER,\n"  # node 2
             "speed REAL,\n"  # speed m/s
             "type INTEGER\n"  # extra field
             ")")
        c.execute(q)
        q = ("CREATE TABLE close_edges(\n"
             "id1 INTEGER,\n"  # edge 1
             "id2 INTEGER\n"  # edge 2
             ")")
        c.execute(q)
        q = ("CREATE TABLE properties(\n"
             "key TEXT,\n"
             "value BLOB\n"
             ")")
        c.execute(q)
        q = "CREATE INDEX edges_from_index ON edges(id1)"
        c.execute(q)
        q = "CREATE INDEX close_edges_index ON close_edges(id1)"
        c.execute(q)
        self.db.commit()

    @classmethod
    def from_file(cls, filename):
        """Read from an existing file."""
        filename = Path(filename).with_suffix('')
        nmap = cls(filename.name, dir=filename.parent, deserializing=True)
        return nmap

    def bb(self):
        """Bounding box.

        :return: (lat_min, lon_min, lat_max, lon_max) or (y_min, x_min, y_max, x_max)
        """
        c = self.db.cursor()
        c.execute('SELECT min(minX), max(minX), min(maxX), max(maxX) FROM nodes_index;')
        lon_min, lon_max, lat_min, lat_max = c.fetchone()
        return lat_min, lon_min, lat_max, lon_max

    def labels(self):
        """All labels."""
        c = self.db.cursor()
        c.execute('SELECT id FROM nodes;')
        result = [row[0] for row in c.fetchall()]
        return result

    def size(self):
        c = self.db.cursor()
        c.execute('SELECT count(*) FROM nodes')
        result = c.fetchone()[0]
        return result

    def node_coordinates(self, node_key):
        """Get the coordinates of the given node.

        :param node_key: Node label/key
        :return: (lat, lon)
        """
        c = self.db.cursor()
        c.execute('SELECT y, x FROM nodes WHERE id = ?', (node_key, ))
        result = c.fetchone()
        if result is None:
            raise Exception(f"No coordinates found for node {node_key}")
        return result

    def add_node(self, node, loc, ignore_doubles=False, no_index=False, no_commit=False):
        """Add new node to the map.

        :param node: label
        :param loc: (lat, lon) or (y, x)
        :param ignore_doubles: When trying to add the same node, ignore it
        :param no_commit: Do not commit to database (remember to commit later)
        """
        c = self.db.cursor()
        lat, lon = loc
        # Nodes
        q = "INSERT INTO nodes VALUES(?, ?, ?)"
        try:
            c.execute(q, (node, lon, lat))
        except sqlite3.IntegrityError as exc:
            if ignore_doubles and "UNIQUE constraint failed: nodes.id" in str(exc):
                return
            logger.error(f"Problem with adding node {node} {loc}")
            raise exc
        # Nodes index
        if not no_index:
            q = "INSERT INTO nodes_index VALUES(?, ?, ?, ?, ?)"
            try:
                c.execute(q, (node, lon, lon, lat, lat))
            except sqlite3.IntegrityError as exc:
                logger.error(f"Problem with adding node to index {node} {loc}")
                raise exc
        if not no_commit:
            self.db.commit()

    def reindex_nodes(self):
        logger.debug("Reindexing nodes ...")
        t_start = time.time()
        c = self.db.cursor()
        c.execute('DELETE FROM nodes_index')
        q = ("INSERT INTO nodes_index "
             "SELECT id, x, x, y, y FROM nodes")
        c.execute(q)
        self.db.commit()
        c.execute('SELECT count(*) FROM nodes_index')
        cnt = c.fetchone()[0]
        t_delta = time.time() - t_start
        logger.debug(f"... done, #rows = {cnt}, time = {t_delta} sec")

    def add_nodes(self, nodes):
        """Add list of nodes to database.

        :param nodes: List[Tuple[node_key, Tuple[lat, lon]]]
        """
        c = self.db.cursor()

        def get_node_index():
            for key, (lat, lon) in nodes:
                yield key, lon, lon, lat, lat

        q = "INSERT INTO nodes_index VALUES(?, ?, ?, ?, ?)"
        c.executemany(q, get_node_index())

        def get_node_vals():
            for key, (lat, lon) in nodes:
                yield key, lon, lat

        q = "INSERT INTO nodes VALUES(?, ?, ?)"
        c.executemany(q, get_node_vals())
        self.db.commit()

    def del_node(self, node):
        raise Exception("TODO")

    def add_edge(self, node_a, node_b, loc_a=None, loc_b=None, speed=None, edge_type=None,
                 path=None, pathnum=None,
                 no_index=False, no_commit=False):
        """Add new edge to the map.

        :param node_a: Label for the node that is the start of the edge
        :param node_b: Label for the node that is the end of the edge
        :param no_commit: Do not commit to database (remember to commit later)
        """
        c = self.db.cursor()
        eid = (node_a, node_b).__hash__()
        c.execute('INSERT OR IGNORE INTO edges(id, path, pathnum, id1, id2, type, speed) VALUES (?, ?, ?, ?, ?, ?, ?)',
                  (eid, path, pathnum, node_a, node_b, edge_type, speed))
        # c.execute('SELECT last_insert_rowid();')
        # eid = c.fetchone()[0]

        if not no_index:
            if loc_a is None:
                c.execute('SELECT y, x FROM nodes WHERE id = ?;', (node_a, ))
                loc_a = c.fetchone()
            if loc_b is None:
                c.execute('SELECT y, x FROM nodes WHERE id = ?;', (node_b, ))
                loc_b = c.fetchone()
            lat1, lon1 = loc_a
            lat2, lon2 = loc_b
            if lat1 > lat2:
                lat1, lat2 = lat2, lat1
            if lon1 > lon2:
                lon1, lon2 = lon2, lon1
            c.execute('INSERT OR IGNORE INTO edges_index(id, minX, maxX, minY, maxY) VALUES (?, ?, ?, ?, ?)',
                      (eid, lon1, lon2, lat1, lat2))
        if not no_commit:
            self.db.commit()

    def add_edges(self, edges, no_index=False):
        """Add list of nodes to database.

        :param edges: List[Tuple[node_key, node_key]] or
            List[Tuple[node_key, node_key, path_key, int]]
        """
        c = self.db.cursor()

        def get_edge():
            for row in edges:
                row = list(row) + ([None] * (6 - len(row)))
                key_a, key_b, path, pathnum, edge_type, speed = row
                eid = (key_a, key_b).__hash__()
                yield eid, path, pathnum, key_a, key_b, edge_type, speed

        q = "INSERT INTO edges(id, path, pathnum, id1, id2, type, speed) VALUES(?, ?, ?, ?, ?, ?, ?);"
        c.executemany(q, get_edge())
        self.db.commit()

        if not no_index:
            self.reindex_edges()

    def reindex_edges(self):
        logger.debug("Reindexing edges ...")
        t_start = time.time()
        c = self.db.cursor()
        # c2 = self.db.cursor()
        c.execute('DELETE FROM edges_index')
        q = ('INSERT INTO edges_index '
             'SELECT e.id, MIN(n1.x,n2.x), MAX(n1.x,n2.x), '
             '             MIN(n1.y,n2.y), MAX(n1.y,n2.y) '
             'FROM edges e '
             'INNER JOIN nodes n1 ON n1.id = e.id1 '
             'INNER JOIN nodes n2 ON n2.id = e.id2')
        c.execute(q)
        # cnt = 0
        # for row in c.execute(q):
        #     # Contained in query
        #     c2.execute('INSERT INTO edges_index(id, minX, maxX, minY, maxY) VALUES (?, ?, ?, ?, ?)', row)
        #     cnt += 1
        self.db.commit()
        c.execute('SELECT count(*) FROM edges_index')
        cnt = c.fetchone()[0]
        t_delta = time.time() - t_start
        logger.debug(f"... done, #rows = {cnt}, time = {t_delta} sec")

    def all_edges(self, bb=None):
        """Return all edges.

        :param bb: Bounding box
        :return: (key_a, loc_a, nbr, loc_b)
        """
        c = self.db.cursor()
        q = 'SELECT e.id1, e.id2, n1.x AS n1x, n2.x AS n2x, n1.y AS n1y, n2.y AS n2y ' + \
            'FROM edges e, edges_index ei ' + \
            'LEFT JOIN nodes n1 ON n1.id = e.id1 ' + \
            'LEFT JOIN nodes n2 ON n2.id = e.id2 ' + \
            'WHERE ei.id == e.id'
        if bb:
            min_y, min_x, max_y, max_x = bb
            # Intersecting with query
            q += ' AND ei.maxX >= ? AND ei.minX <= ? AND ei.maxY >= ? AND ei.minY <= ?'
            c.execute(q, (min_x, max_x, min_y, max_y))
        else:
            c.execute(q)

        for row in c.fetchall():
            key_a, key_b, lon_a, lon_b, lat_a, lat_b = row
            yield key_a, (lat_a, lon_a), key_b, (lat_b, lon_b)

    def all_nodes(self, bb=None):
        """Return all nodes.

        :param bb: Bounding box (minY, minX, maxY, maxX)
        :return:
        """
        c = self.db.cursor()
        q = ('SELECT n.id, n.x, n.y '
             'FROM nodes n, nodes_index ni '
             'WHERE n.id = ni.id ')
        if bb:
            minY, minX, maxY, maxX = bb
            q += 'AND ni.minX >= ? AND ni.maxX <= ? AND ni.minY >= ? AND ni.maxY <= ?'
            c.execute(q, (minX, maxX, minY, maxY))
        else:
            c.execute(q)

        for row in c.fetchall():
            key_a, lon_a, lat_a = row
            yield key_a, (lat_a, lon_a)

    def purge(self):
        pass

    def to_xy(self, name=None):
        """Create a map that uses a projected XY representation on which Euclidean distances
        can be used.
        """
        if not self.use_latlon:
            return self
        if name is None:
            name = self.name + "_xy"

        logger.debug("Start transformation ...")
        t_start = time.time()
        nmap = self.__class__(name, dir=self.dir, use_latlon=self.use_latlon,
                              crs_xy=self.crs_xy, crs_lonlat=self.crs_lonlat)

        raise Exception("to implement")

        t_delta = time.time() - t_start
        logger.debug(f"... done: rtree size = {self.rtree_size()}, time = {t_delta} sec")

        return nmap

    def latlon2xy(self, lat, lon):
        x, y = self.lonlat2xy(lon, lat)
        return x, y

    def latlon2yx(self, lat, lon):
        x, y = self.lonlat2xy(lon, lat)
        return y, x

    def xy2latlon(self, x, y):
        lon, lat = self.xy2lonlat(x, y)
        return lat, lon

    def yx2latlon(self, y, x):
        lon, lat = self.xy2lonlat(x, y)
        return lat, lon

    def nodes_closeto(self, loc, max_dist=None, max_elmt=None):
        """Return all nodes close to the given location.

        :param loc: Location
        :param max_dist: Maximal distance from the location
        :param max_elmt: Return only the most nearby nodes
        """
        t_start = time.time()
        lat, lon = loc[:2]
        lat_b, lon_l, lat_t, lon_r = self.box_around_point((lat, lon), max_dist)
        bb = (lat_b, lon_l,  # y_min, x_min
              lat_t, lon_r)  # y_max, x_max
        nodes = self.all_nodes(bb=bb)
        t_delta_search = time.time() - t_start
        t_start = time.time()
        results = []
        for key_o, loc_o in nodes:
            dist = self.distance(loc, loc_o)
            if dist < max_dist:
                results.append((dist, key_o, loc_o))
        results.sort()
        t_delta_dist = time.time() - t_start
        logger.debug(f"Found {len(results)} closeby nodes "
                     f"in {t_delta_search} sec and computed distances in {t_delta_dist} sec")
        if max_elmt is not None:
            results = results[:max_elmt]
        return results

    def edges_closeto(self, loc, max_dist=None, max_elmt=None):
        """Return all nodes that are on an edge that is close to the given location.

        :param loc: Location
        :param max_dist: Maximal distance from the location
        :param max_elmt: Return only the most nearby nodes
        """
        print(f"edges_closeto({loc})")
        t_start = time.time()
        lat, lon = loc[:2]
        lat_b, lon_l, lat_t, lon_r = self.box_around_point((lat, lon), max_dist)
        bb = (lat_b, lon_l,  # y_min, x_min
              lat_t, lon_r)  # y_max, x_max
        logger.debug(f"Search in bounding box {bb}")
        nodes = self.all_edges(bb=bb)
        t_delta_search = time.time() - t_start
        t_start = time.time()
        results = []
        for key_a, loc_a, key_b, loc_b in nodes:
            dist, pi, ti = self.distance_point_to_segment(loc, loc_a, loc_b)
            if dist < max_dist:
                results.append((dist, key_a, loc_a, key_b, loc_b, pi, ti))
        results.sort()
        t_delta_dist = time.time() - t_start
        logger.debug(f"Found {len(results)} closeby edges "
                     f"in {t_delta_search} sec and computed distances in {t_delta_dist} sec")
        if max_elmt is not None:
            results = results[:max_elmt]
        return results

    def nodes_nbrto(self, node):
        c = self.db.cursor()
        q = ('SELECT e.id2, n2.y, n2.x FROM edges e '
             'INNER JOIN nodes n2 ON n2.id = e.id2 '
             'WHERE e.id1 = ?')
        results = []
        for nbr_label, nbr_lat, nbr_lon in c.execute(q, (node, )):
            results.append((nbr_label, (nbr_lat, nbr_lon)))
        return results

    def edges_nbrto(self, edge):
        l1, l2 = edge
        c = self.db.cursor()
        c.execute('SELECT n.y, n.x FROM nodes n WHERE id = ?', (l2, ))
        p2 = c.fetchone()
        results = []
        # Edges that connect at end of this edge
        for l3, p3 in self.nodes_nbrto(l2):
            results.append((l2, p2, l3, p3))
        # Edges that are in parallel and close
        edge_id = edge.__hash__()
        q = ('SELECT e.id1, e.id2, n1.y, n1.x, n2.y, n2.x FROM close_edges ce '
             'INNER JOIN edges e ON e.id = ce.id2 '
             'INNER JOIN nodes n1 ON n1.id = e.id1 '
             'INNER JOIN nodes n2 ON n2.id = e.id2 '
             'WHERE ce.id1 = ?')
        for l3, l4, p3lat, p3lon, p4lat, p4lon in c.execute(q, (edge_id,)):
            results.append((l3, (p3lat, p3lon), l4, (p4lat, p4lon)))
        return results

    def find_duplicates(self, func=None):
        """Find entries with identical locations."""
        c = self.db.cursor()
        logger.debug('Find duplicates ...')
        t_start = time.time()
        cnt = 0
        q = ('select count(*)as qty, group_concat(id) '
             'from nodes '
             'group by y, x '
             'having qty > 1 ')
        for ncnt, idxs in c.execute(q):
            func(int(idx) for idx in idxs.split(","))
        t_delta = time.time() - t_start
        logger.info(f"Found {cnt} doubles, time: {t_delta} seconds")

    def connect_parallelroads(self, dist=0.5, bb=None):
        c = self.db.cursor()
        it = self.all_edges(bb=bb)
        if tqdm:
            it = tqdm.tqdm(list(it))
        cnt = 0
        for key_a, loc_a, key_b, loc_b in it:
            e_id1 = (key_a, key_b).__hash__()
            bb2 = [min(loc_a[0], loc_b[0]), min(loc_a[1], loc_b[1]),
                   max(loc_a[0], loc_b[0]), max(loc_a[1], loc_b[1])]
            for key_c, loc_c, key_d, loc_d in self.all_edges(bb=bb2):
                e_id2 = (key_c, key_d).__hash__()
                if key_a == key_c or key_a == key_d or key_b == key_c or key_b == key_d:
                    continue
                # print(f"Test: ({key_a},{key_b}) - ({key_c},{key_d})")
                if self.lines_parallel(loc_a, loc_b, loc_c, loc_d, d=dist):
                    # print(f"Parallel: ({key_a},{key_b}) - ({key_c},{key_d})")
                    c.execute('INSERT INTO close_edges(id1, id2) VALUES (?, ?)', (e_id1, e_id2))
                    c.execute('INSERT INTO close_edges(id1, id2) VALUES (?, ?)', (e_id2, e_id1))
                    cnt += 1
        logger.debug(f"Linked {cnt} edges")
        self.db.commit()

    def nodes_to_paths(self, nodes, ignore_nopath=True):
        c = self.db.cursor()
        prev_path = None
        paths = []
        for begin, end in zip(nodes[:-1], nodes[1:]):
            c.execute("SELECT path FROM edges WHERE id1=? AND id2=?", (begin, end))
            path = c.fetchone()[0]
            if path is None and ignore_nopath:
                continue
            if path != prev_path:
                paths.append(path)
                prev_path = path
        return paths

    def path_dist(self, path):
        c = self.db.cursor()
        dist = 0
        q = ('SELECT n1.y, n1.x, n2.y, n2.x FROM edges e '
             'INNER JOIN nodes n1 ON n1.id = e.id1 '
             'INNER JOIN nodes n2 ON n2.id = e.id2 '
             'WHERE e.pathnum>0 AND e.path=?')
        for lat1, lon1, lat2, lon2 in c.execute(q, (path,)):
            dist += self.distance((lat1, lon1), (lat2, lon2))
        return dist

    def print_stats(self):
        print("Graph\n-----")
        print("Nodes: {}".format(len(self.graph)))

    def __str__(self):
        # s = ""
        # for label, (loc, nbrs, _) in self.graph.items():
        #     s += f"{label:<10} - ({loc[0]:10.4f}, {loc[1]:10.4f})\n"
        # return s
        c = self.db.cursor()
        c.execute("select sqlite_version()")
        row = c.fetchone()
        version = row[0]
        return f"SqliteMap({self.name}, size={self.size()}, version={version})"


================================================
FILE: leuvenmapmatching/matcher/__init__.py
================================================
# encoding: utf-8
"""
leuvenmapmatching.matcher
~~~~~~~~~~~~~~~~~~~~~~~~~

:author: Wannes Meert
:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.
:license: Apache License, Version 2.0, see LICENSE for details.
"""


================================================
FILE: leuvenmapmatching/matcher/base.py
================================================
# encoding: utf-8
"""
leuvenmapmatching.matcher.base
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Base Matcher and Matching classes.

This a generic base class to be used by matchers. This class itself
does not implement a working matcher.

:author: Wannes Meert
:copyright: Copyright 2015-2021 DTAI, KU Leuven and Sirris.
:license: Apache License, Version 2.0, see LICENSE for details.
"""
from __future__ import print_function

import math
import sys
import logging
import time
from collections import OrderedDict, defaultdict, namedtuple
from itertools import islice
from typing import List, Tuple, Dict, Any, Optional, Set

import numpy as np

from ..util.segment import Segment
from ..util import approx_equal, approx_leq


logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching")
approx_value = 0.0000000001
ema_const = namedtuple('EMAConst', ['prev', 'cur'])(0.7, 0.3)
default_label_width = 25


class BaseMatching(object):
    """Matching object that represents a node in the Viterbi lattice."""
    __slots__ = ['matcher', 'edge_m', 'edge_o',
                 'logprob', 'logprobema', 'logprobe', 'logprobne',
                 'obs', 'obs_ne', 'dist_obs',
                 'prev', 'prev_other', 'stop', 'length', 'delayed']

    def __init__(self, matcher: 'BaseMatcher', edge_m: Segment, edge_o: Segment,
                 logprob=-np.inf, logprobema=-np.inf, logprobe=-np.inf, logprobne=-np.inf,
                 dist_obs: float = 0.0, obs: int = 0, obs_ne: int = 0,
                 prev: Optional[Set['BaseMatching']] = None, stop: bool = False, length: int = 1,
                 delayed: int = 0, **_kwargs):
        """

        :param matcher: Reference to the Matcher used to generate this matching object.
        :param edge_m: Segment in the given graph (thus line between two nodes in the graph).
        :param edge_o: Segment in the given observations (thus line in between two observations).
        :param logprob: Log probability of this matching.
        :param logprobema: Exponential Mean Average of Log probability.
        :param logprobe: Emitting
        :param logprobne: Non-emitting
        :param dist_obs: Distance between map point and observation
        :param obs: Reference to path entry index (observation)
        :param obs_ne: Number of non-emitting states for this observation
        :param prev: Previous best matching objects
        :param stop: Stop after this matching (e.g. because probability is too low)
        :param length: Lenght of current matching sequence through lattice.
        :param delayed: This matching is temporarily stopped if >0 (e.g. to first explore better options).
        :param dist_m: Distance over graph
        :param dist_o: Distance over observations
        :param _kwargs:
        """
        self.edge_m: Segment = edge_m
        self.edge_o: Segment = edge_o
        self.logprob: float = logprob        # max probability
        self.logprobe: float = logprobe      # Emitting
        self.logprobne: float = logprobne    # Non-emitting
        self.logprobema: float = logprobema  # exponential moving average log probability  # TODO: Not used anymore?
        self.obs: int = obs  # reference to path entry index (observation)
        self.obs_ne: int = obs_ne  # number of non-emitting states for this observation
        self.dist_obs: float = dist_obs  # Distance between map point and observation
        self.prev: Set[BaseMatching] = set() if prev is None else prev  # Previous best matching objects
        self.prev_other: Set[BaseMatching] = set()  # Previous matching objects with lower logprob
        self.stop: bool = stop
        self.length: int = length
        self.delayed: int = delayed
        self.matcher: BaseMatcher = matcher

    @property
    def prune_value(self):
        """Pruning the lattice (e.g. to delay) is based on this key."""
        return self.logprob
        # return self.logprobema

    def next(self, edge_m: Segment, edge_o: Segment, obs: int = 0, obs_ne: int = 0):
        """Create a next lattice Matching object with this Matching object as the previous one in the lattice."""
        new_stop = False
        if edge_m.is_point() and edge_o.is_point():
            # node to node
            dist = self.matcher.map.distance(edge_m.p1, edge_o.p1)
            # proj_m = edge_m.p1
            # proj_o = edge_o.pi
        elif edge_m.is_point() and not edge_o.is_point():
            # node to edge
            dist, proj_o, t_o = self.matcher.map.distance_point_to_segment(edge_m.p1, edge_o.p1, edge_o.p2)
            # proj_m = edge_m.p1
            edge_o.pi = proj_o
            edge_o.ti = t_o
        elif not edge_m.is_point() and edge_o.is_point():
            # edge to node
            dist, proj_m, t_m = self.matcher.map.distance_point_to_segment(edge_o.p1, edge_m.p1, edge_m.p2)
            if not self.matcher.only_edges and (approx_equal(t_m, 0.0) or approx_equal(t_m, 1.0)):
                if __debug__ and logger.isEnabledFor(logging.DEBUG):
                    logger.debug(f"   | Stopped trace: Too close to end, {t_m}")
                    new_stop = True
                else:
                    return None
            edge_m.pi = proj_m
            edge_m.ti = t_m
            # proj_o = edge_o.pi
        elif not edge_m.is_point() and not edge_o.is_point():
            # edge to edge
            dist, proj_m, proj_o, t_m, t_o = self.matcher.map.distance_segment_to_segment(edge_m.p1, edge_m.p2,
                                                                                          edge_o.p1, edge_o.p2)
            edge_m.pi = proj_m
            edge_m.ti = t_m
            edge_o.pi = proj_o
            edge_o.ti = t_o
        else:
            raise Exception(f"Should not happen")

        logprob_trans, props_trans = self.matcher.logprob_trans(self, edge_m, edge_o,
                                                                is_prev_ne=(self.obs_ne != 0),
                                                                is_next_ne=(obs_ne != 0))
        logprob_obs, props_obs = self.matcher.logprob_obs(dist, self, edge_m, edge_o,
                                                          is_ne=(obs_ne != 0))
        if __debug__ and logprob_trans > 0:
            raise Exception(f"logprob_trans = {logprob_trans} > 0")
        if __debug__ and logprob_obs > 0:
            raise Exception(f"logprob_obs = {logprob_obs} > 0")
        new_logprob_delta = logprob_trans + logprob_obs
        if obs_ne == 0:
            new_logprobe = self.logprob + new_logprob_delta
            new_logprobne = 0
            new_logprob = new_logprobe
            new_length = self.length + 1
        else:
            # Non-emitting states require normalisation
            # "* e^(ne_length_factor_log)" or "- ne_length_factor_log" for every step to a non-emitting
            # state to prefer shorter paths
            new_logprobe = self.logprobe + self.matcher.ne_length_factor_log
            # The obvious choice would be average to compensate for that non-emitting states
            # create different path lengths between emitting nodes.
            # We use min() as it is a monotonic function, in contrast with an average
            new_logprobne = min(self.logprobne, new_logprob_delta)
            new_logprob = new_logprobe + new_logprobne
            # Alternative approach with an average
            # new_logprobne = self.logprobne + new_logprob_delta
            # "+ 1" to punish non-emitting states a bit less. Otherwise it would be
            # similar to (Pr_tr*Pr_obs)**2, which punishes just one non-emitting state too much.
            # new_logprob = new_logprobe + new_logprobne / (obs_ne + 1)
            new_length = self.length
        new_logprobema = ema_const.cur * new_logprob_delta + ema_const.prev * self.logprobema
        new_stop |= self.matcher.do_stop(new_logprob / new_length, dist, logprob_trans, logprob_obs)
        if __debug__ and new_logprob > self.logprob:
            raise Exception(f"Expecting a monotonic probability, "
                            f"new_logprob = {new_logprob} > logprob = {self.logprob}")
        if not new_stop or (__debug__ and logger.isEnabledFor(logging.DEBUG)):
            m_next = self.__class__(self.matcher, edge_m, edge_o,
                                    logprob=new_logprob, logprobne=new_logprobne,
                                    logprobe=new_logprobe, logprobema=new_logprobema,
                                    obs=obs, obs_ne=obs_ne, prev={self}, dist_obs=dist,
                                    stop=new_stop, length=new_length, delayed=self.delayed,
                                    **props_trans, **props_obs)
            return m_next
        else:
            return None

    @classmethod
    def first(cls, logprob_init, edge_m, edge_o, matcher, dist_obs):
        """Create an initial lattice Matching object."""
        logprob_obs, props_obs = matcher.logprob_obs(dist_obs, None, edge_m, edge_o)
        logprob = logprob_init + logprob_obs
        new_stop = matcher.do_stop(logprob, dist_obs, logprob_init, logprob_obs)
        if not new_stop or logger.isEnabledFor(logging.DEBUG):
            m_next = cls(matcher, edge_m=edge_m, edge_o=edge_o,
                         logprob=logprob, logprobema=logprob, logprobe=logprob, logprobne=0,
                         dist_obs=dist_obs, obs=0, stop=new_stop, **props_obs)
            return m_next
        else:
            return None

    def update(self, m_next):
        """Update the current entry if the new matching object for this state is better.

        :param m_next: The new matching object representing the same node in the lattice.
        :return: True if the current object is replaced, False otherwise
        """
        # if self.length != m_next.length:
        #     slogprob_norm = self.logprob / self.length
        #     nlogprob_norm = m_next.logprob / m_next.length
        # else:
        #     slogprob_norm = self.logprob
        #     nlogprob_norm = m_next.logprob
        # if (self.stop == m_next.stop and slogprob_norm < nlogprob_norm) or (self.stop and not m_next.stop):
        #     self._update_inner(m_next)
        #     return True
        # elif abs(slogprob_norm - nlogprob_norm) < approx_value and self.stop == m_next.stop:
        #     self.prev.update(m_next.prev)
        #     self.stop = m_next.stop
        #     return False
        assert self.length == m_next.length
        if (self.stop and not m_next.stop) \
                or (self.stop == m_next.stop and self.logprob < m_next.logprob):
            self._update_inner(m_next)
            return True
        else:
            self.prev_other.update(m_next.prev)
            return False

    def _update_inner(self, m_other: 'BaseMatching'):
        self.edge_m = m_other.edge_m
        self.edge_o = m_other.edge_o
        self.logprob = m_other.logprob
        self.logprobe = m_other.logprobe
        self.logprobne = m_other.logprobne
        self.logprobema = m_other.logprobema
        self.dist_obs = m_other.dist_obs
        self.obs = m_other.obs
        self.obs_ne = m_other.obs_ne
        self.prev_other.update(self.prev)  # Do we use this?
        self.prev = m_other.prev
        self.stop = m_other.stop
        self.delayed = m_other.delayed
        self.length = m_other.length

    def is_nonemitting(self):
        return self.obs_ne != 0

    def is_emitting(self):
        return self.obs_ne == 0

    def last_emitting_logprob(self):
        if self.is_emitting():
            return self.logprob
        elif self.prev is None or len(self.prev) == 0:
            return 0
        else:
            return next(iter(self.prev)).last_emitting_logprob()

    def __str__(self, label_width=None):
        stop = ''
        if self.stop:
            stop = 'x'
        else:
            stop = f'{self.delayed}'
        if label_width is None:
            label_width = default_label_width
        repr_tmpl = "{:<2} | {:<"+str(label_width)+"} | {:10.5f} | {:10.5f} | {:10.5f} | {:10.5f} | " +\
                    "{:<3} | {:10.5f} | {:<" + str(label_width) + "} |"
        return repr_tmpl.format(stop, self.label, self.logprob, self.logprob / self.length,
                                self.logprobema, self.logprobne, self.obs,
                                self.dist_obs, ",".join([str(prev.label) for prev in self.prev]))

    def __repr__(self):
        return "Matching<"+str(self.label)+">"

    @staticmethod
    def repr_header(label_width=None, stop=""):
        if label_width is None:
            label_width = default_label_width
        repr_tmpl = "{:<2} | {:<"+str(label_width)+"} | {:<10} | {:<10} | {:<10} | {:<10} | " + \
                    "{:<3} | {:<10} | {:<"+str(label_width)+"} |"
        return repr_tmpl.format(stop, "", "lg(Pr)", "nlg(Pr)", "slg(Pr)", "lg(Pr-ne)", "obs", "d(obs)", "prev")

    @staticmethod
    def repr_static(fields, label_width=None):
        if label_width is None:
            label_width = default_label_width
        default_fields = ["", "", float('nan'), float('nan'), float('nan'), float('nan'), "", float('nan'), "", ""]
        repr_tmpl = "{:<2} | {:<" + str(label_width) + "} | {:10.5f} | {:10.5f} | {:10.5f} | {:10.5f} | " + \
                    "{:<3} | {:10.5f} | {:<" + str(label_width) + "} |"
        if len(fields) < 8:
            fields = list(fields) + default_fields[len(fields):]
        return repr_tmpl.format(*fields)

    @property
    def label(self):
        if self.edge_m.p2 is None:
            return "{}---{}-{}".format(self.edge_m.l1, self.obs, self.obs_ne)
        else:
            return "{}-{}-{}-{}".format(self.edge_m.l1, self.edge_m.l2, self.obs, self.obs_ne)

    @property
    def cname(self):
        if self.edge_m.l2 is None:
            return "{}_{}_{}".format(self.edge_m.l1, self.obs, self.obs_ne)
        else:
            return "{}_{}_{}_{}".format(self.edge_m.l1, self.edge_m.l2, self.obs, self.obs_ne)

    @property
    def key(self):
        """Key that indicates the node or edge, observation and non-emitting step.
        This is the unique key that is used in the lattice.
        """
        if self.edge_m.l2 is None:
            return tuple([self.edge_m.l1, self.obs, self.obs_ne])
        else:
            return tuple([self.edge_m.l1, self.edge_m.l2, self.obs, self.obs_ne])

    @property
    def shortkey(self):
        """Key that indicates the node or edge. Irrespective of the current observation."""
        if self.edge_m.l2 is None:
            return self.edge_m.l1
        else:
            return tuple([self.edge_m.l1, self.edge_m.l2])

    @property
    def nodes(self):
        if self.edge_m.l2 is None:
            return [self.edge_m.l1]
        else:
            return [self.edge_m.l1, self.edge_m.l2]

    def __hash__(self):
        return self.cname.__hash__()

    def __lt__(self, o):
        return self.logprob < o.logprob

    def __le__(self, o):
        return self.logprob <= o.logprob

    def __eq__(self, o):
        return self.logprob == o.logprob

    def __ne__(self, o):
        return self.logprob != o.logprob

    def __ge__(self, o):
        return self.logprob >= o.logprob

    def __gt__(self, o):
        return self.logprob > o.logprob


class LatticeColumn:

    def __init__(self, obs_idx):
        # 0 = obs, >0 = non-emitting between this obs and next
        self.obs_idx = obs_idx
        self.o = []  # type list[dict[label,Matching]]

    def __contains__(self, item):
        for c in self.o:
            if item in c:
                return True
        return False

    def __len__(self):
        return len(self.o)

    def set_delayed(self, delayed):
        """Update all delayed values."""
        for c in self.o:
            for m in c.values():
                m.delayed = delayed

    def dict(self, obs_ne=None):
        if obs_ne is None:
            raise AttributeError('obs_ne should be value')
        while obs_ne >= len(self.o):
            self.o.append({})
        return self.o[obs_ne]

    def values_all(self):
        """All matches for the emitting layer and all non-emitting layers."""
        values = set()
        for o in self.o:
            values.update(o.values())
        return values

    def values(self, obs_ne=None):
        if obs_ne is None:
            raise AttributeError('obs_ne should be value')
        if len(self.o) <= obs_ne:
            return []
        return self.o[obs_ne].values()

    def upsert(self, matching):
        # type: (BaseMatching) -> None
        if matching is None:
            return None
        while matching.obs_ne >= len(self.o):
            self.o.append({})
        c = self.o[matching.obs_ne]
        if matching.key in c:
            other_matching = c[matching.key]  # type: BaseMatching
            other_matching.update(matching)
        else:
            c[matching.key] = matching
        return c[matching.key]

    def prune(self, obs_ne, max_lattice_width, expand_upto, prune_thr=None):
        """Prune given column in the lattice to fit in max_lattice_width.
        Also ignore all matchings with a probability lower than prune_thr. These are
        matchings that are worse than the matchings at the next observation that are
        retained after pruning.

        :param obs_ne:
        :param max_lattice_width:
        :param expand_upto: The current expand level
        :return:
        """
        cur_lattice = [m for m in self.values(obs_ne) if not m.stop]
        if __debug__:
            logger.debug('Prune lattice[{},{}] from {} to {}, with prune thr {}'
                         .format(self.obs_idx, obs_ne,
                                 len([m for m in cur_lattice if not m.stop and m.delayed == expand_upto]),
                                 max_lattice_width, prune_thr))
            cnt_pruned = 0
        if max_lattice_width is not None and len(cur_lattice) > max_lattice_width:
            ms = sorted(cur_lattice, key=lambda t: t.prune_value, reverse=True)
            cur_width = max_lattice_width
            m_last = ms[cur_width - 1]
            # Extend current width if next pruned matching has same logprob as last kept matching
            # This increases the lattice width but otherwise the algorithm depends on the
            # order of edges/nodes and is not deterministic.
            while cur_width < len(ms) and ms[cur_width].prune_value == m_last.prune_value:
                m_last = ms[cur_width]
                cur_width += 1
            if prune_thr is not None:
                while cur_width > 0 and ms[cur_width - 1].prune_value < prune_thr:
                    cur_width -= 1
            for m in ms[:cur_width]:  # type: BaseMatching
                if m.delayed > expand_upto:
                    m.delayed = expand_upto  # expand now
            for m in ms[cur_width:]:
                if m.delayed <= expand_upto:
                    if __debug__:
                        cnt_pruned += 1
                    m.delayed = expand_upto + 1  # expand later
            if cur_width > 0:
                prune_thr = ms[cur_width - 1].prune_value
        if __debug__:
            logger.debug(f'Pruned {cnt_pruned} matchings, return {prune_thr}')
        return prune_thr


class BaseMatcher:

    def __init__(self, map_con, obs_noise=1, max_dist_init=None, max_dist=None, min_prob_norm=None,
                 non_emitting_states=True, max_lattice_width=None,
                 only_edges=True, obs_noise_ne=None, matching=BaseMatching,
                 non_emitting_length_factor=0.75, **kwargs):
        """Initialize a matcher for map matching.

        This a generic base class to be used by matchers. This class itself
        does not implement a working matcher.

        Distances are in meters when using latitude-longitude.

        :param map_con: Map object to connect to map database
        :param obs_noise: Standard deviation of noise
        :param obs_noise_ne: Standard deviation of noise for non-emitting states (is set to obs_noise if not give)
        :param max_dist_init: Maximum distance from start location (if not given, uses max_dist)
        :param max_dist: Maximum distance from path (this is a hard cut, min_prob_norm should be better)
        :param min_prob_norm: Minimum normalized probability of observations (ema)
        :param non_emitting_states: Allow non-emitting states. A non-emitting state is a state that is
            not associated with an observation. Here we assume it can be associated with a location in between
            two observations to allow for pruning. It is advised to set min_prob_norm and/or max_dist to avoid
            visiting all possible nodes in the graph.
        :param max_lattice_width: Only continue from a limited number of states (thus locations) for a given observation.
            This possibly speeds up the matching by a lot.
            If there are more possible next states, the states with the best likelihood so far are selected.
            The other states are 'delayed'. If the matching is continued later with a larger value using
            `increase_max_lattice_width`, the algorithms continuous from these delayed states.
        :param only_edges: Do not include nodes as states, only edges. This is the typical setting for HMM methods.
        :param matching: Matching type
        :param non_emitting_length_factor: Reduce the probability of a sequence of non-emitting states the longer it
            is. This can be used to prefer shorter paths. This is separate from the transition probabilities because
            transition probabilities are averaged for non-emitting states and thus the length is also averaged out.

        To define a custom transition and/or emission probability distribtion, overwrite the following functions:

        - :meth:`logprob_trans`
        - :meth:`logprob_obs`

        """
        self.map = map_con  # type: BaseMap
        if max_dist:
            self.max_dist = max_dist
        else:
            self.max_dist = np.inf
        if max_dist_init:
            self.max_dist_init = max_dist_init
        else:
            self.max_dist_init = self.max_dist
        if min_prob_norm:
            self.min_logprob_norm = math.log(min_prob_norm)
        else:
            self.min_logprob_norm = -np.inf
        logger.debug(f"Matcher.min_logprob_norm = {self.min_logprob_norm}, Matcher.max_dist = {self.max_dist}")
        self.obs_noise = obs_noise
        if obs_noise_ne is None:
            self.obs_noise_ne = obs_noise
        else:
            self.obs_noise_ne = obs_noise_ne

        self.path = None
        self.lattice = None  # type: Optional[dict[int,LatticeColumn]]
        # Best path through lattice:
        self.lattice_best = None  # type: Optional[list[BaseMatching]]
        self.node_path = None  # type: Optional[list[str]]
        self.matching = matching
        self.non_emitting_states = non_emitting_states  # type: bool
        self.non_emitting_states_maxnb = 100
        self.max_lattice_width = max_lattice_width  # type: Optional[int]
        self.only_edges = only_edges  # type: bool
        self.expand_now = 0  # all m.delayed <= expand_upto will be expanded
        self.early_stop_idx = None

        # Penalties
        self.ne_length_factor_log = math.log(non_emitting_length_factor)

    def logprob_trans(self, prev_m, edge_m, edge_o,
                      is_prev_ne=False, is_next_ne=False):
        # type: (BaseMatcher, BaseMatching, Segment, Segment, bool, bool) -> Tuple[float, Dict[str, Any]]
        """Transition probability.

        Note: In contrast with a regular HMM, this cannot be a probability density function, it needs
              to be a proper probability (thus values between 0.0 and 1.0).

        :return: probability, properties that are passed to the matching object
        """
        return 0, {}  # All probabilities are 1 (thus technically not a distribution)

    def logprob_obs(self, dist, prev_m, new_edge_m, new_edge_o, is_ne=False):
        """Emission probability.

        Note: In contrast with a regular HMM, this cannot be a probability density function, it needs
              to be a proper probability (thus values between 0.0 and 1.0).

        :return: probability, properties that are passed to the matching object
        """
        return 0, {}

    def match_gpx(self, gpx_file, unique=True):
        """Map matching from a gpx file"""
        from ..util.gpx import gpx_to_path
        path = gpx_to_path(gpx_file)
        return self.match(path, unique=unique)

    def do_stop(self, logprob_norm, dist, logprob_trans, logprob_obs):
        if logprob_norm < self.min_logprob_norm:
            logger.debug(f"   | Stopped trace: norm(log(Pr)) too small: {logprob_norm} < {self.min_logprob_norm}"
                         f"  -- lPr_t = {logprob_trans:.3f}, lPr_o = {logprob_obs:.3f}")
            return True
        if dist > self.max_dist:
            logger.debug(f"   | Stopped trace: distance too large: {dist} > {self.max_dist}")
            return True
        return False

    def _insert(self, m_next):
        return self.lattice[m_next.obs].upsert(m_next)

    def match(self, path, unique=False, tqdm=None, expand=False):
        """Dynamic Programming based (HMM-like) map matcher.

        If the matcher fails to match the entire path, the last matched index is returned.
        This index can be used to run the matcher again from that observation onwards.

        :param path: list[Union[tuple[lat, lon], tuple[lat, lon, time]]
        :param unique: Only retain unique nodes in the sequence (avoid repetitions)
        :param tqdm: Use a tqdm progress reporter (default is None)
        :param expand: Expand the current lattice (delayed matches)
        :return: Tuple of (List of BaseMatching, index of last observation that was matched)
        """
        if __debug__:
            logger.debug("Start matching path of length {}".format(len(path)))

        # Initialisation
        if expand:
            self.expand_now += 1
            if self.path != path:
                is_path_extended = True
                if len(path) > len(self.path):
                    for pi, spi in zip(path, self.path):
                        if pi != spi:
                            is_path_extended = False
                            break
                else:
                    is_path_extended = False
                if is_path_extended:
                    self.lattice[len(self.path) - 1].set_delayed(self.expand_now)
                    for obs_idx in range(len(self.path), len(path)):
                        if obs_idx not in self.lattice:
                            self.lattice[obs_idx] = LatticeColumn(obs_idx)
                    self.path = path
                else:
                    raise Exception(f'Cannot expand for a new path, should be the same path (or an extension).')
        else:
            self.path = path
            self.expand_now = 0

        nb_start_nodes = self._create_start_nodes(use_edges=self.only_edges)
        if nb_start_nodes == 0:
            self.lattice_best = []
            return [], 0
        if __debug__ and logger.isEnabledFor(logging.DEBUG):
            self.print_lattice(obs_idx=0, label_width=default_label_width, debug=True)

        # Start iterating over observations 1..end
        t_start = time.time()
        iterator = range(1, len(path))
        if tqdm:
            iterator = tqdm(iterator)
        self.early_stop_idx = None
        for obs_idx in iterator:
            if __debug__:
                logger.debug("--- obs {} --- {} ---".format(obs_idx, self.path[obs_idx]))
            # check if early stopping has occured
            cnt_lat_size_not_zero = False
            for m_tmp in self.lattice[obs_idx - 1].values(0):
                if not m_tmp.stop:
                    cnt_lat_size_not_zero = True
                    break
            # if len(self.lattice[obs_idx - 1]) == 0:
            if not cnt_lat_size_not_zero:
                if __debug__:
                    logger.debug("No solutions found anymore")
                self.early_stop_idx = obs_idx - 1
                logger.info(f'Stopped early at observation {self.early_stop_idx}')
                break
            # Expand matches
            self._match_states(obs_idx)
            if self.non_emitting_states:
                # Fill in non-emitting states between previous and current observation
                self._match_non_emitting_states(obs_idx - 1, expand=expand)
            if self.max_lattice_width:
                # Prune again if non_emitting_states reactives matches from match_states
                self.lattice[obs_idx].prune(0, self.max_lattice_width, self.expand_now)
            if __debug__ and logger.isEnabledFor(logging.DEBUG):
                self.print_lattice(obs_idx=obs_idx, label_width=default_label_width, debug=True)
                logger.debug(f"--- end obs {obs_idx} ---")

        t_delta = time.time() - t_start
        logger.info("--- end ---")
        logger.info("Build lattice in {} seconds".format(t_delta))

        # Backtrack to find best path
        if not self.early_stop_idx:
            one_no_stop = False
            for m in self.lattice[len(path) - 1].values_all():  # todo: could be values(0) ?
                if not m.stop:
                    one_no_stop = True
                    break
            if not one_no_stop:
                self.early_stop_idx = len(path) - 1
        if self.early_stop_idx is not None:
            if self.early_stop_idx == 0:
                self.lattice_best = []
                return [], 0
            start_idx = self.early_stop_idx - 1
        else:
            start_idx = len(self.path) - 1
        node_path = self._build_node_path(start_idx, unique)
        return node_path, start_idx

    def _skip_ne_states(self, prev_m):
        # type: (BaseMatcher, BaseMatching) -> bool
        return False

    def _create_start_nodes(self, use_edges=True):
        """Find those nodes that are close to the first point in the path.

        :return: Number of created start points.
        """
        # Initialisation on first observation
        if self.expand_now > 0:
            # No need to search for new points, only activate delayed matches
            self.lattice[0].prune(0, self.max_lattice_width, self.expand_now)
            return len(self.lattice[0])

        t_start = time.time()
        self.lattice = dict()
        for obs_idx in range(len(self.path)):
            self.lattice[obs_idx] = LatticeColumn(obs_idx)

        if use_edges:
            nodes = self.map.edges_closeto(self.path[0], max_dist=self.max_dist_init)
        else:
            nodes = self.map.nodes_closeto(self.path[0], max_dist=self.max_dist_init)
        if __debug__:
            logger.debug("--- obs {} --- {} ---".format(0, self.path[0]))
        t_delta = time.time() - t_start
        logger.info("Initialized lattice with {} starting points in {} seconds".format(len(nodes), t_delta))
        if len(nodes) == 0:
            logger.info(f'Stopped early at observation 0'
                        f', no starting points/edges x found for which '
                        f'|x - ({self.path[0][0]:.2f},{self.path[0][1]:.2f})| < {self.max_dist_init}')
            return 0
        if __debug__:
            logger.debug(self.matching.repr_header())
        logprob_init = 0  # math.log(1.0/len(nodes))
        if use_edges:
            # Search for nearby edges
            for dist_obs, label1, loc1, label2, loc2, pi, ti in nodes:
                if label2 == label1:
                    continue
                edge_m = Segment(label1, loc1, label2, loc2, pi, ti)
                edge_o = Segment(f"O{0}", self.path[0])
                m_next = self.matching.first(logprob_init, edge_m, edge_o, self, dist_obs)
                if m_next is not None:
                    self.lattice[0].upsert(m_next)
                    if __debug__:
                        logger.debug(str(m_next))
        else:
            # Search for nearby nodes
            for dist_obs, label, loc in nodes:
                edge_m = Segment(label, loc)
                edge_o = Segment(f"O{0}", self.path[0])
                m_next = self.matching.first(logprob_init, edge_m, edge_o, self, dist_obs)
                if m_next is not None:
                    self.lattice[0].upsert(m_next)
                    if __debug__:
                        logger.debug(str(m_next))
        if self.max_lattice_width:
            self.lattice[0].prune(0, max_lattice_width=self.max_lattice_width, expand_upto=self.expand_now)
            # if self.non_emitting_states:
            #     self._match_non_emitting_states(0, path)
        return len(self.lattice[0])

    def increase_delayed(self, expand_from=None):
        if expand_from is None:
            expand_from = self.expand_now + 1
        for col in self.lattice.values():
            for colo in col.o:
                for m in colo.values():
                    if m.delayed >= expand_from:
                        m.delayed += 1

    def _match_states(self, obs_idx, prev_lattice=None, max_dist=None, inc_delayed=False):
        """Match states

        :param obs_idx:
        :param prev_lattice: Start from this list instead of the previous
            column in the lattice
        :param max_dist: Use map.*_closeto instead of map.*_nbrto
        :param inc_delayed: Increase delayed property when new state is created
        :return: True is new states have been found, False otherwise.
        """
        if prev_lattice is None:
            prev_lattice = [m for m in self.lattice[obs_idx - 1].values(0) if not m.stop and m.delayed == self.expand_now]
        count = 0
        for m in prev_lattice:  # type: BaseMatching
            if m.stop:
                assert False  # should not happen
                continue
            count += 1
            if m.edge_m.is_point():
                # == Move to neighbour from node ==
                if max_dist is None:
                    nbrs = self.map.nodes_nbrto(m.edge_m.l1)
                else:
                    nbrs = self.map.nodes_closeto(m.edge_m.p1, max_dist=max_dist)
                # print("Neighbours for {}: {}".format(m, nbrs))
                if nbrs is None:
                    if __debug__:
                        logger.debug("No neighbours found for node {}".format(m.edge_m.l1))
                    continue
                if __debug__:
                    logger.debug("   + Move to {} neighbours from node {}".format(len(nbrs), m.edge_m.l1))
                    logger.debug(m.repr_header())
                for nbr_label, nbr_loc in nbrs:
                    # === Move from node to node (or stay on node) ===
                    if not self.only_edges:
                        edge_m = Segment(nbr_label, nbr_loc)
                        edge_o = Segment(f"O{obs_idx}", self.path[obs_idx])
                        m_next = m.next(edge_m, edge_o, obs=obs_idx)
                        if m_next is not None:
                            if inc_delayed:
                                m_next.delayed += 1
                            self._insert(m_next)
                            if __debug__:
                                logger.debug(str(m_next))

                    # === Move from node to edge ===
                    if m.edge_m.l1 != nbr_label:
                        edge_m = Segment(m.edge_m.l1, m.edge_m.p1, nbr_label, nbr_loc)
                        edge_o = Segment(f"O{obs_idx}", self.path[obs_idx])
                        m_next = m.next(edge_m, edge_o, obs=obs_idx)
                        if m_next is not None:
                            if inc_delayed:
                                m_next.delayed += 1
                            self._insert(m_next)
                            if __debug__:
                                logger.debug(str(m_next))
                    else:
                        if __debug__:
                            logger.debug(self.matching.repr_static(('x', f'{nbr_label}-{nbr_label} < self-loop')))

            else:
                # == Move to neighbour from edge ==
                if __debug__:
                    logger.debug("   + Move to neighbour from edge {}".format(m.label))
                    logger.debug(m.repr_header())

                # === Stay on edge ===
                edge_m = Segment(m.edge_m.l1, m.edge_m.p1, m.edge_m.l2, m.edge_m.p2)
                edge_o = Segment(f"O{obs_idx}", self.path[obs_idx])
                m_next = m.next(edge_m, edge_o, obs=obs_idx)
                if m_next is not None:
                    if inc_delayed:
                        m_next.delayed += 1
                    self._insert(m_next)
                    if __debug__:
                        logger.debug(str(m_next))

                # === Move from edge to node ===
                if not self.only_edges:
                    edge_m = Segment(m.edge_m.l2, m.edge_m.p2)
                    edge_o = Segment(f"O{obs_idx}", self.path[obs_idx])
                    m_next = m.next(edge_m, edge_o, obs=obs_idx)
                    if m_next is not None:
                        if inc_delayed:
                            m_next.delayed += 1
                        self._insert(m_next)
                        if __debug__:
                            logger.debug(str(m_next))

                else:
                    # === Move from edge to next edge ===
                    if max_dist is None:
                        nbrs = self.map.edges_nbrto((m.edge_m.l1, m.edge_m.l2))  # type: list
                    else:
                        nbrs = [(l1, p1, l2, p2) for _, l1, p1, l2, p2, _, _
                                in self.map.edges_closeto(m.edge_m.pi, max_dist=max_dist)]
                    if nbrs is None or len(nbrs) == 0:
                        if __debug__:
                            logger.debug(f"No neighbours found for edge {m.edge_m.label}")
                        continue
                    for nbr_label1, nbr_loc1, nbr_label2, nbr_loc2 in nbrs:
                        # same edge is different action, opposite edge should be allowed to return in a one-way street
                        if m.edge_m.l2 != nbr_label2 and m.edge_m.l1 != nbr_label1:
                            edge_m = Segment(nbr_label1, nbr_loc1, nbr_label2, nbr_loc2)
                            edge_o = Segment(f"O{obs_idx}", self.path[obs_idx])
                            m_next = m.next(edge_m, edge_o, obs=obs_idx)
                            if m_next is not None:
                                if inc_delayed:
                                    m_next.delayed += 1
                                self._insert(m_next)
                                if __debug__:
                                    mstr = str(m_next)
                                    logger.debug(mstr)
        if self.max_lattice_width:
            self.lattice[obs_idx].prune(0, self.max_lattice_width, self.expand_now)
        if count == 0:
            if __debug__:
                logger.debug("No active solution found anymore")
            return False
        return True

    def _match_non_emitting_states(self, obs_idx, expand=False):
        """Match sequences of nodes that all refer to the same observation at obs_idx.

        Assumptions:
        This method assumes that the lattice is filled up for both obs_idx and obs_idx + 1.

        :param obs_idx: Index of the first observation used (the second will be obs_idx + 1)
        :return: None
        """
        obs = self.path[obs_idx]
        if obs_idx < len(self.path) - 1:
            obs_next = self.path[obs_idx + 1]
        else:
            obs_next = None
        # The current states are the current observation's states
        if expand:
            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)
        else:
            cur_lattice = dict((m.key, m) for m in self.lattice[obs_idx].values(0) if not (m.stop or m.delayed > 0))
        lattice_toinsert = list()
        # The current best states are the next observation's states if you would ignore non-emitting states
        lattice_best = dict((m.shortkey, m)
                            for m in self.lattice[obs_idx + 1].values(0) if not m.stop)
        lattice_ne = set(m.shortkey
                         for m in self.lattice[obs_idx + 1].values(0) if not m.stop and self._skip_ne_states(m))
        # cur_lattice = set(self.lattice[obs_idx].values())
        nb_ne = 0
        prune_thr = None
        while len(cur_lattice) > 0 and nb_ne < self.non_emitting_states_maxnb:
            nb_ne += 1
            if __debug__:
                logger.debug("--- obs {}:{} --- {} - {} ---".format(obs_idx, nb_ne, obs, obs_next))
            cur_lattice = self._match_non_emitting_states_inner(cur_lattice, obs_idx, obs, obs_next, nb_ne,
                                                                lattice_best, lattice_ne)
            if self.max_lattice_width is not None:
                self.lattice[obs_idx].prune(nb_ne, self.max_lattice_width, self.expand_now, prune_thr)
            # Link to next observation
            self._match_non_emitting_states_end(cur_lattice, obs_idx + 1, obs_next,
                                                lattice_best, expand=expand)
            if self.max_lattice_width is not None:
                prune_thr = self.lattice[obs_idx + 1].prune(0, self.max_lattice_width, self.expand_now, None)
        if self.max_lattice_width is not None:
            self.lattice[obs_idx + 1].prune(0, self.max_lattice_width, self.expand_now, None)
        # logger.info('Used {} levels of non-emitting states'.format(nb_ne))
        # for m in lattice_toinsert:
        #     self._insert(m)

    def _node_in_prev_ne(self, m_next, label):
        """Is the given node already visited in the chain of non-emitting states.

        :param m_next:
        :param label: Node label
        :return: True or False
        """
        # for m in itertools.chain(m_next.prev, m_next.prev_other):
        for m in m_next.prev:  # type: BaseMatching
            if m.obs != m_next.obs:
                return False
            assert(m_next.obs_ne != m.obs_ne)
            # print('prev', m.shortkey, 'checking for ', label)
            # if label == m.shortkey:
            if label in m.nodes:
                return True
            if m.obs_ne == 0:
                return False
            if self._node_in_prev_ne(m, label):
                return True
        return False

    @staticmethod
    def _insert_tmp(m_next, lattice):
        if m_next.key in lattice:
            return lattice[m_next.key].update(m_next)
        else:
            lattice[m_next.key] = m_next
            return True

    def _match_non_emitting_states_inner(self, cur_lattice, obs_idx, obs, obs_next, nb_ne,
                                         lattice_best, lattice_ne):
        # cur_lattice_new = dict()
        cur_lattice_new = self.lattice[obs_idx].dict(nb_ne)
        for m in cur_lattice.values():  # type: BaseMatching
            if m.stop or m.delayed != self.expand_now:
                continue
            if m.shortkey in lattice_ne:
                logger.debug(f"Skip non-emitting states from {m.label}, already visited")
                continue
            # == Move to neighbour edge from edge ==
            if m.edge_m.l2 is not None and self.only_edges:
                nbrs = self.map.edges_nbrto((m.edge_m.l1, m.edge_m.l2))
                # print("Neighbours for {}: {}".format(m, nbrs))
                if nbrs is None or len(nbrs) == 0:
                    if __debug__:
                        logger.debug(f"No neighbours found for edge {m.edge_m.label} ({m.label}, non-emitting)")
                    continue
                for nbr_label1, nbr_loc1, nbr_label2, nbr_loc2 in nbrs:
                    if self._node_in_prev_ne(m, nbr_label2):
                        if __debug__:
                            logger.debug(self.matching.repr_static(('x', '{} < node in prev ne'.format(nbr_label2))))
                        continue
                    # === Move to next edge ===
                    if m.edge_m.l2 != nbr_label2 and m.edge_m.l1 != nbr_label2:
                        edge_m = Segment(nbr_label1, nbr_loc1, nbr_label2, nbr_loc2)
                        edge_o = Segment(f"O{obs_idx}", obs, f"O{obs_idx+1}", obs_next)
                        m_next = m.next(edge_m, edge_o, obs=obs_idx, obs_ne=nb_ne)
                        if m_next is not None:
                            if m_next.key in cur_lattice_new:
                                if m_next.shortkey in lattice_best:
                                    if approx_leq(m_next.dist_obs, lattice_best[m_next.shortkey].dist_obs):
                                        cur_lattice_new[m_next.key].update(m_next)
                                    else:
                                        m_next.stop = True
                                        if __debug__ and logger.isEnabledFor(logging.DEBUG):
                                            logger.debug(f"   | Stopped trace: distance larger than best for key {m_next.shortkey}: "
                                                         f"{m_next.dist_obs} > {lattice_best[m_next.shortkey].dist_obs}")
                                else:
                                    cur_lattice_new[m_next.key].update(m_next)
                            else:
                                if m_next.shortkey in lattice_best:
                                    # if m_next.logprob > lattice_best[m_next.shortkey].logprob:
                                    if approx_leq(m_next.dist_obs, lattice_best[m_next.shortkey].dist_obs):
                                        cur_lattice_new[m_next.key] = m_next
                                        # lattice_best[m_next.shortkey] = m_next
                                        # lattice_toinsert.append(m_next)
                                    else:
                                        if __debug__ and logger.isEnabledFor(logging.DEBUG):
                                            logger.debug(f"   | Stopped trace: distance larger than best for key {m_next.shortkey}: "
                                                         f"{m_next.dist_obs} > {lattice_best[m_next.shortkey].dist_obs}")
                                        m_next.stop = True
                                else:
                                    cur_lattice_new[m_next.key] = m_next
                                    # lattice_best[m_next.shortkey] = m_next
                                    # lattice_toinsert.append(m_next)
                            # cur_lattice_new.add(m_next)
                            if __debug__:
                                logger.debug(str(m_next))
                    else:
                        if __debug__:
                            logger.debug(self.matching.repr_static(('x', f'{nbr_label1}-{nbr_label2} < goes back (ne)')))
            # == Move to neighbour node from node==
            if m.edge_m.l2 is None and not self.only_edges:
                cur_node = m.edge_m.l1
                nbrs = self.map.nodes_nbrto(cur_node)
                if nbrs is None:
                    if __debug__:
                        logger.debug(
                            f"No neighbours found for node {cur_node} ({m.label}, non-emitting)")
                    continue
                if __debug__:
                    logger.debug(
                        f"   + Move to {len(nbrs)} neighbours from node {cur_node} ({m.label}, non-emitting)")
                    logger.debug(m.repr_header())
                for nbr_label, nbr_loc in nbrs:
                    # print(f"self._node_in_prev_ne({m.label}, {nbr_label}) = {self._node_in_prev_ne(m, nbr_label)}")
                    if self._node_in_prev_ne(m, nbr_label):
                        if __debug__:
                            logger.debug(self.matching.repr_static(('x', '{} < node in prev ne'.format(nbr_label))))
                        continue
                    # === Move to next node ===
                    if m.edge_m.l1 != nbr_label:
                        edge_m = Segment(nbr_label, nbr_loc)
                        edge_o = Segment(f"O{obs_idx}", obs, f"O{obs_idx+1}", obs_next)
                        m_next = m.next(edge_m, edge_o, obs=obs_idx, obs_ne=nb_ne)
                        if m_next is not None:
                            if m_next.key in cur_lattice_new:
                                cur_lattice_new[m_next.key].update(m_next)
                            else:
                                if m_next.shortkey in lattice_best:
                                    # if m_next.logprob > lattice_best[m_next.shortkey].logprob:
                                    if m_next.dist_obs < lattice_best[m_next.shortkey].dist_obs:
                                        cur_lattice_new[m_next.key] = m_next
                                        lattice_best[m_next.shortkey] = m_next
                                        # lattice_toinsert.append(m_next)
                                    elif __debug__ and logger.isEnabledFor(logging.DEBUG):
                                        m_next.stop = True
                                        cur_lattice_new[m_next.key] = m_next
                                        # lattice_toinsert.append(m_next)
                                else:
                                    cur_lattice_new[m_next.key] = m_next
                                    lattice_best[m_next.shortkey] = m_next
                                    # lattice_toinsert.append(m_next)
                            # cur_lattice_new.add(m_next)
                            if __debug__:
                                logger.debug(str(m_next))
                    else:
                        if __debug__:
                            logger.debug(f"x  | {m.edge_m.l1}-{nbr_label} < self-loop")

        return cur_lattice_new

    def _match_non_emitting_states_end(self, cur_lattice, obs_idx, obs_next,
                                       lattice_best, expand=False):
        for m in cur_lattice.values():  # type: BaseMatching
            if m.stop or m.delayed > self.expand_now:
                continue
            if m.edge_m.l2 is not None:
                # Move to neighbour edge from edge
                nbrs = self.map.edges_nbrto((m.edge_m.l1, m.edge_m.l2))
                # print("Neighbours for {}: {}".format(m, nbrs))
                if nbrs is None or len(nbrs) == 0:
                    if __debug__:
                        logger.debug("No neighbours found for edge {} ({})".format(m.edge_m.label, m.label))
                    continue
                if __debug__:
                    logger.debug(f"   + Move to {len(nbrs)} neighbours from edge {m.edge_m.label} "
                                 f"({m.label}, non-emitting->emitting)")
                    logger.debug(m.repr_header())
                for nbr_label1, nbr_loc1, nbr_label2, nbr_loc2 in nbrs:
                    if self._node_in_prev_ne(m, nbr_label2):
                        if __debug__:
                            logger.debug(self.matching.repr_static(('x', '{} < node in prev ne'.format(nbr_label2))))
                        continue
                    # Move to next edge
                    if m.edge_m.l1 != nbr_label2 and m.edge_m.l2 != nbr_label2:
                        edge_m = Segment(nbr_label1, nbr_loc1, nbr_label2, nbr_loc2)
                        edge_o = Segment(f"O{obs_idx+1}", obs_next)
                        m_next = m.next(edge_m, edge_o, obs=obs_idx)
                        if m_next is not None:
                            if m_next.shortkey in lattice_best:
                                # if m_next.dist_obs < lattice_best[m_next.shortkey].dist_obs:
                                if m_next.logprob > lattice_best[m_next.shortkey].logprob:
                                    lattice_best[m_next.shortkey] = m_next
                                    # lattice_toinsert.append(m_next)
                                    self.lattice[obs_idx].upsert(m_next)
                                elif __debug__ and logger.isEnabledFor(logging.DEBUG):
                                    m_next.stop = True
                                    # lattice_toinsert.append(m_next)
                                    self.lattice[obs_idx].upsert(m_next)
                            else:
                                lattice_best[m_next.shortkey] = m_next
                                # lattice_toinsert.append(m_next)
                                self.lattice[obs_idx].upsert(m_next)
                            if __debug__:
                                logger.debug(str(m_next))
                    else:
                        if __debug__:
                            logger.debug(self.matching.repr_static(('x', '{} < going back'.format(nbr_label2))))
            else:  # m.edge_m.l2 is None:
                # Move to neighbour node from node
                cur_node = m.edge_m.l1
                nbrs = self.map.nodes_nbrto(cur_node)
                # print("Neighbours for {}: {}".format(m, nbrs))
                if nbrs is None:
                    if __debug__:
                        logger.debug("No neighbours found for node {}".format(cur_node, m.label))
                    continue
                if __debug__:
                    logger.debug(f"   + Move to {len(nbrs)} neighbours from node {cur_node} "
                                 f"({m.label}, non-emitting->emitting)")
                    logger.debug(m.repr_header())
                for nbr_label, nbr_loc in nbrs:
                    if self._node_in_prev_ne(m, nbr_label):
                        if __debug__:
                            logger.debug(self.matching.repr_static(('x', '{} < node in prev ne'.format(nbr_label))))
                        continue
                    # Move to next node
                    if m.edge_m.l1 != nbr_label:
                        # edge_m = Segment(m.edge_m.l1, m.edge_m.p1, nbr_label, nbr_loc)
                        edge_m = Segment(nbr_label, nbr_loc)
                        edge_o = Segment(f"O{obs_idx+1}", obs_next)
                        m_next = m.next(edge_m, edge_o, obs=obs_idx)
                        if m_next is not None:
                            if m_next.shortkey in lattice_best:
                                # if m_next.dist_obs < lattice_best[m_next.shortkey].dist_obs:
                                if m_next.logprob > lattice_best[m_next.shortkey].logprob:
                                    lattice_best[m_next.shortkey] = m_next
                                    # lattice_toinsert.append(m_next)
                                    self.lattice[obs_idx].upsert(m_next)
                                elif __debug__ and logger.isEnabledFor(logging.DEBUG):
                                    m_next.stop = True
                                    # lattice_toinsert.append(m_next)
                                    self.lattice[obs_idx].upsert(m_next)
                            else:
                                lattice_best[m_next.shortkey] = m_next
                                # lattice_toinsert.append(m_next)
                                self.lattice[obs_idx].upsert(m_next)
                            if __debug__:
                                logger.debug(str(m_next))
                    else:
                        if __debug__:
                            logger.debug(self.matching.repr_static(('x', '{} < self-loop'.format(nbr_label))))

    def get_matching(self, identifier=None):
        m = None  # type: Optional[BaseMatching]
        if isinstance(identifier, BaseMatching):
            m = identifier
        elif identifier is None:
            col = self.lattice[len(self.lattice) - 1]
            for curm in col.values_all():
                if m is None or curm.logprob > m.logprob:
                    m = curm
        elif type(identifier) is int:
            # If integer, search for the best matching at this index in the lattice
            for cur_m in self.lattice[identifier].values_all():  # type:BaseMatching
                if not cur_m.stop and (m is None or cur_m.logprob > m.logprob):
                    m = cur_m
        elif type(identifier) is str:
            # If string, try to parse identifier
            parts = identifier.split('-')
            idx, ne, key = None, None, None
            if len(parts) == 4:
                nodea, nodeb, idx, ne = [int(part) for part in parts]
                key = (nodea, nodeb, idx, ne)
                col = self.lattice[idx]  # type: LatticeColumn
                col_ne = col.o[ne]
                m = col_ne[key]
            elif len(parts) == 3:
                node, idx, ne = [int(part) for part in parts]
                key = (node, idx, ne)
                col = self.lattice[idx]  # type: LatticeColumn
                col_ne = col.o[ne]
                m = col_ne[key]
            elif len(parts) == 1:
                m = None
                l1 = int(parts[0])
                for l in self.lattice.values():  # type: LatticeColumn
                    for curm in l.values_all():
                        if (curm.edge_m.l1 == l1 or curm.edge_m.l2 == l1) and \
                                (m is None or curm.logprob > m.logprob):
                            m = curm
            else:
                raise AttributeError(f'Unknown string format for matching. '
                                     'Expects <node>-<idx>-<ne> or <node>-<node>-<idx>-<ne>.')

        return m

    def get_matching_path(self, start_m):
        """List of Matching objects that end in the given Matching object."""
        start_m = self.get_matching(start_m)
        return self._build_matching_path(start_m)

    def get_node_path(self, start_m, only_nodes=False):
        """List of node/edge names that end in the given Matching object."""
        path = self.get_matching_path(start_m)
        node_path = [m.shortkey for m in path]
        if only_nodes:
            node_path = self.node_path_to_only_nodes(node_path)
        return node_path

    def get_path(self, only_nodes=True, allow_jumps=False, only_closest=True):
        """A list with all the nodes (no edges) the matched path passes through."""
        if only_nodes is False:
            return self.node_path
        if self.node_path is None or len(self.node_path) == 0:
            return []
        path = self.node_path_to_only_nodes(self.node_path, allow_jumps=allow_jumps)
        if only_closest:
            m = self.lattice_best[0]
            if m.edge_m.ti > 0.5:
                path.pop(0)
        return path

    def node_path_to_only_nodes(self, path, allow_jumps=False):
        """Path of nodes and edges to only nodes.

        :param path: List of node names or edges as (node name, node name)
        :param allow_jumps: Allow a path over edges that are not connected.
            This occurs when matches are added without an edge, for example,
            when searching for edges in the distance neighborhood instead in
            the graph.
        :return: List of node names
        """
        nodes = []
        prev_state = path[0]
        if type(prev_state) is tuple:
            nodes.append(prev_state[0])
            nodes.append(prev_state[1])
            prev_node = prev_state[1]
        else:
            nodes.append(prev_state)
            prev_node = prev_state
        for state in path[1:]:
            if state == prev_state:
                continue
            if type(state) is not tuple:
                if state != prev_node:
                    nodes.append(state)
                    prev_node = state
            elif type(state) is tuple:
                if state[0] == prev_node:
                    if state[1] != prev_node:
                        nodes.append(state[1])
                        prev_node = state[1]
                elif state[1] == prev_node:
                    if state[0] != prev_node:
                        nodes.append(state[0])
                        prev_node = state[0]
                elif not allow_jumps:
                    raise Exception(f"State {state} does not have as previous node {prev_node}")
                else:
                    nodes.append(state[0])
                    nodes.append(state[1])
                    prev_node = state[1]
            else:
                raise Exception(f"Unknown type of state: {state} ({type(state)})")
            prev_state = state
        return nodes

    def _build_matching_path(self, start_m, max_depth=None):
        lattice_best = []
        node_max = start_m
        cur_depth = 0
        if __debug__ and logger.isEnabledFor(logging.DEBUG):
            logger.debug(self.matching.repr_header(stop="             "))
        logger.debug("Start ({}): {}".format(node_max.obs, node_max))
        lattice_best.append(node_max)
        if node_max.is_emitting():
            cur_depth += 1
        # for obs_idx in reversed(range(start_idx)):
        if max_depth is None:
            max_depth = len(self.lattice) + 1
        while cur_depth < max_depth and len(node_max.prev) > 0:
            node_max_last = node_max
            node_max: Optional[BaseMatching] = None
            for prev_m in node_max_last.prev:
                if prev_m is not None and (node_max is None or prev_m.logprob > node_max.logprob):
                    node_max = prev_m
            if node_max is None:
                logger.error("Did not find a matching node for path point at index {}. ".format(node_max_last.obs) +
                             "Stopped building path.")
                break
            logger.debug("Max   ({}): {}".format(node_max.obs, node_max))
            lattice_best.append(node_max)
            if node_max.is_emitting():
                cur_depth += 1
        lattice_best = list(reversed(lattice_best))
        return lattice_best

    def _build_node_path(self, start_idx, unique=True, max_depth=None, last_is_e=False):
        """Build the path from the lattice.

        :param start_idx:
        :param unique:
        :param max_depth:
        :param last_is_e: Last matched lattice node should be an emitting state.
            In case the matching stops early, the longest path can be in between two observations
            and thus be a nonemitting state (which by definition has a lower probability than the
            last emitting state). If this argument is set to true, the longer match is preferred.
        :return:
        """
        node_max = None
        node_max_ne = 0
        if last_is_e:
            for m in self.lattice[start_idx].values_all():  # type:BaseMatching
                if not m.stop and (node_max is None or m.logprob > node_max.logprob):
                    node_max = m
        else:
            for m in self.lattice[start_idx].values_all():  # type:BaseMatching
                if not m.stop and (node_max is None or m.obs_ne > node_max_ne or m.logprob > node_max.logprob):
                    node_max_ne = m.obs_ne
                    node_max = m
        if node_max is None:
            logger.error("Did not find a matching node for path point at index {}".format(start_idx))
            return None

        self.lattice_best = self._build_matching_path(node_max, max_depth)

        node_path = [m.shortkey for m in self.lattice_best]
        if unique:
            self.node_path = []
            prev_node = None
            for node in node_path:
                if node != prev_node:
                    self.node_path.append(node)
                    prev_node = node
        else:
            self.node_path = node_path
        return self.node_path

    def increase_max_lattice_width(self, max_lattice_width, unique=False, tqdm=None):
        """Increase the value for max_lattice_width and continue the matching with all
        paths that were ignored so far (up to the new max_lattice_width).

        This is useful when the matcher is first run with a small max_lattice_width
        to be fast, but when the true path is not obvious and excluded from the first
        guesses. When the matcher stops early, this method allows to easily expand the
        search space.

        :param max_lattice_width: New maximal number of paths to consider
        :param unique: See match method
        :param tqdm: See match method
        """
        self.max_lattice_width = max_lattice_width
        return self.match(self.path, unique=unique, tqdm=tqdm, expand=True)

    def continue_with_distance(self, from_matches=None, k=2, nb_obs=2, max_dist=None):
        """Continue the matcher but ignore edges and allow jumps
        to nearby edged.

        :param from_matches: Search in the neigborhood of these matches
        :param k: If from_matches is not given, the k best matches are used
            in the last nb_obs observations since last early_stop_idx
        :praram nb_obs: If from_matches is not given, the k best matches are used
            in the last nb_obs observations since last early_stop_idx
        :param max_dist: Add edges that are maximally max_dist away from the previous
            match. If none, self.max_dist * 3 is used.
        """
        if from_matches is None:
            from_matches = self.best_last_matches(k=k, nb_obs=nb_obs)
        self.increase_delayed()
        if max_dist is None:
            max_dist = self.max_dist * 3
        for obs_idx, cur_matches in from_matches.items():
            self._match_states(obs_idx, prev_lattice=cur_matches, max_dist=max_dist, inc_delayed=True)

    def path_bb(self):
        """Get boundig box of matched path (if it exists, otherwise return None)."""
        path = self.path
        plat, plon = islice(zip(*path), 2)
        lat_min, lat_max = min(plat), max(plat)
        lon_min, lon_max = min(plon), max(plon)
        bb = lat_min, lon_min, lat_max, lon_max
        return bb

    def print_lattice(self, file=None, obs_idx=None, obs_ne=0, label_width=None, debug=False):
        if debug:
            xprint = logger.debug
        else:
            if file is None:
                file = sys.stdout
            xprint = lambda arg: print(arg, file=file)
        # print("Lattice:", file=file)
        if obs_idx is not None:
            idxs = [obs_idx]
        else:
            idxs = range(len(self.lattice))
        for idx in idxs:
            if len(self.lattice[idx]) > 0:
                if label_width is None:
                    label_width = 0
                    for m in self.lattice[idx].values(obs_ne):
                        label_width = max(label_width, len(str(m.label)))
                xprint("--- obs {} ---".format(idx))
                xprint(self.matching.repr_header(label_width=label_width))
                for m in sorted(self.lattice[idx].values(obs_ne), key=lambda t: str(t.label)):
                    xprint(m.__str__(label_width=label_width))

    def lattice_dot(self, file=None, precision=None, render=False):
        """Write the lattice as a Graphviz DOT file.

        :param file: File object to print to. Prints to stdout if None.
        :param precision: Precision of (log) probabilities.
        :param render: Try to render the generated Graphviz file.
        """
        if file is None:
            file = sys.stdout
        if precision is None:
            prfmt = ''
        else:
            prfmt = f'.{precision}f'
        print('digraph lattice {', file=file)
        print('\trankdir=LR;', file=file)
        # Vertices
        for idx_ob in range(len(self.lattice)):
            col = self.lattice[idx_ob]
            for idx_ne in range(len(col)):
                ms = col.values(idx_ne)
                if len(ms) == 0:
                    continue
                cnames = [(m.obs_ne, m.cname, m.stop, m.delayed) for m in ms]
                cnames.sort()
                cur_obs_ne = -1
                print('\t{\n\t\trank=same; ', file=file)
                for obs_ne, cname, stop, delayed in cnames:
                    if obs_ne != cur_obs_ne:
                        if cur_obs_ne != -1:
                            print('\t};\n\t{\n\t\trank=same; ', file=file)
                        cur_obs_ne = obs_ne
                    if stop:
                        options = 'label="{} x",color=gray,fontcolor=gray'.format(cname)
                    elif delayed > self.expand_now:
                        options = 'label="{} d{}",color=gray,fontcolor=gray'.format(cname, delayed)
                    elif self.expand_now != 0:
                        options = 'label="{} d{}"'.format(cname, delayed)
                    else:
                        options = 'label="{}  "'.format(cname)
                    print('\t\t{} [{}];'.format(cname, options), file=file)
                print('\t};', file=file)
        # Edges
        for idx_ob in range(len(self.lattice)):
            col = self.lattice[idx_ob]
            for idx_ne in range(len(col)):
                ms = col.values(idx_ne)
                if len(ms) == 0:
                    continue
                for m in ms:
                    for mp in m.prev:
                        if m.stop or m.delayed > self.expand_now:
                            options = ',color=gray,fontcolor=gray'
                        else:
                            options = ''
                        print(f'\t {mp.cname} -> {m.cname} [label="{m.logprob:{prfmt}}"{options}];', file=file)
                    for mp in m.prev_other:
                        if m.stop or m.delayed > self.expand_now:
                            options = ',color=gray,fontcolor=gray'
                        else:
                            options = ''
                        print(f'\t {mp.cname} -> {m.cname} [color=gray,label="{m.logprob:{prfmt}}"{options}];', file=file)
        print('}', file=file)
        if render and file is not None:
            import subprocess as sp
            from pathlib import Path
            from io import TextIOWrapper
            if isinstance(file, Path):
                fn = str(file.canonical())
            elif isinstance(file, TextIOWrapper):
                file.flush()
                fn = file.name
            else:
                fn = str(file)
            cmd = ['dot', '-Tpdf', '-O', fn]
            logger.debug(' '.join(cmd))
            sp.call(cmd)

    def print_lattice_stats(self, file=None, verbose=False):
        if file is None:
            file = sys.stdout
        print("Stats lattice", file=file)
        print("-------------", file=file)
        stats = OrderedDict()
        stats["nbr levels"] = len(self.lattice) if self.lattice else "?"
        total_nodes = 0
        max_nodes = 0
        min_nodes = 9999999
        if self.lattice:
            sizes = []
            for idx in range(len(self.lattice)):
                level = self.lattice[idx].values(0)
                # stats["#nodes[{}]".format(idx)] = len(level)
                sizes.append(len(level))
                total_nodes += len(level)
                if len(level) < min_nodes:
                    min_nodes = len(level)
                if len(level) > max_nodes:
                    max_nodes = len(level)
            stats["nbr lattice"] = total_nodes
            if verbose:
                stats["nbr lattice[level]"] = ", ".join([str(s) for s in sizes])
            stats["avg lattice[level]"] = total_nodes/len(self.lattice)
            stats["min lattice[level]"] = min_nodes
            stats["max lattice[level]"] = max_nodes
        if self.lattice_best and len(self.lattice_best) > 0:
            stats["avg obs distance"] = np.mean([m.dist_obs for m in self.lattice_best])
            stats["last logprob"] = self.lattice_best[-1].logprob
            stats["last length"] = self.lattice_best[-1].length
            stats["last norm logprob"] = self.lattice_best[-1].logprob / self.lattice_best[-1].length
            if verbose:
                stats["best logprob"] = ", ".join(["{:.3f}".format(m.logprob) for m in self.lattice_best])
                stats["best norm logprob"] = \
                    ", ".join(["{:.3f}".format(m.logprob/m.length) for i, m in enumerate(self.lattice_best)])
                stats["best norm prob"] = \
                    ", ".join(["{:.3f}".format(math.exp(m.logprob/m.length)) for i, m in enumerate(self.lattice_best)])
        for key, val in stats.items():
            print("{:<24} : {}".format(key, val), file=file)

    def node_counts(self):
        if self.lattice is None:
            return None
        counts = defaultdict(lambda: 0)
        for level in self.lattice.values():
            for m in level.values_all():
                counts[m.label] += 1
        return counts

    def inspect_early_stopping(self):
        """Analyze the lattice and try to find most plausible reason why the
        matching stopped early and print to stdout."""
        if self.early_stop_idx is None:
            print("No early stopping.")
            return
        col = self.lattice[self.early_stop_idx - 1]
        print("The last matched nodes or edges were:")
        first_row = True
        ignore = set()
        for ne_i in range(len(col.o) - 1, -1, -1):
            for v in col.o[ne_i].values():
                if v.key not in ignore:
                    if first_row:
                        print(v.repr_header())
                        first_row = False
                    print(v)
                ignore.update(r.key for r in v.prev)

    def best_last_matches(self, k=1, nb_obs=3):
        """Return the k best last matches.

        :param k: Number of best matches to keep for an observation
        :param nb_obs: How many last matched observations to consider
        """
        import heapq
        if self.early_stop_idx is None:
            col_idx = len(self.lattice) - 1
        else:
            col_idx = self.early_stop_idx - 1
        hh = []
        obs_cnt = 0
        while col_idx >= 0 and obs_cnt < nb_obs:
            h = []
            col = self.lattice[col_idx]
            col_oneselected = False
            for ne_i in range(len(col.o) - 1, -1, -1):
                for v in col.o[ne_i].values():
                    if v.stop:
                        continue
                    if len(h) < k:
                        heapq.heappush(h, (v.logprob, v))
                        col_oneselected = True
                    elif v.logprob > h[0][0]:
                        heapq.heappop(h)
                        heapq.heappush(h, (v.logprob, v))
                        col_oneselected = True
            hh.extend(h)
            if col_oneselected is False:
                print(f'break in {col_idx=}')
                break
            col_idx -= 1
            obs_cnt += 1
        result = defaultdict(list)
        for m in hh:
            m = m[1]
            result[m.obs + 1].append(m)
        # return [m[1] for m in hh]
        return result

    def copy_lastinterface(self, nb_interfaces=1):
        """Copy the current matcher and keep the last interface as the start point.

        This method allows you to perform incremental matching without keeping the entire
        lattice in memory.

        You need to run :meth:`match_incremental` on this object to continue from the existing
        (partial) lattice. Otherwise, if you use :meth:`match`, it will be overwritten.

        Open question, if there is no need to keep track of older lattices, it will probably
        be more efficient to clear the older parts of the interface instead of copying the newer
        parts.

        :param nb_interfaces: Nb of interfaces (columns in lattice) to keep. Default is 1, the last one.
        :return: new Matcher object
        """
        matcher = self.__class__(self.map, obs_noise=self.obs_noise, max_dist_init=self.max_dist_init,
                                 max_dist=self.max_dist, min_prob_norm=self.min_logprob_norm,
                                 non_emitting_states=self.non_emitting_states,
                                 max_lattice_width=self.max_lattice_width, only_edges=self.only_edges,
                                 obs_noise_ne=self.obs_noise_ne, matching=self.matching,
                                 avoid_goingback=self.avoid_goingback,
                                 non_emitting_length_factor=math.exp(self.ne_length_factor_log))
        matcher.lattice = []
        matcher.path = []
        for int_i in range(len(self.lattice) - nb_interfaces, len(self.lattice)):
            matcher.lattice.append(self.lattice[int_i])
            matcher.path.append(self.path[int_i])
        return matcher

    @property
    def path_pred(self):
        """The matched path, both nodes and/or edges (depending on your settings)."""
        return self.node_path

    @property
    def path_pred_onlynodes(self):
        """A list with all the nodes (no edges) the matched path passes through."""
        return self.get_path(only_nodes=True, allow_jumps=False)

    @property
    def path_pred_onlynodes_withjumps(self):
        """A list with all the nodes (no edges) the matched path passes through."""
        return self.get_path(only_nodes=True, allow_jumps=True)

    def path_pred_distance(self):
        """Total distance of the matched path."""
        if self.lattice_best is None:
            return None
        if len(self.lattice_best) == 1:
            return 0
        dist = 0
        m_prev = self.lattice_best[0]
        for idx, m in enumerate(self.lattice_best[1:]):
            if m_prev.edge_m.label != m.edge_m.label and m_prev.edge_m.l2 == m.edge_m.l1:
                # Go over the connection between two edges to compute the distance
                cdist = self.map.distance(m_prev.edge_m.pi, m_prev.edge_m.p2)
                cdist += self.map.distance(m_prev.edge_m.p2, m.edge_m.pi)
            else:
                cdist = self.map.distance(m_prev.edge_m.pi, m.edge_m.pi)
            dist += cdist
            m_prev = m
        return dist

    def path_distance(self):
        """Total distance of the observations."""
        if self.lattice_best is None:
            return None
        if len(self.lattice_best) == 1:
            return 0
        dist = 0
        m_prev = self.lattice_best[0]
        for m in self.lattice_best[1:]:
            dist += self.map.distance(m_prev.edge_o.pi, m.edge_o.pi)
            m_prev = m
        return dist

    def path_all_distances(self):
        """Return a list of all distances between observed trace and map.

        One entry for each point in the map and point in the trace that are mapped to each other.
        In case non-emitting nodes are used, extra entries can be present where a point in the trace
        or a point in the map is mapped to a segment.
        """
        path = self.lattice_best
        dists = [m.dist_obs for m in path]
        return dists


================================================
FILE: leuvenmapmatching/matcher/distance.py
================================================
# encoding: utf-8
"""
leuvenmapmatching.matcher.distance
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:author: Wannes Meert
:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.
:license: Apache License, Version 2.0, see LICENSE for details.
"""
import logging
import math

from .base import BaseMatching, BaseMatcher
from ..util.segment import Segment
from ..util.debug import printd

MYPY = False
if MYPY:
    from typing import Tuple, Any, Dict


logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching")


class DistanceMatching(BaseMatching):
    __slots__ = ['d_s', 'd_o', 'lpe', 'lpt']  # Additional fields

    def __init__(self, *args, d_s=0.0, d_o=0.0, lpe=0.0, lpt=0.0, **kwargs):
        """

        :param args: Arguments for BaseMatching
        :param d_s: Distance between two (interpolated) states
        :param d_o: Distance between two (interpolated) observations
        :param lpe: Log probability of emission
        :param lpt: Log probablity of transition
        :param kwargs: Arguments for BaseMatching
        """
        super().__init__(*args, **kwargs)
        self.d_o: float = d_o
        self.d_s: float = d_s
        self.lpe: float = lpe
        self.lpt: float = lpt

    def _update_inner(self, m_other):
        # type: (DistanceMatching, DistanceMatching) -> None
        super()._update_inner(m_other)
        self.d_s = m_other.d_s
        self.d_o = m_other.d_o
        self.lpe = m_other.lpe
        self.lpt = m_other.lpt

    @staticmethod
    def repr_header(label_width=None, stop=""):
        res = BaseMatching.repr_header(label_width)
        res += f" {'dt(o)':<6} | {'dt(s)':<6} |"
        if logger.isEnabledFor(logging.DEBUG):
            res += f" {'lg(Pr-t)':<9} | {'lg(Pr-e)':<9} |"
        return res

    def __str__(self, label_width=None):
        res = super().__str__(label_width)
        res += f" {self.d_o:>6.2f} | {self.d_s:>6.2f} |"
        if logger.isEnabledFor(logging.DEBUG):
            res += f" {self.lpt:>9.2f} | {self.lpe:>9.2f} |"
        return res

    def __repr__(self):
        return self.label


class DistanceMatcher(BaseMatcher):
    """
    Map Matching that takes into account the distance between matched locations on the map compared to
    the distance between the observations (that are matched to these locations). It thus prefers matched
    paths that have a similar distance than the observations.

    Inspired on the method presented in:

        P. Newson and J. Krumm. Hidden markov map matching through noise and sparseness.
        In Proceedings of the 17th ACM SIGSPATIAL international conference on advances
        in geographic information systems, pages 336–343. ACM, 2009.

    The options available in :class:``BaseMatcher`` are inherited. Additionally, this class
    offers:

    - Transition probability is lower if the distance between observations and states is different
    - Transition probability is lower if the the next match is going back on an edge or to a previous edge
    - Transition probability is lower if two neighboring states represent not connected edges
    - Skip non-emitting states if distance between states and observations is close to each other
    """

    def __init__(self, *args, **kwargs):
        """Create a new object.

        :param map_con: Map object to connect to map database
        :param obs_noise: Standard deviation of noise
        :param obs_noise_ne: Standard deviation of noise for non-emitting states (is set to obs_noise if not given)
        :param max_dist_init: Maximum distance from start location (if not given, uses max_dist)
        :param max_dist: Maximum distance from path (this is a hard cut, min_prob_norm should be better)
        :param min_prob_norm: Minimum normalized probability of observations (ema)
        :param non_emitting_states: Allow non-emitting states. A non-emitting state is a state that is
            not associated with an observation. Here we assume it can be associated with a location in between
            two observations to allow for pruning. It is advised to set min_prob_norm and/or max_dist to avoid
            visiting all possible nodes in the graph.
        :param non_emitting_length_factor: Reduce the probability of a sequence of non-emitting states the longer it
            is. This can be used to prefer shorter paths. This is separate from the transition probabilities because
            transition probabilities are averaged for non-emitting states and thus the length is also averaged out.
        :param max_lattice_width: Restrict the lattice (or possible candidate states per observation) to this value.
            If there are more possible next states, the states with the best likelihood so far are selected.

        :param dist_noise: Standard deviation of difference between distance between states and distance
            between observatoins. If not given, set to obs_noise
        :param dist_noise_ne: If not given, set to dist_noise
        :param restrained_ne: Avoid non-emitting states if the distance between states and between
            observations is close to each other.
        :param avoid_goingback: If true, the probability is lowered for a transition that returns back to a
            previous edges or returns to a position on an edge.

        :param args: Arguments for BaseMatcher
        :param kwargs: Arguments for BaseMatcher
        """

        if not kwargs.get("only_edges", True):
            logger.warning("The MatcherDistance method only works on edges as states. Nodes have been disabled.")
        kwargs["only_edges"] = True
        if "matching" not in kwargs:
            kwargs["matching"] = DistanceMatching
        super().__init__(*args, **kwargs)
        self.use_original = kwargs.get('use_original', False)

        # if not use_original, the following value for beta gives a prob of 0.5 at dist=x_half:
        # beta = np.sqrt(np.power(x_half, 2) / (np.log(2)*2))
        self.dist_noise = kwargs.get('dist_noise', self.obs_noise)
        self.dist_noise_ne = kwargs.get('dist_noise_ne', self.dist_noise)
        self.beta = 2 * self.dist_noise ** 2
        self.beta_ne = 2 * self.dist_noise_ne ** 2

        self.sigma = 2 * self.obs_noise ** 2
        self.sigma_ne = 2 * self.obs_noise_ne ** 2

        self.restrained_ne = kwargs.get('restrained_ne', True)
        self.restrained_ne_thr = 1.25  # Threshold
        self.exact_dt_s = True  # Newson and Krumm is 'True'

        self.avoid_goingback = kwargs.get('avoid_goingback', True)
        self.gobackonedge_factor_log = math.log(0.5)
        self.gobacktoedge_factor_log = math.log(0.5)
        self.first_farend_penalty = math.log(0.75)  # should be > gobacktoedge_factor_log

        self.notconnectededges_factor_log = math.log(0.5)

    def logprob_trans(self, prev_m, edge_m, edge_o,
                      is_prev_ne=False, is_next_ne=False):
        # type: (DistanceMatcher, DistanceMatching, Segment, Segment, bool, bool) -> Tuple[float, Dict[str, Any]]
        """Transition probability.

        The probability is defined with a formula from the exponential family.
        :math:`P(dt) = exp(-d_t^2 / (2 * dist_{noise}^2))`

        with :math:`d_t = |d_s - d_o|,
        d_s = |loc_{prev\_state} - loc_{cur\_state}|,
        d_o = |loc_{prev\_obs} - loc_{cur\_obs}|`

        This function is more tolerant for low values. The intuition is that values under a certain
        distance should all be close to probability 1.0.

        Note: We should also smooth the distance between observations to handle outliers better.

        :param prev_m: Previous matching / state
        :param edge_m: Edge between matchings / states
        :param edge_o: Edge between observations
        :param is_prev_ne: Is previous state non-emitting
        :param is_next_ne: Is the next state non-emitting
        :param dist_o: First output of distance_progress
        :param dist_m: Second output of distance_progress
        :return:
        """
        d_z = self.map.distance(prev_m.edge_o.pi, edge_o.pi)
        is_same_edge = False
        if (prev_m.edge_m.l1 == edge_m.l1 and prev_m.edge_m.l2 == edge_m.l2) or \
                (prev_m.edge_m.l1 == edge_m.l2 and prev_m.edge_m.l2 == edge_m.l1):
            is_same_edge = True
        if ((not self.exact_dt_s) or
                is_same_edge or  # On same edge
                prev_m.edge_m.l2 != edge_m.l1):  # Edges are not connected
            d_x = self.map.distance(prev_m.edge_m.pi, edge_m.pi)
        else:
            # Take into account the curvature
            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)

        if is_next_ne:
            # For non-emitting states, the distances are added
            # Otherwise it can map to a sequence of short segments and stay at the same
            # observation because the difference is then always small.
            d_z += prev_m.d_o
            d_x += prev_m.d_s

        d_t = abs(d_z - d_x)
        # p_dt = 1 / beta * math.exp(-d_t / beta)
        if is_prev_ne or is_next_ne:
            beta = self.beta_ne
        else:
            beta = self.beta
        logprob = -d_t ** 2 / beta

        # Penalties
        if prev_m.edge_m.label == edge_m.label:
            # Staying in same state
            if self.avoid_goingback and edge_m.key == prev_m.edge_m.key and edge_m.ti < prev_m.edge_m.ti:
                # Going back on edge (direction is from p1 to p2 of the segment)
                logprob += self.gobackonedge_factor_log  # Prefer not going back
        elif (prev_m.edge_m.l1, prev_m.edge_m.l2) == (edge_m.l2, edge_m.l1):
            if self.avoid_goingback:
                logprob += self.gobackonedge_factor_log
        else:
            # Moving states
            if prev_m.edge_m.l2 != edge_m.l1:
                # We are moving between states that represent edges that are not connected through a node
                logprob += self.notconnectededges_factor_log
            elif self.avoid_goingback:
                # Goin back on state
                going_back = False
                for m in prev_m.prev:
                    if edge_m.label == m.edge_m.label:
                        going_back = True
                        break
                if going_back:
                    logprob += self.gobacktoedge_factor_log  # prefer not going back

        props = {
            'd_o': d_z,
            'd_s': d_x,
            'lpt': logprob
        }
        return logprob, props

    def logprob_obs(self, dist, prev_m=None, new_edge_m=None, new_edge_o=None, is_ne=False):
        # type: (DistanceMatcher, float, DistanceMatching, Segment, Segment, bool) -> Tuple[float, Dict[str, Any]]
        """Emission probability for emitting states.

        Exponential family:
        :math:`P(dt) = exp(-d_o^2 / (2 * obs_{noise}^2))`

        with :math:`d_o = |loc_{state} - loc_{obs}|`

        """
        if is_ne:
            sigma = self.sigma_ne
        else:
            sigma = self.sigma
        result = -dist ** 2 / sigma
        props = {
            'lpe': result
        }
        return result, props

    def _skip_ne_states(self, next_ne_m):
        # type: (DistanceMatcher, DistanceMatching) -> bool
        # Skip searching for non-emitting states when the distances between nodes
        # on the map are similar to the distances between the observation
        if not self.restrained_ne:
            return False
        if next_ne_m.d_s > 0:
            factor = (next_ne_m.d_o + next_ne_m.dist_obs) / next_ne_m.d_s
        else:
            factor = 0
        if factor < self.restrained_ne_thr:
            logger.debug(f"Skip non-emitting states to {next_ne_m.label}: {factor} < {self.restrained_ne_thr} "
                         "(observations close enough to each other)")
            return True
        return False


================================================
FILE: leuvenmapmatching/matcher/newsonkrumm.py
================================================
# encoding: utf-8
"""
leuvenmapmatching.matcher.newsonkrumm
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Methods similar to Newson Krumm 2009 for comparison purposes.

    P. Newson and J. Krumm. Hidden markov map matching through noise and sparseness.
    In Proceedings of the 17th ACM SIGSPATIAL international conference on advances
    in geographic information systems, pages 336–343. ACM, 2009.



:author: Wannes Meert
:copyright: Copyright 2018 DTAI, KU Leuven and Sirris.
:license: Apache License, Version 2.0, see LICENSE for details.
"""
from scipy.stats import norm
import math
import logging

from .base import BaseMatching, BaseMatcher

logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching")


class NewsonKrummMatching(BaseMatching):
    __slots__ = ['d_s', 'd_o', 'lpe', 'lpt']  # Additional fields

    def __init__(self, *args, d_s=1.0, d_o=1.0, lpe=0.0, lpt=0.0, **kwargs):
        """

        :param args: Arguments for BaseMatching
        :param d_s: Distance between two (interpolated) states
        :param d_o: Distance between two (interpolated) observations
        :param lpe: Log probability of emission
        :param lpt: Log probablity of transition
        :param kwargs: Arguments for BaseMatching
        """
        super().__init__(*args, **kwargs)
        self.d_o: float = d_o
        self.d_s: float = d_s
        self.lpe: float = lpe
        self.lpt: float = lpt

    def _update_inner(self, m_other):
        # type: (NewsonKrummMatching, NewsonKrummMatching) -> None
        super()._update_inner(m_other)
        self.d_s = m_other.d_s
        self.d_o = m_other.d_o
        self.lpe = m_other.lpe
        self.lpt = m_other.lpt

    @staticmethod
    def repr_header(label_width=None, stop=""):
        res = BaseMatching.repr_header(label_width)
        res += f" {'dt(o)':<6} | {'dt(s)':<6} |"
        if logger.isEnabledFor(logging.DEBUG):
            res += f" {'lg(Pr-t)':<9} | {'lg(Pr-e)':<9} |"
        return res

    def __str__(self, label_width=None):
        res = super().__str__(label_width)
        res += f" {self.d_o:>6.2f} | {self.d_s:>6.2f} |"
        if logger.isEnabledFor(logging.DEBUG):
            res += f" {self.lpt:>9.2f} | {self.lpe:>9.2f} |"
        return res


class NewsonKrummMatcher(BaseMatcher):
    """
    Take distance between observations vs states into account. Based on the
    method presented in:

        P. Newson and J. Krumm. Hidden markov map matching through noise and sparseness.
        In Proceedings of the 17th ACM SIGSPATIAL international conference on advances
        in geographic information systems, pages 336–343. ACM, 2009.

    Two important differences:

    * Newson and Krumm use shortest path to handle situations where the distances between
      observations are larger than distances between nodes in the graph. The LeuvenMapMatching
      toolbox uses non-emitting states to handle this. We thus do not implement the shortest
      path algorithm in this class.
    * Transition and emission probability are transformed from densities to probababilities by
      taking the 1 - CDF instead of the PDF.


    Newson and Krumm defaults:

    - max_dist = 200 m
    - obs_noise = 4.07 m
    - beta = 1/6
    - only_edges = True
    """

    def __init__(self, *args, **kwargs):
        """

        :param beta: Default is 1/6
        :param beta_ne: Default is beta
        :param args: Arguments for BaseMatcher
        :param kwargs: Arguments for BaseMatcher
        """

        if not kwargs.get("only_edges", True):
            logger.warning("The MatcherDistance method only works on edges as states. Nodes have been disabled.")
        kwargs["only_edges"] = True
        if "matching" not in kwargs:
            kwargs["matching"] = NewsonKrummMatching
        super().__init__(*args, **kwargs)

        # if not use_original, the following value for beta gives a prob of 0.5 at dist=x_half:
        # beta = np.sqrt(np.power(x_half, 2) / (np.log(2)*2))
        self.beta = kwargs.get('beta', 1/6)
        self.beta_ne = kwargs.get('beta_ne', self.beta)

        self.obs_noise_dist = norm(scale=self.obs_noise)
        self.obs_noise_dist_ne = norm(scale=self.obs_noise_ne)
        self.ne_thr = 1.25

    def logprob_trans(self, prev_m: NewsonKrummMatching, edge_m, edge_o,
                      is_prev_ne=False, is_next_ne=False):
        """Transition probability.

        Main difference with Newson and Krumm: we know all points are connected thus do not compute the
        shortest path but the distance between two points.

        Original PDF:
        p(dt) = 1 / beta * e^(-dt / beta)
        with beta = 1/6

        Transformed to probability:
        P(dt) = p(d > dt) = e^(-dt / beta)

        :param prev_m:
        :param edge_m:
        :param edge_o:
        :param is_prev_ne:
        :param is_next_ne:
        :return:
        """
        d_z = self.map.distance(prev_m.edge_o.pi, edge_o.pi)
        if prev_m.edge_m.label == edge_m.label:
            d_x = self.map.distance(prev_m.edge_m.pi, edge_m.pi)
        else:
            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)
        d_t = abs(d_z - d_x)
        # p_dt = 1 / beta * math.exp(-d_t / beta)
        if is_prev_ne or is_next_ne:
            beta = self.beta_ne
        else:
            beta = self.beta
        # icp_dt = math.exp(-d_t / beta)
        # try:
        #     licp_dt = math.log(icp_dt)
        # except ValueError:
        #     licp_dt = float('-inf')
        licp_dt = -d_t / beta
        props = {
            'd_o': d_z,
            'd_s': d_x,
            'lpt': licp_dt
        }
        return licp_dt, props

    def logprob_obs(self, dist, prev_m, new_edge_m, new_edge_o, is_ne=False):
        """Emission probability for emitting states.

        Original pdf:
        p(d) = N(0, sigma)
        with sigma = 4.07m

        Transformed to probability:
        P(d) = 2 * (1 - p(d > D)) = 2 * (1 - cdf)

        """
        if is_ne:
            result = 2 * (1 - self.obs_noise_dist_ne.cdf(dist))
        else:
            result = 2 * (1 - self.obs_noise_dist.cdf(dist))
        try:
            result = math.log(result)
        except ValueError:
            result = -float("inf")
        props = {
            'lpe': result
        }
        return result, props


================================================
FILE: leuvenmapmatching/matcher/simple.py
================================================
# encoding: utf-8
"""
leuvenmapmatching.matcher.simple
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:author: Wannes Meert
:copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris.
:license: Apache License, Version 2.0, see LICENSE for details.
"""
import math
from scipy.stats import halfnorm, norm

from .base import BaseMatcher, BaseMatching
from ..util.segment import Segment


class SimpleMatching(BaseMatching):
    pass


class SimpleMatcher(BaseMatcher):

    def __init__(self, *args, **kwargs):
        """A simple matcher that prefers paths where each matched location is as close as possible to the
        observed position.

        :param avoid_goingback: Change the transition probability to be lower for the direction the path is coming
            from.
        :param kwargs: Arguments passed to :class:`BaseMatcher`.
        """
        if "matching" not in kwargs:
            kwargs['matching'] = SimpleMatching
        super().__init__(*args, **kwargs)

        self.obs_noise_dist = halfnorm(scale=self.obs_noise)
        self.obs_noise_dist_ne = halfnorm(scale=self.obs_noise_ne)
        # normalize to max 1 to simulate a prob instead of density
        self.obs_noise_logint = math.log(self.obs_noise * math.sqrt(2 * math.pi) / 2)
        self.obs_noise_logint_ne = math.log(self.obs_noise_ne * math.sqrt(2 * math.pi) / 2)

        # Transition probability is divided (in logprob_trans) by this factor if we move back on the
        # current edge.
        self.avoid_goingback = kwargs.get('avoid_goingback', True)
        self.gobackonedge_factor_log = math.log(0.99)
        # Transition probability is divided (in logprob_trans) by this factor if the next state is
        # also the previous state, thus if we go back
        self.gobacktoedge_factor_log = math.log(0.5)
        # Transition probability is divided (in logprob_trans) by this factor if a transition is made
        # This is to try to stay on the same node unless there is a good reason
        self.transition_factor = math.log(0.9)

    def logprob_trans(self, prev_m: BaseMatching, edge_m: Segment, edge_o: Segment,
                      is_prev_ne=False, is_next_ne=False):
        """Transition probability.

        Note: In contrast with a regular HMM, this is not a probability density function, it needs
              to be a proper probability (thus values between 0.0 and 1.0).
        """
        logprob = 0
        if prev_m.edge_m.label == edge_m.label:
            # Staying in same state
            if self.avoid_goingback and edge_m.key == prev_m.edge_m.key and edge_m.ti < prev_m.edge_m.ti:
                # Going back on edge
                logprob += self.gobackonedge_factor_log  # prefer not going back
        else:
            # Moving states
            logprob += self.transition_factor
            if self.avoid_goingback:
                # Goin back on state
                going_back = False
                for m in prev_m.prev:
                    if edge_m.label == m.edge_m.label:
                        going_back = True
                        break
                if going_back:
                    logprob += self.gobacktoedge_factor_log  # prefer not going back
        return logprob, {}  # All probabilities are 1 (thus technically not a distribution)

    def logprob_obs(self, dist, prev_m, new_edge_m, new_edge_o, is_ne=False):
        """Emission probability.

        Note: In contrast with a regular HMM, this is not a probability density function, it needs
              to be a proper probability (thus values between 0.0 and 1.0).
        """
        if is_ne:
            result = self.obs_noise_dist_ne.logpdf(dist) + self.obs_noise_logint_ne
        else:
            result = self.obs_noise_dist.logpdf(dist) + self.obs_noise_logint
        # print("logprob_obs: {} -> {:.5f} = {:.5f}".format(dist, result, math.exp(result)))
        return result, {}


================================================
FILE: leuvenmapmatching/util/__init__.py
================================================
# encoding: utf-8
"""
leuvenmapmatching.util
~~~~~~~~~~~~~~~~~~~~~~

:author: Wannes Meert
:copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris.
:license: Apache License, Version 2.0, see LICENSE for details.
"""

# No automatic loading to avoid dependency on packages such as nvector and gpxpy if not used.


def approx_equal(a, b, rtol=0.0, atol=1e-08):
    return abs(a - b) <= (atol + rtol * abs(b))


def approx_leq(a, b, rtol=0.0, atol=1e-08):
    return (a - b) <= (atol + rtol * abs(b))


================================================
FILE: leuvenmapmatching/util/debug.py
================================================
import logging


logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching")


def printd(*args, **kwargs):
    """Print to debug output."""
    logger.debug(*args, **kwargs)


================================================
FILE: leuvenmapmatching/util/dist_euclidean.py
================================================
# encoding: utf-8
"""
leuvenmapmatching.util.dist_euclidean
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:author: Wannes Meert
:copyright: Copyright 2015-2018 DTAI, KU Leuven and Sirris.
:license: Apache License, Version 2.0, see LICENSE for details.
"""
import logging
import math

import numpy as np

logger = logging.getLogger("be.kuleuven.cs.dtai.mapmatching")


def distance(p1, p2):
    result = math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
    # print("distance({}, {}) -> {}".format(p1, p2, result))
    return result


def distance_point_to_segment(p, s1, s2, delta=0.0):
    p_int, ti = project(s1, s2, p, delta=delta)
    return distance(p_int, p), p_int, ti
    # l1a = np.array(s1)
    # l2a = np.array(s2)
    # pa = np.array(p)
    # return np.linalg.norm(np.cross(l2a - l1a, l1a - pa)) / np.linalg.norm(l2a - l1a)


def distance_segment_to_segment(f1, f2, t1, t2):
    """Distance between segments..

    :param f1: From
    :param f2:
    :param t1: To
    :param t2:
    :return: (distance, proj on f, proj on t, rel pos on f, rel pos on t)
    """
    x1, y1 = f1
    x2, y2 = f2
    x3, y3 = t1
    x4, y4 = t2
    n = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1))
    if np.allclose([n], [0], rtol=0):
        # parallel
        is_parallel = True
        n = 0.0001  # TODO: simulates a point far away
    else:
        is_parallel = False
    u_f = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / n
    u_t = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / n
    xi = x1 + u_f * (x2 - x1)
    yi = y1 + u_f * (y2 - y1)
    changed_f = False
    changed_t = False
    if u_t > 1:
        u_t = 1
        changed_t = True
    elif u_t < 0:
        u_t = 0
        changed_t = True
    if u_f > 1:
        u_f = 1
        changed_f = True
    elif u_f < 0:
        u_f = 0
        changed_f = True
    if not changed_t and not changed_f:
        return 0, (xi, yi), (xi, yi), u_f, u_t
    xf = x1 + u_f 
Download .txt
gitextract_6vcq1ios/

├── .gitignore
├── .readthedocs.yaml
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── docs/
│   ├── Makefile
│   ├── classes/
│   │   ├── map/
│   │   │   ├── BaseMap.rst
│   │   │   ├── InMemMap.rst
│   │   │   └── SqliteMap.rst
│   │   ├── matcher/
│   │   │   ├── BaseMatcher.rst
│   │   │   ├── BaseMatching.rst
│   │   │   ├── DistanceMatcher.rst
│   │   │   └── SimpleMatcher.rst
│   │   ├── overview.rst
│   │   └── util/
│   │       └── Segment.rst
│   ├── conf.py
│   ├── index.rst
│   ├── make.bat
│   ├── requirements.txt
│   └── usage/
│       ├── customdistributions.rst
│       ├── debug.rst
│       ├── incremental.rst
│       ├── installation.rst
│       ├── introduction.rst
│       ├── latitudelongitude.rst
│       ├── openstreetmap.rst
│       └── visualisation.rst
├── leuvenmapmatching/
│   ├── __init__.py
│   ├── map/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── inmem.py
│   │   └── sqlite.py
│   ├── matcher/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── distance.py
│   │   ├── newsonkrumm.py
│   │   └── simple.py
│   ├── util/
│   │   ├── __init__.py
│   │   ├── debug.py
│   │   ├── dist_euclidean.py
│   │   ├── dist_latlon.py
│   │   ├── dist_latlon_nvector.py
│   │   ├── evaluation.py
│   │   ├── gpx.py
│   │   ├── kalman.py
│   │   ├── openstreetmap.py
│   │   ├── projections.py
│   │   └── segment.py
│   └── visualization.py
├── setup.cfg
├── setup.py
└── tests/
    ├── examples/
    │   ├── example_1_simple.py
    │   └── example_using_osmnx_and_geopandas.py
    ├── rsrc/
    │   ├── bug2/
    │   │   └── readme.md
    │   ├── newson_krumm_2009/
    │   │   └── readme.md
    │   └── path_latlon/
    │       ├── readme.md
    │       ├── route.gpx
    │       └── route2.gpx
    ├── test_bugs.py
    ├── test_conversion.py
    ├── test_examples.py
    ├── test_newsonkrumm2009.py
    ├── test_nonemitting.py
    ├── test_nonemitting_circle.py
    ├── test_parallelroads.py
    ├── test_path.py
    ├── test_path_latlon.py
    └── test_path_onlyedges.py
Download .txt
SYMBOL INDEX (320 symbols across 30 files)

FILE: leuvenmapmatching/map/base.py
  class BaseMap (line 42) | class BaseMap(object):
    method __init__ (line 45) | def __init__(self, name, use_latlon=True):
    method use_latlon (line 56) | def use_latlon(self):
    method use_latlon (line 60) | def use_latlon(self, value):
    method bb (line 73) | def bb(self):
    method labels (line 80) | def labels(self):
    method size (line 84) | def size(self):
    method node_coordinates (line 88) | def node_coordinates(self, node_key):
    method edges_closeto (line 92) | def edges_closeto(self, loc, max_dist=None, max_elmt=None):
    method nodes_closeto (line 103) | def nodes_closeto(self, loc, max_dist=None, max_elmt=None):
    method nodes_nbrto (line 114) | def nodes_nbrto(self, node):
    method edges_nbrto (line 123) | def edges_nbrto(self, edge):
    method all_nodes (line 140) | def all_nodes(self, bb=None):
    method all_edges (line 147) | def all_edges(self, bb=None):

FILE: leuvenmapmatching/map/inmem.py
  class InMemMap (line 46) | class InMemMap(BaseMap):
    method __init__ (line 47) | def __init__(self, name, use_latlon=True, use_rtree=False, index_edges...
    method vertex_label_to_int (line 103) | def vertex_label_to_int(self, label, create=False):
    method vertices_labels_to_int (line 117) | def vertices_labels_to_int(self):
    method serialize (line 125) | def serialize(self):
    method deserialize (line 142) | def deserialize(cls, data):
    method dump (line 152) | def dump(self):
    method from_pickle (line 171) | def from_pickle(cls, filename):
    method bb (line 179) | def bb(self):
    method labels (line 192) | def labels(self):
    method size (line 196) | def size(self):
    method node_coordinates (line 199) | def node_coordinates(self, node_key):
    method add_node (line 207) | def add_node(self, node, loc):
    method del_node (line 223) | def del_node(self, node):
    method add_edge (line 232) | def add_edge(self, node_a, node_b):
    method _items_in_bb (line 254) | def _items_in_bb(self, bb):
    method all_edges (line 266) | def all_edges(self, bb=None):
    method all_nodes (line 287) | def all_nodes(self, bb=None):
    method purge (line 301) | def purge(self):
    method rtree_size (line 318) | def rtree_size(self):
    method rtree_fn (line 326) | def rtree_fn(self):
    method setup_index (line 332) | def setup_index(self, force=False, deserializing=False):
    method fill_index (line 388) | def fill_index(self):
    method to_xy (line 397) | def to_xy(self, name=None, use_rtree=None):
    method latlon2xy (line 419) | def latlon2xy(self, lat, lon):
    method latlon2yx (line 423) | def latlon2yx(self, lat, lon):
    method xy2latlon (line 427) | def xy2latlon(self, x, y):
    method yx2latlon (line 431) | def yx2latlon(self, y, x):
    method nodes_closeto (line 435) | def nodes_closeto(self, loc, max_dist=None, max_elmt=None):
    method edges_closeto (line 472) | def edges_closeto(self, loc, max_dist=None, max_elmt=None):
    method nodes_nbrto (line 517) | def nodes_nbrto(self, node):
    method edges_nbrto (line 531) | def edges_nbrto(self, edge):
    method find_duplicates (line 547) | def find_duplicates(self, func=None):
    method connect_parallelroads (line 560) | def connect_parallelroads(self, dist=0.5, bb=None):
    method print_stats (line 584) | def print_stats(self):
    method __str__ (line 588) | def __str__(self):

FILE: leuvenmapmatching/map/sqlite.py
  class SqliteMap (line 43) | class SqliteMap(BaseMap):
    method __init__ (line 44) | def __init__(self, name, use_latlon=True,
    method read_properties (line 104) | def read_properties(self):
    method save_properties (line 110) | def save_properties(self):
    method create_db (line 120) | def create_db(self):
    method from_file (line 179) | def from_file(cls, filename):
    method bb (line 185) | def bb(self):
    method labels (line 195) | def labels(self):
    method size (line 202) | def size(self):
    method node_coordinates (line 208) | def node_coordinates(self, node_key):
    method add_node (line 221) | def add_node(self, node, loc, ignore_doubles=False, no_index=False, no...
    method reindex_nodes (line 251) | def reindex_nodes(self):
    method add_nodes (line 265) | def add_nodes(self, nodes):
    method del_node (line 287) | def del_node(self, node):
    method add_edge (line 290) | def add_edge(self, node_a, node_b, loc_a=None, loc_b=None, speed=None,...
    method add_edges (line 324) | def add_edges(self, edges, no_index=False):
    method reindex_edges (line 346) | def reindex_edges(self):
    method all_edges (line 370) | def all_edges(self, bb=None):
    method all_nodes (line 394) | def all_nodes(self, bb=None):
    method purge (line 415) | def purge(self):
    method to_xy (line 418) | def to_xy(self, name=None):
    method latlon2xy (line 439) | def latlon2xy(self, lat, lon):
    method latlon2yx (line 443) | def latlon2yx(self, lat, lon):
    method xy2latlon (line 447) | def xy2latlon(self, x, y):
    method yx2latlon (line 451) | def yx2latlon(self, y, x):
    method nodes_closeto (line 455) | def nodes_closeto(self, loc, max_dist=None, max_elmt=None):
    method edges_closeto (line 483) | def edges_closeto(self, loc, max_dist=None, max_elmt=None):
    method nodes_nbrto (line 513) | def nodes_nbrto(self, node):
    method edges_nbrto (line 523) | def edges_nbrto(self, edge):
    method find_duplicates (line 543) | def find_duplicates(self, func=None):
    method connect_parallelroads (line 558) | def connect_parallelroads(self, dist=0.5, bb=None):
    method nodes_to_paths (line 581) | def nodes_to_paths(self, nodes, ignore_nopath=True):
    method path_dist (line 595) | def path_dist(self, path):
    method print_stats (line 606) | def print_stats(self):
    method __str__ (line 610) | def __str__(self):

FILE: leuvenmapmatching/matcher/base.py
  class BaseMatching (line 37) | class BaseMatching(object):
    method __init__ (line 44) | def __init__(self, matcher: 'BaseMatcher', edge_m: Segment, edge_o: Se...
    method prune_value (line 86) | def prune_value(self):
    method next (line 91) | def next(self, edge_m: Segment, edge_o: Segment, obs: int = 0, obs_ne:...
    method first (line 176) | def first(cls, logprob_init, edge_m, edge_o, matcher, dist_obs):
    method update (line 189) | def update(self, m_next):
    method _update_inner (line 217) | def _update_inner(self, m_other: 'BaseMatching'):
    method is_nonemitting (line 233) | def is_nonemitting(self):
    method is_emitting (line 236) | def is_emitting(self):
    method last_emitting_logprob (line 239) | def last_emitting_logprob(self):
    method __str__ (line 247) | def __str__(self, label_width=None):
    method __repr__ (line 261) | def __repr__(self):
    method repr_header (line 265) | def repr_header(label_width=None, stop=""):
    method repr_static (line 273) | def repr_static(fields, label_width=None):
    method label (line 284) | def label(self):
    method cname (line 291) | def cname(self):
    method key (line 298) | def key(self):
    method shortkey (line 308) | def shortkey(self):
    method nodes (line 316) | def nodes(self):
    method __hash__ (line 322) | def __hash__(self):
    method __lt__ (line 325) | def __lt__(self, o):
    method __le__ (line 328) | def __le__(self, o):
    method __eq__ (line 331) | def __eq__(self, o):
    method __ne__ (line 334) | def __ne__(self, o):
    method __ge__ (line 337) | def __ge__(self, o):
    method __gt__ (line 340) | def __gt__(self, o):
  class LatticeColumn (line 344) | class LatticeColumn:
    method __init__ (line 346) | def __init__(self, obs_idx):
    method __contains__ (line 351) | def __contains__(self, item):
    method __len__ (line 357) | def __len__(self):
    method set_delayed (line 360) | def set_delayed(self, delayed):
    method dict (line 366) | def dict(self, obs_ne=None):
    method values_all (line 373) | def values_all(self):
    method values (line 380) | def values(self, obs_ne=None):
    method upsert (line 387) | def upsert(self, matching):
    method prune (line 401) | def prune(self, obs_ne, max_lattice_width, expand_upto, prune_thr=None):
  class BaseMatcher (line 447) | class BaseMatcher:
    method __init__ (line 449) | def __init__(self, map_con, obs_noise=1, max_dist_init=None, max_dist=...
    method logprob_trans (line 523) | def logprob_trans(self, prev_m, edge_m, edge_o,
    method logprob_obs (line 535) | def logprob_obs(self, dist, prev_m, new_edge_m, new_edge_o, is_ne=False):
    method match_gpx (line 545) | def match_gpx(self, gpx_file, unique=True):
    method do_stop (line 551) | def do_stop(self, logprob_norm, dist, logprob_trans, logprob_obs):
    method _insert (line 561) | def _insert(self, m_next):
    method match (line 564) | def match(self, path, unique=False, tqdm=None, expand=False):
    method _skip_ne_states (line 667) | def _skip_ne_states(self, prev_m):
    method _create_start_nodes (line 671) | def _create_start_nodes(self, use_edges=True):
    method increase_delayed (line 731) | def increase_delayed(self, expand_from=None):
    method _match_states (line 740) | def _match_states(self, obs_idx, prev_lattice=None, max_dist=None, inc...
    method _match_non_emitting_states (line 861) | def _match_non_emitting_states(self, obs_idx, expand=False):
    method _node_in_prev_ne (line 908) | def _node_in_prev_ne(self, m_next, label):
    method _insert_tmp (line 931) | def _insert_tmp(m_next, lattice):
    method _match_non_emitting_states_inner (line 938) | def _match_non_emitting_states_inner(self, cur_lattice, obs_idx, obs, ...
    method _match_non_emitting_states_end (line 1051) | def _match_non_emitting_states_end(self, cur_lattice, obs_idx, obs_next,
    method get_matching (line 1143) | def get_matching(self, identifier=None):
    method get_matching_path (line 1187) | def get_matching_path(self, start_m):
    method get_node_path (line 1192) | def get_node_path(self, start_m, only_nodes=False):
    method get_path (line 1200) | def get_path(self, only_nodes=True, allow_jumps=False, only_closest=Tr...
    method node_path_to_only_nodes (line 1213) | def node_path_to_only_nodes(self, path, allow_jumps=False):
    method _build_matching_path (line 1259) | def _build_matching_path(self, start_m, max_depth=None):
    method _build_node_path (line 1289) | def _build_node_path(self, start_idx, unique=True, max_depth=None, las...
    method increase_max_lattice_width (line 1330) | def increase_max_lattice_width(self, max_lattice_width, unique=False, ...
    method continue_with_distance (line 1346) | def continue_with_distance(self, from_matches=None, k=2, nb_obs=2, max...
    method path_bb (line 1366) | def path_bb(self):
    method print_lattice (line 1375) | def print_lattice(self, file=None, obs_idx=None, obs_ne=0, label_width...
    method lattice_dot (line 1398) | def lattice_dot(self, file=None, precision=None, render=False):
    method print_lattice_stats (line 1475) | def print_lattice_stats(self, file=None, verbose=False):
    method node_counts (line 1516) | def node_counts(self):
    method inspect_early_stopping (line 1525) | def inspect_early_stopping(self):
    method best_last_matches (line 1544) | def best_last_matches(self, k=1, nb_obs=3):
    method copy_lastinterface (line 1585) | def copy_lastinterface(self, nb_interfaces=1):
    method path_pred (line 1616) | def path_pred(self):
    method path_pred_onlynodes (line 1621) | def path_pred_onlynodes(self):
    method path_pred_onlynodes_withjumps (line 1626) | def path_pred_onlynodes_withjumps(self):
    method path_pred_distance (line 1630) | def path_pred_distance(self):
    method path_distance (line 1649) | def path_distance(self):
    method path_all_distances (line 1662) | def path_all_distances(self):

FILE: leuvenmapmatching/matcher/distance.py
  class DistanceMatching (line 25) | class DistanceMatching(BaseMatching):
    method __init__ (line 28) | def __init__(self, *args, d_s=0.0, d_o=0.0, lpe=0.0, lpt=0.0, **kwargs):
    method _update_inner (line 44) | def _update_inner(self, m_other):
    method repr_header (line 53) | def repr_header(label_width=None, stop=""):
    method __str__ (line 60) | def __str__(self, label_width=None):
    method __repr__ (line 67) | def __repr__(self):
  class DistanceMatcher (line 71) | class DistanceMatcher(BaseMatcher):
    method __init__ (line 92) | def __init__(self, *args, **kwargs):
    method logprob_trans (line 152) | def logprob_trans(self, prev_m, edge_m, edge_o,
    method logprob_obs (line 237) | def logprob_obs(self, dist, prev_m=None, new_edge_m=None, new_edge_o=N...
    method _skip_ne_states (line 257) | def _skip_ne_states(self, next_ne_m):

FILE: leuvenmapmatching/matcher/newsonkrumm.py
  class NewsonKrummMatching (line 27) | class NewsonKrummMatching(BaseMatching):
    method __init__ (line 30) | def __init__(self, *args, d_s=1.0, d_o=1.0, lpe=0.0, lpt=0.0, **kwargs):
    method _update_inner (line 46) | def _update_inner(self, m_other):
    method repr_header (line 55) | def repr_header(label_width=None, stop=""):
    method __str__ (line 62) | def __str__(self, label_width=None):
  class NewsonKrummMatcher (line 70) | class NewsonKrummMatcher(BaseMatcher):
    method __init__ (line 97) | def __init__(self, *args, **kwargs):
    method logprob_trans (line 122) | def logprob_trans(self, prev_m: NewsonKrummMatching, edge_m, edge_o,
    method logprob_obs (line 167) | def logprob_obs(self, dist, prev_m, new_edge_m, new_edge_o, is_ne=False):

FILE: leuvenmapmatching/matcher/simple.py
  class SimpleMatching (line 17) | class SimpleMatching(BaseMatching):
  class SimpleMatcher (line 21) | class SimpleMatcher(BaseMatcher):
    method __init__ (line 23) | def __init__(self, *args, **kwargs):
    method logprob_trans (line 52) | def logprob_trans(self, prev_m: BaseMatching, edge_m: Segment, edge_o:...
    method logprob_obs (line 79) | def logprob_obs(self, dist, prev_m, new_edge_m, new_edge_o, is_ne=False):

FILE: leuvenmapmatching/util/__init__.py
  function approx_equal (line 14) | def approx_equal(a, b, rtol=0.0, atol=1e-08):
  function approx_leq (line 18) | def approx_leq(a, b, rtol=0.0, atol=1e-08):

FILE: leuvenmapmatching/util/debug.py
  function printd (line 7) | def printd(*args, **kwargs):

FILE: leuvenmapmatching/util/dist_euclidean.py
  function distance (line 18) | def distance(p1, p2):
  function distance_point_to_segment (line 24) | def distance_point_to_segment(p, s1, s2, delta=0.0):
  function distance_segment_to_segment (line 33) | def distance_segment_to_segment(f1, f2, t1, t2):
  function project (line 97) | def project(s1, s2, p, delta=0.0):
  function interpolate_path (line 114) | def interpolate_path(path, dd):
  function box_around_point (line 137) | def box_around_point(p, dist):
  function lines_parallel (line 144) | def lines_parallel(la, lb, lc, ld, d=None):

FILE: leuvenmapmatching/util/dist_latlon.py
  function distance (line 25) | def distance(p1, p2):
  function distance_point_to_segment (line 40) | def distance_point_to_segment(p, s1, s2, delta=0.0, constrain=True):
  function distance_segment_to_segment (line 98) | def distance_segment_to_segment(f1, f2, t1, t2):
  function project (line 143) | def project(s1, s2, p, delta=0.0):
  function box_around_point (line 148) | def box_around_point(p, dist):
  function interpolate_path (line 160) | def interpolate_path(path, dd):
  function bearing_radians (line 186) | def bearing_radians(lat1, lon1, lat2, lon2):
  function distance_haversine_radians (line 194) | def distance_haversine_radians(lat1, lon1, lat2, lon2, radius=earth_radi...
  function destination_radians (line 204) | def destination_radians(lat1, lon1, bearing, dist):
  function lines_parallel (line 211) | def lines_parallel(f1, f2, t1, t2, d=None):

FILE: leuvenmapmatching/util/dist_latlon_nvector.py
  function distance (line 22) | def distance(p1, p2):
  function distance_gp (line 36) | def distance_gp(p1, p2):
  function distance_point_to_segment (line 41) | def distance_point_to_segment(p, s1, s2, delta=0.0):
  function distance_segment_to_segment (line 60) | def distance_segment_to_segment(f1, f2, t1, t2):
  function project (line 92) | def project(s1, s2, p, delta=0.0):
  function _project_nvector (line 100) | def _project_nvector(s1, s2, p, delta=0.0):
  function _cross_track_point (line 111) | def _cross_track_point(path, point):
  function interpolate_path (line 136) | def interpolate_path(path, dd):

FILE: leuvenmapmatching/util/evaluation.py
  function route_mismatch_factor (line 25) | def route_mismatch_factor(map_con, path_pred, path_grnd, window=None, di...

FILE: leuvenmapmatching/util/gpx.py
  function gpx_to_path (line 21) | def gpx_to_path(gpx_file):
  function path_to_gpx (line 37) | def path_to_gpx(path, filename=None):

FILE: leuvenmapmatching/util/kalman.py
  function smooth_path (line 19) | def smooth_path(path, dt=1, obs_noise=1e-4, loc_var=1e-4, vel_var=1e-6, ...

FILE: leuvenmapmatching/util/openstreetmap.py
  function locations_to_map (line 19) | def locations_to_map(locations, map_con, filename=None):
  function bb_to_map (line 27) | def bb_to_map(bb, map_con, filename=None):
  function file_to_map (line 54) | def file_to_map(filename, map_con):
  function download_map_xml (line 70) | def download_map_xml(fn, bbox, force=False, verbose=False):
  function create_map_from_xml (line 101) | def create_map_from_xml(fn, include_footways=False, include_parking=False,

FILE: leuvenmapmatching/util/projections.py
  function latlon2equirectangular (line 18) | def latlon2equirectangular(lat, lon, phi_er=0, lambda_er=0):
  function equirectangular2latlon (line 32) | def equirectangular2latlon(y, x, phi_er=0, lambda_er=0):
  function latlon2grs80 (line 44) | def latlon2grs80(coordinates, lon_0=0.0, lat_ts=0.0, y_0=0, x_0=0.0, zon...

FILE: leuvenmapmatching/util/segment.py
  class Segment (line 16) | class Segment(object):
    method __init__ (line 20) | def __init__(self, l1, p1, l2=None, p2=None, pi=None, ti=None):
    method label (line 39) | def label(self):
    method rlabel (line 45) | def rlabel(self):
    method key (line 51) | def key(self):
    method pi (line 57) | def pi(self):
    method pi (line 63) | def pi(self, value):
    method ti (line 70) | def ti(self):
    method ti (line 76) | def ti(self, value):
    method is_point (line 79) | def is_point(self):
    method last_point (line 82) | def last_point(self):
    method loc_to_str (line 87) | def loc_to_str(self):
    method __str__ (line 94) | def __str__(self):
    method __repr__ (line 101) | def __repr__(self):

FILE: leuvenmapmatching/visualization.py
  function plot_map (line 31) | def plot_map(map_con, path=None, nodes=None, counts=None, ax=None, use_o...
  function plot_lattice (line 316) | def plot_lattice(ax, to_pixels, matcher):
  function plot_obs_noise_dist (line 342) | def plot_obs_noise_dist(obs_fn, obs_noise, min_dist=0, max_dist=10):

FILE: tests/examples/example_using_osmnx_and_geopandas.py
  function run (line 15) | def run():

FILE: tests/test_bugs.py
  function test_bug1 (line 32) | def test_bug1():
  function test_bug2 (line 71) | def test_bug2():

FILE: tests/test_conversion.py
  function test_path_to_gpx (line 20) | def test_path_to_gpx():
  function test_grs80 (line 31) | def test_grs80():
  function test_distance1 (line 43) | def test_distance1():
  function test_distance2 (line 50) | def test_distance2():
  function test_bearing1 (line 59) | def test_bearing1():
  function test_destination1 (line 68) | def test_destination1():
  function test_distance_segment_to_segment1 (line 78) | def test_distance_segment_to_segment1():
  function test_distance_segment_to_segment2 (line 93) | def test_distance_segment_to_segment2():
  function test_distance_segment_to_segment3 (line 108) | def test_distance_segment_to_segment3():
  function test_distance_segment_to_segment4 (line 123) | def test_distance_segment_to_segment4():
  function test_distance_point_to_segment1 (line 138) | def test_distance_point_to_segment1():
  function plot_distance_point_to_segment_latlon (line 171) | def plot_distance_point_to_segment_latlon(f, t1, t2, pt, fn):
  function plot_distance_segment_to_segment_latlon (line 192) | def plot_distance_segment_to_segment_latlon(f1, f2, t1, t2, pf, pt, fn):
  function plot_distance_segment_to_segment_euc (line 215) | def plot_distance_segment_to_segment_euc(f1, f2, t1, t2, pf, pt, fn):

FILE: tests/test_examples.py
  function test_examples (line 27) | def test_examples():
  function importrun_file (line 33) | def importrun_file(fn, cmp_with_previous=False):
  function execute_file (line 41) | def execute_file(fn, cmp_with_previous=False):

FILE: tests/test_newsonkrumm2009.py
  function read_gps (line 60) | def read_gps(route_fn):
  function read_paths (line 76) | def read_paths(paths_fn):
  function parse_linestring (line 90) | def parse_linestring(line):
  function read_map (line 100) | def read_map(map_fn):
  function correct_map (line 163) | def correct_map(mmap):
  function load_data (line 176) | def load_data():
  function test_route_slice1 (line 238) | def test_route_slice1():
  function test_bug1 (line 258) | def test_bug1():
  function test_route (line 299) | def test_route():
  function test_bug2 (line 386) | def test_bug2():

FILE: tests/test_nonemitting.py
  function setup_map (line 34) | def setup_map():
  function visualize_map (line 53) | def visualize_map(pathnb=1):
  function test_path1 (line 64) | def test_path1():
  function test_path1_inc (line 85) | def test_path1_inc():
  function test_path1_dist (line 123) | def test_path1_dist():
  function test_path2 (line 146) | def test_path2():
  function test_path2_dist (line 167) | def test_path2_dist():
  function test_path2_incremental (line 186) | def test_path2_incremental():
  function test_path_duplicate (line 207) | def test_path_duplicate():
  function test_path3_many_obs (line 244) | def test_path3_many_obs():
  function test_path3_few_obs_en (line 274) | def test_path3_few_obs_en():
  function test_path3_few_obs_e (line 301) | def test_path3_few_obs_e():
  function test_path3_dist (line 328) | def test_path3_dist():

FILE: tests/test_nonemitting_circle.py
  function setup_map (line 30) | def setup_map(disconnect=True):
  function visualize_map (line 65) | def visualize_map():
  function visualize_path (line 84) | def visualize_path(matcher, mapdb, name="test"):
  function test_path1 (line 97) | def test_path1():
  function test_path1_dist (line 108) | def test_path1_dist():
  function test_path2 (line 119) | def test_path2():
  function test_path2_dist (line 138) | def test_path2_dist():

FILE: tests/test_parallelroads.py
  function create_map1 (line 25) | def create_map1():
  function create_path1 (line 52) | def create_path1():
  function test_parallel (line 56) | def test_parallel():
  function test_bb1 (line 61) | def test_bb1():
  function test_merge1 (line 97) | def test_merge1():
  function test_path1 (line 108) | def test_path1():

FILE: tests/test_path.py
  function test_path1 (line 26) | def test_path1():
  function test_path1_dist (line 57) | def test_path1_dist():
  function test_path2 (line 85) | def test_path2():
  function test_path2_inc (line 121) | def test_path2_inc():
  function test_path2_dist (line 176) | def test_path2_dist():
  function test_path_outlier (line 208) | def test_path_outlier():
  function test_path_outlier2 (line 246) | def test_path_outlier2():
  function test_path_outlier_dist (line 280) | def test_path_outlier_dist():
  function test_path3 (line 314) | def test_path3():
  function test_path3_dist (line 339) | def test_path3_dist():
  function test_path4_dist_inc (line 360) | def test_path4_dist_inc():
  function test_path4_dist_inc_missing (line 403) | def test_path4_dist_inc_missing():

FILE: tests/test_path_latlon.py
  function prepare_files (line 37) | def prepare_files(verbose=False, force=False, download_from_osm=False):
  function test_path1 (line 59) | def test_path1(use_rtree=False):
  function test_path1_serialization (line 85) | def test_path1_serialization(use_rtree=False):
  function test_path1_full (line 118) | def test_path1_full():
  function test_path2_proj (line 137) | def test_path2_proj():
  function test_path2 (line 166) | def test_path2():
  function test_path3 (line 193) | def test_path3():

FILE: tests/test_path_onlyedges.py
  function test_path1 (line 24) | def test_path1():
  function test_path3 (line 55) | def test_path3():
Condensed preview — 69 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (378K chars).
[
  {
    "path": ".gitignore",
    "chars": 216,
    "preview": ".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/Le"
  },
  {
    "path": ".readthedocs.yaml",
    "chars": 167,
    "preview": "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   ins"
  },
  {
    "path": "LICENSE",
    "chars": 2719,
    "preview": "Leuven.MapMatching\n------------------\n\nCopyright 2015-2018 KU Leuven, DTAI Research Group\nCopyright 2017-2018 Sirris\n\nLi"
  },
  {
    "path": "MANIFEST.in",
    "chars": 33,
    "preview": "include README.md\ninclude LICENSE"
  },
  {
    "path": "Makefile",
    "chars": 972,
    "preview": "\n.PHONY: test3\ntest3:\n\t@#export PYTHONPATH=.;./venv/bin/py.test --ignore=venv -vv\n\tpython3 setup.py test\n\n.PHONY: test2\n"
  },
  {
    "path": "README.md",
    "chars": 2350,
    "preview": "# Leuven.MapMatching\n\n[![PyPi Version](https://img.shields.io/pypi/v/leuvenmapmatching.svg)](https://pypi.org/project/le"
  },
  {
    "path": "docs/Makefile",
    "chars": 613,
    "preview": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHI"
  },
  {
    "path": "docs/classes/map/BaseMap.rst",
    "chars": 81,
    "preview": "BaseMap\n=======\n\n\n.. autoclass:: leuvenmapmatching.map.base.BaseMap\n   :members:\n"
  },
  {
    "path": "docs/classes/map/InMemMap.rst",
    "chars": 85,
    "preview": "InMemMap\n========\n\n\n.. autoclass:: leuvenmapmatching.map.inmem.InMemMap\n   :members:\n"
  },
  {
    "path": "docs/classes/map/SqliteMap.rst",
    "chars": 89,
    "preview": "SqliteMap\n=========\n\n\n.. autoclass:: leuvenmapmatching.map.sqlite.SqliteMap\n   :members:\n"
  },
  {
    "path": "docs/classes/matcher/BaseMatcher.rst",
    "chars": 269,
    "preview": "BaseMatcher\n===========\n\nThis a generic base class to be used by matchers. This class itself\ndoes not implement a workin"
  },
  {
    "path": "docs/classes/matcher/BaseMatching.rst",
    "chars": 100,
    "preview": "BaseMatching\n============\n\n\n.. autoclass:: leuvenmapmatching.matcher.base.BaseMatching\n   :members:\n"
  },
  {
    "path": "docs/classes/matcher/DistanceMatcher.rst",
    "chars": 113,
    "preview": "DistanceMatcher\n===============\n\n\n.. autoclass:: leuvenmapmatching.matcher.distance.DistanceMatcher\n   :members:\n"
  },
  {
    "path": "docs/classes/matcher/SimpleMatcher.rst",
    "chars": 105,
    "preview": "SimpleMatcher\n=============\n\n\n.. autoclass:: leuvenmapmatching.matcher.simple.SimpleMatcher\n   :members:\n"
  },
  {
    "path": "docs/classes/overview.rst",
    "chars": 343,
    "preview": "\n\nmatcher\n~~~~~~~\n\n.. toctree::\n   :caption: Matcher\n\n   matcher/BaseMatcher\n   matcher/SimpleMatcher\n   matcher/Distanc"
  },
  {
    "path": "docs/classes/util/Segment.rst",
    "chars": 85,
    "preview": "Segment\n=======\n\n\n.. autoclass:: leuvenmapmatching.util.segment.Segment\n   :members:\n"
  },
  {
    "path": "docs/conf.py",
    "chars": 4940,
    "preview": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n#\n# Leuven.MapMatching documentation build configuration file, created by"
  },
  {
    "path": "docs/index.rst",
    "chars": 1292,
    "preview": ".. Leuven.MapMatching documentation master file, created by\n   sphinx-quickstart on Sat Apr 14 23:24:31 2018.\n   You can"
  },
  {
    "path": "docs/make.bat",
    "chars": 820,
    "preview": "@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=sp"
  },
  {
    "path": "docs/requirements.txt",
    "chars": 28,
    "preview": "numpy\nscipy\nsphinx_rtd_theme"
  },
  {
    "path": "docs/usage/customdistributions.rst",
    "chars": 2624,
    "preview": "Custom probability distributions\n================================\n\nYou can use your own custom probability distributions"
  },
  {
    "path": "docs/usage/debug.rst",
    "chars": 3358,
    "preview": "Debug\n=====\n\nIncreasing the verbosity level\n------------------------------\n\nTo inspect the intermediate steps that the a"
  },
  {
    "path": "docs/usage/incremental.rst",
    "chars": 1617,
    "preview": "Incremental matching\n====================\n\nExample: Incremental matching\n-------------------------------\n\nIf the observa"
  },
  {
    "path": "docs/usage/installation.rst",
    "chars": 1110,
    "preview": "Installation\n============\n\nDependencies\n------------\n\nRequired:\n\n-  `numpy <http://www.numpy.org>`__\n-  `scipy <https://"
  },
  {
    "path": "docs/usage/introduction.rst",
    "chars": 4256,
    "preview": "Examples\n========\n\nExample 1: Simple\n-----------------\n\nA first, simple example. Some parameters are given to tune the a"
  },
  {
    "path": "docs/usage/latitudelongitude.rst",
    "chars": 4297,
    "preview": "Dealing with Latitude-Longitude\n===============================\n\nThe toolbox can deal with latitude-longitude coordinate"
  },
  {
    "path": "docs/usage/openstreetmap.rst",
    "chars": 4408,
    "preview": "Map from OpenStreetMap\n======================\n\nYou can download a graph for map-matching from the OpenStreetMap.org serv"
  },
  {
    "path": "docs/usage/visualisation.rst",
    "chars": 2009,
    "preview": "Visualisation\n=============\n\nTo inspect the results, a plotting function is included.\n\nSimple plotting\n---------------\n\n"
  },
  {
    "path": "leuvenmapmatching/__init__.py",
    "chars": 446,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching\n~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2015-2022 DTAI, K"
  },
  {
    "path": "leuvenmapmatching/map/__init__.py",
    "chars": 212,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.map\n~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2018 DTAI"
  },
  {
    "path": "leuvenmapmatching/map/base.py",
    "chars": 4229,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.map.base\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBase Map class.\n\nTo be used in a Matcher ob"
  },
  {
    "path": "leuvenmapmatching/map/inmem.py",
    "chars": 23304,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.map.inmem\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nSimple in-memory map representation. Not "
  },
  {
    "path": "leuvenmapmatching/map/sqlite.py",
    "chars": 22986,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.map.sqlite\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nMap representation based on a sqlite da"
  },
  {
    "path": "leuvenmapmatching/matcher/__init__.py",
    "chars": 220,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.matcher\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2"
  },
  {
    "path": "leuvenmapmatching/matcher/base.py",
    "chars": 79237,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.matcher.base\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBase Matcher and Matching classes.\n"
  },
  {
    "path": "leuvenmapmatching/matcher/distance.py",
    "chars": 11911,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.matcher.distance\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copy"
  },
  {
    "path": "leuvenmapmatching/matcher/newsonkrumm.py",
    "chars": 6372,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.matcher.newsonkrumm\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nMethods similar to Ne"
  },
  {
    "path": "leuvenmapmatching/matcher/simple.py",
    "chars": 3900,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.matcher.simple\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyrigh"
  },
  {
    "path": "leuvenmapmatching/util/__init__.py",
    "chars": 501,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util\n~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2015-20"
  },
  {
    "path": "leuvenmapmatching/util/debug.py",
    "chars": 177,
    "preview": "import logging\n\n\nlogger = logging.getLogger(\"be.kuleuven.cs.dtai.mapmatching\")\n\n\ndef printd(*args, **kwargs):\n    \"\"\"Pri"
  },
  {
    "path": "leuvenmapmatching/util/dist_euclidean.py",
    "chars": 4541,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.dist_euclidean\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert"
  },
  {
    "path": "leuvenmapmatching/util/dist_latlon.py",
    "chars": 8186,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.dist_latlon\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBased on:\nhttps://www.movab"
  },
  {
    "path": "leuvenmapmatching/util/dist_latlon_nvector.py",
    "chars": 5544,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.dist_latlon_nvector\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wa"
  },
  {
    "path": "leuvenmapmatching/util/evaluation.py",
    "chars": 2613,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.evaluation\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nMethods to help set up and ev"
  },
  {
    "path": "leuvenmapmatching/util/gpx.py",
    "chars": 1506,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.gpx\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nSome additional functions to interact with "
  },
  {
    "path": "leuvenmapmatching/util/kalman.py",
    "chars": 2901,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.kalman\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Cop"
  },
  {
    "path": "leuvenmapmatching/util/openstreetmap.py",
    "chars": 4944,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.openstreetmap\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:"
  },
  {
    "path": "leuvenmapmatching/util/projections.py",
    "chars": 2395,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.projections\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copy"
  },
  {
    "path": "leuvenmapmatching/util/segment.py",
    "chars": 2780,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.util.segment\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: C"
  },
  {
    "path": "leuvenmapmatching/visualization.py",
    "chars": 14449,
    "preview": "# encoding: utf-8\n\"\"\"\nleuvenmapmatching.visualization\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright:"
  },
  {
    "path": "setup.cfg",
    "chars": 1158,
    "preview": "[metadata]\nname = leuvenmapmatching\nversion = attr: leuvenmapmatching.__version__\nauthor = Wannes Meert\ndescription = Ma"
  },
  {
    "path": "setup.py",
    "chars": 1171,
    "preview": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\nsetup.py\n~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyright 2017-2021 DT"
  },
  {
    "path": "tests/examples/example_1_simple.py",
    "chars": 1150,
    "preview": "from leuvenmapmatching.matcher.distance import DistanceMatcher\nfrom leuvenmapmatching.map.inmem import InMemMap\n\nmap_con"
  },
  {
    "path": "tests/examples/example_using_osmnx_and_geopandas.py",
    "chars": 2737,
    "preview": "import os\nimport sys\nimport logging\nfrom pathlib import Path\n\nthis_path = Path(os.path.realpath(__file__)).parent.parent"
  },
  {
    "path": "tests/rsrc/bug2/readme.md",
    "chars": 174,
    "preview": "Test data\n=========\n\nDownload from https://people.cs.kuleuven.be/wannes.meert/leuvenmapmatching/leuvenmapmatching_testda"
  },
  {
    "path": "tests/rsrc/newson_krumm_2009/readme.md",
    "chars": 239,
    "preview": "Newson Krum testdata\n====================\n\nFiles will be downloaded from https://www.microsoft.com/en-us/research/public"
  },
  {
    "path": "tests/rsrc/path_latlon/readme.md",
    "chars": 238,
    "preview": "Test data for path_latlon\n=========================\n\nDownload from https://people.cs.kuleuven.be/wannes.meert/leuvenmapm"
  },
  {
    "path": "tests/rsrc/path_latlon/route.gpx",
    "chars": 5797,
    "preview": "<?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://w"
  },
  {
    "path": "tests/rsrc/path_latlon/route2.gpx",
    "chars": 1224,
    "preview": "<?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"
  },
  {
    "path": "tests/test_bugs.py",
    "chars": 5260,
    "preview": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_bugs\n~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyrigh"
  },
  {
    "path": "tests/test_conversion.py",
    "chars": 9680,
    "preview": "#!/usr/bin/env python3\n# encoding: utf-8\nimport sys\nimport logging\nfrom datetime import datetime\nimport pytest\nimport ma"
  },
  {
    "path": "tests/test_examples.py",
    "chars": 2295,
    "preview": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_examples\n~~~~~~~~~~~~~~~~~~~\n\nRun standalone python files that a"
  },
  {
    "path": "tests/test_newsonkrumm2009.py",
    "chars": 20697,
    "preview": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_path_newsonkrumm2009\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBased on t"
  },
  {
    "path": "tests/test_nonemitting.py",
    "chars": 15618,
    "preview": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_nonemitting\n~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyr"
  },
  {
    "path": "tests/test_nonemitting_circle.py",
    "chars": 5476,
    "preview": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_nonemitting_circle\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wanne"
  },
  {
    "path": "tests/test_parallelroads.py",
    "chars": 4062,
    "preview": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_parallelroads\n~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:c"
  },
  {
    "path": "tests/test_path.py",
    "chars": 21121,
    "preview": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_path\n~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyright: Copyrigh"
  },
  {
    "path": "tests/test_path_latlon.py",
    "chars": 16578,
    "preview": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_path_latlon\n~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n:copyr"
  },
  {
    "path": "tests/test_path_onlyedges.py",
    "chars": 3307,
    "preview": "#!/usr/bin/env python3\n# encoding: utf-8\n\"\"\"\ntests.test_path_onlyedges\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\n:author: Wannes Meert\n"
  }
]

About this extraction

This page contains the full source code of the wannesm/LeuvenMapMatching GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 69 files (352.0 KB), approximately 100.9k tokens, and a symbol index with 320 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!