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 033 * of {@link OsmPrimitive}. 034 * 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 PropertiesMergeModel model; 051 052 protected JLabel buildValueLabel(String name) { 053 JLabel lbl = new JLabel(); 054 lbl.setName(name); 055 lbl.setHorizontalAlignment(JLabel.CENTER); 056 lbl.setOpaque(true); 057 lbl.setBorder(BorderFactory.createLoweredBevelBorder()); 058 return lbl; 059 } 060 061 protected void buildHeaderRow() { 062 GridBagConstraints gc = new GridBagConstraints(); 063 064 gc.gridx = 1; 065 gc.gridy = 0; 066 gc.gridwidth = 1; 067 gc.gridheight = 1; 068 gc.fill = GridBagConstraints.NONE; 069 gc.anchor = GridBagConstraints.CENTER; 070 gc.weightx = 0.0; 071 gc.weighty = 0.0; 072 gc.insets = new Insets(10,0,10,0); 073 JLabel lblMyVersion = new JLabel(tr("My version")); 074 lblMyVersion.setToolTipText(tr("Properties in my dataset, i.e. the local dataset")); 075 add(lblMyVersion, gc); 076 077 gc.gridx = 3; 078 gc.gridy = 0; 079 JLabel lblMergedVersion = new JLabel(tr("Merged version")); 080 lblMergedVersion.setToolTipText(tr("Properties in the merged element. They will replace properties in my elements when merge decisions are applied.")); 081 add(lblMergedVersion, gc); 082 083 gc.gridx = 5; 084 gc.gridy = 0; 085 JLabel lblTheirVersion = new JLabel(tr("Their version")); 086 lblTheirVersion.setToolTipText(tr("Properties in their dataset, i.e. the server dataset")); 087 add(lblTheirVersion, gc); 088 } 089 090 protected void buildCoordinateConflictRows() { 091 GridBagConstraints gc = new GridBagConstraints(); 092 093 gc.gridx = 0; 094 gc.gridy = 1; 095 gc.gridwidth = 1; 096 gc.gridheight = 1; 097 gc.fill = GridBagConstraints.HORIZONTAL; 098 gc.anchor = GridBagConstraints.LINE_START; 099 gc.weightx = 0.0; 100 gc.weighty = 0.0; 101 gc.insets = new Insets(0,5,0,5); 102 add(new JLabel(tr("Coordinates:")), gc); 103 104 gc.gridx = 1; 105 gc.gridy = 1; 106 gc.fill = GridBagConstraints.BOTH; 107 gc.anchor = GridBagConstraints.CENTER; 108 gc.weightx = 0.33; 109 gc.weighty = 0.0; 110 add(lblMyCoordinates = buildValueLabel("label.mycoordinates"), gc); 111 112 gc.gridx = 2; 113 gc.gridy = 1; 114 gc.fill = GridBagConstraints.NONE; 115 gc.anchor = GridBagConstraints.CENTER; 116 gc.weightx = 0.0; 117 gc.weighty = 0.0; 118 KeepMyCoordinatesAction actKeepMyCoordinates = new KeepMyCoordinatesAction(); 119 model.addObserver(actKeepMyCoordinates); 120 JButton btnKeepMyCoordinates = new JButton(actKeepMyCoordinates); 121 btnKeepMyCoordinates.setName("button.keepmycoordinates"); 122 add(btnKeepMyCoordinates, gc); 123 124 gc.gridx = 3; 125 gc.gridy = 1; 126 gc.fill = GridBagConstraints.BOTH; 127 gc.anchor = GridBagConstraints.CENTER; 128 gc.weightx = 0.33; 129 gc.weighty = 0.0; 130 add(lblMergedCoordinates = buildValueLabel("label.mergedcoordinates"), gc); 131 132 gc.gridx = 4; 133 gc.gridy = 1; 134 gc.fill = GridBagConstraints.NONE; 135 gc.anchor = GridBagConstraints.CENTER; 136 gc.weightx = 0.0; 137 gc.weighty = 0.0; 138 KeepTheirCoordinatesAction actKeepTheirCoordinates = new KeepTheirCoordinatesAction(); 139 model.addObserver(actKeepTheirCoordinates); 140 JButton btnKeepTheirCoordinates = new JButton(actKeepTheirCoordinates); 141 add(btnKeepTheirCoordinates, gc); 142 143 gc.gridx = 5; 144 gc.gridy = 1; 145 gc.fill = GridBagConstraints.BOTH; 146 gc.anchor = GridBagConstraints.CENTER; 147 gc.weightx = 0.33; 148 gc.weighty = 0.0; 149 add(lblTheirCoordinates = buildValueLabel("label.theircoordinates"), gc); 150 151 // --------------------------------------------------- 152 gc.gridx = 3; 153 gc.gridy = 2; 154 gc.fill = GridBagConstraints.NONE; 155 gc.anchor = GridBagConstraints.CENTER; 156 gc.weightx = 0.0; 157 gc.weighty = 0.0; 158 UndecideCoordinateConflictAction actUndecideCoordinates = new UndecideCoordinateConflictAction(); 159 model.addObserver(actUndecideCoordinates); 160 JButton btnUndecideCoordinates = new JButton(actUndecideCoordinates); 161 add(btnUndecideCoordinates, gc); 162 } 163 164 protected void buildDeletedStateConflictRows() { 165 GridBagConstraints gc = new GridBagConstraints(); 166 167 gc.gridx = 0; 168 gc.gridy = 3; 169 gc.gridwidth = 1; 170 gc.gridheight = 1; 171 gc.fill = GridBagConstraints.BOTH; 172 gc.anchor = GridBagConstraints.LINE_START; 173 gc.weightx = 0.0; 174 gc.weighty = 0.0; 175 gc.insets = new Insets(0,5,0,5); 176 add(new JLabel(tr("Deleted State:")), gc); 177 178 gc.gridx = 1; 179 gc.gridy = 3; 180 gc.fill = GridBagConstraints.BOTH; 181 gc.anchor = GridBagConstraints.CENTER; 182 gc.weightx = 0.33; 183 gc.weighty = 0.0; 184 add(lblMyDeletedState = buildValueLabel("label.mydeletedstate"), gc); 185 186 gc.gridx = 2; 187 gc.gridy = 3; 188 gc.fill = GridBagConstraints.NONE; 189 gc.anchor = GridBagConstraints.CENTER; 190 gc.weightx = 0.0; 191 gc.weighty = 0.0; 192 KeepMyDeletedStateAction actKeepMyDeletedState = new KeepMyDeletedStateAction(); 193 model.addObserver(actKeepMyDeletedState); 194 JButton btnKeepMyDeletedState = new JButton(actKeepMyDeletedState); 195 btnKeepMyDeletedState.setName("button.keepmydeletedstate"); 196 add(btnKeepMyDeletedState, gc); 197 198 gc.gridx = 3; 199 gc.gridy = 3; 200 gc.fill = GridBagConstraints.BOTH; 201 gc.anchor = GridBagConstraints.CENTER; 202 gc.weightx = 0.33; 203 gc.weighty = 0.0; 204 add(lblMergedDeletedState = buildValueLabel("label.mergeddeletedstate"), gc); 205 206 gc.gridx = 4; 207 gc.gridy = 3; 208 gc.fill = GridBagConstraints.NONE; 209 gc.anchor = GridBagConstraints.CENTER; 210 gc.weightx = 0.0; 211 gc.weighty = 0.0; 212 KeepTheirDeletedStateAction actKeepTheirDeletedState = new KeepTheirDeletedStateAction(); 213 model.addObserver(actKeepTheirDeletedState); 214 JButton btnKeepTheirDeletedState = new JButton(actKeepTheirDeletedState); 215 btnKeepTheirDeletedState.setName("button.keeptheirdeletedstate"); 216 add(btnKeepTheirDeletedState, gc); 217 218 gc.gridx = 5; 219 gc.gridy = 3; 220 gc.fill = GridBagConstraints.BOTH; 221 gc.anchor = GridBagConstraints.CENTER; 222 gc.weightx = 0.33; 223 gc.weighty = 0.0; 224 add(lblTheirDeletedState = buildValueLabel("label.theirdeletedstate"), gc); 225 226 // --------------------------------------------------- 227 gc.gridx = 3; 228 gc.gridy = 4; 229 gc.fill = GridBagConstraints.NONE; 230 gc.anchor = GridBagConstraints.CENTER; 231 gc.weightx = 0.0; 232 gc.weighty = 0.0; 233 UndecideDeletedStateConflictAction actUndecideDeletedState = new UndecideDeletedStateConflictAction(); 234 model.addObserver(actUndecideDeletedState); 235 JButton btnUndecideDeletedState = new JButton(actUndecideDeletedState); 236 btnUndecideDeletedState.setName("button.undecidedeletedstate"); 237 add(btnUndecideDeletedState, gc); 238 } 239 240 protected void buildReferrersRow() { 241 GridBagConstraints gc = new GridBagConstraints(); 242 243 gc.gridx = 0; 244 gc.gridy = 7; 245 gc.gridwidth = 1; 246 gc.gridheight = 1; 247 gc.fill = GridBagConstraints.BOTH; 248 gc.anchor = GridBagConstraints.LINE_START; 249 gc.weightx = 0.0; 250 gc.weighty = 0.0; 251 gc.insets = new Insets(0,5,0,5); 252 add(new JLabel(tr("Referenced by:")), gc); 253 254 gc.gridx = 1; 255 gc.gridy = 7; 256 gc.fill = GridBagConstraints.BOTH; 257 gc.anchor = GridBagConstraints.CENTER; 258 gc.weightx = 0.33; 259 gc.weighty = 0.0; 260 add(lblMyReferrers = buildValueLabel("label.myreferrers"), gc); 261 262 gc.gridx = 5; 263 gc.gridy = 7; 264 gc.fill = GridBagConstraints.BOTH; 265 gc.anchor = GridBagConstraints.CENTER; 266 gc.weightx = 0.33; 267 gc.weighty = 0.0; 268 add(lblTheirReferrers = buildValueLabel("label.theirreferrers"), gc); 269 } 270 271 protected final void build() { 272 setLayout(new GridBagLayout()); 273 buildHeaderRow(); 274 buildCoordinateConflictRows(); 275 buildDeletedStateConflictRows(); 276 buildReferrersRow(); 277 } 278 279 /** 280 * Constructs a new {@code PropertiesMerger}. 281 */ 282 public PropertiesMerger() { 283 model = new PropertiesMergeModel(); 284 model.addObserver(this); 285 build(); 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 public 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()); 407 } 408 } 409 410 class KeepTheirCoordinatesAction extends AbstractAction implements Observer { 411 public 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()); 424 } 425 } 426 427 class UndecideCoordinateConflictAction extends AbstractAction implements Observer { 428 public 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 public 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 public 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 public 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}