org.onebusaway.nyc.sms.actions.IndexAction.java Source code

Java tutorial

Introduction

Here is the source code for org.onebusaway.nyc.sms.actions.IndexAction.java

Source

/**
 * Copyright (c) 2011 Metropolitan Transportation Authority
 *
 * 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.
 */
package org.onebusaway.nyc.sms.actions;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.collections.Transformer;
import org.apache.commons.lang.StringUtils;
import org.onebusaway.nyc.presentation.model.SearchResult;
import org.onebusaway.nyc.presentation.model.SearchResultCollection;
import org.onebusaway.nyc.presentation.service.realtime.RealtimeService;
import org.onebusaway.nyc.presentation.service.search.SearchResultFactory;
import org.onebusaway.nyc.presentation.service.search.SearchService;
import org.onebusaway.nyc.sms.actions.model.GeocodeResult;
import org.onebusaway.nyc.sms.actions.model.RouteAtStop;
import org.onebusaway.nyc.sms.actions.model.RouteDirection;
import org.onebusaway.nyc.sms.actions.model.RouteResult;
import org.onebusaway.nyc.sms.actions.model.ServiceAlertResult;
import org.onebusaway.nyc.sms.actions.model.StopResult;
import org.onebusaway.nyc.transit_data.services.NycTransitDataService;
import org.onebusaway.nyc.util.configuration.ConfigurationService;
import org.onebusaway.transit_data.model.RouteBean;
import org.onebusaway.transit_data.model.service_alerts.NaturalLanguageStringBean;
import org.onebusaway.transit_data.model.service_alerts.ServiceAlertBean;
import org.springframework.beans.factory.annotation.Autowired;

import com.dmurph.tracking.AnalyticsConfigData;
import com.dmurph.tracking.JGoogleAnalyticsTracker;
import com.dmurph.tracking.JGoogleAnalyticsTracker.GoogleAnalyticsVersion;

public class IndexAction extends SessionedIndexAction {

    private static final long serialVersionUID = 1L;

    private static final int MAX_SMS_CHARACTER_COUNT = 160;

    @Autowired
    private RealtimeService _realtimeService;

    @Autowired
    private ConfigurationService _configurationService;

    @Autowired
    private SearchService _searchService;

    @Autowired
    private NycTransitDataService _nycTransitDataService;

    private JGoogleAnalyticsTracker _googleAnalytics = null;

    private String _response = null;

    @Override
    public void initializeSession(String sessionId) {

        super.initializeSession(sessionId);

        if (_googleAnalytics == null)
            initAnalytics();

        // send an initial visit to google analytics to tie our events to
        if (_googleAnalytics != null) {
            try {
                _googleAnalytics.trackPageView("/sms", "New SMS Session", "");
            } catch (Exception e) {
                //discard
            }
        }
    };

