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

Last change on this file since 8963 was 8955, checked in by simon04, 8 years ago

fix #11992 see #10730 - Joining Overlapping Areas results in RuntimeException

JoinAreasAction somehow requires that the first way chunk keeps the id of the split way.
SplitWayAction#Strategy allows to specify how to determine the way chunk which keeps the to id.

  • Property svn:eol-style set to native
File size: 55.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.data.UndoRedoHandler;
33import org.openstreetmap.josm.data.coor.EastNorth;
34import org.openstreetmap.josm.data.osm.DataSet;
35import org.openstreetmap.josm.data.osm.Node;
36import org.openstreetmap.josm.data.osm.NodePositionComparator;
37import org.openstreetmap.josm.data.osm.OsmPrimitive;
38import org.openstreetmap.josm.data.osm.Relation;
39import org.openstreetmap.josm.data.osm.RelationMember;
40import org.openstreetmap.josm.data.osm.TagCollection;
41import org.openstreetmap.josm.data.osm.Way;
42import org.openstreetmap.josm.gui.Notification;
43import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
44import org.openstreetmap.josm.tools.Geometry;
45import org.openstreetmap.josm.tools.Pair;
46import org.openstreetmap.josm.tools.Shortcut;
47import org.openstreetmap.josm.tools.UserCancelException;
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;
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 * Returns oriented angle (N1N2, N1N3) in range [0; 2*Math.PI[
262 * @return oriented angle (N1N2, N1N3) in range [0; 2*Math.PI[
263 */
264 private static double getAngle(Node N1, Node N2, Node N3) {
265 EastNorth en1 = N1.getEastNorth();
266 EastNorth en2 = N2.getEastNorth();
267 EastNorth en3 = N3.getEastNorth();
268 double angle = Math.atan2(en3.getY() - en1.getY(), en3.getX() - en1.getX()) -
269 Math.atan2(en2.getY() - en1.getY(), en2.getX() - en1.getX());
270 while (angle >= 2*Math.PI) {
271 angle -= 2*Math.PI;
272 }
273 while (angle < 0) {
274 angle += 2*Math.PI;
275 }
276 return angle;
277 }
278
279 /**
280 * Get the next way creating a clockwise path, ensure it is the most right way. #7959
281 * @return The next way.
282 */
283 public WayInPolygon walk() {
284 Node headNode = getHeadNode();
285 Node prevNode = getPrevNode();
286
287 double headAngle = Math.atan2(headNode.getEastNorth().east() - prevNode.getEastNorth().east(),
288 headNode.getEastNorth().north() - prevNode.getEastNorth().north());
289 double bestAngle = 0;
290
291 //find best next way
292 WayInPolygon bestWay = null;
293 boolean bestWayReverse = false;
294
295 for (WayInPolygon way : availableWays) {
296 Node nextNode;
297
298 // Check for a connected way
299 if (way.way.firstNode().equals(headNode) && way.insideToTheRight) {
300 nextNode = way.way.getNode(1);
301 } else if (way.way.lastNode().equals(headNode) && !way.insideToTheRight) {
302 nextNode = way.way.getNode(way.way.getNodesCount() - 2);
303 } else {
304 continue;
305 }
306
307 if (nextNode == prevNode) {
308 // go back
309 lastWay = way;
310 lastWayReverse = !way.insideToTheRight;
311 return lastWay;
312 }
313
314 double angle = Math.atan2(nextNode.getEastNorth().east() - headNode.getEastNorth().east(),
315 nextNode.getEastNorth().north() - headNode.getEastNorth().north()) - headAngle;
316 if (angle > Math.PI)
317 angle -= 2*Math.PI;
318 if (angle <= -Math.PI)
319 angle += 2*Math.PI;
320
321 // Now we have a valid candidate way, is it better than the previous one ?
322 if (bestWay == null || angle > bestAngle) {
323 //the new way is better
324 bestWay = way;
325 bestWayReverse = !way.insideToTheRight;
326 bestAngle = angle;
327 }
328 }
329
330 lastWay = bestWay;
331 lastWayReverse = bestWayReverse;
332 return lastWay;
333 }
334
335 /**
336 * Search for an other way coming to the same head node at left side from last way. #9951
337 * @return left way or null if none found
338 */
339 public WayInPolygon leftComingWay() {
340 Node headNode = getHeadNode();
341 Node prevNode = getPrevNode();
342
343 WayInPolygon mostLeft = null; // most left way connected to head node
344 boolean comingToHead = false; // true if candidate come to head node
345 double angle = 2*Math.PI;
346
347 for (WayInPolygon candidateWay : availableWays) {
348 boolean candidateComingToHead;
349 Node candidatePrevNode;
350
351 if (candidateWay.way.firstNode().equals(headNode)) {
352 candidateComingToHead = !candidateWay.insideToTheRight;
353 candidatePrevNode = candidateWay.way.getNode(1);
354 } else if (candidateWay.way.lastNode().equals(headNode)) {
355 candidateComingToHead = candidateWay.insideToTheRight;
356 candidatePrevNode = candidateWay.way.getNode(candidateWay.way.getNodesCount() - 2);
357 } else
358 continue;
359 if (candidateWay.equals(lastWay) && candidateComingToHead)
360 continue;
361
362 double candidateAngle = getAngle(headNode, candidatePrevNode, prevNode);
363
364 if (mostLeft == null || candidateAngle < angle || (Utils.equalsEpsilon(candidateAngle, angle) && !candidateComingToHead)) {
365 // Candidate is most left
366 mostLeft = candidateWay;
367 comingToHead = candidateComingToHead;
368 angle = candidateAngle;
369 }
370 }
371
372 return comingToHead ? mostLeft : null;
373 }
374 }
375
376 /**
377 * Helper storage class for finding findOuterWays
378 * @author viesturs
379 */
380 static class PolygonLevel {
381 public final int level;
382 public final AssembledMultipolygon pol;
383
384 PolygonLevel(AssembledMultipolygon pol, int level) {
385 this.pol = pol;
386 this.level = level;
387 }
388 }
389
390 /**
391 * Constructs a new {@code JoinAreasAction}.
392 */
393 public JoinAreasAction() {
394 super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"),
395 Shortcut.registerShortcut("tools:joinareas", tr("Tool: {0}", tr("Join overlapping Areas")),
396 KeyEvent.VK_J, Shortcut.SHIFT), true);
397 }
398
399 /**
400 * Gets called whenever the shortcut is pressed or the menu entry is selected.
401 * Checks whether the selected objects are suitable to join and joins them if so.
402 */
403 @Override
404 public void actionPerformed(ActionEvent e) {
405 join(Main.main.getCurrentDataSet().getSelectedWays());
406 }
407
408 /**
409 * Joins the given ways.
410 * @param ways Ways to join
411 * @since 7534
412 */
413 public void join(Collection<Way> ways) {
414 addedRelations.clear();
415
416 if (ways.isEmpty()) {
417 new Notification(
418 tr("Please select at least one closed way that should be joined."))
419 .setIcon(JOptionPane.INFORMATION_MESSAGE)
420 .show();
421 return;
422 }
423
424 List<Node> allNodes = new ArrayList<>();
425 for (Way way : ways) {
426 if (!way.isClosed()) {
427 new Notification(
428 tr("One of the selected ways is not closed and therefore cannot be joined."))
429 .setIcon(JOptionPane.INFORMATION_MESSAGE)
430 .show();
431 return;
432 }
433
434 allNodes.addAll(way.getNodes());
435 }
436
437 // TODO: Only display this warning when nodes outside dataSourceArea are deleted
438 boolean ok = Command.checkAndConfirmOutlyingOperation("joinarea", tr("Join area confirmation"),
439 trn("The selected way has nodes outside of the downloaded data region.",
440 "The selected ways have nodes outside of the downloaded data region.",
441 ways.size()) + "<br/>"
442 + tr("This can lead to nodes being deleted accidentally.") + "<br/>"
443 + tr("Are you really sure to continue?")
444 + tr("Please abort if you are not sure"),
445 tr("The selected area is incomplete. Continue?"),
446 allNodes, null);
447 if (!ok) return;
448
449 //analyze multipolygon relations and collect all areas
450 List<Multipolygon> areas = collectMultipolygons(ways);
451
452 if (areas == null)
453 //too complex multipolygon relations found
454 return;
455
456 if (!testJoin(areas)) {
457 new Notification(
458 tr("No intersection found. Nothing was changed."))
459 .setIcon(JOptionPane.INFORMATION_MESSAGE)
460 .show();
461 return;
462 }
463
464 if (!resolveTagConflicts(areas))
465 return;
466 //user canceled, do nothing.
467
468 try {
469 // see #11026 - Because <ways> is a dynamic filtered (on ways) of a filtered (on selected objects) collection,
470 // retrieve effective dataset before joining the ways (which affects the selection, thus, the <ways> collection)
471 // Dataset retrieving allows to call this code without relying on Main.getCurrentDataSet(), thus, on a mapview instance
472 DataSet ds = ways.iterator().next().getDataSet();
473
474 // Do the job of joining areas
475 JoinAreasResult result = joinAreas(areas);
476
477 if (result.hasChanges) {
478 // move tags from ways to newly created relations
479 // TODO: do we need to also move tags for the modified relations?
480 for (Relation r: addedRelations) {
481 cmds.addAll(CreateMultipolygonAction.removeTagsFromWaysIfNeeded(r));
482 }
483 commitCommands(tr("Move tags from ways to relations"));
484
485 List<Way> allWays = new ArrayList<>();
486 for (Multipolygon pol : result.polygons) {
487 allWays.add(pol.outerWay);
488 allWays.addAll(pol.innerWays);
489 }
490 if (ds != null) {
491 ds.setSelected(allWays);
492 Main.map.mapView.repaint();
493 }
494 } else {
495 new Notification(
496 tr("No intersection found. Nothing was changed."))
497 .setIcon(JOptionPane.INFORMATION_MESSAGE)
498 .show();
499 }
500 } catch (UserCancelException exception) {
501 //revert changes
502 //FIXME: this is dirty hack
503 makeCommitsOneAction(tr("Reverting changes"));
504 Main.main.undoRedo.undo();
505 Main.main.undoRedo.redoCommands.clear();
506 }
507 }
508
509 /**
510 * Tests if the areas have some intersections to join.
511 * @param areas Areas to test
512 * @return {@code true} if areas are joinable
513 */
514 private boolean testJoin(List<Multipolygon> areas) {
515 List<Way> allStartingWays = new ArrayList<>();
516
517 for (Multipolygon area : areas) {
518 allStartingWays.add(area.outerWay);
519 allStartingWays.addAll(area.innerWays);
520 }
521
522 //find intersection points
523 Set<Node> nodes = Geometry.addIntersections(allStartingWays, true, cmds);
524 return !nodes.isEmpty();
525 }
526
527 /**
528 * Will join two or more overlapping areas
529 * @param areas list of areas to join
530 * @return new area formed.
531 * @throws UserCancelException if user cancels the operation
532 */
533 private JoinAreasResult joinAreas(List<Multipolygon> areas) throws UserCancelException {
534
535 JoinAreasResult result = new JoinAreasResult();
536 result.hasChanges = false;
537
538 List<Way> allStartingWays = new ArrayList<>();
539 List<Way> innerStartingWays = new ArrayList<>();
540 List<Way> outerStartingWays = new ArrayList<>();
541
542 for (Multipolygon area : areas) {
543 outerStartingWays.add(area.outerWay);
544 innerStartingWays.addAll(area.innerWays);
545 }
546
547 allStartingWays.addAll(innerStartingWays);
548 allStartingWays.addAll(outerStartingWays);
549
550 //first remove nodes in the same coordinate
551 boolean removedDuplicates = false;
552 removedDuplicates |= removeDuplicateNodes(allStartingWays);
553
554 if (removedDuplicates) {
555 result.hasChanges = true;
556 commitCommands(marktr("Removed duplicate nodes"));
557 }
558
559 //find intersection points
560 Set<Node> nodes = Geometry.addIntersections(allStartingWays, false, cmds);
561
562 //no intersections, return.
563 if (nodes.isEmpty())
564 return result;
565 commitCommands(marktr("Added node on all intersections"));
566
567 List<RelationRole> relations = new ArrayList<>();
568
569 // Remove ways from all relations so ways can be combined/split quietly
570 for (Way way : allStartingWays) {
571 relations.addAll(removeFromAllRelations(way));
572 }
573
574 // Don't warn now, because it will really look corrupted
575 boolean warnAboutRelations = !relations.isEmpty() && allStartingWays.size() > 1;
576
577 List<WayInPolygon> preparedWays = new ArrayList<>();
578
579 for (Way way : outerStartingWays) {
580 List<Way> splitWays = splitWayOnNodes(way, nodes);
581 preparedWays.addAll(markWayInsideSide(splitWays, false));
582 }
583
584 for (Way way : innerStartingWays) {
585 List<Way> splitWays = splitWayOnNodes(way, nodes);
586 preparedWays.addAll(markWayInsideSide(splitWays, true));
587 }
588
589 // Find boundary ways
590 List<Way> discardedWays = new ArrayList<>();
591 List<AssembledPolygon> boundaries = findBoundaryPolygons(preparedWays, discardedWays);
592
593 //find polygons
594 List<AssembledMultipolygon> preparedPolygons = findPolygons(boundaries);
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 that splits way into smaller parts, using the prepared nodes list as split points.
933 * Uses {@link SplitWayAction#splitWay} for the heavy lifting.
934 * @param way way to split
935 * @param nodes split points
936 * @return list of split ways (or original ways if no splitting is done).
937 */
938 private List<Way> splitWayOnNodes(Way way, Set<Node> nodes) {
939
940 List<Way> result = new ArrayList<>();
941 List<List<Node>> chunks = buildNodeChunks(way, nodes);
942
943 if (chunks.size() > 1) {
944 SplitWayResult split = SplitWayAction.splitWay(getEditLayer(), way, chunks,
945 Collections.<OsmPrimitive>emptyList(), SplitWayAction.Strategy.keepFirstChunk());
946
947 //execute the command, we need the results
948 cmds.add(split.getCommand());
949 commitCommands(marktr("Split ways into fragments"));
950
951 result.add(split.getOriginalWay());
952 result.addAll(split.getNewWays());
953 } else {
954 //nothing to split
955 result.add(way);
956 }
957
958 return result;
959 }
960
961 /**
962 * Simple chunking version. Does not care about circular ways and result being
963 * proper, we will glue it all back together later on.
964 * @param way the way to chunk
965 * @param splitNodes the places where to cut.
966 * @return list of node paths to produce.
967 */
968 private static List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) {
969 List<List<Node>> result = new ArrayList<>();
970 List<Node> curList = new ArrayList<>();
971
972 for (Node node : way.getNodes()) {
973 curList.add(node);
974 if (curList.size() > 1 && splitNodes.contains(node)) {
975 result.add(curList);
976 curList = new ArrayList<>();
977 curList.add(node);
978 }
979 }
980
981 if (curList.size() > 1) {
982 result.add(curList);
983 }
984
985 return result;
986 }
987
988 /**
989 * This method finds which ways are outer and which are inner.
990 * @param boundaries list of joined boundaries to search in
991 * @return outer ways
992 */
993 private List<AssembledMultipolygon> findPolygons(Collection<AssembledPolygon> boundaries) {
994
995 List<PolygonLevel> list = findOuterWaysImpl(0, boundaries);
996 List<AssembledMultipolygon> result = new ArrayList<>();
997
998 //take every other level
999 for (PolygonLevel pol : list) {
1000 if (pol.level % 2 == 0) {
1001 result.add(pol.pol);
1002 }
1003 }
1004
1005 return result;
1006 }
1007
1008 /**
1009 * Collects outer way and corresponding inner ways from all boundaries.
1010 * @param level depth level
1011 * @param boundaryWays list of joined boundaries to search in
1012 * @return the outermost Way.
1013 */
1014 private List<PolygonLevel> findOuterWaysImpl(int level, Collection<AssembledPolygon> boundaryWays) {
1015
1016 //TODO: bad performance for deep nestings...
1017 List<PolygonLevel> result = new ArrayList<>();
1018
1019 for (AssembledPolygon outerWay : boundaryWays) {
1020
1021 boolean outerGood = true;
1022 List<AssembledPolygon> innerCandidates = new ArrayList<>();
1023
1024 for (AssembledPolygon innerWay : boundaryWays) {
1025 if (innerWay == outerWay) {
1026 continue;
1027 }
1028
1029 if (wayInsideWay(outerWay, innerWay)) {
1030 outerGood = false;
1031 break;
1032 } else if (wayInsideWay(innerWay, outerWay)) {
1033 innerCandidates.add(innerWay);
1034 }
1035 }
1036
1037 if (!outerGood) {
1038 continue;
1039 }
1040
1041 //add new outer polygon
1042 AssembledMultipolygon pol = new AssembledMultipolygon(outerWay);
1043 PolygonLevel polLev = new PolygonLevel(pol, level);
1044
1045 //process inner ways
1046 if (!innerCandidates.isEmpty()) {
1047 List<PolygonLevel> innerList = findOuterWaysImpl(level + 1, innerCandidates);
1048 result.addAll(innerList);
1049
1050 for (PolygonLevel pl : innerList) {
1051 if (pl.level == level + 1) {
1052 pol.innerWays.add(pl.pol.outerWay);
1053 }
1054 }
1055 }
1056
1057 result.add(polLev);
1058 }
1059
1060 return result;
1061 }
1062
1063 /**
1064 * Finds all ways that form inner or outer boundaries.
1065 * @param multigonWays A list of (splitted) ways that form a multigon and share common end nodes on intersections.
1066 * @param discardedResult this list is filled with ways that are to be discarded
1067 * @return A list of ways that form the outer and inner boundaries of the multigon.
1068 */
1069 public static List<AssembledPolygon> findBoundaryPolygons(Collection<WayInPolygon> multigonWays,
1070 List<Way> discardedResult) {
1071 //first find all discardable ways, by getting outer shells.
1072 //this will produce incorrect boundaries in some cases, but second pass will fix it.
1073 List<WayInPolygon> discardedWays = new ArrayList<>();
1074
1075 // In multigonWays collection, some way are just a point (i.e. way like nodeA-nodeA)
1076 // This seems to appear when is apply over invalid way like #9911 test-case
1077 // Remove all of these way to make the next work.
1078 List<WayInPolygon> cleanMultigonWays = new ArrayList<>();
1079 for (WayInPolygon way: multigonWays) {
1080 if (way.way.getNodesCount() == 2 && way.way.isClosed())
1081 discardedWays.add(way);
1082 else
1083 cleanMultigonWays.add(way);
1084 }
1085
1086 WayTraverser traverser = new WayTraverser(cleanMultigonWays);
1087 List<AssembledPolygon> result = new ArrayList<>();
1088
1089 WayInPolygon startWay;
1090 while ((startWay = traverser.startNewWay()) != null) {
1091 List<WayInPolygon> path = new ArrayList<>();
1092 List<WayInPolygon> startWays = new ArrayList<>();
1093 path.add(startWay);
1094 while (true) {
1095 WayInPolygon leftComing;
1096 while ((leftComing = traverser.leftComingWay()) != null) {
1097 if (startWays.contains(leftComing))
1098 break;
1099 // Need restart traverser walk
1100 path.clear();
1101 path.add(leftComing);
1102 traverser.setStartWay(leftComing);
1103 startWays.add(leftComing);
1104 break;
1105 }
1106 WayInPolygon nextWay = traverser.walk();
1107 if (nextWay == null)
1108 throw new RuntimeException("Join areas internal error.");
1109 if (path.get(0) == nextWay) {
1110 // path is closed -> stop here
1111 AssembledPolygon ring = new AssembledPolygon(path);
1112 if (ring.getNodes().size() <= 2) {
1113 // Invalid ring (2 nodes) -> remove
1114 traverser.removeWays(path);
1115 for (WayInPolygon way: path) {
1116 discardedResult.add(way.way);
1117 }
1118 } else {
1119 // Close ring -> add
1120 result.add(ring);
1121 traverser.removeWays(path);
1122 }
1123 break;
1124 }
1125 if (path.contains(nextWay)) {
1126 // Inner loop -> remove
1127 int index = path.indexOf(nextWay);
1128 while (path.size() > index) {
1129 WayInPolygon currentWay = path.get(index);
1130 discardedResult.add(currentWay.way);
1131 traverser.removeWay(currentWay);
1132 path.remove(index);
1133 }
1134 traverser.setStartWay(path.get(index-1));
1135 } else {
1136 path.add(nextWay);
1137 }
1138 }
1139 }
1140
1141 return fixTouchingPolygons(result);
1142 }
1143
1144 /**
1145 * This method checks if polygons have several touching parts and splits them in several polygons.
1146 * @param polygons the polygons to process.
1147 * @return the resulting list of polygons
1148 */
1149 public static List<AssembledPolygon> fixTouchingPolygons(List<AssembledPolygon> polygons) {
1150 List<AssembledPolygon> newPolygons = new ArrayList<>();
1151
1152 for (AssembledPolygon ring : polygons) {
1153 ring.reverse();
1154 WayTraverser traverser = new WayTraverser(ring.ways);
1155 WayInPolygon startWay;
1156
1157 while ((startWay = traverser.startNewWay()) != null) {
1158 List<WayInPolygon> simpleRingWays = new ArrayList<>();
1159 simpleRingWays.add(startWay);
1160 WayInPolygon nextWay;
1161 while ((nextWay = traverser.walk()) != startWay) {
1162 if (nextWay == null)
1163 throw new RuntimeException("Join areas internal error.");
1164 simpleRingWays.add(nextWay);
1165 }
1166 traverser.removeWays(simpleRingWays);
1167 AssembledPolygon simpleRing = new AssembledPolygon(simpleRingWays);
1168 simpleRing.reverse();
1169 newPolygons.add(simpleRing);
1170 }
1171 }
1172
1173 return newPolygons;
1174 }
1175
1176 /**
1177 * Tests if way is inside other way
1178 * @param outside outer polygon description
1179 * @param inside inner polygon description
1180 * @return {@code true} if inner is inside outer
1181 */
1182 public static boolean wayInsideWay(AssembledPolygon inside, AssembledPolygon outside) {
1183 Set<Node> outsideNodes = new HashSet<>(outside.getNodes());
1184 List<Node> insideNodes = inside.getNodes();
1185
1186 for (Node insideNode : insideNodes) {
1187
1188 if (!outsideNodes.contains(insideNode))
1189 //simply test the one node
1190 return Geometry.nodeInsidePolygon(insideNode, outside.getNodes());
1191 }
1192
1193 //all nodes shared.
1194 return false;
1195 }
1196
1197 /**
1198 * Joins the lists of ways.
1199 * @param polygon The list of outer ways that belong to that multigon.
1200 * @return The newly created outer way
1201 * @throws UserCancelException if user cancels the operation
1202 */
1203 private Multipolygon joinPolygon(AssembledMultipolygon polygon) throws UserCancelException {
1204 Multipolygon result = new Multipolygon(joinWays(polygon.outerWay.ways));
1205
1206 for (AssembledPolygon pol : polygon.innerWays) {
1207 result.innerWays.add(joinWays(pol.ways));
1208 }
1209
1210 return result;
1211 }
1212
1213 /**
1214 * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway.
1215 * @param ways The list of outer ways that belong to that multigon.
1216 * @return The newly created outer way
1217 * @throws UserCancelException if user cancels the operation
1218 */
1219 private Way joinWays(List<WayInPolygon> ways) throws UserCancelException {
1220
1221 //leave original orientation, if all paths are reverse.
1222 boolean allReverse = true;
1223 for (WayInPolygon way : ways) {
1224 allReverse &= !way.insideToTheRight;
1225 }
1226
1227 if (allReverse) {
1228 for (WayInPolygon way : ways) {
1229 way.insideToTheRight = !way.insideToTheRight;
1230 }
1231 }
1232
1233 Way joinedWay = joinOrientedWays(ways);
1234
1235 //should not happen
1236 if (joinedWay == null || !joinedWay.isClosed())
1237 throw new RuntimeException("Join areas internal error.");
1238
1239 return joinedWay;
1240 }
1241
1242 /**
1243 * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath)
1244 * @param ways The list of ways to join and reverse
1245 * @return The newly created way
1246 * @throws UserCancelException if user cancels the operation
1247 */
1248 private Way joinOrientedWays(List<WayInPolygon> ways) throws UserCancelException {
1249 if (ways.size() < 2)
1250 return ways.get(0).way;
1251
1252 // This will turn ways so all of them point in the same direction and CombineAction won't bug
1253 // the user about this.
1254
1255 //TODO: ReverseWay and Combine way are really slow and we use them a lot here. This slows down large joins.
1256 List<Way> actionWays = new ArrayList<>(ways.size());
1257
1258 for (WayInPolygon way : ways) {
1259 actionWays.add(way.way);
1260
1261 if (!way.insideToTheRight) {
1262 ReverseWayResult res = ReverseWayAction.reverseWay(way.way);
1263 Main.main.undoRedo.add(res.getReverseCommand());
1264 cmdsCount++;
1265 }
1266 }
1267
1268 Pair<Way, Command> result = CombineWayAction.combineWaysWorker(actionWays);
1269
1270 Main.main.undoRedo.add(result.b);
1271 cmdsCount++;
1272
1273 return result.a;
1274 }
1275
1276 /**
1277 * This method analyzes multipolygon relationships of given ways and collects addition inner ways to consider.
1278 * @param selectedWays the selected ways
1279 * @return list of polygons, or null if too complex relation encountered.
1280 */
1281 private List<Multipolygon> collectMultipolygons(Collection<Way> selectedWays) {
1282
1283 List<Multipolygon> result = new ArrayList<>();
1284
1285 //prepare the lists, to minimize memory allocation.
1286 List<Way> outerWays = new ArrayList<>();
1287 List<Way> innerWays = new ArrayList<>();
1288
1289 Set<Way> processedOuterWays = new LinkedHashSet<>();
1290 Set<Way> processedInnerWays = new LinkedHashSet<>();
1291
1292 for (Relation r : OsmPrimitive.getParentRelations(selectedWays)) {
1293 if (r.isDeleted() || !r.isMultipolygon()) {
1294 continue;
1295 }
1296
1297 boolean hasKnownOuter = false;
1298 outerWays.clear();
1299 innerWays.clear();
1300
1301 for (RelationMember rm : r.getMembers()) {
1302 if ("outer".equalsIgnoreCase(rm.getRole())) {
1303 outerWays.add(rm.getWay());
1304 hasKnownOuter |= selectedWays.contains(rm.getWay());
1305 } else if ("inner".equalsIgnoreCase(rm.getRole())) {
1306 innerWays.add(rm.getWay());
1307 }
1308 }
1309
1310 if (!hasKnownOuter) {
1311 continue;
1312 }
1313
1314 if (outerWays.size() > 1) {
1315 new Notification(
1316 tr("Sorry. Cannot handle multipolygon relations with multiple outer ways."))
1317 .setIcon(JOptionPane.INFORMATION_MESSAGE)
1318 .show();
1319 return null;
1320 }
1321
1322 Way outerWay = outerWays.get(0);
1323
1324 //retain only selected inner ways
1325 innerWays.retainAll(selectedWays);
1326
1327 if (processedOuterWays.contains(outerWay)) {
1328 new Notification(
1329 tr("Sorry. Cannot handle way that is outer in multiple multipolygon relations."))
1330 .setIcon(JOptionPane.INFORMATION_MESSAGE)
1331 .show();
1332 return null;
1333 }
1334
1335 if (processedInnerWays.contains(outerWay)) {
1336 new Notification(
1337 tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
1338 .setIcon(JOptionPane.INFORMATION_MESSAGE)
1339 .show();
1340 return null;
1341 }
1342
1343 for (Way way :innerWays) {
1344 if (processedOuterWays.contains(way)) {
1345 new Notification(
1346 tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
1347 .setIcon(JOptionPane.INFORMATION_MESSAGE)
1348 .show();
1349 return null;
1350 }
1351
1352 if (processedInnerWays.contains(way)) {
1353 new Notification(
1354 tr("Sorry. Cannot handle way that is inner in multiple multipolygon relations."))
1355 .setIcon(JOptionPane.INFORMATION_MESSAGE)
1356 .show();
1357 return null;
1358 }
1359 }
1360
1361 processedOuterWays.add(outerWay);
1362 processedInnerWays.addAll(innerWays);
1363
1364 Multipolygon pol = new Multipolygon(outerWay);
1365 pol.innerWays.addAll(innerWays);
1366
1367 result.add(pol);
1368 }
1369
1370 //add remaining ways, not in relations
1371 for (Way way : selectedWays) {
1372 if (processedOuterWays.contains(way) || processedInnerWays.contains(way)) {
1373 continue;
1374 }
1375
1376 result.add(new Multipolygon(way));
1377 }
1378
1379 return result;
1380 }
1381
1382 /**
1383 * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations
1384 * @param inner List of already closed inner ways
1385 * @return The list of relation with roles to add own relation to
1386 */
1387 private RelationRole addOwnMultipolygonRelation(Collection<Way> inner) {
1388 if (inner.isEmpty()) return null;
1389 // Create new multipolygon relation and add all inner ways to it
1390 Relation newRel = new Relation();
1391 newRel.put("type", "multipolygon");
1392 for (Way w : inner) {
1393 newRel.addMember(new RelationMember("inner", w));
1394 }
1395 cmds.add(new AddCommand(newRel));
1396 addedRelations.add(newRel);
1397
1398 // We don't add outer to the relation because it will be handed to fixRelations()
1399 // which will then do the remaining work.
1400 return new RelationRole(newRel, "outer");
1401 }
1402
1403 /**
1404 * Removes a given OsmPrimitive from all relations.
1405 * @param osm Element to remove from all relations
1406 * @return List of relations with roles the primitives was part of
1407 */
1408 private List<RelationRole> removeFromAllRelations(OsmPrimitive osm) {
1409 List<RelationRole> result = new ArrayList<>();
1410
1411 for (Relation r : Main.main.getCurrentDataSet().getRelations()) {
1412 if (r.isDeleted()) {
1413 continue;
1414 }
1415 for (RelationMember rm : r.getMembers()) {
1416 if (rm.getMember() != osm) {
1417 continue;
1418 }
1419
1420 Relation newRel = new Relation(r);
1421 List<RelationMember> members = newRel.getMembers();
1422 members.remove(rm);
1423 newRel.setMembers(members);
1424
1425 cmds.add(new ChangeCommand(r, newRel));
1426 RelationRole saverel = new RelationRole(r, rm.getRole());
1427 if (!result.contains(saverel)) {
1428 result.add(saverel);
1429 }
1430 break;
1431 }
1432 }
1433
1434 commitCommands(marktr("Removed Element from Relations"));
1435 return result;
1436 }
1437
1438 /**
1439 * Adds the previously removed relations again to the outer way. If there are multiple multipolygon
1440 * relations where the joined areas were in "outer" role a new relation is created instead with all
1441 * members of both. This function depends on multigon relations to be valid already, it won't fix them.
1442 * @param rels List of relations with roles the (original) ways were part of
1443 * @param outer The newly created outer area/way
1444 * @param ownMultipol elements to directly add as outer
1445 * @param relationsToDelete set of relations to delete.
1446 */
1447 private void fixRelations(List<RelationRole> rels, Way outer, RelationRole ownMultipol, Set<Relation> relationsToDelete) {
1448 List<RelationRole> multiouters = new ArrayList<>();
1449
1450 if (ownMultipol != null) {
1451 multiouters.add(ownMultipol);
1452 }
1453
1454 for (RelationRole r : rels) {
1455 if (r.rel.isMultipolygon() && "outer".equalsIgnoreCase(r.role)) {
1456 multiouters.add(r);
1457 continue;
1458 }
1459 // Add it back!
1460 Relation newRel = new Relation(r.rel);
1461 newRel.addMember(new RelationMember(r.role, outer));
1462 cmds.add(new ChangeCommand(r.rel, newRel));
1463 }
1464
1465 Relation newRel;
1466 switch (multiouters.size()) {
1467 case 0:
1468 return;
1469 case 1:
1470 // Found only one to be part of a multipolygon relation, so just add it back as well
1471 newRel = new Relation(multiouters.get(0).rel);
1472 newRel.addMember(new RelationMember(multiouters.get(0).role, outer));
1473 cmds.add(new ChangeCommand(multiouters.get(0).rel, newRel));
1474 return;
1475 default:
1476 // Create a new relation with all previous members and (Way)outer as outer.
1477 newRel = new Relation();
1478 for (RelationRole r : multiouters) {
1479 // Add members
1480 for (RelationMember rm : r.rel.getMembers()) {
1481 if (!newRel.getMembers().contains(rm)) {
1482 newRel.addMember(rm);
1483 }
1484 }
1485 // Add tags
1486 for (String key : r.rel.keySet()) {
1487 newRel.put(key, r.rel.get(key));
1488 }
1489 // Delete old relation
1490 relationsToDelete.add(r.rel);
1491 }
1492 newRel.addMember(new RelationMember("outer", outer));
1493 cmds.add(new AddCommand(newRel));
1494 }
1495 }
1496
1497 /**
1498 * Remove all tags from the all the way
1499 * @param ways The List of Ways to remove all tags from
1500 */
1501 private void stripTags(Collection<Way> ways) {
1502 for (Way w : ways) {
1503 stripTags(w);
1504 }
1505 /* I18N: current action printed in status display */
1506 commitCommands(marktr("Remove tags from inner ways"));
1507 }
1508
1509 /**
1510 * Remove all tags from the way
1511 * @param x The Way to remove all tags from
1512 */
1513 private void stripTags(Way x) {
1514 Way y = new Way(x);
1515 for (String key : x.keySet()) {
1516 y.remove(key);
1517 }
1518 cmds.add(new ChangeCommand(x, y));
1519 }
1520
1521 /**
1522 * Takes the last cmdsCount actions back and combines them into a single action
1523 * (for when the user wants to undo the join action)
1524 * @param message The commit message to display
1525 */
1526 private void makeCommitsOneAction(String message) {
1527 UndoRedoHandler ur = Main.main.undoRedo;
1528 cmds.clear();
1529 int i = Math.max(ur.commands.size() - cmdsCount, 0);
1530 for (; i < ur.commands.size(); i++) {
1531 cmds.add(ur.commands.get(i));
1532 }
1533
1534 for (i = 0; i < cmds.size(); i++) {
1535 ur.undo();
1536 }
1537
1538 commitCommands(message == null ? marktr("Join Areas Function") : message);
1539 cmdsCount = 0;
1540 }
1541
1542 @Override
1543 protected void updateEnabledState() {
1544 if (getCurrentDataSet() == null) {
1545 setEnabled(false);
1546 } else {
1547 updateEnabledState(getCurrentDataSet().getSelected());
1548 }
1549 }
1550
1551 @Override
1552 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
1553 setEnabled(selection != null && !selection.isEmpty());
1554 }
1555}
Note: See TracBrowser for help on using the repository browser.