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.event.ActionEvent; 007import java.text.DecimalFormat; 008import java.util.Arrays; 009import java.util.List; 010 011import javax.swing.AbstractAction; 012import javax.swing.Action; 013import javax.swing.BorderFactory; 014import javax.swing.JButton; 015import javax.swing.JComponent; 016import javax.swing.JLabel; 017import javax.swing.JPanel; 018import javax.swing.event.ChangeEvent; 019import javax.swing.event.ChangeListener; 020 021import org.openstreetmap.josm.data.conflict.Conflict; 022import org.openstreetmap.josm.data.coor.LatLon; 023import org.openstreetmap.josm.data.osm.DefaultNameFormatter; 024import org.openstreetmap.josm.data.osm.OsmPrimitive; 025import org.openstreetmap.josm.gui.conflict.ConflictColors; 026import org.openstreetmap.josm.gui.conflict.pair.AbstractMergePanel; 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.GBC; 031import org.openstreetmap.josm.tools.ImageProvider; 032import org.openstreetmap.josm.tools.Utils; 033 034/** 035 * This class represents a UI component for resolving conflicts in some properties of {@link OsmPrimitive}. 036 * @since 1654 037 */ 038public class PropertiesMerger extends AbstractMergePanel implements ChangeListener, IConflictResolver { 039 private static final DecimalFormat COORD_FORMATTER = new DecimalFormat("###0.0000000"); 040 041 private final JLabel lblMyCoordinates = buildValueLabel("label.mycoordinates"); 042 private final JLabel lblMergedCoordinates = buildValueLabel("label.mergedcoordinates"); 043 private final JLabel lblTheirCoordinates = buildValueLabel("label.theircoordinates"); 044 045 private final JLabel lblMyDeletedState = buildValueLabel("label.mydeletedstate"); 046 private final JLabel lblMergedDeletedState = buildValueLabel("label.mergeddeletedstate"); 047 private final JLabel lblTheirDeletedState = buildValueLabel("label.theirdeletedstate"); 048 049 private final JLabel lblMyReferrers = buildValueLabel("label.myreferrers"); 050 private final JLabel lblTheirReferrers = buildValueLabel("label.theirreferrers"); 051 052 private final transient PropertiesMergeModel model = new PropertiesMergeModel(); 053 private final VersionInfoPanel mineVersionInfo = new VersionInfoPanel(); 054 private final VersionInfoPanel theirVersionInfo = new VersionInfoPanel(); 055 056 /** 057 * Constructs a new {@code PropertiesMerger}. 058 */ 059 public PropertiesMerger() { 060 model.addChangeListener(this); 061 buildRows(); 062 } 063 064 @Override 065 protected List<? extends MergeRow> getRows() { 066 return Arrays.asList( 067 new AbstractMergePanel.TitleRow(), 068 new VersionInfoRow(), 069 new MergeCoordinatesRow(), 070 new UndecideCoordinatesRow(), 071 new MergeDeletedStateRow(), 072 new UndecideDeletedStateRow(), 073 new ReferrersRow(), 074 new EmptyFillRow()); 075 } 076 077 protected static JLabel buildValueLabel(String name) { 078 JLabel lbl = new JLabel(); 079 lbl.setName(name); 080 lbl.setHorizontalAlignment(JLabel.CENTER); 081 lbl.setOpaque(true); 082 lbl.setBorder(BorderFactory.createLoweredBevelBorder()); 083 return lbl; 084 } 085 086 protected static String coordToString(LatLon coord) { 087 if (coord == null) 088 return tr("(none)"); 089 StringBuilder sb = new StringBuilder(); 090 sb.append('(') 091 .append(COORD_FORMATTER.format(coord.lat())) 092 .append(',') 093 .append(COORD_FORMATTER.format(coord.lon())) 094 .append(')'); 095 return sb.toString(); 096 } 097 098 protected static String deletedStateToString(Boolean deleted) { 099 if (deleted == null) 100 return tr("(none)"); 101 if (deleted) 102 return tr("deleted"); 103 else 104 return tr("not deleted"); 105 } 106 107 protected static String referrersToString(List<OsmPrimitive> referrers) { 108 if (referrers.isEmpty()) 109 return tr("(none)"); 110 StringBuilder str = new StringBuilder("<html>"); 111 for (OsmPrimitive r: referrers) { 112 str.append(Utils.escapeReservedCharactersHTML(r.getDisplayName(DefaultNameFormatter.getInstance()))).append("<br>"); 113 } 114 str.append("</html>"); 115 return str.toString(); 116 } 117 118 protected void updateCoordinates() { 119 lblMyCoordinates.setText(coordToString(model.getMyCoords())); 120 lblMergedCoordinates.setText(coordToString(model.getMergedCoords())); 121 lblTheirCoordinates.setText(coordToString(model.getTheirCoords())); 122 if (!model.hasCoordConflict()) { 123 lblMyCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 124 lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 125 lblTheirCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 126 } else { 127 if (!model.isDecidedCoord()) { 128 lblMyCoordinates.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get()); 129 lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 130 lblTheirCoordinates.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get()); 131 } else { 132 lblMyCoordinates.setBackground( 133 model.isCoordMergeDecision(MergeDecisionType.KEEP_MINE) 134 ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get() 135 ); 136 lblMergedCoordinates.setBackground(ConflictColors.BGCOLOR_DECIDED.get()); 137 lblTheirCoordinates.setBackground( 138 model.isCoordMergeDecision(MergeDecisionType.KEEP_THEIR) 139 ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get() 140 ); 141 } 142 } 143 } 144 145 protected void updateDeletedState() { 146 lblMyDeletedState.setText(deletedStateToString(model.getMyDeletedState())); 147 lblMergedDeletedState.setText(deletedStateToString(model.getMergedDeletedState())); 148 lblTheirDeletedState.setText(deletedStateToString(model.getTheirDeletedState())); 149 150 if (!model.hasDeletedStateConflict()) { 151 lblMyDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 152 lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 153 lblTheirDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 154 } else { 155 if (!model.isDecidedDeletedState()) { 156 lblMyDeletedState.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get()); 157 lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 158 lblTheirDeletedState.setBackground(ConflictColors.BGCOLOR_UNDECIDED.get()); 159 } else { 160 lblMyDeletedState.setBackground( 161 model.isDeletedStateDecision(MergeDecisionType.KEEP_MINE) 162 ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get() 163 ); 164 lblMergedDeletedState.setBackground(ConflictColors.BGCOLOR_DECIDED.get()); 165 lblTheirDeletedState.setBackground( 166 model.isDeletedStateDecision(MergeDecisionType.KEEP_THEIR) 167 ? ConflictColors.BGCOLOR_DECIDED.get() : ConflictColors.BGCOLOR_NO_CONFLICT.get() 168 ); 169 } 170 } 171 } 172 173 protected void updateReferrers() { 174 lblMyReferrers.setText(referrersToString(model.getMyReferrers())); 175 lblMyReferrers.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 176 lblTheirReferrers.setText(referrersToString(model.getTheirReferrers())); 177 lblTheirReferrers.setBackground(ConflictColors.BGCOLOR_NO_CONFLICT.get()); 178 } 179 180 @Override 181 public void stateChanged(ChangeEvent e) { 182 updateCoordinates(); 183 updateDeletedState(); 184 updateReferrers(); 185 } 186 187 /** 188 * Returns properties merge model. 189 * @return properties merge model 190 */ 191 public PropertiesMergeModel getModel() { 192 return model; 193 } 194 195 private final class MergeDeletedStateRow extends AbstractMergePanel.MergeRow { 196 @Override 197 protected JComponent rowTitle() { 198 return new JLabel(tr("Deleted State:")); 199 } 200 201 @Override 202 protected JComponent mineField() { 203 return lblMyDeletedState; 204 } 205 206 @Override 207 protected JComponent mineButton() { 208 KeepMyDeletedStateAction actKeepMyDeletedState = new KeepMyDeletedStateAction(); 209 model.addChangeListener(actKeepMyDeletedState); 210 JButton btnKeepMyDeletedState = new JButton(actKeepMyDeletedState); 211 btnKeepMyDeletedState.setName("button.keepmydeletedstate"); 212 return btnKeepMyDeletedState; 213 } 214 215 @Override 216 protected JComponent merged() { 217 return lblMergedDeletedState; 218 } 219 220 @Override 221 protected JComponent theirsButton() { 222 KeepTheirDeletedStateAction actKeepTheirDeletedState = new KeepTheirDeletedStateAction(); 223 model.addChangeListener(actKeepTheirDeletedState); 224 JButton btnKeepTheirDeletedState = new JButton(actKeepTheirDeletedState); 225 btnKeepTheirDeletedState.setName("button.keeptheirdeletedstate"); 226 return btnKeepTheirDeletedState; 227 } 228 229 @Override 230 protected JComponent theirsField() { 231 return lblTheirDeletedState; 232 } 233 } 234 235 private final class MergeCoordinatesRow extends AbstractMergePanel.MergeRow { 236 @Override 237 protected JComponent rowTitle() { 238 return new JLabel(tr("Coordinates:")); 239 } 240 241 @Override 242 protected JComponent mineField() { 243 return lblMyCoordinates; 244 } 245 246 @Override 247 protected JComponent mineButton() { 248 KeepMyCoordinatesAction actKeepMyCoordinates = new KeepMyCoordinatesAction(); 249 model.addChangeListener(actKeepMyCoordinates); 250 JButton btnKeepMyCoordinates = new JButton(actKeepMyCoordinates); 251 btnKeepMyCoordinates.setName("button.keepmycoordinates"); 252 return btnKeepMyCoordinates; 253 } 254 255 @Override 256 protected JComponent merged() { 257 return lblMergedCoordinates; 258 } 259 260 @Override 261 protected JComponent theirsButton() { 262 KeepTheirCoordinatesAction actKeepTheirCoordinates = new KeepTheirCoordinatesAction(); 263 model.addChangeListener(actKeepTheirCoordinates); 264 JButton btnKeepTheirCoordinates = new JButton(actKeepTheirCoordinates); 265 btnKeepTheirCoordinates.setName("button.keeptheircoordinates"); 266 return btnKeepTheirCoordinates; 267 } 268 269 @Override 270 protected JComponent theirsField() { 271 return lblTheirCoordinates; 272 } 273 } 274 275 private final class UndecideCoordinatesRow extends AbstractUndecideRow { 276 @Override 277 protected UndecideCoordinateConflictAction createAction() { 278 UndecideCoordinateConflictAction action = new UndecideCoordinateConflictAction(); 279 model.addChangeListener(action); 280 return action; 281 } 282 283 @Override 284 protected String getButtonName() { 285 return "button.undecidecoordinates"; 286 } 287 } 288 289 private final class UndecideDeletedStateRow extends AbstractUndecideRow { 290 @Override 291 protected UndecideDeletedStateConflictAction createAction() { 292 UndecideDeletedStateConflictAction action = new UndecideDeletedStateConflictAction(); 293 model.addChangeListener(action); 294 return action; 295 } 296 297 @Override 298 protected String getButtonName() { 299 return "button.undecidedeletedstate"; 300 } 301 } 302 303 private final class VersionInfoRow extends AbstractMergePanel.MergeRowWithoutButton { 304 @Override 305 protected JComponent mineField() { 306 return mineVersionInfo; 307 } 308 309 @Override 310 protected JComponent theirsField() { 311 return theirVersionInfo; 312 } 313 } 314 315 private final class ReferrersRow extends AbstractMergePanel.MergeRow { 316 @Override 317 protected JComponent rowTitle() { 318 return new JLabel(tr("Referenced by:")); 319 } 320 321 @Override 322 protected JComponent mineField() { 323 return lblMyReferrers; 324 } 325 326 @Override 327 protected JComponent theirsField() { 328 return lblTheirReferrers; 329 } 330 } 331 332 private static final class EmptyFillRow extends AbstractMergePanel.MergeRow { 333 @Override 334 protected JComponent merged() { 335 return new JPanel(); 336 } 337 338 @Override 339 protected void addConstraints(GBC constraints, int columnIndex) { 340 super.addConstraints(constraints, columnIndex); 341 // fill to bottom 342 constraints.weighty = 1; 343 } 344 } 345 346 class KeepMyCoordinatesAction extends AbstractAction implements ChangeListener { 347 KeepMyCoordinatesAction() { 348 new ImageProvider("dialogs/conflict", "tagkeepmine").getResource().attachImageIcon(this, true); 349 putValue(Action.SHORT_DESCRIPTION, tr("Keep my coordinates")); 350 } 351 352 @Override 353 public void actionPerformed(ActionEvent e) { 354 model.decideCoordsConflict(MergeDecisionType.KEEP_MINE); 355 } 356 357 @Override 358 public void stateChanged(ChangeEvent e) { 359 setEnabled(model.hasCoordConflict() && !model.isDecidedCoord() && model.getMyCoords() != null); 360 } 361 } 362 363 class KeepTheirCoordinatesAction extends AbstractAction implements ChangeListener { 364 KeepTheirCoordinatesAction() { 365 new ImageProvider("dialogs/conflict", "tagkeeptheir").getResource().attachImageIcon(this, true); 366 putValue(Action.SHORT_DESCRIPTION, tr("Keep their coordinates")); 367 } 368 369 @Override 370 public void actionPerformed(ActionEvent e) { 371 model.decideCoordsConflict(MergeDecisionType.KEEP_THEIR); 372 } 373 374 @Override 375 public void stateChanged(ChangeEvent e) { 376 setEnabled(model.hasCoordConflict() && !model.isDecidedCoord() && model.getTheirCoords() != null); 377 } 378 } 379 380 class UndecideCoordinateConflictAction extends AbstractAction implements ChangeListener { 381 UndecideCoordinateConflictAction() { 382 new ImageProvider("dialogs/conflict", "tagundecide").getResource().attachImageIcon(this, true); 383 putValue(Action.SHORT_DESCRIPTION, tr("Undecide conflict between different coordinates")); 384 } 385 386 @Override 387 public void actionPerformed(ActionEvent e) { 388 model.decideCoordsConflict(MergeDecisionType.UNDECIDED); 389 } 390 391 @Override 392 public void stateChanged(ChangeEvent e) { 393 setEnabled(model.hasCoordConflict() && model.isDecidedCoord()); 394 } 395 } 396 397 class KeepMyDeletedStateAction extends AbstractAction implements ChangeListener { 398 KeepMyDeletedStateAction() { 399 new ImageProvider("dialogs/conflict", "tagkeepmine").getResource().attachImageIcon(this, true); 400 putValue(Action.SHORT_DESCRIPTION, tr("Keep my deleted state")); 401 } 402 403 @Override 404 public void actionPerformed(ActionEvent e) { 405 model.decideDeletedStateConflict(MergeDecisionType.KEEP_MINE); 406 } 407 408 @Override 409 public void stateChanged(ChangeEvent e) { 410 setEnabled(model.hasDeletedStateConflict() && !model.isDecidedDeletedState()); 411 } 412 } 413 414 class KeepTheirDeletedStateAction extends AbstractAction implements ChangeListener { 415 KeepTheirDeletedStateAction() { 416 new ImageProvider("dialogs/conflict", "tagkeeptheir").getResource().attachImageIcon(this, true); 417 putValue(Action.SHORT_DESCRIPTION, tr("Keep their deleted state")); 418 } 419 420 @Override 421 public void actionPerformed(ActionEvent e) { 422 model.decideDeletedStateConflict(MergeDecisionType.KEEP_THEIR); 423 } 424 425 @Override 426 public void stateChanged(ChangeEvent e) { 427 setEnabled(model.hasDeletedStateConflict() && !model.isDecidedDeletedState()); 428 } 429 } 430 431 class UndecideDeletedStateConflictAction extends AbstractAction implements ChangeListener { 432 UndecideDeletedStateConflictAction() { 433 new ImageProvider("dialogs/conflict", "tagundecide").getResource().attachImageIcon(this, true); 434 putValue(Action.SHORT_DESCRIPTION, tr("Undecide conflict between deleted state")); 435 } 436 437 @Override 438 public void actionPerformed(ActionEvent e) { 439 model.decideDeletedStateConflict(MergeDecisionType.UNDECIDED); 440 } 441 442 @Override 443 public void stateChanged(ChangeEvent e) { 444 setEnabled(model.hasDeletedStateConflict() && model.isDecidedDeletedState()); 445 } 446 } 447 448 @Override 449 public void deletePrimitive(boolean deleted) { 450 if (deleted) { 451 if (model.getMergedCoords() == null) { 452 model.decideCoordsConflict(MergeDecisionType.KEEP_MINE); 453 } 454 } else { 455 model.decideCoordsConflict(MergeDecisionType.UNDECIDED); 456 } 457 } 458 459 @Override 460 public void populate(Conflict<? extends OsmPrimitive> conflict) { 461 model.populate(conflict); 462 mineVersionInfo.update(conflict.getMy(), true); 463 theirVersionInfo.update(conflict.getTheir(), false); 464 } 465 466 @Override 467 public void decideRemaining(MergeDecisionType decision) { 468 if (!model.isDecidedCoord()) { 469 model.decideDeletedStateConflict(decision); 470 } 471 if (!model.isDecidedCoord()) { 472 model.decideCoordsConflict(decision); 473 } 474 } 475}