// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.conflict.pair.properties;
import static org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType.UNDECIDED;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.conflict.CoordinateConflictResolveCommand;
import org.openstreetmap.josm.command.conflict.DeletedStateConflictResolveCommand;
import org.openstreetmap.josm.data.conflict.Conflict;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
import org.openstreetmap.josm.gui.util.ChangeNotifier;
import org.openstreetmap.josm.tools.CheckParameterUtil;
/**
* This is the model for resolving conflicts in the properties of the
* {@link OsmPrimitive}s. In particular, it represents conflicts in the coordinates of {@link Node}s and
* the deleted or visible state of {@link OsmPrimitive}s.
*
* This model is a {@link ChangeNotifier}. It notifies registered {@link javax.swing.event.ChangeListener}s whenever the
* internal state changes.
*
* This model also emits property changes for {@link #RESOLVED_COMPLETELY_PROP}. Property change
* listeners may register themselves using {@link #addPropertyChangeListener(PropertyChangeListener)}.
*
* @see Node#getCoor()
* @see OsmPrimitive#isDeleted
* @see OsmPrimitive#isVisible
*
*/
public class PropertiesMergeModel extends ChangeNotifier {
public static final String RESOLVED_COMPLETELY_PROP = PropertiesMergeModel.class.getName() + ".resolvedCompletely";
public static final String DELETE_PRIMITIVE_PROP = PropertiesMergeModel.class.getName() + ".deletePrimitive";
private OsmPrimitive my;
private LatLon myCoords;
private LatLon theirCoords;
private MergeDecisionType coordMergeDecision;
private boolean myDeletedState;
private boolean theirDeletedState;
private List<OsmPrimitive> myReferrers;
private List<OsmPrimitive> theirReferrers;
private MergeDecisionType deletedMergeDecision;
private final PropertyChangeSupport support;
private Boolean resolvedCompletely;
public void addPropertyChangeListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
support.removePropertyChangeListener(listener);
}
public void fireCompletelyResolved() {
Boolean oldValue = resolvedCompletely;
resolvedCompletely = isResolvedCompletely();
support.firePropertyChange(RESOLVED_COMPLETELY_PROP, oldValue, resolvedCompletely);
}
/**
* Constructs a new {@code PropertiesMergeModel}.
*/
public PropertiesMergeModel() {
coordMergeDecision = UNDECIDED;
deletedMergeDecision = UNDECIDED;
support = new PropertyChangeSupport(this);
resolvedCompletely = null;
}
/**
* replies true if there is a coordinate conflict and if this conflict is resolved
*
* @return true if there is a coordinate conflict and if this conflict is resolved; false, otherwise
*/
public boolean isDecidedCoord() {
return !coordMergeDecision.equals(UNDECIDED);
}
/**
* replies true if there is a conflict in the deleted state and if this conflict is resolved
*
* @return true if there is a conflict in the deleted state and if this conflict is
* resolved; false, otherwise
*/
public boolean isDecidedDeletedState() {
return !deletedMergeDecision.equals(UNDECIDED);
}
/**
* replies true if the current decision for the coordinate conflict is <code>decision</code>
* @param decision conflict resolution decision
*
* @return true if the current decision for the coordinate conflict is <code>decision</code>;
* false, otherwise
*/
public boolean isCoordMergeDecision(MergeDecisionType decision) {
return coordMergeDecision.equals(decision);
}
/**
* replies true if the current decision for the deleted state conflict is <code>decision</code>
* @param decision conflict resolution decision
*
* @return true if the current decision for the deleted state conflict is <code>decision</code>;
* false, otherwise
*/
public boolean isDeletedStateDecision(MergeDecisionType decision) {
return deletedMergeDecision.equals(decision);
}
/**
* Populates the model with the differences between local and server version
*
* @param conflict The conflict information
*/
public void populate(Conflict<? extends OsmPrimitive> conflict) {
this.my = conflict.getMy();
OsmPrimitive their = conflict.getTheir();
if (my instanceof Node) {
myCoords = ((Node) my).getCoor();
theirCoords = ((Node) their).getCoor();
} else {
myCoords = null;
theirCoords = null;
}
myDeletedState = conflict.isMyDeleted() || my.isDeleted();
theirDeletedState = their.isDeleted();
myReferrers = my.getDataSet() == null ? Collections.<OsmPrimitive>emptyList() : my.getReferrers();
theirReferrers = their.getDataSet() == null ? Collections.<OsmPrimitive>emptyList() : their.getReferrers();
coordMergeDecision = UNDECIDED;
deletedMergeDecision = UNDECIDED;
fireStateChanged();
fireCompletelyResolved();
}
/**
* replies the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't
* coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}).
*
* @return the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't
* coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}).
*/
public LatLon getMyCoords() {
return myCoords;
}
/**
* replies the coordinates of their {@link OsmPrimitive}. null, if their primitive hasn't
* coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}).
*
* @return the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't
* coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}).
*/
public LatLon getTheirCoords() {
return theirCoords;
}
/**
* replies the coordinates of the merged {@link OsmPrimitive}. null, if the current primitives
* have no coordinates or if the conflict is yet {@link MergeDecisionType#UNDECIDED}
*
* @return the coordinates of the merged {@link OsmPrimitive}. null, if the current primitives
* have no coordinates or if the conflict is yet {@link MergeDecisionType#UNDECIDED}
*/
public LatLon getMergedCoords() {
switch(coordMergeDecision) {
case KEEP_MINE: return myCoords;
case KEEP_THEIR: return theirCoords;
case UNDECIDED: return null;
}
// should not happen
return null;
}
/**
* Decides a conflict between local and server coordinates
*
* @param decision the decision
*/
public void decideCoordsConflict(MergeDecisionType decision) {
coordMergeDecision = decision;
fireStateChanged();
fireCompletelyResolved();
}
/**
* Replies deleted state of local dataset
* @return The state of deleted flag
*/
public Boolean getMyDeletedState() {
return myDeletedState;
}
/**
* Replies deleted state of Server dataset
* @return The state of deleted flag
*/
public Boolean getTheirDeletedState() {
return theirDeletedState;
}
/**
* Replies deleted state of combined dataset
* @return The state of deleted flag
*/
public Boolean getMergedDeletedState() {
switch(deletedMergeDecision) {
case KEEP_MINE: return myDeletedState;
case KEEP_THEIR: return theirDeletedState;
case UNDECIDED: return null;
}
// should not happen
return null;
}
/**
* Returns local referrers
* @return The referrers
*/
public List<OsmPrimitive> getMyReferrers() {
return myReferrers;
}
/**
* Returns server referrers
* @return The referrers
*/
public List<OsmPrimitive> getTheirReferrers() {
return theirReferrers;
}
private boolean getMergedDeletedState(MergeDecisionType decision) {
switch (decision) {
case KEEP_MINE:
return myDeletedState;
case KEEP_THEIR:
return theirDeletedState;
default:
return false;
}
}
/**
* decides the conflict between two deleted states
* @param decision the decision (must not be null)
*
* @throws IllegalArgumentException if decision is null
*/
public void decideDeletedStateConflict(MergeDecisionType decision) {
CheckParameterUtil.ensureParameterNotNull(decision, "decision");
boolean oldMergedDeletedState = getMergedDeletedState(this.deletedMergeDecision);
boolean newMergedDeletedState = getMergedDeletedState(decision);
this.deletedMergeDecision = decision;
fireStateChanged();
fireCompletelyResolved();
if (oldMergedDeletedState != newMergedDeletedState) {
support.firePropertyChange(DELETE_PRIMITIVE_PROP, oldMergedDeletedState, newMergedDeletedState);
}
}
/**
* replies true if my and their primitive have a conflict between
* their coordinate values
*
* @return true if my and their primitive have a conflict between
* their coordinate values; false otherwise
*/
public boolean hasCoordConflict() {
if (myCoords == null && theirCoords != null) return true;
if (myCoords != null && theirCoords == null) return true;
if (myCoords == null && theirCoords == null) return false;
return myCoords != null && !myCoords.equalsEpsilon(theirCoords);
}
/**
* replies true if my and their primitive have a conflict between
* their deleted states
*
* @return <code>true</code> if my and their primitive have a conflict between
* their deleted states
*/
public boolean hasDeletedStateConflict() {
return myDeletedState != theirDeletedState;
}
/**
* replies true if all conflict in this model are resolved
*
* @return <code>true</code> if all conflict in this model are resolved; <code>false</code> otherwise
*/
public boolean isResolvedCompletely() {
boolean ret = true;
if (hasCoordConflict()) {
ret = ret && !coordMergeDecision.equals(UNDECIDED);
}
if (hasDeletedStateConflict()) {
ret = ret && !deletedMergeDecision.equals(UNDECIDED);
}
return ret;
}
/**
* Builds the command(s) to apply the conflict resolutions to my primitive
*
* @param conflict The conflict information
* @return The list of commands
*/
public List<Command> buildResolveCommand(Conflict<? extends OsmPrimitive> conflict) {
List<Command> cmds = new ArrayList<>();
if (hasCoordConflict() && isDecidedCoord()) {
cmds.add(new CoordinateConflictResolveCommand(conflict, coordMergeDecision));
}
if (hasDeletedStateConflict() && isDecidedDeletedState()) {
cmds.add(new DeletedStateConflictResolveCommand(conflict, deletedMergeDecision));
}
return cmds;
}
public OsmPrimitive getMyPrimitive() {
return my;
}
}