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