    public String execute() throws Exception {

        if (_googleAnalytics == null)
            initAnalytics();

        SearchResultFactory _resultFactory = new SearchResultFactoryImpl(_nycTransitDataService, _realtimeService,
                _configurationService);

        String commandString = getCommand(_query);
        String queryString = getQuery(_query);

        if (queryString != null && !queryString.isEmpty()) {
            _lastQuery = queryString;
            _searchResults = _searchService.getSearchResults(queryString, _resultFactory);
        } else if (commandString != null && commandString.equals("R") && _lastQuery != null
                && !_lastQuery.isEmpty()) {
            _searchResults = _searchService.getSearchResults(_lastQuery, _resultFactory);
        } else if (_searchResults != null && "StopResult".equals(_searchResults.getResultType())
                && commandString != null && getRoutesInSearchResults().contains(commandString)) {
            // We are all set, let the existing stop results be processed given the route command string
        } else if (_searchResults != null && "StopResult".equals(_searchResults.getResultType())
                && commandString != null && StringUtils.isNumeric(commandString)) {
            // We are all set, let the existing stop results be processed given the number (which is the users choice of stop) command string
        } else if ((queryString == null || queryString.isEmpty()) && commandString != null && _searchResults != null
                && _searchResults.getSuggestions().size() > 0) {
            // We have suggestions with a command string for picking one of them or paginating
        } else if (queryString == null || queryString.isEmpty()) {
            _response = errorResponse("No results.");
            return SUCCESS;
        }

        while (true) {
            if (_searchResults.getMatches().size() > 0) {
                // route identifier search
                if (_searchResults.getMatches().size() == 1
                        && "RouteResult".equals(_searchResults.getResultType())) {
                    RouteResult route = (RouteResult) _searchResults.getMatches().get(0);

                    // If we get a route back, but there is no direction information in it,
                    // this is a route in the bundle with no trips/stops/etc.
                    // Show no results.
                    if (route.getDirections() != null && route.getDirections().size() == 0) {
                        _response = errorResponse("No results.");
                        break;

                    } else if (commandString != null && commandString.equals("C")) {
                        // find a unique set of service alerts for the route found
                        Set<String> alerts = new HashSet<String>();
                        for (RouteDirection direction : route.getDirections()) {
                            for (NaturalLanguageStringBean alert : direction.getSerivceAlerts()) {
                                alerts.add(alert.getValue());
                            }
                        }

                        // make the alerts into results
                        SearchResultCollection newResults = new SearchResultCollection();
                        for (String alert : alerts) {
                            newResults.addMatch(new ServiceAlertResult(alert));
                        }

                        if (newResults.getMatches().size() == 0) {
                            _response = errorResponse("No " + route.getShortName() + " alerts.");
                            break;
                        }

                        _searchResults = newResults;
                        continue;

                        // route schedule status
                    } else {
                        _response = routeResponse(route);
                        break;
                    }

                    // paginated service alerts
                } else if ("ServiceAlertResult".equals(_searchResults.getResultType())) {
                    if (commandString != null && commandString.equals("N")) {
                        _response = serviceAlertResponse(_searchResultsCursor);
                    } else {
                        _response = serviceAlertResponse(0);
                    }
                    break;

                    // one or more paginated stops
                } else if ("StopResult".equals(_searchResults.getResultType())) {
                    if (commandString != null && commandString.equals("N")) {

                        _response = directionDisambiguationResponse();

                    } else if (_searchResults.getMatches().size() > 1 && commandString != null
                            && getRoutesInSearchResults().contains(commandString)) {
                        // We presented a list of routes for nearby stops to the user and they chose a route.
                        // Filter the search results to those that contain the chosen route and have the user
                        // pick a direction.

                        // Filter the result set to only the ones containing the chosen route in each direction
                        // might have to do a quick fake search to get a filter object
                        SearchResultCollection justToGetAFilter = _searchService
                                .getSearchResults("00000 " + commandString, _resultFactory);
                        _searchResults.getRouteFilter().clear();
                        _searchResults.addRouteFilters(justToGetAFilter.getRouteFilter());

                        // Filter the stop results down to the provided route for only up to one stop in each direction
                        filterStopSearchResultsToRouteFilterAndDirection();

                        // If there is only one stop after filtering, choose it for the user and don't display the direction disambiguation screen
                        if (_searchResults.getMatches().size() == 1) {
                            commandString = "1";
                            continue;
                        }

                        _response = directionDisambiguationResponse();

                    } else if (StringUtils.isNumeric(commandString)) {

                        StopResult selectedStop = (StopResult) _searchResults.getMatches()
                                .get(Integer.parseInt(commandString) - 1);
                        _searchResults.getMatches().clear();
                        _searchResults.getMatches().add(selectedStop);

                        if (_searchResults.getRouteFilter().size() > 0) {
                            RouteBean routeBean = (RouteBean) _searchResults.getRouteFilter().toArray()[0];
                            _lastQuery = selectedStop.getIdWithoutAgency() + " " + routeBean.getShortName();
                        } else {
                            _lastQuery = selectedStop.getIdWithoutAgency();
                        }

                        _response = singleStopResponse(null);
                        _searchResults = null;

                    } else if (_searchResults.getMatches().size() > 1) {

                        // See if there is a stop in search results that matches our route if filter
                        StopResult aStopServingRouteInFilter = (StopResult) CollectionUtils
                                .find(_searchResults.getMatches(), new Predicate() {

                                    @Override
                                    public boolean evaluate(Object object) {
                                        StopResult stopResult = (StopResult) object;
                                        if (stopResult.matchesRouteIdFilter()) {
                                            return true;
                                        }
                                        return false;
                                    }
                                });

                        // If there is a stop matching our route id filter and there is a route id filter (if filter is empty or null, everything in
                        // search results 'matches' the filter) present the direction disambiguation view.
                        if (aStopServingRouteInFilter != null && _searchResults.getRouteFilter() != null
                                && _searchResults.getRouteFilter().size() > 0) {
                            _response = directionDisambiguationResponse();

                            // If there is only one route served by the stops in our search results, set the command
                            // string as if the user had chosen this route. We are skipping asking them to do that.
                        } else if (getRoutesInSearchResults().size() == 1) {

                            commandString = getRoutesInSearchResults().first();
                            continue;

                            // There is more than one route served by the stops in our results and 
                            // either there is no route id filter or none of our search results match the filter, so present
                            // the multiple stop response
                        } else {
                            _response = multipleStopResponse();
                        }
                    } else if (_searchResults.getMatches().size() == 1) {

                        StopResult stopResult = (StopResult) _searchResults.getMatches().get(0);

                        // Check for case where there is a route id filter, but no service for
                        // that route for the stop in question.
                        if (!stopResult.matchesRouteIdFilter()) {

                            // Search for nearby stops
                            SearchResultCollection nearbyStops = _searchService.findStopsNearPoint(
                                    stopResult.getStop().getLat(), stopResult.getStop().getLon(), _resultFactory,
                                    _searchResults.getRouteFilter());

                            // See if there is at least one stop of the nearby stops that serves the route in the filter
                            StopResult aNearbyStopServingRouteInFilter = (StopResult) CollectionUtils
                                    .find(nearbyStops.getMatches(), new Predicate() {

                                        @Override
                                        public boolean evaluate(Object object) {
                                            StopResult stopResult = (StopResult) object;
                                            if (stopResult.matchesRouteIdFilter()) {
                                                return true;
                                            }
                                            return false;
                                        }
                                    });

                            // If we found nearby stops that service the route(s) in the filter, add those stops to our
                            // search results and call the method that knows how to display them.
                            if (aNearbyStopServingRouteInFilter != null) {

                                _response = singleStopWrongFilterNearbyResponse();
                                _searchResults = nearbyStops;

                                // We can't find nearby stops that service the route in the filter
                                // so call the method that lets the user know that.
                            } else {
                                RouteBean routeBean = (RouteBean) _searchResults.getRouteFilter().toArray()[0];
                                _searchResults = _searchService.getSearchResults(stopResult.getIdWithoutAgency(),
                                        _resultFactory);
                                _lastQuery = stopResult.getIdWithoutAgency();
                                _response = singleStopResponse("No " + routeBean.getShortName() + " stops nearby");
                                _searchResults = null;
                            }
                        } else {
                            _response = singleStopResponse(null);
                            _searchResults = null;
                        }
                    }
                    break;

                    // an exact match for a location--i.e. location isn't ambiguous
                } else if (_searchResults.getMatches().size() == 1
                        && "GeocodeResult".equals(_searchResults.getResultType())) {
                    GeocodeResult geocodeResult = (GeocodeResult) _searchResults.getMatches().get(0);

                    // we don't do anything with regions--too much information to show via SMS.
                    if (geocodeResult.isRegion()) {
                        _response = errorResponse("Too general.");
                        break;

                        // find stops near the point/address/intersection the user provided
                    } else {
                        _searchResults = _searchService.findStopsNearPoint(geocodeResult.getLatitude(),
                                geocodeResult.getLongitude(), _resultFactory, _searchResults.getRouteFilter());
                        _searchResults.setQueryLat(geocodeResult.getLatitude());
                        _searchResults.setQueryLon(geocodeResult.getLongitude());
                        continue;

                    }
                }
            }

            // process suggestions: suggestions can be ambiguous locations, or 
            // multiple routes--e.g. X17 -> X17A,C,J
            if (_searchResults.getSuggestions().size() > 0) {
                if ("RouteResult".equals(_searchResults.getResultType())) {
                    _response = didYouMeanResponse();

                    // if we get a geocode result, the user is choosing among multiple
                    // ambiguous addresses. we also recognize a numeric input that
                    // represents which ambiguous location number the user wants to use.
                } else if ("GeocodeResult".equals(_searchResults.getResultType())) {
                    if (commandString != null) {
                        if (commandString.equals("M")) {
                            _response = locationDisambiguationResponse(_searchResultsCursor);
                            break;

                            // choosing an ambiguous choice by number
                        } else if (StringUtils.isNumeric(commandString)) {
                            GeocodeResult chosenLocation = (GeocodeResult) _searchResults.getSuggestions()
                                    .get(Integer.parseInt(commandString) - 1);

                            if (chosenLocation != null) {
                                _searchResults = _searchService.findStopsNearPoint(chosenLocation.getLatitude(),
                                        chosenLocation.getLongitude(), _resultFactory,
                                        _searchResults.getRouteFilter());
                                _searchResults.setQueryLat(chosenLocation.getLatitude());
                                _searchResults.setQueryLon(chosenLocation.getLongitude());
                                commandString = null;
                                continue;
                            }
                        }

                    } else {
                        _response = locationDisambiguationResponse(0);
                        break;
                    }
                }
            }

            break;
        }

        // no response generated--no results or unrecognized query
        if (StringUtils.isEmpty(_response)) {
            _response = errorResponse("No results.");

            if (_googleAnalytics != null) {
                try {
                    _googleAnalytics.trackEvent("SMS", "No Results", _query);
                } catch (Exception e) {
                    //discard
                }
            }
        }

        return SUCCESS;
    }

