/* Spatial Operations & Editing Tools for uDig
*
* Axios Engineering under a funding contract with:
* Diputación Foral de Gipuzkoa, Ordenación Territorial
*
* http://b5m.gipuzkoa.net
* http://www.axios.es
*
* (C) 2006, Diputación Foral de Gipuzkoa, Ordenación Territorial (DFG-OT).
* DFG-OT agrees to license under Lesser General Public License (LGPL).
*
* You can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software
* Foundation; version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package es.axios.udig.ui.editingtools.internal.commons.behaviour;
import java.io.IOException;
import java.util.List;
import net.refractions.udig.mapgraphic.grid.GridMapGraphic;
import net.refractions.udig.mapgraphic.grid.GridStyle;
import net.refractions.udig.project.ILayer;
import net.refractions.udig.project.IMap;
import net.refractions.udig.project.render.IViewportModel;
import net.refractions.udig.project.ui.AnimationUpdater;
import net.refractions.udig.project.ui.tool.IToolContext;
import net.refractions.udig.tools.edit.EditPlugin;
import net.refractions.udig.tools.edit.EditState;
import net.refractions.udig.tools.edit.EditToolHandler;
import net.refractions.udig.tools.edit.animation.SearchBoxAnimation;
import net.refractions.udig.tools.edit.support.ClosestEdge;
import net.refractions.udig.tools.edit.support.EditBlackboard;
import net.refractions.udig.tools.edit.support.IsBusyStateProvider;
import net.refractions.udig.tools.edit.support.Point;
import net.refractions.udig.tools.edit.support.SnapBehaviour;
import net.refractions.udig.ui.ProgressManager;
import org.geotools.data.FeatureSource;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.OperationNotFoundException;
import org.opengis.referencing.operation.TransformException;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineSegment;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.Polygon;
import es.axios.geotools.util.GeoToolsUtils;
/**
* Utility class to find out the closest segment to a given (screen) point from
* the segments in the edit shape, the current layer, all the active layers,
* etc, as defined by a {@link SnapBehaviour}.
*
* @author Aritz Davila (www.axios.es)
* @author Mauricio Pazos (www.axios.es)
* @since 0.2.0
*/
public class SnapSegmentFinder {
/**
* Which CRS to perform the search and return the snapping segment on
*/
private CoordinateReferenceSystem calculationCrs;
private static final GeometryFactory gfac = new GeometryFactory();
/**
* @param calculationCrs
* which CRS to use to perform the snapping calculation. Needed
* as different CRS's may lead to different segments being the
* closest. This is also going to be the CRS the returned
* LineSegment is on.
*/
public SnapSegmentFinder(CoordinateReferenceSystem calculationCrs) {
assert calculationCrs != null;
this.calculationCrs = calculationCrs;
}
public CoordinateReferenceSystem getCalculationCrs() {
return calculationCrs;
}
/**
* @param handler
* the edit tool handler in use
* @param editBlackboard
* the EditBlackboard of the cureent edit layer
* @param centerPoint
* the snapping point in screen coordinates
* @param includeSegmentsInCurrent
* whether to include the current edit feature segments in the
* lookup
* @param snapBehaviour
* the snap behaviour to use
* @param snappingRadius
* snap search distance in pixels
* @return the closest line segment inside the snapping distance with
* coordinates in the calculation crs provided at the constructor
*/
public LineSegment getClosestSnapSegment( EditToolHandler handler,
final EditBlackboard editBlackboard,
final Point centerPoint,
final boolean includeSegmentsInCurrent,
final SnapBehaviour snapBehaviour,
final int snappingRadius) {
final IToolContext context = handler.getContext();
Coordinate centerCoordInTargetCrs = getCenterCoordInTargetCrs(centerPoint, context);
MinSegFinder minFinder = new MinSegFinder(centerCoordInTargetCrs);
SearchBoxAnimation anim = null;
final EditState previousState = handler.getCurrentState();
try {
handler.setCurrentState(EditState.BUSY);
if (snapBehaviour != SnapBehaviour.OFF && snapBehaviour != SnapBehaviour.GRID) {
anim = new SearchBoxAnimation(centerPoint, new IsBusyStateProvider(handler));
AnimationUpdater.runTimer(context.getMapDisplay(), anim);
}
switch (snapBehaviour) {
case OFF:
return null;
case SELECTED:
searchEditBlackboard(context, editBlackboard, centerPoint, includeSegmentsInCurrent, snappingRadius,
minFinder);
break;
case CURRENT_LAYER:
searchEditBlackboard(context, editBlackboard, centerPoint, includeSegmentsInCurrent, snappingRadius,
minFinder);
searchSegmentInLayer(handler.getEditLayer(), context, centerPoint, snappingRadius, minFinder);
break;
case ALL_LAYERS:
searchEditBlackboard(context, editBlackboard, centerPoint, includeSegmentsInCurrent, snappingRadius,
minFinder);
for (ILayer layer : context.getMapLayers()) {
searchSegmentInLayer(layer, context, centerPoint, snappingRadius, minFinder);
}
break;
case GRID:
findClosestGridSegment(centerPoint, context.getMap(), snappingRadius, minFinder);
default:
break;
}
LineSegment min = minFinder.getMinSegment();
return min;
} finally {
handler.setCurrentState(previousState);
if (anim != null) {
anim.setValid(false);
}
}
}
private Coordinate getCenterCoordInTargetCrs(Point centerPoint, final IToolContext context) {
Coordinate centerCoordInMapCrs = context.pixelToWorld(centerPoint.getX(), centerPoint.getY());
CoordinateReferenceSystem mapCrs = context.getCRS();
Coordinate centerCoordInTargetCrs;
try {
com.vividsolutions.jts.geom.Point pointInCalculationCrs;
com.vividsolutions.jts.geom.Point point = gfac.createPoint(centerCoordInMapCrs);
pointInCalculationCrs = (com.vividsolutions.jts.geom.Point) GeoToolsUtils.reproject(point, mapCrs,
calculationCrs);
centerCoordInTargetCrs = pointInCalculationCrs.getCoordinate();
} catch (OperationNotFoundException e) {
throw new RuntimeException(e);
} catch (TransformException e) {
throw new RuntimeException(e);
}
return centerCoordInTargetCrs;
}
/**
* Searches the editblackboard and adds the closest segment to the
* minFinder.
* <p>
* As a difference with the other search methods, this one makes the search
* in screen coordinates and then adds the result to minFinder in the
* calculation CRS
* </p>
*
* @param snappingRadius
*/
private void searchEditBlackboard( final IToolContext context,
final EditBlackboard editBlackboard,
final Point centerPoint,
final boolean includeVerticesInCurrent,
final int snappingRadius,
MinSegFinder minFinder) {
final boolean treatUnknownAsPolygon = true;
final Coordinate centerPointInMapCrs = context.pixelToWorld(centerPoint.getX(), centerPoint.getY());
MinSegFinder screenFinder = new MinSegFinder(centerPointInMapCrs);
List<ClosestEdge> candidates;
candidates = editBlackboard.getCandidates(centerPoint.getX(), centerPoint.getY(), treatUnknownAsPolygon);
Point closestPoint;
int indexOfPreviousPoint;
Point previousPoint;
Coordinate p0;
Coordinate p1;
for (ClosestEdge edge : candidates) {
if (snappingRadius >= edge.getDistanceToEdge()) {
closestPoint = edge.getPointOnLine();
indexOfPreviousPoint = edge.getIndexOfPrevious();
previousPoint = edge.getPart().getPoint(indexOfPreviousPoint);
p0 = context.pixelToWorld(closestPoint.getX(), closestPoint.getY());
p1 = context.pixelToWorld(previousPoint.getX(), previousPoint.getY());
screenFinder.add(p0, p1);
}
}
final LineSegment screenSegment = screenFinder.getMinSegment();
if (screenSegment != null) {
CoordinateReferenceSystem mapCrs = context.getCRS();
LineSegment segment = GeoToolsUtils.reproject(screenSegment, mapCrs, calculationCrs);
minFinder.add(segment.p0, segment.p1);
}
}
// /**
// * Roughly stolen from EditUtils as its private
// * <p>
// * TODO: consider asking for the EditUtils version to become public
// * </p>
// *
// * @param p
// * @param editBlackboard
// * @param currentShape
// * @return
// */
// private boolean containsNonCurrentShape( Point p, EditBlackboard
// editBlackboard,
// PrimitiveShape currentShape ) {
// if (p == null || currentShape == null)
// return false;
// List<EditGeom> geoms = editBlackboard.getGeoms(p.getX(), p.getY());
// if (geoms.isEmpty())
// return false;
// if (geoms.size() > 1 || geoms.get(0) != currentShape.getEditGeom())
// return true;
//
// return false;
// }
/**
* Searches the layer for line segments within snapping distance
*
* @param layer
* the layer to search that can resolve to {@link FeatureSource}
* @param context
* the context to use for convenience methods
* @param centerPoint
* the current centerPoint.
* @param snappingRadius
* snap search distance in pixels
* @param minFinder
* @return the closest vertex in the layer within the snapping radius or
* null if no segment is found on the snapping radious distance.
*/
private void searchSegmentInLayer( final ILayer layer,
final IToolContext context,
final Point centerPoint,
final int snappingRadius,
MinSegFinder minFinder) {
if (!layer.hasResource(FeatureSource.class) || !layer.isApplicable(EditPlugin.ID) || !layer.isVisible())
return;
final ILayer editLayer = context.getEditManager().getEditLayer();
SimpleFeatureType schema = layer.getSchema();
Class<?> geomType = schema.getGeometryDescriptor().getType().getBinding();
if (Point.class == geomType || MultiPoint.class == geomType) {
return;
}
SimpleFeature editFeature = context.getEditManager().getEditFeature();
String editFeatureID = null;
if (editFeature != null) {
editFeatureID = editFeature.getID();
}
int snapBoxSideLength = snappingRadius * 2;
java.awt.Point screenLocation = new java.awt.Point(centerPoint.getX(), centerPoint.getY());
Envelope bbox = context.getBoundingBox(screenLocation, snapBoxSideLength);
try {
FeatureCollection<SimpleFeatureType, SimpleFeature> features = context.getFeaturesInBbox(layer, bbox);
FeatureIterator<SimpleFeature> iter = null;
try {
final CoordinateReferenceSystem layerCrs = layer.getCRS();
for (iter = features.features(); iter.hasNext();) {
SimpleFeature feature = iter.next();
if (feature.getID().equals(editFeatureID) && layer == editLayer) {
continue;
}
Geometry geometry = (Geometry) feature.getDefaultGeometry();
if (geometry != null) {
geometry = GeoToolsUtils.reproject(geometry, layerCrs, this.calculationCrs);
searchGeometry(geometry, minFinder);
}
}
} finally {
if (iter != null) {
features.close(iter);
}
}
} catch (Exception e) {
EditPlugin.log("", e); //$NON-NLS-1$
}
}
/**
* @param geom
* any geometry except point or multipoint
* @param minFinder
*/
private void searchGeometry(Geometry geom, MinSegFinder minFinder) {
final int numParts = geom.getNumGeometries();
Geometry partN;
for (int i = 0; i < numParts; i++) {
partN = geom.getGeometryN(i);
if (partN instanceof Polygon) {
Polygon polygon = (Polygon) partN;
searchLinearGeom(polygon.getExteriorRing(), minFinder);
int numInteriorRing = polygon.getNumInteriorRing();
for (int ringN = 0; ringN < numInteriorRing; ringN++) {
searchLinearGeom(polygon.getInteriorRingN(ringN), minFinder);
}
} else {
searchLinearGeom((LineString) partN, minFinder);
}
}
}
private void searchLinearGeom(LineString line, MinSegFinder minFinder) {
final int numPoints = line.getNumPoints();
Coordinate coordinate1;
Coordinate coordinate2;
for (int i = 1; i < numPoints; i++) {
coordinate1 = line.getCoordinateN(i - 1);
coordinate2 = line.getCoordinateN(i);
minFinder.add(coordinate1, coordinate2);
}
}
/**
* @param map
* @param minFinder
* @param screenPoint
* @param snappingRadius
*/
public void findClosestGridSegment( final Point screenPoint,
final IMap map,
final int snappingRadius,
MinSegFinder minFinder) {
List<ILayer> layers = map.getMapLayers();
// by default choose something that will work
ILayer found = layers.get(0);
GridMapGraphic graphic = new GridMapGraphic();
for (ILayer layer : layers) {
if (layer.hasResource(GridMapGraphic.class)) {
found = layer;
try {
graphic = layer.getResource(GridMapGraphic.class, ProgressManager.instance().get());
} catch (IOException e) {
throw new RuntimeException(e);
}
break;
}
}
final IViewportModel viewport = map.getViewportModel();
final Coordinate screenPointInMapCrs = viewport.pixelToWorld(screenPoint.getX(), screenPoint.getY());
LineSegment closestInMapCrs = findClosestGridSegmentInMapCrs(screenPoint, found, graphic, viewport,
screenPointInMapCrs);
if (closestInMapCrs != null) {
final CoordinateReferenceSystem mapCrs = viewport.getCRS();
Coordinate coord2 = viewport.pixelToWorld(screenPoint.getX() + snappingRadius, screenPoint.getY()
+ snappingRadius);
final double snapRadiusInMapCrs = screenPointInMapCrs.distance(coord2);
if (closestInMapCrs.distance(screenPointInMapCrs) <= snapRadiusInMapCrs) {
LineSegment gridSegmentInDestinationCrs;
gridSegmentInDestinationCrs = GeoToolsUtils.reproject(closestInMapCrs, mapCrs, calculationCrs);
minFinder.add(gridSegmentInDestinationCrs.p0, gridSegmentInDestinationCrs.p1);
}
}
}
private LineSegment findClosestGridSegmentInMapCrs( final Point screenPoint,
ILayer found,
GridMapGraphic graphic,
final IViewportModel viewport,
final Coordinate screenPointInMapCrs) {
double[] closestPointInMapCrs;
try {
closestPointInMapCrs = graphic.closest(screenPoint.getX(), screenPoint.getY(), found);
} catch (FactoryException e) {
EditPlugin.log(null, e);
throw new RuntimeException(e);
}
final GridStyle style = getStyle(found);
double dx = style.getGridSize()[0];
double dy = style.getGridSize()[1];
if (GridStyle.Type.SCREEN == style.getType()) {
Coordinate coordinate = viewport.pixelToWorld((int) dx, (int) dy);
dx = coordinate.x;
dy = coordinate.y;
}
MinSegFinder mapCrsFinder = new MinSegFinder(screenPointInMapCrs);
double x = closestPointInMapCrs[0];
double y = closestPointInMapCrs[1];
mapCrsFinder.add(x, y, x + dx, y);
mapCrsFinder.add(x, y, x - dx, y);
mapCrsFinder.add(x, y, x, y + dy);
mapCrsFinder.add(x, y, x, y - dy);
LineSegment closestInMapCrs = mapCrsFinder.getMinSegment();
return closestInMapCrs;
}
/**
* Stolen from {@link GridMapGraphic#getStyle(ILayer layer )} as its
* private.
* <p>
* TODO: ask for {@link GridMapGraphic#getStyle(ILayer layer )} to become
* public?
* </p>
*
* @param layer
* @return
*/
private GridStyle getStyle(ILayer layer) {
GridStyle gridStyle = (GridStyle) layer.getStyleBlackboard().get(GridStyle.ID);
if (gridStyle == null) {
return GridStyle.DEFAULT_STYLE;
}
return gridStyle;
}
/**
* Keeps track of the line segment that is at the minimum distance to the
* center point.
* <p>
* Adapted from the original
* {@link net.refractions.udig.tools.edit.support.EditUtils.MinFinder} by
* Jesse
* </p>
*/
private static class MinSegFinder {
private Coordinate centerCoord;
private LineSegment testingSegment;
private LineSegment minDistanceSegment;
private double minDistance = Double.MAX_VALUE;
public MinSegFinder(Coordinate coord) {
this.centerCoord = coord;
this.testingSegment = new LineSegment();
}
public LineSegment getMinSegment() {
return minDistanceSegment;
}
public void add(double x1, double y1, double x2, double y2) {
add(new Coordinate(x1, y1), new Coordinate(x2, y2));
}
public void add(Coordinate start, Coordinate end) {
testingSegment.setCoordinates(start, end);
double distance = testingSegment.distance(centerCoord);
if (distance < minDistance) {
minDistance = distance;
minDistanceSegment = new LineSegment(start, end);
}
}
}
}