Skip to content

Commit

Permalink
Add GTFS object
Browse files Browse the repository at this point in the history
  • Loading branch information
LeoFrachet committed Dec 29, 2017
1 parent c61712a commit b58d97c
Show file tree
Hide file tree
Showing 21 changed files with 1,773 additions and 0 deletions.
283 changes: 283 additions & 0 deletions gtfs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
'use strict';

/* eslint-disable no-underscore-dangle */

const fs = require('fs-extra');

const forEachWithLog = require('./helpers/logging_iterator_wrapper');
const { exportGtfs } = require('./helpers/export');
const getters = require('./helpers/getters');
const { importTable } = require('./helpers/import');
const schema = require('./helpers/schema');

function addItems(items, tableName, gtfs) {
if (items instanceof Array === false) {
throw new Error(`items must be an array instead of: ${items}`);
}

const indexedTable = gtfs.getIndexedTable(tableName);
const indexKeys = schema.indexKeysByTableName[tableName];

if (indexKeys.indexKey) {
items.forEach(item => indexedTable.set(item[indexKeys.indexKey], item));
return;
}

if (indexKeys.firstIndexKey && indexKeys.secondIndexKey) {
items.forEach((item) => {
if (indexedTable.has(item[indexKeys.firstIndexKey]) === false) {
indexedTable.set(item[indexKeys.firstIndexKey], new Map());
}

indexedTable.get(item[indexKeys.firstIndexKey]).set(item[indexKeys.secondIndexKey], item);
});
}
}

function getIndexedTableOfGtfs(tableName, gtfs, options) {
if (gtfs._tables.has(tableName) === false) {
importTable(gtfs, tableName, options);
console.log(`[Importation] Table ${tableName} has been imported.`);
}

return gtfs._tables.get(tableName);
}

function forEachItem(iterator, tableName, gtfs) {
if (typeof iterator !== 'function') {
throw new Error(`iterator mulst be a function, instead of a ${typeof iterator}.`);
}

const indexedTable = gtfs.getIndexedTable(tableName);
const deepness = schema.deepnessByTableName[tableName];

if (deepness === 1) {
forEachWithLog(`Iterating:${tableName}`, indexedTable, (item) => {
iterator(item);
});
return;
}

if (deepness === 2) {
forEachWithLog(`Iterating:${tableName}`, indexedTable, (indexedSubTable) => {
indexedSubTable.forEach(iterator);
});
}
}

function removeItems(items, tableName, gtfs) {
if (items instanceof Array === false) {
throw new Error(`items must be an array instead of: ${items}`);
}

const indexedTable = gtfs.getIndexedTable(tableName);
const indexKeys = schema.indexKeysByTableName[tableName];

if (indexKeys.indexKey) {
items.forEach(item => indexedTable.delete(item[indexKeys.indexKey]));
return;
}

if (indexKeys.firstIndexKey && indexKeys.secondIndexKey) {
items.forEach((item) => {
if (indexedTable.has(item[indexKeys.firstIndexKey]) === true) {
indexedTable.get(item[indexKeys.firstIndexKey]).delete(item[indexKeys.secondIndexKey]);
}

if (indexedTable.get(item[indexKeys.firstIndexKey]).size === 0) {
indexedTable.delete(item[indexKeys.firstIndexKey]);
}
});
}
}

function setIndexedItems(indexedItems, tableName, gtfs) {
if (indexedItems instanceof Map === false && schema.deepnessByTableName[tableName] !== 0) {
throw new Error(`indexedItems must be a Map instead of: ${indexedItems}`);
}

gtfs._tables.set(tableName, indexedItems);
}

