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