/* * 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.plugin.editing.client.snap.algorithm; import org.geomajas.annotation.Api; import org.geomajas.geometry.Coordinate; import org.geomajas.geometry.Geometry; import org.geomajas.geometry.service.MathService; import org.geomajas.plugin.editing.client.snap.SnapAlgorithm; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; /** * <p> Snapping algorithm that snaps to the closest end-point (vertex) of a geometry. Only coordinates that are * effectively stored in the geometries come into account. This makes it a pretty fast algorithm. </p> <p> Also, when * applying the list of geometries to snap to, this list is sorted into 2 lists of coordinates: one sorted by * X-ordinates, one sorted by Y-ordinates. This may take some time initially, but afterwards you'll reap the results, as * possible snapping points can quickly be fetched using the binary search algorithm. </p> * * @author Pieter De Graef * @since 2.0.0 */ @Api(allMethods = true) public class NearestVertexSnapAlgorithm implements SnapAlgorithm { /** * List of coordinates, all sorted (ascending) by their X-ordinate. */ private List<Coordinate> sortedX; /** * List of coordinates, all sorted (ascending) by their Y-ordinate. */ private List<Coordinate> sortedY; private double calculatedDistance; private boolean hasSnapped; // ------------------------------------------------------------------------ // SnappingAlgorithm implementation: // ------------------------------------------------------------------------ /** * Execute the snap operation. * * @param coordinate The original location. * @param distance The maximum distance allowed for snapping. * @return The new location. If no snapping target was found, this may return the original location. */ public Coordinate snap(Coordinate coordinate, double distance) { // Some initialization: calculatedDistance = distance; hasSnapped = false; Coordinate snappingPoint = coordinate; if (sortedX != null && sortedY != null) { // Calculate the distances for all candidates: List<Coordinate> coordinates = getPossibleCoordinates(coordinate, distance); for (Coordinate candidate : coordinates) { double d = MathService.distance(coordinate, candidate); if (d < calculatedDistance) { snappingPoint = candidate; calculatedDistance = d; hasSnapped = true; } } } return snappingPoint; } /** * Set the full list of target geometries. These are the geometries where to this snapping algorithm can snap. * * @param geometries The list of target geometries. */ public void setGeometries(Geometry[] geometries) { List<Coordinate> coordinates = getCoordinates(geometries); sortedX = sortX(coordinates); sortedY = sortY(coordinates); } /** * Get the effective distance that was bridged during the snap operation. In case snapping occurred, this distance * will be smaller than the given "distance" value during the last call to snap. * * @return The effective snapping distance. Only valid if snapping actually occurred. */ public double getCalculatedDistance() { return calculatedDistance; } /** * Has snapping actually occurred during the last call to the <code>snap</code> method? If so the returned snap * location was different from the original location. * * @return Returns if the returned location from the snap method differs from the original location. */ public boolean hasSnapped() { return hasSnapped; } // ------------------------------------------------------------------------- // Private methods: // ------------------------------------------------------------------------- /** * Get a single list of coordinates from an array of geometries. */ private List<Coordinate> getCoordinates(Geometry[] geometries) { List<Coordinate> coordinates = new ArrayList<Coordinate>(); for (Geometry geometry : geometries) { addCoordinateArrays(geometry, coordinates); } return coordinates; } /** * Add all coordinates within the geometry to the list. */ private void addCoordinateArrays(Geometry geometry, List<Coordinate> coordinates) { if (geometry.getGeometries() != null) { for (Geometry child : geometry.getGeometries()) { addCoordinateArrays(child, coordinates); } } else if (geometry.getCoordinates() != null) { for (Coordinate coordinate : geometry.getCoordinates()) { coordinates.add(coordinate); } } } /** * Return a new and sorted list of coordinates. They should be sorted by their X values. */ private List<Coordinate> sortX(List<Coordinate> coordinates) { List<Coordinate> sorted = new ArrayList<Coordinate>(coordinates); Collections.sort(sorted, new XComparator()); return sorted; } /** * Return a new and sorted list of coordinates. They should be sorted by their Y values. */ private List<Coordinate> sortY(List<Coordinate> coordinates) { List<Coordinate> sorted = new ArrayList<Coordinate>(coordinates); Collections.sort(sorted, new YComparator()); return sorted; } /** * Return a possible list of coordinates that are within range of the given coordinate. This function is very fast * as it uses binary search, and it returns a small subset of coordinates. The perfect set to start calculating * from. */ private List<Coordinate> getPossibleCoordinates(Coordinate coordinate, double max) { int xMin = Collections.binarySearch(sortedX, new Coordinate(coordinate.getX() - max, 0), new XComparator()); int xMax = Collections.binarySearch(sortedX, new Coordinate(coordinate.getX() + max, 0), new XComparator()); int yMin = Collections.binarySearch(sortedY, new Coordinate(0, coordinate.getY() - max), new YComparator()); int yMax = Collections.binarySearch(sortedY, new Coordinate(0, coordinate.getY() + max), new YComparator()); if (xMin < 0) { xMin = Math.abs(xMin) - 1; } if (xMax < 0) { xMax = Math.abs(xMax) - 1; } if (yMin < 0) { yMin = Math.abs(yMin) - 1; } if (yMax < 0) { yMax = Math.abs(yMax) - 1; } List<Coordinate> coordinates = new ArrayList<Coordinate>(); for (int i = xMin; i < xMax; i++) { coordinates.add(sortedX.get(i)); } for (int i = yMin; i < yMax; i++) { coordinates.add(sortedY.get(i)); } return coordinates; } // ------------------------------------------------------------------------- // Private classes: // ------------------------------------------------------------------------- /** * Private class that compares a coordinate's X values. * * @author Pieter De Graef */ private class XComparator implements Comparator<Coordinate> { public int compare(Coordinate c1, Coordinate c2) { if (c1.getX() < c2.getX()) { return -1; } if (c1.getX() > c2.getX()) { return 1; } return 0; } } /** * Private class that compares a coordinate's Y values. * * @author Pieter De Graef */ private class YComparator implements Comparator<Coordinate> { public int compare(Coordinate c1, Coordinate c2) { if (c1.getY() < c2.getY()) { return -1; } if (c1.getY() > c2.getY()) { return 1; } return 0; } } }