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

Last change on this file since 8343 was 8342, checked in by Don-vip, 9 years ago

code style - Close curly brace and the next "else", "catch" and "finally" keywords should be located on the same line

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