Ticket #5179: osm-join-areas-6.2.patch
File osm-join-areas-6.2.patch, 74.7 KB (added by , 15 years ago) |
---|
-
.classpath
1 <?xml version="1.0" encoding="UTF-8"?> 2 <classpath> 3 <classpathentry kind="src" path="src"/> 4 <classpathentry kind="src" path="test/unit"/> 5 <classpathentry kind="src" path="test/functional"/> 6 <classpathentry excluding="build/|data_nodist/|dist/|doc/|lib/|macosx/|src/|test/|test/build/|test/functional/|test/performance/|test/unit/|tools/|utils/" kind="src" path=""/> 7 <classpathentry kind="src" path="test/performance"/> 8 <classpathentry kind="lib" path="lib/metadata-extractor-2.3.1-nosun.jar"/> 9 <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/> 10 <classpathentry kind="lib" path="test/lib/fest/debug-1.0.jar"/> 11 <classpathentry kind="lib" path="test/lib/fest/fest-assert-1.0.jar"/> 12 <classpathentry kind="lib" path="test/lib/fest/fest-reflect-1.1.jar"/> 13 <classpathentry kind="lib" path="test/lib/fest/fest-swing-1.1.jar"/> 14 <classpathentry kind="lib" path="test/lib/fest/fest-util-1.0.jar"/> 15 <classpathentry kind="lib" path="test/lib/fest/jcip-annotations-1.0.jar"/> 16 <classpathentry kind="lib" path="test/lib/fest/MRJToolkitStubs-1.0.jar"/> 17 <classpathentry kind="lib" path="test/lib/jfcunit.jar"/> 18 <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER /org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JDK 6"/>19 <classpathentry exported="true" kind="con" path="GROOVY_SUPPORT"/> 20 <classpathentry kind="lib" path="lib/signpost-core-1.2.1.1.jar"/> 21 <classpathentry kind="output" path="bin"/> 22 </classpath> 1 <?xml version="1.0" encoding="UTF-8"?> 2 <classpath> 3 <classpathentry kind="src" path="src"/> 4 <classpathentry kind="src" path="test/unit"/> 5 <classpathentry kind="src" path="test/functional"/> 6 <classpathentry excluding="build/|data_nodist/|dist/|doc/|lib/|macosx/|src/|test/|test/build/|test/functional/|test/performance/|test/unit/|tools/|utils/" kind="src" path=""/> 7 <classpathentry kind="src" path="test/performance"/> 8 <classpathentry kind="lib" path="lib/metadata-extractor-2.3.1-nosun.jar"/> 9 <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/> 10 <classpathentry kind="lib" path="test/lib/fest/debug-1.0.jar"/> 11 <classpathentry kind="lib" path="test/lib/fest/fest-assert-1.0.jar"/> 12 <classpathentry kind="lib" path="test/lib/fest/fest-reflect-1.1.jar"/> 13 <classpathentry kind="lib" path="test/lib/fest/fest-swing-1.1.jar"/> 14 <classpathentry kind="lib" path="test/lib/fest/fest-util-1.0.jar"/> 15 <classpathentry kind="lib" path="test/lib/fest/jcip-annotations-1.0.jar"/> 16 <classpathentry kind="lib" path="test/lib/fest/MRJToolkitStubs-1.0.jar"/> 17 <classpathentry kind="lib" path="test/lib/jfcunit.jar"/> 18 <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> 19 <classpathentry exported="true" kind="con" path="GROOVY_SUPPORT"/> 20 <classpathentry kind="lib" path="lib/signpost-core-1.2.1.1.jar"/> 21 <classpathentry kind="output" path="bin"/> 22 </classpath> -
src/org/openstreetmap/josm/actions/CombineWayAction.java
99 99 * 100 100 * @return the set of referring relations 101 101 */ 102 p rotectedSet<Relation> getParentRelations(Collection<Way> ways) {102 public static Set<Relation> getParentRelations(Collection<Way> ways) { 103 103 HashSet<Relation> ret = new HashSet<Relation>(); 104 104 for (Way w: ways) { 105 105 ret.addAll(OsmPrimitive.getFilteredList(w.getReferrers(), Relation.class)); -
src/org/openstreetmap/josm/actions/JoinAreasAction.java
1 1 // License: GPL. Copyright 2007 by Immanuel Scholz and others 2 2 package org.openstreetmap.josm.actions; 3 3 4 import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.combineTigerTags; 5 import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.completeTagCollectionForEditing; 6 import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing; 4 7 import static org.openstreetmap.josm.tools.I18n.marktr; 5 8 import static org.openstreetmap.josm.tools.I18n.tr; 6 9 import static org.openstreetmap.josm.tools.I18n.trn; 7 10 8 import java.awt.GridBagLayout;9 11 import java.awt.event.ActionEvent; 10 12 import java.awt.event.KeyEvent; 11 13 import java.awt.geom.Area; … … 13 15 import java.util.ArrayList; 14 16 import java.util.Collection; 15 17 import java.util.Collections; 18 import java.util.Comparator; 16 19 import java.util.HashMap; 17 20 import java.util.HashSet; 21 import java.util.LinkedHashSet; 18 22 import java.util.LinkedList; 19 23 import java.util.List; 20 24 import java.util.Map; 21 import java.util.Map.Entry;22 25 import java.util.Set; 23 26 import java.util.TreeMap; 24 import java.util.TreeSet;25 27 26 import javax.swing.Box;27 import javax.swing.JComboBox;28 import javax.swing.JLabel;29 28 import javax.swing.JOptionPane; 30 import javax.swing.JPanel;31 29 32 30 import org.openstreetmap.josm.Main; 33 31 import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult; … … 44 42 import org.openstreetmap.josm.data.osm.OsmPrimitive; 45 43 import org.openstreetmap.josm.data.osm.Relation; 46 44 import org.openstreetmap.josm.data.osm.RelationMember; 47 import org.openstreetmap.josm.data.osm.T igerUtils;45 import org.openstreetmap.josm.data.osm.TagCollection; 48 46 import org.openstreetmap.josm.data.osm.Way; 49 import org.openstreetmap.josm.gui.ExtendedDialog; 50 import org.openstreetmap.josm.tools.GBC; 47 import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog; 51 48 import org.openstreetmap.josm.tools.Shortcut; 52 49 53 50 public class JoinAreasAction extends JosmAction { … … 55 52 private LinkedList<Command> cmds = new LinkedList<Command>(); 56 53 private int cmdsCount = 0; 57 54 55 58 56 /** 59 57 * This helper class describes join ares action result. 60 58 * @author viesturs 61 59 * 62 60 */ 63 public static class JoinAreasResult 61 public static class JoinAreasResult{ 64 62 65 public Way outerWay;66 public List<Way> innerWays;67 68 63 public boolean mergeSuccessful; 69 64 public boolean hasChanges; 70 65 public boolean hasRelationProblems; 66 67 public List<ComplexPolygon> polygons; 71 68 } 72 69 73 // HelperClass 74 // Saves a node and two positions where to insert the node into the ways 75 private static class NodeToSegs implements Comparable<NodeToSegs> { 76 public int pos; 77 public Node n; 78 public double dis; 79 public NodeToSegs(int pos, Node n, LatLon dis) { 80 this.pos = pos; 81 this.n = n; 82 this.dis = n.getCoor().greatCircleDistance(dis); 83 } 70 public static class ComplexPolygon{ 71 public Way outerWay; 72 public List<Way> innerWays; 84 73 85 public int compareTo(NodeToSegs o) { 86 if(this.pos == o.pos) 87 return (this.dis - o.dis) > 0 ? 1 : -1; 88 return this.pos - o.pos; 74 public ComplexPolygon(Way way){ 75 outerWay = way; 76 innerWays = new ArrayList<Way>(); 89 77 } 90 91 @Override92 public int hashCode() {93 return pos;94 }95 96 @Override97 public boolean equals(Object o) {98 if (o instanceof NodeToSegs)99 return compareTo((NodeToSegs) o) == 0;100 else101 return false;102 }103 78 } 104 79 105 80 // HelperClass … … 125 100 } 126 101 } 127 102 128 /** 129 * HelperClass 130 * saves a way and the "inside" side 131 * insideToTheLeft: if true left side is "in", false -right side is "in". 132 * Left and right are determined along the orientation of way. 133 */ 134 private static class WayInPath { 103 104 //HelperClass 105 //saves a way and the "inside" side 106 // insideToTheLeft: if true left side is "in", false -right side is "in". Left and right are determined along the orientation of way. 107 private static class WayInPath{ 135 108 public final Way way; 136 public boolean insideToThe Left;109 public boolean insideToTheRight; 137 110 138 public WayInPath(Way way, boolean insideLeft){139 this.way = way;140 this.insideToThe Left = insideLeft;111 public WayInPath(Way _way, boolean _insideRight){ 112 this.way = _way; 113 this.insideToTheRight = _insideRight; 141 114 } 142 115 143 116 @Override … … 147 120 148 121 @Override 149 122 public boolean equals(Object other) { 150 if (!(other instanceof WayInPath)) 151 return false; 123 if (!(other instanceof WayInPath)) return false; 152 124 WayInPath otherMember = (WayInPath) other; 153 return otherMember.way.equals(this.way) && otherMember.insideToThe Left == this.insideToTheLeft;125 return otherMember.way.equals(this.way) && otherMember.insideToTheRight == this.insideToTheRight; 154 126 } 155 127 } 156 128 129 /** 130 * This hepler class implements algorithm traversing trough connected ways. 131 * Assumes you are going in clockwise orientation. 132 * @author viesturs 133 * 134 */ 135 private static class WayTraverser { 136 137 private Set<WayInPath> availableWays; 138 private WayInPath lastWay; 139 private boolean lastWayReverse; 140 141 public WayTraverser(Collection<WayInPath> ways){ 142 143 availableWays = new HashSet<WayInPath>(ways); 144 lastWay = null; 145 } 146 147 public void removeWays(Collection<WayInPath> ways){ 148 availableWays.removeAll(ways); 149 } 150 151 public boolean hasWays(){ 152 return availableWays.size() > 0; 153 } 154 155 public WayInPath startNewWay(WayInPath way) { 156 lastWay = way; 157 lastWayReverse = !lastWay.insideToTheRight; 158 159 return lastWay; 160 } 161 162 public WayInPath startNewWay() { 163 if (availableWays.size() == 0) { 164 lastWay = null; 165 } else { 166 lastWay = availableWays.iterator().next(); 167 lastWayReverse = !lastWay.insideToTheRight; 168 } 169 170 return lastWay; 171 } 172 173 174 public WayInPath advanceNextLeftmostWay(){ 175 return advanceNextWay(false); 176 } 177 178 public WayInPath advanceNextRightmostWay(){ 179 return advanceNextWay(true); 180 } 181 182 private WayInPath advanceNextWay(boolean rightmost){ 183 184 Node headNode = !lastWayReverse ? lastWay.way.lastNode() : lastWay.way.firstNode(); 185 Node prevNode = !lastWayReverse ? lastWay.way.getNode(lastWay.way.getNodesCount() - 2) : lastWay.way.getNode(1); 186 187 //find best next way 188 WayInPath bestWay = null; 189 Node bestWayNextNode = null; 190 boolean bestWayReverse = false; 191 192 for(WayInPath way: availableWays) 193 { 194 if (way.way.firstNode().equals(headNode)){ 195 //start adjacent to headNode 196 Node nextNode = way.way.getNode(1); 197 198 if (nextNode.equals(prevNode)) 199 { 200 //this is the path we came from - ignore it. 201 } 202 else if (bestWay == null || (isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode) == rightmost) ) 203 { 204 //the new way is better 205 bestWay = way; 206 bestWayReverse = false; 207 bestWayNextNode = nextNode; 208 } 209 } 210 211 if (way.way.lastNode().equals(headNode)) 212 { 213 //end adjacent to headNode 214 Node nextNode = way.way.getNode(way.way.getNodesCount() - 2); 215 216 if (nextNode.equals(prevNode)) 217 { 218 //this is the path we came from - ignore it. 219 } 220 else if (bestWay == null || (isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode) == rightmost)) 221 { 222 //the new way is better 223 bestWay = way; 224 bestWayReverse = true; 225 bestWayNextNode = nextNode; 226 } 227 } 228 } 229 230 lastWay = bestWay; 231 lastWayReverse = bestWayReverse; 232 233 return lastWay; 234 } 235 236 public boolean isLastWayInsideToTheRight(){ 237 return lastWayReverse != lastWay.insideToTheRight; 238 } 239 } 240 241 /** 242 * Provides some node order , based on coordinates, nodes with equal coordinates are equal. 243 * @author viesturs 244 * 245 */ 246 private class NodePositionComparator implements Comparator<Node>{ 247 248 @Override 249 public int compare(Node n1, Node n2) { 250 251 double dLat = n1.getCoor().lat() - n2.getCoor().lat(); 252 double dLon = n1.getCoor().lon() - n2.getCoor().lon(); 253 254 if (dLat > 0) 255 return 1; 256 else if (dLat < 0) 257 return -1; 258 else if (dLon == 0) //dlat is 0 here 259 return 0; 260 else 261 return dLon > 0 ? 1: -1; 262 } 263 } 264 265 266 /** 267 * Helper storage class for finding findOuterWays 268 * @author viesturs 269 */ 270 static class PolygonLevel{ 271 public final int level; 272 public final ComplexPolygon pol; 273 274 public PolygonLevel(ComplexPolygon _pol, int _level){ 275 pol = _pol; 276 level = _level; 277 } 278 } 279 280 157 281 // Adds the menu entry, Shortcuts, etc. 158 282 public JoinAreasAction() { 159 283 super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"), Shortcut.registerShortcut("tools:joinareas", tr("Tool: {0}", tr("Join overlapping Areas")), … … 172 296 return; 173 297 } 174 298 175 // Too many ways176 if(ways.size() > 2) {177 JOptionPane.showMessageDialog(Main.parent, tr("Only up to two areas can be joined at the moment."));178 return;179 }180 181 299 List<Node> allNodes = new ArrayList<Node>(); 182 300 for (Way way: ways) { 183 301 if(!way.isClosed()) { … … 208 326 } 209 327 } 210 328 211 if (checkForTagConflicts(ways.getFirst(), ways.getLast())) 212 //there was conflicts and user canceled abort the action. 329 if(!resolveTagConflicts(ways)) 213 330 return; 331 //user cancelled, do nothing. 214 332 333 //collect ways and analyze multipolygon relations 334 List<ComplexPolygon> areas = new ArrayList<ComplexPolygon>(); 215 335 216 JoinAreasResult result = joinAreas(ways.getFirst(), ways.getLast()); 336 for (Way way : ways) 337 { 338 //TODO: analyze multipolygon relations... 339 areas.add(new ComplexPolygon(way)); 340 } 217 341 218 if (result.hasChanges) { 342 JoinAreasResult result = joinAreas(areas); 343 344 if (result.hasChanges){ 345 219 346 Main.map.mapView.repaint(); 220 347 DataSet ds = Main.main.getCurrentDataSet(); 221 348 ds.fireSelectionChanged(); … … 224 351 } 225 352 } 226 353 354 227 355 /** 228 * Will join two o verlapping areas229 * @param Way First way/area230 * @ param Way Second way/area356 * Will join two or more overlapping areas 357 * @param areas - list of areas to join 358 * @return new area formed. 231 359 */ 232 private JoinAreasResult joinAreas( Way a, Way b) {360 private JoinAreasResult joinAreas(List<ComplexPolygon> areas) { 233 361 234 362 JoinAreasResult result = new JoinAreasResult(); 235 363 result.hasChanges = false; 236 364 237 // Fix self-overlapping first or other errors 238 boolean same = a.equals(b); 239 if(!same) { 240 int i = 0; 365 List<Way> allStartingWays = new ArrayList<Way>(); 366 List<Way> innerStartingWays = new ArrayList<Way>(); 367 List<Way> outerStartingWays = new ArrayList<Way>(); 241 368 242 //join each area with itself, fixing self-crossings. 243 JoinAreasResult resultA = joinAreas(a, a); 244 JoinAreasResult resultB = joinAreas(b, b); 369 for(ComplexPolygon area: areas) 370 { 371 outerStartingWays.add(area.outerWay); 372 innerStartingWays.addAll(area.innerWays); 373 } 245 374 246 if (resultA.mergeSuccessful) { 247 a = resultA.outerWay; 248 ++i; 249 } 250 if(resultB.mergeSuccessful) { 251 b = resultB.outerWay; 252 ++i; 253 } 375 allStartingWays.addAll(innerStartingWays); 376 allStartingWays.addAll(outerStartingWays); 254 377 255 result.hasChanges = i > 0; 256 cmdsCount = i; 378 //first remove nodes in the same coordinate 379 boolean removedDuplicates = false; 380 removedDuplicates |= removeDuplicateNodes(allStartingWays); 381 382 if (removedDuplicates) 383 { 384 result.hasChanges = true; 385 commitCommands(marktr("Removed duplicate nodes")); 257 386 } 258 387 259 ArrayList<Node> nodes = addIntersections(a, b); 388 //find intersection points 389 ArrayList<Node> nodes = addIntersections(allStartingWays); 260 390 261 391 //no intersections, return. 262 392 if(nodes.size() == 0) return result; 263 393 commitCommands(marktr("Added node on all intersections")); 264 394 395 ArrayList<RelationRole> relations = new ArrayList<RelationRole>(); 396 265 397 // Remove ways from all relations so ways can be combined/split quietly 266 ArrayList<RelationRole> relations = removeFromRelations(a);267 if(!same){268 relations.addAll(removeFromRelations( b));398 for(Way way : allStartingWays) 399 { 400 relations.addAll(removeFromRelations(way)); 269 401 } 270 402 271 403 // Don't warn now, because it will really look corrupted 272 404 boolean warnAboutRelations = relations.size() > 0; 273 405 274 ArrayList<Way > allWays = splitWaysOnNodes(a, b, nodes);406 ArrayList<WayInPath> preparedWays = new ArrayList<WayInPath>(); 275 407 408 for (Way way: outerStartingWays){ 409 ArrayList<Way> splitWays = splitWayOnNodes(way, nodes); 410 preparedWays.addAll(markWayInsideSide(splitWays, false)); 411 } 412 413 for (Way way: innerStartingWays){ 414 ArrayList<Way> splitWays = splitWayOnNodes(way, nodes); 415 preparedWays.addAll(markWayInsideSide(splitWays, true)); 416 } 417 276 418 // Find inner ways save them to a list 277 ArrayList<WayInPath> outerWays = findOuterWays(allWays); 278 ArrayList<Way> innerWays = findInnerWays(allWays, outerWays); 419 ArrayList<Way> discardedWays = new ArrayList<Way>(); 420 List<List<WayInPath>> boundryParts = findBoundaryWays(preparedWays, discardedWays); 421 List<Way> boundaries = joinBoundaries(boundryParts); 279 422 280 // Join outer ways 281 Way outerWay = joinOuterWays(outerWays); 423 List<ComplexPolygon> polygons = findPolygons(boundaries); 282 424 283 // Fix Multipolygons if there are any 284 List<Way> newInnerWays = fixMultipolygons(innerWays, outerWay, same); 285 286 // Delete the remaining inner ways 287 if(innerWays != null && innerWays.size() > 0) { 288 cmds.add(DeleteCommand.delete(Main.map.mapView.getEditLayer(), innerWays, true)); 425 // Delete the discarded inner ways 426 if(discardedWays.size() > 0) { 427 cmds.add(DeleteCommand.delete(Main.map.mapView.getEditLayer(), discardedWays, true)); 289 428 } 290 429 commitCommands(marktr("Delete Ways that are not part of an inner multipolygon")); 291 430 292 431 // We can attach our new multipolygon relation and pretend it has always been there 293 addOwnMultigonRelation(newInnerWays, outerWay, relations); 294 fixRelations(relations, outerWay); 432 for(ComplexPolygon pol: polygons) 433 { 434 addOwnMultigonRelation(pol.innerWays, pol.outerWay, relations); 435 fixRelations(relations, pol.outerWay); 436 } 437 295 438 commitCommands(marktr("Fix relations")); 296 439 297 stripTags(newInnerWays); 440 for(ComplexPolygon pol: polygons) 441 { 442 stripTags(pol.innerWays); 443 } 298 444 299 makeCommitsOneAction( 300 same 301 ? marktr("Joined self-overlapping area") 302 : marktr("Joined overlapping areas") 303 ); 445 makeCommitsOneAction(marktr("Joined overlapping areas")); 304 446 305 447 if(warnAboutRelations) { 306 448 JOptionPane.showMessageDialog(Main.parent, tr("Some of the ways were part of relations that have been modified. Please verify no errors have been introduced.")); … … 308 450 309 451 result.hasChanges = true; 310 452 result.mergeSuccessful = true; 311 result.outerWay = outerWay; 312 result.innerWays = newInnerWays; 313 453 result.polygons = polygons; 314 454 return result; 315 455 } 316 456 … … 318 458 * Checks if tags of two given ways differ, and presents the user a dialog to solve conflicts 319 459 * @param Way First way to check 320 460 * @param Way Second Way to check 321 * @return boolean True if not all conflicts could be resolved, False if everything's fine461 * @return boolean True if all conflicts are resolved, False if conflicts remain. 322 462 */ 323 private boolean checkForTagConflicts(Way a, Way b) { 324 ArrayList<Way> ways = new ArrayList<Way>(); 325 ways.add(a); 326 ways.add(b); 463 private boolean resolveTagConflicts(List<Way> ways) { 464 //mostly copied from CombineWayAction.java. 327 465 328 // FIXME: This is mostly copied and pasted from CombineWayAction.java and one day should be moved into tools 329 // We have TagCollection handling for that now - use it here as well 330 Map<String, Set<String>> props = new TreeMap<String, Set<String>>(); 331 for (Way w : ways) { 332 for (String key: w.keySet()) { 333 if (!props.containsKey(key)) { 334 props.put(key, new TreeSet<String>()); 335 } 336 props.get(key).add(w.get(key)); 337 } 466 TagCollection wayTags = TagCollection.unionOfAllPrimitives(ways); 467 TagCollection completeWayTags = new TagCollection(wayTags); 468 combineTigerTags(completeWayTags); 469 normalizeTagCollectionBeforeEditing(completeWayTags, ways); 470 TagCollection tagsToEdit = new TagCollection(completeWayTags); 471 completeTagCollectionForEditing(tagsToEdit); 472 473 CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance(); 474 dialog.getTagConflictResolverModel().populate(tagsToEdit, completeWayTags.getKeysWithMultipleValues()); 475 dialog.setTargetPrimitive(ways.get(0)); 476 Set<Relation> parentRelations = CombineWayAction.getParentRelations(ways); 477 dialog.getRelationMemberConflictResolverModel().populate( 478 parentRelations, 479 ways 480 ); 481 dialog.prepareDefaultDecisions(); 482 483 // resolve tag conflicts if necessary 484 // 485 if (!completeWayTags.isApplicableToPrimitive() || !parentRelations.isEmpty()) { 486 dialog.setVisible(true); 487 if (dialog.isCancelled()) 488 return false; 338 489 } 339 490 340 Way ax = new Way(a); 341 Way bx = new Way(b); 491 for(Way way: ways) 492 { 493 dialog.setTargetPrimitive(way); 494 cmds.addAll(dialog.buildResolutionCommands()); 495 } 342 496 343 Map<String, JComboBox> components = new HashMap<String, JComboBox>(); 344 JPanel p = new JPanel(new GridBagLayout()); 345 for (Entry<String, Set<String>> e : props.entrySet()) { 346 if (TigerUtils.isTigerTag(e.getKey())) { 347 String combined = TigerUtils.combineTags(e.getKey(), e.getValue()); 348 ax.put(e.getKey(), combined); 349 bx.put(e.getKey(), combined); 350 } else if (e.getValue().size() > 1) { 351 if("created_by".equals(e.getKey())) 352 { 353 ax.remove("created_by"); 354 bx.remove("created_by"); 355 } else { 356 JComboBox c = new JComboBox(e.getValue().toArray()); 357 c.setEditable(true); 358 p.add(new JLabel(e.getKey()), GBC.std()); 359 p.add(Box.createHorizontalStrut(10), GBC.std()); 360 p.add(c, GBC.eol()); 361 components.put(e.getKey(), c); 497 commitCommands(marktr("Fix tag conflicts")); 498 return true; 499 } 500 501 502 /** 503 * This method removes duplicate points (if any) from the input way. 504 * @param way the way to process 505 * @return true if any changes where made 506 */ 507 private boolean removeDuplicateNodes(List<Way> ways) 508 { 509 //TODO: maybe join nodes with JoinNodesAction, rather than reconnect the ways. 510 511 Map<Node, Node> nodeMap = new TreeMap<Node, Node>(new NodePositionComparator()); 512 int totalNodesRemoved = 0; 513 514 for(Way way:ways) 515 { 516 if (way.getNodes().size() < 2) { 517 continue; 518 } 519 520 int nodesRemoved = 0; 521 List<Node> newNodes = new ArrayList<Node>(); 522 Node prevNode = null; 523 524 for(Node node: way.getNodes()) 525 { 526 if (!nodeMap.containsKey(node)){ 527 //new node 528 nodeMap.put(node, node); 529 530 //avoid duplicate nodes 531 if (prevNode != node) 532 { 533 newNodes.add(node); 534 } 535 else 536 { 537 nodesRemoved ++; 538 } 362 539 } 363 } else { 364 String val = e.getValue().iterator().next(); 365 ax.put(e.getKey(), val); 366 bx.put(e.getKey(), val); 540 else{ 541 //node with same coordinates already exists, substitute with existing node 542 Node representator = nodeMap.get(node); 543 544 if (representator != node){ 545 nodesRemoved ++; 546 } 547 548 //avoid duplicate node 549 if (prevNode != representator) 550 { 551 newNodes.add(representator); 552 } 553 } 554 555 prevNode = node; 367 556 } 368 }369 557 370 if (components.isEmpty())371 return false; // No conflicts found558 if (nodesRemoved > 0) 559 { 372 560 373 ExtendedDialog ed = new ExtendedDialog(Main.parent, 374 tr("Enter values for all conflicts."), 375 new String[] {tr("Solve Conflicts"), tr("Cancel")}); 376 ed.setButtonIcons(new String[] {"dialogs/conflict.png", "cancel.png"}); 377 ed.setContent(p); 378 ed.showDialog(); 561 if (newNodes.size() == 1) {//all nodes in the same coordinate - add one more node, to have closed way. 562 newNodes.add(newNodes.get(0)); 563 } 379 564 380 if (ed.getValue() != 1) return true; // user cancel, unresolvable conflicts 381 382 for (Entry<String, JComboBox> e : components.entrySet()) { 383 String val = e.getValue().getEditor().getItem().toString(); 384 ax.put(e.getKey(), val); 385 bx.put(e.getKey(), val); 565 Way newWay=new Way(way); 566 newWay.setNodes(newNodes); 567 cmds.add(new ChangeCommand(way, newWay)); 568 totalNodesRemoved += nodesRemoved; 569 } 386 570 } 387 571 388 cmds.add(new ChangeCommand(a, ax)); 389 cmds.add(new ChangeCommand(b, bx)); 390 commitCommands(marktr("Fix tag conflicts")); 391 return false; 572 return totalNodesRemoved > 0; 392 573 } 393 574 575 576 394 577 /** 395 * Will find all intersection and add nodes there for two given ways 396 * @param Way First way 397 * @param Way Second way 398 * @return ArrayList<OsmPrimitive> List of new nodes 578 * Will find all intersection and add nodes there for list of given ways. Handles self-intersections too. 579 * And make commands to add the intersection points to ways. 580 * @param List<Way> - a list of ways to test 581 * @return ArrayList<Node> List of new nodes 582 * Prerequisite: no two nodes have the same coordinates. 399 583 */ 400 private ArrayList<Node> addIntersections(Way a, Way b) { 401 boolean same = a.equals(b); 402 int nodesSizeA = a.getNodesCount(); 403 int nodesSizeB = b.getNodesCount(); 584 private ArrayList<Node> addIntersections(List<Way> ways) { 585 //TODO: this is a bit slow - O( (number of nodes)^2 + numberOfIntersections * numberOfNodes ) 404 586 405 ArrayList<Node> nodes = new ArrayList<Node>(); 406 ArrayList<NodeToSegs> nodesA = new ArrayList<NodeToSegs>(); 407 ArrayList<NodeToSegs> nodesB = new ArrayList<NodeToSegs>(); 587 //stupid java, cannot instantiate array of generic classes.. 588 @SuppressWarnings("unchecked") 589 ArrayList<Node>[] newNodes = new ArrayList[ways.size()]; 590 boolean[] changedWays = new boolean[ways.size()]; 408 591 409 for (int i = (same ? 1 : 0); i < nodesSizeA - 1; i++) { 410 for (int j = (same ? i + 2 : 0); j < nodesSizeB - 1; j++) { 411 // Avoid re-adding nodes that already exist on (some) intersections 412 if(a.getNode(i).equals(b.getNode(j)) || a.getNode(i+1).equals(b.getNode(j))) { 413 nodes.add(b.getNode(j)); 414 continue; 415 } else 416 if(a.getNode(i).equals(b.getNode(j+1)) || a.getNode(i+1).equals(b.getNode(j+1))) { 417 nodes.add(b.getNode(j+1)); 418 continue; 592 Set<Node> intersectionNodes = new LinkedHashSet<Node>(); 593 594 for (int pos = 0; pos < ways.size(); pos ++) 595 { 596 newNodes[pos] = new ArrayList<Node>(ways.get(pos).getNodes()); 597 changedWays[pos] = false; 598 } 599 600 //iterate over all segment pairs and introduce the intersections 601 602 Comparator<Node> coordsComparator = new NodePositionComparator(); 603 604 int seg1Way = 0; 605 int seg1Pos = -1; 606 607 while (true) 608 { 609 //advance to next segment 610 seg1Pos++; 611 if (seg1Pos > newNodes[seg1Way].size() - 2) 612 { 613 seg1Way++; 614 seg1Pos = 0; 615 616 if (seg1Way == ways.size()) { //finished 617 break; 618 } 619 } 620 621 622 //iterate over secondary segment 623 624 int seg2Way = seg1Way; 625 int seg2Pos = seg1Pos + 1;//skip the adjacent segment 626 627 while (true) 628 { 629 630 //advance to next segment 631 seg2Pos++; 632 if (seg2Pos > newNodes[seg2Way].size() - 2) 633 { 634 seg2Way++; 635 seg2Pos = 0; 636 637 if (seg2Way == ways.size()) { //finished 638 break; 419 639 } 420 LatLon intersection = getLineLineIntersection(421 a.getNode(i) .getEastNorth().east(), a.getNode(i) .getEastNorth().north(),422 a.getNode(i+1).getEastNorth().east(), a.getNode(i+1).getEastNorth().north(),423 b.getNode(j) .getEastNorth().east(), b.getNode(j) .getEastNorth().north(),424 b.getNode(j+1).getEastNorth().east(), b.getNode(j+1).getEastNorth().north());425 if(intersection == null) {426 continue;427 640 } 428 641 429 // Create the node. Adding them to the ways must be delayed because we still loop over them 430 Node n = new Node(intersection); 431 cmds.add(new AddCommand(n)); 432 nodes.add(n); 433 // The distance is needed to sort and add the nodes in direction of the way 434 nodesA.add(new NodeToSegs(i, n, a.getNode(i).getCoor())); 435 if(same) { 436 nodesA.add(new NodeToSegs(j, n, a.getNode(j).getCoor())); 437 } else { 438 nodesB.add(new NodeToSegs(j, n, b.getNode(j).getCoor())); 642 //need to get them again every time, because other segments may be changed 643 Node seg1Node1 = newNodes[seg1Way].get(seg1Pos); 644 Node seg1Node2 = newNodes[seg1Way].get(seg1Pos + 1); 645 Node seg2Node1 = newNodes[seg2Way].get(seg2Pos); 646 Node seg2Node2 = newNodes[seg2Way].get(seg2Pos + 1); 647 648 int commonCount = 0; 649 //test if we have common nodes to add. 650 if (seg1Node1 == seg2Node1 || seg1Node1 == seg2Node2) 651 { 652 commonCount ++; 653 654 if (seg1Way == seg2Way && 655 seg1Pos == 0 && 656 seg2Pos == newNodes[seg2Way].size() -2) 657 { 658 //do not add - this is first and last segment of the same way. 659 } 660 else 661 { 662 intersectionNodes.add(seg1Node1); 663 } 439 664 } 665 666 if (seg1Node2 == seg2Node1 || seg1Node2 == seg2Node2) 667 { 668 commonCount ++; 669 670 intersectionNodes.add(seg1Node2); 671 } 672 673 //no common nodes - find intersection 674 if (commonCount == 0) 675 { 676 LatLon intersection = getLineLineIntersection( 677 seg1Node1.getEastNorth().east(), seg1Node1.getEastNorth().north(), 678 seg1Node2.getEastNorth().east(), seg1Node2.getEastNorth().north(), 679 seg2Node1.getEastNorth().east(), seg2Node1.getEastNorth().north(), 680 seg2Node2.getEastNorth().east(), seg2Node2.getEastNorth().north()); 681 682 if (intersection != null) 683 { 684 Node newNode = new Node(intersection); 685 Node intNode = newNode; 686 boolean insertInSeg1 = false; 687 boolean insertInSeg2 = false; 688 689 //find if the intersection point is at end point of one of the segments, if so use that point 690 691 //segment 1 692 if (coordsComparator.compare(newNode, seg1Node1) == 0) { 693 intNode = seg1Node1; 694 } else if (coordsComparator.compare(newNode, seg1Node2) == 0) { 695 intNode = seg1Node2; 696 } else { 697 insertInSeg1 = true; 698 } 699 700 //segment 2 701 if (coordsComparator.compare(newNode, seg2Node1) == 0) { 702 intNode = seg2Node1; 703 } else if (coordsComparator.compare(newNode, seg2Node2) == 0) { 704 intNode = seg2Node2; 705 } else { 706 insertInSeg2 = true; 707 } 708 709 if (insertInSeg1) 710 { 711 newNodes[seg1Way].add(seg1Pos +1, intNode); 712 changedWays[seg1Way] = true; 713 714 //fix seg2 position, as indexes have changed, seg2Pos is always bigger than seg1Pos on the same segment. 715 if (seg2Way == seg1Way) { 716 seg2Pos ++; 717 } 718 } 719 720 if (insertInSeg2){ 721 newNodes[seg2Way].add(seg2Pos +1, intNode); 722 changedWays[seg2Way] = true; 723 724 //Do not need to compare again to already split segment 725 seg2Pos ++; 726 } 727 728 intersectionNodes.add(intNode); 729 730 if (intNode == newNode) 731 { 732 cmds.add(new AddCommand(intNode)); 733 } 734 } 735 } 440 736 } 441 737 } 442 738 443 addNodesToWay(a, nodesA); 444 if(!same) { 445 addNodesToWay(b, nodesB); 739 for (int pos = 0; pos < ways.size(); pos ++) 740 { 741 if (changedWays[pos] == false) { 742 continue; 743 } 744 745 Way way = ways.get(pos); 746 Way newWay = new Way(way); 747 newWay.setNodes(newNodes[pos]); 748 749 cmds.add(new ChangeCommand(way, newWay)); 446 750 } 447 751 448 return n odes;752 return new ArrayList<Node>(intersectionNodes); 449 753 } 450 754 451 755 /** … … 477 781 )); 478 782 } 479 783 480 /**481 * Inserts given nodes with positions into the given ways482 * @param Way The way to insert the nodes into483 * @param Collection<NodeToSegs> The list of nodes with positions to insert484 */485 private void addNodesToWay(Way a, ArrayList<NodeToSegs> nodes) {486 if(nodes.size() == 0)487 return;488 Way ax=new Way(a);489 Collections.sort(nodes);490 784 491 int numOfAdds = 1;492 for(NodeToSegs n : nodes) {493 ax.addNode(n.pos + numOfAdds, n.n);494 numOfAdds++;495 }496 785 497 cmds.add(new ChangeCommand(a, ax));498 }499 500 786 /** 501 787 * Commits the command list with a description 502 788 * @param String The description of what the commands do … … 552 838 return result; 553 839 } 554 840 841 555 842 /** 556 * This method splits ways into smaller parts, using the prepared nodes list as split points. 557 * Uses SplitWayAction.splitWay for the heavy lifting. 843 * This method analyzes the way and assigns each part what direction polygon "inside" is. 844 * @param parts the split parts of the way 845 * @param isInner - if true, reverts the direction (for multipolygon islands) 846 * @return list of parts, marked with the inside orientation. 847 */ 848 private ArrayList<WayInPath> markWayInsideSide(List<Way> parts, boolean isInner){ 849 850 ArrayList<WayInPath> result = new ArrayList<WayInPath>(); 851 852 //prepare prev and next maps 853 Map<Way, Way> nextWayMap = new HashMap<Way, Way>(); 854 Map<Way, Way> prevWayMap = new HashMap<Way, Way>(); 855 856 for (int pos = 0; pos < parts.size(); pos ++){ 857 858 if (!parts.get(pos).lastNode().equals(parts.get((pos + 1) % parts.size()).firstNode())) 859 throw new RuntimeException("Way not circular"); 860 861 nextWayMap.put(parts.get(pos), parts.get((pos + 1) % parts.size())); 862 prevWayMap.put(parts.get(pos), parts.get((pos + parts.size() - 1) % parts.size())); 863 } 864 865 //find the node with minimum y - it's guaranteed to be outer. (What about the south pole?) 866 Way topWay = null; 867 Node topNode = null; 868 int topIndex = 0; 869 double minY = Double.POSITIVE_INFINITY; 870 871 for(Way way: parts) { 872 for (int pos = 0; pos < way.getNodesCount(); pos ++){ 873 Node node = way.getNode(pos); 874 875 if (node.getEastNorth().getY() < minY){ 876 minY = node.getEastNorth().getY(); 877 topWay = way; 878 topNode = node; 879 topIndex = pos; 880 } 881 } 882 } 883 884 //get the upper way and it's orientation. 885 886 boolean wayClockwise; // orientation of the top way. 887 888 if (topNode.equals(topWay.firstNode()) || topNode.equals(topWay.lastNode())) 889 { 890 Node headNode = null; // the node at junction 891 Node prevNode = null; // last node from previous path 892 wayClockwise = false; 893 894 //node is in split point - find the outermost way from this point 895 896 headNode = topNode; 897 //make a fake node that is downwards from head node (smaller Y). It will be a division point between paths. 898 prevNode = new Node(new EastNorth(headNode.getEastNorth().getX(), headNode.getEastNorth().getY() - 1e5)); 899 900 topWay = null; 901 wayClockwise = false; 902 Node bestWayNextNode = null; 903 904 for(Way way: parts) 905 { 906 if (way.firstNode().equals(headNode)) 907 { 908 Node nextNode = way.getNode(1); 909 910 if (topWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) 911 { 912 //the new way is better 913 topWay = way; 914 wayClockwise = true; 915 bestWayNextNode = nextNode; 916 } 917 } 918 919 if (way.lastNode().equals(headNode)) 920 { 921 //end adjacent to headNode 922 Node nextNode = way.getNode(way.getNodesCount() - 2); 923 924 if (topWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) 925 { 926 //the new way is better 927 topWay = way; 928 wayClockwise = false; 929 bestWayNextNode = nextNode; 930 } 931 } 932 } 933 } 934 else 935 { 936 //node is inside way - pick the clockwise going end. 937 Node prev = topWay.getNode(topIndex - 1); 938 Node next = topWay.getNode(topIndex + 1); 939 940 //there will be no parallel segments in the middle of way, so all fine. 941 wayClockwise = angleIsClockwise(prev, topNode, next); 942 } 943 944 Way curWay = topWay; 945 boolean curWayInsideToTheRight = wayClockwise ^ isInner; 946 947 //iterate till full circle is reached 948 while (true){ 949 950 //add cur way 951 WayInPath resultWay = new WayInPath(curWay, curWayInsideToTheRight); 952 result.add(resultWay); 953 954 //process next way 955 Way nextWay = nextWayMap.get(curWay); 956 Node prevNode = curWay.getNode(curWay.getNodesCount() - 2); 957 Node headNode = curWay.lastNode(); 958 Node nextNode = nextWay.getNode(1); 959 960 if (nextWay == topWay) 961 { 962 //full loop traversed - all done. 963 break; 964 } 965 966 967 //find intersecting segments 968 // the intersections will look like this: 969 // 970 // ^ 971 // | 972 // X wayBNode 973 // | 974 // wayB | 975 // | 976 // curWay | nextWay 977 //----X----------------->X----------------------X----> 978 // prevNode ^headNode nextNode 979 // | 980 // | 981 // wayA | 982 // | 983 // X wayANode 984 // | 985 986 int intersectionCount = 0; 987 988 for (Way wayA: parts){ 989 990 if (wayA == curWay){ 991 continue; 992 } 993 994 if (wayA.lastNode().equals(headNode)){ 995 996 Way wayB = nextWayMap.get(wayA); 997 998 //test if wayA is opposite wayB relative to curWay and nextWay 999 1000 Node wayANode = wayA.getNode(wayA.getNodesCount() - 2); 1001 Node wayBNode = wayB.getNode(1); 1002 1003 boolean wayAToTheRight = isToTheRightSideOfLine(prevNode, headNode, nextNode, wayANode); 1004 boolean wayBToTheRight = isToTheRightSideOfLine(prevNode, headNode, nextNode, wayBNode); 1005 1006 if (wayAToTheRight != wayBToTheRight){ 1007 intersectionCount ++; 1008 } 1009 } 1010 } 1011 1012 //if odd number of crossings, invert orientation 1013 if (intersectionCount % 2 == 1){ 1014 curWayInsideToTheRight = !curWayInsideToTheRight; 1015 } 1016 1017 curWay = nextWay; 1018 } 1019 1020 return result; 1021 } 1022 1023 /** 1024 * This is a method splits way into smaller parts, using the prepared nodes list as split points. 1025 * Uses SplitWayAction.splitWay for the heavy lifting. 558 1026 * @return list of split ways (or original ways if no splitting is done). 559 1027 */ 560 private ArrayList<Way> splitWay sOnNodes(Way a, Way b, Collection<Node> nodes) {1028 private ArrayList<Way> splitWayOnNodes(Way way, Collection<Node> nodes) { 561 1029 562 1030 ArrayList<Way> result = new ArrayList<Way>(); 563 List<Way> ways = new ArrayList<Way>(); 564 ways.add(a); 565 ways.add(b); 1031 List<List<Node>> chunks = buildNodeChunks(way, nodes); 566 1032 567 for (Way way: ways) {568 List<List<Node>> chunks = buildNodeChunks(way, nodes);1033 if (chunks.size() > 1) 1034 { 569 1035 SplitWayResult split = SplitWayAction.splitWay(Main.map.mapView.getEditLayer(), way, chunks, Collections.<OsmPrimitive>emptyList()); 570 1036 571 1037 //execute the command, we need the results 572 Main.main.undoRedo.add(split.getCommand());573 c mdsCount ++;1038 cmds.add(split.getCommand()); 1039 commitCommands(marktr("Split ways into fragments")); 574 1040 575 1041 result.add(split.getOriginalWay()); 576 1042 result.addAll(split.getNewWays()); 1043 } else { 1044 //nothing to split 1045 result.add(way); 577 1046 } 578 1047 579 1048 return result; 580 1049 } 581 1050 1051 582 1052 /** 583 1053 * Simple chunking version. Does not care about circular ways and result being proper, we will glue it all back together later on. 584 1054 * @param way the way to chunk 585 1055 * @param splitNodes the places where to cut. 586 * @return list of node segments to produce.1056 * @return list of node paths to produce. 587 1057 */ 588 1058 private List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) 589 1059 { … … 607 1077 return result; 608 1078 } 609 1079 610 /** 611 * Returns all nodes for given ways 612 * @param Collection<Way> The list of ways which nodes are to be returned 613 * @return Collection<Node> The list of nodes the ways contain 614 */ 615 private Collection<Node> getNodesFromWays(Collection<Way> ways) { 616 Collection<Node> allNodes = new ArrayList<Node>(); 617 for(Way w: ways) { 618 allNodes.addAll(w.getNodes()); 1080 1081 private List<ComplexPolygon> findPolygons(Collection<Way> boundaryWays) { 1082 1083 List<PolygonLevel> list = findOuterWaysImpl(0, boundaryWays); 1084 List<ComplexPolygon> result = new ArrayList<ComplexPolygon>(); 1085 1086 //take every other level 1087 for(PolygonLevel pol: list){ 1088 if (pol.level %2 == 0){ 1089 result.add(pol.pol); 1090 } 619 1091 } 620 return allNodes; 1092 1093 return result; 621 1094 } 622 1095 623 1096 /** 624 * Gets all inner ways given all ways and outer ways. 625 * @param multigonWays 626 * @param outerWays 627 * @return list of inner ways. 1097 * Collects outer way and corresponding inner ways from all boundaries. 1098 * @param boundaryWays 1099 * @return the outermostWay. 628 1100 */ 629 private ArrayList<Way> findInnerWays(Collection<Way> multigonWays, Collection<WayInPath> outerWays) { 630 ArrayList<Way> innerWays = new ArrayList<Way>(); 631 Set<Way> outerSet = new HashSet<Way>(); 1101 private List<PolygonLevel> findOuterWaysImpl(int level, Collection<Way> boundaryWays) { 632 1102 633 for(WayInPath w: outerWays) { 634 outerSet.add(w.way); 635 } 1103 //TODO: bad performance for deep nestings... 1104 List<PolygonLevel> result = new ArrayList<PolygonLevel>(); 636 1105 637 for(Way way: multigonWays) { 638 if (!outerSet.contains(way)) { 639 innerWays.add(way); 1106 for(Way outerWay: boundaryWays){ 1107 1108 boolean outerGood = true; 1109 List<Way> innerCandidates = new ArrayList<Way>(); 1110 1111 for (Way innerWay: boundaryWays) 1112 { 1113 if (innerWay == outerWay) { 1114 continue; 1115 } 1116 1117 if (wayInsideWay(outerWay, innerWay)) 1118 { 1119 outerGood = false; 1120 break; 1121 } else if (wayInsideWay(innerWay, outerWay)){ 1122 innerCandidates.add(innerWay); 1123 } 640 1124 } 1125 1126 if (!outerGood) 1127 { 1128 continue; 1129 } 1130 1131 //add new outer polygon 1132 ComplexPolygon pol = new ComplexPolygon(outerWay); 1133 PolygonLevel polLev = new PolygonLevel(pol, level); 1134 1135 //process inner ways 1136 if (innerCandidates.size() > 0) 1137 { 1138 List<PolygonLevel> innerList = findOuterWaysImpl(level + 1, innerCandidates); 1139 result.addAll(innerList); 1140 1141 for (PolygonLevel pl: innerList) 1142 { 1143 if (pl.level == level + 1) 1144 { 1145 pol.innerWays.add(pl.pol.outerWay); 1146 } 1147 } 1148 } 1149 1150 result.add(polLev); 641 1151 } 642 1152 643 return innerWays;1153 return result; 644 1154 } 645 1155 646 1156 1157 647 1158 /** 648 * Finds all ways for a given list of Ways that form the outer hull. 649 * This works by starting with one node and traversing the multigon clockwise, always picking the leftmost path. 650 * Prerequisites - the ways must not intersect and have common end nodes where they meet. 651 * @param Collection<Way> A list of (splitted) ways that form a multigon 652 * @return Collection<Way> A list of ways that form the outer boundary of the multigon. 1159 * Finds all ways that form inner or outer boundaries. 1160 * @param Collection<Way> A list of (splitted) ways that form a multigon and share common end nodes on intersections. 1161 * @param Collection<Way> this list is filled with ways that are to be discarded 1162 * @return Collection<Collection<Way>> A list of ways that form the outer and inner boundaries of the multigon. 653 1163 */ 654 private static ArrayList<WayInPath> findOuterWays(Collection<Way> multigonWays) { 1164 public static List<List<WayInPath>> findBoundaryWays(Collection<WayInPath> multigonWays, List<Way> discardedResult) 1165 { 1166 //first find all discardable ways, by getting outer shells. 1167 //this will produce incorrect boundaries in some cases, but second pass will fix it. 655 1168 656 //find the node with minimum lat - it's guaranteed to be outer. (What about the south pole?) 657 Way bestWay = null; 658 Node topNode = null; 659 int topIndex = 0; 660 double minLat = Double.POSITIVE_INFINITY; 1169 List<WayInPath> discardedWays = new ArrayList<WayInPath>(); 1170 Set<WayInPath> processedWays = new HashSet<WayInPath>(); 1171 WayTraverser traverser = new WayTraverser(multigonWays); 661 1172 662 for(Way way: multigonWays) {663 for (int pos = 0; pos < way.getNodesCount(); pos ++) {664 Node node = way.getNode(pos);665 1173 666 if (node.getCoor().lat() < minLat) { 667 minLat = node.getCoor().lat(); 668 bestWay = way; 669 topNode = node; 670 topIndex = pos; 671 } 1174 for( WayInPath startWay: multigonWays) 1175 { 1176 if (processedWays.contains(startWay)) { 1177 continue; 672 1178 } 673 }674 1179 675 //get two final nodes from best way to mark as starting point and orientation. 676 Node headNode = null; 677 Node prevNode = null; 1180 traverser.startNewWay(startWay); 678 1181 679 if (topNode.equals(bestWay.firstNode()) || topNode.equals(bestWay.lastNode())) { 680 //node is in split point 681 headNode = topNode; 682 //make a fake node that is downwards from head node (smaller latitude). It will be a division point between paths. 683 prevNode = new Node(new LatLon(headNode.getCoor().lat() - 1000, headNode.getCoor().lon())); 684 } else { 685 //node is inside way - pick the clockwise going end. 686 Node prev = bestWay.getNode(topIndex - 1); 687 Node next = bestWay.getNode(topIndex + 1); 1182 List<WayInPath> boundary = new ArrayList<WayInPath>(); 1183 WayInPath lastWay = startWay; 688 1184 689 if (angleIsClockwise(prev, topNode, next)) { 690 headNode = bestWay.lastNode(); 691 prevNode = bestWay.getNode(bestWay.getNodesCount() - 2); 1185 while (true) { 1186 boundary.add(lastWay); 1187 1188 WayInPath bestWay = traverser.advanceNextLeftmostWay(); 1189 boolean wayInsideToTheRight = bestWay == null ? false: traverser.isLastWayInsideToTheRight(); 1190 1191 if (bestWay == null || processedWays.contains(bestWay) || !wayInsideToTheRight) { 1192 //bad segment chain - proceed to discard it 1193 lastWay = null; 1194 break; 1195 } else if (boundary.contains(bestWay)){ 1196 //traversed way found - close the way 1197 lastWay = bestWay; 1198 break; 1199 } else { 1200 //proceed to next segment 1201 lastWay = bestWay; 1202 } 692 1203 } 693 else { 694 headNode = bestWay.firstNode(); 695 prevNode = bestWay.getNode(1); 1204 1205 if (lastWay != null) 1206 { 1207 //way good 1208 processedWays.addAll(boundary); 1209 1210 //remove junk segments at the start 1211 while (boundary.get(0) != lastWay) 1212 { 1213 discardedWays.add(boundary.get(0)); 1214 boundary.remove(0); 1215 } 696 1216 } 1217 else 1218 { 1219 //way bad 1220 discardedWays.addAll(boundary); 1221 processedWays.addAll(boundary); 1222 } 697 1223 } 698 1224 699 Set<Way> outerWays = new HashSet<Way>(); 700 ArrayList<WayInPath> result = new ArrayList<WayInPath>(); 1225 //now we have removed junk segments, collect the real result ways 701 1226 702 //iterate till full circle is reached 703 while (true) { 1227 traverser.removeWays(discardedWays); 704 1228 705 bestWay = null; 706 Node bestWayNextNode = null; 707 boolean bestWayReverse = false; 1229 List<List<WayInPath>> result = new ArrayList<List<WayInPath>>(); 708 1230 709 for (Way way: multigonWays) { 710 boolean wayReverse; 711 Node nextNode; 1231 while(traverser.hasWays()){ 712 1232 713 if (way.firstNode().equals(headNode)) { 714 //start adjacent to headNode 715 nextNode = way.getNode(1); 716 wayReverse = false; 1233 WayInPath startWay = traverser.startNewWay(); 1234 List<WayInPath> boundary = new ArrayList<WayInPath>(); 1235 WayInPath curWay = startWay; 717 1236 718 if (nextNode.equals(prevNode)) { 719 //this is the path we came from - ignore it. 720 } else if (bestWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) { 721 //the new way is better 722 bestWay = way; 723 bestWayReverse = wayReverse; 724 bestWayNextNode = nextNode; 725 } 726 } 1237 do{ 1238 boundary.add(curWay); 1239 curWay = traverser.advanceNextRightmostWay(); 727 1240 728 if (way.lastNode().equals(headNode)) { 729 //end adjacent to headNode 730 nextNode = way.getNode(way.getNodesCount() - 2); 731 wayReverse = true; 1241 //should not happen 1242 if (curWay == null || !traverser.isLastWayInsideToTheRight()) 1243 throw new RuntimeException("Join areas internal error."); 732 1244 733 if (nextNode.equals(prevNode)) { 734 //this is the path we came from - ignore it. 735 } else if (bestWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) { 736 //the new way is better 737 bestWay = way; 738 bestWayReverse = wayReverse; 739 bestWayNextNode = nextNode; 740 } 741 } 742 } 1245 } while (curWay != startWay); 743 1246 744 if (bestWay == null) 745 throw new RuntimeException(); 746 else if (outerWays.contains(bestWay)) { 747 break; //full circle reached, terminate. 748 } else { 749 //add to outer ways, repeat. 750 outerWays.add(bestWay); 751 result.add(new WayInPath(bestWay, bestWayReverse)); 752 headNode = bestWayReverse ? bestWay.firstNode() : bestWay.lastNode(); 753 prevNode = bestWayReverse ? bestWay.getNode(1) : bestWay.getNode(bestWay.getNodesCount() - 2); 754 } 1247 //build result 1248 traverser.removeWays(boundary); 1249 result.add(boundary); 755 1250 } 756 1251 1252 for(WayInPath way:discardedWays){ 1253 discardedResult.add(way.way); 1254 } 1255 757 1256 return result; 758 1257 } 759 1258 1259 760 1260 /** 761 1261 * Tests if given point is to the right side of path consisting of 3 points. 762 1262 * @param lineP1 first point in path … … 784 1284 * @param secondNode second vector end node 785 1285 * @return true if first vector is clockwise before second vector. 786 1286 */ 1287 787 1288 public static boolean angleIsClockwise(Node commonNode, Node firstNode, Node secondNode) 788 1289 { 789 double d la1 = (firstNode.getCoor().lat() - commonNode.getCoor().lat());790 double d la2 = (secondNode.getCoor().lat() - commonNode.getCoor().lat());791 double d lo1 = (firstNode.getCoor().lon() - commonNode.getCoor().lon());792 double d lo2 = (secondNode.getCoor().lon() - commonNode.getCoor().lon());1290 double dy1 = (firstNode.getEastNorth().getY() - commonNode.getEastNorth().getY()); 1291 double dy2 = (secondNode.getEastNorth().getY() - commonNode.getEastNorth().getY()); 1292 double dx1 = (firstNode.getEastNorth().getX() - commonNode.getEastNorth().getX()); 1293 double dx2 = (secondNode.getEastNorth().getX() - commonNode.getEastNorth().getX()); 793 1294 794 return d la1 * dlo2 - dlo1 * dla2 > 0;1295 return dy1 * dx2 - dx1 * dy2 > 0; 795 1296 } 796 1297 1298 797 1299 /** 1300 * Tests if way is inside other way 1301 * @param outside 1302 * @param inside 1303 * @return 1304 */ 1305 public static boolean wayInsideWay(Way inside, Way outside) 1306 { 1307 for(Node insideNode: inside.getNodes()){ 1308 1309 if (!outside.getNodes().contains(insideNode)) 1310 //simply test the one node 1311 return nodeInsidePolygon(insideNode, outside.getNodes()); 1312 } 1313 1314 //all nodes shared. 1315 return false; 1316 } 1317 1318 /** 798 1319 * Tests if point is inside a polygon. The polygon can be self-intersecting. In such case the contains function works in xor-like manner. 799 1320 * @param polygonNodes list of nodes from polygon path. 800 1321 * @param point the point to test 801 1322 * @return true if the point is inside polygon. 802 1323 * FIXME: this should probably be moved to tools.. 803 1324 */ 804 public static boolean nodeInsidePolygon( ArrayList<Node> polygonNodes, Node point)1325 public static boolean nodeInsidePolygon(Node point, List<Node> polygonNodes) 805 1326 { 806 1327 if (polygonNodes.size() < 3) 807 1328 return false; … … 820 1341 } 821 1342 822 1343 //order points so p1.lat <= p2.lat; 823 if (newPoint.get Coor().lat() > oldPoint.getCoor().lat())1344 if (newPoint.getEastNorth().getY() > oldPoint.getEastNorth().getY()) 824 1345 { 825 1346 p1 = oldPoint; 826 1347 p2 = newPoint; … … 832 1353 } 833 1354 834 1355 //test if the line is crossed and if so invert the inside flag. 835 if ((newPoint.get Coor().lat() < point.getCoor().lat()) == (point.getCoor().lat() <= oldPoint.getCoor().lat())836 && (point.get Coor().lon() - p1.getCoor().lon()) * (p2.getCoor().lat() - p1.getCoor().lat())837 < (p2.get Coor().lon() - p1.getCoor().lon()) * (point.getCoor().lat() - p1.getCoor().lat()))1356 if ((newPoint.getEastNorth().getY() < point.getEastNorth().getY()) == (point.getEastNorth().getY() <= oldPoint.getEastNorth().getY()) 1357 && (point.getEastNorth().getX() - p1.getEastNorth().getX()) * (p2.getEastNorth().getY() - p1.getEastNorth().getY()) 1358 < (p2.getEastNorth().getX() - p1.getEastNorth().getX()) * (point.getEastNorth().getY() - p1.getEastNorth().getY())) 838 1359 { 839 1360 inside = !inside; 840 1361 } … … 845 1366 return inside; 846 1367 } 847 1368 1369 848 1370 /** 1371 * Joins the lists of ways. 1372 * @param Collection<Way> The list of outer ways that belong to that multigon. 1373 * @return Way The newly created outer way 1374 */ 1375 private ArrayList<Way> joinBoundaries(List<List<WayInPath>> outerWays) { 1376 ArrayList<Way> result = new ArrayList<Way>(); 1377 1378 for(List<WayInPath> ways: outerWays) 1379 { 1380 Way boundary = joinOuterWays(ways); 1381 result.add(boundary); 1382 } 1383 1384 return result; 1385 } 1386 1387 1388 /** 849 1389 * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway. 850 1390 * @param Collection<Way> The list of outer ways that belong to that multigon. 851 1391 * @return Way The newly created outer way 852 1392 */ 853 private Way joinOuterWays( ArrayList<WayInPath> outerWays) {1393 private Way joinOuterWays(List<WayInPath> outerWays) { 854 1394 855 1395 //leave original orientation, if all paths are reverse. 856 1396 boolean allReverse = true; 857 for(WayInPath way: outerWays) 858 allReverse &= way.insideToTheLeft;1397 for(WayInPath way: outerWays){ 1398 allReverse &= !way.insideToTheRight; 859 1399 } 860 1400 861 if (allReverse) 1401 if (allReverse){ 862 1402 for(WayInPath way: outerWays){ 863 way.insideToThe Left = !way.insideToTheLeft;1403 way.insideToTheRight = !way.insideToTheRight; 864 1404 } 865 1405 } 866 1406 867 1407 commitCommands(marktr("Join Areas: Remove Short Ways")); 1408 868 1409 Way joinedWay = joinOrientedWays(outerWays); 869 1410 if (joinedWay != null) 870 1411 return closeWay(joinedWay); … … 893 1434 * @param ArrayList<Way> The list of ways to join and reverse 894 1435 * @return Way The newly created way 895 1436 */ 896 private Way joinOrientedWays(ArrayList<WayInPath> ways) { 897 if(ways.size() < 2) 898 return ways.get(0).way; 1437 private Way joinOrientedWays(List<WayInPath> ways) { 1438 if(ways.size() < 2) return ways.get(0).way; 899 1439 900 1440 // This will turn ways so all of them point in the same direction and CombineAction won't bug 901 1441 // the user about this. 902 1442 1443 //TODO: ReverseWay and Combine way are really slow and we use them a lot here. This slows down large joins. 1444 903 1445 List<Way> actionWays = new ArrayList<Way>(ways.size()); 904 1446 905 1447 for(WayInPath way : ways) { 906 1448 actionWays.add(way.way); 907 1449 908 if (way.insideToTheLeft) { 1450 if (!way.insideToTheRight) 1451 { 909 1452 Main.main.getCurrentDataSet().setSelected(way.way); 910 1453 new ReverseWayAction().actionPerformed(null); 911 1454 cmdsCount++; … … 920 1463 return result; 921 1464 } 922 1465 923 /**924 * Joins a list of ways (using CombineWayAction and ReverseWayAction if necessary to quiet the former)925 * @param ArrayList<Way> The list of ways to join926 * @return Way The newly created way927 */928 private Way joinWays(ArrayList<Way> ways) {929 if(ways.size() < 2)930 return ways.get(0);931 1466 932 // This will turn ways so all of them point in the same direction and CombineAction won't bug933 // the user about this.934 Way a = null;935 for (Way b : ways) {936 if(a == null) {937 a = b;938 continue;939 }940 if(a.getNode(0).equals(b.getNode(0)) ||941 a.getNode(a.getNodesCount()-1).equals(b.getNode(b.getNodesCount()-1))) {942 Main.main.getCurrentDataSet().setSelected(b);943 new ReverseWayAction().actionPerformed(null);944 cmdsCount++;945 }946 a = b;947 }948 if ((a = new CombineWayAction().combineWays(ways)) != null) {949 cmdsCount++;950 }951 return a;952 }953 954 1467 /** 955 * Finds all ways that may be part of a multipolygon relation and removes them from the given list.956 * It will automatically combine "good" ways957 * @param Collection<Way> The list of inner ways to check958 * @param Way The newly created outer way959 * @return ArrayList<Way> The List of newly created inner ways960 */961 private ArrayList<Way> fixMultipolygons(Collection<Way> uninterestingWays, Way outerWay, boolean selfintersect) {962 Collection<Node> innerNodes = getNodesFromWays(uninterestingWays);963 Collection<Node> outerNodes = outerWay.getNodes();964 965 // The newly created inner ways. uninterestingWays is passed by reference and therefore modified in-place966 ArrayList<Way> newInnerWays = new ArrayList<Way>();967 968 // Now we need to find all inner ways that contain a remaining node, but no outer nodes969 // Remaining nodes are those that contain to more than one way. All nodes that belong to an970 // inner multigon part will have at least two ways, so we can use this to find which ways do971 // belong to the multigon.972 ArrayList<Way> possibleWays = new ArrayList<Way>();973 wayIterator: for(Way w : uninterestingWays) {974 boolean hasInnerNodes = false;975 for(Node n : w.getNodes()) {976 if(outerNodes.contains(n)) {977 if(!selfintersect) { // allow outer point for self intersection978 continue wayIterator;979 }980 }981 else if(!hasInnerNodes && innerNodes.contains(n)) {982 hasInnerNodes = true;983 }984 }985 if(!hasInnerNodes || w.getNodesCount() < 2) {986 continue;987 }988 possibleWays.add(w);989 }990 991 // This removes unnecessary ways that might have been added.992 removeAlmostAlikeWays(possibleWays);993 994 // loop twice995 // in k == 0 prefer ways which allow no Y-joining (i.e. which have only 1 solution)996 for(int k = 0; k < 2; ++k)997 {998 // Join all ways that have one start/ending node in common999 Way joined = null;1000 outerIterator: do {1001 removePartlyUnconnectedWays(possibleWays);1002 joined = null;1003 for(Way w1 : possibleWays) {1004 if(w1.isClosed()) {1005 if(!wayIsCollapsed(w1)) {1006 uninterestingWays.remove(w1);1007 newInnerWays.add(w1);1008 }1009 joined = w1;1010 possibleWays.remove(w1);1011 continue outerIterator;1012 }1013 ArrayList<Way> secondary = new ArrayList<Way>();1014 for(Way w2 : possibleWays) {1015 int i = 0;1016 // w2 cannot be closed, otherwise it would have been removed above1017 if(w1.equals(w2)) {1018 continue;1019 }1020 if(w2.isFirstLastNode(w1.firstNode())) {1021 ++i;1022 }1023 if(w2.isFirstLastNode(w1.lastNode())) {1024 ++i;1025 }1026 if(i == 2) // this way closes w1 - take it!1027 {1028 if(secondary.size() > 0) {1029 secondary.clear();1030 }1031 secondary.add(w2);1032 break;1033 }1034 else if(i > 0) {1035 secondary.add(w2);1036 }1037 }1038 if(k == 0 ? secondary.size() == 1 : secondary.size() > 0)1039 {1040 ArrayList<Way> joinThem = new ArrayList<Way>();1041 joinThem.add(w1);1042 joinThem.add(secondary.get(0));1043 // Although we joined the ways, we cannot simply assume that they are closed1044 if((joined = joinWays(joinThem)) != null)1045 {1046 uninterestingWays.removeAll(joinThem);1047 possibleWays.removeAll(joinThem);1048 1049 //List<Node> nodes = joined.getNodes();1050 // check if we added too much1051 /*for(int i = 1; i < nodes.size()-2; ++i)1052 {1053 if(nodes.get(i) == nodes.get(nodes.size()-1))1054 System.out.println("Joining of ways produced unexpecteded result\n");1055 }*/1056 uninterestingWays.add(joined);1057 possibleWays.add(joined);1058 continue outerIterator;1059 }1060 }1061 }1062 } while(joined != null);1063 }1064 return newInnerWays;1065 }1066 1067 /**1068 * Removes almost alike ways (= ways that are on top of each other for all nodes)1069 * @param ArrayList<Way> the ways to remove almost-duplicates from1070 */1071 private void removeAlmostAlikeWays(ArrayList<Way> ways) {1072 Collection<Way> removables = new ArrayList<Way>();1073 outer: for(int i=0; i < ways.size(); i++) {1074 Way a = ways.get(i);1075 for(int j=i+1; j < ways.size(); j++) {1076 Way b = ways.get(j);1077 List<Node> revNodes = new ArrayList<Node>(b.getNodes());1078 Collections.reverse(revNodes);1079 if(a.getNodes().equals(b.getNodes()) || a.getNodes().equals(revNodes)) {1080 removables.add(a);1081 continue outer;1082 }1083 }1084 }1085 ways.removeAll(removables);1086 }1087 1088 /**1089 * Removes ways from the given list whose starting or ending node doesn't1090 * connect to other ways from the same list (it's like removing spikes).1091 * @param ArrayList<Way> The list of ways to remove "spikes" from1092 */1093 private void removePartlyUnconnectedWays(ArrayList<Way> ways) {1094 List<Way> removables = new ArrayList<Way>();1095 for(Way a : ways) {1096 if(a.isClosed()) {1097 continue;1098 }1099 boolean connectedStart = false;1100 boolean connectedEnd = false;1101 for(Way b : ways) {1102 if(a.equals(b)) {1103 continue;1104 }1105 if(b.isFirstLastNode(a.firstNode())) {1106 connectedStart = true;1107 }1108 if(b.isFirstLastNode(a.lastNode())) {1109 connectedEnd = true;1110 }1111 }1112 if(!connectedStart || !connectedEnd) {1113 removables.add(a);1114 }1115 }1116 ways.removeAll(removables);1117 }1118 1119 /**1120 * Checks if a way is collapsed (i.e. looks like <---->)1121 * @param Way A *closed* way to check if it is collapsed1122 * @return boolean If the closed way is collapsed or not1123 */1124 private boolean wayIsCollapsed(Way w) {1125 if(w.getNodesCount() <= 3) return true;1126 1127 // If a way contains more than one node twice, it must be collapsed (only start/end node may be the same)1128 Way x = new Way(w);1129 int count = 0;1130 for(Node n : w.getNodes()) {1131 x.removeNode(n);1132 if(x.containsNode(n)) {1133 count++;1134 }1135 if(count == 2) return true;1136 }1137 return false;1138 }1139 1140 /**1141 1468 * Will add own multipolygon relation to the "previously existing" relations. Fixup is done by fixRelations 1142 1469 * @param Collection<Way> List of already closed inner ways 1143 1470 * @param Way The outer way … … 1270 1597 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 1271 1598 setEnabled(selection != null && !selection.isEmpty()); 1272 1599 } 1600 1273 1601 } -
test/unit/actions/JoinAreasActionTest.java
1 // License: GPL. For details, see LICENSE file. 2 package actions; 3 4 import org.junit.Assert; 5 import org.junit.Test; 6 import org.openstreetmap.josm.actions.JoinAreasAction; 7 import org.openstreetmap.josm.data.coor.LatLon; 8 import org.openstreetmap.josm.data.osm.Node; 9 10 11 public class JoinAreasActionTest { 12 13 private Node makeNode(double lat, double lon) 14 { 15 Node node = new Node(new LatLon(lat, lon)); 16 return node; 17 } 18 19 @Test 20 public void testAngleIsClockwise() 21 { 22 Assert.assertTrue(JoinAreasAction.angleIsClockwise(makeNode(0,0), makeNode(1,1), makeNode(0,1))); 23 Assert.assertTrue(JoinAreasAction.angleIsClockwise(makeNode(1,1), makeNode(0,1), makeNode(0,0))); 24 Assert.assertTrue(!JoinAreasAction.angleIsClockwise(makeNode(1,1), makeNode(0,1), makeNode(1,0))); 25 } 26 27 @Test 28 public void testisToTheRightSideOfLine() 29 { 30 Assert.assertTrue(JoinAreasAction.isToTheRightSideOfLine(makeNode(0,0), makeNode(1,1), makeNode(0,1), makeNode(0, 0.5))); 31 Assert.assertTrue(!JoinAreasAction.isToTheRightSideOfLine(makeNode(0,0), makeNode(1,1), makeNode(0,1), makeNode(1, 0))); 32 Assert.assertTrue(!JoinAreasAction.isToTheRightSideOfLine(makeNode(1,1), makeNode(0,1), makeNode(1,0), makeNode(0,0))); 33 Assert.assertTrue(JoinAreasAction.isToTheRightSideOfLine(makeNode(1,1), makeNode(0,1), makeNode(1,0), makeNode(2, 0))); 34 } 35 36 }