class Gtfs {
constructor(path, regexPatternObjectsByTableName) {
if (typeof path !== 'string' || path.length === 0) {
throw new Error(`Gtfs need a valid input path as string, instead of: "${path}".`);
}

path = (path[path.length - 1] === '/') ? path : `${path}/`;

if (fs.existsSync(path) === false) {
throw new Error(`inputPath: "${path}" is not a valid folder.`);
}

this.isGtfs = true;

this._path = path;
this._regexPatternObjectsByTableName = regexPatternObjectsByTableName || {};
this._tables = new Map();
}

/* io */
exportAtPath(path, callback) { exportGtfs(this, path, callback); }
getPath() { return this._path; }

/* Generic table & item manipulation */
addItemInTable(item, tableName) { addItems([item], tableName, this); }
addItemsInTable(items, tableName) { addItems(items, tableName, this); }
forEachItemInTable(tableName, iterator) { forEachItem(iterator, tableName, this); }
forEachTableName(iterator) { this.getTableNames().forEach(iterator); }
getIndexedTable(tableName, forcedValuesByKeys) { return getIndexedTableOfGtfs(tableName, this, forcedValuesByKeys); }
getItemWithIndexInTable(index, tableName) { return getters.getItemWithIndex(index, tableName, this); }
getTableNames() { return new Set([...schema.tableNames, ...this._tables.keys()]); }
getParentItem(item, tableName) { return getters.getParentItem(item, tableName, this); }
removeItemInTable(item, tableName) { removeItems([item], tableName, this); }
removeItemsInTable(items, tableName) { removeItems(items, tableName, this); }
setIndexedItemsAsTable(indexedItems, tableName) { setIndexedItems(indexedItems, tableName, this); }

/* agency.txt */
addAgency(agency) { addItems([agency], 'agency', this); }
addAgencies(agencies) { addItems(agencies, 'agency', this); }
forEachAgency(iterator) { forEachItem(iterator, 'agency', this); }
getAgencyOfRoute(route) { return getters.getParentItem(route, 'agency', this); }
getAgencyWithId(agencyId) { return getters.getItemWithIndex(agencyId, 'agency', this); }
getIndexedAgencies() { return getIndexedTableOfGtfs('agency', this); }
removeAgency(agency) { removeItems([agency], 'agency', this); }
removeAgencies(agencies) { removeItems(agencies, 'agency', this); }
setIndexedAgencies(indexedAgencies) { setIndexedItems(indexedAgencies, 'agency', this); }

/* stops.txt */
addStop(stop) { addItems([stop], 'stops', this); }
addStops(stops) { addItems(stops, 'stops', this); }
forEachStop(iterator) { forEachItem(iterator, 'stops', this); }
getIndexedStops() { return getIndexedTableOfGtfs('stops', this); }
getStopOfStopTime(stopTime) { return getters.getParentItem(stopTime, 'stops', this); }
getStopWithId(stopId) { return getters.getItemWithIndex(stopId, 'stops', this); }
removeStop(stop) { removeItems([stop], 'stops', this); }
removeStops(stops) { removeItems(stops, 'stops', this); }
setIndexedStops(indexedStops) { setIndexedItems(indexedStops, 'stops', this); }

/* routes.txt */
addRoute(route) { addItems([route], 'routes', this); }
addRoutes(routes) { addItems(routes, 'routes', this); }
forEachRoute(iterator) { forEachItem(iterator, 'routes', this); }
getIndexedRoutes() { return getIndexedTableOfGtfs('routes', this); }
getRouteOfStopTime(stopTime) { return getters.getGrandParentItem(stopTime, 'trips', 'routes', this); }
getRouteOfTrip(trip) { return getters.getParentItem(trip, 'routes', this); }
getRouteWithId(routeId) { return getters.getItemWithIndex(routeId, 'routes', this); }
removeRoute(route) { removeItems([route], 'routes', this); }
removeRoutes(routes) { removeItems(routes, 'routes', this); }
setIndexedRoutes(indexedRoutes) { setIndexedItems(indexedRoutes, 'routes', this); }

/* trips.txt */
addTrip(trip) { addItems([trip], 'trips', this); }
addTrips(trips) { addItems(trips, 'trips', this); }
forEachTrip(iterator) { forEachItem(iterator, 'trips', this); }
getIndexedTrips() { return getIndexedTableOfGtfs('trips', this); }
getTripOfStopTime(stopTime) { return getters.getParentItem(stopTime, 'trips', this); }
getTripWithId(tripId) { return getters.getItemWithIndex(tripId, 'trips', this); }
removeTrip(trip) { removeItems([trip], 'trips', this); }
removeTrips(trips) { removeItems(trips, 'trips', this); }
setIndexedTrips(indexedTrips) { setIndexedItems(indexedTrips, 'trips', this); }

/* stop_times.txt */
addStopTime(stopTime) { addItems([stopTime], 'stop_times', this); }
addStopTimes(stopTimes) { addItems(stopTimes, 'stop_times', this); }
forEachStopTime(iterator) { forEachItem(iterator, 'stop_times', this); }
forEachStopTimeOfTrip(trip, iterator) {
const stopTimeByStopSequence = this.getStopTimeByStopSequenceOfTrip(trip);
if (stopTimeByStopSequence instanceof Map) {
stopTimeByStopSequence.forEach(iterator);
}
}
getIndexedStopTimes() { return getIndexedTableOfGtfs('stop_times', this); }
getStopTimeByStopSequenceOfTrip(trip) { return getters.getIndexedItemsWithParent(trip, 'stop_times', this); }
getStopTimeWithTripIdAndStopSequence(tripId, stopSequence) {
return getters.getItemWithIndexes(tripId, stopSequence, 'stop_times', this);
}
removeStopTime(stopTime) { removeItems([stopTime], 'stop_times', this); }
removeStopTimes(stopTimes) { removeItems(stopTimes, 'stop_times', this); }
setIndexedStopTimes(indexedStopTimes) { setIndexedItems(indexedStopTimes, 'stop_times', this); }

/* calendar.txt */
addCalendar(calendar) { addItems([calendar], 'calendar', this); }
addCalendars(calendars) { addItems(calendars, 'calendar', this); }
forEachCalendar(iterator) { forEachItem(iterator, 'calendar', this); }
getCalendarOfTrip(trip) { return getters.getParentItem(trip, 'calendar', this); }
getCalendarOfStopTime(stopTime) {
return getters.getGrandParentItem(stopTime, 'trips', 'calendar', this);
}
getCalendarWithServiceId(serviceId) { return getters.getItemWithIndex(serviceId, 'calendar', this); }
getIndexedCalendars() { return getIndexedTableOfGtfs('calendar', this); }
removeCalendar(calendar) { removeItems([calendar], 'calendar', this); }
removeCalendars(calendars) { removeItems(calendars, 'calendar', this); }
setIndexedCalendars(indexedCalendars) { setIndexedItems(indexedCalendars, 'calendar', this); }

/* calendar_dates.txt */
addCalendarDate(calendarDate) { addItems([calendarDate], 'calendar_dates', this); }
addCalendarDates(calendarDates) { addItems(calendarDates, 'calendar_dates', this); }
forEachCalendarDate(iterator) { forEachItem(iterator, 'calendar_dates', this); }
getCalendarDateByDateOfServiceId(serviceId) {
return getters.getIndexedItemsWithParentIndex(serviceId, 'calendar_dates', this);
}
getCalendarDateByDateOfTrip(trip) { return getters.getIndexedItemsWithParent(trip, 'calendar_dates', this); }
getCalendarDateWithServiceIdAndDate(serviceId, date) {
return getters.getItemWithIndexes(serviceId, date, 'calendar_dates', this);
}
getIndexedCalendarDates() { return getIndexedTableOfGtfs('calendar_dates', this); }
removeCalendarDate(calendarDate) { removeItems([calendarDate], 'calendar_dates', this); }
removeCalendarDates(calendarDates) { removeItems(calendarDates, 'calendar_dates', this); }
setIndexedCalendarDates(indexedCalendarDates) { setIndexedItems(indexedCalendarDates, 'calendar_dates', this); }

/* fare_attributes.txt */
// Not used, therefore not implemented

/* fare_rules.txt */
// Not used, therefore not implemented

/* shapes.txt */
addShapePoint(shapePoint) { addItems([shapePoint], 'shapes', this); }
addShapePoints(shapePoints) { addItems(shapePoints, 'shapes', this); }
forEachShapePoint(iterator) { forEachItem(iterator, 'shapes', this); }
getIndexedShapePoints() { return getIndexedTableOfGtfs('shapes', this); }
getShapePointByShapePointSequenceOfShapeId(shapeId) {
return getters.getIndexedItemsWithParentIndex(shapeId, 'shapes', this);
}
getShapePointByShapePointSequenceOfTrip(trip) { return getters.getIndexedItemsWithParent(trip, 'shapes', this); }
getShapePointWithTripIdAndShapePointSequence(tripId, shapePointSequence) {
return getters.getItemWithIndexes(tripId, shapePointSequence, 'shapes', this);
}
removeShapePoint(shapePoint) { removeItems([shapePoint], 'shapes', this); }
removeShapePoints(shapePoints) { removeItems(shapePoints, 'shapes', this); }
setIndexedShapePoints(indexedShapes) { setIndexedItems(indexedShapes, 'shapes', this); }

/* frequencies.txt */
addFrequency(frequency) { addItems([frequency], 'frequencies', this); }
addFrequencies(frequencies) { addItems(frequencies, 'frequencies', this); }
forEachFrequency(iterator) { forEachItem(iterator, 'frequencies', this); }
getIndexedFrequencies() { return getIndexedTableOfGtfs('frequencies', this); }
getFrequencyWithTripIdAndStartTime(tripId, startTime) {
return getters.getItemWithIndexes(tripId, startTime, 'frequencies', this);
}
removeFrequency(frequency) { removeItems([frequency], 'frequencies', this); }
removeFrequencies(frequencies) { removeItems(frequencies, 'frequencies', this); }
setIndexedFrequencies(indexedFrequencies) { setIndexedItems(indexedFrequencies, 'frequencies', this); }

/* transfers.txt */
addTransfer(transfer) { addItems([transfer], 'transfers', this); }
addTransfers(transfers) { addItems(transfers, 'transfers', this); }
forEachTransfer(iterator) { forEachItem(iterator, 'transfers', this); }
getIndexedTransfers() { return getIndexedTableOfGtfs('transfers', this); }
getTransfertWithFromStopIdAndToStopId(fromStopId, toStopId) {
return getters.getItemWithIndexes(fromStopId, toStopId, 'transfers', this);
}
removeTransfer(transfer) { removeItems([transfer], 'transfers', this); }
removeTransfers(transfers) { removeItems(transfers, 'transfers', this); }
setIndexedTransfers(indexedTransfers) { setIndexedItems(indexedTransfers, 'transfers', this); }

/* feed_info.txt */
getFeedInfo() { return getIndexedTableOfGtfs('feed_info', this); }
setFeedInfo(feedInfo) { setIndexedItems(feedInfo, 'feed_info', this); }
}

