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.LinkedHashSet;
010import java.util.List;
011import java.util.Map;
012import java.util.Map.Entry;
013import java.util.Objects;
014import java.util.Set;
015
016import org.openstreetmap.josm.Main;
017import org.openstreetmap.josm.data.osm.DataSet;
018import org.openstreetmap.josm.data.osm.OsmPrimitive;
019import org.openstreetmap.josm.data.osm.Relation;
020import org.openstreetmap.josm.data.osm.RelationMember;
021import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
022import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
023import org.openstreetmap.josm.data.osm.event.DataSetListener;
024import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
025import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
026import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
027import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
028import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
029import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
030import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
031import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
032import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
033import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup;
034import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
035import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
036import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
037import org.openstreetmap.josm.tools.CheckParameterUtil;
038import org.openstreetmap.josm.tools.MultiMap;
039import org.openstreetmap.josm.tools.Utils;
040
041/**
042 * AutoCompletionManager holds a cache of keys with a list of
043 * possible auto completion values for each key.
044 *
045 * Each DataSet is assigned one AutoCompletionManager instance such that
046 * <ol>
047 *   <li>any key used in a tag in the data set is part of the key list in the cache</li>
048 *   <li>any value used in a tag for a specific key is part of the autocompletion list of
049 *     this key</li>
050 * </ol>
051 *
052 * Building up auto completion lists should not
053 * slow down tabbing from input field to input field. Looping through the complete
054 * data set in order to build up the auto completion list for a specific input
055 * field is not efficient enough, hence this cache.
056 *
057 * TODO: respect the relation type for member role autocompletion
058 */
059public class AutoCompletionManager implements DataSetListener {
060
061    /**
062     * Data class to remember tags that the user has entered.
063     */
064    public static class UserInputTag {
065        private final String key;
066        private final String value;
067        private final boolean defaultKey;
068
069        /**
070         * Constructor.
071         *
072         * @param key the tag key
073         * @param value the tag value
074         * @param defaultKey true, if the key was not really entered by the
075         * user, e.g. for preset text fields.
076         * In this case, the key will not get any higher priority, just the value.
077         */
078        public UserInputTag(String key, String value, boolean defaultKey) {
079            this.key = key;
080            this.value = value;
081            this.defaultKey = defaultKey;
082        }
083
084        @Override
085        public int hashCode() {
086            int hash = 7;
087            hash = 59 * hash + Objects.hashCode(this.key);
088            hash = 59 * hash + Objects.hashCode(this.value);
089            hash = 59 * hash + (this.defaultKey ? 1 : 0);
090            return hash;
091        }
092
093        @Override
094        public boolean equals(Object obj) {
095            if (obj == null || getClass() != obj.getClass()) {
096                return false;
097            }
098            final UserInputTag other = (UserInputTag) obj;
099            return Objects.equals(this.key, other.key)
100                && Objects.equals(this.value, other.value)
101                && this.defaultKey == other.defaultKey;
102        }
103    }
104
105    /** If the dirty flag is set true, a rebuild is necessary. */
106    protected boolean dirty;
107    /** The data set that is managed */
108    protected DataSet ds;
109
110    /**
111     * the cached tags given by a tag key and a list of values for this tag
112     * only accessed by getTagCache(), rebuild() and cachePrimitiveTags()
113     * use getTagCache() accessor
114     */
115    protected MultiMap<String, String> tagCache;
116
117    /**
118     * the same as tagCache but for the preset keys and values can be accessed directly
119     */
120    protected static final MultiMap<String, String> PRESET_TAG_CACHE = new MultiMap<>();
121
122    /**
123     * Cache for tags that have been entered by the user.
124     */
125    protected static final Set<UserInputTag> USER_INPUT_TAG_CACHE = new LinkedHashSet<>();
126
127    /**
128     * the cached list of member roles
129     * only accessed by getRoleCache(), rebuild() and cacheRelationMemberRoles()
130     * use getRoleCache() accessor
131     */
132    protected Set<String> roleCache;
133
134    /**
135     * the same as roleCache but for the preset roles can be accessed directly
136     */
137    protected static final Set<String> PRESET_ROLE_CACHE = new HashSet<>();
138
139    /**
140     * Constructs a new {@code AutoCompletionManager}.
141     * @param ds data set
142     */
143    public AutoCompletionManager(DataSet ds) {
144        this.ds = ds;
145        this.dirty = true;
146    }
147
148    protected MultiMap<String, String> getTagCache() {
149        if (dirty) {
150            rebuild();
151            dirty = false;
152        }
153        return tagCache;
154    }
155
156    protected Set<String> getRoleCache() {
157        if (dirty) {
158            rebuild();
159            dirty = false;
160        }
161        return roleCache;
162    }
163
164    /**
165     * initializes the cache from the primitives in the dataset
166     */
167    protected void rebuild() {
168        tagCache = new MultiMap<>();
169        roleCache = new HashSet<>();
170        cachePrimitives(ds.allNonDeletedCompletePrimitives());
171    }
172
173    protected void cachePrimitives(Collection<? extends OsmPrimitive> primitives) {
174        for (OsmPrimitive primitive : primitives) {
175            cachePrimitiveTags(primitive);
176            if (primitive instanceof Relation) {
177                cacheRelationMemberRoles((Relation) primitive);
178            }
179        }
180    }
181
182    /**
183     * make sure, the keys and values of all tags held by primitive are
184     * in the auto completion cache
185     *
186     * @param primitive an OSM primitive
187     */
188    protected void cachePrimitiveTags(OsmPrimitive primitive) {
189        for (String key: primitive.keySet()) {
190            String value = primitive.get(key);
191            tagCache.put(key, value);
192        }
193    }
194
195    /**
196     * Caches all member roles of the relation <code>relation</code>
197     *
198     * @param relation the relation
199     */
200    protected void cacheRelationMemberRoles(Relation relation) {
201        for (RelationMember m: relation.getMembers()) {
202            if (m.hasRole()) {
203                roleCache.add(m.getRole());
204            }
205        }
206    }
207
208    /**
209     * Initialize the cache for presets. This is done only once.
210     * @param presets Tagging presets to cache
211     */
212    public static void cachePresets(Collection<TaggingPreset> presets) {
213        for (final TaggingPreset p : presets) {
214            for (TaggingPresetItem item : p.data) {
215                cachePresetItem(p, item);
216            }
217        }
218    }
219
220    protected static void cachePresetItem(TaggingPreset p, TaggingPresetItem item) {
221        if (item instanceof KeyedItem) {
222            KeyedItem ki = (KeyedItem) item;
223            if (ki.key != null && ki.getValues() != null) {
224                try {
225                    PRESET_TAG_CACHE.putAll(ki.key, ki.getValues());
226                } catch (NullPointerException e) {
227                    Main.error(p + ": Unable to cache " + ki);
228                }
229            }
230        } else if (item instanceof Roles) {
231            Roles r = (Roles) item;
232            for (Role i : r.roles) {
233                if (i.key != null) {
234                    PRESET_ROLE_CACHE.add(i.key);
235                }
236            }
237        } else if (item instanceof CheckGroup) {
238            for (KeyedItem check : ((CheckGroup) item).checks) {
239                cachePresetItem(p, check);
240            }
241        }
242    }
243
244    /**
245     * Remembers user input for the given key/value.
246     * @param key Tag key
247     * @param value Tag value
248     * @param defaultKey true, if the key was not really entered by the user, e.g. for preset text fields
249     */
250    public static void rememberUserInput(String key, String value, boolean defaultKey) {
251        UserInputTag tag = new UserInputTag(key, value, defaultKey);
252        USER_INPUT_TAG_CACHE.remove(tag); // re-add, so it gets to the last position of the LinkedHashSet
253        USER_INPUT_TAG_CACHE.add(tag);
254    }
255
256    /**
257     * replies the keys held by the cache
258     *
259     * @return the list of keys held by the cache
260     */
261    protected List<String> getDataKeys() {
262        return new ArrayList<>(getTagCache().keySet());
263    }
264
265    protected List<String> getPresetKeys() {
266        return new ArrayList<>(PRESET_TAG_CACHE.keySet());
267    }
268
269    protected Collection<String> getUserInputKeys() {
270        List<String> keys = new ArrayList<>();
271        for (UserInputTag tag : USER_INPUT_TAG_CACHE) {
272            if (!tag.defaultKey) {
273                keys.add(tag.key);
274            }
275        }
276        Collections.reverse(keys);
277        return new LinkedHashSet<>(keys);
278    }
279
280    /**
281     * replies the auto completion values allowed for a specific key. Replies
282     * an empty list if key is null or if key is not in {@link #getKeys()}.
283     *
284     * @param key OSM key
285     * @return the list of auto completion values
286     */
287    protected List<String> getDataValues(String key) {
288        return new ArrayList<>(getTagCache().getValues(key));
289    }
290
291    protected static List<String> getPresetValues(String key) {
292        return new ArrayList<>(PRESET_TAG_CACHE.getValues(key));
293    }
294
295    protected static Collection<String> getUserInputValues(String key) {
296        List<String> values = new ArrayList<>();
297        for (UserInputTag tag : USER_INPUT_TAG_CACHE) {
298            if (key.equals(tag.key)) {
299                values.add(tag.value);
300            }
301        }
302        Collections.reverse(values);
303        return new LinkedHashSet<>(values);
304    }
305
306    /**
307     * Replies the list of member roles
308     *
309     * @return the list of member roles
310     */
311    public List<String> getMemberRoles() {
312        return new ArrayList<>(getRoleCache());
313    }
314
315    /**
316     * Populates the {@link AutoCompletionList} with the currently cached
317     * member roles.
318     *
319     * @param list the list to populate
320     */
321    public void populateWithMemberRoles(AutoCompletionList list) {
322        list.add(PRESET_ROLE_CACHE, AutoCompletionItemPriority.IS_IN_STANDARD);
323        list.add(getRoleCache(), AutoCompletionItemPriority.IS_IN_DATASET);
324    }
325
326    /**
327     * Populates the {@link AutoCompletionList} with the roles used in this relation
328     * plus the ones defined in its applicable presets, if any. If the relation type is unknown,
329     * then all the roles known globally will be added, as in {@link #populateWithMemberRoles(AutoCompletionList)}.
330     *
331     * @param list the list to populate
332     * @param r the relation to get roles from
333     * @throws IllegalArgumentException if list is null
334     * @since 7556
335     */
336    public void populateWithMemberRoles(AutoCompletionList list, Relation r) {
337        CheckParameterUtil.ensureParameterNotNull(list, "list");
338        Collection<TaggingPreset> presets = r != null ? TaggingPresets.getMatchingPresets(null, r.getKeys(), false) : null;
339        if (r != null && presets != null && !presets.isEmpty()) {
340            for (TaggingPreset tp : presets) {
341                if (tp.roles != null) {
342                    list.add(Utils.transform(tp.roles.roles, new Utils.Function<Role, String>() {
343                        public String apply(Role x) {
344                            return x.key;
345                        }
346                    }), AutoCompletionItemPriority.IS_IN_STANDARD);
347                }
348            }
349            list.add(r.getMemberRoles(), AutoCompletionItemPriority.IS_IN_DATASET);
350        } else {
351            populateWithMemberRoles(list);
352        }
353    }
354
355    /**
356     * Populates the an {@link AutoCompletionList} with the currently cached tag keys
357     *
358     * @param list the list to populate
359     */
360    public void populateWithKeys(AutoCompletionList list) {
361        list.add(getPresetKeys(), AutoCompletionItemPriority.IS_IN_STANDARD);
362        list.add(new AutoCompletionListItem("source", AutoCompletionItemPriority.IS_IN_STANDARD));
363        list.add(getDataKeys(), AutoCompletionItemPriority.IS_IN_DATASET);
364        list.addUserInput(getUserInputKeys());
365    }
366
367    /**
368     * Populates the an {@link AutoCompletionList} with the currently cached
369     * values for a tag
370     *
371     * @param list the list to populate
372     * @param key the tag key
373     */
374    public void populateWithTagValues(AutoCompletionList list, String key) {
375        populateWithTagValues(list, Arrays.asList(key));
376    }
377
378    /**
379     * Populates the an {@link AutoCompletionList} with the currently cached
380     * values for some given tags
381     *
382     * @param list the list to populate
383     * @param keys the tag keys
384     */
385    public void populateWithTagValues(AutoCompletionList list, List<String> keys) {
386        for (String key : keys) {
387            list.add(getPresetValues(key), AutoCompletionItemPriority.IS_IN_STANDARD);
388            list.add(getDataValues(key), AutoCompletionItemPriority.IS_IN_DATASET);
389            list.addUserInput(getUserInputValues(key));
390        }
391    }
392
393    /**
394     * Returns the currently cached tag keys.
395     * @return a list of tag keys
396     */
397    public List<AutoCompletionListItem> getKeys() {
398        AutoCompletionList list = new AutoCompletionList();
399        populateWithKeys(list);
400        return list.getList();
401    }
402
403    /**
404     * Returns the currently cached tag values for a given tag key.
405     * @param key the tag key
406     * @return a list of tag values
407     */
408    public List<AutoCompletionListItem> getValues(String key) {
409        return getValues(Arrays.asList(key));
410    }
411
412    /**
413     * Returns the currently cached tag values for a given list of tag keys.
414     * @param keys the tag keys
415     * @return a list of tag values
416     */
417    public List<AutoCompletionListItem> getValues(List<String> keys) {
418        AutoCompletionList list = new AutoCompletionList();
419        populateWithTagValues(list, keys);
420        return list.getList();
421    }
422
423    /*********************************************************
424     * Implementation of the DataSetListener interface
425     *
426     **/
427
428    @Override
429    public void primitivesAdded(PrimitivesAddedEvent event) {
430        if (dirty)
431            return;
432        cachePrimitives(event.getPrimitives());
433    }
434
435    @Override
436    public void primitivesRemoved(PrimitivesRemovedEvent event) {
437        dirty = true;
438    }
439
440    @Override
441    public void tagsChanged(TagsChangedEvent event) {
442        if (dirty)
443            return;
444        Map<String, String> newKeys = event.getPrimitive().getKeys();
445        Map<String, String> oldKeys = event.getOriginalKeys();
446
447        if (!newKeys.keySet().containsAll(oldKeys.keySet())) {
448            // Some keys removed, might be the last instance of key, rebuild necessary
449            dirty = true;
450        } else {
451            for (Entry<String, String> oldEntry: oldKeys.entrySet()) {
452                if (!oldEntry.getValue().equals(newKeys.get(oldEntry.getKey()))) {
453                    // Value changed, might be last instance of value, rebuild necessary
454                    dirty = true;
455                    return;
456                }
457            }
458            cachePrimitives(Collections.singleton(event.getPrimitive()));
459        }
460    }
461
462    @Override
463    public void nodeMoved(NodeMovedEvent event) {/* ignored */}
464
465    @Override
466    public void wayNodesChanged(WayNodesChangedEvent event) {/* ignored */}
467
468    @Override
469    public void relationMembersChanged(RelationMembersChangedEvent event) {
470        dirty = true; // TODO: not necessary to rebuid if a member is added
471    }
472
473    @Override
474    public void otherDatasetChange(AbstractDatasetChangedEvent event) {/* ignored */}
475
476    @Override
477    public void dataChanged(DataChangedEvent event) {
478        dirty = true;
479    }
480}