Changeset 18976 in josm


Ignore:
Timestamp:
2024-02-13T09:29:09+01:00 (3 months ago)
Author:
GerdP
Message:

fix #20424, fix #20425: Duplicate relations not detected with incomplete members, Duplicate Relation test is too lazy/too aggressive

  • add code to compare just members so that it doesn't matter if they are complete
  • change original test which compares geometry to use a list instead of a set to store members. That means that the order of members is taken into account as well as duplicated members
  • before comparing tags only the "discardable" tag keys are removed, previously all "uninteresting" keys were removed, so fixme=* or note=* was ignored
  • if two relations have the same members in the same order but different tags the new message informational message "Identical members" is produced.
Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/data/validation/tests/DuplicateRelation.java

    r17476 r18976  
    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;
     
    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;
     
    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
     
    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
     
    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));
     
    176179    protected static final int SAME_RELATION = 1902;
    177180
    178     /** MultiMap of all relations */
    179     private MultiMap<RelationPair, OsmPrimitive> relations;
    180 
    181     /** MultiMap of all relations, regardless of keys */
    182     private MultiMap<List<RelationMember>, OsmPrimitive> relationsNoKeys;
    183 
    184     /** List of keys without useful information */
    185     private final Set<String> ignoreKeys = new HashSet<>(AbstractPrimitive.getUninterestingKeys());
     181    /** Code number of relation with same members error */
     182    protected static final int IDENTICAL_MEMBERLIST = 1903;
     183
     184
     185    /** List of all initially visited testable relations*/
     186    List<Relation> visited;
    186187
    187188    /**
     
    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    }
     
    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        if (!visited.isEmpty())
     206            performChecks();
     207        visited = null;
     208        super.endTest();
     209    }
     210
     211    private void performChecks() {
     212        MultiMap<RelationPair, Relation> relations = new MultiMap<>(1000);
     213        // MultiMap of all relations, regardless of keys
     214        MultiMap<List<RelationMember>, Relation> sameMembers = new MultiMap<>(1000);
     215
     216        for (Relation r : visited) {
     217            final List<RelationMember> rMembers = getSortedMembers(r);
     218            sameMembers.put(rMembers, r);
     219            addToRelations(relations, r, true);
     220        }
     221
     222        if (partialSelection) {
     223            // add data for relations which were not in the initial selection when
     224            // they can be duplicates
     225            DataSet ds = visited.iterator().next().getDataSet();
     226            for (Relation r : ds.getRelations()) {
     227                if (r.isDeleted() || r.getMembers().isEmpty() || visited.contains(r))
     228                    continue;
     229                final List<RelationMember> rMembers = getSortedMembers(r);
     230                if (sameMembers.containsKey(rMembers))
     231                    sameMembers.put(rMembers, r);
     232
     233                addToRelations(relations, r, false);
     234            }
     235        }
     236
     237        for (Set<Relation> duplicated : sameMembers.values()) {
     238            if (duplicated.size() > 1) {
     239                checkOrderAndTags(duplicated);
     240            }
     241        }
     242
     243        performGeometryTest(relations);
     244    }
     245
     246    private void performGeometryTest(MultiMap<RelationPair, Relation> relations) {
     247        // perform special test to find relations with different members but (possibly) same geometry
     248        // this test is rather speculative and works only with complete members
     249        for (Set<Relation> duplicated : relations.values()) {
    205250            if (duplicated.size() > 1) {
    206251                TestError testError = TestError.builder(this, Severity.ERROR, DUPLICATE_RELATION)
    207                         .message(tr("Duplicated relations"))
     252                        .message(tr(DUPLICATED_RELATIONS))
    208253                        .primitives(duplicated)
    209254                        .build();
    210                 errors.add(testError);
    211             }
    212         }
    213         relations = null;
    214         for (Set<OsmPrimitive> duplicated : relationsNoKeys.values()) {
    215             if (duplicated.size() > 1) {
    216                 TestError testError = TestError.builder(this, Severity.OTHER, SAME_RELATION)
    217                         .message(tr("Relations with same members"))
    218                         .primitives(duplicated)
    219                         .build();
    220                 errors.add(testError);
    221             }
    222         }
    223         relationsNoKeys = null;
    224         super.endTest();
     255                if (errors.stream().noneMatch(e -> e.isSimilar(testError))) {
     256                    errors.add(testError);
     257                }
     258            }
     259        }
     260    }
     261
     262    private static void addToRelations(MultiMap<RelationPair, Relation> relations, Relation r, boolean forceAdd) {
     263        if (r.isUsable() && !r.hasIncompleteMembers()) {
     264            RelationPair rKey = new RelationPair(r.getMembers(), cleanedKeys(r));
     265            if (forceAdd || (!relations.isEmpty() && !relations.containsKey(rKey)))
     266                relations.put(rKey, r);
     267        }
    225268    }
    226269
    227270    @Override
    228271    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())
    231             return;
    232         List<RelationMember> rMembers = r.getMembers();
    233         Map<String, String> rkeys = r.getKeys();
    234         for (String key : ignoreKeys) {
    235             rkeys.remove(key);
    236         }
    237         RelationPair rKey = new RelationPair(rMembers, rkeys);
    238         relations.put(rKey, r);
    239         relationsNoKeys.put(rMembers, r);
     272        if (!r.isDeleted() && r.getMembersCount() > 0)
     273            visited.add(r);
     274    }
     275
     276    /**
     277     * Check a list of relations which are guaranteed to have the same members, possibly in different order
     278     * and possibly different tags.
     279     * @param sameMembers the list of relations
     280     */
     281    private void checkOrderAndTags(Set<Relation> sameMembers) {
     282        MultiMap<List<RelationMember>, Relation> sameOrder = new MultiMap<>();
     283        MultiMap<Map<String, String>, Relation> sameKeys = new MultiMap<>();
     284        for (Relation r : sameMembers) {
     285            sameOrder.put(r.getMembers(), r);
     286            sameKeys.put(cleanedKeys(r), r);
     287        }
     288        for (Set<Relation> duplicated : sameKeys.values()) {
     289            if (duplicated.size() > 1) {
     290                reportDuplicateKeys(duplicated);
     291            }
     292        }
     293        for (Set<Relation> duplicated : sameOrder.values()) {
     294            if (duplicated.size() > 1) {
     295                reportDuplicateMembers(duplicated);
     296            }
     297        }
     298        List<Relation> primitives = sameMembers.stream().filter(r -> !ignoredType(r)).collect(Collectors.toList());
     299        // report this collection if not already reported
     300        if (primitives.size() > 1 && errors.stream().noneMatch(e -> e.getPrimitives().containsAll(primitives))) {
     301            // same members, possibly different order
     302            TestError testError = TestError.builder(this, Severity.OTHER, SAME_RELATION)
     303                    .message(tr("Relations with same members"))
     304                    .primitives(primitives)
     305                    .build();
     306            errors.add(testError);
     307        }
     308    }
     309
     310    /**
     311     * Check collection of relations with the same keys and members, possibly in different order
     312     * @param duplicated collection of relations, caller must make sure that they have the same keys and members
     313     */
     314    private void reportDuplicateKeys(Collection<Relation> duplicated) {
     315        Relation first = duplicated.iterator().next();
     316        if (memberOrderMatters(first)) {
     317            List<Relation> toCheck = new ArrayList<>(duplicated);
     318            while (toCheck.size() > 1) {
     319                Relation ref = toCheck.iterator().next();
     320                List<Relation> same = toCheck.stream()
     321                        .filter(r -> r.getMembers().equals(ref.getMembers())).collect(Collectors.toList());
     322                if (same.size() > 1) {
     323                    // same members and keys, members in same order
     324                    TestError testError = TestError.builder(this, Severity.ERROR, DUPLICATE_RELATION)
     325                            .message(tr(DUPLICATED_RELATIONS))
     326                            .primitives(same)
     327                            .build();
     328                    errors.add(testError);
     329                }
     330                toCheck.removeAll(same);
     331            }
     332        } else {
     333            // same members and keys, possibly different order
     334            TestError testError = TestError.builder(this, Severity.ERROR, DUPLICATE_RELATION)
     335                    .message(tr(DUPLICATED_RELATIONS))
     336                    .primitives(duplicated)
     337                    .build();
     338            errors.add(testError);
     339        }
     340    }
     341
     342    /**
     343     * Report relations with the same member(s) in the same order, keys may be different
     344     * @param duplicated collection of relations with identical member list
     345     */
     346    private void reportDuplicateMembers(Set<Relation> duplicated) {
     347        List<Relation> primitives = duplicated.stream().filter(r -> !ignoredType(r)).collect(Collectors.toList());
     348        if (primitives.size() > 1 && errors.stream().noneMatch(e -> e.getPrimitives().containsAll(primitives))) {
     349            TestError testError = TestError.builder(this, Severity.OTHER, IDENTICAL_MEMBERLIST)
     350                    .message(tr("Identical members"))
     351                    .primitives(primitives)
     352                    .build();
     353            errors.add(testError);
     354        }
     355
     356    }
     357
     358    private static boolean memberOrderMatters(Relation r) {
     359        return r.hasTag("type", "route", "waterway"); // add more types?
     360    }
     361
     362    private static boolean ignoredType(Relation r) {
     363        return r.hasTag("type", "tmc", "TMC", "destination_sign"); // see r11783
     364    }
     365
     366    /** return tags of a primitive after removing all discardable tags
     367     *
     368     * @param p the primitive
     369     * @return map with cleaned tags
     370     */
     371    private static Map<String, String> cleanedKeys(OsmPrimitive p) {
     372        Map<String, String> cleaned = p.getKeys();
     373        for (String key : AbstractPrimitive.getDiscardableKeys()) {
     374            cleaned.remove(key);
     375        }
     376        return cleaned;
     377    }
     378
     379    /**
     380     *  Order members of given relation by type, unique id, and role.
     381     *  @param r the relation
     382     * @return sorted list of members
     383     */
     384    private static List<RelationMember> getSortedMembers(Relation r) {
     385        return r.getMembers().stream().sorted((m1, m2) -> {
     386            int d = m1.getMember().compareTo(m2.getMember());
     387            if (d != 0)
     388                return d;
     389            return m1.getRole().compareTo(m2.getRole());
     390        }).collect(Collectors.toList());
    240391    }
    241392
     
    301452    public boolean isFixable(TestError testError) {
    302453        if (!(testError.getTester() instanceof DuplicateRelation)
    303             || testError.getCode() == SAME_RELATION) return false;
     454            || testError.getCode() != DUPLICATE_RELATION) return false;
    304455
    305456        // We fix it only if there is no more than one relation that is relation member.
  • trunk/test/unit/org/openstreetmap/josm/data/validation/tests/DuplicateRelationTest.java

    r18037 r18976  
    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;
     
    1213import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
    1314import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
    14 
    15 import org.junit.jupiter.api.Test;
    1615
    1716/**
     
    4544            assertEquals(expected.code, error.getCode());
    4645            assertEquals(expected.fixable, error.isFixable());
     46            if (error.isFixable())
     47                error.getFix();
    4748        }
    4849    }
     
    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
     
    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
     
    8485    void testDuplicateRelationDifferentTags() {
    8586        doTest("type=boundary", "type=multipolygon",
    86                 new ExpectedResult(DuplicateRelation.SAME_RELATION, false));
     87                new ExpectedResult(DuplicateRelation.IDENTICAL_MEMBERLIST, false));
     88    }
     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));
    87204    }
    88205}
Note: See TracChangeset for help on using the changeset viewer.