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

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

sonar - fb-contrib:SEO_SUBOPTIMAL_EXPRESSION_ORDER - Performance - Method orders expressions in a conditional in a sub optimal way

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