001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.imagery;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.GridBagLayout;
007import java.awt.LayoutManager;
008import java.awt.event.ItemEvent;
009import java.util.ArrayList;
010import java.util.Arrays;
011import java.util.Collection;
012import java.util.List;
013import java.util.Map;
014import java.util.concurrent.TimeUnit;
015
016import javax.swing.AbstractButton;
017import javax.swing.JCheckBox;
018import javax.swing.JComboBox;
019import javax.swing.JLabel;
020import javax.swing.JPanel;
021import javax.swing.JSpinner;
022import javax.swing.SpinnerNumberModel;
023import javax.swing.event.DocumentEvent;
024import javax.swing.event.DocumentListener;
025import javax.swing.text.JTextComponent;
026
027import org.openstreetmap.josm.actions.ExpertToggleAction;
028import org.openstreetmap.josm.data.imagery.ImageryInfo;
029import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
030import org.openstreetmap.josm.data.imagery.TMSCachedTileLoaderJob;
031import org.openstreetmap.josm.gui.widgets.JosmTextArea;
032import org.openstreetmap.josm.gui.widgets.JosmTextField;
033import org.openstreetmap.josm.tools.GBC;
034import org.openstreetmap.josm.tools.Logging;
035
036/**
037 * An abstract imagery panel used to add WMS/TMS imagery sources. See implementations.
038 * @see AddTMSLayerPanel
039 * @see AddWMSLayerPanel
040 * @see AddWMTSLayerPanel
041 * @since 5617
042 */
043public abstract class AddImageryPanel extends JPanel {
044
045    protected final JosmTextArea rawUrl = new JosmTextArea(3, 40).transferFocusOnTab();
046    protected final JosmTextField name = new JosmTextField();
047
048    protected final transient Collection<ContentValidationListener> listeners = new ArrayList<>();
049
050    private final JCheckBox validGeoreference = new JCheckBox(tr("Is layer properly georeferenced?"));
051    private HeadersTable headersTable;
052    private JSpinner minimumCacheExpiry;
053    private JComboBox<String> minimumCacheExpiryUnit;
054    private TimeUnit currentUnit;
055
056    /**
057     * A listener notified when the validation status of this panel change.
058     * @since 10600 (functional interface)
059     */
060    @FunctionalInterface
061    public interface ContentValidationListener {
062        /**
063         * Called when the validation status of this panel changed
064         * @param isValid true if the conditions required to close this panel are met
065         */
066        void contentChanged(boolean isValid);
067    }
068
069    protected AddImageryPanel() {
070        this(new GridBagLayout());
071        headersTable = new HeadersTable();
072        minimumCacheExpiry = new JSpinner(new SpinnerNumberModel(
073                (Number) TimeUnit.MILLISECONDS.toSeconds(TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get()),
074                0L,
075                Long.valueOf(Integer.MAX_VALUE),
076                1
077                ));
078        List<String> units = Arrays.asList(tr("seconds"), tr("minutes"), tr("hours"), tr("days"));
079        minimumCacheExpiryUnit = new JComboBox<>(units.toArray(new String[]{}));
080        currentUnit = TimeUnit.SECONDS;
081        minimumCacheExpiryUnit.addItemListener(e -> {
082            if (e.getStateChange() == ItemEvent.SELECTED) {
083                long newValue = 0;
084                switch (units.indexOf(e.getItem())) {
085                case 0:
086                    newValue = currentUnit.toSeconds((long) minimumCacheExpiry.getValue());
087                    currentUnit = TimeUnit.SECONDS;
088                    break;
089                case 1:
090                    newValue = currentUnit.toMinutes((long) minimumCacheExpiry.getValue());
091                    currentUnit = TimeUnit.MINUTES;
092                    break;
093                case 2:
094                    newValue = currentUnit.toHours((long) minimumCacheExpiry.getValue());
095                    currentUnit = TimeUnit.HOURS;
096                    break;
097                case 3:
098                    newValue = currentUnit.toDays((long) minimumCacheExpiry.getValue());
099                    currentUnit = TimeUnit.DAYS;
100                    break;
101                default:
102                    Logging.warn("Unkown unit: " + units.indexOf(e.getItem()));
103                }
104                minimumCacheExpiry.setValue(newValue);
105            }
106        });
107
108
109    }
110
111    protected void addCommonSettings() {
112        if (ExpertToggleAction.isExpert()) {
113            add(new JLabel(tr("Minimum cache expiry: ")));
114            add(minimumCacheExpiry);
115            add(minimumCacheExpiryUnit, GBC.eol());
116            add(new JLabel(tr("Set custom HTTP headers (if needed):")), GBC.eop());
117            add(headersTable, GBC.eol().fill());
118            add(validGeoreference, GBC.eop().fill(GBC.HORIZONTAL));
119        }
120    }
121
122    protected Map<String, String> getCommonHeaders() {
123        return headersTable.getHeaders();
124    }
125
126    protected boolean getCommonIsValidGeoreference() {
127        return validGeoreference.isSelected();
128    }
129
130    protected AddImageryPanel(LayoutManager layout) {
131        super(layout);
132        registerValidableComponent(name);
133    }
134
135    protected final void registerValidableComponent(AbstractButton component) {
136        component.addChangeListener(e -> notifyListeners());
137    }
138
139    protected final void registerValidableComponent(JTextComponent component) {
140        component.getDocument().addDocumentListener(new DocumentListener() {
141            @Override
142            public void removeUpdate(DocumentEvent e) {
143                notifyListeners();
144            }
145
146            @Override
147            public void insertUpdate(DocumentEvent e) {
148                notifyListeners();
149            }
150
151            @Override
152            public void changedUpdate(DocumentEvent e) {
153                notifyListeners();
154            }
155        });
156    }
157
158    protected abstract ImageryInfo getImageryInfo();
159
160    protected static String sanitize(String s) {
161        return s.replaceAll("[\r\n]+", "").trim();
162    }
163
164    protected static String sanitize(String s, ImageryType type) {
165        String ret = s;
166        String imageryType = type.getTypeString() + ':';
167        if (ret.startsWith(imageryType)) {
168            // remove ImageryType from URL
169            ret = ret.substring(imageryType.length());
170        }
171        return sanitize(ret);
172    }
173
174    protected final String getImageryName() {
175        return sanitize(name.getText());
176    }
177
178    protected final String getImageryRawUrl() {
179        return sanitize(rawUrl.getText());
180    }
181
182    protected abstract boolean isImageryValid();
183
184    /**
185     * Registers a new ContentValidationListener
186     * @param l The new ContentValidationListener that will be notified of validation status changes
187     */
188    public final void addContentValidationListener(ContentValidationListener l) {
189        if (l != null) {
190            listeners.add(l);
191        }
192    }
193
194    private void notifyListeners() {
195        for (ContentValidationListener l : listeners) {
196            l.contentChanged(isImageryValid());
197        }
198    }
199}