001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.conflict.pair.tags;
003
004import java.beans.PropertyChangeEvent;
005import java.beans.PropertyChangeListener;
006import java.util.ArrayList;
007import java.util.HashSet;
008import java.util.List;
009import java.util.Set;
010
011import javax.swing.table.DefaultTableModel;
012
013import org.openstreetmap.josm.command.conflict.TagConflictResolveCommand;
014import org.openstreetmap.josm.data.conflict.Conflict;
015import org.openstreetmap.josm.data.osm.OsmPrimitive;
016import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
017
018/**
019 * This is the {@link javax.swing.table.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 */
031public class TagMergeModel extends DefaultTableModel {
032    public static 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<>();
044        listeners = new ArrayList<>();
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<>();
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 rows the array of row indices
158     * @param decision the decision
159     */
160    public void decide(int [] rows, MergeDecisionType decision) {
161        if (rows == null || rows.length == 0)
162            return;
163        for (int row : rows) {
164            rememberDecision(row, decision);
165        }
166        fireTableDataChanged();
167        refreshNumUndecidedTags();
168    }
169
170    @Override
171    public int getRowCount() {
172        return tagMergeItems == null ? 0 : tagMergeItems.size();
173    }
174
175    @Override
176    public Object getValueAt(int row, int column) {
177        // return the tagMergeItem for both columns. The cell
178        // renderer will dispatch on the column index and get
179        // the key or the value from the TagMergeItem
180        //
181        return tagMergeItems.get(row);
182    }
183
184    @Override
185    public boolean isCellEditable(int row, int column) {
186        return false;
187    }
188
189    public TagConflictResolveCommand buildResolveCommand(Conflict<? extends OsmPrimitive> conflict) {
190        return new TagConflictResolveCommand(conflict, tagMergeItems);
191    }
192
193    public boolean isResolvedCompletely() {
194        for (TagMergeItem item: tagMergeItems) {
195            if (item.getMergeDecision().equals(MergeDecisionType.UNDECIDED))
196                return false;
197        }
198        return true;
199    }
200
201    public int getNumResolvedConflicts() {
202        int n = 0;
203        for (TagMergeItem item: tagMergeItems) {
204            if (!item.getMergeDecision().equals(MergeDecisionType.UNDECIDED)) {
205                n++;
206            }
207        }
208        return n;
209
210    }
211
212    public int getFirstUndecided(int startIndex) {
213        for (int i=startIndex; i<tagMergeItems.size(); i++) {
214            if (tagMergeItems.get(i).getMergeDecision() == MergeDecisionType.UNDECIDED)
215                return i;
216        }
217        return -1;
218    }
219}