// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.dialogs.layer;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Component;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.MouseWheelEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSlider;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.gui.SideButton;
import org.openstreetmap.josm.gui.dialogs.IEnabledStateUpdating;
import org.openstreetmap.josm.gui.dialogs.LayerListDialog.LayerListModel;
import org.openstreetmap.josm.gui.layer.ImageryLayer;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.gui.layer.Layer.LayerAction;
import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.Utils;
/**
* This is a menu that includes all settings for the layer visibility. It combines gamma/opacity sliders and the visible-checkbox.
*
* @author Michael Zangl
*/
public final class LayerVisibilityAction extends AbstractAction implements IEnabledStateUpdating, LayerAction {
private static final int SLIDER_STEPS = 100;
/**
* Steps the value is changed by a mouse wheel change (one full click)
*/
private static final int SLIDER_WHEEL_INCREMENT = 5;
private static final double MAX_SHARPNESS_FACTOR = 2;
private static final double MAX_COLORFUL_FACTOR = 2;
private final LayerListModel model;
private final JPopupMenu popup;
private SideButton sideButton;
private final JCheckBox visibilityCheckbox;
final OpacitySlider opacitySlider = new OpacitySlider();
private final ArrayList<FilterSlider<?>> sliders = new ArrayList<>();
/**
* Creates a new {@link LayerVisibilityAction}
* @param model The list to get the selection from.
*/
public LayerVisibilityAction(LayerListModel model) {
this.model = model;
popup = new JPopupMenu();
// prevent popup close on mouse wheel move
popup.addMouseWheelListener(MouseWheelEvent::consume);
// just to add a border
JPanel content = new JPanel();
popup.add(content);
content.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
content.setLayout(new GridBagLayout());
new ImageProvider("dialogs/layerlist", "visibility").getResource().attachImageIcon(this, true);
putValue(SHORT_DESCRIPTION, tr("Change visibility of the selected layer."));
visibilityCheckbox = new JCheckBox(tr("Show layer"));
visibilityCheckbox.addChangeListener(e -> setVisibleFlag(visibilityCheckbox.isSelected()));
content.add(visibilityCheckbox, GBC.eop());
addSlider(content, opacitySlider);
addSlider(content, new ColorfulnessSlider());
addSlider(content, new GammaFilterSlider());
addSlider(content, new SharpnessSlider());
}
private void addSlider(JPanel content, FilterSlider<?> slider) {
// wrap to a common content pane to allow for mouse wheel listener on label.
JPanel container = new JPanel(new GridBagLayout());
container.add(new JLabel(slider.getIcon()), GBC.std().span(1, 2).insets(0, 0, 5, 0));
container.add(new JLabel(slider.getLabel()), GBC.eol());
container.add(slider, GBC.eol());
content.add(container, GBC.eop());
container.addMouseWheelListener(slider::mouseWheelMoved);
sliders.add(slider);
}
void setVisibleFlag(boolean visible) {
for (Layer l : model.getSelectedLayers()) {
l.setVisible(visible);
}
updateValues();
}
@Override
public void actionPerformed(ActionEvent e) {
updateValues();
if (e.getSource() == sideButton) {
popup.show(sideButton, 0, sideButton.getHeight());
} else {
// Action can be trigger either by opacity button or by popup menu (in case toggle buttons are hidden).
// In that case, show it in the middle of screen (because opacityButton is not visible)
popup.show(Main.parent, Main.parent.getWidth() / 2, (Main.parent.getHeight() - popup.getHeight()) / 2);
}
}
void updateValues() {
List<Layer> layers = model.getSelectedLayers();
visibilityCheckbox.setEnabled(!layers.isEmpty());
boolean allVisible = true;
boolean allHidden = true;
for (Layer l : layers) {
allVisible &= l.isVisible();
allHidden &= !l.isVisible();
}
// TODO: Indicate tristate.
visibilityCheckbox.setSelected(allVisible && !allHidden);
for (FilterSlider<?> slider : sliders) {
slider.updateSlider(layers, allHidden);
}
}
@Override
public boolean supportLayers(List<Layer> layers) {
return !layers.isEmpty();
}
@Override
public Component createMenuComponent() {
return new JMenuItem(this);
}
@Override
public void updateEnabledState() {
setEnabled(!model.getSelectedLayers().isEmpty());
}
/**
* Sets the corresponding side button.
* @param sideButton the corresponding side button
*/
public void setCorrespondingSideButton(SideButton sideButton) {
this.sideButton = sideButton;
}
/**
* This is a slider for a filter value.
* @author Michael Zangl
*
* @param <T> The layer type.
*/
private abstract class FilterSlider<T extends Layer> extends JSlider {
private final double minValue;
private final double maxValue;
private final Class<T> layerClassFilter;
/**
* Create a new filter slider.
* @param minValue The minimum value to map to the left side.
* @param maxValue The maximum value to map to the right side.
* @param layerClassFilter The type of layer influenced by this filter.
*/
FilterSlider(double minValue, double maxValue, Class<T> layerClassFilter) {
super(JSlider.HORIZONTAL);
this.minValue = minValue;
this.maxValue = maxValue;
this.layerClassFilter = layerClassFilter;
setMaximum(SLIDER_STEPS);
int tick = convertFromRealValue(1);
setMinorTickSpacing(tick);
setMajorTickSpacing(tick);
setPaintTicks(true);
addChangeListener(e -> onStateChanged());
}
/**
* Called whenever the state of the slider was changed.
* @see #getValueIsAdjusting()
* @see #getRealValue()
*/
protected void onStateChanged() {
Collection<T> layers = filterLayers(model.getSelectedLayers());
for (T layer : layers) {
applyValueToLayer(layer);
}
}
protected void mouseWheelMoved(MouseWheelEvent e) {
e.consume();
if (!isEnabled()) {
// ignore mouse wheel in disabled state.
return;
}
double rotation = -1 * e.getPreciseWheelRotation();
double destinationValue = getValue() + rotation * SLIDER_WHEEL_INCREMENT;
if (rotation < 0) {
destinationValue = Math.floor(destinationValue);
} else {
destinationValue = Math.ceil(destinationValue);
}
setValue(Utils.clamp((int) destinationValue, getMinimum(), getMaximum()));
}
abstract void applyValueToLayer(T layer);
protected double getRealValue() {
return convertToRealValue(getValue());
}
protected double convertToRealValue(int value) {
double s = (double) value / SLIDER_STEPS;
return s * maxValue + (1-s) * minValue;
}
protected void setRealValue(double value) {
setValue(convertFromRealValue(value));
}
protected int convertFromRealValue(double value) {
int i = (int) ((value - minValue) / (maxValue - minValue) * SLIDER_STEPS + .5);
return Utils.clamp(i, getMinimum(), getMaximum());
}
public abstract ImageIcon getIcon();
public abstract String getLabel();
public void updateSlider(List<Layer> layers, boolean allHidden) {
Collection<? extends Layer> usedLayers = filterLayers(layers);
if (usedLayers.isEmpty() || allHidden) {
setEnabled(false);
} else {
setEnabled(true);
updateSliderWhileEnabled(usedLayers, allHidden);
}
}
protected Collection<T> filterLayers(List<Layer> layers) {
return Utils.filteredCollection(layers, layerClassFilter);
}
protected abstract void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden);
}
/**
* This slider allows you to change the opacity of a layer.
*
* @author Michael Zangl
* @see Layer#setOpacity(double)
*/
class OpacitySlider extends FilterSlider<Layer> {
/**
* Creaate a new {@link OpacitySlider}.
*/
OpacitySlider() {
super(0, 1, Layer.class);
setToolTipText(tr("Adjust opacity of the layer."));
}
@Override
protected void onStateChanged() {
if (getRealValue() <= 0.001 && !getValueIsAdjusting()) {
setVisibleFlag(false);
} else {
super.onStateChanged();
}
}
@Override
protected void mouseWheelMoved(MouseWheelEvent e) {
if (!isEnabled() && !filterLayers(model.getSelectedLayers()).isEmpty() && e.getPreciseWheelRotation() < 0) {
// make layer visible and set the value.
// this allows users to use the mouse wheel to make the layer visible if it was hidden previously.
e.consume();
setVisibleFlag(true);
} else {
super.mouseWheelMoved(e);
}
}
@Override
protected void applyValueToLayer(Layer layer) {
layer.setOpacity(getRealValue());
}
@Override
protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
double opacity = 0;
for (Layer l : usedLayers) {
opacity += l.getOpacity();
}
opacity /= usedLayers.size();
if (opacity == 0) {
opacity = 1;
setVisibleFlag(true);
}
setRealValue(opacity);
}
@Override
public String getLabel() {
return tr("Opacity");
}
@Override
public ImageIcon getIcon() {
return ImageProvider.get("dialogs/layerlist", "transparency");
}
@Override
public String toString() {
return "OpacitySlider [getRealValue()=" + getRealValue() + ']';
}
}
/**
* This slider allows you to change the gamma value of a layer.
*
* @author Michael Zangl
* @see ImageryFilterSettings#setGamma(double)
*/
private class GammaFilterSlider extends FilterSlider<ImageryLayer> {
/**
* Create a new {@link GammaFilterSlider}
*/
GammaFilterSlider() {
super(-1, 1, ImageryLayer.class);
setToolTipText(tr("Adjust gamma value of the layer."));
}
@Override
protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
double gamma = ((ImageryLayer) usedLayers.iterator().next()).getFilterSettings().getGamma();
setRealValue(mapGammaToInterval(gamma));
}
@Override
protected void applyValueToLayer(ImageryLayer layer) {
layer.getFilterSettings().setGamma(mapIntervalToGamma(getRealValue()));
}
@Override
public ImageIcon getIcon() {
return ImageProvider.get("dialogs/layerlist", "gamma");
}
@Override
public String getLabel() {
return tr("Gamma");
}
/**
* Maps a number x from the range (-1,1) to a gamma value.
* Gamma value is in the range (0, infinity).
* Gamma values of 3 and 1/3 have opposite effects, so the mapping
* should be symmetric in that sense.
* @param x the slider value in the range (-1,1)
* @return the gamma value
*/
private double mapIntervalToGamma(double x) {
// properties of the mapping:
// g(-1) = 0
// g(0) = 1
// g(1) = infinity
// g(-x) = 1 / g(x)
return (1 + x) / (1 - x);
}
private double mapGammaToInterval(double gamma) {
return (gamma - 1) / (gamma + 1);
}
}
/**
* This slider allows you to change the sharpness of a layer.
*
* @author Michael Zangl
* @see ImageryFilterSettings#setSharpenLevel(double)
*/
private class SharpnessSlider extends FilterSlider<ImageryLayer> {
/**
* Creates a new {@link SharpnessSlider}
*/
SharpnessSlider() {
super(0, MAX_SHARPNESS_FACTOR, ImageryLayer.class);
setToolTipText(tr("Adjust sharpness/blur value of the layer."));
}
@Override
protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
setRealValue(((ImageryLayer) usedLayers.iterator().next()).getFilterSettings().getSharpenLevel());
}
@Override
protected void applyValueToLayer(ImageryLayer layer) {
layer.getFilterSettings().setSharpenLevel(getRealValue());
}
@Override
public ImageIcon getIcon() {
return ImageProvider.get("dialogs/layerlist", "sharpness");
}
@Override
public String getLabel() {
return tr("Sharpness");
}
}
/**
* This slider allows you to change the colorfulness of a layer.
*
* @author Michael Zangl
* @see ImageryFilterSettings#setColorfulness(double)
*/
private class ColorfulnessSlider extends FilterSlider<ImageryLayer> {
/**
* Create a new {@link ColorfulnessSlider}
*/
ColorfulnessSlider() {
super(0, MAX_COLORFUL_FACTOR, ImageryLayer.class);
setToolTipText(tr("Adjust colorfulness of the layer."));
}
@Override
protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
setRealValue(((ImageryLayer) usedLayers.iterator().next()).getFilterSettings().getColorfulness());
}
@Override
protected void applyValueToLayer(ImageryLayer layer) {
layer.getFilterSettings().setColorfulness(getRealValue());
}
@Override
public ImageIcon getIcon() {
return ImageProvider.get("dialogs/layerlist", "colorfulness");
}
@Override
public String getLabel() {
return tr("Colorfulness");
}
}
}