Files
ThirdParty-6/ParaView-5.0.1/VTK/Web/JavaScript/Lib/vtkweb.viewport.js

893 lines
32 KiB
JavaScript

/**
* 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 = "<div class='renderers'></div>",
DEFAULT_RENDERERS_CONTAINER_CSS = {
"position": "absolute",
"top": "0px",
"left": "0px",
"right": "0px",
"bottom": "0px",
"z-index" : "0",
"overflow": "hidden"
},
DEFAULT_MOUSE_LISTENER_HTML = "<div class='mouse-listener'></div>",
DEFAULT_MOUSE_LISTENER_CSS = {
"position": "absolute",
"top": "0px",
"left": "0px",
"right": "0px",
"bottom": "0px",
"z-index" : "3"
},
DEFAULT_STATISTIC_HTML = "<div class='statistics'></div>",
DEFAULT_STATISTIC_CSS = {
"position": "absolute",
"top": "0px",
"left": "0px",
"right": "0px",
"bottom": "0px",
"z-index" : "2",
"display" : "none"
},
DEFAULT_OVERLAY_HTML = "<canvas class='overlay'></canvas>",
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("<table class='viewport-stat'>");
buffer.append("<tr class='stat-header'><td class='label'>Timed Quantity</td><td class='value'>Current</td><td class='min'>Min</td><td class='max'>Max</td><td class='avg'>Average</td></tr>");
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("<tr><td class='label'>");
buffer.append(formater.label);
buffer.append("</td><td class='value'>");
buffer.append(formater.convert(stat.value));
buffer.append("</td><td class='min'>");
buffer.append((min < max) ? min : max);
buffer.append("</td><td class='max'>");
buffer.append((min > max) ? min : max);
buffer.append("</td><td class='avg'>");
buffer.append(formater.convert(stat.getAverageValue()));
buffer.append("</td></tr>");
}
}
buffer.append("</table>");
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
*
* <div class="renderer"></div>
* viewport.bind(".renderer");
*
* <div id="renderer"></div>
* viewport.bind("#renderer");
*
* <html>
* <body>
* <!-- renderer -->
* <div></div>
* </body>
* </html>
* 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));