/*
* 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 com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.common.api.GoogleApiClient;
import org.onebusaway.android.app.Application;
import org.onebusaway.android.io.ObaApi;
import org.onebusaway.android.io.elements.ObaStop;
import org.onebusaway.android.io.request.ObaStopsForLocationRequest;
import org.onebusaway.android.io.request.ObaStopsForLocationResponse;
import org.onebusaway.android.map.googlemapsv2.BaseMapFragment;
import org.onebusaway.android.util.LocationUtils;
import org.onebusaway.android.util.RegionUtils;
import org.onebusaway.android.util.UIUtils;
import android.app.Activity;
import android.content.Context;
import android.location.Location;
import android.os.Bundle;
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 java.util.Arrays;
import java.util.List;
final class StopsRequest {
private final Location mCenter;
private final double mLatSpan;
private final double mLonSpan;
private final double mZoomLevel;
StopsRequest(MapModeController.ObaMapView view) {
mCenter = view.getMapCenterAsLocation();
mLatSpan = view.getLatitudeSpanInDecDegrees();
mLonSpan = view.getLongitudeSpanInDecDegrees();
mZoomLevel = view.getZoomLevelAsFloat();
}
Location getCenter() {
return mCenter;
}
double getLatSpan() {
return mLatSpan;
}
double getLonSpan() {
return mLonSpan;
}
double getZoomLevel() {
return mZoomLevel;
}
}
final class StopsResponse {
private final StopsRequest mRequest;
private final ObaStopsForLocationResponse mResponse;
StopsResponse(StopsRequest req, ObaStopsForLocationResponse response) {
mRequest = req;
mResponse = response;
}
StopsRequest getRequest() {
return mRequest;
}
ObaStopsForLocationResponse getResponse() {
return mResponse;
}
/**
* Returns true if newReq also fulfills response.
*/
boolean fulfills(StopsRequest newReq) {
if (mRequest.getCenter() == null) {
//Log.d(TAG, "No center");
return false;
}
if (!mRequest.getCenter().equals(newReq.getCenter())) {
//Log.d(TAG, "Center not the same");
return false;
}
if (mResponse != null) {
if ((newReq.getZoomLevel() > mRequest.getZoomLevel()) &&
mResponse.getLimitExceeded()) {
//Log.d(TAG, "Zooming in -- limit exceeded");
return false;
} else if (newReq.getZoomLevel() < mRequest.getZoomLevel()) {
//Log.d(TAG, "Zooming out");
return false;
}
}
return true;
// Otherwise:
// If the new request's is zoomed in and the current
// response has limitExceeded, then no.
// If the new request's lat/lon span is contained
// entirely within the old one:
// Then the new request is fulfilled IFF the old request's
// limitExceeded == false.
// If the new request's lat/lon span is not contained
// entirely within the old one (fuzzy match)
// FALSE
}
}
public class StopMapController implements MapModeController,
LoaderManager.LoaderCallbacks<StopsResponse>,
Loader.OnLoadCompleteListener<StopsResponse>,
MapWatcher.Listener {
private static final String TAG = "StopMapController";
private static final int STOPS_LOADER = 5678;
private final Callback mCallback;
// In lieu of using an actual LoaderManager, which isn't
// available in SherlockMapActivity
private Loader<StopsResponse> mLoader;
private MapWatcher mMapWatcher;
/**
* GoogleApiClient being used for Location Services
*/
GoogleApiClient mGoogleApiClient;
public StopMapController(Callback callback) {
mCallback = callback;
GoogleApiAvailability api = GoogleApiAvailability.getInstance();
// Init Google Play Services as early as possible in the Fragment lifecycle to give it time
if (api.isGooglePlayServicesAvailable(mCallback.getActivity())
== ConnectionResult.SUCCESS) {
Context context = mCallback.getActivity();
mGoogleApiClient = LocationUtils.getGoogleApiClientWithCallbacks(context);
mGoogleApiClient.connect();
}
//mCallback.getLoaderManager().initLoader(STOPS_LOADER, null, this);
mLoader = onCreateLoader(STOPS_LOADER, null);
mLoader.registerListener(0, this);
mLoader.startLoading();
}
/**
* Sets the initial state of where the map is focused, and it's zoom level
*/
@Override
public void setState(Bundle args) {
if (args != null) {
Location center = UIUtils.getMapCenter(args);
// If the STOP_ID was set in the bundle, then we should focus on that stop
String stopId = args.getString(MapParams.STOP_ID);
if (stopId != null && center != null) {
mCallback.getMapView().setZoom(MapParams.DEFAULT_ZOOM);
setMapCenter(center);
return;
}
boolean dontCenterOnLocation = args.getBoolean(MapParams.DO_N0T_CENTER_ON_LOCATION);
// Try to set map based on real-time location, unless state says no
if (!dontCenterOnLocation) {
boolean setLocation = mCallback.setMyLocation(true, false);
if (setLocation) {
return;
}
}
// If we have a previous map view, center map on that
if (center != null) {
float mapZoom = args.getFloat(MapParams.ZOOM, MapParams.DEFAULT_ZOOM);
mCallback.getMapView().setZoom(mapZoom);
setMapCenter(center);
return;
}
} else {
// We don't have any state info - just center on last known location
boolean setLocation = mCallback.setMyLocation(false, false);
if (setLocation) {
return;
}
}
// If all else fails, just center on the region
mCallback.zoomToRegion();
}
/**
* Sets the map center and loads stops for the new map view
*
* @param center new coordinates for the map to center on
*/
private void setMapCenter(Location center) {
mCallback.getMapView().setMapCenter(center, false, false);
onLocation();
}
@Override
public String getMode() {
return MapParams.MODE_STOP;
}
@Override
public void destroy() {
//mCallback.getLoaderManager().destroyLoader(STOPS_LOADER);
getLoader().reset();
watchMap(false);
}
@Override
public void onPause() {
watchMap(false);
// Tear down GoogleApiClient
if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
mGoogleApiClient.disconnect();
}
}
/**
* 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) {
// No op for this controller
}
@Override
public void onResume() {
watchMap(true);
// Make sure GoogleApiClient is connected, if available
if (mGoogleApiClient != null && !mGoogleApiClient.isConnected()) {
mGoogleApiClient.connect();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
}
@Override
public void onViewStateRestored(Bundle savedInstanceState) {
// We don't need to handle the view state here. This is already been handled in HomeActivity
// when the bus stop is selected on the map
}
@Override
public void onLocation() {
refresh();
}
@Override
public void onNoLocation() {
}
@Override
public Loader<StopsResponse> onCreateLoader(int id, Bundle args) {
StopsLoader loader = new StopsLoader(mCallback);
StopsRequest req = new StopsRequest(mCallback.getMapView());
loader.update(req);
return loader;
}
@Override
public void onLoadFinished(Loader<StopsResponse> loader,
StopsResponse _response) {
mCallback.showProgress(false);
final ObaStopsForLocationResponse response = _response.getResponse();
if (response == null) {
// Initial install can generate a null response if all is still ok, so do nothing (#615)
return;
}
if (response.getCode() != ObaApi.OBA_OK) {
BaseMapFragment.showMapError(response);
return;
}
if (response.getOutOfRange()) {
mCallback.notifyOutOfRange();
return;
}
//Workaround for https://github.com/OneBusAway/onebusaway-application-modules/issues/59
//where outOfRange response element is false even if the location was out of range
//We need to also make sure the list of stops is empty, otherwise we screen out valid responses
//TODO - After above issue #59 is resolved, we should also only do this check on OBA server
//versions below the version number in which this is fixed.
Location myLocation = Application.getLastKnownLocation(mCallback.getActivity(),
mGoogleApiClient);
if (myLocation != null && Application.get().getCurrentRegion() != null) {
boolean inRegion = true; // Assume user is in region unless we detect otherwise
try {
inRegion = RegionUtils
.isLocationWithinRegion(myLocation, Application.get().getCurrentRegion());
} catch (IllegalArgumentException e) {
// Issue #69 - some devices are providing invalid lat/long coordinates
Log.e(TAG, "Invalid latitude or longitude - lat = " + myLocation.getLatitude()
+ ", long = " + myLocation.getLongitude());
}
if (!inRegion && Arrays.asList(response.getStops()).isEmpty()) {
Log.d(TAG, "Device location is outside region range, notifying...");
mCallback.notifyOutOfRange();
return;
}
}
List<ObaStop> stops = Arrays.asList(response.getStops());
mCallback.showStops(stops, response);
}
@Override
public void onLoaderReset(Loader<StopsResponse> loader) {
// Clear the overlay.
mCallback.showStops(null, null);
}
// Remove when adding back LoaderManager help.
@Override
public void onLoadComplete(Loader<StopsResponse> loader,
StopsResponse response) {
onLoadFinished(loader, response);
}
//
// Loading
//
private StopsLoader getLoader() {
//Loader<ObaStopsForLocationResponse> l =
// mCallback.getLoaderManager().getLoader(STOPS_LOADER);
//return (StopsLoader)l;
return (StopsLoader) mLoader;
}
private void refresh() {
// First we need to check to see if the current request we have can handle this.
// Otherwise, we need to restart the loader with the new request.
if (mCallback != null) {
Activity a = mCallback.getActivity();
if (a != null) {
a.runOnUiThread(new Runnable() {
@Override
public void run() {
StopsLoader loader = getLoader();
if (loader != null) {
StopsRequest req = new StopsRequest(mCallback.getMapView());
loader.update(req);
}
}
});
}
}
}
//
// Loader
//
private static class StopsLoader extends AsyncTaskLoader<StopsResponse> {
private final Callback mFragment;
private StopsRequest mRequest;
private StopsResponse mResponse;
public StopsLoader(Callback fragment) {
super(fragment.getActivity());
mFragment = fragment;
}
@Override
public StopsResponse loadInBackground() {
StopsRequest req = mRequest;
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 from server without " +
"OBA REST API endpoint, aborting...");
return new StopsResponse(req, null);
}
//Make OBA REST API call to the server and return result
ObaStopsForLocationResponse response =
new ObaStopsForLocationRequest.Builder(getContext(),
req.getCenter())
.setSpan(req.getLatSpan(), req.getLonSpan())
.build()
.call();
return new StopsResponse(req, response);
}
@Override
public void deliverResult(StopsResponse data) {
mResponse = data;
super.deliverResult(data);
}
@Override
public void onStartLoading() {
if (takeContentChanged()) {
forceLoad();
}
}
@Override
public void onForceLoad() {
mFragment.showProgress(true);
super.onForceLoad();
}
public void update(StopsRequest req) {
if (mResponse == null || !mResponse.fulfills(req)) {
mRequest = req;
onContentChanged();
}
}
}
//
// Map watcher
//
private void watchMap(boolean watch) {
// Only instantiate our own map watcher if the mapView isn't capable of watching itself
if (watch && !mCallback.getMapView().canWatchMapChanges()) {
if (mMapWatcher == null) {
mMapWatcher = new MapWatcher(mCallback.getMapView(), this);
}
mMapWatcher.start();
} else {
if (mMapWatcher != null) {
mMapWatcher.stop();
}
mMapWatcher = null;
}
}
@Override
public void onMapZoomChanging() {
//Log.d(TAG, "Map zoom changing");
}
@Override
public void onMapZoomChanged() {
//Log.d(TAG, "Map zoom changed");
refresh();
}
@Override
public void onMapCenterChanging() {
//Log.d(TAG, "Map center changing");
}
@Override
public void onMapCenterChanged() {
// Log.d(TAG, "Map center changed.");
refresh();
}
@Override
public void notifyMapChanged() {
Log.d(TAG, "Map changed (called by MapView)");
refresh();
}
}