Source: bu-street/marzipanoviewer.js

/**
 * @fileoverview bu.street.MarzipanoViewer is 360 viewer that uses internally a 
 * Marzipano.Viewer class from Marzipano to show 360 panorama imagery.
 * Created 23/03/2017.
 * @author josea.hernandez@blom.no (Jose Antonio Hernandez)
 * @author rafael.delaviuda@blom.no (Rafael de la Viuda)
 * @author raul.sangil@blom.no (Raul Sangil)
 * @copyright Blom Data S.L. 2017
 */

goog.provide('bu.street.marzipano.Viewer');

goog.require('ol');
goog.require('ol.Object');
goog.require('bu');
goog.require('bu.street.Viewer');
goog.require('bu.street.Services');
goog.require('bu.street.ImagesManager');


/**
 * @classdesc
 * This is 360 viewer that uses internally a Marzipano.Viewer class from Marzipano 
 * to show 360 panorama imagery.
 *
 * @constructor
 * @extends {bu.street.Viewer}
 * @param {bu.street.marzipano.ViewerOptions} options Viewer options.
 * @api
 */
bu.street.marzipano.Viewer = function(options) {
    this.usertoken = (options.usertoken !== undefined ? options.usertoken : '');
    
    var baseOptions = ol.obj.assign({}, options);
    delete baseOptions.target;
    delete baseOptions.imageid;
    delete baseOptions.center;
    
    bu.street.Viewer.call(this, baseOptions);
    this.marzipanoViewer_ = null;
    this.lastImageRotation_ = null;
    this.lastImageFOV_ = null;
    //TODO initialize this values in degrees that cover a given extent in meters
    this.offset_ = 0.0005;
    this.offsetSmall_ = 0.001;
    this.requestAlwaysHotspots_ = true;

    ol.events.listen(this, ol.Object.getChangeEventType(bu.street.ViewerProperty.IMAGEID),
        this.handleImageIDChanged_, this);
            
    ol.events.listen(this, ol.Object.getChangeEventType(bu.ViewerProperty.TARGET),
        this.handleTargetChanged_, this);
        
    ol.events.listen(this, ol.Object.getChangeEventType(bu.street.ViewerProperty.ROTATION),
        this.handleRotationChanged_, this);

    ol.events.listen(this, ol.Object.getChangeEventType(bu.street.ViewerProperty.PITCH),
        this.handlePitchChanged_, this);
        
    ol.events.listen(this, ol.Object.getChangeEventType(bu.street.ViewerProperty.YAW),
        this.handleYawChanged_, this);
    
    if (options.imageid){
        this.setImageID(options.imageid);
    } else if (options.center){
        this.requestImagesByCenter(options.center);
    }
};
ol.inherits(bu.street.marzipano.Viewer, bu.street.Viewer);


/**
 * @private
 * @param {bu.Coordinate} center The coordinate center of the bounding box to 
 * search images in.
 * @param {number} offset The offset to apply to the coordinate in order to 
 * generate the bounding box.
 */
bu.street.marzipano.Viewer.prototype.requestImagesByCenter = function(center, offset) {
    offset = (offset ? offset : this.offset_);
    var extent = [
        center[0] - offset, 
        center[1] - offset, 
        center[0] + offset, 
        center[1] + offset
    ];
    
    this.servicesManager_.findStreetImagesByExtent(extent, 
        this.successRequestImagesByCenter.bind(this, null, center), 
        this.errorRequestImagesByCenter.bind(this));
};

/**
 * @private
 * @param {string} imageId The identificator of the image to be loaded into viewer.
 */
bu.street.marzipano.Viewer.prototype.handleHotspotClicked_ = function(imageId) {
    this.setImageID(imageId);
}

/**
 * Executed when observable variable pitch is changed. It update viewer pitch.
 * @private
 */
bu.street.marzipano.Viewer.prototype.handlePitchChanged_ = function() {
    if (this.marzipanoViewer_) {
        var view = this.marzipanoViewer_.view();
        if (view) {
            var pitch = this.getPitch();    
            view.setPitch(pitch);              
        }
    }
};

/**
 * Executed when observable variable yaw is changed. It update viewer yaw.
 * @private
 */
bu.street.marzipano.Viewer.prototype.handleYawChanged_ = function() {
    if (this.marzipanoViewer_) {
        var view = this.marzipanoViewer_.view();
        if (view) {
            var yaw = this.getYaw();
            view.setYaw(yaw);
        }
    }
};

