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

Last change on this file since 17760 was 17760, checked in by simon04, 3 years ago

Checkstyle

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