001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.conflict.pair.tags;
003    
004    import java.beans.PropertyChangeEvent;
005    import java.beans.PropertyChangeListener;
006    import java.util.ArrayList;
007    import java.util.HashSet;
008    import java.util.List;
009    import java.util.Set;
010    
011    import javax.swing.table.DefaultTableModel;
012    
013    import org.openstreetmap.josm.command.TagConflictResolveCommand;
014    import org.openstreetmap.josm.data.conflict.Conflict;
015    import org.openstreetmap.josm.data.osm.OsmPrimitive;
016    import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
017    
018    /**
019     * This is the {@link TableModel} used in the tables of the {@link TagMerger}.
020     *
021     * The model can {@link #populate(OsmPrimitive, OsmPrimitive)} itself from the conflicts
022     * in the tag sets of two {@link OsmPrimitive}s. Internally, it keeps a list of {@link TagMergeItem}s.
023     *
024     *  {@link #decide(int, MergeDecisionType)} and {@link #decide(int[], MergeDecisionType)} can be used
025     *  to remember a merge decision for a specific row in the model.
026     *
027     *  The model notifies {@link PropertyChangeListener}s about updates of the number of
028     *  undecided tags (see {@link #PROP_NUM_UNDECIDED_TAGS}).
029     *
030     */
031    public class TagMergeModel extends DefaultTableModel {
032        static public final String PROP_NUM_UNDECIDED_TAGS = TagMergeModel.class.getName() + ".numUndecidedTags";
033    
034        /** the list of tag merge items */
035        private final List<TagMergeItem> tagMergeItems;
036    
037        /** the property change listeners */
038        private final List<PropertyChangeListener> listeners;
039    
040        private int numUndecidedTags = 0;
041    
042        public TagMergeModel() {
043            tagMergeItems = new ArrayList<TagMergeItem>();
044            listeners = new ArrayList<PropertyChangeListener>();
045        }
046    
047        public void addPropertyChangeListener(PropertyChangeListener listener) {
048            synchronized(listeners) {
049                if (listener == null) return;
050                if (listeners.contains(listener)) return;
051                listeners.add(listener);
052            }
053        }
054    
055        public void removePropertyChangeListener(PropertyChangeListener listener) {
056            synchronized(listeners) {
057                if (listener == null) return;
058                if (!listeners.contains(listener)) return;
059                listeners.remove(listener);
060            }
061        }
062    
063        /**
064         * notifies {@link PropertyChangeListener}s about an update of {@link TagMergeModel#PROP_NUM_UNDECIDED_TAGS}
065    
066         * @param oldValue the old value
067         * @param newValue the new value
068         */
069        protected void fireNumUndecidedTagsChanged(int oldValue, int newValue) {
070            PropertyChangeEvent evt = new PropertyChangeEvent(this,PROP_NUM_UNDECIDED_TAGS,oldValue, newValue);
071            synchronized(listeners) {
072                for(PropertyChangeListener l : listeners) {
073                    l.propertyChange(evt);
074                }
075            }
076        }
077    
078        /**
079         * refreshes the number of undecided tag conflicts after an update in the list of
080         * {@link TagMergeItem}s. Notifies {@link PropertyChangeListener} if necessary.
081         *
082         */
083        protected void refreshNumUndecidedTags() {
084            int newValue=0;
085            for(TagMergeItem item: tagMergeItems) {
086                if (MergeDecisionType.UNDECIDED.equals(item.getMergeDecision())) {
087                    newValue++;
088                }
089            }
090            int oldValue = numUndecidedTags;
091            numUndecidedTags = newValue;
092            fireNumUndecidedTagsChanged(oldValue, numUndecidedTags);
093    
094        }
095    
096        /**
097         * Populate the model with conflicts between the tag sets of the two
098         * {@link OsmPrimitive} <code>my</code> and <code>their</code>.
099         *
100         * @param my  my primitive (i.e. the primitive from the local dataset)
101         * @param their their primitive (i.e. the primitive from the server dataset)
102         *
103         */
104        public void populate(OsmPrimitive my, OsmPrimitive their) {
105            tagMergeItems.clear();
106            Set<String> keys = new HashSet<String>();
107            keys.addAll(my.keySet());
108            keys.addAll(their.keySet());
109            for(String key : keys) {
110                String myValue = my.get(key);
111                String theirValue = their.get(key);
112                if (myValue == null || theirValue == null || ! myValue.equals(theirValue)) {
113                    tagMergeItems.add(
114                            new TagMergeItem(key, my, their)
115                    );
116                }
117            }
118            fireTableDataChanged();
119            refreshNumUndecidedTags();
120        }
121    
122        /**
123         * add a {@link TagMergeItem} to the model
124         *
125         * @param item the item
126         */
127        public void addItem(TagMergeItem item) {
128            if (item != null) {
129                tagMergeItems.add(item);
130                fireTableDataChanged();
131                refreshNumUndecidedTags();
132            }
133        }
134    
135        protected void rememberDecision(int row, MergeDecisionType decision) {
136            TagMergeItem item = tagMergeItems.get(row);
137            item.decide(decision);
138        }
139    
140        /**
141         * set the merge decision of the {@link TagMergeItem} in row <code>row</code>
142         * to <code>decision</code>.
143         *
144         * @param row  the row
145         * @param decision the decision
146         */
147        public void decide(int row, MergeDecisionType decision) {
148            rememberDecision(row, decision);
149            fireTableRowsUpdated(row, row);
150            refreshNumUndecidedTags();
151        }
152    
153        /**
154         * set the merge decision of all {@link TagMergeItem} given by indices in <code>rows</code>
155         * to <code>decision</code>.
156         *
157         * @param row  the array of row indices
158         * @param decision the decision
159         */
160    
161        public void decide(int [] rows, MergeDecisionType decision) {
162            if (rows == null || rows.length == 0)
163                return;
164            for (int row : rows) {
165                rememberDecision(row, decision);
166            }
167            fireTableDataChanged();
168            refreshNumUndecidedTags();
169        }
170    
171        @Override
172        public int getRowCount() {
173            return tagMergeItems == null ? 0 : tagMergeItems.size();
174        }
175    
176        @Override
177        public Object getValueAt(int row, int column) {
178            // return the tagMergeItem for both columns. The cell
179            // renderer will dispatch on the column index and get
180            // the key or the value from the TagMergeItem
181            //
182            return tagMergeItems.get(row);
183        }
184    
185        @Override
186        public boolean isCellEditable(int row, int column) {
187            return false;
188        }
189    
190        public TagConflictResolveCommand buildResolveCommand(Conflict<? extends OsmPrimitive> conflict) {
191            return new TagConflictResolveCommand(conflict, tagMergeItems);
192        }
193    
194        public boolean isResolvedCompletely() {
195            for (TagMergeItem item: tagMergeItems) {
196                if (item.getMergeDecision().equals(MergeDecisionType.UNDECIDED))
197                    return false;
198            }
199            return true;
200        }
201    
202        public int getNumResolvedConflicts() {
203            int n = 0;
204            for (TagMergeItem item: tagMergeItems) {
205                if (!item.getMergeDecision().equals(MergeDecisionType.UNDECIDED)) {
206                    n++;
207                }
208            }
209            return n;
210    
211        }
212    
213        public int getFirstUndecided(int startIndex) {
214            for (int i=startIndex; i<tagMergeItems.size(); i++) {
215                if (tagMergeItems.get(i).getMergeDecision() == MergeDecisionType.UNDECIDED)
216                    return i;
217            }
218            return -1;
219        }
220    }