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.preferences.PreferenceSetting;
024import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
025import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
026import org.openstreetmap.josm.gui.preferences.SourceEditor;
027import org.openstreetmap.josm.gui.preferences.SourceEditor.ExtendedSourceEntry;
028import org.openstreetmap.josm.gui.preferences.SourceEntry;
029import org.openstreetmap.josm.gui.preferences.SourceProvider;
030import org.openstreetmap.josm.gui.preferences.SourceType;
031import org.openstreetmap.josm.gui.preferences.SubPreferenceSetting;
032import org.openstreetmap.josm.gui.preferences.TabPreferenceSetting;
033import org.openstreetmap.josm.tools.GBC;
034import org.openstreetmap.josm.tools.Predicate;
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    private static final String OLD_ELEMSTYLES_XML = "resource://styles/standard/elemstyles.xml";
047
048    /**
049     * Registers a new additional style source provider.
050     * @param provider The style source provider
051     * @return {@code true}, if the provider has been added, {@code false} otherwise
052     */
053    public static boolean registerSourceProvider(SourceProvider provider) {
054        if (provider != null)
055            return styleSourceProviders.add(provider);
056        return false;
057    }
058
059    /**
060     * Factory used to create a new {@code MapPaintPreference}.
061     */
062    public static class Factory implements PreferenceSettingFactory {
063        @Override
064        public PreferenceSetting createPreferenceSetting() {
065            return new MapPaintPreference();
066        }
067    }
068
069    @Override
070    public void addGui(PreferenceTabbedPane gui) {
071        enableIconDefault = new JCheckBox(tr("Enable built-in icon defaults"),
072                Main.pref.getBoolean("mappaint.icon.enable-defaults", true));
073
074        sources = new MapPaintSourceEditor();
075
076        final JPanel panel = new JPanel(new GridBagLayout());
077        panel.setBorder(BorderFactory.createEmptyBorder( 0, 0, 0, 0 ));
078
079        panel.add(sources, GBC.eol().fill(GBC.BOTH));
080        panel.add(enableIconDefault, GBC.eol().insets(11,2,5,0));
081
082        final MapPreference mapPref = gui.getMapPreference();
083        mapPref.addSubTab(this, tr("Map Paint Styles"), panel);
084        sources.deferLoading(mapPref, panel);
085    }
086
087    static class MapPaintSourceEditor extends SourceEditor {
088
089        private static final String iconpref = "mappaint.icon.sources";
090
091        public MapPaintSourceEditor() {
092            super(SourceType.MAP_PAINT_STYLE, Main.getJOSMWebsite()+"/styles", styleSourceProviders, true);
093        }
094
095        @Override
096        public Collection<? extends SourceEntry> getInitialSourcesList() {
097            return MapPaintPrefHelper.INSTANCE.get();
098        }
099
100        @Override
101        public boolean finish() {
102            List<SourceEntry> activeStyles = activeSourcesModel.getSources();
103
104            boolean changed = MapPaintPrefHelper.INSTANCE.put(activeStyles);
105
106            if (tblIconPaths != null) {
107                List<String> iconPaths = iconPathsModel.getIconPaths();
108
109                if (!iconPaths.isEmpty()) {
110                    if (Main.pref.putCollection(iconpref, iconPaths)) {
111                        changed = true;
112                    }
113                } else if (Main.pref.putCollection(iconpref, null)) {
114                    changed = true;
115                }
116            }
117            return changed;
118        }
119
120        @Override
121        public Collection<ExtendedSourceEntry> getDefault() {
122            return MapPaintPrefHelper.INSTANCE.getDefault();
123        }
124
125        @Override
126        public Collection<String> getInitialIconPathsList() {
127            return Main.pref.getCollection(iconpref, null);
128        }
129
130        @Override
131        public String getStr(I18nString ident) {
132            switch (ident) {
133            case AVAILABLE_SOURCES:
134                return tr("Available styles:");
135            case ACTIVE_SOURCES:
136                return tr("Active styles:");
137            case NEW_SOURCE_ENTRY_TOOLTIP:
138                return tr("Add a new style by entering filename or URL");
139            case NEW_SOURCE_ENTRY:
140                return tr("New style entry:");
141            case REMOVE_SOURCE_TOOLTIP:
142                return tr("Remove the selected styles from the list of active styles");
143            case EDIT_SOURCE_TOOLTIP:
144                return tr("Edit the filename or URL for the selected active style");
145            case ACTIVATE_TOOLTIP:
146                return tr("Add the selected available styles to the list of active styles");
147            case RELOAD_ALL_AVAILABLE:
148                return marktr("Reloads the list of available styles from ''{0}''");
149            case LOADING_SOURCES_FROM:
150                return marktr("Loading style sources from ''{0}''");
151            case FAILED_TO_LOAD_SOURCES_FROM:
152                return marktr("<html>Failed to load the list of style sources from<br>"
153                        + "''{0}''.<br>"
154                        + "<br>"
155                        + "Details (untranslated):<br>{1}</html>");
156            case FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC:
157                return "/Preferences/Styles#FailedToLoadStyleSources";
158            case ILLEGAL_FORMAT_OF_ENTRY:
159                return marktr("Warning: illegal format of entry in style list ''{0}''. Got ''{1}''");
160            default: throw new AssertionError();
161            }
162        }
163
164    }
165
166    @Override
167    public boolean ok() {
168        boolean reload = Main.pref.put("mappaint.icon.enable-defaults", enableIconDefault.isSelected());
169        reload |= sources.finish();
170        if (reload) {
171            MapPaintStyles.readFromPreferences();
172        }
173        if (Main.isDisplayingMapView()) {
174            MapPaintStyles.getStyles().clearCached();
175        }
176        return false;
177    }
178
179    /**
180     * Initialize the styles
181     */
182    public static void initialize() {
183        MapPaintStyles.readFromPreferences();
184    }
185
186    /**
187     * Helper class for map paint styles preferences.
188     */
189    public static class MapPaintPrefHelper extends SourceEditor.SourcePrefHelper {
190
191        /**
192         * The unique instance.
193         */
194        public static final MapPaintPrefHelper INSTANCE = new MapPaintPrefHelper();
195
196        /**
197         * Constructs a new {@code MapPaintPrefHelper}.
198         */
199        public MapPaintPrefHelper() {
200            super("mappaint.style.entries");
201        }
202
203        @Override
204        public List<SourceEntry> get() {
205            List<SourceEntry> ls = super.get();
206            if (insertNewDefaults(ls)) {
207                put(ls);
208            }
209            return ls;
210        }
211
212        /**
213         * If the selection of default styles changes in future releases, add
214         * the new entries to the user-configured list. Remember the known URLs,
215         * so an item that was deleted explicitly is not added again.
216         */
217        private boolean insertNewDefaults(List<SourceEntry> list) {
218            boolean changed = false;
219
220            boolean addedMapcssStyle = false; // Migration code can be removed ~ Nov. 2014
221
222            Collection<String> knownDefaults = new TreeSet<>(Main.pref.getCollection("mappaint.style.known-defaults"));
223
224            Collection<ExtendedSourceEntry> defaults = getDefault();
225            int insertionIdx = 0;
226            for (final SourceEntry def : defaults) {
227                int i = Utils.indexOf(list,
228                        new Predicate<SourceEntry>() {
229                    @Override
230                    public boolean evaluate(SourceEntry se) {
231                        return Objects.equals(def.url, se.url);
232                    }
233                });
234                if (i == -1 && !knownDefaults.contains(def.url)) {
235                    def.active = false;
236                    list.add(insertionIdx, def);
237                    insertionIdx++;
238                    changed = true;
239                    /* Migration code can be removed ~ Nov. 2014 */
240                    if ("resource://styles/standard/elemstyles.mapcss".equals(def.url)) {
241                        addedMapcssStyle = true;
242                    }
243                } else {
244                    if (i >= insertionIdx) {
245                        insertionIdx = i + 1;
246                    }
247                }
248            }
249
250            for (SourceEntry def : defaults) {
251                knownDefaults.add(def.url);
252            }
253            // XML style is not bundled anymore
254            knownDefaults.remove(OLD_ELEMSTYLES_XML);
255            Main.pref.putCollection("mappaint.style.known-defaults", knownDefaults);
256
257            /* Migration code can be removed ~ Nov. 2014 */
258            if (addedMapcssStyle) {
259                // change title of the XML entry
260                // only do this once. If the user changes it afterward, do not touch
261                if (!Main.pref.getBoolean("mappaint.style.migration.changedXmlName", false)) {
262                    SourceEntry josmXml = Utils.find(list, new Predicate<SourceEntry>() {
263                        @Override
264                        public boolean evaluate(SourceEntry se) {
265                            return OLD_ELEMSTYLES_XML.equals(se.url);
266                        }
267                    });
268                    if (josmXml != null) {
269                        josmXml.title = tr("JOSM default (XML; old version)");
270                        changed = true;
271                    }
272                    Main.pref.put("mappaint.style.migration.changedXmlName", true);
273                }
274            }
275
276            /* Migration code can be removed ~ Nov. 2014 */
277            if (!Main.pref.getBoolean("mappaint.style.migration.switchedToMapCSS", false)) {
278                SourceEntry josmXml = Utils.find(list, new Predicate<SourceEntry>() {
279                    @Override
280                    public boolean evaluate(SourceEntry se) {
281                        return "resource://styles/standard/elemstyles.xml".equals(se.url);
282                    }
283                });
284                SourceEntry josmMapCSS = Utils.find(list, new Predicate<SourceEntry>() {
285                    @Override
286                    public boolean evaluate(SourceEntry se) {
287                        return "resource://styles/standard/elemstyles.mapcss".equals(se.url);
288                    }
289                });
290                if (josmXml != null && josmMapCSS != null && josmXml.active) {
291                    josmMapCSS.active = true;
292                    josmXml.active = false;
293                    Main.info("Switched mappaint style from XML format to MapCSS (one time migration).");
294                    changed = true;
295                }
296                // in any case, do this check only once:
297                Main.pref.put("mappaint.style.migration.switchedToMapCSS", true);
298            }
299
300            // XML style is not bundled anymore
301            list.remove(Utils.find(list, new Predicate<SourceEntry>() {
302                        @Override
303                        public boolean evaluate(SourceEntry se) {
304                            return "resource://styles/standard/elemstyles.xml".equals(se.url);
305                        }}));
306
307            return changed;
308        }
309
310        @Override
311        public Collection<ExtendedSourceEntry> getDefault() {
312            ExtendedSourceEntry defJosmMapcss = new ExtendedSourceEntry("elemstyles.mapcss", "resource://styles/standard/elemstyles.mapcss");
313            defJosmMapcss.active = true;
314            defJosmMapcss.name = "standard";
315            defJosmMapcss.title = tr("JOSM default (MapCSS)");
316            defJosmMapcss.description = tr("Internal style to be used as base for runtime switchable overlay styles");
317            ExtendedSourceEntry defPL2 = new ExtendedSourceEntry("potlatch2.mapcss", "resource://styles/standard/potlatch2.mapcss");
318            defPL2.active = false;
319            defPL2.name = "standard";
320            defPL2.title = tr("Potlatch 2");
321            defPL2.description = tr("the main Potlatch 2 style");
322
323            return Arrays.asList(new ExtendedSourceEntry[] { defJosmMapcss, defPL2 });
324        }
325
326        @Override
327        public Map<String, String> serialize(SourceEntry entry) {
328            Map<String, String> res = new HashMap<>();
329            res.put("url", entry.url);
330            res.put("title", entry.title == null ? "" : entry.title);
331            res.put("active", Boolean.toString(entry.active));
332            if (entry.name != null) {
333                res.put("ptoken", entry.name);
334            }
335            return res;
336        }
337
338        @Override
339        public SourceEntry deserialize(Map<String, String> s) {
340            return new SourceEntry(s.get("url"), s.get("ptoken"), s.get("title"), Boolean.parseBoolean(s.get("active")));
341        }
342    }
343
344    @Override
345    public boolean isExpert() {
346        return false;
347    }
348
349    @Override
350    public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) {
351        return gui.getMapPreference();
352    }
353}