/*
* Copyright 2007 - 2017 the original author or authors.
*
* 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.sf.jailer.ui.graphical_view;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import javax.swing.ImageIcon;
import net.sf.jailer.datamodel.AggregationSchema;
import net.sf.jailer.datamodel.Association;
import net.sf.jailer.datamodel.Cardinality;
import prefuse.Constants;
import prefuse.render.EdgeRenderer;
import prefuse.util.GraphicsLib;
import prefuse.visual.EdgeItem;
import prefuse.visual.VisualItem;
/**
* Renderer for {@link Association}s.
*
* @author Ralf Wisser
*/
public class AssociationRenderer extends EdgeRenderer {
// color setting
public static Color COLOR_IGNORED;
public static Color COLOR_ASSOCIATION;
public static Color COLOR_DEPENDENCY;
public static Color COLOR_REVERSE_DEPENDENCY;
/**
* <code>true</code> for reversed rendering.
*/
boolean reversed;
/**
* <code>true</code> for full rendering (for setting bounds).
*/
boolean full = false;
/**
* Constructor.
*
* @param reversed <code>true</code> for reversed rendering
*/
public AssociationRenderer(boolean reversed) {
super(Constants.EDGE_TYPE_LINE, reversed? Constants.EDGE_ARROW_REVERSE : Constants.EDGE_ARROW_FORWARD);
this.reversed = reversed;
}
/**
* Constructor.
*/
public AssociationRenderer() {
full = true;
}
/**
* Temporary used in getRawShape.
*/
private Point2D m_isctPoints2[] = new Point2D[2];
private Point2D starPosition = null;
/**
* Return a non-transformed shape for the visual representation of the
* {@link Association}.
*
* @param item the VisualItem being drawn
* @return the "raw", untransformed shape
*/
@Override
protected Shape getRawShape(VisualItem item) {
EdgeItem edge = (EdgeItem)item;
VisualItem item1 = edge.getSourceItem();
VisualItem item2 = edge.getTargetItem();
int type = m_edgeType;
boolean reversedCurve = false;
Association association = (Association) item.get("association");
if (association != null && association.source == association.destination) {
type = Constants.EDGE_TYPE_CURVE;
reversedCurve = association.reversed;
}
getAlignedPoint(m_tmpPoints[0], item1.getBounds(),
m_xAlign1, m_yAlign1);
getAlignedPoint(m_tmpPoints[1], item2.getBounds(),
m_xAlign2, m_yAlign2);
m_curWidth = (float)(m_width * getLineWidth(item));
EdgeItem e = (EdgeItem)item;
boolean forward = (m_edgeArrow == Constants.EDGE_ARROW_FORWARD);
// get starting and ending edge endpoints
Point2D start = null, end = null;
start = m_tmpPoints[forward?0:1];
end = m_tmpPoints[forward?1:0];
if (!full) {
double midX;
double midY;
Point2D sp = start, ep = end;
VisualItem dest = forward ? e.getTargetItem() : e.getSourceItem();
int i = GraphicsLib.intersectLineRectangle(start, end,
dest.getBounds(), m_isctPoints);
if ( i > 0 ) ep = m_isctPoints[0];
VisualItem src = !forward ? e.getTargetItem() : e.getSourceItem();
i = GraphicsLib.intersectLineRectangle(start, end,
src.getBounds(), m_isctPoints2);
if ( i > 0 ) sp = m_isctPoints2[0];
midX = (sp.getX() + ep.getX()) / 2;
midY = (sp.getY() + ep.getY()) / 2;
m_tmpPoints[reversed? 1 : 0].setLocation(midX, midY);
}
// create the arrow head, if needed
if ( e.isDirected() && m_edgeArrow != Constants.EDGE_ARROW_NONE) {
if (type == Constants.EDGE_TYPE_CURVE) {
AffineTransform t = new AffineTransform();
t.setToRotation(Math.PI/4 * (reversedCurve? 1 : -1));
Point2D p = new Point2D.Double(), shift = new Point2D.Double();
double d = start.distance(end) / 5.0;
p.setLocation((end.getX() - start.getX()) / d, (end.getY() - start.getY()) / d);
t.transform(p, shift);
start.setLocation(start.getX() + shift.getX(), start.getY() + shift.getY());
end.setLocation(end.getX() + shift.getX(), end.getY() + shift.getY());
}
// compute the intersection with the target bounding box
VisualItem dest = forward ? e.getTargetItem() : e.getSourceItem();
int i = GraphicsLib.intersectLineRectangle(start, end,
dest.getBounds(), m_isctPoints);
if ( i > 0 ) end = m_isctPoints[0];
// create the arrow head shape
AffineTransform at = getArrowTrans(start, end, m_curWidth);
m_curArrow = at.createTransformedShape(m_arrowHead);
// update the endpoints for the edge shape
// need to bias this by arrow head size
if (type == Constants.EDGE_TYPE_CURVE) {
if (!"XML".equals(association.getDataModel().getExportModus()) || !isAggregation(association)) {
m_curArrow = null;
}
}
Point2D lineEnd = m_tmpPoints[forward?1:0];
lineEnd.setLocation(0, type == Constants.EDGE_TYPE_CURVE? 0 : -m_arrowHeight);
at.transform(lineEnd, lineEnd);
} else {
m_curArrow = null;
}
// create the edge shape
Shape shape = null;
double n1x = m_tmpPoints[0].getX();
double n1y = m_tmpPoints[0].getY();
double n2x = m_tmpPoints[1].getX();
double n2y = m_tmpPoints[1].getY();
m_line.setLine(n1x, n1y, n2x, n2y);
shape = m_line;
starBounds = null;
starPosition = null;
if (!forward && (Cardinality.MANY_TO_MANY.equals(association.getCardinality()) || Cardinality.MANY_TO_ONE.equals(association.getCardinality()))
|| forward && (Cardinality.MANY_TO_MANY.equals(association.getCardinality()) || Cardinality.ONE_TO_MANY.equals(association.getCardinality()))) {
starPosition = m_tmpPoints[forward? 1:0];
start = starPosition;
end = m_tmpPoints[forward? 0:1];
AffineTransform t = new AffineTransform();
t.setToRotation(-Math.PI/3);
Point2D p = new Point2D.Double(), shift = new Point2D.Double();
double d = m_tmpPoints[0].distance(m_tmpPoints[1]) / 9.0;
p.setLocation((end.getX() - start.getX()) / d, (end.getY() - start.getY()) / d);
t.transform(p, shift);
starPosition.setLocation(starPosition.getX() + shift.getX(), starPosition.getY() + shift.getY());
starBounds = new Rectangle2D.Double(starPosition.getX() - STAR_SIZE * (starWidth / 2), starPosition.getY() - STAR_SIZE * (starHeight / 2), starWidth * STAR_SIZE, starHeight * STAR_SIZE);
}
return shape;
}
/**
* Returns an affine transformation that maps the arrowhead shape
* to the position and orientation specified by the provided
* line segment end points.
*/
protected AffineTransform getArrowTrans(Point2D p1, Point2D p2,
double width)
{
m_arrowTrans.setToTranslation(p2.getX(), p2.getY());
m_arrowTrans.rotate(-HALF_PI +
Math.atan2(p2.getY()-p1.getY(), p2.getX()-p1.getX()));
if ( width > 1 ) {
double scalar = width/2;
m_arrowTrans.scale(scalar, scalar);
}
return m_arrowTrans;
}
/**
* Renders an {@link Association}.
*
* @param g the 2D graphics
* @param item visual item for the association
* @param isSelected <code>true</code> for selected association
*/
public void render(Graphics2D g, VisualItem item, boolean isSelected) {
Association association = (Association) item.get("association");
item.setSize(isSelected? 3 : 1);
int color;
if (!Boolean.TRUE.equals(item.get("full"))) {
if (!full) {
return;
}
color = associationColor(association);
} else {
if (full) {
return;
}
color = reversed? associationColor(association.reversalAssociation) : associationColor(association);
}
item.setFillColor(color);
item.setStrokeColor(color);
BasicStroke stroke = item.getStroke();
if (stroke != null) {
if (reversed) {
if (association != null) {
association = association.reversalAssociation;
}
}
if (association != null && association.isRestricted() && !association.isIgnored()) {
item.setStroke(new BasicStroke(stroke.getLineWidth(), stroke.getEndCap(), stroke.getLineJoin(), stroke.getMiterLimit(),
new float[] { 8f, 6f }, 1.0f));
} else {
item.setStroke(new BasicStroke(stroke.getLineWidth(), stroke.getEndCap(), stroke.getLineJoin(), stroke.getMiterLimit()));
}
}
if ("XML".equals(association.getDataModel().getExportModus())) {
m_arrowHead = updateArrowHead(m_arrowWidth, m_arrowHeight, association, isSelected);
arrowIsPotAggregation = true;
} else {
if (arrowIsPotAggregation) {
m_arrowHead = updateArrowHead(m_arrowWidth, m_arrowHeight);
}
arrowIsPotAggregation = false;
}
starPosition = null;
render(g, item);
if (starPosition != null && starImage != null) {
double size = STAR_SIZE;
transform.setTransform(size, 0, 0, size, starPosition.getX() - size * (starWidth / 2), starPosition.getY() - size * (starHeight / 2));
g.drawImage(starImage, transform, null);
starPosition = null;
}
}
/**
* @see prefuse.render.Renderer#setBounds(prefuse.visual.VisualItem)
*/
public void setBounds(VisualItem item) {
super.setBounds(item);
if (starBounds != null ) {
Rectangle2D bbox = (Rectangle2D)item.get(VisualItem.BOUNDS);
Rectangle2D.union(bbox, starBounds, bbox);
}
}
private boolean arrowIsPotAggregation = false;
private AffineTransform transform = new AffineTransform();
private Rectangle2D starBounds = null;
/**
* Gets color for association.
*
* @param association the association
* @return the color for the association
*/
private int associationColor(Association association) {
if (association.isIgnored()) {
return COLOR_IGNORED.getRGB();
}
if (association.isInsertDestinationBeforeSource()) {
return COLOR_DEPENDENCY.getRGB();
}
if (association.isInsertSourceBeforeDestination()) {
return COLOR_REVERSE_DEPENDENCY.getRGB();
}
return COLOR_ASSOCIATION.getRGB();
}
/**
* Returns true if the Point is located inside the extents of the item.
* This calculation matches against the exact item shape, and so is more
* sensitive than just checking within a bounding box.
*
* @param p the point to test for containment
* @param item the item to test containment against
* @return true if the point is contained within the the item, else false
*/
@Override
public boolean locatePoint(Point2D p, VisualItem item) {
Shape s = getShape(item);
if ( s == null ) {
return false;
} else {
double width = Math.max(14, getLineWidth(item));
double halfWidth = width/2.0;
return s.intersects(p.getX()-halfWidth,
p.getY()-halfWidth,
width,width);
}
}
/**
* Render aggregation symbols.
*/
protected Polygon updateArrowHead(int w, int h, Association association, boolean isSelected) {
if (isAggregation(association)) {
if ( m_arrowHead == null ) {
m_arrowHead = new Polygon();
} else {
m_arrowHead.reset();
}
double ws = 0.9;
double hs = 2.0/3.0;
if (isSelected) {
ws /= 1.3;
hs /= 1.3;
}
m_arrowHead.addPoint(0, 0);
m_arrowHead.addPoint((int) (ws*-w), (int) (hs*(-h)));
m_arrowHead.addPoint( 0, (int) (hs*(-2*h)));
m_arrowHead.addPoint((int) (ws*w), (int) (hs*(-h)));
m_arrowHead.addPoint(0, 0);
return m_arrowHead;
} else {
return updateArrowHead(w, h);
}
}
/**
* Checks whether association must be rendered as aggregation.
*
* @param association the association to check
* @return <code>true</code> if association must be rendered as aggregation
*/
private boolean isAggregation(Association association) {
return association.reversalAssociation.getAggregationSchema() != AggregationSchema.NONE;
}
private Image starImage = null;
private double starWidth = 0;
private double starHeight = 0;
private final double STAR_SIZE = 0.22;
{
// load images
try {
String dir = "/net/sf/jailer/ui/resource";
starImage = new ImageIcon(getClass().getResource(dir + "/star.png")).getImage();
starWidth = starImage.getWidth(null);
starHeight = starImage.getHeight(null);
} catch (Exception e) {
e.printStackTrace();
}
}
}