Merge branch 'master' of xbib.org:joerg/graphics

This commit is contained in:
Jörg Prante 2021-07-30 09:45:54 +02:00
commit 5d33823074
186 changed files with 23680 additions and 184 deletions

2
.gitignore vendored
View file

@ -6,4 +6,4 @@
/.gradle
build
*~
/*.iml
*.iml

View file

@ -1,16 +1,16 @@
group = org.xbib.graphics
name = graphics
version = 4.0.2
version = 4.0.3
gradle.wrapper.version = 6.6.1
pdfbox.version = 2.0.22
jna.version = 5.7.0
pdfbox.version = 2.0.23
jna.version = 5.8.0
zxing.version = 3.3.1
reflections.version = 0.9.11
jfreechart.version = 1.5.1
batik.version = 1.13
junit.version = 5.7.1
junit4.version = 4.13
junit4.version = 4.13.2
groovy.version = 2.5.12
spock.version = 1.3-groovy-2.5
cglib.version = 3.2.5

View file

@ -29,6 +29,7 @@ task sourcesJar(type: Jar, dependsOn: classes) {
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier 'javadoc'
from javadoc.destinationDir
}
artifacts {

View file

@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.xbib.graphics.barcode.Code93;
import org.xbib.graphics.barcode.render.BarcodeGraphicsRenderer;
import org.xbib.graphics.barcode.MaxiCode;
@ -22,6 +24,7 @@ import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Locale;
@DisabledOnOs(OS.MAC)
public class EPSRendererTest {
private Locale originalDefaultLocale;

View file

@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.xbib.graphics.barcode.Code93;
import org.xbib.graphics.barcode.MaxiCode;
import org.xbib.graphics.barcode.AbstractSymbol;
@ -22,6 +24,7 @@ import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Locale;
@DisabledOnOs(OS.MAC)
public class PDFRendererTest {
private Locale originalDefaultLocale;

View file

@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.xbib.graphics.barcode.Code93;
import org.xbib.graphics.barcode.MaxiCode;
import org.xbib.graphics.barcode.AbstractSymbol;
@ -22,6 +24,7 @@ import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Locale;
@DisabledOnOs(OS.MAC)
public class SVGRendererTest {
private Locale originalDefaultLocale;

View file

@ -0,0 +1,3 @@
dependencies {
testImplementation "org.junit.vintage:junit-vintage-engine:${project.property('junit4.version')}"
}

View file

@ -0,0 +1,3 @@
module org.xbib.graphics.graphics.graph.gral {
requires java.desktop;
}

View file

@ -0,0 +1,323 @@
package org.xbib.graphics.graph.gral.data;
import org.xbib.graphics.graph.gral.data.statistics.Statistics;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
/**
* Abstract implementation of the {@code DataSource} interface.
* This class provides access to statistical information,
* administration and notification of listeners and supports
* iteration of data values.
*/
@SuppressWarnings("serial")
public abstract class AbstractDataSource implements DataSource {
/** Name of the data source. */
private String name;
/** Number of columns. */
private int columnCount;
/** Data types that are allowed in the respective columns. */
private Class<? extends Comparable<?>>[] types;
/** Set of objects that will be notified of changes to the data values. */
private transient Set<DataListener> dataListeners;
/** Statistical description of the data values. */
private transient Statistics statistics;
/**
* Iterator that returns each row of the DataSource.
*/
private class DataSourceIterator implements Iterator<Comparable<?>> {
/** Index of current column. */
private int col;
/** Index of current row. */
private int row;
/**
* Initializes a new iterator instance that starts at (0, 0).
*/
public DataSourceIterator() {
col = 0;
row = 0;
}
/**
* Returns {@code true} if the iteration has more elements.
* (In other words, returns {@code true} if {@code next}
* would return an element rather than throwing an exception.)
* @return {@code true} if the iterator has more elements.
*/
public boolean hasNext() {
return (col < getColumnCount()) && (row < getRowCount());
}
/**
* Returns the next element in the iteration.
* @return the next element in the iteration.
* @exception NoSuchElementException iteration has no more elements.
*/
public Comparable<?> next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
Comparable<?> value = get(col, row);
if (++col >= getColumnCount()) {
col = 0;
++row;
}
return value;
}
/**
* Method that theoretically removes a cell from a data source.
* However, this is not supported.
*/
public void remove() {
throw new UnsupportedOperationException();
}
}
@SuppressWarnings({"unchecked","rawtypes"})
public AbstractDataSource() {
this(null, new Class[0]);
}
/**
* Initializes a new instance with the specified name, number of columns, and
* column types.
* @param name name of the DataSource
* @param types type for each column
*/
@SuppressWarnings("unchecked")
public AbstractDataSource(String name, Class<? extends Comparable<?>>... types) {
this.name = name;
setColumnTypes(types);
dataListeners = new LinkedHashSet<>();
}
/**
* Initializes a new instance with the specified number of columns and
* column types.
* @param types type for each column
*/
@SuppressWarnings("unchecked")
public AbstractDataSource(Class<? extends Comparable<?>>... types) {
this(null, types);
}
@SuppressWarnings({"unchecked","rawtypes"})
public AbstractDataSource(Column<?>... remainingColumns) {
Class<? extends Comparable<?>>[] columnTypes = new Class[remainingColumns.length];
for (int columnIndex = 0; columnIndex < remainingColumns.length; columnIndex++) {
Column<?> column = remainingColumns[columnIndex];
columnTypes[columnIndex] = column.getType();
}
setColumnTypes(columnTypes);
dataListeners = new LinkedHashSet<>();
}
/**
* Retrieves a object instance that contains various statistical
* information on the current data source.
* @return statistical information
*/
public Statistics getStatistics() {
if (statistics == null) {
statistics = new Statistics(this);
}
return statistics;
}
@SuppressWarnings({"unchecked","rawtypes"})
public DataSource getColumnStatistics(String key) {
Class<? extends Comparable<?>>[] columnTypes = new Class[getColumnCount()];
Arrays.fill(columnTypes, Double.class);
DataTable statisticsTable = new DataTable(columnTypes);
List<Double> colStatistics = new ArrayList<>(columnTypes.length);
for (int colIndex = 0; colIndex < getColumnCount(); colIndex++) {
Column<?> col = getColumn(colIndex);
colStatistics.add(col.getStatistics(key));
}
if (!colStatistics.isEmpty()) {
statisticsTable.add(colStatistics);
}
return statisticsTable;
}
@SuppressWarnings("unchecked")
public DataSource getRowStatistics(String key) {
DataTable statisticsTable = getRowCount() != 0 ? new DataTable(Double.class) : new DataTable();
for (int rowIndex = 0; rowIndex < getRowCount(); rowIndex++) {
Record row = getRecord(rowIndex);
statisticsTable.add(new Statistics(row).get(key));
}
return statisticsTable;
}
/**
* Adds the specified {@code DataListener} to this data source.
* @param dataListener listener to be added.
*/
public void addDataListener(DataListener dataListener) {
dataListeners.add(dataListener);
}
/**
* Removes the specified {@code DataListener} from this data source.
* @param dataListener listener to be removed.
*/
public void removeDataListener(DataListener dataListener) {
dataListeners.remove(dataListener);
}
/**
* Returns an iterator over a set of elements of type T.
*
* @return an Iterator.
*/
public Iterator<Comparable<?>> iterator() {
return new DataSourceIterator();
}
/**
* Notifies all registered listeners that data values have been added.
* @param events Event objects describing all values that have been added.
*/
protected void notifyDataAdded(DataChangeEvent... events) {
List<DataListener> listeners = new LinkedList<>(dataListeners);
for (DataListener dataListener : listeners) {
dataListener.dataAdded(this, events);
}
}
/**
* Notifies all registered listeners that data values have been removed.
* @param events Event objects describing all values that have been removed.
*/
protected void notifyDataRemoved(DataChangeEvent... events) {
List<DataListener> listeners = new LinkedList<>(dataListeners);
for (DataListener dataListener : listeners) {
dataListener.dataRemoved(this, events);
}
}
/**
* Notifies all registered listeners that data values have changed.
* @param events Event objects describing all values that have changed.
*/
protected void notifyDataUpdated(DataChangeEvent... events) {
List<DataListener> listeners = new LinkedList<>(dataListeners);
for (DataListener dataListener : listeners) {
dataListener.dataUpdated(this, events);
}
}
/**
* Returns the column with the specified index.
* @param col index of the column to return
* @return the specified column of the data source
*/
@SuppressWarnings({"unchecked","rawtypes"})
@Override
public Column<?> getColumn(int col) {
Class<? extends Comparable<?>> columnType = getColumnTypes()[col];
List<Comparable<?>> columnData = new LinkedList<>();
for (int rowIndex = 0; rowIndex < getRowCount(); rowIndex++) {
Record record = getRecord(rowIndex);
columnData.add(record.get(col));
}
return new Column(columnType, columnData.toArray(new Comparable[0]));
}
@Override
public String getName() {
return name;
}
@Override
public Record getRecord(int row) {
return new Record(getRow(row).toArray(null));
}
// Allows DataTable to reuse the name property
protected void setName(String name) {
this.name = name;
}
/**
* Returns the number of columns of the data source.
* @return number of columns in the data source.
*/
public int getColumnCount() {
return columnCount;
}
/**
* Returns the data types of all columns.
* @return The data types of all column in the data source
*/
public Class<? extends Comparable<?>>[] getColumnTypes() {
return Arrays.copyOf(this.types, this.types.length);
}
/**
* Returns whether the column at the specified index contains numbers.
* @param columnIndex Index of the column to test.
* @return {@code true} if the column is numeric, otherwise {@code false}.
*/
public boolean isColumnNumeric(int columnIndex) {
if (columnIndex < 0 || columnIndex >= types.length) {
return false;
}
Class<?> columnType = types[columnIndex];
return Number.class.isAssignableFrom(columnType);
}
/**
* Sets the data types of all columns. This also changes the number of
* columns.
* @param types Data types.
*/
@SuppressWarnings("unchecked")
protected void setColumnTypes(Class<? extends Comparable<?>>... types) {
this.types = Arrays.copyOf(types, types.length);
columnCount = types.length;
}
/**
* Returns the row with the specified index.
* @param row Index of the row to return
* @return the Specified row of the data source
*/
public Row getRow(int row) {
return new Row(this, row);
}
/**
* Custom deserialization method.
* @param in Input stream.
* @throws ClassNotFoundException if a serialized class doesn't exist anymore.
* @throws IOException if there is an error while reading data from the
* input stream.
*/
private void readObject(ObjectInputStream in)
throws ClassNotFoundException, IOException {
// Normal deserialization
in.defaultReadObject();
// Handle transient fields
dataListeners = new HashSet<>();
// Statistics can be omitted. It's created using a lazy getter.
}
}

View file

@ -0,0 +1,81 @@
package org.xbib.graphics.graph.gral.data;
import org.xbib.graphics.graph.gral.data.statistics.Statistics;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
/**
* <p>Class for accessing a specific column of a data source. The data of the
* column can be accessed using the {@code get(int)} method.</p>
*
* <p>Example for accessing value at column 2, row 3 of a data source:</p>
* <pre>
* Column col = new Column(dataSource, 2);
* Number v = col.get(3);
* </pre>
*
* @see DataSource
*/
public class Column<T extends Comparable<T>> implements Iterable<T> {
private final Class<T> dataType;
private final List<T> data;
@SuppressWarnings("unchecked")
public Column(Class<T> dataType, T... data) {
this(dataType, Arrays.asList(data));
}
public Column(Class<T> dataType, Iterable<T> data) {
this.dataType = dataType;
this.data = new ArrayList<>();
for (T item : data) {
this.data.add(item);
}
}
public T get(int row) {
return row >= data.size() ? null : data.get(row);
}
public int size() {
return data.size();
}
/**
* Returns whether this column only contains numbers.
* @return {@code true} if this column is numeric, otherwise {@code false}.
*/
public boolean isNumeric() {
return Number.class.isAssignableFrom(getType());
}
public Class<? extends Comparable<?>> getType() {
return dataType;
}
public double getStatistics(String key) {
return new Statistics(data).get(key);
}
@Override
public int hashCode() {
return dataType.hashCode() ^ data.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Column)) {
return false;
}
Column<?> column = (Column<?>) obj;
return getType().equals(column.getType()) && data.equals(column.data);
}
@Override
public Iterator<T> iterator() {
return data.iterator();
}
}

View file

@ -0,0 +1,151 @@
package org.xbib.graphics.graph.gral.data;
import org.xbib.graphics.graph.gral.data.statistics.Statistics;
import java.text.MessageFormat;
import java.util.Iterator;
import java.util.Locale;
/**
* Abstract base for reading substructures of a data source, i.e. columns or
* rows. {@code DataAccessor}s are iterable and provide utility methods
* for statistics and array conversion.
* @see DataSource
*/
public abstract class DataAccessor implements Iterable<Comparable<?>> {
/** Data source that provides the values that should be accessed. */
private final DataSource source;
/** Index of current column or row. */
private final int index;
/**
* Initializes a new instance with the specified data source and an access
* index.
* @param source Data source.
* @param index Column index.
*/
public DataAccessor(DataSource source, int index) {
this.source = source;
this.index = index;
}
/**
* Returns the data source containing this column.
* @return Data source containing this column.
*/
public DataSource getSource() {
return source;
}
/**
* Returns the index to access the data source.
* @return Data index.
*/
public int getIndex() {
return index;
}
/**
* Returns the value of the data source for the specified index.
* @param index Index.
* @return Value of the accessed cell.
*/
public abstract Comparable<?> get(int index);
/**
* Returns the number of elements in this column.
* @return Number of elements
*/
public abstract int size();
@Override
public boolean equals(Object obj) {
if (!(obj instanceof DataAccessor)) {
return false;
}
DataAccessor accessor = (DataAccessor) obj;
int size = size();
if (accessor.size() != size) {
return false;
}
for (int i = 0; i < size; i++) {
Comparable<?> foreignValue = accessor.get(i);
Comparable<?> thisValue = get(i);
if (foreignValue == null) {
if (thisValue != null) {
return false;
}
continue;
}
if (!foreignValue.equals(thisValue)) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
return source.hashCode() ^ index;
}
@Override
public String toString() {
return String.format(Locale.US,
"%s[source=%s,index=%d]", //$NON-NLS-1$
getClass().getName(), getSource(), getIndex());
}
/**
* Converts the data column to an array.
* @param data Optional array as data sink.
* If array is {@code null} a new array will be created.
* @return Array with row data;
*/
public Comparable<?>[] toArray(Comparable<?>[] data) {
if (data == null) {
data = new Comparable<?>[size()];
}
if (data.length != size()) {
throw new IllegalArgumentException(MessageFormat.format(
"Array of size {0,number,integer} does not match {1,number,integer} elements.", //$NON-NLS-1$
data.length, size()));
}
for (int i = 0; i < data.length; i++) {
data[i] = get(i);
}
return data;
}
/**
* Returns the specified statistical information for this data.
* @param key Requested Statistical information.
* @return Calculated value.
*/
public double getStatistics(String key) {
Statistics statistics = new Statistics(this);
return statistics.get(key);
}
/**
* Returns an iterator over the elements of this object.
* @return an iterator.
*/
public Iterator<Comparable<?>> iterator() {
return new Iterator<Comparable<?>>() {
private int i;
public boolean hasNext() {
return i < size();
}
public Comparable<?> next() {
return get(i++);
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}

View file

@ -0,0 +1,73 @@
package org.xbib.graphics.graph.gral.data;
import java.util.EventObject;
/**
* Class that stores information on a change of a specific data value in a
* data source.
* @see DataListener
* @see DataSource
*/
@SuppressWarnings("serial")
public class DataChangeEvent extends EventObject {
/** Column of the value that has changed. */
private final int col;
/** Row of the value that has changed. */
private final int row;
/** Value before changes have been applied. */
private final Comparable<?> valOld;
/** Changed value. */
private final Comparable<?> valNew;
/**
* Initializes a new event with data source, position of the data value,
* and the values.
* @param <T> Data type of the cell that has changed.
* @param source Data source.
* @param col Columns of the value.
* @param row Row of the value.
* @param valOld Old value.
* @param valNew New value.
*/
public <T> DataChangeEvent(DataSource source, int col, int row,
Comparable<T> valOld, Comparable<T> valNew) {
super(source);
this.col = col;
this.row = row;
this.valOld = valOld;
this.valNew = valNew;
}
/**
* Returns the column index of the value that was changed.
* @return Column index of the changed value.
*/
public int getCol() {
return col;
}
/**
* Returns the row index of the value that was changed.
* @return Row index of the changed value.
*/
public int getRow() {
return row;
}
/**
* Returns the old value before it has changed.
* @return Value before the change.
*/
public Comparable<?> getOld() {
return valOld;
}
/**
* Returns the new value after the change has been applied.
* @return Value after the change.
*/
public Comparable<?> getNew() {
return valNew;
}
}

View file

@ -0,0 +1,37 @@
package org.xbib.graphics.graph.gral.data;
/**
* Interface that can be implemented to listen for changes in data sources.
* @see DataSource
*/
public interface DataListener {
/**
* Method that is invoked when data has been added.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been added.
*/
void dataAdded(DataSource source, DataChangeEvent... events);
/**
* Method that is invoked when data has been updated.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been updated.
*/
void dataUpdated(DataSource source, DataChangeEvent... events);
/**
* Method that is invoked when data has been removed.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been removed.
*/
void dataRemoved(DataSource source, DataChangeEvent... events);
}

View file

@ -0,0 +1,152 @@
package org.xbib.graphics.graph.gral.data;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.List;
/**
* Class that represents a view on several columns of a {@code DataSource}.
* @see DataSource
*/
public class DataSeries extends AbstractDataSource implements DataListener {
/** Data source that provides the columns for this data series. */
private final DataSource data;
/** Columns that should be mapped to the series. */
private final List<Integer> cols;
/**
* Constructor without name. The first column will be column
* {@code 0}, the second column {@code 1} and so on,
* whereas the value of the specified columns is the column number
* in the data source.
* @param data Data source
* @param cols Column numbers
*/
public DataSeries(DataSource data, int... cols) {
this(null, data, cols);
}
/**
* Constructor that initializes a named data series. The first column will
* be column {@code 0}, the second column {@code 1} and so on,
* whereas the value of the specified columns is the column number in the
* data source.
* @param name Descriptive name
* @param data Data source
* @param cols Column numbers
*/
@SuppressWarnings({"unchecked","rawtypes"})
public DataSeries(String name, DataSource data, int... cols) {
super(name);
this.data = data;
this.cols = new ArrayList<>();
this.data.addDataListener(this);
Class<? extends Comparable<?>>[] typesOrig = data.getColumnTypes();
Class<? extends Comparable<?>>[] types;
if (cols.length > 0) {
types = new Class[cols.length];
int t = 0;
for (int colIndex : cols) {
this.cols.add(colIndex);
types[t++] = typesOrig[colIndex];
}
} else {
for (int colIndex = 0; colIndex < data.getColumnCount(); colIndex++) {
this.cols.add(colIndex);
}
types = typesOrig;
}
setColumnTypes(types);
}
/**
* Returns the row with the specified index.
* @param col index of the column to return
* @param row index of the row to return
* @return the specified value of the data cell
*/
public Comparable<?> get(int col, int row) {
try {
int dataCol = cols.get(col);
return data.get(dataCol, row);
} catch (IndexOutOfBoundsException e) {
return null;
}
}
@Override
public int getColumnCount() {
return cols.size();
}
/**
* Returns the number of rows of the data source.
* @return number of rows in the data source.
*/
public int getRowCount() {
return data.getRowCount();
}
/**
* Method that is invoked when data has been added.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been added.
*/
public void dataAdded(DataSource source, DataChangeEvent... events) {
notifyDataAdded(events);
}
/**
* Method that is invoked when data has been updated.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been updated.
*/
public void dataUpdated(DataSource source, DataChangeEvent... events) {
notifyDataUpdated(events);
}
/**
* Method that is invoked when data has been removed.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been removed.
*/
public void dataRemoved(DataSource source, DataChangeEvent... events) {
notifyDataRemoved(events);
}
@Override
public String toString() {
return getName();
}
/**
* Custom deserialization method.
* @param in Input stream.
* @throws ClassNotFoundException if a serialized class doesn't exist anymore.
* @throws IOException if there is an error while reading data from the
* input stream.
*/
private void readObject(ObjectInputStream in)
throws ClassNotFoundException, IOException {
// Normal deserialization
in.defaultReadObject();
// Restore listeners
data.addDataListener(this);
}
}

View file

@ -0,0 +1,90 @@
package org.xbib.graphics.graph.gral.data;
import org.xbib.graphics.graph.gral.data.statistics.Statistics;
/**
* Interface for an immutable access to tabular data.
*
* @see MutableDataSource
*/
public interface DataSource extends Iterable<Comparable<?>> {
/**
* Returns the column with the specified index.
* @param col index of the column to return
* @return the specified column of the data source
*/
// It is not possible to use this function with a generic type parameter,
// due to broken type inference prior to Java 8.
Column<?> getColumn(int col);
/**
* Returns the data types of all columns.
* @return The data types of all column in the data source
*/
Class<? extends Comparable<?>>[] getColumnTypes();
/**
* Returns the row with the specified index.
* @param row index of the row to return
* @return the specified row of the data source
*/
Row getRow(int row);
/**
* Returns the value with the specified row and column index.
* @param col index of the column to return
* @param row index of the row to return
* @return the specified value of the data cell
*/
Comparable<?> get(int col, int row);
/**
* Retrieves a object instance that contains various statistical
* information on the current data source.
* @return statistical information
*/
Statistics getStatistics();
DataSource getColumnStatistics(String key);
DataSource getRowStatistics(String key);
/**
* Returns the number of rows of the data source.
* @return number of rows in the data source.
*/
int getRowCount();
/**
* Returns the name of this series.
* @return a name string
*/
String getName();
/**
* Returns the number of columns of the data source.
* @return number of columns in the data source.
*/
int getColumnCount();
/**
* Returns whether the column at the specified index contains numbers.
* @param columnIndex Index of the column to test.
* @return {@code true} if the column is numeric, otherwise {@code false}.
*/
boolean isColumnNumeric(int columnIndex);
/**
* Adds the specified {@code DataListener} to this data source.
* @param dataListener listener to be added.
*/
void addDataListener(DataListener dataListener);
/**
* Removes the specified {@code DataListener} from this data source.
* @param dataListener listener to be removed.
*/
void removeDataListener(DataListener dataListener);
Record getRecord(int row);
}

View file

@ -0,0 +1,334 @@
package org.xbib.graphics.graph.gral.data;
import org.xbib.graphics.graph.gral.data.comparators.DataComparator;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* An in-memory, random access implementation of a mutable data source using
* arrays to store its values.
*
* @see DataSource
* @see MutableDataSource
*/
public class DataTable extends AbstractDataSource implements MutableDataSource {
/** All values stored as rows of column arrays. */
private final List<Record> rows;
/**
* Comparator class for comparing two records using a
* specified set of {@code DataComparator}s.
*/
private final class RecordComparator implements Comparator<Record> {
/** Rules to use for sorting. */
private final DataComparator[] comparators;
/**
* Initializes a new instance with a specified set of
* {@code DataComparator}s.
* @param comparators Set of {@code DataComparator}s to use as rules.
*/
public RecordComparator(DataComparator[] comparators) {
this.comparators = comparators;
}
/**
* Compares two records using the rules defined by the
* {@code DataComparator}s of this instance.
* @param record1 First record to compare.
* @param record2 Second record to compare.
* @return A negative number if first argument is less than the second,
* zero if first argument is equal to the second,
* or a positive integer as the greater than the second.
*/
public int compare(Record record1, Record record2) {
for (DataComparator comparator : comparators) {
int result = comparator.compare(record1, record2);
if (result != 0) {
return result;
}
}
return 0;
}
}
public DataTable() {
rows = new ArrayList<>();
}
/**
* Initializes a new instance with the specified number of columns and
* column types.
* @param types Type for each column
*/
@SuppressWarnings("unchecked")
public DataTable(Class<? extends Comparable<?>>... types) {
super(types);
rows = new ArrayList<>();
}
/**
* Initializes a new instance with the specified number of columns and
* a single column type.
* @param cols Number of columns
* @param type Data type for all columns
*/
@SuppressWarnings({"unchecked","rawtypes"})
public DataTable(int cols, Class<? extends Comparable<?>> type) {
this();
Class<? extends Comparable<?>>[] types = new Class[cols];
Arrays.fill(types, type);
setColumnTypes(types);
}
/**
* Initializes a new instance with the column types, and data of another
* data source.
* @param source Data source to clone.
*/
public DataTable(DataSource source) {
this(source.getColumnTypes());
for (int rowIndex = 0; rowIndex < source.getRowCount(); rowIndex++) {
add(source.getRecord(rowIndex));
}
}
@SuppressWarnings("rawtypes")
public DataTable(Column... columns) {
super(columns);
rows = new ArrayList<>();
int maxRowCount = 0;
for (Column<?> column : columns) {
maxRowCount = Math.max(maxRowCount, column.size());
}
for (int rowIndex = 0; rowIndex < maxRowCount; rowIndex++) {
List<Comparable<?>> rowData = new ArrayList<>(1 + columns.length);
for (Column<?> column : columns) {
rowData.add(column.get(rowIndex));
}
rows.add(new Record(rowData));
}
}
/**
* Adds a row with the specified comparable values to the table.
* The values are added in the order they are specified. If the types of
* the table columns and the values do not match, an
* {@code IllegalArgumentException} is thrown.
* @param values values to be added as a row
* @return Index of the row that has been added.
*/
public int add(Comparable<?>... values) {
return add(Arrays.asList(values));
}
/**
* Adds a row with the specified container's elements to the table.
* The values are added in the order they are specified. If the types of
* the table columns and the values do not match, an
* {@code IllegalArgumentException} is thrown.
* @param values values to be added as a row
* @return Index of the row that has been added.
*/
public int add(List<? extends Comparable<?>> values) {
DataChangeEvent[] events;
if (values.size() != getColumnCount()) {
throw new IllegalArgumentException(MessageFormat.format(
"Wrong number of columns! Expected {0,number,integer}, got {1,number,integer}.", //$NON-NLS-1$
getColumnCount(), values.size()));
}
// Check row data types
Class<? extends Comparable<?>>[] types = getColumnTypes();
for (int colIndex = 0; colIndex < values.size(); colIndex++) {
Comparable<?> value = values.get(colIndex);
if ((value != null)
&& !(types[colIndex].isAssignableFrom(value.getClass()))) {
throw new IllegalArgumentException(MessageFormat.format(
"Wrong column type! Expected {0}, got {1}.", //$NON-NLS-1$
types[colIndex], value.getClass()));
}
}
// Add data to row
Record row = new Record(values);
events = new DataChangeEvent[row.size()];
for (int columnIndex = 0; columnIndex < row.size(); columnIndex++) {
Comparable<?> value = values.get(columnIndex);
events[columnIndex] = new DataChangeEvent(this, columnIndex, rows.size(), null, value);
}
int rowIndex;
synchronized (rows) {
rows.add(row);
rowIndex = rows.size();
}
notifyDataAdded(events);
return rowIndex - 1;
}
/**
* Adds the specified row to the table.
* The values are added in the order they are specified. If the types of
* the table columns and the values do not match, an
* {@code IllegalArgumentException} is thrown.
* @param row Row to be added
* @return Index of the row that has been added.
*/
public int add(Row row) {
List<Comparable<?>> values;
synchronized (row) {
values = new ArrayList<>(row.size());
for (Comparable<?> value : row) {
values.add(value);
}
}
return add(values);
}
public void add(Record row) {
if (row.size() != getColumnCount()) {
throw new IllegalArgumentException("Invalid element count in Record to be added. " +
"Expected: "+getColumnCount()+", got: "+row.size());
}
rows.add(row);
}
/**
* Removes a specified row from the table.
* @param row Index of the row to remove
*/
public void remove(int row) {
DataChangeEvent[] events;
synchronized (rows) {
Row r = new Row(this, row);
events = new DataChangeEvent[getColumnCount()];
for (int col = 0; col < events.length; col++) {
events[col] = new DataChangeEvent(this, col, row, r.get(col), null);
}
rows.remove(row);
}
notifyDataRemoved(events);
}
/**
* Removes the last row from the table.
*/
public void removeLast() {
DataChangeEvent[] events;
synchronized (this) {
int row = getRowCount() - 1;
Row r = new Row(this, row);
events = new DataChangeEvent[getColumnCount()];
for (int col = 0; col < events.length; col++) {
events[col] = new DataChangeEvent(this, col, row, r.get(col), null);
}
rows.remove(row);
}
notifyDataRemoved(events);
}
/**
* Deletes all rows this table contains.
*/
public void clear() {
DataChangeEvent[] events;
synchronized (this) {
int cols = getColumnCount();
int rows = getRowCount();
events = new DataChangeEvent[cols*rows];
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
events[col + row*cols] = new DataChangeEvent(
this, col, row, get(col, row), null);
}
}
this.rows.clear();
}
notifyDataRemoved(events);
}
/**
* Returns the row with the specified index.
* @param col index of the column to return
* @param row index of the row to return
* @return the specified value of the data cell
*/
public Comparable<?> get(int col, int row) {
Record r;
synchronized (rows) {
if (row >= rows.size()) {
return null;
}
r = rows.get(row);
}
if (r == null) {
return null;
}
return r.get(col);
}
/**
* Sets the value of a cell specified by its column and row indexes.
* @param <T> Data type of the cell.
* @param col Column of the cell to change.
* @param row Row of the cell to change.
* @param value New value to be set.
* @return Old value that was replaced.
*/
@SuppressWarnings("unchecked")
public <T> Comparable<T> set(int col, int row, Comparable<T> value) {
Comparable<T> old;
DataChangeEvent event = null;
synchronized (this) {
old = (Comparable<T>) get(col, row);
if (old == null || !old.equals(value)) {
Record record = rows.get(row);
ArrayList<Comparable<?>> values = new ArrayList<>(record.size());
for (Comparable<?> element : record) {
values.add(element);
}
values.set(col, value);
Record updatedRecord = new Record(values);
rows.set(row, updatedRecord);
event = new DataChangeEvent(this, col, row, old, value);
}
}
if (event != null) {
notifyDataUpdated(event);
}
return old;
}
/**
* Returns the number of rows of the data source.
* @return number of rows in the data source.
*/
public int getRowCount() {
return rows.size();
}
/**
* Sorts the table rows with the specified DataComparators.
* The row values are compared in the way the comparators are specified.
* @param comparators comparators used for sorting
*/
public void sort(final DataComparator... comparators) {
synchronized (rows) {
RecordComparator comparator = new RecordComparator(comparators);
Collections.sort(rows, comparator);
}
}
@Override
public void setName(String name) {
super.setName(name);
}
}

View file

@ -0,0 +1,60 @@
package org.xbib.graphics.graph.gral.data;
import java.util.Arrays;
/**
* Class that represents a data source containing the same value in each cell.
* It can be used for test purposes or for efficiently creating constant data.
*/
public class DummyData extends AbstractDataSource {
/** Value that will be returned for all positions in this data source. */
private final Comparable<?> value;
/** Number of columns. */
private final int cols;
/** Number of rows. */
private final int rows;
/**
* Creates a new instance with the specified number of columns
* and rows, which are filled all over with the same specified value.
* @param cols Number of columns.
* @param rows Number of rows.
* @param value Value of the cells.
*/
@SuppressWarnings({"unchecked","rawtypes"})
public DummyData(int cols, int rows, Comparable<?> value) {
this.cols = cols;
this.rows = rows;
this.value = value;
Class<? extends Comparable<?>>[] types = new Class[cols];
Arrays.fill(types, value.getClass());
setColumnTypes(types);
}
/**
* Returns the row with the specified index.
* @param col index of the column to return
* @param row index of the row to return
* @return the specified value of the data cell
*/
public Comparable<?> get(int col, int row) {
return value;
}
@Override
public int getColumnCount() {
return cols;
}
/**
* Returns the number of rows of the data source.
* @return number of rows in the data source.
*/
public int getRowCount() {
return rows;
}
}

View file

@ -0,0 +1,151 @@
package org.xbib.graphics.graph.gral.data;
/**
* <p>Class that creates a new data source which adds a leading column
* containing the row number.</p>
*
* <p>Example which creates a two column data source from a one column
* histogram:</p>
* <pre>
* DataSource hist = new Histogram2D(data, Orientation.HORIZONTAL, 10);
* DataSource hist2d = new EnumeratedData(hist);
* </pre>
*
* @see DataSource
*/
public class EnumeratedData extends AbstractDataSource
implements DataListener {
/** Data source which will be used as base for enumeration. */
private final DataSource original;
/** Value to start counting from. */
private final double offset;
/** Width of enumeration steps. */
private final double steps;
/**
* Initializes a new data source based on an original data source which
* will contain an additional column which enumerates all rows. The
* enumeration will start at a specified offset and will have a specified
* step size.
* @param original Original data source.
* @param offset Offset of enumeration
* @param steps Scaling of enumeration
*/
@SuppressWarnings({"unchecked","rawtypes"})
public EnumeratedData(DataSource original, double offset, double steps) {
this.original = original;
this.offset = offset;
this.steps = steps;
Class<? extends Comparable<?>>[] typesOrig = original.getColumnTypes();
Class<? extends Comparable<?>>[] types = new Class[typesOrig.length + 1];
System.arraycopy(typesOrig, 0, types, 1, typesOrig.length);
types[0] = Double.class;
setColumnTypes(types);
original.addDataListener(this);
}
/**
* Initializes a new data source based on an original data source which
* will contain an additional column which enumerates all rows.
* @param original Original data source.
*/
public EnumeratedData(DataSource original) {
this(original, 0, 1);
}
/**
* Returns the row with the specified index.
* @param col index of the column to return
* @param row index of the row to return
* @return the specified value of the data cell
*/
public Comparable<?> get(int col, int row) {
if (col < 1) {
return row*steps + offset;
}
return original.get(col - 1, row);
}
/**
* Returns the number of rows of the data source.
* @return number of rows in the data source.
*/
public int getRowCount() {
return original.getRowCount();
}
/**
* Method that is invoked when data has been added.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been added.
*/
public void dataAdded(DataSource source, DataChangeEvent... events) {
notifyDataAdded(takeEvents(events));
}
/**
* Method that is invoked when data has been updated.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been updated.
*/
public void dataUpdated(DataSource source, DataChangeEvent... events) {
notifyDataUpdated(takeEvents(events));
}
/**
* Method that is invoked when data has been added.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been removed.
*/
public void dataRemoved(DataSource source, DataChangeEvent... events) {
notifyDataRemoved(takeEvents(events));
}
/**
* Changes the source and the columns of the specified event objects to
* make them look as if they originated from this data source.
* @param events Original events.
* @return Changed events.
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private DataChangeEvent[] takeEvents(DataChangeEvent[] events) {
if (events == null || events.length == 0) {
return new DataChangeEvent[] {
new DataChangeEvent(this, 0, 0, null, null)
};
}
DataChangeEvent[] eventsTx = new DataChangeEvent[events.length + 1];
for (int i = 0; i < eventsTx.length; i++) {
DataChangeEvent event;
int col, row;
if (i == 0) {
// Insert an event for the generated column
event = events[0];
col = 0;
row = event.getRow();
} else {
// Process the columns of the original source
event = events[i - 1];
col = event.getCol() + 1;
row = event.getRow();
}
Comparable valOld = event.getOld();
Comparable valNew = event.getNew();
eventsTx[i] = new DataChangeEvent(
this, col, row, valOld, valNew);
}
return eventsTx;
}
}

View file

@ -0,0 +1,82 @@
package org.xbib.graphics.graph.gral.data;
import org.xbib.graphics.graph.gral.data.comparators.DataComparator;
import java.util.List;
/**
* <p>Interface for write access to tabular data. The access includes adding,
* modifying, and deleting of the data.</p>
* <p>All data can be sorted row-wise with the method
* {@code sort(DataComparator...)}. For example, this way column 1 could be
* sorted ascending and column 3 descending.
*
* @see DataSource
*/
public interface MutableDataSource extends DataSource {
/**
* Adds a row with the specified comparable values. The values are added in
* the order they are specified. If the types of the data sink columns and
* the values do not match, an {@code IllegalArgumentException} is thrown.
* @param values values to be added as a row.
* @return Index of the row that has been added.
*/
int add(Comparable<?>... values);
/**
* Adds a row with the specified container's elements to the data sink. The
* values are added in the order they are specified. If the types of the
* data sink columns and the values do not match, an
* {@code IllegalArgumentException} is thrown.
* @param values values to be added as a row.
* @return Index of the row that has been added.
*/
int add(List<? extends Comparable<?>> values);
/**
* Adds the specified row to the data sink. The values are added in the
* order they are specified. If the types of the data sink columns and the
* values do not match, an {@code IllegalArgumentException} is thrown.
* @param row Row to be added.
* @return Index of the row that has been added.
*/
int add(Row row);
/**
* Removes a specified row from the data sink.
* @param row Index of the row to remove.
*/
void remove(int row);
/**
* Removes the last row from the data sink.
*/
void removeLast();
/**
* Deletes all rows this data sink contains.
*/
void clear();
/**
* Sets the value of a cell specified by its column and row indexes.
* @param <T> Data type of the cell.
* @param col Column of the cell to change.
* @param row Row of the cell to change.
* @param value New value to be set.
* @return Old value that was replaced.
*/
<T> Comparable<T> set(int col, int row, Comparable<T> value);
/**
* Sorts the data sink rows with the specified sorting rules. The row
* values are compared in the way the comparators are specified.
* @param comparators Comparators used for sorting.
*/
void sort(final DataComparator... comparators);
/**
* Sets the name of this series.
* @param name name to be set
*/
void setName(String name);
}

View file

@ -0,0 +1,82 @@
package org.xbib.graphics.graph.gral.data;
import static java.util.Arrays.copyOf;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
public class Record implements Iterable<Comparable<?>> {
private final Comparable<?>[] values;
@SuppressWarnings("rawtypes")
public Record(List<? extends Comparable<?>> values) {
this.values = values.toArray(new Comparable[0]);
}
public Record(Comparable<?>... values) {
this.values = copyOf(values, values.length);
}
@SuppressWarnings("unchecked")
public <T extends Comparable<?>> T get(int index) {
return (T) values[index];
}
public int size() {
return values.length;
}
@Override
public Iterator<Comparable<?>> iterator() {
// More readable version using Arrays.asList is prevented by broken Generics system
List<Comparable<?>> list = new ArrayList<>(values.length);
Collections.addAll(list, values);
return list.iterator();
}
public boolean isNumeric(int index) {
return values[index] instanceof Number;
}
public Record insert(Comparable<?> value, int position) {
List<Comparable<?>> recordCopyAsList = new ArrayList<>(values.length + 1);
Collections.addAll(recordCopyAsList, values);
recordCopyAsList.add(position, value);
return new Record(recordCopyAsList);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Record)) {
return false;
}
Record record = (Record) obj;
return size() == record.size() && Arrays.equals(this.values, record.values);
}
@Override
public int hashCode() {
return Objects.hashCode(values);
}
@Override
public String toString() {
StringBuilder representation = new StringBuilder("(");
for (int elementIndex = 0; elementIndex < values.length; elementIndex++) {
Comparable<?> element = values[elementIndex];
representation.append(element);
if (elementIndex != values.length - 1) {
representation.append(", ");
}
}
representation.append(")");
return representation.toString();
}
}

View file

@ -0,0 +1,71 @@
/*
* GRAL: GRAphing Library for Java(R)
*
* (C) Copyright 2009-2019 Erich Seifert <dev[at]erichseifert.de>,
* Michael Seifert <mseifert[at]error-reports.org>
*
* This file is part of GRAL.
*
* GRAL is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* GRAL 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with GRAL. If not, see <http://www.gnu.org/licenses/>.
*/
package org.xbib.graphics.graph.gral.data;
/**
* <p>Class for easily accessing a row of a data source.</p>
*
* <p>Example:</p>
* <pre>
* Row row = new Row(data, 2);
* Number value = row.get(3);
* </pre>
*
* @see DataSource
*/
public class Row extends DataAccessor {
/** Version id for serialization. */
private static final long serialVersionUID = 2725146484866525573L;
/**
* Initializes a new instances with the specified data source and
* row index.
* @param source Data source.
* @param row Row index.
*/
public Row(DataSource source, int row) {
super(source, row);
}
@Override
public Comparable<?> get(int col) {
DataSource source = getSource();
if (source == null) {
return null;
}
return source.get(col, getIndex());
}
@Override
public int size() {
return getSource().getColumnCount();
}
/**
* Returns whether the column at the specified index contains numbers.
* @param columnIndex Index of the column to test.
* @return {@code true} if the column is numeric, otherwise {@code false}.
*/
public boolean isColumnNumeric(int columnIndex) {
return getSource().isColumnNumeric(columnIndex);
}
}

View file

@ -0,0 +1,170 @@
package org.xbib.graphics.graph.gral.data;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.List;
/**
* <p>Abstract class that represents a view on several rows of a data source.
* Implementations must implement the method {@code accept(Row)} which
* decides whether a specific row should be contained in this filtered data
* source.</p>
*
* <p>Example that keeps only every second row:</p>
* <pre>
* DataSource filtered = new RowSubset() {
* public boolean accept(Row row) {
* return row.getIndex()%2 == 0;
* }
* };
* </pre>
*/
public abstract class RowSubset extends AbstractDataSource
implements DataListener {
/** Original data source. */
private final DataSource original;
/** List of row indexes that are stored in this filtered data source. */
private transient List<Integer> accepted;
/**
* Creates a new instance with the specified data source.
* @param original DataSource to be filtered.
*/
@SuppressWarnings("unchecked")
public RowSubset(DataSource original) {
accepted = new ArrayList<>();
this.original = original;
this.original.addDataListener(this);
dataUpdated(this.original);
}
@Override
public Row getRow(int row) {
int rowOrig = accepted.get(row);
return original.getRow(rowOrig);
}
/**
* Returns the row with the specified index.
* @param col index of the column to return
* @param row index of the row to return
* @return the specified value of the data cell
*/
public Comparable<?> get(int col, int row) {
int rowOrig = accepted.get(row);
return original.get(col, rowOrig);
}
@Override
public int getColumnCount() {
return original.getColumnCount();
}
/**
* Returns the number of rows of the data source.
* @return number of rows in the data source.
*/
public int getRowCount() {
return accepted.size();
}
@Override
public Class<? extends Comparable<?>>[] getColumnTypes() {
return original.getColumnTypes();
}
/**
* Method that is invoked when data has been added.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been added.
*/
public void dataAdded(DataSource source, DataChangeEvent... events) {
dataChanged(source, events);
notifyDataAdded(events);
}
/**
* Method that is invoked when data has been updated.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been added
*/
public void dataUpdated(DataSource source, DataChangeEvent... events) {
dataChanged(source, events);
notifyDataUpdated(events);
}
/**
* Method that is invoked when data has been removed.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been removed.
*/
public void dataRemoved(DataSource source, DataChangeEvent... events) {
dataChanged(source, events);
notifyDataRemoved(events);
}
/**
* Method that is invoked when data has been added, updated, or removed.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been changed.
*/
private void dataChanged(DataSource source, DataChangeEvent... events) {
update();
}
/**
* Updates the list of accepted rows.
*/
private void update() {
accepted.clear();
for (int rowIndex = 0; rowIndex < original.getRowCount(); rowIndex++) {
Row row = original.getRow(rowIndex);
if (accept(row)) {
accepted.add(rowIndex);
}
}
}
/**
* Tests whether the specified row is accepted by this DataSubset or not.
* @param row Row to be tested.
* @return True if the row should be kept.
*/
public abstract boolean accept(Row row);
/**
* Custom deserialization method.
* @param in Input stream.
* @throws ClassNotFoundException if a serialized class doesn't exist anymore.
* @throws IOException if there is an error while reading data from the
* input stream.
*/
private void readObject(ObjectInputStream in)
throws ClassNotFoundException, IOException {
// Normal deserialization
in.defaultReadObject();
// Handle transient fields
accepted = new ArrayList<>();
// Update caches
dataUpdated(original);
// Restore listeners
original.addDataListener(this);
}
}

View file

@ -0,0 +1,52 @@
package org.xbib.graphics.graph.gral.data.comparators;
import org.xbib.graphics.graph.gral.data.Record;
/**
* Class that represents a {@code DataComparator} for comparing two records
* at a defined index for ascending order.
*/
public class Ascending extends DataComparator {
/**
* Creates a new Ascending object for sorting according to the specified
* column.
* @param col Column index to be compared.
*/
public Ascending(int col) {
super(col);
}
/**
* <p>Compares the values of two records at the specified column for order and
* returns a corresponding integer:</p>
* <ul>
* <li>a negative value means {@code record1} is smaller than {@code record2}</li>
* <li>0 means {@code record1} is equal to {@code record2}</li>
* <li>a positive value means {@code record1} is larger than {@code record2}</li>
* </ul>
* @param record1 First record
* @param record2 Second record
* @return An integer number describing the order:
* a negative value if {@code record1} is smaller than {@code record2},
* 0 if {@code record1} is equal to {@code record2},
* a positive value if {@code record1} is larger than {@code record2},
*/
@SuppressWarnings("unchecked")
public int compare(Record record1, Record record2) {
Comparable<Object> value1 = record1.get(getColumn());
Comparable<Object> value2 = record2.get(getColumn());
// null values sort as if larger than non-null values
if (value1 == null && value2 == null) {
return 0;
} else if (value1 == null) {
return 1;
} else if (value2 == null) {
return -1;
}
return value1.compareTo(value2);
}
}

View file

@ -0,0 +1,32 @@
package org.xbib.graphics.graph.gral.data.comparators;
import java.util.Comparator;
import org.xbib.graphics.graph.gral.data.Record;
/**
* Abstract implementation of a {@code Comparator} for {@code Record} objects.
* This class allows to specify the index at which the records should be
* compared.
*/
public abstract class DataComparator implements Comparator<Record> {
/** Column that should be used for comparing. */
private final int column;
/**
* Constructor.
* @param col index of the column to be compared
*/
public DataComparator(int col) {
this.column = col;
}
/**
* Returns the column to be compared.
* @return column index
*/
public int getColumn() {
return column;
}
}

View file

@ -0,0 +1,51 @@
package org.xbib.graphics.graph.gral.data.comparators;
import org.xbib.graphics.graph.gral.data.Record;
/**
* Class that represents a {@code DataComparator} for comparing two records
* at a defined index for descending order.
*/
public class Descending extends DataComparator {
/**
* Creates a new Descending object sorting according to the specified
* column.
* @param col Column index to be compared
*/
public Descending(int col) {
super(col);
}
/**
* <p>Compares the values of two records at the specified column for order and
* returns a corresponding integer:</p>
* <ul>
* <li>a negative value means {@code record1} is larger than {@code record2}</li>
* <li>0 means {@code record1} is equal to {@code record2}</li>
* <li>a positive value means {@code record1} is smaller than {@code record2}</li>
* </ul>
* @param record1 First value
* @param record2 Second value
* @return An integer number describing the order:
* a negative value if {@code record1} is larger than {@code record2},
* 0 if {@code record1} is equal to {@code record2},
* a positive value if {@code record1} is smaller than {@code record2},
*/
@SuppressWarnings("unchecked")
public int compare(Record record1, Record record2) {
Comparable<Object> value1 = record1.get(getColumn());
Comparable<Object> value2 = record2.get(getColumn());
// null values sort as if larger than non-null values
if (value1 == null && value2 == null) {
return 0;
} else if (value1 == null) {
return -1;
} else if (value2 == null) {
return 1;
}
return value2.compareTo(value1);
}
}

View file

@ -0,0 +1,43 @@
package org.xbib.graphics.graph.gral.data.filters;
import java.util.Iterator;
public class Accumulation<T extends Number & Comparable<T>> implements Filter<T> {
private final Iterable<T> data;
private static class AccumulationIterator<U extends Number> implements Iterator<Double> {
private final Iterator<U> wrappedIterator;
private double accumulatedValue;
public AccumulationIterator(Iterator<U> wrappedIterator) {
this.wrappedIterator = wrappedIterator;
accumulatedValue = 0.0;
}
@Override
public boolean hasNext() {
return wrappedIterator.hasNext();
}
@Override
public Double next() {
accumulatedValue += wrappedIterator.next().doubleValue();
return accumulatedValue;
}
@Override
public void remove() {
wrappedIterator.remove();
}
}
public Accumulation(Iterable<T> data) {
this.data = data;
}
@Override
public Iterator<Double> iterator() {
return new AccumulationIterator<>(data.iterator());
}
}

View file

@ -0,0 +1,98 @@
package org.xbib.graphics.graph.gral.data.filters;
import java.io.IOException;
import java.io.ObjectInputStream;
import org.xbib.graphics.graph.gral.data.DataSource;
import org.xbib.graphics.graph.gral.util.DataUtils;
import org.xbib.graphics.graph.gral.util.MathUtils;
/**
* <p>Class that applies a specified kernel to a data source to convolve it.</p>
* <p>Functionality includes:</p>
* <ul>
* <li>Getting and setting the {@code Kernel} used for convolution</li>
* </ul>
*/
public class Convolution extends Filter2D {
/** Kernel that provides the values to convolve the data source. */
private final Kernel kernel;
/**
* Initialized a new instance with the specified data source, convolution
* kernel, edge handling mode, and columns to be filtered.
* @param original DataSource to be filtered.
* @param kernel Kernel to be used.
* @param mode Mode of filtering.
* @param cols Column indexes.
*/
public Convolution(DataSource original, Kernel kernel, Mode mode, int... cols) {
super(original, mode, cols);
this.kernel = kernel;
filter();
}
/**
* Returns the kernel.
* @return Kernel used for convolution.
*/
public Kernel getKernel() {
return kernel;
}
@Override
protected void filter() {
clear();
for (int rowIndex = 0; rowIndex < getRowCount(); rowIndex++) {
Double[] filteredRow = new Double[getColumnCountFiltered()];
for (int colIndex = 0; colIndex < filteredRow.length; colIndex++) {
int colIndexOriginal = getIndexOriginal(colIndex);
filteredRow[colIndex] = convolve(colIndexOriginal, rowIndex);
}
add(filteredRow);
}
}
/**
* Calculates the convolved value of the data with the specified column
* and row.
* @param col Column index.
* @param row Row index.
* @return Convolved value using the set kernel.
*/
private double convolve(int col, int row) {
Kernel kernel = getKernel();
if (kernel == null) {
Comparable<?> original = getOriginal(col, row);
return DataUtils.getValueOrDefault((Number) original, Double.NaN);
}
double sum = 0.0;
for (int k = kernel.getMinIndex(); k <= kernel.getMaxIndex(); k++) {
int r = row + k;
Comparable<?> original = getOriginal(col, r);
double v = DataUtils.getValueOrDefault((Number) original, Double.NaN);
if (!MathUtils.isCalculatable(v)) {
return v;
}
sum += kernel.get(k) * v;
}
return sum;
}
/**
* Custom deserialization method.
* @param in Input stream.
* @throws ClassNotFoundException if a serialized class doesn't exist anymore.
* @throws IOException if there is an error while reading data from the
* input stream.
*/
private void readObject(ObjectInputStream in)
throws ClassNotFoundException, IOException {
// Normal deserialization
in.defaultReadObject();
// Update caches
dataUpdated(this);
}
}

View file

@ -0,0 +1,35 @@
package org.xbib.graphics.graph.gral.data.filters;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.xbib.graphics.graph.gral.util.WindowIterator;
public class ConvolutionFilter<T extends Number & Comparable<T>> implements Filter<T> {
private final List<Double> filtered;
private final Iterator<List<T>> windowIterator;
public ConvolutionFilter(Iterable<T> data, Kernel kernel) {
filtered = new LinkedList<>();
windowIterator = new WindowIterator<>(data.iterator(), kernel.size());
while (windowIterator.hasNext()) {
List<T> window = windowIterator.next();
double convolvedValue = 0.0;
for (int windowIndex = 0; windowIndex < window.size(); windowIndex++) {
int kernelIndex = windowIndex - kernel.getOffset();
convolvedValue += kernel.get(kernelIndex)*window.get(windowIndex).doubleValue();
}
filtered.add(convolvedValue);
}
}
@Override
public Iterator<Double> iterator() {
return filtered.iterator();
}
}

View file

@ -0,0 +1,25 @@
/*
* GRAL: GRAphing Library for Java(R)
*
* (C) Copyright 2009-2019 Erich Seifert <dev[at]erichseifert.de>,
* Michael Seifert <mseifert[at]error-reports.org>
*
* This file is part of GRAL.
*
* GRAL is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* GRAL 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with GRAL. If not, see <http://www.gnu.org/licenses/>.
*/
package org.xbib.graphics.graph.gral.data.filters;
public interface Filter<T extends Comparable<T>> extends Iterable<Double> {
}

View file

@ -0,0 +1,360 @@
package org.xbib.graphics.graph.gral.data.filters;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import org.xbib.graphics.graph.gral.data.AbstractDataSource;
import org.xbib.graphics.graph.gral.data.DataChangeEvent;
import org.xbib.graphics.graph.gral.data.DataListener;
import org.xbib.graphics.graph.gral.data.DataSource;
import org.xbib.graphics.graph.gral.util.MathUtils;
/**
* <p>Abstract class that provides basic functions for filtering arbitrary
* columns of a DataSource, in other words a set of one-dimensional data.</p>
*
* <p>Functionality includes:</p>
* <ul>
* <li>Different modes for filtering (see {@link Mode})</li>
* <li>Support for listening for changes of the original data</li>
* <li>Filtering of multiple columns</li>
* </ul>
*
* <p>Values of filtered columns are buffered. Access to unfiltered columns is
* delegated to the original data source. Derived classes must make sure the
* caches are updated when deserialization is done. This can be done by calling
* {@code dataUpdated(this)} in a custom deserialization method.</p>
*/
public abstract class Filter2D extends AbstractDataSource
implements DataListener {
/** Type to define the behavior when engaging the borders of a column, i.e.
the filter would need more data values than available. */
public enum Mode {
/** Ignore missing values. */
OMIT,
/** Treat missing values as zero. */
ZERO,
/** Repeat the last value. */
REPEAT,
/** Mirror values at the last value. */
MIRROR,
/** Repeat the data. */
CIRCULAR
}
/** Original data source. */
private final DataSource original;
/** Columns that should be filtered. */
private final int[] cols;
/** Data that was produced by the filter. */
private transient ArrayList<Double[]> rows;
/** Mode for handling. */
private Mode mode;
/**
* Initializes a new instance with the specified data source, border
* handling and columns to be filtered. The columns must be numeric,
* otherwise an {@code IllegalArgumentException} is thrown.
* @param original Data source to be filtered.
* @param mode Border handling mode to be used.
* @param cols Indexes of numeric columns to be filtered.
*/
@SuppressWarnings("unchecked")
public Filter2D(DataSource original, Mode mode, int... cols) {
this.rows = new ArrayList<>(original.getRowCount());
this.original = original;
this.mode = mode;
this.cols = Arrays.copyOf(cols, cols.length);
// A sorted array is necessary for binary search
Arrays.sort(this.cols);
// Check if columns are numeric
Class<? extends Comparable<?>>[] originalColumnTypes =
original.getColumnTypes();
for (int colIndex : this.cols) {
if (!original.isColumnNumeric(colIndex)) {
throw new IllegalArgumentException(MessageFormat.format(
"Column {0,number,integer} isn't numeric and cannot be filtered.", //$NON-NLS-1$
colIndex));
}
}
Class<? extends Comparable<?>>[] types = originalColumnTypes;
for (int colIndex : this.cols) {
types[colIndex] = Double.class;
}
setColumnTypes(types);
this.original.addDataListener(this);
dataUpdated(this.original);
}
/**
* Returns the original data source that is filtered.
* @return Original data source.
*/
protected DataSource getOriginal() {
return original;
}
/**
* Returns the value of the original data source at the specified column
* and row.
* @param col Column index.
* @param row Row index.
* @return Original value.
*/
protected Comparable<?> getOriginal(int col, int row) {
int rowLast = original.getRowCount() - 1;
if (row < 0 || row > rowLast) {
if (getMode() == Mode.OMIT) {
return Double.NaN;
} else if (getMode() == Mode.ZERO) {
return 0.0;
} else if (getMode() == Mode.REPEAT) {
row = MathUtils.limit(row, 0, rowLast);
} else if (getMode() == Mode.MIRROR) {
int rem = Math.abs(row) / rowLast;
int mod = Math.abs(row) % rowLast;
if ((rem & 1) == 0) {
row = mod;
} else {
row = rowLast - mod;
}
} else if (getMode() == Mode.CIRCULAR) {
if (row >= 0) {
row = row % (rowLast + 1);
} else {
row = (row + 1) % (rowLast + 1) + rowLast;
}
}
}
return original.get(col, row);
}
/**
* Clears this Filter2D.
*/
protected void clear() {
rows.clear();
}
/**
* Adds the specified row data to this Filter2D.
* @param rowData Row data to be added.
*/
protected void add(Double[] rowData) {
rows.add(rowData);
}
/**
* Adds the specified row data to this Filter2D.
* @param rowData Row to be added.
*/
protected void add(Number[] rowData) {
Double[] doubleData = new Double[rowData.length];
int i = 0;
for (Number value : rowData) {
doubleData[i++] = value.doubleValue();
}
rows.add(doubleData);
}
/**
* Returns the row with the specified index.
* @param col index of the column to return
* @param row index of the row to return
* @return the specified value of the data cell
*/
public Comparable<?> get(int col, int row) {
int colPos = getIndex(col);
if (colPos < 0) {
return original.get(col, row);
}
return rows.get(row)[colPos];
}
/**
* Sets a new value for a specified cell.
* @param col Column of the cell.
* @param row Row of the cell.
* @param value New cell value.
* @return The previous value before it has been changed.
*/
protected Number set(int col, int row, Double value) {
int colPos = getIndex(col);
if (colPos < 0) {
throw new IllegalArgumentException(
"Can't set value in unfiltered column."); //$NON-NLS-1$
}
Double old = rows.get(row)[colPos];
rows.get(row)[colPos] = value;
notifyDataUpdated(new DataChangeEvent(this, col, row, old, value));
return old;
}
@Override
public int getColumnCount() {
return original.getColumnCount();
}
/**
* Returns the number of filtered columns.
* @return Number of filtered columns.
*/
protected int getColumnCountFiltered() {
if (cols.length == 0) {
return original.getColumnCount();
}
return cols.length;
}
/**
* Returns the number of rows of the data source.
* @return number of rows in the data source.
*/
public int getRowCount() {
return original.getRowCount();
}
/**
* Returns the number of filtered rows.
* @return Number of filtered rows.
*/
protected int getRowCountFiltered() {
return original.getRowCount();
}
/**
* Method that is invoked when data has been added.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been added.
*/
public void dataAdded(DataSource source, DataChangeEvent... events) {
dataChanged(source, events);
notifyDataAdded(events);
}
/**
* Method that is invoked when data has been updated.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed
* @param events Optional event object describing the data values that
* have been updated.
*/
public void dataUpdated(DataSource source, DataChangeEvent... events) {
dataChanged(source, events);
notifyDataUpdated(events);
}
/**
* Method that is invoked when data has been removed.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed
* @param events Optional event object describing the data values that
* have been removed.
*/
public void dataRemoved(DataSource source, DataChangeEvent... events) {
dataChanged(source, events);
notifyDataRemoved(events);
}
/**
* Method that is invoked when data has been added, updated, or removed.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed
* @param events Optional event object describing the data values that
* have been removed.
*/
private void dataChanged(DataSource source, DataChangeEvent... events) {
filter();
}
/**
* Returns the index of the original column using the index of the
* filtered column.
* @param col Index of the filtered column
* @return Index of the original column
*/
protected int getIndexOriginal(int col) {
if (cols.length == 0) {
return col;
}
return cols[col];
}
/**
* Returns the index of the filtered column using the index of the
* original column.
* @param col Index of the original column
* @return Index of the filtered column
*/
protected int getIndex(int col) {
if (cols.length == 0) {
return col;
}
return Arrays.binarySearch(cols, col);
}
/**
* Returns whether the specified column is filtered.
* @param col Column index.
* @return True, if the column is filtered.
*/
protected boolean isFiltered(int col) {
return getIndex(col) >= 0;
}
/**
* Invokes the filtering routine.
*/
protected abstract void filter();
/**
* Returns the Mode of this Filter2D.
* @return Mode of filtering.
*/
public Mode getMode() {
return mode;
}
/**
* Sets the Mode the specified value.
* @param mode Mode of filtering.
*/
public void setMode(Mode mode) {
this.mode = mode;
dataUpdated(this);
}
/**
* Custom deserialization method.
* @param in Input stream.
* @throws ClassNotFoundException if a serialized class doesn't exist anymore.
* @throws IOException if there is an error while reading data from the
* input stream.
*/
private void readObject(ObjectInputStream in)
throws ClassNotFoundException, IOException {
// Normal deserialization
in.defaultReadObject();
// Handle transient fields
rows = new ArrayList<>();
// Update caches
original.addDataListener(this);
}
}

View file

@ -0,0 +1,228 @@
package org.xbib.graphics.graph.gral.data.filters;
import java.util.Arrays;
/**
* <p>Class that represents an one dimensional array of coefficients for a
* weighted filtering.</p>
* <p>Functionality includes:</p>
* <ul>
* <li>Adding of other kernels or scalars</li>
* <li>Multiplication with other kernels or scalars</li>
* <li>Normalization</li>
* <li>Negation</li>
* </ul>
*/
public class Kernel {
/** Kernel values. */
private final double[] values;
/** Index of the kernel's center value. */
private final int offset;
/**
* Creates a new Kernel object with the specified offset and values.
* @param offset Offset to the first item in the kernel.
* @param values Array of values in the kernel.
*/
public Kernel(int offset, double[] values) {
this.values = Arrays.copyOf(values, values.length);
this.offset = offset;
}
/**
* Creates a new kernel object with the specified values and an offset
* being half the size of this kernel (rounded down).
* @param values Data values for the kernel.
*/
public Kernel(double... values) {
this(values.length/2, values);
}
/**
* Returns a Kernel of specified variance with binomial coefficients.
* @param variance Variance.
* @return Kernel.
*/
public static Kernel getBinomial(double variance) {
int size = (int) (variance * 4.0) + 1;
return getBinomial(size);
}
/**
* Returns a Kernel of specified size with binomial coefficients.
* @param size Size of the Kernel.
* @return Kernel.
*/
public static Kernel getBinomial(int size) {
double[] values = new double[size];
values[0] = 1.0;
for (int i = 0; i < size - 1; i++) {
values[0] /= 2.0;
}
for (int i = 0; i < size; i++) {
for (int j = i; j > 0; j--) {
values[j] += values[j - 1];
}
}
return new Kernel(values);
}
/**
* Returns a Kernel with the specified size and offset, filled with a
* single value.
* @param size Size.
* @param offset Offset.
* @param value Value the Kernel is filled with.
* @return Kernel.
*/
public static Kernel getUniform(int size, int offset, double value) {
double[] values = new double[size];
Arrays.fill(values, value);
return new Kernel(offset, values);
}
/**
* Returns the value at the specified position of this kernel.
* If the position exceeds the minimum or maximum index, 0.0 is
* returned.
* @param i Index to be returned.
* @return Value at the specified index.
*/
public double get(int i) {
if (i < getMinIndex() || i > getMaxIndex()) {
return 0.0;
}
return values[i - getMinIndex()];
}
/**
* Sets the specified index of this kernel to the specified value.
* @param i Index to be changed.
* @param v Value to be set.
*/
protected void set(int i, double v) {
if (i < getMinIndex() || i > getMaxIndex()) {
return;
}
values[i - getMinIndex()] = v;
}
/**
* Returns the offset of this kernel.
* @return Offset.
*/
public int getOffset() {
return offset;
}
/**
* Returns the number of values in this kernel.
* @return Number of values.
*/
public int size() {
return values.length;
}
/**
* Returns the index of the "leftmost" value.
* @return Minimal index.
*/
public int getMinIndex() {
return -getOffset();
}
/**
* Returns the index of the "rightmost" value.
* @return Maximal index.
*/
public int getMaxIndex() {
return size() - getOffset() - 1;
}
/**
* Returns a new Kernel, where the specified value was added to each of
* the items.
* @param v Value to be added.
* @return Kernel with new values.
*/
public Kernel add(double v) {
for (int i = 0; i < values.length; i++) {
values[i] += v;
}
return this;
}
/**
* Returns a new Kernel, where the specified kernel was added.
* @param k Kernel to be added.
* @return Kernel with new values.
*/
public Kernel add(Kernel k) {
int min = getMinIndex();
int max = getMaxIndex();
if (size() > k.size()) {
min = k.getMinIndex();
max = k.getMaxIndex();
}
for (int i = min; i <= max; i++) {
set(i, get(i) + k.get(i));
}
return this;
}
/**
* Returns a new Kernel, where the specified value was multiplied with
* each of the items.
* @param v Value to be multiplied.
* @return Kernel with new values.
*/
public Kernel mul(double v) {
for (int i = 0; i < values.length; i++) {
values[i] *= v;
}
return this;
}
/**
* Returns a new Kernel, where the specified kernel was multiplied.
* @param k Kernel to be multiplied.
* @return Kernel with new values.
*/
public Kernel mul(Kernel k) {
int min = getMinIndex();
int max = getMaxIndex();
if (size() > k.size()) {
min = k.getMinIndex();
max = k.getMaxIndex();
}
for (int i = min; i <= max; i++) {
set(i, get(i) * k.get(i));
}
return this;
}
/**
* Returns a normalized Kernel so that the sum of all values equals 1.
* @return Normalized Kernel.
*/
public Kernel normalize() {
double sum = 0.0;
for (double value : values) {
sum += value;
}
return mul(1.0/sum);
}
/**
* Returns a Kernel with all values being negated.
* @return Negated Kernel.
*/
public Kernel negate() {
mul(-1.0);
return this;
}
}

View file

@ -0,0 +1,162 @@
package org.xbib.graphics.graph.gral.data.filters;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.List;
import org.xbib.graphics.graph.gral.data.DataSource;
import org.xbib.graphics.graph.gral.util.MathUtils;
/**
* <p>Class that calculates the median of a data sequence.</p>
* <ul>
* <li>Setting and getting offset</li>
* <li>Setting and getting window size</li>
* </ul>
*/
public class Median extends Filter2D {
/** Number of values in the window that will be used to calculate the
median. */
private int windowSize;
/** Start of the window. */
private int offset;
/**
* Creates a new Median object with the specified DataSource, window
* size, offset, Mode, and columns.
* @param original DataSource to be filtered.
* @param windowSize Number of rows to be used for the calculation of the
* median.
* @param offset Offset from the current filtered value to the last value
* of the window.
* @param mode Mode of filtering.
* @param cols Column indexes.
*/
public Median(DataSource original, int windowSize, int offset,
Mode mode, int... cols) {
super(original, mode, cols);
this.windowSize = windowSize;
this.offset = offset;
filter();
}
@Override
protected void filter() {
clear();
if (getWindowSize() <= 0) {
return;
}
List<List<Double>> colWindows =
new ArrayList<>(getColumnCount());
for (int colIndex = 0; colIndex < getColumnCountFiltered(); colIndex++) {
int colIndexOriginal = getIndexOriginal(colIndex);
List<Double> window = new ArrayList<>(getWindowSize());
colWindows.add(window);
// Pre-fill window
for (int rowIndex = getOffset() - getWindowSize(); rowIndex < 0; rowIndex++) {
Comparable<?> vOrig = getOriginal(colIndexOriginal, rowIndex);
double v = ((Number) vOrig).doubleValue();
window.add(v);
}
}
for (int rowIndex = 0; rowIndex < getRowCount(); rowIndex++) {
Double[] filteredRow = new Double[getColumnCountFiltered()];
for (int colIndex = 0; colIndex < filteredRow.length; colIndex++) {
List<Double> window = colWindows.get(colIndex);
if (window.size() >= getWindowSize()) {
window.remove(0);
}
int colIndexOriginal = getIndexOriginal(colIndex);
Comparable<?> vOrig = getOriginal(colIndexOriginal,
rowIndex - getOffset() + getWindowSize());
double v = ((Number) vOrig).doubleValue();
window.add(v);
filteredRow[colIndex] = median(window);
}
add(filteredRow);
}
}
/**
* Calculates the median for the specified values in the window.
* @param w List of values the median will be calculated for.
* @return Median.
*/
private double median(List<Double> w) {
if (w.size() == 1) {
return w.get(0);
}
List<Double> window = new ArrayList<>(w.size());
for (Double v : w) {
if (!MathUtils.isCalculatable(v)) {
return Double.NaN;
}
window.add(v);
}
int medianIndex = MathUtils.randomizedSelect(
window, 0, window.size() - 1, window.size()/2);
double median = window.get(medianIndex);
if ((window.size() & 1) == 0) {
int medianUpperIndex = MathUtils.randomizedSelect(
window, 0, window.size() - 1, window.size()/2 + 1);
double medianUpper = window.get(medianUpperIndex);
median = (median + medianUpper)/2.0;
}
return median;
}
/**
* Returns the size of the window which is used to calculate the median.
* @return Number of rows used.
*/
public int getWindowSize() {
return windowSize;
}
/**
* Set the size of the window which is used to calculate the median.
* @param windowSize Number of rows used.
*/
public void setWindowSize(int windowSize) {
this.windowSize = windowSize;
dataUpdated(this);
}
/**
* Returns the offset from the current value used to calculate the
* median to the last value of the window.
* @return Offset.
*/
public int getOffset() {
return offset;
}
/**
* Sets the offset from the current value used to calculate the
* median to the last value of the window.
* @param offset Offset.
*/
public void setOffset(int offset) {
this.offset = offset;
dataUpdated(this);
}
/**
* Custom deserialization method.
* @param in Input stream.
* @throws ClassNotFoundException if a serialized class doesn't exist anymore.
* @throws IOException if there is an error while reading data from the
* input stream.
*/
private void readObject(ObjectInputStream in)
throws ClassNotFoundException, IOException {
// Normal deserialization
in.defaultReadObject();
// Update caches
dataUpdated(this);
}
}

View file

@ -0,0 +1,33 @@
package org.xbib.graphics.graph.gral.data.filters;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.xbib.graphics.graph.gral.data.statistics.Statistics;
import org.xbib.graphics.graph.gral.util.WindowIterator;
public class MedianFilter<T extends Number & Comparable<T>> implements Filter<T> {
private final List<Double> filtered;
private final Iterator<List<T>> windowIterator;
public MedianFilter(Iterable<T> data, int windowSize) {
filtered = new LinkedList<>();
windowIterator = new WindowIterator<>(data.iterator(), windowSize);
while (windowIterator.hasNext()) {
List<T> window = windowIterator.next();
Statistics windowStatistics = new Statistics(window);
double median = windowStatistics.get(Statistics.MEDIAN);
filtered.add(median);
}
}
@Override
public Iterator<Double> iterator() {
return filtered.iterator();
}
}

View file

@ -0,0 +1,187 @@
package org.xbib.graphics.graph.gral.data.filters;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Arrays;
import java.util.Iterator;
import org.xbib.graphics.graph.gral.data.Column;
import org.xbib.graphics.graph.gral.data.DataSource;
import org.xbib.graphics.graph.gral.data.DataTable;
import org.xbib.graphics.graph.gral.data.Record;
/**
* Filter2D to change the size of equally spaced data sources. All columns of the
* data sources must be numeric, otherwise an {@code IllegalArgumentException}
* will be thrown. The values of the scaled result are created by averaging.
*/
public class Resize extends Filter2D {
/** Number of columns. */
private final int cols;
/** Number of rows. */
private final int rows;
/**
* Initializes a new data source from an original data source and a
* specified number of rows and columns.
* @param data Original data source.
* @param cols Number of columns for new data source.
* @param rows Number of rows for new data source.
*/
public Resize(DataSource data, int cols, int rows) {
super(data, Mode.ZERO);
this.cols = cols;
this.rows = rows;
filter();
}
@Override
public int getColumnCount() {
if (cols <= 0) {
return super.getColumnCount();
}
return cols;
}
@Override
public int getRowCount() {
if (rows <= 0) {
return super.getRowCount();
}
return rows;
}
@Override
public Comparable<?> get(int col, int row) {
if ((cols <= 0 || cols == getOriginal().getColumnCount()) &&
(rows <= 0 || rows == getOriginal().getRowCount())) {
return getOriginal(col, row);
}
return super.get(col, row);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
protected void filter() {
clear();
DataSource original = getOriginal();
if ((getRowCount() == original.getRowCount())
&& (getColumnCount() == original.getColumnCount())) {
return;
}
DataSource data = original;
if (getRowCount() != original.getRowCount()) {
Class[] dataTypes = new Class[original.getColumnCount()];
Arrays.fill(dataTypes, Double.class);
DataTable avgRows = new DataTable(dataTypes);
fillWithEmptyRows(avgRows, getRowCount());
double step = original.getRowCount() / (double) getRowCount();
for (int colIndex = 0; colIndex < original.getColumnCount(); colIndex++) {
Column colData = original.getColumn(colIndex);
for (int rowIndex = 0; rowIndex < getRowCount(); rowIndex++) {
double start = rowIndex*step;
double end = (rowIndex + 1)*step;
avgRows.set(colIndex, rowIndex,
average(colData, start, end));
}
}
data = avgRows;
}
if (getColumnCount() != original.getColumnCount()) {
Class[] dataTypes = new Class[getColumnCount()];
Arrays.fill(dataTypes, Double.class);
DataTable avgCols = new DataTable(dataTypes);
fillWithEmptyRows(avgCols, data.getRowCount());
double step = original.getColumnCount() / (double) getColumnCount();
for (int rowIndex = 0; rowIndex < data.getRowCount(); rowIndex++) {
Record rowData = data.getRecord(rowIndex);
for (int colIndex = 0; colIndex < getColumnCount(); colIndex++) {
double start = colIndex*step;
double end = (colIndex + 1)*step;
avgCols.set(colIndex, rowIndex,
average(rowData, start, end));
}
}
data = avgCols;
}
for (int rowIndex = 0; rowIndex < data.getRowCount(); rowIndex++) {
Record row = data.getRecord(rowIndex);
Double[] rowValues = new Double[row.size()];
for (int columnIndex = 0; columnIndex < rowValues.length; columnIndex++) {
rowValues[columnIndex] = row.get(columnIndex);
}
add(rowValues);
}
}
/**
* Utility method that fills a data table with empty rows.
* @param data Data table that should be filled.
* @param count Number of rows that were added.
*/
private static void fillWithEmptyRows(DataTable data, int count) {
while (data.getRowCount() < count) {
Double[] emptyRow = new Double[data.getColumnCount()];
Arrays.fill(emptyRow, 0.0);
data.add(emptyRow);
}
}
private static <T> Iterator<T> advance(Iterator<T> iterator, int elementCount) {
for (int elementIndex = 0; elementIndex < elementCount; elementIndex++) {
iterator.next();
}
return iterator;
}
/**
* Calculates the arithmetic mean of all values between start and end.
* @param data Values.
* @param start Start index.
* @param end End index.
* @return Arithmetic mean.
*/
private static double average(Iterable<? extends Comparable<?>> data, double start, double end) {
int startFloor = (int) Math.floor(start);
int startCeil = (int) Math.ceil(start);
int endFloor = (int) Math.floor(end);
int endCeil = (int) Math.ceil(end);
double sum = 0.0;
Iterator<? extends Comparable<?>> dataIterator = data.iterator();
advance(dataIterator, startFloor);
for (int i = startFloor; i < endCeil; i++) {
Number number = (Number) dataIterator.next();
double val = number.doubleValue();
if (i == startFloor && startCeil != start) {
sum += (startCeil - start) * val;
} else if (i == endCeil - 1 && endFloor != end) {
sum += (end - endFloor) * val;
} else {
sum += val;
}
}
return sum / (end - start);
}
/**
* Custom deserialization method.
* @param in Input stream.
* @throws ClassNotFoundException if a serialized class doesn't exist anymore.
* @throws IOException if there is an error while reading data from the
* input stream.
*/
private void readObject(ObjectInputStream in)
throws ClassNotFoundException, IOException {
// Normal deserialization
in.defaultReadObject();
// Update caches
dataUpdated(this);
}
}

View file

@ -0,0 +1,25 @@
/*
* GRAL: GRAphing Library for Java(R)
*
* (C) Copyright 2009-2019 Erich Seifert <dev[at]erichseifert.de>,
* Michael Seifert <mseifert[at]error-reports.org>
*
* This file is part of GRAL.
*
* GRAL is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* GRAL 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with GRAL. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Data model classes. This package contains classes and interfaces for storing data for plots.
*/
package org.xbib.graphics.graph.gral.data;

View file

@ -0,0 +1,112 @@
package org.xbib.graphics.graph.gral.data.statistics;
import java.io.IOException;
import java.io.ObjectInputStream;
import org.xbib.graphics.graph.gral.data.AbstractDataSource;
import org.xbib.graphics.graph.gral.data.DataChangeEvent;
import org.xbib.graphics.graph.gral.data.DataListener;
import org.xbib.graphics.graph.gral.data.DataSource;
/**
* Abstract base class for histograms. Derived classes must
* make sure the {@code getColumnTypes()} method returns a correct array
* with column types.
* @see AbstractDataSource#setColumnTypes(Class...)
*/
public abstract class AbstractHistogram2D extends AbstractDataSource
implements DataListener {
/** Data source that is used to build the histogram. */
private final DataSource data;
/**
* Initializes a new histograms with a data source.
* @param data Data source to be analyzed.
*/
@SuppressWarnings("unchecked")
public AbstractHistogram2D(DataSource data) {
this.data = data;
this.data.addDataListener(this);
}
/**
* Recalculates the histogram values.
*/
protected abstract void rebuildCells();
/**
* Method that is invoked when data has been added.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been added.
*/
public void dataAdded(DataSource source, DataChangeEvent... events) {
dataChanged(source, events);
notifyDataAdded(events);
}
/**
* Method that is invoked when data has been updated.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been updated.
*/
public void dataUpdated(DataSource source, DataChangeEvent... events) {
dataChanged(source, events);
notifyDataUpdated(events);
}
/**
* Method that is invoked when data has been removed.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been removed.
*/
public void dataRemoved(DataSource source, DataChangeEvent... events) {
dataChanged(source, events);
notifyDataRemoved(events);
}
/**
* Method that is invoked when data has been added, updated, or removed.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been changed.
*/
private void dataChanged(DataSource source, DataChangeEvent... events) {
rebuildCells();
}
/**
* Returns the data source associated to this histogram.
* @return Data source
*/
public DataSource getData() {
return data;
}
/**
* Custom deserialization method.
* @param in Input stream.
* @throws ClassNotFoundException if a serialized class doesn't exist anymore.
* @throws IOException if there is an error while reading data from the
* input stream.
*/
private void readObject(ObjectInputStream in)
throws ClassNotFoundException, IOException {
// Normal deserialization
in.defaultReadObject();
// Restore listeners
data.addDataListener(this);
}
}

View file

@ -0,0 +1,71 @@
package org.xbib.graphics.graph.gral.data.statistics;
import java.util.Arrays;
import java.util.Iterator;
public class Histogram implements Iterable<Integer> {
private Iterable<Comparable<?>> data;
private Number[] breaks;
private Integer[] bins;
public Histogram(Iterable<Comparable<?>> data, int binCount) {
this(data, getEquidistantBreaks(data, binCount + 1));
}
public Histogram(Iterable<Comparable<?>> data, Number... breaks) {
if (breaks.length < 2) {
throw new IllegalArgumentException("Invalid break count: " + breaks.length +
" A histogram requires at least two breaks to form a bucket.");
}
this.data = data;
this.breaks = breaks;
int binCount = breaks.length - 1;
bins = new Integer[binCount];
Arrays.fill(bins, 0);
computeDistribution();
}
private static Number[] getEquidistantBreaks(Iterable<Comparable<?>> data, int breakCount) {
Number[] breaks = new Number[breakCount];
Statistics statistics = new Statistics(data);
double minValue = statistics.get(Statistics.MIN);
double maxValue = statistics.get(Statistics.MAX);
double range = maxValue - minValue;
int binCount = breakCount - 1;
double binWidth = range/binCount;
for (int breakIndex = 0; breakIndex < breaks.length; breakIndex++) {
breaks[breakIndex] = minValue + breakIndex*binWidth;
}
return breaks;
}
private void computeDistribution() {
for (Comparable<?> value : data) {
if (!(value instanceof Number)) {
continue;
}
for (int binIndex = 0; binIndex < bins.length; binIndex++) {
double lowerBinLimit = breaks[binIndex].doubleValue();
double upperBinLimit = breaks[binIndex + 1].doubleValue();
double doubleValue = ((Number) value).doubleValue();
if (doubleValue >= lowerBinLimit && doubleValue < upperBinLimit) {
bins[binIndex]++;
}
}
}
}
public int size() {
return breaks.length - 1;
}
public int get(int binIndex) {
return bins[binIndex];
}
@Override
public Iterator<Integer> iterator() {
return Arrays.asList(bins).iterator();
}
}

View file

@ -0,0 +1,232 @@
package org.xbib.graphics.graph.gral.data.statistics;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.xbib.graphics.graph.gral.data.DataSource;
import org.xbib.graphics.graph.gral.graphics.Orientation;
/**
* <p>View that aggregates the column values of an other data source into
* a histogram with cells. The cells size can be equally sized by defining
* a number of cells or breakpoints between histogram cells can be passed
* as an array to create unequally sized cells.</p>
* <p>For ease of use the histogram is a data source itself.</p>
*/
public class Histogram2D extends AbstractHistogram2D {
/** Direction in which all values will be aggregated. */
private final Orientation orientation;
/** Intervals that will be used for aggregation. */
private final List<Number[]> breaks;
/** Bin cells that store all aggregation counts. */
private final List<long[]> cellList;
/** Minimum values for cells. */
private transient Map<Integer, Long> cacheMin;
/** Maximum values for cells. */
private transient Map<Integer, Long> cacheMax;
private Histogram2D(DataSource data, Orientation orientation) {
super(data);
this.orientation = orientation;
breaks = new ArrayList<>();
cellList = new ArrayList<>();
cacheMin = new HashMap<>();
cacheMax = new HashMap<>();
}
/**
* Creates a new AbstractHistogram2D object with the specified DataSource and
* cell count.
* @param data DataSource so be analyzed.
* @param orientation Orientation of the histogram values.
* @param breakCount Number of subdivisions for analysis.
*/
public Histogram2D(DataSource data, Orientation orientation,
int breakCount) {
this(data, orientation);
// Create equally spaced breaks
int count = getData().getColumnCount();
if (orientation == Orientation.HORIZONTAL) {
count = getData().getRowCount();
}
for (int index = 0; index < count; index++) {
double min, max;
if (orientation == Orientation.HORIZONTAL) {
min = ((Number) getData().getRowStatistics(Statistics.MIN).get(0, index)).doubleValue();
max = ((Number) getData().getRowStatistics(Statistics.MAX).get(0, index)).doubleValue();
} else {
min = ((Number) getData().getColumnStatistics(Statistics.MIN).get(index, 0)).doubleValue();
max = ((Number) getData().getColumnStatistics(Statistics.MAX).get(index, 0)).doubleValue();
}
double delta = (max - min + Double.MIN_VALUE) / breakCount;
Number[] breaks = new Double[breakCount + 1];
for (int i = 0; i < breaks.length; i++) {
breaks[i] = min + i*delta;
}
this.breaks.add(breaks);
}
dataUpdated(getData());
}
/**
* Initializes a new histogram with the specified data source and
* subdivisions at the specified positions.
* @param data Data source to be analyzed.
* @param orientation Orientation in which the data should be sampled.
* @param breaks Values of where a subdivision should occur.
*/
public Histogram2D(DataSource data, Orientation orientation,
Number[]... breaks) {
this(data, orientation);
int count = getData().getColumnCount();
if (orientation == Orientation.HORIZONTAL) {
count = getData().getRowCount();
}
if (breaks.length != count) {
throw new IllegalArgumentException(MessageFormat.format(
"Invalid number of breaks: got {0,number,integer}, expected {1,number,integer}.", //$NON-NLS-1$
breaks.length, count));
}
Collections.addAll(this.breaks, breaks);
dataUpdated(getData());
}
/**
* (Re-)populates the cells of this AbstractHistogram2D.
*/
@Override
protected void rebuildCells() {
// FIXME Very naive implementation
cellList.clear();
cacheMin.clear();
cacheMax.clear();
// Iterate over histogram data sets
int breakIndex = 0;
for (Number[] brk : breaks) {
long[] cells = new long[brk.length - 1];
long colMin = Long.MAX_VALUE;
long colMax = Long.MIN_VALUE;
Iterable<? extends Comparable<?>> data;
if (orientation == Orientation.VERTICAL) {
data = getData().getColumn(breakIndex);
} else {
data = getData().getRecord(breakIndex);
}
// Iterate over data cells
for (Comparable<?> cell : data) {
if (!(cell instanceof Number)) {
continue;
}
Number numericCell = (Number) cell;
double val = numericCell.doubleValue();
// Iterate over histogram rows
for (int i = 0; i < brk.length - 1; i++) {
// Put the value into corresponding class
if ((val >= brk[i].doubleValue())
&& (val < brk[i + 1].doubleValue())) {
cells[i]++;
if (cells[i] > colMax) {
colMax = cells[i];
}
if (cells[i] < colMin) {
colMin = cells[i];
}
break;
}
}
}
cellList.add(cells);
cacheMin.put(breakIndex, colMin);
cacheMax.put(breakIndex, colMax);
breakIndex++;
}
}
/**
* Returns the direction in which the histogram values will be accumulated.
* @return Horizontal or vertical orientation.
*/
public Orientation getOrientation() {
return orientation;
}
/**
* Returns the minimum and maximum value of the specified cell.
* @param col Column index.
* @param cell Cell index.
* @return Extent of the cell.
*/
public Number[] getCellLimits(int col, int cell) {
Number[] breaks = this.breaks.get(col);
Number lower = breaks[cell];
Number upper = breaks[cell + 1];
return new Number[] {lower, upper};
}
/**
* Returns the row with the specified index.
* @param col index of the column to return
* @param row index of the row to return
* @return the specified value of the data cell
*/
public Comparable<?> get(int col, int row) {
return cellList.get(col)[row];
}
/**
* Returns the number of rows of the data source.
* @return number of rows in the data source.
*/
public int getRowCount() {
int rowCount = 0;
for (long[] cells : this.cellList) {
rowCount = Math.max(cells.length, rowCount);
}
return rowCount;
}
@Override
public int getColumnCount() {
return cellList.size();
}
@Override
@SuppressWarnings({"unchecked","rawtypes"})
public Class<? extends Comparable<?>>[] getColumnTypes() {
Class<? extends Comparable<?>>[] types = new Class[getColumnCount()];
Arrays.fill(types, Long.class);
return types;
}
/**
* Custom deserialization method.
* @param in Input stream.
* @throws ClassNotFoundException if a serialized class doesn't exist anymore.
* @throws IOException if there is an error while reading data from the
* input stream.
*/
private void readObject(ObjectInputStream in)
throws ClassNotFoundException, IOException {
// Normal deserialization
in.defaultReadObject();
// Handle transient fields
cacheMin = new HashMap<>();
cacheMax = new HashMap<>();
}
}

View file

@ -0,0 +1,204 @@
package org.xbib.graphics.graph.gral.data.statistics;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.xbib.graphics.graph.gral.util.DataUtils;
import org.xbib.graphics.graph.gral.util.MathUtils;
import org.xbib.graphics.graph.gral.util.SortedList;
/**
* A class that computes and stores various statistical information
* for an Iterable of values.
*/
public class Statistics {
/** Key for specifying the total number of elements.
This is the zeroth central moment: E((x - µ)^0) */
public static final String N = "n"; //$NON-NLS-1$
/** Key for specifying the sum of all values. */
public static final String SUM = "sum"; //$NON-NLS-1$
/** Key for specifying the sum of all value squares. */
public static final String SUM2 = "sum2"; //$NON-NLS-1$
/** Key for specifying the sum of all value cubics. */
public static final String SUM3 = "sum3"; //$NON-NLS-1$
/** Key for specifying the sum of all value quads. */
public static final String SUM4 = "sum4"; //$NON-NLS-1$
/** Key for specifying the minimum, i.e. the smallest value. */
public static final String MIN = "min"; //$NON-NLS-1$
/** Key for specifying the maximum, i.e. the largest value. */
public static final String MAX = "max"; //$NON-NLS-1$
/** Key for specifying the arithmetic mean of all values. */
public static final String MEAN = "mean"; //$NON-NLS-1$
/** Key for specifying the sum of squared differences.
This is identical to the second central moment: E((x - mean)^2) */
public static final String SUM_OF_DIFF_SQUARES = "M2"; //$NON-NLS-1$
/** Key for specifying the sum of squared differences.
This is identical to the third central moment: E((x - mean)^3) */
public static final String SUM_OF_DIFF_CUBICS = "M3"; //$NON-NLS-1$
/** Key for specifying the sum of squared differences.
This is identical to the fourth central moment: E((x - mean)^4) */
public static final String SUM_OF_DIFF_QUADS = "M4"; //$NON-NLS-1$
/** Key for specifying the variance of a sample. Formula:
{@code 1/(N - 1) * sumOfSquares} */
public static final String VARIANCE = "sample variance"; //$NON-NLS-1$
/** Key for specifying the population variance. Formula:
{@code 1/N * sumOfSquares} */
public static final String POPULATION_VARIANCE = "population variance"; //$NON-NLS-1$
/** Key for specifying the skewness. */
public static final String SKEWNESS = "skewness"; //$NON-NLS-1$
/** Key for specifying the kurtosis. */
public static final String KURTOSIS = "kurtosis"; //$NON-NLS-1$
/** Key for specifying the median (or 50% quantile). */
public static final String MEDIAN = "quantile50"; //$NON-NLS-1$
/** Key for specifying the 1st quartile (or 25th quantile). */
public static final String QUARTILE_1 = "quantile25"; //$NON-NLS-1$
/** Key for specifying the 2nd quartile (or 50th quantile). */
public static final String QUARTILE_2 = "quantile50"; //$NON-NLS-1$
/** Key for specifying the 3rd quartile (or 75th quantile). */
public static final String QUARTILE_3 = "quantile75"; //$NON-NLS-1$
/** Data values that are used to build statistical aggregates. */
private final Iterable<? extends Comparable<?>> data;
/** Table statistics stored by key. */
private final Map<String, Double> statistics;
/**
* Initializes a new object with the specified data values.
* @param data Data to be analyzed.
*/
public Statistics(Iterable<? extends Comparable<?>> data) {
statistics = new HashMap<>();
this.data = data;
}
/**
* Utility method that calculates basic statistics like element count, sum,
* or mean.
*
* Notes: Calculation of higher order statistics is based on formulas from
* http://people.xiph.org/~tterribe/notes/homs.html
*
* @param data Data values used to calculate statistics
* @param stats A {@code Map} that should store the new statistics.
*/
private void createBasicStats(Iterable<? extends Comparable<?>> data, Map<String, Double> stats) {
double n = 0.0;
double sum = 0.0;
double sum2 = 0.0;
double sum3 = 0.0;
double sum4 = 0.0;
double mean = 0.0;
double sumOfDiffSquares = 0.0;
double sumOfDiffCubics = 0.0;
double sumOfDiffQuads = 0.0;
for (Comparable<?> cell : data) {
if (!(cell instanceof Number)) {
continue;
}
Number numericCell = (Number) cell;
if (!MathUtils.isCalculatable(numericCell)) {
continue;
}
double val = numericCell.doubleValue();
if (!stats.containsKey(MIN) || val < stats.get(MIN)) {
stats.put(MIN, val);
}
if (!stats.containsKey(MAX) || val > stats.get(MAX)) {
stats.put(MAX, val);
}
n++;
double val2 = val*val;
sum += val;
sum2 += val2;
sum3 += val2*val;
sum4 += val2*val2;
double delta = val - mean;
double deltaN = delta/n;
double deltaN2 = deltaN*deltaN;
double term1 = delta*deltaN*(n - 1.0);
mean += deltaN;
sumOfDiffQuads += term1*deltaN2*(n*n - 3.0*n + 3.0) +
6.0*deltaN2*sumOfDiffSquares - 4.0*deltaN*sumOfDiffCubics;
sumOfDiffCubics += term1*deltaN*(n - 2.0) -
3.0*deltaN*sumOfDiffSquares;
sumOfDiffSquares += term1;
}
stats.put(N, n);
stats.put(SUM, sum);
stats.put(SUM2, sum2);
stats.put(SUM3, sum3);
stats.put(SUM4, sum4);
stats.put(MEAN, mean);
stats.put(SUM_OF_DIFF_QUADS, sumOfDiffQuads);
stats.put(SUM_OF_DIFF_CUBICS, sumOfDiffCubics);
stats.put(SUM_OF_DIFF_SQUARES, sumOfDiffSquares);
stats.put(VARIANCE, sumOfDiffSquares/(n - 1.0));
stats.put(POPULATION_VARIANCE, sumOfDiffSquares/n);
stats.put(SKEWNESS,
(sumOfDiffCubics/n)/Math.pow(sumOfDiffSquares/n, 3.0/2.0) - 3.0);
stats.put(KURTOSIS,
(n*sumOfDiffQuads)/(sumOfDiffSquares*sumOfDiffSquares) - 3.0);
}
/**
* Utility method that calculates quantiles for the given data values and
* stores the results in {@code stats}.
* @param stats {@code Map} for storing results
* @see MathUtils#quantile(List,double)
*/
private void createDistributionStats(Iterable<? extends Comparable<?>> data, Map<String, Double> stats) {
// Create sorted list of data
List<Double> values = new SortedList<>();
for (Comparable<?> cell : data) {
if (!(cell instanceof Number)) {
continue;
}
Number numericCell = (Number) cell;
double value = numericCell.doubleValue();
if (MathUtils.isCalculatable(value)) {
values.add(value);
}
}
if (values.size() <= 0) {
return;
}
stats.put(QUARTILE_1, MathUtils.quantile(values, 0.25));
stats.put(QUARTILE_2, MathUtils.quantile(values, 0.50));
stats.put(QUARTILE_3, MathUtils.quantile(values, 0.75));
stats.put(MEDIAN, stats.get(QUARTILE_2));
}
/**
* Returns the specified statistics value.
* @param key Requested information.
* @return The value for the specified key as value, or <i>NaN</i>
* if the specified statistical value does not exist
*/
public double get(String key) {
if (!statistics.containsKey(key)) {
if (MEDIAN.equals(key) || QUARTILE_1.equals(key) ||
QUARTILE_2.equals(key) || QUARTILE_3.equals(key)) {
createDistributionStats(data, statistics);
} else {
createBasicStats(data, statistics);
}
}
Double v = statistics.get(key);
return DataUtils.getValueOrDefault(v, Double.NaN);
}
}

View file

@ -0,0 +1,97 @@
package org.xbib.graphics.graph.gral.graphics;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
/**
* Abstract implementation of the {@link Drawable} interface.
* This class implements common functionality like the different ways for
* getting and setting the bounding rectangle of the drawable object.
*/
public abstract class AbstractDrawable implements Drawable {
/** Boundaries of the drawable object. */
private final Rectangle2D bounds;
/**
* Creates an AbstractDrawable.
*/
public AbstractDrawable() {
bounds = new Rectangle2D.Double();
}
/**
* Returns the bounds of this {@code Drawable}.
* @return a bounding rectangle
*/
public Rectangle2D getBounds() {
Rectangle2D b = new Rectangle2D.Double();
b.setFrame(bounds);
return b;
}
/**
* Returns the x-position of the bounds.
* @return horizontal position of the upper-left corner of the bounding
* rectangle.
*/
public double getX() {
return bounds.getX();
}
/**
* Returns the y-position of the bounds.
* @return vertical position of the upper-left corner of the bounding
* rectangle.
*/
public double getY() {
return bounds.getY();
}
/**
* Returns the width of the bounds.
* @return horizontal extent.
*/
public double getWidth() {
return bounds.getWidth();
}
/**
* Returns the height of the bounds.
* @return vertical extent.
*/
public double getHeight() {
return bounds.getHeight();
}
/**
* Sets the bounds to the specified bounding rectangle.
* @param bounds rectangle containing the component.
*/
public void setBounds(Rectangle2D bounds) {
setBounds(bounds.getX(), bounds.getY(),
bounds.getWidth(), bounds.getHeight());
}
/**
* Sets the bounds to the specified coordinates, width and height.
* This method should be used when overriding functionality.
* @param x horizontal position of the upper-left corner
* @param y vertical position of the upper-left corner
* @param width horizontal extent
* @param height vertical extent
*/
public void setBounds(double x, double y, double width, double height) {
bounds.setFrame(x, y, width, height);
}
/**
* Returns the preferred size of the {@code Drawable}.
* @return horizontal and vertical extent that wants to be reached
*/
public Dimension2D getPreferredSize() {
return new org.xbib.graphics.graph.gral.graphics.Dimension2D.Double();
}
@Override
public void setPosition(double x, double y) {
bounds.setFrame(x, y, bounds.getWidth(), bounds.getHeight());
}
}

View file

@ -0,0 +1,113 @@
package org.xbib.graphics.graph.gral.graphics;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.List;
import org.xbib.graphics.graph.gral.graphics.layout.Layout;
/**
* An interface that provides functions to build a group of multiple components
* of {@link Drawable}. It is also responsible for managing layout of its
* components using a {@link Layout} and layout constraints for each component.
*/
public interface Container extends Iterable<Drawable> {
/**
* Returns the space that this container must preserve at each of its
* edges.
* @return The insets of this DrawableContainer
*/
Insets2D getInsets();
/**
* Sets the space that this container must preserve at each of its
* edges.
* @param insets Insets to be set.
*/
void setInsets(Insets2D insets);
/**
* Returns the bounds of this container.
* @return bounds
*/
Rectangle2D getBounds();
/**
* Sets the bounds of this container.
* @param bounds Bounds
*/
void setBounds(Rectangle2D bounds);
/**
* Returns the layout associated with this container.
* @return Layout manager
*/
Layout getLayout();
/**
* Recalculates this container's layout.
*/
void layout();
/**
* Sets the layout associated with this container.
* @param layout Layout to be set.
*/
void setLayout(Layout layout);
/**
* Adds a new component to this container.
* @param drawable Component
*/
void add(Drawable drawable);
/**
* Adds a new component to this container.
* @param drawable Component
* @param constraints Additional information (e.g. for layout)
*/
void add(Drawable drawable, Object constraints);
/**
* Returns whether the specified {@code Drawable} is stored.
* @param drawable Element to be checked.
* @return {@code true} if the element is stored in the {@code Container},
* {@code false} otherwise.
*/
boolean contains(Drawable drawable);
/**
* Returns the components at the specified point.
* The first component in the result {@code List} is the most
* specific component, i.e. the component with the deepest nesting level.
* If no component could be found an empty {@code List} will be returned.
* @param point Two-dimensional point.
* @return Components at the specified point, with the deepest nested component first.
*/
List<Drawable> getDrawablesAt(Point2D point);
/**
* Returns a list of stored components.
* @return Contained drawables.
*/
List<Drawable> getDrawables();
/**
* Return additional information on component
* @param drawable Component
* @return Information object or {@code null}
*/
Object getConstraints(Drawable drawable);
/**
* Removes a component from this container.
* @param drawable Component
*/
void remove(Drawable drawable);
/**
* Returns the number of components that are stored in this container.
* @return total number of components
*/
int size();
}

View file

@ -0,0 +1,85 @@
package org.xbib.graphics.graph.gral.graphics;
import java.util.Locale;
/**
* <p>Class that stores the horizontal and vertical extent of an object.</p>
* <p>This implementation adds support of double values to
* {@code java.awt.geom.Dimension2D}.</p>
*/
public abstract class Dimension2D extends java.awt.geom.Dimension2D {
/**
* Creates a new Dimension2D object.
*/
public Dimension2D() {
}
/**
* Class that stores double values.
*/
public static class Double extends Dimension2D {
/** Horizontal extension. */
private double width;
/** Vertical extension. */
private double height;
/**
* Creates a new Dimension2D object with zero width and height.
*/
public Double() {
setSize(0.0, 0.0);
}
/**
* Creates a new Dimension2D object with the specified width and
* height.
* @param width Width.
* @param height Height.
*/
public Double(double width, double height) {
setSize(width, height);
}
@Override
public double getHeight() {
return height;
}
@Override
public double getWidth() {
return width;
}
@Override
public void setSize(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public String toString() {
return String.format(Locale.US,
"%s[width=%f, height=%f]", //$NON-NLS-1$
getClass().getName(), width, height);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof java.awt.geom.Dimension2D)) {
return false;
}
java.awt.geom.Dimension2D dim = (java.awt.geom.Dimension2D) obj;
return (getWidth() == dim.getWidth()) &&
(getHeight() == dim.getHeight());
}
@Override
public int hashCode() {
long bits = java.lang.Double.doubleToLongBits(getWidth());
bits ^= java.lang.Double.doubleToLongBits(getHeight()) * 31;
return ((int) bits) ^ ((int) (bits >> 32));
}
}
}

View file

@ -0,0 +1,76 @@
package org.xbib.graphics.graph.gral.graphics;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
/**
* Interface providing functions for a lightweight component that can be drawn
* on the screen. Functions include management of the bounding rectangle,
* returning a preferred size for layout operations, or drawing using a
* specified context.
*/
public interface Drawable {
/**
* Returns the bounds of this {@code Drawable}.
* @return a bounding rectangle
*/
Rectangle2D getBounds();
/**
* Sets the bounds to the specified bounding rectangle.
* @param bounds rectangle containing the component.
*/
void setBounds(Rectangle2D bounds);
/**
* Sets the bounds to the specified coordinates, width and height.
* This method should be used when overriding functionality.
* @param x horizontal position of the upper-left corner
* @param y vertical position of the upper-left corner
* @param width horizontal extent
* @param height vertical extent
*/
void setBounds(double x, double y, double width, double height);
/**
* Returns the x-position of the bounds.
* @return horizontal position of the upper-left corner of the bounding
* rectangle
*/
double getX();
/**
* Returns the y-position of the bounds.
* @return vertical position of the upper-left corner of the bounding
* rectangle
*/
double getY();
/**
* Sets the position to the specified coordinates.
* @param x Coordinate on the x-axis.
* @param y Coordinate on the y-axis.
*/
void setPosition(double x, double y);
/**
* Returns the width of the bounds.
* @return horizontal extent
*/
double getWidth();
/**
* Returns the height of the bounds.
* @return vertical extent
*/
double getHeight();
/**
* Returns the preferred size of the {@code Drawable}.
* @return horizontal and vertical extent that wants to be reached
*/
Dimension2D getPreferredSize();
/**
* Draws the {@code Drawable} with the specified drawing context.
* @param context Environment used for drawing
*/
void draw(DrawingContext context);
}

View file

@ -0,0 +1,243 @@
package org.xbib.graphics.graph.gral.graphics;
import java.awt.geom.Dimension2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.xbib.graphics.graph.gral.graphics.layout.Layout;
/**
* Implementation of {@code Container} that is a {@code Drawable}
* itself and stores instances of {@code Drawable} as components.
* It takes care of laying out, managing insets for and painting the
* components.
*
* @see Drawable
* @see Container
*/
public class DrawableContainer extends AbstractDrawable implements Container {
/** Empty margins that should be preserved around the contents of this
container. */
private final Insets2D insets;
/** Object that manages the layout of all container components. */
private Layout layout;
/** Elements stored in this container. */
private final Queue<Drawable> components;
/** Supplemental information for components, like layout constraints. */
private final Map<Drawable, Object> constraints;
/**
* Creates a new container for {@code Drawable}s without layout
* manager.
*/
public DrawableContainer() {
this(null);
}
/**
* Creates a new container for {@code Drawable}s with the specified
* layout manager.
* @param layout Layout manager to be set.
*/
public DrawableContainer(Layout layout) {
insets = new Insets2D.Double();
components = new ConcurrentLinkedQueue<>();
constraints = new HashMap<>();
this.layout = layout;
}
/**
* Draws the {@code Drawable} with the specified drawing context.
* @param context Environment used for drawing.
*/
public void draw(DrawingContext context) {
drawComponents(context);
}
/**
* Invokes the draw method of each {@code Drawable}.
* @param context Environment used for drawing.
*/
protected void drawComponents(DrawingContext context) {
for (Drawable d : this) {
d.draw(context);
}
}
/**
* Adds a new component to this container.
* @param drawable Component
*/
public void add(Drawable drawable) {
add(drawable, null);
}
/**
* Adds a new component to this container.
* @param drawable Component
* @param constraints Additional information (e.g. for layout)
*/
public void add(Drawable drawable, Object constraints) {
if (drawable == this) {
throw new IllegalArgumentException(
"A container cannot be added to itself."); //$NON-NLS-1$
}
this.constraints.put(drawable, constraints);
components.add(drawable);
layout();
}
@Override
public boolean contains(Drawable drawable) {
return components.contains(drawable);
}
@Override
public List<Drawable> getDrawablesAt(Point2D point) {
return getDrawablesAt(this, point, new LinkedList<Drawable>());
}
@Override
public List<Drawable> getDrawables() {
/*
* TODO: Size of ArrayList can be different from the number of added components
* in concurrent environments.
*/
List<Drawable> drawableList = new ArrayList<>(components.size());
drawableList.addAll(components);
return drawableList;
}
private static List<Drawable> getDrawablesAt(Container container, Point2D point, LinkedList<Drawable> previousResults) {
if (container instanceof Drawable && container.getBounds().contains(point)) {
previousResults.addFirst((Drawable) container);
}
for (Drawable component : container) {
// Check whether the point is in one of the child elements of the container
if (component instanceof Container) {
getDrawablesAt((Container) component, point, previousResults);
} else if (component != null && component.getBounds().contains(point)) {
previousResults.addFirst(component);
}
}
return previousResults;
}
/**
* Return additional information on component
* @param drawable Component
* @return Information object or {@code null}
*/
public Object getConstraints(Drawable drawable) {
return constraints.get(drawable);
}
/**
* Removes a component from this container.
* @param drawable Component
*/
public void remove(Drawable drawable) {
components.remove(drawable);
constraints.remove(drawable);
layout();
}
/**
* Returns the space that this container must preserve at each of its
* edges.
* @return The insets of this DrawableContainer
*/
public Insets2D getInsets() {
Insets2D insets = new Insets2D.Double();
insets.setInsets(this.insets);
return insets;
}
/**
* Sets the space that this container must preserve at each of its
* edges.
* @param insets Insets to be set.
*/
public void setInsets(Insets2D insets) {
if (insets == this.insets || this.insets.equals(insets)) {
return;
}
this.insets.setInsets(insets);
layout();
}
/**
* Returns the layout associated with this container.
* @return Layout manager
*/
public Layout getLayout() {
return layout;
}
/**
* Sets the layout associated with this container.
* @param layout Layout to be set.
*/
public void setLayout(Layout layout) {
this.layout = layout;
layout();
}
/**
* Recalculates this container's layout.
*/
public void layout() {
Layout layout = getLayout();
if (layout != null) {
layout.layout(this);
}
}
/**
* Returns an iterator over the container's elements.
*
* @return an Iterator.
*/
public Iterator<Drawable> iterator() {
return components.iterator();
}
/**
* Returns the number of components that are stored in this container.
* @return total number of components
*/
public int size() {
return components.size();
}
@Override
public void setBounds(Rectangle2D bounds) {
super.setBounds(bounds);
layout();
}
@Override
public void setBounds(double x, double y, double width, double height) {
super.setBounds(x, y, width, height);
layout();
}
@Override
public Dimension2D getPreferredSize() {
Layout layout = getLayout();
if (layout != null) {
return layout.getPreferredSize(this);
}
return super.getPreferredSize();
}
}

View file

@ -0,0 +1,83 @@
package org.xbib.graphics.graph.gral.graphics;
import java.awt.Graphics2D;
/**
* Class that stores an object for drawing and additional context information
* that may be necessary to determine how to draw the object. This includes
* information on drawing quality and the target media (screen, paper, etc.).
*/
public class DrawingContext {
/**
* Data type that describes the quality mode of drawing operations.
*/
public enum Quality {
/** Fast drawing mode. */
DRAFT,
/** Standard drawing mode. */
NORMAL,
/** High quality drawing mode. */
QUALITY
}
/**
* Data type that describes the type of the drawing target.
*/
public enum Target {
/** Bitmap drawing target consisting of pixels. */
BITMAP,
/** Vector drawing target consisting of lines and curves. */
VECTOR
}
/** Graphics instance used for drawing. */
private final Graphics2D graphics;
/** Quality level used for drawing. */
private final Quality quality;
/** Target media. */
private final Target target;
/**
* Initializes a new context with a {@code Graphics2D} object.
* @param graphics Object for drawing geometry.
*/
public DrawingContext(Graphics2D graphics) {
this(graphics, Quality.NORMAL, Target.BITMAP);
}
/**
* Initializes a new context with a {@code Graphics2D} object.
* @param graphics Object for drawing geometry.
* @param quality Drawing quality.
* @param target Target media.
*/
public DrawingContext(Graphics2D graphics, Quality quality, Target target) {
this.graphics = graphics;
this.quality = quality;
this.target = target;
}
/**
* Returns the object for drawing geometry.
* @return Graphics object.
*/
public Graphics2D getGraphics() {
return graphics;
}
/**
* Returns the desired display quality.
* @return Display quality mode.
*/
public Quality getQuality() {
return quality;
}
/**
* Returns the drawing target.
* @return Drawing target.
*/
public Target getTarget() {
return target;
}
}

View file

@ -0,0 +1,184 @@
package org.xbib.graphics.graph.gral.graphics;
import java.util.Locale;
/**
* Abstract class that stores insets for all four directions.
* <p>Please use this instead of java.awt.Insets, as the java class does not
* support double values.</p>
*/
public abstract class Insets2D {
/**
* Creates a new Insets2D object.
*/
public Insets2D() {
}
/**
* Returns the insets at the top.
* @return Top insets.
*/
public abstract double getTop();
/**
* Returns the insets at the left.
* @return Left insets.
*/
public abstract double getLeft();
/**
* Returns the insets at the bottom.
* @return Bottom insets.
*/
public abstract double getBottom();
/**
* Returns the insets at the right.
* @return Right insets.
*/
public abstract double getRight();
/**
* Returns the sum of horizontal insets.
* @return Horizontal insets.
*/
public double getHorizontal() {
return getRight() + getLeft();
}
/**
* Returns the sum of vertical insets.
* @return Vertical insets.
*/
public double getVertical() {
return getTop() + getBottom();
}
/**
* Sets the insets according to the specified insets.
* @param insets Insets to be set.
*/
public abstract void setInsets(Insets2D insets);
/**
* Sets the insets to the specified values.
* @param top Top insets.
* @param left Left insets.
* @param bottom Bottom insets.
* @param right Right insets.
*/
public abstract void setInsets(double top, double left,
double bottom, double right);
/**
* Class that stores insets as double values.
*/
public static class Double extends Insets2D {
/** Version id for serialization. */
private static final long serialVersionUID = -6637052175330595647L;
/** Top. */
private double top;
/** Left. */
private double left;
/** Bottom. */
private double bottom;
/** Right. */
private double right;
/**
* Creates a new Insets2D object with zero insets.
*/
public Double() {
this(0.0);
}
/**
* Creates a new Insets2D object with the specified insets in all
* directions.
* @param inset Inset value.
*/
public Double(double inset) {
this(inset, inset, inset, inset);
}
/**
* Creates a new Insets2D object with the specified insets.
* @param top Top insets.
* @param left Left insets.
* @param bottom Bottom insets.
* @param right Right insets.
*/
public Double(double top, double left, double bottom, double right) {
setInsets(top, left, bottom, right);
}
@Override
public double getTop() {
return top;
}
@Override
public double getLeft() {
return left;
}
@Override
public double getBottom() {
return bottom;
}
@Override
public double getRight() {
return right;
}
@Override
public void setInsets(Insets2D insets) {
if (insets == null) {
return;
}
setInsets(insets.getTop(), insets.getLeft(),
insets.getBottom(), insets.getRight());
}
@Override
public void setInsets(double top, double left,
double bottom, double right) {
this.top = top;
this.left = left;
this.bottom = bottom;
this.right = right;
}
@Override
public String toString() {
return String.format(Locale.US,
"%s[top=%f, left=%f, bottom=%f, right=%f]", //$NON-NLS-1$
getClass().getName(), top, left, bottom, right);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Insets2D)) {
return false;
}
Insets2D insets = (Insets2D) obj;
return (getTop() == insets.getTop())
&& (getLeft() == insets.getLeft())
&& (getBottom() == insets.getBottom())
&& (getRight() == insets.getRight());
}
@Override
public int hashCode() {
long bits = java.lang.Double.doubleToLongBits(getTop());
bits += java.lang.Double.doubleToLongBits(getLeft()) * 37;
bits += java.lang.Double.doubleToLongBits(getBottom()) * 43;
bits += java.lang.Double.doubleToLongBits(getRight()) * 47;
return ((int) bits) ^ ((int) (bits >> 32));
}
}
}

View file

@ -0,0 +1,409 @@
package org.xbib.graphics.graph.gral.graphics;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.util.Objects;
import org.xbib.graphics.graph.gral.util.GraphicsUtils;
import org.xbib.graphics.graph.gral.util.MathUtils;
/**
* Class that draws a label to a specific location.
* A Label is able to manage its settings and to set and get the
* displayed text, as well as calculating its bounds.
*/
public class Label extends AbstractDrawable {
/** Text for this label. */
private String text;
/** Horizontal label alignment. */
private double alignmentX;
/** Vertical label alignment. */
private double alignmentY;
/** Font used to display the text of this label. */
private Font font;
/** Rotation angle in degrees. */
private double rotation;
/** Paint used to draw the shape. */
private Paint color;
/** Relative text alignment. */
private double textAlignment;
/** Decides whether the text should be wrapped. */
private boolean wordWrapEnabled;
/** Paint used to display the background. */
private Paint background;
/** Cached outline of the label text with word wrapping. */
private transient Shape outlineWrapped;
/** Cached outline of the label text without word wrapping. */
private transient Shape outlineUnwrapped;
/**
* Initializes a new empty {@code Label} instance.
*/
public Label() {
this(""); //$NON-NLS-1$
}
/**
* Initializes a new {@code Label} instance with the specified text.
* @param text Text to be displayed.
*/
public Label(String text) {
this.text = text;
alignmentX = 0.5;
alignmentY = 0.5;
font = Font.decode(null);
rotation = 0.0;
color = Color.BLACK;
textAlignment = 0.5;
wordWrapEnabled = false;
}
/**
* Draws the object with the specified drawing context.
* @param context Environment used for drawing
*/
public void draw(DrawingContext context) {
boolean wordWrap = isWordWrapEnabled();
Shape labelShape = getCachedOutline(wordWrap);
if (labelShape == null) {
return;
}
Rectangle2D textBounds = labelShape.getBounds2D();
// Rotate label text around its center point
double rotation = getRotation();
if (MathUtils.isCalculatable(rotation) && rotation != 0.0) {
AffineTransform txLabelText =
AffineTransform.getRotateInstance(
Math.toRadians(-rotation),
textBounds.getCenterX(),
textBounds.getCenterY()
);
labelShape = txLabelText.createTransformedShape(labelShape);
textBounds = labelShape.getBounds2D();
}
// Get graphics instance and store state information
Graphics2D graphics = context.getGraphics();
AffineTransform txOld = graphics.getTransform();
// Draw background
Paint background = getBackground();
if (background != null) {
GraphicsUtils.fillPaintedShape(graphics, getBounds(), background, null);
}
// Calculate absolute text position:
// First, move the text to the upper left of the bounding rectangle
double shapePosX = getX() - textBounds.getX();
double shapePosY = getY() - textBounds.getY();
// Position the text inside the bounding rectangle using the alignment
// settings
double alignmentX = getAlignmentX();
double alignmentY = getAlignmentY();
shapePosX += alignmentX*(getWidth() - textBounds.getWidth());
shapePosY += alignmentY*(getHeight() - textBounds.getHeight());
// Apply positioning
graphics.translate(shapePosX, shapePosY);
// Paint the shape with the color from settings
Paint paint = getColor();
GraphicsUtils.fillPaintedShape(graphics, labelShape, paint, null);
// Restore previous state
graphics.setTransform(txOld);
}
@Override
public Dimension2D getPreferredSize() {
Dimension2D d = super.getPreferredSize();
if (getCachedOutline(false) != null) {
Shape shape = getTextRectangle();
Rectangle2D bounds = shape.getBounds2D();
double rotation = getRotation();
if (MathUtils.isCalculatable(rotation) && rotation != 0.0) {
AffineTransform txLabelText =
AffineTransform.getRotateInstance(
Math.toRadians(-rotation),
bounds.getCenterX(),
bounds.getCenterY()
);
shape = txLabelText.createTransformedShape(shape);
}
d.setSize(
shape.getBounds2D().getWidth(),
shape.getBounds2D().getHeight()
);
}
return d;
}
/**
* Returns an outline shape for this label.
* @param wordWrap Wrap the words of the text to fit the current size.
* @return Outline for this label.
*/
protected Shape getOutline(boolean wordWrap) {
Font font = getFont();
float wrappingWidth = 0f;
if (wordWrap) {
double rotation = Math.toRadians(getRotation());
wrappingWidth = (float) (
Math.abs(Math.cos(rotation))*getWidth() +
Math.abs(Math.sin(rotation))*getHeight());
}
double alignment = getTextAlignment();
return GraphicsUtils.getOutline(
getText(), font, wrappingWidth, alignment);
}
/**
* Returns a cached instance of the outline shape for this label.
* @param wordWrap Flag, whether to wrap lines to fit the current size.
* @return An instance of the outline shape for this label.
*/
protected Shape getCachedOutline(boolean wordWrap) {
if (!isValid() && getText() != null && !getText().isEmpty()) {
outlineWrapped = getOutline(true);
outlineUnwrapped = getOutline(false);
}
if (wordWrap) {
return outlineWrapped;
} else {
return outlineUnwrapped;
}
}
/**
* Returns the bounding rectangle of the text without rotation or word
* wrapping.
* @return Bounding rectangle.
*/
public Rectangle2D getTextRectangle() {
return getCachedOutline(false).getBounds();
}
/**
* Returns the text of this label.
* @return Text.
*/
public String getText() {
return text;
}
/**
* Sets the displayed text to the specified value.
* @param text Text to be displayed.
*/
public void setText(String text) {
this.text = text;
invalidate();
}
/**
* Marks the text layout as invalid. It has to be refreshed the next time.
*/
protected void invalidate() {
outlineWrapped = null;
outlineUnwrapped = null;
}
/**
* Returns whether the cached values in this label are valid.
* @return {@code true} if all cached values are valid,
* otherwise {@code false}.
*/
protected boolean isValid() {
boolean wordWrap = isWordWrapEnabled();
if (wordWrap) {
return outlineWrapped != null;
} else {
return outlineUnwrapped != null;
}
}
@Override
public void setBounds(double x, double y, double width, double height) {
double widthOld = getWidth();
double heightOld = getHeight();
super.setBounds(x, y, width, height);
if (width != widthOld || height != heightOld) {
invalidate();
}
}
/**
* Returns the horizontal alignment within the bounding rectangle.
* 0.0 means left, 1.0 means right.
* @return Horizontal label alignment.
*/
public double getAlignmentX() {
return alignmentX;
}
/**
* Sets the horizontal alignment within the bounding rectangle.
* 0.0 means left, 1.0 means right.
* @param alignmentX Horizontal label alignment.
*/
public void setAlignmentX(double alignmentX) {
this.alignmentX = alignmentX;
}
/**
* Returns the vertical alignment within the bounding rectangle.
* 0.0 means top, 1.0 means bottom.
* @return Vertical label alignment.
*/
public double getAlignmentY() {
return alignmentY;
}
/**
* Sets the vertical alignment within the bounding rectangle.
* 0.0 means top, 1.0 means bottom.
* @param alignmentY Vertical label alignment.
*/
public void setAlignmentY(double alignmentY) {
this.alignmentY = alignmentY;
}
/**
* Returns the font used to display the text of this label.
* @return Font used for text display.
*/
public Font getFont() {
return font;
}
/**
* Sets the font used to display the text of this label.
* @param font Font used for text display.
*/
public void setFont(Font font) {
this.font = font;
invalidate();
}
/**
* Returns the rotation of this label.
* The rotation will be counterclockwise.
* @return Rotation in degrees.
*/
public double getRotation() {
return rotation;
}
/**
* Sets the rotation of this label.
* The rotation will be counterclockwise.
* @param angle Rotation in degrees.
*/
public void setRotation(double angle) {
this.rotation = angle;
invalidate();
}
/**
* Returns the paint used to draw the label shape.
* @return Paint for shape drawing.
*/
public Paint getColor() {
return color;
}
/**
* Sets the paint used to draw the label shape.
* @param color Paint for shape drawing.
*/
public void setColor(Paint color) {
this.color = color;
}
/**
* Returns the alignment of text with multiple lines.
* 0.0 means left, 1.0 means right.
* @return Relative text alignment.
*/
public double getTextAlignment() {
return textAlignment;
}
/**
* Sets the alignment of text with multiple lines.
* 0.0 means left, 1.0 means right.
* @param textAlignment Relative text alignment.
*/
public void setTextAlignment(double textAlignment) {
this.textAlignment = textAlignment;
invalidate();
}
/**
* Returns whether words of the text should be wrapped to fit the size of the label.
* @return {@code true} if the text should be wrapped, {@code false} otherwise.
*/
public boolean isWordWrapEnabled() {
return wordWrapEnabled;
}
/**
* Sets whether words of the text should be wrapped to fit the size of the label.
* @param wordWrapEnabled {@code true} if the text should be wrapped, {@code false} otherwise.
*/
public void setWordWrapEnabled(boolean wordWrapEnabled) {
this.wordWrapEnabled = wordWrapEnabled;
invalidate();
}
/**
* Returns the background color.
* @return Background color or {@code null}, if no background is defined.
*/
public Paint getBackground() {
return background;
}
/**
* Sets the background color to the specified value.
* @param background Background color.
*/
public void setBackground(Paint background) {
this.background = background;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Label)) {
return false;
}
Label label = (Label) obj;
return ((getText() == null && label.getText() == null) || getText().equals(label.getText()))
&& (getAlignmentX() == label.getAlignmentX())
&& (getAlignmentY() == label.getAlignmentY())
&& ((getFont() == null && label.getFont() == null) || getFont().equals(label.getFont()))
&& (getRotation() == label.getRotation())
&& ((getColor() == null && label.getColor() == null) || getColor().equals(label.getColor()))
&& (getTextAlignment() == label.getTextAlignment())
&& (isWordWrapEnabled() == label.isWordWrapEnabled())
&& ((getBackground() == null && label.getBackground() == null) || getBackground().equals(label.getBackground()));
}
@Override
public int hashCode() {
return Objects.hash(text, alignmentX, alignmentY, font, rotation, color, textAlignment, wordWrapEnabled, background);
}
}

View file

@ -0,0 +1,55 @@
package org.xbib.graphics.graph.gral.graphics;
/**
* Indicates the location of components.
*/
public enum Location {
/** Central location. */
CENTER(0.5, 0.5),
/** Northern location. */
NORTH(0.5, 0.0),
/** North-eastern location. */
NORTH_EAST(1.0, 0.0),
/** Eastern location. */
EAST(1.0, 0.5),
/** South-eastern location. */
SOUTH_EAST(1.0, 1.0),
/** Southern location. */
SOUTH(0.5, 1.0),
/** South-western location. */
SOUTH_WEST(0.0, 1.0),
/** Western location. */
WEST(0.0, 0.5),
/** North-western location. */
NORTH_WEST(0.0, 0.0);
/** Horizontal alignment. */
private final double alignH;
/** Vertical alignment. */
private final double alignV;
/**
* Constructor that initializes a new location.
* @param alignH Horizontal alignment.
* @param alignV Vertical alignment.
*/
Location(double alignH, double alignV) {
this.alignH = alignH;
this.alignV = alignV;
}
/**
* Returns the horizontal alignment as a double value.
* @return horizontal alignment
*/
public double getAlignmentH() {
return alignH;
}
/**
* Returns the vertical alignment as a double value.
* @return vertical alignment
*/
public double getAlignmentV() {
return alignV;
}
}

View file

@ -0,0 +1,11 @@
package org.xbib.graphics.graph.gral.graphics;
/**
* Enumeration type to describe the orientation of a arbitrary elements.
*/
public enum Orientation {
/** Horizontal orientation. */
HORIZONTAL,
/** Vertical orientation. */
VERTICAL
}

View file

@ -0,0 +1,34 @@
package org.xbib.graphics.graph.gral.graphics.layout;
public abstract class AbstractLayout implements Layout {
/** Horizontal spacing of components. */
private double gapX;
/** Vertical spacing of components. */
private double gapY;
public AbstractLayout(double gapX, double gapY) {
this.gapX = gapX;
this.gapY = gapY;
}
@Override
public double getGapX() {
return gapX;
}
@Override
public void setGapX(double gapX) {
this.gapX = gapX;
}
@Override
public double getGapY() {
return gapY;
}
@Override
public void setGapY(double gapY) {
this.gapY = gapY;
}
}

View file

@ -0,0 +1,24 @@
package org.xbib.graphics.graph.gral.graphics.layout;
import org.xbib.graphics.graph.gral.graphics.Orientation;
public abstract class AbstractOrientedLayout extends AbstractLayout implements
OrientedLayout {
/** Orientation in which elements should be laid out. */
private Orientation orientation;
public AbstractOrientedLayout(Orientation orientation, double gapX, double gapY) {
super(gapX, gapY);
this.orientation = orientation;
}
@Override
public Orientation getOrientation() {
return orientation;
}
@Override
public void setOrientation(Orientation orientation) {
this.orientation = orientation;
}
}

View file

@ -0,0 +1,257 @@
package org.xbib.graphics.graph.gral.graphics.layout;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.util.HashMap;
import java.util.Map;
import org.xbib.graphics.graph.gral.graphics.Container;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.graphics.Insets2D;
import org.xbib.graphics.graph.gral.graphics.Location;
/**
* Implementation of Layout that arranges a {@link Container}'s components
* according to a certain grid. This is similar to Java's
* {@link java.awt.BorderLayout}, but also allows components to be placed in
* each of the corners.
*/
public class EdgeLayout extends AbstractLayout {
/**
* Initializes a layout manager object with the specified space between the
* components.
* @param gapH Horizontal gap.
* @param gapV Vertical gap.
*/
public EdgeLayout(double gapH, double gapV) {
super(gapH, gapV);
}
/**
* Initializes a layout manager object without space between the
* components.
*/
public EdgeLayout() {
this(0.0, 0.0);
}
/**
* Arranges the components of the specified container according to this
* layout.
* @param container Container to be laid out.
*/
public void layout(Container container) {
// Fetch components
Map<Location, Drawable> comps = getComponentsByLocation(container);
Drawable north = comps.get(Location.NORTH);
Drawable northEast = comps.get(Location.NORTH_EAST);
Drawable east = comps.get(Location.EAST);
Drawable southEast = comps.get(Location.SOUTH_EAST);
Drawable south = comps.get(Location.SOUTH);
Drawable southWest = comps.get(Location.SOUTH_WEST);
Drawable west = comps.get(Location.WEST);
Drawable northWest = comps.get(Location.NORTH_WEST);
Drawable center = comps.get(Location.CENTER);
// Calculate maximum widths and heights
double widthWest = getMaxWidth(northWest, west, southWest);
double widthEast = getMaxWidth(northEast, east, southEast);
double heightNorth = getMaxHeight(northWest, north, northEast);
double heightSouth = getMaxHeight(southWest, south, southEast);
double gapWest = (widthWest > 0.0 && center != null) ? getGapX() : 0.0;
double gapEast = (widthEast > 0.0 && center != null) ? getGapX() : 0.0;
double gapNorth = (heightNorth > 0.0 && center != null) ? getGapY() : 0.0;
double gapSouth = (heightSouth > 0.0 && center != null) ? getGapY() : 0.0;
Rectangle2D bounds = container.getBounds();
Insets2D insets = container.getInsets();
if (insets == null) {
insets = new Insets2D.Double();
}
double xWest = bounds.getMinX() + insets.getLeft();
double xCenter = xWest + widthWest + gapWest;
double xEast = bounds.getMaxX() - insets.getRight() - widthEast;
double yNorth = bounds.getMinY() + insets.getTop();
double yCenter = yNorth + heightNorth + gapNorth;
double ySouth = bounds.getMaxY() - insets.getBottom() - heightSouth;
double widthAll = widthWest + widthEast;
double heightAll = heightNorth + heightSouth;
double gapHAll = gapWest + gapEast;
double gapVAll = gapNorth - gapSouth;
layoutComponent(northWest,
xWest, yNorth,
widthWest, heightNorth
);
layoutComponent(north,
xCenter, yNorth,
bounds.getWidth() - insets.getHorizontal() - widthAll - gapHAll,
heightNorth
);
layoutComponent(northEast,
xEast, yNorth,
widthEast, heightNorth
);
layoutComponent(east,
xEast, yCenter,
widthEast,
bounds.getHeight() - insets.getVertical() - heightAll - gapVAll
);
layoutComponent(southEast,
xEast, ySouth,
widthEast,
heightSouth
);
layoutComponent(south,
xCenter, ySouth,
bounds.getWidth() - insets.getHorizontal() - widthAll - gapHAll,
heightSouth
);
layoutComponent(southWest,
xWest, ySouth,
widthWest,
heightSouth
);
layoutComponent(west,
xWest, yCenter,
widthWest,
bounds.getHeight() - insets.getVertical() - heightAll - gapVAll
);
layoutComponent(center,
xCenter, yCenter,
bounds.getWidth()
- insets.getLeft() - widthAll
- insets.getRight() - gapHAll,
bounds.getHeight()
- insets.getTop() - heightAll
- insets.getBottom() - gapVAll
);
}
/**
* Returns the preferred size of the specified container using this layout.
* @param container Container whose preferred size is to be returned.
* @return Preferred extent of the specified container.
*/
public Dimension2D getPreferredSize(Container container) {
// Fetch components
Map<Location, Drawable> comps = getComponentsByLocation(container);
Drawable north = comps.get(Location.NORTH);
Drawable northEast = comps.get(Location.NORTH_EAST);
Drawable east = comps.get(Location.EAST);
Drawable southEast = comps.get(Location.SOUTH_EAST);
Drawable south = comps.get(Location.SOUTH);
Drawable southWest = comps.get(Location.SOUTH_WEST);
Drawable west = comps.get(Location.WEST);
Drawable northWest = comps.get(Location.NORTH_WEST);
Drawable center = comps.get(Location.CENTER);
// Calculate maximum widths and heights
double widthWest = getMaxWidth(northWest, west, southWest);
double widthCenter = getMaxWidth(north, center, south);
double widthEast = getMaxWidth(northEast, east, southEast);
double heightNorth = getMaxHeight(northWest, north, northEast);
double heightCenter = getMaxHeight(west, center, east);
double heightSouth = getMaxHeight(southWest, south, southEast);
double gapEast = (widthEast > 0.0 && center != null) ? getGapX() : 0.0;
double gapWest = (widthWest > 0.0 && center != null) ? getGapX() : 0.0;
double gapNorth = (heightNorth > 0.0 && center != null) ? getGapY() : 0.0;
double gapSouth = (heightSouth > 0.0 && center != null) ? getGapY() : 0.0;
// Calculate preferred dimensions
Insets2D insets = container.getInsets();
if (insets == null) {
insets = new Insets2D.Double();
}
double width = insets.getLeft() + widthEast + gapEast + widthCenter +
gapWest + widthWest + insets.getRight();
double height = insets.getTop() + heightNorth + gapNorth + heightCenter +
gapSouth + heightSouth + insets.getBottom();
return new org.xbib.graphics.graph.gral.graphics.Dimension2D.Double(
width, height
);
}
/**
* Returns a map all components which are stored with a {@code Location}
* constraint in the specified container.
* @param container Container which stores the components
* @return A map of all components (values) and their constraints (keys) in
* the specified container.
*/
private static Map<Location, Drawable> getComponentsByLocation(Container container) {
Map<Location, Drawable> drawablesByLocation = new HashMap<>();
for (Drawable d: container) {
Object constraints = container.getConstraints(d);
if (constraints instanceof Location) {
drawablesByLocation.put((Location) constraints, d);
}
}
return drawablesByLocation;
}
/**
* Returns the maximum width of an array of Drawables.
* @param drawables Drawables to be measured.
* @return Maximum horizontal extent.
*/
private static double getMaxWidth(Drawable... drawables) {
double width = 0.0;
for (Drawable d : drawables) {
if (d == null) {
continue;
}
width = Math.max(width, d.getPreferredSize().getWidth());
}
return width;
}
/**
* Returns the maximum height of an array of Drawables.
* @param drawables Drawables to be measured.
* @return Maximum vertical extent.
*/
private static double getMaxHeight(Drawable... drawables) {
double height = 0.0;
for (Drawable d : drawables) {
if (d == null) {
continue;
}
height = Math.max(height, d.getPreferredSize().getHeight());
}
return height;
}
/**
* Sets the bounds of the specified {@code Drawable} to the specified
* values.
* @param component {@code Drawable} that should be resized.
* @param x X coordinate.
* @param y Y coordinate.
* @param w Width.
* @param h Height.
*/
private static void layoutComponent(Drawable component,
double x, double y, double w, double h) {
if (component == null) {
return;
}
component.setBounds(x, y, w, h);
}
}

View file

@ -0,0 +1,51 @@
package org.xbib.graphics.graph.gral.graphics.layout;
import java.awt.geom.Dimension2D;
import org.xbib.graphics.graph.gral.graphics.Container;
/**
* Interface that provides basic functions for arranging a layout.
* Functionality includes the arrangement of components and returning the
* preferred size of a specified container using this layout.
*/
public interface Layout {
/**
* Returns the amount of horizontal space between two layed out components.
* @return Space in pixels.
*/
double getGapX();
/**
* Sets the amount of horizontal space between two layed out components.
* @param gapX Space in pixels.
*/
void setGapX(double gapX);
/**
* Returns the amount of vertical space between two layed out components.
* @return Space in pixels.
*/
double getGapY();
/**
* Sets the amount of horizontal space between two layed out components.
* @param gapY Space in pixels.
*/
void setGapY(double gapY);
/**
* Arranges the components of the specified container according to this
* layout.
* @param container Container to be laid out.
*/
void layout(Container container);
/**
* Returns the preferred size of the specified container using this layout.
* @param container Container whose preferred size is to be returned.
* @return Preferred extent of the specified container.
*/
Dimension2D getPreferredSize(Container container);
}

View file

@ -0,0 +1,21 @@
package org.xbib.graphics.graph.gral.graphics.layout;
import org.xbib.graphics.graph.gral.graphics.Orientation;
/**
* Represents a layout with a specific orientation.
* @see Orientation
*/
public interface OrientedLayout extends Layout {
/**
* Returns the layout direction.
* @return Layout orientation.
*/
Orientation getOrientation();
/**
* Sets the layout direction.
* @param orientation Layout orientation.
*/
void setOrientation(Orientation orientation);
}

View file

@ -0,0 +1,226 @@
package org.xbib.graphics.graph.gral.graphics.layout;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.util.HashMap;
import java.util.Map;
import org.xbib.graphics.graph.gral.graphics.Container;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.graphics.Insets2D;
import org.xbib.graphics.graph.gral.graphics.Location;
/**
* Implementation of Layout that arranges a {@link Container}'s components
* inside or in the regions outside of the container. This is similar to
* {@link EdgeLayout}, but also allows components to be placed outside the
* container.
*/
public class OuterEdgeLayout extends AbstractLayout {
/**
* Initializes a layout manager object with the specified space between the
* container's edges and the components.
* @param gap Spacing between the container's edges and the components.
*/
public OuterEdgeLayout(double gap) {
super(gap, gap);
}
/**
* Initializes a layout manager object without space between the
* components.
*/
public OuterEdgeLayout() {
this(0.0);
}
/**
* Arranges the components of the specified container according to this
* layout.
* @param container Container to be laid out.
*/
public void layout(Container container) {
// Fetch components
Map<Location, Drawable> comps = getComponentsByLocation(container);
Drawable north = comps.get(Location.NORTH);
Drawable northEast = comps.get(Location.NORTH_EAST);
Drawable east = comps.get(Location.EAST);
Drawable southEast = comps.get(Location.SOUTH_EAST);
Drawable south = comps.get(Location.SOUTH);
Drawable southWest = comps.get(Location.SOUTH_WEST);
Drawable west = comps.get(Location.WEST);
Drawable northWest = comps.get(Location.NORTH_WEST);
Drawable center = comps.get(Location.CENTER);
// Calculate maximum widths and heights
double widthWest = getMaxWidth(northWest, west, southWest);
double widthEast = getMaxWidth(northEast, east, southEast);
double heightNorth = getMaxHeight(northWest, north, northEast);
double heightSouth = getMaxHeight(southWest, south, southEast);
double gapEast = (widthEast > 0.0) ? getGapX() : 0.0;
double gapWest = (widthWest > 0.0) ? getGapX() : 0.0;
double gapNorth = (heightNorth > 0.0) ? getGapY() : 0.0;
double gapSouth = (heightSouth > 0.0) ? getGapY() : 0.0;
Rectangle2D bounds = container.getBounds();
Insets2D insets = container.getInsets();
if (insets == null) {
insets = new Insets2D.Double();
}
double xWest = bounds.getMinX() + insets.getLeft() - gapWest - widthWest;
double xCenter = bounds.getMinX() + insets.getLeft();
double xEast = bounds.getMaxX() - insets.getRight() + gapEast;
double yNorth = bounds.getMinY() + insets.getTop() - gapNorth - heightNorth;
double yCenter = bounds.getMinY() + insets.getTop();
double ySouth = bounds.getMaxY() - insets.getBottom() + gapSouth;
layoutComponent(northWest,
xWest, yNorth,
widthWest, heightNorth
);
layoutComponent(north,
xCenter, yNorth,
bounds.getWidth() - insets.getHorizontal(),
heightNorth
);
layoutComponent(northEast,
xEast, yNorth,
widthEast, heightNorth
);
layoutComponent(east,
xEast, yCenter,
widthEast,
bounds.getHeight() - insets.getVertical()
);
layoutComponent(southEast,
xEast, ySouth,
widthEast,
heightSouth
);
layoutComponent(south,
xCenter, ySouth,
bounds.getWidth() - insets.getHorizontal(),
heightSouth
);
layoutComponent(southWest,
xWest, ySouth,
widthWest,
heightSouth
);
layoutComponent(west,
xWest, yCenter,
widthWest,
bounds.getHeight() - insets.getVertical()
);
layoutComponent(center,
xCenter + getGapX(), yCenter + getGapY(),
bounds.getWidth() - insets.getHorizontal() - 2*getGapX(),
bounds.getHeight() - insets.getVertical() - 2*getGapY()
);
}
/**
* Returns the preferred size of the specified container using this layout.
* @param container Container whose preferred size is to be returned.
* @return Preferred extent of the specified container.
*/
public Dimension2D getPreferredSize(Container container) {
// Fetch components
Map<Location, Drawable> comps = getComponentsByLocation(container);
Drawable center = comps.get(Location.CENTER);
// Calculate preferred dimensions
Insets2D insets = container.getInsets();
if (insets == null) {
insets = new Insets2D.Double();
}
double width = center.getWidth() + insets.getHorizontal() + 2*getGapX();
double height = center.getHeight() + insets.getVertical() + 2*getGapY();
return new org.xbib.graphics.graph.gral.graphics.Dimension2D.Double(
width, height
);
}
/**
* Returns a map all components which are stored with a {@code Location}
* constraint in the specified container.
* @param container Container which stores the components
* @return A map of all components (values) and their constraints (keys) in
* the specified container.
*/
private static Map<Location, Drawable> getComponentsByLocation(Container container) {
Map<Location, Drawable> drawablesByLocation = new HashMap<>();
for (Drawable d: container) {
Object constraints = container.getConstraints(d);
if (constraints instanceof Location) {
drawablesByLocation.put((Location) constraints, d);
}
}
return drawablesByLocation;
}
/**
* Returns the maximum width of an array of Drawables.
* @param drawables Drawables to be measured.
* @return Maximum horizontal extent.
*/
private static double getMaxWidth(Drawable... drawables) {
double width = 0.0;
for (Drawable d : drawables) {
if (d == null) {
continue;
}
width = Math.max(width, d.getPreferredSize().getWidth());
}
return width;
}
/**
* Returns the maximum height of an array of Drawables.
* @param drawables Drawables to be measured.
* @return Maximum vertical extent.
*/
private static double getMaxHeight(Drawable... drawables) {
double height = 0.0;
for (Drawable d : drawables) {
if (d == null) {
continue;
}
height = Math.max(height, d.getPreferredSize().getHeight());
}
return height;
}
/**
* Sets the bounds of the specified {@code Drawable} to the specified
* values.
* @param component {@code Drawable} that should be resized.
* @param x X coordinate.
* @param y Y coordinate.
* @param w Width.
* @param h Height.
*/
private static void layoutComponent(Drawable component,
double x, double y, double w, double h) {
if (component == null) {
return;
}
component.setBounds(x, y, w, h);
}
}

View file

@ -0,0 +1,192 @@
package org.xbib.graphics.graph.gral.graphics.layout;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import org.xbib.graphics.graph.gral.graphics.Container;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.graphics.Insets2D;
import org.xbib.graphics.graph.gral.graphics.Orientation;
/**
* Class that represents a layout manager which arranges its components
* as horizontal or vertical stacks.
*/
public class StackedLayout extends AbstractOrientedLayout {
/** Default layout behaviour for components. */
private final Constraints defaultConstraints;
public static class Constraints {
/**
* Whether the component is stretched to the container's width (vertical layout)
* or height (horizontal layout).
*/
private final boolean stretched;
/** Horizontal alignment of the component. */
private final double alignmentX;
/** Vertical alignment of the component. */
private final double alignmentY;
public Constraints(boolean stretched, double alignmentX, double alignmentY) {
this.stretched = stretched;
this.alignmentX = alignmentX;
this.alignmentY = alignmentY;
}
/**
* Returns whether the component is stretched to the container's width (vertical layout)
* or height (horizontal orientation).
* @return {@code true} if the layed out component should be stretched, {@code false} otherwise.
*/
public boolean isStretched() {
return stretched;
}
/**
* Returns the relative horizontal position of the component within the container.
* This value only has effect, if the components do not fill the width of the container.
* @return Relative position of layed out components.
*/
public double getAlignmentX() {
return alignmentX;
}
/**
* Returns the relative vertical position of the components within the container.
* This value only has effect, if the components do not fill the height of the container.
* @return Relative position of layed out components.
*/
public double getAlignmentY() {
return alignmentY;
}
}
/**
* Creates a new StackedLayout object with the specified orientation
* and default gap between the components.
* @param orientation Orientation in which components are stacked.
*/
public StackedLayout(Orientation orientation) {
this(orientation, 0.0, 0.0);
}
/**
* Creates a new StackedLayout object with the specified orientation
* and gap between the components.
* @param orientation Orientation in which components are stacked.
* @param gapX Horizontal gap between the components.
* @param gapY Vertical gap between the components.
*/
public StackedLayout(Orientation orientation, double gapX, double gapY) {
super(orientation, gapX, gapY);
defaultConstraints = new Constraints(true, 0.5, 0.5);
}
/**
* Arranges the components of the specified container according to this
* layout.
* @param container Container to be laid out.
*/
public void layout(Container container) {
Dimension2D size = getPreferredSize(container);
Rectangle2D bounds = container.getBounds();
Insets2D insets = container.getInsets();
double xMin = bounds.getMinX() + insets.getLeft();
double yMin = bounds.getMinY() + insets.getTop();
double width = bounds.getWidth() - insets.getLeft() - insets.getRight();
double height = bounds.getHeight() - insets.getTop() - insets.getBottom();
int count = 0;
if (getOrientation() == Orientation.HORIZONTAL) {
xMin += Math.max(bounds.getWidth() - size.getWidth(), 0.0)*defaultConstraints.getAlignmentX();
for (Drawable component : container) {
if (count++ > 0) {
xMin += getGapX();
}
Dimension2D compBounds = component.getPreferredSize();
Constraints constraints = getConstraints(component, container);
double componentHeight;
double componentY;
if (constraints.isStretched()) {
componentHeight = height;
componentY = yMin;
} else {
componentHeight = Math.min(compBounds.getHeight(), height);
componentY = yMin + (height - componentHeight)*constraints.getAlignmentY();
}
component.setBounds(xMin, componentY, compBounds.getWidth(), componentHeight);
xMin += compBounds.getWidth();
}
} else if (getOrientation() == Orientation.VERTICAL) {
yMin += Math.max(bounds.getHeight() - size.getHeight(), 0.0)*defaultConstraints.getAlignmentY();
for (Drawable component : container) {
if (count++ > 0) {
yMin += getGapY();
}
Dimension2D compBounds = component.getPreferredSize();
Constraints constraints = getConstraints(component, container);
double componentWidth;
double componentX;
if (constraints.isStretched()) {
componentWidth = width;
componentX = xMin;
} else {
componentWidth = Math.min(compBounds.getWidth(), width);
componentX = xMin + (width - componentWidth)*constraints.getAlignmentX();
}
component.setBounds(componentX, yMin, componentWidth, compBounds.getHeight());
yMin += compBounds.getHeight();
}
}
}
/**
* Returns the preferred size of the specified container using this layout.
* @param container Container whose preferred size is to be returned.
* @return Preferred extent of the specified container.
*/
public Dimension2D getPreferredSize(Container container) {
Insets2D insets = container.getInsets();
double width = insets.getLeft();
double height = insets.getTop();
int count = 0;
if (getOrientation() == Orientation.HORIZONTAL) {
double h = 0.0;
for (Drawable component : container) {
if (count++ > 0) {
width += getGapX();
}
Dimension2D itemBounds = component.getPreferredSize();
width += itemBounds.getWidth();
h = Math.max(height, itemBounds.getHeight());
}
height += h;
} else if (getOrientation() == Orientation.VERTICAL) {
double w = 0.0;
for (Drawable component : container) {
if (count++ > 0) {
height += getGapY();
}
Dimension2D itemBounds = component.getPreferredSize();
w = Math.max(w, itemBounds.getWidth());
height += itemBounds.getHeight();
}
width += w;
}
width += insets.getRight();
height += insets.getBottom();
return new org.xbib.graphics.graph.gral.graphics.Dimension2D.Double(width, height);
}
private Constraints getConstraints(Drawable component, Container container) {
Object constraints = container.getConstraints(component);
if (constraints == null || !(constraints instanceof Constraints)) {
constraints = defaultConstraints;
}
return (Constraints) constraints;
}
}

View file

@ -0,0 +1,239 @@
package org.xbib.graphics.graph.gral.graphics.layout;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.util.HashMap;
import java.util.Map;
import org.xbib.graphics.graph.gral.graphics.Container;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.graphics.Insets2D;
/**
* Implementation of Layout that arranges a {@link Container}'s components
* according to a tabular grid with a fixed number of columns. This is similar
* to Java's {@link java.awt.GridLayout}, but the cells in the grid may have
* different dimensions.
*/
public class TableLayout extends AbstractLayout {
/** Number of columns. */
private final int cols;
/** Index of the column values in the array that is returned by
{@link #getInfo(Container)}. */
private static final int COLS = 0;
/** Index of the row values in the array that is returned by
{@link #getInfo(Container)}. */
private static final int ROWS = 1;
/**
* Internal data class to store layout related values.
*/
private static final class Info {
/** Map of column/row index and maximal preferred size. */
public final Map<Integer, Double> sizes;
/** Number of columns/rows */
public int size;
/** Sum of preferred sizes in horizontal/vertical direction. */
public double sizeSum;
/** Sum of insets in horizontal/vertical direction. */
public double insetsSum;
/** Sum of gaps in horizontal/vertical direction. */
public double gapSum;
/** Mean preferred size in horizontal/vertical direction. */
public double sizeMean;
/** Space in horizontal/vertical direction which couldn't be resized
because the size limits of some components have been reached. */
public double unsizeableSpace;
/**
* Initializes a new instance for storing several variables.
*/
public Info() {
sizes = new HashMap<>();
}
}
/**
* Initializes a layout manager object with the specified number of columns
* and the distances between the components.
* @param cols Number of columns
* @param gapH Horizontal gap.
* @param gapV Vertical gap.
*/
public TableLayout(int cols, double gapH, double gapV) {
super(gapH, gapV);
if (cols <= 0) {
throw new IllegalArgumentException("Invalid number of columns.");
}
this.cols = cols;
}
/**
* Initializes a layout manager object with the specified number of columns
* and no gap between the components.
* @param cols Number of columns.
*/
public TableLayout(int cols) {
this(cols, 0.0, 0.0);
}
/**
* Calculates the preferred dimensions for all columns and rows.
* @param container The container for which the dimension should be
* calculated.
* @see #COLS
* @see #ROWS
*/
private Info[] getInfo(Container container) {
Info[] infos = new Info[2];
infos[COLS] = new Info();
infos[ROWS] = new Info();
infos[COLS].size = cols;
infos[ROWS].size = (int) Math.ceil(container.size() / (double) cols);
// Find out the preferred dimensions for each columns and row
int compIndex = 0;
for (Drawable component : container) {
Integer col = compIndex%infos[COLS].size;
Integer row = compIndex/infos[COLS].size;
Double colWidth = infos[COLS].sizes.get(col);
Double rowHeight = infos[ROWS].sizes.get(row);
Dimension2D size = component.getPreferredSize();
infos[COLS].sizes.put(col, max(size.getWidth(), colWidth));
infos[ROWS].sizes.put(row, max(size.getHeight(), rowHeight));
compIndex++;
}
// Calculate container specific variables
Rectangle2D bounds = container.getBounds();
Insets2D insets = container.getInsets();
if (insets == null) {
insets = new Insets2D.Double();
}
infos[COLS].insetsSum = insets.getLeft() + insets.getRight();
infos[ROWS].insetsSum = insets.getTop() + insets.getBottom();
infos[COLS].gapSum = Math.max((infos[COLS].size - 1)*getGapX(), 0.0);
infos[ROWS].gapSum = Math.max((infos[ROWS].size - 1)*getGapY(), 0.0);
double containerWidth =
Math.max(bounds.getWidth() - infos[COLS].insetsSum - infos[COLS].gapSum, 0.0);
double containerHeight =
Math.max(bounds.getHeight() - infos[ROWS].insetsSum - infos[ROWS].gapSum, 0.0);
infos[COLS].sizeMean = (infos[COLS].size > 0) ? containerWidth/infos[COLS].size : 0.0;
infos[ROWS].sizeMean = (infos[ROWS].size > 0) ? containerHeight/infos[ROWS].size : 0.0;
// Values for columns and rows
for (Info info : infos) {
info.sizeSum = 0.0;
info.unsizeableSpace = 0.0;
int sizeable = 0;
for (double size : info.sizes.values()) {
info.sizeSum += size;
if (size >= info.sizeMean) {
info.unsizeableSpace += size - info.sizeMean;
} else {
sizeable++;
}
}
if (sizeable > 0) {
info.unsizeableSpace /= sizeable;
}
}
return infos;
}
/**
* Arranges the components of the specified container according to this
* layout.
* @param container Container to be laid out.
*/
public void layout(Container container) {
Info[] infos = getInfo(container);
Rectangle2D bounds = container.getBounds();
Insets2D insets = container.getInsets();
if (insets == null) {
insets = new Insets2D.Double();
}
Integer lastCol = infos[COLS].size - 1;
int compIndex = 0;
double x = bounds.getX() + insets.getLeft();
double y = bounds.getY() + insets.getTop();
for (Drawable component : container) {
Integer col = compIndex%infos[COLS].size;
Integer row = compIndex/infos[COLS].size;
double colWidth = infos[COLS].sizes.get(col);
double rowHeight = infos[ROWS].sizes.get(row);
double w = Math.max(infos[COLS].sizeMean - infos[COLS].unsizeableSpace, colWidth);
double h = Math.max(infos[ROWS].sizeMean - infos[ROWS].unsizeableSpace, rowHeight);
if (component != null) {
component.setBounds(x, y, w, h);
}
if (col.equals(lastCol)) {
x = bounds.getX() + insets.getLeft();
y += h + getGapY();
} else {
x += w + getGapX();
}
compIndex++;
}
}
/**
* Returns the preferred size of the specified container using this layout.
* @param container Container whose preferred size is to be returned.
* @return Preferred extent of the specified container.
*/
public Dimension2D getPreferredSize(Container container) {
Info[] infos = getInfo(container);
return new org.xbib.graphics.graph.gral.graphics.Dimension2D.Double(
infos[COLS].sizeSum + infos[COLS].gapSum + infos[COLS].insetsSum,
infos[ROWS].sizeSum + infos[ROWS].gapSum + infos[ROWS].insetsSum
);
}
/**
* Returns the number of desired columns.
* @return Number of desired columns.
*/
public int getColumns() {
return cols;
}
/**
* Returns the value that is larger. If both are equal the first value will
* be returned.
* @param <T> Data type for the values.
* @param a First value.
* @param b Second value.
* @return Larger value.
*/
private static <T extends Comparable<T>> T max(T a, T b) {
if (a == null || b == null) {
if (a == null) {
return b;
} else {
return a;
}
}
if (a.compareTo(b) >= 0) {
return a;
}
return b;
}
}

View file

@ -0,0 +1,151 @@
package org.xbib.graphics.graph.gral.io;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
/**
* Abstract implementation of {@code IOFactory} which provides basic
* functionality.
*
* @param <T> The type of objects which should be produced by this factory
*/
public abstract class AbstractIOFactory<T> implements IOFactory<T> {
private final Map<String, Class<? extends T>> entries;
/**
* Constructor that creates a new instance and initializes it with the name
* of the corresponding properties file(s).
* @param propFileName File name of the properties file(s)
* @throws IOException if reading the properties file(s) failed
*/
@SuppressWarnings("unchecked")
protected AbstractIOFactory(String propFileName) throws IOException {
entries = new HashMap<>();
// Retrieve property-files
Enumeration<URL> propFiles;
propFiles = getClass().getClassLoader().getResources(propFileName);
if (!propFiles.hasMoreElements()) {
throw new IOException(MessageFormat.format(
"Property file not found: {0}", propFileName)); //$NON-NLS-1$
}
Properties props = new Properties();
while (propFiles.hasMoreElements()) {
URL propURL = propFiles.nextElement();
InputStream stream = null;
try {
stream = propURL.openStream();
props.load(stream);
} finally {
if (stream != null) {
stream.close();
}
}
// Parse property files and register entries as items
for (Map.Entry<Object, Object> prop : props.entrySet()) {
String mimeType = (String) prop.getKey();
String className = (String) prop.getValue();
Class<?> clazz;
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
throw new IOException(e);
}
// FIXME Missing type safety check
entries.put(mimeType, (Class<? extends T>) clazz);
}
}
}
/**
* Returns the capabilities for a specific format.
* @param mimeType MIME type of the format
* @return Capabilities for the specified format.
*/
@SuppressWarnings("unchecked")
public IOCapabilities getCapabilities(String mimeType) {
Class<? extends T> clazz = entries.get(mimeType);
try {
Method capabilitiesGetter =
clazz.getMethod("getCapabilities"); //$NON-NLS-1$
Set<IOCapabilities> capabilities =
(Set<IOCapabilities>) capabilitiesGetter.invoke(clazz);
for (IOCapabilities c : capabilities) {
if (c.getMimeType().equals(mimeType)) {
return c;
}
}
} catch (SecurityException | InvocationTargetException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
/**
* Returns a list of capabilities for all supported formats.
* @return Supported capabilities.
*/
public List<IOCapabilities> getCapabilities() {
List<IOCapabilities> caps =
new ArrayList<>(entries.size());
for (String mimeType : entries.keySet()) {
IOCapabilities capability = getCapabilities(mimeType);
if (capability != null) {
caps.add(capability);
}
}
return caps;
}
/**
* Returns an array of Strings containing all supported formats.
* @return Supported formats.
*/
public String[] getSupportedFormats() {
String[] formats = new String[entries.size()];
entries.keySet().toArray(formats);
return formats;
}
/**
* Returns whether the specified MIME type is supported.
* @param mimeType MIME type.
* @return {@code true} if supported, otherwise {@code false}.
*/
public boolean isFormatSupported(String mimeType) {
return entries.containsKey(mimeType);
}
/**
* Returns the type of factory products for a specified format.
* @param type Format.
* @return Class type to create new instances.
*/
protected Class<? extends T> getTypeClass(String type) {
return entries.get(type);
}
/**
* Returns an object for reading or writing the specified format.
* @param mimeType MIME type.
* @return Reader or writer for the specified MIME type.
*/
public T get(String mimeType) {
// TODO Auto-generated method stub
return null;
}
}

View file

@ -0,0 +1,66 @@
package org.xbib.graphics.graph.gral.io;
/**
* Class that stores information on a <i>reader</i> or <i>writer</i>
* implementation.
*/
public class IOCapabilities {
/** Short format name. */
private final String format;
/** Long format name. */
private final String name;
/** MIME type of format. */
private final String mimeType;
/** File extensions commonly used for this format. */
private final String[] extensions;
/**
* Creates a new {@code IOCapabilities} object with the specified
* format, name, MIME-Type and filename extensions.
* @param format Format.
* @param name Name.
* @param mimeType MIME-Type
* @param extensions Extensions.
*/
public IOCapabilities(String format, String name, String mimeType,
String[] extensions) {
this.format = format;
this.name = name;
this.mimeType = mimeType;
// TODO Check that there is at least one filename extension
this.extensions = extensions;
}
/**
* Returns the format.
* @return Format.
*/
public String getFormat() {
return format;
}
/**
* Returns the name of the format.
* @return Name.
*/
public String getName() {
return name;
}
/**
* Returns the MIME-Type of the format.
* @return Format.
*/
public String getMimeType() {
return mimeType;
}
/**
* Returns an array with Strings containing all possible filename
* extensions.
* @return Filename Extensions.
*/
public String[] getExtensions() {
return extensions;
}
}

View file

@ -0,0 +1,37 @@
package org.xbib.graphics.graph.gral.io;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* Abstract class that provides the basic functions to store capabilities of
* a reader or a writer implementation.
*/
public abstract class IOCapabilitiesStorage {
/** Set of all registered capabilities. */
private static final Set<IOCapabilities> capabilities
= new HashSet<>();
/**
* Initializes a new storage instance.
*/
protected IOCapabilitiesStorage() {
}
/**
* Returns a {@code Set} with capabilities for all supported formats.
* @return Capabilities.
*/
public static Set<IOCapabilities> getCapabilities() {
return Collections.unmodifiableSet(capabilities);
}
/**
* Adds the specified capabilities to the Set of supported formats.
* @param capabilities Capabilities to be added.
*/
protected static void addCapabilities(IOCapabilities capabilities) {
IOCapabilitiesStorage.capabilities.add(capabilities);
}
}

View file

@ -0,0 +1,44 @@
package org.xbib.graphics.graph.gral.io;
import java.util.List;
/**
* Interface for factories producing input (reader) or output (writer) classes.
* This is be used to create a extensible plug-in system for reading or writing
* data.
* @param <T> Class of the objects produced by the factory.
*/
public interface IOFactory<T> {
/**
* Returns an object for reading or writing the specified format.
* @param mimeType MIME type.
* @return Reader or writer for the specified MIME type.
*/
T get(String mimeType);
/**
* Returns the capabilities for a specific format.
* @param mimeType MIME type of the format
* @return Capabilities for the specified format.
*/
IOCapabilities getCapabilities(String mimeType);
/**
* Returns a list of capabilities for all supported formats.
* @return Supported capabilities.
*/
List<IOCapabilities> getCapabilities();
/**
* Returns an array of Strings containing all supported formats.
* @return Supported formats.
*/
String[] getSupportedFormats();
/**
* Returns whether the specified MIME type is supported.
* @param mimeType MIME type.
* @return {@code true} if supported, otherwise {@code false}.
*/
boolean isFormatSupported(String mimeType);
}

View file

@ -0,0 +1,72 @@
package org.xbib.graphics.graph.gral.io.data;
import java.util.HashMap;
import java.util.Map;
import org.xbib.graphics.graph.gral.io.IOCapabilitiesStorage;
/**
* Base implementation for classes that read data sources from input streams.
*/
public abstract class AbstractDataReader extends IOCapabilitiesStorage
implements DataReader {
/** Settings stored as (key, value) pairs. */
private final Map<String, Object> settings;
/** Default settings. */
private final Map<String, Object> defaults;
/** Data format as MIME type string. */
private final String mimeType;
/**
* Initializes a new reader with MIME type information.
* @param mimeType MIME type
*/
public AbstractDataReader(String mimeType) {
settings = new HashMap<>();
defaults = new HashMap<>();
this.mimeType = mimeType;
}
/**
* Returns the MIME type.
* @return MIME type string.
*/
public String getMimeType() {
return mimeType;
}
/**
* Returns the setting for the specified key.
* @param <T> return type
* @param key key of the setting
* @return the value of the setting
*/
@SuppressWarnings("unchecked")
public <T> T getSetting(String key) {
if (!settings.containsKey(key)) {
return (T) defaults.get(key);
}
return (T) settings.get(key);
}
/**
* Sets the setting for the specified key.
* @param <T> value type
* @param key key of the setting
* @param value value of the setting
*/
public <T> void setSetting(String key, T value) {
settings.put(key, value);
}
/**
* Defines a default value for the setting with the specified key.
* @param <T> Data type of value.
* @param key Setting key.
* @param value Default value.
*/
protected <T> void setDefault(String key, T value) {
defaults.put(key, value);
}
}

View file

@ -0,0 +1,72 @@
package org.xbib.graphics.graph.gral.io.data;
import java.util.HashMap;
import java.util.Map;
import org.xbib.graphics.graph.gral.io.IOCapabilitiesStorage;
/**
* Base implementation for classes that write data sources to output streams.
*/
public abstract class AbstractDataWriter extends IOCapabilitiesStorage
implements DataWriter {
/** Settings stored as (key, value) pairs. */
private final Map<String, Object> settings;
/** Default settings. */
private final Map<String, Object> defaults;
/** Data format as MIME type string. */
private final String mimeType;
/**
* Initializes a new writer with MIME type information.
* @param mimeType MIME type
*/
public AbstractDataWriter(String mimeType) {
settings = new HashMap<>();
defaults = new HashMap<>();
this.mimeType = mimeType;
}
/**
* Returns the MIME type.
* @return MIME type string.
*/
public String getMimeType() {
return mimeType;
}
/**
* Returns the setting for the specified key.
* @param <T> return type
* @param key key of the setting
* @return the value of the setting
*/
@SuppressWarnings("unchecked")
public <T> T getSetting(String key) {
if (!settings.containsKey(key)) {
return (T) defaults.get(key);
}
return (T) settings.get(key);
}
/**
* Sets the setting for the specified key.
* @param <T> value type
* @param key key of the setting
* @param value value of the setting
*/
public <T> void setSetting(String key, T value) {
settings.put(key, value);
}
/**
* Defines a default value for the setting with the specified key.
* @param <T> Data type of value
* @param key Setting key
* @param value Default value
*/
protected <T> void setDefault(String key, T value) {
defaults.put(key, value);
}
}

View file

@ -0,0 +1,269 @@
package org.xbib.graphics.graph.gral.io.data;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.regex.Pattern;
import org.xbib.graphics.graph.gral.data.DataSource;
import org.xbib.graphics.graph.gral.data.DataTable;
import org.xbib.graphics.graph.gral.io.IOCapabilities;
import org.xbib.graphics.graph.gral.util.Messages;
import org.xbib.graphics.graph.gral.util.StatefulTokenizer;
import org.xbib.graphics.graph.gral.util.StatefulTokenizer.Token;
/**
* <p>Class that creates a {@code DataSource} from file contents which are
* separated by a certain delimiter character. The delimiter is chosen based on
* the file type but can also be set manually. By default the comma character
* will be used as a delimiter for separating columns.</p>
* <p>{@code CSVReader} instances should be obtained by the
* {@link DataReaderFactory} rather than being created manually:</p>
* <pre>
* DataReaderFactory factory = DataReaderFactory.getInstance();
* DataReader reader = factory.get("text/csv");
* reader.read(new FileInputStream(filename), Integer.class, Double.class);
* </pre>
* @see <a href="http://tools.ietf.org/html/rfc4180">RFC 4180</a>
*/
public class CSVReader extends AbstractDataReader {
/** Key for specifying a {@link Character} value that defines the
delimiting character used to separate columns. */
public static final String SEPARATOR_CHAR = "separator"; //$NON-NLS-1$
static {
addCapabilities(new IOCapabilities(
"CSV", //$NON-NLS-1$
Messages.getString("DataIO.csvDescription"), //$NON-NLS-1$
"text/csv", //$NON-NLS-1$
new String[] {"csv", "txt"} //$NON-NLS-1$ //$NON-NLS-2$
));
addCapabilities(new IOCapabilities(
"TSV", //$NON-NLS-1$
Messages.getString("DataIO.tsvDescription"), //$NON-NLS-1$
"text/tab-separated-values", //$NON-NLS-1$
new String[] {
"tsv", "tab", "txt"} //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
));
}
/**
* Token types for analyzing CSV or TSV input.
*/
private enum CSVTokenType {
/** Type for text tokens containing empty content. */
EMPTY_SPACE,
/** Type for text tokens containing value content. */
TEXT,
/** Type for quotes that may wrap value content. */
QUOTE,
/** Type for row separators. */
ROW_SEPARATOR,
/** Type for column separators. */
COLUMN_SEPARATOR,
}
/**
*
*/
private static final class CSVTokenizer extends StatefulTokenizer {
/**
* Initializes a new tokenizer instance with a grammar to analyze CSV
* or TSV content. The character that separates columns must be
* provided.
* @param separator Column separator character.
*/
public CSVTokenizer(char separator) {
addJoinedType(CSVTokenType.TEXT);
addIgnoredType(CSVTokenType.QUOTE);
// Basic Set of rules for analyzing CSV content
putRules(
new Rule("\n|\r\n|\r", CSVTokenType.ROW_SEPARATOR),
new Rule(Pattern.quote(String.valueOf(separator)),
CSVTokenType.COLUMN_SEPARATOR),
new Rule("\"", CSVTokenType.QUOTE, "quoted"),
new Rule("[ \t]+", CSVTokenType.EMPTY_SPACE),
new Rule(".", CSVTokenType.TEXT)
);
// Set of rules that is valid inside quoted content
putRules("quoted",
new Rule("(\")\"", CSVTokenType.TEXT),
new Rule("\"", CSVTokenType.QUOTE, "#pop"),
new Rule(".", CSVTokenType.TEXT)
);
}
}
/**
* Creates a new instance with the specified MIME type. The delimiter is
* set depending on the MIME type parameter. By default a comma is used as
* a delimiter.
* @param mimeType MIME type of the file format to be read.
*/
public CSVReader(String mimeType) {
super(mimeType);
if ("text/tab-separated-values".equals(mimeType)) { //$NON-NLS-1$
setDefault(SEPARATOR_CHAR, '\t');
} else {
setDefault(SEPARATOR_CHAR, ',');
}
}
/**
* Returns a DataSource that was imported.
* @param input Input to be read.
* @param types Number types for the columns of the DataSource.
* @return DataSource Imported data.
* @throws IOException when the file format is not valid or when
* experiencing an error during file operations.
*/
@SuppressWarnings("unchecked")
public DataSource read(InputStream input, Class<? extends Comparable<?>>... types)
throws IOException {
// Read all contents from the input stream
Scanner scanner = new Scanner(input).useDelimiter("\\Z");
String content = scanner.next();
// Tokenize the string
Character separator = getSetting(SEPARATOR_CHAR);
CSVTokenizer tokenizer = new CSVTokenizer(separator);
List<Token> tokens = tokenizer.tokenize(content);
// Add row token if there was no trailing line break
Token lastToken = tokens.get(tokens.size() - 1);
if (lastToken.getType() != CSVTokenType.ROW_SEPARATOR) {
Token eof = new Token(lastToken.getEnd(), lastToken.getEnd(),
CSVTokenType.ROW_SEPARATOR, "");
tokens.add(eof);
}
// Find methods for all column data types that can be used to convert
// the text to the column data type
Map<Class<? extends Comparable<?>>, Method> parseMethods =
new HashMap<>();
for (Class<? extends Comparable<?>> type : types) {
if (parseMethods.containsKey(type)) {
continue;
}
Method parseMethod = getParseMethod(type);
if (parseMethod != null) {
parseMethods.put(type, parseMethod);
}
}
// Process the data and store the data.
DataTable data = new DataTable(types);
List<Comparable<?>> row = new LinkedList<>();
int rowIndex = 0;
int colIndex = 0;
StringBuilder cellContent = new StringBuilder();
for (Token token : tokens) {
if (token.getType() == CSVTokenType.TEXT ||
token.getType() == CSVTokenType.EMPTY_SPACE) {
// Store the token text
cellContent.append(token.getContent());
} else if (token.getType() == CSVTokenType.COLUMN_SEPARATOR ||
token.getType() == CSVTokenType.ROW_SEPARATOR) {
// Check for a valid number of columns
if (colIndex >= types.length) {
throw new IllegalArgumentException(MessageFormat.format(
"Too many columns in line {0,number,integer}: got {1,number,integer}, but expected {2,number,integer}.", //$NON-NLS-1$
rowIndex + 1, colIndex + 1, types.length));
}
// We need to add the cell to the row in both cases because
// rows don't have a trailing column token
Class<? extends Comparable<?>> colType = types[colIndex];
Method parseMethod = parseMethods.get(colType);
Comparable<?> cell = null;
try {
cell = (Comparable<?>) parseMethod.invoke(
null, cellContent.toString().trim());
} catch (IllegalArgumentException e) {
throw new RuntimeException(MessageFormat.format(
"Could not invoke method for parsing data type {0} in column {1,number,integer}.", //$NON-NLS-1$
types[colIndex].getSimpleName(), colIndex));
} catch (IllegalAccessException e) {
throw new RuntimeException(MessageFormat.format(
"Could not access method for parsing data type {0} in column {1,number,integer}.", //$NON-NLS-1$
types[colIndex].getSimpleName(), colIndex));
} catch (InvocationTargetException e) {
if (cellContent.length() > 0) {
throw new IOException(MessageFormat.format(
"Type mismatch in line {0,number,integer}, column {1,number,integer}: got \"{2}\", but expected {3} value.", //$NON-NLS-1$
rowIndex + 1, colIndex + 1, cellContent.toString(), colType.getSimpleName()));
}
}
row.add(cell);
colIndex++;
if (token.getType() == CSVTokenType.ROW_SEPARATOR) {
// Check for a valid number of columns
if (row.size() < types.length) {
throw new IllegalArgumentException(MessageFormat.format(
"Not enough columns in line {0,number,integer}: got {1,number,integer}, but expected {2,number,integer}.", //$NON-NLS-1$
rowIndex + 1, row.size(), types.length));
}
// Add the row to the table
data.add(row);
rowIndex++;
// Start a new row
row.clear();
colIndex = 0;
}
cellContent = new StringBuilder();
}
}
return data;
}
/**
* Returns a method that can return a parsed value of the specified type.
* @param c Desired type.
* @return Method that parses a data type.
*/
private static Method getParseMethod(Class<?> c) {
Method parse = null;
if (String.class.isAssignableFrom(c)) {
try {
parse = String.class.getMethod("valueOf", Object.class);
} catch (NoSuchMethodException e) {
}
} else {
for (Method m : c.getMethods()) {
boolean isStatic = m.toString().contains("static"); //$NON-NLS-1$
if (!isStatic) {
continue;
}
Class<?>[] types = m.getParameterTypes();
boolean hasStringParameter =
(types.length == 1) && String.class.equals(types[0]);
if (!hasStringParameter) {
continue;
}
// Check method name for a pattern like "parseInt*" for Integer or
// "parseSho*" for Short to avoid collisions
if (!m.getName().startsWith("parse" + c.getSimpleName().substring(0, 3))) { //$NON-NLS-1$
continue;
}
parse = m;
}
}
return parse;
}
}

View file

@ -0,0 +1,91 @@
package org.xbib.graphics.graph.gral.io.data;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import org.xbib.graphics.graph.gral.data.DataSource;
import org.xbib.graphics.graph.gral.io.IOCapabilities;
import org.xbib.graphics.graph.gral.util.Messages;
/**
* <p>Class that writes all values of a {@code DataSource} to a character
* separated file. The file then stores the values separated by a certain
* delimiter character. The delimiter is chosen based on the file type but can
* also be set manually. By default the comma character will be used as a
* delimiter for separating columns. Lines end with a carriage return and a
* line feed character.</p>
* <p>{@code CSVWriter} instances should be obtained by the
* {@link DataWriterFactory} rather than being created manually:</p>
* <pre>
* DataWriterFactory factory = DataWriterFactory.getInstance();
* DataWriter writer = factory.get("text/csv");
* writer.write(data, new FileOutputStream(filename));
* </pre>
* @see <a href="http://tools.ietf.org/html/rfc4180">RFC 4180</a>
*/
public class CSVWriter extends AbstractDataWriter {
/** Key for specifying a {@link Character} value that defines the
delimiting character used to separate columns. */
public static final String SEPARATOR_CHAR = CSVReader.SEPARATOR_CHAR;
static {
addCapabilities(new IOCapabilities(
"CSV", //$NON-NLS-1$
Messages.getString("DataIO.csvDescription"), //$NON-NLS-1$
"text/csv", //$NON-NLS-1$
new String[] {"csv", "txt"} //$NON-NLS-1$ //$NON-NLS-2$
));
addCapabilities(new IOCapabilities(
"TSV", //$NON-NLS-1$
Messages.getString("DataIO.tsvDescription"), //$NON-NLS-1$
"text/tab-separated-values", //$NON-NLS-1$
new String[] {
"tsv", "tab", "txt"} //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
));
}
/**
* Creates a new instance with the specified MIME-Type. The delimiter is
* set depending on the MIME type parameter. By default a comma is used as
* a delimiter.
* @param mimeType MIME-Type of the output file.
*/
public CSVWriter(String mimeType) {
super(mimeType);
if ("text/tab-separated-values".equals(mimeType)) { //$NON-NLS-1$
setDefault(SEPARATOR_CHAR, '\t'); //$NON-NLS-1$
} else {
setDefault(SEPARATOR_CHAR, ','); //$NON-NLS-1$
}
}
/**
* Stores the specified data source.
* @param data DataSource to be stored.
* @param output OutputStream to be written to.
* @throws IOException if writing the data failed
*/
public void write(DataSource data, OutputStream output) throws IOException {
Character separator = getSetting(SEPARATOR_CHAR);
OutputStreamWriter writer = new OutputStreamWriter(output);
int i = 0;
int colCount = data.getColumnCount();
for (Comparable<?> cell : data) {
writer.write(String.valueOf(cell));
int col = i % colCount;
if (col < colCount - 1) {
writer.write(separator);
} else {
writer.write("\r\n"); //$NON-NLS-1$
}
i++;
}
writer.close();
}
}

View file

@ -0,0 +1,40 @@
package org.xbib.graphics.graph.gral.io.data;
import java.io.IOException;
import java.io.InputStream;
import org.xbib.graphics.graph.gral.data.DataSource;
/**
* Interface that provides a function to retrieve a data source.
*/
public interface DataReader {
/**
* Returns a data source that contains the imported data.
* @param input Input to be read.
* @param types Types for the columns of the data source.
* @return Imported data.
* @throws IOException when the file format is not valid or when
* experiencing an error during file operations.
*/
@SuppressWarnings("unchecked")
DataSource read(InputStream input, Class<? extends Comparable<?>>... types)
throws IOException;
/**
* Returns the setting for the specified key.
* @param <T> return type
* @param key key of the setting
* @return the value of the setting
*/
<T> T getSetting(String key);
/**
* Sets the setting for the specified key.
* @param <T> value type
* @param key key of the setting
* @param value value of the setting
*/
<T> void setSetting(String key, T value);
}

View file

@ -0,0 +1,71 @@
package org.xbib.graphics.graph.gral.io.data;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;
import org.xbib.graphics.graph.gral.io.AbstractIOFactory;
/**
* <p>A factory class that produces {@code DataReader} instances for a
* specified format. The produced readers can be used to retrieve data from
* an {@code InputStream} and to get a {@code DataSource} instance.</p>
* <p>Example usage:</p>
* <pre>
* DataReaderFactory factory = DataReaderFactory.getInstance();
* DataReader reader = factory.get("text/csv");
* DataSource = reader.read(new FileInputStream(filename), Double.class);
* </pre>
*/
public final class DataReaderFactory extends AbstractIOFactory<DataReader> {
/** Singleton instance. */
private static DataReaderFactory instance;
/**
* Constructor that initializes the factory.
* @throws IOException if the properties file could not be found.
*/
private DataReaderFactory() throws IOException {
super("datareaders.properties"); //$NON-NLS-1$
}
/**
* Returns the instance of the factory.
* @return Instance of the factory.
*/
public static DataReaderFactory getInstance() {
if (instance == null) {
try {
instance = new DataReaderFactory();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return instance;
}
@Override
public DataReader get(String mimeType) {
DataReader reader = null;
Class<? extends DataReader> clazz = getTypeClass(mimeType);
//IOCapabilities capabilities = getCapabilities(mimeType);
try {
if (clazz != null) {
Constructor<? extends DataReader> constructor =
clazz.getDeclaredConstructor(String.class);
reader = constructor.newInstance(mimeType);
}
} catch (SecurityException | InvocationTargetException | IllegalAccessException | InstantiationException | IllegalArgumentException | NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (reader == null) {
throw new IllegalArgumentException(MessageFormat.format(
"Unsupported MIME type: {0}", mimeType)); //$NON-NLS-1$
}
return reader;
}
}

View file

@ -0,0 +1,36 @@
package org.xbib.graphics.graph.gral.io.data;
import java.io.IOException;
import java.io.OutputStream;
import org.xbib.graphics.graph.gral.data.DataSource;
/**
* Interface that provides a function to store a data source.
*/
public interface DataWriter {
/**
* Stores the specified data source.
* @param data DataSource to be stored.
* @param output OutputStream to be written to.
* @throws IOException if writing the data failed
*/
void write(DataSource data, OutputStream output) throws IOException;
/**
* Returns the setting for the specified key.
* @param <T> return type
* @param key key of the setting
* @return the value of the setting
*/
<T> T getSetting(String key);
/**
* Sets the setting for the specified key.
* @param <T> value type
* @param key key of the setting
* @param value value of the setting
*/
<T> void setSetting(String key, T value);
}

View file

@ -0,0 +1,71 @@
package org.xbib.graphics.graph.gral.io.data;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;
import org.xbib.graphics.graph.gral.io.AbstractIOFactory;
/**
* <p>A factory class that produces {@code DataWriter} instances for a
* specified format. The produced writers can be used to output a
* {@code DataSource} to a data sink.</p>
* <p>Example usage:</p>
* <pre>
* DataWriterFactory factory = DataWriterFactory.getInstance();
* DataWriter writer = factory.get("image/png");
* writer.write(data);
* </pre>
*/
public final class DataWriterFactory extends AbstractIOFactory<DataWriter> {
/** Singleton instance. */
private static DataWriterFactory instance;
/**
* Constructor that initializes the factory.
* @throws IOException if the properties file could not be found.
*/
private DataWriterFactory() throws IOException {
super("datawriters.properties");
}
/**
* Returns the instance of the factory.
* @return Instance of the factory.
*/
public static DataWriterFactory getInstance() {
if (instance == null) {
try {
instance = new DataWriterFactory();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return instance;
}
@Override
public DataWriter get(String mimeType) {
DataWriter writer = null;
Class<? extends DataWriter> clazz = getTypeClass(mimeType);
//IOCapabilities capabilities = getCapabilities(mimeType);
try {
if (clazz != null) {
Constructor<? extends DataWriter> constructor =
clazz.getDeclaredConstructor(String.class);
writer = constructor.newInstance(mimeType);
}
} catch (SecurityException | InvocationTargetException | IllegalAccessException | InstantiationException | IllegalArgumentException | NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (writer == null) {
throw new IllegalArgumentException(MessageFormat.format(
"Unsupported MIME type: {0}", mimeType)); //$NON-NLS-1$
}
return writer;
}
}

View file

@ -0,0 +1,109 @@
package org.xbib.graphics.graph.gral.io.data;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import javax.imageio.ImageIO;
import org.xbib.graphics.graph.gral.data.DataSource;
import org.xbib.graphics.graph.gral.data.DataTable;
import org.xbib.graphics.graph.gral.io.IOCapabilities;
import org.xbib.graphics.graph.gral.util.Messages;
/**
* Class that reads a data source from a binary image file. This class
* shouldn't be used directly but using the {@link DataReaderFactory}.
*/
public class ImageReader extends AbstractDataReader {
static {
addCapabilities(new IOCapabilities(
"BMP",
Messages.getString("ImageIO.bmpDescription"),
"image/bmp",
new String[] {"bmp", "dib"}
));
addCapabilities(new IOCapabilities(
"GIF",
Messages.getString("ImageIO.gifDescription"),
"image/gif",
new String[] {"gif"}
));
addCapabilities(new IOCapabilities(
"JPEG/JFIF",
Messages.getString("ImageIO.jpegDescription"),
"image/jpeg",
new String[] {
"jpg", "jpeg", "jpe",
"jif", "jfif", "jfi"}
));
addCapabilities(new IOCapabilities(
"PNG",
Messages.getString("ImageIO.pngDescription"),
"image/png",
new String[] {"png"}
));
addCapabilities(new IOCapabilities(
"WBMP",
Messages.getString("ImageIO.wbmpDescription"),
"image/vnd.wap.wbmp",
new String[] {"wbmp"}
));
}
/**
* Creates a new instance with the specified MIME type.
* @param mimeType MIME type of the file format to be read.
*/
public ImageReader(String mimeType) {
super(mimeType);
setDefault("factor", 1.0);
setDefault("offset", 0.0);
}
/**
* Returns a data source that was imported.
* @param input Input to be read.
* @param types Number types for the columns of the data source.
* @return DataSource Imported data.
* @throws IOException when the file format is not valid or when
* experiencing an error during file operations.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public DataSource read(InputStream input, Class<? extends Comparable<?>>... types)
throws IOException {
BufferedImage image = ImageIO.read(input);
int w = image.getWidth();
int h = image.getHeight();
Class[] colTypes = new Class[w];
Arrays.fill(colTypes, Double.class);
DataTable data = new DataTable(colTypes);
double factor = this.<Number>getSetting("factor").doubleValue();
double offset = this.<Number>getSetting("offset").doubleValue();
int[] pixelData = new int[w];
Double[] rowData = new Double[w];
for (int y = 0; y < h; y++) {
image.getRGB(0, y, pixelData.length, 1, pixelData, 0, 0);
for (int x = 0; x < pixelData.length; x++) {
//double a = (pixelData[x] >> 24) & 0xFF;
double r = (pixelData[x] >> 16) & 0xFF;
//double g = (pixelData[x] >> 8) & 0xFF;
//double b = (pixelData[x] >> 0) & 0xFF;
rowData[x] = r*factor + offset;
}
data.add(rowData);
}
return data;
}
}

View file

@ -0,0 +1,115 @@
package org.xbib.graphics.graph.gral.io.data;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.text.MessageFormat;
import java.util.Iterator;
import java.util.NoSuchElementException;
import javax.imageio.ImageIO;
import org.xbib.graphics.graph.gral.data.DataSource;
import org.xbib.graphics.graph.gral.io.IOCapabilities;
import org.xbib.graphics.graph.gral.util.MathUtils;
import org.xbib.graphics.graph.gral.util.Messages;
/**
* Class that writes a data source to a binary image file. This class
* shouldn't be used directly but using the {@link DataWriterFactory}.
*/
public class ImageWriter extends AbstractDataWriter {
static {
addCapabilities(new IOCapabilities(
"BMP",
Messages.getString("ImageIO.bmpDescription"),
"image/bmp",
new String[] {"bmp", "dib"}
));
addCapabilities(new IOCapabilities(
"GIF",
Messages.getString("ImageIO.gifDescription"),
"image/gif",
new String[] {"gif"}
));
addCapabilities(new IOCapabilities(
"JPEG/JFIF",
Messages.getString("ImageIO.jpegDescription"),
"image/jpeg",
new String[] {
"jpg", "jpeg", "jpe",
"jif", "jfif", "jfi"}
));
addCapabilities(new IOCapabilities(
"PNG",
Messages.getString("ImageIO.pngDescription"),
"image/png",
new String[] {"png"}
));
addCapabilities(new IOCapabilities(
"WBMP",
Messages.getString("ImageIO.wbmpDescription"),
"image/vnd.wap.wbmp",
new String[] {"wbmp"}
));
}
/**
* Creates a new instance with the specified MIME type.
* @param mimeType MIME type of the file format to be read.
*/
public ImageWriter(String mimeType) {
super(mimeType);
setDefault("factor", 1.0); //$NON-NLS-1$
setDefault("offset", 0.0); //$NON-NLS-1$
}
/**
* Stores the specified data source.
* @param data DataSource to be stored.
* @param output OutputStream to be written to.
* @throws IOException if writing the data failed
*/
public void write(DataSource data, OutputStream output) throws IOException {
int w = data.getColumnCount();
int h = data.getRowCount();
double factor = this.<Number>getSetting("factor").doubleValue(); //$NON-NLS-1$
double offset = this.<Number>getSetting("offset").doubleValue(); //$NON-NLS-1$
byte[] pixelData = new byte[w*h];
int pos = 0;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
Comparable<?> cell = data.get(x, y);
if (!(cell instanceof Number)) {
continue;
}
Number numericCell = (Number) cell;
double value = numericCell.doubleValue()*factor + offset;
byte v = (byte) Math.round(MathUtils.limit(value, 0.0, 255.0));
pixelData[pos++] = v;
}
}
BufferedImage image =
new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
image.getRaster().setDataElements(0, 0, w, h, pixelData);
Iterator<javax.imageio.ImageWriter> writers =
ImageIO.getImageWritersByMIMEType(getMimeType());
try {
javax.imageio.ImageWriter writer = writers.next();
writer.setOutput(ImageIO.createImageOutputStream(output));
writer.write(image);
} catch (NoSuchElementException e) {
throw new IOException(MessageFormat.format(
"No writer found for MIME type {0}.", getMimeType())); //$NON-NLS-1$
}
}
}

View file

@ -0,0 +1,177 @@
package org.xbib.graphics.graph.gral.io.plots;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.graphics.DrawingContext;
import org.xbib.graphics.graph.gral.io.IOCapabilities;
import org.xbib.graphics.graph.gral.io.IOCapabilitiesStorage;
import org.xbib.graphics.graph.gral.util.Messages;
/**
* Class that stores {@code Drawable} instances as bitmap graphics.
* Supported formats:
* <ul>
* <li>Windows Bitmap (BMP)</li>
* <li>Graphics Interchange Format (GIF)</li>
* <li>JPEG File Interchange Format (JPEG)</li>
* <li>Portable Network Graphics (PNG)</li>
* <li>Wireless Application Protocol Bitmap (WBMP)</li>
* </ul>
* <p>This class shouldn't be used directly but using the
* {@link DrawableWriterFactory}.</p>
*/
public class BitmapWriter extends IOCapabilitiesStorage
implements DrawableWriter {
static {
addCapabilities(new IOCapabilities(
"BMP", //$NON-NLS-1$
Messages.getString("ImageIO.bmpDescription"), //$NON-NLS-1$
"image/bmp", //$NON-NLS-1$
new String[] {"bmp", "dib"} //$NON-NLS-1$ //$NON-NLS-2$
));
addCapabilities(new IOCapabilities(
"GIF", //$NON-NLS-1$
Messages.getString("ImageIO.gifDescription"), //$NON-NLS-1$
"image/gif", //$NON-NLS-1$
new String[] {"gif"} //$NON-NLS-1$
));
addCapabilities(new IOCapabilities(
"JPEG/JFIF", //$NON-NLS-1$
Messages.getString("ImageIO.jpegDescription"), //$NON-NLS-1$
"image/jpeg", //$NON-NLS-1$
new String[] {
"jpg", "jpeg", "jpe", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
"jif", "jfif", "jfi"} //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
));
addCapabilities(new IOCapabilities(
"PNG", //$NON-NLS-1$
Messages.getString("ImageIO.pngDescription"), //$NON-NLS-1$
"image/png", //$NON-NLS-1$
new String[] {"png"} //$NON-NLS-1$
));
addCapabilities(new IOCapabilities(
"WBMP", //$NON-NLS-1$
Messages.getString("ImageIO.wbmpDescription"), //$NON-NLS-1$
"image/vnd.wap.wbmp", //$NON-NLS-1$
new String[] {"wbmp"} //$NON-NLS-1$
));
}
/** Data format as MIME type string. */
private final String mimeType;
/** Bitmap raster format. */
private final int rasterFormat;
/**
* Creates a new {@code BitmapWriter} object with the specified
* MIME-Type.
* @param mimeType Output MIME-Type.
*/
protected BitmapWriter(String mimeType) {
this.mimeType = mimeType;
boolean isAlphaSupported =
"image/png".equals(mimeType); //$NON-NLS-1$
boolean isColorSupported =
!"image/vnd.wap.wbmp".equals(mimeType); //$NON-NLS-1$
boolean isGrayscaleSupported =
!"image/vnd.wap.wbmp".equals(mimeType); //$NON-NLS-1$
if (isColorSupported) {
if (isAlphaSupported) {
rasterFormat = BufferedImage.TYPE_INT_ARGB;
} else {
rasterFormat = BufferedImage.TYPE_INT_RGB;
}
} else {
if (isGrayscaleSupported) {
rasterFormat = BufferedImage.TYPE_BYTE_GRAY;
} else {
rasterFormat = BufferedImage.TYPE_BYTE_BINARY;
}
}
// TODO Option to set transparency
// TODO Possibility to choose a background color
}
/**
* Stores the specified {@code Drawable} instance.
* @param d {@code Drawable} to be written.
* @param destination Stream to write to
* @param width Width of the image.
* @param height Height of the image.
* @throws IOException if writing to stream fails
*/
public void write(Drawable d, OutputStream destination,
double width, double height) throws IOException {
write(d, destination, 0.0, 0.0, width, height);
}
/**
* Stores the specified {@code Drawable} instance.
* @param d {@code Drawable} to be written.
* @param destination Stream to write to
* @param x Horizontal position.
* @param y Vertical position.
* @param width Width of the image.
* @param height Height of the image.
* @throws IOException if writing to stream fails
*/
public void write(Drawable d, OutputStream destination,
double x, double y, double width, double height)
throws IOException {
BufferedImage image = new BufferedImage(
(int)Math.ceil(width), (int)Math.ceil(height), rasterFormat);
Graphics2D imageGraphics = image.createGraphics();
imageGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
imageGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
imageGraphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
imageGraphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
DrawingContext context =
new DrawingContext(imageGraphics);
Iterator<ImageWriter> writers =
ImageIO.getImageWritersByMIMEType(getMimeType());
if (writers.hasNext()) {
ImageWriter writer = writers.next();
ImageOutputStream ios =
ImageIO.createImageOutputStream(destination);
writer.setOutput(ios);
Rectangle2D boundsOld = d.getBounds();
d.setBounds(x, y, width, height);
try {
d.draw(context);
writer.write(image);
} finally {
d.setBounds(boundsOld);
ios.close();
}
}
}
/**
* Returns the output format of this writer.
* @return String representing the MIME-Type.
*/
public String getMimeType() {
return this.mimeType;
}
}

View file

@ -0,0 +1,44 @@
package org.xbib.graphics.graph.gral.io.plots;
import java.io.IOException;
import java.io.OutputStream;
import org.xbib.graphics.graph.gral.graphics.Drawable;
/**
* Interface providing functions for rendering {@code Drawable}
* instances and writing them to an output stream. As an example: a plot
* can be saved into a bitmap file.
* @see DrawableWriterFactory
*/
public interface DrawableWriter {
/**
* Returns the output format of this writer.
* @return String representing the MIME-Type.
*/
String getMimeType();
/**
* Stores the specified {@code Drawable} instance.
* @param d {@code Drawable} to be written.
* @param destination Stream to write to
* @param width Width of the image.
* @param height Height of the image.
* @throws IOException if writing to stream fails
*/
void write(Drawable d, OutputStream destination,
double width, double height) throws IOException;
/**
* Stores the specified {@code Drawable} instance.
* @param d {@code Drawable} to be written.
* @param destination Stream to write to
* @param x Horizontal position.
* @param y Vertical position.
* @param width Width of the image.
* @param height Height of the image.
* @throws IOException if writing to stream fails
*/
void write(Drawable d, OutputStream destination,
double x, double y, double width, double height) throws IOException;
}

View file

@ -0,0 +1,73 @@
package org.xbib.graphics.graph.gral.io.plots;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;
import org.xbib.graphics.graph.gral.io.AbstractIOFactory;
/**
* <p>Class that provides {@code DrawableWriter} implementations for
* different file formats.</p>
*
* <p>Example Usage:</p>
* <pre>
* DrawableWriterFactory factory = DrawableWriterFactory.getInstance();
* DrawableWriter writer = factory.get("application/pdf");
* writer.write(plot, new FileOutputStream(filename));
* </pre>
*
* @see DrawableWriter
*/
public final class DrawableWriterFactory extends AbstractIOFactory<DrawableWriter> {
/** Singleton instance. */
private static DrawableWriterFactory instance;
/**
* Constructor that initializes the factory.
* @throws IOException if the properties file could not be found.
*/
private DrawableWriterFactory() throws IOException {
super("drawablewriters.properties"); //$NON-NLS-1$
}
/**
* Returns an instance of this DrawableWriterFactory.
* @return Instance.
*/
public static DrawableWriterFactory getInstance() {
if (instance == null) {
try {
instance = new DrawableWriterFactory();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return instance;
}
@Override
public DrawableWriter get(String mimeType) {
DrawableWriter writer = null;
Class<? extends DrawableWriter> clazz = getTypeClass(mimeType);
//IOCapabilities capabilities = getCapabilities(mimeType);
try {
if (clazz != null) {
Constructor<? extends DrawableWriter> constructor =
clazz.getDeclaredConstructor(String.class);
writer = constructor.newInstance(mimeType);
}
} catch (SecurityException | InvocationTargetException | IllegalAccessException | InstantiationException | IllegalArgumentException | NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (writer == null) {
throw new IllegalArgumentException(MessageFormat.format(
"Unsupported MIME type: {0}", mimeType)); //$NON-NLS-1$
}
return writer;
}
}

View file

@ -0,0 +1,166 @@
package org.xbib.graphics.graph.gral.io.plots;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.graphics.DrawingContext;
import org.xbib.graphics.graph.gral.graphics.DrawingContext.Quality;
import org.xbib.graphics.graph.gral.graphics.DrawingContext.Target;
import org.xbib.graphics.graph.gral.io.IOCapabilities;
import org.xbib.graphics.graph.gral.io.IOCapabilitiesStorage;
import org.xbib.graphics.graph.gral.util.Messages;
/**
* <p>Class that stores {@code Drawable} instances as vector graphics.
* This implementation requires the <i>VectorGraphics2D</i> library to provide
* support for the following file formats:</p>
* <ul>
* <li>Encapsulated PostScript (EPS)</li>
* <li>Portable Document Format (PDF)</li>
* <li>Scalable Vector Graphics (SVG)</li>
* </ul>
*
* <p>If the <i>VectorGraphics2D</i> library isn't available the file formats
* aren't registered in the plug-in system. This class shouldn't be used directly
* but using the {@link DrawableWriterFactory}.</p>
*/
public class VectorWriter extends IOCapabilitiesStorage
implements DrawableWriter {
/** Mapping of MIME type string to {@code Processor} implementation. */
private static final Map<String, String> processors;
/** Java package that contains the VecorGraphics2D package. */
private static final String VECTORGRAPHICS2D_PACKAGE =
"de.erichseifert.vectorgraphics2d"; //$NON-NLS-1$
static {
processors = new HashMap<>();
addCapabilities(new IOCapabilities(
"EPS", //$NON-NLS-1$
Messages.getString("ImageIO.epsDescription"), //$NON-NLS-1$
"application/postscript", //$NON-NLS-1$
new String[] {"eps", "epsf", "epsi"} //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
));
processors.put("application/postscript", "eps"); //$NON-NLS-1$ //$NON-NLS-2$
addCapabilities(new IOCapabilities(
"PDF", //$NON-NLS-1$
Messages.getString("ImageIO.pdfDescription"), //$NON-NLS-1$
"application/pdf", //$NON-NLS-1$
new String[] {"pdf"} //$NON-NLS-1$
));
processors.put("application/pdf", "pdf"); //$NON-NLS-1$ //$NON-NLS-2$
addCapabilities(new IOCapabilities(
"SVG", //$NON-NLS-1$
Messages.getString("ImageIO.svgDescription"), //$NON-NLS-1$
"image/svg+xml", //$NON-NLS-1$
new String[] {"svg", "svgz"} //$NON-NLS-1$ //$NON-NLS-2$
));
processors.put("image/svg+xml", "svg"); //$NON-NLS-1$ //$NON-NLS-2$
}
/** Current data format as MIME type string. */
private final String mimeType;
/**
* Creates a new {@code VectorWriter} object with the specified
* MIME-Type.
* @param mimeType Output MIME-Type.
*/
@SuppressWarnings("unchecked")
protected VectorWriter(String mimeType) {
this.mimeType = mimeType;
if (!processors.containsKey(mimeType)) {
throw new IllegalArgumentException(MessageFormat.format(
"Unsupported file format: {0}", mimeType)); //$NON-NLS-1$
}
}
/**
* Stores the specified {@code Drawable} instance.
* @param d {@code Drawable} to be written.
* @param destination Stream to write to
* @param width Width of the image.
* @param height Height of the image.
* @throws IOException if writing to stream fails
*/
public void write(Drawable d, OutputStream destination,
double width, double height) throws IOException {
write(d, destination, 0.0, 0.0, width, height);
}
/**
* Stores the specified {@code Drawable} instance.
* @param d {@code Drawable} to be written.
* @param destination Stream to write to
* @param x Horizontal position.
* @param y Vertical position.
* @param width Width of the image.
* @param height Height of the image.
* @throws IOException if writing to stream fails
*/
public void write(Drawable d, OutputStream destination,
double x, double y, double width, double height)
throws IOException {
// Temporary change size of drawable
Rectangle2D boundsOld = d.getBounds();
d.setBounds(x, y, width, height);
try {
// Create an instance of Graphics2D implementation
Class<?> vg2dClass = Class.forName(VECTORGRAPHICS2D_PACKAGE +
".VectorGraphics2D"); //$NON-NLS-1$
Graphics2D g = (Graphics2D) vg2dClass.getDeclaredConstructor().newInstance();
// Paint the Drawable instance
d.draw(new DrawingContext(g, Quality.QUALITY, Target.VECTOR));
// Get sequence of commands
Class<?> commandSequenceClass = Class.forName(VECTORGRAPHICS2D_PACKAGE +
".intermediate.CommandSequence"); //$NON-NLS-1$
Object commands = vg2dClass.getMethod("getCommands").invoke(g); //$NON-NLS-1$
// Define page size
Class<?> pageSizeClass = Class.forName(VECTORGRAPHICS2D_PACKAGE +
".util.PageSize"); //$NON-NLS-1$
Object pageSize = pageSizeClass
.getConstructor(Double.TYPE, Double.TYPE, Double.TYPE, Double.TYPE)
.newInstance(x, y, width, height);
// Get the corresponding VectorGraphics2D processor instance
Class<?> processorsClass = Class.forName(VECTORGRAPHICS2D_PACKAGE +
".Processors"); //$NON-NLS-1$
Object processor = processorsClass.getMethod("get", String.class) //$NON-NLS-1$
.invoke(null, processors.get(mimeType));
Class<?> processorClass = processor.getClass();
// Get document from commands with defined page size
Object document = processorClass
.getMethod("getDocument", commandSequenceClass, pageSizeClass) //$NON-NLS-1$
.invoke(processor, commands, pageSize);
// Write document to destination stream
Class<?> documentClass = Class.forName(VECTORGRAPHICS2D_PACKAGE +
".Document"); //$NON-NLS-1$
documentClass.getMethod("writeTo", OutputStream.class) //$NON-NLS-1$
.invoke(document, destination);
} catch (ClassNotFoundException | SecurityException | InvocationTargetException |
IllegalAccessException | InstantiationException | IllegalArgumentException |
NoSuchMethodException e) {
throw new IllegalStateException(e);
} finally {
d.setBounds(boundsOld);
}
}
/**
* Returns the output format of this writer.
* @return String representing the MIME-Type.
*/
public String getMimeType() {
return mimeType;
}
}

View file

@ -0,0 +1,290 @@
package org.xbib.graphics.graph.gral.navigation;
import java.util.HashSet;
import java.util.Set;
import org.xbib.graphics.graph.gral.util.PointND;
/**
* Abstract base class that can be used to control the zoom and panning of an
* object. The navigator translates actions to operations on the object.
* The class provides implementations for zooming using a zoom factor,
* management of listeners, getting and setting a main direction for actions,
* and synchronizing actions with another navigator.
*
* Derived classes must use the methods
* {@link #fireCenterChanged(NavigationEvent)} and
* {@link #fireZoomChanged(NavigationEvent)} to notify listeners of changes to
* the center or zoom level. To avoid loop states these methods must only be
* called if a value has really been changed.
*/
public abstract class AbstractNavigator implements Navigator {
/** Default zoom factor. */
public static final double DEFAULT_ZOOM_FACTOR = 1.25;
/** Default minimum of zoom factor. */
public static final double DEFAULT_ZOOM_MIN = 1e-2;
/** Default maximum of zoom factor. */
public static final double DEFAULT_ZOOM_MAX = 1e+2;
/** Object that will be notified on navigation actions. */
private final Set<NavigationListener> navigationListeners;
/** Zoom factor used for zoom in and zoom out actions. */
private double zoomFactor;
/** Minimum allowed zoom level. */
private double zoomMin;
/** Maximum allowed zoom level. */
private double zoomMax;
/** A flag that tells whether to zoom the associated object. */
private boolean zoomable;
/** A flag that tells whether to pan the associated object. */
private boolean pannable;
/** The current navigation direction. */
private NavigationDirection direction;
/**
* Initializes a new instance that is responsible for zooming and panning
* the axes with the specified names of the specified plot.
*/
public AbstractNavigator() {
navigationListeners = new HashSet<>();
zoomFactor = DEFAULT_ZOOM_FACTOR;
zoomMin = DEFAULT_ZOOM_MIN;
zoomMax = DEFAULT_ZOOM_MAX;
zoomable = true;
pannable = true;
}
/**
* Returns whether the associated object can be zoomed.
* @return {@code true} if the object can be zoomed,
* {@code false} otherwise.
*/
public boolean isZoomable() {
return zoomable;
}
/**
* Sets whether the associated object can be zoomed.
* @param zoomable A value that tells whether it should be possible to zoom
* the associated object.
*/
public void setZoomable(boolean zoomable) {
this.zoomable = zoomable;
}
/**
* Increases the current zoom level by the specified zoom factor.
*/
public void zoomIn() {
zoomInAt(null);
}
/**
* Decreases the current zoom level by the specified zoom factor.
*/
public void zoomOut() {
zoomOutAt(null);
}
@Override
public void zoomAt(double zoom, PointND<? extends Number> zoomPoint) {
if (!isZoomable()) {
return;
}
boolean pan = isPannable() && zoomPoint != null;
PointND<? extends Number> center = null;
if (pan) {
center = getCenter();
setCenter(zoomPoint);
}
setZoom(zoom);
if (pan) {
setCenter(center);
}
}
@Override
public void zoomInAt(PointND<? extends Number> zoomPoint) {
double zoom = getZoom();
zoomAt(zoom*getZoomFactor(), zoomPoint);
}
@Override
public void zoomOutAt(PointND<? extends Number> zoomPoint) {
double zoom = getZoom();
zoomAt(zoom/getZoomFactor(), zoomPoint);
}
/**
* Returns whether the associated object can be panned.
* @return {@code true} if the object can be panned,
* {@code false} otherwise.
*/
public boolean isPannable() {
return pannable;
}
/**
* Sets whether the associated object can be panned.
* @param pannable A value that tells whether it should be possible to pan
* the associated object.
*/
public void setPannable(boolean pannable) {
this.pannable = pannable;
}
/**
* Returns the factor which is used to change the zoom level on
* zoom in/out actions.
* @return The current zoom factor.
*/
public double getZoomFactor() {
return zoomFactor;
}
/**
* Sets the factor which should be used to change the zoom level on
* zoom in/out actions.
* @param factor The new zoom factor.
*/
public void setZoomFactor(double factor) {
zoomFactor = factor;
}
/**
* Returns the minimal zoom factor.
* @return Minimal zoom factor.
*/
public double getZoomMin() {
return zoomMin;
}
/**
* Sets the minimal zoom factor.
* @param min New minimal zoom factor.
*/
public void setZoomMin(double min) {
this.zoomMin = min;
}
/**
* Returns the minimal zoom factor.
* @return Maximal zoom factor.
*/
public double getZoomMax() {
return zoomMax;
}
/**
* Sets the maximal zoom factor.
* @param max New maximal zoom factor.
*/
public void setZoomMax(double max) {
this.zoomMax = max;
}
/**
* Adds the specified listener object that gets notified on changes to
* navigation information like panning or zooming.
* @param l Listener object
*/
public void addNavigationListener(NavigationListener l) {
navigationListeners.add(l);
}
/**
* Removes the specified listener object, i.e. it doesn't get notified on
* changes to navigation information like panning or zooming.
* @param l Listener object
*/
public void removeNavigationListener(NavigationListener l) {
navigationListeners.remove(l);
}
/**
* Returns the current direction of the components that will be taken into
* account for zooming and panning.
* @return Direction.
*/
public NavigationDirection getDirection() {
return direction;
}
/**
* Sets the direction of the components that will be taken into account for
* zooming and panning.
* @param direction Direction.
*/
public void setDirection(NavigationDirection direction) {
this.direction = direction;
}
/**
* Couples the actions of the current and the specified navigator. All
* actions applied to this navigator will be also applied to the specified
* navigator and vice versa.
* @param navigator Navigator which should be bound to this instance.
*/
public void connect(Navigator navigator) {
if (navigator != null && navigator != this) {
addNavigationListener(navigator);
navigator.addNavigationListener(this);
}
}
/**
* Decouples the actions of the current and the connected specified
* navigator. All actions will be applied separately to each navigator.
* @param navigator Navigator to be bound to this instance.
*/
public void disconnect(Navigator navigator) {
if (navigator != null && navigator != this) {
removeNavigationListener(navigator);
navigator.removeNavigationListener(this);
}
}
/**
* A method that gets called after the center of an object in a connected
* {@code PlotNavigator} has changed.
* @param event An object describing the change event.
*/
public void centerChanged(NavigationEvent<PointND<? extends Number>> event) {
if (event.getSource() != this) {
setCenter(event.getValueNew());
}
}
/**
* A method that gets called after the zoom level of an object in a
* connected {@code PlotNavigator} has changed.
* @param event An object describing the change event.
*/
public void zoomChanged(NavigationEvent<Double> event) {
if (event.getSource() != this) {
setZoom(event.getValueNew());
}
}
/**
* Notifies all navigation listeners that the center of one or more
* components have been changed.
* @param event An object describing the change event.
*/
protected void fireCenterChanged(NavigationEvent<PointND<? extends Number>> event) {
for (NavigationListener l : navigationListeners) {
l.centerChanged(event);
}
}
/**
* Notifies all navigation listeners that the zoom level of all components
* has been changed.
* @param event An object describing the change event.
*/
protected void fireZoomChanged(NavigationEvent<Double> event) {
for (NavigationListener l : navigationListeners) {
l.zoomChanged(event);
}
}
}

View file

@ -0,0 +1,13 @@
package org.xbib.graphics.graph.gral.navigation;
/**
* Interface for classes that can provide a {@code Navigator} which translates
* navigational actions.
*/
public interface Navigable {
/**
* Returns a navigator instance that can control the current object.
* @return A navigator instance.
*/
Navigator getNavigator();
}

View file

@ -0,0 +1,8 @@
package org.xbib.graphics.graph.gral.navigation;
/**
* Marker interface for implementation specific navigation direction,
* such as horizontal, vertical for two dimensional objects.
*/
public interface NavigationDirection {
}

View file

@ -0,0 +1,51 @@
package org.xbib.graphics.graph.gral.navigation;
/**
* Data class that describes a navigational event, like zooming or panning.
*
* @param <T> Data type of the value that has been changed.
*/
public class NavigationEvent<T> {
/** Object that has caused the change. */
private final Navigator source;
/** Value before the change. */
private final T valueOld;
/** Value after the change. */
private final T valueNew;
/**
* Initializes a new instance.
* @param source Navigator object that has caused the change.
* @param valueOld Value before the change
* @param valueNew Value after the change.
*/
public NavigationEvent(Navigator source, T valueOld, T valueNew) {
this.source = source;
this.valueOld = valueOld;
this.valueNew = valueNew;
}
/**
* Returns the navigator that has caused the change.
* @return Navigator object that has caused the change.
*/
public Navigator getSource() {
return source;
}
/**
* Returns the value before the change.
* @return Value before the change.
*/
public T getValueOld() {
return valueOld;
}
/**
* Returns the value after the change.
* @return Value after the change.
*/
public T getValueNew() {
return valueNew;
}
}

View file

@ -0,0 +1,25 @@
package org.xbib.graphics.graph.gral.navigation;
import org.xbib.graphics.graph.gral.util.PointND;
/**
* An interface for classes that want to be notified on navigation changes like
* panning or zooming.
*
* @see Navigator
*/
public interface NavigationListener {
/**
* A method that gets called after the center of an object in the
* {@code PlotNavigator} has changed.
* @param event An object describing the change event.
*/
void centerChanged(NavigationEvent<PointND<? extends Number>> event);
/**
* A method that gets called after the zoom level of an object in the
* {@code PlotNavigator} has changed.
* @param event An object describing the change event.
*/
void zoomChanged(NavigationEvent<Double> event);
}

View file

@ -0,0 +1,227 @@
package org.xbib.graphics.graph.gral.navigation;
import org.xbib.graphics.graph.gral.util.PointND;
/**
* An interface for translating navigational interactions, such as zooming
* panning to control the associated {@link Navigable} object. At the moment
* the only supported operations are zooming and panning.
*
* A navigator stores an default state of the object that can be used to reset
* the object's state after actions have been performed. This must be
* implemented with the methods {@link #setDefaultState()} and
* {@link #reset()}.
*
* Zooming and panning may be activated and deactivated using the methods
* {@link #setZoomable(boolean)} and {@link #setPannable(boolean)}.
*
* Additionally, the actions can also be bound to a certain directionlike
* horizontal or verticalby the convenience methods {@link #getDirection()}
* and {@link #setDirection(NavigationDirection)}. The data type, e.g. an enum
* type, for directions must implement the interface
* {@link NavigationDirection}.
*
* Sometimes, actions performed on an object should be applied to another
* object synchronously. The methods {@link #connect(Navigator)} and
* {@link #disconnect(Navigator)} may be implemented to provide functionality
* for this use case.
*/
public interface Navigator extends NavigationListener {
/**
* Returns whether the associated object can be zoomed.
* @return {@code true} if the object can be zoomed,
* {@code false} otherwise.
*/
boolean isZoomable();
/**
* Sets whether the associated object can be zoomed.
* @param zoomable A value that tells whether it should be possible to zoom
* the associated object.
*/
void setZoomable(boolean zoomable);
/**
* Returns the current zoom level of the associated object.
* @return Current zoom level.
*/
double getZoom();
/**
* Sets the zoom level of the associated object to the specified value.
* @param zoom New zoom level.
*/
void setZoom(double zoom);
/**
* Increases the current zoom level by the specified zoom factor.
* The zoom will only be changed if the navigator is zoomable.
*
* @see #isZoomable()
* @see #setZoomable(boolean)
*/
void zoomIn();
/**
* Decreases the current zoom level by the specified zoom factor.
* The zoom will only be changed if the navigator is zoomable.
*
* @see #isZoomable()
* @see #setZoomable(boolean)
*/
void zoomOut();
/**
* Scale the associated object at the specified point. If zooming is disabled nothing will be done. If panning is
* disabled zooming will be applied around the current center.
* @param zoom New zoom level.
* @param zoomPoint Center point for zooming in world units.
*/
void zoomAt(double zoom, PointND<? extends Number> zoomPoint);
/**
* Increases the current zoom level by the specified zoom factor and scales
* the associated object at the specified point.
* @param zoomPoint Center point for zooming in world units.
*/
void zoomInAt(PointND<? extends Number> zoomPoint);
/**
* Decreases the current zoom level by the specified zoom factor and scales
* the associated object at the specified point.
* @param zoomPoint Center point for zooming in world units.
*/
void zoomOutAt(PointND<? extends Number> zoomPoint);
/**
* Returns whether the associated object can be panned.
* @return {@code true} if the object can be panned,
* {@code false} otherwise.
*/
boolean isPannable();
/**
* Sets whether the associated object can be panned.
* @param pannable A value that tells whether it should be possible to pan
* the associated object.
*/
void setPannable(boolean pannable);
/**
* Returns the current center point. The returned point contains value in
* world units.
* @return Center point in world units.
*/
PointND<? extends Number> getCenter();
/**
* Sets a new center point. The values of the point are in world units.
* The center point will only be changed if the navigator is pannable.
* @param center New center point in world units.
* @see #isPannable()
* @see #setPannable(boolean)
*/
void setCenter(PointND<? extends Number> center);
/**
* Moves the center by the relative values of the specified point.
* The values of the point are in screen units.
* The center point will only be changed if the navigator is pannable.
* @param deltas Relative values to use for panning.
* @see #isPannable()
* @see #setPannable(boolean)
*/
void pan(PointND<? extends Number> deltas);
/**
* Sets the current state as the default state of the object.
* Resetting the navigator will then return to the default state.
*/
void setDefaultState();
/**
* Sets the object's position and zoom level to the default state.
*/
void reset();
/**
* Returns the factor which is used to change the zoom level on
* zoom in/out actions.
* @return The current zoom factor.
*/
double getZoomFactor();
/**
* Sets the factor which should be used to change the zoom level on
* zoom in/out actions.
* @param factor The new zoom factor.
*/
void setZoomFactor(double factor);
/**
* Returns the minimal zoom factor.
* @return Minimal zoom factor.
*/
double getZoomMin();
/**
* Sets the minimal zoom factor.
* @param min New minimal zoom factor.
*/
void setZoomMin(double min);
/**
* Returns the minimal zoom factor.
* @return Maximal zoom factor.
*/
double getZoomMax();
/**
* Sets the maximal zoom factor.
* @param max New maximal zoom factor.
*/
void setZoomMax(double max);
/**
* Adds the specified listener object that gets notified on changes to
* navigation information like panning or zooming.
* @param l Listener object
*/
void addNavigationListener(NavigationListener l);
/**
* Removes the specified listener object, i.e. it doesn't get notified on
* changes to navigation information like panning or zooming.
* @param l Listener object
*/
void removeNavigationListener(NavigationListener l);
/**
* Returns the current direction of the components that will be taken into
* account for zooming and panning.
* @return Direction.
*/
NavigationDirection getDirection();
/**
* Sets the direction of the components that will be taken into account for
* zooming and panning.
* @param direction Direction.
*/
void setDirection(NavigationDirection direction);
/**
* Couples the actions of the current and the specified navigator. All
* actions applied to this navigator will be also applied to the specified
* navigator and vice versa.
* @param navigator Navigator which should be bound to this instance.
*/
void connect(Navigator navigator);
/**
* Decouples the actions of the current and the connected specified
* navigator. All actions will be applied separately to each navigator.
* @param navigator Navigator to be unbound from this instance.
*/
void disconnect(Navigator navigator);
}

View file

@ -0,0 +1,835 @@
package org.xbib.graphics.graph.gral.plots;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Stroke;
import java.awt.geom.Rectangle2D;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.xbib.graphics.graph.gral.data.Column;
import org.xbib.graphics.graph.gral.data.DataChangeEvent;
import org.xbib.graphics.graph.gral.data.DataListener;
import org.xbib.graphics.graph.gral.data.DataSource;
import org.xbib.graphics.graph.gral.data.statistics.Statistics;
import org.xbib.graphics.graph.gral.graphics.Container;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.graphics.DrawableContainer;
import org.xbib.graphics.graph.gral.graphics.DrawingContext;
import org.xbib.graphics.graph.gral.graphics.layout.EdgeLayout;
import org.xbib.graphics.graph.gral.graphics.Label;
import org.xbib.graphics.graph.gral.graphics.layout.OuterEdgeLayout;
import org.xbib.graphics.graph.gral.plots.axes.Axis;
import org.xbib.graphics.graph.gral.plots.axes.AxisRenderer;
import org.xbib.graphics.graph.gral.plots.legends.Legend;
import org.xbib.graphics.graph.gral.util.GraphicsUtils;
import org.xbib.graphics.graph.gral.graphics.Location;
import org.xbib.graphics.graph.gral.util.MathUtils;
/**
* Basic implementation of a plot that can listen to changes of data sources
* and settings.
*/
public abstract class AbstractPlot extends DrawableContainer
implements Plot, DataListener {
/** Default size of the plot title relative to the size of the base font. */
private static final float DEFAULT_TITLE_FONT_SIZE = 1.5f;
/** Default space between layout components relative to the size of the base font. */
private static final float DEFAULT_LAYOUT_GAP = 2f;
/** Data sources. */
private final List<DataSource> data;
/** Set of all data sources that are visible (not hidden). */
private final Set<DataSource> dataVisible;
/** Mapping of axis names to axis objects. */
private final Map<String, Axis> axes;
/** Mapping of axis names to axis renderer objects. */
private final Map<String, AxisRenderer> axisRenderers;
/** Mapping of axis names to drawable objects. */
private final Map<String, Drawable> axisDrawables;
/** Mapping of data source columns to axes. **/
private final Map<DataSource, Map<Integer, String>> columnToAxisMappingByDataSource;
/** Minimum values of axes. **/
private final Map<String, Double> axisMin;
/** Maximum values of axes. **/
private final Map<String, Double> axisMax;
/** Title text of the plot. */
private final Label title;
/** AbstractPlot area used to render the data. */
private PlotArea plotArea;
/** Container that will store and layout the plot legend. */
private final Container legendContainer;
/** AbstractPlot legend. */
private Legend legend;
/** Paint to fill the plot background. */
private Paint background;
/** Stroke to draw the plot border. */
private transient Stroke borderStroke;
/** Paint to fill the plot border. */
private Paint borderColor;
/** Base font which is used as default for other elements of the plot and
for calculation of relative sizes. */
private Font font;
/** Decides whether a legend will be shown. */
private boolean legendVisible;
/** Positioning of the legend. */
private Location legendLocation;
/** Distance of the legend to the plot area. */
private double legendDistance;
/**
* Initializes a new {@code AbstractPlot} instance with the specified data series.
* The series will be visible by default.
* @param series Initial data series to be displayed.
*/
public AbstractPlot(DataSource... series) {
super(new EdgeLayout());
dataVisible = new HashSet<>();
axes = new HashMap<>();
axisRenderers = new HashMap<>();
axisDrawables = new HashMap<>();
columnToAxisMappingByDataSource = new HashMap<>();
axisMin = new HashMap<>();
axisMax = new HashMap<>();
data = new LinkedList<>();
for (DataSource source : series) {
add(source);
}
// No background or border by default
background = null;
borderStroke = null;
borderColor = Color.BLACK;
// Use system standard font as base font
font = Font.decode(null);
updateBaseFont();
// Create title
title = new Label();
title.setFont(font.deriveFont(DEFAULT_TITLE_FONT_SIZE*font.getSize2D()));
add(title, Location.NORTH);
// Create legend, but don't show it by default
legendContainer = new DrawableContainer(new OuterEdgeLayout(0.0));
legendLocation = Location.CENTER;
legendDistance = 2.0;
legendVisible = false;
refreshLegendLayout();
}
/**
* Draws the {@code Drawable} with the specified drawing context.
* @param context Environment used for drawing
*/
@Override
public void draw(DrawingContext context) {
Graphics2D graphics = context.getGraphics();
Paint bg = getBackground();
if (bg != null) {
GraphicsUtils.fillPaintedShape(graphics, getBounds(), bg, null);
}
Stroke stroke = getBorderStroke();
if (stroke != null) {
Paint fg = getBorderColor();
GraphicsUtils.drawPaintedShape(
graphics, getBounds(), fg, null, stroke);
}
drawComponents(context);
}
/**
* Draws the plot's axes into the specified drawing context.
* @param context Environment used for drawing.
*/
protected void drawAxes(DrawingContext context) {
for (Drawable d : axisDrawables.values()) {
if (d != null) {
d.draw(context);
}
}
}
/**
* Draws the plot's legend into the specified drawing context.
* @param context Environment used for drawing.
*/
protected void drawLegend(DrawingContext context) {
if (!isLegendVisible() || getLegend() == null) {
return;
}
getLegend().draw(context);
}
@Override
public void layout() {
super.layout();
layoutAxes();
layoutLegend();
}
/**
* Calculates the bounds of the axes.
*/
protected void layoutAxes() {
}
/**
* Calculates the bounds of the legend component.
*/
protected void layoutLegend() {
if (getPlotArea() == null) {
return;
}
Container legendContainer = getLegendContainer();
Rectangle2D plotBounds = getPlotArea().getBounds();
legendContainer.setBounds(plotBounds);
}
/**
* Returns the axis with the specified name.
* @param name Name of the axis.
* @return Axis.
*/
public Axis getAxis(String name) {
return axes.get(name);
}
/**
* Sets the axis with the specified name and the associated
* {@code AxisRenderer}.
* @param name Name of the axis.
* @param axis Axis.
*/
public void setAxis(String name, Axis axis) {
if (axis == null) {
removeAxis(name);
} else {
axes.put(name, axis);
}
}
/**
* Removes the axis with the specified name.
* @param name Name of the axis to be removed.
*/
public void removeAxis(String name) {
axes.remove(name);
axisRenderers.remove(name);
axisDrawables.remove(name);
}
/**
* Returns a collection of all names of the axes stored in this plot.
* @return The names of all axes stored in this plot.
*/
public Collection<String> getAxesNames() {
return axes.keySet();
}
/**
* Creates all axes that are defined by the current plot type.
*/
protected void createDefaultAxes() {
}
/**
* Creates all axis renderers that are defined by the current plot type.
*/
protected void createDefaultAxisRenderers() {
}
/**
* Tries to automatically set the ranges of all axes that are set to auto-scale.
* @see Axis#setAutoscaled(boolean)
*/
protected void autoscaleAxes() {
if (data.isEmpty()) {
return;
}
for (String axisName : getAxesNames()) {
autoscaleAxis(axisName);
}
}
/**
* Tries to automatically set the ranges of the axes specified by the name
* if it is set to auto-scale.
* @param axisName Name of the axis that should be scaled.
* @see Axis#setAutoscaled(boolean)
*/
public void autoscaleAxis(String axisName) {
Axis axis = getAxis(axisName);
if (axis == null || !axis.isAutoscaled()) {
return;
}
double min = getAxisMin(axisName);
double max = getAxisMax(axisName);
double margin = 0.0*(max - min);
axis.setRange(min - margin, max + margin);
}
/**
* Returns the renderer for the axis with the specified name.
* @param axisName Axis name.
* @return Instance that renders the axis.
*/
public AxisRenderer getAxisRenderer(String axisName) {
return axisRenderers.get(axisName);
}
/**
* Sets the renderer for the axis with the specified name.
* @param axisName Name of the axis to be rendered.
* @param renderer Instance to render the axis.
*/
public void setAxisRenderer(String axisName, AxisRenderer renderer) {
Drawable comp = null;
if (renderer == null) {
axisRenderers.remove(axisName);
} else {
axisRenderers.put(axisName, renderer);
Axis axis = getAxis(axisName);
comp = renderer.getRendererComponent(axis);
}
setAxisComponent(axisName, comp);
layout();
}
/**
* Returns the component that is used to draw the specified axis.
* @param axisName Name of the axis.
* @return Instance that draws the axis.
*/
protected Drawable getAxisComponent(String axisName) {
return axisDrawables.get(axisName);
}
/**
* Sets the component that should be used for drawing the specified axis.
* @param axisName Name of the axis.
* @param comp Instance that draws the axis.
*/
private void setAxisComponent(String axisName, Drawable comp) {
if (comp == null) {
axisDrawables.remove(axisName);
} else {
axisDrawables.put(axisName, comp);
}
}
/**
* Returns the drawing area of this plot.
* @return {@code PlotArea2D}.
*/
public PlotArea getPlotArea() {
return plotArea;
}
/**
* Sets the drawing area to the specified value.
* @param plotArea {@code PlotArea2D} to be set.
*/
protected void setPlotArea(PlotArea plotArea) {
if (this.plotArea != null) {
remove(this.plotArea);
this.plotArea.setBaseFont(null);
}
this.plotArea = plotArea;
if (this.plotArea != null) {
this.plotArea.setBaseFont(font);
add(this.plotArea, Location.CENTER);
}
}
/**
* Returns the title component of this plot.
* @return Label representing the title.
*/
public Label getTitle() {
return title;
}
/**
* Returns the object containing the Legend.
* @return Container.
*/
protected Container getLegendContainer() {
return legendContainer;
}
/**
* Returns the legend component.
* @return Legend.
*/
public Legend getLegend() {
return legend;
}
/**
* Sets the legend to the specified value.
* @param legend Legend to be set.
*/
protected void setLegend(Legend legend) {
if (this.legend != null) {
legendContainer.remove(this.legend);
this.legend.clear();
this.legend.setBaseFont(null);
}
this.legend = legend;
if (this.legend != null) {
this.legend.setBaseFont(font);
Location constraints = getLegendLocation();
legendContainer.add(legend, constraints);
for (DataSource source : getVisibleData()) {
legend.add(source);
}
}
}
/**
* Refreshes the positioning and spacing of the legend.
*/
protected void refreshLegendLayout() {
double absoluteLegendDistance = 0.0;
if (MathUtils.isCalculatable(legendDistance)) {
absoluteLegendDistance = legendDistance*font.getSize2D();
}
OuterEdgeLayout layout = new OuterEdgeLayout(absoluteLegendDistance);
legendContainer.setLayout(layout);
}
@Override
public Paint getBackground() {
return background;
}
@Override
public void setBackground(Paint background) {
this.background = background;
}
@Override
public Stroke getBorderStroke() {
return borderStroke;
}
@Override
public void setBorderStroke(Stroke border) {
this.borderStroke = border;
}
@Override
public Paint getBorderColor() {
return borderColor;
}
@Override
public void setBorderColor(Paint color) {
this.borderColor = color;
}
@Override
public Font getFont() {
return font;
}
@Override
public void setFont(Font font) {
this.font = font;
updateBaseFont();
}
private void updateBaseFont() {
// Update layout
float gap = DEFAULT_LAYOUT_GAP*font.getSize2D();
getLayout().setGapX(gap);
getLayout().setGapY(gap);
// Update plot area
if (plotArea != null) {
plotArea.setBaseFont(font);
}
// Update legend
if (legend != null) {
legend.setBaseFont(font);
}
}
@Override
public boolean isLegendVisible() {
return legendVisible;
}
@Override
public void setLegendVisible(boolean legendVisible) {
this.legendVisible = legendVisible;
}
@Override
public Location getLegendLocation() {
return legendLocation;
}
@Override
public void setLegendLocation(Location location) {
legendLocation = location;
if (legend != null) {
legendContainer.remove(legend);
legendContainer.add(legend, legendLocation);
}
}
@Override
public double getLegendDistance() {
return legendDistance;
}
@Override
public void setLegendDistance(double distance) {
legendDistance = distance;
refreshLegendLayout();
}
/**
* Adds a new data series to the plot which is visible by default.
* @param source Data series.
*/
public void add(DataSource source) {
add(source, true);
}
/**
* Adds a new data series to the plot.
* @param source Data series.
* @param visible {@code true} if the series should be displayed,
* {@code false} otherwise.
*/
public void add(DataSource source, boolean visible) {
add(data.size(), source, visible);
}
/**
* Inserts the specified data series to the plot at a specified position.
* @param index Position.
* @param source Data series.
* @param visible {@code true} if the series should be displayed,
* {@code false} otherwise.
*/
public void add(int index, DataSource source, boolean visible) {
data.add(index, source);
if (visible) {
dataVisible.add(source);
}
autoscaleAxes();
if (getLegend() != null) {
getLegend().add(source);
}
source.addDataListener(this);
invalidateAxisExtrema();
}
/**
* Returns whether the plot contains the specified data series.
* @param source Data series.
* @return {@code true} if the specified element is stored in the
* plot, otherwise {@code false}
*/
public boolean contains(DataSource source) {
return data.contains(source);
}
/**
* Returns the data series at a specified index.
* @param index Position of the data series.
* @return Instance of the data series.
*/
public DataSource get(int index) {
return data.get(index);
}
/**
* Deletes the specified data series from the plot.
* @param source Data series.
* @return {@code true} if the series existed,
* otherwise {@code false}.
*/
public boolean remove(DataSource source) {
source.removeDataListener(this);
dataVisible.remove(source);
if (getLegend() != null) {
getLegend().remove(source);
}
boolean existed = data.remove(source);
invalidateAxisExtrema();
return existed;
}
/**
* Removes all data series from this plot.
*/
public void clear() {
for (DataSource source : data) {
source.removeDataListener(this);
}
dataVisible.clear();
if (getLegend() != null) {
getLegend().clear();
}
data.clear();
invalidateAxisExtrema();
}
/**
* Returns the mapping of a data source column to an axis name. If no
* mapping exists {@code null} will be returned.
* @param source Data source.
* @param col Column index.
* @return Axis name or {@code null} if no mapping exists.
*/
private String getMapping(DataSource source, int col) {
Map<Integer, String> columnToAxisMapping = columnToAxisMappingByDataSource.get(source);
return columnToAxisMapping != null ? columnToAxisMapping.get(col) : null;
}
/**
* Returns the mapping of data source columns to axis names. The elements
* of returned array equal the column indexes, i.e. the first element (axis
* name) matches the first column of {@code source}. If no mapping exists
* {@code null} will be stored in the array.
* @param source Data source.
* @return Array containing axis names in the order of the columns,
* or {@code null} if no mapping exists for the column.
*/
public String[] getMapping(DataSource source) {
String[] mapping = new String[source.getColumnCount()];
for (int col = 0; col < mapping.length; col++) {
mapping[col] = getMapping(source, col);
}
return mapping;
}
/**
* Sets the mapping of data source columns to axis names. The column index
* is taken from the order of the axis names, i.e. the first column of
* {@code source} will be mapped to first element of {@code axisNames}.
* Axis names with value {@code null} will be ignored.
* @param source Data source.
* @param axisNames Sequence of axis names in the order of the columns.
*/
public void setMapping(DataSource source, String... axisNames) {
if (!contains(source)) {
throw new IllegalArgumentException(
"Data source does not exist in plot."); //$NON-NLS-1$
}
if (axisNames.length > source.getColumnCount()) {
throw new IllegalArgumentException(MessageFormat.format(
"Data source only has {0,number,integer} column, {1,number,integer} values given.", //$NON-NLS-1$
source.getColumnCount(), axisNames.length));
}
Map<Integer, String> columnToAxisMapping = new HashMap<>();
for (int col = 0; col < axisNames.length; col++) {
String axisName = axisNames[col];
if (axisName != null) {
columnToAxisMapping.put(col, axisName);
}
}
columnToAxisMappingByDataSource.put(source, columnToAxisMapping);
invalidateAxisExtrema();
}
/**
* Returns the minimum value of the axis specified by {@code axisName}.
* @param axisName Name of the axis.
* @return Minimum value for the specified axis, or {@code 0.0} if no
* minimum value can be determined.
*/
protected Double getAxisMin(String axisName) {
Double min = axisMin.get(axisName);
if (min == null) {
revalidateAxisExtrema();
min = axisMin.get(axisName);
}
if (min == null) {
min = 0.0;
}
return min;
}
/**
* Returns the maximum value of the axis specified by {@code axisName}.
* @param axisName Name of the axis.
* @return Maximum value for the specified axis, or {@code 0.0} if no
* maximum value can be determined.
*/
protected Double getAxisMax(String axisName) {
Double max = axisMax.get(axisName);
if (max == null) {
revalidateAxisExtrema();
max = axisMax.get(axisName);
}
if (max == null) {
return 0.0;
}
return max;
}
/**
* Returns a list of all data series stored in the plot.
* @return List of all data series.
*/
public List<DataSource> getData() {
return Collections.unmodifiableList(data);
}
/**
* Returns a list of all visible data series stored in the plot.
* @return List of all visible data series.
*/
public List<DataSource> getVisibleData() {
List<DataSource> visible = new LinkedList<>();
for (DataSource s : data) {
if (dataVisible.contains(s)) {
visible.add(s);
}
}
return visible;
}
/**
* Returns whether the specified data series is drawn.
* @param source Data series.
* @return {@code true} if visible, {@code false} otherwise.
*/
public boolean isVisible(DataSource source) {
return dataVisible.contains(source);
}
/**
* Changes the visibility of the specified data series.
* @param source Data series.
* @param visible {@code true} if the series should be visible,
* {@code false} otherwise.
*/
public void setVisible(DataSource source, boolean visible) {
if (visible) {
if (dataVisible.add(source)) {
invalidateAxisExtrema();
}
} else {
if (dataVisible.remove(source)) {
invalidateAxisExtrema();
}
}
}
/**
* Method that is invoked when data has been added.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been added.
*/
public void dataAdded(DataSource source, DataChangeEvent... events) {
dataChanged(source, events);
}
/**
* Method that is invoked when data has been updated.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been updated.
*/
public void dataUpdated(DataSource source, DataChangeEvent... events) {
dataChanged(source, events);
}
/**
* Method that is invoked when data has been removed.
* This method is invoked by objects that provide support for
* {@code DataListener}s and should not be called manually.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been removed.
*/
public void dataRemoved(DataSource source, DataChangeEvent... events) {
dataChanged(source, events);
}
/**
* Method that is invoked when data has been added, updated, or removed.
* @param source Data source that has been changed.
* @param events Optional event object describing the data values that
* have been changed.
*/
protected void dataChanged(DataSource source, DataChangeEvent... events) {
invalidateAxisExtrema();
autoscaleAxes();
layout();
}
/**
* Causes cached plot data to be be updated.
*/
private void invalidateAxisExtrema() {
axisMin.clear();
axisMax.clear();
}
/**
* Rebuilds cached plot data.
*/
private void revalidateAxisExtrema() {
synchronized (this) {
for (Entry<DataSource, Map<Integer, String>> entryByDataSource : columnToAxisMappingByDataSource.entrySet()) {
DataSource dataSource = entryByDataSource.getKey();
Map<Integer, String> columnToAxisMapping = entryByDataSource.getValue();
for (Entry<Integer, String> entry : columnToAxisMapping.entrySet()) {
Integer colIndex = entry.getKey();
String axisName = entry.getValue();
Column<?> col = dataSource.getColumn(colIndex);
Double min = axisMin.get(axisName);
Double max = axisMax.get(axisName);
if (min == null || max == null) {
min = col.getStatistics(Statistics.MIN);
max = col.getStatistics(Statistics.MAX);
} else {
min = Math.min(min, col.getStatistics(Statistics.MIN));
max = Math.max(max, col.getStatistics(Statistics.MAX));
}
axisMin.put(axisName, min);
axisMax.put(axisName, max);
}
}
}
}
}

View file

@ -0,0 +1,452 @@
package org.xbib.graphics.graph.gral.plots;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.util.List;
import org.xbib.graphics.graph.gral.data.DataSource;
import org.xbib.graphics.graph.gral.data.Row;
import org.xbib.graphics.graph.gral.graphics.AbstractDrawable;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.graphics.DrawingContext;
import org.xbib.graphics.graph.gral.graphics.Location;
import org.xbib.graphics.graph.gral.plots.areas.AreaRenderer;
import org.xbib.graphics.graph.gral.plots.axes.Axis;
import org.xbib.graphics.graph.gral.plots.axes.AxisRenderer;
import org.xbib.graphics.graph.gral.plots.colors.ColorMapper;
import org.xbib.graphics.graph.gral.plots.legends.AbstractLegend;
import org.xbib.graphics.graph.gral.plots.legends.Legend;
import org.xbib.graphics.graph.gral.plots.legends.ValueLegend;
import org.xbib.graphics.graph.gral.plots.lines.LineRenderer;
import org.xbib.graphics.graph.gral.plots.points.DefaultPointRenderer2D;
import org.xbib.graphics.graph.gral.plots.points.PointData;
import org.xbib.graphics.graph.gral.plots.points.PointRenderer;
import org.xbib.graphics.graph.gral.util.GraphicsUtils;
import org.xbib.graphics.graph.gral.util.MathUtils;
import org.xbib.graphics.graph.gral.util.PointND;
/**
* <p>Class that displays data in a bar plot.</p>
* <p>To create a new {@code BarPlot} simply create a new instance
* using one or more data sources. Example:</p>
* <pre>
* DataTable data = new DataTable(Integer.class, Double.class);
* data.add(2010, -5.00);
* data.add(2011, 3.25);
* data.add(2012, -0.50);
* data.add(2012, 4.00);
*
* BarPlot plot = new BarPlot(data);
* </pre>
*/
public class BarPlot extends XYPlot {
/** Relative width of the bars. 1.0 means the bars touch each other
* without gap. */
private double barWidth;
/** Minimal height of the bars in pixels. */
private double barHeightMin;
/** Decides whether the bars should be filled as a whole, or each bar on
* its own. This can e.g. be important for gradients. */
private boolean paintAllBars;
/**
* Class that renders a bar in a bar plot.
*/
public static class BarRenderer extends DefaultPointRenderer2D {
/** Plot that contains settings and renderers. */
private final BarPlot plot;
/** Stroke to draw the border of the bar. */
// Custom serialization will be done with a wrapper object
private transient Stroke borderStroke;
/** Color to fill the border of the bar. */
private Paint borderColor;
/**
* Constructor that creates a new instance and initializes it with a
* plot as data provider.
* @param plot The associated plot.
*/
public BarRenderer(BarPlot plot) {
this.plot = plot;
setValueLocation(Location.NORTH);
borderStroke = null;
borderColor = Color.BLACK;
}
/**
* Returns the stroke used to paint the outline of the point shape.
* @return Stroke used to paint the outline of the point shape.
*/
public Stroke getBorderStroke() {
return borderStroke;
}
/**
* Sets the stroke used to paint the outline of the point shape.
* @param stroke Stroke used to paint the outline of the point shape.
*/
public void setBorderStroke(Stroke stroke) {
this.borderStroke = stroke;
}
/**
* Returns the paint which is used to fill the point shape.
* @return Paint which is used to fill the point shape.
*/
public Paint getBorderColor() {
return borderColor;
}
/**
* Sets the paint which will be used to fill the point shape.
* @param color Paint which will be used to fill the point shape.
*/
public void setBorderColor(Paint color) {
this.borderColor = color;
}
@Override
public Drawable getPoint(final PointData data, final Shape shape) {
return new AbstractDrawable() {
/** Version id for serialization. */
private static final long serialVersionUID = -3145112034673683520L;
public void draw(DrawingContext context) {
BarRenderer renderer = BarRenderer.this;
Rectangle2D paintBoundaries = null;
Graphics2D graphics = context.getGraphics();
ColorMapper colors = renderer.getColor();
Paint paint = colors.get(data.index);
if (plot.isPaintAllBars()) {
AffineTransform txOld = graphics.getTransform();
Rectangle2D shapeBounds = shape.getBounds2D();
paintBoundaries = new Rectangle2D.Double();//plot.getPlotArea().getBounds();
paintBoundaries = new Rectangle2D.Double(
shapeBounds.getX(), paintBoundaries.getY() - txOld.getTranslateY(),
shapeBounds.getWidth(), paintBoundaries.getHeight()
);
}
GraphicsUtils.fillPaintedShape(
graphics, shape, paint, paintBoundaries);
Stroke stroke = renderer.getBorderStroke();
Paint strokePaint = renderer.getBorderColor();
if (stroke != null && strokePaint != null) {
GraphicsUtils.drawPaintedShape(
graphics, shape, strokePaint, null, stroke);
}
}
};
}
/**
* Returns a {@code Shape} instance that can be used for further
* calculations.
* @param data Information on axes, renderers, and values.
* @return Outline that describes the point's shape.
*/
@Override
public Shape getPointShape(PointData data) {
int colX = 0;
int colY = 1;
Axis axisX = data.axes.get(0);
Axis axisY = data.axes.get(1);
AxisRenderer axisXRenderer = data.axisRenderers.get(0);
AxisRenderer axisYRenderer = data.axisRenderers.get(1);
Row row = data.row;
if (!row.isColumnNumeric(colX) || !row.isColumnNumeric(colY)) {
return null;
}
double valueX = ((Number) row.get(colX)).doubleValue();
double valueY = ((Number) row.get(colY)).doubleValue();
double axisYOrigin = 0.0;
double barWidthRel = plot.getBarWidth();
barWidthRel = Math.max(barWidthRel, 0.0);
double barAlign = 0.5;
double barXMin = axisXRenderer
.getPosition(axisX, valueX - barWidthRel*barAlign, true, false)
.get(PointND.X);
double barXMax = axisXRenderer
.getPosition(axisX, valueX + barWidthRel*barAlign, true, false)
.get(PointND.X);
double barYVal = axisYRenderer.getPosition(
axisY, valueY, true, false).get(PointND.Y);
double barYOrigin = axisYRenderer.getPosition(
axisY, axisYOrigin, true, false).get(PointND.Y);
double barYMin = Math.min(barYVal, barYOrigin);
double barYMax = Math.max(barYVal, barYOrigin);
double barWidth = Math.abs(barXMax - barXMin);
double barHeight = Math.abs(barYMax - barYMin);
// position of the bar's left edge in screen coordinates
double barX = axisXRenderer.getPosition(
axisX, valueX, true, false).get(PointND.X);
// position of the bar's upper edge in screen coordinates
// (the origin of the screen y axis is at the top)
boolean barAboveAxis = barYMax == barYOrigin;
double barY = barAboveAxis ? 0.0 : -barHeight;
double barHeightMin = plot.getBarHeightMin();
if (MathUtils.isCalculatable(barHeightMin) && barHeightMin > 0.0 &&
barHeight < barHeightMin) {
if (barAboveAxis) {
barY += -barHeightMin + barHeight;
}
barHeight = barHeightMin;
}
return getBarShape(
barXMin - barX, barY, barWidth, barHeight);
}
/**
* Returns the shape for a bar. The default shape is defined in the
* settings, but more complex shapes may be implemented by overriding
* this method.
* @param x Distance from the left in view units (e.g. pixels).
* @param y Distance from the top in view units (e.g. pixels).
* @param width Width of the shape in view units (e.g. pixels).
* @param height Height of the shape in view units (e.g. pixels).
* @return A geometric shape for displaying a bar in bar plot.
*/
protected Shape getBarShape(double x, double y, double width, double height) {
Shape shape = getShape();
Rectangle2D shapeBounds = shape.getBounds2D();
AffineTransform tx = new AffineTransform();
tx.translate(x, y);
tx.scale(width/shapeBounds.getWidth(), height/shapeBounds.getHeight());
tx.translate(-shapeBounds.getMinX(), -shapeBounds.getMinY());
return tx.createTransformedShape(shape);
}
/**
* Returns a graphical representation of the value label to be drawn for
* the specified data value.
* @param data Information on axes, renderers, and values.
* @param shape Outline that describes the bounds for the value label.
* @return Component that can be used to draw the value label.
*/
@Override
public Drawable getValue(final PointData data, final Shape shape) {
return new AbstractDrawable() {
/** Version id for serialization. */
private static final long serialVersionUID1 = -1133369168849171793L;
public void draw(DrawingContext context) {
PointRenderer renderer = BarRenderer.this;
Row row = data.row;
if (renderer.isValueVisible()) {
int colValue = renderer.getValueColumn();
drawValueLabel(context, shape, row, data.index, colValue);
}
}
};
}
}
/**
* A legend implementation for bar plots that displays all values of the
* data source as items.
*/
public static class BarPlotLegend extends ValueLegend {
/** Version id for serialization. */
private static final long serialVersionUID = 4752278896167602641L;
/** Plot that contains settings and renderers. */
private final BarPlot plot;
/**
* Constructor that initializes the instance with a plot acting as a
* provider for settings and renderers.
* @param plot Plot.
*/
public BarPlotLegend(BarPlot plot) {
this.plot = plot;
}
@Override
protected Drawable getSymbol(final Row row) {
List<PointRenderer> pointRenderers = plot.getPointRenderers(row.getSource());
BarRenderer barRenderer = (BarRenderer) pointRenderers.get(0);
return new LegendSymbol(row, barRenderer,
plot.getFont(), plot.getLegend().getSymbolSize());
}
}
private static class LegendSymbol extends AbstractLegend.AbstractSymbol {
private final Row row;
private final BarRenderer barRenderer;
public LegendSymbol(Row row, BarRenderer barRenderer, Font font, Dimension2D symbolSize) {
super(font, symbolSize);
this.row = row;
this.barRenderer = barRenderer;
}
@Override
public void draw(DrawingContext context) {
double width = getPreferredSize().getWidth();
double height = getPreferredSize().getHeight();
Shape shape = barRenderer.getBarShape(0.0, 0.0, width, height);
Graphics2D graphics = context.getGraphics();
AffineTransform txOrig = graphics.getTransform();
graphics.translate(getX(), getY());
GraphicsUtils.fillPaintedShape(
context.getGraphics(), shape, barRenderer.getColor().get(0), null);
GraphicsUtils.drawPaintedShape(
context.getGraphics(), shape, barRenderer.getBorderColor(), null, barRenderer.getBorderStroke());
graphics.setTransform(txOrig);
}
}
/**
* Creates a new instance and initializes it with the specified
* data sources.
* @param data Data to be displayed.
*/
public BarPlot(DataSource... data) {
super(data);
((XYPlotArea2D) getPlotArea()).setMajorGridX(false);
barWidth = 1.0;
barHeightMin = 0.0;
paintAllBars = false;
Legend legend = new BarPlotLegend(this);
setLegend(legend);
autoscaleAxes();
}
@Override
public void autoscaleAxis(String axisName) {
if (!AXIS_X.equals(axisName) && !AXIS_Y.equals(axisName)) {
super.autoscaleAxis(axisName);
}
Axis axis = getAxis(axisName);
if (axis == null || !axis.isAutoscaled()) {
return;
}
List<DataSource> sources = getData();
if (sources.isEmpty()) {
return;
}
int rowCount = 0;
for (DataSource data : sources) {
rowCount = Math.max(rowCount, data.getRowCount());
}
if (rowCount == 0) {
return;
}
double min = getAxisMin(axisName);
double max = getAxisMax(axisName);
double spacing = 0.0;
if (AXIS_X.equals(axisName)) {
// Add margin
double barWidth = getBarWidth();
double margin = barWidth*(max - min)/rowCount;
spacing = margin/2.0;
} else {
// Make sure 0 is always visible for y axis
min = Math.min(min, 0.0);
max = Math.max(max, 0.0);
}
axis.setRange(min - spacing, max + spacing);
}
@Override
public void add(int index, DataSource source, boolean visible) {
super.add(index, source, visible);
// Assign default renderers
PointRenderer pointRendererDefault = new BarRenderer(this);
LineRenderer lineRendererDefault = null;
AreaRenderer areaRendererDefault = null;
// FIXME: Overwrites possible present point and line renderers
setPointRenderers(source, pointRendererDefault);
setLineRenderers(source, lineRendererDefault);
setAreaRenderers(source, areaRendererDefault);
}
/**
* Returns the width of the bars in axis coordinates.
* @return Width of the bars in axis coordinates.
*/
public double getBarWidth() {
return barWidth;
}
/**
* Sets the width of the bars in axis coordinates.
* @param barWidth Width of the bars in axis coordinates.
*/
public void setBarWidth(double barWidth) {
this.barWidth = barWidth;
}
/**
* Returns the minimum height of the bars in view units
* (e.g. pixels on screen).
* @return Minimum height of the bars in view units.
*/
public double getBarHeightMin() {
return barHeightMin;
}
/**
* Sets the minimum height of the bars in view units
* (e.g. pixels on screen).
* @param barHeightMin Minimum height of the bars in view units.
*/
public void setBarHeightMin(double barHeightMin) {
this.barHeightMin = barHeightMin;
}
/**
* Returns whether all bars are filled as a whole, or if each bar is filled
* independently.
* @return {@code true} if all bars are filled as a whole, or
* {@code false} if each bar is filled independently.
*/
public boolean isPaintAllBars() {
return paintAllBars;
}
/**
* Sets whether all bars will be filled as a whole, or if each bar will be
* filled independently.
* @param paintAllBars {@code true} to fill all bars as a whole, or
* {@code false} to fill each bar independently.
*/
public void setPaintAllBars(boolean paintAllBars) {
this.paintAllBars = paintAllBars;
}
}

View file

@ -0,0 +1,760 @@
package org.xbib.graphics.graph.gral.plots;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Dimension2D;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.util.List;
import org.xbib.graphics.graph.gral.data.Column;
import org.xbib.graphics.graph.gral.data.DataSource;
import org.xbib.graphics.graph.gral.data.DataTable;
import org.xbib.graphics.graph.gral.data.Row;
import org.xbib.graphics.graph.gral.data.statistics.Statistics;
import org.xbib.graphics.graph.gral.graphics.AbstractDrawable;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.graphics.DrawingContext;
import org.xbib.graphics.graph.gral.plots.axes.Axis;
import org.xbib.graphics.graph.gral.plots.axes.AxisRenderer;
import org.xbib.graphics.graph.gral.plots.colors.ColorMapper;
import org.xbib.graphics.graph.gral.plots.colors.ContinuousColorMapper;
import org.xbib.graphics.graph.gral.plots.colors.SingleColor;
import org.xbib.graphics.graph.gral.plots.legends.AbstractLegend;
import org.xbib.graphics.graph.gral.plots.legends.ValueLegend;
import org.xbib.graphics.graph.gral.plots.points.AbstractPointRenderer;
import org.xbib.graphics.graph.gral.plots.points.PointData;
import org.xbib.graphics.graph.gral.plots.points.PointRenderer;
import org.xbib.graphics.graph.gral.util.GraphicsUtils;
import org.xbib.graphics.graph.gral.util.PointND;
/**
* <p>Class that displays data as a box-and-whisker plot showing summaries of
* important statistical values. The data source must provide six columns to
* the {@code BoxPlot}:</p>
* <ul>
* <li>Box position (for multiple boxes)</li>
* <li>Position of the center bar (e.g. median)</li>
* <li>Length of the lower whisker and position of the bottom bar
* (e.g. minimum)</li>
* <li>Position of the bottom edge of the box (e.g. first quartile)</li>
* <li>Position of the top edge of the box (e.g. third quartile)</li>
* <li>Length of the upper whisker and position of the top bar
* (e.g. maximum)</li>
* </ul>
* <p>The utility method {@link #createBoxData(DataSource)} can be used to
* obtain common statistics for these properties from the each column of an
* existing data source.</p>
*
* <p>To create a new {@code BoxPlot} simply create a new instance using
* a data source. Example:</p>
* <pre>
* DataTable data = new DataTable(Double.class, Double.class);
* data.add(10.98, -12.34);
* data.add( 7.65, 45.67);
* data.add(43.21, 89.01);
* DataSource boxData = BoxPlot.createBoxData(data);
* BoxPlot plot = new BoxPlot(boxData);
* </pre>
*/
public class BoxPlot extends XYPlot {
/** Version id for serialization. */
private static final long serialVersionUID = -3069831535208696337L;
/**
* Class that renders a box and its whiskers in a box-and-whisker plot.
*/
public static class BoxWhiskerRenderer extends AbstractPointRenderer {
/** Version id for serialization. */
private static final long serialVersionUID = 2944482729753981341L;
/** Index of the column for the horizontal position of a box. */
private int positionColumn;
/** Index of the column for the vertical center bar. */
private int centerBarColumn;
/** Index of the column for the lower vertical bar. */
private int bottomBarColumn;
/** Index of the column for the lower end of the box. */
private int boxBottomColumn;
/** Index of the column for the upper end of the box. */
private int boxTopColumn;
/** Index of the column for the upper vertical bar. */
private int topBarColumn;
/** Relative width of each box. 1.0 means boxes touch each other. */
private double boxWidth;
/** Color mapping to fill the background of the boxes. */
private ColorMapper boxBackground;
/** Paint to fill the border of the boxes. */
private Paint boxBorderColor;
/** Stroke to draw the border of the boxes. */
private transient Stroke boxBorderStroke;
/** Paint to fill the border of the whiskers. */
private Paint whiskerColor;
/** Stroke to draw the border of the whiskers. */
private transient Stroke whiskerStroke;
/** Relative width of the vertical bars. */
private double barWidth;
/** Paint to fill the center bar. */
private Paint centerBarColor;
/** Stroke to draw the center bar. */
private transient Stroke centerBarStroke;
/**
* Constructor that creates a new instance and initializes it with a
* plot as data provider.
*/
public BoxWhiskerRenderer() {
positionColumn = 0;
centerBarColumn = 1;
bottomBarColumn = 2;
boxBottomColumn = 3;
boxTopColumn = 4;
topBarColumn = 5;
boxWidth = 0.75;
boxBackground = new SingleColor(Color.WHITE);
boxBorderColor = Color.BLACK;
boxBorderStroke = new BasicStroke(1f);
whiskerColor = Color.BLACK;
whiskerStroke = new BasicStroke(1f);
barWidth = 0.75;
centerBarColor = Color.BLACK;
centerBarStroke = new BasicStroke(
2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
}
/**
* Returns the index of the column which is used for the horizontal
* position of a box.
* @return Index of the column that is used for the horizontal position
* of a box.
*/
public int getPositionColumn() {
return positionColumn;
}
/**
* Sets the index of the column which will be used for the horizontal
* position of a box.
* @param columnIndex Index of the column that is used for the
* horizontal position of a box.
*/
public void setPositionColumn(int columnIndex) {
this.positionColumn = columnIndex;
}
/**
* Returns the index of the column which is used for the center bar.
* @return Index of the column which is used for the center bar.
*/
public int getCenterBarColumn() {
return centerBarColumn;
}
/**
* Sets the index of the column which will be used for the center bar.
* @param columnIndex Index of the column which will be used for
* the center bar.
*/
public void setCenterBarColumn(int columnIndex) {
this.centerBarColumn = columnIndex;
}
/**
* Returns the index of the column which is used for the bottom bar.
* @return Index of the column which is used for the bottom bar.
*/
public int getBottomBarColumn() {
return bottomBarColumn;
}
/**
* Sets the index of the column which will be used for the bottom bar.
* @param columnIndex Index of the column which will be used for
* the bottom bar.
*/
public void setBottomBarColumn(int columnIndex) {
this.bottomBarColumn = columnIndex;
}
/**
* Returns the index of the column which is used for the bottom edge of
* the box.
* @return Index of the column which is used for the bottom edge of the
* box.
*/
public int getBoxBottomColumn() {
return boxBottomColumn;
}
/**
* Sets the index of the column which will be used for the bottom edge
* of the box.
* @param columnIndex Index of the column which will be used for
* the bottom edge of the box.
*/
public void setColumnBoxBottom(int columnIndex) {
this.boxBottomColumn = columnIndex;
}
/**
* Returns the index of the column which is used for the top edge of
* the box.
* @return Index of the column which is used for the top edge of the
* box.
*/
public int getBoxTopColumn() {
return boxTopColumn;
}
/**
* Sets the index of the column which will be used for the top edge of
* the box.
* @param columnIndex Index of the column which will be used for the
* top edge of the box.
*/
public void setBoxTopColumn(int columnIndex) {
this.boxTopColumn = columnIndex;
}
/**
* Returns the index of the column which is used for the top bar.
* @return Index of the column which is used for the top bar.
*/
public int getTopBarColumn() {
return topBarColumn;
}
/**
* Sets the index of the column which will be used for the top bar.
* @param columnIndex Index of the column which will be used for the
* top bar.
*/
public void setTopBarColumn(int columnIndex) {
this.topBarColumn = columnIndex;
}
/**
* Returns the relative width of the box.
* @return Relative width of the box.
*/
public double getBoxWidth() {
return boxWidth;
}
/**
* Sets the relative width of the box.
* @param boxWidth Relative width of the box.
*/
public void setBoxWidth(double boxWidth) {
this.boxWidth = boxWidth;
}
/**
* Returns the mapping which is used to fill the background of a box.
* @return {@code ColorMapper} instance which is used to fill the
* background of a box.
*/
public ColorMapper getBoxBackground() {
return boxBackground;
}
/**
* Sets the mapping which will be used to fill the background of a box.
* @param color {@code ColorMapper} instance which will be used to fill
* the background of a box.
*/
public void setBoxBackground(ColorMapper color) {
this.boxBackground = color;
}
/**
* Sets the paint which will be used to fill the background of a box.
* @param color {@code Paint} instance which will be used to fill the
* background of a box.
*/
public void setBoxBackground(Paint color) {
setBoxBackground(new SingleColor(color));
}
/**
* Returns the paint which is used to fill the border of a box and the
* lines of bars.
* @return Paint which is used to fill the border of a box and the
* lines of bars.
*/
public Paint getBoxBorderColor() {
return boxBorderColor;
}
/**
* Sets the paint which will be used to fill the border of a box and
* the lines of bars.
* @param color Paint which will be used to fill the border of a box
* and the lines of bars.
*/
public void setBoxBorderColor(Paint color) {
this.boxBorderColor = color;
}
/**
* Returns the stroke which is used to paint the border of a box and
* the lines of the bars.
* @return {@code Stroke} instance which is used to paint the border of
* a box and the lines of the bars.
*/
public Stroke getBoxBorderStroke() {
return boxBorderStroke;
}
/**
* Sets the stroke which will be used to paint the border of a box and
* the lines of the bars.
* @param stroke {@code Stroke} instance which will be used to paint
* the border of a box and the lines of the bars.
*/
public void setBoxBorderStroke(Stroke stroke) {
this.boxBorderStroke = stroke;
}
/**
* Returns the paint which is used to fill the lines of the whiskers.
* @return Paint which is used to fill the lines of the whiskers.
*/
public Paint getWhiskerColor() {
return whiskerColor;
}
/**
* Sets the paint which will be used to fill the lines of the whiskers.
* @param color Paint which will be used to fill the lines of the
* whiskers.
*/
public void setWhiskerColor(Paint color) {
this.whiskerColor = color;
}
/**
* Returns the stroke which is used to paint the lines of the whiskers.
* @return {@code Stroke} instance which is used to paint the lines of
* the whiskers.
*/
public Stroke getWhiskerStroke() {
return whiskerStroke;
}
/**
* Sets the stroke which will be used to paint the lines of the
* whiskers.
* @param stroke {@code Stroke} instance which will be used to paint
* the lines of the whiskers.
*/
public void setWhiskerStroke(Stroke stroke) {
this.whiskerStroke = stroke;
}
/**
* Returns the relative width of the bottom and top bars.
* @return Relative width of the bottom and top bars.
*/
public double getBarWidth() {
return barWidth;
}
/**
* Sets the relative width of the bottom and top bars.
* @param width Relative width of the bottom and top bars.
*/
public void setBarWidth(double width) {
this.barWidth = width;
}
/**
* Returns the paint which is used to fill the lines of the center bar.
* @return Paint which is used to fill the lines of the center bar.
*/
public Paint getCenterBarColor() {
return centerBarColor;
}
/**
* Sets the paint which will be used to fill the lines of the center
* bar.
* @param color Paint which will be used to fill the lines of the
* center bar.
*/
public void setCenterBarColor(Paint color) {
this.centerBarColor = color;
}
/**
* Returns the stroke which is used to paint the lines of the center
* bar.
* @return {@code Stroke} instance which is used to paint the lines of
* the center bar.
*/
public Stroke getCenterBarStroke() {
return centerBarStroke;
}
/**
* Sets the stroke which will be used to paint the lines of the
* center bar.
* @param stroke {@code Stroke} instance which will be used to paint
* the lines of the center bar.
*/
public void setCenterBarStroke(Stroke stroke) {
this.centerBarStroke = stroke;
}
@Override
public Drawable getPoint(final PointData data, final Shape shape) {
return new AbstractDrawable() {
public void draw(DrawingContext context) {
Axis axisX = data.axes.get(0);
Axis axisY = data.axes.get(1);
AxisRenderer axisXRenderer = data.axisRenderers.get(0);
AxisRenderer axisYRenderer = data.axisRenderers.get(1);
Row row = data.row;
// Get the values from data columns
BoxWhiskerRenderer renderer = BoxWhiskerRenderer.this;
int colPos = renderer.getPositionColumn();
int colBarCenter = renderer.getCenterBarColumn();
int colBarBottom = renderer.getBottomBarColumn();
int colBoxBottom = renderer.getBoxBottomColumn();
int colBoxTop = renderer.getBoxTopColumn();
int colBarTop = renderer.getTopBarColumn();
if (!row.isColumnNumeric(colPos) ||
!row.isColumnNumeric(colBarCenter) ||
!row.isColumnNumeric(colBarBottom) ||
!row.isColumnNumeric(colBoxBottom) ||
!row.isColumnNumeric(colBoxTop) ||
!row.isColumnNumeric(colBarTop)) {
return;
}
double valueX = ((Number) row.get(colPos)).doubleValue();
double valueYBarBottom = ((Number) row.get(colBarBottom)).doubleValue();
double valueYBoxBottom = ((Number) row.get(colBoxBottom)).doubleValue();
double valueYBarCenter = ((Number) row.get(colBarCenter)).doubleValue();
double valueYBoxTop = ((Number) row.get(colBoxTop)).doubleValue();
double valueYBarTop = ((Number) row.get(colBarTop)).doubleValue();
// Calculate positions in screen units
double boxWidthRel = getBoxWidth();
double boxAlign = 0.5;
// Box X
double boxXMin = axisXRenderer
.getPosition(axisX, valueX - boxWidthRel*boxAlign, true, false)
.get(PointND.X);
double boxX = axisXRenderer.getPosition(
axisX, valueX, true, false).get(PointND.X);
double boxXMax = axisXRenderer
.getPosition(axisX, valueX + boxWidthRel*boxAlign, true, false)
.get(PointND.X);
// Box Y
double barYbottom = axisYRenderer.getPosition(
axisY, valueYBarBottom, true, false).get(PointND.Y);
double boxYBottom = axisYRenderer.getPosition(
axisY, valueYBoxBottom, true, false).get(PointND.Y);
double barYCenter = axisYRenderer.getPosition(
axisY, valueYBarCenter, true, false).get(PointND.Y);
double boxYTop = axisYRenderer.getPosition(
axisY, valueYBoxTop, true, false).get(PointND.Y);
double barYTop = axisYRenderer.getPosition(
axisY, valueYBarTop, true, false).get(PointND.Y);
double boxWidth = Math.abs(boxXMax - boxXMin);
// Bars
double barWidthRel = getBarWidth();
double barXMin = boxXMin + (1.0 - barWidthRel)*boxWidth/2.0;
double barXMax = boxXMax - (1.0 - barWidthRel)*boxWidth/2.0;
// Create shapes
// The origin of all shapes is (boxX, boxY)
Rectangle2D boxBounds = new Rectangle2D.Double(
boxXMin - boxX, boxYTop - barYCenter,
boxWidth, Math.abs(boxYTop - boxYBottom));
Rectangle2D shapeBounds = shape.getBounds2D();
AffineTransform tx = new AffineTransform();
tx.translate(boxBounds.getX(), boxBounds.getY());
tx.scale(boxBounds.getWidth()/shapeBounds.getWidth(),
boxBounds.getHeight()/shapeBounds.getHeight());
tx.translate(-shapeBounds.getMinX(), -shapeBounds.getMinY());
Shape box = tx.createTransformedShape(shape);
Line2D whiskerMax = new Line2D.Double(
0.0, boxYTop - barYCenter,
0.0, barYTop - barYCenter
);
Line2D whiskerMin = new Line2D.Double(
0.0, boxYBottom - barYCenter,
0.0, barYbottom - barYCenter
);
Line2D barMax = new Line2D.Double(
barXMin - boxX, barYTop - barYCenter,
barXMax - boxX, barYTop - barYCenter
);
Line2D barMin = new Line2D.Double(
barXMin - boxX, barYbottom - barYCenter,
barXMax - boxX, barYbottom - barYCenter
);
Line2D barCenter = new Line2D.Double(
boxXMin - boxX, 0.0,
boxXMax - boxX, 0.0
);
// Paint shapes
Graphics2D graphics = context.getGraphics();
ColorMapper paintBoxMapper = getBoxBackground();
Paint paintBox;
if (paintBoxMapper instanceof ContinuousColorMapper) {
paintBox = ((ContinuousColorMapper) paintBoxMapper)
.get(valueX);
} else {
int index = data.index;
paintBox = paintBoxMapper.get(index);
}
Paint paintStrokeBox = getBoxBorderColor();
Stroke strokeBox = getBoxBorderStroke();
Paint paintWhisker = getWhiskerColor();
Stroke strokeWhisker = getWhiskerStroke();
Paint paintBarCenter = getCenterBarColor();
Stroke strokeBarCenter = getCenterBarStroke();
// Fill box
GraphicsUtils.fillPaintedShape(
graphics, box, paintBox, box.getBounds2D());
// Save current graphics state
Paint paintOld = graphics.getPaint();
Stroke strokeOld = graphics.getStroke();
// Draw whiskers
graphics.setPaint(paintWhisker);
graphics.setStroke(strokeWhisker);
graphics.draw(whiskerMax);
graphics.draw(whiskerMin);
// Draw box and bars
graphics.setPaint(paintStrokeBox);
graphics.setStroke(strokeBox);
graphics.draw(box);
graphics.draw(barMax);
graphics.draw(barMin);
graphics.setPaint(paintBarCenter);
graphics.setStroke(strokeBarCenter);
graphics.draw(barCenter);
// Restore previous graphics state
graphics.setStroke(strokeOld);
graphics.setPaint(paintOld);
}
};
}
/**
* Returns a {@code Shape} instance that can be used for further
* calculations.
* @param data Information on axes, renderers, and values.
* @return Outline that describes the point's shape.
*/
public Shape getPointShape(PointData data) {
return getShape();
}
/**
* Returns a graphical representation of the value label to be drawn for
* the specified data value.
* @param data Information on axes, renderers, and values.
* @param shape Outline that describes the bounds for the value label.
* @return Component that can be used to draw the value label.
*/
public Drawable getValue(final PointData data, final Shape shape) {
return new AbstractDrawable() {
/** Version id for serialization. */
private static final long serialVersionUID1 = 6788431763837737592L;
public void draw(DrawingContext context) {
// TODO Implement rendering of value label
}
};
}
}
/**
* A legend implementation for box-and-whisker plots that displays all
* values of the data source as items.
*/
public static class BoxPlotLegend extends ValueLegend {
/** Version id for serialization. */
private static final long serialVersionUID = 1517792984459627757L;
/** Associated plot. */
private final BoxPlot plot;
/**
* Initializes a new instance with the specified plot.
* @param plot Associated plot.
*/
public BoxPlotLegend(BoxPlot plot) {
this.plot = plot;
}
@Override
protected Drawable getSymbol(final Row row) {
return new LegendSymbol(row, (BoxWhiskerRenderer) plot.getPointRenderers(row.getSource()).get(0),
plot.getFont(), plot.getLegend().getSymbolSize());
}
}
private static class LegendSymbol extends AbstractLegend.AbstractSymbol {
private final Row row;
private final BoxWhiskerRenderer boxWhiskerRenderer;
public LegendSymbol(Row row, BoxWhiskerRenderer boxWhiskerRenderer, Font font, Dimension2D symbolSize) {
super(font, symbolSize);
this.row = row;
this.boxWhiskerRenderer = boxWhiskerRenderer;
}
@Override
public void draw(DrawingContext context) {
Shape shape = new Rectangle2D.Double(0.0, 0.0, getBounds().getWidth(), getBounds().getHeight());
Graphics2D graphics = context.getGraphics();
AffineTransform txOrig = graphics.getTransform();
graphics.translate(getX(), getY());
GraphicsUtils.fillPaintedShape(context.getGraphics(), shape,
boxWhiskerRenderer.getBoxBackground().get(row.getIndex()), null);
GraphicsUtils.drawPaintedShape(context.getGraphics(), shape, boxWhiskerRenderer.getBoxBorderColor(),
null, boxWhiskerRenderer.getBoxBorderStroke());
graphics.setTransform(txOrig);
}
}
/**
* Initializes a new box-and-whisker plot with the specified data source.
* @param data Data to be displayed.
*/
public BoxPlot(DataSource data) {
setLegend(new BoxPlotLegend(this));
((XYPlotArea2D) getPlotArea()).setMajorGridX(false);
getAxisRenderer(AXIS_X).setTickSpacing(1.0);
getAxisRenderer(AXIS_X).setMinorTicksVisible(false);
getAxisRenderer(AXIS_X).setIntersection(-Double.MAX_VALUE);
getAxisRenderer(AXIS_Y).setIntersection(-Double.MAX_VALUE);
add(data);
autoscaleAxes();
}
/**
* Extracts statistics from the columns of an data source that are commonly
* used for box-and-whisker plots. The result is a new data source
* containing <i>column index</i>, <i>median</i>, <i>mininum</i>, <i>first
* quartile</i>, <i>third quartile</i>, and <i>maximum</i> for each column.
* @param data Original data source
* @return New data source with (columnIndex, median, min, quartile1,
* quartile3, max)
*/
@SuppressWarnings("unchecked")
public static DataSource createBoxData(DataSource data) {
if (data == null) {
throw new NullPointerException(
"Cannot extract statistics from null data source.");
}
DataTable stats = new DataTable(Integer.class, Double.class,
Double.class, Double.class, Double.class, Double.class);
// Generate statistical values for each column
for (int c = 0; c < data.getColumnCount(); c++) {
Column<?> col = data.getColumn(c);
if (!col.isNumeric()) {
continue;
}
stats.add(
c + 1,
col.getStatistics(Statistics.MEDIAN),
col.getStatistics(Statistics.MIN),
col.getStatistics(Statistics.QUARTILE_1),
col.getStatistics(Statistics.QUARTILE_3),
col.getStatistics(Statistics.MAX)
);
}
return stats;
}
@Override
public void add(int index, DataSource source, boolean visible) {
if (getData().size() > 0) {
throw new IllegalArgumentException(
"This plot type only supports a single data source."); //$NON-NLS-1$
}
// By the looks of it, some objects depend on a BoxWhiskerRenderer being present when super.add is called
// However, super.add overwrites renderers, so we have to create the BoxWhiskerRenderer twice.
BoxWhiskerRenderer renderer = new BoxWhiskerRenderer();
setPointRenderers(source, renderer);
super.add(index, source, visible);
// FIXME: Overwrites possible present point and line renderers
setLineRenderers(source, null);
setPointRenderers(source, renderer);
}
@Override
public void autoscaleAxis(String axisName) {
if (!AXIS_X.equals(axisName) && !AXIS_Y.equals(axisName)) {
super.autoscaleAxis(axisName);
}
Axis axis = getAxis(axisName);
if (axis == null || !axis.isAutoscaled()) {
return;
}
List<DataSource> sources = getData();
if (sources.isEmpty()) {
return;
}
boolean isXAxis = AXIS_X.equals(axisName);
double min = Double.MAX_VALUE;
double max = Double.MIN_VALUE;
for (DataSource data : sources) {
BoxWhiskerRenderer pointRenderer = null;
for (PointRenderer p : getPointRenderers(data)) {
if (p instanceof BoxWhiskerRenderer) {
pointRenderer = (BoxWhiskerRenderer) p;
break;
}
}
if (pointRenderer == null) {
continue;
}
int minColumnIndex, maxColumnIndex;
if (isXAxis) {
minColumnIndex = pointRenderer.getPositionColumn();
maxColumnIndex = pointRenderer.getPositionColumn();
} else {
minColumnIndex = pointRenderer.getBottomBarColumn();
maxColumnIndex = pointRenderer.getTopBarColumn();
}
min = Math.min(min, data.getColumn(minColumnIndex)
.getStatistics(Statistics.MIN));
max = Math.max(max, data.getColumn(maxColumnIndex)
.getStatistics(Statistics.MAX));
}
double spacing = (isXAxis) ? 0.5 : 0.05*(max - min);
axis.setRange(min - spacing, max + spacing);
}
}

View file

@ -0,0 +1,26 @@
package org.xbib.graphics.graph.gral.plots;
import org.xbib.graphics.graph.gral.plots.points.PointData;
import org.xbib.graphics.graph.gral.util.PointND;
/**
* Class for storing points of a plot.
*/
public class DataPoint {
/** Axes and data values that were used to create the data point. */
public final PointData data;
/** Position of the data point (n-dimensional). */
public final PointND<Double> position;
/**
* Creates a new {@code DataPoint} object with the specified position,
* {@code Drawable}, and shape.
* @param data Data that this point was created from.
* @param position Coordinates in view/screen units.
*/
public DataPoint(PointData data, PointND<Double> position) {
this.data = data;
this.position = position;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,284 @@
package org.xbib.graphics.graph.gral.plots;
import java.awt.Font;
import java.awt.Paint;
import java.awt.Stroke;
import java.util.Collection;
import java.util.List;
import org.xbib.graphics.graph.gral.data.DataSource;
import org.xbib.graphics.graph.gral.graphics.Container;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.graphics.Label;
import org.xbib.graphics.graph.gral.plots.axes.Axis;
import org.xbib.graphics.graph.gral.plots.axes.AxisRenderer;
import org.xbib.graphics.graph.gral.plots.legends.Legend;
import org.xbib.graphics.graph.gral.graphics.Location;
/**
* <p>Interface for classes that display data in a plot.</p>
* <p>Functionality includes:</p>
* <ul>
* <li>Adding axes to the plot</li>
* <li>Adding a title to the plot</li>
* <li>Adding a legend to the plot</li>
* <li>Administration of settings</li>
* </ul>
*/
public interface Plot extends Drawable, Container {
/**
* Returns the axis with the specified name.
* @param name Name of the axis.
* @return Axis.
*/
Axis getAxis(String name);
/**
* Sets the axis with the specified name and the associated
* {@code AxisRenderer}.
* @param name Name of the axis.
* @param axis Axis.
*/
void setAxis(String name, Axis axis);
/**
* Removes the axis with the specified name.
* @param name Name of the axis to be removed.
*/
void removeAxis(String name);
/**
* Returns a collection of all names of the axes stored in this plot.
* @return The names of all axes stored in this plot.
*/
Collection<String> getAxesNames();
/**
* Tries to automatically set the ranges of the axes specified by the name
* if it is set to auto-scale.
* @param axisName Name of the axis that should be scaled.
* @see Axis#setAutoscaled(boolean)
*/
void autoscaleAxis(String axisName);
/**
* Returns the renderer for the axis with the specified name.
* @param axisName Axis name.
* @return Instance that renders the axis.
*/
AxisRenderer getAxisRenderer(String axisName);
/**
* Sets the renderer for the axis with the specified name.
* @param axisName Name of the axis to be rendered.
* @param renderer Instance to render the axis.
*/
void setAxisRenderer(String axisName, AxisRenderer renderer);
/**
* Returns the drawing area of this plot.
* @return {@code PlotArea2D}.
*/
PlotArea getPlotArea();
/**
* Returns the title component of this plot.
* @return Label representing the title.
*/
Label getTitle();
/**
* Returns the legend component.
* @return Legend.
*/
Legend getLegend();
/**
* Adds a new data series to the plot which is visible by default.
* @param source Data series.
*/
void add(DataSource source);
/**
* Adds a new data series to the plot.
* @param source Data series.
* @param visible {@code true} if the series should be displayed,
* {@code false} otherwise.
*/
void add(DataSource source, boolean visible);
/**
* Inserts the specified data series to the plot at a specified position.
* @param index Position.
* @param source Data series.
* @param visible {@code true} if the series should be displayed,
* {@code false} otherwise.
*/
void add(int index, DataSource source, boolean visible);
/**
* Returns whether the plot contains the specified data series.
* @param source Data series.
* @return {@code true} if the specified element is stored in the
* plot, otherwise {@code false}
*/
boolean contains(DataSource source);
/**
* Returns the data series at a specified index.
* @param index Position of the data series.
* @return Instance of the data series.
*/
DataSource get(int index);
/**
* Deletes the specified data series from the plot.
* @param source Data series.
* @return {@code true} if the series existed,
* otherwise {@code false}.
*/
boolean remove(DataSource source);
/**
* Removes all data series from this plot.
*/
void clear();
/**
* Returns the mapping of data source columns to axis names. The elements
* of returned array equal the column indexes, i.e. the first element (axis
* name) matches the first column of {@code source}. If no mapping exists
* {@code null} will be stored in the array.
* @param source Data source.
* @return Array containing axis names in the order of the columns,
* or {@code null} if no mapping exists for the column.
*/
String[] getMapping(DataSource source);
/**
* Sets the mapping of data source columns to axis names. The column index
* is taken from the order of the axis names, i.e. the first column of
* {@code source} will be mapped to first element of {@code axisNames}.
* Axis names with value {@code null} will be ignored.
* @param source Data source.
* @param axisNames Sequence of axis names in the order of the columns.
*/
void setMapping(DataSource source, String... axisNames);
/**
* Returns a list of all data series stored in the plot.
* @return List of all data series.
*/
List<DataSource> getData();
/**
* Returns a list of all visible data series stored in the plot.
* @return List of all visible data series.
*/
List<DataSource> getVisibleData();
/**
* Returns whether the specified data series is drawn.
* @param source Data series.
* @return {@code true} if visible, {@code false} otherwise.
*/
boolean isVisible(DataSource source);
/**
* Changes the visibility of the specified data series.
* @param source Data series.
* @param visible {@code true} if the series should be visible,
* {@code false} otherwise.
*/
void setVisible(DataSource source, boolean visible);
/**
* Returns the paint which is used to fill the background of the plot.
* @return Paint which is used to fill the background of the plot.
*/
Paint getBackground();
/**
* Sets the paint which will be used to fill the background of the plot.
* @param background Paint which will be used to fill the background of the
* plot.
*/
void setBackground(Paint background);
/**
* Returns the stroke which is used to paint the border of the plot.
* @return Stroke which is used to paint the border of the plot.
*/
Stroke getBorderStroke();
/**
* Sets the stroke which will be used to paint the border of the plot.
* @param border Stroke which will be used to paint the border of the plot.
*/
void setBorderStroke(Stroke border);
/**
* Returns the paint which is used to fill the border of the plot.
* @return Paint which is used to fill the border of the plot.
*/
Paint getBorderColor();
/**
* Sets the paint which will be used to fill the border of the plot.
* @param color Paint which will be used to fill the border of the plot.
*/
void setBorderColor(Paint color);
/**
* Returns the base font used by the plot.
* @return Font used by the plot.
*/
Font getFont();
/**
* Sets the base font that will be used by the plot.
* @param font Font that will used by the plot.
*/
void setFont(Font font);
/**
* Returns whether the legend is shown.
* @return {@code true} if the legend is shown,
* {@code false} if the legend is hidden.
*/
boolean isLegendVisible();
/**
* Sets whether the legend will be shown.
* @param legendVisible {@code true} if the legend should be shown,
* {@code false} if the legend should be hidden.
*/
void setLegendVisible(boolean legendVisible);
/**
* Returns the current positioning of the legend inside the plot.
* @return Current positioning of the legend inside the plot.
*/
Location getLegendLocation();
/**
* Sets the positioning of the legend inside the plot.
* @param location Positioning of the legend inside the plot.
*/
void setLegendLocation(Location location);
/**
* Returns the spacing between the plot area and the legend.
* @return Spacing between the plot area and the legend relative to font
* height.
*/
double getLegendDistance();
/**
* Sets the spacing between the plot area and the legend.
* The distance is defined in font height.
* @param distance Spacing between the plot area and the legend relative to font
* height.
*/
void setLegendDistance(double distance);
}

View file

@ -0,0 +1,170 @@
package org.xbib.graphics.graph.gral.plots;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Paint;
import java.awt.Stroke;
import org.xbib.graphics.graph.gral.graphics.AbstractDrawable;
import org.xbib.graphics.graph.gral.graphics.DrawingContext;
import org.xbib.graphics.graph.gral.util.GraphicsUtils;
import org.xbib.graphics.graph.gral.graphics.Insets2D;
/**
* Abstract class that represents a canvas on which plot data will be drawn.
* It serves as base for specialized implementations for different plot types.
* Derived classes have to implement how the actual drawing is done.
*/
public abstract class PlotArea extends AbstractDrawable {
/** Default font used for sub-components and the calculation of relative
sizes. */
private Font baseFont;
/** Paint to fill the background. */
private Paint background;
/** Stroke to draw the border.
Property will be serialized using a wrapper. */
private transient Stroke borderStroke;
/** Paint to fill the border. */
private Paint borderColor;
/** Offset to clip plot graphics in pixels, specified relative to the
outline of the plot area. */
private Insets2D clippingOffset;
/**
* Initializes a new instance with default background color and border.
*/
public PlotArea() {
baseFont = null;
background = Color.WHITE;
borderStroke = new BasicStroke(1f);
borderColor = Color.BLACK;
clippingOffset = new Insets2D.Double(0.0);
}
/**
* Draws the background of this legend with the specified drawing context.
* @param context Environment used for drawing.
*/
protected void drawBackground(DrawingContext context) {
// FIXME duplicate code! See de.erichseifert.gral.Legend
Paint paint = getBackground();
if (paint != null) {
GraphicsUtils.fillPaintedShape(context.getGraphics(),
getBounds(), paint, null);
}
}
/**
* Draws the border of this Legend with the specified drawing context.
* @param context Environment used for drawing.
*/
protected void drawBorder(DrawingContext context) {
// FIXME duplicate code! See de.erichseifert.gral.Legend
Stroke stroke = getBorderStroke();
if (stroke != null) {
Paint borderColor = getBorderColor();
GraphicsUtils.drawPaintedShape(context.getGraphics(),
getBounds(), borderColor, null, stroke);
}
}
/**
* Draws the data using the specified drawing context.
* @param context Environment used for drawing.
*/
protected abstract void drawPlot(DrawingContext context);
/**
* Returns the current font used as a default for sub-components ans for
* calculation of relative sizes.
* @return Current base font.
*/
public Font getBaseFont() {
return baseFont;
}
/**
* Sets the new font that will be used as a default for sub-components and
* for calculation of relative sizes. This method is only used internally
* to propagate the base font and shouldn't be used manually.
* @param baseFont New base font.
*/
public void setBaseFont(Font baseFont) {
this.baseFont = baseFont;
}
/**
* Returns the paint which is used to draw the background of the plot area.
* @return Paint which is used to fill the background.
*/
public Paint getBackground() {
return background;
}
/**
* Sets the paint which will be used to fill the background of the plot
* area.
* @param background Paint which should be used to fill the background.
*/
public void setBackground(Paint background) {
this.background = background;
}
/**
* Returns the stroke which is used to draw the border of the plot area.
* @return Stroke which is used to draw the border.
*/
public Stroke getBorderStroke() {
return borderStroke;
}
/**
* Sets the stroke which will be used to draw the border of the plot area.
* @param stroke Stroke which should be used to draw the border.
*/
public void setBorderStroke(Stroke stroke) {
this.borderStroke = stroke;
}
/**
* Returns the paint which is used to fill the border of the plot area.
* @return Paint which is used to fill the border.
*/
public Paint getBorderColor() {
return borderColor;
}
/**
* Sets the paint which will be used to fill the border of the plot area.
* @param color Paint which should be used to fill the border.
*/
public void setBorderColor(Paint color) {
this.borderColor = color;
}
/**
* Returns the clipping offset of the plotted data relative to the plot
* area. Positive inset values result in clipping inside the plot area,
* negative values result in clipping outside the plot area.
* Specifying a {@code null} values will turn off clipping.
* @return Clipping offset in pixels relative to the outline of the plot
* area.
*/
public Insets2D getClippingOffset() {
return clippingOffset;
}
/**
* Sets the clipping offset of the plotted data relative to the plot area.
* Positive inset values result in clipping inside the plot area,
* negative values result in clipping outside the plot area.
* Specifying a {@code null} values will turn off clipping.
* @param offset Clipping offset in pixels relative to the outline of the
* plot area.
*/
public void setClippingArea(Insets2D offset) {
this.clippingOffset = offset;
}
}

View file

@ -0,0 +1,447 @@
package org.xbib.graphics.graph.gral.plots;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.xbib.graphics.graph.gral.navigation.AbstractNavigator;
import org.xbib.graphics.graph.gral.navigation.NavigationEvent;
import org.xbib.graphics.graph.gral.plots.axes.Axis;
import org.xbib.graphics.graph.gral.plots.axes.AxisRenderer;
import org.xbib.graphics.graph.gral.util.MathUtils;
import org.xbib.graphics.graph.gral.util.PointND;
/**
* <p>Abstract base class that can be used to control the zoom and panning of a
* plot. The navigator translates the interaction to operations on a defined
* set of axes: Zooming is translated as scaling, panning is done by uniformly
* changing the minimum and maximum values of the axes.</p>
*
* <p>Additionally, the actions can also be bound to a certain direction by
* defining a more restricted set of axes. The methods {@link #getDirection()}
* and {@link #setDirection(de.erichseifert.gral.navigation.NavigationDirection)}
* provide a convenient way for setting predefined sets of axes.</p>
*/
public abstract class PlotNavigator extends AbstractNavigator {
/** AbstractPlot that will be navigated. */
private final Plot plot;
/** Mapping of axis name to information on center and zoom. */
private final Map<String, NavigationInfo> infos;
/** Axes affected by navigation. */
private final List<String> axes;
/**
* Data class for storing navigational information for an axis.
*/
protected static final class NavigationInfo {
/** Minimum value of the original axis. */
private final Number minOriginal;
/** Maximum value of the original axis. */
private final Number maxOriginal;
/** Center value of the original axis. */
private final double centerOriginal;
/** Current center value. */
private double center;
/** Current zoom level. */
private double zoom;
/**
* Initializes a new {@code NavigationInfo} instance.
* @param min Minimum value in axis units.
* @param max Maximum value in axis units.
* @param center Center in axis units.
*/
public NavigationInfo(Number min, Number max, double center) {
this.minOriginal = min;
this.maxOriginal = max;
this.centerOriginal = center;
this.center = centerOriginal;
this.zoom = 1.0;
}
/**
* Returns the original minimum value.
* @return Original minimum value.
*/
public Number getMinOriginal() {
return minOriginal;
}
/**
* Returns the original maximum value.
* @return Original maximum value.
*/
public Number getMaxOriginal() {
return maxOriginal;
}
/**
* Returns the original center value.
* @return Original center value.
*/
public double getCenterOriginal() {
return centerOriginal;
}
/**
* Returns the current center value.
* @return Current center value.
*/
public double getCenter() {
return center;
}
/**
* Sets the current center value.
* @param center New center value.
*/
public void setCenter(double center) {
this.center = center;
}
/**
* Returns the current zoom factor.
* @return Current zoom factor.
*/
public double getZoom() {
return zoom;
}
/**
* Sets the current zoom factor.
* @param zoom New zoom factor.
*/
public void setZoom(double zoom) {
this.zoom = zoom;
}
}
/**
* Initializes a new instance that is responsible for zooming and panning
* the axes with the specified names of the specified plot.
* @param plot AbstractPlot to be zoomed and panned.
* @param axesNames Names of the axes that should be controlled by this
* navigator.
*/
public PlotNavigator(Plot plot, List<String> axesNames) {
axes = new LinkedList<>();
infos = new HashMap<>();
this.plot = plot;
setAxes(axesNames);
}
/**
* Initializes a new instance that is responsible for zooming and panning
* the axes with the specified names of the specified plot.
* @param plot AbstractPlot to be zoomed and panned.
* @param axesNames Names of the axes that should be controlled by this
* navigator.
*/
public PlotNavigator(Plot plot, String... axesNames) {
this(plot, Arrays.asList(axesNames));
}
/**
* Refreshes the values of all axis to reflect navigation actions.
*/
private void refresh() {
for (String axisName : getAxes()) {
NavigationInfo info = getInfo(axisName);
if (info == null) {
continue;
}
AxisRenderer renderer = getPlot().getAxisRenderer(axisName);
if (renderer == null) {
continue;
}
Axis axis = getPlot().getAxis(axisName);
// Original range in screen units
// Most up-to-date view coordinates (axis's layout) must be used
double minOrig = renderer.worldToView(
axis, info.getMinOriginal(), true);
double maxOrig = renderer.worldToView(
axis, info.getMaxOriginal(), true);
double rangeOrig = maxOrig - minOrig;
// New axis scale
double zoom = info.getZoom();
double range = rangeOrig/zoom;
double center = renderer.worldToView(axis, info.getCenter(), true);
Number min = renderer.viewToWorld(axis, center - 0.5*range, true);
Number max = renderer.viewToWorld(axis, center + 0.5*range, true);
// Change axis
axis.setRange(min, max);
}
}
/**
* Returns the plot stored in this instance.
* @return Stored plot object.
*/
protected Plot getPlot() {
return plot;
}
/**
* Returns the current zoom level of the associated object.
* @return Current zoom level.
*/
public double getZoom() {
double zoom = 0.0;
int count = 0;
for (String axisName : getAxes()) {
NavigationInfo info = getInfo(axisName);
if (info == null) {
continue;
}
if (!MathUtils.isCalculatable(info.getZoom())) {
continue;
}
zoom += info.getZoom();
count++;
}
return zoom / count;
}
/**
* Sets the zoom level of the associated object to the specified value.
* @param zoomNew New zoom level.
*/
public void setZoom(double zoomNew) {
if (!isZoomable() || (zoomNew <= 0.0) ||
!MathUtils.isCalculatable(zoomNew)) {
return;
}
double zoomOld = getZoom();
zoomNew = MathUtils.limit(zoomNew, getZoomMin(), getZoomMax());
if (zoomOld == zoomNew) {
return;
}
for (String axisName : getAxes()) {
NavigationInfo info = getInfo(axisName);
if (info == null) {
continue;
}
info.setZoom(zoomNew);
}
NavigationEvent<Double> event =
new NavigationEvent<>(this, zoomOld, zoomNew);
fireZoomChanged(event);
refresh();
}
/**
* Returns the current center point. The returned point contains value in
* world units.
* @return Center point in world units.
*/
public PointND<? extends Number> getCenter() {
List<String> axesNames = getAxes();
Double[] centerCoords = new Double[axesNames.size()];
int axisIndex = 0;
for (String axisName : axesNames) {
NavigationInfo info = getInfo(axisName);
if (info != null) {
double axisCenter = info.getCenter();
centerCoords[axisIndex] = axisCenter;
}
axisIndex++;
}
return new PointND<>(centerCoords);
}
/**
* Sets a new center point. The values of the point are in world units.
* @param center New center point in world units.
*/
public void setCenter(PointND<? extends Number> center) {
if (!isPannable()) {
return;
}
PointND<? extends Number> centerOld = getCenter();
if (centerOld.equals(center)) {
return;
}
List<String> axesNames = getAxes();
int axisIndex = 0;
for (String axisName : axesNames) {
NavigationInfo info = getInfo(axisName);
if (info != null) {
Number centerCoordNew = center.get(axisIndex);
info.setCenter(centerCoordNew.doubleValue());
}
axisIndex++;
}
NavigationEvent<PointND<? extends Number>> event =
new NavigationEvent<>(this, centerOld, center);
fireCenterChanged(event);
refresh();
}
/**
* Moves the center by the relative values of the specified point.
* The values of the point are in screen units.
* @param deltas Relative values to use for panning.
*/
public void pan(PointND<? extends Number> deltas) {
if (!isPannable()) {
return;
}
PointND<? extends Number> centerOld = getCenter();
Double[] centerCoords = new Double[centerOld.getDimensions()];
int axisIndex = 0;
for (String axisName : getAxes()) {
NavigationInfo info = getInfo(axisName);
if (info != null) {
double delta = getDimensionValue(axisName, deltas).doubleValue();
AxisRenderer renderer =
getPlot().getAxisRenderer(axisName);
if (renderer != null) {
boolean swapped = renderer.isShapeDirectionSwapped();
if (swapped) {
delta = -delta;
}
Axis axis = getPlot().getAxis(axisName);
// Fetch current center on screen
double center = renderer.worldToView(
axis, info.getCenter(), true);
// Move center and convert it to axis coordinates
Number centerNew = renderer.viewToWorld(
axis, center - delta, true);
// Change axis (world units)
info.setCenter(centerNew.doubleValue());
centerCoords[axisIndex] = centerNew.doubleValue();
}
}
axisIndex++;
}
PointND<? extends Number> centerNew = new PointND<>(centerCoords);
NavigationEvent<PointND<? extends Number>> event =
new NavigationEvent<>(this, centerOld, centerNew);
fireCenterChanged(event);
refresh();
}
/**
* Sets the current state as the default state of the object.
* Resetting the navigator will then return to the default state.
*/
public void setDefaultState() {
infos.clear();
for (String axisName : getAxes()) {
Axis axis = getPlot().getAxis(axisName);
if (axis == null) {
continue;
}
double min;
double max;
Number center = 0.0;
AxisRenderer renderer = getPlot().getAxisRenderer(axisName);
if (renderer != null && axis.isValid()) {
min = renderer.worldToView(axis, axis.getMin(), false);
max = renderer.worldToView(axis, axis.getMax(), false);
if (MathUtils.isCalculatable(min) && MathUtils.isCalculatable(max)) {
center = renderer.viewToWorld(axis, (min + max)/2.0, false);
}
}
NavigationInfo info = new NavigationInfo(
axis.getMin(), axis.getMax(), center.doubleValue());
infos.put(axisName, info);
}
}
/**
* Sets the object's position and zoom level to the default state.
*/
public void reset() {
double zoomOld = getZoom();
double zoomNew = 1.0;
PointND<? extends Number> centerOld = getCenter();
List<String> axesNames = getAxes();
Double[] centerCoordsOriginal = new Double[centerOld.getDimensions()];
int axisIndex = 0;
for (String axisName : axesNames) {
NavigationInfo info = getInfo(axisName);
if (info != null) {
double centerCoordOriginal = info.getCenterOriginal();
centerCoordsOriginal[axisIndex] = centerCoordOriginal;
info.setCenter(centerCoordOriginal);
info.setZoom(zoomNew);
}
axisIndex++;
}
PointND<Double> centerNew = new PointND<>(centerCoordsOriginal);
NavigationEvent<PointND<? extends Number>> panEvent =
new NavigationEvent<>(this, centerOld, centerNew);
fireCenterChanged(panEvent);
NavigationEvent<Double> zoomEvent =
new NavigationEvent<>(this, zoomOld, 1.0);
fireZoomChanged(zoomEvent);
refresh();
}
/**
* Returns navigational information for the axis with specified name.
* @param axisName Axis name.
* @return Navigational information.
*/
protected NavigationInfo getInfo(String axisName) {
return infos.get(axisName);
}
/**
* Returns the names of all axes handled by this object.
* @return Names of all axes handled by this object.
*/
protected List<String> getAxes() {
return Collections.unmodifiableList(axes);
}
/**
* Sets the names of the axes that should be handled by this object.
* @param axesNames Names of the axes that should be handled.
*/
protected void setAxes(List<String> axesNames) {
axes.clear();
axes.addAll(axesNames);
setDefaultState();
}
/**
* Sets the names of the axes that should be handled by this object.
* @param axesNames Names of the axes that should be handled.
*/
protected void setAxes(String... axesNames) {
setAxes(Arrays.asList(axesNames));
}
/**
* Returns the number dimensions the associated plot can handle. For a
* one-dimensional plot like {@link PiePlot} this is 1, for a
* two-dimensional plot like {@link XYPlot} this is 2, and so on.
* @return Number of dimensions the associated plot can handle.
*/
protected abstract int getDimensions();
/**
* Return the index that can be used to access data for the axis with the
* specified name. The returned index must be larger than or equal to 0 and
* smaller than the result of {@link #getDimensions()}.
* @param axisName Name of the axis.
* @param values Data values.
* @return Dimension index.
*/
protected abstract Number getDimensionValue(
String axisName, PointND<? extends Number> values);
}

View file

@ -0,0 +1,400 @@
package org.xbib.graphics.graph.gral.plots;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Dimension2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.xbib.graphics.graph.gral.data.DataSource;
import org.xbib.graphics.graph.gral.data.DataTable;
import org.xbib.graphics.graph.gral.data.Row;
import org.xbib.graphics.graph.gral.data.statistics.Statistics;
import org.xbib.graphics.graph.gral.graphics.AbstractDrawable;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.graphics.DrawingContext;
import org.xbib.graphics.graph.gral.plots.axes.Axis;
import org.xbib.graphics.graph.gral.plots.axes.AxisRenderer;
import org.xbib.graphics.graph.gral.plots.colors.ColorMapper;
import org.xbib.graphics.graph.gral.plots.colors.ContinuousColorMapper;
import org.xbib.graphics.graph.gral.plots.colors.Grayscale;
import org.xbib.graphics.graph.gral.plots.points.AbstractPointRenderer;
import org.xbib.graphics.graph.gral.plots.points.PointData;
import org.xbib.graphics.graph.gral.util.GraphicsUtils;
import org.xbib.graphics.graph.gral.util.PointND;
/**
* <p>Class that displays two coordinate values and a value as a raster of
* boxes. The data source must provide at least three columns:</p>
* <ul>
* <li>x coordinate</li>
* <li>y coordinate</li>
* <li>value</li>
* </ul>
* <p>The method {@link #createRasterData(DataSource)} can be used to convert
* a matrix of values to the (coordinates, value) format.</p>
*
* <p>To create a new {@code RasterPlot} simply create a new instance using
* a suitable data source. Example:</p>
* <pre>
* DataTable data = new DataTable(Double.class, Double.class);
* data.add(10.98, -12.34);
* data.add( 7.65, 45.67);
* data.add(43.21, 89.01);
* DataSource rasterData = RasterPlot.createRasterData(data);
* RasterPlot plot = new RasterPlot(rasterData);
* </pre>
*/
public class RasterPlot extends XYPlot {
/** Version id for serialization. */
private static final long serialVersionUID = 5844862286358250831L;
/** Offset of the raster pixels to the origin. */
private final Point2D offset;
/** Size of the raster pixels. */
private final Dimension2D distance;
/** Color mapping to fill the raster pixels. */
private ColorMapper colors;
/**
* Class that renders the grid points of a {@code RasterPlot}.
*/
protected static class RasterRenderer extends AbstractPointRenderer {
/** Version id for serialization. */
private static final long serialVersionUID = 1266585364126459761L;
/** Plot specific settings. */
private final RasterPlot plot;
/** Horizontal position of the current raster pixel. */
private int xColumn;
/** Vertical position of the current raster pixel. */
private int yColumn;
/** Value of the current raster pixel. */
private int valueColumn;
/**
* Constructor that creates a new instance and initializes it with a
* plot as data provider. The default columns for (x, y, value) are set
* to (0, 1, 2)
* @param plot Plot storing global settings.
*/
public RasterRenderer(RasterPlot plot) {
this.plot = plot;
xColumn = 0;
yColumn = 1;
valueColumn = 2;
}
/**
* Returns the index of the column which is used for the x coordinate
* of a point.
* @return Index of the column for the x coordinate of a point.
*/
public int getXColumn() {
return xColumn;
}
/**
* Sets the index of the column which will be used for the x coordinate
* of a point.
* @param columnIndex Index of the column for the x coordinate of a point.
*/
public void setXColumn(int columnIndex) {
this.xColumn = columnIndex;
}
/**
* Returns the index of the column which is used for the y coordinate
* of a point.
* @return Index of the column for the y coordinate of a point.
*/
public int getYColumn() {
return yColumn;
}
/**
* Sets the index of the column which will be used for the y coordinate
* of a point.
* @param columnIndex Index of the column for the y coordinate of a point.
*/
public void setYColumn(int columnIndex) {
this.yColumn = columnIndex;
}
/**
* Returns the index of the column which is used for the value of a
* point.
* @return Index of the column for the value of a point.
*/
@Override
public int getValueColumn() {
return valueColumn;
}
/**
* Sets the index of the column which will be used for the value of a
* point.
* @param columnIndex Index of the column for the value of a point.
*/
@Override
public void setValueColumn(int columnIndex) {
this.valueColumn = columnIndex;
}
@Override
public Drawable getPoint(final PointData data, final Shape shape) {
return new AbstractDrawable() {
/** Version id for serialization. */
private static final long serialVersionUID = -1136689797647794969L;
public void draw(DrawingContext context) {
RasterRenderer renderer = RasterRenderer.this;
Axis axisX = data.axes.get(0);
Axis axisY = data.axes.get(1);
AxisRenderer axisXRenderer = data.axisRenderers.get(0);
AxisRenderer axisYRenderer = data.axisRenderers.get(1);
Row row = data.row;
int colX = renderer.getXColumn();
if (colX < 0 || colX >= row.size() || !row.isColumnNumeric(colX)) {
return;
}
int colY = renderer.getYColumn();
if (colY < 0 || colY >= row.size() || !row.isColumnNumeric(colY)) {
return;
}
int colValue = renderer.getValueColumn();
if (colValue < 0 || colValue >= row.size() || !row.isColumnNumeric(colValue)) {
return;
}
double valueX = ((Number) row.get(colX)).doubleValue();
double valueY = ((Number) row.get(colY)).doubleValue();
Number value = (Number) row.get(colValue);
// Pixel dimensions
double xMin = axisXRenderer
.getPosition(axisX, valueX - 0.5, true, false)
.get(PointND.X);
double xMax = axisXRenderer
.getPosition(axisX, valueX + 0.5, true, false)
.get(PointND.X);
double width = Math.abs(xMax - xMin) + 1.0;
double yMin = axisYRenderer
.getPosition(axisY, valueY - 0.5, true, false)
.get(PointND.Y);
double yMax = axisYRenderer
.getPosition(axisY, valueY + 0.5, true, false)
.get(PointND.Y);
double height = Math.abs(yMax - yMin) + 1.0;
// Create shape for pixel
// The origin of all shapes is (boxX, boxY)
Rectangle2D shapeBounds = shape.getBounds2D();
AffineTransform tx = new AffineTransform();
tx.scale(width/shapeBounds.getWidth(), height/shapeBounds.getHeight());
tx.translate(-shapeBounds.getMinX(), -shapeBounds.getMinY());
Shape pixel = tx.createTransformedShape(shape);
// Paint pixel
Graphics2D graphics = context.getGraphics();
ColorMapper colorMapper = plot.getColors();
Paint paint;
if (colorMapper instanceof ContinuousColorMapper) {
paint = ((ContinuousColorMapper) colorMapper)
.get(value.doubleValue());
} else if (colorMapper != null) {
Integer index = value.intValue();
paint = colorMapper.get(index);
} else {
paint = Color.BLACK;
}
GraphicsUtils.fillPaintedShape(
graphics, pixel, paint, pixel.getBounds2D());
}
};
}
/**
* Returns a {@code Shape} instance that can be used for further
* calculations.
* @param data Information on axes, renderers, and values.
* @return Outline that describes the point's shape.
*/
public Shape getPointShape(PointData data) {
return getShape();
}
/**
* Returns a graphical representation of the value label to be drawn for
* the specified data value.
* @param data Information on axes, renderers, and values.
* @param shape Outline that describes the bounds for the value label.
* @return Component that can be used to draw the value label.
*/
public Drawable getValue(final PointData data, final Shape shape) {
return new AbstractDrawable() {
public void draw(DrawingContext context) {
// TODO Implement rendering of value label
}
};
}
}
/**
* Initializes a new box-and-whisker plot with the specified data source.
* @param data Data to be displayed.
*/
public RasterPlot(DataSource data) {
offset = new Point2D.Double();
distance = new org.xbib.graphics.graph.gral.graphics.Dimension2D.Double(1.0, 1.0);
colors = new Grayscale();
((XYPlotArea2D) getPlotArea()).setMajorGridX(false);
((XYPlotArea2D) getPlotArea()).setMajorGridY(false);
//getAxisRenderer(AXIS_X).setSetting(AxisRenderer.TICKS, false);
//getAxisRenderer(AXIS_Y).setSetting(AxisRenderer.TICKS, false);
getAxisRenderer(AXIS_X).setIntersection(-Double.MAX_VALUE);
getAxisRenderer(AXIS_Y).setIntersection(-Double.MAX_VALUE);
// Store data
add(data);
// Adjust axes to the data series
autoscaleAxes();
}
@Override
public void autoscaleAxis(String axisName) {
if (AXIS_X.equals(axisName) || AXIS_Y.equals(axisName)) {
Dimension2D dist = getDistance();
// In case we get called before settings defaults have been set,
// just set distance to a sane default
if (dist == null) {
dist = new org.xbib.graphics.graph.gral.graphics.Dimension2D.Double(1.0, 1.0);
}
Axis axis = getAxis(axisName);
if (axis == null || !axis.isAutoscaled()) {
return;
}
double min = getAxisMin(axisName);
double max = getAxisMax(axisName);
if (AXIS_X.equals(axisName)) {
axis.setRange(min, max + dist.getWidth());
} else if (AXIS_Y.equals(axisName)) {
axis.setRange(min - dist.getHeight(), max);
}
} else {
super.autoscaleAxis(axisName);
}
}
/**
* Takes a matrix of values and creates a new data source that stores the
* values in (x, y, value) format.
* @param data Original data source with values in each cell.
* @return New data source with (x, y, value) columns
*/
@SuppressWarnings("unchecked")
public static DataSource createRasterData(DataSource data) {
if (data == null) {
throw new NullPointerException("Cannot convert null data source.");
}
DataTable coordsValueData =
new DataTable(Double.class, Double.class, Double.class);
// Generate pixel data with (x, y, value)
double min = ((Number) data.getRowStatistics(Statistics.MIN).
getColumnStatistics(Statistics.MIN).get(0, 0)).doubleValue();
double max = ((Number) data.getRowStatistics(Statistics.MAX).
getColumnStatistics(Statistics.MAX).get(0, 0)).doubleValue();
double range = max - min;
int i = 0;
for (Comparable<?> cell : data) {
int x = i%data.getColumnCount();
int y = -i/data.getColumnCount();
double v = Double.NaN;
if (cell instanceof Number) {
Number numericCell = (Number) cell;
v = (numericCell.doubleValue() - min) / range;
}
coordsValueData.add((double) x, (double) y, v);
i++;
}
return coordsValueData;
}
@Override
public void add(int index, DataSource source, boolean visible) {
if (getData().size() > 0) {
throw new IllegalArgumentException(
"This plot type only supports a single data source."); //$NON-NLS-1$
}
// Add data source
super.add(index, source, visible);
// Adjust rendering
// FIXME: Overwrites possible present point and line renderers
setLineRenderers(source, null);
setPointRenderers(source, new RasterRenderer(this));
}
/**
* Returns the horizontal and vertical offset of the raster from the
* origin.
* @return Horizontal and vertical offset of the raster from the origin.
*/
public Point2D getOffset() {
return offset;
}
/**
* Sets the horizontal and vertical offset of the raster from the
* origin.
* @param offset Horizontal and vertical offset of the raster from the
* origin.
*/
public void setOffset(Point2D offset) {
this.offset.setLocation(offset);
}
/**
* Returns the horizontal and vertical distance of the raster elements.
* @return Horizontal and vertical distance of the raster elements.
*/
public Dimension2D getDistance() {
return distance;
}
/**
* Returns the horizontal and vertical distance of the raster elements.
* @param distance Horizontal and vertical distance of the raster elements.
*/
public void setDistance(Dimension2D distance) {
this.distance.setSize(distance);
}
/**
* Returns the object which is used to map pixel values to colors.
* @return Object which is used to map pixel values to colors.
*/
public ColorMapper getColors() {
return colors;
}
/**
* Sets the object which will be used to map pixel values to colors.
* @param colors Object which will be used to map pixel values to colors.
*/
public void setColors(ColorMapper colors) {
this.colors = colors;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
package org.xbib.graphics.graph.gral.plots.areas;
import java.awt.Color;
import java.awt.Paint;
/**
* <p>Abstract class that renders an area in two-dimensional space.</p>
* <p>Functionality includes:</p>
* <ul>
* <li>Punching data points out of the area's shape</li>
* <li>Administration of settings</li>
* </ul>
*/
public abstract class AbstractAreaRenderer implements AreaRenderer {
/** Gap between points and the area. */
private double gap;
/** Decides whether the shape of the gap between points and the area is
* rounded. */
private boolean gapRounded;
/** Paint to fill the area. */
private Paint color;
/**
* Initializes a new instance with default settings.
*/
public AbstractAreaRenderer() {
gap = 0.0;
gapRounded = false;
color = Color.GRAY;
}
@Override
public double getGap() {
return gap;
}
@Override
public void setGap(double gap) {
this.gap = gap;
}
@Override
public boolean isGapRounded() {
return gapRounded;
}
@Override
public void setGapRounded(boolean gapRounded) {
this.gapRounded = gapRounded;
}
@Override
public Paint getColor() {
return color;
}
@Override
public void setColor(Paint color) {
this.color = color;
}
}

View file

@ -0,0 +1,66 @@
package org.xbib.graphics.graph.gral.plots.areas;
import java.awt.Paint;
import java.awt.Shape;
import java.util.List;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.plots.DataPoint;
/**
* Interface for renderers that display areas in plots.
*/
public interface AreaRenderer {
/**
* Returns the shape used for rendering the area of a data points.
* @param points Data points.
* @return Geometric shape for the area of the specified data points.
*/
Shape getAreaShape(List<DataPoint> points);
/**
* Returns the graphical representation to be drawn for the specified data
* points.
* @param points Points that define the shape of the area.
* @param shape Geometric shape of the area.
* @return Representation of the area.
*/
Drawable getArea(List<DataPoint> points, Shape shape);
// TODO: Mention which unit the Gap property has (pixels?)
/**
* Returns the value for the gap between the area and a data point.
* @return Gap between area and data point.
*/
double getGap();
/**
* Sets the value for the gap between the area and a data point.
* @param gap Gap between area and data point.
*/
void setGap(double gap);
/**
* Returns whether the gaps should have rounded corners.
* @return {@code true}, if the gaps should have rounded corners.
*/
boolean isGapRounded();
/**
* Sets a value which decides whether the gaps should have rounded corners.
* @param gapRounded {@code true}, if the gaps should have rounded corners.
*/
void setGapRounded(boolean gapRounded);
/**
* Returns the paint used to fill the area shape.
* @return Paint for the area shape.
*/
Paint getColor();
/**
* Sets the paint used to fill the area shape.
* @param color Paint for the area shape.
*/
void setColor(Paint color);
}

View file

@ -0,0 +1,96 @@
package org.xbib.graphics.graph.gral.plots.areas;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.List;
import org.xbib.graphics.graph.gral.graphics.AbstractDrawable;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.graphics.DrawingContext;
import org.xbib.graphics.graph.gral.plots.DataPoint;
import org.xbib.graphics.graph.gral.plots.axes.Axis;
import org.xbib.graphics.graph.gral.plots.axes.AxisRenderer;
import org.xbib.graphics.graph.gral.util.GraphicsUtils;
import org.xbib.graphics.graph.gral.util.MathUtils;
import org.xbib.graphics.graph.gral.util.PointND;
/**
* Default two-dimensional implementation of the {@code AreaRenderer}
* interface.
*/
public class DefaultAreaRenderer2D extends AbstractAreaRenderer {
/**
* Returns the graphical representation to be drawn for the specified
* data points.
* @param points Points to be used for creating the area.
* @param shape Geometric shape of the area.
* @return Representation of the area.
*/
public Drawable getArea(final List<DataPoint> points, final Shape shape) {
return new AbstractDrawable() {
/**
* Draws the {@code Drawable} with the specified drawing context.
* @param context Environment used for drawing
*/
public void draw(DrawingContext context) {
Paint paint = DefaultAreaRenderer2D.this.getColor();
GraphicsUtils.fillPaintedShape(context.getGraphics(),
shape, paint, null);
}
};
}
/**
* Returns the shape used for rendering the area of a data points.
* @param points Data points.
* @return Geometric shape for the area of the specified data points.
*/
public Shape getAreaShape(List<DataPoint> points) {
if (points.isEmpty() || points.get(0) == null) {
return null;
}
Axis axisY = points.get(0).data.axes.get(1);
AxisRenderer axisRendererY = points.get(0).data.axisRenderers.get(1);
double axisYMin = axisY.getMin().doubleValue();
double axisYMax = axisY.getMax().doubleValue();
double axisYOrigin = MathUtils.limit(0.0, axisYMin, axisYMax);
PointND<Double> posOrigin = null;
if (axisRendererY != null) {
posOrigin = axisRendererY.getPosition(
axisY, axisYOrigin, true, false);
}
Path2D shape = new Path2D.Double();
if (posOrigin == null) {
return shape;
}
double posYOrigin = posOrigin.get(PointND.Y);
double x = 0.0;
double y = 0.0;
for (DataPoint p: points) {
Point2D pos = p.position.getPoint2D();
x = pos.getX();
y = pos.getY();
if (shape.getCurrentPoint() == null) {
shape.moveTo(x, posYOrigin);
}
shape.lineTo(x, y);
}
if (shape.getCurrentPoint() != null) {
shape.lineTo(x, posYOrigin);
shape.closePath();
}
return shape;
}
}

View file

@ -0,0 +1,113 @@
package org.xbib.graphics.graph.gral.plots.areas;
import java.awt.BasicStroke;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.List;
import org.xbib.graphics.graph.gral.graphics.AbstractDrawable;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.graphics.DrawingContext;
import org.xbib.graphics.graph.gral.plots.DataPoint;
import org.xbib.graphics.graph.gral.plots.axes.Axis;
import org.xbib.graphics.graph.gral.plots.axes.AxisRenderer;
import org.xbib.graphics.graph.gral.util.GraphicsUtils;
import org.xbib.graphics.graph.gral.util.MathUtils;
import org.xbib.graphics.graph.gral.util.PointND;
/**
* Default two-dimensional implementation of the {@code AreaRenderer} interface
* that draws lines from data points to the main axis.
*/
public class LineAreaRenderer2D extends AbstractAreaRenderer {
/** Stroke that is used to draw the lines from the data points to the
* axis. */
private Stroke stroke;
/**
* Standard constructor that initializes a new instance.
*/
public LineAreaRenderer2D() {
stroke = new BasicStroke(1f);
}
/**
* Returns the graphical representation to be drawn for the specified data
* points.
* @param points Points that define the shape of the area.
* @param shape Geometric shape of the area.
* @return Representation of the area.
*/
public Drawable getArea(final List<DataPoint> points, final Shape shape) {
return new AbstractDrawable() {
/**
* Draws the {@code Drawable} with the specified drawing context.
* @param context Environment used for drawing
*/
public void draw(DrawingContext context) {
Paint paint = LineAreaRenderer2D.this.getColor();
GraphicsUtils.fillPaintedShape(context.getGraphics(),
shape, paint, null);
}
};
}
/**
* Returns the shape used for rendering the area of a data points.
* @param points Data points.
* @return Geometric shape for the area of the specified data points.
*/
public Shape getAreaShape(List<DataPoint> points) {
if (points.isEmpty() || points.get(0) == null) {
return null;
}
Axis axisY = points.get(0).data.axes.get(1);
AxisRenderer axisRendererY = points.get(0).data.axisRenderers.get(1);
double axisYMin = axisY.getMin().doubleValue();
double axisYMax = axisY.getMax().doubleValue();
double axisYOrigin = MathUtils.limit(0.0, axisYMin, axisYMax);
double posYOrigin = 0.0;
if (axisRendererY != null) {
posYOrigin = axisRendererY.getPosition(
axisY, axisYOrigin, true, false).get(PointND.Y);
}
Path2D shape = new Path2D.Double();
double x = 0.0;
double y = 0.0;
for (DataPoint p : points) {
Point2D pos = p.position.getPoint2D();
x = pos.getX();
y = pos.getY();
shape.moveTo(x, y);
shape.lineTo(x, posYOrigin);
}
Stroke stroke = getStroke();
return stroke.createStrokedShape(shape);
}
/**
* Returns the stroke that is used to draw the lines from the
* data points to the axis.
* @return Stroke for line drawing.
*/
public Stroke getStroke() {
return stroke;
}
/**
* Set the stroke that is used to draw the lines from the
* data points to the axis.
* @param stroke Stroke for line drawing.
*/
public void setStroke(Stroke stroke) {
this.stroke = stroke;
}
}

View file

@ -0,0 +1,939 @@
package org.xbib.graphics.graph.gral.plots.axes;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Dimension2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.text.Format;
import java.text.NumberFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.xbib.graphics.graph.gral.graphics.AbstractDrawable;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.graphics.DrawingContext;
import org.xbib.graphics.graph.gral.graphics.Label;
import org.xbib.graphics.graph.gral.plots.axes.Tick.TickType;
import org.xbib.graphics.graph.gral.util.GeometryUtils;
import org.xbib.graphics.graph.gral.util.GraphicsUtils;
import org.xbib.graphics.graph.gral.util.MathUtils;
import org.xbib.graphics.graph.gral.util.PointND;
/**
* <p>Abstract class that provides function for rendering axes in
* two-dimensional space.</p>
* <p>Functionality includes:</p>
* <ul>
* <li>Calculating tick positions of an axis</li>
* <li>Calculating tick normals</li>
* <li>Administration of settings</li>
* </ul>
*/
public abstract class AbstractAxisRenderer2D implements AxisRenderer {
/** Line segments approximating the shape of the axis. */
private Line2D[] shapeLines;
/** Normals of the line segments approximating the axis. */
private Point2D[] shapeLineNormals;
/** Lengths of the line segments approximating the axis. */
private double[] shapeSegmentLengths;
/** Length of the axis up to a certain approximating line segment. */
private double[] shapeSegmentLengthsAccumulated;
/** Intersection point of the axis. */
private Number intersection;
/** Shape used for drawing. */
private Shape shape;
/** Decides whether the shape is drawn. */
private boolean shapeVisible;
/** Decides whether the shape normals are orientated clockwise. */
private boolean shapeNormalOrientationClockwise;
/** Paint used to draw axis shape, ticks, and labels. */
private Paint shapeColor;
/** Stroke used for drawing the axis shape. */
// Property will be serialized using a wrapper
private transient Stroke shapeStroke;
/** Decides whether the axis direction will be changed. */
private boolean shapeDirectionSwapped;
/** Decides whether major ticks are drawn. */
private boolean ticksVisible;
/** Distance on axis in which major ticks are drawn. */
private Number tickSpacing;
/** Decides whether automatic tick spacing is enabled. */
private boolean ticksAutoSpaced;
/** Tick length relative to the font */
private double tickLength;
/** Stroke which is used to draw all major ticks. */
// Property will be serialized using a wrapper
private transient Stroke tickStroke;
/** Alignment of major ticks relative to the axis. */
private double tickAlignment;
/** Font used to display the text of major ticks. */
private Font tickFont;
/** Paint used to draw the shapes of major ticks. */
private Paint tickColor;
/** Decides whether tick labels will be shown. */
private boolean tickLabelsVisible;
/** Format which converts the tick values to labels. */
private Format tickLabelFormat;
/** Distance between labels and ticks relative to the font height. */
private double tickLabelDistance;
/** Decides whether the tick labels are drawn outside of the plot. */
private boolean tickLabelsOutside;
/** Tick label rotation in degrees. */
private double tickLabelRotation;
/** Decides whether minor ticks are drawn. */
private boolean minorTickVisible;
/** Number of minor ticks between two major ticks. */
private int minorTicksCount;
/** Tick length relative to font height.*/
private double minorTickLength;
/** Stroke used to draw all minor ticks. */
// Property will be serialized using a wrapper
private transient Stroke minorTickStroke;
/** Minor tick alignment relative to the axis. */
private double minorTickAlignment;
/** Paint used to draw the shapes of minor ticks. */
private Paint minorTickColor;
/** Custom labels containing their respective position and text. */
private final Map<Double, String> customTicks;
/** Label text of the axis. */
private Label label;
/** Distance relative to font height. */
private double labelDistance;
/**
* Initializes a new instance with default settings.
*/
public AbstractAxisRenderer2D() {
intersection = 0.0;
// The direction must defined as swapped before the shape is evaluated.
shapeDirectionSwapped = false;
shape = new Line2D.Double(0.0, 0.0, 1.0, 0.0);
evaluateShape(shape);
shapeVisible = true;
shapeNormalOrientationClockwise = false;
shapeStroke = new BasicStroke();
shapeColor = Color.BLACK;
ticksVisible = true;
tickSpacing = 0.0;
ticksAutoSpaced = false;
tickLength = 1.0;
tickStroke = new BasicStroke();
tickAlignment = 0.5;
tickFont = Font.decode(null);
tickColor = Color.BLACK;
tickLabelsVisible = true;
tickLabelFormat = NumberFormat.getInstance();
tickLabelDistance = 1.0;
tickLabelsOutside = true;
tickLabelRotation = 0.0;
customTicks = new HashMap<>();
minorTickVisible = true;
minorTicksCount = 1;
minorTickLength = 0.5;
minorTickStroke = new BasicStroke();
minorTickAlignment = 0.5;
minorTickColor = Color.BLACK;
label = new Label();
labelDistance = 1.0;
}
/**
* Returns a component that displays the specified axis.
* @param axis axis to be displayed
* @return component displaying the axis
* @see Axis
*/
public Drawable getRendererComponent(final Axis axis) {
return new AbstractDrawable() {
/** Version id for serialization. */
private static final long serialVersionUID1 = 3605211198378801694L;
/**
* Draws the {@code Drawable} with the specified drawing context.
* @param context Environment used for drawing
*/
public void draw(DrawingContext context) {
if (shapeLines == null || shapeLines.length == 0) {
return;
}
AbstractAxisRenderer2D renderer = AbstractAxisRenderer2D.this;
Graphics2D graphics = context.getGraphics();
// Remember old state of Graphics2D instance
AffineTransform txOrig = graphics.getTransform();
graphics.translate(getX(), getY());
Stroke strokeOld = graphics.getStroke();
Paint paintOld = graphics.getPaint();
// Draw axis shape
Paint axisPaint = renderer.getShapeColor();
Stroke axisStroke = renderer.getShapeStroke();
boolean isShapeVisible = renderer.isShapeVisible();
if (isShapeVisible) {
Shape shape1 = renderer.getShape();
GraphicsUtils.drawPaintedShape(
graphics, shape1, axisPaint, null, axisStroke);
}
double fontSize =
renderer.getTickFont().getSize2D();
// Draw ticks
boolean drawTicksMajor = renderer.isTicksVisible();
boolean drawTicksMinor = renderer.isMinorTicksVisible();
if (drawTicksMajor || (drawTicksMajor && drawTicksMinor)) {
// Calculate tick positions (in pixel coordinates)
List<Tick> ticks = getTicks(axis);
boolean isTickLabelVisible =
renderer.isTickLabelsVisible();
boolean isTickLabelOutside = renderer.isTickLabelsOutside();
double tickLabelRotation1 = renderer.getTickLabelRotation();
double tickLabelDist = renderer.getTickLabelDistanceAbsolute();
Line2D tickShape = new Line2D.Double();
for (Tick tick : ticks) {
// Draw tick
if ((tick.position == null)
|| (tick.normal == null)) {
continue;
}
Point2D tickPoint = tick.position.getPoint2D();
Point2D tickNormal = tick.normal.getPoint2D();
double tickLength1;
double tickAlignment1;
Paint tickPaint;
Stroke tickStroke1;
if (TickType.MINOR.equals(tick.type)) {
tickLength1 = renderer.getTickMinorLengthAbsolute();
tickAlignment1 = renderer.getMinorTickAlignment();
tickPaint = renderer.getMinorTickColor();
tickStroke1 = renderer.getMinorTickStroke();
} else {
tickLength1 = getTickLengthAbsolute();
tickAlignment1 = renderer.getTickAlignment();
tickPaint =
renderer.getTickColor();
tickStroke1 = renderer.getTickStroke();
}
double tickLengthInner = tickLength1*tickAlignment1;
double tickLengthOuter = tickLength1*(1.0 - tickAlignment1);
if ((drawTicksMajor && (tick.type == TickType.MAJOR) ||
tick.type == TickType.CUSTOM) || (drawTicksMinor &&
tick.type == TickType.MINOR)) {
tickShape.setLine(
tickPoint.getX() - tickNormal.getX()*tickLengthInner,
tickPoint.getY() - tickNormal.getY()*tickLengthInner,
tickPoint.getX() + tickNormal.getX()*tickLengthOuter,
tickPoint.getY() + tickNormal.getY()*tickLengthOuter
);
GraphicsUtils.drawPaintedShape(
graphics, tickShape, tickPaint, null, tickStroke1);
}
// Draw label
if (isTickLabelVisible && (tick.type == TickType.MAJOR ||
tick.type == TickType.CUSTOM)) {
String tickLabelText = tick.label;
if (tickLabelText != null && !tickLabelText.trim().isEmpty()) {
Label tickLabel = new Label(tickLabelText);
tickLabel.setFont(renderer.getTickFont());
// TODO Allow separate colors for ticks and tick labels?
tickLabel.setColor(tickPaint);
double labelDist = tickLengthOuter + tickLabelDist;
layoutLabel(tickLabel, tickPoint, tickNormal,
labelDist, isTickLabelOutside, tickLabelRotation1);
tickLabel.draw(context);
}
}
}
}
// Draw axis label
Label axisLabel = renderer.getLabel();
if (axisLabel != null && !axisLabel.getText().trim().isEmpty()) {
double tickLength1 = getTickLengthAbsolute();
double tickAlignment1 = renderer.getTickAlignment();
double tickLengthOuter = tickLength1*(1.0 - tickAlignment1);
double tickLabelDistance1 = renderer.getTickLabelDistanceAbsolute();
double labelDistance1 = renderer.getLabelDistance()*fontSize;
double labelDist =
tickLengthOuter + tickLabelDistance1 + fontSize + labelDistance1;
double axisLabelPos =
(axis.getMin().doubleValue() + axis.getMax().doubleValue()) * 0.5;
boolean isTickLabelOutside = renderer.isTickLabelsOutside();
PointND<Double> labelPos = getPosition(axis, axisLabelPos, false, true);
PointND<Double> labelNormal = getNormal(axis, axisLabelPos, false, true);
if (labelPos != null && labelNormal != null) {
layoutLabel(axisLabel, labelPos.getPoint2D(),
labelNormal.getPoint2D(), labelDist,
isTickLabelOutside, axisLabel.getRotation());
axisLabel.draw(context);
}
}
graphics.setPaint(paintOld);
graphics.setStroke(strokeOld);
graphics.setTransform(txOrig);
}
private void layoutLabel(Label label1, Point2D labelPos, Point2D labelNormal,
double labelDist, boolean isLabelOutside, double rotation) {
Rectangle2D labelSize = label1.getTextRectangle();
Shape marginShape = new Rectangle2D.Double(
0, 0,
labelSize.getWidth() + 2.0*labelDist, labelSize.getHeight() + 2.0*labelDist
);
Rectangle2D marginBounds = marginShape.getBounds2D();
label1.setRotation(rotation);
if ((rotation%360.0) != 0.0) {
marginShape = AffineTransform.getRotateInstance(
Math.toRadians(-rotation),
marginBounds.getCenterX(),
marginBounds.getCenterY()
).createTransformedShape(marginShape);
}
marginBounds = marginShape.getBounds2D();
double intersRayLength = marginBounds.getHeight()*marginBounds.getHeight() +
marginBounds.getWidth()*marginBounds.getWidth();
double intersRayDir = (isLabelOutside?-1.0:1.0)*intersRayLength;
List<Point2D> descriptionBoundsIntersections = GeometryUtils.intersection(
marginBounds,
new Line2D.Double(
marginBounds.getCenterX(),
marginBounds.getCenterY(),
marginBounds.getCenterX() + intersRayDir*labelNormal.getX(),
marginBounds.getCenterY() + intersRayDir*labelNormal.getY()
)
);
if (!descriptionBoundsIntersections.isEmpty()) {
Point2D inters = descriptionBoundsIntersections.get(0);
double intersX = inters.getX() - marginBounds.getCenterX();
double intersY = inters.getY() - marginBounds.getCenterY();
double posX = labelPos.getX() - intersX - labelSize.getWidth()/2.0;
double posY = labelPos.getY() - intersY - labelSize.getHeight()/2.0;
label1.setBounds(posX, posY, labelSize.getWidth(), labelSize.getHeight());
}
}
@Override
public Dimension2D getPreferredSize() {
AbstractAxisRenderer2D renderer = AbstractAxisRenderer2D.this;
double fontSize = renderer.getTickFont().getSize2D();
double tickLength1 = getTickLengthAbsolute();
double tickAlignment1 = renderer.getTickAlignment();
double tickLengthOuter = tickLength1*(1.0 - tickAlignment1);
double labelDistance1 = renderer.getTickLabelDistanceAbsolute() + tickLengthOuter;
double minSize = fontSize + labelDistance1 + tickLengthOuter;
return new org.xbib.graphics.graph.gral.graphics.Dimension2D.Double(minSize, minSize);
}
};
}
/**
* Returns a list of all tick element on the axis.
* @param axis Axis
* @return A list of {@code Tick} instances
*/
public List<Tick> getTicks(Axis axis) {
List<Tick> ticks = new LinkedList<>();
if (!axis.isValid()) {
return ticks;
}
double min = axis.getMin().doubleValue();
double max = axis.getMax().doubleValue();
Set<Double> tickPositions = new HashSet<>();
createTicksCustom(ticks, axis, min, max, tickPositions);
boolean isAutoSpacing = isTicksAutoSpaced();
// If the spacing is invalid, use auto spacing
if (!isAutoSpacing) {
Number tickSpacing = getTickSpacing();
if (tickSpacing == null) {
isAutoSpacing = true;
} else {
double tickSpacingValue = tickSpacing.doubleValue();
if (tickSpacingValue <= 0.0 || !MathUtils.isCalculatable(tickSpacingValue)) {
isAutoSpacing = true;
}
}
}
createTicks(ticks, axis, min, max, tickPositions, isAutoSpacing);
return ticks;
}
/**
* Returns the absolute length of a major tick.
* @return Major tick length in pixels.
*/
protected double getTickLengthAbsolute() {
double fontSize = getTickFont().getSize2D();
return getTickLength()*fontSize;
}
/**
* Returns the absolute length of a minor tick.
* @return Minor tick length in pixels.
*/
protected double getTickMinorLengthAbsolute() {
double fontSize = getTickFont().getSize2D();
return getMinorTickLength()*fontSize;
}
/**
* Returns the absolute distance between ticks and labels.
* @return Distance in pixels.
*/
protected double getTickLabelDistanceAbsolute() {
double fontSize = getTickFont().getSize2D();
return getTickLabelDistance()*fontSize;
}
/**
* Adds minor and major ticks to a list of ticks.
* @param ticks List of ticks
* @param axis Axis
* @param min Minimum value of axis
* @param max Maximum value of axis
* @param tickPositions Set of tick positions
* @param isAutoSpacing Use automatic scaling
*/
protected abstract void createTicks(List<Tick> ticks, Axis axis,
double min, double max, Set<Double> tickPositions,
boolean isAutoSpacing);
/**
* Adds custom ticks to a list of ticks.
* @param ticks List of ticks
* @param axis Axis
* @param min Minimum value of axis
* @param max Maximum value of axis
* @param tickPositions Set of tick positions
*/
protected void createTicksCustom(List<Tick> ticks, Axis axis,
double min, double max, Set<Double> tickPositions) {
Map<? extends Number, String> labelsCustom = getCustomTicks();
if (labelsCustom != null) {
for (Number tickPositionWorldObj : labelsCustom.keySet()) {
double tickPositionWorld = tickPositionWorldObj.doubleValue();
if (tickPositionWorld < min || tickPositionWorld > max) {
continue;
}
Tick tick = getTick(
TickType.CUSTOM, axis, tickPositionWorld);
ticks.add(tick);
tickPositions.add(tickPositionWorld);
}
}
}
/**
* Returns the point of the tick mark (in pixel coordinates) on the
* specified axis with the specified value.
* @param type Type of tick mark.
* @param axis Axis containing the tick mark.
* @param tickPositionWorld Displayed value on the axis.
* @return Object describing the desired tick mark.
*/
protected Tick getTick(TickType type, Axis axis, double tickPositionWorld) {
// Calculate position of tick on axis shape
PointND<Double> tickPoint = getPosition(axis, tickPositionWorld, false, false);
// Calculate tick normal
PointND<Double> tickNormal = getNormal(axis, tickPositionWorld, false, false);
// Retrieve tick label
String tickLabel;
Map<Double, String> labelsCustom = getCustomTicks();
if (labelsCustom != null && labelsCustom.containsKey(tickPositionWorld)) {
tickLabel = labelsCustom.get(tickPositionWorld);
} else {
Format labelFormat = getTickLabelFormat();
if (labelFormat != null) {
tickLabel = labelFormat.format(tickPositionWorld);
} else {
tickLabel = String.valueOf(tickPositionWorld);
}
}
return new Tick(type, tickPoint, tickNormal, null, null, tickLabel);
}
/**
* Returns the normal vector at the position of the specified value.
* The vector is normalized.
* @param axis Axis
* @param value World coordinate value to convert
* @param extrapolate Option to activate extrapolation value that are not
* on the axis
* @param forceLinear Force linear interpolation.
* @return N-dimensional normal vector at the position
*/
public PointND<Double> getNormal(Axis axis, Number value,
boolean extrapolate, boolean forceLinear) {
double valueView;
if (forceLinear) {
valueView = (value.doubleValue() - axis.getMin().doubleValue()) /
axis.getRange()*getShapeLength();
} else {
valueView = worldToView(axis, value, extrapolate);
}
int segmentIndex = MathUtils.binarySearchFloor(shapeSegmentLengthsAccumulated, valueView);
if (segmentIndex < 0 || segmentIndex >= shapeLines.length) {
return null;
}
segmentIndex = MathUtils.limit(
segmentIndex, 0, shapeLineNormals.length - 1);
boolean normalOrientationClockwise = AbstractAxisRenderer2D.this
.isShapeNormalOrientationClockwise();
double normalOrientation =
normalOrientationClockwise ? 1.0 : -1.0;
return new PointND<>(
normalOrientation*shapeLineNormals[segmentIndex].getX(),
normalOrientation*shapeLineNormals[segmentIndex].getY()
);
}
/**
* Returns the length of the shape path which is used to render axes.
* @return Shape length.
*/
protected double getShapeLength() {
if (shapeSegmentLengthsAccumulated == null || shapeSegmentLengthsAccumulated.length == 0) {
return 0.0;
}
return shapeSegmentLengthsAccumulated[shapeSegmentLengthsAccumulated.length - 1];
}
/**
* Returns the position of the specified value on the axis.
* The value is returned in view coordinates.
* @param axis Axis
* @param value World coordinate value to convert
* @param extrapolate Option to activate extrapolation value that are not
* on the axis
* @param forceLinear Force linear interpolation.
* @return N-dimensional point of the value
*/
public PointND<Double> getPosition(Axis axis, Number value,
boolean extrapolate, boolean forceLinear) {
if (shapeLines == null || shapeLines.length == 0 || value == null) {
return null;
}
// Determine relative position of the value
double relativePositionOnShapePath = axis.getPosition(value).doubleValue();
if (!extrapolate) {
relativePositionOnShapePath = MathUtils.limit(relativePositionOnShapePath, 0.0, 1.0);
}
// Determine absolute position of the value
double positionOnShapePath;
if (forceLinear) {
positionOnShapePath = relativePositionOnShapePath*getShapeLength();
} else {
positionOnShapePath = worldToView(axis, value, extrapolate);
}
if (Double.isNaN(positionOnShapePath)) {
return null;
}
// TODO Check if this is a valid way to allow infinite values
if (positionOnShapePath == Double.NEGATIVE_INFINITY) {
positionOnShapePath = 0.0;
} else if (positionOnShapePath == Double.POSITIVE_INFINITY) {
positionOnShapePath = 1.0;
}
// Determine shape segment
int segmentIndex;
if (relativePositionOnShapePath <= 0.0) {
segmentIndex = 0;
} else if (relativePositionOnShapePath >= 1.0) {
segmentIndex = shapeLines.length - 1;
} else {
// Determine to which segment the value belongs using a binary search
segmentIndex = MathUtils.binarySearchFloor(shapeSegmentLengthsAccumulated, positionOnShapePath);
}
if (segmentIndex < 0 || segmentIndex >= shapeLines.length) {
return null;
}
// Compute actual position of the value in view coordinates
Line2D segment = shapeLines[segmentIndex];
double segmentLen = shapeSegmentLengths[segmentIndex];
double segmentLenAcc = shapeSegmentLengthsAccumulated[segmentIndex];
double relLen = (positionOnShapePath - segmentLenAcc)/segmentLen;
double x = segment.getX1() + (segment.getX2() - segment.getX1())*relLen;
double y = segment.getY1() + (segment.getY2() - segment.getY1())*relLen;
return new PointND<>(x, y);
}
/**
* Calculates important aspects of the specified shape.
* @param shape Shape to be evaluated.
*/
protected final void evaluateShape(Shape shape) {
boolean directionSwapped = isShapeDirectionSwapped();
shapeLines = GeometryUtils.shapeToLines(shape, directionSwapped);
shapeSegmentLengths = new double[shapeLines.length];
// First length is always 0.0, last length is the total length
shapeSegmentLengthsAccumulated = new double[shapeLines.length + 1];
shapeLineNormals = new Point2D[shapeLines.length];
if (shapeLines.length == 0) {
return;
}
for (int i = 0; i < shapeLines.length; i++) {
Line2D line = shapeLines[i];
// Calculate length of axis shape at each shape segment
double segmentLength = line.getP1().distance(line.getP2());
shapeSegmentLengths[i] = segmentLength;
shapeSegmentLengthsAccumulated[i + 1] = shapeSegmentLengthsAccumulated[i] + segmentLength;
// Calculate a normalized vector perpendicular to the current
// axis shape segment
shapeLineNormals[i] = new Point2D.Double(
(line.getY2() - line.getY1()) / segmentLength,
-(line.getX2() - line.getX1()) / segmentLength
);
}
}
@Override
public Number getIntersection() {
return intersection;
}
@Override
public void setIntersection(Number intersection) {
this.intersection = intersection;
}
@Override
public Shape getShape() {
return shape;
}
@Override
public void setShape(Shape shape) {
this.shape = shape;
evaluateShape(shape);
}
@Override
public boolean isShapeVisible() {
return shapeVisible;
}
@Override
public void setShapeVisible(boolean shapeVisible) {
this.shapeVisible = shapeVisible;
}
@Override
public boolean isShapeNormalOrientationClockwise() {
return shapeNormalOrientationClockwise;
}
@Override
public void setShapeNormalOrientationClockwise(boolean clockwise) {
this.shapeNormalOrientationClockwise = clockwise;
}
@Override
public Paint getShapeColor() {
return shapeColor;
}
@Override
public void setShapeColor(Paint color) {
this.shapeColor = color;
}
@Override
public Stroke getShapeStroke() {
return shapeStroke;
}
@Override
public void setShapeStroke(Stroke stroke) {
this.shapeStroke = stroke;
}
@Override
public boolean isShapeDirectionSwapped() {
return shapeDirectionSwapped;
}
@Override
public void setShapeDirectionSwapped(boolean directionSwapped) {
this.shapeDirectionSwapped = directionSwapped;
}
@Override
public boolean isTicksVisible() {
return ticksVisible;
}
@Override
public void setTicksVisible(boolean ticksVisible) {
this.ticksVisible = ticksVisible;
}
@Override
public Number getTickSpacing() {
return tickSpacing;
}
@Override
public void setTickSpacing(Number spacing) {
this.tickSpacing = spacing;
}
@Override
public boolean isTicksAutoSpaced() {
return ticksAutoSpaced;
}
@Override
public void setTicksAutoSpaced(boolean autoSpaced) {
this.ticksAutoSpaced = autoSpaced;
}
@Override
public double getTickLength() {
return tickLength;
}
@Override
public void setTickLength(double length) {
this.tickLength = length;
}
@Override
public Stroke getTickStroke() {
return tickStroke;
}
@Override
public void setTickStroke(Stroke stroke) {
this.tickStroke = stroke;
}
@Override
public double getTickAlignment() {
return tickAlignment;
}
@Override
public void setTickAlignment(double alignment) {
this.tickAlignment = alignment;
}
@Override
public Font getTickFont() {
return tickFont;
}
@Override
public void setTickFont(Font font) {
this.tickFont = font;
}
@Override
public Paint getTickColor() {
return tickColor;
}
@Override
public void setTickColor(Paint color) {
this.tickColor = color;
}
@Override
public boolean isTickLabelsVisible() {
return tickLabelsVisible;
}
@Override
public void setTickLabelsVisible(boolean tickLabelsVisible) {
this.tickLabelsVisible = tickLabelsVisible;
}
@Override
public Format getTickLabelFormat() {
return tickLabelFormat;
}
@Override
public void setTickLabelFormat(Format format) {
this.tickLabelFormat = format;
}
@Override
public double getTickLabelDistance() {
return tickLabelDistance;
}
@Override
public void setTickLabelDistance(double distance) {
this.tickLabelDistance = distance;
}
@Override
public boolean isTickLabelsOutside() {
return tickLabelsOutside;
}
@Override
public void setTickLabelsOutside(boolean labelsOutside) {
this.tickLabelsOutside = labelsOutside;
}
@Override
public double getTickLabelRotation() {
return tickLabelRotation;
}
@Override
public void setTickLabelRotation(double angle) {
this.tickLabelRotation = angle;
}
@Override
public boolean isMinorTicksVisible() {
return minorTickVisible;
}
@Override
public void setMinorTicksVisible(boolean minorTicksVisible) {
this.minorTickVisible = minorTicksVisible;
}
@Override
public int getMinorTicksCount() {
return minorTicksCount;
}
@Override
public void setMinorTicksCount(int count) {
this.minorTicksCount = count;
}
@Override
public double getMinorTickLength() {
return minorTickLength;
}
@Override
public void setMinorTickLength(double length) {
this.minorTickLength = length;
}
@Override
public Stroke getMinorTickStroke() {
return minorTickStroke;
}
@Override
public void setMinorTickStroke(Stroke stroke) {
this.minorTickStroke = stroke;
}
@Override
public double getMinorTickAlignment() {
return minorTickAlignment;
}
@Override
public void setMinorTickAlignment(double alignment) {
this.minorTickAlignment = alignment;
}
@Override
public Paint getMinorTickColor() {
return minorTickColor;
}
@Override
public void setMinorTickColor(Paint color) {
this.minorTickColor = color;
}
@Override
public Map<Double, String> getCustomTicks() {
return Collections.unmodifiableMap(customTicks);
}
@Override
public void setCustomTicks(Map<Double, String> positionsAndLabels) {
customTicks.clear();
customTicks.putAll(positionsAndLabels);
}
@Override
public Label getLabel() {
return label;
}
@Override
public void setLabel(Label label) {
this.label = label;
}
@Override
public double getLabelDistance() {
return labelDistance;
}
@Override
public void setLabelDistance(double distance) {
this.labelDistance = distance;
}
}

View file

@ -0,0 +1,185 @@
package org.xbib.graphics.graph.gral.plots.axes;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import org.xbib.graphics.graph.gral.util.MathUtils;
/**
* <p>Class that represents an arbitrary axis.</p>
* <p>Functionality includes:</p>
* <ul>
* <li>Different ways of setting and getting the range of this axis</li>
* <li>Administration of {@link AxisListener AxisListeners}</li>
* </ul>
*/
public class Axis {
/** Objects that will be notified when axis settings are changing. */
private transient Set<AxisListener> axisListeners;
/** Minimal value on axis. */
private Number min;
/** Maximal value on axis. */
private Number max;
/** Has the axis a valid range. Used for auto-scaling. */
private boolean autoscaled;
/**
* Initializes a new instance with a specified automatic scaling mode, but
* without minimum and maximum values.
* @param autoscaled {@code true} to turn automatic scaling on
*/
private Axis(boolean autoscaled) {
axisListeners = new HashSet<>();
this.autoscaled = autoscaled;
}
/**
* Initializes a new instance without minimum and maximum values.
*/
public Axis() {
this(true);
}
/**
* Initializes a new instance with the specified minimum and maximum values.
* @param min minimum value
* @param max maximum value
*/
public Axis(Number min, Number max) {
this(false);
this.min = min;
this.max = max;
}
/**
* Adds the specified {@code AxisListener} to this Axis.
* The Listeners will be notified if changes to the Axis occur,
* for Example if the minimum or maximum value changes.
* @param listener Listener to be added
* @see AxisListener
*/
public void addAxisListener(AxisListener listener) {
axisListeners.add(listener);
}
/**
* Removes the specified {@code AxisListener} from this Axis.
* @param listener Listener to be removed
* @see AxisListener
*/
public void removeAxisListener(AxisListener listener) {
axisListeners.remove(listener);
}
/**
* Notifies all registered {@code AxisListener}s that the value
* range has changed.
* @param min new minimum value
* @param max new maximum value
*/
private void fireRangeChanged(Number min, Number max) {
for (AxisListener listener : axisListeners) {
listener.rangeChanged(this, min, max);
}
}
/**
* Returns the minimum value to be displayed.
* @return Minimum value.
*/
public Number getMin() {
return min;
}
/**
* Sets the minimum value to be displayed.
* @param min Minimum value.
*/
public void setMin(Number min) {
setRange(min, getMax());
}
/**
* Returns the maximum value to be displayed.
* @return Maximum value.
*/
public Number getMax() {
return max;
}
/**
* Sets the maximum value to be displayed.
* @param max Maximum value.
*/
public void setMax(Number max) {
setRange(getMin(), max);
}
/**
* Returns the range of values to be displayed.
* @return Distance between maximum and minimum value.
*/
public double getRange() {
return getMax().doubleValue() - getMin().doubleValue();
}
/**
* Sets the range of values to be displayed.
* @param min Minimum value.
* @param max Maximum value.
*/
public void setRange(Number min, Number max) {
if ((getMin() != null) && getMin().equals(min) &&
(getMax() != null) && getMax().equals(max)) {
return;
}
this.min = min;
this.max = max;
fireRangeChanged(min, max);
}
/**
* Returns the relative position of the specified value on the axis.
* The value is returned in view coordinates.
* @param value Value whose position is to be determined
* @return Position relative to axis range
*/
public Number getPosition(Number value) {
if (value == null) {
return null;
}
return (value.doubleValue() - getMin().doubleValue()) /
getRange();
}
/**
* Returns whether the axis range should be determined automatically rather
* than using the axis's minimum and a maximum values.
* @return whether the axis is scaled automatically to fit the current data
*/
public boolean isAutoscaled() {
return autoscaled;
}
/**
* Sets whether the axis range should be determined automatically rather
* than using the axis's minimum and a maximum values.
* @param autoscaled Defines whether the axis should be automatically
* scaled to fit the current data.
*/
public void setAutoscaled(boolean autoscaled) {
this.autoscaled = autoscaled;
}
/**
* Returns whether the currently set minimum and maximum values are valid.
* @return {@code true} when minimum and maximum values are correct,
* otherwise {@code false}
*/
public boolean isValid() {
return MathUtils.isCalculatable(min) && MathUtils.isCalculatable(max);
}
}

View file

@ -0,0 +1,14 @@
package org.xbib.graphics.graph.gral.plots.axes;
/**
* Interface that provides a function to listen for changes in axes.
*/
public interface AxisListener {
/**
* Notified if the range of the axis has changed.
* @param axis Axis instance that has changed.
* @param min New minimum value.
* @param max New maximum value.
*/
void rangeChanged(Axis axis, Number min, Number max);
}

View file

@ -0,0 +1,453 @@
package org.xbib.graphics.graph.gral.plots.axes;
import java.awt.Font;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.text.Format;
import java.util.List;
import java.util.Map;
import org.xbib.graphics.graph.gral.graphics.Drawable;
import org.xbib.graphics.graph.gral.graphics.Label;
import org.xbib.graphics.graph.gral.util.PointND;
/**
* Interface for generic renderers of axes.
*/
public interface AxisRenderer {
/**
* Returns a component that displays the specified axis.
* @param axis axis to be displayed
* @return component displaying the axis
* @see Axis
*/
Drawable getRendererComponent(Axis axis);
/*
* TODO: Enforce minimum and maximum values when extrapolation is turned off
* by using MathUtils.limit(double, double, double) on the result
*/
/**
* Converts a world (axis) coordinate value to a view (screen) coordinate
* value. If @code{extrapolate == false}, this method should return 0.0 when
* value is smaller than @code{axis.getMin()} and {@code getShapeLength()} when
* value is larger than @code{axis.getMax(}).
* @param axis Axis
* @param value World coordinate value to convert
* @param extrapolate Option to activate extrapolation value that are not
* on the axis
* @return Screen coordinate value
*/
double worldToView(Axis axis, Number value,
boolean extrapolate);
/**
* Converts a view (screen) coordinate value to a world (axis) coordinate
* value.
* @param axis Axis
* @param value View coordinate value to convert
* @param extrapolate Option to activate extrapolation value that are not
* on the axis
* @return World coordinate value
*/
Number viewToWorld(Axis axis, double value,
boolean extrapolate);
/**
* Returns a list of all tick element on the axis.
* @param axis Axis
* @return A list of {@code Tick} instances
*/
List<Tick> getTicks(Axis axis);
/**
* Returns the position of the specified value on the axis.
* The value is returned in view coordinates.
* @param axis Axis
* @param value World coordinate value to convert
* @param extrapolate Option to activate extrapolation value that are not
* on the axis
* @param forceLinear Force linear interpolation.
* @return N-dimensional point of the value
*/
PointND<Double> getPosition(Axis axis, Number value, boolean extrapolate, boolean forceLinear);
/**
* Returns the normal vector at the position of the specified value.
* The vector is normalized.
* @param axis Axis
* @param value World coordinate value to convert
* @param extrapolate Option to activate extrapolation value that are not
* on the axis
* @param forceLinear Force linear interpolation.
* @return N-dimensional normal vector at the position
*/
PointND<Double> getNormal(Axis axis, Number value, boolean extrapolate, boolean forceLinear);
/**
* Returns the intersection point of the axis.
* @return Point at which this axis intersects other axes.
*/
Number getIntersection();
/**
* Sets the intersection point of the axis.
* @param intersection Point at which this axis intersects other axes.
*/
void setIntersection(Number intersection);
/**
* Returns the shape of the axis.
* @return Shape used for drawing.
*/
Shape getShape();
/**
* Sets the shape of the axis.
* @param shape Shape used for drawing.
*/
void setShape(Shape shape);
/**
* Returns whether the shape of the axis will be drawn.
* This doesn't influence ticks or labels.
* @return {@code true} if the shape should be drawn, false otherwise.
*/
boolean isShapeVisible();
/**
* Sets whether the shape of the axis will be drawn.
* This doesn't influence ticks or labels.
* @param shapeVisible {@code true} if the shape should be drawn, false otherwise.
*/
void setShapeVisible(boolean shapeVisible);
/**
* Returns whether the normal vector of the shape is calculated using
* clockwise or counterclockwise rotation.
* @return {@code true} if the orientation is clockwise, {@code false} if it is
* counterclockwise.
*/
boolean isShapeNormalOrientationClockwise();
/**
* Sets whether the normal vector of the shape is calculated using
* clockwise or counterclockwise rotation.
* @param clockwise {@code true} if the orientation is clockwise,
* {@code false} if it is counterclockwise.
*/
void setShapeNormalOrientationClockwise(boolean clockwise);
/**
* Returns the paint used to draw the axis, its ticks and its labels.
* @return Paint used for drawing.
*/
Paint getShapeColor();
/**
* Sets the paint used to draw the axis, its ticks and its labels.
* @param color Paint used for drawing.
*/
void setShapeColor(Paint color);
/**
* Returns the stroke which defines the shape of the axis.
* @return Stroke used for drawing the shape.
*/
Stroke getShapeStroke();
/**
* Sets the stroke which defines the shape of the axis.
* @param stroke Stroke used for drawing the shape.
*/
void setShapeStroke(Stroke stroke);
/**
* Returns whether the axis direction is changed.
* @return {@code true} if the shape of the axis is inverted,
* {@code false} otherwise.
*/
boolean isShapeDirectionSwapped();
/**
* Sets whether the axis direction will be changed.
* @param directionSwapped {@code true} if the shape of the axis
* should be inverted, {@code false} otherwise.
*/
void setShapeDirectionSwapped(boolean directionSwapped);
/**
* Returns whether major ticks are drawn.
* @return {@code true} if major ticks are drawn, {@code false} otherwise.
*/
boolean isTicksVisible();
/**
* Sets whether major ticks will be drawn.
* @param ticksVisible {@code true} if major ticks should be drawn,
* {@code false} otherwise.
*/
void setTicksVisible(boolean ticksVisible);
/**
* Returns the interval for major ticks.
* @return Distance on axis in which major ticks are drawn.
*/
Number getTickSpacing();
/**
* Sets the interval for major ticks.
* @param spacing Distance on axis in which major ticks are drawn.
*/
void setTickSpacing(Number spacing);
/**
* Returns whether the interval for major and minor ticks is chosen automatically.
* @return {@code true} if auto-spacing is enabled, {@code false} otherwise.
*/
boolean isTicksAutoSpaced();
/**
* Sets whether the interval for major and minor ticks is chosen automatically.
* @param autoSpaced {@code true} if auto-spacing is enabled, {@code false} otherwise.
*/
void setTicksAutoSpaced(boolean autoSpaced);
/**
* Returns the length of major tick strokes.
* @return Tick length relative to the font height.
*/
double getTickLength();
/**
* Sets the length of major tick strokes.
* @param length Tick length relative to the font height.
*/
void setTickLength(double length);
/**
* Returns the stroke which is used to draw all major ticks.
* @return Stroke used for major tick drawing.
*/
Stroke getTickStroke();
/**
* Sets the stroke which is used to draw all major ticks.
* @param stroke Stroke used for major tick drawing.
*/
void setTickStroke(Stroke stroke);
/**
* Returns the alignment of major ticks relative to the axis.
* 0.0 means outside the plotting area, 0.5 means centered on the axis,
* 1.0 means inside the plotting area.
* @return Major tick alignment relative to the axis.
*/
double getTickAlignment();
/**
* Sets the alignment of major ticks relative to the axis.
* 0.0 means outside the plotting area, 0.5 means centered on the axis,
* 1.0 means inside the plotting area.
* @param alignment Major tick alignment relative to the axis.
*/
void setTickAlignment(double alignment);
/**
* Returns the font used to display the text of major ticks.
* @return Font used for tick labels.
*/
Font getTickFont();
/**
* Sets the font used to display the text of major ticks.
* @param font Font used for tick labels.
*/
void setTickFont(Font font);
/**
* Returns the paint used to draw the shapes of major ticks.
* @return Paint used for major tick drawing.
*/
Paint getTickColor();
/**
* Sets the paint used to draw the shapes of major ticks.
* @param color Paint used for major tick drawing.
*/
void setTickColor(Paint color);
/**
* Returns whether tick labels will be shown.
* @return {@code true} if tick labels will be drawn, {@code false} otherwise.
*/
boolean isTickLabelsVisible();
/**
* Sets whether tick labels will be shown.
* @param tickLabelsVisible {@code true} if tick labels will be drawn, {@code false} otherwise.
*/
void setTickLabelsVisible(boolean tickLabelsVisible);
/**
* Returns the format which converts the tick values to labels.
* @return Format used for tick labels.
*/
Format getTickLabelFormat();
/**
* Sets the format which converts the tick values to labels.
* @param format Format used for tick labels.
*/
void setTickLabelFormat(Format format);
/**
* Returns the distance of labels to their ticks.
* @return Label distance relative to the font height.
*/
double getTickLabelDistance();
/**
* Sets the distance of labels to their ticks.
* @param distance Label distance relative to the font height.
*/
void setTickLabelDistance(double distance);
/**
* Returns whether the tick labels are drawn outside of the plot.
* @return {@code true} if the labels are drawn outside of the plot, {@code false} otherwise.
*/
boolean isTickLabelsOutside();
/**
* Sets whether the tick labels are drawn outside of the plot.
* @param tickLabelsOutside {@code true} if the labels are drawn outside of the plot,
* {@code false} otherwise.
*/
void setTickLabelsOutside(boolean tickLabelsOutside);
/**
* Returns the rotation of the tick labels.
* @return Tick label rotation in degrees.
*/
double getTickLabelRotation();
/**
* Sets the rotation of the tick labels.
* @param angle Tick label rotation in degrees.
*/
void setTickLabelRotation(double angle);
/**
* Returns whether minor ticks are drawn.
* @return {@code true} if minor ticks are drawn, {@code false} otherwise.
*/
boolean isMinorTicksVisible();
/**
* Sets whether minor ticks are drawn.
* @param minorTicksVisible {@code true} if minor ticks are drawn, {@code false} otherwise.
*/
void setMinorTicksVisible(boolean minorTicksVisible);
/**
* Returns the count of minor ticks.
* @return Number of minor ticks between two major ticks.
*/
int getMinorTicksCount();
/**
* Sets the count of minor ticks.
* @param count Number of minor ticks between two major ticks.
*/
void setMinorTicksCount(int count);
/**
* Returns the length of minor tick strokes.
* @return Tick length relative to font height.
*/
double getMinorTickLength();
/**
* Sets the length of minor tick strokes.
* @param length Tick length relative to font height.
*/
void setMinorTickLength(double length);
/**
* Returns the stroke used to draw all minor ticks.
* @return Stroke used for minor tick drawing.
*/
Stroke getMinorTickStroke();
/**
* Sets the stroke used to draw all minor ticks.
* @param stroke Stroke used for minor tick drawing.
*/
void setMinorTickStroke(Stroke stroke);
/**
* Returns the alignment of minor ticks.
* 0.0 means outside the plotting area, 0.5 means centered on the axis,
* 1.0 means inside the plotting area.
* @return Minor tick alignment relative to the axis.
*/
double getMinorTickAlignment();
/**
* Sets the alignment of minor ticks.
* 0.0 means outside the plotting area, 0.5 means centered on the axis,
* 1.0 means inside the plotting area.
* @param alignment Minor tick alignment relative to the axis.
*/
void setMinorTickAlignment(double alignment);
/**
* Returns the paint used to draw the shapes of minor ticks.
* @return Paint used for minor tick drawing.
*/
Paint getMinorTickColor();
/**
* Sets the paint used to draw the shapes of minor ticks.
* @param ticksMinorColor Paint used for minor tick drawing.
*/
void setMinorTickColor(Paint ticksMinorColor);
/**
* Returns custom ticks with their respective position and label.
* @return A map of custom tick positions and labels.
*/
Map<Double, String> getCustomTicks();
/**
* Sets custom ticks with their respective position and label.
* @param positionsAndLabels A map of custom tick positions and labels.
*/
void setCustomTicks(Map<Double, String> positionsAndLabels);
/**
* Returns the label of the axis.
* @return Axis label.
*/
Label getLabel();
/**
* Sets the label of the axis.
* @param label Axis label.
*/
void setLabel(Label label);
/**
* Returns the distance from the axis to the label.
* @return Distance relative to font height.
*/
double getLabelDistance();
/**
* Sets the distance from the axis to the label.
* @param distance Distance relative to font height.
*/
void setLabelDistance(double distance);
}

View file

@ -0,0 +1,121 @@
package org.xbib.graphics.graph.gral.plots.axes;
import java.util.List;
import java.util.Set;
import org.xbib.graphics.graph.gral.plots.axes.Tick.TickType;
import org.xbib.graphics.graph.gral.util.MathUtils;
/**
* Class that renders axes with a linear scale in two dimensional space.
*/
public class LinearRenderer2D extends AbstractAxisRenderer2D {
/**
* Creates a new renderer for linear axes in two-dimensional space.
*/
public LinearRenderer2D() {
}
/**
* Converts a world (axis) coordinate value to a view (screen) coordinate
* value.
* @param axis Axis
* @param value World coordinate value to convert
* @param extrapolate Option to activate extrapolation value that are not
* on the axis
* @return Screen coordinate value
*/
public double worldToView(Axis axis, Number value, boolean extrapolate) {
double min = axis.getMin().doubleValue();
double max = axis.getMax().doubleValue();
double val = value.doubleValue();
if (!extrapolate) {
if (val <= min) {
return 0.0;
}
if (val >= max) {
return getShapeLength();
}
}
return (val - min)/(max - min)*getShapeLength();
}
/**
* Converts a view (screen) coordinate value to a world (axis) coordinate
* value.
* @param axis Axis
* @param value View coordinate value to convert
* @param extrapolate Option to activate extrapolation value that are not
* on the axis
* @return World coordinate value
*/
public Number viewToWorld(Axis axis, double value, boolean extrapolate) {
double min = axis.getMin().doubleValue();
double max = axis.getMax().doubleValue();
if (!extrapolate) {
if (value <= 0.0) {
return min;
}
if (value >= getShapeLength()) {
return max;
}
}
return value/getShapeLength()*(max - min) + min;
}
@Override
protected void createTicks(List<Tick> ticks, Axis axis, double min,
double max, Set<Double> tickPositions, boolean isAutoSpacing) {
double tickSpacing = 1.0;
int ticksMinorCount = 3;
if (isAutoSpacing) {
// TODO Use number of screen units to decide whether to subdivide
double range = max - min;
// 1-steppings (0.1, 1, 10)
tickSpacing = MathUtils.magnitude(10.0, range/4.0);
// 2-steppings (0.2, 2, 20)
if (range/tickSpacing > 8.0) {
tickSpacing *= 2.0;
ticksMinorCount = 1;
}
// 5-steppings (0.5, 5, 50)
if (range/tickSpacing > 8.0) {
tickSpacing *= 2.5;
ticksMinorCount = 4;
}
} else {
tickSpacing = getTickSpacing().doubleValue();
ticksMinorCount = getMinorTicksCount();
}
double tickSpacingMinor = tickSpacing;
if (ticksMinorCount > 0) {
tickSpacingMinor = tickSpacing/(ticksMinorCount + 1);
}
double minTickMajor = MathUtils.ceil(min, tickSpacing);
double minTickMinor = MathUtils.ceil(min, tickSpacingMinor);
int ticksTotal = (int) Math.ceil((max - min)/tickSpacingMinor);
int initialTicksMinor = (int) ((minTickMajor - min)/tickSpacingMinor);
// Add major and minor ticks
// (Use integer to avoid rounding errors)
for (int tickCur = 0; tickCur < ticksTotal; tickCur++) {
double tickPositionWorld = minTickMinor + tickCur*tickSpacingMinor;
if (tickPositions.contains(tickPositionWorld)) {
continue;
}
TickType tickType = TickType.MINOR;
if ((tickCur - initialTicksMinor) % (ticksMinorCount + 1) == 0) {
tickType = TickType.MAJOR;
}
Tick tick = getTick(tickType, axis, tickPositionWorld);
if (tick.position != null) {
ticks.add(tick);
tickPositions.add(tickPositionWorld);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more