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

Last change on this file since 7531 was 7052, checked in by Balaitous, 10 years ago

fix #9951 - "Join Overlapping Areas" gives strange multipolygons

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