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