Source: layer/starfield/SunPosition.js

/*
 * Copyright 2015-2017 WorldWind Contributors
 *
 * 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.
 */
import WorldWind from 'webworldwind-esa';
const {
    Angle,
    ArgumentError,
    Logger
} = WorldWind;

/**
 * Provides utilities for determining the Sun geographic and celestial location.
 * @exports SunPosition
 */
const SunPosition = {

    /**
     * Computes the geographic location of the sun for a given date
     * @param {Date} date
     * @throws {ArgumentError} if the date is missing
     * @return {{latitude: Number, longitude: Number}} the geographic location
     */
    getAsGeographicLocation: function (date) {
        if (date instanceof Date === false) {
            throw new ArgumentError(
                Logger.logMessage(Logger.LEVEL_SEVERE, "SunPosition", "getAsGeographicLocation",
                    "missingDate"));
        }

        let celestialLocation = this.getAsCelestialLocation(date);
        return this.celestialToGeographic(celestialLocation, date);
    },

    /**
     * Computes the celestial location of the sun for a given julianDate
     * @param {Date} date
     * @throws {ArgumentError} if the date is missing
     * @return {{declination: Number, rightAscension: Number}} the celestial location
     */
    getAsCelestialLocation: function (date) {
        if (date instanceof Date === false) {
            throw new ArgumentError(
                Logger.logMessage(Logger.LEVEL_SEVERE, "SunPosition", "getAsCelestialLocation",
                    "missingDate"));
        }

        let julianDate = this.computeJulianDate(date);

        //number of days (positive or negative) since Greenwich noon, Terrestrial Time, on 1 January 2000 (J2000.0)
        let numDays = julianDate - 2451545;

        let meanLongitude = this.normalizeAngle360(280.460 + 0.9856474 * numDays);

        let meanAnomaly = this.normalizeAngle360(357.528 + 0.9856003 * numDays) * Angle.DEGREES_TO_RADIANS;

        let eclipticLongitude = meanLongitude + 1.915 * Math.sin(meanAnomaly) + 0.02 * Math.sin(2 * meanAnomaly);
        let eclipticLongitudeRad = eclipticLongitude * Angle.DEGREES_TO_RADIANS;

        let obliquityOfTheEcliptic = (23.439 - 0.0000004 * numDays) * Angle.DEGREES_TO_RADIANS;

        let declination = Math.asin(Math.sin(obliquityOfTheEcliptic) * Math.sin(eclipticLongitudeRad)) *
            Angle.RADIANS_TO_DEGREES;

        let rightAscension = Math.atan(Math.cos(obliquityOfTheEcliptic) * Math.tan(eclipticLongitudeRad)) *
            Angle.RADIANS_TO_DEGREES;

        //compensate for atan result
        if (eclipticLongitude >= 90 && eclipticLongitude < 270) {
            rightAscension += 180;
        }
        rightAscension = this.normalizeAngle360(rightAscension);

        return {
            declination: declination,
            rightAscension: rightAscension
        };
    },

    /**
     * Converts from celestial coordinates (declination and right ascension) to geographic coordinates
     * (latitude, longitude) for a given julian date
     * @param {{declination: Number, rightAscension: Number}} celestialLocation
     * @param {Date} date
     * @throws {ArgumentError} if celestialLocation or julianDate are missing
     * @return {{latitude: Number, longitude: Number}} the geographic location
     */
    celestialToGeographic: function (celestialLocation, date) {
        if (!celestialLocation) {
            throw new ArgumentError(
                Logger.logMessage(Logger.LEVEL_SEVERE, "SunPosition", "celestialToGeographic",
                    "missingCelestialLocation"));
        }
        if (date instanceof Date === false) {
            throw new ArgumentError(
                Logger.logMessage(Logger.LEVEL_SEVERE, "SunPosition", "celestialToGeographic", "missingDate"));
        }

        let julianDate = this.computeJulianDate(date);

        //number of days (positive or negative) since Greenwich noon, Terrestrial Time, on 1 January 2000 (J2000.0)
        let numDays = julianDate - 2451545;

        //Greenwich Mean Sidereal Time
        let GMST = this.normalizeAngle360(280.46061837 + 360.98564736629 * numDays);

        //Greenwich Hour Angle
        let GHA = this.normalizeAngle360(GMST - celestialLocation.rightAscension);

        let longitude = Angle.normalizedDegreesLongitude(-GHA);

        return {
            latitude: celestialLocation.declination,
            longitude: longitude
        };
    },

    /**
     * Computes the julian date from a javascript date object
     * @param {Date} date
     * @throws {ArgumentError} if the date is missing
     * @return {Number} the julian date
     */
    computeJulianDate: function (date) {
        if (date instanceof Date === false) {
            throw new ArgumentError(
                Logger.logMessage(Logger.LEVEL_SEVERE, "SunPosition", "computeJulianDate", "missingDate"));
        }

        let year = date.getUTCFullYear();
        let month = date.getUTCMonth() + 1;
        let day = date.getUTCDate();
        let hour = date.getUTCHours();
        let minute = date.getUTCMinutes();
        let second = date.getUTCSeconds();

        let dayFraction = (hour + minute / 60 + second / 3600) / 24;

        if (month <= 2) {
            year -= 1;
            month += 12;
        }

        let A = Math.trunc(year / 100);
        let B = 2 - A + Math.trunc(A / 4);
        let JD0h = Math.trunc(365.25 * (year + 4716)) + Math.trunc(30.6001 * (month + 1)) + day + B - 1524.5;

        return JD0h + dayFraction;
    },

    /**
     * Restricts an angle to the range [0, 360] degrees, wrapping angles outside the range.
     * Wrapping takes place as though traversing the edge of a unit circle;
     * angles less than 0 wrap back to 360, while angles greater than 360 wrap back to 0.
     *
     * @param {Number} degrees the angle to wrap in degrees
     *
     * @return {Number} the specified angle wrapped to [0, 360] degrees
     */
    normalizeAngle360: function(degrees) {
        let angle = degrees % 360;
        return angle >= 0 ? angle : (angle < 0 ? 360 + angle : 360 - angle);
    }

};

export default SunPosition;