Monday, January 3, 2011

 

Two great maps that taste great together

I spent yesterday adding OSMDroid support to iTravelFree; here's a little precis of what I learned about letting users swap back and forth between Google Maps and OpenStreetMaps within the same Android Activity.

Why? you might ask: well, two reasons. One, OpenStreetMaps is a pretty cool project. Two, OSMDroid lets you access OpenStreetMaps even while offline, which is pretty awesome for a travel app, and while Google Maps App 5.0 has some offline support, it not you decides which tiles get cached. Mind you, creating and downloading OSM maps to Android is non-trivial, though the next iTravelFree release will make it much easier.

Also, it's just kinda interesting swapping back and forth between GMaps and OSM and seeing the differences between the two.

Anyway: how to do it? With a basic Decorator pattern, ie we roll our own MapView object (in my case, ITRMapView) which wraps both a Google MapView and and OpenStreetMapView, and forwards method calls appropriately depending on its state.

Arguably the best implementation would be via a common ITRMapViewImplementation interface and a concrete class for every map type. (If there were more than two map types, this would no longer really be arguable.) But I implemented it quick-and-dirty, with a bunch of if statements, which does at least have the serendipitous effect of being easy to use as an example.

Anyhoo, without further ado, here's the code:

/**
* Wrapper class which lets a MapActivity switch back and forth between Google Maps and OpenStreetMaps.
*/
public class ITRMapView {
private MapActivity mapActivity;
private BalloonLayout balloon;

private ITRGMapView gmap;
public ITRGMapView getGMap()
{
if (gmap==null) {
gmap = new ITRGMapView(mapActivity, "my_api_key");
gmap.setClickable(true);
gmap.setMapActivity(mapActivity);
gmap.setBuiltInZoomControls(true);
gmapLocation = new com.google.android.maps.MyLocationOverlay(mapActivity, gmap);
balloon = BalloonLayout.GetBalloonFor(mapActivity);
}
return gmap;
}
private com.google.android.maps.MyLocationOverlay gmapLocation;
private ListingsOverlay gmapListingsOverlay;

private OpenStreetMapView osm;
public OpenStreetMapView getOSMMap() { return osm; }
private org.andnav.osm.views.overlay.MyLocationOverlay osmLocation;
private OpenStreetMapViewItemizedOverlay<OpenStreetMapViewOverlayItem> osmListingsOverlay;

private boolean osmActive=false;
public boolean isOSMActive() { return osmActive; }
public void setOSMActive(boolean doOSM) { osmActive=doOSM; }

public ITRMapView(MapActivity activity) {
mapActivity=activity;
}

public void setOSMMap(OpenStreetMapView theOSMMap) {
osm=theOSMMap;
osmLocation = new org.andnav.osm.views.overlay.MyLocationOverlay(mapActivity, osm);
int centerLat = gmap==null ? 0 : gmap.getMapCenter().getLatitudeE6();
int centerLon = gmap==null ? 0 : gmap.getMapCenter().getLongitudeE6();
int zoom = gmap==null ? 10 : gmap.getZoomLevel();
org.andnav.osm.util.GeoPoint center = new org.andnav.osm.util.GeoPoint(centerLat, centerLon);
osm.getController().setCenter(center);
osm.getController().setZoom(zoom);
osm.setUpFor(mapActivity);
}

public void onResume() {
if (gmapLocation!=null) {
gmapLocation.enableMyLocation();
gmapLocation.enableCompass();
}
if (osmLocation!=null) {
osmLocation.enableMyLocation();
osmLocation.enableCompass();
}
}
public void onPause() {
if (gmapLocation!=null) {
gmapLocation.disableMyLocation();
gmapLocation.disableCompass();
}
if (osmLocation!=null) {
osmLocation.disableMyLocation();
osmLocation.disableCompass();
}
}

public void centerAndZoom(int centerLat, int centerLon, int spanLat, int spanLon) {
Log.i(""+this, "Center and zoom: "+centerLat+" "+centerLon+" "+spanLat+" "+spanLon+" osm "+osmActive);
if (osmActive) {
org.andnav.osm.util.GeoPoint center = new org.andnav.osm.util.GeoPoint(centerLat, centerLon);
OpenStreetMapViewController controller = osm.getController();
controller.setCenter(center);

//quick-and-dirty, because zoomToSpan doesn't work
Integer maxSpan = Math.max(spanLat, spanLon);
Double ln = Math.log(maxSpan.doubleValue());
Integer zoomLevel = 20-ln.intValue();
if (zoomLevel>5)
zoomLevel+=2;
if (zoomLevel>18)
zoomLevel=18;
controller.setZoom(zoomLevel);
}
else {
com.google.android.maps.GeoPoint center = new com.google.android.maps.GeoPoint(centerLat, centerLon);
MapController controller = gmap.getController();
controller.setCenter(center);
controller.zoomToSpan(spanLat, spanLon);
}
}

public int getMapCenterLatE6() {
if (osmActive) {
int lat = osm.getMapCenterLatitudeE6();
//Need to clamp to be consistent with GMaps
if (lat<-180000000)
lat+=360000000;
if (lat>180000000)
lat-=360000000;
return lat;
}
return gmap.getMapCenter().getLatitudeE6();
}
public int getMapCenterLonE6() {
if (osmActive) {
int lon = osm.getMapCenterLongitudeE6();
//Need to clamp to be consistent with GMaps
if (lon<-180000000)
lon+=360000000;
if (lon>180000000)
lon-=360000000;
return lon;
}
return gmap.getMapCenter().getLongitudeE6();
}

public void fillMap(ProgressIndicator indicator, HashSet<MappableItem> mCurrentSubset) {
if (osmActive)
fillOSMMap(indicator, mCurrentSubset);
else
fillGMap(indicator, mCurrentSubset);
}
synchronized void fillOSMMap(ProgressIndicator indicator, HashSet<MappableItem> mCurrentSubset) {
if (mCurrentSubset==null)
return;

ArrayList<OpenStreetMapViewOverlayItem> items = new ArrayList<OpenStreetMapViewOverlayItem>();
MappableItem[] toMap = mCurrentSubset.toArray(new MappableItem[mCurrentSubset.size()]);
int listEnd=Math.min(toMap.length, Settings.GetMapMax());
for (int i=0; i<listEnd; i++) {
OpenStreetMapViewOverlayItem newItem = new OpenStreetMapViewOverlayItem(
toMap[i].getIDString(),
toMap[i].getMapDetailText(),
new GeoPoint(toMap[i].getLatitudeE6(), toMap[i].getLongitudeE6()));
newItem.setMarker(toMap[i].getMarker(mapActivity));
items.add(newItem);
if (indicator!=null)
indicator.showProgress(i, listEnd);
}

osmListingsOverlay = new OpenStreetMapViewItemizedOverlay<OpenStreetMapViewOverlayItem>(
mapActivity,items,
new OpenStreetMapViewItemizedOverlay.OnItemGestureListener<OpenStreetMapViewOverlayItem>(){
@Override
public boolean onItemSingleTapUp(int index, OpenStreetMapViewOverlayItem item) {
RelativeLayout rl = (RelativeLayout) osm.getParent();
rl.removeView(balloon);
balloon.disablePointer();
balloon.setUpTapFor(mapActivity, item.getTitle(), item.getSnippet());
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(200, 100);
params.addRule(RelativeLayout.ALIGN_PARENT_TOP);
params.addRule(RelativeLayout.CENTER_HORIZONTAL);
rl.addView(balloon, params);
return true;
}

@Override
public boolean onItemLongPress(int index, OpenStreetMapViewOverlayItem item) {
// Toast.makeText(mapActivity, "Item '" + item.mTitle + "' (index=" + index + ") got long pressed", Toast.LENGTH_LONG).show();
return false;
}
});
}
synchronized void fillGMap(ProgressIndicator indicator, HashSet<MappableItem> mCurrentSubset) {
if (mCurrentSubset==null)
return;

Drawable defaultMarker = mapActivity.getResources().getDrawable(R.drawable.map_fave);
gmapListingsOverlay = new ListingsOverlay(mapActivity, gmap, defaultMarker);
MappableItem[] toMap = mCurrentSubset.toArray(new MappableItem[mCurrentSubset.size()]);
int listEnd=Math.min(toMap.length, Settings.GetMapMax());
for (int i=0; i<listEnd; i++) {
Drawable marker = toMap[i].getMarker(mapActivity);
ListingOverlayItem overlay = toMap[i].getListingOverlayItem();
if (overlay!=null)
gmapListingsOverlay.addItem(overlay, marker);
if (indicator!=null)
indicator.showProgress(i, listEnd);
}
gmapListingsOverlay.doPopulate();
}

public void showListingsOverlay() {
if (osmActive) {
List<OpenStreetMapViewOverlay> mapOverlays = osm.getOverlays();
if (!mapOverlays.contains(osmListingsOverlay))
mapOverlays.add(osmListingsOverlay);
if (!mapOverlays.contains(osmLocation))
mapOverlays.add(osmLocation);
}
else {
List<Overlay> mapOverlays = gmap.getOverlays();
if (!mapOverlays.contains(gmapListingsOverlay))
mapOverlays.add(gmapListingsOverlay);
if (!mapOverlays.contains(gmapLocation))
mapOverlays.add(gmapLocation);
}
postInvalidate();
}

public void postInvalidate() {
if (osmActive)
osm.postInvalidate();
else
gmap.postInvalidate();
}

public int getLatitudeSpan() {
if (osmActive)
return osm.getLatitudeSpanE6();
return gmap.getLatitudeSpan();
}
public int getLongitudeSpan() {
if (osmActive)
return osm.getLongitudeSpanE6();
return gmap.getLongitudeSpan();
}
public int getZoomLevel() {
if (osmActive)
return osm.getZoomLevel();
return gmap.getZoomLevel();
}

}


