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    static public 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<String>();
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<String>();
135        this.keysWithConflicts = keysWithConflicts == null ? new HashSet<String>() : keysWithConflicts;
136        decisions = new HashMap<String, MultiValueResolutionDecision>();
137        rebuild();
138    }
139
140    @Override
141    public int getRowCount() {
142        if (displayedKeys == null) return 0;
143        return displayedKeys.size();
144    }
145
146    @Override
147    public Object getValueAt(int row, int column) {
148        return decisions.get(displayedKeys.get(row));
149    }
150
151    @Override
152    public boolean isCellEditable(int row, int column) {
153        return column == 2;
154    }
155
156    @Override
157    public void setValueAt(Object value, int row, int column) {
158        MultiValueResolutionDecision decision = decisions.get(displayedKeys.get(row));
159        if (value instanceof String) {
160            decision.keepOne((String)value);
161        } else if (value instanceof MultiValueDecisionType) {
162            MultiValueDecisionType type = (MultiValueDecisionType)value;
163            switch(type) {
164            case KEEP_NONE:
165                decision.keepNone();
166                break;
167            case KEEP_ALL:
168                decision.keepAll();
169                break;
170            }
171        }
172        GuiHelper.runInEDTAndWait(new Runnable() {
173            @Override public void run() {
174                fireTableDataChanged();
175            }
176        });
177        refreshNumConflicts();
178    }
179
180    /**
181     * Replies true if each {@link MultiValueResolutionDecision} is decided.
182     *
183     * @return true if each {@link MultiValueResolutionDecision} is decided; false
184     * otherwise
185     */
186    public boolean isResolvedCompletely() {
187        return numConflicts == 0;
188    }
189
190    public int getNumConflicts() {
191        return numConflicts;
192    }
193
194    public int getNumDecisions() {
195        return decisions == null ? 0 : decisions.size();
196    }
197
198    //TODO Should this method work with all decisions or only with displayed decisions? For MergeNodes it should be
199    //all decisions, but this method is also used on other places, so I've made new method just for MergeNodes
200    public TagCollection getResolution() {
201        TagCollection tc = new TagCollection();
202        for (String key: displayedKeys) {
203            tc.add(decisions.get(key).getResolution());
204        }
205        return tc;
206    }
207
208    public TagCollection getAllResolutions() {
209        TagCollection tc = new TagCollection();
210        for (MultiValueResolutionDecision value: decisions.values()) {
211            tc.add(value.getResolution());
212        }
213        return tc;
214    }
215
216    public MultiValueResolutionDecision getDecision(int row) {
217        return decisions.get(displayedKeys.get(row));
218    }
219
220    /**
221     * Sets whether all tags or only tags with conflicts are displayed
222     *
223     * @param showTagsWithConflictsOnly if true, only tags with conflicts are displayed
224     */
225    public void setShowTagsWithConflictsOnly(boolean showTagsWithConflictsOnly) {
226        this.showTagsWithConflictsOnly = showTagsWithConflictsOnly;
227        rebuild();
228    }
229
230    /**
231     * Sets whether all conflicts or only conflicts with multiple values are displayed
232     *
233     * @param showTagsWithMultiValuesOnly if true, only tags with multiple values are displayed
234     */
235    public void setShowTagsWithMultiValuesOnly(boolean showTagsWithMultiValuesOnly) {
236        this.showTagsWithMultiValuesOnly = showTagsWithMultiValuesOnly;
237        rebuild();
238    }
239
240    /**
241     * Prepare the default decisions for the current model
242     *
243     */
244    public void prepareDefaultTagDecisions() {
245        for (MultiValueResolutionDecision decision: decisions.values()) {
246            List<String> values = decision.getValues();
247            values.remove("");
248            if (values.size() == 1) {
249                decision.keepOne(values.get(0));
250            } else {
251                decision.keepAll();
252            }
253        }
254        rebuild();
255    }
256
257}