Ticket #13307: improve_MultipolygonTest_v9.patch

File improve_MultipolygonTest_v9.patch, 32.5 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/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;
     16import java.util.Iterator;
    1417import java.util.LinkedList;
    1518import java.util.List;
     19import java.util.Map;
     20import java.util.Map.Entry;
    1621import java.util.Set;
    1722
    1823import org.openstreetmap.josm.Main;
    1924import org.openstreetmap.josm.actions.CreateMultipolygonAction;
     25import org.openstreetmap.josm.command.Command;
     26import org.openstreetmap.josm.command.RemoveRepeatedRelationMembersCommand;
     27import org.openstreetmap.josm.data.coor.EastNorth;
     28import org.openstreetmap.josm.data.coor.LatLon;
    2029import org.openstreetmap.josm.data.osm.Node;
    2130import org.openstreetmap.josm.data.osm.OsmPrimitive;
    2231import org.openstreetmap.josm.data.osm.Relation;
    2332import org.openstreetmap.josm.data.osm.RelationMember;
    2433import org.openstreetmap.josm.data.osm.Way;
     34import org.openstreetmap.josm.data.osm.WaySegment;
    2535import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
     36import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.JoinedWay;
    2637import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
    27 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
    2838import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
    2939import org.openstreetmap.josm.data.validation.OsmValidator;
    3040import org.openstreetmap.josm.data.validation.Severity;
     
    6777    public static final int NO_STYLE_POLYGON = 1611;
    6878    /** Area style on outer way */
    6979    public static final int OUTER_STYLE = 1613;
     80    /** Multipolygon member repeated (same primitive, same role */
     81    public static final int REPEATED_MEMBER_SAME_ROLE = 1614;
     82    /** Multipolygon member repeated (same primitive, different role) */
     83    public static final int REPEATED_MEMBER_DIFF_ROLE = 1615;
    7084
    7185    private static volatile ElemStyles styles;
    7286
     
    103117        super.endTest();
    104118    }
    105119
    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());
     120    private static Path2D.Double createPath(List<Node> nodes) {
     121        Path2D.Double result = new Path2D.Double();
     122        result.moveTo(nodes.get(0).getCoor().lon(), nodes.get(0).getCoor().lat());
    109123        for (int i = 1; i < nodes.size(); i++) {
    110124            Node n = nodes.get(i);
    111             result.lineTo((float) n.getCoor().lat(), (float) n.getCoor().lon());
     125            result.lineTo(n.getCoor().lon(), n.getCoor().lat());
    112126        }
    113127        return result;
    114128    }
    115129
    116     private static List<GeneralPath> createPolygons(List<Multipolygon.PolyData> joinedWays) {
    117         List<GeneralPath> result = new ArrayList<>();
     130    private static List<Path2D.Double> createPolygons(List<Multipolygon.PolyData> joinedWays) {
     131        List<Path2D.Double> result = new ArrayList<>();
    118132        for (Multipolygon.PolyData way : joinedWays) {
    119133            result.add(createPath(way.getNodes()));
    120134        }
     
    121135        return result;
    122136    }
    123137
    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 
    140138    @Override
    141139    public void visit(Way w) {
    142140        if (!w.isArea() && ElemStyles.hasOnlyAreaElemStyle(w)) {
     
    156154    public void visit(Relation r) {
    157155        if (r.isMultipolygon()) {
    158156            checkMembersAndRoles(r);
     157            checkRepeatedMembers(r);
    159158            checkOuterWay(r);
     159            List<Node> intersectionNodes = checkIntersectionAtNodes(r);
    160160
    161161            // Rest of checks is only for complete multipolygons
    162162            if (!r.hasIncompleteMembers()) {
     163                boolean rolesWereChecked = checkMemberRoleCorrectness(r);
    163164                Multipolygon polygon = MultipolygonCache.getInstance().get(Main.map.mapView, r);
    164 
    165                 // Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match.
    166                 checkMemberRoleCorrectness(r);
    167165                checkStyleConsistency(r, polygon);
    168                 checkGeometry(r, polygon);
     166                checkGeometry(r, polygon, intersectionNodes, rolesWereChecked);
    169167            }
    170168        }
    171169    }
     
    190188    }
    191189
    192190    /**
    193      * Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match:<ul>
     191     * If simple joining of ways doesn't work, create new multipolygon using the logics from
     192     * CreateMultipolygonAction and see if roles match:<ul>
    194193     * <li>{@link #WRONG_MEMBER_ROLE}: Role for ''{0}'' should be ''{1}''</li>
    195194     * </ul>
    196195     * @param r relation
     196     * @return true if member roles were checked
    197197     */
    198     private void checkMemberRoleCorrectness(Relation r) {
     198    private boolean checkMemberRoleCorrectness(Relation r) {
    199199        final Pair<Relation, Relation> newMP = CreateMultipolygonAction.createMultipolygonRelation(r.getMemberPrimitives(Way.class), false);
     200
    200201        if (newMP != null) {
    201202            for (RelationMember member : r.getMembers()) {
    202203                final Collection<RelationMember> memberInNewMP = newMP.b.getMembersFor(Collections.singleton(member.getMember()));
     
    216217                }
    217218            }
    218219        }
     220        return newMP != null;
    219221    }
    220222
    221223    /**
     
    285287        }
    286288    }
    287289
     290    private static class LatLonPolyData {
     291        final PolyData pd;
     292        final Path2D.Double latLonPath;
     293
     294        LatLonPolyData(PolyData polyData, Path2D.Double path) {
     295            this.pd = polyData;
     296            this.latLonPath = path;
     297        }
     298    }
     299
    288300    /**
    289301     * Various geometry-related checks:<ul>
    290302     * <li>{@link #NON_CLOSED_WAY}: Multipolygon is not closed</li>
     
    293305     * </ul>
    294306     * @param r relation
    295307     * @param polygon multipolygon
     308     * @param intersectionNodes known nodes where ways of this multipolygon intersect or touch
     309     * @param rolesWereChecked might be used to skip most of the tests below
    296310     */
    297     private void checkGeometry(Relation r, Multipolygon polygon) {
    298         List<Node> openNodes = polygon.getOpenEnds();
     311    private void checkGeometry(Relation r, Multipolygon polygon, List<Node> intersectionNodes, boolean rolesWereChecked) {
     312        Collection<JoinedWay> allPolygons = Multipolygon.joinWays(r.getMemberPrimitives(Way.class));
     313        List<Node> openNodes = new ArrayList<>();
     314        Iterator<JoinedWay> iter = allPolygons.iterator();
     315        while (iter.hasNext()) {
     316            JoinedWay jw = iter.next();
     317            if (jw.isClosed())
     318                continue;
     319            openNodes.add(jw.getFirstNode());
     320            openNodes.add(jw.getLastNode());
     321            iter.remove();
     322        }
    299323        if (!openNodes.isEmpty()) {
    300324            List<OsmPrimitive> primitives = new LinkedList<>();
    301325            primitives.add(r);
    302326            primitives.addAll(openNodes);
    303             addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon is not closed"), NON_CLOSED_WAY, primitives, openNodes));
     327            addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon is not closed"), NON_CLOSED_WAY,
     328                    primitives, openNodes));
    304329        }
    305330
    306         // For painting is used Polygon class which works with ints only. For validation we need more precision
    307331        List<PolyData> innerPolygons = polygon.getInnerPolygons();
    308332        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);
     333
     334        HashMap<PolyData, List<PolyData>> crossingPolyMap = checkCrossingWays(r, innerPolygons, outerPolygons);
     335
     336        //Polygons may intersect without crossing ways when one polygon lies completely inside the other
     337        List<LatLonPolyData> inner = calcLatLonPolygons(innerPolygons);
     338        List<LatLonPolyData> outer = calcLatLonPolygons(outerPolygons);
     339        for (int i = 0; i < outer.size(); i++) {
     340            // report outer polygons which lie inside another outer
     341            LatLonPolyData outer1 = outer.get(i);
     342            for (int j = 0; j < outer.size(); j++) {
     343                if (i != j && !crossing(crossingPolyMap, outer1.pd, outer.get(j).pd)) {
     344                    LatLon c = null;
     345                    // find node which is not a intersection of multipolygon ways
     346                    for (int k = 0; k < outer.get(j).pd.getNodes().size(); k++) {
     347                        Node nc = outer.get(j).pd.getNodes().get(k);
     348                        if (intersectionNodes.contains(nc) == false)
     349                            c = nc.getCoor();
     350                    }
     351                    if (c != null && outer1.latLonPath.contains(c.lon(), c.lat())) {
     352//                        List<OsmPrimitive> l = new ArrayList<>();
     353//                        l.add(r);
     354//                        l.add(member.getMember());
     355//                        addError(r, new TestError(this, Severity.WARNING, RelationChecker.ROLE_VERIF_PROBLEM_MSG,
     356//                                tr("Role for ''{0}'' should be ''{1}''",
     357//                                        member.getMember().getDisplayName(DefaultNameFormatter.getInstance()), roleInNewMP),
     358//                                MessageFormat.format("Role for ''{0}'' should be ''{1}''",
     359//                                        member.getMember().getDisplayName(DefaultNameFormatter.getInstance()), roleInNewMP),
     360//                                WRONG_MEMBER_ROLE, l, Collections.singleton(member.getMember())));
     361                        addError(r,
     362                                new TestError(this, Severity.WARNING, tr("Outer inside outer"),
     363                                        CROSSING_WAYS, Collections.singletonList(r),
     364                                        Arrays.asList(outer1.pd.getNodes(), outer.get(j).pd.getNodes())));
     365                    }
     366                }
    316367            }
    317368        }
    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);
     369        for (int i = 0; i < inner.size(); i++) {
     370            LatLonPolyData inner1 = inner.get(i);
     371            for (int j = 0; j < inner.size(); j++) {
     372                LatLonPolyData inner2 = inner.get(j);
     373                if (i != j && !crossing(crossingPolyMap, inner1.pd, inner2.pd)) {
     374                    // we must allow that inner polygons share some nodes
     375                    boolean allInside = true;
     376                    for (Node innerPoint: inner2.pd.getNodes()) {
     377                        if (!inner1.latLonPath.contains(innerPoint.getCoor().lon(), innerPoint.getCoor().lat())) {
     378                            allInside = false;
     379                            break;
     380                        }
     381                    }
     382                    if (allInside) {
     383                        addError(r,
     384                                new TestError(this, Severity.WARNING, tr("Inner inside inner"),
     385                                        CROSSING_WAYS, Collections.singletonList(r),
     386                                        Arrays.asList(inner1.pd.getNodes(), inner2.pd.getNodes())));
     387                    }
     388                }
    323389            }
    324             // Check for intersection between inner and outer members
    325             boolean outside = true;
    326             for (int o = 0; o < outerPolygons.size(); o++) {
    327                 outside &= checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdInner, o) == Intersection.OUTSIDE;
     390
     391            // Find inner polygons which are not inside any outer
     392            if (true || !rolesWereChecked) {
     393                boolean outside = true;
     394                boolean crossingWithOuter = false;
     395                LatLon innerPoint = inner1.pd.getNodes().get(0).getCoor();
     396                for (int o = 0; o < outer.size(); o++) {
     397                    if (crossing(crossingPolyMap, inner1.pd, outer.get(o).pd)) {
     398                        crossingWithOuter = true;
     399                        break;
     400                    }
     401                    outside &= outer.get(o).latLonPath.contains(innerPoint.lon(), innerPoint.lat()) == false;
     402                    if (!outside)
     403                        break;
     404                }
     405                if (outside && !crossingWithOuter) {
     406                    addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon inner way is outside"),
     407                            INNER_WAY_OUTSIDE, Collections.singletonList(r), Arrays.asList(inner1.pd.getNodes())));
     408                }
    328409            }
    329             if (outside) {
    330                 addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon inner way is outside"),
    331                         INNER_WAY_OUTSIDE, Collections.singletonList(r), Arrays.asList(pdInner.getNodes())));
    332             }
    333410        }
    334411    }
    335412
    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             }
     413    private boolean crossing(HashMap<PolyData, List<PolyData>> crossingPolyMap, PolyData pd1, PolyData pd2) {
     414        List<PolyData> crossingWithFirst = crossingPolyMap.get(pd1);
     415        if (crossingWithFirst != null) {
     416            if (crossingWithFirst.contains(pd2))
     417                return true;
    344418        }
    345         return intersection;
     419        List<PolyData> crossingWith2nd = crossingPolyMap.get(pd2);
     420        if (crossingWith2nd != null) {
     421            if (crossingWith2nd.contains(pd1))
     422                return true;
     423        }
     424        return false;
    346425    }
    347426
     427    private List<LatLonPolyData> calcLatLonPolygons(List<PolyData> polygons) {
     428        if (polygons == null || polygons.isEmpty())
     429            return Collections.emptyList();
     430        List<LatLonPolyData> latLonPolygons = new ArrayList<>();
     431        List<Path2D.Double> polygonsPaths = createPolygons(polygons);
     432        for (int i = 0; i < polygons.size(); i++) {
     433            latLonPolygons.add(new LatLonPolyData(polygons.get(i), polygonsPaths.get(i)));
     434        }
     435        return latLonPolygons;
     436    }
     437
    348438    /**
    349439     * Check for:<ul>
    350440     * <li>{@link #WRONG_MEMBER_ROLE}: No useful role for multipolygon member</li>
     
    389479        addRelationIfNeeded(error, r);
    390480        errors.add(error);
    391481    }
     482
     483    /**
     484     * Determine multipolygon ways which are intersecting (crossing without a common node). This is not allowed.
     485     * See {@link CrossingWays}
     486     * @param r the relation (for error reporting)
     487     * @param innerPolygons list of inner polygons
     488     * @param outerPolygons list of outer polygons
     489     * @return map of crossing polygons (including polygons touching outer)
     490     */
     491    private HashMap<PolyData, List<PolyData>> checkCrossingWays(Relation r, List<PolyData> innerPolygons,
     492            List<PolyData> outerPolygons) {
     493        /** All way segments, grouped by cells */
     494        final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000);
     495        /** The already detected ways in error */
     496        final Map<List<Way>, List<WaySegment>> crossingWays = new HashMap<>(50);
     497
     498        for (Way w : r.getMemberPrimitives(Way.class)) {
     499            checkCrossingWay(w, r, cellSegments, crossingWays);
     500        }
     501        HashMap<PolyData, List<PolyData>> crossingPolygonsMap = new HashMap<>();
     502        if (!crossingWays.isEmpty()) {
     503            List<PolyData> allPolygons = new ArrayList<>(innerPolygons.size() + outerPolygons.size());
     504            allPolygons.addAll(innerPolygons);
     505            allPolygons.addAll(outerPolygons);
     506
     507            for (Entry<List<Way>, List<WaySegment>> entry : crossingWays.entrySet()) {
     508                List<Way> ways = entry.getKey();
     509                if (ways.size() != 2)
     510                    continue;
     511                PolyData[] crossingPolys = new PolyData[2];
     512                for (int i = 0; i < 2; i++) {
     513                    Way w = ways.get(i);
     514                    for (PolyData pd : allPolygons) {
     515                        if (pd.getWayIds().contains(w.getUniqueId())) {
     516                            crossingPolys[i] = pd;
     517                            break;
     518                        }
     519                    }
     520                }
     521                if (crossingPolys[0] != null && crossingPolys[1] != null) {
     522                    List<PolyData> crossingPolygons = crossingPolygonsMap.get(crossingPolys[0]);
     523                    if (crossingPolygons == null) {
     524                        crossingPolygons = new ArrayList<>();
     525                        crossingPolygonsMap.put(crossingPolys[0], crossingPolygons);
     526                    }
     527                    crossingPolygons.add(crossingPolys[1]);
     528                }
     529            }
     530        }
     531        return crossingPolygonsMap;
     532    }
     533
     534    /**
     535     * Find ways which are crossing without sharing a node.
     536     * @param w way that is member of the relation
     537     * @param r the relation (used for error messages)
     538     * @param crossingWays
     539     * @param cellSegments
     540     */
     541    private void checkCrossingWay(Way w, Relation r, Map<Point2D, List<WaySegment>> cellSegments, Map<List<Way>, List<WaySegment>> crossingWays) {
     542        int nodesSize = w.getNodesCount();
     543        for (int i = 0; i < nodesSize - 1; i++) {
     544            final WaySegment es1 = new WaySegment(w, i);
     545            final EastNorth en1 = es1.getFirstNode().getEastNorth();
     546            final EastNorth en2 = es1.getSecondNode().getEastNorth();
     547            if (en1 == null || en2 == null) {
     548                Main.warn("Crossing ways test skipped " + es1);
     549                continue;
     550            }
     551            for (List<WaySegment> segments : CrossingWays.getSegments(cellSegments, en1, en2)) {
     552                for (WaySegment es2 : segments) {
     553
     554                    List<WaySegment> highlight;
     555                    if (es2.way == w)
     556                        continue; // reported by CrossingWays.SelfIntersection
     557                    if (!es1.intersects(es2))
     558                        continue;
     559
     560                    List<Way> prims = Arrays.asList(es1.way, es2.way);
     561                    if ((highlight = crossingWays.get(prims)) == null) {
     562                        highlight = new ArrayList<>();
     563                        highlight.add(es1);
     564                        highlight.add(es2);
     565                        addError(r, new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
     566                                CROSSING_WAYS, Arrays.asList(r, es1.way, es2.way), highlight));
     567                        crossingWays.put(prims, highlight);
     568                    } else {
     569                        highlight.add(es1);
     570                        highlight.add(es2);
     571                    }
     572                }
     573                segments.add(es1);
     574            }
     575        }
     576    }
     577
     578    /**
     579     * Detect intersections of multipolygon ways at nodes. If any way node is used by more than two ways
     580     * or two times in one way and at least once in another way we found an intersection.
     581     * @param r the relation
     582     * @return List of nodes were ways intersect
     583     */
     584    private List<Node> checkIntersectionAtNodes(Relation r) {
     585        List<Node> intersectionNodes = new ArrayList<>();
     586        List<Pair<Set<Way>,List<Node>>> intersectionErrors = new ArrayList<>();
     587        HashMap<Way, String> wayRoleMap = new HashMap<>();
     588        for (RelationMember rm : r.getMembers()) {
     589            if (rm.isWay())
     590                wayRoleMap.put(rm.getWay(), rm.getRole());
     591        }
     592        Map<Node, List<Way>> nodeMap = new HashMap<>();
     593        for (RelationMember rm : r.getMembers()) {
     594            if (!rm.isWay())
     595                continue;
     596            int numNodes = rm.getWay().getNodesCount();
     597            for (int i = 0; i < numNodes; i++) {
     598                Node n = rm.getWay().getNode(i);
     599                if (n.getReferrers().size() <= 1) {
     600                    continue; // cannot be a problem node
     601                }
     602                List<Way> ways = nodeMap.get(n);
     603                if (ways == null) {
     604                    ways = new ArrayList<>();
     605                    nodeMap.put(n, ways);
     606                }
     607                ways.add(rm.getWay());
     608                if (ways.size() > 2 || (ways.size() == 2 && i != 0 && i + 1 != numNodes)) {
     609                    intersectionNodes.add(n);
     610                    boolean allInner = true;
     611                    for (Way w : ways) {
     612                        String role = wayRoleMap.get(w);
     613                        if (!"inner".equals(role))
     614                            allInner = false;
     615                    }
     616                    if (!allInner) {
     617                        Set<Way> errorWays = new HashSet<>(ways);
     618                        boolean addNew = true;
     619                        for (Pair<Set<Way>, List<Node>> pair : intersectionErrors) {
     620                            if (pair.a.size() == errorWays.size() && pair.a.containsAll(errorWays)) {
     621                                pair.b.add(n);
     622                                addNew = false;
     623                                break;
     624                            }
     625                        }
     626                        if (addNew) {
     627                            List<Node> errNodes = new ArrayList<>();
     628                            errNodes.add(n);
     629                            intersectionErrors.add(new Pair<>(errorWays, errNodes));
     630                        }
     631                    }
     632                }
     633            }
     634        }
     635        for (Pair<Set<Way>,List<Node>> pair : intersectionErrors) {
     636            errors.add(new TestError(this, Severity.WARNING, tr("Multipolygon ways share node(s)"),
     637                    CROSSING_WAYS, pair.a, pair.b));
     638        }
     639        return intersectionNodes;
     640    }
     641
     642    /**
     643     * Check for:<ul>
     644     * <li>{@link #REPEATED_MEMBER_DIFF_ROLE}: Multipolygon member(s) repeated with different role</li>
     645     * <li>{@link #REPEATED_MEMBER_SAME_ROLE}: Multipolygon member(s) repeated with same role</li>
     646     * </ul>
     647     * @param r relation
     648     */
     649    private void checkRepeatedMembers(Relation r) {
     650        boolean hasDups = false;
     651        Map<OsmPrimitive, List<RelationMember>> seenMemberPrimitives = new HashMap<>();
     652        for (RelationMember rm : r.getMembers()) {
     653            List<RelationMember> list = seenMemberPrimitives.get(rm.getMember());
     654            if (list == null) {
     655                list = new ArrayList<>(2);
     656                seenMemberPrimitives.put(rm.getMember(), list);
     657            } else
     658                hasDups = true;
     659            list.add(rm);
     660        }
     661        if (!hasDups)
     662            return;
     663
     664        List<OsmPrimitive> repeatedSameRole = new ArrayList<>();
     665        List<OsmPrimitive> repeatedDiffRole = new ArrayList<>();
     666        for (Entry<OsmPrimitive, List<RelationMember>> e : seenMemberPrimitives.entrySet()) {
     667            List<RelationMember> visited = e.getValue();
     668            if (e.getValue().size() == 1)
     669                continue;
     670            // we found a duplicate member, check if the roles differ
     671            boolean rolesDiffer = false;
     672            RelationMember rm = visited.get(0);
     673            List<OsmPrimitive> primitives = new ArrayList<>();
     674            for (int i = 1; i < visited.size(); i++) {
     675                RelationMember v = visited.get(i);
     676                primitives.add(rm.getMember());
     677                if (v.getRole().equals(rm.getRole()) == false) {
     678                    rolesDiffer = true;
     679                }
     680            }
     681            if (rolesDiffer)
     682                repeatedDiffRole.addAll(primitives);
     683            else
     684                repeatedSameRole.addAll(primitives);
     685        }
     686        addRepeatedMemberError(r, repeatedDiffRole, REPEATED_MEMBER_DIFF_ROLE, tr("Multipolygon member(s) repeated with different role"));
     687        addRepeatedMemberError(r, repeatedSameRole, REPEATED_MEMBER_SAME_ROLE, tr("Multipolygon member(s) repeated with same role"));
     688    }
     689
     690    private void addRepeatedMemberError(Relation r, List<OsmPrimitive> repeatedMembers, int errorCode, String msg) {
     691        if (!repeatedMembers.isEmpty()) {
     692            List<OsmPrimitive> prims = new ArrayList<>(1 + repeatedMembers.size());
     693            prims.add(r);
     694            prims.addAll(repeatedMembers);
     695            addError(r, new TestError(this, Severity.WARNING, msg, errorCode, prims, repeatedMembers));
     696        }
     697
     698    }
     699
     700    @Override
     701    public Command fixError(TestError testError) {
     702        if (testError.getCode() == REPEATED_MEMBER_SAME_ROLE) {
     703            ArrayList<OsmPrimitive> primitives = new ArrayList<>(testError.getPrimitives());
     704            if (primitives.size() >= 2) {
     705                if (primitives.get(0) instanceof Relation) {
     706                    Relation r = (Relation) primitives.get(0);
     707                    return new RemoveRepeatedRelationMembersCommand(r, primitives.subList(1, primitives.size()));
     708                }
     709            }
     710        }
     711        return null;
     712    }
     713
     714    @Override
     715    public boolean isFixable(TestError testError) {
     716        if (testError.getCode() == REPEATED_MEMBER_SAME_ROLE)
     717            return true;
     718        return false;
     719    }
     720
    392721}