/*
* RapidMiner
*
* Copyright (C) 2001-2014 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.gui.new_plotter.configuration;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import com.rapidminer.gui.new_plotter.ChartConfigurationException;
import com.rapidminer.gui.new_plotter.PlotConfigurationError;
import com.rapidminer.gui.new_plotter.StaticDebug;
import com.rapidminer.gui.new_plotter.configuration.DataTableColumn.ValueType;
import com.rapidminer.gui.new_plotter.configuration.SeriesFormat.VisualizationType;
import com.rapidminer.gui.new_plotter.configuration.SeriesFormat.StackingMode;
import com.rapidminer.gui.new_plotter.configuration.event.AxisParallelLinesConfigurationChangeEvent;
import com.rapidminer.gui.new_plotter.listener.RangeAxisConfigListener;
import com.rapidminer.gui.new_plotter.listener.ValueRangeListener;
import com.rapidminer.gui.new_plotter.listener.ValueSourceListener;
import com.rapidminer.gui.new_plotter.listener.events.DimensionConfigChangeEvent;
import com.rapidminer.gui.new_plotter.listener.events.RangeAxisConfigChangeEvent;
import com.rapidminer.gui.new_plotter.listener.events.RangeAxisConfigChangeEvent.RangeAxisConfigChangeType;
import com.rapidminer.gui.new_plotter.listener.events.ValueRangeChangeEvent;
import com.rapidminer.gui.new_plotter.listener.events.ValueRangeChangeEvent.ValueRangeChangeType;
import com.rapidminer.gui.new_plotter.listener.events.ValueSourceChangeEvent;
import com.rapidminer.gui.new_plotter.utility.DataStructureUtils;
import com.rapidminer.gui.new_plotter.utility.ListUtility;
import com.rapidminer.gui.new_plotter.utility.NumericalValueRange;
import com.rapidminer.tools.I18N;
/**
* This class represents a range axis of a plot. It contains a list of {@link ValueSource}s that are
* displayed on the same range axis.
*
* @author Nils Woehler, Marius Helf
*
*/
public class RangeAxisConfig implements ValueSourceListener, ValueRangeListener, Cloneable, AxisParallelLinesConfigurationListener {
private List<ValueSource> valueSourceList = new LinkedList<ValueSource>();
private String name = "";
private boolean autoNaming = false;
private boolean logarithmicAxis = false;
private NumericalValueRange userDefinedRange;
private boolean useUserDefinedLowerBound;
private boolean useUserDefinedUpperBound;
private AxisParallelLinesConfiguration crosshairLines = new AxisParallelLinesConfiguration();
public static final double padFactor = 0.05;
public static final double logPadFactor = 0.15;
private transient List<WeakReference<RangeAxisConfigListener>> listeners = new LinkedList<WeakReference<RangeAxisConfigListener>>();
private final int Id;
/**
* Constructor that sets the name of the range axis. If name is <code>null</code> auto naming
* will be enabled. If {@link SeriesFormat} is <code>null</code> the value sources default series format will be used.
*/
public RangeAxisConfig(String name, PlotConfiguration plotConfig) {
this(name, plotConfig.getNextId());
}
/**
* Private C'tor that is used for cloning.
*/
private RangeAxisConfig(String name, int Id) {
this.Id = Id;
if (name == null) {
this.autoNaming = true;
setAutoLabelIfEnabled();
} else {
this.name = name;
}
userDefinedRange = new NumericalValueRange(0, 1, -1);
userDefinedRange.addValueRangeListener(this);
crosshairLines.addAxisParallelLinesConfigurationListener(this);
}
/**
* Returns the name of the {@link RangeAxisConfig}. Maybe <code>null</code>.
*/
public String getLabel() {
return name;
}
/**
* Sets the name of the plot range axis
*/
public void setLabel(String name) {
if (name == null ? name != this.name : !name.equals(this.name)) {
this.name = name;
fireLabelChanged();
}
}
/**
* Adds a {@link PlotValueConfig} to the list of {@link ValueSource}s on this axis.
*
* @param seriesFormat
* if this is <code>null</code> the value sources default series format is used. Otherwise this series format will be set for the new value source.
*
*/
public void addValueSource(int index, ValueSource valueSource, SeriesFormat seriesFormat) { //throws ChartConfigurationException {
StaticDebug.debug("RangeAxisConfig: ADDING VALUE SOURCE " + valueSource + " (id: "+valueSource.getId()+") to RangeAxis with ID " + this.getId());
if (seriesFormat != null) {
valueSource.setSeriesFormat(seriesFormat);
}
valueSourceList.add(index, valueSource);
valueSource.addValueSourceListener(this);
setAutoLabelIfEnabled();
fireAdded(index, valueSource);
if (getValueType() == ValueType.DATE_TIME) {
setUseUserDefinedLowerViewBound(false);
setUseUserDefinedUpperViewBound(false);
}
}
/**
* Adds a {@link PlotValueConfig} to the list of {@link ValueSource}s on this axis.
*
* It is mandatory that all {@link ValueSource}s have the same {@link ValueType}. Because of
* this a {@link ChartConfigurationException} is thrown if the {@link ValueType} of the
* {@link ValueSource} differs from other existing {@link ValueSource}'s {@link ValueType}s.
*
* To check if adding a value source is possible call isAddingValueTypePossible(ValueType type).
*
* @param adaptSeriesFormat
* if this is <code>true</code> a new automatic created series format will be set for
* the new value source
*/
public void addValueSource(ValueSource valueSource, SeriesFormat seriesFormat) { //throws ChartConfigurationException {
addValueSource(valueSourceList.size(), valueSource, seriesFormat);
}
/**
* Removes a {@link ValueSource} from the list of plot value configs on this axis.
*
* @param valueSource
*/
public void removeValueSource(ValueSource valueSource) {
int idx = valueSourceList.indexOf(valueSource);
removeValueSource(idx);
}
public void removeValueSource(int index) {
ValueSource cachedValueSource = valueSourceList.get(index);
cachedValueSource.removeValueSourceListener(this);
valueSourceList.remove(index);
setAutoLabelIfEnabled();
fireRemoved(index, cachedValueSource);
if (getValueType() == ValueType.DATE_TIME) {
setUseUserDefinedLowerViewBound(false);
setUseUserDefinedUpperViewBound(false);
}
}
public void changeIndex(int index, ValueSource source) {
if (ListUtility.changeIndex(valueSourceList, source, index)) {
setAutoLabelIfEnabled();
fireMoved(index, source);
}
}
/**
* Returns all plot value configs on this range axis.
*/
public List<ValueSource> getValueSources() {
return valueSourceList;
}
public void addRangeAxisConfigListener(RangeAxisConfigListener l) {
listeners.add(new WeakReference<RangeAxisConfigListener>(l));
}
public void removeRangeAxisConfigListener(RangeAxisConfigListener l) {
Iterator<WeakReference<RangeAxisConfigListener>> it = listeners.iterator();
while (it.hasNext()) {
RangeAxisConfigListener listener = it.next().get();
if (listener == null || listener == l) {
it.remove();
}
}
}
/**
* Unsubscripes all listeners from value sources and clears value source list afterwards.
*/
public void clearValueSources() {
for (ValueSource valueSource : valueSourceList) {
valueSource.removeAllListeners();
}
valueSourceList.clear();
setAutoLabelIfEnabled();
fireCleared();
}
/**
* Returns the {@link ValueType} of this {@link RangeAxisConfig}.
*
* All {@link ValueSource}s mandatory have to be of the same {@link ValueType}. If that is not
* true, INVALID is returned.
*
* If there are no {@link ValueSource}s UNKNOWN is returned.
*
*/
public ValueType getValueType() {
ValueType valueType = ValueType.UNKNOWN;
for (ValueSource valueSource : getValueSources()) {
ValueType valueSourceValueType = valueSource.getValueType();
if (valueSourceValueType != valueType) {
if (valueType == ValueType.UNKNOWN) {
valueType = valueSourceValueType;
} else {
return ValueType.INVALID;
}
}
}
return valueType;
}
private void setAutoLabelIfEnabled() {
if (isAutoNaming()) {
StringBuilder stringBuilder = new StringBuilder();
for (ValueSource valueSource : valueSourceList) {
stringBuilder.append(valueSource.toString());
stringBuilder.append(", ");
}
if (stringBuilder.toString().isEmpty()) {
setLabel(I18N.getMessageOrNull(I18N.getGUIBundle(), "gui.label.plotter.configuration_dialog.empty_dimension.label"));
} else {
setLabel(stringBuilder.toString().substring(0, stringBuilder.toString().length() - 2));
}
}
}
public boolean hasAbsolutStackedPlot() {
for (ValueSource valueSource : valueSourceList) {
VisualizationType seriesType = valueSource.getSeriesFormat().getSeriesType();
if (seriesType == VisualizationType.BARS || seriesType == VisualizationType.AREA) {
if (valueSource.getSeriesFormat().getStackingMode() == StackingMode.ABSOLUTE) {
return true;
}
}
}
return false;
}
public ValueSource getValueSourceById(int id) {
for (ValueSource valueSource : getValueSources()) {
if (valueSource.getId() == id) {
return valueSource;
}
}
return null;
}
/**
* @return the autoName
*/
public boolean isAutoNaming() {
return autoNaming;
}
/**
* @param autoName
* the autoName to set
*/
public void setAutoNaming(boolean autoName) {
if (autoName != this.autoNaming) {
this.autoNaming = autoName;
setAutoLabelIfEnabled();
fireRangeAxisChanged(new RangeAxisConfigChangeEvent(this, RangeAxisConfigChangeType.AUTO_NAMING, autoName));
}
}
public void setUseUserDefinedUpperViewBound(boolean useUpperBound) {
if (this.useUserDefinedUpperBound != useUpperBound) {
this.useUserDefinedUpperBound = useUpperBound;
fireValueRangeChanged(new ValueRangeChangeEvent(userDefinedRange, ValueRangeChangeType.USE_UPPER_BOUND, useUpperBound));
}
}
public boolean isUsingUserDefinedUpperViewBound() {
return useUserDefinedUpperBound;
}
public void setUseUserDefinedLowerViewBound(boolean useLowerBound) {
if (this.useUserDefinedLowerBound != useLowerBound) {
this.useUserDefinedLowerBound = useLowerBound;
fireValueRangeChanged(new ValueRangeChangeEvent(userDefinedRange, ValueRangeChangeType.USE_LOWER_BOUND, useLowerBound));
}
}
public boolean isUsingUserDefinedLowerViewBound() {
return useUserDefinedLowerBound;
}
public int getSize() {
return valueSourceList.size();
}
public NumericalValueRange getUserDefinedRange() {
return userDefinedRange;
}
private void fireCleared() {
fireRangeAxisChanged(new RangeAxisConfigChangeEvent(this));
}
private void fireMoved(int index, ValueSource valueSource) {
fireRangeAxisChanged(new RangeAxisConfigChangeEvent(this, RangeAxisConfigChangeType.VALUE_SOURCE_MOVED, valueSource, index));
}
private void fireAdded(int index, ValueSource valueSource) {
fireRangeAxisChanged(new RangeAxisConfigChangeEvent(this, RangeAxisConfigChangeType.VALUE_SOURCE_ADDED, valueSource, index));
}
private void fireRemoved(int index, ValueSource valueSource) {
fireRangeAxisChanged(new RangeAxisConfigChangeEvent(this, RangeAxisConfigChangeType.VALUE_SOURCE_REMOVED, valueSource, index));
}
private void fireLabelChanged() {
fireRangeAxisChanged(new RangeAxisConfigChangeEvent(this, getLabel()));
}
private void fireAxisScalingChanged() {
fireRangeAxisChanged(new RangeAxisConfigChangeEvent(this, RangeAxisConfigChangeType.SCALING, logarithmicAxis));
}
private void fireValueRangeChanged(ValueRangeChangeEvent e) {
fireRangeAxisChanged(new RangeAxisConfigChangeEvent(this, e));
}
private void fireRangeAxisChanged(RangeAxisConfigChangeEvent e) {
Iterator<WeakReference<RangeAxisConfigListener>> it = listeners.iterator();
while (it.hasNext()) {
RangeAxisConfigListener listener = it.next().get();
if (listener == null) {
it.remove();
} else {
listener.rangeAxisConfigChanged(e);
}
}
}
public boolean isLogarithmicAxis() {
return logarithmicAxis;
}
public void setLogarithmicAxis(boolean logarithmicAxis) {
if (logarithmicAxis != this.logarithmicAxis) {
this.logarithmicAxis = logarithmicAxis;
fireAxisScalingChanged();
}
}
private void usageTypeToColumnMapChange() {
setAutoLabelIfEnabled();
}
public void domainDimensionConfigChanged(DimensionConfigChangeEvent e) {
setAutoLabelIfEnabled();
}
private void aggregationFunctionChanged() {
setAutoLabelIfEnabled();
}
@Override
public void valueSourceChanged(ValueSourceChangeEvent e) {
switch (e.getType()) {
case AGGREGATION_FUNCTION_MAP:
aggregationFunctionChanged();
break;
case DATATABLE_COLUMN_MAP:
usageTypeToColumnMapChange();
break;
case SERIES_FORMAT_CHANGED:
// SeriesFormatChangeEvent change = e.getSeriesFormatChange();
// SeriesFormatChangeType type = change.getType();
// if (type == SeriesFormatChangeType.UTILITY_INDICATOR || type == SeriesFormatChangeType.STACKING_MODE) {
// invalidateCache();
// }
break;
case USES_GROUPING:
aggregationFunctionChanged();
break;
case AGGREGATION_WINDOWING_CHANGED:
aggregationFunctionChanged();
break;
case USE_RELATIVE_UTILITIES:
// invalidateCache();
break;
case UPDATED:
// invalidateCache();
break;
case LABEL:
setAutoLabelIfEnabled();
break;
}
if (getValueType() == ValueType.DATE_TIME) {
setUseUserDefinedLowerViewBound(false);
setUseUserDefinedUpperViewBound(false);
}
fireRangeAxisChanged(new RangeAxisConfigChangeEvent(this, e));
}
@Override
public void valueRangeChanged(ValueRangeChangeEvent e) {
fireValueRangeChanged(e);
}
public boolean mustHaveUpperBoundOne(double initialUpperBound) {
int relativePlots = 0;
for (ValueSource valueSource : valueSourceList) {
VisualizationType seriesType = valueSource.getSeriesFormat().getSeriesType();
if (seriesType == VisualizationType.BARS || seriesType == VisualizationType.AREA) {
if (valueSource.getSeriesFormat().getStackingMode() == StackingMode.RELATIVE) {
++relativePlots;
}
}
}
boolean onlyRelativePlots = (relativePlots == valueSourceList.size());
boolean hasRelativePlotsButUpperBoundBelowOne = (relativePlots > 0) && (initialUpperBound < 1.0);
return onlyRelativePlots || hasRelativePlotsButUpperBoundBelowOne;
}
public RangeAxisConfig clone() {
RangeAxisConfig clone = new RangeAxisConfig(name, Id);
clone.autoNaming = autoNaming;
clone.logarithmicAxis = logarithmicAxis;
clone.crosshairLines = crosshairLines.clone();
clone.userDefinedRange = userDefinedRange.clone();
clone.userDefinedRange.addValueRangeListener(clone);
clone.useUserDefinedLowerBound = useUserDefinedLowerBound;
clone.useUserDefinedUpperBound = useUserDefinedUpperBound;
// add value sources
for (ValueSource valueSource : valueSourceList) {
clone.addValueSource(valueSource.clone(), null);
}
return clone;
}
public void setLowerViewBound(double lowerBound) {
userDefinedRange.setLowerBound(lowerBound);
}
public void setUpperViewBound(double upperBound) {
userDefinedRange.setUpperBound(upperBound);
}
public List<PlotConfigurationError> getErrors() {
List<PlotConfigurationError> errors = new LinkedList<PlotConfigurationError>();
String label = getLabel();
if (label == null) {
label = I18N.getGUILabel("plotter.unnamed_value_label");
}
if (getValueType() == ValueType.INVALID) {
errors.add(new PlotConfigurationError("mixed_value_types_on_axis", label));
}
// check nominal mappings for compatibility
List<ValueSource> valueSources = getValueSources();
if (getValueType() == ValueType.NOMINAL) {
String columnName = null;
for (ValueSource valueSource : valueSources) {
String valueSourceColumnName = valueSource.getLabel();
if (valueSourceColumnName == null) {
valueSourceColumnName = I18N.getGUILabel("plotter.unnamed_value_label");
}
if (columnName == null) {
columnName = valueSourceColumnName;
}
if (columnName != null && !columnName.equals(valueSourceColumnName)) {
errors.add(new PlotConfigurationError("mixed_categories_on_axis", label));
break;
}
}
}
// check if lower user bound is bigger than upper bound
if (isUsingUserDefinedLowerViewBound() && isUsingUserDefinedUpperViewBound()) {
if (DataStructureUtils.greaterOrAlmostEqual(getUserDefinedRange().getLowerBound(), getUserDefinedRange().getUpperBound(), 1E-7)) {
PlotConfigurationError error = new PlotConfigurationError("min_bound_greater_max_bound", getLabel());
errors.add(error);
}
}
//check for mixed domain config value types
ValueType valueType = ValueType.UNKNOWN;
for (ValueSource valueSource : valueSources) {
ValueType valueSourceValueType = valueSource.getDomainConfigValueType();
if (valueSourceValueType != valueType) {
if (valueType == ValueType.UNKNOWN) {
valueType = valueSourceValueType;
} else {
PlotConfigurationError error = new PlotConfigurationError("mixed_domain_dimension_value_types_on_axis", getLabel());
errors.add(error);
break;
}
}
}
// add all errors from value sources
for (ValueSource valueSource : valueSourceList) {
errors.addAll(valueSource.getErrors());
}
return errors;
}
public List<PlotConfigurationError> getWarnings() {
List<PlotConfigurationError> warnings = new LinkedList<PlotConfigurationError>();
for (ValueSource valueSource : valueSourceList) {
warnings.addAll(valueSource.getWarnings());
}
return warnings;
}
@Override
public void axisParallelLineConfigurationsChanged(AxisParallelLinesConfigurationChangeEvent e) {
fireCrosshairLinesChanged(e);
}
private void fireCrosshairLinesChanged(AxisParallelLinesConfigurationChangeEvent e) {
fireRangeAxisChanged(new RangeAxisConfigChangeEvent(this, e));
}
public AxisParallelLinesConfiguration getCrossHairLines() {
return crosshairLines;
}
/**
* @return the id
*/
public int getId() {
return Id;
}
@Override
public String toString() {
return String.valueOf(getLabel());
}
}