/*
* This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
*
* Copyright 2008-2011 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.map;
import java.util.ArrayList;
import java.util.List;
import org.geomajas.configuration.client.ClientLayerInfo;
import org.geomajas.configuration.client.ClientMapInfo;
import org.geomajas.configuration.client.ClientRasterLayerInfo;
import org.geomajas.configuration.client.ClientVectorLayerInfo;
import org.geomajas.configuration.client.ScaleConfigurationInfo;
import org.geomajas.configuration.client.ScaleInfo;
import org.geomajas.annotation.Api;
import org.geomajas.gwt.client.gfx.Paintable;
import org.geomajas.gwt.client.gfx.PainterVisitor;
import org.geomajas.gwt.client.map.event.FeatureDeselectedEvent;
import org.geomajas.gwt.client.map.event.FeatureSelectedEvent;
import org.geomajas.gwt.client.map.event.FeatureSelectionHandler;
import org.geomajas.gwt.client.map.event.FeatureTransactionEvent;
import org.geomajas.gwt.client.map.event.FeatureTransactionHandler;
import org.geomajas.gwt.client.map.event.HasFeatureSelectionHandlers;
import org.geomajas.gwt.client.map.event.LayerDeselectedEvent;
import org.geomajas.gwt.client.map.event.LayerSelectedEvent;
import org.geomajas.gwt.client.map.event.LayerSelectionHandler;
import org.geomajas.gwt.client.map.event.MapModelEvent;
import org.geomajas.gwt.client.map.event.MapModelHandler;
import org.geomajas.gwt.client.map.event.MapViewChangedEvent;
import org.geomajas.gwt.client.map.event.MapViewChangedHandler;
import org.geomajas.gwt.client.map.feature.Feature;
import org.geomajas.gwt.client.map.feature.FeatureEditor;
import org.geomajas.gwt.client.map.feature.FeatureTransaction;
import org.geomajas.gwt.client.map.layer.Layer;
import org.geomajas.gwt.client.map.layer.RasterLayer;
import org.geomajas.gwt.client.map.layer.VectorLayer;
import org.geomajas.gwt.client.spatial.Bbox;
import org.geomajas.gwt.client.spatial.geometry.GeometryFactory;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
/**
* <p>
* The model behind a map. This object contains all the layers related to the map. When re-rendering the entire map, it
* is actually this model that is rendered. Therefore the MapModel implements the <code>Paintable</code> interface.
* </p>
*
* @author Pieter De Graef
* @since 1.6.0
*/
@Api
public class MapModel implements Paintable, MapViewChangedHandler, HasFeatureSelectionHandlers {
/**
* The models ID. This is necessary mainly because of the <code>Paintable</code> interface. Still, every painted
* object needs a unique identifier.
*/
private String id;
/** The map's coordinate system as an EPSG code. (i.e. lonlat = 'epsg:4326' => srid = 4326) */
private int srid;
/**
* An ordered list of layers. The drawing order on the map is as follows: the first layer will be placed at the
* bottom, the last layer on top.
*/
private List<Layer<?>> layers = new ArrayList<Layer<?>>();
/** Reference to the <code>MapView</code> object of the <code>MapWidget</code>. */
private MapView mapView;
private ClientMapInfo mapInfo;
private FeatureEditor featureEditor;
private HandlerManager handlerManager;
private GeometryFactory geometryFactory;
private boolean initialized;
private LayerSelectionPropagator selectionPropagator = new LayerSelectionPropagator();
// -------------------------------------------------------------------------
// Constructors:
// -------------------------------------------------------------------------
/**
* Initialize map model, coordinate system has to be filled in later (from configuration).
*
* @param id
* map id
* @since 1.6.0
*/
@Api
public MapModel(String id) {
this.id = id;
featureEditor = new FeatureEditor(this);
handlerManager = new HandlerManager(this);
mapView = new MapView();
mapView.addMapViewChangedHandler(this);
}
// -------------------------------------------------------------------------
// MapModel event handling:
// -------------------------------------------------------------------------
/**
* Adds this handler to the model.
*
* @param handler
* the handler
* @return {@link com.google.gwt.event.shared.HandlerRegistration} used to remove the handler
* @since 1.6.0
*/
@Api
public final HandlerRegistration addMapModelHandler(final MapModelHandler handler) {
return handlerManager.addHandler(MapModelEvent.TYPE, handler);
}
/**
*
* @param handler
* The handler to be registered.
* @return
* @since 1.6.0
*/
@Api
public final HandlerRegistration addFeatureSelectionHandler(final FeatureSelectionHandler handler) {
return handlerManager.addHandler(FeatureSelectionHandler.TYPE, handler);
}
/**
*
* @param handler
* the handler to be registered
* @return handler registration
* @since 1.6.0
*/
@Api
public HandlerRegistration addLayerSelectionHandler(final LayerSelectionHandler handler) {
return handlerManager.addHandler(LayerSelectionHandler.TYPE, handler);
}
/**
*
* @param handler
* @since 1.6.0
*/
@Api
public void removeMapModelHandler(final MapModelHandler handler) {
handlerManager.removeHandler(MapModelEvent.TYPE, handler);
}
/**
* Add a new handler for {@link FeatureTransactionEvent}s.
*
* @param handler
* the handler to be registered
* @return
* @since 1.7.0
*/
@Api
public HandlerRegistration addFeatureTransactionHandler(final FeatureTransactionHandler handler) {
return handlerManager.addHandler(FeatureTransactionHandler.TYPE, handler);
}
// -------------------------------------------------------------------------
// Implementation of the Paintable interface:
// -------------------------------------------------------------------------
/**
* Paintable implementation. First let the PainterVisitor paint this object, then if recursive is true, painter the
* layers in order.
*/
public void accept(PainterVisitor visitor, Object group, Bbox bounds, boolean recursive) {
// Paint the MapModel itself (see MapModelPainter):
visitor.visit(this, group);
// Paint the layers:
if (recursive) {
for (Layer<?> layer : layers) {
if (layer.isShowing()) {
layer.accept(visitor, group, bounds, recursive);
} else {
// JDM: paint the top part of the layer, if not we loose the map order
layer.accept(visitor, group, bounds, false);
}
}
}
// Paint the editing of a feature (if a feature is being edited):
if (featureEditor.getFeatureTransaction() != null) {
featureEditor.getFeatureTransaction().accept(visitor, group, bounds, recursive);
}
}
/**
* Return this map model's id.
*
* @return id
*/
public String getId() {
return id;
}
// -------------------------------------------------------------------------
// Implementation of the MapViewChangedHandler interface:
// -------------------------------------------------------------------------
/**
* Update the visibility of the layers.
*
* @param event
* change event
*/
public void onMapViewChanged(MapViewChangedEvent event) {
for (Layer<?> layer : layers) {
layer.updateShowing();
// If the map is resized quickly after a previous resize, tile requests are sent out, but when they come
// back, the world-to-pan matrix will have altered, and so the tiles are placed at the wrong positions....
// so we clear the store.
if (layer instanceof RasterLayer && event.isMapResized()) {
((RasterLayer) layer).getStore().clear();
}
}
}
// -------------------------------------------------------------------------
// Public methods:
// -------------------------------------------------------------------------
/**
* Initialize the MapModel object, using a configuration object acquired from the server. This will automatically
* build the list of layers.
*
* @param mapInfo
* The configuration object.
*/
public void initialize(final ClientMapInfo mapInfo) {
if (!initialized) {
this.mapInfo = mapInfo;
srid = Integer.parseInt(mapInfo.getCrs().substring(mapInfo.getCrs().indexOf(":") + 1));
ScaleConfigurationInfo scaleConfigurationInfo = mapInfo.getScaleConfiguration();
List<Double> realResolutions = new ArrayList<Double>();
for (ScaleInfo scale : scaleConfigurationInfo.getZoomLevels()) {
realResolutions.add(1. / scale.getPixelPerUnit());
}
mapView.setResolutions(realResolutions);
mapView.setMaximumScale(scaleConfigurationInfo.getMaximumScale().getPixelPerUnit());
// replace layers by new layers
removeAllLayers();
for (ClientLayerInfo layerInfo : mapInfo.getLayers()) {
addLayer(layerInfo);
}
Bbox maxBounds = new Bbox(mapInfo.getMaxBounds());
Bbox initialBounds = new Bbox(mapInfo.getInitialBounds());
// if the max bounds was not configured, take the union of initial and layer bounds
if (maxBounds.isAll()) {
for (ClientLayerInfo layerInfo : mapInfo.getLayers()) {
maxBounds = (Bbox) initialBounds.clone();
maxBounds = maxBounds.union(new Bbox(layerInfo.getMaxExtent()));
}
}
mapView.setMaxBounds(maxBounds);
mapView.applyBounds(initialBounds, MapView.ZoomOption.LEVEL_CLOSEST);
}
initialized = true;
handlerManager.fireEvent(new MapModelEvent());
}
/**
* Is this map model initialized yet ?
*
* @return true if initialized
* @since 1.6.0
*/
@Api
public boolean isInitialized() {
return initialized;
}
/**
* Search a layer by it's id.
*
* @param layerId
* The layer's client ID.
* @return Returns either a Layer, or null.
* @since 1.6.0
*/
@Api
public Layer<?> getLayer(String layerId) {
if (layers != null) {
for (Layer<?> layer : layers) {
if (layer.getId().equals(layerId)) {
return layer;
}
}
}
return null;
}
/**
* Get all layers with the specified server layer id.
*
* @param serverLayerId
* The layer's server layer ID.
* @return Returns list of layers with the specified server layer id.
*/
public List<Layer<?>> getLayersByServerId(String serverLayerId) {
List<Layer<?>> l = new ArrayList<Layer<?>>();
if (layers != null) {
for (Layer<?> layer : layers) {
if (layer.getServerLayerId().equals(serverLayerId)) {
l.add(layer);
}
}
}
return l;
}
/**
* Get all vector layers with the specified server layer id.
*
* @param serverLayerId
* The layer's server layer ID.
* @return Returns list of layers with the specified server layer id.
*/
public List<VectorLayer> getVectorLayersByServerId(String serverLayerId) {
List<VectorLayer> l = new ArrayList<VectorLayer>();
if (layers != null) {
for (VectorLayer layer : getVectorLayers()) {
if (layer.getServerLayerId().equals(serverLayerId)) {
l.add(layer);
}
}
}
return l;
}
/**
* Search a vector layer by it's id.
*
* @param layerId
* The layer's client ID.
* @return Returns either a Layer, or null.
* @since 1.6.0
*/
@Api
public VectorLayer getVectorLayer(String layerId) {
if (layers != null) {
for (VectorLayer layer : getVectorLayers()) {
if (layer.getId().equals(layerId)) {
return layer;
}
}
}
return null;
}
/**
* Select a new layer. Only one layer can be selected at a time, so this function first tries to deselect the
* currently selected (if there is one).
*
* @param layer
* The layer to select. If layer is null, then the currently selected layer will be deselected!
*/
public void selectLayer(Layer<?> layer) {
if (layer == null) {
deselectLayer(this.getSelectedLayer());
} else {
Layer<?> selLayer = this.getSelectedLayer();
if (selLayer != null && !layer.getId().equals(selLayer.getId())) {
deselectLayer(selLayer);
}
layer.setSelected(true);
handlerManager.fireEvent(new LayerSelectedEvent(layer));
}
}
/** Return a list containing all vector layers within this model. */
public List<VectorLayer> getVectorLayers() {
ArrayList<VectorLayer> list = new ArrayList<VectorLayer>();
for (Layer<?> layer : layers) {
if (layer instanceof VectorLayer) {
list.add((VectorLayer) layer);
}
}
return list;
}
/** Clear the list of selected features in all vector layers. */
public void clearSelectedFeatures() {
for (VectorLayer layer : getVectorLayers()) {
layer.clearSelectedFeatures();
}
}
/** Return the total number of selected features in all vector layers. */
public int getNrSelectedFeatures() {
int count = 0;
for (VectorLayer layer : getVectorLayers()) {
count += layer.getSelectedFeatures().size();
}
return count;
}
/**
* Return the selected feature if there is 1 selected feature.
*
* @return the selected feature or null if none or multiple features are selected
*/
public String getSelectedFeature() {
if (getNrSelectedFeatures() == 1) {
for (VectorLayer layer : getVectorLayers()) {
if (layer.getSelectedFeatures().size() > 0) {
return layer.getSelectedFeatures().iterator().next();
}
}
}
return null;
}
/**
* Searches for the selected layer, and returns it.
*
* @return Returns the selected layer object, or null if none is selected.
*/
public Layer<?> getSelectedLayer() {
if (layers != null) {
for (Layer<?> layer : layers) {
if (layer.isSelected()) {
return layer;
}
}
}
return null;
}
/**
* Apply a certain feature transaction onto the client side map model. This method is usually called after that same
* feature transaction has been successfully applied on the server.
*
* @param ft
* The feature transaction to apply. It can create, update or delete features.
*/
public void applyFeatureTransaction(FeatureTransaction ft) {
if (ft != null) {
VectorLayer layer = ft.getLayer();
if (layer != null) {
// clear all the tiles
layer.getFeatureStore().clear();
}
// now update/add the features
if (ft.getNewFeatures() != null) {
for (Feature feature : ft.getNewFeatures()) {
ft.getLayer().getFeatureStore().addFeature(feature);
}
}
// make it fetch the tiles
mapView.translate(0, 0);
handlerManager.fireEvent(new FeatureTransactionEvent(ft));
}
}
/**
* Set a new position for the given layer. This will automatically redraw the map to apply this new order. Note that
* at any time, all raster layers will always lie behind all vector layers. This means that position 0 for a vector
* layer is the first(=back) vector layer to be drawn AFTER all raster layers have already been drawn.
*
* @param layer
* The vector layer to place at a new position.
* @param position
* The new layer order position in the layer array:
* <ul>
* <li>Back = 0 (but still in front of all raster layers)</li>
* <li>Front = (vector layer count - 1)</li>
* </ul>
* @return Returns if the re-ordering was successful or not.
* @since 1.8.0
*/
public boolean moveVectorLayer(VectorLayer layer, int position) {
if (layer == null) {
return false;
}
// Find attached ClientLayerInfo object:
ClientLayerInfo layerInfo = null;
String layerId = layer.getId();
for (ClientLayerInfo info : mapInfo.getLayers()) {
if (info.getId().equals(layerId)) {
layerInfo = info;
break;
}
}
if (layerInfo == null) {
return false;
}
// First remove the layer from the list:
if (!layers.remove(layer)) {
return false;
}
if (!mapInfo.getLayers().remove(layerInfo)) {
return false;
}
int rasterCount = rasterLayerCount();
position += rasterCount;
if (position < rasterCount) {
position = rasterCount;
} else if (position > layers.size()) {
position = layers.size();
}
try {
layers.add(position, layer);
mapInfo.getLayers().add(position, layerInfo);
} catch (Exception e) {
return false;
}
handlerManager.fireEvent(new MapModelEvent());
return true;
}
/**
* Set a new position for the given layer. This will automatically redraw the map to apply this new order. Note that
* at any time, all raster layers will always lie behind all vector layers. This means that position 0 for a vector
* layer is the first(=back) vector layer to be drawn AFTER all raster layers have already been drawn.
*
* @param layer
* The raster layer to place at a new position.
* @param position
* The new layer order position in the layer array:
* <ul>
* <li>Back = 0</li>
* <li>Front = (raster layer count - 1); Larger numbers won't make a difference. Rasters stay behind
* vectors...</li>
* </ul>
* @return Returns if the re-ordering was successful or not.
* @since 1.8.0
*/
public boolean moveRasterLayer(RasterLayer layer, int position) {
if (layer == null) {
return false;
}
// Find attached ClientLayerInfo object:
ClientLayerInfo layerInfo = null;
String layerId = layer.getId();
for (ClientLayerInfo info : mapInfo.getLayers()) {
if (info.getId().equals(layerId)) {
layerInfo = info;
break;
}
}
if (layerInfo == null) {
return false;
}
int rasterCount = rasterLayerCount();
// First remove the layer from the list:
if (!layers.remove(layer)) {
return false;
}
if (!mapInfo.getLayers().remove(layerInfo)) {
return false;
}
if (position < 0) {
position = 0;
} else if (position > rasterCount - 1) {
position = rasterCount - 1;
}
try {
layers.add(position, layer);
mapInfo.getLayers().add(position, layerInfo);
} catch (Exception e) {
return false;
}
handlerManager.fireEvent(new MapModelEvent());
return true;
}
/**
* Move a vector layer up (=front) one place. Note that at any time, all raster layers will always lie behind all
* vector layers. This means that position 0 for a vector layer is the first(=back) vector layer to be drawn AFTER
* all raster layers have already been drawn.
*
* @param layer
* The vector layer to move more to the front.
* @return Returns if the re-ordering was successful or not.
* @since 1.8.0
*/
public boolean moveVectorLayerUp(VectorLayer layer) {
int position = getLayerPosition(layer);
if (position < 0) {
return false;
}
return moveVectorLayer(layer, position + 1);
}
/**
* Move a vector layer down (=back) one place. Note that at any time, all raster layers will always lie behind all
* vector layers. This means that position 0 for a vector layer is the first(=back) vector layer to be drawn AFTER
* all raster layers have already been drawn.
*
* @param layer
* The vector layer to move more to the back.
* @return Returns if the re-ordering was successful or not.
* @since 1.8.0
*/
public boolean moveVectorLayerDown(VectorLayer layer) {
int position = getLayerPosition(layer);
if (position < 0) {
return false;
}
return moveVectorLayer(layer, position - 1);
}
/**
* Move a raster layer up (=front) one place. Note that at any time, all raster layers will always lie behind all
* vector layers. This means that position 0 for a vector layer is the first(=back) vector layer to be drawn AFTER
* all raster layers have already been drawn.
*
* @param layer
* The raster layer to move more to the front.
* @return Returns if the re-ordering was successful or not.
* @since 1.8.0
*/
public boolean moveRasterLayerUp(RasterLayer layer) {
int position = getLayerPosition(layer);
if (position < 0) {
return false;
}
return moveRasterLayer(layer, position + 1);
}
/**
* Move a raster layer down (=back) one place. Note that at any time, all raster layers will always lie behind all
* vector layers. This means that position 0 for a vector layer is the first(=back) vector layer to be drawn AFTER
* all raster layers have already been drawn.
*
* @param layer
* The raster layer to move more to the back.
* @return Returns if the re-ordering was successful or not.
* @since 1.8.0
*/
public boolean moveRasterLayerDown(RasterLayer layer) {
int position = getLayerPosition(layer);
if (position < 0) {
return false;
}
return moveRasterLayer(layer, position - 1);
}
/**
* Get the position of a certain layer in this map model. Note that for both raster layers and vector layer, the
* count starts at 0! On the map, all raster layers always lie behind all vector layers.
*
* @param layer
* The layer to return the position for.
* @return Returns the position of the layer in the map. This position determines layer order.
* @since 1.8.0
*/
public int getLayerPosition(Layer<?> layer) {
if (layer == null) {
return -1;
}
String layerId = layer.getId();
if (layer instanceof RasterLayer) {
for (int index = 0; index < mapInfo.getLayers().size(); index++) {
if (mapInfo.getLayers().get(index).getId().equals(layerId)) {
return index;
}
}
} else if (layer instanceof VectorLayer) {
int rasterCount = 0;
for (int index = 0; index < mapInfo.getLayers().size(); index++) {
if (layers.get(index) instanceof RasterLayer) {
rasterCount++;
}
if (mapInfo.getLayers().get(index).getId().equals(layerId)) {
return index - rasterCount;
}
}
}
return 0;
}
// -------------------------------------------------------------------------
// Getters:
// -------------------------------------------------------------------------
public List<Layer<?>> getLayers() {
return layers;
}
public MapView getMapView() {
return mapView;
}
public FeatureEditor getFeatureEditor() {
return featureEditor;
}
public int getSrid() {
return srid;
}
public String getCrs() {
return "EPSG:" + srid;
}
public int getPrecision() {
if (mapInfo != null) {
return mapInfo.getPrecision();
}
return -1;
}
public ClientMapInfo getMapInfo() {
return mapInfo;
}
/**
* Return a factory for geometries that is suited perfectly for geometries within this model. The SRID and precision
* will for the factory will be correct.
*/
public GeometryFactory getGeometryFactory() {
if (null == geometryFactory) {
if (0 == srid) {
throw new IllegalArgumentException("srid needs to be set on MapModel to obtain GeometryFactory");
}
geometryFactory = new GeometryFactory(srid, -1); // @todo precision is not yet implemented
}
return geometryFactory;
}
// -------------------------------------------------------------------------
// Private methods:
// -------------------------------------------------------------------------
private void removeAllLayers() {
layers = new ArrayList<Layer<?>>();
}
private void addLayer(ClientLayerInfo layerInfo) {
switch (layerInfo.getLayerType()) {
case RASTER:
RasterLayer rasterLayer = new RasterLayer(this, (ClientRasterLayerInfo) layerInfo);
layers.add(rasterLayer);
break;
default:
VectorLayer vectorLayer = new VectorLayer(this, (ClientVectorLayerInfo) layerInfo);
layers.add(vectorLayer);
vectorLayer.addFeatureSelectionHandler(selectionPropagator);
break;
}
}
/**
* Deselect the currently selected layer, includes sending the deselect events.
*
* @param layer
* layer to clear
*/
private void deselectLayer(Layer<?> layer) {
if (layer != null) {
layer.setSelected(false);
handlerManager.fireEvent(new LayerDeselectedEvent(layer));
}
}
/** Count the total number of raster layers in this model. */
private int rasterLayerCount() {
int rasterLayerCount = 0;
for (int index = 0; index < mapInfo.getLayers().size(); index++) {
if (layers.get(index) instanceof RasterLayer) {
rasterLayerCount++;
}
}
return rasterLayerCount;
}
// -------------------------------------------------------------------------
// Private classes:
// -------------------------------------------------------------------------
/**
* Propagates layer selection events to interested listeners.
*
* @author Jan De Moerloose
*
*/
private class LayerSelectionPropagator implements FeatureSelectionHandler {
public void onFeatureDeselected(FeatureDeselectedEvent event) {
handlerManager.fireEvent(event);
}
public void onFeatureSelected(FeatureSelectedEvent event) {
handlerManager.fireEvent(event);
}
}
}