@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,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
|
+
})
|