@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,290 @@
|
|
|
1
|
+
import { expect } from 'chai'
|
|
2
|
+
import GTFSStopsReader from '../lib/gtfs-parser/readers/GTFSStopsReader.mjs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import url from 'url'
|
|
5
|
+
import suburbs from './sample-data/suburbs.json' with { type: 'json' }
|
|
6
|
+
|
|
7
|
+
const __filename = url.fileURLToPath(import.meta.url)
|
|
8
|
+
const __dirname = path.dirname(__filename)
|
|
9
|
+
|
|
10
|
+
const stopsFile = path.join(__dirname, 'sample-data', 'gtfs-splitting', 'line_reader_test.txt')
|
|
11
|
+
|
|
12
|
+
let stopInput = {
|
|
13
|
+
stop_id: '1000',
|
|
14
|
+
stop_name: 'Dole Ave/Cheddar Rd (Reservoir)',
|
|
15
|
+
stop_lat: '-37.7007748061827',
|
|
16
|
+
stop_lon: '145.018951051008'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let stopInput2 = {
|
|
20
|
+
stop_id: '10001',
|
|
21
|
+
stop_name: 'Rex St/Taylors Rd (Kings Park)',
|
|
22
|
+
stop_lat: '-37.7269752097338',
|
|
23
|
+
stop_lon: '144.776152425766'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let stopInput3 = {
|
|
27
|
+
stop_id: '13486',
|
|
28
|
+
stop_name: 'Narre Warren, Fountain Gate Primary School/Victoria Rd',
|
|
29
|
+
stop_lat: '-38.0093755011572',
|
|
30
|
+
stop_lon: '145.296182027229'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('The GTFSStopReader class', () => {
|
|
34
|
+
describe('The initialProcess function', () => {
|
|
35
|
+
it('Should take in the raw CSV line and process it', () => {
|
|
36
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity(stopInput)
|
|
37
|
+
|
|
38
|
+
expect(stopData.originalName).to.equal(stopInput.stop_name)
|
|
39
|
+
expect(stopData.stopGTFSID).to.equal(stopInput.stop_id)
|
|
40
|
+
expect(stopData.location).to.deep.equal({
|
|
41
|
+
type: 'Point',
|
|
42
|
+
coordinates: [145.018951051008, -37.7007748061827]
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('Should populate the suburb and stop number', () => {
|
|
47
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
48
|
+
...stopInput2,
|
|
49
|
+
stop_name: '51a-Rex St/Taylors Rd (Kings Park)'
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
expect(stopData.suburb).to.equal('Kings Park')
|
|
53
|
+
expect(stopData.stopNumber).to.equal('51A')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('The getNextEntity function', () => {
|
|
58
|
+
it('Should read the CSV file and return the next stop', async () => {
|
|
59
|
+
let stopReader = new GTFSStopsReader(stopsFile)
|
|
60
|
+
await stopReader.open()
|
|
61
|
+
|
|
62
|
+
let stopData = await stopReader.getNextEntity()
|
|
63
|
+
|
|
64
|
+
expect(stopData.rawStopName).to.equal('Dole Ave/Cheddar Rd')
|
|
65
|
+
expect(stopData.stopGTFSID).to.equal('1000')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('The available function', () => {
|
|
70
|
+
it('Should return true if there is more data, and false otherwise', async () => {
|
|
71
|
+
let stopReader = new GTFSStopsReader(stopsFile)
|
|
72
|
+
await stopReader.open()
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < 6; i++) await stopReader.getNextEntity()
|
|
75
|
+
expect(stopReader.available()).to.be.true
|
|
76
|
+
|
|
77
|
+
await stopReader.getNextEntity()
|
|
78
|
+
expect(stopReader.available()).to.be.false
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('The GTFSStop class', () => {
|
|
84
|
+
describe('The requiresSuburb function', () => {
|
|
85
|
+
it('Should return false if the stop name already has a suburb', async () => {
|
|
86
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity(stopInput)
|
|
87
|
+
expect(stopData.requiresSuburb()).to.be.false
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('Should return false if the stop name is missing the suburb', async () => {
|
|
91
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
92
|
+
...stopInput,
|
|
93
|
+
stop_name: 'Dole Ave/Cheddar Rd'
|
|
94
|
+
})
|
|
95
|
+
expect(stopData.requiresSuburb()).to.be.true
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('The getSuburbFromName function', () => {
|
|
100
|
+
it('Should extract the stop suburb from its name in brackets', () => {
|
|
101
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity(stopInput)
|
|
102
|
+
expect(stopData.getSuburbFromName()).to.equal('Reservoir')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('Should extract the stop suburb from its name if it appears at the front', () => {
|
|
106
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity(stopInput3)
|
|
107
|
+
expect(stopData.suburbIsInFront()).to.be.true
|
|
108
|
+
expect(stopData.getSuburbFromName()).to.equal('Narre Warren')
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('The getSuburbState function', () => {
|
|
113
|
+
it('Should extract the state form a suburb in the form (Suburb (State))', () => {
|
|
114
|
+
let stopName = 'Albury Botanic Gardens/Wodonga Pl (Albury (NSW))'
|
|
115
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
116
|
+
...stopInput,
|
|
117
|
+
stop_name: stopName
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
expect(stopData.getSuburbState(stopName.lastIndexOf('('))).to.equal('NSW')
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('Should process interstate suburbs in the format (Suburb (State))', () => {
|
|
125
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
126
|
+
...stopInput,
|
|
127
|
+
stop_name: 'Albury Botanic Gardens/Wodonga Pl (Albury (NSW))'
|
|
128
|
+
})
|
|
129
|
+
expect(stopData.getSuburbFromName()).to.equal('Albury, NSW')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('Should process interstate suburbs in the format (Suburb (Local Area - State))', () => {
|
|
133
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
134
|
+
...stopInput,
|
|
135
|
+
stop_name: 'Emma Way/Wright St (Glenroy (Albury - NSW))'
|
|
136
|
+
})
|
|
137
|
+
expect(stopData.getSuburbFromName()).to.equal('Glenroy, NSW')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('Should expand St in the suburb name', () => {
|
|
141
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
142
|
+
...stopInput,
|
|
143
|
+
stop_name: 'Dundas St/Market St (St Arnaud)'
|
|
144
|
+
})
|
|
145
|
+
expect(stopData.getSuburbFromName()).to.equal('St. Arnaud')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('Should expand Mt in the suburb name', () => {
|
|
149
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
150
|
+
...stopInput,
|
|
151
|
+
stop_name: 'Tourist Information Centre/Jubilee Hwy East (Mt Gambier (SA))'
|
|
152
|
+
})
|
|
153
|
+
expect(stopData.getSuburbFromName()).to.equal('Mount Gambier, SA')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('The getSuburbFromLocation function', () => {
|
|
157
|
+
it('Should expand St in the suburb name', () => {
|
|
158
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
159
|
+
stop_id: '912',
|
|
160
|
+
stop_name: 'Princes St/Barkly St',
|
|
161
|
+
stop_lat: '-37.8597466731382',
|
|
162
|
+
stop_lon: '144.98247799877'
|
|
163
|
+
})
|
|
164
|
+
expect(stopData.getSuburbFromLocation(suburbs)).to.equal('St. Kilda')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('Should return a generic Interstate suburb if it cannot identify the suburb', () => {
|
|
168
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
169
|
+
stop_id: '20815',
|
|
170
|
+
stop_name: '85 Franklin St',
|
|
171
|
+
stop_lat: '-34.9274852283649',
|
|
172
|
+
stop_lon: '138.595751207359'
|
|
173
|
+
})
|
|
174
|
+
expect(stopData.getSuburbFromLocation(suburbs)).to.equal('Interstate')
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe('The getStopNameWithoutSuburb function', () => {
|
|
179
|
+
it('Should strip the suburb from the stop name if the suburb appears behind', () => {
|
|
180
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity(stopInput)
|
|
181
|
+
|
|
182
|
+
expect(stopData.getStopNameWithoutSuburb()).to.equal('Dole Ave/Cheddar Rd')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('Should strip the suburb from the stop name for NSW/SA stops', () => {
|
|
186
|
+
let stopName = 'Albury Railway Station (Albury (NSW))'
|
|
187
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
188
|
+
...stopInput,
|
|
189
|
+
stop_name: stopName
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
expect(stopData.getStopNameWithoutSuburb()).to.equal('Albury Railway Station')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('Should strip the suburb from the stop name if the suburb is in front', () => {
|
|
196
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity(stopInput3)
|
|
197
|
+
|
|
198
|
+
expect(stopData.getStopNameWithoutSuburb()).to.equal('Fountain Gate Primary School/Victoria Rd')
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('The matchStopNumber function', () => {
|
|
203
|
+
it('Should match stop numbers in the format XXX-STOPNAME', () => {
|
|
204
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
205
|
+
...stopInput,
|
|
206
|
+
stop_name: '125-Yuille St/Centenary Ave (Melton)'
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
expect(stopData.matchStopNumber()).to.deep.equal({
|
|
210
|
+
stopNumber: '125',
|
|
211
|
+
stopName: 'Yuille St/Centenary Ave'
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// Can randomly happen when PTV exports the data without suburb names or secondary street names
|
|
216
|
+
// See 25 Jan 2021 archive
|
|
217
|
+
// Seems to be caused when they set some flag incorrectly as this seems to match the stop names on the printed timetables.
|
|
218
|
+
// Weird huh.
|
|
219
|
+
it('Should match stop numbers in the format STOPNAME - Stop XXX', () => {
|
|
220
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
221
|
+
...stopInput,
|
|
222
|
+
stop_name: 'Yuille St - Stop D99'
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
expect(stopData.matchStopNumber()).to.deep.equal({
|
|
226
|
+
stopNumber: 'D99',
|
|
227
|
+
stopName: 'Yuille St'
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('Should ensure the stop numbers are all uppercase', () => {
|
|
232
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
233
|
+
...stopInput,
|
|
234
|
+
stop_name: '125a-Southbank Tram Depot (South Melbourne)'
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
expect(stopData.matchStopNumber()).to.deep.equal({
|
|
238
|
+
stopNumber: '125A',
|
|
239
|
+
stopName: 'Southbank Tram Depot'
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('Should return null if there is no stop number', () => {
|
|
244
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
245
|
+
...stopInput,
|
|
246
|
+
stop_name: 'Southbank Tram Depot (South Melbourne)'
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
expect(stopData.matchStopNumber()).to.deep.equal({
|
|
250
|
+
stopNumber: null,
|
|
251
|
+
stopName: 'Southbank Tram Depot'
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('Should not match street numbers spanning multiple units', () => {
|
|
256
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
257
|
+
...stopInput,
|
|
258
|
+
stop_name: '227-243 Wellington Rd (Mulgrave)'
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
expect(stopData.matchStopNumber()).to.deep.equal({
|
|
262
|
+
stopNumber: null,
|
|
263
|
+
stopName: '227-243 Wellington Rd'
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
describe('The getFullStopName function', () => {
|
|
269
|
+
it('Should expand the stop name', () => {
|
|
270
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity(stopInput2)
|
|
271
|
+
expect(stopData.getFullStopName()).to.equal('Rex Street/Taylors Road')
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('Should handle stops with only a primary name', () => {
|
|
275
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
276
|
+
...stopInput,
|
|
277
|
+
stop_name: 'Knox City SC (Knoxfield)'
|
|
278
|
+
})
|
|
279
|
+
expect(stopData.getFullStopName()).to.equal('Knox City Shopping Centre')
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('Should handle the direction appearing as well', () => {
|
|
283
|
+
let stopData = new GTFSStopsReader('', suburbs).processEntity({
|
|
284
|
+
...stopInput,
|
|
285
|
+
stop_name: 'Newman Cres (north side)/east of Pechell St (Made Up Suburb)'
|
|
286
|
+
})
|
|
287
|
+
expect(stopData.getFullStopName()).to.equal('Newman Crescent - North/Pechell Street - East')
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { expect } from 'chai'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import url from 'url'
|
|
4
|
+
import { TRANSIT_MODES } from '../lib/constants.mjs'
|
|
5
|
+
import { LokiDatabaseConnection } from '@transportme/database'
|
|
6
|
+
import StopsLoader from '../lib/loader/StopsLoader.mjs'
|
|
7
|
+
import RouteLoader from '../lib/loader/RouteLoader.mjs'
|
|
8
|
+
import TripLoader from '../lib/loader/TripLoader.mjs'
|
|
9
|
+
import suburbs from './sample-data/suburbs.json' with { type: 'json' }
|
|
10
|
+
|
|
11
|
+
const __filename = url.fileURLToPath(import.meta.url)
|
|
12
|
+
const __dirname = path.dirname(__filename)
|
|
13
|
+
|
|
14
|
+
const routesFile = path.join(__dirname, 'sample-data', 'routes', 'metro_lines.txt')
|
|
15
|
+
const agencyFile = path.join(__dirname, 'sample-data', 'routes', 'agency.txt')
|
|
16
|
+
|
|
17
|
+
const calendarFile = path.join(__dirname, 'sample-data', 'calendar', 'calendar.txt')
|
|
18
|
+
const calendarDatesFile = path.join(__dirname, 'sample-data', 'calendar', 'calendar_dates.txt')
|
|
19
|
+
|
|
20
|
+
const stopsFile = path.join(__dirname, 'sample-data', 'trips', 'stops.txt')
|
|
21
|
+
const stopTimesFile = path.join(__dirname, 'sample-data', 'trips', 'stop_times.txt')
|
|
22
|
+
const tripsFile = path.join(__dirname, 'sample-data', 'trips', 'trips.txt')
|
|
23
|
+
|
|
24
|
+
const newStopTimesFile = path.join(__dirname, 'sample-data', 'trips', 'stop_times_new.txt')
|
|
25
|
+
const newTripsFile = path.join(__dirname, 'sample-data', 'trips', 'trips_new.txt')
|
|
26
|
+
|
|
27
|
+
describe('The TripLoader class', () => {
|
|
28
|
+
it('Should process the trip and add it to the database', async () => {
|
|
29
|
+
let database = new LokiDatabaseConnection('test-db')
|
|
30
|
+
let stops = await database.createCollection('stops')
|
|
31
|
+
let routes = await database.createCollection('routes')
|
|
32
|
+
let trips = await database.createCollection('gtfs timetables')
|
|
33
|
+
|
|
34
|
+
let stopLoader = new StopsLoader(stopsFile, suburbs, TRANSIT_MODES.metroTrain, database)
|
|
35
|
+
await stopLoader.loadStops()
|
|
36
|
+
|
|
37
|
+
let routeLoader = new RouteLoader(routesFile, agencyFile, TRANSIT_MODES.metroTrain, database)
|
|
38
|
+
await routeLoader.loadRoutes()
|
|
39
|
+
|
|
40
|
+
let routeIDMap = routeLoader.getRouteIDMap()
|
|
41
|
+
|
|
42
|
+
let tripLoader = new TripLoader({
|
|
43
|
+
tripsFile, stopTimesFile,
|
|
44
|
+
calendarFile, calendarDatesFile
|
|
45
|
+
}, TRANSIT_MODES.metroTrain, database)
|
|
46
|
+
await tripLoader.loadTrips({
|
|
47
|
+
routeIDMap
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
let trip = await trips.findDocument({ tripID: '1.T2.2-ALM-vpt-1.1.R' })
|
|
51
|
+
expect(trip).to.not.be.null
|
|
52
|
+
|
|
53
|
+
expect(trip.routeName).to.equal('Alamein')
|
|
54
|
+
expect(trip.routeNumber).to.be.null
|
|
55
|
+
expect(trip.operationDays.length).to.equal(12)
|
|
56
|
+
expect(trip.operationDays[0]).to.equal('20241123')
|
|
57
|
+
|
|
58
|
+
expect(trip.shapeID).to.equal('2-ALM-vpt-1.1.R')
|
|
59
|
+
expect(trip.block).to.be.null
|
|
60
|
+
expect(trip.gtfsDirection).to.equal(1)
|
|
61
|
+
|
|
62
|
+
expect(trip.stopTimings.length).to.equal(7)
|
|
63
|
+
expect(trip.stopTimings[0].stopName).to.equal('Alamein Railway Station')
|
|
64
|
+
expect(trip.stopTimings[0].arrivalTime).to.be.null
|
|
65
|
+
expect(trip.stopTimings[0].arrivalTimeMinutes).to.be.null
|
|
66
|
+
expect(trip.stopTimings[0].stopConditions.dropoff).to.equal(1)
|
|
67
|
+
expect(trip.stopTimings[0].stopConditions.pickup).to.equal(0)
|
|
68
|
+
expect(trip.stopTimings[0].departureTime).to.equal('04:57')
|
|
69
|
+
expect(trip.stopTimings[0].departureTimeMinutes).to.equal(4 * 60 + 57)
|
|
70
|
+
expect(trip.stopTimings[0].platform).to.equal('1')
|
|
71
|
+
|
|
72
|
+
expect(trip.stopTimings[5].stopName).to.equal('Riversdale Railway Station')
|
|
73
|
+
expect(trip.stopTimings[5].platform).to.equal('2')
|
|
74
|
+
|
|
75
|
+
expect(trip.stopTimings[6].stopName).to.equal('Camberwell Railway Station')
|
|
76
|
+
expect(trip.stopTimings[6].arrivalTime).to.equal('05:08')
|
|
77
|
+
expect(trip.stopTimings[6].arrivalTimeMinutes).to.equal(5 * 60 + 8)
|
|
78
|
+
expect(trip.stopTimings[6].departureTime).to.be.null
|
|
79
|
+
expect(trip.stopTimings[6].departureTimeMinutes).to.be.null
|
|
80
|
+
expect(trip.stopTimings[6].stopConditions.dropoff).to.equal(0)
|
|
81
|
+
expect(trip.stopTimings[6].stopConditions.pickup).to.equal(1)
|
|
82
|
+
expect(trip.stopTimings[6].platform).to.equal('3')
|
|
83
|
+
|
|
84
|
+
expect(trip.origin).to.equal('Alamein Railway Station')
|
|
85
|
+
expect(trip.destination).to.equal('Camberwell Railway Station')
|
|
86
|
+
|
|
87
|
+
expect(trip.departureTime).to.equal('04:57')
|
|
88
|
+
expect(trip.destinationArrivalTime).to.equal('05:08')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('It should return a mapping of Shape IDs to Route IDs', async () => {
|
|
92
|
+
let database = new LokiDatabaseConnection('test-db')
|
|
93
|
+
let stops = await database.createCollection('stops')
|
|
94
|
+
let routes = await database.createCollection('routes')
|
|
95
|
+
let trips = await database.createCollection('gtfs timetables')
|
|
96
|
+
|
|
97
|
+
let stopLoader = new StopsLoader(stopsFile, suburbs, TRANSIT_MODES.metroTrain, database)
|
|
98
|
+
await stopLoader.loadStops()
|
|
99
|
+
|
|
100
|
+
let routeLoader = new RouteLoader(routesFile, agencyFile, TRANSIT_MODES.metroTrain, database)
|
|
101
|
+
await routeLoader.loadRoutes()
|
|
102
|
+
|
|
103
|
+
let routeIDMap = routeLoader.getRouteIDMap()
|
|
104
|
+
|
|
105
|
+
let tripLoader = new TripLoader({
|
|
106
|
+
tripsFile, stopTimesFile,
|
|
107
|
+
calendarFile, calendarDatesFile
|
|
108
|
+
}, TRANSIT_MODES.metroTrain, database)
|
|
109
|
+
|
|
110
|
+
await tripLoader.loadTrips({ routeIDMap })
|
|
111
|
+
let shapeIDMap = tripLoader.getShapeIDMap()
|
|
112
|
+
|
|
113
|
+
expect(shapeIDMap['2-ALM-vpt-1.1.R']).to.equal('2-ALM')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('It should accept trip IDs in the new format and extract the TDN from there', async () => {
|
|
117
|
+
let database = new LokiDatabaseConnection('test-db')
|
|
118
|
+
let stops = await database.createCollection('stops')
|
|
119
|
+
let routes = await database.createCollection('routes')
|
|
120
|
+
let trips = await database.createCollection('gtfs timetables')
|
|
121
|
+
|
|
122
|
+
let stopLoader = new StopsLoader(stopsFile, suburbs, TRANSIT_MODES.metroTrain, database)
|
|
123
|
+
await stopLoader.loadStops()
|
|
124
|
+
|
|
125
|
+
let routeLoader = new RouteLoader(routesFile, agencyFile, TRANSIT_MODES.metroTrain, database)
|
|
126
|
+
await routeLoader.loadRoutes()
|
|
127
|
+
|
|
128
|
+
let routeIDMap = routeLoader.getRouteIDMap()
|
|
129
|
+
|
|
130
|
+
let tripLoader = new TripLoader({
|
|
131
|
+
tripsFile: newTripsFile, stopTimesFile: newStopTimesFile,
|
|
132
|
+
calendarFile, calendarDatesFile
|
|
133
|
+
}, TRANSIT_MODES.metroTrain, database)
|
|
134
|
+
|
|
135
|
+
await tripLoader.loadTrips({ routeIDMap })
|
|
136
|
+
|
|
137
|
+
let trip = await trips.findDocument({ tripID: '02-ALM--1-T2-2302' })
|
|
138
|
+
|
|
139
|
+
expect(trip).to.not.be.null
|
|
140
|
+
expect(trip.runID).to.equal('2302')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { expect } from 'chai'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import url from 'url'
|
|
4
|
+
import GTFSCalendar from '../lib/gtfs-parser/GTFSCalendar.mjs'
|
|
5
|
+
import GTFSTrip, { SmartrakTrip } from '../lib/gtfs-parser/GTFSTrip.mjs'
|
|
6
|
+
import GTFSTripReader from '../lib/gtfs-parser/readers/GTFSTripReader.mjs'
|
|
7
|
+
import { TRANSIT_MODES } from '../lib/constants.mjs'
|
|
8
|
+
|
|
9
|
+
const __filename = url.fileURLToPath(import.meta.url)
|
|
10
|
+
const __dirname = path.dirname(__filename)
|
|
11
|
+
|
|
12
|
+
const tripsFile = path.join(__dirname, 'sample-data', 'trips', 'trips.txt')
|
|
13
|
+
const busTripsFile = path.join(__dirname, 'sample-data', 'trips', 'bus_trips.txt')
|
|
14
|
+
|
|
15
|
+
describe('The GTFSTrip class', () => {
|
|
16
|
+
it('Should process the skeleton trip data and populate the operation days', () => {
|
|
17
|
+
let calendar = new GTFSCalendar({
|
|
18
|
+
id: 'T1',
|
|
19
|
+
startDay: '20241122',
|
|
20
|
+
endDay: '20241129',
|
|
21
|
+
daysOfWeek: ["0","0","0","0","1","1","0"]
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// route_id,service_id,trip_id,shape_id,trip_headsign,direction_id,block_id
|
|
25
|
+
// "6-452-mjp-1","T0_16","1.T0.6-452-mjp-1.1.H","6-452-mjp-1.1.H","Eynesbury","0",""
|
|
26
|
+
let trip = new GTFSTrip({
|
|
27
|
+
routeGTFSID: '6-452',
|
|
28
|
+
calendar: calendar,
|
|
29
|
+
id: '1.T0.6-452-mjp-1.1.H',
|
|
30
|
+
shapeID: '6-452-mjp-1.1.H',
|
|
31
|
+
headsign: 'Eynesbury',
|
|
32
|
+
direction: '0',
|
|
33
|
+
block: ''
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect(trip.getOperationDays()).to.have.members([
|
|
37
|
+
'20241122', '20241123', '20241129'
|
|
38
|
+
])
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('The GTFSTripReader class', () => {
|
|
43
|
+
it('Should read the trips.txt file and return a GTFSTrip object', async () => {
|
|
44
|
+
let calendars = {
|
|
45
|
+
'T2_2': new GTFSCalendar({
|
|
46
|
+
id: 'T2_2',
|
|
47
|
+
startDay: '20241122',
|
|
48
|
+
endDay: '20241122',
|
|
49
|
+
daysOfWeek: ["0","0","0","0","1","0","0"]
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let routeMappings = { '2-ALM-vpt-1': '2-ALM' }
|
|
54
|
+
|
|
55
|
+
let reader = new GTFSTripReader(tripsFile, calendars, routeMappings, TRANSIT_MODES.metroTrain)
|
|
56
|
+
await reader.open()
|
|
57
|
+
|
|
58
|
+
let trip = await reader.getNextEntity()
|
|
59
|
+
|
|
60
|
+
expect(trip.getTripID()).to.equal('1.T2.2-ALM-vpt-1.1.R')
|
|
61
|
+
expect(trip.getRouteGTFSID()).to.equal('2-ALM')
|
|
62
|
+
expect(trip.getHeadsign()).to.equal('Camberwell')
|
|
63
|
+
expect(trip.getOperationDays()).to.deep.equal(['20241122'])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('Should return a Smartrak bus trip instance where appropriate', async () => {
|
|
67
|
+
let calendars = {
|
|
68
|
+
'T2_2': new GTFSCalendar({
|
|
69
|
+
id: 'T2_2',
|
|
70
|
+
startDay: '20241122',
|
|
71
|
+
endDay: '20241122',
|
|
72
|
+
daysOfWeek: ["0","0","0","0","1","0","0"]
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let routeMappings = { '43-490-aus-1': '4-490', '6-wr9-mjp-1': '6-wr9' }
|
|
77
|
+
|
|
78
|
+
let reader = new GTFSTripReader(busTripsFile, calendars, routeMappings, TRANSIT_MODES.bus)
|
|
79
|
+
await reader.open()
|
|
80
|
+
|
|
81
|
+
let trip = await reader.getNextEntity()
|
|
82
|
+
|
|
83
|
+
expect(trip).to.be.instanceOf(SmartrakTrip)
|
|
84
|
+
expect(trip.getTripID()).to.equal('43-490--1-MF4-1111914')
|
|
85
|
+
expect(trip.getRouteGTFSID()).to.equal('4-490')
|
|
86
|
+
expect(trip.getDepotID()).to.equal(43)
|
|
87
|
+
|
|
88
|
+
trip = await reader.getNextEntity()
|
|
89
|
+
|
|
90
|
+
expect(trip).to.not.be.instanceOf(SmartrakTrip)
|
|
91
|
+
expect(trip.getTripID()).to.equal('5.T3.6-wr9-mjp-1.2.R')
|
|
92
|
+
expect(trip.getRouteGTFSID()).to.equal('6-wr9')
|
|
93
|
+
|
|
94
|
+
// EXPERIMENTAL DATA:
|
|
95
|
+
trip = await reader.getNextEntity()
|
|
96
|
+
|
|
97
|
+
expect(trip).to.be.instanceOf(SmartrakTrip)
|
|
98
|
+
expect(trip.getTripID()).to.equal('58-G01--1-MF1-9942214')
|
|
99
|
+
expect(trip.getDepotID()).to.equal(58)
|
|
100
|
+
// expect(trip.getRouteGTFSID()).to.equal('6-G01') // Not entirely sure how to handle duplicates
|
|
101
|
+
|
|
102
|
+
trip = await reader.getNextEntity()
|
|
103
|
+
|
|
104
|
+
expect(trip).to.be.instanceOf(SmartrakTrip)
|
|
105
|
+
expect(trip.getTripID()).to.equal('57-83--1-MF2-57586310')
|
|
106
|
+
expect(trip.getDepotID()).to.equal(57)
|
|
107
|
+
// expect(trip.getRouteGTFSID()).to.equal('6-083')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { expect } from 'chai'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import url from 'url'
|
|
4
|
+
import CSVLineReader from '../lib/gtfs-parser/readers/line-reader.mjs'
|
|
5
|
+
|
|
6
|
+
const __filename = url.fileURLToPath(import.meta.url)
|
|
7
|
+
const __dirname = path.dirname(__filename)
|
|
8
|
+
|
|
9
|
+
const stopsFile = path.join(__dirname, 'sample-data', 'gtfs-splitting', 'line_reader_test.txt')
|
|
10
|
+
const oneLineFile = path.join(__dirname, 'sample-data', 'one_line.csv')
|
|
11
|
+
|
|
12
|
+
describe('The CSVLineReader class', () => {
|
|
13
|
+
it('Should open a CSV file and identify the headers', async () => {
|
|
14
|
+
let reader = new CSVLineReader(stopsFile)
|
|
15
|
+
await reader.open()
|
|
16
|
+
|
|
17
|
+
expect(reader.getHeaders()).to.deep.equal([
|
|
18
|
+
'stop_id', 'stop_name', 'stop_lat', 'stop_lon'
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
await reader.close()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('Should return the first line of the CSV, with the fields mapped to their headers', async () => {
|
|
25
|
+
let reader = new CSVLineReader(stopsFile)
|
|
26
|
+
await reader.open()
|
|
27
|
+
|
|
28
|
+
let line = await reader.nextLine()
|
|
29
|
+
expect(line).to.deep.equal({
|
|
30
|
+
stop_id: '1000',
|
|
31
|
+
stop_name: 'Dole Ave/Cheddar Rd (Reservoir)',
|
|
32
|
+
stop_lat: '-37.7007748061827',
|
|
33
|
+
stop_lon: '145.018951051008'
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
await reader.close()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('Should be able to peek if a line is available', async () => {
|
|
40
|
+
let reader = new CSVLineReader(stopsFile)
|
|
41
|
+
await reader.open()
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < 6; i++) await reader.nextLine()
|
|
44
|
+
|
|
45
|
+
expect(reader.available()).to.be.true
|
|
46
|
+
expect(await reader.nextLine()).to.deep.equal({
|
|
47
|
+
stop_id: '10011',
|
|
48
|
+
stop_name: 'Moffat St/Main Rd West (St Albans)',
|
|
49
|
+
stop_lat: '-37.7423246041343',
|
|
50
|
+
stop_lon: '144.783466504334'
|
|
51
|
+
})
|
|
52
|
+
expect(reader.available()).to.be.false
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('Should be unable to unread a single line from the file', async () => {
|
|
56
|
+
let reader = new CSVLineReader(stopsFile)
|
|
57
|
+
await reader.open()
|
|
58
|
+
let expectedLine = {
|
|
59
|
+
stop_id: '1000',
|
|
60
|
+
stop_name: 'Dole Ave/Cheddar Rd (Reservoir)',
|
|
61
|
+
stop_lat: '-37.7007748061827',
|
|
62
|
+
stop_lon: '145.018951051008'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let line = await reader.nextLine()
|
|
66
|
+
expect(line).to.deep.equal(expectedLine)
|
|
67
|
+
reader.unreadLine()
|
|
68
|
+
|
|
69
|
+
expect(await reader.nextLine()).to.deep.equal(expectedLine)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('Should be unable to unread the last line from the file, then read it again', async () => {
|
|
73
|
+
let reader = new CSVLineReader(oneLineFile)
|
|
74
|
+
|
|
75
|
+
await reader.open()
|
|
76
|
+
let expectedLine = {
|
|
77
|
+
trip_id: '1.T2.2-ALM-vpt-1.1.R'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
expect(reader.available()).to.be.true
|
|
81
|
+
let line = await reader.nextLine()
|
|
82
|
+
expect(line).to.deep.equal(expectedLine)
|
|
83
|
+
|
|
84
|
+
expect(reader.available(), 'No more data should be available').to.be.false
|
|
85
|
+
|
|
86
|
+
reader.unreadLine()
|
|
87
|
+
|
|
88
|
+
expect(reader.available(), 'More data should be available after unreading the last line').to.be.true
|
|
89
|
+
|
|
90
|
+
expect(await reader.nextLine()).to.deep.equal(expectedLine)
|
|
91
|
+
|
|
92
|
+
expect(reader.available()).to.be.false
|
|
93
|
+
})
|
|
94
|
+
})
|