/**
* $Id: mxCurveLabelShape.java,v 1.1 2012/11/15 13:26:44 gaudenz Exp $
* Copyright (c) 2010, David Benson, Gaudenz Alder
*/
package com.mxgraph.shape;
import com.mxgraph.canvas.mxGraphics2DCanvas;
import com.mxgraph.util.*;
import com.mxgraph.view.mxCellState;
import java.awt.*;
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;
/**
* 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<mxPoint> 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<String, Object> 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<String, Object> 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<LabelGlyphCache> glyphList = new ArrayList<LabelGlyphCache>();
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<String, Object> 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;
}
}