blob: 1ac929684a95611990633dc58f8d1d5d7ad0ebe8 [file] [log] [blame]
package org.osmdroid.views.overlay;
/**
* ScaleBarOverlay.java
*
* Puts a scale bar in the top-left corner of the screen, offset by a configurable
* number of pixels. The bar is scaled to 1-inch length by querying for the physical
* DPI of the screen. The size of the bar is printed between the tick marks. A
* vertical (longitude) scale can be enabled. Scale is printed in metric (kilometers,
* meters), imperial (miles, feet) and nautical (nautical miles, feet).
*
* Author: Erik Burrows, Griffin Systems LLC
* erik@griffinsystems.org
*
* Change Log:
* 2010-10-08: Inclusion to osmdroid trunk
* 2015-12-17: Allow for top, bottom, left or right placement by W. Strickling
*
* Usage:
* <code>
* MapView map = new MapView(...);
* ScaleBarOverlay scaleBar = new ScaleBarOverlay(map); // Thiw is an important change of calling!
*
* map.getOverlays().add(scaleBar);
* </code>
*
* To Do List:
* 1. Allow for top, bottom, left or right placement. // done in this changement
* 2. Scale bar to precise displayed scale text after rounding.
*
*/
import java.lang.reflect.Field;
import java.util.Locale;
import org.osmdroid.api.IGeoPoint;
import org.osmdroid.library.R;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.util.constants.GeoConstants;
import org.osmdroid.views.MapView;
import org.osmdroid.views.Projection;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.view.WindowManager;
public class ScaleBarOverlay extends Overlay implements GeoConstants {
// ===========================================================
// Fields
// ===========================================================
private static final Rect sTextBoundsRect = new Rect();
public enum UnitsOfMeasure {
metric, imperial, nautical
}
// Defaults
int xOffset = 10;
int yOffset = 10;
int minZoom = 0;
UnitsOfMeasure unitsOfMeasure = UnitsOfMeasure.metric;
boolean latitudeBar = true;
boolean longitudeBar = false;
protected boolean alignBottom = false;
protected boolean alignRight = false;
// Internal
private Context context;
private MapView mMapView;
protected final Path barPath = new Path();
protected final Rect latitudeBarRect = new Rect();
protected final Rect longitudeBarRect = new Rect();
private int lastZoomLevel = -1;
private double lastLatitude = 0.0;
public float xdpi;
public float ydpi;
public int screenWidth;
public int screenHeight;
private Paint barPaint;
private Paint bgPaint;
private Paint textPaint;
private boolean centred = false;
private boolean adjustLength = false;
private float maxLength;
// ===========================================================
// Constructors
// ===========================================================
public ScaleBarOverlay(final MapView mapView) {
super();
this.mMapView = mapView;
this.context = mapView.getContext();
final DisplayMetrics dm = context.getResources().getDisplayMetrics();
this.barPaint = new Paint();
this.barPaint.setColor(Color.BLACK);
this.barPaint.setAntiAlias(true);
this.barPaint.setStyle(Style.STROKE);
this.barPaint.setAlpha(255);
this.barPaint.setStrokeWidth(2 * dm.density);
this.bgPaint = null;
this.textPaint = new Paint();
this.textPaint.setColor(Color.BLACK);
this.textPaint.setAntiAlias(true);
this.textPaint.setStyle(Style.FILL);
this.textPaint.setAlpha(255);
this.textPaint.setTextSize(10 * dm.density);
this.xdpi = dm.xdpi;
this.ydpi = dm.ydpi;
this.screenWidth = dm.widthPixels;
this.screenHeight = dm.heightPixels;
// DPI corrections for specific models
String manufacturer = null;
try {
final Field field = android.os.Build.class.getField("MANUFACTURER");
manufacturer = (String) field.get(null);
} catch (final Exception ignore) {
}
if ("motorola".equals(manufacturer) && "DROIDX".equals(android.os.Build.MODEL)) {
// If the screen is rotated, flip the x and y dpi values
WindowManager windowManager = (WindowManager) this.context
.getSystemService(Context.WINDOW_SERVICE);
if (windowManager.getDefaultDisplay().getOrientation() > 0) {
this.xdpi = (float) (this.screenWidth / 3.75);
this.ydpi = (float) (this.screenHeight / 2.1);
} else {
this.xdpi = (float) (this.screenWidth / 2.1);
this.ydpi = (float) (this.screenHeight / 3.75);
}
} else if ("motorola".equals(manufacturer) && "Droid".equals(android.os.Build.MODEL)) {
// http://www.mail-archive.com/android-developers@googlegroups.com/msg109497.html
this.xdpi = 264;
this.ydpi = 264;
}
// set default max length to 1 inch
maxLength = 2.54f;
}
// ===========================================================
// Getter & Setter
// ===========================================================
/**
* Sets the minimum zoom level for the scale bar to be drawn.
*
* @param zoom
* minimum zoom level
*/
public void setMinZoom(final int zoom) {
this.minZoom = zoom;
}
/**
* Sets the scale bar screen offset for the bar. Note: if the bar is set to be drawn centered,
* this will be the middle of the bar, otherwise the top left corner.
*
* @param x
* x screen offset
* @param y
* z screen offset
*/
public void setScaleBarOffset(final int x, final int y) {
xOffset = x;
yOffset = y;
}
/**
* Sets the bar's line width. (the default is 2)
*
* @param width
* the new line width
*/
public void setLineWidth(final float width) {
this.barPaint.setStrokeWidth(width);
}
/**
* Sets the text size. (the default is 12)
*
* @param size
* the new text size
*/
public void setTextSize(final float size) {
this.textPaint.setTextSize(size);
}
/**
* Sets the units of measure to be shown in the scale bar
*/
public void setUnitsOfMeasure(UnitsOfMeasure unitsOfMeasure) {
this.unitsOfMeasure = unitsOfMeasure;
lastZoomLevel = -1; // Force redraw of scalebar
}
/**
* Gets the units of measure to be shown in the scale bar
*/
public UnitsOfMeasure getUnitsOfMeasure() {
return unitsOfMeasure;
}
/**
* Latitudinal / horizontal scale bar flag
*
* @param latitude
*/
public void drawLatitudeScale(final boolean latitude) {
this.latitudeBar = latitude;
lastZoomLevel = -1; // Force redraw of scalebar
}
/**
* Longitudinal / vertical scale bar flag
*
* @param longitude
*/
public void drawLongitudeScale(final boolean longitude) {
this.longitudeBar = longitude;
lastZoomLevel = -1; // Force redraw of scalebar
}
/**
* Flag to draw the bar centered around the set offset coordinates or to the right/bottom of the
* coordinates (default)
*
* @param centred
* set true to centre the bar around the given screen coordinates
*/
public void setCentred(final boolean centred) {
this.centred = centred;
alignBottom = !centred;
alignRight = !centred;
lastZoomLevel = -1; // Force redraw of scalebar
}
public void setAlignBottom(final boolean alignBottom) {
this.centred = false;
this.alignBottom = alignBottom;
lastZoomLevel = -1; // Force redraw of scalebar
}
public void setAlignRight(final boolean alignRight) {
this.centred = false;
this.alignRight = alignRight;
lastZoomLevel = -1; // Force redraw of scalebar
}
/**
* Return's the paint used to draw the bar
*
* @return the paint used to draw the bar
*/
public Paint getBarPaint() {
return barPaint;
}
/**
* Sets the paint for drawing the bar
*
* @param pBarPaint
* bar drawing paint
*/
public void setBarPaint(final Paint pBarPaint) {
if (pBarPaint == null) {
throw new IllegalArgumentException("pBarPaint argument cannot be null");
}
barPaint = pBarPaint;
lastZoomLevel = -1; // Force redraw of scalebar
}
/**
* Returns the paint used to draw the text
*
* @return the paint used to draw the text
*/
public Paint getTextPaint() {
return textPaint;
}
/**
* Sets the paint for drawing the text
*
* @param pTextPaint
* text drawing paint
*/
public void setTextPaint(final Paint pTextPaint) {
if (pTextPaint == null) {
throw new IllegalArgumentException("pTextPaint argument cannot be null");
}
textPaint = pTextPaint;
lastZoomLevel = -1; // Force redraw of scalebar
}
/**
* Sets the background paint. Set to null to disable drawing of background (default)
*
* @param pBgPaint
* the paint for colouring the bar background
*/
public void setBackgroundPaint(final Paint pBgPaint) {
bgPaint = pBgPaint;
lastZoomLevel = -1; // Force redraw of scalebar
}
/**
* If enabled, the bar will automatically adjust the length to reflect a round number (starting
* with 1, 2 or 5). If disabled, the bar will always be drawn in full length representing a
* fractional distance.
*/
public void setEnableAdjustLength(boolean adjustLength) {
this.adjustLength = adjustLength;
lastZoomLevel = -1; // Force redraw of scalebar
}
/**
* Sets the maximum bar length. If adjustLength is disabled this will match exactly the length
* of the bar. If adjustLength is enabled, the bar will be shortened to reflect a round number
* in length.
*
* @param pMaxLengthInCm
* maximum length of the bar in the screen in cm. Default is 2.54 (=1 inch)
*/
public void setMaxLength(final float pMaxLengthInCm) {
this.maxLength = pMaxLengthInCm;
lastZoomLevel = -1; // Force redraw of scalebar
}
// ===========================================================
// Methods from SuperClass/Interfaces
// ===========================================================
@Override
public void draw(Canvas c, MapView mapView, boolean shadow) {
if (shadow) {
return;
}
// If map view is animating, don't update, scale will be wrong.
if (mapView.isAnimating()) {
return;
}
final int zoomLevel = mapView.getZoomLevel();
if (zoomLevel >= minZoom) {
final Projection projection = mapView.getProjection();
if (projection == null) {
return;
}
int _screenWidth = mapView.getWidth();
int _screenHeight = mapView.getHeight();
boolean screenSizeChanged = _screenHeight!=screenHeight || _screenWidth != screenWidth;
screenHeight = _screenHeight;
screenWidth = _screenWidth;
final IGeoPoint center = projection.fromPixels(screenWidth / 2, screenHeight / 2, null);
if (zoomLevel != lastZoomLevel
|| (int) center.getLatitude() != (int) lastLatitude || screenSizeChanged) {
lastZoomLevel = zoomLevel;
lastLatitude = center.getLatitude();
rebuildBarPath(projection);
}
int offsetX = (int) xOffset;
int offsetY = (int) yOffset;
if (alignBottom) offsetY *=-1;
if (alignRight ) offsetX *=-1;
if (centred && latitudeBar)
offsetX += -latitudeBarRect.width() / 2;
if (centred && longitudeBar)
offsetY += -longitudeBarRect.height() / 2;
projection.save(c, false, true);
c.translate(offsetX, offsetY);
if (latitudeBar && bgPaint != null)
c.drawRect(latitudeBarRect, bgPaint);
if (longitudeBar && bgPaint != null) {
// Don't draw on top of latitude background...
int offsetTop = latitudeBar ? latitudeBarRect.height() : 0;
c.drawRect(longitudeBarRect.left, longitudeBarRect.top + offsetTop,
longitudeBarRect.right, longitudeBarRect.bottom, bgPaint);
}
c.drawPath(barPath, barPaint);
if (latitudeBar) {
drawLatitudeText(c, projection);
}
if (longitudeBar) {
drawLongitudeText(c, projection);
}
projection.restore(c, true);
}
}
// ===========================================================
// Methods
// ===========================================================
public void disableScaleBar() {
setEnabled(false);
}
public void enableScaleBar() {
setEnabled(true);
}
private void drawLatitudeText(final Canvas canvas, final Projection projection) {
// calculate dots per centimeter
int xdpcm = (int) ((float) xdpi / 2.54);
// get length in pixel
int xLen = (int) (maxLength * xdpcm);
// Two points, xLen apart, at scale bar screen location
IGeoPoint p1 = projection.fromPixels((screenWidth / 2) - (xLen / 2), yOffset, null);
IGeoPoint p2 = projection.fromPixels((screenWidth / 2) + (xLen / 2), yOffset, null);
// get distance in meters between points
final double xMeters = ((GeoPoint) p1).distanceToAsDouble(p2);
// get adjusted distance, shortened to the next lower number starting with 1, 2 or 5
final double xMetersAdjusted = this.adjustLength ? adjustScaleBarLength(xMeters) : xMeters;
// get adjusted length in pixels
final int xBarLengthPixels = (int) (xLen * xMetersAdjusted / xMeters);
// create text
final String xMsg = scaleBarLengthText(xMetersAdjusted);
textPaint.getTextBounds(xMsg, 0, xMsg.length(), sTextBoundsRect);
final int xTextSpacing = (int) (sTextBoundsRect.height() / 5.0);
float x = xBarLengthPixels / 2 - sTextBoundsRect.width() / 2;
if (alignRight) x+= screenWidth -xBarLengthPixels;
float y;
if (alignBottom) { y = screenHeight -xTextSpacing*2; }
else y = sTextBoundsRect.height() + xTextSpacing;
canvas.drawText(xMsg, x, y, textPaint);
}
private void drawLongitudeText(final Canvas canvas, final Projection projection) {
// calculate dots per centimeter
int ydpcm = (int) ((float) ydpi / 2.54);
// get length in pixel
int yLen = (int) (maxLength * ydpcm);
// Two points, yLen apart, at scale bar screen location
IGeoPoint p1 = projection
.fromPixels(screenWidth / 2, (screenHeight / 2) - (yLen / 2), null);
IGeoPoint p2 = projection
.fromPixels(screenWidth / 2, (screenHeight / 2) + (yLen / 2), null);
// get distance in meters between points
final double yMeters = ((GeoPoint) p1).distanceToAsDouble(p2);
// get adjusted distance, shortened to the next lower number starting with 1, 2 or 5
final double yMetersAdjusted = this.adjustLength ? adjustScaleBarLength(yMeters) : yMeters;
// get adjusted length in pixels
final int yBarLengthPixels = (int) (yLen * yMetersAdjusted / yMeters);
// create text
final String yMsg = scaleBarLengthText(yMetersAdjusted);
textPaint.getTextBounds(yMsg, 0, yMsg.length(), sTextBoundsRect);
final int yTextSpacing = (int) (sTextBoundsRect.height() / 5.0);
float x;
if (alignRight) {x = screenWidth -yTextSpacing*2;}
else x = sTextBoundsRect.height() + yTextSpacing;
float y = yBarLengthPixels / 2 + sTextBoundsRect.width() / 2;
if (alignBottom) y+= screenHeight -yBarLengthPixels;
canvas.save();
canvas.rotate(-90, x, y);
canvas.drawText(yMsg, x, y, textPaint);
canvas.restore();
}
protected void rebuildBarPath(final Projection projection) { //** modified to protected
// We want the scale bar to be as long as the closest round-number miles/kilometers
// to 1-inch at the latitude at the current center of the screen.
// calculate dots per centimeter
int xdpcm = (int) ((float) xdpi / 2.54);
int ydpcm = (int) ((float) ydpi / 2.54);
// get length in pixel
int xLen = (int) (maxLength * xdpcm);
int yLen = (int) (maxLength * ydpcm);
// Two points, xLen apart, at scale bar screen location
IGeoPoint p1 = projection.fromPixels((screenWidth / 2) - (xLen / 2), yOffset, null);
IGeoPoint p2 = projection.fromPixels((screenWidth / 2) + (xLen / 2), yOffset, null);
// get distance in meters between points
final double xMeters = ((GeoPoint) p1).distanceToAsDouble(p2);
// get adjusted distance, shortened to the next lower number starting with 1, 2 or 5
final double xMetersAdjusted = this.adjustLength ? adjustScaleBarLength(xMeters) : xMeters;
// get adjusted length in pixels
final int xBarLengthPixels = (int) (xLen * xMetersAdjusted / xMeters);
// Two points, yLen apart, at scale bar screen location
p1 = projection.fromPixels(screenWidth / 2, (screenHeight / 2) - (yLen / 2), null);
p2 = projection.fromPixels(screenWidth / 2, (screenHeight / 2) + (yLen / 2), null);
// get distance in meters between points
final double yMeters = ((GeoPoint) p1).distanceToAsDouble(p2);
// get adjusted distance, shortened to the next lower number starting with 1, 2 or 5
final double yMetersAdjusted = this.adjustLength ? adjustScaleBarLength(yMeters) : yMeters;
// get adjusted length in pixels
final int yBarLengthPixels = (int) (yLen * yMetersAdjusted / yMeters);
// create text
final String xMsg = scaleBarLengthText(xMetersAdjusted);
final Rect xTextRect = new Rect();
textPaint.getTextBounds(xMsg, 0, xMsg.length(), xTextRect);
int xTextSpacing = (int) (xTextRect.height() / 5.0);
// create text
final String yMsg = scaleBarLengthText(yMetersAdjusted);
final Rect yTextRect = new Rect();
textPaint.getTextBounds(yMsg, 0, yMsg.length(), yTextRect);
int yTextSpacing = (int) (yTextRect.height() / 5.0);
int xTextHeight = xTextRect.height();
int yTextHeight = yTextRect.height();
barPath.rewind();
//** alignBottom ad-ons
int barOriginX = 0;
int barOriginY = 0;
int barToX = xBarLengthPixels;
int barToY = yBarLengthPixels;
if (alignBottom) {
xTextSpacing *= -1;
xTextHeight *= -1;
barOriginY = mMapView.getHeight();
barToY = barOriginY -yBarLengthPixels;
}
if (alignRight) {
yTextSpacing *= -1;
yTextHeight *= -1;
barOriginX = mMapView.getWidth();
barToX = barOriginX -xBarLengthPixels;
}
if (latitudeBar) {
// draw latitude bar
barPath.moveTo(barToX, barOriginY +xTextHeight + xTextSpacing * 2);
barPath.lineTo(barToX, barOriginY);
barPath.lineTo(barOriginX, barOriginY);
if (!longitudeBar) {
barPath.lineTo(barOriginX, barOriginY +xTextHeight + xTextSpacing * 2);
}
latitudeBarRect.set(barOriginX, barOriginY, barToX, barOriginY +xTextHeight + xTextSpacing * 2);
}
if (longitudeBar) {
// draw longitude bar
if (!latitudeBar) {
barPath.moveTo(barOriginX +yTextHeight + yTextSpacing * 2, barOriginY);
barPath.lineTo(barOriginX, barOriginY);
}
barPath.lineTo(barOriginX, barToY);
barPath.lineTo(barOriginX +yTextHeight + yTextSpacing * 2, barToY);
longitudeBarRect.set(barOriginX, barOriginY, barOriginX +yTextHeight + yTextSpacing * 2, barToY);
}
}
/**
* Returns a reduced length that starts with 1, 2 or 5 and trailing zeros. If set to nautical or
* imperial the input will be transformed before and after the reduction so that the result
* holds in that respective unit.
*
* @param length
* length to round
* @return reduced, rounded (in m, nm or mi depending on setting) result
*/
private double adjustScaleBarLength(double length) {
long pow = 0;
boolean feet = false;
if (unitsOfMeasure == UnitsOfMeasure.imperial) {
if (length >= GeoConstants.METERS_PER_STATUTE_MILE / 5)
length = length / GeoConstants.METERS_PER_STATUTE_MILE;
else {
length = length * GeoConstants.FEET_PER_METER;
feet = true;
}
} else if (unitsOfMeasure == UnitsOfMeasure.nautical) {
if (length >= GeoConstants.METERS_PER_NAUTICAL_MILE / 5)
length = length / GeoConstants.METERS_PER_NAUTICAL_MILE;
else {
length = length * GeoConstants.FEET_PER_METER;
feet = true;
}
}
while (length >= 10) {
pow++;
length /= 10;
}
while (length < 1 && length > 0) {
pow--;
length *= 10;
}
if (length < 2) {
length = 1;
} else if (length < 5) {
length = 2;
} else {
length = 5;
}
if (feet)
length = length / GeoConstants.FEET_PER_METER;
else if (unitsOfMeasure == UnitsOfMeasure.imperial)
length = length * GeoConstants.METERS_PER_STATUTE_MILE;
else if (unitsOfMeasure == UnitsOfMeasure.nautical)
length = length * GeoConstants.METERS_PER_NAUTICAL_MILE;
length *= Math.pow(10, pow);
return length;
}
protected String scaleBarLengthText(final double meters) {
switch (unitsOfMeasure) {
default:
case metric:
if (meters >= 1000 * 5) {
return getScaleString(R.string.format_distance_kilometers, "%.0f", meters / 1000);
} else if (meters >= 1000 / 5) {
return getScaleString(R.string.format_distance_kilometers, "%.1f", meters / 1000);
} else if (meters >= 20){
return getScaleString(R.string.format_distance_meters, "%.0f", meters);
} else {
return getScaleString(R.string.format_distance_meters, "%.2f", meters);
}
case imperial:
if (meters >= METERS_PER_STATUTE_MILE * 5) {
return getScaleString(R.string.format_distance_miles, "%.0f", meters / METERS_PER_STATUTE_MILE);
} else if (meters >= METERS_PER_STATUTE_MILE / 5) {
return getScaleString(R.string.format_distance_miles, "%.1f", meters / METERS_PER_STATUTE_MILE);
} else {
return getScaleString(R.string.format_distance_feet, "%.0f", meters * FEET_PER_METER);
}
case nautical:
if (meters >= METERS_PER_NAUTICAL_MILE * 5) {
return getScaleString(R.string.format_distance_nautical_miles, "%.0f", meters / METERS_PER_NAUTICAL_MILE);
} else if (meters >= METERS_PER_NAUTICAL_MILE / 5) {
return getScaleString(R.string.format_distance_nautical_miles, "%.1f", meters / METERS_PER_NAUTICAL_MILE);
} else {
return getScaleString(R.string.format_distance_feet, "%.0f", meters * FEET_PER_METER);
}
}
}
@Override
public void onDetach(MapView mapView){
this.context=null;
this.mMapView=null;
barPaint=null;
bgPaint=null;
textPaint=null;
}
/**
* @since 5.6.6
*/
private String getScaleString(final int pStringResId, final String pFormat, final double pValue) {
return context.getResources().getString(pStringResId, String.format(Locale.getDefault(), pFormat, pValue));
}
}