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

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

see #15310 - remove most of deprecated APIs

  • Property svn:eol-style set to native
File size: 17.5 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 #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 = set.stream().collect(Collectors.toList());
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 }
487 }
488
489 @Override
490 public void layerOrderChanged(LayerOrderChangeEvent e) {
491 // Do nothing
492 }
493
494 @Override
495 public void layerAdded(LayerAddEvent e) {
496 // Do nothing
497 }
498 });
499 return this;
500 }
501
502 /**
503 * Returns the {@code AutoCompletionManager} for the given data set.
504 * @param dataSet the data set
505 * @return the {@code AutoCompletionManager} for the given data set
506 * @since 12758
507 */
508 public static AutoCompletionManager of(DataSet dataSet) {
509 return INSTANCES.computeIfAbsent(dataSet, ds -> new AutoCompletionManager(ds).registerListeners());
510 }
511}
Note: See TracBrowser for help on using the repository browser.