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

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

Fixes example 5 and 7

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

     
    1414import java.util.Collections; 
    1515import java.util.HashMap; 
    1616import java.util.HashSet; 
     17import java.util.LinkedHashMap; 
     18import java.util.LinkedHashSet; 
    1719import java.util.LinkedList; 
    1820import java.util.List; 
    1921import java.util.Set; 
     
    112114        if (ways == null || ways.isEmpty()) 
    113115            return null; 
    114116        ways.remove(null); // just in case -  remove all null ways from the collection 
    115         ways = new HashSet<Way>(ways); // remove duplicates 
    116117 
     118        // remove duplicates, preserving order 
     119        ways = new LinkedHashSet<Way>(ways); 
     120 
    117121        // try to build a new way which includes all the combined 
    118122        // ways 
    119123        // 
     
    473477        } 
    474478 
    475479        protected void prepare() { 
    476             Set<NodePair> undirectedEdges = new HashSet<NodePair>(); 
    477             successors = new HashMap<Node, List<NodePair>>(); 
    478             predecessors = new HashMap<Node, List<NodePair>>(); 
     480            Set<NodePair> undirectedEdges = new LinkedHashSet<NodePair>(); 
     481            successors = new LinkedHashMap<Node, List<NodePair>>(); 
     482            predecessors = new LinkedHashMap<Node, List<NodePair>>(); 
    479483 
    480484            for (NodePair pair: edges) { 
    481485                if (!undirectedEdges.contains(pair) && ! undirectedEdges.contains(pair.swap())) { 
     
    488492        } 
    489493 
    490494        public NodeGraph() { 
    491             edges = new HashSet<NodePair>(); 
     495            edges = new LinkedHashSet<NodePair>(); 
    492496        } 
    493497 
    494498        public void add(NodePair pair) { 
     
    513517        } 
    514518 
    515519        protected Set<Node> getTerminalNodes() { 
    516             Set<Node> ret = new HashSet<Node>(); 
     520            Set<Node> ret = new LinkedHashSet<Node>(); 
    517521            for (Node n: getNodes()) { 
    518522                if (isTerminalNode(n)) { 
    519523                    ret.add(n); 
     
    523527        } 
    524528 
    525529        protected Set<Node> getNodes(Stack<NodePair> pairs) { 
    526             HashSet<Node> nodes = new HashSet<Node>(); 
     530            HashSet<Node> nodes = new LinkedHashSet<Node>(); 
    527531            for (NodePair pair: pairs) { 
    528532                nodes.add(pair.getA()); 
    529533                nodes.add(pair.getB()); 
     
    543547        } 
    544548 
    545549        protected Set<Node> getNodes() { 
    546             Set<Node> nodes = new HashSet<Node>(); 
     550            Set<Node> nodes = new LinkedHashSet<Node>(); 
    547551            for (NodePair pair: edges) { 
    548552                nodes.add(pair.getA()); 
    549553                nodes.add(pair.getB()); 
  • 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; 
     
    1514import java.util.Collection; 
    1615import java.util.Collections; 
    1716import java.util.HashMap; 
     17import java.util.HashSet; 
    1818import java.util.LinkedList; 
    1919import java.util.List; 
    2020import java.util.Map; 
     
    3030import javax.swing.JPanel; 
    3131 
    3232import org.openstreetmap.josm.Main; 
     33import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult; 
    3334import org.openstreetmap.josm.command.AddCommand; 
    3435import org.openstreetmap.josm.command.ChangeCommand; 
    3536import org.openstreetmap.josm.command.Command; 
     
    5455    private LinkedList<Command> cmds = new LinkedList<Command>(); 
    5556    private int cmdsCount = 0; 
    5657 
     58 
     59    /** 
     60     * This helper class describes join ares action result. 
     61     * @author viesturs 
     62     * 
     63     */ 
     64    public static class JoinAreasResult{ 
     65 
     66        public Way outerWay; 
     67        public List<Way> innerWays; 
     68 
     69        public boolean mergeSuccessful; 
     70        public boolean hasChanges; 
     71        public boolean hasRelationProblems; 
     72    } 
     73 
    5774    // HelperClass 
    5875    // Saves a node and two positions where to insert the node into the ways 
    5976    private static class NodeToSegs implements Comparable<NodeToSegs> { 
     
    109126        } 
    110127    } 
    111128 
     129    //HelperClass 
     130    //saves a way and the "inside" side 
     131    // insideToTheLeft: if true left side is "in", false -right side is "in". Left and right are determined along the orientation of way. 
     132    private static class WayInPath{ 
     133        public final Way way; 
     134        public boolean insideToTheLeft; 
     135 
     136        public WayInPath(Way _way, boolean _insideLeft){ 
     137            this.way = _way; 
     138            this.insideToTheLeft = _insideLeft; 
     139        } 
     140 
     141        @Override 
     142        public int hashCode() { 
     143            return way.hashCode(); 
     144        } 
     145 
     146        @Override 
     147        public boolean equals(Object other) { 
     148            if (!(other instanceof WayInPath)) return false; 
     149            WayInPath otherMember = (WayInPath) other; 
     150            return otherMember.way.equals(this.way) && otherMember.insideToTheLeft == this.insideToTheLeft; 
     151        } 
     152    } 
     153 
    112154    // Adds the menu entry, Shortcuts, etc. 
    113155    public JoinAreasAction() { 
    114156        super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"), Shortcut.registerShortcut("tools:joinareas", tr("Tool: {0}", tr("Join overlapping Areas")), 
     
    163205            } 
    164206        } 
    165207 
    166         if(joinAreas(ways.getFirst(), ways.getLast())) { 
     208        if(checkForTagConflicts(ways.getFirst(), ways.getLast())) 
     209        { 
     210            //do nothing. 
     211        } 
     212 
     213        JoinAreasResult result = joinAreas(ways.getFirst(), ways.getLast()); 
     214 
     215        if (result.hasChanges) { 
    167216            Main.map.mapView.repaint(); 
    168217            DataSet ds = Main.main.getCurrentDataSet(); 
    169218            ds.fireSelectionChanged(); 
     
    178227     * @param Way Second way/area 
    179228     * @return boolean Whether to display the "no operation" message 
    180229     */ 
    181     private boolean joinAreas(Way a, Way b) { 
     230    private JoinAreasResult joinAreas(Way a, Way b) { 
     231 
     232        JoinAreasResult result = new JoinAreasResult(); 
     233        result.hasChanges = false; 
     234 
    182235        // Fix self-overlapping first or other errors 
    183236        boolean same = a.equals(b); 
    184         boolean hadChanges = false; 
    185237        if(!same) { 
    186238            int i = 0; 
    187             if(checkForTagConflicts(a, b)) return true; // User aborted, so don't warn again 
    188             if(joinAreas(a, a)) { 
     239 
     240            //join each area with itself, fixing self-crossings. 
     241            JoinAreasResult resultA = joinAreas(a, a); 
     242            JoinAreasResult resultB = joinAreas(b, b); 
     243 
     244            if (resultA.mergeSuccessful){ 
     245                a = resultA.outerWay; 
    189246                ++i; 
    190247            } 
    191             if(joinAreas(b, b)) { 
     248            if(resultB.mergeSuccessful) { 
     249                b = resultB.outerWay; 
    192250                ++i; 
    193251            } 
    194             hadChanges = i > 0; 
     252 
     253            result.hasChanges = i > 0; 
    195254            cmdsCount = i; 
    196255        } 
    197256 
    198         ArrayList<OsmPrimitive> nodes = addIntersections(a, b); 
    199         if(nodes.size() == 0) return hadChanges; 
     257        ArrayList<Node> nodes = addIntersections(a, b); 
     258 
     259        //no intersections, return. 
     260        if(nodes.size() == 0) return result; 
    200261        commitCommands(marktr("Added node on all intersections")); 
    201262 
    202263        // Remove ways from all relations so ways can be combined/split quietly 
     
    208269        // Don't warn now, because it will really look corrupted 
    209270        boolean warnAboutRelations = relations.size() > 0; 
    210271 
    211         Collection<Way> allWays = splitWaysOnNodes(a, b, nodes); 
     272        ArrayList<Way> allWays = splitWaysOnNodes(a, b, nodes); 
    212273 
    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); 
     274        // Find inner ways save them to a list 
     275        ArrayList<WayInPath> outerWays = findOuterWays(allWays); 
     276        ArrayList<Way> innerWays = findInnerWays(allWays, outerWays); 
    216277 
    217278        // Join outer ways 
    218         Way outerWay = joinOuterWays(allWays, innerWays); 
    219         if (outerWay == null) 
    220             return true; 
     279        Way outerWay = joinOuterWays(outerWays); 
    221280 
    222281        // Fix Multipolygons if there are any 
    223         Collection<Way> newInnerWays = fixMultipolygons(innerWays, outerWay, same); 
     282        List<Way> newInnerWays = fixMultipolygons(innerWays, outerWay, same); 
    224283 
    225284        // Delete the remaining inner ways 
    226285        if(innerWays != null && innerWays.size() > 0) { 
     
    234293        commitCommands(marktr("Fix relations")); 
    235294 
    236295        stripTags(newInnerWays); 
     296 
     297 
    237298        makeCommitsOneAction( 
    238299                same 
    239300                ? marktr("Joined self-overlapping area") 
     
    244305            JOptionPane.showMessageDialog(Main.parent, tr("Some of the ways were part of relations that have been modified. Please verify no errors have been introduced.")); 
    245306        } 
    246307 
    247         return true; 
     308        result.mergeSuccessful = true; 
     309        result.outerWay = outerWay; 
     310        result.innerWays = newInnerWays; 
     311 
     312        return result; 
    248313    } 
    249314 
    250315    /** 
     
    330395     * @param Way Second way 
    331396     * @return ArrayList<OsmPrimitive> List of new nodes 
    332397     */ 
    333     private ArrayList<OsmPrimitive> addIntersections(Way a, Way b) { 
     398    private ArrayList<Node> addIntersections(Way a, Way b) { 
    334399        boolean same = a.equals(b); 
    335400        int nodesSizeA = a.getNodesCount(); 
    336401        int nodesSizeB = b.getNodesCount(); 
    337402 
    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>(); 
     403        ArrayList<Node> nodes = new ArrayList<Node>(); 
    341404        ArrayList<NodeToSegs> nodesA = new ArrayList<NodeToSegs>(); 
    342405        ArrayList<NodeToSegs> nodesB = new ArrayList<NodeToSegs>(); 
    343406 
     
    488551    } 
    489552 
    490553    /** 
    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. 
     554     * This is a method splits ways into smaller parts, using the prepared nodes list as split points. 
     555     * Uses  SplitWayAction.splitWay for the heavy lifting. 
     556     * @return list of split ways (or original ways if no splitting is done). 
    493557     */ 
    494     private Collection<Way> splitWaysOnNodes(Way a, Way b, Collection<OsmPrimitive> nodes) { 
    495         ArrayList<Way> ways = new ArrayList<Way>(); 
     558    private ArrayList<Way> splitWaysOnNodes(Way a, Way b, Collection<Node> nodes) { 
     559 
     560        ArrayList<Way> result = new ArrayList<Way>(); 
     561        List<Way> ways = new ArrayList<Way>(); 
    496562        ways.add(a); 
    497         if(!a.equals(b)) { 
    498             ways.add(b); 
     563        ways.add(b); 
     564 
     565        for (Way way: ways){ 
     566            List<List<Node>> chunks = buildNodeChunks(way, nodes); 
     567            SplitWayResult split = SplitWayAction.splitWay(Main.map.mapView.getEditLayer(), way, chunks, Collections.<OsmPrimitive>emptyList()); 
     568 
     569            //execute the command, we need the results 
     570            Main.main.undoRedo.add(split.getCommand()); 
     571            cmdsCount ++; 
     572 
     573            result.add(split.getOriginalWay()); 
     574            result.addAll(split.getNewWays()); 
    499575        } 
    500576 
    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); 
     577        return result; 
    511578    } 
    512579 
     580 
    513581    /** 
    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 
     582     * Simple chunking version. Does not care about circular ways and result being proper, we will glue it all back together later on. 
     583     * @param way the way to chunk 
     584     * @param splitNodes the places where to cut. 
     585     * @return list of node segments to produce. 
    517586     */ 
    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); 
     587    private List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) 
     588    { 
     589        List<List<Node>> result = new ArrayList<List<Node>>(); 
     590        List<Node> curList = new ArrayList<Node>(); 
     591 
     592        for(Node node: way.getNodes()){ 
     593            curList.add(node); 
     594            if (curList.size() > 1 && splitNodes.contains(node)){ 
     595                result.add(curList); 
     596                curList = new ArrayList<Node>(); 
     597                curList.add(node); 
    523598            } 
    524599        } 
     600 
     601        if (curList.size() > 1) 
     602        { 
     603            result.add(curList); 
     604        } 
     605 
    525606        return result; 
    526607    } 
    527608 
     609 
    528610    /** 
    529611     * Returns all nodes for given ways 
    530612     * @param Collection<Way> The list of ways which nodes are to be returned 
     
    538620        return allNodes; 
    539621    } 
    540622 
     623 
    541624    /** 
    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 
     625     * Gets all inner ways given all ways and outer ways. 
     626     * @param multigonWays 
     627     * @param outerWays 
     628     * @return list of inner ways. 
     629     */ 
     630    private ArrayList<Way> findInnerWays(Collection<Way> multigonWays, Collection<WayInPath> outerWays) { 
     631        ArrayList<Way> innerWays = new ArrayList<Way>(); 
     632        Set<Way> outerSet = new HashSet<Way>(); 
     633 
     634        for(WayInPath w: outerWays) { 
     635            outerSet.add(w.way); 
     636        } 
     637 
     638        for(Way way: multigonWays) { 
     639            if (!outerSet.contains(way)) { 
     640                innerWays.add(way); 
     641            } 
     642        } 
     643 
     644        return innerWays; 
     645    } 
     646 
     647 
     648    /** 
     649     * Finds all ways for a given list of Ways that form the outer hull. 
     650     * This works by starting with one node and traversing the multigon clockwise, always picking the leftmost path. 
     651     * Prerequisites - the ways must not intersect and have common end nodes where they meet. 
    545652     * @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 
     653     * @return Collection<Way> A list of ways that form the outer boundary of the multigon. 
    548654     */ 
    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())); 
     655    public static ArrayList<WayInPath> findOuterWays(Collection<Way> multigonWays) { 
     656 
     657        //find the node with minimum lat - it's guaranteed to be outer. (What about the south pole?) 
     658        Way bestWay = null; 
     659        Node topNode = null; 
     660        int topIndex = 0; 
     661        double minLat = Double.POSITIVE_INFINITY; 
     662 
     663        for(Way way: multigonWays) { 
     664            for (int pos = 0; pos < way.getNodesCount(); pos ++){ 
     665                Node node = way.getNode(pos); 
     666 
     667                if (node.getCoor().lat() < minLat){ 
     668                    minLat = node.getCoor().lat(); 
     669                    bestWay = way; 
     670                    topNode = node; 
     671                    topIndex = pos; 
     672                } 
    555673            } 
     674        } 
    556675 
    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); 
     676        //get two final nodes from best way to mark as starting point and orientation. 
     677        Node headNode = null; 
     678        Node prevNode = null; 
     679 
     680        if (topNode.equals(bestWay.firstNode()) || topNode.equals(bestWay.lastNode())) 
     681        { 
     682            //node is in split point 
     683            headNode = topNode; 
     684            //make a fake node that is downwards from head node (smaller latitude). It will be a division point between paths. 
     685            prevNode = new Node(new LatLon(headNode.getCoor().lat() - 1000, headNode.getCoor().lon())); 
     686        } 
     687        else 
     688        { 
     689            //node is inside way - pick the clockwise going end. 
     690            Node prev = bestWay.getNode(topIndex - 1); 
     691            Node next = bestWay.getNode(topIndex + 1); 
     692 
     693            if (angleIsClockwise(prev, topNode, next)){ 
     694                headNode = bestWay.lastNode(); 
     695                prevNode = bestWay.getNode(bestWay.getNodesCount() - 2); 
     696            } 
     697            else 
     698            { 
     699                headNode = bestWay.firstNode(); 
     700                prevNode = bestWay.getNode(1); 
     701            } 
     702        } 
     703 
     704        Set<Way> outerWays = new HashSet<Way>(); 
     705        ArrayList<WayInPath> result = new ArrayList<WayInPath>(); 
     706 
     707        //iterate till full circle is reached 
     708        while (true){ 
     709 
     710            bestWay = null; 
     711            Node bestWayNextNode = null; 
     712            boolean bestWayReverse = false; 
     713 
     714            for (Way way: multigonWays) 
     715            { 
     716                boolean wayReverse; 
     717                Node nextNode; 
     718 
     719                if (way.firstNode().equals(headNode)){ 
     720                    //start adjacent to headNode 
     721                    nextNode = way.getNode(1); 
     722                    wayReverse = false; 
     723 
     724                    if (nextNode.equals(prevNode)) 
     725                    { 
     726                        //this is the path we came from - ignore it. 
     727                    } 
     728                    else if (bestWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) 
     729                    { 
     730                        //the new way is better 
     731                        bestWay = way; 
     732                        bestWayReverse = wayReverse; 
     733                        bestWayNextNode = nextNode; 
     734                    } 
    560735                } 
     736 
     737                if (way.lastNode().equals(headNode)) 
     738                { 
     739                    //end adjacent to headNode 
     740                    nextNode = way.getNode(way.getNodesCount() - 2); 
     741                    wayReverse = true; 
     742 
     743                    if (nextNode.equals(prevNode)) 
     744                    { 
     745                        //this is the path we came from - ignore it. 
     746                    } 
     747                    else if (bestWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) 
     748                    { 
     749                        //the new way is better 
     750                        bestWay = way; 
     751                        bestWayReverse = wayReverse; 
     752                        bestWayNextNode = nextNode; 
     753                    } 
     754                } 
    561755            } 
     756 
     757            if (bestWay == null) 
     758                //this should not happen. Internal error here. 
     759                return null; 
     760            else if (outerWays.contains(bestWay)){ 
     761                //full circle reached, terminate. 
     762                break; 
     763            } 
     764            else 
     765            { 
     766                //add to outer ways, repeat. 
     767                outerWays.add(bestWay); 
     768                result.add(new WayInPath(bestWay, bestWayReverse)); 
     769                headNode = bestWayReverse ? bestWay.firstNode() : bestWay.lastNode(); 
     770                prevNode = bestWayReverse ? bestWay.getNode(1) : bestWay.getNode(bestWay.getNodesCount() - 2); 
     771            } 
    562772        } 
    563773 
    564         return innerWays; 
     774        return result; 
    565775    } 
    566776 
    567     // Polygon only supports int coordinates, so convert them 
    568     private int latlonToXY(double val) { 
    569         return (int)Math.round(val*1000000); 
     777    /** 
     778     * Tests if given point is to the right side of path consisting of 3 points. 
     779     * @param lineP1 first point in path 
     780     * @param lineP2 second point in path 
     781     * @param lineP3 third point in path 
     782     * @param testPoint 
     783     * @return true if to the right side, false otherwise 
     784     */ 
     785    public static boolean isToTheRightSideOfLine(Node lineP1, Node lineP2, Node lineP3, Node testPoint) 
     786    { 
     787        boolean pathBendToRight = angleIsClockwise(lineP1, lineP2, lineP3); 
     788        boolean rightOfSeg1 = angleIsClockwise(lineP1, lineP2, testPoint); 
     789        boolean rightOfSeg2 = angleIsClockwise(lineP2, lineP3, testPoint); 
     790 
     791        if (pathBendToRight) 
     792            return rightOfSeg1 && rightOfSeg2; 
     793        else 
     794            return !(!rightOfSeg1 && !rightOfSeg2); 
    570795    } 
    571796 
    572797    /** 
    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 
     798     * This method tests if secondNode is clockwise to first node. 
     799     * @param commonNode starting point for both vectors 
     800     * @param firstNode first vector end node 
     801     * @param secondNode second vector end node 
     802     * @return true if first vector is clockwise before second vector. 
    577803     */ 
    578     private void getWaysByNode(Collection<Way> innerWays, Collection<Way> w, Node n) { 
    579         for(Way way : w) { 
    580             if(!(way).containsNode(n)) { 
     804    public static boolean angleIsClockwise(Node commonNode, Node firstNode, Node secondNode) 
     805    { 
     806        double dla1 = (firstNode.getCoor().lat() - commonNode.getCoor().lat()); 
     807        double dla2 = (secondNode.getCoor().lat() - commonNode.getCoor().lat()); 
     808        double dlo1 = (firstNode.getCoor().lon() - commonNode.getCoor().lon()); 
     809        double dlo2 = (secondNode.getCoor().lon() - commonNode.getCoor().lon()); 
     810 
     811        return dla1 * dlo2 - dlo1 * dla2 > 0; 
     812    } 
     813 
     814 
     815    /** 
     816     * Tests if point is inside a polygon. The polygon can be self-intersecting. In such case the contains function works in xor-like manner. 
     817     * @param polygonNodes list of nodes from polygon path. 
     818     * @param point the point to test 
     819     * @return true if the point is inside polygon. 
     820     * FIXME: this should probably be moved to tools.. 
     821     */ 
     822    public static boolean nodeInsidePolygon(ArrayList<Node> polygonNodes, Node point) 
     823    { 
     824        if (polygonNodes.size() < 3) 
     825            return false; 
     826 
     827        boolean inside = false; 
     828        Node p1, p2; 
     829 
     830        //iterate each side of the polygon, start with the last segment 
     831        Node oldPoint = polygonNodes.get(polygonNodes.size() - 1); 
     832 
     833        for(Node newPoint: polygonNodes) 
     834        { 
     835            //skip duplicate points 
     836            if (newPoint.equals(oldPoint)) { 
    581837                continue; 
    582838            } 
    583             if(!innerWays.contains(way)) { 
    584                 innerWays.add(way); // Will need this later for multigons 
     839 
     840            //order points so p1.lat <= p2.lat; 
     841            if (newPoint.getCoor().lat() > oldPoint.getCoor().lat()) 
     842            { 
     843                p1 = oldPoint; 
     844                p2 = newPoint; 
    585845            } 
     846            else 
     847            { 
     848                p1 = newPoint; 
     849                p2 = oldPoint; 
     850            } 
     851 
     852            //test if the line is crossed and if so invert the inside flag. 
     853            if ((newPoint.getCoor().lat() < point.getCoor().lat()) == (point.getCoor().lat() <= oldPoint.getCoor().lat()) 
     854                    && (point.getCoor().lon() - p1.getCoor().lon()) * (p2.getCoor().lat() - p1.getCoor().lat()) 
     855                    < (p2.getCoor().lon() - p1.getCoor().lon()) * (point.getCoor().lat() - p1.getCoor().lat())) 
     856            { 
     857                inside = !inside; 
     858            } 
     859 
     860            oldPoint = newPoint; 
    586861        } 
     862 
     863        return inside; 
    587864    } 
    588865 
     866 
     867 
     868 
    589869    /** 
    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 
     870     * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway. 
     871     * @param Collection<Way> The list of outer ways that belong to that multigon. 
    593872     * @return Way The newly created outer way 
    594873     */ 
    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             } 
     874    private Way joinOuterWays(ArrayList<WayInPath> outerWays) { 
    602875 
    603             if(w.getNodesCount() <= 2) { 
    604                 cmds.add(new DeleteCommand(w)); 
    605             } else { 
    606                 join.add(w); 
     876        //leave original orientation, if all paths are reverse. 
     877        boolean allReverse = true; 
     878        for(WayInPath way: outerWays){ 
     879            allReverse &= way.insideToTheLeft; 
     880        } 
     881 
     882        if (allReverse){ 
     883            for(WayInPath way: outerWays){ 
     884                way.insideToTheLeft = !way.insideToTheLeft; 
    607885            } 
    608886        } 
    609887 
     888 
    610889        commitCommands(marktr("Join Areas: Remove Short Ways")); 
    611         Way joinedWay = joinWays(join); 
     890        Way joinedWay = joinOrientedWays(outerWays); 
    612891        if (joinedWay != null) 
    613892            return closeWay(joinedWay); 
    614893        else 
     
    632911    } 
    633912 
    634913    /** 
     914     * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath) 
     915     * @param ArrayList<Way> The list of ways to join and reverse 
     916     * @return Way The newly created way 
     917     */ 
     918    private Way joinOrientedWays(ArrayList<WayInPath> ways) { 
     919        if(ways.size() < 2) return ways.get(0).way; 
     920 
     921        // This will turn ways so all of them point in the same direction and CombineAction won't bug 
     922        // the user about this. 
     923 
     924        List<Way> actionWays = new ArrayList<Way>(ways.size()); 
     925 
     926        for(WayInPath way : ways) { 
     927            actionWays.add(way.way); 
     928 
     929            if (way.insideToTheLeft) 
     930            { 
     931                Main.main.getCurrentDataSet().setSelected(way.way); 
     932                new ReverseWayAction().actionPerformed(null); 
     933                cmdsCount++; 
     934            } 
     935        } 
     936 
     937        Way result = new CombineWayAction().combineWays(actionWays); 
     938 
     939        if(result != null) { 
     940            cmdsCount++; 
     941        } 
     942        return result; 
     943    } 
     944 
     945    /** 
    635946     * Joins a list of ways (using CombineWayAction and ReverseWayAction if necessary to quiet the former) 
    636947     * @param ArrayList<Way> The list of ways to join 
    637948     * @return Way The newly created way 
     
    661972        return a; 
    662973    } 
    663974 
     975 
    664976    /** 
    665977     * Finds all ways that may be part of a multipolygon relation and removes them from the given list. 
    666978     * It will automatically combine "good" ways 
     
    9801292    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 
    9811293        setEnabled(selection != null && !selection.isEmpty()); 
    9821294    } 
    983 } 
     1295 
     1296} 
     1297 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}