001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.tagging.presets;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.util.ArrayList;
007import java.util.Collection;
008import java.util.Collections;
009import java.util.HashMap;
010import java.util.HashSet;
011import java.util.Map;
012import java.util.Set;
013
014import javax.swing.JMenu;
015import javax.swing.JMenuItem;
016import javax.swing.JSeparator;
017
018import org.openstreetmap.josm.actions.PreferencesAction;
019import org.openstreetmap.josm.data.osm.IPrimitive;
020import org.openstreetmap.josm.data.preferences.BooleanProperty;
021import org.openstreetmap.josm.data.preferences.IntegerProperty;
022import org.openstreetmap.josm.data.preferences.ListProperty;
023import org.openstreetmap.josm.gui.MainApplication;
024import org.openstreetmap.josm.gui.MainMenu;
025import org.openstreetmap.josm.gui.MenuScroller;
026import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
027import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference;
028import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup;
029import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
030import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
031import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
032import org.openstreetmap.josm.tools.Logging;
033import org.openstreetmap.josm.tools.MultiMap;
034import org.openstreetmap.josm.tools.SubclassFilteredCollection;
035
036/**
037 * Class holding Tagging Presets and allowing to manage them.
038 * @since 7100
039 */
040public final class TaggingPresets {
041
042    /** The collection of tagging presets */
043    private static final Collection<TaggingPreset> taggingPresets = new ArrayList<>();
044
045    /** cache for key/value pairs found in the preset */
046    private static final MultiMap<String, String> PRESET_TAG_CACHE = new MultiMap<>();
047    /** cache for roles found in the preset */
048    private static final Set<String> PRESET_ROLE_CACHE = new HashSet<>();
049
050    /** The collection of listeners */
051    private static final Collection<TaggingPresetListener> listeners = new ArrayList<>();
052    /**
053     * Sort presets menu alphabetically
054     */
055    public static final BooleanProperty SORT_MENU = new BooleanProperty("taggingpreset.sortvalues", true);
056    /**
057     * Custom icon sources
058     */
059    public static final ListProperty ICON_SOURCES = new ListProperty("taggingpreset.icon.sources", null);
060    private static final IntegerProperty MIN_ELEMENTS_FOR_SCROLLER = new IntegerProperty("taggingpreset.min-elements-for-scroller", 15);
061
062    private TaggingPresets() {
063        // Hide constructor for utility classes
064    }
065
066    /**
067     * Initializes tagging presets from preferences.
068     */
069    public static void readFromPreferences() {
070        taggingPresets.clear();
071        taggingPresets.addAll(TaggingPresetReader.readFromPreferences(false, false));
072        cachePresets(taggingPresets);
073    }
074
075    /**
076     * Initialize the tagging presets (load and may display error)
077     */
078    public static void initialize() {
079        MainMenu mainMenu = MainApplication.getMenu();
080        JMenu presetsMenu = mainMenu.presetsMenu;
081        if (presetsMenu.getComponentCount() == 0) {
082            MainMenu.add(presetsMenu, mainMenu.presetSearchAction);
083            MainMenu.add(presetsMenu, mainMenu.presetSearchPrimitiveAction);
084            MainMenu.add(presetsMenu, PreferencesAction.forPreferenceTab(tr("Preset preferences..."),
085                    tr("Click to open the tagging presets tab in the preferences"), TaggingPresetPreference.class));
086            presetsMenu.addSeparator();
087        }
088
089        readFromPreferences();
090        for (TaggingPreset tp: taggingPresets) {
091            if (!(tp instanceof TaggingPresetSeparator)) {
092                MainApplication.getToolbar().register(tp);
093                MainApplication.getLayerManager().addActiveLayerChangeListener(tp);
094            }
095        }
096        if (taggingPresets.isEmpty()) {
097            presetsMenu.setVisible(false);
098        } else {
099            Map<TaggingPresetMenu, JMenu> submenus = new HashMap<>();
100            for (final TaggingPreset p : taggingPresets) {
101                JMenu m = p.group != null ? submenus.get(p.group) : presetsMenu;
102                if (m == null && p.group != null) {
103                    Logging.error("No tagging preset submenu for " + p.group);
104                } else if (m == null) {
105                    Logging.error("No tagging preset menu. Tagging preset " + p + " won't be available there");
106                } else if (p instanceof TaggingPresetSeparator) {
107                    m.add(new JSeparator());
108                } else if (p instanceof TaggingPresetMenu) {
109                    JMenu submenu = new JMenu(p);
110                    submenu.setText(p.getLocaleName());
111                    ((TaggingPresetMenu) p).menu = submenu;
112                    submenus.put((TaggingPresetMenu) p, submenu);
113                    m.add(submenu);
114                } else {
115                    JMenuItem mi = new JMenuItem(p);
116                    mi.setText(p.getLocaleName());
117                    m.add(mi);
118                }
119            }
120            for (JMenu submenu : submenus.values()) {
121                if (submenu.getItemCount() >= MIN_ELEMENTS_FOR_SCROLLER.get()) {
122                    MenuScroller.setScrollerFor(submenu);
123                }
124            }
125        }
126        if (SORT_MENU.get()) {
127            TaggingPresetMenu.sortMenu(presetsMenu);
128        }
129        listeners.forEach(TaggingPresetListener::taggingPresetsModified);
130    }
131
132    // Cannot implement Destroyable since this is static
133    /**
134     * Call to deconstruct the TaggingPresets menus and other information so that it
135     * can be rebuilt later.
136     *
137     * @since 15582
138     */
139    public static void destroy() {
140        ToolbarPreferences toolBar = MainApplication.getToolbar();
141        for (TaggingPreset tp: taggingPresets) {
142            toolBar.unregister(tp);
143            if (!(tp instanceof TaggingPresetSeparator)) {
144                MainApplication.getLayerManager().removeActiveLayerChangeListener(tp);
145            }
146        }
147        taggingPresets.clear();
148        PRESET_TAG_CACHE.clear();
149        PRESET_ROLE_CACHE.clear();
150        MainApplication.getMenu().presetsMenu.removeAll();
151    }
152
153    /**
154     * Initialize the cache for presets. This is done only once.
155     * @param presets Tagging presets to cache
156     */
157    public static void cachePresets(Collection<TaggingPreset> presets) {
158        for (final TaggingPreset p : presets) {
159            for (TaggingPresetItem item : p.data) {
160                cachePresetItem(p, item);
161            }
162        }
163    }
164
165    private static void cachePresetItem(TaggingPreset p, TaggingPresetItem item) {
166        if (item instanceof KeyedItem) {
167            KeyedItem ki = (KeyedItem) item;
168            if (ki.key != null && ki.getValues() != null) {
169                PRESET_TAG_CACHE.putAll(ki.key, ki.getValues());
170            }
171        } else if (item instanceof Roles) {
172            Roles r = (Roles) item;
173            for (Role i : r.roles) {
174                if (i.key != null) {
175                    PRESET_ROLE_CACHE.add(i.key);
176                }
177            }
178        } else if (item instanceof CheckGroup) {
179            for (KeyedItem check : ((CheckGroup) item).checks) {
180                cachePresetItem(p, check);
181            }
182        }
183    }
184
185    /**
186     * Replies a new collection containing all tagging presets.
187     * @return a new collection containing all tagging presets. Empty if presets are not initialized (never null)
188     */
189    public static Collection<TaggingPreset> getTaggingPresets() {
190        return Collections.unmodifiableCollection(taggingPresets);
191    }
192
193    /**
194     * Replies a set of all roles in the tagging presets.
195     * @return a set of all roles in the tagging presets.
196     */
197    public static Set<String> getPresetRoles() {
198        return Collections.unmodifiableSet(PRESET_ROLE_CACHE);
199    }
200
201    /**
202     * Replies a set of all keys in the tagging presets.
203     * @return a set of all keys in the tagging presets.
204     */
205    public static Set<String> getPresetKeys() {
206        return Collections.unmodifiableSet(PRESET_TAG_CACHE.keySet());
207    }
208
209    /**
210     * Return set of values for a key in the tagging presets
211     * @param key the key
212     * @return set of values for a key in the tagging presets
213     */
214    public static Set<String> getPresetValues(String key) {
215        Set<String> values = PRESET_TAG_CACHE.get(key);
216        if (values != null)
217            return Collections.unmodifiableSet(values);
218        return Collections.emptySet();
219    }
220
221    /**
222     * Determines if the given key is in the loaded presets.
223     * @param key key
224     * @return {@code true} if the given key in the loaded presets
225     * @since xxx
226     */
227    public static boolean isKeyInPresets(String key) {
228        return PRESET_TAG_CACHE.get(key) != null;
229    }
230
231    /**
232     * Replies a new collection of all presets matching the parameters.
233     *
234     * @param t the preset types to include
235     * @param tags the tags to perform matching on, see {@link TaggingPresetItem#matches(Map)}
236     * @param onlyShowable whether only {@link TaggingPreset#isShowable() showable} presets should be returned
237     * @return a new collection of all presets matching the parameters.
238     * @see TaggingPreset#matches(Collection, Map, boolean)
239     * @since 9266
240     */
241    public static Collection<TaggingPreset> getMatchingPresets(final Collection<TaggingPresetType> t,
242                                                               final Map<String, String> tags, final boolean onlyShowable) {
243        return SubclassFilteredCollection.filter(getTaggingPresets(), preset -> preset.matches(t, tags, onlyShowable));
244    }
245
246    /**
247     * Replies a new collection of all presets matching the given preset.
248     *
249     * @param primitive the primitive
250     * @return a new collection of all presets matching the given preset.
251     * @see TaggingPreset#test(IPrimitive)
252     * @since 13623 (signature)
253     */
254    public static Collection<TaggingPreset> getMatchingPresets(final IPrimitive primitive) {
255        return SubclassFilteredCollection.filter(getTaggingPresets(), preset -> preset.test(primitive));
256    }
257
258    /**
259     * Adds a list of tagging presets to the current list.
260     * @param presets The tagging presets to add
261     */
262    public static void addTaggingPresets(Collection<TaggingPreset> presets) {
263        if (presets != null && taggingPresets.addAll(presets)) {
264            listeners.forEach(TaggingPresetListener::taggingPresetsModified);
265        }
266    }
267
268    /**
269     * Adds a tagging preset listener.
270     * @param listener The listener to add
271     */
272    public static void addListener(TaggingPresetListener listener) {
273        if (listener != null) {
274            listeners.add(listener);
275        }
276    }
277
278    /**
279     * Removes a tagging preset listener.
280     * @param listener The listener to remove
281     */
282    public static void removeListener(TaggingPresetListener listener) {
283        if (listener != null) {
284            listeners.remove(listener);
285        }
286    }
287}