/*******************************************************************************
* Copyright 2013 Geoscience Australia
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package au.gov.ga.earthsci.common.ui.color;
import java.awt.Color;
import java.beans.PropertyChangeListener;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import org.eclipse.jface.layout.TableColumnLayout;
import org.eclipse.jface.preference.ColorSelector;
import org.eclipse.jface.resource.ColorRegistry;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.ColumnPixelData;
import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
import org.eclipse.jface.viewers.ColumnWeightData;
import org.eclipse.jface.viewers.ComboViewer;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.jface.window.ToolTip;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Scale;
import au.gov.ga.earthsci.common.color.ColorMap;
import au.gov.ga.earthsci.common.color.ColorMap.InterpolationMode;
import au.gov.ga.earthsci.common.color.ColorMaps;
import au.gov.ga.earthsci.common.color.MutableColorMap;
import au.gov.ga.earthsci.common.ui.viewers.NamedLabelProvider;
import au.gov.ga.earthsci.common.ui.widgets.NumericTextField;
/**
* A widget that allows for the editing of a {@link ColorMap}, using a gradient
* editor.
* <p/>
* The editor can be initialised with a {@link ColorMap} instance that will be
* used to seed configuration values. {@link ColorMap} instances can be created
* from the editor using {@link #createColorMap()}.
* <p/>
* The editor can be re-seeded after creation using {@link #setSeed(ColorMap)}.
* This will replace all user edits with the values from the provided map.
* <p/>
* <b>Supported Styles</b>
* <dl>
* <dt>{@link SWT#BORDER}</dt>
* <dd>Apply a border around the editor</dd>
* <dt>{@link SWT#VERTICAL}</dt>
* <dd>Orient the gradient editor vertically (default)</dd>
* </dl>
*
* @author James Navin (james.navin@ga.gov.au)
*/
public class ColorMapEditor extends Composite
{
/*
* This implementation uses events and listeners to respond to changes to the model (MutableColorMap).
* Where possible, changes in the UI are applied to the model and then other areas of the UI
* respond to events issued by the model.
*/
private double minDataValue = 0.0;
private double maxDataValue = 1.0;
private boolean hasDataValues = false;
private MutableColorMap map;
private ColorRegistry colorRegistry;
private List<Marker> markers = new ArrayList<Marker>();
private Color[] colors;
// Gradient area
private Composite gradientAreaContainer;
private Label minText;
private Label maxText;
private Composite gradientContainer;
private Canvas gradientCanvas;
// Marker area
private Canvas markerCanvas;
// Options area
private Composite optionsContainer;
// Mode dropdown
private ComboViewer modeCombo;
private Button percentageBasedButton;
// Entries table
private TableViewer entriesTable;
// Entry editor
private Color currentEntryColor;
private Double currentEntryValue;
private NumericTextField editorValueField;
private ColorSelector editorColorField;
private Scale editorAlphaScale;
private NumericTextField editorAlphaField;
// Add/Remove buttons
private Button addEntryButton;
private Button removeEntryButton;
// Nodata editor
private Button nodataCheckBox;
private ColorSelector nodataColorField;
private Scale nodataAlphaScale;
private NumericTextField nodataAlphaField;
/**
* Create a new {@link ColorMap} editor widget with a default seed map.
*
* @param parent
* The parent composite for the editor
*
* @param style
* The style to apply to this editor
*/
public ColorMapEditor(Composite parent, int style)
{
this(ColorMaps.getRGBRainbowMap(), parent, style);
}
/**
* Create a new {@link ColorMap} editor widget with the given seed map.
* <p/>
* The created map will be forced to use percentage values. To provide the
* option to use absolute values, use the constructor
* {@link #ColorMapEditor(ColorMap, Double, Double, Composite, int)} and
* provide the data value range.
*
* @param seed
* The seed map
* @param parent
* The parent composite for the editor
* @param style
* The style to apply to this editor
*/
public ColorMapEditor(ColorMap seed, Composite parent, int style)
{
this(seed, null, null, parent, style);
}
/**
* Create a new {@link ColorMap} editor widget with the given seed map and
* optional data value range.
* <p/>
* If a data value range is provided, the user will be able to create a map
* whose entries are absolute values (rather than percentages).
*
* @param seed
* The see map to base the editor on
* @param minDataValue
* The minimum data value to use when an absolute value colour
* map is used
* @param maxDataValue
* The maximum data value to use when an absolute value colour
* map is used
* @param parent
* The parent composite for the editor
* @param style
* The style to apply to this editor
*/
public ColorMapEditor(ColorMap seed, Double minDataValue, Double maxDataValue, Composite parent, int style)
{
// TODO: Support vertical / horizontal style
super(parent, style);
setLayout(new GridLayout(3, false));
colorRegistry = new ColorRegistry(getDisplay());
map = new MutableColorMap(seed);
if (minDataValue != null && maxDataValue != null)
{
hasDataValues = true;
this.minDataValue = minDataValue;
this.maxDataValue = maxDataValue;
}
addUIElements();
wireListeners();
}
//******************************************
// Public API
//******************************************
/**
* Set the seed map used in this editor.
* <p/>
* This will reset the editor and remove all user edits, re-initialising
* with the new seed map.
*
* @param seed
* The seed map to base the editor on
*/
public void setSeed(ColorMap seed)
{
map.updateTo(seed);
setCurrentEntry(null);
entriesTable.setSelection(null);
removeEntryButton.setEnabled(false);
disableEntryEditor();
}
/**
* Create a new {@link ColorMap} instance from the configuration captured in
* this editor.
*
* @return A new {@link ColorMap} instance created from the configuration
* captured in this editor.
*/
public ColorMap createColorMap()
{
return map.snapshot();
}
@Override
public void dispose()
{
super.dispose();
}
//******************************************
// Wire up listener behaviour
//******************************************
/**
* Wire all the listeners that coordinate refreshes and updates between the
* model and the various UI elements
*/
private void wireListeners()
{
wireMapListeners();
wireTableListeners();
wireMarkerListeners();
wireGradientListeners();
}
private void wireMapListeners()
{
map.addPropertyChangeListener(MutableColorMap.COLOR_MAP_ENTRY_CHANGE_EVENT, new PropertyChangeListener()
{
@Override
public void propertyChange(java.beans.PropertyChangeEvent evt)
{
Display.getCurrent().asyncExec(new Runnable()
{
@Override
public void run()
{
entriesTable.refresh();
populateColors();
gradientCanvas.redraw();
}
});
}
});
map.addPropertyChangeListener(MutableColorMap.ENTRY_MOVED_EVENT, new PropertyChangeListener()
{
@SuppressWarnings("unchecked")
@Override
public void propertyChange(java.beans.PropertyChangeEvent evt)
{
Entry<Double, Color> oldEntry = (Entry<Double, Color>) evt.getOldValue();
Entry<Double, Color> newEntry = (Entry<Double, Color>) evt.getNewValue();
if (currentEntryValue == null || oldEntry.getKey().equals(currentEntryValue))
{
currentEntryValue = newEntry.getKey();
entriesTable.refresh();
selectTableEntry(newEntry);
}
}
});
map.addPropertyChangeListener(MutableColorMap.ENTRY_ADDED_EVENT, new PropertyChangeListener()
{
@Override
public void propertyChange(java.beans.PropertyChangeEvent evt)
{
@SuppressWarnings("unchecked")
Entry<Double, Color> newEntry = (Entry<Double, Color>) evt.getNewValue();
final Marker newMarker = addMarker(newEntry);
Display.getCurrent().asyncExec(new Runnable()
{
@Override
public void run()
{
markerCanvas.redraw(newMarker.bounds.x,
newMarker.bounds.y,
newMarker.bounds.width,
newMarker.bounds.height,
true);
};
});
}
});
map.addPropertyChangeListener(MutableColorMap.MODE_CHANGE_EVENT, new PropertyChangeListener()
{
@Override
public void propertyChange(java.beans.PropertyChangeEvent evt)
{
Display.getCurrent().asyncExec(new Runnable()
{
@Override
public void run()
{
populateColors();
gradientCanvas.redraw();
modeCombo.setSelection(new StructuredSelection(map.getMode()));
};
});
}
});
map.addPropertyChangeListener(MutableColorMap.NODATA_CHANGE_EVENT, new PropertyChangeListener()
{
@Override
public void propertyChange(java.beans.PropertyChangeEvent evt)
{
updateNodataEditorFromMap();
if (map.getMode() == InterpolationMode.EXACT_MATCH)
{
Display.getCurrent().asyncExec(new Runnable()
{
@Override
public void run()
{
populateColors();
gradientCanvas.redraw();
}
});
}
}
});
}
private void wireTableListeners()
{
entriesTable.addSelectionChangedListener(new ISelectionChangedListener()
{
@Override
public void selectionChanged(SelectionChangedEvent event)
{
Entry<Double, Color> selection = getSelectedTableEntry();
if (selection != null)
{
setCurrentEntry(selection);
enableEntryEditor();
selectMarkerByValue(currentEntryValue);
removeEntryButton.setEnabled(true);
}
}
});
}
private void wireGradientListeners()
{
gradientCanvas.addControlListener(new ControlAdapter()
{
@Override
public void controlResized(ControlEvent e)
{
populateColors();
}
});
gradientCanvas.addListener(SWT.Paint, new Listener()
{
@Override
public void handleEvent(Event e)
{
paintGradient(e.gc, e.display);
}
});
}
private void wireMarkerListeners()
{
markerCanvas.addListener(SWT.Paint, new Listener()
{
@Override
public void handleEvent(Event e)
{
paintMarkers(e.gc, e.display, e.getBounds());
}
});
MarkerMouseListener markerMouseListener = new MarkerMouseListener();
markerCanvas.addMouseListener(markerMouseListener);
markerCanvas.addMouseMoveListener(markerMouseListener);
}
//******************************************
// GUI elements
//******************************************
private void addUIElements()
{
addGradientArea();
addOptionsArea();
}
/**
* Build the options editing
*/
private void addOptionsArea()
{
optionsContainer = new Composite(this, SWT.BORDER);
optionsContainer.setLayoutData(new GridData(GridData.FILL_BOTH | GridData.GRAB_HORIZONTAL
| GridData.GRAB_VERTICAL));
optionsContainer.setLayout(new GridLayout(2, false));
Label modeLabel = new Label(optionsContainer, SWT.NONE);
modeLabel.setText(Messages.ColorMapEditor_ModeLabel);
modeCombo = new ComboViewer(optionsContainer, SWT.DROP_DOWN);
modeCombo.setContentProvider(ArrayContentProvider.getInstance());
modeCombo.setInput(InterpolationMode.values());
modeCombo.setLabelProvider(new NamedLabelProvider());
modeCombo.setSelection(new StructuredSelection(map.getMode()));
final Label modeDescription = new Label(optionsContainer, SWT.WRAP);
GridData gd = new GridData(GridData.FILL_HORIZONTAL);
gd.horizontalSpan = 2;
gd.horizontalIndent = 10;
modeDescription.setLayoutData(gd);
modeDescription.setText(map.getMode().getDescription());
modeDescription.setFont(JFaceResources.getFontRegistry().getItalic("default")); //$NON-NLS-1$
modeCombo.addSelectionChangedListener(new ISelectionChangedListener()
{
@Override
public void selectionChanged(SelectionChangedEvent event)
{
InterpolationMode newMode =
(InterpolationMode) ((IStructuredSelection) event.getSelection()).getFirstElement();
if (newMode != map.getMode())
{
map.setMode(newMode);
modeDescription.setText(newMode.getDescription());
}
}
});
if (hasDataValues)
{
percentageBasedButton = new Button(optionsContainer, SWT.CHECK);
percentageBasedButton.setText(Messages.ColorMapEditor_UsePercentagesLabel);
percentageBasedButton.setSelection(map.isPercentageBased());
gd = new GridData();
gd.horizontalSpan = 2;
percentageBasedButton.setLayoutData(gd);
percentageBasedButton.addSelectionListener(new SelectionListener()
{
@Override
public void widgetSelected(SelectionEvent e)
{
map.setValuesArePercentages(percentageBasedButton.getSelection(),
minDataValue,
maxDataValue);
}
@Override
public void widgetDefaultSelected(SelectionEvent e)
{
map.setValuesArePercentages(percentageBasedButton.getSelection(),
minDataValue,
maxDataValue);
}
});
}
addEntryEditor(optionsContainer);
addAddRemoveButtons(optionsContainer);
addEntriesList(optionsContainer);
addNodataEditor(optionsContainer);
}
/**
* Add the entry editor area. Allows users to edit:
* <ul>
* <li>Value
* <li>Colour
* <li>Transparency
* </ul>
* For a single selected entry in the colour map
*/
private void addEntryEditor(Composite parent)
{
Composite editorContainer = new Composite(parent, SWT.BORDER);
GridData gd = new GridData(GridData.FILL_HORIZONTAL);
gd.horizontalSpan = 2;
editorContainer.setLayoutData(gd);
editorContainer.setLayout(new GridLayout(7, false));
Label valueLabel = new Label(editorContainer, SWT.NONE);
valueLabel.setText(Messages.ColorMapEditor_EntryValueLabel);
editorValueField = new NumericTextField(editorContainer, SWT.SINGLE | SWT.BORDER);
editorValueField.addFocusListener(new FocusAdapter()
{
@Override
public void focusLost(FocusEvent e)
{
map.moveEntry(currentEntryValue, editorValueField.getNumber().doubleValue());
currentEntryValue = editorValueField.getNumber().doubleValue();
}
});
Label colorLabel = new Label(editorContainer, SWT.NONE);
colorLabel.setText(Messages.ColorMapEditor_EntryColorLabel);
editorColorField = new ColorSelector(editorContainer);
editorColorField.addListener(new IPropertyChangeListener()
{
@Override
public void propertyChange(PropertyChangeEvent event)
{
updateCurrentEntryColor();
}
});
Label alphaLabel = new Label(editorContainer, SWT.NONE);
alphaLabel.setText(Messages.ColorMapEditor_EntryAlphaLabel);
editorAlphaScale = new Scale(editorContainer, SWT.HORIZONTAL);
editorAlphaScale.setMinimum(0);
editorAlphaScale.setMaximum(255);
editorAlphaScale.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
editorAlphaField = new NumericTextField(editorContainer, SWT.BORDER, false, false);
editorAlphaField.setMinValue(0);
editorAlphaField.setMaxValue(255);
gd = new GridData();
gd.widthHint = 35;
editorAlphaField.setLayoutData(gd);
editorAlphaScale.addSelectionListener(new SelectionAdapter()
{
@Override
public void widgetSelected(SelectionEvent e)
{
updateCurrentEntryColor();
editorAlphaField.setNumber(editorAlphaScale.getSelection());
}
});
editorAlphaField.addFocusListener(new FocusAdapter()
{
@Override
public void focusLost(FocusEvent e)
{
editorAlphaScale.setSelection(editorAlphaField.getNumber().intValue());
updateCurrentEntryColor();
}
});
disableEntryEditor();
}
private void addAddRemoveButtons(Composite parent)
{
Composite buttonContainer = new Composite(parent, SWT.NONE);
GridData gd = new GridData(GridData.FILL_HORIZONTAL);
gd.horizontalSpan = 2;
buttonContainer.setLayoutData(gd);
buttonContainer.setLayout(new GridLayout(2, false));
addEntryButton = new Button(buttonContainer, SWT.PUSH);
addEntryButton.setText(Messages.ColorMapEditor_AddEntryLabel);
addEntryButton.addSelectionListener(new SelectionAdapter()
{
@Override
public void widgetSelected(SelectionEvent e)
{
addNewEntry();
}
});
removeEntryButton = new Button(buttonContainer, SWT.PUSH);
removeEntryButton.setText(Messages.ColorMapEditor_RemoveEntryLabel);
removeEntryButton.setEnabled(false);
removeEntryButton.addSelectionListener(new SelectionAdapter()
{
@Override
public void widgetSelected(SelectionEvent e)
{
removeCurrentEntry();
}
});
}
private void addEntriesList(Composite parent)
{
Composite tableContainer = new Composite(parent, SWT.NONE);
GridData gd = new GridData(GridData.FILL_BOTH);
gd.horizontalSpan = 2;
tableContainer.setLayoutData(gd);
TableColumnLayout layout = new TableColumnLayout();
tableContainer.setLayout(layout);
// Not sure why, but columns and column label providers only work when SWT.VIRTUAL is used
entriesTable = new TableViewer(tableContainer, SWT.SINGLE | SWT.FULL_SELECTION | SWT.BORDER | SWT.VIRTUAL);
entriesTable.setContentProvider(ArrayContentProvider.getInstance());
entriesTable.getTable().setHeaderVisible(true);
entriesTable.getTable().setLinesVisible(true);
entriesTable.setInput(map.getEntries().entrySet());
TableViewerColumn valueColumn = new TableViewerColumn(entriesTable, SWT.NONE);
valueColumn.getColumn().setText(Messages.ColorMapEditor_TableValueColumnLabel);
valueColumn.setLabelProvider(new ColumnLabelProvider()
{
@SuppressWarnings("unchecked")
@Override
public String getText(Object element)
{
Double value = ((Entry<Double, Color>) element).getKey();
String result = "" + value; //$NON-NLS-1$
if (map.isPercentageBased())
{
result += " (" + (int) (value * 100) + "%)"; //$NON-NLS-1$ //$NON-NLS-2$
}
return result;
}
});
layout.setColumnData(valueColumn.getColumn(), new ColumnPixelData(80, false));
TableViewerColumn colorNameColumn = new TableViewerColumn(entriesTable, SWT.NONE);
colorNameColumn.getColumn().setText(Messages.ColorMapEditor_TableColorColumnLabel);
colorNameColumn.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object element)
{
Color color = getColorFromElement(element);
return "RGBA(" + color.getRed() + ", " //$NON-NLS-1$ //$NON-NLS-2$
+ color.getGreen() + ", " //$NON-NLS-1$
+ color.getBlue() + ", " //$NON-NLS-1$
+ color.getAlpha() + ")"; //$NON-NLS-1$
}
@Override
public String getToolTipText(Object element)
{
return "#" + getColorKey(getColorFromElement(element)); //$NON-NLS-1$
}
@SuppressWarnings("unchecked")
private Color getColorFromElement(Object element)
{
return ((Entry<Double, Color>) element).getValue();
}
private String getColorKey(Color color)
{
return Integer.toHexString(color.getRGB());
}
});
layout.setColumnData(colorNameColumn.getColumn(), new ColumnWeightData(100, false));
ColumnViewerToolTipSupport.enableFor(entriesTable, ToolTip.NO_RECREATE);
}
/**
* Add an editor area for changing the NODATA colour used
*/
private void addNodataEditor(Composite parent)
{
Composite editorContainer = new Composite(parent, SWT.BORDER);
GridData gd = new GridData(GridData.FILL_HORIZONTAL);
gd.horizontalSpan = 2;
editorContainer.setLayoutData(gd);
editorContainer.setLayout(new GridLayout(7, false));
nodataCheckBox = new Button(editorContainer, SWT.CHECK);
gd = new GridData(GridData.FILL_HORIZONTAL);
gd.horizontalSpan = 7;
nodataCheckBox.setLayoutData(gd);
nodataCheckBox.setText(Messages.ColorMapEditor_NoDataOptionLabel);
nodataCheckBox.setToolTipText(Messages.ColorMapEditor_NoDataOptionTooltip);
nodataCheckBox.addSelectionListener(new SelectionAdapter()
{
@Override
public void widgetSelected(SelectionEvent e)
{
enableNodataEditor(nodataCheckBox.getSelection());
updateNodataColorFromEditor();
}
});
Label colorLabel = new Label(editorContainer, SWT.NONE);
colorLabel.setText("Color:"); //$NON-NLS-1$
nodataColorField = new ColorSelector(editorContainer);
nodataColorField.addListener(new IPropertyChangeListener()
{
@Override
public void propertyChange(PropertyChangeEvent event)
{
updateNodataColorFromEditor();
}
});
nodataColorField.setColorValue(toRGB(Color.BLACK));
Label alphaLabel = new Label(editorContainer, SWT.NONE);
alphaLabel.setText("Alpha:"); //$NON-NLS-1$
nodataAlphaScale = new Scale(editorContainer, SWT.HORIZONTAL);
nodataAlphaScale.setMinimum(0);
nodataAlphaScale.setMaximum(255);
nodataAlphaScale.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
nodataAlphaScale.setSelection(255);
nodataAlphaField = new NumericTextField(editorContainer, SWT.BORDER, false, false);
nodataAlphaField.setMinValue(0);
nodataAlphaField.setMaxValue(255);
gd = new GridData();
gd.widthHint = 35;
nodataAlphaField.setLayoutData(gd);
nodataAlphaField.setNumber(255);
nodataAlphaScale.addSelectionListener(new SelectionAdapter()
{
@Override
public void widgetSelected(SelectionEvent e)
{
updateNodataColorFromEditor();
nodataAlphaField.setNumber(nodataAlphaScale.getSelection());
}
});
nodataAlphaField.addFocusListener(new FocusAdapter()
{
@Override
public void focusLost(FocusEvent e)
{
nodataAlphaScale.setSelection(nodataAlphaField.getNumber().intValue());
updateNodataColorFromEditor();
}
});
updateNodataEditorFromMap();
}
/**
* Build the gradient edit area
*/
private void addGradientArea()
{
final int gradientWidth = 40;
final int markerAreaWidth = 40;
// Contains label-gradient+markers-label
gradientAreaContainer = new Composite(this, SWT.NONE);
gradientAreaContainer.setLayout(new GridLayout(1, false));
GridData gd = new GridData(GridData.FILL_VERTICAL);
gd.widthHint = gradientWidth + markerAreaWidth;
gradientAreaContainer.setLayoutData(gd);
minText = new Label(gradientAreaContainer, SWT.BORDER);
gd = new GridData();
gd.widthHint = gradientWidth;
minText.setLayoutData(gd);
minText.setText("" + minDataValue); //$NON-NLS-1$
minText.setAlignment(SWT.CENTER);
// Contains gradient-markers
gradientContainer = new Composite(gradientAreaContainer, SWT.BORDER);
GridLayout layout = new GridLayout(2, false);
layout.horizontalSpacing = 0;
layout.marginHeight = 0;
layout.marginWidth = 0;
layout.verticalSpacing = 0;
gradientContainer.setLayout(layout);
gd = new GridData(GridData.FILL_BOTH);
gradientContainer.setLayoutData(gd);
maxText = new Label(gradientAreaContainer, SWT.BORDER);
gd = new GridData();
gd.widthHint = gradientWidth;
maxText.setLayoutData(gd);
maxText.setText("" + maxDataValue); //$NON-NLS-1$
maxText.setAlignment(SWT.CENTER);
gradientCanvas = new Canvas(gradientContainer, SWT.BORDER | SWT.NO_BACKGROUND);
gd = new GridData(GridData.FILL_VERTICAL);
gd.widthHint = gradientWidth;
gradientCanvas.setLayoutData(gd);
markerCanvas = new Canvas(gradientContainer, SWT.NONE);
gd = new GridData(GridData.FILL_BOTH);
gd.widthHint = markerAreaWidth;
markerCanvas.setLayoutData(gd);
gradientAreaContainer.layout();
populateColors();
populateMarkers();
}
//**********************************************
// Selection changes
//**********************************************
private void setCurrentEntry(Entry<Double, Color> entry)
{
if (entry != null)
{
currentEntryValue = entry.getKey();
currentEntryColor = entry.getValue();
}
else
{
currentEntryValue = null;
currentEntryColor = null;
}
}
private void updateCurrentEntryColor()
{
currentEntryColor = fromRGB(editorColorField.getColorValue(),
editorAlphaScale.getSelection());
map.changeColor(currentEntryValue, currentEntryColor);
}
private void selectTableEntry(Entry<Double, Color> entry)
{
if (entry == null)
{
entriesTable.setSelection(null);
return;
}
entriesTable.setSelection(new StructuredSelection(entry));
}
/**
* @return The currently selected entry in the entries table, or
* <code>null</code> if none is selected.
*/
@SuppressWarnings("unchecked")
private Entry<Double, Color> getSelectedTableEntry()
{
Entry<Double, Color> selection =
(Entry<Double, Color>) ((IStructuredSelection) entriesTable.getSelection()).getFirstElement();
return selection;
}
/**
* Select the marker at the given coordinate, if one exists
*/
private Marker selectMarkerByCoordinate(int x, int y)
{
Point p = new Point(x, y);
Marker result = null;
for (Marker m : markers)
{
m.setSelected(m.contains(p));
if (m.selected)
{
result = m;
entriesTable.setSelection(new StructuredSelection(m.getEntry()));
}
}
return result;
}
/**
* Select the marker with the given value, of one exists
*/
private Marker selectMarkerByValue(Double value)
{
if (value == null)
{
return null;
}
Marker result = null;
for (Marker m : markers)
{
m.setSelected(m.getValue().equals(value));
if (m.selected)
{
result = m;
}
}
Display.getCurrent().asyncExec(new Runnable()
{
@Override
public void run()
{
markerCanvas.redraw();
}
});
return result;
}
private void enableEntryEditor()
{
editorValueField.setEnabled(true);
editorValueField.setNumber(currentEntryValue);
editorColorField.setEnabled(true);
editorColorField.setColorValue(toRGB(currentEntryColor));
editorAlphaScale.setEnabled(true);
editorAlphaScale.setSelection(currentEntryColor.getAlpha());
editorAlphaField.setEnabled(true);
editorAlphaField.setNumber(currentEntryColor.getAlpha());
}
private void disableEntryEditor()
{
editorValueField.setNumber(null);
editorValueField.setEnabled(false);
editorColorField.setEnabled(false);
editorAlphaScale.setSelection(255);
editorAlphaScale.setEnabled(false);
editorAlphaField.setNumber(null);
editorAlphaField.setEnabled(false);
}
private void enableNodataEditor(boolean enable)
{
nodataColorField.setEnabled(enable);
nodataAlphaScale.setEnabled(enable);
nodataAlphaField.setEnabled(enable);
}
private void updateNodataColorFromEditor()
{
if (!nodataCheckBox.getSelection())
{
map.setNodataColour(null);
return;
}
Color nodataColor = fromRGB(nodataColorField.getColorValue(),
nodataAlphaScale.getSelection());
map.setNodataColour(nodataColor);
}
private void updateNodataEditorFromMap()
{
Color nodataColor = map.getNodataColour();
if (nodataColor == null)
{
nodataCheckBox.setSelection(false);
enableNodataEditor(false);
return;
}
nodataColorField.setColorValue(toRGB(nodataColor));
nodataAlphaScale.setSelection(nodataColor.getAlpha());
nodataAlphaField.setNumber(nodataColor.getAlpha());
}
//**********************************************
// Add / Remove entries
//**********************************************
/**
* Add a new entry in the colour map that is:
* <ul>
* <li>Half way between the selected entry and the previous entry (if one
* exists); or
* <li>Half way between the selected entry and the min value (if no previous
* entry exists and selected != min)
* <li>Half way between the selected entry and the next entry (if min entry
* is selected); or
* <li>Half way between the selected entry and the max value (if min entry
* is selected and no other entry exists); or
* <li>Half way between the min value and the max value (if no entries exist
* in the table)
* </ul>
*
*/
private void addNewEntry()
{
Double newEntryValue = getMinValue();
Entry<Double, Color> selectedEntry = getSelectedTableEntry();
if (selectedEntry == null)
{
// If nothing selected, either choose the first entry or the mid point
if (map.isEmpty())
{
newEntryValue = getMinValue() + (getMaxValue() - getMinValue()) / 2;
}
else
{
selectedEntry = map.getFirstEntry();
}
}
if (selectedEntry != null)
{
if (map.getSize() == 1)
{
// Map only contains one entry
if (selectedEntry.getKey() == getMinValue())
{
// Selected entry is min value - go higher
newEntryValue = getMinValue() + (getMaxValue() - getMinValue()) / 2;
}
else
{
// Otherwise go lower
newEntryValue = getMinValue() + (selectedEntry.getKey() - getMinValue()) / 2;
}
}
else
{
// Map contains more than one entry
if (selectedEntry.equals(map.getFirstEntry()))
{
// Selected entry is first entry
if (selectedEntry.getKey() == getMinValue())
{
// Go between selected and next
double nextValue = map.getNextEntry(getMinValue()).getKey();
newEntryValue = getMinValue() + (nextValue - getMinValue()) / 2;
}
else
{
// Go between selected and min value
newEntryValue = getMinValue() + (selectedEntry.getKey() - getMinValue()) / 2;
}
}
else
{
// Selected entry is not first - go between previous entry
double previousValue = map.getPreviousEntry(selectedEntry.getKey()).getKey();
newEntryValue = previousValue + (selectedEntry.getKey() - previousValue) / 2;
}
}
}
Color newEntryColor = map.getColor(newEntryValue);
map.addEntry(newEntryValue, newEntryColor);
}
private void removeCurrentEntry()
{
if (currentEntryValue == null)
{
return;
}
map.removeEntry(currentEntryValue);
setCurrentEntry(null);
entriesTable.setSelection(null);
removeEntryButton.setEnabled(false);
disableEntryEditor();
}
//**********************************
// Data population
//**********************************
private void populateMarkers()
{
List<Marker> newMarkers = new ArrayList<Marker>(map.getSize());
int z = 0;
for (Entry<Double, Color> entry : map.getEntries().entrySet())
{
newMarkers.add(new Marker(z++, entry));
}
Collections.sort(newMarkers);
markers = newMarkers;
}
private Marker addMarker(Entry<Double, Color> newEntry)
{
Marker newMarker = new Marker(0, newEntry);
markers.add(newMarker);
return newMarker;
}
private void populateColors()
{
final int exactMatchBarWidth = 3;
Point canvasSize = gradientCanvas.getSize();
if (canvasSize.y == 0)
{
colors = new Color[0];
return;
}
Color[] newColors = new Color[canvasSize.y - 2 * gradientCanvas.getBorderWidth()];
double minValue = getMinValue();
double maxValue = getMaxValue();
if (map.getMode() == InterpolationMode.EXACT_MATCH)
{
// Need a special case here where pixel values don't match exactly with values in the map
// We still want to draw the lines in the gradient
for (int i = 0; i < newColors.length; i++)
{
double pixelValue = ((double) i / newColors.length) * (maxValue - minValue) + minValue;
Entry<Double, Color> nearestEntry = map.getNearestEntry(pixelValue);
if (nearestEntry == null)
{
newColors[i] = map.getNodataColour();
}
else
{
int nearestPixel =
(int) ((nearestEntry.getKey() - minValue) / (maxValue - minValue) * newColors.length);
if (Math.abs(nearestPixel - i) < exactMatchBarWidth)
{
newColors[i] = nearestEntry.getValue();
}
else
{
newColors[i] = map.getNodataColour();
}
}
}
}
else
{
for (int i = 0; i < newColors.length; i++)
{
double pixelValue = ((double) i / newColors.length) * (maxValue - minValue) + minValue;
Color color = map.getColor(pixelValue, minValue, maxValue);
newColors[i] = color;
}
}
colors = newColors;
}
//**********************************
// Painting
//**********************************
private void paintGradient(GC gc, Display display)
{
// Allow colours array to be changed mid-render without locking
Color[] paintColors = colors;
// TODO: I suspect this is a bad way of doing this... optimise based on
// interp mode - we should be able to seriously decrease the number of
// colours created etc. when eg. nearest_match is used
org.eclipse.swt.graphics.Color swtColor = null;
org.eclipse.swt.graphics.Color backgroundColor = gradientCanvas.getBackground();
Point size = gradientCanvas.getSize();
for (int pixel = 0; pixel < paintColors.length; pixel++)
{
Color paintColor = paintColors[pixel];
if (paintColor != null)
{
swtColor = toSwtColor(paintColor, backgroundColor);
}
else
{
swtColor = gradientCanvas.getBackground();
}
gc.setForeground(swtColor);
gc.drawLine(0, pixel, size.x, pixel);
}
}
/**
* Paint the current list of markers in the marker canvas
*/
private void paintMarkers(GC gc, Display display, Rectangle bounds)
{
List<Marker> markers = this.markers;
for (Marker m : markers)
{
if (m.bounds.intersects(bounds))
{
m.paint(gc);
}
}
}
//**********************************
// Utilities
//**********************************
/**
* @return The minimum data value for the current map
*/
private double getMinValue()
{
if (map.isPercentageBased() || !hasDataValues)
{
return 0.0;
}
return minDataValue;
}
/**
* @return The maximum data value for the current map
*/
private double getMaxValue()
{
if (map.isPercentageBased() || !hasDataValues)
{
return 1.0;
}
return maxDataValue;
}
private int getGradientSize()
{
return gradientCanvas.getSize().y - 2 * gradientCanvas.getBorderWidth();
}
private int getMarkerCanvasOffset()
{
return gradientCanvas.getBorderWidth();
}
/**
* Convert an AWT {@link Color} instance to an equivalent SWT {@link RGB}.
* <p/>
* Note that this conversion will ignore the alpha value of the AWT
* {@link Color} as SWT does not support alpha.
*/
private static RGB toRGB(Color color)
{
if (color == null)
{
return null;
}
return new RGB(color.getRed(), color.getGreen(), color.getBlue());
}
/**
* Convert the given SWT {@link RGB} instance to an equivalent AWT
* {@link Color} instance, applying the given alpha value.
*/
private static Color fromRGB(RGB rgb, int alpha)
{
if (rgb == null)
{
return null;
}
return new Color(rgb.red, rgb.green, rgb.blue, alpha);
}
/**
* Convert an AWT colour to an SWT colour.
* <p/>
* If the provided AWT colour contains transparency, will pre-multiply with
* the given background colour (as SWT colours do not support an alpha
* channel).
*
* @param awtColor
* The AWT colour to convert
* @param display
* The display to use when creating the SWT colour
* @param backgroundColor
* The background colour to use for pre-multiplying in the case
* of an AWT colour with transparency.
*
* @return A new SWT colour
*/
private org.eclipse.swt.graphics.Color toSwtColor(Color awtColor,
org.eclipse.swt.graphics.Color backgroundColor)
{
String key = "" + awtColor.getRGB() + "+" + backgroundColor.getRGB().hashCode(); //$NON-NLS-1$ //$NON-NLS-2$
if (colorRegistry.hasValueFor(key))
{
return colorRegistry.get(key);
}
int red;
int green;
int blue;
if (awtColor.getAlpha() < 255 && backgroundColor != null)
{
// Do alpha pre-multiplication as SWT colours don't support an alpha channel
// Use a simple (alpha + 1-alpha) combiner
float alpha = awtColor.getAlpha() / 255.0f;
red = (int) (awtColor.getRed() * alpha) + (int) (backgroundColor.getRed() * (1 - alpha));
green = (int) (awtColor.getGreen() * alpha) + (int) (backgroundColor.getGreen() * (1 - alpha));
blue = (int) (awtColor.getBlue() * alpha) + (int) (backgroundColor.getBlue() * (1 - alpha));
}
else
{
red = awtColor.getRed();
green = awtColor.getGreen();
blue = awtColor.getBlue();
}
colorRegistry.put(key, new RGB(red, green, blue));
return colorRegistry.get(key);
}
//*********************************
// Markers
//*********************************
/**
* Represents a single marker in the colour gradient
* <p/>
* Coordinates stored are relative to the
* {@link ColorMapEditor#markerCanvas}
*/
private class Marker implements Comparable<Marker>
{
final static int markerThickness = 4;
final static int borderWidth = 2;
final static int borderThickness = markerThickness + 2 * borderWidth;
final Color unselectedBorderColor = new Color(0, 0, 0);
final Color selectedBorderColor = new Color(100, 100, 220);
private int zIndex; // Drawn in reverse order of zIndex: 0 = top.
private Double value;
private Color color;
private int midPointY;
private Rectangle bounds;
private boolean selected;
private PropertyChangeListener entryMovedListener = new PropertyChangeListener()
{
@SuppressWarnings("unchecked")
@Override
public void propertyChange(java.beans.PropertyChangeEvent evt)
{
Entry<Double, Color> oldEntry = (Entry<Double, Color>) evt.getOldValue();
if (!isThisMarker(oldEntry.getKey()))
{
return;
}
value = ((Entry<Double, Color>) evt.getNewValue()).getKey();
final Rectangle oldBounds = bounds;
updateBounds();
Display.getCurrent().asyncExec(new Runnable()
{
@Override
public void run()
{
markerCanvas.redraw(oldBounds.x, oldBounds.y, oldBounds.width, oldBounds.height, true);
markerCanvas.redraw(bounds.x, bounds.y, bounds.width, bounds.height, true);
}
});
}
};
private PropertyChangeListener entryRemovedListener = new PropertyChangeListener()
{
@Override
public void propertyChange(java.beans.PropertyChangeEvent evt)
{
@SuppressWarnings("unchecked")
Entry<Double, Color> removed = (Entry<Double, Color>) evt.getOldValue();
if (!isThisMarker(removed.getKey()))
{
return;
}
dispose();
}
};
private PropertyChangeListener colorChangedListener = new PropertyChangeListener()
{
@Override
public void propertyChange(java.beans.PropertyChangeEvent evt)
{
@SuppressWarnings("unchecked")
Entry<Double, Color> entry = (Entry<Double, Color>) evt.getNewValue();
if (!isThisMarker(entry.getKey()))
{
return;
}
color = entry.getValue();
Display.getCurrent().asyncExec(new Runnable()
{
@Override
public void run()
{
markerCanvas.redraw(bounds.x, bounds.y, bounds.width, bounds.height, true);
}
});
}
};
private ControlAdapter controlAdapter = new ControlAdapter()
{
@Override
public void controlResized(ControlEvent e)
{
updateBounds();
}
};
public Marker(int zIndex, Entry<Double, Color> entry)
{
this.zIndex = zIndex;
this.value = entry.getKey();
this.color = entry.getValue();
markerCanvas.addControlListener(controlAdapter);
map.addPropertyChangeListener(MutableColorMap.ENTRY_MOVED_EVENT, entryMovedListener);
map.addPropertyChangeListener(MutableColorMap.COLOR_CHANGED_EVENT, colorChangedListener);
map.addPropertyChangeListener(MutableColorMap.ENTRY_REMOVED_EVENT, entryRemovedListener);
updateBounds();
}
private void updateBounds()
{
int centreX = markerCanvas.getSize().x / 2;
double percent = (getValue() - getMinValue()) / (getMaxValue() - getMinValue());
midPointY = (int) (percent * getGradientSize()) + getMarkerCanvasOffset();
bounds = new Rectangle(0, midPointY - borderThickness / 2, centreX + borderWidth, borderThickness);
}
@Override
public int compareTo(Marker o)
{
return o.zIndex - zIndex;
}
public void paint(GC gc)
{
if (selected)
{
gc.setForeground(toSwtColor(selectedBorderColor, markerCanvas.getBackground()));
}
else
{
gc.setForeground(toSwtColor(unselectedBorderColor, markerCanvas.getBackground()));
}
// Draw border
gc.setLineWidth(borderThickness);
gc.drawLine(0, midPointY, bounds.width, midPointY);
// Draw colour dash
gc.setForeground(toSwtColor(getColor(), markerCanvas.getBackground()));
gc.setLineWidth(markerThickness);
gc.drawLine(0, midPointY, bounds.width - borderWidth, midPointY);
}
public boolean contains(Point p)
{
return bounds.contains(p);
}
public void setSelected(boolean selected)
{
this.selected = selected;
}
public void setValue(double value)
{
map.moveEntry(getValue(), value);
}
public boolean isThisMarker(Double value)
{
return getValue().equals(value);
}
public Double getValue()
{
return value;
}
public Color getColor()
{
return color;
}
public Entry<Double, Color> getEntry()
{
return new AbstractMap.SimpleEntry<Double, Color>(value, color);
}
public int getZIndex()
{
return zIndex;
}
private void dispose()
{
map.removePropertyChangeListener(entryRemovedListener);
markerCanvas.removeControlListener(controlAdapter);
map.removePropertyChangeListener(colorChangedListener);
map.removePropertyChangeListener(entryMovedListener);
markers.remove(this);
Display.getCurrent().asyncExec(new Runnable()
{
@Override
public void run()
{
markerCanvas.redraw(bounds.x, bounds.y, bounds.width, bounds.height, true);
}
});
}
@Override
public String toString()
{
return "Marker" + hashCode() + "[" + getValue() + ", " + getColor() + "](" + getZIndex() + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
}
}
private class MarkerMouseListener extends MouseAdapter implements MouseMoveListener
{
private final double minValueChange = 0.001;
private boolean mouseDown = false;
private int lastY = -1;
private Marker heldMarker = null;
@Override
public void mouseDown(MouseEvent e)
{
mouseDown = true;
lastY = e.y;
heldMarker = selectMarkerByCoordinate(e.x, e.y);
}
@Override
public void mouseUp(MouseEvent e)
{
mouseDown = false;
lastY = -1;
heldMarker = null;
}
@Override
public void mouseMove(MouseEvent e)
{
if (!mouseDown || heldMarker == null)
{
return;
}
int deltaY = e.y - lastY;
double valueChange =
((double) deltaY / getGradientSize()) * (getMaxValue() - getMinValue()) + getMinValue();
double newValue = heldMarker.getValue() + valueChange;
if (doMove(valueChange, newValue))
{
lastY = e.y;
heldMarker.setValue(newValue);
heldMarker.setSelected(true);
}
}
private boolean doMove(double valueChange, double newValue)
{
if (Math.abs(valueChange) < minValueChange ||
newValue < getMinValue() || newValue > getMaxValue())
{
return false;
}
// Check that we don't replace an existing map entry when moving markers around
Entry<Double, Color> nextEntry = map.getNextEntry(newValue);
if (nextEntry != null && Math.abs(newValue - nextEntry.getKey()) < minValueChange)
{
return false;
}
Entry<Double, Color> previousEntry = map.getNextEntry(newValue);
if (previousEntry != null && Math.abs(newValue - previousEntry.getKey()) < minValueChange)
{
return false;
}
return true;
}
}
}