From 19da814262c0f1910f4dbc8bf1516a6a7ecb1416 Mon Sep 17 00:00:00 2001 From: James Ives Date: Tue, 12 Feb 2019 12:39:59 +0000 Subject: [PATCH] [Issue-23] Adds the ability to geo locate nearby bus stops. (#48) * Adds nearby permission intent * Fetch nearby stops WIP * Additional work on bus stops * Additional Updates * Adds nearby intent data * Adds tests * Adding DialogFlow * Fixing tests --- .circleci/config.yml | 1 - DC-Metro.zip | Bin 9155 -> 10728 bytes functions/src/index.ts | 83 +++++++++++++++++++++++++++++--- functions/src/tests/bus.spec.ts | 73 +++++++++++++++++++++++++++- functions/src/util/bus.ts | 22 +++++++++ functions/src/wmata.ts | 17 +++++++ 6 files changed, 187 insertions(+), 9 deletions(-) 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 9cddb810c5a81bbf8c922532a5b4650389ad57b2..31f1a8b4514e7f29e3dc67d8e71688c52a00db3f 100644 GIT binary patch delta 3473 zcmaJ@c|6o>7avQEeI5HYGuPTM3|X%w61P1x~xnMoq0xpKR_<00iyaEHge6dRCKws>I^q1&% zW$w7;FxII+cBK$k;aRGAcQATOb)v`akZx58`KKtz{Q_HG5Zpj(088Uu8sQGVa z5#0?}DZYqlCi9)_fw-1whRq+Q9l5ne;pTEth{gx2a!?EfLVxoN2G45fYTT`nrU;;) z#zR=f7?uaZHD5e@DJ#RAjY+mwnzM@5>uA!P&8(|U98*cT^Lgcauw;*QYtHMAk^cvOm*7B;fIzpfKo^g_T)fOs@uub?e)R|I2Q6mV1)p^kVLJ{B zjdjm{nyx8RnOiX3a?bv4k*&^6PD8Qf@$ij)|H3!F>zUe}AaWMN$m;4Z`i+eZWLT?G z6)#uj0+|JvN4ODvv4K~5;i(>1XZbw{t6>)54tUdV!KP?_9GheoX$fJ1Eg$5{Cw4aL z%MN#h*Pl7xn?62`%HBe}s#eJqR7cuITCu!7RzY&ls0^6ET~ezEoaad-WftMPX0P+E4VHhHPSG2r#olh8GARlRIQNH&^4J5119_Ge>npchpR3^ z0{v6`*LvT3DD{ugTLS#qs*VMyakE#Hc^hX4Dt3PcdF-qK|6HQSyornJka$~?b>-# zqkRf4$arrxqPb9)Jx@-+;sJZcFfKLg%W#F6w`1j#N+=<m7;NV>N@%zA40D;l+S#;8}ddZHzlX}_18E0(c7-o16La>|N8q}M3*gw zVDtLyY5BIL+n=;l3HBI<=|57x5cKZ;i zo$KU3ndHIlY5UlONjG^z_Ou-Q;d??7B!2HVpCA%v;hSZ^-Mo2@Xq5(~XIKAw&^`m^ zP@MV5!V=@o(d$|sb7g^&{^rJyiAIDFv&Q!VMP}EzmoKu)vv0Fpx+mAP`r>GQj9?gN zB%X2d*>Qn&FTyoFuHG;aX$Li~x$#paNVnM1P9~EQX_n3SrZ&#Fgquf9H0Jx9Oxj%B ziA$TYIN-8cHJ&NEn^h>>f0@&;LHoCxR>E^#xveI0!>Dj(2M3reqAb-*%rUm6f_IVO zowr`$YmZm{1g5B>`_ivF(p1^E)cPJq3b}IfaD`@^Wbl^!_{!)qBDFopm5%Ys=*X>w z{NYTq^wEm-Cy5EI9>?hK=b*(lC>C`?XxrDCg}AAvyBCiHylr?|5v%enMmIJ?KT|4k zwqwK+Wkbw}R}_s#2$g26_$wEDDlbMhXEctLyl$5}>!!zcVpD{pn?iO{tyG>;>6m9o zGt-}S_)vIUWQ`QWDUZw#&1}VaF?pqjXdcT(KVylvpzYjRE?oVhY=Tp+MvQe8Ra-gs4_!3r*R$M6ULUbv8?UPn7O+P?vEMjf zFzFdn(qu;NK* z`%}pmyDXsL(sV)r81vmN$m@K#Y8h%VmK{0Nn*nN}0a3zGoP&k$sL^gYaW}}EU+RmT zJIjBHTk0|rm3|_% ztAB+ww)o;0LWjx9r@AB>KcL6bT#U|DS=6nCt*QNp8!JR!%8K4_DXI~-Sk?M2I?}lv z8ToA`tKz~y|C`{6_jgOkGk&KlXNtv<3Bm8k+fWX8>ZgXy?{t{e{kiBv4^<1{jw%~K5)|(zI^?@r%bLIay|nEEcT0Uv znY{PCnJ+P!skm&nVo9fO70aV(YH9N=)9!1Ls_MO5qDu>F_4zR&Slkq7+!#Cz_G?Xv z^_R>0MIchaI?Q;yIKY@v_3p*`_pr!GRAqtrX!HHksxBqZ6`y~p&8$^m zg1hk?l{g*7r+XD!TbJKYc|dGST)@{zdq|8}5}r0}cCPJ6Bt^75i7v_mVYvlap`l!` z&NgyhT^KlHlPMNOqMWof)V-_Fo3tYN@`l8+#?haGrt~E92iFdl*R@Cfxhi}3*@Fq7g z;qeZHq1!PJO1SFB{p%vcE)Mk| zDe2=1JkRp3xLVr7W^>oQR?8m?d0@X_# zdYdGFG2T6{njoP;#@-R%Uec{GfmNvE_co zvCGnaE?>r2~Q-LT+aj9LdkzuJ3hoFu>8>%x>KaZhG4MzY@1{km)!A>(&nHzqj zK{XVowk5;?Lp5fqq4@5Jw7_#DMd>@iKsvJ&Feb@EQ+=VTer2VqQ~-S`cACmkN|YYT z0gO>w+Oz}@?S`b(ZQf4m4eekMNbbN5*Ey+%3cGtPc75JIvUmB>&-H-&XY_}Hr?I6& zI06)^7xKIS8&sa=fzR672{x(+0#pzW=4z1-sOOUZ*ZX9sD7`5JD1s``_JO=fUw{1v zn}eP=3II2FX`a)fM!-brErb9ue(`^Cb%JFzsZ#gyQE@$ZXRk2TPzWf7?PY5JTsRoD zttg-Z7skW)VB9Oxy&65}OuIPMPMzim8ZJtI9STH=$pEErFz0S(#4I5W35mn3i&7}dJ-H>y!Fg=2ms98Y=llD7p7;0r`##U>`}z*=Z17qW=&PuN z0T2iTa5~eHw}!3=h0^eB*h&;OD<4&m9k#&J7zM*)LZ z*Eq}Gf?@-_utZ~7jyj@iJFGOKH9NRY|+XhuTv}<=!^Br3y^f3Q+aA-82c35oQr*`8Jycj3w;EIX=5EnFD-1)E}WjL+hHg6yyI0CH`vYxA*j=j#|x&#!A zl;$=X=9cd^#NKT_U^HJ9?e5iEomLI2!80em3?RJq+*I;5A5lo2@wzA4-y^wq{Ye?; z8m#6$yXVb|dgms~B$XQzuI*2hORv~}cjiddr#&WOhXQO_EU|6(a!hwI>)nUh;g2>x zjH(PtXS&F3+NI{9=jHRpr`01-A=**b1X7O9hATR16c2E}P=5)ovv12n3~NN65QSN? zG&H1@rHx#&9i)$vV~%&6V&slxPuOK_=u=}QM*3&=DjOo*+9^4m;eK$_C3(Fpf zQX{Zurnidq2*a4-4+zq(YpN~0W{7E;ZPM7M2uD4m-gTp#7dm@PiY5EV)L zlSNnCnEUzUr;j&Zx4Ckxouul+vhq}Cg(n=J^+-`4xGWrb@6v&3nYYV$>F-*UJwCn@ z2zK}wNn#%rf6Tty>QoSugSOiE@#3N@%PPL<46!=k(x^&v!eCro?EAT?o{5q}HBGgg zu%2A=`dfWwT`g7Zt+lZSUhJXgt?i1Png6v=`uUgiu_a7bHg?~A;;194J0IWdSuxa5 zShsEAlg;_;6UTSH`Wvv3 zZYA26)hV9ja<1~Pct>eZ8UM63q{VZE{C0~Z_F$P;}s9jPggbw z#NF)icN$Jj2?WyHEEC=i#RLp@GV*X6%l>R5{Lqcsh<54F=gY4id0eil)ZO-E=odRL zr~Dw+kD6KQ*3nTLND=NM=C*;p_o=Q?b*x75clnx+&G$`Ok*z!EPCMlmwT8vG)CR5x6#;2KqCq)##B_<_L!ao+wSsB4F3z`83)oz+{5CuCvMLN z?a$d;U3pF^qt4!}@X`fID#hdt-{iGjK6jB`M|`t$xnQA8eB=}r8HS4Yq{A{ez;>UL z+h1j(ZoA&ug!s8XiIskiNxt_-wP#-8FbQ{as0yHgB!4U$=w$|-slG&$U2^;-AmY`2WU3#DwF_#+m^x7hdG^LM1e<}dmkG)w}_i5xwzqW>pO|L=~M5V zf|*S#ll`p03snwH_Ep^fCR=!6En2>Wi%pzX_B1$P;-CdZf1bZn3QYk3c0g%HpalsJ zej$-$#9A@|c2Efngj<1cDC#mR39=TI!M6x9SVNXuZIE>s4p~N^s;PmDqM8z5AxJRO z6c4R>Eu)oNxamviM(CCX1OQNgub+A~2y~P|9S`yl#~=u+2mpky8Q}FA(8i1^D;{P> zfaPjJQWqp*v{pMSh3rKX*luPnqZS|;hlV5`oW%V{(J2HNfC1wu@>*GtJb{IzF4#da zkoEBOZqWax2NfMKoND>tFrt7lq=%J_GaB&N8S3*gkzwK(*4)0<-`}Xz3g! Hu2lXPpJEo+ 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'.