From e26ab012d832ab711efb64351af6b5c1c12215c1 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Tue, 2 Nov 2021 19:35:25 +0000 Subject: [PATCH] [github-270] Draw correct XDGF circular arc. Thanks to Dmitry Komarov. This closes #270 git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1894696 13f79535-47bb-0310-9956-ffa450edef68 --- .../usermodel/section/geometry/ArcTo.java | 27 ++- .../usermodel/section/geometry/TestArcTo.java | 179 ++++++++++++++++++ 2 files changed, 192 insertions(+), 14 deletions(-) create mode 100644 poi-ooxml/src/test/java/org/apache/poi/xdgf/usermodel/section/geometry/TestArcTo.java diff --git a/poi-ooxml/src/main/java/org/apache/poi/xdgf/usermodel/section/geometry/ArcTo.java b/poi-ooxml/src/main/java/org/apache/poi/xdgf/usermodel/section/geometry/ArcTo.java index 4547229d4a..531b8eb0fd 100644 --- a/poi-ooxml/src/main/java/org/apache/poi/xdgf/usermodel/section/geometry/ArcTo.java +++ b/poi-ooxml/src/main/java/org/apache/poi/xdgf/usermodel/section/geometry/ArcTo.java @@ -17,8 +17,6 @@ package org.apache.poi.xdgf.usermodel.section.geometry; -import java.awt.geom.AffineTransform; -import java.awt.geom.Arc2D; import java.awt.geom.Path2D; import java.awt.geom.Point2D; @@ -112,20 +110,21 @@ public class ArcTo implements GeometryRow { double x0 = last.getX(); double y0 = last.getY(); - double chordLength = Math.hypot(y - y0, x - x0); - double radius = (4 * a * a + chordLength * chordLength) - / (8 * Math.abs(a)); + // Find a normal to the chord of the circle. + double nx = y - y0; + double ny = x0 - x; + double nLength = Math.sqrt(nx * nx + ny * ny); - // center point - double cx = x0 + (x - x0) / 2.0; - double cy = y0 + (y - y0) / 2.0; + // Follow the normal with the height of the arc to get the third point on the circle. + double x1 = (x0 + x) / 2 + a * nx / nLength; + double y1 = (y0 + y) / 2 + a * ny / nLength; - double rotate = Math.atan2(y - cy, x - cx); - - Arc2D arc = new Arc2D.Double(x0, y0 - radius, chordLength, 2 * radius, - 180, x0 < x ? 180 : -180, Arc2D.OPEN); + // Add an elliptical arc with rx / ry = 1 to the path because it's a circle. + EllipticalArcTo.createEllipticalArc(x, y, x1, y1, 0.0, 1.0, path); + } - path.append(AffineTransform.getRotateInstance(rotate, x0, y0) - .createTransformedShape(arc), true); + @Override + public String toString() { + return String.format("ArcTo: x=%f; y=%f; a=%f", x, y, a); } } diff --git a/poi-ooxml/src/test/java/org/apache/poi/xdgf/usermodel/section/geometry/TestArcTo.java b/poi-ooxml/src/test/java/org/apache/poi/xdgf/usermodel/section/geometry/TestArcTo.java new file mode 100644 index 0000000000..5ee3dd0535 --- /dev/null +++ b/poi-ooxml/src/test/java/org/apache/poi/xdgf/usermodel/section/geometry/TestArcTo.java @@ -0,0 +1,179 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.xdgf.usermodel.section.geometry; + +import com.microsoft.schemas.office.visio.x2012.main.CellType; +import com.microsoft.schemas.office.visio.x2012.main.RowType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.awt.geom.Path2D; +import java.awt.geom.PathIterator; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestArcTo { + + private static final double EPS = 0.000001; + + // We draw a circular arc with radius 100 from (0, 0) to (100, 100) + private static final double X0 = 0.0; + private static final double Y0 = 0.0; + private static final double X = 100.0; + private static final double Y = 100.0; + private static final double A = 29.289322; // a = radius - sqrt(((x + x0) / 2) ^ 2 + ((y + y0) / 2) ^2) + + @Test + public void shouldDrawCircularArcWhenArcHeightMoreThanZero() { + ArcTo arcTo = createArcTo(A); + + Path2D.Double actualPath = new Path2D.Double(); + actualPath.moveTo(X0, Y0); + + // Shape isn't used while creating a circular arc + arcTo.addToPath(actualPath, null); + + // This path can be used to draw a curve that approximates calculated arc. + Path2D.Double expectedPath = new Path2D.Double(); + expectedPath.moveTo(X0, Y0); + expectedPath.curveTo(26.521649, 0.0, 51.957040, 10.535684, 70.710678, 29.289321); + expectedPath.curveTo(89.464316, 48.042960, 100.000000, 73.478351, X, Y); + + assertPath(expectedPath, actualPath); + } + + @Test + public void shouldDrawCircularArcWhenArcHeightLessThanZero() { + ArcTo arcTo = createArcTo(-A); + + Path2D.Double actualPath = new Path2D.Double(); + actualPath.moveTo(X0, Y0); + + // Shape isn't used while creating a circular arc + arcTo.addToPath(actualPath, null); + + // This path can be used to draw a curve that approximates calculated arc. + Path2D.Double expectedPath = new Path2D.Double(); + expectedPath.moveTo(X0, Y0); + expectedPath.curveTo(0.0, 26.521649, 10.535684, 51.957040, 29.289321, 70.710678); + expectedPath.curveTo(48.042960, 89.464316, 73.478351, 100.000000, X, Y); + + assertPath(expectedPath, actualPath); + } + + @Test + public void shouldDrawLineInsteadOfArcWhenArcHeightIsZero() { + ArcTo arcTo = createArcTo(0.0); + + Path2D.Double actualPath = new Path2D.Double(); + actualPath.moveTo(X0, Y0); + + // Shape isn't used while creating a circular arc + arcTo.addToPath(actualPath, null); + + // This path can be used to draw a curve that approximates calculated arc. + Path2D.Double expectedPath = new Path2D.Double(); + expectedPath.moveTo(X0, Y0); + expectedPath.lineTo(X, Y); + + assertPath(expectedPath, actualPath); + } + + @Test + public void shouldNotDrawAnythingWhenArcIsDeleted() { + RowType row = RowType.Factory.newInstance(); + row.setIX(0L); + row.setDel(true); + + ArcTo arcTo = new ArcTo(row); + + Path2D.Double actualPath = new Path2D.Double(); + actualPath.moveTo(X0, Y0); + + // Shape isn't used while creating a circular arc + arcTo.addToPath(actualPath, null); + + // This path can be used to draw a curve that approximates calculated arc. + Path2D.Double expectedPath = new Path2D.Double(); + expectedPath.moveTo(X0, Y0); + + assertPath(expectedPath, actualPath); + } + + private static ArcTo createArcTo(double a) { + RowType row = RowType.Factory.newInstance(); + row.setIX(0L); + row.setDel(false); + + CellType xCell = CellType.Factory.newInstance(); + xCell.setN("X"); + xCell.setV(Double.toString(X)); + + CellType yCell = CellType.Factory.newInstance(); + yCell.setN("Y"); + yCell.setV(Double.toString(Y)); + + + CellType aCell = CellType.Factory.newInstance(); + aCell.setN("A"); + aCell.setV(Double.toString(a)); + + CellType[] cells = new CellType[] { xCell , yCell, aCell }; + row.setCellArray(cells); + + return new ArcTo(row); + } + + private static void assertPath(Path2D expected, Path2D actual) { + PathIterator expectedIterator = expected.getPathIterator(null); + PathIterator actualIterator = actual.getPathIterator(null); + + double[] expectedCoordinates = new double[6]; + double[] actualCoordinates = new double[6]; + while (!expectedIterator.isDone() && !actualIterator.isDone()) { + int expectedSegmentType = expectedIterator.currentSegment(expectedCoordinates); + int actualSegmentType = actualIterator.currentSegment(actualCoordinates); + + assertEquals(expectedSegmentType, actualSegmentType); + assertCoordinates(expectedCoordinates, actualCoordinates); + + expectedIterator.next(); + actualIterator.next(); + } + + if (!expectedIterator.isDone() || !actualIterator.isDone()) { + Assertions.fail("Path iterators have different number of segments"); + } + } + + private static void assertCoordinates(double[] expected, double[] actual) { + if (expected.length != actual.length) { + Assertions.fail(String.format("Given coordinates arrays have different length: expected=%s, actual=%s", + Arrays.toString(expected), Arrays.toString(actual))); + } + for (int i = 0; i < expected.length; i++) { + double e = expected[i]; + double a = actual[i]; + + if (Math.abs(e - a) > EPS) { + Assertions.fail(String.format("expected <%f> but found <%f>", e, a)); + } + } + } +} -- 2.39.5