package org.osmdroid.views.overlay;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.MotionEvent;
import android.util.TypedValue;
import org.osmdroid.library.R;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.MapView;
import org.osmdroid.views.Projection;
import org.osmdroid.views.overlay.infowindow.MarkerInfoWindow;
/**
* A marker is an icon placed at a particular point on the map's surface that can have a popup-{@link org.osmdroid.views.overlay.infowindow.InfoWindow} (a bubble)
* Mimics the Marker class from Google Maps Android API v2 as much as possible. Main differences:<br>
*
* - Doesn't support Z-Index: as other osmdroid overlays, Marker is drawn in the order of appearance. <br>
* - The icon can be any standard Android Drawable, instead of the BitmapDescriptor introduced in Google Maps API v2. <br>
* - The icon can be changed at any time. <br>
* - The InfoWindow hosts a standard Android View. It can handle Android widgets like buttons and so on. <br>
* - Supports a "sub-description", to be displayed in the InfoWindow, under the snippet, in a smaller text font. <br>
* - Supports an image, to be displayed in the InfoWindow. <br>
* - Supports "panning to view" on/off option (when touching a marker, center the map on marker position). <br>
* - Opening a Marker InfoWindow automatically close others only if it's the same InfoWindow shared between Markers. <br>
* - Events listeners are set per marker, not per map. <br>
*
* TODO: <br>
* Impact of marker rotation on hitTest<br>
* When map is rotated, when panning the map, bug on the InfoWindow positioning (osmdroid issue #524)<br/>
*
* <img alt="Class diagram around Marker class" width="686" height="413" src='src='./doc-files/marker-infowindow-classes.png' />
*
* @see MarkerInfoWindow
* see also <a href="http://developer.android.com/reference/com/google/android/gms/maps/model/Marker.html">Google Maps Marker</a>
*
* @author M.Kergall
*
*/
public class Marker extends OverlayWithIW {
/**
* this is an OPT IN feature that was used for kml parsing with osmbonuspack (on a remote branch)
* that set the image icon of a kml marker to a text label if no image url was provided. It's also
* used in a few other cases, such as placing a generic text label on the map
*/
public static boolean ENABLE_TEXT_LABELS_WHEN_NO_IMAGE=false;
/* attributes for text labels, used for osmdroid gridlines */
protected int mTextLabelBackgroundColor =Color.WHITE;
protected int mTextLabelForegroundColor = Color.BLACK;
protected int mTextLabelFontSize =24;
/*attributes for standard features:*/
protected Drawable mIcon;
protected GeoPoint mPosition;
protected float mBearing;
protected float mAnchorU, mAnchorV;
protected float mIWAnchorU, mIWAnchorV;
protected float mAlpha;
protected boolean mDraggable, mIsDragged;
protected boolean mFlat;
protected OnMarkerClickListener mOnMarkerClickListener;
protected OnMarkerDragListener mOnMarkerDragListener;
/*attributes for non-standard features:*/
protected Drawable mImage;
protected boolean mPanToView;
protected float mDragOffsetY;
/*internals*/
protected Point mPositionPixels;
protected static MarkerInfoWindow mDefaultInfoWindow = null;
protected static Drawable mDefaultIcon = null; //cache for default icon (resourceProxy.getDrawable being slow)
protected Resources resource;
/** Usual values in the (U,V) coordinates system of the icon image */
public static final float ANCHOR_CENTER=0.5f, ANCHOR_LEFT=0.0f, ANCHOR_TOP=0.0f, ANCHOR_RIGHT=1.0f, ANCHOR_BOTTOM=1.0f;
public Marker(MapView mapView) {
this(mapView, (mapView.getContext()));
}
public Marker(MapView mapView, final Context resourceProxy) {
super();
resource = mapView.getContext().getResources();
mBearing = 0.0f;
mAlpha = 1.0f; //opaque
mPosition = new GeoPoint(0.0, 0.0);
mAnchorU = ANCHOR_CENTER;
mAnchorV = ANCHOR_CENTER;
mIWAnchorU = ANCHOR_CENTER;
mIWAnchorV = ANCHOR_TOP;
mDraggable = false;
mIsDragged = false;
mPositionPixels = new Point();
mPanToView = true;
mDragOffsetY = 0.0f;
mFlat = false; //billboard
mOnMarkerClickListener = null;
mOnMarkerDragListener = null;
if (mDefaultIcon == null)
mDefaultIcon = resourceProxy.getResources().getDrawable(R.drawable.marker_default);
mIcon = mDefaultIcon;
if (mDefaultInfoWindow == null || mDefaultInfoWindow.getMapView() != mapView){
//build default bubble, that will be shared between all markers using the default one:
/* pre-aar version
Context context = mapView.getContext();
String packageName = context.getPackageName();
int defaultLayoutResId = context.getResources().getIdentifier("bonuspack_bubble", "layout", packageName);
if (defaultLayoutResId == 0)
Log.e(BonusPackHelper.LOG_TAG, "Marker: layout/bonuspack_bubble not found in "+packageName);
else
mDefaultInfoWindow = new MarkerInfoWindow(defaultLayoutResId, mapView);
*/
//get the default layout now included in the aar library
mDefaultInfoWindow = new MarkerInfoWindow(R.layout.bonuspack_bubble, mapView);
}
setInfoWindow(mDefaultInfoWindow);
}
/** Sets the icon for the marker. Can be changed at any time.
* @param icon if null, the default osmdroid marker is used.
*/
public void setIcon(final Drawable icon){
if (ENABLE_TEXT_LABELS_WHEN_NO_IMAGE && icon==null && this.mTitle!=null && this.mTitle.length() > 0) {
Paint background = new Paint();
background.setColor(mTextLabelBackgroundColor);
Paint p = new Paint();
p.setTextSize(mTextLabelFontSize);
p.setColor(mTextLabelForegroundColor);
p.setAntiAlias(true);
p.setTypeface(Typeface.DEFAULT_BOLD);
p.setTextAlign(Paint.Align.LEFT);
int width=(int)(p.measureText(getTitle()) + 0.5f);
float baseline=(int)(-p.ascent() + 0.5f);
int height=(int) (baseline +p.descent() + 0.5f);
Bitmap image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(image);
c.drawPaint(background);
c.drawText(getTitle(),0,baseline,p);
mIcon=new BitmapDrawable(resource,image);
} else if (!ENABLE_TEXT_LABELS_WHEN_NO_IMAGE && icon!=null) {
this.mIcon = icon;
} else if (this.mIcon!=null) {
mIcon=icon;
} else
//there's still an edge case here, title label no defined, icon is null and textlabel is enabled
mIcon = mDefaultIcon;
}
public GeoPoint getPosition(){
return mPosition;
}
public void setPosition(GeoPoint position){
mPosition = position.clone();
}
public float getRotation(){
return mBearing;
}
public void setRotation(float rotation){
mBearing = rotation;
}
public void setAnchor(float anchorU, float anchorV){
mAnchorU = anchorU;
mAnchorV= anchorV;
}
public void setInfoWindowAnchor(float anchorU, float anchorV){
mIWAnchorU = anchorU;
mIWAnchorV= anchorV;
}
public void setAlpha(float alpha){
mAlpha = alpha;
}
public float getAlpha(){
return mAlpha;
}
public void setDraggable(boolean draggable){
mDraggable = draggable;
}
public boolean isDraggable(){
return mDraggable;
}
public void setFlat(boolean flat){
mFlat = flat;
}
public boolean isFlat(){
return mFlat;
}
/**
* Removes this Marker from the MapView.
* Note that this method will operate only if the Marker is in the MapView overlays
* (it should not be included in a container like a FolderOverlay).
* @param mapView
*/
public void remove(MapView mapView){
mapView.getOverlays().remove(this);
}
public void setOnMarkerClickListener(OnMarkerClickListener listener){
mOnMarkerClickListener = listener;
}
public void setOnMarkerDragListener(OnMarkerDragListener listener){
mOnMarkerDragListener = listener;
}
/** set an image to be shown in the InfoWindow - this is not the marker icon */
public void setImage(Drawable image){
mImage = image;
}
/** get the image to be shown in the InfoWindow - this is not the marker icon */
public Drawable getImage(){
return mImage;
}
/** set the offset in millimeters that the marker is moved up while dragging */
public void setDragOffset(float mmUp){
mDragOffsetY = mmUp;
}
/** get the offset in millimeters that the marker is moved up while dragging */
public float getDragOffset(){
return mDragOffsetY;
}
/** Set the InfoWindow to be used.
* Default is a MarkerInfoWindow, with the layout named "bonuspack_bubble".
* You can use this method either to use your own layout, or to use your own sub-class of InfoWindow.
* Note that this InfoWindow will receive the Marker object as an input, so it MUST be able to handle Marker attributes.
* If you don't want any InfoWindow to open, you can set it to null. */
public void setInfoWindow(MarkerInfoWindow infoWindow){
if (mInfoWindow!=null && mInfoWindow!=mDefaultInfoWindow )
mInfoWindow.onDetach();
mInfoWindow = infoWindow;
}
/** If set to true, when clicking the marker, the map will be centered on the marker position.
* Default is true. */
public void setPanToView(boolean panToView){
mPanToView = panToView;
}
public void showInfoWindow(){
if (mInfoWindow == null)
return;
int markerWidth = 0, markerHeight = 0;
markerWidth = mIcon.getIntrinsicWidth();
markerHeight = mIcon.getIntrinsicHeight();
int offsetX = (int)(mIWAnchorU*markerWidth) - (int)(mAnchorU*markerWidth);
int offsetY = (int)(mIWAnchorV*markerHeight) - (int)(mAnchorV*markerHeight);
mInfoWindow.open(this, mPosition, offsetX, offsetY);
}
public boolean isInfoWindowShown(){
if (mInfoWindow instanceof MarkerInfoWindow){
MarkerInfoWindow iw = (MarkerInfoWindow)mInfoWindow;
return (iw != null) && iw.isOpen() && (iw.getMarkerReference()==this);
} else
return super.isInfoWindowOpen();
}
@Override public void draw(Canvas canvas, MapView mapView, boolean shadow) {
if (shadow)
return;
if (mIcon == null)
return;
final Projection pj = mapView.getProjection();
pj.toPixels(mPosition, mPositionPixels);
int width = mIcon.getIntrinsicWidth();
int height = mIcon.getIntrinsicHeight();
Rect rect = new Rect(0, 0, width, height);
rect.offset(-(int)(mAnchorU*width), -(int)(mAnchorV*height));
mIcon.setBounds(rect);
mIcon.setAlpha((int)(mAlpha*255));
float rotationOnScreen = (mFlat ? -mBearing : mapView.getMapOrientation()-mBearing);
drawAt(canvas, mIcon, mPositionPixels.x, mPositionPixels.y, false, rotationOnScreen);
}
/** Null out the static references when the MapView is detached to prevent memory leaks. */
@Override
public void onDetach(MapView mapView) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
if (mIcon instanceof BitmapDrawable) {
final Bitmap bitmap = ((BitmapDrawable) mIcon).getBitmap();
if (bitmap != null) {
bitmap.recycle();
}
}
}
mIcon=null;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
if (mImage instanceof BitmapDrawable) {
final Bitmap bitmap = ((BitmapDrawable) mImage).getBitmap();
if (bitmap != null) {
bitmap.recycle();
}
}
}
//cleanDefaults();
this.mOnMarkerClickListener=null;
this.mOnMarkerDragListener=null;
this.resource=null;
setRelatedObject(null);
if (mInfoWindow!=mDefaultInfoWindow) {
if (isInfoWindowShown())
closeInfoWindow();
}
// //if we're using the shared info window, this will cause all instances to close
setInfoWindow(null);
onDestroy();
super.onDetach(mapView);
}
/**
* reference https://github.com/MKergall/osmbonuspack/pull/210
*/
public static void cleanDefaults(){
mDefaultIcon = null;
mDefaultInfoWindow = null;
}
public boolean hitTest(final MotionEvent event, final MapView mapView){
final Projection pj = mapView.getProjection();
pj.toPixels(mPosition, mPositionPixels);
final Rect screenRect = pj.getIntrinsicScreenRect();
int x = -mPositionPixels.x + screenRect.left + (int) event.getX();
int y = -mPositionPixels.y + screenRect.top + (int) event.getY();
boolean hit = mIcon.getBounds().contains(x, y);
return hit;
}
@Override public boolean onSingleTapConfirmed(final MotionEvent event, final MapView mapView){
boolean touched = hitTest(event, mapView);
if (touched){
if (mOnMarkerClickListener == null){
return onMarkerClickDefault(this, mapView);
} else {
return mOnMarkerClickListener.onMarkerClick(this, mapView);
}
} else
return touched;
}
public void moveToEventPosition(final MotionEvent event, final MapView mapView){
float offsetY = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, mDragOffsetY, mapView.getContext().getResources().getDisplayMetrics());
final Projection pj = mapView.getProjection();
mPosition = (GeoPoint) pj.fromPixels((int)event.getX(), (int)(event.getY()-offsetY));
mapView.invalidate();
}
@Override public boolean onLongPress(final MotionEvent event, final MapView mapView) {
boolean touched = hitTest(event, mapView);
if (touched){
if (mDraggable){
//starts dragging mode:
mIsDragged = true;
closeInfoWindow();
if (mOnMarkerDragListener != null)
mOnMarkerDragListener.onMarkerDragStart(this);
moveToEventPosition(event, mapView);
}
}
return touched;
}
@Override public boolean onTouchEvent(final MotionEvent event, final MapView mapView) {
if (mDraggable && mIsDragged){
if (event.getAction() == MotionEvent.ACTION_UP) {
mIsDragged = false;
if (mOnMarkerDragListener != null)
mOnMarkerDragListener.onMarkerDragEnd(this);
return true;
} else if (event.getAction() == MotionEvent.ACTION_MOVE){
moveToEventPosition(event, mapView);
if (mOnMarkerDragListener != null)
mOnMarkerDragListener.onMarkerDrag(this);
return true;
} else
return false;
} else
return false;
}
//-- Marker events listener interfaces ------------------------------------
public interface OnMarkerClickListener{
abstract boolean onMarkerClick(Marker marker, MapView mapView);
}
public interface OnMarkerDragListener{
abstract void onMarkerDrag(Marker marker);
abstract void onMarkerDragEnd(Marker marker);
abstract void onMarkerDragStart(Marker marker);
}
/** default behaviour when no click listener is set */
protected boolean onMarkerClickDefault(Marker marker, MapView mapView) {
marker.showInfoWindow();
if (marker.mPanToView)
mapView.getController().animateTo(marker.getPosition());
return true;
}
public int getTextLabelBackgroundColor() {
return mTextLabelBackgroundColor;
}
public void setTextLabelBackgroundColor(int mTextLabelBackgroundColor) {
this.mTextLabelBackgroundColor = mTextLabelBackgroundColor;
}
public int getTextLabelForegroundColor() {
return mTextLabelForegroundColor;
}
public void setTextLabelForegroundColor(int mTextLabelForegroundColor) {
this.mTextLabelForegroundColor = mTextLabelForegroundColor;
}
public int getTextLabelFontSize() {
return mTextLabelFontSize;
}
public void setTextLabelFontSize(int mTextLabelFontSize) {
this.mTextLabelFontSize = mTextLabelFontSize;
}
}