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, 7 years 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.