Ticket #13307: improve_MultipolygonTest_v14.patch
| File improve_MultipolygonTest_v14.patch, 23.0 KB (added by , 10 years ago) |
|---|
-
src/org/openstreetmap/josm/data/osm/WaySegment.java
114 114 s2.getSecondNode().getEastNorth().east(), s2.getSecondNode().getEastNorth().north()); 115 115 } 116 116 117 /** 118 * Checks whether this segment and another way segment share the same points 119 * @param s2 The other segment 120 * @return true if other way segment is the same or reverse 121 */ 122 public boolean isSimilar(WaySegment s2) { 123 if (getFirstNode().equals(s2.getFirstNode()) && getSecondNode().equals(s2.getSecondNode())) 124 return true; 125 if (getFirstNode().equals(s2.getSecondNode()) && getSecondNode().equals(s2.getFirstNode())) 126 return true; 127 return false; 128 } 129 117 130 @Override 118 131 public String toString() { 119 132 return "WaySegment [way=" + way.getUniqueId() + ", lowerIndex=" + lowerIndex + ']'; -
src/org/openstreetmap/josm/data/validation/tests/MultipolygonTest.java
5 5 import static org.openstreetmap.josm.tools.I18n.tr; 6 6 import static org.openstreetmap.josm.tools.I18n.trn; 7 7 8 import java.awt.geom. GeneralPath;8 import java.awt.geom.Point2D; 9 9 import java.util.ArrayList; 10 10 import java.util.Arrays; 11 11 import java.util.Collection; … … 21 21 import org.openstreetmap.josm.actions.CreateMultipolygonAction; 22 22 import org.openstreetmap.josm.command.ChangeCommand; 23 23 import org.openstreetmap.josm.command.Command; 24 import org.openstreetmap.josm.data.coor.EastNorth; 24 25 import org.openstreetmap.josm.data.osm.Node; 25 26 import org.openstreetmap.josm.data.osm.OsmPrimitive; 26 27 import org.openstreetmap.josm.data.osm.Relation; 27 28 import org.openstreetmap.josm.data.osm.RelationMember; 28 29 import org.openstreetmap.josm.data.osm.Way; 30 import org.openstreetmap.josm.data.osm.WaySegment; 29 31 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon; 30 32 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData; 31 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;32 import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;33 33 import org.openstreetmap.josm.data.validation.OsmValidator; 34 34 import org.openstreetmap.josm.data.validation.Severity; 35 35 import org.openstreetmap.josm.data.validation.Test; … … 76 76 /** Multipolygon member repeated (same primitive, different role) */ 77 77 public static final int REPEATED_MEMBER_DIFF_ROLE = 1615; 78 78 79 private static volatile ElemStyles styles;80 81 79 private final Set<String> keysCheckedByAnotherTest = new HashSet<>(); 82 80 83 81 /** … … 90 88 91 89 @Override 92 90 public void initialize() { 93 styles = MapPaintStyles.getStyles();94 91 } 95 92 96 93 @Override … … 111 108 super.endTest(); 112 109 } 113 110 114 private static GeneralPath createPath(List<Node> nodes) {115 GeneralPath result = new GeneralPath();116 result.moveTo((float) nodes.get(0).getCoor().lat(), (float) nodes.get(0).getCoor().lon());117 for (int i = 1; i < nodes.size(); i++) {118 Node n = nodes.get(i);119 result.lineTo((float) n.getCoor().lat(), (float) n.getCoor().lon());120 }121 return result;122 }123 124 private static List<GeneralPath> createPolygons(List<Multipolygon.PolyData> joinedWays) {125 List<GeneralPath> result = new ArrayList<>();126 for (Multipolygon.PolyData way : joinedWays) {127 result.add(createPath(way.getNodes()));128 }129 return result;130 }131 132 private static Intersection getPolygonIntersection(GeneralPath outer, List<Node> inner) {133 boolean inside = false;134 boolean outside = false;135 136 for (Node n : inner) {137 boolean contains = outer.contains(n.getCoor().lat(), n.getCoor().lon());138 inside = inside | contains;139 outside = outside | !contains;140 if (inside & outside) {141 return Intersection.CROSSING;142 }143 }144 145 return inside ? Intersection.INSIDE : Intersection.OUTSIDE;146 }147 148 111 @Override 149 112 public void visit(Way w) { 150 113 if (!w.isArea() && ElemStyles.hasOnlyAreaElemStyle(w)) { … … 168 131 if (r.isMultipolygon()) { 169 132 checkMembersAndRoles(r); 170 133 checkOuterWay(r); 171 checkRepeatedWayMembers(r); 172 173 // Rest of checks is only for complete multipolygons 174 if (!r.hasIncompleteMembers()) { 175 Multipolygon polygon = new Multipolygon(r); 176 177 // Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match. 178 checkMemberRoleCorrectness(r); 179 checkStyleConsistency(r, polygon); 180 checkGeometry(r, polygon); 134 boolean hasRepeatedMembers = checkRepeatedWayMembers(r); 135 if (!hasRepeatedMembers) { 136 // Rest of checks is only for complete multipolygons 137 if (!r.hasIncompleteMembers()) { 138 boolean rolesWereChecked = checkMemberRoleCorrectness(r); 139 Multipolygon polygon = new Multipolygon(r); 140 checkStyleConsistency(r, polygon); 141 checkGeometry(r, polygon, rolesWereChecked); 142 } 181 143 } 182 144 } 183 145 } … … 205 167 } 206 168 207 169 /** 208 * Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match:<ul> 170 * If simple joining of ways doesn't work, create new multipolygon using the logics from 171 * CreateMultipolygonAction and see if roles match:<ul> 209 172 * <li>{@link #WRONG_MEMBER_ROLE}: Role for ''{0}'' should be ''{1}''</li> 210 173 * </ul> 211 174 * @param r relation 175 * @return true if member roles were checked 212 176 */ 213 private voidcheckMemberRoleCorrectness(Relation r) {177 private boolean checkMemberRoleCorrectness(Relation r) { 214 178 final Pair<Relation, Relation> newMP = CreateMultipolygonAction.createMultipolygonRelation(r.getMemberPrimitives(Way.class), false); 179 215 180 if (newMP != null) { 216 181 for (RelationMember member : r.getMembers()) { 217 182 final Collection<RelationMember> memberInNewMP = newMP.b.getMembersFor(Collections.singleton(member.getMember())); … … 229 194 } 230 195 } 231 196 } 197 return newMP != null; 232 198 } 233 199 234 200 /** … … 242 208 * @param polygon multipolygon 243 209 */ 244 210 private void checkStyleConsistency(Relation r, Multipolygon polygon) { 211 ElemStyles styles = MapPaintStyles.getStyles(); 245 212 if (styles != null && !"boundary".equals(r.get("type"))) { 246 213 AreaElement area = ElemStyles.getAreaElemStyle(r, false); 247 214 boolean areaStyle = area != null; … … 313 280 * </ul> 314 281 * @param r relation 315 282 * @param polygon multipolygon 283 * @param rolesWereChecked might be used to skip most of the tests below 316 284 */ 317 private void checkGeometry(Relation r, Multipolygon polygon ) {285 private void checkGeometry(Relation r, Multipolygon polygon, boolean rolesWereChecked) { 318 286 List<Node> openNodes = polygon.getOpenEnds(); 319 287 if (!openNodes.isEmpty()) { 320 288 errors.add(TestError.builder(this, Severity.WARNING, NON_CLOSED_WAY) … … 323 291 .highlight(openNodes) 324 292 .build()); 325 293 } 326 327 // For painting is used Polygon class which works with ints only. For validation we need more precision 294 List<Node> intersectionNodes = calcIntersectionAtNodes(r); 328 295 List<PolyData> innerPolygons = polygon.getInnerPolygons(); 329 296 List<PolyData> outerPolygons = polygon.getOuterPolygons(); 330 List<GeneralPath> innerPolygonsPaths = innerPolygons.isEmpty() ? Collections.<GeneralPath>emptyList() : createPolygons(innerPolygons); 331 List<GeneralPath> outerPolygonsPaths = createPolygons(outerPolygons); 332 for (int i = 0; i < outerPolygons.size(); i++) { 333 PolyData pdOuter = outerPolygons.get(i); 334 // Check for intersection between outer members 335 for (int j = i+1; j < outerPolygons.size(); j++) { 336 checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdOuter, j); 297 Map<PolyData, List<PolyData>> crossingPolyMap = findIntersectingWays(r, innerPolygons, outerPolygons); 298 299 300 // Polygons may intersect without crossing ways when one polygon lies completely inside the other 301 // or when they cross at shared nodes 302 303 for (PolyData outer1: outerPolygons) { 304 for (PolyData outer2: outerPolygons) { 305 if (outer1 != outer2 && !checkProblemMap(crossingPolyMap, outer1, outer2)) { 306 int foundIntersections = checkSharedNodes(r, outer1, outer2, intersectionNodes, "oo"); 307 if (!rolesWereChecked && foundIntersections == 0) 308 checkIfInside(r, outer1, outer2); 309 } 337 310 } 338 311 } 339 for (int i = 0; i < innerPolygons.size(); i++) { 340 PolyData pdInner = innerPolygons.get(i); 341 // Check for intersection between inner members 342 for (int j = i+1; j < innerPolygons.size(); j++) { 343 checkCrossingWays(r, innerPolygons, innerPolygonsPaths, pdInner, j); 312 313 for (PolyData inner1 : innerPolygons) { 314 if (!intersectionNodes.isEmpty()) { 315 for (PolyData outer: outerPolygons) { 316 checkSharedNodes(r, outer, inner1, intersectionNodes, "oi"); 317 } 344 318 } 345 // Check for intersection between inner and outer members 346 boolean outside = true; 347 for (int o = 0; o < outerPolygons.size(); o++) { 348 outside &= checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdInner, o) == Intersection.OUTSIDE; 319 320 for (PolyData inner2 : innerPolygons) { 321 if (inner1 != inner2 && !checkProblemMap(crossingPolyMap, inner1, inner2)) { 322 int foundIntersections = checkSharedNodes(r, inner1, inner2, intersectionNodes, "ii"); 323 if (!rolesWereChecked && foundIntersections == 0) 324 checkIfInside(r, inner1, inner2); 325 326 } 349 327 } 350 if (outside) { 351 errors.add(TestError.builder(this, Severity.WARNING, INNER_WAY_OUTSIDE) 352 .message(tr("Multipolygon inner way is outside")) 353 .primitives(r) 354 .highlightNodePairs(Collections.singletonList(pdInner.getNodes())) 355 .build()); 328 if (!rolesWereChecked) { 329 // Find inner polygons which are not inside any outer 330 boolean outside = true; 331 boolean crossingWithOuter = false; 332 EastNorth innerPoint = inner1.getNodes().get(0).getEastNorth(); 333 for (PolyData outer : outerPolygons) { 334 if (checkProblemMap(crossingPolyMap, inner1, outer)) { 335 crossingWithOuter = true; 336 break; 337 } 338 outside &= !outer.get().contains(innerPoint.getX(), innerPoint.getY()); 339 if (!outside) 340 break; 341 } 342 if (outside && !crossingWithOuter) { 343 errors.add(TestError.builder(this, Severity.WARNING, INNER_WAY_OUTSIDE) 344 .message(tr("Multipolygon inner way is outside")) 345 .primitives(r) 346 .highlightNodePairs(Collections.singletonList(inner1.getNodes())) 347 .build()); 348 } 356 349 } 357 350 } 358 351 } 359 352 360 private Intersection checkCrossingWays(Relation r, List<PolyData> polygons, List<GeneralPath> polygonsPaths, PolyData pd, int idx) { 361 Intersection intersection = getPolygonIntersection(polygonsPaths.get(idx), pd.getNodes()); 362 if (intersection == Intersection.CROSSING) { 363 PolyData pdOther = polygons.get(idx); 364 if (pdOther != null) { 353 /** 354 * Check two polygons for intersections at shared nodes. 355 * @param r relation 356 * @param p1 1st polygon 357 * @param p2 2nd polygon 358 * @param intersectionNodes list of all intersection nodes in the multipolygon relation 359 * @param roles String with two chars ('o' or 'i') two pass info about roles of the two polygons 360 * @return number of intersections between these two polygons 361 */ 362 private int checkSharedNodes(Relation r, PolyData p1, PolyData p2, List<Node> intersectionNodes, String roles) { 363 if (intersectionNodes.isEmpty()) 364 return 0; 365 366 int numIntersections = 0; 367 int inside = 0; 368 int outside = 0; 369 for (Node n : p2.getNodes()) { 370 if (intersectionNodes.contains(n) && p1.getNodes().contains(n)) { 371 ++numIntersections; 372 } else { 373 EastNorth en = n.getEastNorth(); 374 if (en == null || en.isValid() == false) 375 continue; 376 if (p1.get().contains(en.getX(), en.getY())) 377 ++inside; 378 else 379 ++outside; 380 } 381 } 382 if (numIntersections > 0) { 383 if (inside > 0 && (outside > 0 || "oo".equals(roles))) { 365 384 errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS) 366 385 .message(tr("Intersection between multipolygon ways")) 367 386 .primitives(r) 368 .highlight NodePairs(Arrays.asList(pd.getNodes(), pdOther.getNodes()))387 .highlight(intersectionNodes) 369 388 .build()); 370 389 } 371 390 } 372 return intersection;391 return numIntersections; 373 392 } 374 393 394 private void checkIfInside(Relation r, PolyData p1, PolyData p2) { 395 Node n = p2.getNodes().get(0); 396 EastNorth en = n.getEastNorth(); 397 if (en != null && en.isValid() && p1.get().contains(en.getX(), en.getY())) { 398 errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS) 399 .message(tr("Multipolygon way inside other multipolygon way with same role")) 400 .primitives(r) 401 .highlight(p2.getNodes()) 402 .build()); 403 404 } 405 } 406 375 407 /** 408 * Determine multipolygon ways which are intersecting (crossing without a common node) or sharing one or more way segments. 409 * See also {@link CrossingWays} 410 * @param r the relation (for error reporting) 411 * @param innerPolygons list of inner polygons 412 * @param outerPolygons list of outer polygons 413 * @return map with crossing polygons 414 */ 415 private Map<PolyData, List<PolyData>> findIntersectingWays(Relation r, List<PolyData> innerPolygons, 416 List<PolyData> outerPolygons) { 417 HashMap<PolyData, List<PolyData>> crossingPolygonsMap = new HashMap<>(); 418 HashMap<PolyData, List<PolyData>> sharedWaySegmentsPolygonsMap = new HashMap<>(); 419 420 for (int loop = 0; loop < 2; loop++) { 421 /** All way segments, grouped by cells */ 422 final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000); 423 /** The already detected ways in error */ 424 final Map<List<Way>, List<WaySegment>> problemWays = new HashMap<>(50); 425 426 Map<PolyData, List<PolyData>> problemPolygonMap = (loop == 0) ? crossingPolygonsMap : sharedWaySegmentsPolygonsMap; 427 428 for (Way w : r.getMemberPrimitives(Way.class)) { 429 findIntersectingWay(w, r, cellSegments, problemWays, loop == 1); 430 } 431 432 if (!problemWays.isEmpty()) { 433 List<PolyData> allPolygons = new ArrayList<>(innerPolygons.size() + outerPolygons.size()); 434 allPolygons.addAll(innerPolygons); 435 allPolygons.addAll(outerPolygons); 436 437 for (Entry<List<Way>, List<WaySegment>> entry : problemWays.entrySet()) { 438 List<Way> ways = entry.getKey(); 439 if (ways.size() != 2) 440 continue; 441 PolyData[] crossingPolys = new PolyData[2]; 442 boolean allInner = true; 443 for (int i = 0; i < 2; i++) { 444 Way w = ways.get(i); 445 for (int j = 0; j < allPolygons.size(); j++) { 446 PolyData pd = allPolygons.get(j); 447 if (pd.getWayIds().contains(w.getUniqueId())) { 448 crossingPolys[i] = pd; 449 if (j >= innerPolygons.size()) 450 allInner = false; 451 break; 452 } 453 } 454 } 455 if (loop == 0 || (loop == 1 && !allInner)) { 456 String msg = (loop == 0) ? tr("Intersection between multipolygon ways") 457 : tr("Multipolygon ways share segments"); 458 errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS) 459 .message(msg) 460 .primitives(Arrays.asList(r, ways.get(0), ways.get(1))) 461 .highlightWaySegments(entry.getValue()) 462 .build()); 463 } 464 if (crossingPolys[0] != null && crossingPolys[1] != null) { 465 List<PolyData> crossingPolygons = problemPolygonMap.get(crossingPolys[0]); 466 if (crossingPolygons == null) { 467 crossingPolygons = new ArrayList<>(); 468 problemPolygonMap.put(crossingPolys[0], crossingPolygons); 469 } 470 crossingPolygons.add(crossingPolys[1]); 471 } 472 } 473 } 474 } 475 return crossingPolygonsMap; 476 } 477 478 /** 479 * Find ways which are crossing without sharing a node. 480 * @param w way that is member of the relation 481 * @param r the relation (used for error messages) 482 * @param cellSegments map with already collected way segments 483 * @param crossingWays list to collect crossing ways 484 * @param findSharedWaySegments true: find shared way segments instead of crossings 485 */ 486 private void findIntersectingWay(Way w, Relation r, Map<Point2D, List<WaySegment>> cellSegments, 487 Map<List<Way>, List<WaySegment>> crossingWays, boolean findSharedWaySegments) { 488 int nodesSize = w.getNodesCount(); 489 for (int i = 0; i < nodesSize - 1; i++) { 490 final WaySegment es1 = new WaySegment(w, i); 491 final EastNorth en1 = es1.getFirstNode().getEastNorth(); 492 final EastNorth en2 = es1.getSecondNode().getEastNorth(); 493 if (en1 == null || en2 == null) { 494 Main.warn("Crossing ways test skipped " + es1); 495 continue; 496 } 497 for (List<WaySegment> segments : CrossingWays.getSegments(cellSegments, en1, en2)) { 498 for (WaySegment es2 : segments) { 499 500 List<WaySegment> highlight; 501 if (es2.way == w) 502 continue; // reported by CrossingWays.SelfIntersection 503 if (findSharedWaySegments && !es1.isSimilar(es2)) 504 continue; 505 if (!findSharedWaySegments && !es1.intersects(es2)) 506 continue; 507 508 List<Way> prims = Arrays.asList(es1.way, es2.way); 509 if ((highlight = crossingWays.get(prims)) == null) { 510 highlight = new ArrayList<>(); 511 highlight.add(es1); 512 highlight.add(es2); 513 crossingWays.put(prims, highlight); 514 } else { 515 highlight.add(es1); 516 highlight.add(es2); 517 } 518 } 519 segments.add(es1); 520 } 521 } 522 } 523 524 /** 525 * Detect intersections of multipolygon ways at nodes. If any way node is used by more than two ways 526 * or two times in one way and at least once in another way we found an intersection. 527 * @param r the relation 528 * @return List of nodes were ways intersect 529 */ 530 private List<Node> calcIntersectionAtNodes(Relation r) { 531 List<Node> intersectionNodes = new ArrayList<>(); 532 Map<Node, List<Way>> nodeMap = new HashMap<>(); 533 for (RelationMember rm : r.getMembers()) { 534 if (!rm.isWay()) 535 continue; 536 int numNodes = rm.getWay().getNodesCount(); 537 for (int i = 0; i < numNodes; i++) { 538 Node n = rm.getWay().getNode(i); 539 if (n.getReferrers().size() <= 1) { 540 continue; // cannot be a problem node 541 } 542 List<Way> ways = nodeMap.get(n); 543 if (ways == null) { 544 ways = new ArrayList<>(); 545 nodeMap.put(n, ways); 546 } 547 ways.add(rm.getWay()); 548 if (ways.size() > 2 || (ways.size() == 2 && i != 0 && i + 1 != numNodes)) { 549 intersectionNodes.add(n); 550 } 551 } 552 } 553 return intersectionNodes; 554 } 555 556 /** 557 * Check if map contains combination of two given polygons. 558 * @param problemPolyMap the map 559 * @param pd1 1st polygon 560 * @param pd2 2nd polygon 561 * @return true if the combination of polygons is found in the map 562 */ 563 private boolean checkProblemMap(Map<PolyData, List<PolyData>> problemPolyMap, PolyData pd1, PolyData pd2) { 564 List<PolyData> crossingWithFirst = problemPolyMap.get(pd1); 565 if (crossingWithFirst != null) { 566 if (crossingWithFirst.contains(pd2)) 567 return true; 568 } 569 List<PolyData> crossingWith2nd = problemPolyMap.get(pd2); 570 if (crossingWith2nd != null) { 571 if (crossingWith2nd.contains(pd1)) 572 return true; 573 } 574 return false; 575 } 576 577 /** 376 578 * Check for:<ul> 377 579 * <li>{@link #WRONG_MEMBER_ROLE}: No useful role for multipolygon member</li> 378 580 * <li>{@link #WRONG_MEMBER_TYPE}: Non-Way in multipolygon</li> … … 511 713 } 512 714 } 513 715 newRel.setMembers(newMembers); 514 return new ChangeCommand (oldRel, newRel);716 return new ChangeCommand(oldRel, newRel); 515 717 } 516 718 } 517 719 }
