001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.conflict.tags;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trn;
007
008import java.awt.BorderLayout;
009import java.awt.Component;
010import java.awt.Dimension;
011import java.awt.FlowLayout;
012import java.awt.event.ActionEvent;
013import java.awt.event.HierarchyBoundsListener;
014import java.awt.event.HierarchyEvent;
015import java.awt.event.WindowAdapter;
016import java.awt.event.WindowEvent;
017import java.beans.PropertyChangeEvent;
018import java.beans.PropertyChangeListener;
019import java.util.Collection;
020import java.util.HashSet;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.Set;
024
025import javax.swing.AbstractAction;
026import javax.swing.Action;
027import javax.swing.JDialog;
028import javax.swing.JLabel;
029import javax.swing.JOptionPane;
030import javax.swing.JPanel;
031import javax.swing.JSplitPane;
032
033import org.openstreetmap.josm.Main;
034import org.openstreetmap.josm.actions.ExpertToggleAction;
035import org.openstreetmap.josm.command.ChangePropertyCommand;
036import org.openstreetmap.josm.command.Command;
037import org.openstreetmap.josm.corrector.UserCancelException;
038import org.openstreetmap.josm.data.osm.Node;
039import org.openstreetmap.josm.data.osm.OsmPrimitive;
040import org.openstreetmap.josm.data.osm.Relation;
041import org.openstreetmap.josm.data.osm.TagCollection;
042import org.openstreetmap.josm.data.osm.Way;
043import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
044import org.openstreetmap.josm.gui.DefaultNameFormatter;
045import org.openstreetmap.josm.gui.SideButton;
046import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
047import org.openstreetmap.josm.gui.help.HelpUtil;
048import org.openstreetmap.josm.gui.util.GuiHelper;
049import org.openstreetmap.josm.tools.CheckParameterUtil;
050import org.openstreetmap.josm.tools.ImageProvider;
051import org.openstreetmap.josm.tools.Utils;
052import org.openstreetmap.josm.tools.Utils.Function;
053import org.openstreetmap.josm.tools.WindowGeometry;
054
055/**
056 * This dialog helps to resolve conflicts occurring when ways are combined or
057 * nodes are merged.
058 *
059 * Usage: {@link #launchIfNecessary} followed by {@link #buildResolutionCommands}.
060 *
061 * Prior to {@link #launchIfNecessary}, the following usage sequence was needed:
062 *
063 * There is a singleton instance of this dialog which can be retrieved using
064 * {@link #getInstance()}.
065 *
066 * The dialog uses two models: one  for resolving tag conflicts, the other
067 * for resolving conflicts in relation memberships. For both models there are accessors,
068 * i.e {@link #getTagConflictResolverModel()} and {@link #getRelationMemberConflictResolverModel()}.
069 *
070 * Models have to be <strong>populated</strong> before the dialog is launched. Example:
071 * <pre>
072 *    CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
073 *    dialog.getTagConflictResolverModel().populate(aTagCollection);
074 *    dialog.getRelationMemberConflictResolverModel().populate(aRelationLinkCollection);
075 *    dialog.prepareDefaultDecisions();
076 * </pre>
077 *
078 * You should also set the target primitive which other primitives (ways or nodes) are
079 * merged to, see {@link #setTargetPrimitive(OsmPrimitive)}.
080 *
081 * After the dialog is closed use {@link #isCanceled()} to check whether the user canceled
082 * the dialog. If it wasn't canceled you may build a collection of {@link Command} objects
083 * which reflect the conflict resolution decisions the user made in the dialog:
084 * see {@link #buildResolutionCommands()}
085 */
086public class CombinePrimitiveResolverDialog extends JDialog {
087
088    /** the unique instance of the dialog */
089    static private CombinePrimitiveResolverDialog instance;
090
091    /**
092     * Replies the unique instance of the dialog
093     *
094     * @return the unique instance of the dialog
095     * @deprecated use {@link #launchIfNecessary} instead.
096     */
097    @Deprecated
098    public static CombinePrimitiveResolverDialog getInstance() {
099        if (instance == null) {
100            GuiHelper.runInEDTAndWait(new Runnable() {
101                @Override public void run() {
102                    instance = new CombinePrimitiveResolverDialog(Main.parent);
103                }
104            });
105        }
106        return instance;
107    }
108
109    private AutoAdjustingSplitPane spTagConflictTypes;
110    private TagConflictResolver pnlTagConflictResolver;
111    private RelationMemberConflictResolver pnlRelationMemberConflictResolver;
112    private boolean canceled;
113    private JPanel pnlButtons;
114    private OsmPrimitive targetPrimitive;
115
116    /** the private help action */
117    private ContextSensitiveHelpAction helpAction;
118    /** the apply button */
119    private SideButton btnApply;
120
121    /**
122     * Replies the target primitive the collection of primitives is merged
123     * or combined to.
124     *
125     * @return the target primitive
126     */
127    public OsmPrimitive getTargetPrimitmive() {
128        return targetPrimitive;
129    }
130
131    /**
132     * Sets the primitive the collection of primitives is merged or combined to.
133     *
134     * @param primitive the target primitive
135     */
136    public void setTargetPrimitive(final OsmPrimitive primitive) {
137        this.targetPrimitive = primitive;
138        GuiHelper.runInEDTAndWait(new Runnable() {
139            @Override public void run() {
140                updateTitle();
141                if (primitive instanceof Way) {
142                    pnlRelationMemberConflictResolver.initForWayCombining();
143                } else if (primitive instanceof Node) {
144                    pnlRelationMemberConflictResolver.initForNodeMerging();
145                }
146            }
147        });
148    }
149
150    protected void updateTitle() {
151        if (targetPrimitive == null) {
152            setTitle(tr("Conflicts when combining primitives"));
153            return;
154        }
155        if (targetPrimitive instanceof Way) {
156            setTitle(tr("Conflicts when combining ways - combined way is ''{0}''", targetPrimitive
157                    .getDisplayName(DefaultNameFormatter.getInstance())));
158            helpAction.setHelpTopic(ht("/Action/CombineWay#ResolvingConflicts"));
159            getRootPane().putClientProperty("help", ht("/Action/CombineWay#ResolvingConflicts"));
160        } else if (targetPrimitive instanceof Node) {
161            setTitle(tr("Conflicts when merging nodes - target node is ''{0}''", targetPrimitive
162                    .getDisplayName(DefaultNameFormatter.getInstance())));
163            helpAction.setHelpTopic(ht("/Action/MergeNodes#ResolvingConflicts"));
164            getRootPane().putClientProperty("help", ht("/Action/MergeNodes#ResolvingConflicts"));
165        }
166    }
167
168    protected void build() {
169        getContentPane().setLayout(new BorderLayout());
170        updateTitle();
171        spTagConflictTypes = new AutoAdjustingSplitPane(JSplitPane.VERTICAL_SPLIT);
172        spTagConflictTypes.setTopComponent(buildTagConflictResolverPanel());
173        spTagConflictTypes.setBottomComponent(buildRelationMemberConflictResolverPanel());
174        getContentPane().add(pnlButtons = buildButtonPanel(), BorderLayout.SOUTH);
175        addWindowListener(new AdjustDividerLocationAction());
176        HelpUtil.setHelpContext(getRootPane(), ht("/"));
177    }
178
179    protected JPanel buildTagConflictResolverPanel() {
180        pnlTagConflictResolver = new TagConflictResolver();
181        return pnlTagConflictResolver;
182    }
183
184    protected JPanel buildRelationMemberConflictResolverPanel() {
185        pnlRelationMemberConflictResolver = new RelationMemberConflictResolver();
186        return pnlRelationMemberConflictResolver;
187    }
188
189    protected JPanel buildButtonPanel() {
190        JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
191
192        // -- apply button
193        ApplyAction applyAction = new ApplyAction();
194        pnlTagConflictResolver.getModel().addPropertyChangeListener(applyAction);
195        pnlRelationMemberConflictResolver.getModel().addPropertyChangeListener(applyAction);
196        btnApply = new SideButton(applyAction);
197        btnApply.setFocusable(true);
198        pnl.add(btnApply);
199
200        // -- cancel button
201        CancelAction cancelAction = new CancelAction();
202        pnl.add(new SideButton(cancelAction));
203
204        // -- help button
205        helpAction = new ContextSensitiveHelpAction();
206        pnl.add(new SideButton(helpAction));
207
208        return pnl;
209    }
210
211    /**
212     * Constructs a new {@code CombinePrimitiveResolverDialog}.
213     * @param parent The parent component in which this dialog will be displayed.
214     */
215    public CombinePrimitiveResolverDialog(Component parent) {
216        super(JOptionPane.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL);
217        build();
218    }
219
220    /**
221     * Replies the tag conflict resolver model.
222     * @return The tag conflict resolver model.
223     */
224    public TagConflictResolverModel getTagConflictResolverModel() {
225        return pnlTagConflictResolver.getModel();
226    }
227
228    /**
229     * Replies the relation membership conflict resolver model.
230     * @return The relation membership conflict resolver model.
231     */
232    public RelationMemberConflictResolverModel getRelationMemberConflictResolverModel() {
233        return pnlRelationMemberConflictResolver.getModel();
234    }
235
236    protected List<Command> buildTagChangeCommand(OsmPrimitive primitive, TagCollection tc) {
237        LinkedList<Command> cmds = new LinkedList<Command>();
238        for (String key : tc.getKeys()) {
239            if (tc.hasUniqueEmptyValue(key)) {
240                if (primitive.get(key) != null) {
241                    cmds.add(new ChangePropertyCommand(primitive, key, null));
242                }
243            } else {
244                String value = tc.getJoinedValues(key);
245                if (!value.equals(primitive.get(key))) {
246                    cmds.add(new ChangePropertyCommand(primitive, key, value));
247                }
248            }
249        }
250        return cmds;
251    }
252
253    /**
254     * Replies the list of {@link Command commands} needed to apply resolution choices.
255     * @return The list of {@link Command commands} needed to apply resolution choices.
256     */
257    public List<Command> buildResolutionCommands() {
258        List<Command> cmds = new LinkedList<Command>();
259
260        TagCollection allResolutions = getTagConflictResolverModel().getAllResolutions();
261        if (!allResolutions.isEmpty()) {
262            cmds.addAll(buildTagChangeCommand(targetPrimitive, allResolutions));
263        }
264        for(String p : OsmPrimitive.getDiscardableKeys()) {
265            if (targetPrimitive.get(p) != null) {
266                cmds.add(new ChangePropertyCommand(targetPrimitive, p, null));
267            }
268        }
269
270        if (getRelationMemberConflictResolverModel().getNumDecisions() > 0) {
271            cmds.addAll(getRelationMemberConflictResolverModel().buildResolutionCommands(targetPrimitive));
272        }
273
274        Command cmd = pnlRelationMemberConflictResolver.buildTagApplyCommands(getRelationMemberConflictResolverModel()
275                .getModifiedRelations(targetPrimitive));
276        if (cmd != null) {
277            cmds.add(cmd);
278        }
279        return cmds;
280    }
281
282    protected void prepareDefaultTagDecisions() {
283        TagConflictResolverModel model = getTagConflictResolverModel();
284        for (int i = 0; i < model.getRowCount(); i++) {
285            MultiValueResolutionDecision decision = model.getDecision(i);
286            List<String> values = decision.getValues();
287            values.remove("");
288            if (values.size() == 1) {
289                decision.keepOne(values.get(0));
290            } else {
291                decision.keepAll();
292            }
293        }
294        model.rebuild();
295    }
296
297    protected void prepareDefaultRelationDecisions() {
298        RelationMemberConflictResolverModel model = getRelationMemberConflictResolverModel();
299        Set<Relation> relations = new HashSet<Relation>();
300        for (int i = 0; i < model.getNumDecisions(); i++) {
301            RelationMemberConflictDecision decision = model.getDecision(i);
302            if (!relations.contains(decision.getRelation())) {
303                decision.decide(RelationMemberConflictDecisionType.KEEP);
304                relations.add(decision.getRelation());
305            } else {
306                decision.decide(RelationMemberConflictDecisionType.REMOVE);
307            }
308        }
309        model.refresh();
310    }
311
312    /**
313     * Prepares the default decisions for populated tag and relation membership conflicts.
314     */
315    public void prepareDefaultDecisions() {
316        prepareDefaultTagDecisions();
317        prepareDefaultRelationDecisions();
318    }
319
320    protected JPanel buildEmptyConflictsPanel() {
321        JPanel pnl = new JPanel(new BorderLayout());
322        pnl.add(new JLabel(tr("No conflicts to resolve")));
323        return pnl;
324    }
325
326    protected void prepareGUIBeforeConflictResolutionStarts() {
327        RelationMemberConflictResolverModel relModel = getRelationMemberConflictResolverModel();
328        TagConflictResolverModel tagModel = getTagConflictResolverModel();
329        getContentPane().removeAll();
330
331        if (relModel.getNumDecisions() > 0 && tagModel.getNumDecisions() > 0) {
332            // display both, the dialog for resolving relation conflicts and for resolving tag conflicts
333            spTagConflictTypes.setTopComponent(pnlTagConflictResolver);
334            spTagConflictTypes.setBottomComponent(pnlRelationMemberConflictResolver);
335            getContentPane().add(spTagConflictTypes, BorderLayout.CENTER);
336        } else if (relModel.getNumDecisions() > 0) {
337            // relation conflicts only
338            getContentPane().add(pnlRelationMemberConflictResolver, BorderLayout.CENTER);
339        } else if (tagModel.getNumDecisions() > 0) {
340            // tag conflicts only
341            getContentPane().add(pnlTagConflictResolver, BorderLayout.CENTER);
342        } else {
343            getContentPane().add(buildEmptyConflictsPanel(), BorderLayout.CENTER);
344        }
345
346        getContentPane().add(pnlButtons, BorderLayout.SOUTH);
347        validate();
348        int numTagDecisions = getTagConflictResolverModel().getNumDecisions();
349        int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions();
350        if (numTagDecisions > 0 && numRelationDecisions > 0) {
351            spTagConflictTypes.setDividerLocation(0.5);
352        }
353        pnlRelationMemberConflictResolver.prepareForEditing();
354    }
355
356    protected void setCanceled(boolean canceled) {
357        this.canceled = canceled;
358    }
359
360    /**
361     * Determines if this dialog has been cancelled.
362     * @return true if this dialog has been cancelled, false otherwise.
363     */
364    public boolean isCanceled() {
365        return canceled;
366    }
367
368    @Override
369    public void setVisible(boolean visible) {
370        if (visible) {
371            prepareGUIBeforeConflictResolutionStarts();
372            new WindowGeometry(getClass().getName() + ".geometry", WindowGeometry.centerInWindow(Main.parent,
373                    new Dimension(600, 400))).applySafe(this);
374            setCanceled(false);
375            btnApply.requestFocusInWindow();
376        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
377            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
378        }
379        super.setVisible(visible);
380    }
381
382    class CancelAction extends AbstractAction {
383
384        public CancelAction() {
385            putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution"));
386            putValue(Action.NAME, tr("Cancel"));
387            putValue(Action.SMALL_ICON, ImageProvider.get("", "cancel"));
388            setEnabled(true);
389        }
390
391        @Override
392        public void actionPerformed(ActionEvent arg0) {
393            setCanceled(true);
394            setVisible(false);
395        }
396    }
397
398    class ApplyAction extends AbstractAction implements PropertyChangeListener {
399
400        public ApplyAction() {
401            putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts"));
402            putValue(Action.NAME, tr("Apply"));
403            putValue(Action.SMALL_ICON, ImageProvider.get("ok"));
404            updateEnabledState();
405        }
406
407        @Override
408        public void actionPerformed(ActionEvent arg0) {
409            setVisible(false);
410            pnlTagConflictResolver.rememberPreferences();
411        }
412
413        protected void updateEnabledState() {
414            setEnabled(pnlTagConflictResolver.getModel().getNumConflicts() == 0
415                    && pnlRelationMemberConflictResolver.getModel().getNumConflicts() == 0);
416        }
417
418        @Override
419        public void propertyChange(PropertyChangeEvent evt) {
420            if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) {
421                updateEnabledState();
422            }
423            if (evt.getPropertyName().equals(RelationMemberConflictResolverModel.NUM_CONFLICTS_PROP)) {
424                updateEnabledState();
425            }
426        }
427    }
428
429    class AdjustDividerLocationAction extends WindowAdapter {
430        @Override
431        public void windowOpened(WindowEvent e) {
432            int numTagDecisions = getTagConflictResolverModel().getNumDecisions();
433            int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions();
434            if (numTagDecisions > 0 && numRelationDecisions > 0) {
435                spTagConflictTypes.setDividerLocation(0.5);
436            }
437        }
438    }
439
440    static class AutoAdjustingSplitPane extends JSplitPane implements PropertyChangeListener, HierarchyBoundsListener {
441        private double dividerLocation;
442
443        public AutoAdjustingSplitPane(int newOrientation) {
444            super(newOrientation);
445            addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, this);
446            addHierarchyBoundsListener(this);
447        }
448
449        @Override
450        public void ancestorResized(HierarchyEvent e) {
451            setDividerLocation((int) (dividerLocation * getHeight()));
452        }
453
454        @Override
455        public void ancestorMoved(HierarchyEvent e) {
456            // do nothing
457        }
458
459        @Override
460        public void propertyChange(PropertyChangeEvent evt) {
461            if (evt.getPropertyName().equals(JSplitPane.DIVIDER_LOCATION_PROPERTY)) {
462                int newVal = (Integer) evt.getNewValue();
463                if (getHeight() != 0) {
464                    dividerLocation = (double) newVal / (double) getHeight();
465                }
466            }
467        }
468    }
469
470    /**
471     * Replies the list of {@link Command commands} needed to resolve specified conflicts,
472     * by displaying if necessary a {@link CombinePrimitiveResolverDialog} to the user.
473     * This dialog will allow the user to choose conflict resolution actions.
474     *
475     * Non-expert users are informed first of the meaning of these operations, allowing them to cancel.
476     *
477     * @param tagsOfPrimitives The tag collection of the primitives to be combined.
478     *                         Should generally be equal to {@code TagCollection.unionOfAllPrimitives(primitives)}
479     * @param primitives The primitives to be combined
480     * @param targetPrimitives The primitives the collection of primitives are merged or combined to.
481     * @return The list of {@link Command commands} needed to apply resolution actions.
482     * @throws UserCancelException If the user cancelled a dialog.
483     */
484    public static List<Command> launchIfNecessary(
485            final TagCollection tagsOfPrimitives,
486            final Collection<? extends OsmPrimitive> primitives,
487            final Collection<? extends OsmPrimitive> targetPrimitives) throws UserCancelException {
488
489        CheckParameterUtil.ensureParameterNotNull(tagsOfPrimitives, "tagsOfPrimitives");
490        CheckParameterUtil.ensureParameterNotNull(primitives, "primitives");
491        CheckParameterUtil.ensureParameterNotNull(targetPrimitives, "targetPrimitives");
492
493        final TagCollection completeWayTags = new TagCollection(tagsOfPrimitives);
494        TagConflictResolutionUtil.combineTigerTags(completeWayTags);
495        TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing(completeWayTags, primitives);
496        final TagCollection tagsToEdit = new TagCollection(completeWayTags);
497        TagConflictResolutionUtil.completeTagCollectionForEditing(tagsToEdit);
498
499        final Set<Relation> parentRelations = OsmPrimitive.getParentRelations(primitives);
500
501        // Show information dialogs about conflicts to non-experts
502        if (!ExpertToggleAction.isExpert()) {
503            // Tag conflicts
504            if (!completeWayTags.isApplicableToPrimitive()) {
505                informAboutTagConflicts(primitives, completeWayTags);
506            }
507            // Relation membership conflicts
508            if (!parentRelations.isEmpty()) {
509                informAboutRelationMembershipConflicts(primitives, parentRelations);
510            }
511        }
512
513        // Build conflict resolution dialog
514        final CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
515
516        dialog.getTagConflictResolverModel().populate(tagsToEdit, completeWayTags.getKeysWithMultipleValues());
517        dialog.getRelationMemberConflictResolverModel().populate(parentRelations, primitives);
518        dialog.prepareDefaultDecisions();
519
520        // Ensure a proper title is displayed instead of a previous target (fix #7925)
521        if (targetPrimitives.size() == 1) {
522            dialog.setTargetPrimitive(targetPrimitives.iterator().next());
523        } else {
524            dialog.setTargetPrimitive(null);
525        }
526
527        // Resolve tag conflicts if necessary
528        if (!completeWayTags.isApplicableToPrimitive() || !parentRelations.isEmpty()) {
529            dialog.setVisible(true);
530            if (dialog.isCanceled()) {
531                throw new UserCancelException();
532            }
533        }
534        List<Command> cmds = new LinkedList<Command>();
535        for (OsmPrimitive i : targetPrimitives) {
536            dialog.setTargetPrimitive(i);
537            cmds.addAll(dialog.buildResolutionCommands());
538        }
539        return cmds;
540    }
541
542    /**
543     * Inform a non-expert user about what relation membership conflict resolution means.
544     * @param primitives The primitives to be combined
545     * @param parentRelations The parent relations of the primitives
546     * @throws UserCancelException If the user cancels the dialog.
547     */
548    protected static void informAboutRelationMembershipConflicts(
549            final Collection<? extends OsmPrimitive> primitives,
550            final Set<Relation> parentRelations) throws UserCancelException {
551        String msg = trn("You are about to combine {1} objects, "
552                + "which are part of {0} relation:<br/>{2}"
553                + "Combining these objects may break this relation. If you are unsure, please cancel this operation.<br/>"
554                + "If you want to continue, you are shown a dialog to decide how to adapt the relation.<br/><br/>"
555                + "Do you want to continue?",
556                "You are about to combine {1} objects, "
557                + "which are part of {0} relations:<br/>{2}"
558                + "Combining these objects may break these relations. If you are unsure, please cancel this operation.<br/>"
559                + "If you want to continue, you are shown a dialog to decide how to adapt the relations.<br/><br/>"
560                + "Do you want to continue?",
561                parentRelations.size(), parentRelations.size(), primitives.size(),
562                DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(parentRelations));
563
564        if (!ConditionalOptionPaneUtil.showConfirmationDialog(
565                "combine_tags",
566                Main.parent,
567                "<html>" + msg + "</html>",
568                tr("Combine confirmation"),
569                JOptionPane.YES_NO_OPTION,
570                JOptionPane.QUESTION_MESSAGE,
571                JOptionPane.YES_OPTION)) {
572            throw new UserCancelException();
573        }
574    }
575
576    /**
577     * Inform a non-expert user about what tag conflict resolution means.
578     * @param primitives The primitives to be combined
579     * @param normalizedTags The normalized tag collection of the primitives to be combined
580     * @throws UserCancelException If the user cancels the dialog.
581     */
582    protected static void informAboutTagConflicts(
583            final Collection<? extends OsmPrimitive> primitives,
584            final TagCollection normalizedTags) throws UserCancelException {
585        String conflicts = Utils.joinAsHtmlUnorderedList(Utils.transform(normalizedTags.getKeysWithMultipleValues(), new Function<String, String>() {
586
587            @Override
588            public String apply(String key) {
589                return tr("{0} ({1})", key, Utils.join(tr(", "), Utils.transform(normalizedTags.getValues(key), new Function<String, String>() {
590
591                    @Override
592                    public String apply(String x) {
593                        return x == null || x.isEmpty() ? tr("<i>missing</i>") : x;
594                    }
595                })));
596            }
597        }));
598        String msg = tr("You are about to combine {0} objects, "
599                + "but the following tags are used conflictingly:<br/>{1}"
600                + "If these objects are combined, the resulting object may have unwanted tags.<br/>"
601                + "If you want to continue, you are shown a dialog to fix the conflicting tags.<br/><br/>"
602                + "Do you want to continue?",
603                primitives.size(), conflicts);
604
605        if (!ConditionalOptionPaneUtil.showConfirmationDialog(
606                "combine_tags",
607                Main.parent,
608                "<html>" + msg + "</html>",
609                tr("Combine confirmation"),
610                JOptionPane.YES_NO_OPTION,
611                JOptionPane.QUESTION_MESSAGE,
612                JOptionPane.YES_OPTION)) {
613            throw new UserCancelException();
614        }
615    }
616}