001 // License: GPL. For details, see LICENSE file.
002 package org.openstreetmap.josm.gui.tagging.ac;
003
004 import java.util.ArrayList;
005 import java.util.Arrays;
006 import java.util.Collection;
007 import java.util.Collections;
008 import java.util.HashSet;
009 import java.util.List;
010 import java.util.Map;
011 import java.util.Map.Entry;
012 import java.util.Set;
013
014 import org.openstreetmap.josm.data.osm.DataSet;
015 import org.openstreetmap.josm.data.osm.OsmPrimitive;
016 import org.openstreetmap.josm.data.osm.OsmUtils;
017 import org.openstreetmap.josm.data.osm.Relation;
018 import org.openstreetmap.josm.data.osm.RelationMember;
019 import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
020 import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
021 import org.openstreetmap.josm.data.osm.event.DataSetListener;
022 import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
023 import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
024 import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
025 import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
026 import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
027 import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
028 import org.openstreetmap.josm.gui.tagging.TaggingPreset;
029 import org.openstreetmap.josm.tools.MultiMap;
030
031 /**
032 * AutoCompletionManager holds a cache of keys with a list of
033 * possible auto completion values for each key.
034 *
035 * Each DataSet is assigned one AutoCompletionManager instance such that
036 * <ol>
037 * <li>any key used in a tag in the data set is part of the key list in the cache</li>
038 * <li>any value used in a tag for a specific key is part of the autocompletion list of
039 * this key</li>
040 * </ol>
041 *
042 * Building up auto completion lists should not
043 * slow down tabbing from input field to input field. Looping through the complete
044 * data set in order to build up the auto completion list for a specific input
045 * field is not efficient enough, hence this cache.
046 *
047 * TODO: respect the relation type for member role autocompletion
048 */
049 public class AutoCompletionManager implements DataSetListener {
050
051 /** If the dirty flag is set true, a rebuild is necessary. */
052 protected boolean dirty;
053 /** The data set that is managed */
054 protected DataSet ds;
055
056 /**
057 * the cached tags given by a tag key and a list of values for this tag
058 * only accessed by getTagCache(), rebuild() and cachePrimitiveTags()
059 * use getTagCache() accessor
060 */
061 protected MultiMap<String, String> tagCache;
062 /**
063 * the same as tagCache but for the preset keys and values
064 * can be accessed directly
065 */
066 protected static final MultiMap<String, String> presetTagCache = new MultiMap<String, String>();
067 /**
068 * the cached list of member roles
069 * only accessed by getRoleCache(), rebuild() and cacheRelationMemberRoles()
070 * use getRoleCache() accessor
071 */
072 protected Set<String> roleCache;
073 /**
074 * the same as roleCache but for the preset roles
075 * can be accessed directly
076 */
077 protected static final Set<String> presetRoleCache = new HashSet<String>();
078
079 public AutoCompletionManager(DataSet ds) {
080 this.ds = ds;
081 dirty = true;
082 }
083
084 protected MultiMap<String, String> getTagCache() {
085 if (dirty) {
086 rebuild();
087 dirty = false;
088 }
089 return tagCache;
090 }
091
092 protected Set<String> getRoleCache() {
093 if (dirty) {
094 rebuild();
095 dirty = false;
096 }
097 return roleCache;
098 }
099
100 /**
101 * initializes the cache from the primitives in the dataset
102 *
103 */
104 protected void rebuild() {
105 tagCache = new MultiMap<String, String>();
106 roleCache = new HashSet<String>();
107 cachePrimitives(ds.allNonDeletedCompletePrimitives());
108 }
109
110 protected void cachePrimitives(Collection<? extends OsmPrimitive> primitives) {
111 for (OsmPrimitive primitive : primitives) {
112 cachePrimitiveTags(primitive);
113 if (primitive instanceof Relation) {
114 cacheRelationMemberRoles((Relation) primitive);
115 }
116 }
117 }
118
119 /**
120 * make sure, the keys and values of all tags held by primitive are
121 * in the auto completion cache
122 *
123 * @param primitive an OSM primitive
124 */
125 protected void cachePrimitiveTags(OsmPrimitive primitive) {
126 for (String key: primitive.keySet()) {
127 String value = primitive.get(key);
128 tagCache.put(key, value);
129 }
130 }
131
132 /**
133 * Caches all member roles of the relation <code>relation</code>
134 *
135 * @param relation the relation
136 */
137 protected void cacheRelationMemberRoles(Relation relation){
138 for (RelationMember m: relation.getMembers()) {
139 if (m.hasRole()) {
140 roleCache.add(m.getRole());
141 }
142 }
143 }
144
145 /**
146 * Initialize the cache for presets. This is done only once.
147 */
148 public static void cachePresets(Collection<TaggingPreset> presets) {
149 for (final TaggingPreset p : presets) {
150 for (TaggingPreset.Item item : p.data) {
151 if (item instanceof TaggingPreset.KeyedItem) {
152 TaggingPreset.KeyedItem ki = (TaggingPreset.KeyedItem) item;
153 if (ki.key == null) {
154 continue;
155 }
156 presetTagCache.putAll(ki.key, ki.getValues());
157 } else if (item instanceof TaggingPreset.Roles) {
158 TaggingPreset.Roles r = (TaggingPreset.Roles) item;
159 for (TaggingPreset.Role i : r.roles) {
160 if (i.key != null) {
161 presetRoleCache.add(i.key);
162 }
163 }
164 }
165 }
166 }
167 }
168
169 /**
170 * replies the keys held by the cache
171 *
172 * @return the list of keys held by the cache
173 */
174 protected List<String> getDataKeys() {
175 return new ArrayList<String>(getTagCache().keySet());
176 }
177
178 protected List<String> getPresetKeys() {
179 return new ArrayList<String>(presetTagCache.keySet());
180 }
181
182 /**
183 * replies the auto completion values allowed for a specific key. Replies
184 * an empty list if key is null or if key is not in {@link #getKeys()}.
185 *
186 * @param key
187 * @return the list of auto completion values
188 */
189 protected List<String> getDataValues(String key) {
190 return new ArrayList<String>(getTagCache().getValues(key));
191 }
192
193 protected static List<String> getPresetValues(String key) {
194 return new ArrayList<String>(presetTagCache.getValues(key));
195 }
196
197 /**
198 * Replies the list of member roles
199 *
200 * @return the list of member roles
201 */
202 public List<String> getMemberRoles() {
203 return new ArrayList<String>(getRoleCache());
204 }
205
206 /**
207 * Populates the an {@link AutoCompletionList} with the currently cached
208 * member roles.
209 *
210 * @param list the list to populate
211 */
212 public void populateWithMemberRoles(AutoCompletionList list) {
213 list.add(presetRoleCache, AutoCompletionItemPritority.IS_IN_STANDARD);
214 list.add(getRoleCache(), AutoCompletionItemPritority.IS_IN_DATASET);
215 }
216
217 /**
218 * Populates the an {@link AutoCompletionList} with the currently cached
219 * tag keys
220 *
221 * @param list the list to populate
222 */
223 public void populateWithKeys(AutoCompletionList list) {
224 list.add(getPresetKeys(), AutoCompletionItemPritority.IS_IN_STANDARD);
225 list.add(getDataKeys(), AutoCompletionItemPritority.IS_IN_DATASET);
226 }
227
228 /**
229 * Populates the an {@link AutoCompletionList} with the currently cached
230 * values for a tag
231 *
232 * @param list the list to populate
233 * @param key the tag key
234 */
235 public void populateWithTagValues(AutoCompletionList list, String key) {
236 populateWithTagValues(list, Arrays.asList(key));
237 }
238
239 /**
240 * Populates the an {@link AutoCompletionList} with the currently cached
241 * values for some given tags
242 *
243 * @param list the list to populate
244 * @param keys the tag keys
245 */
246 public void populateWithTagValues(AutoCompletionList list, List<String> keys) {
247 for (String key : keys) {
248 list.add(getPresetValues(key), AutoCompletionItemPritority.IS_IN_STANDARD);
249 list.add(getDataValues(key), AutoCompletionItemPritority.IS_IN_DATASET);
250 }
251 }
252
253 /**
254 * Returns the currently cached tag keys.
255 * @return a list of tag keys
256 */
257 public List<AutoCompletionListItem> getKeys() {
258 AutoCompletionList list = new AutoCompletionList();
259 populateWithKeys(list);
260 return list.getList();
261 }
262
263 /**
264 * Returns the currently cached tag values for a given tag key.
265 * @param key the tag key
266 * @return a list of tag values
267 */
268 public List<AutoCompletionListItem> getValues(String key) {
269 return getValues(Arrays.asList(key));
270 }
271
272 /**
273 * Returns the currently cached tag values for a given list of tag keys.
274 * @param keys the tag keys
275 * @return a list of tag values
276 */
277 public List<AutoCompletionListItem> getValues(List<String> keys) {
278 AutoCompletionList list = new AutoCompletionList();
279 populateWithTagValues(list, keys);
280 return list.getList();
281 }
282
283 /*********************************************************
284 * Implementation of the DataSetListener interface
285 *
286 **/
287
288 public void primitivesAdded(PrimitivesAddedEvent event) {
289 if (dirty)
290 return;
291 cachePrimitives(event.getPrimitives());
292 }
293
294 public void primitivesRemoved(PrimitivesRemovedEvent event) {
295 dirty = true;
296 }
297
298 public void tagsChanged(TagsChangedEvent event) {
299 if (dirty)
300 return;
301 Map<String, String> newKeys = event.getPrimitive().getKeys();
302 Map<String, String> oldKeys = event.getOriginalKeys();
303
304 if (!newKeys.keySet().containsAll(oldKeys.keySet())) {
305 // Some keys removed, might be the last instance of key, rebuild necessary
306 dirty = true;
307 } else {
308 for (Entry<String, String> oldEntry: oldKeys.entrySet()) {
309 if (!oldEntry.getValue().equals(newKeys.get(oldEntry.getKey()))) {
310 // Value changed, might be last instance of value, rebuild necessary
311 dirty = true;
312 return;
313 }
314 }
315 cachePrimitives(Collections.singleton(event.getPrimitive()));
316 }
317 }
318
319 public void nodeMoved(NodeMovedEvent event) {/* ignored */}
320
321 public void wayNodesChanged(WayNodesChangedEvent event) {/* ignored */}
322
323 public void relationMembersChanged(RelationMembersChangedEvent event) {
324 dirty = true; // TODO: not necessary to rebuid if a member is added
325 }
326
327 public void otherDatasetChange(AbstractDatasetChangedEvent event) {/* ignored */}
328
329 public void dataChanged(DataChangedEvent event) {
330 dirty = true;
331 }
332 }