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 != 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 == 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}