/* * Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de) * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) * any later version. * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, see http://www.gnu.org/licenses/ */ package org.esa.snap.ui; import com.bc.ceres.binding.Property; import com.bc.ceres.binding.PropertyContainer; import com.bc.ceres.binding.ValueRange; import com.bc.ceres.swing.binding.BindingContext; import com.jidesoft.popup.JidePopup; import com.jidesoft.swing.JidePopupMenu; import org.esa.snap.core.datamodel.ColorPaletteDef; import org.esa.snap.core.datamodel.ImageInfo; import org.esa.snap.core.util.math.Histogram; import org.esa.snap.core.util.math.MathUtils; import org.esa.snap.core.util.math.Range; import org.esa.snap.ui.color.ColorChooserPanel; import javax.swing.JComponent; import javax.swing.JFormattedTextField; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.text.NumberFormatter; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.text.DecimalFormat; /** * Unstable interface. Do not use. * * @author Norman Fomferra * @version $Revision$ $Date$ * @since BEAM 4.5.1 */ public abstract class ImageInfoEditor extends JPanel { public static final String PROPERTY_NAME_MODEL = "model"; public static final String NO_DISPLAY_INFO_TEXT = "No information available."; public static final String FONT_NAME = "Verdana"; public static final int FONT_SIZE = 9; public static final int INVALID_INDEX = -1; public static final int PALETTE_HEIGHT = 16; public static final int SLIDER_WIDTH = 12; public static final int SLIDER_HEIGHT = 10; public static final int SLIDER_VALUES_AREA_HEIGHT = 50; public static final int HOR_BORDER_SIZE = 10; public static final int VER_BORDER_SIZE = 4; public static final int PREF_HISTO_WIDTH = 256; //196; public static final int PREF_HISTO_HEIGHT = 196; //128; public static final Dimension PREF_COMPONENT_SIZE = new Dimension(PREF_HISTO_WIDTH + 2 * HOR_BORDER_SIZE, PREF_HISTO_HEIGHT + PALETTE_HEIGHT + SLIDER_HEIGHT / 2 + 2 * HOR_BORDER_SIZE + FONT_SIZE); public static final BasicStroke STROKE_1 = new BasicStroke(1.0f); public static final BasicStroke STROKE_2 = new BasicStroke(2.0f); public static final BasicStroke DASHED_STROKE = new BasicStroke(0.75F, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 1.0F, new float[]{5.0F}, 0.0F); private ImageInfoEditorModel model; private Font labelFont; private final Shape sliderShape; private int sliderTextBaseLineY; private final Rectangle sliderBaseLineRect; private final Rectangle paletteRect; private final Rectangle histoRect; private double roundFactor; private final InternalMouseListener internalMouseListener; private double[] factors; private Color[] palette; private ModelCL modelCL; private JidePopup popup; private BufferedImage paletteBackgound; public ImageInfoEditor() { labelFont = createLabelFont(); sliderShape = createSliderShape(); sliderBaseLineRect = new Rectangle(); paletteRect = new Rectangle(); histoRect = new Rectangle(); internalMouseListener = new InternalMouseListener(); modelCL = new ModelCL(); addChangeListener(new RepaintCL()); } public final ImageInfoEditorModel getModel() { return model; } public final void setModel(final ImageInfoEditorModel model) { final ImageInfoEditorModel oldModel = this.model; if (oldModel != model) { this.model = model; deinstallMouseListener(); if (oldModel != null) { oldModel.removeChangeListener(modelCL); } if (this.model != null) { roundFactor = MathUtils.computeRoundFactor(this.model.getMinSample(), this.model.getMaxSample(), 2); installMouseListener(); model.addChangeListener(modelCL); } firePropertyChange(PROPERTY_NAME_MODEL, oldModel, this.model); fireStateChanged(); } if (isShowing()) { repaint(); } } public void addChangeListener(ChangeListener l) { listenerList.add(ChangeListener.class, l); } public void removeChangeListener(ChangeListener l) { listenerList.remove(ChangeListener.class, l); } public ChangeListener[] getChangeListeners() { return listenerList.getListeners(ChangeListener.class); } protected void fireStateChanged() { final ChangeEvent event = new ChangeEvent(this); Object[] listeners = listenerList.getListenerList(); for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == ChangeListener.class) { ((ChangeListener) listeners[i + 1]).stateChanged(event); } } } public void compute95Percent() { final Histogram histogram = new Histogram(getModel().getHistogramBins(), scaleInverse(getModel().getMinSample()), scaleInverse(getModel().getMaxSample())); final Range autoStretchRange = histogram.findRangeFor95Percent(); computeFactors(); setFirstSliderSample(scale(autoStretchRange.getMin())); setLastSliderSample(scale(autoStretchRange.getMax())); partitionSliders(false); computeZoomInToSliderLimits(); } public void compute100Percent() { computeFactors(); setFirstSliderSample(getModel().getMinSample()); setLastSliderSample(getModel().getMaxSample()); partitionSliders(false); computeZoomInToSliderLimits(); } public void distributeSlidersEvenly() { final double pos1 = scaleInverse(getFirstSliderSample()); final double pos2 = scaleInverse(getLastSliderSample()); final double delta = pos2 - pos1; final double evenSpace = delta / (getSliderCount() - 2 + 1); for (int i = 0; i < getSliderCount(); i++) { final double value = scale(pos1 + evenSpace * i); setSliderSample(i, value, false); } } private void partitionSliders(boolean adjusting) { final double pos1 = scaleInverse(getFirstSliderSample()); final double pos2 = scaleInverse(getLastSliderSample()); final double delta = pos2 - pos1; for (int i = 0; i < (getSliderCount() - 1); i++) { final double value = scale(pos1 + factors[i] * delta); setSliderSample(i, value, adjusting); } } private void computeFactors() { factors = new double[getSliderCount()]; final double firstPS = scaleInverse(getFirstSliderSample()); final double lastPS = scaleInverse(getLastSliderSample()); double dsn = lastPS - firstPS; if (dsn == 0 || Double.isNaN(dsn)) { dsn = Double.MIN_VALUE; } for (int i = 0; i < getSliderCount(); i++) { final double sample = scaleInverse(getSliderSample(i)); final double dsi = sample - firstPS; factors[i] = dsi / dsn; } } @Override public Dimension getPreferredSize() { return PREF_COMPONENT_SIZE; } @Override public void setBounds(int x, int y, int width, int heigth) { super.setBounds(x, y, width, heigth); computeSizeAttributes(); } public void computeZoomInToSliderLimits() { final double firstSliderValue = scaleInverse(getFirstSliderSample()); final double lastSliderValue = scaleInverse(getLastSliderSample()); final double percentOffset = 0.0; final double minViewSample = scale(firstSliderValue - percentOffset); final double maxViewSample = scale(lastSliderValue + percentOffset); getModel().setMinHistogramViewSample(minViewSample); getModel().setMaxHistogramViewSample(maxViewSample); repaint(); } public void computeZoomOutToFullHistogramm() { getModel().setMinHistogramViewSample(getMinSample()); getModel().setMaxHistogramViewSample(getMaxSample()); repaint(); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (getModel() == null || getWidth() == 0 || getHeight() == 0) { return; } Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setFont(labelFont); computeSizeAttributes(); if (isValidModel()) { drawPalette(g2d); drawSliders(g2d); drawHistogramPane(g2d); } else { FontMetrics fontMetrics = g2d.getFontMetrics(); drawMissingBasicDisplayInfoMessage(g2d, fontMetrics); } } private boolean isHistogramAvailable() { return model.isHistogramAvailable(); } private boolean isValidModel() { return model != null && model.getMinSample() <= model.getMaxSample() && model.getSampleScaling() != null && model.getSampleStx() != null; } public void computeZoomOutVertical() { getModel().setHistogramViewGain(getModel().getHistogramViewGain() * (1.0 / 1.4)); repaint(); } public void computeZoomInVertical() { getModel().setHistogramViewGain(getModel().getHistogramViewGain() * 1.4); repaint(); } private void drawMissingBasicDisplayInfoMessage(Graphics2D g2d, FontMetrics fontMetrics) { int totWidth = getWidth(); int totHeight = getHeight(); g2d.drawString(NO_DISPLAY_INFO_TEXT, (totWidth - fontMetrics.stringWidth(NO_DISPLAY_INFO_TEXT)) / 2, (totHeight + fontMetrics.getHeight()) / 2); } private void drawPalette(Graphics2D g2d) { if (paletteBackgound == null || paletteBackgound.getWidth() != paletteRect.width || paletteBackgound.getHeight() != paletteRect.height) { this.paletteBackgound = createAlphaBackground(paletteRect.width, paletteRect.height); } g2d.drawImage(paletteBackgound, paletteRect.x, paletteRect.y, null); long paletteX1 = paletteRect.x + Math.round(getRelativeSliderPos(getFirstSliderSample())); long paletteX2 = paletteRect.x + Math.round(getRelativeSliderPos(getLastSliderSample())); g2d.setStroke(STROKE_1); Color[] colorPalette = getColorPalette(); if (colorPalette != null) { for (int x = paletteRect.x; x < paletteRect.x + paletteRect.width; x++) { long divisor = paletteX2 - paletteX1; int palIndex; if (divisor == 0) { palIndex = x < paletteX1 ? 0 : colorPalette.length - 1; } else { palIndex = (int) ((colorPalette.length * (x - paletteX1)) / divisor); } if (palIndex < 0) { palIndex = 0; } if (palIndex > colorPalette.length - 1) { palIndex = colorPalette.length - 1; } g2d.setColor(colorPalette[palIndex]); g2d.drawLine(x, paletteRect.y, x, paletteRect.y + paletteRect.height); } } g2d.setStroke(STROKE_1); g2d.setColor(Color.darkGray); g2d.draw(paletteRect); } private static BufferedImage createAlphaBackground(int width, int height) { BufferedImage background = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); DataBufferInt dataBuffer = (DataBufferInt) background.getRaster().getDataBuffer(); int[] data = dataBuffer.getData(); int gray = Color.LIGHT_GRAY.getRGB(); int white = Color.WHITE.getRGB(); for (int i = 0; i < data.length; i++) { int x = i % width; int y = i / width; data[i] = ((x / 4) % 2 == (y / 4) % 2) ? gray : white; } return background; } private Color[] getColorPalette() { if (palette == null) { palette = getModel().createColorPalette(); } return palette; } private void drawSliders(Graphics2D g2d) { g2d.translate(sliderBaseLineRect.x, sliderBaseLineRect.y); g2d.setStroke(STROKE_1); for (int i = 0; i < getSliderCount(); i++) { if (isSliderVisible(i)) { double sliderPos = getRelativeSliderPos(getSliderSample(i)); g2d.translate(sliderPos, 0.0); final Color sliderColor = getSliderColor(i); g2d.setPaint(sliderColor); g2d.fill(sliderShape); int gray = (sliderColor.getRed() + sliderColor.getGreen() + sliderColor.getBlue()) / 3; g2d.setColor(gray < 128 ? Color.white : Color.black); g2d.draw(sliderShape); String text = getFormattedValue(getSliderSample(i)); g2d.setColor(Color.black); // save the old transformation final AffineTransform oldTransform = g2d.getTransform(); g2d.transform(AffineTransform.getRotateInstance(Math.PI / 2)); g2d.drawString(text, 3 + 0.5f * SLIDER_HEIGHT, 0.35f * FONT_SIZE); // restore the old transformation g2d.setTransform(oldTransform); g2d.translate(-sliderPos, 0.0); } } g2d.translate(-sliderBaseLineRect.x, -sliderBaseLineRect.y); } private String getFormattedValue(double value) { if (value < 0.1 && value > -0.1 && value != 0.0) { return new DecimalFormat("0.##E0").format(value); } return new DecimalFormat("#0.0#").format(round(value)); } private void drawHistogramPane(Graphics2D g2d) { Shape oldClip = g2d.getClip(); g2d.setClip(histoRect.x - 1, histoRect.y - 1, histoRect.width + 2, histoRect.height + 2); drawHistogram(g2d); drawGradationCurve(g2d); drawHistogramBorder(g2d); g2d.setClip(oldClip); } private void drawHistogramBorder(Graphics2D g2d) { g2d.setStroke(STROKE_1); g2d.setColor(Color.darkGray); g2d.draw(histoRect); } private void drawGradationCurve(Graphics2D g2d) { g2d.setColor(Color.white); g2d.setStroke(STROKE_2); int x1 = histoRect.x; int y1 = histoRect.y + histoRect.height - 1; int x2 = (int) getAbsoluteSliderPos(getFirstSliderSample()); int y2 = y1; g2d.drawLine(x1, y1, x2, y2); x1 = x2; y1 = y2; x2 = (int) getAbsoluteSliderPos(getLastSliderSample()); y2 = histoRect.y + 1; final byte[] gammaCurve = getModel().getGammaCurve(); if (gammaCurve != null) { int xx = x1; int yy = y1; for (int x = x1 + 1; x <= x2; x++) { int i = MathUtils.roundAndCrop((255.9f * (x - x1)) / (x2 - x1), 0, 255); int y = y1 + ((y2 - y1) * (gammaCurve[i] & 0xff)) / 256; g2d.drawLine(xx, yy, x, y); //System.out.println("x=" + x + ", y=" + y); xx = x; yy = y; } } else { g2d.drawLine(x1, y1, x2, y2); } x1 = x2; y1 = y2; x2 = histoRect.x + histoRect.width; y2 = histoRect.y + 1; g2d.drawLine(x1, y1, x2, y2); // Vertical lines g2d.setStroke(DASHED_STROKE); for (int i = 0; i < getSliderCount(); i++) { x1 = (int) getAbsoluteSliderPos(getSliderSample(i)); y1 = histoRect.y + histoRect.height - 1; x2 = x1; y2 = histoRect.y + 1; g2d.drawLine(x1, y1, x2, y2); } } private void drawHistogram(Graphics2D g2d) { if (model.isHistogramAvailable()) { final Paint oldPaint = g2d.getPaint(); g2d.setPaint(Color.DARK_GRAY); final int[] histogramBins = model.getHistogramBins(); final double maxHistogramCounts = getMaxVisibleHistogramCounts(histogramBins, 1.0 / 16.0); final double viewBinCount = getHistogramViewBinCount(); if (viewBinCount > 0.0 && maxHistogramCounts > 0.0) { g2d.setStroke(new BasicStroke(1.0f)); final double minViewBinIndex = getMinHistogramViewBinIndex(); final double binsPerPixel = viewBinCount / histoRect.width; final double maxBarHeight = 0.9 * histoRect.height; final double gain = model.getHistogramViewGain(); final double countsScale = (gain * maxBarHeight) / maxHistogramCounts; final Rectangle2D.Double r = new Rectangle2D.Double(); for (int i = 0; i < histoRect.width; i++) { final int binIndex = (int) Math.floor(minViewBinIndex + i * binsPerPixel); double binHeight = 0.0; if (binIndex >= 0 && binIndex < histogramBins.length) { final double counts = histogramBins[binIndex]; binHeight = countsScale * counts; } if (binHeight >= histoRect.height) { // must crop here because on highly centered histograms this value is FAR beyond the rectangle // and then triggers an exception when trying to draw it. binHeight = histoRect.height - 1; } r.setRect(histoRect.x + i, histoRect.y + histoRect.height - 1 - binHeight, 1.0, binHeight); g2d.fill(r); } } g2d.setPaint(oldPaint); } } private static double getMaxVisibleHistogramCounts(final int[] histogramBins, double ratio) { double totalHistogramCounts = 0.0; for (int histogramBin : histogramBins) { totalHistogramCounts += histogramBin; } final double limitHistogramCounts = totalHistogramCounts * ratio; double maxHistogramCounts = 0.0; for (int histogramBin : histogramBins) { if (histogramBin < limitHistogramCounts) { maxHistogramCounts = Math.max(maxHistogramCounts, histogramBin); } } return maxHistogramCounts; } private void installMouseListener() { addMouseListener(internalMouseListener); addMouseMotionListener(internalMouseListener); } private void deinstallMouseListener() { removeMouseListener(internalMouseListener); removeMouseMotionListener(internalMouseListener); } private double getMaxSample() { return getModel().getMaxSample(); } private double getMinSample() { return getModel().getMinSample(); } private int getSliderCount() { return getModel().getSliderCount(); } private double getMinSliderSample(int sliderIndex) { if (sliderIndex == 0) { return getMinSample(); } else { return getSliderSample(sliderIndex - 1); } } private double getMaxSliderSample(int sliderIndex) { if (sliderIndex == getSliderCount() - 1) { return getMaxSample(); } else { return getSliderSample(sliderIndex + 1); } } private double getFirstSliderSample() { return getSliderSample(0); } private void setFirstSliderSample(double v) { setSliderSample(0, v); } private double getLastSliderSample() { return getSliderSample(getModel().getSliderCount() - 1); } private void setLastSliderSample(double v) { setSliderSample(getModel().getSliderCount() - 1, v); } private double getSliderSample(int index) { return getModel().getSliderSample(index); } private void setSliderSample(int index, double v) { getModel().setSliderSample(index, v); } protected abstract void applyChanges(); private void setSliderSample(int index, double newValue, boolean adjusting) { if (adjusting) { double minValue = Double.NEGATIVE_INFINITY; if (index > 0 && index < getSliderCount() - 1) { minValue = getSliderSample(index - 1); } if (newValue < minValue) { newValue = minValue; } } setSliderSample(index, newValue); } private Color getSliderColor(int index) { ImageInfo.UncertaintyVisualisationMode uvMode = getModel().getImageInfo().getUncertaintyVisualisationMode(); if (uvMode != null) { if (uvMode == ImageInfo.UncertaintyVisualisationMode.Transparency_Blending) { return Color.WHITE; } else if (uvMode == ImageInfo.UncertaintyVisualisationMode.Monochromatic_Blending) { return getModel().getSliderColor(getSliderCount() - 1); } } return getModel().getSliderColor(index); } private void setSliderColor(int index, Color c) { getModel().setSliderColor(index, c); applyChanges(); } private static Font createLabelFont() { return new Font(FONT_NAME, Font.PLAIN, FONT_SIZE); } public static Shape createSliderShape() { GeneralPath path = new GeneralPath(); path.moveTo(0.0F, -0.5F * SLIDER_HEIGHT); path.lineTo(+0.5F * SLIDER_WIDTH, 0.5F * SLIDER_HEIGHT); path.lineTo(-0.5F * SLIDER_WIDTH, 0.5F * SLIDER_HEIGHT); path.closePath(); return path; } private double computeSliderValueForX(int sliderIndex, int x) { final double minVS = scaleInverse(getModel().getMinHistogramViewSample()); final double maxVS = scaleInverse(getModel().getMaxHistogramViewSample()); final double value = scale(minVS + (x - sliderBaseLineRect.x) * (maxVS - minVS) / sliderBaseLineRect.width); if (isFirstSliderIndex(sliderIndex)) { return Math.min(value, getLastSliderSample()); } if (isLastSliderIndex(sliderIndex)) { return Math.max(value, getFirstSliderSample()); } return computeAdjustedSliderValue(sliderIndex, value); } private double computeAdjustedSliderValue(int sliderIndex, double value) { double valueD = value; double minSliderValue = getMinSliderSample(sliderIndex); double maxSliderValue = getMaxSliderSample(sliderIndex); if (valueD < minSliderValue) { valueD = minSliderValue; } if (valueD > maxSliderValue) { valueD = maxSliderValue; } return valueD; } private boolean isFirstSliderIndex(int sliderIndex) { return sliderIndex == 0; } private boolean isLastSliderIndex(int sliderIndex) { return getSliderCount() - 1 == sliderIndex; } private double round(double value) { return MathUtils.round(value, roundFactor); } private double getAbsoluteSliderPos(double sample) { return sliderBaseLineRect.x + getRelativeSliderPos(sample); } private double getRelativeSliderPos(double sample) { return getNormalizedHistogramViewSampleValue(sample) * sliderBaseLineRect.width; } private void computeSizeAttributes() { int totWidth = getWidth(); int totHeight = getHeight(); int imageWidth = totWidth - 2 * HOR_BORDER_SIZE; sliderTextBaseLineY = totHeight - VER_BORDER_SIZE - SLIDER_VALUES_AREA_HEIGHT; sliderBaseLineRect.x = HOR_BORDER_SIZE; sliderBaseLineRect.y = sliderTextBaseLineY - SLIDER_HEIGHT / 2; sliderBaseLineRect.width = imageWidth; sliderBaseLineRect.height = 1; paletteRect.x = HOR_BORDER_SIZE; paletteRect.y = sliderBaseLineRect.y - PALETTE_HEIGHT; paletteRect.width = imageWidth; paletteRect.height = PALETTE_HEIGHT; histoRect.x = HOR_BORDER_SIZE; histoRect.y = VER_BORDER_SIZE; histoRect.width = imageWidth; histoRect.height = paletteRect.y - histoRect.y - 3; } private double getHistogramViewBinCount() { return Math.min(getDisplayableBinCount(), model.getHistogramBins().length); } private double getDisplayableBinCount() { final double max = Math.min(getMaxSample(), getModel().getMaxHistogramViewSample()); final double min = Math.max(getMinSample(), getModel().getMinHistogramViewSample()); return getBinCountInRange(min, max); } private double getBinCountInRange(double minSample, double maxSample) { if (!isHistogramAvailable()) { return -1.0; } final double minHistogramSample = model.getMinSample(); final double maxHistogramSample = model.getMaxSample(); if (minSample >= maxHistogramSample || maxSample <= minHistogramSample) { return 0.0; } minSample = Math.max(minSample, minHistogramSample); maxSample = Math.min(maxSample, maxHistogramSample); final double a = scaleInverse(maxSample) - scaleInverse(minSample); final double b = scaleInverse(maxHistogramSample) - scaleInverse(minHistogramSample); return (a / b) * model.getHistogramBins().length; } private double getMinHistogramViewBinIndex() { if (!isHistogramAvailable()) { return -1.0; } final double minHistogramSample = model.getMinSample(); final double minHistogramViewSample = model.getMinHistogramViewSample(); if (minHistogramSample != minHistogramViewSample) { final double a = scaleInverse(minHistogramViewSample) - scaleInverse(minHistogramSample); final double b = scaleInverse(model.getMaxSample()) - scaleInverse(minHistogramSample); return (a / b) * model.getHistogramBins().length; } return 0.0; } private double getNormalizedHistogramViewSampleValue(double sample) { final double minVisibleSample = scaleInverse(getModel().getMinHistogramViewSample()); final double maxVisibleSample = scaleInverse(getModel().getMaxHistogramViewSample()); sample = scaleInverse(sample); double delta = maxVisibleSample - minVisibleSample; if (delta == 0 || Double.isNaN(delta)) { delta = 1; } return (sample - minVisibleSample) / delta; } private void editSliderColor(MouseEvent evt, final int sliderIndex) { final Color selectedColor = getSliderColor(sliderIndex); final ColorChooserPanel panel = new ColorChooserPanel(selectedColor); panel.addPropertyChangeListener(ColorChooserPanel.SELECTED_COLOR_PROPERTY, evt1 -> { setSliderColor(sliderIndex, panel.getSelectedColor()); hidePopup(); }); showPopup(evt, panel); } private boolean isSliderVisible(int sliderIndex) { if (sliderIndex == 0 || sliderIndex == getSliderCount() - 1) { return true; } ImageInfo.UncertaintyVisualisationMode uvMode = getModel().getImageInfo().getUncertaintyVisualisationMode(); return uvMode == null || uvMode == ImageInfo.UncertaintyVisualisationMode.None || uvMode == ImageInfo.UncertaintyVisualisationMode.Polychromatic_Blending || uvMode == ImageInfo.UncertaintyVisualisationMode.Polychromatic_Overlay; } private boolean isSliderEditable(int sliderIndex) { ImageInfo.UncertaintyVisualisationMode uvMode = getModel().getImageInfo().getUncertaintyVisualisationMode(); return uvMode == null || uvMode == ImageInfo.UncertaintyVisualisationMode.None || uvMode == ImageInfo.UncertaintyVisualisationMode.Polychromatic_Blending || uvMode == ImageInfo.UncertaintyVisualisationMode.Polychromatic_Overlay || (uvMode == ImageInfo.UncertaintyVisualisationMode.Monochromatic_Blending && isLastSliderIndex(sliderIndex)); } private boolean canChangeSliderCount() { ImageInfo.UncertaintyVisualisationMode uvMode = getModel().getImageInfo().getUncertaintyVisualisationMode(); return uvMode == null || uvMode == ImageInfo.UncertaintyVisualisationMode.None || uvMode == ImageInfo.UncertaintyVisualisationMode.Polychromatic_Blending || uvMode == ImageInfo.UncertaintyVisualisationMode.Polychromatic_Overlay; } private void editSliderSample(MouseEvent evt, final int sliderIndex) { final PropertyContainer vc = new PropertyContainer(); vc.addProperty(Property.create("sample", getSliderSample(sliderIndex))); vc.getDescriptor("sample").setDisplayName("sample"); vc.getDescriptor("sample").setUnit(getModel().getParameterUnit()); final ValueRange valueRange; if (sliderIndex == 0) { valueRange = new ValueRange(Double.NEGATIVE_INFINITY, round(getMaxSliderSample(sliderIndex))); } else if (sliderIndex == getSliderCount() - 1) { valueRange = new ValueRange(round(getMinSliderSample(sliderIndex)), Double.POSITIVE_INFINITY); } else { valueRange = new ValueRange(round(getMinSliderSample(sliderIndex)), round(getMaxSliderSample(sliderIndex))); } vc.getDescriptor("sample").setValueRange(valueRange); final BindingContext ctx = new BindingContext(vc); final NumberFormatter formatter = new NumberFormatter(new DecimalFormat("#0.0#")); formatter.setValueClass(Double.class); // to ensure that double values are returned final JFormattedTextField field = new JFormattedTextField(formatter); field.setColumns(11); field.setHorizontalAlignment(JFormattedTextField.RIGHT); ctx.bind("sample", field); showPopup(evt, field); ctx.addPropertyChangeListener("sample", pce -> { hidePopup(); setSliderSample(sliderIndex, (Double) ctx.getBinding("sample").getPropertyValue()); computeZoomInToSliderLimits(); applyChanges(); }); } private void showPopup(MouseEvent evt, JComponent component) { hidePopup(); popup = new JidePopup(); popup.setOwner(this); popup.setDefaultFocusComponent(component); popup.getContentPane().add(component); popup.setAttachable(true); popup.setMovable(false); popup.showPopup(evt.getXOnScreen(), evt.getYOnScreen()); } private void hidePopup() { if (popup != null && popup.isVisible()) { popup.hidePopupImmediately(); popup = null; } } private class InternalMouseListener implements MouseListener, MouseMotionListener { private int draggedSliderIndex; private boolean dragging; private InternalMouseListener() { draggedSliderIndex = INVALID_INDEX; factors = null; dragging = false; } public boolean isDragging() { return dragging; } public void setDragging(boolean dragging) { this.dragging = dragging; } @Override public void mousePressed(MouseEvent mouseEvent) { hidePopup(); resetState(); // on linux: popup is triggered on mousePressed // on windows: popup is triggered on mouseReleased if (!maybeShowSliderActions(mouseEvent)) { setDraggedSliderIndex(getNearestSliderIndex(mouseEvent.getX(), mouseEvent.getY())); if (isFirstSliderDragged() || isLastSliderDragged()) { computeFactors(); } } } @Override public void mouseReleased(MouseEvent evt) { if (isDragging()) { doDragSlider(evt, false); setDragging(false); setDraggedSliderIndex(INVALID_INDEX); applyChanges(); } else if (!maybeShowSliderActions(evt) && SwingUtilities.isLeftMouseButton(evt)) { int mode = 0; int sliderIndex = getSelectedSliderIndex(evt); if (sliderIndex != INVALID_INDEX && getModel().isColorEditable()) { mode = 1; } if (mode == 0) { if (sliderIndex == INVALID_INDEX) { sliderIndex = getSelectedSliderTextIndex(evt); } if (sliderIndex != INVALID_INDEX) { mode = 2; } } if (mode == 1) { editSliderColor(evt, sliderIndex); } else if (mode == 2) { editSliderSample(evt, sliderIndex); } } } @Override public void mouseClicked(MouseEvent evt) { maybeShowSliderActions(evt); } @Override public void mouseEntered(MouseEvent mouseEvent) { resetState(); } @Override public void mouseExited(MouseEvent mouseEvent) { resetState(); } @Override public void mouseDragged(MouseEvent mouseEvent) { setDragging(true); doDragSlider(mouseEvent, true); } private void doDragSlider(MouseEvent mouseEvent, boolean adjusting) { if (getDraggedSliderIndex() != INVALID_INDEX) { int x = mouseEvent.getX(); x = Math.max(x, sliderBaseLineRect.x); x = Math.min(x, sliderBaseLineRect.x + sliderBaseLineRect.width); final double newSample = computeSliderValueForX(getDraggedSliderIndex(), x); setSliderSample(getDraggedSliderIndex(), newSample, adjusting); if (isFirstSliderDragged() || isLastSliderDragged()) { partitionSliders(adjusting); } } } @Override public void mouseMoved(MouseEvent mouseEvent) { if (isDragging()) { mouseDragged(mouseEvent); } } private boolean maybeShowSliderActions(MouseEvent mouseEvent) { if (getModel().isColorEditable() && mouseEvent.isPopupTrigger()) { final int sliderIndex = getSelectedSliderIndex(mouseEvent); showSliderActions(mouseEvent, sliderIndex); return true; } return false; } private void setDraggedSliderIndex(final int draggedSliderIndex) { if (this.draggedSliderIndex != draggedSliderIndex) { this.draggedSliderIndex = draggedSliderIndex; } } public int getDraggedSliderIndex() { return draggedSliderIndex; } private void showSliderActions(MouseEvent evt, final int sliderIndex) { final JPopupMenu menu = new JidePopupMenu(); boolean showPopupMenu = false; JMenuItem menuItem; if (canChangeSliderCount()) { menuItem = createMenuItemAddNewSlider(sliderIndex, evt); if (menuItem != null) { menu.add(menuItem); showPopupMenu = true; } if (getSliderCount() > 2 && sliderIndex != INVALID_INDEX) { menuItem = createMenuItemDeleteSlider(sliderIndex); menu.add(menuItem); showPopupMenu = true; } } if (getSliderCount() > 2 && sliderIndex > 0 && sliderIndex < getSliderCount() - 1 && isSliderEditable(sliderIndex)) { menuItem = createMenuItemCenterSampleValue(sliderIndex); menu.add(menuItem); menuItem = createMenuItemCenterColorValue(sliderIndex); menu.add(menuItem); showPopupMenu = true; } if (showPopupMenu) { menu.show(evt.getComponent(), evt.getX(), evt.getY()); } } private JMenuItem createMenuItemCenterColorValue(final int sliderIndex) { JMenuItem menuItem = new JMenuItem(); menuItem.setText("Center Slider Colour"); /* I18N */ menuItem.setMnemonic('c'); menuItem.addActionListener(actionEvent -> { final Color newColor = ColorPaletteDef.getCenterColor(getSliderColor(sliderIndex - 1), getSliderColor(sliderIndex + 1)); setSliderColor(sliderIndex, newColor); hidePopup(); applyChanges(); }); return menuItem; } private JMenuItem createMenuItemCenterSampleValue(final int sliderIndex) { JMenuItem menuItem = new JMenuItem(); menuItem.setText("Center Slider Position"); /* I18N */ menuItem.setMnemonic('s'); menuItem.addActionListener(actionEvent -> { final double center = scale(0.5 * (scaleInverse(getSliderSample(sliderIndex - 1)) + scaleInverse( getSliderSample(sliderIndex + 1)))); setSliderSample(sliderIndex, center, false); hidePopup(); applyChanges(); }); return menuItem; } private JMenuItem createMenuItemDeleteSlider(final int removeIndex) { JMenuItem menuItem = new JMenuItem("Remove Slider"); menuItem.setMnemonic('D'); menuItem.addActionListener(e -> { getModel().removeSlider(removeIndex); hidePopup(); applyChanges(); }); return menuItem; } private JMenuItem createMenuItemAddNewSlider(int insertIndex, final MouseEvent evt) { if (insertIndex == getModel().getSliderCount() - 1) { return null; } if (insertIndex == INVALID_INDEX && isClickOutsideExistingSliders(evt.getX())) { return null; } if (insertIndex == INVALID_INDEX && !isVerticalInColorBarArea(evt.getY())) { return null; } if (insertIndex == INVALID_INDEX) { insertIndex = getNearestLeftSliderIndex(evt.getX()); } if (insertIndex == INVALID_INDEX) { return null; } final int index = insertIndex; JMenuItem menuItem = new JMenuItem("Add new Slider"); menuItem.setMnemonic('A'); menuItem.addActionListener(e -> { assert getModel() != null : "getModel() != null"; if (index < getModel().getSliderCount() - 1) { getModel().createSliderAfter(index); } hidePopup(); applyChanges(); }); return menuItem; } private boolean isClickOutsideExistingSliders(int x) { return x < getAbsoluteSliderPos(getFirstSliderSample()) || x > getAbsoluteSliderPos(getLastSliderSample()); } private boolean isFirstSliderDragged() { return getDraggedSliderIndex() == 0; } private boolean isLastSliderDragged() { return getDraggedSliderIndex() == getSliderCount() - 1; } private boolean isVerticalInColorBarArea(int y) { final int dy = Math.abs(paletteRect.y + PALETTE_HEIGHT / 2 - y); return dy < PALETTE_HEIGHT / 2; } private boolean isVerticalInSliderArea(int y) { final int dy = Math.abs(sliderBaseLineRect.y - y); return dy < SLIDER_HEIGHT / 2; } private int getSelectedSliderIndex(MouseEvent evt) { if (isVerticalInSliderArea(evt.getY())) { final int sliderIndex = getNearestSliderIndex(evt.getX()); final double dx = Math.abs(getAbsoluteSliderPos(getSliderSample(sliderIndex)) - evt.getX()); if (dx < Math.floor(SLIDER_WIDTH / 2.0)) { return sliderIndex; } } return INVALID_INDEX; } private int getSelectedSliderTextIndex(MouseEvent evt) { double dy = Math.abs(sliderTextBaseLineY + SLIDER_VALUES_AREA_HEIGHT - evt.getY()); if (dy < SLIDER_VALUES_AREA_HEIGHT) { final int sliderIndex = getNearestSliderIndex(evt.getX()); final double dx = Math.abs(getAbsoluteSliderPos(getSliderSample(sliderIndex)) - evt.getX()); if (dx < Math.floor(FONT_SIZE / 2.0)) { return sliderIndex; } } return INVALID_INDEX; } private int getNearestSliderIndex(int x, int y) { if (isVerticalInSliderArea(y)) { return getNearestSliderIndex(x); } return INVALID_INDEX; } private int getNearestLeftSliderIndex(int x) { final int index = getNearestSliderIndex(x); final double pos = getRelativeSliderPos(getSliderSample(index)); if (pos > x) { if (index > 0) { return index - 1; } return INVALID_INDEX; } return index; } private int getNearestSliderIndex(int x) { int nearestIndex = INVALID_INDEX; double minDx = Float.MAX_VALUE; double dx = 0.0; for (int i = 0; i < getSliderCount(); i++) { dx = getAbsoluteSliderPos(getSliderSample(i)) - x; if (Math.abs(dx) <= minDx) { nearestIndex = i; minDx = Math.abs(dx); } } // Find correct index for two points at the same, last position if (nearestIndex == getSliderCount() - 1) { final int i = getSliderCount() - 1; if (getAbsoluteSliderPos(getSliderSample(i - 1)) == getAbsoluteSliderPos(getSliderSample(i))) { nearestIndex = dx <= 0.0 ? i : i - 1; } } return nearestIndex; } private void resetState() { setDraggedSliderIndex(INVALID_INDEX); factors = null; } } private double scale(double value) { assert model != null; return model.getSampleScaling().scale(value); } private double scaleInverse(double value) { assert model != null; return model.getSampleScaling().scaleInverse(value); } private class RepaintCL implements ChangeListener { @Override public void stateChanged(ChangeEvent e) { palette = null; repaint(); } } private class ModelCL implements ChangeListener { @Override public void stateChanged(ChangeEvent e) { fireStateChanged(); } } }