/*******************************************************************************
* Copyright (c) 2012-present Jakub Kováč, Jozef Brandýs, Katarína Kotrlová,
* Pavol Lukča, Ladislav Pápay, Viktor Tomkovič, Tatiana Tóthová
*
* 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 algvis.ui.view;
import algvis.core.Node;
import algvis.ui.Fonts;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.Stroke;
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.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.text.AttributedString;
import java.util.ArrayList;
import javax.swing.JPanel;
public class View implements MouseListener, MouseMotionListener,
MouseWheelListener {
private Graphics2D g;
private final static double SCALE_FACTOR = 1.1;
private final static double MIN_ZOOM = 0.16;
private final static double MAX_ZOOM = 5.5;
private int W;
private int H; // display width&height
private int minx;
public int miny;
private int maxx;
private int maxy;
private int mouseX, mouseY; // mouse position
public Alignment align = Alignment.CENTER;
public final AffineTransform at;
private AffineTransform oldTransform;
private ClickListener D;
public View(JPanel P) {
P.addMouseListener(this);
P.addMouseMotionListener(this);
P.addMouseWheelListener(this);
at = new AffineTransform();
setBounds(0, 0, 0, 0);
}
public void setGraphics(Graphics2D g, int W, int H) {
this.g = g;
this.W = W;
this.H = H;
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
resetView();
}
public Graphics2D getGraphics() {
return g;
}
public void resetView() {
at.setToIdentity();
double f = 1, f2 = 1;
if ((maxx - minx) > W || (maxy - miny) > H) {
f2 = Math.min(W / (double) (maxx - minx), H
/ (double) (maxy - miny));
f = Math.max(f2, MIN_ZOOM);
}
if (align == Alignment.CENTER) {
if (f2 < f) {
at.translate(W / 2, 0);
at.scale(f, f);
at.translate(-(maxx + minx) / 2, -miny);
} else if (-f * minx >= W / 1.99999) {
at.scale(f, f);
at.translate(-minx, -miny);
} else if (f * maxx >= W / 1.99999) {
at.translate(W, 0);
at.scale(f, f);
at.translate(-maxx, -miny);
} else {
at.translate(W / 2, 0);
at.scale(f, f);
at.translate(0, -miny);
}
} else if (align == Alignment.LEFT) {
at.scale(f, f);
at.translate(-minx, -miny);
}
}
public void setBounds(int minx, int miny, int maxx, int maxy) {
this.minx = minx - 50;
this.miny = miny - 50;
this.maxx = maxx + 50;
this.maxy = maxy + 50;
}
void zoom(int x, int y, double f) {
if (at.getScaleX() * f <= MAX_ZOOM) {
final AffineTransform t = new AffineTransform();
t.translate(x, y);
t.scale(f, f);
t.translate(-x, -y);
at.preConcatenate(t);
}
}
public void zoomIn() {
zoom(W / 2, H / 2, SCALE_FACTOR);
}
void zoomIn(int x, int y) {
zoom(x, y, SCALE_FACTOR);
}
void zoomOut(int x, int y) {
zoom(x, y, 1 / SCALE_FACTOR);
}
public void zoomOut() {
zoom(W / 2, H / 2, 1 / SCALE_FACTOR);
}
Point2D v2r(double x, double y) {
final Point2D p = new Point2D.Double(x, y);
at.transform(p, p);
return p;
}
public Point2D r2v(double x, double y) {
final Point2D p = new Point2D.Double(x, y);
try {
at.inverseTransform(p, p);
} catch (final NoninvertibleTransformException exc) {
exc.printStackTrace();
}
return p;
}
@Override
public void mousePressed(MouseEvent e) {
mouseX = e.getX();
mouseY = e.getY();
}
@Override
public void mouseDragged(MouseEvent e) {
final int x = e.getX(), y = e.getY();
at.preConcatenate(AffineTransform.getTranslateInstance(x - mouseX, y
- mouseY));
mouseX = x;
mouseY = y;
}
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mouseMoved(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
@Override
public void mouseClicked(MouseEvent e) {
final Point2D p = r2v(e.getX(), e.getY());
if (D != null) {
D.mouseClicked((int) p.getX(), (int) p.getY());
}
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
final int notches = e.getWheelRotation();
if (notches > 0) {
zoomOut(e.getX(), e.getY());
} else {
zoomIn(e.getX(), e.getY());
}
}
public boolean inside(int x, int y) {
final Point2D p = v2r(x, y);
return (0 <= p.getX()) && (p.getX() <= W) && (0 <= p.getY())
&& (p.getY() <= H);
}
public void setColor(Color c) {
g.setColor(c);
}
public void startDrawing() {
oldTransform = g.getTransform();
g.transform(at);
}
public void endDrawing() {
g.setTransform(oldTransform);
}
public Point2D cut(double x, double y, double x2, double y2, double c) {
final double d = new Point2D.Double(x, y).distance(x2, y2);
return new Point2D.Double(x + (x2 - x) * (d - c) / d, y + (y2 - y)
* (d - c) / d);
}
public void fillRect(double x, double y, double a, double b) {
g.fillRect((int) (x - a), (int) (y - b), (int) (2 * a), (int) (2 * b));
}
void drawRect(double x, double y, double a, double b) {
g.drawRect((int) (x - a), (int) (y - b), (int) (2 * a), (int) (2 * b));
}
public void fillSqr(double x, double y, double a) {
fillRect(x, y, a, a);
}
public void drawSqr(double x, double y, double a) {
drawRect(x, y, a, a);
}
public void fillOval(double x, double y, double a, double b) {
g.fillOval((int) x, (int) y, (int) a, (int) b);
}
public void drawOval(double x, double y, double a, double b) {
g.drawOval((int) x, (int) y, (int) a, (int) b);
}
public void fillCircle(double x, double y, double r) {
g.fillOval((int) (x - r), (int) (y - r), 2 * (int) r, 2 * (int) r);
}
public void drawCircle(double x, double y, double r) {
g.drawOval((int) (x - r), (int) (y - r), 2 * (int) r, 2 * (int) r);
}
public void drawLine(double x1, double y1, double x2, double y2) {
g.drawLine((int) x1, (int) y1, (int) x2, (int) y2);
}
public void drawLine(double x1, double y1, double x2, double y2,
float width, Color col) {
final Stroke old = g.getStroke(), wide = new BasicStroke(width,
BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
final Color c = g.getColor();
g.setColor(col);
g.setStroke(wide);
g.drawLine((int) x1, (int) y1, (int) x2, (int) y2);
g.setStroke(old);
g.setColor(c);
}
public void drawWideLine(double x1, double y1, double x2, double y2,
float width) {
drawLine(x1, y1, x2, y2, width, new Color(230, 230, 230));
}
public void drawWideLine(double x1, double y1, double x2, double y2) {
drawLine(x1, y1, x2, y2, 27.0f, new Color(230, 230, 230));
}
public void drawDashedLine(double x1, double y1, double x2, double y2,
float width, Color col) {
final float dash1[] = {
2.0f, 5.0f
};
final Stroke old = g.getStroke(), dashed = new BasicStroke(width,
BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f);
final Color c = g.getColor();
g.setColor(col);
g.setStroke(dashed);
g.drawLine((int) x1, (int) y1, (int) x2, (int) y2);
g.setStroke(old);
g.setColor(c);
}
public void drawDashedLine(double x1, double y1, double x2, double y2) {
drawDashedLine(x1, y1, x2, y2, 1.0f, Color.BLACK);
}
// square; x, y is the center
public void drawSquare(double x, double y, double a) {
g.drawRect((int) (x - a), (int) (y - a), 2 * (int) a, 2 * (int) a);
}
public int stringWidth(String str, Fonts f) {
return f.fm.stringWidth(str);
}
/**
* draw string horizontally; middle character is at (x, y)
*/
public void drawString(String str, double x, double y, Fonts f) {
x -= f.fm.stringWidth(str) / 2;
y -= f.fm.getHeight() / 2 - f.fm.getAscent();
g.setFont(f.font);
g.drawString(str, (int) x, (int) y);
}
/**
* draw string horizontally; last character is at (x, y)
*/
public void drawStringLeft(String str, double x, double y, Fonts f) {
x -= f.fm.stringWidth(str);
y -= f.fm.getHeight() / 2 - f.fm.getAscent();
g.setFont(f.font);
g.drawString(str, (int) x, (int) y);
}
/**
* draw string horizontally; first character is at (x, y)
*/
public void drawStringRight(String str, double x, double y, Fonts f) {
y -= f.fm.getHeight() / 2 - f.fm.getAscent();
g.setFont(f.font);
g.drawString(str, (int) x, (int) y);
}
public void drawStringTop(String str, double x, double y, Fonts f) {
x -= f.fm.stringWidth(str) / 2;
y -= f.fm.getHeight();
g.setFont(f.font);
g.drawString(str, (int) x, (int) y);
}
public void drawVerticalString(String str, double x, double y, Fonts f) {
final int xx = (int) x;
int yy = (int) y - str.length() * f.fm.getHeight() / 2;
g.setFont(f.font);
for (int i = 0; i < str.length(); ++i) {
g.drawString("" + str.charAt(i), xx, yy);
yy += f.fm.getHeight();
}
}
public void fillArc(double x, double y, double w, double h, double a1,
double a2) {
g.fillArc((int) x, (int) y, (int) w, (int) h, (int) a1, (int) a2);
}
public void drawRectangle(Rectangle2D r) {
g.draw(r);
}
public void drawRoundRectangle(double x, double y, double w, double h,
double arcw, double arch) {
g.draw(new RoundRectangle2D.Double(x - w, y - h, 2 * w, 2 * h, arcw,
arch));
}
public void fillRoundRectangle(double x, double y, double w, double h,
double arcw, double arch) {
g.fill(new RoundRectangle2D.Double(x - w, y - h, 2 * w, 2 * h, arcw,
arch));
}
public void drawTextBubble(String s, int x, int y, int w, int alpha, REL pos) {
drawTextBubble(s, x, y, w, alpha, pos, 2 * Node.RADIUS, 2 * Node.RADIUS);
}
public void drawTextBubble(String s, int x, int y, int w, int alpha,
REL pos, int gapx, int gapy) {
FontRenderContext frc = g.getFontRenderContext();
LineBreakMeasurer measurer = new LineBreakMeasurer(
new AttributedString(s).getIterator(), frc);
ArrayList<TextLayout> L = new ArrayList<TextLayout>();
double h = 0;
while (measurer.getPosition() < s.length()) {
TextLayout l = measurer.nextLayout(w);
L.add(l);
h += l.getAscent() + l.getDescent(); // + l.getLeading() usually = 0
}
if (L.size() == 1) {
w = (int) L.get(0).getBounds().getWidth();
}
g.setColor(new Color(((alpha << 24) | 0xfffeed), true));
if (pos == REL.TOPLEFT || pos == REL.LEFT || pos == REL.BOTTOMLEFT) {
x -= w + gapx;
} else if (pos == REL.TOP || pos == REL.CENTER || pos == REL.BOTTOM) {
x -= w / 2;
} else if (pos == REL.TOPRIGHT || pos == REL.RIGHT
|| pos == REL.BOTTOMRIGHT) {
x += gapx;
}
if (pos == REL.TOPLEFT || pos == REL.TOP || pos == REL.TOPRIGHT) {
y -= h + gapy;
} else if (pos == REL.LEFT || pos == REL.CENTER || pos == REL.RIGHT) {
y -= h / 2;
} else if (pos == REL.BOTTOMLEFT || pos == REL.BOTTOM
|| pos == REL.BOTTOMRIGHT) {
y += gapy;
}
g.fill(new RoundRectangle2D.Double(x - 5, y - 5, w + 10, h + 10, 10, 10));
g.setColor(new Color((alpha << 24), true));
g.draw(new RoundRectangle2D.Double(x - 5, y - 5, w + 10, h + 10, 15, 15));
for (TextLayout l : L) {
y += l.getAscent();
l.draw(g, x, y);
y += l.getDescent() + l.getLeading();
}
}
private void arrowHead(double x, double y, double xx, double yy,
double scale) {
final double alpha = 6.0, beta = 1.5;
double vecX, vecY, normX, normY, d, th, ta, baseX, baseY;
// build the line vector
vecX = xx - x;
vecY = yy - y;
// build the arrow base vector - normal to the line
normX = -vecY;
normY = vecX;
// setup length parameters
d = Math.sqrt(vecX * vecX + vecY * vecY);
th = scale * alpha / (2.0 * d);
ta = scale * alpha / (beta * d);
// find the base of the arrow
baseX = xx - ta * vecX;
baseY = yy - ta * vecY;
// build the points on the sides of the arrow
final Path2D p = new Path2D.Double();
p.moveTo(xx + vecX * 2 / d, yy + vecY * 2 / d);
p.lineTo(baseX + th * normX, baseY + th * normY);
p.lineTo(baseX - th * normX, baseY - th * normY);
p.closePath();
g.fill(p);
}
public void drawArrow(double x1, double y1, double x2, double y2) {
g.drawLine((int) x1, (int) y1, (int) x2, (int) y2);
arrowHead((int) x1, (int) y1, (int) x2, (int) y2, 1);
}
public void drawArrow(double x1, double y1, double x2, double y2,
float width, Color col) {
drawLine(x1, y1, x2, y2, width, col);
final Color c = g.getColor();
g.setColor(col);
arrowHead((int) x1, (int) y1, (int) x2, (int) y2, width);
g.setColor(c);
}
public void drawDoubleArrow(double x1, double y1, double x2, double y2) {
g.drawLine((int) x1, (int) y1, (int) x2, (int) y2);
arrowHead((int) x1, (int) y1, (int) x2, (int) y2, 1);
arrowHead((int) x2, (int) y2, (int) x1, (int) y1, 1);
}
// elliptical arc
// x,y,w,h is the bounding rectangle
// a1,a2 is the starting and ending angle in degrees
void drawArc(double x, double y, double w, double h, double a1, double a2) {
g.drawArc((int) x, (int) y, (int) w, (int) h, (int) a1, (int) (a2 - a1));
}
// let A=[x1,y1] and B=[x2,y2]
// _B--\___/--B
// _____\_/
// ______|
// ______A
// ______|
// _____/_\
// _B--/___\--B
//
public void drawQuarterArc(double x1, double y1, double x2, double y2) {
final double w = Math.abs(x1 - x2), h = Math.abs(y1 - y2);
double a1, a2;
if (y2 < y1) {
if (x2 < x1) {
a1 = 0;
a2 = 90;
} else {
a1 = 90;
a2 = 180;
}
} else {
if (x2 > x1) {
a1 = 180;
a2 = 270;
} else {
a1 = 270;
a2 = 360;
}
}
drawArc(x2 - w, y1 - h, 2 * w, 2 * h, a1, a2);
}
public void drawFancyArc(double x1, double y1, double x3, double y3) {
g.draw(new CubicCurve2D.Double(x1, y1, x1, y1 + 10, x3, y3 - 40, x3, y3));
}
public void drawArcArrow(double x, double y, double w, double h, double a1,
double a2) {
drawArc(x, y, w, h, a1, a2);
final double a = a2 * Math.PI / 180;
final double x2 = x + w / 2.0 * (1 + Math.cos(a)), y2 = y + h / 2.0
* (1 - Math.sin(a));
final double dx = 128 * Math.sin(a), dy = 128 * Math.cos(a);
if (a1 <= a2) {
x = x2 + dx;
y = y2 + dy;
} else {
x = x2 - dx;
y = y2 - dy;
}
arrowHead(x, y, x2, y2, 1);
}
public void drawCurve(double x1, double y1, double cx1, double cy1,
double cx2, double cy2, double x2, double y2) {
g.draw(new CubicCurve2D.Double(x1, y1, cx1, cy1, cx2, cy2, x2, y2));
}
public void drawCurve(double x1, double y1, double cx1, double cy1,
double cx2, double cy2, double x2, double y2, float width, Color col) {
final Color c = g.getColor();
final Stroke old = g.getStroke(), wide = new BasicStroke(width,
BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
g.setStroke(wide);
g.setColor(col);
g.draw(new CubicCurve2D.Double(x1, y1, cx1, cy1, cx2, cy2, x2, y2));
g.setColor(c);
g.setStroke(old);
}
public void drawCurveArrow(double x1, double y1, double cx1, double cy1,
double cx2, double cy2, double x2, double y2, float width, Color col) {
final Color c = g.getColor();
final Stroke old = g.getStroke(), wide = new BasicStroke(width,
BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
g.setStroke(wide);
g.setColor(col);
g.draw(new CubicCurve2D.Double(x1, y1, cx1, cy1, cx2, cy2, x2, y2));
arrowHead(cx2, cy2, x2, y2, width);
g.setColor(c);
g.setStroke(old);
}
public void fillPolygon(Polygon p) {
final Stroke old = g.getStroke(), wide = new BasicStroke(27.0f,
BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
final Color c = g.getColor();
g.setColor(new Color(230, 230, 230));
g.setStroke(wide);
g.fillPolygon(p);
g.drawPolygon(p);
g.setStroke(old);
g.setColor(c);
}
public void drawImage(Image img, double x, double y, double w, double h) {
g.drawImage(img, (int) x, (int) y, (int) w, (int) h, null);
}
public void setDS(ClickListener D) {
this.D = D;
}
}