// ***************************************************************************
// *   Copyright (C) 2014 by Paul Lutus                                      *
// *   lutusp@arachnoid.com                                                  *
// *                                                                         *
// *   This program is free software; you can redistribute it and/or modify  *
// *   it under the terms of the GNU General Public License as published by  *
// *   the Free Software Foundation; either version 2 of the License, or     *
// *   (at your option) any later version.                                   *
// *                                                                         *
// *   This program 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 for more details.                          *
// *                                                                         *
// *   You should have received a copy of the GNU General Public License     *
// *   along with this program; if not, write to the                         *
// *   Free Software Foundation, Inc.,                                       *
// *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
// ***************************************************************************

/*
 * This class is identical in TankCalc and TankCalcAndroid
 * Please keep it that way
 */
package tankcalc;

/**
 *
 * @author lutusp
 */
final public class TankProcessor {

    TankCalc parent;
    double sensorPositionX, sensorPositionY, sensorPositionZ, residualVolume;
    double trueSensorPositionX, trueSensorPositionY, trueSensorPositionZ;
    double trueSensorOffset = 0;
    double g_rLSign = 1, g_rRSign = 1;
    double g_rL, g_rR, g_R, g_L, halfCylinder;
    double g_sa, g_ca, g_invca, g_ta;
    double leftTankHeight, rightTankHeight;
    double leftMajorSphereRadius, rightMajorSphereRadius;
    double maxHeight, maxExtent, maxVolume;
    double wallThickness;

    TankProperties sharedTP;

    public boolean setValues(TankCalc p, TankProperties stp) {
        parent = p;
        sharedTP = stp;
        switch (sharedTP.tankOrientation) {
            case Constants.TANKTILTED:
                if (sharedTP.tankAngleUnitIndex == Constants.ENTRYSLOPE) {
                    sharedTP.angleDegrees = Math
                            .atan(sharedTP.enteredAngleDegrees / 100.0)
                            * Constants.degree;
                } else {
                    sharedTP.angleDegrees = sharedTP.enteredAngleDegrees;
                }
                break;
            case Constants.TANKHORIZONTAL:
                sharedTP.angleDegrees = 0;
                break;
            case Constants.TANKVERTICAL:
                sharedTP.angleDegrees = 90;
                break;
        }
        sharedTP.angleRadians = sharedTP.angleDegrees * Constants.radian;
        sharedTP.tankUpright = sharedTP.angleDegrees >= 45;
        // local variables should contain values converted to
        // meters, meters^2 meters^3, etc.
        // sharedTP class should contain values in entered units
        residualVolume = convertVolumeUnits(sharedTP.residualVolume, false);
        g_L = convertLengthUnits(sharedTP.g_L, false);
        g_R = convertLengthUnits(sharedTP.g_R, false);
        g_rL = convertLengthUnits(sharedTP.g_rL, false);
        //g_rLSign = (g_rL < 0) ? -1 : 1;
        g_rL = Math.abs(g_rL);
        g_rR = convertLengthUnits(sharedTP.g_rR, false);
        //g_rRSign = (g_rR < 0) ? -1 : 1;
        g_rR = Math.abs(g_rR);
        sensorPositionX = convertLengthUnits(sharedTP.sensorPositionX, false);
        sensorPositionY = convertLengthUnits(sharedTP.sensorPositionY, false);
        sensorPositionZ = convertLengthUnits(sharedTP.sensorPositionZ, false);
        wallThickness = convertLengthUnits(sharedTP.wallThickness, false);

        halfCylinder = g_L * 0.5;
        // if r > g_R, user is entering r as major sphere radius
        // so compute r from msr
        // otherwise compute msr from r
        if (sharedTP.leftEndCapMode == Constants.SPHEREMODE && g_rL >= g_R) {
            leftMajorSphereRadius = g_rL;
            g_rL = minorRadiusFromMajor(g_R, g_rL);
        } else {
            leftMajorSphereRadius = majorRadiusFromMinor(g_R, g_rL);
        }
        if (sharedTP.rightEndCapMode == Constants.SPHEREMODE && g_rR >= g_R) {
            rightMajorSphereRadius = g_rR;
            g_rR = minorRadiusFromMajor(g_R, g_rR);
        } else {
            rightMajorSphereRadius = majorRadiusFromMinor(g_R, g_rR);
        }
        g_sa = Math.sin(sharedTP.angleRadians);
        g_ca = Math.cos(sharedTP.angleRadians);
        g_invca = 1.0 / g_ca;
        g_ta = Math.tan(sharedTP.angleRadians);
        leftTankHeight = computeHalfHeight(true, sharedTP.leftEndCapMode, g_rL,
                leftMajorSphereRadius);
        rightTankHeight = computeHalfHeight(false, sharedTP.rightEndCapMode,
                g_rR, rightMajorSphereRadius);
        maxHeight = leftTankHeight + rightTankHeight;
        maxExtent = g_L + g_rL + g_rR;
        maxVolume = computeMaxVolume().volume * sharedTP.scalingFactor;
        sharedTP.maxVolumeConverted = convertVolumeUnits(maxVolume, true);
        return computeTrueSensorPosition();
    }

