Merge branch 'master' of xbib.org:joerg/graphics
This commit is contained in:
commit
5d33823074
186 changed files with 23680 additions and 184 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -6,4 +6,4 @@
|
||||||
/.gradle
|
/.gradle
|
||||||
build
|
build
|
||||||
*~
|
*~
|
||||||
/*.iml
|
*.iml
|
|
@ -1,16 +1,16 @@
|
||||||
group = org.xbib.graphics
|
group = org.xbib.graphics
|
||||||
name = graphics
|
name = graphics
|
||||||
version = 4.0.2
|
version = 4.0.3
|
||||||
|
|
||||||
gradle.wrapper.version = 6.6.1
|
gradle.wrapper.version = 6.6.1
|
||||||
pdfbox.version = 2.0.22
|
pdfbox.version = 2.0.23
|
||||||
jna.version = 5.7.0
|
jna.version = 5.8.0
|
||||||
zxing.version = 3.3.1
|
zxing.version = 3.3.1
|
||||||
reflections.version = 0.9.11
|
reflections.version = 0.9.11
|
||||||
jfreechart.version = 1.5.1
|
jfreechart.version = 1.5.1
|
||||||
batik.version = 1.13
|
batik.version = 1.13
|
||||||
junit.version = 5.7.1
|
junit.version = 5.7.1
|
||||||
junit4.version = 4.13
|
junit4.version = 4.13.2
|
||||||
groovy.version = 2.5.12
|
groovy.version = 2.5.12
|
||||||
spock.version = 1.3-groovy-2.5
|
spock.version = 1.3-groovy-2.5
|
||||||
cglib.version = 3.2.5
|
cglib.version = 3.2.5
|
||||||
|
|
|
@ -29,6 +29,7 @@ task sourcesJar(type: Jar, dependsOn: classes) {
|
||||||
|
|
||||||
task javadocJar(type: Jar, dependsOn: javadoc) {
|
task javadocJar(type: Jar, dependsOn: javadoc) {
|
||||||
classifier 'javadoc'
|
classifier 'javadoc'
|
||||||
|
from javadoc.destinationDir
|
||||||
}
|
}
|
||||||
|
|
||||||
artifacts {
|
artifacts {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
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.Code93;
|
||||||
import org.xbib.graphics.barcode.render.BarcodeGraphicsRenderer;
|
import org.xbib.graphics.barcode.render.BarcodeGraphicsRenderer;
|
||||||
import org.xbib.graphics.barcode.MaxiCode;
|
import org.xbib.graphics.barcode.MaxiCode;
|
||||||
|
@ -22,6 +24,7 @@ import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@DisabledOnOs(OS.MAC)
|
||||||
public class EPSRendererTest {
|
public class EPSRendererTest {
|
||||||
|
|
||||||
private Locale originalDefaultLocale;
|
private Locale originalDefaultLocale;
|
||||||
|
|
|
@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
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.Code93;
|
||||||
import org.xbib.graphics.barcode.MaxiCode;
|
import org.xbib.graphics.barcode.MaxiCode;
|
||||||
import org.xbib.graphics.barcode.AbstractSymbol;
|
import org.xbib.graphics.barcode.AbstractSymbol;
|
||||||
|
@ -22,6 +24,7 @@ import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@DisabledOnOs(OS.MAC)
|
||||||
public class PDFRendererTest {
|
public class PDFRendererTest {
|
||||||
|
|
||||||
private Locale originalDefaultLocale;
|
private Locale originalDefaultLocale;
|
||||||
|
|
|
@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
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.Code93;
|
||||||
import org.xbib.graphics.barcode.MaxiCode;
|
import org.xbib.graphics.barcode.MaxiCode;
|
||||||
import org.xbib.graphics.barcode.AbstractSymbol;
|
import org.xbib.graphics.barcode.AbstractSymbol;
|
||||||
|
@ -22,6 +24,7 @@ import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@DisabledOnOs(OS.MAC)
|
||||||
public class SVGRendererTest {
|
public class SVGRendererTest {
|
||||||
|
|
||||||
private Locale originalDefaultLocale;
|
private Locale originalDefaultLocale;
|
||||||
|
|
3
graphics-graph-gral/build.gradle
Normal file
3
graphics-graph-gral/build.gradle
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
dependencies {
|
||||||
|
testImplementation "org.junit.vintage:junit-vintage-engine:${project.property('junit4.version')}"
|
||||||
|
}
|
3
graphics-graph-gral/src/main/java/module-info.java
Normal file
3
graphics-graph-gral/src/main/java/module-info.java
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module org.xbib.graphics.graphics.graph.gral {
|
||||||
|
requires java.desktop;
|
||||||
|
}
|
|
@ -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.
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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> {
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<>();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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$
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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 direction—like
|
||||||
|
* horizontal or vertical—by 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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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
Loading…
Reference in a new issue