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