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