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.gui.tagging.TaggingPresetItems.Role;
032import org.openstreetmap.josm.tools.CheckParameterUtil;
033import org.openstreetmap.josm.tools.MultiMap;
034import org.openstreetmap.josm.tools.Utils;
035
036/**
037 * AutoCompletionManager holds a cache of keys with a list of
038 * possible auto completion values for each key.
039 *
040 * Each DataSet is assigned one AutoCompletionManager instance such that
041 * <ol>
042 *   <li>any key used in a tag in the data set is part of the key list in the cache</li>
043 *   <li>any value used in a tag for a specific key is part of the autocompletion list of
044 *     this key</li>
045 * </ol>
046 *
047 * Building up auto completion lists should not
048 * slow down tabbing from input field to input field. Looping through the complete
049 * data set in order to build up the auto completion list for a specific input
050 * field is not efficient enough, hence this cache.
051 *
052 * TODO: respect the relation type for member role autocompletion
053 */
054public class AutoCompletionManager implements DataSetListener {
055
056    /** If the dirty flag is set true, a rebuild is necessary. */
057    protected boolean dirty;
058    /** The data set that is managed */
059    protected DataSet ds;
060
061    /**
062     * the cached tags given by a tag key and a list of values for this tag
063     * only accessed by getTagCache(), rebuild() and cachePrimitiveTags()
064     * use getTagCache() accessor
065     */
066    protected MultiMap<String, String> tagCache;
067    /**
068     * the same as tagCache but for the preset keys and values
069     * can be accessed directly
070     */
071    protected static final MultiMap<String, String> presetTagCache = new MultiMap<>();
072    /**
073     * the cached list of member roles
074     * only accessed by getRoleCache(), rebuild() and cacheRelationMemberRoles()
075     * use getRoleCache() accessor
076     */
077    protected Set<String> roleCache;
078    /**
079     * the same as roleCache but for the preset roles
080     * can be accessed directly
081     */
082    protected static final Set<String> presetRoleCache = new HashSet<>();
083
084    public AutoCompletionManager(DataSet ds) {
085        this.ds = ds;
086        dirty = true;
087    }
088
089    protected MultiMap<String, String> getTagCache() {
090        if (dirty) {
091            rebuild();
092            dirty = false;
093        }
094        return tagCache;
095    }
096
097    protected Set<String> getRoleCache() {
098        if (dirty) {
099            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}