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

Last change on this file since 12402 was 12402, checked in by Don-vip, 21 months ago

checkstyle

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