Ticket #21825: 21825.4.patch

File 21825.4.patch, 18.0 KB (added by taylor.smock, 3 years ago)
  • src/org/openstreetmap/josm/data/osm/IRelation.java

    diff --git a/src/org/openstreetmap/josm/data/osm/IRelation.java b/src/org/openstreetmap/josm/data/osm/IRelation.java
    index 6d59bd427a..9a7897d36f 100644
    a b  
    22package org.openstreetmap.josm.data.osm;
    33
    44import java.util.Collection;
     5import java.util.Collections;
    56import java.util.List;
    67import java.util.stream.Collectors;
    78
     9import org.openstreetmap.josm.data.validation.TestError;
    810import org.openstreetmap.josm.tools.Utils;
    911
    1012/**
    public interface IRelation<M extends IRelationMember<?>> extends IPrimitive {  
    138140        return getMembers().stream().filter(rmv -> role.equals(rmv.getRole()))
    139141                .map(IRelationMember::getMember).collect(Collectors.toList());
    140142    }
     143
     144    /**
     145     * Check if this relation is useful
     146     * @return {@code true} if this relation is useful
     147     */
     148    default boolean isUseful() {
     149        return !this.isEmpty() && this.hasKey("type");
     150    }
     151
     152    /**
     153     * Check if this relation is valid
     154     * @return A collection of errors, if any
     155     */
     156    default Collection<TestError> validate() {
     157        return Collections.emptyList();
     158    }
    141159}
  • src/org/openstreetmap/josm/data/osm/Relation.java

    diff --git a/src/org/openstreetmap/josm/data/osm/Relation.java b/src/org/openstreetmap/josm/data/osm/Relation.java
    index 3b12c48c80..1653b30f2e 100644
    a b import java.util.stream.Stream;  
    1414
    1515import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
    1616import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
     17import org.openstreetmap.josm.data.validation.OsmValidator;
     18import org.openstreetmap.josm.data.validation.TestError;
     19import org.openstreetmap.josm.data.validation.tests.MultipolygonTest;
     20import org.openstreetmap.josm.data.validation.tests.RelationChecker;
     21import org.openstreetmap.josm.data.validation.tests.TurnrestrictionTest;
     22import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
    1723import org.openstreetmap.josm.spi.preferences.Config;
    1824import org.openstreetmap.josm.tools.CopyList;
    1925import org.openstreetmap.josm.tools.SubclassFilteredCollection;
    public final class Relation extends OsmPrimitive implements IRelation<RelationMe  
    567573    public UniqueIdGenerator getIdGenerator() {
    568574        return idGenerator;
    569575    }
     576
     577    @Override
     578    public Collection<TestError> validate() {
     579        return Stream.of(MultipolygonTest.class, TurnrestrictionTest.class, RelationChecker.class)
     580                .map(OsmValidator::getTest).filter(test -> test.enabled || test.testBeforeUpload).flatMap(test -> {
     581            test.startTest(NullProgressMonitor.INSTANCE);
     582            test.visit(this);
     583            test.endTest();
     584            return test.getErrors().stream();
     585        }).collect(Collectors.toList());
     586    }
    570587}
  • src/org/openstreetmap/josm/data/validation/tests/RelationChecker.java

    diff --git a/src/org/openstreetmap/josm/data/validation/tests/RelationChecker.java b/src/org/openstreetmap/josm/data/validation/tests/RelationChecker.java
    index 959f73d1d0..1c10a7c575 100644
    a b public class RelationChecker extends Test implements TaggingPresetListener {  
    154154                    .message(tr("Route scheme is unspecified. Add {0} ({1}=public_transport; {2}=legacy)", "public_transport:version", "2", "1"))
    155155                    .primitives(n)
    156156                    .build());
    157         } else if (n.hasKey("type") && allroles.isEmpty()) {
     157        } else if ((n.hasKey("type") && allroles.isEmpty()) || !n.hasKey("type")) {
    158158            errors.add(TestError.builder(this, Severity.OTHER, RELATION_UNKNOWN)
    159159                    .message(tr("Relation type is unknown"))
    160160                    .primitives(n)
  • src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java

    diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java b/src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java
    index 743e05f4e0..9665db215d 100644
    a b package org.openstreetmap.josm.gui.dialogs.relation;  
    33
    44import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
    55import static org.openstreetmap.josm.tools.I18n.tr;
     6import static org.openstreetmap.josm.tools.I18n.trn;
    67
    78import java.awt.BorderLayout;
    89import java.awt.Component;
    import java.util.ArrayList;  
    2627import java.util.Arrays;
    2728import java.util.Collection;
    2829import java.util.Collections;
     30import java.util.Comparator;
    2931import java.util.EnumSet;
    3032import java.util.List;
    3133import java.util.Set;
    import javax.swing.JTabbedPane;  
    4749import javax.swing.JTable;
    4850import javax.swing.JToolBar;
    4951import javax.swing.KeyStroke;
     52import javax.swing.event.TableModelListener;
    5053
    5154import org.openstreetmap.josm.actions.JosmAction;
    5255import org.openstreetmap.josm.command.ChangeMembersCommand;
    import org.openstreetmap.josm.command.Command;  
    5457import org.openstreetmap.josm.data.UndoRedoHandler;
    5558import org.openstreetmap.josm.data.UndoRedoHandler.CommandQueueListener;
    5659import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
     60import org.openstreetmap.josm.data.osm.IRelation;
    5761import org.openstreetmap.josm.data.osm.OsmPrimitive;
    5862import org.openstreetmap.josm.data.osm.Relation;
    5963import org.openstreetmap.josm.data.osm.RelationMember;
    6064import org.openstreetmap.josm.data.osm.Tag;
     65import org.openstreetmap.josm.data.validation.TestError;
    6166import org.openstreetmap.josm.data.validation.tests.RelationChecker;
    6267import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
    6368import org.openstreetmap.josm.gui.MainApplication;
    import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;  
    108113import org.openstreetmap.josm.gui.util.WindowGeometry;
    109114import org.openstreetmap.josm.spi.preferences.Config;
    110115import org.openstreetmap.josm.tools.CheckParameterUtil;
     116import org.openstreetmap.josm.tools.GBC;
     117import org.openstreetmap.josm.tools.ImageProvider;
    111118import org.openstreetmap.josm.tools.InputMapUtils;
    112119import org.openstreetmap.josm.tools.Logging;
    113120import org.openstreetmap.josm.tools.Shortcut;
    public class GenericRelationEditor extends RelationEditor implements CommandQueu  
    132139    private final SelectionTableModel selectionTableModel;
    133140
    134141    private final AutoCompletingTextField tfRole;
     142    private final RelationEditorActionAccess actionAccess;
    135143
    136144    /**
    137145     * the menu item in the windows menu. Required to properly hide on dialog close.
    public class GenericRelationEditor extends RelationEditor implements CommandQueu  
    262270            selectedTabPane = sourceTabbedPane.getSelectedComponent();
    263271        });
    264272
    265         IRelationEditorActionAccess actionAccess = new RelationEditorActionAccess();
     273        actionAccess = new RelationEditorActionAccess();
    266274
    267275        refreshAction = new RefreshAction(actionAccess);
    268276        applyAction = new ApplyAction(actionAccess);
    269277        selectAction = new SelectAction(actionAccess);
    270278        duplicateAction = new DuplicateRelationAction(actionAccess);
    271279        deleteAction = new DeleteCurrentRelationAction(actionAccess);
     280
     281        this.memberTableModel.addTableModelListener(applyAction);
     282        this.tagEditorPanel.getModel().addTableModelListener(applyAction);
     283
    272284        addPropertyChangeListener(deleteAction);
    273285
    274286        okAction = new OKAction(actionAccess);
    public class GenericRelationEditor extends RelationEditor implements CommandQueu  
    276288
    277289        getContentPane().add(buildToolBar(refreshAction, applyAction, selectAction, duplicateAction, deleteAction), BorderLayout.NORTH);
    278290        getContentPane().add(tabbedPane, BorderLayout.CENTER);
    279         getContentPane().add(buildOkCancelButtonPanel(okAction, cancelAction), BorderLayout.SOUTH);
     291        getContentPane().add(buildOkCancelButtonPanel(okAction, deleteAction, cancelAction), BorderLayout.SOUTH);
    280292
    281293        setSize(findMaxDialogSize());
    282294
    public class GenericRelationEditor extends RelationEditor implements CommandQueu  
    407419     *
    408420     * @return the panel with the OK and the Cancel button
    409421     */
    410     protected static JPanel buildOkCancelButtonPanel(OKAction okAction, CancelAction cancelAction) {
    411         JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
    412         pnl.add(new JButton(okAction));
     422    protected JPanel buildOkCancelButtonPanel(OKAction okAction, DeleteCurrentRelationAction deleteAction,
     423            CancelAction cancelAction) {
     424        JPanel mainPanel = new JPanel(new GridBagLayout());
     425        final JLabel errorLabel = new JLabel(" ");
     426        final JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
     427        mainPanel.add(errorLabel, GBC.eol().anchor(GridBagConstraints.CENTER));
     428        mainPanel.add(pnl, GBC.eol().anchor(GridBagConstraints.CENTER));
     429        final JButton okButton = new JButton(okAction);
     430        final JButton deleteButton = new JButton(deleteAction);
     431        okButton.setPreferredSize(deleteButton.getPreferredSize());
     432        pnl.add(okButton);
     433        pnl.add(deleteButton);
    413434        pnl.add(new JButton(cancelAction));
    414435        pnl.add(new JButton(new ContextSensitiveHelpAction(ht("/Dialog/RelationEditor"))));
    415         return pnl;
     436        // Keep users from saving invalid relations -- a relation MUST have at least a tag with the key "type"
     437        // AND must contain at least one other OSM object.
     438        final TableModelListener listener = l -> {
     439            final IRelation<?> newRelation = this.actionAccess.getChangedRelation();
     440            updateOkPanel(newRelation, okButton, deleteButton);
     441            updateErrorMessage(newRelation, errorLabel);
     442        };
     443        listener.tableChanged(null);
     444        this.memberTableModel.addTableModelListener(listener);
     445        this.tagEditorPanel.getModel().addTableModelListener(listener);
     446        return mainPanel;
     447    }
     448
     449    /**
     450     * Update the OK panel area
     451     * @param newRelation What the new relation would "look" like if it were to be saved now
     452     * @param okButton The OK button
     453     * @param deleteButton The delete button
     454     */
     455    private void updateOkPanel(IRelation<?> newRelation, JButton okButton, JButton deleteButton) {
     456        okButton.setVisible(newRelation.isUseful() || this.getRelationSnapshot() == null);
     457        deleteButton.setVisible(!newRelation.isUseful() && this.getRelationSnapshot() != null);
     458        if (this.getRelationSnapshot() == null && !newRelation.isUseful()) {
     459            okButton.setText(tr("Delete"));
     460        } else {
     461            okButton.setText(tr("OK"));
     462        }
     463    }
     464
     465    /**
     466     * Update the error message
     467     * @param newRelation What the new relation would "look" like if it were to be saved now
     468     * @param errorLabel The label to update
     469     */
     470    private void updateErrorMessage(IRelation<?> newRelation, JLabel errorLabel) {
     471        Collection<TestError> errors = newRelation.validate();
     472        if (errors.isEmpty()) {
     473            // Setting " " helps keep the label in place for layout calculations
     474            errorLabel.setText(" ");
     475            errorLabel.setIcon(null);
     476        } else {
     477            final TestError error = errors.stream()
     478                    .min(Comparator.comparingInt(testError -> testError.getSeverity().getLevel()))
     479                    .orElse(errors.iterator().next());
     480            final StringBuilder sb = new StringBuilder();
     481            if (error.getDescription() != null) {
     482                sb.append(error.getDescription()).append(": ");
     483            }
     484            sb.append(error.getMessage());
     485            if (errors.size() > 1) {
     486                sb.append(trn(" with {0} more problem", " with {0} more problems", errors.size() - 1, errors.size() - 1));
     487            }
     488            errorLabel.setIcon(ImageProvider.get("data", error.getSeverity().getIcon()));
     489            errorLabel.setText(sb.toString());
     490        }
    416491    }
    417492
    418493    /**
    public class GenericRelationEditor extends RelationEditor implements CommandQueu  
    10011076        @Override
    10021077        public void mouseClicked(MouseEvent e) {
    10031078            if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
    1004                 new EditAction(new RelationEditorActionAccess()).actionPerformed(null);
     1079                new EditAction(actionAccess).actionPerformed(null);
    10051080            }
    10061081        }
    10071082    }
  • src/org/openstreetmap/josm/gui/dialogs/relation/actions/ApplyAction.java

    diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/actions/ApplyAction.java b/src/org/openstreetmap/josm/gui/dialogs/relation/actions/ApplyAction.java
    index 7ddfae63ac..2a51047659 100644
    a b import static org.openstreetmap.josm.tools.I18n.tr;  
    55
    66import java.awt.event.ActionEvent;
    77
     8import org.openstreetmap.josm.data.osm.IRelation;
    89import org.openstreetmap.josm.tools.ImageProvider;
    910
    1011/**
    public class ApplyAction extends SavingAction {  
    3536
    3637    @Override
    3738    public void updateEnabledState() {
    38         setEnabled(isEditorDirty());
     39        final IRelation<?> currentRelation = editorAccess.getChangedRelation();
     40        setEnabled(currentRelation.isUseful() && isEditorDirty());
    3941    }
    4042}
  • src/org/openstreetmap/josm/gui/dialogs/relation/actions/CancelAction.java

    diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/actions/CancelAction.java b/src/org/openstreetmap/josm/gui/dialogs/relation/actions/CancelAction.java
    index 6efdaeabdf..a51be40834 100644
    a b import java.awt.event.ActionEvent;  
    88import javax.swing.JOptionPane;
    99import javax.swing.RootPaneContainer;
    1010
     11import org.openstreetmap.josm.data.osm.IRelation;
    1112import org.openstreetmap.josm.data.osm.Relation;
    1213import org.openstreetmap.josm.gui.HelpAwareOptionPane;
    1314import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
    public class CancelAction extends SavingAction {  
    4748        if ((!getMemberTableModel().hasSameMembersAs(snapshot) || getTagModel().isDirty())
    4849         && !(snapshot == null && getTagModel().getTags().isEmpty())) {
    4950            //give the user a chance to save the changes
    50             int ret = confirmClosingByCancel();
     51            int ret = confirmClosingByCancel(this.editorAccess.getChangedRelation());
    5152            if (ret == 0) { //Yes, save the changes
    5253                //copied from OKAction.run()
    5354                Config.getPref().put("relation.editor.generic.lastrole", Utils.strip(tfRole.getText()));
    public class CancelAction extends SavingAction {  
    6061        hideEditor();
    6162    }
    6263
    63     protected int confirmClosingByCancel() {
     64    protected int confirmClosingByCancel(final IRelation<?> newRelation) {
    6465        ButtonSpec[] options = {
    6566                new ButtonSpec(
    6667                        tr("Yes, save the changes and close"),
    public class CancelAction extends SavingAction {  
    8283                )
    8384        };
    8485
     86        // Keep users from saving invalid relations -- a relation MUST have at least a tag with the key "type"
     87        // AND must contain at least one other OSM object.
     88        options[0].setEnabled(newRelation.isUseful());
     89
    8590        return HelpAwareOptionPane.showOptionDialog(
    8691                MainApplication.getMainFrame(),
    8792                tr("<html>The relation has been changed.<br><br>Do you want to save your changes?</html>"),
  • src/org/openstreetmap/josm/gui/dialogs/relation/actions/IRelationEditorActionAccess.java

    diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/actions/IRelationEditorActionAccess.java b/src/org/openstreetmap/josm/gui/dialogs/relation/actions/IRelationEditorActionAccess.java
    index cddedaf165..b627262197 100644
    a b package org.openstreetmap.josm.gui.dialogs.relation.actions;  
    33
    44import javax.swing.Action;
    55
     6import org.openstreetmap.josm.data.osm.IRelation;
     7import org.openstreetmap.josm.data.osm.Relation;
    68import org.openstreetmap.josm.gui.dialogs.relation.IRelationEditor;
    79import org.openstreetmap.josm.gui.dialogs.relation.MemberTable;
    810import org.openstreetmap.josm.gui.dialogs.relation.MemberTableModel;
    public interface IRelationEditorActionAccess {  
    6567     */
    6668    TagEditorModel getTagModel();
    6769
     70    /**
     71     * Get the changed relation
     72     * @return The changed relation (note: may not be part of a dataset). This should never be {@code null}.
     73     * @since xxx
     74     */
     75    default IRelation<?> getChangedRelation() {
     76        final Relation newRelation;
     77        if (getEditor().getRelation() != null) {
     78            newRelation = getEditor().getRelation();
     79        } else {
     80            newRelation = new Relation();
     81            getTagModel().applyToPrimitive(newRelation);
     82            getMemberTableModel().applyToRelation(newRelation);
     83        }
     84        return newRelation;
     85    }
     86
    6887    /**
    6988     * Get the text field that is used to edit the role.
    7089     * @return The role text field.
  • src/org/openstreetmap/josm/gui/dialogs/relation/actions/SavingAction.java

    diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/actions/SavingAction.java b/src/org/openstreetmap/josm/gui/dialogs/relation/actions/SavingAction.java
    index 26813f23c1..35ca1dcfb7 100644
    a b abstract class SavingAction extends AbstractRelationEditorAction {  
    5454        tagEditorModel.applyToPrimitive(newRelation);
    5555        getMemberTableModel().applyToRelation(newRelation);
    5656        // If the user wanted to create a new relation, but hasn't added any members or
    57         // tags, don't add an empty relation
    58         if (newRelation.isEmpty() && !newRelation.hasKeys())
     57        // tags (specifically the "type" tag), don't add the relation
     58        if (!newRelation.isUseful())
    5959            return;
    6060        UndoRedoHandler.getInstance().add(new AddCommand(getLayer().getDataSet(), newRelation));
    6161