/**
AirCasting - Share your Air!
Copyright (C) 2011-2012 HabitatMap, Inc.
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/>.
You can contact the authors by email at <info@habitatmap.org>
*/
package pl.llp.aircasting.activity;
import android.net.Uri;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.widget.ImageView;
import com.google.android.maps.MapController;
import com.google.android.maps.OverlayItem;
import com.google.common.eventbus.Subscribe;
import pl.llp.aircasting.Intents;
import pl.llp.aircasting.R;
import pl.llp.aircasting.activity.events.SessionChangeEvent;
import pl.llp.aircasting.api.AveragesDriver;
import pl.llp.aircasting.event.sensor.LocationEvent;
import pl.llp.aircasting.event.session.NoteCreatedEvent;
import pl.llp.aircasting.event.ui.DoubleTapEvent;
import pl.llp.aircasting.event.ui.StreamUpdateEvent;
import pl.llp.aircasting.event.ui.ViewStreamEvent;
import pl.llp.aircasting.helper.LocationConversionHelper;
import pl.llp.aircasting.model.Measurement;
import pl.llp.aircasting.model.Note;
import pl.llp.aircasting.model.Sensor;
import pl.llp.aircasting.model.internal.Region;
import pl.llp.aircasting.util.http.HttpResult;
import pl.llp.aircasting.view.AirCastingMapView;
import pl.llp.aircasting.view.MapIdleDetector;
import pl.llp.aircasting.view.overlay.*;
import android.location.Location;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.os.Bundle;
import android.widget.Toast;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.Projection;
import com.google.inject.Inject;
import pl.llp.aircasting.view.presenter.MeasurementPresenter;
import roboguice.inject.InjectResource;
import roboguice.inject.InjectView;
import java.io.File;
import java.util.List;
import static java.lang.Math.min;
import static pl.llp.aircasting.helper.LocationConversionHelper.boundingBox;
import static pl.llp.aircasting.helper.LocationConversionHelper.geoPoint;
import static pl.llp.aircasting.view.MapIdleDetector.detectMapIdle;
/**
* Created by IntelliJ IDEA.
* User: obrok
* Date: 10/17/11
* Time: 5:04 PM
*/
public class AirCastingMapActivity extends AirCastingActivity implements MapIdleDetector.MapIdleListener, MeasurementPresenter.Listener {
@InjectView(R.id.mapview) AirCastingMapView mapView;
@InjectView(R.id.spinner) ImageView spinner;
@InjectResource(R.anim.spinner) Animation spinnerAnimation;
@Inject HeatMapOverlay heatMapOverlay;
@Inject AveragesDriver averagesDriver;
@Inject ConnectivityManager connectivityManager;
@Inject NoteOverlay noteOverlay;
@Inject LocationOverlay locationOverlay;
@Inject TraceOverlay traceOverlay;
@Inject MeasurementPresenter measurementPresenter;
@Inject RouteOverlay routeOverlay;
public static final int HEAT_MAP_UPDATE_TIMEOUT = 500;
public static final int SOUND_TRACE_UPDATE_TIMEOUT = 300;
private boolean soundTraceComplete = true;
private boolean heatMapVisible = false;
private int requestsOutstanding = 0;
private AsyncTask<Void, Void, Void> refreshTask;
private MapIdleDetector heatMapDetector;
private MapIdleDetector soundTraceDetector;
private HeatMapUpdater updater;
private boolean initialized = false;
private Measurement lastMeasurement;
private boolean zoomToSession = true;
private MapIdleDetector routeRefreshDetector;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
noteOverlay.setContext(this);
setContentView(R.layout.heat_map);
mapView.getOverlays().add(routeOverlay);
mapView.getOverlays().add(traceOverlay);
if (!sessionManager.isSessionSaved()) {
mapView.getOverlays().add(locationOverlay);
}
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
zoomToSession = false;
}
private void toggleHeatMapVisibility() {
if (heatMapVisible) {
heatMapVisible = false;
mapView.getOverlays().remove(heatMapOverlay);
mapView.invalidate();
} else {
heatMapVisible = true;
mapView.getOverlays().add(heatMapOverlay);
mapView.invalidate();
}
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.toggle_heat_map_button:
toggleHeatMapVisibility();
updateButtons();
break;
case R.id.zoom_in:
mapView.getController().zoomIn();
break;
case R.id.zoom_out:
mapView.getController().zoomOut();
break;
case R.id.locate:
centerMap();
break;
default:
super.onClick(view);
}
}
@Override
protected void addContextSpecificButtons() {
if (!sessionManager.isSessionSaved()) {
addButton(R.layout.context_button_locate);
}
if (heatMapVisible) {
addButton(R.layout.context_button_crowdmap_active);
} else {
addButton(R.layout.context_button_crowdmap_inactive);
}
addButton(R.layout.context_button_dashboard);
}
@Override
protected void onResume() {
super.onResume();
initialize();
refreshNotes();
spinnerAnimation.start();
initializeMap();
measurementPresenter.registerListener(this);
initializeRouteOverlay();
traceOverlay.startDrawing();
checkConnection();
updater = new HeatMapUpdater();
heatMapDetector = detectMapIdle(mapView, HEAT_MAP_UPDATE_TIMEOUT, updater);
soundTraceDetector = detectMapIdle(mapView, SOUND_TRACE_UPDATE_TIMEOUT, this);
}
@Override
protected void onPause() {
super.onPause();
measurementPresenter.unregisterListener(this);
routeRefreshDetector.stop();
traceOverlay.stopDrawing(mapView);
heatMapDetector.stop();
soundTraceDetector.stop();
}
private void initializeRouteOverlay() {
routeOverlay.clear();
if (shouldShowRoute()) {
Sensor sensor = sensorManager.getVisibleSensor();
List<Measurement> measurements = sessionManager.getMeasurements(sensor);
for (Measurement measurement : measurements) {
GeoPoint geoPoint = geoPoint(measurement);
routeOverlay.addPoint(geoPoint);
}
}
routeRefreshDetector = detectMapIdle(mapView, 100, new MapIdleDetector.MapIdleListener() {
@Override
public void onMapIdle() {
runOnUiThread(new Runnable() {
@Override
public void run() {
routeOverlay.invalidate();
mapView.invalidate();
}
});
}
});
}
private boolean shouldShowRoute() {
return settingsHelper.isShowRoute() &&
(sessionManager.isRecording() || sessionManager.isSessionSaved());
}
private void initializeMap() {
mapView.setSatellite(settingsHelper.isSatelliteView());
if (settingsHelper.isFirstLaunch()) {
mapView.getController().setZoom(16);
Location location = locationHelper.getLastLocation();
if (location != null) {
GeoPoint geoPoint = geoPoint(location);
mapView.getController().setCenter(geoPoint);
}
settingsHelper.setFirstLaunch(false);
}
}
protected void startSpinner() {
if (spinner.getVisibility() != View.VISIBLE) {
spinner.setVisibility(View.VISIBLE);
spinner.setAnimation(spinnerAnimation);
}
}
protected void stopSpinner() {
spinner.setVisibility(View.INVISIBLE);
spinner.setAnimation(null);
}
private void initialize() {
if (!initialized) {
showSession();
mapView.getOverlays().add(noteOverlay);
initialized = true;
}
}
private void showSession() {
if (sessionManager.isSessionSaved() && zoomToSession) {
LocationConversionHelper.BoundingBox boundingBox = boundingBox(sessionManager.getSession());
mapView.getController().zoomToSpan(boundingBox.getLatSpan(), boundingBox.getLonSpan());
mapView.getController().animateTo(boundingBox.getCenter());
}
}
@Override
protected void refreshNotes() {
noteOverlay.clear();
for (Note note : sessionManager.getNotes()) {
noteOverlay.add(note);
}
}
public void noteClicked(OverlayItem item, int index) {
suppressNextTap();
mapView.getController().animateTo(item.getPoint());
noteClicked(index);
}
@Subscribe
@Override
public void onEvent(StreamUpdateEvent event) {
super.onEvent(event);
runOnUiThread(new Runnable() {
@Override
public void run() {
mapView.invalidate();
}
});
updater.onMapIdle();
onMapIdle();
}
@Override
@Subscribe
public void onEvent(SessionChangeEvent event) {
super.onEvent(event);
refreshNotes();
mapView.invalidate();
}
@Subscribe
public void onEvent(NoteCreatedEvent event) {
refreshNotes();
}
@Subscribe
public void onEvent(DoubleTapEvent event) {
mapView.getController().zoomIn();
}
@Subscribe
public void onEvent(MotionEvent event) {
mapView.dispatchTouchEvent(event);
}
@Subscribe
public void onEvent(LocationEvent event) {
updateRoute();
mapView.invalidate();
}
private void updateRoute() {
if (settingsHelper.isShowRoute() && sessionManager.isRecording()) {
GeoPoint geoPoint = geoPoint(locationHelper.getLastLocation());
routeOverlay.addPoint(geoPoint);
}
}
protected void centerMap() {
if (locationHelper.getLastLocation() != null) {
GeoPoint geoPoint = geoPoint(locationHelper.getLastLocation());
MapController controller = mapView.getController();
controller.animateTo(geoPoint);
}
}
@Override
public void onViewUpdated() {
}
@Override
public void onAveragedMeasurement(Measurement measurement) {
if (sessionManager.isSessionStarted()) {
if (!settingsHelper.isAveraging()) {
traceOverlay.update(measurement);
} else if (lastMeasurement != null) {
traceOverlay.update(lastMeasurement);
}
}
if (settingsHelper.isAveraging()) {
lastMeasurement = measurement;
}
runOnUiThread(new Runnable() {
@Override
public void run() {
mapView.invalidate();
}
});
}
private void checkConnection() {
NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
if (activeNetworkInfo == null || !activeNetworkInfo.isConnectedOrConnecting()) {
Toast.makeText(this, R.string.no_internet, Toast.LENGTH_SHORT).show();
}
}
private void refresh() {
boolean complete = (requestsOutstanding == 0) && soundTraceComplete;
if (complete) {
stopSpinner();
} else {
startSpinner();
}
if (!complete) mapView.invalidate();
}
@Override
public void onMapIdle() {
runOnUiThread(new Runnable() {
@Override
public void run() {
refreshSoundTrace();
}
});
}
private void refreshSoundTrace() {
if (refreshTask != null && refreshTask.getStatus() != AsyncTask.Status.FINISHED) return;
soundTraceComplete = false;
refresh();
traceOverlay.refresh(mapView);
soundTraceComplete = true;
mapView.invalidate();
refresh();
}
class HeatMapDownloader extends AsyncTask<Void, Void, HttpResult<Iterable<Region>>> {
public static final int MAP_BUFFER_SIZE = 3;
@Override
protected void onPreExecute() {
requestsOutstanding += 1;
refresh();
}
@Override
protected HttpResult<Iterable<Region>> doInBackground(Void... voids) {
Projection projection = mapView.getProjection();
// We want to download data that's off screen so the user can see something while panning
GeoPoint northWest = projection.fromPixels(-mapView.getWidth(), -mapView.getHeight());
GeoPoint southEast = projection.fromPixels(2 * mapView.getWidth(), 2 * mapView.getHeight());
Location northWestLoc = LocationConversionHelper.location(northWest);
Location southEastLoc = LocationConversionHelper.location(southEast);
int size = min(mapView.getWidth(), mapView.getHeight()) / settingsHelper.getHeatMapDensity();
if (size < 1) size = 1;
int gridSizeX = MAP_BUFFER_SIZE * mapView.getWidth() / size;
int gridSizeY = MAP_BUFFER_SIZE * mapView.getHeight() / size;
return averagesDriver.index(sensorManager.getVisibleSensor(), northWestLoc.getLongitude(), northWestLoc.getLatitude(),
southEastLoc.getLongitude(), southEastLoc.getLatitude(), gridSizeX, gridSizeY);
}
@Override
protected void onPostExecute(HttpResult<Iterable<Region>> regions) {
requestsOutstanding -= 1;
if (regions.getContent() != null) {
heatMapOverlay.setRegions(regions.getContent());
}
mapView.invalidate();
refresh();
}
}
private class HeatMapUpdater implements MapIdleDetector.MapIdleListener {
@Override
public void onMapIdle() {
runOnUiThread(new Runnable() {
@Override
public void run() {
//noinspection unchecked
new HeatMapDownloader().execute();
}
});
}
}
}