And how is it used? Well, the MapActivity includes code like this:

private void switchMapMode() {
int lastLat = mapView.getMapCenterLatE6();
int lastLon = mapView.getMapCenterLonE6();
int lastLatSpan = mapView.getLatitudeSpan();
int lastLonSpan = mapView.getLatitudeSpan();
boolean willHandleCenterZoom = lastLat!=0 && lastLon!=0 && lastLatSpan!=0 && lastLonSpan!=0;

if (mapView.isOSMActive())
useGMap(!willHandleCenterZoom);
else
useOSMMap(!willHandleCenterZoom);

if (willHandleCenterZoom)
mapView.centerAndZoom(lastLat, lastLon, lastLatSpan, lastLonSpan);
}
private void useGMap(boolean doCenterZoom) {
mapView.setOSMActive(false);
setContentView(R.layout.map);
//remove map view from old parent, if any
ITRGMapView gmap = mapView.getGMap();
RelativeLayout rl = (RelativeLayout) gmap.getParent();
if (rl!=null)
rl.removeView(gmap);
//add map view to new parent
rl = (RelativeLayout) findViewById(R.id.mapLayout);
rl.addView(mapView.getGMap());
setUpMap(doCenterZoom);
}
private void useOSMMap(boolean doCenterZoom) {
mapView.setOSMActive(true);
setContentView(R.layout.map_osm);
mapView.setOSMMap((ITROSMMapView)findViewById(R.id.mapview_osm));
setUpMap(doCenterZoom);
new FillMapTask().execute();
}
private void setUpMap(boolean doCenterZoom) {
registerForContextMenu(findViewById(R.id.tagButton));
registerForContextMenu(findViewById(R.id.mapButton));
setUpToolbar();
if (doCenterZoom && mCurrentLat!=0 && mCurrentLong!=0 && mCurrentLatSpan!=0 && mCurrentLongSpan!=0)
mapView.centerAndZoom(mCurrentLat, mCurrentLong, mCurrentLatSpan, mCurrentLongSpan);
else if (doCenterZoom && mapView.isOSMActive() && mCurrentLat==0 && mCurrentLong==0)
mapView.centerAndZoom(51000000, 0, 1000000, 1000000);
}


