@transportme/load-ptv-gtfs 1.4.2 → 1.4.4
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/lib/gtfs-parser/GTFSRoute.mjs +22 -4
- package/lib/gtfs-parser/GTFSTrip.mjs +23 -1
- package/lib/gtfs-parser/readers/GTFSCalendarReader.mjs +1 -0
- package/lib/gtfs-parser/readers/line-reader.mjs +1 -6
- package/lib/loader/RouteLoader.mjs +4 -2
- package/lib/loader/ShapeLoader.mjs +3 -2
- package/lib/loader/StopsLoader.mjs +4 -2
- package/lib/loader/TripLoader.mjs +13 -4
- package/lib/post-processing/set-route-stops.mjs +12 -3
- package/package.json +1 -1
- package/.github/workflows/unit-test.yml +0 -46
- package/test/gtfs-calendar.test.mjs +0 -182
- package/test/gtfs-route-loader.test.mjs +0 -208
- package/test/gtfs-routes.test.mjs +0 -77
- package/test/gtfs-shapes-loader.test.mjs +0 -105
- package/test/gtfs-shapes.test.mjs +0 -23
- package/test/gtfs-stop-loader.test.mjs +0 -415
- package/test/gtfs-stop-times.test.mjs +0 -37
- package/test/gtfs-stops.test.mjs +0 -311
- package/test/gtfs-trip-loader.test.mjs +0 -188
- package/test/gtfs-trips.test.mjs +0 -108
- package/test/line-reader.test.mjs +0 -120
- package/test/merge-stops-full.test.mjs +0 -118
- package/test/merge-stops.test.mjs +0 -376
- package/test/new-metro-loader-test.test.mjs +0 -244
- package/test/new-tram-format.test.mjs +0 -49
- package/test/newer-metro-loader-test.test.mjs +0 -80
- package/test/newer-tram-format.test.mjs +0 -67
- package/test/ptv-gtfs.test.mjs +0 -106
- package/test/sample-data/calendar/calendar.txt +0 -34
- package/test/sample-data/calendar/calendar_dates.txt +0 -26
- package/test/sample-data/gtfs-splitting/force_unique_stops_bus.txt +0 -7
- package/test/sample-data/gtfs-splitting/force_unique_stops_tram.txt +0 -3
- package/test/sample-data/gtfs-splitting/general-stores.txt +0 -19
- package/test/sample-data/gtfs-splitting/line_reader_test.txt +0 -8
- package/test/sample-data/gtfs-splitting/name_overrides.txt +0 -4
- package/test/sample-data/gtfs-splitting/sample_stops.txt +0 -20
- package/test/sample-data/gtfs-splitting/special_chars_stops_csv.txt +0 -6
- package/test/sample-data/gtfs-splitting/stops_without_secondary.txt +0 -6
- package/test/sample-data/gtfs-splitting/union_with_spaces.txt +0 -5
- package/test/sample-data/gtfs-unzipping/gtfs.zip +0 -0
- package/test/sample-data/merge/shapes.txt +0 -1224
- package/test/sample-data/merge/stop_times.txt +0 -103
- package/test/sample-data/merge/stops.txt +0 -39
- package/test/sample-data/merge/trips.txt +0 -7
- package/test/sample-data/mtm-rail/stops.txt +0 -4
- package/test/sample-data/new-metro/calendar.txt +0 -13
- package/test/sample-data/new-metro/calendar_dates.txt +0 -1
- package/test/sample-data/new-metro/routes.txt +0 -18
- package/test/sample-data/new-metro/simple_stop_test.txt +0 -6
- package/test/sample-data/new-metro/stop_times.txt +0 -64
- package/test/sample-data/new-metro/stops.txt +0 -139
- package/test/sample-data/new-metro/trips.txt +0 -6
- package/test/sample-data/new-metro-2/calendar.txt +0 -13
- package/test/sample-data/new-metro-2/calendar_dates.txt +0 -1
- package/test/sample-data/new-metro-2/routes.txt +0 -2
- package/test/sample-data/new-metro-2/stop_times.txt +0 -5
- package/test/sample-data/new-metro-2/stops.txt +0 -9
- package/test/sample-data/new-metro-2/trips.txt +0 -2
- package/test/sample-data/new-tram/routes.txt +0 -25
- package/test/sample-data/new-tram/stops.txt +0 -5
- package/test/sample-data/new-tram-2/routes.txt +0 -25
- package/test/sample-data/new-tram-2/stops.txt +0 -8
- package/test/sample-data/one_line.csv +0 -2
- package/test/sample-data/parent-stop/multi_stop.txt +0 -4
- package/test/sample-data/parent-stop/stops.txt +0 -15
- package/test/sample-data/routes/agency.txt +0 -12
- package/test/sample-data/routes/bad_routes.txt +0 -4
- package/test/sample-data/routes/metro_bus_routes.txt +0 -4
- package/test/sample-data/routes/metro_lines.txt +0 -8
- package/test/sample-data/routes/regional_bus_routes.txt +0 -7
- package/test/sample-data/routes/vline.txt +0 -7
- package/test/sample-data/routes/west_gipp_transit.txt +0 -20
- package/test/sample-data/shapes/shapes.txt +0 -89
- package/test/sample-data/shapes/stop_times.txt +0 -9
- package/test/sample-data/shapes/stops.txt +0 -4
- package/test/sample-data/shapes/trips.txt +0 -4
- package/test/sample-data/stop-data/name-overrides.json +0 -25
- package/test/sample-data/stop-data/unique-stops.json +0 -1
- package/test/sample-data/suburbs.json +0 -77
- package/test/sample-data/trips/bus_trips.txt +0 -5
- package/test/sample-data/trips/stop_times.txt +0 -29
- package/test/sample-data/trips/stops.txt +0 -93
- package/test/sample-data/trips/trips.txt +0 -5
- package/test/utils-test.mjs +0 -12
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { LokiDatabaseConnection } from '@transportme/database'
|
|
2
|
-
import StopsLoader from '../lib/loader/StopsLoader.mjs'
|
|
3
|
-
import { TRANSIT_MODES } from '../lib/constants.mjs'
|
|
4
|
-
import path from 'path'
|
|
5
|
-
import url from 'url'
|
|
6
|
-
import { expect } from 'chai'
|
|
7
|
-
import suburbs from './sample-data/suburbs.json' with { type: 'json' }
|
|
8
|
-
import RouteLoader from '../lib/loader/RouteLoader.mjs'
|
|
9
|
-
import TripLoader from '../lib/loader/TripLoader.mjs'
|
|
10
|
-
import GTFSCalendar from '../lib/gtfs-parser/GTFSCalendar.mjs'
|
|
11
|
-
import { MetroTrip } from '../lib/gtfs-parser/GTFSTrip.mjs'
|
|
12
|
-
|
|
13
|
-
const __filename = url.fileURLToPath(import.meta.url)
|
|
14
|
-
const __dirname = path.dirname(__filename)
|
|
15
|
-
|
|
16
|
-
const agencyFile = path.join(__dirname, 'sample-data', 'routes', 'agency.txt')
|
|
17
|
-
|
|
18
|
-
const calendarFile = path.join(__dirname, 'sample-data', 'new-metro', 'calendar.txt')
|
|
19
|
-
const calendarDatesFile = path.join(__dirname, 'sample-data', 'new-metro', 'calendar_dates.txt')
|
|
20
|
-
|
|
21
|
-
const routesFile = path.join(__dirname, 'sample-data', 'new-metro', 'routes.txt')
|
|
22
|
-
|
|
23
|
-
const stopTimesFile = path.join(__dirname, 'sample-data', 'new-metro', 'stop_times.txt')
|
|
24
|
-
const tripsFile = path.join(__dirname, 'sample-data', 'new-metro', 'trips.txt')
|
|
25
|
-
const stopsFile = path.join(__dirname, 'sample-data', 'new-metro', 'stops.txt')
|
|
26
|
-
|
|
27
|
-
const simpleStopsFile = path.join(__dirname, 'sample-data', 'new-metro', 'simple_stop_test.txt')
|
|
28
|
-
|
|
29
|
-
describe('The GTFS Loaders with the new Metro data', () => {
|
|
30
|
-
describe('The stop loader', () => {
|
|
31
|
-
it('Should process the stops and add them to the database', async () => {
|
|
32
|
-
let database = new LokiDatabaseConnection('test-db')
|
|
33
|
-
let stops = await database.createCollection('gtfs-stops')
|
|
34
|
-
|
|
35
|
-
let loader = new StopsLoader(simpleStopsFile, suburbs, TRANSIT_MODES.metroTrain, database)
|
|
36
|
-
await loader.loadStops()
|
|
37
|
-
|
|
38
|
-
let stop = await stops.findDocument({
|
|
39
|
-
'stopName': 'Camberwell Railway Station'
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
expect(stop).to.not.be.null
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('Should label the individual platforms with their platform numbers', async () => {
|
|
46
|
-
let database = new LokiDatabaseConnection('test-db')
|
|
47
|
-
let stops = await database.createCollection('gtfs-stops')
|
|
48
|
-
|
|
49
|
-
let loader = new StopsLoader(simpleStopsFile, suburbs, TRANSIT_MODES.metroTrain, database)
|
|
50
|
-
await loader.loadStops()
|
|
51
|
-
|
|
52
|
-
let stop = await stops.findDocument({
|
|
53
|
-
'stopName': 'Camberwell Railway Station'
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
let platformIDs = ["11207", "11208", "11209"]
|
|
57
|
-
let platforms = platformIDs.map(id => stop.bays.find(bay => bay.stopGTFSID === id))
|
|
58
|
-
|
|
59
|
-
for (let i = 0; i < platformIDs.length; i++) {
|
|
60
|
-
let platform = platforms[i]
|
|
61
|
-
expect(platform).to.not.be.undefined
|
|
62
|
-
expect(platforms[i].platform).to.equal((i + 1).toString())
|
|
63
|
-
}
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
describe('The route loader', () => {
|
|
68
|
-
it('Should convert the updated route ID format to the legacy format', async () => {
|
|
69
|
-
let database = new LokiDatabaseConnection('test-db')
|
|
70
|
-
let routes = await database.createCollection('gtfs-routes')
|
|
71
|
-
|
|
72
|
-
let routeLoader = new RouteLoader(routesFile, agencyFile, TRANSIT_MODES.metroTrain, database)
|
|
73
|
-
await routeLoader.loadRoutes()
|
|
74
|
-
|
|
75
|
-
let pakenham = await routes.findDocument({ routeName: 'Pakenham' })
|
|
76
|
-
|
|
77
|
-
expect(pakenham).to.exist
|
|
78
|
-
expect(pakenham.routeGTFSID).to.equal('2-PKM')
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
describe('The trip loader', () => {
|
|
83
|
-
it('It should accept trip IDs in the new format and extract the TDN from there', async () => {
|
|
84
|
-
let database = new LokiDatabaseConnection('test-db')
|
|
85
|
-
let stops = await database.createCollection('gtfs-stops')
|
|
86
|
-
let routes = await database.createCollection('gtfs-routes')
|
|
87
|
-
let trips = await database.createCollection('gtfs-gtfs timetables')
|
|
88
|
-
|
|
89
|
-
let stopLoader = new StopsLoader(stopsFile, suburbs, TRANSIT_MODES.metroTrain, database)
|
|
90
|
-
await stopLoader.loadStops()
|
|
91
|
-
|
|
92
|
-
let routeLoader = new RouteLoader(routesFile, agencyFile, TRANSIT_MODES.metroTrain, database)
|
|
93
|
-
await routeLoader.loadRoutes()
|
|
94
|
-
|
|
95
|
-
let routeIDMap = routeLoader.getRouteIDMap()
|
|
96
|
-
|
|
97
|
-
let tripLoader = new TripLoader({
|
|
98
|
-
tripsFile, stopTimesFile,
|
|
99
|
-
calendarFile, calendarDatesFile
|
|
100
|
-
}, TRANSIT_MODES.metroTrain, database)
|
|
101
|
-
|
|
102
|
-
await tripLoader.loadTrips({ routeIDMap })
|
|
103
|
-
|
|
104
|
-
let trip = await trips.findDocument({ runID: 'C000' })
|
|
105
|
-
|
|
106
|
-
expect(trip).to.not.be.null
|
|
107
|
-
expect(trip.tripID).to.equal('02-PKM--12-T5-C000')
|
|
108
|
-
expect(trip.shapeID).to.equal('2-PKM-vpt-12.20.R')
|
|
109
|
-
expect(trip.routeGTFSID).to.equal('2-PKM')
|
|
110
|
-
expect(trip.block).to.equal('3347')
|
|
111
|
-
expect(trip.isRailReplacementBus).to.be.false
|
|
112
|
-
expect(trip.direction).to.equal('Up')
|
|
113
|
-
|
|
114
|
-
expect(trip.stopTimings[0].stopName).to.equal('East Pakenham Railway Station')
|
|
115
|
-
expect(trip.stopTimings[0].platform).to.equal('1')
|
|
116
|
-
|
|
117
|
-
expect(trip.stopTimings[22].stopName).to.equal('Richmond Railway Station')
|
|
118
|
-
expect(trip.stopTimings[22].platform).to.equal('5')
|
|
119
|
-
|
|
120
|
-
expect(trip.stopTimings[27].stopName).to.equal('Flinders Street Railway Station')
|
|
121
|
-
expect(trip.stopTimings[27].platform).to.equal('7')
|
|
122
|
-
|
|
123
|
-
let nextTrip = await trips.findDocument({ block: '3347', runID: { $ne: 'C000' } })
|
|
124
|
-
expect(nextTrip.runID).to.equal('C005')
|
|
125
|
-
expect(nextTrip.direction).to.equal('Down')
|
|
126
|
-
expect(trip.isRailReplacementBus).to.be.false
|
|
127
|
-
|
|
128
|
-
expect(nextTrip.stopTimings[0].stopName).to.equal('Flinders Street Railway Station')
|
|
129
|
-
expect(nextTrip.stopTimings[0].platform).to.equal('7')
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('It identify a rail replacement bus trip and mark it as such', async () => {
|
|
133
|
-
let database = new LokiDatabaseConnection('test-db')
|
|
134
|
-
let stops = await database.createCollection('gtfs-stops')
|
|
135
|
-
let routes = await database.createCollection('gtfs-routes')
|
|
136
|
-
let trips = await database.createCollection('gtfs-gtfs timetables')
|
|
137
|
-
|
|
138
|
-
let stopLoader = new StopsLoader(stopsFile, suburbs, TRANSIT_MODES.metroTrain, database)
|
|
139
|
-
await stopLoader.loadStops()
|
|
140
|
-
|
|
141
|
-
let routeLoader = new RouteLoader(routesFile, agencyFile, TRANSIT_MODES.metroTrain, database)
|
|
142
|
-
await routeLoader.loadRoutes()
|
|
143
|
-
|
|
144
|
-
let routeIDMap = routeLoader.getRouteIDMap()
|
|
145
|
-
|
|
146
|
-
let tripLoader = new TripLoader({
|
|
147
|
-
tripsFile, stopTimesFile,
|
|
148
|
-
calendarFile, calendarDatesFile
|
|
149
|
-
}, TRANSIT_MODES.metroTrain, database)
|
|
150
|
-
|
|
151
|
-
await tripLoader.loadTrips({ routeIDMap })
|
|
152
|
-
|
|
153
|
-
let trip = await trips.findDocument({ runID: 'BL000' })
|
|
154
|
-
|
|
155
|
-
expect(trip).to.not.be.null
|
|
156
|
-
expect(trip.tripID).to.equal('02-LIL--13-T5_w3-BL000')
|
|
157
|
-
expect(trip.shapeID).to.equal('2-LIL-vpt-13.133.R')
|
|
158
|
-
expect(trip.routeGTFSID).to.equal('2-LIL')
|
|
159
|
-
expect(trip.block).to.equal(null)
|
|
160
|
-
expect(trip.isRailReplacementBus).to.be.true
|
|
161
|
-
expect(trip.direction).to.equal('Up')
|
|
162
|
-
|
|
163
|
-
expect(trip.stopTimings[0].stopName).to.equal('Burnley Railway Station')
|
|
164
|
-
expect(trip.stopTimings[0].platform).to.equal('RRB')
|
|
165
|
-
|
|
166
|
-
expect(trip.stopTimings[3].stopName).to.equal('Parliament Railway Station')
|
|
167
|
-
expect(trip.stopTimings[3].platform).to.equal('RRB')
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('Should wrap times past midnight back to 00:00 NEXT DAY in the trip times', async () => {
|
|
171
|
-
let database = new LokiDatabaseConnection('test-db')
|
|
172
|
-
let stops = await database.createCollection('gtfs-stops')
|
|
173
|
-
let routes = await database.createCollection('gtfs-routes')
|
|
174
|
-
let trips = await database.createCollection('gtfs-gtfs timetables')
|
|
175
|
-
|
|
176
|
-
let stopLoader = new StopsLoader(stopsFile, suburbs, TRANSIT_MODES.metroTrain, database)
|
|
177
|
-
await stopLoader.loadStops()
|
|
178
|
-
|
|
179
|
-
let routeLoader = new RouteLoader(routesFile, agencyFile, TRANSIT_MODES.metroTrain, database)
|
|
180
|
-
await routeLoader.loadRoutes()
|
|
181
|
-
|
|
182
|
-
let routeIDMap = routeLoader.getRouteIDMap()
|
|
183
|
-
|
|
184
|
-
let tripLoader = new TripLoader({
|
|
185
|
-
tripsFile, stopTimesFile,
|
|
186
|
-
calendarFile, calendarDatesFile
|
|
187
|
-
}, TRANSIT_MODES.metroTrain, database)
|
|
188
|
-
|
|
189
|
-
await tripLoader.loadTrips({ routeIDMap })
|
|
190
|
-
|
|
191
|
-
let trip = await trips.findDocument({ runID: 'C999' })
|
|
192
|
-
|
|
193
|
-
expect(trip).to.not.be.null
|
|
194
|
-
expect(trip.departureTime).to.equal('23:59')
|
|
195
|
-
expect(trip.destinationArrivalTime).to.equal('24:10')
|
|
196
|
-
|
|
197
|
-
expect(trip.stopTimings[0].departureTime).to.equal('23:59')
|
|
198
|
-
expect(trip.stopTimings[1].departureTime).to.equal('00:05')
|
|
199
|
-
expect(trip.stopTimings[1].arrivalTime).to.equal('00:05')
|
|
200
|
-
expect(trip.stopTimings[2].arrivalTime).to.equal('00:10')
|
|
201
|
-
})
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
describe('The MetroTrip subclass', () => {
|
|
205
|
-
it('Should detect rail buses', () => {
|
|
206
|
-
let calendar = new GTFSCalendar({
|
|
207
|
-
id: 'T1',
|
|
208
|
-
startDay: '20241122',
|
|
209
|
-
endDay: '20241129',
|
|
210
|
-
daysOfWeek: ["0","0","0","0","1","1","0"]
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
expect(new MetroTrip({
|
|
214
|
-
routeGTFSID: '2-PKM',
|
|
215
|
-
calendar,
|
|
216
|
-
id: '02-PKM--54-T6-C962',
|
|
217
|
-
shapeID: '2-PKM-vpt-54.70.R',
|
|
218
|
-
headsign: 'South Yarra',
|
|
219
|
-
direction: '0',
|
|
220
|
-
block: ''
|
|
221
|
-
}).getTripData().isRailReplacementBus).to.be.false
|
|
222
|
-
|
|
223
|
-
expect(new MetroTrip({
|
|
224
|
-
routeGTFSID: '2-PKM',
|
|
225
|
-
calendar,
|
|
226
|
-
id: '02-PKM-R-6-T5_bp-BF400',
|
|
227
|
-
shapeID: '2-PKM-R-vpt-6.7.R',
|
|
228
|
-
headsign: 'South Yarra',
|
|
229
|
-
direction: '0',
|
|
230
|
-
block: ''
|
|
231
|
-
}).getTripData().isRailReplacementBus).to.be.true
|
|
232
|
-
|
|
233
|
-
expect(new MetroTrip({
|
|
234
|
-
routeGTFSID: '2-PKM',
|
|
235
|
-
calendar,
|
|
236
|
-
id: '02-PKM-R-6-T5_bp-BF22',
|
|
237
|
-
shapeID: '2-PKM-R-vpt-6.7.R',
|
|
238
|
-
headsign: 'South Yarra',
|
|
239
|
-
direction: '0',
|
|
240
|
-
block: ''
|
|
241
|
-
}).getTripData().isRailReplacementBus).to.be.true
|
|
242
|
-
})
|
|
243
|
-
})
|
|
244
|
-
})
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { LokiDatabaseConnection } from '@transportme/database'
|
|
2
|
-
import StopsLoader from '../lib/loader/StopsLoader.mjs'
|
|
3
|
-
import { TRANSIT_MODES } from '../lib/constants.mjs'
|
|
4
|
-
import path from 'path'
|
|
5
|
-
import url from 'url'
|
|
6
|
-
import { expect } from 'chai'
|
|
7
|
-
import suburbs from './sample-data/suburbs.json' with { type: 'json' }
|
|
8
|
-
import RouteLoader from '../lib/loader/RouteLoader.mjs'
|
|
9
|
-
|
|
10
|
-
const __filename = url.fileURLToPath(import.meta.url)
|
|
11
|
-
const __dirname = path.dirname(__filename)
|
|
12
|
-
|
|
13
|
-
const agencyFile = path.join(__dirname, 'sample-data', 'routes', 'agency.txt')
|
|
14
|
-
|
|
15
|
-
const routesFile = path.join(__dirname, 'sample-data', 'new-tram', 'routes.txt')
|
|
16
|
-
const stopsFile = path.join(__dirname, 'sample-data', 'new-tram', 'stops.txt')
|
|
17
|
-
|
|
18
|
-
describe('The GTFS Loaders with the new Tram data', () => {
|
|
19
|
-
describe('The stop loader', () => {
|
|
20
|
-
it('Should process the stops and add them to the database', async () => {
|
|
21
|
-
let database = new LokiDatabaseConnection('test-db')
|
|
22
|
-
let stops = await database.createCollection('gtfs-stops')
|
|
23
|
-
|
|
24
|
-
let loader = new StopsLoader(stopsFile, suburbs, TRANSIT_MODES.tram, database)
|
|
25
|
-
await loader.loadStops()
|
|
26
|
-
|
|
27
|
-
let stop = await stops.findDocument({
|
|
28
|
-
'stopName': 'Glenferrie Road/Wattletree Road'
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
expect(stop).to.not.be.null
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
describe('The route loader', () => {
|
|
36
|
-
it('Should convert the updated route ID format to the legacy format', async () => {
|
|
37
|
-
let database = new LokiDatabaseConnection('test-db')
|
|
38
|
-
let routes = await database.createCollection('gtfs-routes')
|
|
39
|
-
|
|
40
|
-
let routeLoader = new RouteLoader(routesFile, agencyFile, TRANSIT_MODES.tram, database)
|
|
41
|
-
await routeLoader.loadRoutes()
|
|
42
|
-
|
|
43
|
-
let route109 = await routes.findDocument({ routeNumber: '109' })
|
|
44
|
-
|
|
45
|
-
expect(route109).to.exist
|
|
46
|
-
expect(route109.routeGTFSID).to.equal('3-109')
|
|
47
|
-
})
|
|
48
|
-
})
|
|
49
|
-
})
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { LokiDatabaseConnection } from '@transportme/database'
|
|
2
|
-
import StopsLoader from '../lib/loader/StopsLoader.mjs'
|
|
3
|
-
import { TRANSIT_MODES } from '../lib/constants.mjs'
|
|
4
|
-
import path from 'path'
|
|
5
|
-
import url from 'url'
|
|
6
|
-
import { expect } from 'chai'
|
|
7
|
-
import suburbs from './sample-data/suburbs.json' with { type: 'json' }
|
|
8
|
-
import RouteLoader from '../lib/loader/RouteLoader.mjs'
|
|
9
|
-
import TripLoader from '../lib/loader/TripLoader.mjs'
|
|
10
|
-
|
|
11
|
-
const __filename = url.fileURLToPath(import.meta.url)
|
|
12
|
-
const __dirname = path.dirname(__filename)
|
|
13
|
-
|
|
14
|
-
const agencyFile = path.join(__dirname, 'sample-data', 'routes', 'agency.txt')
|
|
15
|
-
|
|
16
|
-
const calendarFile = path.join(__dirname, 'sample-data', 'new-metro-2', 'calendar.txt')
|
|
17
|
-
const calendarDatesFile = path.join(__dirname, 'sample-data', 'new-metro-2', 'calendar_dates.txt')
|
|
18
|
-
|
|
19
|
-
const routesFile = path.join(__dirname, 'sample-data', 'new-metro-2', 'routes.txt')
|
|
20
|
-
|
|
21
|
-
const stopTimesFile = path.join(__dirname, 'sample-data', 'new-metro-2', 'stop_times.txt')
|
|
22
|
-
const tripsFile = path.join(__dirname, 'sample-data', 'new-metro-2', 'trips.txt')
|
|
23
|
-
const stopsFile = path.join(__dirname, 'sample-data', 'new-metro-2', 'stops.txt')
|
|
24
|
-
|
|
25
|
-
describe('The GTFS Loaders with the new 230525 Metro data', () => {
|
|
26
|
-
describe('The route loader', () => {
|
|
27
|
-
it('Should convert the updated route ID format to the legacy format', async () => {
|
|
28
|
-
let database = new LokiDatabaseConnection('test-db')
|
|
29
|
-
let routes = await database.createCollection('gtfs-routes')
|
|
30
|
-
|
|
31
|
-
let routeLoader = new RouteLoader(routesFile, agencyFile, TRANSIT_MODES.metroTrain, database)
|
|
32
|
-
await routeLoader.loadRoutes()
|
|
33
|
-
|
|
34
|
-
let williamstown = await routes.findDocument({ routeGTFSID: '2-WIL' })
|
|
35
|
-
|
|
36
|
-
expect(williamstown).to.exist
|
|
37
|
-
expect(williamstown.routeName).to.equal('Williamstown')
|
|
38
|
-
})
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
describe('The trip loader', () => {
|
|
42
|
-
it('It identify a rail replacement bus trip and mark it as such', async () => {
|
|
43
|
-
let database = new LokiDatabaseConnection('test-db')
|
|
44
|
-
let stops = await database.createCollection('gtfs-stops')
|
|
45
|
-
let routes = await database.createCollection('gtfs-routes')
|
|
46
|
-
let trips = await database.createCollection('gtfs-gtfs timetables')
|
|
47
|
-
|
|
48
|
-
let stopLoader = new StopsLoader(stopsFile, suburbs, TRANSIT_MODES.metroTrain, database)
|
|
49
|
-
await stopLoader.loadStops()
|
|
50
|
-
|
|
51
|
-
let routeLoader = new RouteLoader(routesFile, agencyFile, TRANSIT_MODES.metroTrain, database)
|
|
52
|
-
await routeLoader.loadRoutes()
|
|
53
|
-
|
|
54
|
-
let routeIDMap = routeLoader.getRouteIDMap()
|
|
55
|
-
|
|
56
|
-
let tripLoader = new TripLoader({
|
|
57
|
-
tripsFile, stopTimesFile,
|
|
58
|
-
calendarFile, calendarDatesFile
|
|
59
|
-
}, TRANSIT_MODES.metroTrain, database)
|
|
60
|
-
|
|
61
|
-
await tripLoader.loadTrips({ routeIDMap })
|
|
62
|
-
|
|
63
|
-
let trip = await trips.findDocument({ runID: 'BW000' })
|
|
64
|
-
|
|
65
|
-
expect(trip).to.not.be.null
|
|
66
|
-
expect(trip.tripID).to.equal('02-WIL-R-7-T6-BW000')
|
|
67
|
-
expect(trip.shapeID).to.equal('2-WIL-R-vpt-7.1.R')
|
|
68
|
-
expect(trip.routeGTFSID).to.equal('2-WIL')
|
|
69
|
-
expect(trip.block).to.equal(null)
|
|
70
|
-
expect(trip.isRailReplacementBus).to.be.true
|
|
71
|
-
expect(trip.direction).to.equal('Up')
|
|
72
|
-
|
|
73
|
-
expect(trip.stopTimings[0].stopName).to.equal('Williamstown Railway Station')
|
|
74
|
-
expect(trip.stopTimings[0].platform).to.equal('RRB')
|
|
75
|
-
|
|
76
|
-
expect(trip.stopTimings[3].stopName).to.equal('Newport Railway Station')
|
|
77
|
-
expect(trip.stopTimings[3].platform).to.equal('RRB')
|
|
78
|
-
})
|
|
79
|
-
})
|
|
80
|
-
})
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { LokiDatabaseConnection } from '@transportme/database'
|
|
2
|
-
import StopsLoader from '../lib/loader/StopsLoader.mjs'
|
|
3
|
-
import { TRANSIT_MODES } from '../lib/constants.mjs'
|
|
4
|
-
import path from 'path'
|
|
5
|
-
import url from 'url'
|
|
6
|
-
import { expect } from 'chai'
|
|
7
|
-
import suburbs from './sample-data/suburbs.json' with { type: 'json' }
|
|
8
|
-
import RouteLoader from '../lib/loader/RouteLoader.mjs'
|
|
9
|
-
|
|
10
|
-
const __filename = url.fileURLToPath(import.meta.url)
|
|
11
|
-
const __dirname = path.dirname(__filename)
|
|
12
|
-
|
|
13
|
-
const agencyFile = path.join(__dirname, 'sample-data', 'routes', 'agency.txt')
|
|
14
|
-
|
|
15
|
-
const routesFile = path.join(__dirname, 'sample-data', 'new-tram-2', 'routes.txt')
|
|
16
|
-
const stopsFile = path.join(__dirname, 'sample-data', 'new-tram-2', 'stops.txt')
|
|
17
|
-
|
|
18
|
-
describe('The GTFS Loaders with the new Tram data', () => {
|
|
19
|
-
describe('The stop loader', () => {
|
|
20
|
-
it('Should process the stops and add them to the database', async () => {
|
|
21
|
-
let database = new LokiDatabaseConnection('test-db')
|
|
22
|
-
let stops = await database.createCollection('gtfs-stops')
|
|
23
|
-
|
|
24
|
-
let loader = new StopsLoader(stopsFile, suburbs, TRANSIT_MODES.tram, database)
|
|
25
|
-
await loader.loadStops()
|
|
26
|
-
|
|
27
|
-
let stop = await stops.findDocument({
|
|
28
|
-
'stopName': 'Glenferrie Road/Wattletree Road'
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
expect(stop).to.not.be.null
|
|
32
|
-
expect(stop.bays[0].stopNumber).to.equal('45')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('Should clean up extra spaces', async () => {
|
|
36
|
-
let database = new LokiDatabaseConnection('test-db')
|
|
37
|
-
let stops = await database.createCollection('gtfs-stops')
|
|
38
|
-
|
|
39
|
-
let loader = new StopsLoader(stopsFile, suburbs, TRANSIT_MODES.tram, database)
|
|
40
|
-
await loader.loadStops()
|
|
41
|
-
|
|
42
|
-
let stop = await stops.findDocument({
|
|
43
|
-
'stopName': 'Glen Huntly Railway Station/Glen Huntly Road'
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
expect(stop).to.not.be.null
|
|
47
|
-
expect(stop.bays[0].stopNumber).to.equal('61')
|
|
48
|
-
expect(stop.bays[0].originalName).to.equal('Glen Huntly Railway Station/Glen Huntly Rd #61')
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('Should remove half suburbs in the name', async () => {
|
|
52
|
-
let database = new LokiDatabaseConnection('test-db')
|
|
53
|
-
let stops = await database.createCollection('gtfs-stops')
|
|
54
|
-
|
|
55
|
-
let loader = new StopsLoader(stopsFile, suburbs, TRANSIT_MODES.tram, database)
|
|
56
|
-
await loader.loadStops()
|
|
57
|
-
|
|
58
|
-
let stop = await stops.findDocument({
|
|
59
|
-
'stopName': 'Royal Melbourne Hospital-Parkville Railway Station/Royal Parade'
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
expect(stop).to.not.be.null
|
|
63
|
-
expect(stop.bays[0].stopNumber).to.equal('10')
|
|
64
|
-
expect(stop.bays[0].originalName).to.equal('Royal Melbourne Hospital-Parkville Station/Royal Pde #10')
|
|
65
|
-
})
|
|
66
|
-
})
|
|
67
|
-
})
|
package/test/ptv-gtfs.test.mjs
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { expect } from 'chai'
|
|
2
|
-
import nock from 'nock'
|
|
3
|
-
import PTVGTFS from '../lib/gtfs-package/PTVGTFS.mjs'
|
|
4
|
-
import path from 'path'
|
|
5
|
-
import url from 'url'
|
|
6
|
-
import { dir as tmpdir } from 'tmp-promise'
|
|
7
|
-
import fs from 'fs/promises'
|
|
8
|
-
|
|
9
|
-
const __filename = url.fileURLToPath(import.meta.url)
|
|
10
|
-
const __dirname = path.dirname(__filename)
|
|
11
|
-
|
|
12
|
-
let gtfsHost = 'http://data.ptv.vic.gov.au'
|
|
13
|
-
let gtfsURL = '/downloads/gtfs.zip'
|
|
14
|
-
|
|
15
|
-
let responseHeaders = {
|
|
16
|
-
'last-modified': 'Fri, 12 Jul 2024 02:25:21 GMT',
|
|
17
|
-
'content-length': '165927454',
|
|
18
|
-
'content-type': 'application/zip'
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const stubGTFSUnzipping = (await fs.readFile(path.join(__dirname, 'sample-data', 'gtfs-unzipping', 'gtfs.zip')))
|
|
22
|
-
|
|
23
|
-
async function downloadGTFS() {
|
|
24
|
-
nock(gtfsHost).get(gtfsURL).reply(200, stubGTFSUnzipping)
|
|
25
|
-
let tmp = await tmpdir({ unsafeCleanup: true })
|
|
26
|
-
|
|
27
|
-
let gtfs = new PTVGTFS(gtfsHost + gtfsURL)
|
|
28
|
-
await gtfs.download(tmp.path)
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
gtfs, tmp
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe('The PTVGTFS class', () => {
|
|
36
|
-
describe('The getPublishedDate function', () => {
|
|
37
|
-
it('Should read the last-modified header and return that', async () => {
|
|
38
|
-
nock(gtfsHost).head(gtfsURL).reply(200, '', responseHeaders)
|
|
39
|
-
|
|
40
|
-
let gtfs = new PTVGTFS(gtfsHost + gtfsURL)
|
|
41
|
-
let publishedDate = await gtfs.getPublishedDate()
|
|
42
|
-
|
|
43
|
-
expect(publishedDate.toISOString()).to.equal('2024-07-12T02:25:21.000Z')
|
|
44
|
-
})
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
describe('The download function', () => {
|
|
48
|
-
it('Should save the gtfs file to the given folder, with the filename as gtfs.zip', async () => {
|
|
49
|
-
let { gtfs, tmp } = await downloadGTFS()
|
|
50
|
-
|
|
51
|
-
let stat = await fs.stat(path.join(tmp.path, 'gtfs.zip'))
|
|
52
|
-
expect(stat.size).to.equal(23408)
|
|
53
|
-
|
|
54
|
-
await tmp.cleanup()
|
|
55
|
-
})
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
describe('The unzip function', () => {
|
|
59
|
-
it('Should throw and error if the file has not been downloaded first', async () => {
|
|
60
|
-
let gtfs = new PTVGTFS(gtfsHost + gtfsURL)
|
|
61
|
-
expect((await gtfs.unzip().catch(e => e)).message).to.equal('Need to download GTFS file first!')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('Should unzip gtfs.zip, producing the numbered folders', async () => {
|
|
65
|
-
let { gtfs, tmp } = await downloadGTFS()
|
|
66
|
-
await gtfs.unzip()
|
|
67
|
-
|
|
68
|
-
let stat = await fs.stat(path.join(tmp.path, '1', 'google_transit.zip'))
|
|
69
|
-
expect(stat).to.not.be.null
|
|
70
|
-
|
|
71
|
-
await tmp.cleanup()
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('Should unzip the google_transit.zip files in the numbered folders as well', async () => {
|
|
75
|
-
let { gtfs, tmp } = await downloadGTFS()
|
|
76
|
-
await gtfs.unzip()
|
|
77
|
-
|
|
78
|
-
for (let i = 1; i <= 11; i++) {
|
|
79
|
-
if (i === 9) continue
|
|
80
|
-
let stat = await fs.stat(path.join(tmp.path, i.toString(), 'stops.txt'))
|
|
81
|
-
expect(stat).to.not.be.null
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
await tmp.cleanup()
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
describe('The cleanup function', () => {
|
|
89
|
-
it('Should remove the zip files, keeping the CSV files intact', async () => {
|
|
90
|
-
let { gtfs, tmp } = await downloadGTFS()
|
|
91
|
-
await gtfs.unzip()
|
|
92
|
-
await gtfs.cleanup()
|
|
93
|
-
|
|
94
|
-
let mainStat = await fs.stat(path.join(tmp.path, 'gtfs.zip')).catch(e => e)
|
|
95
|
-
expect(mainStat).to.be.instanceof(Error)
|
|
96
|
-
|
|
97
|
-
for (let i = 1; i <= 11; i++) {
|
|
98
|
-
if (i === 9) continue
|
|
99
|
-
let stat = await fs.stat(path.join(tmp.path, i.toString(), 'google_transit.zip')).catch(e => e)
|
|
100
|
-
expect(stat).to.be.instanceof(Error)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
await tmp.cleanup()
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
})
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date
|
|
2
|
-
"T2","0","0","0","0","0","1","0","20241108","20241114"
|
|
3
|
-
"T2_1","0","0","0","0","0","1","0","20241115","20241121"
|
|
4
|
-
"T2_2","0","0","0","0","0","1","0","20241122","20250209"
|
|
5
|
-
"T3","0","0","0","0","0","0","1","20241108","20241114"
|
|
6
|
-
"T3_1","0","0","0","0","0","0","1","20241115","20241121"
|
|
7
|
-
"T3_2","0","0","0","0","0","0","1","20241122","20250209"
|
|
8
|
-
"T5","1","1","1","1","0","0","0","20241108","20241114"
|
|
9
|
-
"T5_1","1","1","1","1","0","0","0","20241115","20241121"
|
|
10
|
-
"T5_2","1","1","1","1","0","0","0","20241122","20250209"
|
|
11
|
-
"T5+n5","0","0","0","1","0","0","0","20241108","20241114"
|
|
12
|
-
"T5+n6","1","1","1","0","0","0","0","20241108","20241114"
|
|
13
|
-
"T5+n7","1","1","0","1","0","0","0","20241108","20241114"
|
|
14
|
-
"T5+n8","0","0","1","0","0","0","0","20241108","20241114"
|
|
15
|
-
"T5+n9","0","1","1","1","0","0","0","20241108","20241114"
|
|
16
|
-
"T5+o0","1","0","0","0","0","0","0","20241108","20241114"
|
|
17
|
-
"T5+o1","1","1","0","0","0","0","0","20241108","20241114"
|
|
18
|
-
"T5+o2","0","0","1","1","0","0","0","20241108","20241114"
|
|
19
|
-
"T5+o3","0","1","1","0","0","0","0","20241108","20241114"
|
|
20
|
-
"T5+o4","0","1","1","1","0","0","0","20241115","20241121"
|
|
21
|
-
"T5+o5","1","0","0","0","0","0","0","20241115","20241121"
|
|
22
|
-
"T5+o6","0","0","1","1","0","0","0","20241115","20241121"
|
|
23
|
-
"T5+o7","1","1","0","0","0","0","0","20241115","20241121"
|
|
24
|
-
"T5+o8","1","1","0","1","0","0","0","20241115","20241121"
|
|
25
|
-
"T5+o9","0","0","1","0","0","0","0","20241115","20241121"
|
|
26
|
-
"T5+p0","1","1","1","0","0","0","0","20241115","20241121"
|
|
27
|
-
"T5+p1","0","0","0","1","0","0","0","20241115","20241121"
|
|
28
|
-
"T5+p2","0","1","0","0","0","0","0","20241115","20241121"
|
|
29
|
-
"T6","0","0","0","0","1","0","0","20241108","20241114"
|
|
30
|
-
"T6_1","0","0","0","0","1","0","0","20241115","20241121"
|
|
31
|
-
"T6_2","0","0","0","0","1","0","0","20241122","20250209"
|
|
32
|
-
"UI","0","0","0","1","0","0","0","20241122","20250209"
|
|
33
|
-
"UL","0","0","1","0","0","0","0","20241122","20250209"
|
|
34
|
-
"MF10-49-601-aus","1","1","1","1","1","0","0","20241122","20250223"
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
service_id,date,exception_type
|
|
2
|
-
"T3_2","20250126","2"
|
|
3
|
-
"T5_2","20241225","2"
|
|
4
|
-
"T5_2","20241226","2"
|
|
5
|
-
"T5_2","20250101","2"
|
|
6
|
-
"T5_2","20250127","2"
|
|
7
|
-
"UI","20241128","2"
|
|
8
|
-
"UI","20241205","2"
|
|
9
|
-
"UI","20241212","2"
|
|
10
|
-
"UI","20241219","2"
|
|
11
|
-
"UI","20250102","2"
|
|
12
|
-
"UI","20250109","2"
|
|
13
|
-
"UI","20250116","2"
|
|
14
|
-
"UI","20250123","2"
|
|
15
|
-
"UI","20250130","2"
|
|
16
|
-
"UI","20250206","2"
|
|
17
|
-
"UL","20241127","2"
|
|
18
|
-
"UL","20241204","2"
|
|
19
|
-
"UL","20241211","2"
|
|
20
|
-
"UL","20241218","2"
|
|
21
|
-
"UL","20250101","2"
|
|
22
|
-
"UL","20250108","2"
|
|
23
|
-
"UL","20250115","2"
|
|
24
|
-
"UL","20250122","2"
|
|
25
|
-
"UL","20250129","2"
|
|
26
|
-
"UL","20250205","2"
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
stop_id,stop_name,stop_lat,stop_lon
|
|
2
|
-
"16830","Melbourne University/Royal Pde (Parkville)","-37.7984297818238","144.957855928775"
|
|
3
|
-
"28670","Melbourne University/Grattan St (Carlton)","-37.8001309323072","144.961897703257"
|
|
4
|
-
"28671","Melbourne University/Grattan St (Carlton)","-37.8002400593444","144.962996363755"
|
|
5
|
-
"28673","Melbourne University/Grattan St (Carlton)","-37.8000452422121","144.960071535123"
|
|
6
|
-
"28675","Melbourne University/Grattan St (Carlton)","-37.800110297337","144.960705753974"
|
|
7
|
-
"87","Melbourne University/Swanston St (Carlton)","-37.7999508835265","144.963992391723"
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
stop_id,stop_name,stop_lat,stop_lon
|
|
2
|
-
"15117","General Store/Murray Valley Hwy (Torrumbarry)","-36.0289328143973","144.516565858212"
|
|
3
|
-
"17124","General Store/Murray St (Piangil)","-35.0542205869387","143.310581971833"
|
|
4
|
-
"17125","General Store/Murray St (Piangil)","-35.0538456443862","143.310697481574"
|
|
5
|
-
"17729","General Store/Macs St (Peterborough)","-38.6070683334915","142.87719698365"
|
|
6
|
-
"17730","General Store/Macs St (Peterborough)","-38.6070003726619","142.877568193123"
|
|
7
|
-
"17977","General Store/Sunraysia Hwy (Tempy)","-35.3453146606652","142.424802508117"
|
|
8
|
-
"17978","General Store/Sunraysia Hwy (Tempy)","-35.344983598941","142.424623331801"
|
|
9
|
-
"36974","General Store/Monash Ave (Nyah West)","-35.1852359469295","143.345799631341"
|
|
10
|
-
"37899","General Store/Princes Hwy (Johnsonville)","-37.8200787799472","147.831089593645"
|
|
11
|
-
"37901","General Store/Princes Hwy (Swan Reach)","-37.8219848899712","147.856891682107"
|
|
12
|
-
"37902","General Store/Princes Hwy (Swan Reach)","-37.822175385113","147.85672345214"
|
|
13
|
-
"37921","General Store/Main St (Bruthen)","-37.7082084114992","147.832470628437"
|
|
14
|
-
"37987","General Store/Phillip Island Rd (Surf Beach)","-38.5078845836624","145.29112542319"
|
|
15
|
-
"38000","General Store/Phillip Island Rd (Surf Beach)","-38.5077808366616","145.291426050202"
|
|
16
|
-
"42284","General Store/Ararat - St Arnaud Rd (Navarre)","-36.8998475986824","143.113535821369"
|
|
17
|
-
"43916","General Store/Champ Elysees Esp (Coronet Bay)","-38.4376225731191","145.436193608845"
|
|
18
|
-
"44217","General Store/Great Ocean Rd (Nullawarre)","-38.469533626788","142.749879594372"
|
|
19
|
-
"47879","General Store/Ararat-St Arnaud Rd (Navarre)","-36.8998967921621","143.113387501491"
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
stop_id,stop_name,stop_lat,stop_lon
|
|
2
|
-
"1000","Dole Ave/Cheddar Rd (Reservoir)","-37.7007748061827","145.018951051008"
|
|
3
|
-
"10001","Rex St/Taylors Rd (Kings Park)","-37.7269752097338","144.776152425766"
|
|
4
|
-
"10002","Yuille St/Centenary Ave (Melton)","-37.676159502415","144.595789405034"
|
|
5
|
-
"10009","Gum Rd/Main Rd West (Albanvale)","-37.7414971143105","144.775899388911"
|
|
6
|
-
"1001","Lloyd Ave/Cheddar Rd (Reservoir)","-37.6991830099559","145.019685286526"
|
|
7
|
-
"10010","Kings Rd/Main Rd West (St Albans)","-37.7419455261301","144.78008474429"
|
|
8
|
-
"10011","Moffat St/Main Rd West (St Albans)","-37.7423246041343","144.783466504334"
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
stop_id,stop_name,stop_lat,stop_lon
|
|
2
|
-
"49016","Ballarat Bus Interchange (Soldiers Hill)","-37.5583880170257","143.859870505315"
|
|
3
|
-
"49017","Ballarat Bus Interchange (Soldiers Hill)","-37.558622046041","143.860200212639"
|
|
4
|
-
"49020","Ballarat Bus Interchange (Soldiers Hill)","-37.558397045775","143.860209663324"
|