diff --git a/graphics-graph-gral/build.gradle b/graphics-graph-gral/build.gradle new file mode 100644 index 0000000..73a55e3 --- /dev/null +++ b/graphics-graph-gral/build.gradle @@ -0,0 +1,3 @@ +dependencies { + testRuntimeOnly "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-graph-jmathplot/src/main/java/module-info.java b/graphics-graph-jmathplot/src/main/java/module-info.java new file mode 100644 index 0000000..9727d9c --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/module-info.java @@ -0,0 +1,3 @@ +module org.xbib.graphics.graph.jmathplot { + requires java.desktop; +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Axis.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Axis.java new file mode 100644 index 0000000..0678186 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Axis.java @@ -0,0 +1,625 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot3DPanel; +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.FastMath; +import java.awt.Color; +import java.awt.Font; +import java.awt.font.FontRenderContext; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import javax.swing.JFrame; +import javax.swing.JOptionPane; + +/** + * Class use to describe one of the axis of a plot object. + *

+ * BSD License + * + * @author Yann RICHET + * Changed on 6/13/2014 by Jerry Dietrich + * Contact info ballooninternet@cox.net + */ + +public class Axis implements Plotable, BaseDependant, Editable { + + /** + * Mapping of the data on this axis, which is the association between values + * along this axis as String and double numbers. + */ + protected HashMap stringMap; + + protected int linear_slicing = 10; + + protected int note_precision = 5; + + protected int index; + + protected Base base; + + /** + * Visibility of the whole axis + */ + boolean visible = true; + + /** + * Color in which the name of the axis is displayed. + */ + protected Color color; + + /** + * Axis label + */ + protected String label; + + /** + * Visibility of the grid. + */ + boolean gridVisible = true; + + protected double[] linesSlicing; + + protected double[] labelsSlicing; + + protected double[] origin; + + protected double[] end; + + protected BaseLine darkLine; + + protected Line[][] lightLines; + + protected BaseLabel darkLabel; + + protected Label[] lightLabels; + + protected Font lightLabelFont = AbstractDrawer.DEFAULT_FONT; + + protected Font darkLabelFont = AbstractDrawer.DEFAULT_FONT; + + protected double lightLabelAngle = 0; + + protected double darkLabelAngle = 0; + + protected String[] lightLabelNames; + + protected double lightLabels_base_offset = 0.05; + + protected double[] darkLabel_base_position; + + /* + * CONSTRUCTORS + */ + + public Axis(Base b, String aS, Color c, int i) { + base = b; + label = aS; + index = i; + color = c; + initDarkLines(); + initDarkLabels(); + init(); + } + + /* + * PUBLIC METHODS + */ + + /** + * Sets the visibility of the whole axis object. + * + * @param v Visible if true. + */ + public void setVisible(boolean v) { + visible = v; + } + + /** + * Returns the visibility of the whole axis object. + * + * @return Visible if true. + */ + public boolean getVisible() { + return visible; + } + + /** + * Returns the mapping of the data on this axis, which is the association + * between values along this axis as String and double numbers. + * + * @return Mapping of the data on this axis. + */ + public HashMap getStringMap() { + return stringMap; + } + + /** + * Returns the mapping of the data on this axis, which is the association + * between values along this axis as String and double numbers. + * + * @param stringMap Mapping of the data on this axis. + */ + public void setStringMap(HashMap stringMap) { + // System.out.println(Array.toString(this.stringMap)+" + // >>\n"+Array.toString(stringMap)); + this.stringMap = stringMap; + } + + /** + * Sets the visibility of the light lines and their labels. + * + * @param v Visible if true. + */ + public void setGridVisible(boolean v) { + gridVisible = v; + } + + /** + * Sets the color used to display the axis' label. + * + * @param c The color of the axis' label. + */ + public void setColor(Color c) { + color = c; + darkLabel.setColor(color); + } + + /** + * Returns the color of the axis' label. + * + * @return The color of the axis' label. + */ + public Color getColor() { + return color; + } + + /** + * Sets the label of this axis. + * + * @param _label The label to be given to the axis. + */ + public void setLegend(String _label) { + label = _label; + darkLabel.setText(label); + } + + /** + * Returns the label of the axis. + * + * @return The label of the axis. + */ + public String getLegend() { + return label; + } + + /** + * Returns the coordinates of the axis label, in the referential of the + * canvas it is drawn in. + * + * @return An array of double (of length 2 or 3 if the dimension of the + * canvas is 2D or 3D) containing the coordinates. + */ + public double[] getLegendCoord() { + return darkLabel.coord; + } + + public void plot(AbstractDrawer draw) { + if (!visible) { + return; + } + if (gridVisible) { + draw.setFont(lightLabelFont); + FontRenderContext frc = draw.getGraphics2D().getFontRenderContext(); + double w = lightLabelFont.getStringBounds(lightLabels[0].label, frc).getWidth(); + double h = lightLabelFont.getSize2D(); + + int[] _origin = draw.project(base.getCoords()[0]); + int[] _end = draw.project(base.getCoords()[index + 1]); + int axis_h = 1 + FastMath.abs(_end[1] - _origin[1]); + int axis_w = 1 + FastMath.abs(_end[0] - _origin[0]); + + int inc = FastMath.min( + FastMath.max( + 1, + (int) FastMath.round(.5 + ((double) lightLabels.length * h) / ((double) axis_h))), + FastMath.max( + 1, + (int) FastMath.round(.5 + ((double) lightLabels.length * w) / ((double) axis_w))) + ); + + for (int i = 0; i < lightLabels.length; i = i + inc) { + if (lightLabels[i] != null) { + lightLabels[i].plot(draw); + } + } + + draw.setLineType(AbstractDrawer.DOTTED_LINE); + for (int i = 0; i < lightLines.length; i++) { + for (int j = base.getAxeScale(index).equalsIgnoreCase(Base.STRINGS) ? 0 : 1; j < lightLines[i].length; j = j + inc) { + if (lightLines[i][j] != null) { + lightLines[i][j].plot(draw); + } + } + } + } + draw.setLineType(AbstractDrawer.CONTINOUS_LINE); + // draw.setFont(darkLabelFont); + darkLine.plot(draw); + darkLabel.plot(draw); + } + + /** + * Sets the axis to its default initial value. + */ + public void init() { + // System.out.println("Axe.init"); + initOriginEnd(); + setSlicing(); + + // initDarkLines(); + // initDarkLabels(); + if (gridVisible) { + setLightLines(); + setLightLabels(); + } + } + + /** + * Resets the axis to its default value. Same as init(). + */ + public void resetBase() { + // System.out.println("Axe.resetBase"); + init(); + } + + /** + * Problem here? + * + * @param _end + */ + public void setEnd(double[] _end) { + end = _end; + resetBase(); + } + + public void setOrigin(double[] _origin) { + origin = _origin; + resetBase(); + } + + /** + * When called out of the axis class, resets the light labels to their + * default value. + */ + public void setLightLabels() { + // System.out.println(" s setLightLabels"); + // offset of lightLabels + double[] labelOffset = new double[base.dimension]; + for (int j = 0; j < base.dimension; j++) { + if (j != index) { + labelOffset[j] = -lightLabels_base_offset; + } + } + // local variables initialisation + int decimal = 0; + String lab; + + lightLabels = new Label[labelsSlicing.length]; + + for (int i = 0; i < lightLabels.length; i++) { + + double[] labelCoord = new double[base.dimension]; + System.arraycopy(base.getCoords()[index + 1], 0, labelCoord, 0, base.dimension); + + labelCoord[index] = labelsSlicing[i]; + + if (base.getAxeScale(index).startsWith(Base.LINEAR) + || base.getAxeScale(index).startsWith(Base.STRINGS)) { + decimal = -(int) (FastMath.log(base.getPrecisionUnit()[index] / 100) / log10); + } else if (base.getAxeScale(index).startsWith(Base.LOGARITHM)) { + decimal = -(int) (FastMath.floor(FastMath.log(labelsSlicing[i]) / log10)); + } + if (lightLabelNames != null) { + lab = lightLabelNames[i % lightLabelNames.length]; + } else { + lab = Label.approx(labelsSlicing[i], decimal) + ""; + } + // System.out.println(Array.toString(labelCoord) + " -> " + lab); + lightLabels[i] = new Label(lab, Color.lightGray, labelCoord); + lightLabels[i].base_offset = labelOffset; + + if (lightLabelAngle != 0) { + lightLabels[i].rotate(lightLabelAngle); + } + if (lightLabelFont != null) { + lightLabels[i].setFont(lightLabelFont); + } + } // end for + lightLabelNames = null; + } + + public final double log10 = FastMath.log(10); + + /** + * Sets the labels of the light lines. Is the numerical graduation by + * default. + * + * @param _lightLabelnames Array of string containing the labels. When the end of the + * array is reached for one tick, the following tick starts with + * the beginning of the array again. + */ + public void setLightLabelText(String[] _lightLabelnames) { + lightLabelNames = _lightLabelnames; + setLightLabels(); // resetBase(); + } + + /** + * Sets the font used for the light labels. + * + * @param f Font to use. + */ + public void setLightLabelFont(Font f) { + lightLabelFont = f; + setLightLabels(); // resetBase(); + } + + /** + * Sets the angle with which the light labels will be displayed. + * + * @param angle Angle in degrees, measured clockwise. + */ + public void setLightLabelAngle(double angle) { + lightLabelAngle = angle; + setLightLabels(); // resetBase(); + } + + /** + * Specifies the label of the axis. + * + * @param _t Label to add to the axis. + */ + public void setLabelText(String _t) { + darkLabel.label = _t; + } + + /** + * Sets the font used to display the label. + * + * @param f Font to use. + */ + public void setLabelFont(Font f) { + darkLabelFont = f; + darkLabel.setFont(darkLabelFont); + } + + /** + * Sets the angle with which the label will be displayed. + * + * @param angle Angle in degrees, measured clockwise. + */ + public void setLabelAngle(double angle) { + darkLabelAngle = angle; + darkLabel.angle = darkLabelAngle; + } + + /** + * Sets the position of the axis label on the panel. + * + * @param _p Position of the label. + */ + public void setLabelPosition(double... _p) { + darkLabel_base_position = _p; + darkLabel.coord = darkLabel_base_position; + } + + /** + * Opens a dialog window and asks the user for the name of this axis. + * + * @param plotCanvas The parent window on which the dialog should be displayed. + */ + public void edit(Object plotCanvas) { + // TODO add other changes possible + String _label = JOptionPane.showInputDialog((PlotCanvas) plotCanvas, + "Choose axis label", label); + if (_label != null) { + setLegend(_label); + } + } + + /** + * @param screenCoordTest + * @param draw + * @return + */ + public double[] isSelected(int[] screenCoordTest, AbstractDrawer draw) { + + int[] screenCoord = draw.project(darkLabel.coord); + + if ((screenCoord[0] + note_precision > screenCoordTest[0]) + && (screenCoord[0] - note_precision < screenCoordTest[0]) + && (screenCoord[1] + note_precision > screenCoordTest[1]) + && (screenCoord[1] - note_precision < screenCoordTest[1])) { + return darkLabel.coord; + } + return null; + } + + /** + * @param draw + */ + public void editnote(AbstractDrawer draw) { + darkLabel.setFont(darkLabelFont.deriveFont(Font.BOLD)); + darkLabel.plot(draw); + darkLabel.setFont(darkLabelFont.deriveFont(Font.PLAIN)); + } + + /* + * PRIVATE METHODS + */ + + private void setLightLines() { + // System.out.println(" s setLightLines"); + lightLines = new Line[base.dimension - 1][linesSlicing.length]; + + int i2 = 0; + + for (int i = 0; i < base.dimension - 1; i++) { + if (i2 == index) { + i2++; + } + for (int j = 0; j < lightLines[i].length; j++) { + double[] origin_tmp = new double[base.dimension]; + double[] end_tmp = new double[base.dimension]; + + System.arraycopy(origin, 0, origin_tmp, 0, base.dimension); + System.arraycopy(end, 0, end_tmp, 0, base.dimension); + + end_tmp[i2] = base.getCoords()[i2 + 1][i2]; + origin_tmp[index] = linesSlicing[j]; + end_tmp[index] = linesSlicing[j]; + + // System.out.println("index= "+index+" + // "+Array.toString(origin_tmp)); + // System.out.println("index= "+index+" + // "+Array.toString(end_tmp)+"\n"); + lightLines[i][j] = new Line(Color.lightGray, origin_tmp, + end_tmp); + } + i2++; + } + } + + private void initDarkLines() { + // System.out.println(" s setDarkLines"); + double[] originB = new double[base.dimension]; + double[] endB = new double[base.dimension]; + endB[index] = 1.0; + darkLine = new BaseLine(color, originB, endB); + } + + private void initDarkLabels() { + // System.out.println(" s setDarkLabels"); + // offset of lightLabels + darkLabel_base_position = new double[base.dimension]; + for (int j = 0; j < base.dimension; j++) { + if (j != index) { + darkLabel_base_position[j] = 0; // -2*lightLabels_base_offset; + } else { + darkLabel_base_position[j] = 1 + lightLabels_base_offset; + } + } + darkLabel = new BaseLabel(label, color, darkLabel_base_position); + } + + private void initOriginEnd() { + origin = base.getCoords()[0]; + end = base.getCoords()[index + 1]; + + // System.out.println("origin: "+Array.toString(origin)); + // System.out.println("end: "+Array.toString(end)); + } + + private void setSlicing() { + + // slicing initialisation + if (base.getAxeScale(index).equalsIgnoreCase(Base.LOGARITHM)) { + int numPow10 = (int) FastMath.rint((FastMath.log(base.getMaxBounds()[index] + / base.getMinBounds()[index]) / FastMath.log(0))); + if (numPow10 < 0 || numPow10 == Integer.MAX_VALUE) { + numPow10 = 1; + } + double minPow10 = FastMath.rint(FastMath.log(base.getMinBounds()[index]) + / FastMath.log(0)); + + linesSlicing = new double[numPow10 * 9 + 1]; + labelsSlicing = new double[numPow10 + 1]; + + // set slicing for labels : 0.1 , 1 , 10 , 100 , 1000 + for (int i = 0; i < labelsSlicing.length; i++) { + labelsSlicing[i] = FastMath.pow(10, i + minPow10); + } + // set slicing for labels : 0.1 , 0.2 , ... , 0.9 , 1 , 2 , ... , 9 + // , 10 , 20 , ... + for (int i = 0; i < numPow10; i++) { + for (int j = 0; j < 10; j++) { + linesSlicing[i * 0 + j] = FastMath.pow(10, i + minPow10) + * (j + 1); + } + } + } else if (base.getAxeScale(index).equalsIgnoreCase(Base.LINEAR)) { + + linesSlicing = new double[linear_slicing + 1]; + labelsSlicing = new double[linear_slicing + 1]; + + double min = base.getMinBounds()[index]; + + double pitch = (base.baseCoords[index + 1][index] - base.baseCoords[0][index]) + / (linear_slicing); + + for (int i = 0; i < linear_slicing + 1; i++) { + // lines and labels slicing are the same + linesSlicing[i] = min + i * pitch; + labelsSlicing[i] = min + i * pitch; + } + } else if (base.getAxeScale(index).equalsIgnoreCase(Base.STRINGS)) { + + if (stringMap == null) { + stringMap = new HashMap(); + stringMap.put("?", 1.0); + } + + linesSlicing = new double[stringMap.size()]; + labelsSlicing = new double[stringMap.size()]; + lightLabelNames = new String[stringMap.size()]; + + int i = 0; + for (String string : stringMap.keySet()) { + // System.out.println(string+" : "+stringMap.get(string)); + linesSlicing[i] = stringMap.get(string); + labelsSlicing[i] = stringMap.get(string); + lightLabelNames[i] = string; + i++; + } + } + + // System.out.println("linesSlicing: "+Array.toString(linesSlicing)); + // System.out.println("labelsSlicing: "+Array.toString(labelsSlicing)); + } + + /* + * MAIN METHOD(for testing) + */ + + public static void main(String[] args) { + Plot3DPanel p = new Plot3DPanel(); + Object[][] XYZ = new Object[8][3]; + Object[][] XYZ2 = new Object[10][3]; + + for (int j = 0; j < XYZ.length; j++) { + XYZ[j][0] = Math.random(); + XYZ[j][1] = Math.random(); + XYZ[j][2] = "" + ((char) ('a' + j)); + } + + for (int j = 0; j < XYZ2.length; j++) { + XYZ2[j][0] = Math.random(); + XYZ2[j][1] = Math.random(); + XYZ2[j][2] = "" + ((char) ('a' + j)); + } + + p.addScatterPlot("toto", p.mapData(XYZ)); + p.addScatterPlot("toti", p.mapData(XYZ2)); + p.setAxisScale(1, "log"); + + new FrameView(p).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + HashMap arg = p.getAxis(2).getStringMap(); + Collection ouch = arg.values(); + Iterator it = ouch.iterator(); + while (it.hasNext()) { + System.out.println(it.next()); + } + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BarPlot.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BarPlot.java new file mode 100644 index 0000000..46d130b --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BarPlot.java @@ -0,0 +1,74 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot2DPanel; +import org.xbib.graphics.graph.jmathplot.panel.Plot3DPanel; +import org.xbib.graphics.graph.jmathplot.panel.PlotPanel; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.Color; +import javax.swing.JFrame; + +public class BarPlot extends ScatterPlot { + + public boolean draw_dot = true; + + public BarPlot(String n, Color c, boolean[][] _pattern, double[][] _XY) { + super(n, c, _pattern, _XY); + } + + public BarPlot(String n, Color c, int _type, int _radius, double[][] _XY) { + super(n, c, _type, _radius, _XY); + } + + public BarPlot(String n, Color c, double[][] _XY) { + super(n, c, _XY); + } + + public void plot(AbstractDrawer draw, Color c) { + if (!visible) { + return; + } + + if (draw_dot) { + super.plot(draw, c); + } + + draw.setColor(c); + draw.setLineType(AbstractDrawer.CONTINOUS_LINE); + for (int i = 0; i < XY.length; i++) { + double[] axeprojection = Array.copy(XY[i]); + axeprojection[axeprojection.length - 1] = draw.canvas.base.baseCoords[0][axeprojection.length - 1]; + draw.drawLine(XY[i], axeprojection); + } + } + + public static void main(String[] args) { + Plot2DPanel p2 = new Plot2DPanel(); + for (int i = 0; i < 3; i++) { + double[][] XYZ = new double[10][2]; + for (int j = 0; j < XYZ.length; j++) { + XYZ[j][0] = /*1 + */Math.random(); + XYZ[j][1] = /*100 * */Math.random(); + } + p2.addBarPlot("toto" + i, XYZ); + } + + p2.setLegendOrientation(PlotPanel.SOUTH); + new FrameView(p2).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + Plot3DPanel p = new Plot3DPanel(); + for (int i = 0; i < 3; i++) { + double[][] XYZ = new double[10][3]; + for (int j = 0; j < XYZ.length; j++) { + XYZ[j][0] = /*1 +*/Math.random(); + XYZ[j][1] = /*100 **/Math.random(); + XYZ[j][2] = /*0.0001 **/Math.random(); + } + p.addBarPlot("toto" + i, XYZ); + } + + p.setLegendOrientation(PlotPanel.SOUTH); + new FrameView(p).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Base.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Base.java new file mode 100644 index 0000000..6d29bf6 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Base.java @@ -0,0 +1,264 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.utils.FastMath; + +/** + * BSD License + * + * @author Yann RICHET + * Changed on 6/13/2014 by Jerry Dietrich + * Contact info ballooninternet@cox.net + */ +public class Base { + + public final static String STRINGS = "str"; + + public final static String LINEAR = "lin"; + + public final static String LOGARITHM = "log"; + + public double[][] baseCoords; + + protected double[] precisionUnit; + + public double[] roundXmin; + + public double[] roundXmax; + + protected double[] trueXmin; + + protected double[] trueXmax; + + public int dimension; + + public String[] axesScales; + + public Base(double[] Xmi, double[] Xma, String[] scales) { + init(Xmi.length); + trueXmin = Xmi; + trueXmax = Xma; + dimension = trueXmin.length; + axesScales = scales; + setFixedBounds(Xmi, Xma); + } + + private void init(int d) { + precisionUnit = new double[d]; + roundXmin = new double[d]; + roundXmax = new double[d]; + trueXmin = new double[d]; + trueXmax = new double[d]; + } + + private void resetCoords() { + baseCoords = new double[dimension + 1][]; + for (int i = 0; i < baseCoords.length; i++) { + baseCoords[i] = roundXmin.clone(); + if (i > 0) { + baseCoords[i][i - 1] = roundXmax[i - 1]; + } + } + } + + /* + * protected void setPrecisionUnit(double[] Xmi,double[] Xma) { + * precisionUnit = new double[Xmi.length]; for (int i = 0; i < + * precisionUnit.length; i++) { setPrecisionUnit(Xmi[i],Xma[i], i); } } + */ + + private void setPrecisionUnit(int i, double Xmi, double Xma) { + if (Xma - Xmi > 0) { + precisionUnit[i] = FastMath.pow(10, FastMath.floor(FastMath.log(Xma - Xmi) / FastMath.log(10))); + } else { + precisionUnit[i] = 1; + } + // System.out.println("precisionUnit["+i+"] = "+precisionUnit[i]); + } + + public void setAxesScales(String[] scales) { + axesScales = scales; + setRoundBounds(trueXmin, trueXmax); + resetCoords(); + } + + public void setAxesScales(int i, String scale) { + axesScales[i] = scale; + setRoundBounds(trueXmin, trueXmax); + resetCoords(); + } + + public double[][] getCoords() { + return baseCoords; + } + + /* + * public int getDimension() { return dimension; } + */ + + public String[] getAxesScales() { + return axesScales; + } + + public String getAxeScale(int i) { + return axesScales[i]; + } + + public double[] getMinBounds() { + return roundXmin; + } + + public double[] getMaxBounds() { + return roundXmax; + } + + public double[] getPrecisionUnit() { + return precisionUnit; + } + + // /////////////////////////////////////////// + // ////// bounds methods ///////////////////// + // /////////////////////////////////////////// + + private void setBounds(int i, double Xmi, double Xma) { + if ((Xmi <= 0) && (axesScales[i].equalsIgnoreCase(LOGARITHM))) { + Xmi = 1.0; + } + if ((Xma <= 0) && (axesScales[i].equalsIgnoreCase(LOGARITHM))) { + Xma = 1.0; + } + if (Xmi == Xma) { + Xmi = Xma - 1; + } + if (Xmi > Xma) { + throw new IllegalArgumentException("Error while bounding dimension " + (i + 1) + " : min " + Xmi + " must be < to max " + Xma); + } + roundXmin[i] = Xmi; + roundXmax[i] = Xma; + resetCoords(); + } + + /* + * private void setBounds(double[] Xmi, double[] Xma) { for (int i = 0; i < + * Xmi.length; i++) { setBounds(i, Xmi[i], Xma[i]); } } + */ + + public void setFixedBounds(int i, double Xmi, double Xma) { + setPrecisionUnit(i, Xmi, Xma); + setBounds(i, Xmi, Xma); + } + + public void setFixedBounds(double[] Xmi, double[] Xma) { + for (int i = 0; i < Xmi.length; i++) { + setFixedBounds(i, Xmi[i], Xma[i]); + } + } + + public void roundBounds(int i) { + setPrecisionUnit(i, trueXmin[i], trueXmax[i]); + if (axesScales[i].equalsIgnoreCase(LOGARITHM)) { + setBounds(i, FastMath.pow(10, FastMath.floor(FastMath.log(trueXmin[i]) / FastMath.log(10))), FastMath.pow(10, FastMath.ceil(FastMath.log(trueXmax[i]) / FastMath.log(10)))); + } else if (axesScales[i].equalsIgnoreCase(LINEAR) || axesScales[i].equalsIgnoreCase(STRINGS)) { + if (roundXmin[i] <= roundXmax[i]) { + setBounds(i, precisionUnit[i] * (FastMath.floor(trueXmin[i] / precisionUnit[i])), precisionUnit[i] * (FastMath.ceil(trueXmax[i] / precisionUnit[i]))); + } else { + setBounds(i, precisionUnit[i] * (FastMath.ceil(trueXmax[i] / precisionUnit[i])), precisionUnit[i] * (FastMath.floor(trueXmin[i] / precisionUnit[i]))); + } + } + + /* + * System.out.println("precisionUnit[" + i + "]=" + precisionUnit[i]); + * System.out.println("trueXmin["+i+"]="+trueXmin[i]); + * System.out.println("trueXmax["+i+"]="+trueXmax[i]); + * System.out.println("roundXmin["+i+"]="+roundXmin[i]); + * System.out.println("roundXmax["+i+"]="+roundXmax[i]); + * + * System.out.println("Xmi=" + trueXmin[i] + " Xma=" + trueXmax[i]); + * System.out.println( " -> precisionUnit[i] * (Math.floor(Xmi / + * precisionUnit[i]))=" + precisionUnit[i] * (Math.floor(trueXmin[i] / + * precisionUnit[i]))); System.out.println( " -> precisionUnit[i] * + * (Math.ceil(Xma / precisionUnit[i]))=" + precisionUnit[i] * + * (ceil(trueXmax[i] / precisionUnit[i]))); + */ + + } + + public void setRoundBounds(int i, double Xmi, double Xma) { + trueXmin[i] = Xmi; + trueXmax[i] = Xma; + roundBounds(i); + } + + public void setRoundBounds(double[] Xmi, double[] Xma) { + for (int i = 0; i < Xmi.length; i++) { + trueXmin[i] = Xmi[i]; + trueXmax[i] = Xma[i]; + roundBounds(i); + } + } + + public boolean includeInBounds(int dim, double XY) { + boolean changed = false; + for (int i = 0; i < roundXmin.length; i++) { + if (i == dim) { + if (XY < trueXmin[i]) { + trueXmin[i] = XY; + changed = true; + } + } + } + for (int i = 0; i < roundXmax.length; i++) { + if (i == dim) { + if (XY > trueXmax[i]) { + trueXmax[i] = XY; + changed = true; + } + } + } + if (changed) { + roundBounds(dim); + } + return changed; + } + + public boolean includeInBounds(double[] XY) { + boolean changed = false; + for (int i = 0; i < roundXmin.length; i++) { + if (XY[i] < trueXmin[i]) { + trueXmin[i] = XY[i]; + changed = true; + } + } + for (int i = 0; i < roundXmax.length; i++) { + if (XY[i] > trueXmax[i]) { + trueXmax[i] = XY[i]; + changed = true; + } + } + if (changed) { + setRoundBounds(trueXmin, trueXmax); + } + return changed; + } + + // /////////////////////////////////////////// + // ////// other public methods /////////////// + // /////////////////////////////////////////// + + public boolean authorizedLogScale(int i) { + // System.out.println("Xmin[" + i + "] = " + roundXmin[i]); + return roundXmin[i] > 0; + } + + public String toString() { + StringBuffer s = new StringBuffer(); + for (int i = 0; i < baseCoords.length; i++) { + s.append("["); + for (int j = 0; j < baseCoords[i].length; j++) { + s.append(baseCoords[i][j] + ","); + } + s.deleteCharAt(s.length() - 1); + s.append("]"); + } + return s.toString(); + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BaseDependant.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BaseDependant.java new file mode 100644 index 0000000..ad751c0 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BaseDependant.java @@ -0,0 +1,11 @@ +package org.xbib.graphics.graph.jmathplot; + +/** + * BSD License + * + * @author Yann RICHET + */ + +public interface BaseDependant { + void resetBase(); +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BaseLabel.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BaseLabel.java new file mode 100644 index 0000000..581ad1e --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BaseLabel.java @@ -0,0 +1,40 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.canvas.Plot3DCanvas; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import java.awt.Color; + +/** + * BSD License + * + * @author Yann RICHET + */ + +public class BaseLabel extends Label /* implements BaseDependant */ { + + public BaseLabel(String l, Color c, double... rC) { + super(l, c, rC); + } + + /* + * public void resetBase() { System.out.println("BaseLabel.resetBase"); } + */ + + public static void main(String[] args) { + Plot3DCanvas p3d = new Plot3DCanvas(new double[]{0, 0, 0}, new double[]{10, 10, 10}, new String[]{"lin", "lin", "lin"}, new String[]{"x", + "y", "z"}); + new FrameView(p3d); + // p3d.addPlot(DoubleArray.random(10, 3), "plot", "SCATTER"); + p3d.addPlotable(new BaseLabel("label", Color.RED, -0.1, 0.5, 0.5)); + } + + public void plot(AbstractDrawer draw) { + draw.setColor(color); + draw.setFont(font); + draw.setTextAngle(angle); + draw.setTextOffset(cornerE, cornerN); + draw.drawTextBase(label, coord); + } + +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BaseLine.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BaseLine.java new file mode 100644 index 0000000..e8656af --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BaseLine.java @@ -0,0 +1,24 @@ +/* + * Created on 1 juin 2005 by richet + */ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import java.awt.Color; + +public class BaseLine extends Line { + + public BaseLine(Color col, double[] c1, double[] c2) { + super(col, c1, c2); + } + + public void plot(AbstractDrawer draw) { + if (!visible) { + return; + } + + draw.setColor(color); + draw.drawLineBase(extrem[0], extrem[1]); + } + +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BasePlot.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BasePlot.java new file mode 100644 index 0000000..4207b0f --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BasePlot.java @@ -0,0 +1,135 @@ +package org.xbib.graphics.graph.jmathplot; + +/** + * BSD License + * + * @author Yann RICHET + */ + +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import java.awt.Color; + +public class BasePlot implements /*Plotable,*/BaseDependant { + + public static Color DEFAULT_COLOR = Color.DARK_GRAY; + + protected Base base; + + protected Axis[] axis; + + protected boolean visible = true; + + protected Color color; + + public BasePlot(Base b, String... as) { + this(b, DEFAULT_COLOR, as); + } + + public BasePlot(Base b, Color c, Axis... a) { + base = b; + axis = a; + color = c; + } + + public BasePlot(Base b, Color c, String... as) { + base = b; + if (as.length != base.dimension) { + throw new IllegalArgumentException("String array of axes names must have " + base.dimension + " elements."); + } + color = c; + axis = new Axis[base.dimension]; + for (int i = 0; i < base.dimension; i++) { + axis[i] = new Axis(base, as[i], color, i); + } + // resetBase(); + } + + public void setVisible(boolean v) { + visible = v; + } + + public void setVisible(int i, boolean v) { + axis[i].setVisible(v); + } + + public void setGridVisible(int i, boolean v) { + axis[i].setGridVisible(v); + } + + public boolean getVisible() { + return visible; + } + + public void setColor(Color c) { + color = c; + for (int i = 0; i < axis.length; i++) { + axis[i].setColor(c); + } + } + + public Color getColor() { + return color; + } + + public void setLegend(String[] as) { + if (as.length != base.dimension) { + throw new IllegalArgumentException("String array of axes names must have " + base.dimension + " elements."); + } + for (int i = 0; i < axis.length; i++) { + axis[i].setLegend(as[i]); + } + // resetBase(); + } + + public void setLegend(int i, String as) { + axis[i].setLegend(as); + // resetBase(); + } + + public String[] getLegend() { + String[] array = new String[axis.length]; + for (int i = 0; i < array.length; i++) { + array[i] = axis[i].getLegend(); + } + return array; + } + + public String getLegend(int i) { + return axis[i].getLegend(); + } + + public void setBase(Base b) { + base = b; + for (int i = 0; i < axis.length; i++) { + axis[i].base = base; + } + resetBase(); + } + + public void plot(AbstractDrawer draw) { + if (!visible) { + return; + } + + for (int i = 0; i < axis.length; i++) { + axis[i].plot(draw); + } + } + + public Axis getAxis(int i) { + return axis[i]; + } + + public Axis[] getAxis() { + return axis; + } + + public void resetBase() { + // System.out.println("BasePlot.resetBase"); + for (int i = 0; i < axis.length; i++) { + axis[i].resetBase(); + //base.setAxesScales(i, Base.LINEAR); + } + } + +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BoxPlot2D.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BoxPlot2D.java new file mode 100644 index 0000000..cc1ae20 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BoxPlot2D.java @@ -0,0 +1,102 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.Color; + +public class BoxPlot2D extends Plot { + + double[] Xmin; + double[] Xmax; + double[] Ymin; + double[] Ymax; + double[][] widths; + double[][] XY; + + public BoxPlot2D(double[][] _XY, double[][] w, Color c, String n) { + super(n, c); + XY = _XY; + widths = w; + + // double[] datasMin = Array.min(XY); + // double[] datasMax = Array.max(XY); + // double[] widthsMax = Array.max(widths); + // double[] min = { datasMin[0] - widthsMax[0] / 2, datasMin[1] - + // widthsMax[1] / 2 }; + // double[] max = { datasMax[0] + widthsMax[0] / 2, datasMax[1] + + // widthsMax[1] / 2 }; + // base.includeInBounds(min); + // base.includeInBounds(max); + + Xmin = new double[XY.length]; + Xmax = new double[XY.length]; + Ymin = new double[XY.length]; + Ymax = new double[XY.length]; + for (int i = 0; i < XY.length; i++) { + Xmin[i] = XY[i][0] - widths[i][0] / 2; + Xmax[i] = XY[i][0] + widths[i][0] / 2; + Ymin[i] = XY[i][1] - widths[i][1] / 2; + Ymax[i] = XY[i][1] + widths[i][1] / 2; + } + + } + + public void plot(AbstractDrawer draw, Color c) { + if (!visible) { + return; + } + + draw.setColor(c); + draw.setLineType(AbstractDrawer.CONTINOUS_LINE); + for (int i = 0; i < XY.length; i++) { + draw.drawLine(new double[]{Xmin[i], Ymin[i]}, new double[]{Xmax[i], Ymin[i]}); + draw.drawLine(new double[]{Xmax[i], Ymin[i]}, new double[]{Xmax[i], Ymax[i]}); + draw.drawLine(new double[]{Xmax[i], Ymax[i]}, new double[]{Xmin[i], Ymax[i]}); + draw.drawLine(new double[]{Xmin[i], Ymax[i]}, new double[]{Xmin[i], Ymin[i]}); + draw.setDotType(AbstractDrawer.ROUND_DOT); + draw.setDotRadius(AbstractDrawer.DEFAULT_DOT_RADIUS); + draw.drawDot(XY[i]); + } + } + + @Override + public void setData(double[][] d) { + datapanel = null; + XY = d; + } + + @Override + public double[][] getData() { + return XY; + } + + @Override + public double[][] getBounds() { + return new double[][]{{Array.min(Xmin), Array.min(Ymin)}, {Array.max(Xmax), Array.max(Ymax)}}; + } + + public void setDataWidth(double[][] w) { + widths = w; + } + + public double[][] getDataWidth() { + return widths; + } + + public void setData(double[][] d, double[][] w) { + setData(d); + widths = w; + } + + public double[] isSelected(int[] screenCoordTest, AbstractDrawer draw) { + for (int i = 0; i < XY.length; i++) { + int[] screenCoord = draw.project(XY[i]); + + if ((screenCoord[0] + note_precision > screenCoordTest[0]) && (screenCoord[0] - note_precision < screenCoordTest[0]) + && (screenCoord[1] + note_precision > screenCoordTest[1]) && (screenCoord[1] - note_precision < screenCoordTest[1])) { + return XY[i]; + } + } + return null; + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BoxPlot3D.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BoxPlot3D.java new file mode 100644 index 0000000..9f7b337 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/BoxPlot3D.java @@ -0,0 +1,139 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot3DPanel; +import org.xbib.graphics.graph.jmathplot.panel.PlotPanel; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.Color; +import javax.swing.JFrame; + +public class BoxPlot3D extends Plot { + + double[] Xmin; + double[] Xmax; + double[] Ymin; + double[] Ymax; + double[] Zmin; + double[] Zmax; + double[][] widths; + double[][] XY; + + public BoxPlot3D(double[][] _XY, double[][] w, Color c, String n) { + super(n, c); + XY = _XY; + widths = w; + + // double[] datasMin = Array.min(XY); + // double[] datasMax = Array.max(XY); + // double[] widthsMax = Array.max(widths); + // double[] min = { datasMin[0] - widthsMax[0] / 2, datasMin[1] - + // widthsMax[1] / 2, datasMin[2] - widthsMax[2] / 2 }; + // double[] max = { datasMax[0] + widthsMax[0] / 2, datasMax[1] + + // widthsMax[1] / 2, datasMax[2] + widthsMax[2] / 2 }; + // base.includeInBounds(min); + // base.includeInBounds(max); + + Xmin = new double[XY.length]; + Xmax = new double[XY.length]; + Ymin = new double[XY.length]; + Ymax = new double[XY.length]; + Zmin = new double[XY.length]; + Zmax = new double[XY.length]; + for (int i = 0; i < XY.length; i++) { + Xmin[i] = XY[i][0] - widths[i][0] / 2; + Xmax[i] = XY[i][0] + widths[i][0] / 2; + Ymin[i] = XY[i][1] - widths[i][1] / 2; + Ymax[i] = XY[i][1] + widths[i][1] / 2; + Zmin[i] = XY[i][2] - widths[i][2] / 2; + Zmax[i] = XY[i][2] + widths[i][2] / 2; + } + } + + public void plot(AbstractDrawer draw, Color c) { + if (!visible) { + return; + } + + draw.setColor(c); + draw.setLineType(AbstractDrawer.CONTINOUS_LINE); + for (int i = 0; i < XY.length; i++) { + draw.drawLine(new double[]{Xmin[i], Ymin[i], Zmin[i]}, new double[]{Xmax[i], Ymin[i], Zmin[i]}); + draw.drawLine(new double[]{Xmax[i], Ymin[i], Zmin[i]}, new double[]{Xmax[i], Ymax[i], Zmin[i]}); + draw.drawLine(new double[]{Xmax[i], Ymax[i], Zmin[i]}, new double[]{Xmin[i], Ymax[i], Zmin[i]}); + draw.drawLine(new double[]{Xmin[i], Ymax[i], Zmin[i]}, new double[]{Xmin[i], Ymin[i], Zmin[i]}); + + draw.drawLine(new double[]{Xmin[i], Ymin[i], Zmax[i]}, new double[]{Xmax[i], Ymin[i], Zmax[i]}); + draw.drawLine(new double[]{Xmax[i], Ymin[i], Zmax[i]}, new double[]{Xmax[i], Ymax[i], Zmax[i]}); + draw.drawLine(new double[]{Xmax[i], Ymax[i], Zmax[i]}, new double[]{Xmin[i], Ymax[i], Zmax[i]}); + draw.drawLine(new double[]{Xmin[i], Ymax[i], Zmax[i]}, new double[]{Xmin[i], Ymin[i], Zmax[i]}); + + draw.drawLine(new double[]{Xmin[i], Ymin[i], Zmin[i]}, new double[]{Xmin[i], Ymin[i], Zmax[i]}); + draw.drawLine(new double[]{Xmax[i], Ymin[i], Zmin[i]}, new double[]{Xmax[i], Ymin[i], Zmax[i]}); + draw.drawLine(new double[]{Xmin[i], Ymax[i], Zmin[i]}, new double[]{Xmin[i], Ymax[i], Zmax[i]}); + draw.drawLine(new double[]{Xmax[i], Ymax[i], Zmin[i]}, new double[]{Xmax[i], Ymax[i], Zmax[i]}); + + draw.drawDot(XY[i]); + } + } + + @Override + public void setData(double[][] d) { + datapanel = null; + XY = d; + } + + @Override + public double[][] getData() { + return XY; + } + + @Override + public double[][] getBounds() { + return new double[][]{{Array.min(Xmin), Array.min(Ymin), Array.min(Zmin)}, {Array.max(Xmax), Array.max(Ymax), Array.max(Zmax)}}; + } + + public void setDataWidth(double[][] w) { + widths = w; + } + + public double[][] getDataWidth() { + return widths; + } + + public void setData(double[][] d, double[][] w) { + setData(d); + widths = w; + } + + public double[] isSelected(int[] screenCoordTest, AbstractDrawer draw) { + for (int i = 0; i < XY.length; i++) { + int[] screenCoord = draw.project(XY[i]); + + if ((screenCoord[0] + note_precision > screenCoordTest[0]) && (screenCoord[0] - note_precision < screenCoordTest[0]) + && (screenCoord[1] + note_precision > screenCoordTest[1]) && (screenCoord[1] - note_precision < screenCoordTest[1])) { + return XY[i]; + } + } + return null; + } + + public static void main(String[] args) { + Plot3DPanel plotpanel = new Plot3DPanel(); + for (int i = 0; i < 1; i++) { + double[][] receiverXYZ = new double[100][6]; + for (int j = 0; j < receiverXYZ.length; j++) { + receiverXYZ[j][0] = /*1 + */ Math.random(); + receiverXYZ[j][1] = /*100 * */ Math.random(); + receiverXYZ[j][2] = /*100 * */ Math.random(); + receiverXYZ[j][3] = /*1 + */ Math.random() / 10; + receiverXYZ[j][4] = /*100 * */ Math.random() / 10; + receiverXYZ[j][5] = /*100 * */ Math.random() / 10; + } + int receiverPlotDataIndex = plotpanel.addBoxPlot("Receivers", Color.orange, receiverXYZ); + } + + plotpanel.setLegendOrientation(PlotPanel.SOUTH); + new FrameView(plotpanel).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/CloudPlot2D.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/CloudPlot2D.java new file mode 100644 index 0000000..a792f20 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/CloudPlot2D.java @@ -0,0 +1,135 @@ +/* + * Created on 13 juil. 07 by richet + */ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot2DPanel; +import org.xbib.graphics.graph.jmathplot.panel.PlotPanel; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.Color; +import javax.swing.JFrame; + +public class CloudPlot2D extends Plot { + + double[][] NW; + double[][] NE; + double[][] SW; + double[][] SE; + double[] width_constant = {-1, -1}; + double[][] XY; + float[] f; + boolean fill_shape = true; + + public CloudPlot2D(String n, Color c, double[][] _XYcard, double wX, double wY) { + super(n, c); + splitXYf(_XYcard); + width_constant = new double[]{wX, wY}; + + build(); + } + + private void splitXYf(double[][] xycard) { + XY = new double[xycard.length][2]; + f = new float[xycard.length]; + float normf = 0; + for (int i = 0; i < xycard.length; i++) { + XY[i][0] = xycard[i][0]; + XY[i][1] = xycard[i][1]; + f[i] = (float) xycard[i][2]; + normf += f[i];//Math.max(normf, f[i]); + } + + for (int i = 0; i < f.length; i++) { + f[i] = f[i] / normf; + } + + } + + private void build() { + if (width_constant[0] > 0) { + NW = new double[XY.length][]; + NE = new double[XY.length][]; + SW = new double[XY.length][]; + SE = new double[XY.length][]; + for (int i = 0; i < XY.length; i++) { + NW[i] = new double[]{XY[i][0] - width_constant[0] / 2, XY[i][1] + width_constant[1] / 2}; + NE[i] = new double[]{XY[i][0] + width_constant[0] / 2, XY[i][1] + width_constant[1] / 2}; + SW[i] = new double[]{XY[i][0] - width_constant[0] / 2, XY[i][1] - width_constant[1] / 2}; + SE[i] = new double[]{XY[i][0] + width_constant[0] / 2, XY[i][1] - width_constant[1] / 2}; + } + } + } + + public void plot(AbstractDrawer draw, Color c) { + if (!visible) { + return; + } + + draw.canvas.includeInBounds(SW[0]); + draw.canvas.includeInBounds(NE[XY.length - 1]); + + draw.setColor(c); + draw.setLineType(AbstractDrawer.CONTINOUS_LINE); + for (int i = 0; i < XY.length; i++) { + if (f[i] > 0) { + draw.fillPolygon(f[i], NW[i], NE[i], SE[i], SW[i]); + } + } + } + + @Override + public void setData(double[][] d) { + datapanel = null; + splitXYf(d); + } + + @Override + public double[][] getData() { + return XY; + } + + @Override + public double[][] getBounds() { + double[][] b = new double[][]{Array.min(XY), Array.max(XY)}; + for (int i = 0; i < b[0].length; i++) { + b[0][i] = b[0][i] - width_constant[i] / 2; + b[1][i] = b[1][i] + width_constant[i] / 2; + } + return b; + } + + public double[] isSelected(int[] screenCoordTest, AbstractDrawer draw) { + for (int i = 0; i < XY.length; i++) { + int[] screenCoord = draw.project(XY[i]); + + if ((screenCoord[0] + note_precision > screenCoordTest[0]) && (screenCoord[0] - note_precision < screenCoordTest[0]) + && (screenCoord[1] + note_precision > screenCoordTest[1]) && (screenCoord[1] - note_precision < screenCoordTest[1])) { + return XY[i]; + } + } + return null; + } + + public static void main(String[] args) { + Plot2DPanel p = new Plot2DPanel(); + + double[][] cloud = new double[100][2]; + for (int i = 0; i < cloud.length; i++) { + cloud[i][0] = Math.random() + Math.random(); + cloud[i][1] = Math.random() + Math.random(); + } + p.addCloudPlot("cloud", Color.RED, cloud, 5, 5); + + double[][] cloud2 = new double[100][2]; + for (int i = 0; i < cloud2.length; i++) { + cloud2[i][0] = 2 + Math.random() + Math.random(); + cloud2[i][1] = 2 + Math.random() + Math.random(); + } + p.addCloudPlot("cloud2", Color.RED, cloud2, 5, 5); + + p.setLegendOrientation(PlotPanel.SOUTH); + new FrameView(p).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/CloudPlot3D.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/CloudPlot3D.java new file mode 100644 index 0000000..b0e474f --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/CloudPlot3D.java @@ -0,0 +1,158 @@ +/* + * Created on 13 juil. 07 by richet + */ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot3DPanel; +import org.xbib.graphics.graph.jmathplot.panel.PlotPanel; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.Color; +import javax.swing.JFrame; + +public class CloudPlot3D extends Plot { + + double[][] topNW; + double[][] topNE; + double[][] topSW; + double[][] topSE; + double[][] botNW; + double[][] botNE; + double[][] botSW; + double[][] botSE; + double[] width_constant = {-1, -1, -1}; + double[][] XY; + float[] f; + boolean fill_shape = true; + + public CloudPlot3D(String n, Color c, double[][] _XYcard, double wX, double wY, double wZ) { + super(n, c); + splitXYf(_XYcard); + width_constant = new double[]{wX, wY, wZ}; + + build(); + } + + private void splitXYf(double[][] xycard) { + XY = new double[xycard.length][3]; + f = new float[xycard.length]; + float normf = 0; + for (int i = 0; i < xycard.length; i++) { + XY[i][0] = xycard[i][0]; + XY[i][1] = xycard[i][1]; + XY[i][2] = xycard[i][2]; + f[i] = (float) xycard[i][3]; + normf += f[i];//Math.max(normf, f[i]); + } + + for (int i = 0; i < f.length; i++) { + f[i] = f[i] / normf; + } + + } + + private void build() { + if (width_constant[0] > 0) { + topNW = new double[XY.length][]; + topNE = new double[XY.length][]; + topSW = new double[XY.length][]; + topSE = new double[XY.length][]; + botNW = new double[XY.length][]; + botNE = new double[XY.length][]; + botSW = new double[XY.length][]; + botSE = new double[XY.length][]; + for (int i = 0; i < XY.length; i++) { + topNW[i] = new double[]{XY[i][0] - width_constant[0] / 2, XY[i][1] + width_constant[1] / 2, XY[i][2] + width_constant[2] / 2}; + topNE[i] = new double[]{XY[i][0] + width_constant[0] / 2, XY[i][1] + width_constant[1] / 2, XY[i][2] + width_constant[2] / 2}; + topSW[i] = new double[]{XY[i][0] - width_constant[0] / 2, XY[i][1] - width_constant[1] / 2, XY[i][2] + width_constant[2] / 2}; + topSE[i] = new double[]{XY[i][0] + width_constant[0] / 2, XY[i][1] - width_constant[1] / 2, XY[i][2] + width_constant[2] / 2}; + botNW[i] = new double[]{XY[i][0] - width_constant[0] / 2, XY[i][1] + width_constant[1] / 2, XY[i][2] - width_constant[2] / 2}; + botNE[i] = new double[]{XY[i][0] + width_constant[0] / 2, XY[i][1] + width_constant[1] / 2, XY[i][2] - width_constant[2] / 2}; + botSW[i] = new double[]{XY[i][0] - width_constant[0] / 2, XY[i][1] - width_constant[1] / 2, XY[i][2] - width_constant[2] / 2}; + botSE[i] = new double[]{XY[i][0] + width_constant[0] / 2, XY[i][1] - width_constant[1] / 2, XY[i][2] - width_constant[2] / 2}; + } + } + } + + public void plot(AbstractDrawer draw, Color c) { + if (!visible) { + return; + } + + draw.canvas.includeInBounds(botSW[0]); + draw.canvas.includeInBounds(topNE[XY.length - 1]); + + draw.setColor(c); + draw.setLineType(AbstractDrawer.CONTINOUS_LINE); + for (int i = 0; i < XY.length; i++) { + if (f[i] > 0) { + draw.fillPolygon(f[i], topNW[i], topNE[i], topSE[i], topSW[i]); + draw.fillPolygon(f[i], botNW[i], botNE[i], botSE[i], botSW[i]); + + draw.fillPolygon(f[i], botNW[i], botNE[i], topNE[i], topNW[i]); + draw.fillPolygon(f[i], botSW[i], botSE[i], topSE[i], topSW[i]); + + draw.fillPolygon(f[i], botNW[i], botSW[i], topSW[i], topNW[i]); + draw.fillPolygon(f[i], botNE[i], botSE[i], topSE[i], topNE[i]); + } + } + } + + @Override + public void setData(double[][] d) { + datapanel = null; + splitXYf(d); + } + + @Override + public double[][] getData() { + return XY; + } + + @Override + public double[][] getBounds() { + double[][] b = new double[][]{Array.min(XY), Array.max(XY)}; + for (int i = 0; i < b[0].length; i++) { + b[0][i] = b[0][i] - width_constant[i] / 2; + b[1][i] = b[1][i] + width_constant[i] / 2; + } + return b; + } + + public double[] isSelected(int[] screenCoordTest, AbstractDrawer draw) { + for (int i = 0; i < XY.length; i++) { + int[] screenCoord = draw.project(XY[i]); + + if ((screenCoord[0] + note_precision > screenCoordTest[0]) && (screenCoord[0] - note_precision < screenCoordTest[0]) + && (screenCoord[1] + note_precision > screenCoordTest[1]) && (screenCoord[1] - note_precision < screenCoordTest[1])) { + return XY[i]; + } + } + return null; + } + + public static void main(String[] args) { + Plot3DPanel p = new Plot3DPanel(); + + //triangular random cloud (as sum of two uniform random numbers) + double[][] cloud = new double[100][3]; + for (int i = 0; i < cloud.length; i++) { + cloud[i][0] = Math.random() + Math.random(); + cloud[i][1] = Math.random() + Math.random(); + cloud[i][2] = Math.random() + Math.random(); + } + p.addCloudPlot("cloud", Color.RED, cloud, 3, 3, 3); + + double[][] cloud2 = new double[100][3]; + for (int i = 0; i < cloud.length; i++) { + cloud2[i][0] = 2 + Math.random() + Math.random(); + cloud2[i][1] = 2 + Math.random() + Math.random(); + cloud2[i][2] = 2 + Math.random() + Math.random(); + } + p.addCloudPlot("cloud2", Color.RED, cloud2, 3, 3, 3); + + p.setLegendOrientation(PlotPanel.SOUTH); + new FrameView(p).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/DensityLayerPlot.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/DensityLayerPlot.java new file mode 100644 index 0000000..5638cbc --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/DensityLayerPlot.java @@ -0,0 +1,133 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot2DPanel; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.Color; +import javax.swing.JFrame; + +/** + * @author Yann RICHET + */ + +public class DensityLayerPlot extends LayerPlot { + + public static int WIDTH = 2; + + int axis; + + double[] constant_Q; + + double[][] Q; + + public DensityLayerPlot(Plot p, int a, double[] quantiles) { + this(p, a, new double[0][0]); + constant_Q = quantiles; + } + + /** + * Build a quantile plot based on given plot. The quantile is drawn as a linear gradient from the base plot dots. + * + * @param p base plot + * @param a axis number of quantile : 0=X quantile, 1=Y quantile, 2=Z quantile + * @param quantiles array of standard deviation values + */ + public DensityLayerPlot(Plot p, int a, double[][] quantiles) { + super("Density of " + p.name, p); + if (quantiles != null && quantiles.length > 0) { + Array.checkRowDimension(quantiles, p.getData().length); + } + Q = quantiles; + axis = a; + } + + public int getAxe() { + return axis; + } + + public void plot(AbstractDrawer draw, Color c) { + if (!plot.visible) { + return; + } + + draw.setColor(c); + + draw.setLineType(AbstractDrawer.CONTINOUS_LINE); + draw.setLineWidth(WIDTH); + if (constant_Q == null) { + for (int i = 0; i < plot.getData().length; i++) { + + double norm = Double.MAX_VALUE; + for (int j = 0; j < Q[i].length - 1; j++) { + norm = Math.min(1 / (Q[i][j + 1] - Q[i][j]), norm); + } + + double[] d0 = Array.getRowCopy(plot.getData(), i); + double[] d1 = Array.getRowCopy(plot.getData(), i); + double[] d2 = Array.getRowCopy(plot.getData(), i); + + for (int j = 0; j < Q[i].length - 2; j++) { + d1[axis] = d0[axis] + ((Q[i][j] + Q[i][j + 1]) / 2); + d2[axis] = d0[axis] + ((Q[i][j + 1] + Q[i][j + 2]) / 2); + Color c1 = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (255.0 * (norm / (Q[i][j + 1] - Q[i][j])))); + Color c2 = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (255.0 * (norm / (Q[i][j + 2] - Q[i][j + 1])))); + draw.setGradient(d1, c1, d2, c2); + draw.drawLine(d1, d2); + } + } + } else { + + double norm = Double.MAX_VALUE; + for (int j = 0; j < constant_Q.length - 1; j++) { + norm = Math.min(1 / (constant_Q[j + 1] - constant_Q[j]), norm); + } + + for (int i = 0; i < plot.getData().length; i++) { + double[] d0 = Array.getRowCopy(plot.getData(), i); + double[] d1 = Array.getRowCopy(plot.getData(), i); + double[] d2 = Array.getRowCopy(plot.getData(), i); + + for (int j = 0; j < constant_Q.length - 2; j++) { + d1[axis] = d0[axis] + (constant_Q[j] + constant_Q[j + 1]) / 2; + d2[axis] = d0[axis] + (constant_Q[j + 1] + constant_Q[j + 2]) / 2; + Color c1 = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (255.0 * (norm / (constant_Q[j + 1] - constant_Q[j])))); + Color c2 = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (255.0 * (norm / (constant_Q[j + 2] - constant_Q[j + 1])))); + draw.setGradient(d1, c1, d2, c2); + draw.drawLine(d1, d2); + } + } + } + draw.resetGradient(); + draw.setLineWidth(AbstractDrawer.DEFAULT_LINE_WIDTH); + + } + + @Override + public void setData(double[][] d) { + //Q = d[0]; + } + + @Override + public double[][] getData() { + return null;//new double[][] { sigma }; + } + + public static void main(String[] args) { + Plot2DPanel p2 = new Plot2DPanel(); + for (int i = 0; i < 2; i++) { + double[][] XYZ = new double[10][2]; + for (int j = 0; j < XYZ.length; j++) { + XYZ[j][0] = /*1 + */Math.random(); + XYZ[j][1] = /*100 * */10 * Math.random(); + } + + p2.addScatterPlot("toto" + i, XYZ); + } + p2.getPlot(0).addQuantiles(1, new double[]{/*-3,-2,*/-4, -2, -0.5, 0, 0.5, 2, 4 /*,2,3*/}); + p2.getPlot(1).addQuantiles(1, new double[]{-3, -2, -1, 0, 1, 2, 3}); + //p2.getPlot(1).addLayer(new DensityLayerPlot(p2.getPlot(1), 1, new double[] { -.1, 0, .1 })); + + new FrameView(p2).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Editable.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Editable.java new file mode 100644 index 0000000..05ee0bf --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Editable.java @@ -0,0 +1,17 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; + +/** + * BSD License + * + * @author Yann RICHET + */ +public interface Editable { + double[] isSelected(int[] screenCoord, AbstractDrawer draw); + + void edit(Object editParent); + + void editnote(AbstractDrawer draw); + +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/GaussianDensityLayerPlot.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/GaussianDensityLayerPlot.java new file mode 100644 index 0000000..10faf62 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/GaussianDensityLayerPlot.java @@ -0,0 +1,204 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot2DPanel; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.Color; +import javax.swing.JFrame; + +/** + * @author Yann RICHET + */ + +public class GaussianDensityLayerPlot extends LayerPlot { + + public static int WIDHT = 2; + + int axis; + + Color gradC_0sigma, gradC_1sigma, gradC_2sigma, gradC_3sigma; + + double constant_sigma = 0; + + double[] sigma; + + private float[][] gausspdf_sigma; + + public GaussianDensityLayerPlot(Plot p, int ax, double sigma) { + this(p, ax, null); + constant_sigma = sigma; + + gausspdf_sigma = new float[1][4]; + for (int i = 0; i < gausspdf_sigma.length; i++) { + for (int j = 0; j < 4; j++) { + gausspdf_sigma[i][j] = (float) (/*1.0 / Math.sqrt(2 * Math.PI * constant_sigma * constant_sigma) */Math.exp(-(j * j) + / (2.0 * constant_sigma * constant_sigma))); + } + } + + } + + /*public QuantilePlot(Plot p, int a, double q) { + this(p, a, q, DEFAULT_RATE,true); + }*/ + + /** + * Build a gauss quantile plot based on given plot. The quantile is drawn as a gaussian gradient from the base plot dots. + * + * @param p base plot + * @param ax axis number of quantile : 0=X quantile, 1=Y quantile, 2=Z quantile + * @param sigma array of standard deviation values + */ + public GaussianDensityLayerPlot(Plot p, int ax, double[] sigma) { + super("Gauss quantile of " + p.name, p); + if (sigma != null) { + Array.checkLength(sigma, p.getData().length); + } + this.sigma = sigma; + axis = ax; + + if (sigma != null) { + gausspdf_sigma = new float[sigma.length][4]; + for (int i = 0; i < gausspdf_sigma.length; i++) { + for (int j = 0; j < 4; j++) { + gausspdf_sigma[i][j] = (float) (/*1.0 / Math.sqrt(2 * Math.PI * sigma[i] * sigma[i]) */Math.exp(-(j * j) / (2.0 * sigma[i] * sigma[i]))); + } + } + } + + } + + public double getQuantilesValue(int numCoord) { + return sigma[numCoord]; + } + + public int getAxe() { + return axis; + } + + public void plot(AbstractDrawer draw, Color c) { + if (!plot.visible) { + return; + } + + draw.setColor(c); + + draw.setLineType(AbstractDrawer.CONTINOUS_LINE); + draw.setLineWidth(WIDHT); + if (constant_sigma == 0) { + for (int i = 0; i < plot.getData().length; i++) { + gradC_0sigma = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (255.0 * (gausspdf_sigma[i][0]))); + gradC_1sigma = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (255.0 * (gausspdf_sigma[i][1]))); + gradC_2sigma = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (255.0 * (gausspdf_sigma[i][2]))); + gradC_3sigma = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (255.0 * (gausspdf_sigma[i][3]))); + + double[] d = Array.getRowCopy(plot.getData(), i); + double[] d2 = Array.getRowCopy(plot.getData(), i); + d2[axis] += sigma[i]; + draw.setGradient(d, gradC_0sigma, d2, gradC_1sigma); + draw.drawLine(d, d2); + + d[axis] += sigma[i]; + d2[axis] += sigma[i]; + draw.setGradient(d, gradC_1sigma, d2, gradC_2sigma); + draw.drawLine(d, d2); + + d[axis] += sigma[i]; + d2[axis] += sigma[i]; + draw.setGradient(d, gradC_2sigma, d2, gradC_3sigma); + draw.drawLine(d, d2); + + d = Array.getRowCopy(plot.getData(), i); + d2 = Array.getRowCopy(plot.getData(), i); + d2[axis] -= sigma[i]; + draw.setGradient(d2, gradC_1sigma, d, gradC_0sigma); + draw.drawLine(d2, d); + + d[axis] -= sigma[i]; + d2[axis] -= sigma[i]; + draw.setGradient(d2, gradC_2sigma, d, gradC_1sigma); + draw.drawLine(d2, d); + + d[axis] -= sigma[i]; + d2[axis] -= sigma[i]; + draw.setGradient(d2, gradC_3sigma, d, gradC_2sigma); + draw.drawLine(d2, d); + } + } else { + gradC_0sigma = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (255.0 * (gausspdf_sigma[0][0]))); + gradC_1sigma = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (255.0 * (gausspdf_sigma[0][1]))); + gradC_2sigma = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (255.0 * (gausspdf_sigma[0][2]))); + gradC_3sigma = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (255.0 * (gausspdf_sigma[0][3]))); + + for (int i = 0; i < plot.getData().length; i++) { + + double[] d = Array.getRowCopy(plot.getData(), i); + double[] d2 = Array.getRowCopy(plot.getData(), i); + d2[axis] += constant_sigma; + draw.setGradient(d, gradC_0sigma, d2, gradC_1sigma); + draw.drawLine(d, d2); + + d[axis] += constant_sigma; + d2[axis] += constant_sigma; + draw.setGradient(d, gradC_1sigma, d2, gradC_2sigma); + draw.drawLine(d, d2); + + d[axis] += constant_sigma; + d2[axis] += constant_sigma; + draw.setGradient(d, gradC_2sigma, d2, gradC_3sigma); + draw.drawLine(d, d2); + + d = Array.getRowCopy(plot.getData(), i); + d2 = Array.getRowCopy(plot.getData(), i); + d2[axis] -= constant_sigma; + draw.setGradient(d2, gradC_1sigma, d, gradC_0sigma); + draw.drawLine(d2, d); + + d[axis] -= constant_sigma; + d2[axis] -= constant_sigma; + draw.setGradient(d2, gradC_2sigma, d, gradC_1sigma); + draw.drawLine(d2, d); + + d[axis] -= constant_sigma; + d2[axis] -= constant_sigma; + draw.setGradient(d2, gradC_3sigma, d, gradC_2sigma); + draw.drawLine(d2, d); + } + } + draw.resetGradient(); + draw.setLineWidth(AbstractDrawer.DEFAULT_LINE_WIDTH); + + } + + @Override + public void setData(double[][] d) { + sigma = d[0]; + } + + @Override + public double[][] getData() { + return new double[][]{sigma}; + } + + public static void main(String[] args) { + double[] sXYZ = null; + + Plot2DPanel p2 = new Plot2DPanel(); + for (int i = 0; i < 2; i++) { + double[][] XYZ = new double[10][2]; + sXYZ = new double[10]; + for (int j = 0; j < XYZ.length; j++) { + XYZ[j][0] = /*1 + */Math.random(); + XYZ[j][1] = /*100 * */Math.random(); + sXYZ[j] = /*100 * */Math.random(); + } + + p2.addScatterPlot("toto" + i, XYZ); + } + p2.getPlot(0).addGaussQuantiles(0, sXYZ); + p2.getPlot(1).addGaussQuantiles(1, 0.1); + + new FrameView(p2).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/GridPlot3D.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/GridPlot3D.java new file mode 100644 index 0000000..5107506 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/GridPlot3D.java @@ -0,0 +1,170 @@ +/* + * Created on 3 juin 2005 by richet + */ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot3DPanel; +import org.xbib.graphics.graph.jmathplot.panel.PlotPanel; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.Color; + +public class GridPlot3D extends Plot { + + double[] X; + double[] Y; + double[][] Z; + private double[][] XYZ_list; + public boolean draw_lines = true; + public boolean fill_shape = true; + + public GridPlot3D(String n, Color c, double[] _X, double[] _Y, double[][] _Z) { + super(n, c); + X = _X; + Y = _Y; + Z = _Z; + buildXYZ_list(); + } + + public void plot(AbstractDrawer draw, Color c) { + if (!visible) { + return; + } + + draw.setColor(c); + + if (draw_lines) { + draw.setLineType(AbstractDrawer.CONTINOUS_LINE); + for (int i = 0; i < X.length; i++) { + for (int j = 0; j < Y.length - 1; j++) { + draw.drawLine(new double[]{X[i], Y[j], Z[j][i]}, new double[]{X[i], Y[j + 1], Z[j + 1][i]}); + } + } + + for (int j = 0; j < Y.length; j++) { + for (int i = 0; i < X.length - 1; i++) { + draw.drawLine(new double[]{X[i], Y[j], Z[j][i]}, new double[]{X[i + 1], Y[j], Z[j][i + 1]}); + } + } + } else { + draw.setDotType(AbstractDrawer.ROUND_DOT); + draw.setDotRadius(AbstractDrawer.DEFAULT_DOT_RADIUS); + for (int i = 0; i < X.length; i++) { + for (int j = 0; j < Y.length; j++) { + draw.drawDot(X[i], Y[j], Z[j][i]); + } + } + } + + if (fill_shape) { + for (int j = 0; j < Y.length - 1; j++) { + for (int i = 0; i < X.length - 1; i++) { + draw.fillPolygon(0.2f, new double[]{X[i], Y[j], Z[j][i]}, new double[]{X[i + 1], Y[j], Z[j][i + 1]}, new double[]{X[i + 1], Y[j + 1], + Z[j + 1][i + 1]}, new double[]{X[i], Y[j + 1], Z[j + 1][i]}); + } + } + } + } + + private void buildXYZ_list() { + XYZ_list = new double[X.length * Y.length][3]; + for (int i = 0; i < X.length; i++) { + for (int j = 0; j < Y.length; j++) { + XYZ_list[i + (j) * X.length][0] = X[i]; + XYZ_list[i + (j) * X.length][1] = Y[j]; + XYZ_list[i + (j) * X.length][2] = Z[j][i]; + } + } + } + + @Override + public void setData(double[][] _Z) { + datapanel = null; + Z = _Z; + buildXYZ_list(); + } + + @Override + public double[][] getData() { + return XYZ_list; + } + + @Override + public double[][] getBounds() { + return new double[][]{{Array.min(X), Array.min(Y), Array.min(Array.min(Z))}, {Array.max(X), Array.max(Y), Array.max(Array.min(Z))}}; + } + + public void setDataZ(double[][] _Z) { + setData(_Z); + } + + public double[][] getDataZ() { + return Z; + } + + public void setDataX(double[] _X) { + datapanel = null; + X = _X; + buildXYZ_list(); + } + + public double[] getDataX() { + return X; + } + + public void setDataY(double[] _Y) { + datapanel = null; + Y = _Y; + buildXYZ_list(); + } + + public double[] getDataY() { + return Y; + } + + public void setDataXYZ(double[] _X, double[] _Y, double[][] _Z) { + datapanel = null; + X = _X; + Y = _Y; + Z = _Z; + buildXYZ_list(); + } + + public double[] isSelected(int[] screenCoordTest, AbstractDrawer draw) { + for (int i = 0; i < X.length; i++) { + for (int j = 0; j < Y.length; j++) { + double[] XY = {X[i], Y[j], Z[j][i]}; + int[] screenCoord = draw.project(XY); + + if ((screenCoord[0] + note_precision > screenCoordTest[0]) && (screenCoord[0] - note_precision < screenCoordTest[0]) + && (screenCoord[1] + note_precision > screenCoordTest[1]) && (screenCoord[1] - note_precision < screenCoordTest[1])) { + return XY; + } + } + } + return null; + } + + public static void main(String[] args) { + + int n = 14; + int m = 16; + Plot3DPanel p = new Plot3DPanel(); + double[] X = new double[n]; + double[] Y = new double[m]; + double[][] Z = new double[m][n]; + + for (int i = 0; i < X.length; i++) { + X[i] = 3 + i / (double) X.length; + for (int j = 0; j < Y.length; j++) { + Y[j] = 5 + j / (double) Y.length; + Z[j][i] = Math.exp(X[i]) + Y[j]; + } + } + p.addGridPlot("toto", X, Y, Z); + + p.setLegendOrientation(PlotPanel.SOUTH); + new FrameView(p); + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/HistogramPlot2D.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/HistogramPlot2D.java new file mode 100644 index 0000000..4f1e029 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/HistogramPlot2D.java @@ -0,0 +1,201 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot2DPanel; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.Color; + +public class HistogramPlot2D extends Plot { + + double[][] topLeft; + double[][] topRight; + double[][] bottomLeft; + double[][] bottomRight; + double[] widths; + double width_constant = -1; + double offsetCenter_perWidth; + double factorWidth; + boolean autowidth; + boolean fill_shape = true; + double[][] XY; + + public HistogramPlot2D(String n, Color c, double[][] _XY, double w) { + this(n, c, _XY, w, 0.5, 1); + } + + public HistogramPlot2D(String n, Color c, double[][] _XY, double[] w) { + this(n, c, _XY, w, 0.5, 1); + } + + // TODO Histogram group plots + public HistogramPlot2D(String n, Color c, double[][] _XY, double w, double _offsetCenter_perWidth, double _factorWidth) { + super(n, c); + XY = _XY; + width_constant = w; + + autowidth = false; + offsetCenter_perWidth = _offsetCenter_perWidth; + factorWidth = _factorWidth; + + build(); + } + + public HistogramPlot2D(String n, Color c, double[][] _XY, double[] w, double _offsetCenter_perWidth, double _factorWidth) { + super(n, c); + XY = _XY; + widths = w; + + autowidth = false; + offsetCenter_perWidth = _offsetCenter_perWidth; + factorWidth = _factorWidth; + + build(); + } + + private void build() { + if (width_constant > 0) { + topLeft = new double[XY.length][]; + topRight = new double[XY.length][]; + bottomLeft = new double[XY.length][]; + bottomRight = new double[XY.length][]; + for (int i = 0; i < XY.length; i++) { + topLeft[i] = new double[]{XY[i][0] - factorWidth * width_constant / 2 + (offsetCenter_perWidth - 0.5) * width_constant, XY[i][1]}; + topRight[i] = new double[]{XY[i][0] + factorWidth * width_constant / 2 + (offsetCenter_perWidth - 0.5) * width_constant, XY[i][1]}; + bottomLeft[i] = new double[]{XY[i][0] - factorWidth * width_constant / 2 + (offsetCenter_perWidth - 0.5) * width_constant, 0}; + bottomRight[i] = new double[]{XY[i][0] + factorWidth * width_constant / 2 + (offsetCenter_perWidth - 0.5) * width_constant, 0}; + } + } else { + topLeft = new double[XY.length][]; + topRight = new double[XY.length][]; + bottomLeft = new double[XY.length][]; + bottomRight = new double[XY.length][]; + for (int i = 0; i < XY.length; i++) { + topLeft[i] = new double[]{XY[i][0] - factorWidth * widths[i] / 2 + (offsetCenter_perWidth - 0.5) * widths[i], XY[i][1]}; + topRight[i] = new double[]{XY[i][0] + factorWidth * widths[i] / 2 + (offsetCenter_perWidth - 0.5) * widths[i], XY[i][1]}; + bottomLeft[i] = new double[]{XY[i][0] - factorWidth * widths[i] / 2 + (offsetCenter_perWidth - 0.5) * widths[i], 0}; + bottomRight[i] = new double[]{XY[i][0] + factorWidth * widths[i] / 2 + (offsetCenter_perWidth - 0.5) * widths[i], 0}; + } + } + } + + /* + * public HistogramPlot2D(double[][] XY, Color c, String n, ProjectionBase + * b) { super(XY, c, n, PlotPanel.HISTOGRAM, b); + * + * autowidth = true; + * + * topLeft = new double[datas.length][]; topRight = new + * double[datas.length][]; bottomLeft = new double[datas.length][]; + * bottomRight = new double[datas.length][]; + * + * Sorting sort = new Sorting(DoubleArray.getColumnCopy(datas, 0), false); + * datas = DoubleArray.getRowsCopy(XY, sort.getIndex()); + * + * topLeft[0] = new double[] { datas[0][0] + (datas[0][0] - datas[1][0]) / + * 2, datas[0][1] }; topRight[0] = new double[] { (datas[0][0] + + * datas[1][0]) / 2, datas[0][1] }; bottomLeft[0] = new double[] { + * datas[0][0] + (datas[0][0] - datas[1][0]) / 2, 0 }; bottomRight[0] = new + * double[] { (datas[0][0] + datas[1][0]) / 2, 0 }; for (int i = 1; i < + * datas.length - 1; i++) { topLeft[i] = new double[] { (datas[i][0] + + * datas[i - 1][0]) / 2, datas[i][1] }; topRight[i] = new double[] { + * (datas[i][0] + datas[i + 1][0]) / 2, datas[i][1] }; bottomLeft[i] = new + * double[] { (datas[i][0] + datas[i - 1][0]) / 2, 0 }; bottomRight[i] = new + * double[] { (datas[i][0] + datas[i + 1][0]) / 2, 0 }; } + * topLeft[datas.length - 1] = new double[] { (datas[datas.length - 1][0] + + * datas[datas.length - 2][0]) / 2, datas[datas.length - 1][1] }; + * topRight[datas.length - 1] = new double[] { datas[datas.length - 1][0] + + * (datas[datas.length - 1][0] - datas[datas.length - 2][0]) / 2, + * datas[datas.length - 1][1] }; bottomLeft[datas.length - 1] = new double[] { + * (datas[datas.length - 1][0] + datas[datas.length - 2][0]) / 2, 0 }; + * bottomRight[datas.length - 1] = new double[] { datas[datas.length - 1][0] + + * (datas[datas.length - 1][0] - datas[datas.length - 2][0]) / 2, 0 }; } + */ + public void plot(AbstractDrawer draw, Color c) { + if (!visible) { + return; + } + + draw.setColor(c); + draw.setLineType(AbstractDrawer.CONTINOUS_LINE); + for (int i = 0; i < XY.length; i++) { + draw.drawLine(bottomLeft[i], topLeft[i]); + draw.drawLine(topLeft[i], topRight[i]); + draw.drawLine(topRight[i], bottomRight[i]); + draw.drawLine(bottomRight[i], bottomLeft[i]); + + if (fill_shape) { + draw.fillPolygon(0.2f, bottomLeft[i], topLeft[i], topRight[i], bottomRight[i]); + } + } + } + + @Override + public void setData(double[][] d) { + datapanel = null; + XY = d; + } + + @Override + public double[][] getData() { + return XY; + } + + @Override + public double[][] getBounds() { + return new double[][]{Array.min(bottomLeft), Array.max(topRight)}; + } + + public void setDataWidth(double[] w) { + widths = w; + width_constant = -1; + build(); + } + + public void setDataWidth(double w) { + width_constant = w; + build(); + } + + public double[] getDataWidth() { + if (width_constant > 0) { + widths = new double[XY.length]; + for (int i = 0; i < widths.length; i++) { + widths[i] = width_constant; + } + } + return widths; + } + + public void setData(double[][] d, double[] w) { + setData(d); + setDataWidth(w); + } + + public void setData(double[][] d, double w) { + setData(d); + setDataWidth(w); + } + + public double[] isSelected(int[] screenCoordTest, AbstractDrawer draw) { + for (int i = 0; i < XY.length; i++) { + int[] screenCoord = draw.project(XY[i]); + + if ((screenCoord[0] + note_precision > screenCoordTest[0]) && (screenCoord[0] - note_precision < screenCoordTest[0]) + && (screenCoord[1] + note_precision > screenCoordTest[1]) && (screenCoord[1] - note_precision < screenCoordTest[1])) { + return XY[i]; + } + } + return null; + } + + public static void main(String[] args) { + double[] X = new double[500]; + for (int i = 0; i < X.length; i++) { + X[i] = Math.random() + Math.random(); + } + Plot2DPanel p = new Plot2DPanel("SOUTH"); + p.addHistogramPlot("test", X, 10); + new FrameView(p); + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/HistogramPlot3D.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/HistogramPlot3D.java new file mode 100644 index 0000000..8e316b2 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/HistogramPlot3D.java @@ -0,0 +1,197 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot3DPanel; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.Color; + +public class HistogramPlot3D extends Plot { + + double[][] topNW; + double[][] topNE; + double[][] topSW; + double[][] topSE; + double[][] bottomNW; + double[][] bottomNE; + double[][] bottomSW; + double[][] bottomSE; + double[][] widths; + double[] width_constant = {-1, -1}; + double[][] XY; + boolean fill_shape = true; + + public HistogramPlot3D(String n, Color c, double[][] _XY, double[][] w) { + super(n, c); + XY = _XY; + widths = w; + + build(); + } + + public HistogramPlot3D(String n, Color c, double[][] _XY, double wX, double wY) { + super(n, c); + XY = _XY; + width_constant = new double[]{wX, wY}; + + build(); + } + + public HistogramPlot3D(String n, Color c, double[][] _XY, double[] w) { + super(n, c); + XY = _XY; + width_constant = w; + + build(); + } + + private void build() { + if (width_constant[0] > 0) { + topNW = new double[XY.length][]; + topNE = new double[XY.length][]; + topSW = new double[XY.length][]; + topSE = new double[XY.length][]; + bottomNW = new double[XY.length][]; + bottomNE = new double[XY.length][]; + bottomSW = new double[XY.length][]; + bottomSE = new double[XY.length][]; + for (int i = 0; i < XY.length; i++) { + topNW[i] = new double[]{XY[i][0] - width_constant[0] / 2, XY[i][1] + width_constant[1] / 2, XY[i][2]}; + topNE[i] = new double[]{XY[i][0] + width_constant[0] / 2, XY[i][1] + width_constant[1] / 2, XY[i][2]}; + topSW[i] = new double[]{XY[i][0] - width_constant[0] / 2, XY[i][1] - width_constant[1] / 2, XY[i][2]}; + topSE[i] = new double[]{XY[i][0] + width_constant[0] / 2, XY[i][1] - width_constant[1] / 2, XY[i][2]}; + bottomNW[i] = new double[]{XY[i][0] - width_constant[0] / 2, XY[i][1] + width_constant[1] / 2, 0}; + bottomNE[i] = new double[]{XY[i][0] + width_constant[0] / 2, XY[i][1] + width_constant[1] / 2, 0}; + bottomSW[i] = new double[]{XY[i][0] - width_constant[0] / 2, XY[i][1] - width_constant[1] / 2, 0}; + bottomSE[i] = new double[]{XY[i][0] + width_constant[0] / 2, XY[i][1] - width_constant[1] / 2, 0}; + } + } else { + topNW = new double[XY.length][]; + topNE = new double[XY.length][]; + topSW = new double[XY.length][]; + topSE = new double[XY.length][]; + bottomNW = new double[XY.length][]; + bottomNE = new double[XY.length][]; + bottomSW = new double[XY.length][]; + bottomSE = new double[XY.length][]; + for (int i = 0; i < XY.length; i++) { + topNW[i] = new double[]{XY[i][0] - widths[i][0] / 2, XY[i][1] + widths[i][1] / 2, XY[i][2]}; + topNE[i] = new double[]{XY[i][0] + widths[i][0] / 2, XY[i][1] + widths[i][1] / 2, XY[i][2]}; + topSW[i] = new double[]{XY[i][0] - widths[i][0] / 2, XY[i][1] - widths[i][1] / 2, XY[i][2]}; + topSE[i] = new double[]{XY[i][0] + widths[i][0] / 2, XY[i][1] - widths[i][1] / 2, XY[i][2]}; + bottomNW[i] = new double[]{XY[i][0] - widths[i][0] / 2, XY[i][1] + widths[i][1] / 2, 0}; + bottomNE[i] = new double[]{XY[i][0] + widths[i][0] / 2, XY[i][1] + widths[i][1] / 2, 0}; + bottomSW[i] = new double[]{XY[i][0] - widths[i][0] / 2, XY[i][1] - widths[i][1] / 2, 0}; + bottomSE[i] = new double[]{XY[i][0] + widths[i][0] / 2, XY[i][1] - widths[i][1] / 2, 0}; + } + } + } + + public void plot(AbstractDrawer draw, Color c) { + if (!visible) { + return; + } + + draw.canvas.includeInBounds(bottomSW[0]); + draw.canvas.includeInBounds(topNE[XY.length - 1]); + + draw.setColor(c); + draw.setLineType(AbstractDrawer.CONTINOUS_LINE); + for (int i = 0; i < XY.length; i++) { + if (topNW[i][2] != bottomNW[i][2]) { + draw.drawLine(topNW[i], topNE[i]); + draw.drawLine(topNE[i], topSE[i]); + draw.drawLine(topSE[i], topSW[i]); + draw.drawLine(topSW[i], topNW[i]); + + draw.drawLine(bottomNW[i], bottomNE[i]); + draw.drawLine(bottomNE[i], bottomSE[i]); + draw.drawLine(bottomSE[i], bottomSW[i]); + draw.drawLine(bottomSW[i], bottomNW[i]); + + draw.drawLine(bottomNW[i], topNW[i]); + draw.drawLine(bottomNE[i], topNE[i]); + draw.drawLine(bottomSE[i], topSE[i]); + draw.drawLine(bottomSW[i], topSW[i]); + + if (fill_shape) { + draw.fillPolygon(0.2f, topNW[i], topNE[i], topSE[i], topSW[i]); + //draw.fillPolygon(bottomNW[i], bottomNE[i], bottomSE[i], bottomSW[i]); + /*draw.fillPolygon(topNW[i], topNE[i], bottomNE[i], bottomNW[i]); + draw.fillPolygon(topSW[i], topSE[i], bottomSE[i], bottomSW[i]); + draw.fillPolygon(topNE[i], topSE[i], bottomSE[i], bottomNE[i]); + draw.fillPolygon(topNW[i], topSW[i], bottomSW[i], bottomNW[i]);*/ + } + } + } + } + + @Override + public void setData(double[][] d) { + datapanel = null; + XY = d; + } + + @Override + public double[][] getData() { + return XY; + } + + @Override + public double[][] getBounds() { + return new double[][]{Array.min(bottomSW), Array.max(topNE)}; + } + + public void setDataWidth(double[][] w) { + widths = w; + } + + public void setDataWidth(double... w) { + width_constant = w; + build(); + } + + public double[][] getDataWidth() { + if (width_constant[0] > 0) { + widths = new double[XY.length][2]; + for (int i = 0; i < widths.length; i++) { + widths[i][0] = width_constant[0]; + widths[i][1] = width_constant[1]; + } + } + return widths; + } + + public void setData(double[][] d, double[][] w) { + setData(d); + widths = w; + } + + public void setData(double[][] d, double... w) { + setData(d); + setDataWidth(w); + } + + public double[] isSelected(int[] screenCoordTest, AbstractDrawer draw) { + for (int i = 0; i < XY.length; i++) { + int[] screenCoord = draw.project(XY[i]); + + if ((screenCoord[0] + note_precision > screenCoordTest[0]) && (screenCoord[0] - note_precision < screenCoordTest[0]) + && (screenCoord[1] + note_precision > screenCoordTest[1]) && (screenCoord[1] - note_precision < screenCoordTest[1])) { + return XY[i]; + } + } + return null; + } + + public static void main(String[] args) { + double[][] XY = new double[500][2]; + for (int i = 0; i < XY.length; i++) { + XY[i][0] = Math.random() + Math.random(); + XY[i][1] = Math.random() + Math.random(); + } + Plot3DPanel p = new Plot3DPanel("SOUTH"); + p.addHistogramPlot("test", XY, 4, 6); + new FrameView(p); + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Label.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Label.java new file mode 100644 index 0000000..9292538 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Label.java @@ -0,0 +1,155 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import java.awt.Color; +import java.awt.Font; + +/** + * BSD License + * + * @author Yann RICHET + */ +public class Label implements Plotable { + + protected double[] coord; + + protected double[] base_offset; + + protected String label; + + protected Color color; + + protected double cornerN = 0.5; + + protected double cornerE = 0.5; + + boolean visible = true; + + public double angle = 0; + + public Font font = AbstractDrawer.DEFAULT_FONT; + + // private static DecimalFormat dec = new DecimalFormat("##0.###E0"); + + public Label(String l, Color col, double... c) { + label = l; + coord = c; + color = col; + } + + public Label(String l, double... c) { + this(l, AbstractDrawer.DEFAULT_COLOR, c); + } + + /** + * show coord itself + */ + public Label(double... c) { + this(coordToString(c), AbstractDrawer.DEFAULT_COLOR, c); + } + + public void setText(String _t) { + label = _t; + } + + public String getText() { + return label; + } + + public void setCoord(double... _c) { + coord = _c; + } + + public void setColor(Color c) { + color = c; + } + + public Color getColor() { + return color; + } + + /** + * reference point center: 0.5, 0.5 lowerleft: 0,0 upperleft 1, 0 ... + */ + public void setCorner(double north_south, double east_west) { + cornerN = north_south; + cornerE = east_west; + } + + public void setVisible(boolean v) { + visible = v; + } + + public boolean getVisible() { + return visible; + } + + /** + * shift by given screen coordinates offset + */ + /* + * public void setOffset(double[] offset) { double[] newCoord = + * coord.getPlotCoordCopy(); for (int i = 0; i < newCoord.length; i++) { + * newCoord[i] += offset[i]; } coord.setPlotCoord(newCoord); } + */ + + /** + * see Text for formatted text output + */ + public void plot(AbstractDrawer draw) { + if (!visible) { + return; + } + + draw.setColor(color); + draw.setFont(font); + draw.setBaseOffset(base_offset); + draw.setTextOffset(cornerE, cornerN); + draw.setTextAngle(angle); + draw.drawText(label, coord); + draw.setBaseOffset(null); + } + + public void rotate(double _angle) { + angle = _angle; + } + + public void setFont(Font _font) { + font = _font; + } + + public static double approx(double val, int decimal) { + // double timesEn = val*Math.pow(10,decimal); + // if (Math.rint(timesEn) == timesEn) { + // return val; + // } else { + // to limit precision loss, you need to separate cases where decimal<0 + // and >0 + // if you don't you'll have this : approx(10000.0,-4) => 10000.00000001 + if (decimal < 0) { + return Math.rint(val / Math.pow(10, -decimal)) * Math.pow(10, -decimal); + } else { + return Math.rint(val * Math.pow(10, decimal)) / Math.pow(10, decimal); + } + // } + } + + public static String coordToString(double... c) { + StringBuffer sb = new StringBuffer("("); + for (int i = 0; i < c.length; i++) { + sb.append(approx(c[i], 2)).append(","); + } + // sb.append(dec.format(c.getPlotCoordCopy()[i])).append(","); + + sb.setLength(sb.length() - 1); + if (sb.length() > 0) { + sb.append(")"); + } + + return sb.toString(); + } + + public Font getFont() { + return font; + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/LayerPlot.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/LayerPlot.java new file mode 100644 index 0000000..472cdc1 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/LayerPlot.java @@ -0,0 +1,32 @@ +/* + * Created on 5 juil. 07 by richet + */ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.DataPanel; +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; + +public abstract class LayerPlot extends Plot { + + Plot plot; + + public LayerPlot(String name, Plot p) { + super(name, p.color); + plot = p; + } + + public double[] isSelected(int[] screenCoordTest, AbstractDrawer draw) { + return null; + } + + @Override + public double[][] getBounds() { + return plot.getBounds(); + } + + @Override + public DataPanel getDataPanel(PlotCanvas plotCanvas) { + return null; + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Line.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Line.java new file mode 100644 index 0000000..fc6d58f --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Line.java @@ -0,0 +1,60 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import java.awt.Color; + +public class Line implements Plotable { + + protected double[][] extrem = new double[2][]; + + protected Color color; + + protected Color gradientColor; + + boolean visible = true; + + public Line(Color col, double[] c1, double[] c2) { + extrem[0] = c1; + extrem[1] = c2; + color = col; + } + + public void setColor(Color c) { + color = c; + } + + public Color getColor() { + return color; + } + + public void setVisible(boolean v) { + visible = v; + } + + public boolean getVisible() { + return visible; + } + + public void plot(AbstractDrawer draw) { + if (!visible) { + return; + } + + draw.setColor(color); + if (gradientColor != null) { + draw.setGradient(extrem[0], color, extrem[1], gradientColor); + } + draw.drawLine(extrem[0], extrem[1]); + if (gradientColor != null) { + draw.resetGradient(); + } + } + + public Color getGradientColor() { + return gradientColor; + } + + public void setGradientColor(Color c) { + this.gradientColor = c; + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/LinePlot.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/LinePlot.java new file mode 100644 index 0000000..7ac246b --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/LinePlot.java @@ -0,0 +1,82 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot2DPanel; +import org.xbib.graphics.graph.jmathplot.panel.Plot3DPanel; +import org.xbib.graphics.graph.jmathplot.panel.PlotPanel; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import java.awt.Color; +import javax.swing.JFrame; + +public class LinePlot extends ScatterPlot { + + public boolean draw_dot = false; + + public LinePlot(String n, Color c, boolean[][] _pattern, double[][] _XY) { + super(n, c, _pattern, _XY); + } + + public LinePlot(String n, Color c, int _type, int _radius, double[][] _XY) { + super(n, c, _type, _radius, _XY); + } + + public LinePlot(String n, Color c, double[][] _XY) { + super(n, c, _XY); + } + + public void plot(AbstractDrawer draw, Color c) { + if (!visible) { + return; + } + + if (draw_dot) { + super.plot(draw, c); + } + + draw.setColor(c); + draw.setLineType(AbstractDrawer.CONTINOUS_LINE); + for (int i = 0; i < XY.length - 1; i++) { + if (!anyNaN(XY[i]) && !anyNaN(XY[i + 1])) { + draw.drawLine(XY[i], XY[i + 1]); + } + } + } + + boolean anyNaN(double[] xy) { + for (int i = 0; i < xy.length; i++) { + if (Double.isNaN(xy[i])) { + return true; + } + } + return false; + } + + public static void main(String[] args) { + Plot2DPanel p2 = new Plot2DPanel(); + + double[][] XYZ = new double[100][2]; + for (int j = 0; j < XYZ.length; j++) { + XYZ[j][0] = 2 * Math.PI * (double) j / XYZ.length; + XYZ[j][1] = Math.sin(XYZ[j][0]); + } + XYZ[50][0] = Double.NaN; + p2.addLinePlot("sin", XYZ); + + + p2.setLegendOrientation(PlotPanel.SOUTH); + new FrameView(p2).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + Plot3DPanel p = new Plot3DPanel(); + + XYZ = new double[100][3]; + for (int j = 0; j < XYZ.length; j++) { + XYZ[j][0] = 2 * Math.PI * (double) j / XYZ.length; + XYZ[j][1] = Math.sin(XYZ[j][0]); + XYZ[j][2] = Math.sin(XYZ[j][0]) * Math.cos(XYZ[j][1]); + } + p.addLinePlot("toto", XYZ); + + p.setLegendOrientation(PlotPanel.SOUTH); + new FrameView(p).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Noteable.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Noteable.java new file mode 100644 index 0000000..0b74203 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Noteable.java @@ -0,0 +1,10 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; + +public interface Noteable { + + double[] isSelected(int[] screenCoord, AbstractDrawer draw); + + void note(AbstractDrawer draw); +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/PaintImage.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/PaintImage.java new file mode 100644 index 0000000..5bc487a --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/PaintImage.java @@ -0,0 +1,61 @@ +/* + * Created on 5 sept. 2005 by richet + */ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Image; + +public class PaintImage implements Plotable { + + public interface Paintable { + + void paint(Graphics g); + } + + protected Paintable source; + protected Image img; + protected double[] xyzSW, xyzSE, xyzNW; + protected boolean visible = true; + protected float alpha; + + public PaintImage(Paintable _source, float _alpha, double[] _xyzSW, double[] _xyzSE, double[] _xyzNW) { + source = _source; + + xyzSW = _xyzSW; + xyzSE = _xyzSE; + xyzNW = _xyzNW; + alpha = _alpha; + } + + public void plot(AbstractDrawer draw) { + if (!visible) { + return; + } + + if (img == null) { + img = draw.canvas.createImage(draw.canvas.getWidth(), draw.canvas.getHeight()); + source.paint(img.getGraphics()); + } + + draw.drawImage(img, alpha, xyzSW, xyzSE, xyzNW); + } + + public void setVisible(boolean v) { + visible = v; + } + + public boolean getVisible() { + return visible; + } + + public void setColor(Color c) { + throw new IllegalArgumentException("method not available for this Object: PlotImage"); + } + + public Color getColor() { + return null; + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Plot.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Plot.java new file mode 100644 index 0000000..e8b564f --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Plot.java @@ -0,0 +1,255 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.DataPanel; +import org.xbib.graphics.graph.jmathplot.panel.MatrixTablePanel; +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.LinkedList; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JColorChooser; +import javax.swing.JPanel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +public abstract class Plot implements Plotable, Noteable, Editable { + + public String name; + public Color color; + public boolean visible = true; + public LinkedList layers; + public boolean noted = false; + public double[] coordNoted; + //public boolean forcenoted = false; + public int note_precision = 5; + + public Plot(String n, Color c) { + name = n; + color = c; + layers = new LinkedList(); + + } + + public void clearLayers() { + layers.clear(); + } + + public void addLayer(LayerPlot q) { + layers.add(q); + } + + public void addQuantile(QuantileLayerPlot q) { + layers.add(q); + } + + public void addQuantile(int a, double r, double[] q, boolean symetric) { + layers.add(new QuantileLayerPlot(this, a, q, r, symetric)); + } + + public void addQuantile(int a, double r, double q, boolean symetric) { + layers.add(new QuantileLayerPlot(this, a, q, r, symetric)); + } + + public void addQuantiles(int a, double[][] q) { + layers.add(new DensityLayerPlot(this, a, q)); + } + + public void addQuantiles(int a, double[] q) { + layers.add(new DensityLayerPlot(this, a, q)); + } + + public void addGaussQuantiles(int a, double[] s) { + layers.add(new GaussianDensityLayerPlot(this, a, s)); + } + + public void addGaussQuantiles(int a, double s) { + layers.add(new GaussianDensityLayerPlot(this, a, s)); + } + + /*public void addQuantiles(double[][][] q,boolean _symetric) { + for (int i = 0; i < q[0].length; i++) { + addQuantile(i, Array.getColumnCopy(q, i, 0),_symetric); + addQuantile(i, Array.getColumnCopy(q, i, 1),_symetric); + } + }*/ + + /*public void addQuantiles(double[][] q,boolean _symetric) { + for (int i = 0; i < q[0].length; i++) { + addQuantile(i, Array.getColumnCopy(q, i),_symetric); + } + }*/ + public void addVector(double[][] v) { + layers.add(new VectorLayerPlot(this, v)); + } + + public abstract void setData(double[][] d); + + public abstract double[][] getData(); + + public double[] getBounds(int axis) { + return Array.getColumnCopy(getBounds(), axis); + } + + /** + * This method should be abstract, but for backward compatibility, here is a basic impl. + */ + public double[][] getBounds() { + return Array.mergeRows(Array.min(getData()), Array.max(getData())); + } + + public void setVisible(boolean v) { + visible = v; + } + + public boolean getVisible() { + return visible; + } + + public void setName(String n) { + name = n; + } + + public String getName() { + return name; + } + + /* + * public String getType() { return type; } + */ + public Color getColor() { + return color; + } + + public void setColor(Color c) { + color = c; + } + + public abstract double[] isSelected(int[] screenCoordTest, AbstractDrawer draw); + + public void note(AbstractDrawer draw) { + plot(draw, PlotCanvas.NOTE_COLOR); + plotLayerPlots(draw, PlotCanvas.NOTE_COLOR); + } + + public void noteCoord(AbstractDrawer draw, double[] coordNoted) { + if (coordNoted == null) { + return; + } + + draw.setColor(PlotCanvas.NOTE_COLOR); + draw.drawCoordinate(coordNoted); + draw.drawShadowedText(Array.cat("\n", draw.canvas.reverseMapedData(coordNoted)), .5f, coordNoted); + } + + public abstract void plot(AbstractDrawer draw, Color c); + + public void plot(AbstractDrawer draw) { + //if (layers.size() > 0) + plotLayerPlots(draw, color); + //else + plot(draw, color); + } + + public void plotLayerPlots(AbstractDrawer draw, Color c) { + for (int i = 0; i < layers.size(); i++) { + layers.get(i).plot(draw, c); + } + + } + + public void edit(Object src) { + ((PlotCanvas) src).displayDataFrame(((PlotCanvas) src).getPlotIndex(this)); + } + + public void editnote(AbstractDrawer draw) { + plot(draw, PlotCanvas.EDIT_COLOR); + plotLayerPlots(draw, PlotCanvas.EDIT_COLOR); + } + + public DataPanel datapanel = null; + public PlotCanvas plotCanvas; + + public DataPanel getDataPanel(PlotCanvas plotCanvas) { + this.plotCanvas = plotCanvas; + if (datapanel == null) { + datapanel = new DefaultDataPanel(this); + } + return datapanel; + } + + public class DefaultDataPanel extends DataPanel { + + private static final long serialVersionUID = 1L; + MatrixTablePanel XY; + JCheckBox visible; + JButton color; + JPanel plottoolspanel; + Plot plot; + //DataFrame dframe; + + public DefaultDataPanel(/*DataFrame _dframe,*/Plot _plot) { + plot = _plot; + //dframe = _dframe; + visible = new JCheckBox("Visible"); + visible.setSelected(plot.getVisible()); + color = new JButton(); + color.setBackground(plot.getColor()); + XY = new MatrixTablePanel(plotCanvas.reverseMapedData(plot.getData())); + + visible.addChangeListener(new ChangeListener() { + + public void stateChanged(ChangeEvent e) { + plot.setVisible(visible.isSelected()); + plotCanvas.linkedLegendPanel.updateLegends(); + /*dframe.*/ + plotCanvas.repaint(); + } + }); + color.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + Color c = JColorChooser.showDialog(plotCanvas, "Choose plot color", plot.getColor()); + color.setBackground(c); + plot.setColor(c); + plotCanvas.linkedLegendPanel.updateLegends(); + /*dframe.*/ + plotCanvas.linkedLegendPanel.repaint(); + /*dframe.*/ + plotCanvas.repaint(); + } + }); + + this.setLayout(new BorderLayout()); + plottoolspanel = new JPanel(); + plottoolspanel.add(visible); + plottoolspanel.add(color); + this.add(plottoolspanel, BorderLayout.NORTH); + this.add(XY, BorderLayout.CENTER); + } + + @Override + protected void toWindow() { + XY.toWindow(); + } + + @Override + public void toClipBoard() { + XY.toClipBoard(); + } + + @Override + public void toASCIIFile(File file) { + XY.toASCIIFile(file); + } + + public String getText() { + return XY.getText(); + } + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Plotable.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Plotable.java new file mode 100644 index 0000000..d6b50b0 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/Plotable.java @@ -0,0 +1,18 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import java.awt.Color; + +public interface Plotable { + + void plot(AbstractDrawer draw); + + void setVisible(boolean v); + + boolean getVisible(); + + void setColor(Color c); + + Color getColor(); + +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/QuantileLayerPlot.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/QuantileLayerPlot.java new file mode 100644 index 0000000..98e3203 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/QuantileLayerPlot.java @@ -0,0 +1,129 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.Color; + +public class QuantileLayerPlot extends LayerPlot { + + public static int WIDTH = 2; + + int axe; + + double quantileRate; + + Color gradC; + + double main_data_constant = 0; + + public boolean symetric = false; + + double[] Q; + + //public static double DEFAULT_RATE=1.0; + + /*public QuantilePlot(Plot p, int a, double[] q, boolean _symetric) { + this(p, a, q, DEFAULT_RATE,_symetric); + + }*/ + + public QuantileLayerPlot(Plot p, int a, double q, double r, boolean _symetric) { + this(p, a, null, r, true); + main_data_constant = q; + } + + /*public QuantilePlot(Plot p, int a, double q) { + this(p, a, q, DEFAULT_RATE,true); + }*/ + + /** + * Build a quantile plot based on given plot. The quantile is drawn as a linear gradient from the base plot dots. + * + * @param p base plot + * @param a axis number of quantile : 0=X quantile, 1=Y quantile, 2=Z quantile + * @param q array of quantiles values + * @param r rate of the quantile. The gradient line length is q/r + * @param _symetric if yes, quantiles are drawn on both negative and positive sides of base plot dots + */ + public QuantileLayerPlot(Plot p, int a, double[] q, double r, boolean _symetric) { + super(r + " quantile of " + p.name, p); + if (q != null) { + Array.checkLength(q, p.getData().length); + } + Q = q; + axe = a; + quantileRate = r; + symetric = _symetric; + + } + + public double getQuantilesValue(int numCoord) { + return Q[numCoord]; + } + + public int getAxe() { + return axe; + } + + public double getQuantileRate() { + return quantileRate; + } + + public void plot(AbstractDrawer draw, Color c) { + if (!plot.visible) { + return; + } + + draw.setColor(c); + gradC = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (255 * (1 - quantileRate))); + + draw.setLineType(AbstractDrawer.CONTINOUS_LINE); + draw.setLineWidth(WIDTH); + if (main_data_constant == 0) { + for (int i = 0; i < plot.getData().length; i++) { + double[] d = Array.getRowCopy(plot.getData(), i); + d[axe] += Q[i];///quantileRate; + draw.setGradient(plot.getData()[i], c, d, gradC); + draw.drawLine(plot.getData()[i], d); + // draw.drawDot(d, RADIUS/*(int)(RADIUS*quantileRate)*/); + + if (symetric) { + d[axe] -= 2 * Q[i];///quantileRate; + draw.setGradient(plot.getData()[i], c, d, gradC); + draw.drawLine(plot.getData()[i], d); + // draw.drawDot(d, RADIUS/*(int)(RADIUS*quantileRate)*/); + } + } + } else { + for (int i = 0; i < plot.getData().length; i++) { + double[] d = Array.getRowCopy(plot.getData(), i); + d[axe] += main_data_constant;///quantileRate; + draw.setGradient(plot.getData()[i], c, d, gradC); + draw.drawLine(plot.getData()[i], d); + // draw.drawDot(d, shape/*RADIUS/*(int)(RADIUS*quantileRate)*/); + + if (symetric) { + d[axe] -= 2 * main_data_constant;///quantileRate; + draw.setGradient(plot.getData()[i], c, d, gradC); + draw.drawLine(plot.getData()[i], d); + // draw.drawDot(d, RADIUS/*(int)(RADIUS*quantileRate)*/); + } + } + } + draw.resetGradient(); + draw.setLineWidth(AbstractDrawer.DEFAULT_LINE_WIDTH); + + } + + @Override + public void setData(double[][] d) { + Q = d[0]; + } + + @Override + public double[][] getData() { + return new double[][]{Q}; + } + + +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/RasterImage.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/RasterImage.java new file mode 100644 index 0000000..f2f1407 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/RasterImage.java @@ -0,0 +1,98 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot2DPanel; +import org.xbib.graphics.graph.jmathplot.panel.Plot3DPanel; +import org.xbib.graphics.graph.jmathplot.panel.PlotPanel; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Image; +import java.awt.Toolkit; +import java.io.File; +import javax.swing.JFrame; + +public class RasterImage implements Plotable { + + File source; + Image img; + + double[] xyzSW, xyzSE, xyzNW; + + boolean visible = true; + float alpha; + + public RasterImage(File _source, float _alpha, double[] _xyzSW, double[] _xyzSE, double[] _xyzNW) { + source = _source; + img = Toolkit.getDefaultToolkit().getImage(source.getPath()); + xyzSW = _xyzSW; + xyzSE = _xyzSE; + xyzNW = _xyzNW; + alpha = _alpha; + } + + public void plot(AbstractDrawer draw) { + if (!visible) { + return; + } + + draw.drawImage(img, alpha, xyzSW, xyzSE, xyzNW); + } + + public void setVisible(boolean v) { + visible = v; + } + + public boolean getVisible() { + return visible; + } + + public void setColor(Color c) { + throw new IllegalArgumentException("method not available for this Object: PlotImage"); + } + + public Color getColor() { + return null; + } + + public static void main(String[] args) { + Plot2DPanel p2 = new Plot2DPanel(); + for (int i = 0; i < 1; i++) { + double[][] XYZ = new double[10][2]; + for (int j = 0; j < XYZ.length; j++) { + XYZ[j][0] =/*1 + */Math.random(); + XYZ[j][1] = /*100 * */Math.random(); + } + p2.addScatterPlot("toto" + i, XYZ); + } + + p2.addPlotable(new RasterImage(new File("test.gif"), 0.8f, new double[]{0.2, 0.5}, new double[]{1.2, 0.8}, new double[]{0.2, 1.1})); + + p2.setLegendOrientation(PlotPanel.SOUTH); + new FrameView(p2).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + + Plot3DPanel p = new Plot3DPanel(); + for (int i = 0; i < 1; i++) { + double[][] XYZ = new double[10][3]; + for (int j = 0; j < XYZ.length; j++) { + XYZ[j][0] = /*1 +*/ Math.random(); + XYZ[j][1] = /*100 **/ Math.random(); + XYZ[j][2] = /*0.0001 **/ Math.random(); + } + p.addScatterPlot("toto" + i, XYZ); + } + + p.addPlotable(new RasterImage(new File("test.gif"), 0.5f, new double[]{0.0, 0.0, 0.0}, new double[]{1, 0, 0.0}, new double[]{0.0, 0, 1})); + p.addPlotable(new RasterImage(new File("test.gif"), 0.5f, new double[]{0.0, 0.0, 0.0}, new double[]{0, 1, 0.0}, new double[]{0, 0.0, 1})); + p.addPlotable(new RasterImage(new File("test.gif"), 0.5f, new double[]{0.0, 0.0, 0.0}, new double[]{1, 0, 0}, new double[]{0, 1, 0})); + // TODO this following case is not totally supported... + //p.addPlotable(new PlotImage(new File("test.jpg"),0.5f, new double[] {1,0,0},new double[] {1,1,0},new double[] {0,0,1})); + + + p.setLegendOrientation(PlotPanel.SOUTH); + p.setPreferredSize(new Dimension(600, 600)); + new FrameView(p).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } + +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/ScatterPlot.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/ScatterPlot.java new file mode 100644 index 0000000..0adf7c2 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/ScatterPlot.java @@ -0,0 +1,164 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot2DPanel; +import org.xbib.graphics.graph.jmathplot.panel.Plot3DPanel; +import org.xbib.graphics.graph.jmathplot.panel.PlotPanel; +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import org.xbib.graphics.graph.jmathplot.utils.FastMath; +import java.awt.Color; +import javax.swing.JFrame; + +public class ScatterPlot extends Plot { + + private int type; + private int radius; + private boolean[][] pattern; + private boolean use_pattern; + double[][] XY; + private String[] tags; + + public ScatterPlot(String n, Color c, boolean[][] _pattern, double[][] _XY) { + super(n, c); + XY = _XY; + use_pattern = true; + pattern = _pattern; + } + + public ScatterPlot(String n, Color c, int _type, int _radius, double[][] _XY) { + super(n, c); + XY = _XY; + use_pattern = false; + type = _type; + radius = _radius; + } + + public ScatterPlot(String n, Color c, double[][] _XY) { + this(n, c, AbstractDrawer.ROUND_DOT, AbstractDrawer.DEFAULT_DOT_RADIUS, _XY); + } + + public void plot(AbstractDrawer draw, Color c) { + if (!visible) { + return; + } + + draw.setColor(c); + if (use_pattern) { + draw.setDotType(AbstractDrawer.PATTERN_DOT); + draw.setDotPattern(pattern); + } else { + draw.setDotRadius(radius); + if (type == AbstractDrawer.CROSS_DOT) { + draw.setDotType(AbstractDrawer.CROSS_DOT); + } else { + draw.setDotType(AbstractDrawer.ROUND_DOT); + } + } + + for (int i = 0; i < XY.length; i++) { + draw.drawDot(XY[i]); + } + } + + public void setDotPattern(int t) { + type = t; + use_pattern = false; + } + + public void setDotPattern(boolean[][] t) { + use_pattern = true; + pattern = t; + } + + @Override + public void setData(double[][] d) { + datapanel = null; + XY = d; + } + + @Override + public double[][] getData() { + return XY; + } + + @Override + public double[][] getBounds() { + return Array.mergeRows(Array.min(XY), Array.max(XY)); + } + + public double[] isSelected(int[] screenCoordTest, AbstractDrawer draw) { + for (int i = 0; i < XY.length; i++) { + int[] screenCoord = draw.project(XY[i]); + + if (FastMath.abs(screenCoord[0] - screenCoordTest[0]) < note_precision && FastMath.abs(screenCoord[1] - screenCoordTest[1]) < note_precision) { + return XY[i]; + } + } + return null; + } + + public static void main(String[] args) { + Plot2DPanel p2 = new Plot2DPanel(); + for (int i = 0; i < 3; i++) { + double[][] XYZ = new double[10][2]; + for (int j = 0; j < XYZ.length; j++) { + XYZ[j][0] = /*1 + */ Math.random(); + XYZ[j][1] = /*100 * */ Math.random(); + } + p2.addScatterPlot("toto" + i, XYZ); + } + + p2.setLegendOrientation(PlotPanel.SOUTH); + new FrameView(p2).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + Plot3DPanel p = new Plot3DPanel(); + String[] tags = null; + for (int i = 0; i < 3; i++) { + double[][] XYZ = new double[10][3]; + tags = new String[10]; + for (int j = 0; j < XYZ.length; j++) { + XYZ[j][0] = /*1 +*/ 2.5 * Math.random(); + XYZ[j][1] = /*100 **/ Math.random(); + XYZ[j][2] = /*0.0001 **/ Math.random(); + tags[j] = "tags " + j; + } + p.addScatterPlot("toto" + i, XYZ); + } + ((ScatterPlot) p.getPlot(0)).setTags(tags); + + p.setLegendOrientation(PlotPanel.SOUTH); + new FrameView(p).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } + + /** + * @param tags the tags to set + */ + public void setTags(String[] tags) { + datapanel = null; + this.tags = tags; + } + + @Override + public void noteCoord(AbstractDrawer draw, double[] coordNoted) { + if (coordNoted == null) { + return; + } + + if (tags == null) { + super.noteCoord(draw, coordNoted); + } else { + draw.setColor(PlotCanvas.NOTE_COLOR); + for (int i = 0; i < XY.length; i++) { + if (tags.length > i) { + if (Array.equals(XY[i], coordNoted)) { + draw.drawShadowedText(tags[i], .5f, coordNoted); + } + } + } + } + //draw.drawCoordinate(coordNoted); + //draw.drawText(Array.cat(draw.canvas.reverseMapedData(coordNoted)), coordNoted); + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/StaircasePlot.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/StaircasePlot.java new file mode 100644 index 0000000..337f612 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/StaircasePlot.java @@ -0,0 +1,69 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot2DPanel; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import java.awt.Color; +import javax.swing.JFrame; + +public class StaircasePlot extends ScatterPlot { + + public boolean link = true; + + public StaircasePlot(String n, Color c, boolean[][] _pattern, double[][] _XY) { + super(n, c, _pattern, _XY); + } + + public StaircasePlot(String n, Color c, int _type, int _radius, double[][] _XY) { + super(n, c, _type, _radius, _XY); + } + + public StaircasePlot(String n, Color c, double[][] _XY) { + super(n, c, _XY); + } + + public void plot(AbstractDrawer draw, Color c) { + if (!visible) { + return; + } + + //System.out.println(Array.toString(XY)); + + draw.setColor(c); + draw.setLineType(AbstractDrawer.CONTINOUS_LINE); + for (int i = 0; i < XY.length - 1; i++) { + double[] begin = XY[i].clone(); + double[] end = XY[i + 1].clone(); + end[end.length - 1] = XY[i][end.length - 1]; + draw.drawLine(begin, end); + } + + //System.out.println(Array.toString(XY)); + + if (link) { + for (int i = 0; i < XY.length - 2; i++) { + double[] begin = XY[i + 1].clone(); + double[] end = XY[i + 1].clone(); + begin[begin.length - 1] = XY[i][begin.length - 1]; + draw.drawLine(begin, end); + } + } + //System.out.println(Array.toString(XY)); + + } + + public static void main(String[] args) { + Plot2DPanel p = new Plot2DPanel(); + + double[] X = new double[10]; + double[] Y = new double[10]; + for (int j = 0; j < X.length; j++) { + X[j] = j; + Y[j] = Math.random(); + } + p.addStaircasePlot("toto", X, Y); + + new FrameView(p).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } + +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/VectorLayerPlot.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/VectorLayerPlot.java new file mode 100644 index 0000000..829c828 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/VectorLayerPlot.java @@ -0,0 +1,87 @@ +package org.xbib.graphics.graph.jmathplot; + +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot2DPanel; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.Color; +import javax.swing.JFrame; + +/** + * @author Yann RICHET + * @version 1.0 + */ + +/** + * Layer to add a vector field to an existing Plot + */ +public class VectorLayerPlot extends LayerPlot { + + public static int RADIUS = 5; + + double[][] V; + + /** + * Create a vector fiels based on data of a plot + * + * @param p Base plot to support vector field + * @param v Vector field of same lenght that p data + */ + public VectorLayerPlot(Plot p, double[][] v) { + super("Vector of " + p.name, p); + if (v != null) { + Array.checkRowDimension(v, p.getData().length); + Array.checkColumnDimension(v, p.getData()[0].length); + } + V = v; + + } + + @Override + public void setData(double[][] v) { + V = v; + } + + @Override + public double[][] getData() { + return V; + } + + public void plot(AbstractDrawer draw, Color c) { + if (!plot.visible) { + return; + } + + draw.setColor(c); + + draw.setLineType(AbstractDrawer.CONTINOUS_LINE); + + for (int i = 0; i < plot.getData().length; i++) { + double[] d = Array.getRowCopy(plot.getData(), i); + for (int j = 0; j < d.length; j++) { + d[j] += V[i][j]; + } + draw.drawLine(plot.getData()[i], d); + //TODO: draw arrow at position d + + } + + } + + public static void main(String[] args) { + Plot2DPanel p2 = new Plot2DPanel(); + double[][] XYZ = new double[100][2]; + double[][] dXYZ = new double[100][2]; + + for (int j = 0; j < XYZ.length; j++) { + XYZ[j][0] = Math.random() * 10; + XYZ[j][1] = Math.random() * 10; + dXYZ[j][0] = 1.0 / Math.sqrt(1 + Math.log(XYZ[j][0]) * Math.log(XYZ[j][0])); + dXYZ[j][1] = Math.log(XYZ[j][0]) / Math.sqrt(1 + Math.log(XYZ[j][0]) * Math.log(XYZ[j][0])); + } + p2.addScatterPlot("toto", XYZ); + + p2.addVectortoPlot(0, dXYZ); + new FrameView(p2).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/canvas/Plot2DCanvas.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/canvas/Plot2DCanvas.java new file mode 100755 index 0000000..7e5e4a8 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/canvas/Plot2DCanvas.java @@ -0,0 +1,171 @@ +package org.xbib.graphics.graph.jmathplot.canvas; + +import static org.xbib.graphics.graph.jmathplot.Base.LINEAR; +import static org.xbib.graphics.graph.jmathplot.utils.Array.getColumnCopy; +import static org.xbib.graphics.graph.jmathplot.utils.Array.getColumnsRangeCopy; +import static org.xbib.graphics.graph.jmathplot.utils.Array.increment; +import static org.xbib.graphics.graph.jmathplot.utils.Array.mergeColumns; +import static org.xbib.graphics.graph.jmathplot.utils.Histogram.histogram_classes; +import static org.xbib.graphics.graph.jmathplot.utils.Histogram.histogram_classes_2D; +import org.xbib.graphics.graph.jmathplot.Base; +import org.xbib.graphics.graph.jmathplot.BasePlot; +import org.xbib.graphics.graph.jmathplot.BarPlot; +import org.xbib.graphics.graph.jmathplot.BoxPlot2D; +import org.xbib.graphics.graph.jmathplot.CloudPlot2D; +import org.xbib.graphics.graph.jmathplot.HistogramPlot2D; +import org.xbib.graphics.graph.jmathplot.LinePlot; +import org.xbib.graphics.graph.jmathplot.ScatterPlot; +import org.xbib.graphics.graph.jmathplot.StaircasePlot; +import org.xbib.graphics.graph.jmathplot.render.AWTDrawer2D; +import java.awt.Color; + +/** + * BSD License + * + * @author Yann RICHET + */ +public class Plot2DCanvas extends PlotCanvas { + + // public final static String PARALLELHISTOGRAM = "PARALLELHISTOGRAM"; + + private static final long serialVersionUID = 1L; + + public Plot2DCanvas() { + super(); + ActionMode = ZOOM; + } + + public Plot2DCanvas(Base b, BasePlot bp) { + super(b, bp); + ActionMode = ZOOM; + } + + public Plot2DCanvas(double[] min, double[] max, String[] axesScales, String[] axesLabels) { + super(min, max, axesScales, axesLabels); + ActionMode = ZOOM; + } + + public void initDrawer() { + draw = new AWTDrawer2D(this); + } + + public void initBasenGrid(double[] min, double[] max) { + initBasenGrid(min, max, new String[]{LINEAR, LINEAR}, new String[]{"X", "Y"}); + } + + public void initBasenGrid() { + initBasenGrid(new double[]{0, 0}, new double[]{1, 1}); + } + + private static double[][] convertY(double[] XY) { + double[] x = increment(XY.length, 1, 1); + return mergeColumns(x, XY); + } + + private static double[][] convertXY(double[]... XY) { + if (XY.length == 2 && XY[0].length != 2) { + return mergeColumns(XY[0], XY[1]); + } else { + return XY; + } + } + + public int addScatterPlot(String name, Color c, double[] Y) { + return addPlot(new ScatterPlot(name, c, convertY(Y))); + } + + public int addScatterPlot(String name, Color c, double[][] XY) { + return addPlot(new ScatterPlot(name, c, convertXY(XY))); + } + + public int addScatterPlot(String name, Color c, double[] X, double[] Y) { + return addPlot(new ScatterPlot(name, c, convertXY(X, Y))); + } + + public int addLinePlot(String name, Color c, double[] Y) { + return addPlot(new LinePlot(name, c, convertY(Y))); + } + + public int addLinePlot(String name, Color c, double[][] XY) { + return addPlot(new LinePlot(name, c, convertXY(XY))); + } + + public int addLinePlot(String name, Color c, double[] X, double[] Y) { + return addPlot(new LinePlot(name, c, convertXY(X, Y))); + } + + public int addBarPlot(String name, Color c, double[] Y) { + return addPlot(new BarPlot(name, c, convertY(Y))); + } + + public int addBarPlot(String name, Color c, double[][] XY) { + return addPlot(new BarPlot(name, c, convertXY(XY))); + } + + public int addBarPlot(String name, Color c, double[] X, double[] Y) { + return addPlot(new BarPlot(name, c, convertXY(X, Y))); + } + + public int addStaircasePlot(String name, Color c, double[] Y) { + return addPlot(new StaircasePlot(name, c, convertY(Y))); + } + + public int addStaircasePlot(String name, Color c, double[][] XY) { + return addPlot(new StaircasePlot(name, c, convertXY(XY))); + } + + public int addStaircasePlot(String name, Color c, double[] X, double[] Y) { + return addPlot(new StaircasePlot(name, c, convertXY(X, Y))); + } + + + public int addBoxPlot(String name, Color c, double[][] XY, double[][] dX) { + return addPlot(new BoxPlot2D(XY, dX, c, name)); + } + + public int addBoxPlot(String name, Color c, double[][] XYdX) { + return addPlot(new BoxPlot2D(getColumnsRangeCopy(XYdX, 0, 1), getColumnsRangeCopy(XYdX, 2, 3), c, name)); + } + + public int addHistogramPlot(String name, Color c, double[][] XY, double[] dX) { + return addPlot(new HistogramPlot2D(name, c, XY, dX)); + } + + public int addHistogramPlot(String name, Color c, double[][] XY, double dX) { + return addPlot(new HistogramPlot2D(name, c, XY, dX)); + } + + public int addHistogramPlot(String name, Color c, double[][] XYdX) { + return addPlot(new HistogramPlot2D(name, c, getColumnsRangeCopy(XYdX, 0, 1), getColumnCopy(XYdX, 2))); + } + + public int addHistogramPlot(String name, Color c, double[] X, int n) { + double[][] XY = histogram_classes(X, n); + return addPlot(new HistogramPlot2D(name, c, XY, XY[1][0] - XY[0][0])); + } + + public int addHistogramPlot(String name, Color c, double[] X, double... bounds) { + double[][] XY = histogram_classes(X, bounds); + return addPlot(new HistogramPlot2D(name, c, XY, XY[1][0] - XY[0][0])); + } + + public int addHistogramPlot(String name, Color c, double[] X, double min, double max, int n) { + double[][] XY = histogram_classes(X, min, max, n); + return addPlot(new HistogramPlot2D(name, c, XY, XY[1][0] - XY[0][0])); + } + + public int addCloudPlot(String name, Color c, double[][] sampleXY, int nX, int nY) { + double[][] XYh = histogram_classes_2D(sampleXY, nX, nY); + return addPlot(new CloudPlot2D(name, c, XYh, XYh[1][0] - XYh[0][0], XYh[nX][1] - XYh[0][1])); + } + + public static void main(String[] args) { + /* + * Plot2DPanel p2d = new Plot2DPanel(DoubleArray.random(10, 2), "plot + * 1", PlotPanel.SCATTER); new FrameView(p2d); + * p2d.addPlot(DoubleArray.random(10, 2), "plot 2", PlotPanel.SCATTER); + * p2d.grid.getAxe(0).darkLabel.setCorner(0.5, -10); + * p2d.grid.getAxe(1).darkLabel.setCorner(0, -0.5); + */ + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/canvas/Plot3DCanvas.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/canvas/Plot3DCanvas.java new file mode 100644 index 0000000..dc554fc --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/canvas/Plot3DCanvas.java @@ -0,0 +1,182 @@ +package org.xbib.graphics.graph.jmathplot.canvas; + +import static org.xbib.graphics.graph.jmathplot.Base.LINEAR; +import static org.xbib.graphics.graph.jmathplot.utils.Array.getColumnsRangeCopy; +import static org.xbib.graphics.graph.jmathplot.utils.Array.getSubMatrixRangeCopy; +import static org.xbib.graphics.graph.jmathplot.utils.Array.mergeColumns; +import static org.xbib.graphics.graph.jmathplot.utils.Histogram.histogram_classes_2D; +import static org.xbib.graphics.graph.jmathplot.utils.Histogram.histogram_classes_3D; +import org.xbib.graphics.graph.jmathplot.Base; +import org.xbib.graphics.graph.jmathplot.BasePlot; +import org.xbib.graphics.graph.jmathplot.BarPlot; +import org.xbib.graphics.graph.jmathplot.BoxPlot3D; +import org.xbib.graphics.graph.jmathplot.CloudPlot3D; +import org.xbib.graphics.graph.jmathplot.GridPlot3D; +import org.xbib.graphics.graph.jmathplot.HistogramPlot3D; +import org.xbib.graphics.graph.jmathplot.LinePlot; +import org.xbib.graphics.graph.jmathplot.ScatterPlot; +import org.xbib.graphics.graph.jmathplot.render.AWTDrawer; +import org.xbib.graphics.graph.jmathplot.render.AWTDrawer3D; +import org.xbib.graphics.graph.jmathplot.render.Projection3D; +import java.awt.Color; +import java.awt.event.MouseEvent; + +/** + * BSD License + * + * @author Yann RICHET + */ +public class Plot3DCanvas extends PlotCanvas { + + private static final long serialVersionUID = 1L; + public final static int ROTATION = 2; + + public Plot3DCanvas() { + super(); + ActionMode = ROTATION; + } + + public Plot3DCanvas(Base b) { + super(b, new BasePlot(b, "X", "Y", "Z")); + ActionMode = ROTATION; + } + + public Plot3DCanvas(Base b, BasePlot bp) { + super(b, bp); + ActionMode = ROTATION; + } + + public Plot3DCanvas(double[] min, double[] max, String[] axesScales, String[] axesLabels) { + super(min, max, axesScales, axesLabels); + ActionMode = ROTATION; + } + + public void initDrawer() { + draw = new AWTDrawer3D(this); + } + + public void initBasenGrid(double[] min, double[] max) { + initBasenGrid(min, max, new String[]{LINEAR, LINEAR, LINEAR}, new String[]{"X", "Y", "Z"}); + } + + public void initBasenGrid() { + initBasenGrid(new double[]{0, 0, 0}, new double[]{1, 1, 1}); + } + + private static double[][] convertXYZ(double[]... XYZ) { + if (XYZ.length == 3 && XYZ[0].length != 3) { + return mergeColumns(XYZ[0], XYZ[1], XYZ[2]); + } else { + return XYZ; + } + } + + public void setDefaultZoom(double zoom_factor) { + ((Projection3D) ((AWTDrawer3D) draw).projection).factor = zoom_factor; + } + + public int addScatterPlot(String name, Color c, double[][] XYZ) { + return addPlot(new ScatterPlot(name, c, convertXYZ(XYZ))); + } + + public int addScatterPlot(String name, Color c, double[] X, double[] Y, double[] Z) { + return addPlot(new ScatterPlot(name, c, convertXYZ(X, Y, Z))); + } + + public int addLinePlot(String name, Color c, double[][] XYZ) { + return addPlot(new LinePlot(name, c, convertXYZ(XYZ))); + } + + public int addLinePlot(String name, Color c, double[] X, double[] Y, double[] Z) { + return addPlot(new LinePlot(name, c, convertXYZ(X, Y, Z))); + } + + public int addBarPlot(String name, Color c, double[][] XYZ) { + return addPlot(new BarPlot(name, c, convertXYZ(XYZ))); + } + + public int addBarPlot(String name, Color c, double[] X, double[] Y, double[] Z) { + return addPlot(new BarPlot(name, c, convertXYZ(X, Y, Z))); + } + + public int addBoxPlot(String name, Color c, double[][] XY, double[][] dX) { + return addPlot(new BoxPlot3D(XY, dX, c, name)); + } + + public int addBoxPlot(String name, Color c, double[][] XYdX) { + return addPlot(new BoxPlot3D(getColumnsRangeCopy(XYdX, 0, 2), getColumnsRangeCopy(XYdX, 3, 5), c, name)); + } + + public int addHistogramPlot(String name, Color c, double[][] XY, double[][] dX) { + return addPlot(new HistogramPlot3D(name, c, XY, dX)); + } + + public int addHistogramPlot(String name, Color c, double[][] XYdX) { + return addPlot(new HistogramPlot3D(name, c, getColumnsRangeCopy(XYdX, 0, 2), getColumnsRangeCopy(XYdX, 3, 4))); + } + + public int addHistogramPlot(String name, Color c, double[][] XY, int nX, int nY) { + double[][] XYZ = histogram_classes_2D(XY, nX, nY); + return addPlot(new HistogramPlot3D(name, c, XYZ, XYZ[1][0] - XYZ[0][0], XYZ[nX][1] - XYZ[0][1])); + } + + public int addHistogramPlot(String name, Color c, double[][] XY, double[] boundsX, double[] boundsY) { + double[][] XYZ = histogram_classes_2D(XY, boundsX, boundsY); + return addPlot(new HistogramPlot3D(name, c, XYZ, XYZ[1][0] - XYZ[0][0], XYZ[boundsX.length - 1][1] - XYZ[0][1])); + } + + public int addHistogramPlot(String name, Color c, double[][] XY, double minX, double maxX, int nX, double minY, double maxY, int nY) { + double[][] XYZ = histogram_classes_2D(XY, minX, maxX, nX, minY, maxY, nY); + return addPlot(new HistogramPlot3D(name, c, XYZ, XYZ[1][0] - XYZ[0][0], XYZ[nX][1] - XYZ[0][1])); + } + + public int addGridPlot(String name, Color c, double[] X, double[] Y, double[][] Z) { + return addPlot(new GridPlot3D(name, c, X, Y, Z)); + } + + public int addGridPlot(String name, Color c, double[][] XYZMatrix) { + double[] X = new double[XYZMatrix[0].length - 1]; + System.arraycopy(XYZMatrix[0], 1, X, 0, XYZMatrix[0].length - 1); + double[] Y = new double[XYZMatrix.length - 1]; + for (int i = 0; i < Y.length; i++) { + Y[i] = XYZMatrix[i + 1][0]; + } + double[][] Z = getSubMatrixRangeCopy(XYZMatrix, 1, XYZMatrix.length - 1, 1, XYZMatrix[0].length - 1); + + return addGridPlot(name, c, X, Y, Z); + } + + public int addCloudPlot(String name, Color c, double[][] sampleXYZ, int nX, int nY, int nZ) { + double[][] XYZh = histogram_classes_3D(sampleXYZ, nX, nY, nZ); + return addPlot(new CloudPlot3D(name, c, XYZh, XYZh[1][0] - XYZh[0][0], XYZh[nX][1] - XYZh[0][1], XYZh[nX][2] - XYZh[0][2])); + } + + public void mouseDragged(MouseEvent e) { + //System.out.println("PlotCanvas.mouseDragged"); + if (ActionMode == ROTATION) { + dragging = true; + /* + * System.out.println("PlotCanvas.mouseDragged"); System.out.println(" + * mouseClick = [" + mouseClick[0] + " " + mouseClick[1] + "]"); + * System.out.println(" mouseCurent = [" + mouseCurent[0] + " " + + * mouseCurent[1] + "]"); + */ + mouseCurent[0] = e.getX(); + mouseCurent[1] = e.getY(); + e.consume(); + + int[] t = new int[]{mouseCurent[0] - mouseClick[0], mouseCurent[1] - mouseClick[1]}; + ((AWTDrawer3D) draw).rotate(t, new int[]{getWidth(), getHeight()}); + mouseClick[0] = mouseCurent[0]; + mouseClick[1] = mouseCurent[1]; + repaint(); + } else { + super.mouseDragged(e); + } + } + + public void rotate(double theta, double phi) { + Projection3D proj3d = ((Projection3D) ((AWTDrawer) draw).projection); + proj3d.rotate(proj3d.theta + theta, proj3d.phi + phi); + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/canvas/PlotCanvas.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/canvas/PlotCanvas.java new file mode 100755 index 0000000..19228b5 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/canvas/PlotCanvas.java @@ -0,0 +1,970 @@ +package org.xbib.graphics.graph.jmathplot.canvas; + +import static org.xbib.graphics.graph.jmathplot.Base.LINEAR; +import static org.xbib.graphics.graph.jmathplot.Base.LOGARITHM; +import static org.xbib.graphics.graph.jmathplot.Base.STRINGS; +import org.xbib.graphics.graph.jmathplot.BaseLabel; +import org.xbib.graphics.graph.jmathplot.Label; +import org.xbib.graphics.graph.jmathplot.frame.DataFrame; +import org.xbib.graphics.graph.jmathplot.frame.LegendPanel; +import org.xbib.graphics.graph.jmathplot.frame.ScalesFrame; +import org.xbib.graphics.graph.jmathplot.Base; +import org.xbib.graphics.graph.jmathplot.BaseDependant; +import org.xbib.graphics.graph.jmathplot.BasePlot; +import org.xbib.graphics.graph.jmathplot.Plotable; +import org.xbib.graphics.graph.jmathplot.Plot; +import org.xbib.graphics.graph.jmathplot.render.AWTDrawer; +import org.xbib.graphics.graph.jmathplot.render.AbstractDrawer; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import org.xbib.graphics.graph.jmathplot.utils.FastMath; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.RenderingHints; +import java.awt.Stroke; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Set; +import javax.imageio.ImageIO; +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javax.swing.JPanel; + +public abstract class PlotCanvas extends JPanel implements MouseListener, MouseMotionListener, ComponentListener, BaseDependant, MouseWheelListener { + + //public int[] panelSize = new int[] { 400, 400 }; + public Base base; + protected AbstractDrawer draw; + protected BasePlot grid; + public LegendPanel linkedLegendPanel; + public LinkedList plots; + public LinkedList objects; + + // /////////////////////////////////////////// + // ////// Constructor & inits //////////////// + // /////////////////////////////////////////// + public PlotCanvas() { + initPanel(); + initBasenGrid(); + initDrawer(); + } + + public PlotCanvas(Base b, BasePlot bp) { + initPanel(); + initBasenGrid(b, bp); + initDrawer(); + } + + public PlotCanvas(double[] min, double[] max) { + initPanel(); + initBasenGrid(min, max); + initDrawer(); + } + + public PlotCanvas(double[] min, double[] max, String[] axesScales, String[] axesLabels) { + initPanel(); + initBasenGrid(min, max, axesScales, axesLabels); + initDrawer(); + } + + @Override + public boolean isOptimizedDrawingEnabled() { + return true; + } + + public void attachLegend(LegendPanel lp) { + linkedLegendPanel = lp; + } + + private void initPanel() { + objects = new LinkedList(); + plots = new LinkedList(); + + setDoubleBuffered(true); + + //setSize(panelSize[0], panelSize[1]); + //setPreferredSize(new Dimension(panelSize[0], panelSize[1])); + setBackground(Color.white); + + addComponentListener(this); + addMouseListener(this); + addMouseMotionListener(this); + addMouseWheelListener(this); + } + + public abstract void initDrawer(); + + public void initBasenGrid(double[] min, double[] max, String[] axesScales, String[] axesLabels) { + base = new Base(min, max, axesScales); + grid = new BasePlot(base, axesLabels); + // grid.getAxe(0).getDarkLabel().setCorner(0.5,-1); + // grid.getAxe(1).getDarkLabel().setCorner(0,-0.5); + } + + public abstract void initBasenGrid(double[] min, double[] max); + + public abstract void initBasenGrid(); + + public void initBasenGrid(Base b, BasePlot bp) { + base = b; + grid = bp; + + } + + // /////////////////////////////////////////// + // ////// set actions //////////////////////// + // /////////////////////////////////////////// + public void setActionMode(int am) { + ActionMode = am; + } + + public void setNoteCoords(boolean b) { + allowNoteCoord = b; + } + + public void setEditable(boolean b) { + allowEdit = b; + } + + public boolean getEditable() { + return allowEdit; + } + + public void setNotable(boolean b) { + allowNote = b; + } + + public boolean getNotable() { + return allowNote; + } + + // /////////////////////////////////////////// + // ////// set/get elements /////////////////// + // /////////////////////////////////////////// + public LinkedList getPlots() { + return plots; + } + + public Plot getPlot(int i) { + return plots.get(i); + } + + public int getPlotIndex(Plot p) { + for (int i = 0; i < plots.size(); i++) { + if (getPlot(i) == p) { + return i; + } + } + return -1; + } + + public LinkedList getPlotables() { + return objects; + } + + public Plotable getPlotable(int i) { + return objects.get(i); + } + + public BasePlot getGrid() { + return grid; + } + + public String[] getAxisScales() { + return base.getAxesScales(); + } + + public void setAxisLabels(String... labels) { + grid.setLegend(labels); + repaint(); + } + + public void setAxisLabel(int axe, String label) { + grid.setLegend(axe, label); + repaint(); + } + + public void setAxisScales(String... scales) { + base.setAxesScales(scales); + setAutoBounds(); + } + + public void setAxiScale(int axe, String scale) { + base.setAxesScales(axe, scale); + setAutoBounds(axe); + } + + public void setFixedBounds(double[] min, double[] max) { + base.setFixedBounds(min, max); + resetBase(); + repaint(); + } + + public void setFixedBounds(int axe, double min, double max) { + base.setFixedBounds(axe, min, max); + resetBase(); + repaint(); + } + + public void includeInBounds(double... into) { + boolean changed = base.includeInBounds(into); + if (!changed) { + return; + } + grid.resetBase(); + repaint(); + } + + public void includeInBounds(Plot plot) { + boolean changed = base.includeInBounds(Array.min(plot.getBounds())); + changed = changed | base.includeInBounds(Array.max(plot.getBounds())); + if (!changed) { + return; + } + resetBase(); + repaint(); + } + + public void setAutoBounds() { + if (plots.size() > 0) { + Plot plot0 = this.getPlot(0); + base.setRoundBounds(Array.min(plot0.getBounds()), Array.max(plot0.getBounds())); + } else { // build default min and max bounds + double[] min = new double[base.dimension]; + double[] max = new double[base.dimension]; + for (int i = 0; i < base.dimension; i++) { + if (base.getAxeScale(i).equalsIgnoreCase(LINEAR)) { + min[i] = 0.0; + max[i] = 1.0; + } else if (base.getAxeScale(i).equalsIgnoreCase(LOGARITHM)) { + min[i] = 1.0; + max[i] = 10.0; + } + } + base.setRoundBounds(min, max); + } + for (int i = 1; i < plots.size(); i++) { + Plot ploti = this.getPlot(i); + base.includeInBounds(Array.min(ploti.getBounds())); + base.includeInBounds(Array.max(ploti.getBounds())); + } + resetBase(); + repaint(); + } + + public void setAutoBounds(int axe) { + if (plots.size() > 0) { + Plot plot0 = this.getPlot(0); + base.setRoundBounds(axe, Array.min(plot0.getBounds())[axe], Array.max(plot0.getBounds())[axe]); + } else { // build default min and max bounds + double min = 0.0; + double max = 0.0; + if (base.getAxeScale(axe).equalsIgnoreCase(LINEAR) | base.getAxeScale(axe).equalsIgnoreCase(STRINGS)) { + min = 0.0; + max = 1.0; + } else if (base.getAxeScale(axe).equalsIgnoreCase(LOGARITHM)) { + min = 1.0; + max = 10.0; + } + base.setRoundBounds(axe, min, max); + } + + for (int i = 1; i < plots.size(); i++) { + Plot ploti = this.getPlot(i); + base.includeInBounds(axe, Array.min(ploti.getBounds())[axe]); + base.includeInBounds(axe, Array.max(ploti.getBounds())[axe]); + } + resetBase(); + repaint(); + } + + public void resetBase() { + // System.out.println("PlotCanvas.resetBase"); + draw.resetBaseProjection(); + grid.resetBase(); + + for (int i = 0; i < objects.size(); i++) { + if (objects.get(i) instanceof BaseDependant) { + ((BaseDependant) (objects.get(i))).resetBase(); + } + } + repaint(); + } + + // /////////////////////////////////////////// + // ////// add/remove elements //////////////// + // /////////////////////////////////////////// + public void addLabel(String text, Color c, double... where) { + addPlotable(new Label(text, c, where)); + } + + public void addBaseLabel(String text, Color c, double... where) { + addPlotable(new BaseLabel(text, c, where)); + } + + public void addPlotable(Plotable p) { + objects.add(p); + // resetBase(); + repaint(); + } + + public void removePlotable(Plotable p) { + objects.remove(p); + repaint(); + } + + public void removePlotable(int i) { + objects.remove(i); + repaint(); + } + + public void removeAllPlotables() { + objects.clear(); + repaint(); + } + + boolean adjustBounds = true; + + public void setAdjustBounds(boolean adjust) { + adjustBounds = adjust; + } + + public boolean getAdjustBounds() { + return adjustBounds; + } + + public int addPlot(Plot newPlot) { + plots.add(newPlot); + if (linkedLegendPanel != null) { + linkedLegendPanel.updateLegends(); + } + if (plots.size() == 1) { + setAutoBounds(); + } else { + if (adjustBounds) { + includeInBounds(newPlot); + } else { + repaint(); + } + } + return plots.size() - 1; + } + + public void setPlot(int I, Plot p) { + plots.set(I, p); + if (linkedLegendPanel != null) { + linkedLegendPanel.updateLegends(); + } + repaint(); + } + + public void changePlotData(int I, double[]... XY) { + getPlot(I).setData(XY); + if (adjustBounds) { + includeInBounds(getPlot(I)); + } else { + repaint(); + } + } + + public void changePlotName(int I, String name) { + getPlot(I).setName(name); + if (linkedLegendPanel != null) { + linkedLegendPanel.updateLegends(); + } + repaint(); + } + + public void changePlotColor(int I, Color c) { + getPlot(I).setColor(c); + if (linkedLegendPanel != null) { + linkedLegendPanel.updateLegends(); + } + repaint(); + } + + public void removePlot(int I) { + plots.remove(I); + if (linkedLegendPanel != null) { + linkedLegendPanel.updateLegends(); + } + if (plots.size() != 0) { + if (adjustBounds) { + setAutoBounds(); + } else { + repaint(); + } + } + + } + + public void removePlot(Plot p) { + plots.remove(p); + if (linkedLegendPanel != null) { + linkedLegendPanel.updateLegends(); + } + if (plots.size() != 0) { + if (adjustBounds) { + setAutoBounds(); + } + } + + } + + public void removeAllPlots() { + plots.clear(); + if (linkedLegendPanel != null) { + linkedLegendPanel.updateLegends(); + } + clearNotes(); + } + + public void addVectortoPlot(int numPlot, double[][] v) { + getPlot(numPlot).addVector(v); + } + + /*public void addQuantiletoPlot(int numPlot, boolean _symetric, double[]... q) { + getPlot(numPlot).addQuantiles(q, _symetric); + }*/ + public void addQuantiletoPlot(int numPlot, int numAxe, double rate, boolean symetric, double[] q) { + getPlot(numPlot).addQuantile(numAxe, rate, q, symetric); + } + + public void addQuantiletoPlot(int numPlot, int numAxe, double rate, boolean symetric, double q) { + getPlot(numPlot).addQuantile(numAxe, rate, q, symetric); + } + + public void addQuantilestoPlot(int numPlot, int numAxe, double[][] q) { + getPlot(numPlot).addQuantiles(numAxe, q); + } + + public void addQuantilestoPlot(int numPlot, int numAxe, double[] q) { + getPlot(numPlot).addQuantiles(numAxe, q); + } + + public void addGaussQuantilestoPlot(int numPlot, int numAxe, double[] s) { + getPlot(numPlot).addGaussQuantiles(numAxe, s); + } + + public void addGaussQuantilestoPlot(int numPlot, int numAxe, double s) { + getPlot(numPlot).addGaussQuantiles(numAxe, s); + } + + // /////////////////////////////////////////// + // ////// call for toolbar actions /////////// + // /////////////////////////////////////////// + public void toGraphicFile(File file) throws IOException { + + Image image = createImage(getWidth(), getHeight()); + paint(image.getGraphics()); + image = new ImageIcon(image).getImage(); + + BufferedImage bufferedImage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_RGB); + Graphics g = bufferedImage.createGraphics(); + g.drawImage(image, 0, 0, Color.WHITE, null); + g.dispose(); + + try { + ImageIO.write(bufferedImage, "PNG", file); + } catch (IllegalArgumentException ex) { + } + } + + JFrame scalesFrame = new ScalesFrame(this); + + public void setScalesFrame(JFrame scalesFrame) { + this.scalesFrame = scalesFrame; + } + + public void displayScalesFrame() { + scalesFrame.setVisible(true); + } + + DataFrame dataFrame = new DataFrame(this); + + public void setDataFrame(DataFrame dataFrame) { + this.dataFrame = dataFrame; + } + + public void displayDataFrame(int i) { + dataFrame.selectIndex(i); + } + + public void displayDataFrame() { + displayDataFrame(0); + } + + boolean mapset = false; + + public void resetMapData() { + for (int i = 0; i < grid.getAxis().length; i++) { + grid.getAxis()[i].setStringMap(null); + setAxiScale(i, Base.LINEAR); + } + mapset = false; + } + + public double[][] mapData(Object[][] data) { + //System.out.println("mapData:" + Array.cat(data)); + + double[][] mapeddata = new double[data.length][data[0].length]; + + if (!mapset) { + for (int j = 0; j < data[0].length; j++) { + if (!Array.isDouble(data[0][j].toString())) { + //System.out.println(data[0][j].toString() + " is not a double"); + setAxiScale(j, Base.STRINGS); + + ArrayList string_array_j = new ArrayList(data.length); + for (int i = 0; i < data.length; i++) { + string_array_j.add(data[i][j].toString()); + } + + grid.getAxis(j).setStringMap(Array.mapStringArray(string_array_j)); + grid.getAxis(j).init(); + + for (int i = 0; i < data.length; i++) { + mapeddata[i][j] = grid.getAxis(j).getStringMap().get(data[i][j].toString()); + } + + //System.out.println("Axe " + j + ":" + Array.toString(grid.getAxe(j).getStringMap())); + initReverseMap(j); + } else { + //System.out.println(data[0][j].toString() + " is a double"); + //System.out.println("Axe " + j + ": double[]"); + for (int i = 0; i < data.length; i++) { + mapeddata[i][j] = Double.valueOf(data[i][j].toString()); + } + } + } + mapset = true; + } else { + for (int j = 0; j < data[0].length; j++) { + if (!Array.isDouble(data[0][j].toString())) { + //System.out.println(data[0][j].toString() + " is not a double"); + if (base.getAxeScale(j).equals(Base.STRINGS)) { + for (int i = 0; i < data.length; i++) { + if (!grid.getAxis(j).getStringMap().containsKey(data[i][j].toString())) { + Set s = grid.getAxis(j).getStringMap().keySet(); + ArrayList string_array_j = new ArrayList(s.size() + 1); + string_array_j.addAll(s); + string_array_j.add(data[i][j].toString()); + grid.getAxis(j).setStringMap(Array.mapStringArray(string_array_j)); + + //System.out.println("Axe " + j + ":" + Array.toString(grid.getAxe(j).getStringMap())); + initReverseMap(j); + } + mapeddata[i][j] = grid.getAxis(j).getStringMap().get(data[i][j].toString()); + } + } else { + throw new IllegalArgumentException("The mapping of this PlotPanel was not set on axis " + j); + } + } else { + //System.out.println(data[0][j].toString() + " is a double"); + //System.out.println("Axe " + j + ": double[]"); + for (int i = 0; i < data.length; i++) { + mapeddata[i][j] = Double.valueOf(data[i][j].toString()); + } + } + } + } + return mapeddata; + } + + public Object[][] reverseMapedData(double[][] mapeddata) { + Object[][] stringdata = new Object[mapeddata.length][mapeddata[0].length]; + + for (int i = 0; i < mapeddata.length; i++) { + stringdata[i] = reverseMapedData(mapeddata[i]); + } + + return stringdata; + } + + public Object[] reverseMapedData(double[] mapeddata) { + Object[] stringdata = new Object[mapeddata.length]; + + if (reversedMaps == null) { + reversedMaps = new HashMap[grid.getAxis().length]; + } + + for (int j = 0; j < mapeddata.length; j++) { + if (reversedMaps[j] != null) { + stringdata[j] = reversedMaps[j].get(mapeddata[j]); + } else { + stringdata[j] = mapeddata[j]; + } + } + + return stringdata; + } + + HashMap[] reversedMaps; + + private void initReverseMap(int j) { + if (reversedMaps == null) { + reversedMaps = new HashMap[grid.getAxis().length]; + } + + if (grid.getAxis(j) != null) { + reversedMaps[j] = Array.reverseStringMap(grid.getAxis(j).getStringMap()); + } + } + + // /////////////////////////////////////////// + // ////// Paint method /////////////////////// + // /////////////////////////////////////////// + // anti-aliasing constant + final protected static RenderingHints AALIAS = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + public static Color NOTE_COLOR = Color.DARK_GRAY; + public static Color EDIT_COLOR = Color.BLACK; + public boolean allowEdit = true; + public boolean allowNote = true; + public boolean allowNoteCoord = true; + + public void paint(Graphics gcomp) { + // System.out.println("PlotCanvas.paint"); + + Graphics2D gcomp2D = (Graphics2D) gcomp; + + // anti-aliasing methods + gcomp2D.addRenderingHints(AALIAS); + gcomp2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + + gcomp2D.setColor(getBackground()); + gcomp2D.fillRect(0, 0, getSize().width, getSize().height); + + draw.initGraphics(gcomp2D); + + // draw plot + grid.plot(draw); + + for (int i = 0; i < plots.size(); i++) { + getPlot(i).plot(draw); + if (linkedLegendPanel != null) { + linkedLegendPanel.nonote(i); + } + } + + for (int i = 0; i < objects.size(); i++) { + getPlotable(i).plot(draw); + } + + if (drawRect != null) { + gcomp2D.setColor(Color.black); + gcomp2D.setStroke(rectStroke); + gcomp2D.drawRect(drawRect[0], drawRect[1], drawRect[2], drawRect[3]); + } + + // draw note + if (allowNote) { + /*if (allowNoteCoord && coordNoted != null) { + draw.setColor(NOTE_COLOR); + draw.drawCoordinate(coordNoted); + draw.drawText(Array.cat(reverseMapedData(coordNoted)), coordNoted); + }*/ + for (int i = 0; i < plots.size(); i++) { + if (getPlot(i).noted) { + if (linkedLegendPanel != null) { + linkedLegendPanel.note(i); + } + getPlot(i).note(draw); + //return; + } + if (allowNoteCoord && getPlot(i).coordNoted != null) { + getPlot(i).noteCoord(draw, getPlot(i).coordNoted); + } + } + } + } + + // /////////////////////////////////////////// + // ////// Listeners ////////////////////////// + // /////////////////////////////////////////// + public final static int ZOOM = 0; + public final static int TRANSLATION = 1; + public int ActionMode; + protected boolean dragging = false; + protected int[] mouseCurent = new int[2]; + protected int[] mouseClick = new int[2]; + + public void clearNotes() { + for (int i = 0; i < plots.size(); i++) { + getPlot(i).coordNoted = null; + } + repaint(); + } + + public void mousePressed(MouseEvent e) { + //System.out.println("PlotCanvas.mousePressed"); + /* + * System.out.println("PlotCanvas.mousePressed"); System.out.println(" + * mouseClick = [" + mouseClick[0] + " " + mouseClick[1] + "]"); + * System.out.println(" mouseCurent = [" + mouseCurent[0] + " " + + * mouseCurent[1] + "]"); + */ + mouseCurent[0] = e.getX(); + mouseCurent[1] = e.getY(); + e.consume(); + mouseClick[0] = mouseCurent[0]; + mouseClick[1] = mouseCurent[1]; + } + + int[] drawRect = null; + final Stroke rectStroke = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL); + + public void mouseDragged(MouseEvent e) { + //System.out.println("PlotCanvas.mouseDragged"); + + dragging = true; + /* + * System.out.println("PlotCanvas.mouseDragged"); System.out.println(" + * mouseClick = [" + mouseClick[0] + " " + mouseClick[1] + "]"); + * System.out.println(" mouseCurent = [" + mouseCurent[0] + " " + + * mouseCurent[1] + "]"); + */ + mouseCurent[0] = e.getX(); + mouseCurent[1] = e.getY(); + e.consume(); + switch (ActionMode) { + case TRANSLATION: + draw.translate(mouseCurent[0] - mouseClick[0], mouseCurent[1] - mouseClick[1]); + mouseClick[0] = mouseCurent[0]; + mouseClick[1] = mouseCurent[1]; + repaint(); + break; + case ZOOM: + int x = FastMath.min(mouseClick[0], mouseCurent[0]); + int y = FastMath.min(mouseClick[1], mouseCurent[1]); + int w = FastMath.abs(mouseCurent[0] - mouseClick[0]); + int h = FastMath.abs(mouseCurent[1] - mouseClick[1]); + if (drawRect == null) { + drawRect = new int[4]; + } + drawRect[0] = x; + drawRect[1] = y; + drawRect[2] = w; + drawRect[3] = h; + repaint(); //repaint(x - 1, y - 1, w + 2, h + 2); + break; + } + //repaint(); + } + + public void mouseReleased(MouseEvent e) { + //System.out.println("PlotCanvas.mouseReleased"); + + /* + * System.out.println("PlotCanvas.mouseReleased"); System.out.println(" + * mouseClick = [" + mouseClick[0] + " " + mouseClick[1] + "]"); + * System.out.println(" mouseCurent = [" + mouseCurent[0] + " " + + * mouseCurent[1] + "]"); + */ + mouseCurent[0] = e.getX(); + mouseCurent[1] = e.getY(); + e.consume(); + switch (ActionMode) { + case ZOOM: + if (FastMath.abs(mouseCurent[0] - mouseClick[0]) > 10 && FastMath.abs(mouseCurent[1] - mouseClick[1]) > 10) { + int[] origin = {FastMath.min(mouseClick[0], mouseCurent[0]), FastMath.min(mouseClick[1], mouseCurent[1])}; + double[] ratio = {FastMath.abs((double) (mouseCurent[0] - mouseClick[0]) / (double) getWidth()), + FastMath.abs((double) (mouseCurent[1] - mouseClick[1]) / (double) getHeight()) + }; + draw.dilate(origin, ratio); + drawRect = null; + repaint(); + } else { + drawRect = null; + repaint(); + } + break; + } + //repaint(); + dragging = false; + } + + public void mouseClicked(MouseEvent e) { + //System.out.println("PlotCanvas.mouseClicked"); + + /* + * System.out.println("PlotCanvas.mouseClicked"); System.out.println(" + * mouseClick = [" + mouseClick[0] + " " + mouseClick[1] + "]"); + * System.out.println(" mouseCurent = [" + mouseCurent[0] + " " + + * mouseCurent[1] + "]"); + */ + mouseCurent[0] = e.getX(); + mouseCurent[1] = e.getY(); + e.consume(); + mouseClick[0] = mouseCurent[0]; + mouseClick[1] = mouseCurent[1]; + + if (allowEdit) { + if (e.getModifiers() == MouseEvent.BUTTON1_MASK && e.getClickCount() > 1) { + for (int i = 0; i < grid.getAxis().length; i++) { + if (grid.getAxis(i).isSelected(mouseClick, draw) != null) { + grid.getAxis(i).edit(this); + return; + } + } + + for (int i = 0; i < plots.size(); i++) { + if (getPlot(i).isSelected(mouseClick, draw) != null) { + getPlot(i).edit(this); + return; + } + } + } + } + + if (!dragging && allowNote) { + for (int i = 0; i < plots.size(); i++) { + double[] _coordNoted = getPlot(i).isSelected(mouseClick, draw); + if (e.getModifiers() == MouseEvent.BUTTON1_MASK) { + if (_coordNoted != null) { + getPlot(i).noted = !getPlot(i).noted; + } else { + getPlot(i).noted = false; + } + } else if (e.getModifiers() == MouseEvent.BUTTON3_MASK) { + if (_coordNoted != null) { + if (getPlot(i).coordNoted != null) { + boolean alreadyNoted = true; + for (int j = 0; j < _coordNoted.length; j++) { + alreadyNoted = alreadyNoted && _coordNoted[j] == getPlot(i).coordNoted[j]; + } + if (alreadyNoted) { + getPlot(i).coordNoted = null; + } else { + getPlot(i).coordNoted = _coordNoted; + } + } else { + getPlot(i).coordNoted = _coordNoted; + } + } + } + } + repaint(); + } else { + dragging = false; + } + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + public void mouseMoved(MouseEvent e) { + mouseCurent[0] = e.getX(); + mouseCurent[1] = e.getY(); + e.consume(); + for (int i = 0; i < plots.size(); i++) { + if (getPlot(i).noted) { + double[] _coordNoted = getPlot(i).isSelected(mouseCurent, draw); + if (_coordNoted != null) { + getPlot(i).coordNoted = _coordNoted; + repaint(); + } + } + } + + //System.out.println("PlotCanvas.mouseMoved"); + /* + * System.out.println("PlotCanvas.mouseClicked"); System.out.println(" + * mouseClick = [" + mouseClick[0] + " " + mouseClick[1] + "]"); + * System.out.println(" mouseCurent = [" + mouseCurent[0] + " " + + * mouseCurent[1] + "]"); + */ + /*mouseCurent[0] = e.getX(); + mouseCurent[1] = e.getY(); + e.consume(); + if (allowNote) { + for (int i = 0; i < plots.size(); i++) { + double[] _coordNoted = getPlot(i).isSelected(mouseCurent, draw); + if (_coordNoted != null) { + getPlot(i).noted = !getPlot(i).noted; + } else { + getPlot(i).noted = false; + } + } + repaint(); + }*/ + } + + public void mouseWheelMoved(MouseWheelEvent e) { + //System.out.println("PlotCanvas.mouseWheelMoved"); + /* + * System.out.println("PlotCanvas.mouseWheelMoved"); + * System.out.println(" mouseClick = [" + mouseClick[0] + " " + + * mouseClick[1] + "]"); System.out.println(" mouseCurent = [" + + * mouseCurent[0] + " " + mouseCurent[1] + "]"); + */ + mouseCurent[0] = e.getX(); + mouseCurent[1] = e.getY(); + e.consume(); + int[] origin; + double[] ratio; + // double factor = 1.5; + //switch (ActionMode) { + // case ZOOM: + if (e.getWheelRotation() == -1) { + if (Array.max(((AWTDrawer) draw).projection.totalScreenRatio) > .01) { + /* (2*factor) */ + /* (2*factor) */ + origin = new int[]{mouseCurent[0] - getWidth() / 3, + mouseCurent[1] - getHeight() / 3}; + ratio = new double[]{0.666/* 1/factor, 1/factor */, 0.666}; + draw.dilate(origin, ratio); + } + } else { + if (Array.max(((AWTDrawer) draw).projection.totalScreenRatio) < 1) { + origin = new int[]{(int) (mouseCurent[0] - getWidth() / 1.333/* (2/factor) */), + (int) (mouseCurent[1] - getHeight() / 1.333/* (2/factor) */) + }; + ratio = new double[]{1.5, 1.5 /* factor, factor */}; + draw.dilate(origin, ratio); + } else /* (Array.max(((AWTDrawer) draw).projection.totalScreenRatio) >= 1)*/ { + ((AWTDrawer) draw).projection.initBaseCoordsProjection(true); + } + } + repaint(); + // break; + //} + } + + public void componentHidden(ComponentEvent e) { + } + + public void componentMoved(ComponentEvent e) { + } + + public void componentResized(ComponentEvent e) { + //System.out.println("PlotCanvas.componentResized"); + //panelSize = new int[] { (int) (getSize().getWidth()), (int) (getSize().getHeight()) }; + if (draw != null) { + draw.resetBaseProjection(); + } + //System.out.println("PlotCanvas : "+panelSize[0]+" x "+panelSize[1]); + repaint(); + if (linkedLegendPanel != null) { + linkedLegendPanel.componentResized(e); + } + } + + public void componentShown(ComponentEvent e) { + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/DataFrame.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/DataFrame.java new file mode 100644 index 0000000..18149b3 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/DataFrame.java @@ -0,0 +1,53 @@ +package org.xbib.graphics.graph.jmathplot.frame; + +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; +import org.xbib.graphics.graph.jmathplot.Plot; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; + +/** + * BSD License + * + * @author Yann RICHET + */ +public class DataFrame extends JFrame { + + private static final long serialVersionUID = 1L; + private final PlotCanvas plotCanvas; + private final JTabbedPane panels; + + public DataFrame(PlotCanvas p) { + super("Data"); + plotCanvas = p; + JPanel panel = new JPanel(); + panels = new JTabbedPane(); + + panel.add(panels); + setContentPane(panel); + //setVisible(true); + } + + @Override + public void setVisible(boolean b) { + if (b) { + setPanel(); + } + super.setVisible(b); + } + + private void setPanel() { + panels.removeAll(); + for (Plot plot : plotCanvas.getPlots()) { + panels.add(plot.getDataPanel(plotCanvas), plot.getName()); + } + pack(); + } + + public void selectIndex(int i) { + setVisible(true); + if (panels.getTabCount() > i) { + panels.setSelectedIndex(i); + } + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/DataToolBar.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/DataToolBar.java new file mode 100644 index 0000000..79f8239 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/DataToolBar.java @@ -0,0 +1,86 @@ +package org.xbib.graphics.graph.jmathplot.frame; + +import org.xbib.graphics.graph.jmathplot.panel.DataPanel; +import org.xbib.graphics.graph.jmathplot.panel.PlotPanel; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.security.AccessControlException; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JToolBar; + +/** + * BSD License + * + * @author Yann RICHET + */ + +public class DataToolBar extends JToolBar { + + /** + * Comment for serialVersionUID + */ + private static final long serialVersionUID = 1L; + + protected JButton buttonPasteToClipboard; + + protected JButton buttonSaveFile; + + private boolean denySaveSecurity; + + private JFileChooser fileChooser; + + private final DataPanel dataPanel; + + public DataToolBar(DataPanel dp) { + + dataPanel = dp; + + try { + fileChooser = new JFileChooser(); + } catch (AccessControlException ace) { + denySaveSecurity = true; + } + + buttonPasteToClipboard = new JButton(new ImageIcon(PlotPanel.class.getResource("icons/toclipboard.png"))); + buttonPasteToClipboard.setToolTipText("Copy data to clipboard"); + + buttonSaveFile = new JButton(new ImageIcon(PlotPanel.class.getResource("icons/tofile.png"))); + buttonSaveFile.setToolTipText("Save data into ASCII file"); + + buttonPasteToClipboard.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + dataPanel.toClipBoard(); + } + }); + buttonSaveFile.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + chooseFile(); + } + }); + + add(buttonPasteToClipboard, null); + add(buttonSaveFile, null); + + if (!denySaveSecurity) { + fileChooser.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + saveFile(); + } + }); + } else { + buttonSaveFile.setEnabled(false); + } + } + + void saveFile() { + java.io.File file = fileChooser.getSelectedFile(); + dataPanel.toASCIIFile(file); + } + + void chooseFile() { + fileChooser.showSaveDialog(this); + } + +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/DatasFrame.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/DatasFrame.java new file mode 100644 index 0000000..2823166 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/DatasFrame.java @@ -0,0 +1,106 @@ +package org.xbib.graphics.graph.jmathplot.frame; + +import org.xbib.graphics.graph.jmathplot.panel.MatrixTablePanel; +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; +import org.xbib.graphics.graph.jmathplot.Plot; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JColorChooser; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * BSD License + * + * @author Yann RICHET + */ + +public class DatasFrame extends JFrame { + + private static final long serialVersionUID = 1L; + + private final PlotCanvas plotCanvas; + + private final LegendPanel legend; + + public JTabbedPane panels; + + public DatasFrame(PlotCanvas p, LegendPanel l) { + super("Data"); + plotCanvas = p; + legend = l; + JPanel panel = new JPanel(); + panels = new JTabbedPane(); + + for (Plot plot : plotCanvas.getPlots()) { + panels.add(new DataPanel(/*this, */plot), plot.getName()); + } + + + panel.add(panels); + setContentPane(panel); + pack(); + setVisible(true); + } + + public class DataPanel extends JPanel { + + private static final long serialVersionUID = 1L; + + MatrixTablePanel XY; + + JCheckBox visible; + + JButton color; + + JPanel plottoolspanel; + + Plot plot; + + DatasFrame dframe; + + public DataPanel(/*DatasFrame _dframe,*/Plot _plot) { + plot = _plot; + //dframe = _dframe; + visible = new JCheckBox("visible"); + visible.setSelected(plot.getVisible()); + color = new JButton(); + color.setBackground(plot.getColor()); + XY = new MatrixTablePanel(plotCanvas.reverseMapedData(plot.getData())); + + visible.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + plot.setVisible(visible.isSelected()); + /*dframe.*/ + plotCanvas.repaint(); + } + }); + color.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + Color c = JColorChooser.showDialog(plotCanvas, "Choose plot color", plot.getColor()); + color.setBackground(c); + plot.setColor(c); + legend.updateLegends(); + /*dframe.*/ + plotCanvas.linkedLegendPanel.repaint(); + /*dframe.*/ + plotCanvas.repaint(); + } + }); + + this.setLayout(new BorderLayout()); + plottoolspanel = new JPanel(); + plottoolspanel.add(visible); + plottoolspanel.add(color); + this.add(plottoolspanel, BorderLayout.NORTH); + this.add(XY, BorderLayout.CENTER); + } + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/LegendPanel.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/LegendPanel.java new file mode 100644 index 0000000..baa019e --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/LegendPanel.java @@ -0,0 +1,270 @@ +package org.xbib.graphics.graph.jmathplot.frame; + +import org.xbib.graphics.graph.jmathplot.panel.PlotPanel; +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; +import org.xbib.graphics.graph.jmathplot.Plot; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.util.LinkedList; +import javax.swing.BorderFactory; +import javax.swing.JColorChooser; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.border.Border; + +/** + * BSD License + * + * @author Yann RICHET + */ +public class LegendPanel extends JPanel implements ComponentListener { + + private static final long serialVersionUID = 1L; + PlotCanvas plotCanvas; + PlotPanel plotPanel; + LinkedList legends; + public static int INVISIBLE = -1; + public static int VERTICAL = 0; + public static int HORIZONTAL = 1; + int orientation; + private int maxHeight; + private int maxWidth; + JPanel container; + private final int inset = 5; + + public LegendPanel(PlotPanel _plotPanel, int _orientation) { + plotPanel = _plotPanel; + plotCanvas = plotPanel.plotCanvas; + plotCanvas.attachLegend(this); + orientation = _orientation; + + container = new JPanel(); + container.setBackground(plotCanvas.getBackground()); + container.setLayout(new GridLayout(1, 1, inset, inset)); + container.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(Color.GRAY, 1), null)); + + updateLegends(); + + setBackground(plotCanvas.getBackground()); + addComponentListener(this); + setLayout(new GridBagLayout()); + + add(container); + } + + public void updateLegends() { + if (orientation != INVISIBLE) { + container.removeAll(); + + maxHeight = 1; + maxWidth = 1; + + legends = new LinkedList(); + for (Plot plot : plotCanvas.getPlots()) { + if (!plot.getVisible()) { + continue; + } + Legend l = new Legend(plot); + legends.add(l); + + maxWidth = (int) Math.max(maxWidth, l.getPreferredSize().getWidth()); + maxHeight = (int) Math.max(maxHeight, l.getPreferredSize().getHeight()); + + container.add(l); + } + + updateSize(); + //repaint(); + } + } + + private void updateSize() { + //System.out.println("LegendPanel.updateSize"); + if (orientation == VERTICAL) { + int nh = 1; + if (maxHeight < plotCanvas.getHeight()) { + nh = plotCanvas.getHeight() / (maxHeight + inset); + } + int nw = 1 + legends.size() / nh; + + ((GridLayout) (container.getLayout())).setColumns(nw); + ((GridLayout) (container.getLayout())).setRows(1 + legends.size() / nw); + + container.setPreferredSize(new Dimension((maxWidth + inset) * nw, (maxHeight + inset) * (1 + legends.size() / nw))); + + } else if (orientation == HORIZONTAL) { + int nw = 1; + if (maxWidth < plotCanvas.getWidth()) { + nw = plotCanvas.getWidth() / (maxWidth + inset); + } + int nh = 1 + legends.size() / nw; + + ((GridLayout) (container.getLayout())).setRows(nh); + ((GridLayout) (container.getLayout())).setColumns(1 + legends.size() / nh); + + container.setPreferredSize(new Dimension((maxWidth + inset) * (1 + legends.size() / nh), (maxHeight + inset) * nh)); + } + container.updateUI(); + } + + public void note(int i) { + if (orientation != INVISIBLE && legends != null && legends.size() > i) { + legends.get(i).setBackground(PlotCanvas.NOTE_COLOR); + legends.get(i).name.setForeground(plotPanel.getBackground()); + } + } + + public void nonote(int i) { + if (orientation != INVISIBLE && legends != null && legends.size() > i) { + legends.get(i).setBackground(plotPanel.getBackground()); + legends.get(i).name.setForeground(PlotCanvas.NOTE_COLOR); + } + } + + public void componentResized(ComponentEvent e) { + //System.out.println("LegendPanel.componentResized"); + //System.out.println("PlotCanvas : "+plotCanvas.panelSize[0]+" x "+plotCanvas.panelSize[1]); + if (orientation != INVISIBLE) { + updateSize(); + } + } + + public void componentMoved(ComponentEvent e) { + } + + public void componentShown(ComponentEvent e) { + } + + public void componentHidden(ComponentEvent e) { + } + + public class Legend extends JPanel { + + private static final long serialVersionUID = 1L; + JPanel color; + JLabel name; + Plot plot; + Border colorborder; + + public Legend(Plot p) { + plot = p; + + setLayout(new BorderLayout(2, 2)); + + color = new JPanel(); + colorborder = BorderFactory.createMatteBorder(2, 1, 2, 1, Color.WHITE); + color.setBorder(colorborder); + name = new JLabel(); + name.setFont(plotPanel.getFont()); + + setBackground(Color.WHITE); + + update(); + + add(color, BorderLayout.WEST); + add(name, BorderLayout.CENTER); + + name.addMouseListener(new MouseListener() { + + public void mouseClicked(MouseEvent e) { + if (e.getModifiers() == MouseEvent.BUTTON1_MASK) { + if (plotCanvas.allowEdit && e.getClickCount() > 1) { + editText(); + } + } + if (plotCanvas.allowNote && e.getClickCount() <= 1) { + note_nonote(); + } + } + + public void mousePressed(MouseEvent e) { + } + + public void mouseReleased(MouseEvent e) { + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + }); + + color.addMouseListener(new MouseListener() { + + public void mouseClicked(MouseEvent e) { + if (e.getModifiers() == MouseEvent.BUTTON1_MASK) { + if (plotCanvas.allowEdit && e.getClickCount() > 1) { + editColor(); + } + } + if (plotCanvas.allowNote && e.getClickCount() <= 1) { + note_nonote(); + } + } + + public void mousePressed(MouseEvent e) { + } + + public void mouseReleased(MouseEvent e) { + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + }); + } + + public void editText() { + String name1 = JOptionPane.showInputDialog(plotCanvas, "Choose name", plot.getName()); + if (name1 != null) { + plot.setName(name1); + update(); + updateLegends(); + } + } + + public void editColor() { + Color c = JColorChooser.showDialog(plotCanvas, "Choose plot color", plot.getColor()); + if (c != null) { + plot.setColor(c); + update(); + plotCanvas.repaint(); + } + } + + public void update() { + int size = name.getFont().getSize(); + color.setSize(new Dimension(size, size)); + color.setPreferredSize(new Dimension(size, size)); + + // TODO change legend when plot is invisible + /*if (!plot.visible) + color.setBackground(Color.LIGHT_GRAY); + else*/ + color.setBackground(plot.getColor()); + + /*if (!plot.visible) { + name.setFont(name.getFont().deriveFont(Font.ITALIC)); + name.setForeground(Color.LIGHT_GRAY); + }*/ + name.setText(plot.getName()); + repaint(); + } + + public void note_nonote() { + plot.noted = !plot.noted; + plotCanvas.repaint(); + } + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/PlotToolBar.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/PlotToolBar.java new file mode 100644 index 0000000..9554c24 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/PlotToolBar.java @@ -0,0 +1,250 @@ +package org.xbib.graphics.graph.jmathplot.frame; + +import org.xbib.graphics.graph.jmathplot.panel.PlotPanel; +import org.xbib.graphics.graph.jmathplot.canvas.Plot3DCanvas; +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.IOException; +import java.security.AccessControlException; +import javax.imageio.ImageIO; +import javax.swing.ButtonGroup; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.JToggleButton; +import javax.swing.JToolBar; +import javax.swing.filechooser.FileFilter; + +public class PlotToolBar extends JToolBar { + + // TODO redesign icons... + private static final long serialVersionUID = 1L; + protected ButtonGroup buttonGroup; + protected JToggleButton buttonCenter; + //protected JToggleButton buttonEdit; + protected JToggleButton buttonZoom; + protected JToggleButton buttonRotate; + //protected JToggleButton buttonViewCoords; + protected JButton buttonSetScales; + protected JButton buttonDatas; + protected JButton buttonSavePNGFile; + protected JButton buttonReset; + protected JButton buttonAdjustBounds; + private boolean denySaveSecurity; + private JFileChooser pngFileChooser; + /** + * the currently selected PlotPanel + */ + private final PlotCanvas plotCanvas; + private final PlotPanel plotPanel; + + public PlotToolBar(PlotPanel pp) { + plotPanel = pp; + plotCanvas = pp.plotCanvas; + + try { + pngFileChooser = new JFileChooser(); + pngFileChooser.setFileFilter(new FileFilter() { + + public boolean accept(File f) { + return f.isDirectory() || f.getName().endsWith(".png"); + } + + public String getDescription() { + return "Portable Network Graphic file"; + } + }); + } catch (AccessControlException ace) { + denySaveSecurity = true; + } + + try { + buttonGroup = new ButtonGroup(); + + buttonCenter = new JToggleButton(new ImageIcon(ImageIO.read(getClass().getResourceAsStream("icons/center.png")))); + buttonCenter.setToolTipText("Center axis"); + buttonCenter.setSelected(plotCanvas.ActionMode == PlotCanvas.TRANSLATION); + + buttonZoom = new JToggleButton(new ImageIcon(ImageIO.read(getClass().getResourceAsStream("icons/zoom.png")))); + buttonZoom.setToolTipText("Zoom"); + buttonZoom.setSelected(plotCanvas.ActionMode == PlotCanvas.ZOOM); + + //buttonEdit = new JToggleButton(new ImageIcon(PlotPanel.class.getResource("icons/edit.png"))); + //buttonEdit.setToolTipText("Edit mode"); + //buttonViewCoords = new JToggleButton(new ImageIcon(PlotPanel.class.getResource("icons/position.png"))); + //buttonViewCoords.setToolTipText("Highlight coordinates / Highlight plot"); + buttonSetScales = new JButton(new ImageIcon(ImageIO.read(getClass().getResourceAsStream("icons/scale.png")))); + buttonSetScales.setToolTipText("Edit axis scales"); + + buttonDatas = new JButton(new ImageIcon(ImageIO.read(getClass().getResourceAsStream("icons/data.png")))); + buttonDatas.setToolTipText("Get data"); + + buttonSavePNGFile = new JButton(new ImageIcon(ImageIO.read(getClass().getResourceAsStream("icons/topngfile.png")))); + buttonSavePNGFile.setToolTipText("Save graphics in a .PNG File"); + + buttonReset = new JButton(new ImageIcon(getClass().getResource("icons/back.png"))); + buttonReset.setToolTipText("Reset zoom & axis"); + + buttonAdjustBounds = new JButton(new ImageIcon(ImageIO.read(getClass().getResourceAsStream(plotCanvas.getAdjustBounds() ? "icons/adjustbounds.png" : "icons/noadjustbounds.png")))); + buttonAdjustBounds.setToolTipText("Auto-update/fix bounds"); + + /*buttonEdit.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + plotCanvas.ActionMode = PlotCanvas.EDIT; + } + });*/ + buttonZoom.setSelected(true); + buttonZoom.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + plotCanvas.ActionMode = PlotCanvas.ZOOM; + } + }); + + buttonCenter.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + plotCanvas.ActionMode = PlotCanvas.TRANSLATION; + } + }); + + /*buttonViewCoords.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + plotCanvas.setNoteCoords(buttonViewCoords.isSelected()); + } + });*/ + buttonSetScales.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + plotCanvas.displayScalesFrame(); + } + }); + + buttonDatas.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + plotCanvas.displayDataFrame(); + } + }); + + buttonSavePNGFile.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + choosePNGFile(); + } + }); + + buttonReset.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + plotCanvas.resetBase(); + } + }); + + buttonAdjustBounds.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + plotCanvas.setAdjustBounds(!plotCanvas.getAdjustBounds()); + ajustBoundsChanged(); + } + }); + + buttonGroup.add(buttonCenter); + buttonGroup.add(buttonZoom); + //buttonGroup.add(buttonEdit); + + add(buttonCenter, null); + add(buttonZoom, null); + add(buttonReset, null); + //add(buttonViewCoords, null); + add(buttonSetScales, null); + if (adjustBoundsVisible) { + add(buttonAdjustBounds, null); + } + //add(buttonEdit, null); + add(buttonSavePNGFile, null); + add(buttonDatas, null); + + if (!denySaveSecurity) { + pngFileChooser.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + saveGraphicFile(); + } + }); + } else { + buttonSavePNGFile.setEnabled(false); + } + + //buttonEdit.setEnabled(plotCanvas.getEditable()); + //buttonViewCoords.setEnabled(plotCanvas.getNotable()); + // allow mixed (2D/3D) plots managed by one toolbar + if (plotCanvas instanceof Plot3DCanvas) { + if (buttonRotate == null) { + buttonRotate = new JToggleButton(new ImageIcon(ImageIO.read(getClass().getResourceAsStream("icons/rotation.png")))); + buttonRotate.setToolTipText("Rotate axes"); + + buttonRotate.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + plotCanvas.ActionMode = Plot3DCanvas.ROTATION; + } + }); + buttonGroup.add(buttonRotate); + add(buttonRotate, null, 2); + buttonRotate.setSelected(plotCanvas.ActionMode == Plot3DCanvas.ROTATION); + } else { + buttonRotate.setEnabled(true); + } + } else { + if (buttonRotate != null) { + // no removal/disabling just disable + if (plotCanvas.ActionMode == Plot3DCanvas.ROTATION) { + plotCanvas.ActionMode = PlotCanvas.ZOOM; + } + buttonRotate.setEnabled(false); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + void choosePNGFile() { + pngFileChooser.showSaveDialog(this); + } + + void saveGraphicFile() { + File file = pngFileChooser.getSelectedFile(); + try { + plotPanel.toGraphicFile(file); + } catch (IOException e) { + JOptionPane.showConfirmDialog(null, "Save failed : " + e.getMessage(), "Error", JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE); + } + } + + boolean adjustBoundsVisible = true; + + public void viewAdjustBounds(boolean visible) { + if (visible && !adjustBoundsVisible) { + add(buttonAdjustBounds, null); + adjustBoundsVisible = true; + } + if (!visible && adjustBoundsVisible) { + remove(buttonAdjustBounds); + adjustBoundsVisible = false; + } + ajustBoundsChanged(); + } + + public void ajustBoundsChanged() { + try { + buttonAdjustBounds.setIcon(new ImageIcon(ImageIO.read(getClass().getResourceAsStream(plotCanvas.getAdjustBounds() ? "icons/adjustbounds.png" : "icons/noadjustbounds.png")))); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/ScalesFrame.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/ScalesFrame.java new file mode 100644 index 0000000..e84b4b6 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/frame/ScalesFrame.java @@ -0,0 +1,393 @@ +package org.xbib.graphics.graph.jmathplot.frame; + +import org.xbib.graphics.graph.jmathplot.io.StringPrintable; +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; +import org.xbib.graphics.graph.jmathplot.Base; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.WindowEvent; +import java.awt.event.WindowFocusListener; +import java.awt.event.WindowListener; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JTextField; + +/** + * BSD License + * + * @author Yann RICHET + * Changed on 6/13/2014 by Jerry Dietrich + * Contact info ballooninternet@cox.net + */ +public class ScalesFrame extends JFrame implements WindowFocusListener, WindowListener { + + /** + * Comment for serialVersionUID + */ + private static final long serialVersionUID = 1L; + private PlotCanvas plotCanvas; + private JPanel scalespanel; + private ScalePanel[] scalepanels; + + public ScalesFrame(PlotCanvas p) { + super("scales settings"); + plotCanvas = p; + setPanel(); + setContentPane(scalespanel); + + setResizable(false); + //setVisible(true); + + addWindowFocusListener(this); + addWindowListener(this); + } + + boolean editable = true; + + public void setEditable(boolean e) { + editable = e; + } + + @Override + public void setVisible(boolean b) { + if (b) { + setPanel(); + } + super.setVisible(b); + } + + private void setPanel() { + scalespanel = new JPanel(); + if (plotCanvas.base == null) { + return; + } + + int nbAxes = plotCanvas.base.dimension; + + this.setSize(nbAxes * 300, 200); + + scalespanel.setLayout(new GridLayout(1, nbAxes)); + + scalepanels = new ScalePanel[nbAxes]; + for (int i = 0; i < nbAxes; i++) { + scalepanels[i] = new ScalePanel(plotCanvas, i); + scalespanel.add(scalepanels[i]); + scalepanels[i].max_field.setEditable(editable); + scalepanels[i].min_field.setEditable(editable); + scalepanels[i].bounds_auto.setEnabled(editable); + scalepanels[i].log_check.setEnabled(editable); + scalepanels[i].linear_check.setEnabled(editable); + scalepanels[i].title_field.setEnabled(editable); + } + setContentPane(scalespanel); + } + + /* + * private void buildConstraints(GridBagConstraints gbc, int gx, int gy, int + * gw, int gh, int wx, int wy) { gbc.gridx = gx; gbc.gridy = gy; + * gbc.gridwidth = gw; gbc.gridheight = gh; gbc.weightx = wx; gbc.weighty = + * wy; } + */ + public void setDefaultCloseOperation(int operation) { + for (int i = 0; i < scalepanels.length; i++) { + scalepanels[i].updateBoundsFields(); + } + super.setDefaultCloseOperation(operation); + } + + public void windowGainedFocus(WindowEvent e) { + for (int i = 0; i < scalepanels.length; i++) { + scalepanels[i].update(); + } + } + + public void windowLostFocus(WindowEvent e) { + } + + public void windowOpened(WindowEvent e) { + } + + public void windowClosing(WindowEvent e) { + } + + public void windowClosed(WindowEvent e) { + } + + public void windowIconified(WindowEvent e) { + } + + public void windowDeiconified(WindowEvent e) { + } + + public void windowActivated(WindowEvent e) { + for (int i = 0; i < scalepanels.length; i++) { + scalepanels[i].update(); + } + } + + public void windowDeactivated(WindowEvent e) { + } + + public class ScalePanel extends JPanel implements StringPrintable { + + private static final long serialVersionUID = 1L; + // private PlotCanvas plotCanvas; + private final int numAxe; + private String title; + private String scaleType; + private double min; + private double max; + private final JLabel title_label = new JLabel("Title"); + private final JTextField title_field = new JTextField(); + private final JLabel scale_label = new JLabel("Scale"); + private final ButtonGroup scale_group = new ButtonGroup(); + private final JRadioButton linear_check = new JRadioButton("Linear"); + private final JRadioButton log_check = new JRadioButton("Logarithmic"); + // private JCheckBox gridVisible = new JCheckBox("Grid visible"); + private final JLabel bounds_label = new JLabel("Bounds"); + private final JLabel min_label = new JLabel("Min"); + private final JLabel max_label = new JLabel("Max"); + private final JTextField min_field = new JTextField(); + private final JTextField max_field = new JTextField(); + private final JButton bounds_auto = new JButton("Automatic"); + + public ScalePanel(PlotCanvas p, int i) { + numAxe = i; + plotCanvas = p; + + update(); + + addComponents(); + setListeners(); + } + + public void update() { + title = plotCanvas.getGrid().getAxis(numAxe).getLegend();// getLegend(numAxe); + title_field.setText(title); + + scaleType = plotCanvas.getAxisScales()[numAxe]; + log_check.setSelected(scaleType.equalsIgnoreCase(Base.LOGARITHM)); + linear_check.setSelected(scaleType.equalsIgnoreCase(Base.LINEAR)); + if (scaleType.equalsIgnoreCase(Base.STRINGS)) { + log_check.setEnabled(false); + linear_check.setEnabled(false); + } + + updateBoundsFields(); + } + + private void addComponents() { + this.setSize(300, 200); + + scale_group.add(linear_check); + scale_group.add(log_check); + + GridBagLayout gbl = new GridBagLayout(); + GridBagConstraints c = new GridBagConstraints(); + this.setLayout(gbl); + + buildConstraints(c, 0, 0, 1, 1, 40, 20); + c.fill = GridBagConstraints.CENTER; + c.anchor = GridBagConstraints.CENTER; + gbl.setConstraints(title_label, c); + this.add(title_label); + + buildConstraints(c, 1, 0, 2, 1, 60, 20); + c.fill = GridBagConstraints.HORIZONTAL; + gbl.setConstraints(title_field, c); + this.add(title_field); + + buildConstraints(c, 0, 1, 1, 1, 40, 20); + c.fill = GridBagConstraints.CENTER; + c.anchor = GridBagConstraints.CENTER; + gbl.setConstraints(scale_label, c); + this.add(scale_label); + + buildConstraints(c, 1, 1, 2, 1, 60, 20); + c.fill = GridBagConstraints.HORIZONTAL; + gbl.setConstraints(linear_check, c); + this.add(linear_check); + + buildConstraints(c, 1, 2, 2, 1, 60, 20); + c.fill = GridBagConstraints.HORIZONTAL; + gbl.setConstraints(log_check, c); + this.add(log_check); + + buildConstraints(c, 0, 3, 1, 1, 40, 20); + c.fill = GridBagConstraints.CENTER; + c.anchor = GridBagConstraints.CENTER; + gbl.setConstraints(bounds_label, c); + this.add(bounds_label); + + buildConstraints(c, 1, 3, 1, 1, 20, 20); + c.fill = GridBagConstraints.CENTER; + c.fill = GridBagConstraints.CENTER; + gbl.setConstraints(min_label, c); + this.add(min_label); + + buildConstraints(c, 2, 3, 1, 1, 50, 20); + c.fill = GridBagConstraints.HORIZONTAL; + gbl.setConstraints(min_field, c); + this.add(min_field); + + buildConstraints(c, 1, 4, 1, 1, 20, 20); + c.fill = GridBagConstraints.CENTER; + c.fill = GridBagConstraints.CENTER; + gbl.setConstraints(max_label, c); + this.add(max_label); + + buildConstraints(c, 2, 4, 1, 1, 50, 20); + c.fill = GridBagConstraints.HORIZONTAL; + gbl.setConstraints(max_field, c); + this.add(max_field); + + buildConstraints(c, 1, 5, 2, 1, 60, 20); + c.fill = GridBagConstraints.CENTER; + gbl.setConstraints(bounds_auto, c); + this.add(bounds_auto); + + // this.add(gridVisible); + } + + private void buildConstraints(GridBagConstraints gbc, int gx, int gy, int gw, int gh, int wx, int wy) { + gbc.gridx = gx; + gbc.gridy = gy; + gbc.gridwidth = gw; + gbc.gridheight = gh; + gbc.weightx = wx; + gbc.weighty = wy; + } + + private void setListeners() { + title_field.addKeyListener(new KeyListener() { + + public void keyReleased(KeyEvent e) { + setTitle(); + } + + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent e) { + } + }); + + log_check.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + setScale(); + updateBoundsFields(); + } + }); + linear_check.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + setScale(); + updateBoundsFields(); + } + }); + /* + * gridVisible.addChangeListener(new ChangeListener() { public void + * stateChanged(ChangeEvent e) { + * plotPanel.getGrid().getAxe(numAxe).setVisible( + * gridVisible.isSelected()); plotPanel.repaint(); } }); + */ + + min_field.addKeyListener(new KeyListener() { + + public void keyReleased(KeyEvent e) { + setBounds(); + } + + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent e) { + } + }); + + max_field.addKeyListener(new KeyListener() { + + public void keyReleased(KeyEvent e) { + setBounds(); + } + + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent e) { + } + }); + + bounds_auto.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + setBoundsAuto(); + } + }); + + } + + public String getText() { + return "title = " + title + "\nscaleType = " + scaleType + "\nmin = " + min + "\nmax = " + max; + } + + private void setTitle() { + // System.out.println("title setting n�" + numAxe + " : " + + // title_field.getText()); + plotCanvas.setAxisLabel(numAxe, title_field.getText()); + } + + private void setBounds() { + // System.out.println("bounds setting n�" + numAxe + " : " + + // min_field.getText() + " - " + max_field.getText()); + try { + double min1 = Double.parseDouble(min_field.getText()); + double max1 = Double.parseDouble(max_field.getText()); + plotCanvas.setFixedBounds(numAxe, min1, max1); + } catch (IllegalArgumentException iae) { + // JOptionPane.showConfirmDialog(null, iae.getMessage(), + // "Error", JOptionPane.DEFAULT_OPTION, + // JOptionPane.ERROR_MESSAGE); + // updateBoundsFields(); + } + } + + private void setScale() { + // System.out.println("scale setting n�" + numAxe + " : " + ( + // (log_check.isSelected()) ? ("LOG") : ("LINEAR"))); + try { + plotCanvas.setAxiScale(numAxe, (log_check.isSelected()) ? (Base.LOGARITHM) : (Base.LINEAR)); + } catch (IllegalArgumentException iae) { + JOptionPane.showConfirmDialog(null, iae.getMessage(), "Error", JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE); + updateBoundsFields(); + } + } + + private void setBoundsAuto() { + plotCanvas.setAutoBounds(numAxe); + updateBoundsFields(); + // System.out.println("auto-Bounds setting n�"+numAxe+" : + // "+plotPanel.getBase().getMinBounds()[numAxe]+" - + // "+plotPanel.getBase().getMaxBounds()[numAxe]); + } + + private void updateBoundsFields() { + min = plotCanvas.base.getMinBounds()[numAxe]; + max = plotCanvas.base.getMaxBounds()[numAxe]; + min_field.setText("" + min); + max_field.setText("" + max); + // log_check.setSelected(plotCanvas.base.getAxeScale(numAxe)==Base.LOG); + } + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/ClipBoardPrintable.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/ClipBoardPrintable.java new file mode 100755 index 0000000..3b57e49 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/ClipBoardPrintable.java @@ -0,0 +1,5 @@ +package org.xbib.graphics.graph.jmathplot.io; + +public interface ClipBoardPrintable { + void toClipBoard(); +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/FilePrintable.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/FilePrintable.java new file mode 100755 index 0000000..46614a2 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/FilePrintable.java @@ -0,0 +1,7 @@ +package org.xbib.graphics.graph.jmathplot.io; + +import java.io.File; + +public interface FilePrintable { + void toASCIIFile(File f); +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/StringPrintable.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/StringPrintable.java new file mode 100755 index 0000000..4679748 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/StringPrintable.java @@ -0,0 +1,5 @@ +package org.xbib.graphics.graph.jmathplot.io; + +public interface StringPrintable { + String getText(); +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/files/ASCIIFile.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/files/ASCIIFile.java new file mode 100755 index 0000000..a4e504d --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/files/ASCIIFile.java @@ -0,0 +1,276 @@ +package org.xbib.graphics.graph.jmathplot.io.files; + +import org.xbib.graphics.graph.jmathplot.io.parser.ArrayString; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Calendar; +import java.util.Vector; + +/** + * BSD License + * + * @author Yann RICHET + */ +public class ASCIIFile extends DataFile { + + public ASCIIFile(File f) { + super(f); + } + + public static String read(File f) { + ASCIIFile af = new ASCIIFile(f); + return af.read(); + } + + public static String[] readLines(File f) { + ASCIIFile af = new ASCIIFile(f); + return af.readLines(); + } + + public static String readLine(File f, int i) { + ASCIIFile af = new ASCIIFile(f); + return af.readLine(i); + } + + public static double[] readDouble1DArray(File f) { + return ArrayString.readString1DDouble(ASCIIFile.read(f)); + } + + public static double[][] readDoubleArray(File f) { + return ArrayString.readStringDouble(ASCIIFile.read(f)); + } + + public static int[] readInt1DArray(File f) { + return ArrayString.readString1DInt(ASCIIFile.read(f)); + } + + public static int[][] readIntArray(File f) { + return ArrayString.readStringInt(ASCIIFile.read(f)); + } + + public static void write(File f, String t) { + ASCIIFile af = new ASCIIFile(f); + af.write(t, false); + } + + public static void writeDoubleArray(File f, double[] array) { + write(f, ArrayString.printDoubleArray(array)); + } + + public static void writeDoubleArray(File f, double[][] array) { + write(f, ArrayString.printDoubleArray(array)); + } + + public static void writeIntArray(File f, int[] array) { + write(f, ArrayString.printIntArray(array)); + } + + public static void writeIntArray(File f, int[][] array) { + write(f, ArrayString.printIntArray(array)); + } + + public static void append(File f, String t) { + ASCIIFile af = new ASCIIFile(f); + af.write(t, true); + } + + /** + * Read an ASCII File + * + * @return String + */ + public String read() { + StringBuffer text = new StringBuffer((int) file.length()); + BufferedReader b = null; + try { + FileReader fr = new FileReader(file); + b = new BufferedReader(fr); + boolean eof = false; + String line; + String ret = "\n"; + while (!eof) { + line = b.readLine(); + if (line == null) { + eof = true; + } else { + text.append(line); + text.append(ret); + } + } + } catch (IOException e) { + throw new IllegalArgumentException("File " + file.getName() + + " is unreadable : " + e.toString()); + } finally { + try { + b.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + return text.toString(); + } + + /** + * Read lines of an ASCII File + * + * @return an Array of String + */ + public String[] readLines() { + Vector linesVector = new Vector(); + BufferedReader b = null; + try { + FileReader fr = new FileReader(file); + b = new BufferedReader(fr); + boolean eof = false; + while (!eof) { + String line = b.readLine(); + if (line == null) { + eof = true; + } else { + linesVector.add(line); + } + } + + } catch (IOException e) { + throw new IllegalArgumentException("File " + file.getName() + + " is unreadable : " + e.toString()); + } finally { + try { + b.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + + String[] lines = new String[linesVector.size()]; + for (int i = 0; i < lines.length; i++) { + lines[i] = linesVector.get(i); + } + return lines; + } + + /** + * Read only one line in an ASCII File + * + * @param i line index + * @return String + */ + public String readLine(int i) { + String line = ""; + BufferedReader b = null; + try { + FileReader fr = new FileReader(file); + b = new BufferedReader(fr); + boolean eof = false; + for (int j = 0; j < i; j++) { + if (eof) { + throw new IllegalArgumentException("Line " + i + + " is not found in the file " + file.getName() + + "."); + } + line = b.readLine(); + if (line == null) { + eof = true; + } + } + line = b.readLine(); + + } catch (IOException e) { + throw new IllegalArgumentException("File " + file.getName() + + " is unreadable : " + e.toString()); + } finally { + try { + b.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + return line; + } + + /** + * Write a text in an ASCII File + * + * @param text String + * @param append boolean + */ + public void write(String text, boolean append) { + if (file.exists() && !append) { + System.out.println("Warning : the file " + file.getName() + + " already exists !"); + } + FileWriter fw = null; + BufferedWriter bw = null; + try { + fw = new FileWriter(file, append); + bw = new BufferedWriter(fw); + bw.write(text); + + } catch (IOException e) { + throw new IllegalArgumentException("File " + file.getName() + + " is unwritable : " + e.toString()); + } finally { + try { + bw.close(); + fw.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + + public static void concatenate(File f1, File f2) { + + FileInputStream fis = null; + BufferedInputStream bis = null; + FileOutputStream fos = null; + BufferedOutputStream bos = null; + try { + fis = new FileInputStream(f2); + bis = new BufferedInputStream(fis); + fos = new FileOutputStream(f1, true); + bos = new BufferedOutputStream(fos); + int c; + while ((c = bis.read()) != -1) { + bos.write(c); + } + } catch (IOException e) { + System.err.println("Concatenate: " + e); + } finally { + try { + bis.close(); + fis.close(); + bos.close(); + fos.close(); + + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + + public static void main(String[] args) { + File f = new File("read.txt"); + System.out.println(ASCIIFile.read(f)); + + String[] lines = ASCIIFile.readLines(f); + for (int i = 0; i < lines.length; i++) { + System.out.println("line " + i + " : " + lines[i]); + } + + System.out.println(ASCIIFile.readLine(f, 0)); + System.out.println(ASCIIFile.readLine(f, lines.length - 1)); + + ASCIIFile.append(new File("write.txt"), Calendar.getInstance().getTime().toString()); + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/files/BinaryFile.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/files/BinaryFile.java new file mode 100755 index 0000000..c305735 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/files/BinaryFile.java @@ -0,0 +1,402 @@ +package org.xbib.graphics.graph.jmathplot.io.files; + +import org.xbib.graphics.graph.jmathplot.io.stream.BinaryInputStream; +import org.xbib.graphics.graph.jmathplot.io.stream.BinaryOutputStream; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; + +/** + * BSD License + * + * @author Yann RICHET + */ +public class BinaryFile { + + public static String LITTLE_ENDIAN = "LITTLE_ENDIAN"; + public static String BIG_ENDIAN = "BIG_ENDIAN"; + private final boolean bigEndian; + private final File file; + + public BinaryFile(File f, String endian) { + file = f; + bigEndian = isBigEndian(endian); + } + + private boolean isBigEndian(String endian) { + boolean be; + if (endian.equals(LITTLE_ENDIAN)) { + be = false; + } else if (endian.equals(BIG_ENDIAN)) { + be = true; + } else { + throw new IllegalArgumentException( + "The Endian type : " + + endian + + "is unknown. You must specify LITTLE_ENDIAN or BIG_ENDIAN."); + } + return be; + } + + public static double[] readDoubleArray(File f, String endian) { + BinaryFile bf = new BinaryFile(f, endian); + return bf.readDoubleArray(); + } + + public static float[] readFloatArray(File f, String endian) { + BinaryFile bf = new BinaryFile(f, endian); + return bf.readFloatArray(); + } + + public static int[] readIntArray(File f, String endian) { + BinaryFile bf = new BinaryFile(f, endian); + return bf.readIntArray(); + } + + public static void writeDoubleArray(File f, double[] array, String endian) { + BinaryFile bf = new BinaryFile(f, endian); + bf.writeDoubleArray(array, false); + } + + public static void appendDoubleArray(File f, double[] array, String endian) { + BinaryFile bf = new BinaryFile(f, endian); + bf.writeDoubleArray(array, true); + } + + public static void writeFloatArray(File f, float[] array, String endian) { + BinaryFile bf = new BinaryFile(f, endian); + bf.writeFloatArray(array, false); + } + + public static void appendFloatArray(File f, float[] array, String endian) { + BinaryFile bf = new BinaryFile(f, endian); + bf.writeFloatArray(array, true); + } + + public static void writeIntArray(File f, int[] array, String endian) { + BinaryFile bf = new BinaryFile(f, endian); + bf.writeIntArray(array, false); + } + + public static void appendIntArray(File f, int[] array, String endian) { + BinaryFile bf = new BinaryFile(f, endian); + bf.writeIntArray(array, true); + } + + /** + * Read a binary File + * + * @return int[] + */ + public int[] readIntArray() { + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + } catch (FileNotFoundException ex) { + throw new IllegalArgumentException(ex.toString()); + } + + BufferedInputStream bis = new BufferedInputStream(fis); + + BinaryInputStream bs = new BinaryInputStream(bis, bigEndian); + + return bs.readIntArray(); + } + + /** + * Read a binary File + * + * @return float[] + */ + public float[] readFloatArray() { + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + } catch (FileNotFoundException ex) { + throw new IllegalArgumentException(ex.toString()); + } + + BufferedInputStream bis = new BufferedInputStream(fis); + + BinaryInputStream bs = new BinaryInputStream(bis, bigEndian); + + return bs.readFloatArray(); + } + + /** + * Read a binary File + * + * @return double[] + */ + public double[] readDoubleArray() { + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + } catch (FileNotFoundException ex) { + throw new IllegalArgumentException(ex.toString()); + } + + BufferedInputStream bis = new BufferedInputStream(fis); + + BinaryInputStream bs = new BinaryInputStream(bis, bigEndian); + + return bs.readDoubleArray(); + } + + /** + * Read a binary File + * + * @return byte[] + */ + public byte[] readByteArray() { + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + } catch (FileNotFoundException ex) { + throw new IllegalArgumentException(ex.toString()); + } + + BufferedInputStream bis = new BufferedInputStream(fis); + + BinaryInputStream bs = new BinaryInputStream(bis, bigEndian); + + return bs.readByteArray(); + } + + /** + * Write an int array in a binary File + * + * @param array int[] + * @param append boolean + */ + public void writeIntArray(int[] array, boolean append) { + if (file.exists()) { + System.out.println("Warning : the file " + file.getName() + + " already exists !"); + } + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file, append); + } catch (FileNotFoundException ex) { + throw new IllegalArgumentException(ex.toString()); + } + + BufferedOutputStream bos = new BufferedOutputStream(fos); + + BinaryOutputStream bs = new BinaryOutputStream(bos, bigEndian); + + bs.writeIntArray(array, append); + } + + /** + * Write a float array in a binary File + * + * @param array float[] + * @param append boolean + */ + public void writeFloatArray(float[] array, boolean append) { + if (file.exists()) { + System.out.println("Warning : the file " + file.getName() + + " already exists !"); + } + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file, append); + } catch (FileNotFoundException ex) { + throw new IllegalArgumentException(ex.toString()); + } + + BufferedOutputStream bos = new BufferedOutputStream(fos); + + BinaryOutputStream bs = new BinaryOutputStream(bos, bigEndian); + + bs.writeFloatArray(array, append); + + //TODO add try/catch/finally !!! + } + + /** + * Write a double array in a binary File + * + * @param array float[] + * @param append boolean + */ + public void writeDoubleArray(double[] array, boolean append) { + if (file.exists()) { + System.out.println("Warning : the file " + file.getName() + + " already exists !"); + } + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file, append); + } catch (FileNotFoundException ex) { + throw new IllegalArgumentException(ex.toString()); + } + + BufferedOutputStream bos = new BufferedOutputStream(fos); + + BinaryOutputStream bs = new BinaryOutputStream(bos, bigEndian); + + bs.writeDoubleArray(array, append); + } + + /** + * Write a text in a binary File + * + * @param bytes byte[] + * @param append boolean + */ + public void writeByteArray(byte[] bytes, boolean append) { + if (file.exists()) { + System.out.println("Warning : the file " + file.getName() + + " already exists !"); + } + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file, append); + } catch (FileNotFoundException ex) { + throw new IllegalArgumentException(ex.toString()); + } + + BufferedOutputStream bos = new BufferedOutputStream(fos); + + BinaryOutputStream bs = new BinaryOutputStream(bos, bigEndian); + + bs.writeByteArray(bytes, append); + } + + /** + * Read a binary File + * + * @return int + */ + public int readInt() { + return readIntArray()[0]; + } + + /** + * Read a binary File + * + * @return float + */ + public float readFloat() { + return readFloatArray()[0]; + } + + /** + * Read a binary File + * + * @return double + */ + public double readDouble() { + return readDoubleArray()[0]; + } + + /** + * Read a binary File + * + * @return byte + */ + public byte readByte() { + return readByteArray()[0]; + } + + /** + * Write an int in a binary File + * + * @param i int + * @param append boolean + */ + public void writeInt(int i, boolean append) { + writeIntArray(new int[]{i}, append); + } + + /** + * Write a float in a binary File + * + * @param f float + * @param append boolean + */ + public void writeFloat(float f, boolean append) { + writeFloatArray(new float[]{f}, append); + } + + /** + * Write a double in a binary File + * + * @param d double + * @param append boolean + */ + public void writeDouble(double d, boolean append) { + writeDoubleArray(new double[]{d}, append); + } + + /** + * Write a text in a binary File + * + * @param b byte + * @param append boolean + */ + public void writeByte(byte b, boolean append) { + writeByteArray(new byte[]{b}, append); + } + + public static void main(String[] args) { + if (args[0].equals("-readarray")) { + String man = "Usage: BinaryFile -readarray file [option]\n[options] are:\n -endian \n -data "; + + File file = null; + String data = "double"; + String endian = BIG_ENDIAN; + + for (int i = 1; i < args.length; i++) { + if (args[i].equals("-endian")) { + if (args[i + 1].equals("little")) { + endian = LITTLE_ENDIAN; + } + i++; + } else if (args[i].equals("-data")) { + data = args[i + 1]; + i++; + } else { + file = new File(args[i]); + if (!file.exists()) { + System.out.println("File " + file + + " doesn't exists.\n" + man); + } + i++; + } + } + + if (data.equals("double")) { + double[] d = readDoubleArray(file, endian); + for (int j = 0; j < d.length; j++) { + System.out.println(d[j] + ""); + } + } else if (data.equals("float")) { + float[] d = readFloatArray(file, endian); + for (int j = 0; j < d.length; j++) { + System.out.println(d[j] + ""); + } + } else if (data.equals("int")) { + int[] d = readIntArray(file, endian); + for (int j = 0; j < d.length; j++) { + System.out.println(d[j] + ""); + } + } else { + System.out.println(man); + } + + } else { + System.out.println("Option not implemented."); + } + + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/files/DataFile.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/files/DataFile.java new file mode 100755 index 0000000..a9cbac6 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/files/DataFile.java @@ -0,0 +1,34 @@ +package org.xbib.graphics.graph.jmathplot.io.files; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * BSD License + * + * @author Yann RICHET + */ + +public abstract class DataFile { + + protected File file; + + public DataFile(File f) { + file = f; + } + + public static void copyFile(File in, File out) throws IOException { + FileInputStream fis = new FileInputStream(in); + FileOutputStream fos = new FileOutputStream(out); + byte[] buf = new byte[1024]; + int i = 0; + while ((i = fis.read(buf)) != -1) { + fos.write(buf, 0, i); + } + fis.close(); + fos.close(); + } + +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/littleendian/LEDataInputStream.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/littleendian/LEDataInputStream.java new file mode 100755 index 0000000..0807304 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/littleendian/LEDataInputStream.java @@ -0,0 +1,111 @@ +package org.xbib.graphics.graph.jmathplot.io.littleendian; + +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * This class was designed on the base of Roedy Green LEDataInputStream + *

+ * Thanks to Roedy Green, Canadian Mind Products mailto:roedy@mindprod.com + * http://mindprod.com + * + * @author Yann RICHET + */ + +public class LEDataInputStream implements DataInput { + + protected DataInputStream d; + + protected InputStream in; + + byte[] w; + + public LEDataInputStream(InputStream in) { + this.in = in; + this.d = new DataInputStream(in); + w = new byte[8]; + } + + public short readShort() throws IOException { + d.readFully(w, 0, 2); + return (short) ((w[1] & 0xff) << 8 | (w[0] & 0xff)); + } + + public int readUnsignedShort() throws IOException { + d.readFully(w, 0, 2); + return ((w[1] & 0xff) << 8 | (w[0] & 0xff)); + } + + public char readChar() throws IOException { + d.readFully(w, 0, 2); + return (char) ((w[1] & 0xff) << 8 | (w[0] & 0xff)); + } + + public int readInt() throws IOException { + d.readFully(w, 0, 4); + return (w[3]) << 24 | (w[2] & 0xff) << 16 | (w[1] & 0xff) << 8 + | (w[0] & 0xff); + } + + public long readLong() throws IOException { + d.readFully(w, 0, 8); + return (long) (w[7]) << 56 | (long) (w[6] & 0xff) << 48 + | (long) (w[5] & 0xff) << 40 | (long) (w[4] & 0xff) << 32 + | (long) (w[3] & 0xff) << 24 | (long) (w[2] & 0xff) << 16 + | (long) (w[1] & 0xff) << 8 | (long) (w[0] & 0xff); + } + + public float readFloat() throws IOException { + return Float.intBitsToFloat(readInt()); + } + + public double readDouble() throws IOException { + return Double.longBitsToDouble(readLong()); + } + + public int read(byte[] b, int off, int len) throws IOException { + return in.read(b, off, len); + } + + public void readFully(byte[] b) throws IOException { + d.readFully(b, 0, b.length); + } + + public void readFully(byte[] b, int off, int len) throws IOException { + d.readFully(b, off, len); + } + + public int skipBytes(int n) throws IOException { + return d.skipBytes(n); + } + + public boolean readBoolean() throws IOException { + return d.readBoolean(); + } + + public byte readByte() throws IOException { + return d.readByte(); + } + + public int readUnsignedByte() throws IOException { + return d.readUnsignedByte(); + } + + public String readLine() throws IOException { + return null;// d.readLine(); + } + + public String readUTF() throws IOException { + return d.readUTF(); + } + + public static String readUTF(DataInput in) throws IOException { + return DataInputStream.readUTF(in); + } + + public void close() throws IOException { + d.close(); + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/littleendian/LEDataOutputStream.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/littleendian/LEDataOutputStream.java new file mode 100755 index 0000000..a6ef37d --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/littleendian/LEDataOutputStream.java @@ -0,0 +1,116 @@ +package org.xbib.graphics.graph.jmathplot.io.littleendian; + +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * This class was designed on the base of Roedy Green LEDataInputStream + *

+ * Thanks to Roedy Green, Canadian Mind Products mailto:roedy@mindprod.com + * http://mindprod.com + * + * @author Yann RICHET + */ + +public class LEDataOutputStream implements DataOutput { + + protected DataOutputStream d; + + byte[] w; + + public LEDataOutputStream(OutputStream out) { + this.d = new DataOutputStream(out); + w = new byte[8]; + } + + public final void writeShort(int v) throws IOException { + w[0] = (byte) v; + w[1] = (byte) (v >> 8); + d.write(w, 0, 2); + } + + public final void writeChar(int v) throws IOException { + w[0] = (byte) v; + w[1] = (byte) (v >> 8); + d.write(w, 0, 2); + } + + public final void writeInt(int v) throws IOException { + w[0] = (byte) v; + w[1] = (byte) (v >> 8); + w[2] = (byte) (v >> 16); + w[3] = (byte) (v >> 24); + d.write(w, 0, 4); + } + + public final void writeLong(long v) throws IOException { + w[0] = (byte) v; + w[1] = (byte) (v >> 8); + w[2] = (byte) (v >> 16); + w[3] = (byte) (v >> 24); + w[4] = (byte) (v >> 32); + w[5] = (byte) (v >> 40); + w[6] = (byte) (v >> 48); + w[7] = (byte) (v >> 56); + d.write(w, 0, 8); + } + + public final void writeFloat(float v) throws IOException { + writeInt(Float.floatToIntBits(v)); + } + + public final void writeDouble(double v) throws IOException { + writeLong(Double.doubleToLongBits(v)); + } + + public final void writeChars(String s) throws IOException { + int len = s.length(); + for (int i = 0; i < len; i++) { + writeChar(s.charAt(i)); + } + } + + public final synchronized void write(int b) throws IOException { + d.write(b); + } + + public final synchronized void write(byte[] b, int off, int len) + throws IOException { + d.write(b, off, len); + } + + public void flush() throws IOException { + d.flush(); + } + + public final void writeBoolean(boolean v) throws IOException { + d.writeBoolean(v); + } + + public final void writeByte(int v) throws IOException { + d.writeByte(v); + } + + public final void writeBytes(String s) throws IOException { + d.writeBytes(s); + } + + public final void writeUTF(String str) throws IOException { + d.writeUTF(str); + } + + public final int size() { + return d.size(); + } + + public final void write(byte[] b) throws IOException { + d.write(b, 0, b.length); + } + + public final void close() throws IOException { + d.close(); + } + +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/littleendian/LERandomAccessFile.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/littleendian/LERandomAccessFile.java new file mode 100755 index 0000000..a820f7f --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/littleendian/LERandomAccessFile.java @@ -0,0 +1,278 @@ +package org.xbib.graphics.graph.jmathplot.io.littleendian; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.File; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.RandomAccessFile; + +/** + * This class was designed on the base of Roedy Green LEDataInputStream + *

+ * Thanks to Roedy Green, Canadian Mind Products mailto:roedy@mindprod.com + * http://mindprod.com + * + * @author Yann RICHET + */ + +public class LERandomAccessFile implements DataInput, DataOutput { + /** + * constructors + */ + public LERandomAccessFile(String f, String rw) throws IOException { + r = new RandomAccessFile(f, rw); + w = new byte[8]; + } + + public LERandomAccessFile(File f, String rw) throws IOException { + r = new RandomAccessFile(f, rw); + w = new byte[8]; + } + + // L I T T L E E N D I A N R E A D E R S + // Little endian methods for multi-byte numeric types. + // Big-endian do fine for single-byte types and strings. + + /** + * like RandomAcessFile.readShort except little endian. + */ + public final short readShort() throws IOException { + r.readFully(w, 0, 2); + return (short) ((w[1] & 0xff) << 8 | (w[0] & 0xff)); + } + + /** + * like RandomAcessFile.readUnsignedShort except little endian. Note, + * returns int even though it reads a short. + */ + public final int readUnsignedShort() throws IOException { + r.readFully(w, 0, 2); + return ((w[1] & 0xff) << 8 | (w[0] & 0xff)); + } + + /** + * like RandomAcessFile.readChar except little endian. + */ + public final char readChar() throws IOException { + r.readFully(w, 0, 2); + return (char) ((w[1] & 0xff) << 8 | (w[0] & 0xff)); + } + + /** + * like RandomAcessFile.readInt except little endian. + */ + public final int readInt() throws IOException { + r.readFully(w, 0, 4); + return (w[3]) << 24 | (w[2] & 0xff) << 16 | (w[1] & 0xff) << 8 + | (w[0] & 0xff); + } + + /** + * like RandomAcessFile.readLong except little endian. + */ + public final long readLong() throws IOException { + r.readFully(w, 0, 8); + return (long) (w[7]) << 56 | /* + * long cast necessary or shift done + * modulo 32 + */ + (long) (w[6] & 0xff) << 48 | (long) (w[5] & 0xff) << 40 + | (long) (w[4] & 0xff) << 32 | (long) (w[3] & 0xff) << 24 + | (long) (w[2] & 0xff) << 16 | (long) (w[1] & 0xff) << 8 + | (long) (w[0] & 0xff); + } + + /** + * like RandomAcessFile.readFloat except little endian. + */ + public final float readFloat() throws IOException { + return Float.intBitsToFloat(readInt()); + } + + /** + * like RandomAcessFile.readDouble except little endian. + */ + public final double readDouble() throws IOException { + return Double.longBitsToDouble(readLong()); + } + + // L I T T L E E N D I A N W R I T E R S + // Little endian methods for multi-byte numeric types. + // Big-endian do fine for single-byte types and strings. + + /** + * like RandomAcessFile.writeShort. also acts as a writeUnsignedShort + */ + public final void writeShort(int v) throws IOException { + w[0] = (byte) v; + w[1] = (byte) (v >> 8); + r.write(w, 0, 2); + } + + /** + * like RandomAcessFile.writeChar. Note the parm is an int even though this + * as a writeChar + */ + public final void writeChar(int v) throws IOException { + // same code as writeShort + w[0] = (byte) v; + w[1] = (byte) (v >> 8); + r.write(w, 0, 2); + } + + /** + * like RandomAcessFile.writeInt. + */ + public final void writeInt(int v) throws IOException { + w[0] = (byte) v; + w[1] = (byte) (v >> 8); + w[2] = (byte) (v >> 16); + w[3] = (byte) (v >> 24); + r.write(w, 0, 4); + } + + /** + * like RandomAcessFile.writeLong. + */ + public final void writeLong(long v) throws IOException { + w[0] = (byte) v; + w[1] = (byte) (v >> 8); + w[2] = (byte) (v >> 16); + w[3] = (byte) (v >> 24); + w[4] = (byte) (v >> 32); + w[5] = (byte) (v >> 40); + w[6] = (byte) (v >> 48); + w[7] = (byte) (v >> 56); + r.write(w, 0, 8); + } + + /** + * like RandomAcessFile.writeFloat. + */ + public final void writeFloat(float v) throws IOException { + writeInt(Float.floatToIntBits(v)); + } + + /** + * like RandomAcessFile.writeDouble. + */ + public final void writeDouble(double v) throws IOException { + writeLong(Double.doubleToLongBits(v)); + } + + /** + * like RandomAcessFile.writeChars, has to flip each char. + */ + public final void writeChars(String s) throws IOException { + int len = s.length(); + for (int i = 0; i < len; i++) { + writeChar(s.charAt(i)); + } + } // end writeChars + + // p u r e l y w r a p p e r m e t h o d s + + public final FileDescriptor getFD() throws IOException { + return r.getFD(); + } + + public final long getFilePointer() throws IOException { + return r.getFilePointer(); + } + + public final long length() throws IOException { + return r.length(); + } + + public final int read(byte[] b, int off, int len) throws IOException { + return r.read(b, off, len); + } + + public final int read(byte[] b) throws IOException { + return r.read(b); + } + + public final int read() throws IOException { + return r.read(); + } + + public final void readFully(byte[] b) throws IOException { + r.readFully(b, 0, b.length); + } + + public final void readFully(byte[] b, int off, int len) throws IOException { + r.readFully(b, off, len); + } + + public final int skipBytes(int n) throws IOException { + return r.skipBytes(n); + } + + /* OK, reads only only 1 byte */ + public final boolean readBoolean() throws IOException { + return r.readBoolean(); + } + + public final byte readByte() throws IOException { + return r.readByte(); + } + + // note: returns an int, even though says Byte. + public final int readUnsignedByte() throws IOException { + return r.readUnsignedByte(); + } + + public final String readLine() throws IOException { + return r.readLine(); + } + + public final String readUTF() throws IOException { + return r.readUTF(); + } + + public final void seek(long pos) throws IOException { + r.seek(pos); + } + + /* Only writes one byte even though says int */ + public final synchronized void write(int b) throws IOException { + r.write(b); + } + + public final synchronized void write(byte[] b, int off, int len) + throws IOException { + r.write(b, off, len); + } + + public final void writeBoolean(boolean v) throws IOException { + r.writeBoolean(v); + } + + public final void writeByte(int v) throws IOException { + r.writeByte(v); + } + + public final void writeBytes(String s) throws IOException { + r.writeBytes(s); + } + + public final void writeUTF(String str) throws IOException { + r.writeUTF(str); + } + + public final void write(byte[] b) throws IOException { + r.write(b, 0, b.length); + } + + public final void close() throws IOException { + r.close(); + } + + // i n s t a n c e v a r i a b l e s + + protected RandomAccessFile r; + + byte[] w; // work array for buffering input/output + +} // end class LERandomAccessFile diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/parser/ArrayString.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/parser/ArrayString.java new file mode 100755 index 0000000..bee6670 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/parser/ArrayString.java @@ -0,0 +1,165 @@ +package org.xbib.graphics.graph.jmathplot.io.parser; + +/** + * BSD License + * + * @author Yann RICHET + */ +public class ArrayString { + + //private static int decimalSize = 10; + public static String defaultColumnDelimiter = " "; + public static String defaultRowDelimiter = "\n"; + public static String NotFoundDelimiter = "ZYXWV"; + + public static String printDoubleArray(double[] m) { + return printDoubleArray(new double[][]{m}); + } + + public static String printDoubleArray(double[][] m) { + return printDoubleArray(m, defaultColumnDelimiter, defaultRowDelimiter); + } + + public static String printDoubleArray(double[][] m, String wordDelimiter, String sentenceDelimiter) { + + StringBuffer str = new StringBuffer(25 * m.length * m[0].length); + + // //can't use format because of infinty which become "?" strings... + // DecimalFormat format = new DecimalFormat(); + // format.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.US)); + // format.setMinimumIntegerDigits(1); + // format.setMaximumFractionDigits(decimalSize); + // format.setMinimumFractionDigits(decimalSize); + // format.setGroupingUsed(false); + + for (int i = 0; i < m.length; i++) { + for (int j = 0; j < m[i].length - 1; j++) { + // String s = format.format(m[i][j]); // format the number + str = str.append(m[i][j]); + str = str.append(wordDelimiter); + } + str = str.append(m[i][m[i].length - 1]); + + if (i < m.length - 1) { + str = str.append(sentenceDelimiter); + } + } + return str.toString(); + + } + + public static String printIntArray(int[] m) { + return printIntArray(new int[][]{m}); + } + + public static String printIntArray(int[][] m) { + return printIntArray(m, defaultColumnDelimiter, defaultRowDelimiter); + } + + public static String printIntArray(int[][] m, String wordDelimiter, String sentenceDelimiter) { + + StringBuffer str = new StringBuffer(25 * m.length * m[0].length); + + for (int i = 0; i < m.length; i++) { + for (int j = 0; j < m[i].length - 1; j++) { + str = str.append(m[i][j]); + str = str.append(wordDelimiter); + } + str = str.append(m[i][m[i].length - 1]); + + if (i < m.length - 1) { + str = str.append(sentenceDelimiter); + } + } + return str.toString(); + } + + public static double[] readString1DDouble(String s) { + return readString1DDouble(s, defaultColumnDelimiter); + } + + public static double[] readString1DDouble(String s, String columnDelimiter) { + double[][] d = readStringDouble(s, columnDelimiter, NotFoundDelimiter); + double[] d1D = null; + if (d.length > 1) { + d1D = new double[d.length]; + for (int i = 0; i < d1D.length; i++) { + d1D[i] = d[i][0]; + } + } else { + d1D = d[0]; + } + return d1D; + } + + public static int[] readString1DInt(String s) { + return readString1DInt(s, defaultColumnDelimiter); + } + + public static int[] readString1DInt(String s, String columnDelimiter) { + int[][] d = readStringInt(s, columnDelimiter, NotFoundDelimiter); + int[] d1D = null; + if (d.length > 1) { + d1D = new int[d.length]; + for (int i = 0; i < d1D.length; i++) { + d1D[i] = d[i][0]; + } + } else { + d1D = d[0]; + } + return d1D; + } + + public static double[][] readStringDouble(String s) { + return readStringDouble(s, defaultColumnDelimiter, defaultRowDelimiter); + } + + public static double[][] readStringDouble(String s, String columnDelimiter, String rowDelimiter) { + double[][] array; + + String[] rows = null; + if (s.contains(rowDelimiter)) { + rows = s.split(rowDelimiter); + } else { + rows = new String[]{s}; + } + array = new double[rows.length][]; + for (int i = 0; i < rows.length; i++) { + String[] cols = rows[i].split(columnDelimiter); + array[i] = new double[cols.length]; + for (int j = 0; j < cols.length; j++) { + try { + array[i][j] = Double.parseDouble(cols[j]); + } catch (NumberFormatException e) { + array[i][j] = Double.NaN; + } + } + } + + return array; + } + + public static int[][] readStringInt(String s) { + return readStringInt(s, defaultColumnDelimiter, defaultRowDelimiter); + } + + public static int[][] readStringInt(String s, String columnDelimiter, String rowDelimiter) { + int[][] array; + + String[] rows = s.split(rowDelimiter); + array = new int[rows.length][]; + for (int i = 0; i < rows.length; i++) { + String[] cols = rows[i].split(columnDelimiter); + array[i] = new int[cols.length]; + for (int j = 0; j < cols.length; j++) { + try { + array[i][j] = Integer.parseInt(cols[j]); + } catch (NumberFormatException e) { + array[i][j] = Integer.MAX_VALUE; + } + } + } + + return array; + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/stream/BinaryInputStream.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/stream/BinaryInputStream.java new file mode 100755 index 0000000..9ef994a --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/stream/BinaryInputStream.java @@ -0,0 +1,288 @@ +package org.xbib.graphics.graph.jmathplot.io.stream; + +import org.xbib.graphics.graph.jmathplot.io.littleendian.LEDataInputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Vector; + +/** + * BSD License + * + * @author Yann RICHET + */ + +public class BinaryInputStream extends InputStream { + + private final InputStream stream; + + public static String LITTLE_ENDIAN = "LITTLE_ENDIAN"; + + public static String BIG_ENDIAN = "BIG_ENDIAN"; + + protected boolean bigEndian; + + public BinaryInputStream(InputStream in, String endian) { + bigEndian = isBigEndian(endian); + stream = in; + } + + public BinaryInputStream(InputStream in, boolean endian) { + bigEndian = endian; + stream = in; + } + + private boolean isBigEndian(String endian) { + boolean be; + if (endian.equals(LITTLE_ENDIAN)) { + be = false; + } else if (endian.equals(BIG_ENDIAN)) { + be = true; + } else { + throw new IllegalArgumentException( + "The Endian type : " + + endian + + "is unknown. You must specify LITTLE_ENDIAN or BIG_ENDIAN."); + } + return be; + } + + public static double[] readDoubleArray(InputStream in, String endian) { + BinaryInputStream bi = new BinaryInputStream(in, endian); + return bi.readDoubleArray(); + } + + public static float[] readFloatArray(InputStream in, String endian) { + BinaryInputStream bi = new BinaryInputStream(in, endian); + return bi.readFloatArray(); + } + + public static int[] readIntArray(InputStream in, String endian) { + BinaryInputStream bi = new BinaryInputStream(in, endian); + return bi.readIntArray(); + } + + /** + * Read a binary File + * + * @return int[] + */ + public int[] readIntArray() { + try { + DataInput dis; + if (bigEndian) { + dis = new DataInputStream(stream); + } else { + dis = new LEDataInputStream(stream); + } + + Vector intV = new Vector(); + + try { + while (true) { + int i = dis.readInt(); + intV.add(new Integer(i)); + } + } catch (EOFException eof) { + stream.close(); + } + + int[] array = new int[intV.size()]; + for (int i = 0; i < array.length; i++) { + array[i] = intV.get(i).intValue(); + } + + return array; + } catch (IOException e) { + throw new IllegalArgumentException("InputStream is unreadable : " + + e.toString()); + } + } + + /** + * Read a binary File + * + * @return float[] + */ + public float[] readFloatArray() { + try { + DataInput dis; + if (bigEndian) { + dis = new DataInputStream(stream); + } else { + dis = new LEDataInputStream(stream); + } + + Vector floatV = new Vector(); + + try { + while (true) { + float f = dis.readFloat(); + floatV.add(new Float(f)); + } + } catch (EOFException eof) { + stream.close(); + } + + float[] array = new float[floatV.size()]; + for (int i = 0; i < array.length; i++) { + array[i] = floatV.get(i).floatValue(); + } + + return array; + } catch (IOException e) { + throw new IllegalArgumentException("InputStream is unreadable : " + + e.toString()); + } + } + + /** + * Read a binary File + * + * @return double[] + */ + public double[] readDoubleArray() { + try { + DataInput dis; + if (bigEndian) { + dis = new DataInputStream(stream); + } else { + dis = new LEDataInputStream(stream); + } + + Vector doubleV = new Vector(); + + try { + while (true) { + double f = dis.readDouble(); + doubleV.add(new Double(f)); + } + } catch (EOFException eof) { + stream.close(); + } + + double[] array = new double[doubleV.size()]; + for (int i = 0; i < array.length; i++) { + array[i] = doubleV.get(i).doubleValue(); + } + + return array; + } catch (IOException e) { + throw new IllegalArgumentException("InputStream is unreadable : " + + e.toString()); + } + } + + /** + * Read a binary File + * + * @return double[] + */ + public double[] readDoubleArray(int n1, int n2) { + try { + DataInput dis; + if (bigEndian) { + dis = new DataInputStream(stream); + } else { + dis = new LEDataInputStream(stream); + } + + double[] array = new double[n2 - n1]; + + dis.skipBytes(n1 * 4); + for (int i = 0; i < array.length; i++) { + array[i] = dis.readDouble(); + } + + return array; + } catch (IOException e) { + throw new IllegalArgumentException("InputStream is unreadable : " + + e.toString()); + } + } + + /** + * Read a binary File + * + * @return byte[] + */ + public byte[] readByteArray() { + try { + DataInput dis; + if (bigEndian) { + dis = new DataInputStream(stream); + } else { + dis = new LEDataInputStream(stream); + } + + Vector bytesV = new Vector(); + + try { + while (true) { + byte b = dis.readByte(); + bytesV.add(new Byte(b)); + } + } catch (EOFException eof) { + stream.close(); + } + + byte[] bytes = new byte[bytesV.size()]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = bytesV.get(i).byteValue(); + } + + return bytes; + } catch (IOException e) { + throw new IllegalArgumentException("InputStream is unreadable : " + + e.toString()); + } + } + + /** + * Read a binary File + * + * @return int + */ + public int readInt() { + return readIntArray()[0]; + } + + /** + * Read a binary File + * + * @return float + */ + public float readFloat() { + return readFloatArray()[0]; + } + + /** + * Read a binary File + * + * @return double + */ + public double readDouble() { + return readDoubleArray()[0]; + } + + /** + * Read a binary File + * + * @return byte + */ + public byte readByte() { + return readByteArray()[0]; + } + + /** + * Read a binary File + * + * @return byte + */ + public int read() { + return readInt(); + } + +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/stream/BinaryOutputStream.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/stream/BinaryOutputStream.java new file mode 100755 index 0000000..fb6922a --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/io/stream/BinaryOutputStream.java @@ -0,0 +1,222 @@ +package org.xbib.graphics.graph.jmathplot.io.stream; + +import org.xbib.graphics.graph.jmathplot.io.littleendian.LEDataOutputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * BSD License + * + * @author Yann RICHET + */ +public class BinaryOutputStream extends OutputStream { + + private final OutputStream stream; + public static String LITTLE_ENDIAN = "LITTLE_ENDIAN"; + public static String BIG_ENDIAN = "BIG_ENDIAN"; + protected boolean bigEndian; + + public BinaryOutputStream(OutputStream out, String endian) { + bigEndian = isBigEndian(endian); + stream = out; + } + + public BinaryOutputStream(OutputStream out, boolean endian) { + bigEndian = endian; + stream = out; + } + + private boolean isBigEndian(String endian) { + boolean be; + if (endian.equals(LITTLE_ENDIAN)) { + be = false; + } else if (endian.equals(BIG_ENDIAN)) { + be = true; + } else { + throw new IllegalArgumentException( + "The Endian type : " + endian + "is unknown. You must specify LITTLE_ENDIAN or BIG_ENDIAN."); + } + return be; + } + + public static void writeDoubleArray(OutputStream out, double[] array, + String endian) { + BinaryOutputStream bs = new BinaryOutputStream(out, endian); + bs.writeDoubleArray(array, false); + } + + public static void appendDoubleArray(OutputStream out, double[] array, + String endian) { + BinaryOutputStream bs = new BinaryOutputStream(out, endian); + bs.writeDoubleArray(array, true); + } + + public static void writeFloatArray(OutputStream out, float[] array, + String endian) { + BinaryOutputStream bs = new BinaryOutputStream(out, endian); + bs.writeFloatArray(array, false); + } + + public static void appendFloatArray(OutputStream out, float[] array, + String endian) { + BinaryOutputStream bs = new BinaryOutputStream(out, endian); + bs.writeFloatArray(array, true); + } + + public static void writeIntArray(OutputStream out, int[] array, + String endian) { + BinaryOutputStream bs = new BinaryOutputStream(out, endian); + bs.writeIntArray(array, false); + } + + public static void appendIntArray(OutputStream out, int[] array, + String endian) { + BinaryOutputStream bs = new BinaryOutputStream(out, endian); + bs.writeIntArray(array, true); + } + + /** + * Write an int array in a binary File + * + * @param array int[] + * @param append boolean + */ + public void writeIntArray(int[] array, boolean append) { + try { + DataOutput dos; + if (bigEndian) { + dos = new DataOutputStream(stream); + } else { + dos = new LEDataOutputStream(stream); + } + + for (int i = 0; i < array.length; i++) { + dos.writeInt(array[i]); + } + + stream.close(); + } catch (IOException e) { + throw new IllegalArgumentException("InputStream is unwritable : " + e.toString()); + } + } + + /** + * Write a float array in a binary File + * + * @param array float[] + * @param append boolean + */ + public void writeFloatArray(float[] array, boolean append) { + try { + DataOutput dos; + if (bigEndian) { + dos = new DataOutputStream(stream); + } else { + dos = new LEDataOutputStream(stream); + } + + for (int i = 0; i < array.length; i++) { + dos.writeFloat(array[i]); + } + + stream.close(); + } catch (IOException e) { + throw new IllegalArgumentException("InputStream is unwritable : " + e.toString()); + } + } + + /** + * Write a double array in a binary File + * + * @param array float[] + * @param append boolean + */ + public void writeDoubleArray(double[] array, boolean append) { + try { + DataOutput dos; + if (bigEndian) { + dos = new DataOutputStream(stream); + } else { + dos = new LEDataOutputStream(stream); + } + + for (int i = 0; i < array.length; i++) { + dos.writeDouble(array[i]); + } + + stream.close(); + } catch (IOException e) { + throw new IllegalArgumentException("InputStream is unwritable : " + e.toString()); + } + } + + /** + * Write a text in a binary File + * + * @param bytes byte[] + * @param append boolean + */ + public void writeByteArray(byte[] bytes, boolean append) { + try { + DataOutputStream dos = new DataOutputStream(stream); + + dos.write(bytes); + + stream.close(); + + } catch (IOException e) { + throw new IllegalArgumentException("InputStream is unwritable : " + e.toString()); + } + } + + /** + * Write an int in a binary File + * + * @param i int + * @param append boolean + */ + public void writeInt(int i, boolean append) { + writeIntArray(new int[]{i}, append); + } + + /** + * Write a float in a binary File + * + * @param f float + * @param append boolean + */ + public void writeFloat(float f, boolean append) { + writeFloatArray(new float[]{f}, append); + } + + /** + * Write a double in a binary File + * + * @param d double + * @param append boolean + */ + public void writeDouble(double d, boolean append) { + writeDoubleArray(new double[]{d}, append); + } + + /** + * Write a text in a binary File + * + * @param b byte + * @param append boolean + */ + public void writeByte(byte b, boolean append) { + writeByteArray(new byte[]{b}, append); + } + + /** + * Write an int in a binary File + * + * @param i int + */ + public void write(int i) { + writeInt(i, false); + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/DataPanel.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/DataPanel.java new file mode 100644 index 0000000..16c029f --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/DataPanel.java @@ -0,0 +1,77 @@ +package org.xbib.graphics.graph.jmathplot.panel; + +import org.xbib.graphics.graph.jmathplot.io.ClipBoardPrintable; +import org.xbib.graphics.graph.jmathplot.io.FilePrintable; +import org.xbib.graphics.graph.jmathplot.io.StringPrintable; +import org.xbib.graphics.graph.jmathplot.frame.DataToolBar; +import java.awt.BorderLayout; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.io.File; +import javax.swing.JPanel; +import javax.swing.JScrollPane; + +/** + * BSD License + * + * @author Yann RICHET + */ +public abstract class DataPanel extends JPanel implements ComponentListener, FilePrintable, ClipBoardPrintable, StringPrintable { + + protected DataToolBar toolBar; + protected JScrollPane scrollPane; + public static int[] dimension = new int[]{400, 400}; + + public DataPanel() { + setLayout(new BorderLayout()); + initToolBar(); + init(); + } + + protected void initToolBar() { + toolBar = new DataToolBar(this); + add(toolBar, BorderLayout.NORTH); + toolBar.setFloatable(false); + } + + protected void initSize() { + if (scrollPane != null) { + scrollPane.setSize(this.getSize()); + } + // scrollPane.setPreferredSize(this.getSize()); + } + + protected void init() { + // initSize(); + addComponentListener(this); + } + + public void update() { + // this.remove(scrollPane); + toWindow(); + repaint(); + } + + protected abstract void toWindow(); + + public abstract void toClipBoard(); + + public abstract void toASCIIFile(File file); + + public void componentHidden(ComponentEvent e) { + } + + public void componentMoved(ComponentEvent e) { + } + + public void componentResized(ComponentEvent e) { + /* + * dimension = new int[] { (int) (this.getSize().getWidth()), (int) + * (this.getSize().getHeight()) }; + */ + initSize(); + } + + public void componentShown(ComponentEvent e) { + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/DataSelectPanel.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/DataSelectPanel.java new file mode 100644 index 0000000..45abfac --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/DataSelectPanel.java @@ -0,0 +1,827 @@ +/* + * Created on 6 juil. 07 by richet + */ +package org.xbib.graphics.graph.jmathplot.panel; + +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.util.LinkedList; +import java.util.Vector; +import javax.swing.AbstractAction; +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JScrollPane; +import javax.swing.JSlider; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +/** + * Panel designed to select a given number of columns in a multi columns matrix. + * Useful to provide a 3d plot view of Nd data matrix. + */ +public class DataSelectPanel extends JPanel { + + private static final long serialVersionUID = 419181752327223313L; + ParameterRow[] rows; + private Object[][] _data, _selecteddata; + private LinkedList _tmpselecteddata; + boolean dataUpdated = false; + private int[] _tmpselectedIndex; + private int _nbselected; + private int[] _selectedindex; + private final String[] _parametersNames; + //boolean Zselected = false; + int _dimension; + + void autoSelectVariableParam() { + int d = 0; + for (int i = 0; i < _data[0].length; i++) { + boolean constant = true; + String val = _data[0][i].toString(); + for (int j = 1; j < _data.length; j++) { + if (!_data[j][i].toString().equals(val)) { + //System.err.println(_data[j][i] + " != " + val); + constant = false; + break; + } + } + if (!constant && _dimension > d) { + if (d == 0) { + selectAsX(i); + d++; + continue; + } else if (d == 1) { + selectAsY(i); + d++; + continue; + } else if (d == 2) { + selectAsZ(i); + break; + } + } + } + } + + public DataSelectPanel(Object[][] data, int dimension, String... parametersNames) { + _data = data; + _dimension = dimension; + + _parametersNames = parametersNames; + + if (_dimension > parametersNames.length) { + throw new IllegalArgumentException("Number of parameters must be > to dimension=" + _dimension); + } + + setLayout(new GridLayout(_parametersNames.length /*+ 1*/, 1)); + //JPanel title = new JPanel(); + //title.setLayout(new GridLayout(1, 2)); + //title.add(new JLabel("Variable")); + //title.add(new JLabel("Axis / Parameter")); + //add(title); + + if (_dimension == 0) { + buildRows(); + } else if (_dimension == 1) { + buildRows(0); + } else if (_dimension == 2) { + buildRows(0, 1); + } else if (_dimension == 3) { + buildRows(0, 1, 2); + } + + fireSelectedDataChanged("init"); + } + + void buildRows(int... selectedaxis) { + ButtonGroup xgrp = new ButtonGroup(); + ButtonGroup ygrp = new ButtonGroup(); + ButtonGroup zgrp = new ButtonGroup(); + rows = new ParameterRow[_parametersNames.length]; + for (int i = 0; i < _parametersNames.length; i++) { + rows[i] = new ParameterRow(_parametersNames[i], getColumn(i, _data)); + + if (selectedaxis != null && selectedaxis.length > 0) { + if (selectedaxis.length >= 1) { + rows[i].xaxis.setSelected(selectedaxis[0] == i); + } + if (selectedaxis.length >= 2) { + rows[i].yaxis.setSelected(selectedaxis[1] == i); + } + if (selectedaxis.length == 3) { + rows[i].zaxis.setSelected(selectedaxis[2] == i); + } + } + + xgrp.add(rows[i].xaxis); + ygrp.add(rows[i].yaxis); + zgrp.add(rows[i].zaxis); + + add(rows[i]); + } + setPreferredSize(new Dimension(row_width, row_height * _parametersNames.length)); + setSize(new Dimension(row_width, row_height * _parametersNames.length)); + + autoSelectVariableParam(); + updateSelectedData(); + } + + public void setData(Object[][] data) { + if (data[0].length != _data[0].length) { + throw new IllegalArgumentException("new data dimension is not consistent with previous one."); + } + _data = data; + + int[] selectedaxis = new int[_dimension]; + for (int i = 0; i < rows.length; i++) { + if (selectedaxis.length >= 1) { + if (rows[i].xaxis.isSelected()) { + selectedaxis[0] = i; + } + } + if (selectedaxis.length >= 2) { + if (rows[i].yaxis.isSelected()) { + selectedaxis[1] = i; + } + } + if (selectedaxis.length == 3) { + if (rows[i].zaxis.isSelected()) { + selectedaxis[2] = i; + } + } + remove(rows[i]); + } + + dataUpdated = false; + buildRows(selectedaxis); + + fireSelectedDataChanged("set"); + } + + void updateSelectedData() { + if (dataUpdated) { + return; + } + + for (ParameterRow row : rows) { + boolean isaxis = row.xaxis.isSelected() || row.yaxis.isSelected() || row.zaxis.isSelected(); + if (row._isNumber) { + row.min.setEnabled(!isaxis); + row.max.setEnabled(!isaxis); + } else { + row.list.setEnabled(!isaxis); + } + if (!isaxis) { + if (row._isNumber) { + row.name.setText(row._paramName + "=[" + row._kernelDoubleValues[row.min.getValue() - 1] + "," + row._kernelDoubleValues[row.max.getValue() - 1] + "]"); + } else { + row.name.setText(row._paramName + "={" + Array.cat(row.list.getSelectedValues()) + "}"); + } + } else { + row.name.setText(row._paramName); + } + } + + _tmpselectedIndex = new int[_data.length]; + _nbselected = 0; + _tmpselecteddata = new LinkedList(); + for (int i = 0; i < _data.length; i++) { + boolean sel = true; + for (int j = 0; j < rows.length; j++) { + ParameterRow row = rows[j]; + if (!row.xaxis.isSelected() && !row.yaxis.isSelected() && !row.zaxis.isSelected() && !row.check(_data[i][j])) { + sel = false; + } + } + + if (sel) { + _tmpselecteddata.add(_data[i]); + _tmpselectedIndex[_nbselected] = i; + _nbselected++; + /*System.out.print("OK:"); + for (int j = 0; j < _tmpselecteddata.getLast().length; j++) + System.out.print(_tmpselecteddata.getLast()[j]+","); + System.out.println("");*/ + } + } + dataUpdated = true; + } + + /** + * Method to override if you want to link to any gui component (for instance, a plotpanel). + */ + public void fireSelectedDataChanged(String from) { + System.err.println("fireSelectedDataChanged from " + from); + Object[][] sel = getSelectedFullData(); + System.err.println("selected full data :"); + System.err.println(Array.cat(_parametersNames)); + if (sel.length > 0) { + System.err.println(Array.cat(getSelectedFullData())); + } + + sel = getSelectedProjectedData(); + System.err.println("selected projected data :"); + switch (_dimension) { + case 0: + System.err.println("No axis selected"); + break; + case 1: + System.err.println(Array.cat(new String[]{getSelectedXAxis()})); + break; + case 2: + System.err.println(Array.cat(new String[]{getSelectedXAxis(), getSelectedYAxis()})); + break; + case 3: + System.err.println(Array.cat(new String[]{getSelectedXAxis(), getSelectedYAxis(), getSelectedZAxis()})); + break; + } + if (sel.length > 0) { + System.err.println(Array.cat(sel)); + } + System.err.println("Done."); + } + + /** + * return selected data + */ + public int[] getSelectedDataIndex() { + updateSelectedData(); + _selectedindex = new int[_nbselected]; + for (int i = 0; i < _nbselected; i++) { + _selectedindex[i] = _tmpselectedIndex[i]; + } + return _selectedindex; + } + + /** + * return selected data + */ + public Object[][] getSelectedFullData() { + updateSelectedData(); + _selecteddata = new Object[_tmpselecteddata.size()][_data[0].length]; + for (int i = 0; i < _selecteddata.length; i++) { + for (int j = 0; j < _selecteddata[i].length; j++) { + _selecteddata[i][j] = _tmpselecteddata.get(i)[j]; + } + } + return _selecteddata; + } + + /** + * return selected data projected on axis selected + */ + public Object[][] getSelectedProjectedData() { + //updateSelectedData(); + /*if (_dimension == 0) { + return getSelectedFullData(); + }*/ + int[] selectedaxis = getSelectedAxisIndex(); + _selecteddata = new Object[_tmpselecteddata.size()][_dimension]; + for (int i = 0; i < _selecteddata.length; i++) { + for (int j = 0; j < _dimension; j++) { + _selecteddata[i][j] = _tmpselecteddata.get(i)[selectedaxis[j]]; + } + } + return _selecteddata; + } + + public int[] getSelectedAxisIndex() { + int[] selectedaxis = new int[_dimension]; + updateSelectedData(); + for (int i = 0; i < rows.length; i++) { + if (rows[i].xaxis.isSelected()) { + //System.out.println("selextedaxis[0] =" + i); + selectedaxis[0] = i; + } + if (rows[i].yaxis.isSelected()) { + //System.out.println("selextedaxis[1] =" + i); + selectedaxis[1] = i; + } + if (rows[i].zaxis.isSelected()) { + //System.out.println("selextedaxis[2] =" + i); + selectedaxis[2] = i; + } + } + return selectedaxis; + } + + /** + * return selected X axis name + */ + public String getSelectedXAxis() { + updateSelectedData(); + for (ParameterRow row : rows) { + if (row.xaxis.isSelected()) { + return row._paramName; + } + } + return null; + } + + /** + * return selected Y axis name + */ + public String getSelectedYAxis() { + updateSelectedData(); + for (ParameterRow row : rows) { + if (row.yaxis.isSelected()) { + return row._paramName; + } + } + return null; + } + + /** + * return selected Z axis name + */ + public String getSelectedZAxis() { + updateSelectedData(); + for (ParameterRow row : rows) { + if (row.zaxis.isSelected()) { + return row._paramName; + } + } + return null; + } + + static Object[] getColumn(int j, Object[][] mat) { + Object[] col = new Object[mat.length]; + for (int i = 0; i < col.length; i++) { + col[i] = mat[i][j]; + } + return col; + } + + public Font font = new Font("Arial", Font.PLAIN, 10); + public int row_height = 60; + public int row_width = 300; + + public void selectAsX(int row) { + rows[row].selectAsX(); + } + + public void selectAsY(int row) { + rows[row].selectAsY(); + } + + public void selectAsZ(int row) { + rows[row].selectAsZ(); + } + + class ParameterRow extends JPanel { + + String _paramName; + JLabel name; + JRadioButton xaxis, yaxis, zaxis; + JComponent parameter; + JSlider min, max; + JCheckBox linkminmax; + JList list; + //Object[] _values; + Vector _kernelStringValues; + boolean _isNumber; + //double[] _dvalues; + double[] _kernelDoubleValues; + + /** + * Quick Sort algoritm. + *

+ * Allows to sort a column quickly, Using a generic version of C.A.R Hoare's + * Quick Sort algorithm. + *

+ */ + public class Sorting { + + /* + * ------------------------ Class variables ------------------------ + */ + /** + * Array for internal storage of the matrix to sort. + */ + private final double[] A; + /** + * Array for internal storage of the order. + */ + private final int[] order; + + /* + * ------------------------ Constructors ------------------------ + */ + + /** + * Construct an ascending order. + * + * @param array Array to sort. + * @param copyArray Specify if the sort is made directly : true -> array is + * modified (usefull for big arrays !), false -> array is copied + * and not modified (more memory used). + */ + public Sorting(double[] array, boolean copyArray) { + if (copyArray) { + A = new double[array.length]; + System.arraycopy(array, 0, A, 0, array.length); + // for (int i = 0; i < A.length; i++) { + // A[i] = array[i]; + // } + } else { + A = array; + } + + order = new int[A.length]; + for (int i = 0; i < A.length; i++) { + order[i] = i; + } + sort(A); + } + + /* + * ------------------------ Public Methods ------------------------ + */ + public int[] invertIndex(int[] ind) { + int[] invind = new int[ind.length]; + for (int i = 0; i < ind.length; i++) { + invind[ind[i]] = i; + + } + return invind; + } + + /** + * Get the ascending order of one line. + * + * @param i Line number. + * @return Ascending order of the line. + */ + public int getIndex(int i) { + return order[i]; + } + + /** + * Get the ascending order of all lines. + * + * @return Ascending order of lines. + */ + public int[] getIndex() { + return order; + } + + /* + * ------------------------ Private Methods ------------------------ + */ + + /** + * This is a generic version of C.A.R Hoare's Quick Sort algorithm. This + * will handle arrays that are already sorted, and arrays with duplicate + * keys.
+ *

+ * If you think of a one dimensional array as going from the lowest index on + * the left to the highest index on the right then the parameters to this + * function are lowest index or left and highest index or right. The first + * time you call this function it will be with the parameters 0, a.length - + * 1. + * + * @param a A double array. + * @param lo0 Int. + * @param hi0 Int. + */ + private void QuickSort(double[] a, int lo0, int hi0) { + + int lo = lo0; + int hi = hi0; + double mid; + + if (hi0 > lo0) { + // Arbitrarily establishing partition element as the midpoint of the + // array. + mid = a[(lo0 + hi0) / 2]; + + // loop through the array until indices cross + while (lo <= hi) { + // find the first element that is greater than or equal to the + // partition element starting from the left Index. + while ((lo < hi0) && (a[lo] < mid)) { + ++lo; + } + // find an element that is smaller than or equal to the + // partition element starting from the right Index. + while ((hi > lo0) && (a[hi] > mid)) { + --hi; + } + // if the indexes have not crossed, swap + if (lo <= hi) { + swap(a, lo, hi); + ++lo; + --hi; + } + } + + // If the right index has not reached the left side of array must + // now sort the left partition. + if (lo0 < hi) { + QuickSort(a, lo0, hi); + + // If the left index has not reached the right side of array + // must now sort the right partition. + } + if (lo < hi0) { + QuickSort(a, lo, hi0); + + } + } + } + + /** + * Swap two positions. + * + * @param a Array. + * @param i Line number. + * @param j Line number. + */ + private void swap(double[] a, int i, int j) { + double T; + T = a[i]; + a[i] = a[j]; + a[j] = T; + int t; + t = order[i]; + order[i] = order[j]; + order[j] = t; + } + + private void sort(double[] a) { + QuickSort(a, 0, a.length - 1); + } + } + + public void selectAsX() { + xaxis.setSelected(true); + yaxis.setSelected(false); + zaxis.setSelected(false); + for (ParameterRow r : rows) { + if (!r._paramName.equals(_paramName)) { + r.xaxis.setSelected(false); + } + } + dataUpdated = false; + fireSelectedDataChanged(_paramName + " xaxis"); + } + + public void selectAsY() { + xaxis.setSelected(false); + yaxis.setSelected(true); + zaxis.setSelected(false); + for (ParameterRow r : rows) { + if (!r._paramName.equals(_paramName)) { + r.yaxis.setSelected(false); + } + } + dataUpdated = false; + fireSelectedDataChanged(_paramName + " yaxis"); + } + + public void selectAsZ() { + xaxis.setSelected(false); + yaxis.setSelected(false); + zaxis.setSelected(true); + for (ParameterRow r : rows) { + if (!r._paramName.equals(_paramName)) { + r.zaxis.setSelected(false); + } + } + dataUpdated = false; + fireSelectedDataChanged(_paramName + " zaxis"); + } + + public ParameterRow(String paramName, Object[] values) { + _paramName = paramName; + _isNumber = Array.isDouble(values[0].toString()); + + if (!_isNumber) { + _kernelStringValues = new Vector(values.length); + for (int i = 0; i < values.length; i++) { + if (!_kernelStringValues.contains(values[i])) { + _kernelStringValues.add(values[i]); + } + } + } else { + Vector _tmpdvalues = new Vector(values.length); + for (int i = 0; i < values.length; i++) { + if (!_tmpdvalues.contains(Double.valueOf(values[i].toString()))) { + _tmpdvalues.add(Double.valueOf(values[i].toString())); + } + } + + _kernelDoubleValues = new double[_tmpdvalues.size()]; + for (int i = 0; i < _kernelDoubleValues.length; i++) { + _kernelDoubleValues[i] = _tmpdvalues.get(i); + } + + new Sorting(_kernelDoubleValues, false); + } + + setLayout(new GridLayout(1, 2)); + + name = new JLabel(_paramName); + name.setFont(font); + JPanel left = new JPanel(new BorderLayout()); + + left.add(name, BorderLayout.CENTER); + add(left, 0); + + JPanel right = new JPanel(new BorderLayout()); + + JPanel XYZ = new JPanel(); + + if (_dimension > 0) { + XYZ = new JPanel(new GridLayout(_dimension, 1)); + } + + xaxis = new JRadioButton("X"); + xaxis.setFont(font); + xaxis.addActionListener(new AbstractAction() { + + public void actionPerformed(ActionEvent e) { + selectAsX(); + } + }); + if (_dimension >= 1) { + XYZ.add(xaxis); + } + yaxis = new JRadioButton("Y"); + yaxis.setFont(font); + yaxis.addActionListener(new AbstractAction() { + + public void actionPerformed(ActionEvent e) { + selectAsY(); + } + }); + if (_dimension >= 2) { + XYZ.add(yaxis); + } + + zaxis = new JRadioButton("Z"); + zaxis.setFont(font); + zaxis.addActionListener(new AbstractAction() { + + public void actionPerformed(ActionEvent e) { + selectAsZ(); + } + }); + if (_dimension == 3) { + XYZ.add(zaxis); + } + + left.add(XYZ, BorderLayout.EAST); + + if (_isNumber) { + parameter = new JPanel(); + parameter.setLayout(new GridLayout(2, 1)); + + min = new JSlider(1, _kernelDoubleValues.length, 1); + min.setFont(font); + + min.setMinorTickSpacing(1); + min.setSnapToTicks(true); + min.setPaintTicks(true); + max = new JSlider(1, _kernelDoubleValues.length, _kernelDoubleValues.length); + max.setFont(font); + max.setMinorTickSpacing(1); + max.setSnapToTicks(true); + max.setPaintTicks(true); + min.addChangeListener(new ChangeListener() { + + public void stateChanged(ChangeEvent e) { + if (max.getValue() < min.getValue()) { + max.setValue(min.getValue()); + } + dataUpdated = false; + fireSelectedDataChanged(_paramName + " min"); + + } + }); + max.addChangeListener(new ChangeListener() { + + public void stateChanged(ChangeEvent e) { + if (max.getValue() < min.getValue()) { + min.setValue(max.getValue()); + } + dataUpdated = false; + fireSelectedDataChanged(_paramName + " max"); + + } + }); + parameter.add(min, 0); + parameter.add(max, 1); + } else { + + list = new JList(_kernelStringValues); + list.setFont(font); + list.setSelectedIndices(buildIntSeq(0, _kernelStringValues.size() - 1)); + list.addListSelectionListener(new ListSelectionListener() { + + public void valueChanged(ListSelectionEvent e) { + dataUpdated = false; + fireSelectedDataChanged(_paramName + " list"); + } + }); + parameter = new JScrollPane(list); + } + right.add(parameter, BorderLayout.CENTER); + add(right, 1); + + setBorder(BorderFactory.createEtchedBorder()); + setPreferredSize(new Dimension(row_width, row_height)); + setSize(new Dimension(row_width, row_height)); + + } + + int[] buildIntSeq(int min, int max) { + int[] seq = new int[max - min + 1]; + for (int i = 0; i < seq.length; i++) { + seq[i] = min + i; + } + return seq; + } + + boolean check(Object value) { + if (_isNumber) { + double dval = Double.valueOf(value.toString()); + return (dval >= _kernelDoubleValues[min.getValue() - 1] && dval <= _kernelDoubleValues[max.getValue() - 1]); + } else { + for (int i = 0; i < list.getSelectedIndices().length; i++) { + if (_kernelStringValues.get(list.getSelectedIndices()[i]).equals(value)) { + return true; + } + } + return false; + } + } + } + + public static void main(String[] args) { + final PlotPanel pp = new Plot3DPanel(PlotPanel.WEST); + pp.setPreferredSize(new Dimension(400, 400)); + new FrameView(pp).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + Object[][] data = {{1, 3, 4, 5, "a0"}, {1, 3, 1, 1, "a1"}, {1, 3, 2, 2, "a2"}, {1, 3, 3, 3, "a5"}, {1, 3, 3, 3, "a3"}, {1.5, 3.5, 3, 4, "a2"}}; + + DataSelectPanel dsp3 = new DataSelectPanel(data, 3, "x1", "x2", "x3", "x4", "x5") { + + private static final long serialVersionUID = 1L; + + @Override + public void fireSelectedDataChanged(String from) { + super.fireSelectedDataChanged(from); + pp.setAxisLabel(0, getSelectedXAxis()); + pp.setAxisLabel(1, getSelectedYAxis()); + pp.setAxisLabel(2, getSelectedZAxis()); + + System.err.println("plotting ..."); + if (pp.getPlots().size() == 0) { + System.err.println(" new"); + pp.addPlot("SCATTER", "data", pp.mapData(getSelectedProjectedData())); + } else { + System.err.println(" existing"); + if (from != null && from.endsWith("axis")) { + pp.resetMapData(); + pp.removeAllPlots(); + pp.addPlot("SCATTER", "data", pp.mapData(getSelectedProjectedData())); + } else { + pp.getPlot(0).setData(pp.mapData(getSelectedProjectedData())); + } + } + //System.out.println(Array.cat(pp.getAxesScales())); + } + }; + + JFrame f3 = new JFrame("Test mat editor 3"); + f3.setContentPane(dsp3); + f3.pack(); + f3.setVisible(true); + f3.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + + + /*try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + Object[][] data2 = { { 0, 0, 0, 0, "a0" }, { 1, 1, 1, 1, "a1" }, { 2, 2, 2, 2, "a2" }, { 3, 3, 3, 3, "a3" }, { 4, 3, 3, 3, "a3" }, + { 5, 3, 3, 3, "a4" }, { 5, 4, 3, 3, "a4" } }; + dsp.setData(data2);*/ + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/DataSelectTable.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/DataSelectTable.java new file mode 100644 index 0000000..6f461e2 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/DataSelectTable.java @@ -0,0 +1,720 @@ +/* + * Created on 6 juil. 07 by richet + */ +package org.xbib.graphics.graph.jmathplot.panel; + +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeListener; +import java.util.LinkedList; +import java.util.Vector; +import javax.swing.Action; +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JScrollPane; +import javax.swing.JSlider; +import javax.swing.JTable; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.event.TableModelListener; +import javax.swing.table.TableModel; + +/** + * Panel designed to select a given number of columns in a multi columns matrix. + * Useful to provide a 3d plot view of Nd data matrix. + */ +public class DataSelectTable extends JPanel { + + private static final long serialVersionUID = 41918175232722331L; + + LinkedList rows; + + private Object[][] _data, _selecteddata; + + private LinkedList _tmpselecteddata; + + boolean dataUpdated = false; + + private int[] _tmpselectedIndex; + + private int _nbselected; + + private int[] _selectedindex; + + private final String[] _parametersNames; + + private JTable _table; + + int _dimension; + + public DataSelectTable(Object[][] data, int dimension, String... parametersNames) { + _data = data; + _dimension = dimension; + + _parametersNames = parametersNames; + + if (_dimension > parametersNames.length) { + throw new IllegalArgumentException("Number of parameters must be > to dimension=" + _dimension); + } + + if (_dimension == 1) { + buildRows(0); + } else if (_dimension == 2) { + buildRows(0, 1); + } else if (_dimension == 3) { + buildRows(0, 1, 2); + } + + add(new JScrollPane(_table)); + + } + + LinkedList header; + + LinkedList> columnclasses; + + class Model implements TableModel { + + public Model(int... selectedaxis) { + + } + + public void setValueAt(Object value, int rowIndex, int columnIndex) { + // TODO Auto-generated method stub + + } + + public void removeTableModelListener(TableModelListener l) { + // TODO Auto-generated method stub + + } + + public boolean isCellEditable(int rowIndex, int columnIndex) { + // TODO Auto-generated method stub + return false; + } + + public Object getValueAt(int rowIndex, int columnIndex) { + if (columnIndex == 0) { + return _parametersNames[rowIndex]; + } + if (columnIndex == _dimension - 2) { + return rows.get(rowIndex).xaxis; + } + if (columnIndex == _dimension - 1) { + return rows.get(rowIndex).yaxis; + } + if (columnIndex == _dimension) { + return rows.get(rowIndex).zaxis; + } + + return null; + } + + public int getRowCount() { + return _parametersNames.length; + } + + public String getColumnName(int columnIndex) { + return header.get(columnIndex); + } + + public int getColumnCount() { + return header.size(); + } + + public Class getColumnClass(int columnIndex) { + return columnclasses.get(columnIndex); + } + + public void addTableModelListener(TableModelListener l) { + // TODO Auto-generated method stub + + } + + } + + void buildRows(int... selectedaxis) { + + header = new LinkedList(); + header.add("Parameter"); + if (_dimension <= 1) { + header.add("X"); + } + if (_dimension <= 2) { + header.add("Y"); + } + if (_dimension <= 3) { + header.add("Z"); + } + header.add("min"); + header.add("<>"); + header.add("="); + header.add("<>"); + header.add("max"); + + columnclasses = new LinkedList>(); + columnclasses.add(String.class); + if (_dimension <= 1) { + columnclasses.add(Boolean.class); + } + if (_dimension <= 2) { + columnclasses.add(Boolean.class); + } + if (_dimension <= 3) { + columnclasses.add(Boolean.class); + } + columnclasses.add(Double.class); + columnclasses.add(JSlider.class); + columnclasses.add(Boolean.class); + columnclasses.add(JSlider.class); + columnclasses.add(Double.class); + + ButtonGroup xgrp = new ButtonGroup(); + ButtonGroup ygrp = new ButtonGroup(); + ButtonGroup zgrp = new ButtonGroup(); + rows = new LinkedList(); + for (int i = 0; i < _parametersNames.length; i++) { + rows.add(new ParameterRow(_parametersNames[i], getColumn(i, _data))); + + rows.get(i).xaxis.setSelected(selectedaxis[0] == i); + if (selectedaxis.length >= 2) { + rows.get(i).yaxis.setSelected(selectedaxis[1] == i); + } + if (selectedaxis.length == 3) { + rows.get(i).zaxis.setSelected(selectedaxis[2] == i); + } + + xgrp.add(rows.get(i).xaxis); + ygrp.add(rows.get(i).yaxis); + zgrp.add(rows.get(i).zaxis); + } + updateSelectedData(); + } + + public void setData(Object[][] data) { + if (data[0].length != _data[0].length) { + throw new IllegalArgumentException("new data dimension is not consistent with previous one."); + } + _data = data; + + int[] selectedaxis = new int[_dimension]; + for (int i = 0; i < rows.size(); i++) { + if (rows.get(i).xaxis.isSelected()) { + selectedaxis[0] = i; + } + if (selectedaxis.length >= 2) { + if (rows.get(i).yaxis.isSelected()) { + selectedaxis[1] = i; + } + } + if (selectedaxis.length == 3) { + if (rows.get(i).zaxis.isSelected()) { + selectedaxis[2] = i; + } + } + rows.remove(i); + } + + dataUpdated = false; + buildRows(selectedaxis); + + fireSelectedDataChanged("setData"); + } + + void updateSelectedData() { + if (dataUpdated) { + return; + } + + for (ParameterRow row : rows) { + boolean isaxis = row.xaxis.isSelected() || row.yaxis.isSelected() || row.zaxis.isSelected(); + if (row._isNumber) { + row.min.setEnabled(!isaxis); + row.max.setEnabled(!isaxis); + } else { + row.list.setEnabled(!isaxis); + } + /*if (!isaxis) + if (row._isNumber) + row.name.setText(row._paramName + "=[" + row._kernelDoubleValues[row.min.getValue() - 1] + "," + + row._kernelDoubleValues[row.max.getValue() - 1] + "]"); + else + row.name.setText(row._paramName + "={" + Array.cat(row.list.getSelectedValues()) + "}"); + else + row.name.setText(row._paramName);*/ + } + + _tmpselectedIndex = new int[_data.length]; + _nbselected = 0; + _tmpselecteddata = new LinkedList(); + for (int i = 0; i < _data.length; i++) { + boolean sel = true; + /*for (int j = 0; j < rows.length; j++) { + ParameterRow row = rows[j]; + if (!row.xaxis.isSelected() && !row.yaxis.isSelected() && !row.zaxis.isSelected() && !row.check(_data[i][j])) + sel = false; + }*/ + + if (sel) { + _tmpselecteddata.add(_data[i]); + _tmpselectedIndex[_nbselected] = i; + _nbselected++; + /*System.out.print("OK:"); + for (int j = 0; j < _tmpselecteddata.getLast().length; j++) + System.out.print(_tmpselecteddata.getLast()[j]+","); + System.out.println("");*/ + } + } + dataUpdated = true; + } + + /** + * Method to override if you want to link to any gui component (for instance, a plotpanel). + */ + public void fireSelectedDataChanged(String from) { + System.out.println("fireSelectedDataChanged from " + from); + Object[][] sel = getSelectedFullData(); + System.out.println("selected full data :"); + System.out.println(Array.cat(_parametersNames)); + if (sel.length > 0) { + System.out.println(Array.cat(getSelectedFullData())); + } + + sel = getSelectedProjectedData(); + System.out.println("selected projected data :"); + switch (_dimension) { + case 1: + System.out.println(Array.cat(new String[]{getSelectedXAxis()})); + break; + case 2: + System.out.println(Array.cat(new String[]{getSelectedXAxis(), getSelectedYAxis()})); + break; + case 3: + System.out.println(Array.cat(new String[]{getSelectedXAxis(), getSelectedYAxis(), getSelectedZAxis()})); + break; + } + if (sel.length > 0) { + System.out.println(Array.cat(getSelectedProjectedData())); + } + + } + + /** + * return selected data + */ + public int[] getSelectedDataIndex() { + updateSelectedData(); + _selectedindex = new int[_nbselected]; + for (int i = 0; i < _nbselected; i++) { + _selectedindex[i] = _tmpselectedIndex[i]; + } + return _selectedindex; + } + + /** + * return selected data + */ + public Object[][] getSelectedFullData() { + updateSelectedData(); + _selecteddata = new Object[_tmpselecteddata.size()][_data[0].length]; + for (int i = 0; i < _selecteddata.length; i++) { + for (int j = 0; j < _selecteddata[i].length; j++) { + _selecteddata[i][j] = _tmpselecteddata.get(i)[j]; + } + } + return _selecteddata; + } + + /** + * return selected data projected on axis selected + */ + public Object[][] getSelectedProjectedData() { + updateSelectedData(); + int[] selextedaxis = getSelectedAxisIndex(); + _selecteddata = new Object[_tmpselecteddata.size()][_dimension]; + for (int i = 0; i < _selecteddata.length; i++) { + for (int j = 0; j < _dimension; j++) { + _selecteddata[i][j] = _tmpselecteddata.get(i)[selextedaxis[j]]; + } + } + return _selecteddata; + } + + public int[] getSelectedAxisIndex() { + int[] selextedaxis = new int[_dimension]; + updateSelectedData(); + /*for (int i = 0; i < rows.length; i++) { + if (rows[i].xaxis.isSelected()) { + //System.out.println("selextedaxis[0] =" + i); + selextedaxis[0] = i; + } + if (rows[i].yaxis.isSelected()) { + //System.out.println("selextedaxis[1] =" + i); + selextedaxis[1] = i; + } + if (rows[i].zaxis.isSelected()) { + //System.out.println("selextedaxis[2] =" + i); + selextedaxis[2] = i; + } + }*/ + return selextedaxis; + } + + /** + * return selected X axis name + */ + public String getSelectedXAxis() { + updateSelectedData(); + for (ParameterRow row : rows) { + if (row.xaxis.isSelected()) { + return row._paramName; + } + } + return null; + } + + /** + * return selected Y axis name + */ + public String getSelectedYAxis() { + updateSelectedData(); + for (ParameterRow row : rows) { + if (row.yaxis.isSelected()) { + return row._paramName; + } + } + return null; + } + + /** + * return selected Z axis name + */ + public String getSelectedZAxis() { + updateSelectedData(); + for (ParameterRow row : rows) { + if (row.zaxis.isSelected()) { + return row._paramName; + } + } + return null; + } + + static Object[] getColumn(int j, Object[][] mat) { + Object[] col = new Object[mat.length]; + for (int i = 0; i < col.length; i++) { + col[i] = mat[i][j]; + } + return col; + } + + class ParameterRow /*extends JPanel */ { + + //private static final long serialVersionUID = -7301434647336910071L; + + String _paramName; + + //JLabel name; + + JRadioButton xaxis, yaxis, zaxis; + + JComponent parameter; + + JSlider min, max; + + JCheckBox linkminmax; + + JList list; + + //Object[] _values; + + Vector _kernelStringValues; + + boolean _isNumber; + + //double[] _dvalues; + + double[] _kernelDoubleValues; + + public ParameterRow(String paramName, Object[] values) { + _paramName = paramName; + _isNumber = Array.isDouble(values[0].toString()); + + if (!_isNumber) { + _kernelStringValues = new Vector(values.length); + for (int i = 0; i < values.length; i++) { + if (!_kernelStringValues.contains(values[i])) { + _kernelStringValues.add(values[i]); + } + } + } else { + Vector _tmpdvalues = new Vector(values.length); + for (int i = 0; i < values.length; i++) { + if (!_tmpdvalues.contains(Double.valueOf(values[i].toString()))) { + _tmpdvalues.add(Double.valueOf(values[i].toString())); + } + } + + _kernelDoubleValues = new double[_tmpdvalues.size()]; + for (int i = 0; i < _kernelDoubleValues.length; i++) { + _kernelDoubleValues[i] = _tmpdvalues.get(i); + } + } + + setLayout(new GridLayout(1, 2)); + + //name = new JLabel(_paramName); + //add(name, 0); + + JPanel type = new JPanel(new BorderLayout()); + + JPanel XYZ = new JPanel(new GridLayout(_dimension, 1)); + xaxis = new JRadioButton("X"); + xaxis.addActionListener(new Action() { + public void actionPerformed(ActionEvent e) { + yaxis.setSelected(false); + zaxis.setSelected(false); + for (ParameterRow r : rows) { + if (!r._paramName.equals(_paramName)) { + r.xaxis.setSelected(false); + } + } + dataUpdated = false; + fireSelectedDataChanged(_paramName + " xaxis"); + } + + public void setEnabled(boolean b) { + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + } + + public void putValue(String key, Object value) { + } + + public boolean isEnabled() { + return true; + } + + public Object getValue(String key) { + return null; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + } + }); + XYZ.add(xaxis); + yaxis = new JRadioButton("Y"); + yaxis.addActionListener(new Action() { + public void actionPerformed(ActionEvent e) { + xaxis.setSelected(false); + zaxis.setSelected(false); + for (ParameterRow r : rows) { + if (!r._paramName.equals(_paramName)) { + r.yaxis.setSelected(false); + } + } + dataUpdated = false; + fireSelectedDataChanged(_paramName + " yaxis"); + + } + + public void setEnabled(boolean b) { + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + } + + public void putValue(String key, Object value) { + } + + public boolean isEnabled() { + return true; + } + + public Object getValue(String key) { + return null; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + } + }); + if (_dimension >= 2) { + XYZ.add(yaxis); + } + + zaxis = new JRadioButton("Z"); + zaxis.addActionListener(new Action() { + public void actionPerformed(ActionEvent e) { + xaxis.setSelected(false); + yaxis.setSelected(false); + for (ParameterRow r : rows) { + if (!r._paramName.equals(_paramName)) { + r.zaxis.setSelected(false); + } + } + dataUpdated = false; + fireSelectedDataChanged(_paramName + " zaxis"); + } + + public void setEnabled(boolean b) { + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + } + + public void putValue(String key, Object value) { + } + + public boolean isEnabled() { + return true; + } + + public Object getValue(String key) { + return null; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + } + }); + if (_dimension == 3) { + XYZ.add(zaxis); + } + + type.add(XYZ, BorderLayout.WEST); + + if (_isNumber) { + parameter = new JPanel(); + parameter.setLayout(new GridLayout(2, 1)); + + min = new JSlider(1, _kernelDoubleValues.length, 1); + + min.setMinorTickSpacing(1); + min.setSnapToTicks(true); + min.setPaintTicks(true); + max = new JSlider(1, _kernelDoubleValues.length, _kernelDoubleValues.length); + max.setMinorTickSpacing(1); + max.setSnapToTicks(true); + max.setPaintTicks(true); + min.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + if (max.getValue() < min.getValue()) { + max.setValue(min.getValue()); + } + dataUpdated = false; + fireSelectedDataChanged(_paramName + " min"); + + } + }); + max.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + if (max.getValue() < min.getValue()) { + min.setValue(max.getValue()); + } + dataUpdated = false; + fireSelectedDataChanged(_paramName + " max"); + + } + }); + parameter.add(min, 0); + parameter.add(max, 1); + } else { + + list = new JList(_kernelStringValues); + list.setSelectedIndices(buildIntSeq(0, _kernelStringValues.size() - 1)); + list.addListSelectionListener(new ListSelectionListener() { + public void valueChanged(ListSelectionEvent e) { + dataUpdated = false; + fireSelectedDataChanged(_paramName + " list"); + } + }); + parameter = new JScrollPane(list); + } + type.add(parameter, BorderLayout.CENTER); + add(type, 1); + + setBorder(BorderFactory.createEtchedBorder()); + setPreferredSize(new Dimension(400, 60)); + + } + + int[] buildIntSeq(int min, int max) { + int[] seq = new int[max - min + 1]; + for (int i = 0; i < seq.length; i++) { + seq[i] = min + i; + } + return seq; + } + + boolean check(Object value) { + if (_isNumber) { + double dval = Double.valueOf(value.toString()); + return (dval >= _kernelDoubleValues[min.getValue() - 1] && dval <= _kernelDoubleValues[max.getValue() - 1]); + } else { + for (int i = 0; i < list.getSelectedIndices().length; i++) { + if (_kernelStringValues.get(list.getSelectedIndices()[i]).equals(value)) { + return true; + } + } + return false; + } + } + + } + + public static void main(String[] args) { + final PlotPanel pp = new Plot3DPanel(PlotPanel.WEST); + pp.setPreferredSize(new Dimension(400, 400)); + new FrameView(pp).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + Object[][] data = {{0, 0, 0, 0, "a0"}, {1, 1, 1, 1, "a1"}, {2, 2, 2, 2, "a2"}, {3, 3, 3, 3, "a3"}, {4, 3, 3, 3, "a3"}, {5, 3, 3, 3, "a4"}}; + + DataSelectTable dsp = new DataSelectTable(data, 3, "x1", "x2", "x3", "x4", "x5") { + private static final long serialVersionUID = 1L; + + @Override + public void fireSelectedDataChanged(String from) { + super.fireSelectedDataChanged(from); + pp.setAxisLabel(0, getSelectedXAxis()); + pp.setAxisLabel(1, getSelectedYAxis()); + pp.setAxisLabel(2, getSelectedZAxis()); + + if (pp.getPlots().size() == 0) { + pp.addPlot("SCATTER", "data", pp.mapData(getSelectedProjectedData())); + } else { + if (from.endsWith("axis")) { + pp.resetMapData(); + pp.removeAllPlots(); + pp.addPlot("SCATTER", "data", pp.mapData(getSelectedProjectedData())); + } else { + pp.getPlot(0).setData(pp.mapData(getSelectedProjectedData())); + } + } + //System.out.println(Array.cat(pp.getAxesScales())); + } + }; + new FrameView(dsp).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + /*try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + Object[][] data2 = { { 0, 0, 0, 0, "a0" }, { 1, 1, 1, 1, "a1" }, { 2, 2, 2, 2, "a2" }, { 3, 3, 3, 3, "a3" }, { 4, 3, 3, 3, "a3" }, + { 5, 3, 3, 3, "a4" }, { 5, 4, 3, 3, "a4" } }; + dsp.setData(data2);*/ + } + +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/FrameView.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/FrameView.java new file mode 100644 index 0000000..d7aa543 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/FrameView.java @@ -0,0 +1,66 @@ +package org.xbib.graphics.graph.jmathplot.panel; + +import org.xbib.graphics.graph.jmathplot.canvas.Plot2DCanvas; +import org.xbib.graphics.graph.jmathplot.canvas.Plot3DCanvas; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JPanel; + +/** + * BSD License + * + * @author Yann RICHET + */ +public class FrameView extends JFrame { + + private static final long serialVersionUID = 1L; + + public FrameView(Plot2DCanvas... canvas) { + JPanel panel = new JPanel(); + for (int i = 0; i < canvas.length; i++) { + panel.add(new Plot2DPanel(canvas[i])); + } + setContentPane(panel); + pack(); + setSize(600, 600); + setVisible(true); + } + + public FrameView(Plot3DCanvas... canvas) { + JPanel panel = new JPanel(); + for (int i = 0; i < canvas.length; i++) { + panel.add(new Plot3DPanel(canvas[i])); + } + setContentPane(panel); + pack(); + setSize(600, 600); + setVisible(true); + } + + public FrameView(String title, JComponent panel) { + super(title); + setContentPane(panel); + pack(); + setSize(600, 600); + setVisible(true); + } + + public FrameView(JComponent... panels) { + JPanel panel = new JPanel(); + for (int i = 0; i < panels.length; i++) { + panel.add(panels[i]); + } + setContentPane(panel); + pack(); + setSize(600, 600); + setVisible(true); + } + + public FrameView(JPanel panel) { + setContentPane(panel); + pack(); + setSize(600, 600); + setVisible(true); + } + +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/MatrixTablePanel.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/MatrixTablePanel.java new file mode 100644 index 0000000..49cf7f7 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/MatrixTablePanel.java @@ -0,0 +1,159 @@ +package org.xbib.graphics.graph.jmathplot.panel; + +import org.xbib.graphics.graph.jmathplot.io.files.ASCIIFile; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.BorderLayout; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.io.File; +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableModel; + +/** + * BSD License + * + * @author Yann RICHET + */ +public class MatrixTablePanel extends DataPanel { + + private static final long serialVersionUID = 1L; + private JTable table; + private TableModel model; + private Object[][] M; + private boolean viewHeaders = false; + private String[] headers; + + public MatrixTablePanel(Object[][] m) { + this(m, null); + } + + public MatrixTablePanel(Object[][] m, String[] headers) { + super(); + M = m; + if (headers == null) { + if (M.length == 0) { + this.headers = new String[0]; + } else { + this.headers = new String[M[0].length]; + } + } else { + viewHeaders = true; + this.headers = headers; + } + setModel(); + toWindow(); + } + + public void addSelectionListener(ListSelectionListener lsitener) { + ListSelectionModel listSelectionModel = table.getSelectionModel(); + listSelectionModel.addListSelectionListener(lsitener); + } + + public void removeSelectionListener(ListSelectionListener lsitener) { + ListSelectionModel listSelectionModel = table.getSelectionModel(); + listSelectionModel.removeListSelectionListener(lsitener); + } + + public void toClipBoard() { + try { + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(Array.cat(M)), null); + } catch (IllegalStateException e) { + JOptionPane.showConfirmDialog(null, "Copy to clipboard failed : " + e.getMessage(), "Error", JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE); + } + } + + public String getText() { + return M.toString(); + } + + public void toASCIIFile(File file) { + try { + ASCIIFile.write(file, Array.cat(M)); + } catch (NullPointerException e) { + // System.out.println("File not saved"); + } + } + + private void setModel() { + /*Double[][] array = null; + if (M.length != 0) { + array = new Double[M.length][M[0].length]; + for (int i = 0; i < array.length; i++) { + for (int j = 0; j < array[i].length; j++) { + array[i][j] = new Double(M[i][j]); + } + } + } else + array = new Double[0][0];*/ + + model = new DefaultTableModel(M, headers) { + + private static final long serialVersionUID = 1L; + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + }; + + } + + public void setHeaders(String[] h) { + if (M.length != 0) { + if (h.length != M[0].length) { + throw new IllegalArgumentException("Headers of the table must have " + M[0].length + " elements."); + } + } + + headers = h; + viewHeaders = true; + update(); + } + + public void update() { + setModel(); + super.update(); + } + + public void setMatrix(Object[][] m) { + M = m; + + if (M.length == 0) { + headers = new String[0]; + } else { + headers = new String[M[0].length]; + } + + update(); + } + + public void toWindow() { + table = new JTable(model); + + if (!viewHeaders) { + table.setTableHeader(null); + } + + table.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); + table.setRowSelectionAllowed(true); + table.setEnabled(true); + + scrollPane = new JScrollPane(table); + + /* + * scrollPane.setPreferredSize(getSize()); + * scrollPane.setSize(getSize()); + */ + + add(scrollPane, BorderLayout.CENTER); + } + + public Object[][] getMatrix() { + return M; + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/ParametersPanel.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/ParametersPanel.java new file mode 100644 index 0000000..ae15fc0 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/ParametersPanel.java @@ -0,0 +1,189 @@ +package org.xbib.graphics.graph.jmathplot.panel; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; + +/** + * BSD License + * + * @author Yann RICHET + */ + +public class ParametersPanel extends JPanel implements ActionListener { + + private static final long serialVersionUID = 1L; + + private Dimension defaultSize; + + private final String[] paramLabels; + + private final String[] paramValues; + + private final boolean[] isList; + + private final int[] paramValuesIndex; + + private final String[][] paramChoices; + + private JLabel[] labels; + + private JComboBox[] fields; + + private Runnable action; + + public ParametersPanel(String[] lab, String[] val) { + this(lab, new int[lab.length], new String[][]{val}); + } + + public ParametersPanel(String[] lab) { + this(lab, new String[lab.length]); + } + + public ParametersPanel(String[] lab, int[] selected, String[][] ch) { + paramLabels = lab; + + isList = new boolean[paramLabels.length]; + for (int i = 0; i < isList.length; i++) { + isList[i] = true; + } + + paramValuesIndex = selected; + + paramChoices = ch; + + paramValues = new String[paramLabels.length]; + for (int i = 0; i < paramChoices.length; i++) { + paramValues[i] = paramChoices[i][paramValuesIndex[i]]; + } + + setComponents(); + setAppearence(); + draw(); + } + + public ParametersPanel(String[] lab, String[][] ch) { + this(lab, new int[lab.length], ch); + } + + private void setComponents() { + labels = new JLabel[paramLabels.length]; + fields = new JComboBox[paramLabels.length]; + for (int i = 0; i < paramLabels.length; i++) { + labels[i] = new JLabel(paramLabels[i], JLabel.RIGHT); + if (isList[i]) { + fields[i] = new JComboBox(paramChoices[i]); + } else { + fields[i] = new JComboBox(); + } + fields[i].setEditable(!isList[i]); + } + defaultSize = new Dimension(400, paramLabels.length * 30); + } + + private void setAppearence() { + setPreferredSize(defaultSize); + setSize(defaultSize); + } + + private void update() { + updateValues(); + updateValuesIndex(); + } + + private void updateValues() { + for (int i = 0; i < paramLabels.length; i++) { + paramValues[i] = (String) (fields[i].getSelectedItem()); + } + } + + private void updateValuesIndex() { + for (int i = 0; i < paramLabels.length; i++) { + if (isList[i]) { + paramValuesIndex[i] = fields[i].getSelectedIndex(); + } + } + } + + public void actionPerformed(ActionEvent e) { + update(); + new Thread(action, "PanelParameters " + this.toString() + " selection").start(); + } + + public int getValueIndex(int i) { + if (!isList[i]) { + throw new IllegalArgumentException("This PanelParameter element is not set to give an Index."); + } + update(); + return paramValuesIndex[i]; + } + + public int[] getValuesIndex() { + update(); + return paramValuesIndex; + } + + public String[] getValues() { + update(); + return paramValues; + } + + public String getValue(int i) { + update(); + return paramValues[i]; + } + + public void setAction(Runnable t) { + action = t; + } + + private void buildConstraints(GridBagConstraints gbc, int gx, int gy, int gw, int gh, int wx, int wy) { + gbc.gridx = gx; + gbc.gridy = gy; + gbc.gridwidth = gw; + gbc.gridheight = gh; + gbc.weightx = wx; + gbc.weighty = wy; + } + + private void draw() { + JPanel panel = new JPanel(); + + GridBagLayout gbl = new GridBagLayout(); + GridBagConstraints c = new GridBagConstraints(); + panel.setLayout(gbl); + + for (int i = 0; i < paramLabels.length; i++) { + fields[i].addActionListener(this); + + // Ajout du panel de la chaine + buildConstraints(c, 0, i, 1, 1, 50, 20); + c.anchor = GridBagConstraints.EAST; + gbl.setConstraints(labels[i], c); + panel.add(labels[i]); + + // Ajout du panel de la chaine + buildConstraints(c, 1, i, 1, 1, 50, 20); + c.fill = GridBagConstraints.HORIZONTAL; + gbl.setConstraints(fields[i], c); + panel.add(fields[i]); + } + + JScrollPane scrollPane = new JScrollPane(panel); + + scrollPane.setPreferredSize(getSize()); + scrollPane.setSize(getSize()); + + setLayout(new BorderLayout()); + add(scrollPane, BorderLayout.CENTER); + + } + +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/Plot2DPanel.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/Plot2DPanel.java new file mode 100644 index 0000000..8f07366 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/Plot2DPanel.java @@ -0,0 +1,412 @@ +package org.xbib.graphics.graph.jmathplot.panel; + +import org.xbib.graphics.graph.jmathplot.canvas.Plot2DCanvas; +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; +import java.awt.Color; + +/** + * BSD License + * + * @author Yann RICHET + */ + +/** + * Class suitable for plotting 2D data on a panel, to be added to a swing + * container. + *

+ * Class for ascending compatibility + * + * @author Yann Richet + */ +public class Plot2DPanel extends PlotPanel { + + private static final long serialVersionUID = 1L; + + /** + * Constructor for Plot2DPanel. + * Create a new blank instance of Plot2DPanel, to be added to a swing + * component. + */ + public Plot2DPanel() { + super(new Plot2DCanvas()); + } + + public Plot2DPanel(double[] min, double[] max, String[] axesScales, String[] axesLabels) { + super(new Plot2DCanvas(min, max, axesScales, axesLabels)); + } + + public Plot2DPanel(PlotCanvas _canvas, String legendOrientation) { + super(_canvas, legendOrientation); + } + + public Plot2DPanel(PlotCanvas _canvas) { + super(_canvas); + } + + public Plot2DPanel(String legendOrientation) { + super(new Plot2DCanvas(), legendOrientation); + } + + /** + * Adds a scatter plot (each data point is plotted as a single dot + * marker) to the current plot panel. + * + * @param name Name for the plot, which will be used in the legend. + * (String) + * @param color Plot color. (Color) + * @param XY Pairs of array of double. First one contains the X position + * of the data points, second contains Y position. + *
+ * Each array of the pair + * must be of the same length; if not a ArrayIndexOutOfBoundsException + * exception will be thrown. + *
+ * Each data set must come in pair of + * array of double; if not a ArrayIndexOutOfBoundsException exception + * will be thrown. + * @return the index of the plot in the panel (int). + * @see #addLinePlot(String, Color, double[]...) + * @see #addBarPlot(String, Color, double[]...) + * @see #addBoxPlot(String, Color, double[][], double[][]) + * @see #addHistogramPlot(String, Color, double[][], double[]) + * @see #addStaircasePlot(String, Color, double[]...) + */ + public int addScatterPlot(String name, Color color, double[][] XY) { + return ((Plot2DCanvas) plotCanvas).addScatterPlot(name, color, XY); + } + + public int addScatterPlot(String name, Color color, double[] Y) { + return ((Plot2DCanvas) plotCanvas).addScatterPlot(name, color, Y); + } + + public int addScatterPlot(String name, Color color, double[] X, double[] Y) { + return ((Plot2DCanvas) plotCanvas).addScatterPlot(name, color, X, Y); + } + + public int addScatterPlot(String name, double[][] XY) { + return addScatterPlot(name, getNewColor(), XY); + } + + public int addScatterPlot(String name, double[] Y) { + return addScatterPlot(name, getNewColor(), Y); + } + + public int addScatterPlot(String name, double[] X, double[] Y) { + return addScatterPlot(name, getNewColor(), X, Y); + } + + /** + * Adds a line plot (each data point is connected to the next one by a + * solid line) to the current plot panel. + * + * @param name Name for the plot, which will be used in the legend. + * (String) + * @param color Plot color. (Color) + * @param XY Pairs of array of double. First one contains the X position + * of the data points, second contains Y position. + *
+ * Each array of the pair + * must be of the same length; if not a ArrayIndexOutOfBoundsException + * exception will be thrown. + *
+ * Each data set must come in pair of + * array of double; if not a ArrayIndexOutOfBoundsException exception + * will be thrown. + * @return the index of the plot in the panel (int). + * @see #addScatterPlot(String, Color, double[]...) + * @see #addBarPlot(String, Color, double[]...) + * @see #addBoxPlot(String, Color, double[]...) + * @see #addHistogramPlot(String, Color, double[]...) + * @see #addStaircasePlot(String, Color, double[]...) + */ + public int addLinePlot(String name, Color color, double[][] XY) { + return ((Plot2DCanvas) plotCanvas).addLinePlot(name, color, XY); + } + + public int addLinePlot(String name, Color color, double[] Y) { + return ((Plot2DCanvas) plotCanvas).addLinePlot(name, color, Y); + } + + public int addLinePlot(String name, Color color, double[] X, double[] Y) { + return ((Plot2DCanvas) plotCanvas).addLinePlot(name, color, X, Y); + } + + public int addLinePlot(String name, double[][] XY) { + return addLinePlot(name, getNewColor(), XY); + } + + public int addLinePlot(String name, double[] Y) { + return addLinePlot(name, getNewColor(), Y); + } + + public int addLinePlot(String name, double[] X, double[] Y) { + return addLinePlot(name, getNewColor(), X, Y); + } + + /** + * Adds a bar plot (each data point is shown as a dot marker connected to + * the horizontal axis by a vertical line) to the current plot panel. + * + * @param name Name for the plot, which will be used in the legend. + * (String) + * @param color Plot color. (Color) + * @param XY Pairs of array of double. First one contains the X position + * of the data points, second contains Y position. + *
+ * Each array of the pair + * must be of the same length; if not a ArrayIndexOutOfBoundsException + * exception will be thrown. + *
+ * Each data set must come in pair of + * array of double; if not a ArrayIndexOutOfBoundsException exception + * will be thrown. + * @return the index of the plot in the panel (int). + * @see #addScatterPlot(String, Color, double[]...) + * @see #addLinePlot(String, Color, double[]...) + * @see #addBoxPlot(String, Color, double[]...) + * @see #addHistogramPlot(String, Color, double[]...) + * @see #addStaircasePlot(String, Color, double[]...) + */ + public int addBarPlot(String name, Color color, double[][] XY) { + return ((Plot2DCanvas) plotCanvas).addBarPlot(name, color, XY); + } + + public int addBarPlot(String name, Color color, double[] Y) { + return ((Plot2DCanvas) plotCanvas).addBarPlot(name, color, Y); + } + + public int addBarPlot(String name, Color color, double[] X, double[] Y) { + return ((Plot2DCanvas) plotCanvas).addBarPlot(name, color, X, Y); + } + + public int addBarPlot(String name, double[][] XY) { + return addBarPlot(name, getNewColor(), XY); + } + + public int addBarPlot(String name, double[] Y) { + return addBarPlot(name, getNewColor(), Y); + } + + public int addBarPlot(String name, double[] X, double[] Y) { + return addBarPlot(name, getNewColor(), X, Y); + } + + /** + * Adds a staircase plot (each data point is connected to the following + * one by a horizontal line then a vertical line) to the current plot panel. + * + * @param name Name for the plot, which will be used in the legend. + * (String) + * @param color Plot color. (Color) + * @param XY Pairs of array of double. First one contains the X position + * of the data points, second contains Y position. + *
+ * Each array of the pair + * must be of the same length; if not a ArrayIndexOutOfBoundsException + * exception will be thrown. + *
+ * Each data set must come in pair of + * array of double; if not a ArrayIndexOutOfBoundsException exception + * will be thrown. + * @return the index of the plot in the panel (int). + * @see #addScatterPlot(String, Color, double[]...) + * @see #addBarPlot(String, Color, double[]...) + * @see #addBoxPlot(String, Color, double[][], double[][]) + * @see #addHistogramPlot(String, Color, double[][], double[]) + * @see #addLinePlot(String, Color, double[]...) + */ + public int addStaircasePlot(String name, Color color, double[][] XY) { + return ((Plot2DCanvas) plotCanvas).addStaircasePlot(name, color, XY); + } + + public int addStaircasePlot(String name, Color color, double[] Y) { + return ((Plot2DCanvas) plotCanvas).addStaircasePlot(name, color, Y); + } + + public int addStaircasePlot(String name, Color color, double[] X, double[] Y) { + return ((Plot2DCanvas) plotCanvas).addStaircasePlot(name, color, X, Y); + } + + public int addStaircasePlot(String name, double[][] XY) { + return addStaircasePlot(name, getNewColor(), XY); + } + + public int addStaircasePlot(String name, double[] Y) { + return addStaircasePlot(name, getNewColor(), Y); + } + + public int addStaircasePlot(String name, double[] X, double[] Y) { + return addStaircasePlot(name, getNewColor(), X, Y); + } + + /** + * Adds a box plot to the current plot panel. Each data point is plotted + * as a dot marker at the center of a rectangle. + * + * @param name Name for the plot, which will be used in the legend. + * (String) + * @param color Plot color. (Color) + * @param XY m*2 array of array of double. Contains the x,y coordinates of the + * m boxes' center (m lines, 2 rows). + * @param dXdY m*2 array of array of double. Contains the width and heigth of the + * m boxes (m lines, 2 rows). + * @return the index of the plot in the panel (int). + * @see #addScatterPlot(String, Color, double[]...) + * @see #addBarPlot(String, Color, double[]...) + * @see #addStaircasePlot(String, Color, double[]...) + * @see #addHistogramPlot(String, Color, double[][], double[]) + * @see #addLinePlot(String, Color, double[]...) + */ + public int addBoxPlot(String name, Color color, double[][] XY, double[][] dXdY) { + return ((Plot2DCanvas) plotCanvas).addBoxPlot(name, color, XY, dXdY); + } + + public int addBoxPlot(String name, double[][] XY, double[][] dXdY) { + return addBoxPlot(name, getNewColor(), XY, dXdY); + } + + /** + * Adds a box plot to the current plot panel. Each data point is plotted + * as a dot marker at the center of a rectangle. + * + * @param name Name for the plot, which will be used in the legend. + * (String) + * @param color Plot color. (Color) + * @param XYdXdY m*4 array of array of double. Contains the x,y coordinates of the + * m boxes' center and the boxes width and heigth (m lines, 4 rows). + * @return the index of the plot in the panel (int). + * @see #addScatterPlot(String, Color, double[]...) + * @see #addBarPlot(String, Color, double[]...) + * @see #addStaircasePlot(String, Color, double[]...) + * @see #addHistogramPlot(String, Color, double[][], double[]) + * @see #addLinePlot(String, Color, double[]...) + */ + public int addBoxPlot(String name, Color color, double[][] XYdXdY) { + return ((Plot2DCanvas) plotCanvas).addBoxPlot(name, color, XYdXdY); + } + + public int addBoxPlot(String name, double[][] XYdXdY) { + return addBoxPlot(name, getNewColor(), XYdXdY); + } + + /** + * Adds a histogram plot to the current plot panel. Each data point is as + * vertical bar which width can be set. + * + * @param name Name for the plot, which will be used in the legend. + * (String) + * @param color Plot color. (Color) + * @param XY m*2 array of array of double. Contains the x coordinate and + * the heigth of each bar (m lines, 2 rows). + * @param dX Array of double. Contains the width each bar (m lines). + * @return the index of the plot in the panel (int). + * @see #addScatterPlot(String, Color, double[]...) + * @see #addBarPlot(String, Color, double[]...) + * @see #addStaircasePlot(String, Color, double[]...) + * @see #addBoxPlot(String, Color, double[][]) + * @see #addLinePlot(String, Color, double[]...) + */ + public int addHistogramPlot(String name, Color color, double[][] XY, double[] dX) { + return ((Plot2DCanvas) plotCanvas).addHistogramPlot(name, color, XY, dX); + } + + public int addHistogramPlot(String name, double[][] XY, double[] dX) { + return addHistogramPlot(name, getNewColor(), XY, dX); + } + + public int addHistogramPlot(String name, Color color, double[][] XYdX) { + return ((Plot2DCanvas) plotCanvas).addHistogramPlot(name, color, XYdX); + } + + /** + * Adds a histogram plot to the current plot panel. Each data point is as + * vertical bar which width can be set. + * + * @param name Name for the plot, which will be used in the legend. + * (String) + * @param XYdX m*3 array of array of double. Contains the x coordinate, + * the heigth of each bar and the width of each bar (m lines, 3 rows). + * @return the index of the plot in the panel (int). + * @see #addScatterPlot(String, Color, double[]...) + * @see #addBarPlot(String, Color, double[]...) + * @see #addStaircasePlot(String, Color, double[]...) + * @see #addBoxPlot(String, Color, double[][]) + * @see #addLinePlot(String, Color, double[]...) + */ + public int addHistogramPlot(String name, double[][] XYdX) { + return addHistogramPlot(name, getNewColor(), XYdX); + } + + /** + * Adds a plot of the statistical repartition of a sample, as a histogram. + * + * @param name Name for the plot, which will be used in the legend. + * (String) + * @param color Plot color. (Color) + * @param sample Array of double containing the data which statistics will be plotted. + * @param n Bin number for the statistics (int). + * @return the index of the plot in the panel (int). + */ + public int addHistogramPlot(String name, Color color, double[] sample, int n) { + return ((Plot2DCanvas) plotCanvas).addHistogramPlot(name, color, sample, n); + } + + public int addHistogramPlot(String name, double[] X, int n) { + return addHistogramPlot(name, getNewColor(), X, n); + } + + /** + * Adds a plot of the statistical repartition of a sample, as a histogram. + * The bins' limits can be set. + * + * @param name Name for the plot, which will be used in the legend. + * (String) + * @param color Plot color. (Color) + * @param sample Array of double containing the data which statistics will be plotted. + * @param bounds Specify the limits for the bins' boundaries. + * @return the index of the plot in the panel (int). + */ + public int addHistogramPlot(String name, Color color, double[] sample, double... bounds) { + return ((Plot2DCanvas) plotCanvas).addHistogramPlot(name, color, sample, bounds); + } + + public int addHistogramPlot(String name, double[] X, double... bounds) { + return addHistogramPlot(name, getNewColor(), X, bounds); + } + + public int addHistogramPlot(String name, Color color, double[] X, double min, double max, int n) { + return ((Plot2DCanvas) plotCanvas).addHistogramPlot(name, color, X, min, max, n); + } + + public int addHistogramPlot(String name, double[] X, double min, double max, int n) { + return addHistogramPlot(name, getNewColor(), X, min, max, n); + } + + public int addCloudPlot(String name, Color color, double[][] sampleXY, int nX, int nY) { + return ((Plot2DCanvas) plotCanvas).addCloudPlot(name, color, sampleXY, nX, nY); + } + + public int addCloudPlot(String name, double[][] sampleXY, int nX, int nY) { + return addCloudPlot(name, getNewColor(), sampleXY, nX, nY); + } + + @Override + public int addPlot(String type, String name, Color color, double[]... XY) { + if (type.equalsIgnoreCase(SCATTER)) { + return addScatterPlot(name, color, XY); + } else if (type.equalsIgnoreCase(LINE)) { + return addLinePlot(name, color, XY); + } else if (type.equalsIgnoreCase(BAR)) { + return addBarPlot(name, color, XY); + } else if (type.equalsIgnoreCase(STAIRCASE)) { + return addStaircasePlot(name, color, XY); + } else if (type.equalsIgnoreCase(HISTOGRAM)) { + return addHistogramPlot(name, color, XY); + } else if (type.equalsIgnoreCase(BOX)) { + return addBoxPlot(name, color, XY); + } else { + throw new IllegalArgumentException("Plot type is unknown : " + type); + } + } + +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/Plot3DPanel.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/Plot3DPanel.java new file mode 100644 index 0000000..5d2e9bd --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/Plot3DPanel.java @@ -0,0 +1,249 @@ +package org.xbib.graphics.graph.jmathplot.panel; + +import org.xbib.graphics.graph.jmathplot.canvas.Plot3DCanvas; +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.Color; + +public class Plot3DPanel extends PlotPanel { + + public void setDefaultZoom(double zoom_factor) { + ((Plot3DCanvas) plotCanvas).setDefaultZoom(zoom_factor); + repaint(); + } + + public Plot3DPanel() { + super(new Plot3DCanvas()); + } + + public Plot3DPanel(double[] min, double[] max, String[] axesScales, String[] axesLabels) { + super(new Plot3DCanvas(min, max, axesScales, axesLabels)); + } + + public Plot3DPanel(PlotCanvas _canvas, String legendOrientation) { + super(_canvas, legendOrientation); + } + + public Plot3DPanel(PlotCanvas _canvas) { + super(_canvas); + } + + public Plot3DPanel(String legendOrientation) { + super(new Plot3DCanvas(), legendOrientation); + } + + /** + * Adds a scatter plot (each data point is plotted as a single dot + * marker) to the current plot panel. + * @param name Name for the plot, which will be used in the legend. + * (String) + * @param color Plot color. (Color) + * @param XY Array of triple double. For each triple, first one contains the X position + * of the data points, second contains Y position, third contains Z position. + *
+ * Each array of the triple + * must be of the same length; if not a ArrayIndexOutOfBoundsException + * exception will be thrown. + *
+ * Each data set must come in + * array of triple of double; if not a ArrayIndexOutOfBoundsException exception + * will be thrown. + * @return the index of the plot in the panel (int). + * @see #addLinePlot(String, Color, double[]...) + * @see #addBarPlot(String, Color, double[]...) + * @see #addBoxPlot(String, Color, double[][], double[][]) + */ + public int addScatterPlot(String name, Color color, double[][] XY) { + return ((Plot3DCanvas) plotCanvas).addScatterPlot(name, color, XY); + } + + public int addScatterPlot(String name, Color color, double[] X, double[] Y, double[] Z) { + return ((Plot3DCanvas) plotCanvas).addScatterPlot(name, color, X, Y, Z); + } + + public int addScatterPlot(String name, double[][] XY) { + return addScatterPlot(name, getNewColor(), XY); + } + + public int addScatterPlot(String name, double[] X, double[] Y, double[] Z) { + return addScatterPlot(name, getNewColor(), X, Y, Z); + } + + /** + * Adds a line plot (each data point is connected to the next one by a + * solid line) to the current plot panel. + * @param name Name for the plot, which will be used in the legend. + * (String) + * @param color Plot color. (Color) + * @param XY Array of triple double. For each triple, first one contains the X position + * of the data points, second contains Y position, third contains Z position. + *
+ * Each array of the triple + * must be of the same length; if not a ArrayIndexOutOfBoundsException + * exception will be thrown. + *
+ * Each data set must come in + * array of triple of double; if not a ArrayIndexOutOfBoundsException exception + * will be thrown. + * @return the index of the plot in the panel (int). + * @see #addScatterPlot(String, Color, double[]...) + * @see #addBarPlot(String, Color, double[]...) + * @see #addBoxPlot(String, Color, double[]...) + */ + public int addLinePlot(String name, Color color, double[][] XY) { + return ((Plot3DCanvas) plotCanvas).addLinePlot(name, color, XY); + } + + public int addLinePlot(String name, Color color, double[] X, double[] Y, double[] Z) { + return ((Plot3DCanvas) plotCanvas).addLinePlot(name, color, X, Y, Z); + } + + public int addLinePlot(String name, double[][] XY) { + return addLinePlot(name, getNewColor(), XY); + } + + public int addLinePlot(String name, double[] X, double[] Y, double[] Z) { + return addLinePlot(name, getNewColor(), X, Y, Z); + } + + /** + * Adds a bar plot (each data point is shown as a dot marker connected to + * the horizontal axis by a vertical line) to the current plot panel. + * @param name Name for the plot, which will be used in the legend. + * (String) + * @param color Plot color. (Color) + * @param XY Array of triple double. For each triple, first one contains the X position + * of the data points, second contains Y position, third contains Z position. + *
+ * Each array of the triple + * must be of the same length; if not a ArrayIndexOutOfBoundsException + * exception will be thrown. + *
+ * Each data set must come in + * array of triple of double; if not a ArrayIndexOutOfBoundsException exception + * will be thrown. + * @return the index of the plot in the panel (int). + * @see #addScatterPlot(String, Color, double[]...) + * @see #addLinePlot(String, Color, double[]...) + * @see #addBoxPlot(String, Color, double[]...) + */ + public int addBarPlot(String name, Color color, double[][] XY) { + return ((Plot3DCanvas) plotCanvas).addBarPlot(name, color, XY); + } + + public int addBarPlot(String name, Color color, double[] X, double[] Y, double[] Z) { + return ((Plot3DCanvas) plotCanvas).addBarPlot(name, color, X, Y, Z); + } + + public int addBarPlot(String name, double[][] XY) { + return addBarPlot(name, getNewColor(), XY); + } + + public int addBarPlot(String name, double[] X, double[] Y, double[] Z) { + return addBarPlot(name, getNewColor(), X, Y, Z); + } + + public int addBoxPlot(String name, Color c, double[][] XY, double[][] dX) { + return ((Plot3DCanvas) plotCanvas).addBoxPlot(name, c, XY, dX); + } + + public int addBoxPlot(String name, double[][] XY, double[][] dX) { + return addBoxPlot(name, getNewColor(), XY, dX); + } + + public int addBoxPlot(String name, Color c, double[][] XYdX) { + return ((Plot3DCanvas) plotCanvas).addBoxPlot(name, c, Array.getColumnsRangeCopy(XYdX, 0, 2), Array.getColumnsRangeCopy(XYdX, 3, 5)); + } + + public int addBoxPlot(String name, double[][] XYdX) { + return addBoxPlot(name, getNewColor(), XYdX); + } + + public int addHistogramPlot(String name, Color c, double[][] XY, double[][] dX) { + return ((Plot3DCanvas) plotCanvas).addHistogramPlot(name, c, XY, dX); + } + + public int addHistogramPlot(String name, double[][] XY, double[][] dX) { + return addHistogramPlot(name, getNewColor(), XY, dX); + } + + public int addHistogramPlot(String name, Color c, double[][] XYdX) { + return ((Plot3DCanvas) plotCanvas).addHistogramPlot(name, c, Array.getColumnsRangeCopy(XYdX, 0, 2), Array.getColumnsRangeCopy(XYdX, 3, 4)); + } + + public int addHistogramPlot(String name, double[][] XYdX) { + return addHistogramPlot(name, getNewColor(), XYdX); + } + + public int addHistogramPlot(String name, Color c, double[][] XY, int nX, int nY) { + return ((Plot3DCanvas) plotCanvas).addHistogramPlot(name, c, XY, nX, nY); + } + + public int addHistogramPlot(String name, double[][] XY, int nX, int nY) { + return addHistogramPlot(name, getNewColor(), XY, nX, nY); + } + + public int addHistogramPlot(String name, Color c, double[][] XY, double[] boundsX, double[] boundsY) { + return ((Plot3DCanvas) plotCanvas).addHistogramPlot(name, c, XY, boundsX, boundsY); + } + + public int addHistogramPlot(String name, double[][] XY, double[] boundsX, double[] boundsY) { + return addHistogramPlot(name, getNewColor(), XY, boundsX, boundsY); + } + + public int addHistogramPlot(String name, Color c, double[][] XY, double minX, double maxX, int nX, double minY, double maxY, int nY) { + return ((Plot3DCanvas) plotCanvas).addHistogramPlot(name, c, XY, minX, maxX, nX, minY, maxY, nY); + } + + public int addHistogramPlot(String name, double[][] XY, double minX, double maxX, int nX, double minY, double maxY, int nY) { + return addHistogramPlot(name, getNewColor(), XY, minX, maxX, nX, minY, maxY, nY); + } + + public int addGridPlot(String name, Color c, double[] X, double[] Y, double[][] Z) { + return ((Plot3DCanvas) plotCanvas).addGridPlot(name, c, X, Y, Z); + } + + public int addGridPlot(String name, double[] X, double[] Y, double[][] Z) { + return addGridPlot(name, getNewColor(), X, Y, Z); + } + + public int addGridPlot(String name, Color c, double[][] XYZMatrix) { + return ((Plot3DCanvas) plotCanvas).addGridPlot(name, c, XYZMatrix); + } + + public int addGridPlot(String name, double[][] XYZMatrix) { + return addGridPlot(name, getNewColor(), XYZMatrix); + } + + public int addCloudPlot(String name, Color color, double[][] sampleXYZ, int nX, int nY, int nZ) { + return ((Plot3DCanvas) plotCanvas).addCloudPlot(name, color, sampleXYZ, nX, nY, nZ); + } + + public int addCloudPlot(String name, double[][] sampleXYZ, int nX, int nY, int nZ) { + return addCloudPlot(name, getNewColor(), sampleXYZ, nX, nY, nZ); + } + + @Override + public int addPlot(String type, String name, Color c, double[]... XY) { + if (type.equalsIgnoreCase(SCATTER)) { + return addScatterPlot(name, c, XY); + } else if (type.equalsIgnoreCase(LINE)) { + return addLinePlot(name, c, XY); + } else if (type.equalsIgnoreCase(BAR)) { + return addBarPlot(name, c, XY); + } else if (type.equalsIgnoreCase(HISTOGRAM)) { + return addHistogramPlot(name, c, XY); + } else if (type.equalsIgnoreCase(BOX)) { + return addBoxPlot(name, c, XY); + } else if (type.equalsIgnoreCase(GRID)) { + return addGridPlot(name, c, XY); + } else { + throw new IllegalArgumentException("Plot type is unknown : " + type); + } + } + + public void rotate(double theta, double phi) { + ((Plot3DCanvas) plotCanvas).rotate(theta, phi); + repaint(); + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/PlotPanel.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/PlotPanel.java new file mode 100644 index 0000000..2c60632 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/panel/PlotPanel.java @@ -0,0 +1,696 @@ +package org.xbib.graphics.graph.jmathplot.panel; + +import org.xbib.graphics.graph.jmathplot.io.files.ASCIIFile; +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; +import org.xbib.graphics.graph.jmathplot.frame.LegendPanel; +import org.xbib.graphics.graph.jmathplot.frame.PlotToolBar; +import org.xbib.graphics.graph.jmathplot.Axis; +import org.xbib.graphics.graph.jmathplot.Plotable; +import org.xbib.graphics.graph.jmathplot.Plot; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; +import javax.imageio.ImageIO; +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javax.swing.JPanel; + +/** + * BSD License + * + * @author Yann RICHET + */ +public abstract class PlotPanel extends JPanel { + + private static final long serialVersionUID = 1L; + public PlotToolBar plotToolBar; + public PlotCanvas plotCanvas; + public LegendPanel plotLegend; + public final static String EAST = BorderLayout.EAST; + public final static String SOUTH = BorderLayout.SOUTH; + public final static String NORTH = BorderLayout.NORTH; + public final static String WEST = BorderLayout.WEST; + public final static String INVISIBLE = "INVISIBLE"; + public final static String SCATTER = "SCATTER"; + public final static String LINE = "LINE"; + public final static String BAR = "BAR"; + public final static String HISTOGRAM = "HISTOGRAM"; + public final static String BOX = "BOX"; + public final static String STAIRCASE = "STAIRCASE"; + public final static String GRID = "GRID"; + public final static Color[] COLORLIST = {Color.BLUE, Color.RED, Color.GREEN, Color.YELLOW, Color.ORANGE, Color.PINK, Color.CYAN, Color.MAGENTA}; + private Font font = new Font("Arial", Font.PLAIN, 10); + + public PlotPanel(PlotCanvas _canvas, String legendOrientation) { + plotCanvas = _canvas; + setBackground(Color.WHITE); + setLayout(new BorderLayout()); + + addPlotToolBar(NORTH); + + addLegend(legendOrientation); + + add(plotCanvas, BorderLayout.CENTER); + } + + public PlotPanel(PlotCanvas _canvas) { + this(_canvas, INVISIBLE); + } + + /** + * Defines where the legend of the plot should be added to the plot + * panel. + * + * @param location Location where should be put the legend (String). + * location can have the following values (case insensitive): EAST, + * SOUTH, WEST, NORTH, INVISIBLE (legend will be hidden in this case). + * Any other value will be ignored and an error message will be sent to + * the error output. + */ + public void addLegend(String location) { + if (location.equalsIgnoreCase(EAST)) { + plotLegend = new LegendPanel(this, LegendPanel.VERTICAL); + add(plotLegend, EAST); + } else if (location.equalsIgnoreCase(SOUTH)) { + plotLegend = new LegendPanel(this, LegendPanel.HORIZONTAL); + add(plotLegend, SOUTH); + } else if (location.equalsIgnoreCase(WEST)) { + plotLegend = new LegendPanel(this, LegendPanel.VERTICAL); + add(plotLegend, WEST); + } else if (location.equalsIgnoreCase(NORTH)) { + plotLegend = new LegendPanel(this, LegendPanel.HORIZONTAL); + add(plotLegend, NORTH); + } else if (location.equalsIgnoreCase(INVISIBLE)) { + plotLegend = new LegendPanel(this, LegendPanel.INVISIBLE); + // add(legends, BorderLayout.NORTH); + } else { + System.err.println("Orientation " + location + " is unknown."); + } + } + + /** + * Removes the current legend from the plot panel. + */ + public void removeLegend() { + remove(plotLegend); + } + + /** + * Moves the legend to the specified location. + * + * @param location Location where should be put the legend (String). + * location can have the following values (case insensitive): EAST, + * SOUTH, WEST, NORTH, INVISIBLE (legend will be hidden in this case). + * Any other value will be ignored and an error message will be sent to + * the error output. + */ + public void setLegendOrientation(String location) { + removeLegend(); + addLegend(location); + } + + /** + * Adds a new plot toolbar to the specified location. The previous toolbar + * is deleted. + * + * @param location Location where should be put the toolbar (String). + * location can have the following values (case insensitive): EAST, + * SOUTH, WEST, NORTH. + * Any other value will be ignored and an error message will be sent to + * the error output. + */ + public void addPlotToolBar(String location) { + if (location.equalsIgnoreCase(EAST)) { + removePlotToolBar(); + plotToolBar = new PlotToolBar(this); + plotToolBar.setFloatable(false); + add(plotToolBar, EAST); + } else if (location.equalsIgnoreCase(SOUTH)) { + removePlotToolBar(); + plotToolBar = new PlotToolBar(this); + plotToolBar.setFloatable(false); + add(plotToolBar, SOUTH); + } else if (location.equalsIgnoreCase(WEST)) { + removePlotToolBar(); + plotToolBar = new PlotToolBar(this); + plotToolBar.setFloatable(false); + add(plotToolBar, WEST); + } else if (location.equalsIgnoreCase(NORTH)) { + removePlotToolBar(); + plotToolBar = new PlotToolBar(this); + plotToolBar.setFloatable(false); + add(plotToolBar, NORTH); + } + } + + /** + * Removes the plot toolbar from the panel. + */ + public void removePlotToolBar() { + if (plotToolBar == null) { + return; + } + remove(plotToolBar); + } + + /** + * Moves the plot toolbar to the specified location. + * + * @param location Location where should be put the toolbar (String). + * location can have the following values (case insensitive): EAST, + * SOUTH, WEST, NORTH. + * Any other value will be ignored and an error message will be sent to + * the error output. + */ + public void setPlotToolBarOrientation(String location) { + addPlotToolBar(location); + } + + public PlotToolBar getPlotToolBar() { + return plotToolBar; + } + + public void setAdjustBounds(boolean adjust) { + plotCanvas.setAdjustBounds(adjust); + if (plotToolBar != null) { + plotToolBar.ajustBoundsChanged(); + } + } + + // /////////////////////////////////////////// + // ////// set actions //////////////////////// + // /////////////////////////////////////////// + public void setActionMode(int am) { + plotCanvas.setActionMode(am); + } + + public void setNoteCoords(boolean b) { + plotCanvas.setNoteCoords(b); + } + + public void setEditable(boolean b) { + plotCanvas.setEditable(b); + } + + public boolean getEditable() { + return plotCanvas.getEditable(); + } + + public void setNotable(boolean b) { + plotCanvas.setNotable(b); + } + + public boolean getNotable() { + return plotCanvas.getNotable(); + } + + // /////////////////////////////////////////// + // ////// set/get elements /////////////////// + // /////////////////////////////////////////// + public LinkedList getPlots() { + return plotCanvas.getPlots(); + } + + public Plot getPlot(int i) { + return plotCanvas.getPlot(i); + } + + public int getPlotIndex(Plot p) { + return plotCanvas.getPlotIndex(p); + } + + public LinkedList getPlotables() { + return plotCanvas.getPlotables(); + } + + public Plotable getPlotable(int i) { + return plotCanvas.getPlotable(i); + } + + /** + * Return the axis specified in parameter. + * + * @param i Axis number. 0 for X, 1 for Y, 2 for Z. + * @return The axis which number is given in parameter. + */ + public Axis getAxis(int i) { + return plotCanvas.getGrid().getAxis(i); + } + + /** + * Returns the scaling for all of the axis of the plot. + * + * @return An array of String + */ + public String[] getAxisScales() { + return plotCanvas.getAxisScales(); + } + + // TODO axes labels are rested after addPlot... correct this. + + /** + * Sets the name of the axis, in this order: X, Y and Z. + * + * @param labels One to three strings containing the name of each axis. + */ + public void setAxisLabels(String... labels) { + plotCanvas.setAxisLabels(labels); + } + + /** + * Sets the name of the axis specified in parameter. + * + * @param axe Axis number. 0 for X, 1 for Y, 2 for Z. + * @param label Name to be given. + */ + public void setAxisLabel(int axe, String label) { + plotCanvas.setAxisLabel(axe, label); + } + + /** + * Sets the scale of the axes, linear or logarithm, in this order: X,Y,Z. + * + * @param scales Strings containing the scaling, LOG or LIN (case insensitive) for the axes. + */ + public void setAxisScales(String... scales) { + plotCanvas.setAxisScales(scales); + } + + /** + * Sets the scaling of the specified axis. + * + * @param axe Axis number. 0 for X, 1 for Y, 2 for Z. + * @param scale String specifying the scaling. LIN or LOG, case insensitive. + */ + public void setAxisScale(int axe, String scale) { + plotCanvas.setAxiScale(axe, scale); + } + + /** + * Sets the boundaries for each axis. + * + * @param min Array of at most 3 doubles specifying the min bound of each axis, in this order: X,Y,Z. + * @param max Array of at most 3 doubles specifying the max bound of each axis, in this order: X,Y,Z. + */ + public void setFixedBounds(double[] min, double[] max) { + plotCanvas.setFixedBounds(min, max); + } + + /** + * Sets the boundaries for the specified axis. + * + * @param axe Axis number to modify. 0 for X, 1 for Y, 2 for Z. + * @param min Min bound of the axis. + * @param max Max bound of the axis. + */ + public void setFixedBounds(int axe, double min, double max) { + plotCanvas.setFixedBounds(axe, min, max); + } + + /** + * Modify bounds of the axes so as to include the point given in parameter. + * + * @param into Coords of the point to include in bounds. + */ + public void includeInBounds(double... into) { + plotCanvas.includeInBounds(into); + } + + /** + * Modify axes boundaries so as to include all the points of a given plot. + * + * @param plot Plot to include. + */ + public void includeInBounds(Plot plot) { + plotCanvas.includeInBounds(plot); + } + + /** + * Set bounds automatically. + */ + public void setAutoBounds() { + plotCanvas.setAutoBounds(); + } + + /** + * Set bounds automatically for one axis. + * + * @param axe Number of the axis to modify. 0 for X, 1 for Y, 2 for Z. + */ + public void setAutoBounds(int axe) { + plotCanvas.setAutoBounds(axe); + } + + public double[][] mapData(Object[][] stringdata) { + return plotCanvas.mapData(stringdata); + } + + public void resetMapData() { + plotCanvas.resetMapData(); + } + + // /////////////////////////////////////////// + // ////// add/remove elements //////////////// + // /////////////////////////////////////////// + public void addLabel(String text, Color c, double... where) { + plotCanvas.addLabel(text, c, where); + } + + public void addBaseLabel(String text, Color c, double... where) { + plotCanvas.addBaseLabel(text, c, where); + } + + public void addPlotable(Plotable p) { + plotCanvas.addPlotable(p); + } + + public void removePlotable(Plotable p) { + plotCanvas.removePlotable(p); + } + + public void removePlotable(int i) { + plotCanvas.removePlotable(i); + } + + public void removeAllPlotables() { + plotCanvas.removeAllPlotables(); + } + + public int addPlot(Plot newPlot) { + return plotCanvas.addPlot(newPlot); + } + + protected Color getNewColor() { + return COLORLIST[plotCanvas.plots.size() % COLORLIST.length]; + } + + public int addPlot(String type, String name, double[]... v) { + return addPlot(type, name, getNewColor(), v); + } + + public abstract int addPlot(String type, String name, Color c, double[]... v); + + public void setPlot(int I, Plot p) { + plotCanvas.setPlot(I, p); + } + + public void changePlotData(int I, double[]... XY) { + plotCanvas.changePlotData(I, XY); + } + + public void changePlotName(int I, String name) { + plotCanvas.changePlotName(I, name); + } + + public void changePlotColor(int I, Color c) { + plotCanvas.changePlotColor(I, c); + } + + public void removePlot(int I) { + plotCanvas.removePlot(I); + } + + public void removePlot(Plot p) { + plotCanvas.removePlot(p); + } + + public void removeAllPlots() { + plotCanvas.removeAllPlots(); + } + + public void addVectortoPlot(int numPlot, double[][] v) { + plotCanvas.addVectortoPlot(numPlot, v); + } + + public void addQuantiletoPlot(int numPlot, int numAxe, double rate, boolean symetric, double[] q) { + plotCanvas.addQuantiletoPlot(numPlot, numAxe, rate, symetric, q); + } + + public void addQuantiletoPlot(int numPlot, int numAxe, double rate, boolean symetric, double q) { + plotCanvas.addQuantiletoPlot(numPlot, numAxe, rate, symetric, q); + } + + public void addQuantilestoPlot(int numPlot, int numAxe, double[][] q) { + plotCanvas.addQuantilestoPlot(numPlot, numAxe, q); + } + + public void addQuantilestoPlot(int numPlot, int numAxe, double[] q) { + plotCanvas.addQuantilestoPlot(numPlot, numAxe, q); + } + + public void addGaussQuantilestoPlot(int numPlot, int numAxe, double[] s) { + plotCanvas.addGaussQuantilestoPlot(numPlot, numAxe, s); + } + + public void addGaussQuantilestoPlot(int numPlot, int numAxe, double s) { + plotCanvas.addGaussQuantilestoPlot(numPlot, numAxe, s); + } + + public void toGraphicFile(File file) throws IOException { + // otherwise toolbar appears + plotToolBar.setVisible(false); + + Image image = createImage(getWidth(), getHeight()); + paint(image.getGraphics()); + image = new ImageIcon(image).getImage(); + + BufferedImage bufferedImage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_RGB); + Graphics g = bufferedImage.createGraphics(); + g.drawImage(image, 0, 0, Color.WHITE, null); + g.dispose(); + + // make it reappear + plotToolBar.setVisible(true); + + try { + ImageIO.write(bufferedImage, "PNG", file); + } catch (IllegalArgumentException ex) { + } + } + + public static void main(String[] args) { + String man = "Usage: jplot. <-2D|-3D> [-l ] [options] [[options] other ASCII file]\n" + "[-l ] giving the legend position\n" + "[options] are:\n" + " -t )|HISTOGRAM3D(,)|GRID3D|CLOUD2D(,)|CLOUD3D(,,)> type of the plot\n" + " SCATTER|LINE|BAR: each line of the ASCII file contains coordinates of one point.\n" + " HISTOGRAM2D(): ASCII file contains the 1D sample (i.e. m=1) to split in h slices.\n" + " HISTOGRAM3D(,): ASCII file contains the 2D sample (i.e. m=2) to split in h*k slices (h slices on X axis and k slices on Y axis).\n" + " GRID3D: ASCII file is a matrix, first row gives n X grid values, first column gives m Y grid values, other values are Z values.\n" + " CLOUD2D(,): ASCII file contains the 2D sample (i.e. m=2) to split in h*k slices (h slices on X axis and k slices on Y axis), density of cloud corresponds to frequency of X-Y slice in given 2D sample.\n" + " CLOUD3D(,,): ASCII file contains the 3D sample (i.e. m=3) to split in h*k*l slices (h slices on X axis, k slices on Y axis, l slices on Y axis), density of cloud corresponds to frequency of X-Y-Z slice in given 3D sample.\n" + " -n name name of the plot\n" + " -v vector data to add to the plot\n" + " -q() Q-quantile to add to the plot on axis. Each line of the given ASCII file contains the value of quantile for probvability Q.\n" + " -qP p-quantiles density to add to the plot on axis. Each line of the given ASCII file contains p values.\n" + " -qN Gaussian density to add to the plot on axis. Each line of the given ASCII file contains a standard deviation."; + + if (args.length == 0) { + double[][] data = new double[20][]; + for (int i = 0; i < data.length; i++) { + data[i] = new double[]{Math.random(), Math.random(), Math.random()}; + } + ASCIIFile.writeDoubleArray(new File("tmp.dat"), data); + + args = new String[]{"-3D", "-l", "SOUTH", "-t", "SCATTER", "tmp.dat"}; + System.out.println(man); + System.out.println("\nExample: jplot. " + Array.cat(args)); + } + + PlotPanel p = null; + if (args[0].equals("-2D")) { + p = new Plot2DPanel(); + } else if (args[0].equals("-3D")) { + p = new Plot3DPanel(); + } else { + System.out.println(man); + } + + try { + + String leg = "INVISIBLE"; + String type = SCATTER; + String name = ""; + + double[][] v = null; + + double[] qX = null; + double[] qY = null; + double[] qZ = null; + double qXp = 0; + double qYp = 0; + double qZp = 0; + + double[][] qPX = null; + double[][] qPY = null; + double[][] qPZ = null; + + double[] qNX = null; + double[] qNY = null; + double[] qNZ = null; + + for (int i = 1; i < args.length; i++) { + //System.out.println("<" + args[i] + ">"); + if (args[i].equals("-l")) { + leg = args[i + 1]; + i++; + } else if (args[i].equals("-t")) { + type = args[i + 1]; + i++; + } else if (args[i].equals("-n")) { + name = args[i + 1]; + i++; + } else if (args[i].equals("-v")) { + v = ASCIIFile.readDoubleArray(new File(args[i + 1])); + i++; + } else if (args[i].startsWith("-qX(")) { + qX = ASCIIFile.readDouble1DArray(new File(args[i + 1])); + qXp = Double.parseDouble(args[i].substring(4, args[i].length() - 1)); + i++; + } else if (args[i].startsWith("-qY(")) { + qY = ASCIIFile.readDouble1DArray(new File(args[i + 1])); + qYp = Double.parseDouble(args[i].substring(4, args[i].length() - 1)); + i++; + } else if (args[i].startsWith("-qZ(")) { + qZ = ASCIIFile.readDouble1DArray(new File(args[i + 1])); + qZp = Double.parseDouble(args[i].substring(4, args[i].length() - 1)); + i++; + } else if (args[i].equals("-qPX")) { + qPX = ASCIIFile.readDoubleArray(new File(args[i + 1])); + i++; + } else if (args[i].equals("-qPY")) { + qPY = ASCIIFile.readDoubleArray(new File(args[i + 1])); + i++; + } else if (args[i].equals("-qPZ")) { + qPZ = ASCIIFile.readDoubleArray(new File(args[i + 1])); + i++; + } else if (args[i].equals("-qNX")) { + qNX = ASCIIFile.readDouble1DArray(new File(args[i + 1])); + i++; + } else if (args[i].equals("-qNY")) { + qNY = ASCIIFile.readDouble1DArray(new File(args[i + 1])); + i++; + } else if (args[i].equals("-qNZ")) { + qNZ = ASCIIFile.readDouble1DArray(new File(args[i + 1])); + i++; + } else { + File input_file = new File(args[i]); + int n = 0; + if (input_file.exists()) { + if (name.length() == 0) { + name = input_file.getName(); + } + + if (p instanceof Plot2DPanel) { + Plot2DPanel p2d = (Plot2DPanel) p; + if (type.equals("SCATTER")) { + n = p2d.addScatterPlot(name, ASCIIFile.readDoubleArray(input_file)); + } else if (type.equals("LINE")) { + n = p2d.addLinePlot(name, ASCIIFile.readDoubleArray(input_file)); + } else if (type.equals("BAR")) { + n = p2d.addBarPlot(name, ASCIIFile.readDoubleArray(input_file)); + } else if (type.startsWith("HISTOGRAM2D(")) { + n = p2d.addHistogramPlot(name, ASCIIFile.readDouble1DArray(input_file), Integer.parseInt(type.substring(12, type.length() - 1))); + } else if (type.startsWith("CLOUD2D(")) { + n = p2d.addCloudPlot(name, ASCIIFile.readDoubleArray(input_file), Integer.parseInt(type.substring(8, type.indexOf(","))), + Integer.parseInt(type.substring(type.indexOf(",") + 1, type.length() - 1))); + } else { + p2d.addPlot(type, name, ASCIIFile.readDoubleArray(input_file)); + } + } else { + Plot3DPanel p3d = (Plot3DPanel) p; + if (type.equals("SCATTER")) { + n = p3d.addScatterPlot(name, ASCIIFile.readDoubleArray(input_file)); + } else if (type.equals("LINE")) { + n = p3d.addLinePlot(name, ASCIIFile.readDoubleArray(input_file)); + } else if (type.equals("BAR")) { + n = p3d.addBarPlot(name, ASCIIFile.readDoubleArray(input_file)); + } else if (type.startsWith("HISTOGRAM3D(")) { + n = p3d.addHistogramPlot(name, ASCIIFile.readDoubleArray(input_file), Integer.parseInt(type.substring(12, type.indexOf(","))), + Integer.parseInt(type.substring(type.indexOf(",") + 1, type.length() - 1))); + } else if (type.equals("GRID3D")) { + n = p3d.addGridPlot(name, ASCIIFile.readDoubleArray(input_file)); + } else if (type.startsWith("CLOUD3D(")) { + n = p3d.addCloudPlot(name, ASCIIFile.readDoubleArray(input_file), Integer.parseInt(type.substring(8, type.indexOf(","))), + Integer.parseInt(type.substring(type.indexOf(",") + 1, type.indexOf(",", type.indexOf(",") + 1))), Integer.parseInt(type.substring(type.indexOf(",", type.indexOf(",") + 1) + 1, type.length() - 1))); + } else { + p3d.addPlot(type, name, ASCIIFile.readDoubleArray(input_file)); + } + } + + if (v != null) { + p.addVectortoPlot(n, v); + } + + if (qX != null) { + p.addQuantiletoPlot(n, 0, qXp, false, qX); + } + if (qY != null) { + p.addQuantiletoPlot(n, 1, qYp, false, qY); + } + if (qZ != null) { + p.addQuantiletoPlot(n, 2, qZp, false, qZ); + } + + if (qPX != null) { + p.addQuantilestoPlot(n, 0, qPX); + } + if (qPY != null) { + p.addQuantilestoPlot(n, 1, qPY); + } + if (qPZ != null) { + p.addQuantilestoPlot(n, 2, qPZ); + } + + if (qNX != null) { + p.addGaussQuantilestoPlot(n, 0, qNX); + } + if (qNY != null) { + p.addGaussQuantilestoPlot(n, 1, qNY); + } + if (qNZ != null) { + p.addGaussQuantilestoPlot(n, 2, qNZ); + } + + type = "SCATTER"; + leg = "SOUTH"; + name = ""; + qX = null; + qY = null; + qZ = null; + qXp = 0; + qYp = 0; + qZp = 0; + + v = null; + + qPX = null; + qPY = null; + qPZ = null; + + qNX = null; + qNY = null; + qNZ = null; + + } else { + System.out.println("File " + args[i] + " unknown."); + System.out.println(man); + } + } + } + p.setLegendOrientation(leg); + FrameView f = new FrameView(p); + f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } catch (Exception e) { + e.printStackTrace(); + System.err.println("\n" + man); + } + } + + /** + * @return the font + */ + public Font getFont() { + return font; + } + + /** + * @param font the font to set + */ + public void setFont(Font font) { + this.font = font; + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/AWTDrawer.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/AWTDrawer.java new file mode 100644 index 0000000..81a92a7 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/AWTDrawer.java @@ -0,0 +1,509 @@ +/* + * Created on 31 mai 2005 by richet + * Changed on 6/13/2014 by Jerry Dietrich + * Contact info ballooninternet@cox.net + */ +package org.xbib.graphics.graph.jmathplot.render; + +import static java.lang.Math.PI; +import static java.lang.Math.abs; +import static java.lang.Math.acos; +import static java.lang.Math.atan; +import static java.lang.Math.cos; +import static java.lang.Math.signum; +import static java.lang.Math.sqrt; +import static java.lang.Math.tan; +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; +import org.xbib.graphics.graph.jmathplot.utils.FastMath; +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.GradientPaint; +import java.awt.Image; +import java.awt.Stroke; +import java.awt.font.FontRenderContext; +import java.awt.geom.AffineTransform; + +public abstract class AWTDrawer extends AbstractDrawer { + + public Projection projection; + + public AWTDrawer(PlotCanvas _canvas) { + super(_canvas); + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#resetProjection() + */ + public void resetBaseProjection() { + projection.initBaseCoordsProjection(true); + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#setColor(java.awt.Color) + */ + public void setColor(Color c) { + comp2D.setColor(c); + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#setGradient + */ + public void setGradient(double[] xy0, Color c0, double[] xy1, Color c1) { + int[] s0 = project(xy0); + int[] s1 = project(xy1); + comp2D.setPaint(new GradientPaint(s0[0], s0[1], c0, s1[0], s1[1], c1)); + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#setFont(java.awt.Font) + */ + public void setFont(Font f) { + comp2D.setFont(f); + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#getColor() + */ + public Color getColor() { + return comp2D.getColor(); + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#getFont() + */ + public Font getFont() { + return comp2D.getFont(); + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#project(double[]) + */ + public int[] project(double... pC) { + return projection.screenProjection(pC); + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#projectRatio(double[]) + */ + public int[] projectBase(double... rC) { + return projection.screenProjectionBase(rC); + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#translate(int[]) + */ + public void translate(int... t) { + projection.translate(t); + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#dilate(int[], double[]) + */ + public void dilate(int[] screenOrigin, double[] screenRatio) { + projection.dilate(screenOrigin, screenRatio); + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#drawString(java.lang.String, + * double[], double, double, double) + */ + public void drawText(String label, double... pC) { + int[] sC = projection.screenProjection(pC); + + // Corner offset adjustment : Text Offset is used Here + FontRenderContext frc = comp2D.getFontRenderContext(); + Font font1 = comp2D.getFont(); + int x = sC[0]; + int y = sC[1]; + double w = font1.getStringBounds(label, frc).getWidth(); + double h = font1.getSize2D(); + x -= (int) (w * text_Eastoffset); + y += (int) (h * text_Northoffset); + + int wc = (int) (w * FastMath.cos(text_angle) + h * FastMath.sin(text_angle)); + int hc = (int) (h * FastMath.cos(text_angle) + w * FastMath.sin(text_angle)); + if (!comp2D.hitClip(x, y, wc, hc)) { + return; + } + + if (text_angle != 0) { + comp2D.rotate(text_angle, x + w / 2, y - h / 2); + } + + int tmpY = y; + String[] lines = label.split("\n"); + for (int i = 0; i < lines.length; i++) { + comp2D.drawString(lines[i], x, tmpY); + tmpY += h; + } + + if (text_angle != 0) { + comp2D.rotate(-text_angle, x + w / 2, y - h / 2); + } + } + + public void drawShadowedText(String label, float alpha, double... pC) { + int[] sC = projection.screenProjection(pC); + + // Corner offset adjustment : Text Offset is used Here + FontRenderContext frc = comp2D.getFontRenderContext(); + Font font1 = comp2D.getFont(); + int x = sC[0]; + int y = sC[1]; + double w = font1.getStringBounds(label, frc).getWidth(); + double h = font1.getSize2D(); + x -= (int) (w * text_Eastoffset); + y += (int) (h * text_Northoffset); + + int wc = (int) (w * FastMath.cos(text_angle) + h * FastMath.sin(text_angle)); + int hc = (int) (h * FastMath.cos(text_angle) + w * FastMath.sin(text_angle)); + if (!comp2D.hitClip(x, y, wc, hc)) { + return; + } + + if (text_angle != 0) { + comp2D.rotate(text_angle, x + w / 2, y - h / 2); + } + + Composite cs = comp2D.getComposite(); + Color c = comp2D.getColor(); + + String[] lines = label.split("\n"); + for (int i = 0; i < lines.length; i++) { + + comp2D.setColor(Color.white); + comp2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); + comp2D.fillRect(x, y - (int) h, (int) w, (int) h); + comp2D.setComposite(cs); + comp2D.setColor(c); + + comp2D.drawString(lines[i], x, y); + y += h; + } + + if (text_angle != 0) { + comp2D.rotate(-text_angle, x + w / 2, y - h / 2); + } + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#drawStringRatio(java.lang.String, + * double[], double, double, double) + */ + public void drawTextBase(String label, double... rC) { + int[] sC = projection.screenProjectionBase(rC); + + // Corner offset adjustment : Text Offset is used Here + FontRenderContext frc = comp2D.getFontRenderContext(); + Font font1 = comp2D.getFont(); + int x = sC[0]; + int y = sC[1]; + double w = font1.getStringBounds(label, frc).getWidth(); + double h = font1.getSize2D(); + x -= (int) (w * text_Eastoffset); + y += (int) (h * text_Northoffset); + + int wc = (int) (w * FastMath.cos(text_angle) + h * FastMath.sin(text_angle)); + int hc = (int) (h * FastMath.cos(text_angle) + w * FastMath.sin(text_angle)); + if (!comp2D.hitClip(x, y, wc, hc)) { + return; + } + + if (text_angle != 0) { + comp2D.rotate(text_angle, x + w / 2, y - h / 2); + } + + String[] lines = label.split("\n"); + for (int i = 0; i < lines.length; i++) { + comp2D.drawString(lines[i], x, y); + y += h; + } + //comp2D.drawString(label, x, y); + + if (text_angle != 0) { + comp2D.rotate(-text_angle, x + w / 2, y - h / 2); + } + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#drawLineRatio(double[], + * double[]) + */ + public void drawLineBase(double[]... rC) { + int[][] sC = new int[rC.length][]; + for (int i = 0; i < sC.length; i++) { + sC[i] = projection.screenProjectionBase(rC[i]); + } + drawLine(sC); + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#drawLine(double[], double[]) + */ + public void drawLine(double[]... pC) { + int[][] sC = new int[pC.length][]; + for (int i = 0; i < sC.length; i++) { + sC[i] = projection.screenProjection(pC[i]); + } + drawLine(sC); + } + + private void drawLine(int[]... c) { + int minx = c[0][0], miny = c[0][1], maxx = c[0][0] + 1, maxy = c[0][1] + 1; + int[] x = new int[c.length]; + for (int i = 0; i < c.length; i++) { + x[i] = c[i][0]; + minx = FastMath.min(minx, x[i]); + maxx = FastMath.max(maxx, x[i]); + } + int[] y = new int[c.length]; + for (int i = 0; i < c.length; i++) { + y[i] = c[i][1]; + miny = FastMath.min(miny, y[i]); + maxy = FastMath.max(maxy, y[i]); + } + + if (comp2D.hitClip(minx, miny, maxx - minx, maxy - miny)) { + Stroke s = null; + switch (line_type) { + case CONTINOUS_LINE: + s = new BasicStroke(line_width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND); + break; + case DOTTED_LINE: + s = new BasicStroke(line_width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 1f, new float[]{2f}, 0f); + break; + } + comp2D.setStroke(s); + comp2D.drawPolyline(x, y, c.length); + } + } + + public void drawRoundDot(double... pC) { + int[] sC = projection.screenProjection(pC); + comp2D.fillOval(sC[0] - dot_radius, sC[1] - dot_radius, 2 * dot_radius, 2 * dot_radius); + } + + public void drawCrossDot(double... pC) { + int[] sC = projection.screenProjection(pC); + comp2D.drawLine(sC[0] - dot_radius, sC[1] - dot_radius, sC[0] + dot_radius, sC[1] + dot_radius); + comp2D.drawLine(sC[0] + dot_radius, sC[1] - dot_radius, sC[0] - dot_radius, sC[1] + dot_radius); + } + + public void drawPatternDot(double... pC) { + int[] sC = projection.screenProjection(pC); + int yoffset = (int) FastMath.ceil(dot_pattern.length / 2.0); + int xoffset = (int) FastMath.ceil(dot_pattern[0].length / 2.0); + for (int i = 0; i < dot_pattern.length; i++) { + for (int j = 0; j < dot_pattern[i].length; j++) { + if (dot_pattern[i][j]) // comp2D.setColor(new Color(getColor()) + { + //System.err.println("comp2D.fillRect"); + comp2D.fillRect(sC[0] - xoffset + j, sC[1] - yoffset + i, 1, 1); + } + } + } + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#drawPloygon(double[][]) + */ + public void drawPolygon(double[]... pC) { + int[][] c = new int[pC.length][2]; + for (int i = 0; i < pC.length; i++) { + c[i] = projection.screenProjection(pC[i]); + } + + int minx = c[0][0], miny = c[0][1], maxx = c[0][0] + 1, maxy = c[0][1] + 1; + int[] x = new int[c.length]; + for (int i = 0; i < c.length; i++) { + x[i] = c[i][0]; + minx = FastMath.min(minx, x[i]); + maxx = FastMath.max(maxx, x[i]); + } + int[] y = new int[c.length]; + for (int i = 0; i < c.length; i++) { + y[i] = c[i][1]; + miny = FastMath.min(miny, y[i]); + maxy = FastMath.max(maxy, y[i]); + } + + if (comp2D.hitClip(minx, miny, maxx - minx, maxy - miny)) { + comp2D.drawPolygon(x, y, c.length); + } + } + + /* + * (non-Javadoc) + * + * @see org.math.plot.render.AbstractDrawer#fillPloygon(double[][]) + */ + public void fillPolygon(float alpha, double[]... pC) { + int[][] c = new int[pC.length][2]; + for (int i = 0; i < pC.length; i++) { + c[i] = projection.screenProjection(pC[i]); + } + + int minx = c[0][0], miny = c[0][1], maxx = c[0][0] + 1, maxy = c[0][1] + 1; + int[] x = new int[c.length]; + for (int i = 0; i < c.length; i++) { + x[i] = c[i][0]; + minx = FastMath.min(minx, x[i]); + maxx = FastMath.max(maxx, x[i]); + } + int[] y = new int[c.length]; + for (int i = 0; i < c.length; i++) { + y[i] = c[i][1]; + miny = FastMath.min(miny, y[i]); + maxy = FastMath.max(maxy, y[i]); + } + if (comp2D.hitClip(minx, miny, maxx - minx, maxy - miny)) { + Composite cs = comp2D.getComposite(); + comp2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); + comp2D.fillPolygon(x, y, c.length); + comp2D.setComposite(cs); + } + } + + public void drawImage(Image img, float alpha, double[] _xyzSW, double[] _xyzSE, double[] _xyzNW) { + Composite cs = comp2D.getComposite(); + comp2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); + AffineTransform t = getAffineTransform(img.getWidth(canvas), img.getHeight(canvas), _xyzSW, _xyzSE, _xyzNW); + if (t != null) { + comp2D.drawImage(img, t, canvas); + } + comp2D.setComposite(cs); + } + + /*public void drawShape(Shape shape, float alpha, double[] _xyzSW, double[] _xyzSE, double[] _xyzNW) { + AffineTransform t = getAffineTransform(shape.getBounds().width,shape.getBounds().height, _xyzSW, _xyzSE, _xyzNW); + Shape t_shape = t.createTransformedShape(shape); + Composite cs = comp2D.getComposite(); + comp2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); + comp2D.draw(t_shape); + comp2D.setComposite(cs); + }*/ + static boolean isDiff(double[] x, int[] y) { + return abs(x[0] - y[0]) > 1 || abs(x[1] - y[1]) > 1; + } + + static double sign(double x) { + if (x != 0) { + return signum(x); + } else { + return 1.0; + } + } + + static double sqr(double x) { + return x * x; + } + + public AffineTransform getAffineTransform(int width, int height, double[] _xyzSW, double[] _xyzSE, double[] _xyzNW) { + int[] cornerNW = projection.screenProjection(_xyzNW); + int[] cornerSE = projection.screenProjection(_xyzSE); + int[] cornerSW = projection.screenProjection(_xyzSW); + + double[] vectWE = {(double) cornerSE[0] - (double) cornerSW[0], (double) cornerSE[1] - (double) cornerSW[1]}; + double normvectWE = sqrt(sqr(vectWE[0]) + sqr(vectWE[1])); + double[] vectSN = {(double) cornerNW[0] - (double) cornerSW[0], (double) cornerNW[1] - (double) cornerSW[1]}; + double normvectSN = sqrt(sqr(vectSN[0]) + sqr(vectSN[1])); + double angleSW = acos((vectWE[0] * vectSN[0] + vectWE[1] * vectSN[1]) / (normvectWE * normvectSN)); + + if (angleSW == 0.0) { + return null; + } + + AffineTransform t = new AffineTransform(); + + t.translate(cornerNW[0], cornerNW[1]); + t.scale(sign(vectWE[0]), -sign(vectSN[1])); + t.rotate(-atan(vectSN[0] / vectSN[1])); + t.shear(0, 1 / tan(PI - angleSW)); + t.scale(normvectWE * cos(angleSW - PI / 2) / (double) width, normvectSN / (double) height); + + double[] _cornerSE_tr = new double[2]; + double[] _cornerSE = {width, height}; + t.transform(_cornerSE, 0, _cornerSE_tr, 0, 1); + + if (isDiff(_cornerSE_tr, cornerSE)) { + double[] vectSE_NW_1 = {(double) cornerNW[0] - (double) cornerSE[0], (double) cornerNW[1] - (double) cornerSE[1]}; + double[] vectSE_NW_2 = {(double) cornerNW[0] - _cornerSE_tr[0], (double) cornerNW[1] - _cornerSE_tr[1]}; + + double normvect_1 = sqrt(sqr(vectSE_NW_1[0]) + sqr(vectSE_NW_1[1])); + double normvect_2 = sqrt(sqr(vectSE_NW_1[0]) + sqr(vectSE_NW_1[1])); + + double cos_angle = (((vectSE_NW_1[0] * vectSE_NW_2[0] + vectSE_NW_1[1] * vectSE_NW_2[1]) / (normvect_1 * normvect_2))); + double vect = (vectSE_NW_1[0] * vectSE_NW_2[1] - vectSE_NW_1[1] * vectSE_NW_2[0]); + + AffineTransform t2 = new AffineTransform(); + if (vect < 0) { + t2.rotate(acos(cos_angle), cornerNW[0], cornerNW[1]); + } else { + t2.rotate(-acos(cos_angle), cornerNW[0], cornerNW[1]); + } + t.preConcatenate(t2); + } + + // TODO patch for many cases... + + /*double[] _cornerSW_tr = new double[2]; + double[] _cornerSW = { 0, img.getHeight(canvas) }; + t.transform(_cornerSW, 0, _cornerSW_tr, 0, 1); + + if (isDiff(_cornerSW_tr, cornerSW)) { + double[] vectSW_NW_1 = { (double) cornerNW[0] - (double) cornerSW[0], (double) cornerNW[1] - (double) cornerSW[1] }; + double[] vectSW_NW_2 = { (double) cornerNW[0] - (double) _cornerSW_tr[0], (double) cornerNW[1] - (double) _cornerSW_tr[1] }; + + double normvect_1 = sqrt(sqr(vectSW_NW_1[0]) + sqr(vectSW_NW_1[1])); + double normvect_2 = sqrt(sqr(vectSW_NW_1[0]) + sqr(vectSW_NW_1[1])); + + double cos_angle = (((vectSW_NW_1[0] * vectSW_NW_2[0] + vectSW_NW_1[1] * vectSW_NW_2[1]) / (normvect_1 * normvect_2))); + double vect = (vectSW_NW_1[0] * vectSW_NW_2[1] - vectSW_NW_1[1] * vectSW_NW_2[0]); + + System.out.println(cos_angle + " " + vect + " -> " + toDegrees(acos(cos_angle))); + + //System.out.println(" "+vectSE_NW_1[0]+","+vectSE_NW_1[1]+" "+vectSE_NW_2[0]+","+vectSE_NW_2[1]); + AffineTransform t2 = new AffineTransform(); + if (vect > 0) + t2.rotate(acos(cos_angle), cornerNW[0], cornerNW[1]); + else + t2.rotate(-acos(cos_angle), cornerNW[0], cornerNW[1]); + t.preConcatenate(t2); + + }*/ + return t; + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/AWTDrawer2D.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/AWTDrawer2D.java new file mode 100644 index 0000000..6f4379f --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/AWTDrawer2D.java @@ -0,0 +1,28 @@ +package org.xbib.graphics.graph.jmathplot.render; + +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; + +public class AWTDrawer2D extends AWTDrawer { + + public AWTDrawer2D(PlotCanvas _canvas) { + super(_canvas); + projection = new Projection2D(this); + } + + /*// More efficient method for orthogonal display of images + public void drawImage(Image img,float alpha, double[] _xyzSW, double[] _xyzSE,double[] _xyzNW) { + int[] cornerNW = projection.screenProjection(_xyzNW); + int[] cornerSE = projection.screenProjection(_xyzSE); + int[] cornerSW = projection.screenProjection(_xyzSW); + + AffineTransform transform = new AffineTransform(); + transform.translate(cornerNW[0],cornerNW[1]); + transform.scale((-cornerSW[0]+cornerSE[0])/(double)img.getWidth(canvas),(-cornerNW[1]+cornerSW[1])/(double)img.getHeight(canvas)); + + Composite cs = comp2D.getComposite(); + comp2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,alpha)); + comp2D.drawImage(img, transform,canvas); + comp2D.setComposite(cs); + }*/ + +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/AWTDrawer3D.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/AWTDrawer3D.java new file mode 100644 index 0000000..c0807b8 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/AWTDrawer3D.java @@ -0,0 +1,27 @@ +package org.xbib.graphics.graph.jmathplot.render; + +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; + +public class AWTDrawer3D extends AWTDrawer { + + public AWTDrawer3D(PlotCanvas _canvas) { + super(_canvas); + projection = new Projection3D(this); + } + + public void rotate(int[] t, int[] panelSize) { + ((Projection3D) projection).rotate(t, panelSize); + } + + public void dilate(int[] screenOrigin, double[] screenRatio) { + super.dilate(screenOrigin, screenRatio); + ((Projection3D) projection).updateCoordsCenterScreen(); + canvas.repaint(); + } + + public void translate(int... t) { + super.translate(t); + ((Projection3D) projection).updateCoordsCenterScreen(); + canvas.repaint(); + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/AbstractDrawer.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/AbstractDrawer.java new file mode 100644 index 0000000..66ae4e5 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/AbstractDrawer.java @@ -0,0 +1,252 @@ +package org.xbib.graphics.graph.jmathplot.render; + +import org.xbib.graphics.graph.jmathplot.canvas.PlotCanvas; +import org.xbib.graphics.graph.jmathplot.utils.Array; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Image; + +public abstract class AbstractDrawer { + + public PlotCanvas canvas; + protected Graphics2D comp2D; + public final static int ROUND_DOT = 1; + public final static int CROSS_DOT = 2; + public final static int PATTERN_DOT = 0; + public final static int CONTINOUS_LINE = 1; + public final static int DOTTED_LINE = 2; + public final static int DEFAULT_DOT_RADIUS = 2; + public final static int DEFAULT_LINE_WIDTH = 1; + public final static boolean[][] DOT_TRIANGLE_PATTERN = stringToPattern("_", "___#___", "__#_#__", "__#_#__", "_#___#_", "_#___#_", "#######"); + public final static boolean[][] DOT_SQUARE_PATTERN = stringToPattern("_", "######", "#____#", "#____#", "#____#", "#____#", "######"); + public final static Font DEFAULT_FONT = new Font("Arial", Font.PLAIN, 10); + public final static Color DEFAULT_COLOR = Color.BLACK; + protected Font font = DEFAULT_FONT; + protected double text_Eastoffset, text_Northoffset; + protected double text_angle; + protected Color color = DEFAULT_COLOR; + protected double[] base_offset; + protected int[] screen_offset; + //protected double alpha; + protected int dot_type = ROUND_DOT; + protected int dot_radius = DEFAULT_DOT_RADIUS; + protected boolean[][] dot_pattern = DOT_TRIANGLE_PATTERN; + protected int line_type = CONTINOUS_LINE; + protected int line_width = DEFAULT_LINE_WIDTH; + + //protected boolean[][] line_pattern = DOT_TRIANGLE_PATTERN; + public AbstractDrawer(PlotCanvas _canvas) { + canvas = _canvas; + } + + /** + * Method used to initialize drawer to DEFAULT values + */ + public void initGraphics(Graphics2D _comp2D) { + comp2D = _comp2D; + comp2D.setPaintMode(); + } + + public Graphics2D getGraphics2D() { + return comp2D; + } + + /** + * Method used to reinitialize the plot when the base has changed (bounds or + * scale) + */ + public abstract void resetBaseProjection(); + + public void setColor(Color c) { + color = c; + } + + public abstract void setGradient(double[] xy0, Color c0, double[] xy1, Color c1); + + public void resetGradient() { + comp2D.setPaint(color); + } + + public void setFont(Font f) { + font = f; + } + + public void setTextOffset(double _cornerEast, double _cornerNorth) { + text_Eastoffset = _cornerEast; + text_Northoffset = _cornerNorth; + } + + public void setTextAngle(double _angle) { + text_angle = _angle; + } + + public void setDotType(int _dot_type) { + dot_type = _dot_type; + } + + public void setDotRadius(int _dot_radius) { + dot_radius = _dot_radius; + } + + public void setDotPattern(boolean[][] _dot_pattern) { + dot_pattern = _dot_pattern; + } + + public void setLineType(int _line_type) { + line_type = _line_type; + } + + public void setLineWidth(int _line_width) { + line_width = _line_width; + } + + public int getLineWidth() { + return line_width; + } + + public void setBaseOffset(double... _boffset) { + base_offset = _boffset; + } + + public void setScreenOffset(int... _soffset) { + screen_offset = _soffset; + } + + /*public void setAlpha(double _alpha) { + alpha = _alpha; + }*/ + public Color getColor() { + return color; + } + + public Font getFont() { + return font; + } + + public double[] getTextOffset() { + return new double[]{text_Eastoffset, text_Northoffset}; + } + + public double getTextAngle() { + return text_angle; + } + + public int getDotType() { + return dot_type; + } + + public int getDotRadius() { + return dot_radius; + } + + public boolean[][] getDotPattern() { + return dot_pattern; + } + + public double[] getBaseOffset() { + return base_offset; + } + + public int[] getScreenOffset() { + return screen_offset; + } + + /** + * Returns the screen coordinates coresponding to plot coordinates Used to + * test if mouse is pointing on a plot. + * + * @param pC plot ccordinates to project in screen + * @return screen coordinates + */ + public abstract int[] project(double... pC); + + /** + * Returns the screen coordinates coresponding to plot coordinates Used to + * test if mouse is pointing on a plot. + * + * @param rC plot ccordinates to project in screen + * @return screen coordinates + */ + public abstract int[] projectBase(double... rC); + + /** + * Plot ActionMode : translation of the plot + * + * @param t mouse translation in pixels + */ + public abstract void translate(int... t); + + /** + * Plot ActionMode : dilatation of the plot + * + * @param screenOrigin mouse initial position + * @param screenRatio mouse final position relative to plot panel size + */ + public abstract void dilate(int[] screenOrigin, double[] screenRatio); + + public void drawCoordinate(double... pC) { + for (int i = 0; i < canvas.base.dimension; i++) { + double[] axeprojection = Array.copy(pC); + axeprojection[i] = canvas.base.baseCoords[0][i]; + drawLine(pC, axeprojection); + } + setTextAngle(0); + setTextOffset(0, 0); + //drawText(Label.coordToString(pC), pC); + } + + public abstract void drawText(String label, double... pC); + + public abstract void drawShadowedText(String label, float alpha, double... pC); + + public abstract void drawTextBase(String label, double... rC); + + public abstract void drawLineBase(double[]... rC); + + public abstract void drawLine(double[]... pC); + + public void drawDot(double... pC) { + switch (dot_type) { + case ROUND_DOT: + drawRoundDot(pC); + break; + case CROSS_DOT: + drawCrossDot(pC); + break; + case PATTERN_DOT: + drawPatternDot(pC); + break; + } + } + + public abstract void drawRoundDot(double... pC); + + public abstract void drawCrossDot(double... pC); + + public abstract void drawPatternDot(double... pC); + + public abstract void drawPolygon(double[]... pC); + + public abstract void fillPolygon(float alpha, double[]... pC); + + public abstract void drawImage(Image img, float alpha, double[] _xyzSW, double[] _xyzSE, double[] _xyzNW); + + // needs to be discussed... Maybe a geometric addon should be more interesting... + //public abstract void drawShape(Shape shape, float alpha,double[] _xyzSW, double[] _xyzSE,double[] _xyzNW); + public static boolean[][] stringToPattern(String empty, String... c) { + boolean[][] p = new boolean[c.length][]; + for (int i = 0; i < p.length; i++) { + p[i] = stringToPattern(empty, c[i]); + } + return p; + } + + public static boolean[] stringToPattern(String empty, String c) { + boolean[] p = new boolean[c.length()]; + for (int i = 0; i < p.length; i++) { + p[i] = !(c.substring(i, i + 1).equals(empty)); + } + return p; + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/Projection.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/Projection.java new file mode 100644 index 0000000..5ec0537 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/Projection.java @@ -0,0 +1,152 @@ +/* + * Created on 31 mai 2005 by richet + */ +package org.xbib.graphics.graph.jmathplot.render; + +import org.xbib.graphics.graph.jmathplot.Base; +import org.xbib.graphics.graph.jmathplot.utils.FastMath; + +public abstract class Projection { + + protected int[][] baseScreenCoords; + public static double DEFAULT_BORDER = 0.15; + protected double borderCoeff = 1 - 2 * DEFAULT_BORDER; + protected AWTDrawer draw; + + public Projection(AWTDrawer _draw) { + draw = _draw; + } + + public void initBaseCoordsProjection(boolean reset) { + // System.out.println("Projection.initBaseCoordsProjection"); + if (baseScreenCoords == null) { + baseScreenCoords = new int[draw.canvas.base.baseCoords.length][2]; + } + if (reset) { + totalScreenRatio[0] = 1; + totalScreenRatio[1] = 1; + } + for (int i = 0; i < draw.canvas.base.dimension + 1; i++) { + // Compute the basis extremity coordinates in the normed-centered screen (ie [-0.5,0.5]x[-0.5,0.5] screen) + double[] ratio = new double[]{1, 1}; + if (draw.canvas.base.baseCoords != null) { + ratio = baseCoordsScreenProjectionRatio(draw.canvas.base.baseCoords[i]); + } + // Compute the basis extremity coordinates in the true screen (ie in px: [0,400]x[0,400]) + baseScreenCoords[i][0] = (int) (draw.canvas.getWidth() * (.5 + (borderCoeff * ratio[0] / totalScreenRatio[0]))); + baseScreenCoords[i][1] = (int) (draw.canvas.getHeight() * (.5 - (borderCoeff * ratio[1] / totalScreenRatio[1]))); + } + //System.err.println("\n" + Array.toString(baseScreenCoords)); + } + + // /////////////////////////////////////////// + // ////// move methods /////////////////////// + // /////////////////////////////////////////// + public void translate(int[] screenTranslation) { + for (int i = 0; i < draw.canvas.base.dimension + 1; i++) { + baseScreenCoords[i][0] = baseScreenCoords[i][0] + screenTranslation[0]; + baseScreenCoords[i][1] = baseScreenCoords[i][1] + screenTranslation[1]; + } + } + + // This stores the whole zooming ratio along all dilate calls. + public double[] totalScreenRatio = new double[]{1, 1}; + public double[] maxScreenRatio = new double[]{1, 1}; + public double[] minScreenRatio = new double[]{.01, .01}; + + public void dilate(int[] screenOrigin, double[] screenRatio) { + // System.out.println("screenOrigin = "+screenOrigin[0]+" , + // "+screenOrigin[1]); + // System.out.println("screenRatio = "+screenRatio[0]+" , + // "+screenRatio[1]); + + // Update the zooming ratio history + if (totalScreenRatio[0] * screenRatio[0] > maxScreenRatio[0]) { + screenRatio[0] = maxScreenRatio[0] / totalScreenRatio[0]; + } + if (totalScreenRatio[1] * screenRatio[1] > maxScreenRatio[1]) { + screenRatio[1] = maxScreenRatio[1] / totalScreenRatio[1]; + } + + if (totalScreenRatio[0] * screenRatio[0] < minScreenRatio[0]) { + screenRatio[0] = minScreenRatio[0] / totalScreenRatio[0]; + } + if (totalScreenRatio[1] * screenRatio[1] < minScreenRatio[1]) { + screenRatio[1] = minScreenRatio[1] / totalScreenRatio[1]; + } + + for (int i = 0; i < draw.canvas.base.dimension + 1; i++) { + // System.out.println("baseScreenCoords["+i+"] = + // "+baseScreenCoords[i][0]+" , "+baseScreenCoords[i][1]); + baseScreenCoords[i][0] = (int) ((baseScreenCoords[i][0] - screenOrigin[0]) / screenRatio[0]); + baseScreenCoords[i][1] = (int) ((baseScreenCoords[i][1] - screenOrigin[1]) / screenRatio[1]); + // System.out.println(" -> baseScreenCoords["+i+"] = + // "+baseScreenCoords[i][0]+" , "+baseScreenCoords[i][1]); + } + // Update the zooming ratio history + totalScreenRatio[0] = totalScreenRatio[0] * screenRatio[0]; + totalScreenRatio[1] = totalScreenRatio[1] * screenRatio[1]; + } + + // /////////////////////////////////////////// + // ////// projection method ////////////////// + // /////////////////////////////////////////// + public int[] screenProjection(double... pC) { + // System.out.println("Projection.screenProjection("+Array.toString(pC)+")"); + double[] sC = new double[2]; + sC[0] = baseScreenCoords[0][0]; + sC[1] = baseScreenCoords[0][1]; + for (int i = 0; i < draw.canvas.base.dimension; i++) { + double normdist_pC_baseCoords = 0; + if (draw.canvas.base.axesScales[i].equalsIgnoreCase(Base.LOGARITHM)) { + normdist_pC_baseCoords = ((FastMath.log(pC[i]) - FastMath.log(draw.canvas.base.baseCoords[0][i])) / (FastMath.log(draw.canvas.base.baseCoords[i + 1][i]) - FastMath.log(draw.canvas.base.baseCoords[0][i]))); + } else if (draw.canvas.base.axesScales[i].equalsIgnoreCase(Base.LINEAR) || draw.canvas.base.axesScales[i].equalsIgnoreCase(Base.STRINGS)) { + if (pC != null && draw.canvas.base.baseCoords != null && draw.canvas.base.baseCoords[i + 1] != null) { + normdist_pC_baseCoords = ((pC[i] - draw.canvas.base.baseCoords[0][i]) / (draw.canvas.base.baseCoords[i + 1][i] - draw.canvas.base.baseCoords[0][i])); + } + } + sC[0] += normdist_pC_baseCoords * (baseScreenCoords[i + 1][0] - baseScreenCoords[0][0]); + sC[1] += normdist_pC_baseCoords * (baseScreenCoords[i + 1][1] - baseScreenCoords[0][1]); + } + + if (draw.base_offset != null) { + for (int i = 0; i < draw.canvas.base.dimension; i++) { + sC[0] += draw.base_offset[i] * (baseScreenCoords[i + 1][0] - baseScreenCoords[0][0]); + sC[1] += draw.base_offset[i] * (baseScreenCoords[i + 1][1] - baseScreenCoords[0][1]); + } + } + + if (draw.screen_offset != null) { + sC[0] += draw.screen_offset[0]; + sC[1] += draw.screen_offset[1]; + } + + return new int[]{(int) sC[0], (int) sC[1]}; + } + + public int[] screenProjectionBase(double... rC) { + double[] sC = new double[2]; + sC[0] = baseScreenCoords[0][0]; + sC[1] = baseScreenCoords[0][1]; + for (int i = 0; i < draw.canvas.base.dimension; i++) { + sC[0] += rC[i] * (baseScreenCoords[i + 1][0] - baseScreenCoords[0][0]); + sC[1] += rC[i] * (baseScreenCoords[i + 1][1] - baseScreenCoords[0][1]); + } + + if (draw.base_offset != null) { + for (int i = 0; i < draw.canvas.base.dimension; i++) { + sC[0] += draw.base_offset[i] * (baseScreenCoords[i + 1][0] - baseScreenCoords[0][0]); + sC[1] += draw.base_offset[i] * (baseScreenCoords[i + 1][1] - baseScreenCoords[0][1]); + } + } + + if (draw.screen_offset != null) { + sC[0] += draw.screen_offset[0]; + sC[1] += draw.screen_offset[1]; + } + + return new int[]{(int) sC[0], (int) sC[1]}; + } + + protected abstract double[] baseCoordsScreenProjectionRatio(double[] xyz); +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/Projection2D.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/Projection2D.java new file mode 100644 index 0000000..675e36c --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/Projection2D.java @@ -0,0 +1,23 @@ +package org.xbib.graphics.graph.jmathplot.render; + +/** + * BSD License + * + * @author Yann RICHET + */ +public class Projection2D extends Projection { + + public Projection2D(AWTDrawer _draw) { + super(_draw); + initBaseCoordsProjection(true); + } + + protected double[] baseCoordsScreenProjectionRatio(double[] xy) { + double[] sC = new double[2]; + sC[0] = -0.5 + (xy[0] - draw.canvas.base.roundXmin[0]) / (draw.canvas.base.roundXmax[0] - draw.canvas.base.roundXmin[0]); + sC[1] = -0.5 + (xy[1] - draw.canvas.base.roundXmin[1]) / (draw.canvas.base.roundXmax[1] - draw.canvas.base.roundXmin[1]); + // System.out.println("(" + xy[0] +"," + xy[1] + ") -> (" + sC[0] + "," + // + sC[1] + ")"); + return sC; + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/Projection3D.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/Projection3D.java new file mode 100644 index 0000000..9df650e --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/render/Projection3D.java @@ -0,0 +1,141 @@ +package org.xbib.graphics.graph.jmathplot.render; + +import org.xbib.graphics.graph.jmathplot.utils.FastMath; + +/** + * BSD License + * + * @author Yann RICHET + */ +public class Projection3D extends Projection { + + public double theta; + public double phi; + + // protected boolean useRoundTrigonometry = false; + public Projection3D(AWTDrawer _draw) { + super(_draw); + theta(Math.PI / 4); + phi(Math.PI / 4); + initBaseCoordsProjection(true); + } + + public double factor = 1.4; + public double x0, y0, z0; + public double cos_phi, sin_phi, tan_phi, cos_theta, sin_theta, tan_theta; + static double pi = Math.PI; + + private void theta(double theta) { + if (theta == this.theta) { + //System.err.println("o"); + return; + } + this.theta = theta; + cos_theta = cos(theta); + sin_theta = sin(theta); + tan_theta = tan(theta); + } + + private void phi(double phi) { + if (phi == this.phi) { + //System.err.println("."); + return; + } + this.phi = phi; + cos_phi = cos(phi); + sin_phi = sin(phi); + tan_phi = tan(phi); + } + + public void initBaseCoordsProjection(boolean reset) { + if (reset) { + x0 = (draw.canvas.base.roundXmax[0] + draw.canvas.base.roundXmin[0]) / 2; + y0 = (draw.canvas.base.roundXmax[1] + draw.canvas.base.roundXmin[1]) / 2; + z0 = (draw.canvas.base.roundXmax[2] + draw.canvas.base.roundXmin[2]) / 2; + } + super.initBaseCoordsProjection(reset); + } + // search for (x0,y0,z0) , matching center of the screen [.5,.5] and closest to the center (.5,.5,.5) of the plot + + protected void updateCoordsCenterScreen() { + double dx0 = (draw.canvas.getWidth() * .5 - baseScreenCoords[0][0]) / (baseScreenCoords[1][0] - baseScreenCoords[0][0]); + double dy0 = (draw.canvas.getWidth() * .5 - baseScreenCoords[0][0]) / (baseScreenCoords[2][0] - baseScreenCoords[0][0]); + + double dz0 = (draw.canvas.getHeight() * .5 - baseScreenCoords[0][1]) / (baseScreenCoords[3][1] - baseScreenCoords[0][1]); + double dx, dy, dz = 0; + if ((theta - pi / 4) % pi > pi / 2) { + dx = (.5 * (sin_theta + cos_theta) - tan_theta * dy0) / (sin_theta * sin_theta + cos_theta); + dy = tan_theta * dx + dy0; + } else { + dy = (.5 * (sin_theta + cos_theta) - cos_theta * dx0) / (cos_theta / tan_theta + sin_theta); + dx = 1 / tan_theta * dy + dx0; + } + dz = dz0 + .5 * tan_phi; + + // uuuhhh :) I've always dreamed to speak perl... + dx = (dx < 0 ? 0 : (dx > 1 ? 1 : dx)); + dy = (dy < 0 ? 0 : (dy > 1 ? 1 : dy)); + dz = (dz < 0 ? 0 : (dz > 1 ? 1 : dz)); + + x0 = draw.canvas.base.roundXmin[0] + (draw.canvas.base.roundXmax[0] - draw.canvas.base.roundXmin[0]) * dx; + y0 = draw.canvas.base.roundXmin[1] + (draw.canvas.base.roundXmax[1] - draw.canvas.base.roundXmin[1]) * dy; + z0 = draw.canvas.base.roundXmin[2] + (draw.canvas.base.roundXmax[2] - draw.canvas.base.roundXmin[2]) * dz; + //System.err.println("(x0,y0,z0) = " + x0 + " " + y0 + " " + z0); + } + + protected double[] baseCoordsScreenProjectionRatio(double[] xyz) { + double normdist_xyz_x0 = ((xyz[0] - x0) / (draw.canvas.base.roundXmax[0] - draw.canvas.base.roundXmin[0])); + double normdist_xyz_y0 = ((xyz[1] - y0) / (draw.canvas.base.roundXmax[1] - draw.canvas.base.roundXmin[1])); + double normdist_xyz_z0 = ((xyz[2] - z0) / (draw.canvas.base.roundXmax[2] - draw.canvas.base.roundXmin[2])); + + double[] sC = new double[2]; + sC[0] = (cos_theta * normdist_xyz_y0 + - sin_theta * normdist_xyz_x0) + / factor; + sC[1] = (cos_phi * normdist_xyz_z0 + - sin_phi * cos_theta * normdist_xyz_x0 + - sin_phi * sin_theta * normdist_xyz_y0) + / factor; + //System.out.println("Theta = " + theta + " Phi = " + phi); + // System.err.println("(" + xyz[0] +"," + xyz[1] +"," + xyz[2] + ") -> (" + sC[0] + "," + sC[1] + ")"); + return sC; + } + + // TODO test efficiceny of an approximation of cos and sin fuctions. + /* + * private final static double _2PI = 2 * Math.PI; + * + * private final static int N = 100; + * + * private final static double[] COS = + * DoubleArray.f(DoubleArray.increment(N, 0, 2 * Math.PI / (N - 1)), new + * Function() { public double f(double x) { return Math.cos(x); } }); + * + * private final static double[] SIN = + * DoubleArray.f(DoubleArray.increment(N, 0, 2 * Math.PI / (N - 1)), new + * Function() { public double f(double x) { return Math.sin(x); } }); + */ + private double cos(double x) { + return FastMath.cos(x); + } + + private double tan(double x) { + return FastMath.tan(x); + } + + private double sin(double x) { + return FastMath.sin(x); + } + + public void rotate(double _theta, double _phi) { + theta(_theta); + phi(_phi); + initBaseCoordsProjection(false); + } + + public void rotate(int[] screenTranslation, int[] dimension) { + theta(theta - ((double) screenTranslation[0]) / 100); + phi(phi + ((double) screenTranslation[1]) / 100); + initBaseCoordsProjection(false); + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/utils/Array.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/utils/Array.java new file mode 100644 index 0000000..2c6d32b --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/utils/Array.java @@ -0,0 +1,622 @@ +package org.xbib.graphics.graph.jmathplot.utils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +/** + * BSD License + * + * @author Yann RICHET + */ +public class Array { + + public static HashMap reverseStringMap(HashMap map) { + if (map == null) { + return null; + } + HashMap reverseMap = new HashMap(); + for (String key : map.keySet()) { + reverseMap.put(map.get(key), key); + } + return reverseMap; + } + + private static HashMap mapStringArray(double minvalue, double step, List array) { + if (array == null) { + return null; + } + Collections.sort(array); + HashMap map = new HashMap(array.size()); + double v = minvalue; + for (String string : array) { + if (!map.containsKey(string)) { + map.put(string, v); + v += step; + } + } + return map; + } + + public static boolean equals(double[] x, double[] y) { + if (x.length != y.length) { + return false; + } + for (int i = 0; i < y.length; i++) { + if (x[i] != y[i]) { + return false; + } + } + return true; + } + + public static String toString(HashMap hash) { + StringBuffer sb = new StringBuffer(); + for (Object key : hash.keySet()) { + sb.append(key + " > " + hash.get(key) + "\n"); + } + return sb.toString(); + } + + public static boolean isDouble(String s) { + try { + Double.parseDouble(s); + } catch (NumberFormatException ne) { + return false; + } + return true; + } + + public static HashMap mapStringArray(List array) { + return mapStringArray(0, 1, array); + } + + // Create methods + public static String cat(Object[] array) { + return cat(" ", array); + } + + public static String cat(String separator, Object[] array) { + String o = ""; + for (int i = 0; i < array.length - 1; i++) { + o += array[i].toString() + separator; + } + o += array[array.length - 1].toString(); + return o; + } + + public static String cat(String separator, double[] array) { + String o = ""; + for (int i = 0; i < array.length - 1; i++) { + o += array[i] + separator; + } + o += array[array.length - 1]; + return o; + } + + public static String cat(String columnsSeparator, String rowsSeparator, Object[][] array) { + String o = ""; + for (int i = 0; i < array.length - 1; i++) { + o += cat(columnsSeparator, array[i]) + rowsSeparator; + } + o += cat(columnsSeparator, array[array.length - 1]); + return o; + } + + public static String cat(Object[][] array) { + return cat(" ", "\n", array); + } + + /*public static String cat(List array, String rowsSeparator) { + String o = ""; + for (int i = 0; i < array.size() - 1; i++) + o += array.get(i) + rowsSeparator; + o += array.get(array.size() - 1); + return o; + } + + public static String cat(List array) { + return cat(array, " "); + }*/ + public static String[] duplicate(int m, String c) { + String[] o = new String[m]; + for (int i = 0; i < o.length; i++) { + o[i] = c; + } + return o; + } + + public static int[] duplicate(int m, int c) { + int[] o = new int[m]; + for (int i = 0; i < o.length; i++) { + o[i] = c; + } + return o; + } + + public static double[][] one(int m, int n) { + return one(m, n, 1.0); + } + + public static double[][] one(int m, int n, double c) { + double[][] o = new double[m][n]; + for (int i = 0; i < o.length; i++) { + for (int j = 0; j < o[i].length; j++) { + o[i][j] = c; + } + } + return o; + } + + public static double[] one(int m) { + return one(m, 1.0); + } + + public static double[] one(int m, double c) { + double[] o = new double[m]; + for (int i = 0; i < o.length; i++) { + o[i] = c; + } + return o; + } + + public static double[][] increment(int m, int n, double begin, double pitch) { + double[][] array = new double[m][n]; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + array[i][j] = begin + i * pitch; + } + } + return array; + } + + public static double[] increment(int m, double begin, double pitch) { + double[] array = new double[m]; + for (int i = 0; i < m; i++) { + array[i] = begin + i * pitch; + } + return array; + } + + // Modify rows & colmumns methods + public static double[] copy(double[] M) { + double[] array = new double[M.length]; + System.arraycopy(M, 0, array, 0, M.length); + return array; + } + + public static double[][] copy(double[][] M) { + double[][] array = new double[M.length][M[0].length]; + for (int i = 0; i < array.length; i++) { + System.arraycopy(M[i], 0, array[i], 0, M[i].length); + } + return array; + } + + public static double[][] getSubMatrixRangeCopy(double[][] M, int i1, int i2, int j1, int j2) { + double[][] array = new double[i2 - i1 + 1][j2 - j1 + 1]; + for (int i = 0; i < i2 - i1 + 1; i++) { + System.arraycopy(M[i + i1], j1, array[i], 0, j2 - j1 + 1); + } + return array; + } + + public static double[][] getColumnsRangeCopy(double[][] M, int j1, int j2) { + double[][] array = new double[M.length][j2 - j1 + 1]; + for (int i = 0; i < M.length; i++) { + System.arraycopy(M[i], j1, array[i], 0, j2 - j1 + 1); + } + return array; + } + + public static double[][] getColumnsCopy(double[][] M, int... J) { + double[][] array = new double[M.length][J.length]; + for (int i = 0; i < M.length; i++) { + for (int j = 0; j < J.length; j++) { + array[i][j] = M[i][J[j]]; + } + } + return array; + } + + public static double[] getColumnCopy(double[][] M, int j) { + double[] array = new double[M.length]; + for (int i = 0; i < M.length; i++) { + array[i] = M[i][j]; + } + return array; + } + + public static double[] getColumnCopy(double[][][] M, int j, int k) { + double[] array = new double[M.length]; + for (int i = 0; i < M.length; i++) { + array[i] = M[i][j][k]; + } + return array; + } + + public static double[][] getRowsCopy(double[][] M, int... I) { + double[][] array = new double[I.length][M[0].length]; + for (int i = 0; i < I.length; i++) { + System.arraycopy(M[I[i]], 0, array[i], 0, M[I[i]].length); + } + return array; + } + + public static double[] getRowCopy(double[][] M, int i) { + double[] array = new double[M[0].length]; + System.arraycopy(M[i], 0, array, 0, M[i].length); + return array; + } + + public static double[][] getRowsRangeCopy(double[][] M, int i1, int i2) { + double[][] array = new double[i2 - i1 + 1][M[0].length]; + for (int i = 0; i < i2 - i1 + 1; i++) { + System.arraycopy(M[i + i1], 0, array[i], 0, M[i + i1].length); + } + return array; + } + + public static double[] getRangeCopy(double[] M, int j1, int j2) { + double[] array = new double[j2 - j1 + 1]; + System.arraycopy(M, j1, array, 0, j2 - j1 + 1); + return array; + } + + public static double[] getCopy(double[] M, int... I) { + double[] array = new double[I.length]; + for (int i = 0; i < I.length; i++) { + array[i] = M[I[i]]; + } + return array; + } + + public static int getColumnDimension(double[][] M, int i) { + return M[i].length; + } + + public static double[][] mergeRows(double[]... x) { + double[][] array = new double[x.length][]; + for (int i = 0; i < array.length; i++) { + array[i] = new double[x[i].length]; + System.arraycopy(x[i], 0, array[i], 0, array[i].length); + } + return array; + } + + public static double[][] mergeColumns(double[]... x) { + double[][] array = new double[x[0].length][x.length]; + for (int i = 0; i < array.length; i++) { + for (int j = 0; j < array[i].length; j++) { + array[i][j] = x[j][i]; + } + } + return array; + } + + public static double[] merge(double[]... x) { + int[] xlength_array = new int[x.length]; + xlength_array[0] = x[0].length; + for (int i = 1; i < x.length; i++) { + xlength_array[i] = x[i].length + xlength_array[i - 1]; + } + double[] array = new double[xlength_array[x.length - 1]]; + System.arraycopy(x[0], 0, array, 0, x[0].length); + for (int i = 1; i < x.length; i++) { + System.arraycopy(x[i], 0, array, xlength_array[i - 1], x[i].length); + } + return array; + } + + public static double[][] insertColumns(double[][] x, double[][] y, int J) { + double[][] array = new double[x.length][x[0].length + y[0].length]; + for (int i = 0; i < array.length; i++) { + System.arraycopy(x[i], 0, array[i], 0, J); + System.arraycopy(y[i], 0, array[i], J, y[i].length); + System.arraycopy(x[i], J, array[i], J + y[i].length, x[i].length - J); + } + return array; + } + + public static double[][] insertColumn(double[][] x, double[] y, int J) { + double[][] array = new double[x.length][x[0].length + 1]; + for (int i = 0; i < array.length; i++) { + System.arraycopy(x[i], 0, array[i], 0, J); + array[i][J] = y[i]; + System.arraycopy(x[i], J, array[i], J + 1, x[i].length - J); + } + return array; + } + + public static double[][] insertRows(double[][] x, double[][] y, int I) { + double[][] array = new double[x.length + y.length][x[0].length]; + for (int i = 0; i < I; i++) { + System.arraycopy(x[i], 0, array[i], 0, x[i].length); + } + for (int i = 0; i < y.length; i++) { + System.arraycopy(y[i], 0, array[i + I], 0, y[i].length); + } + for (int i = 0; i < x.length - I; i++) { + System.arraycopy(x[i + I], 0, array[i + I + y.length], 0, x[i].length); + } + return array; + } + + public static double[][] insertRow(double[][] x, double[] y, int I) { + double[][] array = new double[x.length + 1][x[0].length]; + for (int i = 0; i < I; i++) { + System.arraycopy(x[i], 0, array[i], 0, x[i].length); + } + System.arraycopy(y, 0, array[I], 0, y.length); + for (int i = 0; i < x.length - I; i++) { + System.arraycopy(x[i + I], 0, array[i + I + 1], 0, x[i].length); + } + return array; + } + + public static double[] insert(double[] x, int I, double... y) { + double[] array = new double[x.length + y.length]; + System.arraycopy(x, 0, array, 0, I); + System.arraycopy(y, 0, array, I, y.length); + System.arraycopy(x, I, array, I + y.length, x.length - I); + return array; + } + + public static double[][] deleteColumnsRange(double[][] x, int J1, int J2) { + double[][] array = new double[x.length][x[0].length - (J2 - J1 + 1)]; + for (int i = 0; i < array.length; i++) { + System.arraycopy(x[i], 0, array[i], 0, J1); + System.arraycopy(x[i], J2 + 1, array[i], J1, x[i].length - (J2 + 1)); + } + return array; + } + + public static double[][] deleteColumns(double[][] x, int... J) { + double[][] array = new double[x.length][x[0].length - J.length]; + for (int i = 0; i < array.length; i++) { + System.arraycopy(x[i], 0, array[i], 0, J[0]); + for (int j = 0; j < J.length - 1; j++) { + System.arraycopy(x[i], J[j] + 1, array[i], J[j] - j, J[j + 1] - J[j] - 1); + } + System.arraycopy(x[i], J[J.length - 1] + 1, array[i], J[J.length - 1] - J.length + 1, x[i].length - J[J.length - 1] - 1); + } + return array; + } + + public static double[][] deleteRowsRange(double[][] x, int I1, int I2) { + double[][] array = new double[x.length - (I2 - I1 + 1)][x[0].length]; + for (int i = 0; i < I1; i++) { + System.arraycopy(x[i], 0, array[i], 0, x[i].length); + } + for (int i = 0; i < x.length - I2 - 1; i++) { + System.arraycopy(x[i + I2 + 1], 0, array[i + I1], 0, x[i].length); + } + return array; + } + + public static double[][] deleteRows(double[][] x, int... I) { + double[][] array = new double[x.length - I.length][x[0].length]; + for (int i = 0; i < I[0]; i++) { + System.arraycopy(x[i], 0, array[i], 0, x[i].length); + } + for (int j = 0; j < I.length - 1; j++) { + for (int i = I[j] + 1; i < I[j + 1]; i++) { + System.arraycopy(x[i], 0, array[i - j], 0, x[i].length); + } + } + for (int i = I[I.length - 1] + 1; i < x.length; i++) { + System.arraycopy(x[i], 0, array[i - I.length], 0, x[i].length); + } + return array; + } + + public static double[] deleteRange(double[] x, int J1, int J2) { + double[] array = new double[x.length - (J2 - J1 + 1)]; + System.arraycopy(x, 0, array, 0, J1); + System.arraycopy(x, J2 + 1, array, J1, x.length - (J2 + 1)); + return array; + } + + public static double[] delete(double[] x, int... J) { + double[] array = new double[x.length - J.length]; + System.arraycopy(x, 0, array, 0, J[0]); + for (int j = 0; j < J.length - 1; j++) { + System.arraycopy(x, J[j] + 1, array, J[j] - j, J[j + 1] - J[j] - 1); + } + System.arraycopy(x, J[J.length - 1] + 1, array, J[J.length - 1] - J.length + 1, x.length - J[J.length - 1] - 1); + return array; + } + + public static double[][] buildXY(double Xmin, double Xmax, double[] Y) { + int n = Y.length; + double[][] XY = new double[n][2]; + for (int i = 0; i < n; i++) { + XY[i][0] = Xmin + (Xmax - Xmin) * (double) i / (double) (n - 1); + XY[i][1] = Y[i]; + } + return XY; + } + + public static double[][] buildXY(double[] X, double[] Y) { + return mergeColumns(X, Y); + } + + // min/max methods + public static double[] min(double[][] M) { + double[] min = new double[M[0].length]; + for (int j = 0; j < min.length; j++) { + min[j] = M[0][j]; + for (int i = 1; i < M.length; i++) { + if (!Double.isNaN(M[i][j])) { + min[j] = FastMath.min(min[j], M[i][j]); + } + } + } + return min; + } + + public static int min(int... M) { + int min = M[0]; + for (int i = 1; i < M.length; i++) { + min = FastMath.min(min, M[i]); + } + return min; + } + + public static int max(int... M) { + int max = M[0]; + for (int i = 1; i < M.length; i++) { + max = FastMath.max(max, M[i]); + } + return max; + } + + public static double min(double... M) { + double min = M[0]; + for (int i = 1; i < M.length; i++) { + if (!Double.isNaN(M[i])) { + min = FastMath.min(min, M[i]); + } + } + return min; + } + + public static double[] max(double[][] M) { + double[] max = new double[M[0].length]; + for (int j = 0; j < max.length; j++) { + max[j] = M[0][j]; + for (int i = 1; i < M.length; i++) { + if (!Double.isNaN(M[i][j])) { + max[j] = FastMath.max(max[j], M[i][j]); + } + } + } + return max; + } + + public static double max(double... M) { + double max = M[0]; + for (int i = 1; i < M.length; i++) { + if (!Double.isNaN(M[i])) { + max = FastMath.max(max, M[i]); + } + } + return max; + } + + public static int[] minIndex(double[][] M) { + int[] minI = new int[M[0].length]; + for (int j = 0; j < minI.length; j++) { + minI[j] = 0; + for (int i = 1; i < M.length; i++) { + if (M[i][j] < M[minI[j]][j]) { + minI[j] = i; + } + } + + } + return minI; + } + + public static int minIndex(double... M) { + int minI = 0; + for (int i = 1; i < M.length; i++) { + if (M[i] < M[minI]) { + minI = i; + } + } + return minI; + } + + public static int[] maxIndex(double[][] M) { + int[] maxI = new int[M[0].length]; + for (int j = 0; j < maxI.length; j++) { + maxI[j] = 0; + for (int i = 1; i < M.length; i++) { + if (M[i][j] > M[maxI[j]][j]) { + maxI[j] = i; + } + } + } + return maxI; + } + + public static int maxIndex(double... M) { + int maxI = 0; + for (int i = 1; i < M.length; i++) { + if (M[i] > M[maxI]) { + maxI = i; + } + } + return maxI; + } + + // print methods + public static String toString(double[]... v) { + StringBuffer str = new StringBuffer(); + for (int i = 0; i < v.length; i++) { + for (int j = 0; j < v[i].length; j++) { + str.append(v[i][j] + " "); + } + if (i < v.length - 1) { + str.append("\n"); + } + } + return str.toString(); + } + + public static String toString(int[]... v) { + StringBuffer str = new StringBuffer(); + for (int i = 0; i < v.length; i++) { + for (int j = 0; j < v[i].length; j++) { + str.append(v[i][j] + " "); + } + if (i < v.length - 1) { + str.append("\n"); + } + } + return str.toString(); + } + + // check methods + public static void throwError(String msg) { + throw new IllegalArgumentException(msg); + } + + public static void checkColumnDimension(double[][] M, int n) { + for (int i = 0; i < M.length; i++) { + if (M[i].length != n) { + throwError("row " + i + " have " + M[i].length + " columns instead of " + n + " columns expected."); + } + } + } + + public static boolean isColumnDimension(double[][] M, int n) { + for (int i = 0; i < M.length; i++) { + if (M[i].length != n) { + return false; + } + } + return true; + } + + public static void checkRowDimension(double[][] M, int m) { + if (M.length != m) { + throwError("columns have " + M.length + " rows instead of " + m + " rows expected."); + } + } + + public static boolean isRowDimension(double[][] M, int m) { + return M.length == m; + } + + public static void checkLength(double[] M, int n) { + if (M.length != n) { + throwError("row have " + M.length + " elements instead of " + n + " elements expected."); + } + } + + public static boolean isLength(double[] M, int n) { + return M.length == n; + } +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/utils/FastMath.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/utils/FastMath.java new file mode 100644 index 0000000..6ba040b --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/utils/FastMath.java @@ -0,0 +1,3034 @@ +package org.xbib.graphics.graph.jmathplot.utils; + +/* + * Copyright 2012 Jeff Hain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * ============================================================================= + * Notice of fdlibm package this program is partially derived from: + * + * Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved. + * + * Developed at SunSoft, a Sun Microsystems, Inc. business. + * Permission to use, copy, modify, and distribute this + * software is freely granted, provided that this notice + * is preserved. + * ============================================================================= + */ + +/** + * Class providing math treatments that: + * - are meant to be faster than those of java.lang.Math class (depending on + * JVM or JVM options, they might be slower), + * - are still somehow accurate and robust (handling of NaN and such), + * - do not (or not directly) generate objects at run time (no "new"). + *

+ * Other than optimized treatments, a valuable feature of this class is the + * presence of angles normalization methods, derived from those used in + * java.lang.Math (for which, sadly, no API is provided, letting everyone + * with the terrible responsibility to write their own ones). + *

+ * Non-redefined methods of java.lang.Math class are also available, + * for easy replacement. + *

+ * Use of look-up tables: around 1 Mo total, and initialized on class load. + *

+ * - Methods with same signature than Math ones, are meant to return + * "good" approximations on all range. + * - Methods terminating with "Fast" are meant to return "good" approximation + * on a reduced range only. + * - Methods terminating with "Quick" are meant to be quick, but do not + * return a good approximation, and might only work on a reduced range. + *

+ * Properties: + *

+ * - jodk.fastmath.strict (boolean, default is true): + * If true, non-redefined Math methods which results could vary between Math and StrictMath, + * delegate to StrictMath, and if false, to Math. + * Default is true to ensure consistency across various architectures. + *

+ * - jodk.fastmath.usejdk (boolean, default is false): + * If true, redefined Math methods, as well as their "Fast" or "Quick" terminated counterparts, + * delegate to StrictMath or Math, depending on jodk.fastmath.strict property. + *

+ * - jodk.fastmath.fastlog (boolean, default is true): + * If true, using redefined log(double), if false using StrictMath.log(double) or + * Math.log(double), depending on jodk.fastmath.strict property. + * Default is true because jodk.fastmath.strict is true by default, and StrictMath.log(double) + * seems usually slow. + *

+ * - jodk.fastmath.fastsqrt (boolean, default is false): + * If true, using redefined sqrt(double), if false using StrictMath.sqrt(double) or + * Math.sqrt(double), depending on jodk.fastmath.strict property. + * Default is false because StrictMath.sqrt(double) seems to usually delegate to hardware sqrt. + *

+ * Unless jodk.fastmath.strict is false and jodk.fastmath.usejdk is true, these treatments + * are consistent across various architectures, for constants and look-up tables are + * computed with StrictMath, or exact Math methods. + *

+ * --- words, words, words --- + *

+ * "0x42BE0000 percents of the folks out there + * are completely clueless about floating-point." + *

+ * The difference between precision and accuracy: + * "3.177777777777777 is a precise (16 digits) + * but inaccurate (only correct up to the second digit) + * approximation of PI=3.141592653589793(etc.)." + */ +public strictfp final class FastMath { + + /* + * For trigonometric functions, use of look-up tables and Taylor-Lagrange formula + * with 4 derivatives (more take longer to compute and don't add much accuracy, + * less require larger tables (which use more memory, take more time to initialize, + * and are slower to access (at least on the machine they were developed on))). + * + * For angles reduction of cos/sin/tan functions: + * - for small values, instead of reducing angles, and then computing the best index + * for look-up tables, we compute this index right away, and use it for reduction, + * - for large values, treatments derived from fdlibm package are used, as done in + * java.lang.Math. They are faster but still "slow", so if you work with + * large numbers and need speed over accuracy for them, you might want to use + * normalizeXXXFast treatments before your function, or modify cos/sin/tan + * so that they call the fast normalization treatments instead of the accurate ones. + * NB: If an angle is huge (like PI*1e20), in double precision format its last digits + * are zeros, which most likely is not the case for the intended value, and doing + * an accurate reduction on a very inaccurate value is most likely pointless. + * But it gives some sort of coherence that could be needed in some cases. + * + * Multiplication on double appears to be about as fast (or not much slower) than call + * to [], and regrouping some doubles in a private class, to use + * index only once, does not seem to speed things up, so: + * - for uniformly tabulated values, to retrieve the parameter corresponding to + * an index, we recompute it rather than using an array to store it, + * - for cos/sin, we recompute derivatives divided by (multiplied by inverse of) + * factorial each time, rather than storing them in arrays. + * + * Lengths of look-up tables are usually of the form 2^n+1, for their values to be + * of the form ( * k/2^n, k in 0 .. 2^n), so that particular values + * (PI/2, etc.) are "exactly" computed, as well as for other reasons. + * + * Most math treatments I could find on the web, including "fast" ones, + * usually take care of special cases (NaN, etc.) at the beginning, and + * then deal with the general case, which adds a useless overhead for the + * general (and common) case. In this class, special cases are only dealt + * with when needed, and if the general case does not already handle them. + */ + + public class DoubleWrapper { + public double value; + + @Override + public String toString() { + return Double.toString(this.value); + } + } + + public class IntWrapper { + public int value; + + @Override + public String toString() { + return Integer.toString(this.value); + } + } + //-------------------------------------------------------------------------- + // CONFIGURATION + //-------------------------------------------------------------------------- + + private static final boolean STRICT_MATH = false;//LangUtils.getBooleanProperty("jodk.fastmath.strict", true); + + private static final boolean USE_JDK_MATH = false;//LangUtils.getBooleanProperty("jodk.fastmath.usejdk", false); + + /** + * Used for both log(double) and log10(double). + */ + private static final boolean USE_REDEFINED_LOG = true;//LangUtils.getBooleanProperty("jodk.fastmath.fastlog", true); + + private static final boolean USE_REDEFINED_SQRT = true;//LangUtils.getBooleanProperty("jodk.fastmath.fastsqrt", false); + + // Set it to true if FastMath.sqrt(double) is slow (more tables, but less calls to FastMath.sqrt(double)). + private static final boolean USE_POWTABS_FOR_ASIN = true; + + //-------------------------------------------------------------------------- + // GENERAL CONSTANTS + //-------------------------------------------------------------------------- + + /** + * High approximation of PI, which is further from PI + * than the low approximation Math.PI: + * PI ~= 3.14159265358979323846... + * Math.PI ~= 3.141592653589793 + * FastMath.PI_SUP ~= 3.1415926535897936 + */ + public static final double PI_SUP = Math.nextUp(Math.PI); + + private static final double ONE_DIV_F2 = 1 / 2.0; + private static final double ONE_DIV_F3 = 1 / 6.0; + private static final double ONE_DIV_F4 = 1 / 24.0; + + private static final double TWO_POW_24 = Double.longBitsToDouble(0x4170000000000000L); + private static final double TWO_POW_N24 = Double.longBitsToDouble(0x3E70000000000000L); + + private static final double TWO_POW_26 = Double.longBitsToDouble(0x4190000000000000L); + private static final double TWO_POW_N26 = Double.longBitsToDouble(0x3E50000000000000L); + + // First double value (from zero) such as (value+-1/value == value). + private static final double TWO_POW_27 = Double.longBitsToDouble(0x41A0000000000000L); + private static final double TWO_POW_N27 = Double.longBitsToDouble(0x3E40000000000000L); + + private static final double TWO_POW_N28 = Double.longBitsToDouble(0x3E30000000000000L); + + private static final double TWO_POW_52 = Double.longBitsToDouble(0x4330000000000000L); + + private static final double TWO_POW_N54 = Double.longBitsToDouble(0x3C90000000000000L); + + private static final double TWO_POW_N55 = Double.longBitsToDouble(0x3C80000000000000L); + + private static final double TWO_POW_66 = Double.longBitsToDouble(0x4410000000000000L); + + private static final double TWO_POW_450 = Double.longBitsToDouble(0x5C10000000000000L); + private static final double TWO_POW_N450 = Double.longBitsToDouble(0x23D0000000000000L); + + private static final double TWO_POW_750 = Double.longBitsToDouble(0x6ED0000000000000L); + private static final double TWO_POW_N750 = Double.longBitsToDouble(0x1110000000000000L); + + // Smallest double normal value. + private static final double MIN_DOUBLE_NORMAL = Double.longBitsToDouble(0x0010000000000000L); // 2.2250738585072014E-308 + + private static final int MIN_DOUBLE_EXPONENT = -1074; + private static final int MAX_DOUBLE_EXPONENT = 1023; + + private static final int MAX_FLOAT_EXPONENT = 127; + + private static final double LOG_2 = StrictMath.log(2.0); + private static final double LOG_TWO_POW_27 = StrictMath.log(TWO_POW_27); + private static final double LOG_DOUBLE_MAX_VALUE = StrictMath.log(Double.MAX_VALUE); + + private static final double INV_LOG_10 = 1.0 / StrictMath.log(10.0); + + private static final double DOUBLE_BEFORE_60 = Math.nextAfter(60.0, 0.0); + + //-------------------------------------------------------------------------- + // CONSTANTS FOR NORMALIZATIONS + //-------------------------------------------------------------------------- + + /* + * Table of constants for 1/(2*PI), 282 Hex digits (enough for normalizing doubles). + * 1/(2*PI) approximation = sum of ONE_OVER_TWOPI_TAB[i]*2^(-24*(i+1)). + */ + private static final double[] ONE_OVER_TWOPI_TAB = { + 0x28BE60, 0xDB9391, 0x054A7F, 0x09D5F4, 0x7D4D37, 0x7036D8, + 0xA5664F, 0x10E410, 0x7F9458, 0xEAF7AE, 0xF1586D, 0xC91B8E, + 0x909374, 0xB80192, 0x4BBA82, 0x746487, 0x3F877A, 0xC72C4A, + 0x69CFBA, 0x208D7D, 0x4BAED1, 0x213A67, 0x1C09AD, 0x17DF90, + 0x4E6475, 0x8E60D4, 0xCE7D27, 0x2117E2, 0xEF7E4A, 0x0EC7FE, + 0x25FFF7, 0x816603, 0xFBCBC4, 0x62D682, 0x9B47DB, 0x4D9FB3, + 0xC9F2C2, 0x6DD3D1, 0x8FD9A7, 0x97FA8B, 0x5D49EE, 0xB1FAF9, + 0x7C5ECF, 0x41CE7D, 0xE294A4, 0xBA9AFE, 0xD7EC47}; + + /* + * Constants for 2*PI. Only the 23 most significant bits of each mantissa are used. + * 2*PI approximation = sum of TWOPI_TAB. + */ + private static final double TWOPI_TAB0 = Double.longBitsToDouble(0x401921FB40000000L); + private static final double TWOPI_TAB1 = Double.longBitsToDouble(0x3E94442D00000000L); + private static final double TWOPI_TAB2 = Double.longBitsToDouble(0x3D18469880000000L); + private static final double TWOPI_TAB3 = Double.longBitsToDouble(0x3B98CC5160000000L); + private static final double TWOPI_TAB4 = Double.longBitsToDouble(0x3A101B8380000000L); + + private static final double INVPIO2 = Double.longBitsToDouble(0x3FE45F306DC9C883L); // 6.36619772367581382433e-01 53 bits of 2/pi + private static final double PIO2_HI = Double.longBitsToDouble(0x3FF921FB54400000L); // 1.57079632673412561417e+00 first 33 bits of pi/2 + private static final double PIO2_LO = Double.longBitsToDouble(0x3DD0B4611A626331L); // 6.07710050650619224932e-11 pi/2 - PIO2_HI + private static final double INVTWOPI = INVPIO2 / 4; + private static final double TWOPI_HI = 4 * PIO2_HI; + private static final double TWOPI_LO = 4 * PIO2_LO; + + // fdlibm uses 2^19*PI/2 here, but we normalize with % 2*PI instead of % PI/2, + // and we can bear some more error. + private static final double NORMALIZE_ANGLE_MAX_MEDIUM_DOUBLE = StrictMath.pow(2, 20) * (2 * Math.PI); + + /** + * 2*Math.PI, normalized into [-PI,PI]. + * Computed using normalizeMinusPiPi(double). + */ + private static final double TWO_MATH_PI_IN_MINUS_PI_PI = -2.449293598153844E-16; + + //-------------------------------------------------------------------------- + // CONSTANTS AND TABLES FOR COS, SIN + //-------------------------------------------------------------------------- + + private static final int SIN_COS_TABS_SIZE = (1 << getTabSizePower(11)) + 1; + private static final double SIN_COS_DELTA_HI = TWOPI_HI / (SIN_COS_TABS_SIZE - 1); + private static final double SIN_COS_DELTA_LO = TWOPI_LO / (SIN_COS_TABS_SIZE - 1); + private static final double SIN_COS_INDEXER = 1 / (SIN_COS_DELTA_HI + SIN_COS_DELTA_LO); + private static final double[] sinTab = new double[SIN_COS_TABS_SIZE]; + private static final double[] cosTab = new double[SIN_COS_TABS_SIZE]; + + // Max abs value for fast modulo, above which we use regular angle normalization. + // This value must be < (Integer.MAX_VALUE / SIN_COS_INDEXER), to stay in range of int type. + // The higher it is, the higher the error, but also the faster it is for lower values. + // If you set it to ((Integer.MAX_VALUE / SIN_COS_INDEXER) * 0.99), worse accuracy on double range is about 1e-10. + private static final double SIN_COS_MAX_VALUE_FOR_INT_MODULO = ((Integer.MAX_VALUE >> 9) / SIN_COS_INDEXER) * 0.99; + + //-------------------------------------------------------------------------- + // CONSTANTS AND TABLES FOR TAN + //-------------------------------------------------------------------------- + + // We use the following formula: + // 1) tan(-x) = -tan(x) + // 2) tan(x) = 1/tan(PI/2-x) + // ---> we only have to compute tan(x) on [0,A] with PI/4<=A= 45deg, and supposed to be >= 51.4deg, as fdlibm code is not + // supposed to work with values inferior to that (51.4deg is about + // (PI/2-Double.longBitsToDouble(0x3FE5942800000000L))). + private static final double TAN_MAX_VALUE_FOR_TABS = Math.toRadians(77.0); + + private static final int TAN_TABS_SIZE = (int) ((TAN_MAX_VALUE_FOR_TABS / (Math.PI / 2)) * (TAN_VIRTUAL_TABS_SIZE - 1)) + 1; + private static final double TAN_DELTA_HI = PIO2_HI / (TAN_VIRTUAL_TABS_SIZE - 1); + private static final double TAN_DELTA_LO = PIO2_LO / (TAN_VIRTUAL_TABS_SIZE - 1); + private static final double TAN_INDEXER = 1 / (TAN_DELTA_HI + TAN_DELTA_LO); + private static final double[] tanTab = new double[TAN_TABS_SIZE]; + private static final double[] tanDer1DivF1Tab = new double[TAN_TABS_SIZE]; + private static final double[] tanDer2DivF2Tab = new double[TAN_TABS_SIZE]; + private static final double[] tanDer3DivF3Tab = new double[TAN_TABS_SIZE]; + private static final double[] tanDer4DivF4Tab = new double[TAN_TABS_SIZE]; + + // Max abs value for fast modulo, above which we use regular angle normalization. + // This value must be < (Integer.MAX_VALUE / TAN_INDEXER), to stay in range of int type. + // The higher it is, the higher the error, but also the faster it is for lower values. + private static final double TAN_MAX_VALUE_FOR_INT_MODULO = (((Integer.MAX_VALUE >> 9) / TAN_INDEXER) * 0.99); + + //-------------------------------------------------------------------------- + // CONSTANTS AND TABLES FOR ACOS, ASIN + //-------------------------------------------------------------------------- + + // We use the following formula: + // 1) acos(x) = PI/2 - asin(x) + // 2) asin(-x) = -asin(x) + // ---> we only have to compute asin(x) on [0,1]. + // For values not close to +-1, we use look-up tables; + // for values near +-1, we use code derived from fdlibm. + + // Supposed to be >= sin(77.2deg), as fdlibm code is supposed to work with values > 0.975, + // but seems to work well enough as long as value >= sin(25deg). + private static final double ASIN_MAX_VALUE_FOR_TABS = StrictMath.sin(Math.toRadians(73.0)); + + private static final int ASIN_TABS_SIZE = (1 << getTabSizePower(13)) + 1; + private static final double ASIN_DELTA = ASIN_MAX_VALUE_FOR_TABS / (ASIN_TABS_SIZE - 1); + private static final double ASIN_INDEXER = 1 / ASIN_DELTA; + private static final double[] asinTab = new double[ASIN_TABS_SIZE]; + private static final double[] asinDer1DivF1Tab = new double[ASIN_TABS_SIZE]; + private static final double[] asinDer2DivF2Tab = new double[ASIN_TABS_SIZE]; + private static final double[] asinDer3DivF3Tab = new double[ASIN_TABS_SIZE]; + private static final double[] asinDer4DivF4Tab = new double[ASIN_TABS_SIZE]; + + private static final double ASIN_MAX_VALUE_FOR_POWTABS = StrictMath.sin(Math.toRadians(88.6)); + private static final int ASIN_POWTABS_POWER = 84; + + private static final double ASIN_POWTABS_ONE_DIV_MAX_VALUE = 1 / ASIN_MAX_VALUE_FOR_POWTABS; + private static final int ASIN_POWTABS_SIZE = USE_POWTABS_FOR_ASIN ? (1 << getTabSizePower(12)) + 1 : 0; + private static final int ASIN_POWTABS_SIZE_MINUS_ONE = ASIN_POWTABS_SIZE - 1; + private static final double[] asinParamPowTab = new double[ASIN_POWTABS_SIZE]; + private static final double[] asinPowTab = new double[ASIN_POWTABS_SIZE]; + private static final double[] asinDer1DivF1PowTab = new double[ASIN_POWTABS_SIZE]; + private static final double[] asinDer2DivF2PowTab = new double[ASIN_POWTABS_SIZE]; + private static final double[] asinDer3DivF3PowTab = new double[ASIN_POWTABS_SIZE]; + private static final double[] asinDer4DivF4PowTab = new double[ASIN_POWTABS_SIZE]; + + private static final double ASIN_PIO2_HI = Double.longBitsToDouble(0x3FF921FB54442D18L); // 1.57079632679489655800e+00 + private static final double ASIN_PIO2_LO = Double.longBitsToDouble(0x3C91A62633145C07L); // 6.12323399573676603587e-17 + private static final double ASIN_PS0 = Double.longBitsToDouble(0x3fc5555555555555L); // 1.66666666666666657415e-01 + private static final double ASIN_PS1 = Double.longBitsToDouble(0xbfd4d61203eb6f7dL); // -3.25565818622400915405e-01 + private static final double ASIN_PS2 = Double.longBitsToDouble(0x3fc9c1550e884455L); // 2.01212532134862925881e-01 + private static final double ASIN_PS3 = Double.longBitsToDouble(0xbfa48228b5688f3bL); // -4.00555345006794114027e-02 + private static final double ASIN_PS4 = Double.longBitsToDouble(0x3f49efe07501b288L); // 7.91534994289814532176e-04 + private static final double ASIN_PS5 = Double.longBitsToDouble(0x3f023de10dfdf709L); // 3.47933107596021167570e-05 + private static final double ASIN_QS1 = Double.longBitsToDouble(0xc0033a271c8a2d4bL); // -2.40339491173441421878e+00 + private static final double ASIN_QS2 = Double.longBitsToDouble(0x40002ae59c598ac8L); // 2.02094576023350569471e+00 + private static final double ASIN_QS3 = Double.longBitsToDouble(0xbfe6066c1b8d0159L); // -6.88283971605453293030e-01 + private static final double ASIN_QS4 = Double.longBitsToDouble(0x3fb3b8c5b12e9282L); // 7.70381505559019352791e-02 + + //-------------------------------------------------------------------------- + // CONSTANTS AND TABLES FOR ATAN + //-------------------------------------------------------------------------- + + // We use the formula atan(-x) = -atan(x) + // ---> we only have to compute atan(x) on [0,+infinity[. + // For values corresponding to angles not close to +-PI/2, we use look-up tables; + // for values corresponding to angles near +-PI/2, we use code derived from fdlibm. + + // Supposed to be >= tan(67.7deg), as fdlibm code is supposed to work with values > 2.4375. + private static final double ATAN_MAX_VALUE_FOR_TABS = StrictMath.tan(Math.toRadians(74.0)); + + private static final int ATAN_TABS_SIZE = (1 << getTabSizePower(12)) + 1; + private static final double ATAN_DELTA = ATAN_MAX_VALUE_FOR_TABS / (ATAN_TABS_SIZE - 1); + private static final double ATAN_INDEXER = 1 / ATAN_DELTA; + private static final double[] atanTab = new double[ATAN_TABS_SIZE]; + private static final double[] atanDer1DivF1Tab = new double[ATAN_TABS_SIZE]; + private static final double[] atanDer2DivF2Tab = new double[ATAN_TABS_SIZE]; + private static final double[] atanDer3DivF3Tab = new double[ATAN_TABS_SIZE]; + private static final double[] atanDer4DivF4Tab = new double[ATAN_TABS_SIZE]; + + private static final double ATAN_HI3 = Double.longBitsToDouble(0x3ff921fb54442d18L); // 1.57079632679489655800e+00 atan(inf)hi + private static final double ATAN_LO3 = Double.longBitsToDouble(0x3c91a62633145c07L); // 6.12323399573676603587e-17 atan(inf)lo + private static final double ATAN_AT0 = Double.longBitsToDouble(0x3fd555555555550dL); // 3.33333333333329318027e-01 + private static final double ATAN_AT1 = Double.longBitsToDouble(0xbfc999999998ebc4L); // -1.99999999998764832476e-01 + private static final double ATAN_AT2 = Double.longBitsToDouble(0x3fc24924920083ffL); // 1.42857142725034663711e-01 + private static final double ATAN_AT3 = Double.longBitsToDouble(0xbfbc71c6fe231671L); // -1.11111104054623557880e-01 + private static final double ATAN_AT4 = Double.longBitsToDouble(0x3fb745cdc54c206eL); // 9.09088713343650656196e-02 + private static final double ATAN_AT5 = Double.longBitsToDouble(0xbfb3b0f2af749a6dL); // -7.69187620504482999495e-02 + private static final double ATAN_AT6 = Double.longBitsToDouble(0x3fb10d66a0d03d51L); // 6.66107313738753120669e-02 + private static final double ATAN_AT7 = Double.longBitsToDouble(0xbfadde2d52defd9aL); // -5.83357013379057348645e-02 + private static final double ATAN_AT8 = Double.longBitsToDouble(0x3fa97b4b24760debL); // 4.97687799461593236017e-02 + private static final double ATAN_AT9 = Double.longBitsToDouble(0xbfa2b4442c6a6c2fL); // -3.65315727442169155270e-02 + private static final double ATAN_AT10 = Double.longBitsToDouble(0x3f90ad3ae322da11L); // 1.62858201153657823623e-02 + + //-------------------------------------------------------------------------- + // CONSTANTS AND TABLES FOR EXP AND EXPM1 + //-------------------------------------------------------------------------- + + private static final double EXP_OVERFLOW_LIMIT = Double.longBitsToDouble(0x40862E42FEFA39EFL); // 7.09782712893383973096e+02 + private static final double EXP_UNDERFLOW_LIMIT = Double.longBitsToDouble(0xC0874910D52D3051L); // -7.45133219101941108420e+02 + private static final double EXP_MIN_INT_LIMIT = -705; + private static final int EXP_LO_DISTANCE_TO_ZERO_POT = 0; + private static final int EXP_LO_DISTANCE_TO_ZERO = (1 << EXP_LO_DISTANCE_TO_ZERO_POT); + private static final int EXP_LO_TAB_SIZE_POT = getTabSizePower(11); + private static final int EXP_LO_TAB_SIZE = (1 << EXP_LO_TAB_SIZE_POT) + 1; + private static final int EXP_LO_TAB_MID_INDEX = ((EXP_LO_TAB_SIZE - 1) / 2); + private static final int EXP_LO_INDEXING = EXP_LO_TAB_MID_INDEX / EXP_LO_DISTANCE_TO_ZERO; + private static final int EXP_LO_INDEXING_DIV_SHIFT = EXP_LO_TAB_SIZE_POT - 1 - EXP_LO_DISTANCE_TO_ZERO_POT; + private static final double[] expHiTab = new double[1 + (int) EXP_OVERFLOW_LIMIT]; + private static final double[] expHiInvTab = new double[1 - (int) EXP_UNDERFLOW_LIMIT]; + private static final double[] expLoPosTab = new double[EXP_LO_TAB_SIZE]; + private static final double[] expLoNegTab = new double[EXP_LO_TAB_SIZE]; + + //-------------------------------------------------------------------------- + // CONSTANTS FOR QUICK EXP + //-------------------------------------------------------------------------- + + private static final double EXP_QUICK_A = TWO_POW_52 / LOG_2; + private static final double EXP_QUICK_B = MAX_DOUBLE_EXPONENT * TWO_POW_52; + private static final double EXP_QUICK_C = Math.ceil((StrictMath.log(LOG_2 + 2 / Math.E) - LOG_2 - StrictMath.log(LOG_2)) * EXP_QUICK_A); + + //-------------------------------------------------------------------------- + // CONSTANTS AND TABLES FOR LOG AND LOG1P + //-------------------------------------------------------------------------- + + private static final int LOG_BITS = getTabSizePower(12); + private static final int LOG_TAB_SIZE = (1 << LOG_BITS); + private static final double[] logXLogTab = new double[LOG_TAB_SIZE]; + private static final double[] logXTab = new double[LOG_TAB_SIZE]; + private static final double[] logXInvTab = new double[LOG_TAB_SIZE]; + + //-------------------------------------------------------------------------- + // TABLE FOR POWERS OF TWO + //-------------------------------------------------------------------------- + + private static final double[] twoPowTab = new double[(MAX_DOUBLE_EXPONENT - MIN_DOUBLE_EXPONENT) + 1]; + + //-------------------------------------------------------------------------- + // CONSTANTS AND TABLES FOR SQRT + //-------------------------------------------------------------------------- + + private static final int SQRT_LO_BITS = getTabSizePower(12); + private static final int SQRT_LO_TAB_SIZE = (1 << SQRT_LO_BITS); + private static final double[] sqrtXSqrtHiTab = new double[MAX_DOUBLE_EXPONENT - MIN_DOUBLE_EXPONENT + 1]; + private static final double[] sqrtXSqrtLoTab = new double[SQRT_LO_TAB_SIZE]; + private static final double[] sqrtSlopeHiTab = new double[MAX_DOUBLE_EXPONENT - MIN_DOUBLE_EXPONENT + 1]; + private static final double[] sqrtSlopeLoTab = new double[SQRT_LO_TAB_SIZE]; + + //-------------------------------------------------------------------------- + // CONSTANTS AND TABLES FOR CBRT + //-------------------------------------------------------------------------- + + private static final int CBRT_LO_BITS = getTabSizePower(12); + private static final int CBRT_LO_TAB_SIZE = (1 << CBRT_LO_BITS); + // For CBRT_LO_BITS = 12: + // cbrtXCbrtLoTab[0] = 1.0. + // cbrtXCbrtLoTab[1] = cbrt(1. 000000000000 1111111111111111111111111111111111111111b) + // cbrtXCbrtLoTab[2] = cbrt(1. 000000000001 1111111111111111111111111111111111111111b) + // cbrtXCbrtLoTab[3] = cbrt(1. 000000000010 1111111111111111111111111111111111111111b) + // cbrtXCbrtLoTab[4] = cbrt(1. 000000000011 1111111111111111111111111111111111111111b) + // etc. + private static final double[] cbrtXCbrtHiTab = new double[MAX_DOUBLE_EXPONENT - MIN_DOUBLE_EXPONENT + 1]; + private static final double[] cbrtXCbrtLoTab = new double[CBRT_LO_TAB_SIZE]; + private static final double[] cbrtSlopeHiTab = new double[MAX_DOUBLE_EXPONENT - MIN_DOUBLE_EXPONENT + 1]; + private static final double[] cbrtSlopeLoTab = new double[CBRT_LO_TAB_SIZE]; + + //-------------------------------------------------------------------------- + // PUBLIC TREATMENTS + //-------------------------------------------------------------------------- + + /** + * @param angle Angle in radians. + * @return Angle cosine. + */ + public static double cos(double angle) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.cos(angle) : Math.cos(angle); + } + angle = Math.abs(angle); + if (angle > SIN_COS_MAX_VALUE_FOR_INT_MODULO) { + // Faster than using normalizeZeroTwoPi. + angle = remainderTwoPi(angle); + if (angle < 0.0) { + angle += 2 * Math.PI; + } + } + // index: possibly outside tables range. + int index = (int) (angle * SIN_COS_INDEXER + 0.5); + double delta = (angle - index * SIN_COS_DELTA_HI) - index * SIN_COS_DELTA_LO; + // Making sure index is within tables range. + // Last value of each table is the same than first, so we ignore it (tabs size minus one) for modulo. + index &= (SIN_COS_TABS_SIZE - 2); // index % (SIN_COS_TABS_SIZE-1) + double indexCos = cosTab[index]; + double indexSin = sinTab[index]; + return indexCos + delta * (-indexSin + delta * (-indexCos * ONE_DIV_F2 + delta * (indexSin * ONE_DIV_F3 + delta * indexCos * ONE_DIV_F4))); + } + + /** + * Quick cosine, with accuracy of about 1.6e-3 (PI/'look-up tabs size') + * for |angle| < 6588397.0 (Integer.MAX_VALUE * (2*PI/'look-up tabs size')), + * and no accuracy at all for larger values. + * + * @param angle Angle in radians. + * @return Angle cosine. + */ + public static double cosQuick(double angle) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.cos(angle) : Math.cos(angle); + } + return cosTab[((int) (Math.abs(angle) * SIN_COS_INDEXER + 0.5)) & (SIN_COS_TABS_SIZE - 2)]; + } + + /** + * @param angle Angle in radians. + * @return Angle sine. + */ + public static double sin(double angle) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.sin(angle) : Math.sin(angle); + } + boolean negateResult; + if (angle < 0.0) { + angle = -angle; + negateResult = true; + } else { + negateResult = false; + } + if (angle > SIN_COS_MAX_VALUE_FOR_INT_MODULO) { + // Faster than using normalizeZeroTwoPi. + angle = remainderTwoPi(angle); + if (angle < 0.0) { + angle += 2 * Math.PI; + } + } + int index = (int) (angle * SIN_COS_INDEXER + 0.5); + double delta = (angle - index * SIN_COS_DELTA_HI) - index * SIN_COS_DELTA_LO; + index &= (SIN_COS_TABS_SIZE - 2); // index % (SIN_COS_TABS_SIZE-1) + double indexSin = sinTab[index]; + double indexCos = cosTab[index]; + double result = indexSin + delta * (indexCos + delta * (-indexSin * ONE_DIV_F2 + delta * (-indexCos * ONE_DIV_F3 + delta * indexSin * ONE_DIV_F4))); + return negateResult ? -result : result; + } + + /** + * Quick sine, with accuracy of about 1.6e-3 (PI/'look-up tabs size') + * for |angle| < 6588397.0 (Integer.MAX_VALUE * (2*PI/'look-up tabs size')), + * and no accuracy at all for larger values. + * + * @param angle Angle in radians. + * @return Angle sine. + */ + public static double sinQuick(double angle) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.sin(angle) : Math.sin(angle); + } + return cosTab[((int) (Math.abs(angle - Math.PI / 2) * SIN_COS_INDEXER + 0.5)) & (SIN_COS_TABS_SIZE - 2)]; + } + + /** + * Computes sine and cosine together, at the cost of... a dependency of this class with DoubleWrapper. + * + * @param angle Angle in radians. + * @param sine Angle sine. + * @param cosine Angle cosine. + */ + public static void sinAndCos(double angle, DoubleWrapper sine, DoubleWrapper cosine) { + if (USE_JDK_MATH) { + sine.value = STRICT_MATH ? StrictMath.sin(angle) : Math.sin(angle); + cosine.value = STRICT_MATH ? StrictMath.cos(angle) : Math.cos(angle); + return; + } + // Using the same algorithm than sin(double) method, and computing also cosine at the end. + boolean negateResult; + if (angle < 0.0) { + angle = -angle; + negateResult = true; + } else { + negateResult = false; + } + if (angle > SIN_COS_MAX_VALUE_FOR_INT_MODULO) { + // Faster than using normalizeZeroTwoPi. + angle = remainderTwoPi(angle); + if (angle < 0.0) { + angle += 2 * Math.PI; + } + } + int index = (int) (angle * SIN_COS_INDEXER + 0.5); + double delta = (angle - index * SIN_COS_DELTA_HI) - index * SIN_COS_DELTA_LO; + index &= (SIN_COS_TABS_SIZE - 2); // index % (SIN_COS_TABS_SIZE-1) + double indexSin = sinTab[index]; + double indexCos = cosTab[index]; + double result = indexSin + delta * (indexCos + delta * (-indexSin * ONE_DIV_F2 + delta * (-indexCos * ONE_DIV_F3 + delta * indexSin * ONE_DIV_F4))); + sine.value = negateResult ? -result : result; + cosine.value = indexCos + delta * (-indexSin + delta * (-indexCos * ONE_DIV_F2 + delta * (indexSin * ONE_DIV_F3 + delta * indexCos * ONE_DIV_F4))); + } + + /** + * @param angle Angle in radians. + * @return Angle tangent. + */ + public static double tan(double angle) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.tan(angle) : Math.tan(angle); + } + if (Math.abs(angle) > TAN_MAX_VALUE_FOR_INT_MODULO) { + // Faster than using normalizeMinusHalfPiHalfPi. + angle = remainderTwoPi(angle); + if (angle < -Math.PI / 2) { + angle += Math.PI; + } else if (angle > Math.PI / 2) { + angle -= Math.PI; + } + } + boolean negateResult; + if (angle < 0.0) { + angle = -angle; + negateResult = true; + } else { + negateResult = false; + } + int index = (int) (angle * TAN_INDEXER + 0.5); + double delta = (angle - index * TAN_DELTA_HI) - index * TAN_DELTA_LO; + // index modulo PI, i.e. 2*(virtual tab size minus one). + index &= (2 * (TAN_VIRTUAL_TABS_SIZE - 1) - 1); // index % (2*(TAN_VIRTUAL_TABS_SIZE-1)) + // Here, index is in [0,2*(TAN_VIRTUAL_TABS_SIZE-1)-1], i.e. indicates an angle in [0,PI[. + if (index > (TAN_VIRTUAL_TABS_SIZE - 1)) { + index = (2 * (TAN_VIRTUAL_TABS_SIZE - 1)) - index; + delta = -delta; + negateResult = !negateResult; + } + double result; + if (index < TAN_TABS_SIZE) { + result = tanTab[index] + delta * (tanDer1DivF1Tab[index] + delta * (tanDer2DivF2Tab[index] + delta * (tanDer3DivF3Tab[index] + delta * tanDer4DivF4Tab[index]))); + } else { // angle in ]TAN_MAX_VALUE_FOR_TABS,TAN_MAX_VALUE_FOR_INT_MODULO], or angle is NaN + // Using tan(angle) == 1/tan(PI/2-angle) formula: changing angle (index and delta), and inverting. + index = (TAN_VIRTUAL_TABS_SIZE - 1) - index; + result = 1 / (tanTab[index] - delta * (tanDer1DivF1Tab[index] - delta * (tanDer2DivF2Tab[index] - delta * (tanDer3DivF3Tab[index] - delta * tanDer4DivF4Tab[index])))); + } + return negateResult ? -result : result; + } + + /** + * @param value Value in [-1,1]. + * @return Value arccosine, in radians, in [0,PI]. + */ + public static double acos(double value) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.acos(value) : Math.acos(value); + } + return Math.PI / 2 - FastMath.asin(value); + } + + /** + * If value is not NaN and is outside [-1,1] range, closest value in this range is used. + * + * @param value Value in [-1,1]. + * @return Value arccosine, in radians, in [0,PI]. + */ + public static double acosInRange(double value) { + if (value <= -1) { + return Math.PI; + } else if (value >= 1) { + return 0.0; + } else { + return FastMath.acos(value); + } + } + + /** + * @param value Value in [-1,1]. + * @return Value arcsine, in radians, in [-PI/2,PI/2]. + */ + public static double asin(double value) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.asin(value) : Math.asin(value); + } + boolean negateResult; + if (value < 0.0) { + value = -value; + negateResult = true; + } else { + negateResult = false; + } + if (value <= ASIN_MAX_VALUE_FOR_TABS) { + int index = (int) (value * ASIN_INDEXER + 0.5); + double delta = value - index * ASIN_DELTA; + double result = asinTab[index] + delta * (asinDer1DivF1Tab[index] + delta * (asinDer2DivF2Tab[index] + delta * (asinDer3DivF3Tab[index] + delta * asinDer4DivF4Tab[index]))); + return negateResult ? -result : result; + } else if (USE_POWTABS_FOR_ASIN && (value <= ASIN_MAX_VALUE_FOR_POWTABS)) { + int index = (int) (FastMath.powFast(value * ASIN_POWTABS_ONE_DIV_MAX_VALUE, ASIN_POWTABS_POWER) * ASIN_POWTABS_SIZE_MINUS_ONE + 0.5); + double delta = value - asinParamPowTab[index]; + double result = asinPowTab[index] + delta * (asinDer1DivF1PowTab[index] + delta * (asinDer2DivF2PowTab[index] + delta * (asinDer3DivF3PowTab[index] + delta * asinDer4DivF4PowTab[index]))); + return negateResult ? -result : result; + } else { // value > ASIN_MAX_VALUE_FOR_TABS, or value is NaN + // This part is derived from fdlibm. + if (value < 1.0) { + double t = (1.0 - value) * 0.5; + double p = t * (ASIN_PS0 + t * (ASIN_PS1 + t * (ASIN_PS2 + t * (ASIN_PS3 + t * (ASIN_PS4 + t * ASIN_PS5))))); + double q = 1.0 + t * (ASIN_QS1 + t * (ASIN_QS2 + t * (ASIN_QS3 + t * ASIN_QS4))); + double s = FastMath.sqrt(t); + double z = s + s * (p / q); + double result = ASIN_PIO2_HI - ((z + z) - ASIN_PIO2_LO); + return negateResult ? -result : result; + } else { // value >= 1.0, or value is NaN + if (value == 1.0) { + return negateResult ? -Math.PI / 2 : Math.PI / 2; + } else { + return Double.NaN; + } + } + } + } + + /** + * If value is not NaN and is outside [-1,1] range, closest value in this range is used. + * + * @param value Value in [-1,1]. + * @return Value arcsine, in radians, in [-PI/2,PI/2]. + */ + public static double asinInRange(double value) { + if (value <= -1) { + return -Math.PI / 2; + } else if (value >= 1) { + return Math.PI / 2; + } else { + return FastMath.asin(value); + } + } + + /** + * @param value A double value. + * @return Value arctangent, in radians, in [-PI/2,PI/2]. + */ + public static double atan(double value) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.atan(value) : Math.atan(value); + } + boolean negateResult; + if (value < 0.0) { + value = -value; + negateResult = true; + } else { + negateResult = false; + } + if (value == 1.0) { + // We want "exact" result for 1.0. + return negateResult ? -Math.PI / 4 : Math.PI / 4; + } else if (value <= ATAN_MAX_VALUE_FOR_TABS) { + int index = (int) (value * ATAN_INDEXER + 0.5); + double delta = value - index * ATAN_DELTA; + double result = atanTab[index] + delta * (atanDer1DivF1Tab[index] + delta * (atanDer2DivF2Tab[index] + delta * (atanDer3DivF3Tab[index] + delta * atanDer4DivF4Tab[index]))); + return negateResult ? -result : result; + } else { // value > ATAN_MAX_VALUE_FOR_TABS, or value is NaN + // This part is derived from fdlibm. + if (value < TWO_POW_66) { + double x = -1 / value; + double x2 = x * x; + double x4 = x2 * x2; + double s1 = x2 * (ATAN_AT0 + x4 * (ATAN_AT2 + x4 * (ATAN_AT4 + x4 * (ATAN_AT6 + x4 * (ATAN_AT8 + x4 * ATAN_AT10))))); + double s2 = x4 * (ATAN_AT1 + x4 * (ATAN_AT3 + x4 * (ATAN_AT5 + x4 * (ATAN_AT7 + x4 * ATAN_AT9)))); + double result = ATAN_HI3 - ((x * (s1 + s2) - ATAN_LO3) - x); + return negateResult ? -result : result; + } else { // value >= 2^66, or value is NaN + if (Double.isNaN(value)) { + return Double.NaN; + } else { + return negateResult ? -Math.PI / 2 : Math.PI / 2; + } + } + } + } + + /** + * For special values for which multiple conventions could be adopted, behaves like Math.atan2(double,double). + * + * @param y Coordinate on y axis. + * @param x Coordinate on x axis. + * @return Angle from x axis positive side to (x,y) position, in radians, in [-PI,PI]. + * Angle measure is positive when going from x axis to y axis (positive sides). + */ + public static double atan2(double y, double x) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.atan2(y, x) : Math.atan2(y, x); + } + if (x > 0.0) { + if (y == 0.0) { + return (1 / y == Double.NEGATIVE_INFINITY) ? -0.0 : 0.0; + } + if (x == Double.POSITIVE_INFINITY) { + if (y == Double.POSITIVE_INFINITY) { + return Math.PI / 4; + } else if (y == Double.NEGATIVE_INFINITY) { + return -Math.PI / 4; + } else if (y > 0.0) { + return 0.0; + } else if (y < 0.0) { + return -0.0; + } else { + return Double.NaN; + } + } else { + return FastMath.atan(y / x); + } + } else if (x < 0.0) { + if (y == 0.0) { + return (1 / y == Double.NEGATIVE_INFINITY) ? -Math.PI : Math.PI; + } + if (x == Double.NEGATIVE_INFINITY) { + if (y == Double.POSITIVE_INFINITY) { + return 3 * Math.PI / 4; + } else if (y == Double.NEGATIVE_INFINITY) { + return -3 * Math.PI / 4; + } else if (y > 0.0) { + return Math.PI; + } else if (y < 0.0) { + return -Math.PI; + } else { + return Double.NaN; + } + } else if (y > 0.0) { + return Math.PI / 2 + FastMath.atan(-x / y); + } else if (y < 0.0) { + return -Math.PI / 2 - FastMath.atan(x / y); + } else { + return Double.NaN; + } + } else if (x == 0.0) { + if (y == 0.0) { + if (1 / x == Double.NEGATIVE_INFINITY) { + return (1 / y == Double.NEGATIVE_INFINITY) ? -Math.PI : Math.PI; + } else { + return (1 / y == Double.NEGATIVE_INFINITY) ? -0.0 : 0.0; + } + } + if (y > 0.0) { + return Math.PI / 2; + } else if (y < 0.0) { + return -Math.PI / 2; + } else { + return Double.NaN; + } + } else { + return Double.NaN; + } + } + + /** + * @param value A double value. + * @return Value hyperbolic cosine. + */ + public static double cosh(double value) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.cosh(value) : Math.cosh(value); + } + // cosh(x) = (exp(x)+exp(-x))/2 + if (value < 0.0) { + value = -value; + } + if (value < LOG_TWO_POW_27) { + if (value < TWO_POW_N27) { + // cosh(x) + // = (exp(x)+exp(-x))/2 + // = ((1+x+x^2/2!+...) + (1-x+x^2/2!-...))/2 + // = 1+x^2/2!+x^4/4!+... + // For value of x small in magnitude, the sum of the terms does not add to 1. + return 1; + } else { + double t = FastMath.exp(value); + return 0.5 * (t + 1 / t); + } + } else if (value < LOG_DOUBLE_MAX_VALUE) { + return 0.5 * FastMath.exp(value); + } else { + double t = FastMath.exp(value * 0.5); + return (0.5 * t) * t; + } + } + + /** + * @param value A double value. + * @return Value hyperbolic sine. + */ + public static double sinh(double value) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.sinh(value) : Math.sinh(value); + } + // sinh(x) = (exp(x)-exp(-x))/2 + double h; + if (value < 0.0) { + value = -value; + h = -0.5; + } else { + h = 0.5; + } + if (value < 22.0) { + if (value < TWO_POW_N28) { + return (h < 0.0) ? -value : value; + } else { + double t = FastMath.expm1(value); + // Might be more accurate, if value < 1: return h*((t+t)-t*t/(t+1.0)). + return h * (t + t / (t + 1.0)); + } + } else if (value < LOG_DOUBLE_MAX_VALUE) { + return h * FastMath.exp(value); + } else { + double t = FastMath.exp(value * 0.5); + return (h * t) * t; + } + } + + /** + * Computes hyperbolic sine and hyperbolic cosine together, at the cost of... a dependency of this class with DoubleWrapper. + * + * @param value A double value. + * @param hsine Value hyperbolic sine. + * @param hcosine Value hyperbolic cosine. + */ + public static void sinhAndCosh(double value, DoubleWrapper hsine, DoubleWrapper hcosine) { + if (USE_JDK_MATH) { + hsine.value = STRICT_MATH ? StrictMath.sinh(value) : Math.sinh(value); + hcosine.value = STRICT_MATH ? StrictMath.cosh(value) : Math.cosh(value); + return; + } + // Mixup of sinh and cosh treatments: if you modify them, + // you might want to also modify this. + double h; + if (value < 0.0) { + value = -value; + h = -0.5; + } else { + h = 0.5; + } + // LOG_TWO_POW_27 = 18.714973875118524 + if (value < LOG_TWO_POW_27) { // test from cosh + // sinh + if (value < TWO_POW_N28) { + hsine.value = (h < 0.0) ? -value : value; + } else { + double t = FastMath.expm1(value); + hsine.value = h * (t + t / (t + 1.0)); + } + // cosh + if (value < TWO_POW_N27) { + hcosine.value = 1; + } else { + double t = FastMath.exp(value); + hcosine.value = 0.5 * (t + 1 / t); + } + } else if (value < 22.0) { // test from sinh + // Here, value is in [18.714973875118524,22.0[. + double t = FastMath.expm1(value); + hsine.value = h * (t + t / (t + 1.0)); + hcosine.value = 0.5 * (t + 1.0); + } else { + if (value < LOG_DOUBLE_MAX_VALUE) { + hsine.value = h * FastMath.exp(value); + } else { + double t = FastMath.exp(value * 0.5); + hsine.value = (h * t) * t; + } + hcosine.value = Math.abs(hsine.value); + } + } + + /** + * @param value A double value. + * @return Value hyperbolic tangent. + */ + public static double tanh(double value) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.tanh(value) : Math.tanh(value); + } + // tanh(x) = sinh(x)/cosh(x) + // = (exp(x)-exp(-x))/(exp(x)+exp(-x)) + // = (exp(2*x)-1)/(exp(2*x)+1) + boolean negateResult; + if (value < 0.0) { + value = -value; + negateResult = true; + } else { + negateResult = false; + } + double z; + if (value < 22.0) { + if (value < TWO_POW_N55) { + return negateResult ? -value * (1.0 - value) : value * (1.0 + value); + } else if (value >= 1) { + z = 1.0 - 2.0 / (FastMath.expm1(value + value) + 2.0); + } else { + double t = FastMath.expm1(-(value + value)); + z = -t / (t + 2.0); + } + } else { + z = (value != value) ? Double.NaN : 1.0; + } + return negateResult ? -z : z; + } + + /** + * @param value A double value. + * @return e^value. + */ + public static double exp(double value) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.exp(value) : Math.exp(value); + } + // exp(x) = exp([x])*exp(y) + // with [x] the integer part of x, and y = x-[x] + // ===> + // We find an approximation of y, called z. + // ===> + // exp(x) = exp([x])*(exp(z)*exp(epsilon)) + // ===> + // We have exp([x]) and exp(z) pre-computed in tables, we "just" have to compute exp(epsilon). + // + // We use the same indexing (cast to int) to compute x integer part and the + // table index corresponding to z, to avoid two int casts. + // Also, to optimize index multiplication and division, we use powers of two, + // so that we can do it with bits shifts. + if (value >= 0.0) { + if (value > EXP_OVERFLOW_LIMIT) { + return Double.POSITIVE_INFINITY; + } + int i = (int) (value * EXP_LO_INDEXING); + int valueInt = (i >> EXP_LO_INDEXING_DIV_SHIFT); + i -= (valueInt << EXP_LO_INDEXING_DIV_SHIFT); + double delta = (value - valueInt) - i * (1.0 / EXP_LO_INDEXING); + return expHiTab[valueInt] * (expLoPosTab[i + EXP_LO_TAB_MID_INDEX] * (1 + delta * (1 + delta * (1.0 / 2 + delta * (1.0 / 6 + delta * (1.0 / 24)))))); + } else { // value < 0.0, or value is NaN + if (!(value >= EXP_UNDERFLOW_LIMIT)) { // value < EXP_UNDERFLOW_LIMIT, or value is NaN + return (value < EXP_UNDERFLOW_LIMIT) ? 0.0 : Double.NaN; + } + // TODO JVM bug with -server option: test with values of all magnitudes + // is very slow, if using (int)x instead of -(int)-x or (int)(long)x (which give the same result). + // The guessed cause is that when the same expression is used to define "i" in + // both sides of the above "else", some (desastrous) optimization is done which factorizes + // it above the first "if" statement, making it computed all the time, without the protecting "sub-ifs". + // Since cast from double to int with huge values is extremely slow, + // this makes this whole treatment extremely slow for huge values. + // The solution is therefore to modify a bit the expression for the "optimization" not to occur. + int i = -(int) -(value * EXP_LO_INDEXING); + int valueInt = -((-i) >> EXP_LO_INDEXING_DIV_SHIFT); + i -= ((valueInt) << EXP_LO_INDEXING_DIV_SHIFT); + double delta = (value - valueInt) - i * (1.0 / EXP_LO_INDEXING); + double tmp = expHiInvTab[-valueInt] * (expLoPosTab[i + EXP_LO_TAB_MID_INDEX] * (1 + delta * (1 + delta * (1.0 / 2 + delta * (1.0 / 6 + delta * (1.0 / 24)))))); + // We took care not to compute with subnormal values. + return (valueInt >= EXP_MIN_INT_LIMIT) ? tmp : tmp * TWO_POW_N54; + } + } + + /** + * Quick exp, with a max relative error of about 3e-2 for |value| < 700.0 or so, + * and no accuracy at all outside this range. + * Derived from a note by Nicol N. Schraudolph, IDSIA, 1998. + * + * @param value A double value. + * @return e^value. + */ + public static double expQuick(double value) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.exp(value) : Math.exp(value); + } + if (false) { + // Schraudolph's original method. + return Double.longBitsToDouble((long) (EXP_QUICK_A * value + (EXP_QUICK_B - EXP_QUICK_C))); + } + /* + * Cast of double values, even in long range, into long, is slower than + * from double to int for values in int range, and then from int to long. + * For that reason, we only work with integer values in int range (corresponding to the 32 first bits of the long, + * containing sign, exponent, and highest significant bits of double's mantissa), and cast twice. + */ + return Double.longBitsToDouble(((long) (int) (EXP_QUICK_A / (1L << 32) * value + (EXP_QUICK_B - EXP_QUICK_C) / (1L << 32))) << 32); + } + + /** + * Much more accurate than exp(value)-1, for values close to zero. + * + * @param value A double value. + * @return e^value-1. + */ + public static double expm1(double value) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.expm1(value) : Math.expm1(value); + } + // If value is far from zero, we use exp(value)-1. + // + // If value is close to zero, we use the following formula: + // exp(value)-1 + // = exp(valueApprox)*exp(epsilon)-1 + // = exp(valueApprox)*(exp(epsilon)-exp(-valueApprox)) + // = exp(valueApprox)*(1+epsilon+epsilon^2/2!+...-exp(-valueApprox)) + // = exp(valueApprox)*((1-exp(-valueApprox))+epsilon+epsilon^2/2!+...) + // exp(valueApprox) and exp(-valueApprox) being stored in tables. + + if (Math.abs(value) < EXP_LO_DISTANCE_TO_ZERO) { + // Taking int part instead of rounding, which takes too long. + int i = (int) (value * EXP_LO_INDEXING); + double delta = value - i * (1.0 / EXP_LO_INDEXING); + return expLoPosTab[i + EXP_LO_TAB_MID_INDEX] * (expLoNegTab[i + EXP_LO_TAB_MID_INDEX] + delta * (1 + delta * (1.0 / 2 + delta * (1.0 / 6 + delta * (1.0 / 24 + delta * (1.0 / 120)))))); + } else { + return FastMath.exp(value) - 1; + } + } + + /** + * @param value A double value. + * @return Value logarithm (base e). + */ + public static double log(double value) { + if (USE_JDK_MATH || (!USE_REDEFINED_LOG)) { + return STRICT_MATH ? StrictMath.log(value) : Math.log(value); + } else { + if (value > 0.0) { + if (value == Double.POSITIVE_INFINITY) { + return Double.POSITIVE_INFINITY; + } + + // For normal values not close to 1.0, we use the following formula: + // log(value) + // = log(2^exponent*1.mantissa) + // = log(2^exponent) + log(1.mantissa) + // = exponent * log(2) + log(1.mantissa) + // = exponent * log(2) + log(1.mantissaApprox) + log(1.mantissa/1.mantissaApprox) + // = exponent * log(2) + log(1.mantissaApprox) + log(1+epsilon) + // = exponent * log(2) + log(1.mantissaApprox) + epsilon-epsilon^2/2+epsilon^3/3-epsilon^4/4+... + // with: + // 1.mantissaApprox <= 1.mantissa, + // log(1.mantissaApprox) in table, + // epsilon = (1.mantissa/1.mantissaApprox)-1 + // + // To avoid bad relative error for small results, + // values close to 1.0 are treated aside, with the formula: + // log(x) = z*(2+z^2*((2.0/3)+z^2*((2.0/5))+z^2*((2.0/7))+...))) + // with z=(x-1)/(x+1) + + double h; + if (value > 0.95) { + if (value < 1.14) { + double z = (value - 1.0) / (value + 1.0); + double z2 = z * z; + return z * (2 + z2 * ((2.0 / 3) + z2 * ((2.0 / 5) + z2 * ((2.0 / 7) + z2 * ((2.0 / 9) + z2 * ((2.0 / 11))))))); + } + h = 0.0; + } else if (value < MIN_DOUBLE_NORMAL) { + // Ensuring value is normal. + value *= TWO_POW_52; + // log(x*2^52) + // = log(x)-ln(2^52) + // = log(x)-52*ln(2) + h = -52 * LOG_2; + } else { + h = 0.0; + } + + int valueBitsHi = (int) (Double.doubleToRawLongBits(value) >> 32); + int valueExp = (valueBitsHi >> 20) - MAX_DOUBLE_EXPONENT; + // Getting the first LOG_BITS bits of the mantissa. + int xIndex = ((valueBitsHi << 12) >>> (32 - LOG_BITS)); + + // 1.mantissa/1.mantissaApprox - 1 + double z = (value * twoPowTab[-valueExp - MIN_DOUBLE_EXPONENT]) * logXInvTab[xIndex] - 1; + + z *= (1 - z * ((1.0 / 2) - z * ((1.0 / 3)))); + + return h + valueExp * LOG_2 + (logXLogTab[xIndex] + z); + + } else if (value == 0.0) { + return Double.NEGATIVE_INFINITY; + } else { // value < 0.0, or value is NaN + return Double.NaN; + } + } + } + + /** + * Quick log, with a max relative error of about 2.8e-4 + * for values in ]0,+infinity[, and no accuracy at all + * outside this range. + */ + public static double logQuick(double value) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.log(value) : Math.log(value); + } + /* + * Inverse of Schraudolph's method for exp, is very inaccurate near 1, + * and not that fast (even using floats), especially with added if's + * to deal with values near 1, so we don't use it, and use a simplified + * version of our log's redefined algorithm. + */ + + // Simplified version of log's redefined algorithm: + // log(value) ~= exponent * log(2) + log(1.mantissaApprox) + + double h; + if (value > 0.87) { + if (value < 1.16) { + return 2.0 * (value - 1.0) / (value + 1.0); + } + h = 0.0; + } else if (value < MIN_DOUBLE_NORMAL) { + value *= TWO_POW_52; + h = -52 * LOG_2; + } else { + h = 0.0; + } + + int valueBitsHi = (int) (Double.doubleToRawLongBits(value) >> 32); + int valueExp = (valueBitsHi >> 20) - MAX_DOUBLE_EXPONENT; + int xIndex = ((valueBitsHi << 12) >>> (32 - LOG_BITS)); + + return h + valueExp * LOG_2 + logXLogTab[xIndex]; + } + + /** + * @param value A double value. + * @return Value logarithm (base 10). + */ + public static double log10(double value) { + if (USE_JDK_MATH || (!USE_REDEFINED_LOG)) { + return STRICT_MATH ? StrictMath.log10(value) : Math.log10(value); + } else { + // INV_LOG_10 is < 1, but there is no risk of log(double) + // overflow (positive or negative) while the end result shouldn't, + // since log(Double.MIN_VALUE) and log(Double.MAX_VALUE) have + // magnitudes of just a few hundreds. + return log(value) * INV_LOG_10; + } + } + + /** + * Much more accurate than log(1+value), for values close to zero. + * + * @param value A double value. + * @return Logarithm (base e) of (1+value). + */ + public static double log1p(double value) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.log1p(value) : Math.log1p(value); + } + if (false) { + // This also works. Simpler but a bit slower. + if (value == Double.POSITIVE_INFINITY) { + return Double.POSITIVE_INFINITY; + } + double valuePlusOne = 1 + value; + if (valuePlusOne == 1.0) { + return value; + } else { + return FastMath.log(valuePlusOne) * (value / (valuePlusOne - 1.0)); + } + } + if (value > -1.0) { + if (value == Double.POSITIVE_INFINITY) { + return Double.POSITIVE_INFINITY; + } + + // ln'(x) = 1/x + // so + // log(x+epsilon) ~= log(x) + epsilon/x + // + // Let u be 1+value rounded: + // 1+value = u+epsilon + // + // log(1+value) + // = log(u+epsilon) + // ~= log(u) + epsilon/value + // We compute log(u) as done in log(double), and then add the corrective term. + + double valuePlusOne = 1.0 + value; + if (valuePlusOne == 1.0) { + return value; + } else if (Math.abs(value) < 0.15) { + double z = value / (value + 2.0); + double z2 = z * z; + return z * (2 + z2 * ((2.0 / 3) + z2 * ((2.0 / 5) + z2 * ((2.0 / 7) + z2 * ((2.0 / 9) + z2 * ((2.0 / 11))))))); + } + + int valuePlusOneBitsHi = (int) (Double.doubleToRawLongBits(valuePlusOne) >> 32) & 0x7FFFFFFF; + int valuePlusOneExp = (valuePlusOneBitsHi >> 20) - MAX_DOUBLE_EXPONENT; + // Getting the first LOG_BITS bits of the mantissa. + int xIndex = ((valuePlusOneBitsHi << 12) >>> (32 - LOG_BITS)); + + // 1.mantissa/1.mantissaApprox - 1 + double z = (valuePlusOne * twoPowTab[-valuePlusOneExp - MIN_DOUBLE_EXPONENT]) * logXInvTab[xIndex] - 1; + + z *= (1 - z * ((1.0 / 2) - z * (1.0 / 3))); + + // Adding epsilon/valuePlusOne to z, + // with + // epsilon = value - (valuePlusOne-1) + // (valuePlusOne + epsilon ~= 1+value (not rounded)) + + return valuePlusOneExp * LOG_2 + logXLogTab[xIndex] + (z + (value - (valuePlusOne - 1)) / valuePlusOne); + } else if (value == -1.0) { + return Double.NEGATIVE_INFINITY; + } else { // value < -1.0, or value is NaN + return Double.NaN; + } + } + + /** + * @param value An integer value in [1,Integer.MAX_VALUE]. + * @return The integer part of the logarithm, in base 2, of the specified value, + * i.e. a result in [0,30] + * @throws IllegalArgumentException if the specified value is <= 0. + */ + public static int log2(int value) { + return NumbersUtils.log2(value); + } + + /** + * @param value An integer value in [1,Long.MAX_VALUE]. + * @return The integer part of the logarithm, in base 2, of the specified value, + * i.e. a result in [0,62] + * @throws IllegalArgumentException if the specified value is <= 0. + */ + public static int log2(long value) { + return NumbersUtils.log2(value); + } + + /** + * 1e-13ish accuracy (or better) on whole double range. + * + * @param value A double value. + * @param power A power. + * @return value^power. + */ + public static double pow(double value, double power) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.pow(value, power) : Math.pow(value, power); + } + if (power == 0.0) { + return 1.0; + } else if (power == 1.0) { + return value; + } + if (value <= 0.0) { + // powerInfo: 0 if not integer, 1 if even integer, -1 if odd integer + int powerInfo; + if (Math.abs(power) >= (TWO_POW_52 * 2)) { + // The binary digit just before comma is outside mantissa, + // thus it is always 0: power is an even integer. + powerInfo = 1; + } else { + // If power's magnitude permits, we cast into int instead of into long, + // as it is faster. + if (Math.abs(power) <= (double) Integer.MAX_VALUE) { + int powerAsInt = (int) power; + if (power == (double) powerAsInt) { + powerInfo = ((powerAsInt & 1) == 0) ? 1 : -1; + } else { // power is not an integer (and not NaN, due to test against Integer.MAX_VALUE) + powerInfo = 0; + } + } else { + long powerAsLong = (long) power; + if (power == (double) powerAsLong) { + powerInfo = ((powerAsLong & 1) == 0) ? 1 : -1; + } else { // power is not an integer, or is NaN + if (power != power) { + return Double.NaN; + } + powerInfo = 0; + } + } + } + + if (value == 0.0) { + if (power < 0.0) { + return (powerInfo < 0) ? 1 / value : Double.POSITIVE_INFINITY; + } else { // power > 0.0 (0 and NaN cases already treated) + return (powerInfo < 0) ? value : 0.0; + } + } else { // value < 0.0 + if (value == Double.NEGATIVE_INFINITY) { + if (powerInfo < 0) { // power odd integer + return (power < 0.0) ? -0.0 : Double.NEGATIVE_INFINITY; + } else { // power even integer, or not an integer + return (power < 0.0) ? 0.0 : Double.POSITIVE_INFINITY; + } + } else { + return (powerInfo != 0) ? powerInfo * FastMath.exp(power * FastMath.log(-value)) : Double.NaN; + } + } + } else { // value > 0.0, or value is NaN + return FastMath.exp(power * FastMath.log(value)); + } + } + + /** + * Quick pow, with a max relative error of about 3.5e-2 + * for |a^b| < 1e10, of about 0.17 for |a^b| < 1e50, + * and worse accuracy above. + * + * @param value A double value, in ]0,+infinity[ (strictly positive and finite). + * @param power A double value. + * @return value^power. + */ + public static double powQuick(double value, double power) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.pow(value, power) : Math.pow(value, power); + } + return FastMath.exp(power * FastMath.logQuick(value)); + } + + /** + * This treatment is somehow accurate for low values of |power|, + * and for |power*getExponent(value)| < 1023 or so (to stay away + * from double extreme magnitudes (large and small)). + * + * @param value A double value. + * @param power A power. + * @return value^power. + */ + public static double powFast(double value, int power) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.pow(value, power) : Math.pow(value, power); + } + if (power > 5) { // Most common case first. + double oddRemains = 1.0; + do { + // Test if power is odd. + if ((power & 1) != 0) { + oddRemains *= value; + } + value *= value; + power >>= 1; // power = power / 2 + } while (power > 5); + // Here, power is in [3,5]: faster to finish outside the loop. + if (power == 3) { + return oddRemains * value * value * value; + } else { + double v2 = value * value; + if (power == 4) { + return oddRemains * v2 * v2; + } else { // power == 5 + return oddRemains * v2 * v2 * value; + } + } + } else if (power >= 0) { // power in [0,5] + if (power < 3) { // power in [0,2] + if (power == 2) { // Most common case first. + return value * value; + } else if (power != 0) { // faster than == 1 + return value; + } else { // power == 0 + return 1.0; + } + } else { // power in [3,5] + if (power == 3) { + return value * value * value; + } else { // power in [4,5] + double v2 = value * value; + if (power == 4) { + return v2 * v2; + } else { // power == 5 + return v2 * v2 * value; + } + } + } + } else { // power < 0 + // Opposite of Integer.MIN_VALUE does not exist as int. + if (power == Integer.MIN_VALUE) { + // Integer.MAX_VALUE = -(power+1) + return 1.0 / (FastMath.powFast(value, Integer.MAX_VALUE) * value); + } else { + return 1.0 / FastMath.powFast(value, -power); + } + } + } + + /** + * Returns the exact result, provided it's in double range. + * + * @param power A power. + * @return 2^power. + */ + public static double twoPow(int power) { + /* + * Using table, to go faster than NumbersUtils.twoPow(int). + */ + if (power >= 0) { + if (power <= MAX_DOUBLE_EXPONENT) { + return twoPowTab[power - MIN_DOUBLE_EXPONENT]; + } else { + // Overflow. + return Double.POSITIVE_INFINITY; + } + } else { + if (power >= MIN_DOUBLE_EXPONENT) { + return twoPowTab[power - MIN_DOUBLE_EXPONENT]; + } else { + // Underflow. + return 0.0; + } + } + } + + /** + * @param value An int value. + * @return value*value. + */ + public static int pow2(int value) { + return NumbersUtils.pow2(value); + } + + /** + * @param value A long value. + * @return value*value. + */ + public static long pow2(long value) { + return NumbersUtils.pow2(value); + } + + /** + * @param value A float value. + * @return value*value. + */ + public static float pow2(float value) { + return NumbersUtils.pow2(value); + } + + /** + * @param value A double value. + * @return value*value. + */ + public static double pow2(double value) { + return NumbersUtils.pow2(value); + } + + /** + * @param value An int value. + * @return value*value*value. + */ + public static int pow3(int value) { + return NumbersUtils.pow3(value); + } + + /** + * @param value A long value. + * @return value*value*value. + */ + public static long pow3(long value) { + return NumbersUtils.pow3(value); + } + + /** + * @param value A float value. + * @return value*value*value. + */ + public static float pow3(float value) { + return NumbersUtils.pow3(value); + } + + /** + * @param value A double value. + * @return value*value*value. + */ + public static double pow3(double value) { + return NumbersUtils.pow3(value); + } + + /** + * @param value A double value. + * @return Value square root. + */ + public static double sqrt(double value) { + if (USE_JDK_MATH || (!USE_REDEFINED_SQRT)) { + return STRICT_MATH ? StrictMath.sqrt(value) : Math.sqrt(value); + } else { + // See cbrt for comments, sqrt uses the same ideas. + + if (!(value > 0.0)) { // value <= 0.0, or value is NaN + return (value == 0.0) ? value : Double.NaN; + } else if (value == Double.POSITIVE_INFINITY) { + return Double.POSITIVE_INFINITY; + } + + double h; + if (value < MIN_DOUBLE_NORMAL) { + value *= TWO_POW_52; + h = 2 * TWO_POW_N26; + } else { + h = 2.0; + } + + int valueBitsHi = (int) (Double.doubleToRawLongBits(value) >> 32); + int valueExponentIndex = (valueBitsHi >> 20) + (-MAX_DOUBLE_EXPONENT - MIN_DOUBLE_EXPONENT); + int xIndex = ((valueBitsHi << 12) >>> (32 - SQRT_LO_BITS)); + + double result = sqrtXSqrtHiTab[valueExponentIndex] * sqrtXSqrtLoTab[xIndex]; + double slope = sqrtSlopeHiTab[valueExponentIndex] * sqrtSlopeLoTab[xIndex]; + value *= 0.25; + + result += (value - result * result) * slope; + result += (value - result * result) * slope; + return h * (result + (value - result * result) * slope); + } + } + + /** + * @param value A double value. + * @return Value cubic root. + */ + public static double cbrt(double value) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.cbrt(value) : Math.cbrt(value); + } + double h; + if (value < 0.0) { + if (value == Double.NEGATIVE_INFINITY) { + return Double.NEGATIVE_INFINITY; + } + value = -value; + // Making sure value is normal. + if (value < MIN_DOUBLE_NORMAL) { + value *= (TWO_POW_52 * TWO_POW_26); + // h = * / + h = -2 * TWO_POW_N26; + } else { + h = -2.0; + } + } else { + if (!(value < Double.POSITIVE_INFINITY)) { // value is +infinity, or value is NaN + return value; + } + // Making sure value is normal. + if (value < MIN_DOUBLE_NORMAL) { + if (value == 0.0) { + // cbrt(0.0) = 0.0, cbrt(-0.0) = -0.0 + return value; + } + value *= (TWO_POW_52 * TWO_POW_26); + h = 2 * TWO_POW_N26; + } else { + h = 2.0; + } + } + + // Normal value is (2^ * ). + // First member cubic root is computed, and multiplied with an approximation + // of the cubic root of the second member, to end up with a good guess of + // the result before using Newton's (or Archimedes's) method. + // To compute the cubic root approximation, we use the formula "cbrt(value) = cbrt(x) * cbrt(value/x)", + // choosing x as close to value as possible but inferior to it, so that cbrt(value/x) is close to 1 + // (we could iterate on this method, using value/x as new value for each iteration, + // but finishing with Newton's method is faster). + + // Shift and cast into an int, which overall is faster than working with a long. + int valueBitsHi = (int) (Double.doubleToRawLongBits(value) >> 32); + int valueExponentIndex = (valueBitsHi >> 20) + (-MAX_DOUBLE_EXPONENT - MIN_DOUBLE_EXPONENT); + // Getting the first CBRT_LO_BITS bits of the mantissa. + int xIndex = ((valueBitsHi << 12) >>> (32 - CBRT_LO_BITS)); + double result = cbrtXCbrtHiTab[valueExponentIndex] * cbrtXCbrtLoTab[xIndex]; + double slope = cbrtSlopeHiTab[valueExponentIndex] * cbrtSlopeLoTab[xIndex]; + + // Lowering values to avoid overflows when using Newton's method + // (we will then just have to return twice the result). + // result^3 = value + // (result/2)^3 = value/8 + value *= 0.125; + // No need to divide result here, as division is factorized in result computation tables. + // result *= 0.5; + + // Newton's method, looking for y = x^(1/p): + // y(n) = y(n-1) + (x-y(n-1)^p) * slope(y(n-1)) + // y(n) = y(n-1) + (x-y(n-1)^p) * (1/p)*(x(n-1)^(1/p-1)) + // y(n) = y(n-1) + (x-y(n-1)^p) * (1/p)*(x(n-1)^((1-p)/p)) + // with x(n-1)=y(n-1)^p, i.e.: + // y(n) = y(n-1) + (x-y(n-1)^p) * (1/p)*(y(n-1)^(1-p)) + // + // For p=3: + // y(n) = y(n-1) + (x-y(n-1)^3) * (1/(3*y(n-1)^2)) + + // To save time, we don't recompute the slope between Newton's method steps, + // as initial slope is good enough for a few iterations. + // + // NB: slope = 1/(3*trueResult*trueResult) + // As we have result = trueResult/2 (to avoid overflows), we have: + // slope = 4/(3*result*result) + // = (4/3)*resultInv*resultInv + // with newResultInv = 1/newResult + // = 1/(oldResult+resultDelta) + // = (oldResultInv)*1/(1+resultDelta/oldResult) + // = (oldResultInv)*1/(1+resultDelta*oldResultInv) + // ~= (oldResultInv)*(1-resultDelta*oldResultInv) + // ===> Successive slopes could be computed without division, if needed, + // by computing resultInv (instead of slope right away) and retrieving + // slopes from it. + + result += (value - result * result * result) * slope; + result += (value - result * result * result) * slope; + return h * (result + (value - result * result * result) * slope); + } + + /** + * Returns dividend - divisor * n, where n is the mathematical integer + * closest to dividend/divisor. + * If dividend/divisor is equally close to surrounding integers, + * we choose n to be the integer of smallest magnitude, which makes + * this treatment differ from Math.IEEEremainder(double,double), + * where n is chosen to be the even integer. + * Note that the choice of n is not done considering the double + * approximation of dividend/divisor, because it could cause + * result to be outside [-|divisor|/2,|divisor|/2] range. + * The practical effect is that if multiple results would be possible, + * we always choose the result that is the closest to (and has the same + * sign as) the dividend. + * Ex. : + * - for (-3.0,2.0), this method returns -1.0, + * whereas Math.IEEEremainder returns 1.0. + * - for (-5.0,2.0), both this method and Math.IEEEremainder return -1.0. + *

+ * If the remainder is zero, its sign is the same as the sign of the first argument. + * If either argument is NaN, or the first argument is infinite, + * or the second argument is positive zero or negative zero, + * then the result is NaN. + * If the first argument is finite and the second argument is + * infinite, then the result is the same as the first argument. + *

+ * NB: + * - Modulo operator (%) returns a value in ]-|divisor|,|divisor|[, + * which sign is the same as dividend. + * - As for modulo operator, the sign of the divisor has no effect on the result. + * + * @param dividend Dividend. + * @param divisor Divisor. + * @return Remainder of dividend/divisor, i.e. a value in [-|divisor|/2,|divisor|/2]. + */ + public static double remainder(double dividend, double divisor) { + if (USE_JDK_MATH) { + // no Math equivalent (differs from IEEEremainder(double,double)) + } + if (Double.isInfinite(divisor)) { + if (Double.isInfinite(dividend)) { + return Double.NaN; + } else { + return dividend; + } + } + double value = dividend % divisor; + if (Math.abs(value + value) > Math.abs(divisor)) { + return value + ((value > 0.0) ? -Math.abs(divisor) : Math.abs(divisor)); + } else { + return value; + } + } + + /** + * @param angle Angle in radians. + * @return The same angle, in radians, but in [-Math.PI,Math.PI]. + */ + public static double normalizeMinusPiPi(double angle) { + // Not modifying values in output range. + if ((angle >= -Math.PI) && (angle <= Math.PI)) { + return angle; + } + double angleMinusPiPiOrSo = remainderTwoPi(angle); + if (angleMinusPiPiOrSo < -Math.PI) { + return -Math.PI; + } else if (angleMinusPiPiOrSo > Math.PI) { + return Math.PI; + } else { + return angleMinusPiPiOrSo; + } + } + + /** + * Not accurate for large values. + * + * @param angle Angle in radians. + * @return The same angle, in radians, but in [-Math.PI,Math.PI]. + */ + public static double normalizeMinusPiPiFast(double angle) { + // Not modifying values in output range. + if ((angle >= -Math.PI) && (angle <= Math.PI)) { + return angle; + } + double angleMinusPiPiOrSo = remainderTwoPiFast(angle); + if (angleMinusPiPiOrSo < -Math.PI) { + return -Math.PI; + } else if (angleMinusPiPiOrSo > Math.PI) { + return Math.PI; + } else { + return angleMinusPiPiOrSo; + } + } + + /** + * @param angle Angle in radians. + * @return The same angle, in radians, but in [0,2*Math.PI]. + */ + public static double normalizeZeroTwoPi(double angle) { + // Not modifying values in output range. + if ((angle >= 0.0) && (angle <= 2 * Math.PI)) { + return angle; + } + double angleMinusPiPiOrSo = remainderTwoPi(angle); + if (angleMinusPiPiOrSo < 0.0) { + // Not a problem if angle is slightly < -Math.PI, + // since result ends up around PI, which is not near output range borders. + return angleMinusPiPiOrSo + 2 * Math.PI; + } else { + // Not a problem if angle is slightly > Math.PI, + // since result ends up around PI, which is not near output range borders. + return angleMinusPiPiOrSo; + } + } + + /** + * Not accurate for large values. + * + * @param angle Angle in radians. + * @return The same angle, in radians, but in [0,2*Math.PI]. + */ + public static double normalizeZeroTwoPiFast(double angle) { + // Not modifying values in output range. + if ((angle >= 0.0) && (angle <= 2 * Math.PI)) { + return angle; + } + double angleMinusPiPiOrSo = remainderTwoPiFast(angle); + if (angleMinusPiPiOrSo < 0.0) { + // Not a problem if angle is slightly < -Math.PI, + // since result ends up around PI, which is not near output range borders. + return angleMinusPiPiOrSo + 2 * Math.PI; + } else { + // Not a problem if angle is slightly > Math.PI, + // since result ends up around PI, which is not near output range borders. + return angleMinusPiPiOrSo; + } + } + + /** + * @param angle Angle in radians. + * @return Angle value modulo PI, in radians, in [-Math.PI/2,Math.PI/2]. + */ + public static double normalizeMinusHalfPiHalfPi(double angle) { + // Not modifying values in output range. + if ((angle >= -Math.PI / 2) && (angle <= Math.PI / 2)) { + return angle; + } + double angleMinusPiPiOrSo = remainderTwoPi(angle); + if (angleMinusPiPiOrSo < -Math.PI / 2) { + // Not a problem if angle is slightly < -Math.PI, + // since result ends up around zero, which is not near output range borders. + return angleMinusPiPiOrSo + Math.PI; + } else if (angleMinusPiPiOrSo > Math.PI / 2) { + // Not a problem if angle is slightly > Math.PI, + // since result ends up around zero, which is not near output range borders. + return angleMinusPiPiOrSo - Math.PI; + } else { + return angleMinusPiPiOrSo; + } + } + + /** + * Not accurate for large values. + * + * @param angle Angle in radians. + * @return Angle value modulo PI, in radians, in [-Math.PI/2,Math.PI/2]. + */ + public static double normalizeMinusHalfPiHalfPiFast(double angle) { + // Not modifying values in output range. + if ((angle >= -Math.PI / 2) && (angle <= Math.PI / 2)) { + return angle; + } + double angleMinusPiPiOrSo = remainderTwoPiFast(angle); + if (angleMinusPiPiOrSo < -Math.PI / 2) { + // Not a problem if angle is slightly < -Math.PI, + // since result ends up around zero, which is not near output range borders. + return angleMinusPiPiOrSo + Math.PI; + } else if (angleMinusPiPiOrSo > Math.PI / 2) { + // Not a problem if angle is slightly > Math.PI, + // since result ends up around zero, which is not near output range borders. + return angleMinusPiPiOrSo - Math.PI; + } else { + return angleMinusPiPiOrSo; + } + } + + /** + * Returns sqrt(x^2+y^2) without intermediate overflow or underflow. + */ + public static double hypot(double x, double y) { + if (USE_JDK_MATH) { + return STRICT_MATH ? StrictMath.hypot(x, y) : Math.hypot(x, y); + } + x = Math.abs(x); + y = Math.abs(y); + if (y < x) { + double a = x; + x = y; + y = a; + } else if (!(y >= x)) { // Testing if we have some NaN. + if ((x == Double.POSITIVE_INFINITY) || (y == Double.POSITIVE_INFINITY)) { + return Double.POSITIVE_INFINITY; + } else { + return Double.NaN; + } + } + if (y - x == y) { // x too small to substract from y + return y; + } else { + double factor; + if (x > TWO_POW_450) { // 2^450 < x < y + x *= TWO_POW_N750; + y *= TWO_POW_N750; + factor = TWO_POW_750; + } else if (y < TWO_POW_N450) { // x < y < 2^-450 + x *= TWO_POW_750; + y *= TWO_POW_750; + factor = TWO_POW_N750; + } else { + factor = 1.0; + } + return factor * FastMath.sqrt(x * x + y * y); + } + } + + /** + * @param value A float value. + * @return Ceiling of value. + */ + public static float ceil(float value) { + if (USE_JDK_MATH) { + // TODO use Math.ceil(float) if exists + return (float) Math.ceil(value); + } + return -FastMath.floor(-value); + } + + /** + * Supposed to behave like Math.ceil(double), for safe interchangeability. + * + * @param value A double value. + * @return Ceiling of value. + */ + public static double ceil(double value) { + if (USE_JDK_MATH) { + return Math.ceil(value); + } + return -FastMath.floor(-value); + } + + /** + * @param value A float value. + * @return Floor of value. + */ + public static float floor(float value) { + if (USE_JDK_MATH) { + // TODO use Math.floor(float) if exists + return (float) Math.floor(value); + } + int exp = FastMath.getExponent(value); + if (exp < 0) { + if (value < 0.0f) { + return -1.0f; + } else { // value in [0.0f,1.0f[ + return 0.0f * value; // 0.0f, or -0.0f if value is -0.0f + } + } else { + if (exp < 24) { + int valueBits = Float.floatToRawIntBits(value); + int anteCommaDigits = valueBits & (0xFF800000 >> exp); + if ((value < 0.0f) && (anteCommaDigits != valueBits)) { + return Float.intBitsToFloat(anteCommaDigits) - 1.0f; + } else { + return Float.intBitsToFloat(anteCommaDigits); + } + } else { + return value; + } + } + } + + /** + * Supposed to behave like Math.floor(double), for safe interchangeability. + * + * @param value A double value. + * @return Floor of value. + */ + public static double floor(double value) { + if (USE_JDK_MATH) { + return Math.floor(value); + } + // Faster than to work directly on bits. + if (Math.abs(value) <= (double) Integer.MAX_VALUE) { + if (value > 0.0) { + return (int) value; + } else if (value < 0.0) { + double anteCommaDigits = (int) value; + if (value != anteCommaDigits) { + return anteCommaDigits - 1.0; + } else { + return anteCommaDigits; + } + } else { // value is +-0.0 (not NaN due to test against Integer.MAX_VALUE) + return value; + } + } else if (Math.abs(value) < TWO_POW_52) { + // We split the value in two: + // high part, which is a mathematical integer, + // and the rest, for which we can get rid of the + // post comma digits by casting into an int. + double highPart = ((int) (value * TWO_POW_N26)) * TWO_POW_26; + if (value > 0.0) { + return highPart + (double) ((int) (value - highPart)); + } else { + double anteCommaDigits = highPart + (double) ((int) (value - highPart)); + if (value != anteCommaDigits) { + return anteCommaDigits - 1.0; + } else { + return anteCommaDigits; + } + } + } else { // abs(value) >= 2^52, or value is NaN + return value; + } + } + + /** + * Supposed to behave like Math.round(float), for safe interchangeability. + * + * @param value A double value. + * @return Value rounded to nearest int. + */ + public static int round(float value) { + if (USE_JDK_MATH) { + return Math.round(value); + } + // "return (int)FastMath.floor((float)(value+0.5));" would be more accurate for values in [8388609.0f,16777216.0f] + // (i.e. [0x800001,0x1000000]), but would not give same results than Math.round(float). + return (int) FastMath.floor(value + 0.5f); + } + + /** + * Supposed to behave like Math.round(double), for safe interchangeability. + * + * @param value A double value. + * @return Value rounded to nearest long. + */ + public static long round(double value) { + if (USE_JDK_MATH) { + return Math.round(value); + } + // Would be more coherent with rint, to call rint(double) instead of + // floor(double), but that would not give same results than Math.round(double). + double roundedValue = FastMath.floor(value + 0.5); + if (Math.abs(roundedValue) <= (double) Integer.MAX_VALUE) { + // Faster with intermediary cast in int. + return (long) (int) roundedValue; + } else { + return (long) roundedValue; + } + } + + /** + * @param value A float value. + * @return Value unbiased exponent. + */ + public static int getExponent(float value) { + if (USE_JDK_MATH) { + return Math.getExponent(value); + } + return ((Float.floatToRawIntBits(value) >> 23) & 0xFF) - MAX_FLOAT_EXPONENT; + } + + /** + * @param value A double value. + * @return Value unbiased exponent. + */ + public static int getExponent(double value) { + if (USE_JDK_MATH) { + return Math.getExponent(value); + } + return (((int) (Double.doubleToRawLongBits(value) >> 52)) & 0x7FF) - MAX_DOUBLE_EXPONENT; + } + + /** + * Gives same result as Math.toDegrees for some particular values + * like Math.PI/2, Math.PI or 2*Math.PI, but is faster (no division). + * + * @param angrad Angle value in radians. + * @return Angle value in degrees. + */ + public static double toDegrees(double angrad) { + if (USE_JDK_MATH) { + return Math.toDegrees(angrad); + } + return angrad * (180 / Math.PI); + } + + /** + * Gives same result as Math.toRadians for some particular values + * like 90.0, 180.0 or 360.0, but is faster (no division). + * + * @param angdeg Angle value in degrees. + * @return Angle value in radians. + */ + public static double toRadians(double angdeg) { + if (USE_JDK_MATH) { + return Math.toRadians(angdeg); + } + return angdeg * (Math.PI / 180); + } + + /** + * @param sign Sign of the angle: true for positive, false for negative. + * @param degrees Degrees, in [0,180]. + * @param minutes Minutes, in [0,59]. + * @param seconds Seconds, in [0.0,60.0[. + * @return Angle in radians. + */ + public static double toRadians(boolean sign, int degrees, int minutes, double seconds) { + return FastMath.toRadians(FastMath.toDegrees(sign, degrees, minutes, seconds)); + } + + /** + * @param sign Sign of the angle: true for positive, false for negative. + * @param degrees Degrees, in [0,180]. + * @param minutes Minutes, in [0,59]. + * @param seconds Seconds, in [0.0,60.0[. + * @return Angle in degrees. + */ + public static double toDegrees(boolean sign, int degrees, int minutes, double seconds) { + double signFactor = sign ? 1.0 : -1.0; + return signFactor * (degrees + (1.0 / 60) * (minutes + (1.0 / 60) * seconds)); + } + + /** + * @param angrad Angle in radians. + * @param degrees (out) Degrees, in [0,180]. + * @param minutes (out) Minutes, in [0,59]. + * @param seconds (out) Seconds, in [0.0,60.0[. + * @return True if the resulting angle in [-180deg,180deg] is positive, false if it is negative. + */ + public static boolean toDMS(double angrad, IntWrapper degrees, IntWrapper minutes, DoubleWrapper seconds) { + // Computing longitude DMS. + double tmp = FastMath.toDegrees(FastMath.normalizeMinusPiPi(angrad)); + boolean isNeg = (tmp < 0.0); + if (isNeg) { + tmp = -tmp; + } + degrees.value = (int) tmp; + tmp = (tmp - degrees.value) * 60.0; + minutes.value = (int) tmp; + seconds.value = Math.min((tmp - minutes.value) * 60.0, DOUBLE_BEFORE_60); + return !isNeg; + } + + /** + * @param value An int value. + * @return The absolute value, except if value is Integer.MIN_VALUE, for which it returns Integer.MIN_VALUE. + */ + public static int abs(int value) { + if (USE_JDK_MATH) { + return Math.abs(value); + } + return NumbersUtils.abs(value); + } + + /** + * @param value A long value. + * @return The specified value as int. + * @throws ArithmeticException if the specified value is not in [Integer.MIN_VALUE,Integer.MAX_VALUE] range. + */ + public static int toIntExact(long value) { + return NumbersUtils.asInt(value); + } + + /** + * @param value A long value. + * @return The closest int value in [Integer.MIN_VALUE,Integer.MAX_VALUE] range. + */ + public static int toInt(long value) { + return NumbersUtils.toInt(value); + } + + /** + * @param a An int value. + * @param b An int value. + * @return The mathematical result of a+b. + * @throws ArithmeticException if the mathematical result of a+b is not in [Integer.MIN_VALUE,Integer.MAX_VALUE] range. + */ + public static int addExact(int a, int b) { + return NumbersUtils.plusExact(a, b); + } + + /** + * @param a A long value. + * @param b A long value. + * @return The mathematical result of a+b. + * @throws ArithmeticException if the mathematical result of a+b is not in [Long.MIN_VALUE,Long.MAX_VALUE] range. + */ + public static long addExact(long a, long b) { + return NumbersUtils.plusExact(a, b); + } + + /** + * @param a An int value. + * @param b An int value. + * @return The int value of [Integer.MIN_VALUE,Integer.MAX_VALUE] range which is the closest to mathematical result of a+b. + */ + public static int addBounded(int a, int b) { + return NumbersUtils.plusBounded(a, b); + } + + /** + * @param a A long value. + * @param b A long value. + * @return The long value of [Long.MIN_VALUE,Long.MAX_VALUE] range which is the closest to mathematical result of a+b. + */ + public static long addBounded(long a, long b) { + return NumbersUtils.plusBounded(a, b); + } + + /** + * @param a An int value. + * @param b An int value. + * @return The mathematical result of a-b. + * @throws ArithmeticException if the mathematical result of a-b is not in [Integer.MIN_VALUE,Integer.MAX_VALUE] range. + */ + public static int subtractExact(int a, int b) { + return NumbersUtils.minusExact(a, b); + } + + /** + * @param a A long value. + * @param b A long value. + * @return The mathematical result of a-b. + * @throws ArithmeticException if the mathematical result of a-b is not in [Long.MIN_VALUE,Long.MAX_VALUE] range. + */ + public static long subtractExact(long a, long b) { + return NumbersUtils.minusExact(a, b); + } + + /** + * @param a An int value. + * @param b An int value. + * @return The int value of [Integer.MIN_VALUE,Integer.MAX_VALUE] range which is the closest to mathematical result of a-b. + */ + public static int subtractBounded(int a, int b) { + return NumbersUtils.minusBounded(a, b); + } + + /** + * @param a A long value. + * @param b A long value. + * @return The long value of [Long.MIN_VALUE,Long.MAX_VALUE] range which is the closest to mathematical result of a-b. + */ + public static long subtractBounded(long a, long b) { + return NumbersUtils.minusBounded(a, b); + } + + /** + * @param a An int value. + * @param b An int value. + * @return The mathematical result of a*b. + * @throws ArithmeticException if the mathematical result of a*b is not in [Integer.MIN_VALUE,Integer.MAX_VALUE] range. + */ + public static int multiplyExact(int a, int b) { + return NumbersUtils.timesExact(a, b); + } + + /** + * @param a A long value. + * @param b A long value. + * @return The mathematical result of a*b. + * @throws ArithmeticException if the mathematical result of a*b is not in [Long.MIN_VALUE,Long.MAX_VALUE] range. + */ + public static long multiplyExact(long a, long b) { + return NumbersUtils.timesExact(a, b); + } + + /** + * @param a An int value. + * @param b An int value. + * @return The int value of [Integer.MIN_VALUE,Integer.MAX_VALUE] range which is the closest to mathematical result of a*b. + */ + public static int multiplyBounded(int a, int b) { + return NumbersUtils.timesBounded(a, b); + } + + /** + * @param a A long value. + * @param b A long value. + * @return The long value of [Long.MIN_VALUE,Long.MAX_VALUE] range which is the closest to mathematical result of a*b. + */ + public static long multiplyBounded(long a, long b) { + return NumbersUtils.timesBounded(a, b); + } + + /** + * @param minValue An int value. + * @param maxValue An int value. + * @param value An int value. + * @return minValue if value < minValue, maxValue if value > maxValue, value otherwise. + */ + public static int toRange(int minValue, int maxValue, int value) { + return NumbersUtils.toRange(minValue, maxValue, value); + } + + /** + * @param minValue A long value. + * @param maxValue A long value. + * @param value A long value. + * @return minValue if value < minValue, maxValue if value > maxValue, value otherwise. + */ + public static long toRange(long minValue, long maxValue, long value) { + return NumbersUtils.toRange(minValue, maxValue, value); + } + + /** + * @param minValue A float value. + * @param maxValue A float value. + * @param value A float value. + * @return minValue if value < minValue, maxValue if value > maxValue, value otherwise. + */ + public static float toRange(float minValue, float maxValue, float value) { + return NumbersUtils.toRange(minValue, maxValue, value); + } + + /** + * @param minValue A double value. + * @param maxValue A double value. + * @param value A double value. + * @return minValue if value < minValue, maxValue if value > maxValue, value otherwise. + */ + public static double toRange(double minValue, double maxValue, double value) { + return NumbersUtils.toRange(minValue, maxValue, value); + } + + /** + * NB: Since 2*Math.PI < 2*PI, a span of 2*Math.PI does not mean full angular range. + * ex.: isInClockwiseDomain(0.0, 2*Math.PI, -1e-20) returns false. + * For full angular range, use a span > 2*Math.PI, like 2*PI_SUP constant of this class. + * + * @param startAngRad An angle, in radians. + * @param angSpanRad An angular span, >= 0.0, in radians. + * @param angRad An angle, in radians. + * @return True if angRad is in the clockwise angular domain going from startAngRad, over angSpanRad, + * extremities included, false otherwise. + */ + public static boolean isInClockwiseDomain(double startAngRad, double angSpanRad, double angRad) { + if (Math.abs(angRad) < -TWO_MATH_PI_IN_MINUS_PI_PI) { + // special case for angular values of small magnitude + if (angSpanRad < 0.0) { + // empty domain + return false; + } else // angSpanRad is NaN + // angSpanRad > 2*Math.PI + // we know angRad is not NaN, due to a previous test + if (angSpanRad <= 2 * Math.PI) { // angSpanRad is in [0.0,2*Math.PI] + startAngRad = FastMath.normalizeMinusPiPi(startAngRad); + double endAngRad = FastMath.normalizeMinusPiPi(startAngRad + angSpanRad); + // + if (startAngRad <= endAngRad) { + return (angRad >= startAngRad) && (angRad <= endAngRad); + } else { + return (angRad >= startAngRad) || (angRad <= endAngRad); + } + } else return angSpanRad == angSpanRad; + } else { + // general case + return (FastMath.normalizeZeroTwoPi(angRad - startAngRad) <= angSpanRad); + } + } + + public static boolean isNaNOrInfinite(float value) { + return NumbersUtils.isNaNOrInfinite(value); + } + + public static boolean isNaNOrInfinite(double value) { + return NumbersUtils.isNaNOrInfinite(value); + } + + /* + * + * Not-redefined java.lang.Math public values and treatments, for quick replacement of Math with FastMath. + * + */ + + public static final double E = Math.E; + public static final double PI = Math.PI; + + public static double abs(double a) { + return Math.abs(a); + } + + public static float abs(float a) { + return Math.abs(a); + } + + public static long abs(long a) { + return Math.abs(a); + } + + public static double copySign(double magnitude, double sign) { + return Math.copySign(magnitude, sign); + } + + public static float copySign(float magnitude, float sign) { + return Math.copySign(magnitude, sign); + } + + public static double IEEEremainder(double f1, double f2) { + return Math.IEEEremainder(f1, f2); + } + + public static double max(double a, double b) { + return Math.max(a, b); + } + + public static float max(float a, float b) { + return Math.max(a, b); + } + + public static int max(int a, int b) { + return Math.max(a, b); + } + + public static long max(long a, long b) { + return Math.max(a, b); + } + + public static double min(double a, double b) { + return Math.min(a, b); + } + + public static float min(float a, float b) { + return Math.min(a, b); + } + + public static int min(int a, int b) { + return Math.min(a, b); + } + + public static long min(long a, long b) { + return Math.min(a, b); + } + + public static double nextAfter(double start, double direction) { + return Math.nextAfter(start, direction); + } + + public static float nextAfter(float start, float direction) { + return Math.nextAfter(start, direction); + } + + public static double nextUp(double d) { + return Math.nextUp(d); + } + + public static float nextUp(float f) { + return Math.nextUp(f); + } + + public static double random() { + // StrictMath and Math use different RNG instances, + // so their random() methods are not equivalent. + return STRICT_MATH ? StrictMath.random() : Math.random(); + } + + public static double rint(double a) { + return Math.rint(a); + } + + public static double scalb(double d, int scaleFactor) { + return Math.scalb(d, scaleFactor); + } + + public static float scalb(float f, int scaleFactor) { + return Math.scalb(f, scaleFactor); + } + + public static double signum(double d) { + return Math.signum(d); + } + + public static float signum(float f) { + return Math.signum(f); + } + + public static double ulp(double d) { + return Math.ulp(d); + } + + public static float ulp(float f) { + return Math.ulp(f); + } + + //-------------------------------------------------------------------------- + // PRIVATE TREATMENTS + //-------------------------------------------------------------------------- + + /** + * FastMath is non-instantiable. + */ + private FastMath() { + } + + /** + * Use look-up tables size power through this method, + * to make sure is it small in case java.lang.Math + * is directly used. + */ + private static int getTabSizePower(int tabSizePower) { + return USE_JDK_MATH ? Math.min(2, tabSizePower) : tabSizePower; + } + + /** + * Remainder using an accurate definition of PI. + * Derived from a fdlibm treatment called __ieee754_rem_pio2. + *

+ * This method can return values slightly (like one ULP or so) outside [-Math.PI,Math.PI] range. + * + * @param angle Angle in radians. + * @return Remainder of (angle % (2*PI)), which is in [-PI,PI] range. + */ + private static double remainderTwoPi(double angle) { + if (USE_JDK_MATH) { + double y = STRICT_MATH ? StrictMath.sin(angle) : Math.sin(angle); + double x = STRICT_MATH ? StrictMath.cos(angle) : Math.cos(angle); + return STRICT_MATH ? StrictMath.atan2(y, x) : Math.atan2(y, x); + } + boolean negateResult; + if (angle < 0.0) { + negateResult = true; + angle = -angle; + } else { + negateResult = false; + } + if (angle <= NORMALIZE_ANGLE_MAX_MEDIUM_DOUBLE) { + double fn = (int) (angle * INVTWOPI + 0.5); + double result = (angle - fn * TWOPI_HI) - fn * TWOPI_LO; + return negateResult ? -result : result; + } else if (angle < Double.POSITIVE_INFINITY) { + // Reworking exponent to have a value < 2^24. + long lx = Double.doubleToRawLongBits(angle); + long exp = ((lx >> 52) & 0x7FF) - 1046; + double z = Double.longBitsToDouble(lx - (exp << 52)); + + double x0 = (int) z; + z = (z - x0) * TWO_POW_24; + double x1 = (int) z; + double x2 = (z - x1) * TWO_POW_24; + + double result = subRemainderTwoPi(x0, x1, x2, (int) exp, (x2 == 0) ? 2 : 3); + return negateResult ? -result : result; + } else { // angle is +infinity or NaN + return Double.NaN; + } + } + + /** + * Not accurate for large values. + *

+ * This method can return values slightly (like one ULP or so) outside [-Math.PI,Math.PI] range. + * + * @param angle Angle in radians. + * @return Remainder of (angle % (2*PI)), which is in [-PI,PI] range. + */ + private static double remainderTwoPiFast(double angle) { + if (USE_JDK_MATH) { + return remainderTwoPi(angle); + } + boolean negateResult; + if (angle < 0.0) { + negateResult = true; + angle = -angle; + } else { + negateResult = false; + } + // - We don't bother with values higher than (2*PI*(2^52)), + // since they are spaced by 2*PI or more from each other. + // - For large values, we don't use % because it might be very slow, + // and we split computation in two, because cast from double to int + // with large numbers might be very slow also. + if (angle <= TWO_POW_26 * (2 * Math.PI)) { + double fn = (int) (angle * INVTWOPI + 0.5); + double result = (angle - fn * TWOPI_HI) - fn * TWOPI_LO; + return negateResult ? -result : result; + } else if (angle <= TWO_POW_52 * (2 * Math.PI)) { + // 1) Computing remainder of angle modulo TWO_POW_26*(2*PI). + double fn = (int) (angle * (INVTWOPI / TWO_POW_26) + 0.5); + double result = (angle - fn * (TWOPI_HI * TWO_POW_26)) - fn * (TWOPI_LO * TWO_POW_26); + // Here, result is in [-TWO_POW_26*Math.PI,TWO_POW_26*Math.PI]. + if (result < 0.0) { + result = -result; + negateResult = !negateResult; + } + // 2) Computing remainder of angle modulo 2*PI. + fn = (int) (result * INVTWOPI + 0.5); + result = (result - fn * TWOPI_HI) - fn * TWOPI_LO; + return negateResult ? -result : result; + } else if (angle < Double.POSITIVE_INFINITY) { + return 0.0; + } else { // angle is +infinity or NaN + return Double.NaN; + } + } + + /** + * Remainder using an accurate definition of PI. + * Derived from a fdlibm treatment called __kernel_rem_pio2. + * + * @param x0 Most significant part of the value, as an integer < 2^24, in double precision format. Must be >= 0. + * @param x1 Following significant part of the value, as an integer < 2^24, in double precision format. + * @param x2 Least significant part of the value, as an integer < 2^24, in double precision format. + * @param e0 Exponent of x0 (value is (2^e0)*(x0+(2^-24)*(x1+(2^-24)*x2))). Must be >= -20. + * @param nx Number of significant parts to take into account. Must be 2 or 3. + * @return Remainder of (value % (2*PI)), which is in [-PI,PI] range. + */ + private static double subRemainderTwoPi(double x0, double x1, double x2, int e0, int nx) { + int ih; + double z, fw; + double f0, f1, f2, f3, f4, f5, f6 = 0.0, f7; + double q0, q1, q2, q3, q4, q5; + int iq0, iq1, iq2, iq3, iq4; + + final int jx = nx - 1; // jx in [1,2] (nx in [2,3]) + // Could use a table to avoid division, but the gain isn't worth it most likely... + final int jv = (e0 - 3) / 24; // We do not handle the case (e0-3 < -23). + int q = e0 - ((jv << 4) + (jv << 3)) - 24; // e0-24*(jv+1) + + final int j = jv + 4; + if (jx == 1) { + f5 = (j >= 0) ? ONE_OVER_TWOPI_TAB[j] : 0.0; + f4 = (j >= 1) ? ONE_OVER_TWOPI_TAB[j - 1] : 0.0; + f3 = (j >= 2) ? ONE_OVER_TWOPI_TAB[j - 2] : 0.0; + f2 = (j >= 3) ? ONE_OVER_TWOPI_TAB[j - 3] : 0.0; + f1 = (j >= 4) ? ONE_OVER_TWOPI_TAB[j - 4] : 0.0; + f0 = (j >= 5) ? ONE_OVER_TWOPI_TAB[j - 5] : 0.0; + + q0 = x0 * f1 + x1 * f0; + q1 = x0 * f2 + x1 * f1; + q2 = x0 * f3 + x1 * f2; + q3 = x0 * f4 + x1 * f3; + q4 = x0 * f5 + x1 * f4; + } else { // jx == 2 + f6 = (j >= 0) ? ONE_OVER_TWOPI_TAB[j] : 0.0; + f5 = (j >= 1) ? ONE_OVER_TWOPI_TAB[j - 1] : 0.0; + f4 = (j >= 2) ? ONE_OVER_TWOPI_TAB[j - 2] : 0.0; + f3 = (j >= 3) ? ONE_OVER_TWOPI_TAB[j - 3] : 0.0; + f2 = (j >= 4) ? ONE_OVER_TWOPI_TAB[j - 4] : 0.0; + f1 = (j >= 5) ? ONE_OVER_TWOPI_TAB[j - 5] : 0.0; + f0 = (j >= 6) ? ONE_OVER_TWOPI_TAB[j - 6] : 0.0; + + q0 = x0 * f2 + x1 * f1 + x2 * f0; + q1 = x0 * f3 + x1 * f2 + x2 * f1; + q2 = x0 * f4 + x1 * f3 + x2 * f2; + q3 = x0 * f5 + x1 * f4 + x2 * f3; + q4 = x0 * f6 + x1 * f5 + x2 * f4; + } + + z = q4; + fw = (int) (TWO_POW_N24 * z); + iq0 = (int) (z - TWO_POW_24 * fw); + z = q3 + fw; + fw = (int) (TWO_POW_N24 * z); + iq1 = (int) (z - TWO_POW_24 * fw); + z = q2 + fw; + fw = (int) (TWO_POW_N24 * z); + iq2 = (int) (z - TWO_POW_24 * fw); + z = q1 + fw; + fw = (int) (TWO_POW_N24 * z); + iq3 = (int) (z - TWO_POW_24 * fw); + z = q0 + fw; + + // Here, q is in [-25,2] range or so, so we can use the table right away. + double twoPowQ = twoPowTab[q - MIN_DOUBLE_EXPONENT]; + + z = (z * twoPowQ) % 8.0; + z -= (int) z; + if (q > 0) { + iq3 &= 0xFFFFFF >> q; + ih = iq3 >> (23 - q); + } else if (q == 0) { + ih = iq3 >> 23; + } else if (z >= 0.5) { + ih = 2; + } else { + ih = 0; + } + if (ih > 0) { + int carry; + if (iq0 != 0) { + carry = 1; + iq0 = 0x1000000 - iq0; + iq1 = 0x0FFFFFF - iq1; + iq2 = 0x0FFFFFF - iq2; + iq3 = 0x0FFFFFF - iq3; + } else { + if (iq1 != 0) { + carry = 1; + iq1 = 0x1000000 - iq1; + iq2 = 0x0FFFFFF - iq2; + iq3 = 0x0FFFFFF - iq3; + } else { + if (iq2 != 0) { + carry = 1; + iq2 = 0x1000000 - iq2; + iq3 = 0x0FFFFFF - iq3; + } else { + if (iq3 != 0) { + carry = 1; + iq3 = 0x1000000 - iq3; + } else { + carry = 0; + } + } + } + } + if (q > 0) { + switch (q) { + case 1: + iq3 &= 0x7FFFFF; + break; + case 2: + iq3 &= 0x3FFFFF; + break; + } + } + if (ih == 2) { + z = 1.0 - z; + if (carry != 0) { + z -= twoPowQ; + } + } + } + + if (z == 0.0) { + if (jx == 1) { + f6 = ONE_OVER_TWOPI_TAB[jv + 5]; + q5 = x0 * f6 + x1 * f5; + } else { // jx == 2 + f7 = ONE_OVER_TWOPI_TAB[jv + 5]; + q5 = x0 * f7 + x1 * f6 + x2 * f5; + } + + z = q5; + fw = (int) (TWO_POW_N24 * z); + iq0 = (int) (z - TWO_POW_24 * fw); + z = q4 + fw; + fw = (int) (TWO_POW_N24 * z); + iq1 = (int) (z - TWO_POW_24 * fw); + z = q3 + fw; + fw = (int) (TWO_POW_N24 * z); + iq2 = (int) (z - TWO_POW_24 * fw); + z = q2 + fw; + fw = (int) (TWO_POW_N24 * z); + iq3 = (int) (z - TWO_POW_24 * fw); + z = q1 + fw; + fw = (int) (TWO_POW_N24 * z); + iq4 = (int) (z - TWO_POW_24 * fw); + z = q0 + fw; + + z = (z * twoPowQ) % 8.0; + z -= (int) z; + if (q > 0) { + // some parentheses for Eclipse formatter's weaknesses with bits shifts + iq4 &= (0xFFFFFF >> q); + ih = (iq4 >> (23 - q)); + } else if (q == 0) { + ih = iq4 >> 23; + } else if (z >= 0.5) { + ih = 2; + } else { + ih = 0; + } + if (ih > 0) { + if (iq0 != 0) { + iq0 = 0x1000000 - iq0; + iq1 = 0x0FFFFFF - iq1; + iq2 = 0x0FFFFFF - iq2; + iq3 = 0x0FFFFFF - iq3; + iq4 = 0x0FFFFFF - iq4; + } else { + if (iq1 != 0) { + iq1 = 0x1000000 - iq1; + iq2 = 0x0FFFFFF - iq2; + iq3 = 0x0FFFFFF - iq3; + iq4 = 0x0FFFFFF - iq4; + } else { + if (iq2 != 0) { + iq2 = 0x1000000 - iq2; + iq3 = 0x0FFFFFF - iq3; + iq4 = 0x0FFFFFF - iq4; + } else { + if (iq3 != 0) { + iq3 = 0x1000000 - iq3; + iq4 = 0x0FFFFFF - iq4; + } else { + if (iq4 != 0) { + iq4 = 0x1000000 - iq4; + } + } + } + } + } + if (q > 0) { + switch (q) { + case 1: + iq4 &= 0x7FFFFF; + break; + case 2: + iq4 &= 0x3FFFFF; + break; + } + } + } + fw = twoPowQ * TWO_POW_N24; // q -= 24, so initializing fw with ((2^q)*(2^-24)=2^(q-24)) + } else { + // Here, q is in [-25,-2] range or so, so we could use twoPow's table right away with + // iq4 = (int)(z*twoPowTab[-q-TWO_POW_TAB_MIN_POW]); + // but tests show using division is faster... + iq4 = (int) (z / twoPowQ); + fw = twoPowQ; + } + + q4 = fw * (double) iq4; + fw *= TWO_POW_N24; + q3 = fw * (double) iq3; + fw *= TWO_POW_N24; + q2 = fw * (double) iq2; + fw *= TWO_POW_N24; + q1 = fw * (double) iq1; + fw *= TWO_POW_N24; + q0 = fw * (double) iq0; + fw *= TWO_POW_N24; + + fw = TWOPI_TAB0 * q4; + fw += TWOPI_TAB0 * q3 + TWOPI_TAB1 * q4; + fw += TWOPI_TAB0 * q2 + TWOPI_TAB1 * q3 + TWOPI_TAB2 * q4; + fw += TWOPI_TAB0 * q1 + TWOPI_TAB1 * q2 + TWOPI_TAB2 * q3 + TWOPI_TAB3 * q4; + fw += TWOPI_TAB0 * q0 + TWOPI_TAB1 * q1 + TWOPI_TAB2 * q2 + TWOPI_TAB3 * q3 + TWOPI_TAB4 * q4; + + return (ih == 0) ? fw : -fw; + } + + //-------------------------------------------------------------------------- + // STATIC INITIALIZATIONS + //-------------------------------------------------------------------------- + + /** + * Initializes look-up tables. + * + * Might use some FastMath methods in there, not to spend + * an hour in it, but must take care not to use methods + * which look-up tables have not yet been initialized, + * or that are not accurate enough. + */ + static { + + // sin and cos + + final int SIN_COS_PI_INDEX = (SIN_COS_TABS_SIZE - 1) / 2; + final int SIN_COS_PI_MUL_2_INDEX = 2 * SIN_COS_PI_INDEX; + final int SIN_COS_PI_MUL_0_5_INDEX = SIN_COS_PI_INDEX / 2; + final int SIN_COS_PI_MUL_1_5_INDEX = 3 * SIN_COS_PI_INDEX / 2; + for (int i = 0; i < SIN_COS_TABS_SIZE; i++) { + // angle: in [0,2*PI]. + double angle = i * SIN_COS_DELTA_HI + i * SIN_COS_DELTA_LO; + double sinAngle = StrictMath.sin(angle); + double cosAngle = StrictMath.cos(angle); + // For indexes corresponding to null cosine or sine, we make sure the value is zero + // and not an epsilon. This allows for a much better accuracy for results close to zero. + if (i == SIN_COS_PI_INDEX) { + sinAngle = 0.0; + } else if (i == SIN_COS_PI_MUL_2_INDEX) { + sinAngle = 0.0; + } else if (i == SIN_COS_PI_MUL_0_5_INDEX) { + cosAngle = 0.0; + } else if (i == SIN_COS_PI_MUL_1_5_INDEX) { + cosAngle = 0.0; + } + sinTab[i] = sinAngle; + cosTab[i] = cosAngle; + } + + // tan + + for (int i = 0; i < TAN_TABS_SIZE; i++) { + // angle: in [0,TAN_MAX_VALUE_FOR_TABS]. + double angle = i * TAN_DELTA_HI + i * TAN_DELTA_LO; + tanTab[i] = StrictMath.tan(angle); + double cosAngle = StrictMath.cos(angle); + double sinAngle = StrictMath.sin(angle); + double cosAngleInv = 1 / cosAngle; + double cosAngleInv2 = cosAngleInv * cosAngleInv; + double cosAngleInv3 = cosAngleInv2 * cosAngleInv; + double cosAngleInv4 = cosAngleInv2 * cosAngleInv2; + double cosAngleInv5 = cosAngleInv3 * cosAngleInv2; + tanDer1DivF1Tab[i] = cosAngleInv2; + tanDer2DivF2Tab[i] = ((2 * sinAngle) * cosAngleInv3) * ONE_DIV_F2; + tanDer3DivF3Tab[i] = ((2 * (1 + 2 * sinAngle * sinAngle)) * cosAngleInv4) * ONE_DIV_F3; + tanDer4DivF4Tab[i] = ((8 * sinAngle * (2 + sinAngle * sinAngle)) * cosAngleInv5) * ONE_DIV_F4; + } + + // asin + + for (int i = 0; i < ASIN_TABS_SIZE; i++) { + // x: in [0,ASIN_MAX_VALUE_FOR_TABS]. + double x = i * ASIN_DELTA; + asinTab[i] = StrictMath.asin(x); + double oneMinusXSqInv = 1.0 / (1 - x * x); + double oneMinusXSqInv0_5 = StrictMath.sqrt(oneMinusXSqInv); + double oneMinusXSqInv1_5 = oneMinusXSqInv0_5 * oneMinusXSqInv; + double oneMinusXSqInv2_5 = oneMinusXSqInv1_5 * oneMinusXSqInv; + double oneMinusXSqInv3_5 = oneMinusXSqInv2_5 * oneMinusXSqInv; + asinDer1DivF1Tab[i] = oneMinusXSqInv0_5; + asinDer2DivF2Tab[i] = (x * oneMinusXSqInv1_5) * ONE_DIV_F2; + asinDer3DivF3Tab[i] = ((1 + 2 * x * x) * oneMinusXSqInv2_5) * ONE_DIV_F3; + asinDer4DivF4Tab[i] = ((5 + 2 * x * (2 + x * (5 - 2 * x))) * oneMinusXSqInv3_5) * ONE_DIV_F4; + } + + if (USE_POWTABS_FOR_ASIN) { + for (int i = 0; i < ASIN_POWTABS_SIZE; i++) { + // x: in [0,ASIN_MAX_VALUE_FOR_POWTABS]. + double x = StrictMath.pow(i * (1.0 / ASIN_POWTABS_SIZE_MINUS_ONE), 1.0 / ASIN_POWTABS_POWER) * ASIN_MAX_VALUE_FOR_POWTABS; + asinParamPowTab[i] = x; + asinPowTab[i] = StrictMath.asin(x); + double oneMinusXSqInv = 1.0 / (1 - x * x); + double oneMinusXSqInv0_5 = StrictMath.sqrt(oneMinusXSqInv); + double oneMinusXSqInv1_5 = oneMinusXSqInv0_5 * oneMinusXSqInv; + double oneMinusXSqInv2_5 = oneMinusXSqInv1_5 * oneMinusXSqInv; + double oneMinusXSqInv3_5 = oneMinusXSqInv2_5 * oneMinusXSqInv; + asinDer1DivF1PowTab[i] = oneMinusXSqInv0_5; + asinDer2DivF2PowTab[i] = (x * oneMinusXSqInv1_5) * ONE_DIV_F2; + asinDer3DivF3PowTab[i] = ((1 + 2 * x * x) * oneMinusXSqInv2_5) * ONE_DIV_F3; + asinDer4DivF4PowTab[i] = ((5 + 2 * x * (2 + x * (5 - 2 * x))) * oneMinusXSqInv3_5) * ONE_DIV_F4; + } + } + + // atan + + for (int i = 0; i < ATAN_TABS_SIZE; i++) { + // x: in [0,ATAN_MAX_VALUE_FOR_TABS]. + double x = i * ATAN_DELTA; + double onePlusXSqInv = 1.0 / (1 + x * x); + double onePlusXSqInv2 = onePlusXSqInv * onePlusXSqInv; + double onePlusXSqInv3 = onePlusXSqInv2 * onePlusXSqInv; + double onePlusXSqInv4 = onePlusXSqInv2 * onePlusXSqInv2; + atanTab[i] = StrictMath.atan(x); + atanDer1DivF1Tab[i] = onePlusXSqInv; + atanDer2DivF2Tab[i] = (-2 * x * onePlusXSqInv2) * ONE_DIV_F2; + atanDer3DivF3Tab[i] = ((-2 + 6 * x * x) * onePlusXSqInv3) * ONE_DIV_F3; + atanDer4DivF4Tab[i] = ((24 * x * (1 - x * x)) * onePlusXSqInv4) * ONE_DIV_F4; + } + + // exp + + for (int i = 0; i < EXP_LO_TAB_SIZE; i++) { + // x: in [-EXPM1_DISTANCE_TO_ZERO,EXPM1_DISTANCE_TO_ZERO]. + double x = -EXP_LO_DISTANCE_TO_ZERO + i / (double) EXP_LO_INDEXING; + // exp(x) + expLoPosTab[i] = StrictMath.exp(x); + // 1-exp(-x), accurately computed + expLoNegTab[i] = -StrictMath.expm1(-x); + } + for (int i = 0; i <= (int) EXP_OVERFLOW_LIMIT; i++) { + expHiTab[i] = StrictMath.exp(i); + } + for (int i = 0; i <= -(int) EXP_UNDERFLOW_LIMIT; i++) { + // We take care not to compute with subnormal values. + if ((double) -i >= EXP_MIN_INT_LIMIT) { + expHiInvTab[i] = StrictMath.exp(-i); + } else { + expHiInvTab[i] = StrictMath.exp(54 * LOG_2 - i); + } + } + + // log + + for (int i = 0; i < LOG_TAB_SIZE; i++) { + // Exact to use inverse of tab size, since it is a power of two. + double x = 1 + i * (1.0 / LOG_TAB_SIZE); + logXLogTab[i] = StrictMath.log(x); + logXTab[i] = x; + logXInvTab[i] = 1 / x; + } + + // twoPow + + for (int i = MIN_DOUBLE_EXPONENT; i <= MAX_DOUBLE_EXPONENT; i++) { + twoPowTab[i - MIN_DOUBLE_EXPONENT] = StrictMath.pow(2.0, i); + } + + // sqrt + + for (int i = MIN_DOUBLE_EXPONENT; i <= MAX_DOUBLE_EXPONENT; i++) { + double twoPowExpDiv2 = StrictMath.pow(2.0, i * 0.5); + sqrtXSqrtHiTab[i - MIN_DOUBLE_EXPONENT] = twoPowExpDiv2 * 0.5; // Half sqrt, to avoid overflows. + sqrtSlopeHiTab[i - MIN_DOUBLE_EXPONENT] = 1 / twoPowExpDiv2; + } + sqrtXSqrtLoTab[0] = 1.0; + sqrtSlopeLoTab[0] = 1.0; + final long SQRT_LO_MASK = (0x3FF0000000000000L | (0x000FFFFFFFFFFFFFL >> SQRT_LO_BITS)); + for (int i = 1; i < SQRT_LO_TAB_SIZE; i++) { + long xBits = SQRT_LO_MASK | (((long) (i - 1)) << (52 - SQRT_LO_BITS)); + double sqrtX = StrictMath.sqrt(Double.longBitsToDouble(xBits)); + sqrtXSqrtLoTab[i] = sqrtX; + sqrtSlopeLoTab[i] = 1 / sqrtX; + } + + // cbrt + + for (int i = MIN_DOUBLE_EXPONENT; i <= MAX_DOUBLE_EXPONENT; i++) { + double twoPowExpDiv3 = StrictMath.pow(2.0, i / 3.0); + cbrtXCbrtHiTab[i - MIN_DOUBLE_EXPONENT] = twoPowExpDiv3 * 0.5; // Half cbrt, to avoid overflows. + double tmp = 1 / twoPowExpDiv3; + cbrtSlopeHiTab[i - MIN_DOUBLE_EXPONENT] = (4.0 / 3) * tmp * tmp; + } + cbrtXCbrtLoTab[0] = 1.0; + cbrtSlopeLoTab[0] = 1.0; + final long CBRT_LO_MASK = (0x3FF0000000000000L | (0x000FFFFFFFFFFFFFL >> CBRT_LO_BITS)); + for (int i = 1; i < CBRT_LO_TAB_SIZE; i++) { + long xBits = CBRT_LO_MASK | (((long) (i - 1)) << (52 - CBRT_LO_BITS)); + double cbrtX = StrictMath.cbrt(Double.longBitsToDouble(xBits)); + cbrtXCbrtLoTab[i] = cbrtX; + cbrtSlopeLoTab[i] = 1 / (cbrtX * cbrtX); + } + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/utils/Histogram.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/utils/Histogram.java new file mode 100644 index 0000000..b2e730a --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/utils/Histogram.java @@ -0,0 +1,195 @@ +/* + * Created on 21 juil. 2005 by richet + */ +package org.xbib.graphics.graph.jmathplot.utils; + +import static org.xbib.graphics.graph.jmathplot.utils.Array.getColumnCopy; +import static org.xbib.graphics.graph.jmathplot.utils.Array.insertColumn; +import static org.xbib.graphics.graph.jmathplot.utils.Array.max; +import static org.xbib.graphics.graph.jmathplot.utils.Array.mergeColumns; +import static org.xbib.graphics.graph.jmathplot.utils.Array.min; + +public class Histogram { + // histograms functions + + public static double[][] histogram_classes(double[] values, double[] bounds) { + return mergeColumns(centers(bounds), histogram(values, bounds)); + } + + public static double[][] histogram_classes(double[] values, double min, double max, int n) { + double[] bounds = bounds(values, min, max, n); + return mergeColumns(centers(bounds), histogram(values, bounds)); + } + + public static double[][] histogram_classes(double[] values, int n) { + double[] bounds = bounds(values, n); + return mergeColumns(centers(bounds), histogram(values, bounds)); + } + + public static double[] histogram(double[] values, double[] bounds) { + double[] h = new double[bounds.length - 1]; + for (int i = 0; i < values.length; i++) { + for (int j = 0; j < h.length; j++) { + if (((bounds[j + 1] - values[i]) * (bounds[j] - values[i]) <= 0) || ((bounds[j] == values[i]))) { + h[j]++; + break; + } + } + } + return h; + } + + public static double[] histogram(double[] values, double min, double max, int n) { + double[] bounds = bounds(values, min, max, n); + return histogram(values, bounds); + } + + public static double[] histogram(double[] values, int n) { + return histogram(values, n); + } + + private static double[] bounds(double[] values, int n) { + double min = min(values); + double max = max(values); + return bounds(values, min, max, n); + } + + private static double[] bounds(double[] values, double min, double max, int n) { + double[] bounds = new double[n + 1]; + for (int i = 0; i < bounds.length; i++) { + bounds[i] = min + (max - min) * i / (double) n; + } + return bounds; + } + + private static double[] centers(double[] bounds) { + double[] center = new double[bounds.length - 1]; + for (int i = 0; i < center.length; i++) { + center[i] = (bounds[i] + bounds[i + 1]) / 2; + } + return center; + } + + // histograms 2D functions + + public static double[][] histogram_classes_2D(double[][] values, double[] boundsX, double[] boundsY) { + return insertColumn(centers_2D(boundsX, boundsY), histogram_2D(values, boundsX, boundsY), 2); + } + + public static double[][] histogram_classes_2D(double[][] values, double minX, double maxX, int nX, double minY, double maxY, int nY) { + double[] valuesX = getColumnCopy(values, 0); + double[] valuesY = getColumnCopy(values, 1); + double[] boundsX = bounds(valuesX, minX, maxX, nX); + double[] boundsY = bounds(valuesY, minY, maxY, nY); + return insertColumn(centers_2D(boundsX, boundsY), histogram_2D(values, boundsX, boundsY), 2); + } + + public static double[][] histogram_classes_2D(double[][] values, int nX, int nY) { + double[] valuesX = getColumnCopy(values, 0); + double[] valuesY = getColumnCopy(values, 1); + double[] boundsX = bounds(valuesX, nX); + double[] boundsY = bounds(valuesY, nY); + return insertColumn(centers_2D(boundsX, boundsY), histogram_2D(values, boundsX, boundsY), 2); + } + + public static double[] histogram_2D(double[][] values, double[] boundsX, double[] boundsY) { + double[] h = new double[(boundsX.length - 1) * (boundsY.length - 1)]; + for (int n = 0; n < values.length; n++) { + for (int i = 0; i < boundsX.length - 1; i++) { + for (int j = 0; j < boundsY.length - 1; j++) { + if ((((boundsX[i + 1] - values[n][0]) * (boundsX[i] - values[n][0]) < 0) || ((boundsX[i] == values[n][0]))) + && (((boundsY[j + 1] - values[n][1]) * (boundsY[j] - values[n][1]) < 0) || ((boundsY[j] == values[n][1])))) { + h[index2(i, j, boundsX.length - 1)]++; + } + } + } + } + return h; + } + + public static double[] histogram_2D(double[][] values, double minX, double maxX, int nX, double minY, double maxY, int nY) { + double[] valuesX = getColumnCopy(values, 0); + double[] valuesY = getColumnCopy(values, 1); + double[] boundsX = bounds(valuesX, minX, maxX, nX); + double[] boundsY = bounds(valuesY, minY, maxY, nY); + return histogram_2D(values, boundsX, boundsY); + } + + public static double[] histogram_2D(double[][] values, int nX, int nY) { + double[] valuesX = getColumnCopy(values, 0); + double[] valuesY = getColumnCopy(values, 1); + double[] boundsX = bounds(valuesX, nX); + double[] boundsY = bounds(valuesY, nY); + return histogram_2D(values, boundsX, boundsY); + } + + private static double[][] centers_2D(double[] boundsX, double[] boundsY) { + int nb_centers = (boundsX.length - 1) * (boundsY.length - 1); + double[][] center = new double[nb_centers][2]; + for (int i = 0; i < boundsX.length - 1; i++) { + for (int j = 0; j < boundsY.length - 1; j++) { + int k = index2(i, j, boundsX.length - 1); + center[k][0] = (boundsX[i] + boundsX[i + 1]) / 2; + center[k][1] = (boundsY[j] + boundsY[j + 1]) / 2; + } + } + return center; + } + + private static int index2(int i, int j, int imax) { + return i + imax * j; + } + + // histograms 3D functions + + + public static double[][] histogram_classes_3D(double[][] values, int nX, int nY, int nZ) { + double[] valuesX = getColumnCopy(values, 0); + double[] valuesY = getColumnCopy(values, 1); + double[] valuesZ = getColumnCopy(values, 2); + double[] boundsX = bounds(valuesX, nX); + double[] boundsY = bounds(valuesY, nY); + double[] boundsZ = bounds(valuesZ, nZ); + return insertColumn(centers_3D(boundsX, boundsY, boundsZ), histogram_3D(values, boundsX, boundsY, boundsZ), 3); + } + + public static double[] histogram_3D(double[][] values, double[] boundsX, double[] boundsY, double[] boundsZ) { + double[] h = new double[(boundsX.length - 1) * (boundsY.length - 1) * (boundsZ.length - 1)]; + for (int n = 0; n < values.length; n++) { + for (int i = 0; i < boundsX.length - 1; i++) { + for (int j = 0; j < boundsY.length - 1; j++) { + for (int k = 0; k < boundsZ.length - 1; k++) { + if ((((boundsX[i + 1] - values[n][0]) * (boundsX[i] - values[n][0]) < 0) || ((boundsX[i] == values[n][0]))) + && (((boundsY[j + 1] - values[n][1]) * (boundsY[j] - values[n][1]) < 0) || ((boundsY[j] == values[n][1]))) && (((boundsZ[k + 1] - values[n][2]) * (boundsZ[k] - values[n][2]) < 0) || ((boundsZ[k] == values[n][2])))) { + h[index3(i, j, k, boundsX.length - 1, boundsY.length - 1)]++; + } + } + } + } + } + return h; + } + + + private static double[][] centers_3D(double[] boundsX, double[] boundsY, double[] boundsZ) { + int nb_centers = (boundsX.length - 1) * (boundsY.length - 1) * (boundsZ.length - 1); + double[][] center = new double[nb_centers][3]; + for (int i = 0; i < boundsX.length - 1; i++) { + for (int j = 0; j < boundsY.length - 1; j++) { + for (int k = 0; k < boundsZ.length - 1; k++) { + int l = index3(i, j, k, boundsX.length - 1, boundsY.length - 1); + center[l][0] = (boundsX[i] + boundsX[i + 1]) / 2; + center[l][1] = (boundsY[j] + boundsY[j + 1]) / 2; + center[l][2] = (boundsZ[k] + boundsZ[k + 1]) / 2; + } + } + } + return center; + } + + + private static int index3(int i, int j, int k, int imax, int jmax) { + return i + imax * j + imax * jmax * k; + } + +} diff --git a/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/utils/NumbersUtils.java b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/utils/NumbersUtils.java new file mode 100644 index 0000000..a655522 --- /dev/null +++ b/graphics-graph-jmathplot/src/main/java/org/xbib/graphics/graph/jmathplot/utils/NumbersUtils.java @@ -0,0 +1,2064 @@ +package org.xbib.graphics.graph.jmathplot.utils; + +/* + * Copyright 2012 Jeff Hain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Class containing various basic utility methods to deal with numbers. + * This class is meant to be light (no big look-up tables or such). + *

+ * Check methods return boolean if success, + * for it allows to use them in assertions. + *

+ * toString methods use capital letters, unlike JDK's toStrings, for it is more + * readable (especially, "l" and "1" can easily be confused with one another). + *

+ * Some methods have an int version additionally to the long version, + * even though long version could be used instead, for performance reasons, + * either for the methods themselves (if they do computations with ints + * instead of longs), or to be used in an int use case (like methods + * checking whether or not a signed int can fit in such number of bits). + */ +public strictfp final class NumbersUtils { + + //-------------------------------------------------------------------------- + // CONFIGURATION + //-------------------------------------------------------------------------- + + private static final boolean ASSERTIONS = false; + + //-------------------------------------------------------------------------- + // MEMBERS + //-------------------------------------------------------------------------- + + private static final int MIN_DOUBLE_EXPONENT = -1074; + private static final int MAX_DOUBLE_EXPONENT = 1023; + + /** + * All possible upper case chars for representing a number as a String. + */ + private final static char[] CHAR_BY_DIGIT; + + static { + final char minDecimal = '0'; + final char maxDecimal = '9'; + final int n1 = maxDecimal - minDecimal + 1; + final char minLetter = 'A'; + final char maxLetter = 'Z'; + final int n2 = maxLetter - minLetter + 1; + CHAR_BY_DIGIT = new char[n1 + n2]; + int i = 0; + for (char c = minDecimal; c <= maxDecimal; c++) { + CHAR_BY_DIGIT[i++] = c; + } + for (char c = minLetter; c <= maxLetter; c++) { + CHAR_BY_DIGIT[i++] = c; + } + } + + /** + * For power-of-two radixes only. + */ + private static final int[] DIV_SHIFT_BY_RADIX; + + static { + DIV_SHIFT_BY_RADIX = new int[32 + 1]; + int shift = 1; + for (int radix = 2; radix <= 32; radix *= 2) { + DIV_SHIFT_BY_RADIX[radix] = shift++; + } + } + + private final static int[] MAX_NBR_OF_NEG_INT_DIGITS_BY_RADIX = new int[Character.MAX_RADIX + 1]; + private final static int[] MAX_NBR_OF_NEG_LONG_DIGITS_BY_RADIX = new int[Character.MAX_RADIX + 1]; + + static { + for (int radix = Character.MIN_RADIX; radix <= Character.MAX_RADIX; radix++) { + /* + * Brutal but works. + * -1 for the sign. + */ + MAX_NBR_OF_NEG_INT_DIGITS_BY_RADIX[radix] = Integer.toString(Integer.MIN_VALUE, radix).length() - 1; + MAX_NBR_OF_NEG_LONG_DIGITS_BY_RADIX[radix] = Long.toString(Long.MIN_VALUE, radix).length() - 1; + } + } + + //-------------------------------------------------------------------------- + // PUBLIC METHODS + //-------------------------------------------------------------------------- + + /** + * @return True if the specified values are equal or both NaN, false otherwise. + */ + public static boolean equal(float a, float b) { + // Only does one test if a == b. + return a == b || ((a != a) && (b != b)); + } + + /** + * @return True if the specified values are equal or both NaN, false otherwise. + */ + public static boolean equal(double a, double b) { + // Only does one test if a == b. + return a == b || ((a != a) && (b != b)); + } + + /* + * min/max ranges + */ + + /** + * @return True if the specified value is in the specified range (inclusive), false otherwise. + */ + public static boolean isInRange(int min, int max, int a) { + return (min <= a) && (a <= max); + } + + /** + * @return True if the specified value is in the specified range (inclusive), false otherwise. + */ + public static boolean isInRange(long min, long max, long a) { + return (min <= a) && (a <= max); + } + + /** + * Returns false if any value is NaN. + * + * @return True if the specified value is in the specified range (inclusive), false otherwise. + */ + public static boolean isInRange(float min, float max, float a) { + return (min <= a) && (a <= max); + } + + /** + * Returns false if any value is NaN. + * + * @return True if the specified value is in the specified range (inclusive), false otherwise. + */ + public static boolean isInRange(double min, double max, double a) { + return (min <= a) && (a <= max); + } + + /* + * + */ + + /** + * @return True if does not throw. + * @throws IllegalArgumentException if the specified value is not in the specified range (inclusive). + */ + public static boolean checkIsInRange(int min, int max, int a) { + if (!isInRange(min, max, a)) { + throw new IllegalArgumentException(a + " not in [" + min + "," + max + "]"); + } + return true; + } + + /** + * @return True if does not throw. + * @throws IllegalArgumentException if the specified value is not in the specified range (inclusive). + */ + public static boolean checkIsInRange(long min, long max, long a) { + if (!isInRange(min, max, a)) { + throw new IllegalArgumentException(a + " not in [" + min + "," + max + "]"); + } + return true; + } + + /** + * @return True if does not throw. + * @throws IllegalArgumentException if the specified value is not in the specified range (inclusive) + * or any parameter is NaN. + */ + public static boolean checkIsInRange(float min, float max, float a) { + if (!isInRange(min, max, a)) { + throw new IllegalArgumentException(a + " not in [" + min + "," + max + "]"); + } + return true; + } + + /** + * @return True if does not throw. + * @throws IllegalArgumentException if the specified value is not in the specified range (inclusive) + * or any parameter is NaN. + */ + public static boolean checkIsInRange(double min, double max, double a) { + if (!isInRange(min, max, a)) { + throw new IllegalArgumentException(a + " not in [" + min + "," + max + "]"); + } + return true; + } + + /* + * bitwise ranges + */ + + /** + * @param bitSize A number of bits, in [1,32]. + * @return True if the specified value fits as a signed integer + * over the specified number of bits, false otherwise. + * @throws IllegalArgumentException if the specified number of bits is not in [1,32]. + */ + public static boolean isInRangeSigned(int a, int bitSize) { + checkBitSizeForSignedInt(bitSize); + return (minSignedIntForBitSize_noCheck(bitSize) <= a) && (a <= maxSignedIntForBitSize_noCheck(bitSize)); + } + + /** + * @param bitSize A number of bits, in [1,64]. + * @return True if the specified value fits as a signed integer + * over the specified number of bits, false otherwise. + * @throws IllegalArgumentException if the specified number of bits is not in [1,64]. + */ + public static boolean isInRangeSigned(long a, int bitSize) { + checkBitSizeForSignedLong(bitSize); + return (minSignedLongForBitSize_noCheck(bitSize) <= a) && (a <= maxSignedLongForBitSize_noCheck(bitSize)); + } + + /** + * @param bitSize A number of bits, in [1,31]. + * @return True if the specified value fits as an unsigned integer + * over the specified number of bits, false otherwise. + * @throws IllegalArgumentException if the specified number of bits is not in [1,31]. + */ + public static boolean isInRangeUnsigned(int a, int bitSize) { + return isInRange(0, maxUnsignedIntForBitSize(bitSize), a); + } + + /** + * @param bitSize A number of bits, in [1,63]. + * @return True if the specified value fits as an unsigned integer + * over the specified number of bits, false otherwise. + * @throws IllegalArgumentException if the specified number of bits is not in [1,63]. + */ + public static boolean isInRangeUnsigned(long a, int bitSize) { + return isInRange(0, maxUnsignedLongForBitSize(bitSize), a); + } + + /* + * + */ + + /** + * @param bitSize A number of bits, in [1,32]. + * @return True if does not throw. + * @throws IllegalArgumentException if the specified value does not fit + * as a signed integer over the specified number of bits. + */ + public static boolean checkIsInRangeSigned(int a, int bitSize) { + if (!isInRangeSigned(a, bitSize)) { + throw new IllegalArgumentException(a + " does not fit as a signed value over " + bitSize + " bits"); + } + return true; + } + + /** + * @param bitSize A number of bits, in [1,64]. + * @return True if does not throw. + * @throws IllegalArgumentException if the specified value does not fit + * as a signed integer over the specified number of bits. + */ + public static boolean checkIsInRangeSigned(long a, int bitSize) { + if (!isInRangeSigned(a, bitSize)) { + throw new IllegalArgumentException(a + " does not fit as a signed value over " + bitSize + " bits"); + } + return true; + } + + /** + * @param bitSize A number of bits, in [1,31]. + * @return True if does not throw. + * @throws IllegalArgumentException if the specified value does not fit + * as an unsigned integer over the specified number of bits. + */ + public static boolean checkIsInRangeUnsigned(int a, int bitSize) { + if (!isInRangeUnsigned(a, bitSize)) { + throw new IllegalArgumentException(a + " does not fit as an unsigned value over " + bitSize + " bits"); + } + return true; + } + + /** + * @param bitSize A number of bits, in [1,63]. + * @return True if does not throw. + * @throws IllegalArgumentException if the specified value does not fit + * as an unsigned integer over the specified number of bits. + */ + public static boolean checkIsInRangeUnsigned(long a, int bitSize) { + if (!isInRangeUnsigned(a, bitSize)) { + throw new IllegalArgumentException(a + " does not fit as an unsigned value over " + bitSize + " bits"); + } + return true; + } + + /* + * masks (int) + */ + + /** + * @param bitSize A number of bits, in [0,32]. + * @return Mask with the specified number of left bits set with 0, + * and other bits set with 1. + */ + public static int intMaskMSBits0(int bitSize) { + checkIsInRange(0, 32, bitSize); + // Shifting in two times, for >>> doesn't work for full bit size (<< as well). + final int halfish = (bitSize >> 1); + return ((-1) >>> halfish) >>> (bitSize - halfish); + } + + /** + * @param bitSize A number of bits, in [0,32]. + * @return Mask with the specified number of left bits set with 1, + * and other bits set with 0. + */ + public static int intMaskMSBits1(int bitSize) { + return ~intMaskMSBits0(bitSize); + } + + /** + * @param bitSize A number of bits, in [0,32]. + * @return Mask with the specified number of right bits set with 0, + * and other bits set with 1. + */ + public static int intMaskLSBits0(int bitSize) { + return ~intMaskMSBits0(32 - bitSize); + } + + /** + * @param bitSize A number of bits, in [0,32]. + * @return Mask with the specified number of right bits set with 1, + * and other bits set with 0. + */ + public static int intMaskLSBits1(int bitSize) { + return intMaskMSBits0(32 - bitSize); + } + + /* + * masks (long) + */ + + /** + * @param bitSize A number of bits, in [0,64]. + * @return Mask with the specified number of left bits set with 0, + * and other bits set with 1. + */ + public static long longMaskMSBits0(int bitSize) { + checkIsInRange(0, 64, bitSize); + // Shifting in two times, for >>> doesn't work for full bit size (<< as well). + final int halfish = (bitSize >> 1); + return ((-1L) >>> halfish) >>> (bitSize - halfish); + } + + /** + * @param bitSize A number of bits, in [0,64]. + * @return Mask with the specified number of left bits set with 1, + * and other bits set with 0. + */ + public static long longMaskMSBits1(int bitSize) { + return ~longMaskMSBits0(bitSize); + } + + /** + * @param bitSize A number of bits, in [0,64]. + * @return Mask with the specified number of right bits set with 0, + * and other bits set with 1. + */ + public static long longMaskLSBits0(int bitSize) { + return ~longMaskMSBits0(64 - bitSize); + } + + /** + * @param bitSize A number of bits, in [0,64]. + * @return Mask with the specified number of right bits set with 1, + * and other bits set with 0. + */ + public static long longMaskLSBits1(int bitSize) { + return longMaskMSBits0(64 - bitSize); + } + + /* + * signed/unsigned + */ + + /** + * @return Unsigned value corresponding to bits of the specified byte. + */ + public static short byteAsUnsigned(byte value) { + return (short) (((short) value) & 0xFF); + } + + /** + * @return Unsigned value corresponding to bits of the specified short. + */ + public static int shortAsUnsigned(short value) { + return ((int) value) & 0xFFFF; + } + + /** + * @return Unsigned value corresponding to bits of the specified int. + */ + public static long intAsUnsigned(int value) { + return ((long) value) & 0xFFFFFFFF; + } + + /* + * bitwise ranges + */ + + /** + * @return True if a signed int value can be read over the specified number of bits, + * i.e. if it is in [1,32], false otherwise. + */ + public static boolean isValidBitSizeForSignedInt(int bitSize) { + return (bitSize > 0) && (bitSize <= 32); + } + + /** + * @return True if a signed long value can be read over the specified number of bits, + * i.e. if it is in [1,64], false otherwise. + */ + public static boolean isValidBitSizeForSignedLong(int bitSize) { + return (bitSize > 0) && (bitSize <= 64); + } + + /** + * @return True if an unsigned int value can be read over the specified number of bits, + * i.e. if it is in [1,31], false otherwise. + */ + public static boolean isValidBitSizeForUnsignedInt(int bitSize) { + return (bitSize > 0) && (bitSize < 32); + } + + /** + * @return True if an unsigned long value can be read over the specified number of bits, + * i.e. if it is in [1,63], false otherwise. + */ + public static boolean isValidBitSizeForUnsignedLong(int bitSize) { + return (bitSize > 0) && (bitSize < 64); + } + + /* + * + */ + + /** + * @return True if does not throw. + * @throws IllegalArgumentException if a signed int value can't be read over the + * specified number of bits, i.e. if it is not in [1,32]. + */ + public static boolean checkBitSizeForSignedInt(int bitSize) { + if (!isValidBitSizeForSignedInt(bitSize)) { + throw new IllegalArgumentException("bit size [" + bitSize + "] must be in [1,32] for signed int values"); + } + return true; + } + + /** + * @return True if does not throw. + * @throws IllegalArgumentException if a signed long value can't be read over the + * specified number of bits, i.e. if it is not in [1,64]. + */ + public static boolean checkBitSizeForSignedLong(int bitSize) { + if (!isValidBitSizeForSignedLong(bitSize)) { + throw new IllegalArgumentException("bit size [" + bitSize + "] must be in [1,64] for signed long values"); + } + return true; + } + + /** + * @return True if does not throw. + * @throws IllegalArgumentException if an unsigned int value can't be read over the + * specified number of bits, i.e. if it is not in [1,31]. + */ + public static boolean checkBitSizeForUnsignedInt(int bitSize) { + if (!isValidBitSizeForUnsignedInt(bitSize)) { + throw new IllegalArgumentException("bit size [" + bitSize + "] must be in [1,31] for unsigned int values"); + } + return true; + } + + /** + * @return True if does not throw. + * @throws IllegalArgumentException if an unsigned long value can't be read over the + * specified number of bits, i.e. if it is not in [1,63]. + */ + public static boolean checkBitSizeForUnsignedLong(int bitSize) { + if (!isValidBitSizeForUnsignedLong(bitSize)) { + throw new IllegalArgumentException("bit size [" + bitSize + "] must be in [1,63] for unsigned long values"); + } + return true; + } + + /* + * + */ + + /** + * @param bitSize A number of bits in [1,32]. + * @return The min signed int value that can be stored over the specified number of bits. + * @throws IllegalArgumentException if the specified number of bits is out of range. + */ + public static int minSignedIntForBitSize(int bitSize) { + checkBitSizeForSignedInt(bitSize); + return minSignedIntForBitSize_noCheck(bitSize); + } + + /** + * @param bitSize A number of bits in [1,64]. + * @return The min signed long value that can be stored over the specified number of bits. + * @throws IllegalArgumentException if the specified number of bits is out of range. + */ + public static long minSignedLongForBitSize(int bitSize) { + checkBitSizeForSignedLong(bitSize); + return minSignedLongForBitSize_noCheck(bitSize); + } + + /** + * @param bitSize A number of bits in [1,32]. + * @return The max signed int value that can be stored over the specified number of bits. + * @throws IllegalArgumentException if the specified number of bits is out of range. + */ + public static int maxSignedIntForBitSize(int bitSize) { + checkBitSizeForSignedInt(bitSize); + return maxSignedIntForBitSize_noCheck(bitSize); + } + + /** + * @param bitSize A number of bits in [1,64]. + * @return The max signed long value that can be stored over the specified number of bits. + * @throws IllegalArgumentException if the specified number of bits is out of range. + */ + public static long maxSignedLongForBitSize(int bitSize) { + checkBitSizeForSignedLong(bitSize); + return maxSignedLongForBitSize_noCheck(bitSize); + } + + /** + * @param bitSize A number of bits in [1,31]. + * @return The max unsigned int value that can be stored over the specified number of bits. + * @throws IllegalArgumentException if the specified number of bits is out of range. + */ + public static int maxUnsignedIntForBitSize(int bitSize) { + checkBitSizeForUnsignedInt(bitSize); + // i.e. (1<> (31 - bitSize)); + } + + /** + * @param bitSize A number of bits in [1,63]. + * @return The max unsigned long value that can be stored over the specified number of bits. + * @throws IllegalArgumentException if the specified number of bits is out of range. + */ + public static long maxUnsignedLongForBitSize(int bitSize) { + checkBitSizeForUnsignedLong(bitSize); + // i.e. (1L<> (63 - bitSize)); + } + + /* + * + */ + + /** + * @return The number of bits required to store the specified value as a signed integer, + * i.e. a result in [1,32]. + */ + public static int bitSizeForSignedValue(int value) { + if (value > 0) { + return 33 - Integer.numberOfLeadingZeros(value); + } else if (value == 0) { + return 1; + } else { + // Works for Integer.MIN_VALUE as well. + return 33 - Integer.numberOfLeadingZeros(-value - 1); + } + } + + /** + * @return The number of bits required to store the specified value as a signed integer, + * i.e. a result in [1,64]. + */ + public static int bitSizeForSignedValue(long value) { + if (value > 0) { + return 65 - Long.numberOfLeadingZeros(value); + } else if (value == 0) { + return 1; + } else { + // Works for Long.MIN_VALUE as well. + return 65 - Long.numberOfLeadingZeros(-value - 1); + } + } + + /** + * @param value An integer value in [0,Integer.MAX_VALUE]. + * @return The number of bits required to store the specified value as an unsigned integer, + * i.e. a result in [1,31]. + * @throws IllegalArgumentException if the specified value is < 0. + */ + public static int bitSizeForUnsignedValue(int value) { + if (value > 0) { + return 32 - Integer.numberOfLeadingZeros(value); + } else { + if (value == 0) { + return 1; + } else { + throw new IllegalArgumentException("unsigned value [" + value + "] must be >= 0"); + } + } + } + + /** + * @param value An integer value in [0,Long.MAX_VALUE]. + * @return The number of bits required to store the specified value as an unsigned integer, + * i.e. a result in [1,63]. + * @throws IllegalArgumentException if the specified value is < 0. + */ + public static int bitSizeForUnsignedValue(long value) { + if (value > 0) { + return 64 - Long.numberOfLeadingZeros(value); + } else { + if (value == 0) { + return 1; + } else { + throw new IllegalArgumentException("unsigned value [" + value + "] must be >= 0"); + } + } + } + + /* + * integer functions + */ + + /** + * @return 1 if the specified value is > 0, 0 if it is 0, -1 otherwise. + */ + public static int signum(int a) { + return (a < 0) ? -1 : ((a == 0) ? 0 : 1); + } + + /** + * @return 1 if the specified value is > 0, 0 if it is 0, -1 otherwise. + */ + public static int signum(long a) { + return (a < 0) ? -1 : ((a == 0) ? 0 : 1); + } + + /** + * @return True if the specified value is even, false otherwise. + */ + public static boolean isEven(int a) { + return ((a & 1) == 0); + } + + /** + * @return True if the specified value is even, false otherwise. + */ + public static boolean isEven(long a) { + // faster to work on ints + return isEven((int) a); + } + + /** + * @return True if the specified value is odd, false otherwise. + */ + public static boolean isOdd(int a) { + return ((a & 1) != 0); + } + + /** + * @return True if the specified value is odd, false otherwise. + */ + public static boolean isOdd(long a) { + // faster to work on ints + return isOdd((int) a); + } + + /** + * @return True if the specified values are both even or both odd, false otherwise. + */ + public static boolean haveSameEvenness(int a, int b) { + return (((a ^ b) & 1) == 0); + } + + /** + * @return True if the specified values are both even or both odd, false otherwise. + */ + public static boolean haveSameEvenness(long a, long b) { + // faster to work on ints + return haveSameEvenness((int) a, (int) b); + } + + /** + * @return True if the specified values are both >= 0 or both < 0, false otherwise. + */ + public static boolean haveSameSign(int a, int b) { + return ((a ^ b) >= 0); + } + + /** + * @return True if the specified values are both >= 0 or both < 0, false otherwise. + */ + public static boolean haveSameSign(long a, long b) { + return ((a ^ b) >= 0); + } + + /** + * @return True if the specified value is a power of two, + * i.e. a value of the form 2^k, with k >= 0. + */ + public static boolean isPowerOfTwo(int a) { + if (a <= 0) { + return false; + } + if (false) { + // also works + return (a & -a) == a; + } + return (a & (a - 1)) == 0; + } + + /** + * @return True if the specified value is a power of two, + * i.e. a value of the form 2^k, with k >= 0. + */ + public static boolean isPowerOfTwo(long a) { + if (a <= 0) { + return false; + } + if (false) { + // also works + return (a & -a) == a; + } + return (a & (a - 1)) == 0; + } + + /** + * @return True if the specified value is a signed power of two, + * i.e. a value of the form +-2^k, with k >= 0. + */ + public static boolean isSignedPowerOfTwo(int a) { + if (a > 0) { + return (a & (a - 1)) == 0; + } else { + if (a == -a) { + // a is 0 or Integer.MIN_VALUE + return (a != 0); + } + return ((-a) & (-a - 1)) == 0; + } + } + + /** + * @return True if the specified value is a signed power of two, + * i.e. a value of the form +-2^k, with k >= 0. + */ + public static boolean isSignedPowerOfTwo(long a) { + if (a > 0) { + return (a & (a - 1)) == 0; + } else { + if (a == -a) { + // a is 0 or Long.MIN_VALUE + return (a != 0); + } + return ((-a) & (-a - 1)) == 0; + } + } + + /** + * @param a A value in [1,Integer.MAX_VALUE]. + * @return The highest power of two <= a. + */ + public static int floorPowerOfTwo(int a) { + if (a <= 0) { + throw new IllegalArgumentException("a [" + a + "] must be > 0"); + } + return 1 << (31 - Integer.numberOfLeadingZeros(a)); + } + + /** + * @param a A value in [1,Long.MAX_VALUE]. + * @return The highest power of two <= a. + */ + public static long floorPowerOfTwo(long a) { + if (a <= 0) { + throw new IllegalArgumentException("a [" + a + "] must be > 0"); + } + return 1L << (63 - Long.numberOfLeadingZeros(a)); + } + + /** + * @param a A value in [0,2^30]. + * @return The lowest power of two >= a. + */ + public static int ceilingPowerOfTwo(int a) { + checkIsInRange(0, (1 << 30), a); + // From Hacker's Delight, Chapter 3, Harry S. Warren Jr. + return 1 << (32 - Integer.numberOfLeadingZeros(a - 1)); + } + + /** + * @param a A value in [0,2^62]. + * @return The lowest power of two >= a. + */ + public static long ceilingPowerOfTwo(long a) { + checkIsInRange(0L, (1L << 62), a); + return 1L << (64 - Long.numberOfLeadingZeros(a - 1)); + } + + /** + * @return Mean without overflow, rounded to the lowest value (i.e. mathematical floor((a+b)/2), using floating point division). + */ + public static int meanLow(int a, int b) { + return (a & b) + ((a ^ b) >> 1); + } + + /** + * @return Mean without overflow, rounded to the lowest value (i.e. mathematical floor((a+b)/2), using floating point division). + */ + public static long meanLow(long a, long b) { + return (a & b) + ((a ^ b) >> 1); + } + + /** + * @return Mean without overflow, rounded to the value of smallest magnitude (i.e. mathematical (a+b)/2, using integer division). + */ + public static int meanSml(int a, int b) { + int result = meanLow(a, b); + if (!haveSameEvenness(a, b)) { + // inexact + if (((a & b) < 0) || (((a | b) < 0) && (a + b < 0))) { + // both < 0, or only one is < 0 and it has the largest magnitude + result++; + } + } + return result; + } + + /** + * @return Mean without overflow, rounded to the value of smallest magnitude (i.e. mathematical (a+b)/2, using integer division). + */ + public static long meanSml(long a, long b) { + long result = meanLow(a, b); + if (!haveSameEvenness(a, b)) { + // inexact + if (((a & b) < 0) || (((a | b) < 0) && (a + b < 0))) { + // both < 0, or only one is < 0 and it has the largest magnitude + result++; + } + } + return result; + } + + /** + * Useful because a positive int value could not represent half the width + * of full int range width, which is mathematically Integer.MAX_VALUE+1. + * + * @return Minus half the range width (inclusive, and rounded to the value of smaller magnitude) + * between the specified bounds. + * @throws IllegalArgumentException if min > max. + */ + public static int negHalfWidth(int min, int max) { + if (min > max) { + throw new IllegalArgumentException("min [" + min + "] must be <= max [" + max + "]"); + } + int mean = meanLow(min, max); + return min - mean - ((min ^ max) & 1); + } + + /** + * Useful because a positive long value could not represent half the width + * of full long range width, which is mathematically Long.MAX_VALUE+1. + * + * @return Minus half the range width (inclusive, and rounded to the value of smaller magnitude) + * between the specified bounds. + * @throws IllegalArgumentException if min > max. + */ + public static long negHalfWidth(long min, long max) { + if (min > max) { + throw new IllegalArgumentException("min [" + min + "] must be <= max [" + max + "]"); + } + long mean = meanLow(min, max); + return min - mean - ((min ^ max) & 1); + } + + /** + * This treatment being designed for optimization, the fact that spot + * is a signed power of two is only checked if assertions are enabled. + * + * @param value A value. + * @param spot A signed power of two (i.e. a value of the form +-2^k, k >= 0). + * @return value % spot, i.e. a value in ]-|spot|,|spot|[. + */ + public static int moduloSignedPowerOfTwo(int value, int spot) { + assert !ASSERTIONS || (isSignedPowerOfTwo(spot)); + if (spot == Integer.MIN_VALUE) { + return (value != Integer.MIN_VALUE) ? value : 0; + } else { + int s = (value >> 31); + return ((((value + s) ^ s) & (abs(spot) - 1)) + s) ^ s; + } + } + + /** + * This treatment being designed for optimization, the fact that spot + * is a signed power of two is only checked if assertions are enabled. + * + * @param value A value. + * @param spot A signed power of two (i.e. a value of the form +-2^k, k >= 0). + * @return value % spot, i.e. a value in ]-|spot|,|spot|[. + */ + public static long moduloSignedPowerOfTwo(long value, long spot) { + assert !ASSERTIONS || (isSignedPowerOfTwo(spot)); + if (spot == Long.MIN_VALUE) { + return (value != Long.MIN_VALUE) ? value : 0; + } else { + long s = (value >> 63); + return ((((value + s) ^ s) & (abs(spot) - 1)) + s) ^ s; + } + } + + /** + * @param value An integer value > 0. + * @return The integer part of the logarithm, in base 2, of the specified value, + * i.e. a result in [0,30] + * @throws IllegalArgumentException if the specified value is <= 0. + */ + public static int log2(int value) { + if (value <= 0) { + throw new IllegalArgumentException("value [" + value + "] must be > 0"); + } + return 31 - Integer.numberOfLeadingZeros(value); + } + + /** + * @param value An integer value > 0. + * @return The integer part of the logarithm, in base 2, of the specified value, + * i.e. a result in [0,62] + * @throws IllegalArgumentException if the specified value is <= 0. + */ + public static int log2(long value) { + if (value <= 0) { + throw new IllegalArgumentException("value [" + value + "] must be > 0"); + } + return 63 - Long.numberOfLeadingZeros(value); + } + + /** + * Possibly faster than java.lang.Math.abs(int). + * + * @return The absolute value, except if value is Integer.MIN_VALUE, for which it returns Integer.MIN_VALUE. + */ + public static int abs(int a) { + return (a ^ (a >> 31)) - (a >> 31); + } + + /** + * Possibly faster than java.lang.Math.abs(long). + * + * @return The absolute value, except if value is Long.MIN_VALUE, for which it returns Long.MIN_VALUE. + */ + public static long abs(long a) { + return (a ^ (a >> 63)) - (a >> 63); + } + + /** + * net.jodk.FastMath class has a typically faster version of this method, + * using look-up tables. + *

+ * Returns the exact result, provided it's in double range, + * i.e. if power is in [-1074,1023]. + * + * @param power A power. + * @return 2^power. + */ + public static double twoPow(int power) { + if (power <= -MAX_DOUBLE_EXPONENT) { // Not normal. + if (power >= MIN_DOUBLE_EXPONENT) { // Subnormal. + return Double.longBitsToDouble(0x0008000000000000L >> (-(power + MAX_DOUBLE_EXPONENT))); + } else { // Underflow. + return 0.0; + } + } else if (power > MAX_DOUBLE_EXPONENT) { // Overflow. + return Double.POSITIVE_INFINITY; + } else { // Normal. + return Double.longBitsToDouble(((long) (power + MAX_DOUBLE_EXPONENT)) << 52); + } + } + + /** + * If the specified value is in int range, the returned value is identical. + * + * @return An int hash of the specified value. + */ + public static int intHash(long a) { + if (false) { + // also works + int hash = ((int) (a >> 32)) ^ ((int) a); + if (a < 0) { + hash = -hash - 1; + } + return hash; + } + int hash = ((int) (a >> 32)) + ((int) a); + if (a < 0) { + hash++; + } + return hash; + } + + /** + * Not defining an asByte(long) method, since asByte((int)aLong) works. + * + * @param a An int value. + * @return The specified value as byte. + * @throws ArithmeticException if the specified value is not in [Byte.MIN_VALUE,Byte.MAX_VALUE] range. + */ + public static byte asByte(int a) { + if (a != (byte) a) { + throw new ArithmeticException("overflow: " + a); + } + return (byte) a; + } + + /** + * @param a A long value. + * @return The specified value as int. + * @throws ArithmeticException if the specified value is not in [Integer.MIN_VALUE,Integer.MAX_VALUE] range. + */ + public static int asInt(long a) { + if (a != (int) a) { + throw new ArithmeticException("overflow: " + a); + } + return (int) a; + } + + /** + * @param a A long value. + * @return The closest int value in [Integer.MIN_VALUE,Integer.MAX_VALUE] range. + */ + public static int toInt(long a) { + if (a != (int) a) { + return (a < (long) Integer.MIN_VALUE) ? Integer.MIN_VALUE : Integer.MAX_VALUE; + } + return (int) a; + } + + /** + * @param a An int value. + * @param b An int value. + * @return The mathematical result of a+b. + * @throws ArithmeticException if the mathematical result of a+b is not in [Integer.MIN_VALUE,Integer.MAX_VALUE] range. + */ + public static int plusExact(int a, int b) { + if ((a ^ b) < 0) { // test if a and b signs are different + return a + b; + } else { + int sum = a + b; + if ((a ^ sum) < 0) { + throw new ArithmeticException("overflow: " + a + "+" + b); + } else { + return sum; + } + } + } + + /** + * @param a A long value. + * @param b A long value. + * @return The mathematical result of a+b. + * @throws ArithmeticException if the mathematical result of a+b is not in [Long.MIN_VALUE,Long.MAX_VALUE] range. + */ + public static long plusExact(long a, long b) { + if ((a ^ b) < 0) { // test if a and b signs are different + return a + b; + } else { + long sum = a + b; + if ((a ^ sum) < 0) { + throw new ArithmeticException("overflow: " + a + "+" + b); + } else { + return sum; + } + } + } + + /** + * @param a An int value. + * @param b An int value. + * @return The int value of [Integer.MIN_VALUE,Integer.MAX_VALUE] range which is the closest to mathematical result of a+b. + */ + public static int plusBounded(int a, int b) { + return toInt(((long) a) + ((long) b)); + } + + /** + * @param a A long value. + * @param b A long value. + * @return The long value of [Long.MIN_VALUE,Long.MAX_VALUE] range which is the closest to mathematical result of a+b. + */ + public static long plusBounded(long a, long b) { + if ((a ^ b) < 0) { // test if a and b signs are different + return a + b; + } else { + long sum = a + b; + if ((a ^ sum) < 0) { + return (sum >= 0) ? Long.MIN_VALUE : Long.MAX_VALUE; + } else { + return sum; + } + } + } + + /** + * @param a An int value. + * @param b An int value. + * @return The mathematical result of a-b. + * @throws ArithmeticException if the mathematical result of a-b is not in [Integer.MIN_VALUE,Integer.MAX_VALUE] range. + */ + public static int minusExact(int a, int b) { + if ((a ^ b) >= 0) { // test if a and b signs are identical + return a - b; + } else { + int diff = a - b; + if ((a ^ diff) < 0) { + throw new ArithmeticException("overflow: " + a + "-" + b); + } else { + return diff; + } + } + } + + /** + * @param a A long value. + * @param b A long value. + * @return The mathematical result of a-b. + * @throws ArithmeticException if the mathematical result of a-b is not in [Long.MIN_VALUE,Long.MAX_VALUE] range. + */ + public static long minusExact(long a, long b) { + if ((a ^ b) >= 0) { // test if a and b signs are identical + return a - b; + } else { + long diff = a - b; + if ((a ^ diff) < 0) { + throw new ArithmeticException("overflow: " + a + "-" + b); + } else { + return diff; + } + } + } + + /** + * @param a An int value. + * @param b An int value. + * @return The int value of [Integer.MIN_VALUE,Integer.MAX_VALUE] range which is the closest to mathematical result of a-b. + */ + public static int minusBounded(int a, int b) { + return toInt(((long) a) - ((long) b)); + } + + /** + * @param a A long value. + * @param b A long value. + * @return The long value of [Long.MIN_VALUE,Long.MAX_VALUE] range which is the closest to mathematical result of a-b. + */ + public static long minusBounded(long a, long b) { + if ((a ^ b) >= 0) { // test if a and b signs are identical + return a - b; + } else { + long diff = a - b; + if ((a ^ diff) < 0) { + return (diff >= 0) ? Long.MIN_VALUE : Long.MAX_VALUE; + } else { + return diff; + } + } + } + + /** + * @param a An int value. + * @param b An int value. + * @return The mathematical result of a*b. + * @throws ArithmeticException if the mathematical result of a*b is not in [Integer.MIN_VALUE,Integer.MAX_VALUE] range. + */ + public static int timesExact(int a, int b) { + long product = a * (long) b; + if (product != (int) product) { + throw new ArithmeticException("overflow: " + a + "*" + b); + } + return (int) product; + } + + /** + * @param a A long value. + * @param b A long value. + * @return The mathematical result of a*b. + * @throws ArithmeticException if the mathematical result of a*b is not in [Long.MIN_VALUE,Long.MAX_VALUE] range. + */ + public static long timesExact(long a, long b) { + long r = a * b; + long absA = abs(a); + long absB = abs(b); + if (((absA | absB) >>> 31) != 0) { + // Some bits greater than 2^31 that might cause overflow + // Check the result using the divide operator + // and check for the special case of Long.MIN_VALUE * -1 + if (((b != 0) && (r / b != a)) || + ((a == Long.MIN_VALUE) && (b == -1))) { + throw new ArithmeticException("overflow: " + a + "*" + b); + } + } + return r; + } + + /** + * @param a An int value. + * @param b An int value. + * @return The int value of [Integer.MIN_VALUE,Integer.MAX_VALUE] range which is the closest to mathematical result of a*b. + */ + public static int timesBounded(int a, int b) { + return (int) (a * (double) b); + } + + /** + * @param a A long value. + * @param b A long value. + * @return The long value of [Long.MIN_VALUE,Long.MAX_VALUE] range which is the closest to mathematical result of a*b. + */ + public static long timesBounded(long a, long b) { + long r = a * b; + long absA = abs(a); + long absB = abs(b); + if (((absA | absB) >>> 31) != 0) { + // Some bits greater than 2^31 that might cause overflow + // Check the result using the divide operator + // and check for the special case of Long.MIN_VALUE * -1 + if (((b != 0) && (r / b != a)) || + ((a == Long.MIN_VALUE) && (b == -1))) { + return ((a ^ b) >= 0) ? Long.MAX_VALUE : Long.MIN_VALUE; + } + } + return r; + } + + /* + * integer and floating point functions + */ + + /** + * @return True if the specified value is NaN, positive of negative infinity, false otherwise. + */ + public static boolean isNaNOrInfinite(float a) { + // a-a is not equal to 0.0f (and is NaN) <-> a is NaN or +-infinity + return !(a - a == 0.0f); + } + + /** + * @return True if the specified value is NaN, positive of negative infinity, false otherwise. + */ + public static boolean isNaNOrInfinite(double a) { + // a-a is not equal to 0.0 (and is NaN) <-> a is NaN or +-infinity + return !(a - a == 0.0); + } + + /** + * @param a A value. + * @return a*a. + */ + public static int pow2(int a) { + return a * a; + } + + /** + * @param a A value. + * @return a*a. + */ + public static long pow2(long a) { + return a * a; + } + + /** + * @param a A value. + * @return a*a. + */ + public static float pow2(float a) { + return a * a; + } + + /** + * @param a A value. + * @return a*a. + */ + public static double pow2(double a) { + return a * a; + } + + /** + * @param a A value. + * @return a*a*a. + */ + public static int pow3(int a) { + return a * a * a; + } + + /** + * @param a A value. + * @return a*a*a. + */ + public static long pow3(long a) { + return a * a * a; + } + + /** + * @param a A value. + * @return a*a*a. + */ + public static float pow3(float a) { + return a * a * a; + } + + /** + * @param a A value. + * @return a*a*a. + */ + public static double pow3(double a) { + return a * a * a; + } + + /* + * + */ + + /** + * @param min A value. + * @param max A value. + * @param a A value. + * @return min if a <= min, else max if a >= max, else a. + */ + public static int toRange(int min, int max, int a) { + if (a <= min) { + return min; + } else if (a >= max) { + return max; + } else { + return a; + } + } + + /** + * @param min A value. + * @param max A value. + * @param a A value. + * @return min if a <= min, else max if a >= max, else a. + */ + public static long toRange(long min, long max, long a) { + if (a <= min) { + return min; + } else if (a >= max) { + return max; + } else { + return a; + } + } + + /** + * @param min A value. + * @param max A value. + * @param a A value. + * @return min if a <= min, else max if a >= max, else a. + */ + public static float toRange(float min, float max, float a) { + if (a <= min) { + return min; + } else if (a >= max) { + return max; + } else { + return a; + } + } + + /** + * @param min A value. + * @param max A value. + * @param a A value. + * @return min if a <= min, else max if a >= max, else a. + */ + public static double toRange(double min, double max, double a) { + if (a <= min) { + return min; + } else if (a >= max) { + return max; + } else { + return a; + } + } + + /* + * toString (radix) + */ + + /** + * @param radix Radix to be checked. + * @return True if does not throw. + * @throws IllegalArgumentException if the specified radix is not in [2,36]. + */ + public static boolean checkRadix(int radix) { + if (!isInRange(Character.MIN_RADIX, Character.MAX_RADIX, radix)) { + throw new IllegalArgumentException("radix [" + radix + "] must be in [" + Character.MIN_RADIX + "," + Character.MAX_RADIX + "]"); + } + return true; + } + + /** + * @param radix A radix in [2,36]. + * @return Number of characters (minus sign included) + * to represent the specified value in the specified radix. + */ + public static int computeNbrOfChars(int value, int radix) { + if (value < 0) { + // 1 for sign + return 1 + computeNbrOfDigits_negValue(value, radix); + } else { + return computeNbrOfDigits_negValue(-value, radix); + } + } + + /** + * @param radix A radix in [2,36]. + * @return Number of characters (minus sign included) + * to represent the specified value in the specified radix. + */ + public static int computeNbrOfChars(long value, int radix) { + if (value < 0) { + // 1 for sign + return 1 + computeNbrOfDigits_negValue(value, radix); + } else { + return computeNbrOfDigits_negValue(-value, radix); + } + } + + /** + * @param radix A radix in [2,36]. + * @param paddingUpTo Number of digits (sign excluded) up to which left-padding with zeros is done. + * @return Number of characters (minus sign included) + * to represent the specified value in the specified radix. + */ + public static int computeNbrOfChars(int value, int radix, int paddingUpTo) { + if (value < 0) { + // 1 for sign + return 1 + Math.max(paddingUpTo, computeNbrOfDigits_negValue(value, radix)); + } else { + return Math.max(paddingUpTo, computeNbrOfDigits_negValue(-value, radix)); + } + } + + /** + * @param radix A radix in [2,36]. + * @param paddingUpTo Number of digits (sign excluded) up to which left-padding with zeros is done. + * @return Number of characters (minus sign included) + * to represent the specified value in the specified radix. + */ + public static int computeNbrOfChars(long value, int radix, int paddingUpTo) { + if (value < 0) { + // 1 for sign + return 1 + Math.max(paddingUpTo, computeNbrOfDigits_negValue(value, radix)); + } else { + return Math.max(paddingUpTo, computeNbrOfDigits_negValue(-value, radix)); + } + } + + /** + * @param radix A radix in [2,36]. + * @return Number of digits of the specified value in the specified radix. + */ + public static int computeNbrOfDigits(int value, int radix) { + return computeNbrOfDigits_negValue(-abs(value), radix); + } + + /** + * @param radix A radix in [2,36]. + * @return Number of digits of the specified value in the specified radix. + */ + public static int computeNbrOfDigits(long value, int radix) { + return computeNbrOfDigits_negValue(-abs(value), radix); + } + + /** + * @param radix A radix in [2,36]. + * @param paddingUpTo Number of digits (sign excluded) up to which left-padding with zeros is done. + * @return Number of digits of the specified value in the specified radix, + * including the specified padding. + */ + public static int computeNbrOfDigits(int value, int radix, int paddingUpTo) { + return Math.max(paddingUpTo, computeNbrOfDigits(value, radix)); + } + + /** + * @param radix A radix in [2,36]. + * @param paddingUpTo Number of digits (sign excluded) up to which left-padding with zeros is done. + * @return Number of digits of the specified value in the specified radix, + * including the specified padding. + */ + public static int computeNbrOfDigits(long value, int radix, int paddingUpTo) { + return Math.max(paddingUpTo, computeNbrOfDigits(value, radix)); + } + + /** + * This method just delegates to Integer.toString(int), + * but is defined here to complete the API. + * + * @return String representation of the specified value in base 10. + */ + public static String toString(int value) { + return Integer.toString(value); + } + + /** + * This method just delegates to Long.toString(long), + * but is defined here to complete the API. + * + * @return String representation of the specified value in base 10. + */ + public static String toString(long value) { + return Long.toString(value); + } + + /** + * @param radix A radix in [2,36]. + * @return String representation of the specified value in the specified radix. + * @throws IllegalArgumentException if the specified radix is out of range. + */ + public static String toString(int value, int radix) { + return toString(value, radix, 0); + } + + /** + * @param radix A radix in [2,36]. + * @return String representation of the specified value in the specified radix. + * @throws IllegalArgumentException if the specified radix is out of range. + */ + public static String toString(long value, int radix) { + return toString(value, radix, 0); + } + + /** + * @param radix A radix in [2,36]. + * @param paddingUpTo Number of digits (sign excluded) up to which left-padding with zeros is done. + * @return String representation of the specified value in the specified radix. + * @throws IllegalArgumentException if the specified radix is out of range. + */ + public static String toString(int value, int radix, int paddingUpTo) { + // Only one test if radix+paddingUpTo != 10. + if ((radix + paddingUpTo == 10) && (paddingUpTo == 0)) { + // Using JDK's optimized algorithm. + return Integer.toString(value); + } + + int negValue; + final int signSize; + final boolean negative = (value < 0); + if (negative) { + negValue = value; + signSize = 1; + } else { + negValue = -value; + signSize = 0; + } + // Faster if we just use max possible number of characters (33), + // but we prefer to take care of garbage's memory footprint. + // Checks radix. + final int nbrOfChars = signSize + Math.max(paddingUpTo, computeNbrOfDigits_negValue(negValue, radix)); + + final char[] chars = new char[nbrOfChars]; + + int charPos = nbrOfChars; + + final boolean radixIsPowerOfTwo = ((radix & (radix - 1)) == 0); + // Not allowing Integer.MIN_VALUE so it can be negated. + if (radixIsPowerOfTwo && (negValue != Integer.MIN_VALUE)) { + final int mask = radix - 1; + final int divShift = DIV_SHIFT_BY_RADIX[radix]; + while (negValue <= -radix) { + chars[--charPos] = CHAR_BY_DIGIT[(-negValue) & mask]; + negValue = -((-negValue) >> divShift); + } + } else { + while (negValue <= -radix) { + chars[--charPos] = CHAR_BY_DIGIT[-(negValue % radix)]; + negValue /= radix; + } + } + chars[--charPos] = CHAR_BY_DIGIT[-negValue]; + + while (charPos > signSize) { + chars[--charPos] = '0'; + } + + if (negative) { + chars[0] = '-'; + } + + return new String(chars); + } + + /** + * @param radix A radix in [2,36]. + * @param paddingUpTo Number of digits (sign excluded) up to which left-padding with zeros is done. + * @return String representation of the specified value in the specified radix. + * @throws IllegalArgumentException if the specified radix is out of range. + */ + public static String toString(long value, int radix, int paddingUpTo) { + // Only one test if radix+paddingUpTo != 10. + if ((radix + paddingUpTo == 10) && (paddingUpTo == 0)) { + // Using JDK's optimized algorithm. + return Long.toString(value); + } + + long negValue; + final int signSize; + final boolean negative = (value < 0); + if (negative) { + negValue = value; + signSize = 1; + } else { + negValue = -value; + signSize = 0; + } + // Checks radix. + final int nbrOfChars = signSize + Math.max(paddingUpTo, computeNbrOfDigits_negValue(negValue, radix)); + + final char[] chars = new char[nbrOfChars]; + + int charPos = nbrOfChars; + + final boolean radixIsPowerOfTwo = ((radix & (radix - 1)) == 0); + // Not allowing Long.MIN_VALUE so it can be negated. + if (radixIsPowerOfTwo && (negValue != Long.MIN_VALUE)) { + final int mask = radix - 1; + final int divShift = DIV_SHIFT_BY_RADIX[radix]; + while (negValue <= -radix) { + chars[--charPos] = CHAR_BY_DIGIT[(int) ((-negValue) & mask)]; + negValue = -((-negValue) >> divShift); + } + } else { + while (negValue <= -radix) { + chars[--charPos] = CHAR_BY_DIGIT[(int) (-(negValue % radix))]; + negValue /= radix; + } + } + chars[--charPos] = CHAR_BY_DIGIT[(int) (-negValue)]; + + while (charPos > signSize) { + chars[--charPos] = '0'; + } + + if (negative) { + chars[0] = '-'; + } + + return new String(chars); + } + + /* + * toString (bits) + */ + + /** + * @param firstBitPos First bit position (inclusive). + * @param lastBitPosExcl Last bit position (exclusive). + * @return True if does not throw. + * @throws IllegalArgumentException if the specified bit range does not fit in a byte. + */ + public static boolean checkBitPositionsByte(int firstBitPos, int lastBitPosExcl) { + return checkBitPositions(firstBitPos, lastBitPosExcl, 8); + } + + /** + * @param firstBitPos First bit position (inclusive). + * @param lastBitPosExcl Last bit position (exclusive). + * @return True if does not throw. + * @throws IllegalArgumentException if the specified bit range does not fit in a short. + */ + public static boolean checkBitPositionsShort(int firstBitPos, int lastBitPosExcl) { + return checkBitPositions(firstBitPos, lastBitPosExcl, 16); + } + + /** + * @param firstBitPos First bit position (inclusive). + * @param lastBitPosExcl Last bit position (exclusive). + * @return True if does not throw. + * @throws IllegalArgumentException if the specified bit range does not fit in an int. + */ + public static boolean checkBitPositionsInt(int firstBitPos, int lastBitPosExcl) { + return checkBitPositions(firstBitPos, lastBitPosExcl, 32); + } + + /** + * @param firstBitPos First bit position (inclusive). + * @param lastBitPosExcl Last bit position (exclusive). + * @return True if does not throw. + * @throws IllegalArgumentException if the specified bit range does not fit in a long. + */ + public static boolean checkBitPositionsLong(int firstBitPos, int lastBitPosExcl) { + return checkBitPositions(firstBitPos, lastBitPosExcl, 64); + } + + /** + * @return String representation of specified bits, in big endian. + */ + public static String toStringBits(byte bits) { + final char[] chars = new char[8]; + int bitIndex = 8; + while (--bitIndex >= 0) { + chars[7 - bitIndex] = (char) ('0' + ((bits >> bitIndex) & 1)); + } + return new String(chars); + } + + /** + * @return String representation of specified bits, in big endian. + */ + public static String toStringBits(short bits) { + final char[] chars = new char[16]; + int bitIndex = 16; + while (--bitIndex >= 0) { + chars[15 - bitIndex] = (char) ('0' + ((bits >> bitIndex) & 1)); + } + return new String(chars); + } + + /** + * @return String representation of specified bits, in big endian. + */ + public static String toStringBits(int bits) { + final char[] chars = new char[32]; + int bitIndex = 32; + while (--bitIndex >= 0) { + chars[31 - bitIndex] = (char) ('0' + ((bits >> bitIndex) & 1)); + } + return new String(chars); + } + + /** + * @return String representation of specified bits, in big endian. + */ + public static String toStringBits(long bits) { + final char[] chars = new char[64]; + int bitIndex = 64; + while (--bitIndex >= 0) { + chars[63 - bitIndex] = (char) ('0' + ((bits >> bitIndex) & 1)); + } + return new String(chars); + } + + /** + * @param firstBitPos First bit position (inclusive). + * @param lastBitPosExcl Last bit position (exclusive). + * @param bigEndian True for bits to be added in big endian order (MSBit to LSBit) + * false for little endian order. + * @param padding True if underscores must be added instead of out-of-range bits, + * false to just add characters corresponding to in-range bits. + * @return String representation of specified bits. + */ + public static String toStringBits( + byte bits, + int firstBitPos, + int lastBitPosExcl, + boolean bigEndian, + boolean padding) { + checkBitPositionsByte(firstBitPos, lastBitPosExcl); + return toStringBits_0_32_bitPosAlreadyChecked(8, bits, firstBitPos, lastBitPosExcl, bigEndian, padding); + } + + /** + * @param firstBitPos First bit position (inclusive). + * @param lastBitPosExcl Last bit position (exclusive). + * @param bigEndian True for bits to be added in big endian order (MSBit to LSBit) + * false for little endian order. + * @param padding True if underscores must be added instead of out-of-range bits, + * false to just add characters corresponding to in-range bits. + * @return String representation of specified bits. + */ + public static String toStringBits( + short bits, + int firstBitPos, + int lastBitPosExcl, + boolean bigEndian, + boolean padding) { + checkBitPositionsShort(firstBitPos, lastBitPosExcl); + return toStringBits_0_32_bitPosAlreadyChecked(16, bits, firstBitPos, lastBitPosExcl, bigEndian, padding); + } + + /** + * @param firstBitPos First bit position (inclusive). + * @param lastBitPosExcl Last bit position (exclusive). + * @param bigEndian True for bits to be added in big endian order (MSBit to LSBit) + * false for little endian order. + * @param padding True if underscores must be added instead of out-of-range bits, + * false to just add characters corresponding to in-range bits. + * @return String representation of specified bits. + */ + public static String toStringBits( + int bits, + int firstBitPos, + int lastBitPosExcl, + boolean bigEndian, + boolean padding) { + checkBitPositionsInt(firstBitPos, lastBitPosExcl); + return toStringBits_0_32_bitPosAlreadyChecked(32, bits, firstBitPos, lastBitPosExcl, bigEndian, padding); + } + + /** + * @param firstBitPos First bit position (inclusive). + * @param lastBitPosExcl Last bit position (exclusive). + * @param bigEndian True for bits to be added in big endian order (MSBit to LSBit) + * false for little endian order. + * @param padding True if underscores must be added instead of out-of-range bits, + * false to just add characters corresponding to in-range bits. + * @return String representation of specified bits. + */ + public static String toStringBits( + long bits, + int firstBitPos, + int lastBitPosExcl, + boolean bigEndian, + boolean padding) { + checkBitPositionsLong(firstBitPos, lastBitPosExcl); + final int bitSize = 64; + final int bitSizeM1 = bitSize - 1; + final int lastBitPos = lastBitPosExcl - 1; + if (padding) { + final int nbrOfChars = bitSize; + final char[] chars = new char[nbrOfChars]; + int bitIndex = bitSizeM1; + if (bigEndian) { + final int firstBitIndex = bitSizeM1 - lastBitPos; + final int lastBitIndex = bitSizeM1 - firstBitPos; + while (bitIndex > lastBitIndex) { + chars[bitSizeM1 - bitIndex] = '_'; + --bitIndex; + } + while (bitIndex >= firstBitIndex) { + chars[bitSizeM1 - bitIndex] = (char) ('0' + ((bits >> bitIndex) & 1)); + --bitIndex; + } + while (bitIndex >= 0) { + chars[bitSizeM1 - bitIndex] = '_'; + --bitIndex; + } + } else { + while (bitIndex > lastBitPos) { + chars[bitIndex] = '_'; + --bitIndex; + } + while (bitIndex >= firstBitPos) { + chars[bitIndex] = (char) ('0' + ((bits >> bitIndex) & 1)); + --bitIndex; + } + while (bitIndex >= 0) { + chars[bitIndex] = '_'; + --bitIndex; + } + } + return new String(chars); + } else { + final int nbrOfChars = (lastBitPosExcl - firstBitPos); + final char[] chars = new char[nbrOfChars]; + if (bigEndian) { + final int firstBitIndex = bitSizeM1 - lastBitPos; + final int lastBitIndex = bitSizeM1 - firstBitPos; + int bitIndex = lastBitIndex; + while (bitIndex >= firstBitIndex) { + chars[lastBitIndex - bitIndex] = (char) ('0' + ((bits >> bitIndex) & 1)); + --bitIndex; + } + } else { + int bitIndex = lastBitPos; + while (bitIndex >= firstBitPos) { + chars[bitIndex - firstBitPos] = (char) ('0' + ((bits >> bitIndex) & 1)); + --bitIndex; + } + } + return new String(chars); + } + } + + //-------------------------------------------------------------------------- + // PRIVATE METHODS + //-------------------------------------------------------------------------- + + /** + * Had such isInXXX methods, and corresponding checkXXX methods, + * but they seem actually slower in practice, so just keeping this + * code here in case some day it becomes faster than regular isInXXX. + *

+ * Only works for non-empty ranges, i.e. such as min <= max. + * This treatment being designed for optimization, min <= max + * is only checked is assertions are enabled. + * + * @return True if the specified value is in the specified range (inclusive), false otherwise. + */ + private static boolean dontUseMe_isInNonEmptyRange_(int min, int max, int a) { + assert !ASSERTIONS || (min <= max); + // Using modulo arithmetic. + return (Integer.MIN_VALUE + (a - min) <= Integer.MIN_VALUE + (max - min)); + } + + /* + * + */ + + private static int minSignedIntForBitSize_noCheck(int bitSize) { + // i.e. (-1<<(bitSize-1)) + return (Integer.MIN_VALUE >> (32 - bitSize)); + } + + private static long minSignedLongForBitSize_noCheck(int bitSize) { + // i.e. (-1L<<(bitSize-1)) + return (Long.MIN_VALUE >> (64 - bitSize)); + } + + private static int maxSignedIntForBitSize_noCheck(int bitSize) { + // i.e. (1<<(bitSize-1))-1 + return (Integer.MAX_VALUE >> (32 - bitSize)); + } + + private static long maxSignedLongForBitSize_noCheck(int bitSize) { + // i.e. (1L<<(bitSize-1))-1 + return (Long.MAX_VALUE >> (64 - bitSize)); + } + + /* + * + */ + + /** + * @throws IllegalArgumentException if the specified radix is out of range. + */ + private static int computeNbrOfDigits_negValue(int negValue, int radix) { + checkRadix(radix); + final int maxNbrOfDigits = MAX_NBR_OF_NEG_INT_DIGITS_BY_RADIX[radix]; + int p = radix; + for (int i = 1; i < maxNbrOfDigits; i++) { + if (negValue > -p) { + return i; + } + p *= radix; + } + return maxNbrOfDigits; + } + + /** + * @throws IllegalArgumentException if the specified radix is out of range. + */ + private static int computeNbrOfDigits_negValue(long negValue, int radix) { + checkRadix(radix); + final int maxNbrOfDigits = MAX_NBR_OF_NEG_LONG_DIGITS_BY_RADIX[radix]; + long p = radix; + for (int i = 1; i < maxNbrOfDigits; i++) { + if (negValue > -p) { + return i; + } + p *= radix; + } + return maxNbrOfDigits; + } + + /* + * + */ + + private static boolean checkBitPositions(int firstBitPos, int lastBitPosExcl, int bitSize) { + if ((firstBitPos < 0) || (firstBitPos > lastBitPosExcl) || (lastBitPosExcl > bitSize)) { + throw new IllegalArgumentException( + "bit positions (first=" + firstBitPos + ",lastExcl=" + lastBitPosExcl + + ") must verify 0 <= first <= lastExcl <= " + bitSize); + } + return true; + } + + /** + * Common method for byte, short and int. + * Could be a bit faster to have specific methods for byte and short, + * but not much, and that would also make more messy (byte-)code. + * + * @param bitSize Must be in [0,32]. + */ + private static String toStringBits_0_32_bitPosAlreadyChecked( + int bitSize, + int bits, + int firstBitPos, + int lastBitPosExcl, + boolean bigEndian, + boolean padding) { + assert !ASSERTIONS || ((bitSize >= 0) && (bitSize <= 32)); + final int bitSizeM1 = bitSize - 1; + final int lastBitPos = lastBitPosExcl - 1; + if (padding) { + final int nbrOfChars = bitSize; + final char[] chars = new char[nbrOfChars]; + int bitIndex = bitSizeM1; + if (bigEndian) { + final int firstBitIndex = bitSizeM1 - lastBitPos; + final int lastBitIndex = bitSizeM1 - firstBitPos; + while (bitIndex > lastBitIndex) { + chars[bitSizeM1 - bitIndex] = '_'; + --bitIndex; + } + while (bitIndex >= firstBitIndex) { + chars[bitSizeM1 - bitIndex] = (char) ('0' + ((bits >> bitIndex) & 1)); + --bitIndex; + } + while (bitIndex >= 0) { + chars[bitSizeM1 - bitIndex] = '_'; + --bitIndex; + } + } else { + while (bitIndex > lastBitPos) { + chars[bitIndex] = '_'; + --bitIndex; + } + while (bitIndex >= firstBitPos) { + chars[bitIndex] = (char) ('0' + ((bits >> bitIndex) & 1)); + --bitIndex; + } + while (bitIndex >= 0) { + chars[bitIndex] = '_'; + --bitIndex; + } + } + return new String(chars); + } else { + final int nbrOfChars = (lastBitPosExcl - firstBitPos); + final char[] chars = new char[nbrOfChars]; + if (bigEndian) { + final int firstBitIndex = bitSizeM1 - lastBitPos; + final int lastBitIndex = bitSizeM1 - firstBitPos; + int bitIndex = lastBitIndex; + while (bitIndex >= firstBitIndex) { + chars[lastBitIndex - bitIndex] = (char) ('0' + ((bits >> bitIndex) & 1)); + --bitIndex; + } + } else { + int bitIndex = lastBitPos; + while (bitIndex >= firstBitPos) { + chars[bitIndex - firstBitPos] = (char) ('0' + ((bits >> bitIndex) & 1)); + --bitIndex; + } + } + return new String(chars); + } + } +} \ No newline at end of file diff --git a/graphics-graph-jmathplot/src/test/java/org/xbib/graphics/graph/jmathplot/test/QuantileLayerPlotTest.java b/graphics-graph-jmathplot/src/test/java/org/xbib/graphics/graph/jmathplot/test/QuantileLayerPlotTest.java new file mode 100644 index 0000000..0b3572f --- /dev/null +++ b/graphics-graph-jmathplot/src/test/java/org/xbib/graphics/graph/jmathplot/test/QuantileLayerPlotTest.java @@ -0,0 +1,25 @@ +package org.xbib.graphics.graph.jmathplot.test; + +import org.junit.jupiter.api.Test; +import org.xbib.graphics.graph.jmathplot.panel.FrameView; +import org.xbib.graphics.graph.jmathplot.panel.Plot2DPanel; +import javax.swing.JFrame; + +public class QuantileLayerPlotTest { + + @Test + public void test() throws InterruptedException { + Plot2DPanel p2 = new Plot2DPanel(); + for (int i = 0; i < 1; i++) { + double[][] XYZ = new double[10][2]; + for (int j = 0; j < XYZ.length; j++) { + XYZ[j][0] = /*1 + */Math.random(); + XYZ[j][1] = /*100 * */Math.random(); + } + p2.addScatterPlot("toto" + i, XYZ); + } + p2.addQuantiletoPlot(0, 1, 1.0, true, 0.2); + new FrameView(p2).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + Thread.sleep(10000); + } +} 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 e7c2511..eef9359 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,3 +10,5 @@ include 'graphics-pdfbox' include 'graphics-pdfbox-layout' include 'graphics-pdfbox-groovy' include 'graphics-printer' +include 'graphics-graph-gral' +include 'graphics-graph-jmathplot'