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

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

fix Checkstyle issues

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