/* 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 licence 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.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.udig.ui.commons.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 Gabriel Roldan (www.axios.es)
* @author Mauricio Pazos (www.axios.es)
* @since 0.2.0
*/
public class SnapSegmentFinder {
/**
* Which CRS to perform the seach 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 IllegalStateException( e.getMessage(), e );
} catch (TransformException e) {
throw new IllegalStateException( e.getMessage(), 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.getDefaultGeometry().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 IllegalStateException( e.getMessage(), 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 IllegalStateException( e.getMessage(), 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);
}
}
}
}