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