001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.layer;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.GridBagLayout;
008import java.awt.event.ActionEvent;
009import java.util.ArrayList;
010import java.util.Collection;
011import java.util.List;
012
013import javax.swing.AbstractAction;
014import javax.swing.BorderFactory;
015import javax.swing.ImageIcon;
016import javax.swing.JCheckBox;
017import javax.swing.JLabel;
018import javax.swing.JMenuItem;
019import javax.swing.JPanel;
020import javax.swing.JPopupMenu;
021import javax.swing.JSlider;
022import javax.swing.event.ChangeEvent;
023import javax.swing.event.ChangeListener;
024
025import org.openstreetmap.josm.Main;
026import org.openstreetmap.josm.gui.SideButton;
027import org.openstreetmap.josm.gui.dialogs.LayerListDialog.LayerListModel;
028import org.openstreetmap.josm.gui.layer.ImageryLayer;
029import org.openstreetmap.josm.gui.layer.Layer;
030import org.openstreetmap.josm.gui.layer.Layer.LayerAction;
031import org.openstreetmap.josm.tools.GBC;
032import org.openstreetmap.josm.tools.ImageProvider;
033import org.openstreetmap.josm.tools.Utils;
034
035/**
036 * This is a menu that includes all settings for the layer visibility. It combines gamma/opacity sliders and the visible-checkbox.
037 *
038 * @author Michael Zangl
039 */
040public final class LayerVisibilityAction extends AbstractAction implements IEnabledStateUpdating, LayerAction {
041    private static final int SLIDER_STEPS = 100;
042    private static final double MAX_SHARPNESS_FACTOR = 2;
043    private static final double MAX_COLORFUL_FACTOR = 2;
044    private final LayerListModel model;
045    private final JPopupMenu popup;
046    private SideButton sideButton;
047    private final JCheckBox visibilityCheckbox;
048    final OpacitySlider opacitySlider = new OpacitySlider();
049    private final ArrayList<FilterSlider<?>> sliders = new ArrayList<>();
050
051    /**
052     * Creates a new {@link LayerVisibilityAction}
053     * @param model The list to get the selection from.
054     */
055    public LayerVisibilityAction(LayerListModel model) {
056        this.model = model;
057        popup = new JPopupMenu();
058
059        // just to add a border
060        JPanel content = new JPanel();
061        popup.add(content);
062        content.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
063        content.setLayout(new GridBagLayout());
064
065        new ImageProvider("dialogs/layerlist", "visibility").getResource().attachImageIcon(this, true);
066        putValue(SHORT_DESCRIPTION, tr("Change visibility of the selected layer."));
067
068        visibilityCheckbox = new JCheckBox(tr("Show layer"));
069        visibilityCheckbox.addChangeListener(new ChangeListener() {
070            @Override
071            public void stateChanged(ChangeEvent e) {
072                setVisibleFlag(visibilityCheckbox.isSelected());
073            }
074        });
075        content.add(visibilityCheckbox, GBC.eop());
076
077        addSlider(content, opacitySlider);
078        addSlider(content, new ColorfulnessSlider());
079        addSlider(content, new GammaFilterSlider());
080        addSlider(content, new SharpnessSlider());
081    }
082
083    private void addSlider(JPanel content, FilterSlider<?> slider) {
084        content.add(new JLabel(slider.getIcon()), GBC.std().span(1, 2).insets(0, 0, 5, 0));
085        content.add(new JLabel(slider.getLabel()), GBC.eol());
086        content.add(slider, GBC.eop());
087        sliders.add(slider);
088    }
089
090    protected void setVisibleFlag(boolean visible) {
091        for (Layer l : model.getSelectedLayers()) {
092            l.setVisible(visible);
093        }
094        updateValues();
095    }
096
097    @Override
098    public void actionPerformed(ActionEvent e) {
099        updateValues();
100        if (e.getSource() == sideButton) {
101            popup.show(sideButton, 0, sideButton.getHeight());
102        } else {
103            // Action can be trigger either by opacity button or by popup menu (in case toggle buttons are hidden).
104            // In that case, show it in the middle of screen (because opacityButton is not visible)
105            popup.show(Main.parent, Main.parent.getWidth() / 2, (Main.parent.getHeight() - popup.getHeight()) / 2);
106        }
107    }
108
109    protected void updateValues() {
110        List<Layer> layers = model.getSelectedLayers();
111
112        visibilityCheckbox.setEnabled(!layers.isEmpty());
113        boolean allVisible = true;
114        boolean allHidden = true;
115        for (Layer l : layers) {
116            allVisible &= l.isVisible();
117            allHidden &= !l.isVisible();
118        }
119        // TODO: Indicate tristate.
120        visibilityCheckbox.setSelected(allVisible && !allHidden);
121
122        for (FilterSlider<?> slider : sliders) {
123            slider.updateSlider(layers, allHidden);
124        }
125    }
126
127    @Override
128    public boolean supportLayers(List<Layer> layers) {
129        return !layers.isEmpty();
130    }
131
132    @Override
133    public Component createMenuComponent() {
134        return new JMenuItem(this);
135    }
136
137    @Override
138    public void updateEnabledState() {
139        setEnabled(!model.getSelectedLayers().isEmpty());
140    }
141
142    /**
143     * Sets the corresponding side button.
144     * @param sideButton the corresponding side button
145     */
146    public void setCorrespondingSideButton(SideButton sideButton) {
147        this.sideButton = sideButton;
148    }
149
150    /**
151     * This is a slider for a filter value.
152     * @author Michael Zangl
153     *
154     * @param <T> The layer type.
155     */
156    private abstract class FilterSlider<T extends Layer> extends JSlider {
157        private final double minValue;
158        private final double maxValue;
159        private final Class<T> layerClassFilter;
160
161        /**
162         * Create a new filter slider.
163         * @param minValue The minimum value to map to the left side.
164         * @param maxValue The maximum value to map to the right side.
165         * @param layerClassFilter The type of layer influenced by this filter.
166         */
167        FilterSlider(double minValue, double maxValue, Class<T> layerClassFilter) {
168            super(JSlider.HORIZONTAL);
169            this.minValue = minValue;
170            this.maxValue = maxValue;
171            this.layerClassFilter = layerClassFilter;
172            setMaximum(SLIDER_STEPS);
173            int tick = convertFromRealValue(1);
174            setMinorTickSpacing(tick);
175            setMajorTickSpacing(tick);
176            setPaintTicks(true);
177
178            addChangeListener(new ChangeListener() {
179                @Override
180                public void stateChanged(ChangeEvent e) {
181                    onStateChanged();
182                }
183            });
184        }
185
186        /**
187         * Called whenever the state of the slider was changed.
188         * @see #getValueIsAdjusting()
189         * @see #getRealValue()
190         */
191        protected void onStateChanged() {
192            Collection<T> layers = filterLayers(model.getSelectedLayers());
193            for (T layer : layers) {
194                applyValueToLayer(layer);
195            }
196        }
197
198        protected void applyValueToLayer(T layer) {
199        }
200
201        protected double getRealValue() {
202            return convertToRealValue(getValue());
203        }
204
205        protected double convertToRealValue(int value) {
206            double s = (double) value / SLIDER_STEPS;
207            return s * maxValue + (1-s) * minValue;
208        }
209
210        protected void setRealValue(double value) {
211            setValue(convertFromRealValue(value));
212        }
213
214        protected int convertFromRealValue(double value) {
215            int i = (int) ((value - minValue) / (maxValue - minValue) * SLIDER_STEPS + .5);
216            if (i < getMinimum()) {
217                return getMinimum();
218            } else if (i > getMaximum()) {
219                return getMaximum();
220            } else {
221                return i;
222            }
223        }
224
225        public abstract ImageIcon getIcon();
226
227        public abstract String getLabel();
228
229        public void updateSlider(List<Layer> layers, boolean allHidden) {
230            Collection<? extends Layer> usedLayers = filterLayers(layers);
231            if (usedLayers.isEmpty() || allHidden) {
232                setEnabled(false);
233            } else {
234                setEnabled(true);
235                updateSliderWhileEnabled(usedLayers, allHidden);
236            }
237        }
238
239        protected Collection<T> filterLayers(List<Layer> layers) {
240            return Utils.filteredCollection(layers, layerClassFilter);
241        }
242
243        protected abstract void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden);
244    }
245
246    /**
247     * This slider allows you to change the opacity of a layer.
248     *
249     * @author Michael Zangl
250     * @see Layer#setOpacity(double)
251     */
252    class OpacitySlider extends FilterSlider<Layer> {
253        /**
254         * Creaate a new {@link OpacitySlider}.
255         */
256        OpacitySlider() {
257            super(0, 1, Layer.class);
258            setToolTipText(tr("Adjust opacity of the layer."));
259        }
260
261        @Override
262        protected void onStateChanged() {
263            if (getRealValue() <= 0.001 && !getValueIsAdjusting()) {
264                setVisibleFlag(false);
265            } else {
266                super.onStateChanged();
267            }
268        }
269
270        @Override
271        protected void applyValueToLayer(Layer layer) {
272            layer.setOpacity(getRealValue());
273        }
274
275        @Override
276        protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
277            double opacity = 0;
278            for (Layer l : usedLayers) {
279                opacity += l.getOpacity();
280            }
281            opacity /= usedLayers.size();
282            if (opacity == 0) {
283                opacity = 1;
284                setVisibleFlag(true);
285            }
286            setRealValue(opacity);
287        }
288
289        @Override
290        public String getLabel() {
291            return tr("Opacity");
292        }
293
294        @Override
295        public ImageIcon getIcon() {
296            return ImageProvider.get("dialogs/layerlist", "transparency");
297        }
298
299        @Override
300        public String toString() {
301            return "OpacitySlider [getRealValue()=" + getRealValue() + ']';
302        }
303    }
304
305    /**
306     * This slider allows you to change the gamma value of a layer.
307     *
308     * @author Michael Zangl
309     * @see ImageryLayer#setGamma(double)
310     */
311    private class GammaFilterSlider extends FilterSlider<ImageryLayer> {
312
313        /**
314         * Create a new {@link GammaFilterSlider}
315         */
316        GammaFilterSlider() {
317            super(-1, 1, ImageryLayer.class);
318            setToolTipText(tr("Adjust gamma value of the layer."));
319        }
320
321        @Override
322        protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
323            double gamma = ((ImageryLayer) usedLayers.iterator().next()).getGamma();
324            setRealValue(mapGammaToInterval(gamma));
325        }
326
327        @Override
328        protected void applyValueToLayer(ImageryLayer layer) {
329            layer.setGamma(mapIntervalToGamma(getRealValue()));
330        }
331
332        @Override
333        public ImageIcon getIcon() {
334           return ImageProvider.get("dialogs/layerlist", "gamma");
335        }
336
337        @Override
338        public String getLabel() {
339            return tr("Gamma");
340        }
341
342        /**
343         * Maps a number x from the range (-1,1) to a gamma value.
344         * Gamma value is in the range (0, infinity).
345         * Gamma values of 3 and 1/3 have opposite effects, so the mapping
346         * should be symmetric in that sense.
347         * @param x the slider value in the range (-1,1)
348         * @return the gamma value
349         */
350        private double mapIntervalToGamma(double x) {
351            // properties of the mapping:
352            // g(-1) = 0
353            // g(0) = 1
354            // g(1) = infinity
355            // g(-x) = 1 / g(x)
356            return (1 + x) / (1 - x);
357        }
358
359        private double mapGammaToInterval(double gamma) {
360            return (gamma - 1) / (gamma + 1);
361        }
362    }
363
364    /**
365     * This slider allows you to change the sharpness of a layer.
366     *
367     * @author Michael Zangl
368     * @see ImageryLayer#setSharpenLevel(double)
369     */
370    private class SharpnessSlider extends FilterSlider<ImageryLayer> {
371
372        /**
373         * Creates a new {@link SharpnessSlider}
374         */
375        SharpnessSlider() {
376            super(0, MAX_SHARPNESS_FACTOR, ImageryLayer.class);
377            setToolTipText(tr("Adjust sharpness/blur value of the layer."));
378        }
379
380        @Override
381        protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
382            setRealValue(((ImageryLayer) usedLayers.iterator().next()).getSharpenLevel());
383        }
384
385        @Override
386        protected void applyValueToLayer(ImageryLayer layer) {
387            layer.setSharpenLevel(getRealValue());
388        }
389
390        @Override
391        public ImageIcon getIcon() {
392           return ImageProvider.get("dialogs/layerlist", "sharpness");
393        }
394
395        @Override
396        public String getLabel() {
397            return tr("Sharpness");
398        }
399    }
400
401    /**
402     * This slider allows you to change the colorfulness of a layer.
403     *
404     * @author Michael Zangl
405     * @see ImageryLayer#setColorfulness(double)
406     */
407    private class ColorfulnessSlider extends FilterSlider<ImageryLayer> {
408
409        /**
410         * Create a new {@link ColorfulnessSlider}
411         */
412        ColorfulnessSlider() {
413            super(0, MAX_COLORFUL_FACTOR, ImageryLayer.class);
414            setToolTipText(tr("Adjust colorfulness of the layer."));
415        }
416
417        @Override
418        protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
419            setRealValue(((ImageryLayer) usedLayers.iterator().next()).getColorfulness());
420        }
421
422        @Override
423        protected void applyValueToLayer(ImageryLayer layer) {
424            layer.setColorfulness(getRealValue());
425        }
426
427        @Override
428        public ImageIcon getIcon() {
429           return ImageProvider.get("dialogs/layerlist", "colorfulness");
430        }
431
432        @Override
433        public String getLabel() {
434            return tr("Colorfulness");
435        }
436    }
437}