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