001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions.mapmode;
003
004import java.awt.Point;
005import java.util.List;
006import java.util.Optional;
007
008import org.openstreetmap.josm.data.coor.EastNorth;
009import org.openstreetmap.josm.data.osm.Node;
010import org.openstreetmap.josm.data.osm.OsmPrimitive;
011import org.openstreetmap.josm.data.osm.Way;
012import org.openstreetmap.josm.data.osm.WaySegment;
013import org.openstreetmap.josm.gui.MainApplication;
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::isSelectable);
044
045        if (node != null) {
046            Optional<Way> candidate = node.referrers(Way.class).findFirst();
047            if (candidate.isPresent()) {
048                return candidate.get();
049            }
050        }
051
052        return MainApplication.getMap().mapView.getNearestWay(p, OsmPrimitive::isSelectable);
053    }
054
055    /**
056     * Returns the nearest node to cursor. All nodes that are “behind” segments
057     * are neglected. This is to avoid way self-intersection after moving the
058     * candidateNode to a new place.
059     *
060     * @param mv the current map view
061     * @param w the way to check
062     * @param p the cursor position
063     * @return nearest node to cursor
064     */
065    public static Node findCandidateNode(MapView mv, Way w, Point p) {
066        if (mv == null || w == null || p == null) {
067            return null;
068        }
069
070        EastNorth pEN = mv.getEastNorth(p.x, p.y);
071
072        Double bestDistance = Double.MAX_VALUE;
073        Double currentDistance;
074        List<Pair<Node, Node>> wpps = w.getNodePairs(false);
075
076        Node result = null;
077
078        mainLoop:
079        for (Node n : w.getNodes()) {
080            EastNorth nEN = n.getEastNorth();
081
082            if (nEN == null) {
083                // Might happen if lat/lon for that point are not known.
084                continue;
085            }
086
087            currentDistance = pEN.distance(nEN);
088
089            if (currentDistance < bestDistance) {
090                // Making sure this candidate is not behind any segment.
091                for (Pair<Node, Node> wpp : wpps) {
092                    if (!wpp.a.equals(n)
093                            && !wpp.b.equals(n)
094                            && Geometry.getSegmentSegmentIntersection(
095                            wpp.a.getEastNorth(), wpp.b.getEastNorth(),
096                            pEN, nEN) != null) {
097                        continue mainLoop;
098                    }
099                }
100                result = n;
101                bestDistance = currentDistance;
102            }
103        }
104
105        return result;
106    }
107
108    /**
109     * Returns the nearest way segment to cursor. The distance to segment ab is
110     * the length of altitude from p to ab (say, c) or the minimum distance from
111     * p to a or b if c is out of ab.
112     *
113     * The priority is given to segments where c is in ab. Otherwise, a segment
114     * with the largest angle apb is chosen.
115     *
116     * @param mv the current map view
117     * @param w the way to check
118     * @param p the cursor position
119     * @return nearest way segment to cursor
120     */
121    public static WaySegment findCandidateSegment(MapView mv, Way w, Point p) {
122        if (mv == null || w == null || p == null) {
123            return null;
124        }
125
126        EastNorth pEN = mv.getEastNorth(p.x, p.y);
127
128        Double currentDistance;
129        Double currentAngle;
130        Double bestDistance = Double.MAX_VALUE;
131        Double bestAngle = 0.0;
132
133        int candidate = -1;
134
135        List<Pair<Node, Node>> wpps = w.getNodePairs(true);
136
137        int i = -1;
138        for (Pair<Node, Node> wpp : wpps) {
139            ++i;
140
141            EastNorth a = wpp.a.getEastNorth();
142            EastNorth b = wpp.b.getEastNorth();
143
144            // Finding intersection of the segment with its altitude from p
145            EastNorth altitudeIntersection = Geometry.closestPointToSegment(a, b, pEN);
146            currentDistance = pEN.distance(altitudeIntersection);
147
148            if (!altitudeIntersection.equals(a) && !altitudeIntersection.equals(b)) {
149                // If the segment intersects with the altitude from p,
150                // make an angle too big to let this candidate win any others
151                // having the same distance.
152                currentAngle = Double.MAX_VALUE;
153            } else {
154                // Otherwise measure the angle
155                currentAngle = Math.abs(Geometry.getCornerAngle(a, pEN, b));
156            }
157
158            if (currentDistance < bestDistance
159                    || (currentAngle > bestAngle && currentDistance < bestDistance * 1.0001 /*
160                     * equality
161                     */)) {
162                candidate = i;
163                bestAngle = currentAngle;
164                bestDistance = currentDistance;
165            }
166
167        }
168        return candidate != -1 ? new WaySegment(w, candidate) : null;
169    }
170}