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    }