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