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 == item.getMergeDecision()) {
090                newValue++;
091            }
092        }
093        int oldValue = numUndecidedTags;
094        numUndecidedTags = newValue;
095        fireNumUndecidedTagsChanged(oldValue, numUndecidedTags);
096    }
097
098    /**
099     * Populate the model with conflicts between the tag sets of the two
100     * {@link OsmPrimitive} <code>my</code> and <code>their</code>.
101     *
102     * @param my  my primitive (i.e. the primitive from the local dataset)
103     * @param their their primitive (i.e. the primitive from the server dataset)
104     *
105     */
106    public void populate(OsmPrimitive my, OsmPrimitive their) {
107        tagMergeItems.clear();
108        Set<String> keys = new HashSet<>();
109        keys.addAll(my.keySet());
110        keys.addAll(their.keySet());
111        for (String key : keys) {
112            String myValue = my.get(key);
113            String theirValue = their.get(key);
114            if (myValue == null || theirValue == null || !myValue.equals(theirValue)) {
115                tagMergeItems.add(
116                        new TagMergeItem(key, my, their)
117                );
118            }
119        }
120        fireTableDataChanged();
121        refreshNumUndecidedTags();
122    }
123
124    /**
125     * add a {@link TagMergeItem} to the model
126     *
127     * @param item the item
128     */
129    public void addItem(TagMergeItem item) {
130        if (item != null) {
131            tagMergeItems.add(item);
132            fireTableDataChanged();
133            refreshNumUndecidedTags();
134        }
135    }
136
137    protected void rememberDecision(int row, MergeDecisionType decision) {
138        TagMergeItem item = tagMergeItems.get(row);
139        item.decide(decision);
140    }
141
142    /**
143     * set the merge decision of the {@link TagMergeItem} in row <code>row</code>
144     * to <code>decision</code>.
145     *
146     * @param row the row
147     * @param decision the decision
148     */
149    public void decide(int row, MergeDecisionType decision) {
150        rememberDecision(row, decision);
151        fireTableRowsUpdated(row, row);
152        refreshNumUndecidedTags();
153    }
154
155    /**
156     * set the merge decision of all {@link TagMergeItem} given by indices in <code>rows</code>
157     * to <code>decision</code>.
158     *
159     * @param rows the array of row indices
160     * @param decision the decision
161     */
162    public void decide(int[] rows, MergeDecisionType decision) {
163        if (rows == null || rows.length == 0)
164            return;
165        for (int row : rows) {
166            rememberDecision(row, decision);
167        }
168        fireTableDataChanged();
169        refreshNumUndecidedTags();
170    }
171
172    @Override
173    public int getRowCount() {
174        return tagMergeItems == null ? 0 : tagMergeItems.size();
175    }
176
177    @Override
178    public Object getValueAt(int row, int column) {
179        // return the tagMergeItem for both columns. The cell
180        // renderer will dispatch on the column index and get
181        // the key or the value from the TagMergeItem
182        //
183        return tagMergeItems.get(row);
184    }
185
186    @Override
187    public boolean isCellEditable(int row, int column) {
188        return false;
189    }
190
191    public TagConflictResolveCommand buildResolveCommand(Conflict<? extends OsmPrimitive> conflict) {
192        return new TagConflictResolveCommand(conflict, tagMergeItems);
193    }
194
195    public boolean isResolvedCompletely() {
196        for (TagMergeItem item: tagMergeItems) {
197            if (item.getMergeDecision() == MergeDecisionType.UNDECIDED)
198                return false;
199        }
200        return true;
201    }
202
203    public void decideRemaining(MergeDecisionType decision) {
204        for (TagMergeItem item: tagMergeItems) {
205            if (item.getMergeDecision() == MergeDecisionType.UNDECIDED)
206                item.decide(decision);
207        }
208    }
209
210    public int getNumResolvedConflicts() {
211        int n = 0;
212        for (TagMergeItem item: tagMergeItems) {
213            if (item.getMergeDecision() != MergeDecisionType.UNDECIDED) {
214                n++;
215            }
216        }
217        return n;
218
219    }
220
221    public int getFirstUndecided(int startIndex) {
222        for (int i = startIndex; i < tagMergeItems.size(); i++) {
223            if (tagMergeItems.get(i).getMergeDecision() == MergeDecisionType.UNDECIDED)
224                return i;
225        }
226        return -1;
227    }
228}