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