001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.conflict.tags;
003
004import java.beans.PropertyChangeListener;
005import java.beans.PropertyChangeSupport;
006import java.util.ArrayList;
007import java.util.HashMap;
008import java.util.HashSet;
009import java.util.List;
010import java.util.Map;
011import java.util.Set;
012
013import javax.swing.table.DefaultTableModel;
014
015import org.openstreetmap.josm.data.osm.TagCollection;
016import org.openstreetmap.josm.gui.util.GuiHelper;
017import org.openstreetmap.josm.tools.CheckParameterUtil;
018
019/**
020 * This model holds the information about tags that are currently conflicting and the decision of the user regarding them.
021 */
022public class TagConflictResolverModel extends DefaultTableModel {
023    public static final String NUM_CONFLICTS_PROP = TagConflictResolverModel.class.getName() + ".numConflicts";
024
025    private transient TagCollection tags;
026    private List<String> displayedKeys;
027    private final Set<String> keysWithConflicts = new HashSet<>();
028    private transient Map<String, MultiValueResolutionDecision> decisions;
029    private int numConflicts;
030    private final PropertyChangeSupport support;
031    private boolean showTagsWithConflictsOnly;
032    private boolean showTagsWithMultiValuesOnly;
033
034    /**
035     * Constructs a new {@code TagConflictResolverModel}.
036     */
037    public TagConflictResolverModel() {
038        numConflicts = 0;
039        support = new PropertyChangeSupport(this);
040    }
041
042    public void addPropertyChangeListener(PropertyChangeListener listener) {
043        support.addPropertyChangeListener(listener);
044    }
045
046    public void removePropertyChangeListener(PropertyChangeListener listener) {
047        support.removePropertyChangeListener(listener);
048    }
049
050    protected void setNumConflicts(int numConflicts) {
051        int oldValue = this.numConflicts;
052        this.numConflicts = numConflicts;
053        if (oldValue != this.numConflicts) {
054            support.firePropertyChange(NUM_CONFLICTS_PROP, oldValue, this.numConflicts);
055        }
056    }
057
058    protected void refreshNumConflicts() {
059        setNumConflicts((int) decisions.values().stream().filter(d -> !d.isDecided()).count());
060    }
061
062    protected void sort() {
063        displayedKeys.sort((key1, key2) -> {
064                if (decisions.get(key1).isDecided() && !decisions.get(key2).isDecided())
065                    return 1;
066                else if (!decisions.get(key1).isDecided() && decisions.get(key2).isDecided())
067                    return -1;
068                return key1.compareTo(key2);
069            }
070        );
071    }
072
073    /**
074     * initializes the model from the current tags
075     *
076     */
077    public void rebuild() {
078        rebuild(true);
079    }
080
081    /**
082     * initializes the model from the current tags
083     * @param fireEvent {@code true} to call {@code fireTableDataChanged} (can be a slow operation)
084     * @since 11626
085     */
086    void rebuild(boolean fireEvent) {
087        if (tags == null) return;
088        for (String key: tags.getKeys()) {
089            MultiValueResolutionDecision decision = new MultiValueResolutionDecision(tags.getTagsFor(key));
090            if (decisions.get(key) == null) {
091                decisions.put(key, decision);
092            }
093        }
094        displayedKeys.clear();
095        Set<String> keys = tags.getKeys();
096        if (showTagsWithConflictsOnly) {
097            keys.retainAll(keysWithConflicts);
098            if (showTagsWithMultiValuesOnly) {
099                Set<String> keysWithMultiValues = new HashSet<>();
100                for (String key: keys) {
101                    if (decisions.get(key).canKeepAll()) {
102                        keysWithMultiValues.add(key);
103                    }
104                }
105                keys.retainAll(keysWithMultiValues);
106            }
107            for (String key: tags.getKeys()) {
108                if (!decisions.get(key).isDecided() && !keys.contains(key)) {
109                    keys.add(key);
110                }
111            }
112        }
113        displayedKeys.addAll(keys);
114        refreshNumConflicts();
115        sort();
116        if (fireEvent) {
117            GuiHelper.runInEDTAndWait(this::fireTableDataChanged);
118        }
119    }
120
121    /**
122     * Populates the model with the tags for which conflicts are to be resolved.
123     *
124     * @param tags  the tag collection with the tags. Must not be null.
125     * @param keysWithConflicts the set of tag keys with conflicts
126     * @throws IllegalArgumentException if tags is null
127     */
128    public void populate(TagCollection tags, Set<String> keysWithConflicts) {
129        populate(tags, keysWithConflicts, true);
130    }
131
132    /**
133     * Populates the model with the tags for which conflicts are to be resolved.
134     *
135     * @param tags  the tag collection with the tags. Must not be null.
136     * @param keysWithConflicts the set of tag keys with conflicts
137     * @param fireEvent {@code true} to call {@code fireTableDataChanged} (can be a slow operation)
138     * @throws IllegalArgumentException if tags is null
139     * @since 11626
140     */
141    void populate(TagCollection tags, Set<String> keysWithConflicts, boolean fireEvent) {
142        CheckParameterUtil.ensureParameterNotNull(tags, "tags");
143        this.tags = tags;
144        displayedKeys = new ArrayList<>();
145        if (keysWithConflicts != null) {
146            this.keysWithConflicts.addAll(keysWithConflicts);
147        }
148        decisions = new HashMap<>();
149        rebuild(fireEvent);
150    }
151
152    /**
153     * Returns the OSM key at the given row.
154     * @param row The table row
155     * @return the OSM key at the given row.
156     * @since 6616
157     */
158    public final String getKey(int row) {
159        return displayedKeys.get(row);
160    }
161
162    @Override
163    public int getRowCount() {
164        if (displayedKeys == null) return 0;
165        return displayedKeys.size();
166    }
167
168    @Override
169    public Object getValueAt(int row, int column) {
170        return getDecision(row);
171    }
172
173    @Override
174    public boolean isCellEditable(int row, int column) {
175        return column == 2;
176    }
177
178    @Override
179    public void setValueAt(Object value, int row, int column) {
180        MultiValueResolutionDecision decision = getDecision(row);
181        if (value instanceof String) {
182            decision.keepOne((String) value);
183        } else if (value instanceof MultiValueDecisionType) {
184            MultiValueDecisionType type = (MultiValueDecisionType) value;
185            switch(type) {
186            case KEEP_NONE:
187                decision.keepNone();
188                break;
189            case KEEP_ALL:
190                decision.keepAll();
191                break;
192            case SUM_ALL_NUMERIC:
193                decision.sumAllNumeric();
194                break;
195            default: // Do nothing
196            }
197        }
198        GuiHelper.runInEDTAndWait(this::fireTableDataChanged);
199        refreshNumConflicts();
200    }
201
202    /**
203     * Replies true if each {@link MultiValueResolutionDecision} is decided.
204     *
205     * @return true if each {@link MultiValueResolutionDecision} is decided; false otherwise
206     */
207    public boolean isResolvedCompletely() {
208        return numConflicts == 0;
209    }
210
211    /**
212     * Gets the number of reamining conflicts.
213     * @return The number
214     */
215    public int getNumConflicts() {
216        return numConflicts;
217    }
218
219    /**
220     * Gets the number of decisions the user can take
221     * @return The number of decisions
222     */
223    public int getNumDecisions() {
224        return decisions == null ? 0 : decisions.size();
225    }
226
227    //TODO Should this method work with all decisions or only with displayed decisions? For MergeNodes it should be
228    //all decisions, but this method is also used on other places, so I've made new method just for MergeNodes
229    public TagCollection getResolution() {
230        TagCollection tc = new TagCollection();
231        for (String key: displayedKeys) {
232            tc.add(decisions.get(key).getResolution());
233        }
234        return tc;
235    }
236
237    public TagCollection getAllResolutions() {
238        TagCollection tc = new TagCollection();
239        for (MultiValueResolutionDecision value: decisions.values()) {
240            tc.add(value.getResolution());
241        }
242        return tc;
243    }
244
245    /**
246     * Returns the conflict resolution decision at the given row.
247     * @param row The table row
248     * @return the conflict resolution decision at the given row.
249     */
250    public MultiValueResolutionDecision getDecision(int row) {
251        return decisions.get(getKey(row));
252    }
253
254    /**
255     * Sets whether all tags or only tags with conflicts are displayed
256     *
257     * @param showTagsWithConflictsOnly if true, only tags with conflicts are displayed
258     */
259    public void setShowTagsWithConflictsOnly(boolean showTagsWithConflictsOnly) {
260        this.showTagsWithConflictsOnly = showTagsWithConflictsOnly;
261        rebuild();
262    }
263
264    /**
265     * Sets whether all conflicts or only conflicts with multiple values are displayed
266     *
267     * @param showTagsWithMultiValuesOnly if true, only tags with multiple values are displayed
268     */
269    public void setShowTagsWithMultiValuesOnly(boolean showTagsWithMultiValuesOnly) {
270        this.showTagsWithMultiValuesOnly = showTagsWithMultiValuesOnly;
271        rebuild();
272    }
273
274    /**
275     * Prepare the default decisions for the current model
276     *
277     */
278    public void prepareDefaultTagDecisions() {
279        prepareDefaultTagDecisions(true);
280    }
281
282    /**
283     * Prepare the default decisions for the current model
284     * @param fireEvent {@code true} to call {@code fireTableDataChanged} (can be a slow operation)
285     * @since 11626
286     */
287    void prepareDefaultTagDecisions(boolean fireEvent) {
288        for (MultiValueResolutionDecision decision: decisions.values()) {
289            List<String> values = decision.getValues();
290            values.remove("");
291            if (values.size() == 1) {
292                // TODO: Do not suggest to keep the single value in order to avoid long highways to become tunnels+bridges+...
293                // (only if both primitives are tagged)
294                decision.keepOne(values.get(0));
295            }
296            // else: Do not suggest to keep all values in order to reduce the wrong usage of semicolon values, see #9104!
297        }
298        rebuild(fireEvent);
299    }
300
301    /**
302     * Returns the set of keys in conflict.
303     * @return the set of keys in conflict.
304     * @since 6616
305     */
306    public final Set<String> getKeysWithConflicts() {
307        return new HashSet<>(keysWithConflicts);
308    }
309}