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