diff --git a/.gitignore b/.gitignore index c2710d1..48b192e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ /.gradle build *~ -/*.iml \ No newline at end of file +*.iml \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 3b0b2f0..b5f13be 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,16 +1,16 @@ group = org.xbib.graphics name = graphics -version = 4.0.2 +version = 4.0.3 gradle.wrapper.version = 6.6.1 -pdfbox.version = 2.0.22 -jna.version = 5.7.0 +pdfbox.version = 2.0.23 +jna.version = 5.8.0 zxing.version = 3.3.1 reflections.version = 0.9.11 jfreechart.version = 1.5.1 batik.version = 1.13 junit.version = 5.7.1 -junit4.version = 4.13 +junit4.version = 4.13.2 groovy.version = 2.5.12 spock.version = 1.3-groovy-2.5 cglib.version = 3.2.5 diff --git a/gradle/compile/java.gradle b/gradle/compile/java.gradle index 971a348..30b6d51 100644 --- a/gradle/compile/java.gradle +++ b/gradle/compile/java.gradle @@ -29,6 +29,7 @@ task sourcesJar(type: Jar, dependsOn: classes) { task javadocJar(type: Jar, dependsOn: javadoc) { classifier 'javadoc' + from javadoc.destinationDir } artifacts { diff --git a/graphics-barcode/src/test/java/org/xbib/graphics/barcode/output/EPSRendererTest.java b/graphics-barcode/src/test/java/org/xbib/graphics/barcode/output/EPSRendererTest.java index 8ed5edc..6471e50 100755 --- a/graphics-barcode/src/test/java/org/xbib/graphics/barcode/output/EPSRendererTest.java +++ b/graphics-barcode/src/test/java/org/xbib/graphics/barcode/output/EPSRendererTest.java @@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.xbib.graphics.barcode.Code93; import org.xbib.graphics.barcode.render.BarcodeGraphicsRenderer; import org.xbib.graphics.barcode.MaxiCode; @@ -22,6 +24,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.Locale; +@DisabledOnOs(OS.MAC) public class EPSRendererTest { private Locale originalDefaultLocale; diff --git a/graphics-barcode/src/test/java/org/xbib/graphics/barcode/output/PDFRendererTest.java b/graphics-barcode/src/test/java/org/xbib/graphics/barcode/output/PDFRendererTest.java index cfb88a1..ee8bc21 100755 --- a/graphics-barcode/src/test/java/org/xbib/graphics/barcode/output/PDFRendererTest.java +++ b/graphics-barcode/src/test/java/org/xbib/graphics/barcode/output/PDFRendererTest.java @@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.xbib.graphics.barcode.Code93; import org.xbib.graphics.barcode.MaxiCode; import org.xbib.graphics.barcode.AbstractSymbol; @@ -22,6 +24,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.Locale; +@DisabledOnOs(OS.MAC) public class PDFRendererTest { private Locale originalDefaultLocale; diff --git a/graphics-barcode/src/test/java/org/xbib/graphics/barcode/output/SVGRendererTest.java b/graphics-barcode/src/test/java/org/xbib/graphics/barcode/output/SVGRendererTest.java index 4570e00..7b33afe 100755 --- a/graphics-barcode/src/test/java/org/xbib/graphics/barcode/output/SVGRendererTest.java +++ b/graphics-barcode/src/test/java/org/xbib/graphics/barcode/output/SVGRendererTest.java @@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.xbib.graphics.barcode.Code93; import org.xbib.graphics.barcode.MaxiCode; import org.xbib.graphics.barcode.AbstractSymbol; @@ -22,6 +24,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.Locale; +@DisabledOnOs(OS.MAC) public class SVGRendererTest { private Locale originalDefaultLocale; diff --git a/graphics-graph-gral/build.gradle b/graphics-graph-gral/build.gradle new file mode 100644 index 0000000..fe9a20b --- /dev/null +++ b/graphics-graph-gral/build.gradle @@ -0,0 +1,3 @@ +dependencies { + testImplementation "org.junit.vintage:junit-vintage-engine:${project.property('junit4.version')}" +} diff --git a/graphics-graph-gral/src/main/java/module-info.java b/graphics-graph-gral/src/main/java/module-info.java new file mode 100644 index 0000000..43f241b --- /dev/null +++ b/graphics-graph-gral/src/main/java/module-info.java @@ -0,0 +1,3 @@ +module org.xbib.graphics.graphics.graph.gral { + requires java.desktop; +} \ No newline at end of file diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/AbstractDataSource.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/AbstractDataSource.java new file mode 100644 index 0000000..3a2bfa8 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/AbstractDataSource.java @@ -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>[] types; + /** Set of objects that will be notified of changes to the data values. */ + private transient Set dataListeners; + /** Statistical description of the data values. */ + private transient Statistics statistics; + + /** + * Iterator that returns each row of the DataSource. + */ + private class DataSourceIterator implements Iterator> { + /** 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>... 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>... types) { + this(null, types); + } + + @SuppressWarnings({"unchecked","rawtypes"}) + public AbstractDataSource(Column... remainingColumns) { + Class>[] 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>[] columnTypes = new Class[getColumnCount()]; + Arrays.fill(columnTypes, Double.class); + DataTable statisticsTable = new DataTable(columnTypes); + List 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> 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 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 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 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> columnType = getColumnTypes()[col]; + List> 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>[] 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>... 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. + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/Column.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/Column.java new file mode 100644 index 0000000..19f8588 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/Column.java @@ -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; + +/** + *

Class for accessing a specific column of a data source. The data of the + * column can be accessed using the {@code get(int)} method.

+ * + *

Example for accessing value at column 2, row 3 of a data source:

+ *
+ * Column col = new Column(dataSource, 2);
+ * Number v = col.get(3);
+ * 
+ * + * @see DataSource + */ +public class Column> implements Iterable { + + private final Class dataType; + private final List data; + + @SuppressWarnings("unchecked") + public Column(Class dataType, T... data) { + this(dataType, Arrays.asList(data)); + } + + public Column(Class dataType, Iterable 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> 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 iterator() { + return data.iterator(); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataAccessor.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataAccessor.java new file mode 100644 index 0000000..d82ef66 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataAccessor.java @@ -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> { + + /** 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> iterator() { + return new Iterator>() { + private int i; + + public boolean hasNext() { + return i < size(); + } + + public Comparable next() { + return get(i++); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataChangeEvent.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataChangeEvent.java new file mode 100644 index 0000000..34f9459 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataChangeEvent.java @@ -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 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 DataChangeEvent(DataSource source, int col, int row, + Comparable valOld, Comparable 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataListener.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataListener.java new file mode 100644 index 0000000..3c5014f --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataListener.java @@ -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); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataSeries.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataSeries.java new file mode 100644 index 0000000..81ccc0d --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataSeries.java @@ -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 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>[] typesOrig = data.getColumnTypes(); + Class>[] 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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataSource.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataSource.java new file mode 100644 index 0000000..3b77c13 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataSource.java @@ -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> { + /** + * 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>[] 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); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataTable.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataTable.java new file mode 100644 index 0000000..d1e4f52 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DataTable.java @@ -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 rows; + + /** + * Comparator class for comparing two records using a + * specified set of {@code DataComparator}s. + */ + private final class RecordComparator implements Comparator { + /** 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>... 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> type) { + this(); + Class>[] 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> 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> 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>[] 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> 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 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 Comparable set(int col, int row, Comparable value) { + Comparable old; + DataChangeEvent event = null; + synchronized (this) { + old = (Comparable) get(col, row); + if (old == null || !old.equals(value)) { + Record record = rows.get(row); + ArrayList> 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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DummyData.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DummyData.java new file mode 100644 index 0000000..a2bb08a --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/DummyData.java @@ -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>[] 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; + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/EnumeratedData.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/EnumeratedData.java new file mode 100644 index 0000000..fcedad2 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/EnumeratedData.java @@ -0,0 +1,151 @@ +package org.xbib.graphics.graph.gral.data; + +/** + *

Class that creates a new data source which adds a leading column + * containing the row number.

+ * + *

Example which creates a two column data source from a one column + * histogram:

+ *
+ * DataSource hist = new Histogram2D(data, Orientation.HORIZONTAL, 10);
+ * DataSource hist2d = new EnumeratedData(hist);
+ * 
+ * + * @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>[] typesOrig = original.getColumnTypes(); + Class>[] 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/MutableDataSource.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/MutableDataSource.java new file mode 100644 index 0000000..9e67684 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/MutableDataSource.java @@ -0,0 +1,82 @@ +package org.xbib.graphics.graph.gral.data; + +import org.xbib.graphics.graph.gral.data.comparators.DataComparator; +import java.util.List; + +/** + *

Interface for write access to tabular data. The access includes adding, + * modifying, and deleting of the data.

+ *

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> 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 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. + */ + Comparable set(int col, int row, Comparable 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); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/Record.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/Record.java new file mode 100644 index 0000000..e63b679 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/Record.java @@ -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> { + + private final Comparable[] values; + + @SuppressWarnings("rawtypes") + public Record(List> values) { + this.values = values.toArray(new Comparable[0]); + } + + public Record(Comparable... values) { + this.values = copyOf(values, values.length); + } + + @SuppressWarnings("unchecked") + public > T get(int index) { + return (T) values[index]; + } + + public int size() { + return values.length; + } + + @Override + public Iterator> iterator() { + // More readable version using Arrays.asList is prevented by broken Generics system + List> 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> 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(); + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/Row.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/Row.java new file mode 100644 index 0000000..170dbde --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/Row.java @@ -0,0 +1,71 @@ +/* + * GRAL: GRAphing Library for Java(R) + * + * (C) Copyright 2009-2019 Erich Seifert , + * Michael Seifert + * + * 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 . + */ +package org.xbib.graphics.graph.gral.data; + +/** + *

Class for easily accessing a row of a data source.

+ * + *

Example:

+ *
+ * Row row = new Row(data, 2);
+ * Number value = row.get(3);
+ * 
+ * + * @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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/RowSubset.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/RowSubset.java new file mode 100644 index 0000000..6a685c1 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/RowSubset.java @@ -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; + +/** + *

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.

+ * + *

Example that keeps only every second row:

+ *
+ * DataSource filtered = new RowSubset() {
+ *     public boolean accept(Row row) {
+ *         return row.getIndex()%2 == 0;
+ *     }
+ * };
+ * 
+ */ +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 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>[] 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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/comparators/Ascending.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/comparators/Ascending.java new file mode 100644 index 0000000..4760cd4 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/comparators/Ascending.java @@ -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); + } + + /** + *

Compares the values of two records at the specified column for order and + * returns a corresponding integer:

+ *
    + *
  • a negative value means {@code record1} is smaller than {@code record2}
  • + *
  • 0 means {@code record1} is equal to {@code record2}
  • + *
  • a positive value means {@code record1} is larger than {@code record2}
  • + *
+ * @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 value1 = record1.get(getColumn()); + Comparable 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); + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/comparators/DataComparator.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/comparators/DataComparator.java new file mode 100644 index 0000000..0016b65 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/comparators/DataComparator.java @@ -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 { + + /** 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/comparators/Descending.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/comparators/Descending.java new file mode 100644 index 0000000..04c52ca --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/comparators/Descending.java @@ -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); + } + + /** + *

Compares the values of two records at the specified column for order and + * returns a corresponding integer:

+ *
    + *
  • a negative value means {@code record1} is larger than {@code record2}
  • + *
  • 0 means {@code record1} is equal to {@code record2}
  • + *
  • a positive value means {@code record1} is smaller than {@code record2}
  • + *
+ * @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 value1 = record1.get(getColumn()); + Comparable 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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Accumulation.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Accumulation.java new file mode 100644 index 0000000..d941aa3 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Accumulation.java @@ -0,0 +1,43 @@ +package org.xbib.graphics.graph.gral.data.filters; + +import java.util.Iterator; + +public class Accumulation> implements Filter { + + private final Iterable data; + + private static class AccumulationIterator implements Iterator { + private final Iterator wrappedIterator; + private double accumulatedValue; + + public AccumulationIterator(Iterator 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 data) { + this.data = data; + } + + @Override + public Iterator iterator() { + return new AccumulationIterator<>(data.iterator()); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Convolution.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Convolution.java new file mode 100644 index 0000000..c96c6fc --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Convolution.java @@ -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; + +/** + *

Class that applies a specified kernel to a data source to convolve it.

+ *

Functionality includes:

+ *
    + *
  • Getting and setting the {@code Kernel} used for convolution
  • + *
+ */ +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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/ConvolutionFilter.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/ConvolutionFilter.java new file mode 100644 index 0000000..ad0a059 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/ConvolutionFilter.java @@ -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> implements Filter { + + private final List filtered; + + private final Iterator> windowIterator; + + public ConvolutionFilter(Iterable data, Kernel kernel) { + filtered = new LinkedList<>(); + + windowIterator = new WindowIterator<>(data.iterator(), kernel.size()); + + while (windowIterator.hasNext()) { + List 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 iterator() { + return filtered.iterator(); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Filter.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Filter.java new file mode 100644 index 0000000..e2f0100 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Filter.java @@ -0,0 +1,25 @@ +/* + * GRAL: GRAphing Library for Java(R) + * + * (C) Copyright 2009-2019 Erich Seifert , + * Michael Seifert + * + * 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 . + */ +package org.xbib.graphics.graph.gral.data.filters; + +public interface Filter> extends Iterable { +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Filter2D.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Filter2D.java new file mode 100644 index 0000000..ea31245 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Filter2D.java @@ -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; + +/** + *

Abstract class that provides basic functions for filtering arbitrary + * columns of a DataSource, in other words a set of one-dimensional data.

+ * + *

Functionality includes:

+ *
    + *
  • Different modes for filtering (see {@link Mode})
  • + *
  • Support for listening for changes of the original data
  • + *
  • Filtering of multiple columns
  • + *
+ * + *

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.

+ */ +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 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>[] 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>[] 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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Kernel.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Kernel.java new file mode 100644 index 0000000..c2d36d9 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Kernel.java @@ -0,0 +1,228 @@ +package org.xbib.graphics.graph.gral.data.filters; + +import java.util.Arrays; + +/** + *

Class that represents an one dimensional array of coefficients for a + * weighted filtering.

+ *

Functionality includes:

+ *
    + *
  • Adding of other kernels or scalars
  • + *
  • Multiplication with other kernels or scalars
  • + *
  • Normalization
  • + *
  • Negation
  • + *
+ */ +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; + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Median.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Median.java new file mode 100644 index 0000000..3226a48 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Median.java @@ -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; + + +/** + *

Class that calculates the median of a data sequence.

+ *
    + *
  • Setting and getting offset
  • + *
  • Setting and getting window size
  • + *
+ */ +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> colWindows = + new ArrayList<>(getColumnCount()); + for (int colIndex = 0; colIndex < getColumnCountFiltered(); colIndex++) { + int colIndexOriginal = getIndexOriginal(colIndex); + List 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 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 w) { + if (w.size() == 1) { + return w.get(0); + } + List 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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/MedianFilter.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/MedianFilter.java new file mode 100644 index 0000000..0aec996 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/MedianFilter.java @@ -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> implements Filter { + + private final List filtered; + + private final Iterator> windowIterator; + + public MedianFilter(Iterable data, int windowSize) { + filtered = new LinkedList<>(); + + windowIterator = new WindowIterator<>(data.iterator(), windowSize); + + while (windowIterator.hasNext()) { + List window = windowIterator.next(); + Statistics windowStatistics = new Statistics(window); + double median = windowStatistics.get(Statistics.MEDIAN); + filtered.add(median); + } + } + + @Override + public Iterator iterator() { + return filtered.iterator(); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Resize.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Resize.java new file mode 100644 index 0000000..6892610 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/filters/Resize.java @@ -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 Iterator advance(Iterator 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> 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> 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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/package-info.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/package-info.java new file mode 100755 index 0000000..2961c09 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/package-info.java @@ -0,0 +1,25 @@ +/* + * GRAL: GRAphing Library for Java(R) + * + * (C) Copyright 2009-2019 Erich Seifert , + * Michael Seifert + * + * 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 . + */ +/** + * Data model classes. This package contains classes and interfaces for storing data for plots. + */ +package org.xbib.graphics.graph.gral.data; diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/statistics/AbstractHistogram2D.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/statistics/AbstractHistogram2D.java new file mode 100644 index 0000000..fb01ccc --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/statistics/AbstractHistogram2D.java @@ -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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/statistics/Histogram.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/statistics/Histogram.java new file mode 100644 index 0000000..a56bb82 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/statistics/Histogram.java @@ -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 { + private Iterable> data; + private Number[] breaks; + private Integer[] bins; + + public Histogram(Iterable> data, int binCount) { + this(data, getEquidistantBreaks(data, binCount + 1)); + } + + public Histogram(Iterable> 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> 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 iterator() { + return Arrays.asList(bins).iterator(); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/statistics/Histogram2D.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/statistics/Histogram2D.java new file mode 100644 index 0000000..8414b85 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/statistics/Histogram2D.java @@ -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; + +/** + *

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.

+ *

For ease of use the histogram is a data source itself.

+ */ +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 breaks; + /** Bin cells that store all aggregation counts. */ + private final List cellList; + + /** Minimum values for cells. */ + private transient Map cacheMin; + /** Maximum values for cells. */ + private transient Map 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> 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>[] getColumnTypes() { + Class>[] 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<>(); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/statistics/Statistics.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/statistics/Statistics.java new file mode 100644 index 0000000..707902d --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/data/statistics/Statistics.java @@ -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> data; + /** Table statistics stored by key. */ + private final Map statistics; + + /** + * Initializes a new object with the specified data values. + * @param data Data to be analyzed. + */ + public Statistics(Iterable> 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> data, Map 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> data, Map stats) { + // Create sorted list of data + List 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 NaN + * 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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/AbstractDrawable.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/AbstractDrawable.java new file mode 100644 index 0000000..76fa211 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/AbstractDrawable.java @@ -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()); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Container.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Container.java new file mode 100644 index 0000000..0f451af --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Container.java @@ -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 { + /** + * 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 getDrawablesAt(Point2D point); + + /** + * Returns a list of stored components. + * @return Contained drawables. + */ + List 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(); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Dimension2D.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Dimension2D.java new file mode 100644 index 0000000..11a6462 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Dimension2D.java @@ -0,0 +1,85 @@ +package org.xbib.graphics.graph.gral.graphics; + +import java.util.Locale; + +/** + *

Class that stores the horizontal and vertical extent of an object.

+ *

This implementation adds support of double values to + * {@code java.awt.geom.Dimension2D}.

+ */ +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)); + } + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Drawable.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Drawable.java new file mode 100644 index 0000000..8686540 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Drawable.java @@ -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); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/DrawableContainer.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/DrawableContainer.java new file mode 100644 index 0000000..ad3c726 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/DrawableContainer.java @@ -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 components; + /** Supplemental information for components, like layout constraints. */ + private final Map 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 getDrawablesAt(Point2D point) { + return getDrawablesAt(this, point, new LinkedList()); + } + + @Override + public List getDrawables() { + /* + * TODO: Size of ArrayList can be different from the number of added components + * in concurrent environments. + */ + List drawableList = new ArrayList<>(components.size()); + drawableList.addAll(components); + return drawableList; + } + + private static List getDrawablesAt(Container container, Point2D point, LinkedList 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 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(); + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/DrawingContext.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/DrawingContext.java new file mode 100644 index 0000000..77e1ed9 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/DrawingContext.java @@ -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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Insets2D.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Insets2D.java new file mode 100644 index 0000000..5e993a4 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Insets2D.java @@ -0,0 +1,184 @@ +package org.xbib.graphics.graph.gral.graphics; + +import java.util.Locale; + +/** + * Abstract class that stores insets for all four directions. + *

Please use this instead of java.awt.Insets, as the java class does not + * support double values.

+ */ +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)); + } + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Label.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Label.java new file mode 100644 index 0000000..76b0801 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Label.java @@ -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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Location.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Location.java new file mode 100644 index 0000000..de9d9d2 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Location.java @@ -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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Orientation.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Orientation.java new file mode 100644 index 0000000..80c84e9 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/Orientation.java @@ -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 +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/AbstractLayout.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/AbstractLayout.java new file mode 100644 index 0000000..458dad2 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/AbstractLayout.java @@ -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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/AbstractOrientedLayout.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/AbstractOrientedLayout.java new file mode 100644 index 0000000..26cf03d --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/AbstractOrientedLayout.java @@ -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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/EdgeLayout.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/EdgeLayout.java new file mode 100644 index 0000000..f5b7df1 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/EdgeLayout.java @@ -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 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 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 getComponentsByLocation(Container container) { + Map 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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/Layout.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/Layout.java new file mode 100644 index 0000000..cd04c0e --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/Layout.java @@ -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); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/OrientedLayout.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/OrientedLayout.java new file mode 100644 index 0000000..e16424e --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/OrientedLayout.java @@ -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); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/OuterEdgeLayout.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/OuterEdgeLayout.java new file mode 100644 index 0000000..df2c297 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/OuterEdgeLayout.java @@ -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 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 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 getComponentsByLocation(Container container) { + Map 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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/StackedLayout.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/StackedLayout.java new file mode 100644 index 0000000..15846f7 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/StackedLayout.java @@ -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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/TableLayout.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/TableLayout.java new file mode 100644 index 0000000..79948e7 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/graphics/layout/TableLayout.java @@ -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 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 Data type for the values. + * @param a First value. + * @param b Second value. + * @return Larger value. + */ + private static > 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/AbstractIOFactory.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/AbstractIOFactory.java new file mode 100644 index 0000000..c82f7a9 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/AbstractIOFactory.java @@ -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 The type of objects which should be produced by this factory + */ +public abstract class AbstractIOFactory implements IOFactory { + + private final Map> 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 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 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) 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 clazz = entries.get(mimeType); + try { + Method capabilitiesGetter = + clazz.getMethod("getCapabilities"); //$NON-NLS-1$ + Set capabilities = + (Set) 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 getCapabilities() { + List 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 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/IOCapabilities.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/IOCapabilities.java new file mode 100644 index 0000000..2c97bd8 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/IOCapabilities.java @@ -0,0 +1,66 @@ +package org.xbib.graphics.graph.gral.io; + +/** + * Class that stores information on a reader or writer + * 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/IOCapabilitiesStorage.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/IOCapabilitiesStorage.java new file mode 100644 index 0000000..2537f9a --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/IOCapabilitiesStorage.java @@ -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 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 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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/IOFactory.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/IOFactory.java new file mode 100644 index 0000000..f215f2c --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/IOFactory.java @@ -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 Class of the objects produced by the factory. + */ +public interface IOFactory { + /** + * 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 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); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/AbstractDataReader.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/AbstractDataReader.java new file mode 100644 index 0000000..b5d8406 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/AbstractDataReader.java @@ -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 settings; + /** Default settings. */ + private final Map 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 return type + * @param key key of the setting + * @return the value of the setting + */ + @SuppressWarnings("unchecked") + public 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 value type + * @param key key of the setting + * @param value value of the setting + */ + public void setSetting(String key, T value) { + settings.put(key, value); + } + + /** + * Defines a default value for the setting with the specified key. + * @param Data type of value. + * @param key Setting key. + * @param value Default value. + */ + protected void setDefault(String key, T value) { + defaults.put(key, value); + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/AbstractDataWriter.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/AbstractDataWriter.java new file mode 100644 index 0000000..ba9e709 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/AbstractDataWriter.java @@ -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 settings; + /** Default settings. */ + private final Map 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 return type + * @param key key of the setting + * @return the value of the setting + */ + @SuppressWarnings("unchecked") + public 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 value type + * @param key key of the setting + * @param value value of the setting + */ + public void setSetting(String key, T value) { + settings.put(key, value); + } + + /** + * Defines a default value for the setting with the specified key. + * @param Data type of value + * @param key Setting key + * @param value Default value + */ + protected void setDefault(String key, T value) { + defaults.put(key, value); + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/CSVReader.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/CSVReader.java new file mode 100644 index 0000000..c2f3f6d --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/CSVReader.java @@ -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; + +/** + *

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.

+ *

{@code CSVReader} instances should be obtained by the + * {@link DataReaderFactory} rather than being created manually:

+ *
+ * DataReaderFactory factory = DataReaderFactory.getInstance();
+ * DataReader reader = factory.get("text/csv");
+ * reader.read(new FileInputStream(filename), Integer.class, Double.class);
+ * 
+ * @see RFC 4180 + */ +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>... 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 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>, Method> parseMethods = + new HashMap<>(); + for (Class> 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> 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> 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/CSVWriter.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/CSVWriter.java new file mode 100644 index 0000000..23a3ccc --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/CSVWriter.java @@ -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; + +/** + *

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.

+ *

{@code CSVWriter} instances should be obtained by the + * {@link DataWriterFactory} rather than being created manually:

+ *
+ * DataWriterFactory factory = DataWriterFactory.getInstance();
+ * DataWriter writer = factory.get("text/csv");
+ * writer.write(data, new FileOutputStream(filename));
+ * 
+ * @see RFC 4180 + */ +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(); + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/DataReader.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/DataReader.java new file mode 100644 index 0000000..5e27ee4 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/DataReader.java @@ -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>... types) + throws IOException; + + /** + * Returns the setting for the specified key. + * @param return type + * @param key key of the setting + * @return the value of the setting + */ + T getSetting(String key); + + /** + * Sets the setting for the specified key. + * @param value type + * @param key key of the setting + * @param value value of the setting + */ + void setSetting(String key, T value); + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/DataReaderFactory.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/DataReaderFactory.java new file mode 100644 index 0000000..dbc331f --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/DataReaderFactory.java @@ -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; + +/** + *

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.

+ *

Example usage:

+ *
+ * DataReaderFactory factory = DataReaderFactory.getInstance();
+ * DataReader reader = factory.get("text/csv");
+ * DataSource = reader.read(new FileInputStream(filename), Double.class);
+ * 
+ */ +public final class DataReaderFactory extends AbstractIOFactory { + /** 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 clazz = getTypeClass(mimeType); + //IOCapabilities capabilities = getCapabilities(mimeType); + try { + if (clazz != null) { + Constructor 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/DataWriter.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/DataWriter.java new file mode 100644 index 0000000..02470e9 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/DataWriter.java @@ -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 return type + * @param key key of the setting + * @return the value of the setting + */ + T getSetting(String key); + + /** + * Sets the setting for the specified key. + * @param value type + * @param key key of the setting + * @param value value of the setting + */ + void setSetting(String key, T value); + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/DataWriterFactory.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/DataWriterFactory.java new file mode 100644 index 0000000..c091b81 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/DataWriterFactory.java @@ -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; + +/** + *

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.

+ *

Example usage:

+ *
+ * DataWriterFactory factory = DataWriterFactory.getInstance();
+ * DataWriter writer = factory.get("image/png");
+ * writer.write(data);
+ * 
+ */ +public final class DataWriterFactory extends AbstractIOFactory { + /** 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 clazz = getTypeClass(mimeType); + //IOCapabilities capabilities = getCapabilities(mimeType); + try { + if (clazz != null) { + Constructor 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/ImageReader.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/ImageReader.java new file mode 100644 index 0000000..08b7f17 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/ImageReader.java @@ -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>... 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.getSetting("factor").doubleValue(); + double offset = this.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; + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/ImageWriter.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/ImageWriter.java new file mode 100644 index 0000000..5370e8b --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/data/ImageWriter.java @@ -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.getSetting("factor").doubleValue(); //$NON-NLS-1$ + double offset = this.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 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$ + } + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/plots/BitmapWriter.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/plots/BitmapWriter.java new file mode 100644 index 0000000..65ed849 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/plots/BitmapWriter.java @@ -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: + *
    + *
  • Windows Bitmap (BMP)
  • + *
  • Graphics Interchange Format (GIF)
  • + *
  • JPEG File Interchange Format (JPEG)
  • + *
  • Portable Network Graphics (PNG)
  • + *
  • Wireless Application Protocol Bitmap (WBMP)
  • + *
+ *

This class shouldn't be used directly but using the + * {@link DrawableWriterFactory}.

+ */ +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 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/plots/DrawableWriter.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/plots/DrawableWriter.java new file mode 100644 index 0000000..bebd0c5 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/plots/DrawableWriter.java @@ -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; +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/plots/DrawableWriterFactory.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/plots/DrawableWriterFactory.java new file mode 100644 index 0000000..450c6b0 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/plots/DrawableWriterFactory.java @@ -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; + +/** + *

Class that provides {@code DrawableWriter} implementations for + * different file formats.

+ * + *

Example Usage:

+ *
+ * DrawableWriterFactory factory = DrawableWriterFactory.getInstance();
+ * DrawableWriter writer = factory.get("application/pdf");
+ * writer.write(plot, new FileOutputStream(filename));
+ * 
+ * + * @see DrawableWriter + */ +public final class DrawableWriterFactory extends AbstractIOFactory { + /** 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 clazz = getTypeClass(mimeType); + //IOCapabilities capabilities = getCapabilities(mimeType); + try { + if (clazz != null) { + Constructor 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/plots/VectorWriter.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/plots/VectorWriter.java new file mode 100644 index 0000000..f810710 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/io/plots/VectorWriter.java @@ -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; + +/** + *

Class that stores {@code Drawable} instances as vector graphics. + * This implementation requires the VectorGraphics2D library to provide + * support for the following file formats:

+ *
    + *
  • Encapsulated PostScript (EPS)
  • + *
  • Portable Document Format (PDF)
  • + *
  • Scalable Vector Graphics (SVG)
  • + *
+ * + *

If the VectorGraphics2D 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}.

+ */ +public class VectorWriter extends IOCapabilitiesStorage + implements DrawableWriter { + /** Mapping of MIME type string to {@code Processor} implementation. */ + private static final Map 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; + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/AbstractNavigator.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/AbstractNavigator.java new file mode 100644 index 0000000..c0cc101 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/AbstractNavigator.java @@ -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 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 zoomPoint) { + if (!isZoomable()) { + return; + } + boolean pan = isPannable() && zoomPoint != null; + + PointND center = null; + if (pan) { + center = getCenter(); + setCenter(zoomPoint); + } + setZoom(zoom); + if (pan) { + setCenter(center); + } + } + + @Override + public void zoomInAt(PointND zoomPoint) { + double zoom = getZoom(); + zoomAt(zoom*getZoomFactor(), zoomPoint); + } + + @Override + public void zoomOutAt(PointND 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> 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 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> 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 event) { + for (NavigationListener l : navigationListeners) { + l.zoomChanged(event); + } + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/Navigable.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/Navigable.java new file mode 100644 index 0000000..9c1ddfa --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/Navigable.java @@ -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(); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/NavigationDirection.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/NavigationDirection.java new file mode 100644 index 0000000..26c3e2c --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/NavigationDirection.java @@ -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 { +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/NavigationEvent.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/NavigationEvent.java new file mode 100644 index 0000000..8dde4f5 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/NavigationEvent.java @@ -0,0 +1,51 @@ +package org.xbib.graphics.graph.gral.navigation; + +/** + * Data class that describes a navigational event, like zooming or panning. + * + * @param Data type of the value that has been changed. + */ +public class NavigationEvent { + /** 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/NavigationListener.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/NavigationListener.java new file mode 100644 index 0000000..71ee71e --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/NavigationListener.java @@ -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> 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 event); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/Navigator.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/Navigator.java new file mode 100644 index 0000000..c407ae2 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/navigation/Navigator.java @@ -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 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 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 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 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 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 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); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/AbstractPlot.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/AbstractPlot.java new file mode 100644 index 0000000..e20d1c4 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/AbstractPlot.java @@ -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 data; + /** Set of all data sources that are visible (not hidden). */ + private final Set dataVisible; + + /** Mapping of axis names to axis objects. */ + private final Map axes; + /** Mapping of axis names to axis renderer objects. */ + private final Map axisRenderers; + /** Mapping of axis names to drawable objects. */ + private final Map axisDrawables; + + /** Mapping of data source columns to axes. **/ + private final Map> columnToAxisMappingByDataSource; + /** Minimum values of axes. **/ + private final Map axisMin; + /** Maximum values of axes. **/ + private final Map 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 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 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 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 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 getVisibleData() { + List 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> entryByDataSource : columnToAxisMappingByDataSource.entrySet()) { + DataSource dataSource = entryByDataSource.getKey(); + Map columnToAxisMapping = entryByDataSource.getValue(); + for (Entry 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); + } + } + } + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/BarPlot.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/BarPlot.java new file mode 100644 index 0000000..8e7172c --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/BarPlot.java @@ -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; + +/** + *

Class that displays data in a bar plot.

+ *

To create a new {@code BarPlot} simply create a new instance + * using one or more data sources. Example:

+ *
+ * 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);
+ * 
+ */ +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 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 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/BoxPlot.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/BoxPlot.java new file mode 100644 index 0000000..65d4a7e --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/BoxPlot.java @@ -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; + +/** + *

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}:

+ *
    + *
  • Box position (for multiple boxes)
  • + *
  • Position of the center bar (e.g. median)
  • + *
  • Length of the lower whisker and position of the bottom bar + * (e.g. minimum)
  • + *
  • Position of the bottom edge of the box (e.g. first quartile)
  • + *
  • Position of the top edge of the box (e.g. third quartile)
  • + *
  • Length of the upper whisker and position of the top bar + * (e.g. maximum)
  • + *
+ *

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.

+ * + *

To create a new {@code BoxPlot} simply create a new instance using + * a data source. Example:

+ *
+ * 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);
+ * 
+ */ +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 column index, median, mininum, first + * quartile, third quartile, and maximum 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 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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/DataPoint.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/DataPoint.java new file mode 100644 index 0000000..065890e --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/DataPoint.java @@ -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 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 position) { + this.data = data; + this.position = position; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/PiePlot.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/PiePlot.java new file mode 100644 index 0000000..10e43ce --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/PiePlot.java @@ -0,0 +1,1125 @@ +package org.xbib.graphics.graph.gral.plots; + +import static java.util.Arrays.asList; + +import java.awt.BasicStroke; +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.Arc2D; +import java.awt.geom.Area; +import java.awt.geom.Dimension2D; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.text.Format; +import java.text.NumberFormat; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.xbib.graphics.graph.gral.data.AbstractDataSource; +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.Row; +import org.xbib.graphics.graph.gral.data.filters.Accumulation; +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.Insets2D; +import org.xbib.graphics.graph.gral.graphics.Label; +import org.xbib.graphics.graph.gral.graphics.Location; +import org.xbib.graphics.graph.gral.navigation.AbstractNavigator; +import org.xbib.graphics.graph.gral.navigation.Navigable; +import org.xbib.graphics.graph.gral.navigation.Navigator; +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.axes.LinearRenderer2D; +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.QuasiRandomColors; +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.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; + +/** + *

Class that displays data as segments of a pie plot. Empty segments are + * displayed for negative values.

+ *

To create a new {@code PiePlot} simply create a new instance using + * a data source. Example:

+ *
+ * DataTable data = new DataTable(Integer.class, Double.class);
+ * data.add(-23.50);
+ * data.add(100.00);
+ * data.add( 60.25);
+ *
+ * PiePlot plot = new PiePlot(data);
+ * 
+ */ +public class PiePlot extends AbstractPlot implements Navigable { + /** Version id for serialization. */ + private static final long serialVersionUID = 5486418164040578150L; + + /** Key for specifying the tangential axis of a pie plot. */ + public static final String AXIS_TANGENTIAL = "tangential"; //$NON-NLS-1$ + + /** Mapping from data source to point renderer. */ + private final Map pointRenderers; + /** Cache for the {@code Navigator} implementation. */ + private transient PiePlotNavigator navigator; + + /** Position of the pie center. */ + private final Point2D center; + /** Radius of the the pie. */ + private double radius; + /** Starting angle in degrees. */ + private double start; + /** Decides whether pie slices are drawn in clockwise direction. */ + private boolean clockwise; + + /** + * Navigator implementation for pie plots. Zooming changes the + * {@code RADIUS} setting and panning the {@code CENTER} setting. + */ + public static class PiePlotNavigator extends AbstractNavigator { + /** Pie plot that will be navigated. */ + private final PiePlot plot; + /** Location of center in default state. */ + private PointND centerOriginal; + /** Zoom level in default state. */ + private double zoomOriginal; + /** Current zoom level. */ + private double zoom; + + /** + * Initializes a new instance with a pie plot to be navigated. + * @param plot Pie plot. + */ + public PiePlotNavigator(PiePlot plot) { + this.plot = plot; + this.zoom = 1.0; + setDefaultState(); + } + + /** + * Returns the current zoom level of the associated object. + * @return Current zoom level. + */ + public double getZoom() { + return zoom; + } + + /** + * 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; + } + zoom = zoomNew; + plot.setRadius(zoomOriginal*getZoom()); + } + + /** + * Returns the current center point. The returned point contains value in + * world units. + * @return Center point in world units. + */ + public PointND getCenter() { + Point2D center = plot.getCenter(); + return new PointND(center.getX(), center.getY()); + } + + /** + * 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 center) { + if (center == null || !isPannable()) { + return; + } + Point2D center2d = center.getPoint2D(); + plot.setCenter(center2d); + } + + /** + * 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. + */ + @SuppressWarnings("unchecked") + public void pan(PointND deltas) { + PlotArea plotArea = plot.getPlotArea(); + PointND center = (PointND) getCenter(); + double x = center.get(0).doubleValue(); + x += deltas.get(0).doubleValue()/plotArea.getWidth(); + double y = center.get(1).doubleValue(); + y += deltas.get(1).doubleValue()/plotArea.getHeight(); + center.set(0, x); + center.set(1, y); + setCenter(center); + } + + /** + * Sets the object's position and zoom level to the default state. + */ + public void reset() { + setCenter(centerOriginal); + setZoom(1.0); + } + + /** + * Sets the current state as the default state of the object. + * Resetting the navigator will then return to the default state. + */ + public void setDefaultState() { + centerOriginal = getCenter(); + zoomOriginal = plot.getRadius(); + } + } + + /** + * Class that represents the drawing area of a {@code PiePlot}. + */ + public static class PiePlotArea2D extends PlotArea { + /** Version id for serialization. */ + private static final long serialVersionUID = 5646816099037852271L; + + /** Pie plot that this renderer is associated to. */ + private final PiePlot plot; + + /** + * Constructor that creates a new instance and initializes it with a + * plot acting as data provider. + * @param plot Data provider. + */ + public PiePlotArea2D(PiePlot plot) { + this.plot = plot; + } + + /** + * Draws the {@code Drawable} with the specified drawing context. + * @param context Environment used for drawing + */ + public void draw(DrawingContext context) { + drawBackground(context); + drawBorder(context); + drawPlot(context); + plot.drawLegend(context); + } + + @Override + protected void drawPlot(DrawingContext context) { + Graphics2D graphics = context.getGraphics(); + + Shape clipBoundsOld = graphics.getClip(); + Insets2D clipOffset = getClippingOffset(); + if (clipOffset != null) { + final double fontSize = getBaseFont().getSize2D(); + + // Perform clipping + Shape clipBounds = new Rectangle2D.Double( + getX() + clipOffset.getLeft()*fontSize, + getY() + clipOffset.getTop()*fontSize, + getWidth() - clipOffset.getHorizontal()*fontSize, + getHeight() - clipOffset.getVertical()*fontSize + ); + // Take care of old clipping region. This is used when getting + // scrolled in a JScrollPane for example. + if (clipBoundsOld != null) { + Area clipBoundsNew = new Area(clipBoundsOld); + clipBoundsNew.intersect(new Area(clipBounds)); + clipBounds = clipBoundsNew; + } + graphics.setClip(clipBounds); + } + + AffineTransform txOrig = graphics.getTransform(); + graphics.translate(getX(), getY()); + + // Get width and height of the plot area for relative sizes + Rectangle2D bounds = getBounds(); + + // Move to center, so origin for point renderers will be (0, 0) + Point2D center = plot.getCenter(); + if (center == null) { + center = new Point2D.Double(0.5, 0.5); + } + graphics.translate( + center.getX()*bounds.getWidth(), + center.getY()*bounds.getHeight() + ); + + // Paint points and lines + for (DataSource s : plot.getVisibleData()) { + // Skip empty data source + if (s.getColumnCount() == 0) { + continue; + } + + // TODO Use property for column index + int colIndex = 0; + if (colIndex < 0 || colIndex >= s.getColumnCount() || + !s.isColumnNumeric(colIndex)) { + continue; + } + + PointRenderer pointRenderer = plot.getPointRenderer(s); + + String[] axisNames = plot.getMapping(s); + // TODO Use loop to get all axes instead of direct access + Axis axis = plot.getAxis(axisNames[0]); + if (!axis.isValid()) { + continue; + } + AxisRenderer axisRenderer = plot.getAxisRenderer(axisNames[0]); + + List axes = asList(axis); + List axisRenderers = asList(axisRenderer); + // Draw graphics + for (int rowIndex = 0; rowIndex < s.getRowCount(); rowIndex++) { + Row row = s.getRow(rowIndex); + PointData pointData = new PointData( + axes, axisRenderers, row, row.getIndex(), 0); + Shape shape = pointRenderer.getPointShape(pointData); + Drawable point = pointRenderer.getPoint(pointData, shape); + point.setBounds(bounds); + point.draw(context); + } + // Draw labels + for (int rowIndex = 0; rowIndex < s.getRowCount(); rowIndex++) { + Row row = s.getRow(rowIndex); + PointData pointData = new PointData( + axes, axisRenderers, row, row.getIndex(), 0); + Shape shape = pointRenderer.getPointShape(pointData); + Drawable point = pointRenderer.getValue(pointData, shape); + point.setBounds(bounds); + point.draw(context); + } + } + + graphics.setTransform(txOrig); + + if (clipOffset != null) { + // Reset clipping + graphics.setClip(clipBoundsOld); + } + } + } + + /** + * Data class for storing slice information in world units. + */ + protected static final class Slice { + /** Value where the slice starts. */ + public final double start; + /** Value where the slice ends. */ + public final double end; + /** Whether the slice is visible. */ + public final boolean visible; + + /** + * Initializes a new slice with start and end value. + * @param start Value where the slice starts. + * @param end Value where the slice ends. + * @param visible Visibility of the slice. + */ + public Slice(double start, double end, boolean visible) { + this.start = start; + this.end = end; + this.visible = visible; + } + } + + /** + * A point renderer for a single slice in a pie plot. + */ + public static class PieSliceRenderer extends AbstractPointRenderer { + /** Version id for serialization. */ + private static final long serialVersionUID = 1135636437801090607L; + + /** Pie plot this renderer is attached to. */ + private final PiePlot plot; + + /** Relative outer radius of the current pie slice, + * in percentage of the total radius. */ + private double outerRadius; + /** Relative inner radius of the current pie slice, + * in percentage of the total radius. */ + private double innerRadius; + /** Gap of the current pie slice, in pixels. */ + private double gap; + + /** + * Initializes a new instance with a pie plot object. + * @param plot Pie plot. + */ + public PieSliceRenderer(PiePlot plot) { + this.plot = plot; + + setValueColumn(0); + setErrorColumnTop(1); + setErrorColumnBottom(2); + + setColor(new QuasiRandomColors()); + outerRadius = 1.0; + innerRadius = 0.0; + gap = 0.0; + } + + private Slice getSlice(PointData pointData) { + double sliceStart = (Double) pointData.row.get(0); + double sliceEnd = (Double) pointData.row.get(1); + boolean sliceVisible = (Boolean) pointData.row.get(2); + return new Slice(sliceStart, sliceEnd, sliceVisible); + } + + /** + * Returns the value for the outer radius of a pie relative to the + * radius set in the plot. + * @return Outer radius of a pie relative to the radius of the plot. + */ + public double getOuterRadius() { + return outerRadius; + } + + /** + * Sets the value for the outer radius of a pie relative to the radius + * set in the plot. + * @param radius Outer radius of a pie relative to the radius of the + * plot. + */ + public void setOuterRadius(double radius) { + this.outerRadius = radius; + } + + /** + * Returns the value for the inner radius of a pie relative to the + * radius set in the plot. + * @return Inner radius of a pie relative to the radius of the plot. + */ + public double getInnerRadius() { + return innerRadius; + } + + /** + * Sets the value for the inner radius of a pie relative to the radius + * set in the plot. + * @param radius Inner radius of a pie relative to the radius of the + * plot. + */ + public void setInnerRadius(double radius) { + this.innerRadius = radius; + } + + /** + * Returns the width of gaps between the segments relative to the font + * size. + * @return Width of gaps between the segments relative to the font + * size. + */ + public double getGap() { + return gap; + } + + /** + * Sets the width of gaps between the segments relative to the font + * size. + * @param gap Width of gaps between the segments relative to the font + * size. + */ + public void setGap(double gap) { + this.gap = gap; + } + + @Override + public Drawable getPoint(final PointData data, final Shape shape) { + return new AbstractDrawable() { + /** Version id for serialization. */ + private static final long serialVersionUID = -1783451355453643712L; + + public void draw(DrawingContext context) { + PointRenderer renderer = PieSliceRenderer.this; + + Row row = data.row; + if (shape == null) { + return; + } + + Slice slice = getSlice(data); + if (!slice.visible) { + return; + } + + // Paint slice + ColorMapper colorMapper = renderer.getColor(); + Paint paint; + if (colorMapper instanceof ContinuousColorMapper) { + double sum = plot.getSum(row.getSource()); + if (sum == 0.0) { + return; + } + double sliceStartRel = slice.start/sum; + double sliceEndRel = slice.end/sum; + + double coloringRel = 0.0; + int rows = row.getSource().getRowCount(); + if (rows > 1) { + double posRel = data.index / (double)(rows - 1); + double posRelInv = 1.0 - posRel; + coloringRel = + posRelInv*sliceStartRel + posRel*sliceEndRel; + } + paint = ((ContinuousColorMapper) colorMapper).get(coloringRel); + } else { + paint = colorMapper.get(data.index); + } + GraphicsUtils.fillPaintedShape( + context.getGraphics(), shape, paint, null); + } + }; + } + + /** + * 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) { + Slice slice = getSlice(data); + if (!slice.visible) { + return null; + } + + Font font = getValueFont(); + double fontSize = font.getSize2D(); + + PlotArea plotArea = plot.getPlotArea(); + double plotAreaSize = Math.min( + plotArea.getWidth(), plotArea.getHeight())/2.0; + double radiusRel = plot.getRadius(); + double radius = plotAreaSize*radiusRel; + double radiusRelOuter = getOuterRadius(); + double radiusOuter = radius*radiusRelOuter; + + // Construct slice + Row row = data.row; + double sum = plot.getSum(row.getSource()); + if (sum == 0.0) { + return null; + } + double sliceStartRel = slice.start/sum; + double sliceEndRel = slice.end/sum; + + double start = plot.getStart(); + + double sliceSpan = (sliceEndRel - sliceStartRel)*360.0; + double sliceStart; + if (plot.isClockwise()) { + sliceStart = start - sliceEndRel*360.0; + } else { + sliceStart = start + sliceStartRel*360.0; + } + start = MathUtils.normalizeDegrees(start); + + Arc2D pieSlice = new Arc2D.Double( + -radiusOuter, -radiusOuter, + 2.0*radiusOuter, 2.0*radiusOuter, + sliceStart, sliceSpan, + Arc2D.PIE + ); + Area doughnutSlice = new Area(pieSlice); + + double gap = getGap(); + if (gap > 0.0) { + Stroke sliceStroke = + new BasicStroke((float) (gap*fontSize)); + Area sliceContour = + new Area(sliceStroke.createStrokedShape(pieSlice)); + doughnutSlice.subtract(sliceContour); + } + + double radiusRelInner = getInnerRadius(); + if (radiusRelInner > 0.0 && radiusRelInner < radiusRelOuter) { + double radiusInner = radius*radiusRelInner; + Ellipse2D inner = new Ellipse2D.Double( + -radiusInner, -radiusInner, + 2.0*radiusInner, 2.0*radiusInner + ); + Area hole = new Area(inner); + doughnutSlice.subtract(hole); + } + + return doughnutSlice; + } + + /** + * Draws the specified value label for the specified shape. + * @param context Environment used for drawing. + * @param slice Pie slice to draw. + * @param radius Radius of pie slice in view units (e.g. pixels). + * @param row Data row containing the point. + * @param rowIndex Index number used for coloring. + */ + protected void drawValueLabel(DrawingContext context, Slice slice, + double radius, Row row, int rowIndex) { + Comparable value = slice.end - slice.start; + + // Formatting + Format format = getValueFormat(); + if ((format == null) && (value instanceof Number)) { + format = NumberFormat.getInstance(); + } + + // Text to display + String text = (format != null) ? format.format(value) : value.toString(); + + // Visual settings + ColorMapper colors = getValueColor(); + Paint paint = colors.get(rowIndex); + Font font = getValueFont(); + double fontSize = font.getSize2D(); + + // Layout settings + Location location = getValueLocation(); + double alignX = getValueAlignmentX(); + double alignY = getValueAlignmentY(); + double rotation = getValueRotation(); + double distance = getValueDistance(); + if (MathUtils.isCalculatable(distance)) { + distance *= fontSize; + } else { + distance = 0.0; + } + + // Vertical layout + double radiusRelOuter = getOuterRadius(); + double radiusRelInner = getInnerRadius(); + double radiusOuter = radius*radiusRelOuter; + double radiusInner = radius*radiusRelInner; + double distanceV = distance; + double labelPosV; + if (location == Location.NORTH) { + labelPosV = radiusOuter + distanceV; + } else if (location == Location.SOUTH) { + labelPosV = Math.max(radiusInner - distanceV, 0); + } else { + double sliceHeight = radiusOuter - radiusInner; + if (2.0*distance >= sliceHeight) { + alignY = 0.5; + distanceV = 0.0; + } + labelPosV = radiusInner + distanceV + + alignY*(sliceHeight - 2.0*distanceV); + } + + // Horizontal layout + double sum = plot.getSum(row.getSource()); + if (sum == 0.0) { + return; + } + double sliceStartRel = slice.start/sum; + double sliceEndRel = slice.end/sum; + double circumference = 2.0*labelPosV*Math.PI; + double distanceRelH = distance/circumference; + double sliceWidthRel = sliceEndRel - sliceStartRel; + if (2.0*distanceRelH >= sliceWidthRel) { + alignX = 0.5; + distanceRelH = 0.0; + } + double labelPosRelH = sliceStartRel + distanceRelH + + alignX*(sliceWidthRel - 2.0*distanceRelH); + + double start = plot.getStart(); + + double angleStart = Math.toRadians(-start); + double direction = 1.0; + if (!plot.isClockwise()) { + direction = -1.0; + } + double angle = angleStart + direction*labelPosRelH*2.0*Math.PI; + double dirX = Math.cos(angle); + double dirY = Math.sin(angle); + + // Create a label with the settings + Label label = new Label(text); + label.setAlignmentX(1.0 - 0.5*dirX - 0.5); + label.setAlignmentY(0.5*dirY + 0.5); + label.setRotation(rotation); + label.setColor(paint); + label.setFont(font); + + // Calculate label position + Dimension2D sizeLabel = label.getPreferredSize(); + double anchorX = 0.5; + double anchorY = 0.5; + if (location == Location.NORTH || location == Location.SOUTH) { + anchorX = dirX*sizeLabel.getWidth()/2.0; + anchorY = dirY*sizeLabel.getHeight()/2.0; + if (location == Location.SOUTH) { + anchorX = -anchorX; + anchorY = -anchorY; + } + } + + // Resize label component + double x = labelPosV*dirX + anchorX - sizeLabel.getWidth()/2.0; + double y = labelPosV*dirY + anchorY - sizeLabel.getHeight()/2.0; + double w = sizeLabel.getWidth(); + double h = sizeLabel.getHeight(); + label.setBounds(x, y, w, h); + + label.draw(context); + } + + @Override + public Drawable getValue(final PointData data, final Shape shape) { + return new AbstractDrawable() { + /** Version id for serialization. */ + private static final long serialVersionUID1 = 8389872806138135038L; + + public void draw(DrawingContext context) { + PointRenderer renderer = PieSliceRenderer.this; + + Row row = data.row; + if (shape == null) { + return; + } + + Slice slice = getSlice(data); + if (!slice.visible) { + return; + } + + PlotArea plotArea = plot.getPlotArea(); + double plotAreaSize = Math.min( + plotArea.getWidth(), plotArea.getHeight())/2.0; + double radiusRel = plot.getRadius(); + double radius1 = plotAreaSize*radiusRel; + + if (renderer.isValueVisible()) { + drawValueLabel(context, slice, radius1, row, data.index); + } + } + }; + } + } + + /** + * A legend implementation for pie plots that displays items for each data + * value of a data source. + */ + public static class PiePlotLegend extends ValueLegend { + /** Version id for serialization. */ + private static final long serialVersionUID = 309673490751330686L; + + /** Plot that contains settings and renderers. */ + private final PiePlot plot; + + /** + * Initializes a new instance with a specified plot. + * @param plot Plot. + */ + public PiePlotLegend(PiePlot plot) { + this.plot = plot; + } + + @Override + protected Iterable getEntries(DataSource source) { + Iterable slicesAndGaps = super.getEntries(source); + List slices = new LinkedList<>(); + for (Row row : slicesAndGaps) { + if (!row.isColumnNumeric(0)) { + continue; + } + boolean isVisible = (Boolean) row.get(2); + if (isVisible) { + slices.add(row); + } + } + return slices; + } + + @Override + protected Drawable getSymbol(final Row row) { + return new LegendSymbol(row, plot.getPointRenderer(row.getSource()), + plot.getFont(), plot.getLegend().getSymbolSize()); + } + + @Override + protected String getLabel(Row row) { + Number sliceStart = (Number) row.get(0); + Number sliceEnd = (Number) row.get(1); + Number sliceWidth = sliceEnd.doubleValue() - sliceStart.doubleValue(); + Format format = getLabelFormat(); + if ((format == null)) { + format = NumberFormat.getInstance(); + } + return format.format(sliceWidth); + } + } + + private static class LegendSymbol extends AbstractLegend.AbstractSymbol { + private final Row row; + private final PointRenderer pointRenderer; + + public LegendSymbol(Row row, PointRenderer pointRenderer, Font font, Dimension2D symbolSize) { + super(font, symbolSize); + this.row = row; + this.pointRenderer = pointRenderer; + } + + @Override + public void draw(DrawingContext context) { + Rectangle2D bounds = getBounds(); + + Shape shape = new Rectangle2D.Double( + 0.0, 0.0, bounds.getWidth(), bounds.getHeight()); + + PointData pointData = new PointData( + asList((Axis) null), + asList((AxisRenderer) null), + row, row.getIndex(), 0); + + Drawable drawable = pointRenderer.getPoint(pointData, shape); + + Graphics2D graphics = context.getGraphics(); + AffineTransform txOrig = graphics.getTransform(); + graphics.translate(bounds.getX(), bounds.getY()); + drawable.draw(context); + graphics.setTransform(txOrig); + } + } + + /** + * Initializes a new pie plot with the specified data source. + * @param data Data to be displayed. + */ + public PiePlot(DataSource data) { + super(); + + center = new Point2D.Double(0.5, 0.5); + radius = 1.0; + start = 0.0; + clockwise = true; + + pointRenderers = new HashMap<>(); + + setPlotArea(new PiePlotArea2D(this)); + setLegend(new PiePlotLegend(this)); + + add(data); + + createDefaultAxes(); + createDefaultAxisRenderers(); + + dataUpdated(data); + } + + @Override + protected void createDefaultAxes() { + // Create x axis and y axis by default + Axis axisPie = new Axis(); + setAxis(AXIS_TANGENTIAL, axisPie); + } + + @Override + public void autoscaleAxis(String axisName) { + if (!AXIS_TANGENTIAL.equals(axisName)) { + super.autoscaleAxis(axisName); + return; + } + + List sources = getVisibleData(); + if (sources.isEmpty()) { + return; + } + + DataSource data = sources.get(0); + if (data.getRowCount() == 0) { + return; + } + + double sum = getSum(data); + if (sum == 0.0) { + return; + } + + Axis axis = getAxis(axisName); + if (axis == null || !axis.isAutoscaled()) { + return; + } + axis.setRange(0.0, sum); + } + + @Override + protected void createDefaultAxisRenderers() { + // Create a linear renderer for the pie slices by default + AxisRenderer renderer = new LinearRenderer2D(); + // Create a circle with radius 1.0 as shape for the axis + Shape shape = new Ellipse2D.Double(-1.0, -1.0, 2.0, 2.0); + renderer.setShape(shape); + // Don't show axis + renderer.setShapeVisible(false); + + setAxisRenderer(AXIS_TANGENTIAL, renderer); + } + + @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$ + } + + PointRenderer pointRendererDefault = new PieSliceRenderer(this); + setPointRenderer(source, pointRendererDefault); + + super.add(index, source, visible); + setMapping(source, AXIS_TANGENTIAL); + } + + /** + * Returns the {@code PointRenderer} for the specified data source. + * @param s Data source. + * @return PointRenderer. + */ + public PointRenderer getPointRenderer(DataSource s) { + return pointRenderers.get(s); + } + + /** + * Sets the {@code PointRenderer} for a certain data source to the + * specified value. + * @param s Data source. + * @param pointRenderer PointRenderer to be set. + */ + public void setPointRenderer(DataSource s, PointRenderer pointRenderer) { + this.pointRenderers.put(s, pointRenderer); + } + + /** + * Returns a navigator instance that can control the current object. + * @return A navigator instance. + */ + public Navigator getNavigator() { + if (navigator == null) { + navigator = new PiePlotNavigator(this); + } + return navigator; + } + + /** + * Returns the sum of all absolute values in the data column of a specified + * data source. + * @param source Data source. + * @return Sum of all absolute values for the specified data source. + */ + protected double getSum(DataSource source) { + double sum; + synchronized (source) { + sum = (Double) source.get(1, source.getRowCount() - 1); + } + return sum; + } + + private static class PieData extends AbstractDataSource { + private final DataSource data; + + @SuppressWarnings({"unchecked","rawtypes"}) + public PieData(DataSource data) { + this.data = data; + data.addDataListener(new DataListener() { + @Override + public void dataAdded(DataSource source, DataChangeEvent... events) { + notifyDataAdded(events); + } + + @Override + public void dataUpdated(DataSource source, DataChangeEvent... events) { + notifyDataUpdated(events); + } + + @Override + public void dataRemoved(DataSource source, DataChangeEvent... events) { + notifyDataRemoved(events); + } + }); + setColumnTypes(getColumnTypesFor(data).toArray(new Class[] {})); + } + + private List>> getColumnTypesFor(DataSource data) { + List>> columnTypes = new LinkedList<>(); + for (int colIndex = 0; colIndex < data.getColumnCount(); colIndex++) { + Column column = data.getColumn(colIndex); + if (column.isNumeric()) { + columnTypes.add(Double.class); + columnTypes.add(Double.class); + columnTypes.add(Boolean.class); + } else { + columnTypes.add(column.getType()); + } + } + return columnTypes; + } + + @SuppressWarnings({"rawtypes","unchecked"}) + @Override + public Comparable get(int col, int row) { + Iterable accumulatedColumnData = new Accumulation(data.getColumn(0)); + if (col == 0) { + if (row == 0) { + return 0.0; + } + return get(accumulatedColumnData, row - 1); + } else if (col == 1) { + return get(accumulatedColumnData, row); + } else if (col == 2) { + return ((Number) data.get(0, row)).doubleValue() > 0.0; + } + return null; + } + + @Override + public int getRowCount() { + return data.getRowCount(); + } + + private static T get(Iterable iterable, int index) { + T element = null; + int elementIndex = 0; + for (T e : iterable) { + if (elementIndex == index) { + element = e; + break; + } + elementIndex++; + } + return element; + } + } + + public static DataSource createPieData(DataSource data) { + return new PieData(data); + } + + @Override + protected void dataChanged(DataSource source, DataChangeEvent... events) { + super.dataChanged(source, events); + autoscaleAxes(); + } + + /** + * Returns a point which defines the center of the pie. The coordinates + * are relative to the plot area dimensions, i.e. 0.0 means left/top, + * 0.5 means the center, and 1.0 means right/bottom. + * @return Point which defines the center of the pie. + */ + public Point2D getCenter() { + return center; + } + + /** + * Sets the center of the pie. The coordinates must be relative to the plot + * area dimensions, i.e. 0.0 means left/top, 0.5 means the center, and 1.0 + * means right/bottom. + * @param center Point which defines the center of the pie. + */ + public void setCenter(Point2D center) { + this.center.setLocation(center); + } + + /** + * Returns the radius of the pie relative to the plot area size. + * @return Radius of the pie relative to the plot area size. + */ + public double getRadius() { + return radius; + } + + /** + * Sets the radius of the pie relative to the plot area size. + * @param radius Radius of the pie relative to the plot area size. + */ + public void setRadius(double radius) { + this.radius = radius; + } + + /** + * Returns the starting angle of the first segment. The angle is + * counterclockwise. + * @return Starting angle of the first segment in degrees. + */ + public double getStart() { + return start; + } + + /** + * Sets the starting angle of the first segment. The angle is always + * applied counterclockwise. + * @param start Starting angle of the first segment in degrees. + */ + public void setStart(double start) { + double startOld = this.start; + + this.start = start; + + AxisRenderer axisRenderer = getAxisRenderer(PiePlot.AXIS_TANGENTIAL); + if (axisRenderer != null) { + Shape shape = axisRenderer.getShape(); + if (shape != null) { + double delta = Math.toRadians(startOld - start); + AffineTransform tx = AffineTransform.getRotateInstance(delta); + shape = tx.createTransformedShape(shape); + axisRenderer.setShape(shape); + } + } + } + + /** + * Returns whether the segments are in clockwise or counterclockwise order. + * @return {@code true} if segments are in clockwise order, + * otherwise {@code false}. + */ + public boolean isClockwise() { + return clockwise; + } + + /** + * Sets whether the segments will be in clockwise or counterclockwise order. + * @param clockwise {@code true} if segments should be in clockwise order, + * otherwise {@code false}. + */ + public void setClockwise(boolean clockwise) { + this.clockwise = clockwise; + + AxisRenderer axisRenderer = getAxisRenderer(PiePlot.AXIS_TANGENTIAL); + if (axisRenderer != null) { + Shape shape = axisRenderer.getShape(); + if (shape != null) { + shape = GeometryUtils.reverse(shape); + axisRenderer.setShape(shape); + } + } + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/Plot.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/Plot.java new file mode 100644 index 0000000..54ab156 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/Plot.java @@ -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; + +/** + *

Interface for classes that display data in a plot.

+ *

Functionality includes:

+ *
    + *
  • Adding axes to the plot
  • + *
  • Adding a title to the plot
  • + *
  • Adding a legend to the plot
  • + *
  • Administration of settings
  • + *
+ */ +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 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 getData(); + + /** + * Returns a list of all visible data series stored in the plot. + * @return List of all visible data series. + */ + List 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); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/PlotArea.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/PlotArea.java new file mode 100644 index 0000000..6a3dca0 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/PlotArea.java @@ -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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/PlotNavigator.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/PlotNavigator.java new file mode 100644 index 0000000..ab12064 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/PlotNavigator.java @@ -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; + +/** + *

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.

+ * + *

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.

+ */ +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 infos; + /** Axes affected by navigation. */ + private final List 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 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 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 getCenter() { + List 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 center) { + if (!isPannable()) { + return; + } + PointND centerOld = getCenter(); + if (centerOld.equals(center)) { + return; + } + List 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> 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 deltas) { + if (!isPannable()) { + return; + } + PointND 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 centerNew = new PointND<>(centerCoords); + NavigationEvent> 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 centerOld = getCenter(); + + List 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 centerNew = new PointND<>(centerCoordsOriginal); + + NavigationEvent> panEvent = + new NavigationEvent<>(this, centerOld, centerNew); + fireCenterChanged(panEvent); + + NavigationEvent 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 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 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 values); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/RasterPlot.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/RasterPlot.java new file mode 100644 index 0000000..6e4bf56 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/RasterPlot.java @@ -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; + +/** + *

Class that displays two coordinate values and a value as a raster of + * boxes. The data source must provide at least three columns:

+ *
    + *
  • x coordinate
  • + *
  • y coordinate
  • + *
  • value
  • + *
+ *

The method {@link #createRasterData(DataSource)} can be used to convert + * a matrix of values to the (coordinates, value) format.

+ * + *

To create a new {@code RasterPlot} simply create a new instance using + * a suitable data source. Example:

+ *
+ * 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);
+ * 
+ */ +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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/XYPlot.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/XYPlot.java new file mode 100644 index 0000000..45c5917 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/XYPlot.java @@ -0,0 +1,1107 @@ +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.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Dimension2D; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.ArrayList; +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.data.DataSource; +import org.xbib.graphics.graph.gral.data.DummyData; +import org.xbib.graphics.graph.gral.data.Row; +import org.xbib.graphics.graph.gral.graphics.Drawable; +import org.xbib.graphics.graph.gral.graphics.DrawingContext; +import org.xbib.graphics.graph.gral.graphics.Insets2D; +import org.xbib.graphics.graph.gral.graphics.Orientation; +import org.xbib.graphics.graph.gral.navigation.Navigable; +import org.xbib.graphics.graph.gral.navigation.NavigationDirection; +import org.xbib.graphics.graph.gral.navigation.Navigator; +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.AxisListener; +import org.xbib.graphics.graph.gral.plots.axes.AxisRenderer; +import org.xbib.graphics.graph.gral.plots.axes.LinearRenderer2D; +import org.xbib.graphics.graph.gral.plots.axes.Tick; +import org.xbib.graphics.graph.gral.plots.axes.Tick.TickType; +import org.xbib.graphics.graph.gral.plots.legends.AbstractLegend; +import org.xbib.graphics.graph.gral.plots.legends.SeriesLegend; +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.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; + +/** + *

Class that displays data in an two dimensional coordinate system + * (x-y plot). It also serves as a base class for many other plot types.

+ *

To create a new {@code XYPlot} simply create a new instance + * using one or more data sources. Example:

+ *
+ * DataTable data = new DataTable(Integer.class, Integer.class);
+ * data.add( 1, 2);
+ * data.add(-5, 0);
+ *
+ * XYPlot plot = new XYPlot(data);
+ * 
+ */ +public class XYPlot extends AbstractPlot implements Navigable, AxisListener { + + /** Key for specifying the x-axis of an xy-plot. */ + public static final String AXIS_X = "x"; //$NON-NLS-1$ + /** Key for specifying the secondary x-axis of an xy-plot. */ + public static final String AXIS_X2 = "x2"; //$NON-NLS-1$ + /** Key for specifying the y-axis of an xy-plot. */ + public static final String AXIS_Y = "y"; //$NON-NLS-1$ + /** Key for specifying the secondary y-axis of an xy-plot. */ + public static final String AXIS_Y2 = "y2"; //$NON-NLS-1$ + + /** Mapping from data source to point renderers. */ + private final Map> pointRenderersByDataSource; + /** Mapping from data source to line renderers. */ + private final Map> lineRenderersByDataSource; + /** Mapping from data source to area renderers. */ + private final Map> areaRenderersByDataSource; + + /** Cache for the {@code Navigator} implementation. */ + private transient XYPlotNavigator navigator; + /** A flag that shows whether the navigator has been properly + initialized. */ + private transient boolean navigatorInitialized; + + /** + * Constants which determine the direction of zoom and pan actions. + */ + public enum XYNavigationDirection implements NavigationDirection { + /** Value for zooming and panning horizontally. */ + HORIZONTAL(XYPlot.AXIS_X, XYPlot.AXIS_X2), + /** Value for zooming and panning vertically. */ + VERTICAL(XYPlot.AXIS_Y, XYPlot.AXIS_Y2), + /** Value for zooming and panning in all direction. */ + ARBITRARY(XYPlot.AXIS_X, XYPlot.AXIS_Y, XYPlot.AXIS_X2, XYPlot.AXIS_Y2); + + /** Names of the axes that have the same direction. */ + private final String[] axesNames; + + /** + * Initializes a new instance with the names of the axes that have the + * same direction. + * @param axesNames Names of the axes that have the same direction. + */ + XYNavigationDirection(String... axesNames) { + this.axesNames = axesNames; + } + + /** + * Returns the names of the axes that have the direction described by + * this object. + * @return Names of the axes that have the same direction. + */ + public String[] getAxesNames() { + return axesNames; + } + } + + /** + * Navigator implementation for two-dimensional plots. + */ + public static class XYPlotNavigator extends PlotNavigator { + /** + * Initializes a new Navigator for two-dimensional plots with the + * default axes. + * @param plot Two-dimensional plot that should be controlled. + */ + public XYPlotNavigator(XYPlot plot) { + super(plot, XYNavigationDirection.ARBITRARY.getAxesNames()); + } + + @Override + public void setDirection(NavigationDirection direction) { + if (direction == getDirection()) { + return; + } + if (!(direction instanceof XYNavigationDirection)) { + throw new IllegalArgumentException("Unknown direction."); + } + String[] axesNames = ((XYNavigationDirection)direction).getAxesNames(); + setAxes(axesNames); + super.setDirection(direction); + } + + @Override + protected Number getDimensionValue(String axisName, + PointND values) { + if (XYPlot.AXIS_Y.equals(axisName) || XYPlot.AXIS_Y2.equals(axisName)) { + return -values.get(1).doubleValue(); + } + return values.get(0); + } + + @Override + protected int getDimensions() { + return 2; + } + } + + /** + * Class that represents the drawing area of an {@code XYPlot}. + */ + public static class XYPlotArea2D extends PlotArea { + /** Version id for serialization. */ + private static final long serialVersionUID = -3673157774425536428L; + + /** x-y plot this plot area is associated to. */ + private final XYPlot plot; + + /** Decides whether the horizontal grid lines at major ticks are drawn. */ + private boolean majorGridX; + /** Decides whether the vertical grid lines at major ticks are drawn. */ + private boolean majorGridY; + /** Paint to fill the grid lines at major ticks. */ + private Paint majorGridColor; + + /** Decides whether the horizontal grid lines at minor ticks are drawn. */ + private boolean minorGridX; + /** Decides whether the vertical grid lines at minor ticks are drawn. */ + private boolean minorGridY; + /** Paint to fill the grid lines at minor ticks. */ + private Paint minorGridColor; + + /** + * Creates a new instance with default settings and initializes it with + * a plot serving as data provider. + * @param plot Data provider. + */ + public XYPlotArea2D(XYPlot plot) { + this.plot = plot; + + majorGridX = true; + majorGridY = true; + majorGridColor = new Color(0.0f, 0.0f, 0.0f, 0.1f); + + minorGridX = false; + minorGridY = false; + minorGridColor = new Color(0.0f, 0.0f, 0.0f, 0.05f); + } + + /** + * Draws the {@code Drawable} with the specified {@code Graphics2D} + * object. + * @param context Environment used for drawing + */ + public void draw(DrawingContext context) { + drawBackground(context); + drawGrid(context); + drawBorder(context); + drawPlot(context); + plot.drawAxes(context); + plot.drawLegend(context); + } + + /** + * Draws the grid using the specified drawing context. + * @param context Environment used for drawing. + */ + protected void drawGrid(DrawingContext context) { + Graphics2D graphics = context.getGraphics(); + + AffineTransform txOrig = graphics.getTransform(); + graphics.translate(getX(), getY()); + AffineTransform txOffset = graphics.getTransform(); + Rectangle2D bounds = getBounds(); + + // Draw gridX + if (isMajorGridX() || isMinorGridX()) { + AxisRenderer axisXRenderer = plot.getAxisRenderer(AXIS_X); + Axis axisX = plot.getAxis(AXIS_X); + if (axisXRenderer != null && axisX != null && axisX.isValid()) { + Shape shapeX = axisXRenderer.getShape(); + Rectangle2D shapeBoundsX = shapeX.getBounds2D(); + List ticksX = axisXRenderer.getTicks(axisX); + Line2D gridLineVert = new Line2D.Double( + -shapeBoundsX.getMinX(), + -shapeBoundsX.getMinY(), + -shapeBoundsX.getMinX(), + bounds.getHeight() - shapeBoundsX.getMinY() + ); + for (Tick tick : ticksX) { + if ((tick.type == TickType.MAJOR && !isMajorGridX()) || + (tick.type == TickType.MINOR && !isMinorGridX())) { + continue; + } + Point2D tickPoint = tick.position.getPoint2D(); + if (tickPoint == null) { + continue; + } + + Paint paint = majorGridColor; + if (tick.type == TickType.MINOR) { + paint = getMinorGridColor(); + } + graphics.translate(tickPoint.getX(), tickPoint.getY()); + GraphicsUtils.drawPaintedShape( + graphics, gridLineVert, paint, null, null); + graphics.setTransform(txOffset); + } + } + } + + // Draw gridY + if (isMajorGridY() || isMinorGridY()) { + Axis axisY = plot.getAxis(AXIS_Y); + AxisRenderer axisYRenderer = plot.getAxisRenderer(AXIS_Y); + if (axisY != null && axisY.isValid() && axisYRenderer != null) { + Shape shapeY = axisYRenderer.getShape(); + Rectangle2D shapeBoundsY = shapeY.getBounds2D(); + List ticksY = axisYRenderer.getTicks(axisY); + Line2D gridLineHoriz = new Line2D.Double( + -shapeBoundsY.getMinX(), -shapeBoundsY.getMinY(), + bounds.getWidth() - shapeBoundsY.getMinX(), -shapeBoundsY.getMinY() + ); + for (Tick tick : ticksY) { + boolean isMajorTick = tick.type == TickType.MAJOR; + boolean isMinorTick = tick.type == TickType.MINOR; + if ((isMajorTick && !isMajorGridY()) || + (isMinorTick && !isMinorGridY())) { + continue; + } + Point2D tickPoint = tick.position.getPoint2D(); + if (tickPoint == null) { + continue; + } + + Paint paint = majorGridColor; + if (isMinorTick) { + paint = getMinorGridColor(); + } + graphics.translate(tickPoint.getX(), tickPoint.getY()); + GraphicsUtils.drawPaintedShape( + graphics, gridLineHoriz, paint, null, null); + graphics.setTransform(txOffset); + } + } + } + + graphics.setTransform(txOrig); + } + + @Override + protected void drawPlot(DrawingContext context) { + Graphics2D graphics = context.getGraphics(); + + Shape clipBoundsOld = graphics.getClip(); + Insets2D clipOffset = getClippingOffset(); + if (clipOffset != null) { + final double fontSize = getBaseFont().getSize2D(); + + // Perform clipping + Shape clipBounds = new Rectangle2D.Double( + getX() + clipOffset.getLeft()*fontSize, + getY() + clipOffset.getTop()*fontSize, + getWidth() - clipOffset.getHorizontal()*fontSize, + getHeight() - clipOffset.getVertical()*fontSize + ); + // Take care of old clipping region. This is used when getting + // scrolled in a JScrollPane for example. + if (clipBoundsOld != null) { + Area clipBoundsNew = new Area(clipBoundsOld); + clipBoundsNew.intersect(new Area(clipBounds)); + clipBounds = clipBoundsNew; + } + graphics.setClip(clipBounds); + } + + AffineTransform txOrig = graphics.getTransform(); + graphics.translate(getX(), getY()); + AffineTransform txOffset = graphics.getTransform(); + + + // Paint points and lines + for (DataSource s : plot.getVisibleData()) { + // Skip empty data source + if (s.getColumnCount() == 0) { + continue; + } + + int colX = 0; + if (colX < 0 || colX >= s.getColumnCount() || !s.isColumnNumeric(colX)) { + continue; + } + int colY = 1; + if (colY < 0 || colY >= s.getColumnCount() || !s.isColumnNumeric(colY)) { + continue; + } + + String[] axisNames = plot.getMapping(s); + Axis axisX = plot.getAxis(axisNames[0]); + Axis axisY = plot.getAxis(axisNames[1]); + if (!axisX.isValid() || !axisY.isValid()) { + continue; + } + AxisRenderer axisXRenderer = plot.getAxisRenderer(axisNames[0]); + AxisRenderer axisYRenderer = plot.getAxisRenderer(axisNames[1]); + + List points = new LinkedList<>(); + for (int i = 0; i < s.getRowCount(); i++) { + Row row = new Row(s, i); + Number valueX = (Number) row.get(colX); + Number valueY = (Number) row.get(colY); + + PointND axisPosX = (axisXRenderer != null) + ? axisXRenderer.getPosition(axisX, valueX, true, false) + : new PointND<>(0.0, 0.0); + PointND axisPosY = (axisYRenderer != null) + ? axisYRenderer.getPosition(axisY, valueY, true, false) + : new PointND<>(0.0, 0.0); + if (axisPosX == null || axisPosY == null) { + continue; + } + + PointND pos = new PointND<>( + axisPosX.get(PointND.X), axisPosY.get(PointND.Y)); + + PointData pointData = new PointData( + Arrays.asList(axisX, axisY), + Arrays.asList(axisXRenderer, axisYRenderer), + row, row.getIndex(), colY); + + DataPoint dataPoint = new DataPoint(pointData, pos); + points.add(dataPoint); + } + + List pointRenderers = new ArrayList<>(plot.getPointRenderers(s)); + Collections.reverse(pointRenderers); + + List areaRenderers = new ArrayList<>(plot.getAreaRenderers(s)); + Collections.reverse(areaRenderers); + for (AreaRenderer areaRenderer : areaRenderers) { + Shape punchedArea = areaRenderer.getAreaShape(points); + for (PointRenderer pointRenderer : pointRenderers) { + List punchShapes = new ArrayList<>(points.size()); + for (DataPoint point : points) { + Shape punchShape = pointRenderer.getPointShape(point.data); + punchShapes.add(punchShape); + } + punchedArea = punch(punchedArea, points, punchShapes, areaRenderer.getGap(), areaRenderer.isGapRounded()); + } + Drawable drawable = areaRenderer.getArea(points, punchedArea); + drawable.draw(context); + } + + List lineRenderers = new ArrayList<>(plot.getLineRenderers(s)); + Collections.reverse(lineRenderers); + for (LineRenderer lineRenderer : lineRenderers) { + Shape punchedLine = lineRenderer.getLineShape(points); + for (PointRenderer pointRenderer : pointRenderers) { + List punchShapes = new ArrayList<>(points.size()); + for (DataPoint point : points) { + Shape punchShape = pointRenderer.getPointShape(point.data); + punchShapes.add(punchShape); + } + punchedLine = punch(punchedLine, points, punchShapes, lineRenderer.getGap(), lineRenderer.isGapRounded()); + } + Drawable drawable = lineRenderer.getLine(points, punchedLine); + drawable.draw(context); + } + if (!plot.getPointRenderers(s).isEmpty()) { + // Draw graphics + for (DataPoint point : points) { + PointND pos = point.position; + double pointX = pos.get(PointND.X); + double pointY = pos.get(PointND.Y); + graphics.translate(pointX, pointY); + for (PointRenderer pointRenderer : plot.getPointRenderers(s)) { + Shape pointShape = pointRenderer.getPointShape(point.data); + Drawable pointDrawable = pointRenderer.getPoint(point.data, pointShape); + pointDrawable.draw(context); + } + graphics.setTransform(txOffset); + } + // Draw labels + for (DataPoint point : points) { + PointND pos = point.position; + double pointX = pos.get(PointND.X); + double pointY = pos.get(PointND.Y); + graphics.translate(pointX, pointY); + for (PointRenderer pointRenderer : plot.getPointRenderers(s)) { + Shape pointShape = pointRenderer.getPointShape(point.data); + Drawable labelDrawable = pointRenderer.getValue(point.data, pointShape); + labelDrawable.draw(context); + } + graphics.setTransform(txOffset); + } + } + } + + // Reset transformation (offset) + graphics.setTransform(txOrig); + + if (clipOffset != null) { + // Reset clipping + graphics.setClip(clipBoundsOld); + } + } + + /** + * Returns the shape from which the shapes of the specified points are subtracted. + * @param shape Shape to be modified. + * @param dataPoints Data points on the line. + * @param punchShapes Shape used for punching. + * @param gap Gap between shape and point shapes. + * @param roundedGaps {@code true} if the shape gaps are rounded. + * @return Punched shape. + */ + protected static Shape punch(Shape shape, List dataPoints, List punchShapes, double gap, boolean roundedGaps) { + if (!MathUtils.isCalculatable(gap) || gap == 0.0) { + return shape; + } + + // Subtract shapes of data points from the line to yield gaps. + Area punched = new Area(shape); + for (int pointIndex = 0; pointIndex < dataPoints.size(); pointIndex++) { + DataPoint p = dataPoints.get(pointIndex); + punched = GeometryUtils.punch(punched, gap, roundedGaps, + p.position.getPoint2D(), punchShapes.get(pointIndex)); + } + return punched; + } + + /** + * Returns whether horizontal grid lines at major ticks along the + * x-axis are drawn. + * @return {@code true} if horizontal grid lines at major ticks along + * the x-axis are drawn, otherwise {@code false}. + */ + public boolean isMajorGridX() { + return majorGridX; + } + + /** + * Sets whether horizontal grid lines at major ticks along the x-axis + * will be drawn. + * @param gridMajorX {@code true} if horizontal grid lines at major + * ticks along the x-axis should be drawn, otherwise {@code false}. + */ + public void setMajorGridX(boolean gridMajorX) { + this.majorGridX = gridMajorX; + } + + /** + * Returns whether vertical grid lines at major ticks along the y-axis + * are drawn. + * @return {@code true} if vertical grid lines at major ticks along the + * y-axis are drawn, otherwise {@code false}. + */ + public boolean isMajorGridY() { + return majorGridY; + } + + /** + * Sets whether vertical grid lines at major ticks along the y-axis + * will be drawn. + * @param gridMajorY {@code true} if vertical grid lines at major ticks + * along the y-axis should be drawn, otherwise {@code false}. + */ + public void setMajorGridY(boolean gridMajorY) { + this.majorGridY = gridMajorY; + } + + /** + * Returns the paint which is used to paint the grid lines at major + * ticks. + * @return Paint which is used to paint the grid lines at major ticks. + */ + public Paint getMajorGridColor() { + return majorGridColor; + } + + /** + * Sets the paint which will be used to paint the grid lines at major + * ticks. + * @param color Paint which should be used to paint the grid lines at + * major ticks. + */ + public void setMajorGridColor(Color color) { + this.majorGridColor = color; + } + + /** + * Returns whether horizontal grid lines at minor ticks along the + * x-axis are drawn. + * @return {@code true} if horizontal grid lines at minor ticks along + * the x-axis are drawn, otherwise {@code false}. + */ + public boolean isMinorGridX() { + return minorGridX; + } + + /** + * Sets whether horizontal grid lines at minor ticks along the x-axis + * will be drawn. + * @param gridMinorX {@code true} if horizontal grid lines at minor + * ticks along the x-axis should be drawn, otherwise {@code false}. + */ + public void setMinorGridX(boolean gridMinorX) { + this.minorGridX = gridMinorX; + } + + /** + * Returns whether vertical grid lines at minor ticks along the y-axis + * are drawn. + * @return {@code true} if vertical grid lines at minor ticks along the + * y-axis are drawn, otherwise {@code false}. + */ + public boolean isMinorGridY() { + return minorGridY; + } + + /** + * Sets whether vertical grid lines at minor ticks along the y-axis + * will be drawn. + * @param gridMinorY {@code true} if vertical grid lines at minor ticks + * along the y-axis should be drawn, otherwise {@code false}. + */ + public void setMinorGridY(boolean gridMinorY) { + this.minorGridY = gridMinorY; + } + + /** + * Returns the paint which is used to paint the grid lines at minor + * ticks. + * @return Paint which is used to paint the grid lines at minor ticks. + */ + public Paint getMinorGridColor() { + return minorGridColor; + } + + /** + * Sets the paint which will be used to paint the grid lines at minor + * ticks. + * @param color Paint which should be used to paint the grid lines at + * minor ticks. + */ + public void setMinorGridColor(Color color) { + this.minorGridColor = color; + } + } + + /** + * Class that displays a legend in an {@code XYPlot}. + */ + public static class XYLegend extends SeriesLegend { + /** Version id for serialization. */ + private static final long serialVersionUID = -4629928754001372002L; + + /** Plot that contains settings and renderers. */ + private final XYPlot plot; + + /** + * Constructor that initializes the instance with a plot acting as a + * provider for settings and renderers. + * @param plot Plot. + */ + public XYLegend(XYPlot plot) { + this.plot = plot; + } + + @Override + protected Drawable getSymbol(DataSource data) { + return new LegendSymbol(plot, data, plot.getFont(), plot.getLegend().getSymbolSize()); + } + } + + private static class LegendSymbol extends AbstractLegend.AbstractSymbol { + /** Source for dummy data. */ + private static final DataSource DUMMY_DATA = new DummyData(2, Integer.MAX_VALUE, 0.5); + private final XYPlot plot; + private final DataSource data; + + public LegendSymbol(XYPlot plot, DataSource data, Font font, Dimension2D symbolSize) { + super(font, symbolSize); + this.plot = plot; + this.data = data; + } + + @Override + public void draw(DrawingContext context) { + Row symbolRow = new Row(DUMMY_DATA, 0); + Rectangle2D bounds = getBounds(); + + Axis axisX = new Axis(0.0, 1.0); + AxisRenderer axisRendererX = new LinearRenderer2D(); + axisRendererX.setShape(new Line2D.Double( + bounds.getMinX(), bounds.getCenterY(), + bounds.getMaxX(), bounds.getCenterY())); + Axis axisY = new Axis(0.0, 1.0); + AxisRenderer axisRendererY = new LinearRenderer2D(); + axisRendererY.setShape(new Line2D.Double( + bounds.getCenterX(), bounds.getMaxY(), + bounds.getCenterX(), bounds.getMinY())); + + PointData pointData = new PointData( + Arrays.asList(axisX, axisY), + Arrays.asList(axisRendererX, axisRendererY), + symbolRow, symbolRow.getIndex(), 0); + + DataPoint p1 = new DataPoint( + pointData, + new PointND<>(bounds.getMinX(), bounds.getCenterY()) + ); + DataPoint p2 = new DataPoint( + pointData, + new PointND<>(bounds.getCenterX(), bounds.getCenterY()) + ); + DataPoint p3 = new DataPoint( + pointData, + new PointND<>(bounds.getMaxX(), bounds.getCenterY()) + ); + List points = Arrays.asList(p1, p2, p3); + + // TODO: Provide a means to set the AreaRenderer used for the Legend + AreaRenderer areaRenderer = null; + List areaRenderers = plot.getAreaRenderers(data); + if (!areaRenderers.isEmpty()) { + areaRenderer = areaRenderers.get(0); + } + if (areaRenderer != null) { + Shape area = areaRenderer.getAreaShape(points); + Drawable drawable = areaRenderer.getArea(points, area); + drawable.draw(context); + } + + // TODO: Provide a means to set the LineRenderer used for the Legend + LineRenderer lineRenderer = null; + List lineRenderers = plot.getLineRenderers(data); + if (!lineRenderers.isEmpty()) { + lineRenderer = lineRenderers.get(0); + } + if (lineRenderer != null) { + Shape line = lineRenderer.getLineShape(points); + Drawable drawable = lineRenderer.getLine(points, line); + drawable.draw(context); + } + + // TODO: Provide a means to set the PointRenderer used for the Legend + PointRenderer pointRenderer = null; + List pointRenderers = plot.getPointRenderers(data); + if (!pointRenderers.isEmpty()) { + pointRenderer = pointRenderers.get(0); + } + if (pointRenderer != null) { + Graphics2D graphics = context.getGraphics(); + Point2D pos = p2.position.getPoint2D(); + AffineTransform txOrig = graphics.getTransform(); + graphics.translate(pos.getX(), pos.getY()); + Shape shape = pointRenderer.getPointShape(pointData); + Drawable drawable = pointRenderer.getPoint(pointData, shape); + drawable.draw(context); + graphics.setTransform(txOrig); + } + } + } + + /** + * Initializes a new instance object with the specified data sources and + * reasonable default settings. + * @param data Data to be displayed. + */ + public XYPlot(DataSource... data) { + super(); + + pointRenderersByDataSource = new HashMap<>(data.length); + lineRenderersByDataSource = new HashMap<>(data.length); + areaRenderersByDataSource = new HashMap<>(data.length); + + setPlotArea(new XYPlotArea2D(this)); + setLegend(new XYLegend(this)); + + // Handle data sources after the renderer lists are initialized + for (DataSource source : data) { + add(source); + } + + createDefaultAxes(); + autoscaleAxes(); + createDefaultAxisRenderers(); + + // Listen for changes of the axis range + for (String axisName : getAxesNames()) { + getAxis(axisName).addAxisListener(this); + } + } + + @Override + protected void createDefaultAxes() { + // Create x axis and y axis by default + Axis axisX = new Axis(); + Axis axisY = new Axis(); + setAxis(AXIS_X, axisX); + setAxis(AXIS_Y, axisY); + } + + @Override + protected void createDefaultAxisRenderers() { + // Create renderers for x and y axes by default + AxisRenderer axisXRenderer = new LinearRenderer2D(); + AxisRenderer axisYRenderer = new LinearRenderer2D(); + setAxisRenderer(AXIS_X, axisXRenderer); + setAxisRenderer(AXIS_Y, axisYRenderer); + } + + @Override + protected void layoutAxes() { + if (getPlotArea() == null) { + return; + } + + // Set the new shapes first to allow for correct positioning + layoutAxisShape(AXIS_X, Orientation.HORIZONTAL); + layoutAxisShape(AXIS_X2, Orientation.HORIZONTAL); + layoutAxisShape(AXIS_Y, Orientation.VERTICAL); + layoutAxisShape(AXIS_Y2, Orientation.VERTICAL); + + // Set bounds with new axis shapes + layoutAxisComponent(AXIS_X, Orientation.HORIZONTAL); + layoutAxisComponent(AXIS_X2, Orientation.HORIZONTAL); + layoutAxisComponent(AXIS_Y, Orientation.VERTICAL); + layoutAxisComponent(AXIS_Y2, Orientation.VERTICAL); + } + + private void layoutAxisShape(String axisName, Orientation orientation) { + Rectangle2D plotBounds = getPlotArea().getBounds(); + + Drawable comp = getAxisComponent(axisName); + AxisRenderer renderer = getAxisRenderer(axisName); + + if (comp == null || renderer == null) { + return; + } + + Dimension2D size = comp.getPreferredSize(); + + Shape shape; + if (orientation == Orientation.HORIZONTAL) { + shape = new Line2D.Double( + 0.0, 0.0, + plotBounds.getWidth(), 0.0 + ); + } else { + shape = new Line2D.Double( + size.getWidth(), plotBounds.getHeight(), + size.getWidth(), 0.0 + ); + } + renderer.setShape(shape); + } + + private void layoutAxisComponent(String axisName, Orientation orientation) { + Drawable comp = getAxisComponent(axisName); + AxisRenderer renderer = getAxisRenderer(axisName); + if (comp == null || renderer == null) { + return; + } + + String nameSecondary; + if (orientation == Orientation.HORIZONTAL) { + nameSecondary = AXIS_Y; + } else { + nameSecondary = AXIS_X; + } + Axis axisSecondary = getAxis(nameSecondary); + AxisRenderer rendererSecondary = getAxisRenderer(nameSecondary); + if (axisSecondary == null || !axisSecondary.isValid() || + rendererSecondary == null) { + return; + } + + Number intersection = renderer.getIntersection(); + PointND pos = rendererSecondary.getPosition( + axisSecondary, intersection, false, false); + + if (pos == null) { + pos = new PointND<>(0.0, 0.0); + } + + Rectangle2D plotBounds = getPlotArea().getBounds(); + Dimension2D size = comp.getPreferredSize(); + + if (orientation == Orientation.HORIZONTAL) { + comp.setBounds( + plotBounds.getMinX(), + pos.get(1) + plotBounds.getMinY(), + plotBounds.getWidth(), + size.getHeight() + ); + } else { + comp.setBounds( + plotBounds.getMinX() - size.getWidth() + pos.get(0), + plotBounds.getMinY(), + size.getWidth(), + plotBounds.getHeight() + ); + } + } + + /** + * Returns all {@code PointRenderer}s that display the data of the specified data source. + * @param s Data source in question. + * @return Renderers being applied on the specified data source. + */ + public List getPointRenderers(DataSource s) { + List pointRenderers = pointRenderersByDataSource.get(s); + if (pointRenderers != null) { + return Collections.unmodifiableList(pointRenderers); + } + return Collections.emptyList(); + } + + /** + * Adds a {@code PointRenderer} for the specified data source. + * @param s Data to be rendered. + * @param pointRenderer PointRenderer to be used. + */ + public void addPointRenderer(DataSource s, PointRenderer pointRenderer) { + List pointRenderers = pointRenderersByDataSource.get(s); + if (pointRenderers == null) { + pointRenderers = new ArrayList<>(); + pointRenderersByDataSource.put(s, pointRenderers); + } + pointRenderers.add(pointRenderer); + } + + /** + * Decouples the specified {@code PointRenderer} from the rendering of the specified data source. + * @param s Data to be rendered no longer. + * @param pointRenderer PointRenderer to be removed. + */ + public void removePointRenderer(DataSource s, PointRenderer pointRenderer) { + List pointRenderers = pointRenderersByDataSource.get(s); + if (pointRenderers != null) { + pointRenderers.remove(pointRenderer); + } + } + + /** + * Sets the {@code PointRenderer}s for a certain data source to the specified value. + * @param s Data source. + * @param pointRenderers PointRenderers to be set. + */ + public void setPointRenderers(DataSource s, List pointRenderers) { + this.pointRenderersByDataSource.put(s, pointRenderers); + } + + /** + * Sets the {@code PointRenderer}s for a certain data source to the specified value. + * @param s Data source. + * @param pointRendererFirst First PointRenderer. + * @param pointRenderers Remaining PointRenderers to be set. + */ + public void setPointRenderers(DataSource s, PointRenderer pointRendererFirst, PointRenderer... pointRenderers) { + List pointRendererList = null; + if (pointRendererFirst == null) { + setPointRenderers(s, pointRendererList); + return; + } + pointRendererList = new ArrayList<>(pointRenderers.length + 1); + pointRendererList.add(pointRendererFirst); + for (PointRenderer pointRenderer : pointRenderers) { + if (pointRenderer == null) { + throw new IllegalArgumentException("A PointRenderer for a DataSource cannot be null."); + } + pointRendererList.add(pointRenderer); + } + setPointRenderers(s, pointRendererList); + } + + /** + * Returns all {@code LineRenderer}s that display the data of the specified data source. + * @param s Data source in question. + * @return Renderers being applied on the specified data source. + */ + public List getLineRenderers(DataSource s) { + List lineRenderers = lineRenderersByDataSource.get(s); + if (lineRenderers != null) { + return Collections.unmodifiableList(lineRenderers); + } + return Collections.emptyList(); + } + + /** + * Sets the {@code LineRenderer}s for a certain data source to the specified + * value. + * @param s Data source. + * @param lineRenderers {@code LineRenderer}s to be set. + */ + public void setLineRenderers(DataSource s, List lineRenderers) { + lineRenderersByDataSource.put(s, lineRenderers); + } + + /** + * Sets the {@code LineRenderer}s for a certain data source to the specified + * value. + * @param s Data source. + * @param lineRendererFirst First {@code LineRenderer} to be set. + * @param lineRenderers Remaining {@code LineRenderer}s to be set. + */ + public void setLineRenderers(DataSource s, LineRenderer lineRendererFirst, LineRenderer... lineRenderers) { + List lineRendererList = null; + if (lineRendererFirst == null) { + setLineRenderers(s, lineRendererList); + return; + } + lineRendererList = new ArrayList<>(lineRenderers.length + 1); + lineRendererList.add(lineRendererFirst); + for (LineRenderer lineRenderer : lineRenderers) { + if (lineRenderer == null) { + throw new IllegalArgumentException("A LineRenderer for a DataSource cannot be null."); + } + lineRendererList.add(lineRenderer); + } + setLineRenderers(s, lineRendererList); + } + + /** + * Returns all {@code AreaRenderer}s for the specified data source. + * @param s Data source. + * @return {@code AreaRenderer}s used to render the {@code DataSource}. + */ + public List getAreaRenderers(DataSource s) { + List areaRenderers = areaRenderersByDataSource.get(s); + if (areaRenderers != null) { + return Collections.unmodifiableList(areaRenderers); + } + return Collections.emptyList(); + } + + /** + * Sets the {@code AreaRenderer}s for a certain data source to the specified + * value. + * @param s Data source. + * @param areaRenderers {@code AreaRenderer}s to be set. + */ + public void setAreaRenderers(DataSource s, List areaRenderers) { + areaRenderersByDataSource.put(s, areaRenderers); + } + + /** + * Sets the {@code AreaRenderer}s for a certain data source to the specified + * value. + * @param s Data source. + * @param areaRendererFirst First {@code AreaRenderer} to be set. + * @param areaRenderers Remaining {@code AreaRenderer}s to be set. + */ + public void setAreaRenderers(DataSource s, AreaRenderer areaRendererFirst, AreaRenderer... areaRenderers) { + List areaRendererList = null; + if (areaRendererFirst == null) { + setAreaRenderers(s, areaRendererList); + return; + } + areaRendererList = new ArrayList<>(areaRenderers.length + 1); + areaRendererList.add(areaRendererFirst); + for (AreaRenderer areaRenderer : areaRenderers) { + if (areaRenderer == null) { + throw new IllegalArgumentException("An AreaRenderer for a DataSource cannot be null."); + } + areaRendererList.add(areaRenderer); + } + setAreaRenderers(s, areaRendererList); + } + + @Override + public void setAxisRenderer(String axisName, AxisRenderer renderer) { + if (renderer != null) { + if (AXIS_X2.equals(axisName) || AXIS_Y.equals(axisName)) { + renderer.setShapeNormalOrientationClockwise(true); + } + if (AXIS_Y.equals(axisName)) { + renderer.getLabel().setRotation(90.0); + } + } + super.setAxisRenderer(axisName, renderer); + } + + @Override + public void add(int index, DataSource source, boolean visible) { + super.add(index, source, visible); + + // Set axis mapping + setMapping(source, AXIS_X, AXIS_Y); + // The mapping from columns to axes has changed, so scaling has to be + // refreshed + autoscaleAxes(); + + // Assign default renderers + PointRenderer pointRendererDefault = new DefaultPointRenderer2D(); + LineRenderer lineRendererDefault = null; + AreaRenderer areaRendererDefault = null; + // FIXME: Overwrites possible present point and line renderers + setPointRenderers(source, pointRendererDefault); + setLineRenderers(source, lineRendererDefault); + setAreaRenderers(source, areaRendererDefault); + } + + /** + * Returns a navigator instance that can control the current object. + * @return A navigator instance. + */ + public Navigator getNavigator() { + if (navigator == null) { + navigator = new XYPlotNavigator(this); + } + return navigator; + } + + @Override + public void draw(DrawingContext context) { + super.draw(context); + if (!navigatorInitialized) { + getNavigator().setDefaultState(); + navigatorInitialized = true; + } + } + + /** + * Notified if the range of an axis has changed. + * @param axis Axis instance that has changed. + * @param min New minimum value. + * @param max New maximum value. + */ + public void rangeChanged(Axis axis, Number min, Number max) { + layoutAxes(); + } + + /** + * 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 + for (String axisName : getAxesNames()) { + getAxis(axisName).addAxisListener(this); + } + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/areas/AbstractAreaRenderer.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/areas/AbstractAreaRenderer.java new file mode 100644 index 0000000..a260ee3 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/areas/AbstractAreaRenderer.java @@ -0,0 +1,62 @@ +package org.xbib.graphics.graph.gral.plots.areas; + +import java.awt.Color; +import java.awt.Paint; + +/** + *

Abstract class that renders an area in two-dimensional space.

+ *

Functionality includes:

+ *
    + *
  • Punching data points out of the area's shape
  • + *
  • Administration of settings
  • + *
+ */ +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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/areas/AreaRenderer.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/areas/AreaRenderer.java new file mode 100644 index 0000000..35e844e --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/areas/AreaRenderer.java @@ -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 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 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); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/areas/DefaultAreaRenderer2D.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/areas/DefaultAreaRenderer2D.java new file mode 100644 index 0000000..22267c4 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/areas/DefaultAreaRenderer2D.java @@ -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 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 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 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/areas/LineAreaRenderer2D.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/areas/LineAreaRenderer2D.java new file mode 100644 index 0000000..bf4d51a --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/areas/LineAreaRenderer2D.java @@ -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 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 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/AbstractAxisRenderer2D.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/AbstractAxisRenderer2D.java new file mode 100644 index 0000000..abd2ecb --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/AbstractAxisRenderer2D.java @@ -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; + +/** + *

Abstract class that provides function for rendering axes in + * two-dimensional space.

+ *

Functionality includes:

+ *
    + *
  • Calculating tick positions of an axis
  • + *
  • Calculating tick normals
  • + *
  • Administration of settings
  • + *
+ */ +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 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 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 labelPos = getPosition(axis, axisLabelPos, false, true); + PointND 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 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 getTicks(Axis axis) { + List ticks = new LinkedList<>(); + + if (!axis.isValid()) { + return ticks; + } + + double min = axis.getMin().doubleValue(); + double max = axis.getMax().doubleValue(); + + Set 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 ticks, Axis axis, + double min, double max, Set 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 ticks, Axis axis, + double min, double max, Set tickPositions) { + Map 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 tickPoint = getPosition(axis, tickPositionWorld, false, false); + + // Calculate tick normal + PointND tickNormal = getNormal(axis, tickPositionWorld, false, false); + + // Retrieve tick label + String tickLabel; + Map 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 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 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 getCustomTicks() { + return Collections.unmodifiableMap(customTicks); + } + + @Override + public void setCustomTicks(Map 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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/Axis.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/Axis.java new file mode 100644 index 0000000..6c468f4 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/Axis.java @@ -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; + +/** + *

Class that represents an arbitrary axis.

+ *

Functionality includes:

+ *
    + *
  • Different ways of setting and getting the range of this axis
  • + *
  • Administration of {@link AxisListener AxisListeners}
  • + *
+ */ +public class Axis { + + /** Objects that will be notified when axis settings are changing. */ + private transient Set 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); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/AxisListener.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/AxisListener.java new file mode 100644 index 0000000..82dcb3f --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/AxisListener.java @@ -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); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/AxisRenderer.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/AxisRenderer.java new file mode 100644 index 0000000..2d10eef --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/AxisRenderer.java @@ -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 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 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 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 getCustomTicks(); + + /** + * Sets custom ticks with their respective position and label. + * @param positionsAndLabels A map of custom tick positions and labels. + */ + void setCustomTicks(Map 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); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/LinearRenderer2D.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/LinearRenderer2D.java new file mode 100644 index 0000000..33659bd --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/LinearRenderer2D.java @@ -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 ticks, Axis axis, double min, + double max, Set 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); + } + } + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/LogarithmicRenderer2D.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/LogarithmicRenderer2D.java new file mode 100644 index 0000000..741534c --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/LogarithmicRenderer2D.java @@ -0,0 +1,152 @@ +package org.xbib.graphics.graph.gral.plots.axes; + +import java.util.List; + +import org.xbib.graphics.graph.gral.plots.axes.Tick.TickType; +import org.xbib.graphics.graph.gral.util.MathUtils; + +/** + * Class that renders axes with a logarithmic scale in two dimensional space. + */ +public class LogarithmicRenderer2D extends AbstractAxisRenderer2D { + + /** + * Creates a new renderer for logarithmic scaled axes in two-dimensional + * space. + */ + public LogarithmicRenderer2D() { + } + + /** + * 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) { + checkAxisBounds(axis); + 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(); + } + } + double minLog = (min > 0.0) ? Math.log10(min) : 0.0; + double maxLog = (max > 0.0) ? Math.log10(max) : 1.0; + return (Math.log10(val) - minLog)*getShapeLength() / + (maxLog - minLog); + } + + /** + * 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) { + checkAxisBounds(axis); + double min = axis.getMin().doubleValue(); + double max = axis.getMax().doubleValue(); + if (!extrapolate) { + if (value <= 0.0) { + return min; + } + if (value >= getShapeLength()) { + return max; + } + } + double minLog = (min > 0.0) ? Math.log10(min) : 0.0; + double maxLog = (max > 0.0) ? Math.log10(max) : 1.0; + return Math.pow(10.0, + value*(maxLog - minLog)/getShapeLength() + minLog); + } + + @Override + public List getTicks(Axis axis) { + checkAxisBounds(axis); + return super.getTicks(axis); + } + + @Override + protected void createTicks(List ticks, Axis axis, + double min, double max, java.util.Set tickPositions, + boolean isAutoSpacing) { + double tickSpacing = 1.0; + if (isAutoSpacing) { + // TODO Automatic scaling for logarithmic axes + tickSpacing = 1.0; + } else { + tickSpacing = getTickSpacing().doubleValue(); + } + + int ticksMinorCount = getMinorTicksCount(); + double tickSpacingMinor = (ticksMinorCount > 0) + ? tickSpacing/(ticksMinorCount + 1) : tickSpacing; + + // TODO Check if this is a valid solution to allow zeroes + if (min == 0.0) { + min = 1.0; + } + + final double BASE = 10.0; + double powerMin = MathUtils.magnitude(BASE, min); + double powerMax = MathUtils.magnitude(BASE, max); + double minTickMajor = MathUtils.ceil(min, powerMin*tickSpacing); + + int ticksPerPower = (int) Math.floor(BASE/tickSpacingMinor); + int initialTicksMinor = (int) Math.floor((minTickMajor - min) / + (powerMin*tickSpacingMinor)); + + // Add major ticks + int i = 0; + for (double power = powerMin; power <= powerMax; power *= BASE) { + double multipliedTickSpacingMinor = power*tickSpacingMinor; + double minTick = MathUtils.ceil(power, multipliedTickSpacingMinor); + + for (int pi = 0; pi < ticksPerPower; pi++) { + double tickPositionWorld = + minTick + pi*multipliedTickSpacingMinor; + if (tickPositionWorld < min) { + continue; + } else if (tickPositionWorld > max) { + break; + } + TickType tickType = TickType.MINOR; + if ((i++ - initialTicksMinor) % (ticksMinorCount + 1) == 0) { + tickType = TickType.MAJOR; + } + Tick tick = getTick(tickType, axis, tickPositionWorld); + if (tick.position != null + && !tickPositions.contains(tickPositionWorld)) { + ticks.add(tick); + tickPositions.add(tickPositionWorld); + } + } + } + } + + /** + * Utility method that makes sure that axis bounds comply to rules of + * logarithmic axes. + * @param axis Axis to be checked + */ + private static void checkAxisBounds(Axis axis) { + if ((axis.getMin().doubleValue() < 0.0) + || (axis.getMax().doubleValue() < 0.0)) { + throw new IllegalStateException( + "Axis bounds must be greater than or equal to zero for logarithmic axes."); //$NON-NLS-1$ + } + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/Tick.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/Tick.java new file mode 100644 index 0000000..f6170af --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/axes/Tick.java @@ -0,0 +1,53 @@ +package org.xbib.graphics.graph.gral.plots.axes; + +import java.awt.Shape; + +import org.xbib.graphics.graph.gral.graphics.Drawable; +import org.xbib.graphics.graph.gral.plots.DataPoint; +import org.xbib.graphics.graph.gral.util.PointND; + +/** + * Class for storing the tick mark of an axis. + */ +public class Tick extends DataPoint { + /** Type of tick mark. */ + public enum TickType { + /** Major tick mark. */ + MAJOR, + /** Minor tick mark. */ + MINOR, + /** User-defined tick mark. */ + CUSTOM + } + + /** The type of tick mark (major/minor/custom). */ + public final TickType type; + /** The normal of the tick mark. */ + public final PointND normal; + /** Drawable that will be used to render the tick. */ + public final Drawable drawable; + /** Shape describing the tick. */ + public final Shape shape; + /** Label text associated with this tick mark. */ + public final String label; + + /** + * Creates a new instance with the specified position, normal, + * {@code Drawable}, point and label. + * @param type Type of the tick mark. + * @param position Coordinates. + * @param normal Normal. + * @param drawable Representation. + * @param point Point. + * @param label Description. + */ + public Tick(TickType type, PointND position, PointND normal, + Drawable drawable, Shape point, String label) { + super(null, position); + this.type = type; + this.normal = normal; + this.drawable = drawable; + this.shape = point; + this.label = label; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/AbstractColorMapper.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/AbstractColorMapper.java new file mode 100644 index 0000000..91af5dd --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/AbstractColorMapper.java @@ -0,0 +1,47 @@ +package org.xbib.graphics.graph.gral.plots.colors; + +/** + * Interface that maps numbers to Paint objects. This can be used to generate + * colors or gradients for various elements in a plot, e.g. lines, areas, etc. + * + * @param Data type of input values. + */ +public abstract class AbstractColorMapper + implements ColorMapper { + + /** Handling of values that are outside the mapping range. */ + private Mode mode; + + /** + * Initializes a new instance with default values. + */ + public AbstractColorMapper() { + mode = Mode.REPEAT; + } + + /** + * Returns how values outside of the mapping range will be handled. + * @return Handling of values outside of the mapping range. + */ + public Mode getMode() { + return mode; + } + + /** + * Sets how values outside of the mapping range will be handled. + * @param mode Handling of values outside of the mapping range. + */ + protected void setMode(Mode mode) { + this.mode = mode; + } + + /** + * Transforms a value outside of the mapping range. If the value is inside + * the range, no transformation will be applied. + * @param value Value to be handled. + * @param rangeMin Lower bounds of range + * @param rangeMax Upper bounds of range + * @return Transformed value. + */ + protected abstract T applyMode(T value, T rangeMin, T rangeMax); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/ColorMapper.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/ColorMapper.java new file mode 100644 index 0000000..b7bc294 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/ColorMapper.java @@ -0,0 +1,37 @@ +package org.xbib.graphics.graph.gral.plots.colors; + +import java.awt.Paint; + +/** + * Basic interface for classes that map numbers to Paint objects. This can be + * used to generate colors or gradients for various elements in a plot, e.g. + * points, lines, areas, etc. + * + * {@link ContinuousColorMapper} or {@link IndexedColorMapper} should be used + * as base classes in most cases. + */ +public interface ColorMapper { + /** Data type to define how values outside of the mapping range will be + handled. */ + enum Mode { + /** Ignore missing values. */ + OMIT, + /** Repeat the last value. */ + REPEAT, + /** Repeat the data. */ + CIRCULAR + } + + /** + * Returns the Paint object according to the specified value. + * @param value Numeric value. + * @return Paint object. + */ + Paint get(Number value); + + /** + * Returns how values outside of the mapping range will be handled. + * @return Handling of values outside of the mapping range. + */ + Mode getMode(); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/ContinuousColorMapper.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/ContinuousColorMapper.java new file mode 100644 index 0000000..22f2d05 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/ContinuousColorMapper.java @@ -0,0 +1,49 @@ +package org.xbib.graphics.graph.gral.plots.colors; + +import java.awt.Paint; + +import org.xbib.graphics.graph.gral.util.MathUtils; + +/** + * Class that maps floating point numbers to Paint objects. This can be used to + * generate colors or gradients for various elements in a plot, e.g. lines, + * areas, etc. + */ +public abstract class ContinuousColorMapper extends AbstractColorMapper { + + /** + * Returns the Paint object according to the specified value. + * @param value Numeric value. + * @return Paint object. + */ + public abstract Paint get(double value); + + /** + * Returns the Paint object according to the specified value. The specified + * value will be handled like a double value. + * @param value Numeric value object. + * @return Paint object. + */ + public Paint get(Number value) { + return get(value.doubleValue()); + } + + @Override + protected Double applyMode(Double value, Double rangeMin, Double rangeMax) { + if (value >= rangeMin && value <= rangeMax) { + return value; + } + Mode mode = getMode(); + if (mode == Mode.REPEAT) { + return MathUtils.limit(value, rangeMin, rangeMax); + } else if (mode == Mode.CIRCULAR) { + double range = rangeMax - rangeMin; + double i = value%range; + if (i < 0.0) { + i += range; + } + return i + rangeMin; + } + return null; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/Grayscale.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/Grayscale.java new file mode 100644 index 0000000..8f2452b --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/Grayscale.java @@ -0,0 +1,39 @@ +package org.xbib.graphics.graph.gral.plots.colors; + +import java.awt.Color; +import java.awt.Paint; + +import org.xbib.graphics.graph.gral.util.GraphicsUtils; +import org.xbib.graphics.graph.gral.util.MathUtils; + +/** + * Class that generates shades of gray for values between 0.0 and 1.0. + */ +public class Grayscale extends ScaledContinuousColorMapper { + + /** + * Returns the Paint object according to the specified value. + * @param value Value of color. + * @return Paint object. + */ + @Override + public Paint get(double value) { + Double v = scale(value); + v = applyMode(v, 0.0, 1.0); + if (!MathUtils.isCalculatable(v)) { + return null; + } + double lightness = 100.0*v; + double[] rgb = GraphicsUtils.luv2rgb(new double[] {lightness, 0.0, 0.0}, null); + return new Color( + (float) MathUtils.limit(rgb[0], 0.0, 1.0), + (float) MathUtils.limit(rgb[1], 0.0, 1.0), + (float) MathUtils.limit(rgb[2], 0.0, 1.0) + ); + } + + @Override + public void setMode(Mode mode) { + super.setMode(mode); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/HeatMap.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/HeatMap.java new file mode 100644 index 0000000..a514d28 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/HeatMap.java @@ -0,0 +1,68 @@ +package org.xbib.graphics.graph.gral.plots.colors; + +import java.awt.Color; +import java.awt.Paint; + +import org.xbib.graphics.graph.gral.util.MathUtils; + +/** + * Class that generates different color shades for values between 0.0 and 1.0. + */ +public class HeatMap extends ScaledContinuousColorMapper { + + private static final Color[] COLORS = { + new Color(0.0f, 0.0f, 0.0f), + new Color(0.0f, 0.0f, 1.0f), + new Color(1.0f, 0.0f, 0.0f), + new Color(1.0f, 1.0f, 0.0f), + new Color(1.0f, 1.0f, 1.0f) + }; + + /** + * Returns the Paint according to the specified value. + * @param value Value of color. + * @return Paint. + */ + @Override + public Paint get(double value) { + Double v = scale(value); + v = applyMode(v, 0.0, 1.0); + if (!MathUtils.isCalculatable(v)) { + return null; + } + + double x = v; + double xInv = 1.0 - x; + double xInv2 = xInv*xInv; + double x2 = x*x; + + // Bernstein coefficients + double[] coeffs = { + xInv2*xInv2, + 4.0*x*xInv2*xInv, + 6.0*x2*xInv2, + 4.0*x*x2*xInv, + x2*x2 + }; + + double r = 0.0, g = 0.0, b = 0.0, a = 0.0; + for (int i = 0; i < COLORS.length; i++) { + r += coeffs[i]*COLORS[i].getRed(); + g += coeffs[i]*COLORS[i].getGreen(); + b += coeffs[i]*COLORS[i].getBlue(); + a += coeffs[i]*COLORS[i].getAlpha(); + } + + return new Color( + (float) MathUtils.limit(r, 0.0, 255.0)/255f, + (float) MathUtils.limit(g, 0.0, 255.0)/255f, + (float) MathUtils.limit(b, 0.0, 255.0)/255f, + (float) MathUtils.limit(a, 0.0, 255.0)/255f + ); + } + + @Override + public void setMode(Mode mode) { + super.setMode(mode); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/IndexedColorMapper.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/IndexedColorMapper.java new file mode 100644 index 0000000..acd097e --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/IndexedColorMapper.java @@ -0,0 +1,50 @@ +package org.xbib.graphics.graph.gral.plots.colors; + +import java.awt.Paint; + +import org.xbib.graphics.graph.gral.util.MathUtils; + +/** + * Class that maps integer numbers to Paint objects. This can be used to + * generate colors or gradients for various elements in a plot, e.g. lines, + * areas, etc. + */ +public abstract class IndexedColorMapper + extends AbstractColorMapper { + + /** + * Returns the Paint object according to the specified index. + * @param value Numeric index. + * @return Paint object. + */ + public abstract Paint get(int value); + + /** + * Returns the Paint object according to the specified index. The specified + * value will be handled like an integer index. + * @param index Numeric index object. + * @return Paint object. + */ + public Paint get(Number index) { + return get(index.intValue()); + } + + @Override + protected Integer applyMode(Integer index, Integer rangeMin, Integer rangeMax) { + if (index >= rangeMin && index <= rangeMax) { + return index; + } + Mode mode = getMode(); + if (mode == Mode.REPEAT) { + return MathUtils.limit(index, rangeMin, rangeMax); + } else if (mode == Mode.CIRCULAR) { + int range = rangeMax - rangeMin + 1; + int i = index%range; + if (i < 0) { + i += range; + } + return i + rangeMin; + } + return null; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/IndexedColors.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/IndexedColors.java new file mode 100644 index 0000000..c989d6d --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/IndexedColors.java @@ -0,0 +1,57 @@ +package org.xbib.graphics.graph.gral.plots.colors; + +import java.awt.Color; +import java.awt.Paint; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.xbib.graphics.graph.gral.util.MathUtils; + +/** + * Maps index values to a specified color palette. + */ +public class IndexedColors extends IndexedColorMapper { + + /** Color palette that will be used for mapping. **/ + private final List colors; + + /** + * Creates a new instance with at least one color. + * @param color1 First color. + * @param colors Additional colors. + */ + public IndexedColors(Color color1, Color... colors) { + this.colors = new ArrayList<>(); + this.colors.add(color1); + this.colors.addAll(Arrays.asList(colors)); + } + + /** + * Returns the Paint object associated to the specified index value. + * @param index Numeric index. + * @return Paint object. + */ + @Override + public Paint get(int index) { + Integer i = applyMode(index, 0, colors.size() - 1); + if (!MathUtils.isCalculatable(i)) { + return null; + } + return colors.get(i); + } + + /** + * Returns the colors that are used for mapping. + * @return A list of colors in the order they are used as the color palette. + */ + public List getColors() { + return Collections.unmodifiableList(colors); + } + + @Override + public void setMode(org.xbib.graphics.graph.gral.plots.colors.ColorMapper.Mode mode) { + super.setMode(mode); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/LinearGradient.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/LinearGradient.java new file mode 100644 index 0000000..665e297 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/LinearGradient.java @@ -0,0 +1,90 @@ +package org.xbib.graphics.graph.gral.plots.colors; + +import java.awt.Color; +import java.awt.Paint; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.xbib.graphics.graph.gral.util.MathUtils; + +/** + * Linearly blends different colors for values between 0.0 and 1.0. + */ +public class LinearGradient extends ScaledContinuousColorMapper { + + /** Colors that will be used for blending. **/ + private final List colors; + + /** + * Creates a new instance with at least one color. + * @param color1 First color. + * @param colors Additional colors. + */ + public LinearGradient(Color color1, Color... colors) { + this.colors = new ArrayList<>(); + this.colors.add(color1); + this.colors.addAll(Arrays.asList(colors)); + } + + /** + * Returns the Paint according to the specified value. + * @param value Value of color. + * @return Paint. + */ + @Override + public Paint get(double value) { + Double v = scale(value); + v = applyMode(v, 0.0, 1.0); + if (!MathUtils.isCalculatable(v)) { + return null; + } + + double x = v; + int colorMax = colors.size() - 1; + double pos = MathUtils.limit(x*colorMax, 0.0, colorMax); + + if (pos == 0.0) { + return colors.get(0); + } + if (pos == colorMax) { + return colors.get(colorMax); + } + + double fract = pos - (int) pos; + Color color1 = colors.get((int) pos); + + if (fract == 0.0) { + return color1; + } + + double fractInv = 1.0 - fract; + Color color2 = colors.get((int) pos + 1); + + double r = fractInv*color1.getRed() + fract*color2.getRed(); + double g = fractInv*color1.getGreen() + fract*color2.getGreen(); + double b = fractInv*color1.getBlue() + fract*color2.getBlue(); + double a = fractInv*color1.getAlpha() + fract*color2.getAlpha(); + + return new Color( + (int) Math.round(r), + (int) Math.round(g), + (int) Math.round(b), + (int) Math.round(a) + ); + } + + @Override + public void setMode(Mode mode) { + super.setMode(mode); + } + + /** + * Returns the colors that are used for blending. + * @return A list of colors in the order they will be used for blending. + */ + public List getColors() { + return Collections.unmodifiableList(colors); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/QuasiRandomColors.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/QuasiRandomColors.java new file mode 100644 index 0000000..9f6f31f --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/QuasiRandomColors.java @@ -0,0 +1,80 @@ +package org.xbib.graphics.graph.gral.plots.colors; + +import java.awt.Color; +import java.awt.Paint; +import java.util.HashMap; +import java.util.Map; + +import org.xbib.graphics.graph.gral.util.HaltonSequence; +import org.xbib.graphics.graph.gral.util.MathUtils; + +/** + * Class that generates seemingly random colors for specified index values. + */ +public class QuasiRandomColors extends IndexedColorMapper { + + /** Object for mapping a plot value to a hue. */ + private final HaltonSequence seqHue = new HaltonSequence(3); + /** Object for mapping a plot value to a saturation. */ + private final HaltonSequence seqSat = new HaltonSequence(5); + /** Object for mapping a plot value to a brightness. */ + private final HaltonSequence seqBrightness = new HaltonSequence(2); + /** Cache for colors that have already been generated. */ + private final Map colorCache; + /** Variance settings for hue, saturation and brightness. */ + //FIXME duplicate code! See RandomColors + private float[] colorVariance; + + /** + * Creates a new QuasiRandomColors object with default color variance. + */ + public QuasiRandomColors() { + colorCache = new HashMap<>(); + colorVariance = new float[] { + 0.00f, 1.00f, // Hue + 0.75f, 0.25f, // Saturation + 0.25f, 0.75f // Brightness + }; + } + + /** + * Returns the Paint associated to the specified index value. + * @param index Numeric index. + * @return Paint object. + */ + @Override + public Paint get(int index) { + Integer key = index; + if (colorCache.containsKey(key)) { + return colorCache.get(key); + } + float[] colorVariance = getColorVariance(); + float hue = colorVariance[0] + colorVariance[1]*seqHue.next().floatValue(); + float saturation = colorVariance[2] + colorVariance[3]*seqSat.next().floatValue(); + float brightness = colorVariance[4] + colorVariance[5]*seqBrightness.next().floatValue(); + Color color = Color.getHSBColor( + hue, + MathUtils.limit(saturation, 0f, 1f), + MathUtils.limit(brightness, 0f, 1f) + ); + colorCache.put(key, color); + return color; + } + + /** + * Returns the current color variance. + * @return Range of hue, saturation and brightness a color can have. + */ + public float[] getColorVariance() { + return colorVariance; + } + + /** + * Sets the current color variance. + * @param colorVariance Range of hue, saturation and brightness a color + * can have. + */ + public void setColorVariance(float[] colorVariance) { + this.colorVariance = colorVariance; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/RainbowColors.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/RainbowColors.java new file mode 100644 index 0000000..e46ed9b --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/RainbowColors.java @@ -0,0 +1,34 @@ +package org.xbib.graphics.graph.gral.plots.colors; + +import java.awt.Color; +import java.awt.Paint; + +import org.xbib.graphics.graph.gral.util.MathUtils; + +/** + * Class that generates the colors of a rainbow. + */ +public class RainbowColors extends ScaledContinuousColorMapper { + + /** + * Returns the Paint according to the specified value. + * @param value Value of color. + * @return Paint. + */ + @Override + public Paint get(double value) { + Double v = scale(value); + v = applyMode(v, 0.0, 1.0); + if (!MathUtils.isCalculatable(v)) { + return null; + } + + float hue = v.floatValue(); + return Color.getHSBColor(hue, 1f, 1f); + } + + @Override + public void setMode(Mode mode) { + super.setMode(mode); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/RandomColors.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/RandomColors.java new file mode 100644 index 0000000..0b98a1f --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/RandomColors.java @@ -0,0 +1,139 @@ +package org.xbib.graphics.graph.gral.plots.colors; + +import java.awt.Color; +import java.awt.Paint; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Random; + +import org.xbib.graphics.graph.gral.util.MathUtils; + +/** + * Class that generates pseudo-random colors for specified index values. + */ +public class RandomColors extends IndexedColorMapper { + + /** Number of comparisons that will be done before accepting two similar + random values. */ + private static final int NUM_COMPARISONS = 4; + /** Minimal distance the causes a random values to be to re-generated. */ + private static final double MIN_DIST = 0.3; + + /** Cache for colors that have already been generated. */ + private final Map colorCache; + /** Object for generating random values. */ + private final Random random; + /** Variance settings for hue, saturation and brightness. */ + //FIXME duplicate code! See QuasiRandomColors + private final float[] colorVariance; + + /** + * Creates a new RandomColors object with default seed. + */ + public RandomColors() { + random = new Random(); + colorCache = new LinkedHashMap<>(); + colorVariance = new float[] { + 0.00f, 1.00f, // Hue + 0.75f, 0.25f, // Saturation + 0.25f, 0.75f // Brightness + }; + } + + /** + * Creates a new instances with the specified seed. + * @param seed Random number seed. + */ + public RandomColors(long seed) { + this(); + random.setSeed(seed); + } + + /** + * Returns the Paint associated to the specified index value. + * @param index Numeric index. + * @return Paint. + */ + @Override + public Paint get(int index) { + Integer key = index; + if (colorCache.containsKey(key)) { + return colorCache.get(key); + } + + // Use the same random numbers for the same input value + //long seed = Double.doubleToRawLongBits(value); + //random.setSeed(seed); + + // Generate a new color that is distant enough from previous colors + boolean match; + Color r; + do { + r = getRandomColor(); + match = true; + Iterator colors = colorCache.values().iterator(); + for (int i=0; i> 32)); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/SingleColor.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/SingleColor.java new file mode 100644 index 0000000..7f3e420 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/colors/SingleColor.java @@ -0,0 +1,62 @@ +package org.xbib.graphics.graph.gral.plots.colors; + +import java.awt.Paint; + +/** + * Class that represents a ColorMapper with a single color. + */ +public class SingleColor extends IndexedColorMapper { + + /** The color that will be returned in any case. */ + private Paint color; + + /** + * Creates a new instance with the specified color. + * @param color Color to use. + */ + public SingleColor(Paint color) { + this.color = color; + } + + /** + * Returns the Paint according to the specified value. + * @param value Numeric index. + * @return Paint. + */ + @Override + public Paint get(int value) { + return getColor(); + } + + /** + * Returns the color of this ColorMapper. + * @return Color. + */ + public Paint getColor() { + return color; + } + + /** + * Sets the color of this ColorMapper. + * @param color Color to be set. + */ + public void setColor(Paint color) { + this.color = color; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SingleColor)) { + return false; + } + SingleColor cm = (SingleColor) obj; + return color.equals(cm.color) && getMode() == cm.getMode(); + } + + @Override + public int hashCode() { + long bits = getColor().hashCode(); + bits ^= getMode().hashCode() * 31; + return ((int) bits) ^ ((int) (bits >> 32)); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/legends/AbstractLegend.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/legends/AbstractLegend.java new file mode 100644 index 0000000..76aa6f1 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/legends/AbstractLegend.java @@ -0,0 +1,389 @@ +package org.xbib.graphics.graph.gral.plots.legends; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.Paint; +import java.awt.Stroke; +import java.awt.geom.Dimension2D; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.xbib.graphics.graph.gral.data.DataSource; +import org.xbib.graphics.graph.gral.graphics.AbstractDrawable; +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.Insets2D; +import org.xbib.graphics.graph.gral.graphics.Label; +import org.xbib.graphics.graph.gral.graphics.Location; +import org.xbib.graphics.graph.gral.graphics.Orientation; +import org.xbib.graphics.graph.gral.graphics.layout.EdgeLayout; +import org.xbib.graphics.graph.gral.graphics.layout.Layout; +import org.xbib.graphics.graph.gral.graphics.layout.OrientedLayout; +import org.xbib.graphics.graph.gral.graphics.layout.StackedLayout; +import org.xbib.graphics.graph.gral.util.GraphicsUtils; + +/** + *

Abstract class that serves as a base for legends in plots. + * It stores a list of of items that are used to display a symbol and label for + * each (visible) data source.

+ *

Like other elements legends can be styled using various settings. The + * settings are used to control to control how the legend, and its items + * are displayed. The actual rendering of symbols has to be implemented by + * derived classes.

+ */ +public abstract class AbstractLegend extends DrawableContainer + implements Legend { + + /** List of data sources displayed in this legend. */ + private final Set sources; + + /** Default font used for sub-components and the calculation of relative + sizes. */ + private Font baseFont; + /** Paint used to draw the background. */ + private Paint background; + /** Stroke used to draw the border of the legend. */ + // Property will be serialized using a wrapper + private transient Stroke borderStroke; + /** Font used to display the labels. */ + private Font font; + /** Paint used to fill the border of the legend. */ + private Paint borderColor; + /** Direction of the legend's items. */ + private Orientation orientation; + /** Horizontal alignment of the legend relative to the plot area. */ + private double alignmentX; + /** Vertical alignment of the legend relative to the plot area. */ + private double alignmentY; + /** Gap size relative to the font height. */ + private Dimension2D gap; + /** Symbol size relative to the font height. */ + private Dimension2D symbolSize; + + /** + * An abstract base class for drawable symbols. + */ + public static abstract class AbstractSymbol extends AbstractDrawable { + + /** Settings for determining the visual of the symbol. */ + private final Font font; + private final Dimension2D symbolSize; + + /** + * Initializes a new instance. + * @param font Font used to determine the preferred size. + * @param symbolSize Symbol size + */ + public AbstractSymbol(Font font, Dimension2D symbolSize) { + this.font = font; + this.symbolSize = symbolSize; + } + + @Override + public Dimension2D getPreferredSize() { + double fontSize = font.getSize2D(); + Dimension2D size = super.getPreferredSize(); + size.setSize(symbolSize.getWidth()*fontSize, + symbolSize.getHeight()*fontSize); + return size; + } + } + + /** + * Class that displays a specific data source as an item of a legend. + */ + public static class Item extends DrawableContainer { + + /** Default font used for sub-components and the calculation of relative + sizes. */ + private Font baseFont; + /** Symbol that should be drawn. */ + private final Drawable symbol; + /** Label string that should be drawn. */ + private final Label label; + + /** + * Creates a new Item object with the specified data source and text. + * @param symbol Symbol to be displayed. + * @param labelText Description text. + * @param font Font for the description text. + */ + public Item(Drawable symbol, String labelText, Font font) { + double fontSize = font.getSize2D(); + setLayout(new EdgeLayout(fontSize, 0.0)); + + this.symbol = symbol; + add(symbol, Location.WEST); + + label = new Label(labelText); + label.setFont(font); + label.setAlignmentX(0.0); + label.setAlignmentY(0.5); + add(label, Location.CENTER); + } + + public Label getLabel() { + return label; + } + + public Drawable getSymbol() { + return symbol; + } + } + + /** + * Initializes a new instance with a default background color, a border, + * vertical orientation and a gap between the items. The default alignment + * is set to top-left. + */ + public AbstractLegend() { + setInsets(new Insets2D.Double(10.0)); + + sources = new LinkedHashSet<>(); + + background = Color.WHITE; + borderStroke = new BasicStroke(1f); + font = Font.decode(null); + setDrawableFonts(font); + borderColor = Color.BLACK; + orientation = Orientation.VERTICAL; + alignmentX = 0.0; + alignmentY = 0.0; + // TODO: Replace setter call in constructor + setGap(new org.xbib.graphics.graph.gral.graphics.Dimension2D.Double(2.0, 0.5)); + symbolSize = new org.xbib.graphics.graph.gral.graphics.Dimension2D.Double(2.0, 2.0); + setLayout(new StackedLayout(orientation, gap.getWidth(), gap.getHeight())); + refreshLayout(); + } + + /** + * Draws the {@code Drawable} with the specified drawing context. + * @param context Environment used for drawing. + */ + @Override + public void draw(DrawingContext context) { + drawBackground(context); + drawBorder(context); + drawComponents(context); + } + + /** + * Draws the background of this legend with the specified drawing context. + * @param context Environment used for drawing. + */ + protected void drawBackground(DrawingContext context) { + Paint background = getBackground(); + if (background != null) { + GraphicsUtils.fillPaintedShape( + context.getGraphics(), getBounds(), background, null); + } + } + + /** + * Draws the border of this legend with the specified drawing context. + * @param context Environment used for drawing. + */ + protected void drawBorder(DrawingContext context) { + Stroke stroke = getBorderStroke(); + if (stroke != null) { + Paint borderColor = getBorderColor(); + GraphicsUtils.drawPaintedShape( + context.getGraphics(), getBounds(), borderColor, null, stroke); + } + } + + /** + * Adds the specified data source in order to display it. + * @param source data source to be added. + */ + public void add(DataSource source) { + sources.add(source); + } + + /** + * Returns whether the specified data source was added to the legend. + * @param source Data source. + * @return {@code true} if legend contains the data source, + * otherwise {@code false}. + */ + public boolean contains(DataSource source) { + return sources.contains(source); + } + + /** + * Removes the specified data source. + * @param source Data source to be removed. + */ + public void remove(DataSource source) { + sources.remove(source); + } + + /** + * Returns all data sources displayed in this legend. + * @return Displayed data sources. + */ + public Set getSources() { + return Collections.unmodifiableSet(sources); + } + + /** + * Removes all data sources from the legend. + */ + public void clear() { + Set sources = new HashSet<>(this.sources); + for (DataSource source : sources) { + remove(source); + } + } + + /** + * Refreshes the layout of the legend. It's currently used to handle new + * gap values. + */ + protected final void refreshLayout() { + Dimension2D gap = getGap(); + Layout layout = getLayout(); + layout.setGapX(gap.getWidth()); + layout.setGapY(gap.getHeight()); + if (layout instanceof OrientedLayout) { + OrientedLayout orientedLayout = (OrientedLayout) layout; + orientedLayout.setOrientation(getOrientation()); + } + } + + @Override + public void setBounds(double x, double y, double width, double height) { + Dimension2D size = getPreferredSize(); + double alignX = getAlignmentX(); + double alignY = getAlignmentY(); + super.setBounds( + x + alignX*(width - size.getWidth()), + y + alignY*(height - size.getHeight()), + size.getWidth(), + size.getHeight() + ); + } + + /** + * Sets the font of the contained drawables. + * @param font Font to be set. + */ + protected final void setDrawableFonts(Font font) { + for (Drawable drawable : this) { + if (drawable instanceof Item) { + Item item = (Item) drawable; + item.label.setFont(font); + } + } + } + + @Override + public Font getBaseFont() { + return baseFont; + } + + @Override + public void setBaseFont(Font baseFont) { + this.baseFont = baseFont; + } + + @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 borderStroke) { + this.borderStroke = borderStroke; + } + + @Override + public Font getFont() { + return font; + } + + @Override + public void setFont(Font font) { + this.font = font; + setDrawableFonts(font); + } + + @Override + public Paint getBorderColor() { + return borderColor; + } + + @Override + public void setBorderColor(Paint borderColor) { + this.borderColor = borderColor; + } + + @Override + public Orientation getOrientation() { + return orientation; + } + + @Override + public void setOrientation(Orientation orientation) { + this.orientation = orientation; + refreshLayout(); + } + + @Override + public double getAlignmentX() { + return alignmentX; + } + + @Override + public void setAlignmentX(double alignmentX) { + this.alignmentX = alignmentX; + } + + @Override + public double getAlignmentY() { + return alignmentY; + } + + @Override + public void setAlignmentY(double alignmentY) { + this.alignmentY = alignmentY; + } + + @Override + public Dimension2D getGap() { + return gap; + } + + @Override + public void setGap(Dimension2D gap) { + this.gap = gap; + if (this.gap != null) { + double fontSize = getFont().getSize2D(); + this.gap.setSize(this.gap.getWidth()*fontSize, this.gap.getHeight()*fontSize); + } + } + + @Override + public Dimension2D getSymbolSize() { + return symbolSize; + } + + @Override + public void setSymbolSize(Dimension2D symbolSize) { + this.symbolSize = symbolSize; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/legends/Legend.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/legends/Legend.java new file mode 100644 index 0000000..436b666 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/legends/Legend.java @@ -0,0 +1,168 @@ +package org.xbib.graphics.graph.gral.plots.legends; + +import java.awt.Font; +import java.awt.Paint; +import java.awt.Stroke; +import java.awt.geom.Dimension2D; + +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.Orientation; + +/** + * Interface for a legend that display visual examples of the variables used in + * a plot. + */ +public interface Legend extends Container, Drawable { + /** + * Adds the specified data source in order to display it. + * @param source data source to be added. + */ + void add(DataSource source); + + /** + * Returns whether the specified data source was added to the legend. + * @param source Data source + * @return {@code true} if legend contains the data source, otherwise {@code false} + */ + boolean contains(DataSource source); + + /** + * Removes the specified data source. + * @param source Data source to be removed. + */ + void remove(DataSource source); + + /** + * Removes all data sources from the legend. + */ + void clear(); + + /** + * Returns the current font used as a default for sub-components ans for + * calculation of relative sizes. + * @return Current base font. + */ + Font getBaseFont(); + + /** + * 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. + */ + void setBaseFont(Font baseFont); + + /** + * Returns the paint used to draw the background. + * @return Paint used for background drawing. + */ + Paint getBackground(); + + /** + * Sets the paint used to draw the background. + * @param background Paint used for background drawing. + */ + void setBackground(Paint background); + + /** + * Returns the stroke used to draw the border of the legend. + * @return Stroke used for border drawing. + */ + Stroke getBorderStroke(); + + /** + * Sets the stroke used to draw the border of the legend. + * @param borderStroke Stroke used for border drawing. + */ + void setBorderStroke(Stroke borderStroke); + + /** + * Returns the font used to display the labels. + * @return Font used for labels. + */ + Font getFont(); + + /** + * Sets the font used to display the labels. + * @param font Font used for labels. + */ + void setFont(Font font); + + /** + * Returns the paint used to fill the border of the legend. + * @return Paint used for border drawing. + */ + Paint getBorderColor(); + + /** + * Sets the paint used to fill the border of the legend. + * @param borderColor Paint used for border drawing. + */ + void setBorderColor(Paint borderColor); + + /** + * Returns the direction of the legend's items. + * @return Item orientation. + */ + Orientation getOrientation(); + + /** + * Sets the direction of the legend's items. + * @param orientation Item orientation. + */ + void setOrientation(Orientation orientation); + + /** + * Returns the size of the legend's symbols. + * @return Symbol size relative to the font height. + */ + Dimension2D getSymbolSize(); + + /** + * Sets the size of the legend's symbols. + * @param symbolSize Symbol size relative to the font height. + */ + void setSymbolSize(Dimension2D symbolSize); + + /** + * Returns the horizontal alignment of the legend relative to the plot area. + * {@code 0.0} means left, {@code 0.5} means centered, and {@code 1.0} means right. + * @return Relative horizontal alignment. + */ + double getAlignmentX(); + + /** + * Sets the horizontal alignment of the legend relative to the plot area. + * {@code 0.0} means left, {@code 0.5} means centered, and {@code 1.0} means right. + * @param alignmentX Relative horizontal alignment. + */ + void setAlignmentX(double alignmentX); + + /** + * Returns the vertical alignment of the legend relative to the plot area. + * {@code 0.0} means top, {@code 0.5} means centered, and {@code 1.0} means bottom. + * @return Relative vertical alignment. + */ + double getAlignmentY(); + + /** + * Sets the vertical alignment of the legend relative to the plot area. + * {@code 0.0} means top, {@code 0.5} means centered, and {@code 1.0} means bottom. + * @param alignmentY Relative vertical alignment. + */ + void setAlignmentY(double alignmentY); + + /** + * Returns the horizontal and vertical gap between items. + * @return Gap size relative to the font height. + */ + Dimension2D getGap(); + + /** + * Sets the horizontal and vertical gap between items. + * @param gap Gap size relative to the font height. + */ + void setGap(Dimension2D gap); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/legends/SeriesLegend.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/legends/SeriesLegend.java new file mode 100644 index 0000000..3211cbc --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/legends/SeriesLegend.java @@ -0,0 +1,57 @@ +package org.xbib.graphics.graph.gral.plots.legends; + +import java.awt.Font; +import java.util.HashMap; +import java.util.Map; + +import org.xbib.graphics.graph.gral.data.DataSource; +import org.xbib.graphics.graph.gral.graphics.Drawable; + +/** + * A legend implementation that displays an item for each data series that are + * added to the legend. + */ +public abstract class SeriesLegend extends AbstractLegend { + + /** Mapping of data rows to drawable components. */ + private final Map drawableByDataSource; + + public SeriesLegend() { + drawableByDataSource = new HashMap<>(); + } + + @Override + public void add(DataSource source) { + super.add(source); + String label = getLabel(source); + Font font = getFont(); + Item item = new Item(getSymbol(source), label, font); + add(item); + drawableByDataSource.put(source, item); + } + + @Override + public void remove(DataSource source) { + super.remove(source); + Drawable drawable = drawableByDataSource.remove(source); + if (drawable != null) { + remove(drawable); + } + } + + /** + * Returns the label text for the specified data source. + * @param data Data source. + * @return Label text. + */ + protected String getLabel(DataSource data) { + return data.getName(); + } + + /** + * Returns a symbol for rendering a legend item. + * @param data Data source. + * @return A drawable object that can be used to display the symbol. + */ + protected abstract Drawable getSymbol(DataSource data); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/legends/ValueLegend.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/legends/ValueLegend.java new file mode 100644 index 0000000..e60db65 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/legends/ValueLegend.java @@ -0,0 +1,206 @@ +package org.xbib.graphics.graph.gral.plots.legends; + +import java.awt.Font; +import java.text.Format; +import java.text.NumberFormat; +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.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.Row; +import org.xbib.graphics.graph.gral.graphics.Drawable; + +/** + * A legend implementation that displays items for all data values of all data + * series that are added to the legend. + */ +public abstract class ValueLegend extends AbstractLegend + implements DataListener { + + /** Mapping of data rows to drawable components. */ + private final Map components; + /** Column index containing the labels. */ + private int labelColumn; + /** Format for data to label text conversion. */ + private Format labelFormat; + + /** + * Initializes a new instance with default values. + */ + public ValueLegend() { + components = new HashMap<>(); + labelColumn = 0; + } + + /** + * Returns a sequence of items for the specified data source that should be + * added to the legend. + * @param source Data source. + * @return A sequence of items for the specified data source. + */ + protected Iterable getEntries(DataSource source) { + List items = new LinkedList<>(); + for (int rowIndex = 0; rowIndex < source.getRowCount(); rowIndex++) { + Row row = new Row(source, rowIndex); + items.add(row); + } + return items; + } + + /** + * Returns the label text for the specified row. + * @param row Data row. + * @return Label text. + */ + protected String getLabel(Row row) { + int col = getLabelColumn(); + Comparable value = row.get(col); + if (value == null) { + return ""; + } + + // Formatting + Format format = getLabelFormat(); + if ((format == null) && row.isColumnNumeric(col)) { + format = NumberFormat.getInstance(); + } + + // Text to display + return (format != null) ? format.format(value) : value.toString(); + } + + @Override + public void add(DataSource source) { + super.add(source); + refresh(); + source.addDataListener(this); + } + + @Override + public void remove(DataSource source) { + super.remove(source); + Set rows = new HashSet<>(components.keySet()); + for (Row row : rows) { + if (row.getSource() != source) { + continue; + } + Drawable item = components.remove(row); + if (item != null) { + remove(item); + } + } + refresh(); + source.removeDataListener(this); + } + + /** + * 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. + * 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) { + refresh(); + } + + /** + * Returns the index of the column that contains the labels for the values. + * @return Column index containing the labels. + */ + public int getLabelColumn() { + return labelColumn; + } + + /** + * Sets the index of the column that contains the labels for the values. + * @param labelColumn Column index containing the labels. + */ + public void setLabelColumn(int labelColumn) { + this.labelColumn = labelColumn; + refresh(); + } + + /** + * Returns the format used to display data values. + * @return Format for data to label text conversion. + */ + public Format getLabelFormat() { + return labelFormat; + } + + /** + * Sets the format used to display data values. + * @param labelFormat Format for data to label text conversion. + */ + public void setLabelFormat(Format labelFormat) { + this.labelFormat = labelFormat; + refresh(); + } + + /** + * Returns a symbol for rendering a legend item. + * @param row Data row. + * @return A drawable object that can be used to display the symbol. + */ + protected abstract Drawable getSymbol(Row row); + + private void refresh() { + for (Drawable drawable : components.values()) { + remove(drawable); + } + components.clear(); + for (DataSource source : getSources()) { + for (Row row : getEntries(source)) { + String label = getLabel(row); + Font font = getFont(); + Item item = new Item(getSymbol(row), label, font); + add(item); + components.put(row, item); + } + } + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/lines/AbstractLineRenderer2D.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/lines/AbstractLineRenderer2D.java new file mode 100644 index 0000000..55aef28 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/lines/AbstractLineRenderer2D.java @@ -0,0 +1,93 @@ +package org.xbib.graphics.graph.gral.plots.lines; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Paint; +import java.awt.Shape; +import java.awt.Stroke; + + +/** + *

Abstract class that renders a line in two-dimensional space.

+ *

Functionality includes:

+ *
    + *
  • Punching data points out of the line's shape
  • + *
  • Administration of settings
  • + *
+ */ +public abstract class AbstractLineRenderer2D implements LineRenderer { + + /** Stroke to draw the line. */ + private transient Stroke stroke; + /** Gap between points and the line. */ + private double gap; + /** Decides whether the shape of the gap between points and the line is + * rounded. */ + private boolean gapRounded; + /** Paint to fill the line. */ + private Paint color; + + /** + * Initializes a new {@code AbstractLineRenderer2D} instance with + * default settings. + */ + public AbstractLineRenderer2D() { + stroke = new BasicStroke(1.5f); + gap = 0.0; + gapRounded = false; + color = Color.BLACK; + } + + /** + * Returns the stroked shape of the specified line. + * @param line Shape of the line. + * @return Stroked shape. + */ + protected Shape stroke(Shape line) { + if (line == null) { + return null; + } + Stroke stroke = getStroke(); + return stroke.createStrokedShape(line); + } + + @Override + public Stroke getStroke() { + return stroke; + } + + @Override + public void setStroke(Stroke stroke) { + this.stroke = stroke; + } + + @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; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/lines/DefaultLineRenderer2D.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/lines/DefaultLineRenderer2D.java new file mode 100644 index 0000000..c0f3ccc --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/lines/DefaultLineRenderer2D.java @@ -0,0 +1,71 @@ +package org.xbib.graphics.graph.gral.plots.lines; + +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.util.GraphicsUtils; + +/** + * Class that connects two dimensional data points with a straight line. + */ +public class DefaultLineRenderer2D extends AbstractLineRenderer2D { + /** Number of line segments which will be reserved to avoid unnecessary + copying of array data. */ + private static final int INITIAL_LINE_CAPACITY = 10000; + + /** + * Initializes a new {@code DefaultLineRenderer2D} instance. + */ + public DefaultLineRenderer2D() { + } + + /** + * Returns a graphical representation for the line defined by + * {@code e points}. + * @param points Points used for creating the line. + * @param shape Geometric shape for this line. + * @return Representation of the line. + */ + public Drawable getLine(final List 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) { + // Draw line + Paint paint = DefaultLineRenderer2D.this.getColor(); + GraphicsUtils.fillPaintedShape( + context.getGraphics(), shape, paint, null); + } + }; + } + + /** + * Returns the geometric shape for this line. + * @param points Points used for creating the line. + * @return Geometric shape for this line. + */ + public Shape getLineShape(List points) { + // Construct shape + Path2D shape = new Path2D.Double( + Path2D.WIND_NON_ZERO, INITIAL_LINE_CAPACITY); + for (DataPoint point : points) { + Point2D pos = point.position.getPoint2D(); + if (shape.getCurrentPoint() == null) { + shape.moveTo(pos.getX(), pos.getY()); + } else { + shape.lineTo(pos.getX(), pos.getY()); + } + } + return stroke(shape); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/lines/DiscreteLineRenderer2D.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/lines/DiscreteLineRenderer2D.java new file mode 100644 index 0000000..d323525 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/lines/DiscreteLineRenderer2D.java @@ -0,0 +1,124 @@ +package org.xbib.graphics.graph.gral.plots.lines; + +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.util.GraphicsUtils; +import org.xbib.graphics.graph.gral.graphics.Orientation; + +/** + * Class that connects {@code DataPoint}s with a stair-like line. + */ +public class DiscreteLineRenderer2D extends AbstractLineRenderer2D { + + /** Primary direction of the "steps". */ + private Orientation ascentDirection; + /** Relative distance between points, where the orientation changes. */ + private Number ascendingPoint; + + /** + * Initializes a new {@code DiscreteLineRenderer2D} instance with default + * settings. + */ + public DiscreteLineRenderer2D() { + ascentDirection = Orientation.HORIZONTAL; + ascendingPoint = 0.5; + } + + /** + * Returns a graphical representation for the line defined by + * {@code points}. + * @param points Points to be used for creating the line. + * @param shape Geometric shape for this line. + * @return Representation of the line. + */ + public Drawable getLine(final List 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) { + // Draw path + Paint paint = DiscreteLineRenderer2D.this.getColor(); + GraphicsUtils.fillPaintedShape( + context.getGraphics(), shape, paint, null); + } + }; + } + + /** + * Returns the geometric shape for this line. + * @param points Points used for creating the line. + * @return Geometric shape for this line. + */ + public Shape getLineShape(List points) { + Orientation dir = getAscentDirection(); + double ascendingPoint = getAscendingPoint().doubleValue(); + + // Construct shape + Path2D shape = new Path2D.Double(); + for (DataPoint point : points) { + Point2D pos = point.position.getPoint2D(); + if (shape.getCurrentPoint() == null) { + shape.moveTo(pos.getX(), pos.getY()); + } else { + Point2D posPrev = shape.getCurrentPoint(); + if (dir == Orientation.HORIZONTAL) { + double ascendingX = posPrev.getX() + + (pos.getX() - posPrev.getX()) * ascendingPoint; + shape.lineTo(ascendingX, posPrev.getY()); + shape.lineTo(ascendingX, pos.getY()); + } else { + double ascendingY = posPrev.getY() + + (pos.getY() - posPrev.getY()) * ascendingPoint; + shape.lineTo(posPrev.getX(), ascendingY); + shape.lineTo(pos.getX(), ascendingY); + } + shape.lineTo(pos.getX(), pos.getY()); + } + } + + return stroke(shape); + } + + /** + * Returns the primary direction of the "steps". + * @return Orientation of the "steps". + */ + public Orientation getAscentDirection() { + return ascentDirection; + } + + /** + * Sets the primary direction of the "steps". + * @param ascentDirection Orientation of the "steps". + */ + public void setAscentDirection(Orientation ascentDirection) { + this.ascentDirection = ascentDirection; + } + + /** + * Returns the relative distance between two points, {@literal i.e.} the "step" of a stair. + * @return Relative point distance. + */ + public Number getAscendingPoint() { + return ascendingPoint; + } + + /** + * Sets the relative distance between two points, {@literal i.e.} the "step" of a stair. + * @param ascendingPoint Relative point distance. + */ + public void setAscendingPoint(Number ascendingPoint) { + this.ascendingPoint = ascendingPoint; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/lines/LineRenderer.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/lines/LineRenderer.java new file mode 100644 index 0000000..f0646d8 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/lines/LineRenderer.java @@ -0,0 +1,87 @@ +package org.xbib.graphics.graph.gral.plots.lines; + +import java.awt.Paint; +import java.awt.Shape; +import java.awt.Stroke; +import java.util.List; + +import org.xbib.graphics.graph.gral.graphics.Drawable; +import org.xbib.graphics.graph.gral.plots.DataPoint; + + +/** + *

Interface that provides functions for rendering a line in two dimensional + * space.

+ *

Functionality includes:

+ *
    + *
  • Punching data points out of the line's shape
  • + *
  • Administration of settings
  • + *
+ */ +public interface LineRenderer { + /** + * Returns the geometric shape for this line. + * @param points Points used for creating the line. + * @return Geometric shape for this line. + */ + Shape getLineShape(List points); + + /** + * Returns a graphical representation for the line defined by + * {@code points}. + * @param points Points to be used for creating the line. + * @param shape Geometric shape for this line. + * @return Representation of the line. + */ + Drawable getLine(List points, Shape shape); + + /** + * Returns the stroke to be used to define the line shape. + * @return Stroke used for drawing. + */ + Stroke getStroke(); + + /** + * Sets the stroke to be used to define the line shape. + * @param stroke Stroke used for drawing. + */ + void setStroke(Stroke stroke); + + /** + * Returns the value for the gap between the line and a point. + * If the gap value is equal to or smaller than 0 no gap will be used. + * @return Gap size between drawn line and connected points in pixels. + */ + double getGap(); + + /** + * Sets the value for the gap between the line and a point. + * If the gap value is equal to or smaller than 0 no gap will be used. + * @param gap Gap size between drawn line and connected points in pixels. + */ + void setGap(double gap); + + /** + * Returns whether the gaps should have rounded corners. + * @return {@code true} if the gap corners should be rounded. + */ + boolean isGapRounded(); + + /** + * Sets whether the gaps should have rounded corners. + * @param gapRounded {@code true} if the gap corners should be rounded. + */ + void setGapRounded(boolean gapRounded); + + /** + * Returns the paint to be used to paint the line shape. + * @return Paint for line drawing. + */ + Paint getColor(); + + /** + * Sets the paint to be used to paint the line shape. + * @param color Paint for line drawing. + */ + void setColor(Paint color); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/lines/SmoothLineRenderer2D.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/lines/SmoothLineRenderer2D.java new file mode 100644 index 0000000..b205e1f --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/lines/SmoothLineRenderer2D.java @@ -0,0 +1,193 @@ +package org.xbib.graphics.graph.gral.plots.lines; + +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.util.GraphicsUtils; + +/** + *

Class that connects {@code DataPoint}s with a smooth line.

+ *

See Interpolation + * with Bezier Curves for more information.

+ */ +public class SmoothLineRenderer2D extends AbstractLineRenderer2D { + + /** Degree of "smoothness", where 0.0 means no smoothing, and 1.0 means + * maximal smoothing. */ + private Number smoothness; + + /** + * Initializes a new {@code SmoothLineRenderer2D} instance with + * default settings. + */ + public SmoothLineRenderer2D() { + smoothness = 1.0; + } + + /** + * Returns a graphical representation for the line defined by + * {@code points}. + * @param points Points to be used for creating the line. + * @param shape Geometric shape for this line. + * @return Representation of the line. + */ + public Drawable getLine(final List 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) { + // Draw path + Paint paint = SmoothLineRenderer2D.this.getColor(); + GraphicsUtils.fillPaintedShape( + context.getGraphics(), shape, paint, null); + } + }; + } + + /** + * Returns the geometric shape for this line. + * @param points Points used for creating the line. + * @return Geometric shape for this line. + */ + public Shape getLineShape(List points) { + double smoothness = getSmoothness().doubleValue(); + + // Construct shape + Path2D shape = new Path2D.Double(); + + Point2D p0 = null, p1 = null, p2 = null, p3 = null; + Point2D ctrl1 = new Point2D.Double(); + Point2D ctrl2 = new Point2D.Double(); + for (DataPoint point : points) { + if (point == null) { + continue; + } + p3 = point.position.getPoint2D(); + + addCurve(shape, p0, p1, p2, p3, ctrl1, ctrl2, smoothness); + + p0 = p1; + p1 = p2; + p2 = p3; + } + addCurve(shape, p0, p1, p2, p3, ctrl1, ctrl2, smoothness); + + return stroke(shape); + } + + /** + * Utility method to add a smooth curve segment to a specified line path. + * @param line Line path. + * @param p0 Previous neighbor. + * @param p1 First point. + * @param p2 Second point. + * @param p3 Next neighbor. + * @param ctrl1 First control point. + * @param ctrl2 Second control point. + * @param smoothness Smoothness factor + */ + private static void addCurve(Path2D line, Point2D p0, Point2D p1, + Point2D p2, Point2D p3, Point2D ctrl1, Point2D ctrl2, + double smoothness) { + if (p1 == null ) { + return; + } + if (line.getCurrentPoint() == null) { + line.moveTo(p1.getX(), p1.getY()); + } + if (p2 == null) { + return; + } + getControlsPoints(p0, p1, p2, p3, ctrl1, ctrl2, smoothness); + line.curveTo( + ctrl1.getX(), ctrl1.getY(), + ctrl2.getX(), ctrl2.getY(), + p2.getX(), p2.getY()); + } + + /** + * Set the coordinates of two control points ctrl1 and ctrl2 + * which can be used to draw a smooth Bézier curve through two points + * p1 and p2. To get a smooth curve the two neighboring + * points p0 and p3 are required. However, p0 and + * p3 may also be set to {@code null} in case of end points. + * @param p0 Previous neighbor. + * @param p1 First point. + * @param p2 Second point. + * @param p3 Next neighbor. + * @param ctrl1 First control point. + * @param ctrl2 Second control point. + * @param smoothness Smoothness factor + */ + private static void getControlsPoints(Point2D p0, Point2D p1, Point2D p2, + Point2D p3, Point2D ctrl1, Point2D ctrl2, double smoothness) { + if (p0 == null) { + p0 = p1; + } + if (p3 == null) { + p3 = p2; + } + + Point2D c1 = new Point2D.Double( + (p0.getX() + p1.getX()) / 2.0, + (p0.getY() + p1.getY()) / 2.0); + Point2D c2 = new Point2D.Double( + (p1.getX() + p2.getX()) / 2.0, + (p1.getY() + p2.getY()) / 2.0); + Point2D c3 = new Point2D.Double( + (p2.getX() + p3.getX()) / 2.0, + (p2.getY() + p3.getY()) / 2.0); + + double len1 = p1.distance(p0); + double len2 = p2.distance(p1); + double len3 = p3.distance(p2); + + double k1 = len1 / (len1 + len2); + double k2 = len2 / (len2 + len3); + + Point2D m1 = new Point2D.Double( + c1.getX() + (c2.getX() - c1.getX()) * k1, + c1.getY() + (c2.getY() - c1.getY()) * k1); + Point2D m2 = new Point2D.Double( + c2.getX() + (c3.getX() - c2.getX()) * k2, + c2.getY() + (c3.getY() - c2.getY()) * k2); + + ctrl1.setLocation( + m1.getX() + (c2.getX() - m1.getX()) * smoothness + p1.getX() - m1.getX(), + m1.getY() + (c2.getY() - m1.getY()) * smoothness + p1.getY() - m1.getY() + ); + ctrl2.setLocation( + m2.getX() + (c2.getX() - m2.getX()) * smoothness + p2.getX() - m2.getX(), + m2.getY() + (c2.getY() - m2.getY()) * smoothness + p2.getY() - m2.getY() + ); + } + + /** + * Returns the smoothness of the line. + * The value must be in range 0 (sharpest) to 1 (smoothest). + * @return Line smoothness. + */ + public Number getSmoothness() { + return smoothness; + } + + /** + * Returns the smoothness of the line. + * The value must be in range 0 (sharpest) to 1 (smoothest). + * @param smoothness Line smoothness. + */ + public void setSmoothness(Number smoothness) { + this.smoothness = smoothness; + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/AbstractPointRenderer.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/AbstractPointRenderer.java new file mode 100644 index 0000000..33b872d --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/AbstractPointRenderer.java @@ -0,0 +1,284 @@ +package org.xbib.graphics.graph.gral.plots.points; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.Paint; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.geom.Line2D; +import java.awt.geom.Rectangle2D; +import java.text.Format; + +import org.xbib.graphics.graph.gral.plots.colors.ColorMapper; +import org.xbib.graphics.graph.gral.plots.colors.SingleColor; +import org.xbib.graphics.graph.gral.graphics.Location; + +/** + * Abstract class implementing functions for the administration of settings. + */ +public abstract class AbstractPointRenderer + implements PointRenderer { + + /** Shape to draw for the points. */ + private Shape shape; + /** Color mapping used to fill the points. */ + private ColorMapper color; + + /** Decides whether a value label are drawn at the point. */ + private boolean valueVisible; + /** Index of the column for the value label content. */ + private int valueColumn; + /** Format of the value label content. */ + private Format valueFormat; + /** Position of the value label relative to the point position. */ + private Location valueLocation; + /** Horizontal alignment of the value label. */ + private double valueAlignmentX; + /** Vertical alignment of the value label. */ + private double valueAlignmentY; + /** Rotation angle of the value label in degrees. */ + private double valueRotation; + /** Distance of the value label to the shape of the point. */ + private double valueDistance; + /** Color mapping to fill the value label. */ + private ColorMapper valueColor; + /** Font to draw the value label contents. */ + private Font valueFont; + + /** Decides whether error indicators are drawn for the point. */ + private boolean errorVisible; + /** Index of the column for the upper error bounds. */ + private int errorColumnTop; + /** Index of the column for the lower error bounds. */ + private int errorColumnBottom; + /** Color mapping to fill the error indicators. */ + private ColorMapper errorColor; + /** Shape to draw the error indicators. */ + private Shape errorShape; + /** Stroke to the shapes of the error indicators. */ + private transient Stroke errorStroke; + + /** + * Creates a new AbstractPointRenderer object with default shape and + * color. + */ + public AbstractPointRenderer() { + shape = new Rectangle2D.Double(-2.5, -2.5, 5.0, 5.0); + color = new SingleColor(Color.BLACK); + + valueVisible = false; + valueColumn = 1; + valueLocation = Location.CENTER; + valueAlignmentX = 0.5; + valueAlignmentY = 0.5; + valueRotation = 0.0; + valueDistance = 1.0; + valueColor = new SingleColor(Color.BLACK); + valueFont = Font.decode(null); + + errorVisible = false; + errorColumnTop = 2; + errorColumnBottom = 3; + errorColor = new SingleColor(Color.BLACK); + errorShape = new Line2D.Double(-2.0, 0.0, 2.0, 0.0); + errorStroke = new BasicStroke(1f); + } + + @Override + public Shape getShape() { + return shape; + } + + @Override + public void setShape(Shape shape) { + // TODO Store clone of shape to prevent external modification + this.shape = shape; + } + + @Override + public ColorMapper getColor() { + return color; + } + + @Override + public void setColor(ColorMapper color) { + this.color = color; + } + + @Override + public void setColor(Paint color) { + setColor(new SingleColor(color)); + } + + @Override + public boolean isValueVisible() { + return valueVisible; + } + + @Override + public void setValueVisible(boolean valueVisible) { + this.valueVisible = valueVisible; + } + + @Override + public int getValueColumn() { + return valueColumn; + } + + @Override + public void setValueColumn(int columnIndex) { + this.valueColumn = columnIndex; + } + + @Override + public Format getValueFormat() { + return valueFormat; + } + + @Override + public void setValueFormat(Format format) { + this.valueFormat = format; + } + + @Override + public Location getValueLocation() { + return valueLocation; + } + + @Override + public void setValueLocation(Location location) { + this.valueLocation = location; + } + + @Override + public double getValueAlignmentX() { + return valueAlignmentX; + } + + @Override + public void setValueAlignmentX(double alignmentX) { + this.valueAlignmentX = alignmentX; + } + + @Override + public double getValueAlignmentY() { + return valueAlignmentY; + } + + @Override + public void setValueAlignmentY(double alignmentY) { + this.valueAlignmentY = alignmentY; + } + + @Override + public double getValueRotation() { + return valueRotation; + } + + @Override + public void setValueRotation(double angle) { + this.valueRotation = angle; + } + + @Override + public double getValueDistance() { + return valueDistance; + } + + @Override + public void setValueDistance(double distance) { + this.valueDistance = distance; + } + + @Override + public ColorMapper getValueColor() { + return valueColor; + } + + @Override + public void setValueColor(ColorMapper color) { + this.valueColor = color; + } + + @Override + public void setValueColor(Paint color) { + setValueColor(new SingleColor(color)); + } + + @Override + public Font getValueFont() { + return valueFont; + } + + @Override + public void setValueFont(Font font) { + this.valueFont = font; + } + + @Override + public boolean isErrorVisible() { + return errorVisible; + } + + @Override + public void setErrorVisible(boolean errorVisible) { + this.errorVisible = errorVisible; + } + + @Override + public int getErrorColumnTop() { + return errorColumnTop; + } + + @Override + public void setErrorColumnTop(int columnIndex) { + this.errorColumnTop = columnIndex; + } + + @Override + public int getErrorColumnBottom() { + return errorColumnBottom; + } + + @Override + public void setErrorColumnBottom(int columnIndex) { + this.errorColumnBottom = columnIndex; + } + + @Override + public ColorMapper getErrorColor() { + return errorColor; + } + + @Override + public void setErrorColor(ColorMapper color) { + this.errorColor = color; + } + + @Override + public void setErrorColor(Paint color) { + setErrorColor(new SingleColor(color)); + } + + @Override + public Shape getErrorShape() { + return errorShape; + } + + @Override + public void setErrorShape(Shape shape) { + // TODO Store clone of shape to prevent external modification + this.errorShape = shape; + } + + @Override + public Stroke getErrorStroke() { + return errorStroke; + } + + @Override + public void setErrorStroke(Stroke stroke) { + this.errorStroke = stroke; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/DefaultPointRenderer2D.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/DefaultPointRenderer2D.java new file mode 100644 index 0000000..2c64eb3 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/DefaultPointRenderer2D.java @@ -0,0 +1,226 @@ +package org.xbib.graphics.graph.gral.plots.points; + +import java.awt.BasicStroke; +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.Line2D; +import java.awt.geom.Rectangle2D; +import java.text.Format; +import java.text.NumberFormat; + +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.DrawableContainer; +import org.xbib.graphics.graph.gral.graphics.DrawingContext; +import org.xbib.graphics.graph.gral.graphics.layout.OuterEdgeLayout; +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.colors.ColorMapper; +import org.xbib.graphics.graph.gral.util.GraphicsUtils; +import org.xbib.graphics.graph.gral.graphics.Location; +import org.xbib.graphics.graph.gral.util.MathUtils; +import org.xbib.graphics.graph.gral.util.PointND; + +/** + * Class that creates {@code Drawable}s for a row of data. + */ +public class DefaultPointRenderer2D extends AbstractPointRenderer { + + @Override + public Drawable getPoint(final PointData data, final Shape shape) { + return new AbstractDrawable() { + + public void draw(DrawingContext context) { + PointRenderer renderer = DefaultPointRenderer2D.this; + + Axis axisY = data.axes.get(1); + AxisRenderer axisRendererY = data.axisRenderers.get(1); + Row row = data.row; + int col = data.col; + + ColorMapper colors = getColor(); + Paint paint = colors.get(data.index); + + GraphicsUtils.fillPaintedShape( + context.getGraphics(), shape, paint, null); + + if (renderer.isErrorVisible()) { + int colErrorTop = renderer.getErrorColumnTop(); + int colErrorBottom = renderer.getErrorColumnBottom(); + drawErrorBars(context, shape, + row, data.index, col, colErrorTop, colErrorBottom, + axisY, axisRendererY); + } + } + }; + } + + /** + * Draws the specified value label for the specified shape. + * @param context Environment used for drawing. + * @param point Point shape used to layout the label. + * @param row Data row containing the point. + * @param pointIndex Index number used for coloring. + * @param col Index of the column that will be projected on the axis. + */ + protected void drawValueLabel(DrawingContext context, + Shape point, Row row, int pointIndex, int col) { + Comparable value = row.get(col); + + // Formatting + Format format = getValueFormat(); + if ((format == null) && row.isColumnNumeric(col)) { + format = NumberFormat.getInstance(); + } + + // Text to display + String text = (format != null) ? format.format(value) : value.toString(); + + // Visual settings + ColorMapper colors = getValueColor(); + Paint paint = colors.get(pointIndex); + Font font = getValueFont(); + double fontSize = font.getSize2D(); + + // Layout settings + Location location = getValueLocation(); + double alignX = getValueAlignmentX(); + double alignY = getValueAlignmentY(); + double rotation = getValueRotation(); + double distance = getValueDistance(); + if (MathUtils.isCalculatable(distance)) { + distance *= fontSize; + } else { + distance = 0.0; + } + + // Create a label with the settings + Label label = new Label(text); + label.setAlignmentX(alignX); + label.setAlignmentY(alignY); + label.setRotation(rotation); + label.setColor(paint); + label.setFont(font); + + Rectangle2D boundsPoint = point.getBounds2D(); + DrawableContainer labelContainer = + new DrawableContainer(new OuterEdgeLayout(distance)); + labelContainer.add(label, location); + + labelContainer.setBounds(boundsPoint); + labelContainer.draw(context); + } + + /** + * Draws error bars. + * @param context Environment used for drawing. + * @param point Shape of the point. + * @param row Data row containing the point. + * @param rowIndex Index of the row. + * @param col Index of the column that will be projected on the axis. + * @param colErrorTop Index of the column that contains the upper error value. + * @param colErrorBottom Index of the column that contains the lower error value. + * @param axis Axis. + * @param axisRenderer Axis renderer. + */ + protected void drawErrorBars(DrawingContext context, Shape point, + Row row, int rowIndex, int col, int colErrorTop, int colErrorBottom, + Axis axis, AxisRenderer axisRenderer) { + if (axisRenderer == null) { + return; + } + + if (colErrorTop < 0 || colErrorTop >= row.size() || + !row.isColumnNumeric(colErrorTop) || + colErrorBottom < 0 || colErrorBottom >= row.size() || + !row.isColumnNumeric(colErrorBottom)) { + return; + } + + Number value = (Number) row.get(col); + Number errorTop = (Number) row.get(colErrorTop); + Number errorBottom = (Number) row.get(colErrorBottom); + if (!MathUtils.isCalculatable(value) || + !MathUtils.isCalculatable(errorTop) || + !MathUtils.isCalculatable(errorBottom)) { + return; + } + + Graphics2D graphics = context.getGraphics(); + AffineTransform txOld = graphics.getTransform(); + + // Calculate positions + PointND pointValue = axisRenderer.getPosition(axis, + value, true, false); + PointND pointTop = axisRenderer.getPosition(axis, + value.doubleValue() + errorTop.doubleValue(), true, false); + PointND pointBottom = axisRenderer.getPosition(axis, + value.doubleValue() - errorBottom.doubleValue(), true, false); + if (pointValue == null || pointTop == null || pointBottom == null) { + return; + } + double posY = pointValue.get(PointND.Y); + double posYTop = pointTop.get(PointND.Y) - posY; + double posYBottom = pointBottom.get(PointND.Y) - posY; + + // Draw the error bar + Line2D errorBar = new Line2D.Double(0.0, posYTop, 0.0, posYBottom); + ColorMapper colors = getErrorColor(); + Paint errorPaint = colors.get(rowIndex); + Stroke errorStroke = getErrorStroke(); + GraphicsUtils.drawPaintedShape( + graphics, errorBar, errorPaint, null, errorStroke); + + // Draw the shapes at the end of the error bars + Shape endShape = getErrorShape(); + graphics.translate(0.0, posYTop); + Stroke endShapeStroke = new BasicStroke(1f); + GraphicsUtils.drawPaintedShape( + graphics, endShape, errorPaint, null, endShapeStroke); + graphics.setTransform(txOld); + graphics.translate(0.0, posYBottom); + GraphicsUtils.drawPaintedShape( + graphics, endShape, errorPaint, null, endShapeStroke); + graphics.setTransform(txOld); + } + + /** + * 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 = -2568531344817590175L; + + public void draw(DrawingContext context) { + PointRenderer renderer = DefaultPointRenderer2D.this; + Row row = data.row; + + if (renderer.isValueVisible()) { + int colValue = renderer.getValueColumn(); + drawValueLabel(context, shape, row, data.index, colValue); + } + } + }; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/LabelPointRenderer.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/LabelPointRenderer.java new file mode 100644 index 0000000..7f63cf7 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/LabelPointRenderer.java @@ -0,0 +1,155 @@ +package org.xbib.graphics.graph.gral.plots.points; + +import java.awt.Font; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.text.Format; +import java.text.NumberFormat; + +import org.xbib.graphics.graph.gral.data.Row; +import org.xbib.graphics.graph.gral.util.GraphicsUtils; + +/** + * Class that provides {@code Drawable}s, which display specified data + * values as labels. + */ +public class LabelPointRenderer extends DefaultPointRenderer2D { + + /** Index of the column for the label content. */ + private int column; + /** Format for the label content. */ + private Format format; + /** Font for the label content. */ + private Font font; + /** Horizontal alignment of the label content. */ + private double alignmentX; + /** Vertical alignment of the label content. */ + private double alignmentY; + + /** + * Initializes a new renderer. + */ + public LabelPointRenderer() { + column = 1; + format = NumberFormat.getInstance(); + font = Font.decode(null); + alignmentX = 0.5; + alignmentY = 0.5; + } + + /** + * Returns the index of the column which is used for the label. + * @return Index of the column which is used for the label. + */ + public int getColumn() { + return column; + } + + /** + * Sets the index of the column which will be used for the label. + * @param column Index of the column which will be used for the label. + */ + public void setColumn(int column) { + this.column = column; + } + + /** + * Returns the format which specifies how the labels are displayed. + * @return {@code Format} instance which specifies how the labels are + * displayed. + */ + public Format getFormat() { + return format; + } + + /** + * Sets the format which specifies how the labels will be displayed. + * @param format {@code Format} instance which specifies how the labels will + * be displayed. + */ + public void setFormat(Format format) { + this.format = format; + } + + /** + * Returns the font of this label. + * @return Font of this label. + */ + public Font getFont() { + return font; + } + + /** + * Sets font of this label. + * @param font Font of this label. + */ + public void setFont(Font font) { + this.font = font; + } + + /** + * Returns the horizontal alignment relative to the data point. + * 0 means left, 1 means right. + * @return Horizontal alignment relative to the data point. + */ + public double getAlignmentX() { + return alignmentX; + } + + /** + * Sets the horizontal alignment relative to the data point. + * 0 means left, 1 means right. + * @param alignmentX Horizontal alignment relative to the data point. + */ + public void setAlignmentX(double alignmentX) { + this.alignmentX = alignmentX; + } + + /** + * Returns the vertical alignment relative to the data point. + * 0 means top, 1 means bottom. + * @return Vertical alignment relative to the data point. + */ + public double getAlignmentY() { + return alignmentY; + } + + /** + * Sets the vertical alignment relative to the data point. + * 0 means top, 1 means bottom. + * @param alignmentY Vertical alignment relative to the data point. + */ + public void setAlignmentY(double alignmentY) { + this.alignmentY = alignmentY; + } + + @Override + public Shape getPointShape(PointData data) { + Row row = data.row; + int colLabel = getColumn(); + if (colLabel >= row.size()) { + return null; + } + + Comparable labelValue = row.get(colLabel); + if (labelValue == null) { + return null; + } + + Format format = getFormat(); + Font font = getFont(); + String text = format.format(labelValue); + double alignment = getAlignmentX(); + Shape shape = GraphicsUtils.getOutline(text, font, 0f, alignment); + + double alignX = getAlignmentX(); + double alignY = getAlignmentY(); + Rectangle2D bounds = shape.getBounds2D(); + AffineTransform tx = AffineTransform.getTranslateInstance( + -alignX*bounds.getWidth(), alignY*bounds.getHeight()); + shape = tx.createTransformedShape(shape); + + return shape; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/PointData.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/PointData.java new file mode 100644 index 0000000..ebebff2 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/PointData.java @@ -0,0 +1,41 @@ +package org.xbib.graphics.graph.gral.plots.points; + +import java.util.Collections; +import java.util.List; + +import org.xbib.graphics.graph.gral.data.Row; +import org.xbib.graphics.graph.gral.plots.axes.Axis; +import org.xbib.graphics.graph.gral.plots.axes.AxisRenderer; + +/** + * Class for storing data that will be used to create a data point in a plot. + */ +public class PointData { + /** Axes that will be used to project the point. */ + public final List axes; + /** Renderers for the axes that will be used to project the point. */ + public final List axisRenderers; + /** The index of the row. */ + public final int index; + /** The data row that will get projected. */ + public final Row row; + /** The index of the column in the row that contains the data value. */ + public final int col; + + /** + * Initializes a new instance with the specified data. + * @param axes Axes that are used to project the point. + * @param axisRenderers Renderers for the axes. + * @param row Data row containing that will be projected on the axes. + * @param rowIndex Index of the row. + * @param col Index of the column in the row that contains the data value. + */ + public PointData(List axes, List axisRenderers, + Row row, int rowIndex, int col) { + this.axes = Collections.unmodifiableList(axes); + this.axisRenderers = Collections.unmodifiableList(axisRenderers); + this.row = row; + this.index = rowIndex; + this.col = col; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/PointRenderer.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/PointRenderer.java new file mode 100644 index 0000000..3e91c87 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/PointRenderer.java @@ -0,0 +1,304 @@ +package org.xbib.graphics.graph.gral.plots.points; + +import java.awt.Font; +import java.awt.Paint; +import java.awt.Shape; +import java.awt.Stroke; +import java.text.Format; + +import org.xbib.graphics.graph.gral.graphics.Drawable; +import org.xbib.graphics.graph.gral.plots.colors.ColorMapper; +import org.xbib.graphics.graph.gral.graphics.Location; + +/** + *

An interface providing functions for rendering points in a plot. + * It defines methods for:

+ *
    + *
  • Retrieving the point of a certain row in a DataTable
  • + *
  • Getting and setting the points color
  • + *
  • Getting and setting the bounds of the points
  • + *
+ */ +public interface PointRenderer { + /** + * Returns the shape which is used to draw the point. + * @return {@code Shape} instance for the point. + */ + Shape getShape(); + + /** + * Sets the shape which will be used to draw the point. + * @param shape {@code Shape} instance for the point. + */ + void setShape(Shape shape); + + /** + * Returns a mapping that is used to fill the point shapes. + * @return {@code ColorMapper} that is used to fill the point shapes. + */ + ColorMapper getColor(); + + /** + * Sets the mapping that will be used to fill the point shapes. + * @param color {@code ColorMapper} instance to fill the point shapes. + */ + void setColor(ColorMapper color); + + /** + * Sets the paint that will be used to fill the point shapes. + * @param color {@code Paint} instance to fill the point shapes. + */ + void setColor(Paint color); + + /** + * Returns whether the data value of a point is displayed or not. + * @return {@code true} when the value is displayed, otherwise + * {@code false}. + */ + boolean isValueVisible(); + + /** + * Returns whether the data value of a point will be displayed or not. + * @param valueVisible {@code true} if the value should be displayed, + * otherwise {@code false}. + */ + void setValueVisible(boolean valueVisible); + + /** + * Returns the index of the column that contains the displayed values. + * @return Index of the column that contains the displayed values. + */ + int getValueColumn(); + + /** + * Sets the index of the column that contains the displayed values. + * @param columnIndex Index of the column that contains the displayed + * values. + */ + void setValueColumn(int columnIndex); + + /** + * Returns the format that is used to render the displayed data values. + * @return {@code Format} instance that is used to render the displayed + * data values. + */ + Format getValueFormat(); + + /** + * Sets the format that will be used to render the displayed data values. + * @param format {@code Format} instance that will be used to render the + * displayed data values. + */ + void setValueFormat(Format format); + + /** + * Returns the current positioning of the data value relative to the data + * point. + * @return Current positioning of the data value relative to the data + * point. + */ + Location getValueLocation(); + + /** + * Sets the positioning of the data value relative to the data point. + * @param location Positioning of the data value relative to the data point. + */ + void setValueLocation(Location location); + + /** + * Returns the relative horizontal position of the value. The position will + * be between 0 and 1. + * @return Relative horizontal position of the value. + */ + double getValueAlignmentX(); + + /** + * Sets the relative horizontal position of the value. The position can be + * specified between 0 and 1. + * @param alignmentX Relative horizontal position of the value. + */ + void setValueAlignmentX(double alignmentX); + + /** + * Returns the relative vertical position of the value. The position will + * be between 0 and 1. + * @return Relative vertical position of the value. + */ + double getValueAlignmentY(); + + /** + * Sets the relative vertical position of the value. The position can be + * specified between 0 and 1. + * @param alignmentX Relative vertical position of the value. + */ + void setValueAlignmentY(double alignmentX); + + /** + * Returns the current rotation angle of the value. + * @return Rotation angle in degrees. + */ + double getValueRotation(); + + /** + * Sets the rotation angle of the value. + * @param angle Rotation angle in degrees. + */ + void setValueRotation(double angle); + + /** + * Returns the current distance of values to the point. The distance is + * specified relative to the font height. + * @return Distance relative to the font height. + */ + double getValueDistance(); + + /** + * Sets the distance of values to the point. The distance is specified + * relative to the font height. + * @param distance Distance relative to the font height. + */ + void setValueDistance(double distance); + + /** + * Returns the mapping that is used to fill the value. + * @return {@code ColorMapper} instance that is used to fill the value. + */ + ColorMapper getValueColor(); + + /** + * Sets the mapping that will be used to fill the value. + * @param color {@code ColorMapper} instance that will be used to fill + * the value. + */ + void setValueColor(ColorMapper color); + + /** + * Sets the paint that will be used to fill the value. + * @param color {@code Paint} instance that will be used to fill the + * value. + */ + void setValueColor(Paint color); + + /** + * Returns the font that is used to render the value. + * @return Font that is used to render the value. + */ + Font getValueFont(); + + /** + * Sets the font that will be used to render the value. + * @param font Font that will be used to render the value. + */ + void setValueFont(Font font); + + /** + * Returns whether the error value is displayed. + * @return {@code true} if the error value is displayed, otherwise + * {@code false}. + */ + boolean isErrorVisible(); + + /** + * Sets whether the error value will be displayed. + * @param errorVisible {@code true} if the error value should be displayed, + * otherwise {@code false}. + */ + void setErrorVisible(boolean errorVisible); + + /** + * Returns the index of the column that contains the upper error value. + * @return Index of the column that contains the upper error value. + */ + int getErrorColumnTop(); + + /** + * Sets the index of the column that contains the upper error value. + * @param columnIndex Index of the column that contains the upper error + * value. + */ + void setErrorColumnTop(int columnIndex); + + /** + * Returns the index of the column that contains the lower error value. + * @return Index of the column that contains the lower error value. + */ + int getErrorColumnBottom(); + + /** + * Sets the index of the column that contains the lower error value. + * @param columnIndex Index of the column that contains the lower error + * value. + */ + void setErrorColumnBottom(int columnIndex); + + /** + * Returns the mapping that is used to fill the error indicators. + * @return {@code ColorMapper} instance that is used to fill the error + * indicators. + */ + ColorMapper getErrorColor(); + + /** + * Sets the mapping that will be used to fill the error indicators. + * @param color {@code ColorMapper} instance that will be used to fill + * the error indicators. + */ + void setErrorColor(ColorMapper color); + + /** + * Sets the paint that will be used to fill the error indicators. + * @param color {@code Paint} instance that will be used to fill the + * error indicators. + */ + void setErrorColor(Paint color); + + /** + * Returns the shape which is used to draw the error indicators. + * @return {@code Shape} instance of the error indicators. + */ + Shape getErrorShape(); + + /** + * Sets the shape which will be used to draw the error indicators. + * @param shape {@code Shape} instance for the error indicators. + */ + void setErrorShape(Shape shape); + + /** + * Returns the stroke which is used to draw the error indicators. + * @return Current stroke of the error indicators. + */ + Stroke getErrorStroke(); + + /** + * Sets the stroke which will be used to draw the error indicators. + * @param stroke Stroke of the error indicators. + */ + void setErrorStroke(Stroke 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. + */ + Shape getPointShape(PointData data); + + /** + * Returns the graphical representation to be drawn for the specified data + * value. + * @param data Information on axes, renderers, and values. + * @param shape Outline that describes the point's shape. + * @return Component that can be used to draw the point. + */ + Drawable getPoint(PointData data, Shape 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. + */ + Drawable getValue(PointData data, Shape shape); +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/SizeablePointRenderer.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/SizeablePointRenderer.java new file mode 100644 index 0000000..7b17ace --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/plots/points/SizeablePointRenderer.java @@ -0,0 +1,64 @@ +package org.xbib.graphics.graph.gral.plots.points; + +import java.awt.Shape; +import java.awt.geom.AffineTransform; + +import org.xbib.graphics.graph.gral.data.Row; +import org.xbib.graphics.graph.gral.util.DataUtils; +import org.xbib.graphics.graph.gral.util.MathUtils; + +/** + * Class that provides {@code Drawable}s, which are sized accordingly to + * the data. + */ +public class SizeablePointRenderer extends DefaultPointRenderer2D { + + /** Index of the column for the point size. */ + private int column; + + /** + * Initializes a new object. + */ + public SizeablePointRenderer() { + column = 2; + } + + /** + * Returns the index of the column which is used for point sizes. + * @return index of the column which is used for point sizes. + */ + public int getColumn() { + return column; + } + + /** + * Sets the index of the column which will be used for point sizes. + * @param column Index of the column which will be used for point sizes. + */ + public void setColumn(int column) { + this.column = column; + } + + @Override + public Shape getPointShape(PointData data) { + Shape shape = getShape(); + + Row row = data.row; + int colSize = getColumn(); + if (colSize >= row.size() || colSize < 0 || !row.isColumnNumeric(colSize)) { + return shape; + } + + Number value = (Number) row.get(colSize); + double size = DataUtils.getValueOrDefault(value, Double.NaN); + if (!MathUtils.isCalculatable(size) || size <= 0.0) { + return null; + } + + if (size != 1.0) { + AffineTransform tx = AffineTransform.getScaleInstance(size, size); + shape = tx.createTransformedShape(shape); + } + return shape; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/DrawablePanel.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/DrawablePanel.java new file mode 100644 index 0000000..d23975e --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/DrawablePanel.java @@ -0,0 +1,101 @@ +package org.xbib.graphics.graph.gral.ui; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.geom.Dimension2D; + +import javax.swing.JPanel; + +import org.xbib.graphics.graph.gral.graphics.Drawable; +import org.xbib.graphics.graph.gral.graphics.DrawingContext; + +/** + * A class that represents an adapter between the components of this library + * and Swing. It displays a single {@code Drawable} in a {@code JPanel}. + */ +@SuppressWarnings("serial") +public class DrawablePanel extends JPanel { + + /** Drawable that should be displayed. */ + private final Drawable drawable; + + /** Defines whether this panel uses antialiasing. */ + private boolean antialiased; + + /** + * Initializes a new instance with the specified {@code Drawable}. + * Antialiasing is enabled by default. + * @param drawable {@code Drawable} to be displayed + */ + public DrawablePanel(Drawable drawable) { + this.drawable = drawable; + setOpaque(false); + antialiased = true; + } + + /** + * Returns the {@code Drawable} instance that is displayed by this + * panel. + * @return {@code Drawable} instance + */ + public Drawable getDrawable() { + return drawable; + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + if (isVisible()) { + Graphics2D graphics = (Graphics2D) g; + if (isAntialiased()) { + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + } + + getDrawable().draw(new DrawingContext(graphics)); + } + } + + @Override + public void setBounds(Rectangle bounds) { + super.setBounds(bounds); + getDrawable().setBounds(bounds); + } + + @Override + public void setBounds(int x, int y, int width, int height) { + super.setBounds(x, y, width, height); + getDrawable().setBounds(0.0, 0.0, width, height); + } + + @Override + public Dimension getPreferredSize() { + Dimension dims = super.getPreferredSize(); + Dimension2D dimsPlot = getDrawable().getPreferredSize(); + dims.setSize(dimsPlot); + return dims; + } + + @Override + public Dimension getMinimumSize() { + return super.getPreferredSize(); + } + + /** + * Returns whether antialiasing is applied. + * @return {@code true} if the panel uses antialiasing, {@code false} otherwise. + */ + public boolean isAntialiased() { + return antialiased; + } + + /** + * Sets whether antialiasing should be applied. + * @param antialiased {@code true} if the panel should use antialiasing, {@code false} otherwise. + */ + public void setAntialiased(boolean antialiased) { + this.antialiased = antialiased; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/DrawableWriterFilter.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/DrawableWriterFilter.java new file mode 100644 index 0000000..bae1692 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/DrawableWriterFilter.java @@ -0,0 +1,68 @@ +package org.xbib.graphics.graph.gral.ui; + +import java.io.File; +import java.text.MessageFormat; + +import javax.swing.filechooser.FileFilter; + +import org.xbib.graphics.graph.gral.io.IOCapabilities; +import org.xbib.graphics.graph.gral.util.Messages; + +/** + * File filter that extracts files that can be read with a certain set of + * {@link org.xbib.graphics.graph.gral.io.IOCapabilities}. + */ +public class DrawableWriterFilter extends FileFilter { + /** Capabilities that describe the data formats that can be processed by + this filter. */ + private final IOCapabilities capabilities; + + /** + * Creates a new instance and initializes it with an + * {@link org.xbib.graphics.graph.gral.io.IOCapabilities} object. + * @param capabilities writer capabilities. + */ + public DrawableWriterFilter(IOCapabilities capabilities) { + this.capabilities = capabilities; + } + + @Override + public boolean accept(File f) { + if (f == null) { + return false; + } + if (f.isDirectory()) { + return true; + } + String ext = getExtension(f).toLowerCase(); + for (String extension : capabilities.getExtensions()) { + if (extension.equals(ext)) { + return true; + } + } + return false; + } + + @Override + public String getDescription() { + return MessageFormat.format(Messages.getString("IO.formatDescription"), //$NON-NLS-1$ + capabilities.getFormat(), capabilities.getName()); + } + + /** + * Returns the capabilities filtered by this instance. + * @return writer capabilities. + */ + public IOCapabilities getWriterCapabilities() { + return capabilities; + } + + private static String getExtension(File f) { + String name = f.getName(); + int lastDot = name.lastIndexOf('.'); + if ((lastDot <= 0) || (lastDot == name.length() - 1)) { + return ""; //$NON-NLS-1$ + } + return name.substring(lastDot + 1); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/ExportChooser.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/ExportChooser.java new file mode 100644 index 0000000..222c2af --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/ExportChooser.java @@ -0,0 +1,30 @@ +package org.xbib.graphics.graph.gral.ui; + +import java.util.List; + +import javax.swing.JFileChooser; + +import org.xbib.graphics.graph.gral.io.IOCapabilities; + +/** + * A file chooser implementation that can be for export purposes. + */ +@SuppressWarnings("serial") +public class ExportChooser extends JFileChooser { + + /** + * Creates a new instance and initializes it with an array of + * {@link org.xbib.graphics.graph.gral.io.IOCapabilities}. + * @param strict Determines whether this dialog allows only the file formats + * specified in {@code capabilities}. + * @param capabilities List of objects describing the file formats that + * are supported by this dialog. + */ + public ExportChooser(boolean strict, List capabilities) { + setAcceptAllFileFilterUsed(!strict); + for (IOCapabilities c : capabilities) { + addChoosableFileFilter(new DrawableWriterFilter(c)); + } + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/ExportDialog.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/ExportDialog.java new file mode 100644 index 0000000..f1bd048 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/ExportDialog.java @@ -0,0 +1,199 @@ +package org.xbib.graphics.graph.gral.ui; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.geom.Rectangle2D; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.text.DecimalFormat; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFormattedTextField; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; + +import org.xbib.graphics.graph.gral.graphics.Drawable; +import org.xbib.graphics.graph.gral.util.Messages; + +/** + * A dialog implementation for exporting plots. It allows the user to + * specify the document dimensions. + */ +@SuppressWarnings("serial") +public class ExportDialog extends JDialog { + + /** Type of user feedback. */ + public enum UserAction { + /** User confirmed dialog. */ + APPROVE, + /** User canceled or closed dialog. */ + CANCEL + } + + /** Bounding rectangle for document. */ + private final Rectangle2D documentBounds; + /** Action that was used to close this dialog. */ + private UserAction userAction; + + /** Input component for horizontal document offset. */ + private final JFormattedTextField inputX; + /** Input component for vertical document offset. */ + private final JFormattedTextField inputY; + /** Input component for document width. */ + private final JFormattedTextField inputW; + /** Input component for document height. */ + private final JFormattedTextField inputH; + + /** + * Creates a new instance and initializes it with a parent and a + * drawable component. + * @param parent Parent component. + * @param drawable Drawable component. + */ + public ExportDialog(Component parent, Drawable drawable) { + super(JOptionPane.getFrameForComponent(parent), true); + setTitle(Messages.getString("ExportDialog.exportOptionsTitle")); //$NON-NLS-1$ + + documentBounds = new Rectangle2D.Double(); + documentBounds.setFrame(drawable.getBounds()); + setUserAction(UserAction.CANCEL); + + JPanel cp = new JPanel(new BorderLayout()); + cp.setBorder(new EmptyBorder(10, 10, 10, 10)); + setContentPane(cp); + + DecimalFormat formatMm = new DecimalFormat(); + formatMm.setMinimumFractionDigits(2); + + JPanel options = new JPanel(new GridLayout(4, 2, 10, 2)); + getContentPane().add(options, BorderLayout.NORTH); + + PropertyChangeListener docBoundsListener = + new PropertyChangeListener() { + public void propertyChange(PropertyChangeEvent evt) { + setDocumentBounds( + ((Number) inputX.getValue()).doubleValue(), + ((Number) inputY.getValue()).doubleValue(), + ((Number) inputW.getValue()).doubleValue(), + ((Number) inputH.getValue()).doubleValue()); + } + }; + inputX = new JFormattedTextField(formatMm); + addInputField(inputX, Messages.getString("ExportDialog.left"), //$NON-NLS-1$ + options, documentBounds.getX(), docBoundsListener); + inputY = new JFormattedTextField(formatMm); + addInputField(inputY, Messages.getString("ExportDialog.top"), //$NON-NLS-1$ + options, documentBounds.getY(), docBoundsListener); + inputW = new JFormattedTextField(formatMm); + addInputField(inputW, Messages.getString("ExportDialog.width"), //$NON-NLS-1$ + options, documentBounds.getWidth(), docBoundsListener); + inputH = new JFormattedTextField(formatMm); + addInputField(inputH, Messages.getString("ExportDialog.height"), //$NON-NLS-1$ + options, documentBounds.getHeight(), docBoundsListener); + + JPanel controls = new JPanel(new FlowLayout()); + cp.add(controls, BorderLayout.SOUTH); + + JButton buttonConfirm = new JButton( + Messages.getString("ExportDialog.confirm")); //$NON-NLS-1$ + buttonConfirm.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + setUserAction(UserAction.APPROVE); + dispose(); + } + }); + controls.add(buttonConfirm); + + JButton buttonCancel = new JButton( + Messages.getString("ExportDialog.abort")); //$NON-NLS-1$ + buttonCancel.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + setUserAction(UserAction.CANCEL); + dispose(); + } + }); + controls.add(buttonCancel); + + pack(); + setLocationRelativeTo(parent); + } + + /** + * Utility method that adds a new label and a new input field to the + * dialog. + * @param input Input field. + * @param labelText Text for label. + * @param cont Container. + * @param initialValue Initial value for the input field. + * @param pcl Property change listener that should be associated with the + * input field. + */ + private static void addInputField(JFormattedTextField input, + String labelText, java.awt.Container cont, Object initialValue, + PropertyChangeListener pcl) { + JLabel label = new JLabel(labelText); + label.setHorizontalAlignment(JLabel.RIGHT); + cont.add(label); + input.setValue(initialValue); + input.setHorizontalAlignment(JFormattedTextField.RIGHT); + input.addPropertyChangeListener("value", pcl); //$NON-NLS-1$ + cont.add(input); + label.setLabelFor(input); + } + + /** + * Returns the bounds entered by the user. + * @return Document bounds that should be used to export the plot + */ + public Rectangle2D getDocumentBounds() { + Rectangle2D bounds = new Rectangle2D.Double(); + bounds.setFrame(documentBounds); + return bounds; + } + + /** + * Sets new bounds for the document. + * @param x Top-left corner + * @param y Bottom-right corner + * @param w Width. + * @param h Height. + */ + protected void setDocumentBounds(double x, double y, double w, double h) { + if ((documentBounds.getX() == x) + && (documentBounds.getY() == y) + && (documentBounds.getWidth() == w) + && (documentBounds.getHeight() == h)) { + return; + } + documentBounds.setFrame(x, y, w, h); + inputX.setValue(x); + inputY.setValue(y); + inputW.setValue(w); + inputH.setValue(h); + } + + /** + * Returns the last action by the user. The return value can be used to + * determine whether the user approved or canceled the dialog. + * @return Type of user action. + */ + public UserAction getUserAction() { + return userAction; + } + + /** + * Sets the type of action the user executed. The value can later be used + * to determine whether the user approved or canceled the dialog. + * @param userAction Type of user action. + */ + private void setUserAction(UserAction userAction) { + this.userAction = userAction; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/InteractivePanel.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/InteractivePanel.java new file mode 100644 index 0000000..2cf1739 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/InteractivePanel.java @@ -0,0 +1,543 @@ +package org.xbib.graphics.graph.gral.ui; + +import org.xbib.graphics.graph.gral.graphics.Container; +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.plots.DrawableWriter; +import org.xbib.graphics.graph.gral.io.plots.DrawableWriterFactory; +import org.xbib.graphics.graph.gral.navigation.Navigable; +import org.xbib.graphics.graph.gral.navigation.Navigator; +import org.xbib.graphics.graph.gral.util.Messages; +import org.xbib.graphics.graph.gral.util.PointND; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.print.PageFormat; +import java.awt.print.Printable; +import java.awt.print.PrinterException; +import java.awt.print.PrinterJob; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import javax.swing.AbstractAction; +import javax.swing.ActionMap; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.JPopupMenu; +import javax.swing.SwingUtilities; + +/** + * A panel implementation that displays a {@code Drawable} instance as a + * rich Swing component. + */ +@SuppressWarnings("serial") +public class InteractivePanel extends DrawablePanel implements Printable { + + // FIXME Find better method to adjust resolution + /** Constant that can be used to convert from millimeters to points + (1/72 inch). */ + private static final double MM_TO_PT = 72.0/25.4; + /** Constant that defines how many millimeters a pixel will be. */ + private static final double MM_PER_PX = 0.2*MM_TO_PT; + /** Job for printing the current panel. */ + private final PrinterJob printerJob; + + /** Value that is necessary before panning is triggered. */ + private static final int MIN_DRAG = 0; + + /** Defines whether the panel can be zoomed. */ + private boolean zoomable; + /** Defines whether the panel can be panned. */ + private boolean pannable; + + /** Map that stored actions by names like "zoomIn", "zoomOut", + "resetView", "exportImage", or "print". */ + protected final ActionMap actions; + + /** Cache for the popup menu. */ + private JPopupMenu popupMenu; + private boolean popupMenuEnabled; + private Point2D popupMenuPos; + + /** Chooser for image export. */ + private final JFileChooser exportImageChooser; + + /** Object to be used as listener for zooming actions. */ + private MouseZoomListener zoomListener; + /** Object to be used as listener for panning actions. */ + private NavigationMoveListener panListener; + + /** + * Listener class for zooming actions. + */ + private final static class MouseZoomListener extends MouseAdapter + implements MouseWheelListener, Serializable { + /** Version id for serialization. */ + private static final long serialVersionUID = -7323541053291673122L; + + private final InteractivePanel panel; + + public MouseZoomListener(InteractivePanel panel) { + this.panel = panel; + } + + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + Point2D point = e.getPoint(); + panel.zoom(point, -e.getWheelRotation()); + } + + @Override + public void mouseClicked(MouseEvent e) { + if (SwingUtilities.isLeftMouseButton(e) && + (e.getClickCount() == 2)) { + Point2D point = e.getPoint(); + panel.zoom(point, 1); + } + } + } + + /** + * Creates a new panel instance and initializes it with a + * drawable component. + * @param drawable Drawable component. + */ + @SuppressWarnings("serial") + public InteractivePanel(Drawable drawable) { + super(drawable); + + printerJob = PrinterJob.getPrinterJob(); + printerJob.setPrintable(this); + + List exportFormats = DrawableWriterFactory.getInstance() + .getCapabilities(); + exportImageChooser = new ExportChooser(true, exportFormats); + exportImageChooser.setDialogTitle(Messages.getString( + "InteractivePanel.exportImageTitle")); //$NON-NLS-1$ + + actions = new ActionMap(); + actions.put("zoomIn", new AbstractAction(Messages.getString( //$NON-NLS-1$ + "InteractivePanel.zoomIn")) { //$NON-NLS-1$ + public void actionPerformed(ActionEvent e) { + zoom(popupMenuPos, 1); + } + }); + actions.put("zoomOut", new AbstractAction(Messages.getString( //$NON-NLS-1$ + "InteractivePanel.zoomOut")) { //$NON-NLS-1$ + public void actionPerformed(ActionEvent e) { + zoom(popupMenuPos, -1); + } + }); + actions.put("resetView", new AbstractAction(Messages.getString( //$NON-NLS-1$ + "InteractivePanel.resetView")) { //$NON-NLS-1$ + public void actionPerformed(ActionEvent e) { + resetZoom(popupMenuPos); + } + }); + actions.put("exportImage", new AbstractAction(Messages.getString( //$NON-NLS-1$ + "InteractivePanel.exportImage")) { //$NON-NLS-1$ + public void actionPerformed(ActionEvent e) { + int ret = exportImageChooser.showSaveDialog( + InteractivePanel.this); + // Clear artifacts of the file chooser + repaint(); + // If the user aborted we can stop + if (ret != JFileChooser.APPROVE_OPTION) { + return; + } + // If the user didn't select a file we can stop + File file = exportImageChooser.getSelectedFile(); + if (file == null) { + return; + } + // If the selected an existing file we ask for permission + // to overwrite it + else if (file.exists()) { + int retOverwrite = JOptionPane.showConfirmDialog( + InteractivePanel.this, + Messages.getString("InteractivePanel.exportExistsWarning"), //$NON-NLS-1$ + Messages.getString("InteractivePanel.warning"), //$NON-NLS-1$ + JOptionPane.YES_NO_OPTION + ); + // Clear artifacts of the confirm dialog + repaint(); + if (retOverwrite == JOptionPane.NO_OPTION) { + return; + } + } + + // Export current view to the selected file + Drawable d = getDrawable(); + ExportDialog ed = new ExportDialog(InteractivePanel.this, d); + ed.setVisible(true); + if (!ed.getUserAction().equals( + ExportDialog.UserAction.APPROVE)) { + return; + } + DrawableWriterFilter filter = (DrawableWriterFilter) + exportImageChooser.getFileFilter(); + export(d, filter.getWriterCapabilities().getMimeType(), + file, ed.getDocumentBounds()); + } + }); + actions.put("print", new AbstractAction(Messages.getString( //$NON-NLS-1$ + "InteractivePanel.print")) { //$NON-NLS-1$ + public void actionPerformed(ActionEvent e) { + if (printerJob.printDialog()) { + try { + printerJob.print(); + } catch (PrinterException ex) { + // TODO Show error dialog + ex.printStackTrace(); + } + } + } + }); + + popupMenuEnabled = true; + addMouseListener(new PopupListener()); + + setZoomable(true); + setPannable(true); + } + + /** + * Method that returns the popup menu for a given mouse event. It will be + * called on each popup event if the menu is enabled. If the menu is static + * caching can be used to prevent unnecessary generation of menu objects. + * @param e Mouse event that triggered the popup menu. + * @return A popup menu instance, or {@code null} if no popup menu should be shown. + * @see #isPopupMenuEnabled() + * @see #setPopupMenuEnabled(boolean) + */ + protected JPopupMenu getPopupMenu(MouseEvent e) { + if (popupMenu == null) { + popupMenu = new JPopupMenu(); + popupMenu.add(actions.get("zoomIn")); //$NON-NLS-1$ + popupMenu.add(actions.get("zoomOut")); //$NON-NLS-1$ + popupMenu.add(actions.get("resetView")); //$NON-NLS-1$ + popupMenu.addSeparator(); + popupMenu.add(actions.get("exportImage")); //$NON-NLS-1$ + popupMenu.add(actions.get("print")); //$NON-NLS-1$ + } + return popupMenu; + } + + /** + * Returns whether a popup menu will be shown by this panel when the user + * takes the appropriate action. The necessary action depends on the + * operating system of the user. + * @return {@code true} when a popup menu will be shown, + * otherwise {@code false}. + */ + public boolean isPopupMenuEnabled() { + return popupMenuEnabled; + } + + /** + * Sets whether a popup menu will be shown by this panel when the user + * takes the appropriate action. The necessary action depends on the + * operating system of the user. + * @param popupMenuEnabled {@code true} when a popup menu should be + * shown, otherwise {@code false}. + */ + public void setPopupMenuEnabled(boolean popupMenuEnabled) { + this.popupMenuEnabled = popupMenuEnabled; + } + + /** + * Zooms a navigable object in (positive values) or out (negative values). + * @param point The location where the zoom was triggered. + * @param times Number of times the navigable object will be zoomed. + * Positive values zoom in, negative values zoom out. + */ + private void zoom(Point2D point, int times) { + if (!isZoomable()) { + return; + } + + Navigable navigable = InteractivePanel.getNavigableAt(getDrawable(), point); + if (navigable == null) { + return; + } + + Navigator navigator = navigable.getNavigator(); + if (times >= 0) { + for (int i = 0; i < times; i++) { + navigator.zoomIn(); + } + } else { + for (int i = 0; i < -times; i++) { + navigator.zoomOut(); + } + } + + repaint(); + } + + private void resetZoom(Point2D point) { + if (!isZoomable()) { + return; + } + + Navigable navigable = InteractivePanel.getNavigableAt(getDrawable(), point); + if (navigable == null) { + return; + } + + Navigator navigator = navigable.getNavigator(); + navigator.reset(); + + repaint(); + } + + /** + * Method that exports the current view to a file using a specified file type. + * @param component Drawable that will be exported. + * @param mimeType File format as MIME type string. + * @param file File to export to. + * @param documentBounds Document boundary rectangle + */ + private void export(Drawable component, String mimeType, File file, + Rectangle2D documentBounds) { + try (FileOutputStream destination = new FileOutputStream(file)) { + DrawableWriter writer = DrawableWriterFactory.getInstance().get(mimeType); + writer.write(component, destination, + documentBounds.getX(), documentBounds.getY(), + documentBounds.getWidth(), documentBounds.getHeight()); + } catch (IOException e) { + // TODO: Exception handling + e.printStackTrace(); + } + } + + /** + * Class that is responsible for showing the popup menu. + */ + private class PopupListener extends MouseAdapter { + @Override + public void mousePressed(MouseEvent e) { + showPopup(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + showPopup(e); + } + + private void showPopup(MouseEvent e) { + if (!isPopupMenuEnabled() || !e.isPopupTrigger()) { + return; + } + JPopupMenu menu = getPopupMenu(e); + if (menu == null) { + return; + } + popupMenuPos = e.getPoint(); + menu.show(e.getComponent(), e.getX(), e.getY()); + } + } + + /** + * Class that handles mouse moves for navigation. + */ + private static class NavigationMoveListener extends MouseAdapter { + /** A reference to the panel for refreshing. */ + private final InteractivePanel panel; + /** AbstractPlot that will be changed by this class. */ + private Navigable navigable; + /** Previously clicked point or {@code null}. */ + private Point posPrev; + + /** + * Creates a new listener and initializes it with a panel. + * @param panel InteractivePanel that should be refreshed. + */ + public NavigationMoveListener(InteractivePanel panel) { + this.panel = panel; + } + + @Override + public void mousePressed(MouseEvent e) { + Point point = e.getPoint(); + navigable = InteractivePanel.getNavigableAt(panel.getDrawable(), point); + posPrev = point; + } + + @Override + public void mouseDragged(MouseEvent e) { + if (navigable == null) { + return; + } + + // Calculate distance that the current view was dragged + // (screen units) + Point pos = e.getPoint(); + Navigator navigator = navigable.getNavigator(); + + int dx = pos.x - posPrev.x; + int dy = pos.y - posPrev.y; + posPrev = pos; + + if (Math.abs(dx) > MIN_DRAG || Math.abs(dy) > MIN_DRAG) { + PointND deltas = new PointND<>(dx, dy); + navigator.pan(deltas); + panel.repaint(); + } + } + } + + /** + * Prints the page at the specified index into the specified + * {@link Graphics} context in the specified format. + * @param g the context into which the page is drawn + * @param pageFormat the size and orientation of the page being drawn + * @param pageIndex the zero based index of the page to be drawn + * @return PAGE_EXISTS if the page is rendered successfully + * or NO_SUCH_PAGE if {@code pageIndex} specifies a + * non-existent page. + * @exception PrinterException + * thrown when the print job is terminated. + */ + public int print(Graphics g, PageFormat pageFormat, int pageIndex) + throws PrinterException { + if (pageIndex > 0) { + return Printable.NO_SUCH_PAGE; + } + + Graphics2D graphics = (Graphics2D) g; + AffineTransform txOld = graphics.getTransform(); + graphics.scale(MM_PER_PX, MM_PER_PX); + + Rectangle2D boundsOld = getDrawable().getBounds(); + Rectangle2D pageBounds = new Rectangle2D.Double( + pageFormat.getImageableX()/MM_PER_PX, + pageFormat.getImageableY()/MM_PER_PX, + pageFormat.getImageableWidth()/MM_PER_PX, + pageFormat.getImageableHeight()/MM_PER_PX + ); + + // Set size + // TODO Keep Drawable's aspect ratio when scaling + getDrawable().setBounds(pageBounds); + // TODO Assure to temporarily turn off anti-aliasing before printing + try { + getDrawable().draw(new DrawingContext(graphics)); + } finally { + getDrawable().setBounds(boundsOld); + } + graphics.setTransform(txOld); + return Printable.PAGE_EXISTS; + } + + /** + * Returns whether the plot area in the panel can be zoomed. + * @return {@code true} if the plot can be zoomed, + * {@code false} otherwise. + */ + public boolean isZoomable() { + return zoomable; + } + + /** + * Sets whether the plot area in the panel can be zoomed. + * @param zoomable {@code true} if the plot should be zoomable, + * {@code false} otherwise. + */ + public void setZoomable(boolean zoomable) { + if (this.zoomable == zoomable) { + return; + } + + this.zoomable = zoomable; + + if (zoomListener != null) { + removeMouseWheelListener(zoomListener); + removeMouseListener(zoomListener); + zoomListener = null; + } + + if (zoomable) { + zoomListener = new MouseZoomListener(this); + addMouseListener(zoomListener); + addMouseWheelListener(zoomListener); + } + + actions.get("zoomIn").setEnabled(isZoomable()); //$NON-NLS-1$ + actions.get("zoomOut").setEnabled(isZoomable()); //$NON-NLS-1$ + actions.get("resetView").setEnabled(isZoomable() && isPannable()); //$NON-NLS-1$ + } + + /** + * Returns whether the plot area in the panel can be panned. + * @return {@code true} if the plot can be panned, + * {@code false} otherwise. + */ + public boolean isPannable() { + return pannable; + } + + /** + * Sets whether the plot area in the panel can be panned. + * @param pannable {@code true} if the plot should be pannable, + * {@code false} otherwise. + */ + public void setPannable(boolean pannable) { + if (this.pannable == pannable) { + return; + } + + this.pannable = pannable; + + if (panListener != null) { + removeMouseMotionListener(panListener); + removeMouseListener(panListener); + panListener = null; + } + if (pannable) { + // Register a new handler to move the map by dragging + // This requires that an x- and a y-axis do exist in the plot + panListener = new NavigationMoveListener(this); + addMouseListener(panListener); + addMouseMotionListener(panListener); + } + + actions.get("resetView").setEnabled(isZoomable() && isPannable()); //$NON-NLS-1$ + } + + /** + * Returns a navigable area at the specified point, {@code null} if no + * object could be found. If the specified container isn't navigable, its + * children are recursively checked. + * @param drawable The drawable container to check for navigable children. + * @param point Position that should hit the navigable object. + * @return A navigable object. + */ + private static Navigable getNavigableAt(Drawable drawable, Point2D point) { + List componentsToCheck; + if (drawable instanceof Container) { + componentsToCheck = ((Container) drawable).getDrawablesAt(point); + } else { + componentsToCheck = new ArrayList<>(1); + componentsToCheck.add(drawable); + } + for (Drawable component : componentsToCheck) { + if ((component instanceof Navigable) && component.getBounds().contains(point)) { + return (Navigable) component; + } + } + return null; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/package-info.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/package-info.java new file mode 100755 index 0000000..3d30604 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/ui/package-info.java @@ -0,0 +1,27 @@ +/* + * GRAL: GRAphing Library for Java(R) + * + * (C) Copyright 2009-2019 Erich Seifert , + * Michael Seifert + * + * 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 . + */ +/** + * User interface classes. The classes implement ready-to-use user interface + * components, e.g. for settings and file chooser dialogs, that be used by + * applications. + */ +package org.xbib.graphics.graph.gral.ui; diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/ConcatenationIterator.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/ConcatenationIterator.java new file mode 100644 index 0000000..0ccdd06 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/ConcatenationIterator.java @@ -0,0 +1,38 @@ +package org.xbib.graphics.graph.gral.util; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +public class ConcatenationIterator implements Iterator { + private final Iterator[] inputIterators; + + @SuppressWarnings("unchecked") + public ConcatenationIterator(Iterator... inputIterators) { + this.inputIterators = Arrays.copyOf(inputIterators, inputIterators.length); + } + + @Override + public boolean hasNext() { + for (Iterator inputIterator : inputIterators) { + if (inputIterator.hasNext()) { + return true; + } + } + return false; + } + + @Override + public T next() { + for (Iterator inputIterator : inputIterators) { + if (inputIterator.hasNext()) { + return inputIterator.next(); + } + } + throw new NoSuchElementException("No elements left in concatenated iterator."); + } + + @Override + public void remove() { + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/DataUtils.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/DataUtils.java new file mode 100644 index 0000000..0390ba2 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/DataUtils.java @@ -0,0 +1,77 @@ +/* + * GRAL: GRAphing Library for Java(R) + * + * (C) Copyright 2009-2019 Erich Seifert , + * Michael Seifert + * + * 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 . + */ +package org.xbib.graphics.graph.gral.util; + +import java.util.HashMap; +import java.util.Map; + +/** + * Abstract class that contains utility functions for creating data structures + * and for working with data sources and values. + */ +public abstract class DataUtils { + /** + * Default constructor that prevents creation of class. + */ + private DataUtils() { + throw new UnsupportedOperationException(); + } + + /** + * Creates a mapping from two arrays, one with keys, one with values. + * @param Data type of the keys. + * @param Data type of the values. + * @param keys Array containing the keys. + * @param values Array containing the values. + * @return Map with keys and values from the specified arrays. + */ + public static Map map(K[] keys, V[] values) { + // Check for valid parameters + if (keys.length != values.length) { + throw new IllegalArgumentException( + "Could not create the map because the number of keys and values differs."); + } + // Fill map with keys and values + Map map = new HashMap<>(); + for (int i = 0; i < keys.length; i++) { + K key = keys[i]; + V value = values[i]; + map.put(key, value); + } + return map; + } + + /** + * Returns the double value of the {@code Number} object or the specified + * default value if the object is {@code null}. + * @param n Number object. + * @param defaultValue Default value. + * @return Double value of the {@code Number} object or the default value + * if the object is {@code null} + */ + public static double getValueOrDefault(Number n, double defaultValue) { + if (n == null) { + return defaultValue; + } + return n.doubleValue(); + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/GeometryUtils.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/GeometryUtils.java new file mode 100644 index 0000000..e1ed6f2 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/GeometryUtils.java @@ -0,0 +1,437 @@ +/* + * GRAL: GRAphing Library for Java(R) + * + * (C) Copyright 2009-2019 Erich Seifert , + * Michael Seifert + * + * 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 . + */ +package org.xbib.graphics.graph.gral.util; + +import java.awt.BasicStroke; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.FlatteningPathIterator; +import java.awt.geom.Line2D; +import java.awt.geom.Path2D; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.io.Serializable; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; + +/** + * Abstract class that represents a collection of utility functions + * concerning geometry. + */ +public abstract class GeometryUtils { + /** Precision. */ + public static final double EPSILON = 1e-5; + /** Precision squared. */ + public static final double EPSILON_SQ = EPSILON*EPSILON; + + /** + * Default constructor that prevents creation of class. + */ + private GeometryUtils() { + throw new UnsupportedOperationException(); + } + + /** + * Returns the line fragments of the specified Shape. + * @param path Shape to be divided. + * @param swapped Invert segment direction. + * @return Array of lines. + */ + public static Line2D[] shapeToLines(Shape path, boolean swapped) { + Deque lines = new ArrayDeque<>(); + PathIterator i = + new FlatteningPathIterator(path.getPathIterator(null), 0.5); + + double[] coords = new double[6]; + double[] coordsPrev = new double[6]; + while (!i.isDone()) { + int segment = i.currentSegment(coords); + + if (segment == PathIterator.SEG_LINETO || + segment == PathIterator.SEG_CLOSE) { + Line2D line; + if (!swapped) { + line = new Line2D.Double( + coordsPrev[0], coordsPrev[1], coords[0], coords[1]); + lines.addLast(line); + } else { + line = new Line2D.Double( + coords[0], coords[1], coordsPrev[0], coordsPrev[1]); + lines.addFirst(line); + } + } + if (segment == PathIterator.SEG_CLOSE && !lines.isEmpty()) { + Point2D firstPoint = lines.getFirst().getP1(); + Point2D lastPoint = lines.getLast().getP2(); + if (!firstPoint.equals(lastPoint)) { + Line2D line; + if (!swapped) { + line = new Line2D.Double( + coords[0], coords[1], firstPoint.getX(), firstPoint.getY()); + lines.addLast(line); + } else { + line = new Line2D.Double( + firstPoint.getX(), firstPoint.getY(), coords[0], coords[1]); + lines.addFirst(line); + } + } + } + + System.arraycopy(coords, 0, coordsPrev, 0, 6); + i.next(); + } + Line2D[] linesArray = new Line2D[lines.size()]; + lines.toArray(linesArray); + return linesArray; + } + + /** + * Returns all intersection points of two shapes. + * @param s1 First shape + * @param s2 Second shape + * @return Intersection points, or empty array if + * no intersections were found + */ + public static List intersection(final Shape s1, final Shape s2) { + List intersections = new ArrayList<>(2); + Line2D[] lines1 = shapeToLines(s1, false); + Line2D[] lines2 = shapeToLines(s2, false); + + for (Line2D l1 : lines1) { + for (Line2D l2 : lines2) { + Point2D intersection = intersection(l1, l2); + if (intersection != null) { + intersections.add(intersection); + } + } + } + + return intersections; + } + + /** + * Returns the intersection point of two lines. + * @param l1 First line + * @param l2 Second line + * @return Intersection point, or {@code null} if + * no intersection was found + */ + public static Point2D intersection(final Line2D l1, final Line2D l2) { + Point2D p0 = l1.getP1(); + Point2D d0 = new Point2D.Double(l1.getX2() - p0.getX(), l1.getY2() - p0.getY()); + Point2D p1 = l2.getP1(); + Point2D d1 = new Point2D.Double(l2.getX2() - p1.getX(), l2.getY2() - p1.getY()); + + Point2D e = new Point2D.Double(p1.getX() - p0.getX(), p1.getY() - p0.getY()); + double kross = d0.getX()*d1.getY() - d0.getY()*d1.getX(); + double sqrKross = kross*kross; + double sqrLen0 = d0.distanceSq(0.0, 0.0); + double sqrLen1 = d1.distanceSq(0.0, 0.0); + + if (sqrKross > EPSILON_SQ * sqrLen0 * sqrLen1) { + double s = (e.getX()*d1.getY() - e.getY()*d1.getX())/kross; + if (s < 0d || s > 1d) { + return null; + } + double t = (e.getX()*d0.getY() - e.getY()*d0.getX())/kross; + if (t < 0d || t > 1d) { + return null; + } + return new Point2D.Double( + p0.getX() + s*d0.getX(), p0.getY() + s*d0.getY() + ); + } + + /* + double sqrLenE = e.lengthSq(); + kross = e.cross(d0); + sqrKross = kross*kross; + if (sqrKross > EPSILON_SQ*sqrLen0*sqrLenE) { + return null; + } + */ + + return null; + } + + /** + * Expand or shrink a shape in all directions by a defined offset. + * @param s Shape + * @param offset Offset + * @return New shape that was expanded or shrunk by the specified amount + */ + public static Area grow(final Shape s, final double offset) { + return grow(s, offset, BasicStroke.JOIN_MITER, 10f); + } + + /** + * Expand or shrink a shape in all directions by a defined offset. + * @param s Shape + * @param offset Offset to expand/shrink + * @param join Method for handling edges (see BasicStroke) + * @param miterlimit Limit for miter joining method + * @return New shape that is expanded or shrunk by the specified amount + */ + public static Area grow(final Shape s, final double offset, int join, + float miterlimit) { + Area shape = new Area(s); + + if (MathUtils.almostEqual(offset, 0.0, EPSILON)) { + return shape; + } + + Stroke stroke = new BasicStroke((float)Math.abs(2.0*offset), + BasicStroke.CAP_SQUARE, join, miterlimit); + Area strokeShape = new Area(stroke.createStrokedShape(s)); + + if (offset > 0.0) { + shape.add(strokeShape); + } else { + shape.subtract(strokeShape); + } + + return shape; + } + + /** + * Subtract a specified geometric area of data points from another shape to yield gaps. + * @param shapeArea Shape from which to subtract. + * @param gap Size of the gap. + * @param rounded Gap corners will be rounded if {@code true}. + * @param pointPos Position of the data point + * @param pointShape Shape of the data point + * @return Shape with punched holes + */ + public static Area punch(Area shapeArea, double gap, boolean rounded, + Point2D pointPos, Shape pointShape) { + if (gap <= 1e-10 || pointPos == null || pointShape == null) { + return shapeArea; + } + + AffineTransform tx = AffineTransform.getTranslateInstance( + pointPos.getX(), pointPos.getY()); + + int gapJoin = rounded ? BasicStroke.JOIN_ROUND : BasicStroke.JOIN_MITER; + Area gapArea = GeometryUtils.grow( + tx.createTransformedShape(pointShape), gap, gapJoin, 10f); + + shapeArea.subtract(gapArea); + + return shapeArea; + } + + /** + * Utility data class for the values of the segments in a geometric shape. + */ + public static final class PathSegment implements Serializable { + /** Version id for serialization. */ + private static final long serialVersionUID = 526444553637955799L; + + /** Segment type id as defined in {@link PathIterator}. */ + public final int type; + /** Starting point. */ + public final Point2D start; + /** End point. */ + public final Point2D end; + /** Coordinates necessary to draw the segment. */ + public final double[] coords; + + /** + * Initializes a new instance with type, starting and end point, and + * all other coordinates that are necessary to draw the segment. + * @param type Segment type id as defined in {@link PathIterator}. + * @param start Starting point. + * @param end End point. + * @param coords Array of coordinates necessary to draw the segment. + */ + public PathSegment(int type, Point2D start, Point2D end, double[] coords) { + this.type = type; + this.start = start; + this.end = end; + this.coords = new double[6]; + System.arraycopy(coords, 0, this.coords, 0, 6); + } + } + + /** + * Returns a list of a shape's segments as they are returned by its path + * iterator. + * @param shape Shape to be iterated. + * @return A list of path segment objects. + */ + public static List getSegments(Shape shape) { + PathIterator path = shape.getPathIterator(null); + + Point2D pointStart = null, pointEnd = null; + double[] coords = new double[6]; + List segments = new LinkedList<>(); + while (!path.isDone()) { + int type = path.currentSegment(coords); + + if (type == PathIterator.SEG_MOVETO || type == PathIterator.SEG_LINETO) { + pointEnd = new Point2D.Double(coords[0], coords[1]); + } else if (type == PathIterator.SEG_QUADTO) { + pointEnd = new Point2D.Double(coords[2], coords[3]); + } else if (type == PathIterator.SEG_CUBICTO) { + pointEnd = new Point2D.Double(coords[4], coords[5]); + } + + PathSegment segment = new PathSegment(type, pointStart, pointEnd, coords); + segments.add(segment); + + pointStart = pointEnd; + path.next(); + } + + return segments; + } + + /** + * Constructs a geometric shape from a list of path segments. + * @param segments List of path segments. + * @param isDouble {@code true} if the shape contents should be stored with + * double values, {@code false} if they should be stored as float. + * @return A geometric shape. + */ + public static Shape getShape(List segments, boolean isDouble) { + if (isDouble) { + return getShapeDouble(segments); + } else { + return getShapeFloat(segments); + } + } + + /** + * Constructs a geometric shape with double precision from a list of path + * segments. + * @param segments List of path segments. + * @return A geometric shape. + */ + private static Shape getShapeDouble(List segments) { + Path2D.Double path = + new Path2D.Double(Path2D.WIND_NON_ZERO, segments.size()); + for (PathSegment segment : segments) { + double[] coords = segment.coords; + if (segment.type == PathIterator.SEG_MOVETO) { + path.moveTo(coords[0], coords[1]); + } else if (segment.type == PathIterator.SEG_LINETO) { + path.lineTo(coords[0], coords[1]); + } else if (segment.type == PathIterator.SEG_QUADTO) { + path.quadTo(coords[0], coords[1], + coords[2], coords[3]); + } else if (segment.type == PathIterator.SEG_CUBICTO) { + path.curveTo(coords[0], coords[1], + coords[2], coords[3], + coords[4], coords[5]); + } else if (segment.type == PathIterator.SEG_CLOSE) { + path.closePath(); + } + } + return path; + } + + /** + * Constructs a geometric shape with single precision from a list of path + * segments. + * @param segments List of path segments. + * @return A geometric shape. + */ + private static Shape getShapeFloat(List segments) { + Path2D.Float path = + new Path2D.Float(Path2D.WIND_NON_ZERO, segments.size()); + for (PathSegment segment : segments) { + float[] coords = new float[segment.coords.length]; + for (int i = 0; i < coords.length; i++) { + coords[i] = (float) segment.coords[i]; + } + if (segment.type == PathIterator.SEG_MOVETO) { + path.moveTo(coords[0], coords[1]); + } else if (segment.type == PathIterator.SEG_LINETO) { + path.lineTo(coords[0], coords[1]); + } else if (segment.type == PathIterator.SEG_QUADTO) { + path.quadTo(coords[0], coords[1], + coords[2], coords[3]); + } else if (segment.type == PathIterator.SEG_CUBICTO) { + path.curveTo(coords[0], coords[1], + coords[2], coords[3], + coords[4], coords[5]); + } else if (segment.type == PathIterator.SEG_CLOSE) { + path.closePath(); + } + } + return path; + } + + /** + * Returns a clone of a specified shape which has a reversed order of the + * points, lines and curves. + * @param shape Original shape. + * @return Shape with reversed direction. + */ + public static Shape reverse(Shape shape) { + List segments = getSegments(shape); + + boolean closed = false; + Path2D reversed = + new Path2D.Double(Path2D.WIND_NON_ZERO, segments.size()); + ListIterator i = segments.listIterator(segments.size()); + while (i.hasPrevious()) { + PathSegment segment = i.previous(); + + if (segment.type == PathIterator.SEG_CLOSE) { + closed = true; + continue; + } + + if (reversed.getCurrentPoint() == null) { + reversed.moveTo( + segment.end.getX(), segment.end.getY()); + } + if (segment.type == PathIterator.SEG_LINETO) { + reversed.lineTo( + segment.start.getX(), segment.start.getY()); + } else if (segment.type == PathIterator.SEG_QUADTO) { + reversed.quadTo( + segment.coords[0], segment.coords[1], + segment.start.getX(), segment.start.getY()); + } else if (segment.type == PathIterator.SEG_CUBICTO) { + reversed.curveTo( + segment.coords[2], segment.coords[3], + segment.coords[0], segment.coords[1], + segment.start.getX(), segment.start.getY()); + } else if (segment.type == PathIterator.SEG_MOVETO) { + if (closed) { + reversed.closePath(); + closed = false; + } + } + } + + return reversed; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/GraphicsUtils.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/GraphicsUtils.java new file mode 100644 index 0000000..189a586 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/GraphicsUtils.java @@ -0,0 +1,448 @@ +/* + * GRAL: GRAphing Library for Java(R) + * + * (C) Copyright 2009-2019 Erich Seifert , + * Michael Seifert + * + * 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 . + */ +package org.xbib.graphics.graph.gral.util; + +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.font.FontRenderContext; +import java.awt.font.LineBreakMeasurer; +import java.awt.font.TextAttribute; +import java.awt.font.TextLayout; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Rectangle2D; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.util.LinkedList; +import java.util.List; + +/** + * Abstract class that contains utility functions for working with graphics. + * For example, this includes font handling or color space conversion. + */ +public abstract class GraphicsUtils { + /** Default font render context. */ + private static final FontRenderContext frc = new FontRenderContext(null, true, true); + + /** Constant for the CIE XYZ and CIE L*u*v* color spaces: (6/29)^3 **/ + private static final double CIE_EPSILON = 216.0/24389.0; + /** Constant for the CIE XYZ and CIE L*u*v* color spaces: (29/3)^3 **/ + private static final double CIE_KAPPA = 24389.0/27.0; + + /** Xr, Yr, Zr constants with D50 white point used for CIE XYZ to + CIE L*u*v* conversion **/ + private static final double[] XYZ_R_D50 = { + 0.964221, 1.000000, 0.825211 + }; + /** Precalculated u0 constant for CIE L*u*v* to CIE XYZ conversion. **/ + private static final double XYZ_R_D50_U0 = + 4.0*XYZ_R_D50[0]/(XYZ_R_D50[0] + 15.0*XYZ_R_D50[1] + 3.0*XYZ_R_D50[2]); + /** Precalculated v0 constant for CIE L*u*v* to CIE XYZ conversion. **/ + private static final double XYZ_R_D50_V0 = + 9.0*XYZ_R_D50[1]/(XYZ_R_D50[0] + 15.0*XYZ_R_D50[1] + 3.0*XYZ_R_D50[2]); + + /** sRGB to CIE XYZ conversion matrix. See + http://www.brucelindbloom.com/index.html?WorkingSpaceInfo.html#Specifications **/ + private static final double[] MATRIX_SRGB2XYZ_D50 = { + 0.436052025, 0.385081593, 0.143087414, + 0.222491598, 0.716886060, 0.060621486, + 0.013929122, 0.097097002, 0.714185470 + }; + + /** CIE XYZ to sRGB conversion matrix. See + http://www.brucelindbloom.com/index.html?WorkingSpaceInfo.html#Specifications **/ + private static final double[] MATRIX_XYZ2SRGB_D50 = { + 3.1338561, -1.6168667, -0.4906146, + -0.9787684, 1.9161415, 0.0334540, + 0.0719453, -0.2289914, 1.4052427 + }; + + /** + * Default constructor that prevents creation of class. + */ + protected GraphicsUtils() { + throw new UnsupportedOperationException(); + } + + /** + * Returns the outline for the specified text using the specified font and + * line width. The text may also contain line breaks ({@literal '\n'}). + * @param text Text to be displayed. + * @param font Font of the Text. + * @param wrappingWidth Maximum width of lines + * @param alignment Alignment of the text when it spans multiple lines. + * @return Shape of the text outline in the specified font. + */ + public static Shape getOutline(String text, Font font, float wrappingWidth, + double alignment) { + boolean wordWrap = true; + if (wrappingWidth <= 0f) { + wordWrap = false; + wrappingWidth = Float.MAX_VALUE; + } + + AttributedString string = new AttributedString(text); + string.addAttribute(TextAttribute.FONT, font); + AttributedCharacterIterator iterator = string.getIterator(); + LineBreakMeasurer measurer = new LineBreakMeasurer(iterator, frc); + + List lines = new LinkedList<>(); + while (measurer.getPosition() < text.length()) { + // Find out which character will be wrapped next + int nextBreakPos = measurer.nextOffset(wrappingWidth); + int lineBreakPos = text.indexOf('\n', measurer.getPosition()) + 1; + + int breakPos = nextBreakPos; + if (lineBreakPos > 0 && lineBreakPos < nextBreakPos) { + breakPos = lineBreakPos; + } + TextLayout line = measurer.nextLayout(wrappingWidth, breakPos, false); + lines.add(line); + } + + if (!wordWrap) { + // Determine the maximal line length + float advanceMax = 0f; + for (TextLayout line : lines) { + advanceMax = Math.max(line.getAdvance(), advanceMax); + } + wrappingWidth = advanceMax; + } + + AffineTransform txLinePos = new AffineTransform(); + Area outlineAllLines = null; + for (TextLayout line : lines) { + // Distribute the space that's left + double dx = alignment*(wrappingWidth - line.getAdvance()); + + // Move to baseline + txLinePos.translate(dx, line.getAscent()); + // Get the shape of the current line + Area outlineLine = new Area(line.getOutline(txLinePos)); + // Add the shape of the line to the shape + if (outlineAllLines == null) { + outlineAllLines = outlineLine; + } else { + outlineAllLines.add(outlineLine); + } + + // Move to next line + txLinePos.translate(-dx, line.getDescent() + line.getLeading()); + } + + return outlineAllLines; + } + + /** + * Fills a Shape with the specified Paint object. + * @param graphics Graphics to be painted into. + * @param shape Shape to be filled. + * @param paint Paint to be used. + * @param paintBounds Optional bounds describing the painted area. + */ + public static void fillPaintedShape(Graphics2D graphics, Shape shape, + Paint paint, Rectangle2D paintBounds) { + if (shape == null) { + return; + } + if (paintBounds == null) { + paintBounds = shape.getBounds2D(); + } + if (paintBounds.getWidth() == 0.0 || paintBounds.getHeight() == 0.0) { + return; + } + AffineTransform txOrig = graphics.getTransform(); + graphics.translate(paintBounds.getX(), paintBounds.getY()); + graphics.scale(paintBounds.getWidth(), paintBounds.getHeight()); + Paint paintOld = null; + if (paint != null) { + paintOld = graphics.getPaint(); + graphics.setPaint(paint); + } + AffineTransform tx = AffineTransform.getScaleInstance( + 1.0/paintBounds.getWidth(), 1.0/paintBounds.getHeight()); + tx.translate(-paintBounds.getX(), -paintBounds.getY()); + graphics.fill(tx.createTransformedShape(shape)); + if (paintOld != null) { + graphics.setPaint(paintOld); + } + graphics.setTransform(txOrig); + } + + /** + * Draws a filled Shape with the specified Paint object. + * @param graphics Graphics to be painted into. + * @param shape Shape to be filled. + * @param paint Paint to be used. + * @param paintBounds Optional bounds describing the painted area. + * @param stroke Stroke to be used for outlines. + */ + public static void drawPaintedShape(Graphics2D graphics, Shape shape, + Paint paint, Rectangle2D paintBounds, Stroke stroke) { + if (shape == null) { + return; + } + if (stroke == null) { + stroke = graphics.getStroke(); + } + shape = stroke.createStrokedShape(shape); + fillPaintedShape(graphics, shape, paint, paintBounds); + } + + /** + * Converts color components from the sRGB to the CIE XYZ color space. + * A D50 white point is assumed for the sRGB conversion. If the xyz + * array is {@code null}, a new one will be created with the same + * size as the rgb array. + * + * See http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html + * + * @param rgb Color components in the sRGB color space. + * @param xyz Optional array to store color components in the CIE XYZ color space. + * @return Color components in the CIE XYZ color space. + */ + public static double[] rgb2xyz(double[] rgb, double[] xyz) { + if (xyz == null) { + xyz = new double[rgb.length]; + } + + // Remove sRGB companding to make RGB components linear + double[] rgbLin = new double[rgb.length]; + for (int i = 0; i < rgb.length; i++) { + if (rgb[i] <= 0.04045) { + rgbLin[i] = rgb[i]/12.92; + } else { + rgbLin[i] = Math.pow((rgb[i] + 0.055)/1.055, 2.4); + } + } + + // Convert linear sRGB with D50 white point to CIE XYZ + for (int i = 0; i < xyz.length; i++) { + xyz[i] = MATRIX_SRGB2XYZ_D50[i*3 + 0]*rgbLin[0] + + MATRIX_SRGB2XYZ_D50[i*3 + 1]*rgbLin[1] + + MATRIX_SRGB2XYZ_D50[i*3 + 2]*rgbLin[2]; + } + + return xyz; + } + + /** + * Convert color components from the CIE L*u*v* to the CIE XYZ color space. + * If the xyz array is {@code null}, a new one will be created + * with the same size as the luv array. + * + * See http://www.brucelindbloom.com/index.html?Eqn_Luv_to_XYZ.html + * + * @param luv Color components in the CIE L*u*v* color space + * @param xyz Optional array to store color components in the CIE XYZ color + * space. + * @return Color components in the CIE XYZ color space. + */ + public static double[] luv2xyz(double[] luv, double[] xyz) { + if (xyz == null) { + xyz = new double[luv.length]; + } + + if (luv[0] > CIE_KAPPA*CIE_EPSILON) { + xyz[1] = (luv[0] + 16.0)/116.0; + xyz[1] = xyz[1]*xyz[1]*xyz[1]; + } else { + xyz[1] = luv[0]/CIE_KAPPA; + } + + double a = (luv[0] != 0.0 || luv[1] != 0.0) + ? ((52.0*luv[0])/(luv[1] + 13.0*luv[0]*XYZ_R_D50_U0) - 1.0)/3.0 + : 0.0; + double b = -5*xyz[1]; + double c = -1.0/3.0; + double d = (luv[0] != 0.0 || luv[2] != 0.0) + ? xyz[1]*((39.0*luv[0])/(luv[2] + 13.0*luv[0]*XYZ_R_D50_V0) - 5.0) + : 0.0; + + xyz[0] = !MathUtils.almostEqual(a, c, 1e-15) ? (d - b)/(a - c) : 0.0; + xyz[2] = xyz[0]*a + b; + + return xyz; + } + + /** + * Converts color components from the sRGB to the CIE XYZ color space. + * A D50 white point is assumed for the sRGB conversion. If the rgb + * array is {@code null}, a new one will be created with the same + * size as the xyz array. + * + * See http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html + * + * @param xyz Color components in the CIE XYZ color space. + * @param rgb Optional array for storing color components in the sRGB color + * space. + * @return Color components in the sRGB color space. + */ + public static double[] xyz2rgb(double[] xyz, double[] rgb) { + if (rgb == null) { + rgb = new double[xyz.length]; + } + + // XYZ to linear sRGB with D50 white point + for (int i = 0; i < xyz.length; i++) { + rgb[i] = MATRIX_XYZ2SRGB_D50[i*3 + 0]*xyz[0] + + MATRIX_XYZ2SRGB_D50[i*3 + 1]*xyz[1] + + MATRIX_XYZ2SRGB_D50[i*3 + 2]*xyz[2]; + } + + // Apply sRGB companding + for (int i = 0; i < rgb.length; i++) { + if (rgb[i] <= 0.0031308) { + rgb[i] = 12.92*rgb[i]; + } else { + rgb[i] = 1.055*Math.pow(rgb[i], 1.0/2.4) - 0.055; + } + } + + return rgb; + } + + /** + * Converts color components from the CIE XYZ to the CIE L*u*v* color + * space. If the luv array is {@code null}, a new one will be + * created with the same size as the xyz array. + * + * http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Luv.html + * + * @param xyz Color components in the CIE XYZ color space. + * @param luv Optional array for storing color components in the CIE L*u*v* + * color space. + * @return Color components in the CIE L*u*v* color space. + */ + public static double[] xyz2luv(double[] xyz, double[] luv) { + double tmp = xyz[0] + 15.0*xyz[1] + 3.0*xyz[2]; + if (tmp == 0.0) { + tmp = 1.0; + } + double u1 = 4.0*xyz[0]/tmp; + double v1 = 9.0*xyz[1]/tmp; + + // Relative luminance + double yr = xyz[1]/XYZ_R_D50[1]; + double ur = 4.0*XYZ_R_D50[0]/(XYZ_R_D50[0] + 15.0*XYZ_R_D50[1] + 3.0*XYZ_R_D50[2]); + double vr = 9.0*XYZ_R_D50[1]/(XYZ_R_D50[0] + 15.0*XYZ_R_D50[1] + 3.0*XYZ_R_D50[2]); + + // Mapping relative luminance to lightness + if (luv == null) { + luv = new double[xyz.length]; + } + if (yr > CIE_EPSILON) { + luv[0] = 116.0*Math.pow(yr, 1.0/3.0) - 16.0; + } else { + luv[0] = CIE_KAPPA*yr; + } + luv[1] = 13.0*luv[0]*(u1 - ur); + luv[2] = 13.0*luv[0]*(v1 - vr); + + return luv; + } + + /** + * Converts color components from the CIE L*u*v* to the sRGB color space. + * A D50 white point is assumed for the sRGB conversion. If the luv + * array is {@code null}, a new one will be created with the same + * size as the rgb array. + * + * @param rgb Color components in the sRGB color space. + * @param luv Optional array for storing color components in the CIE L*u*v* + * color space. + * @return Color components in the CIE L*u*v* color space. + */ + public static double[] rgb2luv(double[] rgb, double[] luv) { + double[] xyz = rgb2xyz(rgb, null); + return xyz2luv(xyz, luv); + } + + /** + * Converts color components from the CIE L*u*v* to the sRGB color space. + * A D50 white point is assumed for the sRGB conversion. If the rgb + * array is {@code null}, a new one will be created with the same size + * as the luv array. + * + * @param luv Color components in the CIE L*u*v* color space. + * @param rgb Optional array for storing color components in the sRGB color + * space. + * @return Color components in sRGB color space. + */ + public static double[] luv2rgb(double[] luv, double[] rgb) { + double[] xyz = luv2xyz(luv, null); + return xyz2rgb(xyz, rgb); + } + + /** + * Linearly blends two colors with a defined weight. + * @param color1 First color. + * @param color2 Second color. + * @param weight Weighting factor in the range 0 to 1 (0 means color1, 1 means second color) + * @return New blended color + */ + public static Color blend(Color color1, Color color2, double weight) { + double w2 = MathUtils.limit(weight, 0.0, 1.0); + double w1 = 1.0 - w2; + int r = (int) Math.round(w1*color1.getRed() + w2*color2.getRed()); + int g = (int) Math.round(w1*color1.getGreen() + w2*color2.getGreen()); + int b = (int) Math.round(w1*color1.getBlue() + w2*color2.getBlue()); + int a = (int) Math.round(w1*color1.getAlpha() + w2*color2.getAlpha()); + return new Color(r, g, b, a); + } + + /** + * Creates a new color with the same color components but a different + * alpha value. + * @param color Original color. + * @param alpha Alpha value for new color. + * @return New color with specified alpha value. + */ + public static Color deriveWithAlpha(Color color, int alpha) { + return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); + } + + /** + * Creates a new darker version of a color by blending it with black. The + * derived color has the same alpha value as the original color. + * @param color Original color. + * @return Darker color with same alpha value. + */ + public static Color deriveDarker(Color color) { + return deriveWithAlpha(blend(color, Color.BLACK, 0.5), color.getAlpha()); + } + + /** + * Creates a new brighter version of a color by blending it with white. The + * derived color has the same alpha value as the original color. + * @param color Original color. + * @return Brighter color with same alpha value. + */ + public static Color deriveBrighter(Color color) { + return deriveWithAlpha(blend(color, Color.WHITE, 0.5), color.getAlpha()); + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/HaltonSequence.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/HaltonSequence.java new file mode 100644 index 0000000..21077c0 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/HaltonSequence.java @@ -0,0 +1,96 @@ +/* + * GRAL: GRAphing Library for Java(R) + * + * (C) Copyright 2009-2019 Erich Seifert , + * Michael Seifert + * + * 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 . + */ +package org.xbib.graphics.graph.gral.util; + +import java.io.Serializable; +import java.util.Iterator; + +/** + * Class that calculates the values of the Halton sequence. + */ +public class HaltonSequence implements Iterator, Serializable { + /** Version id for serialization. */ + private static final long serialVersionUID = 7466395251522942013L; + + /** Base. */ + private final int base; + /** Current count. */ + private long c; + + /** + * Creates a new HaltonSequence object to the base of two. + */ + public HaltonSequence() { + this(2); + } + + /** + * Creates a new instance with the specified base. + * @param base Base value. + */ + public HaltonSequence(int base) { + this.base = base; + } + + /** + * Returns whether the iteration has more elements. This means it 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 true; + } + + /** + * Returns the next element in the iteration. + * @return the next element in the iteration. + */ + public Double next() { + long i, digit; + double h, step; + + if (++c == Long.MAX_VALUE) { + c = 0; + } + + i = c; + h = 0.0; + step = 1.0 / base; + + while (i > 0) { + digit = i % base; + h += digit * step; + i = (i - digit) / base; + step /= base; + } + + return h; + } + + /** + * Stub method to fulfill {@code Iterator} interface. + */ + public void remove() { + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/Iterables.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/Iterables.java new file mode 100644 index 0000000..27ea140 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/Iterables.java @@ -0,0 +1,67 @@ +package org.xbib.graphics.graph.gral.util; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +public abstract class Iterables { + private static class ConcatenationIterable implements Iterable { + private final Iterable> inputIterables; + + public ConcatenationIterable(Iterable> inputIterables) { + this.inputIterables = inputIterables; + } + + @SuppressWarnings({"unchecked","rawtypes"}) + @Override + public Iterator iterator() { + List> iterators = new LinkedList<>(); + for (Iterable iterable : inputIterables) { + iterators.add(iterable.iterator()); + } + return new ConcatenationIterator(iterators.toArray(new Iterator[0])); + } + } + + @SuppressWarnings("unchecked") + public static Iterable concatenate(Iterable... iterables) { + return new ConcatenationIterable<>(Arrays.asList(iterables)); + } + + private static class LengthIterator implements Iterator { + private final Iterator inputIterator; + private final int maxElementCount; + private int retrievedElementCount; + + public LengthIterator(Iterator inputIterator, int elementCount) { + this.inputIterator = inputIterator; + this.maxElementCount = elementCount; + } + + @Override + public boolean hasNext() { + return retrievedElementCount < maxElementCount && inputIterator.hasNext(); + } + + @Override + public T next() { + retrievedElementCount++; + return inputIterator.next(); + } + + @Override + public void remove() { + inputIterator.remove(); + } + } + + public static Iterable take(final Iterable iterable, final int elementCount) { + return new Iterable() { + @Override + public Iterator iterator() { + return new LengthIterator<>(iterable.iterator(), elementCount); + } + }; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/MathUtils.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/MathUtils.java new file mode 100644 index 0000000..fccb956 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/MathUtils.java @@ -0,0 +1,425 @@ +/* + * GRAL: GRAphing Library for Java(R) + * + * (C) Copyright 2009-2019 Erich Seifert , + * Michael Seifert + * + * 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 . + */ +package org.xbib.graphics.graph.gral.util; + +import java.util.List; +import java.util.Random; + +/** + * Abstract class that provides utility functions which are useful for + * mathematical calculations. + */ +public abstract class MathUtils { + /** Instance for random values. */ + private static final Random RANDOM = new Random(); + + /** + * Default constructor that prevents creation of class. + */ + private MathUtils() { + throw new UnsupportedOperationException(); + } + + /** + * Check whether two floating point values match with a given precision. + * @param a First value + * @param b Second value + * @param delta Precision + * @return {@code true} if the difference of a and b is + * smaller or equal than delta, otherwise {@code false} + */ + public static boolean almostEqual(double a, double b, double delta) { + return Math.abs(a - b) <= delta; + } + + /** + * Mathematically rounds a number with a defined precision. + * @param a Value + * @param precision Precision + * @return Rounded value + */ + public static double round(double a, double precision) { + if (precision == 0.0) { + return 0.0; + } + return Math.round(a/precision) * precision; + } + + /** + * Returns a rounded number smaller than {@code a} with a defined + * precision. + * @param a Value + * @param precision Precision + * @return Rounded value + */ + public static double floor(double a, double precision) { + if (precision == 0.0) { + return 0.0; + } + return Math.floor(a/precision) * precision; + } + + /** + * Returns a rounded number larger than {@code a} with a defined + * precision. + * @param a Value + * @param precision Precision + * @return Rounded value + */ + public static double ceil(double a, double precision) { + if (precision == 0.0) { + return 0.0; + } + return Math.ceil(a/precision) * precision; + } + + /** + * Perform a binary search on a sorted array {@code a} to find the + * element with the nearest element to {@code key}. + * @param a Array with ascending values + * @param key Pivot value + * @return Index of the array element whose value is nearly or exactly + * {@code key} + */ + public static int binarySearch(double[] a, double key) { + int l = 0; + int h = a.length - 1; + int i; + do { + i = (int)(((long)l + (long)h) / 2L); + if (key > a[i]) { + l = i + 1; + } else if (key < a[i]) { + h = i - 1; + } else { + return i; + } + } while (l <= h); + return i; + } + + /** + * Perform a binary search on a sorted array {@code a} to find the + * element with the smallest distance to {@code key}. The returned + * element's value is always less than or equal to {@code key}. + * @param a Array with ascending values + * @param key Pivot value + * @return Index of the array element whose value is less than or equal to + * {@code key} + */ + public static int binarySearchFloor(double[] a, double key) { + if (a.length == 0) { + return -1; + } + int i = binarySearch(a, key); + if (i >= 0 && a[i] > key) { + i--; + } + return i; + } + + /** + * Perform a binary search on a sorted array {@code a} to find the + * element with the smallest distance to {@code key}. The returned + * element's value is always greater than or equal to {@code key}. + * @param a Array with ascending values + * @param key Pivot value + * @return Index of the array element whose value is greater than or equal + * to {@code key} + */ + public static int binarySearchCeil(double[] a, double key) { + if (a.length == 0) { + return -1; + } + int i = binarySearch(a, key); + if (i >= 0 && a[i] < key) { + i++; + } + return i; + } + + /** + * Clamps a number object to specified limits: if {@code value} is + * greater than {@code max} then {@code max} will be returned. + * If {@code value} is greater than {@code min} then + * {@code min} will be returned. + * @param Numeric data type + * @param value Double value to be clamped + * @param min Minimum + * @param max Maximum + * @return Clamped value + */ + public static T limit(T value, T min, T max) { + if (value.doubleValue() > max.doubleValue()) { + return max; + } + if (value.doubleValue() < min.doubleValue()) { + return min; + } + return value; + } + + /** + * Clamps a double number to specified limits: if {@code value} is + * greater than {@code max} then {@code max} will be returned. + * If {@code value} is greater than {@code min} then + * {@code min} will be returned. + * @param value Double value to be clamped + * @param min Minimum + * @param max Maximum + * @return Clamped value + */ + public static double limit(double value, double min, double max) { + if (value > max) { + return max; + } + if (value < min) { + return min; + } + return value; + } + + /** + * Clamps a float number to specified limits: if {@code value} is + * greater than {@code max} then {@code max} will be returned. + * If {@code value} is greater than {@code min} then + * {@code min} will be returned. + * @param value Float value to be clamped + * @param min Minimum + * @param max Maximum + * @return Clamped value + */ + public static float limit(float value, float min, float max) { + if (value > max) { + return max; + } + if (value < min) { + return min; + } + return value; + } + + /** + * Clamps a integer number to specified limits: if {@code value} is + * greater than {@code max} then {@code max} will be returned. + * If {@code value} is greater than {@code min} then + * {@code min} will be returned. + * @param value Integer value to be clamped + * @param min Minimum + * @param max Maximum + * @return Clamped value + */ + public static int limit(int value, int min, int max) { + if (value > max) { + return max; + } + if (value < min) { + return min; + } + return value; + } + + /** + *

Perform a randomized search on an unsorted array {@code a} to + * find the ith smallest element. The array contents are be modified + * during the operation!

+ *

See Cormen et al. (2001): Introduction to Algorithms. 2nd edition. + * p. 186

+ * @param Data type of the array + * @param a Unsorted array + * @param lower Starting index + * @param upper End index + * @param i Smallness rank of value to search starting at 1 + * @return Index of the element that is the ith smallest in array + * a + */ + public static > int randomizedSelect(List a, + int lower, int upper, int i) { + if (a.isEmpty()) { + return -1; + } + if (lower == upper) { + return lower; + } + int pivot = randomizedPartition(a, lower, upper); + int lowerPartitionElementCount = pivot - lower + 1; + if (i == lowerPartitionElementCount) { + return pivot; + } else if (i < lowerPartitionElementCount) { + return randomizedSelect(a, lower, pivot - 1, i); + } else { + return randomizedSelect(a, pivot + 1, upper, i - lowerPartitionElementCount); + } + } + + /** + * Rearranges an array in two partitions using random sampling. + * The array is permuted so that the elements of the lower partition + * are always smaller than those of the upper partition. + * @param Data type of the array + * @param a Unsorted array + * @param lower Starting index + * @param upper End index + * @return Pivot point of the partitioned array + * @see "Cormen et al. (2001): Introduction to Algorithms. 2nd Edition, page 154" + */ + private static > int randomizedPartition( + List a, int lower, int upper) { + int i = lower + RANDOM.nextInt(upper - lower + 1); + exchange(a, upper, i); + return partition(a, lower, upper); + } + + /** + * Performs QuickSort partitioning: Rearranges an array in two partitions. + * The array is permuted so that the elements of the lower partition are + * always smaller than those of the upper partition. + * @param Data type of the array + * @param a Unsorted array + * @param lower Starting index + * @param upper End index + * @return Pivot point of the partitioned array + * @see "Cormen et al. (2001): Introduction to Algorithms. 2nd Edition, page 146" + */ + private static > int partition( + List a, int lower, int upper) { + T x = a.get(upper); + int i = lower - 1; + for (int j = lower; j < upper; j++) { + if (a.get(j).compareTo(x) <= 0) { + i++; + exchange(a, i, j); + } + } + exchange(a, i + 1, upper); + return i + 1; + } + + /** + * Swaps two elements at indexes {@code i1} and {@code i2} of an + * array in-place. + * @param Data type of the array + * @param a Array + * @param i1 First element index + * @param i2 Second element index + */ + private static void exchange(List a, int i1, int i2) { + T tmp = a.get(i2); + a.set(i2, a.get(i1)); + a.set(i1, tmp); + } + + /** + *

Returns the magnitude of the specified number. Example for magnitude + * base 10:

+ * + * + * + * + * + * + *
Examples of number and corresponding magnitude
-0.05 -0.01
0.05 0.01
3.14 1.00
54.32 10.00
123.45100.00
+ * @param base Base. + * @param n Number. + * @return Magnitude. + */ + public static double magnitude(double base, double n) { + double logN = Math.log(Math.abs(n))/Math.log(base); + return Math.signum(n) * Math.pow(base, Math.floor(logN)); + } + + /** + *

Utility method used to calculate arbitrary quantiles from a sorted + * list of values. Currently only one method is implemented: the default + * method that is used by R (method 7). The list must be sorted.

+ *

For more information see:

+ * + * @param values Data values. + * @param q Quantile in range [0, 1] + * @return Quantile value + */ + public static double quantile(List values, double q) { + // R type 7 parameters + double a = 1.0, b = -1.0, c = 0.0, d = 1.0; + // Number of samples + int n = values.size(); + + double x = a + (n + b) * q - 1.0; + double xInt = (int) x; + double xFrac = x - xInt; + + if (xInt < 0) { + return values.get(0); + } else if (xInt >= n) { + return values.get(n - 1); + } + + int i = (int) xInt; + if (xFrac == 0) { + return values.get(i); + } + return values.get(i) + (values.get(i + 1) - values.get(i))*(c + d*xFrac); + } + + /** + * Returns whether a specified {@code java.lang.Number} object can be + * used for calculations. {@code null} values, {@code NaN} values + * or infinite values are considered as non-calculatable. + * @param n Number object. + * @return whether {@code n} can be used for calculations. + */ + public static boolean isCalculatable(Number n) { + return (n != null) && isCalculatable(n.doubleValue()); + } + + /** + * Returns whether a specified double can be used for calculations. + * {@code NaN} values or infinite values are considered + * non-calculatable. + * @param n double value + * @return whether {@code n} can be used for calculations. + */ + public static boolean isCalculatable(double n) { + return !Double.isNaN(n) && !Double.isInfinite(n); + } + + /** + * Converts an angle in degrees so that it lies between 0.0 and 360.0. + * @param angle Arbitrary angle in degrees. + * @return Angle between 0.0 and 360.0. + */ + public static double normalizeDegrees(double angle) { + while (angle < 0.0) { + angle += 360.0; + } + return angle%360.0; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/Messages.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/Messages.java new file mode 100644 index 0000000..bbb1601 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/Messages.java @@ -0,0 +1,59 @@ +/* + * GRAL: GRAphing Library for Java(R) + * + * (C) Copyright 2009-2019 Erich Seifert , + * Michael Seifert + * + * 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 . + */ +package org.xbib.graphics.graph.gral.util; + +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +/** + * Singleton class that globally provides translated message texts. + */ +public abstract class Messages { + /** Name of resource bundle that contains message texts. */ + private static final String BUNDLE_NAME = "messages"; //$NON-NLS-1$ + + /** Resource bundle that contains message texts. */ + private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle + .getBundle(BUNDLE_NAME); + + /** + * Private constructor. + */ + private Messages() { + } + + /** + * Returns a message text that is determined by the specified key. + * A replacement text generated from the key is returned if the message + * cannot be found. + * @param key Key string that identifies the message + * @return Translated message text, or default key if the message cannot + * be found. + */ + public static String getString(String key) { + try { + return RESOURCE_BUNDLE.getString(key); + } catch (MissingResourceException e) { + return '!' + key + '!'; + } + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/PointND.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/PointND.java new file mode 100644 index 0000000..02635a9 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/PointND.java @@ -0,0 +1,157 @@ +/* + * GRAL: GRAphing Library for Java(R) + * + * (C) Copyright 2009-2019 Erich Seifert , + * Michael Seifert + * + * 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 . + */ +package org.xbib.graphics.graph.gral.util; + +import java.awt.geom.Point2D; +import java.io.Serializable; +import java.text.MessageFormat; +import java.util.Arrays; + +/** + * Class for storing n-dimensional points. + * @param Data type of the coordinates. + */ +public class PointND implements Serializable { + /** Version id for serialization. */ + private static final long serialVersionUID = 3552680202450906771L; + + /** Constant for accessing x-coordinate. */ + public static final int X = 0; + /** Constant for accessing y-coordinate. */ + public static final int Y = 1; + /** Constant for accessing z-coordinate. */ + public static final int Z = 2; + + /** Coordinates along the axes that describe this point. */ + private final T[] coordinates; + + /** + * Constructor that initializes the point with a list of coordinates. + * @param coordinates Coordinate values. + */ + @SuppressWarnings("unchecked") + public PointND(T... coordinates) { + this.coordinates = Arrays.copyOf(coordinates, coordinates.length); + } + + /** + * Returns the number of dimensions. + * @return Number of dimensions. + */ + public int getDimensions() { + return coordinates.length; + } + + /** + * Returns the value of a specified dimension. + * @param dimension Dimension. + * @return Coordinate value. + */ + public T get(int dimension) { + return coordinates[dimension]; + } + + /** + * Sets the value of a specified dimension. + * @param dimension Dimension. + * @param coordinate New coordinate value. + */ + public void set(int dimension, T coordinate) { + coordinates[dimension] = coordinate; + } + + /** + * Sets all coordinate values at once. + * @param coordinates Coordinate values. + */ + @SuppressWarnings("unchecked") + public void setLocation(T... coordinates) { + if (getDimensions() != coordinates.length) { + throw new IllegalArgumentException(MessageFormat.format( + "Wrong number of dimensions: Expected {0,number,integer} values, got {1,number,integer}.", //$NON-NLS-1$ + getDimensions(), coordinates.length)); + } + System.arraycopy(coordinates, 0, this.coordinates, 0, getDimensions()); + } + + /** + * Creates a two-dimensional point from the specified dimensions. + * @param dimX Dimension for x coordinate. + * @param dimY Dimension for y coordinate. + * @return Two-dimensional point. + */ + public Point2D getPoint2D(int dimX, int dimY) { + if (getDimensions() < 2) { + throw new ArrayIndexOutOfBoundsException( + "Can't create two-dimensional point from " + //$NON-NLS-1$ + getDimensions() + "D data."); //$NON-NLS-1$ + } + return new Point2D.Double( + get(dimX).doubleValue(), get(dimY).doubleValue()); + } + + /** + * Creates a two-dimensional point from dimensions 0 and 1. + * @return Two-dimensional point. + */ + public Point2D getPoint2D() { + return getPoint2D(X, Y); + } + + @Override + public String toString() { + return getClass().getName() + Arrays.deepToString(coordinates); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PointND)) { + return false; + } + PointND p = (PointND) obj; + if (getDimensions() != p.getDimensions()) { + return false; + } + for (int dim = 0; dim < coordinates.length; dim++) { + Number dimA = get(dim); + Number dimB = p.get(dim); + if (dimA != null && dimB != null) { + if (!dimA.equals(dimB)) { + return false; + } + } else if (dimA != dimB) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + int hashCode = 0; + for (T coordinate : coordinates) { + hashCode ^= coordinate.hashCode(); + } + return hashCode; + } + +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/SortedList.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/SortedList.java new file mode 100644 index 0000000..7d671c0 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/SortedList.java @@ -0,0 +1,104 @@ +/* + * GRAL: GRAphing Library for Java(R) + * + * (C) Copyright 2009-2019 Erich Seifert , + * Michael Seifert + * + * 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 . + */ +package org.xbib.graphics.graph.gral.util; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Resizable implementation of the {@code List} interface that automatically + * sorts all values. It implements the methods {@code get}, {@code size}, + * {@code add}, and {@code size}. The stored elements must implement the + * interface {@code Comparable}. + * @param Data type of stored elements. + */ +public class SortedList> extends AbstractList { + private final List elements; + + /** + * Constructs an empty list with the specified initial capacity. + * @param initialCapacity Initial capacity of the list. + */ + public SortedList(int initialCapacity) { + elements = new ArrayList<>(initialCapacity); + } + + /** + * Constructs a list containing the elements of the specified collection. + * @param c Collection whose elements are to be added. + */ + public SortedList(Collection c) { + this(c.size()); + for (T e : c) { + add(e); + } + } + + /** + * Constructs an empty list with an initial capacity of ten. + */ + public SortedList() { + this(10); + } + + @Override + public T get(int index) { + return elements.get(index); + } + + @Override + public int size() { + return elements.size(); + } + + @Override + public boolean add(T e) { + if (elements.isEmpty()) { + elements.add(e); + return true; + } + int index = Collections.binarySearch(elements, e); + if (index < 0) { + index = -index - 1; + } + elements.add(index, e); + return true; + } + + @Override + public T remove(int index) { + return elements.remove(index); + } + + @Override + @SuppressWarnings("unchecked") + public int indexOf(Object o) { + try { + return Collections.binarySearch(elements, (T) o); + } catch (NullPointerException | ClassCastException e) { + return -1; + } + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/StatefulTokenizer.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/StatefulTokenizer.java new file mode 100644 index 0000000..5b61d14 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/StatefulTokenizer.java @@ -0,0 +1,303 @@ +/* + * GRAL: GRAphing Library for Java(R) + * + * (C) Copyright 2009-2019 Erich Seifert , + * Michael Seifert + * + * 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 . + */ +package org.xbib.graphics.graph.gral.util; + +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 java.util.Stack; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A tokenizing parser that can analyzes a string using different sets of + * regular expression based parsing rules and produces a list of tokens. + * + * The class is intended to be sub-classed to implement new grammars. + * Different sets of rules can be defined with the method + * {@link #putRules(String, Rule[])}, e.g. for string processing. Each rule + * in a set produces one token with a type name and it can switch to another + * state or switch back to the previous state with the special state + * {@code "#pop"}. The is a list of tokens with arbitrary type. + * + * The list of produced tokens can be filtered: tokens of same type can be + * joined by adding the type with {@link #addJoinedType(Object)} and tokens can + * be omitted from the result for easier post-processing by adding with + * {@link #addIgnoredType(Object)}. + */ +public abstract class StatefulTokenizer { + /** The name of the initial state. */ + protected static final String INITIAL_STATE = ""; + + /** Token types that should be joined when adjacent tokens are found. */ + private final Set joinedTypes; + /** Token types that shouldn't be added to the output. */ + private final Set ignoredTypes; + /** Rules for specific states. */ + private final Map grammar; + + /** + * A token that designates a certain section of a text input. The absolute + * position within the input stream as well as the relevant text are stored + * for later processing. + */ + public static class Token { + /** Absolute position where the token started in the input stream. */ + private final int start; + /** Absolute position where the token ended in the input stream. */ + private int end; + /** Type of the token as defined by the corresponding rule. */ + private final Object type; + /** The relevant content from the input stream. Its lengths can differ + from length of the token. */ + private final StringBuilder content; + + /** + * Initializes a new token with absolute start and end position, a type + * and text content. + * @param start Absolute position where the token started in the input stream. + * @param end Absolute position where the token ended in the input stream. + * @param type Type of the token as defined by the corresponding rule. + * @param content The relevant text content from the input stream. + */ + public Token(int start, int end, Object type, String content) { + this.content = new StringBuilder(); + this.start = start; + this.end = end; + this.type = type; + this.content.append(content); + } + + /** + * Joins two tokens by appending the contents of another token to this + * token. + * @param t Another token that should be appended to this token + */ + public void append(Token t) { + content.append(t.content); + end = t.end; + } + + /** + * Returns the absolute position where the token starts in the input + * stream. + * @return Absolute position in the input stream. + */ + public int getStart() { + return start; + } + + /** + * Returns the absolute position where the token ends in the input + * stream. + * @return Absolute position in the input stream. + */ + public int getEnd() { + return end; + } + + /** + * Returns the type of the token. + * @return Type of the token + */ + public Object getType() { + return type; + } + + /** + * Returns the content of the token. + * @return Content of the token + */ + public String getContent() { + return content.toString(); + } + + @Override + public String toString() { + return String.format("%s[start=%d, end=%d, type=%s, content=\"%s\"]", + getClass().getSimpleName(), getStart(), getEnd(), getType(), getContent()); + } + } + + /** + * A regular expression based rule for building a parsing grammar. + * It stores a regular expression pattern for matching input data, a type + * for tokens generated by this rule, and an optional name of a grammar + * state that should be activated. + * + * The method {@link #getToken(String,int)} can be used to process input + * data: If the rule matches a token is returned, otherwise {@code null} + * will be returned. + */ + protected static class Rule { + /** Compiled regular expression for analyzing the input stream. */ + private final Pattern pattern; + /** Type of the tokens generated by this rule. */ + private final Object tokenType; + /** The grammar state that be used next to analyze the input data if + the rule matched. */ + private final String nextState; + + /** + * Initializes a new instance with the specified regular expression + * pattern, a type for generated tokens, and a name of a grammar state + * that should be triggered. + * @param pattern A regular expression pattern string. + * @param tokenType The type for the tokens generated by this rule. + * @param nextState The grammar state that should be used next to + * analyze the input data if the rule matched. + */ + public Rule(String pattern, Object tokenType, String nextState) { + this.pattern = Pattern.compile(pattern); + this.tokenType = tokenType; + this.nextState = nextState; + } + + /** + * Initializes a new instance with the specified regular expression + * pattern, a type for generated tokens. + * @param pattern A regular expression pattern string. + * @param tokenType The type for the tokens generated by this rule. + */ + public Rule(String pattern, Object tokenType) { + this(pattern, tokenType, null); + } + + /** + * Analyzes the specified input data starting at the given position + * and returns a token with the defined type, the content matched by + * the regular expression if the rule matches. If the rule doesn't + * match {@code null} will be returned. + * @param data Input data. + * @param pos Position to start looking for a match. + * @return A token with information about the matched section of the + * input data, or {@code null} if the rule didn't match. + */ + public Token getToken(String data, int pos) { + Matcher m = pattern.matcher(data); + m.region(pos, data.length()); + if (!m.lookingAt()) { + return null; + } + String content = (m.groupCount() > 0) ? m.group(1) : m.group(); + return new Token(m.start(), m.end(), tokenType, content); + } + } + + /** + * Initializes the internal data structures of a new instance. + */ + protected StatefulTokenizer() { + joinedTypes = new HashSet<>(); + ignoredTypes = new HashSet<>(); + grammar = new HashMap<>(); + } + + /** + * Adds a token type to the set of tokens that should get joined in the + * tokenizer output. + * @param tokenType Type of the tokens that should be joined. + */ + protected void addJoinedType(Object tokenType) { + joinedTypes.add(tokenType); + } + + /** + * Adds a token type to the set of tokens that should be ignored in the + * tokenizer output. + * @param tokenType Type of the tokens that should be ignored. + */ + protected void addIgnoredType(Object tokenType) { + ignoredTypes.add(tokenType); + } + + /** + * Sets the rules for the initial state in the grammar. + * @param rules A sequence or an array with rules to be added. + */ + protected void putRules(Rule... rules) { + putRules(INITIAL_STATE, rules); + } + + /** + * Sets the rules for the specified state in the grammar. + * @param name A unique name to identify the rule set. + * @param rules A sequence or an array with rules to be added. + */ + protected void putRules(String name, Rule... rules) { + grammar.put(name, rules); + } + + /** + * Analyzes the specified input string using different sets of rules and + * returns a list of token objects describing the content structure. + * @param data Input string. + * @return List of tokens. + */ + public List tokenize(String data) { + LinkedList tokens = new LinkedList<>(); + + Stack states = new Stack<>(); + states.push(INITIAL_STATE); + + int pos = 0; + Token tokenCur = null; + while (pos < data.length() && !states.isEmpty()) { + String state = states.peek(); + Rule[] rules = grammar.get(state); + for (Rule rule : rules) { + Token token = rule.getToken(data, pos); + if (token == null) { + continue; + } + + if (tokenCur != null && tokenCur.type.equals(token.type) && + joinedTypes.contains(tokenCur.type)) { + tokenCur.append(token); + } else { + if (tokenCur != null && + !ignoredTypes.contains(tokenCur.type)) { + tokens.add(tokenCur); + } + tokenCur = token; + } + + pos = token.end; + + if ("#pop".equals(rule.nextState)) { + states.pop(); + } else if (rule.nextState != null) { + states.push(rule.nextState); + } + break; + } + } + if (tokenCur != null && !ignoredTypes.contains(tokenCur.type)) { + tokens.add(tokenCur); + } + + return tokens; + } +} diff --git a/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/WindowIterator.java b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/WindowIterator.java new file mode 100644 index 0000000..284f8b7 --- /dev/null +++ b/graphics-graph-gral/src/main/java/org/xbib/graphics/graph/gral/util/WindowIterator.java @@ -0,0 +1,60 @@ +/* + * GRAL: GRAphing Library for Java(R) + * + * (C) Copyright 2009-2019 Erich Seifert , + * Michael Seifert + * + * 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 . + */ +package org.xbib.graphics.graph.gral.util; + +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +public class WindowIterator implements Iterator> { + private final Iterator iterator; + private final Deque window; + + public WindowIterator(Iterator iterator, int windowSize) { + this.iterator = iterator; + + this.window = new LinkedList<>(); + // Before the first call of Iterator.next(), the window contains an empty slot + window.add(null); + // ... and the other cells of the window are filled with values from the source iterator + for (int windowIndex = 0; windowIndex < windowSize - 1; windowIndex++) { + window.add(iterator.next()); + } + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public List next() { + window.removeFirst(); + window.add(iterator.next()); + return new LinkedList<>(window); + } + + @Override + public void remove() { + } +} diff --git a/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/GraphicsTests.java b/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/GraphicsTests.java new file mode 100644 index 0000000..9dfcb3d --- /dev/null +++ b/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/GraphicsTests.java @@ -0,0 +1,20 @@ +package org.xbib.graphics.graph.gral.test.graphics; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.xbib.graphics.graph.gral.test.graphics.layout.LayoutTests; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + // Tests for subpackages + LayoutTests.class, + // Tests for classes + //DrawingContextTest.class, + //DrawableTest.class, + //ContainerTest.class, + //LabelTest.class, + //Dimension2DTest.class, + //Insets2DTest.class +}) +public class GraphicsTests { +} diff --git a/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/AbstractLayoutTest.java b/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/AbstractLayoutTest.java new file mode 100644 index 0000000..b38a23d --- /dev/null +++ b/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/AbstractLayoutTest.java @@ -0,0 +1,40 @@ +package org.xbib.graphics.graph.gral.test.graphics.layout; + +import static org.junit.Assert.assertEquals; + +import java.awt.geom.Dimension2D; + +import org.xbib.graphics.graph.gral.graphics.Container; +import org.junit.Test; + +import org.xbib.graphics.graph.gral.graphics.layout.AbstractLayout; + + +public class AbstractLayoutTest { + private static final double DELTA = 1e-15; + private static final double GAP_H = 5.0; + private static final double GAP_V = 10.0; + + private static class MockAbstractLayout extends AbstractLayout { + + public MockAbstractLayout(double gapX, double gapY) { + super(gapX, gapY); + } + + @Override + public void layout(Container container) { + } + + @Override + public Dimension2D getPreferredSize(Container container) { + return new org.xbib.graphics.graph.gral.graphics.Dimension2D.Double(); + } + } + + @Test + public void testCreate() { + AbstractLayout gapped = new MockAbstractLayout(GAP_H, GAP_V); + assertEquals(GAP_H, gapped.getGapX(), DELTA); + assertEquals(GAP_V, gapped.getGapY(), DELTA); + } +} diff --git a/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/AbstractOrientedLayoutTest.java b/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/AbstractOrientedLayoutTest.java new file mode 100644 index 0000000..19b2d85 --- /dev/null +++ b/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/AbstractOrientedLayoutTest.java @@ -0,0 +1,40 @@ +package org.xbib.graphics.graph.gral.test.graphics.layout; + +import static org.junit.Assert.assertEquals; + +import java.awt.geom.Dimension2D; + +import org.xbib.graphics.graph.gral.graphics.Container; +import org.junit.Test; + +import org.xbib.graphics.graph.gral.graphics.Orientation; +import org.xbib.graphics.graph.gral.graphics.layout.AbstractOrientedLayout; + +public class AbstractOrientedLayoutTest { + private static final double DELTA = 1e-15; + private static final double GAP_H = 5.0; + private static final double GAP_V = 10.0; + + private static class MockAbstractOrientedLayout extends AbstractOrientedLayout { + + public MockAbstractOrientedLayout(Orientation orientation, double gapX, double gapY) { + super(orientation, gapX, gapY); + } + + @Override + public void layout(Container container) { + } + + @Override + public Dimension2D getPreferredSize(Container container) { + return new org.xbib.graphics.graph.gral.graphics.Dimension2D.Double(); + } + } + + @Test + public void testCreate() { + AbstractOrientedLayout gapped = new MockAbstractOrientedLayout(Orientation.HORIZONTAL, GAP_H, GAP_V); + assertEquals(GAP_H, gapped.getGapX(), DELTA); + assertEquals(GAP_V, gapped.getGapY(), DELTA); + } +} diff --git a/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/EdgeLayoutTest.java b/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/EdgeLayoutTest.java new file mode 100644 index 0000000..69d4052 --- /dev/null +++ b/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/EdgeLayoutTest.java @@ -0,0 +1,116 @@ +package org.xbib.graphics.graph.gral.test.graphics.layout; + +import static org.junit.Assert.assertEquals; + +import java.awt.geom.Dimension2D; +import java.awt.geom.Rectangle2D; + +import org.xbib.graphics.graph.gral.graphics.AbstractDrawable; +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.junit.Before; +import org.junit.Test; + +import org.xbib.graphics.graph.gral.graphics.Location; +import org.xbib.graphics.graph.gral.graphics.layout.EdgeLayout; + +public class EdgeLayoutTest { + private static final double DELTA = 1e-15; + private static final double GAP_H = 5.0; + private static final double GAP_V = 10.0; + private static final double COMP_WIDTH = 10.0; + private static final double COMP_HEIGHT = 5.0; + + private DrawableContainer container; + private EdgeLayout layout; + private Drawable nn, nw, ww, sw, ss, se, ee, ne, ce; + + private static final class TestDrawable extends AbstractDrawable { + + public void draw(DrawingContext context) { + } + + @Override + public Dimension2D getPreferredSize() { + Dimension2D size = super.getPreferredSize(); + size.setSize(COMP_WIDTH, COMP_HEIGHT); + return size; + } + } + + @Before + public void setUp() { + layout = new EdgeLayout(GAP_H, GAP_V); + + container = new DrawableContainer(null); + + nn = new TestDrawable(); + nw = new TestDrawable(); + ww = new TestDrawable(); + sw = new TestDrawable(); + ss = new TestDrawable(); + se = new TestDrawable(); + ee = new TestDrawable(); + ne = new TestDrawable(); + ce = new TestDrawable(); + + container.add(nn, Location.NORTH); + container.add(nw, Location.NORTH_WEST); + container.add(ww, Location.WEST); + container.add(sw, Location.SOUTH_WEST); + container.add(ss, Location.SOUTH); + container.add(se, Location.SOUTH_EAST); + container.add(ee, Location.EAST); + container.add(ne, Location.NORTH_EAST); + container.add(ce, Location.CENTER); + } + + @Test + public void testCreate() { + EdgeLayout noGap = new EdgeLayout(); + assertEquals(0.0, noGap.getGapX(), DELTA); + assertEquals(0.0, noGap.getGapY(), DELTA); + + EdgeLayout gapped = new EdgeLayout(GAP_H, GAP_V); + assertEquals(GAP_H, gapped.getGapX(), DELTA); + assertEquals(GAP_V, gapped.getGapY(), DELTA); + } + + @Test + public void testPreferredSize() { + Dimension2D size = layout.getPreferredSize(container); + assertEquals(3.0*COMP_WIDTH + 2.0*GAP_H, size.getWidth(), DELTA); + assertEquals(3.0*COMP_HEIGHT + 2.0*GAP_V, size.getHeight(), DELTA); + } + + @Test + public void testLayout() { + Rectangle2D bounds = new Rectangle2D.Double(5.0, 5.0, 50.0, 50.0); + container.setBounds(bounds); + layout.layout(container); + + // Test x coordinates + assertEquals(bounds.getMinX(), nw.getX(), DELTA); + assertEquals(bounds.getMinX(), ww.getX(), DELTA); + assertEquals(bounds.getMinX(), sw.getX(), DELTA); + assertEquals(bounds.getMinX() + COMP_WIDTH + GAP_H, nn.getX(), DELTA); + assertEquals(bounds.getMinX() + COMP_WIDTH + GAP_H, ce.getX(), DELTA); + assertEquals(bounds.getMinX() + COMP_WIDTH + GAP_H, ss.getX(), DELTA); + assertEquals(bounds.getMaxX() - COMP_WIDTH, ne.getX(), DELTA); + assertEquals(bounds.getMaxX() - COMP_WIDTH, ee.getX(), DELTA); + assertEquals(bounds.getMaxX() - COMP_WIDTH, se.getX(), DELTA); + // Test y coordinates + assertEquals(bounds.getMinY(), nw.getY(), DELTA); + assertEquals(bounds.getMinY(), nn.getY(), DELTA); + assertEquals(bounds.getMinY(), ne.getY(), DELTA); + assertEquals(bounds.getMinY() + COMP_HEIGHT + GAP_V, ww.getY(), DELTA); + assertEquals(bounds.getMinY() + COMP_HEIGHT + GAP_V, ce.getY(), DELTA); + assertEquals(bounds.getMinY() + COMP_HEIGHT + GAP_V, ee.getY(), DELTA); + assertEquals(bounds.getMaxY() - COMP_HEIGHT, sw.getY(), DELTA); + assertEquals(bounds.getMaxY() - COMP_HEIGHT, ss.getY(), DELTA); + assertEquals(bounds.getMaxY() - COMP_HEIGHT, se.getY(), DELTA); + + // TODO Test width and height + } +} diff --git a/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/LayoutTests.java b/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/LayoutTests.java new file mode 100644 index 0000000..bd348a2 --- /dev/null +++ b/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/LayoutTests.java @@ -0,0 +1,15 @@ +package org.xbib.graphics.graph.gral.test.graphics.layout; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + AbstractLayoutTest.class, + AbstractOrientedLayoutTest.class, + EdgeLayoutTest.class, + StackedLayoutTest.class, + TableLayoutTest.class, +}) +public class LayoutTests { +} diff --git a/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/StackedLayoutTest.java b/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/StackedLayoutTest.java new file mode 100644 index 0000000..10334fa --- /dev/null +++ b/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/StackedLayoutTest.java @@ -0,0 +1,145 @@ +package org.xbib.graphics.graph.gral.test.graphics.layout; + +import static org.junit.Assert.assertEquals; + +import java.awt.geom.Dimension2D; +import java.awt.geom.Rectangle2D; + +import org.xbib.graphics.graph.gral.graphics.AbstractDrawable; +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.junit.Before; +import org.junit.Test; + +import org.xbib.graphics.graph.gral.graphics.Orientation; +import org.xbib.graphics.graph.gral.graphics.layout.Layout; +import org.xbib.graphics.graph.gral.graphics.layout.StackedLayout; + +public class StackedLayoutTest { + private static final double DELTA = 1e-15; + private static final double GAP_X = 5.0; + private static final double GAP_Y = 10.0; + private static final double COMP_WIDTH = 10.0; + private static final double COMP_HEIGHT = 5.0; + + private DrawableContainer container; + private Drawable a, b, c; + + private static final class TestDrawable extends AbstractDrawable { + + public void draw(DrawingContext context) { + } + + @Override + public Dimension2D getPreferredSize() { + Dimension2D size = super.getPreferredSize(); + size.setSize(COMP_WIDTH, COMP_HEIGHT); + return size; + } + } + + @Before + public void setUp() { + container = new DrawableContainer(null); + + a = new TestDrawable(); + b = new TestDrawable(); + c = new TestDrawable(); + + container.add(a); + container.add(b); + container.add(c); + } + + @Test + public void testCreate() { + StackedLayout noGap = new StackedLayout(Orientation.VERTICAL); + assertEquals(Orientation.VERTICAL, noGap.getOrientation()); + assertEquals(0.0, noGap.getGapX(), DELTA); + assertEquals(0.0, noGap.getGapY(), DELTA); + + StackedLayout gapped = new StackedLayout(Orientation.HORIZONTAL, GAP_X, GAP_Y); + assertEquals(Orientation.HORIZONTAL, gapped.getOrientation()); + assertEquals(GAP_X, gapped.getGapX(), DELTA); + assertEquals(GAP_Y, gapped.getGapY(), DELTA); + } + + @Test + public void testPreferredSizeVertical() { + Layout layout = new StackedLayout(Orientation.VERTICAL, GAP_X, GAP_Y); + Dimension2D size = layout.getPreferredSize(container); + assertEquals(COMP_WIDTH, size.getWidth(), DELTA); + assertEquals(3.0*COMP_HEIGHT + 2.0*GAP_Y, size.getHeight(), DELTA); + } + + @Test + public void testPreferredSizeHorizontal() { + Layout layout = new StackedLayout(Orientation.HORIZONTAL, GAP_X, GAP_Y); + Dimension2D size = layout.getPreferredSize(container); + assertEquals(3.0*COMP_WIDTH + 2.0*GAP_X, size.getWidth(), DELTA); + assertEquals(COMP_HEIGHT, size.getHeight(), DELTA); + } + + @Test + public void testLayoutVertical() { + Layout layout = new StackedLayout(Orientation.VERTICAL, GAP_X, GAP_Y); + Rectangle2D bounds = new Rectangle2D.Double(5.0, 5.0, 50.0, 50.0); + container.setBounds(bounds); + layout.layout(container); + + // Test x coordinates + assertEquals(bounds.getMinX(), a.getX(), DELTA); + assertEquals(bounds.getMinX(), b.getX(), DELTA); + assertEquals(bounds.getMinX(), c.getX(), DELTA); + // Test y coordinates + assertEquals(12.5, a.getY(), DELTA); + assertEquals(27.5, b.getY(), DELTA); + assertEquals(42.5, c.getY(), DELTA); + + // TODO Test width and height + } + + @Test + public void testLayoutHorizontal() { + Layout layout = new StackedLayout(Orientation.HORIZONTAL, GAP_X, GAP_Y); + Rectangle2D bounds = new Rectangle2D.Double(5.0, 5.0, 50.0, 50.0); + container.setBounds(bounds); + layout.layout(container); + + // Test x coordinates + assertEquals(10.0, a.getX(), DELTA); + assertEquals(25.0, b.getX(), DELTA); + assertEquals(40.0, c.getX(), DELTA); + // Test y coordinates + assertEquals(bounds.getMinY(), a.getY(), DELTA); + assertEquals(bounds.getMinY(), b.getY(), DELTA); + assertEquals(bounds.getMinY(), c.getY(), DELTA); + + // TODO Test width and height + } + + @Test + public void testOrientation() { + StackedLayout layout; + // Vertical + layout = new StackedLayout(Orientation.VERTICAL); + assertEquals(Orientation.VERTICAL, layout.getOrientation()); + // Horizontal + layout = new StackedLayout(Orientation.HORIZONTAL); + assertEquals(Orientation.HORIZONTAL, layout.getOrientation()); + } + + @Test + public void testGap() { + StackedLayout layout; + // Vertical + layout = new StackedLayout(Orientation.VERTICAL, GAP_X, GAP_Y); + assertEquals(GAP_X, layout.getGapX(), DELTA); + assertEquals(GAP_Y, layout.getGapY(), DELTA); + // Horizontal + layout = new StackedLayout(Orientation.HORIZONTAL, GAP_X, GAP_Y); + assertEquals(GAP_X, layout.getGapX(), DELTA); + assertEquals(GAP_Y, layout.getGapY(), DELTA); + } +} diff --git a/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/TableLayoutTest.java b/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/TableLayoutTest.java new file mode 100644 index 0000000..d5b2acf --- /dev/null +++ b/graphics-graph-gral/src/test/java/org/xbib/graphics/graph/gral/test/graphics/layout/TableLayoutTest.java @@ -0,0 +1,135 @@ +package org.xbib.graphics.graph.gral.test.graphics.layout; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.awt.geom.Dimension2D; +import java.awt.geom.Rectangle2D; + +import org.xbib.graphics.graph.gral.graphics.AbstractDrawable; +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.junit.Before; +import org.junit.Test; +import org.xbib.graphics.graph.gral.graphics.layout.Layout; +import org.xbib.graphics.graph.gral.graphics.layout.TableLayout; + +public class TableLayoutTest { + private static final double DELTA = 1e-15; + private static final double GAP_X = 5.0; + private static final double GAP_Y = 10.0; + private static final double COMP_WIDTH = 10.0; + private static final double COMP_HEIGHT = 5.0; + + private DrawableContainer container; + private Drawable a, b, c; + + private static final class TestDrawable extends AbstractDrawable { + + public void draw(DrawingContext context) { + } + + @Override + public Dimension2D getPreferredSize() { + Dimension2D size = super.getPreferredSize(); + size.setSize(COMP_WIDTH, COMP_HEIGHT); + return size; + } + } + + @Before + public void setUp() { + container = new DrawableContainer(null); + + a = new TestDrawable(); + b = new TestDrawable(); + c = new TestDrawable(); + + container.add(a); + container.add(b); + container.add(c); + } + + @Test + public void testCreate() { + TableLayout noGap = new TableLayout(1); + assertEquals(0.0, noGap.getGapX(), DELTA); + assertEquals(0.0, noGap.getGapY(), DELTA); + + TableLayout gapped = new TableLayout(1, GAP_X, GAP_Y); + assertEquals(GAP_X, gapped.getGapX(), DELTA); + assertEquals(GAP_Y, gapped.getGapY(), DELTA); + } + + @Test + public void testCreateInvalid() { + try { + new TableLayout(-1, GAP_X, GAP_Y); + fail("Expected IllegalArgumentException because of negative column number."); + } catch (IllegalArgumentException e) { + } + + try { + new TableLayout(0, GAP_X, GAP_Y); + fail("Expected IllegalArgumentException because column number was zero."); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testPreferredSizeVertical() { + Layout layout = new TableLayout(1, GAP_X, GAP_Y); + Dimension2D size = layout.getPreferredSize(container); + assertEquals(COMP_WIDTH, size.getWidth(), DELTA); + assertEquals(3.0*COMP_HEIGHT + 2.0*GAP_Y, size.getHeight(), DELTA); + } + + @Test + public void testPreferredSizeHorizontal() { + Layout layout = new TableLayout(3, GAP_X, GAP_Y); + Dimension2D size = layout.getPreferredSize(container); + assertEquals(3.0*COMP_WIDTH + 2.0*GAP_X, size.getWidth(), DELTA); + assertEquals(COMP_HEIGHT, size.getHeight(), DELTA); + } + + @Test + public void testLayoutVertical() { + Layout layout = new TableLayout(1, GAP_X, GAP_Y); + Rectangle2D bounds = new Rectangle2D.Double(5.0, 5.0, 50.0, 50.0); + container.setBounds(bounds); + layout.layout(container); + + // Test x coordinates + assertEquals(bounds.getMinX(), a.getX(), DELTA); + assertEquals(bounds.getMinX(), b.getX(), DELTA); + assertEquals(bounds.getMinX(), c.getX(), DELTA); + // Test y coordinates + double meanCompHeight = (bounds.getHeight() - 2.0*GAP_Y)/3.0; + assertEquals(bounds.getMinY() + 0.0*meanCompHeight + 0.0*GAP_Y, a.getY(), DELTA); + assertEquals(bounds.getMinY() + 1.0*meanCompHeight + 1.0*GAP_Y, b.getY(), DELTA); + assertEquals(bounds.getMinY() + 2.0*meanCompHeight + 2.0*GAP_Y, c.getY(), DELTA); + + // TODO Test width and height + } + + @Test + public void testLayoutHorizontal() { + Layout layout = new TableLayout(3, GAP_X, GAP_Y); + Rectangle2D bounds = new Rectangle2D.Double(5.0, 5.0, 50.0, 50.0); + container.setBounds(bounds); + layout.layout(container); + + // Test x coordinates + double meanCompWidth = (bounds.getWidth() - 2.0*GAP_X)/3.0; + assertEquals(bounds.getMinX() + 0.0*meanCompWidth + 0.0*GAP_X, a.getX(), DELTA); + assertEquals(bounds.getMinX() + 1.0*meanCompWidth + 1.0*GAP_X, b.getX(), DELTA); + assertEquals(bounds.getMinX() + 2.0*meanCompWidth + 2.0*GAP_X, c.getX(), DELTA); + // Test y coordinates + assertEquals(bounds.getMinY(), a.getY(), DELTA); + assertEquals(bounds.getMinY(), b.getY(), DELTA); + assertEquals(bounds.getMinY(), c.getY(), DELTA); + + // TODO Test width and height + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Document.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Document.java index 1b8c30a..b3f3e19 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Document.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Document.java @@ -1,5 +1,6 @@ package org.xbib.graphics.pdfbox.layout.elements; +import org.apache.pdfbox.io.MemoryUsageSetting; import org.apache.pdfbox.pdmodel.PDDocument; import org.xbib.graphics.pdfbox.layout.elements.render.Layout; import org.xbib.graphics.pdfbox.layout.elements.render.LayoutHint; @@ -8,20 +9,19 @@ import org.xbib.graphics.pdfbox.layout.elements.render.RenderListener; import org.xbib.graphics.pdfbox.layout.elements.render.Renderer; import org.xbib.graphics.pdfbox.layout.elements.render.VerticalLayout; import org.xbib.graphics.pdfbox.layout.elements.render.VerticalLayoutHint; -import java.io.File; -import java.io.FileOutputStream; +import java.io.Closeable; import java.io.IOException; import java.io.OutputStream; -import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Map.Entry; /** * The central class for creating a document. */ -public class Document implements RenderListener { +public class Document implements Closeable, RenderListener { /** * A4 portrait without margins. @@ -42,7 +42,7 @@ public class Document implements RenderListener { * Creates a Document using the {@link #DEFAULT_PAGE_FORMAT}. */ public Document() { - this(DEFAULT_PAGE_FORMAT); + this(DEFAULT_PAGE_FORMAT, true); } /** @@ -58,7 +58,14 @@ public class Document implements RenderListener { float marginRight, float marginTop, float marginBottom) { - this(PageFormat.with().margins(marginLeft, marginRight, marginTop, marginBottom).build()); + this(marginLeft, marginRight, marginTop, marginBottom, true); + } + + public Document(float marginLeft, + float marginRight, + float marginTop, + float marginBottom, boolean memory) { + this(PageFormat.with().margins(marginLeft, marginRight, marginTop, marginBottom).build(), memory); } /** @@ -68,7 +75,17 @@ public class Document implements RenderListener { * @param pageFormat the page format box to use. */ public Document(PageFormat pageFormat) { + this(pageFormat, true); + } + + public Document(PageFormat pageFormat, boolean memory) { this.pageFormat = pageFormat; + this.pdDocument = new PDDocument(memory ? + MemoryUsageSetting.setupMainMemoryOnly() : MemoryUsageSetting.setupTempFileOnly()); + } + + public PDDocument getPdDocument() { + return pdDocument; } /** @@ -87,12 +104,7 @@ public class Document implements RenderListener { * @param layoutHint the hint for the {@link Layout}. */ public void add(Element element, LayoutHint layoutHint) { - elements.add(createEntry(element, layoutHint)); - } - - private Entry createEntry(Element element, - LayoutHint layoutHint) { - return new SimpleEntry<>(element, layoutHint); + elements.add(Map.entry(element, layoutHint)); } /** @@ -118,27 +130,6 @@ public class Document implements RenderListener { pageFormat.getMarginTop() - pageFormat.getMarginBottom(); } - /** - * Returns the {@link PDDocument} to be created by method {@link #render()}. - * Beware that this PDDocument is released after rendering. This means each - * rendering process creates a new PDDocument. - * - * @return the PDDocument to be used on the next call to {@link #render()}. - */ - public PDDocument getPDDocument() { - if (pdDocument == null) { - pdDocument = new PDDocument(); - } - return pdDocument; - } - - /** - * Called after {@link #render()} in order to release the current document. - */ - protected void resetPDDocument() { - this.pdDocument = null; - } - /** * Adds a (custom) {@link Renderer} that may handle the rendering of an * element. All renderers will be asked to render the current element in the @@ -153,15 +144,39 @@ public class Document implements RenderListener { } } + /** + * Adds a {@link RenderListener} that will be notified during rendering. + * + * @param listener the listener to add. + */ + public void addRenderListener(final RenderListener listener) { + if (listener != null) { + renderListener.add(listener); + } + } + + @Override + public void beforePage(RenderContext renderContext) { + for (RenderListener listener : renderListener) { + listener.beforePage(renderContext); + } + } + + @Override + public void afterPage(RenderContext renderContext) { + for (RenderListener listener : renderListener) { + listener.afterPage(renderContext); + } + } + /** * Renders all elements and returns the resulting {@link PDDocument}. * * @return the resulting {@link PDDocument} * @throws IOException by pdfbox */ - public PDDocument render() throws IOException { - PDDocument document = getPDDocument(); - RenderContext renderContext = new RenderContext(this, document); + public Document render() throws IOException { + RenderContext renderContext = new RenderContext(this, pdDocument); for (Entry entry : elements) { Element element = entry.getKey(); LayoutHint layoutHint = entry.getValue(); @@ -180,65 +195,20 @@ public class Document implements RenderListener { } } renderContext.close(); - resetPDDocument(); - return document; + return this; } - /** - * {@link #render() Renders} the document and saves it to the given file. - * - * @param file the file to save to. - * @throws IOException by pdfbox - */ - public void save(final File file) throws IOException { - try (OutputStream out = new FileOutputStream(file)) { - save(out); - } - } - - /** - * {@link #render() Renders} the document and saves it to the given output - * stream. - * - * @param output the stream to save to. - * @throws IOException by pdfbox - */ - public void save(final OutputStream output) throws IOException { - try (PDDocument document = render()) { - try { - document.save(output); - } catch (IOException ioe) { - throw ioe; - } catch (Exception e) { - throw new IOException(e); - } - } - } - - /** - * Adds a {@link RenderListener} that will be notified during - * {@link #render() rendering}. - * - * @param listener the listener to add. - */ - public void addRenderListener(final RenderListener listener) { - if (listener != null) { - renderListener.add(listener); + public synchronized void save(OutputStream outputStream) throws IOException { + if (pdDocument != null) { + pdDocument.save(outputStream); + pdDocument = null; } } @Override - public void beforePage(RenderContext renderContext) - throws IOException { - for (RenderListener listener : renderListener) { - listener.beforePage(renderContext); - } - } - - @Override - public void afterPage(RenderContext renderContext) throws IOException { - for (RenderListener listener : renderListener) { - listener.afterPage(renderContext); + public void close() throws IOException { + if (pdDocument != null) { + pdDocument.close(); } } } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/RenderListener.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/RenderListener.java index d702fe6..4f97883 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/RenderListener.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/RenderListener.java @@ -1,7 +1,5 @@ package org.xbib.graphics.pdfbox.layout.elements.render; -import java.io.IOException; - /** * A render listener is called before and after a page has been rendered. It may * be used, to perform some custom operations (drawings) to the page. @@ -12,15 +10,13 @@ public interface RenderListener { * Called before any rendering is performed to the page. * * @param renderContext the context providing all rendering state. - * @throws IOException by pdfbox. */ - void beforePage(final RenderContext renderContext) throws IOException; + void beforePage(RenderContext renderContext); /** * Called after any rendering is performed to the page. * * @param renderContext the context providing all rendering state. - * @throws IOException by pdfbox. */ - void afterPage(final RenderContext renderContext) throws IOException; + void afterPage(RenderContext renderContext); } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/Renderer.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/Renderer.java index a5e3a0d..ba2b047 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/Renderer.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/Renderer.java @@ -19,7 +19,6 @@ public interface Renderer { * @return true if the layout is able to render the element. * @throws IOException by pdfbox */ - boolean render(final RenderContext renderContext, final Element element, - final LayoutHint layoutHint) throws IOException; - + boolean render(RenderContext renderContext, Element element, + LayoutHint layoutHint) throws IOException; } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/NotoSansFont.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/NotoSansFont.java index b0b7063..8f4480b 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/NotoSansFont.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/NotoSansFont.java @@ -3,13 +3,14 @@ package org.xbib.graphics.pdfbox.layout.font; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDType0Font; +import org.xbib.graphics.pdfbox.layout.elements.Document; import java.io.IOException; import java.io.UncheckedIOException; import java.util.Objects; public class NotoSansFont implements Font { - private final PDDocument document; + private final PDDocument pdDocument; private static PDType0Font regular; @@ -19,14 +20,14 @@ public class NotoSansFont implements Font { private static PDType0Font bolditalic; - public NotoSansFont(PDDocument document) { - this.document = document; + public NotoSansFont(Document document) { + this.pdDocument = document.getPdDocument(); } @Override public PDFont getRegularFont() { if (regular == null) { - regular = load(document, "NotoSans-Regular.ttf"); + regular = load("NotoSans-Regular.ttf"); } return regular; } @@ -34,7 +35,7 @@ public class NotoSansFont implements Font { @Override public PDFont getBoldFont() { if (bold == null) { - bold = load(document, "NotoSans-Bold.ttf"); + bold = load("NotoSans-Bold.ttf"); } return bold; } @@ -42,7 +43,7 @@ public class NotoSansFont implements Font { @Override public PDFont getItalicFont() { if (italic == null) { - italic = load(document, "NotoSans-Italic.ttf"); + italic = load("NotoSans-Italic.ttf"); } return italic; } @@ -50,14 +51,14 @@ public class NotoSansFont implements Font { @Override public PDFont getBoldItalicFont() { if (bolditalic == null) { - bolditalic = load(document, "NotoSans-BoldItalic.ttf"); + bolditalic = load("NotoSans-BoldItalic.ttf"); } return bolditalic; } - private static PDType0Font load(PDDocument document, String resourceName) { + private PDType0Font load(String resourceName) { try { - return PDType0Font.load(document, Objects.requireNonNull(NotoSansFont.class.getResourceAsStream(resourceName))); + return PDType0Font.load(pdDocument, Objects.requireNonNull(NotoSansFont.class.getResourceAsStream(resourceName))); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/Command.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/Command.java new file mode 100644 index 0000000..2c63696 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/Command.java @@ -0,0 +1,21 @@ +package org.xbib.graphics.pdfbox.layout.script; + +public abstract class Command { + + private final T value; + + public Command(T value) { + this.value = value; + } + + public abstract String getKey(); + + public T getValue() { + return value; + } + + @Override + public String toString() { + return String.format("%s[value=%s]", getKey(), getValue()); + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/DocumentProcessor.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/DocumentProcessor.java new file mode 100644 index 0000000..d1d8dca --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/DocumentProcessor.java @@ -0,0 +1,18 @@ +package org.xbib.graphics.pdfbox.layout.script; + +import org.xbib.graphics.pdfbox.layout.elements.Document; +import org.xbib.graphics.pdfbox.layout.elements.PageFormat; +import java.io.IOException; + +public class DocumentProcessor implements Processor { + + @Override + public ProcessorResult process(Iterable> commands, PageFormat pageFormat) throws IOException { + ProcessorResult processorResult = new DocumentProcessorResult(new Document(pageFormat)); + for (Command command : commands) { + processorResult.handle(command); + } + processorResult.close(); + return processorResult; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/DocumentProcessorResult.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/DocumentProcessorResult.java new file mode 100644 index 0000000..a1e9d6c --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/DocumentProcessorResult.java @@ -0,0 +1,33 @@ +package org.xbib.graphics.pdfbox.layout.script; + +import org.xbib.graphics.pdfbox.layout.elements.Document; +import org.xbib.graphics.pdfbox.layout.script.commands.ParagraphCommand; +import java.io.IOException; +import java.io.OutputStream; + +public class DocumentProcessorResult implements ProcessorResult { + + private final Document document; + + public DocumentProcessorResult(Document document) { + this.document = document; + } + + @Override + public void handle(Command command) throws IOException { + if (command instanceof ParagraphCommand) { + ParagraphCommand paragraphCommand = (ParagraphCommand) command; + document.add(paragraphCommand.getValue()); + } + } + + @Override + public void write(OutputStream out) throws IOException { + document.render().save(out); + } + + @Override + public void close() throws IOException { + document.close(); + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/Processor.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/Processor.java new file mode 100644 index 0000000..3a8872c --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/Processor.java @@ -0,0 +1,9 @@ +package org.xbib.graphics.pdfbox.layout.script; + +import org.xbib.graphics.pdfbox.layout.elements.PageFormat; +import java.io.IOException; + +public interface Processor { + + ProcessorResult process(Iterable> commands, PageFormat pageFormat) throws IOException; +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/ProcessorResult.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/ProcessorResult.java new file mode 100644 index 0000000..b054b8b --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/ProcessorResult.java @@ -0,0 +1,13 @@ +package org.xbib.graphics.pdfbox.layout.script; + +import java.io.IOException; +import java.io.OutputStream; + +public interface ProcessorResult { + + void handle(Command command) throws IOException; + + void write(OutputStream out) throws IOException; + + void close() throws IOException; +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/commands/ParagraphCommand.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/commands/ParagraphCommand.java new file mode 100644 index 0000000..5457535 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/script/commands/ParagraphCommand.java @@ -0,0 +1,16 @@ +package org.xbib.graphics.pdfbox.layout.script.commands; + +import org.xbib.graphics.pdfbox.layout.elements.Paragraph; +import org.xbib.graphics.pdfbox.layout.script.Command; + +public class ParagraphCommand extends Command { + + public ParagraphCommand(Paragraph paragraph) { + super(paragraph); + } + + @Override + public String getKey() { + return null; + } +} diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/AlignedTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/AlignedTest.java index 9e7d1a0..fcf9358 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/AlignedTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/AlignedTest.java @@ -38,6 +38,6 @@ public class AlignedTest { paragraph.setMaxWidth(40); document.add(paragraph, VerticalLayoutHint.CENTER); OutputStream outputStream = new FileOutputStream("build/aligned.pdf"); - document.save(outputStream); + document.render().save(outputStream); } } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/ColumnsTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/ColumnsTest.java index 664472e..04d5a40 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/ColumnsTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/ColumnsTest.java @@ -80,6 +80,6 @@ public class ColumnsTest { document.add(paragraph2); document.add(paragraph2); final OutputStream outputStream = new FileOutputStream("build/columns.pdf"); - document.save(outputStream); + document.render().save(outputStream); } } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomAnnotationTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomAnnotationTest.java index 4cba6a9..0f8325b 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomAnnotationTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomAnnotationTest.java @@ -215,7 +215,7 @@ public class CustomAnnotationTest { paragraph.setMaxWidth(150); document.add(paragraph); final OutputStream outputStream = new FileOutputStream("build/customannotation.pdf"); - document.save(outputStream); + document.render().save(outputStream); } private static PDColor toPDColor(final Color color) { diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomRenderer.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomRenderer.java index 9820f31..bddf2eb 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomRenderer.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomRenderer.java @@ -69,7 +69,7 @@ public class CustomRenderer { document.add(paragraph); final OutputStream outputStream = new FileOutputStream("build/customrenderer.pdf"); - document.save(outputStream); + document.render().save(outputStream); } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/FramesTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/FramesTest.java index fca33f9..07e9112 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/FramesTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/FramesTest.java @@ -14,7 +14,6 @@ import org.xbib.graphics.pdfbox.layout.text.Alignment; import org.xbib.graphics.pdfbox.layout.font.BaseFont; import java.awt.Color; import java.io.FileOutputStream; -import java.io.OutputStream; public class FramesTest { @@ -66,7 +65,6 @@ public class FramesTest { frame.setBorder(Color.green, new Stroke(2)); frame.setBackgroundColor(Color.pink); frame.setPadding(50, 0, 35, 0); -// frame.setMargin(30, 30, 20, 10); document.add(frame); paragraph = new Paragraph(); @@ -87,9 +85,6 @@ public class FramesTest { document.add(frame); - final OutputStream outputStream = new FileOutputStream("build/frames.pdf"); - document.save(outputStream); - + document.render().save(new FileOutputStream("build/frames.pdf")); } - } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloBarcodeTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloBarcodeTest.java index f8e4f4c..0bbcaf1 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloBarcodeTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloBarcodeTest.java @@ -9,7 +9,6 @@ import org.xbib.graphics.pdfbox.layout.elements.Document; import org.xbib.graphics.pdfbox.layout.elements.PageFormats; import org.xbib.graphics.pdfbox.layout.elements.Paragraph; import org.xbib.graphics.pdfbox.layout.elements.render.VerticalLayoutHint; -import org.xbib.graphics.pdfbox.layout.font.NotoSansFont; import org.xbib.graphics.pdfbox.layout.text.Alignment; import org.xbib.graphics.pdfbox.layout.font.BaseFont; import org.xbib.graphics.pdfbox.layout.text.Indent; @@ -32,7 +31,7 @@ public class HelloBarcodeTest { symbol.setHumanReadableLocation(HumanReadableLocation.BOTTOM); BarcodeElement barcodeElement = new BarcodeElement(symbol); document.add(barcodeElement, new VerticalLayoutHint(Alignment.LEFT, 10, 10, 10, 10, true)); - final OutputStream outputStream = new FileOutputStream("build/hellobarcode.pdf"); - document.save(outputStream); + OutputStream outputStream = new FileOutputStream("build/hellobarcode.pdf"); + document.render().save(outputStream); } } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloCatTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloCatTest.java index e7853ba..6fbdbed 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloCatTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloCatTest.java @@ -23,6 +23,6 @@ public class HelloCatTest { imageElement.setScale(0.1f); document.add(imageElement, new VerticalLayoutHint(Alignment.LEFT, 10, 10, 10, 10, true)); final OutputStream outputStream = new FileOutputStream("build/hellocat.pdf"); - document.save(outputStream); + document.render().save(outputStream); } } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloDoc.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloDoc.java index 9534e78..c7ffa92 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloDoc.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloDoc.java @@ -17,7 +17,7 @@ public class HelloDoc { paragraph.addText("Hello Document", 20, BaseFont.HELVETICA); document.add(paragraph); final OutputStream outputStream = new FileOutputStream("build/hellodoc.pdf"); - document.save(outputStream); + document.render().save(outputStream); } } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloNotoFontTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloNotoFontTest.java index cf6acd3..eaaaca5 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloNotoFontTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/HelloNotoFontTest.java @@ -17,12 +17,12 @@ public class HelloNotoFontTest { Document document = new Document(PageFormats.A4_PORTRAIT); Paragraph paragraph = new Paragraph(); paragraph.add(new Indent(32, SpaceUnit.pt)); - paragraph.addMarkup("Hello Noto Regular\n", 12, new NotoSansFont(document.getPDDocument())); - paragraph.addMarkup("*Hello Noto Bold*\n", 12, new NotoSansFont(document.getPDDocument())); - paragraph.addMarkup("_Hello Noto Italic_\n", 12, new NotoSansFont(document.getPDDocument())); - paragraph.addMarkup("*_Hello Noto Bold Italic_*\n", 12, new NotoSansFont(document.getPDDocument())); + paragraph.addMarkup("Hello Noto Regular\n", 12, new NotoSansFont(document)); + paragraph.addMarkup("*Hello Noto Bold*\n", 12, new NotoSansFont(document)); + paragraph.addMarkup("_Hello Noto Italic_\n", 12, new NotoSansFont(document)); + paragraph.addMarkup("*_Hello Noto Bold Italic_*\n", 12, new NotoSansFont(document)); document.add(paragraph); final OutputStream outputStream = new FileOutputStream("build/hellonotofont.pdf"); - document.save(outputStream); + document.render().save(outputStream); } } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/IndentationTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/IndentationTest.java index 2f6675a..a3226f6 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/IndentationTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/IndentationTest.java @@ -129,8 +129,7 @@ public class IndentationTest { paragraph.addMarkup(text1, 11, BaseFont.TIMES); document.add(paragraph); - final OutputStream outputStream = new FileOutputStream("build/indentation.pdf"); - document.save(outputStream); + document.render().save(new FileOutputStream("build/indentation.pdf")); } private static String getBulletCharacter(final int level) { diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Landscape.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Landscape.java index 618d68a..7f0d66b 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Landscape.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Landscape.java @@ -11,7 +11,6 @@ import org.xbib.graphics.pdfbox.layout.elements.render.VerticalLayout; import org.xbib.graphics.pdfbox.layout.elements.render.VerticalLayoutHint; import org.xbib.graphics.pdfbox.layout.font.BaseFont; import java.io.FileOutputStream; -import java.io.OutputStream; public class Landscape { @@ -104,8 +103,6 @@ public class Landscape { document.add(paragraph2); document.add(paragraph3); - final OutputStream outputStream = new FileOutputStream("build/landscape.pdf"); - document.save(outputStream); - + document.render().save(new FileOutputStream("build/landscape.pdf")); } } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Letter.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Letter.java index 16eb7ab..dee0127 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Letter.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Letter.java @@ -80,8 +80,6 @@ public class Letter { paragraph.setAbsolutePosition(new Position(hMargin, vMargin)); document.add(paragraph); - final OutputStream outputStream = new FileOutputStream("build/letter.pdf"); - document.save(outputStream); - + document.render().save(new FileOutputStream("build/letter.pdf")); } } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/LineSpacingTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/LineSpacingTest.java index 8dde980..565ca44 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/LineSpacingTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/LineSpacingTest.java @@ -45,7 +45,7 @@ public class LineSpacingTest { document.add(right); final OutputStream outputStream = new FileOutputStream("build/linespacing.pdf"); - document.save(outputStream); + document.render().save(outputStream); } } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/LinksTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/LinksTest.java index 75b4312..6084328 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/LinksTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/LinksTest.java @@ -66,7 +66,7 @@ public class LinksTest { document.add(paragraph1); final OutputStream outputStream = new FileOutputStream("build/links.pdf"); - document.save(outputStream); + document.render().save(outputStream); } } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Listener.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Listener.java index 5311b29..cf16577 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Listener.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Listener.java @@ -73,7 +73,7 @@ public class Listener { document.add(paragraph); final OutputStream outputStream = new FileOutputStream("build/listener.pdf"); - document.save(outputStream); + document.render().save(outputStream); } } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Margin.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Margin.java index c3657a2..b14b299 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Margin.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Margin.java @@ -52,7 +52,7 @@ public class Margin { 150, 20, 0)); final OutputStream outputStream = new FileOutputStream("build/margin.pdf"); - document.save(outputStream); + document.render().save(outputStream); } } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/MarkupTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/MarkupTest.java index 323333b..448637f 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/MarkupTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/MarkupTest.java @@ -7,7 +7,6 @@ import org.xbib.graphics.pdfbox.layout.elements.render.VerticalLayoutHint; import org.xbib.graphics.pdfbox.layout.text.Alignment; import org.xbib.graphics.pdfbox.layout.font.BaseFont; import java.io.FileOutputStream; -import java.io.OutputStream; public class MarkupTest { @@ -69,8 +68,6 @@ public class MarkupTest { paragraph.addMarkup(text1, 11, BaseFont.TIMES); document.add(paragraph); - final OutputStream outputStream = new FileOutputStream("build/markup.pdf"); - document.save(outputStream); - + document.render().save( new FileOutputStream("build/markup.pdf")); } } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/MultiplePagesTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/MultiplePagesTest.java index a143cdb..ac4cfa6 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/MultiplePagesTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/MultiplePagesTest.java @@ -5,7 +5,6 @@ import org.xbib.graphics.pdfbox.layout.elements.Document; import org.xbib.graphics.pdfbox.layout.elements.Paragraph; import org.xbib.graphics.pdfbox.layout.font.BaseFont; import java.io.FileOutputStream; -import java.io.OutputStream; public class MultiplePagesTest { @@ -69,8 +68,6 @@ public class MultiplePagesTest { document.add(paragraph2); document.add(paragraph2); - final OutputStream outputStream = new FileOutputStream("build/multiplepages.pdf"); - document.save(outputStream); - + document.render().save(new FileOutputStream("build/multiplepages.pdf")); } } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/RotationTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/RotationTest.java index e0fdd19..3da974a 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/RotationTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/RotationTest.java @@ -11,7 +11,6 @@ import org.xbib.graphics.pdfbox.layout.elements.render.VerticalLayout; import org.xbib.graphics.pdfbox.layout.elements.render.VerticalLayoutHint; import org.xbib.graphics.pdfbox.layout.font.BaseFont; import java.io.FileOutputStream; -import java.io.OutputStream; public class RotationTest { @@ -109,8 +108,6 @@ public class RotationTest { document.add(paragraph2); document.add(paragraph3); - final OutputStream outputStream = new FileOutputStream("build/rotation.pdf"); - document.save(outputStream); - + document.render().save(new FileOutputStream("build/rotation.pdf")); } } diff --git a/graphics-vector/src/main/java/org/xbib/graphics/io/vector/VectorGraphics2D.java b/graphics-vector/src/main/java/org/xbib/graphics/io/vector/VectorGraphics2D.java index 5a6c438..a994607 100644 --- a/graphics-vector/src/main/java/org/xbib/graphics/io/vector/VectorGraphics2D.java +++ b/graphics-vector/src/main/java/org/xbib/graphics/io/vector/VectorGraphics2D.java @@ -42,14 +42,12 @@ import java.awt.Shape; import java.awt.Stroke; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; -import java.awt.font.TextLayout; import java.awt.geom.AffineTransform; import java.awt.geom.Arc2D; import java.awt.geom.Area; import java.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import java.awt.geom.Path2D; -import java.awt.geom.PathIterator; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.awt.image.AffineTransformOp; @@ -835,31 +833,6 @@ public class VectorGraphics2D extends Graphics2D implements Cloneable { return op.filter(bufferedImage, null); } - /*private static boolean notEquals(Shape shapeA, Shape shapeB) { - PathIterator pathAIterator = shapeA.getPathIterator(null); - PathIterator pathBIterator = shapeB.getPathIterator(null); - if (pathAIterator.getWindingRule() != pathBIterator.getWindingRule()) { - return true; - } - double[] pathASegment = new double[6]; - double[] pathBSegment = new double[6]; - while (!pathAIterator.isDone()) { - int pathASegmentType = pathAIterator.currentSegment(pathASegment); - int pathBSegmentType = pathBIterator.currentSegment(pathBSegment); - if (pathASegmentType != pathBSegmentType) { - return true; - } - for (int segmentIndex = 0; segmentIndex < pathASegment.length; segmentIndex++) { - if (pathASegment[segmentIndex] != pathBSegment[segmentIndex]) { - return true; - } - } - pathAIterator.next(); - pathBIterator.next(); - } - return !pathBIterator.isDone(); - }*/ - private static Shape intersectShapes(Shape s1, Shape s2) { if (s1 instanceof Rectangle2D && s2 instanceof Rectangle2D) { Rectangle2D r1 = (Rectangle2D) s1; diff --git a/settings.gradle b/settings.gradle index bd6ecab..a70f27a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,3 +10,4 @@ include 'graphics-pdfbox' include 'graphics-pdfbox-layout' include 'graphics-pdfbox-groovy' include 'graphics-pdfbox-print' +include 'graphics-graph-gral'