/*
* Copyright 2012 osmdroid authors
* Copyright 2013 Hannes Janetzek
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see .
*/
package org.oscim.layers.overlay;
// TODO
// - need to sort items back to front for rendering
// - and to make this work for multiple overlays
// a global scenegraph is probably required.
import org.oscim.core.MapPosition;
import org.oscim.core.MercatorProjection;
import org.oscim.core.PointD;
import org.oscim.core.Tile;
import org.oscim.layers.overlay.OverlayItem.HotspotPlace;
import org.oscim.renderer.GLRenderer.Matrices;
import org.oscim.renderer.layer.SymbolLayer;
import org.oscim.renderer.overlays.BasicOverlay;
import org.oscim.utils.GeometryUtils;
import org.oscim.view.MapView;
import android.content.res.Resources;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
/* @author Marc Kurtz
* @author Nicolas Gramlich
* @author Theodore Hong
* @author Fred Eisele
* @author Hannes Janetzek
* */
/**
* 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 -
* ...
*/
public abstract class ItemizedOverlay
- extends Overlay implements
Overlay.Snappable {
//private final static String TAG = ItemizedOverlay.class.getName();
protected final Drawable mDefaultMarker;
protected boolean mDrawFocusedItem = true;
class InternalItem {
InternalItem next;
Item item;
boolean visible;
boolean changes;
float x, y;
double px, py;
}
/* package */InternalItem mItems;
/* package */Object lock = new Object();
/* package */Item mFocusedItem;
/* package */boolean mUpdate;
private int mSize;
class ItemOverlay extends BasicOverlay {
private final SymbolLayer mSymbolLayer;
private final float[] mBox = new float[8];
public ItemOverlay(MapView mapView) {
super(mapView);
mSymbolLayer = new SymbolLayer();
}
// note: this is called from GL-Thread. so check your syncs!
@Override
public synchronized void update(MapPosition pos, boolean changed, Matrices m) {
if (!changed && !mUpdate)
return;
mUpdate = false;
double mx = pos.x;
double my = pos.y;
double scale = Tile.SIZE * pos.scale;
int changesInvisible = 0;
int changedVisible = 0;
int numVisible = 0;
mMapView.getMapViewPosition().getMapViewProjection(mBox);
synchronized (lock) {
if (mItems == null) {
if (layers.textureLayers != null) {
layers.clear();
newData = true;
}
return;
}
// check visibility
for (InternalItem it = mItems; it != null; it = it.next) {
it.x = (float) ((it.px - mx) * scale);
it.y = (float) ((it.py - my) * scale);
it.changes = false;
if (!GeometryUtils.pointInPoly(it.x, it.y, mBox, 8, 0)) {
if (it.visible) {
it.changes = true;
changesInvisible++;
}
continue;
}
if (!it.visible) {
it.visible = true;
changedVisible++;
}
numVisible++;
}
//Log.d(TAG, numVisible + " " + changedVisible + " " + changesInvisible);
// only update when zoomlevel changed, new items are visible
// or more than 10 of the current items became invisible
if ((numVisible == 0) && (changedVisible == 0 && changesInvisible < 10))
return;
// keep position for current state
// updateMapPosition();
mMapPosition.copy(pos);
layers.clear();
for (InternalItem it = mItems; it != null; it = it.next) {
if (!it.visible)
continue;
if (it.changes) {
it.visible = false;
continue;
}
int state = 0;
if (mDrawFocusedItem && (mFocusedItem == it.item))
state = OverlayItem.ITEM_STATE_FOCUSED_MASK;
Drawable marker = it.item.getDrawable();
if (marker == null)
marker = mDefaultMarker;
// if (item.getMarker(state) == null) {
// OverlayItem.setState(mDefaultMarker, state);
// marker = mDefaultMarker;
// } else
// marker = item.getMarker(state);
mSymbolLayer.addDrawable(marker, state, it.x, it.y);
}
}
mSymbolLayer.prepare();
layers.textureLayers = mSymbolLayer;
newData = true;
}
}
/**
* Method by which subclasses create the actual Items. This will only be
* called from populate() we'll cache them for later use.
*
* @param i
* ...
* @return ...
*/
protected abstract Item createItem(int i);
/**
* The number of items in this overlay.
*
* @return ...
*/
public abstract int size();
public ItemizedOverlay(MapView mapView, final Drawable pDefaultMarker) {
super(mapView);
if (pDefaultMarker == null) {
throw new IllegalArgumentException("You must pass a default marker to ItemizedOverlay.");
}
this.mDefaultMarker = pDefaultMarker;
mLayer = new ItemOverlay(mapView);
}
private final PointD mMapPoint = new PointD();
/**
* 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() {
synchronized (lock) {
final int size = size();
mSize = size;
// reuse previous items
InternalItem pool = mItems;
mItems = null;
// flip order to draw in backward cycle, so the items
// with the least index are on the front.
for (int a = 0; a < size; a++) {
InternalItem it;
if (pool != null) {
it = pool;
it.visible = false;
it.changes = false;
pool = pool.next;
} else {
it = new InternalItem();
}
it.next = mItems;
mItems = it;
it.item = createItem(a);
// pre-project points
MercatorProjection.project(it.item.mGeoPoint, mMapPoint);
it.px = mMapPoint.x;
it.py = mMapPoint.y;
}
mUpdate = true;
}
}
/**
* 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) {
synchronized (lock) {
InternalItem item = mItems;
for (int i = mSize - position - 1; i > 0 && item != null; i--)
item = item.next;
if (item != null)
return item.item;
return null;
}
// return mInternalItemList.get(position);
}
// private 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);
}
/**
* 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.
*
* @param drawFocusedItem
* ...
*/
public void setDrawFocusedItem(final boolean drawFocusedItem) {
mDrawFocusedItem = drawFocusedItem;
}
/**
* 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.
*
* @param item
* ...
*/
public void setFocus(final Item item) {
mFocusedItem = item;
}
/**
* @return the currently-focused item, or null if no item is currently
* focused.
*/
public Item getFocus() {
return mFocusedItem;
}
private static final Rect mRect = new Rect();
/**
* 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.
*/
public static 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 static Drawable makeMarker(Resources res, int id, HotspotPlace place) {
Drawable marker = res.getDrawable(id);
if (place == null)
boundToHotspot(marker, HotspotPlace.CENTER);
else
boundToHotspot(marker, place);
return marker;
}
// /**
// * Draw a marker on each of our items. populate() must have been called
// * first.
// *
// * 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.
// *
// * 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.
// *
// * The focused item is always drawn last, which puts it visually on top of
// * the other items.
// *
// * @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
// public void draw(final Canvas canvas, final MapView mapView, final boolean shadow) {
//
// if (shadow) {
// return;
// }
//
// 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.toMapPixels(item.mGeoPoint, mCurScreenCoords);
//
// onDrawItem(canvas, item, mCurScreenCoords);
// }
// }
// /**
// * 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
// * the screen coordinates of the item
// */
// protected void onDrawItem(final Canvas canvas, final Item item, final Point curScreenCoords) {
// int state = 0;
//
// if (mDrawFocusedItem && (mFocusedItem == item))
// state = OverlayItem.ITEM_STATE_FOCUSED_MASK;
//
// Drawable marker;
//
// if (item.getMarker(state) == null)
// marker = getDefaultMarker(state);
// else
// marker = item.getMarker(state);
//
// boundToHotspot(marker, item.getMarkerHotspot());
//
// // draw it
// Overlay.drawAt(canvas, marker, curScreenCoords.x, curScreenCoords.y, false);
// }
}