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