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