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