diff --git a/.circleci/config.yml b/.circleci/config.yml index 8ef960c..3435735 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,7 +9,6 @@ jobs: keys: - v1-dependencies-{{ checksum "./functions/package.json" }} - v1-dependencies- - - run: name: Installing Dependencies working_directory: ~/project/functions diff --git a/DC-Metro.zip b/DC-Metro.zip index 9cddb81..31f1a8b 100644 Binary files a/DC-Metro.zip and b/DC-Metro.zip differ diff --git a/functions/src/index.ts b/functions/src/index.ts index 8e2f7b0..b8cf711 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -5,11 +5,18 @@ import { Table, SimpleResponse, Suggestions, + Permission, LinkOutSuggestion, + List, } from 'actions-on-google'; import {lineNamesEnum, serviceCodesEnum, convertCode} from './util/constants'; import {serviceIncidents} from './util/incidents'; -import {fetchTrainTimetable, fetchBusTimetable} from './wmata'; +import { + fetchTrainTimetable, + fetchBusTimetable, + fetchNearbyStops, +} from './wmata'; +import {createNearbyStopList} from './util/bus'; const app = dialogflow({debug: true}); @@ -20,9 +27,16 @@ app.intent( 'metro_timetable', async ( conv: any, - {transport, station}: {transport: string, station: string} + {transport, station}: {transport: string, station: string}, + option: string ) => { - const transportParam = transport.toLowerCase(); + let transportParam = transport.toLowerCase(); + let stationParam = station.toLowerCase(); + + if (conv.contexts.get('bus_nearby_selection')) { + transportParam = 'bus'; + stationParam = option; + } if ( transportParam === 'train' || @@ -30,7 +44,7 @@ app.intent( transportParam === 'metro' ) { // Handles train times. - const timetable: any = await fetchTrainTimetable(station); + const timetable: any = await fetchTrainTimetable(stationParam); if (!timetable) { conv.ask( @@ -194,7 +208,7 @@ app.intent( } } else if (transportParam === 'bus') { // Handles bus times. - const timetable: any = await fetchBusTimetable(station); + const timetable: any = await fetchBusTimetable(stationParam); if (!timetable) { conv.ask( @@ -427,9 +441,9 @@ app.intent( `To get the next train arrival at a Metro station you can say things such as 'Train times for Farragut North' or 'Rail times for Smithsonian'. What would you like me to do?` ); } else if (transportParam === 'bus') { - conv.ask(new Suggestions(['Train Commands'])); + conv.ask(new Suggestions(['Bus Stops Near Me', 'Train Commands'])); conv.ask( - `To find out when the next bus arrives you can say 'Bus times for 123', replacing the 123 with the stop id found on the Metro bus stop sign. What would you like me to do?` + `To find out when the next bus arrives you can say 'Bus times for 123', replacing the 123 with the stop id found on the Metro bus stop sign. You can also ask me to fetch bus stops near you. What would you like me to do?` ); } else { conv.ask(new Suggestions(['Train Commands', 'Bus Commands'])); @@ -491,4 +505,59 @@ app.intent('feedback_intent', (conv) => { ); }); +/** + * DialogFlow intent to ask for location permissions for nearby bus stops. + */ +app.intent('bus_stop_nearby_permission', (conv) => { + if (conv.surface.capabilities.has('actions.capability.SCREEN_OUTPUT')) { + conv.ask( + new Permission({ + context: 'To get nearby bus stops', + permissions: 'DEVICE_PRECISE_LOCATION', + }) + ); + } else { + conv.ask( + 'This action requires a device with a screen, is there anything else I can do for you?' + ); + } +}); + +/** + * DialogFlow intent for asking the user which bus stop to choose. + */ +app.intent('bus_stop_nearby', async (conv: any, input, granted) => { + if (granted) { + const stops = await fetchNearbyStops( + conv.device.location.coordinates.latitude, + conv.device.location.coordinates.longitude + ); + + if (stops.length) { + conv.ask( + `Here are the bus stops I found nearby, select the one which you'd like to hear about, or say 'Bus stop' followed by the number.` + ); + + conv.contexts.set('bus_nearby_selection', 1); + + const stopCells = await createNearbyStopList(stops); + + conv.ask( + new List({ + title: 'Nearby Bus Stops', + items: stopCells, + }) + ); + } else { + conv.ask( + `I couldn't find any bus stops near your current location. Is there anything else I can do for you?` + ); + } + } else { + conv.ask( + `Unfortunately I require access to your location to show you nearby bus stops. Is there anything else I can do for you?` + ); + } +}); + exports.dcMetro = functions.https.onRequest(app); diff --git a/functions/src/tests/bus.spec.ts b/functions/src/tests/bus.spec.ts index c0e044a..3fd2353 100644 --- a/functions/src/tests/bus.spec.ts +++ b/functions/src/tests/bus.spec.ts @@ -1,5 +1,5 @@ import * as test from 'tape'; -import {getRelevantBusIncidents} from '../util/bus'; +import {getRelevantBusIncidents, createNearbyStopList} from '../util/bus'; test('should get incidents that are relevant to the train lines in the station', (t: any) => { t.plan(3); @@ -93,3 +93,74 @@ test('should get incidents that are relevant to the train lines in the station', 'Should get incidents affecting JI and PQ route.' ); }); + +test('should generate an object with all of the correct keys for the nearby bus stop intent', (t) => { + const stops = [ + { + Lat: 38.878356, + Lon: -76.990378, + Name: 'K ST + POTOMAC AVE', + Routes: ['V7', 'V7c', 'V7cv1', 'V7v1', 'V7v2', 'V8', 'V9'], + StopID: '1000533', + }, + { + Lat: 38.879041, + Lon: -76.988528, + Name: 'POTOMAC AVE + 13TH ST', + Routes: ['V7', 'V7c', 'V7cv1', 'V7v1', 'V7v2', 'V8', 'V9'], + StopID: '1000544', + }, + { + Lat: 38.879347, + Lon: -76.991248, + Name: 'I ST + 11TH ST', + Routes: ['V7', 'V7c', 'V7cv1', 'V7cv2', 'V8', 'V9'], + StopID: '1000550', + }, + ]; + + t.deepEqual( + createNearbyStopList(stops), + { + 1000533: { + synonyms: 'Stop 1000533', + title: 'Stop 1000533: K ST + POTOMAC AVE', + description: 'Routes: V7, V7c, V7cv1, V7v1, V7v2, V8, V9', + image: { + url: + 'https://raw.githubusercontent.com/JamesIves/dc-metro-google-assistant-action/master/assets/app_icon.png', + accessibilityText: '1000533', + height: undefined, + width: undefined, + }, + }, + 1000544: { + synonyms: 'Stop 1000544', + title: 'Stop 1000544: POTOMAC AVE + 13TH ST', + description: 'Routes: V7, V7c, V7cv1, V7v1, V7v2, V8, V9', + image: { + url: + 'https://raw.githubusercontent.com/JamesIves/dc-metro-google-assistant-action/master/assets/app_icon.png', + accessibilityText: '1000544', + height: undefined, + width: undefined, + }, + }, + 1000550: { + synonyms: 'Stop 1000550', + title: 'Stop 1000550: I ST + 11TH ST', + description: 'Routes: V7, V7c, V7cv1, V7cv2, V8, V9', + image: { + url: + 'https://raw.githubusercontent.com/JamesIves/dc-metro-google-assistant-action/master/assets/app_icon.png', + accessibilityText: '1000550', + height: undefined, + width: undefined, + }, + }, + }, + 'Should generate a object used for the stop list.' + ); + + t.end(); +}); diff --git a/functions/src/util/bus.ts b/functions/src/util/bus.ts index c1b9165..4104ffb 100644 --- a/functions/src/util/bus.ts +++ b/functions/src/util/bus.ts @@ -1,3 +1,5 @@ +import {Image} from 'actions-on-google'; + /** * Filters bus incident data and returns a set of incidents which are relevant to the bus stop. * @param {array} routes - An array of routes which arrive at this stop. For example ['ABC', 'EFG'] @@ -22,3 +24,23 @@ export function getRelevantBusIncidents( [] ); } + +/** + * Creates an object which actions-on-google can consume to generate a list. + * @param {array} stops - An array of nearby bus stops. + * @returns {object} Returns an object containing the nearby stops. + */ +export function createNearbyStopList(stops: Array): any { + return stops.reduce((obj, item: any) => { + obj[item.StopID] = {}; + (obj[item.StopID].synonyms = `Stop ${item.StopID}`), + (obj[item.StopID].title = `Stop ${item.StopID}: ${item.Name}`); + obj[item.StopID].description = `Routes: ${item.Routes.join(', ')}`; + obj[item.StopID].image = new Image({ + url: + 'https://raw.githubusercontent.com/JamesIves/dc-metro-google-assistant-action/master/assets/app_icon.png', + alt: item.StopID, + }); + return obj; + }, {}); +} diff --git a/functions/src/wmata.ts b/functions/src/wmata.ts index a275326..ef00df3 100644 --- a/functions/src/wmata.ts +++ b/functions/src/wmata.ts @@ -13,6 +13,23 @@ import {serviceTypeEnum} from './util/constants'; export const rootUrl = 'https://api.wmata.com'; export const wmataApiKey = functions.config().metro.apikey; +export const fetchNearbyStops = async ( + lat: string, + lon: string +): Promise<[]> => { + try { + const stopResponse = await fetch( + `${rootUrl}/Bus.svc/json/jStops?Lat=${lat}&Lon=${lon}&Radius=250&api_key=${wmataApiKey}`, + {method: 'GET'} + ); + const stopObj = await stopResponse.json(); + + return stopObj.Stops; + } catch (error) { + return []; + } +}; + /** * Fetches all incidents which are currently affecting the Metro. * @param {string} transport - The mode of transport, either 'train' or 'bus'.