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

Last change on this file since 13111 was 13068, checked in by Don-vip, 6 years ago

sonar - fix a few issues

  • Property svn:eol-style set to native
File size: 57.3 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 = Objects.requireNonNull(way, "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 // Do the job of joining areas
539 JoinAreasResult result = joinAreas(areas);
540
541 if (result.hasChanges) {
542 // move tags from ways to newly created relations
543 // TODO: do we need to also move tags for the modified relations?
544 for (Relation r: addedRelations) {
545 cmds.addAll(CreateMultipolygonAction.removeTagsFromWaysIfNeeded(r));
546 }
547 commitCommands(tr("Move tags from ways to relations"));
548
549 List<Way> allWays = new ArrayList<>();
550 for (Multipolygon pol : result.polygons) {
551 allWays.add(pol.outerWay);
552 allWays.addAll(pol.innerWays);
553 }
554 if (ds != null) {
555 ds.setSelected(allWays);
556 }
557 } else {
558 new Notification(
559 tr("No intersection found. Nothing was changed."))
560 .setIcon(JOptionPane.INFORMATION_MESSAGE)
561 .show();
562 }
563 } catch (UserCancelException exception) {
564 Logging.trace(exception);
565 //revert changes
566 //FIXME: this is dirty hack
567 makeCommitsOneAction(tr("Reverting changes"));
568 MainApplication.undoRedo.undo();
569 MainApplication.undoRedo.redoCommands.clear();
570 }
571 }
572
573 /**
574 * Tests if the areas have some intersections to join.
575 * @param areas Areas to test
576 * @return {@code true} if areas are joinable
577 */
578 private boolean testJoin(List<Multipolygon> areas) {
579 List<Way> allStartingWays = new ArrayList<>();
580
581 for (Multipolygon area : areas) {
582 allStartingWays.add(area.outerWay);
583 allStartingWays.addAll(area.innerWays);
584 }
585
586 //find intersection points
587 Set<Node> nodes = Geometry.addIntersections(allStartingWays, true, cmds);
588 return !nodes.isEmpty();
589 }
590
591 /**
592 * Will join two or more overlapping areas
593 * @param areas list of areas to join
594 * @return new area formed.
595 * @throws UserCancelException if user cancels the operation
596 */
597 public JoinAreasResult joinAreas(List<Multipolygon> areas) throws UserCancelException {
598
599 // see #11026 - Because <ways> is a dynamic filtered (on ways) of a filtered (on selected objects) collection,
600 // retrieve effective dataset before joining the ways (which affects the selection, thus, the <ways> collection)
601 // Dataset retrieving allows to call this code without relying on Main.getCurrentDataSet(), thus, on a mapview instance
602 if (!areas.isEmpty()) {
603 ds = areas.get(0).getOuterWay().getDataSet();
604 }
605
606 boolean hasChanges = false;
607
608 List<Way> allStartingWays = new ArrayList<>();
609 List<Way> innerStartingWays = new ArrayList<>();
610 List<Way> outerStartingWays = new ArrayList<>();
611
612 for (Multipolygon area : areas) {
613 outerStartingWays.add(area.outerWay);
614 innerStartingWays.addAll(area.innerWays);
615 }
616
617 allStartingWays.addAll(innerStartingWays);
618 allStartingWays.addAll(outerStartingWays);
619
620 //first remove nodes in the same coordinate
621 boolean removedDuplicates = false;
622 removedDuplicates |= removeDuplicateNodes(allStartingWays);
623
624 if (removedDuplicates) {
625 hasChanges = true;
626 commitCommands(marktr("Removed duplicate nodes"));
627 }
628
629 //find intersection points
630 Set<Node> nodes = Geometry.addIntersections(allStartingWays, false, cmds);
631
632 //no intersections, return.
633 if (nodes.isEmpty())
634 return new JoinAreasResult(hasChanges, null);
635 commitCommands(marktr("Added node on all intersections"));
636
637 List<RelationRole> relations = new ArrayList<>();
638
639 // Remove ways from all relations so ways can be combined/split quietly
640 for (Way way : allStartingWays) {
641 relations.addAll(removeFromAllRelations(way));
642 }
643
644 // Don't warn now, because it will really look corrupted
645 boolean warnAboutRelations = !relations.isEmpty() && allStartingWays.size() > 1;
646
647 List<WayInPolygon> preparedWays = new ArrayList<>();
648
649 for (Way way : outerStartingWays) {
650 List<Way> splitWays = splitWayOnNodes(way, nodes);
651 preparedWays.addAll(markWayInsideSide(splitWays, false));
652 }
653
654 for (Way way : innerStartingWays) {
655 List<Way> splitWays = splitWayOnNodes(way, nodes);
656 preparedWays.addAll(markWayInsideSide(splitWays, true));
657 }
658
659 // Find boundary ways
660 List<Way> discardedWays = new ArrayList<>();
661 List<AssembledPolygon> boundaries = findBoundaryPolygons(preparedWays, discardedWays);
662
663 //find polygons
664 List<AssembledMultipolygon> preparedPolygons = findPolygons(boundaries);
665
666 //assemble final polygons
667 List<Multipolygon> polygons = new ArrayList<>();
668 Set<Relation> relationsToDelete = new LinkedHashSet<>();
669
670 for (AssembledMultipolygon pol : preparedPolygons) {
671
672 //create the new ways
673 Multipolygon resultPol = joinPolygon(pol);
674
675 //create multipolygon relation, if necessary.
676 RelationRole ownMultipolygonRelation = addOwnMultipolygonRelation(resultPol.innerWays);
677
678 //add back the original relations, merged with our new multipolygon relation
679 fixRelations(relations, resultPol.outerWay, ownMultipolygonRelation, relationsToDelete);
680
681 //strip tags from inner ways
682 //TODO: preserve tags on existing inner ways
683 stripTags(resultPol.innerWays);
684
685 polygons.add(resultPol);
686 }
687
688 commitCommands(marktr("Assemble new polygons"));
689
690 for (Relation rel: relationsToDelete) {
691 cmds.add(new DeleteCommand(rel));
692 }
693
694 commitCommands(marktr("Delete relations"));
695
696 // Delete the discarded inner ways
697 if (!discardedWays.isEmpty()) {
698 Command deleteCmd = DeleteCommand.delete(discardedWays, true);
699 if (deleteCmd != null) {
700 cmds.add(deleteCmd);
701 commitCommands(marktr("Delete Ways that are not part of an inner multipolygon"));
702 }
703 }
704
705 makeCommitsOneAction(marktr("Joined overlapping areas"));
706
707 if (warnAboutRelations) {
708 new Notification(
709 tr("Some of the ways were part of relations that have been modified.<br>Please verify no errors have been introduced."))
710 .setIcon(JOptionPane.INFORMATION_MESSAGE)
711 .setDuration(Notification.TIME_LONG)
712 .show();
713 }
714
715 return new JoinAreasResult(true, polygons);
716 }
717
718 /**
719 * Checks if tags of two given ways differ, and presents the user a dialog to solve conflicts
720 * @param polygons ways to check
721 * @return {@code true} if all conflicts are resolved, {@code false} if conflicts remain.
722 */
723 private boolean resolveTagConflicts(List<Multipolygon> polygons) {
724
725 List<Way> ways = new ArrayList<>();
726
727 for (Multipolygon pol : polygons) {
728 ways.add(pol.outerWay);
729 ways.addAll(pol.innerWays);
730 }
731
732 if (ways.size() < 2) {
733 return true;
734 }
735
736 TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways);
737 try {
738 cmds.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(wayTags, ways, ways));
739 commitCommands(marktr("Fix tag conflicts"));
740 return true;
741 } catch (UserCancelException ex) {
742 Logging.trace(ex);
743 return false;
744 }
745 }
746
747 /**
748 * This method removes duplicate points (if any) from the input way.
749 * @param ways the ways to process
750 * @return {@code true} if any changes where made
751 */
752 private boolean removeDuplicateNodes(List<Way> ways) {
753 //TODO: maybe join nodes with JoinNodesAction, rather than reconnect the ways.
754
755 Map<Node, Node> nodeMap = new TreeMap<>(new NodePositionComparator());
756 int totalNodesRemoved = 0;
757
758 for (Way way : ways) {
759 if (way.getNodes().size() < 2) {
760 continue;
761 }
762
763 int nodesRemoved = 0;
764 List<Node> newNodes = new ArrayList<>();
765 Node prevNode = null;
766
767 for (Node node : way.getNodes()) {
768 if (!nodeMap.containsKey(node)) {
769 //new node
770 nodeMap.put(node, node);
771
772 //avoid duplicate nodes
773 if (prevNode != node) {
774 newNodes.add(node);
775 } else {
776 nodesRemoved++;
777 }
778 } else {
779 //node with same coordinates already exists, substitute with existing node
780 Node representator = nodeMap.get(node);
781
782 if (representator != node) {
783 nodesRemoved++;
784 }
785
786 //avoid duplicate node
787 if (prevNode != representator) {
788 newNodes.add(representator);
789 }
790 }
791 prevNode = node;
792 }
793
794 if (nodesRemoved > 0) {
795
796 if (newNodes.size() == 1) { //all nodes in the same coordinate - add one more node, to have closed way.
797 newNodes.add(newNodes.get(0));
798 }
799
800 Way newWay = new Way(way);
801 newWay.setNodes(newNodes);
802 cmds.add(new ChangeCommand(way, newWay));
803 totalNodesRemoved += nodesRemoved;
804 }
805 }
806
807 return totalNodesRemoved > 0;
808 }
809
810 /**
811 * Commits the command list with a description
812 * @param description The description of what the commands do
813 */
814 private void commitCommands(String description) {
815 switch(cmds.size()) {
816 case 0:
817 return;
818 case 1:
819 commitCommand(cmds.getFirst());
820 break;
821 default:
822 commitCommand(new SequenceCommand(tr(description), cmds));
823 break;
824 }
825
826 cmds.clear();
827 cmdsCount++;
828 }
829
830 private static void commitCommand(Command c) {
831 if (Main.main != null) {
832 MainApplication.undoRedo.add(c);
833 } else {
834 c.executeCommand();
835 }
836 }
837
838 /**
839 * This method analyzes the way and assigns each part what direction polygon "inside" is.
840 * @param parts the split parts of the way
841 * @param isInner - if true, reverts the direction (for multipolygon islands)
842 * @return list of parts, marked with the inside orientation.
843 * @throws IllegalArgumentException if parts is empty or not circular
844 */
845 private static List<WayInPolygon> markWayInsideSide(List<Way> parts, boolean isInner) {
846
847 //prepare next map
848 Map<Way, Way> nextWayMap = new HashMap<>();
849
850 for (int pos = 0; pos < parts.size(); pos++) {
851
852 if (!parts.get(pos).lastNode().equals(parts.get((pos + 1) % parts.size()).firstNode()))
853 throw new IllegalArgumentException("Way not circular");
854
855 nextWayMap.put(parts.get(pos), parts.get((pos + 1) % parts.size()));
856 }
857
858 //find the node with minimum y - it's guaranteed to be outer. (What about the south pole?)
859 Way topWay = null;
860 Node topNode = null;
861 int topIndex = 0;
862 double minY = Double.POSITIVE_INFINITY;
863
864 for (Way way : parts) {
865 for (int pos = 0; pos < way.getNodesCount(); pos++) {
866 Node node = way.getNode(pos);
867
868 if (node.getEastNorth().getY() < minY) {
869 minY = node.getEastNorth().getY();
870 topWay = way;
871 topNode = node;
872 topIndex = pos;
873 }
874 }
875 }
876
877 if (topWay == null || topNode == null) {
878 throw new IllegalArgumentException();
879 }
880
881 //get the upper way and it's orientation.
882
883 boolean wayClockwise; // orientation of the top way.
884
885 if (topNode.equals(topWay.firstNode()) || topNode.equals(topWay.lastNode())) {
886 Node headNode; // the node at junction
887 Node prevNode; // last node from previous path
888
889 //node is in split point - find the outermost way from this point
890
891 headNode = topNode;
892 //make a fake node that is downwards from head node (smaller Y). It will be a division point between paths.
893 prevNode = new Node(new EastNorth(headNode.getEastNorth().getX(), headNode.getEastNorth().getY() - 1e5));
894
895 topWay = null;
896 wayClockwise = false;
897 Node bestWayNextNode = null;
898
899 for (Way way : parts) {
900 if (way.firstNode().equals(headNode)) {
901 Node nextNode = way.getNode(1);
902
903 if (topWay == null || !Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) {
904 //the new way is better
905 topWay = way;
906 wayClockwise = true;
907 bestWayNextNode = nextNode;
908 }
909 }
910
911 if (way.lastNode().equals(headNode)) {
912 //end adjacent to headNode
913 Node nextNode = way.getNode(way.getNodesCount() - 2);
914
915 if (topWay == null || !Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) {
916 //the new way is better
917 topWay = way;
918 wayClockwise = false;
919 bestWayNextNode = nextNode;
920 }
921 }
922 }
923 } else {
924 //node is inside way - pick the clockwise going end.
925 Node prev = topWay.getNode(topIndex - 1);
926 Node next = topWay.getNode(topIndex + 1);
927
928 //there will be no parallel segments in the middle of way, so all fine.
929 wayClockwise = Geometry.angleIsClockwise(prev, topNode, next);
930 }
931
932 Way curWay = topWay;
933 boolean curWayInsideToTheRight = wayClockwise ^ isInner;
934 List<WayInPolygon> result = new ArrayList<>();
935
936 //iterate till full circle is reached
937 while (curWay != null) {
938
939 //add cur way
940 WayInPolygon resultWay = new WayInPolygon(curWay, curWayInsideToTheRight);
941 result.add(resultWay);
942
943 //process next way
944 Way nextWay = nextWayMap.get(curWay);
945 Node prevNode = curWay.getNode(curWay.getNodesCount() - 2);
946 Node headNode = curWay.lastNode();
947 Node nextNode = nextWay.getNode(1);
948
949 if (nextWay == topWay) {
950 //full loop traversed - all done.
951 break;
952 }
953
954 //find intersecting segments
955 // the intersections will look like this:
956 //
957 // ^
958 // |
959 // X wayBNode
960 // |
961 // wayB |
962 // |
963 // curWay | nextWay
964 //----X----------------->X----------------------X---->
965 // prevNode ^headNode nextNode
966 // |
967 // |
968 // wayA |
969 // |
970 // X wayANode
971 // |
972
973 int intersectionCount = 0;
974
975 for (Way wayA : parts) {
976
977 if (wayA == curWay) {
978 continue;
979 }
980
981 if (wayA.lastNode().equals(headNode)) {
982
983 Way wayB = nextWayMap.get(wayA);
984
985 //test if wayA is opposite wayB relative to curWay and nextWay
986
987 Node wayANode = wayA.getNode(wayA.getNodesCount() - 2);
988 Node wayBNode = wayB.getNode(1);
989
990 boolean wayAToTheRight = Geometry.isToTheRightSideOfLine(prevNode, headNode, nextNode, wayANode);
991 boolean wayBToTheRight = Geometry.isToTheRightSideOfLine(prevNode, headNode, nextNode, wayBNode);
992
993 if (wayAToTheRight != wayBToTheRight) {
994 intersectionCount++;
995 }
996 }
997 }
998
999 //if odd number of crossings, invert orientation
1000 if (intersectionCount % 2 != 0) {
1001 curWayInsideToTheRight = !curWayInsideToTheRight;
1002 }
1003
1004 curWay = nextWay;
1005 }
1006
1007 return result;
1008 }
1009
1010 /**
1011 * This is a method that splits way into smaller parts, using the prepared nodes list as split points.
1012 * Uses {@link SplitWayAction#splitWay} for the heavy lifting.
1013 * @param way way to split
1014 * @param nodes split points
1015 * @return list of split ways (or original ways if no splitting is done).
1016 */
1017 private List<Way> splitWayOnNodes(Way way, Set<Node> nodes) {
1018
1019 List<Way> result = new ArrayList<>();
1020 List<List<Node>> chunks = buildNodeChunks(way, nodes);
1021
1022 if (chunks.size() > 1) {
1023 SplitWayCommand split = SplitWayCommand.splitWay(way, chunks,
1024 Collections.<OsmPrimitive>emptyList(), SplitWayCommand.Strategy.keepFirstChunk());
1025
1026 if (split != null) {
1027 //execute the command, we need the results
1028 cmds.add(split);
1029 commitCommands(marktr("Split ways into fragments"));
1030
1031 result.add(split.getOriginalWay());
1032 result.addAll(split.getNewWays());
1033 }
1034 }
1035 if (result.isEmpty()) {
1036 //nothing to split
1037 result.add(way);
1038 }
1039
1040 return result;
1041 }
1042
1043 /**
1044 * Simple chunking version. Does not care about circular ways and result being
1045 * proper, we will glue it all back together later on.
1046 * @param way the way to chunk
1047 * @param splitNodes the places where to cut.
1048 * @return list of node paths to produce.
1049 */
1050 private static List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) {
1051 List<List<Node>> result = new ArrayList<>();
1052 List<Node> curList = new ArrayList<>();
1053
1054 for (Node node : way.getNodes()) {
1055 curList.add(node);
1056 if (curList.size() > 1 && splitNodes.contains(node)) {
1057 result.add(curList);
1058 curList = new ArrayList<>();
1059 curList.add(node);
1060 }
1061 }
1062
1063 if (curList.size() > 1) {
1064 result.add(curList);
1065 }
1066
1067 return result;
1068 }
1069
1070 /**
1071 * This method finds which ways are outer and which are inner.
1072 * @param boundaries list of joined boundaries to search in
1073 * @return outer ways
1074 */
1075 private static List<AssembledMultipolygon> findPolygons(Collection<AssembledPolygon> boundaries) {
1076
1077 List<PolygonLevel> list = findOuterWaysImpl(0, boundaries);
1078 List<AssembledMultipolygon> result = new ArrayList<>();
1079
1080 //take every other level
1081 for (PolygonLevel pol : list) {
1082 if (pol.level % 2 == 0) {
1083 result.add(pol.pol);
1084 }
1085 }
1086
1087 return result;
1088 }
1089
1090 /**
1091 * Collects outer way and corresponding inner ways from all boundaries.
1092 * @param level depth level
1093 * @param boundaryWays list of joined boundaries to search in
1094 * @return the outermost Way.
1095 */
1096 private static List<PolygonLevel> findOuterWaysImpl(int level, Collection<AssembledPolygon> boundaryWays) {
1097
1098 //TODO: bad performance for deep nestings...
1099 List<PolygonLevel> result = new ArrayList<>();
1100
1101 for (AssembledPolygon outerWay : boundaryWays) {
1102
1103 boolean outerGood = true;
1104 List<AssembledPolygon> innerCandidates = new ArrayList<>();
1105
1106 for (AssembledPolygon innerWay : boundaryWays) {
1107 if (innerWay == outerWay) {
1108 continue;
1109 }
1110
1111 if (wayInsideWay(outerWay, innerWay)) {
1112 outerGood = false;
1113 break;
1114 } else if (wayInsideWay(innerWay, outerWay)) {
1115 innerCandidates.add(innerWay);
1116 }
1117 }
1118
1119 if (!outerGood) {
1120 continue;
1121 }
1122
1123 //add new outer polygon
1124 AssembledMultipolygon pol = new AssembledMultipolygon(outerWay);
1125 PolygonLevel polLev = new PolygonLevel(pol, level);
1126
1127 //process inner ways
1128 if (!innerCandidates.isEmpty()) {
1129 List<PolygonLevel> innerList = findOuterWaysImpl(level + 1, innerCandidates);
1130 result.addAll(innerList);
1131
1132 for (PolygonLevel pl : innerList) {
1133 if (pl.level == level + 1) {
1134 pol.innerWays.add(pl.pol.outerWay);
1135 }
1136 }
1137 }
1138
1139 result.add(polLev);
1140 }
1141
1142 return result;
1143 }
1144
1145 /**
1146 * Finds all ways that form inner or outer boundaries.
1147 * @param multigonWays A list of (splitted) ways that form a multigon and share common end nodes on intersections.
1148 * @param discardedResult this list is filled with ways that are to be discarded
1149 * @return A list of ways that form the outer and inner boundaries of the multigon.
1150 */
1151 public static List<AssembledPolygon> findBoundaryPolygons(Collection<WayInPolygon> multigonWays,
1152 List<Way> discardedResult) {
1153 // In multigonWays collection, some way are just a point (i.e. way like nodeA-nodeA)
1154 // This seems to appear when is apply over invalid way like #9911 test-case
1155 // Remove all of these way to make the next work.
1156 List<WayInPolygon> cleanMultigonWays = new ArrayList<>();
1157 for (WayInPolygon way: multigonWays) {
1158 if (way.way.getNodesCount() != 2 || !way.way.isClosed())
1159 cleanMultigonWays.add(way);
1160 }
1161
1162 WayTraverser traverser = new WayTraverser(cleanMultigonWays);
1163 List<AssembledPolygon> result = new ArrayList<>();
1164
1165 WayInPolygon startWay;
1166 while ((startWay = traverser.startNewWay()) != null) {
1167 List<WayInPolygon> path = new ArrayList<>();
1168 List<WayInPolygon> startWays = new ArrayList<>();
1169 path.add(startWay);
1170 while (true) {
1171 WayInPolygon leftComing;
1172 while ((leftComing = traverser.leftComingWay()) != null) {
1173 if (startWays.contains(leftComing))
1174 break;
1175 // Need restart traverser walk
1176 path.clear();
1177 path.add(leftComing);
1178 traverser.setStartWay(leftComing);
1179 startWays.add(leftComing);
1180 break;
1181 }
1182 WayInPolygon nextWay = traverser.walk();
1183 if (nextWay == null)
1184 throw new JosmRuntimeException("Join areas internal error.");
1185 if (path.get(0) == nextWay) {
1186 // path is closed -> stop here
1187 AssembledPolygon ring = new AssembledPolygon(path);
1188 if (ring.getNodes().size() <= 2) {
1189 // Invalid ring (2 nodes) -> remove
1190 traverser.removeWays(path);
1191 for (WayInPolygon way: path) {
1192 discardedResult.add(way.way);
1193 }
1194 } else {
1195 // Close ring -> add
1196 result.add(ring);
1197 traverser.removeWays(path);
1198 }
1199 break;
1200 }
1201 if (path.contains(nextWay)) {
1202 // Inner loop -> remove
1203 int index = path.indexOf(nextWay);
1204 while (path.size() > index) {
1205 WayInPolygon currentWay = path.get(index);
1206 discardedResult.add(currentWay.way);
1207 traverser.removeWay(currentWay);
1208 path.remove(index);
1209 }
1210 traverser.setStartWay(path.get(index-1));
1211 } else {
1212 path.add(nextWay);
1213 }
1214 }
1215 }
1216
1217 return fixTouchingPolygons(result);
1218 }
1219
1220 /**
1221 * This method checks if polygons have several touching parts and splits them in several polygons.
1222 * @param polygons the polygons to process.
1223 * @return the resulting list of polygons
1224 */
1225 public static List<AssembledPolygon> fixTouchingPolygons(List<AssembledPolygon> polygons) {
1226 List<AssembledPolygon> newPolygons = new ArrayList<>();
1227
1228 for (AssembledPolygon ring : polygons) {
1229 ring.reverse();
1230 WayTraverser traverser = new WayTraverser(ring.ways);
1231 WayInPolygon startWay;
1232
1233 while ((startWay = traverser.startNewWay()) != null) {
1234 List<WayInPolygon> simpleRingWays = new ArrayList<>();
1235 simpleRingWays.add(startWay);
1236 WayInPolygon nextWay;
1237 while ((nextWay = traverser.walk()) != startWay) {
1238 if (nextWay == null)
1239 throw new JosmRuntimeException("Join areas internal error.");
1240 simpleRingWays.add(nextWay);
1241 }
1242 traverser.removeWays(simpleRingWays);
1243 AssembledPolygon simpleRing = new AssembledPolygon(simpleRingWays);
1244 simpleRing.reverse();
1245 newPolygons.add(simpleRing);
1246 }
1247 }
1248
1249 return newPolygons;
1250 }
1251
1252 /**
1253 * Tests if way is inside other way
1254 * @param outside outer polygon description
1255 * @param inside inner polygon description
1256 * @return {@code true} if inner is inside outer
1257 */
1258 public static boolean wayInsideWay(AssembledPolygon inside, AssembledPolygon outside) {
1259 Set<Node> outsideNodes = new HashSet<>(outside.getNodes());
1260 List<Node> insideNodes = inside.getNodes();
1261
1262 for (Node insideNode : insideNodes) {
1263
1264 if (!outsideNodes.contains(insideNode))
1265 //simply test the one node
1266 return Geometry.nodeInsidePolygon(insideNode, outside.getNodes());
1267 }
1268
1269 //all nodes shared.
1270 return false;
1271 }
1272
1273 /**
1274 * Joins the lists of ways.
1275 * @param polygon The list of outer ways that belong to that multipolygon.
1276 * @return The newly created outer way
1277 * @throws UserCancelException if user cancels the operation
1278 */
1279 private Multipolygon joinPolygon(AssembledMultipolygon polygon) throws UserCancelException {
1280 Multipolygon result = new Multipolygon(joinWays(polygon.outerWay.ways));
1281
1282 for (AssembledPolygon pol : polygon.innerWays) {
1283 result.innerWays.add(joinWays(pol.ways));
1284 }
1285
1286 return result;
1287 }
1288
1289 /**
1290 * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway.
1291 * @param ways The list of outer ways that belong to that multigon.
1292 * @return The newly created outer way
1293 * @throws UserCancelException if user cancels the operation
1294 */
1295 private Way joinWays(List<WayInPolygon> ways) throws UserCancelException {
1296
1297 //leave original orientation, if all paths are reverse.
1298 boolean allReverse = true;
1299 for (WayInPolygon way : ways) {
1300 allReverse &= !way.insideToTheRight;
1301 }
1302
1303 if (allReverse) {
1304 for (WayInPolygon way : ways) {
1305 way.insideToTheRight = !way.insideToTheRight;
1306 }
1307 }
1308
1309 Way joinedWay = joinOrientedWays(ways);
1310
1311 //should not happen
1312 if (joinedWay == null || !joinedWay.isClosed())
1313 throw new JosmRuntimeException("Join areas internal error.");
1314
1315 return joinedWay;
1316 }
1317
1318 /**
1319 * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath)
1320 * @param ways The list of ways to join and reverse
1321 * @return The newly created way
1322 * @throws UserCancelException if user cancels the operation
1323 */
1324 private Way joinOrientedWays(List<WayInPolygon> ways) throws UserCancelException {
1325 if (ways.size() < 2)
1326 return ways.get(0).way;
1327
1328 // This will turn ways so all of them point in the same direction and CombineAction won't bug
1329 // the user about this.
1330
1331 //TODO: ReverseWay and Combine way are really slow and we use them a lot here. This slows down large joins.
1332 List<Way> actionWays = new ArrayList<>(ways.size());
1333
1334 for (WayInPolygon way : ways) {
1335 actionWays.add(way.way);
1336
1337 if (!way.insideToTheRight) {
1338 ReverseWayResult res = ReverseWayAction.reverseWay(way.way);
1339 commitCommand(res.getReverseCommand());
1340 cmdsCount++;
1341 }
1342 }
1343
1344 Pair<Way, Command> result = CombineWayAction.combineWaysWorker(actionWays);
1345
1346 commitCommand(result.b);
1347 cmdsCount++;
1348
1349 return result.a;
1350 }
1351
1352 /**
1353 * This method analyzes multipolygon relationships of given ways and collects addition inner ways to consider.
1354 * @param selectedWays the selected ways
1355 * @return list of polygons, or null if too complex relation encountered.
1356 */
1357 public static List<Multipolygon> collectMultipolygons(Collection<Way> selectedWays) {
1358
1359 List<Multipolygon> result = new ArrayList<>();
1360
1361 //prepare the lists, to minimize memory allocation.
1362 List<Way> outerWays = new ArrayList<>();
1363 List<Way> innerWays = new ArrayList<>();
1364
1365 Set<Way> processedOuterWays = new LinkedHashSet<>();
1366 Set<Way> processedInnerWays = new LinkedHashSet<>();
1367
1368 for (Relation r : OsmPrimitive.getParentRelations(selectedWays)) {
1369 if (r.isDeleted() || !r.isMultipolygon()) {
1370 continue;
1371 }
1372
1373 boolean hasKnownOuter = false;
1374 outerWays.clear();
1375 innerWays.clear();
1376
1377 for (RelationMember rm : r.getMembers()) {
1378 if ("outer".equalsIgnoreCase(rm.getRole())) {
1379 outerWays.add(rm.getWay());
1380 hasKnownOuter |= selectedWays.contains(rm.getWay());
1381 } else if ("inner".equalsIgnoreCase(rm.getRole())) {
1382 innerWays.add(rm.getWay());
1383 }
1384 }
1385
1386 if (!hasKnownOuter) {
1387 continue;
1388 }
1389
1390 if (outerWays.size() > 1) {
1391 new Notification(
1392 tr("Sorry. Cannot handle multipolygon relations with multiple outer ways."))
1393 .setIcon(JOptionPane.INFORMATION_MESSAGE)
1394 .show();
1395 return null;
1396 }
1397
1398 Way outerWay = outerWays.get(0);
1399
1400 //retain only selected inner ways
1401 innerWays.retainAll(selectedWays);
1402
1403 if (processedOuterWays.contains(outerWay)) {
1404 new Notification(
1405 tr("Sorry. Cannot handle way that is outer in multiple multipolygon relations."))
1406 .setIcon(JOptionPane.INFORMATION_MESSAGE)
1407 .show();
1408 return null;
1409 }
1410
1411 if (processedInnerWays.contains(outerWay)) {
1412 new Notification(
1413 tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
1414 .setIcon(JOptionPane.INFORMATION_MESSAGE)
1415 .show();
1416 return null;
1417 }
1418
1419 for (Way way :innerWays) {
1420 if (processedOuterWays.contains(way)) {
1421 new Notification(
1422 tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations."))
1423 .setIcon(JOptionPane.INFORMATION_MESSAGE)
1424 .show();
1425 return null;
1426 }
1427
1428 if (processedInnerWays.contains(way)) {
1429 new Notification(
1430 tr("Sorry. Cannot handle way that is inner in multiple multipolygon relations."))
1431 .setIcon(JOptionPane.INFORMATION_MESSAGE)
1432 .show();
1433 return null;
1434 }
1435 }
1436
1437 processedOuterWays.add(outerWay);
1438 processedInnerWays.addAll(innerWays);
1439
1440 Multipolygon pol = new Multipolygon(outerWay);
1441 pol.innerWays.addAll(innerWays);
1442
1443 result.add(pol);
1444 }
1445
1446 //add remaining ways, not in relations
1447 for (Way way : selectedWays) {
1448 if (processedOuterWays.contains(way) || processedInnerWays.contains(way)) {
1449 continue;
1450 }
1451
1452 result.add(new Multipolygon(way));
1453 }
1454
1455 return result;
1456 }
1457
1458 /**
1459 * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations
1460 * @param inner List of already closed inner ways
1461 * @return The list of relation with roles to add own relation to
1462 */
1463 private RelationRole addOwnMultipolygonRelation(Collection<Way> inner) {
1464 if (inner.isEmpty()) return null;
1465 OsmDataLayer layer = getLayerManager().getEditLayer();
1466 // Create new multipolygon relation and add all inner ways to it
1467 Relation newRel = new Relation();
1468 newRel.put("type", "multipolygon");
1469 for (Way w : inner) {
1470 newRel.addMember(new RelationMember("inner", w));
1471 }
1472 cmds.add(layer != null ? new AddCommand(layer.data, newRel) :
1473 new AddCommand(inner.iterator().next().getDataSet(), newRel));
1474 addedRelations.add(newRel);
1475
1476 // We don't add outer to the relation because it will be handed to fixRelations()
1477 // which will then do the remaining work.
1478 return new RelationRole(newRel, "outer");
1479 }
1480
1481 /**
1482 * Removes a given OsmPrimitive from all relations.
1483 * @param osm Element to remove from all relations
1484 * @return List of relations with roles the primitives was part of
1485 */
1486 private List<RelationRole> removeFromAllRelations(OsmPrimitive osm) {
1487 List<RelationRole> result = new ArrayList<>();
1488
1489 for (Relation r : osm.getDataSet().getRelations()) {
1490 if (r.isDeleted()) {
1491 continue;
1492 }
1493 for (RelationMember rm : r.getMembers()) {
1494 if (rm.getMember() != osm) {
1495 continue;
1496 }
1497
1498 Relation newRel = new Relation(r);
1499 List<RelationMember> members = newRel.getMembers();
1500 members.remove(rm);
1501 newRel.setMembers(members);
1502
1503 cmds.add(new ChangeCommand(r, newRel));
1504 RelationRole saverel = new RelationRole(r, rm.getRole());
1505 if (!result.contains(saverel)) {
1506 result.add(saverel);
1507 }
1508 break;
1509 }
1510 }
1511
1512 commitCommands(marktr("Removed Element from Relations"));
1513 return result;
1514 }
1515
1516 /**
1517 * Adds the previously removed relations again to the outer way. If there are multiple multipolygon
1518 * relations where the joined areas were in "outer" role a new relation is created instead with all
1519 * members of both. This function depends on multigon relations to be valid already, it won't fix them.
1520 * @param rels List of relations with roles the (original) ways were part of
1521 * @param outer The newly created outer area/way
1522 * @param ownMultipol elements to directly add as outer
1523 * @param relationsToDelete set of relations to delete.
1524 */
1525 private void fixRelations(List<RelationRole> rels, Way outer, RelationRole ownMultipol, Set<Relation> relationsToDelete) {
1526 List<RelationRole> multiouters = new ArrayList<>();
1527
1528 if (ownMultipol != null) {
1529 multiouters.add(ownMultipol);
1530 }
1531
1532 for (RelationRole r : rels) {
1533 if (r.rel.isMultipolygon() && "outer".equalsIgnoreCase(r.role)) {
1534 multiouters.add(r);
1535 continue;
1536 }
1537 // Add it back!
1538 Relation newRel = new Relation(r.rel);
1539 newRel.addMember(new RelationMember(r.role, outer));
1540 cmds.add(new ChangeCommand(r.rel, newRel));
1541 }
1542
1543 Relation newRel;
1544 RelationRole soleOuter;
1545 switch (multiouters.size()) {
1546 case 0:
1547 return;
1548 case 1:
1549 // Found only one to be part of a multipolygon relation, so just add it back as well
1550 soleOuter = multiouters.get(0);
1551 newRel = new Relation(soleOuter.rel);
1552 newRel.addMember(new RelationMember(soleOuter.role, outer));
1553 cmds.add(new ChangeCommand(ds, soleOuter.rel, newRel));
1554 return;
1555 default:
1556 // Create a new relation with all previous members and (Way)outer as outer.
1557 newRel = new Relation();
1558 for (RelationRole r : multiouters) {
1559 // Add members
1560 for (RelationMember rm : r.rel.getMembers()) {
1561 if (!newRel.getMembers().contains(rm)) {
1562 newRel.addMember(rm);
1563 }
1564 }
1565 // Add tags
1566 for (String key : r.rel.keySet()) {
1567 newRel.put(key, r.rel.get(key));
1568 }
1569 // Delete old relation
1570 relationsToDelete.add(r.rel);
1571 }
1572 newRel.addMember(new RelationMember("outer", outer));
1573 cmds.add(new AddCommand(ds, newRel));
1574 }
1575 }
1576
1577 /**
1578 * Remove all tags from the all the way
1579 * @param ways The List of Ways to remove all tags from
1580 */
1581 private void stripTags(Collection<Way> ways) {
1582 for (Way w : ways) {
1583 final Way wayWithoutTags = new Way(w);
1584 wayWithoutTags.removeAll();
1585 cmds.add(new ChangeCommand(w, wayWithoutTags));
1586 }
1587 /* I18N: current action printed in status display */
1588 commitCommands(marktr("Remove tags from inner ways"));
1589 }
1590
1591 /**
1592 * Takes the last cmdsCount actions back and combines them into a single action
1593 * (for when the user wants to undo the join action)
1594 * @param message The commit message to display
1595 */
1596 private void makeCommitsOneAction(String message) {
1597 cmds.clear();
1598 if (Main.main != null) {
1599 UndoRedoHandler ur = MainApplication.undoRedo;
1600 int i = Math.max(ur.commands.size() - cmdsCount, 0);
1601 for (; i < ur.commands.size(); i++) {
1602 cmds.add(ur.commands.get(i));
1603 }
1604
1605 for (i = 0; i < cmds.size(); i++) {
1606 ur.undo();
1607 }
1608 }
1609
1610 commitCommands(message == null ? marktr("Join Areas Function") : message);
1611 cmdsCount = 0;
1612 }
1613
1614 @Override
1615 protected void updateEnabledState() {
1616 updateEnabledStateOnCurrentSelection();
1617 }
1618
1619 @Override
1620 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
1621 setEnabled(selection != null && !selection.isEmpty());
1622 }
1623}
Note: See TracBrowser for help on using the repository browser.