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

Last change on this file since 12042 was 12042, checked in by bastiK, 7 years ago

applied #13956 - memory optimization with presets cache (patch by GerdP)

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