    // uses global values
    double cylinderFullVolume() {
        return Math.PI * g_R * g_R * g_L;
    }

    // uses provided values
    double cylinderFullVolume(double L, double R) {
        return Math.PI * R * R * L;
    }

    // uses global values
    double endCapFullVolume(int endCapMode, double r, double sign) {
        double result = Double.NaN;
        switch (endCapMode) {
            case Constants.ELLIPSEMODE: // elliptical
                result = sign * Constants.twoPi * g_R * g_R * r / 3.0;
                break;
            case Constants.CONICMODE: // conical
                result = sign * Math.PI * g_R * g_R * r / 3.0;
                break;
            case Constants.SPHEREMODE: // spherical
                result = sign * (Math.PI / 6.0) * (3 * g_R * g_R + r * r) * r;
                break;
        }
        return result; // error fall-through
    }

    // uses provided values
    double endCapFullVolume(int endCapMode, double R, double r, double sign) {
        double result = Double.NaN;
        switch (endCapMode) {
            case Constants.ELLIPSEMODE: // elliptical
                result = sign * Constants.twoPi * R * R * r / 3.0;
                break;
            case Constants.CONICMODE: // conical
                result = sign * Math.PI * R * R * r / 3.0;
                break;
            case Constants.SPHEREMODE: // spherical
                result = sign * (Math.PI / 6.0) * (3 * R * R + r * r) * r;
                break;
        }
        return result; // error fall-through
    }

    // uses global values
    public TankCompResult tankFullVolume() {
        TankCompResult result = new TankCompResult(
                cylinderFullVolume()
                + endCapFullVolume(sharedTP.leftEndCapMode, g_rL, g_rLSign)
                + endCapFullVolume(sharedTP.rightEndCapMode, g_rR, g_rRSign),
                cylinderFullSurfaceArea() + endCapFullSurfaceArea(false)
                + endCapFullSurfaceArea(true),
                0);
        return result;
    }

    // adds wall thickness to global values
    public TankCompResult tankFullVolumeWithWallThickness() {
        TankCompResult result = new TankCompResult(
                cylinderFullVolume(g_L, g_R + wallThickness)
                + endCapFullVolume(sharedTP.leftEndCapMode, g_R + wallThickness, g_rL + wallThickness, g_rLSign)
                + endCapFullVolume(sharedTP.rightEndCapMode, g_R + wallThickness, g_rR + wallThickness, g_rRSign),
                cylinderFullSurfaceArea() + endCapFullSurfaceArea(false)
                + endCapFullSurfaceArea(true),
                0);
        return result;
    }

    void p(String s) {
        System.out.println(s);
    }

