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