Ticket #20425: 20425.patch

File 20425.patch, 19.3 KB (added by GerdP, 21 months ago)
  • src/org/openstreetmap/josm/data/validation/tests/DuplicateRelation.java

     
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.validation.tests;
    33
     4import static org.openstreetmap.josm.tools.I18n.marktr;
    45import static org.openstreetmap.josm.tools.I18n.tr;
    56
    67import java.util.ArrayList;
    78import java.util.Collection;
    8 import java.util.HashSet;
    99import java.util.LinkedList;
    1010import java.util.List;
    1111import java.util.Map;
     
    1919import org.openstreetmap.josm.command.SequenceCommand;
    2020import org.openstreetmap.josm.data.coor.LatLon;
    2121import org.openstreetmap.josm.data.osm.AbstractPrimitive;
     22import org.openstreetmap.josm.data.osm.DataSet;
    2223import org.openstreetmap.josm.data.osm.Node;
    2324import org.openstreetmap.josm.data.osm.OsmPrimitive;
    2425import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
     
    3637 */
    3738public class DuplicateRelation extends Test {
    3839
     40    private static final String DUPLICATED_RELATIONS = marktr("Duplicated relations");
     41
    3942    /**
    4043     * Class to store one relation members and information about it
    4144     */
     
    110113     */
    111114    private static class RelationMembers {
    112115        /** Set of member objects of the relation */
    113         private final Set<RelMember> members;
     116        private final List<RelMember> members;
    114117
    115118        /** Store relation information
    116119         * @param members The list of relation members
    117120         */
    118121        RelationMembers(List<RelationMember> members) {
    119             this.members = new HashSet<>(members.size());
     122            this.members = new ArrayList<>(members.size());
    120123            for (RelationMember member : members) {
    121124                this.members.add(new RelMember(member));
    122125            }
     
    175178    /** Code number of relation with same members error */
    176179    protected static final int SAME_RELATION = 1902;
    177180
    178     /** MultiMap of all relations */
    179     private MultiMap<RelationPair, OsmPrimitive> relations;
     181    /** Code number of relation with same members error */
     182    protected static final int IDENTICAL_MEMBERLIST = 1903;
    180183
    181     /** MultiMap of all relations, regardless of keys */
    182     private MultiMap<List<RelationMember>, OsmPrimitive> relationsNoKeys;
    183184
    184     /** List of keys without useful information */
    185     private final Set<String> ignoreKeys = new HashSet<>(AbstractPrimitive.getUninterestingKeys());
     185    /** List of all initially visited testable relations*/
     186    List<Relation> visited;
    186187
    187188    /**
    188189     * Default constructor
    189190     */
    190191    public DuplicateRelation() {
    191         super(tr("Duplicated relations"),
     192        super(tr(DUPLICATED_RELATIONS),
    192193                tr("This test checks that there are no relations with same tags and same members with same roles."));
    193194    }
    194195
     
    195196    @Override
    196197    public void startTest(ProgressMonitor monitor) {
    197198        super.startTest(monitor);
    198         relations = new MultiMap<>(1000);
    199         relationsNoKeys = new MultiMap<>(1000);
     199        visited = new ArrayList<>();
     200
    200201    }
    201202
    202203    @Override
    203204    public void endTest() {
    204         for (Set<OsmPrimitive> duplicated : relations.values()) {
     205        MultiMap<RelationPair, Relation> relations = new MultiMap<>(1000);
     206        /** MultiMap of all relations, regardless of keys */
     207        MultiMap<List<RelationMember>, Relation> sameMembers = new MultiMap<>(1000);
     208
     209        for (Relation r : visited) {
     210            final List<RelationMember> rMembers = getSortedMembers(r);
     211            sameMembers.put(rMembers, r);
     212            addToRelations(relations, r, true);
     213        }
     214
     215        if (partialSelection) {
     216            // add data for relations which were not in the initial selection when
     217            // they can be duplicates
     218            DataSet ds = visited.iterator().next().getDataSet();
     219            for (Relation r : ds.getRelations()) {
     220                if (r.isDeleted() || r.getMembers().isEmpty() || visited.contains(r))
     221                    continue;
     222                final List<RelationMember> rMembers = getSortedMembers(r);
     223                if (sameMembers.containsKey(rMembers))
     224                    sameMembers.put(rMembers, r);
     225
     226                addToRelations(relations, r, false);
     227            }
     228        }
     229
     230        for (Set<Relation> duplicated : sameMembers.values()) {
    205231            if (duplicated.size() > 1) {
    206                 TestError testError = TestError.builder(this, Severity.ERROR, DUPLICATE_RELATION)
    207                         .message(tr("Duplicated relations"))
    208                         .primitives(duplicated)
    209                         .build();
    210                 errors.add(testError);
     232                checkOrderAndTags(duplicated);
    211233            }
    212234        }
    213         relations = null;
    214         for (Set<OsmPrimitive> duplicated : relationsNoKeys.values()) {
     235
     236        performGeometryTest(relations);
     237        visited = null;
     238        super.endTest();
     239    }
     240
     241    private void performGeometryTest(MultiMap<RelationPair, Relation> relations) {
     242        // perform special test to find relations with different members but (possibly) same geometry
     243        // this test is rather speculative and works only with complete members
     244        for (Set<Relation> duplicated : relations.values()) {
    215245            if (duplicated.size() > 1) {
    216                 TestError testError = TestError.builder(this, Severity.OTHER, SAME_RELATION)
    217                         .message(tr("Relations with same members"))
     246                TestError testError = TestError.builder(this, Severity.ERROR, DUPLICATE_RELATION)
     247                        .message(tr(DUPLICATED_RELATIONS))
    218248                        .primitives(duplicated)
    219249                        .build();
    220                 errors.add(testError);
     250                if (errors.stream().noneMatch(e -> e.isSimilar(testError))) {
     251                    errors.add(testError);
     252                }
    221253            }
    222254        }
    223         relationsNoKeys = null;
    224         super.endTest();
    225255    }
    226256
     257    private static void addToRelations(MultiMap<RelationPair, Relation> relations, Relation r, boolean forceAdd) {
     258        if (r.isUsable() && !r.hasIncompleteMembers()) {
     259            RelationPair rKey = new RelationPair(r.getMembers(), cleanedKeys(r));
     260            if (forceAdd || !relations.isEmpty() && !relations.containsKey(rKey))
     261                relations.put(rKey, r);
     262        }
     263    }
     264
    227265    @Override
    228266    public void visit(Relation r) {
    229         if (!r.isUsable() || r.hasIncompleteMembers() || "tmc".equals(r.get("type")) || "TMC".equals(r.get("type"))
    230                || "destination_sign".equals(r.get("type")) || r.getMembers().isEmpty())
     267        if (r.isDeleted() || r.getMembersCount() == 0)
    231268            return;
    232         List<RelationMember> rMembers = r.getMembers();
    233         Map<String, String> rkeys = r.getKeys();
    234         for (String key : ignoreKeys) {
    235             rkeys.remove(key);
     269        visited.add(r);
     270    }
     271
     272    /**
     273     * Check a list of relations which are guaranteed to have the same members, possibly in different order
     274     * and possibly different tags.
     275     * @param sameMembers the list of relations
     276     */
     277    private void checkOrderAndTags(Set<Relation> sameMembers) {
     278        MultiMap<List<RelationMember>, Relation> sameOrder = new MultiMap<>();
     279        MultiMap<Map<String, String>, Relation> sameKeys = new MultiMap<>();
     280        for (Relation r : sameMembers) {
     281            sameOrder.put(r.getMembers(), r);
     282            sameKeys.put(cleanedKeys(r), r);
    236283        }
    237         RelationPair rKey = new RelationPair(rMembers, rkeys);
    238         relations.put(rKey, r);
    239         relationsNoKeys.put(rMembers, r);
     284        for (Set<Relation> duplicated : sameKeys.values()) {
     285            if (duplicated.size() > 1) {
     286                reportDuplicateKeys(duplicated);
     287            }
     288        }
     289        for (Set<Relation> duplicated : sameOrder.values()) {
     290            if (duplicated.size() > 1) {
     291                reportDuplicateMembers(duplicated);
     292            }
     293        }
     294        List<Relation> primitives = sameMembers.stream().filter(r -> !ignoredType(r)).collect(Collectors.toList());
     295        // report this collection if not already reported
     296        if (primitives.size() > 1 && errors.stream().noneMatch(e -> e.getPrimitives().containsAll(primitives))) {
     297            // same members, possibly different order
     298            TestError testError = TestError.builder(this, Severity.OTHER, SAME_RELATION)
     299                    .message(tr("Relations with same members"))
     300                    .primitives(primitives)
     301                    .build();
     302            errors.add(testError);
     303        }
    240304    }
    241305
    242306    /**
     307     * Check collection of relations with the same keys and members, possibly in different order
     308     * @param duplicated collection of relations, caller must make sure that they have the same keys and members
     309     */
     310    private void reportDuplicateKeys(Collection<Relation> duplicated) {
     311        Relation first = duplicated.iterator().next();
     312        if (memberOrderMatters(first)) {
     313            List<Relation> toCheck = new ArrayList<>(duplicated);
     314            while (toCheck.size() > 1) {
     315                Relation ref = toCheck.iterator().next();
     316                List<Relation> same = toCheck.stream()
     317                        .filter(r -> r.getMembers().equals(ref.getMembers())).collect(Collectors.toList());
     318                if (same.size() > 1) {
     319                    // same members and keys, members in same order
     320                    TestError testError = TestError.builder(this, Severity.ERROR, DUPLICATE_RELATION)
     321                            .message(tr(DUPLICATED_RELATIONS))
     322                            .primitives(same)
     323                            .build();
     324                    errors.add(testError);
     325                }
     326                toCheck.removeAll(same);
     327            }
     328        } else {
     329            // same members and keys, possibly different order
     330            TestError testError = TestError.builder(this, Severity.ERROR, DUPLICATE_RELATION)
     331                    .message(tr(DUPLICATED_RELATIONS))
     332                    .primitives(duplicated)
     333                    .build();
     334            errors.add(testError);
     335        }
     336    }
     337
     338    /**
     339     * Report relations with the same member(s) in the same order, keys may be different
     340     * @param duplicated collection of relations with identical member list
     341     */
     342    private void reportDuplicateMembers(Set<Relation> duplicated) {
     343        List<Relation> primitives = duplicated.stream().filter(r -> !ignoredType(r)).collect(Collectors.toList());
     344        if (primitives.size() > 1 && errors.stream().noneMatch(e -> e.getPrimitives().containsAll(primitives))) {
     345            TestError testError = TestError.builder(this, Severity.OTHER, IDENTICAL_MEMBERLIST)
     346                    .message(tr("Identical members"))
     347                    .primitives(primitives)
     348                    .build();
     349            errors.add(testError);
     350        }
     351
     352    }
     353
     354    private static boolean memberOrderMatters(Relation r) {
     355        return r.hasTag("type", "route", "waterway"); // add more types?
     356    }
     357
     358    private static boolean ignoredType(Relation r) {
     359        return r.hasTag("type", "tmc", "TMC", "destination_sign"); // see r11783
     360    }
     361
     362    /** return tags of a primitive after removing all discardable tags
     363     *
     364     * @param p the primitive
     365     * @return map with cleaned tags
     366     */
     367    private static Map<String, String> cleanedKeys(OsmPrimitive p) {
     368        Map<String, String> cleaned = p.getKeys();
     369        for (String key : AbstractPrimitive.getDiscardableKeys()) {
     370            cleaned.remove(key);
     371        }
     372        return cleaned;
     373    }
     374
     375    /**
     376     *  Order members of given relation by type, unique id, and role.
     377     *  @param r the relation
     378     * @return sorted list of members
     379     */
     380    private static List<RelationMember> getSortedMembers(Relation r) {
     381        return r.getMembers().stream().sorted((m1, m2) -> {
     382            int d = m1.getMember().compareTo(m2.getMember());
     383            if (d != 0)
     384                return d;
     385            return m1.getRole().compareTo(m2.getRole());
     386        }).collect(Collectors.toList());
     387    }
     388
     389    /**
    243390     * Fix the error by removing all but one instance of duplicate relations
    244391     * @param testError The error to fix, must be of type {@link #DUPLICATE_RELATION}
    245392     */
     
    300447    @Override
    301448    public boolean isFixable(TestError testError) {
    302449        if (!(testError.getTester() instanceof DuplicateRelation)
    303             || testError.getCode() == SAME_RELATION) return false;
     450            || testError.getCode() != DUPLICATE_RELATION) return false;
    304451
    305452        // We fix it only if there is no more than one relation that is relation member.
    306453        Set<Relation> rels = testError.primitives(Relation.class)
  • test/unit/org/openstreetmap/josm/data/validation/tests/DuplicateRelationTest.java

     
    33
    44import static org.junit.jupiter.api.Assertions.assertEquals;
    55
     6import org.junit.jupiter.api.Test;
    67import org.openstreetmap.josm.TestUtils;
    78import org.openstreetmap.josm.data.coor.LatLon;
    89import org.openstreetmap.josm.data.osm.DataSet;
     
    1213import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
    1314import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
    1415
    15 import org.junit.jupiter.api.Test;
    16 
    1716/**
    1817 * JUnit Test of "Duplicate relation" validation test.
    1918 */
     
    4443            ExpectedResult expected = expectations[i++];
    4544            assertEquals(expected.code, error.getCode());
    4645            assertEquals(expected.fixable, error.isFixable());
     46            if (error.isFixable())
     47                error.getFix();
    4748        }
    4849    }
    4950
     
    6364    @Test
    6465    void testDuplicateRelationNoTags() {
    6566        doTest("", "",
    66                 new ExpectedResult(DuplicateRelation.DUPLICATE_RELATION, true),
    67                 new ExpectedResult(DuplicateRelation.SAME_RELATION, false));
     67                new ExpectedResult(DuplicateRelation.DUPLICATE_RELATION, true)
     68                );
    6869    }
    6970
    7071    /**
     
    7374    @Test
    7475    void testDuplicateRelationSameTags() {
    7576        doTest("type=boundary", "type=boundary",
    76                 new ExpectedResult(DuplicateRelation.DUPLICATE_RELATION, true),
    77                 new ExpectedResult(DuplicateRelation.SAME_RELATION, false));
     77                new ExpectedResult(DuplicateRelation.DUPLICATE_RELATION, true)
     78                );
    7879    }
    7980
    8081    /**
     
    8384    @Test
    8485    void testDuplicateRelationDifferentTags() {
    8586        doTest("type=boundary", "type=multipolygon",
    86                 new ExpectedResult(DuplicateRelation.SAME_RELATION, false));
     87                new ExpectedResult(DuplicateRelation.IDENTICAL_MEMBERLIST, false));
    8788    }
     89
     90    /**
     91     * Test of duplicate "tmc" relation, should not be ignored
     92     */
     93    @Test
     94    void testTMCRelation1() {
     95        doTest("type=tmc t1=v1", "type=tmc t1=v1",
     96                new ExpectedResult(DuplicateRelation.DUPLICATE_RELATION, true));
     97    }
     98
     99    /**
     100     * Test of "tmc" relation with equal members but different tags, should be ignored
     101     */
     102    @Test
     103    void testTMCRelation2() {
     104        doTest("type=tmc t1=v1", "type=tmc t1=v2");
     105    }
     106
     107    /**
     108     * Test with incomplete members
     109     */
     110    @Test
     111    void testIncomplete() {
     112        DataSet ds = new DataSet();
     113
     114        Node a = new Node(1234);
     115        ds.addPrimitive(a);
     116        ds.addPrimitive(TestUtils.newRelation("type=multipolygon", new RelationMember(null, a)));
     117        ds.addPrimitive(TestUtils.newRelation("type=multipolygon", new RelationMember(null, a)));
     118        performTest(ds, new ExpectedResult(DuplicateRelation.DUPLICATE_RELATION, true));
     119    }
     120
     121    /**
     122     * Test with different order of members, order doesn't count
     123     */
     124    @Test
     125    void testMemberOrder1() {
     126        DataSet ds = new DataSet();
     127
     128        Node a = new Node(1);
     129        Node b = new Node(2);
     130        ds.addPrimitive(a);
     131        ds.addPrimitive(b);
     132        ds.addPrimitive(TestUtils.newRelation("type=multipolygon", new RelationMember(null, a), new RelationMember(null, b)));
     133        ds.addPrimitive(TestUtils.newRelation("type=multipolygon", new RelationMember(null, b), new RelationMember(null, a)));
     134        performTest(ds, new ExpectedResult(DuplicateRelation.DUPLICATE_RELATION, true));
     135    }
     136
     137    /**
     138     * Test with different order of members, order counts
     139     */
     140    @Test
     141    void testMemberOrder2() {
     142        DataSet ds = new DataSet();
     143
     144        Node a = new Node(1);
     145        a.setCoor(new LatLon(10.0, 5.0));
     146        Node b = new Node(2);
     147        b.setCoor(new LatLon(10.0, 6.0));
     148        ds.addPrimitive(a);
     149        ds.addPrimitive(b);
     150        ds.addPrimitive(TestUtils.newRelation("type=route", new RelationMember(null, a), new RelationMember(null, b)));
     151        ds.addPrimitive(TestUtils.newRelation("type=route", new RelationMember(null, b), new RelationMember(null, a)));
     152        performTest(ds, new ExpectedResult(DuplicateRelation.SAME_RELATION, false));
     153    }
     154
     155    /**
     156     * Test with different order of members, one is duplicated, order doesn't matter
     157     */
     158    @Test
     159    void testMemberOrder3() {
     160        DataSet ds = new DataSet();
     161
     162        Node a = new Node(1);
     163        Node b = new Node(2);
     164        ds.addPrimitive(a);
     165        ds.addPrimitive(b);
     166        ds.addPrimitive(TestUtils.newRelation("type=restriction", new RelationMember(null, a),
     167                new RelationMember(null, b), new RelationMember(null, a)));
     168        ds.addPrimitive(TestUtils.newRelation("type=restriction", new RelationMember(null, b),
     169                new RelationMember(null, a), new RelationMember(null, a)));
     170        performTest(ds, new ExpectedResult(DuplicateRelation.DUPLICATE_RELATION, true));
     171    }
     172
     173    /**
     174     * Test with different order of members, one is duplicated in one of the relations
     175     */
     176    @Test
     177    void testMemberOrder4() {
     178        DataSet ds = new DataSet();
     179        Node a = new Node(new LatLon(10.0, 5.0));
     180        Node b = new Node(new LatLon(10.0, 6.0));
     181        ds.addPrimitive(a);
     182        ds.addPrimitive(b);
     183        ds.addPrimitive(TestUtils.newRelation("", new RelationMember(null, a), new RelationMember(null, b)));
     184        ds.addPrimitive(TestUtils.newRelation("", new RelationMember(null, b), new RelationMember(null, a), new RelationMember(null, b)));
     185        performTest(ds);
     186    }
     187
     188    /**
     189     * Test with two relations where members are different but geometry is equal.
     190     */
     191    @Test
     192    void testImport() {
     193        DataSet ds = new DataSet();
     194        Node a = new Node(1234, 1);
     195        a.setCoor(new LatLon(10.0, 5.0));
     196
     197        Node b = new Node(new LatLon(10.0, 5.0));
     198
     199        ds.addPrimitive(a);
     200        ds.addPrimitive(b);
     201        ds.addPrimitive(TestUtils.newRelation("type=multipolygon", new RelationMember(null, a)));
     202        ds.addPrimitive(TestUtils.newRelation("type=multipolygon", new RelationMember(null, b)));
     203        performTest(ds, new ExpectedResult(DuplicateRelation.DUPLICATE_RELATION, true));
     204    }
    88205}