    // not available from Java Math class
    double atanh(double x) {
        return 0.5 * Math.log((1 + x) / (1 - x));
    }

    double oblateSpheroidSurfaceArea(double R, double r) {
        // http://en.wikipedia.org/wiki/Spheroid
        // this equation requires that 0 < r/R < 1
        double e = Math.sqrt(1 - (r * r) / (R * R));
        return Math.PI * R * R * (1 + ((1 - e * e) / e) * atanh(e));
    }

    double endCapFullSurfaceAreaType(int endCapMode, double R, double r,
            double mr) {
        double result = Double.NaN;
        switch (endCapMode) {
            case Constants.ELLIPSEMODE: // elliptical
                // deal with default values:
                if (r >= R) {
                    // return surface area of half-sphere with radius R
                    result = 2 * Math.PI * R * R;
                } else if (r <= 0) {
                    // return area of circle with radius R
                    result = Math.PI * R * R;
                } else {
                    // Treat as oblate spheroid
                    result = oblateSpheroidSurfaceArea(R, r);
                }
                break;
            case Constants.CONICMODE: // conical
                // Pi R s (hypotenuse)
                result = Math.sqrt(R * R + r * r) * R * Math.PI;
                break;
            case Constants.SPHEREMODE: // spherical
                if (r == R) {
                    result = 2 * Math.PI * R * R;
                } else if (r <= 0) {
                    result = Math.PI * R * R;
                } else {
                    // 2 Pi R mr
                    result = Constants.twoPi * r * mr;
                }
                break;
        }
        return result; // error fall-through
    }

    public double endCapFullSurfaceArea(boolean rightCap) {
        if (rightCap) {
            return endCapFullSurfaceAreaType(sharedTP.leftEndCapMode, g_R,
                    g_rL, rightMajorSphereRadius);
        } else {
            return endCapFullSurfaceAreaType(sharedTP.rightEndCapMode, g_R,
                    g_rR, leftMajorSphereRadius);
        }
    }

    public double cylinderFullSurfaceArea() {
        return Constants.twoPi * g_R * g_L;
    }

    // the "y" value partitions a circle
    // resulting in circle area minus segment area
    // NOTE: -r <= y <= r for 0 <= a <= Pi r^2
    double circleAreaMinusSegment(double r, double y) {
        // return zero?
        if (y <= -r || r <= 0) {
            return 0.0;
        } // return full area?
        else if (y >= r) {
            return Math.PI * r * r;
        }
        // see http://mathworld.wolfram.com/CircularSegment.html
        return r * r * Math.acos(-y / r) + y * Math.sqrt(r * r - y * y);
    }

    // liquid wetted perimeter (partial circumference)
    // usaed for cylindrical wetted surface area
    double wettedLengthSegment(double r, double y) {
        double result = 0;
        // return zero?
        if (y <= -r || r <= 0) {
            result = 0.0;
        } // return full circumference?
        else if (y >= r) {
            result = 2 * r * Math.PI;
        } else {
            result = 2 * r * Math.acos(-y / r);
        }
        return result;
    }

    // the above fuction edited for execution speed
    double wettedLengthNormalized(double r, double y) {
        if (y <= -r || r <= 0) {
            return 0.0;
        } // return full circumference?
        else if (y >= r) {
            return 1;
        }
        return Math.acos(-y / r) / Math.PI;
    }

    // Segmented frustum result for end cap surface area
    // area a = (ra+rb) * sqrt((ra-rb)^2+h^2)
    // this method agrees well with oblate
    // spheroid surface area results
    double wettedAreaSegment(double ra, double rb, double y, double h) {
        double sr = ra + rb;
        double dr = ra - rb;
        double a = Math.PI * sr * Math.sqrt(dr * dr + h * h);
        return a * wettedLengthNormalized(sr * 0.5, y);
    }

