From 72773a34e0411aa1162a3c7f5f46fc56c3b38fec Mon Sep 17 00:00:00 2001 From: Nicholas Reinicke Date: Wed, 18 Feb 2026 14:09:12 -0700 Subject: [PATCH 1/4] update lab name --- .gitignore | 1 + CLAUDE.md | 5 + LICENSE | 2 +- README.md | 2 +- docs/_config.yml | 4 +- docs/home.md | 2 +- pixi.lock | 1213 +++++++++++++++++++++++++--------------------- pyproject.toml | 7 +- 8 files changed, 685 insertions(+), 551 deletions(-) diff --git a/.gitignore b/.gitignore index f1b217f..360a6bf 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ __pycache__/ # Distribution / packaging .Python build/ +_build/ develop-eggs/ dist/ downloads/ diff --git a/CLAUDE.md b/CLAUDE.md index 07804a9..d624a1c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,4 +14,9 @@ Mappymatch is a python package used to match a series of GPS waypoints (Trace) t ``` pixi run -e dev check ``` +### building the docs + +``` +pixi run docs +``` diff --git a/LICENSE b/LICENSE index 2682a06..690f3a4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2022, Alliance for Sustainable Energy, LLC +Copyright (c) 2022, Alliance for Energy Innovation, LLC All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/README.md b/README.md index e4cc696..33510c0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # mappymatch -Mappymatch is a pure-python package developed and open sourced by the National Renewable Energy Laboratory. It contains a collection of "Matchers" that enable matching a GPS trace (series of GPS coordinates) to a map. +Mappymatch is a pure-python package developed and open sourced by the National Laboratory of the Rockies. It contains a collection of "Matchers" that enable matching a GPS trace (series of GPS coordinates) to a map. ![Map Matching Animation](docs/images/map-matching.gif?raw=true) diff --git a/docs/_config.yml b/docs/_config.yml index 6077352..e363f77 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -2,7 +2,7 @@ # Learn more at https://jupyterbook.org/customize/config.html title: mappymatch -author: National Renewable Energy Laboratory +author: National Laboratory of the Rockies # Force re-execution of notebooks on each build. # See https://jupyterbook.org/content/execute.html @@ -17,7 +17,7 @@ latex: # Information about where the book exists on the web repository: - url: https://github.com/NREL/mappymatch # Online location of your book + url: https://github.com/NLR/mappymatch # Online location of your book path_to_book: docs # Optional path to your book, relative to the repository root branch: main # Which branch of the repository should be used when creating links (optional) diff --git a/docs/home.md b/docs/home.md index d5caaf3..8bacdac 100644 --- a/docs/home.md +++ b/docs/home.md @@ -1,6 +1,6 @@ # Mappymatch -Mappymatch is a pure-Python package developed and open-sourced by the National Renewable Energy Laboratory. It contains a collection of "Matchers" that enable matching a GPS trace (series of GPS coordinates) to a map. +Mappymatch is a pure-Python package developed and open-sourced by the National Laboratory of the Rockies. It contains a collection of "Matchers" that enable matching a GPS trace (series of GPS coordinates) to a map. ## The Current Matchers diff --git a/pixi.lock b/pixi.lock index 15c7d94..99075c0 100644 --- a/pixi.lock +++ b/pixi.lock @@ -81,20 +81,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - pypi: https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/50/fc9680058e63161f2f63165b84c957a0df1415431104c408e8104a3a18ef/branca-0.8.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl @@ -104,7 +99,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl @@ -112,7 +106,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/a8/5f764f333204db0390362a4356d03a43626997f26818a0e9396f1b3bd8c9/folium-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0b/70/d5cd0696eff08e62fdbdebe5b46527facb4e7220eabe0ac6225efab50168/geopandas-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/01/316ef533114e0de0649e2e925ae2f97dfe26fbe5358f678e84b2a5fa1407/hatch-1.15.1-py3-none-any.whl @@ -124,40 +117,38 @@ environments: - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0d/bc/6281ec7f9baaf71ee57c3b1748da2d3148d15d253e1a03006f204aa68ca5/igraph-1.0.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/46/0d/0a6fdf9587634452b9ced1de89280a3daeb1fee16878ca442dfe76c9b0a8/jupyter_book-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/86/d45756beaeb4b9b06125599b429451f8640b5db6f019d606f33c85743fd4/jupyter_book-1.0.4.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/6b/67b87da9d36bff9df7d0efbd1a325fa372a43be7158effaf43ed7b22341d/jupyter_cache-1.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/40/23569737873cc9637fd488606347e9dd92b9fa37ba4fcda1f98ee5219a97/latexcodec-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7a/f0/8282d9641415e9e33df173516226b404d367a0fc55e1a60424a152913abc/mistune-3.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/a6/03d410c114b8c4856579b3d294dafc27626a7690a552625eec42b16dfa41/myst_nb-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e2/de/21aa8394f16add8f7427f0a1326ccd2b3a2a8a3245c9252bc5ac034c6155/myst_parser-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl @@ -166,7 +157,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/77/5f/2fc0135c5aa3fc36c7cb1ac63852349681908778eb15ab3e1aacbddc1b54/osmnx-2.0.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl @@ -175,12 +165,12 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d5/72/7faa058dc6548e58cbdb7c6e8e0dbb56516ee941a7785fb74fb455fa28b2/polyline-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/11/574fe7d13acf30bfd0a8dd7fa1647040f2b8064f13f43e8c963b1e65093b/pre_commit-4.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/68/ceb5d6679baa326261f5d3e5113d9cfed6efef2810afd9f18bffb8ed312b/pybtex-0.25.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/b1/ce1f4596211efb5410e178a803f08e59b20bedb66837dcf41e21c54f9ec1/pybtex_docutils-1.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/d3/c622950d87a2ffd1654208733b5bd1c5645930014abed8f4c0d74863988b/pydata_sphinx_theme-0.15.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/68/86328e36d010ee565ce0c65cdf9b830afcb1fb5972f537fe1cc561a49247/pyogrio-0.11.1-cp313-cp313-macosx_12_0_arm64.whl @@ -188,43 +178,47 @@ environments: - pypi: https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz - pypi: https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/0f/73/bb1bc2529f852e7bf64a2dec885e89ff9f5cc7bbf6c9340eed30ff2c69c5/ruamel.yaml-0.18.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/ae/e3811f05415594025e96000349d3400978adaed88d8f98d494352d9761ee/ruamel.yaml.clib-0.2.14-cp313-cp313-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/b6/f3/aa319f4afc22cb6fcba2b9cdfc0f03bbf747e59ab7a8c5e90173857a1361/ruff-0.14.5-py3-none-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/f2/9657c98a66973b7c35bfd48ba65d1922860de9598fbb535cd96e3f58a908/sphinx_autodoc_typehints-3.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f3/e0a4ce49da4b6f4e4ce84b3c39a0677831884cb9d8a87ccbf1e9e56e53ac/sphinx_autodoc_typehints-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/9e/c41d68be04eef5b6202b468e0f90faf0c469f3a03353f2a218fd78279710/sphinx_book_theme-1.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/97/a5c39f619375d4f81d5422377fb027075898efa6b6202c1ccf1e5bb38a32/sphinx_comments-0.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/cf/45dd359f6ca0c3762ce0490f681da242f0530c49c81050c035c016bfdd3a/sphinx_design-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9f/80/1704c9179012e289dee2178354e385277ea51f4fa827c4bf7e36c77b0f4b/sphinx_external_toc-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/70/1f/1d4ecaf58b17fe61497644655f40b04d84a88348e41a6f0c6392394d95e4/sphinx_jupyterbook_latex-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/9f/902f2030674cd9473fdbe5a2c2dec2618c27ec853484c35f82cf8df40ece/sphinx_multitoc_numbering-0.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/7c/a53bdb465fd364bc3d255d96d5d70e6ba5183cfb4e45b8aa91c59b099124/sphinx_thebe-0.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/cb/9f6ceb4308ebfe5f393a271ee6206e17883edee0662a9b5c1a371878064b/sphinx_togglebutton-0.4.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/d6/255f0a1d3fa8990b14d2200aacf0f7c87c026e9ec20801e503529537e628/sphinxcontrib_autoyaml-1.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/a0/3a612da94f828f26cabb247817393e79472c32b12c49222bf85fb6d7b6c8/sphinxcontrib_bibtex-2.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cd/c8/784b9ac6ea08aa594c1a4becbd0dbe77186785362e31fd633b8c6ae0197a/sphinxcontrib_mermaid-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/99/4772b8e00a136f3e01236de33b0efda31ee7077203ba5967fcc76da94d65/texttable-1.7.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl @@ -233,16 +227,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cc/47/436863f6d99cfc3e41408e1d28d07fb3d20227d5ff66f52666564a5649f5/uv-0.9.9-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5b/8f/447cc9cb57456d786204af0f450ffb920039104c5eff6626337c9f403bd1/xyzservices-2025.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: ./ docs: @@ -269,132 +262,125 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - pypi: https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/50/fc9680058e63161f2f63165b84c957a0df1415431104c408e8104a3a18ef/branca-0.8.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/a8/5f764f333204db0390362a4356d03a43626997f26818a0e9396f1b3bd8c9/folium-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0b/70/d5cd0696eff08e62fdbdebe5b46527facb4e7220eabe0ac6225efab50168/geopandas-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0d/bc/6281ec7f9baaf71ee57c3b1748da2d3148d15d253e1a03006f204aa68ca5/igraph-1.0.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/46/0d/0a6fdf9587634452b9ced1de89280a3daeb1fee16878ca442dfe76c9b0a8/jupyter_book-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/86/d45756beaeb4b9b06125599b429451f8640b5db6f019d606f33c85743fd4/jupyter_book-1.0.4.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/6b/67b87da9d36bff9df7d0efbd1a325fa372a43be7158effaf43ed7b22341d/jupyter_cache-1.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/40/23569737873cc9637fd488606347e9dd92b9fa37ba4fcda1f98ee5219a97/latexcodec-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7a/f0/8282d9641415e9e33df173516226b404d367a0fc55e1a60424a152913abc/mistune-3.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/a6/03d410c114b8c4856579b3d294dafc27626a7690a552625eec42b16dfa41/myst_nb-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e2/de/21aa8394f16add8f7427f0a1326ccd2b3a2a8a3245c9252bc5ac034c6155/myst_parser-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/77/5f/2fc0135c5aa3fc36c7cb1ac63852349681908778eb15ab3e1aacbddc1b54/osmnx-2.0.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d5/72/7faa058dc6548e58cbdb7c6e8e0dbb56516ee941a7785fb74fb455fa28b2/polyline-2.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/68/ceb5d6679baa326261f5d3e5113d9cfed6efef2810afd9f18bffb8ed312b/pybtex-0.25.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/b1/ce1f4596211efb5410e178a803f08e59b20bedb66837dcf41e21c54f9ec1/pybtex_docutils-1.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/d3/c622950d87a2ffd1654208733b5bd1c5645930014abed8f4c0d74863988b/pydata_sphinx_theme-0.15.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/68/86328e36d010ee565ce0c65cdf9b830afcb1fb5972f537fe1cc561a49247/pyogrio-0.11.1-cp313-cp313-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/0f/73/bb1bc2529f852e7bf64a2dec885e89ff9f5cc7bbf6c9340eed30ff2c69c5/ruamel.yaml-0.18.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/ae/e3811f05415594025e96000349d3400978adaed88d8f98d494352d9761ee/ruamel.yaml.clib-0.2.14-cp313-cp313-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/f2/9657c98a66973b7c35bfd48ba65d1922860de9598fbb535cd96e3f58a908/sphinx_autodoc_typehints-3.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f3/e0a4ce49da4b6f4e4ce84b3c39a0677831884cb9d8a87ccbf1e9e56e53ac/sphinx_autodoc_typehints-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/9e/c41d68be04eef5b6202b468e0f90faf0c469f3a03353f2a218fd78279710/sphinx_book_theme-1.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/97/a5c39f619375d4f81d5422377fb027075898efa6b6202c1ccf1e5bb38a32/sphinx_comments-0.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/cf/45dd359f6ca0c3762ce0490f681da242f0530c49c81050c035c016bfdd3a/sphinx_design-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9f/80/1704c9179012e289dee2178354e385277ea51f4fa827c4bf7e36c77b0f4b/sphinx_external_toc-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/70/1f/1d4ecaf58b17fe61497644655f40b04d84a88348e41a6f0c6392394d95e4/sphinx_jupyterbook_latex-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/9f/902f2030674cd9473fdbe5a2c2dec2618c27ec853484c35f82cf8df40ece/sphinx_multitoc_numbering-0.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/7c/a53bdb465fd364bc3d255d96d5d70e6ba5183cfb4e45b8aa91c59b099124/sphinx_thebe-0.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/cb/9f6ceb4308ebfe5f393a271ee6206e17883edee0662a9b5c1a371878064b/sphinx_togglebutton-0.4.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/d6/255f0a1d3fa8990b14d2200aacf0f7c87c026e9ec20801e503529537e628/sphinxcontrib_autoyaml-1.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/a0/3a612da94f828f26cabb247817393e79472c32b12c49222bf85fb6d7b6c8/sphinxcontrib_bibtex-2.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cd/c8/784b9ac6ea08aa594c1a4becbd0dbe77186785362e31fd633b8c6ae0197a/sphinxcontrib_mermaid-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/99/4772b8e00a136f3e01236de33b0efda31ee7077203ba5967fcc76da94d65/texttable-1.7.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5b/8f/447cc9cb57456d786204af0f450ffb920039104c5eff6626337c9f403bd1/xyzservices-2025.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl - pypi: ./ tests: channels: @@ -479,11 +465,11 @@ packages: - hypothesis ; extra == 'tests' - pytest ; extra == 'tests' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl name: alabaster - version: 1.0.0 - sha256: fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b - requires_python: '>=3.10' + version: 0.7.16 + sha256: b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92 + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl name: anyio version: 4.11.0 @@ -500,42 +486,6 @@ packages: version: 0.1.4 sha256: 502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - name: argon2-cffi - version: 25.1.0 - sha256: fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741 - requires_dist: - - argon2-cffi-bindings - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl - name: argon2-cffi-bindings - version: 25.1.0 - sha256: 7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0 - requires_dist: - - cffi>=1.0.1 ; python_full_version < '3.14' - - cffi>=2.0.0b1 ; python_full_version >= '3.14' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl - name: arrow - version: 1.4.0 - sha256: 749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205 - requires_dist: - - python-dateutil>=2.7.0 - - backports-zoneinfo==0.2.1 ; python_full_version < '3.9' - - tzdata ; python_full_version >= '3.9' - - doc8 ; extra == 'doc' - - sphinx>=7.0.0 ; extra == 'doc' - - sphinx-autobuild ; extra == 'doc' - - sphinx-autodoc-typehints ; extra == 'doc' - - sphinx-rtd-theme>=1.3.0 ; extra == 'doc' - - dateparser==1.* ; extra == 'test' - - pre-commit ; extra == 'test' - - pytest ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-mock ; extra == 'test' - - pytz==2025.2 ; extra == 'test' - - simplejson==3.* ; extra == 'test' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl name: asttokens version: 3.0.0 @@ -580,14 +530,6 @@ packages: - html5lib ; extra == 'html5lib' - lxml ; extra == 'lxml' requires_python: '>=3.7.0' -- pypi: https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl - name: bleach - version: 6.3.0 - sha256: fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6 - requires_dist: - - webencodings - - tinycss2>=1.1.0,<1.5 ; extra == 'css' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/7e/50/fc9680058e63161f2f63165b84c957a0df1415431104c408e8104a3a18ef/branca-0.8.2-py3-none-any.whl name: branca version: 0.8.2 @@ -619,13 +561,6 @@ packages: version: 2025.11.12 sha256: 97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl - name: cffi - version: 2.0.0 - sha256: 45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca - requires_dist: - - pycparser ; implementation_name != 'PyPy' - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl name: cfgv version: 3.4.0 @@ -705,11 +640,6 @@ packages: version: 5.2.1 sha256: d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - name: defusedxml - version: 0.7.1 - sha256: a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*' - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl name: distlib version: 0.4.0 @@ -796,13 +726,6 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.23.0 ; extra == 'all' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - name: fqdn - version: 1.5.1 - sha256: 3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014 - requires_dist: - - cached-property>=1.3.0 ; python_full_version < '3.8' - requires_python: '>=2.7,!=3.0,!=3.1,!=3.2,!=3.3,!=3.4,<4' - pypi: https://files.pythonhosted.org/packages/0b/70/d5cd0696eff08e62fdbdebe5b46527facb4e7220eabe0ac6225efab50168/geopandas-1.1.1-py3-none-any.whl name: geopandas version: 1.1.1 @@ -972,6 +895,32 @@ packages: version: 1.4.1 sha256: 0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' +- pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl + name: importlib-metadata + version: 8.7.1 + sha256: 5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151 + requires_dist: + - zipp>=3.20 + - pytest>=6,!=8.1.* ; extra == 'test' + - packaging ; extra == 'test' + - pyfakefs ; extra == 'test' + - flufl-flake8 ; extra == 'test' + - pytest-perf>=0.9.2 ; extra == 'test' + - jaraco-test>=5.4 ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - ipython ; extra == 'perf' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=3.4 ; extra == 'enabler' + - pytest-mypy>=1.0.1 ; extra == 'type' + - mypy<1.19 ; platform_python_implementation == 'PyPy' and extra == 'type' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl name: iniconfig version: 2.3.0 @@ -1069,13 +1018,6 @@ packages: requires_dist: - pygments requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - name: isoduration - version: 20.11.0 - sha256: b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042 - requires_dist: - - arrow>=0.15.0 - requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl name: jaraco-classes version: 3.4.0 @@ -1183,11 +1125,6 @@ packages: - markupsafe>=2.0 - babel>=2.7 ; extra == 'i18n' requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - name: jsonpointer - version: 3.0.0 - sha256: 13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942 - requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl name: jsonschema version: 4.25.1 @@ -1222,19 +1159,98 @@ packages: requires_dist: - referencing>=0.31.0 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/46/0d/0a6fdf9587634452b9ced1de89280a3daeb1fee16878ca442dfe76c9b0a8/jupyter_book-2.0.2-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/37/86/d45756beaeb4b9b06125599b429451f8640b5db6f019d606f33c85743fd4/jupyter_book-1.0.4.post1-py3-none-any.whl name: jupyter-book - version: 2.0.2 - sha256: 7ccbb5f00dc55d172f148eae179d257e4cfe815ee297050553733d13018cae57 + version: 1.0.4.post1 + sha256: 3a27a6b2581f1894ffe8f347d1a3432f06fc616997547919c42cd41c54db625d + requires_dist: + - click>=7.1,<9 + - jinja2 + - jsonschema<5 + - linkify-it-py>=2,<3 + - myst-nb~=1.0 + - myst-parser~=3.0 + - pyyaml + - sphinx-book-theme~=1.1 + - sphinx-comments~=0.0 + - sphinx-copybutton~=0.5 + - sphinx-design~=0.6 + - sphinx-external-toc~=1.0 + - sphinx-jupyterbook-latex~=1.0 + - sphinx-multitoc-numbering~=0.1 + - sphinx-thebe~=0.3 + - sphinx-togglebutton~=0.3 + - sphinxcontrib-bibtex~=2.5 + - sphinx~=7.0 + - pre-commit~=3.1 ; extra == 'code-style' + - playwright ; extra == 'pdfhtml' + - altair ; extra == 'sphinx' + - bokeh ; extra == 'sphinx' + - folium ; extra == 'sphinx' + - ipywidgets ; extra == 'sphinx' + - jupytext ; extra == 'sphinx' + - matplotlib ; extra == 'sphinx' + - nbclient ; extra == 'sphinx' + - numpy>=2 ; extra == 'sphinx' + - pandas ; extra == 'sphinx' + - plotly ; extra == 'sphinx' + - sphinx-click ; extra == 'sphinx' + - sphinx-examples ; extra == 'sphinx' + - sphinx-inline-tabs ; extra == 'sphinx' + - sphinx-proof ; extra == 'sphinx' + - sphinxext-rediraffe~=0.2.3 ; extra == 'sphinx' + - sympy ; extra == 'sphinx' + - altair ; extra == 'testing' + - beautifulsoup4 ; extra == 'testing' + - cookiecutter ; extra == 'testing' + - coverage ; extra == 'testing' + - jupytext ; extra == 'testing' + - matplotlib ; extra == 'testing' + - numpy>=2 ; extra == 'testing' + - pandas ; extra == 'testing' + - playwright ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - pytest-timeout ; extra == 'testing' + - pytest-xdist ; extra == 'testing' + - pytest>=6.2.4 ; extra == 'testing' + - sphinx-click ; extra == 'testing' + - sphinx-inline-tabs ; extra == 'testing' + - texsoup ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/64/6b/67b87da9d36bff9df7d0efbd1a325fa372a43be7158effaf43ed7b22341d/jupyter_cache-1.0.1-py3-none-any.whl + name: jupyter-cache + version: 1.0.1 + sha256: 9c3cafd825ba7da8b5830485343091143dff903e4d8c69db9349b728b140abf6 requires_dist: - - ipykernel - - jupyter-core - - jupyter-server - - platformdirs>=4.2.2 - - nodeenv>=1.9.1 - - markdown ; extra == 'docs' - - pandas ; extra == 'docs' - - pooch ; extra == 'docs' + - attrs + - click + - importlib-metadata + - nbclient>=0.2 + - nbformat + - pyyaml + - sqlalchemy>=1.3.12,<3 + - tabulate + - click-log ; extra == 'cli' + - pre-commit>=2.12 ; extra == 'code-style' + - nbdime ; extra == 'rtd' + - ipykernel ; extra == 'rtd' + - jupytext ; extra == 'rtd' + - myst-nb ; extra == 'rtd' + - sphinx-book-theme ; extra == 'rtd' + - sphinx-copybutton ; extra == 'rtd' + - nbdime ; extra == 'testing' + - coverage ; extra == 'testing' + - ipykernel ; extra == 'testing' + - jupytext ; extra == 'testing' + - matplotlib ; extra == 'testing' + - nbformat>=5.1 ; extra == 'testing' + - numpy ; extra == 'testing' + - pandas ; extra == 'testing' + - pytest>=6 ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - sympy ; extra == 'testing' requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl name: jupyter-client @@ -1283,110 +1299,6 @@ packages: - pytest-timeout ; extra == 'test' - pytest<9 ; extra == 'test' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - name: jupyter-events - version: 0.12.0 - sha256: 6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb - requires_dist: - - jsonschema[format-nongpl]>=4.18.0 - - packaging - - python-json-logger>=2.0.4 - - pyyaml>=5.3 - - referencing - - rfc3339-validator - - rfc3986-validator>=0.1.1 - - traitlets>=5.3 - - click ; extra == 'cli' - - rich ; extra == 'cli' - - jupyterlite-sphinx ; extra == 'docs' - - myst-parser ; extra == 'docs' - - pydata-sphinx-theme>=0.16 ; extra == 'docs' - - sphinx>=8 ; extra == 'docs' - - sphinxcontrib-spelling ; extra == 'docs' - - click ; extra == 'test' - - pre-commit ; extra == 'test' - - pytest-asyncio>=0.19.0 ; extra == 'test' - - pytest-console-scripts ; extra == 'test' - - pytest>=7.0 ; extra == 'test' - - rich ; extra == 'test' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - name: jupyter-server - version: 2.17.0 - sha256: e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f - requires_dist: - - anyio>=3.1.0 - - argon2-cffi>=21.1 - - jinja2>=3.0.3 - - jupyter-client>=7.4.4 - - jupyter-core>=4.12,!=5.0.* - - jupyter-events>=0.11.0 - - jupyter-server-terminals>=0.4.4 - - nbconvert>=6.4.4 - - nbformat>=5.3.0 - - overrides>=5.0 ; python_full_version < '3.12' - - packaging>=22.0 - - prometheus-client>=0.9 - - pywinpty>=2.0.1 ; os_name == 'nt' - - pyzmq>=24 - - send2trash>=1.8.2 - - terminado>=0.8.3 - - tornado>=6.2.0 - - traitlets>=5.6.0 - - websocket-client>=1.7 - - ipykernel ; extra == 'docs' - - jinja2 ; extra == 'docs' - - jupyter-client ; extra == 'docs' - - myst-parser ; extra == 'docs' - - nbformat ; extra == 'docs' - - prometheus-client ; extra == 'docs' - - pydata-sphinx-theme ; extra == 'docs' - - send2trash ; extra == 'docs' - - sphinx-autodoc-typehints ; extra == 'docs' - - sphinxcontrib-github-alt ; extra == 'docs' - - sphinxcontrib-openapi>=0.8.0 ; extra == 'docs' - - sphinxcontrib-spelling ; extra == 'docs' - - sphinxemoji ; extra == 'docs' - - tornado ; extra == 'docs' - - typing-extensions ; extra == 'docs' - - flaky ; extra == 'test' - - ipykernel ; extra == 'test' - - pre-commit ; extra == 'test' - - pytest-console-scripts ; extra == 'test' - - pytest-jupyter[server]>=0.7 ; extra == 'test' - - pytest-timeout ; extra == 'test' - - pytest>=7.0,<9 ; extra == 'test' - - requests ; extra == 'test' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl - name: jupyter-server-terminals - version: 0.5.3 - sha256: 41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa - requires_dist: - - pywinpty>=2.0.3 ; os_name == 'nt' - - terminado>=0.8.3 - - jinja2 ; extra == 'docs' - - jupyter-server ; extra == 'docs' - - mistune<4.0 ; extra == 'docs' - - myst-parser ; extra == 'docs' - - nbformat ; extra == 'docs' - - packaging ; extra == 'docs' - - pydata-sphinx-theme ; extra == 'docs' - - sphinxcontrib-github-alt ; extra == 'docs' - - sphinxcontrib-openapi ; extra == 'docs' - - sphinxcontrib-spelling ; extra == 'docs' - - sphinxemoji ; extra == 'docs' - - tornado ; extra == 'docs' - - jupyter-server>=2.0.0 ; extra == 'test' - - pytest-jupyter[server]>=0.5.3 ; extra == 'test' - - pytest-timeout ; extra == 'test' - - pytest>=7.0 ; extra == 'test' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - name: jupyterlab-pygments - version: 0.3.0 - sha256: 841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780 - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl name: keyring version: 25.6.0 @@ -1423,16 +1335,11 @@ packages: version: 1.4.9 sha256: 1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl - name: lark - version: 1.3.1 - sha256: c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12 - requires_dist: - - regex ; extra == 'regex' - - js2py ; extra == 'nearley' - - atomicwrites ; extra == 'atomic-cache' - - interegular>=0.3.1,<0.4.0 ; extra == 'interegular' - requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/b5/40/23569737873cc9637fd488606347e9dd92b9fa37ba4fcda1f98ee5219a97/latexcodec-3.0.1-py3-none-any.whl + name: latexcodec + version: 3.0.1 + sha256: a9eb8200bff693f0437a69581f7579eb6bca25c4193515c09900ce76451e452e + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.1-hec049ff_0.conda sha256: 8fbb17a56f51e7113ed511c5787e0dec0d4b10ef9df921c4fd1cccca0458f648 md5: b1ca5f21335782f71a8bd69bdc093f67 @@ -1499,10 +1406,30 @@ packages: purls: [] size: 46438 timestamp: 1727963202283 +- pypi: https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl + name: linkify-it-py + version: 2.0.3 + sha256: 6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79 + requires_dist: + - uc-micro-py + - pytest ; extra == 'benchmark' + - pytest-benchmark ; extra == 'benchmark' + - pre-commit ; extra == 'dev' + - isort ; extra == 'dev' + - flake8 ; extra == 'dev' + - black ; extra == 'dev' + - pyproject-flake8 ; extra == 'dev' + - sphinx ; extra == 'doc' + - sphinx-book-theme ; extra == 'doc' + - myst-parser ; extra == 'doc' + - pytest ; extra == 'test' + - coverage ; extra == 'test' + - pytest-cov ; extra == 'test' + requires_python: '>=3.7' - pypi: ./ name: mappymatch - version: 0.7.0 - sha256: ce0afb10e4ff436fc2331337d9f46caa26533faf116febb658103eb203f6e7c8 + version: 0.7.1 + sha256: f86ba7fd8c2b8da5839ce4eaae1b1c23b96092e310088896a50d5a135a4c5a44 requires_dist: - folium>=0.20,<1 - geopandas>=1,<2 @@ -1518,7 +1445,7 @@ packages: - shapely>=2,<3 - coverage ; extra == 'dev' - hatch>=1,<2 ; extra == 'dev' - - jupyter-book>=2 ; extra == 'dev' + - jupyter-book>=1.0,<2.0 ; extra == 'dev' - mypy>=1,<2 ; extra == 'dev' - pre-commit ; extra == 'dev' - pytest>=9,<10 ; extra == 'dev' @@ -1528,7 +1455,7 @@ packages: - sphinxcontrib-autoyaml ; extra == 'dev' - sphinxcontrib-mermaid ; extra == 'dev' - types-requests ; extra == 'dev' - - jupyter-book>=2 ; extra == 'docs' + - jupyter-book>=1.0,<2.0 ; extra == 'docs' - sphinx-autodoc-typehints ; extra == 'docs' - sphinx-book-theme ; extra == 'docs' - sphinxcontrib-autoyaml ; extra == 'docs' @@ -1539,39 +1466,37 @@ packages: - types-requests ; extra == 'tests' requires_python: '>=3.10' editable: true -- pypi: https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl name: markdown-it-py - version: 4.0.0 - sha256: 87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 + version: 3.0.0 + sha256: 355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 requires_dist: - mdurl~=0.1 - psutil ; extra == 'benchmarking' - pytest ; extra == 'benchmarking' - pytest-benchmark ; extra == 'benchmarking' + - pre-commit~=3.0 ; extra == 'code-style' - commonmark~=0.9 ; extra == 'compare' - markdown~=3.4 ; extra == 'compare' - mistletoe~=1.0 ; extra == 'compare' - - mistune~=3.0 ; extra == 'compare' + - mistune~=2.0 ; extra == 'compare' - panflute~=2.3 ; extra == 'compare' - - markdown-it-pyrs ; extra == 'compare' - linkify-it-py>=1,<3 ; extra == 'linkify' - - mdit-py-plugins>=0.5.0 ; extra == 'plugins' + - mdit-py-plugins ; extra == 'plugins' - gprof2dot ; extra == 'profiling' - - mdit-py-plugins>=0.5.0 ; extra == 'rtd' + - mdit-py-plugins ; extra == 'rtd' - myst-parser ; extra == 'rtd' - pyyaml ; extra == 'rtd' - sphinx ; extra == 'rtd' - sphinx-copybutton ; extra == 'rtd' - sphinx-design ; extra == 'rtd' - - sphinx-book-theme~=1.0 ; extra == 'rtd' + - sphinx-book-theme ; extra == 'rtd' - jupyter-sphinx ; extra == 'rtd' - - ipykernel ; extra == 'rtd' - coverage ; extra == 'testing' - pytest ; extra == 'testing' - pytest-cov ; extra == 'testing' - pytest-regressions ; extra == 'testing' - - requests ; extra == 'testing' - requires_python: '>=3.10' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl name: markupsafe version: 3.0.3 @@ -1608,18 +1533,25 @@ packages: - notebook ; extra == 'test' - pytest ; extra == 'test' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + name: mdit-py-plugins + version: 0.5.0 + sha256: 07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f + requires_dist: + - markdown-it-py>=2.0.0,<5.0.0 + - pre-commit ; extra == 'code-style' + - myst-parser ; extra == 'rtd' + - sphinx-book-theme ; extra == 'rtd' + - coverage ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl name: mdurl version: 0.1.2 sha256: 84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/7a/f0/8282d9641415e9e33df173516226b404d367a0fc55e1a60424a152913abc/mistune-3.1.4-py3-none-any.whl - name: mistune - version: 3.1.4 - sha256: 93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d - requires_dist: - - typing-extensions ; python_full_version < '3.11' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl name: more-itertools version: 10.8.0 @@ -1645,6 +1577,92 @@ packages: version: 1.1.0 sha256: 1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/69/a6/03d410c114b8c4856579b3d294dafc27626a7690a552625eec42b16dfa41/myst_nb-1.3.0-py3-none-any.whl + name: myst-nb + version: 1.3.0 + sha256: 1f36af3c19964960ec4e51ac30949b6ed6df220356ffa8d60dd410885e132d7d + requires_dist: + - importlib-metadata + - ipython + - jupyter-cache>=0.5 + - nbclient + - myst-parser>=1.0.0 + - nbformat>=5.0 + - pyyaml + - sphinx>=5 + - typing-extensions + - ipykernel + - pre-commit ; extra == 'code-style' + - alabaster ; extra == 'rtd' + - altair ; extra == 'rtd' + - bokeh ; extra == 'rtd' + - coconut>=1.4.3 ; extra == 'rtd' + - ipykernel>=5.5 ; extra == 'rtd' + - ipywidgets ; extra == 'rtd' + - jupytext>=1.11.2 ; extra == 'rtd' + - matplotlib ; extra == 'rtd' + - numpy ; extra == 'rtd' + - pandas ; extra == 'rtd' + - plotly ; extra == 'rtd' + - sphinx-book-theme>=0.3 ; extra == 'rtd' + - sphinx-copybutton ; extra == 'rtd' + - sphinx-design ; extra == 'rtd' + - sphinxcontrib-bibtex ; extra == 'rtd' + - sympy ; extra == 'rtd' + - sphinx-autodoc-typehints ; extra == 'rtd' + - coverage>=6.4 ; extra == 'testing' + - beautifulsoup4 ; extra == 'testing' + - ipykernel>=5.5 ; extra == 'testing' + - ipython!=8.1.0 ; extra == 'testing' + - ipywidgets>=8 ; extra == 'testing' + - jupytext>=1.11.2 ; extra == 'testing' + - matplotlib==3.7.* ; extra == 'testing' + - nbdime ; extra == 'testing' + - numpy ; extra == 'testing' + - pandas ; extra == 'testing' + - pyarrow ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov>=3 ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - pytest-param-files ; extra == 'testing' + - sympy>=1.10.1 ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/e2/de/21aa8394f16add8f7427f0a1326ccd2b3a2a8a3245c9252bc5ac034c6155/myst_parser-3.0.1-py3-none-any.whl + name: myst-parser + version: 3.0.1 + sha256: 6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1 + requires_dist: + - docutils>=0.18,<0.22 + - jinja2 + - markdown-it-py~=3.0 + - mdit-py-plugins~=0.4 + - pyyaml + - sphinx>=6,<8 + - pre-commit~=3.0 ; extra == 'code-style' + - linkify-it-py~=2.0 ; extra == 'linkify' + - sphinx>=7 ; extra == 'rtd' + - ipython ; extra == 'rtd' + - sphinx-book-theme~=1.1 ; extra == 'rtd' + - sphinx-design ; extra == 'rtd' + - sphinx-copybutton ; extra == 'rtd' + - sphinxext-rediraffe~=0.2.7 ; extra == 'rtd' + - sphinxext-opengraph~=0.9.0 ; extra == 'rtd' + - sphinx-pyscript ; extra == 'rtd' + - sphinx-tippy>=0.4.3 ; extra == 'rtd' + - sphinx-autodoc2~=0.5.0 ; extra == 'rtd' + - sphinx-togglebutton ; extra == 'rtd' + - beautifulsoup4 ; extra == 'testing' + - coverage[toml] ; extra == 'testing' + - defusedxml ; extra == 'testing' + - pytest>=8,<9 ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - pytest-param-files~=0.6.0 ; extra == 'testing' + - sphinx-pytest ; extra == 'testing' + - pygments ; extra == 'testing-docutils' + - pytest>=8,<9 ; extra == 'testing-docutils' + - pytest-param-files~=0.6.0 ; extra == 'testing-docutils' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl name: nbclient version: 0.10.2 @@ -1683,55 +1701,6 @@ packages: - testpath ; extra == 'test' - xmltodict ; extra == 'test' requires_python: '>=3.9.0' -- pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl - name: nbconvert - version: 7.16.6 - sha256: 1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b - requires_dist: - - beautifulsoup4 - - bleach[css]!=5.0.0 - - defusedxml - - importlib-metadata>=3.6 ; python_full_version < '3.10' - - jinja2>=3.0 - - jupyter-core>=4.7 - - jupyterlab-pygments - - markupsafe>=2.0 - - mistune>=2.0.3,<4 - - nbclient>=0.5.0 - - nbformat>=5.7 - - packaging - - pandocfilters>=1.4.1 - - pygments>=2.4.1 - - traitlets>=5.1 - - flaky ; extra == 'all' - - ipykernel ; extra == 'all' - - ipython ; extra == 'all' - - ipywidgets>=7.5 ; extra == 'all' - - myst-parser ; extra == 'all' - - nbsphinx>=0.2.12 ; extra == 'all' - - playwright ; extra == 'all' - - pydata-sphinx-theme ; extra == 'all' - - pyqtwebengine>=5.15 ; extra == 'all' - - pytest>=7 ; extra == 'all' - - sphinx==5.0.2 ; extra == 'all' - - sphinxcontrib-spelling ; extra == 'all' - - tornado>=6.1 ; extra == 'all' - - ipykernel ; extra == 'docs' - - ipython ; extra == 'docs' - - myst-parser ; extra == 'docs' - - nbsphinx>=0.2.12 ; extra == 'docs' - - pydata-sphinx-theme ; extra == 'docs' - - sphinx==5.0.2 ; extra == 'docs' - - sphinxcontrib-spelling ; extra == 'docs' - - pyqtwebengine>=5.15 ; extra == 'qtpdf' - - pyqtwebengine>=5.15 ; extra == 'qtpng' - - tornado>=6.1 ; extra == 'serve' - - flaky ; extra == 'test' - - ipykernel ; extra == 'test' - - ipywidgets>=7.5 ; extra == 'test' - - pytest>=7 ; extra == 'test' - - playwright ; extra == 'webpdf' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl name: nbformat version: 5.10.4 @@ -1937,11 +1906,6 @@ packages: - xlsxwriter>=3.0.5 ; extra == 'all' - zstandard>=0.19.0 ; extra == 'all' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - name: pandocfilters - version: 1.5.1 - sha256: 93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl name: parso version: 0.8.5 @@ -2049,13 +2013,6 @@ packages: - pyyaml>=5.1 - virtualenv>=20.10.0 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl - name: prometheus-client - version: 0.23.1 - sha256: dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99 - requires_dist: - - twisted ; extra == 'twisted' - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl name: prompt-toolkit version: 3.0.52 @@ -2117,11 +2074,25 @@ packages: sha256: 1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0 requires_dist: - pytest ; extra == 'tests' -- pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - name: pycparser - version: '2.23' - sha256: e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 - requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/25/68/ceb5d6679baa326261f5d3e5113d9cfed6efef2810afd9f18bffb8ed312b/pybtex-0.25.1-py2.py3-none-any.whl + name: pybtex + version: 0.25.1 + sha256: 9053b0d619409a0a83f38abad5d9921de5f7b3ede00742beafcd9f10ad0d8c5c + requires_dist: + - pyyaml>=3.1 + - latexcodec>=1.0.4 + - importlib-metadata ; python_full_version < '3.10' + - pytest ; extra == 'test' + - sphinx ; extra == 'doc' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/11/b1/ce1f4596211efb5410e178a803f08e59b20bedb66837dcf41e21c54f9ec1/pybtex_docutils-1.0.3-py3-none-any.whl + name: pybtex-docutils + version: 1.0.3 + sha256: 8fd290d2ae48e32fcb54d86b0efb8d573198653c7e2447d5bec5847095f430b9 + requires_dist: + - docutils>=0.14 + - pybtex>=0.16 + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/e7/d3/c622950d87a2ffd1654208733b5bd1c5645930014abed8f4c0d74863988b/pydata_sphinx_theme-0.15.4-py3-none-any.whl name: pydata-sphinx-theme version: 0.15.4 @@ -2262,32 +2233,6 @@ packages: requires_dist: - six>=1.5 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' -- pypi: https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl - name: python-json-logger - version: 4.0.0 - sha256: af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2 - requires_dist: - - typing-extensions ; python_full_version < '3.10' - - orjson ; implementation_name != 'pypy' and extra == 'dev' - - msgspec ; implementation_name != 'pypy' and extra == 'dev' - - validate-pyproject[all] ; extra == 'dev' - - black ; extra == 'dev' - - pylint ; extra == 'dev' - - mypy ; extra == 'dev' - - pytest ; extra == 'dev' - - freezegun ; extra == 'dev' - - backports-zoneinfo ; python_full_version < '3.9' and extra == 'dev' - - tzdata ; extra == 'dev' - - build ; extra == 'dev' - - mkdocs ; extra == 'dev' - - mkdocs-material>=8.5 ; extra == 'dev' - - mkdocs-awesome-pages-plugin ; extra == 'dev' - - mdx-truly-sane-lists ; extra == 'dev' - - mkdocstrings[python] ; extra == 'dev' - - mkdocs-gen-files ; extra == 'dev' - - mkdocs-literate-nav ; extra == 'dev' - - mike ; extra == 'dev' - requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda build_number: 8 sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 @@ -2346,26 +2291,6 @@ packages: - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' - chardet>=3.0.2,<6 ; extra == 'use-chardet-on-py3' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - name: rfc3339-validator - version: 0.1.4 - sha256: 24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa - requires_dist: - - six - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*' -- pypi: https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl - name: rfc3986-validator - version: 0.1.1 - sha256: 2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9 - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*' -- pypi: https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl - name: rfc3987-syntax - version: 1.1.0 - sha256: 6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f - requires_dist: - - lark>=1.2.2 - - pytest>=8.3.5 ; extra == 'testing' - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl name: rich version: 14.2.0 @@ -2375,16 +2300,6 @@ packages: - markdown-it-py>=2.2.0 - pygments>=2.13.0,<3.0.0 requires_python: '>=3.8.0' -- pypi: https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl - name: roman-numerals-py - version: 3.1.0 - sha256: 9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c - requires_dist: - - mypy==1.15.0 ; extra == 'lint' - - ruff==0.9.7 ; extra == 'lint' - - pyright==1.1.394 ; extra == 'lint' - - pytest>=8 ; extra == 'test' - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl name: rpds-py version: 0.28.0 @@ -2410,16 +2325,63 @@ packages: version: 0.14.5 sha256: 6d146132d1ee115f8802356a2dc9a634dbf58184c51bff21f313e8cd1c74899a requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl - name: send2trash - version: 1.8.3 - sha256: 0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9 - requires_dist: - - pyobjc-framework-cocoa ; sys_platform == 'darwin' and extra == 'nativelib' - - pywin32 ; sys_platform == 'win32' and extra == 'nativelib' - - pyobjc-framework-cocoa ; sys_platform == 'darwin' and extra == 'objc' - - pywin32 ; sys_platform == 'win32' and extra == 'win32' - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*' +- pypi: https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl + name: setuptools + version: 82.0.0 + sha256: 70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0 + requires_dist: + - pytest>=6,!=8.1.* ; extra == 'test' + - virtualenv>=13.0.0 ; extra == 'test' + - wheel>=0.44.0 ; extra == 'test' + - pip>=19.1 ; extra == 'test' + - packaging>=24.2 ; extra == 'test' + - jaraco-envs>=2.2 ; extra == 'test' + - pytest-xdist>=3 ; extra == 'test' + - jaraco-path>=3.7.2 ; extra == 'test' + - build[virtualenv]>=1.0.3 ; extra == 'test' + - filelock>=3.4.0 ; extra == 'test' + - ini2toml[lite]>=0.14 ; extra == 'test' + - tomli-w>=1.0.0 ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-perf ; sys_platform != 'cygwin' and extra == 'test' + - jaraco-develop>=7.21 ; python_full_version >= '3.9' and sys_platform != 'cygwin' and extra == 'test' + - pytest-home>=0.5 ; extra == 'test' + - pytest-subprocess ; extra == 'test' + - pyproject-hooks!=1.1 ; extra == 'test' + - jaraco-test>=5.5 ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pygments-github-lexers==0.0.5 ; extra == 'doc' + - sphinx-favicon ; extra == 'doc' + - sphinx-inline-tabs ; extra == 'doc' + - sphinx-reredirects ; extra == 'doc' + - sphinxcontrib-towncrier ; extra == 'doc' + - sphinx-notfound-page>=1,<2 ; extra == 'doc' + - pyproject-hooks!=1.1 ; extra == 'doc' + - towncrier<24.7 ; extra == 'doc' + - packaging>=24.2 ; extra == 'core' + - more-itertools>=8.8 ; extra == 'core' + - jaraco-text>=3.7 ; extra == 'core' + - importlib-metadata>=6 ; python_full_version < '3.10' and extra == 'core' + - tomli>=2.0.1 ; python_full_version < '3.11' and extra == 'core' + - wheel>=0.43.0 ; extra == 'core' + - platformdirs>=4.2.2 ; extra == 'core' + - jaraco-functools>=4 ; extra == 'core' + - more-itertools ; extra == 'core' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - ruff>=0.13.0 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=2.2 ; extra == 'enabler' + - pytest-mypy ; extra == 'type' + - mypy==1.18.* ; extra == 'type' + - importlib-metadata>=7.0.2 ; python_full_version < '3.10' and extra == 'type' + - jaraco-develop>=7.21 ; sys_platform != 'cygwin' and extra == 'type' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl name: shapely version: 2.1.2 @@ -2460,66 +2422,62 @@ packages: version: '2.8' sha256: 0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl name: sphinx - version: 8.2.3 - sha256: 4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3 - requires_dist: - - sphinxcontrib-applehelp>=1.0.7 - - sphinxcontrib-devhelp>=1.0.6 - - sphinxcontrib-htmlhelp>=2.0.6 - - sphinxcontrib-jsmath>=1.0.1 - - sphinxcontrib-qthelp>=1.0.6 + version: 7.4.7 + sha256: c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239 + requires_dist: + - sphinxcontrib-applehelp + - sphinxcontrib-devhelp + - sphinxcontrib-jsmath + - sphinxcontrib-htmlhelp>=2.0.0 - sphinxcontrib-serializinghtml>=1.1.9 + - sphinxcontrib-qthelp - jinja2>=3.1 - pygments>=2.17 - docutils>=0.20,<0.22 - snowballstemmer>=2.2 - babel>=2.13 - - alabaster>=0.7.14 + - alabaster~=0.7.14 - imagesize>=1.3 - requests>=2.30.0 - - roman-numerals-py>=1.0.0 - packaging>=23.0 + - importlib-metadata>=6.0 ; python_full_version < '3.10' + - tomli>=2 ; python_full_version < '3.11' - colorama>=0.4.6 ; sys_platform == 'win32' - sphinxcontrib-websupport ; extra == 'docs' - - ruff==0.9.9 ; extra == 'lint' - - mypy==1.15.0 ; extra == 'lint' + - flake8>=6.0 ; extra == 'lint' + - ruff==0.5.2 ; extra == 'lint' + - mypy==1.10.1 ; extra == 'lint' - sphinx-lint>=0.9 ; extra == 'lint' - - types-colorama==0.4.15.20240311 ; extra == 'lint' - - types-defusedxml==0.7.0.20240218 ; extra == 'lint' - - types-docutils==0.21.0.20241128 ; extra == 'lint' - - types-pillow==10.2.0.20240822 ; extra == 'lint' - - types-pygments==2.19.0.20250219 ; extra == 'lint' - - types-requests==2.32.0.20241016 ; extra == 'lint' - - types-urllib3==1.26.25.14 ; extra == 'lint' - - pyright==1.1.395 ; extra == 'lint' - - pytest>=8.0 ; extra == 'lint' - - pypi-attestations==0.0.21 ; extra == 'lint' - - betterproto==2.0.0b6 ; extra == 'lint' + - types-docutils==0.21.0.20240711 ; extra == 'lint' + - types-requests>=2.30.0 ; extra == 'lint' + - importlib-metadata>=6.0 ; extra == 'lint' + - tomli>=2 ; extra == 'lint' + - pytest>=6.0 ; extra == 'lint' - pytest>=8.0 ; extra == 'test' - - pytest-xdist[psutil]>=3.4 ; extra == 'test' - defusedxml>=0.7.1 ; extra == 'test' - cython>=3.0 ; extra == 'test' - setuptools>=70.0 ; extra == 'test' - typing-extensions>=4.9 ; extra == 'test' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/05/f2/9657c98a66973b7c35bfd48ba65d1922860de9598fbb535cd96e3f58a908/sphinx_autodoc_typehints-3.5.2-py3-none-any.whl + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a0/f3/e0a4ce49da4b6f4e4ce84b3c39a0677831884cb9d8a87ccbf1e9e56e53ac/sphinx_autodoc_typehints-2.3.0-py3-none-any.whl name: sphinx-autodoc-typehints - version: 3.5.2 - sha256: 0accd043619f53c86705958e323b419e41667917045ac9215d7be1b493648d8c + version: 2.3.0 + sha256: 3098e2c6d0ba99eacd013eb06861acc9b51c6e595be86ab05c08ee5506ac0c67 requires_dist: - - sphinx>=8.2.3 - - furo>=2025.9.25 ; extra == 'docs' + - sphinx>=7.3.5 + - furo>=2024.1.29 ; extra == 'docs' + - nptyping>=2.5 ; extra == 'numpy' - covdefaults>=2.3 ; extra == 'testing' - - coverage>=7.10.7 ; extra == 'testing' + - coverage>=7.4.4 ; extra == 'testing' - defusedxml>=0.7.1 ; extra == 'testing' - - diff-cover>=9.7.1 ; extra == 'testing' - - pytest-cov>=7 ; extra == 'testing' - - pytest>=8.4.2 ; extra == 'testing' - - sphobjinv>=2.3.1.3 ; extra == 'testing' - - typing-extensions>=4.15 ; extra == 'testing' - requires_python: '>=3.11' + - diff-cover>=9 ; extra == 'testing' + - pytest-cov>=5 ; extra == 'testing' + - pytest>=8.1.1 ; extra == 'testing' + - sphobjinv>=2.3.1 ; extra == 'testing' + - typing-extensions>=4.11 ; extra == 'testing' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/51/9e/c41d68be04eef5b6202b468e0f90faf0c469f3a03353f2a218fd78279710/sphinx_book_theme-1.1.4-py3-none-any.whl name: sphinx-book-theme version: 1.1.4 @@ -2556,6 +2514,150 @@ packages: - pytest-regressions ; extra == 'test' - sphinx-thebe ; extra == 'test' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/26/97/a5c39f619375d4f81d5422377fb027075898efa6b6202c1ccf1e5bb38a32/sphinx_comments-0.0.3-py3-none-any.whl + name: sphinx-comments + version: 0.0.3 + sha256: 1e879b4e9bfa641467f83e3441ac4629225fc57c29995177d043252530c21d00 + requires_dist: + - sphinx>=1.8 + - flake8>=3.7.0,<3.8.0 ; extra == 'code-style' + - black ; extra == 'code-style' + - pre-commit==1.17.0 ; extra == 'code-style' + - sphinx>=2 ; extra == 'sphinx' + - sphinx-book-theme ; extra == 'sphinx' + - myst-parser ; extra == 'sphinx' + - beautifulsoup4 ; extra == 'testing' + - myst-parser ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - sphinx>=2 ; extra == 'testing' + - sphinx-book-theme ; extra == 'testing' +- pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl + name: sphinx-copybutton + version: 0.5.2 + sha256: fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e + requires_dist: + - sphinx>=1.8 + - pre-commit==2.12.1 ; extra == 'code-style' + - sphinx ; extra == 'rtd' + - ipython ; extra == 'rtd' + - myst-nb ; extra == 'rtd' + - sphinx-book-theme ; extra == 'rtd' + - sphinx-examples ; extra == 'rtd' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/30/cf/45dd359f6ca0c3762ce0490f681da242f0530c49c81050c035c016bfdd3a/sphinx_design-0.7.0-py3-none-any.whl + name: sphinx-design + version: 0.7.0 + sha256: f82bf179951d58f55dca78ab3706aeafa496b741a91b1911d371441127d64282 + requires_dist: + - sphinx>=7,<10 + - pre-commit>=3,<4 ; extra == 'code-style' + - myst-parser>=4,<6 ; extra == 'rtd' + - myst-parser>=4,<6 ; extra == 'testing' + - pytest~=8.3 ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - defusedxml ; extra == 'testing' + - pytest~=8.3 ; extra == 'testing-no-myst' + - pytest-cov ; extra == 'testing-no-myst' + - pytest-regressions ; extra == 'testing-no-myst' + - defusedxml ; extra == 'testing-no-myst' + - furo~=2024.7.18 ; extra == 'theme-furo' + - sphinx-immaterial~=0.12.2 ; extra == 'theme-im' + - pydata-sphinx-theme~=0.15.2 ; extra == 'theme-pydata' + - sphinx-rtd-theme~=2.0 ; extra == 'theme-rtd' + - sphinx-book-theme~=1.1 ; extra == 'theme-sbt' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/9f/80/1704c9179012e289dee2178354e385277ea51f4fa827c4bf7e36c77b0f4b/sphinx_external_toc-1.1.0-py3-none-any.whl + name: sphinx-external-toc + version: 1.1.0 + sha256: 26c390b8d85aa641366fed2d3674910ec6820f48b91027affef485a2655ad7d0 + requires_dist: + - click>=7.1 + - pyyaml + - sphinx>=5 + - sphinx-multitoc-numbering>=0.1.3 + - pre-commit>=2.12 ; extra == 'code-style' + - myst-parser>=1.0.0 ; extra == 'rtd' + - sphinx-book-theme>=1.0.0 ; extra == 'rtd' + - coverage ; extra == 'testing' + - pytest>=7.1 ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/70/1f/1d4ecaf58b17fe61497644655f40b04d84a88348e41a6f0c6392394d95e4/sphinx_jupyterbook_latex-1.0.0-py3-none-any.whl + name: sphinx-jupyterbook-latex + version: 1.0.0 + sha256: e0cd3e9e1c5af69136434e21a533343fdf013475c410a414d5b7b4922b4f3891 + requires_dist: + - sphinx>=5 + - packaging + - pre-commit~=2.12 ; extra == 'code-style' + - myst-nb>=1.0.0 ; extra == 'myst' + - myst-parser ; extra == 'rtd' + - sphinx-book-theme ; extra == 'rtd' + - sphinx-design ; extra == 'rtd' + - sphinx-jupyterbook-latex ; extra == 'rtd' + - coverage>=6.0 ; extra == 'testing' + - myst-nb>=1.0.0 ; extra == 'testing' + - pytest-cov>=3 ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - pytest>=7.1 ; extra == 'testing' + - sphinx-external-toc>=1.0.0 ; extra == 'testing' + - sphinxcontrib-bibtex>=2.6.0 ; extra == 'testing' + - texsoup ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ec/9f/902f2030674cd9473fdbe5a2c2dec2618c27ec853484c35f82cf8df40ece/sphinx_multitoc_numbering-0.1.3-py3-none-any.whl + name: sphinx-multitoc-numbering + version: 0.1.3 + sha256: 33d2e707a9b2b8ad636b3d4302e658a008025106fe0474046c651144c26d8514 + requires_dist: + - sphinx>=3 + - flake8>=3.7.0,<3.8.0 ; extra == 'code-style' + - black ; extra == 'code-style' + - pre-commit==1.17.0 ; extra == 'code-style' + - sphinx>=3.0 ; extra == 'rtd' + - sphinx-book-theme ; extra == 'rtd' + - myst-parser ; extra == 'rtd' + - pytest~=5.4 ; extra == 'testing' + - pytest-cov~=2.8 ; extra == 'testing' + - coverage<5.0 ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - jupyter-book ; extra == 'testing' +- pypi: https://files.pythonhosted.org/packages/ca/7c/a53bdb465fd364bc3d255d96d5d70e6ba5183cfb4e45b8aa91c59b099124/sphinx_thebe-0.3.1-py3-none-any.whl + name: sphinx-thebe + version: 0.3.1 + sha256: e7e7edee9f0d601c76bc70156c471e114939484b111dd8e74fe47ac88baffc52 + requires_dist: + - sphinx>=4 + - sphinx-thebe[testing] ; extra == 'dev' + - myst-nb ; extra == 'sphinx' + - sphinx-book-theme ; extra == 'sphinx' + - sphinx-copybutton ; extra == 'sphinx' + - sphinx-design ; extra == 'sphinx' + - beautifulsoup4 ; extra == 'testing' + - matplotlib ; extra == 'testing' + - myst-nb>=1.0.0rc0 ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - sphinx-copybutton ; extra == 'testing' + - sphinx-design ; extra == 'testing' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/fa/cb/9f6ceb4308ebfe5f393a271ee6206e17883edee0662a9b5c1a371878064b/sphinx_togglebutton-0.4.4-py3-none-any.whl + name: sphinx-togglebutton + version: 0.4.4 + sha256: 820658cd4c4c34c2ee7a21105e638b2f65a9e1d43ee991090715eb7fd9683cdf + requires_dist: + - setuptools + - wheel + - sphinx + - docutils + - matplotlib ; extra == 'sphinx' + - numpy ; extra == 'sphinx' + - myst-nb ; extra == 'sphinx' + - sphinx-book-theme ; extra == 'sphinx' + - sphinx-design ; extra == 'sphinx' + - sphinx-examples ; extra == 'sphinx' - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl name: sphinxcontrib-applehelp version: 2.0.0 @@ -2577,6 +2679,20 @@ packages: - sphinx-testing>=1.0.1 ; extra == 'test' - flake8>=3.8.4 ; extra == 'test' requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/9e/a0/3a612da94f828f26cabb247817393e79472c32b12c49222bf85fb6d7b6c8/sphinxcontrib_bibtex-2.6.5-py3-none-any.whl + name: sphinxcontrib-bibtex + version: 2.6.5 + sha256: 455ea4509642ea0b28ede3721550273626f85af65af01f161bfd8e19dc1edd7d + requires_dist: + - sphinx>=3.5 + - docutils>=0.8,!=0.18.*,!=0.19.* + - pybtex>=0.25 + - pybtex-docutils>=1.0.0 + - importlib-metadata>=3.6 ; python_full_version < '3.10' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - sphinx-autoapi ; extra == 'test' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl name: sphinxcontrib-devhelp version: 2.0.0 @@ -2645,6 +2761,44 @@ packages: - sphinx>=5 ; extra == 'standalone' - pytest ; extra == 'test' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl + name: sqlalchemy + version: 2.0.46 + sha256: 93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00 + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' + - typing-extensions>=4.6.0 + - greenlet>=1 ; extra == 'asyncio' + - mypy>=0.910 ; extra == 'mypy' + - pyodbc ; extra == 'mssql' + - pymssql ; extra == 'mssql-pymssql' + - pyodbc ; extra == 'mssql-pyodbc' + - mysqlclient>=1.4.0 ; extra == 'mysql' + - mysql-connector-python ; extra == 'mysql-connector' + - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' + - cx-oracle>=8 ; extra == 'oracle' + - oracledb>=1.0.1 ; extra == 'oracle-oracledb' + - psycopg2>=2.7 ; extra == 'postgresql' + - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' + - greenlet>=1 ; extra == 'postgresql-asyncpg' + - asyncpg ; extra == 'postgresql-asyncpg' + - psycopg2-binary ; extra == 'postgresql-psycopg2binary' + - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' + - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' + - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' + - pymysql ; extra == 'pymysql' + - greenlet>=1 ; extra == 'aiomysql' + - aiomysql>=0.2.0 ; extra == 'aiomysql' + - greenlet>=1 ; extra == 'aioodbc' + - aioodbc ; extra == 'aioodbc' + - greenlet>=1 ; extra == 'asyncmy' + - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' + - greenlet>=1 ; extra == 'aiosqlite' + - aiosqlite ; extra == 'aiosqlite' + - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' + - sqlcipher3-binary ; extra == 'sqlcipher' + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl name: stack-data version: 0.6.3 @@ -2658,38 +2812,17 @@ packages: - pygments ; extra == 'tests' - littleutils ; extra == 'tests' - cython ; extra == 'tests' -- pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - name: terminado - version: 0.18.1 - sha256: a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0 - requires_dist: - - ptyprocess ; os_name != 'nt' - - pywinpty>=1.1.0 ; os_name == 'nt' - - tornado>=6.1.0 - - myst-parser ; extra == 'docs' - - pydata-sphinx-theme ; extra == 'docs' - - sphinx ; extra == 'docs' - - pre-commit ; extra == 'test' - - pytest-timeout ; extra == 'test' - - pytest>=7.0 ; extra == 'test' - - mypy~=1.6 ; extra == 'typing' - - traitlets>=5.11.1 ; extra == 'typing' - requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl + name: tabulate + version: 0.9.0 + sha256: 024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f + requires_dist: + - wcwidth ; extra == 'widechars' + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/24/99/4772b8e00a136f3e01236de33b0efda31ee7077203ba5967fcc76da94d65/texttable-1.7.0-py2.py3-none-any.whl name: texttable version: 1.7.0 sha256: 72227d592c82b3d7f672731ae73e4d1f88cd8e2ef5b075a7a7f01a23a3743917 -- pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl - name: tinycss2 - version: 1.4.0 - sha256: 3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289 - requires_dist: - - webencodings>=0.4 - - sphinx ; extra == 'doc' - - sphinx-rtd-theme ; extra == 'doc' - - pytest ; extra == 'test' - - ruff ; extra == 'test' - requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda sha256: ad0c67cb03c163a109820dc9ecf77faf6ec7150e942d1e8bb13e5d39dc058ab7 md5: a73d54a5abba6543cb2f0af1bfbd6851 @@ -2759,31 +2892,14 @@ packages: purls: [] size: 122968 timestamp: 1742727099393 -- pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - name: uri-template - version: 1.3.0 - sha256: a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363 +- pypi: https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl + name: uc-micro-py + version: 1.0.3 + sha256: db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5 requires_dist: - - types-pyyaml ; extra == 'dev' - - mypy ; extra == 'dev' - - flake8 ; extra == 'dev' - - flake8-annotations ; extra == 'dev' - - flake8-bandit ; extra == 'dev' - - flake8-bugbear ; extra == 'dev' - - flake8-commas ; extra == 'dev' - - flake8-comprehensions ; extra == 'dev' - - flake8-continuation ; extra == 'dev' - - flake8-datetimez ; extra == 'dev' - - flake8-docstrings ; extra == 'dev' - - flake8-import-order ; extra == 'dev' - - flake8-literal ; extra == 'dev' - - flake8-modern-annotations ; extra == 'dev' - - flake8-noqa ; extra == 'dev' - - flake8-pyproject ; extra == 'dev' - - flake8-requirements ; extra == 'dev' - - flake8-typechecking-import ; extra == 'dev' - - flake8-use-fstring ; extra == 'dev' - - pep8-naming ; extra == 'dev' + - pytest ; extra == 'test' + - coverage ; extra == 'test' + - pytest-cov ; extra == 'test' requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl name: urllib3 @@ -2843,33 +2959,44 @@ packages: version: 0.2.14 sha256: a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1 requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - name: webcolors - version: 25.10.0 - sha256: 032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - name: webencodings - version: 0.5.1 - sha256: a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 -- pypi: https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl - name: websocket-client - version: 1.9.0 - sha256: af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef - requires_dist: - - pytest ; extra == 'test' - - websockets ; extra == 'test' - - python-socks ; extra == 'optional' - - wsaccel ; extra == 'optional' - - sphinx>=6.0 ; extra == 'docs' - - sphinx-rtd-theme>=1.1.0 ; extra == 'docs' - - myst-parser>=2.0.0 ; extra == 'docs' +- pypi: https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl + name: wheel + version: 0.46.3 + sha256: 4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d + requires_dist: + - packaging>=24.0 + - pytest>=6.0.0 ; extra == 'test' + - setuptools>=77 ; extra == 'test' requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/5b/8f/447cc9cb57456d786204af0f450ffb920039104c5eff6626337c9f403bd1/xyzservices-2025.10.0-py3-none-any.whl name: xyzservices version: 2025.10.0 sha256: cfd6423367c7bc717ed5824d4dd7de2c91486886c1c193db9d8f0fa7fd43bc1b requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + name: zipp + version: 3.23.0 + sha256: 071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e + requires_dist: + - pytest>=6,!=8.1.* ; extra == 'test' + - jaraco-itertools ; extra == 'test' + - jaraco-functools ; extra == 'test' + - more-itertools ; extra == 'test' + - big-o ; extra == 'test' + - pytest-ignore-flaky ; extra == 'test' + - jaraco-test ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=2.2 ; extra == 'enabler' + - pytest-mypy ; extra == 'type' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl name: zstandard version: 0.25.0 diff --git a/pyproject.toml b/pyproject.toml index 0d5ee28..d5ee208 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,8 +7,8 @@ name = "mappymatch" dynamic = ["version"] description = "Pure python package for map-matching." readme = "README.md" -authors = [{ name = "National Renewable Energy Laboratory" }] -license = { text = "BSD 3-Clause License Copyright (c) 2022, Alliance for Sustainable Energy, LLC" } +authors = [{ name = "National Laboratory of the Rockies" }] +license = { text = "BSD 3-Clause License Copyright (c) 2022, Alliance for Energy Innovation, LLC" } classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", @@ -43,7 +43,7 @@ requires-python = ">=3.10" tests = ["ruff>=0.14,<1", "mypy>=1,<2", "types-requests", "pytest>=9,<10"] # Used to build the docs. docs = [ - "jupyter-book>=2", + "jupyter-book>=1.0,<2.0", "sphinx-book-theme", "sphinx-autodoc-typehints", "sphinxcontrib-autoyaml", @@ -124,4 +124,5 @@ fmt_fix = "ruff format" lint_fix = "ruff check --fix" typing = "mypy ." test = "pytest tests/" +docs = "cd docs/ && jupyter-book build ." check = { depends-on = ["fmt_fix", "lint_fix", "typing", "test"] } From 2a1dfef032d93e5a2616e64fdc309474d11a2454 Mon Sep 17 00:00:00 2001 From: Nicholas Reinicke Date: Wed, 18 Feb 2026 14:30:21 -0700 Subject: [PATCH 2/4] use example notebook converter script --- .github/workflows/deploy-docs.yaml | 4 + .github/workflows/lint-test.yml | 8 + .gitignore | 3 + .../mappymatch.constructs.coordinate.rst | 12 - .../mappymatch.constructs.geofence.rst | 12 - .../mappymatch.constructs.match.rst | 12 - .../mappymatch.constructs.road.rst | 13 - docs/_autosummary/mappymatch.constructs.rst | 17 - .../mappymatch.constructs.trace.rst | 12 - .../mappymatch.maps.igraph.igraph_map.rst | 12 - docs/_autosummary/mappymatch.maps.igraph.rst | 13 - .../mappymatch.maps.map_interface.rst | 12 - .../mappymatch.maps.nx.nx_map.rst | 12 - ...mappymatch.maps.nx.readers.osm_readers.rst | 20 - .../mappymatch.maps.nx.readers.rst | 13 - docs/_autosummary/mappymatch.maps.nx.rst | 14 - docs/_autosummary/mappymatch.maps.rst | 15 - .../mappymatch.matchers.lcss.constructs.rst | 13 - .../mappymatch.matchers.lcss.lcss.rst | 12 - .../mappymatch.matchers.lcss.ops.rst | 23 - .../_autosummary/mappymatch.matchers.lcss.rst | 16 - .../mappymatch.matchers.lcss.utils.rst | 15 - .../mappymatch.matchers.line_snap.rst | 12 - .../mappymatch.matchers.match_result.rst | 12 - .../mappymatch.matchers.matcher_interface.rst | 12 - .../_autosummary/mappymatch.matchers.osrm.rst | 18 - docs/_autosummary/mappymatch.matchers.rst | 18 - .../mappymatch.matchers.valhalla.rst | 19 - docs/_autosummary/mappymatch.utils.crs.rst | 6 - .../mappymatch.utils.exceptions.rst | 12 - docs/_autosummary/mappymatch.utils.geo.rst | 14 - docs/_autosummary/mappymatch.utils.plot.rst | 17 - .../mappymatch.utils.process_trace.rst | 13 - docs/_autosummary/mappymatch.utils.rst | 18 - docs/_autosummary/mappymatch.utils.url.rst | 12 - docs/_toc.yml | 2 +- .../_convert_examples_to_notebooks.py | 76 + docs/examples/lcss_example.py | 216 + docs/lcss-example.ipynb | 158207 --------------- docs/quick-start.md | 2 +- mappymatch/maps/nx/nx_map.py | 3 +- mappymatch/maps/nx/readers/osm_readers.py | 6 +- pixi.lock | 2 +- pyproject.toml | 5 +- 44 files changed, 317 insertions(+), 158668 deletions(-) delete mode 100644 docs/_autosummary/mappymatch.constructs.coordinate.rst delete mode 100644 docs/_autosummary/mappymatch.constructs.geofence.rst delete mode 100644 docs/_autosummary/mappymatch.constructs.match.rst delete mode 100644 docs/_autosummary/mappymatch.constructs.road.rst delete mode 100644 docs/_autosummary/mappymatch.constructs.rst delete mode 100644 docs/_autosummary/mappymatch.constructs.trace.rst delete mode 100644 docs/_autosummary/mappymatch.maps.igraph.igraph_map.rst delete mode 100644 docs/_autosummary/mappymatch.maps.igraph.rst delete mode 100644 docs/_autosummary/mappymatch.maps.map_interface.rst delete mode 100644 docs/_autosummary/mappymatch.maps.nx.nx_map.rst delete mode 100644 docs/_autosummary/mappymatch.maps.nx.readers.osm_readers.rst delete mode 100644 docs/_autosummary/mappymatch.maps.nx.readers.rst delete mode 100644 docs/_autosummary/mappymatch.maps.nx.rst delete mode 100644 docs/_autosummary/mappymatch.maps.rst delete mode 100644 docs/_autosummary/mappymatch.matchers.lcss.constructs.rst delete mode 100644 docs/_autosummary/mappymatch.matchers.lcss.lcss.rst delete mode 100644 docs/_autosummary/mappymatch.matchers.lcss.ops.rst delete mode 100644 docs/_autosummary/mappymatch.matchers.lcss.rst delete mode 100644 docs/_autosummary/mappymatch.matchers.lcss.utils.rst delete mode 100644 docs/_autosummary/mappymatch.matchers.line_snap.rst delete mode 100644 docs/_autosummary/mappymatch.matchers.match_result.rst delete mode 100644 docs/_autosummary/mappymatch.matchers.matcher_interface.rst delete mode 100644 docs/_autosummary/mappymatch.matchers.osrm.rst delete mode 100644 docs/_autosummary/mappymatch.matchers.rst delete mode 100644 docs/_autosummary/mappymatch.matchers.valhalla.rst delete mode 100644 docs/_autosummary/mappymatch.utils.crs.rst delete mode 100644 docs/_autosummary/mappymatch.utils.exceptions.rst delete mode 100644 docs/_autosummary/mappymatch.utils.geo.rst delete mode 100644 docs/_autosummary/mappymatch.utils.plot.rst delete mode 100644 docs/_autosummary/mappymatch.utils.process_trace.rst delete mode 100644 docs/_autosummary/mappymatch.utils.rst delete mode 100644 docs/_autosummary/mappymatch.utils.url.rst create mode 100644 docs/examples/_convert_examples_to_notebooks.py create mode 100644 docs/examples/lcss_example.py delete mode 100644 docs/lcss-example.ipynb diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml index 043bfe2..81fbe61 100644 --- a/.github/workflows/deploy-docs.yaml +++ b/.github/workflows/deploy-docs.yaml @@ -29,6 +29,10 @@ jobs: conda install -c conda-forge osmnx pip install ".[dev]" + - name: Convert examples to notebooks + run: | + python docs/examples/_convert_examples_to_notebooks.py + - name: Build book run: | jupyter-book build docs/ diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml index bbfcade..6107f31 100644 --- a/.github/workflows/lint-test.yml +++ b/.github/workflows/lint-test.yml @@ -18,15 +18,23 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip pip install .[tests] + - name: Run ruff linting run: ruff check . + - name: Run ruff formatting run: ruff format --check + - name: Run mypy run: mypy . + - name: Run Tests run: pytest tests + + - name: Run Examples + run: python docs/examples/lcss_example.py diff --git a/.gitignore b/.gitignore index 360a6bf..6ef6b8f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ .DS_Store +*.ipynb + .idea/ cache/ .vscode/ @@ -80,6 +82,7 @@ instance/ # Sphinx documentation docs/_build/ +docs/_autosummary/ # PyBuilder .pybuilder/ diff --git a/docs/_autosummary/mappymatch.constructs.coordinate.rst b/docs/_autosummary/mappymatch.constructs.coordinate.rst deleted file mode 100644 index e5ffeed..0000000 --- a/docs/_autosummary/mappymatch.constructs.coordinate.rst +++ /dev/null @@ -1,12 +0,0 @@ -mappymatch.constructs.coordinate -================================ - -.. automodule:: mappymatch.constructs.coordinate - - - .. rubric:: Classes - - .. autosummary:: - - Coordinate - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.constructs.geofence.rst b/docs/_autosummary/mappymatch.constructs.geofence.rst deleted file mode 100644 index 71f2a8a..0000000 --- a/docs/_autosummary/mappymatch.constructs.geofence.rst +++ /dev/null @@ -1,12 +0,0 @@ -mappymatch.constructs.geofence -============================== - -.. automodule:: mappymatch.constructs.geofence - - - .. rubric:: Classes - - .. autosummary:: - - Geofence - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.constructs.match.rst b/docs/_autosummary/mappymatch.constructs.match.rst deleted file mode 100644 index 578163d..0000000 --- a/docs/_autosummary/mappymatch.constructs.match.rst +++ /dev/null @@ -1,12 +0,0 @@ -mappymatch.constructs.match -=========================== - -.. automodule:: mappymatch.constructs.match - - - .. rubric:: Classes - - .. autosummary:: - - Match - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.constructs.road.rst b/docs/_autosummary/mappymatch.constructs.road.rst deleted file mode 100644 index ffc1cf0..0000000 --- a/docs/_autosummary/mappymatch.constructs.road.rst +++ /dev/null @@ -1,13 +0,0 @@ -mappymatch.constructs.road -========================== - -.. automodule:: mappymatch.constructs.road - - - .. rubric:: Classes - - .. autosummary:: - - Road - RoadId - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.constructs.rst b/docs/_autosummary/mappymatch.constructs.rst deleted file mode 100644 index 70cb562..0000000 --- a/docs/_autosummary/mappymatch.constructs.rst +++ /dev/null @@ -1,17 +0,0 @@ -mappymatch.constructs -===================== - -.. automodule:: mappymatch.constructs - - -.. rubric:: Modules - -.. autosummary:: - :toctree: - :recursive: - - coordinate - geofence - match - road - trace diff --git a/docs/_autosummary/mappymatch.constructs.trace.rst b/docs/_autosummary/mappymatch.constructs.trace.rst deleted file mode 100644 index 4aba5d7..0000000 --- a/docs/_autosummary/mappymatch.constructs.trace.rst +++ /dev/null @@ -1,12 +0,0 @@ -mappymatch.constructs.trace -=========================== - -.. automodule:: mappymatch.constructs.trace - - - .. rubric:: Classes - - .. autosummary:: - - Trace - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.maps.igraph.igraph_map.rst b/docs/_autosummary/mappymatch.maps.igraph.igraph_map.rst deleted file mode 100644 index b3f81ec..0000000 --- a/docs/_autosummary/mappymatch.maps.igraph.igraph_map.rst +++ /dev/null @@ -1,12 +0,0 @@ -mappymatch.maps.igraph.igraph\_map -================================== - -.. automodule:: mappymatch.maps.igraph.igraph_map - - - .. rubric:: Classes - - .. autosummary:: - - IGraphMap - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.maps.igraph.rst b/docs/_autosummary/mappymatch.maps.igraph.rst deleted file mode 100644 index 0eb47e0..0000000 --- a/docs/_autosummary/mappymatch.maps.igraph.rst +++ /dev/null @@ -1,13 +0,0 @@ -mappymatch.maps.igraph -====================== - -.. automodule:: mappymatch.maps.igraph - - -.. rubric:: Modules - -.. autosummary:: - :toctree: - :recursive: - - igraph_map diff --git a/docs/_autosummary/mappymatch.maps.map_interface.rst b/docs/_autosummary/mappymatch.maps.map_interface.rst deleted file mode 100644 index 28b4ee1..0000000 --- a/docs/_autosummary/mappymatch.maps.map_interface.rst +++ /dev/null @@ -1,12 +0,0 @@ -mappymatch.maps.map\_interface -============================== - -.. automodule:: mappymatch.maps.map_interface - - - .. rubric:: Classes - - .. autosummary:: - - MapInterface - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.maps.nx.nx_map.rst b/docs/_autosummary/mappymatch.maps.nx.nx_map.rst deleted file mode 100644 index 1ea5049..0000000 --- a/docs/_autosummary/mappymatch.maps.nx.nx_map.rst +++ /dev/null @@ -1,12 +0,0 @@ -mappymatch.maps.nx.nx\_map -========================== - -.. automodule:: mappymatch.maps.nx.nx_map - - - .. rubric:: Classes - - .. autosummary:: - - NxMap - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.maps.nx.readers.osm_readers.rst b/docs/_autosummary/mappymatch.maps.nx.readers.osm_readers.rst deleted file mode 100644 index bdbf13b..0000000 --- a/docs/_autosummary/mappymatch.maps.nx.readers.osm_readers.rst +++ /dev/null @@ -1,20 +0,0 @@ -mappymatch.maps.nx.readers.osm\_readers -======================================= - -.. automodule:: mappymatch.maps.nx.readers.osm_readers - - - .. rubric:: Functions - - .. autosummary:: - - compress - nx_graph_from_osmnx - parse_osmnx_graph - - .. rubric:: Classes - - .. autosummary:: - - NetworkType - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.maps.nx.readers.rst b/docs/_autosummary/mappymatch.maps.nx.readers.rst deleted file mode 100644 index e40e0bb..0000000 --- a/docs/_autosummary/mappymatch.maps.nx.readers.rst +++ /dev/null @@ -1,13 +0,0 @@ -mappymatch.maps.nx.readers -========================== - -.. automodule:: mappymatch.maps.nx.readers - - -.. rubric:: Modules - -.. autosummary:: - :toctree: - :recursive: - - osm_readers diff --git a/docs/_autosummary/mappymatch.maps.nx.rst b/docs/_autosummary/mappymatch.maps.nx.rst deleted file mode 100644 index d5f45ee..0000000 --- a/docs/_autosummary/mappymatch.maps.nx.rst +++ /dev/null @@ -1,14 +0,0 @@ -mappymatch.maps.nx -================== - -.. automodule:: mappymatch.maps.nx - - -.. rubric:: Modules - -.. autosummary:: - :toctree: - :recursive: - - nx_map - readers diff --git a/docs/_autosummary/mappymatch.maps.rst b/docs/_autosummary/mappymatch.maps.rst deleted file mode 100644 index f9a0dd5..0000000 --- a/docs/_autosummary/mappymatch.maps.rst +++ /dev/null @@ -1,15 +0,0 @@ -mappymatch.maps -=============== - -.. automodule:: mappymatch.maps - - -.. rubric:: Modules - -.. autosummary:: - :toctree: - :recursive: - - igraph - map_interface - nx diff --git a/docs/_autosummary/mappymatch.matchers.lcss.constructs.rst b/docs/_autosummary/mappymatch.matchers.lcss.constructs.rst deleted file mode 100644 index 4d9268f..0000000 --- a/docs/_autosummary/mappymatch.matchers.lcss.constructs.rst +++ /dev/null @@ -1,13 +0,0 @@ -mappymatch.matchers.lcss.constructs -=================================== - -.. automodule:: mappymatch.matchers.lcss.constructs - - - .. rubric:: Classes - - .. autosummary:: - - CuttingPoint - TrajectorySegment - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.matchers.lcss.lcss.rst b/docs/_autosummary/mappymatch.matchers.lcss.lcss.rst deleted file mode 100644 index 1b53867..0000000 --- a/docs/_autosummary/mappymatch.matchers.lcss.lcss.rst +++ /dev/null @@ -1,12 +0,0 @@ -mappymatch.matchers.lcss.lcss -============================= - -.. automodule:: mappymatch.matchers.lcss.lcss - - - .. rubric:: Classes - - .. autosummary:: - - LCSSMatcher - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.matchers.lcss.ops.rst b/docs/_autosummary/mappymatch.matchers.lcss.ops.rst deleted file mode 100644 index 60f6596..0000000 --- a/docs/_autosummary/mappymatch.matchers.lcss.ops.rst +++ /dev/null @@ -1,23 +0,0 @@ -mappymatch.matchers.lcss.ops -============================ - -.. automodule:: mappymatch.matchers.lcss.ops - - - .. rubric:: Functions - - .. autosummary:: - - add_matches_for_stationary_points - drop_stationary_points - find_stationary_points - new_path - same_trajectory_scheme - split_trajectory_segment - - .. rubric:: Classes - - .. autosummary:: - - StationaryIndex - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.matchers.lcss.rst b/docs/_autosummary/mappymatch.matchers.lcss.rst deleted file mode 100644 index e8d106f..0000000 --- a/docs/_autosummary/mappymatch.matchers.lcss.rst +++ /dev/null @@ -1,16 +0,0 @@ -mappymatch.matchers.lcss -======================== - -.. automodule:: mappymatch.matchers.lcss - - -.. rubric:: Modules - -.. autosummary:: - :toctree: - :recursive: - - constructs - lcss - ops - utils diff --git a/docs/_autosummary/mappymatch.matchers.lcss.utils.rst b/docs/_autosummary/mappymatch.matchers.lcss.utils.rst deleted file mode 100644 index cac3410..0000000 --- a/docs/_autosummary/mappymatch.matchers.lcss.utils.rst +++ /dev/null @@ -1,15 +0,0 @@ -mappymatch.matchers.lcss.utils -============================== - -.. automodule:: mappymatch.matchers.lcss.utils - - - .. rubric:: Functions - - .. autosummary:: - - compress - forward_merge - merge - reverse_merge - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.matchers.line_snap.rst b/docs/_autosummary/mappymatch.matchers.line_snap.rst deleted file mode 100644 index 8a1d37e..0000000 --- a/docs/_autosummary/mappymatch.matchers.line_snap.rst +++ /dev/null @@ -1,12 +0,0 @@ -mappymatch.matchers.line\_snap -============================== - -.. automodule:: mappymatch.matchers.line_snap - - - .. rubric:: Classes - - .. autosummary:: - - LineSnapMatcher - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.matchers.match_result.rst b/docs/_autosummary/mappymatch.matchers.match_result.rst deleted file mode 100644 index e9831c9..0000000 --- a/docs/_autosummary/mappymatch.matchers.match_result.rst +++ /dev/null @@ -1,12 +0,0 @@ -mappymatch.matchers.match\_result -================================= - -.. automodule:: mappymatch.matchers.match_result - - - .. rubric:: Classes - - .. autosummary:: - - MatchResult - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.matchers.matcher_interface.rst b/docs/_autosummary/mappymatch.matchers.matcher_interface.rst deleted file mode 100644 index 502963b..0000000 --- a/docs/_autosummary/mappymatch.matchers.matcher_interface.rst +++ /dev/null @@ -1,12 +0,0 @@ -mappymatch.matchers.matcher\_interface -====================================== - -.. automodule:: mappymatch.matchers.matcher_interface - - - .. rubric:: Classes - - .. autosummary:: - - MatcherInterface - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.matchers.osrm.rst b/docs/_autosummary/mappymatch.matchers.osrm.rst deleted file mode 100644 index 9463901..0000000 --- a/docs/_autosummary/mappymatch.matchers.osrm.rst +++ /dev/null @@ -1,18 +0,0 @@ -mappymatch.matchers.osrm -======================== - -.. automodule:: mappymatch.matchers.osrm - - - .. rubric:: Functions - - .. autosummary:: - - parse_osrm_json - - .. rubric:: Classes - - .. autosummary:: - - OsrmMatcher - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.matchers.rst b/docs/_autosummary/mappymatch.matchers.rst deleted file mode 100644 index f842aa3..0000000 --- a/docs/_autosummary/mappymatch.matchers.rst +++ /dev/null @@ -1,18 +0,0 @@ -mappymatch.matchers -=================== - -.. automodule:: mappymatch.matchers - - -.. rubric:: Modules - -.. autosummary:: - :toctree: - :recursive: - - lcss - line_snap - match_result - matcher_interface - osrm - valhalla diff --git a/docs/_autosummary/mappymatch.matchers.valhalla.rst b/docs/_autosummary/mappymatch.matchers.valhalla.rst deleted file mode 100644 index 39edf42..0000000 --- a/docs/_autosummary/mappymatch.matchers.valhalla.rst +++ /dev/null @@ -1,19 +0,0 @@ -mappymatch.matchers.valhalla -============================ - -.. automodule:: mappymatch.matchers.valhalla - - - .. rubric:: Functions - - .. autosummary:: - - build_match_result - build_path_from_result - - .. rubric:: Classes - - .. autosummary:: - - ValhallaMatcher - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.utils.crs.rst b/docs/_autosummary/mappymatch.utils.crs.rst deleted file mode 100644 index e6126d8..0000000 --- a/docs/_autosummary/mappymatch.utils.crs.rst +++ /dev/null @@ -1,6 +0,0 @@ -mappymatch.utils.crs -==================== - -.. automodule:: mappymatch.utils.crs - - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.utils.exceptions.rst b/docs/_autosummary/mappymatch.utils.exceptions.rst deleted file mode 100644 index a1996b5..0000000 --- a/docs/_autosummary/mappymatch.utils.exceptions.rst +++ /dev/null @@ -1,12 +0,0 @@ -mappymatch.utils.exceptions -=========================== - -.. automodule:: mappymatch.utils.exceptions - - - .. rubric:: Exceptions - - .. autosummary:: - - MapException - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.utils.geo.rst b/docs/_autosummary/mappymatch.utils.geo.rst deleted file mode 100644 index 2016df4..0000000 --- a/docs/_autosummary/mappymatch.utils.geo.rst +++ /dev/null @@ -1,14 +0,0 @@ -mappymatch.utils.geo -==================== - -.. automodule:: mappymatch.utils.geo - - - .. rubric:: Functions - - .. autosummary:: - - coord_to_coord_dist - latlon_to_xy - xy_to_latlon - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.utils.plot.rst b/docs/_autosummary/mappymatch.utils.plot.rst deleted file mode 100644 index 95536b0..0000000 --- a/docs/_autosummary/mappymatch.utils.plot.rst +++ /dev/null @@ -1,17 +0,0 @@ -mappymatch.utils.plot -===================== - -.. automodule:: mappymatch.utils.plot - - - .. rubric:: Functions - - .. autosummary:: - - plot_geofence - plot_map - plot_match_distances - plot_matches - plot_path - plot_trace - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.utils.process_trace.rst b/docs/_autosummary/mappymatch.utils.process_trace.rst deleted file mode 100644 index 21c0d84..0000000 --- a/docs/_autosummary/mappymatch.utils.process_trace.rst +++ /dev/null @@ -1,13 +0,0 @@ -mappymatch.utils.process\_trace -=============================== - -.. automodule:: mappymatch.utils.process_trace - - - .. rubric:: Functions - - .. autosummary:: - - remove_bad_start_from_trace - split_large_trace - \ No newline at end of file diff --git a/docs/_autosummary/mappymatch.utils.rst b/docs/_autosummary/mappymatch.utils.rst deleted file mode 100644 index 614dedc..0000000 --- a/docs/_autosummary/mappymatch.utils.rst +++ /dev/null @@ -1,18 +0,0 @@ -mappymatch.utils -================ - -.. automodule:: mappymatch.utils - - -.. rubric:: Modules - -.. autosummary:: - :toctree: - :recursive: - - crs - exceptions - geo - plot - process_trace - url diff --git a/docs/_autosummary/mappymatch.utils.url.rst b/docs/_autosummary/mappymatch.utils.url.rst deleted file mode 100644 index b927a28..0000000 --- a/docs/_autosummary/mappymatch.utils.url.rst +++ /dev/null @@ -1,12 +0,0 @@ -mappymatch.utils.url -==================== - -.. automodule:: mappymatch.utils.url - - - .. rubric:: Functions - - .. autosummary:: - - multiurljoin - \ No newline at end of file diff --git a/docs/_toc.yml b/docs/_toc.yml index 223a3db..895f519 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -14,7 +14,7 @@ parts: - caption: Example chapters: - - file: lcss-example + - file: examples/lcss_example - caption: Reference chapters: diff --git a/docs/examples/_convert_examples_to_notebooks.py b/docs/examples/_convert_examples_to_notebooks.py new file mode 100644 index 0000000..e217f96 --- /dev/null +++ b/docs/examples/_convert_examples_to_notebooks.py @@ -0,0 +1,76 @@ +from pathlib import Path + +import nbformat + + +def script_to_notebook(script_path: Path, notebook_path: Path) -> None: + # Read the script + with open(script_path, "r") as script_file: + lines = script_file.readlines() + + notebook = nbformat.v4.new_notebook() + current_code_block: list[str] = [] + current_markdown_block: list[str] = [] + + def add_code_cell(block: list[str]) -> None: + if block: + notebook.cells.append(nbformat.v4.new_code_cell("".join(block))) + + def add_markdown_cell(block: list[str]) -> None: + if block: + notebook.cells.append(nbformat.v4.new_markdown_cell("".join(block).strip())) + + # markdown cells will be enclosed in triple quotes + # the remaining cells will be code cells + in_markdown = False + in_main = False + for line in lines: + # strip any ipython code blocks + if line.strip().startswith("# %%"): + continue + # strip def main(): line and track indentation state + if line.strip() == "def main():": + in_main = True + continue + # strip if __name__ == "__main__": and main() call + if line.strip() in ( + 'if __name__ == "__main__":', + "main()", + ): + continue + # dedent lines inside def main() + if in_main and not line.strip().startswith('"""'): + line = line[4:] if line.startswith(" ") else line + if line.strip().startswith('"""'): + # handle triple-quote toggle; dedent the """ line itself if inside main + if in_main and line.startswith(" "): + line = line[4:] + in_markdown = not in_markdown + if in_markdown: + add_code_cell(current_code_block) + current_code_block = [] + else: + add_markdown_cell(current_markdown_block) + current_markdown_block = [] + elif in_markdown: + # dedent markdown content inside def main() + if in_main and line.startswith(" "): + line = line[4:] + current_markdown_block.append(line) + else: + current_code_block.append(line) + + add_code_cell(current_code_block) + add_markdown_cell(current_markdown_block) + + with open(notebook_path, "w") as notebook_file: + nbformat.write(notebook, notebook_file) + + +if __name__ == "__main__": + here = Path(__file__).parent + examples = here.glob("*example.py") + for example in examples: + notebook = example.with_suffix(".ipynb") + script_to_notebook(example, notebook) + print(f"Converted {example} to {notebook}") diff --git a/docs/examples/lcss_example.py b/docs/examples/lcss_example.py new file mode 100644 index 0000000..1b351e1 --- /dev/null +++ b/docs/examples/lcss_example.py @@ -0,0 +1,216 @@ +""" +# LCSS Example + +An example of using the LCSSMatcher to match a gps trace to the Open Street Maps road network +""" + + +def main(): + from mappymatch import package_root + + """ + First, we load the trace from a file. + The mappymatch package has a few sample traces included that we can use for demonstration. + + Before we build the trace, though, let's take a look at the file to see how mappymatch expects the input data: + """ + + import pandas as pd + + df = pd.read_csv(package_root() / "resources/traces/sample_trace_3.csv") + df.head() + + """ + Notice that we expect the input data to be in the EPSG:4326 coordinate reference system. + If your input data is not in this format, you'll need to convert it prior to building a Trace object. + + In order to idenfiy which coordinate is which in a trace, mappymatch uses the dataframe index as the coordinate index and so in this case, we just have a simple range based index for each coordinate. + We could set a different index on the dataframe and mappymatch would use that to identify the coordinates. + + Now, let's load the trace from the same file: + """ + + from mappymatch.constructs.trace import Trace + + trace = Trace.from_csv( + package_root() / "resources/traces/sample_trace_3.csv", + lat_column="latitude", + lon_column="longitude", + xy=True, + ) + + """ + Notice here that we pass three optional arguments to the `from_csv` function. + By default, mappymatch expects the latitude and longitude columns to be named "latitude" and "longitude" but you can pass your own values if needed. + Also by default, mappymatch converts the trace into the web mercator coordinate reference system (EPSG:3857) by setting `xy=True`. + The LCSS matcher computes the cartesian distance between geometries and so a projected coordiante reference system is ideal. + In a future version of mappymatch we hope to support any projected coordiante system but right now we only support EPSG:3857. + + Okay, let's plot the trace to see what it looks like (mappymatch uses folium under the hood for plotting): + """ + + from mappymatch.utils.plot import plot_trace + + plot_trace(trace, point_color="black", line_color="yellow") + + """ + Next, we need to get a road map to match our Trace to. + One way to do this is to build a small geofence around the trace and then download a map that just fits around our trace: + """ + + from mappymatch.constructs.geofence import Geofence + + geofence = Geofence.from_trace(trace, padding=2e3) + + """ + Notice that we pass an optional argument to the constructor. + The padding defines how large around the trace we should build our geofence and is in the same units as the trace. + In our case, the trace has been projected to the web mercator CRS and so our units would be in approximate meters, 1e3 meters or 1 kilomter + + Now, let's plot both the trace and the geofence: + """ + + from mappymatch.utils.plot import plot_geofence + + plot_trace(trace, point_color="black", m=plot_geofence(geofence)) + + """ + At this point, we're ready to download a road network. + Mappymatch has a couple of ways to represent a road network: The `NxMap` and the `IGraphMap` which use `networkx` and `igraph`, respectively, under the hood to represent the road graph structure. + You might experiment with both to see if one is more performant or memory efficient in your use case. + + In this example we'll use the `NxMap`: + """ + + from mappymatch.maps.nx.nx_map import NxMap, NetworkType + + nx_map = NxMap.from_geofence( + geofence, + network_type=NetworkType.DRIVE, + ) + + """ + The `from_geofence` constructor uses the osmnx package under the hood to download a road network. + + Notice we pass the optional argument `network_type` which defaults to `NetworkType.DRIVE` but can be used to get a different network like `NetworkType.BIKE` or `NetworkType.WALK` + + Now, we can plot the map to make sure we have the network that we want to match to: + """ + + from mappymatch.utils.plot import plot_map + + plot_map(nx_map) + + """ + Now, we're ready to perform the actual map matching. + + In this example we'll use the `LCSSMatcher` which implements the algorithm described in this paper: + + [Zhu, Lei, Jacob R. Holden, and Jeffrey D. Gonder. + "Trajectory Segmentation Map-Matching Approach for Large-Scale, High-Resolution GPS Data." + Transportation Research Record: Journal of the Transportation Research Board 2645 (2017): 67-75.](https://doi.org/10.3141%2F2645-08) + + We won't go into detail here for how to tune the paramters but checkout the referenced paper for more details if you're interested. + The default parameters have been set based on internal testing on high resolution driving GPS traces. + """ + + from mappymatch.matchers.lcss.lcss import LCSSMatcher + + matcher = LCSSMatcher(nx_map) + + match_result = matcher.match_trace(trace) + + """ + Now that we have the results, let's plot them: + """ + + from mappymatch.utils.plot import plot_matches + + plot_matches(match_result.matches) + + match_result.path_to_geodataframe().plot() + + """ + The `plot_matches` function plots the roads that each point has been matched to and labels them with the road id. + + In some cases, if the trace is much sparser (for example if it was collected a lower resolution), you might want see the estimated path, rather than the explict matched roads. + + For example, let's reduce the trace frequency to every 30th point and re-match it: + """ + + reduced_trace = trace[0::30] + + plot_trace(reduced_trace, point_color="black", line_color="yellow") + + reduced_matches = matcher.match_trace(reduced_trace) + + plot_matches(reduced_matches.matches) + + """ + The match result also has a `path` attribute with the estiamted path through the network: + """ + + from mappymatch.utils.plot import plot_path + + plot_trace( + reduced_trace, + point_color="blue", + m=plot_path(reduced_matches.path, crs=trace.crs), + ) + + """ + Lastly, we might want to convert the results into a format more suitible for saving to file or merging with some other dataset. + To do this, we can convert the result into a dataframe: + """ + + result_df = reduced_matches.matches_to_dataframe() + result_df.head() + + """ + Here, for each coordinate, we have the distance to the matched road, and then attributes of the road itself like the geometry, the OSM node id and the road distance and travel time. + + We can also get a dataframe for the path: + """ + + path_df = reduced_matches.path_to_dataframe() + path_df.head() + + """ + Another thing we can do is to only get a certain set of road types to match to. For example, let's say I only want to consider highways and primary roads for matching, I can do so by passing a custom filter when building the road network: + """ + + nx_map = NxMap.from_geofence( + geofence, + network_type=NetworkType.DRIVE, + custom_filter='["highway"~"motorway|primary"]', + ) + + plot_map(nx_map) + + """ + Above you can see that now we have a much reduced graph to match to, let's see what happens + """ + + matcher = LCSSMatcher(nx_map) + + match_result = matcher.match_trace(trace) + + plot_matches(match_result.matches) + + """ + Plot the path + """ + + plot_path(match_result.path, crs=trace.crs) + + """ + Plot the geodataframe version of the path + """ + + path_gdf = match_result.path_to_geodataframe() + + path_gdf.plot() + + +if __name__ == "__main__": + main() diff --git a/docs/lcss-example.ipynb b/docs/lcss-example.ipynb deleted file mode 100644 index 815eb4e..0000000 --- a/docs/lcss-example.ipynb +++ /dev/null @@ -1,158207 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "320f93ac-5965-44a4-8d20-40b8944a60a5", - "metadata": {}, - "source": [ - "# LCSS Example\n", - "\n", - "An example of using the LCSSMatcher to match a gps trace to the Open Street Maps road network" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "id": "e8e17861-dfc0-4ae8-a509-2ae7fe6ff608", - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "!pip install mappymatch" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "dbf22ede-f3b6-4940-b866-15263b8eeaff", - "metadata": {}, - "outputs": [], - "source": [ - "from mappymatch import package_root" - ] - }, - { - "cell_type": "markdown", - "id": "fcbd7d7d-91fc-4bbe-849f-b91b4d919c6f", - "metadata": {}, - "source": [ - "First, we load the trace from a file. \n", - "The mappymatch package has a few sample traces included that we can use for demonstration.\n", - "\n", - "Before we build the trace, though, let's take a look at the file to see how mappymatch expects the input data:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "a626d87a-382e-4d04-9e02-886f3a168701", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
latitudelongitude
039.655210-104.919169
139.655449-104.919274
239.655690-104.919381
339.655936-104.919486
439.656182-104.919593
\n", - "
" - ], - "text/plain": [ - " latitude longitude\n", - "0 39.655210 -104.919169\n", - "1 39.655449 -104.919274\n", - "2 39.655690 -104.919381\n", - "3 39.655936 -104.919486\n", - "4 39.656182 -104.919593" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import pandas as pd\n", - "\n", - "df = pd.read_csv(package_root() / \"resources/traces/sample_trace_3.csv\")\n", - "df.head()" - ] - }, - { - "cell_type": "markdown", - "id": "cd2ba449-3b1b-44ea-be6a-a11a12cca1a7", - "metadata": {}, - "source": [ - "Notice that we expect the input data to be in the EPSG:4326 coordinate reference system. \n", - "If your input data is not in this format, you'll need to convert it prior to building a Trace object.\n", - "\n", - "In order to idenfiy which coordinate is which in a trace, mappymatch uses the dataframe index as the coordinate index and so in this case, we just have a simple range based index for each coordinate.\n", - "We could set a different index on the dataframe and mappymatch would use that to identify the coordinates.\n", - "\n", - "Now, let's load the trace from the same file:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "278d868d-1095-427b-a6ef-55db76f9353c", - "metadata": {}, - "outputs": [], - "source": [ - "from mappymatch.constructs.trace import Trace\n", - "\n", - "trace = Trace.from_csv(\n", - " package_root() / \"resources/traces/sample_trace_3.csv\",\n", - " lat_column=\"latitude\",\n", - " lon_column=\"longitude\",\n", - " xy=True,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "d5791707-491f-4c16-b5a9-51147a6ec896", - "metadata": {}, - "source": [ - "Notice here that we pass three optional arguments to the `from_csv` function. \n", - "By default, mappymatch expects the latitude and longitude columns to be named \"latitude\" and \"longitude\" but you can pass your own values if needed.\n", - "Also by default, mappymatch converts the trace into the web mercator coordinate reference system (EPSG:3857) by setting `xy=True`.\n", - "The LCSS matcher computes the cartesian distance between geometries and so a projected coordiante reference system is ideal.\n", - "In a future version of mappymatch we hope to support any projected coordiante system but right now we only support EPSG:3857.\n", - "\n", - "Okay, let's plot the trace to see what it looks like (mappymatch uses folium under the hood for plotting):" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "4e1284f1-ef05-4732-9b20-891c444d5c68", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from mappymatch.utils.plot import plot_trace\n", - "\n", - "plot_trace(trace, point_color=\"black\", line_color=\"yellow\")" - ] - }, - { - "cell_type": "markdown", - "id": "a54fd4fa-974b-4e96-8224-b90e31ffdd37", - "metadata": {}, - "source": [ - "Next, we need to get a road map to match our Trace to.\n", - "One way to do this is to build a small geofence around the trace and then download a map that just fits around our trace:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "cc5f7cb1-8f01-45b3-8216-59d7fd7f1f22", - "metadata": {}, - "outputs": [], - "source": [ - "from mappymatch.constructs.geofence import Geofence\n", - "\n", - "geofence = Geofence.from_trace(trace, padding=2e3)" - ] - }, - { - "cell_type": "markdown", - "id": "c581bf51-878b-4968-991c-de77becea543", - "metadata": {}, - "source": [ - "Notice that we pass an optional argument to the constructor.\n", - "The padding defines how large around the trace we should build our geofence and is in the same units as the trace.\n", - "In our case, the trace has been projected to the web mercator CRS and so our units would be in approximate meters, 1e3 meters or 1 kilomter \n", - "\n", - "Now, let's plot both the trace and the geofence:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "0e8008c3-01ec-42be-b62e-730cfc35e995", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from mappymatch.utils.plot import plot_geofence\n", - "\n", - "plot_trace(trace, point_color=\"black\", m=plot_geofence(geofence))" - ] - }, - { - "cell_type": "markdown", - "id": "c294fc6c-4ad3-4f2a-a819-5772c887a65e", - "metadata": {}, - "source": [ - "At this point, we're ready to download a road network.\n", - "Mappymatch has a couple of ways to represent a road network: The `NxMap` and the `IGraphMap` which use `networkx` and `igraph`, respectively, under the hood to represent the road graph structure.\n", - "You might experiment with both to see if one is more performant or memory efficient in your use case.\n", - "\n", - "In this example we'll use the `NxMap`:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "1259d790-758b-413c-8ab1-19106ee5b3c8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'osmid': 16983030, 'highway': 'residential', 'name': 'East Asbury Avenue', 'oneway': False, 'reversed': True, 'length': 79.54373679802445, 'speed_kph': 39.960309567901234, 'travel_time': 7.166046899269001, 'kilometers': 0.07954373679802446}\n", - "{'osmid': 16983030, 'highway': 'residential', 'name': 'East Asbury Avenue', 'oneway': False, 'reversed': False, 'length': 79.54373679802445, 'speed_kph': 39.960309567901234, 'travel_time': 7.166046899269001, 'kilometers': 0.07954373679802446}\n", - "{'osmid': 16986821, 'highway': 'residential', 'name': 'East Vassar Avenue', 'oneway': False, 'reversed': True, 'length': 83.64761710826589, 'speed_kph': 39.960309567901234, 'travel_time': 7.535762981967634, 'kilometers': 0.08364761710826589}\n", - "{'osmid': 16986821, 'highway': 'residential', 'name': 'East Vassar Avenue', 'oneway': False, 'reversed': False, 'length': 83.64761710826589, 'speed_kph': 39.960309567901234, 'travel_time': 7.535762981967634, 'kilometers': 0.08364761710826589}\n", - "{'osmid': 234196398, 'highway': 'secondary', 'lanes': '5', 'maxspeed': '30 mph', 'name': 'East Yale Avenue', 'oneway': False, 'reversed': True, 'length': 20.542709041809864, 'speed_kph': 48.2802, 'travel_time': 1.531761520261215, 'kilometers': 0.020542709041809864}\n", - "{'osmid': 37886699, 'highway': 'secondary', 'lanes': '6', 'maxspeed': '30 mph', 'name': 'East Yale Avenue', 'oneway': False, 'reversed': False, 'length': 14.354081263098669, 'speed_kph': 48.2802, 'travel_time': 1.0703081707854403, 'kilometers': 0.014354081263098669}\n", - "{'osmid': 16984718, 'highway': 'residential', 'name': 'South Kirkwood Court', 'oneway': False, 'reversed': True, 'length': 32.08319603301365, 'speed_kph': 39.960309567901234, 'travel_time': 2.8903556295676442, 'kilometers': 0.03208319603301365}\n", - "{'osmid': 33105941, 'highway': 'residential', 'name': 'East Kentucky Avenue', 'oneway': False, 'reversed': False, 'length': 41.12338570051541, 'speed_kph': 39.960309567901234, 'travel_time': 3.704780821837636, 'kilometers': 0.041123385700515415}\n", - "{'osmid': 33105941, 'highway': 'residential', 'name': 'East Kentucky Avenue', 'oneway': False, 'reversed': True, 'length': 41.12338570051541, 'speed_kph': 39.960309567901234, 'travel_time': 3.704780821837636, 'kilometers': 0.041123385700515415}\n", - "{'osmid': 1176411245, 'highway': 'residential', 'maxspeed': '25 mph', 'name': 'South Pennsylvania Street', 'oneway': False, 'reversed': False, 'length': 207.47072269064134, 'speed_kph': 40.2335, 'travel_time': 18.563997705551564, 'kilometers': 0.20747072269064135}\n", - "Warning: found 1666 of 10600 links with no geometry; creating link geometries from the node endpoints\n" - ] - } - ], - "source": [ - "from mappymatch.maps.nx.nx_map import NxMap, NetworkType\n", - "\n", - "nx_map = NxMap.from_geofence(\n", - " geofence,\n", - " network_type=NetworkType.DRIVE,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "7388a8a5-4396-4c95-bc23-aeb8c057d57a", - "metadata": {}, - "source": [ - "The `from_geofence` constructor uses the osmnx package under the hood to download a road network.\n", - "\n", - "Notice we pass the optional argument `network_type` which defaults to `NetworkType.DRIVE` but can be used to get a different network like `NetworkType.BIKE` or `NetworkType.WALK`\n", - "\n", - "Now, we can plot the map to make sure we have the network that we want to match to:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "6dd7f2f8-9407-418c-912d-723cf7672624", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from mappymatch.utils.plot import plot_map\n", - "\n", - "plot_map(nx_map)" - ] - }, - { - "cell_type": "markdown", - "id": "3e3e4926-7e63-4098-986a-605aa772a9d8", - "metadata": {}, - "source": [ - "Now, we're ready to perform the actual map matching. \n", - "\n", - "In this example we'll use the `LCSSMatcher` which implements the algorithm described in this paper:\n", - "\n", - "[Zhu, Lei, Jacob R. Holden, and Jeffrey D. Gonder.\n", - "\"Trajectory Segmentation Map-Matching Approach for Large-Scale, High-Resolution GPS Data.\"\n", - "Transportation Research Record: Journal of the Transportation Research Board 2645 (2017): 67-75.](https://doi.org/10.3141%2F2645-08)\n", - "\n", - "We won't go into detail here for how to tune the paramters but checkout the referenced paper for more details if you're interested. \n", - "The default parameters have been set based on internal testing on high resolution driving GPS traces. " - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "272e5779-5b3b-4862-9d5e-850866758554", - "metadata": {}, - "outputs": [], - "source": [ - "from mappymatch.matchers.lcss.lcss import LCSSMatcher\n", - "\n", - "matcher = LCSSMatcher(nx_map)\n", - "\n", - "match_result = matcher.match_trace(trace)" - ] - }, - { - "cell_type": "markdown", - "id": "64ab014b-3084-48a0-a89a-d579ca296dc6", - "metadata": {}, - "source": [ - "Now that we have the results, let's plot them:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "5dcdab12-0592-472e-bd7d-b00add9d8533", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from mappymatch.utils.plot import plot_matches\n", - "\n", - "plot_matches(match_result.matches)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "1e470152-4b65-4844-a014-b922c5cbd8f8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "match_result.path_to_geodataframe().plot()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5f7e52f4-b52a-4788-a990-f603378ea090", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "id": "d9ac54eb-d5e0-45db-a90d-cf42906a3bc6", - "metadata": {}, - "source": [ - "The `plot_matches` function plots the roads that each point has been matched to and labels them with the road id.\n", - "\n", - "In some cases, if the trace is much sparser (for example if it was collected a lower resolution), you might want see the estimated path, rather than the explict matched roads.\n", - "\n", - "For example, let's reduce the trace frequency to every 30th point and re-match it:" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "70d10607-71f6-4706-878a-8ab7332f7543", - "metadata": {}, - "outputs": [], - "source": [ - "reduced_trace = trace[0::30]" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "3668f51e-da5e-485a-8d8b-e2f76fd1c173", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot_trace(reduced_trace, point_color=\"black\", line_color=\"yellow\")" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "b203d89a-3384-4fcb-81ff-dc62f34277d0", - "metadata": {}, - "outputs": [], - "source": [ - "reduced_matches = matcher.match_trace(reduced_trace)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "f56f70f8-7732-4801-af04-bffce4176853", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot_matches(reduced_matches.matches)" - ] - }, - { - "cell_type": "markdown", - "id": "a4d7ac4f-d822-491a-9a1d-77cd42f0a7d7", - "metadata": {}, - "source": [ - "The match result also has a `path` attribute with the estiamted path through the network:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "6c389db6-427f-4b91-8857-06d36ea51809", - "metadata": {}, - "outputs": [], - "source": [ - "from mappymatch.utils.plot import plot_path" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "a8e63658-1de5-4313-b5c3-989afa0c5951", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot_trace(\n", - " reduced_trace, point_color=\"blue\", m=plot_path(reduced_matches.path, crs=trace.crs)\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "fdc744b4-9399-4a64-a0f9-a02791aaf859", - "metadata": {}, - "source": [ - "Lastly, we might want to convert the results into a format more suitible for saving to file or merging with some other dataset. \n", - "To do this, we can convert the result into a dataframe:" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "456d58c4-d507-461d-be9d-75b80341a406", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
coordinate_iddistance_to_roadroad_idgeomorigin_junction_iddestination_junction_idroad_keykilometerstravel_time
0018.704781(432968976, 432968864, 0)LINESTRING (-11679421.184827643 4815736.167510...43296897643296886400.54836426.861128
1306.637295(432968864, 176066679, 0)LINESTRING (-11679700.006756231 4816391.715274...43296886417606667900.71316024.543028
2605.474720(176066679, 442605942, 0)LINESTRING (-11680004.732730329 4817267.513675...17606667944260594200.84967929.241278
3906.923850(442615878, 442618319, 0)LINESTRING (-11680833.539735131 4818830.551805...44261587844261831900.42425314.600453
41207.800845(442618319, 442617120, 0)LINESTRING (-11681140.002293285 4819289.434003...44261831944261712000.95748134.269262
\n", - "
" - ], - "text/plain": [ - " coordinate_id distance_to_road road_id \\\n", - "0 0 18.704781 (432968976, 432968864, 0) \n", - "1 30 6.637295 (432968864, 176066679, 0) \n", - "2 60 5.474720 (176066679, 442605942, 0) \n", - "3 90 6.923850 (442615878, 442618319, 0) \n", - "4 120 7.800845 (442618319, 442617120, 0) \n", - "\n", - " geom origin_junction_id \\\n", - "0 LINESTRING (-11679421.184827643 4815736.167510... 432968976 \n", - "1 LINESTRING (-11679700.006756231 4816391.715274... 432968864 \n", - "2 LINESTRING (-11680004.732730329 4817267.513675... 176066679 \n", - "3 LINESTRING (-11680833.539735131 4818830.551805... 442615878 \n", - "4 LINESTRING (-11681140.002293285 4819289.434003... 442618319 \n", - "\n", - " destination_junction_id road_key kilometers travel_time \n", - "0 432968864 0 0.548364 26.861128 \n", - "1 176066679 0 0.713160 24.543028 \n", - "2 442605942 0 0.849679 29.241278 \n", - "3 442618319 0 0.424253 14.600453 \n", - "4 442617120 0 0.957481 34.269262 " - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "result_df = reduced_matches.matches_to_dataframe()\n", - "result_df.head()" - ] - }, - { - "cell_type": "markdown", - "id": "de20d601-aab6-4cab-ac98-3b25b8b433e1", - "metadata": {}, - "source": [ - "Here, for each coordinate, we have the distance to the matched road, and then attributes of the road itself like the geometry, the OSM node id and the road distance and travel time.\n", - "\n", - "We can also get a dataframe for the path:" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "e4bf8cf9-83a7-4347-9c7c-164ff0fe4413", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
road_idgeomorigin_junction_iddestination_junction_idroad_keykilometerstravel_time
0(432968976, 432968864, 0)LINESTRING (-11679421.184827643 4815736.167510...43296897643296886400.54836426.861128
1(432968864, 176066679, 0)LINESTRING (-11679700.006756231 4816391.715274...43296886417606667900.71316024.543028
2(176066679, 442605942, 0)LINESTRING (-11680004.732730329 4817267.513675...17606667944260594200.84967929.241278
3(442605942, 442615878, 0)LINESTRING (-11680517.325589584 4818246.301911...44260594244261587800.51077917.578186
4(442615878, 442618319, 0)LINESTRING (-11680833.539735131 4818830.551805...44261587844261831900.42425314.600453
\n", - "
" - ], - "text/plain": [ - " road_id \\\n", - "0 (432968976, 432968864, 0) \n", - "1 (432968864, 176066679, 0) \n", - "2 (176066679, 442605942, 0) \n", - "3 (442605942, 442615878, 0) \n", - "4 (442615878, 442618319, 0) \n", - "\n", - " geom origin_junction_id \\\n", - "0 LINESTRING (-11679421.184827643 4815736.167510... 432968976 \n", - "1 LINESTRING (-11679700.006756231 4816391.715274... 432968864 \n", - "2 LINESTRING (-11680004.732730329 4817267.513675... 176066679 \n", - "3 LINESTRING (-11680517.325589584 4818246.301911... 442605942 \n", - "4 LINESTRING (-11680833.539735131 4818830.551805... 442615878 \n", - "\n", - " destination_junction_id road_key kilometers travel_time \n", - "0 432968864 0 0.548364 26.861128 \n", - "1 176066679 0 0.713160 24.543028 \n", - "2 442605942 0 0.849679 29.241278 \n", - "3 442615878 0 0.510779 17.578186 \n", - "4 442618319 0 0.424253 14.600453 " - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "path_df = reduced_matches.path_to_dataframe()\n", - "path_df.head()" - ] - }, - { - "cell_type": "markdown", - "id": "1e38cdb0-0028-4c23-88a0-e6fdafe295ce", - "metadata": {}, - "source": [ - "Another thing we can do is to only get a certain set of road types to match to. For example, let's say I only want to consider highways and primary roads for matching, I can do so by passing a custom filter when building the road network: " - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "57168d3b-c9d0-47ae-9a84-a405b31ddf73", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'osmid': 427708173, 'highway': 'motorway_link', 'lanes': '2', 'oneway': True, 'reversed': False, 'length': 20.414537964823523, 'speed_kph': 74.81098623188406, 'travel_time': 0.9823735840825292, 'kilometers': 0.020414537964823523}\n", - "{'osmid': 344391013, 'highway': 'motorway_link', 'lanes': '1', 'oneway': True, 'reversed': False, 'length': 22.563432845186533, 'speed_kph': 74.81098623188406, 'travel_time': 1.0857811443749208, 'kilometers': 0.022563432845186533}\n", - "{'osmid': 177242486, 'highway': 'motorway_link', 'lanes': '3', 'oneway': True, 'reversed': False, 'length': 20.12349405707779, 'speed_kph': 74.81098623188406, 'travel_time': 0.9683681803222176, 'kilometers': 0.02012349405707779}\n", - "{'osmid': 186241519, 'highway': 'primary', 'lanes': '4', 'maxspeed': '35 mph', 'name': 'South Colorado Boulevard', 'oneway': True, 'ref': 'CO 2', 'reversed': False, 'length': 12.226081069694452, 'speed_kph': 56.3269, 'travel_time': 0.7814009265714965, 'kilometers': 0.012226081069694451}\n", - "{'osmid': 132231509, 'highway': 'motorway', 'lanes': '4', 'maxspeed': '55 mph', 'name': 'West 6th Avenue Freeway', 'oneway': True, 'reversed': False, 'length': 58.72923421018891, 'speed_kph': 88.5137, 'travel_time': 2.388616035220312, 'kilometers': 0.05872923421018891}\n", - "{'osmid': 520031957, 'highway': 'motorway_link', 'lanes': '1', 'oneway': True, 'reversed': False, 'length': 197.08499535304279, 'speed_kph': 74.81098623188406, 'travel_time': 9.48398115046592, 'kilometers': 0.1970849953530428}\n", - "{'osmid': 16982076, 'highway': 'primary_link', 'lanes': '1', 'oneway': True, 'ref': 'US 287', 'reversed': False, 'length': 119.30913844814366, 'speed_kph': 64.3736, 'travel_time': 6.672190127836834, 'kilometers': 0.11930913844814366}\n", - "{'osmid': 646049993, 'highway': 'primary_link', 'lanes': '1', 'oneway': False, 'reversed': False, 'length': 14.763038968321924, 'speed_kph': 64.3736, 'travel_time': 0.8256014932512541, 'kilometers': 0.014763038968321924}\n", - "{'osmid': 645821551, 'highway': 'primary_link', 'lanes': '1', 'oneway': False, 'reversed': False, 'length': 12.546375003231507, 'speed_kph': 64.3736, 'travel_time': 0.7016377833713422, 'kilometers': 0.012546375003231507}\n", - "{'osmid': 646049993, 'highway': 'primary_link', 'lanes': '1', 'oneway': False, 'reversed': True, 'length': 14.763038968321924, 'speed_kph': 64.3736, 'travel_time': 0.8256014932512541, 'kilometers': 0.014763038968321924}\n", - "Warning: found 46 of 388 links with no geometry; creating link geometries from the node endpoints\n" - ] - } - ], - "source": [ - "nx_map = NxMap.from_geofence(\n", - " geofence,\n", - " network_type=NetworkType.DRIVE,\n", - " custom_filter='[\"highway\"~\"motorway|primary\"]',\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "faea5546-bda5-4642-9b75-26692efce16a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot_map(nx_map)" - ] - }, - { - "cell_type": "markdown", - "id": "04f39101-11cc-4cd5-92de-5b1f191de3b4", - "metadata": {}, - "source": [ - "Above you can see that now we have a much reduced graph to match to, let's see what happens" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "35436094-897b-4537-b161-73eadd294625", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "matcher = LCSSMatcher(nx_map)\n", - "\n", - "match_result = matcher.match_trace(trace)\n", - "\n", - "plot_matches(match_result.matches)" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "820813b6-dba1-4a1c-aa37-fcba0deb20f1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot_path(match_result.path, crs=trace.crs)" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "2137e503-0b00-4358-a1c8-be34ab6d10eb", - "metadata": {}, - "outputs": [], - "source": [ - "path_gdf = match_result.path_to_geodataframe()" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "a5b3a413-7325-4629-89ad-664aab4ece10", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "path_gdf.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2731f4d7-a9df-455f-859a-f22e48e40f37", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.7" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/quick-start.md b/docs/quick-start.md index a0fb92e..cfc7266 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -2,4 +2,4 @@ First, follow the [installation instructions](install). -Then, checkout the [LCSS example](lcss-example). +Then, checkout the [LCSS example](examples/lcss_example). diff --git a/mappymatch/maps/nx/nx_map.py b/mappymatch/maps/nx/nx_map.py index 9ea4867..a1844c6 100644 --- a/mappymatch/maps/nx/nx_map.py +++ b/mappymatch/maps/nx/nx_map.py @@ -220,8 +220,7 @@ def from_geofence( network_type: the network type to use for the graph custom_filter: a custom filter to pass to osmnx like '["highway"~"motorway|primary"]' additional_metadata_keys: additional keys to preserve in road metadata like '["maxspeed", "highway"]' - filter_to_largest_component: if True, keep only the largest strongly connected component; - if False, keep all components (may result in routing failures between disconnected components) + filter_to_largest_component: if True, keep only the largest strongly connected component; if False, keep all components (may result in routing failures between disconnected components) Returns: a NxMap diff --git a/mappymatch/maps/nx/readers/osm_readers.py b/mappymatch/maps/nx/readers/osm_readers.py index fe1fec7..66e47ea 100644 --- a/mappymatch/maps/nx/readers/osm_readers.py +++ b/mappymatch/maps/nx/readers/osm_readers.py @@ -49,8 +49,7 @@ def nx_graph_from_osmnx( xy: whether to use xy coordinates or lat/lon custom_filter: a custom filter to pass to osmnx additional_metadata_keys: additional keys to preserve in metadata - filter_to_largest_component: if True, keep only the largest strongly connected component; - if False, keep all components (may result in routing failures between disconnected components) + filter_to_largest_component: if True, keep only the largest strongly connected component; if False, keep all components (may result in routing failures between disconnected components) Returns: a networkx graph of the OSM network @@ -90,8 +89,7 @@ def parse_osmnx_graph( xy: whether to use xy coordinates or lat/lon network_type: the network type to use for the graph additional_metadata_keys: additional keys to preserve in metadata - filter_to_largest_component: if True, keep only the largest strongly connected component; - if False, keep all components (may result in routing failures between disconnected components) + filter_to_largest_component: if True, keep only the largest strongly connected component; if False, keep all components (may result in routing failures between disconnected components) Returns: a cleaned networkx graph of the OSM network diff --git a/pixi.lock b/pixi.lock index 99075c0..4d05e94 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1429,7 +1429,7 @@ packages: - pypi: ./ name: mappymatch version: 0.7.1 - sha256: f86ba7fd8c2b8da5839ce4eaae1b1c23b96092e310088896a50d5a135a4c5a44 + sha256: 72e671a048b98445eef7be2a372bb09a50aa5b84ca082a2d226c15403dd41121 requires_dist: - folium>=0.20,<1 - geopandas>=1,<2 diff --git a/pyproject.toml b/pyproject.toml index d5ee208..d1e6652 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,5 +124,8 @@ fmt_fix = "ruff format" lint_fix = "ruff check --fix" typing = "mypy ." test = "pytest tests/" -docs = "cd docs/ && jupyter-book build ." +convert_notebooks = "python docs/examples/_convert_examples_to_notebooks.py" +docs = { depends-on = [ + "convert_notebooks", +], cmd = "cd docs/ && jupyter-book build ." } check = { depends-on = ["fmt_fix", "lint_fix", "typing", "test"] } From 3ede1375556fb8940a01149ca84dd352d479c96a Mon Sep 17 00:00:00 2001 From: Nicholas Reinicke Date: Wed, 18 Feb 2026 15:10:25 -0700 Subject: [PATCH 3/4] update doc strings --- docs/_config.yml | 2 - mappymatch/constructs/coordinate.py | 68 ++++-- mappymatch/constructs/geofence.py | 86 ++++++-- mappymatch/constructs/match.py | 61 +++++- mappymatch/constructs/road.py | 35 +++- mappymatch/constructs/trace.py | 245 +++++++++++++++++----- mappymatch/maps/igraph/igraph_map.py | 23 +- mappymatch/maps/map_interface.py | 55 +++-- mappymatch/maps/nx/nx_map.py | 147 +++++++++++-- mappymatch/maps/nx/readers/osm_readers.py | 125 +++++++++-- mappymatch/matchers/lcss/constructs.py | 78 +++++-- mappymatch/matchers/lcss/lcss.py | 33 ++- mappymatch/matchers/lcss/ops.py | 100 ++++++--- mappymatch/matchers/lcss/utils.py | 71 ++++--- mappymatch/matchers/line_snap.py | 69 +++++- mappymatch/matchers/match_result.py | 74 ++++++- mappymatch/matchers/matcher_interface.py | 35 +++- mappymatch/matchers/osrm.py | 81 ++++++- mappymatch/matchers/valhalla.py | 106 +++++++++- mappymatch/utils/crs.py | 14 ++ mappymatch/utils/geo.py | 61 +++++- mappymatch/utils/keys.py | 11 + mappymatch/utils/process_trace.py | 60 +++++- mappymatch/utils/url.py | 21 +- 24 files changed, 1377 insertions(+), 284 deletions(-) diff --git a/docs/_config.yml b/docs/_config.yml index e363f77..e6656ad 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -33,7 +33,6 @@ sphinx: - "sphinx.ext.autodoc" - "sphinx.ext.autosummary" - "sphinx.ext.viewcode" - - "sphinx_autodoc_typehints" - "sphinxcontrib.autoyaml" - "sphinxcontrib.mermaid" config: @@ -55,5 +54,4 @@ sphinx: member-order: bysource undoc-members: true private-members: false - autodoc_typehints: both mermaid_version: "10.8" diff --git a/mappymatch/constructs/coordinate.py b/mappymatch/constructs/coordinate.py index 27aefe1..a9c823e 100644 --- a/mappymatch/constructs/coordinate.py +++ b/mappymatch/constructs/coordinate.py @@ -12,14 +12,30 @@ class Coordinate(NamedTuple): """ - Represents a single coordinate with a CRS and a geometry + Represents a single geographic coordinate point with a coordinate reference system (CRS). + + A Coordinate is an immutable object that combines a spatial point geometry with its + coordinate reference system, allowing for accurate coordinate transformations between + different projection systems. Attributes: - coordinate_id: The unique identifier for this coordinate - geom: The geometry of this coordinate - crs: The CRS of this coordinate - x: The x value of this coordinate - y: The y value of this coordinate + coordinate_id: The unique identifier for this coordinate (can be any hashable type) + geom: The Shapely Point geometry representing the spatial location + crs: The pyproj CRS (Coordinate Reference System) defining the coordinate space + x: The x-coordinate value (longitude in lat/lon systems, easting in projected systems) + y: The y-coordinate value (latitude in lat/lon systems, northing in projected systems) + + Examples: + >>> from mappymatch.constructs.coordinate import Coordinate + >>> # Create a coordinate from latitude and longitude + >>> coord = Coordinate.from_lat_lon(40.7128, -74.0060) + >>> print(coord.x, coord.y) + -74.0060 40.7128 + + >>> # Transform to a different CRS (Web Mercator) + >>> web_mercator = coord.to_crs('EPSG:3857') + >>> print(web_mercator.crs.to_epsg()) + 3857 """ coordinate_id: Any @@ -33,14 +49,23 @@ def __repr__(self): @classmethod def from_lat_lon(cls, lat: float, lon: float) -> Coordinate: """ - Build a coordinate from a latitude/longitude + Create a coordinate from latitude and longitude values in WGS84 (EPSG:4326). + + This is a convenience method for creating coordinates from standard GPS coordinates. + The resulting coordinate will use the LATLON_CRS (EPSG:4326) coordinate system. Args: - lat: The latitude - lon: The longitude + lat: The latitude in decimal degrees (range: -90 to 90) + lon: The longitude in decimal degrees (range: -180 to 180) Returns: - A new coordinate + A new Coordinate instance in EPSG:4326 CRS with no coordinate_id + + Examples: + >>> # New York City coordinates + >>> nyc = Coordinate.from_lat_lon(40.7128, -74.0060) + >>> print(f"Lat: {nyc.y}, Lon: {nyc.x}") + Lat: 40.7128, Lon: -74.0060 """ return cls(coordinate_id=None, geom=Point(lon, lat), crs=LATLON_CRS) @@ -54,16 +79,31 @@ def y(self) -> float: def to_crs(self, new_crs: Any) -> Coordinate: """ - Convert this coordinate to a new CRS + Transform this coordinate to a different coordinate reference system (CRS). + + This method reprojects the coordinate geometry from its current CRS to the target CRS + using pyproj transformations. If the target CRS is the same as the current CRS, + the original coordinate is returned unchanged. Args: - new_crs: The new CRS to convert to + new_crs: The target CRS. Can be a pyproj.CRS object, an EPSG code as a string + (e.g., 'EPSG:4326'), an integer EPSG code, or any CRS format that pyproj.CRS() accepts Returns: - A new coordinate with the new CRS + A new Coordinate instance with transformed geometry in the target CRS. + The coordinate_id is preserved from the original coordinate. Raises: - A ValueError if it fails to convert the coordinate + ValueError: If the new_crs cannot be parsed into a valid CRS, or if the + transformation results in infinite coordinate values (indicating an invalid transformation) + + Examples: + >>> # Transform from lat/lon to Web Mercator + >>> coord = Coordinate.from_lat_lon(40.7128, -74.0060) + >>> mercator_coord = coord.to_crs('EPSG:3857') + >>> + >>> # Transform using EPSG integer code + >>> utm_coord = coord.to_crs(32618) # UTM Zone 18N """ # convert the incoming crs to an pyproj.crs.CRS object; this could fail try: diff --git a/mappymatch/constructs/geofence.py b/mappymatch/constructs/geofence.py index b049736..b8e6a0c 100644 --- a/mappymatch/constructs/geofence.py +++ b/mappymatch/constructs/geofence.py @@ -15,11 +15,32 @@ class Geofence: """ - A geofence is basically a shapely polygon with a CRS + A geographic boundary polygon with an associated coordinate reference system (CRS). + + A Geofence defines a spatial region used to constrain map data or filter geographic queries. + It's commonly used to download only the relevant portion of a road network for map matching + by creating a buffer around a GPS trajectory. Args: - geom: The polygon geometry of the geofence + crs: The coordinate reference system of the geofence geometry + geometry: A Shapely Polygon defining the geographic boundary + + Attributes: crs: The CRS of the geofence + geometry: The Polygon geometry representing the bounded area + + Examples: + >>> from mappymatch.constructs.geofence import Geofence + >>> + >>> # Create a geofence from a trace to download relevant map data + >>> trace = Trace.from_gpx('route.gpx') + >>> geofence = Geofence.from_trace(trace, padding=1000) # 1km buffer + >>> + >>> # Load a geofence from a GeoJSON file + >>> geofence = Geofence.from_geojson('city_boundary.geojson') + >>> + >>> # Export to GeoJSON + >>> geojson_str = geofence.to_geojson() """ def __init__(self, crs: CRS, geometry: Polygon): @@ -29,13 +50,25 @@ def __init__(self, crs: CRS, geometry: Polygon): @classmethod def from_geojson(cls, file: Union[Path, str]) -> Geofence: """ - Creates a new geofence from a geojson file. + Create a geofence from a GeoJSON file containing a polygon. + + The GeoJSON file must contain exactly one polygon feature and must include + CRS information. This is typically used to define study areas or regions + for downloading map data. Args: - file: The path to the geojson file + file: Path to the GeoJSON file (as string or Path object) Returns: - A new geofence + A new Geofence instance + + Raises: + TypeError: If the file contains multiple polygons or lacks CRS information + + Examples: + >>> # Load a boundary polygon + >>> geofence = Geofence.from_geojson('study_area.geojson') + >>> print(geofence.crs) # EPSG:4326 """ filepath = Path(file) frame = read_file(filepath) @@ -62,19 +95,31 @@ def from_trace( buffer_res: int = 2, ) -> Geofence: """ - Create a new geofence from a trace. + Create a geofence by buffering around a GPS trace. - This is done by computing a radial buffer around the - entire trace (as a line). + This method creates a polygonal boundary around a trace by converting the trace + to a LineString, creating a buffer zone around the line, and transforming to the + specified CRS. + + This is particularly useful for downloading map data that covers a GPS trajectory, + ensuring you get all relevant roads while minimizing unnecessary data. Args: - trace: The trace to compute the bounding polygon for. - padding: The padding (in meters) around the trace line. - crs: The coordinate reference system to use. - buffer_res: The resolution of the surrounding buffer. + trace: The GPS trace to create a boundary around + padding: The buffer distance in meters around the trace. Default is 1000m (1km). + crs: The target coordinate reference system for the geofence. Default is WGS84 (EPSG:4326). + buffer_res: The resolution of the buffer polygon (number of segments per quadrant). Lower values create simpler polygons. Default is 2. Returns: - The computed bounding polygon. + A new Geofence encompassing the trace with the specified padding + + Examples: + >>> # Create a 500m buffer around a trace for map download + >>> trace = Trace.from_csv('morning_commute.csv') + >>> geofence = Geofence.from_trace(trace, padding=500) + >>> + >>> # Create a larger 2km buffer with higher resolution + >>> geofence = Geofence.from_trace(trace, padding=2000, buffer_res=8) """ trace_line_string = LineString([c.geom for c in trace.coords]) @@ -91,7 +136,20 @@ def from_trace( def to_geojson(self) -> str: """ - Converts the geofence to a geojson string. + Convert the geofence to a GeoJSON string. + + The geofence is automatically transformed to WGS84 (EPSG:4326) if it's in a different CRS, + since GeoJSON uses lat/lon coordinates by convention. + + Returns: + A GeoJSON string representation of the geofence polygon + + Examples: + >>> geofence = Geofence.from_trace(trace, padding=1000) + >>> geojson_str = geofence.to_geojson() + >>> # Write to file + >>> with open('boundary.geojson', 'w') as f: + ... f.write(geojson_str) """ if self.crs != LATLON_CRS: transformer = Transformer.from_crs(self.crs, LATLON_CRS) diff --git a/mappymatch/constructs/match.py b/mappymatch/constructs/match.py index ab5d82c..57520d2 100644 --- a/mappymatch/constructs/match.py +++ b/mappymatch/constructs/match.py @@ -6,12 +6,34 @@ class Match(NamedTuple): """ - Represents a match made by a Matcher + Represents a map-matching result linking a GPS coordinate to a road segment. + + A Match is the fundamental output of map-matching algorithms, connecting a GPS coordinate + to its best-matching road segment. When no suitable road is found within the matching + threshold, the road field is None and the distance is infinite. Attributes: - road: The road that was matched; None if no road was found; - coordinate: The original coordinate that was matched; - distance: The distance to the matched road; If no road was found, this is infinite + road: The road segment that was matched to the coordinate. None if no suitable road was found within the matching parameters. + coordinate: The original GPS coordinate that was matched + distance: The perpendicular distance from the coordinate to the matched road, in the units of the coordinate's CRS (typically meters). Set to infinity if no road was matched. + + Examples: + >>> from mappymatch.constructs.coordinate import Coordinate + >>> from mappymatch.constructs.road import Road, RoadId + >>> from mappymatch.constructs.match import Match + >>> from shapely.geometry import LineString + >>> + >>> # Create a successful match + >>> coord = Coordinate.from_lat_lon(40.7128, -74.0060) + >>> road = Road(RoadId('1', '2', 0), LineString([(0, 0), (1, 1)])) + >>> match = Match(road=road, coordinate=coord, distance=5.2) + >>> + >>> # Check if matching was successful + >>> if match.road is not None: + ... print(f"Matched to road with distance: {match.distance}m") + >>> + >>> # Create a failed match (no road found) + >>> no_match = Match(road=None, coordinate=coord, distance=float('inf')) """ road: Optional[Road] @@ -20,22 +42,43 @@ class Match(NamedTuple): def set_coordinate(self, c: Coordinate): """ - Set the coordinate of this match + Create a new match with a different coordinate. + + This is useful when you need to update the coordinate while preserving the matched + road and distance information. Since Match is immutable (NamedTuple), this returns + a new Match instance. Args: - c: The new coordinate + c: The new coordinate to associate with this match Returns: - The match with the new coordinate + A new Match instance with the updated coordinate, preserving the road and distance """ return self._replace(coordinate=c) def to_flat_dict(self) -> dict: """ - Convert this match to a flat dictionary + Convert this match to a flat dictionary suitable for DataFrame creation. + + This method creates a dictionary representation of the match, unpacking road metadata + if a road was matched. If no road was found, only the coordinate_id is included. Returns: - A flat dictionary with all match information + A flat dictionary containing: + - coordinate_id: The ID of the matched coordinate + - distance_to_road: The distance to the matched road (only if road is not None) + - All fields from road.to_flat_dict() (only if road is not None) + + Examples: + >>> # Successful match + >>> match = Match(road=some_road, coordinate=coord, distance=5.2) + >>> data = match.to_flat_dict() + >>> print(data['distance_to_road']) # 5.2 + >>> + >>> # Failed match + >>> no_match = Match(road=None, coordinate=coord, distance=float('inf')) + >>> data = no_match.to_flat_dict() + >>> print(data) # {'coordinate_id': ..., 'road_id': None} """ out = {"coordinate_id": self.coordinate.coordinate_id} diff --git a/mappymatch/constructs/road.py b/mappymatch/constructs/road.py index 240b998..b77b0ae 100644 --- a/mappymatch/constructs/road.py +++ b/mappymatch/constructs/road.py @@ -28,14 +28,17 @@ def from_json(cls, json: Dict[str, Any]) -> RoadId: class Road(NamedTuple): """ - Represents a road that can be matched to; + Represents a road segment in the road network that can be matched to GPS trajectories. + + A Road is an immutable object representing a directional edge in a road network graph. + Roads have a unique identifier (composed of start/end junctions and a key), a geometry + (typically a LineString), and optional metadata for storing additional attributes like + speed limits, road names, etc. Attributes: - road_id: The unique identifier for this road - geom: The geometry of this road - origin_junction_id: The unique identifier of the origin junction of this road - destination_junction_id: The unique identifier of the destination junction of this road - metadata: an optional dictionary for storing additional metadata + road_id: A RoadId tuple uniquely identifying this road segment (start, end, key) + geom: The Shapely LineString geometry representing the road's path + metadata: An optional dictionary for storing additional road attributes such as speed limits, road names, surface type, etc. """ road_id: RoadId @@ -45,7 +48,14 @@ class Road(NamedTuple): def to_dict(self) -> Dict[str, Any]: """ - Convert the road to a dictionary + Convert the road to a dictionary representation. + + This creates a dictionary with all road attributes, extracting the origin and + destination junction IDs from the road_id for convenience. + + Returns: + A dictionary containing the road's attributes with separate keys for + origin_junction_id, destination_junction_id, and road_key """ d = self._asdict() d["origin_junction_id"] = self.road_id.start @@ -56,7 +66,16 @@ def to_dict(self) -> Dict[str, Any]: def to_flat_dict(self) -> Dict[str, Any]: """ - Convert the road to a flat dictionary + Convert the road to a flat dictionary with metadata unpacked. + + This method creates a single-level dictionary by unpacking the metadata dictionary + and merging it with the road's other attributes. This is useful for creating + DataFrames or exporting to formats that don't support nested structures. + + Returns: + A flat dictionary with all road attributes and metadata fields at the top level. + The 'metadata' key itself is removed. + """ if self.metadata is None: return self.to_dict() diff --git a/mappymatch/constructs/trace.py b/mappymatch/constructs/trace.py index f71f863..36dfa91 100644 --- a/mappymatch/constructs/trace.py +++ b/mappymatch/constructs/trace.py @@ -16,12 +16,37 @@ class Trace: """ - A Trace is a collection of coordinates that represents a trajectory to be matched. + A collection of coordinates representing a GPS trajectory or path to be map-matched. + + A Trace wraps a GeoDataFrame of point geometries and provides methods for creating, + manipulating, and transforming GPS trajectories. Traces are the primary input for + map matching algorithms. + + The underlying GeoDataFrame must have unique indices - duplicate indices will raise + an IndexError during initialization. Attributes: - coords: A list of all the coordinates - crs: The CRS of the trace - index: The index of the trace + coords: A list of Coordinate objects representing each point in the trajectory + crs: The coordinate reference system (CRS) of the trace + index: The pandas Index from the underlying GeoDataFrame + + Examples: + >>> import pandas as pd + >>> from mappymatch.constructs.trace import Trace + >>> + >>> # Create from a DataFrame with lat/lon columns + >>> df = pd.DataFrame({ + ... 'latitude': [40.7128, 40.7589, 40.7614], + ... 'longitude': [-74.0060, -73.9851, -73.9776] + ... }) + >>> trace = Trace.from_dataframe(df) + >>> + >>> # Create from a GPX file + >>> trace = Trace.from_gpx('path/to/track.gpx') + >>> + >>> # Access coordinates + >>> print(len(trace)) # Number of points + >>> first_coord = trace.coords[0] """ _frame: GeoDataFrame @@ -69,7 +94,13 @@ def index(self) -> pd.Index: @cached_property def coords(self) -> List[Coordinate]: """ - Get coordinates as Coordinate objects. + Get all coordinates in the trace as Coordinate objects. + + This property constructs Coordinate objects from the underlying GeoDataFrame, + preserving the index values as coordinate IDs. The result is cached for performance. + + Returns: + A list of Coordinate objects, one for each point in the trace, ordered by the trace index """ coords_list = [ Coordinate(i, g, self.crs) @@ -89,16 +120,29 @@ def from_geo_dataframe( xy: bool = True, ) -> Trace: """ - Builds a trace from a geopandas dataframe + Create a trace from a GeoPandas GeoDataFrame. - Expects the dataframe to have geometry column + The GeoDataFrame must contain a geometry column with Point geometries representing + the GPS trajectory. Additional columns are discarded - only the geometry and index + are retained. Args: - frame: geopandas dataframe with _one_ trace - xy: should the trace be projected to epsg 3857? + frame: A GeoDataFrame with Point geometries representing the trajectory. Must have a valid CRS and unique index values. + xy: If True, reproject the trace to Web Mercator (EPSG:3857) for distance calculations. If False, keep the original CRS. Default is True. Returns: - The trace built from the geopandas dataframe + A new Trace instance + + Examples: + >>> import geopandas as gpd + >>> from shapely.geometry import Point + >>> + >>> # Create a GeoDataFrame with point geometries + >>> gdf = gpd.GeoDataFrame( + ... geometry=[Point(-74.0060, 40.7128), Point(-73.9851, 40.7589)], + ... crs='EPSG:4326' + ... ) + >>> trace = Trace.from_geo_dataframe(gdf) """ # get rid of any extra info besides geometry and index frame = GeoDataFrame(geometry=frame.geometry, index=frame.index) @@ -115,18 +159,36 @@ def from_dataframe( lon_column: str = "longitude", ) -> Trace: """ - Builds a trace from a pandas dataframe + Create a trace from a pandas DataFrame with latitude/longitude columns. - Expects the dataframe to have latitude / longitude information in the epsg 4326 format + This is one of the most common ways to create a Trace from GPS data. The DataFrame + must contain columns with latitude and longitude values in WGS84 (EPSG:4326) format. Args: - dataframe: pandas dataframe with _one_ trace - xy: should the trace be projected to epsg 3857? - lat_column: the name of the latitude column - lon_column: the name of the longitude column + dataframe: A pandas DataFrame containing GPS coordinates in EPSG:4326 format + xy: If True, reproject to Web Mercator (EPSG:3857) for accurate distance calculations. If False, maintain lat/lon coordinates. Default is True. + lat_column: The name of the column containing latitude values. Default is "latitude". + lon_column: The name of the column containing longitude values. Default is "longitude". Returns: - The trace built from the pandas dataframe + A new Trace instance + + Examples: + >>> import pandas as pd + >>> + >>> # Create from a DataFrame with default column names + >>> df = pd.DataFrame({ + ... 'latitude': [40.7128, 40.7589, 40.7614], + ... 'longitude': [-74.0060, -73.9851, -73.9776] + ... }) + >>> trace = Trace.from_dataframe(df) + >>> + >>> # Use custom column names + >>> df_custom = pd.DataFrame({ + ... 'lat': [40.7128, 40.7589], + ... 'lon': [-74.0060, -73.9851] + ... }) + >>> trace = Trace.from_dataframe(df_custom, lat_column='lat', lon_column='lon') """ frame = GeoDataFrame( geometry=points_from_xy(dataframe[lon_column], dataframe[lat_column]), @@ -143,16 +205,28 @@ def from_gpx( xy: bool = True, ) -> Trace: """ - Builds a trace from a gpx file. + Create a trace from a GPX (GPS Exchange Format) file. - Expects the file to have simple gpx structure: a sequence of lat, lon pairs + Parses GPX track data and extracts latitude/longitude coordinates from trackpoints. + This method expects a simple GPX structure with a sequence of lat/lon coordinate pairs. Args: - file: the gpx file - xy: should the trace be projected to epsg 3857? + file: Path to the GPX file (as string or Path object) + xy: If True, reproject to Web Mercator (EPSG:3857) for accurate distance calculations. If False, maintain lat/lon coordinates. Default is True. Returns: - The trace built from the gpx file + A new Trace instance with coordinates extracted from the GPX file + + Raises: + FileNotFoundError: If the specified file does not exist + TypeError: If the file does not have a .gpx extension + + Examples: + >>> # Load a GPX track from a file + >>> trace = Trace.from_gpx('morning_run.gpx') + >>> + >>> # Keep in lat/lon instead of projecting + >>> trace_latlon = Trace.from_gpx('bike_ride.gpx', xy=False) """ filepath = Path(file) if not filepath.is_file(): @@ -178,18 +252,31 @@ def from_csv( lon_column: str = "longitude", ) -> Trace: """ - Builds a trace from a csv file. + Create a trace from a CSV file containing latitude/longitude coordinates. - Expects the file to have latitude / longitude information in the epsg 4326 format + The CSV file must contain columns with latitude and longitude values in WGS84 + (EPSG:4326) format. The DataFrame index will be used as coordinate IDs. Args: - file: the csv file - xy: should the trace be projected to epsg 3857? - lat_column: the name of the latitude column - lon_column: the name of the longitude column + file: Path to the CSV file (as string or Path object) + xy: If True, reproject to Web Mercator (EPSG:3857) for accurate distance calculations. If False, maintain lat/lon coordinates. Default is True. + lat_column: The name of the column containing latitude values. Default is "latitude". + lon_column: The name of the column containing longitude values. Default is "longitude". Returns: - The trace built from the csv file + A new Trace instance with coordinates from the CSV file + + Raises: + FileNotFoundError: If the specified file does not exist + TypeError: If the file does not have a .csv extension + ValueError: If the specified lat/lon columns are not found in the CSV + + Examples: + >>> # Load from CSV with default column names + >>> trace = Trace.from_csv('gps_data.csv') + >>> + >>> # Load with custom column names + >>> trace = Trace.from_csv('track.csv', lat_column='lat', lon_column='lng') """ filepath = Path(file) if not filepath.is_file(): @@ -213,14 +300,21 @@ def from_csv( @classmethod def from_parquet(cls, file: Union[str, Path], xy: bool = True): """ - Read a trace from a parquet file + Create a trace from a GeoParquet file. + + GeoParquet is a columnar storage format for geospatial data. The file must contain + a geometry column with Point geometries and a valid CRS. Args: - file: the parquet file - xy: should the trace be projected to epsg 3857? + file: Path to the GeoParquet file (as string or Path object) + xy: If True, reproject to Web Mercator (EPSG:3857) for accurate distance calculations. If False, maintain the original CRS. Default is True. Returns: - The trace built from the parquet file + A new Trace instance with coordinates from the GeoParquet file + + Examples: + >>> # Load from a GeoParquet file + >>> trace = Trace.from_parquet('trajectory.parquet') """ filepath = Path(file) frame = read_parquet(filepath) @@ -235,16 +329,26 @@ def from_geojson( xy: bool = True, ): """ - Reads a trace from a geojson file; - If index_property is not specified, this will set any property columns as the index. + Create a trace from a GeoJSON file containing Point features. + + The GeoJSON file should contain Point geometries representing the GPS trajectory. + If index_property is specified, that property will be used as the DataFrame index; + otherwise, all non-geometry properties will be combined to create the index. Args: - file: the geojson file - index_property: the name of the property to use as the index - xy: should the trace be projected to epsg 3857? + file: Path to the GeoJSON file (as string or Path object) + index_property: The name of a GeoJSON property to use as the DataFrame index. If None, all properties excluding geometry will be used as index columns. Default is None. + xy: If True, reproject to Web Mercator (EPSG:3857) for accurate distance calculations. If False, maintain the original CRS. Default is True. Returns: - The trace built from the geojson file + A new Trace instance with coordinates from the GeoJSON file + + Examples: + >>> # Load from GeoJSON, using all properties as index + >>> trace = Trace.from_geojson('path.geojson') + >>> + >>> # Use a specific property as index + >>> trace = Trace.from_geojson('points.geojson', index_property='timestamp') """ filepath = Path(file) frame = read_file(filepath) @@ -259,13 +363,23 @@ def from_geojson( def downsample(self, npoints: int) -> Trace: """ - Downsample the trace to a given number of points + Downsample the trace to a specified number of evenly-spaced points. + + This method uses linear interpolation across the trace indices to select a subset + of points that are approximately evenly distributed along the trajectory. Args: - npoints: the number of points to downsample to + npoints: The target number of points in the downsampled trace Returns: - The downsampled trace + A new Trace with approximately npoints evenly-distributed points + + Examples: + >>> # Reduce a 1000-point trace to 100 points + >>> long_trace = Trace.from_csv('detailed_track.csv') + >>> print(len(long_trace)) # 1000 + >>> short_trace = long_trace.downsample(100) + >>> print(len(short_trace)) # 100 """ s = list(np.linspace(0, len(self._frame) - 1, npoints).astype(int)) @@ -275,13 +389,24 @@ def downsample(self, npoints: int) -> Trace: def drop(self, index=List) -> Trace: """ - Remove points from the trace specified by the index parameter + Remove points from the trace by their index values. + + This method creates a new trace with specified points removed. The index parameter + should contain the DataFrame index values (not positional integers) of the points + to remove. Args: - index: the index of the points to drop (0 based index) + index: A list of index values identifying the points to remove. These should be + values from the trace's DataFrame index, not integer positions. Returns: - The trace with the points removed + A new Trace with the specified points removed + + Examples: + >>> # Remove points with specific index values + >>> trace = Trace.from_dataframe(df) # df has index [0, 1, 2, 3, 4] + >>> cleaned_trace = trace.drop([1, 3]) # Removes points at index 1 and 3 + >>> print(len(cleaned_trace)) # 3 (originally 5, removed 2) """ new_frame = self._frame.drop(index) @@ -289,22 +414,42 @@ def drop(self, index=List) -> Trace: def to_crs(self, new_crs: CRS) -> Trace: """ - Converts the crs of a trace to a new crs + Transform the trace to a different coordinate reference system (CRS). + + This method reprojects all coordinates in the trace to the specified CRS. Args: - new_crs: the new crs to convert to + new_crs: The target CRS. Can be a pyproj.CRS object, EPSG code string + (e.g., 'EPSG:4326'), or any format accepted by pyproj.CRS() Returns: - A new trace with the new crs + A new Trace with all coordinates transformed to the target CRS + + Examples: + >>> # Convert from Web Mercator to WGS84 lat/lon + >>> trace_xy = Trace.from_csv('data.csv', xy=True) # In EPSG:3857 + >>> trace_latlon = trace_xy.to_crs('EPSG:4326') + >>> + >>> # Convert to a UTM zone + >>> from pyproj import CRS + >>> utm_crs = CRS('EPSG:32618') # UTM Zone 18N + >>> trace_utm = trace_latlon.to_crs(utm_crs) """ new_frame = self._frame.to_crs(new_crs) return Trace(new_frame) def to_geojson(self, file: Union[str, Path]): """ - Write the trace to a geojson file + Write the trace to a GeoJSON file. + + This exports the trace as a GeoJSON FeatureCollection where each point is a Feature + with Point geometry. The CRS information and any index data are preserved. Args: - file: the file to write to + file: Path where the GeoJSON file should be written (as string or Path object) + + Examples: + >>> trace = Trace.from_csv('input.csv') + >>> trace.to_geojson('output.geojson') """ self._frame.to_file(file, driver="GeoJSON") diff --git a/mappymatch/maps/igraph/igraph_map.py b/mappymatch/maps/igraph/igraph_map.py index f441512..08f6fdc 100644 --- a/mappymatch/maps/igraph/igraph_map.py +++ b/mappymatch/maps/igraph/igraph_map.py @@ -31,12 +31,29 @@ class IGraphMap(MapInterface): """ - A road map that uses an igraph graph to represent its roads. + A road network map implementation using igraph for high-performance graph operations. + + IGraphMap wraps an igraph.Graph to represent a road network, providing potentially + faster performance than NetworkX for very large networks. It uses an R-tree spatial + index for efficient nearest-neighbor searches and supports both distance and time-based routing. + + The underlying igraph must have: + - A pyproj CRS stored in graph['crs'] + - Road geometries (LineStrings) stored as edge attributes + - Node IDs stored in vertex attributes + - Edge IDs (keys) for multi-edges between the same nodes + - Optional distance and time weights for routing + + This implementation is particularly useful when working with very large road networks + where performance is critical. Attributes: - ig: The igraph graph that represents the road map - road_mapping: A mapping from road ids to igraph edge ids + g: The igraph.Graph representing the road network + road_mapping: A dictionary mapping RoadId tuples to igraph edge indices crs: The coordinate reference system of the map + + Note: + igraph must be installed to use this class. Install with: pip install igraph """ def __init__(self, graph: ig.Graph): diff --git a/mappymatch/maps/map_interface.py b/mappymatch/maps/map_interface.py index adf9702..6ec2fd3 100644 --- a/mappymatch/maps/map_interface.py +++ b/mappymatch/maps/map_interface.py @@ -12,17 +12,33 @@ class MapInterface(metaclass=ABCMeta): """ - Abstract base class for a Matcher + Abstract base class defining the interface for road network representations. + + All map implementations in mappymatch implement this interface, providing a consistent + API for road network operations including spatial queries, routing, and road lookups. + This allows different backend implementations (NetworkX, igraph, etc.) to be used + interchangeably. + + Subclasses must implement methods for: + - Finding nearest roads to coordinates + - Computing shortest paths between points + - Looking up roads by ID + - Providing lists of all roads + """ @property @abstractmethod def distance_weight(self) -> str: """ - Get the distance weight + Get the name of the edge attribute used for distance-based routing. + + This property identifies which edge attribute should be used when computing + shortest paths based on physical distance (as opposed to time or other metrics). Returns: - The distance weight + The name of the distance weight attribute (e.g., 'kilometers', 'miles', 'length') + """ return DEFAULT_DISTANCE_WEIGHT @@ -30,10 +46,14 @@ def distance_weight(self) -> str: @abstractmethod def time_weight(self) -> str: """ - Get the time weight + Get the name of the edge attribute used for time-based routing. + + This property identifies which edge attribute should be used when computing + fastest paths based on travel time (as opposed to distance or other metrics). Returns: - The time weight + The name of the time weight attribute (e.g., 'minutes', 'seconds', 'travel_time') + """ return DEFAULT_TIME_WEIGHT @@ -65,13 +85,17 @@ def nearest_road( coord: Coordinate, ) -> Road: """ - Return the nearest road to a coordinate + Find the road segment nearest to a given coordinate. + + This method performs a spatial search to identify the closest road segment + to the specified coordinate. It typically uses a spatial index (like an R-tree) + for efficient querying. Args: - coord: The coordinate to find the nearest road to + coord: The coordinate to search from Returns: - The nearest road to the coordinate + The Road object that is spatially nearest to the coordinate """ @abstractmethod @@ -82,13 +106,18 @@ def shortest_path( weight: Optional[Union[str, Callable]] = None, ) -> List[Road]: """ - Computes the shortest path on the road network + Compute the shortest path through the road network between two coordinates. + + This method finds the optimal route from origin to destination using the + road network graph. The "shortest" path can be based on distance, time, + or any custom weight function. Args: - origin: The origin coordinate - destination: The destination coordinate - weight: The weight to use for the path + origin: The starting coordinate. The path begins from the nearest road to this point. + destination: The ending coordinate. The path ends at the nearest road to this point. + weight: The edge attribute or function to minimize. Can be a string attribute name (e.g., 'kilometers', 'minutes'), a callable that takes edge data and returns a weight, or None to use the default distance weight. Returns: - A list of roads that form the shortest path + A list of Road objects representing the path from origin to destination, + ordered sequentially. Returns empty list if no path exists (disconnected components). """ diff --git a/mappymatch/maps/nx/nx_map.py b/mappymatch/maps/nx/nx_map.py index a1844c6..177f462 100644 --- a/mappymatch/maps/nx/nx_map.py +++ b/mappymatch/maps/nx/nx_map.py @@ -29,11 +29,45 @@ class NxMap(MapInterface): """ - A road map that uses a networkx graph to represent its roads. + A road network map implementation using NetworkX graphs. + + NxMap wraps a NetworkX MultiDiGraph to represent a road network, providing efficient + graph operations and spatial queries. It uses an R-tree spatial index for fast + nearest-neighbor searches and supports both distance and time-based routing. + + The underlying graph must have: + - A pyproj CRS stored in graph.graph['crs'] + - Road geometries (LineStrings) for each edge + - Optional distance and time weights for routing + + NxMap is the primary map implementation in mappymatch and integrates well with + OSMnx for downloading OpenStreetMap data. Attributes: - g: The networkx graph that represents the road map + g: The NetworkX MultiDiGraph representing the road network crs: The coordinate reference system of the map + + Examples: + >>> from mappymatch.maps.nx import NxMap + >>> from mappymatch.constructs.geofence import Geofence + >>> + >>> # Load from a saved file + >>> road_map = NxMap.from_file('network.pickle') + >>> + >>> # Create from OpenStreetMap data + >>> geofence = Geofence.from_geojson('study_area.geojson') + >>> road_map = NxMap.from_geofence( + ... geofence, + ... network_type=NetworkType.DRIVE, + ... xy=True # Use Web Mercator for accurate distances + ... ) + >>> + >>> # Use the map for routing and queries + >>> nearest = road_map.nearest_road(coordinate) + >>> path = road_map.shortest_path(origin, destination) + >>> + >>> # Save for later use + >>> road_map.to_file('network.pickle') """ def __init__(self, graph: nx.MultiDiGraph): @@ -158,13 +192,30 @@ def road_by_id(self, road_id: RoadId) -> Optional[Road]: def set_road_attributes(self, attributes: Dict[RoadId, Dict[str, Any]]): """ - Set the attributes of the roads in the map + Add or update attributes for specific roads in the network. - Args: - attributes: A dictionary mapping road ids to dictionaries of attributes + This allows you to enrich the road network with custom data like measured speeds, + traffic volumes, pavement conditions, etc. The new attributes become part of the + road metadata and can be accessed in matching results. - Returns: - None + Args: + attributes: A dictionary mapping RoadId objects to dictionaries of attribute name-value pairs. + + Note: + After setting attributes, the internal spatial index is rebuilt. For bulk + updates, it's more efficient to set all attributes in a single call. + + Examples: + >>> # Add custom speed data + >>> speed_data = { + ... RoadId('1', '2', 0): {'measured_speed_mph': 32.5}, + ... RoadId('2', '3', 0): {'measured_speed_mph': 28.3} + ... } + >>> road_map.set_road_attributes(speed_data) + >>> + >>> # Access the new attributes + >>> road = road_map.road_by_id(RoadId('1', '2', 0)) + >>> print(road.metadata['measured_speed_mph']) # 32.5 """ for attrs in attributes.values(): for attr_name in attrs.keys(): @@ -183,13 +234,26 @@ def roads(self) -> List[Road]: @classmethod def from_file(cls, file: Union[str, Path]) -> NxMap: """ - Build a NxMap instance from a file + Load a NxMap from a saved file. + + Supports loading from pickle (.pickle) or JSON (.json) formats. Pickle files + are smaller and faster to load, while JSON files are human-readable and portable. Args: - file: The graph pickle file to load the graph from + file: Path to the saved map file (must have .pickle or .json extension) Returns: - A NxMap instance + A NxMap instance loaded from the file + + Raises: + TypeError: If the file extension is not .pickle or .json + + Examples: + >>> # Load from pickle (recommended for large networks) + >>> road_map = NxMap.from_file('network.pickle') + >>> + >>> # Load from JSON + >>> road_map = NxMap.from_file('network.json') """ p = Path(file) if p.suffix == ".pickle": @@ -212,18 +276,47 @@ def from_geofence( filter_to_largest_component: bool = True, ) -> NxMap: """ - Read an OSM network graph into a NxMap + Download and create a NxMap from OpenStreetMap data within a geofence. + + This method uses OSMnx to download road network data from OpenStreetMap for the + specified geographic area. It's the primary way to create maps from online sources. Args: - geofence: the geofence to clip the graph to - xy: whether to use xy coordinates or lat/lon - network_type: the network type to use for the graph - custom_filter: a custom filter to pass to osmnx like '["highway"~"motorway|primary"]' - additional_metadata_keys: additional keys to preserve in road metadata like '["maxspeed", "highway"]' - filter_to_largest_component: if True, keep only the largest strongly connected component; if False, keep all components (may result in routing failures between disconnected components) + geofence: A Geofence defining the area to download. Must be in EPSG:4326 (lat/lon). + xy: If True, convert the network to Web Mercator (EPSG:3857) for accurate distance calculations. If False, keep in EPSG:4326. Default is True. + network_type: The type of road network to download. Options include DRIVE, WALK, BIKE, DRIVE_SERVICE, ALL, etc. Default is DRIVE. + custom_filter: A custom OSMnx filter string for advanced queries. Example: '["highway"~"motorway|primary"]' for only major roads. + additional_metadata_keys: Set or list of OSM tag keys to preserve in road metadata. Example: {'maxspeed', 'highway', 'name', 'surface'} + filter_to_largest_component: If True (default), keep only the largest strongly connected component to ensure routing works. If False, keep all components (may cause routing failures between disconnected parts). Returns: - a NxMap + A new NxMap with roads downloaded from OpenStreetMap + + Raises: + TypeError: If the geofence is not in EPSG:4326 + MapException: If OSMnx is not installed + + Examples: + >>> from mappymatch.maps.nx import NxMap, NetworkType + >>> from mappymatch.constructs.geofence import Geofence + >>> + >>> # Download driving network for a city + >>> geofence = Geofence.from_geojson('city_boundary.geojson') + >>> road_map = NxMap.from_geofence(geofence, network_type=NetworkType.DRIVE) + >>> + >>> # Download with additional metadata + >>> road_map = NxMap.from_geofence( + ... geofence, + ... network_type=NetworkType.DRIVE, + ... additional_metadata_keys={'maxspeed', 'lanes', 'name'} + ... ) + >>> + >>> # Custom filter for only highways + >>> road_map = NxMap.from_geofence( + ... geofence, + ... network_type=NetworkType.DRIVE, + ... custom_filter='["highway"~"motorway|trunk|primary"]' + ... ) """ if geofence.crs != LATLON_CRS: raise TypeError( @@ -246,10 +339,24 @@ def from_geofence( def to_file(self, outfile: Union[str, Path]): """ - Save the graph to a pickle file + Save the map to a file for later use. + + Saves the entire NxMap including the graph structure, geometries, CRS, and all + metadata. Supports pickle (.pickle) and JSON (.json) formats. Args: - outfile: The file to save the graph to + outfile: Path where the file should be saved (extension determines format: + .pickle for binary pickle format, .json for JSON format) + + Raises: + TypeError: If the file extension is not .pickle or .json + + Examples: + >>> # Save as pickle (recommended - faster and smaller) + >>> road_map.to_file('network.pickle') + >>> + >>> # Save as JSON (portable and human-readable) + >>> road_map.to_file('network.json') """ outfile = Path(outfile) diff --git a/mappymatch/maps/nx/readers/osm_readers.py b/mappymatch/maps/nx/readers/osm_readers.py index 66e47ea..36c8f5d 100644 --- a/mappymatch/maps/nx/readers/osm_readers.py +++ b/mappymatch/maps/nx/readers/osm_readers.py @@ -21,7 +21,26 @@ class NetworkType(Enum): """ - Enumerator for Network Types supported by osmnx. + Enumeration of road network types supported by OSMnx for downloading from OpenStreetMap. + + These network types determine which roads are included when downloading OSM data. + Each type corresponds to a predefined filter in OSMnx for different use cases and + transportation modes. + + Values: + ALL_PRIVATE: All road types including private roads + ALL: All public road types + BIKE: Roads suitable for cycling + DRIVE: Roads suitable for driving (excludes footpaths, bike paths, etc.) + DRIVE_SERVICE: Driving roads including service roads (parking aisles, driveways, etc.) + WALK: Roads suitable for walking (includes footpaths, pedestrian areas, etc.) + + Examples: + >>> from mappymatch.maps.nx.readers.osm_readers import NetworkType + >>> # Download a driving network + >>> road_map = NxMap.from_geofence(geofence, network_type=NetworkType.DRIVE) + >>> # Download a walking network + >>> walk_map = NxMap.from_geofence(geofence, network_type=NetworkType.WALK) """ ALL_PRIVATE = "all_private" @@ -41,18 +60,47 @@ def nx_graph_from_osmnx( filter_to_largest_component: bool = True, ) -> nx.MultiDiGraph: """ - Build a networkx graph from OSM data + Download and process a road network from OpenStreetMap using OSMnx. + + This function: + 1. Downloads OSM data within the geofence using OSMnx + 2. Optionally projects to Web Mercator for accurate distances + 3. Adds speed and travel time estimates + 4. Converts distances to kilometers + 5. Optionally filters to the largest connected component + 6. Compresses the graph to remove unnecessary data Args: - geofence: the geofence to clip the graph to - network_type: the network type to use for the graph - xy: whether to use xy coordinates or lat/lon - custom_filter: a custom filter to pass to osmnx - additional_metadata_keys: additional keys to preserve in metadata - filter_to_largest_component: if True, keep only the largest strongly connected component; if False, keep all components (may result in routing failures between disconnected components) + geofence: The geographic boundary for downloading. Must be in EPSG:4326 (lat/lon). + network_type: The type of network to download (DRIVE, WALK, BIKE, etc.) + xy: If True, project to Web Mercator (EPSG:3857) for accurate metric distances. If False, keep in lat/lon. Default is True. + custom_filter: A custom OSMnx filter string for advanced queries (e.g., '["highway"~"motorway|trunk|primary"]'). If specified, overrides network_type. + additional_metadata_keys: Set of OSM tag keys to preserve in road metadata (e.g., {'maxspeed', 'lanes', 'surface'}). + filter_to_largest_component: If True (default), keep only the largest strongly connected component to ensure all parts of the network are routable to each other. If False, keep disconnected components (may cause routing failures). Returns: - a networkx graph of the OSM network + A processed NetworkX MultiDiGraph ready for use with NxMap + + Raises: + MapException: If OSMnx is not installed + + Examples: + >>> from mappymatch.maps.nx.readers.osm_readers import nx_graph_from_osmnx, NetworkType + >>> + >>> # Download a driving network + >>> graph = nx_graph_from_osmnx( + ... geofence=my_geofence, + ... network_type=NetworkType.DRIVE, + ... xy=True + ... ) + >>> + >>> # Download with custom filter and metadata + >>> graph = nx_graph_from_osmnx( + ... geofence=my_geofence, + ... network_type=NetworkType.DRIVE, + ... custom_filter='["highway"~"motorway|primary|secondary"]', + ... additional_metadata_keys={'maxspeed', 'lanes', 'name'} + ... ) """ try: import osmnx as ox @@ -82,17 +130,28 @@ def parse_osmnx_graph( filter_to_largest_component: bool = True, ) -> nx.MultiDiGraph: """ - Parse the raw osmnx graph into a graph that we can use with our NxMap + Process and clean a raw OSMnx graph for use with mappymatch. + + This function takes a graph downloaded from OSMnx and processes it by: + - Projecting to a metric coordinate system (optional) + - Computing edge speeds and travel times + - Adding distance in kilometers + - Filtering to the largest connected component (optional) + - Creating geometries for edges that lack them + - Compressing by removing unnecessary data Args: - geofence: the geofence to clip the graph to - xy: whether to use xy coordinates or lat/lon - network_type: the network type to use for the graph - additional_metadata_keys: additional keys to preserve in metadata - filter_to_largest_component: if True, keep only the largest strongly connected component; if False, keep all components (may result in routing failures between disconnected components) + graph: A raw NetworkX MultiDiGraph from OSMnx + network_type: The type of network (used for metadata) + xy: If True, project to Web Mercator (EPSG:3857). If False, keep in lat/lon. + additional_metadata_keys: Set of OSM tag keys to preserve in road metadata + filter_to_largest_component: If True, keep only the largest strongly connected component. If False, keep all components. Returns: - a cleaned networkx graph of the OSM network + A cleaned and processed NetworkX MultiDiGraph ready for NxMap + + Raises: + MapException: If OSMnx is not installed or if the network has no connected components """ try: import osmnx as ox @@ -146,14 +205,40 @@ def compress( g: nx.MultiDiGraph, additional_metadata_keys: Optional[set] = None ) -> nx.MultiDiGraph: """ - Remove unnecessary data from the networkx graph while preserving essential attributes + Remove unnecessary data from a NetworkX graph while preserving essential attributes. + + This function reduces memory usage and file size by: + - Moving specified OSM tags to a metadata dictionary + - Removing edge attributes that aren't needed for routing or matching + - Removing all node attributes (only the node IDs are needed) + + Essential attributes for edges are preserved: + - geometry: The LineString geometry of the road + - kilometers: Distance in kilometers for routing + - travel_time: Estimated travel time for routing + - metadata: Dictionary of additional OSM tags Args: - g: the networkx graph to compress - additional_metadata_keys: additional keys to preserve in metadata + g: The NetworkX MultiDiGraph to compress + additional_metadata_keys: Set of OSM tag keys to preserve in the metadata dictionary. Default preserved keys are 'osmid' and 'name'. Any additional keys specified here will also be moved to metadata. Returns: - the compressed networkx graph + The compressed NetworkX MultiDiGraph (modified in-place and returned) + + Examples: + >>> # Compress with default metadata + >>> compressed = compress(graph) + >>> + >>> # Preserve additional OSM tags + >>> compressed = compress( + ... graph, + ... additional_metadata_keys={'maxspeed', 'lanes', 'surface', 'highway'} + ... ) + >>> + >>> # Access metadata + >>> edge_data = graph.edges[('123', '456', 0)] + >>> print(edge_data['metadata']['name']) # Street name + >>> print(edge_data['metadata']['maxspeed']) # Speed limit if available """ # Define attributes to keep for edges edge_keep_keys = { diff --git a/mappymatch/matchers/lcss/constructs.py b/mappymatch/matchers/lcss/constructs.py index d1e2609..8e43e9d 100644 --- a/mappymatch/matchers/lcss/constructs.py +++ b/mappymatch/matchers/lcss/constructs.py @@ -18,10 +18,15 @@ class CuttingPoint(NamedTuple): """ - A cutting point represents where the LCSS algorithm cuts the trace into a sub-segment. + Represents a location where the LCSS algorithm splits a trajectory into sub-segments. + + Cutting points are identified during the iterative refinement process as locations where + the GPS trajectory deviates significantly from the candidate path. Splitting at these + points allows the algorithm to find better local matches. Attributes: - trace_index: An index of where to cut the trace + trace_index: The integer index in the trace where the cut should be made. + This indexes into the trace's coordinate list. """ trace_index: Union[signedinteger, int] @@ -29,14 +34,21 @@ class CuttingPoint(NamedTuple): class TrajectorySegment(NamedTuple): """ - Represents a pairing of a trace and candidate path + Represents a pairing of a GPS trace segment with a candidate path through the road network. + + TrajectorySegments are the core data structure used by the LCSS matcher. Each segment + contains a portion of the GPS trace, a candidate path through the road network, matching + results, a similarity score, and potential cutting points for further refinement. + + During the iterative LCSS algorithm, segments are scored, split at cutting points, and + merged to find the best overall match between the GPS trajectory and the road network. Attributes: - trace: The trace in the segment - path: The candidate path in the segment - matches: The matches between the trace and the path - score: The similarity score between the trace and the path - cutting_points: The points where the trace and path are to be cut + trace: The GPS trace segment being matched + path: The candidate path through the road network (list of Road objects) + matches: List of Match objects linking each GPS point to a road. Empty until score_and_match is called. + score: The LCSS similarity score between trace and path (0-1). 0 until scored. + cutting_points: List of CuttingPoint objects indicating where to split this segment for further refinement. Empty until compute_cutting_points is called. """ trace: Trace @@ -95,14 +107,28 @@ def score_and_match( max_distance: float, ) -> TrajectorySegment: """ - Computes the score of a trace, pair matching and also matches the coordinates to the nearest road. + Compute the LCSS similarity score and match GPS points to road segments. + + This method implements the core LCSS (Longest Common Subsequence) algorithm for + trajectory matching. It computes a similarity score between the GPS trace and + the candidate path based on point-to-path distances, and simultaneously matches + each GPS coordinate to its nearest road in the path. + + The LCSS similarity score ranges from 0 (no similarity) to 1 (perfect match), + with higher scores indicating better alignment between the trajectory and path. Args: - distance_epsilon: The distance threshold for matching - max_distance: The maximum distance between the trace and the path + distance_epsilon: The distance threshold (in meters) for similarity. GPS points + within this distance of the path contribute positively to the score. Points + farther away contribute zero. + max_distance: The maximum distance (in meters) for matching. GPS points beyond + this distance from the nearest road are left unmatched (road=None, distance=inf). Returns: - The updated trajectory segment with a score and matches + A new TrajectorySegment with populated matches list and computed score + + Raises: + Exception: If the trace has 0 points (edge case that cannot be matched) """ trace = self.trace path = self.path @@ -176,17 +202,31 @@ def compute_cutting_points( random_cuts: int, ) -> TrajectorySegment: """ - Computes the cutting points for a trajectory segment by: - - computing the furthest point - - adding points that are close to the distance epsilon + Identify locations where the trajectory should be split for refinement. + + Cutting points are locations where the GPS trace deviates from the candidate path, + suggesting that splitting the trajectory at these points and computing separate + paths for each segment may yield better matches. + + The method identifies cutting points by: + 1. Finding the point with maximum distance from the matched path (highest deviation) + 2. Finding points near the distance_epsilon threshold (borderline matches) + 3. Optionally adding random points for exploration + 4. Handling edge cases (no path, circular routes, etc.) + + Adjacent cutting points are compressed to avoid splitting too finely, and cutting + points at the start/end of the trace are removed (can't split there). Args: - distance_epsilon: The distance threshold for matching - cutting_thresh: The threshold for cutting the trace - random_cuts: The number of random cuts to add + distance_epsilon: The distance threshold (in meters) used for matching. + Points near this threshold are candidates for cutting. + cutting_thresh: The distance tolerance (in meters) around distance_epsilon. + Points within cutting_thresh of distance_epsilon are marked as cutting points. + random_cuts: Number of random cutting points to add for exploration. + Typically 0 for deterministic results. Returns: - The updated trajectory segment with cutting points + A new TrajectorySegment with populated cutting_points list """ cutting_points = [] diff --git a/mappymatch/matchers/lcss/lcss.py b/mappymatch/matchers/lcss/lcss.py index 7647134..007eb41 100644 --- a/mappymatch/matchers/lcss/lcss.py +++ b/mappymatch/matchers/lcss/lcss.py @@ -23,7 +23,9 @@ class LCSSMatcher(MatcherInterface): """ - A map matcher based on the paper: + Map matcher based on the Longest Common Subsequence (LCSS) algorithm. + + This matcher implements the trajectory segmentation approach described in: Zhu, Lei, Jacob R. Holden, and Jeffrey D. Gonder. "Trajectory Segmentation Map-Matching Approach for Large-Scale, @@ -31,13 +33,30 @@ class LCSSMatcher(MatcherInterface): Transportation Research Record: Journal of the Transportation Research Board 2645 (2017): 67-75. + The algorithm works by: + 1. Computing candidate paths through the road network + 2. Scoring path segments using LCSS similarity + 3. Iteratively refining segments by identifying cutting points + 4. Merging segments until similarity threshold is met + Args: - road_map: The road map to use for matching - distance_epsilon: The distance epsilon to use for matching (default: 50 meters) - similarity_cutoff: The similarity cutoff to use for stopping the algorithm (default: 0.9) - cutting_threshold: The distance threshold to use for computing cutting points (default: 10 meters) - random_cuts: The number of random cuts to add at each iteration (default: 0) - distance_threshold: The distance threshold above which no match is made (default: 10000 meters) + road_map: The road network to match against (must implement MapInterface) + distance_epsilon: Maximum distance (in meters) for a GPS point to be considered near a road segment. Points beyond this distance contribute less to similarity. Default is 50 meters. + similarity_cutoff: Minimum similarity score (0-1) to stop iterative refinement. Higher values demand better matching quality. Default is 0.9. + cutting_threshold: Distance threshold (in meters) for identifying potential cutting points where trajectory should be split. Default is 10 meters. + random_cuts: Number of random cutting points to add at each iteration for exploration. Usually 0 for deterministic results. Default is 0. + distance_threshold: Maximum distance (in meters) for matching a point to a road. Points beyond this are left unmatched. Default is 10000 meters (10km). + + Examples: + >>> from mappymatch.matchers.lcss import LCSSMatcher + >>> from mappymatch.maps.nx import NxMap + >>> + >>> # Load a road network + >>> road_map = NxMap.from_file('network.pickle') + >>> + >>> # Create matcher with default parameters + >>> matcher = LCSSMatcher(road_map) + >>> result = matcher.match_trace(trace) """ def __init__( diff --git a/mappymatch/matchers/lcss/ops.py b/mappymatch/matchers/lcss/ops.py index be9d69b..4208190 100644 --- a/mappymatch/matchers/lcss/ops.py +++ b/mappymatch/matchers/lcss/ops.py @@ -22,15 +22,24 @@ def join_segment( road_map: MapInterface, a: TrajectorySegment, b: TrajectorySegment ) -> TrajectorySegment: """ - Join two trajectory segments together, attempting to route between them if needed. + Join two trajectory segments together, routing between them if there's a gap. + + This function concatenates two trajectory segments end-to-end. If the paths don't + connect directly (i.e., the end junction of segment A doesn't match the start junction + of segment B), it attempts to find a shortest path to bridge the gap. + + Used during the LCSS algorithm after splitting segments to recombine them into a + complete trajectory. Args: - road_map: The road map to use for routing - a: The first trajectory segment - b: The second trajectory segment + road_map: The road network used for finding connecting paths + a: The first trajectory segment (will be the start of the combined segment) + b: The second trajectory segment (will be the end of the combined segment) Returns: - A new trajectory segment combining both segments + A new TrajectorySegment with combined traces and paths. If a connecting path + is found, it's inserted between the two path segments. If no path exists + (disconnected network components), the paths are simply concatenated. """ new_traces = a.trace + b.trace new_path = a.path + b.path @@ -66,14 +75,19 @@ def new_path( trace: Trace, ) -> List[Road]: """ - Computes a shortest path and returns the path + Compute a candidate path through the road network for a GPS trace. + + This computes the shortest path from the first coordinate to the last coordinate + in the trace, using the road network's shortest path algorithm. This path serves + as the initial candidate path for LCSS matching. Args: - road_map: the road map to match to - trace: the trace to match + road_map: The road network to route on + trace: The GPS trace to compute a path for Returns: - the path that most closely matches the trace + A list of Road objects representing the shortest path from the trace's first + to last coordinate. Returns an empty list if the trace has fewer than 1 coordinate. """ if len(trace.coords) < 1: return [] @@ -91,17 +105,28 @@ def split_trajectory_segment( trajectory_segment: TrajectorySegment, ) -> List[TrajectorySegment]: """ - Splits a trajectory segment based on the provided cutting points. + Split a trajectory segment at its cutting points into multiple sub-segments. - Merge back any segments that are too short + This is a core operation in the LCSS algorithm. It divides a trajectory at identified + cutting points, computes new candidate paths for each sub-segment, and merges any + resulting segments that are too short (fewer than 2 trace points or 1 path edge). + + The splitting process helps refine matches by allowing different parts of the trajectory + to follow different paths through the network. Args: - road_map: the road map to match to - trajectory_segment: the trajectory segment to split - distance_epsilon: the distance epsilon + road_map: The road network used to compute paths for the new segments + trajectory_segment: The segment to split, must have cutting_points populated Returns: - a list of split segments or the original segment if it can't be split + A list of new TrajectorySegments created by splitting at cutting points. + Returns the original segment unchanged if: + - The trace has fewer than 2 points + - No cutting points are defined + - Splitting would not improve the match + + Short segments (< 2 trace points or < 1 path edge) are automatically merged + with adjacent segments. """ trace = trajectory_segment.trace cutting_points = trajectory_segment.cutting_points @@ -193,13 +218,23 @@ class StationaryIndex(NamedTuple): def find_stationary_points(trace: Trace) -> List[StationaryIndex]: """ - Find the positional index of all stationary points in a trace + Identify groups of consecutive GPS points that represent stationary positions. + + Stationary points occur when a GPS device records multiple positions while not moving, + or moving very slowly. These can be caused by waiting at traffic lights, parking, or + GPS noise. Identifying them allows the LCSS matcher to handle them specially. + + Points are considered stationary if they are within 0.001 meters (1mm) of the previous + point - essentially the same location accounting for floating-point precision. Args: - trace: the trace to find the stationary points in + trace: The GPS trace to analyze for stationary points Returns: - a list of stationary indices + A list of StationaryIndex objects, each representing a group of consecutive + stationary points. Each StationaryIndex contains: + - i_index: List of integer indices in the trace + - c_index: List of coordinate IDs """ f = trace._frame coords = trace.coords @@ -234,14 +269,19 @@ def drop_stationary_points( trace: Trace, stationary_index: List[StationaryIndex] ) -> Trace: """ - Drops stationary points from the trace, keeping the first point + Remove stationary points from a trace while keeping the first point of each group. + + This is used to simplify traces before matching by collapsing groups of stationary + points into single representatives. The LCSS matching is performed on the simplified + trace, then stationary points are restored in the final results. Args: - trace: the trace to drop the stationary points from - stationary_index: the stationary indices to drop + trace: The GPS trace to clean + stationary_index: List of StationaryIndex objects identifying stationary point groups (from find_stationary_points) Returns: - the trace with the stationary points dropped + A new Trace with duplicate stationary points removed. For each stationary group, + only the first point is retained. """ for si in stationary_index: trace = trace.drop(si.c_index[1:]) @@ -254,14 +294,22 @@ def add_matches_for_stationary_points( stationary_index: List[StationaryIndex], ) -> List[Match]: """ - Takes a set of matches and adds duplicate match entries for stationary + Restore stationary points to the matched results. + + After matching a simplified trace (with stationary points removed), this function + adds back Match objects for all the removed stationary points. Each stationary point + gets the same road match as the first point in its group, but retains its original + coordinate ID. + + This ensures the final MatchResult has one match for every point in the original trace. Args: - matches: the matches to add the stationary points to - stationary_index: the stationary indices to add + matches: The matches from matching the simplified trace (without stationary points) + stationary_index: List of StationaryIndex objects identifying which points were removed (from find_stationary_points) Returns: - the matches with the stationary points added + A new list of Match objects with stationary points restored, maintaining the + original trace order and coordinate IDs """ matches = deepcopy(matches) diff --git a/mappymatch/matchers/lcss/utils.py b/mappymatch/matchers/lcss/utils.py index 5e7b3d8..9af3a20 100644 --- a/mappymatch/matchers/lcss/utils.py +++ b/mappymatch/matchers/lcss/utils.py @@ -6,24 +6,18 @@ def forward_merge(merge_list: List, condition: Callable[[Any], bool]) -> List: """ - Helper function to merge items in a list by adding them to the next eligible element. - This merge moves left to right. + Merge items in a list by combining matching items with the next eligible element (left-to-right). - For example, given the list: - - [1, 2, 3, 4, 5] - - And the condition, x < 3, the function yields: - - >>> forward_merge([1,2,3,4,5], lambda x: x < 3) - >>> [6, 4, 5] + This function scans through the list from left to right. When it encounters items that + satisfy the condition, it combines them with the next item that doesn't satisfy the + condition. This is useful for merging small trajectory segments with their neighbors. Args: - merge_list: the list to merge - condition: the merge condition + merge_list: The list of items to merge. Items should support addition (+) for combining. + condition: A function that returns True for items that should be merged forward. Items satisfying this condition will be combined with the next non-matching item. Returns: - a list of the merged items + A new list with matching items merged into subsequent items """ items = [] @@ -51,24 +45,18 @@ def _flatten(ml): def reverse_merge(merge_list: List, condition: Callable[[Any], bool]) -> List: """ - Helper function to merge items in a list by adding them to the next eligible element. - This merge moves right to left. - - For example, given the list: - - [1, 2, 3, 4, 5] - - And the condition, x < 3, the function yields: + Merge items in a list by combining matching items with the previous eligible element (right-to-left). - >>> list(reverse_merge([1,2,3,4,5], lambda x: x < 3)) - >>> [3, 3, 4, 5] + This function scans through the list from right to left. When it encounters items that + satisfy the condition, it combines them with the previous item that doesn't satisfy the + condition. This is the reverse of forward_merge. Args: - merge_list: the list to merge - condition: the merge condition + merge_list: The list of items to merge. Items should support addition (+) for combining. + condition: A function that returns True for items that should be merged backward. Items satisfying this condition will be combined with the previous non-matching item. Returns: - a list of the merged items + A new list with matching items merged into preceding items """ items = [] @@ -97,14 +85,21 @@ def _flatten(ml): def merge(merge_list: List, condition: Callable[[Any], bool]) -> List: """ - Combines the forward and reverse merges to catch edge cases at the tail ends of the list + Merge items in a list using both forward and reverse merging to handle edge cases. + + This function first performs a forward merge, then checks if any items still satisfy + the condition. If so, it performs a reverse merge to handle items at the end of the + list that couldn't be merged forward. + + This two-pass approach ensures that items at both ends of the list can be successfully + merged with their neighbors. Args: - merge_list: the list to merge - condition: the merge condition + merge_list: The list of items to merge. Items should support addition (+) for combining. + condition: A function that returns True for items that should be merged with neighbors. Returns: - a list of the merged items + A new list with all matching items merged into neighbors """ f_merge = forward_merge(merge_list, condition) @@ -116,13 +111,21 @@ def merge(merge_list: List, condition: Callable[[Any], bool]) -> List: def compress(cutting_points: List) -> Generator: """ - Compress a list of cutting points if they happen to be directly adjacent to another + Compress adjacent cutting points by keeping only the middle point of each group. + + When multiple cutting points are directly adjacent (differ by 1 index), this function + collapses them into a single representative cutting point. For each group, the middle + point is selected as the representative. + + This prevents the LCSS algorithm from creating too many tiny segments when several + adjacent points all have poor matches. Args: - cutting_points: the list of cutting points + cutting_points: A list of CuttingPoint objects to compress - Returns: - a generator of compressed cutting points + Yields: + CuttingPoint objects: One representative cutting point for each group of + adjacent cutting points. The middle point of each group is selected. """ sorted_cuts = sorted(cutting_points, key=lambda c: c.trace_index) for k, g in groupby(enumerate(sorted_cuts), lambda x: x[0] - x[1].trace_index): diff --git a/mappymatch/matchers/line_snap.py b/mappymatch/matchers/line_snap.py index 9a7aaeb..1cd3b40 100644 --- a/mappymatch/matchers/line_snap.py +++ b/mappymatch/matchers/line_snap.py @@ -11,16 +11,81 @@ class LineSnapMatcher(MatcherInterface): """ - A crude (but fast) map matcher that just snaps points to the nearest road network link. + A simple, fast map matcher that snaps each GPS point to the nearest road segment. + + This is the most basic matching approach - it independently matches each GPS coordinate + to its nearest road without considering the overall trajectory or road network topology. + While fast and simple to implement, it may produce unrealistic results where the matched + path jumps between disconnected roads. + + Use this matcher when: + - You need very fast matching performance + - GPS data is very accurate and roads are well-separated + - You only need point-to-road snapping, not path reconstruction + + For more sophisticated matching that considers path continuity and network topology, + use LCSSMatcher, ValhallaMatcher, or OsrmMatcher instead. + + Args: + road_map: The road network to match against (must implement MapInterface) Attributes: - map: The map to match against + map: The road network being used for matching + + Examples: + >>> from mappymatch.matchers.line_snap import LineSnapMatcher + >>> from mappymatch.maps.nx import NxMap + >>> + >>> # Load a road network + >>> road_map = NxMap.from_file('network.pickle') + >>> + >>> # Create the matcher + >>> matcher = LineSnapMatcher(road_map) + >>> result = matcher.match_trace(trace) + >>> + >>> # Each point is matched independently + >>> for match in result.matches: + ... print(f"Distance to road: {match.distance}m") """ def __init__(self, road_map: MapInterface): self.map = road_map def match_trace(self, trace: Trace) -> MatchResult: + """ + Match a GPS trace by snapping each point to its nearest road. + + This performs simple nearest-neighbor matching without considering trajectory + continuity or network topology. Each GPS coordinate is independently matched + to the spatially nearest road segment. + + Args: + trace: The GPS trace to match. Should be in the same CRS as the road map. + + Returns: + A MatchResult containing: + - matches: List of Match objects, one per GPS point + - path: None (this matcher doesn't compute paths) + + Examples: + >>> from mappymatch.matchers.line_snap import LineSnapMatcher + >>> from mappymatch.maps.nx import NxMap + >>> + >>> road_map = NxMap.from_file('network.gpickle') + >>> matcher = LineSnapMatcher(road_map) + >>> + >>> trace = Trace.from_csv('gps_data.csv') + >>> result = matcher.match_trace(trace) + >>> + >>> # Check matching quality + >>> distances = [m.distance for m in result.matches] + >>> avg_dist = sum(distances) / len(distances) + >>> print(f"Average distance to road: {avg_dist:.2f}m") + >>> + >>> # Find poorly matched points + >>> poor_matches = [m for m in result.matches if m.distance > 50] + >>> print(f"Points > 50m from road: {len(poor_matches)}") + """ matches = [] for coord in trace.coords: diff --git a/mappymatch/matchers/match_result.py b/mappymatch/matchers/match_result.py index dfb4954..e91edbf 100644 --- a/mappymatch/matchers/match_result.py +++ b/mappymatch/matchers/match_result.py @@ -26,7 +26,26 @@ def crs(self): def matches_to_geodataframe(self) -> gpd.GeoDataFrame: """ - Returns a geodataframe with all the coordinates and their resulting match (or NA if no match) in each row + Convert the matching results to a GeoDataFrame with point geometries. + + Each row represents one GPS coordinate and its matched road (or NA if no match). + The CRS of the GeoDataFrame matches the CRS of the input trace. + + Returns: + A GeoDataFrame with columns including: + - geometry: The original coordinate point geometries + - coordinate_id: The ID of each coordinate + - road_id: The ID of the matched road (or NA) + - distance_to_road: The distance to the matched road (or NA) + - Additional columns from road metadata if available + + Examples: + >>> result = matcher.match_trace(trace) + >>> gdf = result.matches_to_geodataframe() + >>> # Save to file + >>> gdf.to_file('matches.geojson', driver='GeoJSON') + >>> # Filter to successful matches + >>> matched = gdf[gdf['road_id'].notna()] """ df = self.matches_to_dataframe() gdf = gpd.GeoDataFrame(df, geometry="geom") @@ -40,10 +59,24 @@ def matches_to_geodataframe(self) -> gpd.GeoDataFrame: def matches_to_dataframe(self) -> pd.DataFrame: """ - Returns a dataframe with all the coordinates and their resulting match (or NA if no match) in each row. + Convert the matching results to a pandas DataFrame. + + Similar to matches_to_geodataframe but without spatial functionality. Each row + represents one GPS coordinate and its matched road. Returns: - A pandas dataframe + A pandas DataFrame with columns including: + - coordinate_id: The ID of each coordinate + - road_id: The ID of the matched road (or NaN) + - distance_to_road: The distance to the matched road (or NaN) + - Additional columns from road metadata if available + + Examples: + >>> result = matcher.match_trace(trace) + >>> df = result.matches_to_dataframe() + >>> # Calculate matching statistics + >>> match_rate = df['road_id'].notna().sum() / len(df) + >>> avg_distance = df['distance_to_road'].mean() """ df = pd.DataFrame([m.to_flat_dict() for m in self.matches]) df = df.fillna(np.nan) @@ -52,11 +85,21 @@ def matches_to_dataframe(self) -> pd.DataFrame: def path_to_dataframe(self) -> pd.DataFrame: """ - Returns a dataframe with the resulting estimated trace path through the road network. - The dataframe is empty if there was no path. + Convert the matched path to a pandas DataFrame. + + The path represents the estimated route through the road network. If no path + was computed (path is None), returns an empty DataFrame. Returns: - A pandas dataframe + A pandas DataFrame where each row represents one road segment in the path. + Contains road IDs, geometries, and metadata. Returns empty DataFrame if no path exists. + + Examples: + >>> result = matcher.match_trace(trace) + >>> if result.path is not None: + ... path_df = result.path_to_dataframe() + ... print(f"Route has {len(path_df)} road segments") + ... total_length = path_df['length_miles'].sum() """ if self.path is None: return pd.DataFrame() @@ -68,11 +111,24 @@ def path_to_dataframe(self) -> pd.DataFrame: def path_to_geodataframe(self) -> gpd.GeoDataFrame: """ - Returns a geodataframe with the resulting estimated trace path through the road network. - The geodataframe is empty if there was no path. + Convert the matched path to a GeoDataFrame with LineString geometries. + + The path represents the estimated route through the road network. If no path + was computed (path is None), returns an empty GeoDataFrame. Returns: - A geopandas geodataframe + A GeoDataFrame where each row represents one road segment with its LineString geometry. + Contains road IDs, geometries, and metadata. Returns empty GeoDataFrame if no path exists. + The CRS matches the input trace CRS. + + Examples: + >>> result = matcher.match_trace(trace) + >>> if result.path is not None: + ... path_gdf = result.path_to_geodataframe() + ... # Save the matched route + ... path_gdf.to_file('matched_route.geojson', driver='GeoJSON') + ... # Visualize with matplotlib + ... path_gdf.plot() """ if self.path is None: return gpd.GeoDataFrame() diff --git a/mappymatch/matchers/matcher_interface.py b/mappymatch/matchers/matcher_interface.py index 23b953f..ca8ad1e 100644 --- a/mappymatch/matchers/matcher_interface.py +++ b/mappymatch/matchers/matcher_interface.py @@ -6,17 +6,44 @@ class MatcherInterface(metaclass=ABCMeta): """ - Abstract base class for a Matcher + Abstract base class defining the interface for all map-matching algorithms. + + All map matchers in mappymatch implement this interface, providing a consistent API + for matching GPS trajectories to road networks. Subclasses must implement the + match_trace method to perform the actual matching algorithm. + + Examples: + >>> from mappymatch.matchers.lcss import LCSSMatcher + >>> from mappymatch.matchers.valhalla import ValhallaMatcher + >>> + >>> # All matchers follow the same interface + >>> matcher = LCSSMatcher(road_map) + >>> result = matcher.match_trace(trace) """ @abstractmethod def match_trace(self, trace: Trace) -> MatchResult: """ - Take in a trace of gps points and return a list of matching link ids + Match a GPS trace to the underlying road network. + + This is the primary method of any matcher. It takes a sequence of GPS coordinates + and returns matching results that link each coordinate to road segments. Args: - trace: The trace to match + trace: A Trace object containing the GPS coordinates to match Returns: - A list of Match objects + A MatchResult containing: + - matches: A list of Match objects linking each GPS point to a road + - path: An optional list of Road objects representing the matched route + + Examples: + >>> matcher = SomeMatcher(road_map) + >>> trace = Trace.from_csv('gps_data.csv') + >>> result = matcher.match_trace(trace) + >>> + >>> # Access the matches + >>> for match in result.matches: + ... if match.road is not None: + ... print(f"Matched to road {match.road.road_id}") """ diff --git a/mappymatch/matchers/osrm.py b/mappymatch/matchers/osrm.py index a33085b..8fb9bc2 100644 --- a/mappymatch/matchers/osrm.py +++ b/mappymatch/matchers/osrm.py @@ -18,11 +18,24 @@ def parse_osrm_json(j: dict, trace: Trace) -> list[Match]: """ - parse the json response from the osrm match service + Parse the JSON response from the OSRM match service into Match objects. - :param j: the json object + Extracts matching information from the OSRM response and creates Match objects + linking GPS coordinates to road segments (represented by OSM node IDs). - :return: a list of matches + Args: + j: The JSON response dictionary from OSRM's match endpoint + trace: The original GPS trace that was matched + + Returns: + A list of Match objects, one for each coordinate in the trace + + Raises: + ValueError: If the response is missing required fields (matchings, legs, annotations, or nodes) + + Note: + Currently the geometry and distance information from OSRM is not fully utilized. + This is a TODO for future improvement. """ matchings = j.get("matchings") if not matchings: @@ -56,7 +69,40 @@ def _parse_leg(d: dict, i: int) -> Match: class OsrmMatcher(MatcherInterface): """ - pings an OSRM server for map matching + Map matcher that uses an OSRM server for matching GPS traces to OpenStreetMap data. + + OSRM (Open Source Routing Machine) is a routing engine for OpenStreetMap data. + This matcher sends GPS coordinates to an OSRM match endpoint and receives back + matched road segments based on OSM node IDs. + + The matcher communicates with OSRM's match API, which snaps GPS points to the + nearest roads in the OSM network. + + Args: + osrm_address: The base URL of the OSRM server. Default is the public OSRM demo server (not recommended for production use). + osrm_profile: The routing profile to use ('driving', 'walking', 'cycling', etc.). Default is 'driving'. + osrm_version: The OSRM API version. Default is 'v1'. + + Attributes: + osrm_api_base: The constructed OSRM API endpoint URL + + Examples: + >>> from mappymatch.matchers.osrm import OsrmMatcher + >>> + >>> # Use default public OSRM server (for testing only) + >>> matcher = OsrmMatcher() + >>> result = matcher.match_trace(trace) + >>> + >>> # Use your own OSRM instance + >>> matcher = OsrmMatcher( + ... osrm_address='http://localhost:5000', + ... osrm_profile='cycling' + ... ) + + Note: + - Traces must be in WGS84 (EPSG:4326) coordinate system + - Traces are automatically downsampled to 100 points if longer + - The public demo server is rate-limited; use your own instance for production """ def __init__( @@ -70,6 +116,33 @@ def __init__( ) def match_trace(self, trace: Trace) -> MatchResult: + """ + Match a GPS trace to roads using the OSRM map matching service. + + This method sends the trace to an OSRM server for map matching against OpenStreetMap data. + The trace must be in EPSG:4326 (lat/lon). If the trace has more than 100 points, + it's automatically downsampled to meet OSRM's typical API limits. + + Args: + trace: The GPS trace to match. Must be in EPSG:4326 (lat/lon). + + Returns: + A MatchResult containing matches for each GPS point. Note that the current + implementation has limited geometry/distance information from OSRM. + + Raises: + TypeError: If the trace is not in EPSG:4326 + requests.HTTPError: If the OSRM server returns an error response + + Examples: + >>> matcher = OsrmMatcher() + >>> trace = Trace.from_csv('gps_data.csv', xy=False) # Keep in lat/lon + >>> result = matcher.match_trace(trace) + >>> + >>> # Long traces are automatically downsampled + >>> long_trace = Trace.from_gpx('long_journey.gpx') # 500 points + >>> result = matcher.match_trace(long_trace) # Downsampled to 100 points + """ if not trace.crs == LATLON_CRS: raise TypeError( f"this matcher requires traces to be in the CRS of EPSG:{LATLON_CRS.to_epsg()} " diff --git a/mappymatch/matchers/valhalla.py b/mappymatch/matchers/valhalla.py index cdc0106..bf5558b 100644 --- a/mappymatch/matchers/valhalla.py +++ b/mappymatch/matchers/valhalla.py @@ -40,7 +40,19 @@ def build_path_from_result( edges: List[dict], shape: List[Tuple[float, float]] ) -> List[Road]: """ - builds a mappymatch path from the result of a Valhalla map matching request + Build a list of Road objects from Valhalla map matching response. + + This parses the 'edges' array from a Valhalla trace_attributes response and converts + each edge into a Road object with geometry and metadata. + + Args: + edges: List of edge dictionaries from the Valhalla response, containing + way_id, begin_shape_index, end_shape_index, speed, length, etc. + shape: List of (lon, lat) coordinate tuples representing the matched path, + decoded from Valhalla's polyline + + Returns: + A list of Road objects representing the matched path through the network """ path = [] for edge in edges: @@ -71,7 +83,18 @@ def build_match_result( trace: Trace, matched_points: List[dict], path: List[Road] ) -> MatchResult: """ - builds a mappymatch MatchResult from the result of a Valhalla map matching request + Build a MatchResult from Valhalla map matching response data. + + This combines the Valhalla matched_points array with the parsed path to create + Match objects linking each GPS coordinate to its matched road segment. + + Args: + trace: The original GPS trace that was submitted for matching + matched_points: List of matched point dictionaries from Valhalla response, containing edge_index and distance_from_trace_point for each GPS point + path: List of Road objects representing the matched route (from build_path_from_result) + + Returns: + A MatchResult containing matches for each coordinate and the full path """ matches = [] for i, coord in enumerate(trace.coords): @@ -98,7 +121,52 @@ def build_match_result( class ValhallaMatcher(MatcherInterface): """ - pings a Valhalla server for map matching + Map matcher that uses a Valhalla server for matching GPS traces to road networks. + + Valhalla is an open-source routing engine that provides map matching capabilities. + This matcher sends GPS coordinates to a Valhalla server and receives back matched + road segments and routing results. + + The matcher communicates with Valhalla's trace_attributes API endpoint, which returns + detailed information about matched edges including geometry, speed, and length. + + Args: + valhalla_url: The base URL of the Valhalla trace_attributes endpoint. + Default is a public demo server (not for production use). + cost_model: The routing cost model to use ('auto', 'bicycle', 'pedestrian', etc.). + Default is 'auto'. + shape_match: The shape matching algorithm ('map_snap', 'edge_walk', 'walk_or_snap'). + Default is 'map_snap'. + attributes: Additional edge attributes to request from Valhalla beyond the required ones. + Default includes 'edge.length' and 'edge.speed'. + + Attributes: + url_base: The Valhalla API endpoint URL + cost_model: The routing cost model being used + shape_match: The shape matching algorithm being used + attributes: List of all requested edge attributes (required + additional) + + Examples: + >>> from mappymatch.matchers.valhalla import ValhallaMatcher + >>> + >>> # Use default demo server (for testing only) + >>> matcher = ValhallaMatcher() + >>> result = matcher.match_trace(trace) + >>> + >>> # Use your own Valhalla instance + >>> matcher = ValhallaMatcher( + ... valhalla_url='http://localhost:8002/trace_attributes', + ... cost_model='bicycle' + ... ) + >>> + >>> # Request additional attributes + >>> matcher = ValhallaMatcher( + ... attributes=['edge.length', 'edge.speed', 'edge.names', 'edge.surface'] + ... ) + + Note: + The default demo server is rate-limited and should only be used for testing. + For production use, deploy your own Valhalla instance. """ def __init__( @@ -116,6 +184,38 @@ def __init__( self.attributes = all_attributes def match_trace(self, trace: Trace) -> MatchResult: + """ + Match a GPS trace to roads using the Valhalla map matching service. + + This method sends the trace to a Valhalla server, which performs map matching + and returns the matched path and statistics. The trace is automatically converted + to EPSG:4326 (lat/lon) if needed, as required by Valhalla. + + Args: + trace: The GPS trace to match. Will be converted to EPSG:4326 if in a different CRS. + + Returns: + A MatchResult containing: + - matches: List of Match objects linking each GPS point to a road + - path: List of Road objects representing the matched route + + Raises: + requests.HTTPError: If the Valhalla server returns an error response + + Examples: + >>> matcher = ValhallaMatcher() + >>> trace = Trace.from_csv('gps_data.csv') + >>> result = matcher.match_trace(trace) + >>> + >>> # Access results + >>> print(f"Matched {len(result.matches)} points") + >>> print(f"Path has {len(result.path)} road segments") + >>> + >>> # Check road metadata from Valhalla + >>> for road in result.path[:5]: + ... print(f"Speed: {road.metadata['speed_mph']} mph") + ... print(f"Length: {road.metadata['length_miles']} miles") + """ if not trace.crs == LATLON_CRS: trace = trace.to_crs(LATLON_CRS) diff --git a/mappymatch/utils/crs.py b/mappymatch/utils/crs.py index ba3fa23..0eb14ed 100644 --- a/mappymatch/utils/crs.py +++ b/mappymatch/utils/crs.py @@ -1,4 +1,18 @@ +"""Coordinate Reference System (CRS) constants used throughout mappymatch. + +This module defines the standard CRS objects used for geographic transformations: +- LATLON_CRS: WGS84 geographic coordinates (EPSG:4326) +- XY_CRS: Web Mercator projected coordinates (EPSG:3857) +""" + from pyproj import CRS +# WGS84 latitude/longitude coordinate system (EPSG:4326) +# Standard GPS coordinates in decimal degrees +# Range: latitude [-90, 90], longitude [-180, 180] LATLON_CRS = CRS(4326) + +# Web Mercator projected coordinate system (EPSG:3857) +# Used for web mapping and accurate distance calculations in meters +# Coordinates are in meters (easting, northing) XY_CRS = CRS(3857) diff --git a/mappymatch/utils/geo.py b/mappymatch/utils/geo.py index 2bcfd62..8160093 100644 --- a/mappymatch/utils/geo.py +++ b/mappymatch/utils/geo.py @@ -8,14 +8,25 @@ def xy_to_latlon(x: float, y: float) -> Tuple[float, float]: """ - Tramsform x,y coordinates to lat and lon + Transform Web Mercator (EPSG:3857) coordinates to WGS84 latitude/longitude. + + This function converts from the projected coordinate system commonly used for + web mapping (meters, sometimes called "xy" coordinates) to standard geographic + coordinates (degrees). Args: - x: X. - y: Y. + x: The x-coordinate (easting) in Web Mercator projection (meters) + y: The y-coordinate (northing) in Web Mercator projection (meters) Returns: - Transformed lat and lon as lat, lon. + A tuple of (latitude, longitude) in decimal degrees (WGS84/EPSG:4326) + + Examples: + >>> # New York City in Web Mercator + >>> x, y = -8238310.4, 4970241.3 + >>> lat, lon = xy_to_latlon(x, y) + >>> print(f"Lat: {lat:.4f}, Lon: {lon:.4f}") + Lat: 40.7128, Lon: -74.0060 """ transformer = Transformer.from_crs(XY_CRS, LATLON_CRS) lat, lon = transformer.transform(x, y) @@ -25,14 +36,25 @@ def xy_to_latlon(x: float, y: float) -> Tuple[float, float]: def latlon_to_xy(lat: float, lon: float) -> Tuple[float, float]: """ - Tramsform lat,lon coordinates to x and y. + Transform WGS84 latitude/longitude to Web Mercator (EPSG:3857) coordinates. + + This function converts from standard geographic coordinates (degrees) to the + projected coordinate system commonly used for web mapping and distance calculations + (meters, sometimes called "xy" coordinates). Args: - lat: The latitude. - lon: The longitude. + lat: The latitude in decimal degrees (range: -90 to 90) + lon: The longitude in decimal degrees (range: -180 to 180) Returns: - Transformed x and y as x, y. + A tuple of (x, y) in Web Mercator projection meters (EPSG:3857) + + Examples: + >>> # New York City coordinates + >>> lat, lon = 40.7128, -74.0060 + >>> x, y = latlon_to_xy(lat, lon) + >>> print(f"X: {x:.1f}m, Y: {y:.1f}m") + X: -8238310.4m, Y: 4970241.3m """ transformer = Transformer.from_crs(LATLON_CRS, XY_CRS) x, y = transformer.transform(lat, lon) @@ -42,14 +64,31 @@ def latlon_to_xy(lat: float, lon: float) -> Tuple[float, float]: def coord_to_coord_dist(a: Coordinate, b: Coordinate) -> float: """ - Compute the distance between two coordinates. + Calculate the Euclidean distance between two coordinates. + + The distance is computed using the geometries' coordinate reference system. + For accurate distance measurements, coordinates should be in a projected CRS + (like EPSG:3857) rather than lat/lon (EPSG:4326). Args: a: The first coordinate - b: The second coordinate + b: The second coordinate. Must be in the same CRS as coordinate a. Returns: - The distance in meters + The Euclidean distance in the units of the coordinates' CRS (typically meters + if using a projected CRS like EPSG:3857) + + Note: + For coordinates in lat/lon (EPSG:4326), this computes angular distance in degrees, + not actual ground distance. Convert to a projected CRS first for accurate distances. + + Examples: + >>> from mappymatch.constructs.coordinate import Coordinate + >>> # Create coordinates in Web Mercator (meters) + >>> coord1 = Coordinate.from_lat_lon(40.7128, -74.0060).to_crs('EPSG:3857') + >>> coord2 = Coordinate.from_lat_lon(40.7589, -73.9851).to_crs('EPSG:3857') + >>> distance = coord_to_coord_dist(coord1, coord2) + >>> print(f"Distance: {distance:.1f} meters") """ dist = a.geom.distance(b.geom) diff --git a/mappymatch/utils/keys.py b/mappymatch/utils/keys.py index 819954d..5631dd5 100644 --- a/mappymatch/utils/keys.py +++ b/mappymatch/utils/keys.py @@ -1,3 +1,14 @@ +"""Standard attribute key names used for NetworkX graph data structures. + +These constants define the dictionary keys used to store road network data in NetworkX graphs. +Using consistent keys ensures compatibility between different map readers and the NxMap class. +""" + +# Key for storing road geometry (LineString) in edge data dictionaries DEFAULT_GEOMETRY_KEY = "geometry" + +# Key for storing additional metadata (OSM tags, custom attributes) in edge data dictionaries DEFAULT_METADATA_KEY = "metadata" + +# Key for storing the CRS (Coordinate Reference System) in graph.graph dictionary DEFAULT_CRS_KEY = "crs" diff --git a/mappymatch/utils/process_trace.py b/mappymatch/utils/process_trace.py index b1a57e0..2d8be8b 100644 --- a/mappymatch/utils/process_trace.py +++ b/mappymatch/utils/process_trace.py @@ -5,14 +5,41 @@ def split_large_trace(trace: Trace, ideal_size: int) -> list[Trace]: """ - Split up a trace into a list of smaller traces. + Split a large GPS trace into smaller sub-traces of approximately equal size. + + This is useful for processing very long GPS trajectories that might be too large + for certain matching algorithms or API limits. The trace is divided into segments + of the specified size, with intelligent handling of the final segment to avoid + creating very small trailing segments. Args: - trace: the trace to split. - ideal_size: the target number of coordinates for each new trace. + trace: The GPS trace to split + ideal_size: The target number of coordinates for each sub-trace. Must be greater than 0. Returns: - A list of split traces. + A list of Trace objects. If the trace is already smaller than ideal_size, + returns a list containing just the original trace. Otherwise, returns multiple + traces of approximately ideal_size coordinates each. + + Raises: + ValueError: If ideal_size is 0 or negative + + Note: + If the final segment would be 10 points or fewer, it's merged with the previous + segment to avoid creating very small traces. + + Examples: + >>> # Split a 500-point trace into ~100-point chunks + >>> long_trace = Trace.from_csv('long_journey.csv') # 500 points + >>> sub_traces = split_large_trace(long_trace, ideal_size=100) + >>> print(f"Split into {len(sub_traces)} traces") # 5 traces + >>> for i, t in enumerate(sub_traces): + ... print(f"Trace {i}: {len(t)} points") + >>> + >>> # Small traces are returned unchanged + >>> small_trace = Trace.from_csv('short_trip.csv') # 50 points + >>> result = split_large_trace(small_trace, ideal_size=100) + >>> assert len(result) == 1 # Just the original trace """ if ideal_size == 0: raise ValueError("ideal_size must be greater than 0") @@ -32,16 +59,31 @@ def split_large_trace(trace: Trace, ideal_size: int) -> list[Trace]: def remove_bad_start_from_trace(trace: Trace, distance_threshold: float) -> Trace: """ - Remove points at the beginning of a trace if it is a gap is too big. + Remove leading points from a trace if there's a large gap at the beginning. - Too big is defined by distance threshold. + This function detects and removes GPS points at the start of a trace that are + separated by unusually large distances, which often indicates GPS initialization + errors or teleportation artifacts. It scans from the beginning until it finds a + pair of consecutive points within the distance threshold. Args: - trace: The trace. - distance_threshold: The distance threshold. + trace: The GPS trace to clean + distance_threshold: The maximum acceptable distance (in the trace's CRS units, typically meters) between consecutive points. Points separated by more than this distance at the start are removed. Returns: - The new trace. + A new Trace with leading outlier points removed. If no large gaps are found, + returns a trace equivalent to the original. + + Examples: + >>> # Remove points with gaps > 500 meters at the start + >>> trace = Trace.from_gpx('gps_track.gpx') + >>> cleaned = remove_bad_start_from_trace(trace, distance_threshold=500) + >>> print(f"Removed {len(trace) - len(cleaned)} leading points") + >>> + >>> # Common use case: remove GPS initialization errors + >>> # GPS might record (0, 0) or last known position before getting fix + >>> trace = Trace.from_csv('raw_gps.csv') + >>> trace = remove_bad_start_from_trace(trace, distance_threshold=1000) """ def _trim_frame(frame): diff --git a/mappymatch/utils/url.py b/mappymatch/utils/url.py index f76056e..423e6d8 100644 --- a/mappymatch/utils/url.py +++ b/mappymatch/utils/url.py @@ -10,13 +10,28 @@ def _parse_uri(uri: str) -> str: def multiurljoin(urls: List[str]) -> str: """ - Make a url from uri's. + Join multiple URL components into a single URL. + + This function combines a list of URL path components, ensuring proper forward + slash separators between components. Each component is normalized to end with + a slash before joining. Args: - urls: list of uri + urls: A list of URL components to join. Can include protocol, domain, and path segments. + Example: ['http://localhost:5000', 'api', 'v1', 'match'] Returns: - Url as uri/uri/... + A complete URL with all components joined using forward slashes. + Example: 'http://localhost:5000/api/v1/match/' + + Examples: + >>> multiurljoin(['http://localhost:5000', 'api', 'v1', 'match']) + 'http://localhost:5000/api/v1/match/' + >>> + >>> # Build OSRM API endpoint + >>> base = 'http://router.project-osrm.org' + >>> endpoint = multiurljoin([base, 'match', 'v1', 'driving']) + >>> print(endpoint) # 'http://router.project-osrm.org/match/v1/driving/' """ parsed_urls = [_parse_uri(uri) for uri in urls] return reduce(urljoin, parsed_urls) From 218af00b2390a03de98eb4dc3ea7a578d6910408 Mon Sep 17 00:00:00 2001 From: Nicholas Reinicke Date: Wed, 18 Feb 2026 15:12:00 -0700 Subject: [PATCH 4/4] run example in check --- pixi.lock | 2 +- pyproject.toml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pixi.lock b/pixi.lock index 4d05e94..acea46b 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1429,7 +1429,7 @@ packages: - pypi: ./ name: mappymatch version: 0.7.1 - sha256: 72e671a048b98445eef7be2a372bb09a50aa5b84ca082a2d226c15403dd41121 + sha256: e504a892be5b6396c3a2a341950038aa4ec9cdf83580e46ff65ac03e04ab751c requires_dist: - folium>=0.20,<1 - geopandas>=1,<2 diff --git a/pyproject.toml b/pyproject.toml index d1e6652..988413a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,8 +124,9 @@ fmt_fix = "ruff format" lint_fix = "ruff check --fix" typing = "mypy ." test = "pytest tests/" +examples = "python docs/examples/lcss_example.py" convert_notebooks = "python docs/examples/_convert_examples_to_notebooks.py" docs = { depends-on = [ "convert_notebooks", ], cmd = "cd docs/ && jupyter-book build ." } -check = { depends-on = ["fmt_fix", "lint_fix", "typing", "test"] } +check = { depends-on = ["fmt_fix", "lint_fix", "typing", "test", "examples"] }