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