001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.conflict.pair.properties;
003    
004    import static org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType.UNDECIDED;
005    
006    import java.beans.PropertyChangeListener;
007    import java.beans.PropertyChangeSupport;
008    import java.util.ArrayList;
009    import java.util.Collections;
010    import java.util.List;
011    import java.util.Observable;
012    
013    import org.openstreetmap.josm.command.Command;
014    import org.openstreetmap.josm.command.CoordinateConflictResolveCommand;
015    import org.openstreetmap.josm.command.DeletedStateConflictResolveCommand;
016    import org.openstreetmap.josm.data.conflict.Conflict;
017    import org.openstreetmap.josm.data.coor.LatLon;
018    import org.openstreetmap.josm.data.osm.Node;
019    import org.openstreetmap.josm.data.osm.OsmPrimitive;
020    import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
021    import 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     */
039    public class PropertiesMergeModel extends Observable {
040    
041        static public final String RESOLVED_COMPLETELY_PROP = PropertiesMergeModel.class.getName() + ".resolvedCompletely";
042        static public 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        public PropertiesMergeModel() {
073            coordMergeDecision = UNDECIDED;
074            deletedMergeDecision = UNDECIDED;
075            support = new PropertyChangeSupport(this);
076            resolvedCompletely = null;
077        }
078    
079        /**
080         * replies true if there is a coordinate conflict and if this conflict is
081         * resolved
082         *
083         * @return true if there is a coordinate conflict and if this conflict is
084         * resolved; false, otherwise
085         */
086        public boolean isDecidedCoord() {
087            return ! coordMergeDecision.equals(UNDECIDED);
088        }
089    
090        /**
091         * replies true if there is a  conflict in the deleted state and if this conflict is
092         * 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         *
104         * @return true if the current decision for the coordinate conflict is <code>decision</code>;
105         *  false, otherwise
106         */
107        public boolean isCoordMergeDecision(MergeDecisionType decision) {
108            return coordMergeDecision.equals(decision);
109        }
110    
111        /**
112         * replies true if the current decision for the deleted state conflict is <code>decision</code>
113         *
114         * @return true if the current decision for the deleted state conflict is <code>decision</code>;
115         *  false, otherwise
116         */
117        public boolean isDeletedStateDecision(MergeDecisionType decision) {
118            return deletedMergeDecision.equals(decision);
119        }
120    
121        /**
122         * Populates the model with the differences between local and server version
123         *
124         * @param conflict The conflict information
125         */
126        public void populate(Conflict<? extends OsmPrimitive> conflict) {
127            this.my = conflict.getMy();
128            OsmPrimitive their = conflict.getTheir();
129            if (my instanceof Node) {
130                myCoords = ((Node)my).getCoor();
131                theirCoords = ((Node)their).getCoor();
132            } else {
133                myCoords = null;
134                theirCoords = null;
135            }
136    
137            myDeletedState =  conflict.isMyDeleted() || my.isDeleted();
138            theirDeletedState = their.isDeleted();
139    
140            myReferrers = my.getDataSet() == null?Collections.<OsmPrimitive>emptyList():my.getReferrers();
141            theirReferrers = their.getDataSet() == null?Collections.<OsmPrimitive>emptyList():their.getReferrers();
142    
143            coordMergeDecision = UNDECIDED;
144            deletedMergeDecision = UNDECIDED;
145            setChanged();
146            notifyObservers();
147            fireCompletelyResolved();
148        }
149    
150        /**
151         * replies the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't
152         * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}).
153         *
154         * @return the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't
155         *  coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}).
156         */
157        public LatLon getMyCoords() {
158            return myCoords;
159        }
160    
161        /**
162         * replies the coordinates of their {@link OsmPrimitive}. null, if their primitive hasn't
163         * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}).
164         *
165         * @return the coordinates of my {@link OsmPrimitive}. null, if my primitive hasn't
166         * coordinates (i.e. because it is a {@link org.openstreetmap.josm.data.osm.Way}).
167         */
168        public LatLon getTheirCoords() {
169            return theirCoords;
170        }
171    
172        /**
173         * replies the coordinates of the merged {@link OsmPrimitive}. null, if the current primitives
174         * have no coordinates or if the conflict is yet {@link MergeDecisionType#UNDECIDED}
175         *
176         * @return the coordinates of the merged {@link OsmPrimitive}. null, if the current primitives
177         * have no coordinates or if the conflict is yet {@link MergeDecisionType#UNDECIDED}
178         */
179        public LatLon getMergedCoords() {
180            switch(coordMergeDecision) {
181            case KEEP_MINE: return myCoords;
182            case KEEP_THEIR: return theirCoords;
183            case UNDECIDED: return null;
184            }
185            // should not happen
186            return null;
187        }
188    
189        /**
190         * Decides a conflict between local and server coordinates
191         *
192         * @param decision the decision
193         */
194        public void decideCoordsConflict(MergeDecisionType decision) {
195            coordMergeDecision = decision;
196            setChanged();
197            notifyObservers();
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 thrown, if decision is null
263         */
264        public void decideDeletedStateConflict(MergeDecisionType decision) throws IllegalArgumentException{
265            CheckParameterUtil.ensureParameterNotNull(decision, "decision");
266    
267            boolean oldMergedDeletedState = getMergedDeletedState(this.deletedMergeDecision);
268            boolean newMergedDeletedState = getMergedDeletedState(decision);
269    
270            this.deletedMergeDecision = decision;
271            setChanged();
272            notifyObservers();
273            fireCompletelyResolved();
274    
275            if (oldMergedDeletedState != newMergedDeletedState) {
276                support.firePropertyChange(DELETE_PRIMITIVE_PROP, oldMergedDeletedState, newMergedDeletedState);
277            }
278        }
279    
280        /**
281         * replies true if my and their primitive have a conflict between
282         * their coordinate values
283         *
284         * @return true if my and their primitive have a conflict between
285         * their coordinate values; false otherwise
286         */
287        public boolean hasCoordConflict() {
288            if (myCoords == null && theirCoords != null) return true;
289            if (myCoords != null && theirCoords == null) return true;
290            if (myCoords == null && theirCoords == null) return false;
291            return !myCoords.equalsEpsilon(theirCoords);
292        }
293    
294        /**
295         * replies true if my and their primitive have a conflict between
296         * their deleted states
297         *
298         * @return <code>true</code> if my and their primitive have a conflict between
299         * their deleted states
300         */
301        public boolean hasDeletedStateConflict() {
302            return myDeletedState != theirDeletedState;
303        }
304    
305        /**
306         * replies true if all conflict in this model are resolved
307         *
308         * @return <code>true</code> if all conflict in this model are resolved; <code>false</code> otherwise
309         */
310        public boolean isResolvedCompletely() {
311            boolean ret = true;
312            if (hasCoordConflict()) {
313                ret = ret && ! coordMergeDecision.equals(UNDECIDED);
314            }
315            if (hasDeletedStateConflict()) {
316                ret = ret && ! deletedMergeDecision.equals(UNDECIDED);
317            }
318            return ret;
319        }
320    
321        /**
322         * Builds the command(s) to apply the conflict resolutions to my primitive
323         *
324         * @param conflict The conflict information
325         * @return The list of commands
326         */
327        public List<Command> buildResolveCommand(Conflict<? extends OsmPrimitive> conflict) {
328            List<Command> cmds = new ArrayList<Command>();
329            if (hasCoordConflict() && isDecidedCoord()) {
330                cmds.add(new CoordinateConflictResolveCommand(conflict, coordMergeDecision));
331            }
332            if (hasDeletedStateConflict() && isDecidedDeletedState()) {
333                cmds.add(new DeletedStateConflictResolveCommand(conflict, deletedMergeDecision));
334            }
335            return cmds;
336        }
337    
338        public OsmPrimitive getMyPrimitive() {
339            return my;
340        }
341    
342    }