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