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.SortedMap;
011import java.util.NoSuchElementException;
012import java.util.TreeMap;
013
014import javax.swing.JPopupMenu;
015
016import org.openstreetmap.josm.data.osm.OsmPrimitive;
017import org.openstreetmap.josm.data.osm.OsmUtils;
018import org.openstreetmap.josm.data.osm.Tag;
019import org.openstreetmap.josm.data.preferences.BooleanProperty;
020import org.openstreetmap.josm.gui.dialogs.properties.HelpTagAction;
021import org.openstreetmap.josm.gui.dialogs.properties.TaginfoAction;
022import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
023
024/**
025 * Preset item associated to an OSM key.
026 */
027public abstract class KeyedItem extends TextItem {
028
029    /** The constant value {@code "<different>"}. */
030    protected static final String DIFFERENT = "<different>";
031    /** Translation of {@code "<different>"}. */
032    protected static final String DIFFERENT_I18N = tr(DIFFERENT);
033
034    /** True if the default value should also be set on primitives that already have tags.  */
035    protected static final BooleanProperty PROP_FILL_DEFAULT = new BooleanProperty("taggingpreset.fill-default-for-tagged-primitives", false);
036
037    /** Last value of each key used in presets, used for prefilling corresponding fields */
038    static final Map<String, String> LAST_VALUES = new HashMap<>();
039
040    /** This specifies the property key that will be modified by the item. */
041    public String key; // NOSONAR
042    /**
043     * Allows to change the matching process, i.e., determining whether the tags of an OSM object fit into this preset.
044     * If a preset fits then it is linked in the Tags/Membership dialog.<ul>
045     * <li>none: neutral, i.e., do not consider this item for matching</li>
046     * <li>key: positive if key matches, neutral otherwise</li>
047     * <li>key!: positive if key matches, negative otherwise</li>
048     * <li>keyvalue: positive if key and value matches, neutral otherwise</li>
049     * <li>keyvalue!: positive if key and value matches, negative otherwise</li></ul>
050     * Note that for a match, at least one positive and no negative is required.
051     * Default is "keyvalue!" for {@link Key} and "none" for {@link Text}, {@link Combo}, {@link MultiSelect} and {@link Check}.
052     */
053    public String match = getDefaultMatch().getValue(); // NOSONAR
054
055    /**
056     * Enum denoting how a match (see {@link TaggingPresetItem#matches}) is performed.
057     */
058    protected enum MatchType {
059
060        /** Neutral, i.e., do not consider this item for matching. */
061        NONE("none"),
062        /** Positive if key matches, neutral otherwise. */
063        KEY("key"),
064        /** Positive if key matches, negative otherwise. */
065        KEY_REQUIRED("key!"),
066        /** Positive if key and value matches, neutral otherwise. */
067        KEY_VALUE("keyvalue"),
068        /** Positive if key and value matches, negative otherwise. */
069        KEY_VALUE_REQUIRED("keyvalue!");
070
071        private final String value;
072
073        MatchType(String value) {
074            this.value = value;
075        }
076
077        /**
078         * Replies the associated textual value.
079         * @return the associated textual value
080         */
081        public String getValue() {
082            return value;
083        }
084
085        /**
086         * Determines the {@code MatchType} for the given textual value.
087         * @param type the textual value
088         * @return the {@code MatchType} for the given textual value
089         */
090        public static MatchType ofString(String type) {
091            for (MatchType i : EnumSet.allOf(MatchType.class)) {
092                if (i.getValue().equals(type))
093                    return i;
094            }
095            throw new IllegalArgumentException(type + " is not allowed");
096        }
097    }
098
099    /**
100     * Usage information on a key
101     *
102     * TODO merge with {@link org.openstreetmap.josm.data.osm.TagCollection}
103     */
104    public static class Usage {
105        /** Usage count for all values used for this key */
106        public final SortedMap<String, Integer> map = new TreeMap<>();
107        private boolean hadKeys;
108        private boolean hadEmpty;
109        private int selectedCount;
110
111        /**
112         * Check if there is exactly one value for this key.
113         * @return <code>true</code> if there was exactly one value.
114         */
115        public boolean hasUniqueValue() {
116            return map.size() == 1 && !hadEmpty;
117        }
118
119        /**
120         * Check if this key was not used in any primitive
121         * @return <code>true</code> if it was unused.
122         */
123        public boolean unused() {
124            return map.isEmpty();
125        }
126
127        /**
128         * Get the first value available.
129         * @return The first value
130         * @throws NoSuchElementException if there is no such value.
131         */
132        public String getFirst() {
133            return map.firstKey();
134        }
135
136        /**
137         * Check if we encountered any primitive that had any keys
138         * @return <code>true</code> if any of the primitives had any tags.
139         */
140        public boolean hadKeys() {
141            return hadKeys;
142        }
143
144        /**
145         * Returns the number of primitives selected.
146         * @return the number of primitives selected.
147         */
148        public int getSelectedCount() {
149            return selectedCount;
150        }
151
152        /**
153         * Splits multiple values and adds their usage counts as single value.
154         * <p>
155         * A value of {@code regional;pizza} will increment the count of {@code regional} and of
156         * {@code pizza}.
157         * @param delimiter The delimiter used for splitting.
158         * @return A new usage object with the new counts.
159         */
160        public Usage splitValues(String delimiter) {
161            Usage usage = new Usage();
162            usage.hadEmpty = hadEmpty;
163            usage.hadKeys = hadKeys;
164            usage.selectedCount = selectedCount;
165            map.forEach((value, count) -> {
166                for (String v : value.split(String.valueOf(delimiter), -1)) {
167                    usage.map.merge(v, count, Integer::sum);
168                }
169            });
170            return usage;
171        }
172    }
173
174    /**
175     * Computes the tag usage for the given key from the given primitives
176     * @param sel the primitives
177     * @param key the key
178     * @return the tag usage
179     */
180    public static Usage determineTextUsage(Collection<OsmPrimitive> sel, String key) {
181        Usage returnValue = new Usage();
182        returnValue.selectedCount = sel.size();
183        for (OsmPrimitive s : sel) {
184            String v = s.get(key);
185            if (v != null) {
186                returnValue.map.merge(v, 1, Integer::sum);
187            } else {
188                returnValue.hadEmpty = true;
189            }
190            if (s.hasKeys()) {
191                returnValue.hadKeys = true;
192            }
193        }
194        return returnValue;
195    }
196
197    protected static Usage determineBooleanUsage(Collection<OsmPrimitive> sel, String key) {
198        Usage returnValue = new Usage();
199        returnValue.selectedCount = sel.size();
200        for (OsmPrimitive s : sel) {
201            String booleanValue = OsmUtils.getNamedOsmBoolean(s.get(key));
202            if (booleanValue != null) {
203                returnValue.map.merge(booleanValue, 1, Integer::sum);
204            }
205        }
206        return returnValue;
207    }
208
209    /**
210     * Determines whether key or key+value are required.
211     * @return whether key or key+value are required
212     */
213    public boolean isKeyRequired() {
214        final MatchType type = MatchType.ofString(match);
215        return MatchType.KEY_REQUIRED == type || MatchType.KEY_VALUE_REQUIRED == type;
216    }
217
218    /**
219     * Returns the default match.
220     * @return the default match
221     */
222    public abstract MatchType getDefaultMatch();
223
224    /**
225     * Returns the list of values.
226     * @return the list of values
227     */
228    public abstract Collection<String> getValues();
229
230    protected String getKeyTooltipText() {
231        return tr("This corresponds to the key ''{0}''", key);
232    }
233
234    @Override
235    public Boolean matches(Map<String, String> tags) {
236        switch (MatchType.ofString(match)) {
237        case NONE:
238            return null; // NOSONAR
239        case KEY:
240            return tags.containsKey(key) ? Boolean.TRUE : null;
241        case KEY_REQUIRED:
242            return tags.containsKey(key);
243        case KEY_VALUE:
244            return tags.containsKey(key) && getValues().contains(tags.get(key)) ? Boolean.TRUE : null;
245        case KEY_VALUE_REQUIRED:
246            return tags.containsKey(key) && getValues().contains(tags.get(key));
247        default:
248            throw new IllegalStateException();
249        }
250    }
251
252    protected JPopupMenu getPopupMenu() {
253        Tag tag = new Tag(key, null);
254        JPopupMenu popupMenu = new JPopupMenu();
255        popupMenu.add(tr("Key: {0}", key)).setEnabled(false);
256        popupMenu.add(new HelpTagAction(() -> tag));
257        TaginfoAction taginfoAction = new TaginfoAction(() -> tag, () -> null);
258        popupMenu.add(taginfoAction.toTagHistoryAction());
259        popupMenu.add(taginfoAction);
260        return popupMenu;
261    }
262
263    @Override
264    public String toString() {
265        return "KeyedItem [key=" + key + ", text=" + text
266                + ", text_context=" + text_context + ", match=" + match
267                + ']';
268    }
269}