Ticket #5179: osm-join-areas.patch

File osm-join-areas.patch, 14.2 KB (added by extropy, 16 years 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}