source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/layer/LayerVisibilityAction.java@ 14387

Last change on this file since 14387 was 14387, checked in by simon04, 5 years ago

fix #15832 - Add labels for background imagery sliders

  • Property svn:eol-style set to native
File size: 22.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs.layer;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trc;
6
7import java.awt.BorderLayout;
8import java.awt.Color;
9import java.awt.Component;
10import java.awt.Dimension;
11import java.awt.GridBagLayout;
12import java.awt.event.ActionEvent;
13import java.awt.event.MouseAdapter;
14import java.awt.event.MouseEvent;
15import java.awt.event.MouseWheelEvent;
16import java.util.ArrayList;
17import java.util.Collection;
18import java.util.Dictionary;
19import java.util.HashMap;
20import java.util.Hashtable;
21import java.util.List;
22import java.util.function.Supplier;
23import java.util.stream.Collectors;
24
25import javax.swing.AbstractAction;
26import javax.swing.BorderFactory;
27import javax.swing.Icon;
28import javax.swing.ImageIcon;
29import javax.swing.JCheckBox;
30import javax.swing.JComponent;
31import javax.swing.JLabel;
32import javax.swing.JMenuItem;
33import javax.swing.JPanel;
34import javax.swing.JPopupMenu;
35import javax.swing.JSlider;
36import javax.swing.UIManager;
37import javax.swing.border.Border;
38
39import org.openstreetmap.josm.gui.MainApplication;
40import org.openstreetmap.josm.gui.MainFrame;
41import org.openstreetmap.josm.gui.SideButton;
42import org.openstreetmap.josm.gui.dialogs.IEnabledStateUpdating;
43import org.openstreetmap.josm.gui.dialogs.LayerListDialog.LayerListModel;
44import org.openstreetmap.josm.gui.layer.GpxLayer;
45import org.openstreetmap.josm.gui.layer.ImageryLayer;
46import org.openstreetmap.josm.gui.layer.Layer;
47import org.openstreetmap.josm.gui.layer.Layer.LayerAction;
48import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
49import org.openstreetmap.josm.tools.GBC;
50import org.openstreetmap.josm.tools.ImageProvider;
51import org.openstreetmap.josm.tools.Utils;
52
53/**
54 * This is a menu that includes all settings for the layer visibility. It combines gamma/opacity sliders and the visible-checkbox.
55 *
56 * @author Michael Zangl
57 */
58public final class LayerVisibilityAction extends AbstractAction implements IEnabledStateUpdating, LayerAction {
59 private static final String DIALOGS_LAYERLIST = "dialogs/layerlist";
60 private static final int SLIDER_STEPS = 100;
61 /**
62 * Steps the value is changed by a mouse wheel change (one full click)
63 */
64 private static final int SLIDER_WHEEL_INCREMENT = 5;
65 private static final double MAX_SHARPNESS_FACTOR = 2;
66 private static final double MAX_COLORFUL_FACTOR = 2;
67 private final LayerListModel model;
68 private final JPopupMenu popup;
69 private SideButton sideButton;
70 /**
71 * The real content, just to add a border
72 */
73 private final JPanel content = new JPanel();
74 final OpacitySlider opacitySlider = new OpacitySlider();
75 private final ArrayList<LayerVisibilityMenuEntry> sliders = new ArrayList<>();
76
77 /**
78 * Creates a new {@link LayerVisibilityAction}
79 * @param model The list to get the selection from.
80 */
81 public LayerVisibilityAction(LayerListModel model) {
82 this.model = model;
83 popup = new JPopupMenu();
84 // prevent popup close on mouse wheel move
85 popup.addMouseWheelListener(MouseWheelEvent::consume);
86
87 popup.add(content);
88 content.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
89 content.setLayout(new GridBagLayout());
90
91 new ImageProvider(DIALOGS_LAYERLIST, "visibility").getResource().attachImageIcon(this, true);
92 putValue(SHORT_DESCRIPTION, tr("Change visibility of the selected layer."));
93
94 addContentEntry(new VisibilityCheckbox());
95
96 addContentEntry(opacitySlider);
97 addContentEntry(new ColorfulnessSlider());
98 addContentEntry(new GammaFilterSlider());
99 addContentEntry(new SharpnessSlider());
100 addContentEntry(new ColorSelector(model::getSelectedLayers));
101 }
102
103 private void addContentEntry(LayerVisibilityMenuEntry slider) {
104 content.add(slider.getPanel(), GBC.eop().fill(GBC.HORIZONTAL));
105 sliders.add(slider);
106 }
107
108 void setVisibleFlag(boolean visible) {
109 for (Layer l : model.getSelectedLayers()) {
110 l.setVisible(visible);
111 }
112 updateValues();
113 }
114
115 @Override
116 public void actionPerformed(ActionEvent e) {
117 updateValues();
118 if (e.getSource() == sideButton) {
119 if (sideButton.isShowing()) {
120 popup.show(sideButton, 0, sideButton.getHeight());
121 }
122 } else {
123 // Action can be trigger either by opacity button or by popup menu (in case toggle buttons are hidden).
124 // In that case, show it in the middle of screen (because opacityButton is not visible)
125 MainFrame mainFrame = MainApplication.getMainFrame();
126 if (mainFrame.isShowing()) {
127 popup.show(mainFrame, mainFrame.getWidth() / 2, (mainFrame.getHeight() - popup.getHeight()) / 2);
128 }
129 }
130 }
131
132 void updateValues() {
133 List<Layer> layers = model.getSelectedLayers();
134
135 boolean allVisible = true;
136 boolean allHidden = true;
137 for (Layer l : layers) {
138 allVisible &= l.isVisible();
139 allHidden &= !l.isVisible();
140 }
141
142 for (LayerVisibilityMenuEntry slider : sliders) {
143 slider.updateLayers(layers, allVisible, allHidden);
144 }
145 }
146
147 @Override
148 public boolean supportLayers(List<Layer> layers) {
149 return !layers.isEmpty();
150 }
151
152 @Override
153 public Component createMenuComponent() {
154 return new JMenuItem(this);
155 }
156
157 @Override
158 public void updateEnabledState() {
159 setEnabled(!model.getSelectedLayers().isEmpty());
160 }
161
162 /**
163 * Sets the corresponding side button.
164 * @param sideButton the corresponding side button
165 */
166 public void setCorrespondingSideButton(SideButton sideButton) {
167 this.sideButton = sideButton;
168 }
169
170 /**
171 * An entry in the visibility settings dropdown.
172 * @author Michael Zangl
173 */
174 private interface LayerVisibilityMenuEntry {
175
176 /**
177 * Update the displayed value depending on the current layers
178 * @param layers The layers
179 * @param allVisible <code>true</code> if all layers are visible
180 * @param allHidden <code>true</code> if all layers are hidden
181 */
182 void updateLayers(List<Layer> layers, boolean allVisible, boolean allHidden);
183
184 /**
185 * Get the panel that should be added to the menu
186 * @return The panel
187 */
188 JComponent getPanel();
189 }
190
191 private class VisibilityCheckbox extends JCheckBox implements LayerVisibilityMenuEntry {
192
193 VisibilityCheckbox() {
194 super(tr("Show layer"));
195
196 // Align all texts
197 Icon icon = UIManager.getIcon("CheckBox.icon");
198 int iconWidth = icon == null ? 20 : icon.getIconWidth();
199 setBorder(BorderFactory.createEmptyBorder(0, Math.max(24 + 5 - iconWidth, 0), 0, 0));
200 addChangeListener(e -> setVisibleFlag(isSelected()));
201 }
202
203 @Override
204 public void updateLayers(List<Layer> layers, boolean allVisible, boolean allHidden) {
205 setEnabled(!layers.isEmpty());
206 // TODO: Indicate tristate.
207 setSelected(allVisible && !allHidden);
208 }
209
210 @Override
211 public JComponent getPanel() {
212 return this;
213 }
214 }
215
216 /**
217 * This is a slider for a filter value.
218 * @author Michael Zangl
219 *
220 * @param <T> The layer type.
221 */
222 private abstract class AbstractFilterSlider<T extends Layer> extends JPanel implements LayerVisibilityMenuEntry {
223 private final double minValue;
224 private final double maxValue;
225 private final Class<T> layerClassFilter;
226
227 protected final JSlider slider = new JSlider(JSlider.HORIZONTAL);
228
229 /**
230 * Create a new filter slider.
231 * @param minValue The minimum value to map to the left side.
232 * @param maxValue The maximum value to map to the right side.
233 * @param layerClassFilter The type of layer influenced by this filter.
234 */
235 AbstractFilterSlider(double minValue, double maxValue, Class<T> layerClassFilter) {
236 super(new GridBagLayout());
237 this.minValue = minValue;
238 this.maxValue = maxValue;
239 this.layerClassFilter = layerClassFilter;
240
241 add(new JLabel(getIcon()), GBC.std().span(1, 2).insets(0, 0, 5, 0));
242 add(new JLabel(getLabel()), GBC.eol().insets(5, 0, 5, 0));
243 add(slider, GBC.eol());
244 addMouseWheelListener(this::mouseWheelMoved);
245
246 slider.setMaximum(SLIDER_STEPS);
247 int tick = convertFromRealValue(1);
248 slider.setMinorTickSpacing(tick);
249 slider.setMajorTickSpacing(tick);
250 slider.setPaintTicks(true);
251
252 slider.addChangeListener(e -> onStateChanged());
253
254 //final NumberFormat format = DecimalFormat.getInstance();
255 //setLabels(format.format(minValue), format.format((minValue + maxValue) / 2), format.format(maxValue));
256 }
257
258 protected void setLabels(String labelMinimum, String labelMiddle, String labelMaximum) {
259 final Dictionary<Integer, JLabel> labels = new Hashtable<>();
260 labels.put(slider.getMinimum(), new JLabel(labelMinimum));
261 labels.put((slider.getMaximum() + slider.getMinimum()) / 2, new JLabel(labelMiddle));
262 labels.put(slider.getMaximum(), new JLabel(labelMaximum));
263 slider.setLabelTable(labels);
264 slider.setPaintLabels(true);
265 }
266
267 /**
268 * Called whenever the state of the slider was changed.
269 * @see JSlider#getValueIsAdjusting()
270 * @see #getRealValue()
271 */
272 protected void onStateChanged() {
273 Collection<T> layers = filterLayers(model.getSelectedLayers());
274 for (T layer : layers) {
275 applyValueToLayer(layer);
276 }
277 }
278
279 protected void mouseWheelMoved(MouseWheelEvent e) {
280 e.consume();
281 if (!isEnabled()) {
282 // ignore mouse wheel in disabled state.
283 return;
284 }
285 double rotation = -1 * e.getPreciseWheelRotation();
286 double destinationValue = slider.getValue() + rotation * SLIDER_WHEEL_INCREMENT;
287 if (rotation < 0) {
288 destinationValue = Math.floor(destinationValue);
289 } else {
290 destinationValue = Math.ceil(destinationValue);
291 }
292 slider.setValue(Utils.clamp((int) destinationValue, slider.getMinimum(), slider.getMaximum()));
293 }
294
295 abstract void applyValueToLayer(T layer);
296
297 protected double getRealValue() {
298 return convertToRealValue(slider.getValue());
299 }
300
301 protected double convertToRealValue(int value) {
302 double s = (double) value / SLIDER_STEPS;
303 return s * maxValue + (1-s) * minValue;
304 }
305
306 protected void setRealValue(double value) {
307 slider.setValue(convertFromRealValue(value));
308 }
309
310 protected int convertFromRealValue(double value) {
311 int i = (int) ((value - minValue) / (maxValue - minValue) * SLIDER_STEPS + .5);
312 return Utils.clamp(i, slider.getMinimum(), slider.getMaximum());
313 }
314
315 public abstract ImageIcon getIcon();
316
317 public abstract String getLabel();
318
319 @Override
320 public void updateLayers(List<Layer> layers, boolean allVisible, boolean allHidden) {
321 Collection<? extends Layer> usedLayers = filterLayers(layers);
322 setVisible(!usedLayers.isEmpty());
323 if (usedLayers.stream().noneMatch(Layer::isVisible)) {
324 slider.setEnabled(false);
325 } else {
326 slider.setEnabled(true);
327 updateSliderWhileEnabled(usedLayers, allHidden);
328 }
329 }
330
331 protected Collection<T> filterLayers(List<Layer> layers) {
332 return Utils.filteredCollection(layers, layerClassFilter);
333 }
334
335 protected abstract void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden);
336
337 @Override
338 public JComponent getPanel() {
339 return this;
340 }
341 }
342
343 /**
344 * This slider allows you to change the opacity of a layer.
345 *
346 * @author Michael Zangl
347 * @see Layer#setOpacity(double)
348 */
349 class OpacitySlider extends AbstractFilterSlider<Layer> {
350 /**
351 * Create a new {@link OpacitySlider}.
352 */
353 OpacitySlider() {
354 super(0, 1, Layer.class);
355 setLabels("0%", "50%", "100%");
356 slider.setToolTipText(tr("Adjust opacity of the layer."));
357 }
358
359 @Override
360 protected void onStateChanged() {
361 if (getRealValue() <= 0.001 && !slider.getValueIsAdjusting()) {
362 setVisibleFlag(false);
363 } else {
364 super.onStateChanged();
365 }
366 }
367
368 @Override
369 protected void mouseWheelMoved(MouseWheelEvent e) {
370 if (!isEnabled() && !filterLayers(model.getSelectedLayers()).isEmpty() && e.getPreciseWheelRotation() < 0) {
371 // make layer visible and set the value.
372 // this allows users to use the mouse wheel to make the layer visible if it was hidden previously.
373 e.consume();
374 setVisibleFlag(true);
375 } else {
376 super.mouseWheelMoved(e);
377 }
378 }
379
380 @Override
381 protected void applyValueToLayer(Layer layer) {
382 layer.setOpacity(getRealValue());
383 }
384
385 @Override
386 protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
387 double opacity = 0;
388 for (Layer l : usedLayers) {
389 opacity += l.getOpacity();
390 }
391 opacity /= usedLayers.size();
392 if (opacity == 0) {
393 opacity = 1;
394 setVisibleFlag(true);
395 }
396 setRealValue(opacity);
397 }
398
399 @Override
400 public String getLabel() {
401 return tr("Opacity");
402 }
403
404 @Override
405 public ImageIcon getIcon() {
406 return ImageProvider.get(DIALOGS_LAYERLIST, "transparency");
407 }
408
409 @Override
410 public String toString() {
411 return "OpacitySlider [getRealValue()=" + getRealValue() + ']';
412 }
413 }
414
415 /**
416 * This slider allows you to change the gamma value of a layer.
417 *
418 * @author Michael Zangl
419 * @see ImageryFilterSettings#setGamma(double)
420 */
421 private class GammaFilterSlider extends AbstractFilterSlider<ImageryLayer> {
422
423 /**
424 * Create a new {@link GammaFilterSlider}
425 */
426 GammaFilterSlider() {
427 super(-1, 1, ImageryLayer.class);
428 setLabels("0", "1", "∞");
429 slider.setToolTipText(tr("Adjust gamma value of the layer."));
430 }
431
432 @Override
433 protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
434 double gamma = ((ImageryLayer) usedLayers.iterator().next()).getFilterSettings().getGamma();
435 setRealValue(mapGammaToInterval(gamma));
436 }
437
438 @Override
439 protected void applyValueToLayer(ImageryLayer layer) {
440 layer.getFilterSettings().setGamma(mapIntervalToGamma(getRealValue()));
441 }
442
443 @Override
444 public ImageIcon getIcon() {
445 return ImageProvider.get(DIALOGS_LAYERLIST, "gamma");
446 }
447
448 @Override
449 public String getLabel() {
450 return tr("Gamma");
451 }
452
453 /**
454 * Maps a number x from the range (-1,1) to a gamma value.
455 * Gamma value is in the range (0, infinity).
456 * Gamma values of 3 and 1/3 have opposite effects, so the mapping
457 * should be symmetric in that sense.
458 * @param x the slider value in the range (-1,1)
459 * @return the gamma value
460 */
461 private double mapIntervalToGamma(double x) {
462 // properties of the mapping:
463 // g(-1) = 0
464 // g(0) = 1
465 // g(1) = infinity
466 // g(-x) = 1 / g(x)
467 return (1 + x) / (1 - x);
468 }
469
470 private double mapGammaToInterval(double gamma) {
471 return (gamma - 1) / (gamma + 1);
472 }
473 }
474
475 /**
476 * This slider allows you to change the sharpness of a layer.
477 *
478 * @author Michael Zangl
479 * @see ImageryFilterSettings#setSharpenLevel(double)
480 */
481 private class SharpnessSlider extends AbstractFilterSlider<ImageryLayer> {
482
483 /**
484 * Creates a new {@link SharpnessSlider}
485 */
486 SharpnessSlider() {
487 super(0, MAX_SHARPNESS_FACTOR, ImageryLayer.class);
488 setLabels(trc("image sharpness", "blurred"), trc("image sharpness", "normal"), trc("image sharpness", "sharp"));
489 slider.setToolTipText(tr("Adjust sharpness/blur value of the layer."));
490 }
491
492 @Override
493 protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
494 setRealValue(((ImageryLayer) usedLayers.iterator().next()).getFilterSettings().getSharpenLevel());
495 }
496
497 @Override
498 protected void applyValueToLayer(ImageryLayer layer) {
499 layer.getFilterSettings().setSharpenLevel(getRealValue());
500 }
501
502 @Override
503 public ImageIcon getIcon() {
504 return ImageProvider.get(DIALOGS_LAYERLIST, "sharpness");
505 }
506
507 @Override
508 public String getLabel() {
509 return tr("Sharpness");
510 }
511 }
512
513 /**
514 * This slider allows you to change the colorfulness of a layer.
515 *
516 * @author Michael Zangl
517 * @see ImageryFilterSettings#setColorfulness(double)
518 */
519 private class ColorfulnessSlider extends AbstractFilterSlider<ImageryLayer> {
520
521 /**
522 * Create a new {@link ColorfulnessSlider}
523 */
524 ColorfulnessSlider() {
525 super(0, MAX_COLORFUL_FACTOR, ImageryLayer.class);
526 setLabels(trc("image colorfulness", "less"), trc("image colorfulness", "normal"), trc("image colorfulness", "more"));
527 slider.setToolTipText(tr("Adjust colorfulness of the layer."));
528 }
529
530 @Override
531 protected void updateSliderWhileEnabled(Collection<? extends Layer> usedLayers, boolean allHidden) {
532 setRealValue(((ImageryLayer) usedLayers.iterator().next()).getFilterSettings().getColorfulness());
533 }
534
535 @Override
536 protected void applyValueToLayer(ImageryLayer layer) {
537 layer.getFilterSettings().setColorfulness(getRealValue());
538 }
539
540 @Override
541 public ImageIcon getIcon() {
542 return ImageProvider.get(DIALOGS_LAYERLIST, "colorfulness");
543 }
544
545 @Override
546 public String getLabel() {
547 return tr("Colorfulness");
548 }
549 }
550
551 /**
552 * Allows to select the color for the GPX layer
553 * @author Michael Zangl
554 */
555 private static class ColorSelector extends JPanel implements LayerVisibilityMenuEntry {
556
557 private static final Border NORMAL_BORDER = BorderFactory.createEmptyBorder(2, 2, 2, 2);
558 private static final Border SELECTED_BORDER = BorderFactory.createLineBorder(Color.BLACK, 2);
559
560 // TODO: Nicer color palette
561 private static final Color[] COLORS = new Color[] {
562 Color.RED,
563 Color.ORANGE,
564 Color.YELLOW,
565 Color.GREEN,
566 Color.BLUE,
567 Color.CYAN,
568 Color.GRAY,
569 };
570 private final Supplier<List<Layer>> layerSupplier;
571 private final HashMap<Color, JPanel> panels = new HashMap<>();
572
573 ColorSelector(Supplier<List<Layer>> layerSupplier) {
574 super(new GridBagLayout());
575 this.layerSupplier = layerSupplier;
576 add(new JLabel(tr("Color")), GBC.eol().insets(24 + 10, 0, 0, 0));
577 for (Color color : COLORS) {
578 addPanelForColor(color);
579 }
580 }
581
582 private void addPanelForColor(Color color) {
583 JPanel innerPanel = new JPanel();
584 innerPanel.setBackground(color);
585
586 JPanel colorPanel = new JPanel(new BorderLayout());
587 colorPanel.setBorder(NORMAL_BORDER);
588 colorPanel.add(innerPanel);
589 colorPanel.setMinimumSize(new Dimension(20, 20));
590 colorPanel.addMouseListener(new MouseAdapter() {
591 @Override
592 public void mouseClicked(MouseEvent e) {
593 List<Layer> layers = layerSupplier.get();
594 for (Layer l : layers) {
595 if (l instanceof GpxLayer) {
596 l.getColorProperty().put(color);
597 }
598 }
599 highlightColor(color);
600 }
601 });
602 add(colorPanel, GBC.std().weight(1, 1).fill().insets(5));
603 panels.put(color, colorPanel);
604
605 List<Color> colors = layerSupplier.get().stream().map(l -> l.getColorProperty().get()).distinct().collect(Collectors.toList());
606 if (colors.size() == 1) {
607 highlightColor(colors.get(0));
608 }
609 }
610
611 @Override
612 public void updateLayers(List<Layer> layers, boolean allVisible, boolean allHidden) {
613 List<Color> colors = layers.stream().filter(l -> l instanceof GpxLayer)
614 .map(l -> ((GpxLayer) l).getColorProperty().get())
615 .distinct()
616 .collect(Collectors.toList());
617 if (colors.size() == 1) {
618 setVisible(true);
619 highlightColor(colors.get(0));
620 } else if (colors.size() > 1) {
621 setVisible(true);
622 highlightColor(null);
623 } else {
624 // no GPX layer
625 setVisible(false);
626 }
627 }
628
629 private void highlightColor(Color color) {
630 panels.values().forEach(panel -> panel.setBorder(NORMAL_BORDER));
631 if (color != null) {
632 JPanel selected = panels.get(color);
633 if (selected != null) {
634 selected.setBorder(SELECTED_BORDER);
635 }
636 }
637 repaint();
638 }
639
640 @Override
641 public JComponent getPanel() {
642 return this;
643 }
644 }
645}
Note: See TracBrowser for help on using the repository browser.