baninaveen
8/5/2017 - 5:36 PM

CTA Bus Lookup With Permissions

CTA Bus Lookup With Permissions

{
  "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"
  }
}
/*
 * 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;
};