    /**
     * RESPONSE GENERATION METHODS
     */
    private String serviceAlertResponse(int offset) throws Exception {
        if (offset >= _searchResults.getMatches().size()) {
            return errorResponse("No more.");
        }

        // worst case in terms of footer length
        String footer = "Send:\n";
        footer += "N for next alert\n";

        String body = "";
        int i = offset;
        while (i < _searchResults.getMatches().size()) {
            ServiceAlertResult serviceAlert = (ServiceAlertResult) _searchResults.getMatches().get(i);
            String textToAdd = serviceAlert.getAlert() + "\n\n";

            // if the alert alone is too long, we have to chop it
            if (textToAdd.length() > MAX_SMS_CHARACTER_COUNT - footer.length() - 5) {
                textToAdd = textToAdd.substring(0, MAX_SMS_CHARACTER_COUNT - footer.length() - 22)
                        + "... more at MTA.info\n\n";
            }

            if (body.length() + footer.length() + textToAdd.length() <= MAX_SMS_CHARACTER_COUNT) {
                body += textToAdd;
            } else {
                break;
            }

            i++;
        }
        _searchResultsCursor = i;

        if (_googleAnalytics != null) {
            try {
                _googleAnalytics.trackEvent("SMS", "Service Alert",
                        _query + " [" + _searchResults.getMatches().size() + "]");
            } catch (Exception e) {
                //discard
            }
        }

        if (i < _searchResults.getMatches().size()) {
            return body + footer;
        } else {
            return body;
        }
    }

