AngleConverter.java

/*
 * Copyright (C) 2018 Alberto Irurueta Carro (alberto@irurueta.com)
 *
 * Licensed 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 com.irurueta.units;

import java.math.BigDecimal;

/**
 * Does angle conversions to different units. To prevent loss of accuracy, conversion should only be done as a final
 * step before displaying angle measurements.
 */
@SuppressWarnings("WeakerAccess")
public class AngleConverter {

    /**
     * Length for degrees and minutes.
     */
    private static final int DEGREES_AND_MINUTES = 2;

    /**
     * Length for degrees, minutes and seconds.
     */
    private static final int DEGREES_MINUTES_AND_SECONDS = 3;

    /**
     * Number of minutes on each degree.
     */
    private static final int DEGREES_TO_MINUTES = 60;

    /**
     * Number of seconds on each minute.
     */
    private static final int MINUTES_TO_SECONDS = 60;

    /**
     * Constructor. Prevents instantiation of helper class.
     */
    private AngleConverter() {
    }

    /**
     * Converts an angle to provided output angle unit.
     *
     * @param input  input angle to be converted.
     * @param output output angle where result will be stored and containing output unit.
     */
    public static void convert(final Angle input, final Angle output) {
        convert(input, output.getUnit(), output);
    }

    /**
     * Converts an angle to requested output unit.
     *
     * @param input      input angle to be converted.
     * @param outputUnit requested output unit.
     * @return converted angle.
     */
    public static Angle convertAndReturnNew(final Angle input, final AngleUnit outputUnit) {
        final var result = new Angle();
        convert(input, outputUnit, result);
        return result;
    }

    /**
     * Converts and updates an angle to requested output unit.
     *
     * @param angle      input angle to be converted and updated.
     * @param outputUnit requested output unit.
     */
    public static void convert(final Angle angle, final AngleUnit outputUnit) {
        convert(angle, outputUnit, angle);
    }

    /**
     * Converts an angle to requested output unit.
     *
     * @param input      input angle to be converted.
     * @param outputUnit requested output unit.
     * @param result     angle instance where result will be stored.
     */
    public static void convert(
            final Angle input, final AngleUnit outputUnit, final Angle result) {
        final var value = convert(input.getValue(), input.getUnit(), outputUnit);
        result.setValue(value);
        result.setUnit(outputUnit);
    }

    /**
     * Converts an angle value from input unit to provided output unit.
     *
     * @param input      angle value.
     * @param inputUnit  input angle unit.
     * @param outputUnit output angle unit.
     * @return converted angle value.
     */
    public static Number convert(
            final Number input, final AngleUnit inputUnit, final AngleUnit outputUnit) {
        return BigDecimal.valueOf(convert(input.doubleValue(), inputUnit, outputUnit));
    }

    /**
     * Converts an angle value from input unit to provided output unit.
     *
     * @param input      angle value.
     * @param inputUnit  input angle unit.
     * @param outputUnit output angle unit.
     * @return converted angle value.
     */
    @SuppressWarnings("DuplicatedCode")
    public static double convert(
            final double input, final AngleUnit inputUnit, final AngleUnit outputUnit) {

        //convert to radians
        final double radians;
        if (inputUnit == AngleUnit.DEGREES) {
            radians = Math.toRadians(input);
        } else {
            radians = input;
        }

        //convert from radians to required output unit
        if (outputUnit == AngleUnit.DEGREES) {
            return Math.toDegrees(radians);
        } else {
            return radians;
        }
    }

    /**
     * Converts provided degree value to radians.
     *
     * @param degree degree value.
     * @return same angle converted to radians.
     */
    public static double degreeToRadian(final double degree) {
        return Math.toRadians(degree);
    }

    /**
     * Converts provided radian value to degrees.
     *
     * @param radian radian value.
     * @return same angle converted to degrees.
     */
    public static double radianToDegree(final double radian) {
        return Math.toDegrees(radian);
    }

    /**
     * Converts provided angle value and unit to integer degrees and decimal minutes.
     *
     * @param value  angle value to be converted from.
     * @param unit   angle unit to be converted from.
     * @param result array where computed degrees and minutes will be stored.
     * @throws IllegalArgumentException if provided array does not have length 2.
     */
    @SuppressWarnings("Duplicates")
    public static void toDegreesAndMinutes(
            final double value, final AngleUnit unit, final double[] result) {
        if (result.length != DEGREES_AND_MINUTES) {
            throw new IllegalArgumentException();
        }

        final var decimalDegrees = convert(value, unit, AngleUnit.DEGREES);

        final var degrees = Math.floor(decimalDegrees);

        final var remainderDegrees = decimalDegrees - degrees;
        final var decimalMinutes = remainderDegrees * DEGREES_TO_MINUTES;

        result[0] = degrees;
        result[1] = decimalMinutes;
    }

