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