@transportme/load-ptv-gtfs 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/unit-test.yml +46 -0
- package/lib/constants.mjs +18 -0
- package/lib/gtfs-package/PTVGTFS.mjs +96 -0
- package/lib/gtfs-parser/GTFSAgency.mjs +15 -0
- package/lib/gtfs-parser/GTFSCalendar.mjs +63 -0
- package/lib/gtfs-parser/GTFSCalendarDate.mjs +15 -0
- package/lib/gtfs-parser/GTFSRoute.mjs +106 -0
- package/lib/gtfs-parser/GTFSShape.mjs +39 -0
- package/lib/gtfs-parser/GTFSStop.mjs +212 -0
- package/lib/gtfs-parser/GTFSStopTime.mjs +30 -0
- package/lib/gtfs-parser/GTFSTrip.mjs +169 -0
- package/lib/gtfs-parser/readers/GTFSAgencyReader.mjs +27 -0
- package/lib/gtfs-parser/readers/GTFSCalendarDatesReader.mjs +25 -0
- package/lib/gtfs-parser/readers/GTFSCalendarReader.mjs +63 -0
- package/lib/gtfs-parser/readers/GTFSReader.mjs +48 -0
- package/lib/gtfs-parser/readers/GTFSRouteReader.mjs +35 -0
- package/lib/gtfs-parser/readers/GTFSShapeReader.mjs +75 -0
- package/lib/gtfs-parser/readers/GTFSStopTimesReader.mjs +85 -0
- package/lib/gtfs-parser/readers/GTFSStopsReader.mjs +36 -0
- package/lib/gtfs-parser/readers/GTFSTripReader.mjs +61 -0
- package/lib/gtfs-parser/readers/line-reader.mjs +112 -0
- package/lib/loader/RouteLoader.mjs +101 -0
- package/lib/loader/ShapeLoader.mjs +86 -0
- package/lib/loader/StopsLoader.mjs +139 -0
- package/lib/loader/TripLoader.mjs +187 -0
- package/lib/post-processing/merge-stops.mjs +85 -0
- package/lib/post-processing/set-route-stops.mjs +73 -0
- package/lib/post-processing/set-stop-services.mjs +64 -0
- package/lib/utils.mjs +16 -0
- package/package.json +27 -0
- package/test/gtfs-calendar.test.mjs +182 -0
- package/test/gtfs-route-loader.test.mjs +190 -0
- package/test/gtfs-routes.test.mjs +77 -0
- package/test/gtfs-shapes-loader.test.mjs +105 -0
- package/test/gtfs-shapes.test.mjs +23 -0
- package/test/gtfs-stop-loader-metro.test.mjs +49 -0
- package/test/gtfs-stop-loader.test.mjs +223 -0
- package/test/gtfs-stop-times.test.mjs +37 -0
- package/test/gtfs-stops.test.mjs +290 -0
- package/test/gtfs-trip-loader.test.mjs +142 -0
- package/test/gtfs-trips.test.mjs +109 -0
- package/test/line-reader.test.mjs +94 -0
- package/test/merge-stops.test.mjs +376 -0
- package/test/ptv-gtfs.test.mjs +106 -0
- package/test/sample-data/calendar/calendar.txt +34 -0
- package/test/sample-data/calendar/calendar_dates.txt +26 -0
- package/test/sample-data/gtfs-splitting/force_unique_stops_bus.txt +7 -0
- package/test/sample-data/gtfs-splitting/force_unique_stops_tram.txt +3 -0
- package/test/sample-data/gtfs-splitting/line_reader_test.txt +8 -0
- package/test/sample-data/gtfs-splitting/name_overrides.txt +4 -0
- package/test/sample-data/gtfs-splitting/nov_2024_train_data.txt +6 -0
- package/test/sample-data/gtfs-splitting/sample_stops.txt +20 -0
- package/test/sample-data/gtfs-splitting/special_chars_stops_csv.txt +6 -0
- package/test/sample-data/gtfs-splitting/stops_without_secondary.txt +6 -0
- package/test/sample-data/gtfs-unzipping/gtfs.zip +0 -0
- package/test/sample-data/one_line.csv +2 -0
- package/test/sample-data/routes/agency.txt +11 -0
- package/test/sample-data/routes/metro_bus_routes.txt +4 -0
- package/test/sample-data/routes/metro_lines.txt +5 -0
- package/test/sample-data/routes/regional_bus_routes.txt +7 -0
- package/test/sample-data/routes/vline.txt +7 -0
- package/test/sample-data/routes/west_gipp_transit.txt +20 -0
- package/test/sample-data/shapes/shapes.txt +89 -0
- package/test/sample-data/shapes/stop_times.txt +9 -0
- package/test/sample-data/shapes/stops.txt +4 -0
- package/test/sample-data/shapes/trips.txt +4 -0
- package/test/sample-data/stop-data/name-overrides.json +25 -0
- package/test/sample-data/stop-data/unique-stops.json +1 -0
- package/test/sample-data/suburbs.json +77 -0
- package/test/sample-data/trips/bus_trips.txt +5 -0
- package/test/sample-data/trips/stop_times.txt +29 -0
- package/test/sample-data/trips/stop_times_new.txt +8 -0
- package/test/sample-data/trips/stops.txt +8 -0
- package/test/sample-data/trips/trips.txt +5 -0
- package/test/sample-data/trips/trips_new.txt +2 -0
- package/test/set-stop-services.test.mjs +69 -0
- package/test/utils-test.mjs +26 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
|
3
|
+
|
|
4
|
+
name: Node.js CI
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: [ "master" ]
|
|
9
|
+
pull_request:
|
|
10
|
+
branches: [ "master" ]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
submodules: recursive
|
|
21
|
+
|
|
22
|
+
- name: Cache node modules
|
|
23
|
+
id: cache-npm
|
|
24
|
+
uses: actions/cache@v4
|
|
25
|
+
env:
|
|
26
|
+
cache-name: cache-node-modules
|
|
27
|
+
with:
|
|
28
|
+
# npm cache files are stored in `~/.npm` on Linux/macOS
|
|
29
|
+
path: ~/.npm
|
|
30
|
+
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
|
31
|
+
restore-keys: |
|
|
32
|
+
${{ runner.os }}-build-${{ env.cache-name }}-
|
|
33
|
+
${{ runner.os }}-build-
|
|
34
|
+
${{ runner.os }}-
|
|
35
|
+
|
|
36
|
+
- name: Use Node.js 20.x
|
|
37
|
+
uses: actions/setup-node@v4
|
|
38
|
+
with:
|
|
39
|
+
node-version: '20.x'
|
|
40
|
+
cache: 'npm'
|
|
41
|
+
|
|
42
|
+
- name: Install dependencies
|
|
43
|
+
run: npm install
|
|
44
|
+
|
|
45
|
+
- name: Run test
|
|
46
|
+
run: npm test
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { GTFS_CONSTANTS } from '@transportme/transportvic-utils'
|
|
2
|
+
const { TRANSIT_MODES, GTFS_MODES } = GTFS_CONSTANTS
|
|
3
|
+
|
|
4
|
+
const TIMEZONES = {
|
|
5
|
+
melbourne: 'Australia/Melbourne'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const CALENDAR_DATES = {
|
|
9
|
+
ADDED: '1',
|
|
10
|
+
REMOVED: '2'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
TRANSIT_MODES,
|
|
15
|
+
TIMEZONES,
|
|
16
|
+
CALENDAR_DATES,
|
|
17
|
+
GTFS_MODES
|
|
18
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fetch from 'node-fetch'
|
|
2
|
+
import AdmZip from 'adm-zip'
|
|
3
|
+
import { pipeline } from 'stream/promises'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { createWriteStream } from 'fs'
|
|
6
|
+
import { unlink } from 'fs/promises'
|
|
7
|
+
|
|
8
|
+
export default class PTVGTFS {
|
|
9
|
+
|
|
10
|
+
#url
|
|
11
|
+
#destinationFolder
|
|
12
|
+
#gtfsFile
|
|
13
|
+
#headResponse
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Constructs a new instance of a PTVGTFS class representing a GTFS release.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} url The URL pointing to the GTFS resource
|
|
19
|
+
*/
|
|
20
|
+
constructor(url) {
|
|
21
|
+
this.#url = url
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gets when the GTFS file was released.
|
|
26
|
+
*
|
|
27
|
+
* @returns {Promise<Date>} The date/time at which the GTFS file was released.
|
|
28
|
+
*/
|
|
29
|
+
async getPublishedDate() {
|
|
30
|
+
this.#headResponse = await fetch(this.#url, {
|
|
31
|
+
method: 'HEAD'
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
let lastModified = this.#headResponse.headers.get('last-modified')
|
|
35
|
+
return new Date(lastModified)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Downloads the PTV GTFS file to the specified folder. It will be given the name `gtfs.zip`.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} destinationFolder The folder to save the GTFS archive to.
|
|
42
|
+
*/
|
|
43
|
+
async download(destinationFolder) {
|
|
44
|
+
this.#destinationFolder = destinationFolder
|
|
45
|
+
let response = await fetch(this.#url)
|
|
46
|
+
|
|
47
|
+
this.#gtfsFile = path.join(destinationFolder, 'gtfs.zip')
|
|
48
|
+
let outputStream = createWriteStream(this.#gtfsFile)
|
|
49
|
+
|
|
50
|
+
await pipeline(response.body, outputStream)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#getModeFolder(mode) {
|
|
54
|
+
return path.join(this.#destinationFolder, mode)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#getModeFile(mode) {
|
|
58
|
+
let modeFolder = this.#getModeFolder(mode)
|
|
59
|
+
return path.join(modeFolder, 'google_transit.zip')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async #unzipMode(mode) {
|
|
63
|
+
let modeFolder = this.#getModeFolder(mode)
|
|
64
|
+
let modeFile = this.#getModeFile(mode)
|
|
65
|
+
let zip = new AdmZip(modeFile)
|
|
66
|
+
|
|
67
|
+
await new Promise(resolve => zip.extractAllToAsync(modeFolder, true, resolve))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Unzips the PTV GTFS archive into its individual transit mode folders, then unzips the invididual mode archives as well.
|
|
72
|
+
*/
|
|
73
|
+
async unzip() {
|
|
74
|
+
if (!this.#destinationFolder) throw new Error('Need to download GTFS file first!')
|
|
75
|
+
|
|
76
|
+
let zip = new AdmZip(this.#gtfsFile)
|
|
77
|
+
await new Promise(resolve => zip.extractAllToAsync(this.#destinationFolder, true, resolve))
|
|
78
|
+
|
|
79
|
+
for (let mode = 1; mode <= 11; mode++) {
|
|
80
|
+
if (mode === 9) continue
|
|
81
|
+
await this.#unzipMode(mode.toString())
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Cleans up the zip files only.
|
|
87
|
+
*/
|
|
88
|
+
async cleanup() {
|
|
89
|
+
await unlink(this.#gtfsFile)
|
|
90
|
+
|
|
91
|
+
for (let mode = 1; mode <= 11; mode++) {
|
|
92
|
+
if (mode === 9) continue
|
|
93
|
+
await unlink(this.#getModeFile(mode.toString()))
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { CALENDAR_DATES } from '../constants.mjs'
|
|
2
|
+
import { parseDate, toGTFSDate } from '../utils.mjs'
|
|
3
|
+
import GTFSCalendarDate from './GTFSCalendarDate.mjs'
|
|
4
|
+
|
|
5
|
+
export default class GTFSCalendar {
|
|
6
|
+
|
|
7
|
+
id
|
|
8
|
+
daysOfWeek
|
|
9
|
+
startDay
|
|
10
|
+
endDay
|
|
11
|
+
|
|
12
|
+
#calendarRemovals = []
|
|
13
|
+
#operationDays = []
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Constructor to create a new GTFS Calendar object.
|
|
17
|
+
*
|
|
18
|
+
* Note that `startDay` and `endDay` are both inclusive.
|
|
19
|
+
*
|
|
20
|
+
* @param {Object} param0 The data as provided by the calendar.txt CSV entry
|
|
21
|
+
* @param {string} param0.id The calendar ID
|
|
22
|
+
* @param {string[]} param0.daysOfWeek An array containing the days of the week. This should be 1 to include the day in the interval and 0 to exclude it. Note that it starts on a Monday.
|
|
23
|
+
* @param {string} param0.startDay The start day of the calendar interval (inclusive).
|
|
24
|
+
* @param {string} param0.endDay The end day of the calendar interval (inclusive).
|
|
25
|
+
* @param {GTFSCalendarDate[]} [calendarDates=[]] A list of calendar date exceptions
|
|
26
|
+
*/
|
|
27
|
+
constructor({ id, daysOfWeek, startDay, endDay }, calendarDates = []) {
|
|
28
|
+
this.id = id
|
|
29
|
+
this.daysOfWeek = daysOfWeek.map(day => day === '1')
|
|
30
|
+
this.startDay = parseDate(startDay)
|
|
31
|
+
this.endDay = parseDate(endDay)
|
|
32
|
+
|
|
33
|
+
for (let date of calendarDates) {
|
|
34
|
+
if (date.type === CALENDAR_DATES.ADDED) this.#operationDays.push(date.date)
|
|
35
|
+
else if (date.type === CALENDAR_DATES.REMOVED) this.#calendarRemovals.push(date.date)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.#findOperationDays()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#findOperationDays() {
|
|
42
|
+
let intervalSize = this.endDay.diff(this.startDay, 'day').get('day')
|
|
43
|
+
|
|
44
|
+
for (let offset = 0; offset <= intervalSize; offset++) {
|
|
45
|
+
let intervalDay = this.startDay.plus({ days: offset })
|
|
46
|
+
let dayOfWeek = intervalDay.weekday - 1
|
|
47
|
+
|
|
48
|
+
let hasRemoval = this.#calendarRemovals.some(removalDate => removalDate.equals(intervalDay))
|
|
49
|
+
if (hasRemoval) continue
|
|
50
|
+
|
|
51
|
+
if (this.daysOfWeek[dayOfWeek]) this.#operationDays.push(intervalDay)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Gets the operation days, taking into account any calendar date exceptions
|
|
57
|
+
* @returns {string[]} A string array of GTFS formatted dates
|
|
58
|
+
*/
|
|
59
|
+
getOperationDays() {
|
|
60
|
+
return this.#operationDays.sort((a, b) => a - b).map(day => toGTFSDate(day)).filter((date, i, arr) => i === 0 || arr[i - 1] != date)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { TRANSIT_MODES } from '../constants.mjs'
|
|
2
|
+
|
|
3
|
+
export default class GTFSRoute {
|
|
4
|
+
|
|
5
|
+
originalRouteGTFSID
|
|
6
|
+
routeGTFSID
|
|
7
|
+
agencyID
|
|
8
|
+
operator
|
|
9
|
+
routeNumber
|
|
10
|
+
routeName
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} param0 The route data
|
|
15
|
+
* @param {string} param0.routeGTFSID The route's GTFS ID
|
|
16
|
+
* @param {string} param0.agencyID The route's agency ID
|
|
17
|
+
* @param {string} param0.routeNumber The route's number. Blank if it does not exist
|
|
18
|
+
* @param {string} param0.routeName The route's name
|
|
19
|
+
*/
|
|
20
|
+
constructor({ routeGTFSID, agencyID, routeNumber, routeName }) {
|
|
21
|
+
this.originalRouteGTFSID = routeGTFSID
|
|
22
|
+
this.routeGTFSID = this.parseRouteID(routeGTFSID)
|
|
23
|
+
this.agencyID = agencyID
|
|
24
|
+
this.routeName = routeName
|
|
25
|
+
|
|
26
|
+
if (routeNumber !== null && routeNumber.length === 0) routeNumber = null
|
|
27
|
+
this.routeNumber = routeNumber
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parses the route GTFS ID into a standardises format.
|
|
32
|
+
* @param {string} routeGTFSID The route GTFS ID
|
|
33
|
+
* @returns A formatted route GTFS ID
|
|
34
|
+
*/
|
|
35
|
+
parseRouteID(routeGTFSID) {
|
|
36
|
+
let [ _, mode, id ] = routeGTFSID.match(/^(\d+)\-(\w+)/)
|
|
37
|
+
|
|
38
|
+
return `${mode}-${`000${id}`.slice(-3)}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Checks if the GTFSRoute (or subclass) is able to process the given route data
|
|
43
|
+
* @param {Object} data The route being checked
|
|
44
|
+
* @param {string} mode The route mode
|
|
45
|
+
* @returns true if the class is able to process the route
|
|
46
|
+
*/
|
|
47
|
+
static canProcess(routeGTFSID, mode) {
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates a new GTFSRoute with the route data that was passed in
|
|
54
|
+
* @param {Object} data The route data being created
|
|
55
|
+
* @param {string} mode The transit mode
|
|
56
|
+
* @returns {GTFSRoute} A route instance representing the data being passed in
|
|
57
|
+
*/
|
|
58
|
+
static create(data, mode) {
|
|
59
|
+
let { routeGTFSID } = data
|
|
60
|
+
let routeTypes = [ MetroGTFSRoute, SmartrakGTFSRoute, GTFSRoute ]
|
|
61
|
+
for (let type of routeTypes) {
|
|
62
|
+
if (type.canProcess(routeGTFSID, mode)) return new type(data)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Used to represent a Smartrak bus route.
|
|
69
|
+
*
|
|
70
|
+
* Route IDs are of the form DEPOT-ROUTE-aus-ROUTE_VARIANT. Note that the route number could contain letters or even dashes
|
|
71
|
+
*
|
|
72
|
+
* Example: 29-737-aus-1
|
|
73
|
+
* Example: 29-695-F-aus-1
|
|
74
|
+
* Example: 53-MFR-aus-1
|
|
75
|
+
*/
|
|
76
|
+
class SmartrakGTFSRoute extends GTFSRoute {
|
|
77
|
+
|
|
78
|
+
parseRouteID(routeGTFSID) {
|
|
79
|
+
let [ _, routeNumber ] = routeGTFSID.match(/^\d+\-(\w+)/)
|
|
80
|
+
return `4-${routeNumber}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static canProcess(routeGTFSID, mode) {
|
|
84
|
+
return routeGTFSID.includes('-aus-')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Used to represent a Metro train route
|
|
91
|
+
*
|
|
92
|
+
* Used for a technicality where the route name (eg Pakenham) is stored in the route number instead.
|
|
93
|
+
*/
|
|
94
|
+
class MetroGTFSRoute extends GTFSRoute {
|
|
95
|
+
|
|
96
|
+
constructor(data) {
|
|
97
|
+
super(data)
|
|
98
|
+
this.routeName = this.routeNumber
|
|
99
|
+
this.routeNumber = null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static canProcess(routeGTFSID, mode) {
|
|
103
|
+
return mode === TRANSIT_MODES.metroTrain
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export class GTFSShapePoint {
|
|
2
|
+
|
|
3
|
+
#latitude
|
|
4
|
+
#longitude
|
|
5
|
+
distance
|
|
6
|
+
|
|
7
|
+
get coordinates() {
|
|
8
|
+
return [
|
|
9
|
+
this.#longitude, this.#latitude
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
constructor({ latitude, longitude, distance }) {
|
|
14
|
+
this.#latitude = latitude
|
|
15
|
+
this.#longitude = longitude
|
|
16
|
+
|
|
17
|
+
this.distance = distance
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class GTFSShape {
|
|
23
|
+
|
|
24
|
+
shapeID
|
|
25
|
+
points
|
|
26
|
+
|
|
27
|
+
shapeLength
|
|
28
|
+
hash
|
|
29
|
+
|
|
30
|
+
constructor(shapeID, points) {
|
|
31
|
+
this.shapeID = shapeID
|
|
32
|
+
this.points = points
|
|
33
|
+
|
|
34
|
+
let lastPoint = points[points.length - 1]
|
|
35
|
+
this.shapeLength = lastPoint.distance
|
|
36
|
+
this.hash = `${this.shapeLength}-${points[0].coordinates.join(',')}-${lastPoint.coordinates.join(',')}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { booleanPointInPolygon, intersect } from '@turf/turf'
|
|
2
|
+
import { STOP_UTILS } from '@transportme/transportvic-utils'
|
|
3
|
+
|
|
4
|
+
const { processName, getPrimaryStopName, getSecondaryStopName } = STOP_UTILS
|
|
5
|
+
|
|
6
|
+
export class GTFSStop {
|
|
7
|
+
|
|
8
|
+
stopGTFSID
|
|
9
|
+
originalName
|
|
10
|
+
rawStopName
|
|
11
|
+
fullStopName
|
|
12
|
+
stopNumber
|
|
13
|
+
location
|
|
14
|
+
suburb
|
|
15
|
+
|
|
16
|
+
#isInterstate = false
|
|
17
|
+
#suburbInFrontMatch
|
|
18
|
+
|
|
19
|
+
constructor({ stopGTFSID, stopName, stopLat, stopLon }, suburbs) {
|
|
20
|
+
this.stopGTFSID = stopGTFSID
|
|
21
|
+
this.originalName = stopName
|
|
22
|
+
this.location = {
|
|
23
|
+
type: 'Point',
|
|
24
|
+
coordinates: [parseFloat(stopLon), parseFloat(stopLat)]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.#setSuburb(suburbs)
|
|
28
|
+
this.#setStopNumber()
|
|
29
|
+
|
|
30
|
+
this.fullStopName = this.getFullStopName()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Checks if a stop name requires the suburb to be appended
|
|
35
|
+
*
|
|
36
|
+
* @returns {boolean} True if the suburb is required
|
|
37
|
+
*/
|
|
38
|
+
requiresSuburb() {
|
|
39
|
+
let { originalName } = this
|
|
40
|
+
return originalName[originalName.length - 1] !== ')'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getSuburbFromLocation(suburbs) {
|
|
44
|
+
// Kinda inefficient but it's not expected this will be used very often unless PTV breaks the whole dataset (again)
|
|
45
|
+
let suburb = suburbs.features.find(suburb => booleanPointInPolygon(this.location, suburb))
|
|
46
|
+
if (suburb) return this.#expandSuburb(suburb.properties.LOC_NAME)
|
|
47
|
+
|
|
48
|
+
this.#isInterstate = true
|
|
49
|
+
return 'Interstate'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getSuburbState(stateIndex) {
|
|
53
|
+
let bracketContent = this.originalName.slice(stateIndex + 1, -2)
|
|
54
|
+
if (!bracketContent.includes('-')) return bracketContent
|
|
55
|
+
|
|
56
|
+
let dashIndex = bracketContent.indexOf('-')
|
|
57
|
+
return bracketContent.slice(dashIndex + 2)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getInterstateSuburb() {
|
|
61
|
+
let stateIndex = this.originalName.lastIndexOf('(')
|
|
62
|
+
let state = this.getSuburbState(stateIndex)
|
|
63
|
+
let suburbIndex = this.originalName.lastIndexOf('(', stateIndex - 1)
|
|
64
|
+
let suburb = this.originalName.slice(suburbIndex + 1, stateIndex - 1)
|
|
65
|
+
|
|
66
|
+
return `${this.#expandSuburb(suburb)}, ${state}`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#expandSuburb(suburb) {
|
|
70
|
+
return suburb.replace(/^St /, 'St. ')
|
|
71
|
+
.replace(/^Mt /, 'Mount ')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
suburbIsInFront() {
|
|
75
|
+
this.#suburbInFrontMatch = this.originalName.match(/^([\w ]+), (.+)/)
|
|
76
|
+
return !this.originalName.endsWith(')') && !!this.#suburbInFrontMatch
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getSuburbFromFrontOfName() {
|
|
80
|
+
return this.#suburbInFrontMatch[1]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getSuburbFromName() {
|
|
84
|
+
if (this.suburbIsInFront()) return this.getSuburbFromFrontOfName()
|
|
85
|
+
|
|
86
|
+
this.#isInterstate = this.originalName.endsWith('))')
|
|
87
|
+
if (this.#isInterstate) return this.getInterstateSuburb()
|
|
88
|
+
|
|
89
|
+
let suburbIndex = this.originalName.lastIndexOf('(')
|
|
90
|
+
return this.#expandSuburb(this.originalName.slice(suburbIndex + 1, -1))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#setSuburb(suburbs) {
|
|
94
|
+
if (this.requiresSuburb()) {
|
|
95
|
+
this.suburb = this.getSuburbFromLocation(suburbs)
|
|
96
|
+
} else {
|
|
97
|
+
this.suburb = this.getSuburbFromName()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getStopNameWithoutSuburb() {
|
|
102
|
+
if (this.suburbIsInFront()) return this.#suburbInFrontMatch[2]
|
|
103
|
+
else if (this.requiresSuburb()) return this.originalName
|
|
104
|
+
else {
|
|
105
|
+
if (!this.#isInterstate) return this.originalName.slice(0, this.originalName.lastIndexOf('(') - 1)
|
|
106
|
+
|
|
107
|
+
let stateIndex = this.originalName.lastIndexOf('(')
|
|
108
|
+
let suburbIndex = this.originalName.lastIndexOf('(', stateIndex - 1)
|
|
109
|
+
|
|
110
|
+
return this.originalName.slice(0, suburbIndex - 1)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
matchStopNumber() {
|
|
115
|
+
let stopNumberParts
|
|
116
|
+
let stopName = this.getStopNameWithoutSuburb()
|
|
117
|
+
|
|
118
|
+
if (stopName.match(/^\d+\-\d+ \w/)) {
|
|
119
|
+
// Stops in the form 123-456 ABC Road - not a stop number!
|
|
120
|
+
return { stopNumber: null, stopName: stopName }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (stopNumberParts = stopName.match(/^(D?\d+[A-Za-z]?)-(.+)$/)) {
|
|
124
|
+
// Stops in the form 123-ABC Road
|
|
125
|
+
return {
|
|
126
|
+
stopNumber: stopNumberParts[1].toUpperCase(),
|
|
127
|
+
stopName: stopNumberParts[2]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (stopNumberParts = stopName.match(/^(.*) - Stop (D?\d*[A-Za-z]?)$/)) {
|
|
132
|
+
// Stops in the form ABC Road - Stop 123
|
|
133
|
+
return {
|
|
134
|
+
stopNumber: stopNumberParts[2].toUpperCase(),
|
|
135
|
+
stopName: stopNumberParts[1]
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { stopNumber: null, stopName: stopName }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#setStopNumber() {
|
|
143
|
+
let { stopNumber, stopName } = this.matchStopNumber()
|
|
144
|
+
this.stopNumber = stopNumber
|
|
145
|
+
this.rawStopName = stopName
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getPrimaryStopName() {
|
|
149
|
+
return getPrimaryStopName(this.rawStopName)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
getSecondaryStopName() {
|
|
153
|
+
return getSecondaryStopName(this.rawStopName)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getFullStopName() {
|
|
157
|
+
let primaryName = this.getPrimaryStopName()
|
|
158
|
+
let secondaryName = this.getSecondaryStopName()
|
|
159
|
+
|
|
160
|
+
let fullStopName = processName(primaryName)
|
|
161
|
+
if (secondaryName.length) fullStopName += `/${processName(secondaryName)}`
|
|
162
|
+
|
|
163
|
+
return fullStopName
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
getBayData(mode) {
|
|
167
|
+
return {
|
|
168
|
+
originalName: this.originalName,
|
|
169
|
+
fullStopName: this.fullStopName,
|
|
170
|
+
stopGTFSID: this.stopGTFSID,
|
|
171
|
+
location: this.location,
|
|
172
|
+
stopNumber: this.stopNumber,
|
|
173
|
+
mode,
|
|
174
|
+
suburb: this.suburb,
|
|
175
|
+
services: [],
|
|
176
|
+
screenServices: []
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
static canProcess(data) {
|
|
181
|
+
return true
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
static create(data, suburbs) {
|
|
185
|
+
let types = [ GTFSPlatformStop, GTFSStop ]
|
|
186
|
+
for (let type of types) {
|
|
187
|
+
if (type.canProcess(data)) return new type(data, suburbs)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export class GTFSPlatformStop extends GTFSStop {
|
|
193
|
+
|
|
194
|
+
platform
|
|
195
|
+
|
|
196
|
+
constructor(data, suburbs) {
|
|
197
|
+
super(data, suburbs)
|
|
198
|
+
this.platform = data.platform
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
getBayData(mode) {
|
|
202
|
+
return {
|
|
203
|
+
...super.getBayData(mode),
|
|
204
|
+
platform: this.platform
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
static canProcess(data) {
|
|
209
|
+
return data.platform && data.platform.length
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { hhmmToMinutesPastMidnight } from '../utils.mjs'
|
|
2
|
+
|
|
3
|
+
export class GTFSStopTime {
|
|
4
|
+
|
|
5
|
+
arrivalTime
|
|
6
|
+
arrivalTimeMinutes
|
|
7
|
+
departureTime
|
|
8
|
+
departureTimeMinutes
|
|
9
|
+
|
|
10
|
+
stopID
|
|
11
|
+
stopSequence
|
|
12
|
+
pickup
|
|
13
|
+
dropoff
|
|
14
|
+
distanceTravelled
|
|
15
|
+
|
|
16
|
+
constructor({ arrivalTime, departureTime, stopID, stopSequence, pickup, dropoff, distance }) {
|
|
17
|
+
this.arrivalTime = arrivalTime.slice(0, 5)
|
|
18
|
+
this.departureTime = departureTime.slice(0, 5)
|
|
19
|
+
|
|
20
|
+
this.arrivalTimeMinutes = hhmmToMinutesPastMidnight(this.arrivalTime)
|
|
21
|
+
this.departureTimeMinutes = hhmmToMinutesPastMidnight(this.departureTime)
|
|
22
|
+
|
|
23
|
+
this.stopID = stopID
|
|
24
|
+
this.stopSequence = stopSequence
|
|
25
|
+
this.pickup = parseInt(pickup)
|
|
26
|
+
this.dropoff = parseInt(dropoff)
|
|
27
|
+
this.distanceTravelled = distance
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
}
|