transit-departures-widget 2.5.5 → 2.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/README.md +86 -14
- package/dist/app/index.js +276 -109
- package/dist/app/index.js.map +1 -1
- package/dist/bin/transit-departures-widget.js +254 -103
- package/dist/bin/transit-departures-widget.js.map +1 -1
- package/dist/frontend_libraries/accessible-autocomplete.min.css +3 -0
- package/dist/frontend_libraries/accessible-autocomplete.min.js +2 -0
- package/dist/frontend_libraries/gtfs-realtime.browser.proto.js +0 -0
- package/dist/frontend_libraries/pbf.js +1 -0
- package/dist/index.js +253 -102
- package/dist/index.js.map +1 -1
- package/package.json +23 -19
package/README.md
CHANGED
|
@@ -60,6 +60,32 @@ The following transit agencies use `transit-departures-widget` on their websites
|
|
|
60
60
|
- [Mountain View Community Shuttle](https://mvcommunityshuttle.com)
|
|
61
61
|
- [MVgo](https://mvgo.org/)
|
|
62
62
|
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
### CLI
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm install transit-departures-widget -g
|
|
69
|
+
transit-departures-widget --configPath ./config-sample.json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Outputs are written to `html/<agency_key>/`:
|
|
73
|
+
|
|
74
|
+
- `index.html` (full page unless `noHead: true`)
|
|
75
|
+
- `data/routes.json`
|
|
76
|
+
- `data/stops.json`
|
|
77
|
+
- `css/`, `js/` (when `noHead` is false)
|
|
78
|
+
|
|
79
|
+
### Programmatic
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import transitDeparturesWidget from 'transit-departures-widget'
|
|
83
|
+
import config from './config.json' assert { type: 'json' }
|
|
84
|
+
|
|
85
|
+
await transitDeparturesWidget(config)
|
|
86
|
+
// outputs to html/<agency_key>/ by default
|
|
87
|
+
```
|
|
88
|
+
|
|
63
89
|
## Command Line Usage
|
|
64
90
|
|
|
65
91
|
The `transit-departures-widget` command-line utility will download the GTFS file specified in `config.js` and then build the transit departures widget and save the HTML, CSS and JS in `html/:agency_key`.
|
|
@@ -92,20 +118,22 @@ Copy `config-sample.json` to `config.json` and then add your projects configurat
|
|
|
92
118
|
|
|
93
119
|
cp config-sample.json config.json
|
|
94
120
|
|
|
95
|
-
| option | type |
|
|
96
|
-
| --------------------------------------------------- | ------- |
|
|
97
|
-
| [`agency`](#agency) | object |
|
|
98
|
-
| [`
|
|
99
|
-
| [`
|
|
100
|
-
| [`
|
|
101
|
-
| [`
|
|
102
|
-
| [`
|
|
103
|
-
| [`
|
|
104
|
-
| [`
|
|
105
|
-
| [`
|
|
106
|
-
| [`
|
|
107
|
-
| [`
|
|
108
|
-
| [`
|
|
121
|
+
| option | type | default | notes |
|
|
122
|
+
| --------------------------------------------------- | ------- | --------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
123
|
+
| [`agency`](#agency) | object | — | Required: GTFS static (url or path) and GTFS-RT trip updates URL. |
|
|
124
|
+
| [`assetPath`](#assetpath) | string | `''` | Prefix for assets in HTML; set when hosting assets elsewhere. |
|
|
125
|
+
| [`beautify`](#beautify) | boolean | `false` | Pretty-print HTML output. |
|
|
126
|
+
| [`endDate`](#enddate) | string | all svc | YYYYMMDD calendar filter upper bound. |
|
|
127
|
+
| [`includeCoordinates`](#includecoordinates) | boolean | `false` | Include stop lat/lon in `stops.json`. |
|
|
128
|
+
| [`locale`](#locale) | string | `en` | UI language code. |
|
|
129
|
+
| [`noHead`](#nohead) | boolean | `false` | If true, omit `<html>/<head>/<body>`; only widget markup is output. |
|
|
130
|
+
| [`outputPath`](#outputpath) | string | `./html/<agency_key>` | Where to write generated files. |
|
|
131
|
+
| [`refreshIntervalSeconds`](#refreshIntervalSeconds) | integer | `20` | Autorefresh interval on the widget page. |
|
|
132
|
+
| [`skipImport`](#skipimport) | boolean | `false` | Skip GTFS import if DB already populated (`sqlitePath` recommended). |
|
|
133
|
+
| [`sqlitePath`](#sqlitepath) | string | in-memory | Path to SQLite DB file; enables reusing imports across runs. |
|
|
134
|
+
| [`startDate`](#startdate) | string | all svc | YYYYMMDD calendar filter lower bound. |
|
|
135
|
+
| [`templatePath`](#templatepath) | string | built-in | Custom templates folder (expects `widget.pug` and `widget_full.pug`). |
|
|
136
|
+
| [`timeFormat`](#timeFormat) | string | `12hour` | `12hour` or `24hour` time display. |
|
|
109
137
|
|
|
110
138
|
### agency
|
|
111
139
|
|
|
@@ -195,6 +223,24 @@ Copy `config-sample.json` to `config.json` and then add your projects configurat
|
|
|
195
223
|
"noHead": false
|
|
196
224
|
```
|
|
197
225
|
|
|
226
|
+
If `noHead` is `true`, you’ll embed the widget into an existing HTML page. See the examples below for including the generated assets.
|
|
227
|
+
|
|
228
|
+
### assetPath
|
|
229
|
+
|
|
230
|
+
{String} Prefix to use when linking to generated assets (`css`, `js`). Useful if you host assets on a CDN or a different path from the HTML file.
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
"assetPath": "/static/widget/"
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### outputPath
|
|
237
|
+
|
|
238
|
+
{String} Path where generated files are written. Defaults to `./html/<agency_key>`.
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
"outputPath": "/var/www/widget-output"
|
|
242
|
+
```
|
|
243
|
+
|
|
198
244
|
### refreshIntervalSeconds
|
|
199
245
|
|
|
200
246
|
{Integer} How often the widget should refresh departure data in seconds. Optional, defaults to 20 seconds.
|
|
@@ -259,6 +305,32 @@ By default, `transit-departures-widget` will look for a `config.json` file in th
|
|
|
259
305
|
|
|
260
306
|
Once running, you can view the HTML in your browser at [localhost:3000](http://localhost:3000)
|
|
261
307
|
|
|
308
|
+
## Embedding examples
|
|
309
|
+
|
|
310
|
+
### Default (with head/footer)
|
|
311
|
+
|
|
312
|
+
If `noHead` is `false` (default), `index.html` is a complete HTML page with linked assets in `css/` and `js/` under the output directory. You can host that folder as-is (e.g., serve `html/<agency_key>/` from your web server root).
|
|
313
|
+
|
|
314
|
+
### Headless embed (`noHead: true`)
|
|
315
|
+
|
|
316
|
+
When `noHead` is `true`, you get only the widget markup. Include the generated CSS/JS and mount it in your page:
|
|
317
|
+
|
|
318
|
+
```html
|
|
319
|
+
<!doctype html>
|
|
320
|
+
<html>
|
|
321
|
+
<head>
|
|
322
|
+
<link rel="stylesheet" href="/path/to/css/transit-departures-widget-styles.css" />
|
|
323
|
+
</head>
|
|
324
|
+
<body>
|
|
325
|
+
<div id="tdw-app"></div>
|
|
326
|
+
<script src="/path/to/js/transit-departures-widget.js"></script>
|
|
327
|
+
<script>
|
|
328
|
+
// Transit Departures Widget initializes itself on load using data/routes.json and data/stops.json
|
|
329
|
+
</script>
|
|
330
|
+
</body>
|
|
331
|
+
</html>
|
|
332
|
+
```
|
|
333
|
+
|
|
262
334
|
## Notes
|
|
263
335
|
|
|
264
336
|
`transit-departures-widget` uses the [`node-gtfs`](https://github.com/blinktaginc/node-gtfs) library to handle importing and querying GTFS data.
|
package/dist/app/index.js
CHANGED
|
@@ -1,39 +1,46 @@
|
|
|
1
1
|
// src/app/index.ts
|
|
2
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
3
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2
4
|
import { readFileSync } from "fs";
|
|
3
|
-
import { join as join3 } from "path";
|
|
4
5
|
import yargs from "yargs";
|
|
5
|
-
import {
|
|
6
|
+
import { hideBin } from "yargs/helpers";
|
|
7
|
+
import { openDb as openDb2, importGtfs } from "gtfs";
|
|
8
|
+
import express from "express";
|
|
6
9
|
import { clone, omit } from "lodash-es";
|
|
7
10
|
import untildify2 from "untildify";
|
|
8
|
-
import express from "express";
|
|
9
|
-
import logger from "morgan";
|
|
10
|
-
|
|
11
|
-
// src/lib/utils.ts
|
|
12
|
-
import { join as join2 } from "path";
|
|
13
|
-
import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
|
|
14
|
-
import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
|
|
15
11
|
|
|
16
12
|
// src/lib/file-utils.ts
|
|
17
13
|
import { dirname, join, resolve } from "path";
|
|
18
14
|
import { fileURLToPath } from "url";
|
|
19
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
access,
|
|
17
|
+
cp,
|
|
18
|
+
copyFile,
|
|
19
|
+
mkdir,
|
|
20
|
+
readdir,
|
|
21
|
+
readFile,
|
|
22
|
+
rm
|
|
23
|
+
} from "fs/promises";
|
|
20
24
|
import beautify from "js-beautify";
|
|
21
25
|
import pug from "pug";
|
|
22
26
|
import untildify from "untildify";
|
|
23
|
-
function
|
|
24
|
-
if (config2.templatePath) {
|
|
25
|
-
return untildify(config2.templatePath);
|
|
26
|
-
}
|
|
27
|
+
function getPathToThisModuleFolder() {
|
|
27
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
|
-
let
|
|
29
|
+
let distFolderPath;
|
|
29
30
|
if (__dirname.endsWith("/dist/bin") || __dirname.endsWith("/dist/app")) {
|
|
30
|
-
|
|
31
|
+
distFolderPath = resolve(__dirname, "../../");
|
|
31
32
|
} else if (__dirname.endsWith("/dist")) {
|
|
32
|
-
|
|
33
|
+
distFolderPath = resolve(__dirname, "../");
|
|
33
34
|
} else {
|
|
34
|
-
|
|
35
|
+
distFolderPath = resolve(__dirname, "../../");
|
|
35
36
|
}
|
|
36
|
-
return
|
|
37
|
+
return distFolderPath;
|
|
38
|
+
}
|
|
39
|
+
function getPathToViewsFolder(config2) {
|
|
40
|
+
if (config2.templatePath) {
|
|
41
|
+
return untildify(config2.templatePath);
|
|
42
|
+
}
|
|
43
|
+
return join(getPathToThisModuleFolder(), "views/default");
|
|
37
44
|
}
|
|
38
45
|
function getPathToTemplateFile(templateFileName, config2) {
|
|
39
46
|
const fullTemplateFileName = config2.noHead !== true ? `${templateFileName}_full.pug` : `${templateFileName}.pug`;
|
|
@@ -49,15 +56,45 @@ async function renderFile(templateFileName, templateVars, config2) {
|
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
// src/lib/utils.ts
|
|
59
|
+
import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
|
|
60
|
+
import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
|
|
52
61
|
import sqlString from "sqlstring-sqlite";
|
|
53
62
|
import toposort from "toposort";
|
|
54
|
-
import { I18n } from "i18n";
|
|
55
63
|
|
|
56
|
-
// src/lib/log
|
|
64
|
+
// src/lib/logging/log.ts
|
|
57
65
|
import { clearLine, cursorTo } from "readline";
|
|
58
66
|
import { noop } from "lodash-es";
|
|
59
67
|
import * as colors from "yoctocolors";
|
|
60
|
-
|
|
68
|
+
var formatWarning = (text) => {
|
|
69
|
+
const warningMessage = `${colors.underline("Warning")}: ${text}`;
|
|
70
|
+
return colors.yellow(warningMessage);
|
|
71
|
+
};
|
|
72
|
+
var formatError = (error) => {
|
|
73
|
+
const messageText = error instanceof Error ? error.message : error;
|
|
74
|
+
const errorMessage = `${colors.underline("Error")}: ${messageText.replace(
|
|
75
|
+
"Error: ",
|
|
76
|
+
""
|
|
77
|
+
)}`;
|
|
78
|
+
return colors.red(errorMessage);
|
|
79
|
+
};
|
|
80
|
+
var logInfo = (config2) => {
|
|
81
|
+
if (config2.verbose === false) {
|
|
82
|
+
return noop;
|
|
83
|
+
}
|
|
84
|
+
if (config2.logFunction) {
|
|
85
|
+
return config2.logFunction;
|
|
86
|
+
}
|
|
87
|
+
return (text, overwrite) => {
|
|
88
|
+
if (overwrite === true && process.stdout.isTTY) {
|
|
89
|
+
clearLine(process.stdout, 0);
|
|
90
|
+
cursorTo(process.stdout, 0);
|
|
91
|
+
} else {
|
|
92
|
+
process.stdout.write("\n");
|
|
93
|
+
}
|
|
94
|
+
process.stdout.write(text);
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
var logWarn = (config2) => {
|
|
61
98
|
if (config2.logFunction) {
|
|
62
99
|
return config2.logFunction;
|
|
63
100
|
}
|
|
@@ -66,10 +103,59 @@ function logWarning(config2) {
|
|
|
66
103
|
${formatWarning(text)}
|
|
67
104
|
`);
|
|
68
105
|
};
|
|
106
|
+
};
|
|
107
|
+
var logError = (config2) => {
|
|
108
|
+
if (config2.logFunction) {
|
|
109
|
+
return config2.logFunction;
|
|
110
|
+
}
|
|
111
|
+
return (text) => {
|
|
112
|
+
process.stdout.write(`
|
|
113
|
+
${formatError(text)}
|
|
114
|
+
`);
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
function createLogger(config2) {
|
|
118
|
+
return {
|
|
119
|
+
info: logInfo(config2),
|
|
120
|
+
warn: logWarn(config2),
|
|
121
|
+
error: logError(config2)
|
|
122
|
+
};
|
|
69
123
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
124
|
+
|
|
125
|
+
// src/lib/logging/messages.ts
|
|
126
|
+
var messages = {
|
|
127
|
+
noActiveCalendarsGlobal: "No active calendars found for the configured date range - returning empty routes and stops",
|
|
128
|
+
noActiveCalendarsForRoute: (routeId) => `route_id ${routeId} has no active calendars in range - skipping directions`,
|
|
129
|
+
noActiveCalendarsForDirection: (routeId, directionId) => `route_id ${routeId} direction ${directionId} has no active calendars in range - skipping stops`,
|
|
130
|
+
routeHasNoDirections: (routeId) => `route_id ${routeId} has no directions - skipping`,
|
|
131
|
+
stopNotFound: (routeId, directionId, stopId) => `stop_id ${stopId} for route ${routeId} direction ${directionId} not found - dropping`
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// src/lib/config/defaults.ts
|
|
135
|
+
import { join as join2 } from "path";
|
|
136
|
+
import { I18n } from "i18n";
|
|
137
|
+
function setDefaultConfig(initialConfig) {
|
|
138
|
+
const defaults = {
|
|
139
|
+
beautify: false,
|
|
140
|
+
noHead: false,
|
|
141
|
+
refreshIntervalSeconds: 20,
|
|
142
|
+
skipImport: false,
|
|
143
|
+
timeFormat: "12hour",
|
|
144
|
+
includeCoordinates: false,
|
|
145
|
+
overwriteExistingFiles: true,
|
|
146
|
+
verbose: true
|
|
147
|
+
};
|
|
148
|
+
const config2 = Object.assign(defaults, initialConfig);
|
|
149
|
+
const viewsFolderPath = getPathToViewsFolder(config2);
|
|
150
|
+
const i18n = new I18n({
|
|
151
|
+
directory: join2(viewsFolderPath, "locales"),
|
|
152
|
+
defaultLocale: config2.locale,
|
|
153
|
+
updateFiles: false
|
|
154
|
+
});
|
|
155
|
+
const configWithI18n = Object.assign(config2, {
|
|
156
|
+
__: i18n.__
|
|
157
|
+
});
|
|
158
|
+
return configWithI18n;
|
|
73
159
|
}
|
|
74
160
|
|
|
75
161
|
// src/lib/utils.ts
|
|
@@ -102,12 +188,20 @@ function formatRouteName(route) {
|
|
|
102
188
|
return routeName;
|
|
103
189
|
}
|
|
104
190
|
function getDirectionsForRoute(route, config2) {
|
|
191
|
+
const logger = createLogger(config2);
|
|
105
192
|
const db = openDb(config2);
|
|
106
193
|
const directions = getDirections({ route_id: route.route_id }, [
|
|
107
194
|
"direction_id",
|
|
108
195
|
"direction"
|
|
109
|
-
])
|
|
196
|
+
]).filter((direction) => direction.direction_id !== void 0).map((direction) => ({
|
|
197
|
+
direction_id: direction.direction_id,
|
|
198
|
+
direction: direction.direction
|
|
199
|
+
}));
|
|
110
200
|
const calendars = getCalendarsForDateRange(config2);
|
|
201
|
+
if (calendars.length === 0) {
|
|
202
|
+
logger.warn(messages.noActiveCalendarsForRoute(route.route_id));
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
111
205
|
if (directions.length === 0) {
|
|
112
206
|
const headsigns = db.prepare(
|
|
113
207
|
`SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars.map((calendar) => `'${calendar.service_id}'`).join(", ")}) GROUP BY direction_id, trip_headsign`
|
|
@@ -148,11 +242,24 @@ function sortStopIdsBySequence(stoptimes) {
|
|
|
148
242
|
Object.values(stoptimesGroupedByTrip),
|
|
149
243
|
(stoptimes2) => size(stoptimes2)
|
|
150
244
|
);
|
|
245
|
+
if (!longestTripStoptimes) {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
151
248
|
return longestTripStoptimes.map((stoptime) => stoptime.stop_id);
|
|
152
249
|
}
|
|
153
|
-
function getStopsForDirection(route, direction, config2) {
|
|
250
|
+
function getStopsForDirection(route, direction, config2, stopCache) {
|
|
251
|
+
const logger = createLogger(config2);
|
|
154
252
|
const db = openDb(config2);
|
|
155
253
|
const calendars = getCalendarsForDateRange(config2);
|
|
254
|
+
if (calendars.length === 0) {
|
|
255
|
+
logger.warn(
|
|
256
|
+
messages.noActiveCalendarsForDirection(
|
|
257
|
+
route.route_id,
|
|
258
|
+
direction.direction_id
|
|
259
|
+
)
|
|
260
|
+
);
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
156
263
|
const whereClause = formatWhereClauses({
|
|
157
264
|
direction_id: direction.direction_id,
|
|
158
265
|
route_id: route.route_id,
|
|
@@ -172,14 +279,34 @@ function getStopsForDirection(route, direction, config2) {
|
|
|
172
279
|
[]
|
|
173
280
|
);
|
|
174
281
|
deduplicatedStopIds.pop();
|
|
175
|
-
const stopFields = [
|
|
282
|
+
const stopFields = [
|
|
283
|
+
"stop_id",
|
|
284
|
+
"stop_name",
|
|
285
|
+
"stop_code",
|
|
286
|
+
"parent_station"
|
|
287
|
+
];
|
|
176
288
|
if (config2.includeCoordinates) {
|
|
177
289
|
stopFields.push("stop_lat", "stop_lon");
|
|
178
290
|
}
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
291
|
+
const missingStopIds = stopCache ? deduplicatedStopIds.filter((stopId) => !stopCache.has(stopId)) : deduplicatedStopIds;
|
|
292
|
+
const fetchedStops = missingStopIds.length ? getStops(
|
|
293
|
+
{ stop_id: missingStopIds },
|
|
294
|
+
stopFields
|
|
295
|
+
) : [];
|
|
296
|
+
if (stopCache) {
|
|
297
|
+
for (const stop of fetchedStops) {
|
|
298
|
+
stopCache.set(stop.stop_id, stop);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return deduplicatedStopIds.map((stopId) => {
|
|
302
|
+
const stop = stopCache?.get(stopId) ?? fetchedStops.find((candidate) => candidate.stop_id === stopId);
|
|
303
|
+
if (!stop) {
|
|
304
|
+
logger.warn(
|
|
305
|
+
messages.stopNotFound(route.route_id, direction.direction_id, stopId)
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
return stop;
|
|
309
|
+
}).filter(Boolean);
|
|
183
310
|
}
|
|
184
311
|
function generateTransitDeparturesWidgetHtml(config2) {
|
|
185
312
|
const templateVars = {
|
|
@@ -189,23 +316,37 @@ function generateTransitDeparturesWidgetHtml(config2) {
|
|
|
189
316
|
return renderFile("widget", templateVars, config2);
|
|
190
317
|
}
|
|
191
318
|
function generateTransitDeparturesWidgetJson(config2) {
|
|
319
|
+
const logger = createLogger(config2);
|
|
320
|
+
const calendars = getCalendarsForDateRange(config2);
|
|
321
|
+
if (calendars.length === 0) {
|
|
322
|
+
logger.warn(messages.noActiveCalendarsGlobal);
|
|
323
|
+
return { routes: [], stops: [] };
|
|
324
|
+
}
|
|
192
325
|
const routes = getRoutes();
|
|
193
326
|
const stops = [];
|
|
194
327
|
const filteredRoutes = [];
|
|
195
|
-
const
|
|
328
|
+
const stopCache = /* @__PURE__ */ new Map();
|
|
196
329
|
for (const route of routes) {
|
|
197
|
-
|
|
198
|
-
|
|
330
|
+
const routeWithFullName = {
|
|
331
|
+
...route,
|
|
332
|
+
route_full_name: formatRouteName(route)
|
|
333
|
+
};
|
|
334
|
+
const directions = getDirectionsForRoute(routeWithFullName, config2);
|
|
199
335
|
if (directions.length === 0) {
|
|
200
|
-
|
|
201
|
-
`route_id ${route.route_id} has no directions - skipping`
|
|
202
|
-
);
|
|
336
|
+
logger.warn(messages.routeHasNoDirections(route.route_id));
|
|
203
337
|
continue;
|
|
204
338
|
}
|
|
205
|
-
|
|
206
|
-
const directionStops = getStopsForDirection(
|
|
339
|
+
const directionsWithData = directions.map((direction) => {
|
|
340
|
+
const directionStops = getStopsForDirection(
|
|
341
|
+
routeWithFullName,
|
|
342
|
+
direction,
|
|
343
|
+
config2,
|
|
344
|
+
stopCache
|
|
345
|
+
);
|
|
346
|
+
if (directionStops.length === 0) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
207
349
|
stops.push(...directionStops);
|
|
208
|
-
direction.stopIds = directionStops.map((stop) => stop?.stop_id);
|
|
209
350
|
const trips = getTrips(
|
|
210
351
|
{
|
|
211
352
|
route_id: route.route_id,
|
|
@@ -216,69 +357,82 @@ function generateTransitDeparturesWidgetJson(config2) {
|
|
|
216
357
|
},
|
|
217
358
|
["trip_id"]
|
|
218
359
|
);
|
|
219
|
-
|
|
360
|
+
return {
|
|
361
|
+
...direction,
|
|
362
|
+
stopIds: directionStops.map((stop) => stop.stop_id),
|
|
363
|
+
tripIds: trips.map((trip) => trip.trip_id)
|
|
364
|
+
};
|
|
365
|
+
}).filter(Boolean);
|
|
366
|
+
if (directionsWithData.length === 0) {
|
|
367
|
+
continue;
|
|
220
368
|
}
|
|
221
|
-
|
|
222
|
-
|
|
369
|
+
filteredRoutes.push({
|
|
370
|
+
...routeWithFullName,
|
|
371
|
+
directions: directionsWithData
|
|
372
|
+
});
|
|
223
373
|
}
|
|
224
|
-
const sortedRoutes =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
374
|
+
const sortedRoutes = [...filteredRoutes].sort((a, b) => {
|
|
375
|
+
const aShort = a.route_short_name ?? "";
|
|
376
|
+
const bShort = b.route_short_name ?? "";
|
|
377
|
+
const aNum = Number.parseInt(aShort, 10);
|
|
378
|
+
const bNum = Number.parseInt(bShort, 10);
|
|
379
|
+
if (!Number.isNaN(aNum) && !Number.isNaN(bNum) && aNum !== bNum) {
|
|
380
|
+
return aNum - bNum;
|
|
381
|
+
}
|
|
382
|
+
if (Number.isNaN(aNum) && !Number.isNaN(bNum)) {
|
|
383
|
+
return 1;
|
|
384
|
+
}
|
|
385
|
+
if (!Number.isNaN(aNum) && Number.isNaN(bNum)) {
|
|
386
|
+
return -1;
|
|
387
|
+
}
|
|
388
|
+
return aShort.localeCompare(bShort, void 0, {
|
|
389
|
+
numeric: true,
|
|
390
|
+
sensitivity: "base"
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
const parentStationIds = new Set(stops.map((stop) => stop.parent_station));
|
|
229
394
|
const parentStationStops = getStops(
|
|
230
395
|
{ stop_id: Array.from(parentStationIds) },
|
|
231
396
|
["stop_id", "stop_name", "stop_code", "parent_station"]
|
|
232
397
|
);
|
|
233
398
|
stops.push(
|
|
234
|
-
...parentStationStops.map((stop) => {
|
|
235
|
-
stop
|
|
236
|
-
|
|
237
|
-
})
|
|
399
|
+
...parentStationStops.map((stop) => ({
|
|
400
|
+
...stop,
|
|
401
|
+
is_parent_station: true
|
|
402
|
+
}))
|
|
238
403
|
);
|
|
239
404
|
const sortedStops = sortBy(uniqBy(stops, "stop_id"), "stop_name");
|
|
240
405
|
return {
|
|
241
|
-
routes: removeNulls(sortedRoutes),
|
|
242
|
-
stops: removeNulls(sortedStops)
|
|
406
|
+
routes: arrayOfArrays(removeNulls(sortedRoutes)),
|
|
407
|
+
stops: arrayOfArrays(removeNulls(sortedStops))
|
|
243
408
|
};
|
|
244
409
|
}
|
|
245
410
|
function removeNulls(data) {
|
|
246
411
|
if (Array.isArray(data)) {
|
|
247
412
|
return data.map(removeNulls).filter((item) => item !== null && item !== void 0);
|
|
248
|
-
} else if (typeof data === "object" && data
|
|
249
|
-
return Object.entries(data).reduce(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
413
|
+
} else if (data !== null && typeof data === "object" && Object.getPrototypeOf(data) === Object.prototype) {
|
|
414
|
+
return Object.entries(data).reduce(
|
|
415
|
+
(acc, [key, value]) => {
|
|
416
|
+
const cleanedValue = removeNulls(value);
|
|
417
|
+
if (cleanedValue !== null && cleanedValue !== void 0) {
|
|
418
|
+
acc[key] = cleanedValue;
|
|
419
|
+
}
|
|
420
|
+
return acc;
|
|
421
|
+
},
|
|
422
|
+
{}
|
|
423
|
+
);
|
|
256
424
|
} else {
|
|
257
425
|
return data;
|
|
258
426
|
}
|
|
259
427
|
}
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
includeCoordinates: false,
|
|
268
|
-
overwriteExistingFiles: true,
|
|
269
|
-
verbose: true
|
|
428
|
+
function arrayOfArrays(array) {
|
|
429
|
+
if (array.length === 0) {
|
|
430
|
+
return { fields: [], rows: [] };
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
fields: Object.keys(array[0]),
|
|
434
|
+
rows: array.map((item) => Object.values(item))
|
|
270
435
|
};
|
|
271
|
-
const config2 = Object.assign(defaults, initialConfig);
|
|
272
|
-
const viewsFolderPath = getPathToViewsFolder(config2);
|
|
273
|
-
const i18n = new I18n({
|
|
274
|
-
directory: join2(viewsFolderPath, "locales"),
|
|
275
|
-
defaultLocale: config2.locale,
|
|
276
|
-
updateFiles: false
|
|
277
|
-
});
|
|
278
|
-
const configWithI18n = Object.assign(config2, {
|
|
279
|
-
__: i18n.__
|
|
280
|
-
});
|
|
281
|
-
return configWithI18n;
|
|
282
436
|
}
|
|
283
437
|
function formatWhereClause(key, value) {
|
|
284
438
|
if (Array.isArray(value)) {
|
|
@@ -304,7 +458,7 @@ function formatWhereClauses(query) {
|
|
|
304
458
|
}
|
|
305
459
|
|
|
306
460
|
// src/app/index.ts
|
|
307
|
-
var argv = yargs(process.argv).option("c", {
|
|
461
|
+
var argv = yargs(hideBin(process.argv)).option("c", {
|
|
308
462
|
alias: "configPath",
|
|
309
463
|
describe: "Path to config file",
|
|
310
464
|
default: "./config.json",
|
|
@@ -319,27 +473,45 @@ config.assetPath = "/";
|
|
|
319
473
|
config.logFunction = console.log;
|
|
320
474
|
try {
|
|
321
475
|
openDb2(config);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
...clone(omit(config, "agency")),
|
|
328
|
-
agencies: [
|
|
329
|
-
{
|
|
330
|
-
agency_key: config.agency.agency_key,
|
|
331
|
-
path: config.agency.gtfs_static_path,
|
|
332
|
-
url: config.agency.gtfs_static_url
|
|
333
|
-
}
|
|
334
|
-
]
|
|
335
|
-
};
|
|
336
|
-
await importGtfs(gtfsImportConfig);
|
|
337
|
-
} catch (error2) {
|
|
338
|
-
console.error(
|
|
339
|
-
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists and import GTFS before running this app.`
|
|
476
|
+
const gtfsPath = config.agency.gtfs_static_path;
|
|
477
|
+
const gtfsUrl = config.agency.gtfs_static_url;
|
|
478
|
+
if (!gtfsPath && !gtfsUrl) {
|
|
479
|
+
throw new Error(
|
|
480
|
+
"Missing GTFS source. Set `agency.gtfs_static_path` or `agency.gtfs_static_url` in config.json."
|
|
340
481
|
);
|
|
341
|
-
throw error2;
|
|
342
482
|
}
|
|
483
|
+
const agencyImportConfig = gtfsPath ? { path: gtfsPath } : { url: gtfsUrl };
|
|
484
|
+
const gtfsImportConfig = {
|
|
485
|
+
...clone(omit(config, "agency")),
|
|
486
|
+
agencies: [agencyImportConfig]
|
|
487
|
+
};
|
|
488
|
+
await importGtfs(gtfsImportConfig);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
console.error(
|
|
491
|
+
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists and run gtfs-to-html to import GTFS before running this app.`
|
|
492
|
+
);
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
app.set("views", getPathToViewsFolder(config));
|
|
496
|
+
app.set("view engine", "pug");
|
|
497
|
+
app.use((req, res, next) => {
|
|
498
|
+
console.log(`${req.method} ${req.url}`);
|
|
499
|
+
next();
|
|
500
|
+
});
|
|
501
|
+
var staticAssetPath = config.templatePath === void 0 ? getPathToViewsFolder(config) : untildify2(config.templatePath);
|
|
502
|
+
app.use(express.static(staticAssetPath));
|
|
503
|
+
var frontendLibraryPaths = [
|
|
504
|
+
{ route: "/js", package: "pbf", subPath: "dist" },
|
|
505
|
+
{ route: "/js", package: "gtfs-realtime-pbf-js-module", subPath: "" },
|
|
506
|
+
{ route: "/js", package: "accessible-autocomplete", subPath: "" },
|
|
507
|
+
{ route: "/css", package: "accessible-autocomplete", subPath: "" }
|
|
508
|
+
];
|
|
509
|
+
var resolvePackagePath = (packageName, subPath) => {
|
|
510
|
+
const packagePath = dirname2(fileURLToPath2(import.meta.resolve(packageName)));
|
|
511
|
+
return subPath ? join3(packagePath, subPath) : packagePath;
|
|
512
|
+
};
|
|
513
|
+
for (const { route, package: pkg, subPath } of frontendLibraryPaths) {
|
|
514
|
+
app.use(route, express.static(resolvePackagePath(pkg, subPath)));
|
|
343
515
|
}
|
|
344
516
|
app.get("/", async (request, response, next) => {
|
|
345
517
|
try {
|
|
@@ -365,11 +537,6 @@ app.get("/data/stops.json", async (request, response, next) => {
|
|
|
365
537
|
next(error);
|
|
366
538
|
}
|
|
367
539
|
});
|
|
368
|
-
app.set("views", getPathToViewsFolder(config));
|
|
369
|
-
app.set("view engine", "pug");
|
|
370
|
-
app.use(logger("dev"));
|
|
371
|
-
var staticAssetPath = config.templatePath === void 0 ? getPathToViewsFolder(config) : untildify2(config.templatePath);
|
|
372
|
-
app.use(express.static(staticAssetPath));
|
|
373
540
|
app.use((req, res) => {
|
|
374
541
|
res.status(404).send("Not Found");
|
|
375
542
|
});
|