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