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