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