src/node-drawing.js
// SAGE2 is available for use under the SAGE2 Software License
//
// University of Illinois at Chicago's Electronic Visualization Laboratory (EVL)
// and University of Hawai'i at Manoa's Laboratory for Advanced Visualization and
// Applications (LAVA)
//
// See full text, terms and conditions in the LICENSE.txt included file
//
// Copyright (c) 2015
/**
* Module for whiteboard app
*
* @module server
* @submodule DrawingManager
*/
"use strict";
// provides utility functions
var sageutils = require('../src/node-utils');
// Put a Tutorial, maybe an overlay text to say that drawing is enabled
function DrawingManager(config) {
this.lastId = 1;
this.dictionaryId = {};
this.idPrequel = "drawing_";
this.clientIDandSockets = {};
this.newDrawingObject = {};
this.style = {fill: "none", stroke: "white", "stroke-width": "5px", "stroke-linecap": "round"};
this.selectionBoxStyle = {fill: "none", stroke: "white", "stroke-width": "5px", "stroke-dasharray": "10,10"};
this.drawingMode = false;
this.eraserMode = false;
this.pointerColorMode = true;
this.drawState = [{id: "drawing_1", type: "path",
options: { points: [{x: 100, y: 200}, {x: 200, y: 300}] }, style: this.style}];
this.drawingsUndone = [];
this.tilesPosition = [];
this.palettePosition = {};
this.idMovingPalette = -1;
this.calculateTileDimensions(config);
// Reformat 1/27/2016
this.nextTouchSelection = false;
this.actionXTouch = {};
// Hardcoded size in pixel squared that serves as a threshold to recognize an eraser
this.ERASER_SIZE = 200;
// This is changed later if the server has a smaller title bar, it is not hardcoded
this.TITLE_BAR_HEIGHT = 58;
this.paletteIsMoving = false;
this.offsetFromPaletteXTouch = {};
this.selectionIsUsed = false;
this.lastPosition = {};
this.undoStack = [];
this.redoStack = [];
this.lastTimeSeen = {};
this.TIMEOUT_TIME = 5000;
// this.possibleActions = ["drawing", "movingPalette"];
this.paintingMode = false;
this.selectionMode = false;
this.selectionStart = {};
this.selectionEnd = {};
this.selectionTouchId = -1;
this.selectedDrawingObject = [];
this.selectionMovementStart = [];
this.selectionBox = null;
this.eraserBox = null;
this.eraserTouchId = -1;
this.interactMgr = null;
this.actionDoneStack = [{type: "drawing", data: [this.drawState[0].id]}];
this.actionRedoStack = [];
this.idAssociatedToAction = [];
this.maxLineSize = 20;
this.movingSelectionStartingPosition = null;
this.resizeSelectionStart = null;
this.oldSelectionInfo = {};
this.currentTouch = [];
// An object drawing is defined as follows:
// {
// id: String
// type: Type of the svg element
// options : {} Object containing all the attributes needed by that type of element
// style: {} Current style of the object to be drawn
// }
}
DrawingManager.prototype.linkInteractableManager = function(mngr) {
this.interactMgr = mngr;
};
// Simple function to scale a point with respect to an origin
DrawingManager.prototype.scalePoint = function(point, origin, scaleX, scaleY) {
var dx = point.x - origin.x;
var dy = point.y - origin.y;
var newDX = dx * scaleX;
var newDY = dy * scaleY;
return {x: origin.x + newDX, y: origin.y + newDY};
};
DrawingManager.prototype.calculateTileDimensions = function(config) {
var clients = config.displays.length;
var width = config.resolution.width;
var height = config.resolution.height;
for (var i = 0; i < clients; i++) {
var display = config.displays[i];
var startX = width * display.column;
var endX = startX + (width * display.width) - 1;
var startY = height * display.row;
var endY = startY + (height * display.height) - 1;
var position = {startX: startX, endX: endX, startY: startY, endY: endY, clientID: i};
this.tilesPosition.push(position);
}
};
// Called when a client connects, sends it a message saying that the whiteboard is now enabled
DrawingManager.prototype.init = function(wsio) {
var clientID = wsio.clientID;
if (clientID in this.clientIDandSockets) {
this.clientIDandSockets[clientID].push(wsio);
} else {
this.clientIDandSockets[clientID] = [wsio];
}
this.drawingInit(wsio, this.drawState);
};
// Calls an init method on all the client, needed for major changes that need to be broadcasted
DrawingManager.prototype.initAll = function() {
for (var clientID in this.clientIDandSockets) {
for (var i in this.clientIDandSockets[clientID]) {
var wsio = this.clientIDandSockets[clientID][i];
this.drawingInit(wsio, this.drawState);
}
}
};
// TODO: manage the check involved client
DrawingManager.prototype.updateWithGroupDrawingObject = function(group) {
for (var drawingObject in group) {
for (var cID in this.tilesPosition) {
var clientID = this.tilesPosition[cID].clientID;
var manipulatedObject = this.manipulateDrawingObject(group[drawingObject], clientID);
this.update(manipulatedObject, clientID);
}
}
};
DrawingManager.prototype.removeDrawingObject = function(group) {
for (var cID in this.tilesPosition) {
var clientID = this.tilesPosition[cID].clientID;
this.remove(group, clientID);
}
};
DrawingManager.prototype.enablePaintingMode = function() {
this.paintingMode = true;
this.sendModesToPalette();
};
DrawingManager.prototype.disablePaintingMode = function() {
this.paintingMode = false;
this.sendModesToPalette();
};
DrawingManager.prototype.selectionModeOnOff = function() {
this.nextTouchSelection = true;
};
DrawingManager.prototype.sendModesToPalette = function() {
var data = {
drawingMode: this.drawingMode,
paintingMode: this.paintingMode,
eraserMode: this.eraserMode,
pointerColorMode: this.pointerColorMode};
this.sendChangeToPalette(this.paletteID, data);
};
DrawingManager.prototype.enableEraserMode = function() {
this.eraserMode = true;
this.sendModesToPalette();
};
DrawingManager.prototype.disableEraserMode = function() {
this.eraserMode = false;
this.sendModesToPalette();
};
DrawingManager.prototype.enablePointerColorMode = function() {
this.pointerColorMode = true;
this.sendModesToPalette();
};
DrawingManager.prototype.disablePointerColorMode = function() {
this.pointerColorMode = false;
this.sendModesToPalette();
};
DrawingManager.prototype.removeWebSocket = function(wsio) {
// Detecting the position of the socket into the corresponding socket array
var clientID = wsio.clientID;
var position = this.clientIDandSockets[clientID].indexOf(wsio);
if (position > -1) {
this.clientIDandSockets[clientID].splice(position, 1);
sageutils.log("DrawingManager", "Socket removed from drawingManager");
} else {
sageutils.log("DrawingManager", "Attempt to remove a socket from drawingManager, but not present");
}
};
DrawingManager.prototype.clearDrawingCanvas = function() {
this.actionDoneStack.push({type: "clearAll", data: this.copy(this.drawState)});
this.drawState = [];
this.deleteSelectionBox();
this.initAll();
};
// Simple helper function to check if element is inside array
function isInside(s, arr) {
for (var i in arr) {
if (arr[i] == s) {
return true;
}
}
return false;
}
DrawingManager.prototype.saveDrawingToUndo = function(e) {
var obj = {type: "drawingToUndo"};
obj.data = this.idAssociatedToAction[e.sourceId];
this.undoStack.push(obj);
};
DrawingManager.prototype.undoThisDrawingGroup = function(array) {
var groupToDelete = [];
var i = 0;
while (i < this.drawState.length) {
if (isInside(this.drawState[i].id, array)) {
groupToDelete.push(this.drawState.splice(i, 1)[0]);
} else {
i++;
}
}
// Tell the clients to remove them
this.removeDrawingObject(groupToDelete);
return groupToDelete;
};
DrawingManager.prototype.redoThisDrawingGroup = function(array) {
var idRedone = [];
for (var i in array) {
this.drawState.push(array[i]);
idRedone.push(array[i].id);
}
this.updateWithGroupDrawingObject(array);
return idRedone;
};
DrawingManager.prototype.undoLastDrawing = function() {
if (this.undoStack.length > 0) {
var last = this.undoStack.pop();
if (last.type == "drawingToUndo") {
last.data = this.undoThisDrawingGroup(last.data);
last.type = "drawingToRedo";
this.redoStack.push(last);
}
}
};
DrawingManager.prototype.redoDrawing = function() {
if (this.redoStack.length > 0) {
var last = this.redoStack.pop();
if (last.type == "drawingToRedo") {
last.data = this.redoThisDrawingGroup(last.data);
last.type = "drawingToUndo";
this.undoStack.push(last);
}
}
};
DrawingManager.prototype.changeStyle = function(data) {
this.style[data.name] = data.value;
this.sendStyleToPalette(this.paletteID, this.style);
this.pointerColorMode = false;
this.sendModesToPalette();
};
DrawingManager.prototype.enableDrawingMode = function(data) {
// sageutils.log("DrawingManager", "Drawing mode enabled");
this.drawingMode = true;
this.paletteID = data.id;
this.sendStyleToPalette(this.paletteID, this.style);
this.sendModesToPalette();
};
DrawingManager.prototype.reEnableDrawingMode = function(data) {
// sageutils.log("DrawingManager", "Drawing mode reEnabled");
this.drawingMode = true;
this.sendStyleToPalette(this.paletteID, this.style);
this.sendModesToPalette();
};
DrawingManager.prototype.disableDrawingMode = function(data) {
// sageutils.log("DrawingManager", "Drawing mode disabled");
this.drawingMode = false;
// this.paletteID = null;
this.sendModesToPalette();
};
// Update a single drawing on the client side
DrawingManager.prototype.update = function(drawingObject, clientID) {
for (var ws in this.clientIDandSockets[clientID]) {
this.drawingUpdate(this.clientIDandSockets[clientID][ws], drawingObject);
}
// Send the object also to client -1, but not manipulated. Maybe create another udpate.
for (ws in this.clientIDandSockets[-1]) {
this.drawingUpdate(this.clientIDandSockets[-1][ws], drawingObject);
}
};
// Delete a single drawing on the client side
DrawingManager.prototype.remove = function(group, clientID) {
for (var ws in this.clientIDandSockets[clientID]) {
this.drawingRemove(this.clientIDandSockets[clientID][ws], group);
}
// Send the object also to client -1, but not manipulated. Maybe create another udpate.
for (ws in this.clientIDandSockets[-1]) {
this.drawingUpdate(this.clientIDandSockets[-1][ws], group);
}
};
DrawingManager.prototype.copy = function(a) {
return JSON.parse(JSON.stringify(a));
};
// This will return the square of the distance, useful for compararisons, not actual distance
DrawingManager.prototype.distance = function(p1, p2) {
return (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y);
};
DrawingManager.prototype.getNewId = function(sourceId) {
this.lastId++;
var newId = this.idPrequel + this.lastId;
this.dictionaryId[sourceId] = newId;
return newId;
};
DrawingManager.prototype.realeaseId = function(sourceId) {
delete this.dictionaryId[sourceId];
};
DrawingManager.prototype.existsId = function(sourceId) {
return sourceId in this.dictionaryId;
};
DrawingManager.prototype.findMaxId = function() {
var max = -1;
for (var drawingObj in this.drawState) {
var idx = this.drawState[drawingObj].id;
idx = parseInt(idx.substring(this.idPrequel.length, idx.length));
if (idx > max) {
max = idx;
}
}
return max;
};
DrawingManager.prototype.eraseArea = function(x, y, w, h) {
// Erase everything in the given area
var eraserBox = {x: x - w / 2, y: y - h / 2, w: w, h: h};
var i = 0;
var groupToDelete = [];
while (i < this.drawState.length) {
var draw = this.drawState[i].options.points;
var inside = false;
for (var j in draw) {
var p = draw[j];
if (p.x > eraserBox.x &&
p.x < eraserBox.x + eraserBox.w &&
p.y > eraserBox.y &&
p.y < eraserBox.y + eraserBox.h) {
inside = true;
break;
}
}
if (inside) {
groupToDelete.push(this.drawState[i]);
for (var k in this.dictionaryId) {
if (this.drawState[i].id == this.dictionaryId[k]) {
this.realeaseId(k);
}
}
this.drawState.splice(i, 1);
} else {
i += 1;
}
}
if (groupToDelete.length > 0) {
this.removeDrawingObject(groupToDelete);
}
// returns all the deleted elements
return groupToDelete;
};
DrawingManager.prototype.createNewDraw = function(e, posX, posY) {
// Create new Drawing object
var drawingId = this.getNewId(e.sourceId);
this.newDrawingObject[drawingId] = {};
this.newDrawingObject[drawingId].id = drawingId;
this.newDrawingObject[drawingId].type = "circle";
this.newDrawingObject[drawingId].options = { points: [{x: posX, y: posY}] };
this.newDrawingObject[drawingId].style = this.copy(this.style);
if (this.pointerColorMode && e.extraDataString !== undefined) {
this.newDrawingObject[drawingId].style.stroke = e.extraDataString;
}
this.drawState.push(this.newDrawingObject[drawingId]);
this.idAssociatedToAction[e.sourceId] = [drawingId];
};
DrawingManager.prototype.updateDrawingObject = function(e, posX, posY) {
if (!this.existsId(e.sourceId)) {
this.createNewDraw(e, posX, posY);
this.idAssociatedToAction[e.sourceId] = [drawingId];
}
var drawingId = this.dictionaryId[e.sourceId];
var lastPointId = this.newDrawingObject[drawingId].options.points.length - 1;
var lastPoint = this.newDrawingObject[drawingId].options.points[lastPointId];
if (this.distance(lastPoint, {x: posX, y: posY}) > 0.5) {
this.newDrawingObject[drawingId].type = "path";
this.newDrawingObject[drawingId].options.points.push({x: posX, y: posY});
}
if (this.newDrawingObject[drawingId].options.points.length > this.maxLineSize) {
var l = this.newDrawingObject[drawingId].options.points.length;
var secondPart = this.newDrawingObject[drawingId].options.points.splice(this.maxLineSize - 1, l);
this.newDrawingObject[drawingId].options.points.push(this.copy(secondPart[0]));
this.realeaseId(e.sourceId);
var id = this.getNewId(e.sourceId);
if (this.idAssociatedToAction[e.sourceId]) {
this.idAssociatedToAction[e.sourceId].push(id);
}
var newDraw = {};
newDraw.type = "path";
newDraw.style = this.newDrawingObject[drawingId].style;
if (this.pointerColorMode && e.extraDataString !== undefined) {
newDraw.style.stroke = e.extraDataString;
}
newDraw.options = {};
newDraw.options.points = secondPart;
newDraw.id = id;
this.drawState.push(newDraw);
this.newDrawingObject[id] = newDraw;
}
};
// Check if the touch is in the recall bar
DrawingManager.prototype.touchNearBottom = function(x, y) {
var c = this.checkInvolvedClient(x, y);
if (c != null && (c > -1)) {
var startY = this.tilesPosition[c].startY;
var endY = this.tilesPosition[c].endY;
var w = endY - startY;
var activeArea = w * 0.1;
return y >= endY - activeArea;
}
return false;
};
// Check if the touch is in the recall bar
DrawingManager.prototype.touchInsidePalette = function(x, y) {
return ((x >= this.palettePosition.startX) && (x <= this.palettePosition.endX) &&
(y >= this.palettePosition.startY) && (y <= this.palettePosition.endY));
};
// Check if the touch is in the palette title bar
DrawingManager.prototype.touchInsidePaletteTitleBar = function(x, y) {
return ((x >= this.palettePosition.startX) && (x <= this.palettePosition.endX) &&
(y >= this.palettePosition.startY - this.TITLE_BAR_HEIGHT) && (y < this.palettePosition.startY));
};
// Check if the touch is inside the current selection on screen
DrawingManager.prototype.touchInsideSelection = function(x, y) {
if (x > this.selectionStart.x &&
y > this.selectionStart.y &&
x < this.selectionEnd.x &&
y < this.selectionEnd.y) {
return true;
}
return false;
};
// Check if the touch is in the zoom box of a selection
DrawingManager.prototype.touchInsideSelectionZoomBox = function(x, y) {
var w = this.selectionEnd.x - this.selectionStart.x;
var h = this.selectionEnd.y - this.selectionStart.y;
if (x >= this.selectionStart.x + 0.9 * w &&
y >= this.selectionStart.y + 0.9 * h &&
x <= this.selectionEnd.x &&
y <= this.selectionEnd.y) {
return true;
}
return false;
};
DrawingManager.prototype.selectDrawingObjects = function() {
this.selectedDrawingObject = [];
for (var drawingObj in this.drawState) {
var points = this.drawState[drawingObj].options.points;
for (var i in points) {
if (this.touchInsideSelection(points[i].x, points[i].y)) {
this.selectedDrawingObject.push(this.drawState[drawingObj]);
break;
}
}
}
};
DrawingManager.prototype.newSelectionBox = function(e) {
// Create new Selection box
var drawingId = this.getNewId(e.sourceId);
this.newDrawingObject[drawingId] = {};
this.newDrawingObject[drawingId].id = drawingId;
this.newDrawingObject[drawingId].type = "rect";
this.newDrawingObject[drawingId].options = { points: [this.selectionStart, this.selectionEnd] };
this.newDrawingObject[drawingId].style = this.selectionBoxStyle;
this.selectionBox = this.newDrawingObject[drawingId];
this.drawState.push(this.newDrawingObject[drawingId]);
};
DrawingManager.prototype.moveSelectionBox = function() {
if (this.selectionBox) {
this.selectionBox.options.points = [this.selectionStart, this.selectionEnd];
this.updateWithGroupDrawingObject([this.selectionBox]);
}
};
DrawingManager.prototype.deleteSelectionBox = function() {
if (this.selectionBox) {
for (var drawingObj in this.drawState) {
var idx = this.drawState[drawingObj].id;
if (idx == this.selectionBox.id) {
this.drawState.splice(drawingObj, 1);
break;
}
}
this.selectedDrawingObject = [];
this.selectionStart = {};
this.selectionEnd = {};
this.selectionBox = null;
this.selectionTouchId = -1;
this.initAll();
}
};
DrawingManager.prototype.selectionMove = function(x, y) {
// var actionSelectionMove = {type: "selectionMove", data: {'x': x, 'y': y, obj: []}};
for (var drawingObj in this.selectedDrawingObject) {
var obj = this.selectedDrawingObject[drawingObj];
// actionSelectionMove['data']['obj'].push(obj);
var points = obj.options.points;
for (var i in points) {
points[i].x += x;
points[i].y += y;
}
}
this.updateWithGroupDrawingObject(this.selectedDrawingObject);
};
DrawingManager.prototype.selectionZoom = function(sx, sy) {
for (var drawingObj in this.selectedDrawingObject) {
var points = this.selectedDrawingObject[drawingObj].options.points;
for (var i in points) {
var p = points[i];
points[i] = this.scalePoint(p, this.selectionStart, sx, sy);
}
}
this.updateWithGroupDrawingObject(this.selectedDrawingObject);
};
DrawingManager.prototype.startSelectionFrom = function(e, posX, posY) {
this.deleteSelectionBox();
this.selectionStart = {x: posX, y: posY};
this.selectionEnd = {x: posX, y: posY};
this.selectionIsUsed = true;
this.newSelectionBox(e);
};
DrawingManager.prototype.updateCreatingSelection = function(posX, posY) {
if (this.selectionStart.x > posX) {
this.selectionStart.x = posX;
} else if (this.selectionEnd.x < posX) {
this.selectionEnd.x = posX;
} else {
var d1 = this.distance({x: posX, y: this.selectionStart.y}, this.selectionStart);
if (d1 < this.distance({x: posX, y: this.selectionEnd.y}, this.selectionEnd)) {
this.selectionStart.x = posX;
} else {
this.selectionEnd.x = posX;
}
}
if (this.selectionStart.y > posY) {
this.selectionStart.y = posY;
} else if (this.selectionEnd.y < posY) {
this.selectionEnd.y = posY;
} else {
var d2 = this.distance({x: this.selectionStart.x, y: posY}, this.selectionStart);
if (d2 < this.distance({x: this.selectionEnd.x, y: posY}, this.selectionEnd)) {
this.selectionStart.y = posY;
} else {
this.selectionEnd.y = posY;
}
}
this.moveSelectionBox();
};
DrawingManager.prototype.moveSelectionTo = function(e, posX, posY) {
var dx = posX - this.lastPosition[e.sourceId].x;
var dy = posY - this.lastPosition[e.sourceId].y;
this.lastPosition[e.sourceId].x = posX;
this.lastPosition[e.sourceId].y = posY;
this.selectionStart.x += dx;
this.selectionStart.y += dy;
this.selectionEnd.x += dx;
this.selectionEnd.y += dy;
this.moveSelectionBox();
this.selectionMove(dx, dy);
};
DrawingManager.prototype.zoomSelectionBy = function(e, posX, posY) {
var dx = posX - this.lastPosition[e.sourceId].x;
var dy = posY - this.lastPosition[e.sourceId].y;
var oldW = this.selectionEnd.x - this.selectionStart.x;
var oldH = this.selectionEnd.y - this.selectionStart.y;
var newW = oldW + dx;
var newH = oldH + dy;
var sx = parseFloat(newW) / oldW;
var sy = parseFloat(newH) / oldH;
this.lastPosition[e.sourceId] = {x: posX, y: posY};
this.selectionEnd.x += dx;
this.selectionEnd.y += dy;
this.moveSelectionBox();
this.selectionZoom(sx, sy);
};
// Timeout system, if interaction is inactive for more than TIMEOUT_TIME, close it
DrawingManager.prototype.updateTimer = function() {
var t = new Date();
var timouted = [];
for (var i in this.lastTimeSeen) {
var e = this.lastTimeSeen[i];
if (t - e > this.TIMEOUT_TIME) {
sageutils.log("DrawingManager", "Timeout for id:", i);
timouted.push(i);
break;
}
}
for (var j in timouted) {
delete this.lastTimeSeen[timouted[j]];
var fake = {sourceId: timouted[j], type: 6};
this.pointerEvent(fake, timouted[j], 0, 0, 0, 0);
}
};
// Returns the action associated with the touch
DrawingManager.prototype.detectDownAction = function(posX, posY, w, h) {
// First Priority: Moving & Using palette
if (this.touchInsidePaletteTitleBar(posX, posY)) {
// Check that nobody else is moving the palette
if (!this.paletteIsMoving) {
this.paletteIsMoving = true;
return "movingPalette";
}
// somebody else is moving
return "ignored";
}
if (this.touchInsidePalette(posX, posY)) {
return "usePalette";
}
if (this.touchNearBottom(posX, posY)) {
if (!this.paletteIsMoving) {
this.paletteIsMoving = true;
return "recallingPalette";
}
return "ignored";
}
// Second Priority: Selections
if (this.nextTouchSelection) {
this.nextTouchSelection = false;
return "creatingSelection";
}
if (this.touchInsideSelectionZoomBox(posX, posY)) {
if (!this.selectionIsUsed) {
this.selectionIsUsed = true;
return "zoomingSelection";
}
return "ignored";
}
if (this.touchInsideSelection(posX, posY)) {
if (!this.selectionIsUsed) {
this.selectionIsUsed = true;
return "movingSelection";
}
return "ignored";
}
// Third Priority: Eraser
if ((!this.paintingMode) && (Math.max(w, h) >= this.ERASER_SIZE)) {
return "eraser";
}
// Default: Drawing
return "drawing";
};
// Called on the first touch for each touch interaction
DrawingManager.prototype.touchDown = function(e, sourceId, posX, posY, w, h) {
// Detect what the user wants to do with this touch
var action = this.detectDownAction(posX, posY, w, h);
this.actionXTouch[e.sourceId] = action;
if (action == "movingPalette") {
// Just save the offset
this.offsetFromPaletteXTouch[e.sourceId] = {
x: posX - this.palettePosition.startX,
y: posY - this.palettePosition.startY + this.TITLE_BAR_HEIGHT};
return;
}
// Action Performed at touch down: Using Palette
if (action == "usePalette") {
this.sendTouchToPalette(this.paletteID, posX - this.palettePosition.startX, posY - this.palettePosition.startY);
return;
}
// Action Performed at touch down: recall Palette
if (action == "recallingPalette") {
this.movePaletteTo(
this.paletteID, posX,
this.palettePosition.startY - this.TITLE_BAR_HEIGHT,
this.palettePosition.endX - this.palettePosition.startX,
this.palettePosition.endY - this.palettePosition.startY
);
return;
}
if (action == "creatingSelection") {
this.startSelectionFrom(e, posX, posY);
return;
}
if (action == "movingSelection") {
this.lastPosition[e.sourceId] = {x: posX, y: posY};
return;
}
if (action == "zoomingSelection") {
this.lastPosition[e.sourceId] = {x: posX, y: posY};
return;
}
// Action Performed at touch down: erasing
if (action == "eraser") {
this.eraseArea(posX, posY, w, h);
return;
}
// Action Performed at touch down: drawing
if (action == "drawing") {
if (!this.selectionIsUsed) {
this.deleteSelectionBox();
}
if (this.paintingMode) {
this.style["stroke-width"] = Math.max(w, h);
}
this.createNewDraw(e, posX, posY);
return;
}
};
DrawingManager.prototype.touchMove = function(e, sourceId, posX, posY, w, h) {
// do what is supposed to happen when a touch is moving
var action = this.actionXTouch[e.sourceId];
// First Priority: Moving palette (using ignored)
if (action == "movingPalette") {
var offX = this.offsetFromPaletteXTouch[e.sourceId].x || 0;
var offY = this.offsetFromPaletteXTouch[e.sourceId].y || 0;
this.movePaletteTo(this.paletteID
, posX - offX
, posY - offY
, this.palettePosition.endX - this.palettePosition.startX
, this.palettePosition.endY - this.palettePosition.startY);
}
if (action == "usePalette") {
this.sendDragToPalette(this.paletteID, posX - this.palettePosition.startX, posY - this.palettePosition.startY);
return;
}
if (action == "recallingPalette") {
this.movePaletteTo(this.paletteID
, posX
, this.palettePosition.startY - this.TITLE_BAR_HEIGHT
, this.palettePosition.endX - this.palettePosition.startX
, this.palettePosition.endY - this.palettePosition.startY);
return;
}
// Second Priority: Selections
if (action == "creatingSelection") {
this.updateCreatingSelection(posX, posY);
return;
}
if (action == "movingSelection") {
this.moveSelectionTo(e, posX, posY);
return;
}
if (action == "zoomingSelection") {
this.zoomSelectionBy(e, posX, posY);
return;
}
// Third Priority: Eraser
// A Drawing can become an eraser
if ((!this.paintingMode) && (Math.max(w, h) >= this.ERASER_SIZE)) {
action = "eraser";
this.actionXTouch[e.sourceId] = action;
}
if (this.eraserMode) {
w = this.ERASER_SIZE / 4;
h = this.ERASER_SIZE / 4;
action = "eraser";
this.actionXTouch[e.sourceId] = action;
this.eraseArea(posX, posY, w, h);
return;
}
// An eraser can never go back to be a drawing
if (action == "eraser") {
this.eraseArea(posX, posY, w, h);
return;
}
if (action == "drawing") {
this.updateDrawingObject(e, posX, posY);
return;
}
};
DrawingManager.prototype.touchRelease = function(e, sourceId, posX, posY, w, h) {
// do what is supposed to happen when a touch is released
var action = this.actionXTouch[e.sourceId];
if (action == "movingPalette") {
delete this.offsetFromPaletteXTouch[e.sourceId];
this.paletteIsMoving = false;
return;
}
if (action == "recallingPalette") {
this.paletteIsMoving = false;
return;
}
if (action == "creatingSelection") {
// Select what's in the selection right now
this.selectDrawingObjects();
this.selectionIsUsed = false;
return;
}
if (action == "movingSelection") {
delete this.lastPosition[e.sourceId];
this.selectionIsUsed = false;
return;
}
if (action == "zoomingSelection") {
delete this.lastPosition[e.sourceId];
this.selectionIsUsed = false;
return;
}
if (action == "drawing") {
// Check if an application is under one of the lines drawn by this id
this.linkToApplication(e.sourceId);
this.saveDrawingToUndo(e);
// Release the drawingId
this.realeaseId(e.sourceId);
}
};
// Called from node drawing when a touch interaction happens, entry point for touches
DrawingManager.prototype.pointerEvent = function(e, sourceId, posX, posY, w, h) {
if (e.type == 5) {
this.touchDown(e, e.sourceId, posX, posY, w, h);
this.lastTimeSeen[e.sourceId] = new Date();
} else if (e.type == 4 && this.lastTimeSeen[e.sourceId] !== undefined) {
this.touchMove(e, e.sourceId, posX, posY, w, h);
this.lastTimeSeen[e.sourceId] = new Date();
} else if (e.type == 6) {
this.touchRelease(e, e.sourceId, posX, posY, w, h);
delete this.lastTimeSeen[e.sourceId];
}
if (this.lastTimeSeen[e.sourceId] !== undefined) {
if (this.actionXTouch[e.sourceId] == "drawing") {
var drawingId = this.dictionaryId[e.sourceId];
var involvedClient = this.checkInvolvedClient(posX, posY);
var manipulatedObject = this.manipulateDrawingObject(this.newDrawingObject[drawingId], involvedClient);
this.update(manipulatedObject, involvedClient);
}
// Timeout
this.updateTimer(e, posX, posY);
}
};
DrawingManager.prototype.linkToApplication = function(touchId) {
var application;
for (var j in this.idAssociatedToAction[touchId]) {
application = this.checkForApplications(this.idAssociatedToAction[touchId][j]);
if (application != undefined) {
break;
}
}
if (application != undefined) {
// Application found, link to all lines
for (var i in this.drawState) {
if (this.drawState[i].id in this.idAssociatedToAction[touchId]) {
this.drawState[i].linkedAppID = application.id;
}
}
}
};
DrawingManager.prototype.checkForApplications = function(id) {
var drawing;
// Find the drawing with that id inside the drawing state
for (var i in this.drawState) {
if (this.drawState[i].id == id) {
drawing = this.drawState[i];
break;
}
}
if (drawing) {
for (i in drawing.options.points) {
var p = drawing.options.points[i];
var obj = this.interactMgr.searchGeometry({x: p.x, y: p.y});
if (obj && obj.layerId == "applications") {
return obj;
}
}
}
return;
};
DrawingManager.prototype.manipulateDrawingObject = function(drawingObject, clientID) {
if (clientID == null || !drawingObject) {
return;
}
// Cloning the drawing object to manipuate its position, in order to send to the clients its relativ position
var manipulatedObject = JSON.parse(JSON.stringify(drawingObject));
var offsetX = this.tilesPosition[clientID].startX;
var offsetY = this.tilesPosition[clientID].startY;
for (var i in manipulatedObject.options.points) {
var point = manipulatedObject.options.points[i];
manipulatedObject.options.points[i].x = point.x - offsetX;
manipulatedObject.options.points[i].y = point.y - offsetY;
}
return manipulatedObject;
};
DrawingManager.prototype.isOnPalette = function(posX, posY) {
if (this.palettePosition.startX <= posX &
this.palettePosition.endX >= posX &
this.palettePosition.startY <= posY &
this.palettePosition.endY >= posY) {
return true;
}
return false;
};
DrawingManager.prototype.updatePalettePosition = function(data) {
this.palettePosition.startX = data.startX;
this.palettePosition.startY = data.startY;
this.palettePosition.endX = data.endX;
this.palettePosition.endY = data.endY;
if (this.palettePosition.startY < 200) {
// this.movePaletteTo(this.paletteID,
// this.palettePosition.startX,
// this.palettePosition.startY + 600m,
// this.palettePosition.endX - this.palettePosition.startX,
// this.palettePosition.endY - this.palettePosition.startY);
}
};
DrawingManager.prototype.applicationMoved = function(id, newX, newY) {
var appObj = this.interactMgr.getObject(id, "applications");
var oldX = appObj.x1;
var oldY = appObj.y1;
var dx = newX - oldX;
var dy = newY - oldY;
var toMove = [];
for (var i in this.drawState) {
var draw = this.drawState[i];
if (draw.linkedAppID == id) {
toMove.push(draw);
for (var j in draw.options.points) {
var p = draw.options.points[j];
p.x += dx;
p.y += dy;
}
}
}
if (toMove != []) {
this.updateWithGroupDrawingObject(toMove);
}
this.palettePosition.startX = appObj.geometry.x;
this.palettePosition.startY = appObj.geometry.y + this.TITLE_BAR_HEIGHT;
this.palettePosition.endX = appObj.geometry.x + appObj.geometry.w;
this.palettePosition.endY = appObj.geometry.y + appObj.geometry.h;
};
DrawingManager.prototype.applicationResized = function(id, newW, newH, origin) {
var oldW = this.interactMgr.getObject(id, "applications").x2 - this.interactMgr.getObject(id, "applications").x1;
var oldH = this.interactMgr.getObject(id, "applications").y2 - this.interactMgr.getObject(id, "applications").y1;
var sx = newW / oldW;
var sy = newH / oldH;
if (!origin) {
origin = {x: this.interactMgr.getObject(id, "applications").x1, y: this.interactMgr.getObject(id, "applications").y1};
}
var toMove = [];
for (var i in this.drawState) {
var draw = this.drawState[i];
if (draw.linkedAppID == id) {
toMove.push(draw);
for (var j in draw.options.points) {
var p = draw.options.points[j];
draw.options.points[j] = this.scalePoint(p, origin, sx, sy);
}
}
}
if (toMove != []) {
this.updateWithGroupDrawingObject(toMove);
}
};
DrawingManager.prototype.checkInvolvedClient = function(posX, posY) {
// Probably this method is inconsistent if the object start from a display and terminates in another
for (var i in this.tilesPosition) {
var client = this.tilesPosition[i];
if (client.startX <= posX &
client.endX >= posX &
client.startY <= posY &
client.endY >= posY) {
return client.clientID;
}
}
sageutils.log("DrawingManager", "No single client involved");
return;
};
DrawingManager.prototype.saveDrawings = function() {
this.saveSession(this.drawState);
};
DrawingManager.prototype.loadDrawings = function(data) {
// asynchronous
this.loadSession(data);
};
DrawingManager.prototype.gotSessionsList = function(data) {
this.sendSessionListToPalette(this.paletteID, data);
};
DrawingManager.prototype.loadOldState = function(data) {
this.drawState = data || [];
this.lastId = this.findMaxId() + 1;
this.initAll();
};
// Get all the callbacks from the server
DrawingManager.prototype.setCallbacks = function(
drawingInitCB,
drawingUpdateCB,
drawingRemoveCB,
sendTouchToPaletteCB,
sendDragToPaletteCB,
sendStyleToPaletteCB,
sendChangeToPaletteCB,
movePaletteToCB,
saveSessionCB,
loadSessionCB,
sendSessionListCB) {
this.drawingInit = drawingInitCB;
this.drawingUpdate = drawingUpdateCB;
this.drawingRemove = drawingRemoveCB;
this.sendTouchToPalette = sendTouchToPaletteCB;
this.sendDragToPalette = sendDragToPaletteCB;
this.sendStyleToPalette = sendStyleToPaletteCB;
this.sendChangeToPalette = sendChangeToPaletteCB;
this.movePaletteTo = movePaletteToCB;
this.saveSession = saveSessionCB;
this.loadSession = loadSessionCB;
this.sendSessionListToPalette = sendSessionListCB;
};
module.exports = DrawingManager;