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