8076419: Path2D copy constructors and clone method propagate size of arrays from source path
authorlbourges
Wed, 01 Apr 2015 11:23:47 -0700
changeset 29888 660cd235afcc
parent 29887 7dc39f3b8058
child 29889 ccb2e126626f
8076419: Path2D copy constructors and clone method propagate size of arrays from source path Reviewed-by: flar, prr
jdk/src/java.desktop/share/classes/java/awt/geom/Path2D.java
jdk/test/java/awt/geom/Path2D/Path2DCopyConstructor.java
--- a/jdk/src/java.desktop/share/classes/java/awt/geom/Path2D.java	Tue Mar 31 16:23:01 2015 -0700
+++ b/jdk/src/java.desktop/share/classes/java/awt/geom/Path2D.java	Wed Apr 01 11:23:47 2015 -0700
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2006, 2014, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -224,8 +224,8 @@
                 Path2D p2d = (Path2D) s;
                 setWindingRule(p2d.windingRule);
                 this.numTypes = p2d.numTypes;
-                this.pointTypes = Arrays.copyOf(p2d.pointTypes,
-                                                p2d.pointTypes.length);
+                // trim arrays:
+                this.pointTypes = Arrays.copyOf(p2d.pointTypes, p2d.numTypes);
                 this.numCoords = p2d.numCoords;
                 this.floatCoords = p2d.cloneCoordsFloat(at);
             } else {
@@ -237,19 +237,23 @@
             }
         }
 
+        @Override
         float[] cloneCoordsFloat(AffineTransform at) {
+            // trim arrays:
             float ret[];
             if (at == null) {
-                ret = Arrays.copyOf(this.floatCoords, this.floatCoords.length);
+                ret = Arrays.copyOf(floatCoords, numCoords);
             } else {
-                ret = new float[floatCoords.length];
+                ret = new float[numCoords];
                 at.transform(floatCoords, 0, ret, 0, numCoords / 2);
             }
             return ret;
         }
 
