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