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}