/*
* Copyright 2010, 2011, 2012 mapsforge.org
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.mapsforge.android.maps.overlay;
import java.util.ArrayList;
import java.util.List;
import org.mapsforge.android.maps.MapView;
import org.mapsforge.android.maps.Projection;
import org.mapsforge.core.model.GeoPoint;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
/**
* ItemizedOverlay is an abstract base class to display {@link OverlayItem OverlayItems}. The class defines some methods
* to access the backing data structure of deriving subclasses. Besides organizing the redrawing process it handles long
* press and tap events and calls {@link #onLongPress(int)} and {@link #onTap(int)} respectively.
*
* @param <Item>
* the type of items handled by this overlay.
*/
public abstract class ItemizedOverlay<Item extends OverlayItem> extends Overlay {
private static final int INITIAL_CAPACITY = 8;
private static final String THREAD_NAME = "ItemizedOverlay";
/**
* Sets the bounds of the given drawable so that (0,0) is the center of the bottom row.
*
* @param balloon
* the drawable whose bounds should be set.
* @return the given drawable with set bounds.
*/
public static Drawable boundCenter(Drawable balloon) {
balloon.setBounds(balloon.getIntrinsicWidth() / -2, balloon.getIntrinsicHeight() / -2,
balloon.getIntrinsicWidth() / 2, balloon.getIntrinsicHeight() / 2);
return balloon;
}
/**
* Sets the bounds of the given drawable so that (0,0) is the center of the bounding box.
*
* @param balloon
* the drawable whose bounds should be set.
* @return the given drawable with set bounds.
*/
public static Drawable boundCenterBottom(Drawable balloon) {
balloon.setBounds(balloon.getIntrinsicWidth() / -2, -balloon.getIntrinsicHeight(),
balloon.getIntrinsicWidth() / 2, 0);
return balloon;
}
private int bottom;
private final Drawable defaultMarker;
private Drawable itemMarker;
private final Point itemPosition;
private int left;
private int right;
private int top;
private List<Integer> visibleItems;
private List<Integer> visibleItemsRedraw;
/**
* @param defaultMarker
* the default marker (may be null).
*/
public ItemizedOverlay(Drawable defaultMarker) {
super();
this.defaultMarker = defaultMarker;
this.itemPosition = new Point();
this.visibleItems = new ArrayList<>(INITIAL_CAPACITY);
this.visibleItemsRedraw = new ArrayList<>(INITIAL_CAPACITY);
}
/**
* Checks whether an item has been long pressed.
*/
@Override
public boolean onLongPress(GeoPoint geoPoint, MapView mapView) {
return checkItemHit(geoPoint, mapView, EventType.LONG_PRESS);
}
/**
* Checks whether an item has been tapped.
*/
@Override
public boolean onTap(GeoPoint geoPoint, MapView mapView) {
return checkItemHit(geoPoint, mapView, EventType.TAP);
}
/**
* @return the numbers of items in this overlay.
*/
public abstract int size();
/**
* Checks whether an item has been hit by an event and calls the appropriate handler.
*
* @param geoPoint
* the point of the event.
* @param mapView
* the {@link MapView} that triggered the event.
* @param eventType
* the type of the event.
* @return true if an item has been hit, false otherwise.
*/
protected boolean checkItemHit(GeoPoint geoPoint, MapView mapView, EventType eventType) {
Projection projection = mapView.getProjection();
Point eventPosition = projection.toPixels(geoPoint, null);
// check if the translation to pixel coordinates has failed
if (eventPosition == null) {
return false;
}
Point checkItemPoint = new Point();
synchronized (this.visibleItems) {
// iterate over all visible items
for (int i = this.visibleItems.size() - 1; i >= 0; --i) {
Integer itemIndex = this.visibleItems.get(i);
// get the current item
Item checkOverlayItem = createItem(itemIndex.intValue());
if (checkOverlayItem == null) {
continue;
}
synchronized (checkOverlayItem) {
// make sure that the current item has a position
if (checkOverlayItem.getPoint() == null) {
continue;
}
checkItemPoint = projection.toPixels(checkOverlayItem.getPoint(), checkItemPoint);
// check if the translation to pixel coordinates has failed
if (checkItemPoint == null) {
continue;
}
// select the correct marker for the item and get the position
Rect checkMarkerBounds;
if (checkOverlayItem.getMarker() == null) {
if (this.defaultMarker == null) {
// no marker to draw the item
continue;
}
checkMarkerBounds = this.defaultMarker.getBounds();
} else {
checkMarkerBounds = checkOverlayItem.getMarker().getBounds();
}
// calculate the bounding box of the marker
int checkLeft = checkItemPoint.x + checkMarkerBounds.left;
int checkRight = checkItemPoint.x + checkMarkerBounds.right;
int checkTop = checkItemPoint.y + checkMarkerBounds.top;
int checkBottom = checkItemPoint.y + checkMarkerBounds.bottom;
// check if the event position is within the bounds of the marker
if (checkRight >= eventPosition.x && checkLeft <= eventPosition.x && checkBottom >= eventPosition.y
&& checkTop <= eventPosition.y) {
switch (eventType) {
case LONG_PRESS:
if (onLongPress(itemIndex.intValue())) {
return true;
}
break;
case TAP:
if (onTap(itemIndex.intValue())) {
return true;
}
break;
}
}
}
}
}
// no hit
return false;
}
/**
* Creates an item in this overlay.
*
* @param index
* the index of the item.
* @return the item.
*/
protected abstract Item createItem(int index);
@Override
protected void drawOverlayBitmap(Canvas canvas, Point drawPosition, Projection projection, byte drawZoomLevel) {
// erase the list of visible items
this.visibleItemsRedraw.clear();
int numberOfItems = size();
for (int itemIndex = 0; itemIndex < numberOfItems; ++itemIndex) {
if (isInterrupted() || sizeHasChanged()) {
// stop working
return;
}
// get the current item
Item overlayItem = createItem(itemIndex);
if (overlayItem == null) {
continue;
}
synchronized (overlayItem) {
// make sure that the current item has a position
if (overlayItem.getPoint() == null) {
continue;
}
// make sure that the cached item position is valid
if (drawZoomLevel != overlayItem.cachedZoomLevel) {
overlayItem.cachedMapPosition = projection.toPoint(overlayItem.getPoint(),
overlayItem.cachedMapPosition, drawZoomLevel);
overlayItem.cachedZoomLevel = drawZoomLevel;
}
// calculate the relative item position on the canvas
this.itemPosition.x = overlayItem.cachedMapPosition.x - drawPosition.x;
this.itemPosition.y = overlayItem.cachedMapPosition.y - drawPosition.y;
// get the correct marker for the item
if (overlayItem.getMarker() == null) {
if (this.defaultMarker == null) {
// no marker to draw the item
continue;
}
this.itemMarker = this.defaultMarker;
} else {
this.itemMarker = overlayItem.getMarker();
}
// get the position of the marker
Rect markerBounds = this.itemMarker.copyBounds();
// calculate the bounding box of the marker
this.left = this.itemPosition.x + markerBounds.left;
this.right = this.itemPosition.x + markerBounds.right;
this.top = this.itemPosition.y + markerBounds.top;
this.bottom = this.itemPosition.y + markerBounds.bottom;
// check if the bounding box of the marker intersects with the canvas
if (this.right >= 0 && this.left <= canvas.getWidth() && this.bottom >= 0
&& this.top <= canvas.getHeight()) {
// set the position of the marker
this.itemMarker.setBounds(this.left, this.top, this.right, this.bottom);
// draw the item marker on the canvas
this.itemMarker.draw(canvas);
// restore the position of the marker
this.itemMarker.setBounds(markerBounds);
// add the current item index to the list of visible items
this.visibleItemsRedraw.add(Integer.valueOf(itemIndex));
}
}
}
// swap the two visible item lists
synchronized (this.visibleItems) {
List<Integer> visibleItemsTemp = this.visibleItems;
this.visibleItems = this.visibleItemsRedraw;
this.visibleItemsRedraw = visibleItemsTemp;
}
}
@Override
protected String getThreadName() {
return THREAD_NAME;
}
/**
* Handles a long press event.
* <p>
* The default implementation of this method does nothing and returns false.
*
* @param index
* the index of the item that has been long pressed.
* @return true if the event was handled, false otherwise.
*/
protected boolean onLongPress(int index) {
return false;
}
/**
* Handles a tap event.
* <p>
* The default implementation of this method does nothing and returns false.
*
* @param index
* the index of the item that has been tapped.
* @return true if the event was handled, false otherwise.
*/
protected boolean onTap(int index) {
return false;
}
/**
* This method should be called after items have been added to the overlay.
*/
protected final void populate() {
super.requestRedraw();
}
}