Skip to content

Instantly share code, notes, and snippets.

@joshafeinberg
Created April 30, 2017 04:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save joshafeinberg/21005880b86914f08de8989480db1ede to your computer and use it in GitHub Desktop.
Save joshafeinberg/21005880b86914f08de8989480db1ede to your computer and use it in GitHub Desktop.
CTA Bus Lookup With Permissions
/*
* Copyright (C) 2017 Josh Feinberg
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
process.env.DEBUG = 'actions-on-google:*';
const Assistant = require('actions-on-google').ApiAiAssistant;
const http = require("http");
const API_KEY = "XXXXXXXXXXXXXX";
const BUS_NUMBER_PARAM = 'busnumber';
const BUS_DIRECTION_PARAM = 'direction';
exports.findBus = (req, res) => {
const assistant = new Assistant({ request: req, response: res });
/**
* asks for a permission to use the users location
* @param assistant the assistant object passed in
*/
function permissionChecker(assistant) {
const permission = assistant.SupportedPermissions.DEVICE_PRECISE_LOCATION;
assistant.askForPermission('To find the closest bus stop', permission);
}
/**
* called when the device gets a callback if they have permission or not
* @param assistant the assistant object passed in
*/
function gotPermission(assistant) {
if (assistant.isPermissionGranted()) {
findClosestBusStop(assistant)
} else {
assistant.tell("I cannot find when the next bus is coming without your location.");
}
}
/**
* finds the closest bus stop to the user on that route
* @param assistant the assistant object passed in
*/
function findClosestBusStop(assistant) {
var busNumber = assistant.getContext("request_permission").parameters[BUS_NUMBER_PARAM];
var busDirection = assistant.getContext("request_permission").parameters[BUS_DIRECTION_PARAM];
http.get('http://www.ctabustracker.com/bustime/api/v2/getstops?key=' + API_KEY + '&rt=' + busNumber + '&dir=' + busDirection + '&format=json', (res) => {
res.setEncoding('utf8');
var rawData = '';
res.on('data', (chunk) => { rawData += chunk; });
res.on('end', () => {
try {
var closestBusStop = parseClosestBusStop(assistant.getDeviceLocation().coordinates, rawData);
findClosestBus(assistant, closestBusStop, busNumber, busDirection);
} catch (e) {
assistant.tell("Sorry, I was unable to load bus information. Please try again.")
console.error("error: " + e.message);
}
});
});
}
/**
* parses the json to return the closest stop
* @param {*} deviceCoordinates the device coordinates
* @param {*} rawData the response from api
* @return an object containing the closest stop
*/
function parseClosestBusStop(deviceCoordinates, rawData) {
var closestStop;
const parsedData = JSON.parse(rawData);
var response = parsedData['bustime-response'];
if (response.error != null) {
noStopFound();
}
var stops = response.stops; // array of stops
var stopsCount = stops.length;
if (stopsCount == 0) {
noStopFound();
} else {
closestStop = findClosestStop(deviceCoordinates, stops, stopsCount);
}
return closestStop;
}
/**
* loops through the stops and finds the closest stop
* @param {*} deviceCoordinates
* @param {*} stops
* @param {*} stopsCount
* @return an object containing the closest stop
*/
function findClosestStop(deviceCoordinates, stops, stopsCount) {
const deviceLatitude = deviceCoordinates.latitude;
const deviceLongitude = deviceCoordinates.longitude;
var shortestDistance = -1;
var closestStop;
for (var i = 0; i < stopsCount; i++) {
var stop = stops[i];
var distance = calculateDistance(deviceLatitude, deviceLongitude, stop);
if (shortestDistance == -1 || shortestDistance > distance) {
shortestDistance = distance;
closestStop = stop;
}
}
return closestStop;
}
/**
* finds the closest bus stop using the folowing haversine formula
* a = sin²(Δφ/2) + cos φ1 ⋅ cos φ2 ⋅ sin²(Δλ/2)
* c = 2 ⋅ atan2( √a, √(1−a) )
* d = R ⋅ c
* @param {*} deviceLatitude
* @param {*} deviceLongitude
* @param {*} stop
* @return the distance in meters between the device and the stop
*/
function calculateDistance(deviceLatitude, deviceLongitude, stop) {
var latitude = stop.lat;
var longitude = stop.lon;
var R = 6371; // metres
var φ1 = Math.radians(deviceLatitude);
var φ2 = Math.radians(latitude);
var Δφ = Math.radians(latitude - deviceLatitude);
var Δλ = Math.radians(longitude - deviceLongitude);
var a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
/**
* finds the closest bus
* @param {*} assistant
* @param {*} stop
* @param {*} busNumber
* @param {*} busDirection
*/
function findClosestBus(assistant, stop, busNumber, busDirection) {
http.get('http://www.ctabustracker.com/bustime/api/v2/getpredictions?key=' + API_KEY + '&stpid=' + stop.stpid + '&rt=' + busNumber + '&format=json', (res) => {
res.setEncoding('utf8');
var rawData = '';
res.on('data', (chunk) => { rawData += chunk; });
res.on('end', () => {
try {
parseData(rawData, stop.stpnm, busNumber, busDirection.toLowerCase());
} catch (e) {
assistant.tell("Sorry, I was unable to load bus information. Please try again.")
console.error("error: " + e.message);
}
});
})
}
/**
* parses the respond from the API and determines a response
* @param rawData the raw data from the HTTP response
* @param busNumber the bus number the user is looking for
* @param busDirection the bus direction the user is looking for
*/
function parseData(rawData, stopName, busNumber, busDirection) {
const parsedData = JSON.parse(rawData);
var response = parsedData['bustime-response'];
if (response.error != null) {
noBusses(stopName, busNumber);
}
var prd = response.prd; // array of preditions
var predictionsCount = prd.length;
if (predictionsCount == 0) {
noBusses(stopName, busNumber);
} else {
var busFound = findBus(stopName, busNumber, busDirection, prd, predictionsCount);
if (!busFound) {
noBusses(stopName, busNumber);
}
}
}
/**
* finds the proper bus and informs the user of the arrival time (in minutes)
* @param busNumber the bus number the user is looking for
* @param busDirection the bus direction the user is looking for
* @param prd the JSON array of predictions
* @param predictionsCount the amount of predictions that were returned
* @return if a bus was found
*/
function findBus(stopName, busNumber, busDirection, prd, predictionsCount) {
var busFound = false;
for (var i = 0; i < predictionsCount; i++) {
var routeDirection = prd[i].rtdir.toLowerCase();
if (routeDirection.localeCompare(busDirection) == 0) {
var prediction = prd[i].prdctdn;
if (prediction == "DUE") {
assistant.tell("Quick, the " + busNumber + " is at " + stopName + "!")
} else {
assistant.ask("<speak>The next <say-as interpret-as=\"cardinal\">" + busNumber + "</say-as> will arrive in " + prd[i].prdctdn + " minutes at " + stopName + ". " +
" Would you like to find another?</speak>")
}
busFound = true;
break;
}
}
return busFound;
}
/**
* alerts that no busses were found and asks if the user would like a different route
* @param busNumber the bus number the user is looking for
*/
function noBusses(stopName, busNumber) {
assistant.ask("<speak>It does not appear there are any <say-as interpret-as=\"cardinal\">" + busNumber + "</say-as> buses on the way to " + stopName + ", would you like to try another route?</speak>")
}
/**
* alerts that no stops were found and asks if the user would like a different route
*/
function noStopFound() {
assistant.ask("<speak>I was unable to find any stops near you, would you like to try another route?</speak>")
}
const actionMap = new Map();
actionMap.set('bus-requested', permissionChecker);
actionMap.set('find-bus', gotPermission);
assistant.handleRequest(actionMap);
};
Math.radians = function(degrees) {
return degrees * Math.PI / 180;
};
{
"name": "get-cta-bus",
"version": "1.0.0",
"description": "API.AI Bus Lookup",
"author": "Josh Feinberg <josh@joshafeinberg.com>",
"license": "Apache-2.0",
"dependencies": {
"actions-on-google": "^1.0.9"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment