001// License: GPL. See LICENSE file for details.
002package org.openstreetmap.josm.actions.mapmode;
003
004import java.awt.Point;
005import java.util.Collection;
006import java.util.List;
007
008import org.openstreetmap.josm.Main;
009import org.openstreetmap.josm.data.coor.EastNorth;
010import org.openstreetmap.josm.data.osm.Node;
011import org.openstreetmap.josm.data.osm.OsmPrimitive;
012import org.openstreetmap.josm.data.osm.Way;
013import org.openstreetmap.josm.data.osm.WaySegment;
014import org.openstreetmap.josm.gui.MapView;
015import org.openstreetmap.josm.tools.Geometry;
016import org.openstreetmap.josm.tools.Pair;
017
018/**
019 * This static class contains functions used to find target way, node to move or
020 * segment to divide.
021 *
022 * @author Alexander Kachkaev <alexander@kachkaev.ru>, 2011
023 */
024final class ImproveWayAccuracyHelper {
025
026    private ImproveWayAccuracyHelper() {
027        // Hide default constructor for utils classes
028    }
029    
030    /**
031     * Finds the way to work on. If the mouse is on the node, extracts one of
032     * the ways containing it. If the mouse is on the way, simply returns it.
033     *
034     * @param mv the current map view
035     * @param p the cursor position
036     * @return {@code Way} or {@code null} in case there is nothing under the cursor.
037     */
038    public static Way findWay(MapView mv, Point p) {
039        if (mv == null || p == null) {
040            return null;
041        }
042
043        Node node = mv.getNearestNode(p, OsmPrimitive.isSelectablePredicate);
044        Way candidate = null;
045
046        if (node != null) {
047            final Collection<OsmPrimitive> candidates = node.getReferrers();
048            for (OsmPrimitive refferer : candidates) {
049                if (refferer instanceof Way) {
050                    candidate = (Way) refferer;
051                    break;
052                }
053            }
054            if (candidate != null) {
055                return candidate;
056            }
057        }
058
059        candidate = Main.map.mapView.getNearestWay(p,
060                OsmPrimitive.isSelectablePredicate);
061
062        return candidate;
063    }
064
065    /**
066     * Returns the nearest node to cursor. All nodes that are “behind” segments
067     * are neglected. This is to avoid way self-intersection after moving the
068     * candidateNode to a new place.
069     *
070     * @param mv the current map view
071     * @param w the way to check
072     * @param p the cursor position
073     * @return nearest node to cursor
074     */
075    public static Node findCandidateNode(MapView mv, Way w, Point p) {
076        if (mv == null || w == null || p == null) {
077            return null;
078        }
079
080        EastNorth pEN = mv.getEastNorth(p.x, p.y);
081
082        Double bestDistance = Double.MAX_VALUE;
083        Double currentDistance;
084        List<Pair<Node, Node>> wpps = w.getNodePairs(false);
085
086        Node result = null;
087
088        mainLoop:
089        for (Node n : w.getNodes()) {
090            EastNorth nEN = n.getEastNorth();
091            currentDistance = pEN.distance(nEN);
092
093            if (currentDistance < bestDistance) {
094                // Making sure this candidate is not behind any segment.
095                for (Pair<Node, Node> wpp : wpps) {
096                    if (!wpp.a.equals(n)
097                            && !wpp.b.equals(n)
098                            && Geometry.getSegmentSegmentIntersection(
099                            wpp.a.getEastNorth(), wpp.b.getEastNorth(),
100                            pEN, nEN) != null) {
101                        continue mainLoop;
102                    }
103                }
104                result = n;
105                bestDistance = currentDistance;
106            }
107        }
108
109        return result;
110    }
111
112    /**
113     * Returns the nearest way segment to cursor. The distance to segment ab is
114     * the length of altitude from p to ab (say, c) or the minimum distance from
115     * p to a or b if c is out of ab.
116     *
117     * The priority is given to segments where c is in ab. Otherwise, a segment
118     * with the largest angle apb is chosen.
119     *
120     * @param mv the current map view
121     * @param w the way to check
122     * @param p the cursor position
123     * @return nearest way segment to cursor
124     */
125    public static WaySegment findCandidateSegment(MapView mv, Way w, Point p) {
126        if (mv == null || w == null || p == null) {
127            return null;
128        }
129
130        EastNorth pEN = mv.getEastNorth(p.x, p.y);
131
132        Double currentDistance;
133        Double currentAngle;
134        Double bestDistance = Double.MAX_VALUE;
135        Double bestAngle = 0.0;
136
137        int candidate = -1;
138
139        List<Pair<Node, Node>> wpps = w.getNodePairs(true);
140
141        int i = -1;
142        for (Pair<Node, Node> wpp : wpps) {
143            ++i;
144
145            // Finding intersection of the segment with its altitude from p (c)
146            EastNorth altitudeIntersection = Geometry.getSegmentAltituteIntersection(wpp.a.getEastNorth(),
147                    wpp.b.getEastNorth(), pEN);
148
149            if (altitudeIntersection != null) {
150                // If the segment intersects with the altitude from p
151                currentDistance = pEN.distance(altitudeIntersection);
152
153                // Making an angle too big to let this candidate win any others
154                // having the same distance.
155                currentAngle = Double.MAX_VALUE;
156
157            } else {
158                // Otherwise: Distance is equal to the shortest distance from p
159                // to a or b
160                currentDistance = Math.min(pEN.distance(wpp.a.getEastNorth()),
161                        pEN.distance(wpp.b.getEastNorth()));
162
163                // Measuring the angle
164                currentAngle = Math.abs(Geometry.getCornerAngle(
165                        wpp.a.getEastNorth(), pEN, wpp.b.getEastNorth()));
166            }
167
168            if (currentDistance < bestDistance
169                    || (currentAngle > bestAngle && currentDistance < bestDistance * 1.0001 /*
170                     * equality
171                     */)) {
172                candidate = i;
173                bestAngle = currentAngle;
174                bestDistance = currentDistance;
175            }
176
177        }
178        return candidate != -1 ? new WaySegment(w, candidate) : null;
179    }
180}