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