/*******************************************************************************
* Mission Control Technologies, Copyright (c) 2009-2012, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* The MCT platform is 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.
*
* MCT includes source code licensed under additional open source licenses. See
* the MCT Open Source Licenses file included with this distribution or the About
* MCT Licenses dialog available at runtime from the MCT Help menu for additional
* information.
*******************************************************************************/
package gov.nasa.arc.mct.table.model;
import gov.nasa.arc.mct.components.AbstractComponent;
import gov.nasa.arc.mct.services.component.ComponentRegistry;
import gov.nasa.arc.mct.table.access.ServiceAccess;
import gov.nasa.arc.mct.table.policy.TableViewPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implements a data structure that maintains the structure of a tabular
* array of objects.
*/
public class TableStructure {
private static final Logger logger = LoggerFactory.getLogger(TableStructure.class);
/** The type of table, 0-dimensional, 1-dimensional, or 2-dimensional. */
private TableType type;
/** The root component. */
private AbstractComponent rootComponent;
/** The maximum number of columns of any row. */
private int columnCount = 0;
/**
* Creates a tabular structure of a given dimensionality.
*
* @param type the type of tabular structure, 0-dimensional, 1-dimensional,
* or 2-dimensional
* @param rootComponent the root of the table structure
*/
public TableStructure(TableType type, AbstractComponent rootComponent) {
this.type = type;
this.rootComponent = rootComponent;
buildTable();
updateColumnCount();
}
/**
* Tells the structure that children have been added to or removed from
* the root component of the structure.
*/
public void notifyTableStructureChanged() {
updateColumnCount();
}
private void updateColumnCount() {
if (type != TableType.TWO_DIMENSIONAL) {
columnCount = 1;
} else {
columnCount = 1; // We always pretend to have at least one column, so that we can drag-and-drop.
for (List<TableCell> row : rowList) {
if (row.size() > columnCount) {
columnCount = row.size();
}
}
}
}
/**
* Gets the number of rows in the tabular structure.
*
* @return the number of rows
*/
public int getRowCount() {
if (type == TableType.ZERO_DIMENSIONAL) {
return 1;
} else {
return Math.max(1, rowList.size());
}
}
/**
* Gets the number of columns in the tabular structure. The number
* of columns is the maximum number of elements in any row.
*
* @return the number of columns
*/
public int getColumnCount() {
return Math.max(1, columnCount);
}
/**
* Tests whether the table structure is a skeleton, ready for values to be
* dropped in to flesh out the table cells and columns. A skeleton table
* structure is a 2-D table that doesn't yet have any values.
*
* @return true, if the table is a skeleton
*/
public boolean isSkeleton() {
return type==TableType.TWO_DIMENSIONAL && rowList.size() == 0;
}
/**
* Gets a value in the tabular array.
*
* @param rowIndex the row for which to get the value
* @param columnIndex the column for which to get the value
* @return the value at the row and column position, or null if there is no value at that position
* @throws ArrayIndexOutOfBoundsException
*/
public AbstractComponent getValue(int rowIndex, int columnIndex) {
try {
if (rowIndex < 0 || rowIndex >= getRowCount()) {
logger.warn("Row index " + rowIndex + " > " + (getRowCount() - 1));
}
if (columnIndex < 0 || columnIndex >= columnCount) {
logger.warn("Column index " + columnIndex + " > " + (columnCount - 1));
}
if (rowList.size() > 0) {
TableRow row = rowList.get(rowIndex);
if (row.size() > 0 && columnIndex < row.size()) {
return row.get(columnIndex).getValue();
} else {
return null; // Not all rows are same width, so pad with nulls
}
} else {
return null; // Not all rows are same width, so pad with nulls
}
} catch (IndexOutOfBoundsException outOfBoundsException) {
logger.warn("IndexOutOfBoundsException: {0}", outOfBoundsException);
return null;
}
}
/**
* Sets a value at a row and column position in the tabular
* array.
*
* @param rowIndex the row for which to set the value
* @param columnIndex the column for which to set the value
* @param isInsertRow true, if the value should be inserted in a new row prior to the indicated row
* @param isInsertColumn true, if the value should be inserted in a new column prior to the indicated row
* @param value the new value
*/
public void setValue(int rowIndex, int columnIndex, boolean isInsertRow, boolean isInsertColumn, AbstractComponent value) {
assert canSetValue(rowIndex, columnIndex, isInsertRow, isInsertColumn);
assert type != TableType.ZERO_DIMENSIONAL;
// Refuse values which cannot be placed in tables
// TODO: User notification? Or disallow drop to begin with?
if (!canEmbedInTable(value)) return;
if (type == TableType.ONE_DIMENSIONAL) {
// Determine where in the root component we want to add the component
int componentIndex;
if (rowIndex < rowList.size()) {
componentIndex = rowList.get(rowIndex).get(0).getIndex();
} else {
componentIndex = rootComponent.getComponents().size(); //Add it to the end
}
// Remove the old component, if there is one
if (!isInsertRow) {
TableCell cell = rowList.get(rowIndex).get(columnIndex);
AbstractComponent toBeRemoved = cell.getValue();
rootComponent.removeDelegateComponent(toBeRemoved);
}
rootComponent.addDelegateComponents(componentIndex, Collections.<AbstractComponent> singletonList(value));
} else {
if (isInsertRow) {
// Should insert new child. Create a collection of the same type as
// the root object.
int componentIndex;
if (rowIndex < rowList.size()) {
componentIndex = rowList.get(rowIndex).getIndex();
} else {
componentIndex = rootComponent.getComponents().size();
}
AbstractComponent newChild = ServiceAccess.getService(ComponentRegistry.class).newCollection(Collections.singleton(value));
rootComponent.addDelegateComponents(componentIndex, Collections.singleton(newChild));
} else { //Insert column, or overwrite cell
TableRow row = rowList.get(rowIndex);
int componentIndex;
AbstractComponent targetComponent;
if (!isInsertColumn && columnIndex < row.size()) {
}
if (columnIndex < row.size()) {
targetComponent = row.get(columnIndex).getParent();
componentIndex = row.get(columnIndex).getIndex();
if (!isInsertColumn) {
targetComponent.removeDelegateComponent(row.get(columnIndex).getValue());
}
} else {
targetComponent = row.get(row.size()-1).getParent();
componentIndex = targetComponent.getComponents().size();
}
targetComponent.addDelegateComponents(componentIndex, Collections.singleton(value));
}
updateColumnCount(); // in case we added a new column
}
buildTable();
}
/**
* Tests whether a value can be set at a position. We can only set values
* where existing values already exist, or where they are within the bounds
* of the matrix and would extend a row or column. That is, we cannot allow gaps to
* exist within an object.
*
* @param rowIndex the row index at which to set the value
* @param columnIndex the column index at which to set the value
* @param isInsertRow true, if the value should be inserted in a new row prior to the indicated row
* @param isInsertColumn true, if the value should be inserted in a new column prior to the indicated row
* @return true, if the value can be set at the position
*/
public boolean canSetValue(int rowIndex, int columnIndex, boolean isInsertRow, boolean isInsertColumn) {
if (isInsertRow && isInsertColumn) {
return false;
}
if (rowIndex < 0 || columnIndex < 0) {
return false;
}
if (rowIndex > rowList.size() || columnIndex > columnCount) {
return false;
}
// Cannot modify a single element table.
if (type == TableType.ZERO_DIMENSIONAL) {
return false;
}
// Can only set values in column zero for a one dimensional table.
if (type == TableType.ONE_DIMENSIONAL) {
if (columnIndex != 0) {
return false;
}
if (isInsertRow) {
return 0<=rowIndex && rowIndex <= rowList.size();
} else {
return 0<=rowIndex && rowIndex < rowList.size();
}
}
// Two dimensional case. We have to ensure there is an existing value at the position, or
// that the position is within the bounds of the matrix and just past the last value.
// Special case of an empty table. We can only insert a row into 0, 0.
if (rowList.size() == 0) {
return rowIndex==0 && columnIndex==0 && isInsertRow;
}
// Can only insert rows at column zero.
if (isInsertRow) {
return columnIndex==0 && 0<=rowIndex && rowIndex <= getRowCount();
}
// Otherwise must set value in an existing row.
if (rowIndex < 0 || rowIndex >= getRowCount()) {
return false;
}
List<TableCell> row = rowList.get(rowIndex);
if (isInsertColumn) {
return 0<=columnIndex && columnIndex <= row.size();
} else {
return 0<=columnIndex && columnIndex <= row.size() && columnIndex < columnCount;
}
}
/**
* Get the component that will be modified by insertion at the given location.
* @param rowIndex the row index.
* @param columnIndex the column index.
* @param isInsertRow boolean flag to check for whether can insert row.
* @param isInsertColumn boolean flag to check for whether can insert column.
* @return the component whose children will change by this insertion.
*/
public AbstractComponent getModifiedComponent(int rowIndex, int columnIndex, boolean isInsertRow, boolean isInsertColumn) {
assert canSetValue(rowIndex, columnIndex, isInsertRow, isInsertColumn);
assert type != TableType.ZERO_DIMENSIONAL;
if (rowIndex == rowList.size()) {
return rootComponent;
}
if (columnIndex == rowList.get(rowIndex).size()) {
return rowList.get(rowIndex).get(columnIndex - 1).getParent();
}
// Otherwise, we'll modify the parent of a cell
return rowList.get(rowIndex).get(columnIndex).getParent();
}
/**
* Gets the type of tabular structure.
*
* @return the type of tabular structure.
*/
public TableType getType() {
return type;
}
/**
* Determine if this component is displayable as a cell within a table.
* @param comp the abstract component.
* @return true if the component can be displayed in a table, false if it should be skipped.
*/
private boolean canEmbedInTable(AbstractComponent comp) {
return TableViewPolicy.canEmbedInTable(comp);
}
private ArrayList<TableRow> rowList;
private void buildTable () {
rowList = new ArrayList<TableRow>();
if (type == TableType.ZERO_DIMENSIONAL) {
TableRow row = new TableRow();
row.add(new TableCell(rootComponent, null, 0));
rowList.add(row);
} else if (type == TableType.ONE_DIMENSIONAL) {
int index = 0;
for (AbstractComponent child : rootComponent.getComponents()) {
if (canEmbedInTable(child)) {
TableRow row = new TableRow();
row.setIndex(index);
row.add(new TableCell(child, rootComponent, index));
rowList.add(row);
}
index++;
}
} else if (type == TableType.TWO_DIMENSIONAL) {
int parentIndex = 0;
for (AbstractComponent child : rootComponent.getComponents()) {
int index = 0;
TableRow row = new TableRow();
row.setIndex(parentIndex);
for (AbstractComponent grandChild : child.getComponents()) {
if (canEmbedInTable(grandChild)) {
row.add(new TableCell(grandChild, child, index));
}
index++;
}
if (!row.isEmpty()) rowList.add(row);
parentIndex++;
}
}
}
private class TableRow extends ArrayList<TableCell> {
private static final long serialVersionUID = 6852306825313602935L;
private int index = -1;
public void setIndex(int index) { this.index=index; }
public int getIndex() { return index; }
}
private class TableCell {
AbstractComponent value;
AbstractComponent parent;
int index;
public TableCell(AbstractComponent value, AbstractComponent parent, int index) {
this.value = value;
this.parent = parent;
this.index = index;
}
public AbstractComponent getValue() { return value; }
public int getIndex() { return index; }
public AbstractComponent getParent(){ return parent;}
}
}