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

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

checkstyle: blocks

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