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