/* * 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.map; import com.google.gwt.event.shared.HandlerManager; import com.google.gwt.event.shared.HandlerRegistration; import org.geomajas.annotation.Api; import org.geomajas.configuration.client.BoundsLimitOption; 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.global.GeomajasConstant; import org.geomajas.gwt.client.command.GwtCommandDispatcher; import org.geomajas.gwt.client.command.event.TokenChangedEvent; import org.geomajas.gwt.client.command.event.TokenChangedHandler; import org.geomajas.gwt.client.gfx.Paintable; import org.geomajas.gwt.client.gfx.PainterVisitor; import org.geomajas.gwt.client.map.MapView.ZoomOption; 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.MapModelChangedEvent; import org.geomajas.gwt.client.map.event.MapModelChangedHandler; import org.geomajas.gwt.client.map.event.MapModelClearEvent; import org.geomajas.gwt.client.map.event.MapModelClearHandler; 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.feature.LazyLoadCallback; import org.geomajas.gwt.client.map.layer.InternalClientWmsLayer; 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.map.layer.configuration.ClientWmsLayerInfo; import org.geomajas.gwt.client.service.ClientConfigurationService; import org.geomajas.gwt.client.service.WidgetConfigurationCallback; import org.geomajas.gwt.client.spatial.Bbox; import org.geomajas.gwt.client.spatial.geometry.GeometryFactory; import org.geomajas.gwt.client.util.Log; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * <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 * @author Joachim Van der Auwera * @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; private String applicationId; /** 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 mapModelEventFired; // assures MapModelEvent is only fired once private LayerSelectionPropagator selectionPropagator = new LayerSelectionPropagator(); private List<Runnable> whenInitializedRunnables = new ArrayList<Runnable>(); private State state = State.IDLE; /** * Internal configuration state of the map. * * @author Jan De Moerloose * */ enum State { IDLE, // initial state INITIALIZING, // waiting for configuration callback (1st time) INITIALIZED, // configuration applied REFRESHING // waiting for configuration callback (> 1st time) } // ------------------------------------------------------------------------- // Constructors: // ------------------------------------------------------------------------- /** * Initialize map model, coordinate system has to be filled in later (from configuration). * * @param mapId map id * @since 1.6.0 * @deprecated use {@link #MapModel(String, String)}, this assume "app" as applicationId */ @Api @Deprecated public MapModel(String mapId) { this(mapId, "app"); Log.logWarn("Using deprecated MapModel constructor, assuming application id is 'app'"); } /** * Initialize map model, coordinate system has to be filled in later (from configuration). * * @param mapId map id * @param applicationId application id * @since 1.10.0 */ @Api public MapModel(String mapId, String applicationId) { this.id = mapId; this.applicationId = applicationId; featureEditor = new FeatureEditor(this); handlerManager = new HandlerManager(this); mapView = new MapView(); mapView.addMapViewChangedHandler(this); // refresh the map when the token changes GwtCommandDispatcher.getInstance().addTokenChangedHandler(new TokenChangedHandler() { public void onTokenChanged(TokenChangedEvent event) { if (event.isLoginPending()) { // avoid double refresh on re-login clear(); ClientConfigurationService.clear(); // refresh because configuration changed, clear cache } else { refresh(); // clearing is done in the refresh } } }); } // constructor for testing public MapModel(ClientMapInfo info) { this.id = info.getId(); this.applicationId = "bla"; featureEditor = new FeatureEditor(this); handlerManager = new HandlerManager(this); mapView = new MapView(); mapView.addMapViewChangedHandler(this); refresh(info); } /** * Run some code once when the map is initialized. * * @param runnable code to run * @since 1.10.0 */ @Api public void runWhenInitialized(Runnable runnable) { if (isInitialized()) { runnable.run(); } else { whenInitializedRunnables.add(runnable); } } // ------------------------------------------------------------------------- // 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); } /** * Remove map model handler. * * @param handler handler to be removed * @since 1.6.0 */ @Api public void removeMapModelHandler(final MapModelHandler handler) { handlerManager.removeHandler(MapModelEvent.TYPE, handler); } /** * Add a handler which listens to all changes in the map model. * * @param handler handler * @return {@link com.google.gwt.event.shared.HandlerRegistration} used to remove the handler * @since 1.10.0 */ @Api public final HandlerRegistration addMapModelChangedHandler(final MapModelChangedHandler handler) { return handlerManager.addHandler(MapModelChangedHandler.TYPE, handler); } /** * Remove map model changed handler. * * @param handler handler to be removed * @since 1.10.0 */ @Api public void removeMapModelChangedHandler(final MapModelChangedHandler handler) { handlerManager.removeHandler(MapModelChangedHandler.TYPE, handler); } /** * Add a handler which listens to clearing the map model. * * @param handler handler * @return {@link com.google.gwt.event.shared.HandlerRegistration} used to remove the handler * @since 1.10.0 */ @Api public final HandlerRegistration addMapModelClearHandler(final MapModelClearHandler handler) { return handlerManager.addHandler(MapModelClearHandler.TYPE, handler); } /** * Remove map model clear handler. * * @param handler handler to be removed * @since 1.10.0 */ @Api public void removeMapModelClearHandler(final MapModelClearHandler handler) { handlerManager.removeHandler(MapModelClearHandler.TYPE, handler); } /** * Add feature selection handler. * * @param handler * The handler to be registered. * @return handler registration * @since 1.6.0 */ @Api public final HandlerRegistration addFeatureSelectionHandler(final FeatureSelectionHandler handler) { return handlerManager.addHandler(FeatureSelectionHandler.TYPE, handler); } /** * Add layer selection 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); } /** * Add a new handler for {@link FeatureTransactionEvent}s. * * @param handler * the handler to be registered * @return handler registration * @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: // ------------------------------------------------------------------------- /** * Refresh the map model. This will re-read the configuration and update the map model, toolbar etc. * <p/> * This should be called if you want the map to redraw itself. it is automatically called when the token changes. * * @since 1.10.0 */ @Api public void refresh() { if (state == State.INITIALIZED) { // to prevent refresh before the map is drawn state = State.REFRESHING; clear(); ClientConfigurationService.clear(); // refresh because configuration changed, clear cache refreshFromConfiguration(); } } /** * Initialize the map model. This will read the configuration. * <p/> * Make sure the handler are registered before initializing the map model or you may miss events. * <p/> * Only works the first time, use {@link #refresh()} later on. * * @since 1.10.0 */ @Api public void init() { if (state == State.IDLE) { state = State.INITIALIZING; refreshFromConfiguration(); } } /** * Clear the map model. Removes all layers and tools. * * @since 1.10.0 */ @Api public void clear() { handlerManager.fireEvent(new MapModelClearEvent(this)); layers.clear(); } private void refreshFromConfiguration() { ClientConfigurationService.getApplicationWidgetInfo(applicationId, id, new WidgetConfigurationCallback<ClientMapInfo>() { public void execute(ClientMapInfo mapInfo) { if (null == mapInfo) { Log.logError("Cannot find map with id " + id); } else { refresh(mapInfo); } } }); } /** * Refresh the MapModel object, using a configuration object acquired from the server. This will automatically * build the list of layers. * * @param mapInfo The configuration object. */ private void refresh(final ClientMapInfo mapInfo) { actualRefresh(mapInfo); if (state == State.INITIALIZING) { // only change the initial bounds the first time around Bbox initialBounds = new Bbox(mapInfo.getInitialBounds()); mapView.applyBounds(initialBounds, MapView.ZoomOption.LEVEL_CLOSEST); } state = State.INITIALIZED; fireRefreshEvents(); while (whenInitializedRunnables.size() > 0) { Runnable runnable = whenInitializedRunnables.remove(0); runnable.run(); } } private void fireRefreshEvents() { // fire first for backwards compatibility (make sure old event listeners have been called) handlerManager.fireEvent(new MapModelChangedEvent(this)); if (!mapModelEventFired) { handlerManager.fireEvent(new MapModelEvent()); } mapModelEventFired = true; } /** * Refresh the MapModel object, using a configuration object acquired from the server. This will automatically * build the list of layers. * * @param mapInfo * The configuration object. */ private void actualRefresh(final ClientMapInfo mapInfo) { if (null == mapInfo) { Log.logError("Cannot find map with id " + id); return; } this.mapInfo = mapInfo; srid = 0; try { int pos = mapInfo.getCrs().indexOf(":"); if (pos >= 0) { srid = Integer.parseInt(mapInfo.getCrs().substring(pos + 1)); } } catch (NumberFormatException nfe) { // warning logged below } if (0 == srid) { Log.logWarn("Cannot parse CRS for map " + id + ", CRS=" + mapInfo.getCrs()); } 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()) { addLayerWithoutFireEvent(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); if (null == mapInfo.getViewBoundsLimitOption()) { mapView.setViewBoundsLimitOption(BoundsLimitOption.COMPLETELY_WITHIN_MAX_BOUNDS); } else { mapView.setViewBoundsLimitOption(mapInfo.getViewBoundsLimitOption()); } } /** * Is this map model initialized yet ? * * @return true if initialized * @since 1.6.0 */ @Api public boolean isInitialized() { return state == State.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. * * @return vector layers */ 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. * * @return number of selected features */ 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; } /** * Pan to the center of the bounds of the specified features. * * @param features list of features, will be lazy-loaded if necessary * @since 1.11.0 */ @Api public void panToFeatures(List<Feature> features) { PanToFeaturesLazyLoadCallback callback = new PanToFeaturesLazyLoadCallback(features.size()); for (Feature feature : features) { // no need to fetch if we already have the geometry ! if (feature.isGeometryLoaded()) { callback.execute(Arrays.asList(feature)); } else { feature.getLayer().getFeatureStore() .getFeature(feature.getId(), GeomajasConstant.FEATURE_INCLUDE_GEOMETRY, callback); } } } /** * Zoom to the bounds of the specified features. * * @param features list of features, will be lazy-loaded if necessary * @since 1.11.0 */ @Api public void zoomToFeatures(List<Feature> features) { // calculate the point scale as the minimum point scale for all layers (only relevant for zooming to multiple // points at the exact same location) double zoomToPointScale = getMapInfo().getScaleConfiguration().getMaximumScale().getPixelPerUnit(); for (Feature feature : features) { double scale = feature.getLayer().getLayerInfo().getZoomToPointScale().getPixelPerUnit(); zoomToPointScale = Math.min(zoomToPointScale, scale); } ZoomToFeaturesLazyLoadCallback callback = new ZoomToFeaturesLazyLoadCallback(features.size(), zoomToPointScale); for (Feature feature : features) { // no need to fetch if we already have the geometry ! if (feature.isGeometryLoaded()) { callback.execute(Arrays.asList(feature)); } else { feature.getLayer().getFeatureStore() .getFeature(feature.getId(), GeomajasConstant.FEATURE_INCLUDE_GEOMETRY, callback); } } } /** * 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 MapModelChangedEvent(this)); 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 MapModelChangedEvent(this)); 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); return position >= 0 && 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); return position >= 0 && 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); return position >= 0 && 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); return position >= 0 && 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. * * @return geometry factory */ 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<?>>(); } /** * Add a layer to the map and fire an event for update. * * @param layerInfo the client layer info */ public void addLayer(ClientLayerInfo layerInfo) { addLayerWithoutFireEvent(layerInfo); mapInfo.getLayers().add(layerInfo); handlerManager.fireEvent(new MapModelChangedEvent(this)); } /** * Add a layer to the map, but do not fire an event for update. * * @param layerInfo the client layer info */ private void addLayerWithoutFireEvent(ClientLayerInfo layerInfo) { switch (layerInfo.getLayerType()) { case RASTER: if (layerInfo instanceof ClientWmsLayerInfo) { InternalClientWmsLayer internalClientWmsLayer = new InternalClientWmsLayer(this, (ClientWmsLayerInfo) layerInfo); layers.add(internalClientWmsLayer); } else { 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; } } /** * Remove a layer from the map. * * @param layer the layer to remove */ public void removeLayer(Layer layer) { if (layers.contains(layer)) { layers.remove(layer); mapInfo.getLayers().remove(layer.getLayerInfo()); handlerManager.fireEvent(new MapModelChangedEvent(this)); } } /** * 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. * * @return number of raster layers */ 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); } } /** * Stateful callback that zooms to bounds when all features have been retrieved. * * @author Kristof Heirwegh * @author Jan De Moerloose */ private class ZoomToFeaturesLazyLoadCallback implements LazyLoadCallback { private int featureCount; private Bbox bounds; private double pointScale; public ZoomToFeaturesLazyLoadCallback(int featureCount, double pointScale) { this.featureCount = featureCount; this.pointScale = pointScale; } public void execute(List<Feature> response) { if (response != null && response.size() > 0) { if (bounds == null) { bounds = (Bbox) response.get(0).getGeometry().getBounds().clone(); } else { bounds = bounds.union(response.get(0).getGeometry().getBounds()); } } featureCount--; if (featureCount == 0) { if (bounds != null) { if (bounds.getWidth() > 0 || bounds.getHeight() > 0) { getMapView().applyBounds(bounds, ZoomOption.LEVEL_FIT); } else { getMapView().setCenterPosition(bounds.getCenterPoint()); getMapView().setCurrentScale(pointScale, ZoomOption.LEVEL_CLOSEST); } } } } } /** * Stateful callback that pans to the center of the bounds of all features when they have been retrieved. * * @author Jan De Moerloose */ private class PanToFeaturesLazyLoadCallback implements LazyLoadCallback { private int featureCount; private Bbox bounds; public PanToFeaturesLazyLoadCallback(int featureCount) { this.featureCount = featureCount; } public void execute(List<Feature> response) { if (response != null && response.size() > 0) { if (bounds == null) { bounds = (Bbox) response.get(0).getGeometry().getBounds().clone(); } else { bounds = bounds.union(response.get(0).getGeometry().getBounds()); } } featureCount--; if (featureCount == 0) { if (bounds != null) { getMapView().setCenterPosition(bounds.getCenterPoint()); } } } } }