/*
* Copyright 2013 Red Hat, Inc. and/or its affiliates.
*
* 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.
*/
package org.optaplanner.examples.common.swingui.timetable;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.LayoutManager2;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class TimeTableLayout implements LayoutManager2, Serializable {
public static final int FILL_COLLISIONS_FLAG = -1;
private List<Column> columns;
private List<Row> rows;
private List<List<Cell>> cells;
private Map<Component, ComponentSpan> spanMap;
private boolean stale;
private int totalColumnWidth;
private int totalRowHeight;
public TimeTableLayout() {
reset();
}
public void reset() {
columns = new ArrayList<>();
rows = new ArrayList<>();
cells = new ArrayList<>();
spanMap = new HashMap<>();
stale = false;
totalColumnWidth = 0;
totalRowHeight = 0;
}
public int addColumn() {
return addColumn(true, 0);
}
public int addColumn(int baseWidth) {
if (baseWidth < 0) {
throw new IllegalArgumentException("Invalid baseWidth (" + baseWidth + ").");
}
return addColumn(false, baseWidth);
}
private int addColumn(boolean autoWidth, int baseWidth) {
if (rows.size() > 0) {
throw new IllegalStateException("Add all columns before adding rows");
}
stale = true;
int index = columns.size();
Column column = new Column(index, autoWidth, baseWidth);
columns.add(column);
cells.add(new ArrayList<>());
return index;
}
public int addRow() {
return addRow(true, 0);
}
public int addRow(int baseHeight) {
if (baseHeight < 0) {
throw new IllegalArgumentException("Invalid baseHeight (" + baseHeight + ").");
}
return addRow(false, baseHeight);
}
public int addRow(boolean autoHeight, int baseHeight) {
stale = true;
int index = rows.size();
Row row = new Row(index, autoHeight, baseHeight);
rows.add(row);
for (int i = 0; i < columns.size(); i++) {
Column column = columns.get(i);
cells.get(i).add(new Cell(column, row));
}
return index;
}
@Override
public void addLayoutComponent(Component component, Object o) {
TimeTableLayoutConstraints c = (TimeTableLayoutConstraints) o;
if (c.getXEnd() > columns.size()) {
throw new IllegalArgumentException("The xEnd (" + c.getXEnd()
+ ") is > columnsSize (" + columns.size() + ").");
}
if (c.getYEnd() > rows.size()) {
throw new IllegalArgumentException("The yEnd (" + c.getYEnd()
+ ") is > rowsSize (" + rows.size() + ").");
}
stale = true;
ComponentSpan span = new ComponentSpan(component);
spanMap.put(component, span);
span.topLeftCell = cells.get(c.getX()).get(c.getY());
span.bottomRightCell = cells.get(c.getXEnd() - 1).get(c.getYEnd() - 1);
Set<Integer> occupiedCollisionIndexes = new HashSet<>();
for (int i = c.getX(); i < c.getXEnd(); i++) {
for (int j = c.getY(); j < c.getYEnd(); j++) {
Cell cell = cells.get(i).get(j);
cell.column.stale = true;
cell.row.stale = true;
cell.spans.add(span);
span.cells.add(cell);
occupiedCollisionIndexes.addAll(cell.occupiedCollisionIndexes);
}
}
Integer collisionIndex = 0;
while (occupiedCollisionIndexes.contains(collisionIndex)) {
collisionIndex++;
}
if (c.isFillCollisions()) {
if (collisionIndex != 0 || occupiedCollisionIndexes.contains(FILL_COLLISIONS_FLAG)) {
throw new IllegalArgumentException("There is a collision in the cell range ("
+ (c.getX() == c.getXEnd() - 1 ? c.getX() : c.getX() + "-" + (c.getXEnd() - 1))
+ ", " + (c.getY() == c.getYEnd() - 1 ? c.getY() : c.getY() + "-" + (c.getYEnd() - 1))
+ ").");
}
collisionIndex = FILL_COLLISIONS_FLAG;
}
span.collisionIndex = collisionIndex;
for (Cell cell : span.cells) {
cell.occupiedCollisionIndexes.add(collisionIndex);
}
}
@Override
public void addLayoutComponent(String name, Component component) {
// No effect
}
@Override
public void removeLayoutComponent(Component component) {
stale = true;
ComponentSpan span = spanMap.remove(component);
for (Cell cell : span.cells) {
cell.spans.remove(span);
cell.column.stale = true;
cell.row.stale = true;
cell.occupiedCollisionIndexes.remove(span.collisionIndex);
}
}
@Override
public Dimension minimumLayoutSize(Container parent) {
update();
return new Dimension(totalColumnWidth, totalRowHeight);
}
@Override
public Dimension preferredLayoutSize(Container parent) {
update();
return new Dimension(totalColumnWidth, totalRowHeight);
}
@Override
public Dimension maximumLayoutSize(Container target) {
update();
return new Dimension(totalColumnWidth, totalRowHeight);
}
@Override
public float getLayoutAlignmentX(Container target) {
return 0.5f;
}
@Override
public float getLayoutAlignmentY(Container target) {
return 0.5f;
}
@Override
public void invalidateLayout(Container target) {
// No effect
}
@Override
public void layoutContainer(Container parent) {
update();
synchronized (parent.getTreeLock()) {
for (ComponentSpan span : spanMap.values()) {
int x1 = span.topLeftCell.column.boundX;
int collisionIndexStart = (span.collisionIndex == FILL_COLLISIONS_FLAG)
? 0 : span.collisionIndex;
int y1 = span.topLeftCell.row.boundY + (collisionIndexStart * span.topLeftCell.row.baseHeight);
int x2 = span.bottomRightCell.column.boundX + span.bottomRightCell.column.baseWidth;
int collisionIndexEnd = (span.collisionIndex == FILL_COLLISIONS_FLAG)
? span.bottomRightCell.row.collisionCount : span.collisionIndex + 1;
int y2 = span.bottomRightCell.row.boundY + (collisionIndexEnd * span.bottomRightCell.row.baseHeight);
span.component.setBounds(x1, y1, x2 - x1, y2 - y1);
}
}
}
public void update() {
if (!stale) {
return;
}
refreshColumns();
refreshRows();
stale = false;
}
private void refreshColumns() {
for (Column column : columns) {
if (column.stale) {
if (column.autoWidth) {
column.baseWidth = getMaxCellWidth(column);
}
column.stale = false;
}
}
refreshColumnsBoundX();
}
private int getMaxCellWidth(Column column) {
int maxCellWidth = 0;
for (int i = 0; i < rows.size(); i++) {
Cell cell = cells.get(column.index).get(i);
for (ComponentSpan span : cell.spans) {
int width = span.getPreferredWidthPerCell();
if (width > maxCellWidth) {
maxCellWidth = width;
}
}
}
return maxCellWidth;
}
private void refreshColumnsBoundX() {
int nextColumnBoundX = 0;
for (Column column : columns) {
column.boundX = nextColumnBoundX;
nextColumnBoundX += column.baseWidth;
}
totalColumnWidth = nextColumnBoundX;
}
private void refreshRows() {
for (Row row : rows) {
if (row.stale) {
if (row.autoHeight) {
row.baseHeight = getMaxCellHeight(row);
}
row.collisionCount = getMaxCollisionCount(row);
}
row.stale = false;
}
freshRowsBoundY();
}
private int getMaxCellHeight(Row row) {
int maxCellHeight = 0;
for (int i = 0; i < columns.size(); i++) {
Cell cell = cells.get(i).get(row.index);
for (ComponentSpan span : cell.spans) {
int height = span.getPreferredHeightPerCell();
if (height > maxCellHeight) {
maxCellHeight = height;
}
}
}
return maxCellHeight;
}
private int getMaxCollisionCount(Row row) {
int maxCollisionCount = 1;
for (int i = 0; i < columns.size(); i++) {
Cell cell = cells.get(i).get(row.index);
if (cell.occupiedCollisionIndexes.size() > maxCollisionCount) {
maxCollisionCount = cell.occupiedCollisionIndexes.size();
}
}
return maxCollisionCount;
}
private void freshRowsBoundY() {
int nextRowBoundY = 0;
for (Row row : rows) {
row.boundY = nextRowBoundY;
nextRowBoundY += row.baseHeight * row.collisionCount;
}
totalRowHeight = nextRowBoundY;
}
private static class Column {
private final int index;
private final boolean autoWidth;
private boolean stale;
private int baseWidth;
private int boundX = -1;
private Column(int index, boolean autoWidth, int baseWidth) {
this.index = index;
this.autoWidth = autoWidth;
stale = true;
this.baseWidth = baseWidth;
}
}
private static class Row {
private final int index;
private final boolean autoHeight;
private boolean stale;
private int baseHeight;
private int collisionCount = 1;
private int boundY = -1;
private Row(int index, boolean autoHeight, int baseHeight) {
this.index = index;
this.autoHeight = autoHeight;
stale = true;
this.baseHeight = baseHeight;
}
}
private static class Cell {
private Column column;
private Row row;
private Set<ComponentSpan> spans = new HashSet<>();
private Set<Integer> occupiedCollisionIndexes = new HashSet<>();
private Cell(Column column, Row row) {
this.column = column;
this.row = row;
}
}
private static class ComponentSpan {
private Component component;
private Set<Cell> cells = new HashSet<>();
private Cell topLeftCell;
private Cell bottomRightCell;
private Integer collisionIndex;
private ComponentSpan(Component component) {
this.component = component;
}
public int getPreferredWidthPerCell() {
int width = component.getPreferredSize().width;
int horizontalCellSize = bottomRightCell.column.index - topLeftCell.column.index + 1;
return (width + (horizontalCellSize - 1)) / horizontalCellSize; // Ceil rounding
}
public int getPreferredHeightPerCell() {
int height = component.getPreferredSize().height;
int verticalCellSize = bottomRightCell.row.index - topLeftCell.row.index + 1;
return (height + (verticalCellSize - 1)) / verticalCellSize; // Ceil rounding
}
}
}