    // liquid/gas surface line
    double surfaceLengthSegment(double r, double y) {
        double result = 0;
        // below intersetion?
        if (y <= -r || r <= 0) {
            return 0.0;
        } // above intersection?
        else if (y >= r) {
            return 0.0;
        }
        double x = y / r;
        result = 2 * r * Math.sqrt(1 - x * x);
        return result;
    }

    // end cap surface area requires special handling
    // for a vertical orientation
    double surfaceAreaSegment(double ra, double rb, double ya, double yb,
            double h, boolean vertical) {
        double result = 0;
        if (vertical) {
            if (yb <= 0 && ya > 0) {
                result = rb * rb * Math.PI;
            }
        } else {
            double la = surfaceLengthSegment(ra, ya);
            double lb = surfaceLengthSegment(rb, yb);
            double avg = (la + lb) * 0.5;
            double dy = ya - yb;
            result = Math.sqrt(h * h + dy * dy) * avg;
        }
        return result;
    }

    // trap sqrt of negative number
    double trapSqrt(double x) {
        double y = Math.sqrt(x);
        if (Double.isNaN(y)) {
            y = 0;
        }
        return y;
    }

    /*
     * integrate end caps
     */
    TankCompResult endCapIntegral(double ax, double bx, boolean rightCapFlag,
            double y, int endCapMode, double g_r, double sign,
            double sphereRadius) {
        double ecfsa = endCapFullSurfaceArea(rightCapFlag);
        TankCompResult result = new TankCompResult();
        if (g_r == 0.0 || bx - ax == 0) {
            // flat cap area of segment
            result.wettedArea = circleAreaMinusSegment(g_R, y) * sharedTP.scalingFactor;
            return result;
        }
        double yia = y - g_sa * ax;
        double yib = y - g_sa * bx;
        if (yia >= g_R && yib >= g_R) {
            result.volume = endCapFullVolume(endCapMode, g_r, sign);
            result.wettedArea = ecfsa;
        } else if (yia <= -g_R && yib <= -g_R) {
            result.volume = 0.0;
        } else { // perform a numrical integral
            boolean vertical = (sharedTP.angleDegrees == 90.0);
            int n = sharedTP.endCapIntegrationSteps;
            double x, xp, ry = 0, ory;
            if (ax > bx) {
                double t = ax;
                ax = bx;
                bx = t;
            }
            double radius = 0, oldRadius, invx;
            double ratio = g_R / g_r;
            double rt2 = g_r * 2;
            double range = bx - ax;
            double xstep = range / n;
            double delta;
            // three independent loops only for performance reasons
            switch (endCapMode) {
                case Constants.ELLIPSEMODE: // elliptical end cap
                    delta = ((rightCapFlag) ? g_r : 0);
                    for (int i = 0; i <= n; i++) {
                        xp = i * xstep;
                        x = xp + delta;
                        invx = rt2 - x;
                        oldRadius = radius;
                        radius = trapSqrt(invx * x) * ratio;
                        ory = ry;
                        // rotate ry for angled tanks
                        ry = (y - g_sa * (xp + ax)) * g_invca;
                        if (i < n) {
                            result.volume += circleAreaMinusSegment(radius, ry);
                        }
                        if (i > 0) {
                            result.wettedArea += wettedAreaSegment(oldRadius,
                                    radius, ry, xstep);
                            result.surfaceArea += surfaceAreaSegment(oldRadius,
                                    radius, ory, ry, xstep, vertical);
                        }
                    }
                    break;
                case Constants.CONICMODE: // conical end cap
                    delta = (rightCapFlag ? g_r : 0);
                    for (int i = 0; i <= n; i++) {
                        xp = i * xstep;
                        x = xp + delta;
                        invx = rt2 - x;
                        oldRadius = radius;
                        radius = ((invx < x) ? invx : x) * ratio;
                        ory = ry;
                        // rotate ry for angled tanks
                        ry = (y - g_sa * (xp + ax)) * g_invca;
                        if (i < n) {
                            result.volume += circleAreaMinusSegment(radius, ry);
                        }
                        if (i > 0) {
                            result.wettedArea += wettedAreaSegment(oldRadius,
                                    radius, ry, xstep);
                            result.surfaceArea += surfaceAreaSegment(oldRadius,
                                    radius, ory, ry, xstep, vertical);
                        }
                    }
                    break;
                case Constants.SPHEREMODE: // spherical end cap
                    delta = (rightCapFlag ? sphereRadius - range : -sphereRadius);
                    double srsq = sphereRadius * sphereRadius;
                    for (int i = 0; i <= n; i++) {
                        xp = i * xstep;
                        x = xp + delta;
                        oldRadius = radius;
                        radius = trapSqrt(srsq - (x * x));
                        ory = ry;
                        // rotate ry for angled tanks
                        ry = (y - g_sa * (xp + ax)) * g_invca;
                        if (i < n) {
                            result.volume += circleAreaMinusSegment(radius, ry);
                        }
                        if (i > 0) {
                            result.wettedArea += wettedAreaSegment(oldRadius,
                                    radius, ry, xstep);
                            result.surfaceArea += surfaceAreaSegment(oldRadius,
                                    radius, ory, ry, xstep, vertical);
                        }
                    }
                    break;
            } // switch (endCapMode)
            result.volume *= sign * xstep;
        }
        result.wettedArea *= sharedTP.scalingFactor;
        result.surfaceArea *= sharedTP.scalingFactor;
        result.volume *= sharedTP.scalingFactor;
        return result;
    }

