package com.mapbox.mapboxsdk.overlay;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import com.mapbox.mapboxsdk.R;
import com.mapbox.mapboxsdk.clustering.ClusterItem;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.util.BitmapUtils;
import com.mapbox.mapboxsdk.views.InfoWindow;
import com.mapbox.mapboxsdk.views.MapView;
import com.mapbox.mapboxsdk.views.util.Projection;
import com.mapbox.mapboxsdk.views.util.constants.MapViewConstants;
/**
* Immutable class describing a LatLng with a Title and a Description.
*/
public class Marker implements MapViewConstants, ClusterItem {
private static String TAG = "Marker";
public static final int ITEM_STATE_FOCUSED_MASK = 4;
public static final int ITEM_STATE_PRESSED_MASK = 1;
public static final int ITEM_STATE_SELECTED_MASK = 2;
private int group = 0;
private final RectF mMyLocationRect = new RectF(0, 0, 0, 0);
private final RectF mMyLocationPreviousRect = new RectF(0, 0, 0, 0);
protected final PointF mCurMapCoords = new PointF();
protected Context context;
protected MapView mapView;
private Icon icon;
private boolean isUsingMakiIcon = true;
protected String mUid;
protected LatLng mLatLng;
protected Drawable mMarker;
protected PointF mAnchor = null;
private String mTitle = "";
private String mDescription = "";
private String mSubDescription = "";
//a third field that can be displayed in the infowindow, on a third line
private Drawable mImage; //that will be shown in the infowindow.
//private GeoPoint mGeoPoint //unfortunately, this is not so simple...
private Object mRelatedObject; //reference to an object (of any kind) linked to this item.
private boolean bubbleShowing;
private ItemizedOverlay mParentHolder;
private Drawable mDefaultPinDrawable;
protected int mDefaultPinRes = R.drawable.defpin;
private boolean isVisible = true;
/**
* Construct a new Marker, given title, description, and place
*
* @param title Marker title
* @param description Marker description
* @param latLng Marker position
*/
public Marker(String title, String description, LatLng latLng) {
this(null, title, description, latLng);
}
/**
* Initialize a new marker object, adding it to a MapView and attaching a tooltip
*
* @param mv a mapview
* @param aTitle the title of the marker, in a potential tooltip
* @param aDescription the description of the marker, in a tooltip
* @param aLatLng the location of the marker
*/
public Marker(MapView mv, String aTitle, String aDescription, LatLng aLatLng) {
super();
this.mapView = mv;
if (mv != null) {
this.context = mv.getContext();
}
this.setTitle(aTitle);
this.setDescription(aDescription);
this.mLatLng = aLatLng;
mParentHolder = null;
mAnchor = DEFAULT_PIN_ANCHOR;
// Note: Only Load Default Marker (if needed) when getMarker() called.
}
/**
* Default Marker image loaded from Library
*
* @return BitMapDrawable of the Default Marker image
*/
public Drawable getDefaultPinDrawable() {
if (mDefaultPinDrawable == null && this.context != null) {
BitmapFactory.Options opts = BitmapUtils.getBitmapOptions(context.getResources().getDisplayMetrics());
mDefaultPinDrawable = new BitmapDrawable(context.getResources(), BitmapFactory.decodeResource(context.getResources(), mDefaultPinRes, opts));
}
return mDefaultPinDrawable;
}
/**
* Attach this marker to a given MapView and that MapView's context
*
* @param mv the MapView to add this marker to
* @return Marker
*/
public Marker addTo(MapView mv) {
mapView = mv;
if (this.context == null) {
context = mv.getContext();
}
return this;
}
/**
* Determine if this marker has a title, description, subdescription,
* or image that could be displayed
*
* @return true if the marker has content
*/
public boolean hasContent() {
return !TextUtils.isEmpty(this.mTitle) ||
!TextUtils.isEmpty(this.mDescription) ||
!TextUtils.isEmpty(this.mSubDescription) ||
this.mImage != null;
}
protected InfoWindow createTooltip(MapView mv) {
return new InfoWindow(R.layout.tooltip, mv);
}
private InfoWindow mToolTip;
/**
* Get this marker's tooltip, creating it if it doesn't exist yet.
*
* @param mv MapView
* @return InfoWindow
*/
public InfoWindow getToolTip(MapView mv) {
if (mToolTip == null || mToolTip.getMapView() != mv) {
mToolTip = createTooltip(mv);
}
return mToolTip;
}
public void setToolTip(InfoWindow mToolTip) {
this.mToolTip = mToolTip;
}
public void closeToolTip() {
if (mToolTip != null && mToolTip.equals(mToolTip.getMapView().getCurrentTooltip())) {
mToolTip.getMapView().closeCurrentTooltip();
}
}
public void blur() {
if (mParentHolder != null) {
mParentHolder.blurItem(this);
}
}
@Override
public LatLng getPosition() {
return mLatLng;
}
/**
* Indicates a hotspot for an area. This is where the origin (0,0)of a point will be located
* relative to the area. In otherwords this acts as an offset. NONE indicates that no
* adjustment
* should be made.
*/
public enum HotspotPlace {
NONE, CENTER, BOTTOM_CENTER, TOP_CENTER, RIGHT_CENTER,
LEFT_CENTER, UPPER_RIGHT_CORNER, LOWER_RIGHT_CORNER,
UPPER_LEFT_CORNER, LOWER_LEFT_CORNER
}
public String getUid() {
return mUid;
}
public String getTitle() {
return mTitle;
}
public LatLng getPoint() {
return mLatLng;
}
public void setTitle(String aTitle) {
mTitle = aTitle;
}
public void setDescription(String aDescription) {
mDescription = aDescription;
}
public void setSubDescription(String aSubDescription) {
mSubDescription = aSubDescription;
}
public void setImage(Drawable anImage) {
mImage = anImage;
}
public void setRelatedObject(Object o) {
mRelatedObject = o;
}
/**
* Set the centerpoint of this marker in geographical coordinates
*
* @param point
*/
public void setPoint(LatLng point) {
mLatLng = point;
invalidate();
}
/**
* Set the description attached to this marker
*
* @return
*/
public String getDescription() {
return mDescription;
}
/**
* Set the sub-description attached to this marker
*
* @return
*/
public String getSubDescription() {
return mSubDescription;
}
/**
* Set the image attached to this marker
*
* @return
*/
public Drawable getImage() {
return mImage;
}
public Object getRelatedObject() {
return mRelatedObject;
}
public ItemizedOverlay getParentHolder() {
return mParentHolder;
}
public void setParentHolder(ItemizedOverlay o) {
mParentHolder = o;
}
/**
* Gets the image (Drawable) used for the Marker's image
*
* @param stateBitset State Of Marker (@see #ITEM_STATE_FOCUSED_MASK , @see #ITEM_STATE_PRESSED_MASK, @see #ITEM_STATE_SELECTED_MASK)
* @return marker drawable corresponding to stateBitset
*/
public Drawable getMarker(final int stateBitset) {
// marker has not been specified yet, so load Default Marker Pin
if (mMarker == null) {
setMarker(getDefaultPinDrawable(), true);
}
// set marker state appropriately
setState(mMarker, stateBitset);
return mMarker;
}
/**
* Set a custom image to be used as the Marker's image
*
* @param marker Drawable resource to be used as Marker image
* @param isMakiIcon True if Maki Icon, False if not (ex: Custom Image)
*/
public void setMarker(final Drawable marker, boolean isMakiIcon) {
this.mMarker = marker;
if (marker != null) {
marker.setBounds(0, 0, marker.getIntrinsicWidth(), marker.getIntrinsicHeight());
isUsingMakiIcon = isMakiIcon;
}
invalidate();
}
/**
* Set a custom image to be used as the Marker's image
* NOTE: Convenience method for setting a custom image as the marker
*
* @param marker Drawable resource to be used as Marker image
*/
public void setMarker(final Drawable marker) {
this.setMarker(marker, false);
}
/**
* Sets the marker hotspot
*
* @param place Hotspot Location @see #HotspotPlace
*/
public void setHotspot(HotspotPlace place) {
if (place == null) {
place = HotspotPlace.BOTTOM_CENTER; //use same default than in osmdroid.
}
switch (place) {
case NONE:
case UPPER_LEFT_CORNER:
mAnchor.set(0, 0);
break;
case BOTTOM_CENTER:
mAnchor.set(0.5f, 1f);
break;
case LOWER_LEFT_CORNER:
mAnchor.set(0, 1);
break;
case LOWER_RIGHT_CORNER:
mAnchor.set(1, 1);
break;
case CENTER:
mAnchor.set(0.5f, 0.5f);
break;
case LEFT_CENTER:
mAnchor.set(0, 0.5f);
break;
case RIGHT_CENTER:
mAnchor.set(1, 0.5f);
break;
case TOP_CENTER:
mAnchor.set(0.5f, 0);
break;
case UPPER_RIGHT_CORNER:
mAnchor.set(1, 0);
break;
}
invalidate();
}
public Point getAnchor() {
if (mAnchor != null) {
int markerWidth = getWidth(), markerHeight = getHeight();
return new Point((int) (-mAnchor.x * markerWidth), (int) (-mAnchor.y * markerHeight));
}
return new Point(0, 0);
}
public Point getAnchor(HotspotPlace place) {
int markerWidth = getWidth(), markerHeight = getHeight();
return getHotspot(place, markerWidth, markerHeight);
}
public void setAnchor(final PointF anchor) {
this.mAnchor = anchor;
invalidate();
}
public static void setState(final Drawable drawable, final int stateBitset) {
final int[] states = new int[3];
int index = 0;
if ((stateBitset & ITEM_STATE_PRESSED_MASK) > 0) {
states[index++] = android.R.attr.state_pressed;
}
if ((stateBitset & ITEM_STATE_SELECTED_MASK) > 0) {
states[index++] = android.R.attr.state_selected;
}
if ((stateBitset & ITEM_STATE_FOCUSED_MASK) > 0) {
states[index++] = android.R.attr.state_focused;
}
if (drawable != null) {
drawable.setState(states);
}
}
public Drawable getDrawable() {
return this.mMarker;
}
/**
* Get the width of the marker, based on the width of the image backing it.
*/
public int getWidth() {
if (mMarker == null) {
return 0;
}
return mMarker.getIntrinsicWidth();
}
public int getHeight() {
if (mMarker == null) {
return 0;
}
int result = getRealHeight();
if (isUsingMakiIcon) {
result = result / 2;
}
return result;
}
public int getRealHeight() {
if (mMarker == null) {
return 0;
}
return mMarker.getIntrinsicHeight();
}
/**
* Get the current position of the marker in pixels
*
* @param projection Projection
* @param reuse PointF to reuse
*/
public PointF getPositionOnScreen(final Projection projection, final PointF reuse) {
return projection.toPixels(mCurMapCoords, reuse);
}
public PointF getDrawingPositionOnScreen(final Projection projection, PointF reuse) {
reuse = getPositionOnScreen(projection, reuse);
Point point = getAnchor();
reuse.offset(point.x, point.y);
return reuse;
}
protected RectF getDrawingBounds(final Projection projection, RectF reuse) {
if (reuse == null) {
reuse = new RectF();
}
final PointF position = getPositionOnScreen(projection, null);
final int w = getWidth();
final int h = isUsingMakiIcon ? getRealHeight() : getHeight();
final float left = position.x - mAnchor.x * w;
final float right = left + w;
final float top = position.y - mAnchor.y * h;
float bottom = top + h;
reuse.set(left, top, right, bottom);
return reuse;
}
protected RectF getMapDrawingBounds(final Projection projection, RectF reuse) {
if (reuse == null) {
reuse = new RectF();
}
projection.toMapPixels(mLatLng, mCurMapCoords);
final int w = getWidth();
final int h = getHeight();
final float x = mCurMapCoords.x - mAnchor.x * w;
final float y = mCurMapCoords.y - mAnchor.y * h;
reuse.set(x, y, x + w, y + h * 2);
return reuse;
}
protected RectF getHitBounds(final Projection projection, RectF reuse) {
return getDrawingBounds(projection, reuse);
}
public PointF getHotspotScale(HotspotPlace place, PointF reuse) {
if (reuse == null) {
reuse = new PointF();
}
if (place == null) {
place = HotspotPlace.BOTTOM_CENTER; //use same default than in osmdroid.
}
switch (place) {
case NONE:
case UPPER_LEFT_CORNER:
reuse.set(0, 0);
break;
case BOTTOM_CENTER:
reuse.set(0.5f, 1f);
break;
case LOWER_LEFT_CORNER:
reuse.set(0, 1);
break;
case LOWER_RIGHT_CORNER:
reuse.set(1, 1);
break;
case CENTER:
reuse.set(0.5f, 0.5f);
break;
case LEFT_CENTER:
reuse.set(0, 0.5f);
break;
case RIGHT_CENTER:
reuse.set(1, 0.5f);
break;
case TOP_CENTER:
reuse.set(0.5f, 0);
break;
case UPPER_RIGHT_CORNER:
reuse.set(1, 0);
break;
}
return reuse;
}
/**
* From a HotspotPlace and drawable dimensions (width, height), return the hotspot position.
* Could be a public method of HotspotPlace or OverlayItem...
*/
public Point getHotspot(HotspotPlace place, int w, int h) {
PointF scale = getHotspotScale(place, null);
return new Point((int) (-w * scale.x), (int) (-h * scale.y));
}
/**
* Populates this tooltip with all item info:
* <ul>title and description in any case, </ul>
* <ul>image and sub-description if any.</ul>
* and centers the map view on the item if panIntoView is true. <br>
*/
public void showBubble(InfoWindow tooltip, MapView aMapView, boolean panIntoView) {
//offset the tooltip to be top-centered on the marker:
Point markerH = getAnchor();
Point tooltipH = getAnchor(HotspotPlace.TOP_CENTER);
markerH.offset(-tooltipH.x, tooltipH.y);
tooltip.open(this, this.getPoint(), markerH.x, markerH.y);
if (panIntoView) {
aMapView.getController().animateTo(getPoint());
}
bubbleShowing = true;
tooltip.setBoundMarker(this);
}
/**
* Sets the Icon image that represents this marker on screen.
*/
public Marker setIcon(Icon aIcon) {
this.icon = aIcon;
icon.setMarker(this);
isUsingMakiIcon = true;
return this;
}
public boolean isUsingMakiIcon() {
return isUsingMakiIcon;
}
public PointF getPositionOnMap() {
return mCurMapCoords;
}
public void updateDrawingPosition() {
if (mapView == null) {
return; //not on map yet
}
getMapDrawingBounds(mapView.getProjection(), mMyLocationRect);
}
/**
* Sets the marker to be redrawn.
*/
public void invalidate() {
if (mapView == null) {
return; //not on map yet
}
// Get new drawing bounds
mMyLocationPreviousRect.set(mMyLocationRect);
updateDrawingPosition();
final RectF newRect = new RectF(mMyLocationRect);
// If we had a previous location, merge in those bounds too
newRect.union(mMyLocationPreviousRect);
// Invalidate the bounds
mapView.post(new Runnable() {
@Override
public void run() {
mapView.invalidateMapCoordinates(newRect);
}
});
}
public void setVisibility(boolean visiblity) {
isVisible = visiblity;
}
public boolean isVisible() {
return isVisible;
}
}