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.GridBagConstraints;
011import java.awt.GridBagLayout;
012import java.awt.event.ActionEvent;
013import java.awt.event.MouseAdapter;
014import java.awt.event.MouseEvent;
015import java.io.IOException;
016import java.net.MalformedURLException;
017import java.net.URL;
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024import java.util.Set;
025import java.util.function.BiConsumer;
026import java.util.function.Function;
027
028import javax.swing.AbstractAction;
029import javax.swing.Box;
030import javax.swing.JButton;
031import javax.swing.JLabel;
032import javax.swing.JOptionPane;
033import javax.swing.JPanel;
034import javax.swing.JScrollPane;
035import javax.swing.JTable;
036import javax.swing.JToolBar;
037import javax.swing.UIManager;
038import javax.swing.event.ListSelectionEvent;
039import javax.swing.event.ListSelectionListener;
040import javax.swing.table.DefaultTableCellRenderer;
041import javax.swing.table.DefaultTableModel;
042import javax.swing.table.TableColumnModel;
043
044import org.openstreetmap.gui.jmapviewer.Coordinate;
045import org.openstreetmap.gui.jmapviewer.MapPolygonImpl;
046import org.openstreetmap.gui.jmapviewer.MapRectangleImpl;
047import org.openstreetmap.gui.jmapviewer.interfaces.MapPolygon;
048import org.openstreetmap.gui.jmapviewer.interfaces.MapRectangle;
049import org.openstreetmap.josm.data.imagery.ImageryInfo;
050import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds;
051import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryCategory;
052import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
053import org.openstreetmap.josm.data.imagery.Shape;
054import org.openstreetmap.josm.data.preferences.NamedColorProperty;
055import org.openstreetmap.josm.gui.MainApplication;
056import org.openstreetmap.josm.gui.bbox.JosmMapViewer;
057import org.openstreetmap.josm.gui.bbox.SlippyMapBBoxChooser;
058import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
059import org.openstreetmap.josm.gui.util.GuiHelper;
060import org.openstreetmap.josm.gui.widgets.FilterField;
061import org.openstreetmap.josm.gui.widgets.HtmlPanel;
062import org.openstreetmap.josm.gui.widgets.JosmEditorPane;
063import org.openstreetmap.josm.spi.preferences.Config;
064import org.openstreetmap.josm.tools.GBC;
065import org.openstreetmap.josm.tools.ImageProvider;
066import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
067import org.openstreetmap.josm.tools.LanguageInfo;
068import org.openstreetmap.josm.tools.Logging;
069
070/**
071 * A panel displaying imagery providers.
072 * @since 15115 (extracted from ImageryPreferences)
073 */
074public class ImageryProvidersPanel extends JPanel {
075    // Public JTables and JosmMapViewer
076    /** The table of active providers **/
077    public final JTable activeTable;
078    /** The table of default providers **/
079    public final JTable defaultTable;
080    /** The filter of default providers **/
081    private final FilterField defaultFilter;
082    /** The selection listener synchronizing map display with table of default providers **/
083    private final transient DefListSelectionListener defaultTableListener;
084    /** The map displaying imagery bounds of selected default providers **/
085    public final JosmMapViewer defaultMap;
086
087    // Public models
088    /** The model of active providers **/
089    public final ImageryLayerTableModel activeModel;
090    /** The model of default providers **/
091    public final ImageryDefaultLayerTableModel defaultModel;
092
093    // Public JToolbars
094    /** The toolbar on the right of active providers **/
095    public final JToolBar activeToolbar;
096    /** The toolbar on the middle of the panel **/
097    public final JToolBar middleToolbar;
098    /** The toolbar on the right of default providers **/
099    public final JToolBar defaultToolbar;
100
101    // Private members
102    private final PreferenceTabbedPane gui;
103    private final transient ImageryLayerInfo layerInfo;
104
105    /**
106     * class to render the URL information of Imagery source
107     * @since 8065
108     */
109    private static class ImageryURLTableCellRenderer extends DefaultTableCellRenderer {
110
111        private static final NamedColorProperty IMAGERY_BACKGROUND_COLOR = new NamedColorProperty(
112                marktr("Imagery Background: Default"),
113                new Color(200, 255, 200));
114
115        private final transient List<ImageryInfo> layers;
116
117        ImageryURLTableCellRenderer(List<ImageryInfo> layers) {
118            this.layers = layers;
119        }
120
121        @Override
122        public Component getTableCellRendererComponent(JTable table, Object value, boolean
123                isSelected, boolean hasFocus, int row, int column) {
124            JLabel label = (JLabel) super.getTableCellRendererComponent(
125                    table, value, isSelected, hasFocus, row, column);
126            GuiHelper.setBackgroundReadable(label, UIManager.getColor("Table.background"));
127            if (value != null) { // Fix #8159
128                String t = value.toString();
129                for (ImageryInfo l : layers) {
130                    if (l.getExtendedUrl().equals(t)) {
131                        GuiHelper.setBackgroundReadable(label, IMAGERY_BACKGROUND_COLOR.get());
132                        break;
133                    }
134                }
135                label.setToolTipText((String) value);
136            }
137            return label;
138        }
139    }
140
141    /**
142     * class to render an information of Imagery source
143     * @param <T> type of information
144     */
145    private static class ImageryTableCellRenderer<T> extends DefaultTableCellRenderer {
146        private final Function<T, Object> mapper;
147        private final Function<T, String> tooltip;
148        private final BiConsumer<T, JLabel> decorator;
149
150        ImageryTableCellRenderer(Function<T, Object> mapper, Function<T, String> tooltip, BiConsumer<T, JLabel> decorator) {
151            this.mapper = mapper;
152            this.tooltip = tooltip;
153            this.decorator = decorator;
154        }
155
156        @Override
157        @SuppressWarnings("unchecked")
158        public final Component getTableCellRendererComponent(JTable table, Object value, boolean
159                isSelected, boolean hasFocus, int row, int column) {
160            T obj = (T) value;
161            JLabel label = (JLabel) super.getTableCellRendererComponent(
162                    table, mapper.apply(obj), isSelected, hasFocus, row, column);
163            GuiHelper.setBackgroundReadable(label,
164                    isSelected ? UIManager.getColor("Table.selectionBackground") : UIManager.getColor("Table.background"));
165            if (obj != null) {
166                label.setToolTipText(tooltip.apply(obj));
167                if (decorator != null) {
168                    decorator.accept(obj, label);
169                }
170            }
171            return label;
172        }
173    }
174
175    /**
176     * class to render the category information of Imagery source
177     */
178    private static class ImageryCategoryTableCellRenderer extends ImageryProvidersPanel.ImageryTableCellRenderer<ImageryCategory> {
179        ImageryCategoryTableCellRenderer() {
180            super(cat -> null, cat -> tr("Imagery category: {0}", cat.getDescription()),
181                  (cat, label) -> label.setIcon(cat.getIcon(ImageSizes.TABLE)));
182        }
183    }
184
185    /**
186     * class to render the country information of Imagery source
187     */
188    private static class ImageryCountryTableCellRenderer extends ImageryProvidersPanel.ImageryTableCellRenderer<String> {
189        ImageryCountryTableCellRenderer() {
190            super(code -> code, ImageryInfo::getLocalizedCountry, null);
191        }
192    }
193
194    /**
195     * class to render the name information of Imagery source
196     */
197    private static class ImageryNameTableCellRenderer extends ImageryProvidersPanel.ImageryTableCellRenderer<ImageryInfo> {
198        ImageryNameTableCellRenderer() {
199            super(info -> info == null ? null : info.getName(), ImageryInfo::getToolTipText, null);
200        }
201    }
202
203    /**
204     * Constructs a new {@code ImageryProvidersPanel}.
205     * @param gui The parent preference tab pane
206     * @param layerInfoArg The list of imagery entries to display
207     */
208    public ImageryProvidersPanel(final PreferenceTabbedPane gui, ImageryLayerInfo layerInfoArg) {
209        super(new GridBagLayout());
210        this.gui = gui;
211        this.layerInfo = layerInfoArg;
212        this.activeModel = new ImageryLayerTableModel();
213
214        activeTable = new JTable(activeModel) {
215            @Override
216            public String getToolTipText(MouseEvent e) {
217                java.awt.Point p = e.getPoint();
218                try {
219                    return activeModel.getValueAt(rowAtPoint(p), columnAtPoint(p)).toString();
220                } catch (ArrayIndexOutOfBoundsException ex) {
221                    Logging.debug(ex);
222                    return null;
223                }
224            }
225        };
226        activeTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
227
228        defaultModel = new ImageryDefaultLayerTableModel();
229        defaultTable = new JTable(defaultModel);
230        defaultTable.setAutoCreateRowSorter(true);
231        defaultFilter = new FilterField().filter(defaultTable, defaultModel);
232
233        defaultModel.addTableModelListener(e -> activeTable.repaint());
234        activeModel.addTableModelListener(e -> defaultTable.repaint());
235
236        TableColumnModel mod = defaultTable.getColumnModel();
237        mod.getColumn(3).setPreferredWidth(775);
238        mod.getColumn(3).setCellRenderer(new ImageryURLTableCellRenderer(layerInfo.getLayers()));
239        mod.getColumn(2).setPreferredWidth(475);
240        mod.getColumn(2).setCellRenderer(new ImageryNameTableCellRenderer());
241        mod.getColumn(1).setCellRenderer(new ImageryCountryTableCellRenderer());
242        mod.getColumn(0).setPreferredWidth(50);
243        mod.getColumn(0).setCellRenderer(new ImageryCategoryTableCellRenderer());
244        mod.getColumn(0).setPreferredWidth(50);
245
246        mod = activeTable.getColumnModel();
247        mod.getColumn(1).setPreferredWidth(800);
248        mod.getColumn(1).setCellRenderer(new ImageryURLTableCellRenderer(layerInfo.getAllDefaultLayers()));
249        mod.getColumn(0).setPreferredWidth(200);
250
251        RemoveEntryAction remove = new RemoveEntryAction();
252        activeTable.getSelectionModel().addListSelectionListener(remove);
253
254        add(new JLabel(tr("Available default entries:")), GBC.std().insets(5, 5, 0, 0));
255        add(new JLabel(tr("Boundaries of selected imagery entries:")), GBC.eol().insets(5, 5, 0, 0));
256
257        // Add default item list
258        JPanel defaultPane = new JPanel(new GridBagLayout());
259        JScrollPane scrolldef = new JScrollPane(defaultTable);
260        scrolldef.setPreferredSize(new Dimension(200, 200));
261        defaultPane.add(defaultFilter, GBC.eol().insets(0, 0, 0, 0).fill(GridBagConstraints.HORIZONTAL));
262        defaultPane.add(scrolldef, GBC.eol().insets(0, 0, 0, 0).fill(GridBagConstraints.BOTH));
263        add(defaultPane, GBC.std().fill(GridBagConstraints.BOTH).weight(1.0, 0.6).insets(5, 0, 0, 0));
264
265        // Add default item map
266        defaultMap = new JosmMapViewer();
267        defaultMap.setTileSource(SlippyMapBBoxChooser.DefaultOsmTileSourceProvider.get()); // for attribution
268        defaultMap.addMouseListener(new MouseAdapter() {
269            @Override
270            public void mouseClicked(MouseEvent e) {
271                if (e.getButton() == MouseEvent.BUTTON1) {
272                    defaultMap.getAttribution().handleAttribution(e.getPoint(), true);
273                }
274            }
275        });
276        defaultMap.setZoomControlsVisible(false);
277        defaultMap.setMinimumSize(new Dimension(100, 200));
278        add(defaultMap, GBC.std().fill(GridBagConstraints.BOTH).weight(0.33, 0.6).insets(5, 0, 0, 0));
279
280        defaultTableListener = new DefListSelectionListener();
281        defaultTable.getSelectionModel().addListSelectionListener(defaultTableListener);
282
283        defaultToolbar = new JToolBar(JToolBar.VERTICAL);
284        defaultToolbar.setFloatable(false);
285        defaultToolbar.setBorderPainted(false);
286        defaultToolbar.setOpaque(false);
287        defaultToolbar.add(new ReloadAction());
288        add(defaultToolbar, GBC.eol().anchor(GBC.SOUTH).insets(0, 0, 5, 0));
289
290        HtmlPanel help = new HtmlPanel(tr("New default entries can be added in the <a href=\"{0}\">Wiki</a>.",
291            Config.getUrls().getJOSMWebsite()+"/wiki/Maps"));
292        help.enableClickableHyperlinks();
293        add(help, GBC.eol().insets(10, 0, 0, 0).fill(GBC.HORIZONTAL));
294
295        ActivateAction activate = new ActivateAction();
296        defaultTable.getSelectionModel().addListSelectionListener(activate);
297        JButton btnActivate = new JButton(activate);
298
299        middleToolbar = new JToolBar(JToolBar.HORIZONTAL);
300        middleToolbar.setFloatable(false);
301        middleToolbar.setBorderPainted(false);
302        middleToolbar.setOpaque(false);
303        middleToolbar.add(btnActivate);
304        add(middleToolbar, GBC.eol().anchor(GBC.CENTER).insets(5, 5, 5, 0));
305
306        add(Box.createHorizontalGlue(), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
307
308        add(new JLabel(tr("Selected entries:")), GBC.eol().insets(5, 0, 0, 0));
309        JScrollPane scroll = new JScrollPane(activeTable);
310        add(scroll, GBC.std().fill(GridBagConstraints.BOTH).span(GridBagConstraints.RELATIVE).weight(1.0, 0.4).insets(5, 0, 0, 5));
311        scroll.setPreferredSize(new Dimension(200, 200));
312
313        activeToolbar = new JToolBar(JToolBar.VERTICAL);
314        activeToolbar.setFloatable(false);
315        activeToolbar.setBorderPainted(false);
316        activeToolbar.setOpaque(false);
317        activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.WMS));
318        activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.TMS));
319        activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.WMTS));
320        activeToolbar.add(remove);
321        add(activeToolbar, GBC.eol().anchor(GBC.NORTH).insets(0, 0, 5, 5));
322    }
323
324    // Listener of default providers list selection
325    private final class DefListSelectionListener implements ListSelectionListener {
326        // The current drawn rectangles and polygons
327        private final Map<Integer, MapRectangle> mapRectangles;
328        private final Map<Integer, List<MapPolygon>> mapPolygons;
329
330        private DefListSelectionListener() {
331            this.mapRectangles = new HashMap<>();
332            this.mapPolygons = new HashMap<>();
333        }
334
335        private void clearMap() {
336            defaultMap.removeAllMapRectangles();
337            defaultMap.removeAllMapPolygons();
338            mapRectangles.clear();
339            mapPolygons.clear();
340        }
341
342        @Override
343        public void valueChanged(ListSelectionEvent e) {
344            // First index can be set to -1 when the list is refreshed, so discard all map rectangles and polygons
345            if (e.getFirstIndex() == -1) {
346                clearMap();
347            } else if (!e.getValueIsAdjusting()) {
348                // Only process complete (final) selection events
349                for (int i = e.getFirstIndex(); i <= e.getLastIndex(); i++) {
350                    updateBoundsAndShapes(defaultTable.convertRowIndexToModel(i));
351                }
352                // If needed, adjust map to show all map rectangles and polygons
353                if (!mapRectangles.isEmpty() || !mapPolygons.isEmpty()) {
354                    defaultMap.setDisplayToFitMapElements(false, true, true);
355                    defaultMap.zoomOut();
356                }
357            }
358        }
359
360        /**
361         * update bounds and shapes for a new entry
362         * @param i model index
363         */
364        private void updateBoundsAndShapes(int i) {
365            ImageryBounds bounds = defaultModel.getRow(i).getBounds();
366            if (bounds != null) {
367                int viewIndex = defaultTable.convertRowIndexToView(i);
368                List<Shape> shapes = bounds.getShapes();
369                if (shapes != null && !shapes.isEmpty()) {
370                    if (defaultTable.getSelectionModel().isSelectedIndex(viewIndex)) {
371                        if (!mapPolygons.containsKey(i)) {
372                            List<MapPolygon> list = new ArrayList<>();
373                            mapPolygons.put(i, list);
374                            // Add new map polygons
375                            for (Shape shape : shapes) {
376                                MapPolygon polygon = new MapPolygonImpl(shape.getPoints());
377                                list.add(polygon);
378                                defaultMap.addMapPolygon(polygon);
379                            }
380                        }
381                    } else if (mapPolygons.containsKey(i)) {
382                        // Remove previously drawn map polygons
383                        for (MapPolygon polygon : mapPolygons.get(i)) {
384                            defaultMap.removeMapPolygon(polygon);
385                        }
386                        mapPolygons.remove(i);
387                    }
388                    // Only display bounds when no polygons (shapes) are defined for this provider
389                } else {
390                    if (defaultTable.getSelectionModel().isSelectedIndex(viewIndex)) {
391                        if (!mapRectangles.containsKey(i)) {
392                            // Add new map rectangle
393                            Coordinate topLeft = new Coordinate(bounds.getMaxLat(), bounds.getMinLon());
394                            Coordinate bottomRight = new Coordinate(bounds.getMinLat(), bounds.getMaxLon());
395                            MapRectangle rectangle = new MapRectangleImpl(topLeft, bottomRight);
396                            mapRectangles.put(i, rectangle);
397                            defaultMap.addMapRectangle(rectangle);
398                        }
399                    } else if (mapRectangles.containsKey(i)) {
400                        // Remove previously drawn map rectangle
401                        defaultMap.removeMapRectangle(mapRectangles.get(i));
402                        mapRectangles.remove(i);
403                    }
404                }
405            }
406        }
407    }
408
409    private class NewEntryAction extends AbstractAction {
410
411        private final ImageryInfo.ImageryType type;
412
413        NewEntryAction(ImageryInfo.ImageryType type) {
414            putValue(NAME, type.toString());
415            putValue(SHORT_DESCRIPTION, tr("Add a new {0} entry by entering the URL", type.toString()));
416            String icon = /* ICON(dialogs/) */ "add";
417            switch (type) {
418            case WMS:
419                icon = /* ICON(dialogs/) */ "add_wms";
420                break;
421            case TMS:
422                icon = /* ICON(dialogs/) */ "add_tms";
423                break;
424            case WMTS:
425                icon = /* ICON(dialogs/) */ "add_wmts";
426                break;
427            default:
428                break;
429            }
430            new ImageProvider("dialogs", icon).getResource().attachImageIcon(this, true);
431            this.type = type;
432        }
433
434        @Override
435        public void actionPerformed(ActionEvent evt) {
436            final AddImageryPanel p;
437            switch (type) {
438            case WMS:
439                p = new AddWMSLayerPanel();
440                break;
441            case TMS:
442                p = new AddTMSLayerPanel();
443                break;
444            case WMTS:
445                p = new AddWMTSLayerPanel();
446                break;
447            default:
448                throw new IllegalStateException("Type " + type + " not supported");
449            }
450
451            final AddImageryDialog addDialog = new AddImageryDialog(gui, p);
452            addDialog.showDialog();
453
454            if (addDialog.getValue() == 1) {
455                try {
456                    activeModel.addRow(p.getImageryInfo());
457                } catch (IllegalArgumentException ex) {
458                    if (ex.getMessage() == null || ex.getMessage().isEmpty())
459                        throw ex;
460                    else {
461                        JOptionPane.showMessageDialog(MainApplication.getMainFrame(),
462                                ex.getMessage(), tr("Error"),
463                                JOptionPane.ERROR_MESSAGE);
464                    }
465                }
466            }
467        }
468    }
469
470    private class RemoveEntryAction extends AbstractAction implements ListSelectionListener {
471
472        /**
473         * Constructs a new {@code RemoveEntryAction}.
474         */
475        RemoveEntryAction() {
476            putValue(NAME, tr("Remove"));
477            putValue(SHORT_DESCRIPTION, tr("Remove entry"));
478            new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this, true);
479            updateEnabledState();
480        }
481
482        protected final void updateEnabledState() {
483            setEnabled(activeTable.getSelectedRowCount() > 0);
484        }
485
486        @Override
487        public void valueChanged(ListSelectionEvent e) {
488            updateEnabledState();
489        }
490
491        @Override
492        public void actionPerformed(ActionEvent e) {
493            Integer i;
494            while ((i = activeTable.getSelectedRow()) != -1) {
495                activeModel.removeRow(i);
496            }
497        }
498    }
499
500    private class ActivateAction extends AbstractAction implements ListSelectionListener {
501
502        /**
503         * Constructs a new {@code ActivateAction}.
504         */
505        ActivateAction() {
506            putValue(NAME, tr("Activate"));
507            putValue(SHORT_DESCRIPTION, tr("Copy selected default entries from the list above into the list below."));
508            new ImageProvider("preferences", "activate-down").getResource().attachImageIcon(this, true);
509        }
510
511        protected void updateEnabledState() {
512            setEnabled(defaultTable.getSelectedRowCount() > 0);
513        }
514
515        @Override
516        public void valueChanged(ListSelectionEvent e) {
517            updateEnabledState();
518        }
519
520        @Override
521        public void actionPerformed(ActionEvent e) {
522            int[] lines = defaultTable.getSelectedRows();
523            if (lines.length == 0) {
524                JOptionPane.showMessageDialog(
525                        gui,
526                        tr("Please select at least one row to copy."),
527                        tr("Information"),
528                        JOptionPane.INFORMATION_MESSAGE);
529                return;
530            }
531
532            Set<String> acceptedEulas = new HashSet<>();
533
534            outer:
535            for (int line : lines) {
536                ImageryInfo info = defaultModel.getRow(defaultTable.convertRowIndexToModel(line));
537
538                // Check if an entry with exactly the same values already exists
539                for (int j = 0; j < activeModel.getRowCount(); j++) {
540                    if (info.equalsBaseValues(activeModel.getRow(j))) {
541                        // Select the already existing row so the user has
542                        // some feedback in case an entry exists
543                        activeTable.getSelectionModel().setSelectionInterval(j, j);
544                        activeTable.scrollRectToVisible(activeTable.getCellRect(j, 0, true));
545                        continue outer;
546                    }
547                }
548
549                String eulaURL = info.getEulaAcceptanceRequired();
550                // If set and not already accepted, ask for EULA acceptance
551                if (eulaURL != null && !acceptedEulas.contains(eulaURL)) {
552                    if (confirmEulaAcceptance(gui, eulaURL)) {
553                        acceptedEulas.add(eulaURL);
554                    } else {
555                        continue outer;
556                    }
557                }
558
559                activeModel.addRow(new ImageryInfo(info));
560                int lastLine = activeModel.getRowCount() - 1;
561                activeTable.getSelectionModel().setSelectionInterval(lastLine, lastLine);
562                activeTable.scrollRectToVisible(activeTable.getCellRect(lastLine, 0, true));
563            }
564        }
565    }
566
567    private class ReloadAction extends AbstractAction {
568
569        /**
570         * Constructs a new {@code ReloadAction}.
571         */
572        ReloadAction() {
573            putValue(SHORT_DESCRIPTION, tr("Update default entries"));
574            new ImageProvider("dialogs", "refresh").getResource().attachImageIcon(this, true);
575        }
576
577        @Override
578        public void actionPerformed(ActionEvent evt) {
579            layerInfo.loadDefaults(true, MainApplication.worker, false);
580            defaultModel.fireTableDataChanged();
581            defaultTable.getSelectionModel().clearSelection();
582            defaultTableListener.clearMap();
583            /* loading new file may change active layers */
584            activeModel.fireTableDataChanged();
585        }
586    }
587
588    /**
589     * The table model for imagery layer list
590     */
591    public class ImageryLayerTableModel extends DefaultTableModel {
592        /**
593         * Constructs a new {@code ImageryLayerTableModel}.
594         */
595        public ImageryLayerTableModel() {
596            setColumnIdentifiers(new String[] {tr("Menu Name"), tr("Imagery URL")});
597        }
598
599        /**
600         * Returns the imagery info at the given row number.
601         * @param row The row number
602         * @return The imagery info at the given row number
603         */
604        public ImageryInfo getRow(int row) {
605            return layerInfo.getLayers().get(row);
606        }
607
608        /**
609         * Adds a new imagery info as the last row.
610         * @param i The imagery info to add
611         */
612        public void addRow(ImageryInfo i) {
613            layerInfo.add(i);
614            int p = getRowCount() - 1;
615            fireTableRowsInserted(p, p);
616        }
617
618        @Override
619        public void removeRow(int i) {
620            layerInfo.remove(getRow(i));
621            fireTableRowsDeleted(i, i);
622        }
623
624        @Override
625        public int getRowCount() {
626            return layerInfo.getLayers().size();
627        }
628
629        @Override
630        public Object getValueAt(int row, int column) {
631            ImageryInfo info = layerInfo.getLayers().get(row);
632            switch (column) {
633            case 0:
634                return info.getName();
635            case 1:
636                return info.getExtendedUrl();
637            default:
638                throw new ArrayIndexOutOfBoundsException(Integer.toString(column));
639            }
640        }
641
642        @Override
643        public void setValueAt(Object o, int row, int column) {
644            if (layerInfo.getLayers().size() <= row) return;
645            ImageryInfo info = layerInfo.getLayers().get(row);
646            switch (column) {
647            case 0:
648                info.setName((String) o);
649                info.clearId();
650                break;
651            case 1:
652                info.setExtendedUrl((String) o);
653                info.clearId();
654                break;
655            default:
656                throw new ArrayIndexOutOfBoundsException(Integer.toString(column));
657            }
658        }
659    }
660
661    /**
662     * The table model for the default imagery layer list
663     */
664    public class ImageryDefaultLayerTableModel extends DefaultTableModel {
665        /**
666         * Constructs a new {@code ImageryDefaultLayerTableModel}.
667         */
668        public ImageryDefaultLayerTableModel() {
669            setColumnIdentifiers(new String[]{"", "", tr("Menu Name (Default)"), tr("Imagery URL (Default)")});
670        }
671
672        /**
673         * Returns the imagery info at the given row number.
674         * @param row The row number
675         * @return The imagery info at the given row number
676         */
677        public ImageryInfo getRow(int row) {
678            return layerInfo.getAllDefaultLayers().get(row);
679        }
680
681        @Override
682        public int getRowCount() {
683            return layerInfo.getAllDefaultLayers().size();
684        }
685
686        @Override
687        public Class<?> getColumnClass(int columnIndex) {
688            switch (columnIndex) {
689            case 0:
690                return ImageryCategory.class;
691            case 1:
692                return String.class;
693            case 2:
694                return ImageryInfo.class;
695            case 3:
696                return String.class;
697            default:
698                return super.getColumnClass(columnIndex);
699            }
700        }
701
702        @Override
703        public Object getValueAt(int row, int column) {
704            ImageryInfo info = layerInfo.getAllDefaultLayers().get(row);
705            switch (column) {
706            case 0:
707                return Optional.ofNullable(info.getImageryCategory()).orElse(ImageryCategory.OTHER);
708            case 1:
709                return info.getCountryCode();
710            case 2:
711                return info;
712            case 3:
713                return info.getExtendedUrl();
714            default:
715                return null;
716            }
717        }
718
719        @Override
720        public boolean isCellEditable(int row, int column) {
721            return false;
722        }
723    }
724
725    private static boolean confirmEulaAcceptance(PreferenceTabbedPane gui, String eulaUrl) {
726        URL url;
727        try {
728            url = new URL(eulaUrl.replaceAll("\\{lang\\}", LanguageInfo.getWikiLanguagePrefix()));
729            JosmEditorPane htmlPane;
730            try {
731                htmlPane = new JosmEditorPane(url);
732            } catch (IOException e1) {
733                Logging.trace(e1);
734                // give a second chance with a default Locale 'en'
735                try {
736                    url = new URL(eulaUrl.replaceAll("\\{lang\\}", ""));
737                    htmlPane = new JosmEditorPane(url);
738                } catch (IOException e2) {
739                    Logging.debug(e2);
740                    JOptionPane.showMessageDialog(gui, tr("EULA license URL not available: {0}", eulaUrl));
741                    return false;
742                }
743            }
744            Box box = Box.createVerticalBox();
745            htmlPane.setEditable(false);
746            JScrollPane scrollPane = new JScrollPane(htmlPane);
747            scrollPane.setPreferredSize(new Dimension(400, 400));
748            box.add(scrollPane);
749            int option = JOptionPane.showConfirmDialog(MainApplication.getMainFrame(), box, tr("Please abort if you are not sure"),
750                    JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
751            if (option == JOptionPane.YES_OPTION)
752                return true;
753        } catch (MalformedURLException e2) {
754            JOptionPane.showMessageDialog(gui, tr("Malformed URL for the EULA licence: {0}", eulaUrl));
755        }
756        return false;
757    }
758}