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