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 778 */ 779 private 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 RuntimeException("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 = null; // the node at junction 821 Node prevNode = null; // last node from previous path 822 wayClockwise = false; 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(getEditLayer(), way, chunks, 959 Collections.<OsmPrimitive>emptyList(), SplitWayAction.Strategy.keepFirstChunk()); 960 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 } else { 968 //nothing to split 969 result.add(way); 970 } 971 972 return result; 973 } 974 975 /** 976 * Simple chunking version. Does not care about circular ways and result being 977 * proper, we will glue it all back together later on. 978 * @param way the way to chunk 979 * @param splitNodes the places where to cut. 980 * @return list of node paths to produce. 981 */ 982 private static List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) { 983 List<List<Node>> result = new ArrayList<>(); 984 List<Node> curList = new ArrayList<>(); 985 986 for (Node node : way.getNodes()) { 987 curList.add(node); 988 if (curList.size() > 1 && splitNodes.contains(node)) { 989 result.add(curList); 990 curList = new ArrayList<>(); 991 curList.add(node); 992 } 993 } 994 995 if (curList.size() > 1) { 996 result.add(curList); 997 } 998 999 return result; 1000 } 1001 1002 /** 1003 * This method finds which ways are outer and which are inner. 1004 * @param boundaries list of joined boundaries to search in 1005 * @return outer ways 1006 */ 1007 private List<AssembledMultipolygon> findPolygons(Collection<AssembledPolygon> boundaries) { 1008 1009 List<PolygonLevel> list = findOuterWaysImpl(0, boundaries); 1010 List<AssembledMultipolygon> result = new ArrayList<>(); 1011 1012 //take every other level 1013 for (PolygonLevel pol : list) { 1014 if (pol.level % 2 == 0) { 1015 result.add(pol.pol); 1016 } 1017 } 1018 1019 return result; 1020 } 1021 1022 /** 1023 * Collects outer way and corresponding inner ways from all boundaries. 1024 * @param level depth level 1025 * @param boundaryWays list of joined boundaries to search in 1026 * @return the outermost Way. 1027 */ 1028 private List<PolygonLevel> findOuterWaysImpl(int level, Collection<AssembledPolygon> boundaryWays) { 1029 1030 //TODO: bad performance for deep nestings... 1031 List<PolygonLevel> result = new ArrayList<>(); 1032 1033 for (AssembledPolygon outerWay : boundaryWays) { 1034 1035 boolean outerGood = true; 1036 List<AssembledPolygon> innerCandidates = new ArrayList<>(); 1037 1038 for (AssembledPolygon innerWay : boundaryWays) { 1039 if (innerWay == outerWay) { 1040 continue; 1041 } 1042 1043 if (wayInsideWay(outerWay, innerWay)) { 1044 outerGood = false; 1045 break; 1046 } else if (wayInsideWay(innerWay, outerWay)) { 1047 innerCandidates.add(innerWay); 1048 } 1049 } 1050 1051 if (!outerGood) { 1052 continue; 1053 } 1054 1055 //add new outer polygon 1056 AssembledMultipolygon pol = new AssembledMultipolygon(outerWay); 1057 PolygonLevel polLev = new PolygonLevel(pol, level); 1058 1059 //process inner ways 1060 if (!innerCandidates.isEmpty()) { 1061 List<PolygonLevel> innerList = findOuterWaysImpl(level + 1, innerCandidates); 1062 result.addAll(innerList); 1063 1064 for (PolygonLevel pl : innerList) { 1065 if (pl.level == level + 1) { 1066 pol.innerWays.add(pl.pol.outerWay); 1067 } 1068 } 1069 } 1070 1071 result.add(polLev); 1072 } 1073 1074 return result; 1075 } 1076 1077 /** 1078 * Finds all ways that form inner or outer boundaries. 1079 * @param multigonWays A list of (splitted) ways that form a multigon and share common end nodes on intersections. 1080 * @param discardedResult this list is filled with ways that are to be discarded 1081 * @return A list of ways that form the outer and inner boundaries of the multigon. 1082 */ 1083 public static List<AssembledPolygon> findBoundaryPolygons(Collection<WayInPolygon> multigonWays, 1084 List<Way> discardedResult) { 1085 //first find all discardable ways, by getting outer shells. 1086 //this will produce incorrect boundaries in some cases, but second pass will fix it. 1087 List<WayInPolygon> discardedWays = new ArrayList<>(); 1088 1089 // In multigonWays collection, some way are just a point (i.e. way like nodeA-nodeA) 1090 // This seems to appear when is apply over invalid way like #9911 test-case 1091 // Remove all of these way to make the next work. 1092 List<WayInPolygon> cleanMultigonWays = new ArrayList<>(); 1093 for (WayInPolygon way: multigonWays) { 1094 if (way.way.getNodesCount() == 2 && way.way.isClosed()) 1095 discardedWays.add(way); 1096 else 1097 cleanMultigonWays.add(way); 1098 } 1099 1100 WayTraverser traverser = new WayTraverser(cleanMultigonWays); 1101 List<AssembledPolygon> result = new ArrayList<>(); 1102 1103 WayInPolygon startWay; 1104 while ((startWay = traverser.startNewWay()) != null) { 1105 List<WayInPolygon> path = new ArrayList<>(); 1106 List<WayInPolygon> startWays = new ArrayList<>(); 1107 path.add(startWay); 1108 while (true) { 1109 WayInPolygon leftComing; 1110 while ((leftComing = traverser.leftComingWay()) != null) { 1111 if (startWays.contains(leftComing)) 1112 break; 1113 // Need restart traverser walk 1114 path.clear(); 1115 path.add(leftComing); 1116 traverser.setStartWay(leftComing); 1117 startWays.add(leftComing); 1118 break; 1119 } 1120 WayInPolygon nextWay = traverser.walk(); 1121 if (nextWay == null) 1122 throw new RuntimeException("Join areas internal error."); 1123 if (path.get(0) == nextWay) { 1124 // path is closed -> stop here 1125 AssembledPolygon ring = new AssembledPolygon(path); 1126 if (ring.getNodes().size() <= 2) { 1127 // Invalid ring (2 nodes) -> remove 1128 traverser.removeWays(path); 1129 for (WayInPolygon way: path) { 1130 discardedResult.add(way.way); 1131 } 1132 } else { 1133 // Close ring -> add 1134 result.add(ring); 1135 traverser.removeWays(path); 1136 } 1137 break; 1138 } 1139 if (path.contains(nextWay)) { 1140 // Inner loop -> remove 1141 int index = path.indexOf(nextWay); 1142 while (path.size() > index) { 1143 WayInPolygon currentWay = path.get(index); 1144 discardedResult.add(currentWay.way); 1145 traverser.removeWay(currentWay); 1146 path.remove(index); 1147 } 1148 traverser.setStartWay(path.get(index-1)); 1149 } else { 1150 path.add(nextWay); 1151 } 1152 } 1153 } 1154 1155 return fixTouchingPolygons(result); 1156 } 1157 1158 /** 1159 * This method checks if polygons have several touching parts and splits them in several polygons. 1160 * @param polygons the polygons to process. 1161 * @return the resulting list of polygons 1162 */ 1163 public static List<AssembledPolygon> fixTouchingPolygons(List<AssembledPolygon> polygons) { 1164 List<AssembledPolygon> newPolygons = new ArrayList<>(); 1165 1166 for (AssembledPolygon ring : polygons) { 1167 ring.reverse(); 1168 WayTraverser traverser = new WayTraverser(ring.ways); 1169 WayInPolygon startWay; 1170 1171 while ((startWay = traverser.startNewWay()) != null) { 1172 List<WayInPolygon> simpleRingWays = new ArrayList<>(); 1173 simpleRingWays.add(startWay); 1174 WayInPolygon nextWay; 1175 while ((nextWay = traverser.walk()) != startWay) { 1176 if (nextWay == null) 1177 throw new RuntimeException("Join areas internal error."); 1178 simpleRingWays.add(nextWay); 1179 } 1180 traverser.removeWays(simpleRingWays); 1181 AssembledPolygon simpleRing = new AssembledPolygon(simpleRingWays); 1182 simpleRing.reverse(); 1183 newPolygons.add(simpleRing); 1184 } 1185 } 1186 1187 return newPolygons; 1188 } 1189 1190 /** 1191 * Tests if way is inside other way 1192 * @param outside outer polygon description 1193 * @param inside inner polygon description 1194 * @return {@code true} if inner is inside outer 1195 */ 1196 public static boolean wayInsideWay(AssembledPolygon inside, AssembledPolygon outside) { 1197 Set<Node> outsideNodes = new HashSet<>(outside.getNodes()); 1198 List<Node> insideNodes = inside.getNodes(); 1199 1200 for (Node insideNode : insideNodes) { 1201 1202 if (!outsideNodes.contains(insideNode)) 1203 //simply test the one node 1204 return Geometry.nodeInsidePolygon(insideNode, outside.getNodes()); 1205 } 1206 1207 //all nodes shared. 1208 return false; 1209 } 1210 1211 /** 1212 * Joins the lists of ways. 1213 * @param polygon The list of outer ways that belong to that multigon. 1214 * @return The newly created outer way 1215 * @throws UserCancelException if user cancels the operation 1216 */ 1217 private Multipolygon joinPolygon(AssembledMultipolygon polygon) throws UserCancelException { 1218 Multipolygon result = new Multipolygon(joinWays(polygon.outerWay.ways)); 1219 1220 for (AssembledPolygon pol : polygon.innerWays) { 1221 result.innerWays.add(joinWays(pol.ways)); 1222 } 1223 1224 return result; 1225 } 1226 1227 /** 1228 * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway. 1229 * @param ways The list of outer ways that belong to that multigon. 1230 * @return The newly created outer way 1231 * @throws UserCancelException if user cancels the operation 1232 */ 1233 private Way joinWays(List<WayInPolygon> ways) throws UserCancelException { 1234 1235 //leave original orientation, if all paths are reverse. 1236 boolean allReverse = true; 1237 for (WayInPolygon way : ways) { 1238 allReverse &= !way.insideToTheRight; 1239 } 1240 1241 if (allReverse) { 1242 for (WayInPolygon way : ways) { 1243 way.insideToTheRight = !way.insideToTheRight; 1244 } 1245 } 1246 1247 Way joinedWay = joinOrientedWays(ways); 1248 1249 //should not happen 1250 if (joinedWay == null || !joinedWay.isClosed()) 1251 throw new RuntimeException("Join areas internal error."); 1252 1253 return joinedWay; 1254 } 1255 1256 /** 1257 * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath) 1258 * @param ways The list of ways to join and reverse 1259 * @return The newly created way 1260 * @throws UserCancelException if user cancels the operation 1261 */ 1262 private Way joinOrientedWays(List<WayInPolygon> ways) throws UserCancelException { 1263 if (ways.size() < 2) 1264 return ways.get(0).way; 1265 1266 // This will turn ways so all of them point in the same direction and CombineAction won't bug 1267 // the user about this. 1268 1269 //TODO: ReverseWay and Combine way are really slow and we use them a lot here. This slows down large joins. 1270 List<Way> actionWays = new ArrayList<>(ways.size()); 1271 1272 for (WayInPolygon way : ways) { 1273 actionWays.add(way.way); 1274 1275 if (!way.insideToTheRight) { 1276 ReverseWayResult res = ReverseWayAction.reverseWay(way.way); 1277 Main.main.undoRedo.add(res.getReverseCommand()); 1278 cmdsCount++; 1279 } 1280 } 1281 1282 Pair<Way, Command> result = CombineWayAction.combineWaysWorker(actionWays); 1283 1284 Main.main.undoRedo.add(result.b); 1285 cmdsCount++; 1286 1287 return result.a; 1288 } 1289 1290 /** 1291 * This method analyzes multipolygon relationships of given ways and collects addition inner ways to consider. 1292 * @param selectedWays the selected ways 1293 * @return list of polygons, or null if too complex relation encountered. 1294 */ 1295 private List<Multipolygon> collectMultipolygons(Collection<Way> selectedWays) { 1296 1297 List<Multipolygon> result = new ArrayList<>(); 1298 1299 //prepare the lists, to minimize memory allocation. 1300 List<Way> outerWays = new ArrayList<>(); 1301 List<Way> innerWays = new ArrayList<>(); 1302 1303 Set<Way> processedOuterWays = new LinkedHashSet<>(); 1304 Set<Way> processedInnerWays = new LinkedHashSet<>(); 1305 1306 for (Relation r : OsmPrimitive.getParentRelations(selectedWays)) { 1307 if (r.isDeleted() || !r.isMultipolygon()) { 1308 continue; 1309 } 1310 1311 boolean hasKnownOuter = false; 1312 outerWays.clear(); 1313 innerWays.clear(); 1314 1315 for (RelationMember rm : r.getMembers()) { 1316 if ("outer".equalsIgnoreCase(rm.getRole())) { 1317 outerWays.add(rm.getWay()); 1318 hasKnownOuter |= selectedWays.contains(rm.getWay()); 1319 } else if ("inner".equalsIgnoreCase(rm.getRole())) { 1320 innerWays.add(rm.getWay()); 1321 } 1322 } 1323 1324 if (!hasKnownOuter) { 1325 continue; 1326 } 1327 1328 if (outerWays.size() > 1) { 1329 new Notification( 1330 tr("Sorry. Cannot handle multipolygon relations with multiple outer ways.")) 1331 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1332 .show(); 1333 return null; 1334 } 1335 1336 Way outerWay = outerWays.get(0); 1337 1338 //retain only selected inner ways 1339 innerWays.retainAll(selectedWays); 1340 1341 if (processedOuterWays.contains(outerWay)) { 1342 new Notification( 1343 tr("Sorry. Cannot handle way that is outer in multiple multipolygon relations.")) 1344 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1345 .show(); 1346 return null; 1347 } 1348 1349 if (processedInnerWays.contains(outerWay)) { 1350 new Notification( 1351 tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations.")) 1352 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1353 .show(); 1354 return null; 1355 } 1356 1357 for (Way way :innerWays) { 1358 if (processedOuterWays.contains(way)) { 1359 new Notification( 1360 tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations.")) 1361 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1362 .show(); 1363 return null; 1364 } 1365 1366 if (processedInnerWays.contains(way)) { 1367 new Notification( 1368 tr("Sorry. Cannot handle way that is inner in multiple multipolygon relations.")) 1369 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1370 .show(); 1371 return null; 1372 } 1373 } 1374 1375 processedOuterWays.add(outerWay); 1376 processedInnerWays.addAll(innerWays); 1377 1378 Multipolygon pol = new Multipolygon(outerWay); 1379 pol.innerWays.addAll(innerWays); 1380 1381 result.add(pol); 1382 } 1383 1384 //add remaining ways, not in relations 1385 for (Way way : selectedWays) { 1386 if (processedOuterWays.contains(way) || processedInnerWays.contains(way)) { 1387 continue; 1388 } 1389 1390 result.add(new Multipolygon(way)); 1391 } 1392 1393 return result; 1394 } 1395 1396 /** 1397 * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations 1398 * @param inner List of already closed inner ways 1399 * @return The list of relation with roles to add own relation to 1400 */ 1401 private RelationRole addOwnMultipolygonRelation(Collection<Way> inner) { 1402 if (inner.isEmpty()) return null; 1403 // Create new multipolygon relation and add all inner ways to it 1404 Relation newRel = new Relation(); 1405 newRel.put("type", "multipolygon"); 1406 for (Way w : inner) { 1407 newRel.addMember(new RelationMember("inner", w)); 1408 } 1409 cmds.add(new AddCommand(newRel)); 1410 addedRelations.add(newRel); 1411 1412 // We don't add outer to the relation because it will be handed to fixRelations() 1413 // which will then do the remaining work. 1414 return new RelationRole(newRel, "outer"); 1415 } 1416 1417 /** 1418 * Removes a given OsmPrimitive from all relations. 1419 * @param osm Element to remove from all relations 1420 * @return List of relations with roles the primitives was part of 1421 */ 1422 private List<RelationRole> removeFromAllRelations(OsmPrimitive osm) { 1423 List<RelationRole> result = new ArrayList<>(); 1424 1425 for (Relation r : Main.main.getCurrentDataSet().getRelations()) { 1426 if (r.isDeleted()) { 1427 continue; 1428 } 1429 for (RelationMember rm : r.getMembers()) { 1430 if (rm.getMember() != osm) { 1431 continue; 1432 } 1433 1434 Relation newRel = new Relation(r); 1435 List<RelationMember> members = newRel.getMembers(); 1436 members.remove(rm); 1437 newRel.setMembers(members); 1438 1439 cmds.add(new ChangeCommand(r, newRel)); 1440 RelationRole saverel = new RelationRole(r, rm.getRole()); 1441 if (!result.contains(saverel)) { 1442 result.add(saverel); 1443 } 1444 break; 1445 } 1446 } 1447 1448 commitCommands(marktr("Removed Element from Relations")); 1449 return result; 1450 } 1451 1452 /** 1453 * Adds the previously removed relations again to the outer way. If there are multiple multipolygon 1454 * relations where the joined areas were in "outer" role a new relation is created instead with all 1455 * members of both. This function depends on multigon relations to be valid already, it won't fix them. 1456 * @param rels List of relations with roles the (original) ways were part of 1457 * @param outer The newly created outer area/way 1458 * @param ownMultipol elements to directly add as outer 1459 * @param relationsToDelete set of relations to delete. 1460 */ 1461 private void fixRelations(List<RelationRole> rels, Way outer, RelationRole ownMultipol, Set<Relation> relationsToDelete) { 1462 List<RelationRole> multiouters = new ArrayList<>(); 1463 1464 if (ownMultipol != null) { 1465 multiouters.add(ownMultipol); 1466 } 1467 1468 for (RelationRole r : rels) { 1469 if (r.rel.isMultipolygon() && "outer".equalsIgnoreCase(r.role)) { 1470 multiouters.add(r); 1471 continue; 1472 } 1473 // Add it back! 1474 Relation newRel = new Relation(r.rel); 1475 newRel.addMember(new RelationMember(r.role, outer)); 1476 cmds.add(new ChangeCommand(r.rel, newRel)); 1477 } 1478 1479 Relation newRel; 1480 switch (multiouters.size()) { 1481 case 0: 1482 return; 1483 case 1: 1484 // Found only one to be part of a multipolygon relation, so just add it back as well 1485 newRel = new Relation(multiouters.get(0).rel); 1486 newRel.addMember(new RelationMember(multiouters.get(0).role, outer)); 1487 cmds.add(new ChangeCommand(multiouters.get(0).rel, newRel)); 1488 return; 1489 default: 1490 // Create a new relation with all previous members and (Way)outer as outer. 1491 newRel = new Relation(); 1492 for (RelationRole r : multiouters) { 1493 // Add members 1494 for (RelationMember rm : r.rel.getMembers()) { 1495 if (!newRel.getMembers().contains(rm)) { 1496 newRel.addMember(rm); 1497 } 1498 } 1499 // Add tags 1500 for (String key : r.rel.keySet()) { 1501 newRel.put(key, r.rel.get(key)); 1502 } 1503 // Delete old relation 1504 relationsToDelete.add(r.rel); 1505 } 1506 newRel.addMember(new RelationMember("outer", outer)); 1507 cmds.add(new AddCommand(newRel)); 1508 } 1509 } 1510 1511 /** 1512 * Remove all tags from the all the way 1513 * @param ways The List of Ways to remove all tags from 1514 */ 1515 private void stripTags(Collection<Way> ways) { 1516 for (Way w : ways) { 1517 final Way wayWithoutTags = new Way(w); 1518 wayWithoutTags.removeAll(); 1519 cmds.add(new ChangeCommand(w, wayWithoutTags)); 1520 } 1521 /* I18N: current action printed in status display */ 1522 commitCommands(marktr("Remove tags from inner ways")); 1523 } 1524 1525 /** 1526 * Takes the last cmdsCount actions back and combines them into a single action 1527 * (for when the user wants to undo the join action) 1528 * @param message The commit message to display 1529 */ 1530 private void makeCommitsOneAction(String message) { 1531 UndoRedoHandler ur = Main.main.undoRedo; 1532 cmds.clear(); 1533 int i = Math.max(ur.commands.size() - cmdsCount, 0); 1534 for (; i < ur.commands.size(); i++) { 1535 cmds.add(ur.commands.get(i)); 1536 } 1537 1538 for (i = 0; i < cmds.size(); i++) { 1539 ur.undo(); 1540 } 1541 1542 commitCommands(message == null ? marktr("Join Areas Function") : message); 1543 cmdsCount = 0; 1544 } 1545 1546 @Override 1547 protected void updateEnabledState() { 1548 if (getCurrentDataSet() == null) { 1549 setEnabled(false); 1550 } else { 1551 updateEnabledState(getCurrentDataSet().getSelected()); 1552 } 1553 } 1554 1555 @Override 1556 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 1557 setEnabled(selection != null && !selection.isEmpty()); 1558 } 1559}