/*
* Copyright (C) 2011 Alasdair C. Hamilton
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package ketUI.panel;
import java.awt.image.*;
import java.util.Vector;
import java.util.*;
import java.awt.Graphics2D;
import java.awt.Color;
import geom.Position;
import geom.Offset;
import ket.*;
import ket.display.*;
import ket.display.Transition;
import ket.display.box.Box;
import ket.display.box.BoxText;
import ket.math.*;
import ket.treeDiff.Step;
import ket.treeDiff.TreeDiff;
/**
* A treemap is fills a box with smaller boxes. Usually this is nested, but
* this is currently only a subset of the current list.
*/
public class GridDisplay implements Display {
Set<BoxEquation> boxSet;
Vector<Equation> equations = null; //? Used?
public static final double GAP = 10.0; // Horizontal gap
static final double ANIMATION_TIME = 250.0; // ms
static final boolean animate = false;
final MathCollection mathCollection;
/**
* When the equation is drawn, record its box from which the arguments
* associated with a mouse click may be determined.
*/
Box afterRootBox;
Box equationBox;
Box labelBox;
Position equationTopLeft;
Vector<Transition> transitions;
boolean repaint;
long initialTime;
int index;
public GridDisplay(MathCollection mathCollection) {
this.mathCollection = mathCollection;
boxSet = null;
initialTime = 0L;
transitions = null;
index = 0;
}
/**
* Find the equation that is closest to the given point on the
* ketPanel.
*/
@Override
public Equation pointToEquation(Position p) {
// Find the closest equation to the mouse click where part of it is above and to the left of the click.
Equation e = null;
Double bestLength2 = null; // Skip unnecessary and expensive square roots.
for (BoxEquation next : boxSet) {
Box box = next.getEquationBox();
if (box==null) continue; //HACK
if (box.getTopLeft()==null) continue; // HACK
double left = box.getTopLeft().x;
double top = box.getTopLeft().y;
double dx = p.x - left;
double dy = p.y - top;
if (dx>0 && dy>0) {
double length2 = dx*dx + dy*dy;
if (bestLength2==null || length2<bestLength2) {
bestLength2 = length2;
e = box.getArgument().getEquation();
}
}
}
return e;
}
/*
* Look through boxSet and while a box contains the location within its
* inner rectangle, return that box.
*/
@Override
public Box findDeepestBox(Position p) {
for (BoxEquation next : boxSet) {
Box match = next.findDeepestBox(p);
if (match!=null) {
return match;
}
}
return null;
}
/*
* Look through boxSet and while a box contains the location within its
* inner rectangle, return that box.
*/
@Override
public Argument findDeepestArgument(Position p) {
for (BoxEquation next : boxSet) {
Argument match = next.findDeepestArgument(p); // used
if (match!=null) {
return match;
}
}
return null;
}
@Override
public Vector<Integer> findVisibleEquationIndices() {
Vector<Integer> indices = new Vector<Integer>();
if (boxSet==null) return indices;
for (BoxEquation next : boxSet) {
//?- if (animate) continue;
//? if (equationBox.getArgument()!=null && next.getEquation()==equationBox.getArgument().getEquation()) continue;
Box equationPart = next.getEquationBox();
if (equationPart==null) continue;
Argument a = equationPart.getArgument();
if (a==null) continue;
int index = a.getEquationIndex();
indices.add(index);
}
return indices;
}
/**
* This is called every time the user interacts with the box model, either changing or selecting.
*/
@Override
public boolean generateBoxes(Graphics2D g2D, ColourScheme colourScheme, int fontSize, Offset panelRectangle) {
generateList(g2D, colourScheme, fontSize, panelRectangle);
return generateAnimation(g2D, colourScheme, fontSize, panelRectangle);
}
private void generateList(Graphics2D g2D, ColourScheme colourScheme, int fontSize, Offset panelRectangle) {
Vector<BoxEquation> source = getValidBoxEquations(g2D, fontSize, colourScheme);
stuff(g2D, colourScheme, fontSize, panelRectangle, source);
HashSet<BoxEquation> boxSet = new HashSet<BoxEquation>();
for (BoxEquation boxEquation : source) {
if (boxEquation!=null && boxEquation.isValid()) {
boxSet.add(boxEquation);
}
}
this.boxSet = boxSet;
}
private void stuff(Graphics2D g2D, ColourScheme colourScheme, int fontSize, Offset panelRectangle, Vector<BoxEquation> source) {
double maxWidth = panelRectangle.width-KetPanel.LEFT_BORDER_SIZE-KetPanel.RIGHT_BORDER_SIZE;
double maxHeight = panelRectangle.height-KetPanel.TOP_BORDER_SIZE-KetPanel.BOTTOM_BORDER_SIZE;
double width = 0.0;
double height = 0.0;
double widest = 0.0;
int ithColumn=0;
Vector<BoxEquation> column = new Vector<BoxEquation>();
for (BoxEquation boxEquation : source) {
if (maxHeight<height+boxEquation.getHeight()) {
if (ithColumn>=index) {
setupColumn(g2D, column, width, widest, true);
width += GAP + widest;
if (maxWidth < width) {
// Boxes are wider than the window so there is nothing left to draw.
return;
}
}
ithColumn += 1;
height = 0.0;
column = new Vector<BoxEquation>();
widest = 0.0;
}
column.add(boxEquation);
widest = boxWrap(boxEquation, widest, maxWidth, g2D, true);
height += boxEquation.getHeight();
}
if (ithColumn<index) {
index = ithColumn;
}
if (!column.isEmpty()) { // There remains a few colun elements.
if (width==0.0) { // Single column: Use the full width of the window.
setupColumn(g2D, column, 0.0, maxWidth, false);
} else {
setupColumn(g2D, column, width, widest, true);
width += GAP + widest;
}
}
// Spread equations out evenly across the horizontal.
if (width>0.0 && width<maxWidth) { // i.e. multiple lines with gaps.
double scale = maxWidth/width; // final 'widest' has already been added.
for (BoxEquation boxEquation : source) {
if (boxEquation==null || !boxEquation.isValid()) continue;
Position topLeft = boxEquation.getTopLeft();
topLeft.x *= scale;
}
}
}
private double boxWrap(BoxEquation boxEquation, double widest, double maxWidth, Graphics2D g2D, boolean scale) {
assert boxEquation!=null;
Equation e = boxEquation.getEquation();
assert e!=null;
// Don't measure the width of text and instead have it wrap to multiple lines.
if (!e.isText()) {
return Math.max(widest, boxEquation.getFullWidth());
} // otherwise:
double wrappedWidth = scale ? Math.min(boxEquation.getWidth(), maxWidth/2.0) : maxWidth;
Offset leftRectangle = new Offset(wrappedWidth, Double.MAX_VALUE);
boxEquation.getEquationBox().setupOuterRectangle(leftRectangle);
return Math.max(widest, boxEquation.getFullWidth());
}
private Vector<Equation> getValidEquations() {
Vector<Equation> es = new Vector<Equation>();
Selection selection = mathCollection.getSelection();
for (int boxIndex=0; boxIndex<getNumberOfEquations(); boxIndex++) {
Equation equation = selection.getEquationList().getEquation(boxIndex);
boolean valid = true;
if (equation==null) continue;
if (equations!=null && !equation.isIn(equations)) continue;
es.add(equation);
}
return es;
}
private Vector<BoxEquation> getValidBoxEquations(Graphics2D g2D, int fontSize, ColourScheme colourScheme) {
Vector<BoxEquation> source = new Vector<BoxEquation>();
for (Equation equation : getValidEquations()) {
Box box = equation.toBox(colourScheme);
box.setupInnerRectangle(fontSize);
Box labelBox = equation.getLabelBox(colourScheme);
labelBox.setupInnerRectangle(fontSize);
BoxEquation boxEquation = new BoxEquation(box, labelBox);
source.add(boxEquation);
}
return source;
}
private void setupColumn(Graphics2D g2D, Vector<BoxEquation> column, double width, double widest, boolean multiple) {
double sum = 0.0;
for (BoxEquation b : column) {
assert b!=null; // HACK
double step = b.getHeight();
Box box = b.getEquationBox();
Box labelBox = b.getLabel();
if (b.getEquation().isText() ) {
box.setProperties(Box.LEFT_ALIGN, Box.TOP_ALIGN, Box.TOP_ALIGN);
} else if (multiple) { // TODO: Indent rather than centre?
box.setProperties(Box.LEFT_ALIGN, Box.Y_CENTRE_ALIGN, Box.Y_CENTRE_ALIGN);
} else {
box.setProperties(Box.X_CENTRE_ALIGN, Box.Y_CENTRE_ALIGN, Box.Y_CENTRE_ALIGN);
}
labelBox.setProperties(Box.RIGHT_ALIGN, Box.SMALL_FONT, Box.BOLD_FONT);
Offset equationRectangle = new Offset(widest, step);
labelBox.setupOuterRectangle(equationRectangle);
Offset leftRectangle = new Offset(equationRectangle);
leftRectangle.width -= labelBox.getInnerRectangle().width;
box.setupOuterRectangle(leftRectangle);
double left = KetPanel.LEFT_BORDER_SIZE + width;
double top = KetPanel.TOP_BORDER_SIZE + sum;
Position topLeft = new Position(left, top);
b.setTopLeft(topLeft);
sum += step;
}
}
private synchronized boolean generateAnimation(Graphics2D g2D, ColourScheme colourScheme, int fontSize, Offset panelRectangle) {
Equation equation = mathCollection.getCursor().getEquation();
if (equation==null) { System.out.println(" @134 "); return false; } // BUG: Why is this required?
equationBox = equation.toBox(colourScheme);
if (equationBox==null) { System.out.println(" @321 "); return false; }
labelBox = equation.getLabelBox(colourScheme);
labelBox.setProperty(Box.RIGHT_ALIGN);
equationBox.setupInnerRectangle(fontSize);
Offset equationRectangle = calcEquationRectangle(panelRectangle);
if (equationRectangle==null) { System.out.println(" @498 "); return false; }
labelBox.setup(fontSize, equationRectangle);
Offset windowWithoutLabel = calcWindowWithoutLabel(equationRectangle);
equationBox.setupOuterRectangle(windowWithoutLabel);
equationBox.calcRootOffset();
return true;
}
@Override
public void paint(Graphics2D g2D, ColourScheme colourScheme, int fontSize, Offset panelRectangle) {
Position position = null; // <--- Locally repurpose (q) version.
if (boxSet!=null) {
position = paintBoxes(g2D, colourScheme);
}
if (equationBox!=null && position!=null) {
paintAnimation(g2D, colourScheme, position);
}
}
private Position paintBoxes(Graphics2D g2D, ColourScheme colourScheme) {
Position position = null;
for (BoxEquation boxEquation : boxSet) {
if (animate && boxEquation.getEquation()==equationBox.getArgument().getEquation()) {
position = boxEquation.getTopLeft();
} else {
g2D.setColor(Color.BLACK); // <-- use colourscheme.
Box eb = boxEquation.getEquationBox();
boxEquation.paint(g2D, colourScheme);
}
}
return position;
}
private void paintAnimation(Graphics2D g2D, ColourScheme colourScheme, Position position) {
double fractionalTime = (System.currentTimeMillis()-initialTime) / ANIMATION_TIME;
if (transitions!=null && fractionalTime<1.0) {
repaint = true;
labelBox.paint(g2D, position, colourScheme);
for (Transition t : transitions) {
t.animate(g2D, colourScheme, fractionalTime, position);
}
} else {
repaint = false;
initialTime = 0L;
labelBox.paint(g2D, position, colourScheme);
equationBox.paint(g2D, position, colourScheme);
transitions = null;
}
}
@Override
public boolean isArgumentVisible(Argument argument) {
if (boxSet==null) return false;
for (BoxEquation boxEquation : boxSet) {
Box box = boxEquation.getEquationBox();
if (box.containsArgument(argument)) {
return true;
}
}
return false;
}
/**
* Set the current argument from the logically significant index of
* visible equations.
*/
public void selectByVisible(Cursor cursor, EquationList equationList) {
int currentIndex = getLogicalIndex();
if (currentIndex<0) {
currentIndex = 0;
}
if (currentIndex>=equationList.size()) {
currentIndex = equationList.size() - 1;
}
Equation e = equationList.getEquations().get(currentIndex);
cursor.setCurrent(e.getVisibleRoot());
}
private int getLogicalIndex() {
return index;
}
public void viewEquation(int index) {
this.index = bound(index, 0, getNumberOfEquations());
}
@Override
public boolean requiresRepaint() {
return animate && repaint;
}
/**
* This provides a list of equations that are to be displayed (or all
* if equations is null).
*/
public void setVisibleEquations(Vector<Equation> equations) {
this.equations = equations;
}
public Vector<Equation> getVisibleEquations() {
return equations;
}
@Override
public void noteChange(Graphics2D g2D, ColourScheme colourScheme, Argument before, Equation afterEquation, int fontSize, Offset panelRectangle) {
Argument after = afterEquation.getRoot();
if ( !animate || !isSingleSelectionSet() || before.subBranchEquals(after) || afterEquation.isText()) { //+ Did the equation change?
transitions = null;
afterRootBox = null;
return;
}
afterRootBox = after.toBox(0L, colourScheme);
labelSetup(afterEquation, colourScheme);
Offset windowWithoutLabel = afterBoxSetup(fontSize, panelRectangle);
Box beforeRootBox = beforeBoxSetup(g2D, colourScheme, before, fontSize, windowWithoutLabel);
initialTime = System.currentTimeMillis();
TreeDiff treeDiff = new TreeDiff(before, after);
transitions = treeDiff.getTransitions(beforeRootBox, afterRootBox);
}
private void labelSetup(Equation afterEquation, ColourScheme colourScheme) {
labelBox = afterEquation.getLabelBox(colourScheme);
labelBox.setProperty(Box.RIGHT_ALIGN);
labelBox.setProperty(Box.Y_CENTRE_ALIGN);
}
/**
* Setup the box that represents the previous state.
*/
private Box beforeBoxSetup(Graphics2D g2D, ColourScheme colourScheme, Argument before, int fontSize, Offset windowWithoutLabel) {
Box beforeRootBox = before.toBox(0L, colourScheme);
beforeRootBox.setupInnerRectangle(fontSize);
beforeRootBox.setupOuterRectangle(windowWithoutLabel);
beforeRootBox.calcRootOffset();
return beforeRootBox;
}
/**
* Animate the box that represents the new state.
*/
private Offset afterBoxSetup(int fontSize, Offset panelRectangle) {
double shapeWidth = panelRectangle.width - KetPanel.BORDER_OFFSET.width;
afterRootBox.setupInnerRectangle(fontSize);
double minimumHeight = afterRootBox.getInnerRectangle().height;
Offset equationRectangle = new Offset(shapeWidth, minimumHeight);
labelBox.setup(fontSize, equationRectangle);
Offset windowWithoutLabel = calcWindowWithoutLabel(equationRectangle);
afterRootBox.setupOuterRectangle(windowWithoutLabel);
afterRootBox.calcRootOffset();
return windowWithoutLabel;
}
private synchronized Offset calcEquationRectangle(Offset panelRectangle) {
double shapeWidth = panelRectangle.width - KetPanel.BORDER_OFFSET.width;
if (equationBox==null) { System.out.println(" @291 "); return null; }
if (equationBox.getInnerRectangle()==null) { System.out.println(" @743 "); return null; }
double minimumHeight = equationBox.getInnerRectangle().height;
return new Offset(shapeWidth, minimumHeight);
}
private Offset calcWindowWithoutLabel(Offset equationRectangle) {
return new Offset(equationRectangle.width-labelBox.getInnerRectangle().width, equationRectangle.height);
}
/*-
private double getBoxHeight(Box box, Offset panelRectangle) {
return KetPanel.TOP_BORDER_SIZE;
}
*/
public void moveViewUp() {
//- setInitialBoxIndex(getInitialBoxIndex()-1);
this.index = bound(index-1, 0, getNumberOfEquations());
}
public void moveViewDown() {
//- setInitialBoxIndex(getInitialBoxIndex()+1);
this.index = bound(index+1, 0, getNumberOfEquations());
}
private int getNumberOfEquations() {
Selection selection = mathCollection.getSelection();
return selection.getEquationList().size();
}
/**
* Restrict the value of index within [min, max).
*/
private int bound(int index, int min, int max) {
if (index <= min) {
return min;
} else if (max <= index) {
return max;
} else {
return index;
}
}
private boolean isSingleSelectionSet() {
return mathCollection.isSingleSelectionSet();
}
}