// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.tagging.presets.items; import static org.openstreetmap.josm.tools.I18n.tr; import java.util.Collection; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; import java.util.SortedSet; import java.util.TreeSet; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.OsmUtils; import org.openstreetmap.josm.data.preferences.BooleanProperty; import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem; /** * Preset item associated to an OSM key. */ public abstract class KeyedItem extends TaggingPresetItem { /** Translatation of "<different>". Use in combo boxes to display an entry matching several different values. */ protected static final String DIFFERENT = tr("<different>"); protected static final BooleanProperty PROP_FILL_DEFAULT = new BooleanProperty("taggingpreset.fill-default-for-tagged-primitives", false); /** Last value of each key used in presets, used for prefilling corresponding fields */ static final Map<String, String> LAST_VALUES = new HashMap<>(); /** This specifies the property key that will be modified by the item. */ public String key; // NOSONAR /** The text to display */ public String text; // NOSONAR /** The context used for translating {@link #text} */ public String text_context; // NOSONAR /** * Allows to change the matching process, i.e., determining whether the tags of an OSM object fit into this preset. * If a preset fits then it is linked in the Tags/Membership dialog.<ul> * <li>none: neutral, i.e., do not consider this item for matching</li> * <li>key: positive if key matches, neutral otherwise</li> * <li>key!: positive if key matches, negative otherwise</li> * <li>keyvalue: positive if key and value matches, neutral otherwise</li> * <li>keyvalue!: positive if key and value matches, negative otherwise</li></ul> * Note that for a match, at least one positive and no negative is required. * Default is "keyvalue!" for {@link Key} and "none" for {@link Text}, {@link Combo}, {@link MultiSelect} and {@link Check}. */ public String match = getDefaultMatch().getValue(); // NOSONAR /** * Enum denoting how a match (see {@link TaggingPresetItem#matches}) is performed. */ protected enum MatchType { /** Neutral, i.e., do not consider this item for matching. */ NONE("none"), /** Positive if key matches, neutral otherwise. */ KEY("key"), /** Positive if key matches, negative otherwise. */ KEY_REQUIRED("key!"), /** Positive if key and value matches, neutral otherwise. */ KEY_VALUE("keyvalue"), /** Positive if key and value matches, negative otherwise. */ KEY_VALUE_REQUIRED("keyvalue!"); private final String value; MatchType(String value) { this.value = value; } /** * Replies the associated textual value. * @return the associated textual value */ public String getValue() { return value; } /** * Determines the {@code MatchType} for the given textual value. * @param type the textual value * @return the {@code MatchType} for the given textual value */ public static MatchType ofString(String type) { for (MatchType i : EnumSet.allOf(MatchType.class)) { if (i.getValue().equals(type)) return i; } throw new IllegalArgumentException(type + " is not allowed"); } } /** * Usage information on a key */ protected static class Usage { /** * A set of values that were used for this key. */ public final SortedSet<String> values = new TreeSet<>();; // NOSONAR private boolean hadKeys; private boolean hadEmpty; /** * Check if there is exactly one value for this key. * @return <code>true</code> if there was exactly one value. */ public boolean hasUniqueValue() { return values.size() == 1 && !hadEmpty; } /** * Check if this key was not used in any primitive * @return <code>true</code> if it was unused. */ public boolean unused() { return values.isEmpty(); } /** * Get the first value available. * @return The first value * @throws NoSuchElementException if there is no such value. */ public String getFirst() { return values.first(); } /** * Check if we encountered any primitive that had any keys * @return <code>true</code> if any of the primtives had any tags. */ public boolean hadKeys() { return hadKeys; } } protected static Usage determineTextUsage(Collection<OsmPrimitive> sel, String key) { Usage returnValue = new Usage(); for (OsmPrimitive s : sel) { String v = s.get(key); if (v != null) { returnValue.values.add(v); } else { returnValue.hadEmpty = true; } if (s.hasKeys()) { returnValue.hadKeys = true; } } return returnValue; } protected static Usage determineBooleanUsage(Collection<OsmPrimitive> sel, String key) { Usage returnValue = new Usage(); for (OsmPrimitive s : sel) { String booleanValue = OsmUtils.getNamedOsmBoolean(s.get(key)); if (booleanValue != null) { returnValue.values.add(booleanValue); } } return returnValue; } /** * Returns the default match. * @return the default match */ public abstract MatchType getDefaultMatch(); /** * Returns the list of values. * @return the list of values */ public abstract Collection<String> getValues(); protected String getKeyTooltipText() { return tr("This corresponds to the key ''{0}''", key); } @Override protected Boolean matches(Map<String, String> tags) { switch (MatchType.ofString(match)) { case NONE: return null; case KEY: return tags.containsKey(key) ? Boolean.TRUE : null; case KEY_REQUIRED: return tags.containsKey(key); case KEY_VALUE: return tags.containsKey(key) && getValues().contains(tags.get(key)) ? Boolean.TRUE : null; case KEY_VALUE_REQUIRED: return tags.containsKey(key) && getValues().contains(tags.get(key)); default: throw new IllegalStateException(); } } @Override public String toString() { return "KeyedItem [key=" + key + ", text=" + text + ", text_context=" + text_context + ", match=" + match + ']'; } }