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