    // whether a route has scheduled service or not--always returns a message that fits into one SMS.
    private String routeResponse(RouteResult result) throws Exception {
        String header = result.getShortName() + "\n\n";

        String footer = "\nSend:\n";
        footer += "STOPCODE or INTERSECTION\n";
        footer += "Add '" + result.getShortName() + "' for best results\n";

        RouteDirection aDirectionWithServiceAlerts = (RouteDirection) CollectionUtils.find(result.getDirections(),
                new Predicate() {

                    @Override
                    public boolean evaluate(Object object) {
                        RouteDirection routeDirection = (RouteDirection) object;
                        if (routeDirection.getSerivceAlerts().size() > 0) {
                            return true;
                        }
                        return false;
                    }
                });

        if (aDirectionWithServiceAlerts != null) {
            footer += "\nC " + result.getShortName() + " for *svc alert";
        }

        // find biggest headsign
        int routeDirectionTruncationLength = -1;
        for (RouteDirection direction : result.getDirections()) {
            routeDirectionTruncationLength = Math.max(routeDirectionTruncationLength,
                    direction.getDestination().length());
        }

        // try to fit the entire headsign, but if we can't, start chopping from the longest one down
        String body = null;
        while (body == null || header.length() + body.length() + footer.length() >= MAX_SMS_CHARACTER_COUNT) {
            body = "";
            for (RouteDirection direction : result.getDirections()) {
                String headsign = direction.getDestination();

                String alertString = "";
                if (direction.getSerivceAlerts().size() > 0) {
                    alertString = "*";
                }

                if (headsign.length() + alertString.length() > routeDirectionTruncationLength) {
                    body += "to "
                            + headsign.substring(0, Math.min(routeDirectionTruncationLength, headsign.length()))
                            + "..." + alertString;
                } else {
                    body += "to " + headsign + alertString;
                }
                body += "\n";

                if (direction.hasUpcomingScheduledService() == false) {
                    body += "not scheduled\n";
                } else {
                    body += "is scheduled\n";
                }
            }

            routeDirectionTruncationLength--;
        }

        if (routeDirectionTruncationLength <= 0) {
            throw new Exception("Couldn't fit any route names!?");
        }

        if (_googleAnalytics != null) {
            try {
                _googleAnalytics.trackEvent("SMS", "Route Response", _query);
            } catch (Exception e) {
                //discard
            }
        }

        return header + body + footer;
    }

