blob: d6d2a10c91851e669635ebb118a68ebf182afe62 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
/* $Id$ */
package org.apache.fop.render.intermediate;
import java.awt.Color;
import java.awt.Rectangle;
import java.io.IOException;
import org.apache.fop.traits.BorderProps;
/**
* This is an abstract base class for handling border painting.
*/
public class BorderPainter {
// TODO Use a class to model border instead of an array
/** Convention index of before top */
protected static final int TOP = 0;
/** Convention index of right border */
protected static final int RIGHT = 1;
/** Convention index of bottom border */
protected static final int BOTTOM = 2;
/** Convention index of left border */
protected static final int LEFT = 3;
// TODO Use a class to model border corners instead of an array
/** Convention index of top-left border corners */
protected static final int TOP_LEFT = 0;
/** Convention index of top-right-end border corners */
protected static final int TOP_RIGHT = 1;
/** Convention index of bottom-right border corners */
protected static final int BOTTOM_RIGHT = 2;
/** Convention index of bottom-left border corners */
protected static final int BOTTOM_LEFT = 3;
/** The ratio between a solid dash and the white-space in a dashed-border */
public static final float DASHED_BORDER_SPACE_RATIO = 0.5f;
/** The length of the dash as a factor of the border width i.e. 2 -> dashWidth = 2*borderWidth */
protected static final float DASHED_BORDER_LENGTH_FACTOR = 2.0f;
private final GraphicsPainter graphicsPainter;
public BorderPainter(GraphicsPainter graphicsPainter) {
this.graphicsPainter = graphicsPainter;
}
/**
* Draws borders.
* @param borderRect the border rectangle
* @param bpsTop the border specification on the top side
* @param bpsBottom the border specification on the bottom side
* @param bpsLeft the border specification on the left side
* @param bpsRight the border specification on the end side
* @param innerBackgroundColor the inner background color
* @throws IFException if an error occurs while drawing the borders
*/
public void drawBorders(Rectangle borderRect,
BorderProps bpsTop, BorderProps bpsBottom,
BorderProps bpsLeft, BorderProps bpsRight, Color innerBackgroundColor)
throws IFException {
try {
drawRoundedBorders(borderRect, bpsTop, bpsBottom, bpsLeft, bpsRight);
} catch (IOException ioe) {
throw new IFException("IO error drawing borders", ioe);
}
}
private BorderProps sanitizeBorderProps(BorderProps bps) {
return bps == null ? bps : bps.width == 0 ? (BorderProps) null : bps;
}
/**
* TODO merge with drawRoundedBorders()?
* @param borderRect the border rectangle
* @param bpsTop the border specification on the top side
* @param bpsBottom the border specification on the bottom side
* @param bpsLeft the border specification on the left side
* @param bpsRight the border specification on the end side
* @throws IOException
*/
protected void drawRectangularBorders(Rectangle borderRect,
BorderProps bpsTop, BorderProps bpsBottom,
BorderProps bpsLeft, BorderProps bpsRight) throws IOException {
bpsTop = sanitizeBorderProps(bpsTop);
bpsBottom = sanitizeBorderProps(bpsBottom);
bpsLeft = sanitizeBorderProps(bpsLeft);
bpsRight = sanitizeBorderProps(bpsRight);
int startx = borderRect.x;
int starty = borderRect.y;
int width = borderRect.width;
int height = borderRect.height;
boolean[] b = new boolean[] {
(bpsTop != null), (bpsRight != null),
(bpsBottom != null), (bpsLeft != null)};
if (!b[TOP] && !b[RIGHT] && !b[BOTTOM] && !b[LEFT]) {
return;
}
int[] bw = new int[] {
(b[TOP] ? bpsTop.width : 0),
(b[RIGHT] ? bpsRight.width : 0),
(b[BOTTOM] ? bpsBottom.width : 0),
(b[LEFT] ? bpsLeft.width : 0)};
int[] clipw = new int[] {
BorderProps.getClippedWidth(bpsTop),
BorderProps.getClippedWidth(bpsRight),
BorderProps.getClippedWidth(bpsBottom),
BorderProps.getClippedWidth(bpsLeft)};
starty += clipw[TOP];
height -= clipw[TOP];
height -= clipw[BOTTOM];
startx += clipw[LEFT];
width -= clipw[LEFT];
width -= clipw[RIGHT];
boolean[] slant = new boolean[] {
(b[LEFT] && b[TOP]),
(b[TOP] && b[RIGHT]),
(b[RIGHT] && b[BOTTOM]),
(b[BOTTOM] && b[LEFT])};
if (bpsTop != null) {
int sx1 = startx;
int sx2 = (slant[TOP_LEFT] ? sx1 + bw[LEFT] - clipw[LEFT] : sx1);
int ex1 = startx + width;
int ex2 = (slant[TOP_RIGHT] ? ex1 - bw[RIGHT] + clipw[RIGHT] : ex1);
int outery = starty - clipw[TOP];
int clipy = outery + clipw[TOP];
int innery = outery + bw[TOP];
saveGraphicsState();
moveTo(sx1, clipy);
int sx1a = sx1;
int ex1a = ex1;
if (isCollapseOuter(bpsTop)) {
if (isCollapseOuter(bpsLeft)) {
sx1a -= clipw[LEFT];
}
if (isCollapseOuter(bpsRight)) {
ex1a += clipw[RIGHT];
}
lineTo(sx1a, outery);
lineTo(ex1a, outery);
}
lineTo(ex1, clipy);
lineTo(ex2, innery);
lineTo(sx2, innery);
closePath();
clip();
drawBorderLine(sx1a, outery, ex1a, innery, true, true,
bpsTop.style, bpsTop.color);
restoreGraphicsState();
}
if (bpsRight != null) {
int sy1 = starty;
int sy2 = (slant[TOP_RIGHT] ? sy1 + bw[TOP] - clipw[TOP] : sy1);
int ey1 = starty + height;
int ey2 = (slant[BOTTOM_RIGHT] ? ey1 - bw[BOTTOM] + clipw[BOTTOM] : ey1);
int outerx = startx + width + clipw[RIGHT];
int clipx = outerx - clipw[RIGHT];
int innerx = outerx - bw[RIGHT];
saveGraphicsState();
moveTo(clipx, sy1);
int sy1a = sy1;
int ey1a = ey1;
if (isCollapseOuter(bpsRight)) {
if (isCollapseOuter(bpsTop)) {
sy1a -= clipw[TOP];
}
if (isCollapseOuter(bpsBottom)) {
ey1a += clipw[BOTTOM];
}
lineTo(outerx, sy1a);
lineTo(outerx, ey1a);
}
lineTo(clipx, ey1);
lineTo(innerx, ey2);
lineTo(innerx, sy2);
closePath();
clip();
drawBorderLine(innerx, sy1a, outerx, ey1a, false, false,
bpsRight.style, bpsRight.color);
restoreGraphicsState();
}
if (bpsBottom != null) {
int sx1 = startx;
int sx2 = (slant[BOTTOM_LEFT] ? sx1 + bw[LEFT] - clipw[LEFT] : sx1);
int ex1 = startx + width;
int ex2 = (slant[BOTTOM_RIGHT] ? ex1 - bw[RIGHT] + clipw[RIGHT] : ex1);
int outery = starty + height + clipw[BOTTOM];
int clipy = outery - clipw[BOTTOM];
int innery = outery - bw[BOTTOM];
saveGraphicsState();
moveTo(ex1, clipy);
int sx1a = sx1;
int ex1a = ex1;
if (isCollapseOuter(bpsBottom)) {
if (isCollapseOuter(bpsLeft)) {
sx1a -= clipw[LEFT];
}
if (isCollapseOuter(bpsRight)) {
ex1a += clipw[RIGHT];
}
lineTo(ex1a, outery);
lineTo(sx1a, outery);
}
lineTo(sx1, clipy);
lineTo(sx2, innery);
lineTo(ex2, innery);
closePath();
clip();
drawBorderLine(sx1a, innery, ex1a, outery, true, false,
bpsBottom.style, bpsBottom.color);
restoreGraphicsState();
}
if (bpsLeft != null) {
int sy1 = starty;
int sy2 = (slant[TOP_LEFT] ? sy1 + bw[TOP] - clipw[TOP] : sy1);
int ey1 = sy1 + height;
int ey2 = (slant[BOTTOM_LEFT] ? ey1 - bw[BOTTOM] + clipw[BOTTOM] : ey1);
int outerx = startx - clipw[LEFT];
int clipx = outerx + clipw[LEFT];
int innerx = outerx + bw[LEFT];
saveGraphicsState();
moveTo(clipx, ey1);
int sy1a = sy1;
int ey1a = ey1;
if (isCollapseOuter(bpsLeft)) {
if (isCollapseOuter(bpsTop)) {
sy1a -= clipw[TOP];
}
if (isCollapseOuter(bpsBottom)) {
ey1a += clipw[BOTTOM];
}
lineTo(outerx, ey1a);
lineTo(outerx, sy1a);
}
lineTo(clipx, sy1);
lineTo(innerx, sy2);
lineTo(innerx, ey2);
closePath();
clip();
drawBorderLine(outerx, sy1a, innerx, ey1a, false, true, bpsLeft.style, bpsLeft.color);
restoreGraphicsState();
}
}
private boolean isCollapseOuter(BorderProps bp) {
return bp != null && bp.isCollapseOuter();
}
/**
* This method calculates the length of the "dash" in a dashed border. The dash satisfies the
* condition that corners start on a dash and end with a dash (rather than ending with a white space).
* @param borderLength The length of the border.
* @param borderWidth The width/thickness of the border.
* @return returns the length of the dash such that it fits the criteria above.
*/
public static float dashWidthCalculator(float borderLength, float borderWidth) {
float dashWidth = DASHED_BORDER_LENGTH_FACTOR * borderWidth;
if (borderWidth < 3) {
dashWidth = (DASHED_BORDER_LENGTH_FACTOR * 3) * borderWidth;
}
int period = (int) ((borderLength - dashWidth) / dashWidth / (1.0f + DASHED_BORDER_SPACE_RATIO));
period = period < 0 ? 0 : period;
return borderLength / (period * (1.0f + DASHED_BORDER_SPACE_RATIO) + 1.0f);
}
/** TODO merge with drawRectangularBorders?
* @param borderRect the border rectangle
* @throws IOException on io exception
* */
protected void drawRoundedBorders(Rectangle borderRect,
BorderProps beforeBorderProps, BorderProps afterBorderProps,
BorderProps startBorderProps, BorderProps endBorderProps) throws IOException {
BorderSegment before = borderSegmentForBefore(beforeBorderProps);
BorderSegment after = borderSegmentForAfter(afterBorderProps);
BorderSegment start = borderSegmentForStart(startBorderProps);
BorderSegment end = borderSegmentForEnd(endBorderProps);
if (before.getWidth() == 0 && after.getWidth() == 0 && start.getWidth() == 0 && end.getWidth() == 0) {
return;
}
final int startx = borderRect.x + start.getClippedWidth();
final int starty = borderRect.y + before.getClippedWidth();
final int width = borderRect.width - start.getClippedWidth() - end.getClippedWidth();
final int height = borderRect.height - before.getClippedWidth() - after.getClippedWidth();
//Determine scale factor if any adjacent elliptic corners overlap
double cornerCorrectionFactor = calculateCornerScaleCorrection(width, height, before, after, start,
end);
drawBorderSegment(start, before, end, 0, width, startx, starty, cornerCorrectionFactor);
drawBorderSegment(before, end, after, 1, height, startx + width, starty, cornerCorrectionFactor);
drawBorderSegment(end, after, start, 2, width, startx + width, starty + height,
cornerCorrectionFactor);
drawBorderSegment(after, start, before, 3, height, startx, starty + height, cornerCorrectionFactor);
}
private void drawBorderSegment(BorderSegment start, BorderSegment before, BorderSegment end,
int orientation, int width, int x, int y, double cornerCorrectionFactor) throws IOException {
if (before.getWidth() != 0) {
//Let x increase in the START->END direction
final int sx2 = start.getWidth() - start.getClippedWidth();
final int ex1 = width;
final int ex2 = ex1 - end.getWidth() + end.getClippedWidth();
final int outery = -before.getClippedWidth();
final int innery = outery + before.getWidth();
final int ellipseSBRadiusX = correctRadius(cornerCorrectionFactor, start.getRadiusEnd());
final int ellipseSBRadiusY = correctRadius(cornerCorrectionFactor, before.getRadiusStart());
final int ellipseBERadiusX = correctRadius(cornerCorrectionFactor, end.getRadiusStart());
final int ellipseBERadiusY = correctRadius(cornerCorrectionFactor, before.getRadiusEnd());
saveGraphicsState();
translateCoordinates(x, y);
if (orientation != 0) {
rotateCoordinates(Math.PI * orientation / 2d);
}
final int ellipseSBX = ellipseSBRadiusX;
final int ellipseSBY = ellipseSBRadiusY;
final int ellipseBEX = ex1 - ellipseBERadiusX;
final int ellipseBEY = ellipseBERadiusY;
int sx1a = 0;
int ex1a = ex1;
if (ellipseSBRadiusX != 0 && ellipseSBRadiusY != 0) {
final double[] joinMetrics = getCornerBorderJoinMetrics(ellipseSBRadiusX,
ellipseSBRadiusY, sx2, innery);
final double outerJoinPointX = joinMetrics[0];
final double outerJoinPointY = joinMetrics[1];
final double sbJoinAngle = joinMetrics[2];
moveTo((int) outerJoinPointX, (int) outerJoinPointY);
arcTo(Math.PI + sbJoinAngle, Math.PI * 3 / 2,
ellipseSBX, ellipseSBY, ellipseSBRadiusX, ellipseSBRadiusY);
} else {
moveTo(0, 0);
if (before.isCollapseOuter()) {
if (start.isCollapseOuter()) {
sx1a -= start.getClippedWidth();
}
if (end.isCollapseOuter()) {
ex1a += end.getClippedWidth();
}
lineTo(sx1a, outery);
lineTo(ex1a, outery);
}
}
if (ellipseBERadiusX != 0 && ellipseBERadiusY != 0) {
final double[] outerJoinMetrics = getCornerBorderJoinMetrics(
ellipseBERadiusX, ellipseBERadiusY, ex1 - ex2, innery);
final double beJoinAngle = ex1 == ex2 ? Math.PI / 2 : Math.PI / 2 - outerJoinMetrics[2];
lineTo(ellipseBEX, 0);
arcTo(Math.PI * 3 / 2 , Math.PI * 3 / 2 + beJoinAngle,
ellipseBEX, ellipseBEY, ellipseBERadiusX, ellipseBERadiusY);
if (ellipseBEX < ex2 && ellipseBEY > innery) {
final double[] innerJoinMetrics = getCornerBorderJoinMetrics(
(double) ex2 - ellipseBEX, (double) ellipseBEY - innery, ex1 - ex2, innery);
final double innerJoinPointX = innerJoinMetrics[0];
final double innerJoinPointY = innerJoinMetrics[1];
final double beInnerJoinAngle = Math.PI / 2 - innerJoinMetrics[2];
lineTo((int) (ex2 - innerJoinPointX), (int) (innerJoinPointY + innery));
arcTo(beInnerJoinAngle + Math.PI * 3 / 2, Math.PI * 3 / 2,
ellipseBEX, ellipseBEY, ex2 - ellipseBEX, ellipseBEY - innery);
} else {
lineTo(ex2, innery);
}
} else {
lineTo(ex1, 0);
lineTo(ex2, innery);
}
if (ellipseSBRadiusX == 0) {
lineTo(sx2, innery);
} else {
if (ellipseSBX > sx2 && ellipseSBY > innery) {
final double[] innerJoinMetrics = getCornerBorderJoinMetrics(ellipseSBRadiusX - sx2,
ellipseSBRadiusY - innery, sx2, innery);
final double sbInnerJoinAngle = innerJoinMetrics[2];
lineTo(ellipseSBX, innery);
arcTo(Math.PI * 3 / 2, sbInnerJoinAngle + Math.PI,
ellipseSBX, ellipseSBY, ellipseSBX - sx2, ellipseSBY - innery);
} else {
lineTo(sx2, innery);
}
}
closePath();
clip();
if (ellipseBERadiusY == 0 && ellipseSBRadiusY == 0) {
drawBorderLine(sx1a, outery, ex1a, innery, true, true,
before.getStyle(), before.getColor());
} else {
int innerFillY = Math.max(Math.max(ellipseBEY, ellipseSBY), innery);
drawBorderLine(sx1a, outery, ex1a, innerFillY, true, true,
before.getStyle(), before.getColor());
}
restoreGraphicsState();
}
}
private static int correctRadius(double cornerCorrectionFactor, int radius) {
return (int) (Math.round(cornerCorrectionFactor * radius));
}
private static BorderSegment borderSegmentForBefore(BorderProps before) {
return AbstractBorderSegment.asBorderSegment(before);
}
private static BorderSegment borderSegmentForAfter(BorderProps after) {
return AbstractBorderSegment.asFlippedBorderSegment(after);
}
private static BorderSegment borderSegmentForStart(BorderProps start) {
return AbstractBorderSegment.asFlippedBorderSegment(start);
}
private static BorderSegment borderSegmentForEnd(BorderProps end) {
return AbstractBorderSegment.asBorderSegment(end);
}
private interface BorderSegment {
Color getColor();
int getStyle();
int getWidth();
int getClippedWidth();
int getRadiusStart();
int getRadiusEnd();
boolean isCollapseOuter();
boolean isSpecified();
}
private abstract static class AbstractBorderSegment implements BorderSegment {
private static BorderSegment asBorderSegment(BorderProps borderProps) {
return borderProps == null ? NullBorderSegment.INSTANCE : new WrappingBorderSegment(borderProps);
}
private static BorderSegment asFlippedBorderSegment(BorderProps borderProps) {
return borderProps == null ? NullBorderSegment.INSTANCE : new FlippedBorderSegment(borderProps);
}
public boolean isSpecified() {
return !(this instanceof NullBorderSegment);
}
private static class WrappingBorderSegment extends AbstractBorderSegment {
protected final BorderProps borderProps;
private final int clippedWidth;
WrappingBorderSegment(BorderProps borderProps) {
this.borderProps = borderProps;
clippedWidth = BorderProps.getClippedWidth(borderProps);
}
public int getStyle() {
return borderProps.style;
}
public Color getColor() {
return borderProps.color;
}
public int getWidth() {
return borderProps.width;
}
public int getClippedWidth() {
return clippedWidth;
}
public boolean isCollapseOuter() {
return borderProps.isCollapseOuter();
}
public int getRadiusStart() {
return borderProps.getRadiusStart();
}
public int getRadiusEnd() {
return borderProps.getRadiusEnd();
}
}
private static class FlippedBorderSegment extends WrappingBorderSegment {
FlippedBorderSegment(BorderProps borderProps) {
super(borderProps);
}
public int getRadiusStart() {
return borderProps.getRadiusEnd();
}
public int getRadiusEnd() {
return borderProps.getRadiusStart();
}
}
private static final class NullBorderSegment extends AbstractBorderSegment {
public static final NullBorderSegment INSTANCE = new NullBorderSegment();
private NullBorderSegment() {
}
public int getWidth() {
return 0;
}
public int getClippedWidth() {
return 0;
}
public int getRadiusStart() {
return 0;
}
public int getRadiusEnd() {
return 0;
}
public boolean isCollapseOuter() {
return false;
}
public Color getColor() {
throw new UnsupportedOperationException();
}
public int getStyle() {
throw new UnsupportedOperationException();
}
public boolean isSpecified() {
return false;
}
}
}
private double[] getCornerBorderJoinMetrics(double ellipseCenterX, double ellipseCenterY, double xWidth,
double yWidth) {
if (xWidth > 0) {
return getCornerBorderJoinMetrics(ellipseCenterX, ellipseCenterY, yWidth / xWidth);
} else {
return new double[]{0, ellipseCenterY, 0};
}
}
private double[] getCornerBorderJoinMetrics(double ellipseCenterX, double ellipseCenterY,
double borderWidthRatio) {
double x = ellipseCenterY * ellipseCenterX * (
ellipseCenterY + ellipseCenterX * borderWidthRatio
- Math.sqrt(2d * ellipseCenterX * ellipseCenterY * borderWidthRatio)
) / (ellipseCenterY * ellipseCenterY
+ ellipseCenterX * ellipseCenterX * borderWidthRatio * borderWidthRatio);
double y = borderWidthRatio * x;
return new double[]{x, y, Math.atan((ellipseCenterY - y) / (ellipseCenterX - x))};
}
/**
* Clip the background to the inner border
* @param rect clipping rectangle
* @param bpsBefore before border
* @param bpsAfter after border
* @param bpsStart start border
* @param bpsEnd end border
* @throws IOException if an I/O error occurs
*/
public void clipBackground(Rectangle rect,
BorderProps bpsBefore, BorderProps bpsAfter,
BorderProps bpsStart, BorderProps bpsEnd) throws IOException {
BorderSegment before = borderSegmentForBefore(bpsBefore);
BorderSegment after = borderSegmentForAfter(bpsAfter);
BorderSegment start = borderSegmentForStart(bpsStart);
BorderSegment end = borderSegmentForEnd(bpsEnd);
int startx = rect.x;
int starty = rect.y;
int width = rect.width;
int height = rect.height;
double correctionFactor = calculateCornerCorrectionFactor(width + start.getWidth() + end.getWidth(),
height + before.getWidth() + after.getWidth(), bpsBefore, bpsAfter, bpsStart, bpsEnd);
Corner cornerBeforeEnd = Corner.createBeforeEndCorner(before, end, correctionFactor);
Corner cornerEndAfter = Corner.createEndAfterCorner(end, after, correctionFactor);
Corner cornerAfterStart = Corner.createAfterStartCorner(after, start, correctionFactor);
Corner cornerStartBefore = Corner.createStartBeforeCorner(start, before, correctionFactor);
new PathPainter(startx + cornerStartBefore.radiusX, starty)
.lineHorizTo(width - cornerStartBefore.radiusX - cornerBeforeEnd.radiusX)
.drawCorner(cornerBeforeEnd)
.lineVertTo(height - cornerBeforeEnd.radiusY - cornerEndAfter.radiusY)
.drawCorner(cornerEndAfter)
.lineHorizTo(cornerEndAfter.radiusX + cornerAfterStart.radiusX - width)
.drawCorner(cornerAfterStart)
.lineVertTo(cornerAfterStart.radiusY + cornerStartBefore.radiusY - height)
.drawCorner(cornerStartBefore);
clip();
}
/**
* The four corners
* SB - Start-Before
* BE - Before-End
* EA - End-After
* AS - After-Start
*
* 0 --> x
* |
* v
* y
*
* SB BE
* *----*
* | |
* | |
* *----*
* AS EA
*
*/
private enum CornerAngles {
/** The before-end angles */
BEFORE_END(Math.PI * 3 / 2, 0),
/** The end-after angles */
END_AFTER(0, Math.PI / 2),
/** The after-start angles*/
AFTER_START(Math.PI / 2, Math.PI),
/** The start-before angles */
START_BEFORE(Math.PI, Math.PI * 3 / 2);
/** Angle of the start of the corner arch relative to the x-axis in the counter-clockwise direction */
private final double start;
/** Angle of the end of the corner arch relative to the x-axis in the counter-clockwise direction */
private final double end;
CornerAngles(double start, double end) {
this.start = start;
this.end = end;
}
}
private static final class Corner {
private static final Corner SQUARE = new Corner(0, 0, null, 0, 0, 0, 0);
/** The radius of the elliptic corner in the x direction */
private final int radiusX;
/** The radius of the elliptic corner in the y direction */
private final int radiusY;
/** The start and end angles of the corner ellipse */
private final CornerAngles angles;
/** The offset in the x direction of the center of the ellipse relative to the starting point */
private final int centerX;
/** The offset in the y direction of the center of the ellipse relative to the starting point */
private final int centerY;
/** The value in the x direction that the corner extends relative to the starting point */
private final int incrementX;
/** The value in the y direction that the corner extends relative to the starting point */
private final int incrementY;
private Corner(int radiusX, int radiusY, CornerAngles angles, int ellipseOffsetX,
int ellipseOffsetY, int incrementX, int incrementY) {
this.radiusX = radiusX;
this.radiusY = radiusY;
this.angles = angles;
this.centerX = ellipseOffsetX;
this.centerY = ellipseOffsetY;
this.incrementX = incrementX;
this.incrementY = incrementY;
}
private static int extentFromRadiusStart(BorderSegment border, double correctionFactor) {
return extentFromRadius(border.getRadiusStart(), border, correctionFactor);
}
private static int extentFromRadiusEnd(BorderSegment border, double correctionFactor) {
return extentFromRadius(border.getRadiusEnd(), border, correctionFactor);
}
private static int extentFromRadius(int radius, BorderSegment border, double correctionFactor) {
return Math.max((int) (radius * correctionFactor) - border.getWidth(), 0);
}
public static Corner createBeforeEndCorner(BorderSegment before, BorderSegment end,
double correctionFactor) {
int width = end.getRadiusStart();
int height = before.getRadiusEnd();
if (width == 0 || height == 0) {
return SQUARE;
}
int x = extentFromRadiusStart(end, correctionFactor);
int y = extentFromRadiusEnd(before, correctionFactor);
return new Corner(x, y, CornerAngles.BEFORE_END, 0, y, x, y);
}
public static Corner createEndAfterCorner(BorderSegment end, BorderSegment after,
double correctionFactor) {
int width = end.getRadiusEnd();
int height = after.getRadiusStart();
if (width == 0 || height == 0) {
return SQUARE;
}
int x = extentFromRadiusEnd(end, correctionFactor);
int y = extentFromRadiusStart(after, correctionFactor);
return new Corner(x, y, CornerAngles.END_AFTER, -x, 0, -x, y);
}
public static Corner createAfterStartCorner(BorderSegment after, BorderSegment start,
double correctionFactor) {
int width = start.getRadiusStart();
int height = after.getRadiusEnd();
if (width == 0 || height == 0) {
return SQUARE;
}
int x = extentFromRadiusStart(start, correctionFactor);
int y = extentFromRadiusEnd(after, correctionFactor);
return new Corner(x, y, CornerAngles.AFTER_START, 0, -y, -x, -y);
}
public static Corner createStartBeforeCorner(BorderSegment start, BorderSegment before,
double correctionFactor) {
int width = start.getRadiusEnd();
int height = before.getRadiusStart();
if (width == 0 || height == 0) {
return SQUARE;
}
int x = extentFromRadiusEnd(start, correctionFactor);
int y = extentFromRadiusStart(before, correctionFactor);
return new Corner(x, y, CornerAngles.START_BEFORE, x, 0, x, -y);
}
}
/**
* This is a helper class for constructing curves composed of move, line and arc operations. Coordinates
* are relative to the terminal point of the previous operation
*/
private final class PathPainter {
/** Current x position */
private int x;
/** Current y position */
private int y;
PathPainter(int x, int y) throws IOException {
moveTo(x, y);
}
private void moveTo(int x, int y) throws IOException {
this.x += x;
this.y += y;
BorderPainter.this.moveTo(this.x, this.y);
}
public PathPainter lineTo(int x, int y) throws IOException {
this.x += x;
this.y += y;
BorderPainter.this.lineTo(this.x, this.y);
return this;
}
public PathPainter lineHorizTo(int x) throws IOException {
return lineTo(x, 0);
}
public PathPainter lineVertTo(int y) throws IOException {
return lineTo(0, y);
}
PathPainter drawCorner(Corner corner) throws IOException {
if (corner.radiusX == 0 && corner.radiusY == 0) {
return this;
}
if (corner.radiusX == 0 || corner.radiusY == 0) {
x += corner.incrementX;
y += corner.incrementY;
BorderPainter.this.lineTo(x, y);
return this;
}
BorderPainter.this.arcTo(corner.angles.start, corner.angles.end, x + corner.centerX,
y + corner.centerY, corner.radiusX, corner.radiusY);
x += corner.incrementX;
y += corner.incrementY;
return this;
}
}
/**
* Calculate the correction factor to handle over-sized elliptic corner radii.
*
* @param width the border width
* @param height the border height
* @param before the before border properties
* @param after the after border properties
* @param start the start border properties
* @param end the end border properties
*
*/
protected static double calculateCornerCorrectionFactor(int width, int height, BorderProps before,
BorderProps after, BorderProps start, BorderProps end) {
return calculateCornerScaleCorrection(width, height, borderSegmentForBefore(before),
borderSegmentForAfter(after), borderSegmentForStart(start), borderSegmentForEnd(end));
}
/**
* Calculate the scaling factor to handle over-sized elliptic corner radii.
*
* @param width the border width
* @param height the border height
* @param before the before border segment
* @param after the after border segment
* @param start the start border segment
* @param end the end border segment
*/
protected static double calculateCornerScaleCorrection(int width, int height, BorderSegment before,
BorderSegment after, BorderSegment start, BorderSegment end) {
return CornerScaleCorrectionCalculator.calculate(width, height, before, after, start, end);
}
private static final class CornerScaleCorrectionCalculator {
private double correctionFactor = 1;
private CornerScaleCorrectionCalculator(int width, int height,
BorderSegment before, BorderSegment after,
BorderSegment start, BorderSegment end) {
calculateForSegment(width, start, before, end);
calculateForSegment(height, before, end, after);
calculateForSegment(width, end, after, start);
calculateForSegment(height, after, start, before);
}
public static double calculate(int width, int height,
BorderSegment before, BorderSegment after,
BorderSegment start, BorderSegment end) {
return new CornerScaleCorrectionCalculator(width, height, before, after, start, end)
.correctionFactor;
}
private void calculateForSegment(int width, BorderSegment bpsStart, BorderSegment bpsBefore,
BorderSegment bpsEnd) {
if (bpsBefore.isSpecified()) {
double ellipseExtent = bpsStart.getRadiusEnd() + bpsEnd.getRadiusStart();
if (ellipseExtent > 0) {
double thisCorrectionFactor = width / ellipseExtent;
if (thisCorrectionFactor < correctionFactor) {
correctionFactor = thisCorrectionFactor;
}
}
}
}
}
private void drawBorderLine(int x1, int y1, int x2, int y2, boolean horz, boolean startOrBefore,
int style, Color color) throws IOException {
graphicsPainter.drawBorderLine(x1, y1, x2, y2, horz, startOrBefore, style, color);
}
private void moveTo(int x, int y) throws IOException {
graphicsPainter.moveTo(x, y);
}
private void lineTo(int x, int y) throws IOException {
graphicsPainter.lineTo(x, y);
}
private void arcTo(final double startAngle, final double endAngle, final int cx, final int cy,
final int width, final int height) throws IOException {
graphicsPainter.arcTo(startAngle, endAngle, cx, cy, width, height);
}
private void rotateCoordinates(double angle) throws IOException {
graphicsPainter.rotateCoordinates(angle);
}
private void translateCoordinates(int xTranslate, int yTranslate) throws IOException {
graphicsPainter.translateCoordinates(xTranslate, yTranslate);
}
private void closePath() throws IOException {
graphicsPainter.closePath();
}
private void clip() throws IOException {
graphicsPainter.clip();
}
private void saveGraphicsState() throws IOException {
graphicsPainter.saveGraphicsState();
}
private void restoreGraphicsState() throws IOException {
graphicsPainter.restoreGraphicsState();
}
}