source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletionManager.java@ 12865

Last change on this file since 12865 was 12865, checked in by Don-vip, 7 years ago

see #11390 - SonarQube - squid:S3824 - "Map.get" and value test should be replaced with single method call

  • Property svn:eol-style set to native
File size: 18.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.tagging.ac;
3
4import java.util.ArrayList;
5import java.util.Arrays;
6import java.util.Collection;
7import java.util.Collections;
8import java.util.Comparator;
9import java.util.HashMap;
10import java.util.HashSet;
11import java.util.LinkedHashSet;
12import java.util.List;
13import java.util.Map;
14import java.util.Map.Entry;
15import java.util.Objects;
16import java.util.Set;
17import java.util.function.Function;
18import java.util.stream.Collectors;
19
20import org.openstreetmap.josm.data.osm.DataSet;
21import org.openstreetmap.josm.data.osm.OsmPrimitive;
22import org.openstreetmap.josm.data.osm.Relation;
23import org.openstreetmap.josm.data.osm.RelationMember;
24import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
25import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
26import org.openstreetmap.josm.data.osm.event.DataSetListener;
27import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
28import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
29import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
30import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
31import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
32import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
33import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
34import org.openstreetmap.josm.data.tagging.ac.AutoCompletionPriority;
35import org.openstreetmap.josm.data.tagging.ac.AutoCompletionSet;
36import org.openstreetmap.josm.gui.MainApplication;
37import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
38import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
39import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
40import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
41import org.openstreetmap.josm.gui.layer.OsmDataLayer;
42import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
43import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
44import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
45import org.openstreetmap.josm.tools.CheckParameterUtil;
46import org.openstreetmap.josm.tools.MultiMap;
47import org.openstreetmap.josm.tools.Utils;
48
49/**
50 * AutoCompletionManager holds a cache of keys with a list of
51 * possible auto completion values for each key.
52 *
53 * Each DataSet can be assigned one AutoCompletionManager instance such that
54 * <ol>
55 * <li>any key used in a tag in the data set is part of the key list in the cache</li>
56 * <li>any value used in a tag for a specific key is part of the autocompletion list of this key</li>
57 * </ol>
58 *
59 * Building up auto completion lists should not
60 * slow down tabbing from input field to input field. Looping through the complete
61 * data set in order to build up the auto completion list for a specific input
62 * field is not efficient enough, hence this cache.
63 *
64 * TODO: respect the relation type for member role autocompletion
65 */
66public class AutoCompletionManager implements DataSetListener {
67
68 /**
69 * Data class to remember tags that the user has entered.
70 */
71 public static class UserInputTag {
72 private final String key;
73 private final String value;
74 private final boolean defaultKey;
75
76 /**
77 * Constructor.
78 *
79 * @param key the tag key
80 * @param value the tag value
81 * @param defaultKey true, if the key was not really entered by the
82 * user, e.g. for preset text fields.
83 * In this case, the key will not get any higher priority, just the value.
84 */
85 public UserInputTag(String key, String value, boolean defaultKey) {
86 this.key = key;
87 this.value = value;
88 this.defaultKey = defaultKey;
89 }
90
91 @Override
92 public int hashCode() {
93 return Objects.hash(key, value, defaultKey);
94 }
95
96 @Override
97 public boolean equals(Object obj) {
98 if (obj == null || getClass() != obj.getClass()) {
99 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 */
148 public AutoCompletionManager(DataSet ds) {
149 this.ds = 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 #getKeys()}.
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 an {@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 /**
348 * Returns the currently cached tag keys.
349 * @return a list of tag keys
350 * @deprecated to be removed end of 2017. Use {@link #getTagKeys()} instead
351 */
352 @Deprecated
353 public List<AutoCompletionListItem> getKeys() {
354 return getTagKeys().stream().map(AutoCompletionListItem::new).collect(Collectors.toList());
355 }
356
357 /**
358 * Returns the currently cached tag values for a given tag key.
359 * @param key the tag key
360 * @return a list of tag values
361 * @deprecated to be removed end of 2017. Use {@link #getTagValues(String)} instead
362 */
363 @Deprecated
364 public List<AutoCompletionListItem> getValues(String key) {
365 return getTagValues(key).stream().map(AutoCompletionListItem::new).collect(Collectors.toList());
366 }
367
368 /**
369 * Returns the currently cached tag values for a given list of tag keys.
370 * @param keys the tag keys
371 * @return a list of tag values
372 * @deprecated to be removed end of 2017. Use {@link #getTagValues(List)} instead
373 */
374 @Deprecated
375 public List<AutoCompletionListItem> getValues(List<String> keys) {
376 return getTagValues(keys).stream().map(AutoCompletionListItem::new).collect(Collectors.toList());
377 }
378
379 private static List<AutoCompletionItem> setToList(AutoCompletionSet set, Comparator<AutoCompletionItem> comparator) {
380 List<AutoCompletionItem> list = set.stream().collect(Collectors.toList());
381 list.sort(comparator);
382 return list;
383 }
384
385 /**
386 * Returns the currently cached tag keys.
387 * @return a set of tag keys
388 * @since 12859
389 */
390 public AutoCompletionSet getTagKeys() {
391 AutoCompletionList list = new AutoCompletionList();
392 populateWithKeys(list);
393 return list.getSet();
394 }
395
396 /**
397 * Returns the currently cached tag keys.
398 * @param comparator the custom comparator used to sort the list
399 * @return a list of tag keys
400 * @since 12859
401 */
402 public List<AutoCompletionItem> getTagKeys(Comparator<AutoCompletionItem> comparator) {
403 return setToList(getTagKeys(), comparator);
404 }
405
406 /**
407 * Returns the currently cached tag values for a given tag key.
408 * @param key the tag key
409 * @return a set of tag values
410 * @since 12859
411 */
412 public AutoCompletionSet getTagValues(String key) {
413 return getTagValues(Arrays.asList(key));
414 }
415
416 /**
417 * Returns the currently cached tag values for a given tag key.
418 * @param key the tag key
419 * @param comparator the custom comparator used to sort the list
420 * @return a list of tag values
421 * @since 12859
422 */
423 public List<AutoCompletionItem> getTagValues(String key, Comparator<AutoCompletionItem> comparator) {
424 return setToList(getTagValues(key), comparator);
425 }
426
427 /**
428 * Returns the currently cached tag values for a given list of tag keys.
429 * @param keys the tag keys
430 * @return a set of tag values
431 * @since 12859
432 */
433 public AutoCompletionSet getTagValues(List<String> keys) {
434 AutoCompletionList list = new AutoCompletionList();
435 populateWithTagValues(list, keys);
436 return list.getSet();
437 }
438
439 /**
440 * Returns the currently cached tag values for a given list of tag keys.
441 * @param keys the tag keys
442 * @param comparator the custom comparator used to sort the list
443 * @return a set of tag values
444 * @since 12859
445 */
446 public List<AutoCompletionItem> getTagValues(List<String> keys, Comparator<AutoCompletionItem> comparator) {
447 return setToList(getTagValues(keys), comparator);
448 }
449
450 /*
451 * Implementation of the DataSetListener interface
452 *
453 */
454
455 @Override
456 public void primitivesAdded(PrimitivesAddedEvent event) {
457 if (dirty)
458 return;
459 cachePrimitives(event.getPrimitives());
460 }
461
462 @Override
463 public void primitivesRemoved(PrimitivesRemovedEvent event) {
464 dirty = true;
465 }
466
467 @Override
468 public void tagsChanged(TagsChangedEvent event) {
469 if (dirty)
470 return;
471 Map<String, String> newKeys = event.getPrimitive().getKeys();
472 Map<String, String> oldKeys = event.getOriginalKeys();
473
474 if (!newKeys.keySet().containsAll(oldKeys.keySet())) {
475 // Some keys removed, might be the last instance of key, rebuild necessary
476 dirty = true;
477 } else {
478 for (Entry<String, String> oldEntry: oldKeys.entrySet()) {
479 if (!oldEntry.getValue().equals(newKeys.get(oldEntry.getKey()))) {
480 // Value changed, might be last instance of value, rebuild necessary
481 dirty = true;
482 return;
483 }
484 }
485 cachePrimitives(Collections.singleton(event.getPrimitive()));
486 }
487 }
488
489 @Override
490 public void nodeMoved(NodeMovedEvent event) {/* ignored */}
491
492 @Override
493 public void wayNodesChanged(WayNodesChangedEvent event) {/* ignored */}
494
495 @Override
496 public void relationMembersChanged(RelationMembersChangedEvent event) {
497 dirty = true; // TODO: not necessary to rebuid if a member is added
498 }
499
500 @Override
501 public void otherDatasetChange(AbstractDatasetChangedEvent event) {/* ignored */}
502
503 @Override
504 public void dataChanged(DataChangedEvent event) {
505 dirty = true;
506 }
507
508 private AutoCompletionManager registerListeners() {
509 ds.addDataSetListener(this);
510 MainApplication.getLayerManager().addLayerChangeListener(new LayerChangeListener() {
511 @Override
512 public void layerRemoving(LayerRemoveEvent e) {
513 if (e.getRemovedLayer() instanceof OsmDataLayer
514 && ((OsmDataLayer) e.getRemovedLayer()).data == ds) {
515 INSTANCES.remove(ds);
516 ds.removeDataSetListener(AutoCompletionManager.this);
517 MainApplication.getLayerManager().removeLayerChangeListener(this);
518 }
519 }
520
521 @Override
522 public void layerOrderChanged(LayerOrderChangeEvent e) {
523 // Do nothing
524 }
525
526 @Override
527 public void layerAdded(LayerAddEvent e) {
528 // Do nothing
529 }
530 });
531 return this;
532 }
533
534 /**
535 * Returns the {@code AutoCompletionManager} for the given data set.
536 * @param dataSet the data set
537 * @return the {@code AutoCompletionManager} for the given data set
538 * @since 12758
539 */
540 public static AutoCompletionManager of(DataSet dataSet) {
541 return INSTANCES.computeIfAbsent(dataSet, ds -> new AutoCompletionManager(ds).registerListeners());
542 }
543}
Note: See TracBrowser for help on using the repository browser.