Ticket #14528: join-areas-rewrite.patch
File join-areas-rewrite.patch, 53.5 KB (added by , 7 years ago) |
---|
-
src/org/openstreetmap/josm/actions/JoinAreasAction.java
5 5 import static org.openstreetmap.josm.tools.I18n.tr; 6 6 import static org.openstreetmap.josm.tools.I18n.trn; 7 7 8 import java.awt.GridBagLayout; 8 9 import java.awt.event.ActionEvent; 9 10 import java.awt.event.KeyEvent; 10 11 import java.util.ArrayList; … … 12 13 import java.util.Collections; 13 14 import java.util.Comparator; 14 15 import java.util.EnumSet; 16 import java.util.HashMap; 15 17 import java.util.HashSet; 16 18 import java.util.LinkedHashSet; 17 19 import java.util.LinkedList; … … 31 33 import java.util.stream.Stream; 32 34 33 35 import javax.swing.JOptionPane; 36 import javax.swing.JPanel; 34 37 35 38 import org.openstreetmap.josm.Main; 36 39 import org.openstreetmap.josm.actions.ReverseWayAction.ReverseWayResult; … … 37 40 import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult; 38 41 import org.openstreetmap.josm.command.AddCommand; 39 42 import org.openstreetmap.josm.command.ChangeCommand; 43 import org.openstreetmap.josm.command.ChangePropertyCommand; 40 44 import org.openstreetmap.josm.command.Command; 41 45 import org.openstreetmap.josm.command.DeleteCommand; 42 46 import org.openstreetmap.josm.command.SequenceCommand; … … 46 50 import org.openstreetmap.josm.data.osm.Node; 47 51 import org.openstreetmap.josm.data.osm.NodePositionComparator; 48 52 import org.openstreetmap.josm.data.osm.OsmPrimitive; 53 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 49 54 import org.openstreetmap.josm.data.osm.Relation; 50 55 import org.openstreetmap.josm.data.osm.RelationMember; 51 56 import org.openstreetmap.josm.data.osm.TagCollection; 52 57 import org.openstreetmap.josm.data.osm.Way; 58 import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; 59 import org.openstreetmap.josm.gui.DefaultNameFormatter; 53 60 import org.openstreetmap.josm.gui.Notification; 54 61 import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog; 55 62 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 63 import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 56 64 import org.openstreetmap.josm.tools.Geometry; 57 65 import org.openstreetmap.josm.tools.JosmRuntimeException; 58 66 import org.openstreetmap.josm.tools.Pair; … … 72 80 private final transient List<Relation> addedRelations = new LinkedList<>(); 73 81 74 82 /** 83 * Defines an exception while joining areas. 84 * @author Michael Zangl 85 */ 86 static class JoinAreasException extends Exception { 87 public JoinAreasException(String message) { 88 super(message); 89 } 90 } 91 92 static class UnclosedAreaException extends JoinAreasException { 93 94 private Pair<Node, Node> gap; 95 96 public UnclosedAreaException(Pair<Node, Node> gap) { 97 super("Gap found between: " + gap.a + " and " + gap.b); 98 this.gap = gap; 99 } 100 101 } 102 103 static class SelfIntersectingAreaException extends JoinAreasException { 104 105 private Pair<UndirectedWaySegment, UndirectedWaySegment> intersect; 106 107 public SelfIntersectingAreaException(Pair<UndirectedWaySegment, UndirectedWaySegment> intersect) { 108 super("Intersection found between: " + intersect.a + " and " + intersect.b); 109 this.intersect = intersect; 110 } 111 112 } 113 114 static class UndirectedWaySegment { 115 private Node a; 116 private Node b; 117 118 UndirectedWaySegment(Node a, Node b) { 119 if (a == b) { 120 throw new IllegalArgumentException("Way segment cannot start and end at the same node."); 121 } 122 this.a = a; 123 this.b = b; 124 } 125 126 public boolean hasEnd(Node current) { 127 return a == current || b == current; 128 } 129 130 public Node getOtherEnd(Node current) { 131 if (current == a) { 132 return b; 133 } else if (current == b) { 134 return a; 135 } else { 136 throw new IllegalArgumentException(current + " is not an endpoint"); 137 } 138 } 139 140 public boolean intersects(UndirectedWaySegment other) { 141 EastNorth intersection = getIntersectionPoint(other); 142 return intersection != null; 143 } 144 145 private EastNorth getIntersectionPoint(UndirectedWaySegment other) { 146 EastNorth intersection = null; 147 if (!hasEnd(other.a) && !hasEnd(other.b)) { 148 // ignore just touching. 149 intersection = Geometry.getSegmentSegmentIntersection( 150 a.getEastNorth(), b.getEastNorth(), 151 other.a.getEastNorth(), other.b.getEastNorth()); 152 } 153 return intersection; 154 } 155 156 @Override 157 public int hashCode() { 158 return a.hashCode() + b.hashCode(); 159 } 160 161 @Override 162 public boolean equals(Object obj) { 163 if (this.getClass() == obj.getClass()) { 164 UndirectedWaySegment other = (UndirectedWaySegment) obj; 165 return (a.equals(other.a) && b.equals(other.b)) || (a.equals(other.b) && b.equals(other.a)); 166 } else { 167 return false; 168 } 169 } 170 171 @Override 172 public String toString() { 173 return "UndirectedWaySegment [" + a + ", " + b + "]"; 174 } 175 176 } 177 178 /** 179 * This class defines an area that might be joined. 180 * @author Michael Zangl 181 */ 182 static class JoinableArea { 183 /** 184 * A list of Node->Node segments that compose this area. 185 * You can reconstruct the interior of this area by XORing those lines. 186 */ 187 private final HashSet<UndirectedWaySegment> waySegments = new HashSet<>(); 188 private final List<Way> ways = new ArrayList<>(); 189 private final List<Relation> relations = new ArrayList<>(); 190 private final Map<String, String> tags; 191 private final OsmPrimitive basePrimitive; 192 193 JoinableArea(Way way) throws JoinAreasException { 194 this(way, Collections.singleton(way), Collections.emptyList()); 195 } 196 197 JoinableArea(Relation relation) throws JoinAreasException { 198 this(relation, getMembers(relation, "outer"), getMembers(relation, "inner")); 199 relations.add(relation); 200 } 201 202 /** 203 * Creates a new joinable area. 204 * @param base The primitive this area is for. 205 * @param outer The ways that should be outer ways. 206 * @param inner The ways that should be inner ways. 207 * @throws JoinAreasException If the area is invalid 208 */ 209 JoinableArea(OsmPrimitive base, Collection<Way> outer, Collection<Way> inner) throws JoinAreasException { 210 basePrimitive = base; 211 tags = new HashMap<>(base.getInterestingTags()); 212 tags.remove("type", "multipolygon"); 213 214 try { 215 for (Way o : outer) { 216 addWayForceNonintersecting(o); 217 } 218 Pair<Node, Node> outerGap = findGap(); 219 if (outerGap != null) { 220 throw new UnclosedAreaException(outerGap); 221 } 222 223 for (Way i : inner) { 224 addWayForceNonintersecting(i); 225 } 226 Pair<Node, Node> innerGap = findGap(); 227 if (innerGap != null) { 228 throw new UnclosedAreaException(innerGap); 229 } 230 } catch (RuntimeException e) { 231 throw BugReport.intercept(e).put("outer", outer).put("inner", inner); 232 } 233 } 234 235 /** 236 * Check if this area is a valid closed area 237 * @return The gap if there is one, null for closed areas. 238 */ 239 private Pair<Node, Node> findGap() { 240 HashSet<UndirectedWaySegment> leftOver = new HashSet<>(waySegments); 241 while (!leftOver.isEmpty()) { 242 LinkedList<Node> part = removeOutlinePart(leftOver); 243 if (part.getFirst() != part.getLast()) { 244 return new Pair<>(part.getFirst(), part.getLast()); 245 } 246 } 247 return null; 248 } 249 250 /** 251 * Add a new Way to the outline (outer or inner) of this area. 252 * @param way The way. 253 * @throws SelfIntersectingAreaException If the way self-intersects 254 */ 255 private void addWayForceNonintersecting(Way way) throws SelfIntersectingAreaException { 256 for (Pair<Node, Node> pair : way.getNodePairs(false)) { 257 this.addWayForceNonintersecting(new UndirectedWaySegment(pair.a, pair.b)); 258 } 259 ways .add(way); 260 } 261 262 private void addWayForceNonintersecting(UndirectedWaySegment s) throws SelfIntersectingAreaException { 263 if (waySegments.contains(s)) { 264 // We add a way segment twice. This means that the outline of the area contains this segment twice. 265 // This cancels out, so we remove the segment, 266 waySegments.remove(s); 267 } else { 268 // Now check for intersections 269 Optional<UndirectedWaySegment> intersection = waySegments.stream().filter(s::intersects).findAny(); 270 if (intersection.isPresent()) { 271 throw new SelfIntersectingAreaException(new Pair<>(intersection.get(), s)); 272 } 273 waySegments.add(s); 274 } 275 } 276 277 private static Collection<Way> getMembers(Relation relation, String role) { 278 return relation.getMembers().stream().filter(m -> role.equals(m.getRole())) 279 .filter(m -> OsmPrimitiveType.WAY.equals(m.getType())).map(m -> m.getWay()) 280 .collect(Collectors.toList()); 281 } 282 283 /** 284 * Check if the area contains a segment. 285 * @param segment The segment. Assumed to not intersect any of our borders. 286 * @return true if the segment is inside. False if it is on the outline or outside. 287 */ 288 public boolean contains(UndirectedWaySegment segment) { 289 if (waySegments.contains(segment)) { 290 return false; 291 } 292 // To find out which side of the way the outer side is, we can follow a ray starting anywhere at the way in any direction. 293 // Computation is done in East/North space. 294 // We use a ray at a fixed yRay coordinate that ends at xRay; 295 // we need to make sure this ray does not go into the same direction the way is going. 296 // This is done by rotating by 90° if we need to. 297 298 int intersections = 0; 299 // Use some "random" start point on the segment 300 EastNorth rayNode1 = segment.a.getEastNorth(); 301 EastNorth rayNode2 = segment.b.getEastNorth(); 302 EastNorth rayFrom = rayNode1.getCenter(rayNode2); 303 304 // Now find the x/y mapping function. We need to ensure that rayNode1->rayNode2 is not parallel to our x axis. 305 ToDoubleFunction<EastNorth> x; 306 ToDoubleFunction<EastNorth> y; 307 if (Math.abs(rayNode1.east() - rayNode2.east()) < Math.abs(rayNode1.north() - rayNode2.north())) { 308 x = en -> en.east(); 309 y = en -> en.north(); 310 } else { 311 x = en -> -en.north(); 312 y = en -> en.east(); 313 } 314 315 double xRay = x.applyAsDouble(rayFrom); 316 double yRay = y.applyAsDouble(rayFrom); 317 318 for (UndirectedWaySegment part : waySegments) { 319 // intersect against all way segments 320 EastNorth n1 = part.a.getEastNorth(); 321 EastNorth n2 = part.b.getEastNorth(); 322 if ((rayNode1.equals(n1) && rayNode2.equals(n2)) || (rayNode2.equals(n1) && rayNode1.equals(n2))) { 323 // This is the segment we are starting the ray from. 324 // We ignore this to avoid rounding errors. 325 continue; 326 } 327 328 double x1 = x.applyAsDouble(n1); 329 double x2 = x.applyAsDouble(n2); 330 double y1 = y.applyAsDouble(n1); 331 double y2 = y.applyAsDouble(n2); 332 333 if (!((y1 <= yRay && yRay < y2) || (y2 <= yRay && yRay < y1))) { 334 // No intersection, since segment is above/below ray 335 continue; 336 } 337 double xIntersect = x1 + (x2 - x1) * (yRay - y1) / (y2 - y1); 338 double onLine = xIntersect / xRay; 339 if (Math.abs(onLine - 1) < 1e-10) { 340 // Lines that are directly on each other are considered outside. 341 return false; 342 } 343 if (xIntersect < xRay) { 344 intersections++; 345 } 346 } 347 348 return intersections % 2 == 1; 349 } 350 351 public Collection<UndirectedWaySegment> getSegments() { 352 return Collections.unmodifiableCollection(waySegments); 353 } 354 } 355 356 /** 357 * A hash set with an xor method. 358 * @param <T> element type 359 */ 360 private static class XOrHashSet<T> extends HashSet<T> { 361 public XOrHashSet() { 362 super(); 363 } 364 365 public XOrHashSet(Collection<? extends T> c) { 366 super(c); 367 } 368 369 public void xor(T e) { 370 if (!this.add(e)) { 371 this.remove(e); 372 } 373 } 374 } 375 376 /** 377 * This class collects the areas to be joined. 378 */ 379 static class JoinAreasCollector { 380 private final List<Node> possibleNewNodes = new ArrayList<>(); 381 private final List<JoinableArea> unionOf = new ArrayList<>(); 382 /** 383 * All hash sets that may be 384 */ 385 private final XOrHashSet<UndirectedWaySegment> waySegments = new XOrHashSet<>(); 386 private final DataSet ds; 387 private final Map<String, String> tags; 388 389 JoinAreasCollector(DataSet ds, Collection<? extends OsmPrimitive> waysAndRelations) throws JoinAreasException { 390 this.ds = ds; 391 Collection<JoinableArea> collectAreas = collectAreas(waysAndRelations); 392 collectAreas.forEach(this::unionWithArea); 393 394 tags = unionOf.isEmpty() ? Collections.emptyMap() : unionOf.iterator().next().tags; 395 } 396 397 private static Collection<JoinableArea> collectAreas(Collection<? extends OsmPrimitive> waysAndRelations) throws JoinAreasException { 398 Collection<JoinableArea> areas = new ArrayList<>(); 399 for(OsmPrimitive osm : waysAndRelations) { 400 if (osm instanceof Way) { 401 areas.add(new JoinableArea((Way) osm)); 402 } else if (osm instanceof Relation) { 403 areas.add(new JoinableArea((Relation) osm)); 404 } 405 } 406 return areas; 407 } 408 409 void unionWithArea(JoinableArea area) { 410 Collection<UndirectedWaySegment> segments = area.getSegments(); 411 412 // Our worker list. Once a way is split, it is re-added to the wroker to check for more splits. 413 XOrHashSet<UndirectedWaySegment> toAdd = new XOrHashSet<>(segments); 414 while (!toAdd.isEmpty()) { 415 UndirectedWaySegment s = toAdd.iterator().next(); 416 toAdd.remove(s); 417 Optional<UndirectedWaySegment> intersects = waySegments.stream().filter(s::intersects).findAny(); 418 if (intersects.isPresent()) { 419 EastNorth intersection = s.getIntersectionPoint(intersects.get()); 420 // Now generate two segments around the intersection. 421 waySegments.remove(intersects.get()); 422 // TODO: Find a node close to newNode to handle intersections of 3 or more lines. 423 Node newNode = new Node(intersection); 424 possibleNewNodes.add(newNode); 425 // We use xor here to fix ways that e.g. reverse on themselves. 426 waySegments.xor(new UndirectedWaySegment(intersects.get().a, newNode)); 427 waySegments.xor(new UndirectedWaySegment(newNode, intersects.get().b)); 428 429 toAdd.xor(new UndirectedWaySegment(s.a, newNode)); 430 toAdd.xor(new UndirectedWaySegment(newNode, s.b)); 431 } else { 432 // No more intersections - we add that segment to our geometry 433 waySegments.xor(s); 434 } 435 } 436 437 unionOf.add(area); 438 } 439 440 private boolean allAreasHaveSameTags() { 441 return unionOf.stream().allMatch(area -> area.tags.equals(tags)); 442 } 443 444 /** 445 * Gets the commands that are required to join the areas. 446 * @return The join commands. 447 */ 448 public List<Command> getCommands() { 449 if (unionOf.isEmpty()) { 450 return Collections.emptyList(); 451 } 452 Collection<UndirectedWaySegment> outline = computeOutline(); 453 454 List<Command> commands = new ArrayList<>(); 455 // The primitives of which we should remove the tags. 456 List<OsmPrimitive> toRemoveTags = new ArrayList<>(); 457 unionOf.stream().map(area -> area.basePrimitive).forEach(toRemoveTags::add); 458 459 // Add the split nodes 460 // Remove nodes of interior segments. 461 possibleNewNodes.stream() 462 .filter(n -> outline.stream().filter(w -> w.hasEnd(n)).findAny().isPresent()) 463 .map(n -> new AddCommand(ds, n)) 464 .forEach(commands::add); 465 466 // Now search all ways which are completely used in our new geometry (e.g. multipolygon inners, ...) 467 // We should not change those ways. 468 List<Way> outlineWays = new ArrayList<>(); 469 List<UndirectedWaySegment> segmentsToContain = new ArrayList<>(outline); 470 for (Way preserve : findOutlinesToPreserve(segmentsToContain)) { 471 List<UndirectedWaySegment> preservedSegments = segmentsForWay(preserve); 472 if (preservedSegments.size() != preservedSegments.stream().distinct().count()) { 473 // This way contains a segment twice. Skip it, we want to fix this. 474 continue; 475 } 476 if (!segmentsToContain.containsAll(preservedSegments)) { 477 // it may happen that two outlines that should be preserved happen to be on the same segment 478 // We need to ignore the second one then. 479 continue; 480 } 481 outlineWays.add(preserve); 482 segmentsToContain.removeAll(preservedSegments); 483 } 484 485 // Multipolygons that were selected and can now be removed 486 List<Relation> relationsToRemove = unionOf.stream().flatMap(area -> area.relations.stream()) 487 .distinct().collect(Collectors.toList()); 488 toRemoveTags.removeAll(relationsToRemove); 489 490 // Compute the ways that need to be removed. 491 // Those are all ways of the old geometry that are not used in any other place. 492 List<Way> waysToRemove = unionOf.stream().flatMap(area -> area.ways.stream()) 493 .distinct() 494 .filter(way -> !outlineWays.contains(way)) 495 // Preserve ways that are member in any relation that we did not modify 496 .filter(way -> way.getReferrers().stream().allMatch(relationsToRemove::contains)) 497 // Preserve ways that have tags 498 .filter(way -> toRemoveTags.contains(way) || way.getInterestingTags().isEmpty()) 499 .collect(Collectors.toList()); 500 toRemoveTags.removeAll(waysToRemove); 501 502 // Now we are left with the remaining outline in the segmentsToContain array. 503 // For each chunk in that outline, we create a new way 504 // TODO: We can reuse the ways we would delete otherwise. 505 while (!segmentsToContain.isEmpty()) { 506 List<Node> wayToCreate = removeOutlinePart(segmentsToContain); 507 Way osm = new Way(); 508 osm.setNodes(wayToCreate); 509 outlineWays.add(osm); 510 commands.add(new AddCommand(ds, osm)); 511 } 512 513 // Now it is time to generate the final area. 514 if (outlineWays.isEmpty()) { 515 throw new AssertionError("No outline ways found."); 516 } else if (outlineWays.size() == 1) { 517 // We only have one way. Add the tags to that way. 518 outlineWays.get(0).setKeys(tags); 519 } else { 520 // find a relation. Use the more complex multipolygon when merging two of them. 521 Relation multipolygon = relationsToRemove.stream().sorted(Comparator.comparingInt(r -> -r.getMembersCount())) 522 .findFirst().orElseGet(Relation::new); 523 multipolygon.setKeys(tags); 524 Pair<Relation, Relation> update = CreateMultipolygonAction.updateMultipolygonRelation(outlineWays, multipolygon); 525 if (update == null) { 526 throw new AssertionError("The outline ways should be continuous but no multipolygon could be created."); 527 } 528 if (update.a.getDataSet() == null) { 529 // used the fake relation. 530 commands.add(new AddCommand(update.b)); 531 } else { 532 commands.add(new ChangeCommand(update.a, update.b)); 533 } 534 relationsToRemove.remove(multipolygon); 535 } 536 537 // Apply deletion of the primitives we don't need any more. 538 if (!relationsToRemove.isEmpty()) { 539 commands.add(new DeleteCommand(ds, relationsToRemove)); 540 } 541 if (!waysToRemove.isEmpty()) { 542 commands.add(new DeleteCommand(ds, waysToRemove)); 543 } 544 for(OsmPrimitive osm : toRemoveTags) { 545 for (String key : osm.getKeys().keySet()) { 546 commands.add(new ChangePropertyCommand(osm, key, "")); 547 } 548 } 549 550 return commands; 551 } 552 553 private List<UndirectedWaySegment> segmentsForWay(Way way) { 554 return way.getNodePairs(false).stream() 555 .map(pair -> new UndirectedWaySegment(pair.a, pair.b)).collect(Collectors.toList()); 556 } 557 558 private List<Way> findOutlinesToPreserve(List<UndirectedWaySegment> segmentsToContain) { 559 return unionOf.stream().flatMap(u -> u.ways.stream()) 560 .filter(w -> segmentsToContain.containsAll(segmentsForWay(w))).collect(Collectors.toList()); 561 } 562 563 private Collection<UndirectedWaySegment> computeOutline() { 564 return waySegments.stream().filter( 565 seg -> unionOf.stream().noneMatch(area -> area.contains(seg)) 566 ).collect(Collectors.toList()); 567 } 568 } 569 570 private static LinkedList<Node> removeOutlinePart(Collection<UndirectedWaySegment> segmentsToContain) { 571 Node start = segmentsToContain.iterator().next().a; 572 LinkedList<Node> nodes = new LinkedList<>(); 573 nodes.add(start); 574 // Move in one direction on that way. 575 while ((nodes.size() < 2 || nodes.getFirst() != nodes.getLast()) && !segmentsToContain.isEmpty()) { 576 Optional<UndirectedWaySegment> traverse = segmentsToContain.stream().filter(s -> s.hasEnd(nodes.getLast())).findAny(); 577 if (!traverse.isPresent()) { 578 break; 579 } 580 segmentsToContain.remove(traverse.get()); 581 nodes.addLast(traverse.get().getOtherEnd(nodes.getLast())); 582 } 583 584 // Now move in the other direction - as far as we can go. 585 while ((nodes.size() < 2 || nodes.getFirst() != nodes.getLast()) && !segmentsToContain.isEmpty()) { 586 Optional<UndirectedWaySegment> traverse = segmentsToContain.stream().filter(s -> s.hasEnd(nodes.getFirst())).findAny(); 587 if (!traverse.isPresent()) { 588 break; 589 } 590 segmentsToContain.remove(traverse.get()); 591 nodes.addFirst(traverse.get().getOtherEnd(nodes.getFirst())); 592 } 593 594 return nodes; 595 } 596 597 /** 75 598 * This helper class describes join areas action result. 76 599 * @author viesturs 77 600 */ 78 601 public static class JoinAreasResult { 79 80 602 private final boolean hasChanges; 81 603 private final List<Multipolygon> polygons; 82 604 … … 155 677 156 678 @Override 157 679 public boolean equals(Object other) { 158 if (this == other) return true; 159 if (other == null || getClass() != other.getClass()) return false; 680 if (this == other) 681 return true; 682 if (other == null || getClass() != other.getClass()) 683 return false; 160 684 RelationRole that = (RelationRole) other; 161 return Objects.equals(rel, that.rel) && 162 Objects.equals(role, that.role); 685 return Objects.equals(rel, that.rel) && Objects.equals(role, that.role); 163 686 } 164 687 } 165 688 … … 185 708 186 709 @Override 187 710 public boolean equals(Object other) { 188 if (this == other) return true; 189 if (other == null || getClass() != other.getClass()) return false; 711 if (this == other) 712 return true; 713 if (other == null || getClass() != other.getClass()) 714 return false; 190 715 WayInPolygon that = (WayInPolygon) other; 191 return insideToTheRight == that.insideToTheRight && 192 Objects.equals(way, that.way); 716 return insideToTheRight == that.insideToTheRight && Objects.equals(way, that.way); 193 717 } 194 718 195 719 @Override … … 232 756 * Inverse inside and outside 233 757 */ 234 758 public void reverse() { 235 for (WayInPolygon way : ways) {759 for (WayInPolygon way : ways) { 236 760 way.insideToTheRight = !way.insideToTheRight; 237 761 } 238 762 Collections.reverse(ways); … … 338 862 EastNorth en1 = n1.getEastNorth(); 339 863 EastNorth en2 = n2.getEastNorth(); 340 864 EastNorth en3 = n3.getEastNorth(); 341 double angle = Math.atan2(en3.getY() - en1.getY(), en3.getX() - en1.getX()) -342 Math.atan2(en2.getY() - en1.getY(), en2.getX() - en1.getX());343 while (angle >= 2 *Math.PI) {344 angle -= 2 *Math.PI;865 double angle = Math.atan2(en3.getY() - en1.getY(), en3.getX() - en1.getX()) 866 - Math.atan2(en2.getY() - en1.getY(), en2.getX() - en1.getX()); 867 while (angle >= 2 * Math.PI) { 868 angle -= 2 * Math.PI; 345 869 } 346 870 while (angle < 0) { 347 angle += 2 *Math.PI;871 angle += 2 * Math.PI; 348 872 } 349 873 return angle; 350 874 } … … 362 886 363 887 // Pairs of (way, nextNode) 364 888 lastWay = Stream.concat( 365 availableWays.stream() 366 .filter(way -> way.way.firstNode().equals(headNode) && way.insideToTheRight) 367 .map(way -> new Pair<>(way, way.way.getNode(1))), 368 availableWays.stream() 369 .filter(way -> way.way.lastNode().equals(headNode) && !way.insideToTheRight) 370 .map(way -> new Pair<>(way, way.way.getNode(way.way.getNodesCount() - 2)))) 889 availableWays.stream().filter(way -> way.way.firstNode().equals(headNode) && way.insideToTheRight) 890 .map(way -> new Pair<>(way, way.way.getNode(1))), 891 availableWays.stream().filter(way -> way.way.lastNode().equals(headNode) && !way.insideToTheRight) 892 .map(way -> new Pair<>(way, way.way.getNode(way.way.getNodesCount() - 2)))) 371 893 372 373 .min(Comparator.comparingDouble(wayAndNext -> {374 Node nextNode = wayAndNext.b;375 if (nextNode == prevNode) {376 // we always prefer going back.377 return Double.POSITIVE_INFINITY;378 }379 double angle = Math.atan2(nextNode.getEastNorth().east() - headNode.getEastNorth().east(),380 nextNode.getEastNorth().north() - headNode.getEastNorth().north()) - headAngle;381 if (angle > Math.PI)382 angle -= 2*Math.PI;383 if (angle <= -Math.PI)384 angle += 2*Math.PI;385 return angle;386 })).map(wayAndNext -> wayAndNext.a).orElse(null);894 // now find the way with the best angle 895 .min(Comparator.comparingDouble(wayAndNext -> { 896 Node nextNode = wayAndNext.b; 897 if (nextNode == prevNode) { 898 // we always prefer going back. 899 return Double.POSITIVE_INFINITY; 900 } 901 double angle = Math.atan2(nextNode.getEastNorth().east() - headNode.getEastNorth().east(), 902 nextNode.getEastNorth().north() - headNode.getEastNorth().north()) - headAngle; 903 if (angle > Math.PI) 904 angle -= 2 * Math.PI; 905 if (angle <= -Math.PI) 906 angle += 2 * Math.PI; 907 return angle; 908 })).map(wayAndNext -> wayAndNext.a).orElse(null); 387 909 lastWayReverse = lastWay != null && !lastWay.insideToTheRight; 388 910 return lastWay; 389 911 } … … 398 920 399 921 WayInPolygon mostLeft = null; // most left way connected to head node 400 922 boolean comingToHead = false; // true if candidate come to head node 401 double angle = 2 *Math.PI;923 double angle = 2 * Math.PI; 402 924 403 925 for (WayInPolygon candidateWay : availableWays) { 404 926 boolean candidateComingToHead; … … 408 930 candidateComingToHead = !candidateWay.insideToTheRight; 409 931 candidatePrevNode = candidateWay.way.getNode(1); 410 932 } else if (candidateWay.way.lastNode().equals(headNode)) { 411 412 933 candidateComingToHead = candidateWay.insideToTheRight; 934 candidatePrevNode = candidateWay.way.getNode(candidateWay.way.getNodesCount() - 2); 413 935 } else 414 936 continue; 415 937 if (candidateComingToHead && candidateWay.equals(lastWay)) … … 417 939 418 940 double candidateAngle = getAngle(headNode, candidatePrevNode, prevNode); 419 941 420 if (mostLeft == null || candidateAngle < angle || (Utils.equalsEpsilon(candidateAngle, angle) && !candidateComingToHead)) { 942 if (mostLeft == null || candidateAngle < angle 943 || (Utils.equalsEpsilon(candidateAngle, angle) && !candidateComingToHead)) { 421 944 // Candidate is most left 422 945 mostLeft = candidateWay; 423 946 comingToHead = candidateComingToHead; … … 462 985 * @since 11611 463 986 */ 464 987 public JoinAreasAction(boolean addShortcut) { 465 super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"), addShortcut ? 466 Shortcut.registerShortcut("tools:joinareas", tr("Tool: {0}", tr("Join overlapping Areas")), KeyEvent.VK_J, Shortcut.SHIFT) 467 : null, true); 988 super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"), 989 addShortcut ? Shortcut.registerShortcut("tools:joinareas", 990 tr("Tool: {0}", tr("Join overlapping Areas")), KeyEvent.VK_J, Shortcut.SHIFT) : null, 991 true); 468 992 } 469 993 470 994 /** … … 473 997 */ 474 998 @Override 475 999 public void actionPerformed(ActionEvent e) { 476 join(Main.getLayerManager().getEditDataSet().getSelected Ways());1000 join(Main.getLayerManager().getEditDataSet().getSelected()); 477 1001 } 478 1002 479 1003 /** 480 1004 * Joins the given ways. 481 * @param ways Ways to join1005 * @param waysAndRelations Ways / Multipolygons to join 482 1006 * @since 7534 483 1007 */ 484 public void join(Collection<Way> ways) { 485 addedRelations.clear(); 486 487 if (ways.isEmpty()) { 488 new Notification( 489 tr("Please select at least one closed way that should be joined.")) 490 .setIcon(JOptionPane.INFORMATION_MESSAGE) 491 .show(); 1008 public void join(Collection<? extends OsmPrimitive> waysAndRelations) { 1009 if (waysAndRelations.isEmpty()) { 1010 new Notification(tr("Please select at least one closed area that should be joined.")) 1011 .setIcon(JOptionPane.INFORMATION_MESSAGE).show(); 492 1012 return; 493 1013 } 494 1014 495 List<Node> allNodes = new ArrayList<>(); 496 for (Way way : ways) { 497 if (!way.isClosed()) { 498 new Notification( 499 tr("One of the selected ways is not closed and therefore cannot be joined.")) 500 .setIcon(JOptionPane.INFORMATION_MESSAGE) 501 .show(); 502 return; 503 } 504 505 allNodes.addAll(way.getNodes()); 1015 if (!ofSameDataset(waysAndRelations)) { 1016 throw new IllegalArgumentException("Not in same DataSet"); 506 1017 } 1018 waysAndRelations = selectRelationsInsteadOfMembers(waysAndRelations); 1019 DataSet ds = waysAndRelations.iterator().next().getDataSet(); 507 1020 508 // TODO: Only display this warning when nodes outside dataSourceArea are deleted 509 boolean ok = Command.checkAndConfirmOutlyingOperation("joinarea", tr("Join area confirmation"), 510 trn("The selected way has nodes outside of the downloaded data region.", 511 "The selected ways have nodes outside of the downloaded data region.", 512 ways.size()) + "<br/>" 513 + tr("This can lead to nodes being deleted accidentally.") + "<br/>" 514 + tr("Are you really sure to continue?") 515 + tr("Please abort if you are not sure"), 516 tr("The selected area is incomplete. Continue?"), 517 allNodes, null); 518 if (!ok) return; 519 520 //analyze multipolygon relations and collect all areas 521 List<Multipolygon> areas = collectMultipolygons(ways); 522 523 if (areas == null) 524 //too complex multipolygon relations found 1021 if (!Command.checkAndConfirmOutlyingOperation("joinarea", tr("Join area confirmation"), 1022 trn("The selected area has nodes outside of the downloaded data region.", 1023 "The selected areas have nodes outside of the downloaded data region.", waysAndRelations.size()) + "<br/>" 1024 + tr("This can lead to nodes being deleted accidentally.") + "<br/>" 1025 + tr("Are you really sure to continue?") + tr("Please abort if you are not sure"), 1026 tr("The selected area is incomplete. Continue?"), waysAndRelations, null)) { 525 1027 return; 526 527 if (!testJoin(areas)) {528 new Notification(529 tr("No intersection found. Nothing was changed."))530 .setIcon(JOptionPane.INFORMATION_MESSAGE)531 .show();532 return;533 1028 } 534 1029 535 if (!resolveTagConflicts(areas))536 return;537 //user canceled, do nothing.538 539 1030 try { 540 // see #11026 - Because <ways> is a dynamic filtered (on ways) of a filtered (on selected objects) collection, 541 // retrieve effective dataset before joining the ways (which affects the selection, thus, the <ways> collection) 542 // Dataset retrieving allows to call this code without relying on Main.getCurrentDataSet(), thus, on a mapview instance 543 DataSet ds = ways.iterator().next().getDataSet(); 1031 JoinAreasCollector collector = new JoinAreasCollector(ds, waysAndRelations); 1032 if (!collector.allAreasHaveSameTags()) { 1033 new Notification(tr("Only areas with the same tags can be joined.")) 1034 .setIcon(JOptionPane.INFORMATION_MESSAGE).show(); 1035 return; 1036 } 544 1037 545 // Do the job of joining areas546 JoinAreasResult result = joinAreas(areas);1038 List<Command> commands = collector.getCommands(); 1039 commitCommand(new SequenceCommand(tr("Join Areas"), commands)); 547 1040 548 if (result.hasChanges) { 549 // move tags from ways to newly created relations 550 // TODO: do we need to also move tags for the modified relations? 551 for (Relation r: addedRelations) { 552 cmds.addAll(CreateMultipolygonAction.removeTagsFromWaysIfNeeded(r)); 553 } 554 commitCommands(tr("Move tags from ways to relations")); 555 556 List<Way> allWays = new ArrayList<>(); 557 for (Multipolygon pol : result.polygons) { 558 allWays.add(pol.outerWay); 559 allWays.addAll(pol.innerWays); 560 } 561 if (ds != null) { 562 ds.setSelected(allWays); 563 } 564 } else { 565 new Notification( 566 tr("No intersection found. Nothing was changed.")) 567 .setIcon(JOptionPane.INFORMATION_MESSAGE) 568 .show(); 569 } 570 } catch (UserCancelException exception) { 571 Main.trace(exception); 572 //revert changes 573 //FIXME: this is dirty hack 574 makeCommitsOneAction(tr("Reverting changes")); 575 Main.main.undoRedo.undo(); 576 Main.main.undoRedo.redoCommands.clear(); 1041 } catch (UnclosedAreaException e) { 1042 new Notification(tr("One of the selected areas is not closed and therefore cannot be joined.")) 1043 .setIcon(JOptionPane.INFORMATION_MESSAGE).show(); 1044 return; 1045 } catch (JoinAreasException e) { 1046 new Notification(tr("One of the selected areas has an invalid geomerty.")) 1047 .setIcon(JOptionPane.INFORMATION_MESSAGE).show(); 1048 return; 577 1049 } 578 1050 } 579 1051 580 1052 /** 581 * Tests if the areas have some intersections to join.582 * @param areas Areas to test583 * @return {@code true} if areas are joinable1053 * If all members of a multipolygon are selected, ask the user to select the polygon instead of the ways. 1054 * @param currentSelection 1055 * @return The new list of primitives the user selected 584 1056 */ 585 private boolean testJoin(List<Multipolygon> areas) { 586 List<Way> allStartingWays = new ArrayList<>(); 1057 private Collection<? extends OsmPrimitive> selectRelationsInsteadOfMembers(Collection<? extends OsmPrimitive> currentSelection) { 1058 List<Relation> selectableMultipolygons = currentSelection.stream() 1059 .filter(osm -> osm.getType() == OsmPrimitiveType.WAY) 1060 // Get all multipolygons refferred by the way 1061 .flatMap(osm -> osm.getReferrers().stream()) 1062 .distinct() 1063 .filter(osm -> osm.isMultipolygon()) 1064 .map(osm -> ((Relation) osm)) 1065 // Filter for those that are completely selected 1066 .filter(r -> r.getMembers().stream().map(m -> m.getMember()).allMatch(currentSelection::contains)) 1067 .collect(Collectors.toList()); 587 1068 588 for (Multipolygon area : areas) { 589 allStartingWays.add(area.outerWay); 590 allStartingWays.addAll(area.innerWays); 1069 if (!selectableMultipolygons.isEmpty()) { 1070 JPanel msg = new JPanel(new GridBagLayout()); 1071 msg.add(new JMultilineLabel("<html>" + 1072 tr("You selected the members of the following multipolygons. " 1073 + "Do you want to join the polygons instead?") 1074 + "<ul>" + selectableMultipolygons.stream() 1075 .map(r -> "<li>" + r.getDisplayName(DefaultNameFormatter.getInstance()) + "</li>") 1076 .collect(Collectors.joining()) 1077 + "</ul></html>")); 1078 boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog( 1079 "join_areas_on_polygons", 1080 Main.parent, 1081 msg, 1082 tr("Join multipolygons?"), 1083 JOptionPane.YES_NO_OPTION, 1084 JOptionPane.QUESTION_MESSAGE, 1085 JOptionPane.YES_OPTION); 1086 if (answer) { 1087 HashSet<OsmPrimitive> select = new HashSet<>(selectableMultipolygons); 1088 currentSelection.stream().filter( 1089 w -> !(w instanceof Way && ((Way) w).getReferrers().stream().allMatch(selectableMultipolygons::contains)) 1090 ).forEach(select::add); 1091 return select; 1092 } 591 1093 } 1094 return currentSelection; 1095 } 592 1096 593 //find intersection points 594 Set<Node> nodes = Geometry.addIntersections(allStartingWays, true, cmds); 595 return !nodes.isEmpty(); 1097 private boolean ofSameDataset(Collection<? extends OsmPrimitive> waysAndRelations) { 1098 return waysAndRelations.stream().map(OsmPrimitive::getDataSet).distinct().count() <= 1; 596 1099 } 597 1100 598 1101 private static class DuplicateWayCollectorAccu { 599 600 1102 private List<Way> currentWays = new ArrayList<>(); 1103 private List<Way> duplicatesFound = new ArrayList<>(); 601 1104 602 603 604 605 606 Optional<Way> duplicate = currentWays.stream()607 .filter(current -> current.getNodes().equals(wayNodes) || current.getNodes().equals(wayNodesReversed))608 .findFirst();609 610 611 612 613 614 615 616 1105 private void add(Way way) { 1106 List<Node> wayNodes = way.getNodes(); 1107 List<Node> wayNodesReversed = way.getNodes(); 1108 Collections.reverse(wayNodesReversed); 1109 Optional<Way> duplicate = currentWays.stream().filter( 1110 current -> current.getNodes().equals(wayNodes) || current.getNodes().equals(wayNodesReversed)) 1111 .findFirst(); 1112 if (duplicate.isPresent()) { 1113 currentWays.remove(duplicate.get()); 1114 duplicatesFound.add(duplicate.get()); 1115 duplicatesFound.add(way); 1116 } else { 1117 currentWays.add(way); 1118 } 1119 } 617 1120 618 619 620 621 622 1121 private DuplicateWayCollectorAccu combine(DuplicateWayCollectorAccu a2) { 1122 duplicatesFound.addAll(a2.duplicatesFound); 1123 a2.currentWays.forEach(this::add); 1124 return this; 1125 } 623 1126 } 624 1127 625 1128 /** … … 709 1212 List<WayInPolygon> preparedWays = new ArrayList<>(); 710 1213 711 1214 // Split the nodes on the 712 List<Way> splitOuterWays = outerStartingWays.stream() 713 . flatMap(way -> splitWayOnNodes(way, nodes).stream()).collect(Collectors.toList());714 List<Way> splitInnerWays = innerStartingWays.stream() 715 . flatMap(way -> splitWayOnNodes(way, nodes).stream()).collect(Collectors.toList());1215 List<Way> splitOuterWays = outerStartingWays.stream().flatMap(way -> splitWayOnNodes(way, nodes).stream()) 1216 .collect(Collectors.toList()); 1217 List<Way> splitInnerWays = innerStartingWays.stream().flatMap(way -> splitWayOnNodes(way, nodes).stream()) 1218 .collect(Collectors.toList()); 716 1219 717 1220 // remove duplicate ways (A->B->C and C->B->A) 718 List<Way> duplicates = Stream.concat(splitOuterWays.stream(), splitInnerWays.stream()).collect(new DuplicateWayCollector()); 1221 List<Way> duplicates = Stream.concat(splitOuterWays.stream(), splitInnerWays.stream()) 1222 .collect(new DuplicateWayCollector()); 719 1223 720 1224 splitOuterWays.removeAll(duplicates); 721 1225 splitInnerWays.removeAll(duplicates); … … 754 1258 755 1259 commitCommands(marktr("Assemble new polygons")); 756 1260 757 for (Relation rel : relationsToDelete) {1261 for (Relation rel : relationsToDelete) { 758 1262 cmds.add(new DeleteCommand(rel)); 759 1263 } 760 1264 … … 774 1278 if (warnAboutRelations) { 775 1279 new Notification( 776 1280 tr("Some of the ways were part of relations that have been modified.<br>Please verify no errors have been introduced.")) 777 .setIcon(JOptionPane.INFORMATION_MESSAGE) 778 .setDuration(Notification.TIME_LONG) 779 .show(); 1281 .setIcon(JOptionPane.INFORMATION_MESSAGE).setDuration(Notification.TIME_LONG).show(); 780 1282 } 781 1283 782 1284 return new JoinAreasResult(true, polygons); … … 788 1290 * @return {@code true} if all conflicts are resolved, {@code false} if conflicts remain. 789 1291 */ 790 1292 private boolean resolveTagConflicts(List<Multipolygon> polygons) { 791 1293 //TODO: Use this 792 1294 List<Way> ways = new ArrayList<>(); 793 1295 794 1296 for (Multipolygon pol : polygons) { … … 879 1381 * @param description The description of what the commands do 880 1382 */ 881 1383 private void commitCommands(String description) { 882 switch (cmds.size()) {1384 switch (cmds.size()) { 883 1385 case 0: 884 1386 return; 885 1387 case 1: … … 904 1406 905 1407 /** 906 1408 * This method analyzes the way and assigns each part what direction polygon "inside" is. 1409 * 1410 * It uses an even/odd winding rule. 1411 * 907 1412 * @param parts the split parts of the way 908 1413 * @param isInner - if true, reverts the direction (for multipolygon islands) 909 1414 * @return list of parts, marked with the inside orientation. … … 983 1488 984 1489 if (chunks.size() > 1) { 985 1490 SplitWayResult split = SplitWayAction.splitWay(getLayerManager().getEditLayer(), way, chunks, 986 Collections.<OsmPrimitive> emptyList(), SplitWayAction.Strategy.keepFirstChunk());1491 Collections.<OsmPrimitive> emptyList(), SplitWayAction.Strategy.keepFirstChunk()); 987 1492 988 1493 if (split != null) { 989 1494 //execute the command, we need the results … … 1116 1621 // This seems to appear when is apply over invalid way like #9911 test-case 1117 1622 // Remove all of these way to make the next work. 1118 1623 List<WayInPolygon> cleanMultigonWays = multigonWays.stream() 1119 .filter(way -> way.way.getNodesCount() != 2 || !way.way.isClosed()) 1120 .collect(Collectors.toList()); 1624 .filter(way -> way.way.getNodesCount() != 2 || !way.way.isClosed()).collect(Collectors.toList()); 1121 1625 WayTraverser traverser = new WayTraverser(cleanMultigonWays); 1122 1626 List<AssembledPolygon> result = new ArrayList<>(); 1123 1627 … … 1133 1637 return fixTouchingPolygons(result); 1134 1638 } 1135 1639 1136 private static void findBoundaryPolygonsStartingWith(List<Way> discardedResult, WayTraverser traverser, List<AssembledPolygon> result,1137 WayInPolygon startWay) {1640 private static void findBoundaryPolygonsStartingWith(List<Way> discardedResult, WayTraverser traverser, 1641 List<AssembledPolygon> result, WayInPolygon startWay) { 1138 1642 List<WayInPolygon> path = new ArrayList<>(); 1139 1643 List<WayInPolygon> startWays = new ArrayList<>(); 1140 1644 try { … … 1158 1662 if (ring.getNodes().size() <= 2) { 1159 1663 // Invalid ring (2 nodes) -> remove 1160 1664 traverser.removeWays(path); 1161 for (WayInPolygon way : path) {1665 for (WayInPolygon way : path) { 1162 1666 discardedResult.add(way.way); 1163 1667 } 1164 1668 } else { … … 1177 1681 traverser.removeWay(currentWay); 1178 1682 path.remove(index); 1179 1683 } 1180 traverser.setStartWay(path.get(index -1));1684 traverser.setStartWay(path.get(index - 1)); 1181 1685 } else { 1182 1686 path.add(nextWay); 1183 1687 } … … 1358 1862 } 1359 1863 1360 1864 if (outerWays.size() > 1) { 1361 new Notification( 1362 tr("Sorry. Cannot handle multipolygon relations with multiple outer ways.")) 1363 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1364 .show(); 1865 new Notification(tr("Sorry. Cannot handle multipolygon relations with multiple outer ways.")) 1866 .setIcon(JOptionPane.INFORMATION_MESSAGE).show(); 1365 1867 return null; 1366 1868 } 1367 1869 … … 1371 1873 innerWays.retainAll(selectedWays); 1372 1874 1373 1875 if (processedOuterWays.contains(outerWay)) { 1374 new Notification( 1375 tr("Sorry. Cannot handle way that is outer in multiple multipolygon relations.")) 1376 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1377 .show(); 1876 new Notification(tr("Sorry. Cannot handle way that is outer in multiple multipolygon relations.")) 1877 .setIcon(JOptionPane.INFORMATION_MESSAGE).show(); 1378 1878 return null; 1379 1879 } 1380 1880 1381 1881 if (processedInnerWays.contains(outerWay)) { 1382 new Notification( 1383 tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations.")) 1384 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1385 .show(); 1882 new Notification(tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations.")) 1883 .setIcon(JOptionPane.INFORMATION_MESSAGE).show(); 1386 1884 return null; 1387 1885 } 1388 1886 1389 for (Way way : innerWays) {1887 for (Way way : innerWays) { 1390 1888 if (processedOuterWays.contains(way)) { 1391 1889 new Notification( 1392 1890 tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations.")) 1393 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1394 .show(); 1891 .setIcon(JOptionPane.INFORMATION_MESSAGE).show(); 1395 1892 return null; 1396 1893 } 1397 1894 1398 1895 if (processedInnerWays.contains(way)) { 1399 new Notification( 1400 tr("Sorry. Cannot handle way that is inner in multiple multipolygon relations.")) 1401 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1402 .show(); 1896 new Notification(tr("Sorry. Cannot handle way that is inner in multiple multipolygon relations.")) 1897 .setIcon(JOptionPane.INFORMATION_MESSAGE).show(); 1403 1898 return null; 1404 1899 } 1405 1900 } … … 1431 1926 * @return The list of relation with roles to add own relation to 1432 1927 */ 1433 1928 private RelationRole addOwnMultipolygonRelation(Collection<Way> inner) { 1434 if (inner.isEmpty()) return null; 1929 if (inner.isEmpty()) 1930 return null; 1435 1931 OsmDataLayer layer = Main.getLayerManager().getEditLayer(); 1436 1932 // Create new multipolygon relation and add all inner ways to it 1437 1933 Relation newRel = new Relation(); … … 1439 1935 for (Way w : inner) { 1440 1936 newRel.addMember(new RelationMember("inner", w)); 1441 1937 } 1442 cmds.add(layer != null ? new AddCommand(layer, newRel) :1443 new AddCommand(inner.iterator().next().getDataSet(), newRel));1938 cmds.add(layer != null ? new AddCommand(layer, newRel) 1939 : new AddCommand(inner.iterator().next().getDataSet(), newRel)); 1444 1940 addedRelations.add(newRel); 1445 1941 1446 1942 // We don't add outer to the relation because it will be handed to fixRelations() … … 1492 1988 * @param ownMultipol elements to directly add as outer 1493 1989 * @param relationsToDelete set of relations to delete. 1494 1990 */ 1495 private void fixRelations(List<RelationRole> rels, Way outer, RelationRole ownMultipol, Set<Relation> relationsToDelete) { 1991 private void fixRelations(List<RelationRole> rels, Way outer, RelationRole ownMultipol, 1992 Set<Relation> relationsToDelete) { 1496 1993 List<RelationRole> multiouters = new ArrayList<>(); 1497 1994 1498 1995 if (ownMultipol != null) {