    /*
     * integrate cylindrical section
     */
    TankCompResult cylinderIntegral(double ax, double bx, double y) {
        TankCompResult result = new TankCompResult();
        double sign = 1;
        if (g_L == 0 || bx - ax == 0) {
            // no cylindrical section, return zero
        } else {
            if (g_L < 0) {
                sign = -sign;
            }
            if (ax > bx) {
                double t = ax;
                ax = bx;
                bx = t;
            }
            double yia = y - g_sa * ax;
            double yib = y - g_sa * bx;
            // handle out-of-range cases
            if (yia >= g_R && yib >= g_R) {
                result.volume = cylinderFullVolume();
                result.wettedArea = cylinderFullSurfaceArea();
            } else if (yia <= -g_R && yib <= -g_R) {
                result.volume = 0.0;
            } else if (sharedTP.angleDegrees == 90.0) {
                // if vertical, simple cylindrical volume, wetted area
                // and surface area will do it.
                // the vertical case is somewhat tricky with respect to
                // surface area -- note the logical test settings below
                result.surfaceArea = (y > ax && y <= bx) ? Math.PI * g_R * g_R
                        : 0;
                y = Math.min(y, bx);
                y = Math.max(y, ax);
                // cylindrical volume: pi r^2 h
                result.volume = Math.PI * g_R * g_R * (y - ax);
                // cylindrical wall area: 2 pi r^2
                result.wettedArea = Constants.twoPi * g_R * (y - ax);

            } else { // must numerically integrate
                // / the y content level bisector always
                // lies on the horizontal
                // so if the tank is horizontal,
                // one integration step is enough
                int n = (sharedTP.angleDegrees == 0.0) ? 1
                        : sharedTP.cylinderIntegrationSteps;
                double xp, ry = 0, ory;
                double xstep = (bx - ax) / n;
                for (int i = 0; i <= n; i++) {
                    xp = i * xstep;
                    ory = ry;
                    // rotate ry for angled tanks
                    ry = (y - g_sa * (xp + ax)) * g_invca;
                    if (i < n) {
                        result.volume += circleAreaMinusSegment(g_R, ry);
                    }
                    if (i > 0) {
                        result.wettedArea += wettedLengthSegment(g_R, ry);
                        result.surfaceArea += surfaceAreaSegment(g_R, g_R, ory,
                                ry, xstep, false);
                    }
                }
                result.wettedArea *= xstep;
                result.volume *= sign * xstep;

            }
            result.wettedArea *= sharedTP.scalingFactor;
            result.surfaceArea *= sharedTP.scalingFactor;
            result.volume *= sharedTP.scalingFactor;
        }
        return result;
    }

