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

Last change on this file since 11279 was 11252, checked in by Don-vip, 7 years ago

see #10387 - refactor actions to fix taginfo script

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