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

Last change on this file since 17534 was 17374, checked in by GerdP, 3 years ago

see #20167: [patch] Improve code readability by replacing indexed loops with foreach
Patch by gaben, slightly modified
I removed the changes for

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