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

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

see #11390 - sonar - squid:S1604 - Java 8: Anonymous inner classes containing only one method should become lambdas

  • Property svn:eol-style set to native
File size: 15.9 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.HashSet;
9import java.util.LinkedHashSet;
10import java.util.List;
11import java.util.Map;
12import java.util.Map.Entry;
13import java.util.Objects;
14import java.util.Set;
15
16import org.openstreetmap.josm.Main;
17import org.openstreetmap.josm.data.osm.DataSet;
18import org.openstreetmap.josm.data.osm.OsmPrimitive;
19import org.openstreetmap.josm.data.osm.Relation;
20import org.openstreetmap.josm.data.osm.RelationMember;
21import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
22import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
23import org.openstreetmap.josm.data.osm.event.DataSetListener;
24import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
25import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
26import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
27import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
28import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
29import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
30import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
31import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
32import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
33import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup;
34import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
35import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
36import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
37import org.openstreetmap.josm.tools.CheckParameterUtil;
38import org.openstreetmap.josm.tools.MultiMap;
39import org.openstreetmap.josm.tools.Utils;
40import org.openstreetmap.josm.tools.Utils.Function;
41
42/**
43 * AutoCompletionManager holds a cache of keys with a list of
44 * possible auto completion values for each key.
45 *
46 * Each DataSet is assigned one AutoCompletionManager instance such that
47 * <ol>
48 * <li>any key used in a tag in the data set is part of the key list in the cache</li>
49 * <li>any value used in a tag for a specific key is part of the autocompletion list of
50 * this key</li>
51 * </ol>
52 *
53 * Building up auto completion lists should not
54 * slow down tabbing from input field to input field. Looping through the complete
55 * data set in order to build up the auto completion list for a specific input
56 * field is not efficient enough, hence this cache.
57 *
58 * TODO: respect the relation type for member role autocompletion
59 */
60public class AutoCompletionManager implements DataSetListener {
61
62 /**
63 * Data class to remember tags that the user has entered.
64 */
65 public static class UserInputTag {
66 private final String key;
67 private final String value;
68 private final boolean defaultKey;
69
70 /**
71 * Constructor.
72 *
73 * @param key the tag key
74 * @param value the tag value
75 * @param defaultKey true, if the key was not really entered by the
76 * user, e.g. for preset text fields.
77 * In this case, the key will not get any higher priority, just the value.
78 */
79 public UserInputTag(String key, String value, boolean defaultKey) {
80 this.key = key;
81 this.value = value;
82 this.defaultKey = defaultKey;
83 }
84
85 @Override
86 public int hashCode() {
87 return Objects.hash(key, value, defaultKey);
88 }
89
90 @Override
91 public boolean equals(Object obj) {
92 if (obj == null || getClass() != obj.getClass()) {
93 return false;
94 }
95 final UserInputTag other = (UserInputTag) obj;
96 return Objects.equals(this.key, other.key)
97 && Objects.equals(this.value, other.value)
98 && this.defaultKey == other.defaultKey;
99 }
100 }
101
102 /** If the dirty flag is set true, a rebuild is necessary. */
103 protected boolean dirty;
104 /** The data set that is managed */
105 protected DataSet ds;
106
107 /**
108 * the cached tags given by a tag key and a list of values for this tag
109 * only accessed by getTagCache(), rebuild() and cachePrimitiveTags()
110 * use getTagCache() accessor
111 */
112 protected MultiMap<String, String> tagCache;
113
114 /**
115 * the same as tagCache but for the preset keys and values can be accessed directly
116 */
117 protected static final MultiMap<String, String> PRESET_TAG_CACHE = new MultiMap<>();
118
119 /**
120 * Cache for tags that have been entered by the user.
121 */
122 protected static final Set<UserInputTag> USER_INPUT_TAG_CACHE = new LinkedHashSet<>();
123
124 /**
125 * the cached list of member roles
126 * only accessed by getRoleCache(), rebuild() and cacheRelationMemberRoles()
127 * use getRoleCache() accessor
128 */
129 protected Set<String> roleCache;
130
131 /**
132 * the same as roleCache but for the preset roles can be accessed directly
133 */
134 protected static final Set<String> PRESET_ROLE_CACHE = new HashSet<>();
135
136 /**
137 * Constructs a new {@code AutoCompletionManager}.
138 * @param ds data set
139 */
140 public AutoCompletionManager(DataSet ds) {
141 this.ds = ds;
142 this.dirty = true;
143 }
144
145 protected MultiMap<String, String> getTagCache() {
146 if (dirty) {
147 rebuild();
148 dirty = false;
149 }
150 return tagCache;
151 }
152
153 protected Set<String> getRoleCache() {
154 if (dirty) {
155 rebuild();
156 dirty = false;
157 }
158 return roleCache;
159 }
160
161 /**
162 * initializes the cache from the primitives in the dataset
163 */
164 protected void rebuild() {
165 tagCache = new MultiMap<>();
166 roleCache = new HashSet<>();
167 cachePrimitives(ds.allNonDeletedCompletePrimitives());
168 }
169
170 protected void cachePrimitives(Collection<? extends OsmPrimitive> primitives) {
171 for (OsmPrimitive primitive : primitives) {
172 cachePrimitiveTags(primitive);
173 if (primitive instanceof Relation) {
174 cacheRelationMemberRoles((Relation) primitive);
175 }
176 }
177 }
178
179 /**
180 * make sure, the keys and values of all tags held by primitive are
181 * in the auto completion cache
182 *
183 * @param primitive an OSM primitive
184 */
185 protected void cachePrimitiveTags(OsmPrimitive primitive) {
186 for (String key: primitive.keySet()) {
187 String value = primitive.get(key);
188 tagCache.put(key, value);
189 }
190 }
191
192 /**
193 * Caches all member roles of the relation <code>relation</code>
194 *
195 * @param relation the relation
196 */
197 protected void cacheRelationMemberRoles(Relation relation) {
198 for (RelationMember m: relation.getMembers()) {
199 if (m.hasRole()) {
200 roleCache.add(m.getRole());
201 }
202 }
203 }
204
205 /**
206 * Initialize the cache for presets. This is done only once.
207 * @param presets Tagging presets to cache
208 */
209 public static void cachePresets(Collection<TaggingPreset> presets) {
210 for (final TaggingPreset p : presets) {
211 for (TaggingPresetItem item : p.data) {
212 cachePresetItem(p, item);
213 }
214 }
215 }
216
217 protected static void cachePresetItem(TaggingPreset p, TaggingPresetItem item) {
218 if (item instanceof KeyedItem) {
219 KeyedItem ki = (KeyedItem) item;
220 if (ki.key != null && ki.getValues() != null) {
221 try {
222 PRESET_TAG_CACHE.putAll(ki.key, ki.getValues());
223 } catch (NullPointerException e) {
224 Main.error(p + ": Unable to cache " + ki);
225 }
226 }
227 } else if (item instanceof Roles) {
228 Roles r = (Roles) item;
229 for (Role i : r.roles) {
230 if (i.key != null) {
231 PRESET_ROLE_CACHE.add(i.key);
232 }
233 }
234 } else if (item instanceof CheckGroup) {
235 for (KeyedItem check : ((CheckGroup) item).checks) {
236 cachePresetItem(p, check);
237 }
238 }
239 }
240
241 /**
242 * Remembers user input for the given key/value.
243 * @param key Tag key
244 * @param value Tag value
245 * @param defaultKey true, if the key was not really entered by the user, e.g. for preset text fields
246 */
247 public static void rememberUserInput(String key, String value, boolean defaultKey) {
248 UserInputTag tag = new UserInputTag(key, value, defaultKey);
249 USER_INPUT_TAG_CACHE.remove(tag); // re-add, so it gets to the last position of the LinkedHashSet
250 USER_INPUT_TAG_CACHE.add(tag);
251 }
252
253 /**
254 * replies the keys held by the cache
255 *
256 * @return the list of keys held by the cache
257 */
258 protected List<String> getDataKeys() {
259 return new ArrayList<>(getTagCache().keySet());
260 }
261
262 protected List<String> getPresetKeys() {
263 return new ArrayList<>(PRESET_TAG_CACHE.keySet());
264 }
265
266 protected Collection<String> getUserInputKeys() {
267 List<String> keys = new ArrayList<>();
268 for (UserInputTag tag : USER_INPUT_TAG_CACHE) {
269 if (!tag.defaultKey) {
270 keys.add(tag.key);
271 }
272 }
273 Collections.reverse(keys);
274 return new LinkedHashSet<>(keys);
275 }
276
277 /**
278 * replies the auto completion values allowed for a specific key. Replies
279 * an empty list if key is null or if key is not in {@link #getKeys()}.
280 *
281 * @param key OSM key
282 * @return the list of auto completion values
283 */
284 protected List<String> getDataValues(String key) {
285 return new ArrayList<>(getTagCache().getValues(key));
286 }
287
288 protected static List<String> getPresetValues(String key) {
289 return new ArrayList<>(PRESET_TAG_CACHE.getValues(key));
290 }
291
292 protected static Collection<String> getUserInputValues(String key) {
293 List<String> values = new ArrayList<>();
294 for (UserInputTag tag : USER_INPUT_TAG_CACHE) {
295 if (key.equals(tag.key)) {
296 values.add(tag.value);
297 }
298 }
299 Collections.reverse(values);
300 return new LinkedHashSet<>(values);
301 }
302
303 /**
304 * Replies the list of member roles
305 *
306 * @return the list of member roles
307 */
308 public List<String> getMemberRoles() {
309 return new ArrayList<>(getRoleCache());
310 }
311
312 /**
313 * Populates the {@link AutoCompletionList} with the currently cached
314 * member roles.
315 *
316 * @param list the list to populate
317 */
318 public void populateWithMemberRoles(AutoCompletionList list) {
319 list.add(PRESET_ROLE_CACHE, AutoCompletionItemPriority.IS_IN_STANDARD);
320 list.add(getRoleCache(), AutoCompletionItemPriority.IS_IN_DATASET);
321 }
322
323 /**
324 * Populates the {@link AutoCompletionList} with the roles used in this relation
325 * plus the ones defined in its applicable presets, if any. If the relation type is unknown,
326 * then all the roles known globally will be added, as in {@link #populateWithMemberRoles(AutoCompletionList)}.
327 *
328 * @param list the list to populate
329 * @param r the relation to get roles from
330 * @throws IllegalArgumentException if list is null
331 * @since 7556
332 */
333 public void populateWithMemberRoles(AutoCompletionList list, Relation r) {
334 CheckParameterUtil.ensureParameterNotNull(list, "list");
335 Collection<TaggingPreset> presets = r != null ? TaggingPresets.getMatchingPresets(null, r.getKeys(), false) : null;
336 if (r != null && presets != null && !presets.isEmpty()) {
337 for (TaggingPreset tp : presets) {
338 if (tp.roles != null) {
339 list.add(Utils.transform(tp.roles.roles, (Function<Role, String>) x -> x.key), AutoCompletionItemPriority.IS_IN_STANDARD);
340 }
341 }
342 list.add(r.getMemberRoles(), AutoCompletionItemPriority.IS_IN_DATASET);
343 } else {
344 populateWithMemberRoles(list);
345 }
346 }
347
348 /**
349 * Populates the an {@link AutoCompletionList} with the currently cached tag keys
350 *
351 * @param list the list to populate
352 */
353 public void populateWithKeys(AutoCompletionList list) {
354 list.add(getPresetKeys(), AutoCompletionItemPriority.IS_IN_STANDARD);
355 list.add(new AutoCompletionListItem("source", AutoCompletionItemPriority.IS_IN_STANDARD));
356 list.add(getDataKeys(), AutoCompletionItemPriority.IS_IN_DATASET);
357 list.addUserInput(getUserInputKeys());
358 }
359
360 /**
361 * Populates the an {@link AutoCompletionList} with the currently cached
362 * values for a tag
363 *
364 * @param list the list to populate
365 * @param key the tag key
366 */
367 public void populateWithTagValues(AutoCompletionList list, String key) {
368 populateWithTagValues(list, Arrays.asList(key));
369 }
370
371 /**
372 * Populates the an {@link AutoCompletionList} with the currently cached
373 * values for some given tags
374 *
375 * @param list the list to populate
376 * @param keys the tag keys
377 */
378 public void populateWithTagValues(AutoCompletionList list, List<String> keys) {
379 for (String key : keys) {
380 list.add(getPresetValues(key), AutoCompletionItemPriority.IS_IN_STANDARD);
381 list.add(getDataValues(key), AutoCompletionItemPriority.IS_IN_DATASET);
382 list.addUserInput(getUserInputValues(key));
383 }
384 }
385
386 /**
387 * Returns the currently cached tag keys.
388 * @return a list of tag keys
389 */
390 public List<AutoCompletionListItem> getKeys() {
391 AutoCompletionList list = new AutoCompletionList();
392 populateWithKeys(list);
393 return list.getList();
394 }
395
396 /**
397 * Returns the currently cached tag values for a given tag key.
398 * @param key the tag key
399 * @return a list of tag values
400 */
401 public List<AutoCompletionListItem> getValues(String key) {
402 return getValues(Arrays.asList(key));
403 }
404
405 /**
406 * Returns the currently cached tag values for a given list of tag keys.
407 * @param keys the tag keys
408 * @return a list of tag values
409 */
410 public List<AutoCompletionListItem> getValues(List<String> keys) {
411 AutoCompletionList list = new AutoCompletionList();
412 populateWithTagValues(list, keys);
413 return list.getList();
414 }
415
416 /*********************************************************
417 * Implementation of the DataSetListener interface
418 *
419 **/
420
421 @Override
422 public void primitivesAdded(PrimitivesAddedEvent event) {
423 if (dirty)
424 return;
425 cachePrimitives(event.getPrimitives());
426 }
427
428 @Override
429 public void primitivesRemoved(PrimitivesRemovedEvent event) {
430 dirty = true;
431 }
432
433 @Override
434 public void tagsChanged(TagsChangedEvent event) {
435 if (dirty)
436 return;
437 Map<String, String> newKeys = event.getPrimitive().getKeys();
438 Map<String, String> oldKeys = event.getOriginalKeys();
439
440 if (!newKeys.keySet().containsAll(oldKeys.keySet())) {
441 // Some keys removed, might be the last instance of key, rebuild necessary
442 dirty = true;
443 } else {
444 for (Entry<String, String> oldEntry: oldKeys.entrySet()) {
445 if (!oldEntry.getValue().equals(newKeys.get(oldEntry.getKey()))) {
446 // Value changed, might be last instance of value, rebuild necessary
447 dirty = true;
448 return;
449 }
450 }
451 cachePrimitives(Collections.singleton(event.getPrimitive()));
452 }
453 }
454
455 @Override
456 public void nodeMoved(NodeMovedEvent event) {/* ignored */}
457
458 @Override
459 public void wayNodesChanged(WayNodesChangedEvent event) {/* ignored */}
460
461 @Override
462 public void relationMembersChanged(RelationMembersChangedEvent event) {
463 dirty = true; // TODO: not necessary to rebuid if a member is added
464 }
465
466 @Override
467 public void otherDatasetChange(AbstractDatasetChangedEvent event) {/* ignored */}
468
469 @Override
470 public void dataChanged(DataChangedEvent event) {
471 dirty = true;
472 }
473}
Note: See TracBrowser for help on using the repository browser.