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}