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