Ticket #13307: improve_MultipolygonTest_v12.patch
File improve_MultipolygonTest_v12.patch, 26.3 KB (added by , 7 years ago) |
---|
-
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; 12 12 import java.util.Collections; 13 import java.util.HashMap; 13 14 import java.util.HashSet; 14 15 import java.util.List; 16 import java.util.Map; 17 import java.util.Map.Entry; 15 18 import java.util.Set; 16 19 17 20 import org.openstreetmap.josm.Main; 18 21 import org.openstreetmap.josm.actions.CreateMultipolygonAction; 22 import org.openstreetmap.josm.command.ChangeCommand; 23 import org.openstreetmap.josm.command.Command; 24 import org.openstreetmap.josm.data.coor.EastNorth; 19 25 import org.openstreetmap.josm.data.osm.Node; 20 26 import org.openstreetmap.josm.data.osm.OsmPrimitive; 21 27 import org.openstreetmap.josm.data.osm.Relation; 22 28 import org.openstreetmap.josm.data.osm.RelationMember; 23 29 import org.openstreetmap.josm.data.osm.Way; 30 import org.openstreetmap.josm.data.osm.WaySegment; 24 31 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon; 25 32 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData; 26 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;27 import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;28 33 import org.openstreetmap.josm.data.validation.OsmValidator; 29 34 import org.openstreetmap.josm.data.validation.Severity; 30 35 import org.openstreetmap.josm.data.validation.Test; … … 66 71 public static final int NO_STYLE_POLYGON = 1611; 67 72 /** Area style on outer way */ 68 73 public static final int OUTER_STYLE = 1613; 74 /** Multipolygon member repeated (same primitive, same role */ 75 public static final int REPEATED_MEMBER_SAME_ROLE = 1614; 76 /** Multipolygon member repeated (same primitive, different role) */ 77 public static final int REPEATED_MEMBER_DIFF_ROLE = 1615; 69 78 70 79 private static volatile ElemStyles styles; 71 80 … … 102 111 super.endTest(); 103 112 } 104 113 105 private static GeneralPath createPath(List<Node> nodes) {106 GeneralPath result = new GeneralPath();107 result.moveTo((float) nodes.get(0).getCoor().lat(), (float) nodes.get(0).getCoor().lon());108 for (int i = 1; i < nodes.size(); i++) {109 Node n = nodes.get(i);110 result.lineTo((float) n.getCoor().lat(), (float) n.getCoor().lon());111 }112 return result;113 }114 115 private static List<GeneralPath> createPolygons(List<Multipolygon.PolyData> joinedWays) {116 List<GeneralPath> result = new ArrayList<>();117 for (Multipolygon.PolyData way : joinedWays) {118 result.add(createPath(way.getNodes()));119 }120 return result;121 }122 123 private static Intersection getPolygonIntersection(GeneralPath outer, List<Node> inner) {124 boolean inside = false;125 boolean outside = false;126 127 for (Node n : inner) {128 boolean contains = outer.contains(n.getCoor().lat(), n.getCoor().lon());129 inside = inside | contains;130 outside = outside | !contains;131 if (inside & outside) {132 return Intersection.CROSSING;133 }134 }135 136 return inside ? Intersection.INSIDE : Intersection.OUTSIDE;137 }138 139 114 @Override 140 115 public void visit(Way w) { 141 116 if (!w.isArea() && ElemStyles.hasOnlyAreaElemStyle(w)) { … … 159 134 if (r.isMultipolygon()) { 160 135 checkMembersAndRoles(r); 161 136 checkOuterWay(r); 162 163 // Rest of checks is only for complete multipolygons 164 if (!r.hasIncompleteMembers()) { 165 Multipolygon polygon = MultipolygonCache.getInstance().get(Main.map.mapView, r); 166 167 // Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match. 168 checkMemberRoleCorrectness(r); 169 checkStyleConsistency(r, polygon); 170 checkGeometry(r, polygon); 137 boolean hasRepeatedMembers = checkRepeatedWayMembers(r); 138 if (!hasRepeatedMembers) { 139 List<Node> intersectionNodes = checkIntersectionAtNodes(r); 140 // Rest of checks is only for complete multipolygons 141 if (!r.hasIncompleteMembers()) { 142 boolean rolesWereChecked = checkMemberRoleCorrectness(r); 143 Multipolygon polygon = new Multipolygon(r); 144 checkStyleConsistency(r, polygon); 145 checkGeometry(r, polygon, intersectionNodes, rolesWereChecked); 146 } 171 147 } 172 148 } 173 149 } … … 195 171 } 196 172 197 173 /** 198 * Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match:<ul> 174 * If simple joining of ways doesn't work, create new multipolygon using the logics from 175 * CreateMultipolygonAction and see if roles match:<ul> 199 176 * <li>{@link #WRONG_MEMBER_ROLE}: Role for ''{0}'' should be ''{1}''</li> 200 177 * </ul> 201 178 * @param r relation 179 * @return true if member roles were checked 202 180 */ 203 private voidcheckMemberRoleCorrectness(Relation r) {181 private boolean checkMemberRoleCorrectness(Relation r) { 204 182 final Pair<Relation, Relation> newMP = CreateMultipolygonAction.createMultipolygonRelation(r.getMemberPrimitives(Way.class), false); 183 205 184 if (newMP != null) { 206 185 for (RelationMember member : r.getMembers()) { 207 186 final Collection<RelationMember> memberInNewMP = newMP.b.getMembersFor(Collections.singleton(member.getMember())); … … 219 198 } 220 199 } 221 200 } 201 return newMP != null; 222 202 } 223 203 224 204 /** … … 303 283 * </ul> 304 284 * @param r relation 305 285 * @param polygon multipolygon 286 * @param intersectionNodes known nodes where ways of this multipolygon intersect or touch 287 * @param rolesWereChecked might be used to skip most of the tests below 306 288 */ 307 private void checkGeometry(Relation r, Multipolygon polygon ) {289 private void checkGeometry(Relation r, Multipolygon polygon, List<Node> intersectionNodes, boolean rolesWereChecked) { 308 290 List<Node> openNodes = polygon.getOpenEnds(); 309 291 if (!openNodes.isEmpty()) { 310 292 errors.add(TestError.builder(this, Severity.WARNING, NON_CLOSED_WAY) … … 314 296 .build()); 315 297 } 316 298 317 // For painting is used Polygon class which works with ints only. For validation we need more precision318 299 List<PolyData> innerPolygons = polygon.getInnerPolygons(); 319 300 List<PolyData> outerPolygons = polygon.getOuterPolygons(); 320 List<GeneralPath> innerPolygonsPaths = innerPolygons.isEmpty() ? Collections.<GeneralPath>emptyList() : createPolygons(innerPolygons); 321 List<GeneralPath> outerPolygonsPaths = createPolygons(outerPolygons); 301 HashMap<PolyData, List<PolyData>> crossingPolyMap = checkCrossingWays(r, innerPolygons, outerPolygons); 302 303 //Polygons may intersect without crossing ways when one polygon lies completely inside the other 322 304 for (int i = 0; i < outerPolygons.size(); i++) { 323 PolyData pdOuter = outerPolygons.get(i); 324 // Check for intersection between outer members 325 for (int j = i+1; j < outerPolygons.size(); j++) { 326 checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdOuter, j); 305 // report outer polygons which lie inside another outer 306 PolyData outer1 = outerPolygons.get(i); 307 for (int j = 0; j < outerPolygons.size(); j++) { 308 if (i != j && !crossing(crossingPolyMap, outer1, outerPolygons.get(j))) { 309 EastNorth en1 = null; 310 // find node which is not an intersection of multipolygon ways 311 for (int k = 0; k < outerPolygons.get(j).getNodes().size(); k++) { 312 Node n = outerPolygons.get(j).getNodes().get(k); 313 if (intersectionNodes.contains(n) == false) 314 en1 = n.getEastNorth(); 315 } 316 if (en1 != null && outer1.get().contains(en1.getX(), en1.getY())) { 317 ArrayList<OsmPrimitive> hilite = new ArrayList<>(); 318 hilite.addAll(outer1.getNodes()); 319 hilite.addAll(outerPolygons.get(j).getNodes()); 320 errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS) 321 .message(tr("Outer inside outer")) 322 .primitives(r) 323 .highlight(hilite) 324 .build()); 325 326 } 327 } 327 328 } 328 329 } 329 for (int i = 0; i < innerPolygons.size(); i++) { 330 PolyData pdInner = innerPolygons.get(i); 331 // Check for intersection between inner members 332 for (int j = i+1; j < innerPolygons.size(); j++) { 333 checkCrossingWays(r, innerPolygons, innerPolygonsPaths, pdInner, j); 330 for (PolyData inner1 : innerPolygons) { 331 for (PolyData inner2 : innerPolygons) { 332 if (inner1 != inner2 && !crossing(crossingPolyMap, inner1, inner2)) { 333 // we must allow that inner polygons share some nodes 334 boolean allInside = true; 335 for (Node innerPoint : inner2.getNodes()) { 336 EastNorth en = innerPoint.getEastNorth(); 337 if (!inner1.get().contains(en.getX(), en.getY())) { 338 allInside = false; 339 break; 340 } 341 } 342 if (allInside) { 343 ArrayList<OsmPrimitive> hilite = new ArrayList<>(); 344 hilite.addAll(inner1.getNodes()); 345 hilite.addAll(inner2.getNodes()); 346 errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS) 347 .message(tr("Inner inside inner")) 348 .primitives(r) 349 .highlight(hilite) 350 .build()); 351 } 352 } 334 353 } 335 // Check for intersection between inner and outer members 336 boolean outside = true; 337 for (int o = 0; o < outerPolygons.size(); o++) { 338 outside &= checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdInner, o) == Intersection.OUTSIDE; 339 } 340 if (outside) { 341 errors.add(TestError.builder(this, Severity.WARNING, INNER_WAY_OUTSIDE) 342 .message(tr("Multipolygon inner way is outside")) 343 .primitives(r) 344 .highlightNodePairs(Collections.singletonList(pdInner.getNodes())) 345 .build()); 346 } 354 // Find inner polygons which are not inside any outer 355 // MAYBE enable the following line to reduce number of warnings for same problem (and adapt multipolygon.osm) 356 // if (!rolesWereChecked) { 357 boolean outside = true; 358 boolean crossingWithOuter = false; 359 EastNorth innerPoint = inner1.getNodes().get(0).getEastNorth(); 360 for (PolyData outer : outerPolygons) { 361 if (crossing(crossingPolyMap, inner1, outer)) { 362 crossingWithOuter = true; 363 break; 364 } 365 outside &= !outer.get().contains(innerPoint.getX(), innerPoint.getY()); 366 if (!outside) 367 break; 368 } 369 if (outside && !crossingWithOuter) { 370 errors.add(TestError.builder(this, Severity.WARNING, INNER_WAY_OUTSIDE) 371 .message(tr("Multipolygon inner way is outside")) 372 .primitives(r) 373 .highlightNodePairs(Collections.singletonList(inner1.getNodes())) 374 .build()); 375 } 376 // } 347 377 } 348 378 } 349 379 350 private Intersection checkCrossingWays(Relation r, List<PolyData> polygons, List<GeneralPath> polygonsPaths, PolyData pd, int idx) { 351 Intersection intersection = getPolygonIntersection(polygonsPaths.get(idx), pd.getNodes()); 352 if (intersection == Intersection.CROSSING) { 353 PolyData pdOther = polygons.get(idx); 354 if (pdOther != null) { 355 errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS) 356 .message(tr("Intersection between multipolygon ways")) 357 .primitives(r) 358 .highlightNodePairs(Arrays.asList(pd.getNodes(), pdOther.getNodes())) 359 .build()); 360 } 380 /** 381 * Check if crossing map contains combination of two given polygons. 382 * @param crossingPolyMap the map 383 * @param pd1 1st polygon 384 * @param pd2 2nd polygon 385 * @return true if the polygons are crossing without sharing a node 386 */ 387 private boolean crossing(HashMap<PolyData, List<PolyData>> crossingPolyMap, PolyData pd1, PolyData pd2) { 388 List<PolyData> crossingWithFirst = crossingPolyMap.get(pd1); 389 if (crossingWithFirst != null) { 390 if (crossingWithFirst.contains(pd2)) 391 return true; 361 392 } 362 return intersection; 393 List<PolyData> crossingWith2nd = crossingPolyMap.get(pd2); 394 if (crossingWith2nd != null) { 395 if (crossingWith2nd.contains(pd1)) 396 return true; 397 } 398 return false; 363 399 } 364 400 365 401 /** … … 412 448 } 413 449 } 414 450 451 /** 452 * Determine multipolygon ways which are intersecting (crossing without a common node). This is not allowed. 453 * See {@link CrossingWays} 454 * @param r the relation (for error reporting) 455 * @param innerPolygons list of inner polygons 456 * @param outerPolygons list of outer polygons 457 * @return map of crossing polygons (including polygons touching outer) 458 */ 459 private HashMap<PolyData, List<PolyData>> checkCrossingWays(Relation r, List<PolyData> innerPolygons, 460 List<PolyData> outerPolygons) { 461 /** All way segments, grouped by cells */ 462 final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000); 463 /** The already detected ways in error */ 464 final Map<List<Way>, List<WaySegment>> crossingWays = new HashMap<>(50); 465 466 for (Way w : r.getMemberPrimitives(Way.class)) { 467 checkCrossingWay(w, r, cellSegments, crossingWays); 468 } 469 HashMap<PolyData, List<PolyData>> crossingPolygonsMap = new HashMap<>(); 470 if (!crossingWays.isEmpty()) { 471 List<PolyData> allPolygons = new ArrayList<>(innerPolygons.size() + outerPolygons.size()); 472 allPolygons.addAll(innerPolygons); 473 allPolygons.addAll(outerPolygons); 474 475 for (Entry<List<Way>, List<WaySegment>> entry : crossingWays.entrySet()) { 476 List<Way> ways = entry.getKey(); 477 if (ways.size() != 2) 478 continue; 479 PolyData[] crossingPolys = new PolyData[2]; 480 for (int i = 0; i < 2; i++) { 481 Way w = ways.get(i); 482 for (PolyData pd : allPolygons) { 483 if (pd.getWayIds().contains(w.getUniqueId())) { 484 crossingPolys[i] = pd; 485 break; 486 } 487 } 488 } 489 if (crossingPolys[0] != null && crossingPolys[1] != null) { 490 List<PolyData> crossingPolygons = crossingPolygonsMap.get(crossingPolys[0]); 491 if (crossingPolygons == null) { 492 crossingPolygons = new ArrayList<>(); 493 crossingPolygonsMap.put(crossingPolys[0], crossingPolygons); 494 } 495 crossingPolygons.add(crossingPolys[1]); 496 } 497 } 498 } 499 return crossingPolygonsMap; 500 } 501 502 /** 503 * Find ways which are crossing without sharing a node. 504 * @param w way that is member of the relation 505 * @param r the relation (used for error messages) 506 * @param crossingWays 507 * @param cellSegments 508 */ 509 private void checkCrossingWay(Way w, Relation r, Map<Point2D, List<WaySegment>> cellSegments, Map<List<Way>, List<WaySegment>> crossingWays) { 510 int nodesSize = w.getNodesCount(); 511 for (int i = 0; i < nodesSize - 1; i++) { 512 final WaySegment es1 = new WaySegment(w, i); 513 final EastNorth en1 = es1.getFirstNode().getEastNorth(); 514 final EastNorth en2 = es1.getSecondNode().getEastNorth(); 515 if (en1 == null || en2 == null) { 516 Main.warn("Crossing ways test skipped " + es1); 517 continue; 518 } 519 for (List<WaySegment> segments : CrossingWays.getSegments(cellSegments, en1, en2)) { 520 for (WaySegment es2 : segments) { 521 522 List<WaySegment> highlight; 523 if (es2.way == w) 524 continue; // reported by CrossingWays.SelfIntersection 525 if (!es1.intersects(es2)) 526 continue; 527 528 List<Way> prims = Arrays.asList(es1.way, es2.way); 529 if ((highlight = crossingWays.get(prims)) == null) { 530 highlight = new ArrayList<>(); 531 highlight.add(es1); 532 highlight.add(es2); 533 errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS) 534 .message(tr("Intersection between multipolygon ways")) 535 .primitives(Arrays.asList(r, es1.way, es2.way)) 536 .highlightWaySegments(highlight) 537 .build()); 538 crossingWays.put(prims, highlight); 539 } else { 540 highlight.add(es1); 541 highlight.add(es2); 542 } 543 } 544 segments.add(es1); 545 } 546 } 547 } 548 549 /** 550 * Detect intersections of multipolygon ways at nodes. If any way node is used by more than two ways 551 * or two times in one way and at least once in another way we found an intersection. 552 * @param r the relation 553 * @return List of nodes were ways intersect 554 */ 555 private List<Node> checkIntersectionAtNodes(Relation r) { 556 List<Node> intersectionNodes = new ArrayList<>(); 557 List<Pair<Set<Way>,List<Node>>> intersectionErrors = new ArrayList<>(); 558 HashMap<Way, String> wayRoleMap = new HashMap<>(); 559 for (RelationMember rm : r.getMembers()) { 560 if (rm.isWay()) 561 wayRoleMap.put(rm.getWay(), rm.getRole()); 562 } 563 Map<Node, List<Way>> nodeMap = new HashMap<>(); 564 for (RelationMember rm : r.getMembers()) { 565 if (!rm.isWay()) 566 continue; 567 int numNodes = rm.getWay().getNodesCount(); 568 for (int i = 0; i < numNodes; i++) { 569 Node n = rm.getWay().getNode(i); 570 if (n.getReferrers().size() <= 1) { 571 continue; // cannot be a problem node 572 } 573 List<Way> ways = nodeMap.get(n); 574 if (ways == null) { 575 ways = new ArrayList<>(); 576 nodeMap.put(n, ways); 577 } 578 ways.add(rm.getWay()); 579 if (ways.size() > 2 || (ways.size() == 2 && i != 0 && i + 1 != numNodes)) { 580 intersectionNodes.add(n); 581 boolean allInner = true; 582 for (Way w : ways) { 583 String role = wayRoleMap.get(w); 584 if (!"inner".equals(role)) 585 allInner = false; 586 } 587 if (!allInner) { 588 Set<Way> errorWays = new HashSet<>(ways); 589 boolean addNew = true; 590 for (Pair<Set<Way>, List<Node>> pair : intersectionErrors) { 591 if (pair.a.size() == errorWays.size() && pair.a.containsAll(errorWays)) { 592 pair.b.add(n); 593 addNew = false; 594 break; 595 } 596 } 597 if (addNew) { 598 List<Node> errNodes = new ArrayList<>(); 599 errNodes.add(n); 600 intersectionErrors.add(new Pair<>(errorWays, errNodes)); 601 } 602 } 603 } 604 } 605 } 606 for (Pair<Set<Way>,List<Node>> pair : intersectionErrors) { 607 // A single shared node between two ways ("touching ways") is considered okay. 608 if (pair.b.size() > 1) { 609 errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS) 610 .message(tr("Multipolygon ways share node(s)")) 611 .primitives(pair.a) 612 .highlight(pair.b) 613 .build()); 614 } 615 } 616 return intersectionNodes; 617 } 618 619 /** 620 * Check for:<ul> 621 * <li>{@link #REPEATED_MEMBER_DIFF_ROLE}: Multipolygon member(s) repeated with different role</li> 622 * <li>{@link #REPEATED_MEMBER_SAME_ROLE}: Multipolygon member(s) repeated with same role</li> 623 * </ul> 624 * @param r relation 625 * @return 626 */ 627 private boolean checkRepeatedWayMembers(Relation r) { 628 boolean hasDups = false; 629 Map<OsmPrimitive, List<RelationMember>> seenMemberPrimitives = new HashMap<>(); 630 for (RelationMember rm : r.getMembers()) { 631 List<RelationMember> list = seenMemberPrimitives.get(rm.getMember()); 632 if (list == null) { 633 list = new ArrayList<>(2); 634 seenMemberPrimitives.put(rm.getMember(), list); 635 } else { 636 hasDups = true; 637 } 638 list.add(rm); 639 } 640 if (hasDups) { 641 List<OsmPrimitive> repeatedSameRole = new ArrayList<>(); 642 List<OsmPrimitive> repeatedDiffRole = new ArrayList<>(); 643 for (Entry<OsmPrimitive, List<RelationMember>> e : seenMemberPrimitives.entrySet()) { 644 List<RelationMember> visited = e.getValue(); 645 if (e.getValue().size() == 1) 646 continue; 647 // we found a duplicate member, check if the roles differ 648 boolean rolesDiffer = false; 649 RelationMember rm = visited.get(0); 650 List<OsmPrimitive> primitives = new ArrayList<>(); 651 for (int i = 1; i < visited.size(); i++) { 652 RelationMember v = visited.get(i); 653 primitives.add(rm.getMember()); 654 if (v.getRole().equals(rm.getRole()) == false) { 655 rolesDiffer = true; 656 } 657 } 658 if (rolesDiffer) 659 repeatedDiffRole.addAll(primitives); 660 else 661 repeatedSameRole.addAll(primitives); 662 } 663 addRepeatedMemberError(r, repeatedDiffRole, REPEATED_MEMBER_DIFF_ROLE, tr("Multipolygon member(s) repeated with different role")); 664 addRepeatedMemberError(r, repeatedSameRole, REPEATED_MEMBER_SAME_ROLE, tr("Multipolygon member(s) repeated with same role")); 665 } 666 return hasDups; 667 } 668 669 private void addRepeatedMemberError(Relation r, List<OsmPrimitive> repeatedMembers, int errorCode, String msg) { 670 if (!repeatedMembers.isEmpty()) { 671 List<OsmPrimitive> prims = new ArrayList<>(1 + repeatedMembers.size()); 672 prims.add(r); 673 prims.addAll(repeatedMembers); 674 errors.add(TestError.builder(this, Severity.WARNING, errorCode) 675 .message(msg) 676 .primitives(prims) 677 .highlight(repeatedMembers) 678 .build()); 679 } 680 } 681 682 @Override 683 public Command fixError(TestError testError) { 684 if (testError.getCode() == REPEATED_MEMBER_SAME_ROLE) { 685 ArrayList<OsmPrimitive> primitives = new ArrayList<>(testError.getPrimitives()); 686 if (primitives.size() >= 2) { 687 if (primitives.get(0) instanceof Relation) { 688 Relation oldRel = (Relation) primitives.get(0); 689 Relation newRel = new Relation(oldRel); 690 List<OsmPrimitive> repeatedPrims = primitives.subList(1, primitives.size()); 691 List<RelationMember> oldMembers = oldRel.getMembers(); 692 693 List<RelationMember> newMembers = new ArrayList<>(); 694 HashSet<OsmPrimitive> toRemove = new HashSet<>(repeatedPrims); 695 HashSet<OsmPrimitive> found = new HashSet<>(repeatedPrims.size()); 696 for (RelationMember rm : oldMembers) { 697 if (toRemove.contains(rm.getMember())) { 698 if (found.contains(rm.getMember()) == false) { 699 found.add(rm.getMember()); 700 newMembers.add(rm); 701 } 702 } else 703 newMembers.add(rm); 704 } 705 newRel.setMembers(newMembers); 706 return new ChangeCommand (oldRel, newRel); 707 } 708 } 709 } 710 return null; 711 } 712 713 @Override 714 public boolean isFixable(TestError testError) { 715 if (testError.getCode() == REPEATED_MEMBER_SAME_ROLE) 716 return true; 717 return false; 718 } 415 719 }