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

Last change on this file since 12395 was 12395, checked in by michael2402, 22 months ago

Add GPS colors to layer visibility panel

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