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}