Ticket #5179: osm-join-areas-3.patch

File osm-join-areas-3.patch, 19.4 KB (added by extropy, 14 years ago)

Improved patch.

  • 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;
     
    3029import javax.swing.JPanel;
    3130
    3231import org.openstreetmap.josm.Main;
     32import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult;
    3333import org.openstreetmap.josm.command.AddCommand;
    3434import org.openstreetmap.josm.command.ChangeCommand;
    3535import org.openstreetmap.josm.command.Command;
     
    185185        if(!same) {
    186186            int i = 0;
    187187            if(checkForTagConflicts(a, b)) return true; // User aborted, so don't warn again
     188
     189            //join each area with itself, fixing self-crossings.
    188190            if(joinAreas(a, a)) {
    189191                ++i;
    190192            }
     
    195197            cmdsCount = i;
    196198        }
    197199
    198         ArrayList<OsmPrimitive> nodes = addIntersections(a, b);
     200        ArrayList<Node> nodes = addIntersections(a, b);
    199201        if(nodes.size() == 0) return hadChanges;
    200202        commitCommands(marktr("Added node on all intersections"));
    201203
     
    208210        // Don't warn now, because it will really look corrupted
    209211        boolean warnAboutRelations = relations.size() > 0;
    210212
    211         Collection<Way> allWays = splitWaysOnNodes(a, b, nodes);
     213        ArrayList<Way> allWays = splitWaysOnNodes(a, b, nodes);
    212214
    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);
     215        // Find inner ways save them to a list
     216        ArrayList<Way> outerWays = findOuterWays(allWays);
     217        ArrayList<Way> innerWays = findInnerWays(allWays, outerWays);
    216218
    217219        // Join outer ways
    218         Way outerWay = joinOuterWays(allWays, innerWays);
     220        Way outerWay = joinOuterWays(outerWays);
    219221        if (outerWay == null)
    220222            return true;
    221223
     
    330332     * @param Way Second way
    331333     * @return ArrayList<OsmPrimitive> List of new nodes
    332334     */
    333     private ArrayList<OsmPrimitive> addIntersections(Way a, Way b) {
     335    private ArrayList<Node> addIntersections(Way a, Way b) {
    334336        boolean same = a.equals(b);
    335337        int nodesSizeA = a.getNodesCount();
    336338        int nodesSizeB = b.getNodesCount();
    337339
    338         // We use OsmPrimitive here instead of Node because we later need to split a way at these nodes.
    339         // With OsmPrimitve we can simply add the way and don't have to loop over the nodes
    340         ArrayList<OsmPrimitive> nodes = new ArrayList<OsmPrimitive>();
     340        ArrayList<Node> nodes = new ArrayList<Node>();
    341341        ArrayList<NodeToSegs> nodesA = new ArrayList<NodeToSegs>();
    342342        ArrayList<NodeToSegs> nodesB = new ArrayList<NodeToSegs>();
    343343
     
    488488    }
    489489
    490490    /**
    491      * This is a hacky implementation to make use of the splitWayAction code and
    492      * should be improved. SplitWayAction needs to expose its splitWay function though.
     491     * This is a method splits ways into smaller parts, using the prepared nodes list as split points.
     492     * Uses  SplitWayAction.splitWay for the heavy lifting.
     493     * @return list of split ways (or original ways if no splitting is done).
    493494     */
    494     private Collection<Way> splitWaysOnNodes(Way a, Way b, Collection<OsmPrimitive> nodes) {
    495         ArrayList<Way> ways = new ArrayList<Way>();
     495    private ArrayList<Way> splitWaysOnNodes(Way a, Way b, Collection<Node> nodes) {
     496
     497        ArrayList<Way> result = new ArrayList<Way>();
     498        List<Way> ways = new ArrayList<Way>();
    496499        ways.add(a);
    497         if(!a.equals(b)) {
    498             ways.add(b);
     500        ways.add(b);
     501
     502        for (Way way: ways){
     503            List<List<Node>> chunks = buildNodeChunks(way, nodes);
     504            SplitWayResult split = SplitWayAction.splitWay(Main.map.mapView.getEditLayer(), way, chunks, Collections.<OsmPrimitive>emptyList());
     505
     506            //execute the command, we need the results
     507            Main.main.undoRedo.add(split.getCommand());
     508            cmdsCount ++;
     509
     510            result.add(split.getOriginalWay());
     511            result.addAll(split.getNewWays());
    499512        }
    500513
    501         List<OsmPrimitive> affected = new ArrayList<OsmPrimitive>();
    502         for (Way way : ways) {
    503             nodes.add(way);
    504             Main.main.getCurrentDataSet().setSelected(nodes);
    505             nodes.remove(way);
    506             new SplitWayAction().actionPerformed(null);
    507             cmdsCount++;
    508             affected.addAll(Main.main.getCurrentDataSet().getSelectedWays());
    509         }
    510         return osmprim2way(affected);
     514        return result;
    511515    }
    512516
     517
    513518    /**
    514      * Converts a list of OsmPrimitives to a list of Ways
    515      * @param Collection<OsmPrimitive> The OsmPrimitives list that's needed as a list of Ways
    516      * @return Collection<Way> The list as list of Ways
     519     * Simple chunking version. Does not care about circular ways and result being proper, we will glue it all back together later on.
     520     * @param way the way to chunk
     521     * @param splitNodes the places where to cut.
     522     * @return list of node segments to produce.
    517523     */
    518     static private Collection<Way> osmprim2way(Collection<OsmPrimitive> ways) {
    519         Collection<Way> result = new ArrayList<Way>();
    520         for(OsmPrimitive w: ways) {
    521             if(w instanceof Way) {
    522                 result.add((Way) w);
     524    private List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes)
     525    {
     526        List<List<Node>> result = new ArrayList<List<Node>>();
     527        List<Node> curList = new ArrayList<Node>();
     528
     529        for(Node node: way.getNodes()){
     530            curList.add(node);
     531            if (curList.size() > 1 && splitNodes.contains(node)){
     532                result.add(curList);
     533                curList = new ArrayList<Node>();
     534                curList.add(node);
    523535            }
    524536        }
     537
     538        if (curList.size() > 1)
     539        {
     540            result.add(curList);
     541        }
     542
    525543        return result;
    526544    }
    527545
     546
    528547    /**
    529548     * Returns all nodes for given ways
    530549     * @param Collection<Way> The list of ways which nodes are to be returned
     
    538557        return allNodes;
    539558    }
    540559
     560
    541561    /**
    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
     562     * Gets all inner ways given all ways and outer ways.
     563     * @param multigonWays
     564     * @param outerWays
     565     * @return list of inner ways.
     566     */
     567    private ArrayList<Way> findInnerWays(Collection<Way> multigonWays, Collection<Way> outerWays) {
     568        ArrayList<Way> innerWays = new ArrayList<Way>();
     569        for(Way way: multigonWays) {
     570            if (!outerWays.contains(way)) {
     571                innerWays.add(way);
     572            }
     573        }
     574
     575        return innerWays;
     576    }
     577
     578
     579    /**
     580     * Finds all ways for a given list of Ways that form the outer hull.
     581     * This works by starting with one node and traversing the multigon clockwise, always picking the leftmost path.
     582     * Prerequisites - the ways must not intersect and have common end nodes where they meet.
    545583     * @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
     584     * @return Collection<Way> A list of ways that form the outer boundary of the multigon.
    548585     */
    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()));
     586    public static ArrayList<Way> findOuterWays(Collection<Way> multigonWays) {
     587
     588        //find the node with minimum lat - it's guaranteed to be outer. (What about the south pole?)
     589        Way bestWay = null;
     590        Node topNode = null;
     591        int topIndex = 0;
     592        double minLat = Double.POSITIVE_INFINITY;
     593
     594        for(Way way: multigonWays) {
     595            for (int pos = 0; pos < way.getNodesCount(); pos ++){
     596                Node node = way.getNode(pos);
     597
     598                if (node.getCoor().lat() < minLat){
     599                    minLat = node.getCoor().lat();
     600                    bestWay = way;
     601                    topNode = node;
     602                    topIndex = pos;
     603                }
    555604            }
     605        }
    556606
    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);
     607        //get two final nodes from best way to mark as starting point and orientation.
     608        Node headNode = null;
     609        Node prevNode = null;
     610
     611        if (topNode.equals(bestWay.firstNode()) || topNode.equals(bestWay.lastNode()))
     612        {
     613            //node is in split point
     614            headNode = topNode;
     615            //make a fake node that is downwards from head node (smaller latitude). It will be a division point between paths.
     616            prevNode = new Node(new LatLon(headNode.getCoor().lat() - 1000, headNode.getCoor().lon()));
     617        }
     618        else
     619        {
     620            //node is inside way - pick the clockwise going end.
     621            Node prev = bestWay.getNode(topIndex - 1);
     622            Node next = bestWay.getNode(topIndex + 1);
     623
     624            if (angleIsClockwise(prev, topNode, next)){
     625                headNode = bestWay.lastNode();
     626                prevNode = bestWay.getNode(bestWay.getNodesCount() - 2);
     627            }
     628            else
     629            {
     630                headNode = bestWay.firstNode();
     631                prevNode = bestWay.getNode(1);
     632            }
     633        }
     634
     635        ArrayList<Way> outerWays = new ArrayList<Way>();
     636
     637        //iterate till full circle is reached
     638        while (true){
     639
     640            bestWay = null;
     641            Node bestWayNextNode = null;
     642            boolean bestWayReverse = false;
     643
     644            for (Way way: multigonWays)
     645            {
     646                boolean wayReverse;
     647                Node nextNode;
     648
     649                if (way.firstNode().equals(headNode)){
     650                    nextNode = way.getNode(1);
     651                    wayReverse = false;
    560652                }
     653                else if (way.lastNode().equals(headNode))
     654                {
     655                    nextNode = way.getNode(way.getNodesCount() - 2);
     656                    wayReverse = true;
     657                }
     658                else
     659                {
     660                    //this way not adjacent to headNode
     661                    continue;
     662                }
     663
     664                if (nextNode.equals(prevNode))
     665                {
     666                    //this is the path we came from - ignore it.
     667                }
     668                else if (bestWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode))
     669                {
     670                    //the new way is better
     671                    bestWay = way;
     672                    bestWayReverse = wayReverse;
     673                    bestWayNextNode = nextNode;
     674                }
    561675            }
     676
     677            if (bestWay == null)
     678                //this should not happen. Internal error here.
     679                return null;
     680            else if (outerWays.contains(bestWay)){
     681                //full circle reached, terminate.
     682                break;
     683            }
     684            else
     685            {
     686                //add to outer ways, repeat.
     687                outerWays.add(bestWay);
     688                headNode = bestWayReverse ? bestWay.firstNode() : bestWay.lastNode();
     689                prevNode = bestWayReverse ? bestWay.getNode(1) : bestWay.getNode(bestWay.getNodesCount() - 2);
     690            }
    562691        }
    563692
    564         return innerWays;
     693        return outerWays;
    565694    }
    566695
    567     // Polygon only supports int coordinates, so convert them
    568     private int latlonToXY(double val) {
    569         return (int)Math.round(val*1000000);
     696    /**
     697     * Tests if given point is to the right side of path consisting of 3 points.
     698     * @param lineP1 first point in path
     699     * @param lineP2 second point in path
     700     * @param lineP3 third point in path
     701     * @param testPoint
     702     * @return true if to the right side, false otherwise
     703     */
     704    public static boolean isToTheRightSideOfLine(Node lineP1, Node lineP2, Node lineP3, Node testPoint)
     705    {
     706        boolean pathBendToRight = angleIsClockwise(lineP1, lineP2, lineP3);
     707        boolean rightOfSeg1 = angleIsClockwise(lineP1, lineP2, testPoint);
     708        boolean rightOfSeg2 = angleIsClockwise(lineP2, lineP3, testPoint);
     709
     710        if (pathBendToRight)
     711            return rightOfSeg1 && rightOfSeg2;
     712        else
     713            return !(!rightOfSeg1 && !rightOfSeg2);
    570714    }
    571715
    572716    /**
    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
     717     * This method tests if secondNode is clockwise to first node.
     718     * @param commonNode starting point for both vectors
     719     * @param firstNode first vector end node
     720     * @param secondNode second vector end node
     721     * @return true if first vector is clockwise before second vector.
    577722     */
    578     private void getWaysByNode(Collection<Way> innerWays, Collection<Way> w, Node n) {
    579         for(Way way : w) {
    580             if(!(way).containsNode(n)) {
     723    public static boolean angleIsClockwise(Node commonNode, Node firstNode, Node secondNode)
     724    {
     725        double dla1 = (firstNode.getCoor().lat() - commonNode.getCoor().lat());
     726        double dla2 = (secondNode.getCoor().lat() - commonNode.getCoor().lat());
     727        double dlo1 = (firstNode.getCoor().lon() - commonNode.getCoor().lon());
     728        double dlo2 = (secondNode.getCoor().lon() - commonNode.getCoor().lon());
     729
     730        return dla1 * dlo2 - dlo1 * dla2 > 0;
     731    }
     732
     733
     734    /**
     735     * Tests if point is inside a polygon. The polygon can be self-intersecting. In such case the contains function works in xor-like manner.
     736     * @param polygonNodes list of nodes from polygon path.
     737     * @param point the point to test
     738     * @return true if the point is inside polygon.
     739     * FIXME: this should probably be moved to tools..
     740     */
     741    public static boolean nodeInsidePolygon(ArrayList<Node> polygonNodes, Node point)
     742    {
     743        if (polygonNodes.size() < 3)
     744            return false;
     745
     746        boolean inside = false;
     747        Node p1, p2;
     748
     749        //iterate each side of the polygon, start with the last segment
     750        Node oldPoint = polygonNodes.get(polygonNodes.size() - 1);
     751
     752        for(Node newPoint: polygonNodes)
     753        {
     754            //skip duplicate points
     755            if (newPoint.equals(oldPoint)) {
    581756                continue;
    582757            }
    583             if(!innerWays.contains(way)) {
    584                 innerWays.add(way); // Will need this later for multigons
     758
     759            //order points so p1.lat <= p2.lat;
     760            if (newPoint.getCoor().lat() > oldPoint.getCoor().lat())
     761            {
     762                p1 = oldPoint;
     763                p2 = newPoint;
    585764            }
     765            else
     766            {
     767                p1 = newPoint;
     768                p2 = oldPoint;
     769            }
     770
     771            //test if the line is crossed and if so invert the inside flag.
     772            if ((newPoint.getCoor().lat() < point.getCoor().lat()) == (point.getCoor().lat() <= oldPoint.getCoor().lat())
     773                    && (point.getCoor().lon() - p1.getCoor().lon()) * (p2.getCoor().lat() - p1.getCoor().lat())
     774                    < (p2.getCoor().lon() - p1.getCoor().lon()) * (point.getCoor().lat() - p1.getCoor().lat()))
     775            {
     776                inside = !inside;
     777            }
     778
     779            oldPoint = newPoint;
    586780        }
     781
     782        return inside;
    587783    }
    588784
     785
     786
     787
    589788    /**
    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
     789     * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway.
     790     * @param Collection<Way> The list of outer ways that belong to that multigon.
    593791     * @return Way The newly created outer way
    594792     */
    595     private Way joinOuterWays(Collection<Way> multigonWays, Collection<Way> innerWays) {
    596         ArrayList<Way> join = new ArrayList<Way>();
    597         for(Way w: multigonWays) {
    598             // Skip inner ways
    599             if(innerWays.contains(w)) {
    600                 continue;
    601             }
     793    private Way joinOuterWays(ArrayList<Way> outerWays) {
    602794
    603             if(w.getNodesCount() <= 2) {
    604                 cmds.add(new DeleteCommand(w));
    605             } else {
    606                 join.add(w);
    607             }
    608         }
    609 
    610795        commitCommands(marktr("Join Areas: Remove Short Ways"));
    611         Way joinedWay = joinWays(join);
     796        Way joinedWay = joinWays(outerWays);
    612797        if (joinedWay != null)
    613798            return closeWay(joinedWay);
    614799        else
     
    9801165    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
    9811166        setEnabled(selection != null && !selection.isEmpty());
    9821167    }
    983 }
     1168
     1169}
     1170 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}