/*
* 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.widget;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import org.geomajas.annotation.Api;
import org.geomajas.gwt.client.i18n.I18nProvider;
import org.geomajas.gwt.client.map.MapView;
import org.geomajas.gwt.client.map.event.MapViewChangedEvent;
import org.geomajas.gwt.client.map.event.MapViewChangedHandler;
import com.smartgwt.client.types.KeyNames;
import com.smartgwt.client.widgets.form.DynamicForm;
import com.smartgwt.client.widgets.form.fields.ComboBoxItem;
import com.smartgwt.client.widgets.form.fields.events.ChangedEvent;
import com.smartgwt.client.widgets.form.fields.events.ChangedHandler;
import com.smartgwt.client.widgets.form.fields.events.KeyPressEvent;
import com.smartgwt.client.widgets.form.fields.events.KeyPressHandler;
import com.smartgwt.client.widgets.form.validator.CustomValidator;
/**
* <p>
* A drop down selection box for setting and displaying the current scale of a map. The displayed scale is a relative
* scale and is expressed as an abstract number, as opposed to the MapView scale which is expressed in pixels per map
* unit. An arbitrary scale can be filled in by the user. Implemented as a canvas so it can be added where needed.
* </p>
* <p>
* This widget has the option to add new scale levels to the list, as the user types them (and presses 'Enter'). By
* default this option is turned off. Turn it on using the <code>setUpdatingScaleList</code> method.
* </p>
*
* @author Jan De Moerloose
* @author An Buyle
*
* @since 1.6.0
*/
@Api
public class ScaleSelect extends DynamicForm implements KeyPressHandler, ChangedHandler, MapViewChangedHandler {
private MapView mapView;
private MapWidget mapWidget;
private ComboBoxItem scaleItem;
private LinkedHashMap<String, Double> valueToScale = new LinkedHashMap<String, Double>();
// pixel length in map units
private double pixelLength;
private boolean updatingScaleList;
// Full list of scales. These will not always be shown (depends on map size and maximum bounds), but they are
// stored here for when the map resizes. At the point the visible set needs to be e-evaluated.
private List<Double> scaleList;
private int precision;
private int significantDigits;
private boolean isScaleItemInitialized; /* Initially false, if true a legal getPixelPerUnit() could be retrieved
from the mapWidget and the selectItem is in a normal state (a list of possible scales, and display value
shows the current scale of the mapView */
// -------------------------------------------------------------------------
// Constructor:
// -------------------------------------------------------------------------
/**
* Constructs a ScaleSelect that acts on the specified map view.
*
* @param mapView
* the map view
* @param pixelLength
* the pixel length in map units
* @since 1.6.0
* @deprecated use {@link #ScaleSelect(MapWidget, double)} instead
*/
@Api
@Deprecated
public ScaleSelect(MapView mapView, double pixelLength) {
this.mapView = mapView;
this.pixelLength = pixelLength; // Possibly NaN if called by new ScaleSelect(MapWidget) and
// mapWidget hasn't been fully initialized yet
init();
updateResolutions();
// Does nothing if mapWidget provided in constructor, hasn't been fully initialized yet.
// Else, will call setDisplayScale() if scaleItem value's hasn't been set yet
// or scaleItem's displayValue is null or empty
if (null != mapView && isScaleItemInitialized) {
setDisplayScale(mapView.getCurrentScale() * ScaleSelect.this.pixelLength);
}
}
/**
* Constructs a ScaleSelect that acts on the specified map widget.
*
* @param mapWidget map widget, must be non-null
* @since 1.15.0
*/
@Api
public ScaleSelect(MapWidget mapWidget, double pixelLength) {
this.mapView = mapWidget.getMapModel().getMapView();
this.mapWidget = mapWidget;
this.pixelLength = pixelLength;
init();
mapWidget.getMapModel().runWhenInitialized(new Runnable() {
@Override
public void run() {
updateResolutions();
// Does nothing if mapWidget provided in constructor, hasn't been fully initialized yet.
// Else, will call setDisplayScale() if scaleItem value's hasn't been set yet
// or scaleItem's displayValue is null or empty
if (null != mapView && isScaleItemInitialized) {
setDisplayScale(mapView.getCurrentScale() * ScaleSelect.this.pixelLength);
}
/* refreshPixelLength();
if (!Double.isNaN(ScaleSelect.this.pixelLength) && (0.0 != ScaleSelect.this.pixelLength)) {
if (!isScaleItemInitialized) {
scaleItem.clearValue();
updateResolutions();
setDisplayScale(mapView.getCurrentScale() * ScaleSelect.this.pixelLength);
} else if (scaleItem.getValueAsString() == null || "".equals(scaleItem.getValueAsString())) {
setDisplayScale(mapView.getCurrentScale() * ScaleSelect.this.pixelLength);
} else if (scaleItem.getDisplayValue() == null
|| "".equals(scaleItem.getDisplayValue())) {
setDisplayScale(mapView.getCurrentScale() * ScaleSelect.this.pixelLength);
}
}*/
}
});
}
/**
* Constructs a ScaleSelect that acts on the specified map widget.
*
* @param mapWidget map widget, must be non-null
* @since 1.10.0
*/
@Api
public ScaleSelect(MapWidget mapWidget) {
this(mapWidget, mapWidget.getPixelPerUnit());
}
// -------------------------------------------------------------------------
// Public methods:
// -------------------------------------------------------------------------
/**
* Set the specified relative scale values in the select item.
*
* @param scales
* array of relative scales (should be multiplied by pixelLength if in pixels/m)
* @since 1.6.0
*/
@Api
public void setScales(Double... scales) {
// Sort decreasing and store the list:
Arrays.sort(scales, Collections.reverseOrder());
scaleList = Arrays.asList(scales);
// Apply the requested scales on the SelectItem. Make sure only available scales are added:
updateScaleList();
}
/**
* Set the precision for displaying the scales.
* <p/>
* For example use 1000 to make sure a scale of 1:12345 is displayed as 1:12000.
* <p/>
* Use zero to leaves all scales untouched.
*
* @param precision precision for displaying scales
* @since 1.10.0
*/
@Api
public void setPrecision(int precision) {
this.precision = precision;
updateScaleList();
}
/**
* Set the maximum number of significant digits to be displayed.
* <p/>
* For example when there are two scales 1:2000 and 1:12345000 and using the setting 3 for significantDigits these
* will be displayed as 1:2000 and 1:12300000.
* <p/>
* User zero for unlimited maximum,
*
* @param significantDigits number of significant digits to display in the scales
* @since 1.10.0
*/
@Api
public void setSignificantDigits(int significantDigits) {
this.significantDigits = significantDigits;
updateScaleList();
}
/**
* Return the {@link MapView} object to which this widget is connected.
*
* @return {@link MapView} object which is connected to this widget
*/
public MapView getMapView() {
return mapView;
}
/**
* When typing custom scale levels in the select item, should these new scale levels be added to the list or not?
*
* @return true when new (typed) scales should be added in list
*/
public boolean isUpdatingScaleList() {
return updatingScaleList;
}
/**
* When typing custom scale levels in the select item, should these new scale levels be added to the list or not?
* The default value is false, which means that the list of scales in the select item does not change.
*
* @param updatingScaleList
* Should the new scale be added?
*/
public void setUpdatingScaleList(boolean updatingScaleList) {
this.updatingScaleList = updatingScaleList;
}
/** Will be called when the MapView changes (caused by e.g zooming); updates the select item to the correct scale.
*
* Note: This method implements MapViewChangedHandler's (only) method, this handler is registered in this.init())
*
* @see org.geomajas.gwt.client.map.event.MapViewChangedHandler#onMapViewChanged
* (org.geomajas.gwt.client.map.event.MapViewChangedEvent)
*/
public void onMapViewChanged(MapViewChangedEvent event) {
refreshPixelLength();
// Note: if this.mapWidget is null, pixelLength must be valid
if ( !Double.isNaN(pixelLength) && (0.0 != pixelLength)) {
if (!isScaleItemInitialized) {
scaleItem.clearValue();
updateResolutions();
setDisplayScale(mapView.getCurrentScale() * pixelLength);
} else if (scaleItem.getValueAsString() == null || "".equals(scaleItem.getValueAsString())) {
setDisplayScale(mapView.getCurrentScale() * pixelLength);
} else if (event.isMapResized()) {
updateScaleList();
setDisplayScale(mapView.getCurrentScale() * pixelLength);
} else if (!event.isSameScaleLevel() || scaleItem.getDisplayValue() == null
|| "".equals(scaleItem.getDisplayValue())) {
setDisplayScale(mapView.getCurrentScale() * pixelLength);
}
}
}
/**
* Make sure that the scale in the scale select is applied on the map, when the user presses the 'Enter' key.
*
* @see com.smartgwt.client.widgets.form.fields.events.KeyPressHandler#onKeyPress
* (com.smartgwt.client.widgets.form.fields.events.KeyPressEvent)
*/
public void onKeyPress(KeyPressEvent event) {
String name = event.getKeyName();
if (KeyNames.ENTER.equals(name)) {
reorderValues();
}
}
/**
* When the user selects a different scale, have the map zoom to it.
*
* @see com.smartgwt.client.widgets.form.fields.events.ChangedHandler#onChanged
* (com.smartgwt.client.widgets.form.fields.events.ChangedEvent)
*/
public void onChanged(ChangedEvent event) {
String value = (String) scaleItem.getValue();
Double scale = valueToScale.get(value);
if (scale != null && !Double.isNaN(pixelLength) && 0.0 != pixelLength) {
mapView.setCurrentScale(scale / pixelLength, MapView.ZoomOption.LEVEL_CLOSEST);
}
}
// -------------------------------------------------------------------------
// Private methods:
// -------------------------------------------------------------------------
protected void setDisplayScale(double scale) {
scaleItem.setValue(ScaleConverter.scaleToString(scale, precision, significantDigits));
}
/**
* Given the full list of desirable resolutions, which ones are actually available? Update the widget accordingly.
*/
private void updateScaleList() {
refreshPixelLength(); /* retrieve pixelLength from mapWidget. Needed in case mapWidget wasn't fully initialized
the previous time the pixelLength was requested */
// Update lookup map (stores user friendly representation):
valueToScale.clear();
if (!Double.isNaN(pixelLength) && (0.0 != pixelLength)) {
for (Double scale : scaleList) {
// Eliminate duplicates and null:
if (scale != null) {
String value = ScaleConverter.scaleToString(scale, precision, significantDigits);
valueToScale.put(value, scale);
}
}
List<String> availableScales = new ArrayList<String>();
for (Double scale : scaleList) {
if (mapView.isResolutionAvailable(pixelLength / scale)) {
availableScales.add(ScaleConverter.scaleToString(scale, precision, significantDigits));
}
}
scaleItem.setValueMap(availableScales.toArray(new String[availableScales.size()]));
isScaleItemInitialized = true;
}
}
private void init() {
setWidth100();
setHeight100();
scaleItem = new ComboBoxItem();
scaleItem.setTitle(I18nProvider.getToolbar().scaleSelectTitle());
scaleItem.setValidators(new ScaleValidator());
scaleItem.setValidateOnChange(true);
scaleItem.addKeyPressHandler(this);
scaleItem.addChangedHandler(this);
setFields(scaleItem);
mapView.addMapViewChangedHandler(this);
}
private void updateResolutions() {
if (mapView.getResolutions() != null && !Double.isNaN(pixelLength) && 0.0 != pixelLength) {
List<Double> scales = new ArrayList<Double>();
for (Double resolution : mapView.getResolutions()) {
scales.add(pixelLength / resolution);
}
setScales(scales.toArray(new Double[scales.size()]));
// this will call updateScaleList() which will set isScaleItemInitialized
// to true if scaleItem can be fully initialized
if (null == scaleItem.getValue() || scaleItem.getValueAsString() == null
|| "".equals(scaleItem.getValueAsString())
) {
setDisplayScale(mapView.getCurrentScale() * pixelLength);
} else if (scaleItem.getDisplayValue() == null || "".equals(scaleItem.getDisplayValue())) {
setDisplayScale(mapView.getCurrentScale() * pixelLength);
}
}
}
private void reorderValues() {
String value = (String) scaleItem.getValue();
if (value != null) {
Double scale = valueToScale.get(value);
if (scale == null) {
scale = ScaleConverter.stringToScale(value);
if (updatingScaleList) {
List<Double> newScales = new ArrayList<Double>(valueToScale.values());
newScales.add(scale);
setScales(newScales.toArray(new Double[newScales.size()]));
}
}
scaleItem.setValue(ScaleConverter.scaleToString(scale, precision, significantDigits));
mapView.setCurrentScale(scale / pixelLength, MapView.ZoomOption.LEVEL_CLOSEST);
}
}
/**
* Custom validation of user entered scale
*/
private class ScaleValidator extends CustomValidator {
@Override
protected boolean condition(Object value) {
try {
return ScaleConverter.stringToScale((String) value) >= 0.0;
} catch (NumberFormatException t) {
return false;
}
}
}
private void refreshPixelLength() {
if (null != this.mapWidget) {
pixelLength = mapWidget.getPixelPerUnit();
if (Double.isNaN(pixelLength) || (0.0 == pixelLength)) {
isScaleItemInitialized = false;
}
}
}
}