    /*
     * y = bisector height wrt tank contents, zero is tank center
     * 
     * mode bits:
     * 
     * 1: compute left-hand ellipse 2: compute cylinder 4: compute right-hand
     * ellipse
     */
    TankCompResult performIntegral(int mode, double y, int leftEndCapMode,
            int rightEndCapMode) {
        TankCompResult result = new TankCompResult();
        if (Double.isNaN(y)) {
            return result;
        }
        if ((mode & Constants.LEFTELLIPSE) != 0) {
            result.add(endCapIntegral(-halfCylinder - g_rL, -halfCylinder,
                    false, y, leftEndCapMode, g_rL, g_rLSign,
                    leftMajorSphereRadius));
        }
        if ((mode & Constants.CYLINDER) != 0) {
            result.add(cylinderIntegral(-halfCylinder, halfCylinder, y));
        }
        if ((mode & Constants.RIGHTELLIPSE) != 0) {
            result.add(endCapIntegral(halfCylinder, halfCylinder + g_rR, true,
                    y, rightEndCapMode, g_rR, g_rRSign, rightMajorSphereRadius));
        }
        return result;
    }

    public TankCompResult performFullIntegral(double y, int leftEndCapMode,
            int rightEndCapMode) {
        return performIntegral(Constants.LEFTELLIPSE | Constants.CYLINDER | Constants.RIGHTELLIPSE, y
                - leftTankHeight, leftEndCapMode, rightEndCapMode);
    }

    // this provides left or right tank height for a given tilt angle and
    // properties of the tank's left or right end cap
    public double computeHalfHeight(boolean leftEnd, int endCapMode,
            double g_r, double majorSphereRadius) {
        if (sharedTP.angleDegrees == 90) {
            return halfCylinder + g_r;
        } else if (sharedTP.angleDegrees == 0) {
            return g_R;
        } else { // intermediate angles
            // x axis component
            double rx = g_R * g_ca;
            // y axis component
            double ly = (halfCylinder * g_sa);
            if (g_r == 0.0) {
                // it's a simple cylinder
                return ly + rx;
            } else {
                double y = g_r * g_sa;
                switch (endCapMode) {
                    case Constants.ELLIPSEMODE: // elliptical end cap
                        return ly + rx + Math.sqrt(rx * rx + y * y) - g_R * g_ca;
                    case Constants.CONICMODE: // conical end cap
                        y -= rx;
                        return ly + rx + ((y > 0) ? y : 0);
                    case Constants.SPHEREMODE: // spherical end cap, by far the most difficult
                        // this should never happen
                        boolean shortRadius = majorSphereRadius < halfCylinder;
                        // test if inside or outside the solid angle subtended by
                        // the end cap
                        double ta = Math.atan(halfCylinder / g_R);
                        if (!shortRadius) {
                            if (sharedTP.angleRadians < ta) { // outside critical
                                // cone, treat as
                                // cylinder
                                // intersection
                                return ly + rx;
                            } else { // inside the end cap region
                                // double ma = (majorSphereRadius * g_sa) -
                                // halfCylinder;
                                double ma = (g_r * g_sa);
                                double mb = (g_R * g_ca);
                                double mr = Math.sqrt(ma * ma + mb * mb);
                                // parent.beep();
                                // p("ly+mr : " + pn(ly+mr) + ", g_r " + pn(g_r) +
                                // " msr: " + pn(majorSphereRadius) );
                                return ly + mr;
                            }
                        } else {
                            // short radius case : msr < half cylinder length but >
                            // cylinder radius
                            double qy = (rx > g_r) ? rx : g_r;
                            // p("short radius msr: " + pn(majorSphereRadius) +
                            // ", g_R = " + pn(g_R) + " qy = " + pn(qy));
                            return ly + qy;
                        }
                }
            }
        }
        // should never get here
        return Double.NaN;
    }

