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            Main.debug(ignore);
170        }
171        return null;
172    }
173
174    @Override
175    public boolean ok() {
176        boolean reload = Main.pref.put("mappaint.icon.enable-defaults", enableIconDefault.isSelected());
177        reload |= sources.finish();
178        if (reload) {
179            MapPaintStyles.readFromPreferences();
180        }
181        if (Main.isDisplayingMapView()) {
182            MapPaintStyles.getStyles().clearCached();
183        }
184        return false;
185    }
186
187    /**
188     * Initialize the styles
189     */
190    public static void initialize() {
191        MapPaintStyles.readFromPreferences();
192    }
193
194    /**
195     * Helper class for map paint styles preferences.
196     */
197    public static class MapPaintPrefHelper extends SourceEditor.SourcePrefHelper {
198
199        /**
200         * The unique instance.
201         */
202        public static final MapPaintPrefHelper INSTANCE = new MapPaintPrefHelper();
203
204        /**
205         * Constructs a new {@code MapPaintPrefHelper}.
206         */
207        public MapPaintPrefHelper() {
208            super("mappaint.style.entries");
209        }
210
211        @Override
212        public List<SourceEntry> get() {
213            List<SourceEntry> ls = super.get();
214            if (insertNewDefaults(ls)) {
215                put(ls);
216            }
217            return ls;
218        }
219
220        /**
221         * If the selection of default styles changes in future releases, add
222         * the new entries to the user-configured list. Remember the known URLs,
223         * so an item that was deleted explicitly is not added again.
224         * @param list new defaults
225         * @return {@code true} if a change occurred
226         */
227        private boolean insertNewDefaults(List<SourceEntry> list) {
228            boolean changed = false;
229
230            Collection<String> knownDefaults = new TreeSet<>(Main.pref.getCollection("mappaint.style.known-defaults"));
231
232            Collection<ExtendedSourceEntry> defaults = getDefault();
233            int insertionIdx = 0;
234            for (final SourceEntry def : defaults) {
235                int i = Utils.indexOf(list,
236                        new Predicate<SourceEntry>() {
237                    @Override
238                    public boolean evaluate(SourceEntry se) {
239                        return Objects.equals(def.url, se.url);
240                    }
241                });
242                if (i == -1 && !knownDefaults.contains(def.url)) {
243                    def.active = false;
244                    list.add(insertionIdx, def);
245                    insertionIdx++;
246                    changed = true;
247                } else {
248                    if (i >= insertionIdx) {
249                        insertionIdx = i + 1;
250                    }
251                }
252                knownDefaults.add(def.url);
253            }
254            Main.pref.putCollection("mappaint.style.known-defaults", knownDefaults);
255
256            // XML style is not bundled anymore
257            list.remove(Utils.find(list, new Predicate<SourceEntry>() {
258                            @Override
259                            public boolean evaluate(SourceEntry se) {
260                                return "resource://styles/standard/elemstyles.xml".equals(se.url);
261                            }
262                        }));
263
264            return changed;
265        }
266
267        @Override
268        public Collection<ExtendedSourceEntry> getDefault() {
269            ExtendedSourceEntry defJosmMapcss = new ExtendedSourceEntry("elemstyles.mapcss", "resource://styles/standard/elemstyles.mapcss");
270            defJosmMapcss.active = true;
271            defJosmMapcss.name = "standard";
272            defJosmMapcss.title = tr("JOSM default (MapCSS)");
273            defJosmMapcss.description = tr("Internal style to be used as base for runtime switchable overlay styles");
274            ExtendedSourceEntry defPL2 = new ExtendedSourceEntry("potlatch2.mapcss", "resource://styles/standard/potlatch2.mapcss");
275            defPL2.active = false;
276            defPL2.name = "standard";
277            defPL2.title = tr("Potlatch 2");
278            defPL2.description = tr("the main Potlatch 2 style");
279
280            return Arrays.asList(new ExtendedSourceEntry[] {defJosmMapcss, defPL2});
281        }
282
283        @Override
284        public Map<String, String> serialize(SourceEntry entry) {
285            Map<String, String> res = new HashMap<>();
286            res.put("url", entry.url);
287            res.put("title", entry.title == null ? "" : entry.title);
288            res.put("active", Boolean.toString(entry.active));
289            if (entry.name != null) {
290                res.put("ptoken", entry.name);
291            }
292            return res;
293        }
294
295        @Override
296        public SourceEntry deserialize(Map<String, String> s) {
297            return new SourceEntry(s.get("url"), s.get("ptoken"), s.get("title"), Boolean.parseBoolean(s.get("active")));
298        }
299    }
300
301    @Override
302    public boolean isExpert() {
303        return false;
304    }
305
306    @Override
307    public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) {
308        return gui.getMapPreference();
309    }
310}