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