mtl_isochrone / tests /test_geometry.py
HenriLD's picture
CI/CD: GitHub Actions (engine + PyPy-install + frontend) and auto-deploy
35dedf6
Raw
History Blame Contribute Delete
2.81 kB
"""Geometry / build helpers: bus-shape smoothing drift bound, simplification,
stop projection monotonicity, GTFS time parsing."""
from __future__ import annotations
import math
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from engine.gtfs import _simplify, smooth_shape # noqa: E402
from engine.model import parse_gtfs_time, project_stops_to_shape # noqa: E402
_SX = 111320 * math.cos(math.radians(45.5))
_SY = 110540
def _pt_seg_m(p, a, b):
px, py = (p[0] - a[0]) * _SX, (p[1] - a[1]) * _SY
dx, dy = (b[0] - a[0]) * _SX, (b[1] - a[1]) * _SY
seg2 = dx * dx + dy * dy
t = 0.0 if seg2 == 0 else max(0.0, min(1.0, (px * dx + py * dy) / seg2))
return math.hypot(px - t * dx, py - t * dy)
def _drift_m(orig, smoothed): # max distance from an original vertex to the smoothed line
return max(min(_pt_seg_m(p, smoothed[k], smoothed[k + 1]) for k in range(len(smoothed) - 1))
for p in orig)
def _zigzag(): # long (~280 m) segments with 90° corners
pts = []
for i in range(9):
lon = -73.57 + i * 0.0026
lat = 45.50 + (0.0018 if i % 2 else 0.0)
pts.append([round(lon, 5), round(lat, 5)])
return pts
class TestGeometry(unittest.TestCase):
def test_smooth_drift_is_capped(self):
z = _zigzag()
sm = smooth_shape(z, iters=2, cap_m=7.0, tol_m=3.0)
# capped corner-cutting keeps the line on the road; uncapped would drift ~50 m
self.assertLess(_drift_m(z, sm), 12.0)
def test_smooth_preserves_endpoints(self):
z = _zigzag()
sm = smooth_shape(z)
self.assertAlmostEqual(sm[0][0], z[0][0], places=5)
self.assertAlmostEqual(sm[0][1], z[0][1], places=5)
self.assertAlmostEqual(sm[-1][0], z[-1][0], places=5)
self.assertAlmostEqual(sm[-1][1], z[-1][1], places=5)
def test_simplify_drops_collinear(self):
line = [[-73.57, 45.50], [-73.56, 45.51], [-73.55, 45.52], [-73.54, 45.53]]
self.assertEqual(len(_simplify(line, tol_m=2.0)), 2)
def test_project_stops_strictly_increasing(self):
shape = [[-73.57, 45.50], [-73.56, 45.50], [-73.55, 45.50], [-73.54, 45.50]]
stops = [(-73.57, 45.501), (-73.558, 45.501), (-73.545, 45.501)]
idx = project_stops_to_shape(stops, shape)
self.assertEqual(idx, sorted(idx))
self.assertTrue(all(idx[i] < idx[i + 1] for i in range(len(idx) - 1)))
def test_parse_gtfs_time(self):
self.assertEqual(parse_gtfs_time("00:00:00"), 0)
self.assertEqual(parse_gtfs_time("08:00:00"), 28800)
self.assertEqual(parse_gtfs_time("25:10:05"), 90605) # GTFS hours may exceed 24
if __name__ == "__main__":
unittest.main()