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