/**
 * Executed when observable variable rotation is changed. It update viewer yaw.
 * @private
 */
bu.street.marzipano.Viewer.prototype.handleRotationChanged_ = function() {
    var imageId = this.getImageID();
    var image = this.imagesManager_.getImageById(imageId);
    if (image != null) {
        if (this.marzipanoViewer_) {
            var view = this.marzipanoViewer_.view();
            if (view) {
                var rotation_deg = this.getRotation();
                var yaw_deg = rotation_deg - image.direction_yaw;
                var yaw = (yaw_deg * Math.PI) / 180.0;
                // Constrain yaw, pitch and roll to the [-π, π] interval using marzipano mod
                yaw = this.mod(yaw - Math.PI, -2*Math.PI) + Math.PI;
                view.setYaw(yaw);
            }
        }
       
    }
};

/**
 * Loads an image into Marzipano viewer.
 * @param {bu.street.Image} image The image.
 */
bu.street.marzipano.Viewer.prototype.openImage = function(image) {
    var scene = this.findScene(image);
    this.setLabelInfoData(image, this);
    if (scene == null) {
                
        var resMatrix = this.calculateFaceSize_(
            image.tile_size, image.zoom_levels, image.min_zoom_level_size);
        var levels = new Array();
        levels.push({
            "tileSize": 256,
            "size": 256,
            "fallbackOnly": true
        });
        var i;
        for (i = 1; i < resMatrix.length; i++) {
            var actual = {
                "tileSize": parseInt(image.tile_size),
                "size": resMatrix[i]
            };
            levels.push(actual);
        }
        var faceSize = resMatrix[i - 1] * 2;
        var geometry = new Marzipano.CubeGeometry(levels);
        var limiter = new Marzipano.RectilinearView.limit.traditional(
            faceSize, 120 * Math.PI / 180, 150 * Math.PI / 180); 
        var urlPrefix = bu.SERVER + "GetStreetTile?";
        var source = Marzipano.ImageUrlSource.fromString(
            urlPrefix + "usertoken=" + this.getUsertoken() + "&" + 
            "id=" + image.id + "&" + 
            "tile=" + "{f}_{z}_{y}_{x}", {
            cubeMapPreviewUrl: null
        });
        
        //Calculate yaw
        var initialYaw = 0.0;
        if (this.lastImageRotation_ != null) {
            var init_yaw_deg = this.lastImageRotation_ - image.direction_yaw;
            var init_yaw_rad = (init_yaw_deg * Math.PI * 2) / 360.0;
            // Constrain yaw, pitch and roll to the [-π, π] interval using marzipano mod
            initialYaw = this.mod(init_yaw_rad - Math.PI, -2*Math.PI) + Math.PI;
        }
        
        //Calculate fov
        var fov = (this.lastImageFOV_ == null ? Math.PI / 2 : this.lastImageFOV_);
        
        var initialViewParameters = {
            "pitch": 0.0,
            "yaw": initialYaw,
            "fov": fov
        };
        var view = new Marzipano.RectilinearView(initialViewParameters, limiter);
        var objScene = {
            source: source,
            geometry: geometry,
            view: view
        };
        
        view.addEventListener('change', this.handleSceneViewChanged_.bind(this, image));
        scene = this.marzipanoViewer_.createScene(objScene);
        scene.id = image.id;
        
        // Add hotspots to hotspotContainer of the scene if hotspots are defined
        if ((image.hotspots) && (image.hotspots.length > 0)) {
            var container = scene.hotspotContainer();
            
            for (var index = 0; index < image.hotspots.length; index++) {
                var hotspot = image.hotspots[index];
                //if (hotspot.id != "C+018.080086_59.282620_160807") continue;
                var distance = bu.street.calculateDistance([hotspot.xcp, hotspot.ycp], [image.xcp, image.ycp]);
                var localCoord = [hotspot.local_xcp, hotspot.local_ycp, hotspot.zcp - image.camera_height];
                
                var phiTheta = bu.street.fromLocalCoordToPhiTheta(localCoord, image);
                var pitchYaw = bu.street.phiThetaToPitchYaw(phiTheta, image);
                //pitchYaw = [0.288765, -0.593102];
                var yaw_deg = pitchYaw[1] * bu.M_RAD_TO_DEG;

                var boresight = 90.0 - image.kappa - image.direction_yaw;
                var finalYaw_deg = yaw_deg + boresight;
                
                finalYaw = this.mod((finalYaw_deg * bu.M_DEG_TO_RAD) - Math.PI, -2*Math.PI) + Math.PI;
                
                var position = {
                    yaw         : finalYaw,
                    pitch       : pitchYaw[0],
                    //rotation    : 0, 
                    target      : image.hotspots[index].id
                };
                
                var divHotspot = document.createElement("div");
                divHotspot.className += "bu-street-hotspot-navigation";
                divHotspot.id = hotspot.id;
                
                var radius = distance * 100;
                if (radius < 300){
                    radius = 300;
                }
                var extraRotations = ((distance / 10) * 70).toFixed(0);
                
                if (extraRotations < 50){
                    extraRotations = 50;
                }
                
                if (extraRotations > 75){
                    extraRotations = 75;
                }
                
                //console.info("distance " + distance + " extra rotation " + extraRotations + " radius " + radius );
                
                divHotspot.addEventListener('click', this.handleHotspotClicked_.bind(this, hotspot.id));
                
                var extraOptions = { 
                    perspective: { 
                        radius: radius, 
                        //extraRotations: "rotateX(70deg)" 
                        extraRotations: "rotateX(" + extraRotations + "deg)"
                    }
                };                
                container.createHotspot(divHotspot, position, extraOptions);
            }
        }
    }    
    scene.switchTo();
};

