/*
* Copyright (C) 2011-2014 Paul Watts (paulcwatts@gmail.com),
* University of South Florida (sjbarbeau@gmail.com), and individual contributors.
*
* 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;
import org.onebusaway.android.R;
import org.onebusaway.android.app.Application;
import org.onebusaway.android.io.ObaApi;
import org.onebusaway.android.io.elements.ObaRoute;
import org.onebusaway.android.io.elements.ObaStop;
import org.onebusaway.android.io.request.ObaStopsForRouteRequest;
import org.onebusaway.android.io.request.ObaStopsForRouteResponse;
import org.onebusaway.android.io.request.ObaTripsForRouteRequest;
import org.onebusaway.android.io.request.ObaTripsForRouteResponse;
import org.onebusaway.android.map.googlemapsv2.BaseMapFragment;
import org.onebusaway.android.util.LocationUtils;
import org.onebusaway.android.util.UIUtils;
import android.app.Activity;
import android.content.Context;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class RouteMapController implements MapModeController {
private static final String TAG = "RouteMapController";
private static final int ROUTES_LOADER = 5677;
private static final int VEHICLES_LOADER = 5678;
private final Callback mFragment;
private String mRouteId;
private boolean mZoomToRoute;
private boolean mZoomIncludeClosestVehicle;
private int mLineOverlayColor;
private RoutePopup mRoutePopup;
private int mShortAnimationDuration;
// In lieu of using an actual LoaderManager, which isn't
// available in SherlockMapActivity
private Loader<ObaStopsForRouteResponse> mRouteLoader;
private RouteLoaderListener mRouteLoaderListener;
private Loader<ObaTripsForRouteResponse> mVehiclesLoader;
private VehicleLoaderListener mVehicleLoaderListener;
private long mLastUpdatedTimeVehicles;
public RouteMapController(Callback callback) {
mFragment = callback;
mLineOverlayColor = mFragment.getActivity()
.getResources()
.getColor(R.color.route_line_color_default);
mShortAnimationDuration = mFragment.getActivity().getResources().getInteger(
android.R.integer.config_shortAnimTime);
mRoutePopup = new RoutePopup();
mRouteLoaderListener = new RouteLoaderListener();
mVehicleLoaderListener = new VehicleLoaderListener();
}
@Override
public void setState(Bundle args) {
if (args == null) {
throw new IllegalArgumentException("args cannot be null");
}
String routeId = args.getString(MapParams.ROUTE_ID);
// If the previous map zoom isn't the default, then zoom to that level as a start
float mapZoom = args.getFloat(MapParams.ZOOM, MapParams.DEFAULT_ZOOM);
if (mapZoom != MapParams.DEFAULT_ZOOM) {
mFragment.getMapView().setZoom(mapZoom);
}
mZoomToRoute = args.getBoolean(MapParams.ZOOM_TO_ROUTE, false);
mZoomIncludeClosestVehicle = args
.getBoolean(MapParams.ZOOM_INCLUDE_CLOSEST_VEHICLE, false);
if (!routeId.equals(mRouteId)) {
if (mRouteId != null) {
clearCurrentState();
}
// Set up the new route
mRouteId = routeId;
mRoutePopup.showLoading();
mFragment.showProgress(true);
//mFragment.getLoaderManager().restartLoader(ROUTES_LOADER, null, this);
mRouteLoader = mRouteLoaderListener.onCreateLoader(ROUTES_LOADER, null);
mRouteLoader.registerListener(0, mRouteLoaderListener);
mRouteLoader.startLoading();
mVehiclesLoader = mVehicleLoaderListener.onCreateLoader(VEHICLES_LOADER, null);
mVehiclesLoader.registerListener(0, mVehicleLoaderListener);
mVehiclesLoader.startLoading();
} else {
// We are returning to the route view with the route already set, so show the header
mRoutePopup.show();
}
}
/**
* Clears the current state of the controller, so a new route can be loaded
*/
private void clearCurrentState() {
// Stop loaders and refresh handler
mRouteLoader.stopLoading();
mRouteLoader.reset();
mVehiclesLoader.stopLoading();
mVehiclesLoader.reset();
mVehicleRefreshHandler.removeCallbacks(mVehicleRefresh);
// Clear the existing route and vehicle overlays
mFragment.getMapView().removeRouteOverlay();
mFragment.getMapView().removeVehicleOverlay();
// Clear the existing stop icons, but leave the currently focused stop
mFragment.getMapView().removeStopOverlay(false);
}
@Override
public String getMode() {
return MapParams.MODE_ROUTE;
}
@Override
public void destroy() {
mRoutePopup.hide();
mFragment.getMapView().removeRouteOverlay();
mVehicleRefreshHandler.removeCallbacks(mVehicleRefresh);
mFragment.getMapView().removeVehicleOverlay();
}
@Override
public void onPause() {
mVehicleRefreshHandler.removeCallbacks(mVehicleRefresh);
}
/**
* This is called when fm.beginTransaction().hide() or fm.beginTransaction().show() is called
*
* @param hidden True if the fragment is now hidden, false if it is not visible.
*/
@Override
public void onHidden(boolean hidden) {
// If the fragment is no longer visible, hide the route header - otherwise, show it
if (hidden) {
mRoutePopup.hide();
} else {
mRoutePopup.show();
}
}
@Override
public void onResume() {
// Make sure we schedule a future update for vehicles
mVehicleRefreshHandler.removeCallbacks(mVehicleRefresh);
if (mLastUpdatedTimeVehicles == 0) {
// We haven't loaded any vehicles yet - schedule the refresh for the full period and defer
// to the loader to reschedule when load is complete
mVehicleRefreshHandler.postDelayed(mVehicleRefresh, VEHICLE_REFRESH_PERIOD);
return;
}
long elapsedTimeMillis = TimeUnit.NANOSECONDS.toMillis(UIUtils.getCurrentTimeForComparison()
- mLastUpdatedTimeVehicles);
long refreshPeriod;
if (elapsedTimeMillis > VEHICLE_REFRESH_PERIOD) {
// Schedule an immediate update, if we're past the normal period after a load
refreshPeriod = 100;
} else {
// Schedule an update so a total of VEHICLE_REFRESH_PERIOD has elapsed since the last update
refreshPeriod = VEHICLE_REFRESH_PERIOD - elapsedTimeMillis;
}
mVehicleRefreshHandler.postDelayed(mVehicleRefresh, refreshPeriod);
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putString(MapParams.ROUTE_ID, mRouteId);
outState.putBoolean(MapParams.ZOOM_TO_ROUTE, mZoomToRoute);
outState.putBoolean(MapParams.ZOOM_INCLUDE_CLOSEST_VEHICLE, mZoomIncludeClosestVehicle);
Location centerLocation = mFragment.getMapView().getMapCenterAsLocation();
outState.putDouble(MapParams.CENTER_LAT, centerLocation.getLatitude());
outState.putDouble(MapParams.CENTER_LON, centerLocation.getLongitude());
outState.putFloat(MapParams.ZOOM, mFragment.getMapView().getZoomLevelAsFloat());
}
@Override
public void onViewStateRestored(Bundle savedInstanceState) {
if (savedInstanceState == null) {
return;
}
String stopId = savedInstanceState.getString(MapParams.STOP_ID);
if (stopId == null) {
// If there is no focused stop then restore the map state otherwise
// let the BaseMapFragment to handle map state with focused stop
float mapZoom = savedInstanceState.getFloat(MapParams.ZOOM, MapParams.DEFAULT_ZOOM);
if (mapZoom != MapParams.DEFAULT_ZOOM) {
mFragment.getMapView().setZoom(mapZoom);
}
double lat = savedInstanceState.getDouble(MapParams.CENTER_LAT);
double lon = savedInstanceState.getDouble(MapParams.CENTER_LON);
if (lat != 0.0d && lon != 0.0d) {
Location location = LocationUtils.makeLocation(lat, lon);
mFragment.getMapView().setMapCenter(location, false, false);
}
}
}
@Override
public void onLocation() {
// Don't care
}
@Override
public void onNoLocation() {
// Don't care
}
@Override
public void notifyMapChanged() {
// Don't care
}
//
// Map popup
//
private class RoutePopup {
private final Activity mActivity;
private final View mView;
private final TextView mRouteShortName;
private final TextView mRouteLongName;
private final TextView mAgencyName;
private final ProgressBar mProgressBar;
// Prevents completely hiding vehicle markers at top of route
private int VEHICLE_MARKER_PADDING;
RoutePopup() {
mActivity = mFragment.getActivity();
float paddingDp =
mActivity.getResources().getDimension(R.dimen.map_route_vehicle_markers_padding)
/ mActivity.getResources().getDisplayMetrics().density;
VEHICLE_MARKER_PADDING = UIUtils.dpToPixels(mActivity, paddingDp);
mView = mActivity.findViewById(R.id.route_info);
mFragment.getMapView()
.setPadding(null, mView.getHeight() + VEHICLE_MARKER_PADDING, null, null);
mRouteShortName = (TextView) mView.findViewById(R.id.short_name);
mRouteLongName = (TextView) mView.findViewById(R.id.long_name);
mAgencyName = (TextView) mView.findViewById(R.id.agency);
mProgressBar = (ProgressBar) mView.findViewById(R.id.route_info_loading_spinner);
// Make sure the cancel button is shown
View cancel = mView.findViewById(R.id.cancel_route_mode);
cancel.setVisibility(View.VISIBLE);
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ObaMapView obaMapView = mFragment.getMapView();
// We want to preserve the current zoom and center.
Bundle bundle = new Bundle();
bundle.putBoolean(MapParams.DO_N0T_CENTER_ON_LOCATION, true);
bundle.putFloat(MapParams.ZOOM, obaMapView.getZoomLevelAsFloat());
Location point = obaMapView.getMapCenterAsLocation();
bundle.putDouble(MapParams.CENTER_LAT, point.getLatitude());
bundle.putDouble(MapParams.CENTER_LON, point.getLongitude());
mFragment.setMapMode(MapParams.MODE_STOP, bundle);
}
});
}
void showLoading() {
mFragment.getMapView()
.setPadding(null, mView.getHeight() + VEHICLE_MARKER_PADDING, null, null);
UIUtils.hideViewWithoutAnimation(mRouteShortName);
UIUtils.hideViewWithoutAnimation(mRouteLongName);
UIUtils.showViewWithoutAnimation(mView);
UIUtils.showViewWithoutAnimation(mProgressBar);
}
/**
* Show the route header and populate it with the provided information
* @param route route information to show in the header
* @param agencyName agency name to show in the header
*/
void show(ObaRoute route, String agencyName) {
mRouteShortName.setText(UIUtils.formatDisplayText(UIUtils.getRouteDisplayName(route)));
mRouteLongName.setText(UIUtils.formatDisplayText(UIUtils.getRouteDescription(route)));
mAgencyName.setText(agencyName);
show();
}
/**
* Show the route header with the existing route information
*/
void show() {
UIUtils.hideViewWithAnimation(mProgressBar, mShortAnimationDuration);
UIUtils.showViewWithAnimation(mRouteShortName, mShortAnimationDuration);
UIUtils.showViewWithAnimation(mRouteLongName, mShortAnimationDuration);
UIUtils.showViewWithAnimation(mView, mShortAnimationDuration);
mFragment.getMapView()
.setPadding(null, mView.getHeight() + VEHICLE_MARKER_PADDING, null, null);
}
void hide() {
mFragment.getMapView().setPadding(null, 0, null, null);
UIUtils.hideViewWithAnimation(mView, mShortAnimationDuration);
}
}
private static final long VEHICLE_REFRESH_PERIOD = TimeUnit.SECONDS.toMillis(10);
private final Handler mVehicleRefreshHandler = new Handler();
private final Runnable mVehicleRefresh = new Runnable() {
public void run() {
refresh();
}
};
/**
* Refresh vehicle data from the OBA server
*/
private void refresh() {
if (mVehiclesLoader != null) {
mVehiclesLoader.onContentChanged();
}
}
//
// Loaders
//
private static class RoutesLoader extends AsyncTaskLoader<ObaStopsForRouteResponse> {
private final String mRouteId;
public RoutesLoader(Context context, String routeId) {
super(context);
mRouteId = routeId;
}
@Override
public ObaStopsForRouteResponse loadInBackground() {
if (Application.get().getCurrentRegion() == null &&
TextUtils.isEmpty(Application.get().getCustomApiUrl())) {
//We don't have region info or manually entered API to know what server to contact
Log.d(TAG, "Trying to load stops for route from server " +
"without OBA REST API endpoint, aborting...");
return null;
}
//Make OBA REST API call to the server and return result
return new ObaStopsForRouteRequest.Builder(getContext(), mRouteId)
.setIncludeShapes(true)
.build()
.call();
}
@Override
public void deliverResult(ObaStopsForRouteResponse data) {
//mResponse = data;
super.deliverResult(data);
}
@Override
public void onStartLoading() {
forceLoad();
}
}
class RouteLoaderListener implements LoaderManager.LoaderCallbacks<ObaStopsForRouteResponse>,
Loader.OnLoadCompleteListener<ObaStopsForRouteResponse> {
@Override
public Loader<ObaStopsForRouteResponse> onCreateLoader(int id,
Bundle args) {
return new RoutesLoader(mFragment.getActivity(), mRouteId);
}
@Override
public void onLoadFinished(Loader<ObaStopsForRouteResponse> loader,
ObaStopsForRouteResponse response) {
ObaMapView obaMapView = mFragment.getMapView();
if (response == null || response.getCode() != ObaApi.OBA_OK) {
BaseMapFragment.showMapError(response);
return;
}
ObaRoute route = response.getRoute(response.getRouteId());
mRoutePopup.show(route, response.getAgency(route.getAgencyId()).getName());
if (route.getColor() != null) {
mLineOverlayColor = route.getColor();
}
obaMapView.setRouteOverlay(mLineOverlayColor, response.getShapes());
// Set the stops for this route
List<ObaStop> stops = response.getStops();
mFragment.showStops(stops, response);
mFragment.showProgress(false);
if (mZoomToRoute) {
obaMapView.zoomToRoute();
mZoomToRoute = false;
}
//
// wait to zoom till we have the right response
obaMapView.postInvalidate();
}
@Override
public void onLoaderReset(Loader<ObaStopsForRouteResponse> loader) {
mFragment.getMapView().removeRouteOverlay();
mFragment.getMapView().removeVehicleOverlay();
}
@Override
public void onLoadComplete(Loader<ObaStopsForRouteResponse> loader,
ObaStopsForRouteResponse response) {
onLoadFinished(loader, response);
}
}
private static class VehiclesLoader extends AsyncTaskLoader<ObaTripsForRouteResponse> {
private final String mRouteId;
public VehiclesLoader(Context context, String routeId) {
super(context);
mRouteId = routeId;
}
@Override
public ObaTripsForRouteResponse loadInBackground() {
if (Application.get().getCurrentRegion() == null &&
TextUtils.isEmpty(Application.get().getCustomApiUrl())) {
//We don't have region info or manually entered API to know what server to contact
Log.d(TAG, "Trying to load trips (vehicles) for route from server " +
"without OBA REST API endpoint, aborting...");
return null;
}
//Make OBA REST API call to the server and return result
return new ObaTripsForRouteRequest.Builder(getContext(), mRouteId)
.setIncludeStatus(true)
.build()
.call();
}
@Override
public void deliverResult(ObaTripsForRouteResponse data) {
super.deliverResult(data);
}
@Override
public void onStartLoading() {
forceLoad();
}
}
class VehicleLoaderListener implements LoaderManager.LoaderCallbacks<ObaTripsForRouteResponse>,
Loader.OnLoadCompleteListener<ObaTripsForRouteResponse> {
HashSet<String> routes = new HashSet<>(1);
@Override
public Loader<ObaTripsForRouteResponse> onCreateLoader(int id,
Bundle args) {
return new VehiclesLoader(mFragment.getActivity(), mRouteId);
}
@Override
public void onLoadFinished(Loader<ObaTripsForRouteResponse> loader,
ObaTripsForRouteResponse response) {
ObaMapView obaMapView = mFragment.getMapView();
if (response == null || response.getCode() != ObaApi.OBA_OK) {
BaseMapFragment.showMapError(response);
return;
}
routes.clear();
routes.add(mRouteId);
obaMapView.updateVehicles(routes, response);
if (mZoomIncludeClosestVehicle) {
obaMapView.zoomIncludeClosestVehicle(routes, response);
mZoomIncludeClosestVehicle = false;
}
mLastUpdatedTimeVehicles = UIUtils.getCurrentTimeForComparison();
// Clear any pending refreshes
mVehicleRefreshHandler.removeCallbacks(mVehicleRefresh);
// Post an update
mVehicleRefreshHandler.postDelayed(mVehicleRefresh, VEHICLE_REFRESH_PERIOD);
}
@Override
public void onLoaderReset(Loader<ObaTripsForRouteResponse> loader) {
mFragment.getMapView().removeVehicleOverlay();
}
@Override
public void onLoadComplete(Loader<ObaTripsForRouteResponse> loader,
ObaTripsForRouteResponse response) {
onLoadFinished(loader, response);
}
}
}