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