/**
 * Handler to load a image when observable variable imageId changes.
 * @private
 */
bu.street.marzipano.Viewer.prototype.handleImageIDChanged_ = function() {
    var imageId = this.getImageID();
    var image = this.imagesManager_.getImageById(imageId);
    if (image == null) {
        this.servicesManager_.getStreetMetadata(
            imageId, this.successGetMetadata_.bind(this, null), 
            this.errorGetMetadata_.bind(this));
    } else {
        this.openImage(image);
    }
};

/**
 * Gets the scene with the image or null if not exists.
 * @private 
 * @param {bu.street.Image} Image to find Scene for.
 * @return {Scene} Scene with the image requested or null if it not exists.
 */
bu.street.marzipano.Viewer.prototype.findScene = function(image) {
    if (this.marzipanoViewer_ != null){
        var max = this.marzipanoViewer_.listScenes().length;
        
        for (var index = 0; index<max; index++){
            if (this.marzipanoViewer_._scenes[index].id == image.id){
                return this.marzipanoViewer_._scenes[index];
            }
        }
    }
    return null;
};

/**
 * @private
 * This handler is executed when the div where street viewer is loaded, it is,
 * when the div is going to be destroyed or created.
 */
bu.street.marzipano.Viewer.prototype.handleTargetChanged_ = function() {
    // target may be undefined, null, a string or an Element.
    // If it's a string we convert it to an Element before proceeding.
    // If it's not now an Element we remove the div elements from the DOM.
    // If it's an Element we append the div elements to it.

    var targetElement;
    if (this.getTarget()) {
        targetElement = this.getTargetElement();
    }

    if (!targetElement) {
        if (this.marzipanoViewer_ !== null) {
            if (this.marzipanoViewer_._scenes) {
                for (var i = 0; i < this.marzipanoViewer_._scenes.length; i++) {
                    var scene = this.marzipanoViewer_._scenes[i];
                    if (scene) {
                        var view = scene.view();
                        if (view) {
                            view.removeEventListener('change', this.handleSceneViewChanged_);
                        }
                        var container = scene.hotspotContainer();
                        var hotspots = container.listHotspots();
                        for(var j=0; j < hotspots.length; j++){
                            hotspots[j].domElement().removeEventListener('click', this.handleHotspotClicked_);
                        }                        
                    }
                }                
            }
        }
        this.marzipanoViewer_ = null;
    } else {
        var streetElement = document.createElement("div");
        streetElement.id = "bu-street";

        targetElement.appendChild(streetElement);

        this.setLabelInfo(targetElement);    
        this.marzipanoViewer_ = new Marzipano.Viewer(streetElement, {
            controls :  {mouseViewMode: "drag"}
        });
        //Marzipano events are captured in view object inside each scene and in
        //  hotspot objects inside a hotspot container
    }
};

/**
 * @private
 * @param {number} tileSize The init level tile size (tipically 256 pixels).
 * @param {number} zoomLvl Number of zoom levels of the image to be loaded.
 * @param {number} minZoomLvl Number of tiles per cube size of the lower zoom level.
 * Calculates the pixel size of each level of the cube and returns this sizes 
 * into an array.
 */
