com.mxgraph.shape.mxCurveLabelShape Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jgraphx Show documentation
Show all versions of jgraphx Show documentation
JGraphX Swing Component - Java Graph Visualization Library
This is a binary & source redistribution of the original, unmodified JGraphX library originating from:
"https://github.com/jgraph/jgraphx/archive/v3.4.1.3.zip".
The purpose of this redistribution is to make the library available to other Maven projects.
/**
* $Id: mxCurveLabelShape.java,v 1.32 2011/06/10 12:11:27 david Exp $
* Copyright (c) 2010, David Benson, Gaudenz Alder
*/
package com.mxgraph.shape;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.text.Bidi;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import com.mxgraph.canvas.mxGraphics2DCanvas;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxCurve;
import com.mxgraph.util.mxLine;
import com.mxgraph.util.mxPoint;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.util.mxUtils;
import com.mxgraph.view.mxCellState;
/**
* Draws the edge label along a curve derived from the curve describing
* the edge's path
*/
public class mxCurveLabelShape implements mxITextShape
{
/**
* Cache of the label text
*/
protected String lastValue;
/**
* Cache of the label font
*/
protected Font lastFont;
/**
* Cache of the last set of guide points that this label was calculated for
*/
protected List lastPoints;
/**
* Cache of the points between which drawing straight lines views as a
* curve
*/
protected mxCurve curve;
/**
* Cache the state associated with this shape
*/
protected mxCellState state;
/**
* Cache of information describing characteristics relating to drawing
* each glyph of this label
*/
protected LabelGlyphCache[] labelGlyphs;
/**
* Cache of the total length of the branch label
*/
protected double labelSize;
/**
* Cache of the bounds of the label
*/
protected mxRectangle labelBounds;
/**
* ADT to encapsulate label positioning information
*/
protected LabelPosition labelPosition = new LabelPosition();
/**
* Buffer at both ends of the label
*/
public static double LABEL_BUFFER = 30;
/**
* Factor by which text on the inside of curve is stretched
*/
public static double CURVE_TEXT_STRETCH_FACTOR = 20.0;
/**
* Indicates that a glyph does not have valid drawing bounds, usually
* because it is not visible
*/
public static mxRectangle INVALID_GLYPH_BOUNDS = new mxRectangle(0, 0, 0, 0);
/**
* The index of the central glyph of the label that is visible
*/
public int centerVisibleIndex = 0;
/**
* Specifies if image aspect should be preserved in drawImage. Default is true.
*/
public static Object FONT_FRACTIONALMETRICS = RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT;
/**
* Cache of BIDI glyph vectors
*/
public GlyphVector[] rtlGlyphVectors;
/**
* Shared FRC for font size calculations
*/
public static FontRenderContext frc = new FontRenderContext(null, false,
false);
/**
*
*/
protected boolean rotationEnabled = true;
public mxCurveLabelShape(mxCellState state, mxCurve value)
{
this.state = state;
this.curve = value;
}
/**
*
*/
public boolean getRotationEnabled()
{
return rotationEnabled;
}
/**
*
*/
public void setRotationEnabled(boolean value)
{
rotationEnabled = value;
}
/**
*
*/
public void paintShape(mxGraphics2DCanvas canvas, String text,
mxCellState state, Map style)
{
Rectangle rect = state.getLabelBounds().getRectangle();
Graphics2D g = canvas.getGraphics();
if (labelGlyphs == null)
{
updateLabelBounds(text, style);
}
if (labelGlyphs != null
&& (g.getClipBounds() == null || g.getClipBounds().intersects(
rect)))
{
// Creates a temporary graphics instance for drawing this shape
float opacity = mxUtils.getFloat(style, mxConstants.STYLE_OPACITY,
100);
Graphics2D previousGraphics = g;
g = canvas.createTemporaryGraphics(style, opacity, state);
Font font = mxUtils.getFont(style, canvas.getScale());
g.setFont(font);
Color fontColor = mxUtils.getColor(style,
mxConstants.STYLE_FONTCOLOR, Color.black);
g.setColor(fontColor);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
FONT_FRACTIONALMETRICS);
for (int j = 0; j < labelGlyphs.length; j++)
{
mxLine parallel = labelGlyphs[j].glyphGeometry;
if (labelGlyphs[j].visible && parallel != null
&& parallel != mxCurve.INVALID_POSITION)
{
mxPoint parallelEnd = parallel.getEndPoint();
double x = parallelEnd.getX();
double rotation = (Math.atan(parallelEnd.getY() / x));
if (x < 0)
{
// atan only ranges from -PI/2 to PI/2, have to offset
// for negative x values
rotation += Math.PI;
}
final AffineTransform old = g.getTransform();
g.translate(parallel.getX(), parallel.getY());
g.rotate(rotation);
Shape letter = labelGlyphs[j].glyphShape;
g.fill(letter);
g.setTransform(old);
}
}
g.dispose();
g = previousGraphics;
}
}
/**
* Updates the cached position and size of each glyph in the edge label.
* @param label the entire string of the label.
* @param style the edge style
*/
public mxRectangle updateLabelBounds(String label, Map style)
{
double scale = state.getView().getScale();
Font font = mxUtils.getFont(style, scale);
FontMetrics fm = mxUtils.getFontMetrics(font);
int descent = 0;
int ascent = 0;
if (fm != null)
{
descent = fm.getDescent();
ascent = fm.getAscent();
}
// Check that the size of the widths array matches
// that of the label size
if (labelGlyphs == null || (!label.equals(lastValue)))
{
labelGlyphs = new LabelGlyphCache[label.length()];
}
if (!label.equals(lastValue) || !font.equals(lastFont))
{
char[] labelChars = label.toCharArray();
ArrayList glyphList = new ArrayList();
boolean bidiRequired = Bidi.requiresBidi(labelChars, 0,
labelChars.length);
labelSize = 0;
if (bidiRequired)
{
Bidi bidi = new Bidi(label,
Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT);
int runCount = bidi.getRunCount();
if (rtlGlyphVectors == null
|| rtlGlyphVectors.length != runCount)
{
rtlGlyphVectors = new GlyphVector[runCount];
}
for (int i = 0; i < bidi.getRunCount(); i++)
{
final String labelSection = label.substring(
bidi.getRunStart(i), bidi.getRunLimit(i));
rtlGlyphVectors[i] = font
.layoutGlyphVector(mxCurveLabelShape.frc,
labelSection.toCharArray(), 0,
labelSection.length(),
Font.LAYOUT_RIGHT_TO_LEFT);
}
int charCount = 0;
for (GlyphVector gv : rtlGlyphVectors)
{
float vectorOffset = 0.0f;
for (int j = 0; j < gv.getNumGlyphs(); j++)
{
Shape shape = gv.getGlyphOutline(j, -vectorOffset, 0);
LabelGlyphCache qlyph = new LabelGlyphCache();
glyphList.add(qlyph);
qlyph.glyphShape = shape;
mxRectangle size = new mxRectangle(gv.getGlyphLogicalBounds(j).getBounds2D());
qlyph.labelGlyphBounds = size;
labelSize += size.getWidth();
vectorOffset += size.getWidth();
charCount++;
}
}
}
else
{
rtlGlyphVectors = null;
//String locale = System.getProperty("user.language");
// Character iterator required where character is split over
// string elements
BreakIterator it = BreakIterator.getCharacterInstance(Locale.getDefault());
it.setText(label);
for (int i = 0; i < label.length();)
{
int next = it.next();
int characterLen = 1;
if (next != BreakIterator.DONE)
{
characterLen = next - i;
}
String glyph = label.substring(i, i + characterLen);
LabelGlyphCache labelGlyph = new LabelGlyphCache();
glyphList.add(labelGlyph);
labelGlyph.glyph = glyph;
GlyphVector vector = font.createGlyphVector(frc, glyph);
labelGlyph.glyphShape = vector.getOutline();
if (fm == null)
{
mxRectangle size = new mxRectangle(
font.getStringBounds(glyph,
mxCurveLabelShape.frc));
labelGlyph.labelGlyphBounds = size;
labelSize += size.getWidth();
}
else
{
double width = fm.stringWidth(glyph);
labelGlyph.labelGlyphBounds = new mxRectangle(0, 0,
width, ascent);
labelSize += width;
}
i += characterLen;
}
}
// Update values used to determine whether or not the label cache
// is valid or not
lastValue = label;
lastFont = font;
lastPoints = curve.getGuidePoints();
this.labelGlyphs = glyphList.toArray(new LabelGlyphCache[glyphList.size()]);
}
// Store the start/end buffers that pad out the ends of the branch so the label is
// visible. We work initially as the start section being at the start of the
// branch and the end at the end of the branch. Note that the actual label curve
// might be reversed, so we allow for this after completing the buffer calculations,
// otherwise they'd need to be constant isReversed() checks throughout
labelPosition.startBuffer = LABEL_BUFFER * scale;
labelPosition.endBuffer = LABEL_BUFFER * scale;
calculationLabelPosition(style, label);
if (curve.isLabelReversed())
{
double temp = labelPosition.startBuffer;
labelPosition.startBuffer = labelPosition.endBuffer;
labelPosition.endBuffer = temp;
}
double curveLength = curve.getCurveLength(mxCurve.LABEL_CURVE);
double currentPos = labelPosition.startBuffer / curveLength;
double endPos = 1.0 - (labelPosition.endBuffer / curveLength);
mxRectangle overallLabelBounds = null;
centerVisibleIndex = 0;
double currentCurveDelta = 0.0;
double curveDeltaSignificant = 0.3;
double curveDeltaMax = 0.5;
mxLine nextParallel = null;
// TODO on translation just move the points, don't recalculate
// Might be better than the curve is the only thing updated and
// the curve shapes listen to curve events
// !lastPoints.equals(curve.getGuidePoints())
for (int j = 0; j < labelGlyphs.length; j++)
{
if (currentPos > endPos)
{
labelGlyphs[j].visible = false;
continue;
}
mxLine parallel = nextParallel;
if (currentCurveDelta > curveDeltaSignificant
|| nextParallel == null)
{
parallel = curve.getCurveParallel(mxCurve.LABEL_CURVE,
currentPos);
currentCurveDelta = 0.0;
nextParallel = null;
}
labelGlyphs[j].glyphGeometry = parallel;
if (parallel == mxCurve.INVALID_POSITION)
{
continue;
}
// Get the four corners of the rotated rectangle bounding the glyph
// The drawing bounds of the glyph is the unrotated rect that
// just bounds those four corners
final double w = labelGlyphs[j].labelGlyphBounds.getWidth();
final double h = labelGlyphs[j].labelGlyphBounds.getHeight();
final double x = parallel.getEndPoint().getX();
final double y = parallel.getEndPoint().getY();
// Bottom left
double p1X = parallel.getX() - (descent * y);
double minX = p1X, maxX = p1X;
double p1Y = parallel.getY() + (descent * x);
double minY = p1Y, maxY = p1Y;
// Top left
double p2X = p1X + ((h + descent) * y);
double p2Y = p1Y - ((h + descent) * x);
minX = Math.min(minX, p2X);
maxX = Math.max(maxX, p2X);
minY = Math.min(minY, p2Y);
maxY = Math.max(maxY, p2Y);
// Bottom right
double p3X = p1X + (w * x);
double p3Y = p1Y + (w * y);
minX = Math.min(minX, p3X);
maxX = Math.max(maxX, p3X);
minY = Math.min(minY, p3Y);
maxY = Math.max(maxY, p3Y);
// Top right
double p4X = p2X + (w * x);
double p4Y = p2Y + (w * y);
minX = Math.min(minX, p4X);
maxX = Math.max(maxX, p4X);
minY = Math.min(minY, p4Y);
maxY = Math.max(maxY, p4Y);
minX -= 2 * scale;
minY -= 2 * scale;
maxX += 2 * scale;
maxY += 2 * scale;
// Hook for sub-classers
postprocessGlyph(curve, label, j, currentPos);
// Need to allow for text on inside of curve bends. Need to get the
// parallel for the next section, if there is an excessive
// inner curve, advance the current position accordingly
double currentPosCandidate = currentPos
+ (labelGlyphs[j].labelGlyphBounds.getWidth() + labelPosition.defaultInterGlyphSpace)
/ curveLength;
nextParallel = curve.getCurveParallel(mxCurve.LABEL_CURVE,
currentPosCandidate);
currentPos = currentPosCandidate;
mxPoint nextVector = nextParallel.getEndPoint();
double end2X = nextVector.getX();
double end2Y = nextVector.getY();
if (nextParallel != mxCurve.INVALID_POSITION
&& j + 1 < label.length())
{
// Extend the current parallel line in its direction
// by the length of the next parallel. Use the approximate
// deviation to work out the angle change
double deltaX = Math.abs(x - end2X);
double deltaY = Math.abs(y - end2Y);
// The difference as a proportion of the length of the next
// vector. 1 means a variation of 60 degrees.
currentCurveDelta = Math
.sqrt(deltaX * deltaX + deltaY * deltaY);
}
if (currentCurveDelta > curveDeltaSignificant)
{
// Work out which direction the curve is going in
int ccw = Line2D.relativeCCW(0, 0, x, y, end2X, end2Y);
if (ccw == 1)
{
// Text is on inside of curve
if (currentCurveDelta > curveDeltaMax)
{
// Don't worry about excessive deltas, if they
// are big the label curve will be screwed anyway
currentCurveDelta = curveDeltaMax;
}
double textBuffer = currentCurveDelta
* CURVE_TEXT_STRETCH_FACTOR / curveLength;
currentPos += textBuffer;
endPos += textBuffer;
}
}
if (labelGlyphs[j].drawingBounds != null)
{
labelGlyphs[j].drawingBounds.setRect(minX, minY, maxX - minX,
maxY - minY);
}
else
{
labelGlyphs[j].drawingBounds = new mxRectangle(minX, minY, maxX
- minX, maxY - minY);
}
if (overallLabelBounds == null)
{
overallLabelBounds = (mxRectangle) labelGlyphs[j].drawingBounds
.clone();
}
else
{
overallLabelBounds.add(labelGlyphs[j].drawingBounds);
}
labelGlyphs[j].visible = true;
centerVisibleIndex++;
}
centerVisibleIndex /= 2;
if (overallLabelBounds == null)
{
// Return a small rectangle in the center of the label curve
// Null label bounds causes NPE when editing
mxLine labelCenter = curve.getCurveParallel(mxCurve.LABEL_CURVE,
0.5);
overallLabelBounds = new mxRectangle(labelCenter.getX(),
labelCenter.getY(), 1, 1);
}
this.labelBounds = overallLabelBounds;
return overallLabelBounds;
}
/**
* Hook for sub-classers to perform additional processing on
* each glyph
* @param curve The curve object holding the label curve
* @param label the text label of the curve
* @param j the index of the label
* @param currentPos the distance along the label curve the glyph is
*/
protected void postprocessGlyph(mxCurve curve, String label, int j,
double currentPos)
{
}
/**
* Returns whether or not the rectangle passed in hits any part of this
* curve.
* @param rect the rectangle to detect for a hit
* @return whether or not the rectangle hits this curve
*/
public boolean intersectsRect(Rectangle rect)
{
// To save CPU, we can test if the rectangle intersects the entire
// bounds of this label
if ( (labelBounds != null
&& (!labelBounds.getRectangle().intersects(rect)) )
|| labelGlyphs == null )
{
return false;
}
for (int i = 0; i < labelGlyphs.length; i++)
{
if (labelGlyphs[i].visible
&& rect.intersects(labelGlyphs[i].drawingBounds
.getRectangle()))
{
return true;
}
}
return false;
}
/**
* Hook method to override how the label is positioned on the curve
* @param style the style of the curve
* @param label the string label to be displayed on the curve
*/
protected void calculationLabelPosition(Map style,
String label)
{
double curveLength = curve.getCurveLength(mxCurve.LABEL_CURVE);
double availableLabelSpace = curveLength - labelPosition.startBuffer
- labelPosition.endBuffer;
labelPosition.startBuffer = Math.max(labelPosition.startBuffer,
labelPosition.startBuffer + availableLabelSpace / 2 - labelSize
/ 2);
labelPosition.endBuffer = Math.max(labelPosition.endBuffer,
labelPosition.endBuffer + availableLabelSpace / 2 - labelSize
/ 2);
}
/**
* @return the curve
*/
public mxCurve getCurve()
{
return curve;
}
/**
* @param curve the curve to set
*/
public void setCurve(mxCurve curve)
{
this.curve = curve;
}
/**
* Utility class to describe the characteristics of each glyph of a branch
* branch label. Each instance represents one glyph
*
*/
public class LabelGlyphCache
{
/**
* Cache of the bounds of the individual element of the label of this
* edge. Note that these are the unrotated values used to determine the
* width of each glyph.
*/
public mxRectangle labelGlyphBounds;
/**
* The un-rotated rectangle that just bounds this character
*/
public mxRectangle drawingBounds;
/**
* The glyph being drawn
*/
public String glyph;
/**
* A line parallel to the curve segment at which the element is to be
* drawn
*/
public mxLine glyphGeometry;
/**
* The cached shape of the glyph
*/
public Shape glyphShape;
/**
* Whether or not the glyph should be drawn
*/
public boolean visible;
}
/**
* Utility class that stores details of how the label is positioned
* on the curve
*/
public class LabelPosition
{
public double startBuffer = LABEL_BUFFER;
public double endBuffer = LABEL_BUFFER;
public double defaultInterGlyphSpace = 0;;
}
public mxRectangle getLabelBounds()
{
return labelBounds;
}
/**
* Returns the drawing bounds of the central indexed visible glyph
* @return the centerVisibleIndex
*/
public mxRectangle getCenterVisiblePosition()
{
return labelGlyphs[centerVisibleIndex].drawingBounds;
}
}