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