    /**
     * Converts provided angle value and unit to integer degrees and decimal minutes.
     *
     * @param value angle value to be converted from.
     * @param unit  angle unit to be converted from.
     * @return array containing degrees and minutes.
     */
    public static double[] toDegreesAndMinutes(final double value, final AngleUnit unit) {
        final var result = new double[DEGREES_AND_MINUTES];
        toDegreesAndMinutes(value, unit, result);
        return result;
    }

    /**
     * Converts provided angle value and unit to integer degrees and decimal minutes.
     *
     * @param value  angle value to be converted from.
     * @param unit   angle unit to be converted from.
     * @param result array where computed degrees and minutes will be stored.
     * @throws IllegalArgumentException if provided array does not have length 2.
     */
    public static void toDegreesAndMinutes(final Number value, final AngleUnit unit, final double[] result) {
        toDegreesAndMinutes(value.doubleValue(), unit, result);
    }

    /**
     * Converts provided angle value and unit to integer degrees and decimal minutes.
     *
     * @param value angle value to be converted from.
     * @param unit  angle unit to be converted from.
     * @return array containing degrees and minutes.
     */
    public static double[] toDegreesAndMinutes(final Number value, final AngleUnit unit) {
        return toDegreesAndMinutes(value.doubleValue(), unit);
    }

    /**
     * Converts provided angle to integer degrees and decimal minutes.
     *
     * @param angle  angle to be converted from.
     * @param result array where computed degrees and minutes will be stored.
     * @throws IllegalArgumentException if provided array does not have length 2.
     */
    public static void toDegreesAndMinutes(final Angle angle, final double[] result) {
        toDegreesAndMinutes(angle.getValue(), angle.getUnit(), result);
    }

    /**
     * Converts provided angle to integer degrees and decimal minutes.
     *
     * @param angle angle to be converted from.
     * @return array containing degrees and minutes.
     */
    public static double[] toDegreesAndMinutes(final Angle angle) {
        return toDegreesAndMinutes(angle.getValue(), angle.getUnit());
    }

    /**
     * Gets angle value from provided integer degrees and decimal minutes.
     *
     * @param degrees    integer degrees.
     * @param minutes    decimal minutes.
     * @param resultUnit unit of returned value.
     * @return decimal degrees.
     * @throws IllegalArgumentException if minutes is negative.
     */
    public static double fromDegreesAndMinutes(
            final int degrees, final double minutes, final AngleUnit resultUnit) {
        if (minutes < 0.0) {
            throw new IllegalArgumentException();
        }
        return convert(degrees + minutes / DEGREES_TO_MINUTES, AngleUnit.DEGREES, resultUnit);
    }

    /**
     * Gets angle from provided integer degrees and decimal minutes and sets the value into provided angle instance.
     *
     * @param degrees integer degrees.
     * @param minutes decimal minutes.
     * @param result  instance where angle value and unit will be stored.
     * @throws IllegalArgumentException if minutes is negative.
     */
    public static void fromDegreesAndMinutes(final int degrees, final double minutes, final Angle result) {
        result.setValue(fromDegreesAndMinutes(degrees, minutes, result.getUnit()));
    }

    /**
     * Gets new angle containing decimal degrees from provided integer degrees and decimal minutes.
     *
     * @param degrees    integer degrees.
     * @param minutes    decimal minutes.
     * @param resultUnit unit of returned value.
     * @return a new angle containing computed decimal degrees.
     * @throws IllegalArgumentException if minutes is negative.
     */
    public static Angle fromDegreesAndMinutesAndReturnNew(
            final int degrees, final double minutes, final AngleUnit resultUnit) {
        final var result = new Angle(0.0, resultUnit);
        fromDegreesAndMinutes(degrees, minutes, result);
        return result;
    }

    /**
     * Converts provided angle value and unit to integer degrees, integer minutes and decimal seconds.
     *
     * @param value  angle value to be converted from.
     * @param unit   angle unit to be converted from.
     * @param result array where computed degrees, minutes and seconds will be stored.
     * @throws IllegalArgumentException if provided array does not have length 3.
     */
    @SuppressWarnings("Duplicates")
    public static void toDegreesMinutesAndSeconds(
            final double value, final AngleUnit unit, final double[] result) {
        if (result.length != DEGREES_MINUTES_AND_SECONDS) {
            throw new IllegalArgumentException();
        }

        final var decimalDegrees = convert(value, unit, AngleUnit.DEGREES);

        final var degrees = Math.floor(decimalDegrees);

        final var remainderDegrees = decimalDegrees - degrees;
        final var decimalMinutes = remainderDegrees * DEGREES_TO_MINUTES;

        final var minutes = Math.floor(decimalMinutes);

        final var remainderMinutes = decimalMinutes - minutes;
        final var decimalSeconds = remainderMinutes * MINUTES_TO_SECONDS;

        result[0] = degrees;
        result[1] = minutes;
        result[2] = decimalSeconds;
    }

