transit-departures-widget 2.4.0 → 2.4.2

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.
@@ -1,687 +0,0 @@
1
- /* global window, $, jQuery, _, Pbf, FeedMessage, alert, accessibleAutocomplete, */
2
- /* eslint no-var: "off", no-unused-vars: "off", no-alert: "off" */
3
-
4
- function setupTransitDeparturesWidget(routes, stops, config) {
5
- let departuresResponse
6
- let departuresTimeout
7
- let initialStop
8
- let selectedParameters
9
- let url = new URL(config.gtfsRtTripupdatesUrl)
10
-
11
- function updateUrlWithStop(stop) {
12
- const url = new URL(window.location.origin + window.location.pathname)
13
- url.searchParams.append('stop', stop.stop_code || stop.stop_id)
14
-
15
- window.history.pushState(null, null, url)
16
- }
17
-
18
- async function fetchTripUpdates() {
19
- url.searchParams.append('cacheBust', Date.now())
20
- const response = await fetch(url)
21
- if (response.ok) {
22
- const bufferResponse = await response.arrayBuffer()
23
- const pbf = new Pbf(new Uint8Array(bufferResponse))
24
- const object = FeedMessage.read(pbf)
25
-
26
- return object.entity
27
- }
28
-
29
- throw new Error(response.status)
30
- }
31
-
32
- function formatMinutes(seconds) {
33
- if (seconds < 60) {
34
- return '&#60;1'
35
- }
36
-
37
- return Math.floor(seconds / 60)
38
- }
39
-
40
- function formatDirectionId(directionId) {
41
- if (directionId === null || directionId === undefined) {
42
- return '0'
43
- }
44
-
45
- return directionId?.toString()
46
- }
47
-
48
- function timeStamp() {
49
- const now = new Date()
50
- let hours = now.getHours()
51
- let minutes = now.getMinutes()
52
-
53
- if (minutes < 10) {
54
- minutes = '0' + minutes
55
- }
56
-
57
- if (config.timeFormat === '24hour') {
58
- return `${hours}:${minutes}`
59
- }
60
-
61
- const suffix = hours < 12 ? 'AM' : 'PM'
62
- hours = hours < 12 ? hours : hours - 12
63
- hours = hours || 12
64
-
65
- return `${hours}:${minutes} ${suffix}`
66
- }
67
-
68
- jQuery(($) => {
69
- // Populate dropdown with all routes
70
- $('#departure_route').append(
71
- routes.map((route) => {
72
- return $('<option>')
73
- .attr('value', route.route_id)
74
- .text(route.route_full_name)
75
- }),
76
- )
77
-
78
- // Read URL parameters on load
79
- readUrlWithParameters()
80
-
81
- function readUrlWithParameters() {
82
- const url = new URL(window.location.href)
83
- const stopFromURL = url.searchParams.get('stop')
84
-
85
- if (!stopFromURL) {
86
- return
87
- }
88
-
89
- const stop = stops.find(
90
- (stop) =>
91
- stop.stop_id === stopFromURL || stop.stop_code === stopFromURL,
92
- )
93
-
94
- if (!stop) {
95
- return
96
- }
97
-
98
- initialStop = stop.stop_code || stop.stop_name
99
-
100
- // Wait for bootstrap js to initialize before triggering click
101
- setTimeout(() => {
102
- $('#stop_form').trigger('submit')
103
- $(
104
- '#real_time_departures input[name="departure_type"][value="stop"]',
105
- ).trigger('click')
106
- }, 100)
107
- }
108
-
109
- function resetResults() {
110
- if (departuresTimeout) {
111
- clearTimeout(departuresTimeout)
112
- }
113
-
114
- $('#departure_results').hide()
115
- }
116
-
117
- function showLoading() {
118
- $('#loading').show()
119
- }
120
-
121
- function hideLoading() {
122
- $('#loading').hide()
123
- }
124
-
125
- function renderStopInfo(selectedStops) {
126
- if (selectedStops && selectedStops.length > 0) {
127
- $('#departure_results .departure-results-stop-unknown').hide()
128
- $('#departure_results .departure-results-stop')
129
- .text(selectedStops[0].stop_name)
130
- .show()
131
- } else {
132
- $('#departure_results .departure-results-stop').hide()
133
- $('#departure_results .departure-results-stop-unknown').show()
134
- }
135
-
136
- if (
137
- selectedStops &&
138
- selectedStops.length === 1 &&
139
- selectedStops[0].stop_code &&
140
- !selectedStops[0].is_parent_station
141
- ) {
142
- $('#departure_results .departure-results-stop-code').text(
143
- selectedStops[0].stop_code,
144
- )
145
- $('#departure_results .departure-results-stop-code-container').show()
146
- } else {
147
- $('#departure_results .departure-results-stop-code-container').hide()
148
- }
149
-
150
- $('#departure_results .departure-results-fetchtime-time').text(
151
- timeStamp(),
152
- )
153
- }
154
-
155
- function formatDepartureGroup(departureGroup) {
156
- const div = $('<div>').addClass('departure-result')
157
- const { route, direction } = departureGroup[0]
158
-
159
- const routeNameDiv = $('<div>')
160
- .addClass('departure-result-route-name')
161
- .appendTo(div)
162
- const departureTimesDiv = $('<div>')
163
- .addClass('departure-result-times')
164
- .appendTo(div)
165
-
166
- if (route.route_short_name) {
167
- const routeColor = route.route_color ? `#${route.route_color}` : '#ccc'
168
- const routeTextColor = route.route_text_color
169
- ? `#${route.route_text_color}`
170
- : '#000'
171
- $('<div>')
172
- .text(route.route_short_name)
173
- .addClass('departure-result-route-circle')
174
- .css({
175
- 'background-color': routeColor,
176
- color: routeTextColor,
177
- })
178
- .appendTo(routeNameDiv)
179
- }
180
-
181
- $('<div>')
182
- .text(direction.direction)
183
- .addClass('departure-result-route-direction')
184
- .appendTo(routeNameDiv)
185
-
186
- const sortedDepartures = _.take(
187
- _.sortBy(departureGroup, (departure) => departure.time),
188
- 3,
189
- )
190
-
191
- for (const departure of sortedDepartures) {
192
- const minutes = formatMinutes(departure.time - Date.now() / 1000)
193
- const minutesLabel = $(
194
- '#departure_results .departure-results-container',
195
- ).data('minutes-label')
196
-
197
- $('<div>')
198
- .addClass('departure-result-time-container')
199
- .append(
200
- $('<div>')
201
- .addClass('departure-result-time')
202
- .html(
203
- `${minutes}<span class="departure-result-time-label">${minutesLabel}</span>`,
204
- ),
205
- )
206
- .appendTo(departureTimesDiv)
207
- }
208
-
209
- return div
210
- }
211
-
212
- function renderResults(selectedStops, departures) {
213
- renderStopInfo(selectedStops)
214
-
215
- if (departures.length === 0) {
216
- $('#departure_results .departure-results-container').hide()
217
- $('#departure_results .departure-results-error').hide()
218
- $('#departure_results .departure-results-none').show()
219
- } else {
220
- const departureGroups = _.groupBy(
221
- departures,
222
- (departure) =>
223
- `${departure.route.route_id}||${departure.direction.direction_id}`,
224
- )
225
- const sortedDepartureGroups = _.sortBy(
226
- departureGroups,
227
- (departureGroup) => {
228
- const { route } = departureGroup[0]
229
- return Number.parseInt(route.route_short_name, 10)
230
- },
231
- )
232
- $('#departure_results .departure-results-none').hide()
233
- $('#departure_results .departure-results-error').hide()
234
- $('#departure_results .departure-results-container')
235
- .html(
236
- sortedDepartureGroups.map((departureGroup) =>
237
- formatDepartureGroup(departureGroup),
238
- ),
239
- )
240
- .show()
241
- }
242
-
243
- hideLoading()
244
- $('#departure_results').show()
245
- }
246
-
247
- function renderError(selectedStops) {
248
- renderStopInfo(selectedStops)
249
- $('#departure_results .departure-results-error').show()
250
-
251
- hideLoading()
252
- $('#departure_results').show()
253
- }
254
-
255
- function findStops(stopId, stopName) {
256
- let selectedStops
257
- if (stopId !== undefined) {
258
- const selectedStop = stops.find((stop) => stop.stop_id === stopId)
259
-
260
- if (selectedStop) {
261
- selectedStops = [selectedStop]
262
- }
263
- } else {
264
- selectedStops = stops.filter(
265
- (stop) =>
266
- stop.stop_id === stopName ||
267
- stop.stop_code === stopName ||
268
- stop.stop_name === stopName,
269
- )
270
- }
271
-
272
- if (selectedStops) {
273
- // Use parent stop if it exists
274
- for (const stop of selectedStops) {
275
- if (stop.parent_station !== null) {
276
- const parentStationStop = stops.find(
277
- (s) => s.stop_id === stop.parent_station,
278
- )
279
- if (parentStationStop) {
280
- return [parentStationStop]
281
- }
282
- }
283
- }
284
-
285
- return selectedStops
286
- }
287
-
288
- // No stops found
289
- return undefined
290
- }
291
-
292
- function selectParameters({ stopId, stopName, directionId, routeId }) {
293
- $('.stop-code-invalid').hide()
294
- const selectedStops = findStops(stopId, stopName)
295
- const route = routes.find((route) => route.route_id === routeId)
296
- const direction = route
297
- ? route.directions.find(
298
- (direction) =>
299
- formatDirectionId(direction.direction_id) === directionId,
300
- )
301
- : undefined
302
-
303
- if (!selectedStops || selectedStops.length === 0) {
304
- $('.stop-code-invalid').show()
305
- return
306
- }
307
-
308
- selectedParameters = { selectedStops, direction, route }
309
-
310
- resetResults()
311
- showLoading()
312
- updateDepartures()
313
-
314
- // Every refresh interval seconds, check for tripupdates
315
- departuresTimeout = setInterval(
316
- () => updateDepartures(),
317
- config.refreshIntervalSeconds * 1000,
318
- )
319
- }
320
-
321
- function getRouteAndDirectionFromTrip(tripId) {
322
- let tripRoute
323
- let tripDirection
324
- for (const route of routes) {
325
- for (const direction of route.directions) {
326
- if (direction.tripIds.includes(tripId)) {
327
- tripDirection = direction
328
- tripRoute = route
329
- break
330
- }
331
- }
332
-
333
- if (tripDirection && tripRoute) {
334
- break
335
- }
336
- }
337
-
338
- return {
339
- route: tripRoute,
340
- direction: tripDirection,
341
- }
342
- }
343
-
344
- function filterDepartures(departures, { selectedStops, direction, route }) {
345
- // Remove departure and arrival information for last stoptime by stop_sequence if it has any
346
- const cleanedDepartures = departures.map((departure) => {
347
- if (departure?.trip_update?.stop_time_update?.length > 0) {
348
- // Find index of largest stop_sequence
349
- let largestStopSequence = 0
350
- let largestStopSequenceIndex = 0
351
- for (
352
- let index = 0;
353
- index < departure.trip_update.stop_time_update.length;
354
- index++
355
- ) {
356
- if (
357
- departure.trip_update.stop_time_update[index].stop_sequence >
358
- largestStopSequence
359
- ) {
360
- largestStopSequence =
361
- departure.trip_update.stop_time_update[index].stop_sequence
362
- largestStopSequenceIndex = index
363
- }
364
- }
365
- departure.trip_update.stop_time_update.splice(
366
- largestStopSequenceIndex,
367
- 1,
368
- )
369
- }
370
-
371
- return departure
372
- })
373
-
374
- const selectedStopIds = selectedStops.flatMap((stop) => {
375
- return stop.is_parent_station
376
- ? stops
377
- .filter((s) => s.parent_station === stop.stop_id)
378
- .map((stop) => stop.stop_id)
379
- : [stop.stop_id]
380
- })
381
- const filteredDepartures = []
382
-
383
- for (const departure of cleanedDepartures) {
384
- let filteredDeparture = {}
385
-
386
- if (route) {
387
- if (
388
- !departure ||
389
- !departure.trip_update ||
390
- !departure.trip_update.trip
391
- ) {
392
- continue
393
- }
394
-
395
- if (
396
- !direction ||
397
- !direction.tripIds.includes(departure.trip_update.trip.trip_id)
398
- ) {
399
- continue
400
- }
401
-
402
- filteredDeparture.route = route
403
- filteredDeparture.direction = direction
404
- } else if (selectedStops && selectedStops.length > 0) {
405
- if (
406
- !departure ||
407
- !departure.trip_update ||
408
- !departure.trip_update.stop_time_update
409
- ) {
410
- continue
411
- }
412
-
413
- // Get route and direction from trip_id
414
- filteredDeparture = getRouteAndDirectionFromTrip(
415
- departure.trip_update.trip.trip_id,
416
- )
417
- }
418
-
419
- const stoptime = departure.trip_update.stop_time_update.find(
420
- (stopTimeUpdate) => selectedStopIds.includes(stopTimeUpdate.stop_id),
421
- )
422
-
423
- if (!stoptime || (!stoptime.arrival && !stoptime.departure)) {
424
- continue
425
- }
426
-
427
- filteredDeparture.time =
428
- stoptime.departure?.time ?? stoptime.arrival?.time
429
-
430
- // Hide departures more than 90 minutes in the future
431
- if (filteredDeparture.time - Date.now() / 1000 > 90 * 60) {
432
- continue
433
- }
434
-
435
- // Hide departures more than 1 minute in the past
436
- if (filteredDeparture.time - Date.now() / 1000 < -60) {
437
- continue
438
- }
439
-
440
- filteredDepartures.push(filteredDeparture)
441
- }
442
-
443
- return filteredDepartures
444
- }
445
-
446
- async function updateDepartures(forceRefresh) {
447
- try {
448
- const { selectedStops, direction, route } = selectedParameters
449
- // Use existing data if less than the refresh interval seconds old
450
- const minimumAge = Date.now() - config.refreshIntervalSeconds * 1000
451
- if (
452
- !departuresResponse ||
453
- departuresResponse.timestamp < minimumAge ||
454
- forceRefresh === true
455
- ) {
456
- const departures = await fetchTripUpdates()
457
-
458
- // Don't use new departure info if nothing is returned
459
- if (!departures) {
460
- console.error('No departures returned')
461
- return
462
- }
463
-
464
- departuresResponse = { departures, timestamp: Date.now() }
465
- }
466
-
467
- renderResults(
468
- selectedStops,
469
- filterDepartures(departuresResponse.departures, {
470
- selectedStops,
471
- direction,
472
- route,
473
- }),
474
- )
475
-
476
- if (selectedStops && selectedStops.length > 0) {
477
- updateUrlWithStop(selectedStops[0])
478
- }
479
- } catch (error) {
480
- console.error(error)
481
- renderError(selectedParameters?.selectedStops)
482
- }
483
- }
484
-
485
- $('#real_time_departures input[name="departure_type"]').change((event) => {
486
- const type = $(event.target).val()
487
-
488
- $('#real_time_departures #route_form').toggleClass(
489
- 'hidden-form',
490
- type !== 'route',
491
- )
492
- $('#real_time_departures #stop_form').toggleClass(
493
- 'hidden-form',
494
- type !== 'stop',
495
- )
496
-
497
- $('#real_time_departures #departure_stop').val('').prop('disabled', true)
498
-
499
- $('#real_time_departures #departure_direction option:gt(0)').remove()
500
- $('#real_time_departures #departure_stop option:gt(0)').remove()
501
- resetResults()
502
- })
503
-
504
- $('#real_time_departures #departure_route').change((event) => {
505
- const routeId = $(event.target).val()
506
-
507
- $('#real_time_departures #departure_stop').val('').prop('disabled', true)
508
-
509
- $('#real_time_departures #departure_direction option:gt(0)').remove()
510
- $('#real_time_departures #departure_stop option:gt(0)').remove()
511
- resetResults()
512
-
513
- if (routeId === '') {
514
- $('#real_time_departures #departure_direction')
515
- .val('')
516
- .prop('disabled', true)
517
- } else {
518
- $('#real_time_departures #departure_direction')
519
- .val('')
520
- .prop('disabled', false)
521
-
522
- const route = routes.find((route) => route.route_id === routeId)
523
-
524
- if (!route) {
525
- return console.warn(`Unable to find route ${routeId}`)
526
- }
527
-
528
- $('#real_time_departures #departure_direction').append(
529
- route.directions.map((direction) =>
530
- $('<option>')
531
- .val(formatDirectionId(direction.direction_id))
532
- .text(direction.direction),
533
- ),
534
- )
535
-
536
- if (route.directions.length === 1) {
537
- $('#real_time_departures #departure_direction').hide()
538
- $('#real_time_departures #departure_direction').val(
539
- formatDirectionId(route.directions[0].direction_id),
540
- )
541
- $('#real_time_departures #departure_direction').trigger('change')
542
- } else {
543
- $('#real_time_departures #departure_direction').show()
544
- }
545
- }
546
- })
547
-
548
- $('#real_time_departures #departure_direction').change((event) => {
549
- const routeId = $('#real_time_departures #departure_route').val()
550
- const directionId = $(event.target).val()
551
-
552
- $('#real_time_departures #departure_stop option:gt(0)').remove()
553
- resetResults()
554
-
555
- if (directionId === '') {
556
- $('#real_time_departures #departure_stop')
557
- .val('')
558
- .prop('disabled', true)
559
- } else {
560
- $('#real_time_departures #departure_stop')
561
- .val('')
562
- .prop('disabled', false)
563
-
564
- const route = routes.find((route) => route.route_id === routeId)
565
-
566
- if (!route) {
567
- return console.warn(`Unable to find route ${routeId}`)
568
- }
569
-
570
- const direction = route.directions.find(
571
- (direction) =>
572
- formatDirectionId(direction.direction_id) === directionId,
573
- )
574
-
575
- $('#real_time_departures #departure_stop').append(
576
- direction.stopIds.map((stopId) => {
577
- const stop = stops.find((stop) => stop.stop_id === stopId)
578
- return $('<option>').val(stop.stop_id).text(stop.stop_name)
579
- }),
580
- )
581
- }
582
- })
583
-
584
- $('#real_time_departures #departure_stop').change((event) => {
585
- const routeId = $('#real_time_departures #departure_route').val()
586
- const directionId = $('#real_time_departures #departure_direction').val()
587
- const stopId = $(event.target).val()
588
-
589
- selectParameters({
590
- stopId,
591
- routeId,
592
- directionId,
593
- })
594
- })
595
-
596
- $('#stop_form').submit((event) => {
597
- event.preventDefault()
598
- $('.stop-code-invalid').hide()
599
-
600
- const stopName = $('#departure_stop_code').val()
601
-
602
- if (stopName === '') {
603
- $('.stop-code-invalid').show()
604
- return
605
- }
606
-
607
- selectParameters({
608
- stopName,
609
- })
610
- })
611
-
612
- $('#departure_results .departure-results-fetchtime').click((event) => {
613
- event.preventDefault()
614
- resetResults()
615
- showLoading()
616
- updateDepartures(true)
617
- })
618
-
619
- accessibleAutocomplete({
620
- element: $('#departure_stop_code_container').get(0),
621
- id: 'departure_stop_code',
622
- source(query, populateResults) {
623
- const filteredResults = stops.filter((stop) => {
624
- // Don't list child stations
625
- if (
626
- stop.parent_station !== null &&
627
- stop.parent_station !== undefined
628
- ) {
629
- return false
630
- }
631
-
632
- if (stop.stop_code?.startsWith(query.trim())) {
633
- return true
634
- }
635
-
636
- return stop.stop_name?.toLowerCase().includes(query.toLowerCase())
637
- })
638
- const sortedResults = _.sortBy(filteredResults, (stop) =>
639
- stop.stop_name?.toLowerCase().startsWith(query.toLowerCase().trim())
640
- ? 0
641
- : 1,
642
- )
643
- populateResults(sortedResults)
644
- },
645
- minLength: 2,
646
- autoselect: true,
647
- placeholder: $('#departure_stop_code_container').data('placeholder'),
648
- showNoOptionsFound: false,
649
- templates: {
650
- inputValue: (result) =>
651
- result && (result.stop_code || result.stop_name),
652
- suggestion(result) {
653
- if (!result) {
654
- return
655
- }
656
-
657
- if (typeof result === 'string') {
658
- return result
659
- }
660
-
661
- const stopCode = result.is_parent_station
662
- ? $('#departure_stop_code_container').data('stop-code-all')
663
- : result.stop_code
664
-
665
- let formattedStopName = `<strong>${result.stop_name}</strong>`
666
-
667
- if (stopCode) {
668
- formattedStopName += ` (${stopCode})`
669
- }
670
-
671
- return formattedStopName
672
- },
673
- },
674
- onConfirm(selectedStop) {
675
- if (!selectedStop) {
676
- return
677
- }
678
-
679
- $('#departure_stop_code').val(
680
- selectedStop.stop_code || selectedStop.stop_name,
681
- )
682
- $('#stop_form').trigger('submit')
683
- },
684
- defaultValue: initialStop,
685
- })
686
- })
687
- }
@@ -1,19 +0,0 @@
1
- doctype html
2
- html
3
- head
4
- title= title
5
- meta(charset="utf-8")
6
- link(rel="stylesheet" href="https://unpkg.com/bootstrap@4.6.0/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous")
7
- link(rel="stylesheet" href="https://unpkg.com/accessible-autocomplete@2.0.3/dist/accessible-autocomplete.min.css" integrity="sha384-QoUNXPSpjM43RbU2UeGHhlZsoDZ4Ve+k3Una2evEDxo1FLc61Y/NhquJOQOh8TRN" crossorigin="anonymous")
8
- link(rel="stylesheet" href=`${config.assetPath}css/transit-departures-widget-styles.css`)
9
- meta(name="viewport" content="initial-scale=1.0, width=device-width")
10
-
11
- script(src="https://unpkg.com/jquery@3.6.0/dist/jquery.min.js" integrity="sha384-vtXRMe3mGCbOeY7l30aIg8H9p3GdeSe4IFlP6G8JMa7o7lXvnz3GFKzPxzJdPfGK" crossorigin="anonymous")
12
- script(src="https://unpkg.com/lodash@4.17.21/lodash.min.js" integrity="sha384-H6KKS1H1WwuERMSm+54dYLzjg0fKqRK5ZRyASdbrI/lwrCc6bXEmtGYr5SwvP1pZ" crossorigin="anonymous")
13
- script(src="https://unpkg.com/pbf@3.2.1/dist/pbf.js" integrity="sha384-v0UA1pHhakpSLzilEI4GK9mx/156zhRyC60GB/GgYwHKHQCfjOYKyNKhDGw5DqaO" crossorigin="anonymous")
14
- script(src="https://unpkg.com/gtfs-realtime-pbf-js-module@1.0.0/gtfs-realtime.browser.proto.js" integrity="sha256-z/blOP7gdmNvq4KQfxVASnetvUTCcybhhIMIpnCIfa0=" crossorigin="anonymous")
15
- script(src="https://unpkg.com/accessible-autocomplete@2.0.3/dist/accessible-autocomplete.min.js" integrity="sha384-lA2U/4JpLCszKZ0EJQwuGmVQHQqQbaBxnWcSgfeVNA9wZrMwjwCxVKYlMnT7u/DD" crossorigin="anonymous")
16
- script(src=`${config.assetPath}js/transit-departures-widget.js`)
17
-
18
- body
19
- block content