// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.conflict.tags;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.table.DefaultTableModel;
import org.openstreetmap.josm.data.osm.TagCollection;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.tools.CheckParameterUtil;
public class TagConflictResolverModel extends DefaultTableModel {
public static final String NUM_CONFLICTS_PROP = TagConflictResolverModel.class.getName() + ".numConflicts";
private transient TagCollection tags;
private List<String> displayedKeys;
private final Set<String> keysWithConflicts = new HashSet<>();
private transient Map<String, MultiValueResolutionDecision> decisions;
private int numConflicts;
private final PropertyChangeSupport support;
private boolean showTagsWithConflictsOnly;
private boolean showTagsWithMultiValuesOnly;
/**
* Constructs a new {@code TagConflictResolverModel}.
*/
public TagConflictResolverModel() {
numConflicts = 0;
support = new PropertyChangeSupport(this);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
support.removePropertyChangeListener(listener);
}
protected void setNumConflicts(int numConflicts) {
int oldValue = this.numConflicts;
this.numConflicts = numConflicts;
if (oldValue != this.numConflicts) {
support.firePropertyChange(NUM_CONFLICTS_PROP, oldValue, this.numConflicts);
}
}
protected void refreshNumConflicts() {
setNumConflicts((int) decisions.values().stream().filter(d -> !d.isDecided()).count());
}
protected void sort() {
displayedKeys.sort((key1, key2) -> {
if (decisions.get(key1).isDecided() && !decisions.get(key2).isDecided())
return 1;
else if (!decisions.get(key1).isDecided() && decisions.get(key2).isDecided())
return -1;
return key1.compareTo(key2);
}
);
}
/**
* initializes the model from the current tags
*
*/
public void rebuild() {
rebuild(true);
}
/**
* initializes the model from the current tags
* @param fireEvent {@code true} to call {@code fireTableDataChanged} (can be a slow operation)
* @since 11626
*/
void rebuild(boolean fireEvent) {
if (tags == null) return;
for (String key: tags.getKeys()) {
MultiValueResolutionDecision decision = new MultiValueResolutionDecision(tags.getTagsFor(key));
if (decisions.get(key) == null) {
decisions.put(key, decision);
}
}
displayedKeys.clear();
Set<String> keys = tags.getKeys();
if (showTagsWithConflictsOnly) {
keys.retainAll(keysWithConflicts);
if (showTagsWithMultiValuesOnly) {
Set<String> keysWithMultiValues = new HashSet<>();
for (String key: keys) {
if (decisions.get(key).canKeepAll()) {
keysWithMultiValues.add(key);
}
}
keys.retainAll(keysWithMultiValues);
}
for (String key: tags.getKeys()) {
if (!decisions.get(key).isDecided() && !keys.contains(key)) {
keys.add(key);
}
}
}
displayedKeys.addAll(keys);
refreshNumConflicts();
sort();
if (fireEvent) {
GuiHelper.runInEDTAndWait(this::fireTableDataChanged);
}
}
/**
* Populates the model with the tags for which conflicts are to be resolved.
*
* @param tags the tag collection with the tags. Must not be null.
* @param keysWithConflicts the set of tag keys with conflicts
* @throws IllegalArgumentException if tags is null
*/
public void populate(TagCollection tags, Set<String> keysWithConflicts) {
populate(tags, keysWithConflicts, true);
}
/**
* Populates the model with the tags for which conflicts are to be resolved.
*
* @param tags the tag collection with the tags. Must not be null.
* @param keysWithConflicts the set of tag keys with conflicts
* @param fireEvent {@code true} to call {@code fireTableDataChanged} (can be a slow operation)
* @throws IllegalArgumentException if tags is null
* @since 11626
*/
void populate(TagCollection tags, Set<String> keysWithConflicts, boolean fireEvent) {
CheckParameterUtil.ensureParameterNotNull(tags, "tags");
this.tags = tags;
displayedKeys = new ArrayList<>();
if (keysWithConflicts != null) {
this.keysWithConflicts.addAll(keysWithConflicts);
}
decisions = new HashMap<>();
rebuild(fireEvent);
}
/**
* Returns the OSM key at the given row.
* @param row The table row
* @return the OSM key at the given row.
* @since 6616
*/
public final String getKey(int row) {
return displayedKeys.get(row);
}
@Override
public int getRowCount() {
if (displayedKeys == null) return 0;
return displayedKeys.size();
}
@Override
public Object getValueAt(int row, int column) {
return getDecision(row);
}
@Override
public boolean isCellEditable(int row, int column) {
return column == 2;
}
@Override
public void setValueAt(Object value, int row, int column) {
MultiValueResolutionDecision decision = getDecision(row);
if (value instanceof String) {
decision.keepOne((String) value);
} else if (value instanceof MultiValueDecisionType) {
MultiValueDecisionType type = (MultiValueDecisionType) value;
switch(type) {
case KEEP_NONE:
decision.keepNone();
break;
case KEEP_ALL:
decision.keepAll();
break;
case SUM_ALL_NUMERIC:
decision.sumAllNumeric();
break;
default: // Do nothing
}
}
GuiHelper.runInEDTAndWait(this::fireTableDataChanged);
refreshNumConflicts();
}
/**
* Replies true if each {@link MultiValueResolutionDecision} is decided.
*
* @return true if each {@link MultiValueResolutionDecision} is decided; false otherwise
*/
public boolean isResolvedCompletely() {
return numConflicts == 0;
}
/**
* Gets the number of reamining conflicts.
* @return The number
*/
public int getNumConflicts() {
return numConflicts;
}
/**
* Gets the number of decisions the user can take
* @return The number of decisions
*/
public int getNumDecisions() {
return decisions == null ? 0 : decisions.size();
}
//TODO Should this method work with all decisions or only with displayed decisions? For MergeNodes it should be
//all decisions, but this method is also used on other places, so I've made new method just for MergeNodes
public TagCollection getResolution() {
TagCollection tc = new TagCollection();
for (String key: displayedKeys) {
tc.add(decisions.get(key).getResolution());
}
return tc;
}
public TagCollection getAllResolutions() {
TagCollection tc = new TagCollection();
for (MultiValueResolutionDecision value: decisions.values()) {
tc.add(value.getResolution());
}
return tc;
}
/**
* Returns the conflict resolution decision at the given row.
* @param row The table row
* @return the conflict resolution decision at the given row.
*/
public MultiValueResolutionDecision getDecision(int row) {
return decisions.get(getKey(row));
}
/**
* Sets whether all tags or only tags with conflicts are displayed
*
* @param showTagsWithConflictsOnly if true, only tags with conflicts are displayed
*/
public void setShowTagsWithConflictsOnly(boolean showTagsWithConflictsOnly) {
this.showTagsWithConflictsOnly = showTagsWithConflictsOnly;
rebuild();
}
/**
* Sets whether all conflicts or only conflicts with multiple values are displayed
*
* @param showTagsWithMultiValuesOnly if true, only tags with multiple values are displayed
*/
public void setShowTagsWithMultiValuesOnly(boolean showTagsWithMultiValuesOnly) {
this.showTagsWithMultiValuesOnly = showTagsWithMultiValuesOnly;
rebuild();
}
/**
* Prepare the default decisions for the current model
*
*/
public void prepareDefaultTagDecisions() {
prepareDefaultTagDecisions(true);
}
/**
* Prepare the default decisions for the current model
* @param fireEvent {@code true} to call {@code fireTableDataChanged} (can be a slow operation)
* @since 11626
*/
void prepareDefaultTagDecisions(boolean fireEvent) {
for (MultiValueResolutionDecision decision: decisions.values()) {
List<String> values = decision.getValues();
values.remove("");
if (values.size() == 1) {
// 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)
decision.keepOne(values.get(0));
}
// else: Do not suggest to keep all values in order to reduce the wrong usage of semicolon values, see #9104!
}
rebuild(fireEvent);
}
/**
* Returns the set of keys in conflict.
* @return the set of keys in conflict.
* @since 6616
*/
public final Set<String> getKeysWithConflicts() {
return new HashSet<>(keysWithConflicts);
}
}