Next alpha stage commit
This commit is contained in:
309
app/routing.py
309
app/routing.py
@@ -20,12 +20,18 @@ from app.serializers import feature_collection
|
||||
|
||||
|
||||
WALK_HEURISTIC_MPS = 1.6
|
||||
BIKE_HEURISTIC_MPS = 6.0
|
||||
DRIVE_HEURISTIC_MPS = 36.0
|
||||
BIKE_SPEED_MPS = 4.8
|
||||
DEFAULT_MAX_VISITED = 160_000
|
||||
PGR_WALK_BBOX_PADDING_KM = [0.5, 1.5, 4, 10, 25]
|
||||
PGR_BIKE_BBOX_PADDING_KM = [1, 3, 8, 20, 60]
|
||||
PGR_DRIVE_BBOX_PADDING_KM = [2, 8, 25, 75, 200]
|
||||
PGR_WALK_STATEMENT_TIMEOUT_MS = 2_500
|
||||
PGR_BIKE_STATEMENT_TIMEOUT_MS = 4_000
|
||||
PGR_DRIVE_STATEMENT_TIMEOUT_MS = 7_500
|
||||
ROUTING_MODES = {"walk", "bike", "drive"}
|
||||
ROUTING_MODE_ALIASES = {"bicycle": "bike", "cycle": "bike", "cycling": "bike", "car": "drive"}
|
||||
ROUTE_CACHE_TTL_SECONDS = 15 * 60
|
||||
ROUTE_CACHE_MAX_ENTRIES = 512
|
||||
_route_cache_lock = threading.RLock()
|
||||
@@ -123,6 +129,43 @@ def _metadata(dataset: Dataset) -> dict[str, object]:
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def _normalize_routing_mode(mode: str) -> str:
|
||||
normalized = ROUTING_MODE_ALIASES.get(str(mode or "").lower(), str(mode or "").lower())
|
||||
if normalized not in ROUTING_MODES:
|
||||
raise ValueError("mode must be walk, bike, or drive")
|
||||
return normalized
|
||||
|
||||
|
||||
def _cost_columns(mode: str) -> tuple[str, str]:
|
||||
if mode == "drive":
|
||||
return "drive_cost_s", "reverse_drive_cost_s"
|
||||
return "walk_cost_s", "reverse_walk_cost_s"
|
||||
|
||||
|
||||
def _heuristic_mps(mode: str) -> float:
|
||||
if mode == "walk":
|
||||
return WALK_HEURISTIC_MPS
|
||||
if mode == "bike":
|
||||
return BIKE_HEURISTIC_MPS
|
||||
return DRIVE_HEURISTIC_MPS
|
||||
|
||||
|
||||
def _pgrouting_padding_km(mode: str) -> list[float]:
|
||||
if mode == "walk":
|
||||
return PGR_WALK_BBOX_PADDING_KM
|
||||
if mode == "bike":
|
||||
return PGR_BIKE_BBOX_PADDING_KM
|
||||
return PGR_DRIVE_BBOX_PADDING_KM
|
||||
|
||||
|
||||
def _pgrouting_timeout_ms(mode: str) -> int:
|
||||
if mode == "walk":
|
||||
return PGR_WALK_STATEMENT_TIMEOUT_MS
|
||||
if mode == "bike":
|
||||
return PGR_BIKE_STATEMENT_TIMEOUT_MS
|
||||
return PGR_DRIVE_STATEMENT_TIMEOUT_MS
|
||||
|
||||
|
||||
def route_between_points(
|
||||
db: Session,
|
||||
*,
|
||||
@@ -133,9 +176,11 @@ def route_between_points(
|
||||
mode: str = "walk",
|
||||
dataset_id: int | None = None,
|
||||
max_visited: int = DEFAULT_MAX_VISITED,
|
||||
allow_python_fallback: bool = True,
|
||||
pgrouting_timeout_ms: int | None = None,
|
||||
pgrouting_padding_km: tuple[float, ...] | list[float] | None = None,
|
||||
) -> dict[str, object]:
|
||||
if mode not in {"walk", "drive"}:
|
||||
raise ValueError("mode must be walk or drive")
|
||||
mode = _normalize_routing_mode(mode)
|
||||
dataset = db.get(Dataset, dataset_id) if dataset_id is not None else active_routing_dataset(db)
|
||||
if dataset is None:
|
||||
raise ValueError("No routing dataset is available.")
|
||||
@@ -152,7 +197,8 @@ def route_between_points(
|
||||
payload = _single_point_route(start, from_lon, from_lat, to_lon, to_lat, mode, dataset_id)
|
||||
_route_cache_put(cache_key, payload)
|
||||
return payload
|
||||
if settings.is_postgresql_database and _pgrouting_installed(db):
|
||||
pgrouting_available = settings.is_postgresql_database and _pgrouting_installed(db)
|
||||
if pgrouting_available:
|
||||
try:
|
||||
payload = _route_with_pgrouting(
|
||||
db,
|
||||
@@ -164,15 +210,23 @@ def route_between_points(
|
||||
from_lat=from_lat,
|
||||
to_lon=to_lon,
|
||||
to_lat=to_lat,
|
||||
timeout_ms=pgrouting_timeout_ms,
|
||||
padding_km=pgrouting_padding_km,
|
||||
)
|
||||
_route_cache_put(cache_key, payload)
|
||||
return payload
|
||||
except ValueError:
|
||||
pass
|
||||
if not allow_python_fallback:
|
||||
raise
|
||||
except SQLAlchemyError:
|
||||
db.rollback()
|
||||
if not allow_python_fallback:
|
||||
raise
|
||||
|
||||
heuristic_mps = WALK_HEURISTIC_MPS if mode == "walk" else DRIVE_HEURISTIC_MPS
|
||||
if not allow_python_fallback:
|
||||
raise ValueError("pgRouting did not find a bounded route and Python fallback is disabled.")
|
||||
|
||||
heuristic_mps = _heuristic_mps(mode)
|
||||
queue: list[tuple[float, float, int]] = []
|
||||
heapq.heappush(queue, (0.0, 0.0, start.osm_node_id))
|
||||
costs: dict[int, float] = {start.osm_node_id: 0.0}
|
||||
@@ -226,8 +280,7 @@ def direct_route_between_points(
|
||||
dataset_id: int | None = None,
|
||||
reason: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
if mode not in {"walk", "drive"}:
|
||||
raise ValueError("mode must be walk or drive")
|
||||
mode = _normalize_routing_mode(mode)
|
||||
dataset = db.get(Dataset, dataset_id) if dataset_id is not None else active_routing_dataset(db)
|
||||
payload = _direct_route_payload(
|
||||
dataset_id=0 if dataset is None else int(dataset.id),
|
||||
@@ -251,8 +304,7 @@ def snap_point_to_routing_graph(
|
||||
dataset_id: int | None = None,
|
||||
max_distance_m: float = 250,
|
||||
) -> dict[str, object] | None:
|
||||
if mode not in {"walk", "drive"}:
|
||||
raise ValueError("mode must be walk or drive")
|
||||
mode = _normalize_routing_mode(mode)
|
||||
dataset = db.get(Dataset, dataset_id) if dataset_id is not None else active_routing_dataset(db)
|
||||
if dataset is None:
|
||||
return None
|
||||
@@ -288,8 +340,8 @@ def _snap_point_to_routing_edge_postgresql(
|
||||
mode: str,
|
||||
max_distance_m: float,
|
||||
) -> dict[str, object] | None:
|
||||
cost_column = "walk_cost_s" if mode == "walk" else "drive_cost_s"
|
||||
reverse_cost_column = "reverse_walk_cost_s" if mode == "walk" else "reverse_drive_cost_s"
|
||||
cost_column, reverse_cost_column = _cost_columns(mode)
|
||||
access_filter = f"AND {_bike_access_sql('edge')}" if mode == "bike" else ""
|
||||
radius_deg = max_distance_m / 111_320
|
||||
row = db.execute(
|
||||
text(
|
||||
@@ -319,6 +371,7 @@ def _snap_point_to_routing_edge_postgresql(
|
||||
CROSS JOIN point
|
||||
WHERE edge.dataset_id = :dataset_id
|
||||
AND (edge.{cost_column} IS NOT NULL OR edge.{reverse_cost_column} IS NOT NULL)
|
||||
{access_filter}
|
||||
AND box(point(edge.max_lon, edge.max_lat), point(edge.min_lon, edge.min_lat))
|
||||
&& box(
|
||||
point(:lon + :radius_deg, :lat + :radius_deg),
|
||||
@@ -337,13 +390,16 @@ def _snap_point_to_routing_edge_postgresql(
|
||||
ST_DistanceSphere(edges.edge_geom, point.geom) AS distance_m,
|
||||
CASE
|
||||
WHEN edges.highway IN ('footway', 'pedestrian', 'steps') THEN 0
|
||||
WHEN edges.highway IN ('path', 'cycleway', 'bridleway') THEN 1
|
||||
WHEN edges.highway = 'cycleway' THEN 0
|
||||
WHEN edges.highway IN ('path', 'bridleway') THEN 1
|
||||
WHEN edges.highway IN ('living_street', 'residential') THEN 2
|
||||
WHEN edges.highway = 'service' THEN 3
|
||||
ELSE 4
|
||||
END AS highway_rank,
|
||||
CASE
|
||||
WHEN :mode != 'walk' THEN 0
|
||||
WHEN :mode NOT IN ('walk', 'bike') THEN 0
|
||||
WHEN :mode = 'bike' AND edges.highway = 'cycleway' THEN -12
|
||||
WHEN :mode = 'bike' AND edges.highway IN ('path', 'living_street', 'residential') THEN -4
|
||||
WHEN edges.highway = 'service' THEN 20
|
||||
WHEN edges.highway IN ('primary', 'primary_link', 'secondary', 'secondary_link') THEN 10
|
||||
WHEN edges.highway IN ('tertiary', 'tertiary_link', 'unclassified', 'road') THEN 5
|
||||
@@ -359,7 +415,9 @@ def _snap_point_to_routing_edge_postgresql(
|
||||
)
|
||||
ORDER BY
|
||||
ST_DistanceSphere(edges.edge_geom, point.geom) + CASE
|
||||
WHEN :mode != 'walk' THEN 0
|
||||
WHEN :mode NOT IN ('walk', 'bike') THEN 0
|
||||
WHEN :mode = 'bike' AND edges.highway = 'cycleway' THEN -12
|
||||
WHEN :mode = 'bike' AND edges.highway IN ('path', 'living_street', 'residential') THEN -4
|
||||
WHEN edges.highway = 'service' THEN 20
|
||||
WHEN edges.highway IN ('primary', 'primary_link', 'secondary', 'secondary_link') THEN 10
|
||||
WHEN edges.highway IN ('tertiary', 'tertiary_link', 'unclassified', 'road') THEN 5
|
||||
@@ -455,15 +513,26 @@ def _route_with_pgrouting(
|
||||
from_lat: float,
|
||||
to_lon: float,
|
||||
to_lat: float,
|
||||
timeout_ms: int | None = None,
|
||||
padding_km: tuple[float, ...] | list[float] | None = None,
|
||||
) -> dict[str, object]:
|
||||
cost_column = "walk_cost_s" if mode == "walk" else "drive_cost_s"
|
||||
reverse_cost_column = "reverse_walk_cost_s" if mode == "walk" else "reverse_drive_cost_s"
|
||||
cost_column, reverse_cost_column = _cost_columns(mode)
|
||||
routing_cost = _routing_cost_expression(cost_column, mode)
|
||||
reverse_routing_cost = _routing_cost_expression(reverse_cost_column, mode)
|
||||
for padding_km in PGR_WALK_BBOX_PADDING_KM if mode == "walk" else PGR_DRIVE_BBOX_PADDING_KM:
|
||||
reverse_routing_cost = _routing_cost_expression(reverse_cost_column, mode, reverse=True)
|
||||
actual_cost_sql = "steps.cost" if mode == "bike" else f"""
|
||||
CASE
|
||||
WHEN steps.from_node = edge.source_osm_node_id THEN edge.{cost_column}
|
||||
ELSE edge.{reverse_cost_column}
|
||||
END
|
||||
"""
|
||||
paddings = list(padding_km or _pgrouting_padding_km(mode))
|
||||
statement_timeout_ms = int(
|
||||
timeout_ms if timeout_ms is not None else _pgrouting_timeout_ms(mode)
|
||||
)
|
||||
for padding_km in paddings:
|
||||
_set_local_statement_timeout(
|
||||
db,
|
||||
PGR_WALK_STATEMENT_TIMEOUT_MS if mode == "walk" else PGR_DRIVE_STATEMENT_TIMEOUT_MS,
|
||||
statement_timeout_ms,
|
||||
)
|
||||
bbox = _expanded_bbox(
|
||||
min(from_lon, to_lon, start.lon, target.lon),
|
||||
@@ -517,10 +586,7 @@ def _route_with_pgrouting(
|
||||
edge.highway,
|
||||
edge.name,
|
||||
edge.geometry_geojson,
|
||||
CASE
|
||||
WHEN steps.from_node = edge.source_osm_node_id THEN edge.{cost_column}
|
||||
ELSE edge.{reverse_cost_column}
|
||||
END AS actual_cost_s
|
||||
{actual_cost_sql} AS actual_cost_s
|
||||
FROM steps
|
||||
JOIN routing_edges AS edge ON edge.id = steps.edge
|
||||
WHERE steps.edge <> -1
|
||||
@@ -616,33 +682,109 @@ def _pgrouting_payload(
|
||||
return payload
|
||||
|
||||
|
||||
def _routing_cost_expression(column: str, mode: str) -> str:
|
||||
if mode != "walk":
|
||||
def _routing_cost_expression(column: str, mode: str, *, reverse: bool = False) -> str:
|
||||
if mode == "drive":
|
||||
return column
|
||||
if mode == "bike":
|
||||
direction_filter = f"WHEN {_bike_direction_forbidden_sql(reverse=reverse)} THEN NULL"
|
||||
return f"""
|
||||
CASE
|
||||
WHEN {column} IS NULL THEN NULL
|
||||
WHEN NOT ({_bike_access_sql()}) THEN NULL
|
||||
{direction_filter}
|
||||
ELSE (length_m / {BIKE_SPEED_MPS:.4f}) * CASE
|
||||
WHEN highway = 'cycleway' THEN 0.82
|
||||
WHEN highway = 'path' THEN 0.95
|
||||
WHEN highway IN ('living_street', 'residential') THEN 1.00
|
||||
WHEN highway = 'service' THEN 1.08
|
||||
WHEN highway = 'track' THEN 1.10
|
||||
WHEN highway IN ('unclassified', 'road', 'tertiary', 'tertiary_link') THEN 1.12
|
||||
WHEN highway IN ('secondary', 'secondary_link') THEN 1.22
|
||||
WHEN highway IN ('primary', 'primary_link') THEN 1.35
|
||||
WHEN highway IN ('footway', 'pedestrian', 'bridleway') THEN 1.40
|
||||
ELSE 1.05
|
||||
END
|
||||
END
|
||||
"""
|
||||
return f"""
|
||||
CASE
|
||||
WHEN {column} IS NULL THEN NULL
|
||||
ELSE {column} * CASE
|
||||
WHEN highway IN ('footway', 'pedestrian') THEN 0.70
|
||||
WHEN highway = 'path' THEN 0.78
|
||||
WHEN highway = 'steps' THEN 0.95
|
||||
WHEN highway = 'cycleway' THEN 1.05
|
||||
WHEN highway = 'bridleway' THEN 1.10
|
||||
WHEN highway IN ('living_street', 'track') THEN 1.15
|
||||
WHEN highway IN ('residential', 'service') THEN 1.35
|
||||
WHEN highway IN ('unclassified', 'road') THEN 1.55
|
||||
WHEN highway IN ('tertiary', 'tertiary_link') THEN 1.80
|
||||
WHEN highway IN ('secondary', 'secondary_link') THEN 2.15
|
||||
WHEN highway IN ('primary', 'primary_link') THEN 2.50
|
||||
ELSE 1.30
|
||||
WHEN highway IN ('footway', 'pedestrian') THEN 0.92
|
||||
WHEN highway = 'path' THEN 0.94
|
||||
WHEN highway = 'steps' THEN 1.05
|
||||
WHEN highway = 'cycleway' THEN 1.02
|
||||
WHEN highway = 'bridleway' THEN 1.05
|
||||
WHEN highway = 'living_street' THEN 1.00
|
||||
WHEN highway IN ('residential', 'service') THEN 1.05
|
||||
WHEN highway = 'track' THEN 1.10
|
||||
WHEN highway IN ('unclassified', 'road') THEN 1.12
|
||||
WHEN highway IN ('tertiary', 'tertiary_link') THEN 1.18
|
||||
WHEN highway IN ('secondary', 'secondary_link') THEN 1.28
|
||||
WHEN highway IN ('primary', 'primary_link') THEN 1.40
|
||||
ELSE 1.08
|
||||
END
|
||||
END
|
||||
"""
|
||||
|
||||
|
||||
def _routing_tags_sql(alias: str | None = None) -> str:
|
||||
prefix = f"{alias}." if alias else ""
|
||||
return f"COALESCE(NULLIF({prefix}tags_json, '')::jsonb, '{{}}'::jsonb)"
|
||||
|
||||
|
||||
def _bike_access_sql(alias: str | None = None) -> str:
|
||||
tags = _routing_tags_sql(alias)
|
||||
highway = f"{alias}.highway" if alias else "highway"
|
||||
return f"""
|
||||
(
|
||||
{highway} NOT IN ('motorway', 'motorway_link', 'steps', 'platform')
|
||||
AND NOT (
|
||||
COALESCE({tags} ->> 'bicycle', '') IN ('no', 'private')
|
||||
OR (
|
||||
COALESCE({tags} ->> 'access', '') IN ('no', 'private')
|
||||
AND COALESCE({tags} ->> 'bicycle', '') NOT IN ('yes', 'designated', 'permissive', 'destination')
|
||||
AND COALESCE({tags} ->> 'vehicle', '') NOT IN ('yes', 'designated', 'permissive', 'destination')
|
||||
)
|
||||
)
|
||||
AND NOT (
|
||||
{highway} IN ('footway', 'pedestrian', 'bridleway')
|
||||
AND COALESCE({tags} ->> 'bicycle', '') NOT IN ('yes', 'designated', 'permissive', 'destination')
|
||||
)
|
||||
AND NOT (
|
||||
{highway} IN ('trunk', 'trunk_link')
|
||||
AND COALESCE({tags} ->> 'bicycle', '') NOT IN ('yes', 'designated', 'permissive', 'destination')
|
||||
)
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def _bike_direction_forbidden_sql(alias: str | None = None, *, reverse: bool) -> str:
|
||||
tags = _routing_tags_sql(alias)
|
||||
highway = f"{alias}.highway" if alias else "highway"
|
||||
bicycle_direction_key = "bicycle:backward" if reverse else "bicycle:forward"
|
||||
one_way_condition = (
|
||||
f"COALESCE({tags} ->> 'oneway', '') IN ('yes', 'true', '1') "
|
||||
f"OR COALESCE({tags} ->> 'junction', '') = 'roundabout' "
|
||||
f"OR {highway} = 'motorway'"
|
||||
if reverse
|
||||
else f"COALESCE({tags} ->> 'oneway', '') = '-1'"
|
||||
)
|
||||
return f"""
|
||||
(
|
||||
(
|
||||
{one_way_condition}
|
||||
)
|
||||
AND COALESCE({tags} ->> 'oneway:bicycle', '') != 'no'
|
||||
AND COALESCE({tags} ->> 'bicycle:oneway', '') != 'no'
|
||||
AND COALESCE({tags} ->> '{bicycle_direction_key}', '') NOT IN ('yes', 'designated', 'permissive')
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def _nearest_node(db: Session, dataset_id: int, lon: float, lat: float, mode: str) -> _GraphNode | None:
|
||||
cost_column = "walk_cost_s" if mode == "walk" else "drive_cost_s"
|
||||
reverse_cost_column = "reverse_walk_cost_s" if mode == "walk" else "reverse_drive_cost_s"
|
||||
cost_column, reverse_cost_column = _cost_columns(mode)
|
||||
access_filter = f"AND {_bike_access_sql('edge')}" if mode == "bike" else ""
|
||||
row = None
|
||||
for candidate_limit in (64, 512, 4096):
|
||||
row = db.execute(
|
||||
@@ -663,6 +805,7 @@ def _nearest_node(db: Session, dataset_id: int, lon: float, lat: float, mode: st
|
||||
SELECT 1
|
||||
FROM routing_edges AS edge
|
||||
WHERE edge.dataset_id = :dataset_id
|
||||
{access_filter}
|
||||
AND (
|
||||
(edge.source_osm_node_id = nearest.osm_node_id AND edge.{cost_column} IS NOT NULL)
|
||||
OR (edge.target_osm_node_id = nearest.osm_node_id AND edge.{reverse_cost_column} IS NOT NULL)
|
||||
@@ -686,8 +829,7 @@ def _nearest_node(db: Session, dataset_id: int, lon: float, lat: float, mode: st
|
||||
|
||||
|
||||
def _outgoing_edges(db: Session, dataset_id: int, node_id: int, mode: str) -> list[_Traversal]:
|
||||
cost_column = "walk_cost_s" if mode == "walk" else "drive_cost_s"
|
||||
reverse_cost_column = "reverse_walk_cost_s" if mode == "walk" else "reverse_drive_cost_s"
|
||||
cost_column, reverse_cost_column = _cost_columns(mode)
|
||||
rows = db.execute(
|
||||
text(
|
||||
f"""
|
||||
@@ -695,6 +837,7 @@ def _outgoing_edges(db: Session, dataset_id: int, node_id: int, mode: str) -> li
|
||||
id, source_osm_node_id, target_osm_node_id,
|
||||
source_lon, source_lat, target_lon, target_lat,
|
||||
length_m, highway, name, geometry_geojson,
|
||||
tags_json,
|
||||
CASE
|
||||
WHEN source_osm_node_id = :node_id THEN {cost_column}
|
||||
ELSE {reverse_cost_column}
|
||||
@@ -713,6 +856,14 @@ def _outgoing_edges(db: Session, dataset_id: int, node_id: int, mode: str) -> li
|
||||
edges = []
|
||||
for row in rows:
|
||||
forward = bool(row.forward)
|
||||
cost_s = float(row.cost_s)
|
||||
if mode == "bike":
|
||||
tags = _json_object(str(row.tags_json or ""))
|
||||
if not _bike_access_python(row.highway, tags):
|
||||
continue
|
||||
if _bike_direction_forbidden_python(row.highway, tags, reverse=not forward):
|
||||
continue
|
||||
cost_s = _bike_edge_seconds(float(row.length_m), row.highway, tags)
|
||||
if forward:
|
||||
to_node = int(row.target_osm_node_id)
|
||||
from_lon, from_lat = float(row.source_lon), float(row.source_lat)
|
||||
@@ -730,7 +881,7 @@ def _outgoing_edges(db: Session, dataset_id: int, node_id: int, mode: str) -> li
|
||||
from_lat=from_lat,
|
||||
to_lon=to_lon,
|
||||
to_lat=to_lat,
|
||||
cost_s=float(row.cost_s),
|
||||
cost_s=cost_s,
|
||||
length_m=float(row.length_m),
|
||||
highway=row.highway,
|
||||
name=row.name,
|
||||
@@ -869,10 +1020,82 @@ def _connector_feature(kind: str, mode: str, coordinates: list[list[float]], dis
|
||||
|
||||
|
||||
def _connector_seconds(distance_m: float, mode: str) -> float:
|
||||
speed = 1.35 if mode == "walk" else 8.0
|
||||
speed = 1.35 if mode == "walk" else BIKE_SPEED_MPS if mode == "bike" else 8.0
|
||||
return float(distance_m) / speed
|
||||
|
||||
|
||||
def _bike_edge_seconds(length_m: float, highway: str | None, tags: dict[str, object]) -> float:
|
||||
factor = {
|
||||
"cycleway": 0.82,
|
||||
"path": 0.95,
|
||||
"living_street": 1.0,
|
||||
"residential": 1.0,
|
||||
"service": 1.08,
|
||||
"track": 1.10,
|
||||
"unclassified": 1.12,
|
||||
"road": 1.12,
|
||||
"tertiary": 1.12,
|
||||
"tertiary_link": 1.12,
|
||||
"secondary": 1.22,
|
||||
"secondary_link": 1.22,
|
||||
"primary": 1.35,
|
||||
"primary_link": 1.35,
|
||||
"footway": 1.40,
|
||||
"pedestrian": 1.40,
|
||||
"bridleway": 1.40,
|
||||
}.get(str(highway or ""), 1.05)
|
||||
return float(length_m) / BIKE_SPEED_MPS * factor
|
||||
|
||||
|
||||
def _bike_access_python(highway: str | None, tags: dict[str, object]) -> bool:
|
||||
highway_text = str(highway or "")
|
||||
bicycle = _tag_text(tags, "bicycle")
|
||||
access = _tag_text(tags, "access")
|
||||
vehicle = _tag_text(tags, "vehicle")
|
||||
if highway_text in {"motorway", "motorway_link", "steps", "platform"}:
|
||||
return False
|
||||
if bicycle in {"no", "private"}:
|
||||
return False
|
||||
if access in {"no", "private"} and bicycle not in {"yes", "designated", "permissive", "destination"} and vehicle not in {"yes", "designated", "permissive", "destination"}:
|
||||
return False
|
||||
if highway_text in {"footway", "pedestrian", "bridleway"} and bicycle not in {"yes", "designated", "permissive", "destination"}:
|
||||
return False
|
||||
if highway_text in {"trunk", "trunk_link"} and bicycle not in {"yes", "designated", "permissive", "destination"}:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _bike_direction_forbidden_python(highway: str | None, tags: dict[str, object], *, reverse: bool) -> bool:
|
||||
oneway = _tag_text(tags, "oneway")
|
||||
junction = _tag_text(tags, "junction")
|
||||
if reverse:
|
||||
one_way = oneway in {"yes", "true", "1"} or junction == "roundabout" or str(highway or "") == "motorway"
|
||||
else:
|
||||
one_way = oneway == "-1"
|
||||
if not one_way:
|
||||
return False
|
||||
bicycle_direction_key = "bicycle:backward" if reverse else "bicycle:forward"
|
||||
return (
|
||||
_tag_text(tags, "oneway:bicycle") != "no"
|
||||
and _tag_text(tags, "bicycle:oneway") != "no"
|
||||
and _tag_text(tags, bicycle_direction_key) not in {"yes", "designated", "permissive"}
|
||||
)
|
||||
|
||||
|
||||
def _tag_text(tags: dict[str, object], key: str) -> str:
|
||||
return str(tags.get(key) or "").strip().lower()
|
||||
|
||||
|
||||
def _json_object(value: str | None) -> dict[str, object]:
|
||||
if not value:
|
||||
return {}
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
|
||||
|
||||
def _duration_minutes_ceil(seconds: int | float | None) -> int | None:
|
||||
if seconds is None:
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user