Of some interest, I suppose, are the BalloonLayout and ListingsOverlay classes mentioned above, so:


public class BalloonLayout extends LinearLayout {
private boolean pointerEnabled=true;
public void disablePointer() { pointerEnabled=false; }
public void enablePointer() {pointerEnabled=true; }

public BalloonLayout(Context context) {
super(context);
}

public BalloonLayout(Context context, AttributeSet set) {
super(context, set);
}

@Override
protected void dispatchDraw(Canvas canvas) {

Paint panelPaint = new Paint();
panelPaint.setARGB(0, 0, 0, 0);
RectF baloonRect = new RectF();
baloonRect.set(0,0, getMeasuredWidth(), 2*(getMeasuredHeight()/3));
panelPaint.setARGB(230, 255, 255, 255);
canvas.drawRoundRect(baloonRect, 10, 10, panelPaint);

if (pointerEnabled) {
Path baloonTip = new Path();
baloonTip.moveTo(5*(getMeasuredWidth()/8), 2*(getMeasuredHeight()/3));
baloonTip.lineTo(getMeasuredWidth()/2, getMeasuredHeight());
baloonTip.lineTo(3*(getMeasuredWidth()/4), 2*(getMeasuredHeight()/3));
canvas.drawPath(baloonTip, panelPaint);
}

super.dispatchDraw(canvas);
}

public void setUpTapFor(final Activity activity, String title, String snippet)
{
setVisibility(View.VISIBLE);
TextView titleText = (TextView) findViewById(R.id.detailText);
titleText.setText(snippet);

ImageButton detailButton = (ImageButton) findViewById(R.id.detailGo);
try {
final Long id = Long.valueOf(title);
detailButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Intent intent = new Intent(activity, ListingActivity.class);
intent.putExtra(ListingActivity.KEY_ID, id);
activity.startActivityForResult(intent, 0);
//not pretty!
if (activity instanceof ITRMapActivity)
((ITRMapActivity)activity).editedListingID=id;
}
});
}
catch (NumberFormatException ex) {
ex.printStackTrace();
}

