source: josm/trunk/src/org/openstreetmap/josm/gui/conflict/tags/CombinePrimitiveResolverDialog.java @ 5241

Revision 5132, 21.5 KB checked in by simon04, 8 weeks ago (diff)

fix #7513 - Warn non-experts when combining ways with conflicting tags or ways being part of relations

  • Property svn:eol-style set to native
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.conflict.tags;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.BorderLayout;
9import java.awt.Component;
10import java.awt.Dimension;
11import java.awt.FlowLayout;
12import java.awt.event.ActionEvent;
13import java.awt.event.HierarchyBoundsListener;
14import java.awt.event.HierarchyEvent;
15import java.awt.event.WindowAdapter;
16import java.awt.event.WindowEvent;
17import java.beans.PropertyChangeEvent;
18import java.beans.PropertyChangeListener;
19import java.util.Collection;
20import java.util.HashSet;
21import java.util.LinkedList;
22import java.util.List;
23import java.util.Set;
24
25import javax.swing.AbstractAction;
26import javax.swing.Action;
27import javax.swing.JDialog;
28import javax.swing.JLabel;
29import javax.swing.JOptionPane;
30import javax.swing.JPanel;
31import javax.swing.JSplitPane;
32
33import org.openstreetmap.josm.Main;
34import org.openstreetmap.josm.actions.ExpertToggleAction;
35import org.openstreetmap.josm.command.ChangePropertyCommand;
36import org.openstreetmap.josm.command.Command;
37import org.openstreetmap.josm.corrector.UserCancelException;
38import org.openstreetmap.josm.data.osm.NameFormatter;
39import org.openstreetmap.josm.data.osm.Node;
40import org.openstreetmap.josm.data.osm.OsmPrimitive;
41import org.openstreetmap.josm.data.osm.Relation;
42import org.openstreetmap.josm.data.osm.TagCollection;
43import org.openstreetmap.josm.data.osm.Way;
44import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
45import org.openstreetmap.josm.gui.DefaultNameFormatter;
46import org.openstreetmap.josm.gui.SideButton;
47import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
48import org.openstreetmap.josm.gui.help.HelpUtil;
49import org.openstreetmap.josm.tools.ImageProvider;
50import org.openstreetmap.josm.tools.Utils;
51import org.openstreetmap.josm.tools.Utils.Function;
52import org.openstreetmap.josm.tools.WindowGeometry;
53
54/**
55 * This dialog helps to resolve conflicts occurring when ways are combined or
56 * nodes are merged.
57 *
58 * Usage: {@link #launchIfNecessary} followed by {@link #buildResolutionCommands}.
59 *
60 * Prior to {@link #launchIfNecessary}, the following usage sequence was needed:
61 *
62 * There is a singleton instance of this dialog which can be retrieved using
63 * {@see #getInstance()}.
64 *
65 * The dialog uses two models: one  for resolving tag conflicts, the other
66 * for resolving conflicts in relation memberships. For both models there are accessors,
67 * i.e {@see #getTagConflictResolverModel()} and {@see #getRelationMemberConflictResolverModel()}.
68 *
69 * Models have to be <strong>populated</strong> before the dialog is launched. Example:
70 * <pre>
71 *    CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
72 *    dialog.getTagConflictResolverModel().populate(aTagCollection);
73 *    dialog.getRelationMemberConflictResolverModel().populate(aRelationLinkCollection);
74 *    dialog.prepareDefaultDecisions();
75 * </pre>
76 *
77 * You should also set the target primitive which other primitives (ways or nodes) are
78 * merged to, see {@see #setTargetPrimitive(OsmPrimitive)}.
79 *
80 * After the dialog is closed use {@see #isCanceled()} to check whether the user canceled
81 * the dialog. If it wasn't canceled you may build a collection of {@see Command} objects
82 * which reflect the conflict resolution decisions the user made in the dialog:
83 * see {@see #buildResolutionCommands()}
84 *
85 *
86 */
87public class CombinePrimitiveResolverDialog extends JDialog {
88
89    /** the unique instance of the dialog */
90    static private CombinePrimitiveResolverDialog instance;
91
92    /**
93     * Replies the unique instance of the dialog
94     *
95     * @return the unique instance of the dialog
96     * @deprecated use {@link #launchIfNecessary} instead.
97     */
98    @Deprecated
99    public static CombinePrimitiveResolverDialog getInstance() {
100        if (instance == null) {
101            instance = new CombinePrimitiveResolverDialog(Main.parent);
102        }
103        return instance;
104    }
105
106    private AutoAdjustingSplitPane spTagConflictTypes;
107    private TagConflictResolver pnlTagConflictResolver;
108    private RelationMemberConflictResolver pnlRelationMemberConflictResolver;
109    private boolean canceled;
110    private JPanel pnlButtons;
111    private OsmPrimitive targetPrimitive;
112
113    /** the private help action */
114    private ContextSensitiveHelpAction helpAction;
115    /** the apply button */
116    private SideButton btnApply;
117
118    /**
119     * Replies the target primitive the collection of primitives is merged
120     * or combined to.
121     *
122     * @return the target primitive
123     */
124    public OsmPrimitive getTargetPrimitmive() {
125        return targetPrimitive;
126    }
127
128    /**
129     * Sets the primitive the collection of primitives is merged or combined
130     * to.
131     *
132     * @param primitive the target primitive
133     */
134    public void setTargetPrimitive(OsmPrimitive primitive) {
135        this.targetPrimitive = primitive;
136        updateTitle();
137        if (primitive instanceof Way) {
138            pnlRelationMemberConflictResolver.initForWayCombining();
139        } else if (primitive instanceof Node) {
140            pnlRelationMemberConflictResolver.initForNodeMerging();
141        }
142    }
143
144    protected void updateTitle() {
145        if (targetPrimitive == null) {
146            setTitle(tr("Conflicts when combining primitives"));
147            return;
148        }
149        if (targetPrimitive instanceof Way) {
150            setTitle(tr("Conflicts when combining ways - combined way is ''{0}''", targetPrimitive
151                    .getDisplayName(DefaultNameFormatter.getInstance())));
152            helpAction.setHelpTopic(ht("/Action/CombineWay#ResolvingConflicts"));
153            getRootPane().putClientProperty("help", ht("/Action/CombineWay#ResolvingConflicts"));
154        } else if (targetPrimitive instanceof Node) {
155            setTitle(tr("Conflicts when merging nodes - target node is ''{0}''", targetPrimitive
156                    .getDisplayName(DefaultNameFormatter.getInstance())));
157            helpAction.setHelpTopic(ht("/Action/MergeNodes#ResolvingConflicts"));
158            getRootPane().putClientProperty("help", ht("/Action/MergeNodes#ResolvingConflicts"));
159        }
160    }
161
162    protected void build() {
163        getContentPane().setLayout(new BorderLayout());
164        updateTitle();
165        spTagConflictTypes = new AutoAdjustingSplitPane(JSplitPane.VERTICAL_SPLIT);
166        spTagConflictTypes.setTopComponent(buildTagConflictResolverPanel());
167        spTagConflictTypes.setBottomComponent(buildRelationMemberConflictResolverPanel());
168        getContentPane().add(pnlButtons = buildButtonPanel(), BorderLayout.SOUTH);
169        addWindowListener(new AdjustDividerLocationAction());
170        HelpUtil.setHelpContext(getRootPane(), ht("/"));
171    }
172
173    protected JPanel buildTagConflictResolverPanel() {
174        pnlTagConflictResolver = new TagConflictResolver();
175        return pnlTagConflictResolver;
176    }
177
178    protected JPanel buildRelationMemberConflictResolverPanel() {
179        pnlRelationMemberConflictResolver = new RelationMemberConflictResolver();
180        return pnlRelationMemberConflictResolver;
181    }
182
183    protected JPanel buildButtonPanel() {
184        JPanel pnl = new JPanel();
185        pnl.setLayout(new FlowLayout(FlowLayout.CENTER));
186
187        // -- apply button
188        ApplyAction applyAction = new ApplyAction();
189        pnlTagConflictResolver.getModel().addPropertyChangeListener(applyAction);
190        pnlRelationMemberConflictResolver.getModel().addPropertyChangeListener(applyAction);
191        btnApply = new SideButton(applyAction);
192        btnApply.setFocusable(true);
193        pnl.add(btnApply);
194
195        // -- cancel button
196        CancelAction cancelAction = new CancelAction();
197        pnl.add(new SideButton(cancelAction));
198
199        // -- help button
200        helpAction = new ContextSensitiveHelpAction();
201        pnl.add(new SideButton(helpAction));
202
203        return pnl;
204    }
205
206    public CombinePrimitiveResolverDialog(Component owner) {
207        super(JOptionPane.getFrameForComponent(owner), ModalityType.DOCUMENT_MODAL);
208        build();
209    }
210
211    public TagConflictResolverModel getTagConflictResolverModel() {
212        return pnlTagConflictResolver.getModel();
213    }
214
215    public RelationMemberConflictResolverModel getRelationMemberConflictResolverModel() {
216        return pnlRelationMemberConflictResolver.getModel();
217    }
218
219    protected List<Command> buildTagChangeCommand(OsmPrimitive primitive, TagCollection tc) {
220        LinkedList<Command> cmds = new LinkedList<Command>();
221        for (String key : tc.getKeys()) {
222            if (tc.hasUniqueEmptyValue(key)) {
223                if (primitive.get(key) != null) {
224                    cmds.add(new ChangePropertyCommand(primitive, key, null));
225                }
226            } else {
227                String value = tc.getJoinedValues(key);
228                if (!value.equals(primitive.get(key))) {
229                    cmds.add(new ChangePropertyCommand(primitive, key, value));
230                }
231            }
232        }
233        return cmds;
234    }
235
236    public List<Command> buildResolutionCommands() {
237        List<Command> cmds = new LinkedList<Command>();
238
239        TagCollection allResolutions = getTagConflictResolverModel().getAllResolutions();
240        if (allResolutions.size() > 0) {
241            cmds.addAll(buildTagChangeCommand(targetPrimitive, allResolutions));
242        }
243        if (targetPrimitive.get("created_by") != null) {
244            cmds.add(new ChangePropertyCommand(targetPrimitive, "created_by", null));
245        }
246
247        if (getRelationMemberConflictResolverModel().getNumDecisions() > 0) {
248            cmds.addAll(getRelationMemberConflictResolverModel().buildResolutionCommands(targetPrimitive));
249        }
250
251        Command cmd = pnlRelationMemberConflictResolver.buildTagApplyCommands(getRelationMemberConflictResolverModel()
252                .getModifiedRelations(targetPrimitive));
253        if (cmd != null) {
254            cmds.add(cmd);
255        }
256        return cmds;
257    }
258
259    protected void prepareDefaultTagDecisions() {
260        TagConflictResolverModel model = getTagConflictResolverModel();
261        for (int i = 0; i < model.getRowCount(); i++) {
262            MultiValueResolutionDecision decision = model.getDecision(i);
263            List<String> values = decision.getValues();
264            values.remove("");
265            if (values.size() == 1) {
266                decision.keepOne(values.get(0));
267            } else {
268                decision.keepAll();
269            }
270        }
271        model.rebuild();
272    }
273
274    protected void prepareDefaultRelationDecisions() {
275        RelationMemberConflictResolverModel model = getRelationMemberConflictResolverModel();
276        Set<Relation> relations = new HashSet<Relation>();
277        for (int i = 0; i < model.getNumDecisions(); i++) {
278            RelationMemberConflictDecision decision = model.getDecision(i);
279            if (!relations.contains(decision.getRelation())) {
280                decision.decide(RelationMemberConflictDecisionType.KEEP);
281                relations.add(decision.getRelation());
282            } else {
283                decision.decide(RelationMemberConflictDecisionType.REMOVE);
284            }
285        }
286        model.refresh();
287    }
288
289    public void prepareDefaultDecisions() {
290        prepareDefaultTagDecisions();
291        prepareDefaultRelationDecisions();
292    }
293
294    protected JPanel buildEmptyConflictsPanel() {
295        JPanel pnl = new JPanel();
296        pnl.setLayout(new BorderLayout());
297        pnl.add(new JLabel(tr("No conflicts to resolve")));
298        return pnl;
299    }
300
301    protected void prepareGUIBeforeConflictResolutionStarts() {
302        RelationMemberConflictResolverModel relModel = getRelationMemberConflictResolverModel();
303        TagConflictResolverModel tagModel = getTagConflictResolverModel();
304        getContentPane().removeAll();
305
306        if (relModel.getNumDecisions() > 0 && tagModel.getNumDecisions() > 0) {
307            // display both, the dialog for resolving relation conflicts and for resolving
308            // tag conflicts
309            spTagConflictTypes.setTopComponent(pnlTagConflictResolver);
310            spTagConflictTypes.setBottomComponent(pnlRelationMemberConflictResolver);
311            getContentPane().add(spTagConflictTypes, BorderLayout.CENTER);
312        } else if (relModel.getNumDecisions() > 0) {
313            // relation conflicts only
314            //
315            getContentPane().add(pnlRelationMemberConflictResolver, BorderLayout.CENTER);
316        } else if (tagModel.getNumDecisions() > 0) {
317            // tag conflicts only
318            //
319            getContentPane().add(pnlTagConflictResolver, BorderLayout.CENTER);
320        } else {
321            getContentPane().add(buildEmptyConflictsPanel(), BorderLayout.CENTER);
322        }
323
324        getContentPane().add(pnlButtons, BorderLayout.SOUTH);
325        validate();
326        int numTagDecisions = getTagConflictResolverModel().getNumDecisions();
327        int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions();
328        if (numTagDecisions > 0 && numRelationDecisions > 0) {
329            spTagConflictTypes.setDividerLocation(0.5);
330        }
331        pnlRelationMemberConflictResolver.prepareForEditing();
332    }
333
334    protected void setCanceled(boolean canceled) {
335        this.canceled = canceled;
336    }
337
338    public boolean isCanceled() {
339        return canceled;
340    }
341
342    @Override
343    public void setVisible(boolean visible) {
344        if (visible) {
345            prepareGUIBeforeConflictResolutionStarts();
346            new WindowGeometry(getClass().getName() + ".geometry", WindowGeometry.centerInWindow(Main.parent,
347                    new Dimension(600, 400))).applySafe(this);
348            setCanceled(false);
349            btnApply.requestFocusInWindow();
350        } else {
351            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
352        }
353        super.setVisible(visible);
354    }
355
356    class CancelAction extends AbstractAction {
357
358        public CancelAction() {
359            putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution"));
360            putValue(Action.NAME, tr("Cancel"));
361            putValue(Action.SMALL_ICON, ImageProvider.get("", "cancel"));
362            setEnabled(true);
363        }
364
365        public void actionPerformed(ActionEvent arg0) {
366            setCanceled(true);
367            setVisible(false);
368        }
369    }
370
371    class ApplyAction extends AbstractAction implements PropertyChangeListener {
372
373        public ApplyAction() {
374            putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts"));
375            putValue(Action.NAME, tr("Apply"));
376            putValue(Action.SMALL_ICON, ImageProvider.get("ok"));
377            updateEnabledState();
378        }
379
380        public void actionPerformed(ActionEvent arg0) {
381            setVisible(false);
382            pnlTagConflictResolver.rememberPreferences();
383        }
384
385        protected void updateEnabledState() {
386            setEnabled(pnlTagConflictResolver.getModel().getNumConflicts() == 0
387                    && pnlRelationMemberConflictResolver.getModel().getNumConflicts() == 0);
388        }
389
390        public void propertyChange(PropertyChangeEvent evt) {
391            if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) {
392                updateEnabledState();
393            }
394            if (evt.getPropertyName().equals(RelationMemberConflictResolverModel.NUM_CONFLICTS_PROP)) {
395                updateEnabledState();
396            }
397        }
398    }
399
400    class AdjustDividerLocationAction extends WindowAdapter {
401        @Override
402        public void windowOpened(WindowEvent e) {
403            int numTagDecisions = getTagConflictResolverModel().getNumDecisions();
404            int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions();
405            if (numTagDecisions > 0 && numRelationDecisions > 0) {
406                spTagConflictTypes.setDividerLocation(0.5);
407            }
408        }
409    }
410
411    static class AutoAdjustingSplitPane extends JSplitPane implements PropertyChangeListener, HierarchyBoundsListener {
412        private double dividerLocation;
413
414        public AutoAdjustingSplitPane(int newOrientation) {
415            super(newOrientation);
416            addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, this);
417            addHierarchyBoundsListener(this);
418        }
419
420        public void ancestorResized(HierarchyEvent e) {
421            setDividerLocation((int) (dividerLocation * getHeight()));
422        }
423
424        public void ancestorMoved(HierarchyEvent e) {
425            // do nothing
426        }
427
428        public void propertyChange(PropertyChangeEvent evt) {
429            if (evt.getPropertyName().equals(JSplitPane.DIVIDER_LOCATION_PROPERTY)) {
430                int newVal = (Integer) evt.getNewValue();
431                if (getHeight() != 0) {
432                    dividerLocation = (double) newVal / (double) getHeight();
433                }
434            }
435        }
436    }
437
438    public static List<Command> launchIfNecessary(
439            final TagCollection tagsOfPrimitives,
440            final Collection<? extends OsmPrimitive> primitives,
441            final Collection<? extends OsmPrimitive> targetPrimitives) throws UserCancelException {
442
443        final TagCollection completeWayTags = new TagCollection(tagsOfPrimitives);
444        TagConflictResolutionUtil.combineTigerTags(completeWayTags);
445        TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing(completeWayTags, primitives);
446        final TagCollection tagsToEdit = new TagCollection(completeWayTags);
447        TagConflictResolutionUtil.completeTagCollectionForEditing(tagsToEdit);
448
449        final CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
450
451        dialog.getTagConflictResolverModel().populate(tagsToEdit, completeWayTags.getKeysWithMultipleValues());
452
453        final Set<Relation> parentRelations = OsmPrimitive.getParentRelations(primitives);
454        dialog.getRelationMemberConflictResolverModel().populate(parentRelations, primitives);
455        dialog.prepareDefaultDecisions();
456
457        // show information dialog to non-experts
458        if (!completeWayTags.isApplicableToPrimitive() && !ExpertToggleAction.isExpert()) {
459            String conflicts = Utils.joinAsHtmlUnorderedList(Utils.transform(completeWayTags.getKeysWithMultipleValues(), new Function<String, String>() {
460
461                @Override
462                public String apply(String key) {
463                    return tr("{0} ({1})", key, Utils.join(tr(", "), Utils.transform(completeWayTags.getValues(key), new Function<String, String>() {
464
465                        @Override
466                        public String apply(String x) {
467                            return x == null || x.isEmpty() ? tr("<i>missing</i>") : x;
468                        }
469                    })));
470                }
471            }));
472            String msg = tr("You are about to combine {0} objects, "
473                    + "but the following tags are used conflictingly:<br/>{1}"
474                    + "If these objects are combined, the resulting object may have unwanted tags.<br/>"
475                    + "If you want to continue, you are shown a dialog to fix the conflicting tags.<br/><br/>"
476                    + "Do you want to continue?",
477                    primitives.size(), conflicts);
478            if (!ConditionalOptionPaneUtil.showConfirmationDialog(
479                    "combine_tags",
480                    Main.parent,
481                    "<html>" + msg + "</html>",
482                    tr("Combine confirmation"),
483                    JOptionPane.YES_NO_OPTION,
484                    JOptionPane.QUESTION_MESSAGE,
485                    JOptionPane.YES_OPTION)) {
486                throw new UserCancelException();
487            }
488        }
489
490        if (!parentRelations.isEmpty() && !ExpertToggleAction.isExpert()) {
491            String msg = trn("You are about to combine {1} objects, "
492                    + "which are part of {0} relation:<br/>{2}"
493                    + "Combining these objects may break this relation. If you are unsure, please cancel this operation.<br/>"
494                    + "If you want to continue, you are shown a dialog to decide how to adapt the relation.<br/><br/>"
495                    + "Do you want to continue?",
496                    "You are about to combine {1} objects, "
497                    + "which are part of {0} relations:<br/>{2}"
498                    + "Combining these objects may break these relations. If you are unsure, please cancel this operation.<br/>"
499                    + "If you want to continue, you are shown a dialog to decide how to adapt the relations.<br/><br/>"
500                    + "Do you want to continue?",
501                    parentRelations.size(), parentRelations.size(), primitives.size(),
502                    DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(parentRelations));
503            if (!ConditionalOptionPaneUtil.showConfirmationDialog(
504                    "combine_tags",
505                    Main.parent,
506                    "<html>" + msg + "</html>",
507                    tr("Combine confirmation"),
508                    JOptionPane.YES_NO_OPTION,
509                    JOptionPane.QUESTION_MESSAGE,
510                    JOptionPane.YES_OPTION)) {
511                throw new UserCancelException();
512            }
513        }
514
515        // resolve tag conflicts if necessary
516        if (!completeWayTags.isApplicableToPrimitive() || !parentRelations.isEmpty()) {
517            dialog.setVisible(true);
518            if (dialog.isCanceled()) {
519                throw new UserCancelException();
520            }
521        }
522        List<Command> cmds = new LinkedList<Command>();
523        for (OsmPrimitive i : targetPrimitives) {
524            dialog.setTargetPrimitive(i);
525            cmds.addAll(dialog.buildResolutionCommands());
526        }
527        return cmds;
528    }
529}
Note: See TracBrowser for help on using the repository browser.