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