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