Ticket #23444: 23444.patch

File 23444.patch, 11.1 KB (added by taylor.smock, 4 months ago)
  • src/org/openstreetmap/josm/actions/mapmode/ImproveWayAccuracyHelper.java

     
    33
    44import java.awt.Point;
    55import java.util.List;
    6 import java.util.Optional;
    76
    87import org.openstreetmap.josm.data.coor.EastNorth;
    98import org.openstreetmap.josm.data.osm.Node;
     
    4342        Node node = mv.getNearestNode(p, OsmPrimitive::isSelectable);
    4443
    4544        if (node != null) {
    46             Optional<Way> candidate = node.referrers(Way.class).findFirst();
    47             if (candidate.isPresent()) {
    48                 return candidate.get();
     45            for (Way way : node.getParentWays()) {
     46                // We don't want to return ways that are not complete
     47                if (way.isUsable()) {
     48                    return way;
     49                }
    4950            }
    5051        }
    5152
     
    6970
    7071        EastNorth pEN = mv.getEastNorth(p.x, p.y);
    7172
    72         Double bestDistance = Double.MAX_VALUE;
    73         Double currentDistance;
     73        double bestDistance = Double.MAX_VALUE;
     74        double currentDistance;
    7475        List<Pair<Node, Node>> wpps = w.getNodePairs(false);
    7576
    7677        Node result = null;
    7778
    78         mainLoop:
    7979        for (Node n : w.getNodes()) {
    8080            EastNorth nEN = n.getEastNorth();
    8181
     
    8686
    8787            currentDistance = pEN.distance(nEN);
    8888
    89             if (currentDistance < bestDistance) {
    90                 // Making sure this candidate is not behind any segment.
    91                 for (Pair<Node, Node> wpp : wpps) {
    92                     if (!wpp.a.equals(n)
    93                             && !wpp.b.equals(n)
    94                             && Geometry.getSegmentSegmentIntersection(
    95                             wpp.a.getEastNorth(), wpp.b.getEastNorth(),
    96                             pEN, nEN) != null) {
    97                         continue mainLoop;
    98                     }
    99                 }
     89            if (currentDistance < bestDistance && ensureCandidateIsNotBehindSegments(wpps, n, pEN, nEN)) {
    10090                result = n;
    10191                bestDistance = currentDistance;
    10292            }
     
    10696    }
    10797
    10898    /**
     99     * Check to see if a candidate node is underneath a way segment
     100     *
     101     * @param wpps The pairs of nodes to check for crossing way segments
     102     * @param n The current node to check
     103     * @param pEN The cursor east-north position
     104     * @param nEN The node east-north position
     105     * @return {@code true} if the candidate node is underneath a way segment
     106     */
     107    private static boolean ensureCandidateIsNotBehindSegments(Iterable<Pair<Node, Node>> wpps, Node n, EastNorth pEN, EastNorth nEN) {
     108        // Making sure this candidate is not behind any segment.
     109        for (Pair<Node, Node> wpp : wpps) {
     110            if (!wpp.a.equals(n)
     111                    && !wpp.b.equals(n)
     112                    && Geometry.getSegmentSegmentIntersection(
     113                    wpp.a.getEastNorth(), wpp.b.getEastNorth(),
     114                    pEN, nEN) != null) {
     115                return false;
     116            }
     117        }
     118        return true;
     119    }
     120
     121    /**
    109122     * Returns the nearest way segment to cursor. The distance to segment ab is
    110123     * the length of altitude from p to ab (say, c) or the minimum distance from
    111124     * p to a or b if c is out of ab.
    112      *
     125     * <p>
    113126     * The priority is given to segments where c is in ab. Otherwise, a segment
    114127     * with the largest angle apb is chosen.
    115128     *
     
    125138
    126139        EastNorth pEN = mv.getEastNorth(p.x, p.y);
    127140
    128         Double currentDistance;
    129         Double currentAngle;
    130         Double bestDistance = Double.MAX_VALUE;
    131         Double bestAngle = 0.0;
     141        double bestDistance = Double.MAX_VALUE;
     142        double bestAngle = 0.0;
    132143
    133144        int candidate = -1;
    134145
     
    143154
    144155            // Finding intersection of the segment with its altitude from p
    145156            EastNorth altitudeIntersection = Geometry.closestPointToSegment(a, b, pEN);
    146             currentDistance = pEN.distance(altitudeIntersection);
     157            final double currentDistance = pEN.distance(altitudeIntersection);
    147158
     159            final double currentAngle;
    148160            if (!altitudeIntersection.equals(a) && !altitudeIntersection.equals(b)) {
    149161                // If the segment intersects with the altitude from p,
    150162                // make an angle too big to let this candidate win any others
  • test/unit/org/openstreetmap/josm/actions/mapmode/ImproveWayAccuracyActionTest.java

     
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.actions.mapmode;
    33
     4import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
    45import static org.junit.jupiter.api.Assertions.assertEquals;
     6import static org.junit.jupiter.api.Assertions.assertFalse;
    57import static org.junit.jupiter.api.Assertions.assertTrue;
    68
     9import java.awt.Graphics2D;
     10import java.awt.Point;
     11import java.awt.event.MouseEvent;
     12import java.awt.image.BufferedImage;
     13
    714import org.junit.jupiter.api.Test;
     15import org.junit.jupiter.api.extension.RegisterExtension;
    816import org.openstreetmap.josm.TestUtils;
    917import org.openstreetmap.josm.actions.mapmode.ImproveWayAccuracyAction.State;
     18import org.openstreetmap.josm.data.Bounds;
     19import org.openstreetmap.josm.data.coor.ILatLon;
     20import org.openstreetmap.josm.data.coor.LatLon;
    1021import org.openstreetmap.josm.data.osm.DataSet;
     22import org.openstreetmap.josm.data.osm.Node;
     23import org.openstreetmap.josm.data.osm.Way;
    1124import org.openstreetmap.josm.gui.MainApplication;
    1225import org.openstreetmap.josm.gui.MapFrame;
     26import org.openstreetmap.josm.gui.MapView;
    1327import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     28import org.openstreetmap.josm.gui.util.GuiHelper;
    1429import org.openstreetmap.josm.testutils.annotations.Main;
    1530import org.openstreetmap.josm.testutils.annotations.Projection;
     31import org.openstreetmap.josm.testutils.mockers.WindowlessMapViewStateMocker;
     32import org.openstreetmap.josm.testutils.mockers.WindowlessNavigatableComponentMocker;
    1633
     34import mockit.Mock;
     35
    1736/**
    1837 * Unit tests for class {@link ImproveWayAccuracyAction}.
    1938 */
    20 @Main
    2139@Projection
    2240class ImproveWayAccuracyActionTest {
     41    @RegisterExtension
     42    Main.MainExtension mainExtension = new Main.MainExtension().setMapViewMocker(SizedWindowlessMapViewStateMocker::new)
     43            .setNavigableComponentMocker(SizedWindowlessNavigatableComponentMocker::new);
     44
     45    private static final int WIDTH = 800;
     46    private static final int HEIGHT = 600;
     47    private static class SizedWindowlessMapViewStateMocker extends WindowlessMapViewStateMocker {
     48        @Mock
     49        public int getWidth() {
     50            return WIDTH;
     51        }
     52
     53        @Mock
     54        public int getHeight() {
     55            return HEIGHT;
     56        }
     57    }
     58
     59    private static class SizedWindowlessNavigatableComponentMocker extends WindowlessNavigatableComponentMocker {
     60        @Mock
     61        public int getWidth() {
     62            return WIDTH;
     63        }
     64
     65        @Mock
     66        public int getHeight() {
     67            return HEIGHT;
     68        }
     69    }
     70
     71    private static MouseEvent generateEvent(MapView mapView, ILatLon location) {
     72        final Point p = mapView.getPoint(location);
     73        return new MouseEvent(mapView, 0, 0, 0, p.x, p.y, p.x, p.y, 1, false, MouseEvent.BUTTON1);
     74    }
     75
    2376    /**
    2477     * Unit test of {@link ImproveWayAccuracyAction#enterMode} and {@link ImproveWayAccuracyAction#exitMode}.
    2578     */
     
    4699    void testEnumState() {
    47100        TestUtils.superficialEnumCodeCoverage(State.class);
    48101    }
     102
     103    @Test
     104    void testNonRegression23444() {
     105        final int width = 800;
     106        final int height = 600;
     107        final DataSet dataSet = new DataSet();
     108        final OsmDataLayer layer = new OsmDataLayer(dataSet, "ImproveWayAccuracyActionT", null);
     109        MainApplication.getLayerManager().addLayer(layer);
     110        final ImproveWayAccuracyAction mapMode = new ImproveWayAccuracyAction();
     111        final MapFrame map = MainApplication.getMap();
     112        assertTrue(map.selectMapMode(mapMode));
     113        assertEquals(mapMode, map.mapMode);
     114        final Way testWay = TestUtils.newWay("", new Node(1, 1), new Node(2, 1),
     115                new Node(3), new Node(4, 1), new Node(5, 1));
     116        testWay.firstNode().setCoor(new LatLon(0, 0));
     117        testWay.lastNode().setCoor(new LatLon(0.001, 0.001));
     118        testWay.getNode(1).setCoor(new LatLon(0.0001, 0.0001));
     119        testWay.getNode(3).setCoor(new LatLon(0.0009, 0.0009));
     120        dataSet.addPrimitiveRecursive(testWay);
     121        assertFalse(testWay.getNode(2).isLatLonKnown(), "The second node should not have valid coordinates");
     122        final Graphics2D g2d = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB).createGraphics();
     123        g2d.setClip(0, 0, WIDTH, HEIGHT);
     124        try {
     125            // If this fails, something else is wrong
     126            assertDoesNotThrow(() -> map.mapView.paint(g2d), "The mapview should be able to handle a null coordinate node");
     127            // Ensure that the test way is selected (and use the methods from the action to do so)
     128            GuiHelper.runInEDTAndWaitWithException(() -> {
     129                map.mapView.zoomTo(new Bounds(0, 0, 0.001, 0.001));
     130                // Get the target way selected (note: not selected in dataset -- use mapMode.mapReleased for that)
     131                mapMode.mouseMoved(generateEvent(map.mapView, testWay.getNode(1)));
     132            });
     133            // mouseMoved shouldn't cause the way to get selected
     134            assertFalse(dataSet.getAllSelected().contains(testWay));
     135
     136            // Now check painting (where the problem should occur; the mapMode.paint call should be called as part of the map.mapView.paint call)
     137            assertDoesNotThrow(() -> map.mapView.paint(g2d));
     138            assertDoesNotThrow(() -> mapMode.paint(g2d, map.mapView, new Bounds(0, 0, 0.001, 0.001)));
     139
     140            // Finally, check painting during selection
     141            GuiHelper.runInEDTAndWaitWithException(() -> {
     142                // Set the way as selected
     143                mapMode.mouseReleased(generateEvent(map.mapView, new LatLon(0.0001, 0.0001)));
     144                // Set the mouse location (unset in mouseReleased call)
     145                mapMode.mouseMoved(generateEvent(map.mapView, new LatLon(0.0001, 0.0001)));
     146            });
     147            // The way shouldn't be selected, since it isn't usable for the improve way tool
     148            assertFalse(dataSet.getAllSelected().contains(testWay));
     149            // Now check painting again (just in case)
     150            assertDoesNotThrow(() -> map.paint(g2d));
     151            assertDoesNotThrow(() -> mapMode.paint(g2d, map.mapView, new Bounds(0, 0, 0.001, 0.001)));
     152        } finally {
     153            g2d.dispose();
     154        }
     155    }
    49156}