/*
BostonBusMap
Copyright (C) 2009 George Schneeloch
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.schneeloch.bostonbusmap_library.data;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import android.content.OperationApplicationException;
import android.os.RemoteException;
import android.util.Log;
import com.google.common.collect.Maps;
import com.schneeloch.bostonbusmap_library.provider.IDatabaseAgent;
import com.schneeloch.bostonbusmap_library.transit.ITransitSystem;
import com.schneeloch.bostonbusmap_library.transit.TransitSource;
import com.schneeloch.bostonbusmap_library.util.Constants;
import com.schneeloch.bostonbusmap_library.util.FeedException;
import com.schneeloch.bostonbusmap_library.util.LogUtil;
public final class Locations
{
/**
* A mapping of the bus number to bus location
*/
private final VehicleLocations busMapping = new VehicleLocations();
/**
* A mapping of a route id to a RouteConfig object.
*/
private final RoutePool routeMapping;
private final Directions directions;
private long lastUpdateTime = 0;
private Selection mutableSelection;
private final ITransitSystem transitSystem;
public Locations(IDatabaseAgent databaseAgent,
ITransitSystem transitSystem, Selection selection)
{
this.transitSystem = transitSystem;
routeMapping = new RoutePool(databaseAgent, transitSystem);
directions = new Directions(databaseAgent);
mutableSelection = selection;
}
public String getRouteTitle(String key)
{
return transitSystem.getRouteKeysToTitles().getTitle(key);
}
public int getRouteAsIndex(String key) {
return transitSystem.getRouteKeysToTitles().getIndexForTag(key);
}
/**
* Update the bus locations based on data from the XML feed
*
* @throws SAXException
* @throws IOException
* @throws ParserConfigurationException
* @throws FactoryConfigurationError
* @throws OperationApplicationException
* @throws RemoteException
* @throws FeedException
*/
public void refresh(IDatabaseAgent databaseAgent, Selection selection,
double centerLatitude, double centerLongitude,
boolean showRoute, Runnable refreshRunnable) throws SAXException, IOException,
ParserConfigurationException, FactoryConfigurationError, RemoteException, OperationApplicationException
{
final int maxStops = 15;
//see if route overlays need to be downloaded
String routeToUpdate = selection.getRoute();
RouteConfig routeConfig = routeMapping.get(routeToUpdate);
transitSystem.startObtainAlerts(databaseAgent, refreshRunnable);
Selection.Mode mode = selection.getMode();
if (mode == Selection.Mode.BUS_PREDICTIONS_ALL ||
mode == Selection.Mode.BUS_PREDICTIONS_ONE ||
mode == Selection.Mode.BUS_PREDICTIONS_STAR) {
routeMapping.clearRecentlyUpdated();
}
if (mode == Selection.Mode.BUS_PREDICTIONS_STAR ||
mode == Selection.Mode.VEHICLE_LOCATIONS_ALL ||
mode == Selection.Mode.BUS_PREDICTIONS_ALL)
{
//get data from many transit sources
transitSystem.refreshData(routeConfig, selection, maxStops, centerLatitude,
centerLongitude, busMapping, routeMapping, directions, this);
}
else
{
TransitSource transitSource = routeConfig.getTransitSource();
transitSource.refreshData(routeConfig, selection, maxStops,
centerLatitude, centerLongitude, busMapping,
routeMapping, directions, this);
}
}
/**
* Given a sorted list of locations
* @param locations Sorted list of locations
* @param maxLocations Returned list will have this many groups at most
* @return
*/
public static ImmutableList<ImmutableList<Location>> groupLocations(
List<Location> locations, int maxLocations) {
int builderCount = 0;
List<List<Location>> groupsBuilder = Lists.newArrayList();
Map<GroupKey, List<Location>> groupMap = Maps.newHashMap();
Location lastLocation = null;
for (Location location : locations) {
GroupKey groupKey = location.makeGroupKey();
if (lastLocation == null) {
List<Location> builder = Lists.newArrayList();
builder.add(location);
groupMap.put(groupKey, builder);
groupsBuilder.add(builder);
builderCount++;
}
else if (groupMap.containsKey(groupKey)) {
groupMap.get(groupKey).add(location);
}
else {
if (builderCount == maxLocations) {
// we're over our count
break;
}
List<Location> builder = Lists.newArrayList();
builder.add(location);
groupMap.put(groupKey, builder);
groupsBuilder.add(builder);
builderCount++;
}
lastLocation = location;
}
ImmutableList.Builder<ImmutableList<Location>> builder = ImmutableList.builder();
for (List<Location> locationBuilder : groupsBuilder) {
// HACK: This sort only establishes an absolute ordering. The code currently relies on the first item
// in the list being special so we have to make sure that the first item is consistent.
Collections.sort(locationBuilder, new LocationComparator(0, 0));
builder.add(ImmutableList.copyOf(locationBuilder));
}
return builder.build();
}
/**
* Return the 20 (or whatever maxLocations is) closest buses to the center
*
* NOTE: this is run in the UI thread, so be speedy
*
* @param maxLocations
* @return
* @throws IOException
*/
public ImmutableList<ImmutableList<Location>> getLocations(int maxLocations, double centerLatitude, double centerLongitude,
boolean doShowUnpredictable, Selection selection) throws IOException {
ArrayList<Location> newLocations = Lists.newArrayListWithCapacity(maxLocations);
String selectedRoute = selection.getRoute();
Selection.Mode mode = selection.getMode();
if (mode == Selection.Mode.VEHICLE_LOCATIONS_ALL ||
mode == Selection.Mode.VEHICLE_LOCATIONS_ONE)
{
if (doShowUnpredictable == false)
{
for (BusLocation busLocation : busMapping.values())
{
if (mode == Selection.Mode.VEHICLE_LOCATIONS_ONE) {
if (selectedRoute != null && selectedRoute.equals(busLocation.getRouteId())) {
newLocations.add(busLocation);
}
} else if (mode == Selection.Mode.VEHICLE_LOCATIONS_ALL) {
newLocations.add(busLocation);
} else {
throw new RuntimeException("selectedBusPredictions is invalid");
}
}
}
else
{
if (mode == Selection.Mode.VEHICLE_LOCATIONS_ALL)
{
newLocations.addAll(busMapping.values());
}
else
{
for (BusLocation location : busMapping.values())
{
if (selectedRoute != null && selectedRoute.equals(location.getRouteId()))
{
newLocations.add(location);
}
}
}
}
}
else if (mode == Selection.Mode.BUS_PREDICTIONS_ONE)
{
RouteConfig routeConfig = routeMapping.get(selection.getRoute());
if (routeConfig != null)
{
newLocations.addAll(routeConfig.getStops());
}
}
else if (mode == Selection.Mode.BUS_PREDICTIONS_ALL)
{
Collection<StopLocation> stops = routeMapping.getClosestStops(centerLatitude, centerLongitude, maxLocations);
for (StopLocation stop : stops)
{
if (stop.supportsBusPredictionsAllMode())
{
newLocations.add(stop);
}
}
}
else if (mode == Selection.Mode.BUS_PREDICTIONS_STAR)
{
for (StopLocation stopLocation : routeMapping.getFavoriteStops())
{
newLocations.add(stopLocation);
}
}
if (mode == Selection.Mode.BUS_PREDICTIONS_ALL ||
mode == Selection.Mode.BUS_PREDICTIONS_ONE ||
mode == Selection.Mode.BUS_PREDICTIONS_STAR) {
newLocations.addAll(routeMapping.getIntersections());
}
if (maxLocations > newLocations.size())
{
maxLocations = newLocations.size();
}
Collections.sort(newLocations, new LocationComparator(centerLatitude, centerLongitude));
return groupLocations(newLocations, maxLocations);
}
public Path[] getPaths(String route) {
try
{
RouteConfig routeConfig = routeMapping.get(route);
if (routeConfig != null)
{
return routeConfig.getPaths();
}
else
{
return RouteConfig.nullPaths;
}
}
catch (IOException e) {
LogUtil.e(e);
return RouteConfig.nullPaths;
}
}
public Favorite toggleFavorite(StopLocation location) throws RemoteException
{
Favorite isFavorite = routeMapping.isFavorite(location);
Favorite isNotFavorite = isFavorite == Favorite.IsFavorite ? Favorite.IsNotFavorite : Favorite.IsFavorite;
return routeMapping.setFavorite(location, isNotFavorite);
}
public boolean addIntersection(IntersectionLocation.Builder builder) {
return routeMapping.addIntersection(builder);
}
public ImmutableList<StopLocation> getCurrentFavorites()
{
return routeMapping.getFavoriteStops();
}
public void setLastUpdateTime(long lastUpdateTime) {
this.lastUpdateTime = lastUpdateTime;
}
public long getLastUpdateTime()
{
return lastUpdateTime;
}
public ConcurrentMap<String, StopLocation> getAllStopsAtStop(String stopTag)
{
return routeMapping.getAllStopTagsAtLocation(stopTag);
}
public StopLocation setSelectedStop(String route, String stopTag)
{
try
{
RouteConfig routeConfig = routeMapping.get(route);
if (routeConfig != null)
{
StopLocation stopLocation = routeConfig.getStop(stopTag);
Selection newSelection = new Selection(Selection.Mode.BUS_PREDICTIONS_ONE, route);
mutableSelection = newSelection;
return stopLocation;
}
else
{
Log.e("BostonBusMap", "bizarre... route doesn't exist: " + (route != null ? route : ""));
}
}
catch (IOException e)
{
LogUtil.e(e);
}
return null;
}
/**
* Do not modify return value!
* @return
*/
public String makeNewIntersectionName() {
int count = 1;
while (true) {
String name = "Place " + count;
if (routeMapping.hasIntersection(name) == false) {
return name;
}
count++;
}
}
public Selection getSelection() {
return mutableSelection;
}
public void setSelection(Selection selection) {
mutableSelection = selection;
}
public RouteConfig getRoute(String route) throws IOException {
return routeMapping.get(route);
}
public RouteTitles getRouteTitles() {
return transitSystem.getRouteKeysToTitles();
}
public void removeIntersection(String name) {
routeMapping.removeIntersection(name);
}
public void editIntersection(String oldName, String newName) {
routeMapping.editIntersectionName(oldName, newName);
}
public boolean containsIntersection(String intersection) {
return routeMapping.hasIntersection(intersection);
}
public IntersectionLocation getIntersection(String name) {
return routeMapping.getIntersection(name);
}
public Collection<String> getIntersectionNames() {
return routeMapping.getIntersectionNames();
}
public ITransitSystem getTransitSystem() {
return routeMapping.getTransitSystem();
}
public void replaceStops(String route, ImmutableMap<String, StopLocation> values) throws IOException {
routeMapping.replaceStops(route, values);
}
public VehicleLocations getVehicleLocations() {
return busMapping;
}
}