001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.tagging.ac; 003 004import java.util.ArrayList; 005import java.util.Arrays; 006import java.util.Collection; 007import java.util.Collections; 008import java.util.HashSet; 009import java.util.LinkedHashSet; 010import java.util.List; 011import java.util.Map; 012import java.util.Map.Entry; 013import java.util.Objects; 014import java.util.Set; 015 016import org.openstreetmap.josm.Main; 017import org.openstreetmap.josm.data.osm.DataSet; 018import org.openstreetmap.josm.data.osm.OsmPrimitive; 019import org.openstreetmap.josm.data.osm.Relation; 020import org.openstreetmap.josm.data.osm.RelationMember; 021import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; 022import org.openstreetmap.josm.data.osm.event.DataChangedEvent; 023import org.openstreetmap.josm.data.osm.event.DataSetListener; 024import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; 025import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; 026import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; 027import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; 028import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; 029import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; 030import org.openstreetmap.josm.gui.tagging.TaggingPreset; 031import org.openstreetmap.josm.gui.tagging.TaggingPresetItem; 032import org.openstreetmap.josm.gui.tagging.TaggingPresetItems; 033import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Role; 034import org.openstreetmap.josm.tools.CheckParameterUtil; 035import org.openstreetmap.josm.tools.MultiMap; 036import org.openstreetmap.josm.tools.Utils; 037 038/** 039 * AutoCompletionManager holds a cache of keys with a list of 040 * possible auto completion values for each key. 041 * 042 * Each DataSet is assigned one AutoCompletionManager instance such that 043 * <ol> 044 * <li>any key used in a tag in the data set is part of the key list in the cache</li> 045 * <li>any value used in a tag for a specific key is part of the autocompletion list of 046 * this key</li> 047 * </ol> 048 * 049 * Building up auto completion lists should not 050 * slow down tabbing from input field to input field. Looping through the complete 051 * data set in order to build up the auto completion list for a specific input 052 * field is not efficient enough, hence this cache. 053 * 054 * TODO: respect the relation type for member role autocompletion 055 */ 056public class AutoCompletionManager implements DataSetListener { 057 058 /** 059 * Data class to remember tags that the user has entered. 060 */ 061 public static class UserInputTag { 062 private final String key; 063 private final String value; 064 private final boolean defaultKey; 065 066 /** 067 * Constructor. 068 * 069 * @param key the tag key 070 * @param value the tag value 071 * @param defaultKey true, if the key was not really entered by the 072 * user, e.g. for preset text fields. 073 * In this case, the key will not get any higher priority, just the value. 074 */ 075 public UserInputTag(String key, String value, boolean defaultKey) { 076 this.key = key; 077 this.value = value; 078 this.defaultKey = defaultKey; 079 } 080 081 @Override 082 public int hashCode() { 083 int hash = 7; 084 hash = 59 * hash + Objects.hashCode(this.key); 085 hash = 59 * hash + Objects.hashCode(this.value); 086 hash = 59 * hash + (this.defaultKey ? 1 : 0); 087 return hash; 088 } 089 090 @Override 091 public boolean equals(Object obj) { 092 if (obj == null || getClass() != obj.getClass()) { 093 return false; 094 } 095 final UserInputTag other = (UserInputTag) obj; 096 return Objects.equals(this.key, other.key) 097 && Objects.equals(this.value, other.value) 098 && this.defaultKey == other.defaultKey; 099 } 100 } 101 102 /** If the dirty flag is set true, a rebuild is necessary. */ 103 protected boolean dirty; 104 /** The data set that is managed */ 105 protected DataSet ds; 106 107 /** 108 * the cached tags given by a tag key and a list of values for this tag 109 * only accessed by getTagCache(), rebuild() and cachePrimitiveTags() 110 * use getTagCache() accessor 111 */ 112 protected MultiMap<String, String> tagCache; 113 114 /** 115 * the same as tagCache but for the preset keys and values can be accessed directly 116 */ 117 protected static final MultiMap<String, String> PRESET_TAG_CACHE = new MultiMap<>(); 118 119 /** 120 * Cache for tags that have been entered by the user. 121 */ 122 protected static final Set<UserInputTag> USER_INPUT_TAG_CACHE = new LinkedHashSet<>(); 123 124 /** 125 * the cached list of member roles 126 * only accessed by getRoleCache(), rebuild() and cacheRelationMemberRoles() 127 * use getRoleCache() accessor 128 */ 129 protected Set<String> roleCache; 130 131 /** 132 * the same as roleCache but for the preset roles can be accessed directly 133 */ 134 protected static final Set<String> PRESET_ROLE_CACHE = new HashSet<>(); 135 136 /** 137 * Constructs a new {@code AutoCompletionManager}. 138 * @param ds data set 139 */ 140 public AutoCompletionManager(DataSet ds) { 141 this.ds = ds; 142 this.dirty = true; 143 } 144 145 protected MultiMap<String, String> getTagCache() { 146 if (dirty) { 147 rebuild(); 148 dirty = false; 149 } 150 return tagCache; 151 } 152 153 protected Set<String> getRoleCache() { 154 if (dirty) { 155 rebuild(); 156 dirty = false; 157 } 158 return roleCache; 159 } 160 161 /** 162 * initializes the cache from the primitives in the dataset 163 */ 164 protected void rebuild() { 165 tagCache = new MultiMap<>(); 166 roleCache = new HashSet<>(); 167 cachePrimitives(ds.allNonDeletedCompletePrimitives()); 168 } 169 170 protected void cachePrimitives(Collection<? extends OsmPrimitive> primitives) { 171 for (OsmPrimitive primitive : primitives) { 172 cachePrimitiveTags(primitive); 173 if (primitive instanceof Relation) { 174 cacheRelationMemberRoles((Relation) primitive); 175 } 176 } 177 } 178 179 /** 180 * make sure, the keys and values of all tags held by primitive are 181 * in the auto completion cache 182 * 183 * @param primitive an OSM primitive 184 */ 185 protected void cachePrimitiveTags(OsmPrimitive primitive) { 186 for (String key: primitive.keySet()) { 187 String value = primitive.get(key); 188 tagCache.put(key, value); 189 } 190 } 191 192 /** 193 * Caches all member roles of the relation <code>relation</code> 194 * 195 * @param relation the relation 196 */ 197 protected void cacheRelationMemberRoles(Relation relation){ 198 for (RelationMember m: relation.getMembers()) { 199 if (m.hasRole()) { 200 roleCache.add(m.getRole()); 201 } 202 } 203 } 204 205 /** 206 * Initialize the cache for presets. This is done only once. 207 * @param presets Tagging presets to cache 208 */ 209 public static void cachePresets(Collection<TaggingPreset> presets) { 210 for (final TaggingPreset p : presets) { 211 for (TaggingPresetItem item : p.data) { 212 if (item instanceof TaggingPresetItems.KeyedItem) { 213 TaggingPresetItems.KeyedItem ki = (TaggingPresetItems.KeyedItem) item; 214 if (ki.key != null && ki.getValues() != null) { 215 try { 216 PRESET_TAG_CACHE.putAll(ki.key, ki.getValues()); 217 } catch (NullPointerException e) { 218 Main.error(p+": Unable to cache "+ki); 219 } 220 } 221 } else if (item instanceof TaggingPresetItems.Roles) { 222 TaggingPresetItems.Roles r = (TaggingPresetItems.Roles) item; 223 for (TaggingPresetItems.Role i : r.roles) { 224 if (i.key != null) { 225 PRESET_ROLE_CACHE.add(i.key); 226 } 227 } 228 } 229 } 230 } 231 } 232 233 /** 234 * Remembers user input for the given key/value. 235 * @param key Tag key 236 * @param value Tag value 237 * @param defaultKey true, if the key was not really entered by the user, e.g. for preset text fields 238 */ 239 public static void rememberUserInput(String key, String value, boolean defaultKey) { 240 UserInputTag tag = new UserInputTag(key, value, defaultKey); 241 USER_INPUT_TAG_CACHE.remove(tag); // re-add, so it gets to the last position of the LinkedHashSet 242 USER_INPUT_TAG_CACHE.add(tag); 243 } 244 245 /** 246 * replies the keys held by the cache 247 * 248 * @return the list of keys held by the cache 249 */ 250 protected List<String> getDataKeys() { 251 return new ArrayList<>(getTagCache().keySet()); 252 } 253 254 protected List<String> getPresetKeys() { 255 return new ArrayList<>(PRESET_TAG_CACHE.keySet()); 256 } 257 258 protected Collection<String> getUserInputKeys() { 259 List<String> keys = new ArrayList<>(); 260 for (UserInputTag tag : USER_INPUT_TAG_CACHE) { 261 if (!tag.defaultKey) { 262 keys.add(tag.key); 263 } 264 } 265 Collections.reverse(keys); 266 return new LinkedHashSet<>(keys); 267 } 268 269 /** 270 * replies the auto completion values allowed for a specific key. Replies 271 * an empty list if key is null or if key is not in {@link #getKeys()}. 272 * 273 * @param key 274 * @return the list of auto completion values 275 */ 276 protected List<String> getDataValues(String key) { 277 return new ArrayList<>(getTagCache().getValues(key)); 278 } 279 280 protected static List<String> getPresetValues(String key) { 281 return new ArrayList<>(PRESET_TAG_CACHE.getValues(key)); 282 } 283 284 protected static Collection<String> getUserInputValues(String key) { 285 List<String> values = new ArrayList<>(); 286 for (UserInputTag tag : USER_INPUT_TAG_CACHE) { 287 if (key.equals(tag.key)) { 288 values.add(tag.value); 289 } 290 } 291 Collections.reverse(values); 292 return new LinkedHashSet<>(values); 293 } 294 295 /** 296 * Replies the list of member roles 297 * 298 * @return the list of member roles 299 */ 300 public List<String> getMemberRoles() { 301 return new ArrayList<>(getRoleCache()); 302 } 303 304 /** 305 * Populates the {@link AutoCompletionList} with the currently cached 306 * member roles. 307 * 308 * @param list the list to populate 309 */ 310 public void populateWithMemberRoles(AutoCompletionList list) { 311 list.add(PRESET_ROLE_CACHE, AutoCompletionItemPriority.IS_IN_STANDARD); 312 list.add(getRoleCache(), AutoCompletionItemPriority.IS_IN_DATASET); 313 } 314 315 /** 316 * Populates the {@link AutoCompletionList} with the roles used in this relation 317 * plus the ones defined in its applicable presets, if any. If the relation type is unknown, 318 * then all the roles known globally will be added, as in {@link #populateWithMemberRoles(AutoCompletionList)}. 319 * 320 * @param list the list to populate 321 * @param r the relation to get roles from 322 * @throws IllegalArgumentException if list is null 323 * @since 7556 324 */ 325 public void populateWithMemberRoles(AutoCompletionList list, Relation r) { 326 CheckParameterUtil.ensureParameterNotNull(list, "list"); 327 Collection<TaggingPreset> presets = r != null ? TaggingPreset.getMatchingPresets(null, r.getKeys(), false) : null; 328 if (r != null && presets != null && !presets.isEmpty()) { 329 for (TaggingPreset tp : presets) { 330 if (tp.roles != null) { 331 list.add(Utils.transform(tp.roles.roles, new Utils.Function<Role, String>() { 332 public String apply(Role x) { 333 return x.key; 334 } 335 }), AutoCompletionItemPriority.IS_IN_STANDARD); 336 } 337 } 338 list.add(r.getMemberRoles(), AutoCompletionItemPriority.IS_IN_DATASET); 339 } else { 340 populateWithMemberRoles(list); 341 } 342 } 343 344 /** 345 * Populates the an {@link AutoCompletionList} with the currently cached tag keys 346 * 347 * @param list the list to populate 348 */ 349 public void populateWithKeys(AutoCompletionList list) { 350 list.add(getPresetKeys(), AutoCompletionItemPriority.IS_IN_STANDARD); 351 list.add(new AutoCompletionListItem("source", AutoCompletionItemPriority.IS_IN_STANDARD)); 352 list.add(getDataKeys(), AutoCompletionItemPriority.IS_IN_DATASET); 353 list.addUserInput(getUserInputKeys()); 354 } 355 356 /** 357 * Populates the an {@link AutoCompletionList} with the currently cached 358 * values for a tag 359 * 360 * @param list the list to populate 361 * @param key the tag key 362 */ 363 public void populateWithTagValues(AutoCompletionList list, String key) { 364 populateWithTagValues(list, Arrays.asList(key)); 365 } 366 367 /** 368 * Populates the an {@link AutoCompletionList} with the currently cached 369 * values for some given tags 370 * 371 * @param list the list to populate 372 * @param keys the tag keys 373 */ 374 public void populateWithTagValues(AutoCompletionList list, List<String> keys) { 375 for (String key : keys) { 376 list.add(getPresetValues(key), AutoCompletionItemPriority.IS_IN_STANDARD); 377 list.add(getDataValues(key), AutoCompletionItemPriority.IS_IN_DATASET); 378 list.addUserInput(getUserInputValues(key)); 379 } 380 } 381 382 /** 383 * Returns the currently cached tag keys. 384 * @return a list of tag keys 385 */ 386 public List<AutoCompletionListItem> getKeys() { 387 AutoCompletionList list = new AutoCompletionList(); 388 populateWithKeys(list); 389 return list.getList(); 390 } 391 392 /** 393 * Returns the currently cached tag values for a given tag key. 394 * @param key the tag key 395 * @return a list of tag values 396 */ 397 public List<AutoCompletionListItem> getValues(String key) { 398 return getValues(Arrays.asList(key)); 399 } 400 401 /** 402 * Returns the currently cached tag values for a given list of tag keys. 403 * @param keys the tag keys 404 * @return a list of tag values 405 */ 406 public List<AutoCompletionListItem> getValues(List<String> keys) { 407 AutoCompletionList list = new AutoCompletionList(); 408 populateWithTagValues(list, keys); 409 return list.getList(); 410 } 411 412 /********************************************************* 413 * Implementation of the DataSetListener interface 414 * 415 **/ 416 417 @Override 418 public void primitivesAdded(PrimitivesAddedEvent event) { 419 if (dirty) 420 return; 421 cachePrimitives(event.getPrimitives()); 422 } 423 424 @Override 425 public void primitivesRemoved(PrimitivesRemovedEvent event) { 426 dirty = true; 427 } 428 429 @Override 430 public void tagsChanged(TagsChangedEvent event) { 431 if (dirty) 432 return; 433 Map<String, String> newKeys = event.getPrimitive().getKeys(); 434 Map<String, String> oldKeys = event.getOriginalKeys(); 435 436 if (!newKeys.keySet().containsAll(oldKeys.keySet())) { 437 // Some keys removed, might be the last instance of key, rebuild necessary 438 dirty = true; 439 } else { 440 for (Entry<String, String> oldEntry: oldKeys.entrySet()) { 441 if (!oldEntry.getValue().equals(newKeys.get(oldEntry.getKey()))) { 442 // Value changed, might be last instance of value, rebuild necessary 443 dirty = true; 444 return; 445 } 446 } 447 cachePrimitives(Collections.singleton(event.getPrimitive())); 448 } 449 } 450 451 @Override 452 public void nodeMoved(NodeMovedEvent event) {/* ignored */} 453 454 @Override 455 public void wayNodesChanged(WayNodesChangedEvent event) {/* ignored */} 456 457 @Override 458 public void relationMembersChanged(RelationMembersChangedEvent event) { 459 dirty = true; // TODO: not necessary to rebuid if a member is added 460 } 461 462 @Override 463 public void otherDatasetChange(AbstractDatasetChangedEvent event) {/* ignored */} 464 465 @Override 466 public void dataChanged(DataChangedEvent event) { 467 dirty = true; 468 } 469}