@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.
Files changed (77) hide show
  1. package/.github/workflows/unit-test.yml +46 -0
  2. package/lib/constants.mjs +18 -0
  3. package/lib/gtfs-package/PTVGTFS.mjs +96 -0
  4. package/lib/gtfs-parser/GTFSAgency.mjs +15 -0
  5. package/lib/gtfs-parser/GTFSCalendar.mjs +63 -0
  6. package/lib/gtfs-parser/GTFSCalendarDate.mjs +15 -0
  7. package/lib/gtfs-parser/GTFSRoute.mjs +106 -0
  8. package/lib/gtfs-parser/GTFSShape.mjs +39 -0
  9. package/lib/gtfs-parser/GTFSStop.mjs +212 -0
  10. package/lib/gtfs-parser/GTFSStopTime.mjs +30 -0
  11. package/lib/gtfs-parser/GTFSTrip.mjs +169 -0
  12. package/lib/gtfs-parser/readers/GTFSAgencyReader.mjs +27 -0
  13. package/lib/gtfs-parser/readers/GTFSCalendarDatesReader.mjs +25 -0
  14. package/lib/gtfs-parser/readers/GTFSCalendarReader.mjs +63 -0
  15. package/lib/gtfs-parser/readers/GTFSReader.mjs +48 -0
  16. package/lib/gtfs-parser/readers/GTFSRouteReader.mjs +35 -0
  17. package/lib/gtfs-parser/readers/GTFSShapeReader.mjs +75 -0
  18. package/lib/gtfs-parser/readers/GTFSStopTimesReader.mjs +85 -0
  19. package/lib/gtfs-parser/readers/GTFSStopsReader.mjs +36 -0
  20. package/lib/gtfs-parser/readers/GTFSTripReader.mjs +61 -0
  21. package/lib/gtfs-parser/readers/line-reader.mjs +112 -0
  22. package/lib/loader/RouteLoader.mjs +101 -0
  23. package/lib/loader/ShapeLoader.mjs +86 -0
  24. package/lib/loader/StopsLoader.mjs +139 -0
  25. package/lib/loader/TripLoader.mjs +187 -0
  26. package/lib/post-processing/merge-stops.mjs +85 -0
  27. package/lib/post-processing/set-route-stops.mjs +73 -0
  28. package/lib/post-processing/set-stop-services.mjs +64 -0
  29. package/lib/utils.mjs +16 -0
  30. package/package.json +27 -0
  31. package/test/gtfs-calendar.test.mjs +182 -0
  32. package/test/gtfs-route-loader.test.mjs +190 -0
  33. package/test/gtfs-routes.test.mjs +77 -0
  34. package/test/gtfs-shapes-loader.test.mjs +105 -0
  35. package/test/gtfs-shapes.test.mjs +23 -0
  36. package/test/gtfs-stop-loader-metro.test.mjs +49 -0
  37. package/test/gtfs-stop-loader.test.mjs +223 -0
  38. package/test/gtfs-stop-times.test.mjs +37 -0
  39. package/test/gtfs-stops.test.mjs +290 -0
  40. package/test/gtfs-trip-loader.test.mjs +142 -0
  41. package/test/gtfs-trips.test.mjs +109 -0
  42. package/test/line-reader.test.mjs +94 -0
  43. package/test/merge-stops.test.mjs +376 -0
  44. package/test/ptv-gtfs.test.mjs +106 -0
  45. package/test/sample-data/calendar/calendar.txt +34 -0
  46. package/test/sample-data/calendar/calendar_dates.txt +26 -0
  47. package/test/sample-data/gtfs-splitting/force_unique_stops_bus.txt +7 -0
  48. package/test/sample-data/gtfs-splitting/force_unique_stops_tram.txt +3 -0
  49. package/test/sample-data/gtfs-splitting/line_reader_test.txt +8 -0
  50. package/test/sample-data/gtfs-splitting/name_overrides.txt +4 -0
  51. package/test/sample-data/gtfs-splitting/nov_2024_train_data.txt +6 -0
  52. package/test/sample-data/gtfs-splitting/sample_stops.txt +20 -0
  53. package/test/sample-data/gtfs-splitting/special_chars_stops_csv.txt +6 -0
  54. package/test/sample-data/gtfs-splitting/stops_without_secondary.txt +6 -0
  55. package/test/sample-data/gtfs-unzipping/gtfs.zip +0 -0
  56. package/test/sample-data/one_line.csv +2 -0
  57. package/test/sample-data/routes/agency.txt +11 -0
  58. package/test/sample-data/routes/metro_bus_routes.txt +4 -0
  59. package/test/sample-data/routes/metro_lines.txt +5 -0
  60. package/test/sample-data/routes/regional_bus_routes.txt +7 -0
  61. package/test/sample-data/routes/vline.txt +7 -0
  62. package/test/sample-data/routes/west_gipp_transit.txt +20 -0
  63. package/test/sample-data/shapes/shapes.txt +89 -0
  64. package/test/sample-data/shapes/stop_times.txt +9 -0
  65. package/test/sample-data/shapes/stops.txt +4 -0
  66. package/test/sample-data/shapes/trips.txt +4 -0
  67. package/test/sample-data/stop-data/name-overrides.json +25 -0
  68. package/test/sample-data/stop-data/unique-stops.json +1 -0
  69. package/test/sample-data/suburbs.json +77 -0
  70. package/test/sample-data/trips/bus_trips.txt +5 -0
  71. package/test/sample-data/trips/stop_times.txt +29 -0
  72. package/test/sample-data/trips/stop_times_new.txt +8 -0
  73. package/test/sample-data/trips/stops.txt +8 -0
  74. package/test/sample-data/trips/trips.txt +5 -0
  75. package/test/sample-data/trips/trips_new.txt +2 -0
  76. package/test/set-stop-services.test.mjs +69 -0
  77. 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,15 @@
1
+ export default class GTFSAgency {
2
+
3
+ id
4
+ name
5
+ website
6
+ phone
7
+
8
+ constructor({ id, name, website, phone }) {
9
+ this.id = id
10
+ this.name = name
11
+ this.website = website
12
+ this.phone = phone
13
+ }
14
+
15
+ }
@@ -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,15 @@
1
+ import { parseDate } from '../utils.mjs'
2
+
3
+ export default class GTFSCalendarDate {
4
+
5
+ id
6
+ date
7
+ type
8
+
9
+ constructor({ id, date, type }) {
10
+ this.id = id
11
+ this.date = parseDate(date)
12
+ this.type = type
13
+ }
14
+
15
+ }
@@ -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
+ }