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 transient List<TagMergeItem> tagMergeItems;
036
037    /** the property change listeners */
038    private final transient Set<PropertyChangeListener> listeners;
039
040    private int numUndecidedTags;
041
042    /**
043     * Constructs a new {@code TagMergeModel}.
044     */
045    public TagMergeModel() {
046        tagMergeItems = new ArrayList<>();
047        listeners = new HashSet<>();
048    }
049
050    public void addPropertyChangeListener(PropertyChangeListener listener) {
051        synchronized (listeners) {
052            if (listener == null) return;
053            if (listeners.contains(listener)) return;
054            listeners.add(listener);
055        }
056    }
057
058    public void removePropertyChangeListener(PropertyChangeListener listener) {
059        synchronized (listeners) {
060            if (listener == null) return;
061            if (!listeners.contains(listener)) return;
062            listeners.remove(listener);
063        }
064    }
065
066    /**
067     * notifies {@link PropertyChangeListener}s about an update of {@link TagMergeModel#PROP_NUM_UNDECIDED_TAGS}
068
069     * @param oldValue the old value
070     * @param newValue the new value
071     */
072    protected void fireNumUndecidedTagsChanged(int oldValue, int newValue) {
073        PropertyChangeEvent evt = new PropertyChangeEvent(this, PROP_NUM_UNDECIDED_TAGS, oldValue, newValue);
074        synchronized (listeners) {
075            for (PropertyChangeListener l : listeners) {
076                l.propertyChange(evt);
077            }
078        }
079    }
080
081    /**
082     * refreshes the number of undecided tag conflicts after an update in the list of
083     * {@link TagMergeItem}s. Notifies {@link PropertyChangeListener} if necessary.
084     *
085     */
086    protected void refreshNumUndecidedTags() {
087        int newValue = 0;
088        for (TagMergeItem item: tagMergeItems) {
089            if (MergeDecisionType.UNDECIDED.equals(item.getMergeDecision())) {
090                newValue++;
091            }
092        }
093        int oldValue = numUndecidedTags;
094        numUndecidedTags = newValue;
095        fireNumUndecidedTagsChanged(oldValue, numUndecidedTags);
096
097    }
098
099    /**
100     * Populate the model with conflicts between the tag sets of the two
101     * {@link OsmPrimitive} <code>my</code> and <code>their</code>.
102     *
103     * @param my  my primitive (i.e. the primitive from the local dataset)
104     * @param their their primitive (i.e. the primitive from the server dataset)
105     *
106     */
107    public void populate(OsmPrimitive my, OsmPrimitive their) {
108        tagMergeItems.clear();
109        Set<String> keys = new HashSet<>();
110        keys.addAll(my.keySet());
111        keys.addAll(their.keySet());
112        for (String key : keys) {
113            String myValue = my.get(key);
114            String theirValue = their.get(key);
115            if (myValue == null || theirValue == null || !myValue.equals(theirValue)) {
116                tagMergeItems.add(
117                        new TagMergeItem(key, my, their)
118                );
119            }
120        }
121        fireTableDataChanged();
122        refreshNumUndecidedTags();
123    }
124
125    /**
126     * add a {@link TagMergeItem} to the model
127     *
128     * @param item the item
129     */
130    public void addItem(TagMergeItem item) {
131        if (item != null) {
132            tagMergeItems.add(item);
133            fireTableDataChanged();
134            refreshNumUndecidedTags();
135        }
136    }
137
138    protected void rememberDecision(int row, MergeDecisionType decision) {
139        TagMergeItem item = tagMergeItems.get(row);
140        item.decide(decision);
141    }
142
143    /**
144     * set the merge decision of the {@link TagMergeItem} in row <code>row</code>
145     * to <code>decision</code>.
146     *
147     * @param row the row
148     * @param decision the decision
149     */
150    public void decide(int row, MergeDecisionType decision) {
151        rememberDecision(row, decision);
152        fireTableRowsUpdated(row, row);
153        refreshNumUndecidedTags();
154    }
155
156    /**
157     * set the merge decision of all {@link TagMergeItem} given by indices in <code>rows</code>
158     * to <code>decision</code>.
159     *
160     * @param rows the array of row indices
161     * @param decision the decision
162     */
163    public void decide(int[] rows, MergeDecisionType decision) {
164        if (rows == null || rows.length == 0)
165            return;
166        for (int row : rows) {
167            rememberDecision(row, decision);
168        }
169        fireTableDataChanged();
170        refreshNumUndecidedTags();
171    }
172
173    @Override
174    public int getRowCount() {
175        return tagMergeItems == null ? 0 : tagMergeItems.size();
176    }
177
178    @Override
179    public Object getValueAt(int row, int column) {
180        // return the tagMergeItem for both columns. The cell
181        // renderer will dispatch on the column index and get
182        // the key or the value from the TagMergeItem
183        //
184        return tagMergeItems.get(row);
185    }
186
187    @Override
188    public boolean isCellEditable(int row, int column) {
189        return false;
190    }
191
192    public TagConflictResolveCommand buildResolveCommand(Conflict<? extends OsmPrimitive> conflict) {
193        return new TagConflictResolveCommand(conflict, tagMergeItems);
194    }
195
196    public boolean isResolvedCompletely() {
197        for (TagMergeItem item: tagMergeItems) {
198            if (item.getMergeDecision().equals(MergeDecisionType.UNDECIDED))
199                return false;
200        }
201        return true;
202    }
203
204    public void decideRemaining(MergeDecisionType decision) {
205        for (TagMergeItem item: tagMergeItems) {
206            if (item.getMergeDecision().equals(MergeDecisionType.UNDECIDED))
207                item.decide(decision);
208        }
209    }
210
211    public int getNumResolvedConflicts() {
212        int n = 0;
213        for (TagMergeItem item: tagMergeItems) {
214            if (!item.getMergeDecision().equals(MergeDecisionType.UNDECIDED)) {
215                n++;
216            }
217        }
218        return n;
219
220    }
221
222    public int getFirstUndecided(int startIndex) {
223        for (int i = startIndex; i < tagMergeItems.size(); i++) {
224            if (tagMergeItems.get(i).getMergeDecision() == MergeDecisionType.UNDECIDED)
225                return i;
226        }
227        return -1;
228    }
229}