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.
package/lib/utils.js DELETED
@@ -1,329 +0,0 @@
1
- import { fileURLToPath } from 'url'
2
- import { dirname, join } from 'path'
3
- import { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'
4
- import { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'
5
- import { renderFile } from './file-utils.js'
6
- import sqlString from 'sqlstring-sqlite'
7
- import toposort from 'toposort'
8
- import i18n from 'i18n'
9
-
10
- /*
11
- * Get calendars for a specified date range
12
- */
13
- const getCalendarsForDateRange = (config) => {
14
- const db = openDb(config)
15
- let whereClause = ''
16
- const whereClauses = []
17
-
18
- if (config.endDate) {
19
- whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)
20
- }
21
-
22
- if (config.startDate) {
23
- whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)
24
- }
25
-
26
- if (whereClauses.length > 0) {
27
- whereClause = `WHERE ${whereClauses.join(' AND ')}`
28
- }
29
-
30
- return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()
31
- }
32
-
33
- /*
34
- * Format a route name.
35
- */
36
- function formatRouteName(route) {
37
- let routeName = ''
38
-
39
- if (route.route_short_name !== null) {
40
- routeName += route.route_short_name
41
- }
42
-
43
- if (route.route_short_name !== null && route.route_long_name !== null) {
44
- routeName += ' - '
45
- }
46
-
47
- if (route.route_long_name !== null) {
48
- routeName += route.route_long_name
49
- }
50
-
51
- return routeName
52
- }
53
-
54
- /*
55
- * Get directions for a route
56
- */
57
- function getDirectionsForRoute(route, config) {
58
- const db = openDb(config)
59
-
60
- // Lookup direction names from non-standard directions.txt file
61
- const directions = getDirections({ route_id: route.route_id }, [
62
- 'direction_id',
63
- 'direction',
64
- ])
65
-
66
- const calendars = getCalendarsForDateRange(config)
67
-
68
- // Else use the most common headsigns as directions from trips.txt file
69
- if (directions.length === 0) {
70
- const headsigns = db
71
- .prepare(
72
- `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars
73
- .map((calendar) => `'${calendar.service_id}'`)
74
- .join(', ')}) GROUP BY direction_id, trip_headsign`,
75
- )
76
- .all(route.route_id)
77
-
78
- for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {
79
- const mostCommonHeadsign = maxBy(group, 'count')
80
- directions.push({
81
- direction_id: mostCommonHeadsign.direction_id,
82
- direction: i18n.__('To {{{headsign}}}', {
83
- headsign: mostCommonHeadsign.trip_headsign,
84
- }),
85
- })
86
- }
87
- }
88
-
89
- return directions
90
- }
91
-
92
- /*
93
- * Sort an array of stoptimes by stop_sequence using a directed graph
94
- */
95
- function sortStopIdsBySequence(stoptimes) {
96
- const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')
97
-
98
- // First, try using a directed graph to determine stop order.
99
- try {
100
- const stopGraph = []
101
-
102
- for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {
103
- const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(
104
- (stoptime) => stoptime.stop_id,
105
- )
106
-
107
- for (const [index, stopId] of sortedStopIds.entries()) {
108
- if (index === sortedStopIds.length - 1) {
109
- continue
110
- }
111
-
112
- stopGraph.push([stopId, sortedStopIds[index + 1]])
113
- }
114
- }
115
-
116
- return toposort(stopGraph)
117
- } catch {
118
- // Ignore errors and move to next strategy.
119
- }
120
-
121
- // Finally, fall back to using the stop order from the trip with the most stoptimes.
122
- const longestTripStoptimes = maxBy(
123
- Object.values(stoptimesGroupedByTrip),
124
- (stoptimes) => size(stoptimes),
125
- )
126
-
127
- return longestTripStoptimes.map((stoptime) => stoptime.stop_id)
128
- }
129
-
130
- /*
131
- * Get stops in order for a route and direction
132
- */
133
- function getStopsForDirection(route, direction, config) {
134
- const db = openDb(config)
135
- const calendars = getCalendarsForDateRange(config)
136
- const whereClause = formatWhereClauses({
137
- direction_id: direction.direction_id,
138
- route_id: route.route_id,
139
- service_id: calendars.map((calendar) => calendar.service_id),
140
- })
141
- const stoptimes = db
142
- .prepare(
143
- `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,
144
- )
145
- .all()
146
-
147
- const sortedStopIds = sortStopIdsBySequence(stoptimes)
148
-
149
- const deduplicatedStopIds = sortedStopIds.reduce((memo, stopId) => {
150
- // Remove duplicated stop_ids in a row
151
- if (last(memo) !== stopId) {
152
- memo.push(stopId)
153
- }
154
-
155
- return memo
156
- }, [])
157
-
158
- // Remove last stop of route since boarding is not allowed
159
- deduplicatedStopIds.pop()
160
-
161
- // Fetch stop details
162
- const stops = getStops({ stop_id: deduplicatedStopIds }, [
163
- 'stop_id',
164
- 'stop_name',
165
- 'stop_code',
166
- 'parent_station',
167
- ])
168
-
169
- return deduplicatedStopIds.map((stopId) =>
170
- stops.find((stop) => stop.stop_id === stopId),
171
- )
172
- }
173
-
174
- /*
175
- * Generate HTML for transit departures widget.
176
- */
177
- export function generateTransitDeparturesWidgetHtml(config) {
178
- i18n.configure({
179
- directory: join(dirname(fileURLToPath(import.meta.url)), '..', 'locales'),
180
- defaultLocale: config.locale,
181
- updateFiles: false,
182
- })
183
-
184
- const templateVars = {
185
- __: i18n.__,
186
- config,
187
- }
188
- return renderFile('widget', templateVars, config)
189
- }
190
-
191
- /*
192
- * Generate JSON of routes and stops for transit departures widget.
193
- */
194
- export function generateTransitDeparturesWidgetJson(config) {
195
- const routes = getRoutes()
196
- const stops = []
197
- const filteredRoutes = []
198
- const calendars = getCalendarsForDateRange(config)
199
-
200
- for (const route of routes) {
201
- route.route_full_name = formatRouteName(route)
202
-
203
- const directions = getDirectionsForRoute(route, config)
204
-
205
- // Filter out routes with no directions
206
- if (directions.length === 0) {
207
- config.logWarning(
208
- `route_id ${route.route_id} has no directions - skipping`,
209
- )
210
- continue
211
- }
212
-
213
- for (const direction of directions) {
214
- const directionStops = getStopsForDirection(route, direction, config)
215
- stops.push(...directionStops)
216
- direction.stopIds = directionStops.map((stop) => stop.stop_id)
217
-
218
- const trips = getTrips(
219
- {
220
- route_id: route.route_id,
221
- direction_id: direction.direction_id,
222
- service_id: calendars.map((calendar) => calendar.service_id),
223
- },
224
- ['trip_id'],
225
- )
226
- direction.tripIds = trips.map((trip) => trip.trip_id)
227
- }
228
-
229
- route.directions = directions
230
- filteredRoutes.push(route)
231
- }
232
-
233
- // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']
234
- const sortedRoutes = sortBy(
235
- sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),
236
- (route) => Number.parseInt(route.route_short_name, 10),
237
- )
238
-
239
- // Get Parent Station Stops
240
- const parentStationIds = new Set(stops.map((stop) => stop.parent_station))
241
-
242
- const parentStationStops = getStops(
243
- { stop_id: Array.from(parentStationIds) },
244
- ['stop_id', 'stop_name', 'stop_code', 'parent_station'],
245
- )
246
-
247
- stops.push(
248
- ...parentStationStops.map((stop) => {
249
- stop.is_parent_station = true
250
- return stop
251
- }),
252
- )
253
-
254
- // Sort unique list of stops
255
- const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')
256
-
257
- return {
258
- routes: removeNulls(sortedRoutes),
259
- stops: removeNulls(sortedStops),
260
- }
261
- }
262
-
263
- /*
264
- * Remove null values from array or object
265
- */
266
- function removeNulls(data) {
267
- if (Array.isArray(data)) {
268
- return data
269
- .map(removeNulls)
270
- .filter((item) => item !== null && item !== undefined)
271
- } else if (typeof data === 'object' && data !== null) {
272
- return Object.entries(data).reduce((acc, [key, value]) => {
273
- const cleanedValue = removeNulls(value)
274
- if (cleanedValue !== null && cleanedValue !== undefined) {
275
- acc[key] = cleanedValue
276
- }
277
- return acc
278
- }, {})
279
- } else {
280
- return data
281
- }
282
- }
283
-
284
- /*
285
- * Initialize configuration with defaults.
286
- */
287
- export function setDefaultConfig(initialConfig) {
288
- const defaults = {
289
- beautify: false,
290
- noHead: false,
291
- refreshIntervalSeconds: 20,
292
- skipImport: false,
293
- timeFormat: '12hour',
294
- }
295
-
296
- return Object.assign(defaults, initialConfig)
297
- }
298
-
299
- export function formatWhereClause(key, value) {
300
- if (Array.isArray(value)) {
301
- let whereClause = `${sqlString.escapeId(key)} IN (${value
302
- .filter((v) => v !== null)
303
- .map((v) => sqlString.escape(v))
304
- .join(', ')})`
305
-
306
- if (value.includes(null)) {
307
- whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`
308
- }
309
-
310
- return whereClause
311
- }
312
-
313
- if (value === null) {
314
- return `${sqlString.escapeId(key)} IS NULL`
315
- }
316
-
317
- return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`
318
- }
319
-
320
- export function formatWhereClauses(query) {
321
- if (Object.keys(query).length === 0) {
322
- return ''
323
- }
324
-
325
- const whereClauses = Object.entries(query).map(([key, value]) =>
326
- formatWhereClause(key, value),
327
- )
328
- return `WHERE ${whereClauses.join(' AND ')}`
329
- }
package/locales/en.json DELETED
@@ -1,20 +0,0 @@
1
- {
2
- "Realtime Departures": "Realtime Departures",
3
- "By Stop": "By Stop",
4
- "By Route": "By Route",
5
- "Search by stop name or stop code": "Search by stop name or stop code",
6
- "all": "all",
7
- "Invalid stop code": "Invalid stop code",
8
- "Get Departures": "Get Departures",
9
- "Choose a route": "Choose a route",
10
- "Choose a direction": "Choose a direction",
11
- "Choose a stop": "Choose a stop",
12
- "Loading": "Loading",
13
- "Unknown Stop": "Unknown Stop",
14
- "As of": "As of",
15
- "Stop Code": "Stop Code",
16
- "min": "min",
17
- "No upcoming departures": "No upcoming departures",
18
- "Unable to fetch departures": "Unable to fetch departures",
19
- "To {{{headsign}}}": "To {{{headsign}}}"
20
- }
package/locales/pl.json DELETED
@@ -1,20 +0,0 @@
1
- {
2
- "Realtime Departures": "Przyjazdy w czasie rzeczywistym",
3
- "By Stop": "Wg przystanku",
4
- "By Route": "Wg linii",
5
- "Search by stop name or stop code": "Wyszukaj po nazwie przystanku lub jego identyfikatorze",
6
- "all": "wszystkie",
7
- "Invalid stop code": "Nieprawidłowy identyfikator przystanku",
8
- "Get Departures": "Uzyskaj przyjazdy",
9
- "Choose a route": "Wybierz linię",
10
- "Choose a direction": "Wybierz kierunek",
11
- "Choose a stop": "Wybierz przystanek",
12
- "Loading": "Ładowanie",
13
- "Unknown Stop": "Nieznany przystanek",
14
- "As of": "Aktualizacja",
15
- "Stop Code": "Przystanek",
16
- "min": "min",
17
- "No upcoming departures": "Brak nadchodzących przyjazdów",
18
- "Unable to fetch departures": "Nie można było pobrać przyjazdów",
19
- "To {{{headsign}}}": "Do {{{headsign}}}"
20
- }
@@ -1,200 +0,0 @@
1
- /* General Styles */
2
-
3
- body {
4
- font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
5
- font-size: 14px;
6
- line-height: 1.4;
7
- color: #333333;
8
- background-color: #ffffff;
9
- }
10
-
11
- .transit-departures-widget {
12
- max-width: 600px;
13
- }
14
-
15
- .transit-departures-widget .hidden-form {
16
- display: none;
17
- }
18
-
19
- .transit-departures-widget .stop-code-invalid {
20
- display: none;
21
- color: #821515;
22
- padding-bottom: 10px;
23
- }
24
-
25
- .transit-departures-widget .departure-results {
26
- display: none;
27
- margin-top: 25px;
28
- }
29
-
30
- .transit-departures-widget .departure-results-none {
31
- display: none;
32
- margin-top: 10px;
33
- }
34
-
35
- .transit-departures-widget .departure-results-error {
36
- display: none;
37
- margin-top: 10px;
38
- color: #821515;
39
- }
40
-
41
- .transit-departures-widget .departure-results-stop,
42
- .transit-departures-widget .departure-results-stop-unknown {
43
- font-size: 24px;
44
- line-height: 1;
45
- }
46
-
47
- .transit-departures-widget .departure-result {
48
- background-color: #eee;
49
- display: flex;
50
- align-items: center;
51
- justify-content: space-between;
52
- margin-top: 8px;
53
- }
54
-
55
- .transit-departures-widget .departure-results-header {
56
- display: flex;
57
- justify-content: space-between;
58
- align-items: flex-end;
59
- }
60
-
61
- .transit-departures-widget .departure-results-fetchtime {
62
- flex-shrink: 0;
63
- font-size: 12px;
64
- margin-left: 8px;
65
- padding: 0 0 0 15px;
66
- border: none;
67
- background-color: transparent;
68
- background-image: url('../img/refresh.svg');
69
- background-repeat: no-repeat;
70
- background-size: 12px 12px;
71
- background-position-y: 1px;
72
- }
73
-
74
- .transit-departures-widget .departure-results-fetchtime:hover {
75
- text-decoration: underline;
76
- }
77
-
78
- .transit-departures-widget .departure-result-route-name {
79
- display: flex;
80
- align-items: center;
81
- line-height: 1;
82
- }
83
-
84
- .transit-departures-widget .departure-result-route-direction {
85
- font-size: 22px;
86
- margin-top: 2px;
87
- }
88
-
89
- .transit-departures-widget .departure-result-route-circle {
90
- margin: 5px 9px 5px;
91
- width: 30px;
92
- height: 30px;
93
- border-radius: 50%;
94
- line-height: 30px;
95
- text-align: center;
96
- flex-shrink: 0;
97
- overflow: hidden;
98
- }
99
-
100
- .transit-departures-widget .departure-result-times {
101
- display: flex;
102
- align-items: stretch;
103
- flex-shrink: 0;
104
- align-self: stretch;
105
- }
106
-
107
- .transit-departures-widget .departure-result-time-container {
108
- padding: 0 5px;
109
- width: 76px;
110
- border-left: 1px solid #ddd;
111
- align-self: stretch;
112
- display: flex;
113
- align-items: center;
114
- justify-content: center;
115
- flex-shrink: 0;
116
- flex-grow: 0;
117
- }
118
-
119
- .transit-departures-widget .departure-result-time {
120
- font-size: 24px;
121
- }
122
-
123
- .transit-departures-widget .departure-result-time-label {
124
- font-size: 14px;
125
- padding-left: 2px;
126
- }
127
-
128
- .transit-departures-widget .loader,
129
- .transit-departures-widget .loader:after {
130
- border-radius: 50%;
131
- width: 10em;
132
- height: 10em;
133
- }
134
-
135
- .transit-departures-widget .loader {
136
- margin: 20px auto;
137
- font-size: 10px;
138
- position: relative;
139
- text-indent: -9999em;
140
- border-top: 1.1em solid rgba(79, 79, 79, 0.2);
141
- border-right: 1.1em solid rgba(79, 79, 79, 0.2);
142
- border-bottom: 1.1em solid rgba(79, 79, 79, 0.2);
143
- border-left: 1.1em solid #4f4f4f;
144
- -webkit-transform: translateZ(0);
145
- -ms-transform: translateZ(0);
146
- transform: translateZ(0);
147
- -webkit-animation: transitDeparturesWidgetLoader 1.1s infinite linear;
148
- animation: transitDeparturesWidgetLoader 1.1s infinite linear;
149
- display: none;
150
- }
151
-
152
- @-webkit-keyframes transitDeparturesWidgetLoader {
153
- 0% {
154
- -webkit-transform: rotate(0deg);
155
- transform: rotate(0deg);
156
- }
157
- 100% {
158
- -webkit-transform: rotate(360deg);
159
- transform: rotate(360deg);
160
- }
161
- }
162
-
163
- @keyframes transitDeparturesWidgetLoader {
164
- 0% {
165
- -webkit-transform: rotate(0deg);
166
- transform: rotate(0deg);
167
- }
168
- 100% {
169
- -webkit-transform: rotate(360deg);
170
- transform: rotate(360deg);
171
- }
172
- }
173
-
174
- .autocomplete {
175
- background: white;
176
- z-index: 1000;
177
- overflow: auto;
178
- box-sizing: border-box;
179
- border: 1px solid rgba(0, 0, 0, 0.125);
180
- }
181
-
182
- .autocomplete * {
183
- font: inherit;
184
- }
185
-
186
- .autocomplete > div {
187
- padding: 4px 4px;
188
- }
189
-
190
- .autocomplete .group {
191
- background: #eee;
192
- }
193
-
194
- .autocomplete > div:hover:not(.group),
195
- .autocomplete > div.selected {
196
- background: #007bff;
197
- color: #fff;
198
- font-weight: bold;
199
- cursor: pointer;
200
- }
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 256 256" style="enable-background:new 0 0 256 256;" xml:space="preserve"><path d="M226.74,199.28c-0.76,0.48-1.68,0.49-2.46,0.04l-16.82-9.7l-1.08,1.29c-19.26,23.11-47.55,36.37-77.61,36.37 c-48.57,0-90.32-34.6-99.26-82.27c-0.43-2.31,0.18-4.68,1.69-6.5c1.53-1.85,3.78-2.91,6.18-2.91H50.9c3.77,0,6.98,2.59,7.81,6.3 c3.49,15.69,12.32,29.94,24.87,40.12c12.73,10.33,28.78,16.02,45.21,16.02c19.4,0,37.57-7.62,51.17-21.45l1.81-1.84l-15.56-8.98 c-0.78-0.45-1.22-1.25-1.19-2.15c0.03-0.9,0.54-1.67,1.35-2.05l53.59-25.73c0.71-0.35,1.52-0.32,2.21,0.08 c0.69,0.4,1.12,1.08,1.18,1.88l4.51,59.27C227.92,197.99,227.5,198.81,226.74,199.28z"></path><path d="M226.03,119.33c-1.53,1.85-3.78,2.91-6.18,2.91h-13.51c-3.77,0-6.98-2.59-7.81-6.3c-3.49-15.69-12.32-29.94-24.87-40.12 c-12.73-10.33-28.78-16.02-45.21-16.02c-19.4,0-37.57,7.62-51.17,21.45l-1.81,1.84l15.56,8.98c0.78,0.45,1.22,1.25,1.19,2.15 c-0.03,0.9-0.54,1.67-1.35,2.05L37.29,122c-0.71,0.35-1.52,0.32-2.21-0.08c-0.69-0.4-1.12-1.08-1.18-1.88l-4.51-59.27 c-0.07-0.9,0.35-1.72,1.11-2.2s1.68-0.49,2.46-0.04l16.82,9.7l1.08-1.29c19.26-23.11,47.55-36.37,77.61-36.37 c48.57,0,90.32,34.6,99.26,82.27C228.15,115.14,227.54,117.51,226.03,119.33z"></path></svg>