├── .fig ├── demo.png ├── map_matching.png ├── map_matching_futian.png ├── map_matching_futian_with_satellite.png ├── observ_prob_distribution.png └── v大于1的情况.png ├── .gitignore ├── LICENSE ├── README.md ├── bug.py ├── changelog.md ├── data ├── network │ └── LXD_graph.ckpt └── trajs │ ├── gt.json │ ├── traj_0.geojson │ ├── traj_1.geojson │ ├── traj_10.geojson │ ├── traj_11.geojson │ ├── traj_12.geojson │ ├── traj_13.geojson │ ├── traj_14.geojson │ ├── traj_15.geojson │ ├── traj_2.geojson │ ├── traj_3.geojson │ ├── traj_4.geojson │ ├── traj_5.geojson │ ├── traj_6.geojson │ ├── traj_7.geojson │ ├── traj_8.geojson │ └── traj_9.geojson ├── demo.py ├── docs └── API.md ├── eval.py ├── mapmatching ├── __init__.py ├── geo │ ├── __init__.py │ ├── azimuth.py │ ├── coord │ │ ├── __init__.py │ │ ├── coordTransform_py.py │ │ └── coordTransfrom_shp.py │ ├── io.py │ ├── metric │ │ ├── __init__.py │ │ └── trajDist.py │ ├── ops │ │ ├── __init__.py │ │ ├── distance.py │ │ ├── linear_referencing.py │ │ ├── point2line.py │ │ ├── resample.py │ │ ├── simplify.py │ │ ├── substring.py │ │ └── to_array.py │ ├── query.py │ └── vis │ │ ├── __init__.py │ │ ├── linestring.py │ │ └── point.py ├── graph │ ├── __init__.py │ ├── astar.py │ ├── base.py │ ├── bi_astar.py │ ├── geograph.py │ └── geographx.py ├── match │ ├── __int__.py │ ├── candidatesGraph.py │ ├── dir_similarity.py │ ├── geometricAnalysis.py │ ├── io.py │ ├── metric.py │ ├── misc.py │ ├── postprocess.py │ ├── spatialAnalysis.py │ ├── status.py │ ├── temporalAnalysis.py │ ├── topologicalAnalysis.py │ ├── visualization.py │ └── viterbi.py ├── matching.py ├── osmnet │ ├── __init__.py │ ├── build_graph.py │ ├── combine_edges.py │ ├── downloader.py │ ├── misc.py │ ├── osm_io.py │ ├── parse_osm_xml.py │ └── twoway_edge.py ├── setting.py ├── update_network.py └── utils │ ├── __init__.py │ ├── db.py │ ├── img.py │ ├── interval_helper.py │ ├── log_helper.py │ ├── logger_helper.py │ ├── misc.py │ ├── parallel_helper.py │ ├── serialization.py │ └── timer.py ├── requirement.txt └── test.py /.fig/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wenke727/ST-MapMatching/2b88c219142cfc1d1460669027798538ee0b2ad0/.fig/demo.png -------------------------------------------------------------------------------- /.fig/map_matching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wenke727/ST-MapMatching/2b88c219142cfc1d1460669027798538ee0b2ad0/.fig/map_matching.png -------------------------------------------------------------------------------- /.fig/map_matching_futian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wenke727/ST-MapMatching/2b88c219142cfc1d1460669027798538ee0b2ad0/.fig/map_matching_futian.png -------------------------------------------------------------------------------- /.fig/map_matching_futian_with_satellite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wenke727/ST-MapMatching/2b88c219142cfc1d1460669027798538ee0b2ad0/.fig/map_matching_futian_with_satellite.png -------------------------------------------------------------------------------- /.fig/observ_prob_distribution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wenke727/ST-MapMatching/2b88c219142cfc1d1460669027798538ee0b2ad0/.fig/observ_prob_distribution.png -------------------------------------------------------------------------------- /.fig/v大于1的情况.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wenke727/ST-MapMatching/2b88c219142cfc1d1460669027798538ee0b2ad0/.fig/v大于1的情况.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | network/ 2 | result/ 3 | data/ 4 | check.py 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *.pkl 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Others 16 | .DS_Store 17 | debug 18 | cache 19 | log 20 | 21 | # Distribution / packaging 22 | .vscode/ 23 | test/ 24 | tmp/ 25 | api/ 26 | rsrc 27 | .Python 28 | build/ 29 | develop-eggs/ 30 | dist/ 31 | downloads/ 32 | eggs/ 33 | .eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | wheels/ 40 | pip-wheel-metadata/ 41 | share/python-wheels/ 42 | *.egg-info/ 43 | .installed.cfg 44 | *.egg 45 | MANIFEST 46 | 47 | # PyInstaller 48 | # Usually these files are written by a python script from a template 49 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 50 | *.manifest 51 | *.spec 52 | 53 | # Installer logs 54 | pip-log.txt 55 | pip-delete-this-directory.txt 56 | 57 | # Unit test / coverage reports 58 | htmlcov/ 59 | .tox/ 60 | .nox/ 61 | .coverage 62 | .coverage.* 63 | .cache 64 | nosetests.xml 65 | coverage.xml 66 | *.cover 67 | *.py,cover 68 | .hypothesis/ 69 | .pytest_cache/ 70 | 71 | # Translations 72 | *.mo 73 | *.pot 74 | 75 | # Django stuff: 76 | *.log 77 | local_settings.py 78 | db.sqlite3 79 | db.sqlite3-journal 80 | 81 | # Flask stuff: 82 | instance/ 83 | .webassets-cache 84 | 85 | # Scrapy stuff: 86 | .scrapy 87 | 88 | # Sphinx documentation 89 | docs/_build/ 90 | 91 | # PyBuilder 92 | target/ 93 | 94 | # Jupyter Notebook 95 | .ipynb_checkpoints 96 | 97 | # IPython 98 | profile_default/ 99 | ipython_config.py 100 | 101 | # pyenv 102 | .python-version 103 | 104 | # pipenv 105 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 106 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 107 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 108 | # install all needed dependencies. 109 | #Pipfile.lock 110 | 111 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 112 | __pypackages__/ 113 | 114 | # Celery stuff 115 | celerybeat-schedule 116 | celerybeat.pid 117 | 118 | # SageMath parsed files 119 | *.sage.py 120 | 121 | # Environments 122 | .env 123 | .venv 124 | env/ 125 | venv/ 126 | ENV/ 127 | env.bak/ 128 | venv.bak/ 129 | 130 | # Spyder project settings 131 | .spyderproject 132 | .spyproject 133 | 134 | # Rope project settings 135 | .ropeproject 136 | 137 | # mkdocs documentation 138 | /site 139 | 140 | # mypy 141 | .mypy_cache/ 142 | .dmypy.json 143 | dmypy.json 144 | 145 | # Pyre type checker 146 | .pyre/ 147 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 wenke727 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ST-MapMatching 2 | 3 | ## 版本 4 | 5 | V2.0.0 6 | 7 | ## 描述 8 | 9 | 基于时间和空间特性的全局地图匹配算法(ST-Matching),一种针对低采样率的GPS轨迹的新颖全局地图匹配算法。算法的基础路网信息源为 [OSM](https://wiki.openstreetmap.org/wiki/Main_Page),可通过`DigraphOSM`自动下载。算法匹配过程考虑以下两个方面特征: 10 | 11 | 1. 道路网络的空间几何和拓扑结构 12 | 13 | 2. 轨迹的速度/时间约束。基于时空分析,构建候选图,从中确定最佳匹配路径。 14 | 15 | 输入WGS坐标系的`GPS轨迹点集`,输出途径的路段; 16 | 17 | 本算法为 MSRA《[Map-Matching for Low-Sampling-Rate GPS Trajectories](https://www.microsoft.com/en-us/research/publication/map-matching-for-low-sampling-rate-gps-trajectories/)》的复现,并根据自己的认识有一些改动,中文解读可参考 [CSDN文章](https://blog.csdn.net/qq_43281895/article/details/103145327)。 18 | 19 | ## 调用说明 20 | 21 | 详见 `demo.py` 22 | 23 | ```python 24 | from mapmatching import build_geograph, ST_Matching 25 | 26 | """step 1: 获取/加载路网""" 27 | # 方法1: 28 | # 根据 bbox 从 OSM 下载路网,从头解析获得路网数据 29 | # net = build_geograph(bbox=[113.930914, 22.570536, 113.945456, 22.585613], 30 | # xml_fn="./data/network/LXD.osm.xml", ll=False) 31 | # 将预处理路网保存为 ckpt 32 | # net.save_checkpoint('./data/network/LXD_graph.ckpt') 33 | 34 | # 方法2: 35 | # 使用预处理路网 36 | net = build_geograph(ckpt='./data/network/LXD_graph.ckpt') 37 | 38 | """step 2: 创建地图匹配 matcher""" 39 | matcher = ST_Matching(net=net, ll=False) 40 | 41 | """step 3: 加载轨迹点集合,以打石一路为例""" 42 | idx = 4 43 | traj = matcher.load_points(f"./data/trajs/traj_{idx}.geojson").reset_index(drop=True) 44 | res = matcher.matching(traj, top_k=5, dir_trans=True, details=False, plot=True, 45 | simplify=True, debug_in_levels=False) 46 | 47 | # 后续步骤可按需选择 48 | """step 4: 将轨迹点映射到匹配道路上""" 49 | path = matcher.transform_res_2_path(res, ori_crs=True) 50 | proj_traj = matcher.project(traj, path) 51 | 52 | """step 5: eval""" 53 | matcher.eval(traj, res, resample=5, eps=10) 54 | ``` 55 | 56 | ### 输入示例 57 | 58 | ```json 59 | { 60 | "type": "FeatureCollection", 61 | "name": "traj_debug_dashiyilu_0", 62 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 63 | "features": [ 64 | { "type": "Feature", "properties": {"geometry": { "type": "Point", "coordinates": [ 113.931956598012064, 22.575930582940785 ] } }}, 65 | { "type": "Feature", "properties": {"geometry": { "type": "Point", "coordinates": [ 113.932515057750763, 22.575632036146079 ] } }}, 66 | { "type": "Feature", "properties": {"geometry": { "type": "Point", "coordinates": [ 113.932920306714124, 22.575490522559665 ] } }}, 67 | { "type": "Feature", "properties": {"geometry": { "type": "Point", "coordinates": [ 113.933781789624888, 22.575346314537452 ] } }}, 68 | { "type": "Feature", "properties": {"geometry": { "type": "Point", "coordinates": [ 113.943190113338488, 22.575121559997108 ] } }}, 69 | { "type": "Feature", "properties": {"geometry": { "type": "Point", "coordinates": [ 113.943816093693101, 22.575196482404341 ] } }} 70 | ] 71 | } 72 | 73 | ``` 74 | 75 | 注: 76 | 77 | 1. 示例输入对应`./data/trajs/traj_4.geojson`,其中 `geometry` 为唯一需要提供的字段,在`vscode`中可借助插件`Geo Data Viewer`可视化; 78 | 2. 输入轨迹点的坐标系默认为 `wgs84`, `gcj02` 的轨迹需在调用函数`load_points`明确坐标系`in_sys='gcj'`; 79 | 3. 提供的预处理路网仅覆盖深圳南山区万科云城片区,并没有完成覆盖`./data/trajs`中所有的测试用例。 若需测试所有用例,需自行调整 bbox 获取相应区域的路网。 80 | 81 | ### 输出示例 82 | 83 | #### demo 输出 84 | 85 | ```python 86 | { 87 | # 输出状态码,0 为正常输出 88 | 'status': 0, 89 | # 匹配路段 index 90 | 'epath': [123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135], 91 | # 第一条路被通过的比例(即第一条路上, 第一个轨迹点及之后的部分的占比) 92 | 'step_0': 0.7286440473726905, 93 | # 最后一条路被通过的比例(即最后一条路上, 最后一个轨迹点及之前的部分的占比) 94 | 'step_n': 0.8915310605450645, 95 | # 概率 96 | 'probs': { 97 | 'prob': 0.9457396931471692, 98 | 'norm_prob': 0.9861498301181256, 99 | 'dist_prob': 0.9946361835772438, 100 | 'trans_prob': 0.9880031610906268, 101 | 'dir_prob': 0.9933312073337599} 102 | } 103 | ``` 104 | 105 | 可视化效果如下: 106 | 107 | ![](.fig/demo.png) 108 | 109 | - matcher.matching 将 plot 参数设置为 True 110 | - 瓦片地图,需要安装 [Tilemap](https://github.com/wenke727/TileMap) 111 | 112 | #### 其他地图匹配效果 113 | 114 | `./data/trajs/traj_0.geojson` 匹配效果 115 | 116 | 117 | ![](.fig/map_matching_futian.png) 118 | 119 | ## 环境安装 120 | 121 | 详见 requirement.txt, 建议`geopandas`使用conda安装 122 | 123 | ```bash 124 | conda create -n stmm python=3.9 125 | conda activate stmm 126 | conda install -c conda-forge geopandas==0.12.2 127 | pip install -r requirement.txt 128 | ``` 129 | 130 | -------------------------------------------------------------------------------- /bug.py: -------------------------------------------------------------------------------- 1 | # %% 2 | import pandas as pd 3 | import geopandas as gpd 4 | from mapmatching import build_geograph, ST_Matching 5 | 6 | pd.set_option('display.width', 5000) # 打印结果不换行方法 7 | pd.set_option('display.max_rows', 500) 8 | 9 | # %% 10 | net = build_geograph(ckpt='./data/network/GZ_test.ckpt') 11 | matcher = ST_Matching(net=net) 12 | 13 | traj = gpd.read_file('./data/traj_others.geojson').set_crs(epsg=4326) 14 | res = matcher.matching(traj, top_k=8, search_radius=80, plot=True, 15 | dir_trans=False, details=True, simplify=False, debug_in_levels=True) 16 | graph = res['details']['graph'] 17 | res['details']['steps'].query('trans_prob < .85') 18 | 19 | # %% 20 | -------------------------------------------------------------------------------- /data/network/LXD_graph.ckpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wenke727/ST-MapMatching/2b88c219142cfc1d1460669027798538ee0b2ad0/data/network/LXD_graph.ckpt -------------------------------------------------------------------------------- /data/trajs/gt.json: -------------------------------------------------------------------------------- 1 | { 2 | "traj_0.geojson": [ 3 | 1491845271, 1491845278, 499265789, 499281499, 499281680, 499256522, 499374765, 499374692, 499374699, 7232025515, 499374438, 2491091193, 7959990771, 1982884838, 7959990781, 7959990780, 7232047153, 5834799311, 7959603120, 7959602896, 7959603265, 7959603254, 7959603255, 7959603111, 7959603029, 7959602931, 7959602934, 7963887479, 7959602935, 7959603012, 7959603013, 9867711844, 7959602961, 499482004, 2750667738, 7959590815, 499478781, 2525944467, 2525990691, 2525990692, 499543123, 2750667601, 7961465922, 2750667627, 5834799167, 499543165, 7965680795, 5271707993, 4848290283, 2750593369, 277673161, 277052181, 2750593322, 8444060575, 8076307463, 499542953, 5834799144, 7959603245, 7959932863, 7959932864, 5834799157, 7959932862, 7959932869, 6496420812, 7959990662, 7959990663, 6496420788, 7959932876, 6033482523, 7959990664, 7959990665, 7959932880, 499681331, 499681324, 5435020958, 499681326, 500020999, 7707208812, 7640452829], 4 | "traj_1.geojson": [ 5 | 7959602916, 7959990653, 7959990662, 7959603239, 7959602899, 7959602919, 7959603232, 7959603209, 7263135412, 6496420768, 7959603216, 7959603210, 7959590851, 2044316564, 7959603170, 7959603096, 7959590857, 7959603272, 7959603093], 6 | "traj_2.geojson": [ 7 | 8526860927, 8526860929, 8526860961], 8 | "traj_3.geojson": [ 9 | 8526860977, 8526860891], 10 | "traj_4.geojson": [ 11 | 7834079836, 8526860922, 5345110208, 8526860926, 8526860927, 8526860977, 8526860891, 9908986643, 8526861026, 8526860998, 5345110822, 8526861014, 8526861012, 5140241022], 12 | "traj_5.geojson": [ 13 | 8526860966, 8526860961], 14 | "traj_6.geojson": [ 15 | 10121421919, 8526861038], 16 | "traj_7.geojson": [ 17 | 2508061907, 4044798340], 18 | "traj_8.geojson": [ 19 | 10121421924, 8526861072, 5179129482, 8526861079], 20 | "traj_9.geojson": [ 21 | 500016494, 7959990625, 7959990623, 7241618417, 7959990538, 7959990621, 7959990622, 7959990546, 7959990558, 7959990556, 7249081512, 6033481332, 7959990534, 8109971622, 8109971643, 8109971632, 4397491519, 4397491540, 499374672, 1114538640, 1114538642, 499256255, 1116467143, 6410193855, 1116467144, 6410193851, 499237147, 499237159, 9730051941, 1491845135, 499237230, 9671934765], 22 | "traj_10.geojson": [ 23 | 7959990732, 6496420992], 24 | "traj_11.geojson": [ 25 | 499543283, 6302207410, 6467166907, 2525990702, 6467166929, 499478538, 1116501297, 7973099538, 1116492767, 7973099584, 267602472, 7973099537, 7973114863, 7899265523, 8298779513, 8298792534, 7973099533], 26 | "traj_12.geojson": [ 27 | 8169270272, 2376751183, 2376751145, 8168061649, 8168061760, 8168061648, 8169270272, 2376751183], 28 | "traj_13.geojson": [ 29 | 2750593369, 277673161, 277052181, 2750593322, 8444060575], 30 | "traj_14.geojson": [ 31 | 2508061907, 4044798340, 2508061873], 32 | "traj_15.geojson": [ 33 | 10121421923, 8526861038, 2366083151, 8526860966, 5345110197, 8526860929, 8526860928, 8526860926, 8526860916, 8526860846, 9144224473, 8526860847, 8526860884, 5345110215, 2508090891, 5569752402, 5569752372, 1981097845, 2366083157, 2366083173, 2366083171, 6072476402, 277486223, 277486228, 277939196, 277486222, 277663457, 2701105203, 2702591034, 2701105231, 2701105309, 277049979, 1116420116, 5445976849, 5445976847, 793893699, 1116420144, 277664219, 2427779668, 279077760, 277673550, 277664239, 1932007679, 2407737640, 277664224, 277664226, 6366992734, 2132634054, 2132634188, 6366992731, 2291907903, 1169606344, 2433356711, 6465619119, 2403175276, 2403189538, 9527226880, 9527226879, 8442017240, 9527231181, 2467373573, 2467373546, 277323315] 34 | } -------------------------------------------------------------------------------- /data/trajs/traj_0.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "name": "trips", 4 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 5 | "features": [ 6 | { "type": "Feature", "properties": { "id": null }, "geometry": { "type": "Point", "coordinates": [ 114.042192099217814, 22.530825799254831 ] } }, 7 | { "type": "Feature", "properties": { "id": null }, "geometry": { "type": "Point", "coordinates": [ 114.048087551857591, 22.53141414915628 ] } }, 8 | { "type": "Feature", "properties": { "id": null }, "geometry": { "type": "Point", "coordinates": [ 114.050457097022772, 22.530254493344991 ] } }, 9 | { "type": "Feature", "properties": { "id": null }, "geometry": { "type": "Point", "coordinates": [ 114.051374300525396, 22.534269663922935 ] } }, 10 | { "type": "Feature", "properties": { "id": null }, "geometry": { "type": "Point", "coordinates": [ 114.050237176637481, 22.537490331019249 ] } }, 11 | { "type": "Feature", "properties": { "id": null }, "geometry": { "type": "Point", "coordinates": [ 114.044716748650771, 22.537863550640491 ] } }, 12 | { "type": "Feature", "properties": { "id": null }, "geometry": { "type": "Point", "coordinates": [ 114.046725298091147, 22.542379323865038 ] } }, 13 | { "type": "Feature", "properties": { "id": null }, "geometry": { "type": "Point", "coordinates": [ 114.056957680637467, 22.542526131019244 ] } }, 14 | { "type": "Feature", "properties": { "id": null }, "geometry": { "type": "Point", "coordinates": [ 114.058074914718418, 22.537513356219687 ] } }, 15 | { "type": "Feature", "properties": { "id": null }, "geometry": { "type": "Point", "coordinates": [ 114.058331080637473, 22.531227627019256 ] } }, 16 | { "type": "Feature", "properties": { "id": null }, "geometry": { "type": "Point", "coordinates": [ 114.062977233476687, 22.529223325030639 ] } } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /data/trajs/traj_1.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 4 | "features": [ 5 | { "type": "Feature", "properties": { "x": 114.063323, "y": 22.534902, "time": "2019\/08\/04 09:54:56", "speed": 23, "direction": 133, "event": 5, "alarmCode": null, "altitude": 0, "mileage": 0, "errorCode": 0, "plate": "粤B*****Y", "point_in_p": 1, "t": 9.9155555555555566, "time_inter": 0.0041666666666650004, "x1": 114.061877, "y1": 22.534855, "dis": 0.148603264545345, "v": 35.664783490900007 }, "geometry": { "type": "Point", "coordinates": [ 114.058204, 22.537611 ] } }, 6 | { "type": "Feature", "properties": { "x": 114.061877, "y": 22.534855, "time": "2019\/08\/04 09:55:11", "speed": 41, "direction": 134, "event": 5, "alarmCode": null, "altitude": 0, "mileage": 0, "errorCode": 0, "plate": "粤B*****Y", "point_in_p": 1, "t": 9.9197222222222212, "time_inter": 0.0041666666666659996, "x1": 114.061367, "y1": 22.534857, "dis": 0.052379998046211997, "v": 12.57119953109156 }, "geometry": { "type": "Point", "coordinates": [ 114.056759, 22.537566 ] } }, 7 | { "type": "Feature", "properties": { "x": 114.061367, "y": 22.534857, "time": "2019\/08\/04 09:55:41", "speed": 0, "direction": 133, "event": 5, "alarmCode": null, "altitude": 0, "mileage": 0, "errorCode": 0, "plate": "粤B*****Y", "point_in_p": 1, "t": 9.9280555555555541, "time_inter": 0.21666666666666701, "x1": 114.055378, "y1": 22.537428, "dis": 0.67828441441240395, "v": 3.1305434511341681 }, "geometry": { "type": "Point", "coordinates": [ 114.05625, 22.537569 ] } }, 8 | { "type": "Feature", "properties": { "x": 114.058723, "y": 22.536897, "time": "2019\/08\/04 19:28:41", "speed": 18, "direction": 137, "event": 5, "alarmCode": null, "altitude": 0, "mileage": 3623, "errorCode": 0, "plate": "粤B*****Y", "point_in_p": 1, "t": 19.478055555555553, "time_inter": 0.016666666666669001, "x1": 114.05634, "y1": 22.536877, "dis": 0.24475240643540599, "v": 14.685144386122037 }, "geometry": { "type": "Point", "coordinates": [ 114.053608, 22.539613 ] } }, 9 | { "type": "Feature", "properties": { "x": 114.05634, "y": 22.536877, "time": "2019\/08\/04 19:29:41", "speed": 14, "direction": 174, "event": 5, "alarmCode": null, "altitude": 0, "mileage": 3625, "errorCode": 0, "plate": "粤B*****Y", "point_in_p": 1, "t": 19.494722222222222, "time_inter": 0.022222222222222001, "x1": 114.057492, "y1": 22.538428, "dis": 0.209145443690229, "v": 9.4115449660603616 }, "geometry": { "type": "Point", "coordinates": [ 114.051228, 22.539597 ] } } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /data/trajs/traj_10.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 4 | "features": [ 5 | { "type": "Feature", "properties": { "PID": "09005700121902181123005192D", "DIR": 88, "Order": 0, "Type": "street", "X": 1269779986, "Y": 255987452, "RID": "d957e2-8486-e315-b325-e0a0a0", "MoveDir": 179, "dir_sim": 0.97600402622262106, "revert": false, "lane_num": 3.0 }, "geometry": { "type": "Point", "coordinates": [ 114.053374, 22.536374 ] } }, 6 | { "type": "Feature", "properties": { "PID": "09005700121902181122585292D", "DIR": 0, "Order": 1, "Type": "street", "X": 1269779987, "Y": 255989066, "RID": "d957e2-8486-e315-b325-e0a0a0", "MoveDir": 179, "dir_sim": null, "revert": false, "lane_num": 3.0 }, "geometry": { "type": "Point", "coordinates": [ 114.053374, 22.536509 ] } } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /data/trajs/traj_11.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 4 | "features": [ 5 | { "type": "Feature", "properties": { "index": 0, "PID": "09005700122003271208461303O", "DIR": 268, "Order": 0, "Type": "street", "X": 1269695445, "Y": 256061732, "RID": "24fd43-b288-813c-b717-c8f6f8", "pid_order": 0 }, "geometry": { "type": "Point", "coordinates": [ 114.045793, 22.542549 ] } }, 6 | { "type": "Feature", "properties": { "index": 31, "PID": "09005700122003271403302783O", "DIR": 268, "Order": 12, "Type": "street", "X": 1269649100, "Y": 256061810, "RID": "852936-8486-e315-b324-e0a043", "pid_order": 31 }, "geometry": { "type": "Point", "coordinates": [ 114.041645, 22.542523 ] } }, 7 | { "type": "Feature", "properties": { "index": 32, "PID": "09005700122003271403312563O", "DIR": 268, "Order": 0, "Type": "street", "X": 1269647736, "Y": 256061778, "RID": "d9faab-09d2-a493-b92a-06ce71", "pid_order": 32 }, "geometry": { "type": "Point", "coordinates": [ 114.041523, 22.54252 ] } }, 8 | { "type": "Feature", "properties": { "index": 33, "PID": "09005700122003271209165313O", "DIR": 268, "Order": 0, "Type": "street", "X": 1269647619, "Y": 256061094, "RID": "4cf167-c86e-f803-6b33-f71d66", "pid_order": 33 }, "geometry": { "type": "Point", "coordinates": [ 114.041513, 22.542463 ] } }, 9 | { "type": "Feature", "properties": { "index": 48, "PID": "09005700122003271209342533O", "DIR": 262, "Order": 4, "Type": "street", "X": 1269628967, "Y": 256059848, "RID": "47e199-f68d-1124-71cb-1b9515", "pid_order": 48 }, "geometry": { "type": "Point", "coordinates": [ 114.039844, 22.542342 ] } }, 10 | { "type": "Feature", "properties": { "index": 49, "PID": "09005700122003271209360523O", "DIR": 261, "Order": 5, "Type": "street", "X": 1269627178, "Y": 256059601, "RID": "47e199-f68d-1124-71cb-1b9515", "pid_order": 49 }, "geometry": { "type": "Point", "coordinates": [ 114.039684, 22.54232 ] } }, 11 | { "type": "Feature", "properties": { "index": 50, "PID": "09005700122003271209371303O", "DIR": 260, "Order": 0, "Type": "street", "X": 1269625955, "Y": 256059427, "RID": "543ddf-f5a6-16e9-6440-54d70f", "pid_order": 50 }, "geometry": { "type": "Point", "coordinates": [ 114.039574, 22.542305 ] } }, 12 | { "type": "Feature", "properties": { "index": 85, "PID": "09005700122003271211104943O", "DIR": 259, "Order": 10, "Type": "street", "X": 1269583400, "Y": 256052227, "RID": "6e0ac7-4562-19d2-b192-114c14", "pid_order": 85 }, "geometry": { "type": "Point", "coordinates": [ 114.035769, 22.541659 ] } }, 13 | { "type": "Feature", "properties": { "index": 86, "PID": "09005700122003271211116973O", "DIR": 259, "Order": 0, "Type": "street", "X": 1269582291, "Y": 256052044, "RID": "dc6a1f-5d1b-dd32-f0f7-e73be1", "pid_order": 86 }, "geometry": { "type": "Point", "coordinates": [ 114.035671, 22.541642 ] } }, 14 | { "type": "Feature", "properties": { "index": 87, "PID": "09005700122003271211130883O", "DIR": 259, "Order": 0, "Type": "street", "X": 1269580961, "Y": 256051828, "RID": "c8b1bb-e6c4-b2b3-b83f-9a5efd", "pid_order": 87 }, "geometry": { "type": "Point", "coordinates": [ 114.035551, 22.541622 ] } } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /data/trajs/traj_12.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 4 | "features": [ 5 | { "type": "Feature", "properties": { "PID": "09005700121709121245083628V", "DIR": 246, "Order": 0, "Type": "street", "X": 1269866362, "Y": 255849901, "RID": "778ee8-ce04-d8b0-f08c-1a89bc" }, "geometry": { "type": "Point", "coordinates": [ 114.061128, 22.524865 ] } }, 6 | { "type": "Feature", "properties": { "PID": "09005700121709121245113878V", "DIR": 257, "Order": 0, "Type": "street", "X": 1269865552, "Y": 255849631, "RID": "a524cd-7cfc-5679-4e04-82c2c0" }, "geometry": { "type": "Point", "coordinates": [ 114.061055, 22.524843 ] } }, 7 | { "type": "Feature", "properties": { "PID": "09005700121709121245134728V", "DIR": 257, "Order": 1, "Type": "street", "X": 1269864750, "Y": 255849442, "RID": "a524cd-7cfc-5679-4e04-82c2c0" }, "geometry": { "type": "Point", "coordinates": [ 114.060983, 22.524828 ] } }, 8 | { "type": "Feature", "properties": { "PID": "09005700121709121245155908V", "DIR": 254, "Order": 2, "Type": "street", "X": 1269863866, "Y": 255849239, "RID": "a524cd-7cfc-5679-4e04-82c2c0" }, "geometry": { "type": "Point", "coordinates": [ 114.060904, 22.524812 ] } }, 9 | { "type": "Feature", "properties": { "PID": "09005700121709121245176388V", "DIR": 255, "Order": 3, "Type": "street", "X": 1269862966, "Y": 255848996, "RID": "a524cd-7cfc-5679-4e04-82c2c0" }, "geometry": { "type": "Point", "coordinates": [ 114.060823, 22.524792 ] } }, 10 | { "type": "Feature", "properties": { "PID": "09005700121709121245208858V", "DIR": 254, "Order": 4, "Type": "street", "X": 1269861430, "Y": 255848593, "RID": "a524cd-7cfc-5679-4e04-82c2c0" }, "geometry": { "type": "Point", "coordinates": [ 114.060685, 22.524759 ] } }, 11 | { "type": "Feature", "properties": { "PID": "09005700121709121245237738V", "DIR": 257, "Order": 5, "Type": "street", "X": 1269860030, "Y": 255848200, "RID": "a524cd-7cfc-5679-4e04-82c2c0" }, "geometry": { "type": "Point", "coordinates": [ 114.060559, 22.524727 ] } }, 12 | { "type": "Feature", "properties": { "PID": "09005700121709121245252308V", "DIR": 253, "Order": 6, "Type": "street", "X": 1269859327, "Y": 255847988, "RID": "a524cd-7cfc-5679-4e04-82c2c0" }, "geometry": { "type": "Point", "coordinates": [ 114.060496, 22.52471 ] } }, 13 | { "type": "Feature", "properties": { "PID": "09005700121709121245271368V", "DIR": 257, "Order": 0, "Type": "street", "X": 1269858467, "Y": 255847720, "RID": "b14bfe-8493-fd1d-a9c7-2ba1c3" }, "geometry": { "type": "Point", "coordinates": [ 114.060419, 22.524688 ] } }, 14 | { "type": "Feature", "properties": { "PID": "09005700121709121245295998V", "DIR": 248, "Order": 1, "Type": "street", "X": 1269857382, "Y": 255847366, "RID": "b14bfe-8493-fd1d-a9c7-2ba1c3" }, "geometry": { "type": "Point", "coordinates": [ 114.060321, 22.524659 ] } }, 15 | { "type": "Feature", "properties": { "PID": "09005700121709121245310818V", "DIR": 259, "Order": 0, "Type": "street", "X": 1269856744, "Y": 255847096, "RID": "550a27-40c5-f0d3-5717-a1907d" }, "geometry": { "type": "Point", "coordinates": [ 114.060264, 22.524637 ] } }, 16 | { "type": "Feature", "properties": { "PID": "09005700121709121245338928V", "DIR": 268, "Order": 1, "Type": "street", "X": 1269855959, "Y": 255846855, "RID": "550a27-40c5-f0d3-5717-a1907d" }, "geometry": { "type": "Point", "coordinates": [ 114.060193, 22.524618 ] } } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /data/trajs/traj_13.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 4 | "features": [ 5 | { "type": "Feature", "properties": { "index": 0, "PID": "09005700122003271656163343O", "DIR": 104, "Order": 0, "Type": "street", "X": 1269776083, "Y": 256056993, "RID": "c127c5-0a78-e80e-cf05-fef57d", "pid_order": 0 }, "geometry": { "type": "Point", "coordinates": [ 114.053021, 22.542176 ] } }, 6 | { "type": "Feature", "properties": { "index": 14, "PID": "09005700122003271656337113O", "DIR": 85, "Order": 6, "Type": "street", "X": 1269794417, "Y": 256054175, "RID": "cb7422-27d2-c73b-b682-a12ebd", "pid_order": 14 }, "geometry": { "type": "Point", "coordinates": [ 114.054666, 22.54194 ] } }, 7 | { "type": "Feature", "properties": { "index": 15, "PID": "09005700122003271656348363O", "DIR": 83, "Order": 7, "Type": "street", "X": 1269795653, "Y": 256054268, "RID": "cb7422-27d2-c73b-b682-a12ebd", "pid_order": 15 }, "geometry": { "type": "Point", "coordinates": [ 114.054777, 22.541948 ] } }, 8 | { "type": "Feature", "properties": { "index": 36, "PID": "09005700122003271237374253O", "DIR": 88, "Order": 1, "Type": "street", "X": 1269820336, "Y": 256060903, "RID": "706762-0847-0b0e-0332-10af49", "pid_order": 36 }, "geometry": { "type": "Point", "coordinates": [ 114.056992, 22.542496 ] } }, 9 | { "type": "Feature", "properties": { "index": 37, "PID": "09005700122003271237384253O", "DIR": 91, "Order": 0, "Type": "street", "X": 1269821783, "Y": 256060908, "RID": "fbca6b-289f-05a5-aabf-a822ec", "pid_order": 37 }, "geometry": { "type": "Point", "coordinates": [ 114.057122, 22.542495 ] } } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /data/trajs/traj_14.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { "type": "Feature", "properties": { "index": 0 }, "geometry": { "type": "Point", "coordinates": [ 113.933129, 22.57567 ] } }, 5 | { "type": "Feature", "properties": { "index": 6 }, "geometry": { "type": "Point", "coordinates": [ 113.93361, 22.575661 ] } }, 6 | { "type": "Feature", "properties": { "index": 7 }, "geometry": { "type": "Point", "coordinates": [ 113.933647, 22.575727 ] } }, 7 | { "type": "Feature", "properties": { "index": 20 }, "geometry": { "type": "Point", "coordinates": [ 113.933641, 22.576783 ] } }, 8 | { "type": "Feature", "properties": { "index": 21 }, "geometry": { "type": "Point", "coordinates": [ 113.93364, 22.576839 ] } } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /data/trajs/traj_2.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { "type": "Feature", "properties": { "index": 0 }, "geometry": { "type": "Point", "coordinates": [ 113.934189, 22.575404 ] } }, 5 | { "type": "Feature", "properties": { "index": 1 }, "geometry": { "type": "Point", "coordinates": [ 113.934189, 22.575481 ] } } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /data/trajs/traj_3.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { "type": "Feature", "properties": { "index": 0 }, "geometry": { "type": "Point", "coordinates": [ 113.936943, 22.575324 ] } }, 5 | { "type": "Feature", "properties": { "index": 1 }, "geometry": { "type": "Point", "coordinates": [ 113.936943, 22.575324 ] } } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /data/trajs/traj_4.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "name": "traj_debug_dashiyilu_0", 4 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 5 | "features": [ 6 | { "type": "Feature", "properties": { "index": 0, "PID": "09005700121709091547447799Y", "DIR": 126, "Order": 0, "Type": "street", "X": 1268428106, "Y": 256456350, "RID": "81ce8c-d832-1db9-61dc-ee8b61", "MoveDir": 124, "dir_sim": 0.99964534332822241, "revert": false, "pid_order": 0 }, "geometry": { "type": "Point", "coordinates": [ 113.931956598012064, 22.576130582940785 ] } }, 7 | { "type": "Feature", "properties": { "index": 1, "PID": "1", "DIR": 106, "Order": 2, "Type": "street", "X": 1268436351, "Y": 256452423, "RID": "e03ca1-a0bd-f8ba-e8b6-f42f8f", "MoveDir": 106, "dir_sim": null, "revert": false, "pid_order": 6 }, "geometry": { "type": "Point", "coordinates": [ 113.932515057750763, 22.575632036146079 ] } }, 8 | { "type": "Feature", "properties": { "index": 7, "PID": "09005700121709091547560449Y", "DIR": 106, "Order": 2, "Type": "street", "X": 1268436351, "Y": 256452423, "RID": "e03ca1-a0bd-f8ba-e8b6-f42f8f", "MoveDir": 106, "dir_sim": null, "revert": false, "pid_order": 7 }, "geometry": { "type": "Point", "coordinates": [ 113.932920306714124, 22.575490522559665 ] } }, 9 | { "type": "Feature", "properties": { "index": 15, "PID": "09005700121709091548069209Y", "DIR": 95, "Order": 2, "Type": "street", "X": 1268445444, "Y": 256450532, "RID": "ce6e7e-7263-e9af-a5fb-dc8582", "MoveDir": 95, "dir_sim": null, "revert": false, "pid_order": 15 }, "geometry": { "type": "Point", "coordinates": [ 113.933781789624888, 22.575346314537452 ] } }, 10 | { "type": "Feature", "properties": { "index": 101, "PID": "09005700121709091551224079Y", "DIR": 92, "Order": 4, "Type": "street", "X": 1268551143, "Y": 256448826, "RID": "025131-7415-f096-fb9f-ec2bf0", "MoveDir": 92, "dir_sim": null, "revert": false, "pid_order": 101 }, "geometry": { "type": "Point", "coordinates": [ 113.943190113338488, 22.575121559997108 ] } }, 11 | { "type": "Feature", "properties": { "index": 108, "PID": "09005700121709091551348359Y", "DIR": 0, "Order": 3, "Type": "street", "X": 1268558107, "Y": 256449791, "RID": "690002-535e-1613-f0b2-077d39", "MoveDir": 70, "dir_sim": null, "revert": false, "pid_order": 108 }, "geometry": { "type": "Point", "coordinates": [ 113.943816093693101, 22.575196482404341 ] } } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /data/trajs/traj_5.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { "type": "Feature", "properties": { "index": 0 }, "geometry": { "type": "Point", "coordinates": [ 113.934365, 22.575465 ] } }, 5 | { "type": "Feature", "properties": { "index": 2 }, "geometry": { "type": "Point", "coordinates": [ 113.93425, 22.575567 ] } } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /data/trajs/traj_6.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { "type": "Feature", "properties": { "index": 0 }, "geometry": { "type": "Point", "coordinates": [ 113.937851, 22.575306 ] } }, 5 | { "type": "Feature", "properties": { "index": 8 }, "geometry": { "type": "Point", "coordinates": [ 113.937059, 22.57532 ] } } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /data/trajs/traj_7.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { "type": "Feature", "properties": { "index": 0 }, "geometry": { "type": "Point", "coordinates": [ 113.932763, 22.575714 ] } }, 5 | { "type": "Feature", "properties": { "index": 4 }, "geometry": { "type": "Point", "coordinates": [ 113.933053, 22.575698 ] } } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /data/trajs/traj_8.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { "type": "Feature", "properties": { "index": 0 }, "geometry": { "type": "Point", "coordinates": [ 113.934151, 22.577512 ] } }, 5 | { "type": "Feature", "properties": { "index": 6 }, "geometry": { "type": "Point", "coordinates": [ 113.934144, 22.577979 ] } } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | from mapmatching import build_geograph, ST_Matching 2 | 3 | """step 1: 获取/加载路网""" 4 | # 方法1: 5 | # 根据 bbox 从 OSM 下载路网,从头解析获得路网数据 6 | # net = build_geograph(bbox=[113.930914, 22.570536, 113.945456, 22.585613], 7 | # xml_fn="./data/network/LXD.osm.xml", ll=False, n_jobs=16) 8 | # 将预处理路网保存为 ckpt 9 | # net.save_checkpoint('./data/network/LXD_graph.ckpt') 10 | 11 | # 方法2: 12 | # 使用预处理路网 13 | net = build_geograph(ckpt='./data/network/LXD_graph.ckpt') 14 | # net = build_geograph(ckpt='./data/network/Shenzhen_graph_pygeos.ckpt') 15 | 16 | """step 2: 创建地图匹配 matcher""" 17 | matcher = ST_Matching(net=net, ll=False) 18 | 19 | """step 3: 加载轨迹点集合,以打石一路为例""" 20 | idx = 4 21 | traj = matcher.load_points(f"./data/trajs/traj_{idx}.geojson").reset_index(drop=True) 22 | res = matcher.matching(traj, top_k=5, dir_trans=True, details=False, plot=True, 23 | simplify=True, debug_in_levels=False) 24 | 25 | # 后续步骤可按需选择 26 | """step 4: 将轨迹点映射到匹配道路上""" 27 | path = matcher.transform_res_2_path(res) 28 | proj_traj = matcher.project(traj, path) 29 | 30 | """step 5: eval""" 31 | matcher.eval(traj, res, resample=5, eps=10) 32 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # API 设计文档 2 | 3 | ## 地图匹配模块 4 | 5 | | 模块 | 函数 | 输入 | 输出 | 说明 | 6 | | :-------------------------: | :---------------------------: | :----------------------------------------------------------- | ------------ | ------------------------------------------------------------ | 7 | | *candidate
Graph* | construct_graph | cands
common_attrs
left_attrs
right_attrs
rename_dict | gt | Construct the candiadte graph (level, src, dst) for spatial and temporal analysis.
针对 od 落在同一个 edge 上时,将 od 对调 | 8 | | *geometric Analysis* | _filter_candidate | df_candidates
top_k
pid=‘eid’
edge_keys | df_cands | 过滤cands
1 按照距离顺序排序,并针对每一个道路保留最近的一个路段
2 针对每一个节点,保留 top_k 个记录 | 9 | | | get_k_neigbor_edges | points
edges
top_k
radius
| df_cands | [sindex.query_bulk](https://geopandas.org/en/stable/docs/reference/api/geopandas.sindex.SpatialIndex.query_bulk.html#geopandas.sindex.SpatialIndex.query_bulk),返回的是tree geom 的 整数 index | 10 | | | cal_observ_prob | dist
bias
deviation
normal=True | observe_prob | 正态分布 | 11 | | | project_point_to_line_segment | points
edges
keeps_colsFai | | | 12 | | | analyse_geometric_info | | | | 13 | | *spatial
Analysis* | cal_traj_distance | | | | 14 | | | _move_dir_similarity | | | | 15 | | | _trans_prob | | | | 16 | | | analyse_spatial_info | | | | 17 | | *topological
Analysis* | -- | | | | 18 | | *temporal
Analysis* | cos_similarity | | | | 19 | | *viterbi* | process_viterbi_pipeline | | | | 20 | | *postprocess* | get_path | | | | 21 | | | get_one_step | | | | 22 | | | get_connectors | | | | 23 | | *visualization* | plot_matching | | | | 24 | | | matching_debug_level | | | | 25 | | | matching_debug_subplot | | | | 26 | | | | | | | 27 | 28 | -------------------------------------------------------------------------------- /eval.py: -------------------------------------------------------------------------------- 1 | import json 2 | import numpy as np 3 | from tqdm import tqdm 4 | from pathlib import Path 5 | 6 | from mapmatching import ST_Matching, build_geograph 7 | from mapmatching.setting import DATA_FOLDER 8 | from mapmatching.utils.timer import Timer 9 | 10 | from loguru import logger 11 | 12 | def save_lables(res, fn): 13 | with open(fn, 'w') as f: 14 | json.dump(res, f) 15 | 16 | 17 | def load_labels(fn): 18 | with open(fn, 'r') as f: 19 | _dict = json.load(f) 20 | 21 | _dict = {k:np.array(v) for k, v in _dict.items() } 22 | 23 | return _dict 24 | 25 | 26 | def evaluation(matcher, trajs_folder, debug_folder=None): 27 | trajs = trajs_folder.glob("*.geojson") 28 | gt_fn = trajs_folder / 'gt.json' 29 | labels = load_labels(gt_fn) 30 | 31 | if debug_folder is None: 32 | debug_folder = DATA_FOLDER / "result" 33 | debug_folder.mkdir(exist_ok=True) 34 | 35 | preds = {} 36 | hit = 0 37 | errors = {} 38 | timer = Timer() 39 | timer.start() 40 | 41 | for fn in tqdm(sorted(trajs)): 42 | name = fn.name 43 | traj = matcher.load_points(fn, simplify=False) 44 | save_fn = debug_folder / str(name).replace('geojson', 'jpg') if debug_folder else None 45 | res = matcher.matching(traj, simplify=True, plot=False, dir_trans=True, debug_in_levels=False, save_fn=None) # 46 | # matcher.plot_result(traj, res) 47 | vpath = net.transform_epath_to_vpath(res['epath']) 48 | preds[fn.name] = [int(i) for i in res['epath']] 49 | 50 | if np.array(vpath == labels[name]).all(): 51 | hit += 1 52 | else: 53 | errors[name] = fn 54 | 55 | print(f"Prcision: {hit / (hit + len(errors)) * 100:.1f} %, time cost: {timer.stop():.2f} s") 56 | if len(errors): 57 | print(f"Errors: {errors.keys()}") 58 | 59 | return preds 60 | 61 | 62 | if __name__ == "__main__": 63 | trajs_folder = DATA_FOLDER / "trajs" 64 | 65 | net = build_geograph(ckpt = DATA_FOLDER / 'network/Shenzhen_graph_pygeos.ckpt') 66 | matcher = ST_Matching(net=net) 67 | 68 | preds = evaluation(matcher, trajs_folder, debug_folder=Path("./debug")) 69 | 70 | save_lables(preds, DATA_FOLDER / "trajs/gt_epath.json") 71 | -------------------------------------------------------------------------------- /mapmatching/__init__.py: -------------------------------------------------------------------------------- 1 | from .graph import GeoDigraph 2 | from .utils.timer import Timer, timeit 3 | from .matching import build_geograph, ST_Matching, STATUS 4 | -------------------------------------------------------------------------------- /mapmatching/geo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wenke727/ST-MapMatching/2b88c219142cfc1d1460669027798538ee0b2ad0/mapmatching/geo/__init__.py -------------------------------------------------------------------------------- /mapmatching/geo/azimuth.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | from shapely import wkt 4 | from haversine import haversine, haversine_vector, Unit 5 | from shapely.geometry import Point, LineString 6 | 7 | 8 | def azimuth_diff(a, b, unit='radian'): 9 | """calcaluate the angle diff between two azimuth, the imput unit is `degree`. 10 | Args: 11 | a (float): Unit: degree 12 | b (float): Unit: degree 13 | unit(string): `radian` or `degree` 14 | Returns: 15 | [type]: [description] 16 | """ 17 | assert unit in ['degree', 'radian'] 18 | diff = np.abs(a-b) 19 | 20 | if isinstance(diff, np.ndarray): 21 | diff[diff > 180] = 360 - diff[diff > 180] 22 | else: 23 | if diff > 180: 24 | diff = 360 - diff 25 | 26 | return diff if unit =='degree' else diff * math.pi / 180 27 | 28 | 29 | def azimuthAngle(x1, y1, x2, y2): 30 | """calculate the azimuth angle from (x1, y1) to (x2, y2) 31 | 32 | Args: 33 | x1 (float): [description] 34 | y1 (float): [description] 35 | x2 (float): [description] 36 | y2 (float): [description] 37 | 38 | Returns: 39 | float: The angle in degree. 40 | """ 41 | angle = 0.0 42 | dx, dy = x2 - x1, y2 - y1 43 | 44 | if dx == 0: 45 | angle = math.pi * 0 46 | if y2 == y1 : 47 | angle = 0.0 48 | elif y2 < y1 : 49 | angle = math.pi 50 | elif dy == 0: 51 | angle = 0 52 | if dx > 0: 53 | angle = math.pi / 2.0 54 | else: 55 | angle = math.pi / 2.0 * 3.0 56 | elif x2 > x1 and y2 > y1: 57 | angle = math.atan(dx / dy) 58 | elif x2 > x1 and y2 < y1 : 59 | angle = math.pi / 2 + math.atan(-dy / dx) 60 | elif x2 < x1 and y2 < y1 : 61 | angle = math.pi + math.atan(dx / dy) 62 | elif x2 < x1 and y2 > y1 : 63 | angle = 3.0 * math.pi / 2.0 + math.atan(dy / -dx) 64 | 65 | return angle * 180 / math.pi 66 | 67 | 68 | def azimuthAngle_vector(x1, y1, x2, y2): 69 | angle = 0 70 | dx = x2 - x1 71 | dy = y2 - y1 72 | 73 | ans = np.zeros_like(dx) 74 | 75 | x_euqal = dx == 0 76 | x_smaller = dx < 0 77 | x_bigger = dx > 0 78 | 79 | y_equal = dy == 0 80 | y_smaller = dy < 0 81 | y_bigger = dy > 0 82 | 83 | ans[x_euqal] = 0.0 84 | # ans[dx == 0 and dy == 0] = 0.0 85 | ans[x_euqal & y_smaller ] = np.pi 86 | 87 | ans[y_equal & x_bigger] = np.pi / 2.0 88 | ans[y_equal & x_smaller] = np.pi / 2.0 * 3.0 89 | 90 | ans[x_bigger & y_bigger] = np.arctan(dx[x_bigger & y_bigger] / dy[x_bigger & y_bigger]) 91 | ans[x_bigger & y_smaller] = np.pi / 2.0 \ 92 | + np.arctan(-dy[x_bigger & y_smaller] / dx[x_bigger & y_smaller]) 93 | 94 | ans[x_smaller & y_smaller] = np.pi \ 95 | + np.arctan(dx[x_smaller & y_smaller] / dy[x_smaller & y_smaller]) 96 | ans[x_smaller & y_bigger] = np.pi / 2.0 * 3.0 \ 97 | + np.arctan(dy[x_smaller & y_bigger] / -dx[x_smaller & y_bigger]) 98 | 99 | return ans * 180 / np.pi 100 | 101 | 102 | def azimuth_cos_similarity(angel_0:float, angel_1:float, normal=False): 103 | """Calculate the `cosine similarity` bewteen `angel_0` and `angel_1`. 104 | 105 | Args: 106 | angel_0 (float): Angel 0, unit degree. 107 | angel_1 (float): Angel 1, unit degree. 108 | normal (bool): Normal the cosine similarity from [-1, 1] to [0, 1]. 109 | 110 | Returns: 111 | cos similarity(float): [-1, 1] 112 | """ 113 | 114 | res = np.cos(azimuth_diff(angel_0, angel_1, unit='radian')) 115 | if normal: 116 | res = (res + 1) / 2 117 | 118 | return res 119 | 120 | 121 | def azimuth_cos_distance(angel_0:float, angel_1:float): 122 | """Calculate the `cosine distance` bewteen `angel_0` and `angel_1`. 123 | 124 | Args: 125 | angel_0 (float): Angel 0, unit degree. 126 | angel_1 (float): Angel 1, unit degree. 127 | 128 | Returns: 129 | cos distance(float): [0, 2] 130 | """ 131 | 132 | return 1 - azimuth_cos_similarity(angel_0, angel_1) 133 | 134 | 135 | def cal_linestring_azimuth(geom): 136 | """caculate the azimuth of eahc line segment in a polyline. 137 | 138 | Args: 139 | geom (LineString): The polyline geometry. 140 | 141 | Returns: 142 | [list]: The list of azimuth(unit: degree). 143 | """ 144 | if isinstance(geom, LineString): 145 | coords = np.array(geom.coords) 146 | if isinstance(geom, (list, np.ndarray)): 147 | coords = geom 148 | 149 | seg_angels = azimuthAngle_vector(coords[:-1, 0], coords[:-1, 1], 150 | coords[1:, 0], coords[1:, 1]) 151 | 152 | return seg_angels 153 | 154 | 155 | def cal_points_azimuth(geoms:list): 156 | """caculate the azimuth of a trajectory. 157 | 158 | Args: 159 | geom (LineString): The polyline geometry. 160 | 161 | Returns: 162 | [list]: The list of azimuth (unit: degree). 163 | """ 164 | if not geoms or not geoms[0]: 165 | return None 166 | if not isinstance( geoms[0], Point): 167 | return None 168 | 169 | coords = [ g.coords[0] for g in geoms ] 170 | seg_angels = [azimuthAngle( *coords[i], *coords[i+1] ) for i in range(len(coords)-1) ] 171 | 172 | return seg_angels 173 | 174 | 175 | def cal_linestring_azimuth_cos_dist(geom, head_azimuth, weight=True, offset=1): 176 | if geom is None: 177 | return None 178 | 179 | if isinstance(geom, LineString): 180 | coords = np.array(geom.coords) 181 | elif isinstance(geom, list): 182 | coords = np.array(geom) 183 | elif isinstance(geom, np.ndarray): 184 | coords = geom 185 | else: 186 | assert False, print(type(geom), geom) 187 | 188 | road_angels = cal_linestring_azimuth(coords) 189 | 190 | lst = azimuth_cos_similarity(road_angels, head_azimuth) 191 | if offset: 192 | lst = (lst + 1) / 2 193 | 194 | if not weight: 195 | val = np.mean(lst) 196 | else: 197 | # FIXME: coords 198 | try: 199 | coords = coords[:, ::-1] 200 | weights = haversine_vector(coords[:-1], coords[1:], unit=Unit.METERS) 201 | except: 202 | weights = np.linalg.norm(coords[:-1] - coords[1:], axis=1) 203 | if np.sum(weights) == 0: 204 | val = np.mean(lst) 205 | else: 206 | val = np.average(lst, weights=weights) 207 | 208 | return val 209 | 210 | 211 | def cal_coords_seq_azimuth(coords): 212 | return azimuthAngle_vector(coords[:-1, 0], coords[:-1, 1], 213 | coords[1: , 0], coords[1:, 1]) 214 | 215 | 216 | if __name__ == '__main__': 217 | p0 = wkt.loads('POINT (113.934151 22.577512)') 218 | p1 = wkt.loads('POINT (113.934144 22.577979)') 219 | polyline = wkt.loads('LINESTRING (113.9340705 22.577737, 113.9340788 22.5777828, 113.934093 22.5778236, 113.9341161 22.5778661, 113.934144 22.5779051, 113.934186 22.57795, 113.9342268 22.5779823, 113.9342743 22.5780131, 113.9343212 22.5780352, 113.9343734 22.5780515, 113.9344212 22.5780605, 113.9344796 22.5780669)') 220 | 221 | import matplotlib.pyplot as plt 222 | import geopandas as gpd 223 | gpd.GeoDataFrame({'geometry': [p0, p1, polyline]}).plot() 224 | plt.show() 225 | 226 | angels = azimuthAngle(*p0.coords[0], *p1.coords[0]) 227 | 228 | road_angels = cal_linestring_azimuth(polyline) 229 | head_azimuth = cal_linestring_azimuth(LineString([p0.coords[0], p1.coords[0]])) 230 | 231 | cal_linestring_azimuth_cos_dist(LineString([p0.coords[0], p1.coords[0]]), head_azimuth, True) 232 | # head_azimuth = cal_points_azimuth([p0, p1]) 233 | # head_azimuth = cal_points_azimuth([p1, p0]) 234 | 235 | # azimuth_cos_distance(road_angels, head_azimuth[0]) 236 | 237 | cal_linestring_azimuth_cos_dist(polyline, head_azimuth[0], True) 238 | 239 | cal_linestring_azimuth_cos_dist(polyline, head_azimuth[0], False) 240 | -------------------------------------------------------------------------------- /mapmatching/geo/coord/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wenke727/ST-MapMatching/2b88c219142cfc1d1460669027798538ee0b2ad0/mapmatching/geo/coord/__init__.py -------------------------------------------------------------------------------- /mapmatching/geo/coord/coordTransform_py.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import urllib 4 | import math 5 | 6 | x_pi = 3.14159265358979324 * 3000.0 / 180.0 7 | pi = 3.1415926535897932384626 # π 8 | a = 6378245.0 # 长半轴 9 | ee = 0.00669342162296594323 # 偏心率平方 10 | 11 | 12 | class Geocoding: 13 | def __init__(self, api_key): 14 | self.api_key = api_key 15 | 16 | def geocode(self, address): 17 | """ 18 | 利用高德geocoding服务解析地址获取位置坐标 19 | :param address:需要解析的地址 20 | :return: 21 | """ 22 | geocoding = {'s': 'rsv3', 23 | 'key': self.api_key, 24 | 'city': '全国', 25 | 'address': address} 26 | geocoding = urllib.urlencode(geocoding) 27 | ret = urllib.urlopen("%s?%s" % ("http://restapi.amap.com/v3/geocode/geo", geocoding)) 28 | 29 | if ret.getcode() == 200: 30 | res = ret.read() 31 | json_obj = json.loads(res) 32 | if json_obj['status'] == '1' and int(json_obj['count']) >= 1: 33 | geocodes = json_obj['geocodes'][0] 34 | lng = float(geocodes.get('location').split(',')[0]) 35 | lat = float(geocodes.get('location').split(',')[1]) 36 | return [lng, lat] 37 | else: 38 | return None 39 | else: 40 | return None 41 | 42 | 43 | def gcj02_to_bd09(lng, lat): 44 | """ 45 | 火星坐标系(GCJ-02)转百度坐标系(BD-09) 46 | 谷歌、高德——>百度 47 | :param lng:火星坐标经度 48 | :param lat:火星坐标纬度 49 | :return: 50 | """ 51 | z = math.sqrt(lng * lng + lat * lat) + 0.00002 * math.sin(lat * x_pi) 52 | theta = math.atan2(lat, lng) + 0.000003 * math.cos(lng * x_pi) 53 | bd_lng = z * math.cos(theta) + 0.0065 54 | bd_lat = z * math.sin(theta) + 0.006 55 | return [bd_lng, bd_lat] 56 | 57 | 58 | def bd09_to_gcj02(bd_lon, bd_lat): 59 | """ 60 | 百度坐标系(BD-09)转火星坐标系(GCJ-02) 61 | 百度——>谷歌、高德 62 | :param bd_lat:百度坐标纬度 63 | :param bd_lon:百度坐标经度 64 | :return:转换后的坐标列表形式 65 | """ 66 | x = bd_lon - 0.0065 67 | y = bd_lat - 0.006 68 | z = math.sqrt(x * x + y * y) - 0.00002 * math.sin(y * x_pi) 69 | theta = math.atan2(y, x) - 0.000003 * math.cos(x * x_pi) 70 | gg_lng = z * math.cos(theta) 71 | gg_lat = z * math.sin(theta) 72 | return [gg_lng, gg_lat] 73 | 74 | 75 | def wgs84_to_gcj02(lng, lat): 76 | """ 77 | WGS84转GCJ02(火星坐标系) 78 | :param lng:WGS84坐标系的经度 79 | :param lat:WGS84坐标系的纬度 80 | :return: 81 | """ 82 | if out_of_china(lng, lat): # 判断是否在国内 83 | return [lng, lat] 84 | dlat = _transformlat(lng - 105.0, lat - 35.0) 85 | dlng = _transformlng(lng - 105.0, lat - 35.0) 86 | radlat = lat / 180.0 * pi 87 | magic = math.sin(radlat) 88 | magic = 1 - ee * magic * magic 89 | sqrtmagic = math.sqrt(magic) 90 | dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi) 91 | dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi) 92 | mglat = lat + dlat 93 | mglng = lng + dlng 94 | return [mglng, mglat] 95 | 96 | 97 | def gcj02_to_wgs84(lng, lat): 98 | """ 99 | GCJ02(火星坐标系)转GPS84 100 | :param lng:火星坐标系的经度 101 | :param lat:火星坐标系纬度 102 | :return: 103 | """ 104 | if out_of_china(lng, lat): 105 | return [lng, lat] 106 | dlat = _transformlat(lng - 105.0, lat - 35.0) 107 | dlng = _transformlng(lng - 105.0, lat - 35.0) 108 | radlat = lat / 180.0 * pi 109 | magic = math.sin(radlat) 110 | magic = 1 - ee * magic * magic 111 | sqrtmagic = math.sqrt(magic) 112 | dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi) 113 | dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi) 114 | mglat = lat + dlat 115 | mglng = lng + dlng 116 | return [lng * 2 - mglng, lat * 2 - mglat] 117 | 118 | 119 | def bd09_to_wgs84(bd_lon, bd_lat): 120 | lon, lat = bd09_to_gcj02(bd_lon, bd_lat) 121 | return gcj02_to_wgs84(lon, lat) 122 | 123 | 124 | def wgs84_to_bd09(lon, lat): 125 | lon, lat = wgs84_to_gcj02(lon, lat) 126 | return gcj02_to_bd09(lon, lat) 127 | 128 | 129 | def _transformlat(lng, lat): 130 | ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + \ 131 | 0.1 * lng * lat + 0.2 * math.sqrt(math.fabs(lng)) 132 | ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 * 133 | math.sin(2.0 * lng * pi)) * 2.0 / 3.0 134 | ret += (20.0 * math.sin(lat * pi) + 40.0 * 135 | math.sin(lat / 3.0 * pi)) * 2.0 / 3.0 136 | ret += (160.0 * math.sin(lat / 12.0 * pi) + 320 * 137 | math.sin(lat * pi / 30.0)) * 2.0 / 3.0 138 | return ret 139 | 140 | 141 | def _transformlng(lng, lat): 142 | ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + \ 143 | 0.1 * lng * lat + 0.1 * math.sqrt(math.fabs(lng)) 144 | ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 * 145 | math.sin(2.0 * lng * pi)) * 2.0 / 3.0 146 | ret += (20.0 * math.sin(lng * pi) + 40.0 * 147 | math.sin(lng / 3.0 * pi)) * 2.0 / 3.0 148 | ret += (150.0 * math.sin(lng / 12.0 * pi) + 300.0 * 149 | math.sin(lng / 30.0 * pi)) * 2.0 / 3.0 150 | return ret 151 | 152 | 153 | def out_of_china(lng, lat): 154 | """ 155 | 判断是否在国内,不在国内不做偏移 156 | :param lng: 157 | :param lat: 158 | :return: 159 | """ 160 | return not (lng > 73.66 and lng < 135.05 and lat > 3.86 and lat < 53.55) 161 | 162 | 163 | if __name__ == '__main__': 164 | lng = 128.543 165 | lat = 37.065 166 | result1 = gcj02_to_bd09(lng, lat) 167 | result2 = bd09_to_gcj02(lng, lat) 168 | result3 = wgs84_to_gcj02(lng, lat) 169 | result4 = gcj02_to_wgs84(lng, lat) 170 | result5 = bd09_to_wgs84(lng, lat) 171 | result6 = wgs84_to_bd09(lng, lat) 172 | 173 | g = Geocoding('API_KEY') # 这里填写你的高德api的key 174 | result7 = g.geocode('北京市朝阳区朝阳公园') 175 | print(result1, result2, result3, result4, result5, result6, result7) 176 | -------------------------------------------------------------------------------- /mapmatching/geo/coord/coordTransfrom_shp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import geopandas as gpd 4 | 5 | from shapely.geometry import Point, LineString, Polygon, MultiPolygon 6 | from . import coordTransform_py as ct 7 | 8 | 9 | def polyline_wgs_to_gcj(gdf): 10 | ''' 11 | transfer the shapfile coordination system 12 | ''' 13 | gdf['geometry'] = gdf.apply(lambda i: LineString(pd.DataFrame(i.geometry.coords.xy).T.rename( 14 | columns={0: 'x', 1: 'y'}).apply(lambda x: ct.wgs84_to_gcj02(x.x, x.y), axis=1)), axis=1) 15 | return gdf 16 | 17 | 18 | def polyline_gcj_to_wgs(gdf): 19 | ''' 20 | transfer the shapfile coordination system 21 | ''' 22 | gdf['geometry'] = gdf.apply(lambda i: LineString(pd.DataFrame(i.geometry.coords.xy).T.rename( 23 | columns={0: 'x', 1: 'y'}).apply(lambda x: ct.gcj02_to_wgs84(x.x, x.y), axis=1)), axis=1) 24 | return gdf 25 | 26 | 27 | # new function 28 | def gdf_wgs_to_gcj(gdf): 29 | ''' 30 | transfer the shapfile coordination system 31 | ''' 32 | if isinstance(gdf.iloc[0].geometry, Polygon): 33 | gdf['geometry'] = gdf.apply(lambda i: Polygon(pd.DataFrame(i.geometry.exterior.coords.xy).T.rename( 34 | columns={0: 'x', 1: 'y'}).apply(lambda x: ct.wgs84_to_gcj02(x.x, x.y), axis=1)), axis=1) 35 | elif isinstance(gdf.iloc[0].geometry, LineString): 36 | gdf['geometry'] = gdf.apply(lambda i: LineString(pd.DataFrame(i.geometry.coords.xy).T.rename( 37 | columns={0: 'x', 1: 'y'}).apply(lambda x: ct.wgs84_to_gcj02(x.x, x.y), axis=1)), axis=1) 38 | elif isinstance(gdf.iloc[0].geometry, MultiPolygon): 39 | gdf['geometry'] = gdf.geometry.apply(lambda item: MultiPolygon([Polygon(pd.DataFrame(geom.exterior.coords.xy).T.rename( 40 | columns={0: 'x', 1: 'y'}).apply(lambda x: ct.wgs84_to_gcj02(x.x, x.y), axis=1)) for geom in item.geoms])) 41 | elif isinstance(gdf.iloc[0].geometry, Point): 42 | gdf['geometry'] = gdf.apply(lambda i: Point( 43 | ct.wgs84_to_gcj02(i.geometry.x, i.geometry.y)), axis=1) 44 | return gdf 45 | 46 | 47 | def gdf_gcj_to_wgs(gdf): 48 | ''' 49 | transfer the shapfile coordination system 50 | ''' 51 | if isinstance(gdf.iloc[0].geometry, Polygon): 52 | gdf['geometry'] = gdf.apply(lambda i: Polygon(pd.DataFrame(i.geometry.exterior.coords.xy).T.rename( 53 | columns={0: 'x', 1: 'y'}).apply(lambda x: ct.gcj02_to_wgs84(x.x, x.y), axis=1)), axis=1) 54 | elif isinstance(gdf.iloc[0].geometry, LineString): 55 | gdf['geometry'] = gdf.apply(lambda i: LineString(pd.DataFrame(i.geometry.coords.xy).T.rename( 56 | columns={0: 'x', 1: 'y'}).apply(lambda x: ct.gcj02_to_wgs84(x.x, x.y), axis=1)), axis=1) 57 | elif isinstance(gdf.iloc[0].geometry, MultiPolygon): 58 | gdf['geometry'] = gdf.geometry.apply(lambda item: MultiPolygon([Polygon(pd.DataFrame(geom.exterior.coords.xy).T.rename( 59 | columns={0: 'x', 1: 'y'}).apply(lambda x: ct.gcj02_to_wgs84(x.x, x.y), axis=1)) for geom in item.geoms])) 60 | elif isinstance(gdf.iloc[0].geometry, Point): 61 | gdf['geometry'] = gdf.apply(lambda i: Point( 62 | ct.gcj02_to_wgs84(i.geometry.x, i.geometry.y)), axis=1) 63 | return gdf 64 | 65 | def coord_transfer( res, in_sys = 'gcj', out_sys = 'wgs' ): 66 | assert in_sys in ['gcj', 'wgs'] and out_sys in ['gcj', 'wgs'], "check coordination system" 67 | if in_sys != out_sys: 68 | if in_sys == 'gcj': 69 | res = gdf_gcj_to_wgs(res) 70 | else: 71 | res = gdf_wgs_to_gcj(res) 72 | return res 73 | 74 | def df_to_gdf_points( trip, in_sys = 'gcj', out_sys = 'wgs', keep_datetime =True ): 75 | if not keep_datetime and len(trip.dtypes[trip.dtypes == 'datetime64[ns]'].index)>0: 76 | trip = trip.drop(columns = trip.dtypes[trip.dtypes == 'datetime64[ns]'].index) 77 | # gpd.GeoDataFrame(trip, geometry= trip.apply( lambda x: Point( x.x, x.y ),axis=1)).to_file( f'{plate}.geojson', driver='GeoJSON' ) 78 | trip = gpd.GeoDataFrame( trip, geometry = trip.apply( lambda x: Point( x.x, x.y ),axis=1), crs={'init':'epsg:4326'}) 79 | trip = coord_transfer( trip, in_sys, out_sys ) 80 | return trip 81 | 82 | 83 | def traj_points_to_line( df_tra, df_trip, plate, save = False ): 84 | gdf = gpd.GeoDataFrame() 85 | for i in df_trip.trip_id.unique(): 86 | tra = LineString( df_tra[df_tra.trip_id==i][['x','y','t']].values ) 87 | gdf = gdf.append( {'trip_id':i, 'geometry':LineString( df_tra[df_tra.trip_id==i][['x','y','t']].values ) }, ignore_index=True) 88 | gdf = gdf.merge( df_trip, on ='trip_id' ) 89 | gdf = gdf_gcj_to_wgs( gdf ) 90 | gdf.crs={'init':'epsg:4326'} 91 | # gdf.to_crs(epsg=4547) 92 | if save: gdf.to_file( '%s.shp'%(plate), encoding='utf-8' ) 93 | return gdf 94 | 95 | 96 | if __name__ == '__main__': 97 | # a = gpd.read_file('../trajectory_related/input/Futian_boundary_wgs.shp') 98 | # df_to_gdf_points(trip) 99 | pass 100 | -------------------------------------------------------------------------------- /mapmatching/geo/io.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import shapely 4 | import warnings 5 | import geopandas as gpd 6 | from loguru import logger 7 | import sqlalchemy 8 | 9 | from ..setting import postgre_url 10 | 11 | 12 | ENGINE = sqlalchemy.create_engine(postgre_url) 13 | 14 | def has_table(name, con=None, engine=None): 15 | flag = False 16 | if con is None: 17 | con = engine.connect() 18 | flag = True 19 | 20 | status = con.dialect.has_table(con, name) 21 | if flag: 22 | con.close() 23 | 24 | return status 25 | 26 | def read_postgis(name, atts="*", condition=None, engine=ENGINE, bbox=None, mask=None, geom_col='geometry', *args, **kwargs): 27 | """ 28 | Refs: https://geopandas.org/en/stable/docs/reference/api/geopandas.read_postgis.html#geopandas.read_postgis 29 | """ 30 | with engine.connect() as conn: 31 | if not has_table(name, con=conn): 32 | warnings.warn(f"Not exist {name}") 33 | return None 34 | 35 | if bbox is not None: 36 | wkt = shapely.box(*bbox).to_wkt() 37 | elif shapely.is_geometry(mask): 38 | wkt = mask.wkt 39 | else: 40 | wkt = None 41 | 42 | if mask is None: 43 | sql = f"SELECT {atts} FROM {name}" 44 | else: 45 | sql = f"""SELECT {atts} FROM {name} WHERE ST_Intersects( geometry, ST_GeomFromText('{wkt}', 4326) )""" 46 | 47 | if condition: 48 | sql += f" WHERE {condition}" if wkt is None else f" {condition}" 49 | 50 | gdf = gpd.read_postgis(sqlalchemy.text(sql), con=conn, geom_col=geom_col, *args, **kwargs) 51 | 52 | return gdf 53 | 54 | def to_postgis(gdf:gpd.GeoDataFrame, name, duplicates_idx=None, engine=ENGINE, if_exists='fail', *args, **kwargs): 55 | """ 56 | Upload GeoDataFrame into PostGIS database. 57 | 58 | This method requires SQLAlchemy and GeoAlchemy2, and a PostgreSQL 59 | Python driver (e.g. psycopg2) to be installed. 60 | 61 | Parameters 62 | ---------- 63 | name : str 64 | Name of the target table. 65 | con : sqlalchemy.engine.Connection or sqlalchemy.engine.Engine 66 | Active connection to the PostGIS database. 67 | if_exists : {'fail', 'replace', 'append'}, default 'fail' 68 | How to behave if the table already exists: 69 | 70 | - fail: Raise a ValueError. 71 | - replace: Drop the table before inserting new values. 72 | - append: Insert new values to the existing table. 73 | schema : string, optional 74 | Specify the schema. If None, use default schema: 'public'. 75 | index : bool, default False 76 | Write DataFrame index as a column. 77 | Uses *index_label* as the column name in the table. 78 | index_label : string or sequence, default None 79 | Column label for index column(s). 80 | If None is given (default) and index is True, 81 | then the index names are used. 82 | chunksize : int, optional 83 | Rows will be written in batches of this size at a time. 84 | By default, all rows will be written at once. 85 | dtype : dict of column name to SQL type, default None 86 | Specifying the datatype for columns. 87 | The keys should be the column names and the values 88 | should be the SQLAlchemy types. 89 | """ 90 | 91 | ori_gdf = None 92 | flag = False 93 | if if_exists=='append' and duplicates_idx is not None: 94 | if has_table(name, engine=engine): 95 | ori_gdf = read_postgis(name, engine=engine) 96 | # FIXME 目前因为版本的原因出问题 97 | if_exists = 'replace' 98 | flag = True 99 | 100 | with engine.connect() as conn: 101 | if flag: 102 | tmp = ori_gdf.append(ori_gdf).append(gdf).drop_duplicates(duplicates_idx) 103 | if tmp.shape[0] == 0: 104 | print(f"There is no new record in {name}") 105 | return True 106 | 107 | # Check newly added att, if exist then delete it 108 | drop_cols = [] 109 | remain_cols = [] 110 | for i in tmp.columns: 111 | if i not in ori_gdf.columns: 112 | drop_cols.append(i) 113 | continue 114 | remain_cols.append(i) 115 | 116 | if drop_cols: 117 | logger.warning(f"Drop column `{drop_cols}`, for not exit in the db") 118 | 119 | gdf = tmp[remain_cols] 120 | 121 | status = gdf.to_postgis(name=name, con=conn, if_exists=if_exists, *args, **kwargs) 122 | 123 | return status 124 | 125 | def to_geojson(gdf, fn): 126 | if not isinstance(gdf, gpd.GeoDataFrame): 127 | print('Check the format of the gdf.') 128 | return False 129 | 130 | if 'geojson' not in str(fn): 131 | fn = f'{fn}.geojson' 132 | 133 | gdf.to_file(fn, driver="GeoJSON") 134 | 135 | return 136 | 137 | def set_engine(url): 138 | global ENGINE 139 | ENGINE = sqlalchemy.create_engine(url) 140 | 141 | return ENGINE 142 | -------------------------------------------------------------------------------- /mapmatching/geo/metric/__init__.py: -------------------------------------------------------------------------------- 1 | from .trajDist import lcss, edr, erp 2 | -------------------------------------------------------------------------------- /mapmatching/geo/metric/trajDist.py: -------------------------------------------------------------------------------- 1 | # refs: https://github.com/bguillouet/traj-dist 2 | 3 | import numpy as np 4 | from haversine import haversine_vector, Unit 5 | from ..ops.distance import haversine_matrix 6 | import numba 7 | 8 | @numba.njit 9 | def lcss_dp(n0, n1, M): 10 | # An (m+1) times (n+1) matrix 11 | C = [[0] * (n1 + 1) for _ in range(n0 + 1)] 12 | for i in range(1, n0 + 1): 13 | for j in range(1, n1 + 1): 14 | if M[i - 1, j - 1]: 15 | C[i][j] = C[i - 1][j - 1] + 1 16 | else: 17 | C[i][j] = max(C[i][j - 1], C[i - 1][j]) 18 | 19 | val = float(C[n0][n1]) / min([n0, n1]) 20 | 21 | return val 22 | 23 | def cal_dist_matrix(array1:np.ndarray, array2:np.ndarray, ll=True): 24 | if ll: 25 | M = haversine_matrix(array1, array2, xy=True) 26 | else: 27 | M = np.linalg.norm((array1[:, np.newaxis, :] - array2[np.newaxis, :, :]), axis=-1) 28 | 29 | return M 30 | 31 | def lcss(array1:np.ndarray, array2:np.ndarray, eps:float=10.0, ll=True): 32 | """ 33 | Usage 34 | ----- 35 | The `Longuest-Common-Subsequence distance` (Spherical Geometry) between trajectory t0 and t1. 36 | Parameters 37 | ---------- 38 | param t0 : len(t0) x 2 numpy_array 39 | param t1 : len(t1) x 2 numpy_array 40 | eps : float 41 | Returns 42 | ------- 43 | lcss : float 44 | The Longuest-Common-Subsequence distance between trajectory t0 and t1 45 | """ 46 | M = cal_dist_matrix(array1, array2, ll) 47 | mask = M < eps 48 | M[mask] = True 49 | M[~mask] = False 50 | 51 | val = lcss_dp(len(array1), len(array2), M) 52 | 53 | return val 54 | 55 | def edr(array1, array2, eps, ll=False): 56 | """ 57 | Usage 58 | ----- 59 | The `Edit Distance on Real sequence` between trajectory t0 and t1. 60 | Parameters 61 | ---------- 62 | param t0 : len(t0)x2 numpy_array 63 | param t1 : len(t1)x2 numpy_array 64 | eps : float 65 | Returns 66 | ------- 67 | edr : float 68 | The Longuest-Common-Subsequence distance between trajectory t0 and t1 69 | """ 70 | n0 = len(array1) 71 | n1 = len(array2) 72 | 73 | dist_matrix = cal_dist_matrix(array1, array2, ll) 74 | M = dist_matrix.copy() 75 | mask = M < eps 76 | M[mask] = True 77 | M[~mask] = False 78 | M.astype(int) 79 | 80 | # An (m+1) times (n+1) matrix 81 | C = [[0] * (n1 + 1) for _ in range(n0 + 1)] 82 | for i in range(1, n0 + 1): 83 | for j in range(1, n1 + 1): 84 | subcost = M[i -1, j - 1] 85 | C[i][j] = min(C[i][j - 1] + 1, C[i - 1][j] + 1, C[i - 1][j - 1] + subcost) 86 | edr = float(C[n0][n1]) / max([n0, n1]) 87 | 88 | return edr 89 | 90 | def erp(array1, array2, g, ll=False): 91 | """ 92 | Usage 93 | ----- 94 | The `Edit distance with Real Penalty` between trajectory t0 and t1. 95 | Parameters 96 | ---------- 97 | param t0 : len(t0)x2 numpy_array 98 | param t1 : len(t1)x2 numpy_array 99 | Returns 100 | ------- 101 | dtw : float 102 | The Dynamic-Time Warping distance between trajectory t0 and t1 103 | """ 104 | n0 = len(array1) 105 | n1 = len(array2) 106 | C = np.zeros((n0 + 1, n1 + 1)) 107 | 108 | dist_matrix = cal_dist_matrix(array1, array2, ll) 109 | 110 | ref_1 = haversine_vector(array1[:, ::-1], g[::-1], unit=Unit.METERS) 111 | ref_2 = haversine_vector(array2[:, ::-1], g[::-1], unit=Unit.METERS) 112 | 113 | C[1:, 0] = np.sum(ref_1) 114 | C[0, 1:] = np.sum(ref_2) 115 | for i in np.arange(n0) + 1: 116 | for j in np.arange(n1) + 1: 117 | derp0 = C[i - 1, j] + ref_1[i - 1] 118 | derp1 = C[i, j - 1] + ref_2[j - 1] 119 | derp01 = C[i - 1, j - 1] + M[i - 1, j - 1] 120 | C[i, j] = min(derp0, derp1, derp01) 121 | 122 | erp = C[n0, n1] 123 | 124 | return erp 125 | 126 | """ Euclidean Geometry """ 127 | def e_lcss(t0, t1, eps, ll=False): 128 | """ 129 | Usage 130 | ----- 131 | The Longuest-Common-Subsequence distance between trajectory t0 and t1. 132 | Parameters 133 | ---------- 134 | param t0 : len(t0)x2 numpy_array 135 | param t1 : len(t1)x2 numpy_array 136 | eps : float 137 | Returns 138 | ------- 139 | lcss : float 140 | The Longuest-Common-Subsequence distance between trajectory t0 and t1 141 | """ 142 | n0 = len(t0) 143 | n1 = len(t1) 144 | # An (m+1) times (n+1) matrix 145 | C = [[0] * (n1 + 1) for _ in range(n0 + 1)] 146 | for i in range(1, n0 + 1): 147 | for j in range(1, n1 + 1): 148 | if eucl_dist(t0[i - 1], t1[j - 1]) < eps: 149 | C[i][j] = C[i - 1][j - 1] + 1 150 | else: 151 | C[i][j] = max(C[i][j - 1], C[i - 1][j]) 152 | lcss = 1 - float(C[n0][n1]) / min([n0, n1]) 153 | return lcss 154 | 155 | 156 | -------------------------------------------------------------------------------- /mapmatching/geo/ops/__init__.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import geopandas as gpd 3 | 4 | def check_duplicate_points(points:gpd.GeoDataFrame): 5 | """Check for duplicate nodes in a sequence of coordinates 6 | 7 | Args: 8 | points (gpd.GeoDataFrame): _description_ 9 | 10 | Returns: 11 | _type_: _description_ 12 | """ 13 | coords = np.concatenate(points.geometry.apply(lambda x: x.coords)) 14 | mask = np.sum(coords[:-1] == coords[1:], axis=1) == 2 15 | mask = np.concatenate([mask, [False]]) 16 | 17 | if mask.sum(): 18 | idxs = np.where(mask == True)[0] 19 | print(f"Exist duplicate points, idx: {idxs}.") 20 | 21 | return points[~mask] 22 | 23 | return points 24 | 25 | from .point2line import project_point_2_linestring, project_points_2_linestrings -------------------------------------------------------------------------------- /mapmatching/geo/ops/distance.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import geopandas as gpd 4 | from shapely.geometry import Point 5 | from haversine import haversine, haversine_vector, Unit 6 | 7 | # from .to_array import points_geoseries_2_ndarray 8 | 9 | 10 | def get_length(geoms): 11 | crs = geoms.estimate_utm_crs() 12 | return geoms.to_crs(crs).length 13 | 14 | def cal_pointwise_distance_geoseries(arr1, arr2, align=True): 15 | """calculate two geoseries distance 16 | 17 | Args: 18 | arr1 (gpd.GeoSeries): Geom array 1. 19 | arr2 (gpd.GeoSeries): Geom array 2. 20 | align (bool, optional): Align the two Geom arrays. Defaults to True. 21 | 22 | Returns: 23 | pd.Series: Distance array 24 | """ 25 | if isinstance(arr1, pd.Series): 26 | arr1 = gpd.GeoSeries(arr1) 27 | if isinstance(arr2, pd.Series): 28 | arr2 = gpd.GeoSeries(arr2) 29 | arr1.reset_index(drop=True) 30 | arr2.reset_index(drop=True) 31 | 32 | crs_1 = arr1.crs 33 | crs_2 = arr2.crs 34 | assert crs_1 is not None or crs_2 is not None, "arr1 and arr2 must have one has crs" 35 | 36 | if align: 37 | if crs_1 is None: 38 | arr1.set_crs(crs_2, inplace=True) 39 | if crs_2 is None: 40 | arr2.set_crs(crs_1, inplace=True) 41 | else: 42 | assert crs_1 is not None and crs_2 is not None, "Turn `align` on to align geom1 and geom2" 43 | 44 | if arr1.crs.to_epsg() == 4326: 45 | crs = arr1.estimate_utm_crs() 46 | dist = arr1.to_crs(crs).distance(arr2.to_crs(crs)) 47 | else: 48 | dist = arr1.distance(arr2) 49 | 50 | return dist 51 | 52 | def cal_distance_matrix_geoseries(points1, points2, align=True): 53 | """Generate a pairwise distance matrix between two GeoSeries. 54 | 55 | Args: 56 | arr1 (gpd.GeoSeries): Geom array 1. 57 | arr2 (gpd.GeoSeries): Geom array 2. 58 | 59 | Returns: 60 | pd.DataFrame: A distance matrix of size n x m 61 | """ 62 | n, m = len(points1), len(points2) 63 | 64 | # Replicate arr1 and arr2 65 | repeated_arr1 = points1.repeat(m)#.reset_index(drop=True) 66 | repeated_arr2 = gpd.GeoSeries(pd.concat([points2] * n), crs=points2.crs)#.reset_index(drop=True) 67 | 68 | # Calculate distances 69 | distances = cal_pointwise_distance_geoseries(repeated_arr1, repeated_arr2, align=align) 70 | 71 | # Reshape into matrix 72 | distance_matrix = distances.values.reshape(n, m) 73 | 74 | return pd.DataFrame(distance_matrix, index=points1.index, columns=points2.index) 75 | 76 | def coords_seq_distance(coords): 77 | # for matrix 78 | dist_np = np.linalg.norm(coords[:-1] - coords[1:], axis=1) 79 | 80 | return dist_np, np.sum(dist_np) 81 | 82 | def get_vertical_dist(pointX, pointA, pointB, ll=False): 83 | if ll: 84 | a, b, c = haversine_vector( 85 | np.array([pointA, pointA, pointB])[:, ::-1], 86 | np.array([pointB, pointX, pointX])[:, ::-1], 87 | unit=Unit.METERS 88 | ) 89 | else: 90 | a, b, c = np.linalg.norm( 91 | np.array([pointA, pointA, pointB]) - np.array([pointB, pointX, pointX]), axis = 1) 92 | 93 | #当弦两端重合时,点到弦的距离变为点间距离 94 | if a==0: 95 | return b 96 | 97 | p = (a + b + c) / 2 98 | S = np.sqrt(np.abs(p*(p-a)*(p-b)*(p-c))) 99 | 100 | vertical_dist = S * 2 / a 101 | 102 | return vertical_dist 103 | 104 | """ haversine """ 105 | def geom_series_distance(col1, col2, in_crs=4326, out_crs=900913): 106 | assert isinstance(col1, gpd.GeoSeries) and isinstance(col2, gpd.GeoSeries) 107 | 108 | if in_crs == out_crs: 109 | return col1.distance(col2) 110 | 111 | if isinstance(col1, pd.Series): 112 | a = gpd.GeoSeries(col1).set_crs(in_crs, allow_override=True).to_crs(out_crs) 113 | if isinstance(col2, pd.Series): 114 | b = gpd.GeoSeries(col2).set_crs(in_crs, allow_override=True).to_crs(out_crs) 115 | 116 | return a.distance(b) 117 | 118 | def haversine_matrix(array1, array2, xy=True, unit=Unit.METERS): 119 | ''' 120 | The exact same function as "haversine", except that this 121 | version replaces math functions with numpy functions. 122 | This may make it slightly slower for computing the haversine 123 | distance between two points, but is much faster for computing 124 | the distance matrix between two vectors of points due to vectorization. 125 | ''' 126 | if xy: 127 | array1 = array1[:, ::-1] 128 | array2 = array2[:, ::-1] 129 | 130 | dist = haversine_vector(np.repeat(array1, len(array2), axis=0), 131 | np.concatenate([array2] * len(array1)), 132 | unit=unit) 133 | 134 | matrix = dist.reshape((len(array1), len(array2))) 135 | 136 | return matrix 137 | 138 | def haversine_vector_xy(array1, array2, unit=Unit.METERS, comb=False, normalize=False): 139 | # ensure arrays are numpy ndarrays 140 | if not isinstance(array1, np.ndarray): 141 | array1 = np.array(array1) 142 | if not isinstance(array2, np.ndarray): 143 | array2 = np.array(array2) 144 | 145 | array1 = array1[:, ::-1] 146 | array2 = array2[:, ::-1] 147 | ans = haversine_vector(array1, array2, unit, comb, normalize) 148 | 149 | return ans 150 | 151 | def coords_pair_dist(o, d, xy=True): 152 | if isinstance(o, Point) and isinstance(d, Point): 153 | return haversine((o.y, o.x), (d.y, d.x), unit=Unit.METERS) 154 | 155 | if (isinstance(o, tuple) and isinstance(d, tuple)) or \ 156 | (isinstance(o, list) and isinstance(d, list)): 157 | if xy: 158 | return haversine(o[:2][::-1], d[:2][::-1], unit=Unit.METERS) 159 | else: 160 | return haversine(o[:2], d[:2], unit=Unit.METERS) 161 | 162 | return np.inf 163 | 164 | def cal_coords_seq_distance(points:np.ndarray, xy=True): 165 | if xy: 166 | points = points.copy() 167 | points = points[:, ::-1] 168 | 169 | # FIXME 170 | try: 171 | dist_np = haversine_vector(points[:-1], points[1:], unit=Unit.METERS) 172 | except: 173 | dist_np = np.linalg.norm(points[:-1] - points[1:], axis=1) 174 | 175 | return dist_np, dist_np.sum() 176 | 177 | def cal_points_geom_seq_distacne(geoms:gpd.GeoSeries): 178 | coords = points_geoseries_2_ndarray(geoms) 179 | dist, total = cal_coords_seq_distance(coords, xy=True) 180 | 181 | return dist, coords 182 | 183 | def haversine_geoseries(points1, points2, unit=Unit.METERS, comb=False, normalize=False): 184 | coords_0 = points_geoseries_2_ndarray(points1) 185 | coords_1 = points_geoseries_2_ndarray(points2) 186 | dist = haversine_vector_xy(coords_0, coords_1, unit, comb, normalize) 187 | 188 | return dist 189 | 190 | 191 | if __name__ == "__main__": 192 | # matrix = haversine_matrix(traj_points, points_, xy=True) 193 | 194 | # 创建两个测试 GeoSeries 195 | points1 = gpd.GeoSeries([Point(0, 0), Point(1, 1)]) 196 | points2 = gpd.GeoSeries([Point(1, 1), Point(0, 0), Point(1, 1)]) 197 | 198 | # 确保两个 GeoSeries 使用相同的 CRS 199 | points1.set_crs(epsg=4326, inplace=True) 200 | points2.set_crs(epsg=4326, inplace=True) 201 | 202 | # 计算两个 GeoSeries 之间的距离矩阵 203 | distance_matrix = cal_distance_matrix_geoseries(points1, points2) 204 | distance_matrix 205 | 206 | -------------------------------------------------------------------------------- /mapmatching/geo/ops/linear_referencing.py: -------------------------------------------------------------------------------- 1 | import numba 2 | import numpy as np 3 | from shapely import LineString, Point 4 | from .distance import coords_seq_distance 5 | from .to_array import geoseries_to_coords, points_geoseries_2_ndarray 6 | 7 | def _check(point, line): 8 | res = linear_referencing(point, polyline, cut=False) 9 | dist = res['offset'] 10 | _dist = line.project(point) 11 | 12 | assert (dist - _dist) / (dist + 1e-8) < 1e-8, "check" 13 | 14 | def plot(point, polyline, res): 15 | proj = res['proj_point'] 16 | if 'seg_0' in res: 17 | seg_0 = LineString(res['seg_0']) 18 | if 'seg_1' in res: 19 | seg_1 = LineString(res['seg_1']) 20 | 21 | import geopandas as gpd 22 | ax = gpd.GeoDataFrame({ 23 | 'geometry':[point, polyline], 24 | 'name': ['seg_0', 'seg_1'] 25 | }).plot(color='red', linewidth=5, alpha=.5) 26 | 27 | gpd.GeoDataFrame({"geometry": [proj]}).plot(ax=ax, color='blue', label='Project') 28 | 29 | segs = gpd.GeoDataFrame({"name": ['seg_0', "seg_1"], 30 | "geometry": [LineString(seg_0), LineString(seg_1)]}) 31 | segs.plot(ax=ax, column='name', legend=True, linestyle="--") 32 | 33 | return ax 34 | 35 | def closest_point_on_segments(point:np.ndarray, lines:np.ndarray, eps=1e-9): 36 | """Calculate the closest point p' and its params on each segments on a polyline. 37 | 38 | Args: 39 | point (np.array): Point (shape: [2,]). 40 | lines (np.array): Polyline in the form of coords sequence(shape: [n, 2]). 41 | eps (float, optional): Defaults to 1e-9. 42 | 43 | Returns: 44 | (array, array, array): proj, dist, ratio 45 | """ 46 | segs = np.hstack([lines[:-1][:, np.newaxis], 47 | lines[1:][:, np.newaxis]]) 48 | pq = segs[:, 1] - segs[:, 0] 49 | d = np.power(pq, 2).sum(axis=1) 50 | d[d == 0] = eps 51 | 52 | x, y = point 53 | dx = x - segs[:, 0, 0] 54 | dy = y - segs[:, 0, 1] 55 | t = pq[:, 0] * dx + pq[:, 1] * dy 56 | 57 | ratio = t / d 58 | ratio[ratio < 0] = 0 59 | ratio[ratio > 1] = 1 60 | 61 | offset = pq * ratio[:, np.newaxis] 62 | proj = offset + segs[:, 0] 63 | dist = np.linalg.norm(point - proj, axis=1) 64 | 65 | return proj, dist, ratio 66 | 67 | # @numba.jit 68 | def cut_lines(idx, proj, ratio, coords): 69 | NONE_COORD = None 70 | if idx == 0 and ratio == 0: 71 | return NONE_COORD, coords 72 | if idx == coords.shape[0] - 2 and ratio == 1: 73 | return coords, NONE_COORD 74 | 75 | if ratio == 0: 76 | seg_0 = coords[:idx + 1] 77 | seg_1 = coords[idx:] 78 | elif ratio < 1: 79 | seg_0 = np.concatenate([coords[:idx+1], [proj]]) 80 | seg_1 = np.concatenate([[proj], coords[idx+1:]]) 81 | else: 82 | seg_0 = coords[:idx+2] 83 | seg_1 = coords[idx+1:] 84 | 85 | return seg_0, seg_1 86 | 87 | def linear_referencing(point:Point, polyline:LineString, cut=True, to_geom=False): 88 | # TODO vectorized 89 | # iterating through each segment in the polyline and returning the one with minimum distance 90 | 91 | p_coords = np.array(point.coords[0]) 92 | l_coords = np.array(polyline.coords) 93 | 94 | projs, dists, ratios = closest_point_on_segments(p_coords, l_coords) 95 | idx = np.argmin(dists) 96 | proj = projs[idx] 97 | ratio = ratios[idx] 98 | len_np, total_len = coords_seq_distance(l_coords) 99 | offset = len_np[:idx].sum() + len_np[idx] * ratio 100 | 101 | res = {} 102 | res['proj_point'] = Point(proj) if to_geom else proj 103 | res['dist_p2c'] = dists[idx] 104 | if not cut: 105 | res['offset'] = offset 106 | else: 107 | seg_0, seg_1 = cut_lines(idx, proj, ratio, l_coords) 108 | if to_geom: 109 | seg_0 = LineString(seg_0) 110 | seg_1 = LineString(seg_1) 111 | res['seg_0'] = seg_0 112 | res['seg_1'] = seg_1 113 | res['len_0'] = offset 114 | res['len_1'] = total_len - offset 115 | 116 | return res 117 | 118 | # @numba.jit 119 | def lines_to_matrix(lines, n_rows, n_cols): 120 | _lines = np.zeros((n_rows, n_cols, 2)) 121 | mask = np.ones((n_rows, n_cols), dtype=np.bool_) 122 | 123 | for i, line in enumerate(lines): 124 | n = len(line) 125 | _lines[i, :n] = line 126 | _lines[i, n:] = line[-1] 127 | mask[i, n:] = 0 128 | 129 | return _lines, mask 130 | 131 | # @numba.jit 132 | def cut_line(idx, proj, ratio, coords): 133 | NONE_COORD = None 134 | if idx == 0 and ratio == 0: 135 | return NONE_COORD, coords 136 | if idx == coords.shape[0] - 2 and ratio == 1: 137 | return coords, NONE_COORD 138 | 139 | if ratio == 0: 140 | seg_0 = coords[:idx + 1] 141 | seg_1 = coords[idx:] 142 | elif ratio < 1: 143 | seg_0 = np.concatenate([coords[:idx+1], [proj]]) 144 | seg_1 = np.concatenate([[proj], coords[idx+1:]]) 145 | else: 146 | seg_0 = coords[:idx+2] 147 | seg_1 = coords[idx+1:] 148 | 149 | return seg_0, seg_1 150 | 151 | # @numba.jit 152 | def numba_cut_lines(col_idxs, closest, ratio, lines): 153 | res = [cut_line(i, c, r, s) 154 | for i, c, r, s in zip(col_idxs, closest, ratio, lines)] 155 | 156 | return res 157 | 158 | def linear_referencing_vector(points:np.array, lines:np.array, cut=True, eps=1e-9): 159 | n_len = [len(i) for i in lines] 160 | n_cols = max(n_len) 161 | n_rows = len(lines) 162 | _lines, mask = lines_to_matrix(lines, n_rows, n_cols) 163 | 164 | segs = np.dstack([_lines[:, :-1][:,:,np.newaxis], 165 | _lines[:, 1:][:,:,np.newaxis]]) 166 | pq = segs[:, :, 1] - segs[:, :, 0] 167 | d = np.power(pq, 2).sum(axis=-1) 168 | len_np = np.sqrt(d) 169 | d[d == 0] = eps 170 | 171 | x, y = points[:, 0], points[:, 1] 172 | dx = x[:, np.newaxis] - segs[:, :, 0, 0] 173 | dy = y[:, np.newaxis] - segs[:, :, 0, 1] 174 | t = pq[:, :, 0] * dx + pq[:, :, 1] * dy 175 | 176 | ratios = t / d 177 | ratios[ratios < 0] = 0 178 | ratios[ratios > 1] = 1 179 | 180 | offset = pq * ratios[:, :, np.newaxis] # (n, l, 2) 181 | closests = offset + segs[:, :, 0] 182 | dists = np.linalg.norm(points[:, np.newaxis] - closests, axis=-1) 183 | 184 | col_idxs = np.argmin(dists, axis=1) 185 | row_idxs = np.arange(n_rows) 186 | 187 | cp = closests[row_idxs, col_idxs] 188 | r = ratios[row_idxs, col_idxs] 189 | dist_p2c = dists[row_idxs, col_idxs] 190 | 191 | sum_mask = np.zeros((n_rows, n_cols-1), dtype=np.bool_) 192 | for i, col in enumerate(col_idxs): 193 | sum_mask[i, :col] = True 194 | 195 | offset = np.sum(len_np, axis=1, where=sum_mask) + len_np[row_idxs, col_idxs] * r 196 | 197 | res = {} 198 | res['proj_point'] = [i for i in cp] 199 | res['dist_p2c'] = dist_p2c 200 | 201 | if not cut: 202 | res['offset'] = offset 203 | else: 204 | # TODO normalized = True 205 | tmp = numba_cut_lines(col_idxs, cp, r, lines) 206 | seg_0, seg_1 = list(zip(*tmp)) 207 | res['seg_0'] = seg_0 208 | res['seg_1'] = seg_1 209 | res['len_0'] = offset 210 | res['len_1'] = len_np.sum(axis=1) - offset 211 | 212 | return res 213 | 214 | def linear_referencing_geom(point_geoms, line_geoms, cut=True, eps=1e-9): 215 | _points = points_geoseries_2_ndarray(point_geoms) 216 | _lines = geoseries_to_coords(line_geoms) 217 | 218 | res = linear_referencing_vector(_points, _lines, cut, eps) 219 | 220 | return res 221 | 222 | 223 | if __name__ == "__main__": 224 | polyline = LineString([[-1,0], [0, 0], [1,1], [1, 1], [2,3]]) 225 | point = Point([-0.5, 1]) 226 | res = linear_referencing(point, polyline) 227 | 228 | # case 0 229 | point = Point([-0.5, 1]) 230 | _check(point, polyline) 231 | res = linear_referencing(point, polyline) 232 | plot(point, polyline, res) 233 | 234 | # case 1 235 | point = Point([-1.5, .5]) 236 | _check(point, polyline) 237 | res = linear_referencing(point, polyline) 238 | plot(point, polyline, res) 239 | 240 | # case 2 241 | point = Point([2.2, 3.5]) 242 | _check(point, polyline) 243 | res = linear_referencing(point, polyline) 244 | plot(point, polyline, res) 245 | 246 | # case 3 247 | point = Point([0.5, 1]) 248 | # _check(point, polyline) 249 | res = linear_referencing(point, polyline) 250 | plot(point, polyline, res); 251 | 252 | # case 4 253 | point = Point([-.1, 1.2]) 254 | polyline = LineString([[0, 0], [0, 1], [1,1]]) 255 | res = linear_referencing(point, polyline) 256 | plot(point, polyline, res) 257 | 258 | 259 | from shapely import wkt 260 | # case 0 261 | point = wkt.loads('POINT (113.934194 22.577979)') 262 | # case 1 263 | point = wkt.loads('POINT (113.934144 22.577979)') 264 | 265 | # case 0, 创科路/打石二路路口 266 | polyline = wkt.loads("LINESTRING (113.934186 22.57795, 113.934227 22.577982, 113.934274 22.578013, 113.934321 22.578035, 113.934373 22.578052, 113.934421 22.57806, 113.93448 22.578067)") 267 | 268 | res = linear_referencing(point, polyline) 269 | plot(point, polyline, res) 270 | -------------------------------------------------------------------------------- /mapmatching/geo/ops/point2line.py: -------------------------------------------------------------------------------- 1 | import numba 2 | import numpy as np 3 | import shapely 4 | from shapely import Point, LineString 5 | import geopandas as gpd 6 | from geopandas import GeoDataFrame 7 | 8 | from .distance import cal_coords_seq_distance, cal_pointwise_distance_geoseries 9 | 10 | 11 | @numba.jit 12 | def get_first_index(arr, val): 13 | """有效地返回数组中第一个值满足条件的索引 14 | Refs: https://blog.csdn.net/weixin_39707612/article/details/111457329; 15 | 耗时: 0.279 us; np.argmax(arr> vak)[0] 1.91 us 16 | 17 | Args: 18 | A (np.array): Numpy arr 19 | k (float): value 20 | 21 | Returns: 22 | int: The first index that large that `val` 23 | """ 24 | for i in range(len(arr)): 25 | if arr[i] >= val: 26 | return i + 1 27 | val -= arr[i] 28 | 29 | return -1 30 | 31 | def project_point_2_linestring(point:Point, line:LineString, normalized:bool=True): 32 | dist = line.project(point, normalized) 33 | proj_point = line.interpolate(dist, normalized) 34 | 35 | return proj_point, dist 36 | 37 | def cut_linestring(line:LineString, offset:float, point:Point=None, normalized=False): 38 | _len = 1 if normalized else line.length 39 | coords = np.array(line.coords) 40 | 41 | if offset <= 0: 42 | res = {"seg_0": None, "seg_1": coords} 43 | elif offset >= _len: 44 | res = {"seg_0": coords, "seg_1": None} 45 | else: 46 | # points = np.array([Point(*i) for i in coords]) 47 | # dist_intervals = line.project(points, normalized) 48 | dist_arr, _ = cal_coords_seq_distance(coords) 49 | 50 | idx = get_first_index(dist_arr, offset) 51 | pd = np.sum(dist_arr[:idx]) 52 | if pd == offset: 53 | coords_0 = coords[:idx+1] 54 | coords_1 = coords[idx:] 55 | else: 56 | if point is None: 57 | point = line.interpolate(offset, normalized) 58 | cp = np.array(point.coords) 59 | coords_0 = np.concatenate([coords[:idx], cp]) 60 | coords_1 = np.concatenate([cp, coords[idx:]]) 61 | 62 | res = {'seg_0': coords_0, 'seg_1': coords_1} 63 | 64 | res['seg_0'] = LineString(res['seg_0']) 65 | res['seg_1'] = LineString(res['seg_1']) 66 | 67 | return res 68 | 69 | def test_cut_linestring(line, point): 70 | # test: project_point_2_linestring 71 | cp, dist = project_point_2_linestring(point, line) 72 | data = {'name': ['point', 'line', 'cp'], 73 | 'geometry': [point, line, cp] 74 | } 75 | ax = gpd.GeoDataFrame(data).plot(column='name', alpha=.5) 76 | 77 | # test: cut_linestring 78 | seg_0, seg_1 = cut_linestring(line, dist) 79 | data = {'name': ['ori', 'seg_0', 'seg_1'], 80 | 'geometry': [line, seg_0, seg_1] 81 | } 82 | gpd.GeoDataFrame(data).plot(column='name', legend=True, linestyle="--", ax=ax) 83 | 84 | def project_points_2_linestrings(points:GeoDataFrame, lines:GeoDataFrame, 85 | normalized:bool=True, drop_ori_geom=True, 86 | keep_attrs:list=['eid', 'geometry'], precision=1e-7, 87 | ll=True, cal_dist=True): 88 | """projects points to the nearest linestring 89 | 90 | Args: 91 | panos (GeoDataFrame | GeoSeries): Points 92 | paths (GeoDataFrame | GeoSeries): Edges 93 | keep_attrs (list, optional): _description_. Defaults to ['eid', 'geometry']. 94 | drop_ori_geom (bool, optional): Drop the origin point and line geometry. Defaults to True. 95 | 96 | Returns: 97 | GeoDataFrame: The GeoDataFrame of projected points with `proj_point`, `offset` 98 | 99 | Example: 100 | ``` 101 | import geopandas as gpd 102 | from shapely import Point, LineString 103 | 104 | points = gpd.GeoDataFrame( 105 | geometry=[ 106 | Point(113.93195659801206, 22.575930582940785), 107 | Point(113.93251505775076, 22.57563203614608), 108 | Point(113.93292030671412, 22.575490522559665), 109 | Point(113.93378178962489, 22.57534631453745) 110 | ] 111 | ) 112 | 113 | lines = gpd.GeoDataFrame({ 114 | "eid": [63048, 63935], 115 | "geometry": [ 116 | LineString([(113.9319709, 22.5759509), (113.9320297, 22.5759095), (113.9321652, 22.5758192), (113.9323286, 22.575721), (113.9324839, 22.5756433), (113.9326791, 22.5755563), (113.9328524, 22.5754945), (113.9330122, 22.5754474), (113.933172, 22.5754073), (113.9333692, 22.5753782), (113.9334468, 22.5753503), (113.9335752, 22.5753413), (113.9336504, 22.5753383)]), 117 | LineString([(113.9336504, 22.5753383), (113.9336933, 22.5753314), (113.9337329, 22.5753215), (113.9337624, 22.5753098), (113.933763, 22.5753095)])] 118 | }) 119 | 120 | prod_ps = project_points_2_linestrings(points.geometry, lines) 121 | _, ax = plot_geodata(prod_ps, color='red', label='proj', marker='*') 122 | lines.plot(ax=ax, label='lines') 123 | points.plot(ax=ax, label='points', alpha=.5) 124 | ax.legend() 125 | ``` 126 | """ 127 | proj_df = points.geometry.apply(lambda x: lines.loc[lines.distance(x).idxmin(), keep_attrs])\ 128 | .rename(columns={"geometry": 'edge_geom'}) 129 | 130 | att_lst = ['proj_point', 'offset'] 131 | proj_df.loc[:, 'point_geom'] = points.geometry 132 | proj_df.loc[:, att_lst] = proj_df.apply( 133 | lambda x: project_point_2_linestring( 134 | x.point_geom, x.edge_geom, normalized), 135 | axis=1, result_type='expand' 136 | ).values 137 | 138 | proj_df.loc[:, 'dist_p2c'] = cal_pointwise_distance_geoseries(proj_df['point_geom'], proj_df['proj_point']) 139 | 140 | if drop_ori_geom: 141 | proj_df.drop(columns=['point_geom', 'edge_geom'], inplace=True) 142 | 143 | return gpd.GeoDataFrame(proj_df).set_geometry('proj_point') 144 | 145 | 146 | """ decrapted """ 147 | def get_foot_point(point, line_p1, line_p2): 148 | """ 149 | @point, line_p1, line_p2 : [x, y, z] 150 | """ 151 | x0 = point[0] 152 | y0 = point[1] 153 | # z0 = point[2] 154 | 155 | x1 = line_p1[0] 156 | y1 = line_p1[1] 157 | # z1 = line_p1[2] 158 | 159 | x2 = line_p2[0] 160 | y2 = line_p2[1] 161 | # z2 = line_p2[2] 162 | assert not (x1 == x2 and y1 == y2), f"check line {line_p1}, {line_p2}" 163 | # k = -((x1 - x0) * (x2 - x1) + (y1 - y0) * (y2 - y1) + (z1 - z0) * (z2 - z1)) / \ 164 | # ((x2 - x1) ** 2 + (y2 - y1) ** 2 + (z2 - z1) ** 2)*1.0 165 | k = -((x1 - x0) * (x2 - x1) + (y1 - y0) * (y2 - y1)) / ((x2 - x1) ** 2 + (y2 - y1) ** 2 )*1.0 166 | xn = k * (x2 - x1) + x1 167 | yn = k * (y2 - y1) + y1 168 | # zn = k * (z2 - z1) + z1 169 | 170 | return (round(xn, 6), round(yn, 6)) 171 | 172 | def relation_bet_point_and_line( point, line ): 173 | """Judge the realtion between point and the line, there are three situation: 174 | 1) the foot point is on the line, the value is in [0,1]; 175 | 2) the foot point is on the extension line of segment AB, near the starting point, the value < 0; 176 | 3) the foot point is on the extension line of segment AB, near the ending point, the value >1; 177 | 178 | Args: 179 | point ([double, double]): point corrdination 180 | line ([x0, y0, x1, y1]): line coordiantions 181 | 182 | Returns: 183 | [float]: the realtion between point and the line (起点 < 0 <= 线段中 <= 1 < 终点) 184 | """ 185 | pqx = line[2] - line[0] 186 | pqy = line[3] - line[1] 187 | dx = point[0]- line[0] 188 | dy = point[1]- line[1] 189 | 190 | d = pow(pqx, 2) + pow(pqy, 2) 191 | t = pqx * dx + pqy * dy 192 | 193 | flag = 1 194 | if(d > 0): 195 | t = t / d 196 | flag = t 197 | 198 | return flag 199 | 200 | def cal_foot_point_on_polyline( point: Point, line: LineString, foot=True, ratio_thres=.0): 201 | """caculate the foot point is on the line or not 202 | 203 | Args: 204 | point (list): coordination (x, y) 205 | line (pd.Series): [description] 206 | ratio_thres (float, optional): [ratio threshold]. Defaults to 0.005. 207 | 208 | Returns: 209 | [bool]: locate on the lane or not 210 | """ 211 | line_ = line.coords[0] + line.coords[-1] 212 | factor = relation_bet_point_and_line((point.x, point.y), line_) 213 | flag = 0 - ratio_thres <= factor <= 1 + ratio_thres 214 | 215 | if foot: 216 | _foot = get_foot_point((point.x, point.y), line.coords[0], line.coords[-1]) 217 | return {'flag': factor, 'foot':_foot} 218 | 219 | return flag 220 | 221 | 222 | if __name__ == "__main__": 223 | line = LineString([(0, 0), (0, 1), (1, 1)]) 224 | 225 | test_cut_linestring(line, Point((0.5, 0))) 226 | test_cut_linestring(line, Point((0, 1))) 227 | test_cut_linestring(line, Point((1.1, 1.5))) 228 | 229 | 230 | -------------------------------------------------------------------------------- /mapmatching/geo/ops/resample.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import geopandas as gpd 4 | from shapely.geometry import Point 5 | 6 | from .distance import cal_points_geom_seq_distacne 7 | 8 | 9 | def resample_point_seq(points, step=2, last=True): 10 | # TODO linear referencing + speedup 11 | points = points[~(points == points.shift(1))] 12 | if points.shape[0] == 1: 13 | return gpd.GeoDataFrame(points), np.array([points.iloc[0].coords[0]]) 14 | 15 | dist, coords = cal_points_geom_seq_distacne(points) 16 | dxdy = coords[1:] - coords[:-1] 17 | 18 | cum_dist = np.cumsum(dist) 19 | cum_dist = np.concatenate([[0], cum_dist]) 20 | samples = np.arange(0, cum_dist[-1], step) 21 | seg_ids = pd.cut(samples, bins=cum_dist, labels=range(len(dist)), right=False) 22 | 23 | samples_lst = [] 24 | samples_coords = [] 25 | for s, idx in zip(samples, seg_ids): 26 | ratio = (s - cum_dist[idx]) / dist[idx] 27 | xy = coords[idx] + dxdy[idx] * ratio 28 | samples_coords.append(xy) 29 | samples_lst.append({"seg_idx": idx, "offset": s, "geometry": Point(xy)}) 30 | if last: 31 | samples_lst.append({"seg_idx": len(dist) - 1, "offset": dist[-1], "geometry": Point(coords[-1])}) 32 | samples_coords.append(coords[-1]) 33 | 34 | df_samples = gpd.GeoDataFrame(samples_lst) 35 | 36 | return df_samples, np.array(samples_coords) 37 | 38 | def resample_polyline_seq_to_point_seq(polyline, step=2, last=True): 39 | coords = np.concatenate(polyline.apply(lambda x: x.coords).values) 40 | 41 | mask = np.sum(coords[:-1] == coords[1:], axis=1) == 2 42 | mask = np.concatenate([mask, [False]]) 43 | geoms = gpd.GeoSeries([Point(i) for i in coords[~mask]]) 44 | 45 | return resample_point_seq(geoms, step, last) 46 | 47 | 48 | if __name__ == "__main__": 49 | df = gpd.read_file('./data/trajs/traj_9.geojson').head(20) 50 | 51 | df_samples, coords = resample_point_seq(df.geometry) 52 | 53 | df.plot(color='r', alpha=.1) 54 | 55 | -------------------------------------------------------------------------------- /mapmatching/geo/ops/simplify.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import geopandas as gpd 3 | from .distance import get_vertical_dist 4 | 5 | 6 | def dp_compress(point_list, dist_thres=8, verbose=False): 7 | """Douglas-Peucker compress alg Douglas-Peucker. 8 | 9 | Args: 10 | point_list (lst): The ordered coordinations [(x1, y1, id1), (x2, y2, id2), ... , (xn, yn, idn)] 11 | dist_max (int, optional): The max distance (Unit: meters). Defaults to 8. 12 | verbose (bool, optional): [description]. Defaults to False. 13 | """ 14 | def _dfs(point_list, start, end, res, dist_max): 15 | # start, end = 0, len(point_list)-1 16 | if start >= end: 17 | return 18 | 19 | res.append(point_list[start]) 20 | res.append(point_list[end]) 21 | 22 | if start < end: 23 | index = start + 1 24 | max_vertical_dist = 0 25 | key_point_index = 0 26 | 27 | while(index < end): 28 | cur_vertical_dist = get_vertical_dist( 29 | point_list[index][:2], 30 | point_list[start][:2], 31 | point_list[end][:2], 32 | ll=False 33 | ) 34 | if cur_vertical_dist > max_vertical_dist: 35 | max_vertical_dist = cur_vertical_dist 36 | key_point_index = index 37 | index += 1 38 | 39 | if max_vertical_dist >= dist_max: 40 | _dfs(point_list, start, key_point_index, res, dist_max) 41 | _dfs(point_list, key_point_index, end, res, dist_max) 42 | 43 | res = [] 44 | _dfs(point_list, 0, len(point_list)-1, res, dist_thres) 45 | 46 | res = list(set(res)) 47 | res = sorted(res, key=lambda x:x[2]) 48 | 49 | if verbose: 50 | print(f"Compression rate {len(res)/len(point_list)*100:.2f}% (={len(point_list)}/{len(res)}), "\ 51 | f"mean error: {get_MeanErr(point_list,res):.2f}") 52 | 53 | return res 54 | 55 | 56 | def get_MeanErr(point_list, output_point_list): 57 | Err=0 58 | start, end = 0, len(output_point_list)-1 59 | 60 | while(start < end): 61 | pointA_id = int(output_point_list[start][2]) 62 | pointB_id = int(output_point_list[start+1][2]) 63 | 64 | id = pointA_id + 1 65 | while(id < pointB_id): 66 | Err += get_vertical_dist(output_point_list[start][:2], output_point_list[start+1][:2], point_list[id][:2]) 67 | id += 1 68 | start += 1 69 | 70 | return Err/len(point_list) 71 | 72 | 73 | def dp_compress_for_points(df, dist_thres=10, verbose=False, reset_index=True): 74 | traj = df.copy() 75 | traj.loc[:, 'pid_order'] = traj.index 76 | point_lst = traj.apply(lambda x: (x.geometry.x, x.geometry.y, x.pid_order), axis=1).values.tolist() 77 | point_lst = dp_compress(point_lst, dist_thres, verbose) 78 | 79 | if reset_index: 80 | return traj.loc[[ i[2] for i in point_lst]].reset_index() 81 | 82 | return traj.loc[[ i[2] for i in point_lst]] 83 | 84 | 85 | def simplify_trajetory_points(points: gpd.GeoDataFrame, tolerance: int = None, inplace=False, logger=None): 86 | """The algorithm (Douglas-Peucker) recursively splits the original line into smaller parts 87 | and connects these parts’ endpoints by a straight line. Then, it removes all points whose 88 | distance to the straight line is smaller than tolerance. It does not move any points and 89 | it always preserves endpoints of the original line or polygon. 90 | 91 | Args: 92 | points (gpd.GeoDataFrame): _description_ 93 | traj_thres (int, optional): The compression threshold(Unit: meter). Defaults to None. 94 | inplace (bool, optional): _description_. Defaults to False. 95 | 96 | Returns: 97 | gpd.GeoDataFrame: _description_ 98 | """ 99 | ori_size = points.shape[0] 100 | if ori_size == 1: 101 | return points 102 | 103 | points = points if inplace else points.copy() 104 | points = dp_compress_for_points(points, dist_thres=tolerance) 105 | 106 | if ori_size == 2: 107 | if points.iloc[0].geometry.distance(points.iloc[1].geometry) < 1e-6: 108 | points = points.head(1) 109 | if logger: 110 | logger.info( 111 | f"Trajectory only has one point or all the same points.") 112 | return points 113 | 114 | if logger: 115 | logger.debug( 116 | f"Trajectory compression rate: {points.shape[0]/ori_size*100:.1f}% ({ori_size} -> {points.shape[0]})") 117 | 118 | return points 119 | 120 | 121 | if __name__ == '__main__': 122 | point_list = [] 123 | output_point_list = [] 124 | 125 | fd=open(r"./Dguiji.txt",'r') 126 | for line in fd: 127 | line=line.strip() 128 | id=int(line.split(",")[0]) 129 | longitude=float(line.split(",")[1]) 130 | latitude=float(line.split(",")[2]) 131 | point_list.append((longitude,latitude,id)) 132 | fd.close() 133 | 134 | output_point_list = dp_compress(point_list, dist_thres=8, verbose=True) 135 | 136 | import geopandas as gpd 137 | traj = gpd.read_file("../traj_for_compress.geojson") 138 | dp_compress_for_points(traj, 8, True) 139 | -------------------------------------------------------------------------------- /mapmatching/geo/ops/substring.py: -------------------------------------------------------------------------------- 1 | import shapely 2 | import numpy as np 3 | 4 | 5 | def substrings(linestring:np.ndarray, start_dist:float, end_dist:float, normalized=False) -> np.ndarray: 6 | """Cut a linestring at two offset values 7 | 8 | Args: 9 | linestring (np.ndarray): input line 10 | start_dist (float): starting offset, distance to the start point of linestring 11 | end_dist (float): ending offset, distance to the start point of linestring 12 | normalized (bool, optional): If the normalized arg is True, the distance will be 13 | interpreted as a fraction of the geometry’s length. Defaults to False. 14 | 15 | Returns: 16 | np.ndarray: a linestring containing only the part covering starting offset to ending offset 17 | 18 | Ref: 19 | https://github.com/cyang-kth/fmm/blob/master/src/algorithm/geom_algorithm.cpp#L351-L417 20 | https://shapely.readthedocs.io/en/stable/manual.html?highlight=substring#shapely.ops.substring 21 | """ 22 | 23 | return NotImplementedError 24 | 25 | -------------------------------------------------------------------------------- /mapmatching/geo/ops/to_array.py: -------------------------------------------------------------------------------- 1 | import numba 2 | import numpy as np 3 | import geopandas as gpd 4 | 5 | @numba.jit 6 | def points_geoseries_2_ndarray(geoms:gpd.GeoSeries): 7 | return np.concatenate([np.array(i.coords) for i in geoms]) 8 | 9 | @numba.jit 10 | def geoseries_to_coords(geoms): 11 | return [np.array(i.coords) for i in geoms] 12 | -------------------------------------------------------------------------------- /mapmatching/geo/query.py: -------------------------------------------------------------------------------- 1 | import shapely 2 | import warnings 3 | import numpy as np 4 | import geopandas as gpd 5 | from geopandas import GeoDataFrame 6 | from shapely import geometry as shapely_geom 7 | 8 | from .ops.linear_referencing import linear_referencing_geom 9 | from ..utils import timeit 10 | 11 | @timeit 12 | def get_k_neigh_geoms(query: GeoDataFrame, gdf: GeoDataFrame, query_id='qid', 13 | radius: float = 50, top_k=None, predicate: str = 'intersects', 14 | check_diff=True, project=True, keep_geom=True): 15 | """ 16 | Get the k nearest geometries of the query within a search radius using a built-in grid-based spatial index. 17 | 18 | Args: 19 | query (GeoDataFrame, GeoSeries, geometry): The query object. 20 | gdf (GeoDataFrame): The base geometry. 21 | query_id (str, optional): The index of the query object. Defaults to 'qid'. 22 | radius (float, optional): The search radius. Defaults to 50 (in meters for the WGS system). 23 | top_k (int, optional): The number of top-k elements to retrieve. Defaults to None (retrieve all). 24 | predicate (str, optional): The predicate operation in geopandas. Defaults to 'intersects'. 25 | check_diff (bool, optional): Check if there are no matching queries. Defaults to True. 26 | project (bool, optional): Project the query object to gdf. Only supports Point geometries. Defaults to True. 27 | keep_geom (bool, optional): Whether to keep the geometry columns in the result. Defaults to True. 28 | normalized (bool, optional): Normalize the distances. Defaults to False. 29 | 30 | Returns: 31 | GeoDataFrame: The query result. 32 | 33 | Example: 34 | # Example usage 1 35 | import geopandas as gpd 36 | from stmm.geo.query import get_K_neighbors 37 | 38 | traj = matcher.load_points("./data/trajs/traj_4.geojson").head(4) 39 | query = traj[['PID','geometry']].head(1).copy() 40 | gdf = net.df_edges[['eid', 'geometry']].copy() 41 | 42 | df_cands, no_cands_query = get_K_neighbors(query, gdf, top_k=8) 43 | plot_candidates(query, gdf, df_cands) 44 | 45 | # Example usage 2 46 | import geopandas as gpd 47 | from shapely import LineString, Point 48 | from stmm.geo.query import plot_candidates, get_K_neighbors 49 | 50 | lines = [LineString([[0, i], [10, i]]) for i in range(0, 10)] 51 | lines += [LineString(([5.2,5.2], [5.8, 5.8]))] 52 | edges = gpd.GeoDataFrame({'geometry': lines, 53 | 'way_id':[i for i in range(10)] + [5]}) 54 | 55 | a, b = Point(1, 1.1), Point(5, 5.1) 56 | points = gpd.GeoDataFrame({'geometry': [a, b]}, index=[1, 3]) 57 | points.loc[:, 'PID'] = points.index 58 | 59 | res, _ = get_K_neighbors(points, edges, buffer=2, top_k=2, ll=False) 60 | ax = plot_candidates(points, edges, res) 61 | """ 62 | 63 | # TODO: Determine appropriate index for gdf 64 | 65 | # Check spatial index 66 | if not gdf.has_sindex: 67 | try: 68 | print("rebuild sindex: ") 69 | gdf.sindex 70 | except: 71 | raise ValueError() 72 | 73 | # Prepare query 74 | if isinstance(query, shapely_geom.base.BaseGeometry): 75 | _query = gpd.GeoSeries([query]) 76 | if isinstance(query, GeoDataFrame): 77 | if query_id in list(query): 78 | _query = query.set_index(query_id)['geometry'] 79 | else: 80 | _query = query['geometry'].copy() 81 | _query.index.set_names(query_id, inplace=True) 82 | elif isinstance(query, gpd.GeoSeries): 83 | _query = query.copy() 84 | _query.index.set_names(query_id, inplace=True) 85 | else: 86 | raise TypeError(query) 87 | 88 | if _query.crs != gdf.crs: 89 | _query = _query.to_crs(gdf.crs) 90 | _query.index.set_names(query_id, inplace=True) 91 | 92 | # Query bulk 93 | get_box = lambda i: shapely_geom.box(i.x - radius, i.y - radius, i.x + radius, i.y + radius) 94 | query_geoms = _query.apply(get_box) 95 | cands = gdf.sindex.query_bulk(query_geoms, predicate) 96 | if len(cands[0]) == 0: 97 | return None, None 98 | 99 | df_cands = _get_cands(_query, gdf, cands, query_id) 100 | _project(df_cands, project) 101 | 102 | if radius: 103 | df_cands.query(f"dist_p2c <= {radius}", inplace=True) 104 | if top_k: 105 | df_cands = _filter_candidate(df_cands, query_id, top_k) 106 | 107 | if not keep_geom: 108 | df_cands.drop(columns=["query_geom", "edge_geom"], inplace=True) 109 | 110 | # Check difference 111 | no_cands_query = None 112 | if check_diff: 113 | cands_pid = set(cands[0]) 114 | all_pid = set(_query.index.unique()) 115 | no_cands_query = all_pid.difference(cands_pid) 116 | warnings.warn(f"{no_cands_query} has no neighbors within the {radius} search zone.") 117 | 118 | return df_cands.set_geometry('edge_geom').set_crs(gdf.crs), no_cands_query 119 | 120 | 121 | @timeit 122 | def _get_cands(_query, gdf, cands, query_id): 123 | _points = _query.iloc[cands[0]] 124 | 125 | df_cands = gdf.iloc[cands[1]] 126 | df_cands.rename(columns={'geometry': 'edge_geom'}, inplace=True) 127 | df_cands.loc[:, query_id] = _points.index 128 | df_cands.loc[:, "query_geom"] = _points.values 129 | 130 | return df_cands 131 | 132 | @timeit 133 | def _project(df_cands, project=True): 134 | # dist_p2c 135 | if not project: 136 | cal_proj_dist = lambda x: x['query_geom'].distance(x['edge_geom']) 137 | df_cands.loc[:, 'dist_p2c'] = df_cands.apply(cal_proj_dist, axis=1) 138 | 139 | return df_cands 140 | 141 | df_projs = linear_referencing_geom(df_cands['query_geom'], df_cands['edge_geom']) 142 | df_cands.loc[:, df_projs.keys()] = df_projs.values() 143 | # df_cands = gpd.GeoDataFrame(df_cands, crs=gdf.crs, geometry='proj_point') 144 | 145 | return df_cands 146 | 147 | 148 | def plot_candidates(cands): 149 | # TODO draw buffer 150 | from ..geo.vis import plot_geodata 151 | _, ax = plot_geodata(cands, color='r', tile_alpha=.6, alpha=0) 152 | 153 | cands.set_geometry('edge_geom').plot(ax=ax, column='dist_p2c', cmap='Reds_r', legend='candidates') 154 | if 'proj_point' in list(cands): 155 | cands.loc[:, 'proj_point'] = cands['proj_point'].apply(shapely.Point) 156 | cands.set_geometry('proj_point').plot(ax=ax, cmap='Reds_r') 157 | cands.set_geometry('query_geom').plot(ax=ax, marker='*', label='Point', zorder=9) 158 | 159 | return ax 160 | 161 | @timeit 162 | def _filter_candidate(df: gpd.GeoDataFrame, 163 | pid: str = 'pid', 164 | top_k: int = 5, 165 | ): 166 | """Filter candidates, which belongs to the same way, and pickup the nearest one. 167 | 168 | Args: 169 | df (gpd.GeoDataFrame): df candidates. 170 | top_k (int, optional): _description_. Defaults to 5. 171 | pid (str, optional): _description_. Defaults to 'pid'. 172 | 173 | Returns: 174 | gpd.GeoDataFrame: The filtered candidates. 175 | """ 176 | # origin_size = df.shape[0] 177 | df = df.sort_values([pid, 'dist_p2c'])\ 178 | .groupby(pid)\ 179 | .head(top_k)\ 180 | .reset_index(drop=True) 181 | 182 | return df 183 | -------------------------------------------------------------------------------- /mapmatching/geo/vis/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | TILEMAP_FLAG = True 3 | from tilemap import plot_geodata, add_basemap 4 | except: 5 | TILEMAP_FLAG = False 6 | 7 | def plot_geodata(data, *args, **kwargs): 8 | return None, data.plot() 9 | 10 | def add_basemap(ax, *args, **kwargs): 11 | return ax 12 | -------------------------------------------------------------------------------- /mapmatching/geo/vis/linestring.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def plot_linestring_with_arrows(gdf_line, ax, color='red'): 5 | coord_arrs = gdf_line.geometry.apply(lambda x: np.array(x.coords)) 6 | gdf_line.plot(ax=ax, color=color) 7 | 8 | for coords in coord_arrs: 9 | # refs: https://wizardforcel.gitbooks.io/matplotlib-user-guide/content/4.5.html 10 | mid = coords.shape[0] // 2 11 | ax.annotate('', xy=(coords[mid+1] + coords[mid]) / 2, xytext=coords[mid], 12 | arrowprops=dict(arrowstyle="-|>", color=color), 13 | zorder=9 14 | ) 15 | 16 | return 17 | -------------------------------------------------------------------------------- /mapmatching/geo/vis/point.py: -------------------------------------------------------------------------------- 1 | import math 2 | from . import plot_geodata 3 | 4 | def plot_points_with_dir(points, heading=None, arrowprops=dict(facecolor='blue', shrink=0.05, alpha=0.6)): 5 | """plot points with dirs 6 | 7 | Args: 8 | points (_type_): _description_ 9 | heading (_type_, optional): _description_. Defaults to None. 10 | arrowprops (_type_, optional): _description_. Defaults to dict(facecolor='blue', shrink=0.05, alpha=0.6). 11 | 12 | Returns: 13 | _type_: _description_ 14 | 15 | Example: 16 | ``` 17 | import shapely 18 | import geopandas as gpd 19 | from mapmatching.geo.vis.point import plot_point_with_dir 20 | 21 | gdf = gpd.GeoDataFrame({'geometry': [shapely.Point((113.912154, 22.784351))]}) 22 | plot_points_with_dir(gdf, 347) 23 | ``` 24 | """ 25 | types = points.geom_type.unique() 26 | assert len(types) == 1 and types[0] == "Point", "check points geom_type" 27 | fig, ax = plot_geodata(points, zorder=2) 28 | 29 | if not heading: 30 | return ax 31 | 32 | if isinstance(heading, (int, float)): 33 | heading = [heading] * points.shape[0] 34 | 35 | for i, geom in enumerate(points.geometry): 36 | x, y = geom.coords[0] 37 | x0, x1 = ax.get_xlim() 38 | aux_line_len = (x1-x0) / 12 39 | dy, dx = math.cos(heading[i]/180*math.pi) * aux_line_len, math.sin(heading[i]/180*math.pi) * aux_line_len 40 | ax.annotate('', xy=(x+dx, y+dy), xytext=(x,y), arrowprops=arrowprops, zorder=1) 41 | 42 | return ax 43 | 44 | -------------------------------------------------------------------------------- /mapmatching/graph/__init__.py: -------------------------------------------------------------------------------- 1 | from .geograph import GeoDigraph -------------------------------------------------------------------------------- /mapmatching/graph/astar.py: -------------------------------------------------------------------------------- 1 | import heapq 2 | import numpy as np 3 | from loguru import logger 4 | from collections import deque 5 | from haversine import haversine, Unit 6 | 7 | 8 | def calculate_nodes_dist(nodes:dict, src:int, dst:int, memo:dict={}, ll=True): 9 | assert src in nodes and dst in nodes, "Check the input o and d." 10 | if (src, dst) in memo: 11 | return memo[(src, dst)] 12 | 13 | if ll: 14 | _src = nodes[src] 15 | _dst = nodes[dst] 16 | _len = haversine( 17 | (_src['y'], _src['x']), 18 | (_dst['y'], _dst['x']), 19 | unit=Unit.METERS 20 | ) 21 | else: 22 | _len = nodes[src]['geometry'].distance(nodes[dst]['geometry']) 23 | 24 | return _len 25 | 26 | 27 | class PathPlanning: 28 | def __init__(self, graph: dict, nodes: dict, 29 | search_memo: dict = {}, nodes_dist_memo: dict = {}, 30 | max_steps: int = 2000, max_dist: int = 10000, level='debug', ll=True): 31 | 32 | self.graph = graph 33 | self.nodes = nodes 34 | self.search_memo = search_memo 35 | self.nodes_dist_memo = nodes_dist_memo 36 | self.max_steps = max_steps 37 | self.max_dist = max_dist 38 | self.level = level 39 | self.ll = ll 40 | 41 | def has_edge(self, src, dst): 42 | if src in self.graph and dst in self.graph: 43 | return True 44 | 45 | info = f"Trip ({src}, {dst})" + \ 46 | f"{', `src` not in graph' if src not in self.graph else ', '}" + \ 47 | f"{', `dst` not in graph' if dst not in self.graph else ''}" 48 | 49 | getattr(logger, self.level)(info) 50 | 51 | return False 52 | 53 | def search(self, src, dst): 54 | return NotImplementedError 55 | 56 | def reconstruct_path(self): 57 | return NotImplementedError 58 | 59 | 60 | class Astar(PathPlanning): 61 | def __init__(self, graph: dict, nodes: dict, 62 | search_memo: dict = {}, nodes_dist_memo: dict = {}, 63 | max_steps: int = 2000, max_dist: int = 10000, level='debug', ll=True): 64 | super().__init__(graph, nodes, search_memo, nodes_dist_memo, max_steps, max_dist, level, ll) 65 | 66 | def search(self, src, dst, max_steps=None, max_dist=None, weight='cost'): 67 | if src == dst: 68 | return {'status': 0, 'vpath': [src], 'cost': 0} 69 | 70 | if (src, dst) in self.search_memo: 71 | res = self.search_memo[(src, dst)] 72 | return res 73 | 74 | if not self.has_edge(src, dst): 75 | return {"status": 1, 'vpath': [], 'cost': np.inf} 76 | 77 | # init 78 | queue = [(0, src)] 79 | came_from = {src: None} 80 | distance = {src: 0} 81 | step_counter = 0 82 | 83 | max_steps = self.max_steps if max_steps is None else max_steps 84 | max_dist = self.max_dist if max_dist is None else max_dist 85 | 86 | # searching 87 | while queue: 88 | _, cur = heapq.heappop(queue) 89 | if cur == dst or step_counter > max_steps: 90 | break 91 | 92 | for nxt, attrs in self.graph[cur].items(): 93 | if nxt not in self.graph: 94 | continue 95 | 96 | new_cost = distance[cur] + attrs[weight] 97 | if nxt in distance and new_cost >= distance[nxt]: 98 | continue 99 | 100 | distance[nxt] = new_cost 101 | if distance[nxt] > max_dist: 102 | continue 103 | 104 | _h = calculate_nodes_dist(self.nodes, dst, nxt, self.nodes_dist_memo, self.ll) 105 | heapq.heappush(queue, (new_cost + _h, nxt) ) 106 | came_from[nxt] = cur 107 | 108 | step_counter += 1 109 | 110 | # abnormal situation 111 | if cur != dst: 112 | res = {"status": 2, 'vpath': [], 'cost': np.inf} 113 | self.search_memo[(src, dst)] = res 114 | return res 115 | 116 | # reconstruct path 117 | path = self.reconstruct_path(dst, came_from) 118 | res = {'status': 0, 'vpath': path, 'cost': distance[dst]} 119 | self.search_memo[(src, dst)] = res 120 | 121 | return res 122 | 123 | def reconstruct_path(self, dst, came_from): 124 | route, queue = [dst], deque([dst]) 125 | while queue: 126 | node = queue.popleft() 127 | if came_from[node] is None: 128 | continue 129 | route.append(came_from[node]) 130 | queue.append(came_from[node]) 131 | 132 | return route[::-1] 133 | 134 | 135 | if __name__ == "__main__": 136 | from stmm.graph import GeoDigraph 137 | network = GeoDigraph() 138 | network.load_checkpoint(ckpt='../../data/network/Shenzhen_graph_pygeos.ckpt') 139 | # network.to_postgis('shenzhen') 140 | 141 | from tqdm import tqdm 142 | from stmm.utils.serialization import load_checkpoint 143 | astar_search_memo = load_checkpoint('../../data/debug/astar_search_memo.pkl') 144 | -------------------------------------------------------------------------------- /mapmatching/graph/base.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from collections import defaultdict 4 | 5 | class Node: 6 | """ 7 | Define the node in the road network 8 | """ 9 | 10 | def __init__(self, id): 11 | self.val = id 12 | self.x, self.y = [float(i) for i in id.split(',')] 13 | self.prev = set() 14 | self.nxt = set() 15 | self.indegree = 0 16 | self.outdegree = 0 17 | 18 | def add(self, point): 19 | self.nxt.add(point) 20 | self.outdegree += 1 21 | 22 | point.prev.add(self) 23 | point.indegree += 1 24 | 25 | def check_0_out_more_2_in(self): 26 | return self.outdegree == 0 and self.indegree >= 2 27 | 28 | def move_nxt_to_prev(self, node): 29 | if node not in self.nxt: 30 | return False 31 | 32 | self.nxt.remove(node) 33 | self.prev.add(node) 34 | self.indegree += 1 35 | self.outdegree -= 1 36 | return True 37 | 38 | def move_prev_to_nxt(self, node): 39 | if node not in self.prev: 40 | return False 41 | 42 | self.prev.remove(node) 43 | self.nxt.add(node) 44 | self.indegree -= 1 45 | self.outdegree += 1 46 | return True 47 | 48 | 49 | class Digraph: 50 | def __init__(self, edges:list=None, nodes:dict=None, *args, **kwargs): 51 | """[summary] 52 | 53 | Args: 54 | edges (list, optional): Shape: (N, 2/3). Defaults to None. 55 | nodes (dict, optional): [description]. Defaults to None. 56 | """ 57 | self.graph = {} 58 | self.graph_r = {} 59 | self.edges = {} 60 | self.nodes = {} 61 | 62 | self.eid_2_od = {} 63 | self.max_eid = 0 64 | 65 | if edges is not None: 66 | self.build_graph(edges) 67 | 68 | if nodes is not None: 69 | assert isinstance(nodes, dict), "Check the Node format" 70 | self.nodes = nodes 71 | 72 | self.calculate_degree() 73 | 74 | def __str__(self): 75 | return "" 76 | 77 | def add_edge(self, start, end, length=None): 78 | for p in [start, end]: 79 | for g in [self.graph, self.graph_r]: 80 | if p in g: 81 | continue 82 | g[p] = {} 83 | 84 | self.graph[start][end] = {"eid": self.max_eid, "cost": length} 85 | self.graph_r[end][start] = {"eid": self.max_eid, "cost": length} 86 | self.eid_2_od[self.max_eid] = (start, end) 87 | self.max_eid += 1 88 | 89 | if length is not None: 90 | self.edges[(start, end)] = length 91 | 92 | pass 93 | 94 | def remove_edge(self, start, end): 95 | eid = self.get_eid(start, end) 96 | if eid is not None: 97 | del self.eid_2_od[eid] 98 | 99 | del self.graph[start][end] 100 | if len(self.graph[start]) == 0: 101 | del self.graph[start] 102 | 103 | del self.graph_r[end][start] 104 | if len(self.graph_r[end]) == 0: 105 | del self.graph_r[end] 106 | 107 | return True 108 | 109 | def get_eid(self, src, dst): 110 | item = self.graph.get(src, None) 111 | if item is None: 112 | return None 113 | 114 | r = item.get(dst, None) 115 | if r is None: 116 | return None 117 | 118 | return r.get('eid', None) 119 | 120 | def build_graph(self, edges): 121 | for edge in edges: 122 | start, end, length = edge 123 | assert not(np.isnan(start) or np.isnan(end)), f"Check the input ({start}, {end})" 124 | 125 | if isinstance(start, float): 126 | start = int(start) 127 | if isinstance(end, float): 128 | end = int(end) 129 | 130 | self.add_edge(start, end, length) 131 | 132 | return self.graph 133 | 134 | def clean_empty_set(self): 135 | for item in [self.graph_r, self.graph]: 136 | for i in list(item.keys()): 137 | if len(item[i]) == 0: 138 | del item[i] 139 | pass 140 | 141 | def calculate_degree(self,): 142 | self.clean_empty_set() 143 | self.degree = pd.merge( 144 | pd.DataFrame([[key, len(self.graph_r[key])] 145 | for key in self.graph_r], columns=['pid', 'indegree']), 146 | pd.DataFrame([[key, len(self.graph[key])] 147 | for key in self.graph], columns=['pid', 'outdegree']), 148 | how='outer', 149 | on='pid' 150 | ).fillna(0).astype(int).set_index('pid') 151 | 152 | return self.degree 153 | 154 | def get_origin_point(self,): 155 | 156 | return self.calculate_degree().reset_index().query( "indegree == 0 and outdegree != 0" ).pid.values 157 | 158 | def cal_nodes_dist(self, src, dst): 159 | return NotImplementedError 160 | 161 | def _simpify(self): 162 | """ 163 | Simplify the graph, namely combine the edges with 1 indegree and 1 out degree 164 | """ 165 | return NotImplementedError 166 | 167 | def search(self, src, dst, *args, **kwargs): 168 | return NotImplementedError 169 | 170 | def _get_aux_nodes(self, exclude_list=None): 171 | if getattr(self, 'degree') is None: 172 | self.calculate_degree() 173 | 174 | aux_nids = self.degree.query( "indegree == 1 and outdegree == 1" ).index.unique() 175 | if exclude_list is not None: 176 | aux_nids = [id for id in aux_nids if id not in exclude_list] 177 | 178 | return aux_nids 179 | 180 | """ transfrom """ 181 | def transform_vpath_to_epath(self, seq:np.array): 182 | if seq is None or len(seq) <= 1: 183 | return None 184 | 185 | eids = [self.get_eid(seq[i], seq[i+1]) 186 | for i in range(len(seq)-1)] 187 | 188 | return eids 189 | 190 | def transform_epath_to_vpath(self, path): 191 | ods = [self.eid_2_od[e][0] for e in path[:-1]] 192 | ods.extend(self.eid_2_od[path[-1]]) 193 | 194 | return ods -------------------------------------------------------------------------------- /mapmatching/graph/bi_astar.py: -------------------------------------------------------------------------------- 1 | import heapq 2 | import numpy as np 3 | from loguru import logger 4 | from haversine import haversine, Unit 5 | from .astar import PathPlanning 6 | 7 | 8 | class Bi_Astar(PathPlanning): 9 | def __init__(self, graph: dict, graph_r: dict, nodes: dict, 10 | search_memo: dict = {}, nodes_dist_memo: dict = {}, 11 | max_steps: int = 2000, max_dist: int = 10000, level='debug'): 12 | super().__init__(graph, nodes, search_memo, nodes_dist_memo, max_steps, max_dist, level) 13 | self.graph_r= graph_r 14 | 15 | def search(self, src, dst, max_steps=None, max_dist=None, level='debug'): 16 | status, info = self._check_od(src, dst, level) 17 | if not status: 18 | return info 19 | 20 | _memo = self._check_memo(src, dst) 21 | if _memo is not None: 22 | return _memo 23 | 24 | meet =self._searching(src, dst) 25 | if meet is None: 26 | return {"status": 2, 'vpath': [], 'cost': np.inf} 27 | 28 | path = self.extract_path(src, dst) 29 | cost = self.visited_backward[self.meet] + self.visited_forward[self.meet] 30 | res = {'status': 0, 'vpath': path, 'cost': cost} 31 | 32 | return res 33 | 34 | def _searching(self, src, dst): 35 | self.search_init(src, dst) 36 | 37 | def _helper(q1, q2): 38 | self.extend_queue(**q1) 39 | if self.meet is not None: 40 | return True 41 | 42 | self.extend_queue(**q2) 43 | if self.meet is not None: 44 | return True 45 | 46 | return False 47 | 48 | while self.queue_forward and self.queue_backward: 49 | if len(self.queue_forward) < len(self.queue_backward): 50 | if _helper(self.params_forward, self.params_backward): 51 | break 52 | else: 53 | if _helper(self.params_backward, self.params_forward): 54 | break 55 | 56 | if self.meet == -1: 57 | return -1 58 | 59 | return self.meet 60 | 61 | def search_init(self, src, dst): 62 | l0 = self.calculate_nodes_dist(src, dst) 63 | 64 | self.queue_forward = [] 65 | self.parent_forward = {src: None} 66 | self.visited_forward = {src: 0} 67 | 68 | self.queue_backward = [] 69 | self.parent_backward = {dst: None} 70 | self.visited_backward = {dst: 0} 71 | 72 | heapq.heappush(self.queue_forward, (l0, 0, src)) 73 | heapq.heappush(self.queue_backward, (l0, 0, dst)) 74 | 75 | self.params_forward = { 76 | 'dst': dst, 77 | 'queue': self.queue_forward, 78 | 'visited': self.visited_forward, 79 | 'opposite_visited': self.visited_backward, 80 | 'parent': self.parent_forward, 81 | 'graph': self.graph 82 | } 83 | 84 | self.params_backward = { 85 | 'dst': src, 86 | 'queue': self.queue_backward, 87 | 'visited': self.visited_backward, 88 | 'opposite_visited': self.visited_forward, 89 | 'parent': self.parent_backward, 90 | 'graph': self.graph_r 91 | } 92 | 93 | self.meet = None 94 | 95 | def extend_queue(self, dst, queue, visited, opposite_visited, parent, graph): 96 | _, dis, cur = heapq.heappop(queue) 97 | if cur not in graph: 98 | return None 99 | 100 | for nxt, cost in graph[cur].items(): 101 | nxt_cost = dis + cost 102 | if not self.is_valid(nxt, nxt_cost, graph, visited): 103 | continue 104 | 105 | visited[nxt] = nxt_cost 106 | parent[nxt] = cur 107 | if nxt in opposite_visited: 108 | self.meet = nxt 109 | return nxt 110 | 111 | _h = self.calculate_nodes_dist(nxt, dst) 112 | heapq.heappush(queue, (nxt_cost + _h, nxt_cost, nxt)) 113 | 114 | return None 115 | 116 | def is_valid(self, nxt, nxt_cost, graph, visited): 117 | if nxt not in graph: 118 | return False 119 | 120 | if nxt in visited and nxt_cost >= visited[nxt]: 121 | return False 122 | 123 | return True 124 | 125 | def calculate_nodes_dist(self, src: int, dst: int, type='coord'): 126 | assert src in self.nodes and dst in self.nodes, "Check the input o and d." 127 | if (src, dst) in self.nodes_dist_memo: 128 | return self.nodes_dist_memo[(src, dst)] 129 | 130 | if type == 'coord': 131 | _src = self.nodes[src] 132 | _dst = self.nodes[dst] 133 | _len = haversine( 134 | (_src['y'], _src['x']), 135 | (_dst['y'], _dst['x']), 136 | unit=Unit.METERS 137 | ) 138 | else: 139 | raise NotImplementedError 140 | 141 | return _len 142 | 143 | def extract_path(self, src, dst): 144 | # extract path for foreward part 145 | path_fore = [self.meet] 146 | s = self.meet 147 | 148 | while True: 149 | s = self.parent_forward[s] 150 | if s is None: 151 | break 152 | path_fore.append(s) 153 | 154 | # extract path for backward part 155 | path_back = [] 156 | s = self.meet 157 | 158 | while True: 159 | s = self.parent_backward[s] 160 | if s is None: 161 | break 162 | path_back.append(s) 163 | 164 | return list(reversed(path_fore)) + list(path_back) 165 | 166 | def _check_od(self, src, dst, level='debug'): 167 | if src in self.graph or dst in self.graph: 168 | return True, None 169 | 170 | info = f"Trip ({src}, {dst})" + \ 171 | f"{', `src` not in graph' if src not in self.graph else ', '}" + \ 172 | f"{', `dst` not in graph' if dst not in self.graph else ''}" 173 | 174 | getattr(logger, level)(info) 175 | 176 | return False, {"status": 1, 'vpath': [], 'cost': np.inf} 177 | 178 | def _check_memo(self, src, dst): 179 | if (src, dst) not in self.search_memo: 180 | return None 181 | 182 | return self.search_memo[(src, dst)] 183 | 184 | def plot_searching_boundary(self, path, network): 185 | points = set.union(set(self.visited_backward.keys()), 186 | set(self.visited_forward.keys())) 187 | ax = network.df_nodes.loc[points].plot() 188 | 189 | eids = network.transform_node_seq_to_edge_seq(path) 190 | network.df_edges.loc[eids].plot(ax=ax, label='Path') 191 | network.df_nodes.loc[self.visited_backward].plot( 192 | ax=ax, label='Backword', color='r', alpha=.8) 193 | network.df_nodes.query(f"nid == {self.meet}").plot( 194 | ax=ax,label='Meet', color='blue', alpha=.8, marker="*", zorder=8) 195 | network.df_nodes.loc[self.visited_forward].plot( 196 | ax=ax, label='Forword', color='y', alpha=.8) 197 | 198 | ax.legend() 199 | 200 | if __name__ == "__main__": 201 | from stmm.graph import GeoDigraph 202 | network = GeoDigraph() 203 | network.load_checkpoint(ckpt='../../data/network/Shenzhen_graph_pygeos.ckpt') 204 | # network.to_postgis('shenzhen') 205 | 206 | from stmm.utils.serialization import load_checkpoint 207 | astar_search_memo = load_checkpoint('../../data/debug/astar_search_memo.pkl') 208 | 209 | searcher = Bi_Astar(network.graph, network.graph_r, network.nodes) 210 | 211 | error_lst = [] 212 | for (src, dst), ans in astar_search_memo.items(): 213 | res = searcher.search(src, dst) 214 | cond = np.array(res['vpath']) == np.array(ans['vpath']) 215 | if isinstance(cond, np.ndarray): 216 | cond = cond.all() 217 | if not cond: 218 | # print(res['cost'] == ans['cost'], cond) 219 | print(f"\n\n({src}, {dst})\n\tans: {ans['vpath']}, {ans['cost']}\n\tres: {res['vpath']}, {res['cost']}") 220 | -------------------------------------------------------------------------------- /mapmatching/graph/geographx.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | 3 | class GeoGraph(nx.DiGraph): 4 | def __init__(self, incoming_graph_data=None, **attr): 5 | super().__init__(incoming_graph_data, **attr) 6 | 7 | 8 | """ vis """ 9 | def add_edge_map(self, ax, *arg, **kwargs): 10 | return 11 | 12 | """ property""" 13 | @property 14 | def crs(self): 15 | return self.df_edges.crs 16 | 17 | @property 18 | def epsg(self): 19 | return self.df_edges.crs.to_epsg() 20 | 21 | -------------------------------------------------------------------------------- /mapmatching/match/__int__.py: -------------------------------------------------------------------------------- 1 | from .code import STATUS -------------------------------------------------------------------------------- /mapmatching/match/candidatesGraph.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from shapely import LineString 4 | from geopandas import GeoDataFrame 5 | 6 | from ..utils import timeit 7 | from .status import CANDS_EDGE_TYPE 8 | from ..geo.azimuth import cal_coords_seq_azimuth 9 | from ..geo.ops.distance import coords_seq_distance 10 | from ..geo.ops.to_array import points_geoseries_2_ndarray 11 | from .misc import get_shared_line, merge_step_arrs 12 | 13 | 14 | def cal_traj_params(points, move_dir=True, check=False): 15 | """ 16 | Calculate trajectory parameters (e.g., euc dist, move dir) based on a series of points. 17 | 18 | Args: 19 | points (GeoSeries): A GeoSeries containing the trajectory points. 20 | move_dir (bool, optional): Whether to calculate the movement direction. Defaults to True. 21 | check (bool, optional): Whether to check for duplicate points. Defaults to False. 22 | 23 | Returns: 24 | DataFrame: A DataFrame containing the calculated trajectory parameters. 25 | 26 | Example: 27 | >>> points = gpd.GeoSeries([...]) # GeoSeries containing trajectory points 28 | >>> traj_params = cal_traj_params(points, move_dir=True, check=False) 29 | >>> print(traj_params) 30 | 31 | Notes: 32 | - The input points should be in a GeoSeries with a valid geometry column. 33 | - The DataFrame returned will contain columns such as 'pid_0', 'pid_1', 'd_euc' (Euclidean distance), 34 | and 'move_dir' (movement direction) if move_dir=True. 35 | 36 | """ 37 | coords = points_geoseries_2_ndarray(points.geometry) 38 | dist_arr, _ = coords_seq_distance(coords) 39 | idxs = points.index 40 | 41 | if check: 42 | zero_idxs = np.where(dist_arr==0)[0] 43 | if len(zero_idxs): 44 | print(f"Exists dumplicates points: {[(i, i+1) for i in zero_idxs]}") 45 | 46 | _dict = {'pid_0': idxs[:-1], 47 | 'pid_1': idxs[1:], 48 | 'd_euc': dist_arr} 49 | 50 | if move_dir: 51 | dirs = cal_coords_seq_azimuth(coords) 52 | _dict['move_dir'] = dirs 53 | 54 | res = pd.DataFrame(_dict) 55 | 56 | return res 57 | 58 | def identify_edge_flag(gt: pd.DataFrame, cands: GeoDataFrame, ratio_eps: float = 0.05, dist_eps: float = 5): 59 | """ 60 | Identify the type of querying the shortest path from the candidate `src` to `dst` on the graph. 61 | 62 | Args: 63 | gt (pd.DataFrame): The graph DataFrame. 64 | cands (GeoDataFrame): The DataFrame containing candidate edges. 65 | ratio_eps (float, optional): The ratio epsilon parameter. Defaults to 0.05. 66 | dist_eps (float, optional): The distance epsilon parameter. Defaults to 5. 67 | 68 | Returns: 69 | pd.DataFrame: The graph DataFrame with the 'flag' column appended. 70 | 71 | Example: 72 | >>> graph = pd.DataFrame([...]) # Graph DataFrame 73 | >>> candidates = gpd.GeoDataFrame([...]) # Candidate edges DataFrame 74 | >>> flagged_graph = identify_edge_flag(graph, candidates, ratio_eps=0.05, dist_eps=5) 75 | >>> print(flagged_graph) 76 | 77 | Notes: 78 | - The 'gt' DataFrame represents the graph and should contain necessary columns such as 'eid_0', 'eid_1', 79 | 'dist_0', 'step_0_len', 'step_n_len', etc. 80 | - The 'cands' DataFrame should contain candidate edges information, including columns such as 'pid', 'eid', 81 | 'seg_0', 'len_0', etc. 82 | - The 'ratio_eps' and 'dist_eps' parameters control the thresholds for identifying different edge types. 83 | - The resulting graph DataFrame will have an additional 'flag' column indicating the edge type. 84 | 85 | Refs: 86 | - Fast map matching, an algorithm integrating hidden Markov model with precomputation, Fig 4. 87 | """ 88 | # (src, dst) on the same edge 89 | gt.loc[:, 'flag'] = CANDS_EDGE_TYPE.NORMAL 90 | 91 | same_edge = gt.eid_0 == gt.eid_1 92 | tmp = gt['dist_0'] - gt['step_0_len'] 93 | cond_1 = tmp <= gt['step_n_len'] 94 | 95 | tmp = tmp.apply(lambda x: min(max(0, x - dist_eps), x * (1 - ratio_eps))) 96 | cond = tmp <= gt['step_n_len'] 97 | 98 | # Perform merging of adjacent nodes within a certain range (5 meter) 99 | cond_approx_points = cond & (~cond_1) 100 | _cands = cands[['pid', 'eid', 'seg_0', 'len_0']]\ 101 | .set_index(['pid', 'eid']).to_dict('index') 102 | # reset related params 103 | gt.loc[cond_approx_points, ['step_n', 'step_n_len']] = gt.loc[cond_approx_points].apply( 104 | lambda x: _cands[(x.pid_0, x.eid_0)].values(), axis=1, result_type='expand' 105 | ).rename(columns={0: 'step_n', 1: 'step_n_len'}) 106 | 107 | same_edge_normal = same_edge & cond 108 | gt.loc[same_edge_normal, 'flag'] = CANDS_EDGE_TYPE.SAME_SRC_FIRST 109 | gt.loc[same_edge_normal, ['src', 'dst']] = gt.loc[same_edge_normal, ['dst', 'src']].values 110 | 111 | same_edge_revert = same_edge & (~cond) 112 | gt.loc[same_edge_revert, 'flag'] = CANDS_EDGE_TYPE.SAME_SRC_LAST 113 | 114 | return gt 115 | 116 | @timeit 117 | def construct_graph( points, 118 | cands, 119 | common_attrs = ['pid', 'eid', 'dist', 'speed'], 120 | left_attrs = ['dst', 'len_1', 'seg_1'], 121 | right_attrs = ['src', 'len_0', 'seg_0', 'observ_prob'], 122 | rename_dict = { 123 | 'seg_0': 'step_n', 124 | 'len_0': 'step_n_len', 125 | 'seg_1': 'step_0', 126 | 'len_1': 'step_0_len', 127 | 'cost': 'd_sht'}, 128 | dir_trans = True, 129 | gt_keys = ['pid_0', 'eid_0', 'eid_1'] 130 | ): 131 | """ 132 | Construct the candiadte graph (level, src, dst) for spatial and temporal analysis. 133 | 134 | Parameters: 135 | path = step_0 + step_1 + step_n 136 | """ 137 | layer_ids = np.sort(cands.pid.unique()) 138 | prev_layer_dict = {cur: layer_ids[i] 139 | for i, cur in enumerate(layer_ids[1:])} 140 | prev_layer_dict[layer_ids[0]] = -1 141 | 142 | # left 143 | left = cands[common_attrs + left_attrs] 144 | left.loc[:, 'mgd'] = left.pid 145 | 146 | # right 147 | right = cands[common_attrs + right_attrs] 148 | right.loc[:, 'mgd'] = right.pid.apply(lambda x: prev_layer_dict[x]) 149 | right.query("mgd >= 0", inplace=True) 150 | 151 | # Cartesian product 152 | gt = left.merge(right, on='mgd', suffixes=["_0", '_1'])\ 153 | .drop(columns='mgd')\ 154 | .reset_index(drop=True)\ 155 | .rename(columns=rename_dict) 156 | 157 | identify_edge_flag(gt, cands) 158 | traj_info = cal_traj_params(points.loc[cands.pid.unique()], move_dir=dir_trans) 159 | 160 | gt = gt.merge(traj_info, on=['pid_0', 'pid_1']) 161 | gt.loc[:, ['src', 'dst']] = gt.loc[:, ['src', 'dst']].astype(np.int64) 162 | 163 | if gt_keys: 164 | gt.set_index(gt_keys, inplace=True) 165 | 166 | return gt 167 | 168 | def get_shortest_geometry(gt:GeoDataFrame, geom='geometry', format='LineString'): 169 | """ 170 | Generate the shortest path geometry based on the given conditions. 171 | 172 | Parameters: 173 | gt (GeoDataFrame): A geospatial dataframe containing geometry objects and other attributes. 174 | geom (str, optional): The column name for the geometry objects. Default is 'geometry'. 175 | format (str, optional): The format of the returned geometry objects. Available options are 'LineString' or 'array'. 176 | Default is 'LineString'. 177 | 178 | Returns: 179 | GeoDataFrame: An updated geospatial dataframe with the shortest path geometry objects. 180 | 181 | Notes: 182 | - Only 'LineString' and 'array' formats are supported. 183 | - The input GeoDataFrame must have a 'flag' column indicating whether it represents the shortest path. 184 | 185 | Example: 186 | >>> shortest_geo = get_shortest_geometry(geo_data, format='array') 187 | >>> print(shortest_geo) 188 | 189 | Raises: 190 | - AssertionError: If the provided format is not supported. 191 | """ 192 | assert format in ['LineString', 'array'] 193 | 194 | # FIXME: 1) step_1 is None; 2) same edge:w2h, level=27, 555->555 195 | mask = gt.flag == 1 196 | gt.loc[mask, geom] = gt.loc[mask].apply(lambda x: 197 | get_shared_line(x.step_0, x.step_n), axis=1) 198 | gt.loc[~mask, geom] = gt.loc[~mask].apply( 199 | merge_step_arrs, axis=1) 200 | 201 | if format == 'LineString': 202 | gt.loc[:, geom] = gt[geom].apply(LineString) 203 | 204 | return gt 205 | -------------------------------------------------------------------------------- /mapmatching/match/dir_similarity.py: -------------------------------------------------------------------------------- 1 | from geopandas import GeoDataFrame 2 | 3 | from ..geo.azimuth import cal_linestring_azimuth_cos_dist 4 | from .candidatesGraph import get_shortest_geometry 5 | 6 | def cal_dir_prob(gt:GeoDataFrame, geom='geometry'): 7 | # TODO: 使用 sub_string 代替现有的情况 8 | # Add: dir_prob 9 | def _cal_dir_similarity(x): 10 | return cal_linestring_azimuth_cos_dist(x[geom], x['move_dir'], weight=True) 11 | 12 | gt = get_shortest_geometry(gt, geom, format='array') 13 | gt.loc[:, 'dir_prob'] = gt.apply(_cal_dir_similarity, axis=1) 14 | 15 | return gt 16 | 17 | -------------------------------------------------------------------------------- /mapmatching/match/geometricAnalysis.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import geopandas as gpd 3 | from ..geo.query import get_k_neigh_geoms 4 | 5 | 6 | def cal_observ_prob(dist, bias=0, deviation=20, normal=True): 7 | """The obervation prob is defined as the likelihood that a GPS sampling point `p_i` 8 | matches a candidate point `C_ij` computed based on the distance between the two points. 9 | 10 | Args: 11 | df (gpd.GeoDataFrame): Distance series or arrays. 12 | bias (float, optional): GPS measurement error bias. Defaults to 0. 13 | deviation (float, optional): GPS measurement error deviation. Defaults to 20. 14 | normal (bool, optional): Min-Max Scaling. Defaults to False. 15 | 16 | Returns: 17 | _type_: _description_ 18 | """ 19 | observ_prob_factor = 1 / (np.sqrt(2 * np.pi) * deviation) 20 | 21 | def f(x): return observ_prob_factor * \ 22 | np.exp(-np.power(x - bias, 2)/(2 * np.power(deviation, 2))) 23 | 24 | _dist = f(dist) 25 | if normal: 26 | _dist /= _dist.max() 27 | 28 | return np.sqrt(_dist) 29 | 30 | def analyse_geometric_info(points: gpd.GeoDataFrame, 31 | edges: gpd.GeoDataFrame, 32 | top_k: int = 5, 33 | radius: float = 50, 34 | pid: str = 'pid', 35 | eid: str = 'eid', 36 | ): 37 | # TODO improve effeciency: get_k_neigbor_edges 50 %, project_point_to_line_segment 50 % 38 | cands, _ = get_k_neigh_geoms(points.geometry, edges, 39 | query_id=pid, project=True, top_k=top_k, 40 | keep_geom=True, radius=radius) 41 | if cands is not None: 42 | cands.loc[:, 'observ_prob'] = cal_observ_prob(cands.dist_p2c) 43 | 44 | return cands 45 | 46 | -------------------------------------------------------------------------------- /mapmatching/match/io.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import geopandas as gpd 3 | from ..geo.coord.coordTransfrom_shp import coord_transfer 4 | from ..geo.ops.simplify import simplify_trajetory_points 5 | 6 | 7 | def load_points(fn, simplify: bool = False, dp_thres: int = None, crs: int = None, in_sys: str = 'wgs', out_sys: str = 'wgs'): 8 | # BUG 重复节点需删除 9 | traj = gpd.read_file(fn, encoding='utf-8') 10 | if crs is not None: 11 | traj.set_crs(crs, allow_override=True, inplace=True) 12 | 13 | if 'time' in traj.columns: 14 | traj.time = pd.to_datetime( 15 | traj['time'], format='%Y-%m-%d %H:%M:%S') 16 | 17 | traj = coord_transfer(traj, in_sys, out_sys) 18 | 19 | if simplify: 20 | traj_bak = traj.copy() 21 | traj = traj = simplify_trajetory_points(traj, dp_thres, inplace=True) 22 | else: 23 | traj_bak = None 24 | traj = traj 25 | 26 | return traj, traj_bak 27 | -------------------------------------------------------------------------------- /mapmatching/match/metric.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from ..geo.metric import lcss, edr, erp 4 | from ..geo.ops.resample import resample_polyline_seq_to_point_seq, resample_point_seq 5 | 6 | 7 | def eval(traj, res=None, path=None, resample=5, eps=10, metric='lcss', g=None): 8 | """ 9 | lcss 的 dp 数组 循环部分,使用numba 加速,这个环节可以降低 10% 的时间消耗(20 ms) 10 | """ 11 | # BUG 12 | assert res is not None or path is not None 13 | assert metric in ['lcss', 'edr', 'erp'] 14 | 15 | if path is None: 16 | path = self.transform_res_2_path(res) 17 | 18 | if traj.crs.to_epsg() != path.crs.to_epsg(): 19 | traj = traj.to_crs(path.crs.to_epsg()) 20 | 21 | if resample: 22 | _, path_coords_np = resample_polyline_seq_to_point_seq(path.geometry, step=resample,) 23 | _, traj_coords_np = resample_point_seq(traj.geometry, step=resample) 24 | else: 25 | path_coords_np = np.concatenate(path.geometry.apply(lambda x: x.coords[:]).values) 26 | traj_coords_np = np.concatenate(traj.geometry.apply(lambda x: x.coords[:]).values) 27 | 28 | eval_funs = { 29 | 'lcss': [lcss, (traj_coords_np, path_coords_np, eps, self.ll)], 30 | 'edr': [edr, (traj_coords_np, path_coords_np, eps)], 31 | 'edp': [erp, (traj_coords_np, path_coords_np, g)] 32 | } 33 | _eval = eval_funs[metric] 34 | 35 | return _eval[0](*_eval[1]) -------------------------------------------------------------------------------- /mapmatching/match/misc.py: -------------------------------------------------------------------------------- 1 | import numba 2 | import shapely 3 | import warnings 4 | import numpy as np 5 | from shapely import LineString 6 | from shapely.ops import linemerge 7 | 8 | def merge_step_arrs(x, check=True): 9 | lst = [i for i in [x.step_0, x.step_1, x.step_n] if isinstance(i, (np.ndarray, list))] 10 | if len(lst) == 0: 11 | warnings.warn("All geoms are None") 12 | return None 13 | 14 | if len(lst) == 1: 15 | return lst[0] 16 | 17 | # TODO Nodes may be duplicated at the join 18 | coords = np.concatenate(lst) 19 | 20 | return coords 21 | 22 | @numba.jit 23 | def get_shared_arr(arr1:np.ndarray, arr2:np.ndarray): 24 | lst = [arr1[0]] 25 | right = 0 26 | left = 1 27 | n, m = len(arr1), len(arr2) 28 | 29 | while left < n: 30 | while right < m and np.all(arr1[left] != arr2[right]): 31 | right += 1 32 | if right >= m: 33 | break 34 | lst.append(arr1[left]) 35 | left += 1 36 | 37 | if np.all(arr2[-1] != lst[-1]): 38 | lst.append(arr2[-1]) 39 | 40 | return lst 41 | 42 | def get_shared_line(line_1:np.ndarray, line_2:np.ndarray): 43 | if line_1 is not None: 44 | warnings.warn('line_1 is empty') 45 | coords = line_2 46 | elif line_2 is not None: 47 | warnings.warn('line_2 is empty') 48 | coords = line_1 49 | else: 50 | coords = get_shared_arr(line_1, line_2) 51 | 52 | return coords 53 | 54 | 55 | if __name__ == "__main__": 56 | # get_shared_line 57 | line_1 = LineString([[.9, .9], [1, 1], [2,2]]) 58 | line_2 = LineString([[0,0], [1,1], [1.5, 1.5]]) 59 | print(get_shared_line(line_1, line_2)) 60 | 61 | -------------------------------------------------------------------------------- /mapmatching/match/postprocess.py: -------------------------------------------------------------------------------- 1 | import shapely 2 | import numpy as np 3 | import pandas as pd 4 | import geopandas as gpd 5 | from geopandas import GeoDataFrame 6 | 7 | from .status import STATUS 8 | from ..utils.timer import timeit 9 | from ..graph import GeoDigraph 10 | from ..geo.ops.point2line import project_points_2_linestrings 11 | 12 | 13 | @timeit 14 | def get_path(rList:gpd.GeoDataFrame, 15 | graph:gpd.GeoDataFrame, 16 | cands:gpd.GeoDataFrame, 17 | metric = {}, 18 | prob_thres = .8 19 | ): 20 | """Get path by matched sequence node. 21 | 22 | Args: 23 | rList ([type]): [description] 24 | graph_t ([type]): [description] 25 | net ([type]): [description] 26 | 27 | Returns: 28 | [list]: [path, connectors, steps] 29 | 30 | Example: 31 | rList 32 | | | pid | eid | src | dst |\n 33 | |---:|------:|------:|------------:|------------:|\n 34 | | 0 | 0 | 17916 | 8169270272 | 2376751183 |\n 35 | | 1 | 1 | 17916 | 8169270272 | 2376751183 | 36 | """ 37 | steps = rList.copy() 38 | steps.loc[:, 'eid_1'] = steps.eid.shift(-1).fillna(0).astype(int) 39 | idxs = steps[['pid', 'eid', 'eid_1']].values[:-1].tolist() 40 | steps = graph.loc[idxs, ['epath', 'd_sht', 'avg_speed', 'dist_prob', 'trans_prob']].reset_index() 41 | 42 | # FIXME 使用 numba 加速 loop 测试 43 | extract_eids = lambda x: np.concatenate([[x.eid_0], x.epath]) if x.epath else [x.eid_0] 44 | eids = np.concatenate(steps.apply(extract_eids, axis=1)) 45 | eids = np.append(eids, [steps.iloc[-1].eid_1]) 46 | keep_cond = np.append([True], eids[:-1] != eids[1:]) 47 | eids_lst = eids[keep_cond].tolist() 48 | 49 | res = {'epath': eids_lst} 50 | step_0, step_n = _get_first_and_step_n(cands, rList) 51 | 52 | # Case: one step 53 | if len(eids_lst) == 1: 54 | # tmp = get_shared_arr(step_0, step_n) 55 | res['step_0'] = step_0 56 | res['step_n'] = step_n 57 | if metric.get('prob', 1) < prob_thres: 58 | metric['status'] = STATUS.FAILED 59 | else: 60 | metric['status'] = STATUS.SAME_LINK 61 | 62 | return res, None 63 | 64 | # update first/last step 65 | n = len(eids_lst) - 1 66 | assert n > 0, "Check od list" 67 | res['step_0'] = step_0 68 | res['step_n'] = step_n 69 | res['dist'] = steps.d_sht.sum() 70 | res['avg_speed'] = np.average(steps['avg_speed'].values, weights = steps['d_sht'].values) 71 | 72 | # update metric 73 | coef = 1 / len(steps.dist_prob) 74 | dist_prob = np.prod(steps.dist_prob) 75 | trans_prob = np.prod(steps.trans_prob) 76 | metric["norm_prob"], metric["dist_prob"], metric["trans_prob"] = \ 77 | np.power([metric['prob'], dist_prob, trans_prob], coef) 78 | if "dir_prob" in list(graph): 79 | metric["dir_prob"] = metric["trans_prob"] / metric["dist_prob"] 80 | 81 | # status 82 | if metric["trans_prob"] < prob_thres: 83 | metric['status'] = STATUS.FAILED 84 | else: 85 | metric['status'] = STATUS.SUCCESS 86 | 87 | return res, steps 88 | 89 | def _get_first_and_step_n(cands, rList): 90 | step_0 = cands.query( 91 | f'pid == {rList.iloc[0].pid} and eid == {rList.iloc[0].eid}').iloc[0] 92 | step_n = cands.query( 93 | f'pid == {rList.iloc[-1].pid} and eid == {rList.iloc[-1].eid}').iloc[0] 94 | 95 | cal_offset = lambda x: x['len_0'] / (x['len_0'] + x['len_1']) 96 | 97 | return cal_offset(step_0), cal_offset(step_n) 98 | 99 | def transform_mathching_res_2_path(res: dict, net: GeoDigraph, ori_crs: bool=True, attrs: list=None): 100 | if attrs is None: 101 | attrs = ['eid', 'way_id', 'src', 'dst', 'name', 'road_type', 'link', 'speed', 'dist', 'geometry'] 102 | 103 | path = net.get_edge(res['epath'], attrs, reset_index=True) 104 | 105 | _len = len(res['epath']) 106 | if _len == 1: 107 | path.loc[0, 'dist'] *= res['step_n'] - res['step_0'] 108 | path.loc[0, 'geometry'] = shapely.ops.substring( 109 | path.iloc[0].geometry, res['step_0'], res['step_n'], normalized=True) 110 | else: 111 | path.loc[0, 'dist'] *= 1 - res['step_0'] 112 | path.loc[0, 'geometry'] = shapely.ops.substring( 113 | path.iloc[0].geometry, res['step_0'], 1, normalized=True) 114 | 115 | path.loc[_len - 1, 'dist'] *= res['step_n'] 116 | path.loc[_len - 1, 'geometry'] = shapely.ops.substring( 117 | path.iloc[-1].geometry, 0, res['step_n'], normalized=True) 118 | 119 | path = path[~path.geometry.is_empty] 120 | if ori_crs: 121 | path = path.to_crs(res['ori_crs']) 122 | 123 | return path 124 | 125 | def project(points: GeoDataFrame, path: GeoDataFrame, keep_attrs=['eid', 'proj_point'], normalized=True, reset_geom=True): 126 | """ 127 | Project points onto a path represented by a GeoDataFrame. 128 | 129 | Args: 130 | points (GeoDataFrame): Points to be projected. 131 | path (GeoDataFrame): Path to project the points onto. 132 | keep_attributes (list, optional): Attributes to keep in the projected points. Defaults to ['eid', 'proj_point']. 133 | normalize (bool, optional): Whether to normalize the projection. Defaults to True. 134 | reset_geometry (bool, optional): Whether to reset the geometry column in the projected points. Defaults to True. 135 | 136 | Returns: 137 | GeoDataFrame: Projected points. 138 | 139 | Example: 140 | projected_points = project_points(points, path) 141 | """ 142 | _points = points[[points.geometry.name]] 143 | ps = project_points_2_linestrings(_points, path.to_crs(points.crs), normalized=normalized) 144 | 145 | if keep_attrs: 146 | ps = ps[keep_attrs] 147 | 148 | ps = gpd.GeoDataFrame(pd.concat([_points, ps], axis=1), crs=points.crs) 149 | if reset_geom: 150 | ps.loc[:, 'ori_geom'] = points.geometry.apply(lambda x: x.wkt) 151 | ps.set_geometry('proj_point', inplace=True) 152 | ps.drop(columns=['geometry'], inplace=True) 153 | 154 | return ps 155 | -------------------------------------------------------------------------------- /mapmatching/match/spatialAnalysis.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | import numpy as np 3 | from geopandas import GeoDataFrame 4 | 5 | from ..graph import GeoDigraph 6 | from .dir_similarity import cal_dir_prob 7 | from .candidatesGraph import construct_graph 8 | 9 | 10 | def cal_dist_prob(gt: GeoDataFrame, net: GeoDigraph, max_steps: int = 2000, max_dist: int = 10000, eps: float = 1e-6): 11 | """ 12 | Calculate the distance probability for each edge in the graph. 13 | 14 | Args: 15 | gt (GeoDataFrame): The graph GeoDataFrame. 16 | net (GeoDigraph): The network GeoDigraph. 17 | max_steps (int, optional): The maximum number of steps for route planning. Defaults to 2000. 18 | max_dist (int, optional): The maximum distance for route planning. Defaults to 10000. 19 | eps (float, optional): The epsilon value for comparing distances. Defaults to 1e-6. 20 | 21 | Returns: 22 | GeoDataFrame: The graph GeoDataFrame with additional columns 'd_sht' and 'dist_prob'. 23 | 24 | Example: 25 | >>> graph = GeoDataFrame([...]) # Graph GeoDataFrame 26 | >>> network = GeoDigraph([...]) # Network GeoDigraph 27 | >>> graph = cal_dist_prob(graph, network, max_steps=3000, max_dist=15000, eps=1e-5) 28 | >>> print(graph) 29 | 30 | Notes: 31 | - The 'gt' GeoDataFrame should contain the graph data with required columns including 'flag', 'cost', 'avg_speed', 'epath', 'coords', 'step_0_len', 'step_n_len', 'dist_0', 'd_euc'. 32 | - The 'net' GeoDigraph should be a network representation used for route planning. 33 | - The 'max_steps' parameter specifies the maximum number of steps for route planning. 34 | - The 'max_dist' parameter specifies the maximum distance for route planning. 35 | - The 'eps' parameter is used for comparing distances and should be a small positive value. 36 | - The function calculates the shortest paths and temporal probabilities for each edge in the graph. 37 | - It adds the following columns to the 'gt' GeoDataFrame: 38 | - 'cost': The cost of the shortest path. 39 | - 'avg_speed': The average speed on the shortest path. 40 | - 'epath': The edge path of the shortest path. 41 | - 'step_1': The first step of the shortest path. 42 | - 'd_sht': The total distance of the shortest path. 43 | - 'dist_prob': The distance probability for the edge. 44 | - The function modifies the 'gt' GeoDataFrame in place and returns the modified GeoDataFrame. 45 | """ 46 | 47 | assert 'flag' in gt, "Check the attribute `flag` in gt or not" 48 | if gt.empty: 49 | warnings.warn("Empty graph layer") 50 | return gt 51 | 52 | sp_attrs = ['cost', "avg_speed", 'epath', 'coords'] 53 | gt_sp_attrs = ['cost', "avg_speed", 'epath', 'step_1'] 54 | rout_planning = lambda x: net.search(x.dst, x.src, max_steps, max_dist) 55 | paths = gt.apply(rout_planning, axis=1, result_type='expand')[sp_attrs] 56 | gt.loc[:, gt_sp_attrs] = paths.values 57 | 58 | cal_temporal_prob(gt) 59 | 60 | gt.loc[:, 'd_sht'] = gt.cost + gt.step_0_len + gt.step_n_len 61 | 62 | # OD is on the same edge, but the starting point is relatively ahead of the endpoint 63 | flag_1_idxs = gt.query("flag == 1").index 64 | if len(flag_1_idxs): 65 | gt.loc[flag_1_idxs, ['epath', 'step_1']] = None, None 66 | gt.loc[flag_1_idxs, 'd_sht'] = gt.step_0_len + gt.step_n_len - gt.dist_0 67 | 68 | idx = gt.query(f"flag == 1 and d_sht < {eps}").index 69 | gt.loc[idx, 'd_sht'] = gt.d_euc 70 | 71 | # distance trans prob 72 | dist = gt.d_euc / gt.d_sht 73 | mask = dist > 1 74 | dist[mask] = 1 / dist[mask] 75 | gt.loc[:, 'dist_prob'] = dist 76 | 77 | return gt 78 | 79 | def cal_temporal_prob(gt: GeoDataFrame, eps=1e-6): 80 | """ 81 | Calculate the temporal probability for each edge in the graph. 82 | 83 | Args: 84 | gt (GeoDataFrame): The graph GeoDataFrame. 85 | eps (float, optional): The epsilon value for handling infinite or zero weights. Defaults to 1e-6. 86 | 87 | Returns: 88 | GeoDataFrame: The graph GeoDataFrame with additional column 'avg_speed'. 89 | 90 | Example: 91 | >>> graph = GeoDataFrame([...]) # Graph GeoDataFrame 92 | >>> graph = cal_temporal_prob(graph, eps=1e-5) 93 | >>> print(graph) 94 | 95 | Notes: 96 | - The 'gt' GeoDataFrame should contain the graph data with required columns including 'speed_0', 'speed_1', 'avg_speed', 'step_0_len', 'step_n_len', 'cost'. 97 | - The 'eps' parameter is used for handling infinite or zero weights and should be a small positive value. 98 | - The function calculates the average speed for each edge based on the given weights. 99 | - It adds the 'avg_speed' column to the 'gt' GeoDataFrame. 100 | - The function modifies the 'gt' GeoDataFrame in place and returns the modified GeoDataFrame. 101 | """ 102 | speeds = gt[['speed_0', 'speed_1', 'avg_speed']].values 103 | weights = gt[['step_0_len', 'step_n_len', 'cost']].values 104 | weights[weights == np.inf] = eps 105 | weights[weights == 0] = eps 106 | avg_speeds = np.average(speeds, weights=weights, axis=1) 107 | 108 | gt.loc[:, 'avg_speed'] = avg_speeds 109 | # gt.loc[:, 'eta'] = gt.d_sht.values / avg_speeds 110 | 111 | return gt 112 | 113 | def cal_trans_prob(gt, geometry, dir_trans): 114 | if not dir_trans: 115 | gt.loc[:, 'trans_prob'] = gt.dist_prob 116 | return gt 117 | 118 | cal_dir_prob(gt, geometry) 119 | gt.loc[:, 'trans_prob'] = gt.dist_prob * gt.dir_prob 120 | 121 | return gt 122 | 123 | def analyse_spatial_info(net: GeoDigraph, 124 | points: GeoDataFrame, 125 | cands: GeoDataFrame, 126 | dir_trans=False, 127 | max_steps: int = 2e3, 128 | max_dist: int = 1e5, 129 | gt_keys: list = ['pid_0', 'eid_0', 'eid_1'], 130 | geometry='whole_path'): 131 | """ 132 | Geometric and topological info, the product of `observation prob` and the `transmission prob` 133 | """ 134 | gt = construct_graph(points, cands, dir_trans=dir_trans, gt_keys=gt_keys) 135 | 136 | gt = cal_dist_prob(gt, net, max_steps, max_dist) 137 | cal_trans_prob(gt, geometry, dir_trans) 138 | 139 | return gt 140 | 141 | def get_trans_prob_bet_layers(gt, net, dir_trans=True, geometry='path'): 142 | """ 143 | For beam-search 144 | """ 145 | if gt.empty: 146 | return gt 147 | 148 | gt = cal_dist_prob(gt, net) 149 | cal_trans_prob(gt, geometry, dir_trans) 150 | 151 | return gt 152 | -------------------------------------------------------------------------------- /mapmatching/match/status.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | class STATUS: 4 | SUCCESS = 0 # 成功匹配 5 | SAME_LINK = 1 # 所有轨迹点位于同一条线上 6 | ONE_POINT = 2 # 所有轨迹点位于同一个点上 7 | NO_CANDIDATES = 3 # 轨迹点无法映射到候选边上 8 | FAILED = 4 # 匹配结果,prob低于阈值 9 | UNKNOWN = 99 10 | 11 | class CANDS_EDGE_TYPE: 12 | NORMAL = 0 # od 不一样 13 | SAME_SRC_FIRST = 1 # od 位于同一条edge上,但起点相对终点位置偏前 14 | SAME_SRC_LAST = 2 # 相对偏后 15 | -------------------------------------------------------------------------------- /mapmatching/match/temporalAnalysis.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | 4 | 5 | def cos_similarity(self, path_, v_cal=30): 6 | # TODO cos similarity for speed 7 | # path_ = [5434742616, 7346193109, 7346193114, 5434742611, 7346193115, 5434742612, 7346193183, 7346193182] 8 | seg = [[path_[i-1], path_[i]] for i in range(1, len(path_))] 9 | v_roads = pd.DataFrame(seg, columns=['src', 'dst']).merge(self.edges, on=['src', 'dst']).v.values 10 | 11 | num = np.sum(v_roads.T * v_cal) 12 | denom = np.linalg.norm(v_roads) * np.linalg.norm([v_cal for x in v_roads]) 13 | cos = num / denom 14 | 15 | return cos 16 | 17 | -------------------------------------------------------------------------------- /mapmatching/match/topologicalAnalysis.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wenke727/ST-MapMatching/2b88c219142cfc1d1460669027798538ee0b2ad0/mapmatching/match/topologicalAnalysis.py -------------------------------------------------------------------------------- /mapmatching/match/viterbi.py: -------------------------------------------------------------------------------- 1 | import heapq 2 | import numpy as np 3 | import pandas as pd 4 | from loguru import logger 5 | from collections import defaultdict 6 | 7 | from .spatialAnalysis import get_trans_prob_bet_layers 8 | from ..utils import Timer, timeit 9 | 10 | 11 | def cal_prob_func(x, y, mode): 12 | if mode == '+': 13 | return x + y 14 | elif mode == '*': 15 | return x * y 16 | 17 | def merge_k_heapq(arrays, count=100): 18 | queue, res = [], [] 19 | eid_set = set() 20 | 21 | for eid, arr in arrays.items(): 22 | if len(arr) == 0: 23 | continue 24 | heapq.heappush(queue, (arr[0][0], eid)) 25 | 26 | while queue and count: 27 | _, eid = heapq.heappop(queue) 28 | prob, keys = heapq.heappop(arrays[eid]) 29 | count -= 1 30 | 31 | if arrays[eid]: 32 | heapq.heappush(queue, (arrays[eid][0][0], eid)) 33 | if eid not in eid_set: 34 | res.append(list(keys) + [-prob]) 35 | eid_set.add(eid) 36 | 37 | return res 38 | 39 | def prune_layer(df_layer, level, scores, start_level=3, prune=True, trim_factor=.75, use_pandas=False): 40 | if start_level > level: 41 | df = df_layer[['prob']].sort_values('prob', ascending=False)\ 42 | .groupby('eid_1')\ 43 | .head(1).reset_index() 44 | 45 | return df.set_index('eid_1') 46 | 47 | # prune -> pick the most likely one 48 | 49 | if use_pandas: 50 | _max_prob = df_layer['prob'].max() 51 | df = df_layer[['prob']].sort_values('prob', ascending=False)\ 52 | .head(100 if prune else 5)\ 53 | .query(f"prob > {_max_prob * trim_factor}")\ 54 | .groupby('eid_1')\ 55 | .head(1).reset_index() 56 | else: 57 | ps = df_layer.apply(lambda x: scores[x.name[1]] * x.prob, axis=1) 58 | prob_thred = ps.max() * trim_factor 59 | arrs = defaultdict(list) 60 | for row in df_layer[['prob']].itertuples(): 61 | idx, prob = getattr(row, "Index"), getattr(row, "prob") 62 | if prob < prob_thred: 63 | continue 64 | heapq.heappush(arrs[idx[2]], (-prob, idx)) 65 | 66 | records = merge_k_heapq(arrs, 100 if prune else 5) 67 | df = pd.DataFrame(records, columns=['pid_0', 'eid_0', 'eid_1', 'prob']) 68 | 69 | return df.set_index('eid_1') 70 | 71 | def reconstruct_path(f_score, prev_path): 72 | epath = [] 73 | state = None 74 | end_probs = [] 75 | 76 | for idx in range(len(f_score) - 1, 0, -1): 77 | if state is None: 78 | state = get_max_state(f_score, idx) 79 | if state is None: 80 | continue 81 | end_probs.append(f_score[idx][state]) 82 | 83 | cur = (idx, state) 84 | if idx not in prev_path or state not in prev_path[idx]: 85 | state = None 86 | continue 87 | prev = prev_path[idx].get(state) 88 | if not epath or cur != epath[-1]: 89 | epath.append(cur) 90 | epath.append(prev) 91 | state = prev[1] 92 | 93 | epath = epath[::-1] 94 | 95 | return epath, sum(end_probs) / len(end_probs) 96 | 97 | def get_max_state(f_score, idx): 98 | f = f_score[idx] 99 | if len(f) == 0: 100 | return None 101 | return max(f, key=f.get) 102 | 103 | def print_level(df_layer): 104 | f = lambda x: sorted(df_layer.index.get_level_values(x).unique()) 105 | return f"{f(1)} -> {f(2)}" 106 | 107 | def find_matched_sequence(cands, gt, net, dir_trans=True, mode='*', prune_factor=0.75, prune_start_layer=3, level='trace'): 108 | # Initialize 109 | times = [] 110 | timer = Timer() 111 | 112 | gt_beam = [] 113 | layer_ids = np.sort(cands.pid.unique()) 114 | start_prob = cands.query("pid == 0").set_index('eid')['observ_prob'].to_dict() 115 | f_score = [start_prob] 116 | prev_path = defaultdict(dict) 117 | prev_path[0] = {st: None for st in start_prob} 118 | prev_states = list(start_prob.keys()) 119 | 120 | for idx, lvl in enumerate(layer_ids[:-1]): 121 | df_layer = gt.query(f"pid_0 == @lvl and eid_0 in @prev_states") 122 | if df_layer.empty: 123 | print(f"Matching traj break at idx: {idx}, level: {lvl}") 124 | df_layer = gt.query(f"pid_0 == @lvl") 125 | prev_probs = 0 if mode == '+' else 1 126 | else: 127 | prev_probs = np.array( 128 | [f_score[-1][i] for i in df_layer.index.get_level_values(1)]) 129 | 130 | # timer.start() 131 | df_layer = get_trans_prob_bet_layers(df_layer, net, dir_trans) 132 | # ti mes.append(timer.stop()) 133 | df_layer.loc[:, 'prob'] = cal_prob_func(prev_probs, df_layer.trans_prob * df_layer.observ_prob, mode) 134 | _df = prune_layer(df_layer, idx, f_score[-1], prune_start_layer, prune_factor) 135 | 136 | # post-process 137 | for name, item in _df.iterrows(): 138 | prev_path[idx + 1][name] = (idx, int(item.eid_0)) 139 | prev_states = list(_df.index.unique()) 140 | f_score.append(_df['prob'].to_dict()) 141 | gt_beam.append(df_layer) 142 | 143 | # epath 144 | epath, end_prob = reconstruct_path(f_score, prev_path) 145 | epath = ((layer_ids[idx], eid) for idx, eid in epath) 146 | rList = cands.set_index(['pid', 'eid'])\ 147 | .loc[epath, ['src', 'dst']].reset_index() 148 | 149 | gt_beam = pd.concat(gt_beam) 150 | ratio = gt_beam.shape[0] / gt.shape[0] 151 | _log = f"Route planning time cost: {np.sum(times):.3f} s, trim ratio: {(1 - ratio) * 100:.1f} %" 152 | getattr(logger, level)(_log) 153 | 154 | return end_prob, rList, gt_beam 155 | 156 | 157 | """ normal """ 158 | def viterbi_decode(nodes, trans): 159 | """ 160 | Viterbi算法求最优路径 161 | 其中 nodes.shape=[seq_len, num_labels], 162 | trans.shape=[num_labels, num_labels]. 163 | """ 164 | # 获得输入状态序列的长度,以及观察标签的个数 165 | seq_len, num_labels = len(nodes), len(trans) 166 | # 简单起见,先不考虑发射概率,直接用起始0时刻的分数 167 | scores = nodes[0].reshape((-1, 1)) # (num_labels, 1) 168 | 169 | paths = [] 170 | # 递推求解上一时刻t-1到当前时刻t的最优 171 | for t in range(1, seq_len): 172 | # scores 表示起始0到t-1时刻的每个标签的最优分数 173 | scores_repeat = np.repeat(scores, num_labels, axis=1) # (num_labels, num_labels) 174 | 175 | # observe当前时刻t的每个标签的观测分数 176 | observe = nodes[t].reshape((1, -1)) # (1, num_labels) 177 | observe_repeat = np.repeat(observe, num_labels, axis=0) # (num_labels, num_labels) 178 | 179 | # 从t-1时刻到t时刻最优分数的计算,这里需要考虑转移分数trans 180 | M = scores_repeat + trans + observe_repeat 181 | 182 | # 寻找到t时刻的最优路径 183 | scores = np.max(M, axis=0).reshape((-1, 1)) 184 | idxs = np.argmax(M, axis=0) 185 | 186 | # 路径保存 187 | paths.append(idxs.tolist()) 188 | 189 | best_path = [0] * seq_len 190 | best_path[-1] = np.argmax(scores) 191 | 192 | # 最优路径回溯 193 | for i in range(seq_len-2, -1, -1): 194 | idx = best_path[i+1] 195 | best_path[i] = paths[i][idx] 196 | 197 | def get_trans_prob(trans_prob, layer_id): 198 | return trans_prob[layer_id] 199 | 200 | def decode(observations, states, start_prob, trans_prob, emit_prob, mode='+'): 201 | def _formula(x, y): 202 | if mode == '+': 203 | return x + y 204 | elif mode == '*': 205 | return x * y 206 | 207 | V = [{}] 208 | path = {} 209 | 210 | # Initialize 211 | for st in states: 212 | if st not in start_prob: 213 | continue 214 | V[0][st] = start_prob[st] 215 | path[st] = [(observations[0], st)] 216 | 217 | # Run Viterbi when t > 0 218 | for t in range(1, len(observations)): 219 | V.append({}) 220 | newpath = {} 221 | 222 | for curr_st in states: 223 | paths_to_curr_st = [] 224 | for prev_st in V[t-1]: 225 | _trans_prob = get_trans_prob(trans_prob, t-1) 226 | if (prev_st, curr_st) not in _trans_prob: 227 | continue 228 | 229 | v = V[t-1][prev_st] 230 | _v = _trans_prob[(prev_st, curr_st)] 231 | _e = emit_prob[curr_st][observations[t]] 232 | paths_to_curr_st.append(( _formula(v, _v * _e), prev_st)) 233 | 234 | if not paths_to_curr_st: 235 | continue 236 | 237 | cur_prob, prev_state = max(paths_to_curr_st) 238 | V[t][curr_st] = cur_prob 239 | newpath[curr_st] = path[prev_state] + [(observations[t], curr_st)] 240 | 241 | # No need to keep the old paths 242 | path = newpath 243 | 244 | prob, end_state = max([(V[-1][st], st) for st in states if st in V[-1]]) 245 | 246 | return prob, path[end_state] 247 | 248 | def prepare_viterbi_input(cands, gt): 249 | states = cands.eid.unique() 250 | observations = cands.pid.unique() 251 | start_prob = cands.query("pid == 0").set_index('eid')['observ_prob'].to_dict() 252 | # start_prob = {key:1 for key in start_prob} 253 | 254 | observ_dict = cands[['pid', 'eid', 'observ_prob']].set_index(['eid']) 255 | emit_prob = {i: observ_dict.loc[[i]].set_index('pid')['observ_prob'].to_dict() for i in states} 256 | 257 | # BUG cands 坐标不连续的问题, 莫非是中断 258 | trans_prob = [gt.loc[i]['f'].to_dict() for i in observations[:-1] ] 259 | 260 | return states, observations, start_prob, trans_prob, emit_prob 261 | 262 | def process_viterbi_pipeline(cands, gt): 263 | states, observations, start_prob, trans_prob, emit_prob = prepare_viterbi_input(cands, gt) 264 | prob, rList = decode(observations, states, start_prob, trans_prob, emit_prob) 265 | 266 | rList = cands.set_index(['pid', 'eid']).loc[rList][[ 'src', 'dst']].reset_index() 267 | 268 | return prob, rList 269 | 270 | 271 | if __name__ == "__main__": 272 | import sys 273 | sys.path.append('../') 274 | from utils.serialization import load_checkpoint 275 | 276 | fn = "../debug/traj_0_data_for_viterbi.pkl" 277 | fn = "../../debug/traj_1_data_for_viterbi.pkl" 278 | # fn = Path(__file__).parent / fn 279 | data = load_checkpoint(fn) 280 | 281 | cands = data['cands'] 282 | gt = data['graph'] 283 | rList = data['rList'] 284 | 285 | res = process_viterbi_pipeline(cands, gt) 286 | -------------------------------------------------------------------------------- /mapmatching/osmnet/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mapmatching/osmnet/build_graph.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ..graph import GeoDigraph 3 | from ..osmnet.downloader import download_osm_xml 4 | from ..osmnet.parse_osm_xml import parse_xml_to_graph 5 | from ..setting import DATA_FOLDER 6 | 7 | 8 | def load_geograph(ckpt, ll): 9 | graph = GeoDigraph() 10 | graph.load_checkpoint(ckpt) 11 | graph.init_searcher() 12 | 13 | if not ll: 14 | graph.to_proj() 15 | 16 | return graph 17 | 18 | 19 | def build_geograph(xml_fn:str=None, bbox:list=None, ckpt:str=None, 20 | ll=False, *args, **kwargs): 21 | """Build geograph by one of the three type: 1) xml_fn, 2) bbox, 3) ckpt. 22 | The prior is: ckpt > xml_fn > bbox 23 | 24 | Args: 25 | xml_fn (str, optional): Local OSM network file path. When `xml_fn` is not exist 26 | and the `bbox` is config, the OSM file would be downloaded and save at that location. Defaults to None. 27 | bbox (list, optional): Download the OSM netkork by the Bounding box. Defaults to None. 28 | ckpt (str, optional): Checkpoint. Defaults to None. 29 | ll (bool, optional): Use lon/lat coordination system. Defaults to False. 30 | 31 | Returns: 32 | GeoDigraph: graph 33 | """ 34 | assert xml_fn is not None or bbox is not None or ckpt is not None 35 | if ckpt: 36 | return load_geograph(ckpt, ll) 37 | 38 | if not os.path.exists(xml_fn): 39 | assert bbox is not None, \ 40 | "The local osm file is not exists, please config bbox to dowload" 41 | download_osm_xml(xml_fn, bbox, False) 42 | 43 | df_nodes, df_edges, df_ways = parse_xml_to_graph(xml_fn, *args, **kwargs) 44 | 45 | graph = GeoDigraph(df_edges, df_nodes, ll=ll) 46 | if not ll: 47 | graph.to_proj() 48 | 49 | return graph 50 | 51 | if __name__ == "__main__": 52 | # new graph 53 | name = 'GBA' 54 | graph = build_geograph(xml_fn = f"../../data/network/{name}.osm.xml") 55 | graph.save_checkpoint(f'../../data/network/{name}_graph_pygeos.ckpt') 56 | 57 | # load ckpt 58 | graph = build_geograph(ckpt=f'../../data/network/{name}_graph_pygeos.ckpt') 59 | 60 | # check 61 | path = graph.search(src=7959990710, dst=499265789) 62 | graph.get_edge(path['epath']).plot() 63 | 64 | # save to DB 65 | # graph.to_postgis(name) 66 | 67 | import networkx -------------------------------------------------------------------------------- /mapmatching/osmnet/combine_edges.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | 4 | from ..utils.interval_helper import merge_intervals 5 | from ..utils.parallel_helper import parallel_process 6 | 7 | 8 | def calculate_degree(df_edges): 9 | indegree = df_edges.groupby('dst').agg({'order': 'count'}).rename(columns={'order': 'indegree'}) 10 | outdegree = df_edges.groupby('src').agg({'order': 'count'}).rename(columns={'order': 'outdegree'}) 11 | 12 | return pd.concat([indegree, outdegree], axis=1).fillna(0).astype(int) 13 | 14 | def get_aux_points(df_edges, exclude_list=None): 15 | degree = calculate_degree(df_edges) 16 | 17 | aux_node_lst = degree.query( "indegree == 1 and outdegree == 1" ).index.unique() 18 | if exclude_list is not None: 19 | aux_node_lst = [id for id in aux_node_lst if id not in exclude_list] 20 | 21 | return aux_node_lst 22 | 23 | def combine_links(edges, combine_intervals): 24 | """Combine OSM links with `rid`. 25 | 26 | Args: 27 | rid (int): The id of link in OSM. 28 | nodes (gdf.GeoDataFrame): The all nodes related to `rid` road. 29 | links (gdf.GeoDataFrame): The all links related to `rid` road. 30 | omit_points (list): The points don't meet: 1) only has 1 indegree and 1 outdegree; 2) not the traffic_signals point. 31 | 32 | Returns: 33 | gpd.GeoDataFrame: The links after combination. 34 | """ 35 | if len(combine_intervals) == 0: 36 | return edges 37 | 38 | if 'order' in edges.columns: 39 | edges.set_index('order', inplace=True) 40 | 41 | drop_index = [] 42 | keep_iddex = [] 43 | # FIXME 区间 44 | for start, end, _ in combine_intervals: 45 | segs = edges.query(f"{start} <= order <= {end}") 46 | _dst = segs.iloc[-1]['dst'] 47 | nids = np.append(segs.src.values, _dst).tolist() 48 | 49 | edges.loc[start, 'dst'] = _dst 50 | edges.loc[start, 'dist'] = segs.dist.sum() 51 | edges.loc[start, "waypoints"] = str(nids) 52 | 53 | drop_index += [i for i in range(start+1, end+1)] 54 | 55 | edges.drop(index=drop_index, inplace=True) 56 | edges.reset_index(inplace=True) 57 | edges.loc[:, "waypoints"] = edges.loc[:, "waypoints"].apply(lambda x: eval(x) if isinstance(x, str) else x) 58 | 59 | return edges 60 | 61 | def pipeline_combine_links(df_edges:pd.DataFrame, exclude_list, n_jobs=8): 62 | # BUG multi_edges 63 | aux_nids = get_aux_points(df_edges, exclude_list=exclude_list) 64 | 65 | cands_edges = df_edges.query("src in @aux_nids").sort_values(by=['way_id', 'order']) 66 | cands_way_ids = cands_edges.way_id.unique().tolist() 67 | aux_edge_intervals = cands_edges.groupby('way_id')\ 68 | .order.apply(list)\ 69 | .apply(lambda lst: merge_intervals([[i-1, i] for i in lst if i > 0])) 70 | 71 | # parallel process, 不能使用 cands_edges, 因为涉及到上下游的合并 72 | _df_edges = df_edges.query(f"way_id in @cands_way_ids") 73 | params = ((df, aux_edge_intervals[i]) 74 | for i, df in _df_edges.groupby('way_id')) 75 | combined_edges = parallel_process(combine_links, params, pbar_switch=True, 76 | n_jobs=n_jobs, total=len(cands_way_ids), desc='Combine edges') 77 | 78 | # keep edges 79 | keep_edges = df_edges.query(f"way_id not in @cands_way_ids") 80 | 81 | # combine 82 | df_edges_ = pd.concat(combined_edges + [keep_edges]).sort_values(['way_id', 'order']).reset_index(drop=True) 83 | 84 | return df_edges_ 85 | -------------------------------------------------------------------------------- /mapmatching/osmnet/downloader.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from pathlib import Path 3 | 4 | 5 | def download_osm_xml(fn, bbox, verbose=False): 6 | """Download OSM map of bbox from Internet. 7 | 8 | Args: 9 | fn (function): [description] 10 | bbox ([type]): [description] 11 | verbose (bool, optional): [description]. Defaults to False. 12 | """ 13 | if type(fn) == str: 14 | fn = Path(fn) 15 | 16 | if fn.exists(): 17 | return True 18 | 19 | fn.parent.mkdir(parents=True, exist_ok=True) 20 | 21 | if verbose: 22 | print("Downloading {}".format(fn)) 23 | 24 | if isinstance(bbox, list) or isinstance(bbox, np.array): 25 | bbox = ",".join(map(str, bbox)) 26 | 27 | try: 28 | import requests 29 | # https://dev.overpass-api.de/overpass-doc/en/index.html 30 | # 通过参数控制的 API 可参考 https://github.com/categulario/map_matching/blob/master/mapmatching/overpass/streets.overpassql 31 | url = f'http://overpass-api.de/api/map?bbox={bbox}' 32 | 33 | print(f"url: {url}") 34 | r = requests.get(url, stream=True) 35 | with open(fn, 'wb') as ofile: 36 | for chunk in r.iter_content(chunk_size=1024): 37 | if chunk: 38 | ofile.write(chunk) 39 | 40 | if verbose: 41 | print("Downloaded success.\n") 42 | 43 | return True 44 | except: 45 | return False 46 | 47 | 48 | if __name__ == "__main__": 49 | import sys 50 | sys.path.append('..') 51 | from setting import GBA_BBOX, SZ_BBOX 52 | 53 | download_osm_xml('/home/pcl/minio/geo_data/Shenzhen.osm.xml', SZ_BBOX) 54 | download_osm_xml('../../cache/Futian.osm.xml', [114.03814, 22.51675, 114.06963, 22.56533]) 55 | download_osm_xml('../../cache/GBA.osm.xml', GBA_BBOX) 56 | 57 | proj_name = "GaoxinParkMiddle" 58 | GaoxinParkMiddle_BBOX = [113.92517, 22.54057, 113.95619, 22.55917] 59 | download_osm_xml(f'../../cache/{proj_name}.osm.xml', GaoxinParkMiddle_BBOX) 60 | 61 | -------------------------------------------------------------------------------- /mapmatching/osmnet/misc.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import geopandas as gpd 3 | from shapely.geometry import LineString 4 | from haversine import haversine, Unit 5 | 6 | 7 | class Bunch(dict): 8 | """A dict with attribute-access""" 9 | 10 | def __getattr__(self, key): 11 | try: 12 | return self.__getitem__(key) 13 | except KeyError: 14 | raise AttributeError(key) 15 | 16 | def __dir__(self): 17 | return self.keys() 18 | 19 | 20 | 21 | def cal_od_straight_distance(df_edges, df_nodes, od=['src', 'dst']): 22 | dist = df_edges.merge(df_nodes[['x', 'y']], left_on=od[0], right_index=True, suffixes=('_0', '_1'))\ 23 | .merge(df_nodes[['x', 'y']], left_on=od[1], right_index=True, suffixes=('_0', '_1'))\ 24 | .apply(lambda x: haversine((x.y_0, x.x_0), (x.y_1, x.x_1), unit=Unit.METERS), axis=1) 25 | 26 | return dist 27 | 28 | def points_2_polyline(df_nodes:gpd.GeoDataFrame, points:list): 29 | coords = [] 30 | for p in points: 31 | item = df_nodes.loc[p] 32 | coords.append(item.geometry.coords[0]) 33 | 34 | return LineString(coords) 35 | 36 | 37 | def get_geom_length(geoms, from_crs='epsg:4326', to_crs='epsg:900913'): 38 | assert isinstance(geoms, (pd.Series, gpd.GeoSeries)) 39 | 40 | if geoms.name != 'geometry': 41 | geoms.name = 'geometry' 42 | lines = gpd.GeoDataFrame(geoms, crs=from_crs) 43 | 44 | return lines.to_crs(to_crs).length 45 | -------------------------------------------------------------------------------- /mapmatching/osmnet/osm_io.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | 3 | 4 | def load_graph(fn): 5 | with open(fn, 'rb') as f: 6 | graph = pickle.load(f) 7 | 8 | return graph['df_nodes'], graph['df_edges'], graph['df_ways'] 9 | 10 | 11 | def save_graph(df_nodes, df_edges, df_ways, fn): 12 | graph = { 13 | 'df_nodes': df_nodes, 14 | 'df_edges': df_edges, 15 | 'df_ways': df_ways 16 | } 17 | 18 | with open(fn, 'wb') as f: 19 | pickle.dump(graph, f) 20 | 21 | return True 22 | -------------------------------------------------------------------------------- /mapmatching/osmnet/twoway_edge.py: -------------------------------------------------------------------------------- 1 | import shapely 2 | import numpy as np 3 | import pandas as pd 4 | from loguru import logger 5 | from shapely.geometry import LineString 6 | 7 | 8 | def swap_od(df_edge_rev:pd.DataFrame, od_attrs=['src', 'dst']): 9 | if df_edge_rev.empty: 10 | return df_edge_rev 11 | 12 | df_edge_rev.loc[:, 'dir'] = -1 13 | df_edge_rev.loc[:, 'order'] = -df_edge_rev.order - 1 14 | df_edge_rev.loc[:, 'waypoints'] = df_edge_rev.waypoints.apply(lambda x: x[::-1]) 15 | df_edge_rev.rename(columns={od_attrs[0]: od_attrs[1], od_attrs[1]: od_attrs[0]}, inplace=True) 16 | if 'geometry' in list(df_edge_rev): 17 | df_edge_rev.loc[:, 'geometry'] = df_edge_rev.geometry.apply(lambda x: LineString(x.coords[::-1]) ) 18 | 19 | return df_edge_rev 20 | 21 | 22 | def add_reverse_edge(df_edges, df_ways, od_attrs=['src', 'dst'], offset=True): 23 | """Add reverse edge. 24 | 25 | Args: 26 | df_edges (gpd.GeoDataFrame): The edge file parsed from OSM. 27 | Check: 28 | rid = 34900355 29 | net.df_edges.query( f"rid == {rid} or rid == -{rid}" ).sort_values(['order','rid']) 30 | """ 31 | assert 'oneway' in df_ways.columns, "Check `oneway` tag" 32 | df_edges.loc[:, 'dir'] = 1 33 | 34 | idxs = df_ways.query('oneway == False').index 35 | df_edge_rev = df_edges.query("way_id in @idxs") 36 | 37 | has_geom = 'geometry' in list(df_edges) 38 | if has_geom: 39 | ring_mask = df_edge_rev.geometry.apply(lambda x: x.is_ring) 40 | df_edge_rev = df_edge_rev[~ring_mask] 41 | 42 | df_edge_rev = swap_od(df_edge_rev, od_attrs) 43 | 44 | df_edges = pd.concat([df_edges, df_edge_rev]).reset_index(drop=True) 45 | 46 | if offset: 47 | df_edges = edge_offset(df_edges) 48 | 49 | return df_edges 50 | 51 | 52 | def edge_offset(df_edges): 53 | way_ids = df_edges.query("dir == -1").way_id.unique() 54 | _df_edges = df_edges.query("way_id in @way_ids") 55 | 56 | _df_edges.loc[:, 'geom_origin'] = _df_edges.geometry.copy() 57 | # df_edge.loc[:, 'geom_origin'] = df_edge.geometry.apply(lambda x: x.to_wkt()) 58 | geom_offset = _df_edges.apply( lambda x: parallel_offset_edge(x), axis=1 ) 59 | _df_edges.loc[geom_offset.index, 'geometry'] = geom_offset 60 | 61 | df_edges = pd.concat([df_edges.query("way_id not in @way_ids"), _df_edges]) 62 | 63 | 64 | return df_edges 65 | 66 | 67 | def parallel_offset_edge(record:pd.Series, distance=1.25/110/1000, process_two_point=True, keep_endpoint_pos=True, logger=None): 68 | """Returns a LineString or MultiLineString geometry at a distance from the object on its right or its left side 69 | 70 | Args: 71 | record (LineString): The record object should have the `geometry` attitude. 72 | distance (float, optional): [description]. Defaults to 2/110/1000. 73 | keep_endpoint_pos (bool, optional): keep hte endpoints position or not. Defaults to False. 74 | 75 | Returns: 76 | [LineString]: The offset LineString. 77 | """ 78 | if 'geometry' not in record: 79 | return None 80 | geom = record.geometry 81 | 82 | if len(geom.coords) <= 1: 83 | if logger is not None: 84 | logger.warning(f"{geom}: the length of it is less than 1.") 85 | return geom 86 | 87 | if geom.is_ring: 88 | return geom 89 | 90 | def _cal_dxdy(p0, p1, scale = 15): 91 | return ((p1[0]-p0[0])/scale, (p1[1]-p0[1])/scale) 92 | 93 | def _point_offset(p, dxdy, add=True): 94 | if add: 95 | return (p[0]+dxdy[0], p[1]+dxdy[1]) 96 | 97 | return (p[0]-dxdy[0], p[1]-dxdy[1]) 98 | 99 | try: 100 | # shapely 2.0 以上,`[::-1]` 需删除 101 | offset_coords = geom.parallel_offset(distance, side='right').coords 102 | if int(shapely.__version__.split('.')[0]) < 2: 103 | offset_coords = offset_coords[::-1] 104 | 105 | ori_s, ori_e = geom.coords[0], geom.coords[-1] 106 | dxdy_s = _cal_dxdy(*geom.coords[:2]) 107 | dxdy_e = _cal_dxdy(*geom.coords[-2:]) 108 | turing_s = _point_offset(offset_coords[0], dxdy_s, add=True ) 109 | turing_e = _point_offset(offset_coords[-1], dxdy_e, add=False ) 110 | 111 | coords = [ori_s] + [turing_s] + offset_coords[1:-1] + [turing_e] + [ori_e] 112 | coords = np.round(coords, 7) 113 | geom_new = LineString(coords) 114 | 115 | if logger is not None: 116 | logger.info(f"{len(geom.coords)},{len(geom_new.coords)}\n{geom}\n{geom_new}") 117 | 118 | return geom_new 119 | except: 120 | if logger is not None: 121 | logger.error(f"{record.name}, geom: {geom}, offset error") 122 | 123 | return geom 124 | -------------------------------------------------------------------------------- /mapmatching/setting.py: -------------------------------------------------------------------------------- 1 | """ Global config """ 2 | from pathlib import Path 3 | 4 | IP = "192.168.135.16" 5 | postgre_url= f"postgresql://postgres:pcl_A5A@{IP}:5432/gis" 6 | 7 | root = Path(__file__).parent 8 | DEBUG_FOLDER = root / "../debug" 9 | LOG_FOLDER = root / "../log" 10 | DATA_FOLDER = root / "../data" 11 | 12 | DIS_FACTOR = 1/110/1000 13 | 14 | GBA_BBOX = [111.35669933, 21.56670092, 115.41989933, 24.39190092] 15 | SZ_BBOX = [113.746280, 22.441466, 114.623972, 22.864722] 16 | PCL_BBOX = [113.930914, 22.570536, 113.945456, 22.585613] 17 | FT_BBOX = [114.05097, 22.53447, 114.05863, 22.54605] 18 | 19 | 20 | """ road_type_filter """ 21 | # Note: we adopt the filter logic from osmnx (https://github.com/gboeing/osmnx) 22 | # exclude links with tag attributes in the filters 23 | filters = {} 24 | 25 | 26 | # 道路含义:'service':通往设施的道路 27 | filters['auto'] = {'area':['yes'], 28 | 'highway':['cycleway','footway','path','pedestrian','steps','track','corridor','elevator','escalator', 29 | 'proposed','construction','bridleway','abandoned','platform','raceway'], 30 | 'motor_vehicle':['no'], 31 | 'motorcar':['no'], 32 | 'access':['private'], 33 | 'service':['parking','parking_aisle','driveway','private','emergency_access'] 34 | } 35 | 36 | filters['bike'] = {'area':['yes'], 37 | 'highway':['footway','steps','corridor','elevator','escalator','motor','proposed','construction','abandoned','platform','raceway'], 38 | 'bicycle':['no'], 39 | 'service':['private'], 40 | 'access':['private'] 41 | } 42 | 43 | filters['walk'] = {'area':['yes'], 44 | 'highway':['cycleway','motor','proposed','construction','abandoned','platform','raceway'], 45 | 'foot':['no'], 46 | 'service':['private'], 47 | 'access':['private'] 48 | } 49 | 50 | highway_filters = filters['auto']['highway'] 51 | 52 | osm_highway_type_dict = {'motorway': ('motorway', False), 53 | 'motorway_link': ('motorway', True), 54 | 'trunk': ('trunk', False), 55 | 'trunk_link': ('trunk', True), 56 | 'primary': ('primary', False), 57 | 'primary_link': ('primary', True), 58 | 'secondary': ('secondary', False), 59 | 'secondary_link': ('secondary', True), 60 | 'tertiary': ('tertiary', False), 61 | 'tertiary_link': ('tertiary', True), 62 | 'residential': ('residential', False), 63 | 'residential_link': ('residential', True), 64 | 'service': ('service', False), 65 | 'services': ('service', False), 66 | 'cycleway': ('cycleway', False), 67 | 'footway': ('footway', False), 68 | 'pedestrian': ('footway', False), 69 | 'steps': ('footway', False), 70 | 'track': ('track', False), 71 | 'unclassified': ('unclassified', False)} 72 | 73 | link_type_level_dict = {'motorway':1, 'trunk':2, 'primary':3, 'secondary':4, 'tertiary':5, 'residential':6, 'service':7, 74 | 'cycleway':8, 'footway':9, 'track':10, 'unclassified':11, 'connector':20, 'railway':30, 'aeroway':31} 75 | 76 | default_lanes_dict = {'motorway': 4, 'trunk': 3, 'primary': 3, 'secondary': 2, 'tertiary': 2, 'residential': 1, 'service': 1, 77 | 'cycleway':1, 'footway':1, 'track':1, 'unclassified': 1, 'connector': 2} 78 | 79 | 80 | link_speed_reduction_rate = .6 81 | 82 | congestion_index = 1 83 | default_speed = 30 / congestion_index / 3.6 84 | default_speed_dict = {'motorway': 120, 'trunk': 100, 'primary': 80, 'secondary': 60, 'tertiary': 40, 'residential': 30, 'service': 30, 85 | 'cycleway':5, 'footway':5, 'track':30, 'unclassified': 30, 'connector':120} 86 | default_speed_dict = {k: v / congestion_index / 3.6 for k, v in default_speed_dict.items()} 87 | -------------------------------------------------------------------------------- /mapmatching/update_network.py: -------------------------------------------------------------------------------- 1 | import geopandas as gpd 2 | from loguru import logger 3 | 4 | def plot_topo_helper(seg, pos, neg, matcher): 5 | fig, ax = matcher.plot_result(seg, pos) 6 | neg_path = matcher.transform_res_2_path(neg) 7 | neg_path.plot(ax=ax, color='b', label = 'revert', linestyle=':') 8 | ax.legend() 9 | 10 | return 11 | 12 | def check_each_step(matcher, traj:gpd.GeoDataFrame, idx:int, factor=1.2, plot=False): 13 | flag = False 14 | seg = traj.iloc[idx: idx + 2] 15 | net = matcher.net 16 | 17 | pos = matcher.matching(seg.reset_index(drop=True)) 18 | neg = matcher.matching(seg[::-1].reset_index(drop=True)) 19 | 20 | if neg['status'] == 1 and pos['status'] == 4 or\ 21 | neg['probs']['prob'] > pos['probs']['prob'] * factor: 22 | eids = neg['epath'] 23 | way_ids = net.get_edge(eids, 'way_id').unique() 24 | 25 | for idx in way_ids: 26 | # TODO 上游确定是单向的,且仅有一个的情况下才增加 27 | status = net.add_reverse_way(way_id=idx) 28 | if status: 29 | if not flag: 30 | flag = True 31 | logger.info(f"add {idx}") 32 | else: 33 | logger.info(f"{idx} exist") 34 | pass 35 | 36 | if plot: 37 | plot_topo_helper(seg, pos, neg, matcher) 38 | 39 | return True 40 | 41 | def check_steps(matcher, res, prob_thred=.75, factor=1.2): 42 | flag = True 43 | traj = res['details']['simplified_traj'] 44 | steps = res['details']['steps'] 45 | 46 | if steps is None: 47 | # FIXME 更精炼 48 | logger.warning("Steps is None") 49 | if res['status'] == 4: 50 | _res = matcher.matching(traj[::-1].reset_index(drop=True)) 51 | if _res['status'] == 1: 52 | eids = _res['epath'] 53 | way_ids = matcher.net.get_edge(eids, 'way_id').unique() 54 | 55 | for idx in way_ids: 56 | # TODO 上游确定是单向的,且仅有一个的情况下才增加 57 | status = matcher.net.add_reverse_way(way_id=idx) 58 | if status: 59 | if not flag: 60 | flag = True 61 | logger.info(f"add {idx}") 62 | else: 63 | logger.info(f"{idx} exist") 64 | 65 | return flag 66 | 67 | cand_steps = steps.query(f'trans_prob < {prob_thred}') 68 | for i, item in cand_steps.iterrows(): 69 | flag |= check_each_step(matcher, traj, i, factor) 70 | 71 | return flag 72 | -------------------------------------------------------------------------------- /mapmatching/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .timer import Timer, timeit 2 | -------------------------------------------------------------------------------- /mapmatching/utils/db.py: -------------------------------------------------------------------------------- 1 | import geopandas as gpd 2 | from sqlalchemy import create_engine 3 | from ..setting import postgre_url 4 | 5 | ENGINE = create_engine(postgre_url) 6 | 7 | def gdf_to_postgis(gdf, name, engine=ENGINE, if_exists='replace', *args, **kwargs): 8 | """Save the GeoDataFrame to the db 9 | 10 | Args: 11 | gdf ([type]): [description] 12 | name ([type]): [description] 13 | engine ([type], optional): [description]. Defaults to ENGINE. 14 | if_exists (str, optional): [description]. Defaults to 'replace'. if_exists{‘fail’, ‘replace’, ‘append’} 15 | 16 | Returns: 17 | [type]: [description] 18 | """ 19 | gdf.to_postgis(name=name, con=engine, if_exists=if_exists) 20 | 21 | 22 | def gdf_to_geojson(gdf, fn): 23 | if not isinstance(gdf, gpd.GeoDataFrame): 24 | print('Check the format of the gdf.') 25 | return False 26 | 27 | if 'geojson' not in fn: 28 | fn = f'{fn}.geojson' 29 | 30 | gdf.to_file(fn, driver="GeoJSON") 31 | 32 | return 33 | 34 | -------------------------------------------------------------------------------- /mapmatching/utils/img.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def merge_np_imgs(arrays, n_row, n_col): 4 | """ 5 | Merge a set of tiles into a single array. 6 | 7 | Parameters 8 | --------- 9 | tiles : list of mercantile.Tile objects 10 | The tiles to merge. 11 | arrays : list of numpy arrays 12 | The corresponding arrays (image pixels) of the tiles. This list 13 | has the same length and order as the `tiles` argument. 14 | 15 | Returns 16 | ------- 17 | img : np.ndarray 18 | Merged arrays. 19 | extent : tuple 20 | Bounding box [west, south, east, north] of the returned image 21 | in long/lat. 22 | """ 23 | # get indices starting at zero 24 | indices = [] 25 | for r in range(n_row): 26 | for c in range(n_col): 27 | indices.append((r, c)) 28 | 29 | # the shape of individual tile images 30 | h, w, d = arrays[0].shape 31 | h = max([i.shape[0] for i in arrays]) 32 | w = max([i.shape[1] for i in arrays]) 33 | 34 | # empty merged tiles array to be filled in 35 | img = np.ones((h * n_row, w * n_col, d), dtype=np.uint8) * 255 36 | 37 | for ind, arr in zip(indices, arrays): 38 | y, x = ind 39 | _h, _w, _ = arr.shape 40 | ori_x = x * w + (w - _w) // 2 41 | ori_y = y * h + (h - _h) // 2 42 | img[ori_y : ori_y + _h, ori_x : ori_x + _w, :] = arr 43 | 44 | return img 45 | 46 | -------------------------------------------------------------------------------- /mapmatching/utils/interval_helper.py: -------------------------------------------------------------------------------- 1 | 2 | def merge_intervals(intervals): 3 | res = [] 4 | for i in intervals: 5 | merge_intervals_helper(res, i[0], i[1]) 6 | 7 | return res 8 | 9 | 10 | def merge_intervals_helper(intervals, start, end, height=None): 11 | """merge intervals 12 | 13 | Args: 14 | intervals ([type]): [description] 15 | start ([type]): [description] 16 | end ([type]): [description] 17 | height ([type], optional): [description]. Defaults to None. 18 | """ 19 | if start is None or height ==0 or start == end: 20 | return 21 | 22 | if not intervals: 23 | intervals.append( [start, end, height] ) 24 | return 25 | 26 | _, prev_end, prev_height = intervals[-1] 27 | if prev_height == height and prev_end == start: 28 | intervals[-1][1] = end 29 | 30 | return 31 | intervals.append([start, end, height]) 32 | 33 | 34 | def insert_intervals(intervals, newInterval): 35 | res = [] 36 | insertPos = 0 37 | newInterval = newInterval.copy() 38 | for interval in intervals: 39 | if interval[1] < newInterval[0]: 40 | res.append(interval) 41 | insertPos += 1 42 | elif interval[0] > newInterval[1]: 43 | res.append(interval) 44 | else: 45 | newInterval[0] = min(interval[0], newInterval[0]) 46 | newInterval[1] = max(interval[1], newInterval[1]) 47 | newInterval[2] = interval[2] 48 | 49 | res.insert(insertPos, newInterval) 50 | 51 | return res 52 | 53 | -------------------------------------------------------------------------------- /mapmatching/utils/log_helper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import logbook 5 | from logbook import Logger, TimedRotatingFileHandler 6 | from logbook.more import ColorizedStderrHandler 7 | 8 | FILE_DIR = os.path.dirname(os.path.abspath(__file__)) 9 | BASE_DIR = os.path.join(FILE_DIR, '../../log/') 10 | logbook.set_datetime_format('local') 11 | 12 | 13 | def log_type(record, handler): 14 | log_info = "[{date}] [{level}] [{filename}] [{func_name}] [{lineno}] {msg}".format( 15 | date=record.time, # 日志时间 16 | level=record.level_name, # 日志等级 17 | filename=os.path.split(record.filename)[-1], # 文件名 18 | func_name=record.func_name, # 函数名 19 | lineno=record.lineno, # 行号 20 | msg=record.message # 日志内容 21 | ) 22 | 23 | return log_info 24 | 25 | 26 | class LogHelper(object): 27 | def __init__(self, log_dir=BASE_DIR, log_name='log.log', backup_count=10, log_type=log_type, stdOutFlag=False): 28 | if not os.path.exists(log_dir): 29 | os.mkdir(log_dir) 30 | 31 | self.log_dir = log_dir 32 | self.backup_count = backup_count 33 | 34 | handler = TimedRotatingFileHandler(filename= os.path.join(self.log_dir, log_name), 35 | date_format='%Y-%m-%d', 36 | backup_count=self.backup_count) 37 | self.handler = handler 38 | if log_type is not None: 39 | handler.formatter = log_type 40 | handler.push_application() 41 | 42 | if not stdOutFlag: 43 | return 44 | 45 | handler_std = ColorizedStderrHandler(bubble=True) 46 | if log_type is not None: 47 | handler_std.formatter = log_type 48 | handler_std.push_application() 49 | 50 | def get_current_handler(self): 51 | return self.handler 52 | 53 | @staticmethod 54 | def make_logger(level, name=str(os.getpid())): 55 | return Logger(name=name, level=level) 56 | 57 | 58 | def log_helper(log_file, content): 59 | log_file.write( f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}, {content}\n" ) 60 | return 61 | 62 | 63 | if __name__ == "__main__": 64 | g_log_helper = LogHelper(log_name='log.log', stdOutFlag=True) 65 | log = g_log_helper.make_logger(level=logbook.INFO) 66 | log.critical("critical") # 严重错误,会导致程序退出 67 | log.error("error") # 可控范围内的错误 68 | log.warning("warning") # 警告信息 69 | log.notice("notice") # 大多情况下希望看到的记录 70 | log.info("info") # 大多情况不希望看到的记录 71 | log.debug("debug") # 调试程序时详细输出的记录 72 | pass 73 | -------------------------------------------------------------------------------- /mapmatching/utils/logger_helper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | def make_logger(folder, level='DEBUG', mode='w', console=False): 5 | from loguru import logger 6 | 7 | if not console: 8 | logger.remove() 9 | 10 | logger.add( 11 | os.path.join(folder, f"pano_base_{time.strftime('%Y-%m-%d', time.localtime())}.log"), 12 | enqueue=True, 13 | backtrace=True, 14 | diagnose=True, 15 | level=level, 16 | mode=mode 17 | ) 18 | 19 | return logger 20 | -------------------------------------------------------------------------------- /mapmatching/utils/misc.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from datetime import date 3 | 4 | def get_date(fmt="%Y-%m-%d"): 5 | return date.today().strftime(fmt) 6 | 7 | 8 | def add_datetime_attr(nodes): 9 | extract_date_from_pid = lambda pid: {'area': pid[: 10], "date": pid[10: 16], "time": pid[16: 22]} 10 | nodes.loc[:, ['area', 'date', 'time']] = nodes.apply(lambda x: extract_date_from_pid(x.pid), axis=1, result_type='expand') 11 | 12 | return nodes 13 | 14 | def SET_PANDAS_LOG_FORMET(): 15 | pd.set_option('display.max_rows', 50) 16 | pd.set_option('display.max_columns', 500) 17 | pd.set_option('display.width', 5000) 18 | 19 | return 20 | 21 | -------------------------------------------------------------------------------- /mapmatching/utils/parallel_helper.py: -------------------------------------------------------------------------------- 1 | from tqdm import tqdm 2 | from multiprocessing import cpu_count, Pool 3 | 4 | 5 | def parallel_process(func, queue, pbar_switch=False, desc='Parallel processing', total=None, n_jobs=-1): 6 | """parallel process helper 7 | 8 | Args: 9 | func (Function): The func need to be parallel accelerated. 10 | queue ([tuple, tuple, ..., tuple]): the columns in df must contains the parmas in the func. 11 | desc (str, optional): [description]. Defaults to 'Parallel processing'. 12 | n_jobs (int, optional): [description]. Defaults to -1. 13 | 14 | Returns: 15 | [type]: [description] 16 | """ 17 | size = total 18 | if hasattr(queue, "__len__"): 19 | size = len(queue) 20 | if size == 0: 21 | return [] 22 | 23 | n_jobs = cpu_count() if n_jobs == -1 or n_jobs > cpu_count() else n_jobs 24 | pool = Pool(n_jobs) 25 | 26 | if pbar_switch: 27 | pbar = tqdm(total=size, desc=desc) 28 | update = lambda *args: pbar.update() 29 | 30 | res = [] 31 | for id, params in enumerate(queue): 32 | tmp = pool.apply_async(func, params, callback=update if pbar_switch else None) 33 | res.append(tmp) 34 | pool.close() 35 | pool.join() 36 | res = [r.get() for r in res] 37 | 38 | return res 39 | 40 | 41 | def _add(x, y): 42 | res = x + y 43 | # print(f"{x} + {y} = {res}") 44 | 45 | return res 46 | 47 | 48 | def parallel_process_for_df(df, pipeline, n_jobs=8): 49 | """_summary_ 50 | 51 | Args: 52 | df (_type_): _description_ 53 | pipeline (_type_): _description_ 54 | n_jobs (int, optional): _description_. Defaults to 8. 55 | 56 | Returns: 57 | _type_: _description_ 58 | """ 59 | # FIXME 多进程中多个参数的情况下,现有代码是串行的, 因此 pipeline 中需要固定其他的参数 60 | import pandas as pd 61 | 62 | _size = df.shape[0] // n_jobs + 1 63 | df.loc[:, 'part'] = df.index // _size 64 | params = zip(df.groupby('part')) 65 | df.drop(columns=['part'], inplace=True) 66 | 67 | res = parallel_process(pipeline, params, n_jobs=n_jobs) 68 | sorted(res, key=lambda x: x[0]) 69 | 70 | return pd.concat([i for _, i in res]) 71 | 72 | 73 | def pipeline_for_df_test(df_tuple, bias=-2, verbose=True): 74 | import time 75 | name, df = df_tuple 76 | if verbose: 77 | print(f"Part {name} start, size: {df.shape[0]}\n") 78 | 79 | time.sleep(10) 80 | res = df.x + df.y + bias 81 | if verbose: 82 | print(f"Part {name} Done\n") 83 | 84 | return name, res 85 | 86 | 87 | if __name__ == "__main__": 88 | res = parallel_process(_add, ((i, i) for i in range(10000)), True) 89 | 90 | # 基于 DataFrame 的多进程版本示例 91 | import pandas as pd 92 | df = pd.DataFrame({'x': range(0, 10000), 'y': range(0, 10000)}) 93 | ans = parallel_process_for_df(df, pipeline_for_df_test, n_jobs=8) 94 | 95 | ans -------------------------------------------------------------------------------- /mapmatching/utils/serialization.py: -------------------------------------------------------------------------------- 1 | import time 2 | import hashlib 3 | import pickle 4 | import os 5 | 6 | 7 | def load_checkpoint(ckpt_file_name, obj=None): 8 | _dict = {} 9 | if obj is not None and hasattr(obj, "__dict__"): 10 | _dict = obj.__dict__ 11 | 12 | with open(ckpt_file_name,'rb') as f: 13 | dict_ = pickle.load(f) 14 | _dict.update(dict_) 15 | 16 | return _dict 17 | 18 | 19 | def save_checkpoint(obj, ckpt_file_name, ignore_att=[]): 20 | def _save(tmp): 21 | with open(ckpt_file_name, 'wb') as f: 22 | pickle.dump({ k: v for k, v in tmp.items() if k not in ignore_att}, f) 23 | 24 | if isinstance(obj, dict): 25 | _save(obj) 26 | return True 27 | 28 | try: 29 | _save(obj.__dict__) 30 | return True 31 | except: 32 | return False 33 | 34 | 35 | 36 | class PickleSaver(): 37 | def __init__(self, folder='../cache'): 38 | self.create_time=time.time() 39 | self.folder = folder 40 | 41 | def md5(self): 42 | m=hashlib.md5() 43 | m.update(str(self.create_time).encode('utf-8')) 44 | 45 | return m.hexdigest() 46 | 47 | def save(self, obj, fn): 48 | if not os.path.exists(self.folder): 49 | os.mkdir(self.folder) 50 | 51 | if '.pkl' not in fn: 52 | fn = f"{fn}.pkl" 53 | 54 | with open(os.path.join(self.folder, fn),'wb') as f: 55 | pickle.dump(obj, f) 56 | 57 | return True 58 | 59 | # @staticmethod 60 | def read(self, fn): 61 | if '/' not in fn: 62 | fn = os.path.join(self.folder, fn) 63 | 64 | if '.pkl' not in fn: 65 | fn = f"{fn}.pkl" 66 | 67 | with open(fn,'rb') as f: 68 | try: 69 | obj = pickle.load(f) 70 | return obj 71 | except Exception as e: 72 | pass 73 | 74 | return None 75 | 76 | 77 | class Saver: 78 | def __init__(self, snapshot_file, desc=None): 79 | self.desc = desc 80 | self.snapshot_file = snapshot_file 81 | self.create_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 82 | 83 | pass 84 | 85 | def _save(self, ignore_att=['logger']): 86 | try: 87 | with open(self.snapshot_file,'wb') as f: 88 | pickle.dump({ k: v for k, v in self.__dict__.items() if k not in ignore_att}, f) 89 | return True 90 | except: 91 | return False 92 | 93 | def _load(self, fn): 94 | with open(fn,'rb') as f: 95 | dict_ = pickle.load(f) 96 | self.__dict__.update(dict_) 97 | 98 | return True 99 | 100 | 101 | if __name__ == "__main__": 102 | # fn = "../../cache/tmp.pkl" 103 | # tmp = Saver(fn) 104 | # print(tmp.create_time) 105 | # tmp.save_() 106 | 107 | # tmp.load_(fn) 108 | 109 | ckpt = '../../cache/Shenzhen.graph.ckpt' 110 | info = load_checkpoint(ckpt) -------------------------------------------------------------------------------- /mapmatching/utils/timer.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | from loguru import logger 4 | 5 | class Timer: 6 | """Record multiple running times.""" 7 | def __init__(self): 8 | self.times = [] 9 | self.start() 10 | 11 | def start(self): 12 | """Start the timer.""" 13 | self.tik = time.time() 14 | 15 | def stop(self): 16 | """Stop the timer and record the time in a list.""" 17 | self.times.append(time.time() - self.tik) 18 | return self.times[-1] 19 | 20 | def avg(self): 21 | """Return the average time.""" 22 | return sum(self.times) / len(self.times) 23 | 24 | def sum(self): 25 | """Return the sum of time.""" 26 | return sum(self.times) 27 | 28 | def cumsum(self): 29 | """Return the accumulated time.""" 30 | return np.array(self.times).cumsum().tolist() 31 | 32 | 33 | def timeit(func): 34 | def inner(*args, **kwargs): 35 | start = time.time() 36 | res = func(*args, **kwargs) 37 | end = time.time() 38 | _log = f"{func.__name__}, cost: {(end - start) * 1000: .2f} ms" 39 | # print(_log) 40 | logger.info(_log) 41 | 42 | return res 43 | 44 | return inner -------------------------------------------------------------------------------- /requirement.txt: -------------------------------------------------------------------------------- 1 | geopandas==0.12.2 2 | pandas==1.5.3 3 | shapely==2.0.1 4 | sqlalchemy==1.4.46 5 | psycopg2==2.9.5 6 | geoalchemy2==0.13.1 7 | matplotlib==3.6.3 8 | loguru==0.6.0 9 | haversine==2.8.0 10 | numba==0.56.4 11 | osmium==3.6.0 12 | tqdm 13 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #%% 2 | from tqdm import tqdm 3 | import networkx as nx 4 | import itertools 5 | import geopandas as gpd 6 | from networkx import shortest_simple_paths 7 | from pathlib import Path 8 | 9 | from stmm import build_geograph, ST_Matching 10 | from tilemap import plot_geodata 11 | 12 | """step 1: 获取/加载路网""" 13 | folder = Path("./data/network") 14 | # 方法1: 15 | # 根据 bbox 从 OSM 下载路网,从头解析获得路网数据 16 | # net = build_geograph(bbox = [113.928518, 22.551085, 114.100451, 22.731744], 17 | # xml_fn = folder / "SZN.osm.xml", ll=False, n_jobs=32) 18 | # 将预处理路网保存为 ckpt 19 | # net.save_checkpoint(folder / 'SZN_graph.ckpt') 20 | 21 | # net = build_geograph(ckpt='../dataset/cache/SZN_graph.ckpt') 22 | net = build_geograph(ckpt = folder / 'SZN_graph.ckpt') 23 | matcher = ST_Matching(net=net, ll=False) 24 | 25 | #%% 26 | plot_geodata(net.df_edges.to_crs(4326)) 27 | net.df_edges.head(5) 28 | 29 | # %% 30 | 31 | def get_k_shortest_paths(G, u, v, k): 32 | paths_gen = shortest_simple_paths(G, u, v, "length") 33 | for path in itertools.islice(paths_gen, 0, k): 34 | yield path 35 | 36 | def plot_top_k_shortest_path(): 37 | geoms = [] 38 | 39 | for path in get_k_shortest_paths(G, 9168697035, 9167366553, 3): 40 | epath = net.transform_vpath_to_epath(path) 41 | path_geom = net.transform_epath_to_linestring(epath) 42 | geoms.append(path_geom) 43 | 44 | geoms = gpd.GeoDataFrame(geometry=geoms, crs=net.df_edges.crs) 45 | 46 | plot_geodata(geoms.to_crs(4326).reset_index(), column='index', legend=True, alpha=.5) 47 | 48 | return 49 | 50 | G = nx.DiGraph() 51 | 52 | # # 最短路测试 53 | # nx.shortest_path(G, 9168697035, 9167366553, weight='dist') 54 | 55 | 56 | #%% 57 | import networkx as nx 58 | import numpy as np 59 | import pandas as pd 60 | import geopandas as gpd 61 | 62 | class GeoGraph(nx.DiGraph): 63 | def __init__(self, incoming_graph_data=None, reindex_node=True, **attr): 64 | super().__init__(incoming_graph_data, **attr) 65 | self.nodeid_long2short = {} 66 | self.nodeid_short2long = {} 67 | self.nxt_nid = 0 68 | self.reindex_node = reindex_node 69 | 70 | def search(self, o, d): 71 | return nx.shortest_path(self, o, d, weight='weight') 72 | 73 | def load_graph(self, edges:gpd.GeoDataFrame, src='src', dst='dst', weight='dist'): 74 | # 新增边 75 | for name, item in tqdm(edges.iterrows()): 76 | o = item[src] 77 | d = item[dst] 78 | 79 | if self.reindex_node: 80 | o = self._get_short_node_id(o) 81 | d = self._get_short_node_id(d) 82 | 83 | _w = item[weight] 84 | self.add_edge(o, d, weight=_w) 85 | 86 | def _get_short_node_id(self, nid): 87 | if not self.reindex_node: 88 | return nid 89 | 90 | if nid in self.nodeid_long2short: 91 | return self.nodeid_long2short[nid] 92 | 93 | self.nodeid_long2short[nid] = self.nxt_nid 94 | self.nodeid_short2long[self.nxt_nid] = nid 95 | tmp = self.nxt_nid 96 | self.nxt_nid += 1 97 | 98 | return tmp 99 | 100 | """ coordination """ 101 | def align_crs(self, gdf): 102 | return 103 | 104 | """ vis """ 105 | def add_edge_map(self, ax, *arg, **kwargs): 106 | return 107 | 108 | """ property""" 109 | @property 110 | def crs(self): 111 | return self.df_edges.crs 112 | 113 | @property 114 | def epsg(self): 115 | return self.df_edges.crs.to_epsg() 116 | 117 | 118 | digraph = GeoGraph(reindex_node=False) 119 | digraph.load_graph(net.df_edges) 120 | o, d = 9168697035, 9167366553 121 | 122 | o = digraph._get_short_node_id(o) 123 | d = digraph._get_short_node_id(d) 124 | digraph.search(o, d) 125 | 126 | 127 | # %% 128 | --------------------------------------------------------------------------------