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