bu.street.marzipano.Viewer.prototype.calculateFaceSize_ = function(tileSize, zoomLvl, minZoomLvl) {
    var minSize = tileSize * Math.pow(2, minZoomLvl - 1);
    var size = [minSize];
    for (var i = 0; i < zoomLvl - 1; i++) {
        var x = size[i] * 2;
        size.push(x);
    }
    return size;
};

/**
 * @private
 * @param {bu.street.Image} image The image we want to load.
 */
bu.street.marzipano.Viewer.prototype.handleSceneViewChanged_ = function(image) {
    var view = this.marzipanoViewer_.view();
    if (view != null) {
        var yaw = view.yaw();
        this.setYaw(yaw);
        
        //Add direction_yaw to adjust rotation to north
        var rotation_deg = (view.yaw() * 180.0 / Math.PI) + image.direction_yaw;
        rotation_deg = this.mod(rotation_deg, 360.0);
        if (rotation_deg < 0.0) rotation_deg += 360.0;
        this.lastImageRotation_ = rotation_deg;
        this.setRotation(rotation_deg);
        
        //Convert FOV to degrees
        var fov_deg = view.fov() * 180.0 / Math.PI;
        fov_deg = this.mod(fov_deg, 360.0);
        if (fov_deg < 0.0) fov_deg += 360.0;
        this.lastImageFOV_ = fov_deg;
        this.setFOV(fov_deg);
        
        this.setPitch(view.pitch());
    }
};

/**
 * @private
 * @param {?bu.street.image} image The image for which this request GetMetadata is
 *  done or null if no image is currently known.
 * @param {bu.Coordinate} The center used to do the request.
 * @param {Event} event The load event.
 * @callback
 */
bu.street.marzipano.Viewer.prototype.successGetMetadata_ = function(image, event) {
    var client = /** @type {XMLHttpRequest} */ (event.target);
    // status will be 0 for file:// urls
    if (!client.status || client.status >= 200 && client.status < 300) {
        var returnedImage = JSON.parse(client.response);
        var id = returnedImage.idPanorama;
        delete returnedImage.idPanorama;
        returnedImage.id = id;
        if (image == null) {
            if (this.requestAlwaysHotspots_) {
                var center = [returnedImage.xcp, returnedImage.ycp];
                var extent = [
                    center[0] - this.offset_, 
                    center[1] - this.offset_, 
                    center[0] + this.offset_, 
                    center[1] + this.offset_
                ];
                
                this.servicesManager_.findStreetImagesByExtent(extent, 
                    this.successRequestImagesByCenter.bind(this, returnedImage, center), 
                    this.errorRequestImagesByCenter.bind(this));
            }
        } else {
            returnedImage.hotspots = [];
            var hotspots = image.hotspots;
            if (hotspots && hotspots.length > 0) {
                for(var i = 0; i < hotspots.length; i++){
                    var hotspot = hotspots[i];
                    var hs = {
                        id        :  hotspot.id,
                        xcp       :  hotspot.xcp,
                        ycp       :  hotspot.ycp,
                        zcp       :  hotspot.zcp,
                        local_xcp :  hotspot.local_xcp,
                        local_ycp :  hotspot.local_ycp,
                        type      :  "navigation"
                    };
                    returnedImage.hotspots.push(hs);
                }                
            }
            this.imagesManager_.addImage(returnedImage);
            this.setImageID(returnedImage.id);
        }
    }
};

/**
 * @private
 * @callback
 * @param {Event} event The error event.
 */
bu.street.marzipano.Viewer.prototype.errorGetMetadata_ = function(event){

};

/**
 * @callback
 * @private
 * @param {?bu.street.image} image The image for which this request ByCenter is
 *  done or null if no image is currently known.
 * @param {bu.Coordinate} The center used to do the request.
 * @param {Event} event The load event.
 */
