Ticket #13307: improve_MultipolygonTest_v11.patch

File improve_MultipolygonTest_v11.patch, 27.4 KB (added by GerdP, 10 years ago)
  • src/org/openstreetmap/josm/data/osm/visitor/paint/relations/Multipolygon.java

     
    99import java.util.Collections;
    1010import java.util.HashSet;
    1111import java.util.Iterator;
     12import java.util.LinkedHashSet;
    1213import java.util.List;
    1314import java.util.Set;
    1415
     
    462463
    463464    private void load(Relation r) {
    464465        MultipolygonRoleMatcher matcher = getMultipolygonRoleMatcher();
    465 
     466        // use HashSet to eliminate repeated members
     467        Set<Way> tmpInnerWays = new LinkedHashSet<>();
     468        Set<Way> tmpOuterWays = new LinkedHashSet<>();
    466469        // Fill inner and outer list with valid ways
    467470        for (RelationMember m : r.getMembers()) {
    468471            if (m.getMember().isIncomplete()) {
     
    475478                }
    476479
    477480                if (matcher.isInnerRole(m.getRole())) {
    478                     innerWays.add(w);
     481                    tmpInnerWays.add(w);
    479482                } else if (!m.hasRole() || matcher.isOuterRole(m.getRole())) {
    480                     outerWays.add(w);
     483                    tmpOuterWays.add(w);
    481484                } // Remaining roles ignored
    482485            } // Non ways ignored
    483486        }
    484 
     487        innerWays.addAll(tmpInnerWays);
     488        outerWays.addAll(tmpOuterWays);
    485489        final List<PolyData> innerPolygons = new ArrayList<>();
    486490        final List<PolyData> outerPolygons = new ArrayList<>();
    487491        createPolygons(innerWays, innerPolygons);
  • src/org/openstreetmap/josm/data/validation/tests/MultipolygonTest.java

     
    55import static org.openstreetmap.josm.tools.I18n.tr;
    66import static org.openstreetmap.josm.tools.I18n.trn;
    77
    8 import java.awt.geom.GeneralPath;
     8import java.awt.geom.Point2D;
    99import java.util.ArrayList;
    1010import java.util.Arrays;
    1111import java.util.Collection;
    1212import java.util.Collections;
     13import java.util.HashMap;
    1314import java.util.HashSet;
    1415import java.util.List;
     16import java.util.Map;
     17import java.util.Map.Entry;
    1518import java.util.Set;
    1619
    1720import org.openstreetmap.josm.Main;
    1821import org.openstreetmap.josm.actions.CreateMultipolygonAction;
     22import org.openstreetmap.josm.command.ChangeCommand;
     23import org.openstreetmap.josm.command.Command;
     24import org.openstreetmap.josm.data.coor.EastNorth;
    1925import org.openstreetmap.josm.data.osm.Node;
    2026import org.openstreetmap.josm.data.osm.OsmPrimitive;
    2127import org.openstreetmap.josm.data.osm.Relation;
    2228import org.openstreetmap.josm.data.osm.RelationMember;
    2329import org.openstreetmap.josm.data.osm.Way;
     30import org.openstreetmap.josm.data.osm.WaySegment;
    2431import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
    2532import 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;
    2833import org.openstreetmap.josm.data.validation.OsmValidator;
    2934import org.openstreetmap.josm.data.validation.Severity;
    3035import org.openstreetmap.josm.data.validation.Test;
     
    6671    public static final int NO_STYLE_POLYGON = 1611;
    6772    /** Area style on outer way */
    6873    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;
    6978
    7079    private static volatile ElemStyles styles;
    7180
     
    102111        super.endTest();
    103112    }
    104113
    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 
    139114    @Override
    140115    public void visit(Way w) {
    141116        if (!w.isArea() && ElemStyles.hasOnlyAreaElemStyle(w)) {
     
    158133    public void visit(Relation r) {
    159134        if (r.isMultipolygon()) {
    160135            checkMembersAndRoles(r);
     136            checkRepeatedWayMembers(r);
    161137            checkOuterWay(r);
     138            List<Node> intersectionNodes = checkIntersectionAtNodes(r);
    162139
    163140            // Rest of checks is only for complete multipolygons
    164141            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);
     142                boolean rolesWereChecked = checkMemberRoleCorrectness(r);
     143                Multipolygon polygon = new Multipolygon(r);
    169144                checkStyleConsistency(r, polygon);
    170                 checkGeometry(r, polygon);
     145                checkGeometry(r, polygon, intersectionNodes, rolesWereChecked);
    171146            }
    172147        }
    173148    }
     
    195170    }
    196171
    197172    /**
    198      * Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match:<ul>
     173     * If simple joining of ways doesn't work, create new multipolygon using the logics from
     174     * CreateMultipolygonAction and see if roles match:<ul>
    199175     * <li>{@link #WRONG_MEMBER_ROLE}: Role for ''{0}'' should be ''{1}''</li>
    200176     * </ul>
    201177     * @param r relation
     178     * @return true if member roles were checked
    202179     */
    203     private void checkMemberRoleCorrectness(Relation r) {
     180    private boolean checkMemberRoleCorrectness(Relation r) {
    204181        final Pair<Relation, Relation> newMP = CreateMultipolygonAction.createMultipolygonRelation(r.getMemberPrimitives(Way.class), false);
     182
    205183        if (newMP != null) {
    206184            for (RelationMember member : r.getMembers()) {
    207185                final Collection<RelationMember> memberInNewMP = newMP.b.getMembersFor(Collections.singleton(member.getMember()));
     
    219197                }
    220198            }
    221199        }
     200        return newMP != null;
    222201    }
    223202
    224203    /**
     
    303282     * </ul>
    304283     * @param r relation
    305284     * @param polygon multipolygon
     285     * @param intersectionNodes known nodes where ways of this multipolygon intersect or touch
     286     * @param rolesWereChecked might be used to skip most of the tests below
    306287     */
    307     private void checkGeometry(Relation r, Multipolygon polygon) {
     288    private void checkGeometry(Relation r, Multipolygon polygon, List<Node> intersectionNodes, boolean rolesWereChecked) {
    308289        List<Node> openNodes = polygon.getOpenEnds();
    309290        if (!openNodes.isEmpty()) {
    310291            errors.add(TestError.builder(this, Severity.WARNING, NON_CLOSED_WAY)
     
    314295                    .build());
    315296        }
    316297
    317         // For painting is used Polygon class which works with ints only. For validation we need more precision
    318298        List<PolyData> innerPolygons = polygon.getInnerPolygons();
    319299        List<PolyData> outerPolygons = polygon.getOuterPolygons();
    320         List<GeneralPath> innerPolygonsPaths = innerPolygons.isEmpty() ? Collections.<GeneralPath>emptyList() : createPolygons(innerPolygons);
    321         List<GeneralPath> outerPolygonsPaths = createPolygons(outerPolygons);
     300        HashMap<PolyData, List<PolyData>> crossingPolyMap = checkCrossingWays(r, innerPolygons, outerPolygons);
     301
     302        //Polygons may intersect without crossing ways when one polygon lies completely inside the other
    322303        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);
     304            // report outer polygons which lie inside another outer
     305            PolyData outer1 = outerPolygons.get(i);
     306            for (int j = 0; j < outerPolygons.size(); j++) {
     307                if (i != j && !crossing(crossingPolyMap, outer1, outerPolygons.get(j))) {
     308                    EastNorth en1 = null;
     309                    // find node which is not a intersection of multipolygon ways
     310                    for (int k = 0; k < outerPolygons.get(j).getNodes().size(); k++) {
     311                        Node n = outerPolygons.get(j).getNodes().get(k);
     312                        if (intersectionNodes.contains(n) == false)
     313                            en1 = n.getEastNorth();
     314                    }
     315                    if (en1 != null && outer1.get().contains(en1.getX(), en1.getY())) {
     316                        ArrayList<OsmPrimitive> hilite = new ArrayList<>();
     317                        hilite.addAll(outer1.getNodes());
     318                        hilite.addAll(outerPolygons.get(j).getNodes());
     319                        errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS)
     320                                .message(tr("Outer inside outer"))
     321                                .primitives(r)
     322                                .highlight(hilite)
     323                                .build());
     324
     325                    }
     326                }
    327327            }
    328328        }
    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);
     329        for (PolyData inner1 : innerPolygons) {
     330            for (PolyData inner2 : innerPolygons) {
     331                if (inner1 != inner2 && !crossing(crossingPolyMap, inner1, inner2)) {
     332                    // we must allow that inner polygons share some nodes
     333                    boolean allInside = true;
     334                    for (Node innerPoint : inner2.getNodes()) {
     335                        EastNorth en = innerPoint.getEastNorth();
     336                        if (!inner1.get().contains(en.getX(), en.getY())) {
     337                            allInside = false;
     338                            break;
     339                        }
     340                    }
     341                    if (allInside) {
     342                        ArrayList<OsmPrimitive> hilite = new ArrayList<>();
     343                        hilite.addAll(inner1.getNodes());
     344                        hilite.addAll(inner2.getNodes());
     345                        errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS)
     346                                .message(tr("Inner inside inner"))
     347                                .primitives(r)
     348                                .highlight(hilite)
     349                                .build());
     350                    }
     351                }
    334352            }
    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             }
     353            // Find inner polygons which are not inside any outer
     354            // MAYBE enable the following line to reduce number of warnings for same problem (and adapt multipolygon.osm)
     355//            if (!rolesWereChecked) {
     356                boolean outside = true;
     357                boolean crossingWithOuter = false;
     358                EastNorth innerPoint = inner1.getNodes().get(0).getEastNorth();
     359                for (PolyData outer : outerPolygons) {
     360                    if (crossing(crossingPolyMap, inner1, outer)) {
     361                        crossingWithOuter = true;
     362                        break;
     363                    }
     364                    outside &= !outer.get().contains(innerPoint.getX(), innerPoint.getY());
     365                    if (!outside)
     366                        break;
     367                }
     368                if (outside && !crossingWithOuter) {
     369                    errors.add(TestError.builder(this, Severity.WARNING, INNER_WAY_OUTSIDE)
     370                            .message(tr("Multipolygon inner way is outside"))
     371                            .primitives(r)
     372                            .highlightNodePairs(Collections.singletonList(inner1.getNodes()))
     373                            .build());
     374                }
     375//            }
    347376        }
    348377    }
    349378
    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             }
     379    /**
     380     * Check if crossing map contains combination of two given polygons.
     381     * @param crossingPolyMap the map
     382     * @param pd1 1st polygon
     383     * @param pd2 2nd polygon
     384     * @return true if the polygons are crossing without sharing a node
     385     */
     386    private boolean crossing(HashMap<PolyData, List<PolyData>> crossingPolyMap, PolyData pd1, PolyData pd2) {
     387        List<PolyData> crossingWithFirst = crossingPolyMap.get(pd1);
     388        if (crossingWithFirst != null) {
     389            if (crossingWithFirst.contains(pd2))
     390                return true;
    361391        }
    362         return intersection;
     392        List<PolyData> crossingWith2nd = crossingPolyMap.get(pd2);
     393        if (crossingWith2nd != null) {
     394            if (crossingWith2nd.contains(pd1))
     395                return true;
     396        }
     397        return false;
    363398    }
    364399
    365400    /**
     
    412447        }
    413448    }
    414449
     450    /**
     451     * Determine multipolygon ways which are intersecting (crossing without a common node). This is not allowed.
     452     * See {@link CrossingWays}
     453     * @param r the relation (for error reporting)
     454     * @param innerPolygons list of inner polygons
     455     * @param outerPolygons list of outer polygons
     456     * @return map of crossing polygons (including polygons touching outer)
     457     */
     458    private HashMap<PolyData, List<PolyData>> checkCrossingWays(Relation r, List<PolyData> innerPolygons,
     459            List<PolyData> outerPolygons) {
     460        /** All way segments, grouped by cells */
     461        final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000);
     462        /** The already detected ways in error */
     463        final Map<List<Way>, List<WaySegment>> crossingWays = new HashMap<>(50);
     464
     465        for (Way w : r.getMemberPrimitives(Way.class)) {
     466            checkCrossingWay(w, r, cellSegments, crossingWays);
     467        }
     468        HashMap<PolyData, List<PolyData>> crossingPolygonsMap = new HashMap<>();
     469        if (!crossingWays.isEmpty()) {
     470            List<PolyData> allPolygons = new ArrayList<>(innerPolygons.size() + outerPolygons.size());
     471            allPolygons.addAll(innerPolygons);
     472            allPolygons.addAll(outerPolygons);
     473
     474            for (Entry<List<Way>, List<WaySegment>> entry : crossingWays.entrySet()) {
     475                List<Way> ways = entry.getKey();
     476                if (ways.size() != 2)
     477                    continue;
     478                PolyData[] crossingPolys = new PolyData[2];
     479                for (int i = 0; i < 2; i++) {
     480                    Way w = ways.get(i);
     481                    for (PolyData pd : allPolygons) {
     482                        if (pd.getWayIds().contains(w.getUniqueId())) {
     483                            crossingPolys[i] = pd;
     484                            break;
     485                        }
     486                    }
     487                }
     488                if (crossingPolys[0] != null && crossingPolys[1] != null) {
     489                    List<PolyData> crossingPolygons = crossingPolygonsMap.get(crossingPolys[0]);
     490                    if (crossingPolygons == null) {
     491                        crossingPolygons = new ArrayList<>();
     492                        crossingPolygonsMap.put(crossingPolys[0], crossingPolygons);
     493                    }
     494                    crossingPolygons.add(crossingPolys[1]);
     495                }
     496            }
     497        }
     498        return crossingPolygonsMap;
     499    }
     500
     501    /**
     502     * Find ways which are crossing without sharing a node.
     503     * @param w way that is member of the relation
     504     * @param r the relation (used for error messages)
     505     * @param crossingWays
     506     * @param cellSegments
     507     */
     508    private void checkCrossingWay(Way w, Relation r, Map<Point2D, List<WaySegment>> cellSegments, Map<List<Way>, List<WaySegment>> crossingWays) {
     509        int nodesSize = w.getNodesCount();
     510        for (int i = 0; i < nodesSize - 1; i++) {
     511            final WaySegment es1 = new WaySegment(w, i);
     512            final EastNorth en1 = es1.getFirstNode().getEastNorth();
     513            final EastNorth en2 = es1.getSecondNode().getEastNorth();
     514            if (en1 == null || en2 == null) {
     515                Main.warn("Crossing ways test skipped " + es1);
     516                continue;
     517            }
     518            for (List<WaySegment> segments : CrossingWays.getSegments(cellSegments, en1, en2)) {
     519                for (WaySegment es2 : segments) {
     520
     521                    List<WaySegment> highlight;
     522                    if (es2.way == w)
     523                        continue; // reported by CrossingWays.SelfIntersection
     524                    if (!es1.intersects(es2))
     525                        continue;
     526
     527                    List<Way> prims = Arrays.asList(es1.way, es2.way);
     528                    if ((highlight = crossingWays.get(prims)) == null) {
     529                        highlight = new ArrayList<>();
     530                        highlight.add(es1);
     531                        highlight.add(es2);
     532                        errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS)
     533                                .message(tr("Intersection between multipolygon ways"))
     534                                .primitives(Arrays.asList(r, es1.way, es2.way))
     535                                .highlightWaySegments(highlight)
     536                                .build());
     537                        crossingWays.put(prims, highlight);
     538                    } else {
     539                        highlight.add(es1);
     540                        highlight.add(es2);
     541                    }
     542                }
     543                segments.add(es1);
     544            }
     545        }
     546    }
     547
     548    /**
     549     * Detect intersections of multipolygon ways at nodes. If any way node is used by more than two ways
     550     * or two times in one way and at least once in another way we found an intersection.
     551     * @param r the relation
     552     * @return List of nodes were ways intersect
     553     */
     554    private List<Node> checkIntersectionAtNodes(Relation r) {
     555        List<Node> intersectionNodes = new ArrayList<>();
     556        List<Pair<Set<Way>,List<Node>>> intersectionErrors = new ArrayList<>();
     557        HashMap<Way, String> wayRoleMap = new HashMap<>();
     558        for (RelationMember rm : r.getMembers()) {
     559            if (rm.isWay())
     560                wayRoleMap.put(rm.getWay(), rm.getRole());
     561        }
     562        Map<Node, List<Way>> nodeMap = new HashMap<>();
     563        for (RelationMember rm : r.getMembers()) {
     564            if (!rm.isWay())
     565                continue;
     566            int numNodes = rm.getWay().getNodesCount();
     567            for (int i = 0; i < numNodes; i++) {
     568                Node n = rm.getWay().getNode(i);
     569                if (n.getReferrers().size() <= 1) {
     570                    continue; // cannot be a problem node
     571                }
     572                List<Way> ways = nodeMap.get(n);
     573                if (ways == null) {
     574                    ways = new ArrayList<>();
     575                    nodeMap.put(n, ways);
     576                }
     577                ways.add(rm.getWay());
     578                if (ways.size() > 2 || (ways.size() == 2 && i != 0 && i + 1 != numNodes)) {
     579                    intersectionNodes.add(n);
     580                    boolean allInner = true;
     581                    for (Way w : ways) {
     582                        String role = wayRoleMap.get(w);
     583                        if (!"inner".equals(role))
     584                            allInner = false;
     585                    }
     586                    if (!allInner) {
     587                        Set<Way> errorWays = new HashSet<>(ways);
     588                        boolean addNew = true;
     589                        for (Pair<Set<Way>, List<Node>> pair : intersectionErrors) {
     590                            if (pair.a.size() == errorWays.size() && pair.a.containsAll(errorWays)) {
     591                                pair.b.add(n);
     592                                addNew = false;
     593                                break;
     594                            }
     595                        }
     596                        if (addNew) {
     597                            List<Node> errNodes = new ArrayList<>();
     598                            errNodes.add(n);
     599                            intersectionErrors.add(new Pair<>(errorWays, errNodes));
     600                        }
     601                    }
     602                }
     603            }
     604        }
     605        for (Pair<Set<Way>,List<Node>> pair : intersectionErrors) {
     606            errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS)
     607                    .message(tr("Multipolygon ways share node(s)"))
     608                    .primitives(pair.a)
     609                    .highlight(pair.b)
     610                    .build());
     611        }
     612        return intersectionNodes;
     613    }
     614
     615    /**
     616     * Check for:<ul>
     617     * <li>{@link #REPEATED_MEMBER_DIFF_ROLE}: Multipolygon member(s) repeated with different role</li>
     618     * <li>{@link #REPEATED_MEMBER_SAME_ROLE}: Multipolygon member(s) repeated with same role</li>
     619     * </ul>
     620     * @param r relation
     621     */
     622    private void checkRepeatedWayMembers(Relation r) {
     623        boolean hasDups = false;
     624        Map<OsmPrimitive, List<RelationMember>> seenMemberPrimitives = new HashMap<>();
     625        for (RelationMember rm : r.getMembers()) {
     626            List<RelationMember> list = seenMemberPrimitives.get(rm.getMember());
     627            if (list == null) {
     628                list = new ArrayList<>(2);
     629                seenMemberPrimitives.put(rm.getMember(), list);
     630            } else {
     631                hasDups = true;
     632            }
     633            list.add(rm);
     634        }
     635        if (!hasDups)
     636            return;
     637
     638        List<OsmPrimitive> repeatedSameRole = new ArrayList<>();
     639        List<OsmPrimitive> repeatedDiffRole = new ArrayList<>();
     640        for (Entry<OsmPrimitive, List<RelationMember>> e : seenMemberPrimitives.entrySet()) {
     641            List<RelationMember> visited = e.getValue();
     642            if (e.getValue().size() == 1)
     643                continue;
     644            // we found a duplicate member, check if the roles differ
     645            boolean rolesDiffer = false;
     646            RelationMember rm = visited.get(0);
     647            List<OsmPrimitive> primitives = new ArrayList<>();
     648            for (int i = 1; i < visited.size(); i++) {
     649                RelationMember v = visited.get(i);
     650                primitives.add(rm.getMember());
     651                if (v.getRole().equals(rm.getRole()) == false) {
     652                    rolesDiffer = true;
     653                }
     654            }
     655            if (rolesDiffer)
     656                repeatedDiffRole.addAll(primitives);
     657            else
     658                repeatedSameRole.addAll(primitives);
     659        }
     660        addRepeatedMemberError(r, repeatedDiffRole, REPEATED_MEMBER_DIFF_ROLE, tr("Multipolygon member(s) repeated with different role"));
     661        addRepeatedMemberError(r, repeatedSameRole, REPEATED_MEMBER_SAME_ROLE, tr("Multipolygon member(s) repeated with same role"));
     662    }
     663
     664    private void addRepeatedMemberError(Relation r, List<OsmPrimitive> repeatedMembers, int errorCode, String msg) {
     665        if (!repeatedMembers.isEmpty()) {
     666            List<OsmPrimitive> prims = new ArrayList<>(1 + repeatedMembers.size());
     667            prims.add(r);
     668            prims.addAll(repeatedMembers);
     669            errors.add(TestError.builder(this, Severity.WARNING, errorCode)
     670                    .message(msg)
     671                    .primitives(prims)
     672                    .highlight(repeatedMembers)
     673                    .build());
     674        }
     675    }
     676
     677    @Override
     678    public Command fixError(TestError testError) {
     679        if (testError.getCode() == REPEATED_MEMBER_SAME_ROLE) {
     680            ArrayList<OsmPrimitive> primitives = new ArrayList<>(testError.getPrimitives());
     681            if (primitives.size() >= 2) {
     682                if (primitives.get(0) instanceof Relation) {
     683                    Relation oldRel = (Relation) primitives.get(0);
     684                    Relation newRel = new Relation(oldRel);
     685                    List<OsmPrimitive> repeatedPrims = primitives.subList(1, primitives.size());
     686                    List<RelationMember> oldMembers = oldRel.getMembers();
     687
     688                    List<RelationMember> newMembers = new ArrayList<>();
     689                    HashSet<OsmPrimitive> toRemove = new HashSet<>(repeatedPrims);
     690                    HashSet<OsmPrimitive> found = new HashSet<>(repeatedPrims.size());
     691                    for (RelationMember rm : oldMembers) {
     692                        if (toRemove.contains(rm.getMember())) {
     693                            if (found.contains(rm.getMember()) == false) {
     694                                found.add(rm.getMember());
     695                                newMembers.add(rm);
     696                            }
     697                        } else
     698                            newMembers.add(rm);
     699                    }
     700                    newRel.setMembers(newMembers);
     701                    return new ChangeCommand (oldRel, newRel);
     702                }
     703            }
     704        }
     705        return null;
     706    }
     707
     708    @Override
     709    public boolean isFixable(TestError testError) {
     710        if (testError.getCode() == REPEATED_MEMBER_SAME_ROLE)
     711            return true;
     712        return false;
     713    }
    415714}