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 * Determines whether key or key+value are required. 169 * @return whether key or key+value are required 170 */ 171 public boolean isKeyRequired() { 172 final MatchType type = MatchType.ofString(match); 173 return MatchType.KEY_REQUIRED == type || MatchType.KEY_VALUE_REQUIRED == type; 174 } 175 176 /** 177 * Returns the default match. 178 * @return the default match 179 */ 180 public abstract MatchType getDefaultMatch(); 181 182 /** 183 * Returns the list of values. 184 * @return the list of values 185 */ 186 public abstract Collection<String> getValues(); 187 188 protected String getKeyTooltipText() { 189 return tr("This corresponds to the key ''{0}''", key); 190 } 191 192 @Override 193 protected Boolean matches(Map<String, String> tags) { 194 switch (MatchType.ofString(match)) { 195 case NONE: 196 return null; 197 case KEY: 198 return tags.containsKey(key) ? Boolean.TRUE : null; 199 case KEY_REQUIRED: 200 return tags.containsKey(key); 201 case KEY_VALUE: 202 return tags.containsKey(key) && getValues().contains(tags.get(key)) ? Boolean.TRUE : null; 203 case KEY_VALUE_REQUIRED: 204 return tags.containsKey(key) && getValues().contains(tags.get(key)); 205 default: 206 throw new IllegalStateException(); 207 } 208 } 209 210 @Override 211 public String toString() { 212 return "KeyedItem [key=" + key + ", text=" + text 213 + ", text_context=" + text_context + ", match=" + match 214 + ']'; 215 } 216}