/*******************************************************************************
* 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.color;
import java.awt.Color;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* A mutable version of the {@link ColorMap} class that allows entries to be
* manipulated.
* <p/>
* In general, it is recommended that {@link ColorMap} be used where
* appropriate, and that this class only be used where necessary (e.g. where
* users manipulate color map values etc.)
* <p/>
* <b>Events:</b>
* <dl>
* <dt>{@value #COLOR_MAP_ENTRY_CHANGE_EVENT}</dt>
* <dd>Fired when a colour map entry changes. The contents of the event will
* include the value that has changed.</dt>
* <dt>{@value #ENTRY_ADDED_EVENT}</dt>
* <dd>Fired when a new entry is added to the colour map. Value will the be
* added {@link Entry}.</dd>
* <dt>{@value #ENTRY_REMOVED_EVENT}</dt>
* <dd>Fired when a new entry is added to the colour map. Value will be the
* removed {@link Entry}</dd>
* <dt>{@value #ENTRY_MOVED_EVENT}</dt>
* <dd>Fired when an entry is moved in the colour map. Value will be the moved
* {@link Entry}</dd>
* <dt>{@value #COLOR_CHANGED_EVENT}</dt>
* <dd>Fired when a new entry is added to the colour map. Value will be the
* changed {@link Entry}</dd>
* <dt>{@value #MODE_CHANGE_EVENT}</dt>
* <dd>Fired when the interpolation mode changes</dd>
* <dt>{@value #VALUE_TYPE_CHANGE_EVENT}</dt>
* <dd>Fired when the the value type is changed between absolute and percentage
* based</dd>
* <dt>{@value #NAME_CHANGE_EVENT}</dt>
* <dd>Fired when the name for this map changes</dd>
* <dt>{@value #DESCRIPTION_CHANGE_EVENT}</dt>
* <dd>Fired when the description for this map changes</dd>
* <dt>{@value #NODATA_CHANGE_EVENT}</dt>
* <dd>Fired when the NODATA colour of this map changes</dd>
* </dl>
*
* @author James Navin (james.navin@ga.gov.au)
*
*/
public class MutableColorMap extends ColorMap
{
/** The event fired when the colour map changes */
public static final String COLOR_MAP_ENTRY_CHANGE_EVENT = "colorMapEntry"; //$NON-NLS-1$
/** The event fired when an entry is added to the map */
public static final String ENTRY_ADDED_EVENT = "entryAdded"; //$NON-NLS-1$
/** The event fired when an entry is removed from the map */
public static final String ENTRY_REMOVED_EVENT = "entryRemoved"; //$NON-NLS-1$
/** The event fired when an entry is moved in the map */
public static final String ENTRY_MOVED_EVENT = "entryMoved"; //$NON-NLS-1$
/** The event fired when an entry colour changes in the map */
public static final String COLOR_CHANGED_EVENT = "colorChanged"; //$NON-NLS-1$
/** The event fired when the mode changes */
public static final String MODE_CHANGE_EVENT = "mode"; //$NON-NLS-1$
/**
* The event fired when the value type is changed between absolute and
* percentage based
*/
public static final String VALUE_TYPE_CHANGE_EVENT = "valueType"; //$NON-NLS-1$
/** The event fired when the map name changes */
public static final String NAME_CHANGE_EVENT = "name"; //$NON-NLS-1$
/** The event fired when the description changes */
public static final String DESCRIPTION_CHANGE_EVENT = "description"; //$NON-NLS-1$
/** The event fired when the nodata value changes */
public static final String NODATA_CHANGE_EVENT = "nodata"; //$NON-NLS-1$
final private PropertyChangeSupport propertyChange = new PropertyChangeSupport(this);
private ReadWriteLock entriesLock = new ReentrantReadWriteLock();
/**
* Create a new empty mutable colour map
*/
public MutableColorMap()
{
this((ColorMap) null);
}
/**
* Create a new mutable version of the given colour map
*
* @param map
* The map to create a mutable version of. If <code>null</code>,
* creates an empty, unnamed color map with no entries.
*/
public MutableColorMap(ColorMap map)
{
this(map == null ? null : map.getName(),
map == null ? null : map.getDescription(),
map == null ? null : map.getEntries(),
map == null ? null : map.getNodataColour(),
map == null ? null : map.getMode(),
map == null ? false : map.isPercentageBased());
}
/**
* @see ColorMap#ColorMap(Map)
*/
public MutableColorMap(Map<Double, Color> entries)
{
super(entries);
}
/**
* @see ColorMap#ColorMap(String, String, Map, Color, InterpolationMode,
* boolean)
*/
public MutableColorMap(String name, String description, Map<Double, Color> entries, Color nodataColour,
InterpolationMode mode, boolean valuesArePercentages)
{
super(name,
description,
entries == null ? new HashMap<Double, Color>() : entries,
nodataColour,
mode,
valuesArePercentages);
}
/**
* Update this mutable map to the same values as those contained in the
* provided {@link ColorMap}. Similar to the copy constructor
* {@link #MutableColorMap(ColorMap)} but changes the current instance
* rather than creating a new one.
* <p/>
* All listener events will be fired as appropriate.
*
* @param map
* The map to update this instance to
*/
public void updateTo(ColorMap map)
{
if (map == null || this.equals(map))
{
return;
}
setName(map.getName());
setDescription(map.getDescription());
setNodataColour(map.getNodataColour());
setMode(map.getMode());
for (Entry<Double, Color> existing : new HashSet<Entry<Double, Color>>(entries.entrySet()))
{
removeEntry(existing.getKey());
}
for (Entry<Double, Color> newEntry : map.getEntries().entrySet())
{
addEntry(newEntry.getKey(), newEntry.getValue());
}
}
@Override
public Color getColor(double value)
{
entriesLock.readLock().lock();
try
{
return super.getColor(value);
}
finally
{
entriesLock.readLock().unlock();
}
}
@Override
public Color getColor(double absoluteValue, double min, double max)
{
entriesLock.readLock().lock();
try
{
return super.getColor(absoluteValue, min, max);
}
finally
{
entriesLock.readLock().unlock();
}
}
/**
* Add a new entry to the colour map
*
* @param value
* The value to add a colour at
* @param color
* The colour to add at that value
*/
public void addEntry(double value, Color color)
{
Entry<Double, Color> entry = null;
entriesLock.writeLock().lock();
try
{
entries.put(value, color);
entry = getEntry(value);
}
finally
{
entriesLock.writeLock().unlock();
}
propertyChange.firePropertyChange(ENTRY_ADDED_EVENT, null, entry);
propertyChange.firePropertyChange(COLOR_MAP_ENTRY_CHANGE_EVENT, null, value);
}
/**
* Remove an entry from the colour map
*
* @param value
* The value to remove
*/
public void removeEntry(double value)
{
Entry<Double, Color> entry = null;
entriesLock.writeLock().lock();
try
{
entry = getEntry(value);
entries.remove(value);
}
finally
{
entriesLock.writeLock().unlock();
}
propertyChange.firePropertyChange(ENTRY_REMOVED_EVENT, entry, null);
propertyChange.firePropertyChange(COLOR_MAP_ENTRY_CHANGE_EVENT, value, null);
}
/**
* Move an entry from its current value to a new value
*
* @param oldValue
* The old value of the entry
* @param newValue
* The new value of the entry
*/
public void moveEntry(double oldValue, double newValue)
{
if (oldValue == newValue)
{
return;
}
entriesLock.readLock().lock();
try
{
if (!entries.containsKey(oldValue))
{
return;
}
}
finally
{
entriesLock.readLock().unlock();
}
Entry<Double, Color> oldEntry = null;
Entry<Double, Color> newEntry = null;
entriesLock.writeLock().lock();
try
{
oldEntry = getEntry(oldValue);
entries.put(newValue, oldEntry.getValue());
entries.remove(oldValue);
newEntry = getEntry(newValue);
}
finally
{
entriesLock.writeLock().unlock();
}
propertyChange.firePropertyChange(ENTRY_MOVED_EVENT, oldEntry, newEntry);
propertyChange.firePropertyChange(COLOR_MAP_ENTRY_CHANGE_EVENT, oldValue, newValue);
}
/**
* Change the colour associated with the given value (if there is one)
*
* @param value
* The value to change the colour for
* @param newColor
* The colour to change to
*/
public void changeColor(double value, Color newColor)
{
Entry<Double, Color> oldEntry = null;
Entry<Double, Color> newEntry = null;
entriesLock.writeLock().lock();
try
{
if (!entries.containsKey(value))
{
return;
}
oldEntry = getEntry(value);
entries.put(value, newColor);
newEntry = getEntry(value);
}
finally
{
entriesLock.writeLock().unlock();
}
propertyChange.firePropertyChange(COLOR_CHANGED_EVENT, oldEntry, newEntry);
propertyChange.firePropertyChange(COLOR_MAP_ENTRY_CHANGE_EVENT, value, null);
}
/**
* Set the interpolation mode on this colour map
*/
public void setMode(InterpolationMode mode)
{
propertyChange.firePropertyChange(MODE_CHANGE_EVENT, this.mode, this.mode = mode == null ? this.mode : mode);
}
/**
* Set whether values are to be interpreted as percentages
*
* @param valuesArePercentages
* Whether values are to be interpreted as percentages
*/
public void setValuesArePercentages(boolean valuesArePercentages, double minValue, double maxValue)
{
if (this.valuesArePercentages == valuesArePercentages)
{
return;
}
this.valuesArePercentages = valuesArePercentages;
Map<Double, Color> newMap = new HashMap<Double, Color>();
// Adjust the values in the map to change to/from percentage values as required
for (Entry<Double, Color> e : entries.entrySet())
{
Double newValue = null;
if (valuesArePercentages)
{
newValue = toPercentage(e.getKey(), minValue, maxValue);
}
else
{
newValue = fromPercentage(e.getKey(), minValue, maxValue);
}
newMap.put(newValue, e.getValue());
}
entriesLock.writeLock().lock();
try
{
entries.clear();
entries.putAll(newMap);
}
finally
{
entriesLock.writeLock().unlock();
}
propertyChange.firePropertyChange(VALUE_TYPE_CHANGE_EVENT, !valuesArePercentages, valuesArePercentages);
}
private static double toPercentage(double value, double minValue, double maxValue)
{
return (value - minValue) / (maxValue - minValue);
}
private static double fromPercentage(double percentage, double minValue, double maxValue)
{
return percentage * (maxValue - minValue) + minValue;
}
/**
* Set the name on this color map
*/
public void setName(String name)
{
propertyChange.firePropertyChange(NAME_CHANGE_EVENT, this.name,
this.name = name == null ? createDefaultName() : name);
}
/**
* Set the description on this color map
*/
public void setDescription(String description)
{
propertyChange.firePropertyChange(DESCRIPTION_CHANGE_EVENT, this.description, this.description = description);
}
/**
* Set the nodata colour on this map
*/
public void setNodataColour(Color nodata)
{
propertyChange.firePropertyChange(NODATA_CHANGE_EVENT, this.nodataColour, this.nodataColour = nodata);
}
/**
* Create an immutable snapshot of this mutable map in its current state
*
* @return a new immutable snapshot of this map in its current state
*/
public ColorMap snapshot()
{
entriesLock.readLock().lock();
try
{
return new ColorMap(name, description, entries, nodataColour, mode, valuesArePercentages);
}
finally
{
entriesLock.readLock().unlock();
}
}
/**
* @see PropertyChangeSupport#addPropertyChangeListener(PropertyChangeListener)
*/
public void addPropertyChangeListener(PropertyChangeListener listener)
{
propertyChange.addPropertyChangeListener(listener);
}
/**
* @see PropertyChangeSupport#removePropertyChangeListener(PropertyChangeListener)
*/
public void removePropertyChangeListener(PropertyChangeListener listener)
{
propertyChange.removePropertyChangeListener(listener);
}
/**
* @see PropertyChangeSupport#addPropertyChangeListener(String,
* PropertyChangeListener)
*/
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener)
{
propertyChange.addPropertyChangeListener(propertyName, listener);
}
/**
* @see PropertyChangeSupport#removePropertyChangeListener(String,
* PropertyChangeListener)
*/
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener)
{
propertyChange.removePropertyChangeListener(propertyName, listener);
}
}