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