001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.conflict.tags; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.text.MessageFormat; 007import java.util.ArrayList; 008import java.util.Collection; 009import java.util.Collections; 010import java.util.List; 011import java.util.Optional; 012 013import org.openstreetmap.josm.command.ChangePropertyCommand; 014import org.openstreetmap.josm.command.Command; 015import org.openstreetmap.josm.data.osm.OsmPrimitive; 016import org.openstreetmap.josm.data.osm.Tag; 017import org.openstreetmap.josm.data.osm.TagCollection; 018import org.openstreetmap.josm.tools.CheckParameterUtil; 019 020/** 021 * Represents a decision for a conflict due to multiple possible value for a tag. 022 * @since 2008 023 */ 024public class MultiValueResolutionDecision { 025 026 /** the type of decision */ 027 private MultiValueDecisionType type; 028 /** the collection of tags for which a decision is needed */ 029 private final TagCollection tags; 030 /** the selected value if {@link #type} is {@link MultiValueDecisionType#KEEP_ONE} */ 031 private String value; 032 033 private static final String[] SUMMABLE_KEYS = new String[] { 034 "capacity(:.+)?", "step_count" 035 }; 036 037 /** 038 * constructor 039 */ 040 public MultiValueResolutionDecision() { 041 type = MultiValueDecisionType.UNDECIDED; 042 tags = new TagCollection(); 043 autoDecide(); 044 } 045 046 /** 047 * Creates a new decision for the tag collection <code>tags</code>. 048 * All tags must have the same key. 049 * 050 * @param tags the tags. Must not be null. 051 * @throws IllegalArgumentException if tags is null 052 * @throws IllegalArgumentException if there are more than one keys 053 * @throws IllegalArgumentException if tags is empty 054 */ 055 public MultiValueResolutionDecision(TagCollection tags) { 056 CheckParameterUtil.ensureParameterNotNull(tags, "tags"); 057 if (tags.isEmpty()) 058 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' must not be empty.", "tags")); 059 if (tags.getKeys().size() != 1) 060 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' with tags for exactly one key expected. Got {1}.", 061 "tags", tags.getKeys().size())); 062 this.tags = tags; 063 autoDecide(); 064 } 065 066 /** 067 * Tries to find the best decision based on the current values. 068 */ 069 protected final void autoDecide() { 070 this.type = MultiValueDecisionType.UNDECIDED; 071 // exactly one empty value ? -> delete the tag 072 if (tags.size() == 1 && tags.getValues().contains("")) { 073 this.type = MultiValueDecisionType.KEEP_NONE; 074 075 // exactly one non empty value? -> keep this value 076 } else if (tags.size() == 1) { 077 this.type = MultiValueDecisionType.KEEP_ONE; 078 this.value = tags.getValues().iterator().next(); 079 } 080 } 081 082 /** 083 * Apply the decision to keep no value 084 */ 085 public void keepNone() { 086 this.type = MultiValueDecisionType.KEEP_NONE; 087 } 088 089 /** 090 * Apply the decision to keep all values 091 */ 092 public void keepAll() { 093 this.type = MultiValueDecisionType.KEEP_ALL; 094 } 095 096 /** 097 * Apply the decision to sum all numeric values 098 * @since 7743 099 */ 100 public void sumAllNumeric() { 101 this.type = MultiValueDecisionType.SUM_ALL_NUMERIC; 102 } 103 104 /** 105 * Apply the decision to keep exactly one value 106 * 107 * @param value the value to keep 108 * @throws IllegalArgumentException if value is null 109 * @throws IllegalStateException if value is not in the list of known values for this tag 110 */ 111 public void keepOne(String value) { 112 CheckParameterUtil.ensureParameterNotNull(value, "value"); 113 if (!tags.getValues().contains(value)) 114 throw new IllegalStateException(tr("Tag collection does not include the selected value ''{0}''.", value)); 115 this.value = value; 116 this.type = MultiValueDecisionType.KEEP_ONE; 117 } 118 119 /** 120 * sets a new value for this 121 * 122 * @param value the new vlaue 123 */ 124 public void setNew(String value) { 125 this.value = Optional.ofNullable(value).orElse(""); 126 this.type = MultiValueDecisionType.KEEP_ONE; 127 } 128 129 /** 130 * marks this as undecided 131 * 132 */ 133 public void undecide() { 134 this.type = MultiValueDecisionType.UNDECIDED; 135 } 136 137 /** 138 * Replies the chosen value 139 * 140 * @return the chosen value 141 * @throws IllegalStateException if this resolution is not yet decided 142 */ 143 public String getChosenValue() { 144 switch(type) { 145 case UNDECIDED: throw new IllegalStateException(tr("Not decided yet")); 146 case KEEP_ONE: return value; 147 case SUM_ALL_NUMERIC: return tags.getSummedValues(getKey()); 148 case KEEP_ALL: return tags.getJoinedValues(getKey()); 149 case KEEP_NONE: 150 default: return null; 151 } 152 } 153 154 /** 155 * Replies the list of possible, non empty values 156 * 157 * @return the list of possible, non empty values 158 */ 159 public List<String> getValues() { 160 List<String> ret = new ArrayList<>(tags.getValues()); 161 ret.remove(""); 162 ret.remove(null); 163 Collections.sort(ret); 164 return ret; 165 } 166 167 /** 168 * Replies the key of the tag to be resolved by this resolution 169 * 170 * @return the key of the tag to be resolved by this resolution 171 */ 172 public String getKey() { 173 return tags.getKeys().iterator().next(); 174 } 175 176 /** 177 * Replies true if the empty value is a possible value in this resolution 178 * 179 * @return true if the empty value is a possible value in this resolution 180 */ 181 public boolean canKeepNone() { 182 return tags.getValues().contains(""); 183 } 184 185 /** 186 * Replies true, if this resolution has more than 1 possible non-empty values 187 * 188 * @return true, if this resolution has more than 1 possible non-empty values 189 */ 190 public boolean canKeepAll() { 191 return getValues().size() > 1; 192 } 193 194 /** 195 * Replies true, if summing all numeric values is a possible value in this resolution 196 * 197 * @return true, if summing all numeric values is a possible value in this resolution 198 * @since 7743 199 */ 200 public boolean canSumAllNumeric() { 201 if (!canKeepAll()) { 202 return false; 203 } 204 for (String key : SUMMABLE_KEYS) { 205 if (getKey().matches(key)) { 206 return true; 207 } 208 } 209 return false; 210 } 211 212 /** 213 * Replies true if this resolution is decided 214 * 215 * @return true if this resolution is decided 216 */ 217 public boolean isDecided() { 218 return !type.equals(MultiValueDecisionType.UNDECIDED); 219 } 220 221 /** 222 * Replies the type of the resolution 223 * 224 * @return the type of the resolution 225 */ 226 public MultiValueDecisionType getDecisionType() { 227 return type; 228 } 229 230 /** 231 * Applies the resolution to an {@link OsmPrimitive} 232 * 233 * @param primitive the primitive 234 * @throws IllegalStateException if this resolution is not resolved yet 235 * 236 */ 237 public void applyTo(OsmPrimitive primitive) { 238 if (primitive == null) return; 239 if (!isDecided()) 240 throw new IllegalStateException(tr("Not decided yet")); 241 String key = tags.getKeys().iterator().next(); 242 if (type.equals(MultiValueDecisionType.KEEP_NONE)) { 243 primitive.remove(key); 244 } else { 245 primitive.put(key, getChosenValue()); 246 } 247 } 248 249 /** 250 * Applies this resolution to a collection of primitives 251 * 252 * @param primitives the collection of primitives 253 * @throws IllegalStateException if this resolution is not resolved yet 254 */ 255 public void applyTo(Collection<? extends OsmPrimitive> primitives) { 256 if (primitives == null) return; 257 for (OsmPrimitive primitive: primitives) { 258 if (primitive == null) { 259 continue; 260 } 261 applyTo(primitive); 262 } 263 } 264 265 /** 266 * Builds a change command for applying this resolution to a primitive 267 * 268 * @param primitive the primitive 269 * @return the change command 270 * @throws IllegalArgumentException if primitive is null 271 * @throws IllegalStateException if this resolution is not resolved yet 272 */ 273 public Command buildChangeCommand(OsmPrimitive primitive) { 274 CheckParameterUtil.ensureParameterNotNull(primitive, "primitive"); 275 if (!isDecided()) 276 throw new IllegalStateException(tr("Not decided yet")); 277 String key = tags.getKeys().iterator().next(); 278 return new ChangePropertyCommand(primitive, key, getChosenValue()); 279 } 280 281 /** 282 * Builds a change command for applying this resolution to a collection of primitives 283 * 284 * @param primitives the collection of primitives 285 * @return the change command 286 * @throws IllegalArgumentException if primitives is null 287 * @throws IllegalStateException if this resolution is not resolved yet 288 */ 289 public Command buildChangeCommand(Collection<? extends OsmPrimitive> primitives) { 290 CheckParameterUtil.ensureParameterNotNull(primitives, "primitives"); 291 if (!isDecided()) 292 throw new IllegalStateException(tr("Not decided yet")); 293 String key = tags.getKeys().iterator().next(); 294 return new ChangePropertyCommand(primitives, key, getChosenValue()); 295 } 296 297 /** 298 * Replies a tag representing the current resolution. Null, if this resolution is not resolved yet. 299 * 300 * @return a tag representing the current resolution. Null, if this resolution is not resolved yet 301 */ 302 public Tag getResolution() { 303 switch(type) { 304 case SUM_ALL_NUMERIC: return new Tag(getKey(), tags.getSummedValues(getKey())); 305 case KEEP_ALL: return new Tag(getKey(), tags.getJoinedValues(getKey())); 306 case KEEP_ONE: return new Tag(getKey(), value); 307 case KEEP_NONE: return new Tag(getKey(), ""); 308 case UNDECIDED: 309 default: return null; 310 } 311 } 312}