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 * @since 2575 052 */ 053public class JoinAreasAction extends JosmAction { 054 // This will be used to commit commands and unite them into one large command sequence at the end 055 private final LinkedList<Command> cmds = new LinkedList<>(); 056 private int cmdsCount = 0; 057 private final List<Relation> addedRelations = new LinkedList<>(); 058 059 /** 060 * This helper class describes join areas action result. 061 * @author viesturs 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 join(Main.main.getCurrentDataSet().getSelectedWays()); 402 } 403 404 /** 405 * Joins the given ways. 406 * @param ways Ways to join 407 * @since 7534 408 */ 409 public void join(Collection<Way> ways) { 410 addedRelations.clear(); 411 412 if (ways.isEmpty()) { 413 new Notification( 414 tr("Please select at least one closed way that should be joined.")) 415 .setIcon(JOptionPane.INFORMATION_MESSAGE) 416 .show(); 417 return; 418 } 419 420 List<Node> allNodes = new ArrayList<>(); 421 for (Way way : ways) { 422 if (!way.isClosed()) { 423 new Notification( 424 tr("One of the selected ways is not closed and therefore cannot be joined.")) 425 .setIcon(JOptionPane.INFORMATION_MESSAGE) 426 .show(); 427 return; 428 } 429 430 allNodes.addAll(way.getNodes()); 431 } 432 433 // TODO: Only display this warning when nodes outside dataSourceArea are deleted 434 boolean ok = Command.checkAndConfirmOutlyingOperation("joinarea", tr("Join area confirmation"), 435 trn("The selected way has nodes outside of the downloaded data region.", 436 "The selected ways have nodes outside of the downloaded data region.", 437 ways.size()) + "<br/>" 438 + tr("This can lead to nodes being deleted accidentally.") + "<br/>" 439 + tr("Are you really sure to continue?") 440 + tr("Please abort if you are not sure"), 441 tr("The selected area is incomplete. Continue?"), 442 allNodes, null); 443 if(!ok) return; 444 445 //analyze multipolygon relations and collect all areas 446 List<Multipolygon> areas = collectMultipolygons(ways); 447 448 if (areas == null) 449 //too complex multipolygon relations found 450 return; 451 452 if (!testJoin(areas)) { 453 new Notification( 454 tr("No intersection found. Nothing was changed.")) 455 .setIcon(JOptionPane.INFORMATION_MESSAGE) 456 .show(); 457 return; 458 } 459 460 if (!resolveTagConflicts(areas)) 461 return; 462 //user canceled, do nothing. 463 464 try { 465 JoinAreasResult result = joinAreas(areas); 466 467 if (result.hasChanges) { 468 // move tags from ways to newly created relations 469 // TODO: do we need to also move tags for the modified relations? 470 for (Relation r: addedRelations) { 471 cmds.addAll(CreateMultipolygonAction.removeTagsFromWaysIfNeeded(r)); 472 } 473 commitCommands(tr("Move tags from ways to relations")); 474 475 List<Way> allWays = new ArrayList<>(); 476 for (Multipolygon pol : result.polygons) { 477 allWays.add(pol.outerWay); 478 allWays.addAll(pol.innerWays); 479 } 480 DataSet ds = Main.main.getCurrentDataSet(); 481 if (ds != null) { 482 ds.setSelected(allWays); 483 Main.map.mapView.repaint(); 484 } 485 } else { 486 new Notification( 487 tr("No intersection found. Nothing was changed.")) 488 .setIcon(JOptionPane.INFORMATION_MESSAGE) 489 .show(); 490 } 491 } catch (UserCancelException exception) { 492 //revert changes 493 //FIXME: this is dirty hack 494 makeCommitsOneAction(tr("Reverting changes")); 495 Main.main.undoRedo.undo(); 496 Main.main.undoRedo.redoCommands.clear(); 497 } 498 } 499 500 /** 501 * Tests if the areas have some intersections to join. 502 * @param areas Areas to test 503 * @return {@code true} if areas are joinable 504 */ 505 private boolean testJoin(List<Multipolygon> areas) { 506 List<Way> allStartingWays = new ArrayList<>(); 507 508 for (Multipolygon area : areas) { 509 allStartingWays.add(area.outerWay); 510 allStartingWays.addAll(area.innerWays); 511 } 512 513 //find intersection points 514 Set<Node> nodes = Geometry.addIntersections(allStartingWays, true, cmds); 515 return !nodes.isEmpty(); 516 } 517 518 /** 519 * Will join two or more overlapping areas 520 * @param areas list of areas to join 521 * @return new area formed. 522 */ 523 private JoinAreasResult joinAreas(List<Multipolygon> areas) throws UserCancelException { 524 525 JoinAreasResult result = new JoinAreasResult(); 526 result.hasChanges = false; 527 528 List<Way> allStartingWays = new ArrayList<>(); 529 List<Way> innerStartingWays = new ArrayList<>(); 530 List<Way> outerStartingWays = new ArrayList<>(); 531 532 for (Multipolygon area : areas) { 533 outerStartingWays.add(area.outerWay); 534 innerStartingWays.addAll(area.innerWays); 535 } 536 537 allStartingWays.addAll(innerStartingWays); 538 allStartingWays.addAll(outerStartingWays); 539 540 //first remove nodes in the same coordinate 541 boolean removedDuplicates = false; 542 removedDuplicates |= removeDuplicateNodes(allStartingWays); 543 544 if (removedDuplicates) { 545 result.hasChanges = true; 546 commitCommands(marktr("Removed duplicate nodes")); 547 } 548 549 //find intersection points 550 Set<Node> nodes = Geometry.addIntersections(allStartingWays, false, cmds); 551 552 //no intersections, return. 553 if (nodes.isEmpty()) 554 return result; 555 commitCommands(marktr("Added node on all intersections")); 556 557 List<RelationRole> relations = new ArrayList<>(); 558 559 // Remove ways from all relations so ways can be combined/split quietly 560 for (Way way : allStartingWays) { 561 relations.addAll(removeFromAllRelations(way)); 562 } 563 564 // Don't warn now, because it will really look corrupted 565 boolean warnAboutRelations = !relations.isEmpty() && allStartingWays.size() > 1; 566 567 List<WayInPolygon> preparedWays = new ArrayList<>(); 568 569 for (Way way : outerStartingWays) { 570 List<Way> splitWays = splitWayOnNodes(way, nodes); 571 preparedWays.addAll(markWayInsideSide(splitWays, false)); 572 } 573 574 for (Way way : innerStartingWays) { 575 List<Way> splitWays = splitWayOnNodes(way, nodes); 576 preparedWays.addAll(markWayInsideSide(splitWays, true)); 577 } 578 579 // Find boundary ways 580 List<Way> discardedWays = new ArrayList<>(); 581 List<AssembledPolygon> bounadries = findBoundaryPolygons(preparedWays, discardedWays); 582 583 //find polygons 584 List<AssembledMultipolygon> preparedPolygons = findPolygons(bounadries); 585 586 587 //assemble final polygons 588 List<Multipolygon> polygons = new ArrayList<>(); 589 Set<Relation> relationsToDelete = new LinkedHashSet<>(); 590 591 for (AssembledMultipolygon pol : preparedPolygons) { 592 593 //create the new ways 594 Multipolygon resultPol = joinPolygon(pol); 595 596 //create multipolygon relation, if necessary. 597 RelationRole ownMultipolygonRelation = addOwnMultigonRelation(resultPol.innerWays, resultPol.outerWay); 598 599 //add back the original relations, merged with our new multipolygon relation 600 fixRelations(relations, resultPol.outerWay, ownMultipolygonRelation, relationsToDelete); 601 602 //strip tags from inner ways 603 //TODO: preserve tags on existing inner ways 604 stripTags(resultPol.innerWays); 605 606 polygons.add(resultPol); 607 } 608 609 commitCommands(marktr("Assemble new polygons")); 610 611 for(Relation rel: relationsToDelete) { 612 cmds.add(new DeleteCommand(rel)); 613 } 614 615 commitCommands(marktr("Delete relations")); 616 617 // Delete the discarded inner ways 618 if (!discardedWays.isEmpty()) { 619 Command deleteCmd = DeleteCommand.delete(Main.main.getEditLayer(), discardedWays, true); 620 if (deleteCmd != null) { 621 cmds.add(deleteCmd); 622 commitCommands(marktr("Delete Ways that are not part of an inner multipolygon")); 623 } 624 } 625 626 makeCommitsOneAction(marktr("Joined overlapping areas")); 627 628 if (warnAboutRelations) { 629 new Notification( 630 tr("Some of the ways were part of relations that have been modified.<br>Please verify no errors have been introduced.")) 631 .setIcon(JOptionPane.INFORMATION_MESSAGE) 632 .setDuration(Notification.TIME_LONG) 633 .show(); 634 } 635 636 result.hasChanges = true; 637 result.polygons = polygons; 638 return result; 639 } 640 641 /** 642 * Checks if tags of two given ways differ, and presents the user a dialog to solve conflicts 643 * @param polygons ways to check 644 * @return {@code true} if all conflicts are resolved, {@code false} if conflicts remain. 645 */ 646 private boolean resolveTagConflicts(List<Multipolygon> polygons) { 647 648 List<Way> ways = new ArrayList<>(); 649 650 for (Multipolygon pol : polygons) { 651 ways.add(pol.outerWay); 652 ways.addAll(pol.innerWays); 653 } 654 655 if (ways.size() < 2) { 656 return true; 657 } 658 659 TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways); 660 try { 661 cmds.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(wayTags, ways, ways)); 662 commitCommands(marktr("Fix tag conflicts")); 663 return true; 664 } catch (UserCancelException ex) { 665 return false; 666 } 667 } 668 669 /** 670 * This method removes duplicate points (if any) from the input way. 671 * @param ways the ways to process 672 * @return {@code true} if any changes where made 673 */ 674 private boolean removeDuplicateNodes(List<Way> ways) { 675 //TODO: maybe join nodes with JoinNodesAction, rather than reconnect the ways. 676 677 Map<Node, Node> nodeMap = new TreeMap<>(new NodePositionComparator()); 678 int totalNodesRemoved = 0; 679 680 for (Way way : ways) { 681 if (way.getNodes().size() < 2) { 682 continue; 683 } 684 685 int nodesRemoved = 0; 686 List<Node> newNodes = new ArrayList<>(); 687 Node prevNode = null; 688 689 for (Node node : way.getNodes()) { 690 if (!nodeMap.containsKey(node)) { 691 //new node 692 nodeMap.put(node, node); 693 694 //avoid duplicate nodes 695 if (prevNode != node) { 696 newNodes.add(node); 697 } else { 698 nodesRemoved ++; 699 } 700 } else { 701 //node with same coordinates already exists, substitute with existing node 702 Node representator = nodeMap.get(node); 703 704 if (representator != node) { 705 nodesRemoved ++; 706 } 707 708 //avoid duplicate node 709 if (prevNode != representator) { 710 newNodes.add(representator); 711 } 712 } 713 prevNode = node; 714 } 715 716 if (nodesRemoved > 0) { 717 718 if (newNodes.size() == 1) { //all nodes in the same coordinate - add one more node, to have closed way. 719 newNodes.add(newNodes.get(0)); 720 } 721 722 Way newWay=new Way(way); 723 newWay.setNodes(newNodes); 724 cmds.add(new ChangeCommand(way, newWay)); 725 totalNodesRemoved += nodesRemoved; 726 } 727 } 728 729 return totalNodesRemoved > 0; 730 } 731 732 /** 733 * Commits the command list with a description 734 * @param description The description of what the commands do 735 */ 736 private void commitCommands(String description) { 737 switch(cmds.size()) { 738 case 0: 739 return; 740 case 1: 741 Main.main.undoRedo.add(cmds.getFirst()); 742 break; 743 default: 744 Command c = new SequenceCommand(tr(description), cmds); 745 Main.main.undoRedo.add(c); 746 break; 747 } 748 749 cmds.clear(); 750 cmdsCount++; 751 } 752 753 /** 754 * This method analyzes the way and assigns each part what direction polygon "inside" is. 755 * @param parts the split parts of the way 756 * @param isInner - if true, reverts the direction (for multipolygon islands) 757 * @return list of parts, marked with the inside orientation. 758 */ 759 private List<WayInPolygon> markWayInsideSide(List<Way> parts, boolean isInner) { 760 761 List<WayInPolygon> result = new ArrayList<>(); 762 763 //prepare prev and next maps 764 Map<Way, Way> nextWayMap = new HashMap<>(); 765 Map<Way, Way> prevWayMap = new HashMap<>(); 766 767 for (int pos = 0; pos < parts.size(); pos ++) { 768 769 if (!parts.get(pos).lastNode().equals(parts.get((pos + 1) % parts.size()).firstNode())) 770 throw new RuntimeException("Way not circular"); 771 772 nextWayMap.put(parts.get(pos), parts.get((pos + 1) % parts.size())); 773 prevWayMap.put(parts.get(pos), parts.get((pos + parts.size() - 1) % parts.size())); 774 } 775 776 //find the node with minimum y - it's guaranteed to be outer. (What about the south pole?) 777 Way topWay = null; 778 Node topNode = null; 779 int topIndex = 0; 780 double minY = Double.POSITIVE_INFINITY; 781 782 for (Way way : parts) { 783 for (int pos = 0; pos < way.getNodesCount(); pos ++) { 784 Node node = way.getNode(pos); 785 786 if (node.getEastNorth().getY() < minY) { 787 minY = node.getEastNorth().getY(); 788 topWay = way; 789 topNode = node; 790 topIndex = pos; 791 } 792 } 793 } 794 795 //get the upper way and it's orientation. 796 797 boolean wayClockwise; // orientation of the top way. 798 799 if (topNode.equals(topWay.firstNode()) || topNode.equals(topWay.lastNode())) { 800 Node headNode = null; // the node at junction 801 Node prevNode = null; // last node from previous path 802 wayClockwise = false; 803 804 //node is in split point - find the outermost way from this point 805 806 headNode = topNode; 807 //make a fake node that is downwards from head node (smaller Y). It will be a division point between paths. 808 prevNode = new Node(new EastNorth(headNode.getEastNorth().getX(), headNode.getEastNorth().getY() - 1e5)); 809 810 topWay = null; 811 wayClockwise = false; 812 Node bestWayNextNode = null; 813 814 for (Way way : parts) { 815 if (way.firstNode().equals(headNode)) { 816 Node nextNode = way.getNode(1); 817 818 if (topWay == null || !Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) { 819 //the new way is better 820 topWay = way; 821 wayClockwise = true; 822 bestWayNextNode = nextNode; 823 } 824 } 825 826 if (way.lastNode().equals(headNode)) { 827 //end adjacent to headNode 828 Node nextNode = way.getNode(way.getNodesCount() - 2); 829 830 if (topWay == null || !Geometry.isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) { 831 //the new way is better 832 topWay = way; 833 wayClockwise = false; 834 bestWayNextNode = nextNode; 835 } 836 } 837 } 838 } else { 839 //node is inside way - pick the clockwise going end. 840 Node prev = topWay.getNode(topIndex - 1); 841 Node next = topWay.getNode(topIndex + 1); 842 843 //there will be no parallel segments in the middle of way, so all fine. 844 wayClockwise = Geometry.angleIsClockwise(prev, topNode, next); 845 } 846 847 Way curWay = topWay; 848 boolean curWayInsideToTheRight = wayClockwise ^ isInner; 849 850 //iterate till full circle is reached 851 while (true) { 852 853 //add cur way 854 WayInPolygon resultWay = new WayInPolygon(curWay, curWayInsideToTheRight); 855 result.add(resultWay); 856 857 //process next way 858 Way nextWay = nextWayMap.get(curWay); 859 Node prevNode = curWay.getNode(curWay.getNodesCount() - 2); 860 Node headNode = curWay.lastNode(); 861 Node nextNode = nextWay.getNode(1); 862 863 if (nextWay == topWay) { 864 //full loop traversed - all done. 865 break; 866 } 867 868 //find intersecting segments 869 // the intersections will look like this: 870 // 871 // ^ 872 // | 873 // X wayBNode 874 // | 875 // wayB | 876 // | 877 // curWay | nextWay 878 //----X----------------->X----------------------X----> 879 // prevNode ^headNode nextNode 880 // | 881 // | 882 // wayA | 883 // | 884 // X wayANode 885 // | 886 887 int intersectionCount = 0; 888 889 for (Way wayA : parts) { 890 891 if (wayA == curWay) { 892 continue; 893 } 894 895 if (wayA.lastNode().equals(headNode)) { 896 897 Way wayB = nextWayMap.get(wayA); 898 899 //test if wayA is opposite wayB relative to curWay and nextWay 900 901 Node wayANode = wayA.getNode(wayA.getNodesCount() - 2); 902 Node wayBNode = wayB.getNode(1); 903 904 boolean wayAToTheRight = Geometry.isToTheRightSideOfLine(prevNode, headNode, nextNode, wayANode); 905 boolean wayBToTheRight = Geometry.isToTheRightSideOfLine(prevNode, headNode, nextNode, wayBNode); 906 907 if (wayAToTheRight != wayBToTheRight) { 908 intersectionCount ++; 909 } 910 } 911 } 912 913 //if odd number of crossings, invert orientation 914 if (intersectionCount % 2 != 0) { 915 curWayInsideToTheRight = !curWayInsideToTheRight; 916 } 917 918 curWay = nextWay; 919 } 920 921 return result; 922 } 923 924 /** 925 * This is a method splits way into smaller parts, using the prepared nodes list as split points. 926 * Uses {@link SplitWayAction#splitWay} for the heavy lifting. 927 * @return list of split ways (or original ways if no splitting is done). 928 */ 929 private List<Way> splitWayOnNodes(Way way, Set<Node> nodes) { 930 931 List<Way> result = new ArrayList<>(); 932 List<List<Node>> chunks = buildNodeChunks(way, nodes); 933 934 if (chunks.size() > 1) { 935 SplitWayResult split = SplitWayAction.splitWay(getEditLayer(), way, chunks, Collections.<OsmPrimitive>emptyList()); 936 937 //execute the command, we need the results 938 cmds.add(split.getCommand()); 939 commitCommands(marktr("Split ways into fragments")); 940 941 result.add(split.getOriginalWay()); 942 result.addAll(split.getNewWays()); 943 } else { 944 //nothing to split 945 result.add(way); 946 } 947 948 return result; 949 } 950 951 /** 952 * Simple chunking version. Does not care about circular ways and result being 953 * proper, we will glue it all back together later on. 954 * @param way the way to chunk 955 * @param splitNodes the places where to cut. 956 * @return list of node paths to produce. 957 */ 958 private List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) { 959 List<List<Node>> result = new ArrayList<>(); 960 List<Node> curList = new ArrayList<>(); 961 962 for (Node node : way.getNodes()) { 963 curList.add(node); 964 if (curList.size() > 1 && splitNodes.contains(node)) { 965 result.add(curList); 966 curList = new ArrayList<>(); 967 curList.add(node); 968 } 969 } 970 971 if (curList.size() > 1) { 972 result.add(curList); 973 } 974 975 return result; 976 } 977 978 /** 979 * This method finds which ways are outer and which are inner. 980 * @param boundaries list of joined boundaries to search in 981 * @return outer ways 982 */ 983 private List<AssembledMultipolygon> findPolygons(Collection<AssembledPolygon> boundaries) { 984 985 List<PolygonLevel> list = findOuterWaysImpl(0, boundaries); 986 List<AssembledMultipolygon> result = new ArrayList<>(); 987 988 //take every other level 989 for (PolygonLevel pol : list) { 990 if (pol.level % 2 == 0) { 991 result.add(pol.pol); 992 } 993 } 994 995 return result; 996 } 997 998 /** 999 * Collects outer way and corresponding inner ways from all boundaries. 1000 * @param level depth level 1001 * @param boundaryWays 1002 * @return the outermostWay. 1003 */ 1004 private List<PolygonLevel> findOuterWaysImpl(int level, Collection<AssembledPolygon> boundaryWays) { 1005 1006 //TODO: bad performance for deep nestings... 1007 List<PolygonLevel> result = new ArrayList<>(); 1008 1009 for (AssembledPolygon outerWay : boundaryWays) { 1010 1011 boolean outerGood = true; 1012 List<AssembledPolygon> innerCandidates = new ArrayList<>(); 1013 1014 for (AssembledPolygon innerWay : boundaryWays) { 1015 if (innerWay == outerWay) { 1016 continue; 1017 } 1018 1019 if (wayInsideWay(outerWay, innerWay)) { 1020 outerGood = false; 1021 break; 1022 } else if (wayInsideWay(innerWay, outerWay)) { 1023 innerCandidates.add(innerWay); 1024 } 1025 } 1026 1027 if (!outerGood) { 1028 continue; 1029 } 1030 1031 //add new outer polygon 1032 AssembledMultipolygon pol = new AssembledMultipolygon(outerWay); 1033 PolygonLevel polLev = new PolygonLevel(pol, level); 1034 1035 //process inner ways 1036 if (!innerCandidates.isEmpty()) { 1037 List<PolygonLevel> innerList = findOuterWaysImpl(level + 1, innerCandidates); 1038 result.addAll(innerList); 1039 1040 for (PolygonLevel pl : innerList) { 1041 if (pl.level == level + 1) { 1042 pol.innerWays.add(pl.pol.outerWay); 1043 } 1044 } 1045 } 1046 1047 result.add(polLev); 1048 } 1049 1050 return result; 1051 } 1052 1053 /** 1054 * Finds all ways that form inner or outer boundaries. 1055 * @param multigonWays A list of (splitted) ways that form a multigon and share common end nodes on intersections. 1056 * @param discardedResult this list is filled with ways that are to be discarded 1057 * @return A list of ways that form the outer and inner boundaries of the multigon. 1058 */ 1059 public static List<AssembledPolygon> findBoundaryPolygons(Collection<WayInPolygon> multigonWays, 1060 List<Way> discardedResult) { 1061 //first find all discardable ways, by getting outer shells. 1062 //this will produce incorrect boundaries in some cases, but second pass will fix it. 1063 List<WayInPolygon> discardedWays = new ArrayList<>(); 1064 1065 // In multigonWays collection, some way are just a point (i.e. way like nodeA-nodeA) 1066 // This seems to appear when is apply over invalid way like #9911 test-case 1067 // Remove all of these way to make the next work. 1068 ArrayList<WayInPolygon> cleanMultigonWays = new ArrayList<>(); 1069 for(WayInPolygon way: multigonWays) 1070 if(way.way.getNodesCount() == 2 && way.way.firstNode() == way.way.lastNode()) 1071 discardedWays.add(way); 1072 else 1073 cleanMultigonWays.add(way); 1074 1075 WayTraverser traverser = new WayTraverser(cleanMultigonWays); 1076 List<AssembledPolygon> result = new ArrayList<>(); 1077 1078 WayInPolygon startWay; 1079 while((startWay = traverser.startNewWay()) != null) { 1080 ArrayList<WayInPolygon> path = new ArrayList<>(); 1081 List<WayInPolygon> startWays = new ArrayList<>(); 1082 path.add(startWay); 1083 while(true) { 1084 WayInPolygon leftComing; 1085 while((leftComing = traverser.leftComingWay()) != null) { 1086 if(startWays.contains(leftComing)) 1087 break; 1088 // Need restart traverser walk 1089 path.clear(); 1090 path.add(leftComing); 1091 traverser.setStartWay(leftComing); 1092 startWays.add(leftComing); 1093 break; 1094 } 1095 WayInPolygon nextWay = traverser.walk(); 1096 if(nextWay == null) 1097 throw new RuntimeException("Join areas internal error."); 1098 if(path.get(0) == nextWay) { 1099 // path is closed -> stop here 1100 AssembledPolygon ring = new AssembledPolygon(path); 1101 if(ring.getNodes().size() <= 2) { 1102 // Invalid ring (2 nodes) -> remove 1103 traverser.removeWays(path); 1104 for(WayInPolygon way: path) 1105 discardedResult.add(way.way); 1106 } else { 1107 // Close ring -> add 1108 result.add(ring); 1109 traverser.removeWays(path); 1110 } 1111 break; 1112 } 1113 if(path.contains(nextWay)) { 1114 // Inner loop -> remove 1115 int index = path.indexOf(nextWay); 1116 while(path.size() > index) { 1117 WayInPolygon currentWay = path.get(index); 1118 discardedResult.add(currentWay.way); 1119 traverser.removeWay(currentWay); 1120 path.remove(index); 1121 } 1122 traverser.setStartWay(path.get(index-1)); 1123 } else { 1124 path.add(nextWay); 1125 } 1126 } 1127 } 1128 1129 return fixTouchingPolygons(result); 1130 } 1131 1132 /** 1133 * This method checks if polygons have several touching parts and splits them in several polygons. 1134 * @param polygons the polygons to process. 1135 */ 1136 public static List<AssembledPolygon> fixTouchingPolygons(List<AssembledPolygon> polygons) { 1137 List<AssembledPolygon> newPolygons = new ArrayList<>(); 1138 1139 for (AssembledPolygon ring : polygons) { 1140 ring.reverse(); 1141 WayTraverser traverser = new WayTraverser(ring.ways); 1142 WayInPolygon startWay; 1143 1144 while((startWay = traverser.startNewWay()) != null) { 1145 List<WayInPolygon> simpleRingWays = new ArrayList<>(); 1146 simpleRingWays.add(startWay); 1147 WayInPolygon nextWay; 1148 while((nextWay = traverser.walk()) != startWay) { 1149 if(nextWay == null) 1150 throw new RuntimeException("Join areas internal error."); 1151 simpleRingWays.add(nextWay); 1152 } 1153 traverser.removeWays(simpleRingWays); 1154 AssembledPolygon simpleRing = new AssembledPolygon(simpleRingWays); 1155 simpleRing.reverse(); 1156 newPolygons.add(simpleRing); 1157 } 1158 } 1159 1160 return newPolygons; 1161 } 1162 1163 /** 1164 * Tests if way is inside other way 1165 * @param outside outer polygon description 1166 * @param inside inner polygon description 1167 * @return {@code true} if inner is inside outer 1168 */ 1169 public static boolean wayInsideWay(AssembledPolygon inside, AssembledPolygon outside) { 1170 Set<Node> outsideNodes = new HashSet<>(outside.getNodes()); 1171 List<Node> insideNodes = inside.getNodes(); 1172 1173 for (Node insideNode : insideNodes) { 1174 1175 if (!outsideNodes.contains(insideNode)) 1176 //simply test the one node 1177 return Geometry.nodeInsidePolygon(insideNode, outside.getNodes()); 1178 } 1179 1180 //all nodes shared. 1181 return false; 1182 } 1183 1184 /** 1185 * Joins the lists of ways. 1186 * @param polygon The list of outer ways that belong to that multigon. 1187 * @return The newly created outer way 1188 */ 1189 private Multipolygon joinPolygon(AssembledMultipolygon polygon) throws UserCancelException { 1190 Multipolygon result = new Multipolygon(joinWays(polygon.outerWay.ways)); 1191 1192 for (AssembledPolygon pol : polygon.innerWays) { 1193 result.innerWays.add(joinWays(pol.ways)); 1194 } 1195 1196 return result; 1197 } 1198 1199 /** 1200 * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway. 1201 * @param ways The list of outer ways that belong to that multigon. 1202 * @return The newly created outer way 1203 */ 1204 private Way joinWays(List<WayInPolygon> ways) throws UserCancelException { 1205 1206 //leave original orientation, if all paths are reverse. 1207 boolean allReverse = true; 1208 for (WayInPolygon way : ways) { 1209 allReverse &= !way.insideToTheRight; 1210 } 1211 1212 if (allReverse) { 1213 for (WayInPolygon way : ways) { 1214 way.insideToTheRight = !way.insideToTheRight; 1215 } 1216 } 1217 1218 Way joinedWay = joinOrientedWays(ways); 1219 1220 //should not happen 1221 if (joinedWay == null || !joinedWay.isClosed()) 1222 throw new RuntimeException("Join areas internal error."); 1223 1224 return joinedWay; 1225 } 1226 1227 /** 1228 * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath) 1229 * @param ways The list of ways to join and reverse 1230 * @return The newly created way 1231 */ 1232 private Way joinOrientedWays(List<WayInPolygon> ways) throws UserCancelException{ 1233 if (ways.size() < 2) 1234 return ways.get(0).way; 1235 1236 // This will turn ways so all of them point in the same direction and CombineAction won't bug 1237 // the user about this. 1238 1239 //TODO: ReverseWay and Combine way are really slow and we use them a lot here. This slows down large joins. 1240 List<Way> actionWays = new ArrayList<>(ways.size()); 1241 1242 for (WayInPolygon way : ways) { 1243 actionWays.add(way.way); 1244 1245 if (!way.insideToTheRight) { 1246 ReverseWayResult res = ReverseWayAction.reverseWay(way.way); 1247 Main.main.undoRedo.add(res.getReverseCommand()); 1248 cmdsCount++; 1249 } 1250 } 1251 1252 Pair<Way, Command> result = CombineWayAction.combineWaysWorker(actionWays); 1253 1254 Main.main.undoRedo.add(result.b); 1255 cmdsCount ++; 1256 1257 return result.a; 1258 } 1259 1260 /** 1261 * This method analyzes multipolygon relationships of given ways and collects addition inner ways to consider. 1262 * @param selectedWays the selected ways 1263 * @return list of polygons, or null if too complex relation encountered. 1264 */ 1265 private List<Multipolygon> collectMultipolygons(Collection<Way> selectedWays) { 1266 1267 List<Multipolygon> result = new ArrayList<>(); 1268 1269 //prepare the lists, to minimize memory allocation. 1270 List<Way> outerWays = new ArrayList<>(); 1271 List<Way> innerWays = new ArrayList<>(); 1272 1273 Set<Way> processedOuterWays = new LinkedHashSet<>(); 1274 Set<Way> processedInnerWays = new LinkedHashSet<>(); 1275 1276 for (Relation r : OsmPrimitive.getParentRelations(selectedWays)) { 1277 if (r.isDeleted() || !r.isMultipolygon()) { 1278 continue; 1279 } 1280 1281 boolean hasKnownOuter = false; 1282 outerWays.clear(); 1283 innerWays.clear(); 1284 1285 for (RelationMember rm : r.getMembers()) { 1286 if ("outer".equalsIgnoreCase(rm.getRole())) { 1287 outerWays.add(rm.getWay()); 1288 hasKnownOuter |= selectedWays.contains(rm.getWay()); 1289 } 1290 else if ("inner".equalsIgnoreCase(rm.getRole())) { 1291 innerWays.add(rm.getWay()); 1292 } 1293 } 1294 1295 if (!hasKnownOuter) { 1296 continue; 1297 } 1298 1299 if (outerWays.size() > 1) { 1300 new Notification( 1301 tr("Sorry. Cannot handle multipolygon relations with multiple outer ways.")) 1302 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1303 .show(); 1304 return null; 1305 } 1306 1307 Way outerWay = outerWays.get(0); 1308 1309 //retain only selected inner ways 1310 innerWays.retainAll(selectedWays); 1311 1312 if (processedOuterWays.contains(outerWay)) { 1313 new Notification( 1314 tr("Sorry. Cannot handle way that is outer in multiple multipolygon relations.")) 1315 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1316 .show(); 1317 return null; 1318 } 1319 1320 if (processedInnerWays.contains(outerWay)) { 1321 new Notification( 1322 tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations.")) 1323 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1324 .show(); 1325 return null; 1326 } 1327 1328 for (Way way :innerWays) 1329 { 1330 if (processedOuterWays.contains(way)) { 1331 new Notification( 1332 tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations.")) 1333 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1334 .show(); 1335 return null; 1336 } 1337 1338 if (processedInnerWays.contains(way)) { 1339 new Notification( 1340 tr("Sorry. Cannot handle way that is inner in multiple multipolygon relations.")) 1341 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1342 .show(); 1343 return null; 1344 } 1345 } 1346 1347 processedOuterWays.add(outerWay); 1348 processedInnerWays.addAll(innerWays); 1349 1350 Multipolygon pol = new Multipolygon(outerWay); 1351 pol.innerWays.addAll(innerWays); 1352 1353 result.add(pol); 1354 } 1355 1356 //add remaining ways, not in relations 1357 for (Way way : selectedWays) { 1358 if (processedOuterWays.contains(way) || processedInnerWays.contains(way)) { 1359 continue; 1360 } 1361 1362 result.add(new Multipolygon(way)); 1363 } 1364 1365 return result; 1366 } 1367 1368 /** 1369 * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations 1370 * @param inner List of already closed inner ways 1371 * @param outer The outer way 1372 * @return The list of relation with roles to add own relation to 1373 */ 1374 private RelationRole addOwnMultigonRelation(Collection<Way> inner, Way outer) { 1375 if (inner.isEmpty()) return null; 1376 // Create new multipolygon relation and add all inner ways to it 1377 Relation newRel = new Relation(); 1378 newRel.put("type", "multipolygon"); 1379 for (Way w : inner) { 1380 newRel.addMember(new RelationMember("inner", w)); 1381 } 1382 cmds.add(new AddCommand(newRel)); 1383 addedRelations.add(newRel); 1384 1385 // We don't add outer to the relation because it will be handed to fixRelations() 1386 // which will then do the remaining work. 1387 return new RelationRole(newRel, "outer"); 1388 } 1389 1390 /** 1391 * Removes a given OsmPrimitive from all relations. 1392 * @param osm Element to remove from all relations 1393 * @return List of relations with roles the primitives was part of 1394 */ 1395 private List<RelationRole> removeFromAllRelations(OsmPrimitive osm) { 1396 List<RelationRole> result = new ArrayList<>(); 1397 1398 for (Relation r : Main.main.getCurrentDataSet().getRelations()) { 1399 if (r.isDeleted()) { 1400 continue; 1401 } 1402 for (RelationMember rm : r.getMembers()) { 1403 if (rm.getMember() != osm) { 1404 continue; 1405 } 1406 1407 Relation newRel = new Relation(r); 1408 List<RelationMember> members = newRel.getMembers(); 1409 members.remove(rm); 1410 newRel.setMembers(members); 1411 1412 cmds.add(new ChangeCommand(r, newRel)); 1413 RelationRole saverel = new RelationRole(r, rm.getRole()); 1414 if (!result.contains(saverel)) { 1415 result.add(saverel); 1416 } 1417 break; 1418 } 1419 } 1420 1421 commitCommands(marktr("Removed Element from Relations")); 1422 return result; 1423 } 1424 1425 /** 1426 * Adds the previously removed relations again to the outer way. If there are multiple multipolygon 1427 * relations where the joined areas were in "outer" role a new relation is created instead with all 1428 * members of both. This function depends on multigon relations to be valid already, it won't fix them. 1429 * @param rels List of relations with roles the (original) ways were part of 1430 * @param outer The newly created outer area/way 1431 * @param ownMultipol elements to directly add as outer 1432 * @param relationsToDelete set of relations to delete. 1433 */ 1434 private void fixRelations(List<RelationRole> rels, Way outer, RelationRole ownMultipol, Set<Relation> relationsToDelete) { 1435 List<RelationRole> multiouters = new ArrayList<>(); 1436 1437 if (ownMultipol != null) { 1438 multiouters.add(ownMultipol); 1439 } 1440 1441 for (RelationRole r : rels) { 1442 if (r.rel.isMultipolygon() && "outer".equalsIgnoreCase(r.role)) { 1443 multiouters.add(r); 1444 continue; 1445 } 1446 // Add it back! 1447 Relation newRel = new Relation(r.rel); 1448 newRel.addMember(new RelationMember(r.role, outer)); 1449 cmds.add(new ChangeCommand(r.rel, newRel)); 1450 } 1451 1452 Relation newRel; 1453 switch (multiouters.size()) { 1454 case 0: 1455 return; 1456 case 1: 1457 // Found only one to be part of a multipolygon relation, so just add it back as well 1458 newRel = new Relation(multiouters.get(0).rel); 1459 newRel.addMember(new RelationMember(multiouters.get(0).role, outer)); 1460 cmds.add(new ChangeCommand(multiouters.get(0).rel, newRel)); 1461 return; 1462 default: 1463 // Create a new relation with all previous members and (Way)outer as outer. 1464 newRel = new Relation(); 1465 for (RelationRole r : multiouters) { 1466 // Add members 1467 for (RelationMember rm : r.rel.getMembers()) 1468 if (!newRel.getMembers().contains(rm)) { 1469 newRel.addMember(rm); 1470 } 1471 // Add tags 1472 for (String key : r.rel.keySet()) { 1473 newRel.put(key, r.rel.get(key)); 1474 } 1475 // Delete old relation 1476 relationsToDelete.add(r.rel); 1477 } 1478 newRel.addMember(new RelationMember("outer", outer)); 1479 cmds.add(new AddCommand(newRel)); 1480 } 1481 } 1482 1483 /** 1484 * Remove all tags from the all the way 1485 * @param ways The List of Ways to remove all tags from 1486 */ 1487 private void stripTags(Collection<Way> ways) { 1488 for (Way w : ways) { 1489 stripTags(w); 1490 } 1491 /* I18N: current action printed in status display */ 1492 commitCommands(marktr("Remove tags from inner ways")); 1493 } 1494 1495 /** 1496 * Remove all tags from the way 1497 * @param x The Way to remove all tags from 1498 */ 1499 private void stripTags(Way x) { 1500 Way y = new Way(x); 1501 for (String key : x.keySet()) { 1502 y.remove(key); 1503 } 1504 cmds.add(new ChangeCommand(x, y)); 1505 } 1506 1507 /** 1508 * Takes the last cmdsCount actions back and combines them into a single action 1509 * (for when the user wants to undo the join action) 1510 * @param message The commit message to display 1511 */ 1512 private void makeCommitsOneAction(String message) { 1513 UndoRedoHandler ur = Main.main.undoRedo; 1514 cmds.clear(); 1515 int i = Math.max(ur.commands.size() - cmdsCount, 0); 1516 for (; i < ur.commands.size(); i++) { 1517 cmds.add(ur.commands.get(i)); 1518 } 1519 1520 for (i = 0; i < cmds.size(); i++) { 1521 ur.undo(); 1522 } 1523 1524 commitCommands(message == null ? marktr("Join Areas Function") : message); 1525 cmdsCount = 0; 1526 } 1527 1528 @Override 1529 protected void updateEnabledState() { 1530 if (getCurrentDataSet() == null) { 1531 setEnabled(false); 1532 } else { 1533 updateEnabledState(getCurrentDataSet().getSelected()); 1534 } 1535 } 1536 1537 @Override 1538 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 1539 setEnabled(selection != null && !selection.isEmpty()); 1540 } 1541}