/**
* DO NOT MANUALLY EDIT THIS FILE
*
* This file is auto-generated by the build.gradle copyMapsApiV2Classes task based on the map
* classes in the Google build flavor (src/google/java/org/onebusaway/android/map/googlemapsv2).
* If you want to change something in this file, please edit the sources in
* src/google/java/org/onebusaway/android/map/googlemapsv2 and rebuild the project. Gradle will
* detect that the files in the the Google build flavor changed and will re-generate the Amazon
* build flavor map classes. See Github Issues #158 and #254 for details.
*/
/*
* Copyright (C) 2014 University of South Florida (sjbarbeau@gmail.com)
*
* 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.android.map.googlemapsv2;
import com.amazon.geo.mapsv2.AmazonMap;
import com.amazon.geo.mapsv2.model.BitmapDescriptor;
import com.amazon.geo.mapsv2.model.BitmapDescriptorFactory;
import com.amazon.geo.mapsv2.model.Marker;
import com.amazon.geo.mapsv2.model.MarkerOptions;
import org.onebusaway.android.R;
import org.onebusaway.android.app.Application;
import org.onebusaway.android.io.elements.ObaRoute;
import org.onebusaway.android.io.elements.ObaTrip;
import org.onebusaway.android.io.elements.ObaTripDetails;
import org.onebusaway.android.io.elements.ObaTripStatus;
import org.onebusaway.android.io.request.ObaTripsForRouteResponse;
import org.onebusaway.android.ui.TripDetailsActivity;
import org.onebusaway.android.ui.TripDetailsListFragment;
import org.onebusaway.android.util.ArrivalInfoUtils;
import org.onebusaway.android.util.MathUtils;
import org.onebusaway.android.util.UIUtils;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.GradientDrawable;
import android.location.Location;
import android.os.Handler;
import android.support.v4.content.ContextCompat;
import android.support.v4.util.LruCache;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* A map overlay that shows vehicle positions on the map
*/
public class VehicleOverlay implements AmazonMap.OnInfoWindowClickListener {
interface Controller {
String getFocusedStopId();
}
private static final String TAG = "VehicleOverlay";
private AmazonMap mMap;
private MarkerData mMarkerData;
private final Activity mActivity;
private ObaTripsForRouteResponse mLastResponse;
private CustomInfoWindowAdapter mCustomInfoWindowAdapter;
private Controller mController;
private static final int NORTH = 0; // directions are clockwise, consistent with MathUtils class
private static final int NORTH_EAST = 1;
private static final int EAST = 2;
private static final int SOUTH_EAST = 3;
private static final int SOUTH = 4;
private static final int SOUTH_WEST = 5;
private static final int WEST = 6;
private static final int NORTH_WEST = 7;
private static final int NO_DIRECTION = 8;
private static final int NUM_DIRECTIONS = 9; // 8 directions + undirected mVehicles
private static final int DEFAULT_VEHICLE_TYPE = ObaRoute.TYPE_BUS; // fall back on bus
// Vehicle type (if available) -> icon set
private static LruCache<String, Bitmap> mVehicleUncoloredIcons;
private static LruCache<String, Bitmap> mVehicleColoredIconCache;
// Colored versions of vehicle_icons
/**
* If a vehicle moves less than this distance (in meters), it will be animated, otherwise it
* will just disappear and then re-appear
*/
private static final double MAX_VEHICLE_ANIMATION_DISTANCE = 400;
/**
* z-index used to show vehicle markers on top of stop markers (default marker z-index is 0)
*/
private static final float VEHICLE_MARKER_Z_INDEX = 1;
public VehicleOverlay(Activity activity, AmazonMap map) {
mActivity = activity;
mMap = map;
loadIcons();
mMap.setOnInfoWindowClickListener(this);
// Set adapter for custom info window that appears when tapping on vehicle markers
mCustomInfoWindowAdapter = new CustomInfoWindowAdapter(mActivity);
mMap.setInfoWindowAdapter(mCustomInfoWindowAdapter);
}
public void setController(Controller controller) {
mController = controller;
}
/**
* Updates vehicles for the provided routeIds from the status info from the given
* ObaTripsForRouteResponse
*
* @param routeIds routeIds for which to add vehicle markers to the map. If a vehicle is
* running a route that is not contained in this list, the vehicle won't be
* shown on the map.
* @param response response that contains the real-time status info
*/
public void updateVehicles(HashSet<String> routeIds, ObaTripsForRouteResponse response) {
// Make sure that the MarkerData has been initialized
setupMarkerData();
// Cache the response, so when a marker is tapped we can look up route names from routeIds, etc.
mLastResponse = response;
// Show the markers on the map
mMarkerData.populate(routeIds, response);
}
public synchronized int size() {
if (mMarkerData != null) {
return mMarkerData.size();
} else {
return 0;
}
}
/**
* Clears any vehicle markers from the map
*/
public synchronized void clear() {
if (mMarkerData != null) {
mMarkerData.clear();
mMarkerData = null;
}
if (mCustomInfoWindowAdapter != null) {
mCustomInfoWindowAdapter.cancelUpdates();
}
}
/**
* Cache the core black template Bitmaps used for vehicle icons
*/
private static final void loadIcons() {
/**
* Cache for colored versions of the vehicle icons. Total possible number of entries is
* 9 directions * 4 color types (early, ontime, delayed, scheduled) = 36. In a test,
* the RouteMapController used around 15 bitmaps over a 30 min period for 4 vehicles on the
* map at 10 sec refresh rate. This can be more depending on the route configuration (if
* the route has lots of curves) and number of vehicles. To conserve memory, we'll set the
* max cache size at 15.
*/
final int MAX_CACHE_SIZE = 15;
if (mVehicleUncoloredIcons == null) {
mVehicleUncoloredIcons = new LruCache<>(MAX_CACHE_SIZE);
}
if (mVehicleColoredIconCache == null) {
mVehicleColoredIconCache = new LruCache<>(MAX_CACHE_SIZE);
}
}
/**
* Gets the icon, ready to color for the given direction and vehicle type
*
* @param halfWind an index between 0 and numHalfWinds-1 that can be used to retrieve
* the direction name for that heading (known as "boxing the compass", down to the half-wind
* level).
* @param vehicleType type as defined by GTFS spec. Acceptable values contained in OBARoute.TYPE_*
*
* @return the icon ready to color
*/
private static Bitmap getIcon(int halfWind, int vehicleType) {
if (!supportedVehicleType(vehicleType)) {
vehicleType = DEFAULT_VEHICLE_TYPE;
}
String cacheKey = String.format("%d %d", halfWind, vehicleType);
Bitmap b = mVehicleUncoloredIcons.get(cacheKey);
if (b == null) { // cache miss
switch (vehicleType) {
case ObaRoute.TYPE_BUS:
b = createBusIcon(halfWind);
break;
case ObaRoute.TYPE_FERRY:
b = createFerryIcon(halfWind);
break;
case ObaRoute.TYPE_TRAM:
b = createTramIcon(halfWind);
break;
case ObaRoute.TYPE_SUBWAY:
b = createSubwayIcon(halfWind);
break;
case ObaRoute.TYPE_RAIL:
b = createRailIcon(halfWind);
break;
// default: not needed, since supported vehicles are checked prior
}
}
mVehicleUncoloredIcons.put(cacheKey, b);
return b;
}
private static boolean supportedVehicleType(int vehicleType) {
return vehicleType == ObaRoute.TYPE_BUS ||
vehicleType == ObaRoute.TYPE_FERRY ||
vehicleType == ObaRoute.TYPE_TRAM ||
vehicleType == ObaRoute.TYPE_SUBWAY ||
vehicleType == ObaRoute.TYPE_RAIL;
}
/**
* Create the bus icon with the given direction arrows or without a direction arrow
* for direction of NO_DIRECTION. Color is black so they can be tinted later.
*
* @return vehicle icon bitmap with the arrow pointing the appropriate direction, or with
* no arrow for NO_DIRECTION
*/
private static Bitmap createBusIcon(int halfWind) {
Resources r = Application.get().getResources();
switch (halfWind) {
case NORTH:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_bus_smaller_north_inside);
case NORTH_EAST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_bus_smaller_north_east_inside);
case EAST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_bus_smaller_east_inside);
case SOUTH_EAST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_bus_smaller_south_east_inside);
case SOUTH:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_bus_smaller_south_inside);
case SOUTH_WEST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_bus_smaller_south_west_inside);
case WEST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_bus_smaller_west_inside);
case NORTH_WEST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_bus_smaller_north_west_inside);
default:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_bus_smaller_none_inside);
}
}
/**
* Create the tram icon with the given direction arrows or without a direction arrow
* for direction of NO_DIRECTION. Color is black so they can be tinted later.
*
* @return vehicle icon bitmap with the arrow pointing the appropriate direction, or with
* no arrow for NO_DIRECTION
*/
private static Bitmap createTramIcon(int halfWind) {
Resources r = Application.get().getResources();
switch (halfWind) {
case NORTH:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_tram_smaller_north_inside);
case NORTH_EAST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_tram_smaller_north_east_inside);
case EAST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_tram_smaller_east_inside);
case SOUTH_EAST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_tram_smaller_south_east_inside);
case SOUTH:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_tram_smaller_south_inside);
case SOUTH_WEST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_tram_smaller_south_west_inside);
case WEST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_tram_smaller_west_inside);
case NORTH_WEST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_tram_smaller_north_west_inside);
default:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_tram_smaller_none_inside);
}
}
/**
* Create the rail icon with the given direction arrows or without a direction arrow
* for direction of NO_DIRECTION. Color is black so they can be tinted later.
*
* @return vehicle icon bitmap with the arrow pointing the appropriate direction, or with
* no arrow for NO_DIRECTION
*/
private static Bitmap createRailIcon(int halfWind) {
Resources r = Application.get().getResources();
switch (halfWind) {
case NORTH:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_train_smaller_north_inside);
case NORTH_EAST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_train_smaller_north_east_inside);
case EAST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_train_smaller_east_inside);
case SOUTH_EAST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_train_smaller_south_east_inside);
case SOUTH:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_train_smaller_south_inside);
case SOUTH_WEST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_train_smaller_south_west_inside);
case WEST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_train_smaller_west_inside);
case NORTH_WEST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_train_smaller_north_west_inside);
default:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_train_smaller_none_inside);
}
}
/**
* Create the ferry icon with the given direction arrows or without a direction arrow
* for direction of NO_DIRECTION. Color is black so they can be tinted later.
*
* @return vehicle icon bitmap with the arrow pointing the appropriate direction, or with
* no arrow for NO_DIRECTION
*/
private static Bitmap createFerryIcon(int halfWind) {
Resources r = Application.get().getResources();
switch (halfWind) {
case NORTH:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_boat_smaller_north_inside);
case NORTH_EAST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_boat_smaller_north_east_inside);
case EAST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_boat_smaller_east_inside);
case SOUTH_EAST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_boat_smaller_south_east_inside);
case SOUTH:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_boat_smaller_south_inside);
case SOUTH_WEST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_boat_smaller_south_west_inside);
case WEST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_boat_smaller_west_inside);
case NORTH_WEST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_boat_smaller_north_west_inside);
default:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_boat_smaller_none_inside);
}
}
/**
* Create the subway icon with the given direction arrows or without a direction arrow
* for direction of NO_DIRECTION. Color is black so they can be tinted later.
*
* @return vehicle icon bitmap with the arrow pointing the appropriate direction, or with
* no arrow for NO_DIRECTION
*/
private static Bitmap createSubwayIcon(int halfWind) {
Resources r = Application.get().getResources();
switch (halfWind) {
case NORTH:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_subway_smaller_north_inside);
case NORTH_EAST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_subway_smaller_north_east_inside);
case EAST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_subway_smaller_east_inside);
case SOUTH_EAST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_subway_smaller_south_east_inside);
case SOUTH:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_subway_smaller_south_inside);
case SOUTH_WEST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_subway_smaller_south_west_inside);
case WEST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_subway_smaller_west_inside);
case NORTH_WEST:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_subway_smaller_north_west_inside);
default:
return BitmapFactory
.decodeResource(r, R.drawable.ic_marker_with_subway_smaller_none_inside);
}
}
/**
* Add a Bitmap for a colored vehicle icon to the cache
*
* @param key Key for the Bitmap to be added, created by createBitmapCacheKey(halfWind, colorResource)
* @param bitmap Bitmap to be added that is a colored version of the core black vehicle icons
*/
private void addBitmapToCache(String key, Bitmap bitmap) {
// Only add if its not already in the cache
if (getBitmapFromCache(key) == null) {
mVehicleColoredIconCache.put(key, bitmap);
}
}
/**
* Get a Bitmap for a colored vehicle icon from the cache
*
* @param key Key for the Bitmap, created by createBitmapCacheKey(halfWind, colorResource)
* @return Bitmap that is a colored version of the core black vehicle icons corresponding to the given key
*/
private Bitmap getBitmapFromCache(String key) {
return mVehicleColoredIconCache.get(key);
}
/**
* Creates a key for the vehicle colored icons cache, based on the halfWind (direction) and
* colorResource
*
* @param vehicleType The type of vehicle based on the GTFS value
*
* @param halfWind an index between 0 and numHalfWinds-1 that can be used to retrieve
* the direction name for that heading (known as "boxing the compass", down to the half-wind
* level).
* @param colorResource the color resource ID for the schedule deviation
* @return a String key for this direction and color vehicle bitmap icon
*/
private String createBitmapCacheKey(int vehicleType, int halfWind, int colorResource) {
if (!supportedVehicleType(vehicleType)) {
vehicleType = DEFAULT_VEHICLE_TYPE;
}
return String.valueOf(vehicleType) + " " + String.valueOf(halfWind) + " " + String.valueOf(colorResource);
}
/**
* Get the bitmap, using the cache where possible
* @param vehicleType the vehicle type, as defined by the GTFS value
* @param colorResource color resource ID for schedule deviation
* @param halfWind the direction pointed for the icon
* @return The bitmap representing the vehicle type with the color and direction
*/
private Bitmap getBitmap(int vehicleType, int colorResource, int halfWind) {
int color = ContextCompat.getColor(mActivity, colorResource);
// Use tram icon for cablecar
if (vehicleType == ObaRoute.TYPE_CABLECAR) {
vehicleType = ObaRoute.TYPE_TRAM;
}
String key = createBitmapCacheKey(vehicleType, halfWind, colorResource);
Bitmap b = getBitmapFromCache(key);
if (b == null) {
// Cache miss - create Bitmap and add to cache
b = UIUtils.colorBitmap(getIcon(halfWind, vehicleType), color);
addBitmapToCache(key, b);
}
return b;
}
@Override
public void onInfoWindowClick(Marker marker) {
// Stop any callbacks to refresh the vehicle marker popup balloons
mCustomInfoWindowAdapter.cancelUpdates();
// Show trip details screen for the vehicle associated with this marker
ObaTripStatus status = mMarkerData.getStatusFromMarker(marker);
if (mController != null && mController.getFocusedStopId() != null) {
TripDetailsActivity.start(mActivity, status.getActiveTripId(),
mController.getFocusedStopId(), TripDetailsListFragment.SCROLL_MODE_VEHICLE);
} else {
TripDetailsActivity.start(mActivity, status.getActiveTripId(),
TripDetailsListFragment.SCROLL_MODE_VEHICLE);
}
}
private void setupMarkerData() {
if (mMarkerData == null) {
mMarkerData = new MarkerData();
}
}
/**
* Data structures to track what vehicles are currently shown on the map
*/
class MarkerData {
/**
* A cached set of vehicles that are currently shown on the map. Since onMarkerClick()
* provides a marker, we need a mapping of that marker to a vehicle/trip.
* Marker that represents a vehicle is the key, and value is the status for the vehicle.
*/
private HashMap<Marker, ObaTripStatus> mVehicles;
/**
* A cached set of vehicle markers currently shown on the map. This is needed to
* add/remove markers from the map. activeTripId is the key - we can't use vehicleId
* because we want to show an interpolated position (based on schedule data) for trips
* without real-time data, and those statuses do not have vehicleIds associated with them,
* but do have activeTripIds.
*/
private HashMap<String, Marker> mVehicleMarkers;
private static final int INITIAL_HASHMAP_SIZE = 5;
MarkerData() {
mVehicles = new HashMap<>(INITIAL_HASHMAP_SIZE);
mVehicleMarkers = new HashMap<>(INITIAL_HASHMAP_SIZE);
}
/**
* Updates markers for the provided routeIds from the status info from the given
* ObaTripsForRouteResponse
*
* @param routeIds markers representing real-time positions for the provided routeIds will
* be
* added to the map. The response may contain status info for other routes
* as well - we'll only show markers for the routeIds in this HashSet.
* @param response response that contains the real-time status info
*/
synchronized void populate(HashSet<String> routeIds, ObaTripsForRouteResponse response) {
int added = 0;
int updated = 0;
ObaTripDetails[] trips = response.getTrips();
// Keep track of the activeTripIds that should be shown on the map, so we don't need
// to iterate again later for this same info
HashSet<String> activeTripIds = new HashSet<>();
// Add or move markers for vehicles included in response
for (ObaTripDetails trip : trips) {
ObaTripStatus status = trip.getStatus();
if (status != null) {
// Check if this vehicle is running a route we're interested in
String activeRoute = response.getTrip(status.getActiveTripId()).getRouteId();
if (routeIds.contains(activeRoute)) {
Location l = status.getLastKnownLocation();
boolean isRealtime = true;
if (l == null) {
// Use a potentially extrapolated position instead of real last known location
l = status.getPosition();
isRealtime = false;
}
if (!status.isPredicted()) {
isRealtime = false;
}
Marker m = mVehicleMarkers.get(status.getActiveTripId());
if (m == null) {
// New activeTripId
addMarkerToMap(l, isRealtime, status, response);
added++;
} else {
updateMarker(m, l, isRealtime, status, response);
updated++;
}
activeTripIds.add(status.getActiveTripId());
}
}
}
// Remove markers for any previously added tripIds that aren't in the current response
int removed = removeInactiveMarkers(activeTripIds);
Log.d(TAG,
"Added " + added + ", updated " + updated + ", removed " + removed
+ ", total vehicle markers = "
+ mVehicleMarkers.size());
Log.d(TAG, "Vehicle LRU cache size=" + mVehicleColoredIconCache.size() + ", hits="
+ mVehicleColoredIconCache.hitCount() + ", misses=" + mVehicleColoredIconCache
.missCount());
Log.d(TAG, String.format("Raw uncolored vehicle LRU cache size=%d, hits=%d, misses=%d",
mVehicleUncoloredIcons.size(),
mVehicleUncoloredIcons.hitCount(),
mVehicleUncoloredIcons.missCount()));
}
/**
* Places a marker on the map for this vehicle, and adds it to our marker HashMap
*
* @param l Location to add the marker at
* @param isRealtime true if the marker shown indicate real-time info, false if it should indicate schedule
* @param status the vehicles status to add to the map
* @param response the response which contained the provided status
*/
private void addMarkerToMap(Location l, boolean isRealtime, ObaTripStatus status,
ObaTripsForRouteResponse response) {
Marker m = mMap.addMarker(new MarkerOptions()
.position(MapHelpV2.makeLatLng(l))
.title(status.getVehicleId())
.icon(getVehicleIcon(isRealtime, status, response))
);
ProprietaryMapHelpV2.setZIndex(m, VEHICLE_MARKER_Z_INDEX);
mVehicleMarkers.put(status.getActiveTripId(), m);
mVehicles.put(m, status);
}
/**
* Update an existing marker on the map with the current vehicle status
*
* @param m Marker to update
* @param l Location to add the marker at
* @param isRealtime true if the marker shown indicate real-time info, false if it should
* indicate schedule
* @param status real-time status of the vehicle
* @param response response containing the provided status
*/
private void updateMarker(Marker m, Location l, boolean isRealtime, ObaTripStatus status,
ObaTripsForRouteResponse response) {
boolean showInfo = m.isInfoWindowShown();
m.setIcon(getVehicleIcon(isRealtime, status, response));
// Update Hashmap with newest status - needed to show info when tapping on marker
mVehicles.put(m, status);
// Update vehicle position
Location markerLoc = MapHelpV2.makeLocation(m.getPosition());
// If its a small distance, animate the movement
if (l.distanceTo(markerLoc) < MAX_VEHICLE_ANIMATION_DISTANCE) {
AnimationUtil.animateMarkerTo(m, MapHelpV2.makeLatLng(l));
} else {
// Just snap the marker to the new location - large animations look weird
m.setPosition(MapHelpV2.makeLatLng(l));
}
// If the info window was shown, make sure its open (changing the icon could have closed it)
if (showInfo) {
m.showInfoWindow();
}
}
/**
* Removes any markers that don't currently represent active vehicles running a route
*
* @param activeTripIds a set of active tripIds that are currently running the routes. Any
* markers for tripIds that aren't in this set will be removed
* from the map.
* @return the number of removed markers
*/
private int removeInactiveMarkers(HashSet<String> activeTripIds) {
int removed = 0;
// Loop using an Iterator, since per Oracle Iterator.remove() is the only safe way
// to remove an item from a Collection during iteration:
// http://docs.oracle.com/javase/tutorial/collections/interfaces/collection.html
try {
Iterator<Map.Entry<String, Marker>> iterator = mVehicleMarkers.entrySet()
.iterator();
while (iterator.hasNext()) {
Map.Entry<String, Marker> entry = iterator.next();
String tripId = entry.getKey();
Marker m = entry.getValue();
if (!activeTripIds.contains(tripId)) {
// Remove the marker from map and data structures
entry.getValue().remove();
mVehicles.remove(m);
iterator.remove();
removed++;
}
}
} catch (UnsupportedOperationException e) {
Log.w(TAG, "Problem removing vehicle from HashMap using iterator: " + e);
//The platform apparently didn't like the "efficient" way to do this, so we'll just
//loop through a copy and remove what we don't want from the original
HashMap<String, Marker> copy = new HashMap<>(mVehicleMarkers);
for (Map.Entry<String, Marker> entry : copy.entrySet()) {
String tripId = entry.getKey();
Marker m = entry.getValue();
if (!activeTripIds.contains(tripId)) {
// Remove the marker from map and data structures
entry.getValue().remove();
mVehicles.remove(m);
mVehicleMarkers.remove(tripId);
removed++;
}
}
}
return removed;
}
/**
* Returns an icon for the vehicle that should be shown on the map
*
* @param isRealtime true if the marker shown indicate real-time info, false if it should
* indicate schedule
* @param status the vehicles status to add to the map
* @param response the response which contained the provided status
* @return an icon for the vehicle that should be shown on the map
*/
private BitmapDescriptor getVehicleIcon(boolean isRealtime, ObaTripStatus status,
ObaTripsForRouteResponse response) {
String routeId = response.getTrip(status.getActiveTripId()).getRouteId();
ObaRoute route = response.getRoute(routeId);
int vehicleType = route.getType();
int colorResource;
if (isRealtime) {
long deviationMin = TimeUnit.SECONDS.toMinutes(status.getScheduleDeviation());
colorResource = ArrivalInfoUtils.computeColorFromDeviation(deviationMin);
} else {
colorResource = R.color.stop_info_scheduled_time;
}
double direction = MathUtils.toDirection(status.getOrientation());
int halfWind = MathUtils.getHalfWindIndex((float) direction, NUM_DIRECTIONS - 1);
//Log.d(TAG, "VehicleId=" + status.getVehicleId() + ", orientation= " + status.getOrientation() + ", direction=" + direction + ", halfWind= " + halfWind + ", deviation=" + status.getScheduleDeviation());
Bitmap b = getBitmap(vehicleType, colorResource, halfWind);
return BitmapDescriptorFactory.fromBitmap(b);
}
synchronized ObaTripStatus getStatusFromMarker(Marker marker) {
return mVehicles.get(marker);
}
private void removeMarkersFromMap() {
for (Map.Entry<String, Marker> entry : mVehicleMarkers.entrySet()) {
entry.getValue().remove();
}
}
/**
* Clears any stop markers from the map
*/
synchronized void clear() {
if (mVehicleMarkers != null) {
// Clear all markers from the map
removeMarkersFromMap();
// Clear the data structures
mVehicleMarkers.clear();
mVehicleMarkers = null;
}
if (mVehicles != null) {
mVehicles.clear();
mVehicles = null;
}
}
synchronized int size() {
return mVehicleMarkers.size();
}
}
/**
* Returns true if there is real-time location information for the given status, false if there
* is not
*
* @param status The trip status information that includes location information
* @return true if there is real-time location information for the given status, false if there
* is not
*/
protected static boolean isLocationRealtime(ObaTripStatus status) {
boolean isRealtime = true;
Location l = status.getLastKnownLocation();
if (l == null) {
isRealtime = false;
}
if (!status.isPredicted()) {
isRealtime = false;
}
return isRealtime;
}
/**
* Adapter to show custom info windows when tapping on vehicle markers
*/
class CustomInfoWindowAdapter implements AmazonMap.InfoWindowAdapter {
private LayoutInflater mInflater;
private Context mContext;
private Marker mCurrentFocusVehicleMarker;
public CustomInfoWindowAdapter(Context context) {
this.mInflater = LayoutInflater.from(context);
this.mContext = context;
}
@Override
public View getInfoWindow(Marker marker) {
return null;
}
@Override
public View getInfoContents(Marker marker) {
if (mMarkerData == null) {
// Markers haven't been initialized yet - use default rendering
return null;
}
ObaTripStatus status = mMarkerData.getStatusFromMarker(marker);
if (status == null) {
// Marker that the user tapped on wasn't a vehicle - use default rendering
mCurrentFocusVehicleMarker = null;
return null;
}
mCurrentFocusVehicleMarker = marker;
View view = mInflater.inflate(R.layout.vehicle_info_window, null);
Resources r = mContext.getResources();
TextView routeView = (TextView) view.findViewById(R.id.route_and_destination);
TextView statusView = (TextView) view.findViewById(R.id.status);
TextView lastUpdatedView = (TextView) view.findViewById(R.id.last_updated);
ImageView moreView = (ImageView) view.findViewById(R.id.trip_more_info);
moreView.setColorFilter(r.getColor(R.color.switch_thumb_normal_material_dark));
// Get route/trip details
ObaTrip trip = mLastResponse.getTrip(status.getActiveTripId());
ObaRoute route = mLastResponse.getRoute(trip.getRouteId());
routeView.setText(UIUtils.getRouteDisplayName(route) + " " +
mContext.getString(R.string.trip_info_separator) + " " + UIUtils
.formatDisplayText(trip.getHeadsign()));
boolean isRealtime = isLocationRealtime(status);
statusView.setBackgroundResource(R.drawable.round_corners_style_b_status);
GradientDrawable d = (GradientDrawable) statusView.getBackground();
// Set padding on status view
int pSides = UIUtils.dpToPixels(mContext, 5);
int pTopBottom = UIUtils.dpToPixels(mContext, 2);
int statusColor;
if (isRealtime) {
long deviationMin = TimeUnit.SECONDS.toMinutes(status.getScheduleDeviation());
String statusString = ArrivalInfoUtils.computeArrivalLabelFromDelay(r, deviationMin);
statusView.setText(statusString);
statusColor = ArrivalInfoUtils.computeColorFromDeviation(deviationMin);
d.setColor(r.getColor(statusColor));
statusView.setPadding(pSides, pTopBottom, pSides, pTopBottom);
} else {
// Scheduled info
statusView.setText(r.getString(R.string.stop_info_scheduled));
statusColor = R.color.stop_info_scheduled_time;
d.setColor(r.getColor(statusColor));
lastUpdatedView.setText(r.getString(R.string.vehicle_last_updated_scheduled));
statusView.setPadding(pSides, pTopBottom, pSides, pTopBottom);
return view;
}
// Update last updated time (only shown for real-time info)
long now = System.currentTimeMillis();
long lastUpdateTime;
// Use the last updated time for the position itself, if its available
if (status.getLastLocationUpdateTime() != 0) {
lastUpdateTime = status.getLastLocationUpdateTime();
} else {
// Use the status timestamp for last updated time
lastUpdateTime = status.getLastUpdateTime();
}
long elapsedSec = TimeUnit.MILLISECONDS.toSeconds(now - lastUpdateTime);
long elapsedMin = TimeUnit.SECONDS.toMinutes(elapsedSec);
long secMod60 = elapsedSec % 60;
String lastUpdated;
if (elapsedSec < 60) {
lastUpdated = r.getString(R.string.vehicle_last_updated_sec,
elapsedSec);
} else {
lastUpdated = r.getString(R.string.vehicle_last_updated_min_and_sec,
elapsedMin, secMod60);
}
lastUpdatedView.setText(lastUpdated);
if (mMarkerRefreshHandler != null) {
mMarkerRefreshHandler.removeCallbacks(mMarkerRefresh);
mMarkerRefreshHandler.postDelayed(mMarkerRefresh, MARKER_REFRESH_PERIOD);
}
return view;
}
private final long MARKER_REFRESH_PERIOD = TimeUnit.SECONDS.toMillis(1);
private final Handler mMarkerRefreshHandler = new Handler();
private final Runnable mMarkerRefresh = new Runnable() {
public void run() {
if (mCurrentFocusVehicleMarker != null &&
mCurrentFocusVehicleMarker.isInfoWindowShown()) {
// Force an update of the marker balloon, so "last updated" time ticks up
mCurrentFocusVehicleMarker.showInfoWindow();
}
}
};
/**
* Cancels any pending updates of the marker balloon contents
*/
public void cancelUpdates() {
if (mMarkerRefreshHandler != null) {
mMarkerRefreshHandler.removeCallbacks(mMarkerRefresh);
}
}
}
}