minotor
Version:
A lightweight client-side transit routing library.
98 lines (76 loc) • 4.62 kB
Markdown
Many GTFS feeds only partially populate `transfers.txt`, so nearby stops that belong to different stations (e.g. a subway platform and an adjacent tram or bus terminal) end up with no walking connection and cannot be used as an interchange. A `TransferGenerator` can be passed to the parser to synthesize the missing walking transfers as a pre-processing step, before the timetable is serialized.
## Built-in implementation
`StraightLineTransferGenerator` connects nearby stops that aren't already linked in the feed — for every stop it adds walking transfers to the other stops within a radius, estimating the walking time from the straight-line distance:
```ts
import {
GtfsParser,
StraightLineTransferGenerator,
extendedGtfsProfile,
} from 'minotor/parser';
const parser = new GtfsParser(
'gtfs-feed.zip',
extendedGtfsProfile,
new StraightLineTransferGenerator({ maxDistanceMeters: 500 }),
);
const timetable = await parser.parseTimetable(new Date());
```
On top of the walking time, some adjustments make the estimate more realistic:
- a **detour factor** as no path in the real world is a straight line
- a flat **change penalty** added to every transfer, for the general overhead of changing
The change penalty is symmetric, so the two directions of a transfer share the same minimum time. Existing transfers from the feed are always preserved:
| Option | Default | Description |
| ---------------------- | ------- | ----------------------------------------------------------------------- |
| `maxDistanceMeters` | `500` | Radius within which two stops are connected |
| `walkingSpeedKmh` | `4` | Assumed walking speed |
| `detourFactor` | `1.3` | Multiplier from straight-line to real walking distance (`1` to disable) |
| `changePenaltyMinutes` | `3` | Flat penalty added to every generated transfer (`0` to disable) |
## Custom implementation
The straight-line estimate is relatively basic but works without external dependencies. For realistic walking times, you can implement the `TransferGenerator` interface yourself by e.g. delegating to an external pedestrian routing engine.
The parser passes the candidate origin stops (route-served stops with coordinates) and deduplicates the result against the transfers already in the feed, keeping only links between stops a route actually calls at — so you can emit a transfer for every nearby pair without tracking what already exists. Return the directed transfers to add, using `TransferTypes.REQUIRES_MINIMAL_TIME` so the router treats them as walking connections (for both transfers and access/egress). `minTransferTime` is expressed in minutes.
```ts
import { GtfsParser, TransferTypes, extendedGtfsProfile } from 'minotor/parser';
import type { GeneratedTransfers, TransferGenerator } from 'minotor/parser';
import type { Stop, StopsIndex } from 'minotor';
// Delegates to a hypothetical external walking-router HTTP API.
class WalkingApiTransferGenerator implements TransferGenerator {
async generate(
originStops: Stop[],
stops: StopsIndex,
): Promise<GeneratedTransfers> {
const generated: GeneratedTransfers = new Map();
for (const origin of originStops) {
// Reuse minotor's geo index to find nearby candidate stops.
const neighbors = stops.findStopsByLocation(
origin.lat,
origin.lon,
Infinity,
this.maxDistanceKm,
);
for (const neighbor of neighbors) {
// Ask the external engine for a real door-to-door walking time.
const walkTimeInMinutes = await callExternalRoutingEngine(
origin.lat,
origin.lon,
neighbor.lat,
neighbor.lon,
);
const transfers = generated.get(origin.id) ?? [];
transfers.push({
destination: neighbor.id,
type: TransferTypes.REQUIRES_MINIMAL_TIME,
minTransferTime: walkTimeInMinutes,
});
generated.set(origin.id, transfers);
}
}
return generated;
}
}
const parser = new GtfsParser(
'gtfs-feed.zip',
extendedGtfsProfile,
new WalkingApiTransferGenerator('https://walk.example.com'),
);
```
Transfers are directed. The example above emits one edge per `origin -> neighbor`; the reverse edge is produced when the neighbor is itself visited as an origin (every candidate stop is passed as an origin), so a symmetric walking time yields a usable connection both ways.