    private String locationDisambiguationResponse(int offset) throws Exception {
        if (offset >= _searchResults.getSuggestions().size()) {
            return errorResponse("No more.");
        }

        String header = "Did you mean?\n\n";

        String footer = "\n";
        footer += "Send:\n";
        footer += "1-" + _searchResults.getSuggestions().size() + "\n";

        // the worst case in terms of length for footer
        String moreFooter = footer + "M for more\n";

        String body = "";
        int i = offset;
        while (i < _searchResults.getSuggestions().size()) {
            GeocodeResult serviceAlert = (GeocodeResult) _searchResults.getSuggestions().get(i);

            String textToAdd = (i + 1) + ") " + serviceAlert.getFormattedAddress() + "\n";
            textToAdd += "(" + serviceAlert.getNeighborhood() + ")\n";

            if (header.length() + body.length() + moreFooter.length()
                    + textToAdd.length() < MAX_SMS_CHARACTER_COUNT) {
                body += textToAdd;
            } else {
                break;
            }

            i++;
        }
        _searchResultsCursor = i;

        if (_googleAnalytics != null) {
            try {
                _googleAnalytics.trackEvent("SMS", "Location Disambiguation",
                        _query + " [" + _searchResults.getSuggestions().size() + "]");
            } catch (Exception e) {
                //discard
            }
        }

        if (i < _searchResults.getSuggestions().size()) {
            return header + body + moreFooter;
        } else {
            return header + body + footer;
        }
    }

    private String multipleStopResponse() throws Exception {

        String b = "";

        RouteBean routeBean = null;
        if (_searchResults.getRouteFilter() != null && _searchResults.getRouteFilter().size() > 0) {
            routeBean = (RouteBean) _searchResults.getRouteFilter().toArray()[0];
        }

        if (routeBean != null && !getRoutesInSearchResults().contains(routeBean.getShortName())) {
            b += "No " + routeBean.getShortName() + " stops nearby\n\n";
            b += "Routes nearby:\n\n";
        } else {
            b += _searchResults.getMatches().size() + " stops nearby\n\n";
            b += "Pick a route:\n\n";
        }

        for (String route : getRoutesInSearchResults()) {
            b += route + "\n";
        }

        b += "\nSend:\n";
        b += "ROUTE";

        if (_googleAnalytics != null) {
            try {
                _googleAnalytics.trackEvent("SMS", "Stop Realtime Response For Multiple Stops", _query);
            } catch (Exception e) {
                //discard
            }
        }

        return b;
    }

    private String directionDisambiguationResponse() throws Exception {

        filterStopSearchResultsToRouteFilterAndDirection();

        RouteBean routeBean = (RouteBean) _searchResults.getRouteFilter().toArray()[0];

        String a = null;
        if (routeBean.getShortName().toUpperCase().matches("^(A|E|I|O|U).*$")) {
            a = "an";
        } else {
            a = "a";
        }

        String header = "Pick " + a + " " + routeBean.getShortName() + " direction:\n\n";

        List<String> choices = new ArrayList<String>();
        List<String> choiceNumbers = new ArrayList<String>();

        for (SearchResult searchResult : _searchResults.getMatches()) {
            StopResult stopResult = (StopResult) searchResult;

            RouteAtStop routeAtStopInFilter = (RouteAtStop) CollectionUtils.find(stopResult.getRoutesAvailable(),
                    new Predicate() {

                        @Override
                        public boolean evaluate(Object object) {
                            RouteAtStop routeAtStop = (RouteAtStop) object;
                            if (routeAtStop.getRoute().equals(_searchResults.getRouteFilter().toArray()[0])) {
                                return true;
                            }
                            return false;
                        }
                    });

            String destination = routeAtStopInFilter.getDirections().get(0).getDestination();

            int choiceNumber = _searchResults.getMatches().indexOf(searchResult) + 1;
            choiceNumbers.add(String.valueOf(choiceNumber));
            choices.add(choiceNumber + ") " + destination);
        }

        String footer = "Send:\n";
        footer += StringUtils.join(choiceNumbers, " or ");

        // find biggest choice
        int choiceTruncationLength = -1;
        for (String choice : choices) {
            choiceTruncationLength = Math.max(choiceTruncationLength, choice.length());
        }

        // try to fit the entire choice, but if we can't, start chopping from the longest one down
        String body = null;
        while (body == null || header.length() + body.length() + footer.length() >= MAX_SMS_CHARACTER_COUNT) {
            body = "";
            for (String choice : choices) {

                if (choice.length() > choiceTruncationLength) {
                    body += choice.substring(0, Math.min(choiceTruncationLength, choice.length())) + "...";
                } else {
                    body += choice;
                }
                body += "\n\n";
            }

            choiceTruncationLength--;
        }

        if (choiceTruncationLength <= 0) {
            throw new Exception("Couldn't fit any direction choice names!?");
        }

        if (_googleAnalytics != null) {
            try {
                _googleAnalytics.trackEvent("SMS", "Direction Disambiguation Response",
                        ((RouteBean) (_searchResults.getRouteFilter().toArray()[0])).getShortName());
            } catch (Exception e) {
                //discard
            }
        }

        return header + body + footer;
    }

