/*
* 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 ket.display.box;
import geom.Offset;
import geom.Position;
import java.awt.geom.*;
import java.awt.*;
import ket.math.*;
import ket.display.ColourScheme;
public class BoxGraphic extends Box {
/////////////////////
// DISPLAY OPTIONS //
/////////////////////
public static final int HORIZONTAL_LINE = 0x01;
public static final int VERTICAL_LINE = 0x02;
public static final int RADICAL = 0x03;
public static final int NOTHING = 0x04;
public static final int OPEN_SQUARE = 0x05;
public static final int CLOSE_SQUARE = 0x06;
public static final int OPEN_ROUND = 0x07;
public static final int CLOSE_ROUND = 0x08;
public static final int OPEN_PARENTHESIS = 0x09;
public static final int CLOSE_PARENTHESIS = 0x0a;
public static final int INTEGRAL = 0x0b;
public static final int CONTOUR = 0x0c;
public static final int BRA = 0x0d;
public static final int KET = 0x0e;
///////////////////////
// GRAPHICS SETTINGS //
///////////////////////
// Fractional height (from base) of left tick line (without thickness).
static final double FRACTIONAL_LEFT_TICK_HEIGHT = 2.0/5.0;
// Fractional inset of horizontal part of the square root.
static final double FRACTIONAL_HORIZONTAL_INSET = 1.0/9.0;
static final double INTEGRAL_RATIO = 1.9;
public static final int THICKNESS = 2;
public static final int GUESS_SIZE = 20;
// Spacing around round brackets on the left, top and right of the arc.
static final double GAP = 0.025;
/**
* A flag that denotes which of the pre-determined shapes this instance
* represents.
*/
private int shape;
@Override
public Box cloneBox() {
return new BoxGraphic(getArgument(), getSettings(), shape);
}
public BoxGraphic(Argument argument, long settings, int shape) {
super(argument, settings);
this.shape = shape;
}
@Override
protected void calcMinimumSize() {
switch (shape) {
case HORIZONTAL_LINE:
innerRectangle = new Offset(0.0, THICKNESS);
break;
case VERTICAL_LINE:
innerRectangle = new Offset(THICKNESS, 0.0);
break;
case NOTHING:
innerRectangle = new Offset(0.0, 0.0);
break;
case RADICAL:
// TODO: While using a minimum size to ensure a
// kind of aspect ratio, this should be
// replaced with a context aware alternative.
innerRectangle = new Offset(GUESS_SIZE, GUESS_SIZE);
break;
case OPEN_SQUARE:
innerRectangle = new Offset(3*THICKNESS, 0.0);
break;
case CLOSE_SQUARE:
innerRectangle = new Offset(3*THICKNESS, 0.0);
break;
case OPEN_ROUND:
innerRectangle = new Offset(3*THICKNESS, 0.0);
break;
case CLOSE_ROUND:
innerRectangle = new Offset(3*THICKNESS, 0.0);
break;
case CONTOUR:
innerRectangle = new Offset(GUESS_SIZE, GUESS_SIZE);
break;
case INTEGRAL:
innerRectangle = new Offset(GUESS_SIZE, GUESS_SIZE);
break;
case OPEN_PARENTHESIS:
innerRectangle = new Offset(PARENTHESIS_MIN);
break;
case CLOSE_PARENTHESIS:
innerRectangle = new Offset(PARENTHESIS_MIN);
break;
case BRA:
innerRectangle = new Offset(GUESS_SIZE, GUESS_SIZE);
break;
case KET:
innerRectangle = new Offset(GUESS_SIZE, GUESS_SIZE);
break;
default:
throw new RuntimeException("unfinished");
}
}
@Override
public void setupOuterRectangle(Offset actualSize) {
switch (shape) {
case HORIZONTAL_LINE:
innerRectangle = new Offset(actualSize.width, THICKNESS);
break;
case VERTICAL_LINE:
innerRectangle = new Offset(THICKNESS, actualSize.height);
break;
case NOTHING:
innerRectangle = new Offset(0.0, 0.0);
break;
case CONTOUR:
innerRectangle = new Offset(actualSize);
break;
case INTEGRAL:
innerRectangle = new Offset(actualSize);
if (actualSize.height < INTEGRAL_RATIO*actualSize.width) { // Does this matter?
innerRectangle.width = innerRectangle.height/INTEGRAL_RATIO;
}
break;
case RADICAL:
// TODO: While using a minimum size to ensure a
// kind of aspect ratio, this should be
// replaced with a context aware alternative.
innerRectangle = new Offset(actualSize);
break;
case OPEN_SQUARE:
innerRectangle = new Offset(2*THICKNESS, actualSize.height);
break;
case CLOSE_SQUARE:
innerRectangle = new Offset(2*THICKNESS, actualSize.height);
break;
case OPEN_ROUND:
innerRectangle = new Offset(0, actualSize.height);
break;
case CLOSE_ROUND:
innerRectangle = new Offset(0, actualSize.height); //? 0?
break;
case OPEN_PARENTHESIS:
innerRectangle = new Offset(PARENTHESIS_MIN.width, actualSize.height);
break;
case CLOSE_PARENTHESIS:
innerRectangle = new Offset(PARENTHESIS_MIN.height, actualSize.height);
break;
case BRA:
innerRectangle = new Offset(actualSize);
break;
case KET:
innerRectangle = new Offset(actualSize);
break;
default:
throw new RuntimeException("unfinished");
}
super.setupOuterRectangle(actualSize);
}
//? static final int scale = 1;
//?public static Offset PARENTHESIS_MIN = new Offset(scale*GUESS_SIZE, scale*5*GUESS_SIZE/2);
//- public static Offset PARENTHESIS_MIN = new Offset(0.6*GUESS_SIZE, GUESS_SIZE);
public static Offset PARENTHESIS_MIN = new Offset(GUESS_SIZE, 5.0*GUESS_SIZE/3.0);
@Override
public void draw(Graphics2D g2D, Position topLeft, ColourScheme colourScheme) {
if (outerRectangle==null) return; //BUG: Cause?
colourSetup(g2D, colourScheme);
double left = getXPosition(topLeft);
double top = getYPosition(topLeft);
double width = outerRectangle.width;
double height = outerRectangle.height;
Point A = new Point((int) left, (int) top);
Point B = new Point((int) (left+width), (int) (top+height));
switch (shape) {
case HORIZONTAL_LINE:
g2D.fillRect((int) left, (int) top, (int) width, THICKNESS);
break;
case VERTICAL_LINE:
g2D.fillRect((int) left, (int) top, THICKNESS, (int) height);
break;
case NOTHING:
break;
case CONTOUR:
// Draw anulus here and use INTEGRAL to draw the rest.
GeneralPath annulus = new GeneralPath();
double cx = left + width/2.0;
double cy = top + height/2.0;
double r = width/4.0;
annulus.moveTo(cx+r, cy); // 1
oneEighty(annulus, cx+r,cy, cx,cy+r, cx-r,cy); // 2,3,4
oneEighty(annulus, cx-r,cy, cx,cy-r, cx+r,cy); // 5,6,7
r -= THICKNESS;
annulus.moveTo(cx+r,cy);
oneEighty(annulus, cx+r,cy, cx,cy-r, cx-r,cy);
oneEighty(annulus, cx-r,cy, cx,cy+r, cx+r,cy);
annulus.closePath();
g2D.fill(annulus);
//...
case INTEGRAL:
{
if (height < INTEGRAL_RATIO*width) {
width = innerRectangle.height/INTEGRAL_RATIO;
}
double a = width/2.0;
double t = THICKNESS/2.0;
double q = (a + t)/2.0;
GeneralPath path = new GeneralPath();
path.moveTo(left+width, top+q); // 1
oneEighty(path, left+width, top+q, left+width-q, top, left+a-t, top+q);
path.lineTo(left+a-t, top+height-q); // 4
oneEighty(path, left+a-t,top+height-q,left+q,top+height-THICKNESS,left+THICKNESS,top+height-q);
path.lineTo(left, top+height-q); // 7
oneEighty(path, left,top+height-q,left+q,top+height,left+a+t,top+height-q);
path.lineTo(left+a+t, top+q); // 10
oneEighty(path, left+a+t,top+q, left+width-q,top+THICKNESS, left+width-THICKNESS,top+q);
path.closePath();
g2D.fill(path);
}
break;
case RADICAL:
double M = FRACTIONAL_HORIZONTAL_INSET * width;
double h = FRACTIONAL_LEFT_TICK_HEIGHT * height;
double tanPhi = (h+height-THICKNESS)/(width - M);
double sinPhi = tanPhi / Math.sqrt(1.0 + tanPhi*tanPhi);
double m = h/tanPhi;
double k = THICKNESS*(sinPhi - 1.0/tanPhi);
int[] xPoints = new int[]{
(int) left,
(int) left,
(int) (left+M),
(int) (left+M+m),
(int) (left+width),
(int) (left+width),
(int) (left+width-k),
(int) (left+m+M),
(int) (left+M+k),
(int) left
};
int[] yPoints = new int[]{
(int) (top+height-THICKNESS-h),
(int) (top+height-h),
(int) (top+height-h),
(int) (top+height),
(int) (top+THICKNESS),
(int) top,
(int) top,
(int) (top+height-2.0*THICKNESS*sinPhi),
(int) (top+height-THICKNESS-h),
(int) (top+height-THICKNESS-h)
};
g2D.fillPolygon(xPoints, yPoints, xPoints.length);
break;
case OPEN_SQUARE:
int[] openSquareX = new int[]{
(int) left,
(int) (left+2*THICKNESS),
(int) (left+2*THICKNESS),
(int) (left+THICKNESS),
(int) (left+THICKNESS),
(int) (left+2*THICKNESS),
(int) (left+2*THICKNESS),
(int) left,
(int) left};
int[] openSquareY = new int[]{
(int) top,
(int) top,
(int) (top+THICKNESS),
(int) (top+THICKNESS),
(int) (top+height-THICKNESS),
(int) (top+height-THICKNESS),
(int) (top+height),
(int) (top+height),
(int) top};
g2D.fillPolygon(openSquareX, openSquareY, openSquareX.length);
break;
case CLOSE_SQUARE:
int[] closeSquareX = new int[]{
(int) (left + 2*THICKNESS),
(int) left,
(int) left,
(int) (left + THICKNESS),
(int) (left + THICKNESS),
(int) left,
(int) left,
(int) (left + 2*THICKNESS),
(int) (left + 2*THICKNESS)};
int[] closeSquareY = new int[]{
(int) top,
(int) top,
(int) (top+THICKNESS),
(int) (top+THICKNESS),
(int) (top+height-THICKNESS),
(int) (top+height-THICKNESS),
(int) (top+height),
(int) (top+height),
(int) top};
g2D.fillPolygon(closeSquareX, closeSquareY, closeSquareX.length);
break;
case OPEN_ROUND:
drawOpenRoundBracket(g2D, THICKNESS/2.0+left+GAP*width, top+GAP*height, (1.0-2.0*GAP)*width, (1.0-GAP)*height);
break;
case CLOSE_ROUND:
drawCloseRoundBracket(g2D, left+GAP*width, top+GAP*height, (1.0-2.0*GAP)*width, (1.0-GAP)*height);
break;
case OPEN_PARENTHESIS:
drawParenthesis(g2D, A, B, true, THICKNESS);
break;
case CLOSE_PARENTHESIS:
drawParenthesis(g2D, A, B, false, THICKNESS);
break;
case BRA:
lessThan(g2D, left, top, width, height);
break;
case KET:
greaterThan(g2D, left, top, width, height);
break;
default:
throw new RuntimeException("unfinished");
}
}
private void lessThan(Graphics2D g2D, double left, double top, double width, double height) {
int w = (int) (2.0*THICKNESS/Math.sqrt(2.0)); // ish
width = height/2 + w;
//> height = 2*(width - w);
int[] xs = new int[]{
(int) width,
(int) width,
(int) (width-(height-2*w)/2),
(int) width,
(int) width,
(int) width-w,
0,
(int) width-w};
int[] ys = new int[]{0, w, (int) height/2, (int) height-w, (int) height, (int) height, (int) height/2, 0};
for (int i=0; i<xs.length; i++) {
xs[i] += left;
ys[i] += top;
}
g2D.fillPolygon(xs, ys, xs.length);
}
private void greaterThan(Graphics2D g2D, double left, double top, double width, double height) {
int w = (int) (THICKNESS / Math.sqrt(2.0)); //?
width = height/2 + w;
//> height = 2*(width - w);
int[] xs = new int[]{0, 0, ((int) height-2*w)/2, 0, 0, w, (int) width, w};
int[] ys = new int[]{0, w, (int) height/2, (int) height-w, (int) height, (int) height, (int) height/2, 0};
for (int i=0; i<xs.length; i++) {
xs[i] += left;
ys[i] += top;
}
g2D.fillPolygon(xs, ys, xs.length);
}
private void drawArc(Graphics2D g2D, double x, double y, double radius, double angleDeg, double subtendDeg) {
Stroke old = g2D.getStroke();
float arcWidth = (float)THICKNESS/1.0f;
BasicStroke bracketStroke = new BasicStroke(arcWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
g2D.setStroke(bracketStroke);
Arc2D.Double arc = new Arc2D.Double();
arc.setArcByCenter(x, y, radius, angleDeg, subtendDeg, Arc2D.OPEN);
g2D.draw(arc);
g2D.setStroke(old);
}
private void drawOpenRoundBracket(Graphics2D g2D, double left, double top, double width, double height) {
double radius = height*height/8.0/width + width/2.0;
double u = height*height/8.0/width - width/2.0;
double angle = 180.0*Math.asin(height/2.0/radius)/Math.PI;
drawArc(g2D, left+u, top+height/2.0, radius, 180.0-angle, 2.0*angle);
}
private void drawCloseRoundBracket(Graphics2D g2D, double left, double top, double width, double height) {
double radius = height*height/8.0/width + width/2.0;
double u = height*height/8.0/width - width/2.0;
double angle = 180.0*Math.asin(height/2.0/radius)/Math.PI;
drawArc(g2D, left-u, top+height/2.0, radius, -angle, 2.0*angle);
}
private void oneEighty(GeneralPath path, double x1, double y1, double x2, double y2, double x3, double y3) {
ninty(path, x1, y1, x2, y2, x1, y2);
ninty(path, x2, y2, x3, y3, x3, y2);
}
private void ninty(GeneralPath path, double Ax, double Ay, double Bx, double By, double Cx, double Cy) {
//- double Ux = (Ax + Bx)/2.0;
//- double Uy = (Ay + By)/2.0;
double ACx = (Ax + Cx)/2.0;
double ACy = (Ay + Cy)/2.0;
double BCx = (Bx + Cx)/2.0;
double BCy = (By + Cy)/2.0;
path.curveTo(ACx, ACy, BCx, BCy, Bx, By);
}
public String description() {
switch (shape) {
case HORIZONTAL_LINE:
return "'horizontal line'";
case VERTICAL_LINE:
return "'vertical line'";
case NOTHING:
return "'nothing'";
case RADICAL:
return "'radical'";
case INTEGRAL:
return "'integral'";
case OPEN_SQUARE:
return "'['";
case CLOSE_SQUARE:
return "']'";
case OPEN_ROUND:
return "'('";
case CLOSE_ROUND:
return "')'";
case OPEN_PARENTHESIS:
return "{";
case CLOSE_PARENTHESIS:
return "}";
default:
throw new RuntimeException("unfinished");
}
}
@Override
public String toString() {
return "BoxGraphic:" + description() + super.toString() + " ";
}
// ===================
// === PARENTHESIS ===
// ===================
public Point calcCircle(int sign, Point A, Point B, int radius, int thickness) {
int Ax = (A.x + B.x)/2 + sign*thickness/2;
int s = radius - (B.x - Ax);
int v = (int) Math.sqrt(radius*radius - s*s);
//Ket.out.printf("s=%13.7g, v=%13.7g, r=%13.7g\n", 1.0*s, 1.0*v, 1.0*radius); //D
return new Point(B.x+s, B.y-v);
}
public Area generateCrescent(Point U, Point V, int radius) { // Remove g2D
Ellipse2D.Double a = new Ellipse2D.Double(U.x-radius, U.y-radius, 2*radius, 2*radius);
Ellipse2D.Double b = new Ellipse2D.Double(V.x-radius, V.y-radius, 2*radius, 2*radius);
Area areaA = new Area(a);
Area areaB = new Area(b);
areaA.subtract(areaB);
return areaA;
}
public void drawParenthesis(Graphics2D g2D, Point A, Point B, boolean open, int thickness) {
int radius = (2 * (B.x - A.x)) / 5; // 2/5 'th width is too simple.
int m = (A.y+B.y) / 2;
drawS(g2D, A, new Point(B.x, m), open, radius, thickness);
drawS(g2D, new Point(A.x, m), B, !open, radius, thickness);
}
private void swapX(Point a, Point b) {
int x = a.x;
a.x = b.x;
b.x = x;
}
private void drawS(Graphics2D g2D, Point A, Point B, boolean reversed, int radius, int thickness) {
Point U = calcCircle(-1, A, B, radius, thickness);
Point V = calcCircle(+1, A, B, radius, thickness);
Point W = new Point(B.x+A.x-U.x, B.y+A.y-U.y);
Point X = new Point(B.x+A.x-V.x, B.y+A.y-V.y);
if (reversed) {
swapX(U, W);
swapX(V, X);
}
int sign = reversed?+1:-1;
Point QU = new Point(U.x+sign*radius, U.y);
Point QV = new Point(V.x+sign*radius, V.y);
Point QW = new Point(B.x+A.x-QU.x, B.y+A.y-QU.y);
Point QX = new Point(B.x+A.x-QV.x, B.y+A.y-QV.y);
if (reversed) {
swapX(QU, QW);
swapX(QV, QX);
}
Area core = calcCore(QU, QV, QW, QX);
// (intersection (union core bottom top) mask)
core.add(generateCrescent(W, X, radius));
core.add(generateCrescent(U, V, radius));
mask(core, A, B, QU, QV, QW, QX, reversed);
g2D.fill(core);
}
private void mask(Area core, Point A, Point B, Point QU, Point QV, Point QW, Point QX, boolean reversed) {
int[] xs;
if (reversed) {
xs = new int[]{QU.x, A.x, A.x, QV.x, QV.x, QV.x, QW.x, B.x, B.x, QX.x, QX.x, QX.x};
} else {
xs = new int[]{QU.x, QU.x, QU.x, B.x, B.x, QV.x, QW.x, QW.x, QW.x, A.x, A.x, QX.x};
}
int[] ys = new int[]{QU.y, QU.y, B.y, B.y, QV.y, QV.y, QW.y, QW.y, A.y, A.y, QX.y, QX.y};
core.intersect(new Area(new Polygon(xs, ys, 12)));
}
private Area calcCore(Point QU, Point QV, Point QW, Point QX) {
int[] xs = new int[]{QU.x, QV.x, QW.x, QX.x};
int[] ys = new int[]{QU.y, QV.y, QW.y, QX.y};
return new Area(new Polygon(xs, ys, 4));
}
}