package me.guillaumin.android.osmtracker.activity;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import me.guillaumin.android.osmtracker.OSMTracker;
import me.guillaumin.android.osmtracker.R;
import me.guillaumin.android.osmtracker.db.TrackContentProvider;
import me.guillaumin.android.osmtracker.db.TrackContentProvider.Schema;
import me.guillaumin.android.osmtracker.overlay.WayPointsOverlay;
import org.osmdroid.api.IMapController;
import org.osmdroid.tileprovider.tilesource.ITileSource;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.MapView;
import org.osmdroid.views.overlay.PathOverlay;
import org.osmdroid.views.overlay.mylocation.SimpleLocationOverlay;
import android.app.Activity;
import android.content.ContentUris;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.database.Cursor;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
/**
* Display current track over an OSM map.
* Based on osmdroid code http://osmdroid.googlecode.com/
*<P>
* Used only if {@link OSMTracker.Preferences#KEY_UI_DISPLAYTRACK_OSM} is set.
* Otherwise {@link DisplayTrack} is used (track only, no OSM background tiles).
*
* @author Viesturs Zarins
*
*/
public class DisplayTrackMap extends Activity {
private static final String TAG = DisplayTrackMap.class.getSimpleName();
/**
* Key for keeping the zoom level in the saved instance bundle
*/
private static final String CURRENT_ZOOM = "currentZoom";
/**
* Key for keeping scrolled left position of OSM view activity re-creation
*
*/
private static final String CURRENT_SCROLL_X = "currentScrollX";
/**
* Key for keeping scrolled top position of OSM view across activity re-creation
*
*/
private static final String CURRENT_SCROLL_Y = "currentScrollY";
/**
* Key for keeping whether the map display should be centered to the gps location
*
*/
private static final String CURRENT_CENTER_TO_GPS_POS = "currentCenterToGpsPos";
/**
* Key for keeping whether the map display was zoomed and centered
* on an old track id loaded from the database (boolean {@link #zoomedToTrackAlready})
*/
private static final String CURRENT_ZOOMED_TO_TRACK = "currentZoomedToTrack";
/**
* Key for keeping the last zoom level across app. restart
*/
private static final String LAST_ZOOM = "lastZoomLevel";
/**
* Default zoom level
*/
private static final int DEFAULT_ZOOM = 16;
/**
* Main OSM view
*/
private MapView osmView;
/**
* Controller to interact with view
*/
private IMapController osmViewController;
/**
* OSM view overlay that displays current location
*/
private SimpleLocationOverlay myLocationOverlay;
/**
* OSM view overlay that displays current path
*/
private PathOverlay pathOverlay;
/**
* OSM view overlay that displays waypoints
*/
private WayPointsOverlay wayPointsOverlay;
/**
* Current track id
*/
private long currentTrackId;
/**
* whether the map display should be centered to the gps location
*/
private boolean centerToGpsPos = true;
/**
* whether the map display was already zoomed and centered
* on an old track loaded from the database (should be done only once).
*/
private boolean zoomedToTrackAlready = false;
/**
* the last position we know
*/
private GeoPoint currentPosition;
/**
* The row id of the last location read from the database that has been added to the
* list of layout points. Using this we to reduce DB load by only reading new points.
* Initially null, to indicate that no data has yet been read.
*/
private Integer lastTrackPointIdProcessed = null;
/**
* Observes changes on trackpoints
*/
private ContentObserver trackpointContentObserver;
/**
* Keeps the SharedPreferences
*/
private SharedPreferences prefs = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// loading the preferences
prefs = PreferenceManager.getDefaultSharedPreferences(this);
setContentView(R.layout.displaytrackmap);
currentTrackId = getIntent().getExtras().getLong(Schema.COL_TRACK_ID);
setTitle(getTitle() + ": #" + currentTrackId);
// Initialize OSM view
osmView = (MapView) findViewById(R.id.displaytrackmap_osmView);
osmView.setMultiTouchControls(true); // pinch to zoom
// we'll use osmView to define if the screen is always on or not
osmView.setKeepScreenOn(prefs.getBoolean(OSMTracker.Preferences.KEY_UI_DISPLAY_KEEP_ON, OSMTracker.Preferences.VAL_UI_DISPLAY_KEEP_ON));
osmViewController = osmView.getController();
// Check if there is a saved zoom level
if(savedInstanceState != null) {
osmViewController.setZoom(savedInstanceState.getInt(CURRENT_ZOOM, DEFAULT_ZOOM));
osmView.scrollTo(savedInstanceState.getInt(CURRENT_SCROLL_X, 0),
savedInstanceState.getInt(CURRENT_SCROLL_Y, 0));
centerToGpsPos = savedInstanceState.getBoolean(CURRENT_CENTER_TO_GPS_POS, centerToGpsPos);
zoomedToTrackAlready = savedInstanceState.getBoolean(CURRENT_ZOOMED_TO_TRACK, zoomedToTrackAlready);
} else {
// Try to get last zoom Level from Shared Preferences
SharedPreferences settings = getPreferences(MODE_PRIVATE);
osmViewController.setZoom(settings.getInt(LAST_ZOOM, DEFAULT_ZOOM));
}
selectTileSource();
createOverlays();
// Create content observer for trackpoints
trackpointContentObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
pathChanged();
}
};
// Register listeners for zoom buttons
findViewById(R.id.displaytrackmap_imgZoomIn).setOnClickListener( new OnClickListener() {
@Override
public void onClick(View v) {
osmViewController.zoomIn();
}
});
findViewById(R.id.displaytrackmap_imgZoomOut).setOnClickListener( new OnClickListener() {
@Override
public void onClick(View v) {
osmViewController.zoomOut();
}
});
}
/**
* Sets the map tile provider according to the user's demands in the settings.
*/
public void selectTileSource() {
String mapTile = prefs.getString(OSMTracker.Preferences.KEY_UI_MAP_TILE, OSMTracker.Preferences.VAL_UI_MAP_TILE_MAPNIK);
osmView.setTileSource(selectMapTile(mapTile));
}
/**
* Returns a ITileSource for the map according to the selected mapTile
* String. The default is mapnik.
*
* @param mapTile String that is the name of the tile provider
* @return ITileSource with the selected Tile-Source
*/
private ITileSource selectMapTile(String mapTile) {
try {
Field f = TileSourceFactory.class.getField(mapTile);
return (ITileSource) f.get(null);
} catch (Exception e) {
Log.e(TAG, "Invalid tile source '"+mapTile+"'", e);
return TileSourceFactory.MAPNIK;
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putInt(CURRENT_ZOOM, osmView.getZoomLevel());
outState.putInt(CURRENT_SCROLL_X, osmView.getScrollX());
outState.putInt(CURRENT_SCROLL_Y, osmView.getScrollY());
outState.putBoolean(CURRENT_CENTER_TO_GPS_POS, centerToGpsPos);
outState.putBoolean(CURRENT_ZOOMED_TO_TRACK, zoomedToTrackAlready);
super.onSaveInstanceState(outState);
}
@Override
protected void onResume() {
// setKeepScreenOn depending on user's preferences
osmView.setKeepScreenOn(prefs.getBoolean(OSMTracker.Preferences.KEY_UI_DISPLAY_KEEP_ON, OSMTracker.Preferences.VAL_UI_DISPLAY_KEEP_ON));
// Register content observer for any trackpoint changes
getContentResolver().registerContentObserver(
TrackContentProvider.trackPointsUri(currentTrackId),
true, trackpointContentObserver);
// Forget the last waypoint read from the DB
// This ensures that all waypoints for the track will be reloaded
// from the database to populate the path layout
lastTrackPointIdProcessed = null;
// Reload path
pathChanged();
selectTileSource();
// Refresh way points
// wayPointsOverlay.refresh();
super.onResume();
}
@Override
protected void onPause() {
// Unregister content observer
getContentResolver().unregisterContentObserver(trackpointContentObserver);
// Clear the points list.
pathOverlay.clearPath();
super.onPause();
}
@Override
protected void onStop() {
super.onStop();
// Save zoom level in shared preferences
SharedPreferences settings = getPreferences(MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putInt(LAST_ZOOM, osmView.getZoomLevel());
editor.commit();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.displaytrackmap_menu, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.displaytrackmap_menu_center_to_gps).setEnabled( (!centerToGpsPos && currentPosition != null ) );
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()){
case R.id.displaytrackmap_menu_center_to_gps:
centerToGpsPos = true;
if(currentPosition != null){
osmViewController.animateTo(currentPosition);
}
break;
case R.id.displaytrackmap_menu_settings:
// Start settings activity
startActivity(new Intent(this, Preferences.class));
break;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch(event.getAction()){
case MotionEvent.ACTION_MOVE:
if (currentPosition != null)
centerToGpsPos = false;
break;
}
return super.onTouchEvent(event);
}
/**
* Creates overlays over the OSM view
*/
private void createOverlays() {
pathOverlay = new PathOverlay(Color.BLUE, this);
osmView.getOverlays().add(pathOverlay);
myLocationOverlay = new SimpleLocationOverlay(this);
osmView.getOverlays().add(myLocationOverlay);
wayPointsOverlay = new WayPointsOverlay(this, currentTrackId);
osmView.getOverlays().add(wayPointsOverlay);
}
/**
* On track path changed, update the two overlays and repaint view.
* If {@link #lastTrackPointIdProcessed} is null, this is the initial call
* from {@link #onResume()}, and not the periodic call from
* {@link ContentObserver#onChange(boolean) trackpointContentObserver.onChange(boolean)}
* while recording.
*/
private void pathChanged() {
if (isFinishing()) {
return;
}
// See if the track is active.
// If not, we'll calculate initial track bounds
// while retrieving from the database.
// (the first point will overwrite these lat/lon bounds.)
boolean doInitialBoundsCalc = false;
double minLat = 91.0, minLon = 181.0;
double maxLat = -91.0, maxLon = -181.0;
if ((! zoomedToTrackAlready) && (lastTrackPointIdProcessed == null)) {
final String[] proj_active = {Schema.COL_ACTIVE};
Cursor cursor = getContentResolver().query(
ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_TRACK, currentTrackId),
proj_active, null, null, null);
if (cursor.moveToFirst()) {
doInitialBoundsCalc =
(cursor.getInt(cursor.getColumnIndex(Schema.COL_ACTIVE)) == Schema.VAL_TRACK_INACTIVE);
}
cursor.close();
}
// Projection: The columns to retrieve. Here, we want the latitude,
// longitude and primary key only
String[] projection = {Schema.COL_LATITUDE, Schema.COL_LONGITUDE, Schema.COL_ID};
// Selection: The where clause to use
String selection = null;
// SelectionArgs: The parameter replacements to use for the '?' in the selection
String[] selectionArgs = null;
// Only request the track points that we have not seen yet
// If we have processed any track points in this session then
// lastTrackPointIdProcessed will not be null. We only want
// to see data from rows with a primary key greater than lastTrackPointIdProcessed
if (lastTrackPointIdProcessed != null) {
selection = TrackContentProvider.Schema.COL_ID + " > ?";
List<String> selectionArgsList = new ArrayList<String>();
selectionArgsList.add(lastTrackPointIdProcessed.toString());
selectionArgs = selectionArgsList.toArray(new String[1]);
}
// Retrieve any points we have not yet seen
Cursor c = getContentResolver().query(
TrackContentProvider.trackPointsUri(currentTrackId),
projection, selection, selectionArgs, Schema.COL_ID + " asc");
int numberOfPointsRetrieved = c.getCount();
if (numberOfPointsRetrieved > 0 ) {
c.moveToFirst();
double lastLat = 0;
double lastLon = 0;
int primaryKeyColumnIndex = c.getColumnIndex(Schema.COL_ID);
int latitudeColumnIndex = c.getColumnIndex(Schema.COL_LATITUDE);
int longitudeColumnIndex = c.getColumnIndex(Schema.COL_LONGITUDE);
// Add each new point to the track
while(!c.isAfterLast()) {
lastLat = c.getDouble(latitudeColumnIndex);
lastLon = c.getDouble(longitudeColumnIndex);
lastTrackPointIdProcessed = c.getInt(primaryKeyColumnIndex);
pathOverlay.addPoint((int)(lastLat * 1e6), (int)(lastLon * 1e6));
if (doInitialBoundsCalc) {
if (lastLat < minLat) minLat = lastLat;
if (lastLon < minLon) minLon = lastLon;
if (lastLat > maxLat) maxLat = lastLat;
if (lastLon > maxLon) maxLon = lastLon;
}
c.moveToNext();
}
// Last point is current position.
currentPosition = new GeoPoint(lastLat, lastLon);
myLocationOverlay.setLocation(currentPosition);
if(centerToGpsPos) {
osmViewController.setCenter(currentPosition);
}
// Repaint
osmView.invalidate();
if (doInitialBoundsCalc && (numberOfPointsRetrieved > 1)) {
// osmdroid-3.0.8 hangs if we directly call zoomToSpan during initial onResume,
// so post a Runnable instead for after it's done initializing.
final double north = maxLat, east = maxLon, south = minLat, west = minLon;
osmView.post(new Runnable() {
@Override
public void run() {
osmViewController.zoomToSpan((int) (north-south), (int) (east-west));
osmViewController.setCenter(new GeoPoint((north + south) / 2, (east + west) / 2));
zoomedToTrackAlready = true;
}
});
}
}
c.close();
}
}