    private String singleStopResponse(String message) throws Exception {

        if (message == null) {
            message = "";
        }
        message = message.trim();
        if (!message.isEmpty()) {
            message = "\n" + message + "\n";
        }

        StopResult stopResult = (StopResult) _searchResults.getMatches().get(0);

        String header = "Stop " + stopResult.getIdWithoutAgency() + "\n\n";

        String footer = "\nSend:\n";
        footer += "R for refresh\n";
        if (_searchResults.getRouteFilter().isEmpty() && stopResult.getStop().getRoutes().size() > 1) {
            footer += stopResult.getIdWithoutAgency() + "+ROUTE for bus info\n";
        }

        // worst case for footer length
        String alertsFooter = footer + "C+ROUTE for *svc alert\n";

        // body content for stops
        String body = "";
        if (stopResult.getRoutesAvailable().size() == 0) {
            // if we found a stop with no routes because of a stop+route filter, 
            // indicate that specifically
            if (_searchResults.getRouteFilter().size() > 0) {
                body += "No filter matches\n";
            } else {
                body += "No routes\n";
            }
        } else {
            // bulid map of sorted vehicle observation strings for this stop, sorted by closest->farthest
            TreeMap<Double, String> observationsByDistanceFromStopAcrossAllRoutes = new TreeMap<Double, String>();
            // Keep track of not scheduled and not en route so we can display that later
            Set<String> notScheduledRoutes = new HashSet<String>();
            Set<String> notEnRouteRoutes = new HashSet<String>();

            for (RouteAtStop routeHere : stopResult.getRoutesAvailable()) {

                if (_searchResults.getRouteFilter() != null && !_searchResults.getRouteFilter().isEmpty()
                        && !_searchResults.getRouteFilter().contains(routeHere.getRoute())) {
                    continue;
                }

                for (RouteDirection direction : routeHere.getDirections()) {
                    String prefix = "";
                    if (!direction.getSerivceAlerts().isEmpty()) {
                        footer = alertsFooter;
                        prefix += "*";
                    }
                    prefix += routeHere.getShortName();

                    if (!direction.hasUpcomingScheduledService() && direction.getDistanceAways().isEmpty()) {
                        notScheduledRoutes.add(prefix);
                    } else {
                        if (!direction.getDistanceAways().isEmpty()) {
                            HashMap<Double, String> sortableDistanceAways = direction.getDistanceAwaysWithSortKey();
                            for (Double distanceAway : sortableDistanceAways.keySet()) {
                                String distanceAwayString = sortableDistanceAways.get(distanceAway);

                                observationsByDistanceFromStopAcrossAllRoutes.put(distanceAway,
                                        prefix + ": " + distanceAwayString);
                            }
                        } else {
                            notEnRouteRoutes.add(prefix);
                        }
                    }
                }
            }

            // if there are no upcoming buses, provide info about the routes that are not en route or not scheduled
            if (observationsByDistanceFromStopAcrossAllRoutes.isEmpty()) {
                if (notEnRouteRoutes.size() > 0) {
                    body += StringUtils.join(notEnRouteRoutes, ",") + ": no buses en-route\n";
                }
                if (notScheduledRoutes.size() > 0) {
                    body += StringUtils.join(notScheduledRoutes, ",") + ": not scheduled\n";
                }
                // as many observations as will fit, sorted by soonest to arrive out
            } else {
                for (String observationString : observationsByDistanceFromStopAcrossAllRoutes.values()) {
                    String textToAdd = observationString + "\n";

                    if (message.length() + body.length() + header.length() + alertsFooter.length()
                            + textToAdd.length() < MAX_SMS_CHARACTER_COUNT) {
                        body += textToAdd;
                    } else {
                        break;
                    }
                }
            }
        }

        if (_googleAnalytics != null) {
            try {
                _googleAnalytics.trackEvent("SMS", "Stop Realtime Response for Single Stop", _query);
            } catch (Exception e) {
                //discard
            }
        }

        return header + body + message + footer;
    }

