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

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

see #15229 - see #15182 - see #13036 - convert SplitWayAction to SplitWayCommand to remove dependence of DeleteCommand on actions package

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