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

Last change on this file since 12636 was 12636, checked in by Don-vip, 7 years ago

see #15182 - deprecate Main.getLayerManager(). Replacement: gui.MainApplication.getLayerManager()

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