    private String singleStopWrongFilterNearbyResponse() {

        StopResult searchedForStop = (StopResult) _searchResults.getMatches().get(0);

        String body = "Stop " + searchedForStop.getIdWithoutAgency() + ":\n";

        List<String> searchedForStopRoutes = new ArrayList<String>();
        for (RouteBean route : searchedForStop.getStop().getRoutes()) {
            searchedForStopRoutes.add(route.getShortName());
        }

        body += StringUtils.join(searchedForStopRoutes, " ") + "\n\n";

        RouteBean routeBean = (RouteBean) _searchResults.getRouteFilter().toArray()[0];
        body += "No " + routeBean.getShortName() + " at this stop\n\n";

        body += "Send:\n";
        body += "N for nearby " + routeBean.getShortName() + " stops\n";
        body += searchedForStop.getIdWithoutAgency() + "+ROUTE for bus info";

        if (_googleAnalytics != null) {
            try {
                _googleAnalytics.trackEvent("SMS", "Stop Realtime Response for Single Stop with Bad Filter",
                        _query);
            } catch (Exception e) {
                //discard
            }
        }

        return body;
    }

    private String didYouMeanResponse() {
        String header = "Did you mean?\n\n";

        String footer = "\nSend:\n";
        footer += "ROUTE for schedule info\n";

        String body = "";
        for (SearchResult _route : _searchResults.getSuggestions()) {
            RouteResult route = (RouteResult) _route;
            body += route.getShortName() + " ";
        }
        body = body.trim();
        body += "\n";

        if (_googleAnalytics != null) {
            try {
                _googleAnalytics.trackEvent("SMS", "Did You Mean Response", _query);
            } catch (Exception e) {
                //discard
            }
        }

        return header + body + footer;
    }

    private String errorResponse(String message) throws Exception {
        String staticStuff = "Send:\n\n";

        staticStuff += "STOPCODE or\n";
        staticStuff += "INTERSECTION\n";
        staticStuff += "Add ROUTE for best results\n\n";

        staticStuff += "Examples:\n";
        staticStuff += "'PARK AV & 21 ST X1'\n";
        staticStuff += "'400145 X1'\n\n";

        staticStuff += "Find 6-digit stopcode on stop pole";

        if (_googleAnalytics != null) {
            try {
                _googleAnalytics.trackEvent("SMS", "Error Response", _query);
            } catch (Exception e) {
                //discard
            }
        }

        if (message != null) {
            if (staticStuff.length() + 1 + message.length() > MAX_SMS_CHARACTER_COUNT) {
                throw new Exception("Error message text is too long.");
            }

            return message + " " + staticStuff;
        } else {
            return staticStuff;
        }
    }

    /**
     * PRIVATE HELPER METHODS
     */
    private String getQuery(String query) {
        if (query == null) {
            return null;
        }

        query = query.trim();

        // if this is a command prefix, one with a parameter, the command is "C", the query is the
        // "parameter".
        if (query.toUpperCase().startsWith("C ") || query.toUpperCase().startsWith("C+")) {
            return query.substring(2);
        }

        // if this is a command, no query can be pressent
        if (getCommand(query) != null) {
            return null;
        }

        return query;
    }

