gtfs 4.11.2 → 4.12.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/.github/workflows/nodejs.yml +1 -1
- package/CHANGELOG.md +17 -0
- package/lib/export.js +4 -0
- package/lib/gtfs/routes.js +23 -7
- package/lib/import.js +49 -46
- package/package.json +2 -2
- package/test/mocha/get-locations.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ 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.12.0] - 2024-06-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Support for `service_id` in `getRoutes`
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Use tempy for temporary directory creation
|
|
17
|
+
|
|
18
|
+
## [4.11.3] - 2024-06-13
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Better tests for locations.geojson
|
|
23
|
+
- Handle unknown filename extensions in models
|
|
24
|
+
|
|
8
25
|
## [4.11.2] - 2024-06-13
|
|
9
26
|
|
|
10
27
|
### Added
|
package/lib/export.js
CHANGED
|
@@ -117,6 +117,10 @@ const exportGtfs = async (initialConfig) => {
|
|
|
117
117
|
} else if (model.filenameExtension === 'geojson') {
|
|
118
118
|
const fileText = lines?.[0].geojson ?? '';
|
|
119
119
|
await writeFile(filePath, fileText);
|
|
120
|
+
} else {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Unexpected filename extension: ${model.filenameExtension}`,
|
|
123
|
+
);
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
log(`Exporting - ${model.filenameBase}.${model.filenameExtension}\r`);
|
package/lib/gtfs/routes.js
CHANGED
|
@@ -17,14 +17,30 @@ function buildStoptimeSubquery(query) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function buildTripSubquery(query) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
)
|
|
20
|
+
let whereClause = '';
|
|
21
|
+
const tripQuery = omit(query, ['stop_id']);
|
|
22
|
+
const stoptimeQuery = pick(query, ['stop_id']);
|
|
23
|
+
|
|
24
|
+
const whereClauses = Object.entries(tripQuery).map(([key, value]) =>
|
|
25
|
+
formatWhereClause(key, value),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (Object.values(stoptimeQuery).length > 0) {
|
|
29
|
+
whereClauses.push(`trip_id IN (${buildStoptimeSubquery(stoptimeQuery)})`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (whereClauses.length > 0) {
|
|
33
|
+
whereClause = `WHERE ${whereClauses.join(' AND ')}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return `SELECT DISTINCT route_id FROM trips ${whereClause}`;
|
|
23
37
|
}
|
|
24
38
|
|
|
25
39
|
/*
|
|
26
40
|
* Returns an array of routes that match the query parameters. A `stop_id`
|
|
27
41
|
* query parameter may be passed to find all routes that contain that stop.
|
|
42
|
+
* A `service_id` query parameter may be passed to limit routes to specific
|
|
43
|
+
* calendars.
|
|
28
44
|
*/
|
|
29
45
|
export function getRoutes(query = {}, fields = [], orderBy = [], options = {}) {
|
|
30
46
|
const db = options.db ?? openDb();
|
|
@@ -33,15 +49,15 @@ export function getRoutes(query = {}, fields = [], orderBy = [], options = {}) {
|
|
|
33
49
|
let whereClause = '';
|
|
34
50
|
const orderByClause = formatOrderByClause(orderBy);
|
|
35
51
|
|
|
36
|
-
const routeQuery = omit(query, ['stop_id']);
|
|
37
|
-
const
|
|
52
|
+
const routeQuery = omit(query, ['stop_id', 'service_id']);
|
|
53
|
+
const tripQuery = pick(query, ['stop_id', 'service_id']);
|
|
38
54
|
|
|
39
55
|
const whereClauses = Object.entries(routeQuery).map(([key, value]) =>
|
|
40
56
|
formatWhereClause(key, value),
|
|
41
57
|
);
|
|
42
58
|
|
|
43
|
-
if (Object.values(
|
|
44
|
-
whereClauses.push(`route_id IN (${buildTripSubquery(
|
|
59
|
+
if (Object.values(tripQuery).length > 0) {
|
|
60
|
+
whereClauses.push(`route_id IN (${buildTripSubquery(tripQuery)})`);
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
if (whereClauses.length > 0) {
|
package/lib/import.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { createReadStream, existsSync, lstatSync } from 'node:fs';
|
|
3
|
-
import { cp, readdir, rename, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { cp, readdir, rename, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import fetch from 'node-fetch';
|
|
5
5
|
import { parse } from 'csv-parse';
|
|
6
6
|
import pluralize from 'pluralize';
|
|
7
7
|
import stripBomStream from 'strip-bom-stream';
|
|
8
|
-
import {
|
|
8
|
+
import { temporaryDirectory } from 'tempy';
|
|
9
9
|
import untildify from 'untildify';
|
|
10
10
|
import mapSeries from 'promise-map-series';
|
|
11
11
|
import GtfsRealtimeBindings from 'gtfs-realtime-bindings';
|
|
@@ -604,8 +604,46 @@ const importFiles = (task) =>
|
|
|
604
604
|
`Importing - ${model.filenameBase}.${model.filenameExtension}\r`,
|
|
605
605
|
);
|
|
606
606
|
|
|
607
|
-
|
|
608
|
-
|
|
607
|
+
if (model.filenameExtension === 'txt') {
|
|
608
|
+
const parser = parse({
|
|
609
|
+
columns: true,
|
|
610
|
+
relax_quotes: true,
|
|
611
|
+
trim: true,
|
|
612
|
+
skip_empty_lines: true,
|
|
613
|
+
...task.csvOptions,
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
parser.on('readable', () => {
|
|
617
|
+
let record;
|
|
618
|
+
|
|
619
|
+
while ((record = parser.read())) {
|
|
620
|
+
try {
|
|
621
|
+
totalLineCount += 1;
|
|
622
|
+
lines.push(formatLine(record, model, totalLineCount));
|
|
623
|
+
// If we have a bunch of lines ready to insert, then do it
|
|
624
|
+
if (lines.length >= maxInsertVariables / model.schema.length) {
|
|
625
|
+
importLines(task, lines, model, totalLineCount);
|
|
626
|
+
}
|
|
627
|
+
} catch (error) {
|
|
628
|
+
reject(error);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
parser.on('end', () => {
|
|
634
|
+
try {
|
|
635
|
+
// Insert all remaining lines
|
|
636
|
+
importLines(task, lines, model, totalLineCount);
|
|
637
|
+
} catch (error) {
|
|
638
|
+
reject(error);
|
|
639
|
+
}
|
|
640
|
+
resolve();
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
parser.on('error', reject);
|
|
644
|
+
|
|
645
|
+
createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
|
|
646
|
+
} else if (model.filenameExtension === 'geojson') {
|
|
609
647
|
readFile(filepath, 'utf8')
|
|
610
648
|
.then((data) => {
|
|
611
649
|
if (isValidJSON(data) === false) {
|
|
@@ -620,46 +658,11 @@ const importFiles = (task) =>
|
|
|
620
658
|
resolve();
|
|
621
659
|
})
|
|
622
660
|
.catch(reject);
|
|
661
|
+
} else {
|
|
662
|
+
reject(
|
|
663
|
+
new Error(`Unsupported file type: ${model.filenameExtension}`),
|
|
664
|
+
);
|
|
623
665
|
}
|
|
624
|
-
|
|
625
|
-
const parser = parse({
|
|
626
|
-
columns: true,
|
|
627
|
-
relax_quotes: true,
|
|
628
|
-
trim: true,
|
|
629
|
-
skip_empty_lines: true,
|
|
630
|
-
...task.csvOptions,
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
parser.on('readable', () => {
|
|
634
|
-
let record;
|
|
635
|
-
|
|
636
|
-
while ((record = parser.read())) {
|
|
637
|
-
try {
|
|
638
|
-
totalLineCount += 1;
|
|
639
|
-
lines.push(formatLine(record, model, totalLineCount));
|
|
640
|
-
// If we have a bunch of lines ready to insert, then do it
|
|
641
|
-
if (lines.length >= maxInsertVariables / model.schema.length) {
|
|
642
|
-
importLines(task, lines, model, totalLineCount);
|
|
643
|
-
}
|
|
644
|
-
} catch (error) {
|
|
645
|
-
reject(error);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
parser.on('end', () => {
|
|
651
|
-
try {
|
|
652
|
-
// Insert all remaining lines
|
|
653
|
-
importLines(task, lines, model, totalLineCount);
|
|
654
|
-
} catch (error) {
|
|
655
|
-
reject(error);
|
|
656
|
-
}
|
|
657
|
-
resolve();
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
parser.on('error', reject);
|
|
661
|
-
|
|
662
|
-
createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
|
|
663
666
|
}),
|
|
664
667
|
);
|
|
665
668
|
|
|
@@ -684,7 +687,7 @@ export async function importGtfs(initialConfig) {
|
|
|
684
687
|
createTables(db);
|
|
685
688
|
|
|
686
689
|
await mapSeries(config.agencies, async (agency) => {
|
|
687
|
-
const
|
|
690
|
+
const tempPath = temporaryDirectory();
|
|
688
691
|
|
|
689
692
|
const task = {
|
|
690
693
|
exclude: agency.exclude,
|
|
@@ -692,7 +695,7 @@ export async function importGtfs(initialConfig) {
|
|
|
692
695
|
headers: agency.headers || false,
|
|
693
696
|
realtime_headers: agency.realtimeHeaders || false,
|
|
694
697
|
realtime_urls: agency.realtimeUrls || false,
|
|
695
|
-
downloadDir:
|
|
698
|
+
downloadDir: tempPath,
|
|
696
699
|
downloadTimeout: config.downloadTimeout,
|
|
697
700
|
path: agency.path,
|
|
698
701
|
csvOptions: config.csvOptions || {},
|
|
@@ -716,7 +719,7 @@ export async function importGtfs(initialConfig) {
|
|
|
716
719
|
await updateRealtimeData(task);
|
|
717
720
|
}
|
|
718
721
|
|
|
719
|
-
|
|
722
|
+
await rm(tempPath, { recursive: true });
|
|
720
723
|
} catch (error) {
|
|
721
724
|
if (config.ignoreErrors) {
|
|
722
725
|
logError(error.message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gtfs",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.12.0",
|
|
4
4
|
"description": "Import GTFS transit data into SQLite and query routes, stops, times, fares and more",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"transit",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"sanitize-filename": "^1.6.3",
|
|
92
92
|
"sqlstring-sqlite": "^0.1.1",
|
|
93
93
|
"strip-bom-stream": "^5.0.0",
|
|
94
|
-
"
|
|
94
|
+
"tempy": "^3.1.0",
|
|
95
95
|
"untildify": "^5.0.0",
|
|
96
96
|
"yargs": "^17.7.2",
|
|
97
97
|
"yoctocolors": "^2.0.2"
|
|
@@ -17,7 +17,6 @@ const locationsConfig = {
|
|
|
17
17
|
},
|
|
18
18
|
],
|
|
19
19
|
verbose: false,
|
|
20
|
-
sqlitePath: '/tmp/locations',
|
|
21
20
|
};
|
|
22
21
|
|
|
23
22
|
describe('getLocations():', () => {
|
|
@@ -25,7 +24,6 @@ describe('getLocations():', () => {
|
|
|
25
24
|
openDb(locationsConfig);
|
|
26
25
|
|
|
27
26
|
// Add locations.geojson to test GTFS dataset
|
|
28
|
-
const temporaryDir = path.join(import.meta.dirname, '../fixture/tmp2/');
|
|
29
27
|
await prepDirectory(temporaryDir);
|
|
30
28
|
await unzip(config.agencies[0].path, temporaryDir);
|
|
31
29
|
|