org.onebusaway.nyc.presentation.impl.NycSearchServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.onebusaway.nyc.presentation.impl.NycSearchServiceImpl.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.presentation.impl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.onebusaway.geocoder.model.GeocoderResult;
import org.onebusaway.geocoder.services.GeocoderService;
import org.onebusaway.geospatial.model.CoordinateBounds;
import org.onebusaway.geospatial.services.SphericalGeometryLibrary;
import org.onebusaway.nyc.presentation.model.search.RouteSearchResult;
import org.onebusaway.nyc.presentation.model.search.SearchResult;
import org.onebusaway.nyc.presentation.model.search.StopSearchResult;
import org.onebusaway.nyc.presentation.service.NycSearchService;
import org.onebusaway.nyc.presentation.model.FormattingContext;
import org.onebusaway.nyc.presentation.model.RouteItem;
import org.onebusaway.nyc.presentation.model.DistanceAway;
import org.onebusaway.nyc.presentation.model.Mode;
import org.onebusaway.nyc.presentation.model.StopItem;
import org.onebusaway.nyc.presentation.service.ConfigurationBean;
import org.onebusaway.nyc.presentation.service.NycConfigurationService;
import org.onebusaway.presentation.model.ArrivalDepartureBeanListFilter;
import org.onebusaway.presentation.services.ServiceAreaService;
import org.onebusaway.transit_data.model.ArrivalAndDepartureBean;
import org.onebusaway.transit_data.model.ArrivalsAndDeparturesQueryBean;
import org.onebusaway.transit_data.model.ListBean;
import org.onebusaway.transit_data.model.NameBean;
import org.onebusaway.transit_data.model.RouteBean;
import org.onebusaway.transit_data.model.RoutesBean;
import org.onebusaway.transit_data.model.SearchQueryBean;
import org.onebusaway.transit_data.model.StopBean;
import org.onebusaway.transit_data.model.StopGroupBean;
import org.onebusaway.transit_data.model.StopGroupingBean;
import org.onebusaway.transit_data.model.StopWithArrivalsAndDeparturesBean;
import org.onebusaway.transit_data.model.StopsBean;
import org.onebusaway.transit_data.model.StopsForRouteBean;
import org.onebusaway.transit_data.model.TripStopTimeBean;
import org.onebusaway.transit_data.model.TripStopTimesBean;
import org.onebusaway.transit_data.model.service_alerts.NaturalLanguageStringBean;
import org.onebusaway.transit_data.model.service_alerts.SituationBean;
import org.onebusaway.transit_data.model.trips.TripBean;
import org.onebusaway.transit_data.model.trips.TripDetailsBean;
import org.onebusaway.transit_data.model.trips.TripDetailsInclusionBean;
import org.onebusaway.transit_data.model.trips.TripStatusBean;
import org.onebusaway.transit_data.model.trips.TripsForRouteQueryBean;
import org.onebusaway.transit_data.services.TransitDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class NycSearchServiceImpl implements NycSearchService {
    private final static Pattern routePattern = Pattern.compile("(?:[BMQS]|BX)[0-9]+", Pattern.CASE_INSENSITIVE);

    // print vehicle display logic debugging messages to console?
    private boolean debug = false;

    // when querying for stops from a lat/lng, use this distance in meters
    private double distanceToStops = 100;

    private WebappIdParser idParser = new WebappIdParser();

    @Autowired
    private TransitDataService transitService;

    @Autowired
    private GeocoderService geocoderService;

    @Autowired
    private NycConfigurationService configurationService;

    @Autowired
    private ServiceAreaService serviceArea;

    @Autowired
    private ArrivalDepartureBeanListFilter adbeanFilter;

    private static final SearchResultComparator searchResultComparator = new SearchResultComparator();

    @Override
    public List<SearchResult> search(String q, Mode m) {
        List<SearchResult> results = new ArrayList<SearchResult>();

        if (q == null)
            return results;

        q = q.trim();

        if (q.isEmpty())
            return results;

        String[] fields = q.split(" ");
        // we are probably a query for a route or a stop ID
        if (fields.length == 1) {
            if (isStop(q)) {
                SearchQueryBean queryBean = makeSearchQuery(q);
                StopsBean stops = transitService.getStops(queryBean);

                for (StopBean stopBean : stops.getStops()) {
                    StopSearchResult stopSearchResult = makeStopSearchResult(stopBean, m);
                    results.add(stopSearchResult);
                }

            } else if (isRoute(q)) {
                SearchQueryBean queryBean = makeSearchQuery(q);
                RoutesBean routes = transitService.getRoutes(queryBean);

                for (RouteBean routeBean : routes.getRoutes()) {
                    List<RouteSearchResult> routeSearchResult = makeRouteSearchResult(routeBean, m);
                    results.addAll(routeSearchResult);
                }
            }

            // we should be either an intersection and route or an intersection
        } else {
            // if we're a route, set route and the geocoder query appropriately
            String route = null;
            String queryNoRoute = null;

            if (isRoute(fields[0])) {
                route = fields[0];
                StringBuilder sb = new StringBuilder(32);

                for (int i = 1; i < fields.length; i++) {
                    sb.append(fields[i]);
                    sb.append(" ");
                }

                queryNoRoute = sb.toString().trim();

            } else if (isRoute(fields[fields.length - 1])) {
                route = fields[fields.length - 1];
                StringBuilder sb = new StringBuilder(32);

                for (int i = 0; i < fields.length - 1; i++) {
                    sb.append(fields[i]);
                    sb.append(" ");
                }

                queryNoRoute = sb.toString().trim();
            }

            // we are a route and intersection
            if (route != null) {
                List<StopsBean> stopsList = fetchStopsFromGeocoder(queryNoRoute);

                for (StopsBean stopsBean : stopsList) {
                    for (StopBean stopBean : stopsBean.getStops()) {
                        boolean useStop = false;

                        List<RouteBean> routes = stopBean.getRoutes();
                        for (RouteBean routeBean : routes) {
                            String idNoAgency = idParser.parseIdWithoutAgency(routeBean.getId());

                            if (idNoAgency.equalsIgnoreCase(route)) {
                                useStop = true;
                                break;
                            }
                        }

                        if (useStop) {
                            StopSearchResult stopSearchResult = makeStopSearchResult(stopBean, m);
                            results.add(stopSearchResult);
                        }
                    }
                }
            } else {
                // try an intersection search
                List<StopsBean> stopsBeanList = fetchStopsFromGeocoder(q);
                for (StopsBean stopsBean : stopsBeanList) {
                    List<StopBean> stopsList = stopsBean.getStops();

                    for (StopBean stopBean : stopsList) {
                        StopSearchResult stopSearchResult = makeStopSearchResult(stopBean, m);
                        results.add(stopSearchResult);
                    }
                }
            }
        }

        return sortSearchResults(results);
    }

    private Map<String, List<StopBean>> createDirectionToStopBeansMap(String routeId) {
        Map<String, List<StopBean>> directionIdToStopBeans = new HashMap<String, List<StopBean>>();

        StopsForRouteBean stopsForRoute = transitService.getStopsForRoute(routeId);
        List<StopBean> stopBeansList = stopsForRoute.getStops();

        List<StopGroupingBean> stopGroupings = stopsForRoute.getStopGroupings();
        for (StopGroupingBean stopGroupingBean : stopGroupings) {
            List<StopGroupBean> stopGroups = stopGroupingBean.getStopGroups();
            for (StopGroupBean stopGroupBean : stopGroups) {
                NameBean name = stopGroupBean.getName();
                String type = name.getType();

                if (!type.equals("destination"))
                    continue;

                List<StopBean> stopsForDirection = new ArrayList<StopBean>();
                String directionId = stopGroupBean.getId();
                Set<String> stopIds = new HashSet<String>(stopGroupBean.getStopIds());

                for (StopBean stopBean : stopBeansList) {
                    String stopBeanId = stopBean.getId();
                    if (stopIds.contains(stopBeanId))
                        stopsForDirection.add(stopBean);
                }

                directionIdToStopBeans.put(directionId, stopsForDirection);
            }
        }
        return directionIdToStopBeans;
    }

    private TripsForRouteQueryBean makeTripsForRouteQueryBean(String routeId) {
        TripsForRouteQueryBean tripRouteQueryBean = new TripsForRouteQueryBean();
        tripRouteQueryBean.setRouteId(routeId);
        tripRouteQueryBean.setMaxCount(100);
        tripRouteQueryBean.setTime(new Date().getTime());
        TripDetailsInclusionBean inclusionBean = new TripDetailsInclusionBean();
        inclusionBean.setIncludeTripBean(true);
        inclusionBean.setIncludeTripSchedule(true);
        inclusionBean.setIncludeTripStatus(true);
        tripRouteQueryBean.setInclusion(inclusionBean);
        return tripRouteQueryBean;
    }

    public boolean isStop(String s) {
        // stops are 6 digits
        int n = s.length();
        if (n != 6)
            return false;
        for (int i = 0; i < n; i++) {
            if (!Character.isDigit(s.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    public boolean isRoute(String s) {
        Matcher matcher = routePattern.matcher(s);
        return matcher.matches();
    }

    private List<StopsBean> fetchStopsFromGeocoder(String q) {
        List<StopsBean> result = new ArrayList<StopsBean>();

        // FIXME HACK: append brooklyn to addresses to prevent manhattan adresses from being
        // returned instead--use google viewport biasing instead?
        if (q != null && q.isEmpty() == false) {
            q = q + " brooklyn, ny";
        }

        List<GeocoderResult> geocoderResults = geocoderService.geocode(q).getResults();
        for (GeocoderResult geocoderResult : geocoderResults) {
            double lat = geocoderResult.getLatitude();
            double lng = geocoderResult.getLongitude();
            CoordinateBounds bounds = SphericalGeometryLibrary.bounds(lat, lng, distanceToStops);

            // and add any stops for it
            SearchQueryBean searchQueryBean = makeSearchQuery(bounds);
            searchQueryBean.setMaxCount(100);
            StopsBean stops = transitService.getStops(searchQueryBean);
            result.add(stops);
        }
        return result;
    }

    private List<RouteSearchResult> makeRouteSearchResult(RouteBean routeBean, Mode m) {
        ConfigurationBean config = configurationService.getConfiguration();

        List<RouteSearchResult> results = new ArrayList<RouteSearchResult>();

        String routeId = routeBean.getId();
        String routeShortName = routeBean.getShortName();
        String routeLongName = routeBean.getLongName();

        // create lookups keyed by directionId to the different bits of
        // information we need for the response
        Map<String, List<StopBean>> directionIdToStopBeans = createDirectionToStopBeansMap(routeId);
        Map<String, String> directionIdToHeadsign = new HashMap<String, String>();
        Map<String, String> directionIdToTripId = new HashMap<String, String>();
        Map<String, Map<String, NaturalLanguageStringBean>> directionIdToServiceAlerts = new HashMap<String, Map<String, NaturalLanguageStringBean>>();

        Map<String, Map<String, Double>> tripIdToStopDistancesMap = new HashMap<String, Map<String, Double>>();
        Map<String, List<DistanceAway>> stopIdToDistanceAways = new HashMap<String, List<DistanceAway>>();

        TripsForRouteQueryBean tripQueryBean = makeTripsForRouteQueryBean(routeId);
        ListBean<TripDetailsBean> tripsForRoute = transitService.getTripsForRoute(tripQueryBean);
        List<TripDetailsBean> tripsList = tripsForRoute.getList();

        // create a map of trip ids->(stop ids->distances along trip values) to
        // figure out which stop the vehicle is between. This is all with schedule data only so far...
        for (TripDetailsBean tripDetailsBean : tripsList) {
            TripBean trip = tripDetailsBean.getTrip();
            String tripId = trip.getId();
            String tripDirectionId = trip.getDirectionId();
            String tripHeadsign = trip.getTripHeadsign();

            // FIXME: most common headsign? 
            directionIdToHeadsign.put(tripDirectionId, tripHeadsign);
            directionIdToTripId.put(tripDirectionId, tripId);

            if (!tripIdToStopDistancesMap.containsKey(tripId)) {
                TripStopTimesBean schedule = tripDetailsBean.getSchedule();
                List<TripStopTimeBean> stopTimes = schedule.getStopTimes();

                for (TripStopTimeBean tripStopTimeBean : stopTimes) {
                    StopBean stopBean = tripStopTimeBean.getStop();
                    String stopId = stopBean.getId();
                    double distanceAlongTrip = tripStopTimeBean.getDistanceAlongTrip();

                    Map<String, Double> stopIdToDistance = tripIdToStopDistancesMap.get(tripId);

                    if (stopIdToDistance == null) {
                        stopIdToDistance = new HashMap<String, Double>();
                        tripIdToStopDistancesMap.put(tripId, stopIdToDistance);
                    }

                    stopIdToDistance.put(stopId, distanceAlongTrip);
                }
            }

            TripStatusBean tripStatusBean = tripDetailsBean.getStatus();

            // compile a list of service alerts that affect this route+direction
            Map<String, NaturalLanguageStringBean> serviceAlertIdsToNaturalLanguageStringBeans = directionIdToServiceAlerts
                    .get(tripDirectionId);

            if (serviceAlertIdsToNaturalLanguageStringBeans == null) {
                serviceAlertIdsToNaturalLanguageStringBeans = new HashMap<String, NaturalLanguageStringBean>();
                directionIdToServiceAlerts.put(tripDirectionId, serviceAlertIdsToNaturalLanguageStringBeans);
            }

            if (tripStatusBean.getSituations() != null) {
                for (SituationBean situationBean : tripStatusBean.getSituations()) {
                    NaturalLanguageStringBean serviceAlert = serviceAlertIdsToNaturalLanguageStringBeans
                            .get(situationBean.getId());

                    if (serviceAlert == null) {
                        serviceAlertIdsToNaturalLanguageStringBeans.put(situationBean.getId(),
                                situationBean.getDescription());
                    }
                }
            }

            // should we display this vehicle on the UI specified by "m"?
            if (tripStatusBean == null || !shouldDisplayTripForUIMode(tripStatusBean, m))
                continue;

            /*
             * Now that we have a structure of stops and each stop's distance along
             * the route, we can calculate how far the vehicle (for which we only have
             * distance along *route*) is away from its next stop in meters (which we
             * also have a distance along route for).
             */
            StopBean closestStop = tripStatusBean.getNextStop();

            if (closestStop != null) {
                String closestStopId = closestStop.getId();
                double distanceAwayFromClosestStopInMeters = tripStatusBean.getNextStopDistanceFromVehicle();
                int distanceAwayFromClosestStopInFeet = (int) this
                        .metersToFeet(distanceAwayFromClosestStopInMeters);

                List<DistanceAway> stopDistanceAways = stopIdToDistanceAways.get(closestStopId);

                if (stopDistanceAways == null) {
                    stopDistanceAways = new ArrayList<DistanceAway>();
                    stopIdToDistanceAways.put(closestStopId, stopDistanceAways);
                }

                // (we're always 0 stops away from our next stop, by definition)
                DistanceAway distanceAway = new DistanceAway(0, distanceAwayFromClosestStopInFeet,
                        new Date(tripStatusBean.getLastUpdateTime()), m, config.getStaleDataTimeout(),
                        FormattingContext.ROUTE, tripStatusBean);

                stopDistanceAways.add(distanceAway);
            }
        }

        // for each route direction for the route we are creating a result for...
        for (Map.Entry<String, List<StopBean>> directionStopBeansEntry : directionIdToStopBeans.entrySet()) {
            String directionId = directionStopBeansEntry.getKey();
            String tripId = directionIdToTripId.get(directionId);
            String tripHeadsign = directionIdToHeadsign.get(directionId);

            // on the off chance that this happens degrade more gracefully
            if (tripHeadsign == null)
                continue;

            // get list of stops for this route *direction*
            List<StopBean> stopBeansList = directionStopBeansEntry.getValue();
            Map<String, Double> stopIdToDistances = tripIdToStopDistancesMap.get(tripId);

            // (sort by order in route)
            if (stopIdToDistances != null) {
                Comparator<StopBean> stopBeanComparator = new NycSearchServiceImpl.StopBeanComparator(
                        stopIdToDistances);
                Collections.sort(stopBeansList, stopBeanComparator);
            }

            // for each stop on the route, by order bus will stop there...
            List<StopItem> stopItemsList = new ArrayList<StopItem>();

            for (StopBean stopBean : stopBeansList) {
                String stopId = stopBean.getId();
                List<DistanceAway> distances = stopIdToDistanceAways.get(stopId);

                if (distances == null)
                    distances = Collections.emptyList();

                // if there is more than one vehicle stopping at this stop, sort the
                // vehicles by distance away from this stop
                Collections.sort(distances);

                StopItem stopItem = new StopItem(stopBean, distances);
                stopItemsList.add(stopItem);
            }

            // get a list of unique service alerts that affect this route direction
            List<NaturalLanguageStringBean> serviceAlerts = null;
            Map<String, NaturalLanguageStringBean> serviceAlertIdsToServiceAlerts = directionIdToServiceAlerts
                    .get(directionId);
            if (serviceAlertIdsToServiceAlerts == null) {
                serviceAlerts = Collections.emptyList();
            } else {
                serviceAlerts = new ArrayList<NaturalLanguageStringBean>(serviceAlertIdsToServiceAlerts.values());
            }

            RouteSearchResult routeSearchResult = new RouteSearchResult(routeId, routeShortName, routeLongName,
                    tripHeadsign, directionId, stopItemsList, serviceAlerts);

            results.add(routeSearchResult);
        }

        return results;
    }

    private boolean shouldDisplayTripForUIMode(TripStatusBean statusBean, Mode m) {
        // UI states here:
        // https://spreadsheets.google.com/ccc?key=0Al2nqv1nCD71dGt5SkpHajRQZmdLaVZScnhoYVhiZWc&hl=en#gid=0

        // don't show non-realtime trips (row 8)
        if (statusBean == null || statusBean.isPredicted() == false
                || Double.isNaN(statusBean.getDistanceAlongTrip())) {
            return false;
        }

        String status = statusBean.getStatus();
        String phase = statusBean.getPhase();

        // hide disabled vehicles (row 7)
        if (status != null && status.toLowerCase().compareTo("disabled") == 0) {
            return false;
        }

        // hide deadheading vehicles (except within a block) (row 3)
        // hide vehicles at the depot (row 1)
        if (phase != null && phase.toLowerCase().compareTo("in_progress") != 0
                && phase.toLowerCase().compareTo("layover_before") != 0
                && phase.toLowerCase().compareTo("layover_during") != 0) {
            return false;
        }

        // hide deviated vehicles from mobile web + sms interfaces (row 4)
        if (m == Mode.MOBILE_WEB || m == Mode.SMS) {
            boolean routeIsOnDetour = false;

            List<SituationBean> situations = statusBean.getSituations();
            if (situations != null) {
                for (SituationBean situationBean : situations) {
                    String miscelleanousReason = situationBean.getMiscellaneousReason();

                    if (miscelleanousReason != null && miscelleanousReason.compareTo("detour") == 0) {
                        routeIsOnDetour = true;
                        break;
                    }
                }
            }

            if ((status != null && status.toLowerCase().compareTo("deviated") == 0) && !routeIsOnDetour) {
                return false;
            }
        }

        // hide data >= (hide timeout) minutes old (row 5)
        ConfigurationBean config = configurationService.getConfiguration();

        if (new Date().getTime() - statusBean.getLastUpdateTime() >= 1000 * config.getHideTimeout()) {
            return false;
        }

        return true;
    }

    private StopSearchResult makeStopSearchResult(StopBean stopBean, Mode m) {
        ConfigurationBean config = configurationService.getConfiguration();

        String stopId = stopBean.getId();
        String stopName = stopBean.getName();
        String stopDirection = stopBean.getDirection();
        List<Double> latLng = Arrays.asList(new Double[] { stopBean.getLat(), stopBean.getLon() });

        Map<String, List<DistanceAway>> routeIdToDistanceAways = new HashMap<String, List<DistanceAway>>();
        Map<String, String> headsignToDirectionId = new HashMap<String, String>();
        Map<String, String> routeIdToHeadsign = new HashMap<String, String>();
        Map<String, Boolean> routeIdToDetouredFlag = new HashMap<String, Boolean>();
        List<RouteItem> availableRoutes = new ArrayList<RouteItem>();
        Map<String, NaturalLanguageStringBean> serviceAlertIdsToServiceAlerts = new HashMap<String, NaturalLanguageStringBean>();

        ArrivalsAndDeparturesQueryBean query = new ArrivalsAndDeparturesQueryBean();
        query.setTime(System.currentTimeMillis());
        query.setMinutesBefore(60);
        query.setMinutesAfter(90);

        StopWithArrivalsAndDeparturesBean stopWithArrivalsAndDepartures = transitService
                .getStopWithArrivalsAndDepartures(stopId, query);

        // this process uses unfiltered routes so that the interfaces show routes that
        // typically stop at a given stop, even if there are no "live"/tracked vehicles currently 
        // serving that route right now.
        List<ArrivalAndDepartureBean> arrivalsAndDepartures = stopWithArrivalsAndDepartures
                .getArrivalsAndDepartures();
        for (ArrivalAndDepartureBean arrivalAndDepartureBean : arrivalsAndDepartures) {
            TripBean tripBean = arrivalAndDepartureBean.getTrip();
            String headsign = tripBean.getTripHeadsign();
            String routeId = tripBean.getRoute().getId();
            String directionId = tripBean.getDirectionId();

            // FIXME: most common headsign?
            routeIdToHeadsign.put(routeId, headsign);
            headsignToDirectionId.put(headsign, directionId);

            // add service alerts to our list of service alerts for all routes at this stop
            // and check if route is on detour
            if (arrivalAndDepartureBean.getSituations() != null) {
                for (SituationBean situationBean : arrivalAndDepartureBean.getSituations()) {
                    NaturalLanguageStringBean serviceAlert = serviceAlertIdsToServiceAlerts
                            .get(situationBean.getId());

                    if (serviceAlert == null)
                        serviceAlertIdsToServiceAlerts.put(situationBean.getId(), situationBean.getDescription());

                    String miscelleanousReason = situationBean.getMiscellaneousReason();
                    if (miscelleanousReason != null && miscelleanousReason.equals("detour")) {
                        routeIdToDetouredFlag.put(routeId, true);
                        break;
                    }
                }
            }
        }

        // ...since this is data used for display of realtime data only, we use the filtered bean list here:
        List<ArrivalAndDepartureBean> filteredArrivalsAndDepartures = adbeanFilter.filter(arrivalsAndDepartures);
        for (ArrivalAndDepartureBean arrivalAndDepartureBean : filteredArrivalsAndDepartures) {
            TripBean tripBean = arrivalAndDepartureBean.getTrip();
            TripStatusBean tripStatusBean = arrivalAndDepartureBean.getTripStatus();
            String routeId = tripBean.getRoute().getId();

            // hide deviated vehicles from mobile web + sms interfaces (row 4)
            Boolean routeIsOnDetour = routeIdToDetouredFlag.get(routeId);

            if (m == Mode.MOBILE_WEB || m == Mode.SMS) {
                String status = tripStatusBean.getStatus();
                if ((status != null && status.toLowerCase().compareTo("deviated") == 0)
                        && (routeIsOnDetour == null || routeIsOnDetour == false)) {
                    continue;
                }
            }

            double distanceFromStopInMeters = arrivalAndDepartureBean.getDistanceFromStop();
            int distanceFromStopInFeet = (int) this.metersToFeet(distanceFromStopInMeters);
            int numberOfStopsAway = arrivalAndDepartureBean.getNumberOfStopsAway();

            if (debug)
                System.out.println("   +++ ADDING TO ARRIVAL LIST");

            DistanceAway distanceAway = new DistanceAway(numberOfStopsAway, distanceFromStopInFeet,
                    new Date(arrivalAndDepartureBean.getTripStatus().getLastUpdateTime()), m,
                    config.getStaleDataTimeout(), FormattingContext.STOP, tripStatusBean);

            List<DistanceAway> distanceAways = routeIdToDistanceAways.get(routeId);

            if (distanceAways == null) {
                distanceAways = new ArrayList<DistanceAway>();
                routeIdToDistanceAways.put(routeId, distanceAways);
            }

            distanceAways.add(distanceAway);
        } // for arrivalanddeparture beans

        /*
         * Create AvailableRoute objects for each route at the given stop. Bring in
         * DistanceAway objects, if available, from above.
         */
        List<RouteBean> routes = stopBean.getRoutes();

        for (RouteBean routeBean : routes) {
            String routeId = routeBean.getId();
            String shortName = routeBean.getShortName();
            String longName = routeBean.getLongName();
            String headsign = routeIdToHeadsign.get(routeId);

            if (headsign == null)
                continue;

            List<DistanceAway> distanceAways = routeIdToDistanceAways.get(routeId);

            if (distanceAways == null)
                distanceAways = Collections.emptyList();

            Collections.sort(distanceAways);

            RouteItem availableRoute = new RouteItem(shortName, longName, headsign,
                    headsignToDirectionId.get(headsign), distanceAways);
            availableRoutes.add(availableRoute);
        }

        // Make list of service alerts from our list of unique alerts that apply to routes at this stop
        List<NaturalLanguageStringBean> serviceAlerts = null;
        if (serviceAlertIdsToServiceAlerts.isEmpty()) {
            serviceAlerts = Collections.emptyList();
        } else {
            serviceAlerts = new ArrayList<NaturalLanguageStringBean>(serviceAlertIdsToServiceAlerts.values());
        }

        StopSearchResult stopSearchResult = new StopSearchResult(stopId, stopName, latLng, stopDirection,
                availableRoutes, serviceAlerts);

        return stopSearchResult;
    }

    private double metersToFeet(double meters) {
        double feetInMeters = 3.28083989501312;
        return meters * feetInMeters;
    }

    private SearchQueryBean makeSearchQuery(String q) {
        return makeSearchQuery(q, serviceArea.getServiceArea());
    }

    private SearchQueryBean makeSearchQuery(CoordinateBounds bounds) {
        return makeSearchQuery(null, bounds);
    }

    private SearchQueryBean makeSearchQuery(String q, CoordinateBounds bounds) {
        SearchQueryBean queryBean = new SearchQueryBean();
        queryBean.setType(SearchQueryBean.EQueryType.BOUNDS_OR_CLOSEST);
        queryBean.setBounds(bounds);
        queryBean.setMaxCount(5);
        if (q != null)
            queryBean.setQuery(q);
        return queryBean;
    }

    private List<SearchResult> sortSearchResults(List<SearchResult> searchResults) {
        Collections.sort(searchResults, NycSearchServiceImpl.searchResultComparator);
        return searchResults;
    }

    public static class StopBeanComparator implements Comparator<StopBean> {

        private final Map<String, Double> stopIdToDistances;

        public StopBeanComparator(Map<String, Double> stopIdToDistances) {
            this.stopIdToDistances = stopIdToDistances;
        }

        @Override
        public int compare(StopBean o1, StopBean o2) {
            Double d1 = stopIdToDistances.get(o1.getId());
            Double d2 = stopIdToDistances.get(o2.getId());
            if (d1 == null)
                d1 = Double.valueOf(0);
            if (d2 == null)
                d2 = Double.valueOf(0);
            return d1.compareTo(d2);
        }

    }

    public static class SearchResultComparator implements Comparator<SearchResult> {

        @Override
        public int compare(SearchResult o1, SearchResult o2) {
            if ((o1 instanceof RouteSearchResult) && (o2 instanceof StopSearchResult))
                return -1;
            else if ((o1 instanceof StopSearchResult) && (o2 instanceof RouteSearchResult))
                return 1;
            else if ((o1 instanceof RouteSearchResult) && (o2 instanceof RouteSearchResult))
                return ((RouteSearchResult) o1).getName().compareTo(((RouteSearchResult) o2).getName());
            else if ((o1 instanceof StopSearchResult) && (o2 instanceof StopSearchResult))
                return ((StopSearchResult) o1).getName().compareTo(((StopSearchResult) o2).getName());
            else
                throw new IllegalStateException("Unknown search result types");
        }
    }
}