Ticket #14528: 14528-rework-join-areas.patch
File 14528-rework-join-areas.patch, 100.1 KB (added by , 8 years ago) |
---|
-
src/org/openstreetmap/josm/actions/JoinAreasAction.java
1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm.actions; 3 3 4 import static org.openstreetmap.josm.tools.I18n.marktr;5 4 import static org.openstreetmap.josm.tools.I18n.tr; 6 5 import static org.openstreetmap.josm.tools.I18n.trn; 7 6 7 import java.awt.GridBagLayout; 8 8 import java.awt.event.ActionEvent; 9 9 import java.awt.event.KeyEvent; 10 10 import java.util.ArrayList; … … 11 11 import java.util.Collection; 12 12 import java.util.Collections; 13 13 import java.util.Comparator; 14 import java.util. EnumSet;14 import java.util.HashMap; 15 15 import java.util.HashSet; 16 import java.util.LinkedHashSet;17 16 import java.util.LinkedList; 18 17 import java.util.List; 19 18 import java.util.Map; 20 import java.util.Objects;21 19 import java.util.Optional; 22 import java.util.Set;23 import java.util.TreeMap;24 import java.util.function.BiConsumer;25 import java.util.function.BinaryOperator;26 import java.util.function.Function;27 import java.util.function.Supplier;28 20 import java.util.function.ToDoubleFunction; 29 import java.util.stream.Collector;30 21 import java.util.stream.Collectors; 31 22 import java.util.stream.Stream; 32 23 33 24 import javax.swing.JOptionPane; 25 import javax.swing.JPanel; 34 26 35 27 import org.openstreetmap.josm.Main; 36 import org.openstreetmap.josm.actions.ReverseWayAction.ReverseWayResult;37 import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult;38 28 import org.openstreetmap.josm.command.AddCommand; 39 29 import org.openstreetmap.josm.command.ChangeCommand; 30 import org.openstreetmap.josm.command.ChangePropertyCommand; 40 31 import org.openstreetmap.josm.command.Command; 41 32 import org.openstreetmap.josm.command.DeleteCommand; 42 33 import org.openstreetmap.josm.command.SequenceCommand; 43 import org.openstreetmap.josm.data.UndoRedoHandler;44 34 import org.openstreetmap.josm.data.coor.EastNorth; 45 35 import org.openstreetmap.josm.data.osm.DataSet; 46 36 import org.openstreetmap.josm.data.osm.Node; 47 import org.openstreetmap.josm.data.osm.NodePositionComparator;48 37 import org.openstreetmap.josm.data.osm.OsmPrimitive; 38 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 49 39 import org.openstreetmap.josm.data.osm.Relation; 50 import org.openstreetmap.josm.data.osm.RelationMember;51 40 import org.openstreetmap.josm.data.osm.TagCollection; 52 41 import org.openstreetmap.josm.data.osm.Way; 42 import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; 43 import org.openstreetmap.josm.gui.DefaultNameFormatter; 53 44 import org.openstreetmap.josm.gui.Notification; 54 45 import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog; 55 import org.openstreetmap.josm.gui. layer.OsmDataLayer;46 import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 56 47 import org.openstreetmap.josm.tools.Geometry; 57 import org.openstreetmap.josm.tools.JosmRuntimeException;58 48 import org.openstreetmap.josm.tools.Pair; 59 49 import org.openstreetmap.josm.tools.Shortcut; 60 50 import org.openstreetmap.josm.tools.UserCancelException; 61 import org.openstreetmap.josm.tools.Utils;62 51 import org.openstreetmap.josm.tools.bugreport.BugReport; 63 52 64 53 /** … … 66 55 * @since 2575 67 56 */ 68 57 public class JoinAreasAction extends JosmAction { 69 // This will be used to commit commands and unite them into one large command sequence at the end70 private final transient LinkedList<Command> cmds = new LinkedList<>();71 private int cmdsCount;72 private final transient List<Relation> addedRelations = new LinkedList<>();73 74 58 /** 75 * This helper class describes join areas action result.76 * @author viesturs59 * Defines an exception while joining areas. 60 * @author Michael Zangl 77 61 */ 78 public static class JoinAreasResult { 79 80 private final boolean hasChanges; 81 private final List<Multipolygon> polygons; 82 83 /** 84 * Constructs a new {@code JoinAreasResult}. 85 * @param hasChanges whether the result has changes 86 * @param polygons the result polygons, can be null 87 */ 88 public JoinAreasResult(boolean hasChanges, List<Multipolygon> polygons) { 89 this.hasChanges = hasChanges; 90 this.polygons = polygons; 62 public static class JoinAreasException extends Exception { 63 protected JoinAreasException(String message) { 64 super(message); 91 65 } 66 } 92 67 93 /** 94 * Determines if the result has changes. 95 * @return {@code true} if the result has changes 96 */ 97 public final boolean hasChanges() { 98 return hasChanges; 99 } 68 static class UnclosedAreaException extends JoinAreasException { 69 private Pair<Node, Node> gap; 100 70 101 /** 102 * Returns the result polygons, can be null. 103 * @return the result polygons, can be null 104 */ 105 public final List<Multipolygon> getPolygons() { 106 return polygons; 71 public UnclosedAreaException(Pair<Node, Node> gap) { 72 super("Gap found between: " + gap.a + " and " + gap.b); 73 this.gap = gap; 107 74 } 108 75 } 109 76 110 public static class Multipolygon { 111 private final Way outerWay; 112 private final List<Way> innerWays; 77 static class SelfIntersectingAreaException extends JoinAreasException { 78 private Pair<UndirectedWaySegment, UndirectedWaySegment> intersect; 113 79 114 /** 115 * Constructs a new {@code Multipolygon}. 116 * @param way outer way 117 */ 118 public Multipolygon(Way way) { 119 outerWay = way; 120 innerWays = new ArrayList<>(); 80 public SelfIntersectingAreaException(Pair<UndirectedWaySegment, UndirectedWaySegment> intersect) { 81 super("Intersection found between: " + intersect.a + " and " + intersect.b); 82 this.intersect = intersect; 121 83 } 84 } 122 85 123 /** 124 * Returns the outer way. 125 * @return the outer way 126 */ 127 public final Way getOuterWay() { 128 return outerWay; 129 } 86 static class UndirectedWaySegment { 87 private Node a; 88 private Node b; 130 89 131 /**132 * Returns the inner ways.133 * @return the inner ways134 */135 public final List<Way> getInnerWays() {136 return innerWays;90 UndirectedWaySegment(Node a, Node b) { 91 if (a == b) { 92 throw new IllegalArgumentException("Way segment cannot start and end at the same node."); 93 } 94 this.a = a; 95 this.b = b; 137 96 } 138 }139 97 140 // HelperClass 141 // Saves a relation and a role an OsmPrimitve was part of until it was stripped from all relations 142 private static class RelationRole { 143 public final Relation rel; 144 public final String role; 145 146 RelationRole(Relation rel, String role) { 147 this.rel = rel; 148 this.role = role; 98 public boolean hasEnd(Node current) { 99 return a == current || b == current; 149 100 } 150 101 151 @Override 152 public int hashCode() { 153 return Objects.hash(rel, role); 102 public Node getOtherEnd(Node current) { 103 if (current == a) { 104 return b; 105 } else if (current == b) { 106 return a; 107 } else { 108 throw new IllegalArgumentException(current + " is not an endpoint"); 109 } 154 110 } 155 111 156 @Override 157 public boolean equals(Object other) { 158 if (this == other) return true; 159 if (other == null || getClass() != other.getClass()) return false; 160 RelationRole that = (RelationRole) other; 161 return Objects.equals(rel, that.rel) && 162 Objects.equals(role, that.role); 112 public boolean intersects(UndirectedWaySegment other) { 113 EastNorth intersection = getIntersectionPoint(other); 114 return intersection != null; 163 115 } 164 }165 116 166 /** 167 * HelperClass - saves a way and the "inside" side. 168 * 169 * insideToTheLeft: if true left side is "in", false -right side is "in". 170 * Left and right are determined along the orientation of way. 171 */ 172 public static class WayInPolygon { 173 public final Way way; 174 public boolean insideToTheRight; 175 176 public WayInPolygon(Way way, boolean insideRight) { 177 this.way = way; 178 this.insideToTheRight = insideRight; 117 private EastNorth getIntersectionPoint(UndirectedWaySegment other) { 118 EastNorth intersection = null; 119 if (!hasEnd(other.a) && !hasEnd(other.b)) { 120 // ignore just touching. 121 intersection = Geometry.getSegmentSegmentIntersection( 122 a.getEastNorth(), b.getEastNorth(), 123 other.a.getEastNorth(), other.b.getEastNorth()); 124 } 125 return intersection; 179 126 } 180 127 181 128 @Override 182 129 public int hashCode() { 183 return Objects.hash(way, insideToTheRight);130 return a.hashCode() + b.hashCode(); 184 131 } 185 132 186 133 @Override 187 public boolean equals(Object other) { 188 if (this == other) return true; 189 if (other == null || getClass() != other.getClass()) return false; 190 WayInPolygon that = (WayInPolygon) other; 191 return insideToTheRight == that.insideToTheRight && 192 Objects.equals(way, that.way); 134 public boolean equals(Object obj) { 135 if (this.getClass() == obj.getClass()) { 136 UndirectedWaySegment other = (UndirectedWaySegment) obj; 137 return (a.equals(other.a) && b.equals(other.b)) || (a.equals(other.b) && b.equals(other.a)); 138 } else { 139 return false; 140 } 193 141 } 194 142 195 143 @Override 196 144 public String toString() { 197 return " WayInPolygon [way=" + way + ", insideToTheRight=" + insideToTheRight+ "]";145 return "UndirectedWaySegment [" + a + ", " + b + "]"; 198 146 } 147 199 148 } 200 149 201 150 /** 202 * This helper class describes a polygon, assembled from several ways. 203 * @author viesturs 204 * 151 * This class defines an area that might be joined. 152 * @author Michael Zangl 205 153 */ 206 public static class AssembledPolygon { 207 public List<WayInPolygon> ways; 208 209 public AssembledPolygon(List<WayInPolygon> boundary) { 210 this.ways = boundary; 211 } 212 213 public List<Node> getNodes() { 214 List<Node> nodes = new ArrayList<>(); 215 for (WayInPolygon way : this.ways) { 216 //do not add the last node as it will be repeated in the next way 217 if (way.insideToTheRight) { 218 for (int pos = 0; pos < way.way.getNodesCount() - 1; pos++) { 219 nodes.add(way.way.getNode(pos)); 220 } 221 } else { 222 for (int pos = way.way.getNodesCount() - 1; pos > 0; pos--) { 223 nodes.add(way.way.getNode(pos)); 224 } 225 } 226 } 227 228 return nodes; 229 } 230 154 static class JoinableArea { 231 155 /** 232 * Inverse inside and outside 156 * A list of Node->Node segments that compose this area. 157 * You can reconstruct the interior of this area by XORing those lines. 233 158 */ 234 public void reverse() { 235 for (WayInPolygon way: ways) { 236 way.insideToTheRight = !way.insideToTheRight; 237 } 238 Collections.reverse(ways); 239 } 240 } 159 private final HashSet<UndirectedWaySegment> waySegments = new HashSet<>(); 160 private final List<Way> ways = new ArrayList<>(); 161 private final List<Relation> relations = new ArrayList<>(); 162 private final Map<String, String> tags; 163 private final OsmPrimitive basePrimitive; 241 164 242 public static class AssembledMultipolygon { 243 public AssembledPolygon outerWay; 244 public List<AssembledPolygon> innerWays; 245 246 public AssembledMultipolygon(AssembledPolygon way) { 247 outerWay = way; 248 innerWays = new ArrayList<>(); 165 JoinableArea(Way way) throws JoinAreasException { 166 this(way, Collections.singleton(way), Collections.emptyList()); 249 167 } 250 }251 168 252 /** 253 * This hepler class implements algorithm traversing trough connected ways. 254 * Assumes you are going in clockwise orientation. 255 * @author viesturs 256 */ 257 private static class WayTraverser { 258 259 /** Set of {@link WayInPolygon} to be joined by walk algorithm */ 260 private final List<WayInPolygon> availableWays; 261 /** Current state of walk algorithm */ 262 private WayInPolygon lastWay; 263 /** Direction of current way */ 264 private boolean lastWayReverse; 265 266 /** Constructor 267 * @param ways available ways 268 */ 269 WayTraverser(Collection<WayInPolygon> ways) { 270 availableWays = new ArrayList<>(ways); 271 lastWay = null; 169 JoinableArea(Relation relation) throws JoinAreasException { 170 this(relation, getMembers(relation, "outer"), getMembers(relation, "inner")); 171 relations.add(relation); 272 172 } 273 173 274 174 /** 275 * Remove ways from available ways 276 * @param ways Collection of WayInPolygon 175 * Creates a new joinable area. 176 * @param base The primitive this area is for. 177 * @param outer The ways that should be outer ways. 178 * @param inner The ways that should be inner ways. 179 * @throws JoinAreasException If the area is invalid 277 180 */ 278 public void removeWays(Collection<WayInPolygon> ways) { 279 availableWays.removeAll(ways); 280 } 181 JoinableArea(OsmPrimitive base, Collection<Way> outer, Collection<Way> inner) throws JoinAreasException { 182 basePrimitive = base; 183 tags = new HashMap<>(base.getInterestingTags()); 184 tags.remove("type", "multipolygon"); 281 185 282 /** 283 * Remove a single way from available ways 284 * @param way WayInPolygon 285 */ 286 public void removeWay(WayInPolygon way) { 287 availableWays.remove(way); 288 } 186 try { 187 for (Way o : outer) { 188 addWayForceNonintersecting(o); 189 } 190 Pair<Node, Node> outerGap = findGap(); 191 if (outerGap != null) { 192 throw new UnclosedAreaException(outerGap); 193 } 289 194 290 /** 291 * Reset walk algorithm to a new start point 292 * @param way New start point 293 */ 294 public void setStartWay(WayInPolygon way) { 295 lastWay = way; 296 lastWayReverse = !way.insideToTheRight; 195 for (Way i : inner) { 196 addWayForceNonintersecting(i); 197 } 198 Pair<Node, Node> innerGap = findGap(); 199 if (innerGap != null) { 200 throw new UnclosedAreaException(innerGap); 201 } 202 } catch (RuntimeException e) { 203 throw BugReport.intercept(e).put("outer", outer).put("inner", inner); 204 } 297 205 } 298 206 299 207 /** 300 * Reset walk algorithm to a new start point.301 * @return The new start point or null if no available way remains208 * Check if this area is a valid closed area 209 * @return The gap if there is one, null for closed areas. 302 210 */ 303 public WayInPolygon startNewWay() { 304 if (availableWays.isEmpty()) { 305 lastWay = null; 306 } else { 307 lastWay = availableWays.iterator().next(); 308 lastWayReverse = !lastWay.insideToTheRight; 211 private Pair<Node, Node> findGap() { 212 HashSet<UndirectedWaySegment> leftOver = new HashSet<>(waySegments); 213 while (!leftOver.isEmpty()) { 214 LinkedList<Node> part = removeOutlinePart(leftOver); 215 if (part.getFirst() != part.getLast()) { 216 return new Pair<>(part.getFirst(), part.getLast()); 217 } 309 218 } 310 311 return lastWay; 219 return null; 312 220 } 313 221 314 222 /** 315 * Walking through {@link WayInPolygon} segments, head node is the current position 316 * @return Head node 223 * Add a new Way to the outline (outer or inner) of this area. 224 * @param way The way. 225 * @throws SelfIntersectingAreaException If the way self-intersects 317 226 */ 318 private Node getHeadNode() { 319 return !lastWayReverse ? lastWay.way.lastNode() : lastWay.way.firstNode(); 227 private void addWayForceNonintersecting(Way way) throws SelfIntersectingAreaException { 228 for (Pair<Node, Node> pair : way.getNodePairs(false)) { 229 this.addWayForceNonintersecting(new UndirectedWaySegment(pair.a, pair.b)); 230 } 231 ways .add(way); 320 232 } 321 233 322 /** 323 * Node just before head node. 324 * @return Previous node 325 */ 326 private Node getPrevNode() { 327 return !lastWayReverse ? lastWay.way.getNode(lastWay.way.getNodesCount() - 2) : lastWay.way.getNode(1); 328 } 329 330 /** 331 * Returns oriented angle (N1N2, N1N3) in range [0; 2*Math.PI[ 332 * @param n1 first node 333 * @param n2 second node 334 * @param n3 third node 335 * @return oriented angle (N1N2, N1N3) in range [0; 2*Math.PI[ 336 */ 337 private static double getAngle(Node n1, Node n2, Node n3) { 338 EastNorth en1 = n1.getEastNorth(); 339 EastNorth en2 = n2.getEastNorth(); 340 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; 234 private void addWayForceNonintersecting(UndirectedWaySegment s) throws SelfIntersectingAreaException { 235 if (waySegments.contains(s)) { 236 // We add a way segment twice. This means that the outline of the area contains this segment twice. 237 // This cancels out, so we remove the segment, 238 waySegments.remove(s); 239 } else { 240 // Now check for intersections 241 Optional<UndirectedWaySegment> intersection = waySegments.stream().filter(s::intersects).findAny(); 242 if (intersection.isPresent()) { 243 throw new SelfIntersectingAreaException(new Pair<>(intersection.get(), s)); 244 } 245 waySegments.add(s); 345 246 } 346 while (angle < 0) {347 angle += 2*Math.PI;348 }349 return angle;350 247 } 351 248 352 /** 353 * Get the next way creating a clockwise path, ensure it is the most right way. #7959 354 * @return The next way. 355 */ 356 public WayInPolygon walk() { 357 Node headNode = getHeadNode(); 358 Node prevNode = getPrevNode(); 359 360 double headAngle = Math.atan2(headNode.getEastNorth().east() - prevNode.getEastNorth().east(), 361 headNode.getEastNorth().north() - prevNode.getEastNorth().north()); 362 363 // Pairs of (way, nextNode) 364 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)))) 371 372 // now find the way with the best angle 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); 387 lastWayReverse = lastWay != null && !lastWay.insideToTheRight; 388 return lastWay; 249 private static Collection<Way> getMembers(Relation relation, String role) { 250 return relation.getMembers().stream().filter(m -> role.equals(m.getRole())) 251 .filter(m -> OsmPrimitiveType.WAY.equals(m.getType())).map(m -> m.getWay()) 252 .collect(Collectors.toList()); 389 253 } 390 254 391 255 /** 392 * Search for an other way coming to the same head node at left side from last way. #9951 393 * @return left way or null if none found 256 * Check if the area contains a segment. 257 * @param segment The segment. Assumed to not intersect any of our borders. 258 * @return true if the segment is inside. False if it is on the outline or outside. 394 259 */ 395 public WayInPolygon leftComingWay() { 396 Node headNode = getHeadNode(); 397 Node prevNode = getPrevNode(); 398 399 WayInPolygon mostLeft = null; // most left way connected to head node 400 boolean comingToHead = false; // true if candidate come to head node 401 double angle = 2*Math.PI; 402 403 for (WayInPolygon candidateWay : availableWays) { 404 boolean candidateComingToHead; 405 Node candidatePrevNode; 406 407 if (candidateWay.way.firstNode().equals(headNode)) { 408 candidateComingToHead = !candidateWay.insideToTheRight; 409 candidatePrevNode = candidateWay.way.getNode(1); 410 } else if (candidateWay.way.lastNode().equals(headNode)) { 411 candidateComingToHead = candidateWay.insideToTheRight; 412 candidatePrevNode = candidateWay.way.getNode(candidateWay.way.getNodesCount() - 2); 413 } else 414 continue; 415 if (candidateComingToHead && candidateWay.equals(lastWay)) 416 continue; 417 418 double candidateAngle = getAngle(headNode, candidatePrevNode, prevNode); 419 420 if (mostLeft == null || candidateAngle < angle || (Utils.equalsEpsilon(candidateAngle, angle) && !candidateComingToHead)) { 421 // Candidate is most left 422 mostLeft = candidateWay; 423 comingToHead = candidateComingToHead; 424 angle = candidateAngle; 425 } 260 public boolean contains(UndirectedWaySegment segment) { 261 if (waySegments.contains(segment)) { 262 return false; 426 263 } 264 // 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. 265 // Computation is done in East/North space. 266 // We use a ray at a fixed yRay coordinate that ends at xRay; 267 // we need to make sure this ray does not go into the same direction the way is going. 268 // This is done by rotating by 90° if we need to. 427 269 428 return comingToHead ? mostLeft : null; 429 } 270 int intersections = 0; 271 // Use some "random" start point on the segment 272 EastNorth rayNode1 = segment.a.getEastNorth(); 273 EastNorth rayNode2 = segment.b.getEastNorth(); 274 EastNorth rayFrom = rayNode1.getCenter(rayNode2); 430 275 431 @Override 432 public String toString() { 433 return "WayTraverser [availableWays=" + availableWays + ", lastWay=" + lastWay + ", lastWayReverse=" 434 + lastWayReverse + "]"; 435 } 436 } 437 438 /** 439 * Helper storage class for finding findOuterWays 440 * @author viesturs 441 */ 442 static class PolygonLevel { 443 public final int level; 444 public final AssembledMultipolygon pol; 445 446 PolygonLevel(AssembledMultipolygon pol, int level) { 447 this.pol = pol; 448 this.level = level; 449 } 450 } 451 452 /** 453 * Constructs a new {@code JoinAreasAction}. 454 */ 455 public JoinAreasAction() { 456 this(true); 457 } 458 459 /** 460 * Constructs a new {@code JoinAreasAction} with optional shortcut. 461 * @param addShortcut controls whether the shortcut should be registered or not 462 * @since 11611 463 */ 464 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); 468 } 469 470 /** 471 * Gets called whenever the shortcut is pressed or the menu entry is selected. 472 * Checks whether the selected objects are suitable to join and joins them if so. 473 */ 474 @Override 475 public void actionPerformed(ActionEvent e) { 476 join(Main.getLayerManager().getEditDataSet().getSelectedWays()); 477 } 478 479 /** 480 * Joins the given ways. 481 * @param ways Ways to join 482 * @since 7534 483 */ 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(); 492 return; 493 } 494 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; 276 // Now find the x/y mapping function. We need to ensure that rayNode1->rayNode2 is not parallel to our x axis. 277 ToDoubleFunction<EastNorth> x; 278 ToDoubleFunction<EastNorth> y; 279 if (Math.abs(rayNode1.east() - rayNode2.east()) < Math.abs(rayNode1.north() - rayNode2.north())) { 280 x = en -> en.east(); 281 y = en -> en.north(); 282 } else { 283 x = en -> -en.north(); 284 y = en -> en.east(); 503 285 } 504 286 505 allNodes.addAll(way.getNodes());506 }287 double xRay = x.applyAsDouble(rayFrom); 288 double yRay = y.applyAsDouble(rayFrom); 507 289 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; 290 for (UndirectedWaySegment part : waySegments) { 291 // intersect against all way segments 292 EastNorth n1 = part.a.getEastNorth(); 293 EastNorth n2 = part.b.getEastNorth(); 294 if ((rayNode1.equals(n1) && rayNode2.equals(n2)) || (rayNode2.equals(n1) && rayNode1.equals(n2))) { 295 // This is the segment we are starting the ray from. 296 // We ignore this to avoid rounding errors. 297 continue; 298 } 519 299 520 //analyze multipolygon relations and collect all areas 521 List<Multipolygon> areas = collectMultipolygons(ways); 300 double x1 = x.applyAsDouble(n1); 301 double x2 = x.applyAsDouble(n2); 302 double y1 = y.applyAsDouble(n1); 303 double y2 = y.applyAsDouble(n2); 522 304 523 if (areas == null) 524 //too complex multipolygon relations found 525 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 } 534 535 if (!resolveTagConflicts(areas)) 536 return; 537 //user canceled, do nothing. 538 539 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(); 544 545 // Do the job of joining areas 546 JoinAreasResult result = joinAreas(areas); 547 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)); 305 if (!((y1 <= yRay && yRay < y2) || (y2 <= yRay && yRay < y1))) { 306 // No intersection, since segment is above/below ray 307 continue; 553 308 } 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); 309 double xIntersect = x1 + (x2 - x1) * (yRay - y1) / (y2 - y1); 310 double onLine = xIntersect / xRay; 311 if (Math.abs(onLine - 1) < 1e-10) { 312 // Lines that are directly on each other are considered outside. 313 return false; 560 314 } 561 if ( ds != null) {562 ds.setSelected(allWays);315 if (xIntersect < xRay) { 316 intersections++; 563 317 } 564 } else {565 new Notification(566 tr("No intersection found. Nothing was changed."))567 .setIcon(JOptionPane.INFORMATION_MESSAGE)568 .show();569 318 } 570 } catch (UserCancelException exception) {571 Main.trace(exception);572 //revert changes573 //FIXME: this is dirty hack574 makeCommitsOneAction(tr("Reverting changes"));575 Main.main.undoRedo.undo();576 Main.main.undoRedo.redoCommands.clear();577 }578 }579 319 580 /** 581 * Tests if the areas have some intersections to join. 582 * @param areas Areas to test 583 * @return {@code true} if areas are joinable 584 */ 585 private boolean testJoin(List<Multipolygon> areas) { 586 List<Way> allStartingWays = new ArrayList<>(); 587 588 for (Multipolygon area : areas) { 589 allStartingWays.add(area.outerWay); 590 allStartingWays.addAll(area.innerWays); 320 return intersections % 2 == 1; 591 321 } 592 322 593 //find intersection points 594 Set<Node> nodes = Geometry.addIntersections(allStartingWays, true, cmds); 595 return !nodes.isEmpty(); 596 } 597 598 private static class DuplicateWayCollectorAccu { 599 private List<Way> currentWays = new ArrayList<>(); 600 private List<Way> duplicatesFound = new ArrayList<>(); 601 602 private void add(Way way) { 603 List<Node> wayNodes = way.getNodes(); 604 List<Node> wayNodesReversed = way.getNodes(); 605 Collections.reverse(wayNodesReversed); 606 Optional<Way> duplicate = currentWays.stream() 607 .filter(current -> current.getNodes().equals(wayNodes) || current.getNodes().equals(wayNodesReversed)) 608 .findFirst(); 609 if (duplicate.isPresent()) { 610 currentWays.remove(duplicate.get()); 611 duplicatesFound.add(duplicate.get()); 612 duplicatesFound.add(way); 613 } else { 614 currentWays.add(way); 615 } 616 } 617 618 private DuplicateWayCollectorAccu combine(DuplicateWayCollectorAccu a2) { 619 duplicatesFound.addAll(a2.duplicatesFound); 620 a2.currentWays.forEach(this::add); 621 return this; 622 } 623 } 624 625 /** 626 * A collector that collects to a list of duplicated way pairs. 627 * 628 * It does not scale well (O(n²)), but the data base should be small enough to make this efficient. 629 * 630 * @author Michael Zangl 631 */ 632 private static class DuplicateWayCollector implements Collector<Way, DuplicateWayCollectorAccu, List<Way>> { 633 @Override 634 public Supplier<DuplicateWayCollectorAccu> supplier() { 635 return DuplicateWayCollectorAccu::new; 323 public Collection<UndirectedWaySegment> getSegments() { 324 return Collections.unmodifiableCollection(waySegments); 636 325 } 637 638 @Override639 public BiConsumer<DuplicateWayCollectorAccu, Way> accumulator() {640 return DuplicateWayCollectorAccu::add;641 }642 643 @Override644 public BinaryOperator<DuplicateWayCollectorAccu> combiner() {645 return DuplicateWayCollectorAccu::combine;646 }647 648 @Override649 public Function<DuplicateWayCollectorAccu, List<Way>> finisher() {650 return a -> a.duplicatesFound;651 }652 653 @Override654 public Set<Collector.Characteristics> characteristics() {655 return EnumSet.of(Collector.Characteristics.UNORDERED);656 }657 658 326 } 659 327 660 328 /** 661 * Will join two or more overlapping areas 662 * @param areas list of areas to join 663 * @return new area formed. 664 * @throws UserCancelException if user cancels the operation 329 * A hash set with an xor method. 330 * @param <T> element type 665 331 */ 666 public JoinAreasResult joinAreas(List<Multipolygon> areas) throws UserCancelException { 667 668 boolean hasChanges = false; 669 670 List<Way> allStartingWays = new ArrayList<>(); 671 List<Way> innerStartingWays = new ArrayList<>(); 672 List<Way> outerStartingWays = new ArrayList<>(); 673 674 for (Multipolygon area : areas) { 675 outerStartingWays.add(area.outerWay); 676 innerStartingWays.addAll(area.innerWays); 332 private static class XOrHashSet<T> extends HashSet<T> { 333 public XOrHashSet() { 334 super(); 677 335 } 678 336 679 allStartingWays.addAll(innerStartingWays); 680 allStartingWays.addAll(outerStartingWays); 681 682 //first remove nodes in the same coordinate 683 boolean removedDuplicates = false; 684 removedDuplicates |= removeDuplicateNodes(allStartingWays); 685 686 if (removedDuplicates) { 687 hasChanges = true; 688 commitCommands(marktr("Removed duplicate nodes")); 337 public XOrHashSet(Collection<? extends T> c) { 338 super(c); 689 339 } 690 340 691 //find intersection points 692 Set<Node> nodes = Geometry.addIntersections(allStartingWays, false, cmds); 693 694 //no intersections, return. 695 if (nodes.isEmpty()) 696 return new JoinAreasResult(hasChanges, null); 697 commitCommands(marktr("Added node on all intersections")); 698 699 List<RelationRole> relations = new ArrayList<>(); 700 701 // Remove ways from all relations so ways can be combined/split quietly 702 for (Way way : allStartingWays) { 703 relations.addAll(removeFromAllRelations(way)); 704 } 705 706 // Don't warn now, because it will really look corrupted 707 boolean warnAboutRelations = !relations.isEmpty() && allStartingWays.size() > 1; 708 709 List<WayInPolygon> preparedWays = new ArrayList<>(); 710 711 // 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()); 716 717 // remove duplicate ways (A->B->C and C->B->A) 718 List<Way> duplicates = Stream.concat(splitOuterWays.stream(), splitInnerWays.stream()).collect(new DuplicateWayCollector()); 719 720 splitOuterWays.removeAll(duplicates); 721 splitInnerWays.removeAll(duplicates); 722 723 preparedWays.addAll(markWayInsideSide(splitOuterWays, false)); 724 preparedWays.addAll(markWayInsideSide(splitInnerWays, true)); 725 726 // Find boundary ways 727 List<Way> discardedWays = new ArrayList<>(duplicates); 728 List<AssembledPolygon> boundaries = findBoundaryPolygons(preparedWays, discardedWays); 729 730 //find polygons 731 List<AssembledMultipolygon> preparedPolygons = findPolygons(boundaries); 732 733 //assemble final polygons 734 List<Multipolygon> polygons = new ArrayList<>(); 735 Set<Relation> relationsToDelete = new LinkedHashSet<>(); 736 737 for (AssembledMultipolygon pol : preparedPolygons) { 738 739 //create the new ways 740 Multipolygon resultPol = joinPolygon(pol); 741 742 //create multipolygon relation, if necessary. 743 RelationRole ownMultipolygonRelation = addOwnMultipolygonRelation(resultPol.innerWays); 744 745 //add back the original relations, merged with our new multipolygon relation 746 fixRelations(relations, resultPol.outerWay, ownMultipolygonRelation, relationsToDelete); 747 748 //strip tags from inner ways 749 //TODO: preserve tags on existing inner ways 750 stripTags(resultPol.innerWays); 751 752 polygons.add(resultPol); 753 } 754 755 commitCommands(marktr("Assemble new polygons")); 756 757 for (Relation rel: relationsToDelete) { 758 cmds.add(new DeleteCommand(rel)); 759 } 760 761 commitCommands(marktr("Delete relations")); 762 763 // Delete the discarded inner ways 764 if (!discardedWays.isEmpty()) { 765 Command deleteCmd = DeleteCommand.delete(Main.getLayerManager().getEditLayer(), discardedWays, true); 766 if (deleteCmd != null) { 767 cmds.add(deleteCmd); 768 commitCommands(marktr("Delete Ways that are not part of an inner multipolygon")); 341 public void xor(T e) { 342 if (!this.add(e)) { 343 this.remove(e); 769 344 } 770 345 } 771 772 makeCommitsOneAction(marktr("Joined overlapping areas"));773 774 if (warnAboutRelations) {775 new Notification(776 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();780 }781 782 return new JoinAreasResult(true, polygons);783 346 } 784 347 785 348 /** 786 * Checks if tags of two given ways differ, and presents the user a dialog to solve conflicts 787 * @param polygons ways to check 788 * @return {@code true} if all conflicts are resolved, {@code false} if conflicts remain. 349 * This class collects the areas to be joined. 789 350 */ 790 private boolean resolveTagConflicts(List<Multipolygon> polygons) { 351 static class JoinAreasCollector { 352 /** 353 * All nodes that are touched by this the geometry for algorithm. 354 */ 355 private final Collection<Node> oldTouchedNodes = new HashSet<>(); 356 /** 357 * The nodes that are added. 358 */ 359 private final List<Node> possibleNewNodes = new ArrayList<>(); 360 private final List<JoinableArea> unionOf = new ArrayList<>(); 361 /** 362 * All hash sets that may be 363 */ 364 private final XOrHashSet<UndirectedWaySegment> waySegments = new XOrHashSet<>(); 365 private final DataSet ds; 791 366 792 List<Way> ways = new ArrayList<>(); 793 794 for (Multipolygon pol : polygons) { 795 ways.add(pol.outerWay); 796 ways.addAll(pol.innerWays); 367 JoinAreasCollector(DataSet ds, Collection<? extends OsmPrimitive> waysAndRelations) throws JoinAreasException { 368 this.ds = ds; 369 Collection<JoinableArea> collectAreas = collectAreas(waysAndRelations); 370 collectAreas.forEach(this::unionWithArea); 797 371 } 798 372 799 if (ways.size() < 2) { 800 return true; 801 } 802 803 TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways); 804 try { 805 cmds.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(wayTags, ways, ways)); 806 commitCommands(marktr("Fix tag conflicts")); 807 return true; 808 } catch (UserCancelException ex) { 809 Main.trace(ex); 810 return false; 811 } 812 } 813 814 /** 815 * This method removes duplicate points (if any) from the input way. 816 * @param ways the ways to process 817 * @return {@code true} if any changes where made 818 */ 819 private boolean removeDuplicateNodes(List<Way> ways) { 820 //TODO: maybe join nodes with JoinNodesAction, rather than reconnect the ways. 821 822 Map<Node, Node> nodeMap = new TreeMap<>(new NodePositionComparator()); 823 int totalNodesRemoved = 0; 824 825 for (Way way : ways) { 826 if (way.getNodes().size() < 2) { 827 continue; 828 } 829 830 int nodesRemoved = 0; 831 List<Node> newNodes = new ArrayList<>(); 832 Node prevNode = null; 833 834 for (Node node : way.getNodes()) { 835 if (!nodeMap.containsKey(node)) { 836 //new node 837 nodeMap.put(node, node); 838 839 //avoid duplicate nodes 840 if (prevNode != node) { 841 newNodes.add(node); 842 } else { 843 nodesRemoved++; 844 } 845 } else { 846 //node with same coordinates already exists, substitute with existing node 847 Node representator = nodeMap.get(node); 848 849 if (representator != node) { 850 nodesRemoved++; 851 } 852 853 //avoid duplicate node 854 if (prevNode != representator) { 855 newNodes.add(representator); 856 } 373 private static Collection<JoinableArea> collectAreas(Collection<? extends OsmPrimitive> waysAndRelations) throws JoinAreasException { 374 Collection<JoinableArea> areas = new ArrayList<>(); 375 for(OsmPrimitive osm : waysAndRelations) { 376 if (osm instanceof Way) { 377 areas.add(new JoinableArea((Way) osm)); 378 } else if (osm instanceof Relation) { 379 areas.add(new JoinableArea((Relation) osm)); 857 380 } 858 prevNode = node;859 381 } 860 861 if (nodesRemoved > 0) { 862 863 if (newNodes.size() == 1) { //all nodes in the same coordinate - add one more node, to have closed way. 864 newNodes.add(newNodes.get(0)); 865 } 866 867 Way newWay = new Way(way); 868 newWay.setNodes(newNodes); 869 cmds.add(new ChangeCommand(way, newWay)); 870 totalNodesRemoved += nodesRemoved; 871 } 382 return areas; 872 383 } 873 384 874 return totalNodesRemoved > 0;875 }385 void unionWithArea(JoinableArea area) { 386 Collection<UndirectedWaySegment> segments = area.getSegments(); 876 387 877 /** 878 * Commits the command list with a description 879 * @param description The description of what the commands do 880 */ 881 private void commitCommands(String description) { 882 switch(cmds.size()) { 883 case 0: 884 return; 885 case 1: 886 commitCommand(cmds.getFirst()); 887 break; 888 default: 889 commitCommand(new SequenceCommand(tr(description), cmds)); 890 break; 891 } 388 segments.stream().flatMap(s -> Stream.of(s.a, s.b)).forEach(oldTouchedNodes::add); 892 389 893 cmds.clear(); 894 cmdsCount++; 895 } 390 // Our worker list. Once a way is split, it is re-added to the worker to check for more splits. 391 XOrHashSet<UndirectedWaySegment> toAdd = new XOrHashSet<>(segments); 392 while (!toAdd.isEmpty()) { 393 UndirectedWaySegment s = toAdd.iterator().next(); 394 toAdd.remove(s); 395 Optional<UndirectedWaySegment> intersects = waySegments.stream().filter(s::intersects).findAny(); 396 if (intersects.isPresent()) { 397 EastNorth intersection = s.getIntersectionPoint(intersects.get()); 398 // Now generate two segments around the intersection. 399 waySegments.remove(intersects.get()); 400 Node newNode = findOrCreateNode(intersection); 896 401 897 private static void commitCommand(Command c) { 898 if (Main.main != null) { 899 Main.main.undoRedo.add(c); 900 } else { 901 c.executeCommand(); 902 } 903 } 904 905 /** 906 * This method analyzes the way and assigns each part what direction polygon "inside" is. 907 * @param parts the split parts of the way 908 * @param isInner - if true, reverts the direction (for multipolygon islands) 909 * @return list of parts, marked with the inside orientation. 910 * @throws IllegalArgumentException if parts is empty or not circular 911 */ 912 private static List<WayInPolygon> markWayInsideSide(List<Way> parts, boolean isInner) { 913 // the data is prepared so that all ways are split at possible intersection points. 914 // 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. 915 // Computation is done in East/North space. 916 // We use a ray at a fixed yRay coordinate that ends at xRay; 917 // we need to make sure this ray does not go into the same direction the way is going. 918 // This is done by rotating by 90° if we need to. 919 920 return parts.stream().map(way -> { 921 int intersections = 0; 922 // Use some random start point on the way 923 EastNorth rayNode1 = way.getNode(0).getEastNorth(); 924 EastNorth rayNode2 = way.getNode(1).getEastNorth(); 925 EastNorth rayFrom = rayNode1.getCenter(rayNode2); 926 927 // Now find the x/y mapping function. We need to ensure that rayNode1->rayNode2 is not parallel to our x axis. 928 ToDoubleFunction<EastNorth> x; 929 ToDoubleFunction<EastNorth> y; 930 if (Math.abs(rayNode1.east() - rayNode2.east()) < Math.abs(rayNode1.north() - rayNode2.north())) { 931 x = en -> en.east(); 932 y = en -> en.north(); 933 } else { 934 x = en -> -en.north(); 935 y = en -> en.east(); 936 } 937 938 double xRay = x.applyAsDouble(rayFrom); 939 double yRay = y.applyAsDouble(rayFrom); 940 941 for (Way part : parts) { 942 // intersect against all way segments 943 for (int i = 0; i < part.getNodesCount() - 1; i++) { 944 EastNorth n1 = part.getNode(i).getEastNorth(); 945 EastNorth n2 = part.getNode(i + 1).getEastNorth(); 946 if ((rayNode1.equals(n1) && rayNode2.equals(n2)) || (rayNode2.equals(n1) && rayNode1.equals(n2))) { 947 // This is the segment we are starting the ray from. 948 // We ignore this to avoid rounding errors. 949 continue; 402 // it may be that newNode is one of the end nodes 403 if (newNode != intersects.get().a) { 404 // We use xor here to fix ways that e.g. reverse on themselves. 405 waySegments.xor(new UndirectedWaySegment(intersects.get().a, newNode)); 950 406 } 951 952 double x1 = x.applyAsDouble(n1); 953 double x2 = x.applyAsDouble(n2); 954 double y1 = y.applyAsDouble(n1); 955 double y2 = y.applyAsDouble(n2); 956 957 if (!((y1 <= yRay && yRay < y2) || (y2 <= yRay && yRay < y1))) { 958 // No intersection, since segment is above/below ray 959 continue; 407 if (newNode != s.a) { 408 toAdd.xor(new UndirectedWaySegment(s.a, newNode)); 960 409 } 961 double xIntersect = x1 + (x2 - x1) * (yRay - y1) / (y2 - y1); 962 if (xIntersect < xRay) { 963 intersections++; 410 if (newNode != intersects.get().b) { 411 waySegments.xor(new UndirectedWaySegment(newNode, intersects.get().b)); 964 412 } 413 if (newNode != s.b) { 414 toAdd.xor(new UndirectedWaySegment(newNode, s.b)); 415 } 416 } else { 417 // No more intersections - we add that segment to our geometry 418 waySegments.xor(s); 965 419 } 966 420 } 967 421 968 return new WayInPolygon(way, (intersections % 2 == 0) ^ isInner ^ (y.applyAsDouble(rayNode1) > yRay)); 969 }).collect(Collectors.toList()); 970 } 971 972 /** 973 * This is a method that splits way into smaller parts, using the prepared nodes list as split points. 974 * Uses {@link SplitWayAction#splitWay} for the heavy lifting. 975 * @param way way to split 976 * @param nodes split points 977 * @return list of split ways (or original ways if no splitting is done). 978 */ 979 private List<Way> splitWayOnNodes(Way way, Set<Node> nodes) { 980 981 List<Way> result = new ArrayList<>(); 982 List<List<Node>> chunks = buildNodeChunks(way, nodes); 983 984 if (chunks.size() > 1) { 985 SplitWayResult split = SplitWayAction.splitWay(getLayerManager().getEditLayer(), way, chunks, 986 Collections.<OsmPrimitive>emptyList(), SplitWayAction.Strategy.keepFirstChunk()); 987 988 if (split != null) { 989 //execute the command, we need the results 990 cmds.add(split.getCommand()); 991 commitCommands(marktr("Split ways into fragments")); 992 993 result.add(split.getOriginalWay()); 994 result.addAll(split.getNewWays()); 995 } 422 unionOf.add(area); 996 423 } 997 if (result.isEmpty()) {998 //nothing to split999 result.add(way);1000 }1001 424 1002 return result; 1003 } 1004 1005 /** 1006 * Simple chunking version. Does not care about circular ways and result being 1007 * proper, we will glue it all back together later on. 1008 * @param way the way to chunk 1009 * @param splitNodes the places where to cut. 1010 * @return list of node paths to produce. 1011 */ 1012 private static List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) { 1013 List<List<Node>> result = new ArrayList<>(); 1014 List<Node> curList = new ArrayList<>(); 1015 1016 for (Node node : way.getNodes()) { 1017 curList.add(node); 1018 if (curList.size() > 1 && splitNodes.contains(node)) { 1019 result.add(curList); 1020 curList = new ArrayList<>(); 1021 curList.add(node); 1022 } 425 /** 426 * Find a node close to newNode to handle intersections of 3 or more lines. 427 * @param intersection The position of the node 428 * @return A node. 429 */ 430 Node findOrCreateNode(EastNorth intersection) { 431 return Stream.concat(oldTouchedNodes.stream(), possibleNewNodes.stream()) 432 .filter(node -> node.getEastNorth().distanceSq(intersection) < 1e-40) 433 .findAny() 434 .orElseGet(() -> this.createNode(intersection)); 1023 435 } 1024 436 1025 if (curList.size() > 1) { 1026 result.add(curList); 437 private Node createNode(EastNorth intersection) { 438 Node newNode = new Node(intersection); 439 possibleNewNodes.add(newNode); 440 return newNode; 1027 441 } 1028 442 1029 return result; 1030 } 443 /** 444 * Gets the outlines of this area 445 * @return The outline polygons as Even/Odd area. Not all nodes need to be contained in the data set. 446 */ 447 List<List<Node>> getOutlines() { 448 Collection<UndirectedWaySegment> outline = computeOutline(); 1031 449 1032 /** 1033 * This method finds which ways are outer and which are inner. 1034 * @param boundaries list of joined boundaries to search in 1035 * @return outer ways 1036 */ 1037 private static List<AssembledMultipolygon> findPolygons(Collection<AssembledPolygon> boundaries) { 1038 1039 List<PolygonLevel> list = findOuterWaysImpl(0, boundaries); 1040 List<AssembledMultipolygon> result = new ArrayList<>(); 1041 1042 //take every other level 1043 for (PolygonLevel pol : list) { 1044 if (pol.level % 2 == 0) { 1045 result.add(pol.pol); 450 ArrayList<List<Node>> res = new ArrayList<>(); 451 while (!outline.isEmpty()) { 452 res.add(removeOutlinePart(outline)); 1046 453 } 454 return res; 1047 455 } 1048 456 1049 return result; 1050 } 457 /** 458 * Gets the commands that are required to join the areas. 459 * @return The join commands. 460 */ 461 List<Command> getCommands() { 462 if (unionOf.isEmpty()) { 463 return Collections.emptyList(); 464 } 465 Collection<UndirectedWaySegment> outline = computeOutline(); 1051 466 1052 /** 1053 * Collects outer way and corresponding inner ways from all boundaries. 1054 * @param level depth level 1055 * @param boundaryWays list of joined boundaries to search in 1056 * @return the outermost Way. 1057 */ 1058 private static List<PolygonLevel> findOuterWaysImpl(int level, Collection<AssembledPolygon> boundaryWays) { 467 List<Command> commands = new ArrayList<>(); 468 // The primitives of which we should remove the tags. 469 List<OsmPrimitive> toRemoveTags = new ArrayList<>(); 470 unionOf.stream().map(area -> area.basePrimitive).forEach(toRemoveTags::add); 1059 471 1060 //TODO: bad performance for deep nestings... 1061 List<PolygonLevel> result = new ArrayList<>(); 472 // Add the split nodes 473 // Remove nodes of interior segments. 474 possibleNewNodes.stream() 475 .filter(n -> outline.stream().filter(w -> w.hasEnd(n)).findAny().isPresent()) 476 .map(n -> new AddCommand(ds, n)) 477 .forEach(commands::add); 1062 478 1063 for (AssembledPolygon outerWay : boundaryWays) { 1064 1065 boolean outerGood = true; 1066 List<AssembledPolygon> innerCandidates = new ArrayList<>(); 1067 1068 for (AssembledPolygon innerWay : boundaryWays) { 1069 if (innerWay == outerWay) { 479 // Now search all ways which are completely used in our new geometry (e.g. multipolygon inners, ...) 480 // We should not change those ways. 481 List<Way> outlineWays = new ArrayList<>(); 482 List<UndirectedWaySegment> segmentsToContain = new ArrayList<>(outline); 483 for (Way preserve : findOutlinesToPreserve(segmentsToContain)) { 484 List<UndirectedWaySegment> preservedSegments = segmentsForWay(preserve); 485 if (preservedSegments.size() != preservedSegments.stream().distinct().count()) { 486 // This way contains a segment twice. Skip it, we want to fix this. 1070 487 continue; 1071 488 } 1072 1073 if (wayInsideWay(outerWay, innerWay)) { 1074 outerGood = false; 1075 break; 1076 } else if (wayInsideWay(innerWay, outerWay)) { 1077 innerCandidates.add(innerWay); 489 if (!segmentsToContain.containsAll(preservedSegments)) { 490 // it may happen that two outlines that should be preserved happen to be on the same segment 491 // We need to ignore the second one then. 492 continue; 1078 493 } 494 outlineWays.add(preserve); 495 segmentsToContain.removeAll(preservedSegments); 1079 496 } 1080 497 1081 if (!outerGood) { 1082 continue; 1083 } 498 // Multipolygons that were selected and can now be removed 499 List<Relation> relationsToDelete = unionOf.stream().flatMap(area -> area.relations.stream()) 500 .distinct().collect(Collectors.toList()); 501 toRemoveTags.removeAll(relationsToDelete); 1084 502 1085 //add new outer polygon 1086 AssembledMultipolygon pol = new AssembledMultipolygon(outerWay); 1087 PolygonLevel polLev = new PolygonLevel(pol, level); 503 // Compute the ways that need to be removed. 504 // Those are all ways of the old geometry that are not used in any other place. 505 List<Way> waysToDelete = unionOf.stream().flatMap(area -> area.ways.stream()) 506 .distinct() 507 .filter(way -> !outlineWays.contains(way)) 508 // Preserve ways that are member in any relation that we did not modify 509 .filter(way -> way.getReferrers().stream().allMatch(relationsToDelete::contains)) 510 // Preserve ways that have tags 511 .filter(way -> toRemoveTags.contains(way) || way.getInterestingTags().isEmpty()) 512 .collect(Collectors.toList()); 513 toRemoveTags.removeAll(waysToDelete); 1088 514 1089 //process inner ways 1090 if (!innerCandidates.isEmpty()) { 1091 List<PolygonLevel> innerList = findOuterWaysImpl(level + 1, innerCandidates); 1092 result.addAll(innerList); 515 // Now we are left with the remaining outline in the segmentsToContain array. 516 // For each chunk in that outline, we create a new way 517 // TODO: We can reuse the ways we would delete otherwise. 518 while (!segmentsToContain.isEmpty()) { 519 List<Node> wayToCreate = removeOutlinePart(segmentsToContain); 520 Way osm = new Way(); 521 osm.setNodes(wayToCreate); 522 outlineWays.add(osm); 523 commands.add(new AddCommand(ds, osm)); 524 } 1093 525 1094 for (PolygonLevel pl : innerList) { 1095 if (pl.level == level + 1) { 1096 pol.innerWays.add(pl.pol.outerWay); 1097 } 526 OsmPrimitive resultPrimitive; 527 // Now it is time to generate the final area. 528 if (outlineWays.isEmpty()) { 529 throw new AssertionError("No outline ways found."); 530 } else if (outlineWays.size() == 1) { 531 // We only have one way. Add the tags to that way. 532 resultPrimitive = outlineWays.get(0); 533 } else { 534 // find a relation. Use the more complex multipolygon when merging two of them. 535 Relation multipolygon = relationsToDelete.stream().sorted(Comparator.comparingInt(r -> -r.getMembersCount())) 536 .findFirst().orElseGet(Relation::new); 537 Pair<Relation, Relation> update = CreateMultipolygonAction.updateMultipolygonRelation(outlineWays, multipolygon); 538 if (update == null) { 539 throw new AssertionError("The outline ways should be continuous but no multipolygon could be created."); 1098 540 } 541 if (update.a.getDataSet() == null) { 542 // used the fake relation. 543 commands.add(new AddCommand(ds, update.b)); 544 } else { 545 commands.add(new ChangeCommand(ds, update.a, update.b)); 546 } 547 resultPrimitive = multipolygon; 548 relationsToDelete.remove(multipolygon); 1099 549 } 1100 550 1101 result.add(polLev); 1102 } 551 // We check the tags now. 552 try { 553 List<OsmPrimitive> baseTagged = unionOf.stream().map(area -> area.basePrimitive).collect(Collectors.toList()); 554 TagCollection tags = TagCollection.unionOfAllPrimitives(baseTagged); 555 commands.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(tags, baseTagged, Collections.singleton(resultPrimitive))); 556 } catch (UserCancelException ex) { 557 // User aborted. This is simple, since we did not commit anything. 558 Main.trace(ex); 559 return Collections.emptyList(); 560 } 1103 561 1104 return result; 1105 } 1106 1107 /** 1108 * Finds all ways that form inner or outer boundaries. 1109 * @param multigonWays A list of (splitted) ways that form a multigon and share common end nodes on intersections. 1110 * @param discardedResult this list is filled with ways that are to be discarded 1111 * @return A list of ways that form the outer and inner boundaries of the multigon. 1112 */ 1113 public static List<AssembledPolygon> findBoundaryPolygons(Collection<WayInPolygon> multigonWays, 1114 List<Way> discardedResult) { 1115 // In multigonWays collection, some way are just a point (i.e. way like nodeA-nodeA) 1116 // This seems to appear when is apply over invalid way like #9911 test-case 1117 // Remove all of these way to make the next work. 1118 List<WayInPolygon> cleanMultigonWays = multigonWays.stream() 1119 .filter(way -> way.way.getNodesCount() != 2 || !way.way.isClosed()) 1120 .collect(Collectors.toList()); 1121 WayTraverser traverser = new WayTraverser(cleanMultigonWays); 1122 List<AssembledPolygon> result = new ArrayList<>(); 1123 1124 try { 1125 WayInPolygon startWay; 1126 while ((startWay = traverser.startNewWay()) != null) { 1127 findBoundaryPolygonsStartingWith(discardedResult, traverser, result, startWay); 562 // Apply deletion of the primitives we don't need any more. 563 if (!relationsToDelete.isEmpty()) { 564 commands.add(new DeleteCommand(ds, relationsToDelete)); 1128 565 } 1129 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) { 1130 throw BugReport.intercept(t).put("traverser", traverser); 1131 } 566 if (!waysToDelete.isEmpty()) { 567 commands.add(new DeleteCommand(ds, waysToDelete)); 1132 568 1133 return fixTouchingPolygons(result);1134 }1135 569 1136 private static void findBoundaryPolygonsStartingWith(List<Way> discardedResult, WayTraverser traverser, List<AssembledPolygon> result, 1137 WayInPolygon startWay) { 1138 List<WayInPolygon> path = new ArrayList<>(); 1139 List<WayInPolygon> startWays = new ArrayList<>(); 1140 try { 1141 path.add(startWay); 1142 while (true) { 1143 WayInPolygon leftComing = traverser.leftComingWay(); 1144 if (leftComing != null && !startWays.contains(leftComing)) { 1145 // Need restart traverser walk 1146 path.clear(); 1147 path.add(leftComing); 1148 traverser.setStartWay(leftComing); 1149 startWays.add(leftComing); 570 Collection<Node> nodesToDelete = oldTouchedNodes.stream() 571 .filter(n -> !n.isTagged()) 572 // don't delete nodes that are part of the new outline 573 .filter(n -> outlineWays.stream().allMatch(w -> !w.containsNode(n))) 574 .filter(n -> n.getReferrers().stream().allMatch(r -> waysToDelete.contains(r) || r.isDeleted())) 575 .collect(Collectors.toList()); 576 if (!nodesToDelete.isEmpty()) { 577 commands.add(new DeleteCommand(ds, nodesToDelete)); 1150 578 } 1151 WayInPolygon nextWay = traverser.walk(); 1152 if (nextWay == null) { 1153 throw new JosmRuntimeException("Join areas internal error: traverser could not find a next way."); 579 } 580 for(OsmPrimitive osm : toRemoveTags) { 581 for (String key : osm.getKeys().keySet()) { 582 commands.add(new ChangePropertyCommand(osm, key, "")); 1154 583 } 1155 if (path.get(0) == nextWay) {1156 // path is closed -> stop here1157 AssembledPolygon ring = new AssembledPolygon(path);1158 if (ring.getNodes().size() <= 2) {1159 // Invalid ring (2 nodes) -> remove1160 traverser.removeWays(path);1161 for (WayInPolygon way: path) {1162 discardedResult.add(way.way);1163 }1164 } else {1165 // Close ring -> add1166 result.add(ring);1167 traverser.removeWays(path);1168 }1169 break;1170 }1171 if (path.contains(nextWay)) {1172 // Inner loop -> remove1173 int index = path.indexOf(nextWay);1174 while (path.size() > index) {1175 WayInPolygon currentWay = path.get(index);1176 discardedResult.add(currentWay.way);1177 traverser.removeWay(currentWay);1178 path.remove(index);1179 }1180 traverser.setStartWay(path.get(index-1));1181 } else {1182 path.add(nextWay);1183 }1184 584 } 1185 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) { 1186 throw BugReport.intercept(t).put("path", path);585 586 return commands; 1187 587 } 1188 }1189 588 1190 /** 1191 * This method checks if polygons have several touching parts and splits them in several polygons. 1192 * @param polygons the polygons to process. 1193 * @return the resulting list of polygons 1194 */ 1195 public static List<AssembledPolygon> fixTouchingPolygons(List<AssembledPolygon> polygons) { 1196 List<AssembledPolygon> newPolygons = new ArrayList<>(); 1197 1198 for (AssembledPolygon ring : polygons) { 1199 ring.reverse(); 1200 WayTraverser traverser = new WayTraverser(ring.ways); 1201 WayInPolygon startWay; 1202 1203 while ((startWay = traverser.startNewWay()) != null) { 1204 List<WayInPolygon> simpleRingWays = new ArrayList<>(); 1205 simpleRingWays.add(startWay); 1206 WayInPolygon nextWay; 1207 while ((nextWay = traverser.walk()) != startWay) { 1208 if (nextWay == null) 1209 throw new JosmRuntimeException("Join areas internal error."); 1210 simpleRingWays.add(nextWay); 1211 } 1212 traverser.removeWays(simpleRingWays); 1213 AssembledPolygon simpleRing = new AssembledPolygon(simpleRingWays); 1214 simpleRing.reverse(); 1215 newPolygons.add(simpleRing); 1216 } 589 private List<UndirectedWaySegment> segmentsForWay(Way way) { 590 return way.getNodePairs(false).stream() 591 .map(pair -> new UndirectedWaySegment(pair.a, pair.b)).collect(Collectors.toList()); 1217 592 } 1218 593 1219 return newPolygons; 1220 } 1221 1222 /** 1223 * Tests if way is inside other way 1224 * @param outside outer polygon description 1225 * @param inside inner polygon description 1226 * @return {@code true} if inner is inside outer 1227 */ 1228 public static boolean wayInsideWay(AssembledPolygon inside, AssembledPolygon outside) { 1229 Set<Node> outsideNodes = new HashSet<>(outside.getNodes()); 1230 List<Node> insideNodes = inside.getNodes(); 1231 1232 for (Node insideNode : insideNodes) { 1233 1234 if (!outsideNodes.contains(insideNode)) 1235 //simply test the one node 1236 return Geometry.nodeInsidePolygon(insideNode, outside.getNodes()); 594 private List<Way> findOutlinesToPreserve(List<UndirectedWaySegment> segmentsToContain) { 595 return unionOf.stream().flatMap(u -> u.ways.stream()) 596 .filter(w -> segmentsToContain.containsAll(segmentsForWay(w))).collect(Collectors.toList()); 1237 597 } 1238 598 1239 //all nodes shared. 1240 return false; 1241 } 1242 1243 /** 1244 * Joins the lists of ways. 1245 * @param polygon The list of outer ways that belong to that multipolygon. 1246 * @return The newly created outer way 1247 * @throws UserCancelException if user cancels the operation 1248 */ 1249 private Multipolygon joinPolygon(AssembledMultipolygon polygon) throws UserCancelException { 1250 Multipolygon result = new Multipolygon(joinWays(polygon.outerWay.ways)); 1251 1252 for (AssembledPolygon pol : polygon.innerWays) { 1253 result.innerWays.add(joinWays(pol.ways)); 599 private Collection<UndirectedWaySegment> computeOutline() { 600 return waySegments.stream().filter( 601 seg -> unionOf.stream().noneMatch(area -> area.contains(seg)) 602 ).collect(Collectors.toList()); 1254 603 } 1255 1256 return result;1257 604 } 1258 605 1259 606 /** 1260 * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway. 1261 * @param ways The list of outer ways that belong to that multigon. 1262 * @return The newly created outer way 1263 * @throws UserCancelException if user cancels the operation 607 * Remove a continuous part of the segments 608 * @param segments The segments that may be removed 609 * @return A list of the end nodes of the segments. It is either a closed loop or a segment as long as possible. 1264 610 */ 1265 private Way joinWays(List<WayInPolygon> ways) throws UserCancelException { 1266 1267 //leave original orientation, if all paths are reverse. 1268 boolean allReverse = true; 1269 for (WayInPolygon way : ways) { 1270 allReverse &= !way.insideToTheRight; 611 private static LinkedList<Node> removeOutlinePart(Collection<UndirectedWaySegment> segments) { 612 Node start = segments.iterator().next().a; 613 LinkedList<Node> nodes = new LinkedList<>(); 614 nodes.add(start); 615 // Move in one direction on that way. 616 while ((nodes.size() < 2 || nodes.getFirst() != nodes.getLast()) && !segments.isEmpty()) { 617 Optional<UndirectedWaySegment> traverse = segments.stream().filter(s -> s.hasEnd(nodes.getLast())).findAny(); 618 if (!traverse.isPresent()) { 619 break; 620 } 621 segments.remove(traverse.get()); 622 nodes.addLast(traverse.get().getOtherEnd(nodes.getLast())); 1271 623 } 1272 624 1273 if (allReverse) { 1274 for (WayInPolygon way : ways) { 1275 way.insideToTheRight = !way.insideToTheRight; 625 // Now move in the other direction - as far as we can go. 626 while ((nodes.size() < 2 || nodes.getFirst() != nodes.getLast()) && !segments.isEmpty()) { 627 Optional<UndirectedWaySegment> traverse = segments.stream().filter(s -> s.hasEnd(nodes.getFirst())).findAny(); 628 if (!traverse.isPresent()) { 629 break; 1276 630 } 631 segments.remove(traverse.get()); 632 nodes.addFirst(traverse.get().getOtherEnd(nodes.getFirst())); 1277 633 } 1278 634 1279 Way joinedWay = joinOrientedWays(ways); 1280 1281 //should not happen 1282 if (joinedWay == null || !joinedWay.isClosed()) 1283 throw new JosmRuntimeException("Join areas internal error."); 1284 1285 return joinedWay; 635 return nodes; 1286 636 } 1287 637 1288 638 /** 1289 * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath) 1290 * @param ways The list of ways to join and reverse 1291 * @return The newly created way 1292 * @throws UserCancelException if user cancels the operation 639 * Constructs a new {@code JoinAreasAction}. 1293 640 */ 1294 private Way joinOrientedWays(List<WayInPolygon> ways) throws UserCancelException { 1295 if (ways.size() < 2) 1296 return ways.get(0).way; 1297 1298 // This will turn ways so all of them point in the same direction and CombineAction won't bug 1299 // the user about this. 1300 1301 //TODO: ReverseWay and Combine way are really slow and we use them a lot here. This slows down large joins. 1302 List<Way> actionWays = new ArrayList<>(ways.size()); 1303 1304 for (WayInPolygon way : ways) { 1305 actionWays.add(way.way); 1306 1307 if (!way.insideToTheRight) { 1308 ReverseWayResult res = ReverseWayAction.reverseWay(way.way); 1309 commitCommand(res.getReverseCommand()); 1310 cmdsCount++; 1311 } 1312 } 1313 1314 Pair<Way, Command> result = CombineWayAction.combineWaysWorker(actionWays); 1315 1316 commitCommand(result.b); 1317 cmdsCount++; 1318 1319 return result.a; 641 public JoinAreasAction() { 642 this(true); 1320 643 } 1321 644 1322 645 /** 1323 * This method analyzes multipolygon relationships of given ways and collects addition inner ways to consider.1324 * @param selectedWays the selected ways1325 * @ return list of polygons, or null if too complex relation encountered.646 * Constructs a new {@code JoinAreasAction} with optional shortcut. 647 * @param addShortcut controls whether the shortcut should be registered or not 648 * @since 11611 1326 649 */ 1327 public static List<Multipolygon> collectMultipolygons(Collection<Way> selectedWays) { 1328 1329 List<Multipolygon> result = new ArrayList<>(); 1330 1331 //prepare the lists, to minimize memory allocation. 1332 List<Way> outerWays = new ArrayList<>(); 1333 List<Way> innerWays = new ArrayList<>(); 1334 1335 Set<Way> processedOuterWays = new LinkedHashSet<>(); 1336 Set<Way> processedInnerWays = new LinkedHashSet<>(); 1337 1338 for (Relation r : OsmPrimitive.getParentRelations(selectedWays)) { 1339 if (r.isDeleted() || !r.isMultipolygon()) { 1340 continue; 1341 } 1342 1343 boolean hasKnownOuter = false; 1344 outerWays.clear(); 1345 innerWays.clear(); 1346 1347 for (RelationMember rm : r.getMembers()) { 1348 if ("outer".equalsIgnoreCase(rm.getRole())) { 1349 outerWays.add(rm.getWay()); 1350 hasKnownOuter |= selectedWays.contains(rm.getWay()); 1351 } else if ("inner".equalsIgnoreCase(rm.getRole())) { 1352 innerWays.add(rm.getWay()); 1353 } 1354 } 1355 1356 if (!hasKnownOuter) { 1357 continue; 1358 } 1359 1360 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(); 1365 return null; 1366 } 1367 1368 Way outerWay = outerWays.get(0); 1369 1370 //retain only selected inner ways 1371 innerWays.retainAll(selectedWays); 1372 1373 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(); 1378 return null; 1379 } 1380 1381 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(); 1386 return null; 1387 } 1388 1389 for (Way way :innerWays) { 1390 if (processedOuterWays.contains(way)) { 1391 new Notification( 1392 tr("Sorry. Cannot handle way that is both inner and outer in multipolygon relations.")) 1393 .setIcon(JOptionPane.INFORMATION_MESSAGE) 1394 .show(); 1395 return null; 1396 } 1397 1398 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(); 1403 return null; 1404 } 1405 } 1406 1407 processedOuterWays.add(outerWay); 1408 processedInnerWays.addAll(innerWays); 1409 1410 Multipolygon pol = new Multipolygon(outerWay); 1411 pol.innerWays.addAll(innerWays); 1412 1413 result.add(pol); 1414 } 1415 1416 //add remaining ways, not in relations 1417 for (Way way : selectedWays) { 1418 if (processedOuterWays.contains(way) || processedInnerWays.contains(way)) { 1419 continue; 1420 } 1421 1422 result.add(new Multipolygon(way)); 1423 } 1424 1425 return result; 650 public JoinAreasAction(boolean addShortcut) { 651 super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"), 652 addShortcut ? Shortcut.registerShortcut("tools:joinareas", 653 tr("Tool: {0}", tr("Join overlapping Areas")), KeyEvent.VK_J, Shortcut.SHIFT) : null, 654 true); 1426 655 } 1427 656 1428 657 /** 1429 * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations 1430 * @param inner List of already closed inner ways 1431 * @return The list of relation with roles to add own relation to 658 * Gets called whenever the shortcut is pressed or the menu entry is selected. 659 * Checks whether the selected objects are suitable to join and joins them if so. 1432 660 */ 1433 private RelationRole addOwnMultipolygonRelation(Collection<Way> inner) { 1434 if (inner.isEmpty()) return null; 1435 OsmDataLayer layer = Main.getLayerManager().getEditLayer(); 1436 // Create new multipolygon relation and add all inner ways to it 1437 Relation newRel = new Relation(); 1438 newRel.put("type", "multipolygon"); 1439 for (Way w : inner) { 1440 newRel.addMember(new RelationMember("inner", w)); 1441 } 1442 cmds.add(layer != null ? new AddCommand(layer, newRel) : 1443 new AddCommand(inner.iterator().next().getDataSet(), newRel)); 1444 addedRelations.add(newRel); 1445 1446 // We don't add outer to the relation because it will be handed to fixRelations() 1447 // which will then do the remaining work. 1448 return new RelationRole(newRel, "outer"); 661 @Override 662 public void actionPerformed(ActionEvent e) { 663 join(Main.getLayerManager().getEditDataSet().getSelected()); 1449 664 } 1450 665 1451 666 /** 1452 * Removes a given OsmPrimitive from all relations.1453 * @param osm Element to remove from all relations1454 * @ return List of relations with roles the primitives was part of667 * Joins the given ways. 668 * @param waysAndRelations Ways / Multipolygons to join 669 * @since 7534 1455 670 */ 1456 private List<RelationRole> removeFromAllRelations(OsmPrimitive osm) { 1457 List<RelationRole> result = new ArrayList<>(); 671 public void join(Collection<? extends OsmPrimitive> waysAndRelations) { 672 waysAndRelations = waysAndRelations.stream() 673 .filter(osm -> (osm instanceof Way && ((Way) osm).isClosed()) || osm.isMultipolygon()) 674 .collect(Collectors.toList()); 675 if (waysAndRelations.isEmpty()) { 676 new Notification(tr("Please select at least one closed area that should be joined.")) 677 .setIcon(JOptionPane.INFORMATION_MESSAGE).show(); 678 return; 679 } 1458 680 1459 for (Relation r : osm.getDataSet().getRelations()) { 1460 if (r.isDeleted()) { 1461 continue; 1462 } 1463 for (RelationMember rm : r.getMembers()) { 1464 if (rm.getMember() != osm) { 1465 continue; 1466 } 681 if (!ofSameDataset(waysAndRelations)) { 682 throw new IllegalArgumentException("Not in same DataSet"); 683 } 684 waysAndRelations = selectRelationsInsteadOfMembers(waysAndRelations); 685 DataSet ds = waysAndRelations.iterator().next().getDataSet(); 1467 686 1468 Relation newRel = new Relation(r); 1469 List<RelationMember> members = newRel.getMembers(); 1470 members.remove(rm); 1471 newRel.setMembers(members); 1472 1473 cmds.add(new ChangeCommand(r, newRel)); 1474 RelationRole saverel = new RelationRole(r, rm.getRole()); 1475 if (!result.contains(saverel)) { 1476 result.add(saverel); 1477 } 1478 break; 1479 } 687 if (!Command.checkAndConfirmOutlyingOperation("joinarea", tr("Join area confirmation"), 688 trn("The selected area has nodes outside of the downloaded data region.", 689 "The selected areas have nodes outside of the downloaded data region.", waysAndRelations.size()) + "<br/>" 690 + tr("This can lead to nodes being deleted accidentally.") + "<br/>" 691 + tr("Are you really sure to continue?") + tr("Please abort if you are not sure"), 692 tr("The selected area is incomplete. Continue?"), waysAndRelations, null)) { 693 return; 1480 694 } 1481 695 1482 commitCommands(marktr("Removed Element from Relations")); 1483 return result; 1484 } 696 try { 697 JoinAreasCollector collector = new JoinAreasCollector(ds, waysAndRelations); 1485 698 1486 /** 1487 * Adds the previously removed relations again to the outer way. If there are multiple multipolygon 1488 * relations where the joined areas were in "outer" role a new relation is created instead with all 1489 * members of both. This function depends on multigon relations to be valid already, it won't fix them. 1490 * @param rels List of relations with roles the (original) ways were part of 1491 * @param outer The newly created outer area/way 1492 * @param ownMultipol elements to directly add as outer 1493 * @param relationsToDelete set of relations to delete. 1494 */ 1495 private void fixRelations(List<RelationRole> rels, Way outer, RelationRole ownMultipol, Set<Relation> relationsToDelete) { 1496 List<RelationRole> multiouters = new ArrayList<>(); 699 List<Command> commands = collector.getCommands(); 700 Main.main.undoRedo.add(new SequenceCommand(tr("Join Areas"), commands)); 1497 701 1498 if (ownMultipol != null) { 1499 multiouters.add(ownMultipol); 1500 } 1501 1502 for (RelationRole r : rels) { 1503 if (r.rel.isMultipolygon() && "outer".equalsIgnoreCase(r.role)) { 1504 multiouters.add(r); 1505 continue; 1506 } 1507 // Add it back! 1508 Relation newRel = new Relation(r.rel); 1509 newRel.addMember(new RelationMember(r.role, outer)); 1510 cmds.add(new ChangeCommand(r.rel, newRel)); 1511 } 1512 1513 OsmDataLayer layer = Main.getLayerManager().getEditLayer(); 1514 Relation newRel; 1515 switch (multiouters.size()) { 1516 case 0: 702 } catch (UnclosedAreaException e) { 703 new Notification(tr("One of the selected areas is not closed and therefore cannot be joined.")) 704 .setIcon(JOptionPane.INFORMATION_MESSAGE).show(); 1517 705 return; 1518 case 1: 1519 // Found only one to be part of a multipolygon relation, so just add it back as well 1520 newRel = new Relation(multiouters.get(0).rel); 1521 newRel.addMember(new RelationMember(multiouters.get(0).role, outer)); 1522 cmds.add(new ChangeCommand(multiouters.get(0).rel, newRel)); 706 } catch (JoinAreasException e) { 707 new Notification(tr("One of the selected areas has an invalid geomerty.")) 708 .setIcon(JOptionPane.INFORMATION_MESSAGE).show(); 1523 709 return; 1524 default:1525 // Create a new relation with all previous members and (Way)outer as outer.1526 newRel = new Relation();1527 for (RelationRole r : multiouters) {1528 // Add members1529 for (RelationMember rm : r.rel.getMembers()) {1530 if (!newRel.getMembers().contains(rm)) {1531 newRel.addMember(rm);1532 }1533 }1534 // Add tags1535 for (String key : r.rel.keySet()) {1536 newRel.put(key, r.rel.get(key));1537 }1538 // Delete old relation1539 relationsToDelete.add(r.rel);1540 }1541 newRel.addMember(new RelationMember("outer", outer));1542 cmds.add(layer != null ? new AddCommand(layer, newRel) : new AddCommand(outer.getDataSet(), newRel));1543 710 } 1544 711 } 1545 712 1546 713 /** 1547 * Remove all tags from the all the way 1548 * @param ways The List of Ways to remove all tags from 714 * If all members of a multipolygon are selected, ask the user to select the polygon instead of the ways. 715 * @param currentSelection 716 * @return The new list of primitives the user selected 1549 717 */ 1550 private void stripTags(Collection<Way> ways) { 1551 for (Way w : ways) { 1552 final Way wayWithoutTags = new Way(w); 1553 wayWithoutTags.removeAll(); 1554 cmds.add(new ChangeCommand(w, wayWithoutTags)); 1555 } 1556 /* I18N: current action printed in status display */ 1557 commitCommands(marktr("Remove tags from inner ways")); 1558 } 718 private Collection<? extends OsmPrimitive> selectRelationsInsteadOfMembers(Collection<? extends OsmPrimitive> currentSelection) { 719 List<Relation> selectableMultipolygons = currentSelection.stream() 720 .filter(osm -> osm.getType() == OsmPrimitiveType.WAY) 721 // Get all multipolygons referred by the way 722 .flatMap(osm -> osm.getReferrers().stream()) 723 .distinct() 724 .filter(osm -> osm.isMultipolygon()) 725 .map(osm -> ((Relation) osm)) 726 // Filter for those that are completely selected 727 .filter(r -> r.getMembers().stream().map(m -> m.getMember()).allMatch(currentSelection::contains)) 728 .collect(Collectors.toList()); 1559 729 1560 /** 1561 * Takes the last cmdsCount actions back and combines them into a single action 1562 * (for when the user wants to undo the join action) 1563 * @param message The commit message to display 1564 */ 1565 private void makeCommitsOneAction(String message) { 1566 cmds.clear(); 1567 if (Main.main != null) { 1568 UndoRedoHandler ur = Main.main.undoRedo; 1569 int i = Math.max(ur.commands.size() - cmdsCount, 0); 1570 for (; i < ur.commands.size(); i++) { 1571 cmds.add(ur.commands.get(i)); 730 if (!selectableMultipolygons.isEmpty()) { 731 JPanel msg = new JPanel(new GridBagLayout()); 732 msg.add(new JMultilineLabel("<html>" + 733 tr("You selected the members of the following multipolygons. " 734 + "Do you want to join the polygons instead?") 735 + "<ul>" + selectableMultipolygons.stream() 736 .map(r -> "<li>" + r.getDisplayName(DefaultNameFormatter.getInstance()) + "</li>") 737 .collect(Collectors.joining()) 738 + "</ul></html>")); 739 boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog( 740 "join_areas_on_polygons", 741 Main.parent, 742 msg, 743 tr("Join multipolygons?"), 744 JOptionPane.YES_NO_OPTION, 745 JOptionPane.QUESTION_MESSAGE, 746 JOptionPane.YES_OPTION); 747 if (answer) { 748 // the new polygons 749 HashSet<OsmPrimitive> select = new HashSet<>(selectableMultipolygons); 750 // part of the selection that was untouched 751 currentSelection.stream().filter( 752 w -> !(w instanceof Way && ((Way) w).getReferrers().stream().allMatch(selectableMultipolygons::contains)) 753 ).forEach(select::add); 754 return select; 1572 755 } 1573 1574 for (i = 0; i < cmds.size(); i++) {1575 ur.undo();1576 }1577 756 } 757 return currentSelection; 758 } 1578 759 1579 commitCommands(message == null ? marktr("Join Areas Function") : message);1580 cmdsCount = 0;760 private boolean ofSameDataset(Collection<? extends OsmPrimitive> waysAndRelations) { 761 return waysAndRelations.stream().map(OsmPrimitive::getDataSet).distinct().count() <= 1; 1581 762 } 1582 763 764 public static List<List<Node>> getOutline(DataSet data, Collection<? extends OsmPrimitive> primitives) throws JoinAreasException { 765 return new JoinAreasCollector(data, primitives).getOutlines(); 766 } 767 1583 768 @Override 1584 769 protected void updateEnabledState() { 1585 770 updateEnabledStateOnCurrentSelection(); -
src/org/openstreetmap/josm/actions/search/SearchAction.java
525 525 * @param mode the search mode to use 526 526 */ 527 527 public static void search(String search, SearchMode mode) { 528 final SearchSetting searchSetting = new SearchSetting(); 529 searchSetting.text = search; 530 searchSetting.mode = mode; 531 search(searchSetting); 528 search(new SearchSetting(search, mode)); 532 529 } 533 530 534 531 static void search(SearchSetting s) { … … 544 541 * @since 10457 545 542 */ 546 543 public static Collection<OsmPrimitive> searchAndReturn(String search, SearchMode mode) { 547 final SearchSetting searchSetting = new SearchSetting();548 searchSetting.text = search;549 searchSetting.mode = mode;550 544 CapturingSearchReceiver receiver = new CapturingSearchReceiver(); 551 SearchTask.newSearchTask( searchSetting, receiver).run();545 SearchTask.newSearchTask(new SearchSetting(search, mode), receiver).run(); 552 546 return receiver.result; 553 547 } 554 548 555 549 /** 550 * Performs the search specified by the search string {@code search} and the search mode {@code mode} and returns the result of the search. 551 * 552 * @param search the search string to use 553 * @param ds The dataset to search in 554 * @param mode the search mode to use 555 * @return The result of the search. 556 * @since xxx 557 */ 558 public static Collection<OsmPrimitive> searchAndReturn(String search, DataSet ds, SearchMode mode) { 559 CapturingSearchReceiver receiver = new CapturingSearchReceiver(); 560 SearchTask.newSearchTask(new SearchSetting(search, mode), ds, receiver).run(); 561 return receiver.result; 562 } 563 564 /** 556 565 * Interfaces implementing this may receive the result of the current search. 557 566 * @author Michael Zangl 558 567 * @since 10457 … … 735 744 * Constructs a new {@code SearchSetting}. 736 745 */ 737 746 public SearchSetting() { 738 text = ""; 739 mode = SearchMode.replace; 747 this("", SearchMode.replace); 740 748 } 741 749 742 750 /** 751 * Constructs a new {@code SearchSetting} using the given text / mode 752 * @param text The search definition 753 * @param mode The search mode. 754 */ 755 public SearchSetting(String text, SearchMode mode) { 756 this.text = Objects.requireNonNull(text, "text"); 757 this.mode = Objects.requireNonNull(mode, "mode"); 758 } 759 760 /** 743 761 * Constructs a new {@code SearchSetting} from an existing one. 744 762 * @param original original search settings 745 763 */ -
src/org/openstreetmap/josm/tools/RightAndLefthandTraffic.java
10 10 import java.io.PrintWriter; 11 11 import java.io.Writer; 12 12 import java.nio.charset.StandardCharsets; 13 import java.util.ArrayList;14 13 import java.util.Collection; 15 14 import java.util.Collections; 16 15 import java.util.List; 17 16 import java.util.Set; 17 import java.util.stream.Collectors; 18 import java.util.stream.Stream; 18 19 19 20 import org.openstreetmap.josm.Main; 20 21 import org.openstreetmap.josm.actions.JoinAreasAction; 21 import org.openstreetmap.josm.actions.JoinAreasAction.JoinAreasResult; 22 import org.openstreetmap.josm.actions.JoinAreasAction.Multipolygon; 23 import org.openstreetmap.josm.actions.PurgeAction; 22 import org.openstreetmap.josm.actions.JoinAreasAction.JoinAreasException; 24 23 import org.openstreetmap.josm.data.coor.LatLon; 25 24 import org.openstreetmap.josm.data.osm.DataSet; 25 import org.openstreetmap.josm.data.osm.Node; 26 26 import org.openstreetmap.josm.data.osm.OsmPrimitive; 27 27 import org.openstreetmap.josm.data.osm.Relation; 28 import org.openstreetmap.josm.data.osm.RelationMember;29 28 import org.openstreetmap.josm.data.osm.Way; 30 29 import org.openstreetmap.josm.io.IllegalDataException; 31 30 import org.openstreetmap.josm.io.OsmReader; … … 62 61 * TODO: Synchronization can be refined inside the {@link GeoPropertyIndex} as most look-ups are read-only. 63 62 */ 64 63 public static synchronized void initialize() { 65 Collection<Way>optimizedWays = loadOptimizedBoundaries();66 if (optimizedWays. isEmpty()) {64 DataSet optimizedWays = loadOptimizedBoundaries(); 65 if (optimizedWays.getWays().isEmpty()) { 67 66 optimizedWays = computeOptimizedBoundaries(); 68 67 saveOptimizedBoundaries(optimizedWays); 69 68 } 70 rlCache = new GeoPropertyIndex<>(new DefaultGeoProperty(optimizedWays ), 24);69 rlCache = new GeoPropertyIndex<>(new DefaultGeoProperty(optimizedWays.getWays()), 24); 71 70 } 72 71 73 private static Collection<Way> computeOptimizedBoundaries() { 74 Collection<Way> ways = new ArrayList<>(); 75 Collection<OsmPrimitive> toPurge = new ArrayList<>(); 72 private static DataSet computeOptimizedBoundaries() { 76 73 // Find all outer ways of left-driving countries. Many of them are adjacent (African and Asian states) 77 74 DataSet data = Territories.getDataSet(); 78 Collection<Relation> allRelations = data.getRelations(); 79 Collection<Way> allWays = data.getWays(); 80 for (Way w : allWays) { 81 if (LEFT.equals(w.get(DRIVING_SIDE))) { 82 addWayIfNotInner(ways, w); 83 } 75 Collection<OsmPrimitive> leftHandTraffic = data.getPrimitives(osm -> (osm instanceof Way || osm.isMultipolygon()) && LEFT.equals(osm.get(DRIVING_SIDE))); 76 77 List<List<Node>> outline; 78 try { 79 outline = JoinAreasAction.getOutline(data, leftHandTraffic); 80 } catch (JoinAreasException e) { 81 // This should not happen. 82 Main.warn(e); 83 84 outline = leftHandTraffic.stream() 85 .flatMap(RightAndLefthandTraffic::generateSimpleOutline) 86 .map(Way::getNodes) 87 .collect(Collectors.toList()); 84 88 } 85 for (Relation r : allRelations) { 86 if (r.isMultipolygon() && LEFT.equals(r.get(DRIVING_SIDE))) { 87 for (RelationMember rm : r.getMembers()) { 88 if (rm.isWay() && "outer".equals(rm.getRole()) && !RIGHT.equals(rm.getMember().get(DRIVING_SIDE))) { 89 addWayIfNotInner(ways, (Way) rm.getMember()); 90 } 91 } 89 90 // Remove all ways that are not part of the outline. 91 data.getWays().forEach(data::removePrimitive); 92 data.getRelations().forEach(data::removePrimitive); 93 outline.stream().forEach(nodes -> generateWay(nodes, data)); 94 data.getNodes().stream().filter(n -> n.getReferrers().isEmpty()).forEach(data::removePrimitive); 95 96 return data; 97 } 98 99 private static void generateWay(List<Node> nodes, DataSet data) { 100 for (Node node : nodes) { 101 // Don't use stream, we can have dupplicate nodes. 102 if (node.getDataSet() == null) { 103 data.addPrimitive(node); 92 104 } 93 105 } 94 toPurge.addAll(allRelations); 95 toPurge.addAll(allWays); 96 toPurge.removeAll(ways); 97 // Remove ways from parent relations for following optimizations 98 for (Relation r : OsmPrimitive.getParentRelations(ways)) { 99 r.setMembers(null); 106 Way w = new Way(); 107 w.setNodes(nodes); 108 data.addPrimitive(w); 109 } 110 111 /** 112 * A dirty way to get all left hand traffic outlines. 113 * @param osm The primitives 114 * @return The collection of outline ways. 115 */ 116 private static Stream<Way> generateSimpleOutline(OsmPrimitive osm) { 117 if (osm instanceof Way) { 118 return Stream.of((Way) osm); 119 } else { 120 return ((Relation) osm) 121 .getMembers() 122 .stream() 123 .filter(m -> "outer".equals(m.getRole())) 124 .map(m -> (Way) m.getMember()); 100 125 } 101 // Remove all tags to avoid any conflict102 for (Way w : ways) {103 w.removeAll();104 }105 // Purge all other ways and relations so dataset only contains lefthand traffic data106 new PurgeAction(false).getPurgeCommand(toPurge).executeCommand();107 // Combine adjacent countries into a single polygon108 Collection<Way> optimizedWays = new ArrayList<>();109 List<Multipolygon> areas = JoinAreasAction.collectMultipolygons(ways);110 if (areas != null) {111 try {112 JoinAreasResult result = new JoinAreasAction(false).joinAreas(areas);113 if (result.hasChanges()) {114 for (Multipolygon mp : result.getPolygons()) {115 optimizedWays.add(mp.getOuterWay());116 }117 }118 } catch (UserCancelException ex) {119 Main.warn(ex);120 } catch (JosmRuntimeException ex) {121 // Workaround to #10511 / #14185. To remove when #10511 is solved122 Main.error(ex);123 }124 }125 if (optimizedWays.isEmpty()) {126 // Problem: don't optimize127 Main.warn("Unable to join left-driving countries polygons");128 optimizedWays.addAll(ways);129 }130 return optimizedWays;131 126 } 132 127 133 128 /** … … 150 145 ways.add(w); 151 146 } 152 147 153 private static void saveOptimizedBoundaries(Collection<Way> optimizedWays) { 154 DataSet ds = optimizedWays.iterator().next().getDataSet(); 148 private static void saveOptimizedBoundaries(DataSet ds) { 155 149 File file = new File(Main.pref.getCacheDirectory(), "left-right-hand-traffic.osm"); 156 150 try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8); 157 151 OsmWriter w = OsmWriterFactory.createOsmWriter(new PrintWriter(writer), false, ds.getVersion()) … … 164 158 } 165 159 } 166 160 167 private static Collection<Way>loadOptimizedBoundaries() {161 private static DataSet loadOptimizedBoundaries() { 168 162 try (InputStream is = new FileInputStream(new File(Main.pref.getCacheDirectory(), "left-right-hand-traffic.osm"))) { 169 return OsmReader.parseDataSet(is, null) .getWays();163 return OsmReader.parseDataSet(is, null); 170 164 } catch (IllegalDataException | IOException ex) { 171 165 Main.trace(ex); 172 return Collections.emptyList();166 return new DataSet(); 173 167 } 174 168 } 175 169 }