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