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