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

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

fix #8262 - Relation editor: filter autocompletion of roles by relation type

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