Ticket #14528: 14528-rework-join-areas.patch

File 14528-rework-join-areas.patch, 100.1 KB (added by michael2402, 8 years ago)
  • src/org/openstreetmap/josm/actions/JoinAreasAction.java

     
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.actions;
    33
    4 import static org.openstreetmap.josm.tools.I18n.marktr;
    54import static org.openstreetmap.josm.tools.I18n.tr;
    65import static org.openstreetmap.josm.tools.I18n.trn;
    76
     7import java.awt.GridBagLayout;
    88import java.awt.event.ActionEvent;
    99import java.awt.event.KeyEvent;
    1010import java.util.ArrayList;
     
    1111import java.util.Collection;
    1212import java.util.Collections;
    1313import java.util.Comparator;
    14 import java.util.EnumSet;
     14import java.util.HashMap;
    1515import java.util.HashSet;
    16 import java.util.LinkedHashSet;
    1716import java.util.LinkedList;
    1817import java.util.List;
    1918import java.util.Map;
    20 import java.util.Objects;
    2119import java.util.Optional;
    22 import java.util.Set;
    23 import java.util.TreeMap;
    24 import java.util.function.BiConsumer;
    25 import java.util.function.BinaryOperator;
    26 import java.util.function.Function;
    27 import java.util.function.Supplier;
    2820import java.util.function.ToDoubleFunction;
    29 import java.util.stream.Collector;
    3021import java.util.stream.Collectors;
    3122import java.util.stream.Stream;
    3223
    3324import javax.swing.JOptionPane;
     25import javax.swing.JPanel;
    3426
    3527import org.openstreetmap.josm.Main;
    36 import org.openstreetmap.josm.actions.ReverseWayAction.ReverseWayResult;
    37 import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult;
    3828import org.openstreetmap.josm.command.AddCommand;
    3929import org.openstreetmap.josm.command.ChangeCommand;
     30import org.openstreetmap.josm.command.ChangePropertyCommand;
    4031import org.openstreetmap.josm.command.Command;
    4132import org.openstreetmap.josm.command.DeleteCommand;
    4233import org.openstreetmap.josm.command.SequenceCommand;
    43 import org.openstreetmap.josm.data.UndoRedoHandler;
    4434import org.openstreetmap.josm.data.coor.EastNorth;
    4535import org.openstreetmap.josm.data.osm.DataSet;
    4636import org.openstreetmap.josm.data.osm.Node;
    47 import org.openstreetmap.josm.data.osm.NodePositionComparator;
    4837import org.openstreetmap.josm.data.osm.OsmPrimitive;
     38import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
    4939import org.openstreetmap.josm.data.osm.Relation;
    50 import org.openstreetmap.josm.data.osm.RelationMember;
    5140import org.openstreetmap.josm.data.osm.TagCollection;
    5241import org.openstreetmap.josm.data.osm.Way;
     42import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
     43import org.openstreetmap.josm.gui.DefaultNameFormatter;
    5344import org.openstreetmap.josm.gui.Notification;
    5445import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
    55 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     46import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
    5647import org.openstreetmap.josm.tools.Geometry;
    57 import org.openstreetmap.josm.tools.JosmRuntimeException;
    5848import org.openstreetmap.josm.tools.Pair;
    5949import org.openstreetmap.josm.tools.Shortcut;
    6050import org.openstreetmap.josm.tools.UserCancelException;
    61 import org.openstreetmap.josm.tools.Utils;
    6251import org.openstreetmap.josm.tools.bugreport.BugReport;
    6352
    6453/**
     
    6655 * @since 2575
    6756 */
    6857public class JoinAreasAction extends JosmAction {
    69     // This will be used to commit commands and unite them into one large command sequence at the end
    70     private final transient LinkedList<Command> cmds = new LinkedList<>();
    71     private int cmdsCount;
    72     private final transient List<Relation> addedRelations = new LinkedList<>();
    73 
    7458    /**
    75      * This helper class describes join areas action result.
    76      * @author viesturs
     59     * Defines an exception while joining areas.
     60     * @author Michael Zangl
    7761     */
    78     public static class JoinAreasResult {
    79 
    80         private final boolean hasChanges;
    81         private final List<Multipolygon> polygons;
    82 
    83         /**
    84          * Constructs a new {@code JoinAreasResult}.
    85          * @param hasChanges whether the result has changes
    86          * @param polygons the result polygons, can be null
    87          */
    88         public JoinAreasResult(boolean hasChanges, List<Multipolygon> polygons) {
    89             this.hasChanges = hasChanges;
    90             this.polygons = polygons;
     62    public static class JoinAreasException extends Exception {
     63        protected JoinAreasException(String message) {
     64            super(message);
    9165        }
     66    }
    9267
    93         /**
    94          * Determines if the result has changes.
    95          * @return {@code true} if the result has changes
    96          */
    97         public final boolean hasChanges() {
    98             return hasChanges;
    99         }
     68    static class UnclosedAreaException extends JoinAreasException {
     69        private Pair<Node, Node> gap;
    10070
    101         /**
    102          * Returns the result polygons, can be null.
    103          * @return the result polygons, can be null
    104          */
    105         public final List<Multipolygon> getPolygons() {
    106             return polygons;
     71        public UnclosedAreaException(Pair<Node, Node> gap) {
     72            super("Gap found between: " + gap.a + " and " + gap.b);
     73            this.gap = gap;
    10774        }
    10875    }
    10976
    110     public static class Multipolygon {
    111         private final Way outerWay;
    112         private final List<Way> innerWays;
     77    static class SelfIntersectingAreaException extends JoinAreasException {
     78        private Pair<UndirectedWaySegment, UndirectedWaySegment> intersect;
    11379
    114         /**
    115          * Constructs a new {@code Multipolygon}.
    116          * @param way outer way
    117          */
    118         public Multipolygon(Way way) {
    119             outerWay = way;
    120             innerWays = new ArrayList<>();
     80        public SelfIntersectingAreaException(Pair<UndirectedWaySegment, UndirectedWaySegment> intersect) {
     81            super("Intersection found between: " + intersect.a + " and " + intersect.b);
     82            this.intersect = intersect;
    12183        }
     84    }
    12285
    123         /**
    124          * Returns the outer way.
    125          * @return the outer way
    126          */
    127         public final Way getOuterWay() {
    128             return outerWay;
    129         }
     86    static class UndirectedWaySegment {
     87        private Node a;
     88        private Node b;
    13089
    131         /**
    132          * Returns the inner ways.
    133          * @return the inner ways
    134          */
    135         public final List<Way> getInnerWays() {
    136             return innerWays;
     90        UndirectedWaySegment(Node a, Node b) {
     91            if (a == b) {
     92                throw new IllegalArgumentException("Way segment cannot start and end at the same node.");
     93            }
     94            this.a = a;
     95            this.b = b;
    13796        }
    138     }
    13997
    140     // HelperClass
    141     // Saves a relation and a role an OsmPrimitve was part of until it was stripped from all relations
    142     private static class RelationRole {
    143         public final Relation rel;
    144         public final String role;
    145 
    146         RelationRole(Relation rel, String role) {
    147             this.rel = rel;
    148             this.role = role;
     98        public boolean hasEnd(Node current) {
     99            return a == current || b == current;
    149100        }
    150101
    151         @Override
    152         public int hashCode() {
    153             return Objects.hash(rel, role);
     102        public Node getOtherEnd(Node current) {
     103            if (current == a) {
     104                return b;
     105            } else if (current == b) {
     106                return a;
     107            } else {
     108                throw new IllegalArgumentException(current + " is not an endpoint");
     109            }
    154110        }
    155111
    156         @Override
    157         public boolean equals(Object other) {
    158             if (this == other) return true;
    159             if (other == null || getClass() != other.getClass()) return false;
    160             RelationRole that = (RelationRole) other;
    161             return Objects.equals(rel, that.rel) &&
    162                     Objects.equals(role, that.role);
     112        public boolean intersects(UndirectedWaySegment other) {
     113            EastNorth intersection = getIntersectionPoint(other);
     114            return intersection != null;
    163115        }
    164     }
    165116
    166     /**
    167      * HelperClass - saves a way and the "inside" side.
    168      *
    169      * insideToTheLeft: if true left side is "in", false -right side is "in".
    170      * Left and right are determined along the orientation of way.
    171      */
    172     public static class WayInPolygon {
    173         public final Way way;
    174         public boolean insideToTheRight;
    175 
    176         public WayInPolygon(Way way, boolean insideRight) {
    177             this.way = way;
    178             this.insideToTheRight = insideRight;
     117        private EastNorth getIntersectionPoint(UndirectedWaySegment other) {
     118            EastNorth intersection = null;
     119            if (!hasEnd(other.a) && !hasEnd(other.b)) {
     120                // ignore just touching.
     121                intersection = Geometry.getSegmentSegmentIntersection(
     122                        a.getEastNorth(), b.getEastNorth(),
     123                        other.a.getEastNorth(), other.b.getEastNorth());
     124            }
     125            return intersection;
    179126        }
    180127
    181128        @Override
    182129        public int hashCode() {
    183             return Objects.hash(way, insideToTheRight);
     130            return a.hashCode() + b.hashCode();
    184131        }
    185132
    186133        @Override
    187         public boolean equals(Object other) {
    188             if (this == other) return true;
    189             if (other == null || getClass() != other.getClass()) return false;
    190             WayInPolygon that = (WayInPolygon) other;
    191             return insideToTheRight == that.insideToTheRight &&
    192                     Objects.equals(way, that.way);
     134        public boolean equals(Object obj) {
     135            if (this.getClass() == obj.getClass()) {
     136                UndirectedWaySegment other = (UndirectedWaySegment) obj;
     137                return (a.equals(other.a) && b.equals(other.b)) || (a.equals(other.b) && b.equals(other.a));
     138            } else {
     139                return false;
     140            }
    193141        }
    194142
    195143        @Override
    196144        public String toString() {
    197             return "WayInPolygon [way=" + way + ", insideToTheRight=" + insideToTheRight + "]";
     145            return "UndirectedWaySegment [" + a + ", " + b + "]";
    198146        }
     147
    199148    }
    200149
    201150    /**
    202      * This helper class describes a polygon, assembled from several ways.
    203      * @author viesturs
    204      *
     151     * This class defines an area that might be joined.
     152     * @author Michael Zangl
    205153     */
    206     public static class AssembledPolygon {
    207         public List<WayInPolygon> ways;
    208 
    209         public AssembledPolygon(List<WayInPolygon> boundary) {
    210             this.ways = boundary;
    211         }
    212 
    213         public List<Node> getNodes() {
    214             List<Node> nodes = new ArrayList<>();
    215             for (WayInPolygon way : this.ways) {
    216                 //do not add the last node as it will be repeated in the next way
    217                 if (way.insideToTheRight) {
    218                     for (int pos = 0; pos < way.way.getNodesCount() - 1; pos++) {
    219                         nodes.add(way.way.getNode(pos));
    220                     }
    221                 } else {
    222                     for (int pos = way.way.getNodesCount() - 1; pos > 0; pos--) {
    223                         nodes.add(way.way.getNode(pos));
    224                     }
    225                 }
    226             }
    227 
    228             return nodes;
    229         }
    230 
     154    static class JoinableArea {
    231155        /**
    232          * Inverse inside and outside
     156         * A list of Node->Node segments that compose this area.
     157         * You can reconstruct the interior of this area by XORing those lines.
    233158         */
    234         public void reverse() {
    235             for (WayInPolygon way: ways) {
    236                 way.insideToTheRight = !way.insideToTheRight;
    237             }
    238             Collections.reverse(ways);
    239         }
    240     }
     159        private final HashSet<UndirectedWaySegment> waySegments = new HashSet<>();
     160        private final List<Way> ways = new ArrayList<>();
     161        private final List<Relation> relations = new ArrayList<>();
     162        private final Map<String, String> tags;
     163        private final OsmPrimitive basePrimitive;
    241164
    242     public static class AssembledMultipolygon {
    243         public AssembledPolygon outerWay;
    244         public List<AssembledPolygon> innerWays;
    245 
    246         public AssembledMultipolygon(AssembledPolygon way) {
    247             outerWay = way;
    248             innerWays = new ArrayList<>();
     165        JoinableArea(Way way) throws JoinAreasException {
     166            this(way, Collections.singleton(way), Collections.emptyList());
    249167        }
    250     }
    251168
    252     /**
    253      * This hepler class implements algorithm traversing trough connected ways.
    254      * Assumes you are going in clockwise orientation.
    255      * @author viesturs
    256      */
    257     private static class WayTraverser {
    258 
    259         /** Set of {@link WayInPolygon} to be joined by walk algorithm */
    260         private final List<WayInPolygon> availableWays;
    261         /** Current state of walk algorithm */
    262         private WayInPolygon lastWay;
    263         /** Direction of current way */
    264         private boolean lastWayReverse;
    265 
    266         /** Constructor
    267          * @param ways available ways
    268          */
    269         WayTraverser(Collection<WayInPolygon> ways) {
    270             availableWays = new ArrayList<>(ways);
    271             lastWay = null;
     169        JoinableArea(Relation relation) throws JoinAreasException {
     170            this(relation, getMembers(relation, "outer"), getMembers(relation, "inner"));
     171            relations.add(relation);
    272172        }
    273173
    274174        /**
    275          *  Remove ways from available ways
    276          *  @param ways Collection of WayInPolygon
     175         * Creates a new joinable area.
     176         * @param base The primitive this area is for.
     177         * @param outer The ways that should be outer ways.
     178         * @param inner The ways that should be inner ways.
     179         * @throws JoinAreasException If the area is invalid
    277180         */
    278         public void removeWays(Collection<WayInPolygon> ways) {
    279             availableWays.removeAll(ways);
    280         }
     181        JoinableArea(OsmPrimitive base, Collection<Way> outer, Collection<Way> inner) throws JoinAreasException {
     182            basePrimitive = base;
     183            tags = new HashMap<>(base.getInterestingTags());
     184            tags.remove("type", "multipolygon");
    281185
    282         /**
    283          * Remove a single way from available ways
    284          * @param way WayInPolygon
    285          */
    286         public void removeWay(WayInPolygon way) {
    287             availableWays.remove(way);
    288         }
     186            try {
     187                for (Way o : outer) {
     188                    addWayForceNonintersecting(o);
     189                }
     190                Pair<Node, Node> outerGap = findGap();
     191                if (outerGap != null) {
     192                    throw new UnclosedAreaException(outerGap);
     193                }
    289194
    290         /**
    291          * Reset walk algorithm to a new start point
    292          * @param way New start point
    293          */
    294         public void setStartWay(WayInPolygon way) {
    295             lastWay = way;
    296             lastWayReverse = !way.insideToTheRight;
     195                for (Way i : inner) {
     196                    addWayForceNonintersecting(i);
     197                }
     198                Pair<Node, Node> innerGap = findGap();
     199                if (innerGap != null) {
     200                    throw new UnclosedAreaException(innerGap);
     201                }
     202            } catch (RuntimeException e) {
     203                throw BugReport.intercept(e).put("outer", outer).put("inner", inner);
     204            }
    297205        }
    298206
    299207        /**
    300          * Reset walk algorithm to a new start point.
    301          * @return The new start point or null if no available way remains
     208         * Check if this area is a valid closed area
     209         * @return The gap if there is one, null for closed areas.
    302210         */
    303         public WayInPolygon startNewWay() {
    304             if (availableWays.isEmpty()) {
    305                 lastWay = null;
    306             } else {
    307                 lastWay = availableWays.iterator().next();
    308                 lastWayReverse = !lastWay.insideToTheRight;
     211        private Pair<Node, Node> findGap() {
     212            HashSet<UndirectedWaySegment> leftOver = new HashSet<>(waySegments);
     213            while (!leftOver.isEmpty()) {
     214                LinkedList<Node> part = removeOutlinePart(leftOver);
     215                if (part.getFirst() != part.getLast()) {
     216                    return new Pair<>(part.getFirst(), part.getLast());
     217                }
    309218            }
    310 
    311             return lastWay;
     219            return null;
    312220        }
    313221
    314222        /**
    315          * Walking through {@link WayInPolygon} segments, head node is the current position
    316          * @return Head node
     223         * Add a new Way to the outline (outer or inner) of this area.
     224         * @param way The way.
     225         * @throws SelfIntersectingAreaException If the way self-intersects
    317226         */
    318         private Node getHeadNode() {
    319             return !lastWayReverse ? lastWay.way.lastNode() : lastWay.way.firstNode();
     227        private void addWayForceNonintersecting(Way way) throws SelfIntersectingAreaException {
     228            for (Pair<Node, Node> pair : way.getNodePairs(false)) {
     229                this.addWayForceNonintersecting(new UndirectedWaySegment(pair.a, pair.b));
     230            }
     231            ways .add(way);
    320232        }
    321233
    322         /**
    323          * Node just before head node.
    324          * @return Previous node
    325          */
    326         private Node getPrevNode() {
    327             return !lastWayReverse ? lastWay.way.getNode(lastWay.way.getNodesCount() - 2) : lastWay.way.getNode(1);
    328         }
    329 
    330         /**
    331          * Returns oriented angle (N1N2, N1N3) in range [0; 2*Math.PI[
    332          * @param n1 first node
    333          * @param n2 second node
    334          * @param n3 third node
    335          * @return oriented angle (N1N2, N1N3) in range [0; 2*Math.PI[
    336          */
    337         private static double getAngle(Node n1, Node n2, Node n3) {
    338             EastNorth en1 = n1.getEastNorth();
    339             EastNorth en2 = n2.getEastNorth();
    340             EastNorth en3 = n3.getEastNorth();
    341             double angle = Math.atan2(en3.getY() - en1.getY(), en3.getX() - en1.getX()) -
    342                     Math.atan2(en2.getY() - en1.getY(), en2.getX() - en1.getX());
    343             while (angle >= 2*Math.PI) {
    344                 angle -= 2*Math.PI;
     234        private void addWayForceNonintersecting(UndirectedWaySegment s) throws SelfIntersectingAreaException {
     235            if (waySegments.contains(s)) {
     236                // We add a way segment twice. This means that the outline of the area contains this segment twice.
     237                // This cancels out, so we remove the segment,
     238                waySegments.remove(s);
     239            } else {
     240                // Now check for intersections
     241                Optional<UndirectedWaySegment> intersection = waySegments.stream().filter(s::intersects).findAny();
     242                if (intersection.isPresent()) {
     243                    throw new SelfIntersectingAreaException(new Pair<>(intersection.get(), s));
     244                }
     245                waySegments.add(s);
    345246            }
    346             while (angle < 0) {
    347                 angle += 2*Math.PI;
    348             }
    349             return angle;
    350247        }
    351248
    352         /**
    353          * Get the next way creating a clockwise path, ensure it is the most right way. #7959
    354          * @return The next way.
    355          */
    356         public WayInPolygon walk() {
    357             Node headNode = getHeadNode();
    358             Node prevNode = getPrevNode();
    359 
    360             double headAngle = Math.atan2(headNode.getEastNorth().east() - prevNode.getEastNorth().east(),
    361                     headNode.getEastNorth().north() - prevNode.getEastNorth().north());
    362 
    363             // Pairs of (way, nextNode)
    364             lastWay = Stream.concat(
    365                 availableWays.stream()
    366                     .filter(way -> way.way.firstNode().equals(headNode) && way.insideToTheRight)
    367                     .map(way -> new Pair<>(way, way.way.getNode(1))),
    368                 availableWays.stream()
    369                     .filter(way -> way.way.lastNode().equals(headNode) && !way.insideToTheRight)
    370                     .map(way -> new Pair<>(way, way.way.getNode(way.way.getNodesCount() - 2))))
    371 
    372                 // now find the way with the best angle
    373                 .min(Comparator.comparingDouble(wayAndNext -> {
    374                     Node nextNode = wayAndNext.b;
    375                     if (nextNode == prevNode) {
    376                         // we always prefer going back.
    377                         return Double.POSITIVE_INFINITY;
    378                     }
    379                     double angle = Math.atan2(nextNode.getEastNorth().east() - headNode.getEastNorth().east(),
    380                             nextNode.getEastNorth().north() - headNode.getEastNorth().north()) - headAngle;
    381                     if (angle > Math.PI)
    382                         angle -= 2*Math.PI;
    383                     if (angle <= -Math.PI)
    384                         angle += 2*Math.PI;
    385                     return angle;
    386                 })).map(wayAndNext -> wayAndNext.a).orElse(null);
    387             lastWayReverse = lastWay != null && !lastWay.insideToTheRight;
    388             return lastWay;
     249        private static Collection<Way> getMembers(Relation relation, String role) {
     250            return relation.getMembers().stream().filter(m -> role.equals(m.getRole()))
     251                    .filter(m -> OsmPrimitiveType.WAY.equals(m.getType())).map(m -> m.getWay())
     252                    .collect(Collectors.toList());
    389253        }
    390254
    391255        /**
    392          * Search for an other way coming to the same head node at left side from last way. #9951
    393          * @return left way or null if none found
     256         * Check if the area contains a segment.
     257         * @param segment The segment. Assumed to not intersect any of our borders.
     258         * @return true if the segment is inside. False if it is on the outline or outside.
    394259         */
    395         public WayInPolygon leftComingWay() {
    396             Node headNode = getHeadNode();
    397             Node prevNode = getPrevNode();
    398 
    399             WayInPolygon mostLeft = null; // most left way connected to head node
    400             boolean comingToHead = false; // true if candidate come to head node
    401             double angle = 2*Math.PI;
    402 
    403             for (WayInPolygon candidateWay : availableWays) {
    404                 boolean candidateComingToHead;
    405                 Node candidatePrevNode;
    406 
    407                 if (candidateWay.way.firstNode().equals(headNode)) {
    408                     candidateComingToHead = !candidateWay.insideToTheRight;
    409                     candidatePrevNode = candidateWay.way.getNode(1);
    410                 } else if (candidateWay.way.lastNode().equals(headNode)) {
    411                      candidateComingToHead = candidateWay.insideToTheRight;
    412                      candidatePrevNode = candidateWay.way.getNode(candidateWay.way.getNodesCount() - 2);
    413                 } else
    414                     continue;
    415                 if (candidateComingToHead && candidateWay.equals(lastWay))
    416                     continue;
    417 
    418                 double candidateAngle = getAngle(headNode, candidatePrevNode, prevNode);
    419 
    420                 if (mostLeft == null || candidateAngle < angle || (Utils.equalsEpsilon(candidateAngle, angle) && !candidateComingToHead)) {
    421                     // Candidate is most left
    422                     mostLeft = candidateWay;
    423                     comingToHead = candidateComingToHead;
    424                     angle = candidateAngle;
    425                 }
     260        public boolean contains(UndirectedWaySegment segment) {
     261            if (waySegments.contains(segment)) {
     262                return false;
    426263            }
     264            // To find out which side of the way the outer side is, we can follow a ray starting anywhere at the way in any direction.
     265            // Computation is done in East/North space.
     266            // We use a ray at a fixed yRay coordinate that ends at xRay;
     267            // we need to make sure this ray does not go into the same direction the way is going.
     268            // This is done by rotating by 90° if we need to.
    427269
    428             return comingToHead ? mostLeft : null;
    429         }
     270            int intersections = 0;
     271            // Use some "random" start point on the segment
     272            EastNorth rayNode1 = segment.a.getEastNorth();
     273            EastNorth rayNode2 = segment.b.getEastNorth();
     274            EastNorth rayFrom = rayNode1.getCenter(rayNode2);
    430275
    431         @Override
    432         public String toString() {
    433             return "WayTraverser [availableWays=" + availableWays + ", lastWay=" + lastWay + ", lastWayReverse="
    434                     + lastWayReverse + "]";
    435         }
    436     }
    437 
    438     /**
    439      * Helper storage class for finding findOuterWays
    440      * @author viesturs
    441      */
    442     static class PolygonLevel {
    443         public final int level;
    444         public final AssembledMultipolygon pol;
    445 
    446         PolygonLevel(AssembledMultipolygon pol, int level) {
    447             this.pol = pol;
    448             this.level = level;
    449         }
    450     }
    451 
    452     /**
    453      * Constructs a new {@code JoinAreasAction}.
    454      */
    455     public JoinAreasAction() {
    456         this(true);
    457     }
    458 
    459     /**
    460      * Constructs a new {@code JoinAreasAction} with optional shortcut.
    461      * @param addShortcut controls whether the shortcut should be registered or not
    462      * @since 11611
    463      */
    464     public JoinAreasAction(boolean addShortcut) {
    465         super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"), addShortcut ?
    466         Shortcut.registerShortcut("tools:joinareas", tr("Tool: {0}", tr("Join overlapping Areas")), KeyEvent.VK_J, Shortcut.SHIFT)
    467         : null, true);
    468     }
    469 
    470     /**
    471      * Gets called whenever the shortcut is pressed or the menu entry is selected.
    472      * Checks whether the selected objects are suitable to join and joins them if so.
    473      */
    474     @Override
    475     public void actionPerformed(ActionEvent e) {
    476         join(Main.getLayerManager().getEditDataSet().getSelectedWays());
    477     }
    478 
    479     /**
    480      * Joins the given ways.
    481      * @param ways Ways to join
    482      * @since 7534
    483      */
    484     public void join(Collection<Way> ways) {
    485         addedRelations.clear();
    486 
    487         if (ways.isEmpty()) {
    488             new Notification(
    489                     tr("Please select at least one closed way that should be joined."))
    490                     .setIcon(JOptionPane.INFORMATION_MESSAGE)
    491                     .show();
    492             return;
    493         }
    494 
    495         List<Node> allNodes = new ArrayList<>();
    496         for (Way way : ways) {
    497             if (!way.isClosed()) {
    498                 new Notification(
    499                         tr("One of the selected ways is not closed and therefore cannot be joined."))
    500                         .setIcon(JOptionPane.INFORMATION_MESSAGE)
    501                         .show();
    502                 return;
     276            // Now find the x/y mapping function. We need to ensure that rayNode1->rayNode2 is not parallel to our x axis.
     277            ToDoubleFunction<EastNorth> x;
     278            ToDoubleFunction<EastNorth> y;
     279            if (Math.abs(rayNode1.east() - rayNode2.east()) < Math.abs(rayNode1.north() - rayNode2.north())) {
     280                x = en -> en.east();
     281                y = en -> en.north();
     282            } else {
     283                x = en -> -en.north();
     284                y = en -> en.east();
    503285            }
    504286
    505             allNodes.addAll(way.getNodes());
    506         }
     287            double xRay = x.applyAsDouble(rayFrom);
     288            double yRay = y.applyAsDouble(rayFrom);
    507289
    508         // TODO: Only display this warning when nodes outside dataSourceArea are deleted
    509         boolean ok = Command.checkAndConfirmOutlyingOperation("joinarea", tr("Join area confirmation"),
    510                 trn("The selected way has nodes outside of the downloaded data region.",
    511                     "The selected ways have nodes outside of the downloaded data region.",
    512                     ways.size()) + "<br/>"
    513                     + tr("This can lead to nodes being deleted accidentally.") + "<br/>"
    514                     + tr("Are you really sure to continue?")
    515                     + tr("Please abort if you are not sure"),
    516                 tr("The selected area is incomplete. Continue?"),
    517                 allNodes, null);
    518         if (!ok) return;
     290            for (UndirectedWaySegment part : waySegments) {
     291                // intersect against all way segments
     292                EastNorth n1 = part.a.getEastNorth();
     293                EastNorth n2 = part.b.getEastNorth();
     294                if ((rayNode1.equals(n1) && rayNode2.equals(n2)) || (rayNode2.equals(n1) && rayNode1.equals(n2))) {
     295                    // This is the segment we are starting the ray from.
     296                    // We ignore this to avoid rounding errors.
     297                    continue;
     298                }
    519299
    520         //analyze multipolygon relations and collect all areas
    521         List<Multipolygon> areas = collectMultipolygons(ways);
     300                double x1 = x.applyAsDouble(n1);
     301                double x2 = x.applyAsDouble(n2);
     302                double y1 = y.applyAsDouble(n1);
     303                double y2 = y.applyAsDouble(n2);
    522304
    523         if (areas == null)
    524             //too complex multipolygon relations found
    525             return;
    526 
    527         if (!testJoin(areas)) {
    528             new Notification(
    529                     tr("No intersection found. Nothing was changed."))
    530                     .setIcon(JOptionPane.INFORMATION_MESSAGE)
    531                     .show();
    532             return;
    533         }
    534 
    535         if (!resolveTagConflicts(areas))
    536             return;
    537         //user canceled, do nothing.
    538 
    539         try {
    540             // see #11026 - Because <ways> is a dynamic filtered (on ways) of a filtered (on selected objects) collection,
    541             // retrieve effective dataset before joining the ways (which affects the selection, thus, the <ways> collection)
    542             // Dataset retrieving allows to call this code without relying on Main.getCurrentDataSet(), thus, on a mapview instance
    543             DataSet ds = ways.iterator().next().getDataSet();
    544 
    545             // Do the job of joining areas
    546             JoinAreasResult result = joinAreas(areas);
    547 
    548             if (result.hasChanges) {
    549                 // move tags from ways to newly created relations
    550                 // TODO: do we need to also move tags for the modified relations?
    551                 for (Relation r: addedRelations) {
    552                     cmds.addAll(CreateMultipolygonAction.removeTagsFromWaysIfNeeded(r));
     305                if (!((y1 <= yRay && yRay < y2) || (y2 <= yRay && yRay < y1))) {
     306                    // No intersection, since segment is above/below ray
     307                    continue;
    553308                }
    554                 commitCommands(tr("Move tags from ways to relations"));
    555 
    556                 List<Way> allWays = new ArrayList<>();
    557                 for (Multipolygon pol : result.polygons) {
    558                     allWays.add(pol.outerWay);
    559                     allWays.addAll(pol.innerWays);
     309                double xIntersect = x1 + (x2 - x1) * (yRay - y1) / (y2 - y1);
     310                double onLine = xIntersect / xRay;
     311                if (Math.abs(onLine - 1) < 1e-10) {
     312                    // Lines that are directly on each other are considered outside.
     313                    return false;
    560314                }
    561                 if (ds != null) {
    562                     ds.setSelected(allWays);
     315                if (xIntersect < xRay) {
     316                    intersections++;
    563317                }
    564             } else {
    565                 new Notification(
    566                         tr("No intersection found. Nothing was changed."))
    567                         .setIcon(JOptionPane.INFORMATION_MESSAGE)
    568                         .show();
    569318            }
    570         } catch (UserCancelException exception) {
    571             Main.trace(exception);
    572             //revert changes
    573             //FIXME: this is dirty hack
    574             makeCommitsOneAction(tr("Reverting changes"));
    575             Main.main.undoRedo.undo();
    576             Main.main.undoRedo.redoCommands.clear();
    577         }
    578     }
    579319
    580     /**
    581      * Tests if the areas have some intersections to join.
    582      * @param areas Areas to test
    583      * @return {@code true} if areas are joinable
    584      */
    585     private boolean testJoin(List<Multipolygon> areas) {
    586         List<Way> allStartingWays = new ArrayList<>();
    587 
    588         for (Multipolygon area : areas) {
    589             allStartingWays.add(area.outerWay);
    590             allStartingWays.addAll(area.innerWays);
     320            return intersections % 2 == 1;
    591321        }
    592322
    593         //find intersection points
    594         Set<Node> nodes = Geometry.addIntersections(allStartingWays, true, cmds);
    595         return !nodes.isEmpty();
    596     }
    597 
    598     private static class DuplicateWayCollectorAccu {
    599            private List<Way> currentWays = new ArrayList<>();
    600            private List<Way> duplicatesFound = new ArrayList<>();
    601 
    602            private void add(Way way) {
    603                List<Node> wayNodes = way.getNodes();
    604                List<Node> wayNodesReversed = way.getNodes();
    605                Collections.reverse(wayNodesReversed);
    606                Optional<Way> duplicate = currentWays.stream()
    607                    .filter(current -> current.getNodes().equals(wayNodes) || current.getNodes().equals(wayNodesReversed))
    608                    .findFirst();
    609                if (duplicate.isPresent()) {
    610                    currentWays.remove(duplicate.get());
    611                    duplicatesFound.add(duplicate.get());
    612                    duplicatesFound.add(way);
    613                } else {
    614                    currentWays.add(way);
    615                }
    616            }
    617 
    618            private DuplicateWayCollectorAccu combine(DuplicateWayCollectorAccu a2) {
    619                duplicatesFound.addAll(a2.duplicatesFound);
    620                a2.currentWays.forEach(this::add);
    621                return this;
    622            }
    623     }
    624 
    625     /**
    626      * A collector that collects to a list of duplicated way pairs.
    627      *
    628      * It does not scale well (O(n²)), but the data base should be small enough to make this efficient.
    629      *
    630      * @author Michael Zangl
    631      */
    632     private static class DuplicateWayCollector implements Collector<Way, DuplicateWayCollectorAccu, List<Way>> {
    633         @Override
    634         public Supplier<DuplicateWayCollectorAccu> supplier() {
    635             return DuplicateWayCollectorAccu::new;
     323        public Collection<UndirectedWaySegment> getSegments() {
     324            return Collections.unmodifiableCollection(waySegments);
    636325        }
    637 
    638         @Override
    639         public BiConsumer<DuplicateWayCollectorAccu, Way> accumulator() {
    640             return DuplicateWayCollectorAccu::add;
    641         }
    642 
    643         @Override
    644         public BinaryOperator<DuplicateWayCollectorAccu> combiner() {
    645             return DuplicateWayCollectorAccu::combine;
    646         }
    647 
    648         @Override
    649         public Function<DuplicateWayCollectorAccu, List<Way>> finisher() {
    650             return a -> a.duplicatesFound;
    651         }
    652 
    653         @Override
    654         public Set<Collector.Characteristics> characteristics() {
    655             return EnumSet.of(Collector.Characteristics.UNORDERED);
    656         }
    657 
    658326    }
    659327
    660328    /**
    661      * Will join two or more overlapping areas
    662      * @param areas list of areas to join
    663      * @return new area formed.
    664      * @throws UserCancelException if user cancels the operation
     329     * A hash set with an xor method.
     330     * @param <T> element type
    665331     */
    666     public JoinAreasResult joinAreas(List<Multipolygon> areas) throws UserCancelException {
    667 
    668         boolean hasChanges = false;
    669 
    670         List<Way> allStartingWays = new ArrayList<>();
    671         List<Way> innerStartingWays = new ArrayList<>();
    672         List<Way> outerStartingWays = new ArrayList<>();
    673 
    674         for (Multipolygon area : areas) {
    675             outerStartingWays.add(area.outerWay);
    676             innerStartingWays.addAll(area.innerWays);
     332    private static class XOrHashSet<T> extends HashSet<T> {
     333        public XOrHashSet() {
     334            super();
    677335        }
    678336
    679         allStartingWays.addAll(innerStartingWays);
    680         allStartingWays.addAll(outerStartingWays);
    681 
    682         //first remove nodes in the same coordinate
    683         boolean removedDuplicates = false;
    684         removedDuplicates |= removeDuplicateNodes(allStartingWays);
    685 
    686         if (removedDuplicates) {
    687             hasChanges = true;
    688             commitCommands(marktr("Removed duplicate nodes"));
     337        public XOrHashSet(Collection<? extends T> c) {
     338            super(c);
    689339        }
    690340
    691         //find intersection points
    692         Set<Node> nodes = Geometry.addIntersections(allStartingWays, false, cmds);
    693 
    694         //no intersections, return.
    695         if (nodes.isEmpty())
    696             return new JoinAreasResult(hasChanges, null);
    697         commitCommands(marktr("Added node on all intersections"));
    698 
    699         List<RelationRole> relations = new ArrayList<>();
    700 
    701         // Remove ways from all relations so ways can be combined/split quietly
    702         for (Way way : allStartingWays) {
    703             relations.addAll(removeFromAllRelations(way));
    704         }
    705 
    706         // Don't warn now, because it will really look corrupted
    707         boolean warnAboutRelations = !relations.isEmpty() && allStartingWays.size() > 1;
    708 
    709         List<WayInPolygon> preparedWays = new ArrayList<>();
    710 
    711         // Split the nodes on the
    712         List<Way> splitOuterWays = outerStartingWays.stream()
    713                 .flatMap(way -> splitWayOnNodes(way, nodes).stream()).collect(Collectors.toList());
    714         List<Way> splitInnerWays = innerStartingWays.stream()
    715                 .flatMap(way -> splitWayOnNodes(way, nodes).stream()).collect(Collectors.toList());
    716 
    717         // remove duplicate ways (A->B->C and C->B->A)
    718         List<Way> duplicates = Stream.concat(splitOuterWays.stream(), splitInnerWays.stream()).collect(new DuplicateWayCollector());
    719 
    720         splitOuterWays.removeAll(duplicates);
    721         splitInnerWays.removeAll(duplicates);
    722 
    723         preparedWays.addAll(markWayInsideSide(splitOuterWays, false));
    724         preparedWays.addAll(markWayInsideSide(splitInnerWays, true));
    725 
    726         // Find boundary ways
    727         List<Way> discardedWays = new ArrayList<>(duplicates);
    728         List<AssembledPolygon> boundaries = findBoundaryPolygons(preparedWays, discardedWays);
    729 
    730         //find polygons
    731         List<AssembledMultipolygon> preparedPolygons = findPolygons(boundaries);
    732 
    733         //assemble final polygons
    734         List<Multipolygon> polygons = new ArrayList<>();
    735         Set<Relation> relationsToDelete = new LinkedHashSet<>();
    736 
    737         for (AssembledMultipolygon pol : preparedPolygons) {
    738 
    739             //create the new ways
    740             Multipolygon resultPol = joinPolygon(pol);
    741 
    742             //create multipolygon relation, if necessary.
    743             RelationRole ownMultipolygonRelation = addOwnMultipolygonRelation(resultPol.innerWays);
    744 
    745             //add back the original relations, merged with our new multipolygon relation
    746             fixRelations(relations, resultPol.outerWay, ownMultipolygonRelation, relationsToDelete);
    747 
    748             //strip tags from inner ways
    749             //TODO: preserve tags on existing inner ways
    750             stripTags(resultPol.innerWays);
    751 
    752             polygons.add(resultPol);
    753         }
    754 
    755         commitCommands(marktr("Assemble new polygons"));
    756 
    757         for (Relation rel: relationsToDelete) {
    758             cmds.add(new DeleteCommand(rel));
    759         }
    760 
    761         commitCommands(marktr("Delete relations"));
    762 
    763         // Delete the discarded inner ways
    764         if (!discardedWays.isEmpty()) {
    765             Command deleteCmd = DeleteCommand.delete(Main.getLayerManager().getEditLayer(), discardedWays, true);
    766             if (deleteCmd != null) {
    767                 cmds.add(deleteCmd);
    768                 commitCommands(marktr("Delete Ways that are not part of an inner multipolygon"));
     341        public void xor(T e) {
     342            if (!this.add(e)) {
     343                this.remove(e);
    769344            }
    770345        }
    771 
    772         makeCommitsOneAction(marktr("Joined overlapping areas"));
    773 
    774         if (warnAboutRelations) {
    775             new Notification(
    776                     tr("Some of the ways were part of relations that have been modified.<br>Please verify no errors have been introduced."))
    777                     .setIcon(JOptionPane.INFORMATION_MESSAGE)
    778                     .setDuration(Notification.TIME_LONG)
    779                     .show();
    780         }
    781 
    782         return new JoinAreasResult(true, polygons);
    783346    }
    784347
    785348    /**
    786      * Checks if tags of two given ways differ, and presents the user a dialog to solve conflicts
    787      * @param polygons ways to check
    788      * @return {@code true} if all conflicts are resolved, {@code false} if conflicts remain.
     349     * This class collects the areas to be joined.
    789350     */
    790     private boolean resolveTagConflicts(List<Multipolygon> polygons) {
     351    static class JoinAreasCollector {
     352        /**
     353         * All nodes that are touched by this the geometry for algorithm.
     354         */
     355        private final Collection<Node> oldTouchedNodes = new HashSet<>();
     356        /**
     357         * The nodes that are added.
     358         */
     359        private final List<Node> possibleNewNodes = new ArrayList<>();
     360        private final List<JoinableArea> unionOf = new ArrayList<>();
     361        /**
     362         * All hash sets that may be
     363         */
     364        private final XOrHashSet<UndirectedWaySegment> waySegments = new XOrHashSet<>();
     365        private final DataSet ds;
    791366
    792         List<Way> ways = new ArrayList<>();
    793 
    794         for (Multipolygon pol : polygons) {
    795             ways.add(pol.outerWay);
    796             ways.addAll(pol.innerWays);
     367        JoinAreasCollector(DataSet ds, Collection<? extends OsmPrimitive> waysAndRelations) throws JoinAreasException {
     368            this.ds = ds;
     369            Collection<JoinableArea> collectAreas = collectAreas(waysAndRelations);
     370            collectAreas.forEach(this::unionWithArea);
    797371        }
    798372
    799         if (ways.size() < 2) {
    800             return true;
    801         }
    802 
    803         TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways);
    804         try {
    805             cmds.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(wayTags, ways, ways));
    806             commitCommands(marktr("Fix tag conflicts"));
    807             return true;
    808         } catch (UserCancelException ex) {
    809             Main.trace(ex);
    810             return false;
    811         }
    812     }
    813 
    814     /**
    815      * This method removes duplicate points (if any) from the input way.
    816      * @param ways the ways to process
    817      * @return {@code true} if any changes where made
    818      */
    819     private boolean removeDuplicateNodes(List<Way> ways) {
    820         //TODO: maybe join nodes with JoinNodesAction, rather than reconnect the ways.
    821 
    822         Map<Node, Node> nodeMap = new TreeMap<>(new NodePositionComparator());
    823         int totalNodesRemoved = 0;
    824 
    825         for (Way way : ways) {
    826             if (way.getNodes().size() < 2) {
    827                 continue;
    828             }
    829 
    830             int nodesRemoved = 0;
    831             List<Node> newNodes = new ArrayList<>();
    832             Node prevNode = null;
    833 
    834             for (Node node : way.getNodes()) {
    835                 if (!nodeMap.containsKey(node)) {
    836                     //new node
    837                     nodeMap.put(node, node);
    838 
    839                     //avoid duplicate nodes
    840                     if (prevNode != node) {
    841                         newNodes.add(node);
    842                     } else {
    843                         nodesRemoved++;
    844                     }
    845                 } else {
    846                     //node with same coordinates already exists, substitute with existing node
    847                     Node representator = nodeMap.get(node);
    848 
    849                     if (representator != node) {
    850                         nodesRemoved++;
    851                     }
    852 
    853                     //avoid duplicate node
    854                     if (prevNode != representator) {
    855                         newNodes.add(representator);
    856                     }
     373        private static Collection<JoinableArea> collectAreas(Collection<? extends OsmPrimitive> waysAndRelations) throws JoinAreasException {
     374            Collection<JoinableArea> areas = new ArrayList<>();
     375            for(OsmPrimitive osm : waysAndRelations) {
     376                if (osm instanceof Way) {
     377                    areas.add(new JoinableArea((Way) osm));
     378                } else if (osm instanceof Relation) {
     379                    areas.add(new JoinableArea((Relation) osm));
    857380                }
    858                 prevNode = node;
    859381            }
    860 
    861             if (nodesRemoved > 0) {
    862 
    863                 if (newNodes.size() == 1) { //all nodes in the same coordinate - add one more node, to have closed way.
    864                     newNodes.add(newNodes.get(0));
    865                 }
    866 
    867                 Way newWay = new Way(way);
    868                 newWay.setNodes(newNodes);
    869                 cmds.add(new ChangeCommand(way, newWay));
    870                 totalNodesRemoved += nodesRemoved;
    871             }
     382            return areas;
    872383        }
    873384
    874         return totalNodesRemoved > 0;
    875     }
     385        void unionWithArea(JoinableArea area) {
     386            Collection<UndirectedWaySegment> segments = area.getSegments();
    876387
    877     /**
    878      * Commits the command list with a description
    879      * @param description The description of what the commands do
    880      */
    881     private void commitCommands(String description) {
    882         switch(cmds.size()) {
    883         case 0:
    884             return;
    885         case 1:
    886             commitCommand(cmds.getFirst());
    887             break;
    888         default:
    889             commitCommand(new SequenceCommand(tr(description), cmds));
    890             break;
    891         }
     388            segments.stream().flatMap(s -> Stream.of(s.a, s.b)).forEach(oldTouchedNodes::add);
    892389
    893         cmds.clear();
    894         cmdsCount++;
    895     }
     390            // Our worker list. Once a way is split, it is re-added to the worker to check for more splits.
     391            XOrHashSet<UndirectedWaySegment> toAdd = new XOrHashSet<>(segments);
     392            while (!toAdd.isEmpty()) {
     393                UndirectedWaySegment s = toAdd.iterator().next();
     394                toAdd.remove(s);
     395                Optional<UndirectedWaySegment> intersects = waySegments.stream().filter(s::intersects).findAny();
     396                if (intersects.isPresent()) {
     397                    EastNorth intersection = s.getIntersectionPoint(intersects.get());
     398                     // Now generate two segments around the intersection.
     399                    waySegments.remove(intersects.get());
     400                    Node newNode = findOrCreateNode(intersection);
    896401
    897     private static void commitCommand(Command c) {
    898         if (Main.main != null) {
    899             Main.main.undoRedo.add(c);
    900         } else {
    901             c.executeCommand();
    902         }
    903     }
    904 
    905     /**
    906      * This method analyzes the way and assigns each part what direction polygon "inside" is.
    907      * @param parts the split parts of the way
    908      * @param isInner - if true, reverts the direction (for multipolygon islands)
    909      * @return list of parts, marked with the inside orientation.
    910      * @throws IllegalArgumentException if parts is empty or not circular
    911      */
    912     private static List<WayInPolygon> markWayInsideSide(List<Way> parts, boolean isInner) {
    913         // the data is prepared so that all ways are split at possible intersection points.
    914         // To find out which side of the way the outer side is, we can follow a ray starting anywhere at the way in any direction.
    915         // Computation is done in East/North space.
    916         // We use a ray at a fixed yRay coordinate that ends at xRay;
    917         // we need to make sure this ray does not go into the same direction the way is going.
    918         // This is done by rotating by 90° if we need to.
    919 
    920         return parts.stream().map(way -> {
    921             int intersections = 0;
    922             // Use some random start point on the way
    923             EastNorth rayNode1 = way.getNode(0).getEastNorth();
    924             EastNorth rayNode2 = way.getNode(1).getEastNorth();
    925             EastNorth rayFrom = rayNode1.getCenter(rayNode2);
    926 
    927             // Now find the x/y mapping function. We need to ensure that rayNode1->rayNode2 is not parallel to our x axis.
    928             ToDoubleFunction<EastNorth> x;
    929             ToDoubleFunction<EastNorth> y;
    930             if (Math.abs(rayNode1.east() - rayNode2.east()) < Math.abs(rayNode1.north() - rayNode2.north())) {
    931                 x = en -> en.east();
    932                 y = en -> en.north();
    933             } else {
    934                 x = en -> -en.north();
    935                 y = en -> en.east();
    936             }
    937 
    938             double xRay = x.applyAsDouble(rayFrom);
    939             double yRay = y.applyAsDouble(rayFrom);
    940 
    941             for (Way part : parts) {
    942                 // intersect against all way segments
    943                 for (int i = 0; i < part.getNodesCount() - 1; i++) {
    944                     EastNorth n1 = part.getNode(i).getEastNorth();
    945                     EastNorth n2 = part.getNode(i + 1).getEastNorth();
    946                     if ((rayNode1.equals(n1) && rayNode2.equals(n2)) || (rayNode2.equals(n1) && rayNode1.equals(n2))) {
    947                         // This is the segment we are starting the ray from.
    948                         // We ignore this to avoid rounding errors.
    949                         continue;
     402                    // it may be that newNode is one of the end nodes
     403                    if (newNode != intersects.get().a) {
     404                        // We use xor here to fix ways that e.g. reverse on themselves.
     405                        waySegments.xor(new UndirectedWaySegment(intersects.get().a, newNode));
    950406                    }
    951 
    952                     double x1 = x.applyAsDouble(n1);
    953                     double x2 = x.applyAsDouble(n2);
    954                     double y1 = y.applyAsDouble(n1);
    955                     double y2 = y.applyAsDouble(n2);
    956 
    957                     if (!((y1 <= yRay && yRay < y2) || (y2 <= yRay && yRay < y1))) {
    958                         // No intersection, since segment is above/below ray
    959                         continue;
     407                    if (newNode != s.a) {
     408                        toAdd.xor(new UndirectedWaySegment(s.a, newNode));
    960409                    }
    961                     double xIntersect = x1 + (x2 - x1) * (yRay - y1) / (y2 - y1);
    962                     if (xIntersect < xRay) {
    963                         intersections++;
     410                    if (newNode != intersects.get().b) {
     411                        waySegments.xor(new UndirectedWaySegment(newNode, intersects.get().b));
    964412                    }
     413                    if (newNode != s.b) {
     414                        toAdd.xor(new UndirectedWaySegment(newNode, s.b));
     415                    }
     416                } else {
     417                    // No more intersections - we add that segment to our geometry
     418                    waySegments.xor(s);
    965419                }
    966420            }
    967421
    968             return new WayInPolygon(way, (intersections % 2 == 0) ^ isInner ^ (y.applyAsDouble(rayNode1) > yRay));
    969         }).collect(Collectors.toList());
    970     }
    971 
    972     /**
    973      * This is a method that splits way into smaller parts, using the prepared nodes list as split points.
    974      * Uses {@link SplitWayAction#splitWay} for the heavy lifting.
    975      * @param way way to split
    976      * @param nodes split points
    977      * @return list of split ways (or original ways if no splitting is done).
    978      */
    979     private List<Way> splitWayOnNodes(Way way, Set<Node> nodes) {
    980 
    981         List<Way> result = new ArrayList<>();
    982         List<List<Node>> chunks = buildNodeChunks(way, nodes);
    983 
    984         if (chunks.size() > 1) {
    985             SplitWayResult split = SplitWayAction.splitWay(getLayerManager().getEditLayer(), way, chunks,
    986                     Collections.<OsmPrimitive>emptyList(), SplitWayAction.Strategy.keepFirstChunk());
    987 
    988             if (split != null) {
    989                 //execute the command, we need the results
    990                 cmds.add(split.getCommand());
    991                 commitCommands(marktr("Split ways into fragments"));
    992 
    993                 result.add(split.getOriginalWay());
    994                 result.addAll(split.getNewWays());
    995             }
     422            unionOf.add(area);
    996423        }
    997         if (result.isEmpty()) {
    998             //nothing to split
    999             result.add(way);
    1000         }
    1001424
    1002         return result;
    1003     }
    1004 
    1005     /**
    1006      * Simple chunking version. Does not care about circular ways and result being
    1007      * proper, we will glue it all back together later on.
    1008      * @param way the way to chunk
    1009      * @param splitNodes the places where to cut.
    1010      * @return list of node paths to produce.
    1011      */
    1012     private static List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) {
    1013         List<List<Node>> result = new ArrayList<>();
    1014         List<Node> curList = new ArrayList<>();
    1015 
    1016         for (Node node : way.getNodes()) {
    1017             curList.add(node);
    1018             if (curList.size() > 1 && splitNodes.contains(node)) {
    1019                 result.add(curList);
    1020                 curList = new ArrayList<>();
    1021                 curList.add(node);
    1022             }
     425        /**
     426         * Find a node close to newNode to handle intersections of 3 or more lines.
     427         * @param intersection The position of the node
     428         * @return A node.
     429         */
     430        Node findOrCreateNode(EastNorth intersection) {
     431            return Stream.concat(oldTouchedNodes.stream(), possibleNewNodes.stream())
     432                    .filter(node -> node.getEastNorth().distanceSq(intersection) < 1e-40)
     433                    .findAny()
     434                    .orElseGet(() -> this.createNode(intersection));
    1023435        }
    1024436
    1025         if (curList.size() > 1) {
    1026             result.add(curList);
     437        private Node createNode(EastNorth intersection) {
     438            Node newNode = new Node(intersection);
     439            possibleNewNodes.add(newNode);
     440            return newNode;
    1027441        }
    1028442
    1029         return result;
    1030     }
     443        /**
     444         * Gets the outlines of this area
     445         * @return The outline polygons as Even/Odd area. Not all nodes need to be contained in the data set.
     446         */
     447        List<List<Node>> getOutlines() {
     448            Collection<UndirectedWaySegment> outline = computeOutline();
    1031449
    1032     /**
    1033      * This method finds which ways are outer and which are inner.
    1034      * @param boundaries list of joined boundaries to search in
    1035      * @return outer ways
    1036      */
    1037     private static List<AssembledMultipolygon> findPolygons(Collection<AssembledPolygon> boundaries) {
    1038 
    1039         List<PolygonLevel> list = findOuterWaysImpl(0, boundaries);
    1040         List<AssembledMultipolygon> result = new ArrayList<>();
    1041 
    1042         //take every other level
    1043         for (PolygonLevel pol : list) {
    1044             if (pol.level % 2 == 0) {
    1045                 result.add(pol.pol);
     450            ArrayList<List<Node>> res = new ArrayList<>();
     451            while (!outline.isEmpty()) {
     452                res.add(removeOutlinePart(outline));
    1046453            }
     454            return res;
    1047455        }
    1048456
    1049         return result;
    1050     }
     457        /**
     458         * Gets the commands that are required to join the areas.
     459         * @return The join commands.
     460         */
     461        List<Command> getCommands() {
     462            if (unionOf.isEmpty()) {
     463                return Collections.emptyList();
     464            }
     465            Collection<UndirectedWaySegment> outline = computeOutline();
    1051466
    1052     /**
    1053      * Collects outer way and corresponding inner ways from all boundaries.
    1054      * @param level depth level
    1055      * @param boundaryWays list of joined boundaries to search in
    1056      * @return the outermost Way.
    1057      */
    1058     private static List<PolygonLevel> findOuterWaysImpl(int level, Collection<AssembledPolygon> boundaryWays) {
     467            List<Command> commands = new ArrayList<>();
     468            // The primitives of which we should remove the tags.
     469            List<OsmPrimitive> toRemoveTags = new ArrayList<>();
     470            unionOf.stream().map(area -> area.basePrimitive).forEach(toRemoveTags::add);
    1059471
    1060         //TODO: bad performance for deep nestings...
    1061         List<PolygonLevel> result = new ArrayList<>();
     472            // Add the split nodes
     473            // Remove nodes of interior segments.
     474            possibleNewNodes.stream()
     475                .filter(n -> outline.stream().filter(w -> w.hasEnd(n)).findAny().isPresent())
     476                .map(n -> new AddCommand(ds, n))
     477                .forEach(commands::add);
    1062478
    1063         for (AssembledPolygon outerWay : boundaryWays) {
    1064 
    1065             boolean outerGood = true;
    1066             List<AssembledPolygon> innerCandidates = new ArrayList<>();
    1067 
    1068             for (AssembledPolygon innerWay : boundaryWays) {
    1069                 if (innerWay == outerWay) {
     479            // Now search all ways which are completely used in our new geometry (e.g. multipolygon inners, ...)
     480            // We should not change those ways.
     481            List<Way> outlineWays = new ArrayList<>();
     482            List<UndirectedWaySegment> segmentsToContain = new ArrayList<>(outline);
     483            for (Way preserve : findOutlinesToPreserve(segmentsToContain)) {
     484                List<UndirectedWaySegment> preservedSegments = segmentsForWay(preserve);
     485                if (preservedSegments.size() != preservedSegments.stream().distinct().count()) {
     486                    // This way contains a segment twice. Skip it, we want to fix this.
    1070487                    continue;
    1071488                }
    1072 
    1073                 if (wayInsideWay(outerWay, innerWay)) {
    1074                     outerGood = false;
    1075                     break;
    1076                 } else if (wayInsideWay(innerWay, outerWay)) {
    1077                     innerCandidates.add(innerWay);
     489                if (!segmentsToContain.containsAll(preservedSegments)) {
     490                    // it may happen that two outlines that should be preserved happen to be on the same segment
     491                    // We need to ignore the second one then.
     492                    continue;
    1078493                }
     494                outlineWays.add(preserve);
     495                segmentsToContain.removeAll(preservedSegments);
    1079496            }
    1080497
    1081             if (!outerGood) {
    1082                 continue;
    1083             }
     498            // Multipolygons that were selected and can now be removed
     499            List<Relation> relationsToDelete = unionOf.stream().flatMap(area -> area.relations.stream())
     500                    .distinct().collect(Collectors.toList());
     501            toRemoveTags.removeAll(relationsToDelete);
    1084502
    1085             //add new outer polygon
    1086             AssembledMultipolygon pol = new AssembledMultipolygon(outerWay);
    1087             PolygonLevel polLev = new PolygonLevel(pol, level);
     503            // Compute the ways that need to be removed.
     504            // Those are all ways of the old geometry that are not used in any other place.
     505            List<Way> waysToDelete = unionOf.stream().flatMap(area -> area.ways.stream())
     506                    .distinct()
     507                    .filter(way -> !outlineWays.contains(way))
     508                    // Preserve ways that are member in any relation that we did not modify
     509                    .filter(way -> way.getReferrers().stream().allMatch(relationsToDelete::contains))
     510                    // Preserve ways that have tags
     511                    .filter(way -> toRemoveTags.contains(way) || way.getInterestingTags().isEmpty())
     512                    .collect(Collectors.toList());
     513            toRemoveTags.removeAll(waysToDelete);
    1088514
    1089             //process inner ways
    1090             if (!innerCandidates.isEmpty()) {
    1091                 List<PolygonLevel> innerList = findOuterWaysImpl(level + 1, innerCandidates);
    1092                 result.addAll(innerList);
     515            // Now we are left with the remaining outline in the segmentsToContain array.
     516            // For each chunk in that outline, we create a new way
     517            // TODO: We can reuse the ways we would delete otherwise.
     518            while (!segmentsToContain.isEmpty()) {
     519                List<Node> wayToCreate = removeOutlinePart(segmentsToContain);
     520                Way osm = new Way();
     521                osm.setNodes(wayToCreate);
     522                outlineWays.add(osm);
     523                commands.add(new AddCommand(ds, osm));
     524            }
    1093525
    1094                 for (PolygonLevel pl : innerList) {
    1095                     if (pl.level == level + 1) {
    1096                         pol.innerWays.add(pl.pol.outerWay);
    1097                     }
     526            OsmPrimitive resultPrimitive;
     527            // Now it is time to generate the final area.
     528            if (outlineWays.isEmpty()) {
     529                throw new AssertionError("No outline ways found.");
     530            } else if (outlineWays.size() == 1) {
     531                // We only have one way. Add the tags to that way.
     532                resultPrimitive = outlineWays.get(0);
     533            } else {
     534                // find a relation. Use the more complex multipolygon when merging two of them.
     535                Relation multipolygon = relationsToDelete.stream().sorted(Comparator.comparingInt(r -> -r.getMembersCount()))
     536                        .findFirst().orElseGet(Relation::new);
     537                Pair<Relation, Relation> update = CreateMultipolygonAction.updateMultipolygonRelation(outlineWays, multipolygon);
     538                if (update == null) {
     539                    throw new AssertionError("The outline ways should be continuous but no multipolygon could be created.");
    1098540                }
     541                if (update.a.getDataSet() == null) {
     542                    // used the fake relation.
     543                    commands.add(new AddCommand(ds, update.b));
     544                } else {
     545                    commands.add(new ChangeCommand(ds, update.a, update.b));
     546                }
     547                resultPrimitive = multipolygon;
     548                relationsToDelete.remove(multipolygon);
    1099549            }
    1100550
    1101             result.add(polLev);
    1102         }
     551            // We check the tags now.
     552            try {
     553                List<OsmPrimitive> baseTagged = unionOf.stream().map(area -> area.basePrimitive).collect(Collectors.toList());
     554                TagCollection tags = TagCollection.unionOfAllPrimitives(baseTagged);
     555                commands.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(tags, baseTagged, Collections.singleton(resultPrimitive)));
     556            } catch (UserCancelException ex) {
     557                // User aborted. This is simple, since we did not commit anything.
     558                Main.trace(ex);
     559                return Collections.emptyList();
     560            }
    1103561
    1104         return result;
    1105     }
    1106 
    1107     /**
    1108      * Finds all ways that form inner or outer boundaries.
    1109      * @param multigonWays A list of (splitted) ways that form a multigon and share common end nodes on intersections.
    1110      * @param discardedResult this list is filled with ways that are to be discarded
    1111      * @return A list of ways that form the outer and inner boundaries of the multigon.
    1112      */
    1113     public static List<AssembledPolygon> findBoundaryPolygons(Collection<WayInPolygon> multigonWays,
    1114             List<Way> discardedResult) {
    1115         // In multigonWays collection, some way are just a point (i.e. way like nodeA-nodeA)
    1116         // This seems to appear when is apply over invalid way like #9911 test-case
    1117         // Remove all of these way to make the next work.
    1118         List<WayInPolygon> cleanMultigonWays = multigonWays.stream()
    1119                 .filter(way -> way.way.getNodesCount() != 2 || !way.way.isClosed())
    1120                 .collect(Collectors.toList());
    1121         WayTraverser traverser = new WayTraverser(cleanMultigonWays);
    1122         List<AssembledPolygon> result = new ArrayList<>();
    1123 
    1124         try {
    1125             WayInPolygon startWay;
    1126             while ((startWay = traverser.startNewWay()) != null) {
    1127                 findBoundaryPolygonsStartingWith(discardedResult, traverser, result, startWay);
     562            // Apply deletion of the primitives we don't need any more.
     563            if (!relationsToDelete.isEmpty()) {
     564                commands.add(new DeleteCommand(ds, relationsToDelete));
    1128565            }
    1129         } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) {
    1130             throw BugReport.intercept(t).put("traverser", traverser);
    1131         }
     566            if (!waysToDelete.isEmpty()) {
     567                commands.add(new DeleteCommand(ds, waysToDelete));
    1132568
    1133         return fixTouchingPolygons(result);
    1134     }
    1135569
    1136     private static void findBoundaryPolygonsStartingWith(List<Way> discardedResult, WayTraverser traverser, List<AssembledPolygon> result,
    1137             WayInPolygon startWay) {
    1138         List<WayInPolygon> path = new ArrayList<>();
    1139         List<WayInPolygon> startWays = new ArrayList<>();
    1140         try {
    1141             path.add(startWay);
    1142             while (true) {
    1143                 WayInPolygon leftComing = traverser.leftComingWay();
    1144                 if (leftComing != null && !startWays.contains(leftComing)) {
    1145                     // Need restart traverser walk
    1146                     path.clear();
    1147                     path.add(leftComing);
    1148                     traverser.setStartWay(leftComing);
    1149                     startWays.add(leftComing);
     570                Collection<Node> nodesToDelete = oldTouchedNodes.stream()
     571                        .filter(n -> !n.isTagged())
     572                        // don't delete nodes that are part of the new outline
     573                        .filter(n -> outlineWays.stream().allMatch(w -> !w.containsNode(n)))
     574                        .filter(n -> n.getReferrers().stream().allMatch(r -> waysToDelete.contains(r) || r.isDeleted()))
     575                        .collect(Collectors.toList());
     576                if (!nodesToDelete.isEmpty()) {
     577                    commands.add(new DeleteCommand(ds, nodesToDelete));
    1150578                }
    1151                 WayInPolygon nextWay = traverser.walk();
    1152                 if (nextWay == null) {
    1153                     throw new JosmRuntimeException("Join areas internal error: traverser could not find a next way.");
     579            }
     580            for(OsmPrimitive osm : toRemoveTags) {
     581                for (String key : osm.getKeys().keySet()) {
     582                    commands.add(new ChangePropertyCommand(osm, key, ""));
    1154583                }
    1155                 if (path.get(0) == nextWay) {
    1156                     // path is closed -> stop here
    1157                     AssembledPolygon ring = new AssembledPolygon(path);
    1158                     if (ring.getNodes().size() <= 2) {
    1159                         // Invalid ring (2 nodes) -> remove
    1160                         traverser.removeWays(path);
    1161                         for (WayInPolygon way: path) {
    1162                             discardedResult.add(way.way);
    1163                         }
    1164                     } else {
    1165                         // Close ring -> add
    1166                         result.add(ring);
    1167                         traverser.removeWays(path);
    1168                     }
    1169                     break;
    1170                 }
    1171                 if (path.contains(nextWay)) {
    1172                     // Inner loop -> remove
    1173                     int index = path.indexOf(nextWay);
    1174                     while (path.size() > index) {
    1175                         WayInPolygon currentWay = path.get(index);
    1176                         discardedResult.add(currentWay.way);
    1177                         traverser.removeWay(currentWay);
    1178                         path.remove(index);
    1179                     }
    1180                     traverser.setStartWay(path.get(index-1));
    1181                 } else {
    1182                     path.add(nextWay);
    1183                 }
    1184584            }
    1185         } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) {
    1186             throw BugReport.intercept(t).put("path", path);
     585
     586            return commands;
    1187587        }
    1188     }
    1189588
    1190     /**
    1191      * This method checks if polygons have several touching parts and splits them in several polygons.
    1192      * @param polygons the polygons to process.
    1193      * @return the resulting list of polygons
    1194      */
    1195     public static List<AssembledPolygon> fixTouchingPolygons(List<AssembledPolygon> polygons) {
    1196         List<AssembledPolygon> newPolygons = new ArrayList<>();
    1197 
    1198         for (AssembledPolygon ring : polygons) {
    1199             ring.reverse();
    1200             WayTraverser traverser = new WayTraverser(ring.ways);
    1201             WayInPolygon startWay;
    1202 
    1203             while ((startWay = traverser.startNewWay()) != null) {
    1204                 List<WayInPolygon> simpleRingWays = new ArrayList<>();
    1205                 simpleRingWays.add(startWay);
    1206                 WayInPolygon nextWay;
    1207                 while ((nextWay = traverser.walk()) != startWay) {
    1208                     if (nextWay == null)
    1209                         throw new JosmRuntimeException("Join areas internal error.");
    1210                     simpleRingWays.add(nextWay);
    1211                 }
    1212                 traverser.removeWays(simpleRingWays);
    1213                 AssembledPolygon simpleRing = new AssembledPolygon(simpleRingWays);
    1214                 simpleRing.reverse();
    1215                 newPolygons.add(simpleRing);
    1216             }
     589        private List<UndirectedWaySegment> segmentsForWay(Way way) {
     590            return way.getNodePairs(false).stream()
     591                    .map(pair -> new UndirectedWaySegment(pair.a, pair.b)).collect(Collectors.toList());
    1217592        }
    1218593
    1219         return newPolygons;
    1220     }
    1221 
    1222     /**
    1223      * Tests if way is inside other way
    1224      * @param outside outer polygon description
    1225      * @param inside inner polygon description
    1226      * @return {@code true} if inner is inside outer
    1227      */
    1228     public static boolean wayInsideWay(AssembledPolygon inside, AssembledPolygon outside) {
    1229         Set<Node> outsideNodes = new HashSet<>(outside.getNodes());
    1230         List<Node> insideNodes = inside.getNodes();
    1231 
    1232         for (Node insideNode : insideNodes) {
    1233 
    1234             if (!outsideNodes.contains(insideNode))
    1235                 //simply test the one node
    1236                 return Geometry.nodeInsidePolygon(insideNode, outside.getNodes());
     594        private List<Way> findOutlinesToPreserve(List<UndirectedWaySegment> segmentsToContain) {
     595            return unionOf.stream().flatMap(u -> u.ways.stream())
     596                .filter(w -> segmentsToContain.containsAll(segmentsForWay(w))).collect(Collectors.toList());
    1237597        }
    1238598
    1239         //all nodes shared.
    1240         return false;
    1241     }
    1242 
    1243     /**
    1244      * Joins the lists of ways.
    1245      * @param polygon The list of outer ways that belong to that multipolygon.
    1246      * @return The newly created outer way
    1247      * @throws UserCancelException if user cancels the operation
    1248      */
    1249     private Multipolygon joinPolygon(AssembledMultipolygon polygon) throws UserCancelException {
    1250         Multipolygon result = new Multipolygon(joinWays(polygon.outerWay.ways));
    1251 
    1252         for (AssembledPolygon pol : polygon.innerWays) {
    1253             result.innerWays.add(joinWays(pol.ways));
     599        private Collection<UndirectedWaySegment> computeOutline() {
     600            return waySegments.stream().filter(
     601                        seg -> unionOf.stream().noneMatch(area -> area.contains(seg))
     602                    ).collect(Collectors.toList());
    1254603        }
    1255 
    1256         return result;
    1257604    }
    1258605
    1259606    /**
    1260      * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway.
    1261      * @param ways The list of outer ways that belong to that multigon.
    1262      * @return The newly created outer way
    1263      * @throws UserCancelException if user cancels the operation
     607     * Remove a continuous part of the segments
     608     * @param segments The segments that may be removed
     609     * @return A list of the end nodes of the segments. It is either a closed loop or a segment as long as possible.
    1264610     */
    1265     private Way joinWays(List<WayInPolygon> ways) throws UserCancelException {
    1266 
    1267         //leave original orientation, if all paths are reverse.
    1268         boolean allReverse = true;
    1269         for (WayInPolygon way : ways) {
    1270             allReverse &= !way.insideToTheRight;
     611    private static LinkedList<Node> removeOutlinePart(Collection<UndirectedWaySegment> segments) {
     612        Node start = segments.iterator().next().a;
     613        LinkedList<Node> nodes = new LinkedList<>();
     614        nodes.add(start);
     615        // Move in one direction on that way.
     616        while ((nodes.size() < 2 || nodes.getFirst() != nodes.getLast()) && !segments.isEmpty()) {
     617            Optional<UndirectedWaySegment> traverse = segments.stream().filter(s -> s.hasEnd(nodes.getLast())).findAny();
     618            if (!traverse.isPresent()) {
     619                break;
     620            }
     621            segments.remove(traverse.get());
     622            nodes.addLast(traverse.get().getOtherEnd(nodes.getLast()));
    1271623        }
    1272624
    1273         if (allReverse) {
    1274             for (WayInPolygon way : ways) {
    1275                 way.insideToTheRight = !way.insideToTheRight;
     625        // Now move in the other direction - as far as we can go.
     626        while ((nodes.size() < 2 || nodes.getFirst() != nodes.getLast()) && !segments.isEmpty()) {
     627            Optional<UndirectedWaySegment> traverse = segments.stream().filter(s -> s.hasEnd(nodes.getFirst())).findAny();
     628            if (!traverse.isPresent()) {
     629                break;
    1276630            }
     631            segments.remove(traverse.get());
     632            nodes.addFirst(traverse.get().getOtherEnd(nodes.getFirst()));
    1277633        }
    1278634
    1279         Way joinedWay = joinOrientedWays(ways);
    1280 
    1281         //should not happen
    1282         if (joinedWay == null || !joinedWay.isClosed())
    1283             throw new JosmRuntimeException("Join areas internal error.");
    1284 
    1285         return joinedWay;
     635        return nodes;
    1286636    }
    1287637
    1288638    /**
    1289      * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath)
    1290      * @param ways The list of ways to join and reverse
    1291      * @return The newly created way
    1292      * @throws UserCancelException if user cancels the operation
     639     * Constructs a new {@code JoinAreasAction}.
    1293640     */
    1294     private Way joinOrientedWays(List<WayInPolygon> ways) throws UserCancelException {
    1295         if (ways.size() < 2)
    1296             return ways.get(0).way;
    1297 
    1298         // This will turn ways so all of them point in the same direction and CombineAction won't bug
    1299         // the user about this.
    1300 
    1301         //TODO: ReverseWay and Combine way are really slow and we use them a lot here. This slows down large joins.
    1302         List<Way> actionWays = new ArrayList<>(ways.size());
    1303 
    1304         for (WayInPolygon way : ways) {
    1305             actionWays.add(way.way);
    1306 
    1307             if (!way.insideToTheRight) {
    1308                 ReverseWayResult res = ReverseWayAction.reverseWay(way.way);
    1309                 commitCommand(res.getReverseCommand());
    1310                 cmdsCount++;
    1311             }
    1312         }
    1313 
    1314         Pair<Way, Command> result = CombineWayAction.combineWaysWorker(actionWays);
    1315 
    1316         commitCommand(result.b);
    1317         cmdsCount++;
    1318 
    1319         return result.a;
     641    public JoinAreasAction() {
     642        this(true);
    1320643    }
    1321644
    1322645    /**
    1323      * This method analyzes multipolygon relationships of given ways and collects addition inner ways to consider.
    1324      * @param selectedWays the selected ways
    1325      * @return list of polygons, or null if too complex relation encountered.
     646     * Constructs a new {@code JoinAreasAction} with optional shortcut.
     647     * @param addShortcut controls whether the shortcut should be registered or not
     648     * @since 11611
    1326649     */
    1327     public static List<Multipolygon> collectMultipolygons(Collection<Way> selectedWays) {
    1328 
    1329         List<Multipolygon> result = new ArrayList<>();
    1330 
    1331         //prepare the lists, to minimize memory allocation.
    1332         List<Way> outerWays = new ArrayList<>();
    1333         List<Way> innerWays = new ArrayList<>();
    1334 
    1335         Set<Way> processedOuterWays = new LinkedHashSet<>();
    1336         Set<Way> processedInnerWays = new LinkedHashSet<>();
    1337 
    1338         for (Relation r : OsmPrimitive.getParentRelations(selectedWays)) {
    1339             if (r.isDeleted() || !r.isMultipolygon()) {
    1340                 continue;
    1341             }
    1342 
    1343             boolean hasKnownOuter = false;
    1344             outerWays.clear();
    1345             innerWays.clear();
    1346 
    1347             for (RelationMember rm : r.getMembers()) {
    1348                 if ("outer".equalsIgnoreCase(rm.getRole())) {
    1349                     outerWays.add(rm.getWay());
    1350                     hasKnownOuter |= selectedWays.contains(rm.getWay());
    1351                 } else if ("inner".equalsIgnoreCase(rm.getRole())) {
    1352                     innerWays.add(rm.getWay());
    1353                 }
    1354             }
    1355 
    1356             if (!hasKnownOuter) {
    1357                 continue;
    1358             }
    1359 
    1360             if (outerWays.size() > 1) {
    1361                 new Notification(
    1362                         tr("Sorry. Cannot handle multipolygon relations with multiple outer ways."))
    1363                         .setIcon(JOptionPane.INFORMATION_MESSAGE)
    1364                         .show();
    1365                 return null;
    1366             }
    1367 
    1368             Way outerWay = outerWays.get(0);
    1369 
    1370             //retain only selected inner ways
    1371             innerWays.retainAll(selectedWays);
    1372 
    1373             if (processedOuterWays.contains(outerWay)) {
    1374                 new Notification(
    1375                         tr("Sorry. Cannot handle way that is outer in multiple multipolygon relations."))
    1376                         .setIcon(JOptionPane.INFORMATION_MESSAGE)
    1377                         .show();
    1378                 return null;
    1379             }
    1380 
    1381             if (processedInnerWays.contains(outerWay)) {
    1382                 new Notification(
    1383                         tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
    1384                         .setIcon(JOptionPane.INFORMATION_MESSAGE)
    1385                         .show();
    1386                 return null;
    1387             }
    1388 
    1389             for (Way way :innerWays) {
    1390                 if (processedOuterWays.contains(way)) {
    1391                     new Notification(
    1392                             tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
    1393                             .setIcon(JOptionPane.INFORMATION_MESSAGE)
    1394                             .show();
    1395                     return null;
    1396                 }
    1397 
    1398                 if (processedInnerWays.contains(way)) {
    1399                     new Notification(
    1400                             tr("Sorry. Cannot handle way that is inner in multiple multipolygon relations."))
    1401                             .setIcon(JOptionPane.INFORMATION_MESSAGE)
    1402                             .show();
    1403                     return null;
    1404                 }
    1405             }
    1406 
    1407             processedOuterWays.add(outerWay);
    1408             processedInnerWays.addAll(innerWays);
    1409 
    1410             Multipolygon pol = new Multipolygon(outerWay);
    1411             pol.innerWays.addAll(innerWays);
    1412 
    1413             result.add(pol);
    1414         }
    1415 
    1416         //add remaining ways, not in relations
    1417         for (Way way : selectedWays) {
    1418             if (processedOuterWays.contains(way) || processedInnerWays.contains(way)) {
    1419                 continue;
    1420             }
    1421 
    1422             result.add(new Multipolygon(way));
    1423         }
    1424 
    1425         return result;
     650    public JoinAreasAction(boolean addShortcut) {
     651        super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"),
     652                addShortcut ? Shortcut.registerShortcut("tools:joinareas",
     653                        tr("Tool: {0}", tr("Join overlapping Areas")), KeyEvent.VK_J, Shortcut.SHIFT) : null,
     654                true);
    1426655    }
    1427656
    1428657    /**
    1429      * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations
    1430      * @param inner List of already closed inner ways
    1431      * @return The list of relation with roles to add own relation to
     658     * Gets called whenever the shortcut is pressed or the menu entry is selected.
     659     * Checks whether the selected objects are suitable to join and joins them if so.
    1432660     */
    1433     private RelationRole addOwnMultipolygonRelation(Collection<Way> inner) {
    1434         if (inner.isEmpty()) return null;
    1435         OsmDataLayer layer = Main.getLayerManager().getEditLayer();
    1436         // Create new multipolygon relation and add all inner ways to it
    1437         Relation newRel = new Relation();
    1438         newRel.put("type", "multipolygon");
    1439         for (Way w : inner) {
    1440             newRel.addMember(new RelationMember("inner", w));
    1441         }
    1442         cmds.add(layer != null ? new AddCommand(layer, newRel) :
    1443             new AddCommand(inner.iterator().next().getDataSet(), newRel));
    1444         addedRelations.add(newRel);
    1445 
    1446         // We don't add outer to the relation because it will be handed to fixRelations()
    1447         // which will then do the remaining work.
    1448         return new RelationRole(newRel, "outer");
     661    @Override
     662    public void actionPerformed(ActionEvent e) {
     663        join(Main.getLayerManager().getEditDataSet().getSelected());
    1449664    }
    1450665
    1451666    /**
    1452      * Removes a given OsmPrimitive from all relations.
    1453      * @param osm Element to remove from all relations
    1454      * @return List of relations with roles the primitives was part of
     667     * Joins the given ways.
     668     * @param waysAndRelations Ways / Multipolygons to join
     669     * @since 7534
    1455670     */
    1456     private List<RelationRole> removeFromAllRelations(OsmPrimitive osm) {
    1457         List<RelationRole> result = new ArrayList<>();
     671    public void join(Collection<? extends OsmPrimitive> waysAndRelations) {
     672        waysAndRelations = waysAndRelations.stream()
     673                .filter(osm -> (osm instanceof Way && ((Way) osm).isClosed()) || osm.isMultipolygon())
     674                .collect(Collectors.toList());
     675        if (waysAndRelations.isEmpty()) {
     676            new Notification(tr("Please select at least one closed area that should be joined."))
     677                    .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
     678            return;
     679        }
    1458680
    1459         for (Relation r : osm.getDataSet().getRelations()) {
    1460             if (r.isDeleted()) {
    1461                 continue;
    1462             }
    1463             for (RelationMember rm : r.getMembers()) {
    1464                 if (rm.getMember() != osm) {
    1465                     continue;
    1466                 }
     681        if (!ofSameDataset(waysAndRelations)) {
     682            throw new IllegalArgumentException("Not in same DataSet");
     683        }
     684        waysAndRelations = selectRelationsInsteadOfMembers(waysAndRelations);
     685        DataSet ds = waysAndRelations.iterator().next().getDataSet();
    1467686
    1468                 Relation newRel = new Relation(r);
    1469                 List<RelationMember> members = newRel.getMembers();
    1470                 members.remove(rm);
    1471                 newRel.setMembers(members);
    1472 
    1473                 cmds.add(new ChangeCommand(r, newRel));
    1474                 RelationRole saverel = new RelationRole(r, rm.getRole());
    1475                 if (!result.contains(saverel)) {
    1476                     result.add(saverel);
    1477                 }
    1478                 break;
    1479             }
     687        if (!Command.checkAndConfirmOutlyingOperation("joinarea", tr("Join area confirmation"),
     688                trn("The selected area has nodes outside of the downloaded data region.",
     689                        "The selected areas have nodes outside of the downloaded data region.", waysAndRelations.size()) + "<br/>"
     690                        + tr("This can lead to nodes being deleted accidentally.") + "<br/>"
     691                        + tr("Are you really sure to continue?") + tr("Please abort if you are not sure"),
     692                tr("The selected area is incomplete. Continue?"), waysAndRelations, null)) {
     693            return;
    1480694        }
    1481695
    1482         commitCommands(marktr("Removed Element from Relations"));
    1483         return result;
    1484     }
     696        try {
     697            JoinAreasCollector collector = new JoinAreasCollector(ds, waysAndRelations);
    1485698
    1486     /**
    1487      * Adds the previously removed relations again to the outer way. If there are multiple multipolygon
    1488      * relations where the joined areas were in "outer" role a new relation is created instead with all
    1489      * members of both. This function depends on multigon relations to be valid already, it won't fix them.
    1490      * @param rels List of relations with roles the (original) ways were part of
    1491      * @param outer The newly created outer area/way
    1492      * @param ownMultipol elements to directly add as outer
    1493      * @param relationsToDelete set of relations to delete.
    1494      */
    1495     private void fixRelations(List<RelationRole> rels, Way outer, RelationRole ownMultipol, Set<Relation> relationsToDelete) {
    1496         List<RelationRole> multiouters = new ArrayList<>();
     699            List<Command> commands = collector.getCommands();
     700            Main.main.undoRedo.add(new SequenceCommand(tr("Join Areas"), commands));
    1497701
    1498         if (ownMultipol != null) {
    1499             multiouters.add(ownMultipol);
    1500         }
    1501 
    1502         for (RelationRole r : rels) {
    1503             if (r.rel.isMultipolygon() && "outer".equalsIgnoreCase(r.role)) {
    1504                 multiouters.add(r);
    1505                 continue;
    1506             }
    1507             // Add it back!
    1508             Relation newRel = new Relation(r.rel);
    1509             newRel.addMember(new RelationMember(r.role, outer));
    1510             cmds.add(new ChangeCommand(r.rel, newRel));
    1511         }
    1512 
    1513         OsmDataLayer layer = Main.getLayerManager().getEditLayer();
    1514         Relation newRel;
    1515         switch (multiouters.size()) {
    1516         case 0:
     702        } catch (UnclosedAreaException e) {
     703            new Notification(tr("One of the selected areas is not closed and therefore cannot be joined."))
     704                .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
    1517705            return;
    1518         case 1:
    1519             // Found only one to be part of a multipolygon relation, so just add it back as well
    1520             newRel = new Relation(multiouters.get(0).rel);
    1521             newRel.addMember(new RelationMember(multiouters.get(0).role, outer));
    1522             cmds.add(new ChangeCommand(multiouters.get(0).rel, newRel));
     706        } catch (JoinAreasException e) {
     707            new Notification(tr("One of the selected areas has an invalid geomerty."))
     708                .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
    1523709            return;
    1524         default:
    1525             // Create a new relation with all previous members and (Way)outer as outer.
    1526             newRel = new Relation();
    1527             for (RelationRole r : multiouters) {
    1528                 // Add members
    1529                 for (RelationMember rm : r.rel.getMembers()) {
    1530                     if (!newRel.getMembers().contains(rm)) {
    1531                         newRel.addMember(rm);
    1532                     }
    1533                 }
    1534                 // Add tags
    1535                 for (String key : r.rel.keySet()) {
    1536                     newRel.put(key, r.rel.get(key));
    1537                 }
    1538                 // Delete old relation
    1539                 relationsToDelete.add(r.rel);
    1540             }
    1541             newRel.addMember(new RelationMember("outer", outer));
    1542             cmds.add(layer != null ? new AddCommand(layer, newRel) : new AddCommand(outer.getDataSet(), newRel));
    1543710        }
    1544711    }
    1545712
    1546713    /**
    1547      * Remove all tags from the all the way
    1548      * @param ways The List of Ways to remove all tags from
     714     * If all members of a multipolygon are selected, ask the user to select the polygon instead of the ways.
     715     * @param currentSelection
     716     * @return The new list of primitives the user selected
    1549717     */
    1550     private void stripTags(Collection<Way> ways) {
    1551         for (Way w : ways) {
    1552             final Way wayWithoutTags = new Way(w);
    1553             wayWithoutTags.removeAll();
    1554             cmds.add(new ChangeCommand(w, wayWithoutTags));
    1555         }
    1556         /* I18N: current action printed in status display */
    1557         commitCommands(marktr("Remove tags from inner ways"));
    1558     }
     718    private Collection<? extends OsmPrimitive> selectRelationsInsteadOfMembers(Collection<? extends OsmPrimitive> currentSelection) {
     719        List<Relation> selectableMultipolygons = currentSelection.stream()
     720            .filter(osm -> osm.getType() == OsmPrimitiveType.WAY)
     721            // Get all multipolygons referred by the way
     722            .flatMap(osm -> osm.getReferrers().stream())
     723            .distinct()
     724            .filter(osm -> osm.isMultipolygon())
     725            .map(osm -> ((Relation) osm))
     726            // Filter for those that are completely selected
     727            .filter(r -> r.getMembers().stream().map(m -> m.getMember()).allMatch(currentSelection::contains))
     728            .collect(Collectors.toList());
    1559729
    1560     /**
    1561      * Takes the last cmdsCount actions back and combines them into a single action
    1562      * (for when the user wants to undo the join action)
    1563      * @param message The commit message to display
    1564      */
    1565     private void makeCommitsOneAction(String message) {
    1566         cmds.clear();
    1567         if (Main.main != null) {
    1568             UndoRedoHandler ur = Main.main.undoRedo;
    1569             int i = Math.max(ur.commands.size() - cmdsCount, 0);
    1570             for (; i < ur.commands.size(); i++) {
    1571                 cmds.add(ur.commands.get(i));
     730        if (!selectableMultipolygons.isEmpty()) {
     731            JPanel msg = new JPanel(new GridBagLayout());
     732            msg.add(new JMultilineLabel("<html>" +
     733                    tr("You selected the members of the following multipolygons. "
     734                            + "Do you want to join the polygons instead?")
     735                    + "<ul>" + selectableMultipolygons.stream()
     736                            .map(r -> "<li>" + r.getDisplayName(DefaultNameFormatter.getInstance()) + "</li>")
     737                            .collect(Collectors.joining())
     738                    + "</ul></html>"));
     739            boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
     740                    "join_areas_on_polygons",
     741                    Main.parent,
     742                    msg,
     743                    tr("Join multipolygons?"),
     744                    JOptionPane.YES_NO_OPTION,
     745                    JOptionPane.QUESTION_MESSAGE,
     746                    JOptionPane.YES_OPTION);
     747            if (answer) {
     748                // the new polygons
     749                HashSet<OsmPrimitive> select = new HashSet<>(selectableMultipolygons);
     750                // part of the selection that was untouched
     751                currentSelection.stream().filter(
     752                        w -> !(w instanceof Way && ((Way) w).getReferrers().stream().allMatch(selectableMultipolygons::contains))
     753                        ).forEach(select::add);
     754                return select;
    1572755            }
    1573 
    1574             for (i = 0; i < cmds.size(); i++) {
    1575                 ur.undo();
    1576             }
    1577756        }
     757        return currentSelection;
     758    }
    1578759
    1579         commitCommands(message == null ? marktr("Join Areas Function") : message);
    1580         cmdsCount = 0;
     760    private boolean ofSameDataset(Collection<? extends OsmPrimitive> waysAndRelations) {
     761        return waysAndRelations.stream().map(OsmPrimitive::getDataSet).distinct().count() <= 1;
    1581762    }
    1582763
     764    public static List<List<Node>> getOutline(DataSet data, Collection<? extends OsmPrimitive> primitives) throws JoinAreasException {
     765        return new JoinAreasCollector(data, primitives).getOutlines();
     766    }
     767
    1583768    @Override
    1584769    protected void updateEnabledState() {
    1585770        updateEnabledStateOnCurrentSelection();
  • src/org/openstreetmap/josm/actions/search/SearchAction.java

     
    525525     * @param mode the search mode to use
    526526     */
    527527    public static void search(String search, SearchMode mode) {
    528         final SearchSetting searchSetting = new SearchSetting();
    529         searchSetting.text = search;
    530         searchSetting.mode = mode;
    531         search(searchSetting);
     528        search(new SearchSetting(search, mode));
    532529    }
    533530
    534531    static void search(SearchSetting s) {
     
    544541     * @since 10457
    545542     */
    546543    public static Collection<OsmPrimitive> searchAndReturn(String search, SearchMode mode) {
    547         final SearchSetting searchSetting = new SearchSetting();
    548         searchSetting.text = search;
    549         searchSetting.mode = mode;
    550544        CapturingSearchReceiver receiver = new CapturingSearchReceiver();
    551         SearchTask.newSearchTask(searchSetting, receiver).run();
     545        SearchTask.newSearchTask(new SearchSetting(search, mode), receiver).run();
    552546        return receiver.result;
    553547    }
    554548
    555549    /**
     550     * Performs the search specified by the search string {@code search} and the search mode {@code mode} and returns the result of the search.
     551     *
     552     * @param search the search string to use
     553     * @param ds The dataset to search in
     554     * @param mode the search mode to use
     555     * @return The result of the search.
     556     * @since xxx
     557     */
     558    public static Collection<OsmPrimitive> searchAndReturn(String search, DataSet ds, SearchMode mode) {
     559        CapturingSearchReceiver receiver = new CapturingSearchReceiver();
     560        SearchTask.newSearchTask(new SearchSetting(search, mode), ds, receiver).run();
     561        return receiver.result;
     562    }
     563
     564    /**
    556565     * Interfaces implementing this may receive the result of the current search.
    557566     * @author Michael Zangl
    558567     * @since 10457
     
    735744         * Constructs a new {@code SearchSetting}.
    736745         */
    737746        public SearchSetting() {
    738             text = "";
    739             mode = SearchMode.replace;
     747            this("", SearchMode.replace);
    740748        }
    741749
    742750        /**
     751         * Constructs a new {@code SearchSetting} using the given text / mode
     752         * @param text The search definition
     753         * @param mode The search mode.
     754         */
     755        public SearchSetting(String text, SearchMode mode) {
     756            this.text = Objects.requireNonNull(text, "text");
     757            this.mode = Objects.requireNonNull(mode, "mode");
     758        }
     759
     760        /**
    743761         * Constructs a new {@code SearchSetting} from an existing one.
    744762         * @param original original search settings
    745763         */
  • src/org/openstreetmap/josm/tools/RightAndLefthandTraffic.java

     
    1010import java.io.PrintWriter;
    1111import java.io.Writer;
    1212import java.nio.charset.StandardCharsets;
    13 import java.util.ArrayList;
    1413import java.util.Collection;
    1514import java.util.Collections;
    1615import java.util.List;
    1716import java.util.Set;
     17import java.util.stream.Collectors;
     18import java.util.stream.Stream;
    1819
    1920import org.openstreetmap.josm.Main;
    2021import org.openstreetmap.josm.actions.JoinAreasAction;
    21 import org.openstreetmap.josm.actions.JoinAreasAction.JoinAreasResult;
    22 import org.openstreetmap.josm.actions.JoinAreasAction.Multipolygon;
    23 import org.openstreetmap.josm.actions.PurgeAction;
     22import org.openstreetmap.josm.actions.JoinAreasAction.JoinAreasException;
    2423import org.openstreetmap.josm.data.coor.LatLon;
    2524import org.openstreetmap.josm.data.osm.DataSet;
     25import org.openstreetmap.josm.data.osm.Node;
    2626import org.openstreetmap.josm.data.osm.OsmPrimitive;
    2727import org.openstreetmap.josm.data.osm.Relation;
    28 import org.openstreetmap.josm.data.osm.RelationMember;
    2928import org.openstreetmap.josm.data.osm.Way;
    3029import org.openstreetmap.josm.io.IllegalDataException;
    3130import org.openstreetmap.josm.io.OsmReader;
     
    6261     * TODO: Synchronization can be refined inside the {@link GeoPropertyIndex} as most look-ups are read-only.
    6362     */
    6463    public static synchronized void initialize() {
    65         Collection<Way> optimizedWays = loadOptimizedBoundaries();
    66         if (optimizedWays.isEmpty()) {
     64        DataSet optimizedWays = loadOptimizedBoundaries();
     65        if (optimizedWays.getWays().isEmpty()) {
    6766            optimizedWays = computeOptimizedBoundaries();
    6867            saveOptimizedBoundaries(optimizedWays);
    6968        }
    70         rlCache = new GeoPropertyIndex<>(new DefaultGeoProperty(optimizedWays), 24);
     69        rlCache = new GeoPropertyIndex<>(new DefaultGeoProperty(optimizedWays.getWays()), 24);
    7170    }
    7271
    73     private static Collection<Way> computeOptimizedBoundaries() {
    74         Collection<Way> ways = new ArrayList<>();
    75         Collection<OsmPrimitive> toPurge = new ArrayList<>();
     72    private static DataSet computeOptimizedBoundaries() {
    7673        // Find all outer ways of left-driving countries. Many of them are adjacent (African and Asian states)
    7774        DataSet data = Territories.getDataSet();
    78         Collection<Relation> allRelations = data.getRelations();
    79         Collection<Way> allWays = data.getWays();
    80         for (Way w : allWays) {
    81             if (LEFT.equals(w.get(DRIVING_SIDE))) {
    82                 addWayIfNotInner(ways, w);
    83             }
     75        Collection<OsmPrimitive> leftHandTraffic = data.getPrimitives(osm -> (osm instanceof Way || osm.isMultipolygon()) && LEFT.equals(osm.get(DRIVING_SIDE)));
     76
     77        List<List<Node>> outline;
     78        try {
     79            outline = JoinAreasAction.getOutline(data, leftHandTraffic);
     80        } catch (JoinAreasException e) {
     81            // This should not happen.
     82            Main.warn(e);
     83
     84            outline = leftHandTraffic.stream()
     85                    .flatMap(RightAndLefthandTraffic::generateSimpleOutline)
     86                    .map(Way::getNodes)
     87                    .collect(Collectors.toList());
    8488        }
    85         for (Relation r : allRelations) {
    86             if (r.isMultipolygon() && LEFT.equals(r.get(DRIVING_SIDE))) {
    87                 for (RelationMember rm : r.getMembers()) {
    88                     if (rm.isWay() && "outer".equals(rm.getRole()) && !RIGHT.equals(rm.getMember().get(DRIVING_SIDE))) {
    89                         addWayIfNotInner(ways, (Way) rm.getMember());
    90                     }
    91                 }
     89
     90        // Remove all ways that are not part of the outline.
     91        data.getWays().forEach(data::removePrimitive);
     92        data.getRelations().forEach(data::removePrimitive);
     93        outline.stream().forEach(nodes -> generateWay(nodes, data));
     94        data.getNodes().stream().filter(n -> n.getReferrers().isEmpty()).forEach(data::removePrimitive);
     95
     96        return data;
     97    }
     98
     99    private static void generateWay(List<Node> nodes, DataSet data) {
     100        for (Node node : nodes) {
     101            // Don't use stream, we can have dupplicate nodes.
     102            if (node.getDataSet() == null) {
     103                data.addPrimitive(node);
    92104            }
    93105        }
    94         toPurge.addAll(allRelations);
    95         toPurge.addAll(allWays);
    96         toPurge.removeAll(ways);
    97         // Remove ways from parent relations for following optimizations
    98         for (Relation r : OsmPrimitive.getParentRelations(ways)) {
    99             r.setMembers(null);
     106        Way w = new Way();
     107        w.setNodes(nodes);
     108        data.addPrimitive(w);
     109    }
     110
     111    /**
     112     * A dirty way to get all left hand traffic outlines.
     113     * @param osm The primitives
     114     * @return The collection of outline ways.
     115     */
     116    private static Stream<Way> generateSimpleOutline(OsmPrimitive osm) {
     117        if (osm instanceof Way) {
     118            return Stream.of((Way) osm);
     119        } else {
     120            return ((Relation) osm)
     121                    .getMembers()
     122                    .stream()
     123                    .filter(m -> "outer".equals(m.getRole()))
     124                    .map(m -> (Way) m.getMember());
    100125        }
    101         // Remove all tags to avoid any conflict
    102         for (Way w : ways) {
    103             w.removeAll();
    104         }
    105         // Purge all other ways and relations so dataset only contains lefthand traffic data
    106         new PurgeAction(false).getPurgeCommand(toPurge).executeCommand();
    107         // Combine adjacent countries into a single polygon
    108         Collection<Way> optimizedWays = new ArrayList<>();
    109         List<Multipolygon> areas = JoinAreasAction.collectMultipolygons(ways);
    110         if (areas != null) {
    111             try {
    112                 JoinAreasResult result = new JoinAreasAction(false).joinAreas(areas);
    113                 if (result.hasChanges()) {
    114                     for (Multipolygon mp : result.getPolygons()) {
    115                         optimizedWays.add(mp.getOuterWay());
    116                     }
    117                 }
    118             } catch (UserCancelException ex) {
    119                 Main.warn(ex);
    120             } catch (JosmRuntimeException ex) {
    121                 // Workaround to #10511 / #14185. To remove when #10511 is solved
    122                 Main.error(ex);
    123             }
    124         }
    125         if (optimizedWays.isEmpty()) {
    126             // Problem: don't optimize
    127             Main.warn("Unable to join left-driving countries polygons");
    128             optimizedWays.addAll(ways);
    129         }
    130         return optimizedWays;
    131126    }
    132127
    133128    /**
     
    150145        ways.add(w);
    151146    }
    152147
    153     private static void saveOptimizedBoundaries(Collection<Way> optimizedWays) {
    154         DataSet ds = optimizedWays.iterator().next().getDataSet();
     148    private static void saveOptimizedBoundaries(DataSet ds) {
    155149        File file = new File(Main.pref.getCacheDirectory(), "left-right-hand-traffic.osm");
    156150        try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);
    157151             OsmWriter w = OsmWriterFactory.createOsmWriter(new PrintWriter(writer), false, ds.getVersion())
     
    164158        }
    165159    }
    166160
    167     private static Collection<Way> loadOptimizedBoundaries() {
     161    private static DataSet loadOptimizedBoundaries() {
    168162        try (InputStream is = new FileInputStream(new File(Main.pref.getCacheDirectory(), "left-right-hand-traffic.osm"))) {
    169            return OsmReader.parseDataSet(is, null).getWays();
     163           return OsmReader.parseDataSet(is, null);
    170164        } catch (IllegalDataException | IOException ex) {
    171165            Main.trace(ex);
    172             return Collections.emptyList();
     166            return new DataSet();
    173167        }
    174168    }
    175169}