    /**
     * Converts provided angle value and unit to integer degrees, integer minutes and decimal seconds.
     *
     * @param value angle value to be converted from.
     * @param unit  angle unit to be converted from.
     * @return array containing degrees, minutes and seconds.
     */
    public static double[] toDegreesMinutesAndSeconds(final double value, final AngleUnit unit) {
        final var result = new double[DEGREES_MINUTES_AND_SECONDS];
        toDegreesMinutesAndSeconds(value, unit, result);
        return result;
    }

    /**
     * Converts provided angle value and unit to integer degrees, integer minutes and decimal seconds.
     *
     * @param value  angle value to be converted from.
     * @param unit   angle unit to be converted from.
     * @param result array where computed degrees, minutes and seconds will be stored.
     * @throws IllegalArgumentException if provided array does not have length 3.
     */
    public static void toDegreesMinutesAndSeconds(final Number value, final AngleUnit unit, final double[] result) {
        toDegreesMinutesAndSeconds(value.doubleValue(), unit, result);
    }

    /**
     * Converts provided angle value and unit to integer degrees, integer minutes and decimal seconds.
     *
     * @param value angle value to be converted from.
     * @param unit  angle unit to be converted from.
     * @return array containing degrees, minutes and seconds.
     */
    public static double[] toDegreesMinutesAndSeconds(final Number value, final AngleUnit unit) {
        return toDegreesMinutesAndSeconds(value.doubleValue(), unit);
    }

    /**
     * Converts provided angle to integer degrees, integer minutes and decimal seconds.
     *
     * @param angle  angle to be converted from.
     * @param result array where computed degrees and minutes will be stored.
     * @throws IllegalArgumentException if provided array does not have length 3.
     */
    public static void toDegreesMinutesAndSeconds(final Angle angle, final double[] result) {
        toDegreesMinutesAndSeconds(angle.getValue(), angle.getUnit(), result);
    }

    /**
     * Converts provided angle to integer degrees, integer minutes and decimal seconds.
     *
     * @param angle angle to be converted from.
     * @return array containing degrees and minutes.
     */
    public static double[] toDegreesMinutesAndSeconds(final Angle angle) {
        return toDegreesMinutesAndSeconds(angle.getValue(), angle.getUnit());
    }

    /**
     * Gets decimal degrees from provided integer degrees, integer minutes and decimal seconds.
     *
     * @param degrees    integer degrees.
     * @param minutes    integer minutes.
     * @param seconds    decimal seconds.
     * @param resultUnit unit of returned value.
     * @return decimal degrees.
     * @throws IllegalArgumentException if minutes or seconds are negative.
     */
    public static double fromDegreesMinutesAndSeconds(
            final int degrees, final int minutes, final double seconds, final AngleUnit resultUnit) {
        if (minutes < 0 || seconds < 0.0) {
            throw new IllegalArgumentException();
        }
        return convert(degrees + (minutes + (seconds / MINUTES_TO_SECONDS)) / DEGREES_TO_MINUTES,
                AngleUnit.DEGREES, resultUnit);
    }

    /**
     * Gets decimal degrees from provided integer degrees, integer minutes and decimal seconds and sets the value
     * into provided angle instance.
     *
     * @param degrees integer degrees.
     * @param minutes integer minutes.
     * @param seconds decimal seconds.
     * @param result  instance where angle value and unit will be stored.
     * @throws IllegalArgumentException if minutes or seconds are negative.
     */
    public static void fromDegreesMinutesAndSeconds(
            final int degrees, final int minutes, final double seconds, final Angle result) {
        result.setValue(fromDegreesMinutesAndSeconds(degrees, minutes, seconds, result.getUnit()));
    }

    /**
     * Gets new angle containing decimal degrees from provided integer degrees, integer minutes and decimal seconds.
     *
     * @param degrees    integer degrees.
     * @param minutes    integer minutes.
     * @param seconds    decimal seconds.
     * @param resultUnit unit of returned value.
     * @return a new angle containing computed decimal degrees.
     * @throws IllegalArgumentException if minutes or seconds are negative.
     */
    public static Angle fromDegreesMinutesAndSecondsAndReturnNew(
            final int degrees, final int minutes, final double seconds, final AngleUnit resultUnit) {
        final var result = new Angle(0.0, resultUnit);
        fromDegreesMinutesAndSeconds(degrees, minutes, seconds, result);
        return result;
    }
}