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 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 void build() { 272 setLayout(new GridBagLayout()); 273 buildHeaderRow(); 274 buildCoordinateConflictRows(); 275 buildDeletedStateConflictRows(); 276 buildReferrersRow(); 277 } 278 279 public PropertiesMerger() { 280 model = new PropertiesMergeModel(); 281 model.addObserver(this); 282 build(); 283 } 284 285 public String coordToString(LatLon coord) { 286 if (coord == null) 287 return tr("(none)"); 288 StringBuilder sb = new StringBuilder(); 289 sb.append("(") 290 .append(COORD_FORMATTER.format(coord.lat())) 291 .append(",") 292 .append(COORD_FORMATTER.format(coord.lon())) 293 .append(")"); 294 return sb.toString(); 295 } 296 297 public String deletedStateToString(Boolean deleted) { 298 if (deleted == null) 299 return tr("(none)"); 300 if (deleted) 301 return tr("deleted"); 302 else 303 return tr("not deleted"); 304 } 305 306 public String referrersToString(List<OsmPrimitive> referrers) { 307 if (referrers.isEmpty()) 308 return tr("(none)"); 309 StringBuilder str = new StringBuilder("<html>"); 310 for (OsmPrimitive r: referrers) { 311 str.append(r.getDisplayName(DefaultNameFormatter.getInstance())).append("<br>"); 312 } 313 str.append("</html>"); 314 return str.toString(); 315 } 316 317 protected void updateCoordinates() { 318 lblMyCoordinates.setText(coordToString(model.getMyCoords())); 319 lblMergedCoordinates.setText(coordToString(model.getMergedCoords())); 320 lblTheirCoordinates.setText(coordToString(model.getTheirCoords())); 321 if (! model.hasCoordConflict()) { 322 lblMyCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 323 lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 324 lblTheirCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 325 } else { 326 if (!model.isDecidedCoord()) { 327 lblMyCoordinates.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get()); 328 lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 329 lblTheirCoordinates.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get()); 330 } else { 331 lblMyCoordinates.setBackground( 332 model.isCoordMergeDecision(MergeDecisionType.KEEP_MINE) 333 ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get() 334 ); 335 lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_DECIDED.get()); 336 lblTheirCoordinates.setBackground( 337 model.isCoordMergeDecision(MergeDecisionType.KEEP_THEIR) 338 ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get() 339 ); 340 } 341 } 342 } 343 344 protected void updateDeletedState() { 345 lblMyDeletedState.setText(deletedStateToString(model.getMyDeletedState())); 346 lblMergedDeletedState.setText(deletedStateToString(model.getMergedDeletedState())); 347 lblTheirDeletedState.setText(deletedStateToString(model.getTheirDeletedState())); 348 349 if (! model.hasDeletedStateConflict()) { 350 lblMyDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 351 lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 352 lblTheirDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 353 } else { 354 if (!model.isDecidedDeletedState()) { 355 lblMyDeletedState.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get()); 356 lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 357 lblTheirDeletedState.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get()); 358 } else { 359 lblMyDeletedState.setBackground( 360 model.isDeletedStateDecision(MergeDecisionType.KEEP_MINE) 361 ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get() 362 ); 363 lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_DECIDED.get()); 364 lblTheirDeletedState.setBackground( 365 model.isDeletedStateDecision(MergeDecisionType.KEEP_THEIR) 366 ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get() 367 ); 368 } 369 } 370 } 371 372 protected void updateReferrers() { 373 lblMyReferrers.setText(referrersToString(model.getMyReferrers())); 374 lblMyReferrers.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 375 lblTheirReferrers.setText(referrersToString(model.getTheirReferrers())); 376 lblTheirReferrers.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 377 } 378 379 @Override 380 public void update(Observable o, Object arg) { 381 updateCoordinates(); 382 updateDeletedState(); 383 updateReferrers(); 384 } 385 386 public PropertiesMergeModel getModel() { 387 return model; 388 } 389 390 class KeepMyCoordinatesAction extends AbstractAction implements Observer { 391 public KeepMyCoordinatesAction() { 392 putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeepmine")); 393 putValue(Action.SHORT_DESCRIPTION, tr("Keep my coordinates")); 394 } 395 396 @Override 397 public void actionPerformed(ActionEvent e) { 398 model.decideCoordsConflict(MergeDecisionType.KEEP_MINE); 399 } 400 401 @Override 402 public void update(Observable o, Object arg) { 403 setEnabled(model.hasCoordConflict() && ! model.isDecidedCoord()); 404 } 405 } 406 407 class KeepTheirCoordinatesAction extends AbstractAction implements Observer { 408 public KeepTheirCoordinatesAction() { 409 putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeeptheir")); 410 putValue(Action.SHORT_DESCRIPTION, tr("Keep their coordinates")); 411 } 412 413 @Override 414 public void actionPerformed(ActionEvent e) { 415 model.decideCoordsConflict(MergeDecisionType.KEEP_THEIR); 416 } 417 418 @Override 419 public void update(Observable o, Object arg) { 420 setEnabled(model.hasCoordConflict() && ! model.isDecidedCoord()); 421 } 422 } 423 424 class UndecideCoordinateConflictAction extends AbstractAction implements Observer { 425 public UndecideCoordinateConflictAction() { 426 putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagundecide")); 427 putValue(Action.SHORT_DESCRIPTION, tr("Undecide conflict between different coordinates")); 428 } 429 430 @Override 431 public void actionPerformed(ActionEvent e) { 432 model.decideCoordsConflict(MergeDecisionType.UNDECIDED); 433 } 434 435 @Override 436 public void update(Observable o, Object arg) { 437 setEnabled(model.hasCoordConflict() && model.isDecidedCoord()); 438 } 439 } 440 441 class KeepMyDeletedStateAction extends AbstractAction implements Observer { 442 public KeepMyDeletedStateAction() { 443 putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeepmine")); 444 putValue(Action.SHORT_DESCRIPTION, tr("Keep my deleted state")); 445 } 446 447 @Override 448 public void actionPerformed(ActionEvent e) { 449 model.decideDeletedStateConflict(MergeDecisionType.KEEP_MINE); 450 } 451 452 @Override 453 public void update(Observable o, Object arg) { 454 setEnabled(model.hasDeletedStateConflict() && ! model.isDecidedDeletedState()); 455 } 456 } 457 458 class KeepTheirDeletedStateAction extends AbstractAction implements Observer { 459 public KeepTheirDeletedStateAction() { 460 putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagkeeptheir")); 461 putValue(Action.SHORT_DESCRIPTION, tr("Keep their deleted state")); 462 } 463 464 @Override 465 public void actionPerformed(ActionEvent e) { 466 model.decideDeletedStateConflict(MergeDecisionType.KEEP_THEIR); 467 } 468 469 @Override 470 public void update(Observable o, Object arg) { 471 setEnabled(model.hasDeletedStateConflict() && ! model.isDecidedDeletedState()); 472 } 473 } 474 475 class UndecideDeletedStateConflictAction extends AbstractAction implements Observer { 476 public UndecideDeletedStateConflictAction() { 477 putValue(Action.SMALL_ICON, ImageProvider.get("dialogs/conflict", "tagundecide")); 478 putValue(Action.SHORT_DESCRIPTION, tr("Undecide conflict between deleted state")); 479 } 480 481 @Override 482 public void actionPerformed(ActionEvent e) { 483 model.decideDeletedStateConflict(MergeDecisionType.UNDECIDED); 484 } 485 486 @Override 487 public void update(Observable o, Object arg) { 488 setEnabled(model.hasDeletedStateConflict() && model.isDecidedDeletedState()); 489 } 490 } 491 492 @Override 493 public void deletePrimitive(boolean deleted) { 494 if (deleted) { 495 if (model.getMergedCoords() == null) { 496 model.decideCoordsConflict(MergeDecisionType.KEEP_MINE); 497 } 498 } else { 499 model.decideCoordsConflict(MergeDecisionType.UNDECIDED); 500 } 501 } 502 503 @Override 504 public void populate(Conflict<? extends OsmPrimitive> conflict) { 505 model.populate(conflict); 506 } 507}