sameer-graph-lib 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sameer_graph_lib-0.1.0/src/sameer_graph_lib.egg-info → sameer_graph_lib-0.2.0}/PKG-INFO +5 -1
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/pyproject.toml +4 -2
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib/__init__.py +26 -1
- sameer_graph_lib-0.2.0/src/sameer_graph_lib/motion.py +295 -0
- sameer_graph_lib-0.2.0/src/sameer_graph_lib/plylinedecoding.py +43 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0/src/sameer_graph_lib.egg-info}/PKG-INFO +5 -1
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib.egg-info/SOURCES.txt +2 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib.egg-info/requires.txt +5 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/LICENSE +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/README.md +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/setup.cfg +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib/_h3.py +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib/affinity_graph.py +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib/corridor_extractor.py +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib/geometry.py +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib/hex_graph.py +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib/plotting.py +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib/spatial_ingestor.py +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib/topology_analyzer.py +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib.egg-info/dependency_links.txt +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib.egg-info/top_level.txt +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/tests/test_geometry.py +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/tests/test_h3_cell_plotting.py +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/tests/test_h3_cells_map_plotting.py +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/tests/test_h3_grid_distance.py +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/tests/test_plotting.py +0 -0
- {sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sameer-graph-lib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: H3 and NetworkX based route affinity graph toolkit.
|
|
5
5
|
Author-email: Sameer <sameerkumarroy073@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,12 +20,16 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
Requires-Dist: h3>=4.0.0
|
|
22
22
|
Requires-Dist: networkx>=3.0
|
|
23
|
+
Requires-Dist: polyline>=2.0
|
|
23
24
|
Provides-Extra: plot
|
|
24
25
|
Requires-Dist: matplotlib>=3.7; extra == "plot"
|
|
25
26
|
Provides-Extra: geo
|
|
26
27
|
Requires-Dist: contextily>=1.5; extra == "geo"
|
|
27
28
|
Requires-Dist: geopandas>=0.14; extra == "geo"
|
|
28
29
|
Requires-Dist: shapely>=2.0; extra == "geo"
|
|
30
|
+
Provides-Extra: motion
|
|
31
|
+
Requires-Dist: numpy>=1.24; extra == "motion"
|
|
32
|
+
Requires-Dist: pyproj>=3.5; extra == "motion"
|
|
29
33
|
Provides-Extra: dev
|
|
30
34
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
31
35
|
Requires-Dist: build>=1.2; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sameer-graph-lib"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "H3 and NetworkX based route affinity graph toolkit."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -14,7 +14,8 @@ authors = [
|
|
|
14
14
|
]
|
|
15
15
|
dependencies = [
|
|
16
16
|
"h3>=4.0.0",
|
|
17
|
-
"networkx>=3.0"
|
|
17
|
+
"networkx>=3.0",
|
|
18
|
+
"polyline>=2.0"
|
|
18
19
|
]
|
|
19
20
|
keywords = ["h3", "networkx", "geospatial", "routing", "graph"]
|
|
20
21
|
classifiers = [
|
|
@@ -35,6 +36,7 @@ Issues = "https://github.com/iams31/sameer_graph_lib/issues"
|
|
|
35
36
|
[project.optional-dependencies]
|
|
36
37
|
plot = ["matplotlib>=3.7"]
|
|
37
38
|
geo = ["contextily>=1.5", "geopandas>=0.14", "shapely>=2.0"]
|
|
39
|
+
motion = ["numpy>=1.24", "pyproj>=3.5"]
|
|
38
40
|
dev = ["pytest>=8.0", "build>=1.2", "twine>=5.1"]
|
|
39
41
|
|
|
40
42
|
[tool.setuptools.packages.find]
|
|
@@ -4,7 +4,20 @@ from .affinity_graph import AffinityGraph
|
|
|
4
4
|
from .corridor_extractor import CorridorExtractor
|
|
5
5
|
from .geometry import getLatLng, get_latlng, h3_convex_hull, h3_convex_hull_geojson, makingHull, making_hull
|
|
6
6
|
from .hex_graph import HexGraph
|
|
7
|
+
from .motion import (
|
|
8
|
+
compute_accelerations,
|
|
9
|
+
compute_accelerations_from_timestamps,
|
|
10
|
+
compute_speeds,
|
|
11
|
+
compute_speeds_from_timestamps,
|
|
12
|
+
detect_sudden_braking_from_timestamps,
|
|
13
|
+
dominant_frequency_hz,
|
|
14
|
+
get_max_speed,
|
|
15
|
+
max_accelaration,
|
|
16
|
+
resample_polyline,
|
|
17
|
+
smooth_speeds_median,
|
|
18
|
+
)
|
|
7
19
|
from .plotting import cells_to_geodataframe, plot_h3_cells, plot_h3_cells_map
|
|
20
|
+
from .plylinedecoding import decode_polyline, get_hexes_from_polyline
|
|
8
21
|
from .spatial_ingestor import SpatialIngestor
|
|
9
22
|
from .topology_analyzer import TopologyAnalyzer
|
|
10
23
|
|
|
@@ -13,16 +26,28 @@ __all__ = [
|
|
|
13
26
|
"CorridorExtractor",
|
|
14
27
|
"HexGraph",
|
|
15
28
|
"cells_to_geodataframe",
|
|
29
|
+
"compute_accelerations",
|
|
30
|
+
"compute_accelerations_from_timestamps",
|
|
31
|
+
"compute_speeds",
|
|
32
|
+
"compute_speeds_from_timestamps",
|
|
33
|
+
"decode_polyline",
|
|
34
|
+
"detect_sudden_braking_from_timestamps",
|
|
35
|
+
"dominant_frequency_hz",
|
|
16
36
|
"getLatLng",
|
|
37
|
+
"get_hexes_from_polyline",
|
|
17
38
|
"get_latlng",
|
|
39
|
+
"get_max_speed",
|
|
18
40
|
"h3_convex_hull",
|
|
19
41
|
"h3_convex_hull_geojson",
|
|
20
42
|
"makingHull",
|
|
21
43
|
"making_hull",
|
|
44
|
+
"max_accelaration",
|
|
22
45
|
"plot_h3_cells",
|
|
23
46
|
"plot_h3_cells_map",
|
|
47
|
+
"resample_polyline",
|
|
48
|
+
"smooth_speeds_median",
|
|
24
49
|
"SpatialIngestor",
|
|
25
50
|
"TopologyAnalyzer",
|
|
26
51
|
]
|
|
27
52
|
|
|
28
|
-
__version__ = "0.
|
|
53
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Motion analysis for polylines sampled at a fixed time interval.
|
|
2
|
+
|
|
3
|
+
All inputs use the convention ``polyline = [(lat, lng), (lat, lng), ...]`` and
|
|
4
|
+
``interval_seconds`` is the time between consecutive points. Distances are
|
|
5
|
+
computed with the haversine formula on a sphere of radius 6_371_000 m.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from .plylinedecoding import decode_polyline
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
Point = tuple[float, float]
|
|
12
|
+
TimedPoint = tuple[float, float, float]
|
|
13
|
+
def _require_pyproj():
|
|
14
|
+
try:
|
|
15
|
+
import pyproj
|
|
16
|
+
except ImportError as exc:
|
|
17
|
+
raise ImportError(
|
|
18
|
+
"Motion helpers require pyproj: pip install 'sameer-graph-lib[motion]'"
|
|
19
|
+
) from exc
|
|
20
|
+
return pyproj
|
|
21
|
+
def _require_numpy():
|
|
22
|
+
try:
|
|
23
|
+
import numpy as np
|
|
24
|
+
except ImportError as exc:
|
|
25
|
+
raise ImportError(
|
|
26
|
+
"Motion helpers require numpy: pip install 'sameer-graph-lib[motion]'"
|
|
27
|
+
) from exc
|
|
28
|
+
return np
|
|
29
|
+
def _haversine_m(p1, p2,geojson=True) -> float:
|
|
30
|
+
pyproj=_require_pyproj()
|
|
31
|
+
geodesic = pyproj.Geod(ellps='WGS84')
|
|
32
|
+
if geojson:
|
|
33
|
+
u,v, distance_m = geodesic.inv(p1[0], p1[1], p2[0], p2[1])
|
|
34
|
+
else:
|
|
35
|
+
u,v, distance_m = geodesic.inv(p1[1], p1[0], p2[1], p2[0])
|
|
36
|
+
return distance_m/1000.0
|
|
37
|
+
def get_max_speed(polyline, interval_seconds: float, smooth: bool = True,geojson=True,precision=5,max_bucket=5) -> float:
|
|
38
|
+
decoded=decode_polyline(polyline_str=polyline, precision=precision, geojson=geojson)
|
|
39
|
+
if len(decoded) < 2:
|
|
40
|
+
return {}
|
|
41
|
+
speed_array=[]
|
|
42
|
+
max_speed={}
|
|
43
|
+
if len(decoded) <= max_bucket + 1:
|
|
44
|
+
for j in range(len(decoded)-1):
|
|
45
|
+
dist=_haversine_m(decoded[j], decoded[j+1],geojson=geojson)
|
|
46
|
+
speed=dist*3600/interval_seconds
|
|
47
|
+
speed_array.append(speed)
|
|
48
|
+
max_speed[1]=max(speed_array)
|
|
49
|
+
max_speed['all']=max(speed_array)
|
|
50
|
+
return max_speed
|
|
51
|
+
for i in range(1,max_bucket+1):
|
|
52
|
+
speed_array1=[]
|
|
53
|
+
for j in range(len(decoded)-1-i):
|
|
54
|
+
dist=_haversine_m(decoded[j], decoded[j+i],geojson=geojson)
|
|
55
|
+
speed=dist*3600/(interval_seconds*i)
|
|
56
|
+
speed_array1.append(speed)
|
|
57
|
+
speed_array.append(speed)
|
|
58
|
+
max_speed[i]=max(speed_array1)
|
|
59
|
+
max_speed['all']=max(speed_array)
|
|
60
|
+
return max_speed
|
|
61
|
+
|
|
62
|
+
def max_accelaration(polyline, interval_seconds: float, smooth: bool = True,geojson=True,precision=5) -> float:
|
|
63
|
+
accelaration_array=[]
|
|
64
|
+
decoded=decode_polyline(polyline_str=polyline, precision=precision, geojson=geojson)
|
|
65
|
+
if len(decoded) < 3:
|
|
66
|
+
return {}
|
|
67
|
+
for i in range(len(decoded)-2):
|
|
68
|
+
dist1=_haversine_m(decoded[i], decoded[i+1],geojson=geojson)
|
|
69
|
+
dist2=_haversine_m(decoded[i+1], decoded[i+2],geojson=geojson)
|
|
70
|
+
speed1=dist1*3600/interval_seconds
|
|
71
|
+
speed2=dist2*3600/interval_seconds
|
|
72
|
+
accelaration=(speed2-speed1)/interval_seconds
|
|
73
|
+
accelaration_array.append(accelaration)
|
|
74
|
+
return {'max':max(accelaration_array),'min':min(accelaration_array)}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# New helpers below: m/s and m/s^2 outputs. They use _geodesic_m (true
|
|
79
|
+
# meters) so they stay independent of _haversine_m's km return value.
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _geodesic_m(p1, p2, geojson: bool = True) -> float:
|
|
84
|
+
"""Geodesic distance in meters between two points."""
|
|
85
|
+
pyproj = _require_pyproj()
|
|
86
|
+
geodesic = pyproj.Geod(ellps="WGS84")
|
|
87
|
+
if geojson:
|
|
88
|
+
_, _, d = geodesic.inv(p1[0], p1[1], p2[0], p2[1])
|
|
89
|
+
else:
|
|
90
|
+
_, _, d = geodesic.inv(p1[1], p1[0], p2[1], p2[0])
|
|
91
|
+
return d
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _decode_or_passthrough(polyline, geojson: bool, precision: int) -> list:
|
|
95
|
+
"""Accept either an encoded polyline string or a sequence of points."""
|
|
96
|
+
if isinstance(polyline, str):
|
|
97
|
+
return decode_polyline(polyline_str=polyline, precision=precision, geojson=geojson)
|
|
98
|
+
return list(polyline)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _validate_timed(points: Sequence[TimedPoint], min_points: int) -> None:
|
|
102
|
+
if len(points) < min_points:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"timestamped polyline needs at least {min_points} points, "
|
|
105
|
+
f"got {len(points)}"
|
|
106
|
+
)
|
|
107
|
+
for i in range(len(points) - 1):
|
|
108
|
+
if points[i + 1][2] <= points[i][2]:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"timestamps must be strictly increasing; "
|
|
111
|
+
f"got t[{i}]={points[i][2]} >= t[{i+1}]={points[i + 1][2]}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def compute_speeds(
|
|
116
|
+
polyline,
|
|
117
|
+
interval_seconds: float,
|
|
118
|
+
geojson: bool = True,
|
|
119
|
+
precision: int = 5,
|
|
120
|
+
) -> list[float]:
|
|
121
|
+
"""Return instantaneous speeds (m/s) between consecutive points.
|
|
122
|
+
|
|
123
|
+
Length is ``n - 1``. ``polyline`` may be an encoded string or a sequence
|
|
124
|
+
of points already in the order set by ``geojson``.
|
|
125
|
+
"""
|
|
126
|
+
if interval_seconds <= 0:
|
|
127
|
+
raise ValueError("interval_seconds must be positive")
|
|
128
|
+
decoded = _decode_or_passthrough(polyline, geojson, precision)
|
|
129
|
+
if len(decoded) < 2:
|
|
130
|
+
raise ValueError(f"polyline needs at least 2 points, got {len(decoded)}")
|
|
131
|
+
return [
|
|
132
|
+
_geodesic_m(decoded[i], decoded[i + 1], geojson=geojson) / interval_seconds
|
|
133
|
+
for i in range(len(decoded) - 1)
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def compute_accelerations(
|
|
138
|
+
polyline,
|
|
139
|
+
interval_seconds: float,
|
|
140
|
+
geojson: bool = True,
|
|
141
|
+
precision: int = 5,
|
|
142
|
+
) -> list[float]:
|
|
143
|
+
"""Return accelerations (m/s^2) between consecutive speed samples.
|
|
144
|
+
|
|
145
|
+
Length is ``n - 2``. Positive values are acceleration, negative values
|
|
146
|
+
are deceleration.
|
|
147
|
+
"""
|
|
148
|
+
speeds = compute_speeds(polyline, interval_seconds, geojson=geojson, precision=precision)
|
|
149
|
+
if len(speeds) < 2:
|
|
150
|
+
raise ValueError("polyline needs at least 3 points to compute acceleration")
|
|
151
|
+
return [
|
|
152
|
+
(speeds[i + 1] - speeds[i]) / interval_seconds
|
|
153
|
+
for i in range(len(speeds) - 1)
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def smooth_speeds_median(
|
|
158
|
+
polyline,
|
|
159
|
+
interval_seconds: float,
|
|
160
|
+
window_size: int = 7,
|
|
161
|
+
geojson: bool = True,
|
|
162
|
+
precision: int = 5,
|
|
163
|
+
) -> list[float]:
|
|
164
|
+
"""Return speeds (m/s) with outliers nullified via sliding-window median.
|
|
165
|
+
|
|
166
|
+
Each sample is replaced by the median of itself and its
|
|
167
|
+
``window_size - 1`` neighbours (edge-padded at the boundaries). A median
|
|
168
|
+
is robust: a single bad GPS reading is replaced by its neighbourhood's
|
|
169
|
+
middle value, instead of being smeared across surrounding samples
|
|
170
|
+
(which is what FFT or polynomial smoothers would do).
|
|
171
|
+
|
|
172
|
+
Default ``window_size=7`` handles route-shape polylines where outliers
|
|
173
|
+
can come in clusters of 2–3 consecutive samples; for clean GPS traces
|
|
174
|
+
where outliers are isolated, ``window_size=3`` or ``5`` is enough.
|
|
175
|
+
"""
|
|
176
|
+
np = _require_numpy()
|
|
177
|
+
if window_size < 3 or window_size % 2 == 0:
|
|
178
|
+
raise ValueError("window_size must be an odd integer >= 3")
|
|
179
|
+
|
|
180
|
+
speeds = compute_speeds(polyline, interval_seconds, geojson=geojson, precision=precision)
|
|
181
|
+
if len(speeds) < window_size:
|
|
182
|
+
return speeds
|
|
183
|
+
|
|
184
|
+
from numpy.lib.stride_tricks import sliding_window_view
|
|
185
|
+
|
|
186
|
+
half = window_size // 2
|
|
187
|
+
arr = np.asarray(speeds, dtype=float)
|
|
188
|
+
padded = np.pad(arr, half, mode="edge")
|
|
189
|
+
windows = sliding_window_view(padded, window_size)
|
|
190
|
+
return np.median(windows, axis=1).tolist()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def dominant_frequency_hz(
|
|
194
|
+
polyline,
|
|
195
|
+
interval_seconds: float,
|
|
196
|
+
geojson: bool = True,
|
|
197
|
+
precision: int = 5,
|
|
198
|
+
) -> float:
|
|
199
|
+
"""Return the dominant non-DC frequency (Hz) of the speed signal."""
|
|
200
|
+
np = _require_numpy()
|
|
201
|
+
speeds = compute_speeds(polyline, interval_seconds, geojson=geojson, precision=precision)
|
|
202
|
+
if len(speeds) < 2:
|
|
203
|
+
return 0.0
|
|
204
|
+
|
|
205
|
+
spectrum = np.abs(np.fft.rfft(speeds))
|
|
206
|
+
freqs = np.fft.rfftfreq(len(speeds), d=interval_seconds)
|
|
207
|
+
spectrum[0] = 0
|
|
208
|
+
if spectrum.max() == 0:
|
|
209
|
+
return 0.0
|
|
210
|
+
return float(freqs[int(spectrum.argmax())])
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
# Timestamped polylines: each point is (lat, lng, t_seconds)
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def compute_speeds_from_timestamps(points: Sequence[TimedPoint]) -> list[float]:
|
|
219
|
+
"""Return speeds (m/s) using each segment's actual delta-t."""
|
|
220
|
+
_validate_timed(points, min_points=2)
|
|
221
|
+
return [
|
|
222
|
+
_geodesic_m(points[i], points[i + 1], geojson=False)
|
|
223
|
+
/ (points[i + 1][2] - points[i][2])
|
|
224
|
+
for i in range(len(points) - 1)
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def compute_accelerations_from_timestamps(points: Sequence[TimedPoint]) -> list[float]:
|
|
229
|
+
"""Return accelerations (m/s^2) for a non-uniformly-sampled trace.
|
|
230
|
+
|
|
231
|
+
For each interior point ``i+1`` we take ``(v[i+1] - v[i]) / dt`` where
|
|
232
|
+
``dt = (segment_i_duration + segment_{i+1}_duration) / 2``.
|
|
233
|
+
"""
|
|
234
|
+
_validate_timed(points, min_points=3)
|
|
235
|
+
speeds = compute_speeds_from_timestamps(points)
|
|
236
|
+
out = []
|
|
237
|
+
for i in range(len(speeds) - 1):
|
|
238
|
+
dt_i = points[i + 1][2] - points[i][2]
|
|
239
|
+
dt_next = points[i + 2][2] - points[i + 1][2]
|
|
240
|
+
out.append((speeds[i + 1] - speeds[i]) / ((dt_i + dt_next) / 2))
|
|
241
|
+
return out
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def detect_sudden_braking_from_timestamps(
|
|
245
|
+
points: Sequence[TimedPoint],
|
|
246
|
+
threshold_mps2: float = -3.0,
|
|
247
|
+
) -> list[dict]:
|
|
248
|
+
"""Return sudden-braking events for a timestamped trace.
|
|
249
|
+
|
|
250
|
+
Each event is ``{index, time_s, deceleration_mps2}`` where ``index``
|
|
251
|
+
is the index in ``points`` of the boundary sample.
|
|
252
|
+
"""
|
|
253
|
+
if threshold_mps2 >= 0:
|
|
254
|
+
raise ValueError("threshold_mps2 must be negative (it is a deceleration)")
|
|
255
|
+
accels = compute_accelerations_from_timestamps(points)
|
|
256
|
+
return [
|
|
257
|
+
{
|
|
258
|
+
"index": i + 1,
|
|
259
|
+
"time_s": points[i + 1][2],
|
|
260
|
+
"deceleration_mps2": a,
|
|
261
|
+
}
|
|
262
|
+
for i, a in enumerate(accels)
|
|
263
|
+
if a <= threshold_mps2
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def resample_polyline(
|
|
268
|
+
points: Sequence[TimedPoint],
|
|
269
|
+
interval_seconds: float,
|
|
270
|
+
) -> list[Point]:
|
|
271
|
+
"""Resample a timestamped polyline to a uniform time grid (linear interp).
|
|
272
|
+
|
|
273
|
+
Returns ``[(lat, lng), ...]``. Pass to :func:`compute_speeds` with
|
|
274
|
+
``geojson=False`` to use FFT smoothing on real GPS logs.
|
|
275
|
+
"""
|
|
276
|
+
if interval_seconds <= 0:
|
|
277
|
+
raise ValueError("interval_seconds must be positive")
|
|
278
|
+
_validate_timed(points, min_points=2)
|
|
279
|
+
|
|
280
|
+
t_start = points[0][2]
|
|
281
|
+
t_end = points[-1][2]
|
|
282
|
+
n_steps = int((t_end - t_start) / interval_seconds) + 1
|
|
283
|
+
|
|
284
|
+
out: list[Point] = []
|
|
285
|
+
j = 0
|
|
286
|
+
for k in range(n_steps):
|
|
287
|
+
t = t_start + k * interval_seconds
|
|
288
|
+
while j < len(points) - 2 and points[j + 1][2] < t:
|
|
289
|
+
j += 1
|
|
290
|
+
t1, t2 = points[j][2], points[j + 1][2]
|
|
291
|
+
alpha = 0.0 if t2 == t1 else (t - t1) / (t2 - t1)
|
|
292
|
+
lat = points[j][0] + alpha * (points[j + 1][0] - points[j][0])
|
|
293
|
+
lng = points[j][1] + alpha * (points[j + 1][1] - points[j][1])
|
|
294
|
+
out.append((lat, lng))
|
|
295
|
+
return out
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Google encoded-polyline helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
def _require_polyline():
|
|
5
|
+
try:
|
|
6
|
+
import polyline
|
|
7
|
+
except ImportError as exc:
|
|
8
|
+
raise ImportError(
|
|
9
|
+
"Motion helpers require polyline: pip install 'sameer-graph-lib[motion]'"
|
|
10
|
+
) from exc
|
|
11
|
+
return polyline
|
|
12
|
+
def _require_h3():
|
|
13
|
+
try:
|
|
14
|
+
import h3
|
|
15
|
+
except ImportError as exc:
|
|
16
|
+
raise ImportError(
|
|
17
|
+
"Motion helpers require polyline: pip install 'sameer-graph-lib[motion]'"
|
|
18
|
+
) from exc
|
|
19
|
+
return h3
|
|
20
|
+
|
|
21
|
+
def decode_polyline(
|
|
22
|
+
polyline_str: str,
|
|
23
|
+
precision: int = 5,
|
|
24
|
+
geojson: bool = False,
|
|
25
|
+
) -> list[tuple[float, float]]:
|
|
26
|
+
"""Decode a Google encoded polyline string.
|
|
27
|
+
|
|
28
|
+
Returns ``[(lat, lng), ...]`` by default. Pass ``geojson=True`` to get
|
|
29
|
+
GeoJSON-order ``[(lng, lat), ...]`` instead.
|
|
30
|
+
"""
|
|
31
|
+
polyline=_require_polyline()
|
|
32
|
+
return polyline.decode(polyline_str, precision=precision, geojson=geojson)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_hexes_from_polyline(
|
|
36
|
+
polyline_str: str,
|
|
37
|
+
precision: int = 5,
|
|
38
|
+
res: int = 9,
|
|
39
|
+
) -> list[str]:
|
|
40
|
+
"""Decode a polyline and return the H3 cell at ``res`` for each point."""
|
|
41
|
+
h3=_require_h3()
|
|
42
|
+
coordinates = decode_polyline(polyline_str, precision=precision, geojson=False)
|
|
43
|
+
return [h3.latlng_to_cell(lat, lng, res) for lat, lng in coordinates]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sameer-graph-lib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: H3 and NetworkX based route affinity graph toolkit.
|
|
5
5
|
Author-email: Sameer <sameerkumarroy073@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,12 +20,16 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
Requires-Dist: h3>=4.0.0
|
|
22
22
|
Requires-Dist: networkx>=3.0
|
|
23
|
+
Requires-Dist: polyline>=2.0
|
|
23
24
|
Provides-Extra: plot
|
|
24
25
|
Requires-Dist: matplotlib>=3.7; extra == "plot"
|
|
25
26
|
Provides-Extra: geo
|
|
26
27
|
Requires-Dist: contextily>=1.5; extra == "geo"
|
|
27
28
|
Requires-Dist: geopandas>=0.14; extra == "geo"
|
|
28
29
|
Requires-Dist: shapely>=2.0; extra == "geo"
|
|
30
|
+
Provides-Extra: motion
|
|
31
|
+
Requires-Dist: numpy>=1.24; extra == "motion"
|
|
32
|
+
Requires-Dist: pyproj>=3.5; extra == "motion"
|
|
29
33
|
Provides-Extra: dev
|
|
30
34
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
31
35
|
Requires-Dist: build>=1.2; extra == "dev"
|
|
@@ -7,7 +7,9 @@ src/sameer_graph_lib/affinity_graph.py
|
|
|
7
7
|
src/sameer_graph_lib/corridor_extractor.py
|
|
8
8
|
src/sameer_graph_lib/geometry.py
|
|
9
9
|
src/sameer_graph_lib/hex_graph.py
|
|
10
|
+
src/sameer_graph_lib/motion.py
|
|
10
11
|
src/sameer_graph_lib/plotting.py
|
|
12
|
+
src/sameer_graph_lib/plylinedecoding.py
|
|
11
13
|
src/sameer_graph_lib/spatial_ingestor.py
|
|
12
14
|
src/sameer_graph_lib/topology_analyzer.py
|
|
13
15
|
src/sameer_graph_lib.egg-info/PKG-INFO
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib/corridor_extractor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{sameer_graph_lib-0.1.0 → sameer_graph_lib-0.2.0}/src/sameer_graph_lib.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|