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