// Created by plusminus on 23:18:23 - 02.10.2008 package com.mapbox.mapboxsdk.overlay; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Point; import android.graphics.PointF; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.view.MotionEvent; import com.mapbox.mapboxsdk.clustering.Cluster; import com.mapbox.mapboxsdk.clustering.algo.NonHierarchicalDistanceBasedAlgorithm; import com.mapbox.mapboxsdk.clustering.algo.PreCachingAlgorithmDecorator; import com.mapbox.mapboxsdk.events.MapListener; import com.mapbox.mapboxsdk.events.RotateEvent; import com.mapbox.mapboxsdk.events.ScrollEvent; import com.mapbox.mapboxsdk.events.ZoomEvent; import com.mapbox.mapboxsdk.views.MapView; import com.mapbox.mapboxsdk.views.safecanvas.ISafeCanvas; import com.mapbox.mapboxsdk.views.safecanvas.ISafeCanvas.UnsafeCanvasHandler; import com.mapbox.mapboxsdk.views.safecanvas.SafePaint; import com.mapbox.mapboxsdk.views.util.Projection; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; /** * Draws a list of {@link Marker} 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(). * * @author Marc Kurtz * @author Nicolas Gramlich * @author Theodore Hong * @author Fred Eisele */ public abstract class ItemizedOverlay extends SafeDrawOverlay implements Overlay.Snappable, MapListener { private static final String TAG = ItemizedOverlay.class.getSimpleName(); private final ArrayList<Marker> mInternalItemList; private ArrayList<ClusterMarker> mInternalClusterList; protected boolean mDrawFocusedItem = true; private Marker mFocusedItem; private boolean mPendingFocusChangedEvent = false; private OnFocusChangeListener mOnFocusChangeListener; private boolean mIsClusteringEnabled; private ClusterMarker.OnDrawClusterListener mOnDrawClusterListener; private static SafePaint mClusterTextPaint; private CalculateClusterTask mCalculateClusterTask; private float mMinZoomForClustering = 22; private PreCachingAlgorithmDecorator<Marker> mAlgorithm; /** * Method by which subclasses create the actual Items. This will only be called from populate() * we'll cache them for later use. */ protected abstract Marker createItem(int i); /** * The number of items in this overlay. */ public abstract int size(); public ItemizedOverlay() { super(); if (mClusterTextPaint == null) { mClusterTextPaint = new SafePaint(); mClusterTextPaint.setTextAlign(Paint.Align.CENTER); mClusterTextPaint.setTextSize(30); mClusterTextPaint.setFakeBoldText(true); } mAlgorithm = new PreCachingAlgorithmDecorator<>(new NonHierarchicalDistanceBasedAlgorithm<Marker>()); mInternalItemList = new ArrayList<>(); mInternalClusterList = new ArrayList<>(); } /** * 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 Marker.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 drawSafe(ISafeCanvas canvas, MapView mapView, boolean shadow) { if (shadow) { return; } if (mPendingFocusChangedEvent && mOnFocusChangeListener != null) { mOnFocusChangeListener.onFocusChanged(this, mFocusedItem); } mPendingFocusChangedEvent = false; sortListByLatitude(); final Projection pj = mapView.getProjection(); final int size = size() - 1; final RectF bounds = new RectF(0, 0, mapView.getMeasuredWidth(), mapView.getMeasuredHeight()); pj.rotateRect(bounds); final float mapScale = 1 / mapView.getScale(); if (!mIsClusteringEnabled || mapView.getZoomLevel() > mMinZoomForClustering) { /* Draw in backward cycle, so the items with the least index are on the front. */ for (int i = size; i >= 0; i--) { final Marker item = getItem(i); if (item == mFocusedItem) { continue; } onDrawItem(canvas, item, pj, mapView.getMapOrientation(), bounds, mapScale); } if (mFocusedItem != null) { onDrawItem(canvas, mFocusedItem, pj, mapView.getMapOrientation(), bounds, mapScale); } } else if (mInternalClusterList != null) { for (int i = mInternalClusterList.size() - 1; i >= 0; --i) { final ClusterMarker clusterMarker = mInternalClusterList.get(i); List<Marker> markerList = clusterMarker.getMarkersReadOnly(); if (markerList.size() > 1) { // if (mOnDrawClusterListener != null) { // Drawable drawable = mOnDrawClusterListener.drawCluster(clusterMarker); // clusterMarker.setMarker(drawable); // } onDrawItem(canvas, clusterMarker, pj, mapView.getMapOrientation(), bounds, mapScale); } else { onDrawItem(canvas, markerList.get(0), pj, mapView.getMapOrientation(), bounds, mapScale); } } } } /** * 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 void populate() { final int size = size(); mAlgorithm.clearItems(); mInternalItemList.clear(); mInternalItemList.ensureCapacity(size); for (int a = 0; a < size; a++) { mInternalItemList.add(createItem(a)); } mAlgorithm.addItems(mInternalItemList); } /** * Returns the Item at the given index. * * @param position the position of the item to return * @return the Item of the given index. */ public Marker getItem(final int position) { return mInternalItemList.get(position); } protected abstract void sortListByLatitude(); /** * 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 projection the projection to use. * @param aMapOrientation * @param mapBounds * @param mapScale */ protected void onDrawItem(ISafeCanvas canvas, final Marker item, final Projection projection, final float aMapOrientation, final RectF mapBounds, final float mapScale) { item.updateDrawingPosition(); final PointF position = item.getPositionOnMap(); final Point roundedCoords = new Point((int) position.x, (int) position.y); if (!RectF.intersects(mapBounds, item.getDrawingBounds(projection, null)) || !item.isVisible()) { //dont draw item if offscreen return; } canvas.save(); canvas.scale(mapScale, mapScale, position.x, position.y); final int state = (mDrawFocusedItem && (mFocusedItem == item) ? Marker.ITEM_STATE_FOCUSED_MASK : 0); final Drawable marker = item.getMarker(state); if (marker == null) { return; } final Point point = item.getAnchor(); // draw it if (this.isUsingSafeCanvas()) { Overlay.drawAt(canvas.getSafeCanvas(), marker, roundedCoords, point, false, aMapOrientation); } else { canvas.getUnsafeCanvas(new UnsafeCanvasHandler() { @Override public void onUnsafeCanvas(Canvas canvas) { Overlay.drawAt(canvas, marker, roundedCoords, point, false, aMapOrientation); } }); } canvas.restore(); } protected boolean markerHitTest(final Marker pMarker, final Projection pProjection, final float pX, final float pY) { RectF rect = pMarker.getHitBounds(pProjection, null); /* RectF rect = pMarker.getDrawingBounds(pProjection, null); if (pMarker.isUsingMakiIcon()) { //a marker drawing bounds is twice the actual size of the marker rect.bottom -= rect.height() / 2; } */ return rect.contains(pX, pY); } @Override public boolean onSingleTapConfirmed(MotionEvent e, MapView mapView) { final int size = this.size(); final Projection projection = mapView.getProjection(); final float x = e.getX(); final float y = e.getY(); for (int i = 0; i < size; i++) { final Marker item = getItem(i); if (markerHitTest(item, projection, x, y)) { // 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; } /** * If the given Item is found in the overlay, force it to be the current focus-bearer. Any * registered {@link ItemizedOverlay} 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 Marker item) { mPendingFocusChangedEvent = item != mFocusedItem; mFocusedItem = item; } /** * @return the currently-focused item, or null if no item is currently focused. */ public Marker getFocus() { return mFocusedItem; } /** * an item want's to be blured, if it is the focused one, blur it */ public void blurItem(final Marker item) { if (mFocusedItem == item) { setFocus(null); } } // /** // * Adjusts a drawable's bounds so that (0,0) is a pixel in the location described by the anchor // * parameter. Useful for "pin"-like graphics. For convenience, returns the same drawable that // * was passed in. // * // * @param marker the drawable to adjust // * @param anchor the anchor for the drawable (float between 0 and 1) // * @return the same drawable that was passed in. // */ // protected synchronized Drawable boundToHotspot(final Drawable marker, Point anchor) { // final int markerWidth = marker.getIntrinsicWidth(); // final int markerHeight = marker.getIntrinsicHeight(); // // mRect.set(0, 0, markerWidth, markerHeight); // mRect.offset(anchor.x, anchor.y); // marker.setBounds(mRect); // return marker; // } public void setOnFocusChangeListener(OnFocusChangeListener l) { mOnFocusChangeListener = l; } public interface OnFocusChangeListener { void onFocusChanged(ItemizedOverlay overlay, Marker newFocus); } /** * Enable or disable clustering * * @param enabled * @param onDrawClusterListener A listener that allows the modification of the cluster's drawable */ public void setClusteringEnabled(final boolean enabled, final ClusterMarker.OnDrawClusterListener onDrawClusterListener, float minZoom) { mIsClusteringEnabled = enabled; mOnDrawClusterListener = onDrawClusterListener; mMinZoomForClustering = minZoom; } public void onScroll(ScrollEvent event) { } /** * Called when a map is zoomed. */ public void onZoom(ZoomEvent event) { if (mIsClusteringEnabled && event.getZoomLevel() < mMinZoomForClustering) { if (mCalculateClusterTask != null && mCalculateClusterTask.getStatus() != AsyncTask.Status.FINISHED) { mCalculateClusterTask.cancel(true); } mCalculateClusterTask = new CalculateClusterTask(event); mCalculateClusterTask.execute(); } } /** * Called when a map is rotated. */ public void onRotate(RotateEvent event) { } public ClusterMarker.OnDrawClusterListener getOnDrawClusterListener() { return mOnDrawClusterListener; } public boolean isClusteringEnabled() { return mIsClusteringEnabled; } private class CalculateClusterTask extends AsyncTask<Void, Void, ArrayList<ClusterMarker>> { private ZoomEvent mZoomEvent; public CalculateClusterTask(ZoomEvent event) { mZoomEvent = event; } @Override protected ArrayList<ClusterMarker> doInBackground(final Void... voids) { ArrayList<ClusterMarker> clusterMarkers = new ArrayList<>(); Set<? extends Cluster<Marker>> clusters = mAlgorithm.getClusters(mZoomEvent.getZoomLevel()); for (Cluster<Marker> cluster : clusters) { Collection<Marker> markers = cluster.getItems(); if (markers.size() > 0) { ClusterMarker clusterMarker; clusterMarker = new ClusterMarker(); clusterMarker.addMarkersToCluster(markers); clusterMarker.addTo(mZoomEvent.getSource()); clusterMarker.setPoint(cluster.getPosition()); if (mOnDrawClusterListener != null) { Drawable drawable = mOnDrawClusterListener.drawCluster(clusterMarker); clusterMarker.setMarker(drawable); } clusterMarkers.add(clusterMarker); } } return clusterMarkers; } @Override protected void onPostExecute(final ArrayList<ClusterMarker> clusterList) { mInternalClusterList = clusterList; mZoomEvent.getSource().invalidate(); } } }