"""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()