gtfs 4.6.0 → 4.7.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/CHANGELOG.md +10 -0
- package/README.md +29 -0
- package/lib/gtfs/stops.js +29 -6
- package/lib/utils.js +41 -5
- package/package.json +8 -7
- package/test/mocha/get-stops-as-geojson.js +20 -0
- package/test/mocha/get-stops.js +82 -8
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [4.7.0] - 2024-02-09
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added `bounding_box_side_m` option to `getStops` and `getStopsAsGeoJSON` to find all stops within a bounding box.
|
|
13
|
+
|
|
14
|
+
### Updated
|
|
15
|
+
|
|
16
|
+
- Dependency updates
|
|
17
|
+
|
|
8
18
|
## [4.6.0] - 2024-01-17
|
|
9
19
|
|
|
10
20
|
### Changed
|
package/README.md
CHANGED
|
@@ -737,6 +737,24 @@ const stops = getStops({
|
|
|
737
737
|
const stops = getStops({
|
|
738
738
|
shape_id: 'cal_sf_tam',
|
|
739
739
|
});
|
|
740
|
+
|
|
741
|
+
/*
|
|
742
|
+
* `getStops` allows passing a `bounding_box_side_m` value in the options
|
|
743
|
+
* parameter object. If included, it will return all stops within a square
|
|
744
|
+
* bounding box around the `stop_lat` and `stop_lon` parameters passed to
|
|
745
|
+
* the query using the size in meters specified.
|
|
746
|
+
*/
|
|
747
|
+
const stops = getStops(
|
|
748
|
+
{
|
|
749
|
+
stop_lat: 37.58764,
|
|
750
|
+
stop_lon: -122.36265
|
|
751
|
+
},
|
|
752
|
+
[],
|
|
753
|
+
[],
|
|
754
|
+
{
|
|
755
|
+
bounding_box_side_m: 1000
|
|
756
|
+
}
|
|
757
|
+
);
|
|
740
758
|
```
|
|
741
759
|
|
|
742
760
|
#### getStopsAsGeoJSON(query, options)
|
|
@@ -753,6 +771,17 @@ const stopsGeojson = getStopsAsGeoJSON();
|
|
|
753
771
|
const stopsGeojson = getStopsAsGeoJSON({
|
|
754
772
|
route_id: 'Lo-16APR',
|
|
755
773
|
});
|
|
774
|
+
|
|
775
|
+
// Get all stops within a 1000m bounding box as geoJSON
|
|
776
|
+
const stopsGeojson = getStopsAsGeoJSON(
|
|
777
|
+
{
|
|
778
|
+
stop_lat: 37.58764,
|
|
779
|
+
stop_lon: -122.36265
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
bounding_box_side_m: 1000
|
|
783
|
+
}
|
|
784
|
+
);
|
|
756
785
|
```
|
|
757
786
|
|
|
758
787
|
#### getStoptimes(query, fields, sortBy, options)
|
package/lib/gtfs/stops.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
formatOrderByClause,
|
|
8
8
|
formatSelectClause,
|
|
9
9
|
formatWhereClause,
|
|
10
|
+
formatWhereClauseBoundingBox,
|
|
10
11
|
formatWhereClauses,
|
|
11
12
|
} from '../utils.js';
|
|
12
13
|
import { stopsToGeoJSON } from '../geojson-utils.js';
|
|
@@ -21,7 +22,7 @@ function buildTripSubquery(query) {
|
|
|
21
22
|
|
|
22
23
|
function buildStoptimeSubquery(query) {
|
|
23
24
|
return `SELECT DISTINCT stop_id FROM stop_times WHERE trip_id IN (${buildTripSubquery(
|
|
24
|
-
query
|
|
25
|
+
query,
|
|
25
26
|
)})`;
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -39,13 +40,21 @@ export function getStops(query = {}, fields = [], orderBy = [], options = {}) {
|
|
|
39
40
|
let whereClause = '';
|
|
40
41
|
const orderByClause = formatOrderByClause(orderBy);
|
|
41
42
|
|
|
42
|
-
const
|
|
43
|
+
const stopQueryOmitKeys = [
|
|
43
44
|
'route_id',
|
|
44
45
|
'trip_id',
|
|
45
46
|
'service_id',
|
|
46
47
|
'direction_id',
|
|
47
48
|
'shape_id',
|
|
48
|
-
]
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// If bounding_box_side_m is defined, search for stops inside a bounding box so omit `stop_lat` and `stop_lon`.
|
|
52
|
+
if (options.bounding_box_side_m !== undefined) {
|
|
53
|
+
stopQueryOmitKeys.push('stop_lat', 'stop_lon');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let stopQuery = omit(query, stopQueryOmitKeys);
|
|
57
|
+
|
|
49
58
|
const tripQuery = pick(query, [
|
|
50
59
|
'route_id',
|
|
51
60
|
'trip_id',
|
|
@@ -55,9 +64,23 @@ export function getStops(query = {}, fields = [], orderBy = [], options = {}) {
|
|
|
55
64
|
]);
|
|
56
65
|
|
|
57
66
|
const whereClauses = Object.entries(stopQuery).map(([key, value]) =>
|
|
58
|
-
formatWhereClause(key, value)
|
|
67
|
+
formatWhereClause(key, value),
|
|
59
68
|
);
|
|
60
69
|
|
|
70
|
+
if (
|
|
71
|
+
options.bounding_box_side_m !== undefined &&
|
|
72
|
+
query.stop_lat !== undefined &&
|
|
73
|
+
query.stop_lon !== undefined
|
|
74
|
+
) {
|
|
75
|
+
whereClauses.push(
|
|
76
|
+
formatWhereClauseBoundingBox(
|
|
77
|
+
query.stop_lat,
|
|
78
|
+
query.stop_lon,
|
|
79
|
+
options.bounding_box_side_m,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
61
84
|
if (Object.values(tripQuery).length > 0) {
|
|
62
85
|
whereClauses.push(`stop_id IN (${buildStoptimeSubquery(tripQuery)})`);
|
|
63
86
|
}
|
|
@@ -68,7 +91,7 @@ export function getStops(query = {}, fields = [], orderBy = [], options = {}) {
|
|
|
68
91
|
|
|
69
92
|
return db
|
|
70
93
|
.prepare(
|
|
71
|
-
`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause}
|
|
94
|
+
`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`,
|
|
72
95
|
)
|
|
73
96
|
.all();
|
|
74
97
|
}
|
|
@@ -96,7 +119,7 @@ export function getStopsAsGeoJSON(query = {}, options = {}) {
|
|
|
96
119
|
const stopAttributes = getStopAttributes({ stop_id: stop.stop_id });
|
|
97
120
|
|
|
98
121
|
stop.routes = orderBy(routes, (route) =>
|
|
99
|
-
Number.parseInt(route.route_short_name, 10)
|
|
122
|
+
Number.parseInt(route.route_short_name, 10),
|
|
100
123
|
);
|
|
101
124
|
stop.agency_name = agencies[0].agency_name;
|
|
102
125
|
|
package/lib/utils.js
CHANGED
|
@@ -12,7 +12,7 @@ export function validateConfigForImport(config) {
|
|
|
12
12
|
for (const [index, agency] of config.agencies.entries()) {
|
|
13
13
|
if (!agency.path && !agency.url) {
|
|
14
14
|
throw new Error(
|
|
15
|
-
`No Agency \`url\` or \`path\` specified in config for agency index ${index}
|
|
15
|
+
`No Agency \`url\` or \`path\` specified in config for agency index ${index}.`,
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -75,7 +75,7 @@ export function formatSelectClause(fields) {
|
|
|
75
75
|
|
|
76
76
|
const selectItem = Object.entries(fields)
|
|
77
77
|
.map(
|
|
78
|
-
(key) => `${sqlString.escapeId(key[0])} AS ${sqlString.escapeId(key[1])}
|
|
78
|
+
(key) => `${sqlString.escapeId(key[0])} AS ${sqlString.escapeId(key[1])}`,
|
|
79
79
|
)
|
|
80
80
|
.join(', ');
|
|
81
81
|
return `SELECT ${selectItem}`;
|
|
@@ -86,12 +86,48 @@ export function formatJoinClause(joinObject) {
|
|
|
86
86
|
.map(
|
|
87
87
|
(data) =>
|
|
88
88
|
`${data.type ? data.type + ' JOIN' : 'INNER JOIN'} ${sqlString.escapeId(
|
|
89
|
-
data.table
|
|
90
|
-
)} ON ${data.on}
|
|
89
|
+
data.table,
|
|
90
|
+
)} ON ${data.on}`,
|
|
91
91
|
)
|
|
92
92
|
.join(' ');
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
function degree2radian(angle) {
|
|
96
|
+
return (angle * Math.PI) / 180;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function radian2degree(angle) {
|
|
100
|
+
return (angle / Math.PI) * 180;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/*
|
|
104
|
+
* Search inside GPS coordinates boundary box
|
|
105
|
+
* Distance unit: meters
|
|
106
|
+
* */
|
|
107
|
+
export function formatWhereClauseBoundingBox(
|
|
108
|
+
latitudeDegree,
|
|
109
|
+
longitudeDegree,
|
|
110
|
+
boundingBoxSideMeters,
|
|
111
|
+
) {
|
|
112
|
+
const earthRadius = 6371000;
|
|
113
|
+
latitudeDegree = parseFloat(latitudeDegree);
|
|
114
|
+
longitudeDegree = parseFloat(longitudeDegree);
|
|
115
|
+
|
|
116
|
+
const latitudeRadian = degree2radian(latitudeDegree);
|
|
117
|
+
const radiusFromLatitude = Math.cos(latitudeRadian) * earthRadius;
|
|
118
|
+
|
|
119
|
+
// `boundingBoxSideMeters` is divided by 2 we are centering the coordinates in the middle of this square
|
|
120
|
+
const deltaLatitude = radian2degree(boundingBoxSideMeters / 2 / earthRadius);
|
|
121
|
+
const deltaLongitude = radian2degree(
|
|
122
|
+
boundingBoxSideMeters / 2 / radiusFromLatitude,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
let query = `stop_lat BETWEEN ${latitudeDegree - deltaLatitude} AND ${latitudeDegree + deltaLatitude}`;
|
|
126
|
+
query += ` AND stop_lon BETWEEN ${longitudeDegree - deltaLongitude} AND ${longitudeDegree + deltaLongitude}`;
|
|
127
|
+
|
|
128
|
+
return query;
|
|
129
|
+
}
|
|
130
|
+
|
|
95
131
|
export function formatWhereClause(key, value) {
|
|
96
132
|
if (Array.isArray(value)) {
|
|
97
133
|
let whereClause = `${sqlString.escapeId(key)} IN (${value
|
|
@@ -119,7 +155,7 @@ export function formatWhereClauses(query) {
|
|
|
119
155
|
}
|
|
120
156
|
|
|
121
157
|
const whereClauses = Object.entries(query).map(([key, value]) =>
|
|
122
|
-
formatWhereClause(key, value)
|
|
158
|
+
formatWhereClause(key, value),
|
|
123
159
|
);
|
|
124
160
|
return `WHERE ${whereClauses.join(' AND ')}`;
|
|
125
161
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gtfs",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"description": "Import GTFS transit data into SQLite and query routes, stops, times, fares and more",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"transit",
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
"Daniel Sörlöv",
|
|
59
59
|
"Ali Zarghami <alizarghami@gmail.com>",
|
|
60
60
|
"David Abell",
|
|
61
|
-
"Matthias Feist <matze@matf.de>"
|
|
61
|
+
"Matthias Feist <matze@matf.de>",
|
|
62
|
+
"Oliv4945"
|
|
62
63
|
],
|
|
63
64
|
"type": "module",
|
|
64
65
|
"main": "index.js",
|
|
@@ -73,7 +74,7 @@
|
|
|
73
74
|
},
|
|
74
75
|
"dependencies": {
|
|
75
76
|
"@turf/helpers": "^6.5.0",
|
|
76
|
-
"better-sqlite3": "^9.
|
|
77
|
+
"better-sqlite3": "^9.4.0",
|
|
77
78
|
"csv-parse": "^5.5.3",
|
|
78
79
|
"csv-stringify": "^6.4.5",
|
|
79
80
|
"gtfs-realtime-bindings": "^1.1.1",
|
|
@@ -94,10 +95,10 @@
|
|
|
94
95
|
"yoctocolors": "^1.0.0"
|
|
95
96
|
},
|
|
96
97
|
"devDependencies": {
|
|
97
|
-
"husky": "^
|
|
98
|
-
"lint-staged": "^15.2.
|
|
99
|
-
"mocha": "^10.
|
|
100
|
-
"prettier": "^3.2.
|
|
98
|
+
"husky": "^9.0.10",
|
|
99
|
+
"lint-staged": "^15.2.2",
|
|
100
|
+
"mocha": "^10.3.0",
|
|
101
|
+
"prettier": "^3.2.5",
|
|
101
102
|
"should": "^13.2.3"
|
|
102
103
|
},
|
|
103
104
|
"engines": {
|
|
@@ -64,4 +64,24 @@ describe('getStopsAsGeoJSON(): ', () => {
|
|
|
64
64
|
should.exist(geojson.features[0].geometry.coordinates);
|
|
65
65
|
geojson.features[0].geometry.coordinates.length.should.equal(2);
|
|
66
66
|
});
|
|
67
|
+
|
|
68
|
+
it('should return geojson with specific stops for bounding box query', () => {
|
|
69
|
+
const distance = 100;
|
|
70
|
+
const stopLatitude = 37.709538;
|
|
71
|
+
const stopLongitude = -122.401586;
|
|
72
|
+
|
|
73
|
+
const geojson = getStopsAsGeoJSON(
|
|
74
|
+
{
|
|
75
|
+
stop_lat: stopLatitude,
|
|
76
|
+
stop_lon: stopLongitude,
|
|
77
|
+
},
|
|
78
|
+
{ bounding_box_side_m: distance },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
should.exist(geojson);
|
|
82
|
+
geojson.type.should.equal('FeatureCollection');
|
|
83
|
+
geojson.features.length.should.equal(2);
|
|
84
|
+
should.exist(geojson.features[0].geometry.coordinates);
|
|
85
|
+
geojson.features[0].geometry.coordinates.length.should.equal(2);
|
|
86
|
+
});
|
|
67
87
|
});
|
package/test/mocha/get-stops.js
CHANGED
|
@@ -92,7 +92,7 @@ describe('getStops():', () => {
|
|
|
92
92
|
route_id: routeId,
|
|
93
93
|
},
|
|
94
94
|
[],
|
|
95
|
-
[['stop_id', 'ASC']]
|
|
95
|
+
[['stop_id', 'ASC']],
|
|
96
96
|
);
|
|
97
97
|
|
|
98
98
|
const expectedStopIds = [
|
|
@@ -127,7 +127,7 @@ describe('getStops():', () => {
|
|
|
127
127
|
for (const [idx, stop] of results.entries()) {
|
|
128
128
|
expectedStopIds[idx].should.equal(
|
|
129
129
|
stop.stop_id,
|
|
130
|
-
'The order of stops are expected to be the same'
|
|
130
|
+
'The order of stops are expected to be the same',
|
|
131
131
|
);
|
|
132
132
|
}
|
|
133
133
|
});
|
|
@@ -142,7 +142,7 @@ describe('getStops():', () => {
|
|
|
142
142
|
direction_id: directionId,
|
|
143
143
|
},
|
|
144
144
|
[],
|
|
145
|
-
[['stop_id', 'ASC']]
|
|
145
|
+
[['stop_id', 'ASC']],
|
|
146
146
|
);
|
|
147
147
|
|
|
148
148
|
const expectedStopIds = [
|
|
@@ -165,7 +165,7 @@ describe('getStops():', () => {
|
|
|
165
165
|
for (const [idx, stop] of results.entries()) {
|
|
166
166
|
expectedStopIds[idx].should.equal(
|
|
167
167
|
stop.stop_id,
|
|
168
|
-
'The order of stops are expected to be the same'
|
|
168
|
+
'The order of stops are expected to be the same',
|
|
169
169
|
);
|
|
170
170
|
}
|
|
171
171
|
});
|
|
@@ -178,7 +178,7 @@ describe('getStops():', () => {
|
|
|
178
178
|
trip_id: tripId,
|
|
179
179
|
},
|
|
180
180
|
[],
|
|
181
|
-
[['stop_id', 'ASC']]
|
|
181
|
+
[['stop_id', 'ASC']],
|
|
182
182
|
);
|
|
183
183
|
|
|
184
184
|
const expectedStopIds = [
|
|
@@ -213,7 +213,7 @@ describe('getStops():', () => {
|
|
|
213
213
|
for (const [idx, stop] of results.entries()) {
|
|
214
214
|
expectedStopIds[idx].should.equal(
|
|
215
215
|
stop.stop_id,
|
|
216
|
-
'The order of stops are expected to be the same'
|
|
216
|
+
'The order of stops are expected to be the same',
|
|
217
217
|
);
|
|
218
218
|
}
|
|
219
219
|
});
|
|
@@ -226,7 +226,7 @@ describe('getStops():', () => {
|
|
|
226
226
|
shape_id: shapeId,
|
|
227
227
|
},
|
|
228
228
|
[],
|
|
229
|
-
[['stop_id', 'ASC']]
|
|
229
|
+
[['stop_id', 'ASC']],
|
|
230
230
|
);
|
|
231
231
|
|
|
232
232
|
const expectedStopIds = [
|
|
@@ -262,8 +262,82 @@ describe('getStops():', () => {
|
|
|
262
262
|
for (const [idx, stop] of results.entries()) {
|
|
263
263
|
expectedStopIds[idx].should.equal(
|
|
264
264
|
stop.stop_id,
|
|
265
|
-
'The order of stops are expected to be the same'
|
|
265
|
+
'The order of stops are expected to be the same',
|
|
266
266
|
);
|
|
267
267
|
}
|
|
268
268
|
});
|
|
269
|
+
|
|
270
|
+
it('should return array of stops for bounding box query', () => {
|
|
271
|
+
const distance = 100;
|
|
272
|
+
const stopLatitude = 37.709538;
|
|
273
|
+
const stopLongitude = -122.401586;
|
|
274
|
+
|
|
275
|
+
const results = getStops(
|
|
276
|
+
{
|
|
277
|
+
stop_lat: stopLatitude,
|
|
278
|
+
stop_lon: stopLongitude,
|
|
279
|
+
},
|
|
280
|
+
[],
|
|
281
|
+
[],
|
|
282
|
+
{ bounding_box_side_m: distance },
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const expectedResult = [
|
|
286
|
+
{
|
|
287
|
+
stop_id: 'ctba',
|
|
288
|
+
stop_code: null,
|
|
289
|
+
stop_name: 'Bayshore Caltrain',
|
|
290
|
+
tts_stop_name: null,
|
|
291
|
+
stop_desc: null,
|
|
292
|
+
stop_lat: 37.709544,
|
|
293
|
+
stop_lon: -122.401318,
|
|
294
|
+
zone_id: null,
|
|
295
|
+
stop_url: 'http://www.caltrain.com/stations/bayshorestation.html',
|
|
296
|
+
location_type: 1,
|
|
297
|
+
parent_station: null,
|
|
298
|
+
stop_timezone: null,
|
|
299
|
+
wheelchair_boarding: 1,
|
|
300
|
+
level_id: null,
|
|
301
|
+
platform_code: null,
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
stop_id: '70032',
|
|
305
|
+
stop_code: '70032',
|
|
306
|
+
stop_name: 'Bayshore Caltrain',
|
|
307
|
+
tts_stop_name: null,
|
|
308
|
+
stop_desc: null,
|
|
309
|
+
stop_lat: 37.709544,
|
|
310
|
+
stop_lon: -122.40198,
|
|
311
|
+
zone_id: '1',
|
|
312
|
+
stop_url: 'http://www.caltrain.com/stations/bayshorestation.html',
|
|
313
|
+
location_type: 0,
|
|
314
|
+
parent_station: 'ctba',
|
|
315
|
+
stop_timezone: null,
|
|
316
|
+
wheelchair_boarding: 1,
|
|
317
|
+
level_id: null,
|
|
318
|
+
platform_code: 'SB',
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
stop_id: '70031',
|
|
322
|
+
stop_code: '70031',
|
|
323
|
+
stop_name: 'Bayshore Caltrain',
|
|
324
|
+
tts_stop_name: null,
|
|
325
|
+
stop_desc: null,
|
|
326
|
+
stop_lat: 37.709537,
|
|
327
|
+
stop_lon: -122.401586,
|
|
328
|
+
zone_id: '1',
|
|
329
|
+
stop_url: 'http://www.caltrain.com/stations/bayshorestation.html',
|
|
330
|
+
location_type: 0,
|
|
331
|
+
parent_station: 'ctba',
|
|
332
|
+
stop_timezone: null,
|
|
333
|
+
wheelchair_boarding: 1,
|
|
334
|
+
level_id: null,
|
|
335
|
+
platform_code: 'NB',
|
|
336
|
+
},
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
should.exist(results);
|
|
340
|
+
results.length.should.equal(3);
|
|
341
|
+
results.should.match(expectedResult);
|
|
342
|
+
});
|
|
269
343
|
});
|