module.exports = Gtfs;
100 changes: 100 additions & 0 deletions helpers/csv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
'use strict';

/**
* Private functions
*/

const SPECIAL_CHARACTERS_REGEX = /[",\\]/;

function formatRegularValue(value) {
if (value === undefined || value === null) {
return '';
}

value = (typeof value === 'string') ? value : String(value);

if (value.match(SPECIAL_CHARACTERS_REGEX)) {
return `"${value.replace(/"/g, '""')}"`;
}

return value;
}

/**
* Public functions
*/

function fromObjectToCsvString(object, sortedKeys) {
return `${sortedKeys.map(key => formatRegularValue(object[key], key)).join(',')}\n`;
}

/*
Source: http://stackoverflow.com/questions/8493195/how-can-i-parse-a-csv-string-with-javascript
Explaination:
(?!\s*$) # Don't match empty last value.
\s* # Strip whitespace before value.
(?: # Group for value alternatives.
'([^'\\]*(?:\\[\S\s][^'\\]*)*)' # Either $1: Single quoted string,
| "([^"\\]*(?:\\[\S\s][^"\\]*)*)" # or $2: Double quoted string,
| ([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*) # or $3: Non-comma, non-quote stuff.
) # End group of value alternatives.
\s* # Strip whitespace after value.
(?:,|$) # Field ends on comma or EOS.
For practical reason, we remove the case with single quoted string.
*/

// eslint-disable-next-line max-len
// var re_valid_original = /^\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,'"\s\\]*(?:\s+[^,'"\s\\]+)*)\s*(?:,\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,'"\s\\]*(?:\s+[^,'"\s\\]+)*)\s*)*$/;

// eslint-disable-next-line max-len
// var re_value_original = /(?!\s*$)\s*(?:'([^'\\]*(?:\\[\S\s][^'\\]*)*)'|"([^"\\]*(?:\\[\S\s][^"\\]*)*)"|([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*))\s*(?:,|$)/g;

// eslint-disable-next-line max-len
const reValid = /^\s*(?:"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,"\s\\]*(?:\s+[^,"\s\\]+)*)\s*(?:,\s*(?:"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,"\s\\]*(?:\s+[^,"\s\\]+)*)\s*)*$/;

