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.io.IOException;
009import java.util.ArrayList;
010import java.util.Collection;
011import java.util.Collections;
012import java.util.HashMap;
013import java.util.List;
014import java.util.Map;
015
016import javax.swing.BorderFactory;
017import javax.swing.JCheckBox;
018import javax.swing.JLabel;
019import javax.swing.JOptionPane;
020import javax.swing.JPanel;
021
022import org.openstreetmap.josm.Main;
023import org.openstreetmap.josm.gui.ExtendedDialog;
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.PreferenceTabbedPane.ValidationListener;
028import org.openstreetmap.josm.gui.preferences.SourceEditor;
029import org.openstreetmap.josm.gui.preferences.SourceEditor.ExtendedSourceEntry;
030import org.openstreetmap.josm.gui.preferences.SourceEntry;
031import org.openstreetmap.josm.gui.preferences.SourceProvider;
032import org.openstreetmap.josm.gui.preferences.SourceType;
033import org.openstreetmap.josm.gui.preferences.SubPreferenceSetting;
034import org.openstreetmap.josm.gui.preferences.TabPreferenceSetting;
035import org.openstreetmap.josm.gui.tagging.TaggingPresetReader;
036import org.openstreetmap.josm.tools.GBC;
037import org.xml.sax.SAXException;
038import org.xml.sax.SAXParseException;
039
040/**
041 * Preference settings for tagging presets.
042 */
043public final class TaggingPresetPreference implements SubPreferenceSetting {
044
045    /**
046     * Factory used to create a new {@code TaggingPresetPreference}.
047     */
048    public static class Factory implements PreferenceSettingFactory {
049        @Override
050        public PreferenceSetting createPreferenceSetting() {
051            return new TaggingPresetPreference();
052        }
053    }
054
055    private TaggingPresetPreference() {
056        super();
057    }
058
059    private static final List<SourceProvider> presetSourceProviders = new ArrayList<>();
060
061    private SourceEditor sources;
062    private JCheckBox sortMenu;
063
064    /**
065     * Registers a new additional preset source provider.
066     * @param provider The preset source provider
067     * @return {@code true}, if the provider has been added, {@code false} otherwise
068     */
069    public static final boolean registerSourceProvider(SourceProvider provider) {
070        if (provider != null)
071            return presetSourceProviders.add(provider);
072        return false;
073    }
074
075    private ValidationListener validationListener = new ValidationListener() {
076        @Override
077        public boolean validatePreferences() {
078            if (sources.hasActiveSourcesChanged()) {
079                List<Integer> sourcesToRemove = new ArrayList<>();
080                int i = -1;
081                SOURCES:
082                    for (SourceEntry source: sources.getActiveSources()) {
083                        i++;
084                        boolean canLoad = false;
085                        try {
086                            TaggingPresetReader.readAll(source.url, false);
087                            canLoad = true;
088                        } catch (IOException e) {
089                            Main.warn(tr("Could not read tagging preset source: {0}", source));
090                            ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Error"),
091                                    new String[] {tr("Yes"), tr("No"), tr("Cancel")});
092                            ed.setContent(tr("Could not read tagging preset source: {0}\nDo you want to keep it?", source));
093                            switch (ed.showDialog().getValue()) {
094                            case 1:
095                                continue SOURCES;
096                            case 2:
097                                sourcesToRemove.add(i);
098                                continue SOURCES;
099                            default:
100                                return false;
101                            }
102                        } catch (SAXException e) {
103                            // We will handle this in step with validation
104                        }
105
106                        String errorMessage = null;
107
108                        try {
109                            TaggingPresetReader.readAll(source.url, true);
110                        } catch (IOException e) {
111                            // Should not happen, but at least show message
112                            String msg = tr("Could not read tagging preset source {0}", source);
113                            Main.error(msg);
114                            JOptionPane.showMessageDialog(Main.parent, msg);
115                            return false;
116                        } catch (SAXParseException e) {
117                            if (canLoad) {
118                                errorMessage = tr("<html>Tagging preset source {0} can be loaded but it contains errors. " +
119                                        "Do you really want to use it?<br><br><table width=600>Error is: [{1}:{2}] {3}</table></html>",
120                                        source, e.getLineNumber(), e.getColumnNumber(), e.getMessage());
121                            } else {
122                                errorMessage = tr("<html>Unable to parse tagging preset source: {0}. " +
123                                        "Do you really want to use it?<br><br><table width=400>Error is: [{1}:{2}] {3}</table></html>",
124                                        source, e.getLineNumber(), e.getColumnNumber(), e.getMessage());
125                            }
126                        } catch (SAXException e) {
127                            if (canLoad) {
128                                errorMessage = tr("<html>Tagging preset source {0} can be loaded but it contains errors. " +
129                                        "Do you really want to use it?<br><br><table width=600>Error is: {1}</table></html>",
130                                        source,  e.getMessage());
131                            } else {
132                                errorMessage = tr("<html>Unable to parse tagging preset source: {0}. " +
133                                        "Do you really want to use it?<br><br><table width=600>Error is: {1}</table></html>",
134                                        source, e.getMessage());
135                            }
136
137                        }
138
139                        if (errorMessage != null) {
140                            Main.error(errorMessage);
141                            int result = JOptionPane.showConfirmDialog(Main.parent, new JLabel(errorMessage), tr("Error"),
142                                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.ERROR_MESSAGE);
143
144                            switch (result) {
145                            case JOptionPane.YES_OPTION:
146                                continue SOURCES;
147                            case JOptionPane.NO_OPTION:
148                                sourcesToRemove.add(i);
149                                continue SOURCES;
150                            default:
151                                return false;
152                            }
153                        }
154                    }
155                sources.removeSources(sourcesToRemove);
156                return true;
157            }  else
158                return true;
159        }
160    };
161
162    @Override
163    public void addGui(PreferenceTabbedPane gui) {
164        sortMenu = new JCheckBox(tr("Sort presets menu"),
165                Main.pref.getBoolean("taggingpreset.sortmenu", false));
166
167        final JPanel panel = new JPanel(new GridBagLayout());
168        panel.setBorder(BorderFactory.createEmptyBorder( 0, 0, 0, 0 ));
169        panel.add(sortMenu, GBC.eol().insets(5,5,5,0));
170        sources = new TaggingPresetSourceEditor();
171        panel.add(sources, GBC.eol().fill(GBC.BOTH));
172        final MapPreference mapPref = gui.getMapPreference();
173        mapPref.addSubTab(this, tr("Tagging Presets"), panel);
174        sources.deferLoading(mapPref, panel);
175        gui.addValidationListener(validationListener);
176    }
177
178    static class TaggingPresetSourceEditor extends SourceEditor {
179
180        private static final String iconpref = "taggingpreset.icon.sources";
181
182        public TaggingPresetSourceEditor() {
183            super(SourceType.TAGGING_PRESET, Main.getJOSMWebsite()+"/presets", presetSourceProviders, true);
184        }
185
186        @Override
187        public Collection<? extends SourceEntry> getInitialSourcesList() {
188            return PresetPrefHelper.INSTANCE.get();
189        }
190
191        @Override
192        public boolean finish() {
193            List<SourceEntry> activeStyles = activeSourcesModel.getSources();
194
195            boolean changed = PresetPrefHelper.INSTANCE.put(activeStyles);
196
197            if (tblIconPaths != null) {
198                List<String> iconPaths = iconPathsModel.getIconPaths();
199
200                if (!iconPaths.isEmpty()) {
201                    if (Main.pref.putCollection(iconpref, iconPaths)) {
202                        changed = true;
203                    }
204                } else if (Main.pref.putCollection(iconpref, null)) {
205                    changed = true;
206                }
207            }
208            return changed;
209        }
210
211        @Override
212        public Collection<ExtendedSourceEntry> getDefault() {
213            return PresetPrefHelper.INSTANCE.getDefault();
214        }
215
216        @Override
217        public Collection<String> getInitialIconPathsList() {
218            return Main.pref.getCollection(iconpref, null);
219        }
220
221        @Override
222        public String getStr(I18nString ident) {
223            switch (ident) {
224            case AVAILABLE_SOURCES:
225                return tr("Available presets:");
226            case ACTIVE_SOURCES:
227                return tr("Active presets:");
228            case NEW_SOURCE_ENTRY_TOOLTIP:
229                return tr("Add a new preset by entering filename or URL");
230            case NEW_SOURCE_ENTRY:
231                return tr("New preset entry:");
232            case REMOVE_SOURCE_TOOLTIP:
233                return tr("Remove the selected presets from the list of active presets");
234            case EDIT_SOURCE_TOOLTIP:
235                return tr("Edit the filename or URL for the selected active preset");
236            case ACTIVATE_TOOLTIP:
237                return tr("Add the selected available presets to the list of active presets");
238            case RELOAD_ALL_AVAILABLE:
239                return marktr("Reloads the list of available presets from ''{0}''");
240            case LOADING_SOURCES_FROM:
241                return marktr("Loading preset sources from ''{0}''");
242            case FAILED_TO_LOAD_SOURCES_FROM:
243                return marktr("<html>Failed to load the list of preset sources from<br>"
244                        + "''{0}''.<br>"
245                        + "<br>"
246                        + "Details (untranslated):<br>{1}</html>");
247            case FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC:
248                return "/Preferences/Presets#FailedToLoadPresetSources";
249            case ILLEGAL_FORMAT_OF_ENTRY:
250                return marktr("Warning: illegal format of entry in preset list ''{0}''. Got ''{1}''");
251            default: throw new AssertionError();
252            }
253        }
254    }
255
256    @Override
257    public boolean ok() {
258        boolean restart = Main.pref.put("taggingpreset.sortmenu", sortMenu.getSelectedObjects() != null);
259        restart |= sources.finish();
260
261        return restart;
262    }
263
264    /**
265     * Helper class for tagging presets preferences.
266     */
267    public static class PresetPrefHelper extends SourceEditor.SourcePrefHelper {
268
269        /**
270         * The unique instance.
271         */
272        public static final PresetPrefHelper INSTANCE = new PresetPrefHelper();
273
274        /**
275         * Constructs a new {@code PresetPrefHelper}.
276         */
277        public PresetPrefHelper() {
278            super("taggingpreset.entries");
279        }
280
281        @Override
282        public Collection<ExtendedSourceEntry> getDefault() {
283            ExtendedSourceEntry i = new ExtendedSourceEntry("defaultpresets.xml", "resource://data/defaultpresets.xml");
284            i.title = tr("Internal Preset");
285            i.description = tr("The default preset for JOSM");
286            return Collections.singletonList(i);
287        }
288
289        @Override
290        public Map<String, String> serialize(SourceEntry entry) {
291            Map<String, String> res = new HashMap<>();
292            res.put("url", entry.url);
293            res.put("title", entry.title == null ? "" : entry.title);
294            return res;
295        }
296
297        @Override
298        public SourceEntry deserialize(Map<String, String> s) {
299            return new SourceEntry(s.get("url"), null, s.get("title"), true);
300        }
301    }
302
303    @Override
304    public boolean isExpert() {
305        return false;
306    }
307
308    @Override
309    public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) {
310        return gui.getMapPreference();
311    }
312}