transit-departures-widget 2.0.0 → 2.1.1

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,34 @@ 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
+ ## [2.1.1] - 2023-12-04
9
+
10
+ ## Fixed
11
+
12
+ - Fix for routes with no directions
13
+
14
+ ## Updated
15
+
16
+ - Dependency updates
17
+
18
+ ## [2.1.0] - 2023-09-13
19
+
20
+ ## Changed
21
+
22
+ - Remove last stop of each route
23
+ - Improved autocomplete sorting
24
+ - Use stop_code as value in autocomplete
25
+ - Populate route dropdown on page load
26
+
27
+ ## Fixed
28
+
29
+ - Fix for grouping child stops
30
+ - Handle case with no departures
31
+
32
+ ## Updated
33
+
34
+ - Dependency updates
35
+
8
36
  ## [2.0.0] - 2023-09-14
9
37
 
10
38
  ## Changed
package/README.md CHANGED
@@ -52,9 +52,10 @@ An demo of the widget is available at https://transit-departures-widget.blinktag
52
52
 
53
53
  ## Current Usage
54
54
 
55
- The following transit agencies use `transit-departures-widget` as the departures tool on their websites:
55
+ The following transit agencies use `transit-departures-widget` on their websites:
56
56
 
57
57
  - [Marin Transit](https://marintransit.org/)
58
+ - [MVgo](https://mvgo.org/)
58
59
 
59
60
  ## Command Line Usage
60
61
 
package/lib/utils.js CHANGED
@@ -126,6 +126,9 @@ function getStopsForDirection(route, direction, config) {
126
126
  return memo
127
127
  }, [])
128
128
 
129
+ // Remove last stop of route since boarding is not allowed
130
+ deduplicatedStopIds.pop()
131
+
129
132
  // Fetch stop details
130
133
  const stops = getStops({ stop_id: deduplicatedStopIds }, [
131
134
  'stop_id',
@@ -162,7 +165,7 @@ export function generateTransitDeparturesWidgetHtml(config) {
162
165
  config.logWarning(
163
166
  `route_id ${route.route_id} has no directions - skipping`,
164
167
  )
165
- return
168
+ continue
166
169
  }
167
170
 
168
171
  for (const direction of directions) {
@@ -181,23 +184,29 @@ export function generateTransitDeparturesWidgetHtml(config) {
181
184
  filteredRoutes.push(route)
182
185
  }
183
186
 
184
- // Sort twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']
187
+ // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']
185
188
  const sortedRoutes = sortBy(
186
189
  sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),
187
190
  (route) => Number.parseInt(route.route_short_name, 10),
188
191
  )
189
192
 
190
- // Sort unique list of stops and indicate parent stations
191
- const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')
192
- const parentStationIds = new Set(
193
- sortedStops.map((stop) => stop.parent_station),
193
+ // Get Parent Station Stops
194
+ const parentStationIds = new Set(stops.map((stop) => stop.parent_station))
195
+
196
+ const parentStationStops = getStops(
197
+ { stop_id: Array.from(parentStationIds) },
198
+ ['stop_id', 'stop_name', 'stop_code', 'parent_station'],
194
199
  )
195
200
 
196
- for (const stop of sortedStops) {
197
- if (parentStationIds.has(stop.stop_id)) {
201
+ stops.push(
202
+ ...parentStationStops.map((stop) => {
198
203
  stop.is_parent_station = true
199
- }
200
- }
204
+ return stop
205
+ }),
206
+ )
207
+
208
+ // Sort unique list of stops
209
+ const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')
201
210
 
202
211
  const templateVars = {
203
212
  __: i18n.__,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "transit-departures-widget",
3
3
  "description": "Build a realtime transit departures tool from GTFS and GTFS-Realtime.",
4
- "version": "2.0.0",
4
+ "version": "2.1.1",
5
5
  "keywords": [
6
6
  "transit",
7
7
  "gtfs",
@@ -31,9 +31,9 @@
31
31
  "dependencies": {
32
32
  "copy-dir": "^1.3.0",
33
33
  "express": "^4.18.2",
34
- "gtfs": "^4.4.3",
34
+ "gtfs": "^4.5.1",
35
35
  "i18n": "^0.15.1",
36
- "js-beautify": "^1.14.9",
36
+ "js-beautify": "^1.14.11",
37
37
  "lodash-es": "^4.17.21",
38
38
  "morgan": "^1.10.0",
39
39
  "pretty-error": "^4.0.0",
@@ -48,8 +48,8 @@
48
48
  },
49
49
  "devDependencies": {
50
50
  "husky": "^8.0.3",
51
- "lint-staged": "^13.2.3",
52
- "prettier": "^3.0.1"
51
+ "lint-staged": "^15.2.0",
52
+ "prettier": "^3.1.0"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">= 14.15.4"
@@ -1,7 +1,7 @@
1
1
  /* global window, $, jQuery, _, Pbf, FeedMessage, alert, accessibleAutocomplete, */
2
2
  /* eslint no-var: "off", no-unused-vars: "off", no-alert: "off" */
3
3
 
4
- function setuptransitDeparturesWidget(routes, stops, config) {
4
+ function setupTransitDeparturesWidget(routes, stops, config) {
5
5
  let departuresResponse
6
6
  let departuresTimeout
7
7
  let initialStop
@@ -65,6 +65,15 @@ function setuptransitDeparturesWidget(routes, stops, config) {
65
65
  }
66
66
 
67
67
  jQuery(($) => {
68
+ // Populate dropdown with all routes
69
+ $('#departure_route').append(
70
+ routes.map((route) => {
71
+ return $('<option>')
72
+ .attr('value', route.route_id)
73
+ .text(route.route_full_name)
74
+ }),
75
+ )
76
+
68
77
  // Read URL parameters on load
69
78
  readUrlWithParameters()
70
79
 
@@ -85,7 +94,7 @@ function setuptransitDeparturesWidget(routes, stops, config) {
85
94
  return
86
95
  }
87
96
 
88
- initialStop = stop.stop_name
97
+ initialStop = stop.stop_code || stop.stop_name
89
98
 
90
99
  // Wait for bootstrap js to initialize before triggering click
91
100
  setTimeout(() => {
@@ -112,20 +121,25 @@ function setuptransitDeparturesWidget(routes, stops, config) {
112
121
  $('#loading').hide()
113
122
  }
114
123
 
115
- function renderStopInfo(stop) {
116
- if (stop) {
124
+ function renderStopInfo(selectedStops) {
125
+ if (selectedStops && selectedStops.length > 0) {
117
126
  $('#departure_results .departure-results-stop-unknown').hide()
118
127
  $('#departure_results .departure-results-stop')
119
- .text(stop.stop_name)
128
+ .text(selectedStops[0].stop_name)
120
129
  .show()
121
130
  } else {
122
131
  $('#departure_results .departure-results-stop').hide()
123
132
  $('#departure_results .departure-results-stop-unknown').show()
124
133
  }
125
134
 
126
- if (stop && stop.stop_code) {
135
+ if (
136
+ selectedStops &&
137
+ selectedStops.length === 1 &&
138
+ selectedStops[0].stop_code &&
139
+ !selectedStops[0].is_parent_station
140
+ ) {
127
141
  $('#departure_results .departure-results-stop-code').text(
128
- stop.stop_code,
142
+ selectedStops[0].stop_code,
129
143
  )
130
144
  $('#departure_results .departure-results-stop-code-container').show()
131
145
  } else {
@@ -169,17 +183,12 @@ function setuptransitDeparturesWidget(routes, stops, config) {
169
183
  .appendTo(routeNameDiv)
170
184
 
171
185
  const sortedDepartures = _.take(
172
- _.sortBy(
173
- departureGroup,
174
- (departure) => departure.stoptime.departure.time,
175
- ),
186
+ _.sortBy(departureGroup, (departure) => departure.time),
176
187
  3,
177
188
  )
178
189
 
179
190
  for (const departure of sortedDepartures) {
180
- const minutes = formatMinutes(
181
- departure.stoptime.departure.time - Date.now() / 1000,
182
- )
191
+ const minutes = formatMinutes(departure.time - Date.now() / 1000)
183
192
  const minutesLabel = $(
184
193
  '#departure_results .departure-results-container',
185
194
  ).data('minutes-label')
@@ -199,8 +208,8 @@ function setuptransitDeparturesWidget(routes, stops, config) {
199
208
  return div
200
209
  }
201
210
 
202
- function renderResults(stop, departures) {
203
- renderStopInfo(stop)
211
+ function renderResults(selectedStops, departures) {
212
+ renderStopInfo(selectedStops)
204
213
 
205
214
  if (departures.length === 0) {
206
215
  $('#departure_results .departure-results-container').hide()
@@ -234,22 +243,54 @@ function setuptransitDeparturesWidget(routes, stops, config) {
234
243
  $('#departure_results').show()
235
244
  }
236
245
 
237
- function renderError(stop) {
238
- renderStopInfo(stop)
246
+ function renderError(selectedStops) {
247
+ renderStopInfo(selectedStops)
239
248
  $('#departure_results .departure-results-error').show()
240
249
 
241
250
  hideLoading()
242
251
  $('#departure_results').show()
243
252
  }
244
253
 
245
- function selectStop({ stopId, stopName, directionId, routeId }) {
254
+ function findStops(stopId, stopName) {
255
+ let selectedStops
256
+ if (stopId !== undefined) {
257
+ const selectedStop = stops.find((stop) => stop.stop_id === stopId)
258
+
259
+ if (selectedStop) {
260
+ selectedStops = [selectedStop]
261
+ }
262
+ } else {
263
+ selectedStops = stops.filter(
264
+ (stop) =>
265
+ stop.stop_id === stopName ||
266
+ stop.stop_code === stopName ||
267
+ stop.stop_name === stopName,
268
+ )
269
+ }
270
+
271
+ if (selectedStops) {
272
+ // Use parent stop if it exists
273
+ for (const stop of selectedStops) {
274
+ if (stop.parent_station !== null) {
275
+ const parentStationStop = stops.find(
276
+ (s) => s.stop_id === stop.parent_station,
277
+ )
278
+ if (parentStationStop) {
279
+ return [parentStationStop]
280
+ }
281
+ }
282
+ }
283
+
284
+ return selectedStops
285
+ }
286
+
287
+ // No stops found
288
+ return undefined
289
+ }
290
+
291
+ function selectParameters({ stopId, stopName, directionId, routeId }) {
246
292
  $('.stop-code-invalid').hide()
247
- const stop = stops.find(
248
- (stop) =>
249
- stop.stop_id === stopId ||
250
- stop.stop_name === stopName ||
251
- stop.stop_code === stopName,
252
- )
293
+ const selectedStops = findStops(stopId, stopName)
253
294
  const route = routes.find((route) => route.route_id === routeId)
254
295
  const direction = route
255
296
  ? route.directions.find(
@@ -258,12 +299,12 @@ function setuptransitDeparturesWidget(routes, stops, config) {
258
299
  )
259
300
  : undefined
260
301
 
261
- if (!stop) {
302
+ if (!selectedStops || selectedStops.length === 0) {
262
303
  $('.stop-code-invalid').show()
263
304
  return
264
305
  }
265
306
 
266
- selectedParameters = { stop, direction, route }
307
+ selectedParameters = { selectedStops, direction, route }
267
308
 
268
309
  resetResults()
269
310
  showLoading()
@@ -299,7 +340,7 @@ function setuptransitDeparturesWidget(routes, stops, config) {
299
340
  }
300
341
  }
301
342
 
302
- function filterDepartures(departures, { stop, direction, route }) {
343
+ function filterDepartures(departures, { selectedStops, direction, route }) {
303
344
  // Remove departure information for last stoptime by stop_sequence if it has any
304
345
  const cleanedDepartures = departures.map((departure) => {
305
346
  const stopTimeUpdates = departure?.trip_update?.stop_time_update
@@ -312,6 +353,13 @@ function setuptransitDeparturesWidget(routes, stops, config) {
312
353
  return departure
313
354
  })
314
355
 
356
+ const selectedStopIds = selectedStops.flatMap((stop) => {
357
+ return stop.is_parent_station
358
+ ? stops
359
+ .filter((s) => s.parent_station === stop.stop_id)
360
+ .map((stop) => stop.stop_id)
361
+ : [stop.stop_id]
362
+ })
315
363
  const filteredDepartures = []
316
364
 
317
365
  for (const departure of cleanedDepartures) {
@@ -335,7 +383,7 @@ function setuptransitDeparturesWidget(routes, stops, config) {
335
383
 
336
384
  filteredDeparture.route = route
337
385
  filteredDeparture.direction = direction
338
- } else if (stop) {
386
+ } else if (selectedStops && selectedStops.length > 0) {
339
387
  if (
340
388
  !departure ||
341
389
  !departure.trip_update ||
@@ -350,31 +398,24 @@ function setuptransitDeparturesWidget(routes, stops, config) {
350
398
  )
351
399
  }
352
400
 
353
- filteredDeparture.stoptime =
354
- departure.trip_update.stop_time_update.find(
355
- (stopTimeUpdate) => stopTimeUpdate.stop_id === stop.stop_id,
356
- )
401
+ const stoptime = departure.trip_update.stop_time_update.find(
402
+ (stopTimeUpdate) => selectedStopIds.includes(stopTimeUpdate.stop_id),
403
+ )
357
404
 
358
- if (
359
- !filteredDeparture.stoptime ||
360
- !filteredDeparture.stoptime.departure
361
- ) {
405
+ if (!stoptime || (!stoptime.arrival && !stoptime.departure)) {
362
406
  continue
363
407
  }
364
408
 
409
+ filteredDeparture.time =
410
+ stoptime.departure?.time ?? stoptime.arrival?.time
411
+
365
412
  // Hide departures more than 90 minutes in the future
366
- if (
367
- filteredDeparture.stoptime.departure.time - Date.now() / 1000 >
368
- 90 * 60
369
- ) {
413
+ if (filteredDeparture.time - Date.now() / 1000 > 90 * 60) {
370
414
  continue
371
415
  }
372
416
 
373
417
  // Hide departures more than 1 minute in the past
374
- if (
375
- filteredDeparture.stoptime.departure.time - Date.now() / 1000 <
376
- -60
377
- ) {
418
+ if (filteredDeparture.time - Date.now() / 1000 < -60) {
378
419
  continue
379
420
  }
380
421
 
@@ -386,7 +427,7 @@ function setuptransitDeparturesWidget(routes, stops, config) {
386
427
 
387
428
  async function updateDepartures(forceRefresh) {
388
429
  try {
389
- const { stop, direction, route } = selectedParameters
430
+ const { selectedStops, direction, route } = selectedParameters
390
431
  // Use existing data if less than the refresh interval seconds old
391
432
  const minimumAge = Date.now() - config.refreshIntervalSeconds * 1000
392
433
  if (
@@ -396,8 +437,8 @@ function setuptransitDeparturesWidget(routes, stops, config) {
396
437
  ) {
397
438
  const departures = await fetchTripUpdates()
398
439
 
399
- // Don't use new departure info if nothing is returned.
400
- if (!departures || departures.length === 0) {
440
+ // Don't use new departure info if nothing is returned
441
+ if (!departures) {
401
442
  console.error('No departures returned')
402
443
  return
403
444
  }
@@ -406,20 +447,20 @@ function setuptransitDeparturesWidget(routes, stops, config) {
406
447
  }
407
448
 
408
449
  renderResults(
409
- stop,
450
+ selectedStops,
410
451
  filterDepartures(departuresResponse.departures, {
411
- stop,
452
+ selectedStops,
412
453
  direction,
413
454
  route,
414
455
  }),
415
456
  )
416
457
 
417
- if (stop.stop_id) {
418
- updateUrlWithStop(stop)
458
+ if (selectedStops && selectedStops.length > 0) {
459
+ updateUrlWithStop(selectedStops[0])
419
460
  }
420
461
  } catch (error) {
421
462
  console.error(error)
422
- renderError(selectedParameters?.stop)
463
+ renderError(selectedParameters?.selectedStops)
423
464
  }
424
465
  }
425
466
 
@@ -527,7 +568,7 @@ function setuptransitDeparturesWidget(routes, stops, config) {
527
568
  const directionId = $('#real_time_departures #departure_direction').val()
528
569
  const stopId = $(event.target).val()
529
570
 
530
- selectStop({
571
+ selectParameters({
531
572
  stopId,
532
573
  routeId,
533
574
  directionId,
@@ -545,7 +586,7 @@ function setuptransitDeparturesWidget(routes, stops, config) {
545
586
  return
546
587
  }
547
588
 
548
- selectStop({
589
+ selectParameters({
549
590
  stopName,
550
591
  })
551
592
  })
@@ -562,20 +603,31 @@ function setuptransitDeparturesWidget(routes, stops, config) {
562
603
  id: 'departure_stop_code',
563
604
  source(query, populateResults) {
564
605
  const filteredResults = stops.filter((stop) => {
606
+ // Don't list child stations
607
+ if (stop.parent_station !== null) {
608
+ return false
609
+ }
610
+
565
611
  if (stop.stop_code?.startsWith(query.trim())) {
566
612
  return true
567
613
  }
568
614
 
569
615
  return stop.stop_name?.toLowerCase().includes(query.toLowerCase())
570
616
  })
571
- populateResults(filteredResults)
617
+ const sortedResults = _.sortBy(filteredResults, (stop) =>
618
+ stop.stop_name?.toLowerCase().startsWith(query.toLowerCase().trim())
619
+ ? 0
620
+ : 1,
621
+ )
622
+ populateResults(sortedResults)
572
623
  },
573
624
  minLength: 2,
574
625
  autoselect: true,
575
626
  placeholder: $('#departure_stop_code_container').data('placeholder'),
576
627
  showNoOptionsFound: false,
577
628
  templates: {
578
- inputValue: (result) => result && result.stop_name,
629
+ inputValue: (result) =>
630
+ result && (result.stop_code || result.stop_name),
579
631
  suggestion(result) {
580
632
  if (!result) {
581
633
  return
@@ -603,7 +655,9 @@ function setuptransitDeparturesWidget(routes, stops, config) {
603
655
  return
604
656
  }
605
657
 
606
- $('#departure_stop_code').val(selectedStop.stop_name)
658
+ $('#departure_stop_code').val(
659
+ selectedStop.stop_code || selectedStop.stop_name,
660
+ )
607
661
  $('#stop_form').trigger('submit')
608
662
  },
609
663
  defaultValue: initialStop,
@@ -23,8 +23,6 @@
23
23
  .form-group.select
24
24
  select.form-control#departure_route(name="route")
25
25
  option(value="")= __('Choose a route')
26
- each route in routes
27
- option(value=route.route_id)= route.route_full_name
28
26
 
29
27
  .form-group.select
30
28
  select.form-control#departure_direction(name="direction" disabled)
@@ -59,5 +57,5 @@ script.
59
57
  refreshIntervalSeconds: #{config.refreshIntervalSeconds},
60
58
  timeFormat: '#{config.timeFormat}'
61
59
  }
62
- setuptransitDeparturesWidget(routes, stops, config);
60
+ setupTransitDeparturesWidget(routes, stops, config);
63
61
  })()