I've been developing an Android Maps app that needs to add some overlays to the map as the user pans the map around. Since it seemed like such an obvious thing to do with maps, I thought there would be a built in listener that would handle that for me. Boy was I wrong. It seems that the Maps API doesn't really have any support for that and so we're left to try to figure out the best approach.
After researching and coding I've written up two ways to listen to see if the user pans the map.
- Set an onTouchListener on the MapView
- Run a listener thread that pings the mapView every so often to see if it's moved
Both approaches use a DistanceCalculator (I forgot where I saw it posted otherwise I would link to it) to see if we've panned enough distance to update the overlays. The code will be at the end of this post.
I prefer the first approach since it seems cleaner, but it does have the drawback that the built in zoom controls won't work with it. But here is the code:
mapView = (MapView) findViewById(R.id.mapview);
//Built in Zoom Controls don't work with the onTouchListener
//mapView.setBuiltInZoomControls(true);
mapView.setSatellite(true);
mapView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int actionId = event.getAction();
if (actionId == MotionEvent.ACTION_UP && userIsPanning) {
userIsPanning = false;
if(mapCenter == null || DistanceCalculator.calculateDistance(mapCenter, mapView.getMapCenter()) > DISTANCE_CHANGE) {
mapCenter = mapView.getMapCenter();
//TODO Update your overlays because the map has moved
Toast.makeText(context, "Panned", Toast.LENGTH_SHORT).show();
}
return true;
} else if (actionId == MotionEvent.ACTION_MOVE) {
userIsPanning = true;
}
return false;
}
}); |
As you can see it simply sets a flag when it detects we're moving and then waits for the ACTION UP to show that the user has removed their finger.
The listener thread has a little more to it and I've noticed it doesn't always work as expected. In the onCreate method in your main class you'll add this:
//updateMapOverlaysThread is declared as a member variable
updateMapOverlaysThread = new UpdateMapOverlaysThread(this, mapView, messageHandler);
new Thread(updateMapOverlaysThread).start(); |
And here is the thread class:
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
class UpdateMapOverlaysThread implements Runnable {
private static final String TAG = "UpdateMapOverlaysThread";
private static final int INTERVAL = 1500;
private static final int PAUSE_INTERVAL = 7500;
private final Double DISTANCE_CHANGE = .01; //in km
private boolean enabled = true;
private boolean interrupted = false;
private int zoomLevel = 0;
private Handler mainThreadMessageHandler = null;
private Context context = null;
private MapView mapView = null;
private GeoPoint mapCenter = null;
/**
* @param context
* @param mapView
*/
public UpdateMapOverlaysThread(Context context, MapView mapView, Handler messageHandler) {
this.context = context;
this.mapView = mapView;
this.mainThreadMessageHandler = messageHandler;
}
@Override
public void run() {
Log.d(TAG, "Start");
boolean fireOverlayUpdater = false;
while(!interrupted) {
if(enabled) {
Log.v(TAG, "zoomLevel: " + zoomLevel + " map zoomLevel: " + mapView.getZoomLevel());
if(zoomLevel != mapView.getZoomLevel() ){
zoomLevel = mapView.getZoomLevel();
fireOverlayUpdater = true;
}
Log.v(TAG, "mapCenter: " + mapCenter + " map mapCenter: " + mapView.getMapCenter());
if(mapCenter == null || DistanceCalculator.calculateDistance(mapCenter, mapView.getMapCenter()) > DISTANCE_CHANGE) {
mapCenter = mapView.getMapCenter();
fireOverlayUpdater = true;
}
if(fireOverlayUpdater) {
Log.d(TAG, "Need to update overlays");
//TODO Update the overlays
// Send the overlays to the UI Thread
// Message message = mainThreadMessageHandler.obtainMessage();
// OverlayItem overlayItem = new OverlayItem(mapView.getMapCenter(), "POINT", "BOOYA");
// message.obj = overlayItem;
// mainThreadMessageHandler.sendMessage(message);
// Reset
fireOverlayUpdater = false;
}
// Wait until time to fire again
try {
Thread.sleep(INTERVAL);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// Wait until your turn to work again
try {
Thread.sleep(PAUSE_INTERVAL);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* @param enabled
*/
public void setEnabled (boolean enabled) {
Log.d(TAG, "setEnabled: " + enabled);
this.enabled = enabled;
}
/**
*
*/
public void killThread() {
Log.d(TAG, "killThread");
interrupted = true;
}
} |
You'll notice that we use a MessageHandler to post the overlays on the map. Here is the Handler defined in the onCreate method in our main class:
//Setup Message Handler
messageHandler = new Handler() {
public void handleMessage(Message msg) {
OverlayItem overlayItem = (OverlayItem)msg.obj;
//TODO Add the overlay to the map
}
}; |
But these are the approaches that I've tried. Has anyone tried a different approach if so how has it worked out?
import com.google.android.maps.GeoPoint;
public class DistanceCalculator {
//earth’s radius (mean radius = 6,371km)
private static final double radius = 6371;
public static double calculateDistance(GeoPoint startP, GeoPoint endP) {
double distance = 0;
if(startP != null && endP != null) {
double lat1 = startP.getLatitudeE6()/1E6;
double lat2 = endP.getLatitudeE6()/1E6;
double lon1 = startP.getLongitudeE6()/1E6;
double lon2 = endP.getLongitudeE6()/1E6;
double dLat = Math.toRadians(lat2-lat1);
double dLon = Math.toRadians(lon2-lon1);
double a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
Math.sin(dLon/2) * Math.sin(dLon/2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
distance = radius * c;
}
return distance;
}
} |
Print This Post
The Introduction
Yesterday I spent a good part of the day setting up the absolutely fantastic framework PhoneGap. If you don't know what PhoneGap is, and you are a web developer, you are truly missing out. The only thing I can compare it to (from a high level) is Adobe Air. Air lets you build a page out of html, css, and js and then renders it in a webkit self-contained browser that you can then package up and distribute on any platform that runs air (Win/Mac/Linux), and yes you can also build Air apps with Flex and Flash. PhoneGap is similar to Air as it too uses html, css, and js to build apps for Android, iOS, WebOS, Blackberry, Symbian, and Nokia.
Now don't let my title fool you, there is a guide (and a pretty good one at that) waiting for you in the PhoneGap Wiki, the problem is that it is now out of date and will cause you grief if you try to follow a few of the steps because they look like they are the right steps to follow. That's why I've created this entry to walk you through the mistakes I blundered through to get it set up and working on my Windows 7 box (My mac died, and my linux lappy is a netbook - not ideal for developing on... plausible, but not ideal).
Print This Post