/*******************************************************************************
* Copyright 2011 The Regents of the University of California
*
* 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.ohmage.triggers.types.location;
/*
* The maps activity to display and modify the coordinates
* associated with each place (category).
*
* This class has a complex implementation because of the
* limitations in the maps api. The following scenarios are
* implemented here:
* - Overlays of different types
* - Drawing circles around overlays
* - Long press on any geo point
* - Tap on overlays
* - Show and hide additional views on a geo point.
*
* Since all the above scenarios cannot be address at once
* using the built in mechanisms, the implementation below
* uses its own overridden overlay mechanism and touch handling.
*/
/*
* TODO: This Activity has leaking dialogs. Needs to be fixed.
* Currently, activity rotation is disabled in the manifest
*/
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.PendingIntent;
import android.app.ProgressDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.util.FloatMath;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.ItemizedOverlay;
import com.google.android.maps.ItemizedOverlay.OnFocusChangeListener;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;
import com.google.android.maps.OverlayItem;
import org.ohmage.OhmageApplication;
import org.ohmage.R;
import org.ohmage.logprobe.Analytics;
import org.ohmage.logprobe.Log;
import org.ohmage.logprobe.LogProbe.Status;
import org.ohmage.triggers.config.LocTrigConfig;
import org.ohmage.triggers.utils.TrigTextInput;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/* The maps activity */
public class LocTrigMapsActivity extends MapActivity
implements LocationListener,
OnFocusChangeListener,
LocTrigAddLocBalloon.ActionListener {
private static final String TAG = "LocTrigMapsActivity";
public static final String TOOL_TIP_PREF_NAME =
LocTrigMapsActivity.class.getName() + "tool_tip_pref";
public static final String KEY_TOOL_TIP_DO_NT_SHOW =
LocTrigMapsActivity.class.getName() + "tool_tip_do_not_show";
//The delay before showing the tool tip
private static long TOOL_TIP_DELAY = 500; //ms
//Location id for the 'my location' overlay
private static final int CURR_LOC_ID = -1;
//Number of retries of address look up in case of failures
private static final int GEOCODING_RETRIES = 5;
//Interval between consecutive address lookups
private static final long GEOCODING_RETRY_INTERVAL = 500; //ms
/* Menu ids */
private static final int MENU_MY_LOC_ID = Menu.FIRST;
private static final int MENU_SEARCH_ID = Menu.FIRST + 1;
private static final int MENU_SATELLITE_ID = Menu.FIRST + 2;
private static final int MENU_HELP_ID = Menu.FIRST + 3;
//Timeout for 'my location' GPS sampling
private static final long MY_LOC_SAMPLE_TIMEOUT = 180000; //3 mins
private static final long MY_LOC_EXPIRY = 60000; //1min
//Alarm action string for GPS timeout
private static final String ACTION_ALRM_GPS_TIMEOUT =
"edu.ucla.cens.loctriggers.activity.MapsActivity.myloc_timeout";
//GPS timeout alarm PI
private PendingIntent mGpsTimeoutPI = null;
//Mapview instance
private MapView mMapView;
//The list of overlay items. This class also handles
//the touch actions
private MapsOverlay mOverlayItems = null;
//Location manager (for GPS)
private LocationManager mLocMan;
//Db instance
private LocTrigDB mDb;
//The category id for which this activity is opened
private int mCategId = 0;
//Balloon displaying 'add this/my location'
private LocTrigAddLocBalloon mAddLocBalloon = null;
//The async task for address search
private AsyncTask<String, Void, Address> mSearchTask = null;
//Alarm manager (for GPS timeout)
private AlarmManager mAlarmMan = null;
private Location mLatestLoc = null;
/*****************************************************************************/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.trigger_loc_maps);
//Get the category id from the intent
Bundle extras = getIntent().getExtras();
if(extras == null) {
Log.e(TAG, "Maps: Intent extras is null");
finish();
return;
}
mCategId = extras.getInt(LocTrigDB.KEY_ID);
Log.v(TAG, "Maps: category id = " + mCategId);
FrameLayout mapContainer = (FrameLayout) findViewById(R.id.mapViewContainer);
mMapView = new MapView(this, OhmageApplication.isDebugBuild() ?
getString(R.string.maps_debug_api_key) : getString(R.string.maps_release_api_key));
mapContainer.addView(mMapView);
mMapView.setClickable(true);
mMapView.setBuiltInZoomControls(true);
//Handle done button and exit this activity
Button bDone = (Button) findViewById(R.id.button_maps_done);
bDone.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
exitMaps();
}
});
mAlarmMan = (AlarmManager) getSystemService(ALARM_SERVICE);
mLocMan = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
mDb = new LocTrigDB(this);
mDb.open();
//Create the balloon to display 'add location'
mAddLocBalloon = new LocTrigAddLocBalloon(this, mMapView);
mAddLocBalloon.setActionListener(this);
//Create the itemized overlay with red marker as the default
Drawable redMarker = this.getResources().getDrawable(
R.drawable.trigger_loc_marker_red);
mOverlayItems = new MapsOverlay(redMarker);
mOverlayItems.setOnFocusChangeListener(this);
//Initialize the overlays with an ItemizedOverlay object.
//The object contains zero items initially
//This is required to handle events when there are
//no markers on the map
mMapView.getOverlays().clear();
mMapView.getOverlays().add(mOverlayItems);
/* Add all the markers and
* Set the zoom level to display all the markers */
int minLat = Integer.MAX_VALUE;
int minLong = Integer.MAX_VALUE;
int maxLat = Integer.MIN_VALUE;
int maxLong = Integer.MIN_VALUE;
Cursor c = mDb.getLocations(mCategId);
if(c.moveToFirst()) {
do {
int latE6 = c.getInt(c.getColumnIndexOrThrow(LocTrigDB.KEY_LAT));
int longE6 = c.getInt(c.getColumnIndexOrThrow(LocTrigDB.KEY_LONG));
int locationId = c.getInt(c.getColumnIndexOrThrow(LocTrigDB.KEY_ID));
float r = c.getFloat(c.getColumnIndexOrThrow(LocTrigDB.KEY_RADIUS));
GeoPoint prevLoc = new GeoPoint(latE6, longE6);
MapsOverlayItem prevLocItem = new MapsOverlayItem(prevLoc, locationId, r);
mOverlayItems.addOverlay(prevLocItem);
minLat = Math.min(latE6, minLat);
minLong = Math.min(longE6, minLong);
maxLat = Math.max(latE6, maxLat);
maxLong = Math.max(longE6, maxLong);
}while(c.moveToNext());
}
if(c.getCount() > 0) {
//Make all the markers visible
mMapView.getController().zoomToSpan(
Math.abs(minLat - maxLat), Math.abs(minLong - maxLong));
GeoPoint centerPoint = new GeoPoint((minLat + maxLat)/2, (minLong + maxLong)/2);
mMapView.getController().setCenter(centerPoint);
}
else {
//No makers, show the last known location
Location lastP = mLocMan.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if(lastP != null) {
GeoPoint lastGp = new GeoPoint((int) (lastP.getLatitude() * 1E6),
(int) (lastP.getLongitude() * 1E6));
mMapView.getController().setCenter(lastGp);
mMapView.getController().setZoom(mMapView.getMaxZoomLevel());
}
}
c.close();
//Display appropriate title text
updateTitleText();
Runnable runnable = new Runnable() {
@Override
public void run() {
showHelpDialog();
}
};
if(!shouldSkipToolTip()) {
//Show the tool tip after a small delay
new Handler().postDelayed(runnable, TOOL_TIP_DELAY);
}
// TrigPrefManager.registerPreferenceFile(LocTrigMapsActivity.this,
// TOOL_TIP_PREF_NAME);
}
@Override
protected void onResume() {
super.onResume();
Analytics.activity(this, Status.ON);
}
@Override
protected void onPause() {
super.onPause();
Analytics.activity(this, Status.OFF);
}
private boolean shouldSkipToolTip() {
SharedPreferences pref = getSharedPreferences(TOOL_TIP_PREF_NAME,
Context.MODE_PRIVATE);
return pref.getBoolean(KEY_TOOL_TIP_DO_NT_SHOW, false);
}
private void showHelpDialog() {
Dialog dialog = new Dialog(this);
dialog.setContentView(R.layout.trigger_loc_maps_tips);
dialog.setTitle(R.string.trigger_loc_defining_locations);
dialog.setOwnerActivity(this);
dialog.show();
WebView webView = (WebView) dialog.findViewById(R.id.web_view);
webView.loadUrl("file:///android_asset/trigger_loc_maps_help.html");
CheckBox checkBox = (CheckBox) dialog.findViewById(R.id.check_do_not_show);
checkBox.setChecked(shouldSkipToolTip());
checkBox.setOnCheckedChangeListener(
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked) {
SharedPreferences pref = LocTrigMapsActivity.this
.getSharedPreferences(
TOOL_TIP_PREF_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = pref.edit();
editor.putBoolean(KEY_TOOL_TIP_DO_NT_SHOW, isChecked);
editor.commit();
}
});
Button button = (Button) dialog.findViewById(R.id.button_close);
button.setTag(dialog);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Object tag = v.getTag();
if(tag != null && tag instanceof Dialog) {
((Dialog) tag).dismiss();
}
}
});
}
@Override
public void onDestroy() {
Log.v(TAG, "Maps: onDestroy");
if(mSearchTask != null) {
mSearchTask.cancel(true);
mSearchTask = null;
}
stopGPS();
mOverlayItems.setFocus(null);
mOverlayItems = null;
mDb.close();
mDb = null;
System.gc();
super.onDestroy();
}
@Override
public void onStop() {
Log.v(TAG, "Maps: onStop");
stopGPS();
super.onStop();
}
/* Exit the maps activity */
private void exitMaps() {
finish();
}
/* Notify the service when the location list changes */
private void notifyService() {
Intent i = new Intent(this, LocTrigService.class);
i.setAction(LocTrigService.ACTION_UPDATE_LOCATIONS);
startService(i);
}
/* Handle the address search result. Display
* the 'add location' balloon
*/
private void handleSearchResult(Address adr) {
mSearchTask = null;
GeoPoint gp = new GeoPoint((int) (adr.getLatitude() * 1E6),
(int) (adr.getLongitude() * 1E6));
String addrText = "";
int addrLines = adr.getMaxAddressLineIndex();
for (int i=0; i<Math.min(2, addrLines); i++) {
addrText += adr.getAddressLine(i) + "\n";
}
mMapView.getController().setZoom(mMapView.getMaxZoomLevel());
mAddLocBalloon.show(gp, getString(R.string.add_this_loc), addrText);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
boolean ret = super.onCreateOptionsMenu(menu);
menu.add(0, MENU_MY_LOC_ID, 0, R.string.menu_my_loc)
.setIcon(android.R.drawable.ic_menu_mylocation);
menu.add(0, MENU_SEARCH_ID, 1, R.string.menu_search)
.setIcon(android.R.drawable.ic_menu_search);
menu.add(0, MENU_HELP_ID, 4, R.string.menu_help)
.setIcon(android.R.drawable.ic_menu_help);
return ret;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
boolean ret = super.onPrepareOptionsMenu(menu);
menu.removeItem(MENU_SATELLITE_ID);
int txt = R.string.trigger_loc_menu_satellite_mode;
if(mMapView.isSatellite()) {
txt = R.string.trigger_loc_menu_map_mode;
}
menu.add(0, MENU_SATELLITE_ID, 3, txt)
.setIcon(android.R.drawable.ic_menu_mapmode);
return ret;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case MENU_MY_LOC_ID: //Show my location
if(mOverlayItems.getCurrentLocOverlay() != null) {
mOverlayItems.removeOverlay(CURR_LOC_ID);
mMapView.invalidate();
}
List<String> provs = mLocMan.getProviders(true);
if(provs == null || provs.size() == 0) {
Toast.makeText(this, R.string.trigger_loc_location_providers_disabled, Toast.LENGTH_SHORT)
.show();
return true;
}
Toast.makeText(this,
R.string.determining_loc, Toast.LENGTH_SHORT).show();
//Start GPS
getGPSSamples();
return true;
case MENU_SEARCH_ID: //Search an address
//get the address
TrigTextInput ti = new TrigTextInput(this);
ti.setTitle(getString(R.string.search_addr_title));
ti.setPositiveButtonText(getString(R.string.menu_search));
ti.setNegativeButtonText(getString(R.string.cancel));
ti.setAllowEmptyText(false);
ti.setOnClickListener(new TrigTextInput.onClickListener() {
@Override
public void onClick(TrigTextInput ti, int which) {
if(which == TrigTextInput.BUTTON_POSITIVE) {
//Start the search task
mSearchTask = new SearchAddressTask()
.execute(ti.getText());
}
}
});
ti.showDialog()
.setOwnerActivity(this);
return true;
case MENU_SATELLITE_ID:
mMapView.setSatellite(!mMapView.isSatellite());
mMapView.invalidate();
return true;
case MENU_HELP_ID: //Show help
showHelpDialog();
return true;
}
return super.onOptionsItemSelected(item);
}
/*
* Set dynamic help on the title
*/
private void updateTitleText() {
MapsOverlayItem item = mOverlayItems.getFocus();
String categName = mDb.getCategoryName(mCategId);
//Default title help text
String title = getString(R.string.maps_tile_default, categName);
if(mOverlayItems.size() == 0) {
//No overlay items (including blue circle) present
//retain the default title
}
else if(item != null && item.locationId != CURR_LOC_ID) {
//a red marker is in focus
title = getString(R.string.maps_title_focused);
}
else if(mOverlayItems.getCurrentLocOverlay() != null) {
//no focus and blue circle is present
title = getString(R.string.maps_title_myloc, categName);
}
else if(item == null) {
//no focus and there are markers present
title = getString(R.string.maps_title_nofocus);
}
TextView header = (TextView) findViewById(R.id.text_maps_header);
header.setText(title);
}
@Override
protected boolean isRouteDisplayed() {
return false;
}
//Suppress warning related to unparameterized ItemizedOverlay
@SuppressWarnings("unchecked")
@Override
public void onFocusChanged(ItemizedOverlay overlay,
OverlayItem focused) {
//Update dynamic title text when the overlay focus changes
updateTitleText();
}
/* Stop GPS */
private void stopGPS() {
Log.v(TAG, "Maps: stopGPS");
mLocMan.removeUpdates(this);
//Remove the GPS timeout timer
if(mGpsTimeoutPI != null) {
mAlarmMan.cancel(mGpsTimeoutPI);
mGpsTimeoutPI.cancel();
mGpsTimeoutPI = null;
}
}
/* Get GPS samples. Also, set GPS timeout alarm */
private void getGPSSamples() {
Log.v(TAG, "Maps: getGPSSamples");
BroadcastReceiver bRecr = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(ACTION_ALRM_GPS_TIMEOUT)) {
stopGPS();
}
}
};
IntentFilter intFilter = new IntentFilter(ACTION_ALRM_GPS_TIMEOUT);
registerReceiver(bRecr, intFilter);
if(mGpsTimeoutPI != null) {
mAlarmMan.cancel(mGpsTimeoutPI);
mGpsTimeoutPI.cancel();
}
mLatestLoc = null;
mLocMan.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 0, 0, this);
//Use network location as well
mLocMan.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER, 0, 0, this);
Intent intent = new Intent(ACTION_ALRM_GPS_TIMEOUT);
mGpsTimeoutPI = PendingIntent.getBroadcast(this, 0, intent,
PendingIntent.FLAG_CANCEL_CURRENT);
mAlarmMan.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + MY_LOC_SAMPLE_TIMEOUT,
mGpsTimeoutPI);
}
/* Handle long press on maps. Display the 'add location' balloon */
private void handleLongPress(Point p) {
Log.v(TAG, "Maps: Handling long press");
GeoPoint gp = mMapView.getProjection().fromPixels(p.x, p.y);
mAddLocBalloon.show(gp, getString(R.string.add_this_loc), "");
}
/* Handle tap on maps. If tapped on 'my location' overlay,
* display 'add my location' balloon
*/
private void handleMarkerTap(int locId) {
Log.v(TAG, "Maps: Hanlding overlay tap");
if(locId == CURR_LOC_ID) {
//Stop the GPS, no need of any more samples
stopGPS();
GeoPoint gp = mOverlayItems.getCurrentLocOverlay().getPoint();
mOverlayItems.removeOverlay(CURR_LOC_ID);
mAddLocBalloon.show(gp, getString(R.string.add_my_loc), "");
//Update help text
updateTitleText();
}
}
/* Handle long press on a marker. Display the 'delete' message */
private void handleMarkerLongPress(int locId) {
Log.v(TAG, "Maps: Hanlding overlay long press");
if(locId != CURR_LOC_ID) {
new DeleteLocDialog(locId).show(this);
}
}
/* GPS location changed callback. Display the blue marker */
@Override
public void onLocationChanged(Location loc) {
Log.v(TAG, "Maps: new location received: " +
loc.getLatitude() + ", " +
loc.getLongitude() + " (" +
loc.getProvider() + "), accuracy = " +
loc.getAccuracy() + ", speed = " +
loc.getSpeed());
//Check to see if the activity has exited. This callback might get
//invoked even after that. A boolean flag can be used here, but this
//variable is anyway set to null on exit. So, its better to make use of it
if(mDb == null) {
return;
}
//Use this loc only if it more accurate than the last one.
//If the most accurate one is more than MY_LOC_EXPIRY old,
//discard it.
if(mLatestLoc != null) {
if(loc.getAccuracy() > mLatestLoc.getAccuracy()) {
long dTime = loc.getTime() - mLatestLoc.getTime();
if(dTime < MY_LOC_EXPIRY) {
return;
}
}
}
mLatestLoc = new Location(loc);
GeoPoint currLoc = new GeoPoint((int)(loc.getLatitude() * 1E6),
(int) (loc.getLongitude() * 1E6));
boolean currOverlayPresent = (mOverlayItems.getCurrentLocOverlay() == null) ? false :
true;
MapsOverlayItem currLocItem = new MapsOverlayItem(currLoc, CURR_LOC_ID,
loc.getAccuracy());
mOverlayItems.setCurrentLocOverlay(currLocItem);
mMapView.invalidate();
//Animate to my location only the first time
if(!currOverlayPresent) {
mAddLocBalloon.hide();
mMapView.getController().animateTo(currLoc);
mMapView.getController().setZoom(mMapView.getMaxZoomLevel());
mOverlayItems.setFocus(null);
updateTitleText();
}
}
@Override
public void onProviderDisabled(String prov) {
}
@Override
public void onProviderEnabled(String arg0) {
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
if(provider.equals(LocationManager.GPS_PROVIDER)) {
switch(status) {
case LocationProvider.OUT_OF_SERVICE:
Toast.makeText(this, R.string.gps_out,
Toast.LENGTH_SHORT).show();
break;
case LocationProvider.TEMPORARILY_UNAVAILABLE:
break;
}
}
}
/* Display a message */
private void displayMessage(String cName, int resId) {
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle(R.string.loc_overlap_title)
.setMessage(getString(resId, cName))
.setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.create();
dialog.show();
dialog.setOwnerActivity(this);
}
/* Check if a location overlaps a location is another category
* Returns the name of the overlapping category. Returns null
* otherwise
*/
private String chekLocOverlap(int categId, GeoPoint gp, float radius) {
String cName = null;
Cursor c = mDb.getAllLocations();
if(c.moveToFirst()) {
do {
int cId = c.getInt(c.getColumnIndexOrThrow(
LocTrigDB.KEY_CATEGORY_ID));
if(cId != categId) {
int lat = c.getInt(c.getColumnIndexOrThrow(
LocTrigDB.KEY_LAT));
int lng = c.getInt(c.getColumnIndexOrThrow(
LocTrigDB.KEY_LONG));
float locr = c.getFloat(c.getColumnIndexOrThrow(
LocTrigDB.KEY_RADIUS));
float[] dist = new float[1];
Location.distanceBetween(gp.getLatitudeE6() / 1E6,
gp.getLongitudeE6() / 1E6,
lat / 1E6,
lng / 1E6,
dist);
//Check overlap
if(dist[0] < locr + radius + LocTrigConfig.MIN_LOC_GAP) {
cName = mDb.getCategoryName(cId);
break;
}
}
} while(c.moveToNext());
}
c.close();
//return the name of overlapping category
return cName;
}
@Override
/* Callback from add location balloon */
public void onAddLocationClick(GeoPoint gp) {
mAddLocBalloon.hide();
//Add this location with default radius. But before that,
//check if it is too close to any other category
String cName = chekLocOverlap(mCategId, gp,
LocTrigConfig.LOC_RADIUS_DEFAULT);
if(cName != null) {
displayMessage(cName, R.string.loc_overlap_msg);
return;
}
int locId = mDb.addLocation(mCategId, gp,
LocTrigConfig.LOC_RADIUS_DEFAULT);
MapsOverlayItem newLocItem = new MapsOverlayItem(gp, locId,
LocTrigConfig.LOC_RADIUS_DEFAULT);
mOverlayItems.addOverlay(newLocItem);
mOverlayItems.setFocus(newLocItem);
mMapView.invalidate();
notifyService();
}
private static Drawable boundBottomLeft(Drawable marker) {
marker.setBounds(0, -marker.getIntrinsicHeight(),
marker.getIntrinsicWidth(), 0);
return marker;
}
/******************************* INNER CLASSES ********************************/
/* The class to represent an overlay item. Stores the
* location details.
*/
private class MapsOverlayItem extends OverlayItem {
public int locationId = 0;
public GeoPoint point;
public float radius = 0;
public MapsOverlayItem(GeoPoint point, int locId, float radius) {
super(point, "", "");
this.point = point;
this.locationId = locId;
this.radius = radius;
}
}
/* The class representing all the overlay items
* Implements functions to handle touch and gestures
*/
private class MapsOverlay extends ItemizedOverlay<MapsOverlayItem> {
//Drag gesture threshold
private static final int DRAG_THREHOLD = 16; //pixels
//Long press action delay
private static final int LONG_PRESS_DELAY = 500; //ms
//Alpha value for light circles
private static final int ALPHA_LIGHT = 20;
//Alpha value for dark circles
private static final int ALPHA_DARK = 50;
//Overlay list
private final ArrayList<MapsOverlayItem> mOverlays =
new ArrayList<MapsOverlayItem>();
//Flag to check if the user if touching the screen
private boolean mTouching = false;
//Flag to check if a drag action is being performed
private boolean mDragging = false;
//The number of runnables posted while a touch down even is
//received. Only the last runnable needs to handled.
private int mPendingRunnables = 0;
//The point which was touched
private final Point mTouchPoint = new Point();
//The point where a drag operation is being performed
private final Point mDragPoint = new Point();
//Flag to check if the circle around a marker is being resized
private boolean mCircleResizing = false;
//The radius if a circle being resized
private float mDragItemRadius = LocTrigConfig.LOC_RADIUS_DEFAULT;
//Index of 'my location' (blue marker) overlay in the list
private int mMyLocOverlayIndex = -1;
//The marker drawables
private final Drawable mBluMarker = getResources()
.getDrawable(R.drawable.trigger_loc_marker_blue);
private final Drawable mRedMarker = getResources()
.getDrawable(R.drawable.trigger_loc_marker_red);
//Thread handler (for posting runnables)
private final Handler mHandler = new Handler();
public MapsOverlay(Drawable defaultMarker) {
super(boundBottomLeft(defaultMarker));
populate();
}
@Override
protected MapsOverlayItem createItem(int i) {
return mOverlays.get(i);
}
@Override
public int size() {
return mOverlays.size();
}
/* Handle touch event to detect long press, tap and drag */
@Override
public boolean onTouchEvent(MotionEvent ev, MapView view) {
//Log.v(DEBUG_TAG, "Maps: onTouchEvent: " + ev.getAction());
Point currP = new Point((int) ev.getX(), (int) ev.getY());
/* State machine for handling touch events.
Long press is handled by posting a delayed
runnable to this thread. If the action is still
'down' when the runnable is run, it is a long press.
The variable 'pendingRunnables' is used to ignore
unwanted runs due to previous 'down' events */
//Runnable for detecting long press
Runnable runLongpress = new Runnable() {
@Override
public void run() {
//If the user hasn't lifted the finger, invoke the
//long press handler
if(mTouching && mPendingRunnables == 1) {
//set 'touching' to false to avoid a tap
//event
mTouching = false;
int hitIndex = hitTestMarker(mTouchPoint);
if(hitIndex != -1) {
//Long pressed a red marker
handleMarkerLongPress(mOverlays.get(hitIndex).locationId);
}
else if(hitTestOverlayCircle(mTouchPoint) != null) {
//Long pressed a red circle
if(!mDragging && hitTestOverlayCircle(mTouchPoint) != null) {
mDragging = true;
handleCircleDragStart(mTouchPoint);
}
}
else {
//Long pressed elsewhere
handleLongPress(mTouchPoint);
}
}
mPendingRunnables--; //Handle only the last runnable
}
};
//Check if the drag ended
if(ev.getAction() != MotionEvent.ACTION_MOVE && mDragging) {
mDragging = false;
handleCircleDragEnd(currP);
return true;
}
//Check for other events
switch(ev.getAction()) {
case MotionEvent.ACTION_DOWN: //Press down
//Set the flag and post the runnable for long press
mTouching = true;
mTouchPoint.set((int) ev.getX(), (int) ev.getY());
//wait for long press
mPendingRunnables++;
mHandler.postDelayed(runLongpress, LONG_PRESS_DELAY);
break;
case MotionEvent.ACTION_UP: //Lift finger
if(mTouching) {
mTouching = false;
//remove the balloon
if(mAddLocBalloon != null) {
mAddLocBalloon.hide();
}
//Check if it is a 'tap' action on the overlays
int hitIndex = hitTestMarker(mTouchPoint);
if(hitIndex != -1) {
handleMarkerTap(mOverlays.get(hitIndex).locationId);
}
//Prevent marker from going out of focus when tapping
//on the circle
else if(hitTestOverlayCircle(mTouchPoint) != null) {
return true;
}
}
break;
case MotionEvent.ACTION_MOVE: //Drag
if(mDragging) {
//The circle is being dragged
handleCircleDrag(currP);
return true;
}
//Clear the 'touching' flag only after dragging beyond
//a threshold
if(mTouching) {
if(euclidDist(currP, mTouchPoint) > DRAG_THREHOLD) {
mTouching = false;
}
}
break;
default:
mTouching = false;
break;
}
return false;
}
/* Check if a point hits a red circle.
* Returns the overlay item in that case.
* Returns null otherwise.
*/
private MapsOverlayItem hitTestOverlayCircle(Point p) {
MapsOverlayItem item = mOverlayItems.getFocus();
if(item == null || item.locationId == CURR_LOC_ID) {
//No red circle is visible
return null;
}
Point markerP = mMapView.getProjection().toPixels(
item.getPoint(), null);
float rInPixels = mMapView.getProjection().metersToEquatorPixels(item.radius);
if(euclidDist(p, markerP) > rInPixels) {
return null;
}
return item;
}
/* Test if a point belongs to any of the markers.
* If the test if positive, returns the index of
* the marker in the list. Otherwise, returns -1.
*/
private int hitTestMarker(Point p) {
//Check all the markers for a hit
for(int i=0; i<mOverlays.size(); i++) {
MapsOverlayItem item = mOverlays.get(i);
GeoPoint ovGp = item.getPoint();
Point ovP = mMapView.getProjection().toPixels(ovGp, null);
RectF markerRect = new RectF();
int w;
int h;
if(i == mMyLocOverlayIndex) {
w = mBluMarker.getIntrinsicWidth();
h = mBluMarker.getIntrinsicHeight();
markerRect.set(-w/2 - 5, -h/2 - 5, w/2 + 5, h/2 + 5);
}
else {
w = mRedMarker.getIntrinsicWidth();
h = mRedMarker.getIntrinsicHeight();
markerRect.set(0, -h, w, 0);
}
markerRect.offset(ovP.x, ovP.y);
if(markerRect.contains(p.x, p.y)) {
return i;
}
}
return -1;
}
/* Calculate the euclidian distance between two points */
private float euclidDist(Point a, Point b) {
int dx = a.x - b.x;
int dy = a.y - b.y;
return FloatMath.sqrt(dx * dx + dy * dy);
}
/* Draw a circle */
private void drawCircle(Canvas c, int color, GeoPoint center,
float radius, int alpha, boolean border) {
Point scCoord = mMapView.getProjection().toPixels(center, null);
float r = mMapView.getProjection().metersToEquatorPixels(radius);
Paint p = new Paint();
p.setStyle(Style.FILL);
p.setColor(color);
p.setAlpha(alpha);
p.setAntiAlias(true);
c.drawCircle(scCoord.x, scCoord.y, r, p);
if(border) { //Draw border
p.setStyle(Style.STROKE);
p.setColor(color);
p.setAlpha(100);
p.setStrokeWidth(1.2F);
p.setAntiAlias(true);
c.drawCircle(scCoord.x, scCoord.y,r, p);
}
}
/* The overridden draw method of the overlay.
* This method is overridden to draw the circle when the
* overlay is in focus
*/
@Override
public void draw(Canvas canvas, MapView mMapView, boolean shadow) {
MapsOverlayItem item = getFocus();
//Draw circle if a red marker is focused
if(item != null && item.locationId != CURR_LOC_ID) {
//Draw dark circle while resizing
int alpha = mCircleResizing ? ALPHA_DARK : ALPHA_LIGHT;
boolean border = mCircleResizing ? true : false;
drawCircle(canvas, Color.RED, item.getPoint(),
item.radius, alpha, border);
}
//Draw blue circle (accuracy) if my location dot is present
if(mMyLocOverlayIndex != -1) {
MapsOverlayItem currItem = getItem(mMyLocOverlayIndex);
if(currItem.radius > 0) {
drawCircle(canvas, Color.BLUE, currItem.getPoint(),
currItem.radius, ALPHA_LIGHT, true);
}
}
super.draw(canvas, mMapView, shadow);
}
/* The focused red circle is about to get resized */
private void handleCircleDragStart(Point p) {
Log.v(TAG, "Maps: Drag Start");
MapsOverlayItem item = getFocus();
if(item == null) {
return;
}
//Backup the old radius (needed if the resize fails)
mDragItemRadius = item.radius;
mCircleResizing = true;
mDragPoint.set(p.x, p.y);
mMapView.invalidate();
}
/* The focused red circle is being resized */
private void handleCircleDrag(Point p) {
//Log.v(DEBUG_TAG, "Maps: Dragging");
if(!mCircleResizing) {
return;
}
MapsOverlayItem item = getFocus();
if(item == null) {
return;
}
if(euclidDist(mDragPoint, p) < DRAG_THREHOLD) {
return;
}
//If the resize radius falls within allowed range, update
//the radius of the overlay
GeoPoint gp = mMapView.getProjection().fromPixels(p.x, p.y);
float dist[] = new float[1];
Location.distanceBetween(item.point.getLatitudeE6() / 1E6,
item.point.getLongitudeE6() / 1E6,
gp.getLatitudeE6() / 1E6,
gp.getLongitudeE6() / 1E6, dist);
if(dist[0] <= LocTrigConfig.LOC_RADIUS_MAX &&
dist[0] >= LocTrigConfig.LOC_RADIUS_MIN) {
item.radius = dist[0];
}
mMapView.invalidate();
}
/* Handle circle resize end */
private void handleCircleDragEnd(Point p) {
Log.v(TAG, "Maps: Drag End");
mCircleResizing = false;
MapsOverlayItem item = getFocus();
if(item == null) {
return;
}
String cName = chekLocOverlap(mCategId, item.point, item.radius);
if(cName != null) {
//Overlapping, restore the radius
item.radius = mDragItemRadius;
displayMessage(cName, R.string.loc_too_big_msg);
}
else {
//Everything ok, update the radius in the db and notify service
mDb.updateLocationRadius(item.locationId, item.radius);
notifyService();
}
mMapView.invalidate();
}
/* Add a new overlay item */
public void addOverlay(MapsOverlayItem overlay) {
mOverlays.add(overlay);
populate();
}
/* Remove an overlay item */
public void removeOverlay(int locId) {
if(locId == CURR_LOC_ID) {
mOverlays.remove(mMyLocOverlayIndex);
mMyLocOverlayIndex = -1;
}
else {
for(int i=0; i<mOverlays.size(); i++) {
if(mOverlays.get(i).locationId == locId) {
mOverlays.remove(i);
break;
}
}
if(mMyLocOverlayIndex > 0) {
mMyLocOverlayIndex--;
}
}
//Work around for platform bug
setLastFocusedIndex(-1);
populate();
}
/* Add the 'my location' (blue marker) overlay.
* This will add a new item to the overlay list if there
* is not 'my location' overlay item in the list. If there is
* one, it will be replaced.
*/
public void setCurrentLocOverlay(MapsOverlayItem overlay) {
overlay.setMarker(boundCenter(mBluMarker));
if(mMyLocOverlayIndex == -1) {
if(mOverlays.add(overlay)) {
mMyLocOverlayIndex = mOverlays.size() - 1;
}
}
else {
mOverlays.set(mMyLocOverlayIndex, overlay);
}
populate();
}
/*
* Return 'my location' overlay item if present
*/
public MapsOverlayItem getCurrentLocOverlay() {
if(mMyLocOverlayIndex != -1) {
return mOverlays.get(mMyLocOverlayIndex);
}
return null;
}
}
/* The class to display the delete message and delete the
* overlay item (location) is required
*/
private class DeleteLocDialog
implements DialogInterface.OnClickListener{
private final int mLocId;
public DeleteLocDialog(int locId) {
this.mLocId = locId;
}
/* Display the dialog */
public void show(Context context) {
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(R.string.exisiting_loc)
.setMessage(R.string.delete_loc)
.setPositiveButton(R.string.delete, this)
.setNegativeButton(R.string.cancel, this)
.create();
dialog.show();
dialog.setOwnerActivity(LocTrigMapsActivity.this);
}
/* Delete the location */
private void deleteLocation() {
mDb.removeLocation(mLocId);
mOverlayItems.removeOverlay(mLocId);
mOverlayItems.setFocus(null);
mMapView.invalidate();
}
@Override
public void onClick(DialogInterface dialog, int which) {
if(which == AlertDialog.BUTTON_POSITIVE) {
deleteLocation();
notifyService();
}
dialog.dismiss();
}
}
/* The search address task class. Performs address search in the bg thread */
private class SearchAddressTask extends AsyncTask<String, Void, Address> {
private ProgressDialog mBusyPrg = null;
@Override
protected void onPreExecute() {
//Start progress bar
mBusyPrg = ProgressDialog.show(LocTrigMapsActivity.this, "",
getString(R.string.searching_msg), true);
mBusyPrg.setOwnerActivity(LocTrigMapsActivity.this);
}
@Override
protected Address doInBackground(String... params) {
//Search the address
Geocoder geoCoder = new Geocoder(LocTrigMapsActivity.this,
Locale.getDefault());
for(int i = 0; i < GEOCODING_RETRIES; i++) {
try {
List<Address> addrs = geoCoder.getFromLocationName(params[0], 5);
if(addrs.size() > 0) {
return addrs.get(0);
}
}
catch (Exception e) {
}
try {
Thread.sleep(GEOCODING_RETRY_INTERVAL);
} catch (InterruptedException e) {
}
}
return null;
}
@Override
protected void onPostExecute(Address adr) {
//Address search done, kill progressbar and notify
if(mBusyPrg != null) {
mBusyPrg.cancel();
mBusyPrg = null;
}
if(adr != null && adr.hasLongitude() && adr.hasLatitude()) {
handleSearchResult(adr);
}
else {
Toast.makeText(getApplicationContext(),
getString(R.string.search_fail),
Toast.LENGTH_SHORT).show();
}
}
}
}