    // msr = (R*R+r*r) / (2*r), r != 0
    double majorRadiusFromMinor(double R, double r) {
        if (r > 0) {
            return (R * R + r * r) / (2 * r);
        } else {
            return Double.NaN;
        }

    }

    // r = msr - sqrt(sr*sr-R*R)
    double minorRadiusFromMajor(double R, double sr) {
        return sr - Math.sqrt(sr * sr - R * R);
    }

    boolean computeTrueSensorPosition() {
        // the Y integration plane is initially ofset to the bottom
        // of the tank, so no radius offsets
        trueSensorPositionZ = sensorPositionZ;
        double delta;
        double x, y, z = 0;
        // build horizontal case
        delta = g_R - Math.sqrt(g_R * g_R - sensorPositionZ * sensorPositionZ);
        x = sensorPositionX;
        y = -g_R + delta + sensorPositionY;
        CartesianNumber hCase = new CartesianNumber(x, y, z);
        // build vertical case
        delta = endCapSurfacePos(sharedTP.leftEndCapMode, sensorPositionY,
                sensorPositionZ);
        x = -((halfCylinder + g_rL) - delta) + sensorPositionX;
        y = sensorPositionY;
        CartesianNumber vCase = new CartesianNumber(x, y, z);
        int tankPos = (sharedTP.angleDegrees == 0) ? 0
                : (sharedTP.angleDegrees < 90) ? 1 : 2;
        switch (tankPos) {
            case 0: // 0 degrees
                trueSensorPositionX = hCase.x;
                trueSensorPositionY = hCase.y;
                break;
            case 1: // 0 < angle < 90 degrees
                double sa = Math.sin(sharedTP.angleRadians);
                double ca = Math.cos(sharedTP.angleRadians);
                if (sharedTP.tankUpright) { // nearing vertical
                    trueSensorPositionX = vCase.x * ca + vCase.y * sa;
                    trueSensorPositionY = vCase.x * sa - vCase.y * ca;
                } else { // nearing horizontal
                    trueSensorPositionX = hCase.x * ca - hCase.y * sa;
                    trueSensorPositionY = hCase.x * sa + hCase.y * ca;
                }
                break;
            case 2: // 90 degrees
                trueSensorPositionX = vCase.y;
                trueSensorPositionY = vCase.x;
                break;
        }
        trueSensorOffset = leftTankHeight + trueSensorPositionY;
        return (!Double.isNaN(trueSensorOffset));
    }

    double endCapSurfacePos(int capMode, double x, double z) {
        double h = Math.sqrt(x * x + z * z);
        double v = 0;
        switch (capMode) {
            case Constants.ELLIPSEMODE:
                v = g_rL - ((Math.sqrt(g_R * g_R - h * h)) * g_rL / g_R);
                break;
            case Constants.CONICMODE:
                v = h * g_rL / g_R;
                break;
            case Constants.SPHEREMODE:
                double msr = leftMajorSphereRadius;
                v = msr - Math.sqrt(msr * msr - h * h);
                break;
        }
        return v;
    }

    TankCompResult tankVolumeForHeight(double y) {
        return performFullIntegral(y, sharedTP.leftEndCapMode,
                sharedTP.rightEndCapMode);
        // return v;
    }

    TankCompResult volumeForHeightCore(double y) {
        return tankVolumeForHeight(y);
    }

    // this is a root finder that uses a binary search
    // to get a height result from a volume computation
    double heightForVolumeCore(double v) {
        // trap volume range errors
        if (v == 0.0) {
            return 0.0;
        }
        TankCompResult mv = computeMaxVolume();
        double my = computeFullTankHeight();
        if (v == mv.volume) {
            return my;
        }
        int n = 0;
        double dy = my;
        double y = dy * 0.5;
        double dv;
        while (n++ < 64
                && Math.abs(dv = volumeForHeightCore(y).volume - v) > sharedTP.rootFinderEpsilon) {
            dy *= 0.5;
            y += (dv < 0) ? dy : -dy;
        }
        return y;
    }

