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