001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.tagging.presets.items;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.util.Collection;
007import java.util.EnumSet;
008import java.util.HashMap;
009import java.util.Map;
010import java.util.NoSuchElementException;
011import java.util.SortedSet;
012import java.util.TreeSet;
013
014import org.openstreetmap.josm.data.osm.OsmPrimitive;
015import org.openstreetmap.josm.data.osm.OsmUtils;
016import org.openstreetmap.josm.data.preferences.BooleanProperty;
017import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
018
019/**
020 * Preset item associated to an OSM key.
021 */
022public abstract class KeyedItem extends TaggingPresetItem {
023
024    /** Translatation of "<different>". Use in combo boxes to display an entry matching several different values. */
025    protected static final String DIFFERENT = tr("<different>");
026
027    protected static final BooleanProperty PROP_FILL_DEFAULT = new BooleanProperty("taggingpreset.fill-default-for-tagged-primitives", false);
028
029    /** Last value of each key used in presets, used for prefilling corresponding fields */
030    static final Map<String, String> LAST_VALUES = new HashMap<>();
031
032    /** This specifies the property key that will be modified by the item. */
033    public String key; // NOSONAR
034    /** The text to display */
035    public String text; // NOSONAR
036    /** The context used for translating {@link #text} */
037    public String text_context; // NOSONAR
038    /**
039     * Allows to change the matching process, i.e., determining whether the tags of an OSM object fit into this preset.
040     * If a preset fits then it is linked in the Tags/Membership dialog.<ul>
041     * <li>none: neutral, i.e., do not consider this item for matching</li>
042     * <li>key: positive if key matches, neutral otherwise</li>
043     * <li>key!: positive if key matches, negative otherwise</li>
044     * <li>keyvalue: positive if key and value matches, neutral otherwise</li>
045     * <li>keyvalue!: positive if key and value matches, negative otherwise</li></ul>
046     * Note that for a match, at least one positive and no negative is required.
047     * Default is "keyvalue!" for {@link Key} and "none" for {@link Text}, {@link Combo}, {@link MultiSelect} and {@link Check}.
048     */
049    public String match = getDefaultMatch().getValue(); // NOSONAR
050
051    /**
052     * Enum denoting how a match (see {@link TaggingPresetItem#matches}) is performed.
053     */
054    protected enum MatchType {
055
056        /** Neutral, i.e., do not consider this item for matching. */
057        NONE("none"),
058        /** Positive if key matches, neutral otherwise. */
059        KEY("key"),
060        /** Positive if key matches, negative otherwise. */
061        KEY_REQUIRED("key!"),
062        /** Positive if key and value matches, neutral otherwise. */
063        KEY_VALUE("keyvalue"),
064        /** Positive if key and value matches, negative otherwise. */
065        KEY_VALUE_REQUIRED("keyvalue!");
066
067        private final String value;
068
069        MatchType(String value) {
070            this.value = value;
071        }
072
073        /**
074         * Replies the associated textual value.
075         * @return the associated textual value
076         */
077        public String getValue() {
078            return value;
079        }
080
081        /**
082         * Determines the {@code MatchType} for the given textual value.
083         * @param type the textual value
084         * @return the {@code MatchType} for the given textual value
085         */
086        public static MatchType ofString(String type) {
087            for (MatchType i : EnumSet.allOf(MatchType.class)) {
088                if (i.getValue().equals(type))
089                    return i;
090            }
091            throw new IllegalArgumentException(type + " is not allowed");
092        }
093    }
094
095    /**
096     * Usage information on a key
097     */
098    protected static class Usage {
099        /**
100         * A set of values that were used for this key.
101         */
102        public final SortedSet<String> values = new TreeSet<>();; // NOSONAR
103        private boolean hadKeys;
104        private boolean hadEmpty;
105
106        /**
107         * Check if there is exactly one value for this key.
108         * @return <code>true</code> if there was exactly one value.
109         */
110        public boolean hasUniqueValue() {
111            return values.size() == 1 && !hadEmpty;
112        }
113
114        /**
115         * Check if this key was not used in any primitive
116         * @return <code>true</code> if it was unused.
117         */
118        public boolean unused() {
119            return values.isEmpty();
120        }
121
122        /**
123         * Get the first value available.
124         * @return The first value
125         * @throws NoSuchElementException if there is no such value.
126         */
127        public String getFirst() {
128            return values.first();
129        }
130
131        /**
132         * Check if we encountered any primitive that had any keys
133         * @return <code>true</code> if any of the primtives had any tags.
134         */
135        public boolean hadKeys() {
136            return hadKeys;
137        }
138    }
139
140    protected static Usage determineTextUsage(Collection<OsmPrimitive> sel, String key) {
141        Usage returnValue = new Usage();
142        for (OsmPrimitive s : sel) {
143            String v = s.get(key);
144            if (v != null) {
145                returnValue.values.add(v);
146            } else {
147                returnValue.hadEmpty = true;
148            }
149            if (s.hasKeys()) {
150                returnValue.hadKeys = true;
151            }
152        }
153        return returnValue;
154    }
155
156    protected static Usage determineBooleanUsage(Collection<OsmPrimitive> sel, String key) {
157        Usage returnValue = new Usage();
158        for (OsmPrimitive s : sel) {
159            String booleanValue = OsmUtils.getNamedOsmBoolean(s.get(key));
160            if (booleanValue != null) {
161                returnValue.values.add(booleanValue);
162            }
163        }
164        return returnValue;
165    }
166
167    /**
168     * Returns the default match.
169     * @return the default match
170     */
171    public abstract MatchType getDefaultMatch();
172
173    /**
174     * Returns the list of values.
175     * @return the list of values
176     */
177    public abstract Collection<String> getValues();
178
179    protected String getKeyTooltipText() {
180        return tr("This corresponds to the key ''{0}''", key);
181    }
182
183    @Override
184    protected Boolean matches(Map<String, String> tags) {
185        switch (MatchType.ofString(match)) {
186        case NONE:
187            return null;
188        case KEY:
189            return tags.containsKey(key) ? Boolean.TRUE : null;
190        case KEY_REQUIRED:
191            return tags.containsKey(key);
192        case KEY_VALUE:
193            return tags.containsKey(key) && getValues().contains(tags.get(key)) ? Boolean.TRUE : null;
194        case KEY_VALUE_REQUIRED:
195            return tags.containsKey(key) && getValues().contains(tags.get(key));
196        default:
197            throw new IllegalStateException();
198        }
199    }
200
201    @Override
202    public String toString() {
203        return "KeyedItem [key=" + key + ", text=" + text
204                + ", text_context=" + text_context + ", match=" + match
205                + ']';
206    }
207}