001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.conflict.pair.properties; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.GridBagConstraints; 007import java.awt.GridBagLayout; 008import java.awt.Insets; 009import java.awt.event.ActionEvent; 010import java.text.DecimalFormat; 011import java.util.List; 012 013import javax.swing.AbstractAction; 014import javax.swing.Action; 015import javax.swing.BorderFactory; 016import javax.swing.JButton; 017import javax.swing.JLabel; 018import javax.swing.JPanel; 019import javax.swing.event.ChangeEvent; 020import javax.swing.event.ChangeListener; 021 022import org.openstreetmap.josm.data.conflict.Conflict; 023import org.openstreetmap.josm.data.coor.LatLon; 024import org.openstreetmap.josm.data.osm.OsmPrimitive; 025import org.openstreetmap.josm.gui.DefaultNameFormatter; 026import org.openstreetmap.josm.gui.conflict.ConflictColors; 027import org.openstreetmap.josm.gui.conflict.pair.IConflictResolver; 028import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType; 029import org.openstreetmap.josm.gui.history.VersionInfoPanel; 030import org.openstreetmap.josm.tools.ImageProvider; 031 032/** 033 * This class represents a UI component for resolving conflicts in some properties of {@link OsmPrimitive}. 034 * @since 1654 035 */ 036public class PropertiesMerger extends JPanel implements ChangeListener, IConflictResolver { 037 private static final DecimalFormat COORD_FORMATTER = new DecimalFormat("###0.0000000"); 038 039 private final JLabel lblMyCoordinates = buildValueLabel("label.mycoordinates"); 040 private final JLabel lblMergedCoordinates = buildValueLabel("label.mergedcoordinates"); 041 private final JLabel lblTheirCoordinates = buildValueLabel("label.theircoordinates"); 042 043 private final JLabel lblMyDeletedState = buildValueLabel("label.mydeletedstate"); 044 private final JLabel lblMergedDeletedState = buildValueLabel("label.mergeddeletedstate"); 045 private final JLabel lblTheirDeletedState = buildValueLabel("label.theirdeletedstate"); 046 047 private final JLabel lblMyReferrers = buildValueLabel("label.myreferrers"); 048 private final JLabel lblTheirReferrers = buildValueLabel("label.theirreferrers"); 049 050 private final transient PropertiesMergeModel model; 051 private final VersionInfoPanel mineVersionInfo = new VersionInfoPanel(); 052 private final VersionInfoPanel theirVersionInfo = new VersionInfoPanel(); 053 054 /** 055 * Constructs a new {@code PropertiesMerger}. 056 */ 057 public PropertiesMerger() { 058 model = new PropertiesMergeModel(); 059 model.addChangeListener(this); 060 build(); 061 } 062 063 protected static JLabel buildValueLabel(String name) { 064 JLabel lbl = new JLabel(); 065 lbl.setName(name); 066 lbl.setHorizontalAlignment(JLabel.CENTER); 067 lbl.setOpaque(true); 068 lbl.setBorder(BorderFactory.createLoweredBevelBorder()); 069 return lbl; 070 } 071 072 protected void buildHeaderRow() { 073 GridBagConstraints gc = new GridBagConstraints(); 074 075 gc.gridx = 1; 076 gc.gridy = 0; 077 gc.gridwidth = 1; 078 gc.gridheight = 1; 079 gc.fill = GridBagConstraints.NONE; 080 gc.anchor = GridBagConstraints.CENTER; 081 gc.weightx = 0.0; 082 gc.weighty = 0.0; 083 gc.insets = new Insets(10, 0, 0, 0); 084 JLabel lblMyVersion = new JLabel(tr("My version")); 085 lblMyVersion.setToolTipText(tr("Properties in my dataset, i.e. the local dataset")); 086 lblMyVersion.setLabelFor(mineVersionInfo); 087 add(lblMyVersion, gc); 088 089 gc.gridx = 3; 090 JLabel lblMergedVersion = new JLabel(tr("Merged version")); 091 lblMergedVersion.setToolTipText( 092 tr("Properties in the merged element. They will replace properties in my elements when merge decisions are applied.")); 093 add(lblMergedVersion, gc); 094 095 gc.gridx = 5; 096 JLabel lblTheirVersion = new JLabel(tr("Their version")); 097 lblTheirVersion.setToolTipText(tr("Properties in their dataset, i.e. the server dataset")); 098 lblMyVersion.setLabelFor(theirVersionInfo); 099 add(lblTheirVersion, gc); 100 101 gc.gridx = 1; 102 gc.gridy = 1; 103 gc.fill = GridBagConstraints.HORIZONTAL; 104 gc.anchor = GridBagConstraints.LINE_START; 105 gc.insets = new Insets(0, 0, 20, 0); 106 add(mineVersionInfo, gc); 107 108 gc.gridx = 5; 109 add(theirVersionInfo, gc); 110 } 111 112 protected void buildCoordinateConflictRows() { 113 GridBagConstraints gc = new GridBagConstraints(); 114 115 gc.gridx = 0; 116 gc.gridy = 2; 117 gc.gridwidth = 1; 118 gc.gridheight = 1; 119 gc.fill = GridBagConstraints.HORIZONTAL; 120 gc.anchor = GridBagConstraints.LINE_START; 121 gc.weightx = 0.0; 122 gc.weighty = 0.0; 123 gc.insets = new Insets(0, 5, 0, 5); 124 add(new JLabel(tr("Coordinates:")), gc); 125 126 gc.gridx = 1; 127 gc.fill = GridBagConstraints.BOTH; 128 gc.anchor = GridBagConstraints.CENTER; 129 gc.weightx = 0.33; 130 gc.weighty = 0.0; 131 add(lblMyCoordinates, gc); 132 133 gc.gridx = 2; 134 gc.fill = GridBagConstraints.NONE; 135 gc.anchor = GridBagConstraints.CENTER; 136 gc.weightx = 0.0; 137 gc.weighty = 0.0; 138 KeepMyCoordinatesAction actKeepMyCoordinates = new KeepMyCoordinatesAction(); 139 model.addChangeListener(actKeepMyCoordinates); 140 JButton btnKeepMyCoordinates = new JButton(actKeepMyCoordinates); 141 btnKeepMyCoordinates.setName("button.keepmycoordinates"); 142 add(btnKeepMyCoordinates, gc); 143 144 gc.gridx = 3; 145 gc.fill = GridBagConstraints.BOTH; 146 gc.anchor = GridBagConstraints.CENTER; 147 gc.weightx = 0.33; 148 gc.weighty = 0.0; 149 add(lblMergedCoordinates, gc); 150 151 gc.gridx = 4; 152 gc.fill = GridBagConstraints.NONE; 153 gc.anchor = GridBagConstraints.CENTER; 154 gc.weightx = 0.0; 155 gc.weighty = 0.0; 156 KeepTheirCoordinatesAction actKeepTheirCoordinates = new KeepTheirCoordinatesAction(); 157 model.addChangeListener(actKeepTheirCoordinates); 158 JButton btnKeepTheirCoordinates = new JButton(actKeepTheirCoordinates); 159 add(btnKeepTheirCoordinates, gc); 160 161 gc.gridx = 5; 162 gc.fill = GridBagConstraints.BOTH; 163 gc.anchor = GridBagConstraints.CENTER; 164 gc.weightx = 0.33; 165 gc.weighty = 0.0; 166 add(lblTheirCoordinates, gc); 167 168 // --------------------------------------------------- 169 gc.gridx = 3; 170 gc.gridy = 3; 171 gc.fill = GridBagConstraints.NONE; 172 gc.anchor = GridBagConstraints.CENTER; 173 gc.weightx = 0.0; 174 gc.weighty = 0.0; 175 UndecideCoordinateConflictAction actUndecideCoordinates = new UndecideCoordinateConflictAction(); 176 model.addChangeListener(actUndecideCoordinates); 177 JButton btnUndecideCoordinates = new JButton(actUndecideCoordinates); 178 add(btnUndecideCoordinates, gc); 179 } 180 181 protected void buildDeletedStateConflictRows() { 182 GridBagConstraints gc = new GridBagConstraints(); 183 184 gc.gridx = 0; 185 gc.gridy = 4; 186 gc.gridwidth = 1; 187 gc.gridheight = 1; 188 gc.fill = GridBagConstraints.BOTH; 189 gc.anchor = GridBagConstraints.LINE_START; 190 gc.weightx = 0.0; 191 gc.weighty = 0.0; 192 gc.insets = new Insets(0, 5, 0, 5); 193 add(new JLabel(tr("Deleted State:")), gc); 194 195 gc.gridx = 1; 196 gc.fill = GridBagConstraints.BOTH; 197 gc.anchor = GridBagConstraints.CENTER; 198 gc.weightx = 0.33; 199 gc.weighty = 0.0; 200 add(lblMyDeletedState, gc); 201 202 gc.gridx = 2; 203 gc.fill = GridBagConstraints.NONE; 204 gc.anchor = GridBagConstraints.CENTER; 205 gc.weightx = 0.0; 206 gc.weighty = 0.0; 207 KeepMyDeletedStateAction actKeepMyDeletedState = new KeepMyDeletedStateAction(); 208 model.addChangeListener(actKeepMyDeletedState); 209 JButton btnKeepMyDeletedState = new JButton(actKeepMyDeletedState); 210 btnKeepMyDeletedState.setName("button.keepmydeletedstate"); 211 add(btnKeepMyDeletedState, gc); 212 213 gc.gridx = 3; 214 gc.fill = GridBagConstraints.BOTH; 215 gc.anchor = GridBagConstraints.CENTER; 216 gc.weightx = 0.33; 217 gc.weighty = 0.0; 218 add(lblMergedDeletedState, gc); 219 220 gc.gridx = 4; 221 gc.fill = GridBagConstraints.NONE; 222 gc.anchor = GridBagConstraints.CENTER; 223 gc.weightx = 0.0; 224 gc.weighty = 0.0; 225 KeepTheirDeletedStateAction actKeepTheirDeletedState = new KeepTheirDeletedStateAction(); 226 model.addChangeListener(actKeepTheirDeletedState); 227 JButton btnKeepTheirDeletedState = new JButton(actKeepTheirDeletedState); 228 btnKeepTheirDeletedState.setName("button.keeptheirdeletedstate"); 229 add(btnKeepTheirDeletedState, gc); 230 231 gc.gridx = 5; 232 gc.fill = GridBagConstraints.BOTH; 233 gc.anchor = GridBagConstraints.CENTER; 234 gc.weightx = 0.33; 235 gc.weighty = 0.0; 236 add(lblTheirDeletedState, gc); 237 238 // --------------------------------------------------- 239 gc.gridx = 3; 240 gc.gridy = 5; 241 gc.fill = GridBagConstraints.NONE; 242 gc.anchor = GridBagConstraints.CENTER; 243 gc.weightx = 0.0; 244 gc.weighty = 0.0; 245 UndecideDeletedStateConflictAction actUndecideDeletedState = new UndecideDeletedStateConflictAction(); 246 model.addChangeListener(actUndecideDeletedState); 247 JButton btnUndecideDeletedState = new JButton(actUndecideDeletedState); 248 btnUndecideDeletedState.setName("button.undecidedeletedstate"); 249 add(btnUndecideDeletedState, gc); 250 } 251 252 protected void buildReferrersRow() { 253 GridBagConstraints gc = new GridBagConstraints(); 254 255 gc.gridx = 0; 256 gc.gridy = 7; 257 gc.gridwidth = 1; 258 gc.gridheight = 1; 259 gc.fill = GridBagConstraints.BOTH; 260 gc.anchor = GridBagConstraints.LINE_START; 261 gc.weightx = 0.0; 262 gc.weighty = 0.0; 263 gc.insets = new Insets(0, 5, 0, 5); 264 add(new JLabel(tr("Referenced by:")), gc); 265 266 gc.gridx = 1; 267 gc.gridy = 7; 268 gc.fill = GridBagConstraints.BOTH; 269 gc.anchor = GridBagConstraints.CENTER; 270 gc.weightx = 0.33; 271 gc.weighty = 0.0; 272 add(lblMyReferrers, gc); 273 274 gc.gridx = 5; 275 gc.gridy = 7; 276 gc.fill = GridBagConstraints.BOTH; 277 gc.anchor = GridBagConstraints.CENTER; 278 gc.weightx = 0.33; 279 gc.weighty = 0.0; 280 add(lblTheirReferrers, gc); 281 } 282 283 protected final void build() { 284 setLayout(new GridBagLayout()); 285 buildHeaderRow(); 286 buildCoordinateConflictRows(); 287 buildDeletedStateConflictRows(); 288 buildReferrersRow(); 289 } 290 291 protected static String coordToString(LatLon coord) { 292 if (coord == null) 293 return tr("(none)"); 294 StringBuilder sb = new StringBuilder(); 295 sb.append('(') 296 .append(COORD_FORMATTER.format(coord.lat())) 297 .append(',') 298 .append(COORD_FORMATTER.format(coord.lon())) 299 .append(')'); 300 return sb.toString(); 301 } 302 303 protected static String deletedStateToString(Boolean deleted) { 304 if (deleted == null) 305 return tr("(none)"); 306 if (deleted) 307 return tr("deleted"); 308 else 309 return tr("not deleted"); 310 } 311 312 protected static String referrersToString(List<OsmPrimitive> referrers) { 313 if (referrers.isEmpty()) 314 return tr("(none)"); 315 StringBuilder str = new StringBuilder("<html>"); 316 for (OsmPrimitive r: referrers) { 317 str.append(r.getDisplayName(DefaultNameFormatter.getInstance())).append("<br>"); 318 } 319 str.append("</html>"); 320 return str.toString(); 321 } 322 323 protected void updateCoordinates() { 324 lblMyCoordinates.setText(coordToString(model.getMyCoords())); 325 lblMergedCoordinates.setText(coordToString(model.getMergedCoords())); 326 lblTheirCoordinates.setText(coordToString(model.getTheirCoords())); 327 if (!model.hasCoordConflict()) { 328 lblMyCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 329 lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 330 lblTheirCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 331 } else { 332 if (!model.isDecidedCoord()) { 333 lblMyCoordinates.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get()); 334 lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 335 lblTheirCoordinates.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get()); 336 } else { 337 lblMyCoordinates.setBackground( 338 model.isCoordMergeDecision(MergeDecisionType.KEEP_MINE) 339 ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get() 340 ); 341 lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_DECIDED.get()); 342 lblTheirCoordinates.setBackground( 343 model.isCoordMergeDecision(MergeDecisionType.KEEP_THEIR) 344 ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get() 345 ); 346 } 347 } 348 } 349 350 protected void updateDeletedState() { 351 lblMyDeletedState.setText(deletedStateToString(model.getMyDeletedState())); 352 lblMergedDeletedState.setText(deletedStateToString(model.getMergedDeletedState())); 353 lblTheirDeletedState.setText(deletedStateToString(model.getTheirDeletedState())); 354 355 if (!model.hasDeletedStateConflict()) { 356 lblMyDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 357 lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 358 lblTheirDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 359 } else { 360 if (!model.isDecidedDeletedState()) { 361 lblMyDeletedState.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get()); 362 lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 363 lblTheirDeletedState.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get()); 364 } else { 365 lblMyDeletedState.setBackground( 366 model.isDeletedStateDecision(MergeDecisionType.KEEP_MINE) 367 ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get() 368 ); 369 lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_DECIDED.get()); 370 lblTheirDeletedState.setBackground( 371 model.isDeletedStateDecision(MergeDecisionType.KEEP_THEIR) 372 ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get() 373 ); 374 } 375 } 376 } 377 378 protected void updateReferrers() { 379 lblMyReferrers.setText(referrersToString(model.getMyReferrers())); 380 lblMyReferrers.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 381 lblTheirReferrers.setText(referrersToString(model.getTheirReferrers())); 382 lblTheirReferrers.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 383 } 384 385 @Override 386 public void stateChanged(ChangeEvent e) { 387 updateCoordinates(); 388 updateDeletedState(); 389 updateReferrers(); 390 } 391 392 /** 393 * Returns properties merge model. 394 * @return properties merge model 395 */ 396 public PropertiesMergeModel getModel() { 397 return model; 398 } 399 400 class KeepMyCoordinatesAction extends AbstractAction implements ChangeListener { 401 KeepMyCoordinatesAction() { 402 putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeepmine")); 403 putValue(Action.SHORT_DESCRIPTION, tr("Keep my coordinates")); 404 } 405 406 @Override 407 public void actionPerformed(ActionEvent e) { 408 model.decideCoordsConflict(MergeDecisionType.KEEP_MINE); 409 } 410 411 @Override 412 public void stateChanged(ChangeEvent e) { 413 setEnabled(model.hasCoordConflict() && !model.isDecidedCoord() && model.getMyCoords() != null); 414 } 415 } 416 417 class KeepTheirCoordinatesAction extends AbstractAction implements ChangeListener { 418 KeepTheirCoordinatesAction() { 419 putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeeptheir")); 420 putValue(Action.SHORT_DESCRIPTION, tr("Keep their coordinates")); 421 } 422 423 @Override 424 public void actionPerformed(ActionEvent e) { 425 model.decideCoordsConflict(MergeDecisionType.KEEP_THEIR); 426 } 427 428 @Override 429 public void stateChanged(ChangeEvent e) { 430 setEnabled(model.hasCoordConflict() && !model.isDecidedCoord() && model.getTheirCoords() != null); 431 } 432 } 433 434 class UndecideCoordinateConflictAction extends AbstractAction implements ChangeListener { 435 UndecideCoordinateConflictAction() { 436 putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagundecide")); 437 putValue(Action.SHORT_DESCRIPTION, tr("Undecide conflict between different coordinates")); 438 } 439 440 @Override 441 public void actionPerformed(ActionEvent e) { 442 model.decideCoordsConflict(MergeDecisionType.UNDECIDED); 443 } 444 445 @Override 446 public void stateChanged(ChangeEvent e) { 447 setEnabled(model.hasCoordConflict() && model.isDecidedCoord()); 448 } 449 } 450 451 class KeepMyDeletedStateAction extends AbstractAction implements ChangeListener { 452 KeepMyDeletedStateAction() { 453 putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeepmine")); 454 putValue(Action.SHORT_DESCRIPTION, tr("Keep my deleted state")); 455 } 456 457 @Override 458 public void actionPerformed(ActionEvent e) { 459 model.decideDeletedStateConflict(MergeDecisionType.KEEP_MINE); 460 } 461 462 @Override 463 public void stateChanged(ChangeEvent e) { 464 setEnabled(model.hasDeletedStateConflict() && !model.isDecidedDeletedState()); 465 } 466 } 467 468 class KeepTheirDeletedStateAction extends AbstractAction implements ChangeListener { 469 KeepTheirDeletedStateAction() { 470 putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeeptheir")); 471 putValue(Action.SHORT_DESCRIPTION, tr("Keep their deleted state")); 472 } 473 474 @Override 475 public void actionPerformed(ActionEvent e) { 476 model.decideDeletedStateConflict(MergeDecisionType.KEEP_THEIR); 477 } 478 479 @Override 480 public void stateChanged(ChangeEvent e) { 481 setEnabled(model.hasDeletedStateConflict() && !model.isDecidedDeletedState()); 482 } 483 } 484 485 class UndecideDeletedStateConflictAction extends AbstractAction implements ChangeListener { 486 UndecideDeletedStateConflictAction() { 487 putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagundecide")); 488 putValue(Action.SHORT_DESCRIPTION, tr("Undecide conflict between deleted state")); 489 } 490 491 @Override 492 public void actionPerformed(ActionEvent e) { 493 model.decideDeletedStateConflict(MergeDecisionType.UNDECIDED); 494 } 495 496 @Override 497 public void stateChanged(ChangeEvent e) { 498 setEnabled(model.hasDeletedStateConflict() && model.isDecidedDeletedState()); 499 } 500 } 501 502 @Override 503 public void deletePrimitive(boolean deleted) { 504 if (deleted) { 505 if (model.getMergedCoords() == null) { 506 model.decideCoordsConflict(MergeDecisionType.KEEP_MINE); 507 } 508 } else { 509 model.decideCoordsConflict(MergeDecisionType.UNDECIDED); 510 } 511 } 512 513 @Override 514 public void populate(Conflict<? extends OsmPrimitive> conflict) { 515 model.populate(conflict); 516 mineVersionInfo.update(conflict.getMy(), true); 517 theirVersionInfo.update(conflict.getTheir(), false); 518 } 519 520 @Override 521 public void decideRemaining(MergeDecisionType decision) { 522 if (!model.isDecidedCoord()) { 523 model.decideDeletedStateConflict(decision); 524 } 525 if (!model.isDecidedCoord()) { 526 model.decideCoordsConflict(decision); 527 } 528 } 529}