+        @Override
         double[] cloneCoordsDouble(AffineTransform at) {
-            double ret[] = new double[floatCoords.length];
+            // trim arrays:
+            double ret[] = new double[numCoords];
             if (at == null) {
                 for (int i = 0; i < numCoords; i++) {
                     ret[i] = floatCoords[i];
@@ -475,6 +479,9 @@
         }
 
         int pointCrossings(double px, double py) {
+            if (numTypes == 0) {
+                return 0;
+            }
             double movx, movy, curx, cury, endx, endy;
             float coords[] = floatCoords;
             curx = movx = coords[0];
@@ -552,6 +559,9 @@
         int rectCrossings(double rxmin, double rymin,
                           double rxmax, double rymax)
         {
+            if (numTypes == 0) {
+                return 0;
+            }
             float coords[] = floatCoords;
             double curx, cury, movx, movy, endx, endy;
             curx = movx = coords[0];
@@ -1061,8 +1071,8 @@
                 Path2D p2d = (Path2D) s;
                 setWindingRule(p2d.windingRule);
                 this.numTypes = p2d.numTypes;
-                this.pointTypes = Arrays.copyOf(p2d.pointTypes,
-                                                p2d.pointTypes.length);
+                // trim arrays:
+                this.pointTypes = Arrays.copyOf(p2d.pointTypes, p2d.numTypes);
                 this.numCoords = p2d.numCoords;
                 this.doubleCoords = p2d.cloneCoordsDouble(at);
             } else {
@@ -1074,8 +1084,10 @@
             }
         }
 
+        @Override
         float[] cloneCoordsFloat(AffineTransform at) {
-            float ret[] = new float[doubleCoords.length];
+            // trim arrays:
+            float ret[] = new float[numCoords];
             if (at == null) {
                 for (int i = 0; i < numCoords; i++) {
                     ret[i] = (float) doubleCoords[i];
@@ -1086,13 +1098,14 @@
             return ret;
         }
 
+        @Override
         double[] cloneCoordsDouble(AffineTransform at) {
+            // trim arrays:
             double ret[];
             if (at == null) {
-                ret = Arrays.copyOf(this.doubleCoords,
-                                    this.doubleCoords.length);
+                ret = Arrays.copyOf(doubleCoords, numCoords);
             } else {
-                ret = new double[doubleCoords.length];
+                ret = new double[numCoords];
                 at.transform(doubleCoords, 0, ret, 0, numCoords / 2);
             }
             return ret;
@@ -1202,6 +1215,9 @@
         }
 
         int pointCrossings(double px, double py) {
+            if (numTypes == 0) {
+                return 0;
+            }
             double movx, movy, curx, cury, endx, endy;
             double coords[] = doubleCoords;
             curx = movx = coords[0];
@@ -1279,6 +1295,9 @@
         int rectCrossings(double rxmin, double rymin,
                           double rxmax, double rymax)
         {
+            if (numTypes == 0) {
+                return 0;
+            }
             double coords[] = doubleCoords;
             double curx, cury, movx, movy, endx, endy;
             curx = movx = coords[0];
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/java/awt/geom/Path2D/Path2DCopyConstructor.java	Wed Apr 01 11:23:47 2015 -0700
@@ -0,0 +1,537 @@
+/*
+ * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+
+import java.awt.Rectangle;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.GeneralPath;
+import java.awt.geom.IllegalPathStateException;
+import java.awt.geom.Path2D;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.Arrays;
+
+/**
+ * @test
+ * @bug 8076419
+ * @summary Check Path2D copy constructor (trims arrays)
+ *          and constructor with zero capacity
+ * @run main Path2DTrimCopy
+ */
+public class Path2DCopyConstructor {
+
+    private final static float EPSILON = 5e-6f;
+    private final static float FLATNESS = 1e-2f;
+
+    private final static AffineTransform at
+        = AffineTransform.getScaleInstance(1.3, 2.4);
+
+    private final static Rectangle2D.Double rect2d
+        = new Rectangle2D.Double(3.2, 4.1, 5.0, 10.0);
+
+    private final static Point2D.Double pt2d
+        = new Point2D.Double(2.0, 2.5);
+
+    public static boolean verbose;
+
+    static void log(String msg) {
+        if (verbose) {
+            System.out.println(msg);
+        }
+    }
+
+    public static void main(String argv[]) {
+        verbose = (argv.length != 0);
+
+        testEmptyDoublePaths();
+        testDoublePaths();
+
+        testEmptyFloatPaths();
+        testFloatPaths();
+
+        testEmptyGeneralPath();
+        testGeneralPath();
+    }
+
+    static void testEmptyDoublePaths() {
+        log("\n - Test(Path2D.Double[0]) ---");
+        test(() -> new Path2D.Double(Path2D.WIND_NON_ZERO, 0));
+    }
+
+    static void testDoublePaths() {
+        log("\n - Test(Path2D.Double) ---");
+        test(() -> new Path2D.Double());
+    }
+
+    static void testEmptyFloatPaths() {
+        log("\n - Test(Path2D.Float[0]) ---");
+        test(() -> new Path2D.Float(Path2D.WIND_NON_ZERO, 0));
+    }
+
+    static void testFloatPaths() {
+        log("\n - Test(Path2D.Float) ---");
+        test(() -> new Path2D.Float());
+    }
+
+    static void testEmptyGeneralPath() {
+        log("\n - Test(GeneralPath[0]) ---");
+        test(() -> new GeneralPath(Path2D.WIND_NON_ZERO, 0));
+    }
+
+    static void testGeneralPath() {
+        log("\n - Test(GeneralPath) ---");
+        test(() -> new GeneralPath());
+    }
+
+    interface PathFactory {
+        Path2D makePath();
+    }
+
+    static void test(PathFactory pf) {
+        log("\n --- test: path(empty) ---");
+        test(pf.makePath(), true);
+        log("\n\n --- test: path(addMove) ---");
+        test(addMove(pf.makePath()), false);
+        log("\n\n --- test: path(addMoveAndLines) ---");
+        test(addMoveAndLines(pf.makePath()), false);
+        log("\n\n --- test: path(addMoveAndQuads) ---");
+        test(addMoveAndQuads(pf.makePath()), false);
+        log("\n\n --- test: path(addMoveAndCubics) ---");
+        test(addMoveAndCubics(pf.makePath()), false);
+        log("\n\n --- test: path(addMoveAndClose) ---");
+        test(addMoveAndClose(pf.makePath()), false);
+    }
+
+    static Path2D addMove(Path2D p2d) {
+        p2d.moveTo(1.0, 0.5);
+        return p2d;
+    }
+
+    static Path2D addMoveAndLines(Path2D p2d) {
+        addMove(p2d);
+        addLines(p2d);
+        return p2d;
+    }
+
+    static Path2D addLines(Path2D p2d) {
+        for (int i = 0; i < 10; i++) {
+            p2d.lineTo(1.1 * i, 2.3 * i);
+        }
+        return p2d;
+    }
+
+    static Path2D addMoveAndCubics(Path2D p2d) {
+        addMove(p2d);
+        addCubics(p2d);
+        return p2d;
+    }
+
+    static Path2D addCubics(Path2D p2d) {
+        for (int i = 0; i < 10; i++) {
+            p2d.curveTo(1.1 * i, 1.2 * i, 1.3 * i, 1.4 * i, 1.5 * i, 1.6 * i);
+        }
+        return p2d;
+    }
+
+    static Path2D addMoveAndQuads(Path2D p2d) {
+        addMove(p2d);
+        addQuads(p2d);
+        return p2d;
+    }
+
+    static Path2D addQuads(Path2D p2d) {
+        for (int i = 0; i < 10; i++) {
+            p2d.quadTo(1.1 * i, 1.2 * i, 1.3 * i, 1.4 * i);
+        }
+        return p2d;
+    }
+
+    static Path2D addMoveAndClose(Path2D p2d) {
+        addMove(p2d);
+        addClose(p2d);
+        return p2d;
+    }
+
+    static Path2D addClose(Path2D p2d) {
+        p2d.closePath();
+        return p2d;
+    }
+
+    static void test(Path2D p2d, boolean isEmpty) {
+        testEqual(new Path2D.Float(p2d), p2d);
+        testEqual(new Path2D.Double(p2d), p2d);
+        testEqual(new GeneralPath(p2d), p2d);
+
+        testIterator(new Path2D.Float(p2d), p2d);
+        testIterator(new Path2D.Double(p2d), p2d);
+        testIterator((Path2D) p2d.clone(), p2d);
+
+        testFlattening(new Path2D.Float(p2d), p2d);
+        testFlattening(new Path2D.Double(p2d), p2d);
+        testFlattening((Path2D) p2d.clone(), p2d);
+
+        testAddMove(new Path2D.Float(p2d));
+        testAddMove(new Path2D.Double(p2d));
+        testAddMove((Path2D) p2d.clone());
+
+        // These should expect exception if empty
+        testAddLine(new Path2D.Float(p2d), isEmpty);
+        testAddLine(new Path2D.Double(p2d), isEmpty);
+        testAddLine((Path2D) p2d.clone(), isEmpty);
+
+        testAddQuad(new Path2D.Float(p2d), isEmpty);
+        testAddQuad(new Path2D.Double(p2d), isEmpty);
+        testAddQuad((Path2D) p2d.clone(), isEmpty);
+
+        testAddCubic(new Path2D.Float(p2d), isEmpty);
+        testAddCubic(new Path2D.Double(p2d), isEmpty);
+        testAddCubic((Path2D) p2d.clone(), isEmpty);
+
+        testAddClose(new Path2D.Float(p2d), isEmpty);
+        testAddClose(new Path2D.Double(p2d), isEmpty);
+        testAddClose((Path2D) p2d.clone(), isEmpty);
+
+        testGetBounds(new Path2D.Float(p2d), p2d);
+        testGetBounds(new Path2D.Double(p2d), p2d);
+        testGetBounds((Path2D) p2d.clone(), p2d);
+
+        testTransform(new Path2D.Float(p2d));
+        testTransform(new Path2D.Double(p2d));
+        testTransform((Path2D) p2d.clone());
+
+        testIntersect(new Path2D.Float(p2d), p2d);
+        testIntersect(new Path2D.Double(p2d), p2d);
+        testIntersect((Path2D) p2d.clone(), p2d);
+
+        testContains(new Path2D.Float(p2d), p2d);
+        testContains(new Path2D.Double(p2d), p2d);
+        testContains((Path2D) p2d.clone(), p2d);
+
+        testGetCurrentPoint(new Path2D.Float(p2d), p2d);
+        testGetCurrentPoint(new Path2D.Double(p2d), p2d);
+        testGetCurrentPoint((Path2D) p2d.clone(), p2d);
+    }
+
+    static void testEqual(Path2D pathA, Path2D pathB) {
+        final PathIterator itA = pathA.getPathIterator(null);
+        final PathIterator itB = pathB.getPathIterator(null);
+
+        float[] coordsA = new float[6];
+        float[] coordsB = new float[6];
+
+        int n = 0;
+        for (; !itA.isDone() && !itB.isDone(); itA.next(), itB.next(), n++) {
+            int typeA = itA.currentSegment(coordsA);
+            int typeB = itB.currentSegment(coordsB);
+
+            if (typeA != typeB) {
+                throw new IllegalStateException("Path-segment[" + n + "] "
+                    + " type are not equals [" + typeA + "|" + typeB + "] !");
+            }
+            if (!equalsArray(coordsA, coordsB, getLength(typeA))) {
+                throw new IllegalStateException("Path-segment[" + n + "] coords"
+                    + " are not equals [" + Arrays.toString(coordsA) + "|"
+                    + Arrays.toString(coordsB) + "] !");
+            }
+        }
+        if (!itA.isDone() || !itB.isDone()) {
+            throw new IllegalStateException("Paths do not have same lengths !");
+        }
+        log("testEqual: " + n + " segments.");
+    }
+
+    static void testIterator(Path2D pathA, Path2D pathB) {
+        final PathIterator itA = pathA.getPathIterator(at);
+        final PathIterator itB = pathB.getPathIterator(at);
+
+        float[] coordsA = new float[6];
+        float[] coordsB = new float[6];
+
+        int n = 0;
+        for (; !itA.isDone() && !itB.isDone(); itA.next(), itB.next(), n++) {
+            int typeA = itA.currentSegment(coordsA);
+            int typeB = itB.currentSegment(coordsB);
+
+            if (typeA != typeB) {
+                throw new IllegalStateException("Path-segment[" + n + "] "
+                    + "type are not equals [" + typeA + "|" + typeB + "] !");
+            }
+            // Take care of floating-point precision:
+            if (!equalsArrayEps(coordsA, coordsB, getLength(typeA))) {
+                throw new IllegalStateException("Path-segment[" + n + "] coords"
+                    + " are not equals [" + Arrays.toString(coordsA) + "|"
+                    + Arrays.toString(coordsB) + "] !");
+            }
+        }
+        if (!itA.isDone() || !itB.isDone()) {
+            throw new IllegalStateException("Paths do not have same lengths !");
+        }
+        log("testIterator: " + n + " segments.");
+    }
+
+    static void testFlattening(Path2D pathA, Path2D pathB) {
+        final PathIterator itA = pathA.getPathIterator(at, FLATNESS);
+        final PathIterator itB = pathB.getPathIterator(at, FLATNESS);
+
+        float[] coordsA = new float[6];
+        float[] coordsB = new float[6];
+
+        int n = 0;
+        for (; !itA.isDone() && !itB.isDone(); itA.next(), itB.next(), n++) {
+            int typeA = itA.currentSegment(coordsA);
+            int typeB = itB.currentSegment(coordsB);
+
+            if (typeA != typeB) {
+                throw new IllegalStateException("Path-segment[" + n + "] "
+                    + "type are not equals [" + typeA + "|" + typeB + "] !");
+            }
+            // Take care of floating-point precision:
+            if (!equalsArrayEps(coordsA, coordsB, getLength(typeA))) {
+                throw new IllegalStateException("Path-segment[" + n + "] coords"
+                    + " are not equals [" + Arrays.toString(coordsA) + "|"
+                    + Arrays.toString(coordsB) + "] !");
+            }
+        }
+        if (!itA.isDone() || !itB.isDone()) {
+            throw new IllegalStateException("Paths do not have same lengths !");
+        }
+        log("testFlattening: " + n + " segments.");
+    }
+
+    static void testAddMove(Path2D pathA) {
+        addMove(pathA);
+        log("testAddMove: passed.");
+    }
+
+    static void testAddLine(Path2D pathA, boolean isEmpty) {
+        try {
+            addLines(pathA);
+        }
+        catch (IllegalPathStateException ipse) {
+            if (isEmpty) {
+                log("testAddLine: passed "
+                    + "(expected IllegalPathStateException catched).");
+                return;
+            } else {
+                throw ipse;
+            }
+        }
+        if (isEmpty) {
+            throw new IllegalStateException("IllegalPathStateException not thrown !");
+        }
+        log("testAddLine: passed.");
+    }
+
+    static void testAddQuad(Path2D pathA, boolean isEmpty) {
+        try {
+            addQuads(pathA);
+        }
+        catch (IllegalPathStateException ipse) {
+            if (isEmpty) {
+                log("testAddQuad: passed "
+                    + "(expected IllegalPathStateException catched).");
+                return;
+            } else {
+                throw ipse;
+            }
+        }
+        if (isEmpty) {
+            throw new IllegalStateException("IllegalPathStateException not thrown !");
+        }
+        log("testAddQuad: passed.");
+    }
+
+    static void testAddCubic(Path2D pathA, boolean isEmpty) {
+        try {
+            addCubics(pathA);
+        }
+        catch (IllegalPathStateException ipse) {
+            if (isEmpty) {
+                log("testAddCubic: passed "
+                    + "(expected IllegalPathStateException catched).");
+                return;
+            } else {
+                throw ipse;
+            }
+        }
+        if (isEmpty) {
+            throw new IllegalStateException("IllegalPathStateException not thrown !");
+        }
+        log("testAddCubic: passed.");
+    }
+
+    static void testAddClose(Path2D pathA, boolean isEmpty) {
+        try {
+            addClose(pathA);
+        }
+        catch (IllegalPathStateException ipse) {
+            if (isEmpty) {
+                log("testAddClose: passed "
+                    + "(expected IllegalPathStateException catched).");
+                return;
+            } else {
+                throw ipse;
+            }
+        }
+        if (isEmpty) {
+            throw new IllegalStateException("IllegalPathStateException not thrown !");
+        }
+        log("testAddClose: passed.");
+    }
+
+    static void testGetBounds(Path2D pathA, Path2D pathB) {
+        final Rectangle rA = pathA.getBounds();
+        final Rectangle rB = pathB.getBounds();
+
+        if (!rA.equals(rB)) {
+            throw new IllegalStateException("Bounds are not equals [" + rA
+                + "|" + rB + "] !");
+        }
+        final Rectangle2D r2dA = pathA.getBounds2D();
+        final Rectangle2D r2dB = pathB.getBounds2D();
+
+        if (!equalsRectangle2D(r2dA, r2dB)) {
+            throw new IllegalStateException("Bounds2D are not equals ["
+                + r2dA + "|" + r2dB + "] !");
+        }
+        log("testGetBounds: passed.");
+    }
+
+    static void testTransform(Path2D pathA) {
+        pathA.transform(at);
+        log("testTransform: passed.");
+    }
+
+    static void testIntersect(Path2D pathA, Path2D pathB) {
+        boolean resA = pathA.intersects(rect2d);
+        boolean resB = pathB.intersects(rect2d);
+        if (resA != resB) {
+            throw new IllegalStateException("Intersects(rect2d) are not equals ["
+                + resA + "|" + resB + "] !");
+        }
+        resA = pathA.intersects(1.0, 2.0, 13.0, 17.0);
+        resB = pathB.intersects(1.0, 2.0, 13.0, 17.0);
+        if (resA != resB) {
+            throw new IllegalStateException("Intersects(doubles) are not equals ["
+                + resA + "|" + resB + "] !");
+        }
+        log("testIntersect: passed.");
+    }
+
+    static void testContains(Path2D pathA, Path2D pathB) {
+        boolean resA = pathA.contains(pt2d);
+        boolean resB = pathB.contains(pt2d);
+        if (resA != resB) {
+            throw new IllegalStateException("Contains(pt) are not equals ["
+                + resA + "|" + resB + "] !");
+        }
+        resA = pathA.contains(pt2d.getX(), pt2d.getY());
+        resB = pathB.contains(pt2d.getX(), pt2d.getY());
+        if (resA != resB) {
+            throw new IllegalStateException("Contains(x,y) are not equals ["
+                + resA + "|" + resB + "] !");
+        }
+        resA = pathA.contains(rect2d);
+        resB = pathB.contains(rect2d);
+        if (resA != resB) {
+            throw new IllegalStateException("Contains(rect2d) are not equals ["
+                + resA + "|" + resB + "] !");
+        }
+        resA = pathA.contains(1.0, 2.0, 13.0, 17.0);
+        resB = pathB.contains(1.0, 2.0, 13.0, 17.0);
+        if (resA != resB) {
+            throw new IllegalStateException("Contains(doubles) are not equals ["
+                + resA + "|" + resB + "] !");
+        }
+        log("testContains: passed.");
+    }
+
+    static void testGetCurrentPoint(Path2D pathA, Path2D pathB) {
+        final Point2D ptA = pathA.getCurrentPoint();
+        final Point2D ptB = pathA.getCurrentPoint();
+        if (((ptA == null) && (ptB != null))
+            || ((ptA != null) && !ptA.equals(ptB)))
+        {
+            throw new IllegalStateException("getCurrentPoint() are not equals ["
+                + ptA + "|" + ptB + "] !");
+        }
+        log("testGetCurrentPoint: passed.");
+    }
+
+    static int getLength(int type) {
+        switch(type) {
+            case PathIterator.SEG_CUBICTO:
+                return 6;
+            case PathIterator.SEG_QUADTO:
+                return 4;
+            case PathIterator.SEG_LINETO:
+            case PathIterator.SEG_MOVETO:
+                return 2;
+            case PathIterator.SEG_CLOSE:
+                return 0;
+            default:
+                throw new IllegalStateException("Invalid type: " + type);
+        }
+    }
+
+
+    // Custom equals methods ---
+
+    public static boolean equalsArray(float[] a, float[] a2, final int len) {
+        for (int i = 0; i < len; i++) {
+            if (Float.floatToIntBits(a[i]) != Float.floatToIntBits(a2[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static boolean equalsArrayEps(float[] a, float[] a2, final int len) {
+        for (int i = 0; i < len; i++) {
+            if (!equalsEps(a[i], a2[i])) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    static boolean equalsRectangle2D(Rectangle2D a, Rectangle2D b) {
+        if (a == b) {
+            return true;
+        }
+        return equalsEps(a.getX(), b.getX())
+            && equalsEps(a.getY(), b.getY())
+            && equalsEps(a.getWidth(), b.getWidth())
+            && equalsEps(a.getHeight(), b.getHeight());
+    }
+
+    static boolean equalsEps(float a, float b) {
+        return (Math.abs(a - b) <= EPSILON);
+    }
+
+    static boolean equalsEps(double a, double b) {
+        return (Math.abs(a - b) <= EPSILON);
+    }
+}