001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.imagery;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.Color;
008import java.awt.Component;
009import java.awt.Dimension;
010import java.awt.FlowLayout;
011import java.awt.Font;
012import java.awt.GridBagConstraints;
013import java.awt.GridBagLayout;
014import java.awt.event.ActionEvent;
015import java.awt.event.ActionListener;
016import java.awt.event.MouseEvent;
017import java.io.IOException;
018import java.net.MalformedURLException;
019import java.net.URL;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027import javax.swing.AbstractAction;
028import javax.swing.BorderFactory;
029import javax.swing.Box;
030import javax.swing.JButton;
031import javax.swing.JLabel;
032import javax.swing.JOptionPane;
033import javax.swing.JPanel;
034import javax.swing.JScrollPane;
035import javax.swing.JSeparator;
036import javax.swing.JTabbedPane;
037import javax.swing.JTable;
038import javax.swing.JToolBar;
039import javax.swing.event.ListSelectionEvent;
040import javax.swing.event.ListSelectionListener;
041import javax.swing.event.TableModelEvent;
042import javax.swing.event.TableModelListener;
043import javax.swing.table.DefaultTableCellRenderer;
044import javax.swing.table.DefaultTableModel;
045import javax.swing.table.TableColumnModel;
046
047import org.openstreetmap.gui.jmapviewer.Coordinate;
048import org.openstreetmap.gui.jmapviewer.JMapViewer;
049import org.openstreetmap.gui.jmapviewer.MapPolygonImpl;
050import org.openstreetmap.gui.jmapviewer.MapRectangleImpl;
051import org.openstreetmap.gui.jmapviewer.interfaces.MapPolygon;
052import org.openstreetmap.gui.jmapviewer.interfaces.MapRectangle;
053import org.openstreetmap.josm.Main;
054import org.openstreetmap.josm.data.imagery.ImageryInfo;
055import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds;
056import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
057import org.openstreetmap.josm.data.imagery.OffsetBookmark;
058import org.openstreetmap.josm.data.imagery.Shape;
059import org.openstreetmap.josm.gui.download.DownloadDialog;
060import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting;
061import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
062import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
063import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
064import org.openstreetmap.josm.gui.widgets.JosmEditorPane;
065import org.openstreetmap.josm.tools.GBC;
066import org.openstreetmap.josm.tools.ImageProvider;
067import org.openstreetmap.josm.tools.LanguageInfo;
068
069/**
070 * Imagery preferences, including imagery providers, settings and offsets.
071 */
072public final class ImageryPreference extends DefaultTabPreferenceSetting {
073
074    /**
075     * Factory used to create a new {@code ImageryPreference}.
076     */
077    public static class Factory implements PreferenceSettingFactory {
078        @Override
079        public PreferenceSetting createPreferenceSetting() {
080            return new ImageryPreference();
081        }
082    }
083
084    private ImageryPreference() {
085        super("imagery", tr("Imagery Preferences"), tr("Modify list of imagery layers displayed in the Imagery menu"), false, new JTabbedPane());
086    }
087
088    private ImageryProvidersPanel imageryProviders;
089    private ImageryLayerInfo layerInfo;
090
091    private CommonSettingsPanel commonSettings;
092    private WMSSettingsPanel wmsSettings;
093    private TMSSettingsPanel tmsSettings;
094
095    private void addSettingsSection(final JPanel p, String name, JPanel section) {
096        addSettingsSection(p, name, section, GBC.eol());
097    }
098
099    private void addSettingsSection(final JPanel p, String name, JPanel section, GBC gbc) {
100        final JLabel lbl = new JLabel(name);
101        lbl.setFont(lbl.getFont().deriveFont(Font.BOLD));
102        p.add(lbl,GBC.std());
103        p.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 0));
104        p.add(section, gbc.insets(20,5,0,10));
105    }
106
107    private Component buildSettingsPanel() {
108        final JPanel p = new JPanel(new GridBagLayout());
109        p.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
110
111        addSettingsSection(p, tr("Common Settings"), commonSettings = new CommonSettingsPanel());
112        addSettingsSection(p, tr("WMS Settings"), wmsSettings = new WMSSettingsPanel(),
113                GBC.eol().fill(GBC.HORIZONTAL));
114        addSettingsSection(p, tr("TMS Settings"), tmsSettings = new TMSSettingsPanel(),
115                GBC.eol().fill(GBC.HORIZONTAL));
116
117        p.add(new JPanel(),GBC.eol().fill(GBC.BOTH));
118        return new JScrollPane(p);
119    }
120
121    @Override
122    public void addGui(final PreferenceTabbedPane gui) {
123        JPanel p = gui.createPreferenceTab(this);
124        JTabbedPane pane = getTabPane();
125        layerInfo = new ImageryLayerInfo(ImageryLayerInfo.instance);
126        imageryProviders = new ImageryProvidersPanel(gui, layerInfo);
127        pane.addTab(tr("Imagery providers"), imageryProviders);
128        pane.addTab(tr("Settings"), buildSettingsPanel());
129        pane.addTab(tr("Offset bookmarks"), new OffsetBookmarksPanel(gui));
130        loadSettings();
131        p.add(pane,GBC.std().fill(GBC.BOTH));
132    }
133
134    /**
135     * Returns the imagery providers panel.
136     * @return The imagery providers panel.
137     */
138    public ImageryProvidersPanel getProvidersPanel() {
139        return imageryProviders;
140    }
141
142    private void loadSettings() {
143        commonSettings.loadSettings();
144        wmsSettings.loadSettings();
145        tmsSettings.loadSettings();
146    }
147
148    @Override
149    public boolean ok() {
150        layerInfo.save();
151        ImageryLayerInfo.instance.clear();
152        ImageryLayerInfo.instance.load();
153        Main.main.menu.imageryMenu.refreshOffsetMenu();
154        OffsetBookmark.saveBookmarks();
155        
156        DownloadDialog.getInstance().refreshTileSources();
157
158        boolean commonRestartRequired = commonSettings.saveSettings();
159        boolean wmsRestartRequired = wmsSettings.saveSettings();
160        boolean tmsRestartRequired = tmsSettings.saveSettings();
161
162        return commonRestartRequired || wmsRestartRequired || tmsRestartRequired;
163    }
164
165    /**
166     * Updates a server URL in the preferences dialog. Used by plugins.
167     *
168     * @param server
169     *            The server name
170     * @param url
171     *            The server URL
172     */
173    public void setServerUrl(String server, String url) {
174        for (int i = 0; i < imageryProviders.activeModel.getRowCount(); i++) {
175            if (server.equals(imageryProviders.activeModel.getValueAt(i, 0).toString())) {
176                imageryProviders.activeModel.setValueAt(url, i, 1);
177                return;
178            }
179        }
180        imageryProviders.activeModel.addRow(new String[] { server, url });
181    }
182
183    /**
184     * Gets a server URL in the preferences dialog. Used by plugins.
185     *
186     * @param server
187     *            The server name
188     * @return The server URL
189     */
190    public String getServerUrl(String server) {
191        for (int i = 0; i < imageryProviders.activeModel.getRowCount(); i++) {
192            if (server.equals(imageryProviders.activeModel.getValueAt(i, 0).toString()))
193                return imageryProviders.activeModel.getValueAt(i, 1).toString();
194        }
195        return null;
196    }
197
198    /**
199     * A panel displaying imagery providers.
200     */
201    public static class ImageryProvidersPanel extends JPanel {
202        // Public JTables and JMapViewer
203        /** The table of active providers **/
204        public final JTable activeTable;
205        /** The table of default providers **/
206        public final JTable defaultTable;
207        /** The map displaying imagery bounds of selected default providers **/
208        public final JMapViewer defaultMap;
209
210        // Public models
211        /** The model of active providers **/
212        public final ImageryLayerTableModel activeModel;
213        /** The model of default providers **/
214        public final ImageryDefaultLayerTableModel defaultModel;
215
216        // Public JToolbars
217        /** The toolbar on the right of active providers **/
218        public final JToolBar activeToolbar;
219        /** The toolbar on the middle of the panel **/
220        public final JToolBar middleToolbar;
221        /** The toolbar on the right of default providers **/
222        public final JToolBar defaultToolbar;
223
224        // Private members
225        private final PreferenceTabbedPane gui;
226        private final ImageryLayerInfo layerInfo;
227
228        private static class ImageryTableCellRenderer extends DefaultTableCellRenderer {
229
230            private List<ImageryInfo> layers;
231
232            public ImageryTableCellRenderer(List<ImageryInfo> layers) {
233                this.layers = layers;
234            }
235
236            @Override
237            public Component getTableCellRendererComponent(JTable table, Object value, boolean
238                    isSelected, boolean hasFocus, int row, int column) {
239                JLabel label = (JLabel) super.getTableCellRendererComponent(
240                        table, value, isSelected, hasFocus, row, column);
241                label.setBackground(Main.pref.getUIColor("Table.background"));
242                if (isSelected) {
243                    label.setForeground(Main.pref.getUIColor("Table.foreground"));
244                }
245                if (value != null) { // Fix #8159
246                    String t = value.toString();
247                    for (ImageryInfo l : layers) {
248                        if (l.getExtendedUrl().equals(t)) {
249                            label.setBackground(Main.pref.getColor(
250                                    marktr("Imagery Background: Default"),
251                                    new Color(200,255,200)));
252                            break;
253                        }
254                    }
255                }
256                return label;
257            }
258        }
259
260        /**
261         * Constructs a new {@code ImageryProvidersPanel}.
262         * @param gui The parent preference tab pane
263         * @param layerInfoArg The list of imagery entries to display
264         */
265        public ImageryProvidersPanel(final PreferenceTabbedPane gui, ImageryLayerInfo layerInfoArg) {
266            super(new GridBagLayout());
267            this.gui = gui;
268            this.layerInfo = layerInfoArg;
269            this.activeModel = new ImageryLayerTableModel();
270
271            activeTable = new JTable(activeModel) {
272                @Override
273                public String getToolTipText(MouseEvent e) {
274                    java.awt.Point p = e.getPoint();
275                    return activeModel.getValueAt(rowAtPoint(p), columnAtPoint(p)).toString();
276                }
277            };
278            activeTable.putClientProperty("terminateEditOnFocusLost", true);
279
280            defaultModel = new ImageryDefaultLayerTableModel();
281            defaultTable = new JTable(defaultModel) {
282                @Override
283                public String getToolTipText(MouseEvent e) {
284                    java.awt.Point p = e.getPoint();
285                    return (String) defaultModel.getValueAt(rowAtPoint(p), columnAtPoint(p));
286                }
287            };
288
289            defaultModel.addTableModelListener(
290                    new TableModelListener() {
291                        @Override
292                        public void tableChanged(TableModelEvent e) {
293                            activeTable.repaint();
294                        }
295                    }
296                    );
297
298            activeModel.addTableModelListener(
299                    new TableModelListener() {
300                        @Override
301                        public void tableChanged(TableModelEvent e) {
302                            defaultTable.repaint();
303                        }
304                    }
305                    );
306
307            TableColumnModel mod = defaultTable.getColumnModel();
308            mod.getColumn(2).setPreferredWidth(800);
309            mod.getColumn(2).setCellRenderer(new ImageryTableCellRenderer(layerInfo.getLayers()));
310            mod.getColumn(1).setPreferredWidth(400);
311            mod.getColumn(0).setPreferredWidth(50);
312
313            mod = activeTable.getColumnModel();
314            mod.getColumn(1).setPreferredWidth(800);
315            mod.getColumn(1).setCellRenderer(new ImageryTableCellRenderer(layerInfo.getDefaultLayers()));
316            mod.getColumn(0).setPreferredWidth(200);
317
318            RemoveEntryAction remove = new RemoveEntryAction();
319            activeTable.getSelectionModel().addListSelectionListener(remove);
320
321            add(new JLabel(tr("Available default entries:")), GBC.eol().insets(5, 5, 0, 0));
322            // Add default item list
323            JScrollPane scrolldef = new JScrollPane(defaultTable);
324            scrolldef.setPreferredSize(new Dimension(200, 200));
325            add(scrolldef, GBC.std().insets(0, 5, 0, 0).fill(GridBagConstraints.BOTH).weight(1.0, 0.6).insets(5, 0, 0, 0));
326
327            // Add default item map
328            defaultMap = new JMapViewer();
329            defaultMap.setZoomContolsVisible(false);
330            defaultMap.setMinimumSize(new Dimension(100, 200));
331            add(defaultMap, GBC.std().insets(5, 5, 0, 0).fill(GridBagConstraints.BOTH).weight(0.33, 0.6).insets(5, 0, 0, 0));
332
333            defaultTable.getSelectionModel().addListSelectionListener(new DefListSelectionListener());
334
335            defaultToolbar = new JToolBar(JToolBar.VERTICAL);
336            defaultToolbar.setFloatable(false);
337            defaultToolbar.setBorderPainted(false);
338            defaultToolbar.setOpaque(false);
339            defaultToolbar.add(new ReloadAction());
340            add(defaultToolbar, GBC.eol().anchor(GBC.SOUTH).insets(0, 0, 5, 0));
341
342            ActivateAction activate = new ActivateAction();
343            defaultTable.getSelectionModel().addListSelectionListener(activate);
344            JButton btnActivate = new JButton(activate);
345
346            middleToolbar = new JToolBar(JToolBar.HORIZONTAL);
347            middleToolbar.setFloatable(false);
348            middleToolbar.setBorderPainted(false);
349            middleToolbar.setOpaque(false);
350            middleToolbar.add(btnActivate);
351            add(middleToolbar, GBC.eol().anchor(GBC.CENTER).insets(5, 15, 5, 0));
352
353            add(Box.createHorizontalGlue(), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
354
355            add(new JLabel(tr("Selected entries:")), GBC.eol().insets(5, 0, 0, 0));
356            JScrollPane scroll = new JScrollPane(activeTable);
357            add(scroll, GBC.std().fill(GridBagConstraints.BOTH).span(GridBagConstraints.RELATIVE).weight(1.0, 0.4).insets(5, 0, 0, 5));
358            scroll.setPreferredSize(new Dimension(200, 200));
359
360            activeToolbar = new JToolBar(JToolBar.VERTICAL);
361            activeToolbar.setFloatable(false);
362            activeToolbar.setBorderPainted(false);
363            activeToolbar.setOpaque(false);
364            activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.WMS));
365            activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.TMS));
366            //activeToolbar.add(edit); TODO
367            activeToolbar.add(remove);
368            add(activeToolbar, GBC.eol().anchor(GBC.NORTH).insets(0, 0, 5, 5));
369
370        }
371
372        // Listener of default providers list selection
373        private final class DefListSelectionListener implements ListSelectionListener {
374            // The current drawn rectangles and polygons
375            private final Map<Integer, MapRectangle> mapRectangles;
376            private final Map<Integer, List<MapPolygon>> mapPolygons;
377
378            private DefListSelectionListener() {
379                this.mapRectangles = new HashMap<>();
380                this.mapPolygons = new HashMap<>();
381            }
382
383            @Override
384            public void valueChanged(ListSelectionEvent e) {
385                // First index is set to -1 when the list is refreshed, so discard all map rectangles and polygons
386                if (e.getFirstIndex() == -1) {
387                    defaultMap.removeAllMapRectangles();
388                    defaultMap.removeAllMapPolygons();
389                    mapRectangles.clear();
390                    mapPolygons.clear();
391                    // Only process complete (final) selection events
392                } else if (!e.getValueIsAdjusting()) {
393                    for (int i = e.getFirstIndex(); i<=e.getLastIndex(); i++) {
394                        updateBoundsAndShapes(i);
395                    }
396                    // If needed, adjust map to show all map rectangles and polygons
397                    if (!mapRectangles.isEmpty() || !mapPolygons.isEmpty()) {
398                        defaultMap.setDisplayToFitMapElements(false, true, true);
399                        defaultMap.zoomOut();
400                    }
401                }
402            }
403
404            private void updateBoundsAndShapes(int i) {
405                ImageryBounds bounds = defaultModel.getRow(i).getBounds();
406                if (bounds != null) {
407                    List<Shape> shapes = bounds.getShapes();
408                    if (shapes != null && !shapes.isEmpty()) {
409                        if (defaultTable.getSelectionModel().isSelectedIndex(i)) {
410                            if (!mapPolygons.containsKey(i)) {
411                                List<MapPolygon> list = new ArrayList<>();
412                                mapPolygons.put(i, list);
413                                // Add new map polygons
414                                for (Shape shape : shapes) {
415                                    MapPolygon polygon = new MapPolygonImpl(shape.getPoints());
416                                    list.add(polygon);
417                                    defaultMap.addMapPolygon(polygon);
418                                }
419                            }
420                        } else if (mapPolygons.containsKey(i)) {
421                            // Remove previously drawn map polygons
422                            for (MapPolygon polygon : mapPolygons.get(i)) {
423                                defaultMap.removeMapPolygon(polygon);
424                            }
425                            mapPolygons.remove(i);
426                        }
427                        // Only display bounds when no polygons (shapes) are defined for this provider
428                    } else {
429                        if (defaultTable.getSelectionModel().isSelectedIndex(i)) {
430                            if (!mapRectangles.containsKey(i)) {
431                                // Add new map rectangle
432                                Coordinate topLeft = new Coordinate(bounds.getMaxLat(), bounds.getMinLon());
433                                Coordinate bottomRight = new Coordinate(bounds.getMinLat(), bounds.getMaxLon());
434                                MapRectangle rectangle = new MapRectangleImpl(topLeft, bottomRight);
435                                mapRectangles.put(i, rectangle);
436                                defaultMap.addMapRectangle(rectangle);
437                            }
438                        } else if (mapRectangles.containsKey(i)) {
439                            // Remove previously drawn map rectangle
440                            defaultMap.removeMapRectangle(mapRectangles.get(i));
441                            mapRectangles.remove(i);
442                        }
443                    }
444                }
445            }
446        }
447
448        private class NewEntryAction extends AbstractAction {
449
450            private final ImageryInfo.ImageryType type;
451
452            public NewEntryAction(ImageryInfo.ImageryType type) {
453                putValue(NAME, type.toString());
454                putValue(SHORT_DESCRIPTION, tr("Add a new {0} entry by entering the URL", type.toString()));
455                putValue(SMALL_ICON, ImageProvider.get("dialogs",
456                            "add" + (ImageryInfo.ImageryType.WMS.equals(type) ? "_wms" : ImageryInfo.ImageryType.TMS.equals(type) ? "_tms" : "")));
457                this.type = type;
458            }
459
460            @Override
461            public void actionPerformed(ActionEvent evt) {
462                final AddImageryPanel p;
463                if (ImageryInfo.ImageryType.WMS.equals(type)) {
464                    p = new AddWMSLayerPanel();
465                } else if (ImageryInfo.ImageryType.TMS.equals(type)) {
466                    p = new AddTMSLayerPanel();
467                } else {
468                    throw new IllegalStateException("Type " + type + " not supported");
469                }
470
471                final AddImageryDialog addDialog = new AddImageryDialog(gui, p);
472                addDialog.showDialog();
473
474                if (addDialog.getValue() == 1) {
475                    try {
476                        activeModel.addRow(p.getImageryInfo());
477                    } catch (IllegalArgumentException ex) {
478                        if (ex.getMessage() == null || ex.getMessage().isEmpty())
479                            throw ex;
480                        else {
481                            JOptionPane.showMessageDialog(Main.parent,
482                                    ex.getMessage(), tr("Error"),
483                                    JOptionPane.ERROR_MESSAGE);
484                        }
485                    }
486                }
487            }
488        }
489
490        private class RemoveEntryAction extends AbstractAction implements ListSelectionListener {
491
492            public RemoveEntryAction() {
493                putValue(NAME, tr("Remove"));
494                putValue(SHORT_DESCRIPTION, tr("Remove entry"));
495                putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
496                updateEnabledState();
497            }
498
499            protected final void updateEnabledState() {
500                setEnabled(activeTable.getSelectedRowCount() > 0);
501            }
502
503            @Override
504            public void valueChanged(ListSelectionEvent e) {
505                updateEnabledState();
506            }
507
508            @Override
509            public void actionPerformed(ActionEvent e) {
510                Integer i;
511                while ((i = activeTable.getSelectedRow()) != -1) {
512                    activeModel.removeRow(i);
513                }
514            }
515        }
516
517        private class ActivateAction extends AbstractAction implements ListSelectionListener {
518            public ActivateAction() {
519                putValue(NAME, tr("Activate"));
520                putValue(SHORT_DESCRIPTION, tr("copy selected defaults"));
521                putValue(SMALL_ICON, ImageProvider.get("preferences", "activate-down"));
522            }
523
524            protected void updateEnabledState() {
525                setEnabled(defaultTable.getSelectedRowCount() > 0);
526            }
527
528            @Override
529            public void valueChanged(ListSelectionEvent e) {
530                updateEnabledState();
531            }
532
533            @Override
534            public void actionPerformed(ActionEvent e) {
535                int[] lines = defaultTable.getSelectedRows();
536                if (lines.length == 0) {
537                    JOptionPane.showMessageDialog(
538                            gui,
539                            tr("Please select at least one row to copy."),
540                            tr("Information"),
541                            JOptionPane.INFORMATION_MESSAGE);
542                    return;
543                }
544
545                Set<String> acceptedEulas = new HashSet<>();
546
547                outer:
548                for (int line : lines) {
549                    ImageryInfo info = defaultModel.getRow(line);
550
551                    // Check if an entry with exactly the same values already exists
552                    for (int j = 0; j < activeModel.getRowCount(); j++) {
553                        if (info.equalsBaseValues(activeModel.getRow(j))) {
554                            // Select the already existing row so the user has
555                            // some feedback in case an entry exists
556                            activeTable.getSelectionModel().setSelectionInterval(j, j);
557                            activeTable.scrollRectToVisible(activeTable.getCellRect(j, 0, true));
558                            continue outer;
559                        }
560                    }
561
562                    String eulaURL = info.getEulaAcceptanceRequired();
563                    // If set and not already accepted, ask for EULA acceptance
564                    if (eulaURL != null && !acceptedEulas.contains(eulaURL)) {
565                        if (confirmEulaAcceptance(gui, eulaURL)) {
566                            acceptedEulas.add(eulaURL);
567                        } else {
568                            continue outer;
569                        }
570                    }
571
572                    activeModel.addRow(new ImageryInfo(info));
573                    int lastLine = activeModel.getRowCount() - 1;
574                    activeTable.getSelectionModel().setSelectionInterval(lastLine, lastLine);
575                    activeTable.scrollRectToVisible(activeTable.getCellRect(lastLine, 0, true));
576                }
577            }
578        }
579
580        private class ReloadAction extends AbstractAction {
581            public ReloadAction() {
582                putValue(SHORT_DESCRIPTION, tr("reload defaults"));
583                putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh"));
584            }
585
586            @Override
587            public void actionPerformed(ActionEvent evt) {
588                layerInfo.loadDefaults(true);
589                defaultModel.fireTableDataChanged();
590            }
591        }
592
593        /**
594         * The table model for imagery layer list
595         */
596        public class ImageryLayerTableModel extends DefaultTableModel {
597            /**
598             * Constructs a new {@code ImageryLayerTableModel}.
599             */
600            public ImageryLayerTableModel() {
601                setColumnIdentifiers(new String[] { tr("Menu Name"), tr("Imagery URL")});
602            }
603
604            /**
605             * Returns the imagery info at the given row number.
606             * @param row The row number
607             * @return The imagery info at the given row number
608             */
609            public ImageryInfo getRow(int row) {
610                return layerInfo.getLayers().get(row);
611            }
612
613            /**
614             * Adds a new imagery info as the last row.
615             * @param i The imagery info to add
616             */
617            public void addRow(ImageryInfo i) {
618                layerInfo.add(i);
619                int p = getRowCount() - 1;
620                fireTableRowsInserted(p, p);
621            }
622
623            @Override
624            public void removeRow(int i) {
625                layerInfo.remove(getRow(i));
626                fireTableRowsDeleted(i, i);
627            }
628
629            @Override
630            public int getRowCount() {
631                return layerInfo.getLayers().size();
632            }
633
634            @Override
635            public Object getValueAt(int row, int column) {
636                ImageryInfo info = layerInfo.getLayers().get(row);
637                switch (column) {
638                case 0:
639                    return info.getName();
640                case 1:
641                    return info.getExtendedUrl();
642                default:
643                    throw new ArrayIndexOutOfBoundsException();
644                }
645            }
646
647            @Override
648            public void setValueAt(Object o, int row, int column) {
649                if (layerInfo.getLayers().size() <= row) return;
650                ImageryInfo info = layerInfo.getLayers().get(row);
651                switch (column) {
652                case 0:
653                    info.setName((String) o);
654                    info.clearId();
655                    break;
656                case 1:
657                    info.setExtendedUrl((String)o);
658                    info.clearId();
659                    break;
660                default:
661                    throw new ArrayIndexOutOfBoundsException();
662                }
663            }
664
665            @Override
666            public boolean isCellEditable(int row, int column) {
667                return true;
668            }
669        }
670
671        /**
672         * The table model for the default imagery layer list
673         */
674        public class ImageryDefaultLayerTableModel extends DefaultTableModel {
675            /**
676             * Constructs a new {@code ImageryDefaultLayerTableModel}.
677             */
678            public ImageryDefaultLayerTableModel() {
679                setColumnIdentifiers(new String[]{"", tr("Menu Name (Default)"), tr("Imagery URL (Default)")});
680            }
681
682            /**
683             * Returns the imagery info at the given row number.
684             * @param row The row number
685             * @return The imagery info at the given row number
686             */
687            public ImageryInfo getRow(int row) {
688                return layerInfo.getDefaultLayers().get(row);
689            }
690
691            @Override
692            public int getRowCount() {
693                return layerInfo.getDefaultLayers().size();
694            }
695
696            @Override
697            public Object getValueAt(int row, int column) {
698                ImageryInfo info = layerInfo.getDefaultLayers().get(row);
699                switch (column) {
700                case 0:
701                    return info.getCountryCode();
702                case 1:
703                    return info.getName();
704                case 2:
705                    return info.getExtendedUrl();
706                }
707                return null;
708            }
709
710            @Override
711            public boolean isCellEditable(int row, int column) {
712                return false;
713            }
714        }
715
716        private boolean confirmEulaAcceptance(PreferenceTabbedPane gui, String eulaUrl) {
717            URL url = null;
718            try {
719                url = new URL(eulaUrl.replaceAll("\\{lang\\}", LanguageInfo.getWikiLanguagePrefix()));
720                JosmEditorPane htmlPane = null;
721                try {
722                    htmlPane = new JosmEditorPane(url);
723                } catch (IOException e1) {
724                    // give a second chance with a default Locale 'en'
725                    try {
726                        url = new URL(eulaUrl.replaceAll("\\{lang\\}", ""));
727                        htmlPane = new JosmEditorPane(url);
728                    } catch (IOException e2) {
729                        JOptionPane.showMessageDialog(gui ,tr("EULA license URL not available: {0}", eulaUrl));
730                        return false;
731                    }
732                }
733                Box box = Box.createVerticalBox();
734                htmlPane.setEditable(false);
735                JScrollPane scrollPane = new JScrollPane(htmlPane);
736                scrollPane.setPreferredSize(new Dimension(400, 400));
737                box.add(scrollPane);
738                int option = JOptionPane.showConfirmDialog(Main.parent, box, tr("Please abort if you are not sure"), JOptionPane.YES_NO_OPTION,
739                        JOptionPane.WARNING_MESSAGE);
740                if (option == JOptionPane.YES_OPTION)
741                    return true;
742            } catch (MalformedURLException e2) {
743                JOptionPane.showMessageDialog(gui ,tr("Malformed URL for the EULA licence: {0}", eulaUrl));
744            }
745            return false;
746        }
747    }
748
749    static class OffsetBookmarksPanel extends JPanel {
750        List<OffsetBookmark> bookmarks = OffsetBookmark.allBookmarks;
751        OffsetsBookmarksModel model = new OffsetsBookmarksModel();
752
753        public OffsetBookmarksPanel(final PreferenceTabbedPane gui) {
754            super(new GridBagLayout());
755            final JTable list = new JTable(model) {
756                @Override
757                public String getToolTipText(MouseEvent e) {
758                    java.awt.Point p = e.getPoint();
759                    return model.getValueAt(rowAtPoint(p), columnAtPoint(p)).toString();
760                }
761            };
762            JScrollPane scroll = new JScrollPane(list);
763            add(scroll, GBC.eol().fill(GridBagConstraints.BOTH));
764            scroll.setPreferredSize(new Dimension(200, 200));
765
766            TableColumnModel mod = list.getColumnModel();
767            mod.getColumn(0).setPreferredWidth(150);
768            mod.getColumn(1).setPreferredWidth(200);
769            mod.getColumn(2).setPreferredWidth(300);
770            mod.getColumn(3).setPreferredWidth(150);
771            mod.getColumn(4).setPreferredWidth(150);
772
773            JPanel buttonPanel = new JPanel(new FlowLayout());
774
775            JButton add = new JButton(tr("Add"));
776            buttonPanel.add(add, GBC.std().insets(0, 5, 0, 0));
777            add.addActionListener(new ActionListener() {
778                @Override
779                public void actionPerformed(ActionEvent e) {
780                    OffsetBookmark b = new OffsetBookmark(Main.getProjection().toCode(),"","",0,0);
781                    model.addRow(b);
782                }
783            });
784
785            JButton delete = new JButton(tr("Delete"));
786            buttonPanel.add(delete, GBC.std().insets(0, 5, 0, 0));
787            delete.addActionListener(new ActionListener() {
788                @Override
789                public void actionPerformed(ActionEvent e) {
790                    if (list.getSelectedRow() == -1) {
791                        JOptionPane.showMessageDialog(gui, tr("Please select the row to delete."));
792                    } else {
793                        Integer i;
794                        while ((i = list.getSelectedRow()) != -1) {
795                            model.removeRow(i);
796                        }
797                    }
798                }
799            });
800
801            add(buttonPanel,GBC.eol());
802        }
803
804        /**
805         * The table model for imagery offsets list
806         */
807        class OffsetsBookmarksModel extends DefaultTableModel {
808            public OffsetsBookmarksModel() {
809                setColumnIdentifiers(new String[] { tr("Projection"),  tr("Layer"), tr("Name"), tr("Easting"), tr("Northing"),});
810            }
811
812            public OffsetBookmark getRow(int row) {
813                return bookmarks.get(row);
814            }
815
816            public void addRow(OffsetBookmark i) {
817                bookmarks.add(i);
818                int p = getRowCount() - 1;
819                fireTableRowsInserted(p, p);
820            }
821
822            @Override
823            public void removeRow(int i) {
824                bookmarks.remove(getRow(i));
825                fireTableRowsDeleted(i, i);
826            }
827
828            @Override
829            public int getRowCount() {
830                return bookmarks.size();
831            }
832
833            @Override
834            public Object getValueAt(int row, int column) {
835                OffsetBookmark info = bookmarks.get(row);
836                switch (column) {
837                case 0:
838                    if (info.projectionCode == null) return "";
839                    return info.projectionCode.toString();
840                case 1:
841                    return info.layerName;
842                case 2:
843                    return info.name;
844                case 3:
845                    return info.dx;
846                case 4:
847                    return info.dy;
848                default:
849                    throw new ArrayIndexOutOfBoundsException();
850                }
851            }
852
853            @Override
854            public void setValueAt(Object o, int row, int column) {
855                OffsetBookmark info = bookmarks.get(row);
856                switch (column) {
857                case 1:
858                    info.layerName = o.toString();
859                    break;
860                case 2:
861                    info.name = o.toString();
862                    break;
863                case 3:
864                    info.dx = Double.parseDouble((String) o);
865                    break;
866                case 4:
867                    info.dy = Double.parseDouble((String) o);
868                    break;
869                default:
870                    throw new ArrayIndexOutOfBoundsException();
871                }
872            }
873
874            @Override
875            public boolean isCellEditable(int row, int column) {
876                return column >= 1;
877            }
878        }
879    }
880
881    /**
882     * Initializes imagery preferences.
883     */
884    public static void initialize() {
885        ImageryLayerInfo.instance.load();
886        OffsetBookmark.loadBookmarks();
887        Main.main.menu.imageryMenu.refreshImageryMenu();
888        Main.main.menu.imageryMenu.refreshOffsetMenu();
889    }
890}