Source: bu/workspace.js

/**
 * @fileoverview bu.Workspace is an object that manages a collection of
 * {@link bu.Viewer} arranged through a {@link bu.Layout}.
 * Created 27/03/2017.
 * @author josea.hernandez@blom.no (Jose Antonio Hernandez)
 * @copyright Blom Data S.L. 2017
 */
 
goog.provide('bu.Workspace');

goog.require('ol');
goog.require('ol.Object');
goog.require('ol.events');

/**
 * @classdesc
 * The workspace is the core component of BlomUrbex API. It manages a collection
 * of several {@link bu.Viewer} arranged in a {@link bu.Layout}. 
 *
 *     var workspace = new bu.Workspace({
 *         layout: {
 *             name: 'Dual view',
 *             views: [
 *                 {
 *                     column: 0, 
 *                     viewer: new bu.ortho.OpenLayersViewer({
 *                         center: [0, 0],
 *                         zoom: 1
 *                         layers: [
 *                             "OSM"
 *                         ],
 *                         baselayer: bu.Orientation.ORTHO
 *                      })
 *                 },
 *                 {
 *                     column: 1,
 *                     viewer: new bu.street.MarzipanoViewer({
 *                         swfPath: 'swf/marzipano.swf'
 *                     })
 *                 }
 *           ]
 *       },
 *       target: 'workspace'
 *     });
 *
 * The above snippet creates a workspace inside a div element with id 'workspace'
 * and initializes it with a dual-view layout in two columns using an ortho view
 * for the first column and a street view for the second column.
 *
 * The constructor places a viewport container (with CSS class name 
 * `bu-workspace-viewport`) in the target element (see `getViewerContainer()`),
 * and then several containers inside the viewport, one per each different value
 * of column property (with a CSS class name `bu-workspace-container`) and
 * finally several subcontainers inside the previous ones, one per each view with
 * same column value (with a CSS class name `bu-workspace-subcontainer`). This
 * simple but powerful method allow the creation of complex layouts.
 * 
 * @constructor
 * @extends {ol.Object}
 * @param {bu.WorkspaceOptions} options Workspace options.
 * @api
 */
bu.Workspace = function(options) {
    ol.Object.call(this);
    
    /**
     * @private
     * @type {Element}
     */
    this.viewport_ = document.createElement('DIV');
    this.viewport_.className = 'bu-workspace-viewport';
    this.viewport_.style.position = 'relative';
    this.viewport_.style.overflow = 'hidden';
    this.viewport_.style.width = '100%';
    this.viewport_.style.height = '100%';
    // prevent page zoom on IE >= 10 browsers
    this.viewport_.style.msTouchAction = 'none';
    this.viewport_.style.touchAction = 'none';
    
    /**
    * @type {Array.<Element>}
    * @private
    */
    this.containers_ = null;
    
    /**
    * @type {Array.<Element>}
    * @private
    */
    this.subcontainers_ = null;
    
    /**
    * @type {Array.<bu.Viewer>}
    * @private
    */
    this.viewers_ = null;
    
    /**
    * @type {function(Event)|undefined}
    * @private
    */
    this.handleResize_;
    
    /**
    * @private
    * @type {Array.<ol.EventsKey>}
    */
    this.viewerChangeHandlerKeys_ = null;
    
    ol.events.listen(this, ol.Object.getChangeEventType(bu.WorkspaceProperty.TARGET),
        this.handleTargetChanged_, this);
    ol.events.listen(this, ol.Object.getChangeEventType(bu.WorkspaceProperty.LAYOUT),
        this.handleLayoutChanged_, this);

    this.setTarget(options.target);
    //this.setLayout(options.layout);
    this.setupLayout(options.layout);
};
ol.inherits(bu.Workspace, ol.Object);

/**
 * Get the layout using {@link bu.ViewType} in the definition.
 * @return {bu.Layout} The current layout.
 * @observable
 * @api stable
 */
bu.Workspace.prototype.getLayout = function() {
    return this.layout_;
};

