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