/**
* @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 © 2017 Blom ";
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));
}