001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.map;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.GridBagLayout;
008import java.util.ArrayList;
009import java.util.Arrays;
010import java.util.Collection;
011import java.util.HashMap;
012import java.util.List;
013import java.util.Map;
014import java.util.Objects;
015import java.util.TreeSet;
016
017import javax.swing.BorderFactory;
018import javax.swing.JCheckBox;
019import javax.swing.JPanel;
020
021import org.openstreetmap.josm.Main;
022import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
023import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
024import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
025import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
026import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
027import org.openstreetmap.josm.gui.preferences.SourceEditor;
028import org.openstreetmap.josm.gui.preferences.SourceEditor.ExtendedSourceEntry;
029import org.openstreetmap.josm.gui.preferences.SourceEntry;
030import org.openstreetmap.josm.gui.preferences.SourceProvider;
031import org.openstreetmap.josm.gui.preferences.SourceType;
032import org.openstreetmap.josm.gui.preferences.SubPreferenceSetting;
033import org.openstreetmap.josm.gui.preferences.TabPreferenceSetting;
034import org.openstreetmap.josm.tools.GBC;
035import org.openstreetmap.josm.tools.Predicate;
036import org.openstreetmap.josm.tools.Utils;
037
038/**
039 * Preference settings for map paint styles.
040 */
041public class MapPaintPreference implements SubPreferenceSetting {
042    private SourceEditor sources;
043    private JCheckBox enableIconDefault;
044
045    private static final List<SourceProvider> styleSourceProviders = new ArrayList<>();
046
047    /**
048     * Registers a new additional style source provider.
049     * @param provider The style source provider
050     * @return {@code true}, if the provider has been added, {@code false} otherwise
051     */
052    public static boolean registerSourceProvider(SourceProvider provider) {
053        if (provider != null)
054            return styleSourceProviders.add(provider);
055        return false;
056    }
057
058    /**
059     * Factory used to create a new {@code MapPaintPreference}.
060     */
061    public static class Factory implements PreferenceSettingFactory {
062        @Override
063        public PreferenceSetting createPreferenceSetting() {
064            return new MapPaintPreference();
065        }
066    }
067
068    @Override
069    public void addGui(PreferenceTabbedPane gui) {
070        enableIconDefault = new JCheckBox(tr("Enable built-in icon defaults"),
071                Main.pref.getBoolean("mappaint.icon.enable-defaults", true));
072
073        sources = new MapPaintSourceEditor();
074
075        final JPanel panel = new JPanel(new GridBagLayout());
076        panel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
077
078        panel.add(sources, GBC.eol().fill(GBC.BOTH));
079        panel.add(enableIconDefault, GBC.eol().insets(11, 2, 5, 0));
080
081        final MapPreference mapPref = gui.getMapPreference();
082        mapPref.addSubTab(this, tr("Map Paint Styles"), panel);
083        sources.deferLoading(mapPref, panel);
084    }
085
086    static class MapPaintSourceEditor extends SourceEditor {
087
088        private static final String ICONPREF = "mappaint.icon.sources";
089
090        MapPaintSourceEditor() {
091            super(SourceType.MAP_PAINT_STYLE, Main.getJOSMWebsite()+"/styles", styleSourceProviders, true);
092        }
093
094        @Override
095        public Collection<? extends SourceEntry> getInitialSourcesList() {
096            return MapPaintPrefHelper.INSTANCE.get();
097        }
098
099        @Override
100        public boolean finish() {
101            return doFinish(MapPaintPrefHelper.INSTANCE, ICONPREF);
102        }
103
104        @Override
105        public Collection<ExtendedSourceEntry> getDefault() {
106            return MapPaintPrefHelper.INSTANCE.getDefault();
107        }
108
109        @Override
110        public Collection<String> getInitialIconPathsList() {
111            return Main.pref.getCollection(ICONPREF, null);
112        }
113
114        @Override
115        public String getStr(I18nString ident) {
116            switch (ident) {
117            case AVAILABLE_SOURCES:
118                return tr("Available styles:");
119            case ACTIVE_SOURCES:
120                return tr("Active styles:");
121            case NEW_SOURCE_ENTRY_TOOLTIP:
122                return tr("Add a new style by entering filename or URL");
123            case NEW_SOURCE_ENTRY:
124                return tr("New style entry:");
125            case REMOVE_SOURCE_TOOLTIP:
126                return tr("Remove the selected styles from the list of active styles");
127            case EDIT_SOURCE_TOOLTIP:
128                return tr("Edit the filename or URL for the selected active style");
129            case ACTIVATE_TOOLTIP:
130                return tr("Add the selected available styles to the list of active styles");
131            case RELOAD_ALL_AVAILABLE:
132                return marktr("Reloads the list of available styles from ''{0}''");
133            case LOADING_SOURCES_FROM:
134                return marktr("Loading style sources from ''{0}''");
135            case FAILED_TO_LOAD_SOURCES_FROM:
136                return marktr("<html>Failed to load the list of style sources from<br>"
137                        + "''{0}''.<br>"
138                        + "<br>"
139                        + "Details (untranslated):<br>{1}</html>");
140            case FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC:
141                return "/Preferences/Styles#FailedToLoadStyleSources";
142            case ILLEGAL_FORMAT_OF_ENTRY:
143                return marktr("Warning: illegal format of entry in style list ''{0}''. Got ''{1}''");
144            default: throw new AssertionError();
145            }
146        }
147
148        @Override
149        protected String getTitleForSourceEntry(SourceEntry entry) {
150            final String title = getTitleFromSourceEntry(entry);
151            return title != null ? title : super.getTitleForSourceEntry(entry);
152        }
153    }
154
155    /**
156     * Returns title from a source entry.
157     * @param entry source entry
158     * @return title
159     * @see MapCSSStyleSource#title
160     */
161    public static String getTitleFromSourceEntry(SourceEntry entry) {
162        try {
163            final MapCSSStyleSource css = new MapCSSStyleSource(entry);
164            css.loadStyleSource();
165            if (css.title != null && !css.title.isEmpty()) {
166                return css.title;
167            }
168        } catch (RuntimeException ignore) {
169            if (Main.isTraceEnabled()) {
170                Main.trace(ignore.getMessage());
171            }
172        }
173        return null;
174    }
175
176    @Override
177    public boolean ok() {
178        boolean reload = Main.pref.put("mappaint.icon.enable-defaults", enableIconDefault.isSelected());
179        reload |= sources.finish();
180        if (reload) {
181            MapPaintStyles.readFromPreferences();
182        }
183        if (Main.isDisplayingMapView()) {
184            MapPaintStyles.getStyles().clearCached();
185        }
186        return false;
187    }
188
189    /**
190     * Initialize the styles
191     */
192    public static void initialize() {
193        MapPaintStyles.readFromPreferences();
194    }
195
196    /**
197     * Helper class for map paint styles preferences.
198     */
199    public static class MapPaintPrefHelper extends SourceEditor.SourcePrefHelper {
200
201        /**
202         * The unique instance.
203         */
204        public static final MapPaintPrefHelper INSTANCE = new MapPaintPrefHelper();
205
206        /**
207         * Constructs a new {@code MapPaintPrefHelper}.
208         */
209        public MapPaintPrefHelper() {
210            super("mappaint.style.entries");
211        }
212
213        @Override
214        public List<SourceEntry> get() {
215            List<SourceEntry> ls = super.get();
216            if (insertNewDefaults(ls)) {
217                put(ls);
218            }
219            return ls;
220        }
221
222        /**
223         * If the selection of default styles changes in future releases, add
224         * the new entries to the user-configured list. Remember the known URLs,
225         * so an item that was deleted explicitly is not added again.
226         * @param list new defaults
227         * @return {@code true} if a change occurred
228         */
229        private boolean insertNewDefaults(List<SourceEntry> list) {
230            boolean changed = false;
231
232            Collection<String> knownDefaults = new TreeSet<>(Main.pref.getCollection("mappaint.style.known-defaults"));
233
234            Collection<ExtendedSourceEntry> defaults = getDefault();
235            int insertionIdx = 0;
236            for (final SourceEntry def : defaults) {
237                int i = Utils.indexOf(list,
238                        new Predicate<SourceEntry>() {
239                    @Override
240                    public boolean evaluate(SourceEntry se) {
241                        return Objects.equals(def.url, se.url);
242                    }
243                });
244                if (i == -1 && !knownDefaults.contains(def.url)) {
245                    def.active = false;
246                    list.add(insertionIdx, def);
247                    insertionIdx++;
248                    changed = true;
249                } else {
250                    if (i >= insertionIdx) {
251                        insertionIdx = i + 1;
252                    }
253                }
254                knownDefaults.add(def.url);
255            }
256            Main.pref.putCollection("mappaint.style.known-defaults", knownDefaults);
257
258            // XML style is not bundled anymore
259            list.remove(Utils.find(list, new Predicate<SourceEntry>() {
260                            @Override
261                            public boolean evaluate(SourceEntry se) {
262                                return "resource://styles/standard/elemstyles.xml".equals(se.url);
263                            }
264                        }));
265
266            return changed;
267        }
268
269        @Override
270        public Collection<ExtendedSourceEntry> getDefault() {
271            ExtendedSourceEntry defJosmMapcss = new ExtendedSourceEntry("elemstyles.mapcss", "resource://styles/standard/elemstyles.mapcss");
272            defJosmMapcss.active = true;
273            defJosmMapcss.name = "standard";
274            defJosmMapcss.title = tr("JOSM default (MapCSS)");
275            defJosmMapcss.description = tr("Internal style to be used as base for runtime switchable overlay styles");
276            ExtendedSourceEntry defPL2 = new ExtendedSourceEntry("potlatch2.mapcss", "resource://styles/standard/potlatch2.mapcss");
277            defPL2.active = false;
278            defPL2.name = "standard";
279            defPL2.title = tr("Potlatch 2");
280            defPL2.description = tr("the main Potlatch 2 style");
281
282            return Arrays.asList(new ExtendedSourceEntry[] {defJosmMapcss, defPL2});
283        }
284
285        @Override
286        public Map<String, String> serialize(SourceEntry entry) {
287            Map<String, String> res = new HashMap<>();
288            res.put("url", entry.url);
289            res.put("title", entry.title == null ? "" : entry.title);
290            res.put("active", Boolean.toString(entry.active));
291            if (entry.name != null) {
292                res.put("ptoken", entry.name);
293            }
294            return res;
295        }
296
297        @Override
298        public SourceEntry deserialize(Map<String, String> s) {
299            return new SourceEntry(s.get("url"), s.get("ptoken"), s.get("title"), Boolean.parseBoolean(s.get("active")));
300        }
301    }
302
303    @Override
304    public boolean isExpert() {
305        return false;
306    }
307
308    @Override
309    public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) {
310        return gui.getMapPreference();
311    }
312}