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 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 stopQuery = omit(query, [
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.6.0",
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.3.0",
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": "^8.0.3",
98
- "lint-staged": "^15.2.0",
99
- "mocha": "^10.2.0",
100
- "prettier": "^3.2.4",
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
  });
@@ -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
  });