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