Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SCM syntax highlighting & preventing 3-way merges
pixi.lock merge=binary linguist-language=YAML linguist-generated=true
2 changes: 1 addition & 1 deletion .github/workflows/lint-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.9"
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,6 @@ cython_debug/

# HTMLs
*.html
# pixi environments
.pixi/*
!.pixi/config.toml
19 changes: 0 additions & 19 deletions environment.yml

This file was deleted.

36 changes: 0 additions & 36 deletions environment_dev.yml

This file was deleted.

41 changes: 17 additions & 24 deletions mappymatch/maps/igraph/igraph_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@

import igraph as ig
import networkx as nx
import numpy as np
import rtree as rt
from shapely.geometry import Point
from shapely.strtree import STRtree

from mappymatch.constructs.coordinate import Coordinate
from mappymatch.constructs.geofence import Geofence
Expand Down Expand Up @@ -154,15 +153,19 @@ def _build_road(
return road

def _build_rtree(self):
idx = rt.index.Index()
geometries = []
edge_indices = []

for e in self.g.es:
geom = e.attributes()[self._geom_key]
box = geom.bounds
geometries.append(geom)
edge_indices.append(e.index)

idx.insert(e.index, box)
if len(geometries) == 0:
raise ValueError("No geometries found in graph; cannot build spatial index")

self.rtree = idx
self.strtree = STRtree(geometries)
self.edge_indices = edge_indices

def __str__(self):
output_lines = [
Expand Down Expand Up @@ -299,13 +302,12 @@ def to_file(self, outfile: Union[str, Path]):

self.g.write_pickle(str(outfile))

def _nearest_edge_index(self, coord: Coordinate, buffer: float = 10.0) -> int:
def _nearest_edge_index(self, coord: Coordinate) -> int:
"""
An internal method to find the nearest edge to a coordinate

Args:
coord: The coordinate to find the nearest road to
buffer: The buffer to search around the coordinate

Returns:
The nearest edge index to the coordinate
Expand All @@ -314,35 +316,26 @@ def _nearest_edge_index(self, coord: Coordinate, buffer: float = 10.0) -> int:
raise ValueError(
f"crs of origin {coord.crs} must match crs of map {self.crs}"
)
nearest_candidates = list(
self.rtree.nearest(coord.geom.buffer(buffer).bounds, 1),
)

if len(nearest_candidates) == 0:
nearest_idx = self.strtree.nearest(coord.geom)
if nearest_idx is None:
raise ValueError(f"No roads found for {coord}")
elif len(nearest_candidates) == 1:
nearest_id = nearest_candidates[0]
else:
distances = [
self._build_road(i).geom.distance(coord.geom)
for i in nearest_candidates
]
nearest_id = nearest_candidates[np.argmin(distances)]

return nearest_id
nearest_edge_index = self.edge_indices[nearest_idx]

return nearest_edge_index

def nearest_road(self, coord: Coordinate, buffer: float = 10.0) -> Road:
def nearest_road(self, coord: Coordinate) -> Road:
"""
A helper function to get the nearest road.

Args:
coord: The coordinate to find the nearest road to
buffer: The buffer to search around the coordinate

Returns:
The nearest road to the coordinate
"""
nearest_edge_index = self._nearest_edge_index(coord, buffer)
nearest_edge_index = self._nearest_edge_index(coord)
return self._build_road(nearest_edge_index)

def shortest_path(
Expand Down
67 changes: 30 additions & 37 deletions mappymatch/maps/nx/nx_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@

import json
from pathlib import Path
import pickle
from typing import Any, Callable, Dict, List, Optional, Set, Union

import networkx as nx
import numpy as np
import rtree as rt
import shapely.wkt as wkt
from shapely.geometry import Point
from shapely.strtree import STRtree

from mappymatch.constructs.coordinate import Coordinate
from mappymatch.constructs.geofence import Geofence
from mappymatch.constructs.road import Road, RoadId
from mappymatch.maps.igraph.igraph_map import DEFAULT_METADATA_KEY
from mappymatch.maps.map_interface import (
DEFAULT_DISTANCE_WEIGHT,
DEFAULT_TIME_WEIGHT,
Expand All @@ -23,10 +24,7 @@
nx_graph_from_osmnx,
)
from mappymatch.utils.crs import CRS, LATLON_CRS

DEFAULT_GEOMETRY_KEY = "geometry"
DEFAULT_METADATA_KEY = "metadata"
DEFAULT_CRS_KEY = "crs"
from mappymatch.utils.keys import DEFAULT_CRS_KEY, DEFAULT_GEOMETRY_KEY


class NxMap(MapInterface):
Expand Down Expand Up @@ -110,16 +108,20 @@ def _build_road(
return road

def _build_rtree(self):
idx = rt.index.Index()
for i, gtuple in enumerate(self.g.edges(data=True, keys=True)):
u, v, k, d = gtuple
geoms = []
road_ids = []

for u, v, k, d in self.g.edges(data=True, keys=True):
road_id = RoadId(u, v, k)
geom = d[self._geom_key]
box = geom.bounds
geoms.append(geom)
road_ids.append(road_id)

idx.insert(i, box, obj=road_id)
if len(geoms) == 0:
raise ValueError("No geometries found in graph; cannot build spatial index")

self.rtree = idx
self.rtree = STRtree(geoms)
self._road_id_mapping = road_ids

def __str__(self):
output_lines = [
Expand Down Expand Up @@ -191,14 +193,13 @@ def from_file(cls, file: Union[str, Path]) -> NxMap:
"""
p = Path(file)
if p.suffix == ".pickle":
raise ValueError(
"NxMap does not support reading from pickle files, please use .json instead"
)
with open(p, "rb") as f:
return pickle.load(f)
elif p.suffix == ".json":
with p.open("r") as f:
return NxMap.from_dict(json.load(f))
else:
raise TypeError("NxMap only supports reading from json files")
raise TypeError("NxMap only supports reading from json and pickle files")

@classmethod
def from_geofence(
Expand All @@ -207,6 +208,7 @@ def from_geofence(
xy: bool = True,
network_type: NetworkType = NetworkType.DRIVE,
custom_filter: Optional[str] = None,
additional_metadata_keys: Optional[set | list] = None,
) -> NxMap:
"""
Read an OSM network graph into a NxMap
Expand All @@ -216,6 +218,7 @@ def from_geofence(
xy: whether to use xy coordinates or lat/lon
network_type: the network type to use for the graph
custom_filter: a custom filter to pass to osmnx like '["highway"~"motorway|primary"]'
additional_metadata_keys: additional keys to preserve in road metadata like '["maxspeed", "highway"]

Returns:
a NxMap
Expand All @@ -225,11 +228,15 @@ def from_geofence(
f"the geofence must in the epsg:4326 crs but got {geofence.crs.to_authority()}"
)

if additional_metadata_keys is not None:
additional_metadata_keys = set(additional_metadata_keys)

nx_graph = nx_graph_from_osmnx(
geofence=geofence,
network_type=network_type,
xy=xy,
custom_filter=custom_filter,
additional_metadata_keys=additional_metadata_keys,
)

return NxMap(nx_graph)
Expand All @@ -244,15 +251,14 @@ def to_file(self, outfile: Union[str, Path]):
outfile = Path(outfile)

if outfile.suffix == ".pickle":
raise ValueError(
"NxMap does not support writing to pickle files, please use .json instead"
)
with open(outfile, "wb") as f:
pickle.dump(self, f)
elif outfile.suffix == ".json":
graph_dict = self.to_dict()
with open(outfile, "w") as f:
json.dump(graph_dict, f)
else:
raise TypeError("NxMap only supports writing to json files")
raise TypeError("NxMap only supports writing to json and pickle files")

@classmethod
def from_dict(cls, d: Dict[str, Any]) -> NxMap:
Expand Down Expand Up @@ -290,13 +296,12 @@ def to_dict(self) -> Dict[str, Any]:

return graph_dict

def nearest_road(self, coord: Coordinate, buffer: float = 10.0) -> Road:
def nearest_road(self, coord: Coordinate) -> Road:
"""
A helper function to get the nearest road.

Args:
coord: The coordinate to find the nearest road to
buffer: The buffer to search around the coordinate

Returns:
The nearest road to the coordinate
Expand All @@ -305,23 +310,11 @@ def nearest_road(self, coord: Coordinate, buffer: float = 10.0) -> Road:
raise ValueError(
f"crs of origin {coord.crs} must match crs of map {self.crs}"
)
nearest_candidates = list(
map(
lambda c: c.object,
(self.rtree.nearest(coord.geom.buffer(buffer).bounds, 1, objects=True)),
)
)

if len(nearest_candidates) == 0:
nearest_idx = self.rtree.nearest(coord.geom)
if nearest_idx is None:
raise ValueError(f"No roads found for {coord}")
elif len(nearest_candidates) == 1:
nearest_id = nearest_candidates[0]
else:
distances = [
self._build_road(i).geom.distance(coord.geom)
for i in nearest_candidates
]
nearest_id = nearest_candidates[np.argmin(distances)]
nearest_id = self._road_id_mapping[nearest_idx]

road = self._build_road(nearest_id)

Expand Down
Loading