// Created by plusminus on 23:18:23 - 02.10.2008
package org.mozilla.osmdroid.views.overlay;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.view.MotionEvent;
import org.mozilla.osmdroid.ResourceProxy;
import org.mozilla.osmdroid.views.MapView;
import org.mozilla.osmdroid.views.Projection;
import org.mozilla.osmdroid.views.overlay.OverlayItem.HotspotPlace;
import java.util.ArrayList;
/**
* Draws a list of {@link OverlayItem} as markers to a map. The item with the lowest index is drawn
* as last and therefore the 'topmost' marker. It also gets checked for onTap first. This class is
* generic, because you then you get your custom item-class passed back in onTap().
*
* @param <Item>
* @author Marc Kurtz
* @author Nicolas Gramlich
* @author Theodore Hong
* @author Fred Eisele
*/
public abstract class ItemizedOverlay<Item extends OverlayItem> extends Overlay implements
Overlay.Snappable {
// ===========================================================
// Constants
// ===========================================================
// ===========================================================
// Fields
// ===========================================================
protected final Drawable mDefaultMarker;
private final ArrayList<Item> mInternalItemList;
private final Rect mRect = new Rect();
private final Point mCurScreenCoords = new Point();
protected boolean mDrawFocusedItem = true;
private Item mFocusedItem;
private boolean mPendingFocusChangedEvent = false;
private OnFocusChangeListener mOnFocusChangeListener;
// ===========================================================
// Abstract methods
// ===========================================================
public ItemizedOverlay(final Drawable pDefaultMarker, final ResourceProxy pResourceProxy) {
super(pResourceProxy);
if (pDefaultMarker == null) {
throw new IllegalArgumentException("You must pass a default marker to ItemizedOverlay.");
}
this.mDefaultMarker = pDefaultMarker;
mInternalItemList = new ArrayList<Item>();
}
/**
* Method by which subclasses create the actual Items. This will only be called from populate()
* we'll cache them for later use.
*/
protected abstract Item createItem(int i);
// ===========================================================
// Constructors
// ===========================================================
/**
* The number of items in this overlay.
*/
public abstract int size();
// ===========================================================
// Methods from SuperClass/Interfaces (and supporting methods)
// ===========================================================
/**
* Draw a marker on each of our items. populate() must have been called first.<br/>
* <br/>
* The marker will be drawn twice for each Item in the Overlay--once in the shadow phase, skewed
* and darkened, then again in the non-shadow phase. The bottom-center of the marker will be
* aligned with the geographical coordinates of the Item.<br/>
* <br/>
* The order of drawing may be changed by overriding the getIndexToDraw(int) method. An item may
* provide an alternate marker via its OverlayItem.getMarker(int) method. If that method returns
* null, the default marker is used.<br/>
* <br/>
* The focused item is always drawn last, which puts it visually on top of the other items.<br/>
*
* @param canvas the Canvas upon which to draw. Note that this may already have a transformation
* applied, so be sure to leave it the way you found it
* @param mapView the MapView that requested the draw. Use MapView.getProjection() to convert
* between on-screen pixels and latitude/longitude pairs
* @param shadow if true, draw the shadow layer. If false, draw the overlay contents.
*/
@Override
protected void draw(Canvas c, MapView mapView, boolean shadow) {
if (shadow) {
return;
}
if (mPendingFocusChangedEvent && mOnFocusChangeListener != null)
mOnFocusChangeListener.onFocusChanged(this, mFocusedItem);
mPendingFocusChangedEvent = false;
final Projection pj = mapView.getProjection();
final int size = this.mInternalItemList.size() - 1;
/* Draw in backward cycle, so the items with the least index are on the front. */
for (int i = size; i >= 0; i--) {
final Item item = getItem(i);
pj.toPixels(item.getPoint(), mCurScreenCoords);
onDrawItem(c, item, mCurScreenCoords, mapView.getMapOrientation());
}
}
// ===========================================================
// Methods
// ===========================================================
/**
* Utility method to perform all processing on a new ItemizedOverlay. Subclasses provide Items
* through the createItem(int) method. The subclass should call this as soon as it has data,
* before anything else gets called.
*/
protected final void populate() {
final int size = size();
mInternalItemList.clear();
mInternalItemList.ensureCapacity(size);
for (int a = 0; a < size; a++) {
mInternalItemList.add(createItem(a));
}
}
/**
* Returns the Item at the given index.
*
* @param position the position of the item to return
* @return the Item of the given index.
*/
public final Item getItem(final int position) {
return mInternalItemList.get(position);
}
/**
* Draws an item located at the provided screen coordinates to the canvas.
*
* @param canvas what the item is drawn upon
* @param item the item to be drawn
* @param curScreenCoords
* @param aMapOrientation
*/
protected void onDrawItem(final Canvas canvas, final Item item, final Point curScreenCoords,
final float aMapOrientation) {
final int state = (mDrawFocusedItem && (mFocusedItem == item) ? OverlayItem.ITEM_STATE_FOCUSED_MASK
: 0);
final Drawable marker = (item.getMarker(state) == null) ? getDefaultMarker(state) : item
.getMarker(state);
final HotspotPlace hotspot = item.getMarkerHotspot();
boundToHotspot(marker, hotspot);
// draw it
Overlay.drawAt(canvas, marker, curScreenCoords.x, curScreenCoords.y, false, aMapOrientation);
}
protected Drawable getDefaultMarker(final int state) {
OverlayItem.setState(mDefaultMarker, state);
return mDefaultMarker;
}
/**
* See if a given hit point is within the bounds of an item's marker. Override to modify the way
* an item is hit tested. The hit point is relative to the marker's bounds. The default
* implementation just checks to see if the hit point is within the touchable bounds of the
* marker.
*
* @param item the item to hit test
* @param marker the item's marker
* @param hitX x coordinate of point to check
* @param hitY y coordinate of point to check
* @return true if the hit point is within the marker
*/
protected boolean hitTest(final Item item, final android.graphics.drawable.Drawable marker, final int hitX,
final int hitY) {
return marker.getBounds().contains(hitX, hitY);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e, MapView mapView) {
final Projection pj = mapView.getProjection();
final Rect screenRect = pj.getIntrinsicScreenRect();
final int size = this.size();
for (int i = 0; i < size; i++) {
final Item item = getItem(i);
pj.toPixels(item.getPoint(), mCurScreenCoords);
final int state = (mDrawFocusedItem && (mFocusedItem == item) ? OverlayItem.ITEM_STATE_FOCUSED_MASK
: 0);
final Drawable marker = (item.getMarker(state) == null) ? getDefaultMarker(state)
: item.getMarker(state);
boundToHotspot(marker, item.getMarkerHotspot());
if (hitTest(item, marker, -mCurScreenCoords.x + screenRect.left + (int) e.getX(),
-mCurScreenCoords.y + screenRect.top + (int) e.getY())) {
// We have a hit, do we get a response from onTap?
if (onTap(i)) {
// We got a response so consume the event
return true;
}
}
}
return super.onSingleTapConfirmed(e, mapView);
}
/**
* Override this method to handle a "tap" on an item. This could be from a touchscreen tap on an
* onscreen Item, or from a trackball click on a centered, selected Item. By default, does
* nothing and returns false.
*
* @return true if you handled the tap, false if you want the event that generated it to pass to
* other overlays.
*/
protected boolean onTap(int index) {
return false;
}
/**
* Set whether or not to draw the focused item. The default is to draw it, but some clients may
* prefer to draw the focused item themselves.
*/
public void setDrawFocusedItem(final boolean drawFocusedItem) {
mDrawFocusedItem = drawFocusedItem;
}
/**
* @return the currently-focused item, or null if no item is currently focused.
*/
public Item getFocus() {
return mFocusedItem;
}
/**
* If the given Item is found in the overlay, force it to be the current focus-bearer. Any
* registered {@link ItemizedOverlay#OnFocusChangeListener} will be notified. This does not move
* the map, so if the Item isn't already centered, the user may get confused. If the Item is not
* found, this is a no-op. You can also pass null to remove focus.
*/
public void setFocus(final Item item) {
mPendingFocusChangedEvent = item != mFocusedItem;
mFocusedItem = item;
}
/**
* Adjusts a drawable's bounds so that (0,0) is a pixel in the location described by the hotspot
* parameter. Useful for "pin"-like graphics. For convenience, returns the same drawable that
* was passed in.
*
* @param marker the drawable to adjust
* @param hotspot the hotspot for the drawable
* @return the same drawable that was passed in.
*/
protected synchronized Drawable boundToHotspot(final Drawable marker, HotspotPlace hotspot) {
final int markerWidth = marker.getIntrinsicWidth();
final int markerHeight = marker.getIntrinsicHeight();
mRect.set(0, 0, 0 + markerWidth, 0 + markerHeight);
if (hotspot == null) {
hotspot = HotspotPlace.BOTTOM_CENTER;
}
switch (hotspot) {
default:
case NONE:
break;
case CENTER:
mRect.offset(-markerWidth / 2, -markerHeight / 2);
break;
case BOTTOM_CENTER:
mRect.offset(-markerWidth / 2, -markerHeight);
break;
case TOP_CENTER:
mRect.offset(-markerWidth / 2, 0);
break;
case RIGHT_CENTER:
mRect.offset(-markerWidth, -markerHeight / 2);
break;
case LEFT_CENTER:
mRect.offset(0, -markerHeight / 2);
break;
case UPPER_RIGHT_CORNER:
mRect.offset(-markerWidth, 0);
break;
case LOWER_RIGHT_CORNER:
mRect.offset(-markerWidth, -markerHeight);
break;
case UPPER_LEFT_CORNER:
mRect.offset(0, 0);
break;
case LOWER_LEFT_CORNER:
mRect.offset(0, -markerHeight);
break;
}
marker.setBounds(mRect);
return marker;
}
public void setOnFocusChangeListener(OnFocusChangeListener l) {
mOnFocusChangeListener = l;
}
public static interface OnFocusChangeListener {
void onFocusChanged(ItemizedOverlay<?> overlay, OverlayItem newFocus);
}
}