Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

List each platform as GTFS stop in the generated GTFS, and various GTFS semantic fixes #73

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4fef5a1
build on modern npm
Mar 31, 2023
e1214a1
add executable bit into script
Mar 31, 2023
8e60095
increase Javascript heap space
Mar 31, 2023
219c446
update station data from latest knowledge base
Apr 5, 2023
9c5fb5f
update agency list
Mar 31, 2023
ac53c79
enable source map for debugging
Apr 4, 2023
86bd0e0
let's assume all trains can convey wheelchair at this moment
Apr 5, 2023
073a764
remove pass time and handle public / scheduled departure / arrival ti…
Apr 18, 2023
c08ec0e
increase memory for gtfs and gtfs-zip scripts
Apr 24, 2023
6ecdf54
add Locomotive Services
Apr 26, 2023
997cc2d
Merged PR 92: Fix errors when building on latest Node.js platform
May 9, 2023
765596f
Merged PR 78: Update agency and stations list
May 9, 2023
16c8f88
Merged PR 83: Remove passing points as stops
May 9, 2023
3500c70
return stations and platforms in stops.txt
Mar 31, 2023
5f1118b
output platform locations in schedule and remove incorrect use of hea…
Mar 31, 2023
a11320c
use more description route names
Apr 4, 2023
07c0385
fix platform generation not returning platforms from sub-stations (e.…
Apr 4, 2023
aa3ecc0
fix stop query for sub-stations
Apr 5, 2023
8fcfa0f
add comments explaining main and minor CRS codes
Apr 5, 2023
29be97a
allow specifying alternative station data
Apr 12, 2023
730e090
add bracket to platform in station name
Apr 18, 2023
46d3a04
routes short name can't be used as the unique key to query the route
May 16, 2023
f0bb915
add platform_code field into the generated GTFS
Jun 15, 2023
d8da5c2
revert change to arrival and departure time handling
Aug 23, 2023
bdd653e
add alternative station data example
Aug 23, 2023
be738e3
Merge branch 'master' into feature/platforms
Aug 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ Convert the DTD/TTIS version of the timetable (up to 3 months into the future) t
```
dtd2mysql --timetable /path/to/RJTTFxxx.ZIP
dtd2mysql --gtfs-zip filename-of-gtfs.zip

# use alternative source of station data
# the provided example contains station and platform coordinates extracted from OpenStreetMap
dtd2mysql --gtfs-zip filename-of-gtfs.zip stations.example.json
```

## Routeing Guide
Expand Down
13 changes: 9 additions & 4 deletions src/cli/OutputGTFSCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export class OutputGTFSCommand implements CLICommand {
throw new Error(`Output path ${this.baseDir} does not exist.`);
}

if (argv.length > 4) {
const json = JSON.parse(fs.readFileSync(argv[4], 'utf-8'));
this.repository.stationCoordinates = json;
}

const associationsP = this.repository.getAssociations();
const scheduleResultsP = this.repository.getSchedules();
const transfersP = this.copy(this.repository.getTransfers(), "transfers.txt");
Expand Down Expand Up @@ -75,7 +80,7 @@ export class OutputGTFSCommand implements CLICommand {
/**
* trips.txt, stop_times.txt and routes.txt have interdependencies so they are written together
*/
private copyTrips(schedules: Schedule[], serviceIds: ServiceIdIndex): Promise<any> {
private async copyTrips(schedules: Schedule[], serviceIds: ServiceIdIndex): Promise<any> {
console.log("Writing trips.txt, stop_times.txt and routes.txt");
const trips = this.output.open(this.baseDir + "trips.txt");
const stopTimes = this.output.open(this.baseDir + "stop_times.txt");
Expand All @@ -87,9 +92,9 @@ export class OutputGTFSCommand implements CLICommand {
continue;
}

const route = schedule.toRoute();
routes[route.route_short_name] = routes[route.route_short_name] || route;
const routeId = routes[route.route_short_name].route_id;
const route = await schedule.toRoute(this.repository);
routes[route.route_id] = routes[route.route_id] || route;
const routeId = routes[route.route_id].route_id;
const serviceId = serviceIds[schedule.calendar.id];

trips.write(schedule.toTrip(serviceId, routeId));
Expand Down
9 changes: 5 additions & 4 deletions src/gtfs/file/Stop.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {StopPlatform} from './StopTime';

export interface Stop {
stop_id: CRS;
stop_code: TIPLOC;
stop_id: StopPlatform;
stop_code: CRS;
stop_name: string;
stop_desc: string;
stop_lat: number;
Expand All @@ -12,7 +13,7 @@ export interface Stop {
parent_station: CRS;
stop_timezone: string;
wheelchair_boarding: 0 | 1 | 2;
platform_code: string;
}

export type CRS = string;
export type TIPLOC = string;
export type CRS = string;
9 changes: 3 additions & 6 deletions src/gtfs/file/StopTime.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@

import {CRS} from "./Stop";

export interface StopTime {
trip_id: number;
arrival_time: string;
departure_time: string;
stop_id: CRS;
stop_id: StopPlatform;
stop_sequence: number;
stop_headsign: Platform;
stop_headsign: null;
pickup_type: 0 | 1 | 2 | 3;
drop_off_type: 0 | 1 | 2 | 3;
shape_dist_traveled: null;
timepoint: 0 | 1;
}

export type Platform = string;
export type StopPlatform = string;
4 changes: 2 additions & 2 deletions src/gtfs/file/Trip.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {RSID, TUID} from "../native/OverlayRecord";
import {RSID} from "../native/OverlayRecord";

export interface Trip {
route_id: number;
service_id: string;
trip_id: number;
trip_headsign: TUID;
trip_headsign: null;
trip_short_name: RSID;
direction_id: 0 | 1;
wheelchair_accessible: 0 | 1 | 2;
Expand Down
24 changes: 14 additions & 10 deletions src/gtfs/native/Schedule.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

import {StopTime} from "../file/StopTime";
import {CIFRepository} from '../repository/CIFRepository';
import {OverlapType, ScheduleCalendar} from "./ScheduleCalendar";
import {Trip} from "../file/Trip";
import {Route, RouteType} from "../file/Route";
Expand All @@ -17,7 +18,7 @@ export class Schedule implements OverlayRecord {
public readonly id: number,
public readonly stopTimes: StopTime[],
public readonly tuid: TUID,
public readonly rsid: RSID,
public readonly rsid: RSID | null,
public readonly calendar: ScheduleCalendar,
public readonly mode: RouteType,
public readonly operator: AgencyID | null,
Expand All @@ -27,11 +28,11 @@ export class Schedule implements OverlayRecord {
) {}

public get origin(): CRS {
return this.stopTimes[0].stop_id;
return this.stopTimes[0].stop_id.substr(0, 3);
}

public get destination(): CRS {
return this.stopTimes[this.stopTimes.length - 1].stop_id;
return this.stopTimes[this.stopTimes.length - 1].stop_id.substr(0, 3);
}

public get hash(): string {
Expand Down Expand Up @@ -64,8 +65,8 @@ export class Schedule implements OverlayRecord {
route_id: routeId,
service_id: serviceId,
trip_id: this.id,
trip_headsign: this.tuid,
trip_short_name: this.rsid,
trip_headsign: null,
trip_short_name: this.rsid ?? this.tuid,
direction_id: 0,
wheelchair_accessible: 1,
bikes_allowed: 0
Expand All @@ -75,17 +76,20 @@ export class Schedule implements OverlayRecord {
/**
* Convert to GTFS Route
*/
public toRoute(): Route {
public async toRoute(cifRepository : CIFRepository): Promise<Route> {
const stop_data = await cifRepository.getStops();
const origin = stop_data.find(stop => stop.stop_code === this.origin)?.stop_name ?? this.origin;
const destination = stop_data.find(stop => stop.stop_code === this.destination)?.stop_name ?? this.destination;
return {
route_id: this.id,
agency_id: this.operator || "ZZ",
route_short_name: `${this.operator || "Z"}:${this.origin}->${this.destination}:${this.mode}`,
route_long_name: `${this.operator || "Z"} ${this.modeDescription.toLowerCase()} service from ${this.origin} to ${this.destination}`,
route_short_name: this.rsid?.substr(0, 6) ?? this.tuid,
route_long_name: `${origin} ${destination}`,
route_type: this.mode,
route_text_color: null,
route_color: null,
route_url: null,
route_desc: [this.modeDescription, this.classDescription, this.reservationDescription].join(". ")
route_desc: `${this.modeDescription} service from ${origin} to ${destination}, ${this.classDescription}, ${this.reservationDescription}`,
};
}

Expand Down Expand Up @@ -118,7 +122,7 @@ export class Schedule implements OverlayRecord {
}

public stopAt(location: CRS): StopTime | undefined {
return <StopTime>this.stopTimes.find(s => s.stop_id === location);
return <StopTime>this.stopTimes.find(s => s.stop_id.substr(0, 3) === location);
}

}
Expand Down
108 changes: 92 additions & 16 deletions src/gtfs/repository/CIFRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class CIFRepository {
constructor(
private readonly db: DatabaseConnection,
private readonly stream,
private readonly stationCoordinates: StationCoordinates
public stationCoordinates: StationCoordinates
) {}

/**
Expand All @@ -43,27 +43,101 @@ export class CIFRepository {
* Return all the stops with some configurable long/lat applied
*/
public async getStops(): Promise<Stop[]> {
const [results] = await this.db.query<Stop[]>(`
SELECT
crs_code AS stop_id,
tiploc_code AS stop_code,
station_name AS stop_name,
cate_interchange_status AS stop_desc,
return this.stops;
}

/*
Every passenger station in the National Rail network has a CRS code, however, some multi-part stations
may have additional minor CRS code specifying a part of it. For example,
STP (London St Pancras) has a minor code SPL representing the Thameslink platforms, while the main code represents the terminal platforms;
PAD (London Paddington) has a minor code PDX representing the Crossrail platforms, while the main code represents the terminal platforms.

In the database, such stations will have multiple entries, one for each TIPLOC code, where the one with cate_interchange_status <> 9 is the main entry
which the CRS code (crs_code) and the minor CRS code (crs_reference_code) are the same.

Using St Pancras as an example, there are 4 entries listed in the station database:
TIPLOC CRS minor CRS main entry? MCT location
------------------------------------------------------------------------------------
STPX STP STP * 15 Midland Main Line platforms
STPADOM STP STP 15 Domestic High Speed platforms
STPXBOX STP SPL 15 Thameslink platforms
STPANCI SPX SPX * 35 International platforms

In the National Rail systems, the international station is treated as a distinct station from the domestic one,
however the 3 remaining parts (Midland, Thameslink and High Speed Domestic) are the same station.

The stop list will return one entry for each station (not its constituent parts) as a GTFS station identified by its main CRS code,
and one entry for each platform as a GTFS stop identified by its minor CRS code and the platform number, associated to the station with the main CRS code.
*/
private stops : Promise<Stop[]> = (async () => {
const [results] : [Stop[]] = await this.db.query<Stop[]>(`
SELECT -- select all the physical stations
crs_code AS stop_id, -- using the main CRS code as both the id
crs_code AS stop_code, -- and the public facing code
MIN(station_name) AS stop_name,
MIN(cate_interchange_status) AS stop_desc,
0 AS stop_lat,
0 AS stop_lon,
NULL AS zone_id,
NULL AS stop_url,
NULL AS location_type,
1 AS location_type,
NULL AS parent_station,
IF(POSITION("(CIE" IN station_name), "Europe/Dublin", "Europe/London") AS stop_timezone,
0 AS wheelchair_boarding
FROM physical_station WHERE crs_code IS NOT NULL
IF(POSITION('(CIE' IN MIN(station_name)), 'Europe/Dublin', 'Europe/London') AS stop_timezone,
0 AS wheelchair_boarding,
null AS platform_code
FROM physical_station WHERE crs_code IS NOT NULL AND cate_interchange_status <> 9 -- from the main part of the station
GROUP BY crs_code
UNION SELECT -- and select all the platforms where scheduled services call at
CONCAT(crs_reference_code, '_', IFNULL(platform, '')) AS stop_id, -- using the minor CRS code and the platform number as the id
crs_reference_code AS stop_code, -- and the minor CRS code as the public facing code
IF(ISNULL(platform), MIN(station_name), CONCAT(MIN(station_name), ' (platform ', platform, ')')) AS stop_name,
MIN(cate_interchange_status) AS stop_desc,
0 AS stop_lat,
0 AS stop_lon,
NULL AS zone_id,
NULL AS stop_url,
0 AS location_type,
crs_code AS parent_station,
IF(POSITION('(CIE' IN MIN(station_name)), 'Europe/Dublin', 'Europe/London') AS stop_timezone,
0 AS wheelchair_boarding,
platform AS platform_code
FROM physical_station
INNER JOIN (
SELECT distinct location, platform FROM stop_time
) platforms on physical_station.tiploc_code = platforms.location
WHERE crs_code IS NOT NULL
GROUP BY crs_code, platform
`);

// overlay the long and latitude values from configuration
return results.map(stop => Object.assign(stop, this.stationCoordinates[stop.stop_id]));
}
return results.map(stop => {
const station_data = this.stationCoordinates[stop.stop_code] ?? this.stationCoordinates[stop.parent_station];
if (stop.stop_id.includes('_')) {
const parts = stop.stop_id.split('_');
if (parts[1] !== '') {
const platform_data = (station_data?.platforms ?? [])[parts[1]];
if (platform_data !== undefined) {
// use platform data if available
return Object.assign(stop, platform_data);
}
}
if (station_data !== undefined) {
// otherwise inherit station data
const result = Object.assign(stop, station_data);
delete result['platforms'];
result.stop_name += parts[1] === '' ? '' : ` (platform ${parts[1]})`;
result.location_type = 0;
return result;
} else {
return stop;
}
} else {
const result = Object.assign(stop, this.stationCoordinates[stop.stop_code])
delete result['platforms'];
return result;
}
});
})();

/**
* Return the schedules and z trains. These queries probably require some explanation:
Expand All @@ -83,7 +157,7 @@ export class CIFRepository {
SELECT
schedule.id AS id, train_uid, retail_train_id, runs_from, runs_to,
monday, tuesday, wednesday, thursday, friday, saturday, sunday,
crs_code, stp_indicator, public_arrival_time, public_departure_time,
crs_reference_code as crs_code, stp_indicator, public_arrival_time, public_departure_time,
IF(train_status="S", "SS", train_category) AS train_category,
scheduled_arrival_time AS scheduled_arrival_time,
scheduled_departure_time AS scheduled_departure_time,
Expand All @@ -103,7 +177,7 @@ export class CIFRepository {
`)),
scheduleBuilder.loadSchedules(this.stream.query(`
SELECT
${lastSchedule.id} + z_schedule.id AS id, train_uid, null, runs_from, runs_to,
${lastSchedule.id} + z_schedule.id AS id, train_uid, null as retail_train_id, runs_from, runs_to,
monday, tuesday, wednesday, thursday, friday, saturday, sunday,
stp_indicator, location AS crs_code, train_category,
public_arrival_time, public_departure_time, scheduled_arrival_time, scheduled_departure_time,
Expand Down Expand Up @@ -252,7 +326,9 @@ export type StationCoordinates = {
stop_lat: number,
stop_lon: number,
stop_name: string,
wheelchair_boarding: 0 | 1 | 2
location_type?: number,
wheelchair_boarding: 0 | 1 | 2,
platforms?: {[key : string] : StationCoordinates}
}
};

Expand Down
4 changes: 2 additions & 2 deletions src/gtfs/repository/ScheduleBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ export class ScheduleBuilder {
trip_id: row.id,
arrival_time: (arrivalTime || departureTime),
departure_time: (departureTime || arrivalTime),
stop_id: row.crs_code,
stop_id: `${row.crs_code}_${row.platform ?? ''}`,
stop_sequence: stopId,
stop_headsign: row.platform,
stop_headsign: null,
pickup_type: coordinatedDropOff || pickup,
drop_off_type: coordinatedDropOff || dropOff,
shape_dist_traveled: null,
Expand Down