// eslint-disable-next-line max-len
const reValue = /(?!\s*$)\s*(?:"([^"\\]*(?:\\[\S\s][^"\\]*)*)"|([^,"\s\\]*(?:\s+[^,"\s\\]+)*))\s*(?:,|$)/g;

function fromCsvStringToArray(string, tableName) {
string = string.trim();

if (string.length === 0) {
return null;
}

if (string.includes('"') === false) {
return string.split(',');
}

if (!reValid.test(string)) {
if (string.match(/""/)) {
string = string.replace(/""/g, '\\"');
return fromCsvStringToArray(string, tableName);
}
process.notices.addWarning(`Row not valid in table ${tableName}: ${string}`);
return null;
}

const a = []; // Initialize array to receive values.
// "Walk" the string using replace with callback.
string.replace(reValue, (m0, /* m1, */ m2, m3) => {
// Remove backslash from \' in single quoted values.
/* if (m1 !== undefined) a.push(m1.replace(/\\'/g, "'")); */
// Remove backslash from \" in double quoted values.
/* else */
if (m2 !== undefined) a.push(m2.replace(/\\"/g, '"'));
else if (m3 !== undefined) a.push(m3);
return ''; // Return empty string.
});
// Handle special case of empty last value.
if (/,\s*$/.test(string)) {
a.push('');
}
return a;
}

module.exports = {
fromCsvStringToArray,
fromObjectToCsvString,
};
Loading

0 comments on commit b58d97c

Please sign in to comment.