Source: layer/StarFieldLayer.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.
 */
/**
 * @exports StarFieldLayer
 */
define([
        './Layer',
        '../util/Logger',
        '../geom/Matrix',
        '../shaders/StarFieldProgram',
        '../util/SunPosition'
    ],
    function (Layer,
              Logger,
              Matrix,
              StarFieldProgram,
              SunPosition) {
        'use strict';

        /**
         * Constructs a layer showing stars and the Sun around the Earth.
         * If used together with the AtmosphereLayer, the StarFieldLayer must be inserted before the AtmosphereLayer.
         *
         * If you want to use your own star data, the file provided must be .json
         * and the fields 'ra', 'dec' and 'vmag' must be present in the metadata.
         * ra and dec must be expressed in degrees.
         *
         * This layer uses J2000.0 as the ref epoch.
         *
         * If the star data .json file is too big, consider enabling gzip compression on your web server.
         * For more info about enabling gzip compression consult the configuration for your web server.
         *
         * @alias StarFieldLayer
         * @constructor
         * @classdesc Provides a layer showing stars, and the Sun around the Earth
         * @param {URL} starDataSource optional url for the stars data
         * @augments Layer
         */
        var StarFieldLayer = function (starDataSource) {
            Layer.call(this, 'StarField');

            // The StarField Layer is not pickable.
            this.pickEnabled = false;

            /**
             * The size of the Sun in pixels.
             * This can not exceed the maximum allowed pointSize of the GPU.
             * A warning will be given if the size is too big and the allowed max size will be used.
             * @type {Number}
             * @default 128
             */
            this.sunSize = 128;

            /**
             * Indicates weather to show or hide the Sun
             * @type {Boolean}
             * @default true
             */
            this.showSun = true;

            //Documented in defineProperties below.
            this._starDataSource = starDataSource || WorldWind.configuration.baseUrl + 'images/stars.json';
            this._sunImageSource = WorldWind.configuration.baseUrl + 'images/sunTexture.png';

            //Internal use only.
            //The MVP matrix of this layer.
            this._matrix = Matrix.fromIdentity();

            //Internal use only.
            //gpu cache key for the stars vbo.
            this._starsPositionsVboCacheKey = null;

            //Internal use only.
            this._numStars = 0;

            //Internal use only.
            this._starData = null;

            //Internal use only.
            this._minMagnitude = Number.MAX_VALUE;
            this._maxMagnitude = Number.MIN_VALUE;

            //Internal use only.
            //A flag to indicate the star data is currently being retrieved.
            this._loadStarted = false;

            //Internal use only.
            this._minScale = 10e6;

            //Internal use only.
            this._sunPositionsCacheKey = '';
            this._sunBufferView = new Float32Array(4);

            //Internal use only.
            this._MAX_GL_POINT_SIZE = 0;
        };

        StarFieldLayer.prototype = Object.create(Layer.prototype);

        Object.defineProperties(StarFieldLayer.prototype, {
            /**
             * Url for the stars data.
             * @memberof StarFieldLayer.prototype
             * @type {URL}
             */
            starDataSource: {
                get: function () {
                    return this._starDataSource;
                },
                set: function (value) {
                    this._starDataSource = value;
                    this.invalidateStarData();
                }
            },

            /**
             * Url for the sun texture image.
             * @memberof StarFieldLayer.prototype
             * @type {URL}
             */
            sunImageSource: {
                get: function () {
                    return this._sunImageSource;
                },
                set: function (value) {
                    this._sunImageSource = value;
                }
            }
        });

        // Documented in superclass.
        StarFieldLayer.prototype.doRender = function (dc) {
            if (dc.globe.is2D()) {
                return;
            }

            if (!this.haveResources(dc)) {
                this.loadResources(dc);
                return;
            }

            this.beginRendering(dc);
            try {
                this.doDraw(dc);
            }
            finally {
                this.endRendering(dc);
            }
        };

        // Internal. Intentionally not documented.
        StarFieldLayer.prototype.haveResources = function (dc) {
            var sunTexture = dc.gpuResourceCache.resourceForKey(this._sunImageSource);
            return (
                this._starData != null &&
                sunTexture != null
            );
        };

        // Internal. Intentionally not documented.
        StarFieldLayer.prototype.loadResources = function (dc) {
            var gl = dc.currentGlContext;
            var gpuResourceCache = dc.gpuResourceCache;

            if (!this._starData) {
                this.fetchStarData();
            }

            var sunTexture = gpuResourceCache.resourceForKey(this._sunImageSource);
            if (!sunTexture) {
                gpuResourceCache.retrieveTexture(gl, this._sunImageSource);
            }
        };

        // Internal. Intentionally not documented.
        StarFieldLayer.prototype.beginRendering = function (dc) {
            var gl = dc.currentGlContext;
            dc.findAndBindProgram(StarFieldProgram);
            gl.enableVertexAttribArray(0);
            gl.depthMask(false);
        };

        // Internal. Intentionally not documented.
        StarFieldLayer.prototype.doDraw = function (dc) {
            this.loadCommonUniforms(dc);
            this.renderStars(dc);
            if (this.showSun) {
                this.renderSun(dc);
            }
        };

        // Internal. Intentionally not documented.
        StarFieldLayer.prototype.loadCommonUniforms = function (dc) {
            var gl = dc.currentGlContext;
            var program = dc.currentProgram;

            var eyePoint = dc.navigatorState.eyePoint;
            var eyePosition = dc.globe.computePositionFromPoint(eyePoint[0], eyePoint[1], eyePoint[2], {});
            var scale = Math.max(eyePosition.altitude * 1.5, this._minScale);
            this._matrix.copy(dc.navigatorState.modelviewProjection);
            this._matrix.multiplyByScale(scale, scale, scale);
            program.loadModelviewProjection(gl, this._matrix);

            //this subtraction does not work properly on the GPU, it must be done on the CPU
            //possibly due to precision loss
            //number of days (positive or negative) since Greenwich noon, Terrestrial Time, on 1 January 2000 (J2000.0)
            var julianDate = SunPosition.computeJulianDate(this.time || new Date());
            program.loadNumDays(gl, julianDate - 2451545.0);
        };

        // Internal. Intentionally not documented.
        StarFieldLayer.prototype.renderStars = function (dc) {
            var gl = dc.currentGlContext;
            var gpuResourceCache = dc.gpuResourceCache;
            var program = dc.currentProgram;

            if (!this._starsPositionsVboCacheKey) {
                this._starsPositionsVboCacheKey = gpuResourceCache.generateCacheKey();
            }
            var vboId = gpuResourceCache.resourceForKey(this._starsPositionsVboCacheKey);
            if (!vboId) {
                vboId = gl.createBuffer();
                var positions = this.createStarsGeometry();
                gpuResourceCache.putResource(this._starsPositionsVboCacheKey, vboId, positions.length * 4);
                gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
                gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
            }
            else {
                gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
            }
            dc.frameStatistics.incrementVboLoadCount(1);

            gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 0, 0);

            program.loadMagnitudeRange(gl, this._minMagnitude, this._maxMagnitude);
            program.loadTextureEnabled(gl, false);

            gl.drawArrays(gl.POINTS, 0, this._numStars);
        };

        // Internal. Intentionally not documented.
        StarFieldLayer.prototype.renderSun = function (dc) {
            var gl = dc.currentGlContext;
            var program = dc.currentProgram;
            var gpuResourceCache = dc.gpuResourceCache;

            if (!this._MAX_GL_POINT_SIZE) {
                this._MAX_GL_POINT_SIZE = gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE)[1];
            }
            if (this.sunSize > this._MAX_GL_POINT_SIZE) {
                Logger.log(Logger.LEVEL_WARNING, 'StarFieldLayer - sunSize is to big, max size allowed is: ' +
                    this._MAX_GL_POINT_SIZE);
            }

            var sunCelestialLocation = SunPosition.getAsCelestialLocation(this.time || new Date());

            //.x = declination
            //.y = right ascension
            //.z = point size
            //.w = magnitude
            this._sunBufferView[0] = sunCelestialLocation.declination;
            this._sunBufferView[1] = sunCelestialLocation.rightAscension;
            this._sunBufferView[2] = Math.min(this.sunSize, this._MAX_GL_POINT_SIZE);
            this._sunBufferView[3] = 1;

            if (!this._sunPositionsCacheKey) {
                this._sunPositionsCacheKey = gpuResourceCache.generateCacheKey();
            }
            var vboId = gpuResourceCache.resourceForKey(this._sunPositionsCacheKey);
            if (!vboId) {
                vboId = gl.createBuffer();
                gpuResourceCache.putResource(this._sunPositionsCacheKey, vboId, this._sunBufferView.length * 4);
                gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
                gl.bufferData(gl.ARRAY_BUFFER, this._sunBufferView, gl.DYNAMIC_DRAW);
            }
            else {
                gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
                gl.bufferSubData(gl.ARRAY_BUFFER, 0, this._sunBufferView);
            }
            dc.frameStatistics.incrementVboLoadCount(1);
            gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 0, 0);

            program.loadTextureEnabled(gl, true);

            var sunTexture = dc.gpuResourceCache.resourceForKey(this._sunImageSource);
            sunTexture.bind(dc);

            gl.drawArrays(gl.POINTS, 0, 1);
        };

        // Internal. Intentionally not documented.
        StarFieldLayer.prototype.endRendering = function (dc) {
            var gl = dc.currentGlContext;
            gl.depthMask(true);
            gl.disableVertexAttribArray(0);
        };

        // Internal. Intentionally not documented.
        StarFieldLayer.prototype.fetchStarData = function () {
            if (this._loadStarted) {
                return;
            }

            this._loadStarted = true;
            var self = this;
            var xhr = new XMLHttpRequest();

            xhr.onload = function () {
                if (this.status >= 200 && this.status < 300) {
                    try {
                        self._starData = JSON.parse(this.response);
                        self.sendRedrawRequest();
                    }
                    catch (e) {
                        Logger.log(Logger.LEVEL_SEVERE, 'StarFieldLayer unable to parse JSON for star data ' +
                            e.toString());
                    }
                }
                else {
                    Logger.log(Logger.LEVEL_SEVERE, 'StarFieldLayer unable to fetch star data. Status: ' +
                        this.status + ' ' + this.statusText);
                }
                self._loadStarted = false;
            };

            xhr.onerror = function () {
                Logger.log(Logger.LEVEL_SEVERE, 'StarFieldLayer unable to fetch star data');
                self._loadStarted = false;
            };

            xhr.ontimeout = function () {
                Logger.log(Logger.LEVEL_SEVERE, 'StarFieldLayer fetch star data has timeout');
                self._loadStarted = false;
            };

            xhr.open('GET', this._starDataSource, true);
            xhr.send();
        };

        // Internal. Intentionally not documented.
        StarFieldLayer.prototype.createStarsGeometry = function () {
            var indexes = this.parseStarsMetadata(this._starData.metadata);

            if (indexes.raIndex === -1) {
                throw new Error(
                    Logger.logMessage(Logger.LEVEL_SEVERE, 'StarFieldLayer', 'createStarsGeometry',
                        'Missing ra field in star data.'));
            }
            if (indexes.decIndex === -1) {
                throw new Error(
                    Logger.logMessage(Logger.LEVEL_SEVERE, 'StarFieldLayer', 'createStarsGeometry',
                        'Missing dec field in star data.'));
            }
            if (indexes.magIndex === -1) {
                throw new Error(
                    Logger.logMessage(Logger.LEVEL_SEVERE, 'StarFieldLayer', 'createStarsGeometry',
                        'Missing vmag field in star data.'));
            }

            var data = this._starData.data;
            var positions = [];

            this._minMagnitude = Number.MAX_VALUE;
            this._maxMagnitude = Number.MIN_VALUE;

            for (var i = 0, len = data.length; i < len; i++) {
                var starInfo = data[i];
                var declination = starInfo[indexes.decIndex]; //for latitude
                var rightAscension = starInfo[indexes.raIndex]; //for longitude
                var magnitude = starInfo[indexes.magIndex];
                var pointSize = magnitude < 2 ? 2 : 1;

                positions.push(declination, rightAscension, pointSize, magnitude);

                this._minMagnitude = Math.min(this._minMagnitude, magnitude);
                this._maxMagnitude = Math.max(this._maxMagnitude, magnitude);
            }
            this._numStars = Math.floor(positions.length / 4);

            return positions;
        };

        // Internal. Intentionally not documented.
        StarFieldLayer.prototype.parseStarsMetadata = function (metadata) {
            var raIndex = -1,
                decIndex = -1,
                magIndex = -1;
            for (var i = 0, len = metadata.length; i < len; i++) {
                var starMetaInfo = metadata[i];
                if (starMetaInfo.name === 'ra') {
                    raIndex = i;
                }
                if (starMetaInfo.name === 'dec') {
                    decIndex = i;
                }
                if (starMetaInfo.name === 'vmag') {
                    magIndex = i;
                }
            }
            return {
                raIndex: raIndex,
                decIndex: decIndex,
                magIndex: magIndex
            };
        };

        // Internal. Intentionally not documented.
        StarFieldLayer.prototype.invalidateStarData = function () {
            this._starData = null;
            this._starsPositionsVboCacheKey = null;
        };

        // Internal. Intentionally not documented.
        StarFieldLayer.prototype.sendRedrawRequest = function () {
            var e = document.createEvent('Event');
            e.initEvent(WorldWind.REDRAW_EVENT_TYPE, true, true);
            window.dispatchEvent(e);
        };

        return StarFieldLayer;

    });