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

Last change on this file since 14628 was 14429, checked in by GerdP, 5 years ago

fix #16674 CCE: Relation cannot be cast to Way

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