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