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; 015import java.util.function.Function; 016 017import org.openstreetmap.josm.Main; 018import org.openstreetmap.josm.data.osm.DataSet; 019import org.openstreetmap.josm.data.osm.OsmPrimitive; 020import org.openstreetmap.josm.data.osm.Relation; 021import org.openstreetmap.josm.data.osm.RelationMember; 022import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; 023import org.openstreetmap.josm.data.osm.event.DataChangedEvent; 024import org.openstreetmap.josm.data.osm.event.DataSetListener; 025import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; 026import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; 027import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; 028import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; 029import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; 030import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; 031import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset; 032import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem; 033import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets; 034import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup; 035import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem; 036import org.openstreetmap.josm.gui.tagging.presets.items.Roles; 037import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role; 038import org.openstreetmap.josm.tools.CheckParameterUtil; 039import org.openstreetmap.josm.tools.MultiMap; 040import org.openstreetmap.josm.tools.Utils; 041 042/** 043 * AutoCompletionManager holds a cache of keys with a list of 044 * possible auto completion values for each key. 045 * 046 * Each DataSet is assigned one AutoCompletionManager instance such that 047 * <ol> 048 * <li>any key used in a tag in the data set is part of the key list in the cache</li> 049 * <li>any value used in a tag for a specific key is part of the autocompletion list of 050 * this key</li> 051 * </ol> 052 * 053 * Building up auto completion lists should not 054 * slow down tabbing from input field to input field. Looping through the complete 055 * data set in order to build up the auto completion list for a specific input 056 * field is not efficient enough, hence this cache. 057 * 058 * TODO: respect the relation type for member role autocompletion 059 */ 060public class AutoCompletionManager implements DataSetListener { 061 062 /** 063 * Data class to remember tags that the user has entered. 064 */ 065 public static class UserInputTag { 066 private final String key; 067 private final String value; 068 private final boolean defaultKey; 069 070 /** 071 * Constructor. 072 * 073 * @param key the tag key 074 * @param value the tag value 075 * @param defaultKey true, if the key was not really entered by the 076 * user, e.g. for preset text fields. 077 * In this case, the key will not get any higher priority, just the value. 078 */ 079 public UserInputTag(String key, String value, boolean defaultKey) { 080 this.key = key; 081 this.value = value; 082 this.defaultKey = defaultKey; 083 } 084 085 @Override 086 public int hashCode() { 087 return Objects.hash(key, value, defaultKey); 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 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 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 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 cachePresetItem(p, item); 213 } 214 } 215 } 216 217 protected static void cachePresetItem(TaggingPreset p, TaggingPresetItem item) { 218 if (item instanceof KeyedItem) { 219 KeyedItem ki = (KeyedItem) item; 220 if (ki.key != null && ki.getValues() != null) { 221 try { 222 PRESET_TAG_CACHE.putAll(ki.key, ki.getValues()); 223 } catch (NullPointerException e) { 224 Main.error(e, p + ": Unable to cache " + ki); 225 } 226 } 227 } else if (item instanceof Roles) { 228 Roles r = (Roles) item; 229 for (Role i : r.roles) { 230 if (i.key != null) { 231 PRESET_ROLE_CACHE.add(i.key); 232 } 233 } 234 } else if (item instanceof CheckGroup) { 235 for (KeyedItem check : ((CheckGroup) item).checks) { 236 cachePresetItem(p, check); 237 } 238 } 239 } 240 241 /** 242 * Remembers user input for the given key/value. 243 * @param key Tag key 244 * @param value Tag value 245 * @param defaultKey true, if the key was not really entered by the user, e.g. for preset text fields 246 */ 247 public static void rememberUserInput(String key, String value, boolean defaultKey) { 248 UserInputTag tag = new UserInputTag(key, value, defaultKey); 249 USER_INPUT_TAG_CACHE.remove(tag); // re-add, so it gets to the last position of the LinkedHashSet 250 USER_INPUT_TAG_CACHE.add(tag); 251 } 252 253 /** 254 * replies the keys held by the cache 255 * 256 * @return the list of keys held by the cache 257 */ 258 protected List<String> getDataKeys() { 259 return new ArrayList<>(getTagCache().keySet()); 260 } 261 262 protected List<String> getPresetKeys() { 263 return new ArrayList<>(PRESET_TAG_CACHE.keySet()); 264 } 265 266 protected Collection<String> getUserInputKeys() { 267 List<String> keys = new ArrayList<>(); 268 for (UserInputTag tag : USER_INPUT_TAG_CACHE) { 269 if (!tag.defaultKey) { 270 keys.add(tag.key); 271 } 272 } 273 Collections.reverse(keys); 274 return new LinkedHashSet<>(keys); 275 } 276 277 /** 278 * replies the auto completion values allowed for a specific key. Replies 279 * an empty list if key is null or if key is not in {@link #getKeys()}. 280 * 281 * @param key OSM key 282 * @return the list of auto completion values 283 */ 284 protected List<String> getDataValues(String key) { 285 return new ArrayList<>(getTagCache().getValues(key)); 286 } 287 288 protected static List<String> getPresetValues(String key) { 289 return new ArrayList<>(PRESET_TAG_CACHE.getValues(key)); 290 } 291 292 protected static Collection<String> getUserInputValues(String key) { 293 List<String> values = new ArrayList<>(); 294 for (UserInputTag tag : USER_INPUT_TAG_CACHE) { 295 if (key.equals(tag.key)) { 296 values.add(tag.value); 297 } 298 } 299 Collections.reverse(values); 300 return new LinkedHashSet<>(values); 301 } 302 303 /** 304 * Replies the list of member roles 305 * 306 * @return the list of member roles 307 */ 308 public List<String> getMemberRoles() { 309 return new ArrayList<>(getRoleCache()); 310 } 311 312 /** 313 * Populates the {@link AutoCompletionList} with the currently cached 314 * member roles. 315 * 316 * @param list the list to populate 317 */ 318 public void populateWithMemberRoles(AutoCompletionList list) { 319 list.add(PRESET_ROLE_CACHE, AutoCompletionItemPriority.IS_IN_STANDARD); 320 list.add(getRoleCache(), AutoCompletionItemPriority.IS_IN_DATASET); 321 } 322 323 /** 324 * Populates the {@link AutoCompletionList} with the roles used in this relation 325 * plus the ones defined in its applicable presets, if any. If the relation type is unknown, 326 * then all the roles known globally will be added, as in {@link #populateWithMemberRoles(AutoCompletionList)}. 327 * 328 * @param list the list to populate 329 * @param r the relation to get roles from 330 * @throws IllegalArgumentException if list is null 331 * @since 7556 332 */ 333 public void populateWithMemberRoles(AutoCompletionList list, Relation r) { 334 CheckParameterUtil.ensureParameterNotNull(list, "list"); 335 Collection<TaggingPreset> presets = r != null ? TaggingPresets.getMatchingPresets(null, r.getKeys(), false) : null; 336 if (r != null && presets != null && !presets.isEmpty()) { 337 for (TaggingPreset tp : presets) { 338 if (tp.roles != null) { 339 list.add(Utils.transform(tp.roles.roles, (Function<Role, String>) x -> x.key), AutoCompletionItemPriority.IS_IN_STANDARD); 340 } 341 } 342 list.add(r.getMemberRoles(), AutoCompletionItemPriority.IS_IN_DATASET); 343 } else { 344 populateWithMemberRoles(list); 345 } 346 } 347 348 /** 349 * Populates the an {@link AutoCompletionList} with the currently cached tag keys 350 * 351 * @param list the list to populate 352 */ 353 public void populateWithKeys(AutoCompletionList list) { 354 list.add(getPresetKeys(), AutoCompletionItemPriority.IS_IN_STANDARD); 355 list.add(new AutoCompletionListItem("source", AutoCompletionItemPriority.IS_IN_STANDARD)); 356 list.add(getDataKeys(), AutoCompletionItemPriority.IS_IN_DATASET); 357 list.addUserInput(getUserInputKeys()); 358 } 359 360 /** 361 * Populates the an {@link AutoCompletionList} with the currently cached 362 * values for a tag 363 * 364 * @param list the list to populate 365 * @param key the tag key 366 */ 367 public void populateWithTagValues(AutoCompletionList list, String key) { 368 populateWithTagValues(list, Arrays.asList(key)); 369 } 370 371 /** 372 * Populates the an {@link AutoCompletionList} with the currently cached 373 * values for some given tags 374 * 375 * @param list the list to populate 376 * @param keys the tag keys 377 */ 378 public void populateWithTagValues(AutoCompletionList list, List<String> keys) { 379 for (String key : keys) { 380 list.add(getPresetValues(key), AutoCompletionItemPriority.IS_IN_STANDARD); 381 list.add(getDataValues(key), AutoCompletionItemPriority.IS_IN_DATASET); 382 list.addUserInput(getUserInputValues(key)); 383 } 384 } 385 386 /** 387 * Returns the currently cached tag keys. 388 * @return a list of tag keys 389 */ 390 public List<AutoCompletionListItem> getKeys() { 391 AutoCompletionList list = new AutoCompletionList(); 392 populateWithKeys(list); 393 return list.getList(); 394 } 395 396 /** 397 * Returns the currently cached tag values for a given tag key. 398 * @param key the tag key 399 * @return a list of tag values 400 */ 401 public List<AutoCompletionListItem> getValues(String key) { 402 return getValues(Arrays.asList(key)); 403 } 404 405 /** 406 * Returns the currently cached tag values for a given list of tag keys. 407 * @param keys the tag keys 408 * @return a list of tag values 409 */ 410 public List<AutoCompletionListItem> getValues(List<String> keys) { 411 AutoCompletionList list = new AutoCompletionList(); 412 populateWithTagValues(list, keys); 413 return list.getList(); 414 } 415 416 /********************************************************* 417 * Implementation of the DataSetListener interface 418 * 419 **/ 420 421 @Override 422 public void primitivesAdded(PrimitivesAddedEvent event) { 423 if (dirty) 424 return; 425 cachePrimitives(event.getPrimitives()); 426 } 427 428 @Override 429 public void primitivesRemoved(PrimitivesRemovedEvent event) { 430 dirty = true; 431 } 432 433 @Override 434 public void tagsChanged(TagsChangedEvent event) { 435 if (dirty) 436 return; 437 Map<String, String> newKeys = event.getPrimitive().getKeys(); 438 Map<String, String> oldKeys = event.getOriginalKeys(); 439 440 if (!newKeys.keySet().containsAll(oldKeys.keySet())) { 441 // Some keys removed, might be the last instance of key, rebuild necessary 442 dirty = true; 443 } else { 444 for (Entry<String, String> oldEntry: oldKeys.entrySet()) { 445 if (!oldEntry.getValue().equals(newKeys.get(oldEntry.getKey()))) { 446 // Value changed, might be last instance of value, rebuild necessary 447 dirty = true; 448 return; 449 } 450 } 451 cachePrimitives(Collections.singleton(event.getPrimitive())); 452 } 453 } 454 455 @Override 456 public void nodeMoved(NodeMovedEvent event) {/* ignored */} 457 458 @Override 459 public void wayNodesChanged(WayNodesChangedEvent event) {/* ignored */} 460 461 @Override 462 public void relationMembersChanged(RelationMembersChangedEvent event) { 463 dirty = true; // TODO: not necessary to rebuid if a member is added 464 } 465 466 @Override 467 public void otherDatasetChange(AbstractDatasetChangedEvent event) {/* ignored */} 468 469 @Override 470 public void dataChanged(DataChangedEvent event) { 471 dirty = true; 472 } 473}