Ticket #14528: join-areas-rewrite.patch

File join-areas-rewrite.patch, 53.5 KB (added by michael2402, 7 years ago)
  • src/org/openstreetmap/josm/actions/JoinAreasAction.java

     
    55import static org.openstreetmap.josm.tools.I18n.tr;
    66import static org.openstreetmap.josm.tools.I18n.trn;
    77
     8import java.awt.GridBagLayout;
    89import java.awt.event.ActionEvent;
    910import java.awt.event.KeyEvent;
    1011import java.util.ArrayList;
     
    1213import java.util.Collections;
    1314import java.util.Comparator;
    1415import java.util.EnumSet;
     16import java.util.HashMap;
    1517import java.util.HashSet;
    1618import java.util.LinkedHashSet;
    1719import java.util.LinkedList;
     
    3133import java.util.stream.Stream;
    3234
    3335import javax.swing.JOptionPane;
     36import javax.swing.JPanel;
    3437
    3538import org.openstreetmap.josm.Main;
    3639import org.openstreetmap.josm.actions.ReverseWayAction.ReverseWayResult;
     
    3740import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult;
    3841import org.openstreetmap.josm.command.AddCommand;
    3942import org.openstreetmap.josm.command.ChangeCommand;
     43import org.openstreetmap.josm.command.ChangePropertyCommand;
    4044import org.openstreetmap.josm.command.Command;
    4145import org.openstreetmap.josm.command.DeleteCommand;
    4246import org.openstreetmap.josm.command.SequenceCommand;
     
    4650import org.openstreetmap.josm.data.osm.Node;
    4751import org.openstreetmap.josm.data.osm.NodePositionComparator;
    4852import org.openstreetmap.josm.data.osm.OsmPrimitive;
     53import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
    4954import org.openstreetmap.josm.data.osm.Relation;
    5055import org.openstreetmap.josm.data.osm.RelationMember;
    5156import org.openstreetmap.josm.data.osm.TagCollection;
    5257import org.openstreetmap.josm.data.osm.Way;
     58import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
     59import org.openstreetmap.josm.gui.DefaultNameFormatter;
    5360import org.openstreetmap.josm.gui.Notification;
    5461import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
    5562import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     63import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
    5664import org.openstreetmap.josm.tools.Geometry;
    5765import org.openstreetmap.josm.tools.JosmRuntimeException;
    5866import org.openstreetmap.josm.tools.Pair;
     
    7280    private final transient List<Relation> addedRelations = new LinkedList<>();
    7381
    7482    /**
     83     * Defines an exception while joining areas.
     84     * @author Michael Zangl
     85     */
     86    static class JoinAreasException extends Exception {
     87        public JoinAreasException(String message) {
     88            super(message);
     89        }
     90    }
     91
     92    static class UnclosedAreaException extends JoinAreasException {
     93
     94        private Pair<Node, Node> gap;
     95
     96        public UnclosedAreaException(Pair<Node, Node> gap) {
     97            super("Gap found between: " + gap.a + " and " + gap.b);
     98            this.gap = gap;
     99        }
     100
     101    }
     102
     103    static class SelfIntersectingAreaException extends JoinAreasException {
     104
     105        private Pair<UndirectedWaySegment, UndirectedWaySegment> intersect;
     106
     107        public SelfIntersectingAreaException(Pair<UndirectedWaySegment, UndirectedWaySegment> intersect) {
     108            super("Intersection found between: " + intersect.a + " and " + intersect.b);
     109            this.intersect = intersect;
     110        }
     111
     112    }
     113
     114    static class UndirectedWaySegment {
     115        private Node a;
     116        private Node b;
     117
     118        UndirectedWaySegment(Node a, Node b) {
     119            if (a == b) {
     120                throw new IllegalArgumentException("Way segment cannot start and end at the same node.");
     121            }
     122            this.a = a;
     123            this.b = b;
     124        }
     125
     126        public boolean hasEnd(Node current) {
     127            return a == current || b == current;
     128        }
     129
     130        public Node getOtherEnd(Node current) {
     131            if (current == a) {
     132                return b;
     133            } else if (current == b) {
     134                return a;
     135            } else {
     136                throw new IllegalArgumentException(current + " is not an endpoint");
     137            }
     138        }
     139
     140        public boolean intersects(UndirectedWaySegment other) {
     141            EastNorth intersection = getIntersectionPoint(other);
     142            return intersection != null;
     143        }
     144
     145        private EastNorth getIntersectionPoint(UndirectedWaySegment other) {
     146            EastNorth intersection = null;
     147            if (!hasEnd(other.a) && !hasEnd(other.b)) {
     148                // ignore just touching.
     149                intersection = Geometry.getSegmentSegmentIntersection(
     150                        a.getEastNorth(), b.getEastNorth(),
     151                        other.a.getEastNorth(), other.b.getEastNorth());
     152            }
     153            return intersection;
     154        }
     155
     156        @Override
     157        public int hashCode() {
     158            return a.hashCode() + b.hashCode();
     159        }
     160
     161        @Override
     162        public boolean equals(Object obj) {
     163            if (this.getClass() == obj.getClass()) {
     164                UndirectedWaySegment other = (UndirectedWaySegment) obj;
     165                return (a.equals(other.a) && b.equals(other.b)) || (a.equals(other.b) && b.equals(other.a));
     166            } else {
     167                return false;
     168            }
     169        }
     170
     171        @Override
     172        public String toString() {
     173            return "UndirectedWaySegment [" + a + ", " + b + "]";
     174        }
     175
     176    }
     177
     178    /**
     179     * This class defines an area that might be joined.
     180     * @author Michael Zangl
     181     */
     182    static class JoinableArea {
     183        /**
     184         * A list of Node->Node segments that compose this area.
     185         * You can reconstruct the interior of this area by XORing those lines.
     186         */
     187        private final HashSet<UndirectedWaySegment> waySegments = new HashSet<>();
     188        private final List<Way> ways = new ArrayList<>();
     189        private final List<Relation> relations = new ArrayList<>();
     190        private final Map<String, String> tags;
     191        private final OsmPrimitive basePrimitive;
     192
     193        JoinableArea(Way way) throws JoinAreasException {
     194            this(way, Collections.singleton(way), Collections.emptyList());
     195        }
     196
     197        JoinableArea(Relation relation) throws JoinAreasException {
     198            this(relation, getMembers(relation, "outer"), getMembers(relation, "inner"));
     199            relations.add(relation);
     200        }
     201
     202        /**
     203         * Creates a new joinable area.
     204         * @param base The primitive this area is for.
     205         * @param outer The ways that should be outer ways.
     206         * @param inner The ways that should be inner ways.
     207         * @throws JoinAreasException If the area is invalid
     208         */
     209        JoinableArea(OsmPrimitive base, Collection<Way> outer, Collection<Way> inner) throws JoinAreasException {
     210            basePrimitive = base;
     211            tags = new HashMap<>(base.getInterestingTags());
     212            tags.remove("type", "multipolygon");
     213
     214            try {
     215                for (Way o : outer) {
     216                    addWayForceNonintersecting(o);
     217                }
     218                Pair<Node, Node> outerGap = findGap();
     219                if (outerGap != null) {
     220                    throw new UnclosedAreaException(outerGap);
     221                }
     222
     223                for (Way i : inner) {
     224                    addWayForceNonintersecting(i);
     225                }
     226                Pair<Node, Node> innerGap = findGap();
     227                if (innerGap != null) {
     228                    throw new UnclosedAreaException(innerGap);
     229                }
     230            } catch (RuntimeException e) {
     231                throw BugReport.intercept(e).put("outer", outer).put("inner", inner);
     232            }
     233        }
     234
     235        /**
     236         * Check if this area is a valid closed area
     237         * @return The gap if there is one, null for closed areas.
     238         */
     239        private Pair<Node, Node> findGap() {
     240            HashSet<UndirectedWaySegment> leftOver = new HashSet<>(waySegments);
     241            while (!leftOver.isEmpty()) {
     242                LinkedList<Node> part = removeOutlinePart(leftOver);
     243                if (part.getFirst() != part.getLast()) {
     244                    return new Pair<>(part.getFirst(), part.getLast());
     245                }
     246            }
     247            return null;
     248        }
     249
     250        /**
     251         * Add a new Way to the outline (outer or inner) of this area.
     252         * @param way The way.
     253         * @throws SelfIntersectingAreaException If the way self-intersects
     254         */
     255        private void addWayForceNonintersecting(Way way) throws SelfIntersectingAreaException {
     256            for (Pair<Node, Node> pair : way.getNodePairs(false)) {
     257                this.addWayForceNonintersecting(new UndirectedWaySegment(pair.a, pair.b));
     258            }
     259            ways .add(way);
     260        }
     261
     262        private void addWayForceNonintersecting(UndirectedWaySegment s) throws SelfIntersectingAreaException {
     263            if (waySegments.contains(s)) {
     264                // We add a way segment twice. This means that the outline of the area contains this segment twice.
     265                // This cancels out, so we remove the segment,
     266                waySegments.remove(s);
     267            } else {
     268                // Now check for intersections
     269                Optional<UndirectedWaySegment> intersection = waySegments.stream().filter(s::intersects).findAny();
     270                if (intersection.isPresent()) {
     271                    throw new SelfIntersectingAreaException(new Pair<>(intersection.get(), s));
     272                }
     273                waySegments.add(s);
     274            }
     275        }
     276
     277        private static Collection<Way> getMembers(Relation relation, String role) {
     278            return relation.getMembers().stream().filter(m -> role.equals(m.getRole()))
     279                    .filter(m -> OsmPrimitiveType.WAY.equals(m.getType())).map(m -> m.getWay())
     280                    .collect(Collectors.toList());
     281        }
     282
     283        /**
     284         * Check if the area contains a segment.
     285         * @param segment The segment. Assumed to not intersect any of our borders.
     286         * @return true if the segment is inside. False if it is on the outline or outside.
     287         */
     288        public boolean contains(UndirectedWaySegment segment) {
     289            if (waySegments.contains(segment)) {
     290                return false;
     291            }
     292            // 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.
     293            // Computation is done in East/North space.
     294            // We use a ray at a fixed yRay coordinate that ends at xRay;
     295            // we need to make sure this ray does not go into the same direction the way is going.
     296            // This is done by rotating by 90° if we need to.
     297
     298            int intersections = 0;
     299            // Use some "random" start point on the segment
     300            EastNorth rayNode1 = segment.a.getEastNorth();
     301            EastNorth rayNode2 = segment.b.getEastNorth();
     302            EastNorth rayFrom = rayNode1.getCenter(rayNode2);
     303
     304            // Now find the x/y mapping function. We need to ensure that rayNode1->rayNode2 is not parallel to our x axis.
     305            ToDoubleFunction<EastNorth> x;
     306            ToDoubleFunction<EastNorth> y;
     307            if (Math.abs(rayNode1.east() - rayNode2.east()) < Math.abs(rayNode1.north() - rayNode2.north())) {
     308                x = en -> en.east();
     309                y = en -> en.north();
     310            } else {
     311                x = en -> -en.north();
     312                y = en -> en.east();
     313            }
     314
     315            double xRay = x.applyAsDouble(rayFrom);
     316            double yRay = y.applyAsDouble(rayFrom);
     317
     318            for (UndirectedWaySegment part : waySegments) {
     319                // intersect against all way segments
     320                EastNorth n1 = part.a.getEastNorth();
     321                EastNorth n2 = part.b.getEastNorth();
     322                if ((rayNode1.equals(n1) && rayNode2.equals(n2)) || (rayNode2.equals(n1) && rayNode1.equals(n2))) {
     323                    // This is the segment we are starting the ray from.
     324                    // We ignore this to avoid rounding errors.
     325                    continue;
     326                }
     327
     328                double x1 = x.applyAsDouble(n1);
     329                double x2 = x.applyAsDouble(n2);
     330                double y1 = y.applyAsDouble(n1);
     331                double y2 = y.applyAsDouble(n2);
     332
     333                if (!((y1 <= yRay && yRay < y2) || (y2 <= yRay && yRay < y1))) {
     334                    // No intersection, since segment is above/below ray
     335                    continue;
     336                }
     337                double xIntersect = x1 + (x2 - x1) * (yRay - y1) / (y2 - y1);
     338                double onLine = xIntersect / xRay;
     339                if (Math.abs(onLine - 1) < 1e-10) {
     340                    // Lines that are directly on each other are considered outside.
     341                    return false;
     342                }
     343                if (xIntersect < xRay) {
     344                    intersections++;
     345                }
     346            }
     347
     348            return intersections % 2 == 1;
     349        }
     350
     351        public Collection<UndirectedWaySegment> getSegments() {
     352            return Collections.unmodifiableCollection(waySegments);
     353        }
     354    }
     355
     356    /**
     357     * A hash set with an xor method.
     358     * @param <T> element type
     359     */
     360    private static class XOrHashSet<T> extends HashSet<T> {
     361        public XOrHashSet() {
     362            super();
     363        }
     364
     365        public XOrHashSet(Collection<? extends T> c) {
     366            super(c);
     367        }
     368
     369        public void xor(T e) {
     370            if (!this.add(e)) {
     371                this.remove(e);
     372            }
     373        }
     374    }
     375
     376    /**
     377     * This class collects the areas to be joined.
     378     */
     379    static class JoinAreasCollector {
     380        private final List<Node> possibleNewNodes = new ArrayList<>();
     381        private final List<JoinableArea> unionOf = new ArrayList<>();
     382        /**
     383         * All hash sets that may be
     384         */
     385        private final XOrHashSet<UndirectedWaySegment> waySegments = new XOrHashSet<>();
     386        private final DataSet ds;
     387        private final Map<String, String> tags;
     388
     389        JoinAreasCollector(DataSet ds, Collection<? extends OsmPrimitive> waysAndRelations) throws JoinAreasException {
     390            this.ds = ds;
     391            Collection<JoinableArea> collectAreas = collectAreas(waysAndRelations);
     392            collectAreas.forEach(this::unionWithArea);
     393
     394            tags = unionOf.isEmpty() ? Collections.emptyMap() : unionOf.iterator().next().tags;
     395        }
     396
     397        private static Collection<JoinableArea> collectAreas(Collection<? extends OsmPrimitive> waysAndRelations) throws JoinAreasException {
     398            Collection<JoinableArea> areas = new ArrayList<>();
     399            for(OsmPrimitive osm : waysAndRelations) {
     400                if (osm instanceof Way) {
     401                    areas.add(new JoinableArea((Way) osm));
     402                } else if (osm instanceof Relation) {
     403                    areas.add(new JoinableArea((Relation) osm));
     404                }
     405            }
     406            return areas;
     407        }
     408
     409        void unionWithArea(JoinableArea area) {
     410            Collection<UndirectedWaySegment> segments = area.getSegments();
     411
     412            // Our worker list. Once a way is split, it is re-added to the wroker to check for more splits.
     413            XOrHashSet<UndirectedWaySegment> toAdd = new XOrHashSet<>(segments);
     414            while (!toAdd.isEmpty()) {
     415                UndirectedWaySegment s = toAdd.iterator().next();
     416                toAdd.remove(s);
     417                Optional<UndirectedWaySegment> intersects = waySegments.stream().filter(s::intersects).findAny();
     418                if (intersects.isPresent()) {
     419                    EastNorth intersection = s.getIntersectionPoint(intersects.get());
     420                     // Now generate two segments around the intersection.
     421                    waySegments.remove(intersects.get());
     422                    // TODO: Find a node close to newNode to handle intersections of 3 or more lines.
     423                    Node newNode = new Node(intersection);
     424                    possibleNewNodes.add(newNode);
     425                    // We use xor here to fix ways that e.g. reverse on themselves.
     426                    waySegments.xor(new UndirectedWaySegment(intersects.get().a, newNode));
     427                    waySegments.xor(new UndirectedWaySegment(newNode, intersects.get().b));
     428
     429                    toAdd.xor(new UndirectedWaySegment(s.a, newNode));
     430                    toAdd.xor(new UndirectedWaySegment(newNode, s.b));
     431                } else {
     432                    // No more intersections - we add that segment to our geometry
     433                    waySegments.xor(s);
     434                }
     435            }
     436
     437            unionOf.add(area);
     438        }
     439
     440        private boolean allAreasHaveSameTags() {
     441            return unionOf.stream().allMatch(area -> area.tags.equals(tags));
     442        }
     443
     444        /**
     445         * Gets the commands that are required to join the areas.
     446         * @return The join commands.
     447         */
     448        public List<Command> getCommands() {
     449            if (unionOf.isEmpty()) {
     450                return Collections.emptyList();
     451            }
     452            Collection<UndirectedWaySegment> outline = computeOutline();
     453
     454            List<Command> commands = new ArrayList<>();
     455            // The primitives of which we should remove the tags.
     456            List<OsmPrimitive> toRemoveTags = new ArrayList<>();
     457            unionOf.stream().map(area -> area.basePrimitive).forEach(toRemoveTags::add);
     458
     459            // Add the split nodes
     460            // Remove nodes of interior segments.
     461            possibleNewNodes.stream()
     462                .filter(n -> outline.stream().filter(w -> w.hasEnd(n)).findAny().isPresent())
     463                .map(n -> new AddCommand(ds, n))
     464                .forEach(commands::add);
     465
     466            // Now search all ways which are completely used in our new geometry (e.g. multipolygon inners, ...)
     467            // We should not change those ways.
     468            List<Way> outlineWays = new ArrayList<>();
     469            List<UndirectedWaySegment> segmentsToContain = new ArrayList<>(outline);
     470            for (Way preserve : findOutlinesToPreserve(segmentsToContain)) {
     471                List<UndirectedWaySegment> preservedSegments = segmentsForWay(preserve);
     472                if (preservedSegments.size() != preservedSegments.stream().distinct().count()) {
     473                    // This way contains a segment twice. Skip it, we want to fix this.
     474                    continue;
     475                }
     476                if (!segmentsToContain.containsAll(preservedSegments)) {
     477                    // it may happen that two outlines that should be preserved happen to be on the same segment
     478                    // We need to ignore the second one then.
     479                    continue;
     480                }
     481                outlineWays.add(preserve);
     482                segmentsToContain.removeAll(preservedSegments);
     483            }
     484
     485            // Multipolygons that were selected and can now be removed
     486            List<Relation> relationsToRemove = unionOf.stream().flatMap(area -> area.relations.stream())
     487                    .distinct().collect(Collectors.toList());
     488            toRemoveTags.removeAll(relationsToRemove);
     489
     490            // Compute the ways that need to be removed.
     491            // Those are all ways of the old geometry that are not used in any other place.
     492            List<Way> waysToRemove = unionOf.stream().flatMap(area -> area.ways.stream())
     493                    .distinct()
     494                    .filter(way -> !outlineWays.contains(way))
     495                    // Preserve ways that are member in any relation that we did not modify
     496                    .filter(way -> way.getReferrers().stream().allMatch(relationsToRemove::contains))
     497                    // Preserve ways that have tags
     498                    .filter(way -> toRemoveTags.contains(way) || way.getInterestingTags().isEmpty())
     499                    .collect(Collectors.toList());
     500            toRemoveTags.removeAll(waysToRemove);
     501
     502            // Now we are left with the remaining outline in the segmentsToContain array.
     503            // For each chunk in that outline, we create a new way
     504            // TODO: We can reuse the ways we would delete otherwise.
     505            while (!segmentsToContain.isEmpty()) {
     506                List<Node> wayToCreate = removeOutlinePart(segmentsToContain);
     507                Way osm = new Way();
     508                osm.setNodes(wayToCreate);
     509                outlineWays.add(osm);
     510                commands.add(new AddCommand(ds, osm));
     511            }
     512
     513            // Now it is time to generate the final area.
     514            if (outlineWays.isEmpty()) {
     515                throw new AssertionError("No outline ways found.");
     516            } else if (outlineWays.size() == 1) {
     517                // We only have one way. Add the tags to that way.
     518                outlineWays.get(0).setKeys(tags);
     519            } else {
     520                // find a relation. Use the more complex multipolygon when merging two of them.
     521                Relation multipolygon = relationsToRemove.stream().sorted(Comparator.comparingInt(r -> -r.getMembersCount()))
     522                        .findFirst().orElseGet(Relation::new);
     523                multipolygon.setKeys(tags);
     524                Pair<Relation, Relation> update = CreateMultipolygonAction.updateMultipolygonRelation(outlineWays, multipolygon);
     525                if (update == null) {
     526                    throw new AssertionError("The outline ways should be continuous but no multipolygon could be created.");
     527                }
     528                if (update.a.getDataSet() == null) {
     529                    // used the fake relation.
     530                    commands.add(new AddCommand(update.b));
     531                } else {
     532                    commands.add(new ChangeCommand(update.a, update.b));
     533                }
     534                relationsToRemove.remove(multipolygon);
     535            }
     536
     537            // Apply deletion of the primitives we don't need any more.
     538            if (!relationsToRemove.isEmpty()) {
     539                commands.add(new DeleteCommand(ds, relationsToRemove));
     540            }
     541            if (!waysToRemove.isEmpty()) {
     542                commands.add(new DeleteCommand(ds, waysToRemove));
     543            }
     544            for(OsmPrimitive osm : toRemoveTags) {
     545                for (String key : osm.getKeys().keySet()) {
     546                    commands.add(new ChangePropertyCommand(osm, key, ""));
     547                }
     548            }
     549
     550            return commands;
     551        }
     552
     553        private List<UndirectedWaySegment> segmentsForWay(Way way) {
     554            return way.getNodePairs(false).stream()
     555                    .map(pair -> new UndirectedWaySegment(pair.a, pair.b)).collect(Collectors.toList());
     556        }
     557
     558        private List<Way> findOutlinesToPreserve(List<UndirectedWaySegment> segmentsToContain) {
     559            return unionOf.stream().flatMap(u -> u.ways.stream())
     560                .filter(w -> segmentsToContain.containsAll(segmentsForWay(w))).collect(Collectors.toList());
     561        }
     562
     563        private Collection<UndirectedWaySegment> computeOutline() {
     564            return waySegments.stream().filter(
     565                        seg -> unionOf.stream().noneMatch(area -> area.contains(seg))
     566                    ).collect(Collectors.toList());
     567        }
     568    }
     569
     570    private static LinkedList<Node> removeOutlinePart(Collection<UndirectedWaySegment> segmentsToContain) {
     571        Node start = segmentsToContain.iterator().next().a;
     572        LinkedList<Node> nodes = new LinkedList<>();
     573        nodes.add(start);
     574        // Move in one direction on that way.
     575        while ((nodes.size() < 2 || nodes.getFirst() != nodes.getLast()) && !segmentsToContain.isEmpty()) {
     576            Optional<UndirectedWaySegment> traverse = segmentsToContain.stream().filter(s -> s.hasEnd(nodes.getLast())).findAny();
     577            if (!traverse.isPresent()) {
     578                break;
     579            }
     580            segmentsToContain.remove(traverse.get());
     581            nodes.addLast(traverse.get().getOtherEnd(nodes.getLast()));
     582        }
     583
     584        // Now move in the other direction - as far as we can go.
     585        while ((nodes.size() < 2 || nodes.getFirst() != nodes.getLast()) && !segmentsToContain.isEmpty()) {
     586            Optional<UndirectedWaySegment> traverse = segmentsToContain.stream().filter(s -> s.hasEnd(nodes.getFirst())).findAny();
     587            if (!traverse.isPresent()) {
     588                break;
     589            }
     590            segmentsToContain.remove(traverse.get());
     591            nodes.addFirst(traverse.get().getOtherEnd(nodes.getFirst()));
     592        }
     593
     594        return nodes;
     595    }
     596
     597    /**
    75598     * This helper class describes join areas action result.
    76599     * @author viesturs
    77600     */
    78601    public static class JoinAreasResult {
    79 
    80602        private final boolean hasChanges;
    81603        private final List<Multipolygon> polygons;
    82604
     
    155677
    156678        @Override
    157679        public boolean equals(Object other) {
    158             if (this == other) return true;
    159             if (other == null || getClass() != other.getClass()) return false;
     680            if (this == other)
     681                return true;
     682            if (other == null || getClass() != other.getClass())
     683                return false;
    160684            RelationRole that = (RelationRole) other;
    161             return Objects.equals(rel, that.rel) &&
    162                     Objects.equals(role, that.role);
     685            return Objects.equals(rel, that.rel) && Objects.equals(role, that.role);
    163686        }
    164687    }
    165688
     
    185708
    186709        @Override
    187710        public boolean equals(Object other) {
    188             if (this == other) return true;
    189             if (other == null || getClass() != other.getClass()) return false;
     711            if (this == other)
     712                return true;
     713            if (other == null || getClass() != other.getClass())
     714                return false;
    190715            WayInPolygon that = (WayInPolygon) other;
    191             return insideToTheRight == that.insideToTheRight &&
    192                     Objects.equals(way, that.way);
     716            return insideToTheRight == that.insideToTheRight && Objects.equals(way, that.way);
    193717        }
    194718
    195719        @Override
     
    232756         * Inverse inside and outside
    233757         */
    234758        public void reverse() {
    235             for (WayInPolygon way: ways) {
     759            for (WayInPolygon way : ways) {
    236760                way.insideToTheRight = !way.insideToTheRight;
    237761            }
    238762            Collections.reverse(ways);
     
    338862            EastNorth en1 = n1.getEastNorth();
    339863            EastNorth en2 = n2.getEastNorth();
    340864            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;
     865            double angle = Math.atan2(en3.getY() - en1.getY(), en3.getX() - en1.getX())
     866                    - Math.atan2(en2.getY() - en1.getY(), en2.getX() - en1.getX());
     867            while (angle >= 2 * Math.PI) {
     868                angle -= 2 * Math.PI;
    345869            }
    346870            while (angle < 0) {
    347                 angle += 2*Math.PI;
     871                angle += 2 * Math.PI;
    348872            }
    349873            return angle;
    350874        }
     
    362886
    363887            // Pairs of (way, nextNode)
    364888            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))))
     889                    availableWays.stream().filter(way -> way.way.firstNode().equals(headNode) && way.insideToTheRight)
     890                            .map(way -> new Pair<>(way, way.way.getNode(1))),
     891                    availableWays.stream().filter(way -> way.way.lastNode().equals(headNode) && !way.insideToTheRight)
     892                            .map(way -> new Pair<>(way, way.way.getNode(way.way.getNodesCount() - 2))))
    371893
    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);
     894            // now find the way with the best angle
     895                    .min(Comparator.comparingDouble(wayAndNext -> {
     896                        Node nextNode = wayAndNext.b;
     897                        if (nextNode == prevNode) {
     898                            // we always prefer going back.
     899                            return Double.POSITIVE_INFINITY;
     900                        }
     901                        double angle = Math.atan2(nextNode.getEastNorth().east() - headNode.getEastNorth().east(),
     902                                nextNode.getEastNorth().north() - headNode.getEastNorth().north()) - headAngle;
     903                        if (angle > Math.PI)
     904                            angle -= 2 * Math.PI;
     905                        if (angle <= -Math.PI)
     906                            angle += 2 * Math.PI;
     907                        return angle;
     908                    })).map(wayAndNext -> wayAndNext.a).orElse(null);
    387909            lastWayReverse = lastWay != null && !lastWay.insideToTheRight;
    388910            return lastWay;
    389911        }
     
    398920
    399921            WayInPolygon mostLeft = null; // most left way connected to head node
    400922            boolean comingToHead = false; // true if candidate come to head node
    401             double angle = 2*Math.PI;
     923            double angle = 2 * Math.PI;
    402924
    403925            for (WayInPolygon candidateWay : availableWays) {
    404926                boolean candidateComingToHead;
     
    408930                    candidateComingToHead = !candidateWay.insideToTheRight;
    409931                    candidatePrevNode = candidateWay.way.getNode(1);
    410932                } else if (candidateWay.way.lastNode().equals(headNode)) {
    411                      candidateComingToHead = candidateWay.insideToTheRight;
    412                      candidatePrevNode = candidateWay.way.getNode(candidateWay.way.getNodesCount() - 2);
     933                    candidateComingToHead = candidateWay.insideToTheRight;
     934                    candidatePrevNode = candidateWay.way.getNode(candidateWay.way.getNodesCount() - 2);
    413935                } else
    414936                    continue;
    415937                if (candidateComingToHead && candidateWay.equals(lastWay))
     
    417939
    418940                double candidateAngle = getAngle(headNode, candidatePrevNode, prevNode);
    419941
    420                 if (mostLeft == null || candidateAngle < angle || (Utils.equalsEpsilon(candidateAngle, angle) && !candidateComingToHead)) {
     942                if (mostLeft == null || candidateAngle < angle
     943                        || (Utils.equalsEpsilon(candidateAngle, angle) && !candidateComingToHead)) {
    421944                    // Candidate is most left
    422945                    mostLeft = candidateWay;
    423946                    comingToHead = candidateComingToHead;
     
    462985     * @since 11611
    463986     */
    464987    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);
     988        super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"),
     989                addShortcut ? Shortcut.registerShortcut("tools:joinareas",
     990                        tr("Tool: {0}", tr("Join overlapping Areas")), KeyEvent.VK_J, Shortcut.SHIFT) : null,
     991                true);
    468992    }
    469993
    470994    /**
     
    473997     */
    474998    @Override
    475999    public void actionPerformed(ActionEvent e) {
    476         join(Main.getLayerManager().getEditDataSet().getSelectedWays());
     1000        join(Main.getLayerManager().getEditDataSet().getSelected());
    4771001    }
    4781002
    4791003    /**
    4801004     * Joins the given ways.
    481      * @param ways Ways to join
     1005     * @param waysAndRelations Ways / Multipolygons to join
    4821006     * @since 7534
    4831007     */
    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();
     1008    public void join(Collection<? extends OsmPrimitive> waysAndRelations) {
     1009        if (waysAndRelations.isEmpty()) {
     1010            new Notification(tr("Please select at least one closed area that should be joined."))
     1011                    .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
    4921012            return;
    4931013        }
    4941014
    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;
    503             }
    504 
    505             allNodes.addAll(way.getNodes());
     1015        if (!ofSameDataset(waysAndRelations)) {
     1016            throw new IllegalArgumentException("Not in same DataSet");
    5061017        }
     1018        waysAndRelations = selectRelationsInsteadOfMembers(waysAndRelations);
     1019        DataSet ds = waysAndRelations.iterator().next().getDataSet();
    5071020
    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;
    519 
    520         //analyze multipolygon relations and collect all areas
    521         List<Multipolygon> areas = collectMultipolygons(ways);
    522 
    523         if (areas == null)
    524             //too complex multipolygon relations found
     1021        if (!Command.checkAndConfirmOutlyingOperation("joinarea", tr("Join area confirmation"),
     1022                trn("The selected area has nodes outside of the downloaded data region.",
     1023                        "The selected areas have nodes outside of the downloaded data region.", waysAndRelations.size()) + "<br/>"
     1024                        + tr("This can lead to nodes being deleted accidentally.") + "<br/>"
     1025                        + tr("Are you really sure to continue?") + tr("Please abort if you are not sure"),
     1026                tr("The selected area is incomplete. Continue?"), waysAndRelations, null)) {
    5251027            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;
    5331028        }
    5341029
    535         if (!resolveTagConflicts(areas))
    536             return;
    537         //user canceled, do nothing.
    538 
    5391030        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();
     1031            JoinAreasCollector collector = new JoinAreasCollector(ds, waysAndRelations);
     1032            if (!collector.allAreasHaveSameTags()) {
     1033                new Notification(tr("Only areas with the same tags can be joined."))
     1034                    .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
     1035                return;
     1036            }
    5441037
    545             // Do the job of joining areas
    546             JoinAreasResult result = joinAreas(areas);
     1038            List<Command> commands = collector.getCommands();
     1039            commitCommand(new SequenceCommand(tr("Join Areas"), commands));
    5471040
    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));
    553                 }
    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);
    560                 }
    561                 if (ds != null) {
    562                     ds.setSelected(allWays);
    563                 }
    564             } else {
    565                 new Notification(
    566                         tr("No intersection found. Nothing was changed."))
    567                         .setIcon(JOptionPane.INFORMATION_MESSAGE)
    568                         .show();
    569             }
    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();
     1041        } catch (UnclosedAreaException e) {
     1042            new Notification(tr("One of the selected areas is not closed and therefore cannot be joined."))
     1043                .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
     1044            return;
     1045        } catch (JoinAreasException e) {
     1046            new Notification(tr("One of the selected areas has an invalid geomerty."))
     1047                .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
     1048            return;
    5771049        }
    5781050    }
    5791051
    5801052    /**
    581      * Tests if the areas have some intersections to join.
    582      * @param areas Areas to test
    583      * @return {@code true} if areas are joinable
     1053     * If all members of a multipolygon are selected, ask the user to select the polygon instead of the ways.
     1054     * @param currentSelection
     1055     * @return The new list of primitives the user selected
    5841056     */
    585     private boolean testJoin(List<Multipolygon> areas) {
    586         List<Way> allStartingWays = new ArrayList<>();
     1057    private Collection<? extends OsmPrimitive> selectRelationsInsteadOfMembers(Collection<? extends OsmPrimitive> currentSelection) {
     1058        List<Relation> selectableMultipolygons = currentSelection.stream()
     1059            .filter(osm -> osm.getType() == OsmPrimitiveType.WAY)
     1060            // Get all multipolygons refferred by the way
     1061            .flatMap(osm -> osm.getReferrers().stream())
     1062            .distinct()
     1063            .filter(osm -> osm.isMultipolygon())
     1064            .map(osm -> ((Relation) osm))
     1065            // Filter for those that are completely selected
     1066            .filter(r -> r.getMembers().stream().map(m -> m.getMember()).allMatch(currentSelection::contains))
     1067            .collect(Collectors.toList());
    5871068
    588         for (Multipolygon area : areas) {
    589             allStartingWays.add(area.outerWay);
    590             allStartingWays.addAll(area.innerWays);
     1069        if (!selectableMultipolygons.isEmpty()) {
     1070            JPanel msg = new JPanel(new GridBagLayout());
     1071            msg.add(new JMultilineLabel("<html>" +
     1072                    tr("You selected the members of the following multipolygons. "
     1073                            + "Do you want to join the polygons instead?")
     1074                    + "<ul>" + selectableMultipolygons.stream()
     1075                            .map(r -> "<li>" + r.getDisplayName(DefaultNameFormatter.getInstance()) + "</li>")
     1076                            .collect(Collectors.joining())
     1077                    + "</ul></html>"));
     1078            boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
     1079                    "join_areas_on_polygons",
     1080                    Main.parent,
     1081                    msg,
     1082                    tr("Join multipolygons?"),
     1083                    JOptionPane.YES_NO_OPTION,
     1084                    JOptionPane.QUESTION_MESSAGE,
     1085                    JOptionPane.YES_OPTION);
     1086            if (answer) {
     1087                HashSet<OsmPrimitive> select = new HashSet<>(selectableMultipolygons);
     1088                currentSelection.stream().filter(
     1089                        w -> !(w instanceof Way && ((Way) w).getReferrers().stream().allMatch(selectableMultipolygons::contains))
     1090                        ).forEach(select::add);
     1091                return select;
     1092            }
    5911093        }
     1094        return currentSelection;
     1095    }
    5921096
    593         //find intersection points
    594         Set<Node> nodes = Geometry.addIntersections(allStartingWays, true, cmds);
    595         return !nodes.isEmpty();
     1097    private boolean ofSameDataset(Collection<? extends OsmPrimitive> waysAndRelations) {
     1098        return waysAndRelations.stream().map(OsmPrimitive::getDataSet).distinct().count() <= 1;
    5961099    }
    5971100
    5981101    private static class DuplicateWayCollectorAccu {
    599            private List<Way> currentWays = new ArrayList<>();
    600            private List<Way> duplicatesFound = new ArrayList<>();
     1102        private List<Way> currentWays = new ArrayList<>();
     1103        private List<Way> duplicatesFound = new ArrayList<>();
    6011104
    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            }
     1105        private void add(Way way) {
     1106            List<Node> wayNodes = way.getNodes();
     1107            List<Node> wayNodesReversed = way.getNodes();
     1108            Collections.reverse(wayNodesReversed);
     1109            Optional<Way> duplicate = currentWays.stream().filter(
     1110                    current -> current.getNodes().equals(wayNodes) || current.getNodes().equals(wayNodesReversed))
     1111                    .findFirst();
     1112            if (duplicate.isPresent()) {
     1113                currentWays.remove(duplicate.get());
     1114                duplicatesFound.add(duplicate.get());
     1115                duplicatesFound.add(way);
     1116            } else {
     1117                currentWays.add(way);
     1118            }
     1119        }
    6171120
    618            private DuplicateWayCollectorAccu combine(DuplicateWayCollectorAccu a2) {
    619                duplicatesFound.addAll(a2.duplicatesFound);
    620                a2.currentWays.forEach(this::add);
    621                return this;
    622            }
     1121        private DuplicateWayCollectorAccu combine(DuplicateWayCollectorAccu a2) {
     1122            duplicatesFound.addAll(a2.duplicatesFound);
     1123            a2.currentWays.forEach(this::add);
     1124            return this;
     1125        }
    6231126    }
    6241127
    6251128    /**
     
    7091212        List<WayInPolygon> preparedWays = new ArrayList<>();
    7101213
    7111214        // 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());
     1215        List<Way> splitOuterWays = outerStartingWays.stream().flatMap(way -> splitWayOnNodes(way, nodes).stream())
     1216                .collect(Collectors.toList());
     1217        List<Way> splitInnerWays = innerStartingWays.stream().flatMap(way -> splitWayOnNodes(way, nodes).stream())
     1218                .collect(Collectors.toList());
    7161219
    7171220        // remove duplicate ways (A->B->C and C->B->A)
    718         List<Way> duplicates = Stream.concat(splitOuterWays.stream(), splitInnerWays.stream()).collect(new DuplicateWayCollector());
     1221        List<Way> duplicates = Stream.concat(splitOuterWays.stream(), splitInnerWays.stream())
     1222                .collect(new DuplicateWayCollector());
    7191223
    7201224        splitOuterWays.removeAll(duplicates);
    7211225        splitInnerWays.removeAll(duplicates);
     
    7541258
    7551259        commitCommands(marktr("Assemble new polygons"));
    7561260
    757         for (Relation rel: relationsToDelete) {
     1261        for (Relation rel : relationsToDelete) {
    7581262            cmds.add(new DeleteCommand(rel));
    7591263        }
    7601264
     
    7741278        if (warnAboutRelations) {
    7751279            new Notification(
    7761280                    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();
     1281                            .setIcon(JOptionPane.INFORMATION_MESSAGE).setDuration(Notification.TIME_LONG).show();
    7801282        }
    7811283
    7821284        return new JoinAreasResult(true, polygons);
     
    7881290     * @return {@code true} if all conflicts are resolved, {@code false} if conflicts remain.
    7891291     */
    7901292    private boolean resolveTagConflicts(List<Multipolygon> polygons) {
    791 
     1293        //TODO: Use this
    7921294        List<Way> ways = new ArrayList<>();
    7931295
    7941296        for (Multipolygon pol : polygons) {
     
    8791381     * @param description The description of what the commands do
    8801382     */
    8811383    private void commitCommands(String description) {
    882         switch(cmds.size()) {
     1384        switch (cmds.size()) {
    8831385        case 0:
    8841386            return;
    8851387        case 1:
     
    9041406
    9051407    /**
    9061408     * This method analyzes the way and assigns each part what direction polygon "inside" is.
     1409     *
     1410     * It uses an even/odd winding rule.
     1411     *
    9071412     * @param parts the split parts of the way
    9081413     * @param isInner - if true, reverts the direction (for multipolygon islands)
    9091414     * @return list of parts, marked with the inside orientation.
     
    9831488
    9841489        if (chunks.size() > 1) {
    9851490            SplitWayResult split = SplitWayAction.splitWay(getLayerManager().getEditLayer(), way, chunks,
    986                     Collections.<OsmPrimitive>emptyList(), SplitWayAction.Strategy.keepFirstChunk());
     1491                    Collections.<OsmPrimitive> emptyList(), SplitWayAction.Strategy.keepFirstChunk());
    9871492
    9881493            if (split != null) {
    9891494                //execute the command, we need the results
     
    11161621        // This seems to appear when is apply over invalid way like #9911 test-case
    11171622        // Remove all of these way to make the next work.
    11181623        List<WayInPolygon> cleanMultigonWays = multigonWays.stream()
    1119                 .filter(way -> way.way.getNodesCount() != 2 || !way.way.isClosed())
    1120                 .collect(Collectors.toList());
     1624                .filter(way -> way.way.getNodesCount() != 2 || !way.way.isClosed()).collect(Collectors.toList());
    11211625        WayTraverser traverser = new WayTraverser(cleanMultigonWays);
    11221626        List<AssembledPolygon> result = new ArrayList<>();
    11231627
     
    11331637        return fixTouchingPolygons(result);
    11341638    }
    11351639
    1136     private static void findBoundaryPolygonsStartingWith(List<Way> discardedResult, WayTraverser traverser, List<AssembledPolygon> result,
    1137             WayInPolygon startWay) {
     1640    private static void findBoundaryPolygonsStartingWith(List<Way> discardedResult, WayTraverser traverser,
     1641            List<AssembledPolygon> result, WayInPolygon startWay) {
    11381642        List<WayInPolygon> path = new ArrayList<>();
    11391643        List<WayInPolygon> startWays = new ArrayList<>();
    11401644        try {
     
    11581662                    if (ring.getNodes().size() <= 2) {
    11591663                        // Invalid ring (2 nodes) -> remove
    11601664                        traverser.removeWays(path);
    1161                         for (WayInPolygon way: path) {
     1665                        for (WayInPolygon way : path) {
    11621666                            discardedResult.add(way.way);
    11631667                        }
    11641668                    } else {
     
    11771681                        traverser.removeWay(currentWay);
    11781682                        path.remove(index);
    11791683                    }
    1180                     traverser.setStartWay(path.get(index-1));
     1684                    traverser.setStartWay(path.get(index - 1));
    11811685                } else {
    11821686                    path.add(nextWay);
    11831687                }
     
    13581862            }
    13591863
    13601864            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();
     1865                new Notification(tr("Sorry. Cannot handle multipolygon relations with multiple outer ways."))
     1866                        .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
    13651867                return null;
    13661868            }
    13671869
     
    13711873            innerWays.retainAll(selectedWays);
    13721874
    13731875            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();
     1876                new Notification(tr("Sorry. Cannot handle way that is outer in multiple multipolygon relations."))
     1877                        .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
    13781878                return null;
    13791879            }
    13801880
    13811881            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();
     1882                new Notification(tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
     1883                        .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
    13861884                return null;
    13871885            }
    13881886
    1389             for (Way way :innerWays) {
     1887            for (Way way : innerWays) {
    13901888                if (processedOuterWays.contains(way)) {
    13911889                    new Notification(
    13921890                            tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
    1393                             .setIcon(JOptionPane.INFORMATION_MESSAGE)
    1394                             .show();
     1891                                    .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
    13951892                    return null;
    13961893                }
    13971894
    13981895                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();
     1896                    new Notification(tr("Sorry. Cannot handle way that is inner in multiple multipolygon relations."))
     1897                            .setIcon(JOptionPane.INFORMATION_MESSAGE).show();
    14031898                    return null;
    14041899                }
    14051900            }
     
    14311926     * @return The list of relation with roles to add own relation to
    14321927     */
    14331928    private RelationRole addOwnMultipolygonRelation(Collection<Way> inner) {
    1434         if (inner.isEmpty()) return null;
     1929        if (inner.isEmpty())
     1930            return null;
    14351931        OsmDataLayer layer = Main.getLayerManager().getEditLayer();
    14361932        // Create new multipolygon relation and add all inner ways to it
    14371933        Relation newRel = new Relation();
     
    14391935        for (Way w : inner) {
    14401936            newRel.addMember(new RelationMember("inner", w));
    14411937        }
    1442         cmds.add(layer != null ? new AddCommand(layer, newRel) :
    1443             new AddCommand(inner.iterator().next().getDataSet(), newRel));
     1938        cmds.add(layer != null ? new AddCommand(layer, newRel)
     1939                : new AddCommand(inner.iterator().next().getDataSet(), newRel));
    14441940        addedRelations.add(newRel);
    14451941
    14461942        // We don't add outer to the relation because it will be handed to fixRelations()
     
    14921988     * @param ownMultipol elements to directly add as outer
    14931989     * @param relationsToDelete set of relations to delete.
    14941990     */
    1495     private void fixRelations(List<RelationRole> rels, Way outer, RelationRole ownMultipol, Set<Relation> relationsToDelete) {
     1991    private void fixRelations(List<RelationRole> rels, Way outer, RelationRole ownMultipol,
     1992            Set<Relation> relationsToDelete) {
    14961993        List<RelationRole> multiouters = new ArrayList<>();
    14971994
    14981995        if (ownMultipol != null) {