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