public static BalloonLayout GetBalloonFor(Activity activity) {
LayoutInflater layoutInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final BalloonLayout balloon = (BalloonLayout) layoutInflater.inflate(R.layout.map_balloon, null);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(200,100);
layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
layoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL);
balloon.setLayoutParams(layoutParams);

ImageButton closeButton = (ImageButton) balloon.findViewById(R.id.detailClose);
closeButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
balloon.setVisibility(View.GONE);
}
});
return balloon;
}
}


although I have to work the kinks out of placing the balloon on the OSM map still. As for the ListingsOverlay,


public class ListingsOverlay extends ItemizedOverlay<ListingOverlayItem> {
private MapActivity activity;
private ITRGMapView mapView;
private BalloonLayout balloon;
private ArrayList<ListingOverlayItem> items=new ArrayList<ListingOverlayItem>();
public ArrayList<ListingOverlayItem> getItems() { return items; }

public ListingsOverlay(MapActivity anActivity, ITRGMapView aMapView, android.graphics.drawable.Drawable defaultMarker) {
super(defaultMarker);
activity=anActivity;
mapView=aMapView;
balloon = BalloonLayout.GetBalloonFor(activity);
}

public void addItem(ListingOverlayItem item, Drawable marker) {
if (item!=null && marker!=null && !items.contains(item)) {
super.boundCenterBottom(marker);
item.setMarker(marker);
items.add(item);
}
}

public void doPopulate() {
populate();
setLastFocusedIndex(-1);
}

@Override
protected ListingOverlayItem createItem(int i) {
return(items.get(i));
}

@Override
public int size() {
return(items.size());
}

@Override
protected boolean onTap(int i) {
mapView.removeView(balloon);
balloon.enablePointer();
ListingOverlayItem item = items.get(i);
balloon.setUpTapFor(activity, item.getTitle(), item.getSnippet());
MapView.LayoutParams params = new MapView.LayoutParams(200, 100, item.getPoint(), MapView.LayoutParams.BOTTOM_CENTER);
mapView.addView(balloon, params);
return true;
}

}



I hereby BSD 2.0-release all the above code, if anyone wants to use it - enjoy!

Labels: , , , , ,


Comments:
Thanks for sharing this informative content.,
Leanpitch provides online training in Agile team facilitation during this lockdown period everyone can use it wisely.

Agile team facilitation

