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

File osm-join-areas-6.2.patch, 74.7 KB (added by extropy, 15 years ago)

New shiny implementation, handles all cases up to date.

  • .classpath

     
    1 <?xml version="1.0" encoding="UTF-8"?>
    2 <classpath>
    3         <classpathentry kind="src" path="src"/>
    4         <classpathentry kind="src" path="test/unit"/>
    5         <classpathentry kind="src" path="test/functional"/>
    6         <classpathentry excluding="build/|data_nodist/|dist/|doc/|lib/|macosx/|src/|test/|test/build/|test/functional/|test/performance/|test/unit/|tools/|utils/" kind="src" path=""/>
    7         <classpathentry kind="src" path="test/performance"/>
    8         <classpathentry kind="lib" path="lib/metadata-extractor-2.3.1-nosun.jar"/>
    9         <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
    10         <classpathentry kind="lib" path="test/lib/fest/debug-1.0.jar"/>
    11         <classpathentry kind="lib" path="test/lib/fest/fest-assert-1.0.jar"/>
    12         <classpathentry kind="lib" path="test/lib/fest/fest-reflect-1.1.jar"/>
    13         <classpathentry kind="lib" path="test/lib/fest/fest-swing-1.1.jar"/>
    14         <classpathentry kind="lib" path="test/lib/fest/fest-util-1.0.jar"/>
    15         <classpathentry kind="lib" path="test/lib/fest/jcip-annotations-1.0.jar"/>
    16         <classpathentry kind="lib" path="test/lib/fest/MRJToolkitStubs-1.0.jar"/>
    17         <classpathentry kind="lib" path="test/lib/jfcunit.jar"/>
    18         <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JDK 6"/>
    19         <classpathentry exported="true" kind="con" path="GROOVY_SUPPORT"/>
    20         <classpathentry kind="lib" path="lib/signpost-core-1.2.1.1.jar"/>
    21         <classpathentry kind="output" path="bin"/>
    22 </classpath>
     1<?xml version="1.0" encoding="UTF-8"?>
     2<classpath>
     3        <classpathentry kind="src" path="src"/>
     4        <classpathentry kind="src" path="test/unit"/>
     5        <classpathentry kind="src" path="test/functional"/>
     6        <classpathentry excluding="build/|data_nodist/|dist/|doc/|lib/|macosx/|src/|test/|test/build/|test/functional/|test/performance/|test/unit/|tools/|utils/" kind="src" path=""/>
     7        <classpathentry kind="src" path="test/performance"/>
     8        <classpathentry kind="lib" path="lib/metadata-extractor-2.3.1-nosun.jar"/>
     9        <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
     10        <classpathentry kind="lib" path="test/lib/fest/debug-1.0.jar"/>
     11        <classpathentry kind="lib" path="test/lib/fest/fest-assert-1.0.jar"/>
     12        <classpathentry kind="lib" path="test/lib/fest/fest-reflect-1.1.jar"/>
     13        <classpathentry kind="lib" path="test/lib/fest/fest-swing-1.1.jar"/>
     14        <classpathentry kind="lib" path="test/lib/fest/fest-util-1.0.jar"/>
     15        <classpathentry kind="lib" path="test/lib/fest/jcip-annotations-1.0.jar"/>
     16        <classpathentry kind="lib" path="test/lib/fest/MRJToolkitStubs-1.0.jar"/>
     17        <classpathentry kind="lib" path="test/lib/jfcunit.jar"/>
     18        <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
     19        <classpathentry exported="true" kind="con" path="GROOVY_SUPPORT"/>
     20        <classpathentry kind="lib" path="lib/signpost-core-1.2.1.1.jar"/>
     21        <classpathentry kind="output" path="bin"/>
     22</classpath>
  • src/org/openstreetmap/josm/actions/CombineWayAction.java

     
    9999     *
    100100     * @return the set of referring relations
    101101     */
    102     protected Set<Relation> getParentRelations(Collection<Way> ways) {
     102    public static Set<Relation> getParentRelations(Collection<Way> ways) {
    103103        HashSet<Relation> ret = new HashSet<Relation>();
    104104        for (Way w: ways) {
    105105            ret.addAll(OsmPrimitive.getFilteredList(w.getReferrers(), Relation.class));
  • src/org/openstreetmap/josm/actions/JoinAreasAction.java

     
    11// License: GPL. Copyright 2007 by Immanuel Scholz and others
    22package org.openstreetmap.josm.actions;
    33
     4import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.combineTigerTags;
     5import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.completeTagCollectionForEditing;
     6import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing;
    47import static org.openstreetmap.josm.tools.I18n.marktr;
    58import static org.openstreetmap.josm.tools.I18n.tr;
    69import static org.openstreetmap.josm.tools.I18n.trn;
    710
    8 import java.awt.GridBagLayout;
    911import java.awt.event.ActionEvent;
    1012import java.awt.event.KeyEvent;
    1113import java.awt.geom.Area;
     
    1315import java.util.ArrayList;
    1416import java.util.Collection;
    1517import java.util.Collections;
     18import java.util.Comparator;
    1619import java.util.HashMap;
    1720import java.util.HashSet;
     21import java.util.LinkedHashSet;
    1822import java.util.LinkedList;
    1923import java.util.List;
    2024import java.util.Map;
    21 import java.util.Map.Entry;
    2225import java.util.Set;
    2326import java.util.TreeMap;
    24 import java.util.TreeSet;
    2527
    26 import javax.swing.Box;
    27 import javax.swing.JComboBox;
    28 import javax.swing.JLabel;
    2928import javax.swing.JOptionPane;
    30 import javax.swing.JPanel;
    3129
    3230import org.openstreetmap.josm.Main;
    3331import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult;
     
    4442import org.openstreetmap.josm.data.osm.OsmPrimitive;
    4543import org.openstreetmap.josm.data.osm.Relation;
    4644import org.openstreetmap.josm.data.osm.RelationMember;
    47 import org.openstreetmap.josm.data.osm.TigerUtils;
     45import org.openstreetmap.josm.data.osm.TagCollection;
    4846import org.openstreetmap.josm.data.osm.Way;
    49 import org.openstreetmap.josm.gui.ExtendedDialog;
    50 import org.openstreetmap.josm.tools.GBC;
     47import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
    5148import org.openstreetmap.josm.tools.Shortcut;
    5249
    5350public class JoinAreasAction extends JosmAction {
     
    5552    private LinkedList<Command> cmds = new LinkedList<Command>();
    5653    private int cmdsCount = 0;
    5754
     55
    5856    /**
    5957     * This helper class describes join ares action result.
    6058     * @author viesturs
    6159     *
    6260     */
    63     public static class JoinAreasResult {
     61    public static class JoinAreasResult{
    6462
    65         public Way outerWay;
    66         public List<Way> innerWays;
    67 
    6863        public boolean mergeSuccessful;
    6964        public boolean hasChanges;
    7065        public boolean hasRelationProblems;
     66
     67        public List<ComplexPolygon> polygons;
    7168    }
    7269
    73     // HelperClass
    74     // Saves a node and two positions where to insert the node into the ways
    75     private static class NodeToSegs implements Comparable<NodeToSegs> {
    76         public int pos;
    77         public Node n;
    78         public double dis;
    79         public NodeToSegs(int pos, Node n, LatLon dis) {
    80             this.pos = pos;
    81             this.n = n;
    82             this.dis = n.getCoor().greatCircleDistance(dis);
    83         }
     70    public static class ComplexPolygon{
     71        public Way outerWay;
     72        public List<Way> innerWays;
    8473
    85         public int compareTo(NodeToSegs o) {
    86             if(this.pos == o.pos)
    87                 return (this.dis - o.dis) > 0 ? 1 : -1;
    88                 return this.pos - o.pos;
     74        public ComplexPolygon(Way way){
     75            outerWay = way;
     76            innerWays = new ArrayList<Way>();
    8977        }
    90 
    91         @Override
    92         public int hashCode() {
    93             return pos;
    94         }
    95 
    96         @Override
    97         public boolean equals(Object o) {
    98             if (o instanceof NodeToSegs)
    99                 return compareTo((NodeToSegs) o) == 0;
    100             else
    101                 return false;
    102         }
    10378    }
    10479
    10580    // HelperClass
     
    125100        }
    126101    }
    127102
    128     /**
    129      * HelperClass
    130      * saves a way and the "inside" side
    131      * insideToTheLeft: if true left side is "in", false -right side is "in".
    132      * Left and right are determined along the orientation of way.
    133      */
    134     private static class WayInPath {
     103
     104    //HelperClass
     105    //saves a way and the "inside" side
     106    // insideToTheLeft: if true left side is "in", false -right side is "in". Left and right are determined along the orientation of way.
     107    private static class WayInPath{
    135108        public final Way way;
    136         public boolean insideToTheLeft;
     109        public boolean insideToTheRight;
    137110
    138         public WayInPath(Way way, boolean insideLeft) {
    139             this.way = way;
    140             this.insideToTheLeft = insideLeft;
     111        public WayInPath(Way _way, boolean _insideRight){
     112            this.way = _way;
     113            this.insideToTheRight = _insideRight;
    141114        }
    142115
    143116        @Override
     
    147120
    148121        @Override
    149122        public boolean equals(Object other) {
    150             if (!(other instanceof WayInPath))
    151                 return false;
     123            if (!(other instanceof WayInPath)) return false;
    152124            WayInPath otherMember = (WayInPath) other;
    153             return otherMember.way.equals(this.way) && otherMember.insideToTheLeft == this.insideToTheLeft;
     125            return otherMember.way.equals(this.way) && otherMember.insideToTheRight == this.insideToTheRight;
    154126        }
    155127    }
    156128
     129    /**
     130     * This hepler class implements algorithm traversing trough connected ways.
     131     * Assumes you are going in clockwise orientation.
     132     * @author viesturs
     133     *
     134     */
     135    private static class WayTraverser {
     136
     137        private Set<WayInPath> availableWays;
     138        private WayInPath lastWay;
     139        private boolean lastWayReverse;
     140
     141        public WayTraverser(Collection<WayInPath> ways){
     142
     143            availableWays = new HashSet<WayInPath>(ways);
     144            lastWay = null;
     145        }
     146
     147        public void removeWays(Collection<WayInPath> ways){
     148            availableWays.removeAll(ways);
     149        }
     150
     151        public boolean hasWays(){
     152            return availableWays.size() > 0;
     153        }
     154
     155        public WayInPath startNewWay(WayInPath way) {
     156            lastWay = way;
     157            lastWayReverse = !lastWay.insideToTheRight;
     158
     159            return lastWay;
     160        }
     161
     162        public WayInPath startNewWay() {
     163            if (availableWays.size() == 0) {
     164                lastWay = null;
     165            } else {
     166                lastWay = availableWays.iterator().next();
     167                lastWayReverse = !lastWay.insideToTheRight;
     168            }
     169
     170            return lastWay;
     171        }
     172
     173
     174        public  WayInPath advanceNextLeftmostWay(){
     175            return advanceNextWay(false);
     176        }
     177
     178        public  WayInPath advanceNextRightmostWay(){
     179            return advanceNextWay(true);
     180        }
     181
     182        private WayInPath advanceNextWay(boolean rightmost){
     183
     184            Node headNode = !lastWayReverse ? lastWay.way.lastNode() : lastWay.way.firstNode();
     185            Node prevNode = !lastWayReverse ? lastWay.way.getNode(lastWay.way.getNodesCount() - 2) : lastWay.way.getNode(1);
     186
     187            //find best next way
     188            WayInPath bestWay = null;
     189            Node bestWayNextNode = null;
     190            boolean bestWayReverse = false;
     191
     192            for(WayInPath way: availableWays)
     193            {
     194                if (way.way.firstNode().equals(headNode)){
     195                    //start adjacent to headNode
     196                    Node nextNode = way.way.getNode(1);
     197
     198                    if (nextNode.equals(prevNode))
     199                    {
     200                        //this is the path we came from - ignore it.
     201                    }
     202                    else if (bestWay == null || (isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode) == rightmost) )
     203                    {
     204                        //the new way is better
     205                        bestWay = way;
     206                        bestWayReverse = false;
     207                        bestWayNextNode = nextNode;
     208                    }
     209                }
     210
     211                if (way.way.lastNode().equals(headNode))
     212                {
     213                    //end adjacent to headNode
     214                    Node nextNode = way.way.getNode(way.way.getNodesCount() - 2);
     215
     216                    if (nextNode.equals(prevNode))
     217                    {
     218                        //this is the path we came from - ignore it.
     219                    }
     220                    else if (bestWay == null || (isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode) == rightmost))
     221                    {
     222                        //the new way is better
     223                        bestWay = way;
     224                        bestWayReverse = true;
     225                        bestWayNextNode = nextNode;
     226                    }
     227                }
     228            }
     229
     230            lastWay = bestWay;
     231            lastWayReverse = bestWayReverse;
     232
     233            return lastWay;
     234        }
     235
     236        public boolean isLastWayInsideToTheRight(){
     237            return lastWayReverse != lastWay.insideToTheRight;
     238        }
     239    }
     240
     241    /**
     242     * Provides some node order , based on coordinates, nodes with equal coordinates are equal.
     243     * @author viesturs
     244     *
     245     */
     246    private class NodePositionComparator implements Comparator<Node>{
     247
     248        @Override
     249        public int compare(Node n1, Node n2) {
     250
     251            double dLat = n1.getCoor().lat() - n2.getCoor().lat();
     252            double dLon = n1.getCoor().lon() - n2.getCoor().lon();
     253
     254            if (dLat > 0)
     255                return 1;
     256            else if (dLat < 0)
     257                return -1;
     258            else if (dLon == 0) //dlat is 0 here
     259                return 0;
     260            else
     261                return dLon > 0 ? 1: -1;
     262        }
     263    }
     264
     265
     266    /**
     267     * Helper storage class for finding findOuterWays
     268     * @author viesturs
     269     */
     270    static class PolygonLevel{
     271        public final int level;
     272        public final ComplexPolygon pol;
     273
     274        public PolygonLevel(ComplexPolygon _pol, int _level){
     275            pol = _pol;
     276            level = _level;
     277        }
     278    }
     279
     280
    157281    // Adds the menu entry, Shortcuts, etc.
    158282    public JoinAreasAction() {
    159283        super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"), Shortcut.registerShortcut("tools:joinareas", tr("Tool: {0}", tr("Join overlapping Areas")),
     
    172296            return;
    173297        }
    174298
    175         // Too many ways
    176         if(ways.size() > 2) {
    177             JOptionPane.showMessageDialog(Main.parent, tr("Only up to two areas can be joined at the moment."));
    178             return;
    179         }
    180 
    181299        List<Node> allNodes = new ArrayList<Node>();
    182300        for (Way way: ways) {
    183301            if(!way.isClosed()) {
     
    208326            }
    209327        }
    210328
    211         if (checkForTagConflicts(ways.getFirst(), ways.getLast()))
    212             //there was conflicts and user canceled abort the action.
     329        if(!resolveTagConflicts(ways))
    213330            return;
     331        //user cancelled, do nothing.
    214332
     333        //collect ways and analyze multipolygon relations
     334        List<ComplexPolygon> areas = new ArrayList<ComplexPolygon>();
    215335
    216         JoinAreasResult result = joinAreas(ways.getFirst(), ways.getLast());
     336        for (Way way : ways)
     337        {
     338            //TODO: analyze multipolygon relations...
     339            areas.add(new ComplexPolygon(way));
     340        }
    217341
    218         if (result.hasChanges) {
     342        JoinAreasResult result = joinAreas(areas);
     343
     344        if (result.hasChanges){
     345
    219346            Main.map.mapView.repaint();
    220347            DataSet ds = Main.main.getCurrentDataSet();
    221348            ds.fireSelectionChanged();
     
    224351        }
    225352    }
    226353
     354
    227355    /**
    228      * Will join two overlapping areas
    229      * @param Way First way/area
    230      * @param Way Second way/area
     356     * Will join two or more overlapping areas
     357     * @param areas - list of areas to join
     358     * @return new area formed.
    231359     */
    232     private JoinAreasResult joinAreas(Way a, Way b) {
     360    private JoinAreasResult joinAreas(List<ComplexPolygon> areas) {
    233361
    234362        JoinAreasResult result = new JoinAreasResult();
    235363        result.hasChanges = false;
    236364
    237         // Fix self-overlapping first or other errors
    238         boolean same = a.equals(b);
    239         if(!same) {
    240             int i = 0;
     365        List<Way> allStartingWays = new ArrayList<Way>();
     366        List<Way> innerStartingWays = new ArrayList<Way>();
     367        List<Way> outerStartingWays = new ArrayList<Way>();
    241368
    242             //join each area with itself, fixing self-crossings.
    243             JoinAreasResult resultA = joinAreas(a, a);
    244             JoinAreasResult resultB = joinAreas(b, b);
     369        for(ComplexPolygon area: areas)
     370        {
     371            outerStartingWays.add(area.outerWay);
     372            innerStartingWays.addAll(area.innerWays);
     373        }
    245374
    246             if (resultA.mergeSuccessful) {
    247                 a = resultA.outerWay;
    248                 ++i;
    249             }
    250             if(resultB.mergeSuccessful) {
    251                 b = resultB.outerWay;
    252                 ++i;
    253             }
     375        allStartingWays.addAll(innerStartingWays);
     376        allStartingWays.addAll(outerStartingWays);
    254377
    255             result.hasChanges = i > 0;
    256             cmdsCount = i;
     378        //first remove nodes in the same coordinate
     379        boolean removedDuplicates = false;
     380        removedDuplicates |= removeDuplicateNodes(allStartingWays);
     381
     382        if (removedDuplicates)
     383        {
     384            result.hasChanges = true;
     385            commitCommands(marktr("Removed duplicate nodes"));
    257386        }
    258387
    259         ArrayList<Node> nodes = addIntersections(a, b);
     388        //find intersection points
     389        ArrayList<Node> nodes = addIntersections(allStartingWays);
    260390
    261391        //no intersections, return.
    262392        if(nodes.size() == 0) return result;
    263393        commitCommands(marktr("Added node on all intersections"));
    264394
     395        ArrayList<RelationRole> relations = new ArrayList<RelationRole>();
     396
    265397        // Remove ways from all relations so ways can be combined/split quietly
    266         ArrayList<RelationRole> relations = removeFromRelations(a);
    267         if(!same) {
    268             relations.addAll(removeFromRelations(b));
     398        for(Way way : allStartingWays)
     399        {
     400            relations.addAll(removeFromRelations(way));
    269401        }
    270402
    271403        // Don't warn now, because it will really look corrupted
    272404        boolean warnAboutRelations = relations.size() > 0;
    273405
    274         ArrayList<Way> allWays = splitWaysOnNodes(a, b, nodes);
     406        ArrayList<WayInPath> preparedWays = new ArrayList<WayInPath>();
    275407
     408        for (Way way: outerStartingWays){
     409            ArrayList<Way> splitWays = splitWayOnNodes(way, nodes);
     410            preparedWays.addAll(markWayInsideSide(splitWays, false));
     411        }
     412
     413        for (Way way: innerStartingWays){
     414            ArrayList<Way> splitWays = splitWayOnNodes(way, nodes);
     415            preparedWays.addAll(markWayInsideSide(splitWays, true));
     416        }
     417
    276418        // Find inner ways save them to a list
    277         ArrayList<WayInPath> outerWays = findOuterWays(allWays);
    278         ArrayList<Way> innerWays = findInnerWays(allWays, outerWays);
     419        ArrayList<Way> discardedWays = new ArrayList<Way>();
     420        List<List<WayInPath>> boundryParts = findBoundaryWays(preparedWays, discardedWays);
     421        List<Way> boundaries = joinBoundaries(boundryParts);
    279422
    280         // Join outer ways
    281         Way outerWay = joinOuterWays(outerWays);
     423        List<ComplexPolygon> polygons = findPolygons(boundaries);
    282424
    283         // Fix Multipolygons if there are any
    284         List<Way> newInnerWays = fixMultipolygons(innerWays, outerWay, same);
    285 
    286         // Delete the remaining inner ways
    287         if(innerWays != null && innerWays.size() > 0) {
    288             cmds.add(DeleteCommand.delete(Main.map.mapView.getEditLayer(), innerWays, true));
     425        // Delete the discarded inner ways
     426        if(discardedWays.size() > 0) {
     427            cmds.add(DeleteCommand.delete(Main.map.mapView.getEditLayer(), discardedWays, true));
    289428        }
    290429        commitCommands(marktr("Delete Ways that are not part of an inner multipolygon"));
    291430
    292431        // We can attach our new multipolygon relation and pretend it has always been there
    293         addOwnMultigonRelation(newInnerWays, outerWay, relations);
    294         fixRelations(relations, outerWay);
     432        for(ComplexPolygon pol: polygons)
     433        {
     434            addOwnMultigonRelation(pol.innerWays, pol.outerWay, relations);
     435            fixRelations(relations, pol.outerWay);
     436        }
     437
    295438        commitCommands(marktr("Fix relations"));
    296439
    297         stripTags(newInnerWays);
     440        for(ComplexPolygon pol: polygons)
     441        {
     442            stripTags(pol.innerWays);
     443        }
    298444
    299         makeCommitsOneAction(
    300                 same
    301                 ? marktr("Joined self-overlapping area")
    302                         : marktr("Joined overlapping areas")
    303         );
     445        makeCommitsOneAction(marktr("Joined overlapping areas"));
    304446
    305447        if(warnAboutRelations) {
    306448            JOptionPane.showMessageDialog(Main.parent, tr("Some of the ways were part of relations that have been modified. Please verify no errors have been introduced."));
     
    308450
    309451        result.hasChanges = true;
    310452        result.mergeSuccessful = true;
    311         result.outerWay = outerWay;
    312         result.innerWays = newInnerWays;
    313 
     453        result.polygons = polygons;
    314454        return result;
    315455    }
    316456
     
    318458     * Checks if tags of two given ways differ, and presents the user a dialog to solve conflicts
    319459     * @param Way First way to check
    320460     * @param Way Second Way to check
    321      * @return boolean True if not all conflicts could be resolved, False if everything's fine
     461     * @return boolean True if all conflicts are resolved, False if conflicts remain.
    322462     */
    323     private boolean checkForTagConflicts(Way a, Way b) {
    324         ArrayList<Way> ways = new ArrayList<Way>();
    325         ways.add(a);
    326         ways.add(b);
     463    private boolean resolveTagConflicts(List<Way> ways) {
     464        //mostly copied from CombineWayAction.java.
    327465
    328         // FIXME: This is mostly copied and pasted from CombineWayAction.java and one day should be moved into tools
    329         // We have TagCollection handling for that now - use it here as well
    330         Map<String, Set<String>> props = new TreeMap<String, Set<String>>();
    331         for (Way w : ways) {
    332             for (String key: w.keySet()) {
    333                 if (!props.containsKey(key)) {
    334                     props.put(key, new TreeSet<String>());
    335                 }
    336                 props.get(key).add(w.get(key));
    337             }
     466        TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways);
     467        TagCollection completeWayTags = new TagCollection(wayTags);
     468        combineTigerTags(completeWayTags);
     469        normalizeTagCollectionBeforeEditing(completeWayTags, ways);
     470        TagCollection tagsToEdit = new TagCollection(completeWayTags);
     471        completeTagCollectionForEditing(tagsToEdit);
     472
     473        CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
     474        dialog.getTagConflictResolverModel().populate(tagsToEdit, completeWayTags.getKeysWithMultipleValues());
     475        dialog.setTargetPrimitive(ways.get(0));
     476        Set<Relation> parentRelations = CombineWayAction.getParentRelations(ways);
     477        dialog.getRelationMemberConflictResolverModel().populate(
     478                parentRelations,
     479                ways
     480        );
     481        dialog.prepareDefaultDecisions();
     482
     483        // resolve tag conflicts if necessary
     484        //
     485        if (!completeWayTags.isApplicableToPrimitive() || !parentRelations.isEmpty()) {
     486            dialog.setVisible(true);
     487            if (dialog.isCancelled())
     488                return false;
    338489        }
    339490
    340         Way ax = new Way(a);
    341         Way bx = new Way(b);
     491        for(Way way: ways)
     492        {
     493            dialog.setTargetPrimitive(way);
     494            cmds.addAll(dialog.buildResolutionCommands());
     495        }
    342496
    343         Map<String, JComboBox> components = new HashMap<String, JComboBox>();
    344         JPanel p = new JPanel(new GridBagLayout());
    345         for (Entry<String, Set<String>> e : props.entrySet()) {
    346             if (TigerUtils.isTigerTag(e.getKey())) {
    347                 String combined = TigerUtils.combineTags(e.getKey(), e.getValue());
    348                 ax.put(e.getKey(), combined);
    349                 bx.put(e.getKey(), combined);
    350             } else if (e.getValue().size() > 1) {
    351                 if("created_by".equals(e.getKey()))
    352                 {
    353                     ax.remove("created_by");
    354                     bx.remove("created_by");
    355                 } else {
    356                     JComboBox c = new JComboBox(e.getValue().toArray());
    357                     c.setEditable(true);
    358                     p.add(new JLabel(e.getKey()), GBC.std());
    359                     p.add(Box.createHorizontalStrut(10), GBC.std());
    360                     p.add(c, GBC.eol());
    361                     components.put(e.getKey(), c);
     497        commitCommands(marktr("Fix tag conflicts"));
     498        return true;
     499    }
     500
     501
     502    /**
     503     * This method removes duplicate points (if any) from the input way.
     504     * @param way the way to process
     505     * @return true if any changes where made
     506     */
     507    private boolean removeDuplicateNodes(List<Way> ways)
     508    {
     509        //TODO: maybe join nodes with JoinNodesAction, rather than reconnect the ways.
     510
     511        Map<Node, Node> nodeMap = new TreeMap<Node, Node>(new NodePositionComparator());
     512        int totalNodesRemoved = 0;
     513
     514        for(Way way:ways)
     515        {
     516            if (way.getNodes().size() < 2) {
     517                continue;
     518            }
     519
     520            int nodesRemoved = 0;
     521            List<Node> newNodes = new ArrayList<Node>();
     522            Node prevNode = null;
     523
     524            for(Node node: way.getNodes())
     525            {
     526                if (!nodeMap.containsKey(node)){
     527                    //new node
     528                    nodeMap.put(node, node);
     529
     530                    //avoid duplicate nodes
     531                    if (prevNode != node)
     532                    {
     533                        newNodes.add(node);
     534                    }
     535                    else
     536                    {
     537                        nodesRemoved ++;
     538                    }
    362539                }
    363             } else {
    364                 String val = e.getValue().iterator().next();
    365                 ax.put(e.getKey(), val);
    366                 bx.put(e.getKey(), val);
     540                else{
     541                    //node with same coordinates already exists, substitute with existing node
     542                    Node representator = nodeMap.get(node);
     543
     544                    if (representator != node){
     545                        nodesRemoved ++;
     546                    }
     547
     548                    //avoid duplicate node
     549                    if (prevNode != representator)
     550                    {
     551                        newNodes.add(representator);
     552                    }
     553                }
     554
     555                prevNode = node;
    367556            }
    368         }
    369557
    370         if (components.isEmpty())
    371             return false; // No conflicts found
     558            if (nodesRemoved > 0)
     559            {
    372560
    373         ExtendedDialog ed = new ExtendedDialog(Main.parent,
    374                 tr("Enter values for all conflicts."),
    375                 new String[] {tr("Solve Conflicts"), tr("Cancel")});
    376         ed.setButtonIcons(new String[] {"dialogs/conflict.png", "cancel.png"});
    377         ed.setContent(p);
    378         ed.showDialog();
     561                if (newNodes.size() == 1) {//all nodes in the same coordinate - add one more node, to have closed way.
     562                    newNodes.add(newNodes.get(0));
     563                }
    379564
    380         if (ed.getValue() != 1) return true; // user cancel, unresolvable conflicts
    381 
    382         for (Entry<String, JComboBox> e : components.entrySet()) {
    383             String val = e.getValue().getEditor().getItem().toString();
    384             ax.put(e.getKey(), val);
    385             bx.put(e.getKey(), val);
     565                Way newWay=new Way(way);
     566                newWay.setNodes(newNodes);
     567                cmds.add(new ChangeCommand(way, newWay));
     568                totalNodesRemoved += nodesRemoved;
     569            }
    386570        }
    387571
    388         cmds.add(new ChangeCommand(a, ax));
    389         cmds.add(new ChangeCommand(b, bx));
    390         commitCommands(marktr("Fix tag conflicts"));
    391         return false;
     572        return totalNodesRemoved > 0;
    392573    }
    393574
     575
     576
    394577    /**
    395      * Will find all intersection and add nodes there for two given ways
    396      * @param Way First way
    397      * @param Way Second way
    398      * @return ArrayList<OsmPrimitive> List of new nodes
     578     * Will find all intersection and add nodes there for list of given ways. Handles self-intersections too.
     579     * And make commands to add the intersection points to ways.
     580     * @param List<Way> - a list of ways to test
     581     * @return ArrayList<Node> List of new nodes
     582     * Prerequisite: no two nodes have the same coordinates.
    399583     */
    400     private ArrayList<Node> addIntersections(Way a, Way b) {
    401         boolean same = a.equals(b);
    402         int nodesSizeA = a.getNodesCount();
    403         int nodesSizeB = b.getNodesCount();
     584    private ArrayList<Node> addIntersections(List<Way> ways) {
     585        //TODO: this is a bit slow - O( (number of nodes)^2 + numberOfIntersections * numberOfNodes )
    404586
    405         ArrayList<Node> nodes = new ArrayList<Node>();
    406         ArrayList<NodeToSegs> nodesA = new ArrayList<NodeToSegs>();
    407         ArrayList<NodeToSegs> nodesB = new ArrayList<NodeToSegs>();
     587        //stupid java, cannot instantiate array of generic classes..
     588        @SuppressWarnings("unchecked")
     589        ArrayList<Node>[] newNodes = new ArrayList[ways.size()];
     590        boolean[] changedWays = new boolean[ways.size()];
    408591
    409         for (int i = (same ? 1 : 0); i < nodesSizeA - 1; i++) {
    410             for (int j = (same ? i + 2 : 0); j < nodesSizeB - 1; j++) {
    411                 // Avoid re-adding nodes that already exist on (some) intersections
    412                 if(a.getNode(i).equals(b.getNode(j)) || a.getNode(i+1).equals(b.getNode(j)))   {
    413                     nodes.add(b.getNode(j));
    414                     continue;
    415                 } else
    416                     if(a.getNode(i).equals(b.getNode(j+1)) || a.getNode(i+1).equals(b.getNode(j+1))) {
    417                         nodes.add(b.getNode(j+1));
    418                         continue;
     592        Set<Node> intersectionNodes = new LinkedHashSet<Node>();
     593
     594        for (int pos = 0; pos < ways.size(); pos ++)
     595        {
     596            newNodes[pos] = new ArrayList<Node>(ways.get(pos).getNodes());
     597            changedWays[pos] = false;
     598        }
     599
     600        //iterate over all segment pairs and introduce the intersections
     601
     602        Comparator<Node> coordsComparator = new NodePositionComparator();
     603
     604        int seg1Way = 0;
     605        int seg1Pos = -1;
     606
     607        while (true)
     608        {
     609            //advance to next segment
     610            seg1Pos++;
     611            if (seg1Pos > newNodes[seg1Way].size() - 2)
     612            {
     613                seg1Way++;
     614                seg1Pos = 0;
     615
     616                if (seg1Way == ways.size()) { //finished
     617                    break;
     618                }
     619            }
     620
     621
     622            //iterate over secondary segment
     623
     624            int seg2Way = seg1Way;
     625            int seg2Pos = seg1Pos + 1;//skip the adjacent segment
     626
     627            while (true)
     628            {
     629
     630                //advance to next segment
     631                seg2Pos++;
     632                if (seg2Pos > newNodes[seg2Way].size() - 2)
     633                {
     634                    seg2Way++;
     635                    seg2Pos = 0;
     636
     637                    if (seg2Way == ways.size()) { //finished
     638                        break;
    419639                    }
    420                 LatLon intersection = getLineLineIntersection(
    421                         a.getNode(i)  .getEastNorth().east(), a.getNode(i)  .getEastNorth().north(),
    422                         a.getNode(i+1).getEastNorth().east(), a.getNode(i+1).getEastNorth().north(),
    423                         b.getNode(j)  .getEastNorth().east(), b.getNode(j)  .getEastNorth().north(),
    424                         b.getNode(j+1).getEastNorth().east(), b.getNode(j+1).getEastNorth().north());
    425                 if(intersection == null) {
    426                     continue;
    427640                }
    428641
    429                 // Create the node. Adding them to the ways must be delayed because we still loop over them
    430                 Node n = new Node(intersection);
    431                 cmds.add(new AddCommand(n));
    432                 nodes.add(n);
    433                 // The distance is needed to sort and add the nodes in direction of the way
    434                 nodesA.add(new NodeToSegs(i,  n, a.getNode(i).getCoor()));
    435                 if(same) {
    436                     nodesA.add(new NodeToSegs(j,  n, a.getNode(j).getCoor()));
    437                 } else {
    438                     nodesB.add(new NodeToSegs(j,  n, b.getNode(j).getCoor()));
     642                //need to get them again every time, because other segments may be changed
     643                Node seg1Node1 = newNodes[seg1Way].get(seg1Pos);
     644                Node seg1Node2 = newNodes[seg1Way].get(seg1Pos + 1);
     645                Node seg2Node1 = newNodes[seg2Way].get(seg2Pos);
     646                Node seg2Node2 = newNodes[seg2Way].get(seg2Pos + 1);
     647
     648                int commonCount = 0;
     649                //test if we have common nodes to add.
     650                if (seg1Node1 == seg2Node1 || seg1Node1 == seg2Node2)
     651                {
     652                    commonCount ++;
     653
     654                    if (seg1Way == seg2Way &&
     655                            seg1Pos == 0 &&
     656                            seg2Pos == newNodes[seg2Way].size() -2)
     657                    {
     658                        //do not add - this is first and last segment of the same way.
     659                    }
     660                    else
     661                    {
     662                        intersectionNodes.add(seg1Node1);
     663                    }
    439664                }
     665
     666                if (seg1Node2 == seg2Node1 || seg1Node2 == seg2Node2)
     667                {
     668                    commonCount ++;
     669
     670                    intersectionNodes.add(seg1Node2);
     671                }
     672
     673                //no common nodes - find intersection
     674                if (commonCount == 0)
     675                {
     676                    LatLon intersection = getLineLineIntersection(
     677                            seg1Node1.getEastNorth().east(), seg1Node1.getEastNorth().north(),
     678                            seg1Node2.getEastNorth().east(), seg1Node2.getEastNorth().north(),
     679                            seg2Node1.getEastNorth().east(), seg2Node1.getEastNorth().north(),
     680                            seg2Node2.getEastNorth().east(), seg2Node2.getEastNorth().north());
     681
     682                    if (intersection != null)
     683                    {
     684                        Node newNode = new Node(intersection);
     685                        Node intNode = newNode;
     686                        boolean insertInSeg1 = false;
     687                        boolean insertInSeg2 = false;
     688
     689                        //find if the intersection point is at end point of one of the segments, if so use that point
     690
     691                        //segment 1
     692                        if (coordsComparator.compare(newNode, seg1Node1) == 0) {
     693                            intNode = seg1Node1;
     694                        } else if (coordsComparator.compare(newNode, seg1Node2) == 0) {
     695                            intNode = seg1Node2;
     696                        } else {
     697                            insertInSeg1 = true;
     698                        }
     699
     700                        //segment 2
     701                        if (coordsComparator.compare(newNode, seg2Node1) == 0) {
     702                            intNode = seg2Node1;
     703                        } else if (coordsComparator.compare(newNode, seg2Node2) == 0) {
     704                            intNode = seg2Node2;
     705                        } else {
     706                            insertInSeg2 = true;
     707                        }
     708
     709                        if (insertInSeg1)
     710                        {
     711                            newNodes[seg1Way].add(seg1Pos +1, intNode);
     712                            changedWays[seg1Way] = true;
     713
     714                            //fix seg2 position, as indexes have changed, seg2Pos is always bigger than seg1Pos on the same segment.
     715                            if (seg2Way == seg1Way) {
     716                                seg2Pos ++;
     717                            }
     718                        }
     719
     720                        if (insertInSeg2){
     721                            newNodes[seg2Way].add(seg2Pos +1, intNode);
     722                            changedWays[seg2Way] = true;
     723
     724                            //Do not need to compare again to already split segment
     725                            seg2Pos ++;
     726                        }
     727
     728                        intersectionNodes.add(intNode);
     729
     730                        if (intNode == newNode)
     731                        {
     732                            cmds.add(new AddCommand(intNode));
     733                        }
     734                    }
     735                }
    440736            }
    441737        }
    442738
    443         addNodesToWay(a, nodesA);
    444         if(!same) {
    445             addNodesToWay(b, nodesB);
     739        for (int pos = 0; pos < ways.size(); pos ++)
     740        {
     741            if (changedWays[pos] == false) {
     742                continue;
     743            }
     744
     745            Way way = ways.get(pos);
     746            Way newWay = new Way(way);
     747            newWay.setNodes(newNodes[pos]);
     748
     749            cmds.add(new ChangeCommand(way, newWay));
    446750        }
    447751
    448         return nodes;
     752        return new ArrayList<Node>(intersectionNodes);
    449753    }
    450754
    451755    /**
     
    477781        ));
    478782    }
    479783
    480     /**
    481      * Inserts given nodes with positions into the given ways
    482      * @param Way The way to insert the nodes into
    483      * @param Collection<NodeToSegs> The list of nodes with positions to insert
    484      */
    485     private void addNodesToWay(Way a, ArrayList<NodeToSegs> nodes) {
    486         if(nodes.size() == 0)
    487             return;
    488         Way ax=new Way(a);
    489         Collections.sort(nodes);
    490784
    491         int numOfAdds = 1;
    492         for(NodeToSegs n : nodes) {
    493             ax.addNode(n.pos + numOfAdds, n.n);
    494             numOfAdds++;
    495         }
    496785
    497         cmds.add(new ChangeCommand(a, ax));
    498     }
    499 
    500786    /**
    501787     * Commits the command list with a description
    502788     * @param String The description of what the commands do
     
    552838        return result;
    553839    }
    554840
     841
    555842    /**
    556      * This method splits ways into smaller parts, using the prepared nodes list as split points.
    557      * Uses SplitWayAction.splitWay for the heavy lifting.
     843     * This method analyzes the way and assigns each part what direction polygon "inside" is.
     844     * @param parts the split parts of the way
     845     * @param isInner - if true, reverts the direction (for multipolygon islands)
     846     * @return list of parts, marked with the inside orientation.
     847     */
     848    private ArrayList<WayInPath> markWayInsideSide(List<Way> parts, boolean isInner){
     849
     850        ArrayList<WayInPath> result = new ArrayList<WayInPath>();
     851
     852        //prepare prev and next maps
     853        Map<Way, Way> nextWayMap = new HashMap<Way, Way>();
     854        Map<Way, Way> prevWayMap = new HashMap<Way, Way>();
     855
     856        for (int pos = 0; pos < parts.size(); pos ++){
     857
     858            if (!parts.get(pos).lastNode().equals(parts.get((pos + 1) % parts.size()).firstNode()))
     859                throw new RuntimeException("Way not circular");
     860
     861            nextWayMap.put(parts.get(pos), parts.get((pos + 1) % parts.size()));
     862            prevWayMap.put(parts.get(pos), parts.get((pos + parts.size() - 1) % parts.size()));
     863        }
     864
     865        //find the node with minimum y - it's guaranteed to be outer. (What about the south pole?)
     866        Way topWay = null;
     867        Node topNode = null;
     868        int topIndex = 0;
     869        double minY = Double.POSITIVE_INFINITY;
     870
     871        for(Way way: parts) {
     872            for (int pos = 0; pos < way.getNodesCount(); pos ++){
     873                Node node = way.getNode(pos);
     874
     875                if (node.getEastNorth().getY() < minY){
     876                    minY = node.getEastNorth().getY();
     877                    topWay = way;
     878                    topNode = node;
     879                    topIndex = pos;
     880                }
     881            }
     882        }
     883
     884        //get the upper way and it's orientation.
     885
     886        boolean wayClockwise; // orientation of the top way.
     887
     888        if (topNode.equals(topWay.firstNode()) || topNode.equals(topWay.lastNode()))
     889        {
     890            Node headNode = null; // the node at junction
     891            Node prevNode = null; // last node from previous path
     892            wayClockwise = false;
     893
     894            //node is in split point - find the outermost way from this point
     895
     896            headNode = topNode;
     897            //make a fake node that is downwards from head node (smaller Y). It will be a division point between paths.
     898            prevNode = new Node(new EastNorth(headNode.getEastNorth().getX(), headNode.getEastNorth().getY() - 1e5));
     899
     900            topWay = null;
     901            wayClockwise = false;
     902            Node bestWayNextNode = null;
     903
     904            for(Way way: parts)
     905            {
     906                if (way.firstNode().equals(headNode))
     907                {
     908                    Node nextNode = way.getNode(1);
     909
     910                    if (topWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode))
     911                    {
     912                        //the new way is better
     913                        topWay = way;
     914                        wayClockwise = true;
     915                        bestWayNextNode = nextNode;
     916                    }
     917                }
     918
     919                if (way.lastNode().equals(headNode))
     920                {
     921                    //end adjacent to headNode
     922                    Node nextNode = way.getNode(way.getNodesCount() - 2);
     923
     924                    if (topWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode))
     925                    {
     926                        //the new way is better
     927                        topWay = way;
     928                        wayClockwise = false;
     929                        bestWayNextNode = nextNode;
     930                    }
     931                }
     932            }
     933        }
     934        else
     935        {
     936            //node is inside way - pick the clockwise going end.
     937            Node prev = topWay.getNode(topIndex - 1);
     938            Node next = topWay.getNode(topIndex + 1);
     939
     940            //there will be no parallel segments in the middle of way, so all fine.
     941            wayClockwise = angleIsClockwise(prev, topNode, next);
     942        }
     943
     944        Way curWay = topWay;
     945        boolean curWayInsideToTheRight = wayClockwise ^ isInner;
     946
     947        //iterate till full circle is reached
     948        while (true){
     949
     950            //add cur way
     951            WayInPath resultWay = new WayInPath(curWay, curWayInsideToTheRight);
     952            result.add(resultWay);
     953
     954            //process next way
     955            Way nextWay = nextWayMap.get(curWay);
     956            Node prevNode = curWay.getNode(curWay.getNodesCount() - 2);
     957            Node headNode = curWay.lastNode();
     958            Node nextNode = nextWay.getNode(1);
     959
     960            if (nextWay == topWay)
     961            {
     962                //full loop traversed - all done.
     963                break;
     964            }
     965
     966
     967            //find intersecting segments
     968            // the intersections will look like this:
     969            //
     970            //                       ^
     971            //                       |
     972            //                       X wayBNode
     973            //                       |
     974            //                  wayB |
     975            //                       |
     976            //             curWay    |       nextWay
     977            //----X----------------->X----------------------X---->
     978            //    prevNode           ^headNode              nextNode
     979            //                       |
     980            //                       |
     981            //                  wayA |
     982            //                       |
     983            //                       X wayANode
     984            //                       |
     985
     986            int intersectionCount = 0;
     987
     988            for (Way wayA: parts){
     989
     990                if (wayA == curWay){
     991                    continue;
     992                }
     993
     994                if (wayA.lastNode().equals(headNode)){
     995
     996                    Way wayB = nextWayMap.get(wayA);
     997
     998                    //test if wayA is opposite wayB relative to curWay and nextWay
     999
     1000                    Node wayANode = wayA.getNode(wayA.getNodesCount() - 2);
     1001                    Node wayBNode = wayB.getNode(1);
     1002
     1003                    boolean wayAToTheRight = isToTheRightSideOfLine(prevNode, headNode, nextNode, wayANode);
     1004                    boolean wayBToTheRight = isToTheRightSideOfLine(prevNode, headNode, nextNode, wayBNode);
     1005
     1006                    if (wayAToTheRight != wayBToTheRight){
     1007                        intersectionCount ++;
     1008                    }
     1009                }
     1010            }
     1011
     1012            //if odd number of crossings, invert orientation
     1013            if (intersectionCount % 2 == 1){
     1014                curWayInsideToTheRight = !curWayInsideToTheRight;
     1015            }
     1016
     1017            curWay = nextWay;
     1018        }
     1019
     1020        return result;
     1021    }
     1022
     1023    /**
     1024     * This is a method splits way into smaller parts, using the prepared nodes list as split points.
     1025     * Uses  SplitWayAction.splitWay for the heavy lifting.
    5581026     * @return list of split ways (or original ways if no splitting is done).
    5591027     */
    560     private ArrayList<Way> splitWaysOnNodes(Way a, Way b, Collection<Node> nodes) {
     1028    private ArrayList<Way> splitWayOnNodes(Way way, Collection<Node> nodes) {
    5611029
    5621030        ArrayList<Way> result = new ArrayList<Way>();
    563         List<Way> ways = new ArrayList<Way>();
    564         ways.add(a);
    565         ways.add(b);
     1031        List<List<Node>> chunks = buildNodeChunks(way, nodes);
    5661032
    567         for (Way way: ways) {
    568             List<List<Node>> chunks = buildNodeChunks(way, nodes);
     1033        if (chunks.size() > 1)
     1034        {
    5691035            SplitWayResult split = SplitWayAction.splitWay(Main.map.mapView.getEditLayer(), way, chunks, Collections.<OsmPrimitive>emptyList());
    5701036
    5711037            //execute the command, we need the results
    572             Main.main.undoRedo.add(split.getCommand());
    573             cmdsCount ++;
     1038            cmds.add(split.getCommand());
     1039            commitCommands(marktr("Split ways into fragments"));
    5741040
    5751041            result.add(split.getOriginalWay());
    5761042            result.addAll(split.getNewWays());
     1043        } else {
     1044            //nothing to split
     1045            result.add(way);
    5771046        }
    5781047
    5791048        return result;
    5801049    }
    5811050
     1051
    5821052    /**
    5831053     * Simple chunking version. Does not care about circular ways and result being proper, we will glue it all back together later on.
    5841054     * @param way the way to chunk
    5851055     * @param splitNodes the places where to cut.
    586      * @return list of node segments to produce.
     1056     * @return list of node paths to produce.
    5871057     */
    5881058    private List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes)
    5891059    {
     
    6071077        return result;
    6081078    }
    6091079
    610     /**
    611      * Returns all nodes for given ways
    612      * @param Collection<Way> The list of ways which nodes are to be returned
    613      * @return Collection<Node> The list of nodes the ways contain
    614      */
    615     private Collection<Node> getNodesFromWays(Collection<Way> ways) {
    616         Collection<Node> allNodes = new ArrayList<Node>();
    617         for(Way w: ways) {
    618             allNodes.addAll(w.getNodes());
     1080
     1081    private List<ComplexPolygon> findPolygons(Collection<Way> boundaryWays) {
     1082
     1083        List<PolygonLevel> list = findOuterWaysImpl(0, boundaryWays);
     1084        List<ComplexPolygon> result = new ArrayList<ComplexPolygon>();
     1085
     1086        //take every other level
     1087        for(PolygonLevel pol: list){
     1088            if (pol.level %2 == 0){
     1089                result.add(pol.pol);
     1090            }
    6191091        }
    620         return allNodes;
     1092
     1093        return result;
    6211094    }
    6221095
    6231096    /**
    624      * Gets all inner ways given all ways and outer ways.
    625      * @param multigonWays
    626      * @param outerWays
    627      * @return list of inner ways.
     1097     * Collects outer way and corresponding inner ways from all boundaries.
     1098     * @param boundaryWays
     1099     * @return the outermostWay.
    6281100     */
    629     private ArrayList<Way> findInnerWays(Collection<Way> multigonWays, Collection<WayInPath> outerWays) {
    630         ArrayList<Way> innerWays = new ArrayList<Way>();
    631         Set<Way> outerSet = new HashSet<Way>();
     1101    private List<PolygonLevel> findOuterWaysImpl(int level, Collection<Way> boundaryWays) {
    6321102
    633         for(WayInPath w: outerWays) {
    634             outerSet.add(w.way);
    635         }
     1103        //TODO: bad performance for deep nestings...
     1104        List<PolygonLevel> result = new ArrayList<PolygonLevel>();
    6361105
    637         for(Way way: multigonWays) {
    638             if (!outerSet.contains(way)) {
    639                 innerWays.add(way);
     1106        for(Way outerWay: boundaryWays){
     1107
     1108            boolean outerGood = true;
     1109            List<Way> innerCandidates = new ArrayList<Way>();
     1110
     1111            for (Way innerWay: boundaryWays)
     1112            {
     1113                if (innerWay == outerWay) {
     1114                    continue;
     1115                }
     1116
     1117                if (wayInsideWay(outerWay, innerWay))
     1118                {
     1119                    outerGood = false;
     1120                    break;
     1121                } else if (wayInsideWay(innerWay, outerWay)){
     1122                    innerCandidates.add(innerWay);
     1123                }
    6401124            }
     1125
     1126            if (!outerGood)
     1127            {
     1128                continue;
     1129            }
     1130
     1131            //add new outer polygon
     1132            ComplexPolygon pol = new ComplexPolygon(outerWay);
     1133            PolygonLevel polLev = new PolygonLevel(pol, level);
     1134
     1135            //process inner ways
     1136            if (innerCandidates.size() > 0)
     1137            {
     1138                List<PolygonLevel> innerList = findOuterWaysImpl(level + 1, innerCandidates);
     1139                result.addAll(innerList);
     1140
     1141                for (PolygonLevel pl: innerList)
     1142                {
     1143                    if (pl.level == level + 1)
     1144                    {
     1145                        pol.innerWays.add(pl.pol.outerWay);
     1146                    }
     1147                }
     1148            }
     1149
     1150            result.add(polLev);
    6411151        }
    6421152
    643         return innerWays;
     1153        return result;
    6441154    }
    6451155
    6461156
     1157
    6471158    /**
    648      * Finds all ways for a given list of Ways that form the outer hull.
    649      * This works by starting with one node and traversing the multigon clockwise, always picking the leftmost path.
    650      * Prerequisites - the ways must not intersect and have common end nodes where they meet.
    651      * @param Collection<Way> A list of (splitted) ways that form a multigon
    652      * @return Collection<Way> A list of ways that form the outer boundary of the multigon.
     1159     * Finds all ways that form inner or outer boundaries.
     1160     * @param Collection<Way> A list of (splitted) ways that form a multigon and share common end nodes on intersections.
     1161     * @param Collection<Way> this list is filled with ways that are to be discarded
     1162     * @return Collection<Collection<Way>> A list of ways that form the outer and inner boundaries of the multigon.
    6531163     */
    654     private static ArrayList<WayInPath> findOuterWays(Collection<Way> multigonWays) {
     1164    public static List<List<WayInPath>> findBoundaryWays(Collection<WayInPath> multigonWays, List<Way> discardedResult)
     1165    {
     1166        //first find all discardable ways, by getting outer shells.
     1167        //this will produce incorrect boundaries in some cases, but second pass will fix it.
    6551168
    656         //find the node with minimum lat - it's guaranteed to be outer. (What about the south pole?)
    657         Way bestWay = null;
    658         Node topNode = null;
    659         int topIndex = 0;
    660         double minLat = Double.POSITIVE_INFINITY;
     1169        List<WayInPath> discardedWays = new ArrayList<WayInPath>();
     1170        Set<WayInPath> processedWays = new HashSet<WayInPath>();
     1171        WayTraverser traverser = new WayTraverser(multigonWays);
    6611172
    662         for(Way way: multigonWays) {
    663             for (int pos = 0; pos < way.getNodesCount(); pos ++) {
    664                 Node node = way.getNode(pos);
    6651173
    666                 if (node.getCoor().lat() < minLat) {
    667                     minLat = node.getCoor().lat();
    668                     bestWay = way;
    669                     topNode = node;
    670                     topIndex = pos;
    671                 }
     1174        for( WayInPath startWay: multigonWays)
     1175        {
     1176            if (processedWays.contains(startWay)) {
     1177                continue;
    6721178            }
    673         }
    6741179
    675         //get two final nodes from best way to mark as starting point and orientation.
    676         Node headNode = null;
    677         Node prevNode = null;
     1180            traverser.startNewWay(startWay);
    6781181
    679         if (topNode.equals(bestWay.firstNode()) || topNode.equals(bestWay.lastNode())) {
    680             //node is in split point
    681             headNode = topNode;
    682             //make a fake node that is downwards from head node (smaller latitude). It will be a division point between paths.
    683             prevNode = new Node(new LatLon(headNode.getCoor().lat() - 1000, headNode.getCoor().lon()));
    684         } else {
    685             //node is inside way - pick the clockwise going end.
    686             Node prev = bestWay.getNode(topIndex - 1);
    687             Node next = bestWay.getNode(topIndex + 1);
     1182            List<WayInPath> boundary = new ArrayList<WayInPath>();
     1183            WayInPath lastWay = startWay;
    6881184
    689             if (angleIsClockwise(prev, topNode, next)) {
    690                 headNode = bestWay.lastNode();
    691                 prevNode = bestWay.getNode(bestWay.getNodesCount() - 2);
     1185            while (true) {
     1186                boundary.add(lastWay);
     1187
     1188                WayInPath bestWay = traverser.advanceNextLeftmostWay();
     1189                boolean wayInsideToTheRight = bestWay == null ? false: traverser.isLastWayInsideToTheRight();
     1190
     1191                if (bestWay == null || processedWays.contains(bestWay) || !wayInsideToTheRight) {
     1192                    //bad segment chain - proceed to discard it
     1193                    lastWay = null;
     1194                    break;
     1195                } else if (boundary.contains(bestWay)){
     1196                    //traversed way found - close the way
     1197                    lastWay = bestWay;
     1198                    break;
     1199                } else {
     1200                    //proceed to next segment
     1201                    lastWay = bestWay;
     1202                }
    6921203            }
    693             else {
    694                 headNode = bestWay.firstNode();
    695                 prevNode = bestWay.getNode(1);
     1204
     1205            if (lastWay != null)
     1206            {
     1207                //way good
     1208                processedWays.addAll(boundary);
     1209
     1210                //remove junk segments at the start
     1211                while (boundary.get(0) != lastWay)
     1212                {
     1213                    discardedWays.add(boundary.get(0));
     1214                    boundary.remove(0);
     1215                }
    6961216            }
     1217            else
     1218            {
     1219                //way bad
     1220                discardedWays.addAll(boundary);
     1221                processedWays.addAll(boundary);
     1222            }
    6971223        }
    6981224
    699         Set<Way> outerWays = new HashSet<Way>();
    700         ArrayList<WayInPath> result = new ArrayList<WayInPath>();
     1225        //now we have removed junk segments, collect the real result ways
    7011226
    702         //iterate till full circle is reached
    703         while (true) {
     1227        traverser.removeWays(discardedWays);
    7041228
    705             bestWay = null;
    706             Node bestWayNextNode = null;
    707             boolean bestWayReverse = false;
     1229        List<List<WayInPath>> result = new ArrayList<List<WayInPath>>();
    7081230
    709             for (Way way: multigonWays) {
    710                 boolean wayReverse;
    711                 Node nextNode;
     1231        while(traverser.hasWays()){
    7121232
    713                 if (way.firstNode().equals(headNode)) {
    714                     //start adjacent to headNode
    715                     nextNode = way.getNode(1);
    716                     wayReverse = false;
     1233            WayInPath startWay = traverser.startNewWay();
     1234            List<WayInPath> boundary = new ArrayList<WayInPath>();
     1235            WayInPath curWay = startWay;
    7171236
    718                     if (nextNode.equals(prevNode)) {
    719                         //this is the path we came from - ignore it.
    720                     } else if (bestWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) {
    721                         //the new way is better
    722                         bestWay = way;
    723                         bestWayReverse = wayReverse;
    724                         bestWayNextNode = nextNode;
    725                     }
    726                 }
     1237            do{
     1238                boundary.add(curWay);
     1239                curWay = traverser.advanceNextRightmostWay();
    7271240
    728                 if (way.lastNode().equals(headNode)) {
    729                     //end adjacent to headNode
    730                     nextNode = way.getNode(way.getNodesCount() - 2);
    731                     wayReverse = true;
     1241                //should not happen
     1242                if (curWay == null || !traverser.isLastWayInsideToTheRight())
     1243                    throw new RuntimeException("Join areas internal error.");
    7321244
    733                     if (nextNode.equals(prevNode)) {
    734                         //this is the path we came from - ignore it.
    735                     } else if (bestWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) {
    736                         //the new way is better
    737                         bestWay = way;
    738                         bestWayReverse = wayReverse;
    739                         bestWayNextNode = nextNode;
    740                     }
    741                 }
    742             }
     1245            } while (curWay != startWay);
    7431246
    744             if (bestWay == null)
    745                 throw new RuntimeException();
    746             else if (outerWays.contains(bestWay)) {
    747                 break; //full circle reached, terminate.
    748             } else {
    749                 //add to outer ways, repeat.
    750                 outerWays.add(bestWay);
    751                 result.add(new WayInPath(bestWay, bestWayReverse));
    752                 headNode = bestWayReverse ? bestWay.firstNode() : bestWay.lastNode();
    753                 prevNode = bestWayReverse ? bestWay.getNode(1) : bestWay.getNode(bestWay.getNodesCount() - 2);
    754             }
     1247            //build result
     1248            traverser.removeWays(boundary);
     1249            result.add(boundary);
    7551250        }
    7561251
     1252        for(WayInPath way:discardedWays){
     1253            discardedResult.add(way.way);
     1254        }
     1255
    7571256        return result;
    7581257    }
    7591258
     1259
    7601260    /**
    7611261     * Tests if given point is to the right side of path consisting of 3 points.
    7621262     * @param lineP1 first point in path
     
    7841284     * @param secondNode second vector end node
    7851285     * @return true if first vector is clockwise before second vector.
    7861286     */
     1287
    7871288    public static boolean angleIsClockwise(Node commonNode, Node firstNode, Node secondNode)
    7881289    {
    789         double dla1 = (firstNode.getCoor().lat() - commonNode.getCoor().lat());
    790         double dla2 = (secondNode.getCoor().lat() - commonNode.getCoor().lat());
    791         double dlo1 = (firstNode.getCoor().lon() - commonNode.getCoor().lon());
    792         double dlo2 = (secondNode.getCoor().lon() - commonNode.getCoor().lon());
     1290        double dy1 = (firstNode.getEastNorth().getY() - commonNode.getEastNorth().getY());
     1291        double dy2 = (secondNode.getEastNorth().getY() - commonNode.getEastNorth().getY());
     1292        double dx1 = (firstNode.getEastNorth().getX() - commonNode.getEastNorth().getX());
     1293        double dx2 = (secondNode.getEastNorth().getX() - commonNode.getEastNorth().getX());
    7931294
    794         return dla1 * dlo2 - dlo1 * dla2 > 0;
     1295        return dy1 * dx2 - dx1 * dy2 > 0;
    7951296    }
    7961297
     1298
    7971299    /**
     1300     * Tests if way is inside other way
     1301     * @param outside
     1302     * @param inside
     1303     * @return
     1304     */
     1305    public static boolean wayInsideWay(Way inside, Way outside)
     1306    {
     1307        for(Node insideNode: inside.getNodes()){
     1308
     1309            if (!outside.getNodes().contains(insideNode))
     1310                //simply test the one node
     1311                return nodeInsidePolygon(insideNode, outside.getNodes());
     1312        }
     1313
     1314        //all nodes shared.
     1315        return false;
     1316    }
     1317
     1318    /**
    7981319     * Tests if point is inside a polygon. The polygon can be self-intersecting. In such case the contains function works in xor-like manner.
    7991320     * @param polygonNodes list of nodes from polygon path.
    8001321     * @param point the point to test
    8011322     * @return true if the point is inside polygon.
    8021323     * FIXME: this should probably be moved to tools..
    8031324     */
    804     public static boolean nodeInsidePolygon(ArrayList<Node> polygonNodes, Node point)
     1325    public static boolean nodeInsidePolygon(Node point, List<Node> polygonNodes)
    8051326    {
    8061327        if (polygonNodes.size() < 3)
    8071328            return false;
     
    8201341            }
    8211342
    8221343            //order points so p1.lat <= p2.lat;
    823             if (newPoint.getCoor().lat() > oldPoint.getCoor().lat())
     1344            if (newPoint.getEastNorth().getY() > oldPoint.getEastNorth().getY())
    8241345            {
    8251346                p1 = oldPoint;
    8261347                p2 = newPoint;
     
    8321353            }
    8331354
    8341355            //test if the line is crossed and if so invert the inside flag.
    835             if ((newPoint.getCoor().lat() < point.getCoor().lat()) == (point.getCoor().lat() <= oldPoint.getCoor().lat())
    836                     && (point.getCoor().lon() - p1.getCoor().lon()) * (p2.getCoor().lat() - p1.getCoor().lat())
    837                     < (p2.getCoor().lon() - p1.getCoor().lon()) * (point.getCoor().lat() - p1.getCoor().lat()))
     1356            if ((newPoint.getEastNorth().getY() < point.getEastNorth().getY()) == (point.getEastNorth().getY() <= oldPoint.getEastNorth().getY())
     1357                    && (point.getEastNorth().getX() - p1.getEastNorth().getX()) * (p2.getEastNorth().getY() - p1.getEastNorth().getY())
     1358                    < (p2.getEastNorth().getX() - p1.getEastNorth().getX()) * (point.getEastNorth().getY() - p1.getEastNorth().getY()))
    8381359            {
    8391360                inside = !inside;
    8401361            }
     
    8451366        return inside;
    8461367    }
    8471368
     1369
    8481370    /**
     1371     * Joins the lists of ways.
     1372     * @param Collection<Way> The list of outer ways that belong to that multigon.
     1373     * @return Way The newly created outer way
     1374     */
     1375    private ArrayList<Way> joinBoundaries(List<List<WayInPath>> outerWays) {
     1376        ArrayList<Way> result = new ArrayList<Way>();
     1377
     1378        for(List<WayInPath> ways: outerWays)
     1379        {
     1380            Way boundary = joinOuterWays(ways);
     1381            result.add(boundary);
     1382        }
     1383
     1384        return result;
     1385    }
     1386
     1387
     1388    /**
    8491389     * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway.
    8501390     * @param Collection<Way> The list of outer ways that belong to that multigon.
    8511391     * @return Way The newly created outer way
    8521392     */
    853     private Way joinOuterWays(ArrayList<WayInPath> outerWays) {
     1393    private Way joinOuterWays(List<WayInPath> outerWays) {
    8541394
    8551395        //leave original orientation, if all paths are reverse.
    8561396        boolean allReverse = true;
    857         for(WayInPath way: outerWays) {
    858             allReverse &= way.insideToTheLeft;
     1397        for(WayInPath way: outerWays){
     1398            allReverse &= !way.insideToTheRight;
    8591399        }
    8601400
    861         if (allReverse) {
     1401        if (allReverse){
    8621402            for(WayInPath way: outerWays){
    863                 way.insideToTheLeft = !way.insideToTheLeft;
     1403                way.insideToTheRight = !way.insideToTheRight;
    8641404            }
    8651405        }
    8661406
    8671407        commitCommands(marktr("Join Areas: Remove Short Ways"));
     1408
    8681409        Way joinedWay = joinOrientedWays(outerWays);
    8691410        if (joinedWay != null)
    8701411            return closeWay(joinedWay);
     
    8931434     * @param ArrayList<Way> The list of ways to join and reverse
    8941435     * @return Way The newly created way
    8951436     */
    896     private Way joinOrientedWays(ArrayList<WayInPath> ways) {
    897         if(ways.size() < 2)
    898             return ways.get(0).way;
     1437    private Way joinOrientedWays(List<WayInPath> ways) {
     1438        if(ways.size() < 2) return ways.get(0).way;
    8991439
    9001440        // This will turn ways so all of them point in the same direction and CombineAction won't bug
    9011441        // the user about this.
    9021442
     1443        //TODO: ReverseWay and Combine way are really slow and we use them a lot here. This slows down large joins.
     1444
    9031445        List<Way> actionWays = new ArrayList<Way>(ways.size());
    9041446
    9051447        for(WayInPath way : ways) {
    9061448            actionWays.add(way.way);
    9071449
    908             if (way.insideToTheLeft) {
     1450            if (!way.insideToTheRight)
     1451            {
    9091452                Main.main.getCurrentDataSet().setSelected(way.way);
    9101453                new ReverseWayAction().actionPerformed(null);
    9111454                cmdsCount++;
     
    9201463        return result;
    9211464    }
    9221465
    923     /**
    924      * Joins a list of ways (using CombineWayAction and ReverseWayAction if necessary to quiet the former)
    925      * @param ArrayList<Way> The list of ways to join
    926      * @return Way The newly created way
    927      */
    928     private Way joinWays(ArrayList<Way> ways) {
    929         if(ways.size() < 2)
    930             return ways.get(0);
    9311466
    932         // This will turn ways so all of them point in the same direction and CombineAction won't bug
    933         // the user about this.
    934         Way a = null;
    935         for (Way b : ways) {
    936             if(a == null) {
    937                 a = b;
    938                 continue;
    939             }
    940             if(a.getNode(0).equals(b.getNode(0)) ||
    941                     a.getNode(a.getNodesCount()-1).equals(b.getNode(b.getNodesCount()-1))) {
    942                 Main.main.getCurrentDataSet().setSelected(b);
    943                 new ReverseWayAction().actionPerformed(null);
    944                 cmdsCount++;
    945             }
    946             a = b;
    947         }
    948         if ((a = new CombineWayAction().combineWays(ways)) != null) {
    949             cmdsCount++;
    950         }
    951         return a;
    952     }
    953 
    9541467    /**
    955      * Finds all ways that may be part of a multipolygon relation and removes them from the given list.
    956      * It will automatically combine "good" ways
    957      * @param Collection<Way> The list of inner ways to check
    958      * @param Way The newly created outer way
    959      * @return ArrayList<Way> The List of newly created inner ways
    960      */
    961     private ArrayList<Way> fixMultipolygons(Collection<Way> uninterestingWays, Way outerWay, boolean selfintersect) {
    962         Collection<Node> innerNodes = getNodesFromWays(uninterestingWays);
    963         Collection<Node> outerNodes = outerWay.getNodes();
    964 
    965         // The newly created inner ways. uninterestingWays is passed by reference and therefore modified in-place
    966         ArrayList<Way> newInnerWays = new ArrayList<Way>();
    967 
    968         // Now we need to find all inner ways that contain a remaining node, but no outer nodes
    969         // Remaining nodes are those that contain to more than one way. All nodes that belong to an
    970         // inner multigon part will have at least two ways, so we can use this to find which ways do
    971         // belong to the multigon.
    972         ArrayList<Way> possibleWays = new ArrayList<Way>();
    973         wayIterator: for(Way w : uninterestingWays) {
    974             boolean hasInnerNodes = false;
    975             for(Node n : w.getNodes()) {
    976                 if(outerNodes.contains(n)) {
    977                     if(!selfintersect) { // allow outer point for self intersection
    978                         continue wayIterator;
    979                     }
    980                 }
    981                 else if(!hasInnerNodes && innerNodes.contains(n)) {
    982                     hasInnerNodes = true;
    983                 }
    984             }
    985             if(!hasInnerNodes || w.getNodesCount() < 2) {
    986                 continue;
    987             }
    988             possibleWays.add(w);
    989         }
    990 
    991         // This removes unnecessary ways that might have been added.
    992         removeAlmostAlikeWays(possibleWays);
    993 
    994         // loop twice
    995         // in k == 0 prefer ways which allow no Y-joining (i.e. which have only 1 solution)
    996         for(int k = 0; k < 2; ++k)
    997         {
    998             // Join all ways that have one start/ending node in common
    999             Way joined = null;
    1000             outerIterator: do {
    1001                 removePartlyUnconnectedWays(possibleWays);
    1002                 joined = null;
    1003                 for(Way w1 : possibleWays) {
    1004                     if(w1.isClosed()) {
    1005                         if(!wayIsCollapsed(w1)) {
    1006                             uninterestingWays.remove(w1);
    1007                             newInnerWays.add(w1);
    1008                         }
    1009                         joined = w1;
    1010                         possibleWays.remove(w1);
    1011                         continue outerIterator;
    1012                     }
    1013                     ArrayList<Way> secondary = new ArrayList<Way>();
    1014                     for(Way w2 : possibleWays) {
    1015                         int i = 0;
    1016                         // w2 cannot be closed, otherwise it would have been removed above
    1017                         if(w1.equals(w2)) {
    1018                             continue;
    1019                         }
    1020                         if(w2.isFirstLastNode(w1.firstNode())) {
    1021                             ++i;
    1022                         }
    1023                         if(w2.isFirstLastNode(w1.lastNode())) {
    1024                             ++i;
    1025                         }
    1026                         if(i == 2) // this way closes w1 - take it!
    1027                         {
    1028                             if(secondary.size() > 0) {
    1029                                 secondary.clear();
    1030                             }
    1031                             secondary.add(w2);
    1032                             break;
    1033                         }
    1034                         else if(i > 0) {
    1035                             secondary.add(w2);
    1036                         }
    1037                     }
    1038                     if(k == 0 ? secondary.size() == 1 : secondary.size() > 0)
    1039                     {
    1040                         ArrayList<Way> joinThem = new ArrayList<Way>();
    1041                         joinThem.add(w1);
    1042                         joinThem.add(secondary.get(0));
    1043                         // Although we joined the ways, we cannot simply assume that they are closed
    1044                         if((joined = joinWays(joinThem)) != null)
    1045                         {
    1046                             uninterestingWays.removeAll(joinThem);
    1047                             possibleWays.removeAll(joinThem);
    1048 
    1049                             //List<Node> nodes = joined.getNodes();
    1050                             // check if we added too much
    1051                             /*for(int i = 1; i < nodes.size()-2; ++i)
    1052                             {
    1053                                 if(nodes.get(i) == nodes.get(nodes.size()-1))
    1054                                     System.out.println("Joining of ways produced unexpecteded result\n");
    1055                             }*/
    1056                             uninterestingWays.add(joined);
    1057                             possibleWays.add(joined);
    1058                             continue outerIterator;
    1059                         }
    1060                     }
    1061                 }
    1062             } while(joined != null);
    1063         }
    1064         return newInnerWays;
    1065     }
    1066 
    1067     /**
    1068      * Removes almost alike ways (= ways that are on top of each other for all nodes)
    1069      * @param ArrayList<Way> the ways to remove almost-duplicates from
    1070      */
    1071     private void removeAlmostAlikeWays(ArrayList<Way> ways) {
    1072         Collection<Way> removables = new ArrayList<Way>();
    1073         outer: for(int i=0; i < ways.size(); i++) {
    1074             Way a = ways.get(i);
    1075             for(int j=i+1; j < ways.size(); j++) {
    1076                 Way b = ways.get(j);
    1077                 List<Node> revNodes = new ArrayList<Node>(b.getNodes());
    1078                 Collections.reverse(revNodes);
    1079                 if(a.getNodes().equals(b.getNodes()) || a.getNodes().equals(revNodes)) {
    1080                     removables.add(a);
    1081                     continue outer;
    1082                 }
    1083             }
    1084         }
    1085         ways.removeAll(removables);
    1086     }
    1087 
    1088     /**
    1089      * Removes ways from the given list whose starting or ending node doesn't
    1090      * connect to other ways from the same list (it's like removing spikes).
    1091      * @param ArrayList<Way> The list of ways to remove "spikes" from
    1092      */
    1093     private void removePartlyUnconnectedWays(ArrayList<Way> ways) {
    1094         List<Way> removables = new ArrayList<Way>();
    1095         for(Way a : ways) {
    1096             if(a.isClosed()) {
    1097                 continue;
    1098             }
    1099             boolean connectedStart = false;
    1100             boolean connectedEnd = false;
    1101             for(Way b : ways) {
    1102                 if(a.equals(b)) {
    1103                     continue;
    1104                 }
    1105                 if(b.isFirstLastNode(a.firstNode())) {
    1106                     connectedStart = true;
    1107                 }
    1108                 if(b.isFirstLastNode(a.lastNode())) {
    1109                     connectedEnd = true;
    1110                 }
    1111             }
    1112             if(!connectedStart || !connectedEnd) {
    1113                 removables.add(a);
    1114             }
    1115         }
    1116         ways.removeAll(removables);
    1117     }
    1118 
    1119     /**
    1120      * Checks if a way is collapsed (i.e. looks like <---->)
    1121      * @param Way A *closed* way to check if it is collapsed
    1122      * @return boolean If the closed way is collapsed or not
    1123      */
    1124     private boolean wayIsCollapsed(Way w) {
    1125         if(w.getNodesCount() <= 3) return true;
    1126 
    1127         // If a way contains more than one node twice, it must be collapsed (only start/end node may be the same)
    1128         Way x = new Way(w);
    1129         int count = 0;
    1130         for(Node n : w.getNodes()) {
    1131             x.removeNode(n);
    1132             if(x.containsNode(n)) {
    1133                 count++;
    1134             }
    1135             if(count == 2) return true;
    1136         }
    1137         return false;
    1138     }
    1139 
    1140     /**
    11411468     * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations
    11421469     * @param Collection<Way> List of already closed inner ways
    11431470     * @param Way The outer way
     
    12701597    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
    12711598        setEnabled(selection != null && !selection.isEmpty());
    12721599    }
     1600
    12731601}
  • 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}