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

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