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