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

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

PMD - Strict Exceptions

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