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; 011import java.util.Observable; 012 013import org.openstreetmap.josm.command.Command; 014import org.openstreetmap.josm.command.conflict.CoordinateConflictResolveCommand; 015import org.openstreetmap.josm.command.conflict.DeletedStateConflictResolveCommand; 016import org.openstreetmap.josm.data.conflict.Conflict; 017import org.openstreetmap.josm.data.coor.LatLon; 018import org.openstreetmap.josm.data.osm.Node; 019import org.openstreetmap.josm.data.osm.OsmPrimitive; 020import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType; 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 an {@link Observable}. It notifies registered {@link java.util.Observer}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 Observable { 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 setChanged(); 148 notifyObservers(); 149 fireCompletelyResolved(); 150 } 151 152 /** 153 * replies the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't 154 * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}). 155 * 156 * @return the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't 157 * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}). 158 */ 159 public LatLon getMyCoords() { 160 return myCoords; 161 } 162 163 /** 164 * replies the coordinates of their {@link OsmPrimitive}. null, if their primitive hasn't 165 * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}). 166 * 167 * @return the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't 168 * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}). 169 */ 170 public LatLon getTheirCoords() { 171 return theirCoords; 172 } 173 174 /** 175 * replies the coordinates of the merged {@link OsmPrimitive}. null, if the current primitives 176 * have no coordinates or if the conflict is yet {@link MergeDecisionType#UNDECIDED} 177 * 178 * @return the coordinates of the merged {@link OsmPrimitive}. null, if the current primitives 179 * have no coordinates or if the conflict is yet {@link MergeDecisionType#UNDECIDED} 180 */ 181 public LatLon getMergedCoords() { 182 switch(coordMergeDecision) { 183 case KEEP_MINE: return myCoords; 184 case KEEP_THEIR: return theirCoords; 185 case UNDECIDED: return null; 186 } 187 // should not happen 188 return null; 189 } 190 191 /** 192 * Decides a conflict between local and server coordinates 193 * 194 * @param decision the decision 195 */ 196 public void decideCoordsConflict(MergeDecisionType decision) { 197 coordMergeDecision = decision; 198 setChanged(); 199 notifyObservers(); 200 fireCompletelyResolved(); 201 } 202 203 /** 204 * Replies deleted state of local dataset 205 * @return The state of deleted flag 206 */ 207 public Boolean getMyDeletedState() { 208 return myDeletedState; 209 } 210 211 /** 212 * Replies deleted state of Server dataset 213 * @return The state of deleted flag 214 */ 215 public Boolean getTheirDeletedState() { 216 return theirDeletedState; 217 } 218 219 /** 220 * Replies deleted state of combined dataset 221 * @return The state of deleted flag 222 */ 223 public Boolean getMergedDeletedState() { 224 switch(deletedMergeDecision) { 225 case KEEP_MINE: return myDeletedState; 226 case KEEP_THEIR: return theirDeletedState; 227 case UNDECIDED: return null; 228 } 229 // should not happen 230 return null; 231 } 232 233 /** 234 * Returns local referrers 235 * @return The referrers 236 */ 237 public List<OsmPrimitive> getMyReferrers() { 238 return myReferrers; 239 } 240 241 /** 242 * Returns server referrers 243 * @return The referrers 244 */ 245 public List<OsmPrimitive> getTheirReferrers() { 246 return theirReferrers; 247 } 248 249 private boolean getMergedDeletedState(MergeDecisionType decision) { 250 switch (decision) { 251 case KEEP_MINE: 252 return myDeletedState; 253 case KEEP_THEIR: 254 return theirDeletedState; 255 default: 256 return false; 257 } 258 } 259 260 /** 261 * decides the conflict between two deleted states 262 * @param decision the decision (must not be null) 263 * 264 * @throws IllegalArgumentException if decision is null 265 */ 266 public void decideDeletedStateConflict(MergeDecisionType decision) { 267 CheckParameterUtil.ensureParameterNotNull(decision, "decision"); 268 269 boolean oldMergedDeletedState = getMergedDeletedState(this.deletedMergeDecision); 270 boolean newMergedDeletedState = getMergedDeletedState(decision); 271 272 this.deletedMergeDecision = decision; 273 setChanged(); 274 notifyObservers(); 275 fireCompletelyResolved(); 276 277 if (oldMergedDeletedState != newMergedDeletedState) { 278 support.firePropertyChange(DELETE_PRIMITIVE_PROP, oldMergedDeletedState, newMergedDeletedState); 279 } 280 } 281 282 /** 283 * replies true if my and their primitive have a conflict between 284 * their coordinate values 285 * 286 * @return true if my and their primitive have a conflict between 287 * their coordinate values; false otherwise 288 */ 289 public boolean hasCoordConflict() { 290 if (myCoords == null && theirCoords != null) return true; 291 if (myCoords != null && theirCoords == null) return true; 292 if (myCoords == null && theirCoords == null) return false; 293 return myCoords != null && !myCoords.equalsEpsilon(theirCoords); 294 } 295 296 /** 297 * replies true if my and their primitive have a conflict between 298 * their deleted states 299 * 300 * @return <code>true</code> if my and their primitive have a conflict between 301 * their deleted states 302 */ 303 public boolean hasDeletedStateConflict() { 304 return myDeletedState != theirDeletedState; 305 } 306 307 /** 308 * replies true if all conflict in this model are resolved 309 * 310 * @return <code>true</code> if all conflict in this model are resolved; <code>false</code> otherwise 311 */ 312 public boolean isResolvedCompletely() { 313 boolean ret = true; 314 if (hasCoordConflict()) { 315 ret = ret && !coordMergeDecision.equals(UNDECIDED); 316 } 317 if (hasDeletedStateConflict()) { 318 ret = ret && !deletedMergeDecision.equals(UNDECIDED); 319 } 320 return ret; 321 } 322 323 /** 324 * Builds the command(s) to apply the conflict resolutions to my primitive 325 * 326 * @param conflict The conflict information 327 * @return The list of commands 328 */ 329 public List<Command> buildResolveCommand(Conflict<? extends OsmPrimitive> conflict) { 330 List<Command> cmds = new ArrayList<>(); 331 if (hasCoordConflict() && isDecidedCoord()) { 332 cmds.add(new CoordinateConflictResolveCommand(conflict, coordMergeDecision)); 333 } 334 if (hasDeletedStateConflict() && isDecidedDeletedState()) { 335 cmds.add(new DeletedStateConflictResolveCommand(conflict, deletedMergeDecision)); 336 } 337 return cmds; 338 } 339 340 public OsmPrimitive getMyPrimitive() { 341 return my; 342 } 343 344}