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