001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.conflict.pair.properties; 003 004import static org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType.UNDECIDED; 005 006import java.beans.PropertyChangeListener; 007import java.beans.PropertyChangeSupport; 008import java.util.ArrayList; 009import java.util.Collections; 010import java.util.List; 011 012import org.openstreetmap.josm.command.Command; 013import org.openstreetmap.josm.command.conflict.CoordinateConflictResolveCommand; 014import org.openstreetmap.josm.command.conflict.DeletedStateConflictResolveCommand; 015import org.openstreetmap.josm.data.conflict.Conflict; 016import org.openstreetmap.josm.data.coor.LatLon; 017import org.openstreetmap.josm.data.osm.Node; 018import org.openstreetmap.josm.data.osm.OsmPrimitive; 019import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType; 020import org.openstreetmap.josm.gui.util.ChangeNotifier; 021import org.openstreetmap.josm.tools.CheckParameterUtil; 022 023/** 024 * This is the model for resolving conflicts in the properties of the 025 * {@link OsmPrimitive}s. In particular, it represents conflicts in the coordinates of {@link Node}s and 026 * the deleted or visible state of {@link OsmPrimitive}s. 027 * 028 * This model is a {@link ChangeNotifier}. It notifies registered {@link javax.swing.event.ChangeListener}s whenever the 029 * internal state changes. 030 * 031 * This model also emits property changes for {@link #RESOLVED_COMPLETELY_PROP}. Property change 032 * listeners may register themselves using {@link #addPropertyChangeListener(PropertyChangeListener)}. 033 * 034 * @see Node#getCoor() 035 * @see OsmPrimitive#isDeleted 036 * @see OsmPrimitive#isVisible 037 * 038 */ 039public class PropertiesMergeModel extends ChangeNotifier { 040 041 public static final String RESOLVED_COMPLETELY_PROP = PropertiesMergeModel.class.getName() + ".resolvedCompletely"; 042 public static final String DELETE_PRIMITIVE_PROP = PropertiesMergeModel.class.getName() + ".deletePrimitive"; 043 044 private OsmPrimitive my; 045 046 private LatLon myCoords; 047 private LatLon theirCoords; 048 private MergeDecisionType coordMergeDecision; 049 050 private boolean myDeletedState; 051 private boolean theirDeletedState; 052 private List<OsmPrimitive> myReferrers; 053 private List<OsmPrimitive> theirReferrers; 054 private MergeDecisionType deletedMergeDecision; 055 private final PropertyChangeSupport support; 056 private Boolean resolvedCompletely; 057 058 public void addPropertyChangeListener(PropertyChangeListener listener) { 059 support.addPropertyChangeListener(listener); 060 } 061 062 public void removePropertyChangeListener(PropertyChangeListener listener) { 063 support.removePropertyChangeListener(listener); 064 } 065 066 public void fireCompletelyResolved() { 067 Boolean oldValue = resolvedCompletely; 068 resolvedCompletely = isResolvedCompletely(); 069 support.firePropertyChange(RESOLVED_COMPLETELY_PROP, oldValue, resolvedCompletely); 070 } 071 072 /** 073 * Constructs a new {@code PropertiesMergeModel}. 074 */ 075 public PropertiesMergeModel() { 076 coordMergeDecision = UNDECIDED; 077 deletedMergeDecision = UNDECIDED; 078 support = new PropertyChangeSupport(this); 079 resolvedCompletely = null; 080 } 081 082 /** 083 * replies true if there is a coordinate conflict and if this conflict is resolved 084 * 085 * @return true if there is a coordinate conflict and if this conflict is resolved; false, otherwise 086 */ 087 public boolean isDecidedCoord() { 088 return !coordMergeDecision.equals(UNDECIDED); 089 } 090 091 /** 092 * replies true if there is a conflict in the deleted state and if this conflict is resolved 093 * 094 * @return true if there is a conflict in the deleted state and if this conflict is 095 * resolved; false, otherwise 096 */ 097 public boolean isDecidedDeletedState() { 098 return !deletedMergeDecision.equals(UNDECIDED); 099 } 100 101 /** 102 * replies true if the current decision for the coordinate conflict is <code>decision</code> 103 * @param decision conflict resolution decision 104 * 105 * @return true if the current decision for the coordinate conflict is <code>decision</code>; 106 * false, otherwise 107 */ 108 public boolean isCoordMergeDecision(MergeDecisionType decision) { 109 return coordMergeDecision.equals(decision); 110 } 111 112 /** 113 * replies true if the current decision for the deleted state conflict is <code>decision</code> 114 * @param decision conflict resolution decision 115 * 116 * @return true if the current decision for the deleted state conflict is <code>decision</code>; 117 * false, otherwise 118 */ 119 public boolean isDeletedStateDecision(MergeDecisionType decision) { 120 return deletedMergeDecision.equals(decision); 121 } 122 123 /** 124 * Populates the model with the differences between local and server version 125 * 126 * @param conflict The conflict information 127 */ 128 public void populate(Conflict<? extends OsmPrimitive> conflict) { 129 this.my = conflict.getMy(); 130 OsmPrimitive their = conflict.getTheir(); 131 if (my instanceof Node) { 132 myCoords = ((Node) my).getCoor(); 133 theirCoords = ((Node) their).getCoor(); 134 } else { 135 myCoords = null; 136 theirCoords = null; 137 } 138 139 myDeletedState = conflict.isMyDeleted() || my.isDeleted(); 140 theirDeletedState = their.isDeleted(); 141 142 myReferrers = my.getDataSet() == null ? Collections.<OsmPrimitive>emptyList() : my.getReferrers(); 143 theirReferrers = their.getDataSet() == null ? Collections.<OsmPrimitive>emptyList() : their.getReferrers(); 144 145 coordMergeDecision = UNDECIDED; 146 deletedMergeDecision = UNDECIDED; 147 fireStateChanged(); 148 fireCompletelyResolved(); 149 } 150 151 /** 152 * replies the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't 153 * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}). 154 * 155 * @return the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't 156 * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}). 157 */ 158 public LatLon getMyCoords() { 159 return myCoords; 160 } 161 162 /** 163 * replies the coordinates of their {@link OsmPrimitive}. null, if their primitive hasn't 164 * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}). 165 * 166 * @return the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't 167 * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}). 168 */ 169 public LatLon getTheirCoords() { 170 return theirCoords; 171 } 172 173 /** 174 * replies the coordinates of the merged {@link OsmPrimitive}. null, if the current primitives 175 * have no coordinates or if the conflict is yet {@link MergeDecisionType#UNDECIDED} 176 * 177 * @return the coordinates of the merged {@link OsmPrimitive}. null, if the current primitives 178 * have no coordinates or if the conflict is yet {@link MergeDecisionType#UNDECIDED} 179 */ 180 public LatLon getMergedCoords() { 181 switch(coordMergeDecision) { 182 case KEEP_MINE: return myCoords; 183 case KEEP_THEIR: return theirCoords; 184 case UNDECIDED: return null; 185 } 186 // should not happen 187 return null; 188 } 189 190 /** 191 * Decides a conflict between local and server coordinates 192 * 193 * @param decision the decision 194 */ 195 public void decideCoordsConflict(MergeDecisionType decision) { 196 coordMergeDecision = decision; 197 fireStateChanged(); 198 fireCompletelyResolved(); 199 } 200 201 /** 202 * Replies deleted state of local dataset 203 * @return The state of deleted flag 204 */ 205 public Boolean getMyDeletedState() { 206 return myDeletedState; 207 } 208 209 /** 210 * Replies deleted state of Server dataset 211 * @return The state of deleted flag 212 */ 213 public Boolean getTheirDeletedState() { 214 return theirDeletedState; 215 } 216 217 /** 218 * Replies deleted state of combined dataset 219 * @return The state of deleted flag 220 */ 221 public Boolean getMergedDeletedState() { 222 switch(deletedMergeDecision) { 223 case KEEP_MINE: return myDeletedState; 224 case KEEP_THEIR: return theirDeletedState; 225 case UNDECIDED: return null; 226 } 227 // should not happen 228 return null; 229 } 230 231 /** 232 * Returns local referrers 233 * @return The referrers 234 */ 235 public List<OsmPrimitive> getMyReferrers() { 236 return myReferrers; 237 } 238 239 /** 240 * Returns server referrers 241 * @return The referrers 242 */ 243 public List<OsmPrimitive> getTheirReferrers() { 244 return theirReferrers; 245 } 246 247 private boolean getMergedDeletedState(MergeDecisionType decision) { 248 switch (decision) { 249 case KEEP_MINE: 250 return myDeletedState; 251 case KEEP_THEIR: 252 return theirDeletedState; 253 default: 254 return false; 255 } 256 } 257 258 /** 259 * decides the conflict between two deleted states 260 * @param decision the decision (must not be null) 261 * 262 * @throws IllegalArgumentException if decision is null 263 */ 264 public void decideDeletedStateConflict(MergeDecisionType decision) { 265 CheckParameterUtil.ensureParameterNotNull(decision, "decision"); 266 267 boolean oldMergedDeletedState = getMergedDeletedState(this.deletedMergeDecision); 268 boolean newMergedDeletedState = getMergedDeletedState(decision); 269 270 this.deletedMergeDecision = decision; 271 fireStateChanged(); 272 fireCompletelyResolved(); 273 274 if (oldMergedDeletedState != newMergedDeletedState) { 275 support.firePropertyChange(DELETE_PRIMITIVE_PROP, oldMergedDeletedState, newMergedDeletedState); 276 } 277 } 278 279 /** 280 * replies true if my and their primitive have a conflict between 281 * their coordinate values 282 * 283 * @return true if my and their primitive have a conflict between 284 * their coordinate values; false otherwise 285 */ 286 public boolean hasCoordConflict() { 287 if (myCoords == null && theirCoords != null) return true; 288 if (myCoords != null && theirCoords == null) return true; 289 if (myCoords == null && theirCoords == null) return false; 290 return myCoords != null && !myCoords.equalsEpsilon(theirCoords); 291 } 292 293 /** 294 * replies true if my and their primitive have a conflict between 295 * their deleted states 296 * 297 * @return <code>true</code> if my and their primitive have a conflict between 298 * their deleted states 299 */ 300 public boolean hasDeletedStateConflict() { 301 return myDeletedState != theirDeletedState; 302 } 303 304 /** 305 * replies true if all conflict in this model are resolved 306 * 307 * @return <code>true</code> if all conflict in this model are resolved; <code>false</code> otherwise 308 */ 309 public boolean isResolvedCompletely() { 310 boolean ret = true; 311 if (hasCoordConflict()) { 312 ret = ret && !coordMergeDecision.equals(UNDECIDED); 313 } 314 if (hasDeletedStateConflict()) { 315 ret = ret && !deletedMergeDecision.equals(UNDECIDED); 316 } 317 return ret; 318 } 319 320 /** 321 * Builds the command(s) to apply the conflict resolutions to my primitive 322 * 323 * @param conflict The conflict information 324 * @return The list of commands 325 */ 326 public List<Command> buildResolveCommand(Conflict<? extends OsmPrimitive> conflict) { 327 List<Command> cmds = new ArrayList<>(); 328 if (hasCoordConflict() && isDecidedCoord()) { 329 cmds.add(new CoordinateConflictResolveCommand(conflict, coordMergeDecision)); 330 } 331 if (hasDeletedStateConflict() && isDecidedDeletedState()) { 332 cmds.add(new DeletedStateConflictResolveCommand(conflict, deletedMergeDecision)); 333 } 334 return cmds; 335 } 336 337 public OsmPrimitive getMyPrimitive() { 338 return my; 339 } 340 341}