ICP ATF

 
Thanks for sharing this informative content.,
Leanpitch provides online training in Agile team facilitation during this lockdown period everyone can use it wisely.
Team facilitator Agile

Agile facilitator
 

Thanks for sharing this informative content.,
Leanpitch provides online training in Agile team facilitation during this lockdown period everyone can use it wisely.
Team facilitator in Agile

ICAGILE ATF

 
Thanks for sharing this informative content.,
Leanpitch provides online training in Agile team facilitation ,everyone can use it wisely.

Agile team facilitation

ICAGILE ATF

 


Thanks for sharing this informative content.,
Leanpitch provides online training in Agile team facilitation ,everyone can use it wisely.
ICP ATF

Agile facilitator

 
Thanks for sharing this informative content.,
Leanpitch provides online training in Agile team facilitation ,everyone can use it wisely.

Team facilitator in Agile

ICP ATF


 
Excellent content ,Thanks for sharing this .,
CSPO certification
CSPO TRAINING
 
This is the best article for reading and gets important information, reading the article is my hobby, but I really like read your article, amazing article thank you very much!!

사설토토
카지노사이트
파워볼
온라인카지노
 
Awesome! Its really amazing post, I have got much clear idea on the topic from this post.

스포츠토토
토토
안전놀이터
토토사이트

 
something that is remarkable and should be learned. Thank you for providing this great info theres so much to learn.

사설토토
카지노
파워볼사이트
바카라사이트

 
Thanks for sharing with us this important Content. I feel strongly about it and really enjoyed learning more about this topic.
카지노사이트

 
Thanks for sharing this marvelous post. I m very pleased to read this article.
카지노사이트

 
Its an amazing website, I really enjoy reading your articles.
스포츠토토

 
Such an amazing and helpful post. I really really love it.
바카라사이트

 
I blog often and I truly appreciate your content.
야설
Feel free to visit my blog :
야설


 
This great article has truly peaked my interest.
일본야동
Feel free to visit my blog : 일본야동

 
I’m going to bookmark your site and keep checking for new details about once per week.
국산야동
Feel free to visit my blog : 국산야동

 
I subscribed to your Feed too.
일본야동
Feel free to visit my blog : 일본야동

 
Hi there! This article could not be written much better!
야설
Feel free to visit my blog : 야설

 
Hello, I'm happy to see some great articles on your site. Would you like to come to my site later? My site also has posts, comments and communities similar to yours. Please visit and take a look 메이저놀이터

 
I can see that you are an expert at your field! I am launching a website soon, and your information will be very useful for me.. Thanks for all your help and wishing you all the success in your business
사설경마

magosucowep

 
토토사이트 I cherished up to you’ll receive performed right here. The caricature is tasteful, your authored subject matter stylish. nevertheless, you command get got an impatience over that you want be delivering the following. sick certainly come more in the past once more as precisely the similar just about very often inside case you shield this hike.


 
Simply want to say your article is as astonishing. The clarity in your post is just great and i could assume you’re an expert on this subject. Well with your permission allow me to grab your RSS feed to keep updated with forthcoming post. Thanks a million and please continue the enjoyable work. 스포츠토토

 
Pretty portion of content. I just stumbled upon your weblog and in accession capital to claim that I acquire actually loved account your blog posts. Anyway I will be subscribing on your augment or even I fulfillment you get entry to constantly quickly. 카지노사이트

 
카지노사이트 You’ve got knowlege about everything that’s why I love this site! Very useful and helpful.. Try browsing my website to gather some information.. Will appreciated it a lot!


 
스포츠토토 Great beat ! I wish to apprentgice at tthe same time as you amend yor site, how can i subscribe for a blog website?
Thee account aided mee a applicable deal. I have been a little bit familiar of this your broadcast provided bright clear concept


 
Many thanks for the article, I have a lot of spray lining knowledge but always learn something new. Keep up the good work and thank you again. 먹튀사이트

 
I've been using WordPress on a number of websites for about a year and am worried about switching to another platform. I have heard good things about keonhacai. Is there a way I can transfer all my wordpress content into it? Any help would be really appreciated!

 

Post a Comment

Subscribe to Post Comments [Atom]





<< Home

This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]