source: josm/trunk/src/org/openstreetmap/josm/actions/JoinAreasAction.java @ 12718

Last change on this file since 12718 was 12718, checked in by Don-vip, 3 months ago

see #13036 - see #15229 - see #15182 - make Commands depends only on a DataSet, not a Layer. This removes a lot of GUI dependencies

  • Property svn:eol-style set to native
File size: 57.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.event.ActionEvent;
9import java.awt.event.KeyEvent;
10import java.util.ArrayList;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.HashMap;
14import java.util.HashSet;
15import java.util.LinkedHashSet;
16import java.util.LinkedList;
17import java.util.List;
18import java.util.Map;
19import java.util.Objects;
20import java.util.Set;
21import java.util.TreeMap;
22
23import javax.swing.JOptionPane;
24
25import org.openstreetmap.josm.Main;
26import org.openstreetmap.josm.actions.ReverseWayAction.ReverseWayResult;
27import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult;
28import org.openstreetmap.josm.command.AddCommand;
29import org.openstreetmap.josm.command.ChangeCommand;
30import org.openstreetmap.josm.command.Command;
31import org.openstreetmap.josm.command.DeleteCommand;
32import org.openstreetmap.josm.command.SequenceCommand;
33import org.openstreetmap.josm.data.UndoRedoHandler;
34import org.openstreetmap.josm.data.coor.EastNorth;
35import org.openstreetmap.josm.data.osm.DataSet;
36import org.openstreetmap.josm.data.osm.Node;
37import org.openstreetmap.josm.data.osm.NodePositionComparator;
38import org.openstreetmap.josm.data.osm.OsmPrimitive;
39import org.openstreetmap.josm.data.osm.Relation;
40import org.openstreetmap.josm.data.osm.RelationMember;
41import org.openstreetmap.josm.data.osm.TagCollection;
42import org.openstreetmap.josm.data.osm.Way;
43import org.openstreetmap.josm.gui.MainApplication;
44import org.openstreetmap.josm.gui.Notification;
45import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
46import org.openstreetmap.josm.gui.layer.OsmDataLayer;
47import org.openstreetmap.josm.tools.Geometry;
48import org.openstreetmap.josm.tools.JosmRuntimeException;
49import org.openstreetmap.josm.tools.Logging;
50import org.openstreetmap.josm.tools.Pair;
51import org.openstreetmap.josm.tools.Shortcut;
52import org.openstreetmap.josm.tools.UserCancelException;
53import org.openstreetmap.josm.tools.Utils;
54
55/**
56 * Join Areas (i.e. closed ways and multipolygons).
57 * @since 2575
58 */
59public class JoinAreasAction extends JosmAction {
60    // This will be used to commit commands and unite them into one large command sequence at the end
61    private final transient LinkedList<Command> cmds = new LinkedList<>();
62    private int cmdsCount;
63    private final transient List<Relation> addedRelations = new LinkedList<>();
64
65    /**
66     * This helper class describes join areas action result.
67     * @author viesturs
68     */
69    public static class JoinAreasResult {
70
71        private final boolean hasChanges;
72        private final List<Multipolygon> polygons;
73
74        /**
75         * Constructs a new {@code JoinAreasResult}.
76         * @param hasChanges whether the result has changes
77         * @param polygons the result polygons, can be null
78         */
79        public JoinAreasResult(boolean hasChanges, List<Multipolygon> polygons) {
80            this.hasChanges = hasChanges;
81            this.polygons = polygons;
82        }
83
84        /**
85         * Determines if the result has changes.
86         * @return {@code true} if the result has changes
87         */
88        public final boolean hasChanges() {
89            return hasChanges;
90        }
91
92        /**
93         * Returns the result polygons, can be null.
94         * @return the result polygons, can be null
95         */
96        public final List<Multipolygon> getPolygons() {
97            return polygons;
98        }
99    }
100
101    public static class Multipolygon {
102        private final Way outerWay;
103        private final List<Way> innerWays;
104
105        /**
106         * Constructs a new {@code Multipolygon}.
107         * @param way outer way
108         */
109        public Multipolygon(Way way) {
110            outerWay = way;
111            innerWays = new ArrayList<>();
112        }
113
114        /**
115         * Returns the outer way.
116         * @return the outer way
117         */
118        public final Way getOuterWay() {
119            return outerWay;
120        }
121
122        /**
123         * Returns the inner ways.
124         * @return the inner ways
125         */
126        public final List<Way> getInnerWays() {
127            return innerWays;
128        }
129    }
130
131    // HelperClass
132    // Saves a relation and a role an OsmPrimitve was part of until it was stripped from all relations
133    private static class RelationRole {
134        public final Relation rel;
135        public final String role;
136
137        RelationRole(Relation rel, String role) {
138            this.rel = rel;
139            this.role = role;
140        }
141
142        @Override
143        public int hashCode() {
144            return Objects.hash(rel, role);
145        }
146
147        @Override
148        public boolean equals(Object other) {
149            if (this == other) return true;
150            if (other == null || getClass() != other.getClass()) return false;
151            RelationRole that = (RelationRole) other;
152            return Objects.equals(rel, that.rel) &&
153                    Objects.equals(role, that.role);
154        }
155    }
156
157    /**
158     * HelperClass - saves a way and the "inside" side.
159     *
160     * insideToTheLeft: if true left side is "in", false -right side is "in".
161     * Left and right are determined along the orientation of way.
162     */
163    public static class WayInPolygon {
164        public final Way way;
165        public boolean insideToTheRight;
166
167        public WayInPolygon(Way way, boolean insideRight) {
168            this.way = way;
169            this.insideToTheRight = insideRight;
170        }
171
172        @Override
173        public int hashCode() {
174            return Objects.hash(way, insideToTheRight);
175        }
176
177        @Override
178        public boolean equals(Object other) {
179            if (this == other) return true;
180            if (other == null || getClass() != other.getClass()) return false;
181            WayInPolygon that = (WayInPolygon) other;
182            return insideToTheRight == that.insideToTheRight &&
183                    Objects.equals(way, that.way);
184        }
185    }
186
187    /**
188     * This helper class describes a polygon, assembled from several ways.
189     * @author viesturs
190     *
191     */
192    public static class AssembledPolygon {
193        public List<WayInPolygon> ways;
194
195        public AssembledPolygon(List<WayInPolygon> boundary) {
196            this.ways = boundary;
197        }
198
199        public List<Node> getNodes() {
200            List<Node> nodes = new ArrayList<>();
201            for (WayInPolygon way : this.ways) {
202                //do not add the last node as it will be repeated in the next way
203                if (way.insideToTheRight) {
204                    for (int pos = 0; pos < way.way.getNodesCount() - 1; pos++) {
205                        nodes.add(way.way.getNode(pos));
206                    }
207                } else {
208                    for (int pos = way.way.getNodesCount() - 1; pos > 0; pos--) {
209                        nodes.add(way.way.getNode(pos));
210                    }
211                }
212            }
213
214            return nodes;
215        }
216
217        /**
218         * Inverse inside and outside
219         */
220        public void reverse() {
221            for (WayInPolygon way: ways) {
222                way.insideToTheRight = !way.insideToTheRight;
223            }
224            Collections.reverse(ways);
225        }
226    }
227
228    public static class AssembledMultipolygon {
229        public AssembledPolygon outerWay;
230        public List<AssembledPolygon> innerWays;
231
232        public AssembledMultipolygon(AssembledPolygon way) {
233            outerWay = way;
234            innerWays = new ArrayList<>();
235        }
236    }
237
238    /**
239     * This hepler class implements algorithm traversing trough connected ways.
240     * Assumes you are going in clockwise orientation.
241     * @author viesturs
242     */
243    private static class WayTraverser {
244
245        /** Set of {@link WayInPolygon} to be joined by walk algorithm */
246        private final Set<WayInPolygon> availableWays;
247        /** Current state of walk algorithm */
248        private WayInPolygon lastWay;
249        /** Direction of current way */
250        private boolean lastWayReverse;
251
252        /** Constructor
253         * @param ways available ways
254         */
255        WayTraverser(Collection<WayInPolygon> ways) {
256            availableWays = new HashSet<>(ways);
257            lastWay = null;
258        }
259
260        /**
261         *  Remove ways from available ways
262         *  @param ways Collection of WayInPolygon
263         */
264        public void removeWays(Collection<WayInPolygon> ways) {
265            availableWays.removeAll(ways);
266        }
267
268        /**
269         * Remove a single way from available ways
270         * @param way WayInPolygon
271         */
272        public void removeWay(WayInPolygon way) {
273            availableWays.remove(way);
274        }
275
276        /**
277         * Reset walk algorithm to a new start point
278         * @param way New start point
279         */
280        public void setStartWay(WayInPolygon way) {
281            lastWay = way;
282            lastWayReverse = !way.insideToTheRight;
283        }
284
285        /**
286         * Reset walk algorithm to a new start point.
287         * @return The new start point or null if no available way remains
288         */
289        public WayInPolygon startNewWay() {
290            if (availableWays.isEmpty()) {
291                lastWay = null;
292            } else {
293                lastWay = availableWays.iterator().next();
294                lastWayReverse = !lastWay.insideToTheRight;
295            }
296
297            return lastWay;
298        }
299
300        /**
301         * Walking through {@link WayInPolygon} segments, head node is the current position
302         * @return Head node
303         */
304        private Node getHeadNode() {
305            return !lastWayReverse ? lastWay.way.lastNode() : lastWay.way.firstNode();
306        }
307
308        /**
309         * Node just before head node.
310         * @return Previous node
311         */
312        private Node getPrevNode() {
313            return !lastWayReverse ? lastWay.way.getNode(lastWay.way.getNodesCount() - 2) : lastWay.way.getNode(1);
314        }
315
316        /**
317         * Returns oriented angle (N1N2, N1N3) in range [0; 2*Math.PI[
318         * @param n1 first node
319         * @param n2 second node
320         * @param n3 third node
321         * @return oriented angle (N1N2, N1N3) in range [0; 2*Math.PI[
322         */
323        private static double getAngle(Node n1, Node n2, Node n3) {
324            EastNorth en1 = n1.getEastNorth();
325            EastNorth en2 = n2.getEastNorth();
326            EastNorth en3 = n3.getEastNorth();
327            double angle = Math.atan2(en3.getY() - en1.getY(), en3.getX() - en1.getX()) -
328                    Math.atan2(en2.getY() - en1.getY(), en2.getX() - en1.getX());
329            while (angle >= 2*Math.PI) {
330                angle -= 2*Math.PI;
331            }
332            while (angle < 0) {
333                angle += 2*Math.PI;
334            }
335            return angle;
336        }
337
338        /**
339         * Get the next way creating a clockwise path, ensure it is the most right way. #7959
340         * @return The next way.
341         */
342        public WayInPolygon walk() {
343            Node headNode = getHeadNode();
344            Node prevNode = getPrevNode();
345
346            double headAngle = Math.atan2(headNode.getEastNorth().east() - prevNode.getEastNorth().east(),
347                    headNode.getEastNorth().north() - prevNode.getEastNorth().north());
348            double bestAngle = 0;
349
350            //find best next way
351            WayInPolygon bestWay = null;
352            boolean bestWayReverse = false;
353
354            for (WayInPolygon way : availableWays) {
355                Node nextNode;
356
357                // Check for a connected way
358                if (way.way.firstNode().equals(headNode) && way.insideToTheRight) {
359                    nextNode = way.way.getNode(1);
360                } else if (way.way.lastNode().equals(headNode) && !way.insideToTheRight) {
361                    nextNode = way.way.getNode(way.way.getNodesCount() - 2);
362                } else {
363                    continue;
364                }
365
366                if (nextNode == prevNode) {
367                    // go back
368                    lastWay = way;
369                    lastWayReverse = !way.insideToTheRight;
370                    return lastWay;
371                }
372
373                double angle = Math.atan2(nextNode.getEastNorth().east() - headNode.getEastNorth().east(),
374                        nextNode.getEastNorth().north() - headNode.getEastNorth().north()) - headAngle;
375                if (angle > Math.PI)
376                    angle -= 2*Math.PI;
377                if (angle <= -Math.PI)
378                    angle += 2*Math.PI;
379
380                // Now we have a valid candidate way, is it better than the previous one ?
381                if (bestWay == null || angle > bestAngle) {
382                    //the new way is better
383                    bestWay = way;
384                    bestWayReverse = !way.insideToTheRight;
385                    bestAngle = angle;
386                }
387            }
388
389            lastWay = bestWay;
390            lastWayReverse = bestWayReverse;
391            return lastWay;
392        }
393
394        /**
395         * Search for an other way coming to the same head node at left side from last way. #9951
396         * @return left way or null if none found
397         */
398        public WayInPolygon leftComingWay() {
399            Node headNode = getHeadNode();
400            Node prevNode = getPrevNode();
401
402            WayInPolygon mostLeft = null; // most left way connected to head node
403            boolean comingToHead = false; // true if candidate come to head node
404            double angle = 2*Math.PI;
405
406            for (WayInPolygon candidateWay : availableWays) {
407                boolean candidateComingToHead;
408                Node candidatePrevNode;
409
410                if (candidateWay.way.firstNode().equals(headNode)) {
411                    candidateComingToHead = !candidateWay.insideToTheRight;
412                    candidatePrevNode = candidateWay.way.getNode(1);
413                } else if (candidateWay.way.lastNode().equals(headNode)) {
414                     candidateComingToHead = candidateWay.insideToTheRight;
415                     candidatePrevNode = candidateWay.way.getNode(candidateWay.way.getNodesCount() - 2);
416                } else
417                    continue;
418                if (candidateComingToHead && candidateWay.equals(lastWay))
419                    continue;
420
421                double candidateAngle = getAngle(headNode, candidatePrevNode, prevNode);
422
423                if (mostLeft == null || candidateAngle < angle || (Utils.equalsEpsilon(candidateAngle, angle) && !candidateComingToHead)) {
424                    // Candidate is most left
425                    mostLeft = candidateWay;
426                    comingToHead = candidateComingToHead;
427                    angle = candidateAngle;
428                }
429            }
430
431            return comingToHead ? mostLeft : null;
432        }
433    }
434
435    /**
436     * Helper storage class for finding findOuterWays
437     * @author viesturs
438     */
439    static class PolygonLevel {
440        public final int level;
441        public final AssembledMultipolygon pol;
442
443        PolygonLevel(AssembledMultipolygon pol, int level) {
444            this.pol = pol;
445            this.level = level;
446        }
447    }
448
449    /**
450     * Constructs a new {@code JoinAreasAction}.
451     */
452    public JoinAreasAction() {
453        this(true);
454    }
455
456    /**
457     * Constructs a new {@code JoinAreasAction} with optional shortcut.
458     * @param addShortcut controls whether the shortcut should be registered or not, as for toolbar registration
459     * @since 11611
460     */
461    public JoinAreasAction(boolean addShortcut) {
462        super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"), addShortcut ?
463        Shortcut.registerShortcut("tools:joinareas", tr("Tool: {0}", tr("Join overlapping Areas")), KeyEvent.VK_J, Shortcut.SHIFT)
464        : null, addShortcut);
465    }
466
467    /**
468     * Gets called whenever the shortcut is pressed or the menu entry is selected.
469     * Checks whether the selected objects are suitable to join and joins them if so.
470     */
471    @Override
472    public void actionPerformed(ActionEvent e) {
473        join(getLayerManager().getEditDataSet().getSelectedWays());
474    }
475
476    /**
477     * Joins the given ways.
478     * @param ways Ways to join
479     * @since 7534
480     */
481    public void join(Collection<Way> ways) {
482        addedRelations.clear();
483
484        if (ways.isEmpty()) {
485            new Notification(
486                    tr("Please select at least one closed way that should be joined."))
487                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
488                    .show();
489            return;
490        }
491
492        List<Node> allNodes = new ArrayList<>();
493        for (Way way : ways) {
494            if (!way.isClosed()) {
495                new Notification(
496                        tr("One of the selected ways is not closed and therefore cannot be joined."))
497                        .setIcon(JOptionPane.INFORMATION_MESSAGE)
498                        .show();
499                return;
500            }
501
502            allNodes.addAll(way.getNodes());
503        }
504
505        // TODO: Only display this warning when nodes outside dataSourceArea are deleted
506        boolean ok = Command.checkAndConfirmOutlyingOperation("joinarea", tr("Join area confirmation"),
507                trn("The selected way has nodes outside of the downloaded data region.",
508                    "The selected ways have nodes outside of the downloaded data region.",
509                    ways.size()) + "<br/>"
510                    + tr("This can lead to nodes being deleted accidentally.") + "<br/>"
511                    + tr("Are you really sure to continue?")
512                    + tr("Please abort if you are not sure"),
513                tr("The selected area is incomplete. Continue?"),
514                allNodes, null);
515        if (!ok) return;
516
517        //analyze multipolygon relations and collect all areas
518        List<Multipolygon> areas = collectMultipolygons(ways);
519
520        if (areas == null)
521            //too complex multipolygon relations found
522            return;
523
524        if (!testJoin(areas)) {
525            new Notification(
526                    tr("No intersection found. Nothing was changed."))
527                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
528                    .show();
529            return;
530        }
531
532        if (!resolveTagConflicts(areas))
533            return;
534        //user canceled, do nothing.
535
536        try {
537            // see #11026 - Because <ways> is a dynamic filtered (on ways) of a filtered (on selected objects) collection,
538            // retrieve effective dataset before joining the ways (which affects the selection, thus, the <ways> collection)
539            // Dataset retrieving allows to call this code without relying on Main.getCurrentDataSet(), thus, on a mapview instance
540            DataSet ds = ways.iterator().next().getDataSet();
541
542            // Do the job of joining areas
543            JoinAreasResult result = joinAreas(areas);
544
545            if (result.hasChanges) {
546                // move tags from ways to newly created relations
547                // TODO: do we need to also move tags for the modified relations?
548                for (Relation r: addedRelations) {
549                    cmds.addAll(CreateMultipolygonAction.removeTagsFromWaysIfNeeded(r));
550                }
551                commitCommands(tr("Move tags from ways to relations"));
552
553                List<Way> allWays = new ArrayList<>();
554                for (Multipolygon pol : result.polygons) {
555                    allWays.add(pol.outerWay);
556                    allWays.addAll(pol.innerWays);
557                }
558                if (ds != null) {
559                    ds.setSelected(allWays);
560                }
561            } else {
562                new Notification(
563                        tr("No intersection found. Nothing was changed."))
564                        .setIcon(JOptionPane.INFORMATION_MESSAGE)
565                        .show();
566            }
567        } catch (UserCancelException exception) {
568            Logging.trace(exception);
569            //revert changes
570            //FIXME: this is dirty hack
571            makeCommitsOneAction(tr("Reverting changes"));
572            MainApplication.undoRedo.undo();
573            MainApplication.undoRedo.redoCommands.clear();
574        }
575    }
576
577    /**
578     * Tests if the areas have some intersections to join.
579     * @param areas Areas to test
580     * @return {@code true} if areas are joinable
581     */
582    private boolean testJoin(List<Multipolygon> areas) {
583        List<Way> allStartingWays = new ArrayList<>();
584
585        for (Multipolygon area : areas) {
586            allStartingWays.add(area.outerWay);
587            allStartingWays.addAll(area.innerWays);
588        }
589
590        //find intersection points
591        Set<Node> nodes = Geometry.addIntersections(allStartingWays, true, cmds);
592        return !nodes.isEmpty();
593    }
594
595    /**
596     * Will join two or more overlapping areas
597     * @param areas list of areas to join
598     * @return new area formed.
599     * @throws UserCancelException if user cancels the operation
600     */
601    public JoinAreasResult joinAreas(List<Multipolygon> areas) throws UserCancelException {
602
603        boolean hasChanges = false;
604
605        List<Way> allStartingWays = new ArrayList<>();
606        List<Way> innerStartingWays = new ArrayList<>();
607        List<Way> outerStartingWays = new ArrayList<>();
608
609        for (Multipolygon area : areas) {
610            outerStartingWays.add(area.outerWay);
611            innerStartingWays.addAll(area.innerWays);
612        }
613
614        allStartingWays.addAll(innerStartingWays);
615        allStartingWays.addAll(outerStartingWays);
616
617        //first remove nodes in the same coordinate
618        boolean removedDuplicates = false;
619        removedDuplicates |= removeDuplicateNodes(allStartingWays);
620
621        if (removedDuplicates) {
622            hasChanges = true;
623            commitCommands(marktr("Removed duplicate nodes"));
624        }
625
626        //find intersection points
627        Set<Node> nodes = Geometry.addIntersections(allStartingWays, false, cmds);
628
629        //no intersections, return.
630        if (nodes.isEmpty())
631            return new JoinAreasResult(hasChanges, null);
632        commitCommands(marktr("Added node on all intersections"));
633
634        List<RelationRole> relations = new ArrayList<>();
635
636        // Remove ways from all relations so ways can be combined/split quietly
637        for (Way way : allStartingWays) {
638            relations.addAll(removeFromAllRelations(way));
639        }
640
641        // Don't warn now, because it will really look corrupted
642        boolean warnAboutRelations = !relations.isEmpty() && allStartingWays.size() > 1;
643
644        List<WayInPolygon> preparedWays = new ArrayList<>();
645
646        for (Way way : outerStartingWays) {
647            List<Way> splitWays = splitWayOnNodes(way, nodes);
648            preparedWays.addAll(markWayInsideSide(splitWays, false));
649        }
650
651        for (Way way : innerStartingWays) {
652            List<Way> splitWays = splitWayOnNodes(way, nodes);
653            preparedWays.addAll(markWayInsideSide(splitWays, true));
654        }
655
656        // Find boundary ways
657        List<Way> discardedWays = new ArrayList<>();
658        List<AssembledPolygon> boundaries = findBoundaryPolygons(preparedWays, discardedWays);
659
660        //find polygons
661        List<AssembledMultipolygon> preparedPolygons = findPolygons(boundaries);
662
663        //assemble final polygons
664        List<Multipolygon> polygons = new ArrayList<>();
665        Set<Relation> relationsToDelete = new LinkedHashSet<>();
666
667        for (AssembledMultipolygon pol : preparedPolygons) {
668
669            //create the new ways
670            Multipolygon resultPol = joinPolygon(pol);
671
672            //create multipolygon relation, if necessary.
673            RelationRole ownMultipolygonRelation = addOwnMultipolygonRelation(resultPol.innerWays);
674
675            //add back the original relations, merged with our new multipolygon relation
676            fixRelations(relations, resultPol.outerWay, ownMultipolygonRelation, relationsToDelete);
677
678            //strip tags from inner ways
679            //TODO: preserve tags on existing inner ways
680            stripTags(resultPol.innerWays);
681
682            polygons.add(resultPol);
683        }
684
685        commitCommands(marktr("Assemble new polygons"));
686
687        for (Relation rel: relationsToDelete) {
688            cmds.add(new DeleteCommand(rel));
689        }
690
691        commitCommands(marktr("Delete relations"));
692
693        // Delete the discarded inner ways
694        if (!discardedWays.isEmpty()) {
695            Command deleteCmd = DeleteCommand.delete(discardedWays, true);
696            if (deleteCmd != null) {
697                cmds.add(deleteCmd);
698                commitCommands(marktr("Delete Ways that are not part of an inner multipolygon"));
699            }
700        }
701
702        makeCommitsOneAction(marktr("Joined overlapping areas"));
703
704        if (warnAboutRelations) {
705            new Notification(
706                    tr("Some of the ways were part of relations that have been modified.<br>Please verify no errors have been introduced."))
707                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
708                    .setDuration(Notification.TIME_LONG)
709                    .show();
710        }
711
712        return new JoinAreasResult(true, polygons);
713    }
714
715    /**
716     * Checks if tags of two given ways differ, and presents the user a dialog to solve conflicts
717     * @param polygons ways to check
718     * @return {@code true} if all conflicts are resolved, {@code false} if conflicts remain.
719     */
720    private boolean resolveTagConflicts(List<Multipolygon> polygons) {
721
722        List<Way> ways = new ArrayList<>();
723
724        for (Multipolygon pol : polygons) {
725            ways.add(pol.outerWay);
726            ways.addAll(pol.innerWays);
727        }
728
729        if (ways.size() < 2) {
730            return true;
731        }
732
733        TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways);
734        try {
735            cmds.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(wayTags, ways, ways));
736            commitCommands(marktr("Fix tag conflicts"));
737            return true;
738        } catch (UserCancelException ex) {
739            Logging.trace(ex);
740            return false;
741        }
742    }
743
744    /**
745     * This method removes duplicate points (if any) from the input way.
746     * @param ways the ways to process
747     * @return {@code true} if any changes where made
748     */
749    private boolean removeDuplicateNodes(List<Way> ways) {
750        //TODO: maybe join nodes with JoinNodesAction, rather than reconnect the ways.
751
752        Map<Node, Node> nodeMap = new TreeMap<>(new NodePositionComparator());
753        int totalNodesRemoved = 0;
754
755        for (Way way : ways) {
756            if (way.getNodes().size() < 2) {
757                continue;
758            }
759
760            int nodesRemoved = 0;
761            List<Node> newNodes = new ArrayList<>();
762            Node prevNode = null;
763
764            for (Node node : way.getNodes()) {
765                if (!nodeMap.containsKey(node)) {
766                    //new node
767                    nodeMap.put(node, node);
768
769                    //avoid duplicate nodes
770                    if (prevNode != node) {
771                        newNodes.add(node);
772                    } else {
773                        nodesRemoved++;
774                    }
775                } else {
776                    //node with same coordinates already exists, substitute with existing node
777                    Node representator = nodeMap.get(node);
778
779                    if (representator != node) {
780                        nodesRemoved++;
781                    }
782
783                    //avoid duplicate node
784                    if (prevNode != representator) {
785                        newNodes.add(representator);
786                    }
787                }
788                prevNode = node;
789            }
790
791            if (nodesRemoved > 0) {
792
793                if (newNodes.size() == 1) { //all nodes in the same coordinate - add one more node, to have closed way.
794                    newNodes.add(newNodes.get(0));
795                }
796
797                Way newWay = new Way(way);
798                newWay.setNodes(newNodes);
799                cmds.add(new ChangeCommand(way, newWay));
800                totalNodesRemoved += nodesRemoved;
801            }
802        }
803
804        return totalNodesRemoved > 0;
805    }
806
807    /**
808     * Commits the command list with a description
809     * @param description The description of what the commands do
810     */
811    private void commitCommands(String description) {
812        switch(cmds.size()) {
813        case 0:
814            return;
815        case 1:
816            commitCommand(cmds.getFirst());
817            break;
818        default:
819            commitCommand(new SequenceCommand(tr(description), cmds));
820            break;
821        }
822
823        cmds.clear();
824        cmdsCount++;
825    }
826
827    private static void commitCommand(Command c) {
828        if (Main.main != null) {
829            MainApplication.undoRedo.add(c);
830        } else {
831            c.executeCommand();
832        }
833    }
834
835    /**
836     * This method analyzes the way and assigns each part what direction polygon "inside" is.
837     * @param parts the split parts of the way
838     * @param isInner - if true, reverts the direction (for multipolygon islands)
839     * @return list of parts, marked with the inside orientation.
840     * @throws IllegalArgumentException if parts is empty or not circular
841     */
842    private static List<WayInPolygon> markWayInsideSide(List<Way> parts, boolean isInner) {
843
844        //prepare next map
845        Map<Way, Way> nextWayMap = new HashMap<>();
846
847        for (int pos = 0; pos < parts.size(); pos++) {
848
849            if (!parts.get(pos).lastNode().equals(parts.get((pos + 1) % parts.size()).firstNode()))
850                throw new IllegalArgumentException("Way not circular");
851
852            nextWayMap.put(parts.get(pos), parts.get((pos + 1) % parts.size()));
853        }
854
855        //find the node with minimum y - it's guaranteed to be outer. (What about the south pole?)
856        Way topWay = null;
857        Node topNode = null;
858        int topIndex = 0;
859        double minY = Double.POSITIVE_INFINITY;
860
861        for (Way way : parts) {
862            for (int pos = 0; pos < way.getNodesCount(); pos++) {
863                Node node = way.getNode(pos);
864
865                if (node.getEastNorth().getY() < minY) {
866                    minY = node.getEastNorth().getY();
867                    topWay = way;
868                    topNode = node;
869                    topIndex = pos;
870                }
871            }
872        }
873
874        if (topWay == null || topNode == null) {
875            throw new IllegalArgumentException();
876        }
877
878        //get the upper way and it's orientation.
879
880        boolean wayClockwise; // orientation of the top way.
881
882        if (topNode.equals(topWay.firstNode()) || topNode.equals(topWay.lastNode())) {
883            Node headNode; // the node at junction
884            Node prevNode; // last node from previous path
885
886            //node is in split point - find the outermost way from this point
887
888            headNode = topNode;
889            //make a fake node that is downwards from head node (smaller Y). It will be a division point between paths.
890            prevNode = new Node(new EastNorth(headNode.getEastNorth().getX(), headNode.getEastNorth().getY() - 1e5));
891
892            topWay = null;
893            wayClockwise = false;
894            Node bestWayNextNode = null;
895
896            for (Way way : parts) {
897                if (way.firstNode().equals(headNode)) {
898                    Node nextNode = way.getNode(1);
899
900                    if (topWay == null || !Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) {
901                        //the new way is better
902                        topWay = way;
903                        wayClockwise = true;
904                        bestWayNextNode = nextNode;
905                    }
906                }
907
908                if (way.lastNode().equals(headNode)) {
909                    //end adjacent to headNode
910                    Node nextNode = way.getNode(way.getNodesCount() - 2);
911
912                    if (topWay == null || !Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) {
913                        //the new way is better
914                        topWay = way;
915                        wayClockwise = false;
916                        bestWayNextNode = nextNode;
917                    }
918                }
919            }
920        } else {
921            //node is inside way - pick the clockwise going end.
922            Node prev = topWay.getNode(topIndex - 1);
923            Node next = topWay.getNode(topIndex + 1);
924
925            //there will be no parallel segments in the middle of way, so all fine.
926            wayClockwise = Geometry.angleIsClockwise(prev, topNode, next);
927        }
928
929        Way curWay = topWay;
930        boolean curWayInsideToTheRight = wayClockwise ^ isInner;
931        List<WayInPolygon> result = new ArrayList<>();
932
933        //iterate till full circle is reached
934        while (curWay != null) {
935
936            //add cur way
937            WayInPolygon resultWay = new WayInPolygon(curWay, curWayInsideToTheRight);
938            result.add(resultWay);
939
940            //process next way
941            Way nextWay = nextWayMap.get(curWay);
942            Node prevNode = curWay.getNode(curWay.getNodesCount() - 2);
943            Node headNode = curWay.lastNode();
944            Node nextNode = nextWay.getNode(1);
945
946            if (nextWay == topWay) {
947                //full loop traversed - all done.
948                break;
949            }
950
951            //find intersecting segments
952            // the intersections will look like this:
953            //
954            //                       ^
955            //                       |
956            //                       X wayBNode
957            //                       |
958            //                  wayB |
959            //                       |
960            //             curWay    |       nextWay
961            //----X----------------->X----------------------X---->
962            //    prevNode           ^headNode              nextNode
963            //                       |
964            //                       |
965            //                  wayA |
966            //                       |
967            //                       X wayANode
968            //                       |
969
970            int intersectionCount = 0;
971
972            for (Way wayA : parts) {
973
974                if (wayA == curWay) {
975                    continue;
976                }
977
978                if (wayA.lastNode().equals(headNode)) {
979
980                    Way wayB = nextWayMap.get(wayA);
981
982                    //test if wayA is opposite wayB relative to curWay and nextWay
983
984                    Node wayANode = wayA.getNode(wayA.getNodesCount() - 2);
985                    Node wayBNode = wayB.getNode(1);
986
987                    boolean wayAToTheRight = Geometry.isToTheRightSideOfLine(prevNode, headNode, nextNode, wayANode);
988                    boolean wayBToTheRight = Geometry.isToTheRightSideOfLine(prevNode, headNode, nextNode, wayBNode);
989
990                    if (wayAToTheRight != wayBToTheRight) {
991                        intersectionCount++;
992                    }
993                }
994            }
995
996            //if odd number of crossings, invert orientation
997            if (intersectionCount % 2 != 0) {
998                curWayInsideToTheRight = !curWayInsideToTheRight;
999            }
1000
1001            curWay = nextWay;
1002        }
1003
1004        return result;
1005    }
1006
1007    /**
1008     * This is a method that splits way into smaller parts, using the prepared nodes list as split points.
1009     * Uses {@link SplitWayAction#splitWay} for the heavy lifting.
1010     * @param way way to split
1011     * @param nodes split points
1012     * @return list of split ways (or original ways if no splitting is done).
1013     */
1014    private List<Way> splitWayOnNodes(Way way, Set<Node> nodes) {
1015
1016        List<Way> result = new ArrayList<>();
1017        List<List<Node>> chunks = buildNodeChunks(way, nodes);
1018
1019        if (chunks.size() > 1) {
1020            SplitWayResult split = SplitWayAction.splitWay(way, chunks,
1021                    Collections.<OsmPrimitive>emptyList(), SplitWayAction.Strategy.keepFirstChunk());
1022
1023            if (split != null) {
1024                //execute the command, we need the results
1025                cmds.add(split.getCommand());
1026                commitCommands(marktr("Split ways into fragments"));
1027
1028                result.add(split.getOriginalWay());
1029                result.addAll(split.getNewWays());
1030            }
1031        }
1032        if (result.isEmpty()) {
1033            //nothing to split
1034            result.add(way);
1035        }
1036
1037        return result;
1038    }
1039
1040    /**
1041     * Simple chunking version. Does not care about circular ways and result being
1042     * proper, we will glue it all back together later on.
1043     * @param way the way to chunk
1044     * @param splitNodes the places where to cut.
1045     * @return list of node paths to produce.
1046     */
1047    private static List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) {
1048        List<List<Node>> result = new ArrayList<>();
1049        List<Node> curList = new ArrayList<>();
1050
1051        for (Node node : way.getNodes()) {
1052            curList.add(node);
1053            if (curList.size() > 1 && splitNodes.contains(node)) {
1054                result.add(curList);
1055                curList = new ArrayList<>();
1056                curList.add(node);
1057            }
1058        }
1059
1060        if (curList.size() > 1) {
1061            result.add(curList);
1062        }
1063
1064        return result;
1065    }
1066
1067    /**
1068     * This method finds which ways are outer and which are inner.
1069     * @param boundaries list of joined boundaries to search in
1070     * @return outer ways
1071     */
1072    private static List<AssembledMultipolygon> findPolygons(Collection<AssembledPolygon> boundaries) {
1073
1074        List<PolygonLevel> list = findOuterWaysImpl(0, boundaries);
1075        List<AssembledMultipolygon> result = new ArrayList<>();
1076
1077        //take every other level
1078        for (PolygonLevel pol : list) {
1079            if (pol.level % 2 == 0) {
1080                result.add(pol.pol);
1081            }
1082        }
1083
1084        return result;
1085    }
1086
1087    /**
1088     * Collects outer way and corresponding inner ways from all boundaries.
1089     * @param level depth level
1090     * @param boundaryWays list of joined boundaries to search in
1091     * @return the outermost Way.
1092     */
1093    private static List<PolygonLevel> findOuterWaysImpl(int level, Collection<AssembledPolygon> boundaryWays) {
1094
1095        //TODO: bad performance for deep nestings...
1096        List<PolygonLevel> result = new ArrayList<>();
1097
1098        for (AssembledPolygon outerWay : boundaryWays) {
1099
1100            boolean outerGood = true;
1101            List<AssembledPolygon> innerCandidates = new ArrayList<>();
1102
1103            for (AssembledPolygon innerWay : boundaryWays) {
1104                if (innerWay == outerWay) {
1105                    continue;
1106                }
1107
1108                if (wayInsideWay(outerWay, innerWay)) {
1109                    outerGood = false;
1110                    break;
1111                } else if (wayInsideWay(innerWay, outerWay)) {
1112                    innerCandidates.add(innerWay);
1113                }
1114            }
1115
1116            if (!outerGood) {
1117                continue;
1118            }
1119
1120            //add new outer polygon
1121            AssembledMultipolygon pol = new AssembledMultipolygon(outerWay);
1122            PolygonLevel polLev = new PolygonLevel(pol, level);
1123
1124            //process inner ways
1125            if (!innerCandidates.isEmpty()) {
1126                List<PolygonLevel> innerList = findOuterWaysImpl(level + 1, innerCandidates);
1127                result.addAll(innerList);
1128
1129                for (PolygonLevel pl : innerList) {
1130                    if (pl.level == level + 1) {
1131                        pol.innerWays.add(pl.pol.outerWay);
1132                    }
1133                }
1134            }
1135
1136            result.add(polLev);
1137        }
1138
1139        return result;
1140    }
1141
1142    /**
1143     * Finds all ways that form inner or outer boundaries.
1144     * @param multigonWays A list of (splitted) ways that form a multigon and share common end nodes on intersections.
1145     * @param discardedResult this list is filled with ways that are to be discarded
1146     * @return A list of ways that form the outer and inner boundaries of the multigon.
1147     */
1148    public static List<AssembledPolygon> findBoundaryPolygons(Collection<WayInPolygon> multigonWays,
1149            List<Way> discardedResult) {
1150        // In multigonWays collection, some way are just a point (i.e. way like nodeA-nodeA)
1151        // This seems to appear when is apply over invalid way like #9911 test-case
1152        // Remove all of these way to make the next work.
1153        List<WayInPolygon> cleanMultigonWays = new ArrayList<>();
1154        for (WayInPolygon way: multigonWays) {
1155            if (way.way.getNodesCount() != 2 || !way.way.isClosed())
1156                cleanMultigonWays.add(way);
1157        }
1158
1159        WayTraverser traverser = new WayTraverser(cleanMultigonWays);
1160        List<AssembledPolygon> result = new ArrayList<>();
1161
1162        WayInPolygon startWay;
1163        while ((startWay = traverser.startNewWay()) != null) {
1164            List<WayInPolygon> path = new ArrayList<>();
1165            List<WayInPolygon> startWays = new ArrayList<>();
1166            path.add(startWay);
1167            while (true) {
1168                WayInPolygon leftComing;
1169                while ((leftComing = traverser.leftComingWay()) != null) {
1170                    if (startWays.contains(leftComing))
1171                        break;
1172                    // Need restart traverser walk
1173                    path.clear();
1174                    path.add(leftComing);
1175                    traverser.setStartWay(leftComing);
1176                    startWays.add(leftComing);
1177                    break;
1178                }
1179                WayInPolygon nextWay = traverser.walk();
1180                if (nextWay == null)
1181                    throw new JosmRuntimeException("Join areas internal error.");
1182                if (path.get(0) == nextWay) {
1183                    // path is closed -> stop here
1184                    AssembledPolygon ring = new AssembledPolygon(path);
1185                    if (ring.getNodes().size() <= 2) {
1186                        // Invalid ring (2 nodes) -> remove
1187                        traverser.removeWays(path);
1188                        for (WayInPolygon way: path) {
1189                            discardedResult.add(way.way);
1190                        }
1191                    } else {
1192                        // Close ring -> add
1193                        result.add(ring);
1194                        traverser.removeWays(path);
1195                    }
1196                    break;
1197                }
1198                if (path.contains(nextWay)) {
1199                    // Inner loop -> remove
1200                    int index = path.indexOf(nextWay);
1201                    while (path.size() > index) {
1202                        WayInPolygon currentWay = path.get(index);
1203                        discardedResult.add(currentWay.way);
1204                        traverser.removeWay(currentWay);
1205                        path.remove(index);
1206                    }
1207                    traverser.setStartWay(path.get(index-1));
1208                } else {
1209                    path.add(nextWay);
1210                }
1211            }
1212        }
1213
1214        return fixTouchingPolygons(result);
1215    }
1216
1217    /**
1218     * This method checks if polygons have several touching parts and splits them in several polygons.
1219     * @param polygons the polygons to process.
1220     * @return the resulting list of polygons
1221     */
1222    public static List<AssembledPolygon> fixTouchingPolygons(List<AssembledPolygon> polygons) {
1223        List<AssembledPolygon> newPolygons = new ArrayList<>();
1224
1225        for (AssembledPolygon ring : polygons) {
1226            ring.reverse();
1227            WayTraverser traverser = new WayTraverser(ring.ways);
1228            WayInPolygon startWay;
1229
1230            while ((startWay = traverser.startNewWay()) != null) {
1231                List<WayInPolygon> simpleRingWays = new ArrayList<>();
1232                simpleRingWays.add(startWay);
1233                WayInPolygon nextWay;
1234                while ((nextWay = traverser.walk()) != startWay) {
1235                    if (nextWay == null)
1236                        throw new JosmRuntimeException("Join areas internal error.");
1237                    simpleRingWays.add(nextWay);
1238                }
1239                traverser.removeWays(simpleRingWays);
1240                AssembledPolygon simpleRing = new AssembledPolygon(simpleRingWays);
1241                simpleRing.reverse();
1242                newPolygons.add(simpleRing);
1243            }
1244        }
1245
1246        return newPolygons;
1247    }
1248
1249    /**
1250     * Tests if way is inside other way
1251     * @param outside outer polygon description
1252     * @param inside inner polygon description
1253     * @return {@code true} if inner is inside outer
1254     */
1255    public static boolean wayInsideWay(AssembledPolygon inside, AssembledPolygon outside) {
1256        Set<Node> outsideNodes = new HashSet<>(outside.getNodes());
1257        List<Node> insideNodes = inside.getNodes();
1258
1259        for (Node insideNode : insideNodes) {
1260
1261            if (!outsideNodes.contains(insideNode))
1262                //simply test the one node
1263                return Geometry.nodeInsidePolygon(insideNode, outside.getNodes());
1264        }
1265
1266        //all nodes shared.
1267        return false;
1268    }
1269
1270    /**
1271     * Joins the lists of ways.
1272     * @param polygon The list of outer ways that belong to that multipolygon.
1273     * @return The newly created outer way
1274     * @throws UserCancelException if user cancels the operation
1275     */
1276    private Multipolygon joinPolygon(AssembledMultipolygon polygon) throws UserCancelException {
1277        Multipolygon result = new Multipolygon(joinWays(polygon.outerWay.ways));
1278
1279        for (AssembledPolygon pol : polygon.innerWays) {
1280            result.innerWays.add(joinWays(pol.ways));
1281        }
1282
1283        return result;
1284    }
1285
1286    /**
1287     * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway.
1288     * @param ways The list of outer ways that belong to that multigon.
1289     * @return The newly created outer way
1290     * @throws UserCancelException if user cancels the operation
1291     */
1292    private Way joinWays(List<WayInPolygon> ways) throws UserCancelException {
1293
1294        //leave original orientation, if all paths are reverse.
1295        boolean allReverse = true;
1296        for (WayInPolygon way : ways) {
1297            allReverse &= !way.insideToTheRight;
1298        }
1299
1300        if (allReverse) {
1301            for (WayInPolygon way : ways) {
1302                way.insideToTheRight = !way.insideToTheRight;
1303            }
1304        }
1305
1306        Way joinedWay = joinOrientedWays(ways);
1307
1308        //should not happen
1309        if (joinedWay == null || !joinedWay.isClosed())
1310            throw new JosmRuntimeException("Join areas internal error.");
1311
1312        return joinedWay;
1313    }
1314
1315    /**
1316     * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath)
1317     * @param ways The list of ways to join and reverse
1318     * @return The newly created way
1319     * @throws UserCancelException if user cancels the operation
1320     */
1321    private Way joinOrientedWays(List<WayInPolygon> ways) throws UserCancelException {
1322        if (ways.size() < 2)
1323            return ways.get(0).way;
1324
1325        // This will turn ways so all of them point in the same direction and CombineAction won't bug
1326        // the user about this.
1327
1328        //TODO: ReverseWay and Combine way are really slow and we use them a lot here. This slows down large joins.
1329        List<Way> actionWays = new ArrayList<>(ways.size());
1330
1331        for (WayInPolygon way : ways) {
1332            actionWays.add(way.way);
1333
1334            if (!way.insideToTheRight) {
1335                ReverseWayResult res = ReverseWayAction.reverseWay(way.way);
1336                commitCommand(res.getReverseCommand());
1337                cmdsCount++;
1338            }
1339        }
1340
1341        Pair<Way, Command> result = CombineWayAction.combineWaysWorker(actionWays);
1342
1343        commitCommand(result.b);
1344        cmdsCount++;
1345
1346        return result.a;
1347    }
1348
1349    /**
1350     * This method analyzes multipolygon relationships of given ways and collects addition inner ways to consider.
1351     * @param selectedWays the selected ways
1352     * @return list of polygons, or null if too complex relation encountered.
1353     */
1354    public static List<Multipolygon> collectMultipolygons(Collection<Way> selectedWays) {
1355
1356        List<Multipolygon> result = new ArrayList<>();
1357
1358        //prepare the lists, to minimize memory allocation.
1359        List<Way> outerWays = new ArrayList<>();
1360        List<Way> innerWays = new ArrayList<>();
1361
1362        Set<Way> processedOuterWays = new LinkedHashSet<>();
1363        Set<Way> processedInnerWays = new LinkedHashSet<>();
1364
1365        for (Relation r : OsmPrimitive.getParentRelations(selectedWays)) {
1366            if (r.isDeleted() || !r.isMultipolygon()) {
1367                continue;
1368            }
1369
1370            boolean hasKnownOuter = false;
1371            outerWays.clear();
1372            innerWays.clear();
1373
1374            for (RelationMember rm : r.getMembers()) {
1375                if ("outer".equalsIgnoreCase(rm.getRole())) {
1376                    outerWays.add(rm.getWay());
1377                    hasKnownOuter |= selectedWays.contains(rm.getWay());
1378                } else if ("inner".equalsIgnoreCase(rm.getRole())) {
1379                    innerWays.add(rm.getWay());
1380                }
1381            }
1382
1383            if (!hasKnownOuter) {
1384                continue;
1385            }
1386
1387            if (outerWays.size() > 1) {
1388                new Notification(
1389                        tr("Sorry. Cannot handle multipolygon relations with multiple outer ways."))
1390                        .setIcon(JOptionPane.INFORMATION_MESSAGE)
1391                        .show();
1392                return null;
1393            }
1394
1395            Way outerWay = outerWays.get(0);
1396
1397            //retain only selected inner ways
1398            innerWays.retainAll(selectedWays);
1399
1400            if (processedOuterWays.contains(outerWay)) {
1401                new Notification(
1402                        tr("Sorry. Cannot handle way that is outer in multiple multipolygon relations."))
1403                        .setIcon(JOptionPane.INFORMATION_MESSAGE)
1404                        .show();
1405                return null;
1406            }
1407
1408            if (processedInnerWays.contains(outerWay)) {
1409                new Notification(
1410                        tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
1411                        .setIcon(JOptionPane.INFORMATION_MESSAGE)
1412                        .show();
1413                return null;
1414            }
1415
1416            for (Way way :innerWays) {
1417                if (processedOuterWays.contains(way)) {
1418                    new Notification(
1419                            tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
1420                            .setIcon(JOptionPane.INFORMATION_MESSAGE)
1421                            .show();
1422                    return null;
1423                }
1424
1425                if (processedInnerWays.contains(way)) {
1426                    new Notification(
1427                            tr("Sorry. Cannot handle way that is inner in multiple multipolygon relations."))
1428                            .setIcon(JOptionPane.INFORMATION_MESSAGE)
1429                            .show();
1430                    return null;
1431                }
1432            }
1433
1434            processedOuterWays.add(outerWay);
1435            processedInnerWays.addAll(innerWays);
1436
1437            Multipolygon pol = new Multipolygon(outerWay);
1438            pol.innerWays.addAll(innerWays);
1439
1440            result.add(pol);
1441        }
1442
1443        //add remaining ways, not in relations
1444        for (Way way : selectedWays) {
1445            if (processedOuterWays.contains(way) || processedInnerWays.contains(way)) {
1446                continue;
1447            }
1448
1449            result.add(new Multipolygon(way));
1450        }
1451
1452        return result;
1453    }
1454
1455    /**
1456     * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations
1457     * @param inner List of already closed inner ways
1458     * @return The list of relation with roles to add own relation to
1459     */
1460    private RelationRole addOwnMultipolygonRelation(Collection<Way> inner) {
1461        if (inner.isEmpty()) return null;
1462        OsmDataLayer layer = getLayerManager().getEditLayer();
1463        // Create new multipolygon relation and add all inner ways to it
1464        Relation newRel = new Relation();
1465        newRel.put("type", "multipolygon");
1466        for (Way w : inner) {
1467            newRel.addMember(new RelationMember("inner", w));
1468        }
1469        cmds.add(layer != null ? new AddCommand(layer.data, newRel) :
1470            new AddCommand(inner.iterator().next().getDataSet(), newRel));
1471        addedRelations.add(newRel);
1472
1473        // We don't add outer to the relation because it will be handed to fixRelations()
1474        // which will then do the remaining work.
1475        return new RelationRole(newRel, "outer");
1476    }
1477
1478    /**
1479     * Removes a given OsmPrimitive from all relations.
1480     * @param osm Element to remove from all relations
1481     * @return List of relations with roles the primitives was part of
1482     */
1483    private List<RelationRole> removeFromAllRelations(OsmPrimitive osm) {
1484        List<RelationRole> result = new ArrayList<>();
1485
1486        for (Relation r : osm.getDataSet().getRelations()) {
1487            if (r.isDeleted()) {
1488                continue;
1489            }
1490            for (RelationMember rm : r.getMembers()) {
1491                if (rm.getMember() != osm) {
1492                    continue;
1493                }
1494
1495                Relation newRel = new Relation(r);
1496                List<RelationMember> members = newRel.getMembers();
1497                members.remove(rm);
1498                newRel.setMembers(members);
1499
1500                cmds.add(new ChangeCommand(r, newRel));
1501                RelationRole saverel = new RelationRole(r, rm.getRole());
1502                if (!result.contains(saverel)) {
1503                    result.add(saverel);
1504                }
1505                break;
1506            }
1507        }
1508
1509        commitCommands(marktr("Removed Element from Relations"));
1510        return result;
1511    }
1512
1513    /**
1514     * Adds the previously removed relations again to the outer way. If there are multiple multipolygon
1515     * relations where the joined areas were in "outer" role a new relation is created instead with all
1516     * members of both. This function depends on multigon relations to be valid already, it won't fix them.
1517     * @param rels List of relations with roles the (original) ways were part of
1518     * @param outer The newly created outer area/way
1519     * @param ownMultipol elements to directly add as outer
1520     * @param relationsToDelete set of relations to delete.
1521     */
1522    private void fixRelations(List<RelationRole> rels, Way outer, RelationRole ownMultipol, Set<Relation> relationsToDelete) {
1523        List<RelationRole> multiouters = new ArrayList<>();
1524
1525        if (ownMultipol != null) {
1526            multiouters.add(ownMultipol);
1527        }
1528
1529        for (RelationRole r : rels) {
1530            if (r.rel.isMultipolygon() && "outer".equalsIgnoreCase(r.role)) {
1531                multiouters.add(r);
1532                continue;
1533            }
1534            // Add it back!
1535            Relation newRel = new Relation(r.rel);
1536            newRel.addMember(new RelationMember(r.role, outer));
1537            cmds.add(new ChangeCommand(r.rel, newRel));
1538        }
1539
1540        Relation newRel;
1541        switch (multiouters.size()) {
1542        case 0:
1543            return;
1544        case 1:
1545            // Found only one to be part of a multipolygon relation, so just add it back as well
1546            newRel = new Relation(multiouters.get(0).rel);
1547            newRel.addMember(new RelationMember(multiouters.get(0).role, outer));
1548            cmds.add(new ChangeCommand(multiouters.get(0).rel, newRel));
1549            return;
1550        default:
1551            // Create a new relation with all previous members and (Way)outer as outer.
1552            newRel = new Relation();
1553            for (RelationRole r : multiouters) {
1554                // Add members
1555                for (RelationMember rm : r.rel.getMembers()) {
1556                    if (!newRel.getMembers().contains(rm)) {
1557                        newRel.addMember(rm);
1558                    }
1559                }
1560                // Add tags
1561                for (String key : r.rel.keySet()) {
1562                    newRel.put(key, r.rel.get(key));
1563                }
1564                // Delete old relation
1565                relationsToDelete.add(r.rel);
1566            }
1567            newRel.addMember(new RelationMember("outer", outer));
1568            cmds.add(new AddCommand(outer.getDataSet(), newRel));
1569        }
1570    }
1571
1572    /**
1573     * Remove all tags from the all the way
1574     * @param ways The List of Ways to remove all tags from
1575     */
1576    private void stripTags(Collection<Way> ways) {
1577        for (Way w : ways) {
1578            final Way wayWithoutTags = new Way(w);
1579            wayWithoutTags.removeAll();
1580            cmds.add(new ChangeCommand(w, wayWithoutTags));
1581        }
1582        /* I18N: current action printed in status display */
1583        commitCommands(marktr("Remove tags from inner ways"));
1584    }
1585
1586    /**
1587     * Takes the last cmdsCount actions back and combines them into a single action
1588     * (for when the user wants to undo the join action)
1589     * @param message The commit message to display
1590     */
1591    private void makeCommitsOneAction(String message) {
1592        cmds.clear();
1593        if (Main.main != null) {
1594            UndoRedoHandler ur = MainApplication.undoRedo;
1595            int i = Math.max(ur.commands.size() - cmdsCount, 0);
1596            for (; i < ur.commands.size(); i++) {
1597                cmds.add(ur.commands.get(i));
1598            }
1599
1600            for (i = 0; i < cmds.size(); i++) {
1601                ur.undo();
1602            }
1603        }
1604
1605        commitCommands(message == null ? marktr("Join Areas Function") : message);
1606        cmdsCount = 0;
1607    }
1608
1609    @Override
1610    protected void updateEnabledState() {
1611        updateEnabledStateOnCurrentSelection();
1612    }
1613
1614    @Override
1615    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
1616        setEnabled(selection != null && !selection.isEmpty());
1617    }
1618}
Note: See TracBrowser for help on using the repository browser.