Ticket #19312: 19312.2.patch

File 19312.2.patch, 11.8 KB (added by GerdP, 5 years ago)
  • src/org/openstreetmap/josm/data/validation/tests/RelationChecker.java

     
    66
    77import java.util.ArrayList;
    88import java.util.Collection;
     9import java.util.Collections;
    910import java.util.EnumSet;
    1011import java.util.HashMap;
     12import java.util.Iterator;
    1113import java.util.LinkedHashMap;
     14import java.util.LinkedHashSet;
    1215import java.util.LinkedList;
    1316import java.util.List;
    1417import java.util.Map;
    1518import java.util.stream.Collectors;
    1619
     20import org.openstreetmap.josm.command.ChangeCommand;
    1721import org.openstreetmap.josm.command.Command;
    1822import org.openstreetmap.josm.command.DeleteCommand;
    1923import org.openstreetmap.josm.data.osm.OsmPrimitive;
     
    6165    public static final int RELATION_EMPTY   = 1708;
    6266    /** Type ''{0}'' of relation member with role ''{1}'' does not match accepted types ''{2}'' in preset {3} */
    6367    public static final int WRONG_TYPE       = 1709;
     68    /** Relations build circular dependencies */
     69    public static final int RELATION_LOOP    = 1710;
    6470    // CHECKSTYLE.ON: SingleSpaceSeparator
    6571
    6672    /**
     
    7076    public static final String ROLE_VERIF_PROBLEM_MSG = tr("Role verification problem");
    7177    private boolean ignoreMultiPolygons;
    7278    private boolean ignoreTurnRestrictions;
    73 
     79    private final List<List<Relation>> loops = new ArrayList<>();
    7480    /**
    7581     * Constructor
    7682     */
     
    154160        if (!map.isEmpty() && !allroles.isEmpty()) {
    155161            checkRoles(n, allroles, map);
    156162        }
     163        checkLoop(n);
    157164    }
    158165
    159166    private static Map<String, RoleInfo> buildRoleInfoMap(Relation n) {
     
    365372    public Command fixError(TestError testError) {
    366373        Collection<? extends OsmPrimitive> primitives = testError.getPrimitives();
    367374        if (isFixable(testError) && !primitives.iterator().next().isDeleted()) {
    368             return new DeleteCommand(primitives);
     375            if (testError.getCode() == RELATION_EMPTY) {
     376                return new DeleteCommand(primitives);
     377            }
     378            if (testError.getCode() == RELATION_LOOP) {
     379                Relation old = (Relation) primitives.iterator().next();
     380                Relation mod = new Relation(old);
     381                mod.removeMembersFor(primitives);
     382                return new ChangeCommand(old, mod);
     383            }
    369384        }
    370385        return null;
    371386    }
     
    373388    @Override
    374389    public boolean isFixable(TestError testError) {
    375390        Collection<? extends OsmPrimitive> primitives = testError.getPrimitives();
    376         return testError.getCode() == RELATION_EMPTY && !primitives.isEmpty() && primitives.iterator().next().isNew();
     391        return testError.getCode() == RELATION_EMPTY && !primitives.isEmpty() && primitives.iterator().next().isNew() ||
     392                testError.getCode() == RELATION_LOOP && primitives.size() == 1;
    377393    }
    378394
    379395    @Override
     
    381397        relationpresets.clear();
    382398        initializePresets();
    383399    }
     400
     401    @Override
     402    public void endTest() {
     403        loops.forEach(loop -> errors.add(TestError.builder(this, Severity.ERROR, RELATION_LOOP)
     404                .message(loop.size() == 2 ? tr("Relation contains itself as a member") : tr("Relations build circular dependencies"))
     405                .primitives(new LinkedHashSet<>(loop))
     406                .build()));
     407        loops.clear();
     408        super.endTest();
     409    }
     410
     411    /**
     412     * Check if a given relation is part of a circular dependency loop.
     413     * @param r the relation
     414     */
     415    private void checkLoop(Relation r) {
     416        checkLoop(r, new LinkedList<>());
     417    }
     418
     419    private void checkLoop(Relation parent, List<Relation> path) {
     420        if (path.contains(parent)) {
     421            Iterator<List<Relation>> iter = loops.iterator();
     422            while (iter.hasNext()) {
     423                List<Relation> loop = iter.next();
     424                if (loop.size() > path.size() && loop.containsAll(path)) {
     425                    // remove same loop with irrelevant parent
     426                    iter.remove();
     427                } else if (path.size() >= loop.size() && path.containsAll(loop)) {
     428                    // same or smaller loop is already known
     429                    return;
     430                }
     431            }
     432            if (path.get(0).equals(parent)) {
     433                path.add(parent);
     434                loops.add(path);
     435            }
     436            return;
     437        }
     438        path.add(parent);
     439        for (Relation sub : parent.getMemberPrimitives(Relation.class)) {
     440            if (sub.isUsable() && !sub.isIncomplete()) {
     441                checkLoop(sub, new LinkedList<>(path));
     442            }
     443        }
     444    }
     445
     446    /**
     447     * Check if adding one relation to another would produce a circular dependency.
     448     * @param parent the relation which would be changed
     449     * @param child the child relation which should be added to parent
     450     * @return An unmodifiable list of relations which is empty when no circular dependency was found,
     451     * else it contains the relations that form circular dependencies.
     452     * The list then contains at least two items. Normally first and last item are both {@code parent},
     453     * but if child is already part of a circular dependency the returned list may not end with {@code parent}.
     454     */
     455    public static List<Relation> checkAddMember(Relation parent, Relation child) {
     456        if (parent == null || child == null || child.isIncomplete())
     457            return Collections.emptyList();
     458        RelationChecker test = new RelationChecker();
     459        LinkedList<Relation> path = new LinkedList<>();
     460        path.add(parent);
     461        test.checkLoop(child, path);
     462        if (test.loops.isEmpty())
     463            return Collections.emptyList();
     464        else
     465            return Collections.unmodifiableList(test.loops.iterator().next());
     466    }
     467
    384468}
  • src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java

     
    5555import org.openstreetmap.josm.data.osm.Relation;
    5656import org.openstreetmap.josm.data.osm.RelationMember;
    5757import org.openstreetmap.josm.data.osm.Tag;
     58import org.openstreetmap.josm.data.validation.tests.RelationChecker;
    5859import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
    5960import org.openstreetmap.josm.gui.MainApplication;
    6061import org.openstreetmap.josm.gui.MainMenu;
     
    884885     * @param primitive the concerned primitive
    885886     */
    886887    public static void warnOfCircularReferences(OsmPrimitive primitive) {
    887         String msg = tr("<html>You are trying to add a relation to itself.<br>"
    888                 + "<br>"
    889                 + "This creates circular references and is therefore discouraged.<br>"
    890                 + "Skipping relation ''{0}''.</html>",
    891                 Utils.escapeReservedCharactersHTML(primitive.getDisplayName(DefaultNameFormatter.getInstance())));
     888        warnOfCircularReferences(primitive, Collections.emptyList());
     889    }
     890
     891    /**
     892     * Warn about circular references.
     893     * @param primitive the concerned primitive
     894     * @param loop list of relation that form the circular dependencies
     895     * @since xxx
     896     */
     897
     898    public static void warnOfCircularReferences(OsmPrimitive primitive, List<Relation> loop) {
     899        final String msg;
     900        if (loop.size() <= 2) {
     901            msg = tr("<html>You are trying to add a relation to itself.<br>"
     902                    + "<br>"
     903                    + "This creates a circular dependency and is therefore discouraged.<br>"
     904                    + "Skipping relation ''{0}''.</html>",
     905                    Utils.escapeReservedCharactersHTML(primitive.getDisplayName(DefaultNameFormatter.getInstance())));
     906
     907        } else {
     908            msg = tr("<html>You are trying to add a relation which refers to the relation in the editor.<br>"
     909                    + "<br>"
     910                    + "This creates a circular dependency and is therefore discouraged.<br>"
     911                    + "Skipping relation ''{0}''."
     912                    + "<br>"
     913                    + "Relations that would build the circular dependency:<br>{1}</html>",
     914                    Utils.escapeReservedCharactersHTML(primitive.getDisplayName(DefaultNameFormatter.getInstance())),
     915                    loop.stream().map(
     916                            p -> Utils.escapeReservedCharactersHTML(p.getDisplayName(DefaultNameFormatter.getInstance())))
     917                    .collect(Collectors.joining(" -> <br>"))
     918                    );
     919        }
    892920        JOptionPane.showMessageDialog(
    893921                MainApplication.getMainFrame(),
    894922                msg,
     
    895923                tr("Warning"),
    896924                JOptionPane.WARNING_MESSAGE);
    897925    }
    898 
    899926    /**
    900927     * Adds primitives to a given relation.
    901928     * @param orig The relation to modify
     
    911938            Relation relation = new Relation(orig);
    912939            boolean modified = false;
    913940            for (OsmPrimitive p : primitivesToAdd) {
    914                 if (p instanceof Relation && orig.equals(p)) {
    915                     warnOfCircularReferences(p);
    916                     continue;
     941                if (p instanceof Relation) {
     942                    List<Relation> loop = RelationChecker.checkAddMember(relation, (Relation) p);
     943                    if (!loop.isEmpty() && loop.get(0).equals(loop.get(loop.size() - 1))) {
     944                        warnOfCircularReferences(p, loop);
     945                        continue;
     946                    }
    917947                } else if (MemberTableModel.hasMembersReferringTo(relation.getMembers(), Collections.singleton(p))
    918948                        && !confirmAddingPrimitive(p)) {
    919949                    continue;
  • src/org/openstreetmap/josm/gui/dialogs/relation/actions/AddFromSelectionAction.java

     
    77
    88import org.openstreetmap.josm.data.osm.OsmPrimitive;
    99import org.openstreetmap.josm.data.osm.Relation;
     10import org.openstreetmap.josm.data.validation.tests.RelationChecker;
    1011import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
    1112import org.openstreetmap.josm.gui.dialogs.relation.GenericRelationEditor;
    1213import org.openstreetmap.josm.gui.dialogs.relation.GenericRelationEditor.AddAbortException;
     
    3334        List<OsmPrimitive> ret = new ArrayList<>();
    3435        ConditionalOptionPaneUtil.startBulkOperation("add_primitive_to_relation");
    3536        for (OsmPrimitive primitive : primitives) {
    36             if (primitive instanceof Relation
    37                     && editorAccess.getEditor().getRelation() != null && editorAccess.getEditor().getRelation().equals(primitive)) {
    38                 GenericRelationEditor.warnOfCircularReferences(primitive);
    39                 continue;
     37            if (primitive instanceof Relation) {
     38                List<Relation> loop = RelationChecker.checkAddMember(editorAccess.getEditor().getRelation(), (Relation) primitive);
     39                if (!loop.isEmpty() && loop.get(0).equals(loop.get(loop.size() - 1))) {
     40                    GenericRelationEditor.warnOfCircularReferences(primitive, loop);
     41                    continue;
     42                }
    4043            }
    4144            if (isPotentialDuplicate(primitive)) {
    4245                if (GenericRelationEditor.confirmAddingPrimitive(primitive)) {