    TankCompResult volumeForHeight(double height) {
        // TankCompResult result = new TankCompResult();
        height *= sensorScalingFactor();
        if (trueSensorOffset != 0) {
            height += trueSensorOffset;
        }
        // height *= sensorScalingFactor();
        // if (trueSensorOffset != 0 && ((height > maxHeight) || (height < 0)))
        // {
        // result.error();
        // } else {
        TankCompResult result = volumeForHeightCore(height);
        if (!Double.isNaN(result.volume)) {
            if (residualVolume != 0) {
                // add user-entere residual volume
                result.volume += residualVolume;
            }
        }
        // }
        // p("result: " + height + "," + result.volume);
        return result;
    }

    TankCompResult heightForVolume(double volume) {
        if (residualVolume != 0) {
            // subtract user-entered residual volume
            volume -= residualVolume;
        }
        double height = heightForVolumeCore(volume);
        if (!Double.isNaN(height)) {
            height /= sensorScalingFactor();
        }
        if (trueSensorOffset != 0) {
            height -= trueSensorOffset;
            if (height < 0 || height > maxHeight) {
                height = Double.NaN;
            }
        }
        TankCompResult result = new TankCompResult();
        if (!Double.isNaN(height)) {
            result = tankVolumeForHeight(height);
            result.height = height;
        }
        return result;
    }

    TankCompResult computeHeightOrVolume(double y) {
        if (parent.inverseMode) {
            return heightForVolume(y);
        } else {
            return volumeForHeight(y);
        }
    }

    /*
     * full tank volume = PI R^2 L (cylindrical part) + 4/3 PI R^2 r (elliptical
     * part)
     */
    TankCompResult computeMaxVolume() {
        return tankFullVolume();
        // double v = Math.PI * R * R * L;
        // v += Math.PI * R * R * r * 4.0 / 3.0;
        // return v;
    }

    double computeFullTankHeight() {
        return maxHeight;
        /*
         * double my; if (computationMode == 0) { my = 2.0 * R; } else if
         * (computationMode == 1) { my =
         * tankVolumeIntegrator.computeTiltedTankHeight(); } else { // vertical
         * tank my = L + (2 * r); } return my;
         */
    }

    double sensorScalingFactor() {
        double result = 1;
        switch (sharedTP.sensorOrientation) {
            case Constants.SENSORSURFACE:
                /*
                 * if (this.tankUpright) { result = Math.sin(tankAngleRadians); }
                 * else { result = Math.cos(tankAngleRadians); }
                 */
                break;
            case Constants.SENSORXAXIS:
                // result = Math.cos(tankAngleRadians);
                result = Math.cos(sharedTP.angleRadians);
                break;
            case Constants.SENSORYAXIS:
                result = Math.sin(sharedTP.angleRadians);
                break;
        }
        return result;
    }

    // from meters to/from user-selected units of length
    double convertLengthUnits(double v, boolean inverse) {
        double cv = Constants.conversionValues[sharedTP.inputUnitIndex];
        return (inverse) ? v * cv : v / cv;
    }

    // from square meters to/from user-selected units of area
    double convertAreaUnits(double v, boolean inverse) {
        double ou = Constants.conversionValues[sharedTP.inputUnitIndex];
        ou = (ou * ou);
        return (inverse) ? v * ou : v / ou;
    }

    // from cubic meters to/from user-selected units of volume
    double convertVolumeUnits(double v, boolean inverse) {
        double ou = Constants.conversionValues[sharedTP.outputUnitIndex];
        ou = (ou * ou * ou);
        return (inverse) ? v * ou : v / ou;
    }

}
