From 925586b692bfc886373241ce165d76e374c51950 Mon Sep 17 00:00:00 2001 From: Andy McCoy Date: Tue, 23 Apr 2024 23:15:08 -0700 Subject: [PATCH] add filter to real time APIs --- src/config.js | 9 ++-- src/gtfs-rt/client.js | 1 + src/index.js | 1 + src/lib/logger.js | 1 + src/realtime-gtfs/router.js | 99 +++++++++++++++++++++++++++---------- 5 files changed, 82 insertions(+), 29 deletions(-) diff --git a/src/config.js b/src/config.js index 7e2b110..ae29160 100644 --- a/src/config.js +++ b/src/config.js @@ -2,6 +2,7 @@ import fs from 'fs'; import * as dotenv from 'dotenv'; import * as dotenvExpand from 'dotenv-expand'; import { isEmpty } from 'ramda'; +import { URL } from 'node:url'; export const PORT = Number(process.env.PORT); export const PROTOCOL = process.env.PROTOCOL; @@ -54,10 +55,10 @@ for (const dotenvFile of dotenvFiles) { } } -export const GTFS_REALTIME_TOKEN = process.env.GTFS_REALTIME_TOKEN; -export const GTFS_REALTIME_ALERTS_URL = process.env.GTFS_REALTIME_ALERTS_URL; -export const GTFS_REALTIME_VEHICLE_POSITIONS_URL = process.env.GTFS_REALTIME_VEHICLE_POSITIONS_URL; -export const GTFS_REALTIME_TRIP_UPDATES_URL = process.env.GTFS_REALTIME_TRIP_UPDATES_URL; +export const GTFS_REALTIME_TOKEN = isEmpty(process.env.GTFS_REALTIME_TOKEN) ? null : process.env.GTFS_REALTIME_TOKEN; +export const GTFS_REALTIME_ALERTS_URL = isEmpty(process.env.GTFS_REALTIME_ALERTS_URL) ? null : new URL(process.env.GTFS_REALTIME_ALERTS_URL); +export const GTFS_REALTIME_VEHICLE_POSITIONS_URL = isEmpty(process.env.GTFS_REALTIME_VEHICLE_POSITIONS_URL) ? null : new URL(process.env.GTFS_REALTIME_VEHICLE_POSITIONS_URL); +export const GTFS_REALTIME_TRIP_UPDATES_URL = isEmpty(process.env.GTFS_REALTIME_TRIP_UPDATES_URL) ? null : new URL(process.env.GTFS_REALTIME_TRIP_UPDATES_URL); // 511.org allows us 60 requests per hour, let's be conservative and // cache for 3min to make it a maximum of 20. diff --git a/src/gtfs-rt/client.js b/src/gtfs-rt/client.js index 5ed4ac6..e04c39e 100644 --- a/src/gtfs-rt/client.js +++ b/src/gtfs-rt/client.js @@ -51,6 +51,7 @@ async function _getAlertsNoCache() { const url = new URL(GTFS_REALTIME_ALERTS_URL); const usp = new URLSearchParams(url.search); usp.append('api_key', GTFS_REALTIME_TOKEN); + usp.delete('format', 'json'); url.search = usp; const response = await fetch(url); diff --git a/src/index.js b/src/index.js index 7d44d9b..b1d0abf 100644 --- a/src/index.js +++ b/src/index.js @@ -66,6 +66,7 @@ app.use((req, res, next) => { app.use('/v1/config', geoConfigRouter); app.use('/api/v1/config', geoConfigRouter); +app.use('/v1/realtime', realtimeRouter); app.use('/api/v1/realtime', realtimeRouter); // generic API path so we don't have a leaky abstraction diff --git a/src/lib/logger.js b/src/lib/logger.js index 242ea76..cb235a7 100644 --- a/src/lib/logger.js +++ b/src/lib/logger.js @@ -10,6 +10,7 @@ const logger = pino({ translateTime: true, }, } : null, + level: process.env.LOG_LEVEL || 'info' }); export default logger; diff --git a/src/realtime-gtfs/router.js b/src/realtime-gtfs/router.js index 7f3a179..2bf7552 100644 --- a/src/realtime-gtfs/router.js +++ b/src/realtime-gtfs/router.js @@ -1,4 +1,3 @@ -import { URL } from 'node:url'; import express from 'express'; import realtimeClient from './client.js'; import cache from '../lib/cache.js'; @@ -9,13 +8,57 @@ import { GTFS_REALTIME_TRIP_UPDATES_URL, } from '../config.js'; -const vehiclePositionsUrl = new URL(GTFS_REALTIME_VEHICLE_POSITIONS_URL); -const serviceAlertsUrl = new URL(GTFS_REALTIME_ALERTS_URL); -const tripUpdatesUrl = new URL(GTFS_REALTIME_TRIP_UPDATES_URL); +const vehiclePositionsUrl = GTFS_REALTIME_VEHICLE_POSITIONS_URL; +const serviceAlertsUrl = GTFS_REALTIME_ALERTS_URL; +const tripUpdatesUrl = GTFS_REALTIME_TRIP_UPDATES_URL; const router = express.Router(); +function filterVehiclePositions(tripId, routeId, entities) { + const vehicleFilters = [{ + key: 'TripId', + value: tripId + }, + { + key: 'RouteId', + value: routeId + }].filter(vehicleFilter => vehicleFilter.value) + + return vehicleFilters.reduce((entities, filter) => { + return entities + .filter(entity => entity.Vehicle.Trip) + .filter(entity => entity.Vehicle.Trip[filter.key] === filter.value); + }, entities); +} + +function filterTripUpdates(tripId, routeId, entities) { + const vehicleFilters = [{ + key: 'TripId', + value: tripId + }, + { + key: 'RouteId', + value: routeId + }].filter(vehicleFilter => vehicleFilter.value) + + return vehicleFilters.reduce((entities, filter) => { + return entities + .filter(entity => entity.TripUpdate.Trip) + .filter(entity => entity.TripUpdate.Trip[filter.key] === filter.value); + }, entities); +} + async function vehiclePositionsCb (req, res) { + const tripId = req.query.tripid; + const routeId = req.query.routeid; + + if (!vehiclePositionsUrl) { + logger.info('env var GTFS_REALTIME_VEHICLE_POSITIONS_URL not found'); + res.sendStatus(404); + res.end(); + return; + } + // try to get data from cache try { const cacheResult = await cache.get('vehiclePositions', {raw: true}); @@ -25,8 +68,8 @@ async function vehiclePositionsCb (req, res) { 'Cache-Control': 'public, max-age=60', 'Age': Math.floor((cacheResult.expires - Math.floor(new Date().getTime())) / 1000) }); - - res.json(cacheResult.value); + + res.json(filterVehiclePositions(tripId, routeId, cacheResult.value.Entity)); return; } } catch (error) { @@ -55,7 +98,7 @@ async function vehiclePositionsCb (req, res) { 'Cache-Control': 'public, max-age=60', 'Age': 0 }); - res.json(vehiclePositions); + res.json(filterVehiclePositions(tripId, routeId, vehiclePositions.Entity)); } catch (error) { if (error.response) { res.sendStatus(error.response.status); @@ -68,6 +111,13 @@ async function vehiclePositionsCb (req, res) { } async function serviceAlertsCb (req, res) { + if (!serviceAlertsUrl) { + logger.info('env var GTFS_REALTIME_ALERTS_URL not found'); + res.sendStatus(404); + res.end(); + return; + } + // try to get data from cache try { const cacheResult = await cache.get('serviceAlerts', {raw: true}); @@ -78,7 +128,7 @@ async function serviceAlertsCb (req, res) { 'Age': Math.floor((cacheResult.expires - Math.floor(new Date().getTime())) / 1000) }); - res.json(cacheResult.value); + res.json(cacheResult.value.Entity); return; } } catch (error) { @@ -107,7 +157,7 @@ async function serviceAlertsCb (req, res) { 'Cache-Control': 'public, max-age=60', 'Age': 0 }); - res.json(serviceAlerts); + res.json(serviceAlerts.Entity); } catch (error) { if (error.response) { res.sendStatus(error.response.status); @@ -120,6 +170,16 @@ async function serviceAlertsCb (req, res) { } async function tripUpdatesCb (req, res) { + const tripId = req.query.tripid; + const routeId = req.query.routeid; + + if (!tripUpdatesUrl) { + logger.info('env var GTFS_REALTIME_TRIP_UPDATES_URL not found'); + res.sendStatus(404); + res.end(); + return; + } + // try to get data from cache try { const cacheResult = await cache.get('tripUpdates', {raw: true}); @@ -130,7 +190,7 @@ async function tripUpdatesCb (req, res) { 'Age': Math.floor((cacheResult.expires - Math.floor(new Date().getTime())) / 1000) }); - res.json(cacheResult.value); + res.json(filterTripUpdates(tripId, routeId, cacheResult.value.Entity)); return; } } catch (error) { @@ -159,7 +219,7 @@ async function tripUpdatesCb (req, res) { 'Cache-Control': 'public, max-age=60', 'Age': 0 }); - res.json(tripUpdates); + res.json(filterTripUpdates(tripId, routeId, tripUpdates.Entity)); } catch (error) { if (error.response) { res.sendStatus(error.response.status); @@ -171,19 +231,8 @@ async function tripUpdatesCb (req, res) { res.end(); } -// only add vehicle position endpoint if the GTFS_REALTIME_VEHICLE_POSITIONS_URL env var is set -if (vehiclePositionsUrl) { - router.get('/vehiclepositions', vehiclePositionsCb); -} - -// only add service alerts endpoint if the GTFS_REALTIME_ALERTS_URL env var is set -if (serviceAlertsUrl) { - router.get('/servicealerts', serviceAlertsCb); -} - -// only add trip updates endpoint if the GTFS_REALTIME_TRIP_UPDATES_URL env var is set -if (tripUpdatesUrl) { - router.get('/tripupdates', tripUpdatesCb); -} +router.get('/vehiclepositions', vehiclePositionsCb); +router.get('/servicealerts', serviceAlertsCb); +router.get('/tripupdates', tripUpdatesCb); export default router;