GTFS Shape Matching & Polyline Clipping
Selects the correct travel direction from a GTFS route's multiple direction variants, then clips the full route shape to only the segment the passenger will actually ride — from their boarding stop to their alighting stop.
Where It Is Used
apps/passengers-app/src/app/pages/gtfs-route-detail/gtfs-route-detail.page.ts
Applied when a passenger selects a GTFS route (Cochabamba public transit) to view their specific trip segment on the map.
The Problem
A single GTFS route typically has 2+ direction variants (outbound / inbound / circular), each with a full polyline covering the entire route. The passenger only travels a portion of one direction. Displaying the wrong direction or the full polyline creates a confusing and cluttered map.
Algorithm
Phase 1: Direction Selection
For each direction variant of the GTFS route:
-
Find the polyline point closest to the boarding stop: $$i_{\text{board}} = \arg\min_{i} \text{Haversine}(pts[i], \text{boardStop})$$
-
Find the polyline point closest to the alighting stop, constrained to come after $i_{\text{board}}$ (enforces forward travel): $$i_{\text{alight}} = \arg\min_{i > i_{\text{board}}} \text{Haversine}(pts[i], \text{alightStop})$$
-
Score the direction: $\text{score} = d_{\text{board}} + d_{\text{alight}}$ (sum of the two closest distances).
Select the direction with the lowest score.
let bestDir = null, bestScore = Infinity;
for (const dir of directions) {
const pts = dir.polyline;
let boardMin = Infinity, boardIdx = 0;
for (let i = 0; i < pts.length; i++) {
const d = haversine(pts[i], boardStop);
if (d < boardMin) { boardMin = d; boardIdx = i; }
}
let alightMin = Infinity, alightIdx = boardIdx;
for (let i = boardIdx; i < pts.length; i++) {
const d = haversine(pts[i], alightStop);
if (d < alightMin) { alightMin = d; alightIdx = i; }
}
const score = boardMin + alightMin;
if (score < bestScore) { bestScore = score; bestDir = { dir, boardIdx, alightIdx }; }
}
Phase 2: Polyline Clipping
Extract the slice of the selected direction's polyline between the two matched indices:
const { dir, boardIdx, alightIdx } = bestDir;
const clipped = dir.polyline.slice(
Math.min(boardIdx, alightIdx),
Math.max(boardIdx, alightIdx) + 1
);
Math.min/max handles edge cases where the route is circular and the indices may appear reversed.
syncc Route Equivalent
For syncc-registered routes (non-GTFS), the same polyline clipping is performed in route-detail.page.ts:
- Decode
Line.polyline→allPts[] - Find nearest index to boarding stop: brute-force Haversine scan
- Find nearest index to alighting stop: separate brute-force scan
- Clip:
allPts.slice(pStart, pEnd + 1)
Related
- Haversine Distance — used for all distance comparisons
- Google Encoded Polyline — decodes route shapes before matching
- Along-Route Distance Accumulation — applied to the clipped segment
- GTFS Index Building — provides the direction polylines matched here