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

Last change on this file was 19050, checked in by taylor.smock, 25 hours ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

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