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