@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,73 @@
1
+ import async, { dir } from 'async'
2
+ import merge from './merge-stops.mjs'
3
+
4
+ export default async function setRouteStops(database) {
5
+ let gtfsTimetables = database.getCollection('gtfs timetables')
6
+ let routes = database.getCollection('routes')
7
+
8
+ let allRoutes = await routes.distinct('routeGTFSID')
9
+
10
+ let stopsByService = {}
11
+
12
+ await async.forEachLimit(allRoutes, 100, async routeGTFSID => {
13
+ let routeData = await routes.findDocument({ routeGTFSID })
14
+ let routeVariants = routeData.routePath.map(variant => ({ shapeID: variant.fullGTFSIDs[0], routeGTFSID }))
15
+ let routeDirections = [[], []]
16
+
17
+ for (let variant of routeVariants) {
18
+ let timetable = await gtfsTimetables.findDocument(variant)
19
+ if (!timetable) {
20
+ console.log('No timetable match for shapeID', variant)
21
+ continue
22
+ }
23
+
24
+ routeDirections[timetable.gtfsDirection].push(timetable.stopTimings.map(stop => ({
25
+ stopName: stop.stopName,
26
+ stopNumber: stop.stopNumber,
27
+ suburb: stop.suburb,
28
+ stopGTFSID: stop.stopGTFSID
29
+ })))
30
+ }
31
+
32
+ for (let gtfsDirection = 0; gtfsDirection < routeDirections.length; gtfsDirection++) {
33
+ let routeDirection = routeDirections[gtfsDirection]
34
+ if (!routeDirection.length) continue
35
+
36
+ let directionStops = merge(routeDirection, (a, b) => a.stopName === b.stopName)
37
+ let mostCommonDestinations = (await gtfsTimetables.aggregate([
38
+ { $match: { routeGTFSID, gtfsDirection } },
39
+ { $sortByCount: '$destination' }
40
+ ]).toArray()).map(e => e._id)
41
+
42
+ let lastStop = directionStops[directionStops.length - 1]
43
+ let lastStopName = lastStop.stopName
44
+
45
+ let directionName = lastStopName
46
+ if (directionName.includes('School') || directionName.includes('College')) {
47
+ directionName = mostCommonDestinations[0]
48
+ }
49
+
50
+ if (!stopsByService[routeGTFSID]) stopsByService[routeGTFSID] = []
51
+ stopsByService[routeGTFSID][gtfsDirection] = {
52
+ directionName,
53
+ gtfsDirection,
54
+ stops: directionStops
55
+ }
56
+ }
57
+ })
58
+
59
+ let bulkOperations = []
60
+
61
+ Object.keys(stopsByService).forEach(routeGTFSID => {
62
+ bulkOperations.push({
63
+ updateOne: {
64
+ filter: { routeGTFSID },
65
+ update: { $set: {
66
+ directions: stopsByService[routeGTFSID].filter(Boolean) // Apparently some routes dont have GTFS Dir 0
67
+ } }
68
+ }
69
+ })
70
+ })
71
+
72
+ await routes.bulkWrite(bulkOperations)
73
+ }
@@ -0,0 +1,64 @@
1
+ import async from 'async'
2
+
3
+ const AGGREGATION_GROUP = {
4
+ $group: {
5
+ _id: {
6
+ routeGTFSID: "$routeGTFSID",
7
+ gtfsDirection: "$gtfsDirection",
8
+ routeNumber: "$routeNumber"
9
+ }
10
+ }
11
+ }
12
+
13
+ function compareService(a, b) {
14
+ return a.routeGTFSID.localeCompare(b.routeGTFSID) || a.gtfsDirection - b.gtfsDirection
15
+ }
16
+
17
+ export default async function setStopServices(database, onMilestone = () => {}) {
18
+ let gtfsTimetables = database.getCollection('gtfs timetables')
19
+ let stops = database.getCollection('stops')
20
+
21
+ let stopIDs = await stops.distinct('_id')
22
+
23
+ let total = stopIDs.length
24
+
25
+ let count = 0
26
+ await async.forEachLimit(stopIDs, 100, async id => {
27
+ let stop = await stops.findDocument({ _id: id })
28
+
29
+ await async.forEach(stop.bays, async bay => {
30
+ let screenServices = await gtfsTimetables.aggregate([
31
+ {
32
+ $match: {
33
+ mode: bay.mode,
34
+ stopTimings: {
35
+ $elemMatch: { stopGTFSID: bay.stopGTFSID, 'stopConditions.pickup': 0 }
36
+ }
37
+ }
38
+ },
39
+ AGGREGATION_GROUP
40
+ ]).toArray()
41
+
42
+ let services = await gtfsTimetables.aggregate([
43
+ {
44
+ $match: {
45
+ mode: bay.mode,
46
+ 'stopTimings.stopGTFSID': bay.stopGTFSID
47
+ }
48
+ },
49
+ AGGREGATION_GROUP
50
+ ]).toArray()
51
+
52
+ bay.services = services.map(e => e._id).sort(compareService)
53
+ bay.screenServices = screenServices.map(e => e._id).sort(compareService)
54
+ })
55
+
56
+ await stops.updateDocument({ _id: id }, {
57
+ $set: {
58
+ bays: stop.bays
59
+ }
60
+ })
61
+
62
+ if (++count % 500 === 0) onMilestone(count, total)
63
+ })
64
+ }
package/lib/utils.mjs ADDED
@@ -0,0 +1,16 @@
1
+ import { DateTime } from 'luxon'
2
+ import { TIMEZONES } from './constants.mjs'
3
+
4
+ // Note: Need to allow for SA time as PTV does not handle cross border times well... or at all
5
+ export function parseDate(date) {
6
+ return DateTime.fromFormat(date, 'yyyyMMdd', { zone: TIMEZONES.melbourne })
7
+ }
8
+
9
+ export function toGTFSDate(date) {
10
+ return date.toFormat('yyyyMMdd')
11
+ }
12
+
13
+ export function hhmmToMinutesPastMidnight(date) {
14
+ let [ hours, minutes ] = date.split(':')
15
+ return parseInt(minutes) + parseInt(hours) * 60
16
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@transportme/load-ptv-gtfs",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "mocha"
8
+ },
9
+ "author": "",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "@transportme/database": "^1.0.2",
13
+ "@transportme/transportvic-utils": "^1.0.0",
14
+ "@turf/turf": "^7.0.0",
15
+ "adm-zip": "^0.5.14",
16
+ "async": "^3.2.6",
17
+ "line-reader": "^0.4.0",
18
+ "luxon": "^3.5.0",
19
+ "node-fetch": "^3.3.2"
20
+ },
21
+ "devDependencies": {
22
+ "chai": "^5.1.0",
23
+ "mocha": "^10.3.0",
24
+ "nock": "^14.0.0-beta.5",
25
+ "tmp-promise": "^3.0.3"
26
+ }
27
+ }
@@ -0,0 +1,182 @@
1
+ import { expect } from 'chai'
2
+ import GTFSCalendarReader from '../lib/gtfs-parser/readers/GTFSCalendarReader.mjs'
3
+ import GTFSCalendarDateReader from '../lib/gtfs-parser/readers/GTFSCalendarDatesReader.mjs'
4
+ import GTFSCalendar from '../lib/gtfs-parser/GTFSCalendar.mjs'
5
+ import GTFSCalendarDate from '../lib/gtfs-parser/GTFSCalendarDate.mjs'
6
+ import path from 'path'
7
+ import url from 'url'
8
+ import { CALENDAR_DATES } from '../lib/constants.mjs'
9
+ import { toGTFSDate } from '../lib/utils.mjs'
10
+
11
+ const __filename = url.fileURLToPath(import.meta.url)
12
+ const __dirname = path.dirname(__filename)
13
+
14
+ const calendarFile = path.join(__dirname, 'sample-data', 'calendar', 'calendar.txt')
15
+ const calendarDatesFile = path.join(__dirname, 'sample-data', 'calendar', 'calendar_dates.txt')
16
+
17
+ describe('The GTFSCalendar class', () => {
18
+ it('Should process the calendar data to a list of operation days', () => {
19
+ expect(new GTFSCalendar({
20
+ id: 'T1',
21
+ startDay: '20241122',
22
+ endDay: '20241129',
23
+ daysOfWeek: ["0","0","0","0","1","1","0"]
24
+ }).getOperationDays()).to.have.members([
25
+ "20241122", "20241123", "20241129"
26
+ ])
27
+
28
+ expect(new GTFSCalendar({
29
+ id: 'T2',
30
+ startDay: '20241123',
31
+ endDay: '20241130',
32
+ daysOfWeek: ["1","1","1","1","1","0","0"]
33
+ }).getOperationDays()).to.have.members([
34
+ "20241125", "20241126", "20241127", "20241128", "20241129"
35
+ ])
36
+ })
37
+
38
+ it('Be inclusive of the end date', () => {
39
+ expect(new GTFSCalendar({
40
+ id: 'T1',
41
+ startDay: '20241122',
42
+ endDay: '20241122',
43
+ daysOfWeek: ["1","1","1","1","1","1","1"]
44
+ }).getOperationDays()).to.have.members([
45
+ "20241122"
46
+ ])
47
+ })
48
+
49
+ it('Should allow adding of an extra date into the calendar', () => {
50
+ let cal = new GTFSCalendar({
51
+ id: 'T1',
52
+ startDay: '20241122',
53
+ endDay: '20241122',
54
+ daysOfWeek: ["1","1","1","1","1","1","1"]
55
+ }, [new GTFSCalendarDate({
56
+ id: 'T1',
57
+ date: '20241125',
58
+ type: CALENDAR_DATES.ADDED
59
+ })])
60
+
61
+ expect(cal.getOperationDays()).to.have.members([
62
+ "20241122", "20241125"
63
+ ])
64
+ })
65
+
66
+ it('Should allow removing of a specific date from the calendar', () => {
67
+ let cal = new GTFSCalendar({
68
+ id: 'T1',
69
+ startDay: '20241224',
70
+ endDay: '20241226',
71
+ daysOfWeek: ["1","1","1","1","1","1","1"]
72
+ }, [new GTFSCalendarDate({
73
+ id: 'T1',
74
+ date: '20241225',
75
+ type: CALENDAR_DATES.REMOVED
76
+ })])
77
+
78
+ expect(cal.getOperationDays()).to.deep.equal([
79
+ "20241224", "20241226"
80
+ ])
81
+ })
82
+
83
+ it('Should ensure that adding an extra date into the calendar does not introduce duplicates', () => {
84
+ let cal = new GTFSCalendar({
85
+ id: 'T1',
86
+ startDay: '20241122',
87
+ endDay: '20241122',
88
+ daysOfWeek: ["1","1","1","1","1","1","1"]
89
+ }, [new GTFSCalendarDate({
90
+ id: 'T1',
91
+ date: '20241122',
92
+ type: CALENDAR_DATES.ADDED
93
+ })])
94
+
95
+ expect(cal.getOperationDays()).to.deep.equal([
96
+ "20241122"
97
+ ])
98
+ })
99
+
100
+ it('Should ensure that duplicate extra dates do not introduce duplicates', () => {
101
+ let calendarDate = new GTFSCalendarDate({
102
+ id: 'T1',
103
+ date: '20241122',
104
+ type: CALENDAR_DATES.ADDED
105
+ })
106
+
107
+ let cal = new GTFSCalendar({
108
+ id: 'T1',
109
+ startDay: '20241122',
110
+ endDay: '20241122',
111
+ daysOfWeek: ["1","1","1","1","1","1","1"]
112
+ }, [calendarDate, calendarDate, calendarDate])
113
+
114
+ expect(cal.getOperationDays()).to.deep.equal([
115
+ "20241122"
116
+ ])
117
+ })
118
+
119
+ it('Should ensure that the date exists when removing a specific date from the calendar, and not do anything otherwise', () => {
120
+ let cal = new GTFSCalendar({
121
+ id: 'T1',
122
+ startDay: '20241224',
123
+ endDay: '20241226',
124
+ daysOfWeek: ["1","1","1","1","1","1","1"]
125
+ }, [new GTFSCalendarDate({
126
+ id: 'T1',
127
+ date: '20241227',
128
+ type: CALENDAR_DATES.REMOVED
129
+ })])
130
+
131
+ expect(cal.getOperationDays()).to.deep.equal([
132
+ "20241224", "20241225", "20241226"
133
+ ])
134
+ })
135
+ })
136
+
137
+ describe('The GTFSCalendarDate class', () => {
138
+ it('Should convert the CSV data into a proper date object', () => {
139
+ expect(new GTFSCalendarDate({
140
+ id: 'T6',
141
+ date: '20241125',
142
+ type: CALENDAR_DATES.ADDED
143
+ }).date.toUTC().toISO()).to.equal('2024-11-24T13:00:00.000Z')
144
+ })
145
+ })
146
+
147
+ describe('The GTFSCalendarDatesReader class', () => {
148
+ it('Should read a calendar dates and return a GTFSCalendarDate object', async () => {
149
+ let reader = new GTFSCalendarDateReader(calendarDatesFile)
150
+ await reader.open()
151
+
152
+ let date = await reader.getNextEntity()
153
+ expect(date.id).to.equal('T3_2')
154
+ expect(toGTFSDate(date.date)).to.equal('20250126')
155
+ expect(date.type).to.equal(CALENDAR_DATES.REMOVED)
156
+
157
+ let date2 = await reader.getNextEntity()
158
+ expect(date2.id).to.equal('T5_2')
159
+ expect(toGTFSDate(date2.date)).to.equal('20241225')
160
+ expect(date2.type).to.equal(CALENDAR_DATES.REMOVED)
161
+ })
162
+ })
163
+
164
+ describe('The GTFSCalendarReader class', () => {
165
+ it('Should read both a calendar dates and calendar file and return a GTFSCalendar object with the appropriate exceptions filled', async () => {
166
+ let reader = new GTFSCalendarReader(calendarFile, calendarDatesFile)
167
+ await reader.open()
168
+
169
+ let allCalendars = {}
170
+ while (reader.available()) {
171
+ let calendar = await reader.getNextEntity()
172
+ allCalendars[calendar.id] = calendar
173
+ }
174
+
175
+ expect(allCalendars['T2'].getOperationDays()).to.deep.equal(["20241109"])
176
+
177
+ expect(allCalendars['T3_2'].getOperationDays()).to.contain("20241124")
178
+ expect(allCalendars['T3_2'].getOperationDays()).to.contain("20241201")
179
+ expect(allCalendars['T3_2'].getOperationDays()).to.contain("20250209")
180
+ expect(allCalendars['T3_2'].getOperationDays()).to.not.contain("20250126")
181
+ })
182
+ })
@@ -0,0 +1,190 @@
1
+ import { LokiDatabaseConnection } from '@transportme/database'
2
+ import RouteLoader from '../lib/loader/RouteLoader.mjs'
3
+ import GTFSAgencyReader from '../lib/gtfs-parser/readers/GTFSAgencyReader.mjs'
4
+
5
+ import { TRANSIT_MODES } from '../lib/constants.mjs'
6
+
7
+ import path from 'path'
8
+ import url from 'url'
9
+ import { expect } from 'chai'
10
+
11
+ const __filename = url.fileURLToPath(import.meta.url)
12
+ const __dirname = path.dirname(__filename)
13
+
14
+ const regionalRoutesFile = path.join(__dirname, 'sample-data', 'routes', 'regional_bus_routes.txt')
15
+ const metroRoutesFile = path.join(__dirname, 'sample-data', 'routes', 'metro_bus_routes.txt')
16
+ const metroLinesFile = path.join(__dirname, 'sample-data', 'routes', 'metro_lines.txt')
17
+ const agencyFile = path.join(__dirname, 'sample-data', 'routes', 'agency.txt')
18
+ const wgtFile = path.join(__dirname, 'sample-data', 'routes', 'west_gipp_transit.txt')
19
+ const vlineFile = path.join(__dirname, 'sample-data', 'routes', 'vline.txt')
20
+
21
+ describe('The GTFS Agency Reader', () => {
22
+ it('Should read the agencies one line at a time', async () => {
23
+ let reader = new GTFSAgencyReader(agencyFile)
24
+ await reader.open()
25
+ let operator = await reader.getNextEntity()
26
+
27
+ expect(operator.id).to.equal('43')
28
+ expect(operator.name).to.equal('CDC Ballarat')
29
+ expect(operator.website).to.equal('https://www.ptv.vic.gov.au/footer/customer-service/operator-contact-details/?utm_source=open_data_click_agency&utm_medium=open_data_agency_click&utm_campaign=open_data_click')
30
+ expect(operator.phone).to.equal('1800 800 007')
31
+ })
32
+ })
33
+
34
+ describe('The GTFS Routes Loader', () => {
35
+ it('Should process the routes and add them to the database', async () => {
36
+ let database = new LokiDatabaseConnection('test-db')
37
+ let routes = await database.createCollection('routes')
38
+
39
+ let loader = new RouteLoader(regionalRoutesFile, agencyFile, TRANSIT_MODES.bus, database)
40
+ await loader.loadRoutes()
41
+
42
+ let bal10 = await routes.findDocument({
43
+ routeGTFSID: '6-10x'
44
+ })
45
+
46
+ expect(bal10).to.not.be.null
47
+ expect(bal10.routeNumber).to.equal('10')
48
+ expect(bal10.operators).to.deep.equal(['CDC Ballarat'])
49
+ })
50
+
51
+ it('Should open the agency.txt file and load all operators', async () => {
52
+ let database = new LokiDatabaseConnection('test-db')
53
+ await database.createCollection('routes')
54
+
55
+ let loader = new RouteLoader(regionalRoutesFile, agencyFile, TRANSIT_MODES.bus, database)
56
+ await loader.loadAgencies()
57
+
58
+ let paynesville = loader.getOperator('99')
59
+
60
+ expect(paynesville).to.not.be.undefined
61
+ expect(paynesville.name).to.equal('Paynesville Bus Lines')
62
+ })
63
+
64
+ it('Should handle smartrak routes', async () => {
65
+ let database = new LokiDatabaseConnection('test-db')
66
+ let routes = await database.createCollection('routes')
67
+
68
+ let loader = new RouteLoader(metroRoutesFile, agencyFile, TRANSIT_MODES.bus, database)
69
+ await loader.loadRoutes()
70
+
71
+ let smart900 = await routes.findDocument({
72
+ routeGTFSID: '4-900'
73
+ })
74
+
75
+ expect(smart900).to.not.be.null
76
+ expect(smart900.mode).to.equal(TRANSIT_MODES.bus)
77
+ expect(smart900.routeNumber).to.equal('900')
78
+ expect(smart900.operators).to.have.members(['CDC', 'Ventura Bus Lines'])
79
+ })
80
+
81
+ it('Should handle metro train routes', async () => {
82
+ let database = new LokiDatabaseConnection('test-db')
83
+ let routes = await database.createCollection('routes')
84
+
85
+ let loader = new RouteLoader(metroLinesFile, agencyFile, TRANSIT_MODES.metroTrain, database)
86
+ await loader.loadRoutes()
87
+
88
+ let stony = await routes.findDocument({
89
+ routeGTFSID: '2-STY'
90
+ })
91
+
92
+ expect(stony).to.not.be.null
93
+ expect(stony.mode).to.equal('metro train')
94
+ expect(stony.routeNumber).to.be.null
95
+ expect(stony.routeName).to.equal('Stony Point')
96
+ expect(stony.operators).to.have.members(['Metro'])
97
+ })
98
+
99
+ it('Should allow for custom route rewriting', async () => {
100
+ let database = new LokiDatabaseConnection('test-db')
101
+ let routes = await database.createCollection('routes')
102
+
103
+ let loader = new RouteLoader(wgtFile, agencyFile, TRANSIT_MODES.bus, database)
104
+ await loader.loadRoutes({
105
+ processRoute: route => {
106
+ if (route.routeGTFSID.match(/6-w\d\d/)) {
107
+ route.routeGTFSID = '6-WGT'
108
+ route.routeName = 'West Gippsland Transit'
109
+ }
110
+
111
+ return route
112
+ }
113
+ })
114
+
115
+ let wgt = await routes.findDocument({
116
+ routeGTFSID: '6-WGT'
117
+ })
118
+
119
+ expect(wgt, 'Expected West Gippsland Transit route 6-WGT to be created').to.not.be.null
120
+ expect(wgt.mode).to.equal('bus')
121
+ expect(wgt.routeNumber).to.be.null
122
+ expect(wgt.routeName).to.equal('West Gippsland Transit')
123
+ expect(wgt.operators).to.have.members(['Warragul Bus Lines'])
124
+
125
+ expect(await routes.findDocument({
126
+ routeGTFSID: '6-w40'
127
+ }), 'Expected 6-w40 Moe - Garfield to not exist').to.be.null
128
+
129
+ expect(await routes.findDocument({
130
+ routeGTFSID: '6-w41'
131
+ }), 'Expected 6-w41 Warragul - Pakenham to not exist').to.be.null
132
+
133
+ let nojee = await routes.findDocument({
134
+ routeGTFSID: '6-W89'
135
+ })
136
+ expect(nojee, 'Expected 6-W89 Warragul - Nojee to exist and not be modified').to.not.be.null
137
+ expect(nojee.mode).to.equal('bus')
138
+ expect(nojee.routeNumber).to.equal('89')
139
+ expect(nojee.routeName).to.equal('Noojee - Warragul Station Via Buln Buln')
140
+ expect(nojee.operators).to.have.members(['Warragul Bus Lines'])
141
+ })
142
+
143
+
144
+ it('Should return a route ID map', async () => {
145
+ let database = new LokiDatabaseConnection('test-db')
146
+ let routes = await database.createCollection('routes')
147
+
148
+ let loader = new RouteLoader(wgtFile, agencyFile, TRANSIT_MODES.bus, database)
149
+ await loader.loadRoutes({
150
+ processRoute: route => {
151
+ if (route.routeGTFSID.match(/6-w\d\d/)) {
152
+ route.routeGTFSID = '6-WGT'
153
+ route.routeName = 'West Gippsland Transit'
154
+ }
155
+
156
+ return route
157
+ }
158
+ })
159
+
160
+ let map = loader.getRouteIDMap()
161
+ expect(map['6-w19-mjp-1']).to.equal('6-WGT')
162
+ expect(map['6-W89-mjp-1']).to.equal('6-W89')
163
+ })
164
+
165
+ it('Should allow for dropping of unwanted routes', async () => {
166
+ let database = new LokiDatabaseConnection('test-db')
167
+ let routes = await database.createCollection('routes')
168
+
169
+ let loader = new RouteLoader(vlineFile, agencyFile, TRANSIT_MODES.regionalTrain, database)
170
+ await loader.loadRoutes({
171
+ processRoute: route => {
172
+ if (route.routeGTFSID === '1-vPK') return null
173
+ return route
174
+ }
175
+ })
176
+
177
+ expect(await routes.findDocument({
178
+ routeGTFSID: '1-vPK'
179
+ }), 'Expected 1-vPK V/Line Dandenong Metro Services to not exist').to.be.null
180
+
181
+ let traralgon = await routes.findDocument({
182
+ routeGTFSID: '1-TRN'
183
+ })
184
+ expect(traralgon, 'Expected 1-TRN Warragul - Nojee to exist and not be modified').to.not.be.null
185
+ expect(traralgon.mode).to.equal('regional train')
186
+ expect(traralgon.routeNumber).to.be.null
187
+ expect(traralgon.routeName).to.equal('Traralgon - Melbourne Via Pakenham, Moe & Morwell')
188
+ expect(traralgon.operators).to.have.members(['V/Line'])
189
+ })
190
+ })
@@ -0,0 +1,77 @@
1
+ import { expect } from 'chai'
2
+ import GTFSRouteReader from '../lib/gtfs-parser/readers/GTFSRouteReader.mjs'
3
+ import { TRANSIT_MODES } from '../lib/constants.mjs'
4
+ import path from 'path'
5
+ import url from 'url'
6
+
7
+ const __filename = url.fileURLToPath(import.meta.url)
8
+ const __dirname = path.dirname(__filename)
9
+
10
+ const regionalRoutesFile = path.join(__dirname, 'sample-data', 'routes', 'regional_bus_routes.txt')
11
+ const metroRoutesFile = path.join(__dirname, 'sample-data', 'routes', 'metro_bus_routes.txt')
12
+
13
+ let regionalBusRoute = {
14
+ route_id: '6-10x-mjp-1',
15
+ agency_id: '43',
16
+ route_short_name: '10',
17
+ route_long_name: 'Alfredton - Ballarat Station Via Wendouree'
18
+ }
19
+
20
+ describe('The GTFSRouteReader class', () => {
21
+ describe('The processRoute function', () => {
22
+ it('Should take in the raw CSV line and process it', () => {
23
+ let routeData = new GTFSRouteReader('', TRANSIT_MODES.bus).processEntity(regionalBusRoute)
24
+
25
+ expect(routeData.routeGTFSID).to.equal('6-10x')
26
+ expect(routeData.agencyID).to.equal(regionalBusRoute.agency_id)
27
+ expect(routeData.routeNumber).to.equal(regionalBusRoute.route_short_name)
28
+ expect(routeData.routeName).to.equal(regionalBusRoute.route_long_name)
29
+ })
30
+ })
31
+
32
+ describe('The getNextEntity function', () => {
33
+ it('Should read the CSV file and return the next stop', async () => {
34
+ let routeReader = new GTFSRouteReader(regionalRoutesFile, TRANSIT_MODES.bus)
35
+ await routeReader.open()
36
+
37
+ await routeReader.getNextEntity()
38
+ let routeData = await routeReader.getNextEntity()
39
+
40
+ expect(routeData.agencyID).to.equal('99')
41
+ expect(routeData.routeNumber).to.equal('13')
42
+ expect(routeData.routeName).to.equal('Paynesville - Bairnsdale Via Paynesville')
43
+ })
44
+ })
45
+ })
46
+
47
+ describe('The GTFSRoute class', () => {
48
+ describe('The route ID parsing', () => {
49
+ it('Should parse a regular route ID and remove the -mjp- portion', async () => {
50
+ let routeReader = new GTFSRouteReader(regionalRoutesFile, TRANSIT_MODES.bus)
51
+ await routeReader.open()
52
+
53
+ let routeData = await routeReader.getNextEntity()
54
+
55
+ expect(routeData.routeGTFSID).to.equal('6-10x')
56
+ })
57
+
58
+ it('Should parse a regular route ID and pad with 0s as needed', async () => {
59
+ let routeReader = new GTFSRouteReader(regionalRoutesFile, TRANSIT_MODES.bus)
60
+ await routeReader.open()
61
+
62
+ await routeReader.getNextEntity()
63
+ let routeData = await routeReader.getNextEntity()
64
+
65
+ expect(routeData.routeGTFSID).to.equal('6-013')
66
+ })
67
+
68
+ it('Should detect a smartrak route ID and convert to a legacy 4-XXX trip', async () => {
69
+ let routeReader = new GTFSRouteReader(metroRoutesFile, TRANSIT_MODES.bus)
70
+ await routeReader.open()
71
+
72
+ expect((await routeReader.getNextEntity()).routeGTFSID).to.equal('4-900')
73
+ expect((await routeReader.getNextEntity()).routeGTFSID).to.equal('4-900')
74
+ expect((await routeReader.getNextEntity()).routeGTFSID).to.equal('4-601')
75
+ })
76
+ })
77
+ })