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

Last change on this file since 16436 was 16436, checked in by simon04, 4 years ago

see #19251 - Java 8: use Stream

  • Property svn:eol-style set to native
File size: 17.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 * @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 = USER_INPUT_TAG_CACHE.stream()
237 .filter(tag -> !tag.defaultKey)
238 .map(tag -> tag.key)
239 .collect(Collectors.toList());
240 Collections.reverse(keys);
241 return new LinkedHashSet<>(keys);
242 }
243
244 /**
245 * replies the auto completion values allowed for a specific key. Replies
246 * an empty list if key is null or if key is not in {@link #getTagKeys()}.
247 *
248 * @param key OSM key
249 * @return the list of auto completion values
250 */
251 protected List<String> getDataValues(String key) {
252 return new ArrayList<>(getTagCache().getValues(key));
253 }
254
255 protected static Collection<String> getUserInputValues(String key) {
256 List<String> values = USER_INPUT_TAG_CACHE.stream()
257 .filter(tag -> Objects.equals(key, tag.key))
258 .map(tag -> tag.value)
259 .collect(Collectors.toList());
260 Collections.reverse(values);
261 return new LinkedHashSet<>(values);
262 }
263
264 /**
265 * Replies the list of member roles
266 *
267 * @return the list of member roles
268 */
269 public List<String> getMemberRoles() {
270 return new ArrayList<>(getRoleCache());
271 }
272
273 /**
274 * Populates the {@link AutoCompletionList} with the currently cached member roles.
275 *
276 * @param list the list to populate
277 */
278 public void populateWithMemberRoles(AutoCompletionList list) {
279 list.add(TaggingPresets.getPresetRoles(), AutoCompletionPriority.IS_IN_STANDARD);
280 list.add(getRoleCache(), AutoCompletionPriority.IS_IN_DATASET);
281 }
282
283 /**
284 * Populates the {@link AutoCompletionList} with the roles used in this relation
285 * plus the ones defined in its applicable presets, if any. If the relation type is unknown,
286 * then all the roles known globally will be added, as in {@link #populateWithMemberRoles(AutoCompletionList)}.
287 *
288 * @param list the list to populate
289 * @param r the relation to get roles from
290 * @throws IllegalArgumentException if list is null
291 * @since 7556
292 */
293 public void populateWithMemberRoles(AutoCompletionList list, Relation r) {
294 CheckParameterUtil.ensureParameterNotNull(list, "list");
295 Collection<TaggingPreset> presets = r != null ? TaggingPresets.getMatchingPresets(null, r.getKeys(), false) : null;
296 if (r != null && presets != null && !presets.isEmpty()) {
297 for (TaggingPreset tp : presets) {
298 if (tp.roles != null) {
299 list.add(Utils.transform(tp.roles.roles, (Function<Role, String>) x -> x.key), AutoCompletionPriority.IS_IN_STANDARD);
300 }
301 }
302 list.add(r.getMemberRoles(), AutoCompletionPriority.IS_IN_DATASET);
303 } else {
304 populateWithMemberRoles(list);
305 }
306 }
307
308 /**
309 * Populates the an {@link AutoCompletionList} with the currently cached tag keys
310 *
311 * @param list the list to populate
312 */
313 public void populateWithKeys(AutoCompletionList list) {
314 list.add(TaggingPresets.getPresetKeys(), AutoCompletionPriority.IS_IN_STANDARD);
315 list.add(new AutoCompletionItem("source", AutoCompletionPriority.IS_IN_STANDARD));
316 list.add(getDataKeys(), AutoCompletionPriority.IS_IN_DATASET);
317 list.addUserInput(getUserInputKeys());
318 }
319
320 /**
321 * Populates the an {@link AutoCompletionList} with the currently cached values for a tag
322 *
323 * @param list the list to populate
324 * @param key the tag key
325 */
326 public void populateWithTagValues(AutoCompletionList list, String key) {
327 populateWithTagValues(list, Arrays.asList(key));
328 }
329
330 /**
331 * Populates the {@link AutoCompletionList} with the currently cached values for some given tags
332 *
333 * @param list the list to populate
334 * @param keys the tag keys
335 */
336 public void populateWithTagValues(AutoCompletionList list, List<String> keys) {
337 for (String key : keys) {
338 list.add(TaggingPresets.getPresetValues(key), AutoCompletionPriority.IS_IN_STANDARD);
339 list.add(getDataValues(key), AutoCompletionPriority.IS_IN_DATASET);
340 list.addUserInput(getUserInputValues(key));
341 }
342 }
343
344 private static List<AutoCompletionItem> setToList(AutoCompletionSet set, Comparator<AutoCompletionItem> comparator) {
345 List<AutoCompletionItem> list = new ArrayList<>(set);
346 list.sort(comparator);
347 return list;
348 }
349
350 /**
351 * Returns the currently cached tag keys.
352 * @return a set of tag keys
353 * @since 12859
354 */
355 public AutoCompletionSet getTagKeys() {
356 AutoCompletionList list = new AutoCompletionList();
357 populateWithKeys(list);
358 return list.getSet();
359 }
360
361 /**
362 * Returns the currently cached tag keys.
363 * @param comparator the custom comparator used to sort the list
364 * @return a list of tag keys
365 * @since 12859
366 */
367 public List<AutoCompletionItem> getTagKeys(Comparator<AutoCompletionItem> comparator) {
368 return setToList(getTagKeys(), comparator);
369 }
370
371 /**
372 * Returns the currently cached tag values for a given tag key.
373 * @param key the tag key
374 * @return a set of tag values
375 * @since 12859
376 */
377 public AutoCompletionSet getTagValues(String key) {
378 return getTagValues(Arrays.asList(key));
379 }
380
381 /**
382 * Returns the currently cached tag values for a given tag key.
383 * @param key the tag key
384 * @param comparator the custom comparator used to sort the list
385 * @return a list of tag values
386 * @since 12859
387 */
388 public List<AutoCompletionItem> getTagValues(String key, Comparator<AutoCompletionItem> comparator) {
389 return setToList(getTagValues(key), comparator);
390 }
391
392 /**
393 * Returns the currently cached tag values for a given list of tag keys.
394 * @param keys the tag keys
395 * @return a set of tag values
396 * @since 12859
397 */
398 public AutoCompletionSet getTagValues(List<String> keys) {
399 AutoCompletionList list = new AutoCompletionList();
400 populateWithTagValues(list, keys);
401 return list.getSet();
402 }
403
404 /**
405 * Returns the currently cached tag values for a given list of tag keys.
406 * @param keys the tag keys
407 * @param comparator the custom comparator used to sort the list
408 * @return a set of tag values
409 * @since 12859
410 */
411 public List<AutoCompletionItem> getTagValues(List<String> keys, Comparator<AutoCompletionItem> comparator) {
412 return setToList(getTagValues(keys), comparator);
413 }
414
415 /*
416 * Implementation of the DataSetListener interface
417 *
418 */
419
420 @Override
421 public void primitivesAdded(PrimitivesAddedEvent event) {
422 if (dirty)
423 return;
424 cachePrimitives(event.getPrimitives());
425 }
426
427 @Override
428 public void primitivesRemoved(PrimitivesRemovedEvent event) {
429 dirty = true;
430 }
431
432 @Override
433 public void tagsChanged(TagsChangedEvent event) {
434 if (dirty)
435 return;
436 Map<String, String> newKeys = event.getPrimitive().getKeys();
437 Map<String, String> oldKeys = event.getOriginalKeys();
438
439 if (!newKeys.keySet().containsAll(oldKeys.keySet())) {
440 // Some keys removed, might be the last instance of key, rebuild necessary
441 dirty = true;
442 } else {
443 for (Entry<String, String> oldEntry: oldKeys.entrySet()) {
444 if (!oldEntry.getValue().equals(newKeys.get(oldEntry.getKey()))) {
445 // Value changed, might be last instance of value, rebuild necessary
446 dirty = true;
447 return;
448 }
449 }
450 cachePrimitives(Collections.singleton(event.getPrimitive()));
451 }
452 }
453
454 @Override
455 public void nodeMoved(NodeMovedEvent event) {/* ignored */}
456
457 @Override
458 public void wayNodesChanged(WayNodesChangedEvent event) {/* ignored */}
459
460 @Override
461 public void relationMembersChanged(RelationMembersChangedEvent event) {
462 dirty = true; // TODO: not necessary to rebuid if a member is added
463 }
464
465 @Override
466 public void otherDatasetChange(AbstractDatasetChangedEvent event) {/* ignored */}
467
468 @Override
469 public void dataChanged(DataChangedEvent event) {
470 dirty = true;
471 }
472
473 private AutoCompletionManager registerListeners() {
474 ds.addDataSetListener(this);
475 MainApplication.getLayerManager().addLayerChangeListener(new LayerChangeListener() {
476 @Override
477 public void layerRemoving(LayerRemoveEvent e) {
478 if (e.getRemovedLayer() instanceof OsmDataLayer
479 && ((OsmDataLayer) e.getRemovedLayer()).data == ds) {
480 INSTANCES.remove(ds);
481 ds.removeDataSetListener(AutoCompletionManager.this);
482 MainApplication.getLayerManager().removeLayerChangeListener(this);
483 dirty = true;
484 tagCache = null;
485 roleCache = null;
486 ds = null;
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}
Note: See TracBrowser for help on using the repository browser.