bu.street.marzipano.Viewer.prototype.successRequestImagesByCenter = function(image, center, event) {
    var client = /** @type {XMLHttpRequest} */ (event.target);
    // status will be 0 for file:// urls
    if (!client.status || client.status >= 200 && client.status < 300) {
        var imgs = JSON.parse(client.response);
        if (imgs) {
            var nearestImage = image;
            var mindd = Infinity;
            //Loop through response images
            for(var i = 0; i < imgs.length; i++){
                var img = imgs[i];
                //Fix id name
                var id = img.idPanorama;
                delete img.idPanorama;
                img.id = id;
                if (image == null) {
                    var dd = Math.pow(img.xcp - center[0] , 2)
                        + Math.pow(img.ycp - center[1] , 2);
                    if (dd < mindd) {
                        mindd = dd;
                        nearestImage = img;
                    }                    
                }
            }

            if (nearestImage != null) {
                nearestImage.hotspots = [];
                // Loop to set hotspots for nearest image
                for(var i = 0; i < imgs.length; i++){
                    var img = imgs[i];
                    if (nearestImage.id != img.id) {
                        var hs = {
                            id        :  img.id,
                            xcp       :  img.xcp,
                            ycp       :  img.ycp,
                            zcp       :  img.zcp,
                            local_xcp :  img.local_xcp,
                            local_ycp :  img.local_ycp,
                            type      :  "navigation"
                        };
                        nearestImage.hotspots.push(hs);
                    }
                }
                if (image == null) {
                    this.servicesManager_.getStreetMetadata(
                        nearestImage.id, this.successGetMetadata_.bind(this, nearestImage), 
                        this.errorGetMetadata_.bind(this));                    
                } else {
                    this.imagesManager_.addImage(nearestImage);
                    var imageId = this.getImageID();
                    if (nearestImage.id == imageId) {
                        this.openImage(nearestImage);
                    } else {
                        this.setImageID(nearestImage.id);
                    }                        
                }
            }
        }
    }
};

/**
 * Callback executed when a AJAX request for images by center fails.
 * @param {Event} event The error event.
 * @private
 * @callback
 */
bu.street.marzipano.Viewer.prototype.errorRequestImagesByCenter = function(event) {

};

/**
 * Clones this viewer reinitializing the new one with main values of current one.
 * @return {bu.street.marzipano.Viewer} The viewer cloned.
 * @api
 */
bu.street.marzipano.Viewer.prototype.clone = function() {
    //TODO
};

/**
 * @private
 * @param {number} a The value of a
 * @param {number} b The value of b
 * @return {number} 
 */
bu.street.marzipano.Viewer.prototype.mod = function (a, b) {
  return (+a % (b = +b) + b) % b;
}


/**
 * Set the label info control.
 * @private
 * @param {element} targetElement HTML Element
 */
bu.street.marzipano.Viewer.prototype.setLabelInfo = function (targetElement){
    var icono = document.createElement("div");
        icono.id = "bu-street-logo";

        icono.addEventListener("click", function(){
            menu.classList.toggle("enable");

        });

        var menu = document.createElement("menu");
        menu.id = "bu-street-menuInfo";

        var list = document.createElement("ul");
        var imgName = document.createElement("li");
        imgName.id="bu-street-imgName";
        
        list.appendChild(imgName);

        var date = document.createElement("li");
        date.id = "bu-street-dateShoot";
        
        list.appendChild(date);

        var cpr = document.createElement("div");
        cpr.id = "bu-street-copyright";

        var txtCpr = document.createElement("small");
        txtCpr.innerHTML = "Copyright &copy; 2017 Blom&nbsp;";
        cpr.appendChild(txtCpr);

        var arrow = document.createElement("div");
        arrow.id = "bu-street-arrow";

         arrow.addEventListener("click", function(){
            arrow.classList.toggle("enable");
            angles.classList.toggle("enable");

        });

        var angles = document.createElement("div");
        angles.id = "bu-srteet-viewerAngles";


        menu.appendChild(list);
        menu.appendChild(arrow);
        menu.appendChild(angles);

        targetElement.appendChild(icono);
        targetElement.appendChild(menu);
        targetElement.appendChild(cpr);
};

/**
 * Set info data into some DOM element.
 * @private
 * @param {bu.street.Image} image Image object to fetch data from.
 */
bu.street.marzipano.Viewer.prototype.setLabelInfoData = function (image, viewer) {
    document.getElementById("bu-street-imgName").innerHTML = "<strong>Image Name: </strong><br>"
        + image.id;

    document.getElementById("bu-street-dateShoot").innerHTML = "<strong>Date: </strong><br>"
        + image.shotDate;

                
    viewer.on(['change:imageid', 'change:center', 'change:rotation', 'change:fov', 
        'change:pitch', 'change:yaw'], function(evt) {
        document.getElementById('bu-srteet-viewerAngles').innerHTML = 
            '<strong>Rot: </strong>' + this.getRotation().toFixed(2) + " <strong>FOV: </strong>" + this.getFOV().toFixed(2) + " <strong>Pitch: </strong>" + this.getPitch().toFixed(2) +
            " <strong>Yaw: </strong>" + this.getYaw().toFixed(2);
    }.bind(viewer));
}