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