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 putValue(SMALL_ICON, ImageProvider.get("dialogs/layerlist", "visibility")); 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}