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

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

see #8039, see #10456 - support read-only data layers

  • Property svn:eol-style set to native
File size: 17.6 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 = 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}
Note: See TracBrowser for help on using the repository browser.