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