    private String getCommand(String query) {
        if (query == null) {
            return null;
        }

        query = query.trim();

        if (query.toUpperCase().equals("R")) {
            return "R";
        }

        if (query.toUpperCase().equals("M") && _searchResults != null
                && "GeocodeResult".equals(_searchResults.getResultType())) {
            return "M";
        }

        if (query.toUpperCase().startsWith("C ") || query.toUpperCase().startsWith("C+")) {
            return "C";
        }

        // if we have nearby stops and the user wants to see them
        if (query.toUpperCase().equals("N") && _searchResults != null
                && ("StopResult".equals(_searchResults.getResultType())
                        || "ServiceAlertResult".equals(_searchResults.getResultType()))) {
            return "N";
        }

        // Check if it's a route currently in our result set
        if (query != null && _searchResults != null && "StopResult".equals(_searchResults.getResultType())
                && getRoutesInSearchResults().contains(query.toUpperCase())) {
            return query.toUpperCase();
        }

        // try as ambiguous location result choice:
        try {
            // no results means this can't be a pick from ambiguous location results
            if (_searchResults == null || _searchResults.isEmpty()) {
                return null;
            }

            // a list of things other than location results can't be a pick from ambiguous locations.
            if (!("GeocodeResult".equals(_searchResults.getResultType()))
                    && !("StopResult".equals(_searchResults.getResultType()))) {
                return null;
            }

            // is the choice within the bounds of the number of things we have? Either geocode suggestions or search matches
            Integer choice = Integer.parseInt(query);
            if (choice >= 1 && (choice <= _searchResults.getSuggestions().size()
                    || choice <= _searchResults.getMatches().size())) {
                return choice.toString();
            }
        } catch (NumberFormatException e) {
            return null;
        }

        return null;
    }

    private SortedSet<String> getRoutesInSearchResults() {
        SortedSet<String> routes = new TreeSet<String>();
        if (_searchResults != null) {
            for (SearchResult searchResult : _searchResults.getMatches()) {
                StopResult stopResult = (StopResult) searchResult;
                for (RouteBean routeBean : stopResult.getStop().getRoutes()) {
                    routes.add(routeBean.getShortName().toUpperCase());
                }
            }
        }
        return routes;
    }

    private void filterStopSearchResultsToRouteFilterAndDirection() {

        CollectionUtils.filter(_searchResults.getMatches(), new Predicate() {

            private Set<String> directionsInResults = new HashSet<String>();

            @Override
            public boolean evaluate(Object arg0) {
                StopResult stopResult = (StopResult) arg0;
                for (RouteAtStop routeAtStop : stopResult.getRoutesAvailable()) {
                    if (_searchResults.getRouteFilter().contains(routeAtStop.getRoute())
                            && !directionsInResults.contains(routeAtStop.getDirections().get(0).getDestination())) {
                        directionsInResults.add(routeAtStop.getDirections().get(0).getDestination());
                        return true;
                    }
                }
                return false;
            }
        });
    }

    /**
     * METHODS FOR VIEWS
     */
    public String getResponse() {

        String response = _response.trim();

        if (_needsGlobalAlert != null && _needsGlobalAlert) {
            List<ServiceAlertBean> globalAlerts = _realtimeService.getServiceAlertsGlobal();
            if (globalAlerts != null && globalAlerts.size() > 0) {
                String alertsThatFit = "\n\nService Notice: ";
                String end = "... More at mta.info";
                for (ServiceAlertBean alert : globalAlerts) {

                    @SuppressWarnings("unchecked")
                    Collection<String> descriptions = CollectionUtils.collect(alert.getDescriptions(),
                            new Transformer() {
                                @Override
                                public Object transform(Object input) {
                                    return ((NaturalLanguageStringBean) input).getValue();
                                }
                            });

                    String descriptionString = StringUtils.join(descriptions, "\n\n");

                    if (response.length() + descriptionString.length()
                            + alertsThatFit.length() > MAX_SMS_CHARACTER_COUNT * 2) {
                        int endIndex = MAX_SMS_CHARACTER_COUNT * 2 - response.length() - alertsThatFit.length()
                                - end.length();
                        descriptionString = descriptionString.substring(0, endIndex) + end;
                        alertsThatFit += descriptionString;
                        break;
                    }
                    alertsThatFit += descriptionString + "\n\n";
                }
                response += alertsThatFit;
            }
            _needsGlobalAlert = false;
        }

        syncSession();

        return response;
    }

    private void initAnalytics() {
        // Initialize Google Analytics
        String googleAnalyticsSiteId = _configurationService
                .getConfigurationValueAsString("display.googleAnalyticsSiteId", null);
        try {
            if (googleAnalyticsSiteId != null) {
                AnalyticsConfigData config = new AnalyticsConfigData(googleAnalyticsSiteId, _visitorCookie);
                _googleAnalytics = new JGoogleAnalyticsTracker(config, GoogleAnalyticsVersion.V_4_7_2);
            }
        } catch (Exception e) {
            // discard
        }

    }
}