/** * vtkWeb JavaScript Library. * * This module allow the Web client to create viewport to vtkWeb views. * Those viewport are interactive windows that are used to render 2D/3D content * and response to user mouse interactions. * * This module registers itself as: 'vtkweb-viewport' * * @class vtkWeb.Viewport */ (function (GLOBAL, $) { // ---------------------------------------------------------------------- // Viewport constants // ---------------------------------------------------------------------- var DEFAULT_RENDERERS_CONTAINER_HTML = "
", DEFAULT_RENDERERS_CONTAINER_CSS = { "position": "absolute", "top": "0px", "left": "0px", "right": "0px", "bottom": "0px", "z-index" : "0", "overflow": "hidden" }, DEFAULT_MOUSE_LISTENER_HTML = "
", DEFAULT_MOUSE_LISTENER_CSS = { "position": "absolute", "top": "0px", "left": "0px", "right": "0px", "bottom": "0px", "z-index" : "3" }, DEFAULT_STATISTIC_HTML = "
", DEFAULT_STATISTIC_CSS = { "position": "absolute", "top": "0px", "left": "0px", "right": "0px", "bottom": "0px", "z-index" : "2", "display" : "none" }, DEFAULT_OVERLAY_HTML = "", DEFAULT_OVERLAY_CSS = { "position": "absolute", "top": "0px", "left": "0px", "right": "0px", "bottom": "0px", "z-index" : "1" }, module = {}, DEFAULT_VIEWPORT_OPTIONS = { session: null, view: -1, enableInteractions: true, renderer: 'image' }; // ---------------------------------------------------------------------- // Mouse interaction helper methods for viewport // ---------------------------------------------------------------------- function preventDefault(event) { event.preventDefault(); } // ---------------------------------------------------------------------- function attachMouseListener(mouseListenerContainer, renderersContainer, overlayContainer, viewport) { var current_button = null, area = [0,0,0,0], vp = viewport, overlayCtx2D = overlayContainer[0].getContext('2d'); // Draw rectangle in overlay function clearOverlay() { if(overlayCtx2D !== null) { overlayCtx2D.canvas.width = mouseListenerContainer.width(); overlayCtx2D.canvas.height = mouseListenerContainer.height(); overlayCtx2D.clearRect(0,0,overlayCtx2D.canvas.width,overlayCtx2D.canvas.height); } } function redrawSelection() { clearOverlay(); if(overlayCtx2D !== null) { overlayCtx2D.strokeStyle="#FFFFFF"; var x1 = Math.min(area[0],area[2]); var y1 = Math.min(area[1],area[3]); var x2 = Math.max(area[0],area[2]); var y2 = Math.max(area[1],area[3]); var width = Math.abs(x2 - x1); var height = Math.abs(y2 - y1); overlayCtx2D.rect(x1, overlayContainer.height()-y2, width, height); overlayCtx2D.stroke(); } } function extractCoordinates(event, start, reorder) { var elem_position = $(event.delegateTarget).offset(), height = $(event.delegateTarget).height(), x = (event.pageX - elem_position.left), y = (event.pageY - elem_position.top), offset = start ? 0 : 2; area[0 + offset] = x; area[1 + offset] = height - y; if(reorder) { // Re-order area var newArea = [ Math.min(area[0], area[2]), Math.min(area[1], area[3]), Math.max(area[0], area[2]), Math.max(area[1], area[3]) ]; area[0] = newArea[0]; area[1] = newArea[1]; area[2] = newArea[2]; area[3] = newArea[3]; } } // Internal method used to pre-process the interaction to standardise it // for a vtkWeb usage. function mouseInteraction(event) { if(event.hasOwnProperty("type")) { if(event.type === 'mouseup') { current_button = null; if (vp.getSelectionMode() === true) { clearOverlay(); } renderersContainer.trigger($.extend(event, { type: 'mouse', action: 'up', current_button: current_button })); extractCoordinates(event, false, true); renderersContainer.trigger({ type: 'endInteraction', area: area }); } else if(event.type === 'mousedown') { current_button = event.which; // Override button if modifier is used // Middle: Alt - Right: Shift if(event.shiftKey) { current_button = 3; event.shiftKey = false; } else if(event.altKey) { current_button = 2; event.altKey = false; } extractCoordinates(event, true, false); renderersContainer.trigger('startInteraction'); renderersContainer.trigger($.extend(event, { type: 'mouse', action: 'down', current_button: current_button })); } else if(event.type === 'mousemove' && current_button != null) { if (vp.getSelectionMode() === true) { extractCoordinates(event, false, false); redrawSelection(); } renderersContainer.trigger($.extend(event, { type: 'mouse', action: 'move', current_button: current_button })); } } } // Bind listener to UI container mouseListenerContainer.bind("contextmenu mouseover click", preventDefault); mouseListenerContainer.bind('mousedown mouseup mousemove', mouseInteraction); mouseListenerContainer.dblclick(function(event){ renderersContainer.trigger($.extend(event, { type: 'mouse', action: 'dblclick', current_button: event.which })); }); mouseListenerContainer.bind("DOMMouseScroll mousewheel",function(event){ var scrollValue = (event.originalEvent.wheelDeltaY || -event.originalEvent.detail); renderersContainer.trigger($.extend(event, { type: 'mouse', action: 'scroll', current_button: current_button, scroll: scrollValue })); }); } // ---------------------------------------------------------------------- function attachTouchListener(mouseListenerContainer, renderersContainer, viewport) { var current_button = null, posX, posY, defaultDragButton = 1, isZooming = false, isDragging = false, mouseAction = 'up', target; function mobileTouchInteraction(evt) { evt.gesture.preventDefault(); switch(evt.type) { case 'drag': if(isZooming) { return; } current_button = defaultDragButton; if(mouseAction === 'up') { mouseAction = "down"; target = evt.gesture.target; isDragging = true; } else { mouseAction = "move"; } posX = evt.gesture.touches[0].pageX; posY = evt.gesture.touches[0].pageY; break; case 'hold': if(defaultDragButton === 1) { defaultDragButton = 2; mouseListenerContainer.html("Pan mode").css('color','#FFFFFF'); } else { defaultDragButton = 1; mouseListenerContainer.html("Rotation mode").css('color','#FFFFFF'); } break; case 'release': mouseListenerContainer.html(''); current_button = 0; mouseAction = "up"; isZooming = false; isDragging = false; break; case 'doubletap': viewport.resetCamera(); return; case 'pinch': if(isDragging) { return; } current_button = 3; if(mouseAction === 'up') { mouseAction = 'down'; posX = 0; posY = mouseListenerContainer.height(); target = evt.gesture.target; isZooming = true; } else { mouseAction = 'move'; posY = mouseListenerContainer.height() * (1+(evt.gesture.scale-1)/2); } break; } //mouseListenerContainer.html(mouseAction + ' (' + posX + ', ' + posY + ') b:' + current_button + ' z: ' + isZooming ).css('color','#FFFFFF'); // Trigger event renderersContainer.trigger({ type: 'mouse', action: mouseAction, current_button: current_button, charCode: '', altKey: false, ctrlKey: false, shiftKey: false, metaKey: false, delegateTarget: target, pageX: posX, pageY: posY }); } // Bind listener to UI container mouseListenerContainer.hammer({ prevent_default : true, prevent_mouseevents : true, transform : true, transform_always_block : true, transform_min_scale : 0.03, transform_min_rotation : 2, drag : true, drag_max_touches : 1, drag_min_distance : 10, swipe : false, hold : true // To switch from rotation to pan }).on("doubletap pinch drag release hold", mobileTouchInteraction); } // ---------------------------------------------------------------------- // Viewport statistic manager // ---------------------------------------------------------------------- function createStatisticManager() { var statistics = {}, formatters = {}; // Fill stat formatters for(var factoryKey in vtkWeb.ViewportFactory) { var factory = vtkWeb.ViewportFactory[factoryKey]; if(factory.hasOwnProperty('stats')) { for(var key in factory.stats) { formatters[key] = factory.stats[key]; } } } function handleEvent(event) { var id = event.stat_id, value = event.stat_value, statObject = null; if(!statistics.hasOwnProperty(id) && formatters.hasOwnProperty(id)) { if(formatters[id].type === 'time') { statObject = statistics[id] = createTimeValueRecord(); } else if (formatters[id].type === 'value') { statObject = statistics[id] = createValueRecord(); } } else { statObject = statistics[id]; } if(statObject != null) { statObject.record(value); } } // ------------------------------------------------------------------ function toHTML() { var buffer = createBuffer(), hasContent = false, key, formater, stat, min, max; // Extract stat data buffer.append(""); buffer.append(""); for(key in statistics) { if(formatters.hasOwnProperty(key) && statistics[key].valid) { formater = formatters[key]; stat = statistics[key]; hasContent = true; // The functiion may swap the order min = formater.convert(stat.min); max = formater.convert(stat.max); buffer.append(""); } } buffer.append("
Timed QuantityCurrentMinMaxAverage
"); buffer.append(formater.label); buffer.append(""); buffer.append(formater.convert(stat.value)); buffer.append(""); buffer.append((min < max) ? min : max); buffer.append(""); buffer.append((min > max) ? min : max); buffer.append(""); buffer.append(formater.convert(stat.getAverageValue())); buffer.append("
"); return hasContent ? buffer.toString() : ""; } // ------------------------------------------------------------------ return { eventHandler: handleEvent, toHTML: toHTML, reset: function() { statistics = {}; } } } // ---------------------------------------------------------------------- function createBuffer() { var idx = -1, buffer = []; return { clear: function(){ idx = -1; buffer = []; }, append: function(str) { buffer[++idx] = str; return this; }, toString: function() { return buffer.join(''); } }; } // ---------------------------------------------------------------------- function createTimeValueRecord() { var lastTime, sum, count; // Default values lastTime = 0; sum = 0; count = 0; return { value: 0.0, valid: false, min: +1000000000.0, max: -1000000000.0, record: function(v) { if(v === 0) { this.start(); } else if (v === 1) { this.stop(); } }, start: function() { lastTime = new Date().getTime(); }, stop: function() { if(lastTime != 0) { this.valid = true; var time = new Date().getTime(); this.value = time - lastTime; this.min = (this.min < this.value) ? this.min : this.value; this.max = (this.max > this.value) ? this.max : this.value; // sum += this.value; count++; } }, reset: function() { count = 0; sum = 0; lastTime = 0; this.value = 0; this.min = +1000000000.0; this.max = -1000000000.0; this.valid = false; }, getAverageValue: function() { if(count == 0) { return 0; } return Math.floor(sum / count); } } } // ---------------------------------------------------------------------- function createValueRecord() { var sum = 0, count = 0; return { value: 0.0, valid: false, min: +1000000000.0, max: -1000000000.0, record: function(v) { this.valid = true; this.value = v; this.min = (this.min < this.value) ? this.min : this.value; this.max = (this.max > this.value) ? this.max : this.value; // sum += this.value; count++; }, reset: function() { count = 0; sum = 0; this.value = 0; this.min = +1000000000.0; this.max = -1000000000.0; this.valid = false; }, getAverageValue: function() { if(count === 0) { return 0; } return Math.floor(sum / count); } } } // ---------------------------------------------------------------------- // Viewport container definition // ---------------------------------------------------------------------- /** * Create a new viewport for a vtkWeb View. * The options are explained below. * * @member vtkWeb.Viewport * @param {Object} options * Configure the viewport to create the way we want. * * options = { * session: sessionObject, // Object used to communicate with the remote server. * view: -1, // -1 for active view or use the proper viewId * enableInteractions: true, // True if mouse interaction should be forwarded to the server * renderer: 'image' // Type of renderer to be used. Can only be 'image' 'webgl' or 'vgl'. * // --- image renderer options * interactiveQuality: 30, // StillRender quality when interacting * stillQuality: 100, // StillRender quality when not interacting * // --- webgl/vgl renderer options * keepServerInSynch: false. // Push camera information to server if true * } * * @return {vtkWeb.Viewport} */ function createViewport(options) { // Make sure we have a valid autobahn session if (options.session === null) { throw "'session' must be provided within the option."; } function onReadyInternal() { if(userReadyCallback.fn) { // Providing just a short delay before triggering the "image-not-state" // event provides a break in the action so that the browser can draw the // image we just copied to the src attribute. setTimeout(userReadyCallback.fn, 0); if(userReadyCallback.once) { userReadyCallback.fn = null; } } } // Create viewport var config = $.extend({}, DEFAULT_VIEWPORT_OPTIONS, options), session = options.session, rendererContainer = $(DEFAULT_RENDERERS_CONTAINER_HTML).css(DEFAULT_RENDERERS_CONTAINER_CSS), mouseListener = $(DEFAULT_MOUSE_LISTENER_HTML).css(DEFAULT_MOUSE_LISTENER_CSS), statContainer = $(DEFAULT_STATISTIC_HTML).css(DEFAULT_STATISTIC_CSS), overlayContainer = $(DEFAULT_OVERLAY_HTML).css(DEFAULT_OVERLAY_CSS), onDoneQueue = [], statisticManager = createStatisticManager(), inSelectionMode = false, userReadyCallback = { fn: null, once: false}, viewport = { /** * Update the active renderer to be something else. * This allow the user to switch from Image Delivery to Geometry delivery * or even any other available renderer type available. * * The available renderers are indexed inside the following object vtkWeb.ViewportFactory. * * @member vtkWeb.Viewport * @param {String} rendererName * Key used to ID the renderer type. */ setActiveRenderer: function(rendererName) { $('.' + rendererName, rendererContainer).addClass('active').show().siblings().removeClass('active').hide(); rendererContainer.trigger('active'); statContainer[0].innerHTML = ''; statisticManager.reset(); }, /** * Method that should be called each time something in the scene as changed * and we want to update the viewport to reflect the latest state of the scene. * * @member vtkWeb.Viewport * @param {Function} ondone Function to call after rendering is complete. */ invalidateScene: function(onDone) { onDoneQueue.push(onDone); rendererContainer.trigger('invalidateScene'); }, /** * Method that should be called when nothing has changed in the scene * but for some reason the viewport has been dirty. * (i.e. Toggeling the statistic information within the viewport) * * @member vtkWeb.Viewport * @param {Function} ondone Function to call after rendering is complete. */ render: function(onDone, args) { onDoneQueue.push(onDone); rendererContainer.trigger({type: 'render', options: args}); }, /** * Reset the camera of the scene to make it fit in the screen as well * as invalidating the scene automatically. * * @member vtkWeb.Viewport * @param {Function} ondone Function to call after rendering is complete. */ resetCamera: function(onDone) { onDoneQueue.push(onDone); return session.call("viewport.camera.reset", [Number(config.view)]).then(function () { rendererContainer.trigger('invalidateScene'); }); }, /** * Update Orientation Axes Visibility for the given view * * @member vtkWeb.Viewport * @param {Boolean} show * Show: true / Hide: false * @param {Function} ondone Function to call after rendering is complete. */ updateOrientationAxesVisibility: function (show, onDone) { return session.call("viewport.axes.orientation.visibility.update", [Number(config.view), show]).then(function () { onDoneQueue.push(onDone); rendererContainer.trigger('invalidateScene'); }); }, /** * Update the Center Axes Visibility for the given view * * @member vtkWeb.Viewport * @param {Boolean} show * Show: true / Hide: false * @param {Function} ondone Function to call after rendering is complete. */ updateCenterAxesVisibility: function (show, onDone) { return session.call("viewport.axes.center.visibility.update", [Number(config.view), show]).then(function () { onDoneQueue.push(onDone); rendererContainer.trigger('invalidateScene'); }); }, /** * Reset view id. * This allow to invalidate the viewport and use the new active view * * @member vtkWeb.Viewport */ resetViewId: function () { rendererContainer.trigger('resetViewId'); }, /** * Ask the concrete renderer to generate an image and trigger its own * event when the image is ready. * * @member vtkWeb.Viewport */ captureScreenImage: function () { rendererContainer.trigger('captureRenderedImage'); }, /** * */ downloadTimestepData: function(shaList) { rendererContainer.trigger('downloadAllTimesteps'); }, /* * */ clearGeometryCache: function() { rendererContainer.trigger('clearCache'); }, /* * Provide a function that will be called when */ onReady: function(readyCallback, once) { userReadyCallback.fn = readyCallback; userReadyCallback.once = once; }, /** * Attach viewport to a DOM element * * @member vtkWeb.Viewport * @param {String} selector * The will be used internally to get the jQuery associated element * *
* viewport.bind(".renderer"); * *
* viewport.bind("#renderer"); * * * * *
* * * viewport.bind("body > div"); */ bind: function (selector) { var container = $(selector); if (container.attr("__vtkWeb_viewport__") !== "true") { container.attr("__vtkWeb_viewport__", "true"); container.append(rendererContainer).append(mouseListener).append(statContainer).append(overlayContainer); rendererContainer.trigger('invalidateScene'); } }, /** * Remove viewport from DOM element * * @member vtkWeb.Viewport */ unbind: function () { var parentElement = rendererContainer.parent(); if (parentElement) { parentElement.attr("__vtkWeb_viewport__", "false"); rendererContainer.remove(); mouseListener.remove(); statContainer.remove(); overlayContainer.remove(); } }, /** * Update statistic visibility * * @member vtkWeb.Viewport * @param {Boolean} visible */ showStatistics: function(isVisible) { if(isVisible) { statContainer.show(); } else { statContainer.hide(); } }, /** * Clear current statistic values * * @member vtkWeb.Viewport */ resetStatistics: function() { statisticManager.reset(); statContainer.empty(); }, /** * Event triggered before a mouse down. * * @member vtkWeb.Viewport * @event startInteraction */ /** * Event triggered after a mouse up. * * @member vtkWeb.Viewport * @event endInteraction * @param area * Provide the area in pixel between the startInteraction and endInteraction. * This can be used for selection or zoom to box... [minX, minY, maxX, maxY] */ /** * Toggle selection mode. * * When selection mode is active, the next interaction will * create a 2D rectangle that will be used to define an area to select. * This won't perform any selection but will provide another interaction * mode on the client side which will then trigger an event with * the given area. It is the responsibility of the user to properly * handle that event and eventually trigger the appropriate server * code to perform a selection or a zoom-to-box type of action. * * @member vtkWeb.Viewport * @return {Boolean} inSelectionMode state */ toggleSelectionMode: function() { inSelectionMode = !inSelectionMode; return inSelectionMode; }, /** * Return whether or not viewport is in selection mode. * * @return {Boolean} true if viewport is in selection mode, false otherwise. */ getSelectionMode: function() { return inSelectionMode; } }; // Attach config object to renderer parent rendererContainer.data('config', config); // Attach onDone listener rendererContainer.bind('done', function(){ while(onDoneQueue.length > 0) { var callback = onDoneQueue.pop(); try { if(callback) { callback(); } } catch(error) { console.log("On Done callback error:"); console.log(error); } } }); // Attach the on ready internal rendererContainer.bind('renderer-ready', onReadyInternal); // Create any renderer type that is available for(var key in vtkWeb.ViewportFactory) { try { vtkWeb.ViewportFactory[key].builder(rendererContainer); } catch(error) { console.log("Error while trying to load renderer: " + key); console.log(error); } } // Set default renderer viewport.setActiveRenderer(config.renderer); // Attach mouse listener if requested if (config.enableInteractions) { attachMouseListener(mouseListener, rendererContainer, overlayContainer, viewport); try { attachTouchListener(mouseListener, rendererContainer, viewport); } catch(error) { console.log('Hammer is not properly initialized'); console.log(error); } } // Attach stat listener rendererContainer.bind('stats', function(event){ statisticManager.eventHandler(event); statContainer[0].innerHTML = statisticManager.toHTML(); }); return viewport; } // ---------------------------------------------------------------------- // Init vtkWeb module if needed // ---------------------------------------------------------------------- if (GLOBAL.hasOwnProperty("vtkWeb")) { module = GLOBAL.vtkWeb || {}; } else { GLOBAL.vtkWeb = module; } // ---------------------------------------------------------------------- // Export internal methods to the vtkWeb module // ---------------------------------------------------------------------- module.createViewport = function (option) { return createViewport(option); }; // ---------------------------------------------------------------------- // Local module registration // ---------------------------------------------------------------------- try { // Tests for presence of jQuery, then registers this module if ($ !== undefined) { module.registerModule('vtkweb-viewport'); } else { console.error('Module failed to register, jQuery is missing'); } } catch(err) { console.error('Caught exception while registering module: ' + err.message); } }(window, jQuery));