/* * 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.Paint; import android.graphics.Path; import android.graphics.Point; /** * CircleOverlay is an abstract base class to display {@link OverlayCircle OverlayCircles}. 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. * <p> * The overlay may be used to indicate positions which have a known accuracy, such as GPS fixes. The radius of the * circles is specified in meters and will be automatically converted to pixels at each redraw. * * @param <Circle> * the type of circles handled by this overlay. */ public abstract class CircleOverlay<Circle extends OverlayCircle> extends Overlay { private static final int INITIAL_CAPACITY = 8; private static final String THREAD_NAME = "CircleOverlay"; private final Point circlePosition; private final Paint defaultPaintFill; private final Paint defaultPaintOutline; private final boolean hasDefaultPaint; private final Path path; private List<Integer> visibleCircles; private List<Integer> visibleCirclesRedraw; /** * @param defaultPaintFill * the default paint which will be used to fill the circles (may be null). * @param defaultPaintOutline * the default paint which will be used to draw the circle outlines (may be null). */ public CircleOverlay(Paint defaultPaintFill, Paint defaultPaintOutline) { super(); this.defaultPaintFill = defaultPaintFill; this.defaultPaintOutline = defaultPaintOutline; this.hasDefaultPaint = defaultPaintFill != null || defaultPaintOutline != null; this.circlePosition = new Point(); this.visibleCircles = new ArrayList<>(INITIAL_CAPACITY); this.visibleCirclesRedraw = new ArrayList<>(INITIAL_CAPACITY); this.path = new Path(); } /** * Checks whether a circle has been long pressed. */ @Override public boolean onLongPress(GeoPoint geoPoint, MapView mapView) { return checkItemHit(geoPoint, mapView, EventType.LONG_PRESS); } /** * Checks whether a circle has been tapped. */ @Override public boolean onTap(GeoPoint geoPoint, MapView mapView) { return checkItemHit(geoPoint, mapView, EventType.TAP); } /** * @return the numbers of circles in this overlay. */ public abstract int size(); private void drawPathOnCanvas(Canvas canvas, Circle overlayCircle) { if (overlayCircle.hasPaint) { // use the paints from the current circle if (overlayCircle.paintOutline != null) { canvas.drawPath(this.path, overlayCircle.paintOutline); } if (overlayCircle.paintFill != null) { canvas.drawPath(this.path, overlayCircle.paintFill); } } else if (this.hasDefaultPaint) { // use the default paint objects if (this.defaultPaintOutline != null) { canvas.drawPath(this.path, this.defaultPaintOutline); } if (this.defaultPaintFill != null) { canvas.drawPath(this.path, this.defaultPaintFill); } } } /** * Checks whether a circle 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 a circle 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 checkCirclePoint = new Point(); synchronized (this.visibleCircles) { // iterate over all visible circles for (int i = this.visibleCircles.size() - 1; i >= 0; --i) { Integer circleIndex = this.visibleCircles.get(i); // get the current circle Circle checkOverlayCircle = createCircle(circleIndex.intValue()); if (checkOverlayCircle == null) { continue; } synchronized (checkOverlayCircle) { // make sure that the current circle has a center position and a radius if (checkOverlayCircle.center == null || checkOverlayCircle.radius < 0) { continue; } checkCirclePoint = projection.toPixels(checkOverlayCircle.center, checkCirclePoint); // check if the translation to pixel coordinates has failed if (checkCirclePoint == null) { continue; } // calculate the Euclidian distance between the circle and the event position float diffX = checkCirclePoint.x - eventPosition.x; float diffY = checkCirclePoint.y - eventPosition.y; double distance = Math.sqrt(diffX * diffX + diffY * diffY); // check if the event position is within the circle radius if (distance <= checkOverlayCircle.cachedRadius) { switch (eventType) { case LONG_PRESS: if (onLongPress(circleIndex.intValue())) { return true; } break; case TAP: if (onTap(circleIndex.intValue())) { return true; } break; } } } } } // no hit return false; } /** * Creates a circle in this overlay. * * @param index * the index of the circle. * @return the circle. */ protected abstract Circle createCircle(int index); @Override protected void drawOverlayBitmap(Canvas canvas, Point drawPosition, Projection projection, byte drawZoomLevel) { // erase the list of visible circles this.visibleCirclesRedraw.clear(); int numberOfCircles = size(); for (int circleIndex = 0; circleIndex < numberOfCircles; ++circleIndex) { if (isInterrupted() || sizeHasChanged()) { // stop working return; } // get the current circle Circle overlayCircle = createCircle(circleIndex); if (overlayCircle == null) { continue; } synchronized (overlayCircle) { // make sure that the current circle has a center position and a radius if (overlayCircle.center == null || overlayCircle.radius < 0) { continue; } // make sure that the cached center position is valid if (drawZoomLevel != overlayCircle.cachedZoomLevel) { overlayCircle.cachedCenterPosition = projection.toPoint(overlayCircle.center, overlayCircle.cachedCenterPosition, drawZoomLevel); overlayCircle.cachedZoomLevel = drawZoomLevel; overlayCircle.cachedRadius = projection.metersToPixels(overlayCircle.radius, drawZoomLevel); } // calculate the relative circle position on the canvas this.circlePosition.x = overlayCircle.cachedCenterPosition.x - drawPosition.x; this.circlePosition.y = overlayCircle.cachedCenterPosition.y - drawPosition.y; float circleRadius = overlayCircle.cachedRadius; // check if the bounding box of the circle intersects with the canvas if ((this.circlePosition.x + circleRadius) >= 0 && (this.circlePosition.x - circleRadius) <= canvas.getWidth() && (this.circlePosition.y + circleRadius) >= 0 && (this.circlePosition.y - circleRadius) <= canvas.getHeight()) { // assemble the path this.path.reset(); this.path.addCircle(this.circlePosition.x, this.circlePosition.y, circleRadius, Path.Direction.CCW); if (overlayCircle.hasPaint || this.hasDefaultPaint) { drawPathOnCanvas(canvas, overlayCircle); // add the current circle index to the list of visible circles this.visibleCirclesRedraw.add(Integer.valueOf(circleIndex)); } } } } // swap the two visible circle lists synchronized (this.visibleCircles) { List<Integer> visibleCirclesTemp = this.visibleCircles; this.visibleCircles = this.visibleCirclesRedraw; this.visibleCirclesRedraw = visibleCirclesTemp; } } @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 circle 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 circle 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 circles have been added to the overlay. */ protected final void populate() { super.requestRedraw(); } }