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