/**
 * Get the target in which the workspace is rendered.
 * Note that this returns what is entered as an option or in setTarget:
 * if that was an element, it returns an element; if a string, it returns that.
 * @return {Element|string|undefined} The Element or id of the Element that the
 *     workspace is rendered in.
 * @observable
 * @api stable
 */
bu.Workspace.prototype.getTarget = function() {
    return /** @type {Element|string|undefined} */ (
      this.get(bu.WorkspaceProperty.TARGET));
};

/**
 * Set the target element to render this workspace into.
 * @param {Element|string|undefined} target The Element or id of the Element
 *     that the workspace is rendered in.
 * @observable
 * @api stable
 */
bu.Workspace.prototype.setTarget = function(target) {
    this.set(bu.WorkspaceProperty.TARGET, target);
};


/**
 * Get the DOM element into which this workspace is rendered. In contrast to
 * `getTarget` this method always return an `Element`, or `null` if the
 * workspace has no target.
 * @return {Element} The element that the workspace is rendered in.
 * @api
 */
bu.Workspace.prototype.getTargetElement = function() {
    var target = this.getTarget();
    if (target !== undefined) {
        return typeof target === 'string' ? document.getElementById(target) : target;
    } else {
        return null;
    }
};

/**
 *
 * @inheritDoc
 */
bu.Workspace.prototype.disposeInternal = function() {
    this.setTarget(null);
    ol.Object.prototype.disposeInternal.call(this);
};

/**
 * @private
 */
bu.Workspace.prototype.handleStreetViewChanged_ = function(viewIndex) {
    var layout = this.getLayout();
    if (layout && layout.views.length > 0) {
        var syncedViews = layout.views[viewIndex].syncedViews;
        if (syncedViews && syncedViews.length > 0) {
            var viewer = layout.views[viewIndex].viewer;
            var center = viewer.getCenter();
            var rotation = viewer.getRotation();
            var fov = viewer.getFOV();
            for (var i = 0; i < syncedViews.length; i++) {
                var syncedViewer = layout.views[syncedViews[i]].viewer;
                if (syncedViewer instanceof bu.ortho.Viewer) {
                    if (syncedViewer instanceof bu.ortho.openlayers.Viewer) {
                        syncedViewer.updatePanoramaFOV(center, rotation, 
                            fov, viewIndex);
                        if (syncedViewer.getSyncCenterWithStreetViewers()) {
                            //Careful here: a bu.street.Viewer.getCenter() returns always latlon
                            var projectedCenter = syncedViewer.toCurrentProjection(center, 
                                "EPSG:4326")
                            if (projectedCenter != null) {
                                syncedViewer.setCenter(projectedCenter);
                            }

                        }
                    }
                } else if (syncedViewer instanceof bu.street.Viewer) {
                    syncedViewer.setCenter(center);
                }
            }
        }
    }
}

/**
 * @private
 */
bu.Workspace.prototype.handleOrthoViewChanged_ = function(viewIndex) {
    var layout = this.getLayout();
    if (layout && layout.views.length > 0) {
        var syncedViews = layout.views[viewIndex].syncedViews;
        if (syncedViews && syncedViews.length > 0) {
            var viewer = layout.views[viewIndex].viewer;
            var center = viewer.getCenter();
            var resolution = viewer.getResolution();
            var rotation = viewer.getRotation();
            for (var i = 0; i < syncedViews.length; i++) {
                var syncedViewer = layout.views[syncedViews[i]].viewer;
                if (syncedViewer instanceof bu.ortho.Viewer) {
                    syncedViewer.setCenter(center);
                    syncedViewer.setResolution(resolution);
                    syncedViewer.setRotation(rotation);
                } else if (syncedViewer instanceof bu.street.Viewer) {
                    
                }
            }
        }
    }
};

/**
 * @private
 */
bu.Workspace.prototype.handleOrthoViewPanoramaClicked_ = function(viewIndex, evt) {
    var layout = this.getLayout();
    if (layout && layout.views.length > 0) {
        var syncedViews = layout.views[viewIndex].syncedViews;
        if (syncedViews && syncedViews.length > 0) {
            var viewer = layout.views[viewIndex].viewer;
            for (var i = 0; i < syncedViews.length; i++) {
                var syncedViewer = layout.views[syncedViews[i]].viewer;
                if (syncedViewer instanceof bu.street.Viewer) {
                    syncedViewer.setImageID(evt.image.id);
                }
            }
        }
    }
};

