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

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

remove extra whitespaces

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