/*
* 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.spatial.snapping;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.geomajas.command.CommandResponse;
import org.geomajas.command.dto.SearchByLocationRequest;
import org.geomajas.command.dto.SearchByLocationResponse;
import org.geomajas.configuration.SnappingRuleInfo;
import org.geomajas.configuration.SnappingRuleInfo.SnappingType;
import org.geomajas.geometry.Coordinate;
import org.geomajas.global.GeomajasConstant;
import org.geomajas.gwt.client.command.CommandCallback;
import org.geomajas.gwt.client.command.GwtCommand;
import org.geomajas.gwt.client.command.GwtCommandDispatcher;
import org.geomajas.gwt.client.map.MapModel;
import org.geomajas.gwt.client.map.feature.Feature;
import org.geomajas.gwt.client.map.layer.VectorLayer;
import org.geomajas.gwt.client.spatial.Bbox;
import org.geomajas.gwt.client.spatial.geometry.Polygon;
import org.geomajas.gwt.client.util.GeometryConverter;
import org.geomajas.layer.LayerType;
/**
* <p>
* This is the main handler for snapping to coordinates. It supports different
* modes of operating and different algorithms for the actual snapping. The
* different algorithms to use are defined in the vector layer configurations
* objects, while the modes are defined by the {@link SnappingMode} class.
* </p>
* <p>
* All you have to do to make use of snapping, is to make an instance of this
* class, and call the <code>snap</code> method.
* </p>
*
* @author Pieter De Graef
* @author Kristof Heirwegh
*/
public class Snapper {
/**
* General definition for the different snapping modes. The
* ALL_GEOMETRIES_EQUAL will use a SnappingMode that treats all geometries
* equally (@see EqualSnappingMode), while the
* PRIORITY_TO_INTERSECTING_GEOMETRIES will give priority to geometries that
* intersect the given coordinate (@see IntersectPriorityMode).
*/
public static enum SnapMode {
ALL_GEOMETRIES_EQUAL, PRIORITY_TO_INTERSECTING_GEOMETRIES
};
/**
* The MapModel that contains the layers to which snapping is possible. If a
* snapping rule should exist that points to a layer that does not exist
* within this MapModel, then no snapping will occur.
*/
private MapModel mapModel;
/**
* Features are cached so they do not need to be retrieved from the server
* every time the mouse is moved.
*/
private Map<VectorLayer, List<Feature>> featureCache;
/**
* The featureCache needs to be invalidated if the bounds change between
* requests.
*/
private Bbox currentBounds;
/**
* The full list of snapping rules to be used.
*/
private List<SnappingRuleInfo> rules;
private String[] serverLayerIds;
private Map<String, String> layerFilters;
/**
* The current snapping mode to use.
*/
private SnapMode mode;
// -------------------------------------------------------------------------
// Constructors:
// -------------------------------------------------------------------------
/**
* Create a snapper with a certain set of rules and the given MapModel. As
* mode, the SnapMode.ALL_GEOMETRIES_EQUAL is used by default.
*/
public Snapper(MapModel mapModel, List<SnappingRuleInfo> rules) {
this(mapModel, rules, SnapMode.ALL_GEOMETRIES_EQUAL);
}
/**
* Create a snapper with a certain set of rules and the given MapModel,
* thereby immediately setting the snapping mode to use.
*/
public Snapper(MapModel mapModel, List<SnappingRuleInfo> rules, SnapMode mode) {
this.featureCache = new HashMap<VectorLayer, List<Feature>>();
this.mapModel = mapModel;
this.rules = rules;
setMode(mode);
retrieveFeatures();
}
// -------------------------------------------------------------------------
// Public methods:
// -------------------------------------------------------------------------
/**
* Execute the actual snapping!
*
* @param coordinate
* The original coordinate that needs snapping.
* @return Returns the given coordinate, or a snapped coordinate if one was
* found.
*/
public Coordinate snap(Coordinate coordinate) {
if (rules == null || mapModel == null) {
return coordinate;
}
Coordinate snappedCoordinate = coordinate;
double snappedDistance = Double.MAX_VALUE;
for (int i = 0; i < rules.size(); i++) {
SnappingRuleInfo rule = rules.get(i);
// Check for supported snapping algorithms: TODO use factory
if (rule.getType() != SnappingType.CLOSEST_ENDPOINT && rule.getType() != SnappingType.NEAREST_POINT) {
throw new IllegalArgumentException("Unknown snapping rule type was found: " + rule.getType());
}
// Get the target snap layer:
VectorLayer snapLayer;
try {
snapLayer = mapModel.getVectorLayer(rule.getLayerId());
} catch (Exception e) {
throw new IllegalArgumentException("Target snapping layer (" + rule.getLayerId()
+ ") was not a vector layer.");
}
SnapMode tempMode = this.mode;
if (snapLayer.getLayerInfo().getLayerType() != LayerType.POLYGON
&& snapLayer.getLayerInfo().getLayerType() != LayerType.MULTIPOLYGON) {
// For mode=MODE_PRIORITY_TO_INTERSECTING_GEOMETRIES, an area > 0 is required.
tempMode = SnapMode.ALL_GEOMETRIES_EQUAL;
}
// TODO: don't create the handler every time...
SnappingMode handler;
if (tempMode == SnapMode.ALL_GEOMETRIES_EQUAL) {
handler = new EqualSnappingMode(rule);
} else {
handler = new IntersectPriorityMode(rule);
}
// Calculate snapping:
handler.setCoordinate(coordinate);
iterateFeatures(snapLayer, handler);
if (handler.getDistance() < snappedDistance) {
snappedCoordinate = handler.getSnappedCoordinate();
snappedDistance = handler.getDistance();
}
}
return snappedCoordinate;
}
// -------------------------------------------------------------------------
// Getters and setters:
// -------------------------------------------------------------------------
public SnapMode getMode() {
return mode;
}
public void setMode(SnapMode mode) {
this.mode = mode;
}
public List<SnappingRuleInfo> getRules() {
return rules;
}
public void setRules(List<SnappingRuleInfo> rules) {
this.rules = rules;
}
// ----------------------------------------------------------
// private methods
// ----------------------------------------------------------
private void iterateFeatures(VectorLayer layer, SnappingMode handler) {
if (currentBounds != null && currentBounds.equals(mapModel.getMapView().getBounds(), 0)) {
List<Feature> feats = featureCache.get(layer);
if (feats != null) {
handler.execute(feats);
}
} else {
retrieveFeatures();
}
}
private void retrieveFeatures() {
// setting current bounds before method returns so it isn't called
// multiple times while waiting for result
// (this is adequate for javascript, no real concurrency)
currentBounds = mapModel.getMapView().getBounds();
if (serverLayerIds == null) {
init();
}
Polygon polygon = mapModel.getGeometryFactory().createPolygon(currentBounds);
GwtCommand commandRequest = new GwtCommand(SearchByLocationRequest.COMMAND);
SearchByLocationRequest request = new SearchByLocationRequest();
request.setLayerIds(serverLayerIds);
addFilters(request);
request.setFeatureIncludes(GeomajasConstant.FEATURE_INCLUDE_GEOMETRY);
request.setLocation(GeometryConverter.toDto(polygon));
request.setCrs(mapModel.getCrs());
request.setQueryType(SearchByLocationRequest.QUERY_INTERSECTS);
request.setSearchType(SearchByLocationRequest.SEARCH_ALL_LAYERS);
commandRequest.setCommandRequest(request);
GwtCommandDispatcher.getInstance().execute(commandRequest, new CommandCallback() {
public void execute(CommandResponse commandResponse) {
if (commandResponse instanceof SearchByLocationResponse) {
SearchByLocationResponse response = (SearchByLocationResponse) commandResponse;
Map<String, List<org.geomajas.layer.feature.Feature>> featureMap = response.getFeatureMap();
featureCache.clear();
for (String serverLayerId : featureMap.keySet()) {
VectorLayer vl = findLayer(serverLayerId);
List<Feature> features = new ArrayList<Feature>();
featureCache.put(vl, features);
for (org.geomajas.layer.feature.Feature dtoFeat : featureMap.get(serverLayerId)) {
features.add(new Feature(dtoFeat, vl));
}
}
}
}
});
}
private VectorLayer findLayer(String serverLayerId) {
List<VectorLayer> res = mapModel.getVectorLayersByServerId(serverLayerId);
if (res.size() == 1) {
return res.get(0);
} else {
for (VectorLayer vl : res) {
for (SnappingRuleInfo sri : rules) {
if (sri.getLayerId().equals(vl.getId())) {
return vl; // there's no way to know if it's the correct one
}
}
}
return null; // shouldn't happen
}
}
private void addFilters(SearchByLocationRequest sblr) {
// -- a simple String[], like ids would have been easier...
for (Entry<String, String> entry : layerFilters.entrySet()) {
sblr.setFilter(entry.getKey(), entry.getValue());
}
}
private void init() {
List<String> layerIds = new ArrayList<String>();
layerFilters = new HashMap<String, String>();
for (SnappingRuleInfo sri : rules) {
VectorLayer vl = mapModel.getVectorLayer(sri.getLayerId());
layerIds.add(vl.getServerLayerId());
if (vl.getFilter() != null) {
layerFilters.put(vl.getServerLayerId(), vl.getFilter());
}
}
serverLayerIds = layerIds.toArray(new String[layerIds.size()]);
}
}