Ticket #5179: osm-join-areas.patch

File osm-join-areas.patch, 14.2 KB (added by extropy, 21 months ago)

New implementation of findInnerWays, fixes reported problems.

  • src/org/openstreetmap/josm/actions/JoinAreasAction.java

     
    66import static org.openstreetmap.josm.tools.I18n.trn; 
    77 
    88import java.awt.GridBagLayout; 
    9 import java.awt.Polygon; 
    109import java.awt.event.ActionEvent; 
    1110import java.awt.event.KeyEvent; 
    1211import java.awt.geom.Area; 
     
    185184        if(!same) { 
    186185            int i = 0; 
    187186            if(checkForTagConflicts(a, b)) return true; // User aborted, so don't warn again 
     187 
     188            //join each area with itself, fixing self-crossings. 
    188189            if(joinAreas(a, a)) { 
    189190                ++i; 
    190191            } 
     
    210211 
    211212        Collection<Way> allWays = splitWaysOnNodes(a, b, nodes); 
    212213 
    213         // Find all nodes and inner ways save them to a list 
    214         Collection<Node> allNodes = getNodesFromWays(allWays); 
    215         Collection<Way> innerWays = findInnerWays(allWays, allNodes); 
     214        // Find inner ways save them to a list 
     215        Collection<Way> outerWays = findOuterWays(allWays); 
     216        Collection<Way> innerWays = findInnerWays(allWays, outerWays); 
    216217 
    217218        // Join outer ways 
    218         Way outerWay = joinOuterWays(allWays, innerWays); 
     219        Way outerWay = joinOuterWays(outerWays); 
    219220        if (outerWay == null) 
    220221            return true; 
    221222 
     
    538539        return allNodes; 
    539540    } 
    540541 
     542 
    541543    /** 
    542      * Finds all inner ways for a given list of Ways and Nodes from a multigon by constructing a polygon 
    543      * for each way, looking for inner nodes that are not part of this way. If a node is found, all ways 
    544      * containing this node are added to the list 
     544     * Gets all inner ways given all ways and outer ways. 
     545     * @param multigonWays 
     546     * @param outerWays 
     547     * @return list of inner ways. 
     548     */ 
     549    private Collection<Way> findInnerWays(Collection<Way> multigonWays,Collection<Way> outerWays) { 
     550        ArrayList<Way> innerWays = new ArrayList<Way>(); 
     551        for(Way way: multigonWays) { 
     552            if (!outerWays.contains(way)) { 
     553                innerWays.add(way); 
     554            } 
     555        } 
     556 
     557        return innerWays; 
     558    } 
     559 
     560 
     561    /** 
     562     * Finds all ways for a given list of Ways that form the outer hull. 
     563     * This works by starting with one node and traversing the multigon clockwise, always picking the leftmost path. 
     564     * Prerequisites - the ways must not intersect and have common end nodes where they meet. 
    545565     * @param Collection<Way> A list of (splitted) ways that form a multigon 
    546      * @param Collection<Node> A list of nodes that belong to the multigon 
    547      * @return Collection<Way> A list of ways that are positioned inside the outer borders of the multigon 
     566     * @return Collection<Way> A list of ways that form the outer boundary of the multigon. 
    548567     */ 
    549     private Collection<Way> findInnerWays(Collection<Way> multigonWays, Collection<Node> multigonNodes) { 
    550         Collection<Way> innerWays = new ArrayList<Way>(); 
    551         for(Way w: multigonWays) { 
    552             Polygon poly = new Polygon(); 
    553             for(Node n: (w).getNodes()) { 
    554                 poly.addPoint(latlonToXY(n.getCoor().lat()), latlonToXY(n.getCoor().lon())); 
     568    public static Collection<Way> findOuterWays(Collection<Way> multigonWays) { 
     569 
     570        //find the node with minimum lat - it's guaranteed to be outer. (What about the south pole?) 
     571        Way bestWay = null; 
     572        Node topNode = null; 
     573        int topIndex = 0; 
     574        double minLat = Double.POSITIVE_INFINITY; 
     575 
     576        for(Way way: multigonWays) { 
     577            for (int pos = 0; pos < way.getNodesCount(); pos ++){ 
     578                Node node = way.getNode(pos); 
     579 
     580                if (node.getCoor().lat() < minLat){ 
     581                    minLat = node.getCoor().lat(); 
     582                    bestWay = way; 
     583                    topNode = node; 
     584                    topIndex = pos; 
     585                } 
    555586            } 
     587        } 
    556588 
    557             for(Node n: multigonNodes) { 
    558                 if(!(w).containsNode(n) && poly.contains(latlonToXY(n.getCoor().lat()), latlonToXY(n.getCoor().lon()))) { 
    559                     getWaysByNode(innerWays, multigonWays, n); 
     589        //get two final nodes from best way to mark as starting point and orientation. 
     590        Node headNode = null; 
     591        Node prevNode = null; 
     592 
     593        if (topNode.equals(bestWay.firstNode()) || topNode.equals(bestWay.lastNode())) 
     594        { 
     595            //node is in split point 
     596            headNode = topNode; 
     597            //make a fake node that is downwards from head node (smaller latitude). It will be a division point between paths. 
     598            prevNode = new Node(new LatLon(headNode.getCoor().lat() - 1000, headNode.getCoor().lon())); 
     599        } 
     600        else 
     601        { 
     602            //node is inside way - pick the clockwise going end. 
     603            Node prev = bestWay.getNode(topIndex - 1); 
     604            Node next = bestWay.getNode(topIndex + 1); 
     605 
     606            if (angleIsClockwise(prev, topNode, next)){ 
     607                headNode = bestWay.lastNode(); 
     608                prevNode = bestWay.getNode(bestWay.getNodesCount() - 2); 
     609            } 
     610            else 
     611            { 
     612                headNode = bestWay.firstNode(); 
     613                prevNode = bestWay.getNode(1); 
     614            } 
     615        } 
     616 
     617        ArrayList<Way> outerWays = new ArrayList<Way>(); 
     618 
     619        //iterate till full circle is reached 
     620        while (true){ 
     621 
     622            bestWay = null; 
     623            Node bestWayNextNode = null; 
     624            boolean bestWayReverse = false; 
     625 
     626            for (Way way: multigonWays) 
     627            { 
     628                boolean wayReverse; 
     629                Node nextNode; 
     630 
     631                if (way.firstNode().equals(headNode)){ 
     632                    nextNode = way.getNode(1); 
     633                    wayReverse = false; 
    560634                } 
     635                else if (way.lastNode().equals(headNode)) 
     636                { 
     637                    nextNode = way.getNode(way.getNodesCount() - 2); 
     638                    wayReverse = true; 
     639                } 
     640                else 
     641                { 
     642                    //this way not adjacent to headNode 
     643                    continue; 
     644                } 
     645 
     646                if (nextNode.equals(prevNode)) 
     647                { 
     648                    //this is the path we came from - ignore it. 
     649                } 
     650                else if (bestWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) 
     651                { 
     652                    //the new way is better 
     653                    bestWay = way; 
     654                    bestWayReverse = wayReverse; 
     655                    bestWayNextNode = nextNode; 
     656                } 
    561657            } 
     658 
     659            if (bestWay == null) 
     660                //this should not happen. Internal error here. 
     661                return null; 
     662            else if (outerWays.contains(bestWay)){ 
     663                //full circle reached, terminate. 
     664                break; 
     665            } 
     666            else 
     667            { 
     668                //add to outer ways, repeat. 
     669                outerWays.add(bestWay); 
     670                headNode = bestWayReverse ? bestWay.firstNode() : bestWay.lastNode(); 
     671                prevNode = bestWayReverse ? bestWay.getNode(2) : bestWay.getNode(bestWay.getNodesCount() - 2); 
     672            } 
    562673        } 
    563674 
    564         return innerWays; 
     675        return outerWays; 
    565676    } 
    566677 
    567     // Polygon only supports int coordinates, so convert them 
    568     private int latlonToXY(double val) { 
    569         return (int)Math.round(val*1000000); 
     678    /** 
     679     * Tests if given point is to the right side of path consisting of 3 points. 
     680     * @param lineP1 first point in path 
     681     * @param lineP2 second point in path 
     682     * @param lineP3 third point in path 
     683     * @param testPoint 
     684     * @return true if to the right side, false otherwise 
     685     */ 
     686    public static boolean isToTheRightSideOfLine(Node lineP1, Node lineP2, Node lineP3, Node testPoint) 
     687    { 
     688        boolean pathBendToRight = angleIsClockwise(lineP1, lineP2, lineP3); 
     689        boolean rightOfSeg1 = angleIsClockwise(lineP1, lineP2, testPoint); 
     690        boolean rightOfSeg2 = angleIsClockwise(lineP2, lineP3, testPoint); 
     691 
     692        if (pathBendToRight) 
     693            return rightOfSeg1 && rightOfSeg2; 
     694        else 
     695            return !(!rightOfSeg1 && !rightOfSeg2); 
    570696    } 
    571697 
    572698    /** 
    573      * Finds all ways that contain the given node. 
    574      * @param Collection<Way> A list to which matching ways will be added 
    575      * @param Collection<Way> A list of ways to check 
    576      * @param Node The node the ways should be checked against 
     699     * This method tests if secondNode is clockwise to first node. 
     700     * @param commonNode starting point for both vectors 
     701     * @param firstNode first vector end node 
     702     * @param secondNode second vector end node 
     703     * @return true if first vector is clockwise before second vector. 
    577704     */ 
    578     private void getWaysByNode(Collection<Way> innerWays, Collection<Way> w, Node n) { 
    579         for(Way way : w) { 
    580             if(!(way).containsNode(n)) { 
     705    public static boolean angleIsClockwise(Node commonNode, Node firstNode, Node secondNode) 
     706    { 
     707        double dla1 = (firstNode.getCoor().lat() - commonNode.getCoor().lat()); 
     708        double dla2 = (secondNode.getCoor().lat() - commonNode.getCoor().lat()); 
     709        double dlo1 = (firstNode.getCoor().lon() - commonNode.getCoor().lon()); 
     710        double dlo2 = (secondNode.getCoor().lon() - commonNode.getCoor().lon()); 
     711 
     712        return dla1 * dlo2 - dlo1 * dla2 > 0; 
     713    } 
     714 
     715 
     716    /** 
     717     * Tests if point is inside a polygon. The polygon can be self-intersecting. In such case the contains function works in xor-like manner. 
     718     * @param polygonNodes list of nodes from polygon path. 
     719     * @param point the point to test 
     720     * @return true if the point is inside polygon. 
     721     * FIXME: this should probably be moved to tools.. 
     722     */ 
     723    public static boolean nodeInsidePolygon(ArrayList<Node> polygonNodes, Node point) 
     724    { 
     725        if (polygonNodes.size() < 3) 
     726            return false; 
     727 
     728        boolean inside = false; 
     729        Node p1, p2; 
     730 
     731        //iterate each side of the polygon, start with the last segment 
     732        Node oldPoint = polygonNodes.get(polygonNodes.size() - 1); 
     733 
     734        for(Node newPoint: polygonNodes) 
     735        { 
     736            //skip duplicate points 
     737            if (newPoint.equals(oldPoint)) { 
    581738                continue; 
    582739            } 
    583             if(!innerWays.contains(way)) { 
    584                 innerWays.add(way); // Will need this later for multigons 
     740 
     741            //order points so p1.lat <= p2.lat; 
     742            if (newPoint.getCoor().lat() > oldPoint.getCoor().lat()) 
     743            { 
     744                p1 = oldPoint; 
     745                p2 = newPoint; 
    585746            } 
     747            else 
     748            { 
     749                p1 = newPoint; 
     750                p2 = oldPoint; 
     751            } 
     752 
     753            //test if the line is crossed and if so invert the inside flag. 
     754            if ((newPoint.getCoor().lat() < point.getCoor().lat()) == (point.getCoor().lat() <= oldPoint.getCoor().lat()) 
     755                    && (point.getCoor().lon() - p1.getCoor().lon()) * (p2.getCoor().lat() - p1.getCoor().lat()) 
     756                    < (p2.getCoor().lon() - p1.getCoor().lon()) * (point.getCoor().lat() - p1.getCoor().lat())) 
     757            { 
     758                inside = !inside; 
     759            } 
     760 
     761            oldPoint = newPoint; 
    586762        } 
     763 
     764        return inside; 
    587765    } 
    588766 
     767 
     768 
     769 
    589770    /** 
    590      * Joins the two outer ways and deletes all short ways that can't be part of a multipolygon anyway 
    591      * @param Collection<OsmPrimitive> The list of all ways that belong to that multigon 
    592      * @param Collection<Way> The list of inner ways that belong to that multigon 
     771     * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway. 
     772     * @param Collection<Way> The list of outer ways that belong to that multigon. 
    593773     * @return Way The newly created outer way 
    594774     */ 
    595     private Way joinOuterWays(Collection<Way> multigonWays, Collection<Way> innerWays) { 
     775    private Way joinOuterWays(Collection<Way> outerWays) { 
    596776        ArrayList<Way> join = new ArrayList<Way>(); 
    597         for(Way w: multigonWays) { 
    598             // Skip inner ways 
    599             if(innerWays.contains(w)) { 
    600                 continue; 
    601             } 
     777        for(Way w: outerWays) { 
    602778 
    603779            if(w.getNodesCount() <= 2) { 
    604780                cmds.add(new DeleteCommand(w)); 
     
    9801156    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 
    9811157        setEnabled(selection != null && !selection.isEmpty()); 
    9821158    } 
    983 } 
     1159 
     1160} 
     1161 No newline at end of file 
  • test/unit/actions/JoinAreasActionTest.java

     
     1// License: GPL. For details, see LICENSE file. 
     2package actions; 
     3 
     4import org.junit.Assert; 
     5import org.junit.Test; 
     6import org.openstreetmap.josm.actions.JoinAreasAction; 
     7import org.openstreetmap.josm.data.coor.LatLon; 
     8import org.openstreetmap.josm.data.osm.Node; 
     9 
     10 
     11public class JoinAreasActionTest { 
     12 
     13    private Node makeNode(double lat, double lon) 
     14    { 
     15        Node node = new Node(new LatLon(lat, lon)); 
     16        return node; 
     17    } 
     18 
     19    @Test 
     20    public void testAngleIsClockwise() 
     21    { 
     22        Assert.assertTrue(JoinAreasAction.angleIsClockwise(makeNode(0,0), makeNode(1,1), makeNode(0,1))); 
     23        Assert.assertTrue(JoinAreasAction.angleIsClockwise(makeNode(1,1), makeNode(0,1), makeNode(0,0))); 
     24        Assert.assertTrue(!JoinAreasAction.angleIsClockwise(makeNode(1,1), makeNode(0,1), makeNode(1,0))); 
     25    } 
     26 
     27    @Test 
     28    public void testisToTheRightSideOfLine() 
     29    { 
     30        Assert.assertTrue(JoinAreasAction.isToTheRightSideOfLine(makeNode(0,0), makeNode(1,1), makeNode(0,1), makeNode(0, 0.5))); 
     31        Assert.assertTrue(!JoinAreasAction.isToTheRightSideOfLine(makeNode(0,0), makeNode(1,1), makeNode(0,1), makeNode(1, 0))); 
     32        Assert.assertTrue(!JoinAreasAction.isToTheRightSideOfLine(makeNode(1,1), makeNode(0,1), makeNode(1,0), makeNode(0,0))); 
     33        Assert.assertTrue(JoinAreasAction.isToTheRightSideOfLine(makeNode(1,1), makeNode(0,1), makeNode(1,0), makeNode(2, 0))); 
     34    } 
     35 
     36}