]> source.dussan.org Git - poi.git/commitdiff
[github-270] Draw correct XDGF circular arc. Thanks to Dmitry Komarov. This closes...
authorPJ Fanning <fanningpj@apache.org>
Tue, 2 Nov 2021 19:35:25 +0000 (19:35 +0000)
committerPJ Fanning <fanningpj@apache.org>
Tue, 2 Nov 2021 19:35:25 +0000 (19:35 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1894696 13f79535-47bb-0310-9956-ffa450edef68

poi-ooxml/src/main/java/org/apache/poi/xdgf/usermodel/section/geometry/ArcTo.java
poi-ooxml/src/test/java/org/apache/poi/xdgf/usermodel/section/geometry/TestArcTo.java [new file with mode: 0644]

index 4547229d4aa51c06deef21df6d4853c50c4e4a48..531b8eb0fd935a54dc36dba93231f38b3fa20e86 100644 (file)
@@ -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 (file)
index 0000000..5ee3dd0
--- /dev/null
@@ -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));
+            }
+        }
+    }
+}