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