/*
* #!
* Ontopia Vizigator
* #-
* Copyright (C) 2001 - 2013 The Ontopia Project
* #-
* 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 net.ontopia.topicmaps.viz;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.GeneralPath;
import java.awt.geom.QuadCurve2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.swing.Icon;
import net.ontopia.topicmaps.core.TopicIF;
import net.ontopia.utils.StringifierIF;
import com.touchgraph.graphlayout.Edge;
import com.touchgraph.graphlayout.Node;
import com.touchgraph.graphlayout.TGPaintListener;
import com.touchgraph.graphlayout.TGPanel;
/**
* INTERNAL: Common abstract superclass for all edges representing
* Topic Maps constructs.
*/
public abstract class TMAbstractEdge extends Edge
implements VizTMObjectIF, TGPaintListener {
/**
* setVisible is not supported in Vizigator.
*/
public final void setVisible(boolean visible) {
}
public TMAbstractEdge(Node f, Node t) {
super(f, t);
super.setVisible(true);
}
protected int lineWeight = TMRoleEdge.DEFAULT_LINE_WEIGHT;
protected int shape = TMRoleEdge.DEFAULT_SHAPE;
protected Icon icon;
protected Font font;
protected StringifierIF stringifier;
protected boolean underMouse = false;
static protected final int LOADING = 50;
protected double calculateMidPointBetween(double from, double to) {
return (from + to) / 2;
}
/**
* Returns the mid point between the two assocaition targets.
*/
public Point getMidPoint() {
int index = getIndexInParents();
if (index == 0) return getSimpleMidPoint();
return getMiddleOf(getCurvedLine(index));
}
protected Point getFromRolePosition() {
int index = getIndexInParents();
// Optimize out the general case
if (index == 0)
return getMidPointBetween(from, getMidPoint());
return getMiddleOf(getFromCurve(index));
}
private Point getMidPointBetween(Node node, Point midPoint) {
return new Point(((int)calculateMidPointBetween(node.drawx, midPoint.getX())),
((int)calculateMidPointBetween(node.drawy, midPoint.getY())));
}
private QuadCurve2D getToCurve(int index) {
QuadCurve2D curve = getCurvedLine(index);
QuadCurve2D result = new QuadCurve2D.Double();
curve.subdivide(null,result);
return result;
}
private QuadCurve2D getFromCurve(int index) {
QuadCurve2D curve = getCurvedLine(index);
QuadCurve2D result = new QuadCurve2D.Double();
curve.subdivide(result,null);
return result;
}
protected Point getToRolePosition() {
int index = getIndexInParents();
// Optimize out the general case
if(index == 0)
return getMidPointBetween(to, getMidPoint());
return getMiddleOf(getToCurve(index));
}
private Point getMiddleOf(QuadCurve2D curve) {
QuadCurve2D result = new QuadCurve2D.Double();
curve.subdivide(result,null);
return new Point((int)result.getP2().getX(), (int)result.getP2().getY());
}
private Point getSimpleMidPoint() {
double midX = this.calculateMidPointBetween(this.from.drawx, this.to.drawx);
double midY = this.calculateMidPointBetween(this.from.drawy, this.to.drawy);
return new Point((int) midX, (int) midY);
}
public void paintAfterEdges(Graphics g) {
// Currently do nothing
}
protected void paintConnection(Graphics g) {
// Optimize out the "normal" case where index = 0
int index = getIndexInParents();
Graphics2D g2D = (Graphics2D)g;
if (index == 0) {
switch (shape) {
case TMRoleEdge.SHAPE_BOWTIE:
this.paintBowTie(g2D);
break;
case TMRoleEdge.SHAPE_LINE:
this.paintLine(g2D);
break;
}}
else {
switch (shape) {
case TMRoleEdge.SHAPE_BOWTIE:
this.paintCurvedBowTie(g2D, index);
break;
case TMRoleEdge.SHAPE_LINE:
this.paintCurvedLine(g2D, index);
break;
}
}
}
protected void paintLine(Graphics2D g) {
g.setColor(this.getColor());
Stroke old = g.getStroke();
g.setStroke(new BasicStroke(lineWeight));
g.drawLine((int) from.drawx, (int) from.drawy, (int) to.drawx,
(int) to.drawy);
g.setStroke(old);
}
protected void paintCurvedLine(Graphics2D g, int index) {
g.setColor(this.getColor());
Stroke old = g.getStroke();
g.setStroke(new BasicStroke(lineWeight));
g.draw(getCurvedLine(index));
g.setStroke(old);
}
protected QuadCurve2D getCurvedLine(int index) {
double x1 = from.drawx;
double x2 = to.drawx;
double y1 = from.drawy;
double y2 = to.drawy;
double midx = calculateMidPointBetween(x1, x2);
double midy = calculateMidPointBetween(y1, y2);
int weight = index / 2;
if (index % 2 == 1) {
weight++;
weight = -weight;
}
Dimension offset = calculateOffset(x1, x2, y1, y2, LOADING * weight);
QuadCurve2D curve = new QuadCurve2D.Double(x1, y1,
midx-offset.width, midy+offset.height,
x2, y2);
return curve;
}
protected void paintCurvedBowTie(Graphics2D g, int index) {
g.setColor(this.getColor());
Shape path = getCurvedBowTie(index);
g.draw(path);
g.fill(path);
}
protected GeneralPath getCurvedBowTie(int index) {
double x1 = from.drawx;
double x2 = to.drawx;
double y1 = from.drawy;
double y2 = to.drawy;
double midx = calculateMidPointBetween(x1, x2);
double midy = calculateMidPointBetween(y1, y2);
int weight = index / 2;
if (index % 2 == 1) {
weight++;
weight = -weight;
}
Dimension offset = calculateOffset(x1, x2, y1, y2, LOADING * weight);
Dimension fromExtra = calculateOffset(x1, (int)midx - offset.width, y1, (int)midy + offset.height, getLineWeight());
Dimension toExtra = calculateOffset(x2, (int)midx - offset.width, y2, (int)midy + offset.height, getLineWeight());
GeneralPath path = new GeneralPath(GeneralPath.WIND_NON_ZERO);
path.moveTo((int)x1-fromExtra.width, (int)y1+fromExtra.height);
path.lineTo((int)x1+fromExtra.width, (int)y1-fromExtra.height);
path.quadTo((float)midx-offset.width, (float)midy+offset.height, (float)x2-toExtra.width, (float)y2+toExtra.height);
path.lineTo((int)x2+toExtra.width, (int)y2-toExtra.height);
path.quadTo((float)midx-offset.width, (float)midy+offset.height, (float)x1-fromExtra.width, (float)y1+fromExtra.height);
return path;
}
protected int getIndexInParents() {
ArrayList possible = new ArrayList();
for (Iterator iter = from.getEdges(); iter.hasNext();)
possible.add(iter.next());
if (possible.size() > intBuffer.length) intBuffer = new int[possible.size()];
int index = 0;
for (Iterator iter = to.getEdges(); iter.hasNext();) {
TMAbstractEdge edge = (TMAbstractEdge) iter.next();
if (possible.contains(edge)) {
intBuffer[index] = System.identityHashCode(edge);
index++;
}
}
int[] result = new int[index];
System.arraycopy(intBuffer, 0, result, 0, index);
Arrays.sort(result);
int offset = Arrays.binarySearch(result, System.identityHashCode(this));
// If the total size is even, increase the index.
if (result.length % 2 == 0) offset++;
return offset;
}
protected void paintBowTie(Graphics2D g) {
double x1 = from.drawx;
double x2 = to.drawx;
double y1 = from.drawy;
double y2 = to.drawy;
double midx = calculateMidPointBetween(x1, x2);
double midy = calculateMidPointBetween(y1, y2);
Dimension offset = calculateOffset(x1, x2, y1, y2, getLineWeight());
g.setColor(getColor());
int xPoints[] = new int[3];
xPoints[0] = (int) midx;
int yPoints[] = new int[3];
yPoints[0] = (int) midy;
xPoints[1] = (int) (x1 - offset.width);
yPoints[1] = (int) (y1 + offset.height);
xPoints[2] = (int) (x1 + offset.width);
yPoints[2] = (int) (y1 - offset.height);
g.fillPolygon(xPoints, yPoints, 3);
xPoints[1] = (int) (x2 - offset.width);
yPoints[1] = (int) (y2 + offset.height);
xPoints[2] = (int) (x2 + offset.width);
yPoints[2] = (int) (y2 - offset.height);
g.fillPolygon(xPoints, yPoints, 3);
g.drawLine((int) x1, (int) y1, (int) x2, (int) y2);
}
protected Dimension calculateOffset(double x1, double x2, double y1, double y2,
int weight) {
double beta = calculateBeta(x1, x2, y1, y2);
return new Dimension((int) (weight * Math.cos(beta)),
(int) (weight * Math.sin(beta)));
}
private double calculateBeta(double x1, double x2, double y1, double y2) {
double deltaX = (x2 - x1);
double deltaY = (y2 - y1);
double alpha = 0;
if (deltaY != 0 && deltaX != 0) alpha = Math.atan(deltaY / deltaX);
double beta = (Math.PI / 2) - alpha;
return beta;
}
public void paintLast(Graphics g) {
if (underMouse) this.paintToolTip(g);
}
protected void paintToolTip(Graphics g) {
// Default is to do nothing
}
public void setLineWeight(int lineWeight) {
this.lineWeight = lineWeight;
}
public int getShape() {
return this.shape;
}
public void setShape(int shape) {
this.shape = shape;
}
public Icon getIcon() {
return this.icon;
}
public void setIcon(Icon icon) {
this.icon = icon;
}
public Font getFont() {
return this.font;
}
public void setFont(Font font) {
this.font = font;
}
public void addTo(TGPanel tgpanel) {
tgpanel.addEdge(this);
}
public int getLineWeight() {
return this.lineWeight;
}
public void paintFirst(Graphics g) {
// Currently do nothing
}
/**
* @param g -
* The graphic context for the drawing operation.
* @param string -
* The String to be rendered.
* @param x -
* The x coordinate where the String should be positioned.
* @param y -
* The y coordinate where the String should be positioned. NOTE: The text is <b>centered </b> over the given coordinates.
*/
protected void paintToolTipText(Graphics g, String string, int x, int y) {
g.setFont(this.getFont());
FontMetrics fontMetrics = g.getFontMetrics();
int a = fontMetrics.getAscent();
int h = a + fontMetrics.getDescent();
int w = fontMetrics.stringWidth(string);
int xPosition = x - (w / 2);
int yPosition = y - (h / 2);
// Draw the background
Color c = this.getColor();
g.setColor(c);
int r = h / 2;
int vPad = h / 8;
int hPad = h / 4;
g.fillRoundRect(xPosition - hPad, yPosition - vPad, w + (2 * hPad), h
+ (2 * vPad), r, r);
//Draw a defined edge to the popup
g.setColor(TMTopicNode.textColourForBackground(c));
g.drawRoundRect(xPosition - hPad, yPosition - vPad, w + (2 * hPad), h
+ (2 * vPad), r, r);
// Draw the text
g.drawString(string, xPosition, yPosition + a);
}
public static final int SHAPE_BOWTIE = 1;
public static final int SHAPE_LINE = 2;
public static int DEFAULT_SHAPE = SHAPE_BOWTIE;
public static int DEFAULT_LINE_WEIGHT = 4;
protected static int[] intBuffer = new int[64];
protected boolean shouldDisplayRoleHoverHelp;
protected TopicIF scopingTopic;
public void paint(Graphics g, TGPanel tgPanel) {
underMouse = (tgPanel.getMouseOverE() == this);
if (intersects(tgPanel.getSize())) paintConnection(g);
if (icon != null) {
Point mid = this.getMidPoint();
icon.paintIcon(tgPanel, g, mid.x - (icon.getIconHeight() / 2), mid.y
- (icon.getIconHeight() / 2));
}
}
public void deleteFrom(TGPanel tgpanel) {
tgpanel.deleteEdge(this);
}
protected void paintTypeToolTip(Graphics g) {
this.paintToolTipText(g, this.getMainHoverHelpText(), getMidPoint());
}
protected void paintToolTipText(Graphics g, String text, Point aPoint) {
paintToolTipText(g, text, aPoint.x, aPoint.y);
}
protected String getMainHoverHelpText() {
return Messages.getString("Viz.Unknown");
}
public void setShouldDisplayRoleHoverHelp(boolean newValue) {
shouldDisplayRoleHoverHelp = newValue;
}
public boolean isEdge() {
return true;
}
public boolean isAssociation() {
return false;
}
public List getTargetsFrom(Node find) {
return Collections.singletonList(this.getOtherEndpt(find));
}
public StringifierIF getStringifier() {
return this.stringifier;
}
public void setScopingTopic(TopicIF aTopic) {
this.scopingTopic = aTopic;
this.setStringifier(VizUtils.stringifierFor(aTopic));
}
protected void setStringifier(StringifierIF aStringifier) {
this.stringifier = aStringifier;
}
public TopicIF getTopicMapType() {
return null;
}
protected Shape getDisplayShape() {
int index = getIndexInParents();
// Optimize out the "normal" case where index = 0
if (index != 0){
switch (shape) {
case TMRoleEdge.SHAPE_BOWTIE:
return this.getCurvedBowTie(index);
case TMRoleEdge.SHAPE_LINE:
return this.getCurvedLine(index);
}}
return null;
}
public double distFromPoint(double x, double y) {
Shape shape = getDisplayShape();
if (shape == null)
return super.distFromPoint(x, y);
// Bit of a hack, but just because TG does something rather
// stupid here anyway.
if (shape.intersects(x -2, y-2, 4, 4))
return 0;
return 1000;
}
}