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}