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

Last change on this file since 9349 was 9295, checked in by simon04, 8 years ago

Fixup r9293

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