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