Ticket #13307: improve_MultipolygonTest_v7.patch

File improve_MultipolygonTest_v7.patch, 28.2 KB (added by GerdP, 10 years ago)
  • src/org/openstreetmap/josm/command/RemoveRepeatedRelationMembersCommand.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.command;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.util.ArrayList;
     7import java.util.Collection;
     8import java.util.HashSet;
     9import java.util.List;
     10import java.util.Objects;
     11
     12import javax.swing.Icon;
     13
     14import org.openstreetmap.josm.Main;
     15import org.openstreetmap.josm.data.osm.OsmPrimitive;
     16import org.openstreetmap.josm.data.osm.Relation;
     17import org.openstreetmap.josm.data.osm.RelationMember;
     18import org.openstreetmap.josm.gui.DefaultNameFormatter;
     19import org.openstreetmap.josm.tools.ImageProvider;
     20
     21/**
     22 * Command that removes repeated relation members (first occurrence is kept)
     23 *
     24 * @author Gerd Petermann
     25 */
     26public class RemoveRepeatedRelationMembersCommand extends Command {
     27
     28    // The relation to be changed
     29    private final Relation relation;
     30    // Old value of modified
     31    private Boolean oldModified;
     32    private final List<OsmPrimitive> repeatedPrims;
     33    private List<RelationMember> oldMembers;
     34
     35
     36    /**
     37     * @param r the relation
     38     * @param membersToRemove list of members to remove
     39     */
     40    public RemoveRepeatedRelationMembersCommand(Relation r, List<OsmPrimitive> membersToRemove) {
     41        this.relation = r;
     42        this.repeatedPrims = membersToRemove;
     43    }
     44
     45    @Override
     46    public boolean executeCommand() {
     47        boolean executed = false;
     48        long t1 = System.currentTimeMillis();
     49        oldMembers = relation.getMembers();
     50
     51        List<RelationMember> newMembers = new ArrayList<>();
     52        HashSet<OsmPrimitive> toRemove = new HashSet<>(repeatedPrims);
     53        HashSet<OsmPrimitive> found = new HashSet<>(repeatedPrims.size());
     54        for (RelationMember rm : oldMembers) {
     55            if (toRemove.contains(rm.getMember())) {
     56                if (found.contains(rm.getMember()) == false) {
     57                    found.add(rm.getMember());
     58                    newMembers.add(rm);
     59                }
     60            } else
     61                newMembers.add(rm);
     62        }
     63        oldModified = relation.isModified();
     64        relation.setMembers(newMembers);
     65        long t2 = System.currentTimeMillis();
     66        Main.debug("remove repeated relation members action took " + (t2 - t1) + " ms, removed"
     67                + (oldMembers.size() - newMembers.size()) + " members");
     68        return executed;
     69    }
     70
     71    @Override
     72    public void undoCommand() {
     73        if (oldMembers != null) {
     74            relation.setMembers(oldMembers);
     75            if (oldModified != null) {
     76                relation.setModified(oldModified);
     77            }
     78        }
     79    }
     80
     81    @Override
     82    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
     83        modified.add(relation);
     84    }
     85
     86    @Override
     87    public String getDescriptionText() {
     88        return tr("Remove relation member(s) from {0}",
     89                relation.getDisplayName(DefaultNameFormatter.getInstance()));
     90    }
     91
     92    @Override
     93    public Icon getDescriptionIcon() {
     94        return ImageProvider.get(relation.getDisplayType());
     95    }
     96
     97    @Override
     98    public int hashCode() {
     99        return Objects.hash(super.hashCode(), relation, repeatedPrims.hashCode());
     100    }
     101
     102    @Override
     103    public boolean equals(Object obj) {
     104        if (this == obj) return true;
     105        if (obj == null || getClass() != obj.getClass()) return false;
     106        if (!super.equals(obj)) return false;
     107        RemoveRepeatedRelationMembersCommand that = (RemoveRepeatedRelationMembersCommand) obj;
     108        return
     109                Objects.equals(relation, that.relation) &&
     110                repeatedPrims.size() == that.repeatedPrims.size() &&
     111                repeatedPrims.containsAll(that.repeatedPrims);
     112    }
     113}
  • 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
     
    175176         */
    176177        public JoinedWay(List<Node> nodes, Collection<Long> wayIds, boolean selected) {
    177178            this.nodes = new ArrayList<>(nodes);
    178             this.wayIds = new ArrayList<>(wayIds);
     179            this.wayIds = new LinkedHashSet<>(wayIds);
    179180            this.selected = selected;
    180181        }
    181182
  • src/org/openstreetmap/josm/data/osm/WaySegment.java

     
    114114                s2.getSecondNode().getEastNorth().east(), s2.getSecondNode().getEastNorth().north());
    115115    }
    116116
     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     * @since r10819
     122     */
     123    public boolean isSimilar(WaySegment s2) {
     124        if (getFirstNode().equals(s2.getFirstNode()) && getSecondNode().equals(s2.getSecondNode()))
     125            return true;
     126        if (getFirstNode().equals(s2.getSecondNode()) && getSecondNode().equals(s2.getFirstNode()))
     127            return true;
     128        return false;
     129    }
     130
    117131    @Override
    118132    public String toString() {
    119133        return "WaySegment [way=" + way.getUniqueId() + ", lowerIndex=" + lowerIndex + ']';
  • src/org/openstreetmap/josm/data/validation/tests/MultipolygonTest.java

     
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55import static org.openstreetmap.josm.tools.I18n.trn;
    66
    7 import java.awt.geom.GeneralPath;
     7import java.awt.geom.Path2D;
     8import java.awt.geom.Point2D;
    89import java.text.MessageFormat;
    910import java.util.ArrayList;
    1011import java.util.Arrays;
    1112import java.util.Collection;
    1213import java.util.Collections;
     14import java.util.HashMap;
    1315import java.util.HashSet;
    1416import java.util.LinkedList;
    1517import java.util.List;
     18import java.util.Map;
     19import java.util.Map.Entry;
    1620import java.util.Set;
    1721
    1822import org.openstreetmap.josm.Main;
    1923import org.openstreetmap.josm.actions.CreateMultipolygonAction;
     24import org.openstreetmap.josm.command.Command;
     25import org.openstreetmap.josm.command.RemoveRepeatedRelationMembersCommand;
     26import org.openstreetmap.josm.data.coor.EastNorth;
     27import org.openstreetmap.josm.data.coor.LatLon;
    2028import org.openstreetmap.josm.data.osm.Node;
    2129import org.openstreetmap.josm.data.osm.OsmPrimitive;
    2230import org.openstreetmap.josm.data.osm.Relation;
    2331import org.openstreetmap.josm.data.osm.RelationMember;
    2432import org.openstreetmap.josm.data.osm.Way;
     33import org.openstreetmap.josm.data.osm.WaySegment;
    2534import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
    2635import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
    27 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
    2836import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
    2937import org.openstreetmap.josm.data.validation.OsmValidator;
    3038import org.openstreetmap.josm.data.validation.Severity;
     
    6775    public static final int NO_STYLE_POLYGON = 1611;
    6876    /** Area style on outer way */
    6977    public static final int OUTER_STYLE = 1613;
     78    /** Multipolygon member repeated (same primitive, same role */
     79    public static final int REPEATED_MEMBER_SAME_ROLE = 1614;
     80    /** Multipolygon member repeated (same primitive, different role) */
     81    public static final int REPEATED_MEMBER_DIFF_ROLE = 1615;
    7082
    7183    private static volatile ElemStyles styles;
    7284
    7385    private final Set<String> keysCheckedByAnotherTest = new HashSet<>();
     86    /** All way segments, grouped by cells */
     87    private final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000);
     88    /** The already detected ways in error */
     89    private final Map<List<Way>, List<WaySegment>> crossingWays = new HashMap<>(50);
    7490
    7591    /**
    7692     * Constructs a new {@code MultipolygonTest}.
     
    103119        super.endTest();
    104120    }
    105121
    106     private static GeneralPath createPath(List<Node> nodes) {
    107         GeneralPath result = new GeneralPath();
    108         result.moveTo((float) nodes.get(0).getCoor().lat(), (float) nodes.get(0).getCoor().lon());
     122    private static Path2D.Double createPath(List<Node> nodes) {
     123        Path2D.Double result = new Path2D.Double();
     124        result.moveTo(nodes.get(0).getCoor().lon(), nodes.get(0).getCoor().lat());
    109125        for (int i = 1; i < nodes.size(); i++) {
    110126            Node n = nodes.get(i);
    111             result.lineTo((float) n.getCoor().lat(), (float) n.getCoor().lon());
     127            result.lineTo(n.getCoor().lon(), n.getCoor().lat());
    112128        }
    113129        return result;
    114130    }
    115131
    116     private static List<GeneralPath> createPolygons(List<Multipolygon.PolyData> joinedWays) {
    117         List<GeneralPath> result = new ArrayList<>();
     132    private static List<Path2D.Double> createPolygons(List<Multipolygon.PolyData> joinedWays) {
     133        List<Path2D.Double> result = new ArrayList<>();
    118134        for (Multipolygon.PolyData way : joinedWays) {
    119135            result.add(createPath(way.getNodes()));
    120136        }
     
    121137        return result;
    122138    }
    123139
    124     private static Intersection getPolygonIntersection(GeneralPath outer, List<Node> inner) {
    125         boolean inside = false;
    126         boolean outside = false;
    127 
    128         for (Node n : inner) {
    129             boolean contains = outer.contains(n.getCoor().lat(), n.getCoor().lon());
    130             inside = inside | contains;
    131             outside = outside | !contains;
    132             if (inside & outside) {
    133                 return Intersection.CROSSING;
    134             }
    135         }
    136 
    137         return inside ? Intersection.INSIDE : Intersection.OUTSIDE;
    138     }
    139 
    140140    @Override
    141141    public void visit(Way w) {
    142142        if (!w.isArea() && ElemStyles.hasOnlyAreaElemStyle(w)) {
     
    155155    @Override
    156156    public void visit(Relation r) {
    157157        if (r.isMultipolygon()) {
     158            crossingWays.clear();
     159            cellSegments.clear();
     160
    158161            checkMembersAndRoles(r);
     162            checkRepeatedMembers(r);
    159163            checkOuterWay(r);
    160164
    161165            // Rest of checks is only for complete multipolygons
     
    163167                Multipolygon polygon = MultipolygonCache.getInstance().get(Main.map.mapView, r);
    164168
    165169                // Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match.
    166                 checkMemberRoleCorrectness(r);
     170                boolean rolesWereChecked = checkMemberRoleCorrectness(r);
    167171                checkStyleConsistency(r, polygon);
    168                 checkGeometry(r, polygon);
     172                checkGeometry(r, polygon, rolesWereChecked);
    169173            }
    170174        }
    171175    }
     
    194198     * <li>{@link #WRONG_MEMBER_ROLE}: Role for ''{0}'' should be ''{1}''</li>
    195199     * </ul>
    196200     * @param r relation
     201     * @return true if member roles were checked
    197202     */
    198     private void checkMemberRoleCorrectness(Relation r) {
     203    private boolean checkMemberRoleCorrectness(Relation r) {
    199204        final Pair<Relation, Relation> newMP = CreateMultipolygonAction.createMultipolygonRelation(r.getMemberPrimitives(Way.class), false);
    200205        if (newMP != null) {
    201206            for (RelationMember member : r.getMembers()) {
     
    216221                }
    217222            }
    218223        }
     224        return newMP != null;
    219225    }
    220226
    221227    /**
     
    285291        }
    286292    }
    287293
     294    private static class LatLonPolyData {
     295        final PolyData pd;
     296        final Path2D.Double latLonPath;
     297
     298        LatLonPolyData(PolyData polyData, Path2D.Double path) {
     299            this.pd = polyData;
     300            this.latLonPath = path;
     301        }
     302    }
     303
    288304    /**
    289305     * Various geometry-related checks:<ul>
    290306     * <li>{@link #NON_CLOSED_WAY}: Multipolygon is not closed</li>
     
    293309     * </ul>
    294310     * @param r relation
    295311     * @param polygon multipolygon
     312     * @param rolesWereChecked might be used to skip most of the tests below
    296313     */
    297     private void checkGeometry(Relation r, Multipolygon polygon) {
     314    private void checkGeometry(Relation r, Multipolygon polygon, boolean rolesWereChecked) {
    298315        List<Node> openNodes = polygon.getOpenEnds();
    299316        if (!openNodes.isEmpty()) {
    300317            List<OsmPrimitive> primitives = new LinkedList<>();
    301318            primitives.add(r);
    302319            primitives.addAll(openNodes);
    303             addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon is not closed"), NON_CLOSED_WAY, primitives, openNodes));
     320            addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon is not closed"), NON_CLOSED_WAY,
     321                    primitives, openNodes));
    304322        }
     323        List<PolyData> innerPolygons = polygon.getInnerPolygons();
     324        List<PolyData> outerPolygons = polygon.getOuterPolygons();
    305325
     326        HashMap<PolyData, List<PolyData>> crossingPolyMap = checkCrossingWays(r, innerPolygons, outerPolygons);
     327
     328        //Polygons may intersect without crossing ways when one polygon lies completely inside the other
     329        List<LatLonPolyData> inner = calcLatLonPolygons(innerPolygons);
     330        List<LatLonPolyData> outer = calcLatLonPolygons(outerPolygons);
    306331        // For painting is used Polygon class which works with ints only. For validation we need more precision
    307         List<PolyData> innerPolygons = polygon.getInnerPolygons();
    308         List<PolyData> outerPolygons = polygon.getOuterPolygons();
    309         List<GeneralPath> innerPolygonsPaths = innerPolygons.isEmpty() ? Collections.<GeneralPath>emptyList() : createPolygons(innerPolygons);
    310         List<GeneralPath> outerPolygonsPaths = createPolygons(outerPolygons);
    311         for (int i = 0; i < outerPolygons.size(); i++) {
    312             PolyData pdOuter = outerPolygons.get(i);
    313             // Check for intersection between outer members
    314             for (int j = i+1; j < outerPolygons.size(); j++) {
    315                 checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdOuter, j);
     332        for (int i = 0; i + 1 < outer.size(); i++) {
     333            // report outer polygons which lie inside another outer
     334            LatLonPolyData outer1 = outer.get(i);
     335            for (int j = 0; j < outer.size(); j++) {
     336                if (i != j && !crossing(crossingPolyMap, outer1.pd, outer.get(j).pd)) {
     337                    LatLon c = outer.get(j).pd.getNodes().get(0).getCoor();
     338                    if (outer1.latLonPath.contains(c.lon(), c.lat())) {
     339                        addError(r,
     340                                new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
     341                                        CROSSING_WAYS, Collections.singletonList(r),
     342                                        Arrays.asList(outer1.pd.getNodes(), outer.get(j).pd.getNodes())));
     343                    }
     344                }
    316345            }
    317346        }
    318         for (int i = 0; i < innerPolygons.size(); i++) {
    319             PolyData pdInner = innerPolygons.get(i);
    320             // Check for intersection between inner members
    321             for (int j = i+1; j < innerPolygons.size(); j++) {
    322                 checkCrossingWays(r, innerPolygons, innerPolygonsPaths, pdInner, j);
     347        for (int i = 0; i < inner.size(); i++) {
     348            LatLonPolyData inner1 = inner.get(i);
     349            LatLon innerPoint = inner1.pd.getNodes().get(0).getCoor();
     350            for (int j = 0; j < inner.size(); j++) {
     351                LatLonPolyData inner2 = inner.get(j);
     352                if (i != j && !crossing(crossingPolyMap, inner1.pd, inner2.pd)) {
     353                    if (inner.get(j).latLonPath.contains(innerPoint.lon(), innerPoint.lat())) {
     354                        addError(r,
     355                                new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
     356                                        CROSSING_WAYS, Collections.singletonList(r),
     357                                        Arrays.asList(inner1.pd.getNodes(), inner2.pd.getNodes())));
     358                    }
     359                }
    323360            }
    324             // Check for intersection between inner and outer members
     361
     362            // Find inner polygons which are not inside any outer
    325363            boolean outside = true;
    326             for (int o = 0; o < outerPolygons.size(); o++) {
    327                 outside &= checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdInner, o) == Intersection.OUTSIDE;
     364            boolean crossingWithOuter = false;
     365
     366            for (int o = 0; o < outer.size(); o++) {
     367                if (crossing(crossingPolyMap, inner1.pd, outer.get(o).pd)) {
     368                    crossingWithOuter = true;
     369                    break;
     370                }
     371                outside &= outer.get(o).latLonPath.contains(innerPoint.lon(), innerPoint.lat()) == false;
     372                if (!outside)
     373                    break;
    328374            }
    329             if (outside) {
     375            if (outside && !crossingWithOuter) {
    330376                addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon inner way is outside"),
    331                         INNER_WAY_OUTSIDE, Collections.singletonList(r), Arrays.asList(pdInner.getNodes())));
     377                        INNER_WAY_OUTSIDE, Collections.singletonList(r), Arrays.asList(inner1.pd.getNodes())));
    332378            }
    333379        }
    334380    }
    335381
    336     private Intersection checkCrossingWays(Relation r, List<PolyData> polygons, List<GeneralPath> polygonsPaths, PolyData pd, int idx) {
    337         Intersection intersection = getPolygonIntersection(polygonsPaths.get(idx), pd.getNodes());
    338         if (intersection == Intersection.CROSSING) {
    339             PolyData pdOther = polygons.get(idx);
    340             if (pdOther != null) {
    341                 addError(r, new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
    342                         CROSSING_WAYS, Collections.singletonList(r), Arrays.asList(pd.getNodes(), pdOther.getNodes())));
    343             }
     382    private boolean crossing(HashMap<PolyData, List<PolyData>> crossingPolyMap, PolyData pd1, PolyData pd2) {
     383        List<PolyData> crossingWithFirst = crossingPolyMap.get(pd1);
     384        if (crossingWithFirst != null) {
     385            if (crossingWithFirst.contains(pd2))
     386                return true;
    344387        }
    345         return intersection;
     388        List<PolyData> crossingWith2nd = crossingPolyMap.get(pd2);
     389        if (crossingWith2nd != null) {
     390            if (crossingWith2nd.contains(pd1))
     391                return true;
     392        }
     393        return false;
    346394    }
    347395
     396    private List<LatLonPolyData> calcLatLonPolygons(List<PolyData> polygons) {
     397        if (polygons == null || polygons.isEmpty())
     398            return Collections.emptyList();
     399        List<LatLonPolyData> latLonPolygons = new ArrayList<>();
     400        List<Path2D.Double> polygonsPaths = createPolygons(polygons);
     401        for (int i = 0; i < polygons.size(); i++) {
     402            latLonPolygons.add(new LatLonPolyData(polygons.get(i), polygonsPaths.get(i)));
     403        }
     404        return latLonPolygons;
     405    }
     406
    348407    /**
    349408     * Check for:<ul>
    350409     * <li>{@link #WRONG_MEMBER_ROLE}: No useful role for multipolygon member</li>
     
    389448        addRelationIfNeeded(error, r);
    390449        errors.add(error);
    391450    }
     451
     452    /**
     453     * Determine multipolygon ways which are intersecting. This is now allowed.
     454     * See {@link CrossingWays}
     455     * @param r the relation (for error reporting)
     456     * @param innerPolygons list of inner polygons
     457     * @param outerPolygons list of outer polygons
     458     * @return map of crossing polygons (including polygons touching outer)
     459     */
     460    private HashMap<PolyData, List<PolyData>> checkCrossingWays(Relation r, List<PolyData> innerPolygons,
     461            List<PolyData> outerPolygons) {
     462        List<Way> innerWays = new ArrayList<>();
     463        List<Way> outerWays = new ArrayList<>();
     464        for (Way w : r.getMemberPrimitives(Way.class)) {
     465            for (PolyData pd : innerPolygons) {
     466                if (pd.getWayIds().contains(w.getUniqueId())) {
     467                    innerWays.add(w);
     468                    break;
     469                }
     470            }
     471            for (PolyData pd : outerPolygons) {
     472                if (pd.getWayIds().contains(w.getUniqueId())) {
     473                    outerWays.add(w);
     474                    break;
     475                }
     476            }
     477        }
     478        for (Way w : innerWays) {
     479            checkCrossingWay(w, r, true /* allow shared ways */);
     480        }
     481        for (Way w : outerWays) {
     482            checkCrossingWay(w, r, false/* don't allow shared ways */);
     483        }
     484        HashMap<PolyData, List<PolyData>> crossingPolygonsMap = new HashMap<>();
     485        for (Entry<List<Way>, List<WaySegment>> entry : crossingWays.entrySet()) {
     486            List<Way> ways = entry.getKey();
     487            PolyData[] crossingPolys = new PolyData[2];
     488            for (Way w : ways) {
     489                for (int j = 0; j < crossingPolys.length; j++) {
     490                    for (PolyData pd : innerPolygons) {
     491                        if (pd.getWayIds().contains(w.getUniqueId())) {
     492                            crossingPolys[j] = pd;
     493                            break;
     494                        }
     495                    }
     496                    if (crossingPolys[j] != null)
     497                        break;
     498                    for (PolyData pd : outerPolygons) {
     499                        if (pd.getWayIds().contains(w.getUniqueId())) {
     500                            crossingPolys[j] = pd;
     501                            break;
     502                        }
     503                    }
     504                }
     505            }
     506            if (crossingPolys[0] != null && crossingPolys[1] != null) {
     507                List<PolyData> x = crossingPolygonsMap.get(crossingPolys[0]);
     508                if (x == null) {
     509                    x = new ArrayList<>();
     510                    crossingPolygonsMap.put(crossingPolys[0], x);
     511                }
     512                x.add(crossingPolys[1]);
     513            }
     514        }
     515        return crossingPolygonsMap;
     516    }
     517
     518    /**
     519     *
     520     * @param w way that is member of the relation
     521     * @param r the relation (used for error messages)
     522     * @param allowSharedWaySegment false: treat similar way segment as crossing
     523     */
     524    private void checkCrossingWay(Way w, Relation r, boolean allowSharedWaySegment) {
     525
     526        int nodesSize = w.getNodesCount();
     527        for (int i = 0; i < nodesSize - 1; i++) {
     528            final WaySegment es1 = new WaySegment(w, i);
     529            final EastNorth en1 = es1.getFirstNode().getEastNorth();
     530            final EastNorth en2 = es1.getSecondNode().getEastNorth();
     531            if (en1 == null || en2 == null) {
     532                Main.warn("Crossing ways test skipped " + es1);
     533                continue;
     534            }
     535            for (List<WaySegment> segments : CrossingWays.getSegments(cellSegments, en1, en2)) {
     536                for (WaySegment es2 : segments) {
     537
     538                    List<WaySegment> highlight;
     539                    if (es2.way == w)
     540                        continue; // reported by CrossingWays.SelfIntersection
     541                    if (!es1.intersects(es2)) {
     542                        if (allowSharedWaySegment || !es1.isSimilar(es2))
     543                            continue;
     544                    }
     545
     546                    List<Way> prims = Arrays.asList(es1.way, es2.way);
     547                    if ((highlight = crossingWays.get(prims)) == null) {
     548                        highlight = new ArrayList<>();
     549                        highlight.add(es1);
     550                        highlight.add(es2);
     551
     552                        addError(r, new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
     553                                CROSSING_WAYS, Arrays.asList(r, es1.way, es2.way), highlight));
     554                        crossingWays.put(prims, highlight);
     555                    } else {
     556                        highlight.add(es1);
     557                        highlight.add(es2);
     558                    }
     559                }
     560                segments.add(es1);
     561            }
     562        }
     563    }
     564
     565    /**
     566     * Check for:<ul>
     567     * <li>{@link #REPEATED_MEMBER_DIFF_ROLE}: Multipolygon member(s) repeated with different role</li>
     568     * <li>{@link #REPEATED_MEMBER_SAME_ROLE}: Multipolygon member(s) repeated with same role</li>
     569     * </ul>
     570     * @param r relation
     571     */
     572    private void checkRepeatedMembers(Relation r) {
     573        boolean hasDups = false;
     574        Map<OsmPrimitive, List<RelationMember>> seenMemberPrimitives = new HashMap<>();
     575        for (RelationMember rm : r.getMembers()) {
     576            List<RelationMember> list = seenMemberPrimitives.get(rm.getMember());
     577            if (list == null) {
     578                list = new ArrayList<>(2);
     579                seenMemberPrimitives.put(rm.getMember(), list);
     580            } else
     581                hasDups = true;
     582            list.add(rm);
     583        }
     584        if (!hasDups)
     585            return;
     586
     587        List<OsmPrimitive> repeatedSameRole = new ArrayList<>();
     588        List<OsmPrimitive> repeatedDiffRole = new ArrayList<>();
     589        for (Entry<OsmPrimitive, List<RelationMember>> e : seenMemberPrimitives.entrySet()) {
     590            List<RelationMember> visited = e.getValue();
     591            if (e.getValue().size() == 1)
     592                continue;
     593            // we found a duplicate member, check if the roles differ
     594            boolean rolesDiffer = false;
     595            RelationMember rm = visited.get(0);
     596            List<OsmPrimitive> primitives = new ArrayList<>();
     597            for (int i = 1; i < visited.size(); i++) {
     598                RelationMember v = visited.get(i);
     599                primitives.add(rm.getMember());
     600                if (v.getRole().equals(rm.getRole()) == false) {
     601                    rolesDiffer = true;
     602                }
     603            }
     604            if (rolesDiffer)
     605                repeatedDiffRole.addAll(primitives);
     606            else
     607                repeatedSameRole.addAll(primitives);
     608        }
     609        addRepeatedMemberError(r, repeatedDiffRole, REPEATED_MEMBER_DIFF_ROLE, tr("Multipolygon member(s) repeated with different role"));
     610        addRepeatedMemberError(r, repeatedSameRole, REPEATED_MEMBER_SAME_ROLE, tr("Multipolygon member(s) repeated with same role"));
     611    }
     612
     613    private void addRepeatedMemberError(Relation r, List<OsmPrimitive> repeatedMembers, int errorCode, String msg) {
     614        if (!repeatedMembers.isEmpty()) {
     615            List<OsmPrimitive> prims = new ArrayList<>(1 + repeatedMembers.size());
     616            prims.add(r);
     617            prims.addAll(repeatedMembers);
     618            addError(r, new TestError(this, Severity.WARNING, msg, errorCode, prims, repeatedMembers));
     619        }
     620
     621    }
     622
     623    @Override
     624    public Command fixError(TestError testError) {
     625        if (testError.getCode() == REPEATED_MEMBER_SAME_ROLE) {
     626            ArrayList<OsmPrimitive> primitives = new ArrayList<>(testError.getPrimitives());
     627            if (primitives.size() >= 2) {
     628                if (primitives.get(0) instanceof Relation) {
     629                    Relation r = (Relation) primitives.get(0);
     630                    return new RemoveRepeatedRelationMembersCommand(r, primitives.subList(1, primitives.size()));
     631                }
     632            }
     633        }
     634        return null;
     635    }
     636
     637    @Override
     638    public boolean isFixable(TestError testError) {
     639        if (testError.getCode() == REPEATED_MEMBER_SAME_ROLE)
     640            return true;
     641        return false;
     642    }
    392643}