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            List<SourceEntry> activeStyles = activeSourcesModel.getSources();
102
103            boolean changed = MapPaintPrefHelper.INSTANCE.put(activeStyles);
104
105            if (tblIconPaths != null) {
106                List<String> iconPaths = iconPathsModel.getIconPaths();
107
108                if (!iconPaths.isEmpty()) {
109                    if (Main.pref.putCollection(iconpref, iconPaths)) {
110                        changed = true;
111                    }
112                } else if (Main.pref.putCollection(iconpref, null)) {
113                    changed = true;
114                }
115            }
116            return changed;
117        }
118
119        @Override
120        public Collection<ExtendedSourceEntry> getDefault() {
121            return MapPaintPrefHelper.INSTANCE.getDefault();
122        }
123
124        @Override
125        public Collection<String> getInitialIconPathsList() {
126            return Main.pref.getCollection(iconpref, null);
127        }
128
129        @Override
130        public String getStr(I18nString ident) {
131            switch (ident) {
132            case AVAILABLE_SOURCES:
133                return tr("Available styles:");
134            case ACTIVE_SOURCES:
135                return tr("Active styles:");
136            case NEW_SOURCE_ENTRY_TOOLTIP:
137                return tr("Add a new style by entering filename or URL");
138            case NEW_SOURCE_ENTRY:
139                return tr("New style entry:");
140            case REMOVE_SOURCE_TOOLTIP:
141                return tr("Remove the selected styles from the list of active styles");
142            case EDIT_SOURCE_TOOLTIP:
143                return tr("Edit the filename or URL for the selected active style");
144            case ACTIVATE_TOOLTIP:
145                return tr("Add the selected available styles to the list of active styles");
146            case RELOAD_ALL_AVAILABLE:
147                return marktr("Reloads the list of available styles from ''{0}''");
148            case LOADING_SOURCES_FROM:
149                return marktr("Loading style sources from ''{0}''");
150            case FAILED_TO_LOAD_SOURCES_FROM:
151                return marktr("<html>Failed to load the list of style sources from<br>"
152                        + "''{0}''.<br>"
153                        + "<br>"
154                        + "Details (untranslated):<br>{1}</html>");
155            case FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC:
156                return "/Preferences/Styles#FailedToLoadStyleSources";
157            case ILLEGAL_FORMAT_OF_ENTRY:
158                return marktr("Warning: illegal format of entry in style list ''{0}''. Got ''{1}''");
159            default: throw new AssertionError();
160            }
161        }
162
163        @Override
164        protected String getTitleForSourceEntry(SourceEntry entry) {
165            final String title = getTitleFromSourceEntry(entry);
166            return title != null ? title : super.getTitleForSourceEntry(entry);
167        }
168    }
169
170    public static String getTitleFromSourceEntry(SourceEntry entry) {
171        try {
172            final MapCSSStyleSource css = new MapCSSStyleSource(entry);
173            css.loadStyleSource();
174            if (css.title != null && !css.title.isEmpty()) {
175                return css.title;
176            }
177        } catch (RuntimeException ignore) {
178            if (Main.isTraceEnabled()) {
179                Main.trace(ignore.getMessage());
180            }
181        }
182        return null;
183    }
184
185    @Override
186    public boolean ok() {
187        boolean reload = Main.pref.put("mappaint.icon.enable-defaults", enableIconDefault.isSelected());
188        reload |= sources.finish();
189        if (reload) {
190            MapPaintStyles.readFromPreferences();
191        }
192        if (Main.isDisplayingMapView()) {
193            MapPaintStyles.getStyles().clearCached();
194        }
195        return false;
196    }
197
198    /**
199     * Initialize the styles
200     */
201    public static void initialize() {
202        MapPaintStyles.readFromPreferences();
203    }
204
205    /**
206     * Helper class for map paint styles preferences.
207     */
208    public static class MapPaintPrefHelper extends SourceEditor.SourcePrefHelper {
209
210        /**
211         * The unique instance.
212         */
213        public static final MapPaintPrefHelper INSTANCE = new MapPaintPrefHelper();
214
215        /**
216         * Constructs a new {@code MapPaintPrefHelper}.
217         */
218        public MapPaintPrefHelper() {
219            super("mappaint.style.entries");
220        }
221
222        @Override
223        public List<SourceEntry> get() {
224            List<SourceEntry> ls = super.get();
225            if (insertNewDefaults(ls)) {
226                put(ls);
227            }
228            return ls;
229        }
230
231        /**
232         * If the selection of default styles changes in future releases, add
233         * the new entries to the user-configured list. Remember the known URLs,
234         * so an item that was deleted explicitly is not added again.
235         */
236        private boolean insertNewDefaults(List<SourceEntry> list) {
237            boolean changed = false;
238
239            Collection<String> knownDefaults = new TreeSet<>(Main.pref.getCollection("mappaint.style.known-defaults"));
240
241            Collection<ExtendedSourceEntry> defaults = getDefault();
242            int insertionIdx = 0;
243            for (final SourceEntry def : defaults) {
244                int i = Utils.indexOf(list,
245                        new Predicate<SourceEntry>() {
246                    @Override
247                    public boolean evaluate(SourceEntry se) {
248                        return Objects.equals(def.url, se.url);
249                    }
250                });
251                if (i == -1 && !knownDefaults.contains(def.url)) {
252                    def.active = false;
253                    list.add(insertionIdx, def);
254                    insertionIdx++;
255                    changed = true;
256                } else {
257                    if (i >= insertionIdx) {
258                        insertionIdx = i + 1;
259                    }
260                }
261                knownDefaults.add(def.url);
262            }
263            Main.pref.putCollection("mappaint.style.known-defaults", knownDefaults);
264
265            // XML style is not bundled anymore
266            list.remove(Utils.find(list, new Predicate<SourceEntry>() {
267                            @Override
268                            public boolean evaluate(SourceEntry se) {
269                                return "resource://styles/standard/elemstyles.xml".equals(se.url);
270                            }
271                        }));
272
273            return changed;
274        }
275
276        @Override
277        public Collection<ExtendedSourceEntry> getDefault() {
278            ExtendedSourceEntry defJosmMapcss = new ExtendedSourceEntry("elemstyles.mapcss", "resource://styles/standard/elemstyles.mapcss");
279            defJosmMapcss.active = true;
280            defJosmMapcss.name = "standard";
281            defJosmMapcss.title = tr("JOSM default (MapCSS)");
282            defJosmMapcss.description = tr("Internal style to be used as base for runtime switchable overlay styles");
283            ExtendedSourceEntry defPL2 = new ExtendedSourceEntry("potlatch2.mapcss", "resource://styles/standard/potlatch2.mapcss");
284            defPL2.active = false;
285            defPL2.name = "standard";
286            defPL2.title = tr("Potlatch 2");
287            defPL2.description = tr("the main Potlatch 2 style");
288
289            return Arrays.asList(new ExtendedSourceEntry[] {defJosmMapcss, defPL2});
290        }
291
292        @Override
293        public Map<String, String> serialize(SourceEntry entry) {
294            Map<String, String> res = new HashMap<>();
295            res.put("url", entry.url);
296            res.put("title", entry.title == null ? "" : entry.title);
297            res.put("active", Boolean.toString(entry.active));
298            if (entry.name != null) {
299                res.put("ptoken", entry.name);
300            }
301            return res;
302        }
303
304        @Override
305        public SourceEntry deserialize(Map<String, String> s) {
306            return new SourceEntry(s.get("url"), s.get("ptoken"), s.get("title"), Boolean.parseBoolean(s.get("active")));
307        }
308    }
309
310    @Override
311    public boolean isExpert() {
312        return false;
313    }
314
315    @Override
316    public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) {
317        return gui.getMapPreference();
318    }
319}