//----------------------------------------------------------------------------// // // // B r o k e n L i n e // // // //----------------------------------------------------------------------------// // <editor-fold defaultstate="collapsed" desc="hdr"> // // Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. // // This software is released under the GNU General Public License. // // Goto http://kenai.com/projects/audiveris to report bugs or suggestions. // //----------------------------------------------------------------------------// // </editor-fold> package omr.util; import omr.constant.Constant; import omr.constant.ConstantSet; import omr.math.GeoPath; import net.jcip.annotations.NotThreadSafe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Line2D; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; /** * Class {@code BrokenLine} handles the broken line defined by a * sequence of points which can be modified at any time. * * <p>This class make use of several distance parameters, presented here from * smaller to larger: * <dl> * <dt><b>colinear</b></dt> * <dd>A point sufficiently close to a segment can be considered as colinear * and thus removed. See {@link #isColinear} method.</dd> * <dt><b>sticky</b></dt> * <dd>A point sufficiently close to a reference point or to a segment allows * to select this reference point or segment. * See {@link #findPoint} and {@link #findSegment} methods.</dd> * <dt><b>dragging</b></dt> * <dd>A point sufficiently close to the last location of a point being dragged * is considered as the new location for this point. This UI feature is actually * beyond the scope of BrokenLine, so only the default dragging value is handled * here for convenience. See {@link #getDraggingDistance}. * </dd> * </dl> * * <p><b>Nota:</b> Internal reference points data can still be modified at any * time, since the BrokenLine, just like a List, merely handles points pointers. * For example, to move a point, just call point.setLocation() method.</p> * * <p>This ability of dynamic modification is the main reason why this class * is not simply implemented as a Path2D. If a Path2D instance is needed, * use the {@link #toGeoPath()} conversion method. * * @author Hervé Bitteur */ @NotThreadSafe @XmlAccessorType(XmlAccessType.NONE) @XmlRootElement(name = "broken-line") public class BrokenLine { //~ Static fields/initializers --------------------------------------------- /** Specific application parameters */ private static final Constants constants = new Constants(); /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(BrokenLine.class); //~ Instance fields -------------------------------------------------------- // /** The ordered sequence of points */ private final List<Point> points = new ArrayList<>(); /** Dummy collection of points, just for (un) marshalling */ @XmlElement(name = "point") private final List<PointFacade> xps = new ArrayList<>(); //~ Constructors ----------------------------------------------------------- // //------------// // BrokenLine // //------------// /** * Creates a new BrokenLine object with an initially empty sequence * of points. */ public BrokenLine () { } //------------// // BrokenLine // //------------// /** * Creates a new BrokenLine object with a few initial points. * * @param points array of initial points */ public BrokenLine (Point... points) { resetPoints(Arrays.asList(points)); } //------------// // BrokenLine // //------------// /** * Creates a new BrokenLine object with a few initial points. * * @param points collection of initial points */ public BrokenLine (Collection<Point> points) { resetPoints(points); } //~ Methods ---------------------------------------------------------------- // //-------------// // resetPoints // //-------------// /** * Replace the current line points with the provided ones. * * @param points the new collection of points */ public final void resetPoints (Collection<Point> points) { if (this.points != points) { this.points.clear(); if (points != null) { this.points.addAll(points); } } } //----------// // addPoint // //----------// /** * Append a point at the end of the current sequence. * * @param point the new point to append */ public void addPoint (Point point) { points.add(point); } //-----------// // findPoint // //-----------// /** * Find the first point of the current sequence which is close to * the provided point (less than sticky distance). * * @param point the provided point * @return the point found, or null if not found */ public Point findPoint (Point point) { Rectangle window = new Rectangle(point); window.grow(getStickyDistance(), getStickyDistance()); for (Point pt : points) { if (window.contains(pt)) { return pt; } } return null; } //-------------// // findSegment // //-------------// /** * Find the closest segment (if any) which lies at a maximum of * sticky distance from the provided point. * * @param point the provided point * @return the sequence point that starts the segment found * (or null if not found) */ public Point findSegment (Point point) { final int sqrStickyDistance = getStickyDistance() * getStickyDistance(); Point bestPoint = null; double bestDistSq = java.lang.Double.MAX_VALUE; if (points.size() < 2) { return null; } Point prevPt = points.get(0); for (Point pt : points) { // Skip first point if (pt == prevPt) { continue; } Line2D.Double line = new Line2D.Double(prevPt, pt); double distSq = line.ptSegDistSq(point); if (distSq < bestDistSq) { bestPoint = prevPt; bestDistSq = distSq; } prevPt = pt; } if (bestDistSq <= sqrStickyDistance) { return bestPoint; } else { return null; } } //----------// // getPoint // //----------// /** * Report the point at 'index' position in current sequence. * * @param index the desired index * @return the desired point */ public Point getPoint (int index) { return points.get(index); } //-----------// // getPoints // //-----------// /** * Report current sequence (meant for debugging). * * @return an unmodifiable view (perhaps empty) of list of current points */ public List<Point> getPoints () { return Collections.unmodifiableList(points); } //-------------------// // getSequenceString // //-------------------// /** * Report a string which summarizes the current sequence of points. * * @return a string of the sequence points */ public String getSequenceString () { StringBuilder sb = new StringBuilder("["); boolean started = false; for (Point p : getPoints()) { if (started) { sb.append(' '); } sb.append('(') .append(p.x) .append(',') .append(p.y) .append(')'); started = true; } sb.append("]"); return sb.toString(); } //---------// // indexOf // //---------// /** * Retrieve the index of provided point. * * @param point the point to look for * @return the index of the point, or -1 */ public int indexOf (Point point) { return points.indexOf(point); } //-------------// // insertPoint // //-------------// /** * Insert a point at the specified index value. * * @param index the insertion position in the sequence * @param point the new point to insert */ public void insertPoint (int index, Point point) { points.add(index, point); } //------------------// // insertPointAfter // //------------------// /** * Insert a point right after the specified point. * * @param point the new point to insert * @param after the point after which insertion must be done */ public void insertPointAfter (Point point, Point after) { int ptIndex = points.indexOf(after); if (ptIndex != -1) { points.add(ptIndex + 1, point); } else { throw new IllegalArgumentException("Insertion point not found"); } } //------------// // isColinear // //------------// /** * Check whether the specified point is colinear (within * colinearDistance) with the previous and the following points in * the sequence. * * @param point the point to check * @return true if the 3 points are colinear or nearly so */ public boolean isColinear (Point point) { int index = points.indexOf(point); if ((index > 0) && (index < (points.size() - 1))) { Line2D.Double line = new Line2D.Double( getPoint(index - 1), getPoint(index + 1)); double dist = line.ptLineDist(point); return dist <= constants.colinearDistance.getValue(); } else { return false; } } //-------------// // removePoint // //-------------// /** * Remove the specified point from the current sequence. * * @param point the point to remove */ public void removePoint (Point point) { points.remove(point); } //------// // size // //------// /** * Report the number of points in the current sequence. * * @return the current size of the points sequence */ public int size () { return points.size(); } //----------// // toString // //----------// @Override public String toString () { return "{BrokenLine " + getSequenceString() + "}"; } //----------------// // afterUnmarshal // //----------------// /** * Called after all the properties (except IDREF) are unmarshalled * for this object, but before this object is set to the parent * object. */ @SuppressWarnings("unused") private void afterUnmarshal (Unmarshaller um, Object parent) { // Convert xps -> points points.clear(); for (PointFacade xp : xps) { points.add(xp.getPoint()); } } //---------------// // beforeMarshal // //---------------// /** * Called immediately before the marshalling of this object begins. */ @SuppressWarnings("unused") private void beforeMarshal (Marshaller m) { // Convert points -> xps xps.clear(); for (Point point : points) { xps.add(new PointFacade(point)); } } //-----------// // toGeoPath // //-----------// /** * Build a GeoPath instance from this BrokenLine instance * * @return the corresponding standard GeoPath instance */ public GeoPath toGeoPath () { GeoPath path = new GeoPath(); boolean started = false; for (Point point : points) { if (!started) { path.moveTo(point.x, point.y); started = true; } else { path.lineTo(point.x, point.y); } } return path; } //---------------------// // getDraggingDistance // //---------------------// /** * Report the dragging distance. * * @return the dragging distance, specified in pixels */ public static int getDraggingDistance () { return constants.draggingDistance.getValue(); } //-------------------// // getStickyDistance // //-------------------// /** * Report the maximum distance (from a point, from a segment). * * @return the maximum distance, specified in pixels */ public static int getStickyDistance () { return constants.stickyDistance.getValue(); } //~ Inner Classes ---------------------------------------------------------- //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- Constant.Integer colinearDistance = new Constant.Integer( "Pixels", 2, "Maximum distance from a point to a segment to be colinear"); Constant.Integer stickyDistance = new Constant.Integer( "Pixels", 5, "Maximum distance from a point or segment to get stuck to it"); Constant.Integer draggingDistance = new Constant.Integer( "Pixels", 25, "Maximum distance from a point to drag it"); } }