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.GraphicsEnvironment;
013import java.awt.event.ActionEvent;
014import java.awt.event.HierarchyBoundsListener;
015import java.awt.event.HierarchyEvent;
016import java.awt.event.WindowAdapter;
017import java.awt.event.WindowEvent;
018import java.beans.PropertyChangeEvent;
019import java.beans.PropertyChangeListener;
020import java.util.Collection;
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.data.osm.Node;
038import org.openstreetmap.josm.data.osm.OsmPrimitive;
039import org.openstreetmap.josm.data.osm.Relation;
040import org.openstreetmap.josm.data.osm.TagCollection;
041import org.openstreetmap.josm.data.osm.Way;
042import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
043import org.openstreetmap.josm.gui.DefaultNameFormatter;
044import org.openstreetmap.josm.gui.SideButton;
045import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
046import org.openstreetmap.josm.gui.help.HelpUtil;
047import org.openstreetmap.josm.gui.util.GuiHelper;
048import org.openstreetmap.josm.tools.CheckParameterUtil;
049import org.openstreetmap.josm.tools.ImageProvider;
050import org.openstreetmap.josm.tools.UserCancelException;
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 #isApplied()} to check whether the dialog has been
082 * applied. If it was applied 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    private static 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 synchronized 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    protected RelationMemberConflictResolver pnlRelationMemberConflictResolver;
112    private boolean applied;
113    private JPanel pnlButtons;
114    protected transient 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 final 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        pnlButtons = buildButtonPanel();
175        getContentPane().add(pnlButtons, BorderLayout.SOUTH);
176        addWindowListener(new AdjustDividerLocationAction());
177        HelpUtil.setHelpContext(getRootPane(), ht("/"));
178    }
179
180    protected JPanel buildTagConflictResolverPanel() {
181        pnlTagConflictResolver = new TagConflictResolver();
182        return pnlTagConflictResolver;
183    }
184
185    protected JPanel buildRelationMemberConflictResolverPanel() {
186        pnlRelationMemberConflictResolver = new RelationMemberConflictResolver(new RelationMemberConflictResolverModel());
187        return pnlRelationMemberConflictResolver;
188    }
189
190    protected ApplyAction buildApplyAction() {
191        return new ApplyAction();
192    }
193
194    protected JPanel buildButtonPanel() {
195        JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
196
197        // -- apply button
198        ApplyAction applyAction = buildApplyAction();
199        pnlTagConflictResolver.getModel().addPropertyChangeListener(applyAction);
200        pnlRelationMemberConflictResolver.getModel().addPropertyChangeListener(applyAction);
201        btnApply = new SideButton(applyAction);
202        btnApply.setFocusable(true);
203        pnl.add(btnApply);
204
205        // -- cancel button
206        CancelAction cancelAction = new CancelAction();
207        pnl.add(new SideButton(cancelAction));
208
209        // -- help button
210        helpAction = new ContextSensitiveHelpAction();
211        pnl.add(new SideButton(helpAction));
212
213        return pnl;
214    }
215
216    /**
217     * Constructs a new {@code CombinePrimitiveResolverDialog}.
218     * @param parent The parent component in which this dialog will be displayed.
219     */
220    public CombinePrimitiveResolverDialog(Component parent) {
221        super(GuiHelper.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL);
222        build();
223    }
224
225    /**
226     * Replies the tag conflict resolver model.
227     * @return The tag conflict resolver model.
228     */
229    public TagConflictResolverModel getTagConflictResolverModel() {
230        return pnlTagConflictResolver.getModel();
231    }
232
233    /**
234     * Replies the relation membership conflict resolver model.
235     * @return The relation membership conflict resolver model.
236     */
237    public RelationMemberConflictResolverModel getRelationMemberConflictResolverModel() {
238        return pnlRelationMemberConflictResolver.getModel();
239    }
240
241    /**
242     * Replies true if all tag and relation member conflicts have been decided.
243     *
244     * @return true if all tag and relation member conflicts have been decided; false otherwise
245     */
246    public boolean isResolvedCompletely() {
247        return getTagConflictResolverModel().isResolvedCompletely()
248                && getRelationMemberConflictResolverModel().isResolvedCompletely();
249    }
250
251    protected List<Command> buildTagChangeCommand(OsmPrimitive primitive, TagCollection tc) {
252        List<Command> cmds = new LinkedList<>();
253        for (String key : tc.getKeys()) {
254            if (tc.hasUniqueEmptyValue(key)) {
255                if (primitive.get(key) != null) {
256                    cmds.add(new ChangePropertyCommand(primitive, key, null));
257                }
258            } else {
259                String value = tc.getJoinedValues(key);
260                if (!value.equals(primitive.get(key))) {
261                    cmds.add(new ChangePropertyCommand(primitive, key, value));
262                }
263            }
264        }
265        return cmds;
266    }
267
268    /**
269     * Replies the list of {@link Command commands} needed to apply resolution choices.
270     * @return The list of {@link Command commands} needed to apply resolution choices.
271     */
272    public List<Command> buildResolutionCommands() {
273        List<Command> cmds = new LinkedList<>();
274
275        TagCollection allResolutions = getTagConflictResolverModel().getAllResolutions();
276        if (!allResolutions.isEmpty()) {
277            cmds.addAll(buildTagChangeCommand(targetPrimitive, allResolutions));
278        }
279        for (String p : OsmPrimitive.getDiscardableKeys()) {
280            if (targetPrimitive.get(p) != null) {
281                cmds.add(new ChangePropertyCommand(targetPrimitive, p, null));
282            }
283        }
284
285        if (getRelationMemberConflictResolverModel().getNumDecisions() > 0) {
286            cmds.addAll(getRelationMemberConflictResolverModel().buildResolutionCommands(targetPrimitive));
287        }
288
289        Command cmd = pnlRelationMemberConflictResolver.buildTagApplyCommands(getRelationMemberConflictResolverModel()
290                .getModifiedRelations(targetPrimitive));
291        if (cmd != null) {
292            cmds.add(cmd);
293        }
294        return cmds;
295    }
296
297    /**
298     * Prepares the default decisions for populated tag and relation membership conflicts.
299     */
300    public void prepareDefaultDecisions() {
301        getTagConflictResolverModel().prepareDefaultTagDecisions();
302        getRelationMemberConflictResolverModel().prepareDefaultRelationDecisions();
303    }
304
305    protected JPanel buildEmptyConflictsPanel() {
306        JPanel pnl = new JPanel(new BorderLayout());
307        pnl.add(new JLabel(tr("No conflicts to resolve")));
308        return pnl;
309    }
310
311    protected void prepareGUIBeforeConflictResolutionStarts() {
312        RelationMemberConflictResolverModel relModel = getRelationMemberConflictResolverModel();
313        TagConflictResolverModel tagModel = getTagConflictResolverModel();
314        getContentPane().removeAll();
315
316        if (relModel.getNumDecisions() > 0 && tagModel.getNumDecisions() > 0) {
317            // display both, the dialog for resolving relation conflicts and for resolving tag conflicts
318            spTagConflictTypes.setTopComponent(pnlTagConflictResolver);
319            spTagConflictTypes.setBottomComponent(pnlRelationMemberConflictResolver);
320            getContentPane().add(spTagConflictTypes, BorderLayout.CENTER);
321        } else if (relModel.getNumDecisions() > 0) {
322            // relation conflicts only
323            getContentPane().add(pnlRelationMemberConflictResolver, BorderLayout.CENTER);
324        } else if (tagModel.getNumDecisions() > 0) {
325            // tag conflicts only
326            getContentPane().add(pnlTagConflictResolver, BorderLayout.CENTER);
327        } else {
328            getContentPane().add(buildEmptyConflictsPanel(), BorderLayout.CENTER);
329        }
330
331        getContentPane().add(pnlButtons, BorderLayout.SOUTH);
332        validate();
333        adjustDividerLocation();
334        pnlRelationMemberConflictResolver.prepareForEditing();
335    }
336
337    protected void setApplied(boolean applied) {
338        this.applied = applied;
339    }
340
341    /**
342     * Determines if this dialog has been closed with "Apply".
343     * @return true if this dialog has been closed with "Apply", false otherwise.
344     */
345    public boolean isApplied() {
346        return applied;
347    }
348
349    @Override
350    public void setVisible(boolean visible) {
351        if (visible) {
352            prepareGUIBeforeConflictResolutionStarts();
353            setMinimumSize(new Dimension(400, 400));
354            new WindowGeometry(getClass().getName() + ".geometry", WindowGeometry.centerInWindow(Main.parent,
355                    new Dimension(800, 600))).applySafe(this);
356            setApplied(false);
357            btnApply.requestFocusInWindow();
358        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
359            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
360        }
361        super.setVisible(visible);
362    }
363
364    class CancelAction extends AbstractAction {
365
366        CancelAction() {
367            putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution"));
368            putValue(Action.NAME, tr("Cancel"));
369            putValue(Action.SMALL_ICON, ImageProvider.get("", "cancel"));
370            setEnabled(true);
371        }
372
373        @Override
374        public void actionPerformed(ActionEvent arg0) {
375            setVisible(false);
376        }
377    }
378
379    protected class ApplyAction extends AbstractAction implements PropertyChangeListener {
380
381        public ApplyAction() {
382            putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts"));
383            putValue(Action.NAME, tr("Apply"));
384            putValue(Action.SMALL_ICON, ImageProvider.get("ok"));
385            updateEnabledState();
386        }
387
388        @Override
389        public void actionPerformed(ActionEvent arg0) {
390            setApplied(true);
391            setVisible(false);
392            pnlTagConflictResolver.rememberPreferences();
393        }
394
395        protected final void updateEnabledState() {
396            setEnabled(pnlTagConflictResolver.getModel().getNumConflicts() == 0
397                    && pnlRelationMemberConflictResolver.getModel().getNumConflicts() == 0);
398        }
399
400        @Override
401        public void propertyChange(PropertyChangeEvent evt) {
402            if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) {
403                updateEnabledState();
404            }
405            if (evt.getPropertyName().equals(RelationMemberConflictResolverModel.NUM_CONFLICTS_PROP)) {
406                updateEnabledState();
407            }
408        }
409    }
410
411    private void adjustDividerLocation() {
412        int numTagDecisions = getTagConflictResolverModel().getNumDecisions();
413        int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions();
414        if (numTagDecisions > 0 && numRelationDecisions > 0) {
415            double nTop = 1.0 + numTagDecisions;
416            double nBottom = 2.5 + numRelationDecisions;
417            spTagConflictTypes.setDividerLocation(nTop/(nTop+nBottom));
418        }
419    }
420
421    class AdjustDividerLocationAction extends WindowAdapter {
422        @Override
423        public void windowOpened(WindowEvent e) {
424            adjustDividerLocation();
425        }
426    }
427
428    static class AutoAdjustingSplitPane extends JSplitPane implements PropertyChangeListener, HierarchyBoundsListener {
429        private double dividerLocation;
430
431        AutoAdjustingSplitPane(int newOrientation) {
432            super(newOrientation);
433            addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, this);
434            addHierarchyBoundsListener(this);
435        }
436
437        @Override
438        public void ancestorResized(HierarchyEvent e) {
439            setDividerLocation((int) (dividerLocation * getHeight()));
440        }
441
442        @Override
443        public void ancestorMoved(HierarchyEvent e) {
444            // do nothing
445        }
446
447        @Override
448        public void propertyChange(PropertyChangeEvent evt) {
449            if (JSplitPane.DIVIDER_LOCATION_PROPERTY.equals(evt.getPropertyName())) {
450                int newVal = (Integer) evt.getNewValue();
451                if (getHeight() != 0) {
452                    dividerLocation = (double) newVal / (double) getHeight();
453                }
454            }
455        }
456    }
457
458    /**
459     * Replies the list of {@link Command commands} needed to resolve specified conflicts,
460     * by displaying if necessary a {@link CombinePrimitiveResolverDialog} to the user.
461     * This dialog will allow the user to choose conflict resolution actions.
462     *
463     * Non-expert users are informed first of the meaning of these operations, allowing them to cancel.
464     *
465     * @param tagsOfPrimitives The tag collection of the primitives to be combined.
466     *                         Should generally be equal to {@code TagCollection.unionOfAllPrimitives(primitives)}
467     * @param primitives The primitives to be combined
468     * @param targetPrimitives The primitives the collection of primitives are merged or combined to.
469     * @return The list of {@link Command commands} needed to apply resolution actions.
470     * @throws UserCancelException If the user cancelled a dialog.
471     */
472    public static List<Command> launchIfNecessary(
473            final TagCollection tagsOfPrimitives,
474            final Collection<? extends OsmPrimitive> primitives,
475            final Collection<? extends OsmPrimitive> targetPrimitives) throws UserCancelException {
476
477        CheckParameterUtil.ensureParameterNotNull(tagsOfPrimitives, "tagsOfPrimitives");
478        CheckParameterUtil.ensureParameterNotNull(primitives, "primitives");
479        CheckParameterUtil.ensureParameterNotNull(targetPrimitives, "targetPrimitives");
480
481        final TagCollection completeWayTags = new TagCollection(tagsOfPrimitives);
482        TagConflictResolutionUtil.combineTigerTags(completeWayTags);
483        TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing(completeWayTags, primitives);
484        final TagCollection tagsToEdit = new TagCollection(completeWayTags);
485        TagConflictResolutionUtil.completeTagCollectionForEditing(tagsToEdit);
486
487        final Set<Relation> parentRelations = OsmPrimitive.getParentRelations(primitives);
488
489        // Show information dialogs about conflicts to non-experts
490        if (!ExpertToggleAction.isExpert()) {
491            // Tag conflicts
492            if (!completeWayTags.isApplicableToPrimitive()) {
493                informAboutTagConflicts(primitives, completeWayTags);
494            }
495            // Relation membership conflicts
496            if (!parentRelations.isEmpty()) {
497                informAboutRelationMembershipConflicts(primitives, parentRelations);
498            }
499        }
500
501        List<Command> cmds = new LinkedList<>();
502
503        if (!GraphicsEnvironment.isHeadless()) {
504            // Build conflict resolution dialog
505            final CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
506
507            dialog.getTagConflictResolverModel().populate(tagsToEdit, completeWayTags.getKeysWithMultipleValues());
508            dialog.getRelationMemberConflictResolverModel().populate(parentRelations, primitives);
509            dialog.prepareDefaultDecisions();
510
511            // Ensure a proper title is displayed instead of a previous target (fix #7925)
512            if (targetPrimitives.size() == 1) {
513                dialog.setTargetPrimitive(targetPrimitives.iterator().next());
514            } else {
515                dialog.setTargetPrimitive(null);
516            }
517
518            // Resolve tag conflicts if necessary
519            if (!dialog.isResolvedCompletely()) {
520                dialog.setVisible(true);
521                if (!dialog.isApplied()) {
522                    throw new UserCancelException();
523                }
524            }
525            for (OsmPrimitive i : targetPrimitives) {
526                dialog.setTargetPrimitive(i);
527                cmds.addAll(dialog.buildResolutionCommands());
528            }
529        }
530        return cmds;
531    }
532
533    /**
534     * Inform a non-expert user about what relation membership conflict resolution means.
535     * @param primitives The primitives to be combined
536     * @param parentRelations The parent relations of the primitives
537     * @throws UserCancelException If the user cancels the dialog.
538     */
539    protected static void informAboutRelationMembershipConflicts(
540            final Collection<? extends OsmPrimitive> primitives,
541            final Set<Relation> parentRelations) throws UserCancelException {
542        /* I18n: object count < 2 is not possible */
543        String msg = trn("You are about to combine {1} object, "
544                + "which is part of {0} relation:<br/>{2}"
545                + "Combining these objects may break this relation. If you are unsure, please cancel this operation.<br/>"
546                + "If you want to continue, you are shown a dialog to decide how to adapt the relation.<br/><br/>"
547                + "Do you want to continue?",
548                "You are about to combine {1} objects, "
549                + "which are part of {0} relations:<br/>{2}"
550                + "Combining these objects may break these relations. If you are unsure, please cancel this operation.<br/>"
551                + "If you want to continue, you are shown a dialog to decide how to adapt the relations.<br/><br/>"
552                + "Do you want to continue?",
553                parentRelations.size(), parentRelations.size(), primitives.size(),
554                DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(parentRelations, 20));
555
556        if (!ConditionalOptionPaneUtil.showConfirmationDialog(
557                "combine_tags",
558                Main.parent,
559                "<html>" + msg + "</html>",
560                tr("Combine confirmation"),
561                JOptionPane.YES_NO_OPTION,
562                JOptionPane.QUESTION_MESSAGE,
563                JOptionPane.YES_OPTION)) {
564            throw new UserCancelException();
565        }
566    }
567
568    /**
569     * Inform a non-expert user about what tag conflict resolution means.
570     * @param primitives The primitives to be combined
571     * @param normalizedTags The normalized tag collection of the primitives to be combined
572     * @throws UserCancelException If the user cancels the dialog.
573     */
574    protected static void informAboutTagConflicts(
575            final Collection<? extends OsmPrimitive> primitives,
576            final TagCollection normalizedTags) throws UserCancelException {
577        String conflicts = Utils.joinAsHtmlUnorderedList(Utils.transform(normalizedTags.getKeysWithMultipleValues(),
578                new Function<String, String>() {
579            @Override
580            public String apply(String key) {
581                return tr("{0} ({1})", key, Utils.join(tr(", "), Utils.transform(normalizedTags.getValues(key),
582                        new Function<String, String>() {
583                    @Override
584                    public String apply(String x) {
585                        return x == null || x.isEmpty() ? tr("<i>missing</i>") : x;
586                    }
587                })));
588            }
589        }));
590        String msg = /* for correct i18n of plural forms - see #9110 */ trn("You are about to combine {0} objects, "
591                + "but the following tags are used conflictingly:<br/>{1}"
592                + "If these objects are combined, the resulting object may have unwanted tags.<br/>"
593                + "If you want to continue, you are shown a dialog to fix the conflicting tags.<br/><br/>"
594                + "Do you want to continue?", "You are about to combine {0} objects, "
595                + "but the following tags are used conflictingly:<br/>{1}"
596                + "If these objects are combined, the resulting object may have unwanted tags.<br/>"
597                + "If you want to continue, you are shown a dialog to fix the conflicting tags.<br/><br/>"
598                + "Do you want to continue?",
599                primitives.size(), primitives.size(), conflicts);
600
601        if (!ConditionalOptionPaneUtil.showConfirmationDialog(
602                "combine_tags",
603                Main.parent,
604                "<html>" + msg + "</html>",
605                tr("Combine confirmation"),
606                JOptionPane.YES_NO_OPTION,
607                JOptionPane.QUESTION_MESSAGE,
608                JOptionPane.YES_OPTION)) {
609            throw new UserCancelException();
610        }
611    }
612}