/**
 * @private
 */
bu.Workspace.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) {
        for (i = 0; i < this.divs_.length; i++) {
            ol.dom.removeNode(this.divs_[i]);
        }
        ol.dom.removeNode(this.viewport_);
    } else {
        targetElement.appendChild(this.viewport_);
    }

};

/**
 * @private
 */
//bu.Workspace.prototype.handleLayoutChanged_ = function() {
bu.Workspace.prototype.setupLayout = function(layout) {
    //The final structure of divs would be:
    // div_target
    //     |___ div_container (columns)
    //             |___ div_subcontainer (rows inside each column)
    //                       |___ viewer (for example, an ol.Map div_target)
    
    if (!layout){
        return false;
    }
    
    // We save current layout schema on a local variable
    this.layout_ = layout;
    
    //var layout = this.getLayout();
    if (layout && layout.views && layout.views.length > 0) {
        
        //If previous layout exists remove all content
        if (this.containers_) {
            //Unset change handlers
            if (this.viewerChangeHandlerKeys_) {
                for (var i = 0, ii = this.viewerChangeHandlerKeys_.length; i < ii; ++i) {
                    ol.events.unlistenByKey(this.viewerChangeHandlerKeys_[i]);
                }
                this.viewerChangeHandlerKeys_ = null;
            }
            
            //Remove viewers
            if (this.viewers_) {
                for (var i = 0, ii = this.viewers_.length; i < ii; ++i) {
                    this.viewers_[i].setTarget(null);
                }
                this.viewers_ = null;
            }
            
            //Remove subcontainers
            if (this.subcontainers_) {
                for (var i = 0, ii = this.subcontainers_.length; i < ii; ++i) {
                    ol.dom.removeNode(this.subcontainers_[i]);
                }
                this.subcontainers_ = null;
            }
            
            //Remove column containers
            if (this.containers_) {
                for (var i = 0, ii = this.containers_.length; i < ii; ++i) {
                    ol.dom.removeNode(this.containers_[i]);
                }
                this.containers_ = null;
            }
        }
        
        //Create column containers
        var columnRows = [];
        var maxcolumn = 0;
        for (var i = 0; i < layout.views.length; i++) {
            var column = layout.views[i].column;
            if (maxcolumn < column) maxcolumn = column;
        }
        for (var i = 0; i <= maxcolumn; i++) {
            columnRows[i] = 0;
        }
        for (var i = 0; i < layout.views.length; i++) {
            var column = layout.views[i].column;
            if (column < 0) column = 0;
            columnRows[column] = columnRows[column] + 1;
        }
        var columnCount = maxcolumn + 1;
        var w = (columnCount == 1 ? 100.0 : 100.0 / columnCount);
        if (w != 100.0) w = parseFloat(w.toFixed(2));
        
        this.containers_ = [];
        for (var c = 0; c < columnCount; c++) {
            //Ensure that sum of all column widths equals exactly to 100.0%
            if (columnCount != 1 && c == columnCount - 1) {
                w = 100.0 - w * (columnCount - 1);
            }
            
            this.containers_[c] = document.createElement('DIV');
            this.containers_[c].className = 'bu-workspace-container';
            this.containers_[c].style.position = 'relative';
            this.containers_[c].style.overflow = 'hidden';
            this.containers_[c].style.float = 'left';
            this.containers_[c].style.width = w.toFixed(2) + '%';
            this.containers_[c].style.height = '100%';
            // prevent page zoom on IE >= 10 browsers
            this.containers_[c].style.msTouchAction = 'none';
            this.containers_[c].style.touchAction = 'none';

        }
        
        //Create subcontainers and add to containers
        var rowCount = [];
        this.subcontainers_ = [];
        for (var i = 0; i < layout.views.length; i++) {
            var column = layout.views[i].column;
            
            //Ensure that sum of all row heights equals exactly to 100.0%
            if (typeof rowCount[column] === 'undefined')
                rowCount[column] = 1;
            else
                rowCount[column] = rowCount[column] + 1;
            var h = (columnRows[column] <= 1 ? 
                100.0 : 100.0 / columnRows[column]);
            if (h != 100.0) h = parseFloat(h.toFixed(2));
            if (columnRows[column] != 1 && rowCount[column] == columnRows[column]) {
                h = 100.0 - h * (columnRows[column] - 1);
            }
            
            this.subcontainers_[i] = document.createElement('DIV');
            this.subcontainers_[i].className = 'bu-workspace-subcontainer';
            this.subcontainers_[i].style.position = 'relative';
            this.subcontainers_[i].style.overflow = 'hidden';
            this.subcontainers_[i].style.width = '100%';
            this.subcontainers_[i].style.height = h.toFixed(2) + '%';
            // prevent page zoom on IE >= 10 browsers
            this.subcontainers_[i].style.msTouchAction = 'none';
            this.subcontainers_[i].style.touchAction = 'none';
            this.containers_[column].appendChild(this.subcontainers_[i]);
            
        }
        
        //Add containers to viewport
        if (this.viewport_) 
            for (var i = 0; i < this.containers_.length; i++) 
                this.viewport_.appendChild(this.containers_[i]);
            
        //Add viewers to subcontainers if they are defined
        this.viewers_ = [];
        for (var i = 0; i < layout.views.length; i++) {
            var viewer = layout.views[i].viewer;
            if (viewer) {
                this.viewers_.push(viewer);
                viewer.setTarget(this.subcontainers_[i]);
            }
        }
        
        //Set change handlers
        this.viewerChangeHandlerKeys_ = [];
        for (var i = 0; i < layout.views.length; i++) {                
            var viewer = layout.views[i].viewer;
            if (viewer instanceof bu.ortho.Viewer) {
                this.viewerChangeHandlerKeys_.push(
                    ol.events.listen(viewer, ol.Object.getChangeEventType(
                        bu.ortho.ViewerProperty.CENTER),
                        this.handleOrthoViewChanged_.bind(this, i))                  
                );
                this.viewerChangeHandlerKeys_.push(
                    ol.events.listen(viewer, ol.Object.getChangeEventType(
                        bu.ortho.ViewerProperty.RESOLUTION),
                        this.handleOrthoViewChanged_.bind(this, i))
                );
                this.viewerChangeHandlerKeys_.push(
                    ol.events.listen(viewer, ol.Object.getChangeEventType(
                        bu.ortho.ViewerProperty.ROTATION),
                        this.handleOrthoViewChanged_.bind(this, i))
                );
                this.viewerChangeHandlerKeys_.push(
                    ol.events.listen(viewer, 
                        bu.ortho.ViewerEventType.PANORAMACLICKED,
                        this.handleOrthoViewPanoramaClicked_.bind(this, i))
                );
            } else if (viewer instanceof bu.street.Viewer) {
                this.viewerChangeHandlerKeys_.push(
                    ol.events.listen(viewer, ol.Object.getChangeEventType(
                        bu.street.ViewerProperty.IMAGEID),
                        this.handleStreetViewChanged_.bind(this, i))
                );
                this.viewerChangeHandlerKeys_.push(
                    ol.events.listen(viewer, ol.Object.getChangeEventType(
                        bu.street.ViewerProperty.ROTATION),
                        this.handleStreetViewChanged_.bind(this, i))
                );
                this.viewerChangeHandlerKeys_.push(
                    ol.events.listen(viewer, ol.Object.getChangeEventType(
                        bu.street.ViewerProperty.FOV),
                        this.handleStreetViewChanged_.bind(this, i))
                );
                this.viewerChangeHandlerKeys_.push(
                    ol.events.listen(viewer, ol.Object.getChangeEventType(
                        bu.street.ViewerProperty.PITCH),
                        this.handleStreetViewChanged_.bind(this, i))
                );
                this.viewerChangeHandlerKeys_.push(
                    ol.events.listen(viewer, ol.Object.getChangeEventType(
                        bu.street.ViewerProperty.YAW),
                        this.handleStreetViewChanged_.bind(this, i))
                );
            }
        }
    }
};