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    public 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 otherwise
194     */
195    public boolean isResolvedCompletely() {
196        return numConflicts == 0 && keysWithConflicts != null && keysWithConflicts.isEmpty();
197    }
198
199    public int getNumConflicts() {
200        return numConflicts;
201    }
202
203    public int getNumDecisions() {
204        return decisions == null ? 0 : decisions.size();
205    }
206
207    //TODO Should this method work with all decisions or only with displayed decisions? For MergeNodes it should be
208    //all decisions, but this method is also used on other places, so I've made new method just for MergeNodes
209    public TagCollection getResolution() {
210        TagCollection tc = new TagCollection();
211        for (String key: displayedKeys) {
212            tc.add(decisions.get(key).getResolution());
213        }
214        return tc;
215    }
216
217    public TagCollection getAllResolutions() {
218        TagCollection tc = new TagCollection();
219        for (MultiValueResolutionDecision value: decisions.values()) {
220            tc.add(value.getResolution());
221        }
222        return tc;
223    }
224
225    /**
226     * Returns the conflict resolution decision at the given row.
227     * @param row The table row
228     * @return the conflict resolution decision at the given row.
229     */
230    public MultiValueResolutionDecision getDecision(int row) {
231        return decisions.get(getKey(row));
232    }
233
234    /**
235     * Sets whether all tags or only tags with conflicts are displayed
236     *
237     * @param showTagsWithConflictsOnly if true, only tags with conflicts are displayed
238     */
239    public void setShowTagsWithConflictsOnly(boolean showTagsWithConflictsOnly) {
240        this.showTagsWithConflictsOnly = showTagsWithConflictsOnly;
241        rebuild();
242    }
243
244    /**
245     * Sets whether all conflicts or only conflicts with multiple values are displayed
246     *
247     * @param showTagsWithMultiValuesOnly if true, only tags with multiple values are displayed
248     */
249    public void setShowTagsWithMultiValuesOnly(boolean showTagsWithMultiValuesOnly) {
250        this.showTagsWithMultiValuesOnly = showTagsWithMultiValuesOnly;
251        rebuild();
252    }
253
254    /**
255     * Prepare the default decisions for the current model
256     *
257     */
258    public void prepareDefaultTagDecisions() {
259        for (MultiValueResolutionDecision decision: decisions.values()) {
260            List<String> values = decision.getValues();
261            values.remove("");
262            if (values.size() == 1) {
263                // 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)
264                decision.keepOne(values.get(0));
265            } else {
266                // Do not suggest to keep all values in order to reduce the wrong usage of semicolon values, see #9104!
267            }
268        }
269        rebuild();
270    }
271
272    /**
273     * Returns the set of keys in conflict.
274     * @return the set of keys in conflict.
275     * @since 6616
276     */
277    public final Set<String> getKeysWithConflicts() {
278        return new HashSet<>(keysWithConflicts);
279    }
280}