/*
* This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
*
* Copyright 2008-2015 Geosparc nv, http://www.geosparc.com/, Belgium.
*
* The program is available in open source according to the GNU Affero
* General Public License. All contributions in this program are covered
* by the Geomajas Contributors License Agreement. For full licensing
* details, see LICENSE.txt in the project root.
*/
package org.geomajas.gwt.client.gfx.paintable.mapaddon;
import org.geomajas.configuration.client.UnitType;
import org.geomajas.geometry.Coordinate;
import org.geomajas.gwt.client.gfx.PainterVisitor;
import org.geomajas.gwt.client.gfx.paintable.Rectangle;
import org.geomajas.gwt.client.gfx.paintable.Text;
import org.geomajas.gwt.client.gfx.style.FontStyle;
import org.geomajas.gwt.client.gfx.style.ShapeStyle;
import org.geomajas.gwt.client.spatial.Bbox;
import org.geomajas.gwt.client.widget.MapWidget;
import com.google.gwt.i18n.client.NumberFormat;
/**
* <p>
* Map add-on that displays a scalebar.
* </p>
*
* @author Kristof Heirwegh
* @author Pieter De Graef
*/
public class ScaleBar extends MapAddon {
private static final ShapeStyle STYLE_BACKGROUND = new ShapeStyle("white", 0.60f, "white", 0.30f, 1);
private static final ShapeStyle STYLE_MARKER = new ShapeStyle("black", 1f, null, 0f, 0);
private static final FontStyle STYLE_FONT = new FontStyle("#000000", 10, "Arial, Verdana", "normal", "normal");
private static final double METERS_IN_MILE = 1609.344d;
private static final double METERS_IN_YARD = 0.9144d;
private static final double FEET_IN_METER = 3.2808399d;
private static final int HORIZONTAL_PADDING = 3; // px @todo WidgetLayout
private static final int VERTICAL_PADDING = 3; // px @todo WidgetLayout
private static final int MARKER_HEIGHT = 13; // px @todo WidgetLayout
// max width this scale bar should be @todo WidgetLayout
private static final int MAX_SIZE_IN_PIXELS = 125;
private static final int[] LENGTHS = new int[] { 1, 2, 5, 10, 25, 50, 100, 250, 500, 750, 1000, 2000, 5000, 10000,
25000, 50000, 75000, 100000, 250000, 500000, 750000, 1000000, 2000000, 5000000, 10000000 };
// position in lengths array up to where to test for yards (larger values is for miles)
private static final int YARD_STARTING_POINT = 11;
private Rectangle backGround;
private Rectangle leftMarker;
private Rectangle rightMarker;
private Text distance;
private Rectangle bottomLine;
private Rectangle dummy;
/** Unit length in meter. */
private double unitLength;
/** How should units be shown to the user. */
private UnitType unitType;
// -- for internal use, holds the last calculated best value
private int widthInUnits;
// -- for internal use, holds the last calculated best value
private int widthInPixels;
// -- for internal use, for UnitType.ENGLISH only
private boolean widthInUnitsIsMiles;
private final MapWidget map;
/**
* Construct a scale bar.
*
* @param id object id
* @param map map
*/
public ScaleBar(String id, MapWidget map) {
super(id, MAX_SIZE_IN_PIXELS, MARKER_HEIGHT + VERTICAL_PADDING * 2);
setRepaintOnMapViewChange(true);
this.map = map;
}
@Override
public void accept(PainterVisitor visitor, Object group, Bbox bounds, boolean recursive) {
map.getVectorContext().drawGroup(group, this);
adjustScale(map.getMapModel().getMapView().getCurrentScale());
// Draw a dummy at 0,0 so that Internet Explorer knows where coordinate 0,0 is. If this is not drawn, the text
// will disappear, because the parent group will have coordinate 0,0 at the upper left corner of the union of
// all the rectangles that are drawn here.
map.getVectorContext().drawRectangle(this, dummy.getId(), dummy.getBounds(), (ShapeStyle) dummy.getStyle());
map.getVectorContext().drawRectangle(this, backGround.getId(), backGround.getBounds(),
backGround.getStyle());
map.getVectorContext().drawRectangle(this, bottomLine.getId(), bottomLine.getBounds(),
bottomLine.getStyle());
map.getVectorContext().drawRectangle(this, leftMarker.getId(), leftMarker.getBounds(),
leftMarker.getStyle());
map.getVectorContext().drawRectangle(this, rightMarker.getId(), rightMarker.getBounds(),
rightMarker.getStyle());
map.getVectorContext().drawText(this, distance.getId(), distance.getContent(), distance.getPosition(),
distance.getStyle());
}
@Override
public void onDraw() {
}
@Override
public void onRemove() {
}
/**
* Initialize scale bar.
*
* @param unitType
* how are units shown (english/metric/crs)
* @param unitLength
* length of a unit in meter
* @param position
* coordinate where scalebar should be drawn on the graphics context
*/
public void initialize(UnitType unitType, double unitLength, Coordinate position) {
if (null == unitType) {
throw new IllegalArgumentException("please provide a unitType");
}
this.unitType = unitType;
this.unitLength = unitLength;
backGround = new Rectangle((null == getId() ? "" : getId() + "-bg"));
backGround.setBounds(new Bbox(0, 0, 5, MARKER_HEIGHT + 2 * VERTICAL_PADDING));
backGround.setStyle(STYLE_BACKGROUND);
leftMarker = new Rectangle((null == getId() ? "" : getId() + "-lm"));
leftMarker.setStyle(STYLE_MARKER);
leftMarker.setBounds(new Bbox(0, 0, 1, MARKER_HEIGHT));
rightMarker = new Rectangle((null == getId() ? "" : getId() + "-rm"));
rightMarker.setStyle(STYLE_MARKER);
rightMarker.setBounds(new Bbox(0, 0, 1, MARKER_HEIGHT));
bottomLine = new Rectangle((null == getId() ? "" : getId() + "-bm"));
bottomLine.setStyle(STYLE_MARKER);
bottomLine.setBounds(new Bbox(0, 0, 0, 1));
distance = new Text((null == getId() ? "" : getId() + "-text"));
distance.setStyle(STYLE_FONT);
dummy = new Rectangle(getId() + "-dummy");
dummy.setStyle(new ShapeStyle("#FFFFFF", 0, "#FFFFFF", 0, 0));
dummy.setBounds(new Bbox(0, 0, 1, 1));
}
// ----------------------------------------------------------
/**
* Get unit length.
*
* @return unit length
*/
public double getUnitLength() {
return unitLength;
}
/**
* Get unit type.
*
* @return unit type
*/
public UnitType getUnitType() {
return unitType;
}
/**
* Adjust the scale bar to reflect the given scale.
*
* @param scale scale
*/
private void adjustScale(double scale) {
if (null == unitType) {
throw new IllegalStateException("Please initialize scalebar before using");
}
calculateBestFit(scale);
updatePaintables();
}
/**
* Set map size.
*
* @param mapWidth map width
* @param mapHeight map height
*/
public void setMapSize(int mapWidth, int mapHeight) {
super.setMapSize(mapWidth, mapHeight);
if (null == unitType) {
throw new IllegalStateException("Please initialize scale bar before using");
}
backGround.getBounds().setX(getUpperLeftCorner().getX());
backGround.getBounds().setY(getUpperLeftCorner().getY());
bottomLine.getBounds().setX(getUpperLeftCorner().getX() + HORIZONTAL_PADDING);
bottomLine.getBounds().setY(getUpperLeftCorner().getY() + VERTICAL_PADDING + MARKER_HEIGHT);
leftMarker.getBounds().setX(getUpperLeftCorner().getX() + HORIZONTAL_PADDING);
leftMarker.getBounds().setY(getUpperLeftCorner().getY() + VERTICAL_PADDING);
rightMarker.getBounds().setY(getUpperLeftCorner().getY() + VERTICAL_PADDING);
distance.setPosition(new Coordinate(getUpperLeftCorner().getX() + HORIZONTAL_PADDING + 6,
getUpperLeftCorner().getY() + VERTICAL_PADDING));
}
// ----------------------------------------------------------
private void updatePaintables() {
distance.setContent(formatUnits(widthInUnits));
backGround.getBounds().setWidth(widthInPixels + 2 * HORIZONTAL_PADDING);
rightMarker.getBounds().setX(leftMarker.getBounds().getX() + widthInPixels - 1);
bottomLine.getBounds().setWidth(widthInPixels);
}
/**
* Set the rounded value (from the lengths array) which fits the closest into the maxSizeInPixels for the given
* scale.
*
* @param scale scale
*/
public void calculateBestFit(double scale) {
int len = 0;
long px = 0;
if (UnitType.ENGLISH.equals(unitType)) {
// try miles
for (int i = LENGTHS.length - 1; i > -1; i--) {
len = LENGTHS[i];
px = Math.round((len * scale / unitLength) * METERS_IN_MILE);
if (px < MAX_SIZE_IN_PIXELS) {
break;
}
}
// try yards
if (px > MAX_SIZE_IN_PIXELS) {
for (int i = YARD_STARTING_POINT; i > -1; i--) {
len = LENGTHS[i];
px = Math.round((len * scale / unitLength) * METERS_IN_YARD);
if (px < MAX_SIZE_IN_PIXELS) {
break;
}
}
widthInUnitsIsMiles = false;
} else {
widthInUnitsIsMiles = true;
}
} else if (UnitType.ENGLISH_FOOT.equals(unitType)) {
// try miles
for (int i = LENGTHS.length - 1; i > -1; i--) {
len = LENGTHS[i];
px = Math.round((len * scale / unitLength) * METERS_IN_MILE);
if (px < MAX_SIZE_IN_PIXELS) {
break;
}
}
// try feet
if (px > MAX_SIZE_IN_PIXELS) {
for (int i = YARD_STARTING_POINT; i > -1; i--) {
len = LENGTHS[i];
px = Math.round((len * scale / unitLength) / FEET_IN_METER);
if (px < MAX_SIZE_IN_PIXELS) {
break;
}
}
widthInUnitsIsMiles = false;
} else {
widthInUnitsIsMiles = true;
}
} else {
for (int i = LENGTHS.length - 1; i > -1; i--) {
len = LENGTHS[i];
px = Math.round(len * scale / unitLength);
if (px < MAX_SIZE_IN_PIXELS) {
break;
}
}
}
widthInUnits = len;
widthInPixels = (int) px;
}
/**
* Is the unit width in miles? Provided for unit testing only.
*
* @return true when current unit is miles.
*/
protected boolean isUnitWidthInMiles() {
return widthInUnitsIsMiles;
}
/**
* Get width in units.
*
* @return width in units
*/
public int getWidthInUnits() {
return widthInUnits;
}
/**
* Get width in pixels.
*
* @return width in pixels
*/
public int getWidthInPixels() {
return widthInPixels;
}
/**
* Convert distance (in CRS units) to human readable format (including unit indicator).
*
* @param units unit
* @return human readable string in unit type
*/
String formatUnits(int units) {
switch (unitType) {
case ENGLISH:
return NumberFormat.getDecimalFormat().format(units) + (widthInUnitsIsMiles ? " mi" : " yd");
case ENGLISH_FOOT:
return NumberFormat.getDecimalFormat().format(units) + (widthInUnitsIsMiles ? " mi" : " ft");
case METRIC:
if (units < 10000) {
return NumberFormat.getDecimalFormat().format(units) + " m";
} else {
return NumberFormat.getDecimalFormat().format((double) units / 1000) + " km";
}
case CRS:
return NumberFormat.getDecimalFormat().format(units) + " u";
default:
throw new IllegalStateException("Unknown unit type " + unitType);
}
}
}