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