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