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.HashMap; 008import java.util.HashSet; 009import java.util.List; 010import java.util.Map; 011import java.util.Set; 012 013import javax.swing.table.DefaultTableModel; 014 015import org.openstreetmap.josm.data.osm.TagCollection; 016import org.openstreetmap.josm.gui.util.GuiHelper; 017import org.openstreetmap.josm.tools.CheckParameterUtil; 018 019public class TagConflictResolverModel extends DefaultTableModel { 020 public static final String NUM_CONFLICTS_PROP = TagConflictResolverModel.class.getName() + ".numConflicts"; 021 022 private transient TagCollection tags; 023 private List<String> displayedKeys; 024 private final Set<String> keysWithConflicts = new HashSet<>(); 025 private transient Map<String, MultiValueResolutionDecision> decisions; 026 private int numConflicts; 027 private final PropertyChangeSupport support; 028 private boolean showTagsWithConflictsOnly; 029 private boolean showTagsWithMultiValuesOnly; 030 031 /** 032 * Constructs a new {@code TagConflictResolverModel}. 033 */ 034 public TagConflictResolverModel() { 035 numConflicts = 0; 036 support = new PropertyChangeSupport(this); 037 } 038 039 public void addPropertyChangeListener(PropertyChangeListener listener) { 040 support.addPropertyChangeListener(listener); 041 } 042 043 public void removePropertyChangeListener(PropertyChangeListener listener) { 044 support.removePropertyChangeListener(listener); 045 } 046 047 protected void setNumConflicts(int numConflicts) { 048 int oldValue = this.numConflicts; 049 this.numConflicts = numConflicts; 050 if (oldValue != this.numConflicts) { 051 support.firePropertyChange(NUM_CONFLICTS_PROP, oldValue, this.numConflicts); 052 } 053 } 054 055 protected void refreshNumConflicts() { 056 setNumConflicts((int) decisions.values().stream().filter(d -> !d.isDecided()).count()); 057 } 058 059 protected void sort() { 060 displayedKeys.sort((key1, key2) -> { 061 if (decisions.get(key1).isDecided() && !decisions.get(key2).isDecided()) 062 return 1; 063 else if (!decisions.get(key1).isDecided() && decisions.get(key2).isDecided()) 064 return -1; 065 return key1.compareTo(key2); 066 } 067 ); 068 } 069 070 /** 071 * initializes the model from the current tags 072 * 073 */ 074 public void rebuild() { 075 if (tags == null) return; 076 for (String key: tags.getKeys()) { 077 MultiValueResolutionDecision decision = new MultiValueResolutionDecision(tags.getTagsFor(key)); 078 if (decisions.get(key) == null) { 079 decisions.put(key, decision); 080 } 081 } 082 displayedKeys.clear(); 083 Set<String> keys = tags.getKeys(); 084 if (showTagsWithConflictsOnly) { 085 keys.retainAll(keysWithConflicts); 086 if (showTagsWithMultiValuesOnly) { 087 Set<String> keysWithMultiValues = new HashSet<>(); 088 for (String key: keys) { 089 if (decisions.get(key).canKeepAll()) { 090 keysWithMultiValues.add(key); 091 } 092 } 093 keys.retainAll(keysWithMultiValues); 094 } 095 for (String key: tags.getKeys()) { 096 if (!decisions.get(key).isDecided() && !keys.contains(key)) { 097 keys.add(key); 098 } 099 } 100 } 101 displayedKeys.addAll(keys); 102 refreshNumConflicts(); 103 sort(); 104 GuiHelper.runInEDTAndWait(this::fireTableDataChanged); 105 } 106 107 /** 108 * Populates the model with the tags for which conflicts are to be resolved. 109 * 110 * @param tags the tag collection with the tags. Must not be null. 111 * @param keysWithConflicts the set of tag keys with conflicts 112 * @throws IllegalArgumentException if tags is null 113 */ 114 public void populate(TagCollection tags, Set<String> keysWithConflicts) { 115 CheckParameterUtil.ensureParameterNotNull(tags, "tags"); 116 this.tags = tags; 117 displayedKeys = new ArrayList<>(); 118 if (keysWithConflicts != null) { 119 this.keysWithConflicts.addAll(keysWithConflicts); 120 } 121 decisions = new HashMap<>(); 122 rebuild(); 123 } 124 125 /** 126 * Returns the OSM key at the given row. 127 * @param row The table row 128 * @return the OSM key at the given row. 129 * @since 6616 130 */ 131 public final String getKey(int row) { 132 return displayedKeys.get(row); 133 } 134 135 @Override 136 public int getRowCount() { 137 if (displayedKeys == null) return 0; 138 return displayedKeys.size(); 139 } 140 141 @Override 142 public Object getValueAt(int row, int column) { 143 return getDecision(row); 144 } 145 146 @Override 147 public boolean isCellEditable(int row, int column) { 148 return column == 2; 149 } 150 151 @Override 152 public void setValueAt(Object value, int row, int column) { 153 MultiValueResolutionDecision decision = getDecision(row); 154 if (value instanceof String) { 155 decision.keepOne((String) value); 156 } else if (value instanceof MultiValueDecisionType) { 157 MultiValueDecisionType type = (MultiValueDecisionType) value; 158 switch(type) { 159 case KEEP_NONE: 160 decision.keepNone(); 161 break; 162 case KEEP_ALL: 163 decision.keepAll(); 164 break; 165 case SUM_ALL_NUMERIC: 166 decision.sumAllNumeric(); 167 break; 168 default: // Do nothing 169 } 170 } 171 GuiHelper.runInEDTAndWait(this::fireTableDataChanged); 172 refreshNumConflicts(); 173 } 174 175 /** 176 * Replies true if each {@link MultiValueResolutionDecision} is decided. 177 * 178 * @return true if each {@link MultiValueResolutionDecision} is decided; false otherwise 179 */ 180 public boolean isResolvedCompletely() { 181 return numConflicts == 0; 182 } 183 184 /** 185 * Gets the number of reamining conflicts. 186 * @return The number 187 */ 188 public int getNumConflicts() { 189 return numConflicts; 190 } 191 192 /** 193 * Gets the number of decisions the user can take 194 * @return The number of decisions 195 */ 196 public int getNumDecisions() { 197 return decisions == null ? 0 : decisions.size(); 198 } 199 200 //TODO Should this method work with all decisions or only with displayed decisions? For MergeNodes it should be 201 //all decisions, but this method is also used on other places, so I've made new method just for MergeNodes 202 public TagCollection getResolution() { 203 TagCollection tc = new TagCollection(); 204 for (String key: displayedKeys) { 205 tc.add(decisions.get(key).getResolution()); 206 } 207 return tc; 208 } 209 210 public TagCollection getAllResolutions() { 211 TagCollection tc = new TagCollection(); 212 for (MultiValueResolutionDecision value: decisions.values()) { 213 tc.add(value.getResolution()); 214 } 215 return tc; 216 } 217 218 /** 219 * Returns the conflict resolution decision at the given row. 220 * @param row The table row 221 * @return the conflict resolution decision at the given row. 222 */ 223 public MultiValueResolutionDecision getDecision(int row) { 224 return decisions.get(getKey(row)); 225 } 226 227 /** 228 * Sets whether all tags or only tags with conflicts are displayed 229 * 230 * @param showTagsWithConflictsOnly if true, only tags with conflicts are displayed 231 */ 232 public void setShowTagsWithConflictsOnly(boolean showTagsWithConflictsOnly) { 233 this.showTagsWithConflictsOnly = showTagsWithConflictsOnly; 234 rebuild(); 235 } 236 237 /** 238 * Sets whether all conflicts or only conflicts with multiple values are displayed 239 * 240 * @param showTagsWithMultiValuesOnly if true, only tags with multiple values are displayed 241 */ 242 public void setShowTagsWithMultiValuesOnly(boolean showTagsWithMultiValuesOnly) { 243 this.showTagsWithMultiValuesOnly = showTagsWithMultiValuesOnly; 244 rebuild(); 245 } 246 247 /** 248 * Prepare the default decisions for the current model 249 * 250 */ 251 public void prepareDefaultTagDecisions() { 252 for (MultiValueResolutionDecision decision: decisions.values()) { 253 List<String> values = decision.getValues(); 254 values.remove(""); 255 if (values.size() == 1) { 256 // TODO: Do not suggest to keep the single value in order to avoid long highways to become tunnels+bridges+... 257 // (only if both primitives are tagged) 258 decision.keepOne(values.get(0)); 259 } 260 // else: Do not suggest to keep all values in order to reduce the wrong usage of semicolon values, see #9104! 261 } 262 rebuild(); 263 } 264 265 /** 266 * Returns the set of keys in conflict. 267 * @return the set of keys in conflict. 268 * @since 6616 269 */ 270 public final Set<String> getKeysWithConflicts() { 271 return new HashSet<>(keysWithConflicts); 272 } 273}