From 5f5b86069154dde8d40b2a0d3ea53911edfb6887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Dornier?= Date: Mon, 22 Jul 2024 17:05:47 +0200 Subject: [PATCH 1/8] Update zoom panel when copy-pasting view --- .../shape-editor-4.0.0/shape-editor.js | 2429 ++++ omero_figure/static/figure/figure.js | 10020 ++++++++++++++++ omero_figure/static/figure/templates.js | 1048 ++ src/js/views/right_panel_view.js | 5 + 4 files changed, 13502 insertions(+) create mode 100644 omero_figure/static/figure/3rdparty/shape-editor-4.0.0/shape-editor.js create mode 100644 omero_figure/static/figure/figure.js create mode 100644 omero_figure/static/figure/templates.js diff --git a/omero_figure/static/figure/3rdparty/shape-editor-4.0.0/shape-editor.js b/omero_figure/static/figure/3rdparty/shape-editor-4.0.0/shape-editor.js new file mode 100644 index 000000000..d738abc11 --- /dev/null +++ b/omero_figure/static/figure/3rdparty/shape-editor-4.0.0/shape-editor.js @@ -0,0 +1,2429 @@ +//! BSD License. www.openmicroscopy.org + +//! DO NOT EDIT THIS FILE! - Edit under src/js/*.js +//! created by $ grunt concat + +/* globals Raphael: false */ +/* globals console: false */ + +var Line = function Line(options) { + + var self = this; + this.manager = options.manager; + this.paper = options.paper; + + if (options.id) { + this._id = options.id; + } else { + this._id = this.manager.getRandomId(); + } + this._x1 = options.x1; + this._y1 = options.y1; + this._x2 = options.x2; + this._y2 = options.y2; + this._strokeColor = options.strokeColor; + this._strokeWidth = options.strokeWidth || 2; + this.handle_wh = 6; + this._selected = false; + this._zoomFraction = 1; + if (options.zoom) { + this._zoomFraction = options.zoom / 100; + } + + this.element = this.paper.path(); + this.element.attr({'cursor': 'pointer'}); + + // Drag handling of line + if (this.manager.canEdit) { + this.element.drag( + function(dx, dy) { + // DRAG, update location and redraw + dx = dx / self._zoomFraction; + dy = dy / self._zoomFraction; + + var offsetX = dx - this.prevX; + var offsetY = dy - this.prevY; + this.prevX = dx; + this.prevY = dy; + + // Manager handles move and redraw + self.manager.moveSelectedShapes(offsetX, offsetY, true); + return false; + }, + function() { + // START drag: note the location of all points + self._handleMousedown(); + this.prevX = 0; + this.prevY = 0; + return false; + }, + function() { + // STOP + // notify manager if line has moved + if (this.prevX !== 0 || this.prevY !== 0) { + self.manager.notifySelectedShapesChanged(); + } + return false; + } + ); + } + + this.createHandles(); + + this.drawShape(); +}; + +Line.prototype.toJson = function toJson() { + var rv = { + 'type': 'Line', + 'x1': this._x1, + 'x2': this._x2, + 'y1': this._y1, + 'y2': this._y2, + 'strokeWidth': this._strokeWidth, + 'strokeColor': this._strokeColor + }; + if (this._id) { + rv.id = this._id; + } + return rv; +}; + +Line.prototype.compareCoords = function compareCoords(json) { + + var selfJson = this.toJson(), + match = true; + if (json.type !== selfJson.type) { + return false; + } + ['x1', 'y1', 'x2', 'y2'].forEach(function(c){ + if (json[c] !== selfJson[c]) { + match = false; + } + }); + return match; +}; + +// Useful for pasting json with an offset +Line.prototype.offsetCoords = function offsetCoords(json, dx, dy) { + json.x1 = json.x1 + dx; + json.y1 = json.y1 + dy; + json.x2 = json.x2 + dx; + json.y2 = json.y2 + dy; + return json; +}; + +// Shift this shape by dx and dy +Line.prototype.offsetShape = function offsetShape(dx, dy) { + this._x1 = this._x1 + dx; + this._y1 = this._y1 + dy; + this._x2 = this._x2 + dx; + this._y2 = this._y2 + dy; + this.drawShape(); +}; + +// handle start of drag by selecting this shape +// if not already selected +Line.prototype._handleMousedown = function _handleMousedown() { + if (!this._selected) { + this.manager.selectShapes([this]); + } +}; + +Line.prototype.setCoords = function setCoords(coords) { + this._x1 = coords.x1 || this._x1; + this._y1 = coords.y1 || this._y1; + this._x2 = coords.x2 || this._x2; + this._y2 = coords.y2 || this._y2; + this.drawShape(); +}; + +Line.prototype.getCoords = function getCoords() { + return {'x1': this._x1, + 'y1': this._y1, + 'x2': this._x2, + 'y2': this._y2}; +}; + +Line.prototype.setStrokeColor = function setStrokeColor(strokeColor) { + this._strokeColor = strokeColor; + this.drawShape(); +}; + +Line.prototype.getStrokeColor = function getStrokeColor() { + return this._strokeColor; +}; + +Line.prototype.setStrokeWidth = function setStrokeWidth(strokeWidth) { + this._strokeWidth = strokeWidth; + this.drawShape(); +}; + +Line.prototype.getStrokeWidth = function getStrokeWidth() { + return this._strokeWidth; +}; + +Line.prototype.destroy = function destroy() { + this.element.remove(); + this.handles.remove(); +}; + +Line.prototype.intersectRegion = function intersectRegion(region) { + var path = this.manager.regionToPath(region, this._zoomFraction * 100); + var f = this._zoomFraction, + x = parseInt(this._x1 * f, 10), + y = parseInt(this._y1 * f, 10); + + if (Raphael.isPointInsidePath(path, x, y)) { + return true; + } + var path2 = this.getPath(), + i = Raphael.pathIntersection(path, path2); + return (i.length > 0); +}; + +Line.prototype.getPath = function getPath() { + var f = this._zoomFraction, + x1 = this._x1 * f, + y1 = this._y1 * f, + x2 = this._x2 * f, + y2 = this._y2 * f; + return "M" + x1 + " " + y1 + "L" + x2 + " " + y2; +}; + +Line.prototype.isSelected = function isSelected() { + return this._selected; +}; + +Line.prototype.setZoom = function setZoom(zoom) { + this._zoomFraction = zoom / 100; + this.drawShape(); +}; + +Line.prototype._getLineWidth = function _getLineWidth() { + return this._strokeWidth; +}; + +Line.prototype.drawShape = function drawShape() { + + var p = this.getPath(), + strokeColor = this._strokeColor, + strokeW = this._getLineWidth(); + + this.element.attr({'path': p, + 'stroke': strokeColor, + 'fill': strokeColor, + 'stroke-width': strokeW}); + + if (this.isSelected()) { + this.element.toFront(); + this.handles.show().toFront(); + } else { + this.handles.hide(); + } + + // update Handles + var handleIds = this.getHandleCoords(); + var hnd, h_id, hx, hy; + for (var h=0, l=this.handles.length; h 1) { + self._y1 = this.old.y2; + } else if (Math.abs(x/y) < 1){ + self._x1 = this.old.x2; + } + } + } + if (this.h_id === "end" || this.h_id === "middle") { + self._x2 = this.old.x2 + dx; + self._y2 = this.old.y2 + dy; + // if shift, make line horizontal or vertical + if (event.shiftKey && this.h_id === "end") { + x = self._x1 - self._x2; + y = self._y1 - self._y2; + if (Math.abs(x/y) > 1) { + self._y2 = this.old.y1; + } else if (Math.abs(x/y) < 1){ + self._x2 = this.old.x1; + } + } + } + self.drawShape(); + return false; + }; + }; + var _handle_drag_start = function() { + return function () { + // START drag: cache the starting coords of the line + this.old = { + 'x1': self._x1, + 'x2': self._x2, + 'y1': self._y1, + 'y2': self._y2 + }; + return false; + }; + }; + var _handle_drag_end = function() { + return function() { + // notify manager if line has moved + if (self._x1 !== this.old.x1 || self._y1 !== this.old.y1 || + self._x2 !== this.old.x2 || self._y2 !== this.old.y2) { + self.manager.notifyShapesChanged([self]); + } + return false; + }; + }; + + var hsize = this.handle_wh, + hx, hy, handle; + for (var key in handleIds) { + hx = handleIds[key].x; + hy = handleIds[key].y; + handle = this.paper.rect(hx-hsize/2, hy-hsize/2, hsize, hsize); + handle.attr({'cursor': 'move'}); + handle.h_id = key; + handle.line = self; + + if (this.manager.canEdit) { + handle.drag( + _handle_drag(), + _handle_drag_start(), + _handle_drag_end() + ); + } + self.handles.push(handle); + } + self.handles.attr(handleAttrs).hide(); // show on selection +}; + +Line.prototype.getHandleCoords = function getHandleCoords() { + var f = this._zoomFraction, + x1 = this._x1 * f, + y1 = this._y1 * f, + x2 = this._x2 * f, + y2 = this._y2 * f; + return {'start': {x: x1, y: y1}, + 'middle': {x: (x1+x2)/2, y: (y1+y2)/2}, + 'end': {x: x2, y: y2} + }; +}; + + + +var Arrow = function Arrow(options) { + + var that = new Line(options); + + var toJ = that.toJson; + + that.toJson = function toJson() { + var lineJson = toJ.call(that); + lineJson.type = "Arrow"; + return lineJson; + }; + + // Since we draw arrow by outline, always use thin line + that._getLineWidth = function _getLineWidth() { + return 0; + }; + + that.getPath = function getPath() { + + // We want the arrow tip to be precisely at x2, y2, so we + // can't have a fat line at x2, y2. Instead we need to + // trace the whole outline of the arrow with a thin line + + var zf = this._zoomFraction, + x1 = this._x1 * zf, + y1 = this._y1 * zf, + x2 = this._x2 * zf, + y2 = this._y2 * zf, + w = this._strokeWidth * 0.5; + + var headSize = (this._strokeWidth * 4) + 5, + dx = x2 - x1, + dy = y2 - y1; + + var lineAngle = Math.atan(dx / dy); + var f = (dy < 0 ? 1 : -1); + + // Angle of arrow head is 0.8 radians (0.4 either side of lineAngle) + var arrowPoint1x = x2 + (f * Math.sin(lineAngle - 0.4) * headSize), + arrowPoint1y = y2 + (f * Math.cos(lineAngle - 0.4) * headSize), + arrowPoint2x = x2 + (f * Math.sin(lineAngle + 0.4) * headSize), + arrowPoint2y = y2 + (f * Math.cos(lineAngle + 0.4) * headSize), + arrowPointMidx = x2 + (f * Math.sin(lineAngle) * headSize * 0.5), + arrowPointMidy = y2 + (f * Math.cos(lineAngle) * headSize * 0.5); + + var lineOffsetX = f * Math.cos(lineAngle) * w, + lineOffsetY = f * Math.sin(lineAngle) * w, + startLeftX = x1 - lineOffsetX, + startLeftY = y1 + lineOffsetY, + startRightX = x1 + lineOffsetX, + startRightY = y1 - lineOffsetY, + endLeftX = arrowPointMidx - lineOffsetX, + endLeftY = arrowPointMidy + lineOffsetY, + endRightX = arrowPointMidx + lineOffsetX, + endRightY = arrowPointMidy - lineOffsetY; + + // Outline goes around the 'line' (starting in middle of arrowhead) + var linePath = "M" + endRightX + " " + endRightY + " L" + endLeftX + " " + endLeftY; + linePath = linePath + " L" + startLeftX + " " + startLeftY + " L" + startRightX + " " + startRightY; + linePath = linePath + " L" + endRightX + " " + endRightY; + + // Then goes around the arrow head enough to fill it all in! + var arrowPath = linePath + " L" + arrowPoint1x + " " + arrowPoint1y + " L" + arrowPoint2x + " " + arrowPoint2y; + arrowPath = arrowPath + " L" + x2 + " " + y2 + " L" + arrowPoint1x + " " + arrowPoint1y + " L" + x2 + " " + y2; + arrowPath = arrowPath + " L" + arrowPoint1x + " " + arrowPoint1y; + return arrowPath; + }; + + // since we've over-ridden getPath() after it is called + // during new Line(options) + // we need to call it again! + that.drawShape(); + + return that; +}; + + + +// Class for creating Lines. +var CreateLine = function CreateLine(options) { + + this.paper = options.paper; + this.manager = options.manager; +}; + +CreateLine.prototype.startDrag = function startDrag(startX, startY) { + + var strokeColor = this.manager.getStrokeColor(), + strokeWidth = this.manager.getStrokeWidth(), + zoom = this.manager.getZoom(); + + this.startX = startX; + this.startY = startY; + + this.line = new Line({ + 'manager': this.manager, + 'paper': this.paper, + 'x1': startX, + 'y1': startY, + 'x2': startX, + 'y2': startY, + 'strokeWidth': strokeWidth, + 'zoom': zoom, + 'strokeColor': strokeColor}); +}; + +CreateLine.prototype.drag = function drag(dragX, dragY, shiftKey) { + + // if shiftKey, constrain to horizontal / vertical + if (shiftKey) { + var dx = this.startX - dragX, + dy = this.startY - dragY; + + if (Math.abs(dx/dy) > 1) { + dy = 0; + } else { + dx = 0; + } + dragX = (dx - this.startX) * -1; + dragY = (dy - this.startY) * -1; + } + + this.line.setCoords({'x2': dragX, 'y2': dragY}); +}; + +CreateLine.prototype.stopDrag = function stopDrag() { + + var coords = this.line.getCoords(); + if ((Math.abs(coords.x1 - coords.x2) < 2) && + (Math.abs(coords.y1 - coords.y2) < 2)) { + this.line.destroy(); + delete this.line; + return; + } + // on the 'new:shape' trigger, this shape will already be selected + this.line.setSelected(true); + this.manager.addShape(this.line); +}; + + +var CreateArrow = function CreateArrow(options) { + + var that = new CreateLine(options); + + that.startDrag = function startDrag(startX, startY) { + var strokeColor = this.manager.getStrokeColor(), + strokeWidth = this.manager.getStrokeWidth(), + zoom = this.manager.getZoom(); + + this.startX = startX; + this.startY = startY; + + this.line = new Arrow({ + 'manager': this.manager, + 'paper': this.paper, + 'x1': startX, + 'y1': startY, + 'x2': startX, + 'y2': startY, + 'strokeWidth': strokeWidth, + 'zoom': zoom, + 'strokeColor': strokeColor}); + }; + + return that; +}; + +/* globals Raphael: false */ +/* globals console: false */ + +var Rect = function Rect(options) { + + var self = this; + this.paper = options.paper; + this.manager = options.manager; + + if (options.id) { + this._id = options.id; + } else { + this._id = this.manager.getRandomId(); + } + this._x = options.x; + this._y = options.y; + this._width = options.width; + this._height = options.height; + this._strokeColor = options.strokeColor; + this._strokeWidth = options.strokeWidth || 2; + this._selected = false; + this._zoomFraction = 1; + if (options.zoom) { + this._zoomFraction = options.zoom / 100; + } + this.handle_wh = 6; + + this.element = this.paper.rect(); + this.element.attr({'fill-opacity': 0.01, + 'fill': '#fff', + 'cursor': 'pointer'}); + + if (this.manager.canEdit) { + // Drag handling of element + this.element.drag( + function(dx, dy) { + // DRAG, update location and redraw + dx = dx / self._zoomFraction; + dy = dy / self._zoomFraction; + + var offsetX = dx - this.prevX; + var offsetY = dy - this.prevY; + this.prevX = dx; + this.prevY = dy; + + // Manager handles move and redraw + self.manager.moveSelectedShapes(offsetX, offsetY, true); + }, + function() { + self._handleMousedown(); + this.prevX = 0; + this.prevY = 0; + return false; + }, + function() { + // STOP + // notify manager if rectangle has moved + if (this.prevX !== 0 || this.prevY !== 0) { + self.manager.notifySelectedShapesChanged(); + } + return false; + } + ); + } + + this.createHandles(); + + this.drawShape(); +}; + +Rect.prototype.toJson = function toJson() { + var rv = { + 'type': 'Rectangle', + 'x': this._x, + 'y': this._y, + 'width': this._width, + 'height': this._height, + 'strokeWidth': this._strokeWidth, + 'strokeColor': this._strokeColor + }; + if (this._id) { + rv.id = this._id; + } + return rv; +}; + +// Does this intersect a 'region' as defined by MODEL coords (not zoom dependent) +Rect.prototype.intersectRegion = function intersectRegion(region) { + var path = this.manager.regionToPath(region, this._zoomFraction * 100); + var f = this._zoomFraction, + x = parseInt(this._x * f, 10), + y = parseInt(this._y * f, 10); + + if (Raphael.isPointInsidePath(path, x, y)) { + return true; + } + var path2 = this.getPath(), + i = Raphael.pathIntersection(path, path2); + return (i.length > 0); +}; + +// Useful for testing intersection of paths +Rect.prototype.getPath = function getPath() { + + var f = this._zoomFraction, + x = parseInt(this._x * f, 10), + y = parseInt(this._y * f, 10), + width = parseInt(this._width * f, 10), + height = parseInt(this._height * f, 10); + + var cornerPoints = [ + [x, y], + [x + width, y], + [x + width, y + height], + [x, y + height] + ]; + var path = []; + for (var i = 0; i <= 3; i++) { + if (i === 0) { + path.push("M" + cornerPoints[0].join(",")); + } + if (i < 3) { + path.push("L" + cornerPoints[i + 1].join(",")); + } else { + path.push("Z"); + } + } + return path.join(","); +}; + +Rect.prototype.compareCoords = function compareCoords(json) { + if (json.type !== "Rectangle") { + return false; + } + var selfJson = this.toJson(), + match = true; + ['x', 'y', 'width', 'height'].forEach(function(c){ + if (json[c] !== selfJson[c]) { + match = false; + } + }); + return match; +}; + +// Useful for pasting json with an offset +Rect.prototype.offsetCoords = function offsetCoords(json, dx, dy) { + json.x = json.x + dx; + json.y = json.y + dy; + return json; +}; + +// Shift this shape by dx and dy +Rect.prototype.offsetShape = function offsetShape(dx, dy) { + this._x = this._x + dx; + this._y = this._y + dy; + this.drawShape(); +}; + +// handle start of drag by selecting this shape +// if not already selected +Rect.prototype._handleMousedown = function _handleMousedown() { + if (!this._selected) { + this.manager.selectShapes([this]); + } +}; + +Rect.prototype.setSelected = function setSelected(selected) { + this._selected = !!selected; + this.drawShape(); +}; + +Rect.prototype.isSelected = function isSelected() { + return this._selected; +}; + +Rect.prototype.setZoom = function setZoom(zoom) { + this._zoomFraction = zoom / 100; + this.drawShape(); +}; + +Rect.prototype.setCoords = function setCoords(coords) { + this._x = coords.x || this._x; + this._y = coords.y || this._y; + this._width = coords.width || this._width; + this._height = coords.height || this._height; + this.drawShape(); +}; + +Rect.prototype.getCoords = function getCoords() { + return {'x': this._x, + 'y': this._y, + 'width': this._width, + 'height': this._height}; +}; + +Rect.prototype.setStrokeColor = function setStrokeColor(strokeColor) { + this._strokeColor = strokeColor; + this.drawShape(); +}; + +Rect.prototype.getStrokeColor = function getStrokeColor() { + return this._strokeColor; +}; + +Rect.prototype.setStrokeWidth = function setStrokeWidth(strokeWidth) { + this._strokeWidth = strokeWidth; + this.drawShape(); +}; + +Rect.prototype.getStrokeWidth = function getStrokeWidth() { + return this._strokeWidth; +}; + +Rect.prototype.destroy = function destroy() { + this.element.remove(); + this.handles.remove(); +}; + +Rect.prototype.drawShape = function drawShape() { + + var strokeColor = this._strokeColor, + lineW = this._strokeWidth; + + var f = this._zoomFraction, + x = this._x * f, + y = this._y * f, + w = this._width * f, + h = this._height * f; + + this.element.attr({'x':x, 'y':y, + 'width':w, 'height':h, + 'stroke': strokeColor, + 'stroke-width': lineW}); + + if (this.isSelected()) { + this.element.toFront(); + this.handles.show().toFront(); + } else { + this.handles.hide(); + } + + // update Handles + var handleIds = this.getHandleCoords(); + var hnd, h_id, hx, hy; + for (var i=0, l=this.handles.length; i this.aspect) { + dy = dx/this.aspect; + } else { + dx = dy*this.aspect; + } + } else { + if (Math.abs(dx/dy) > this.aspect) { + dy = -dx/this.aspect; + } else { + dx = -dy*this.aspect; + } + } + } + // Use dx & dy to update the location of the handle and the corresponding point of the parent + var new_x = this.ox + dx; + var new_y = this.oy + dy; + var newRect = { + x: self._x, + y: self._y, + width: self._width, + height: self._height + }; + if (this.h_id.indexOf('e') > -1) { // if we're dragging an 'EAST' handle, update width + newRect.width = new_x - self._x + self.handle_wh/2; + } + if (this.h_id.indexOf('s') > -1) { // if we're dragging an 'SOUTH' handle, update height + newRect.height = new_y - self._y + self.handle_wh/2; + } + if (this.h_id.indexOf('n') > -1) { // if we're dragging an 'NORTH' handle, update y and height + newRect.y = new_y + self.handle_wh/2; + newRect.height = this.oheight - dy; + } + if (this.h_id.indexOf('w') > -1) { // if we're dragging an 'WEST' handle, update x and width + newRect.x = new_x + self.handle_wh/2; + newRect.width = this.owidth - dx; + } + // Don't allow zero sized rect. + if (newRect.width < 1 || newRect.height < 1) { + return false; + } + + self._x = newRect.x; + self._y = newRect.y; + self._width = newRect.width; + self._height = newRect.height; + self.drawShape(); + return false; + }; + }; + var _handle_drag_start = function() { + return function () { + // START drag: simply note the location we started + this.ox = this.attr("x") / self._zoomFraction; + this.oy = this.attr("y") / self._zoomFraction; + this.owidth = self._width; + this.oheight = self._height; + this.aspect = self._width / self._height; + return false; + }; + }; + var _handle_drag_end = function() { + return function() { + if (this.owidth !== self._width || this.oheight !== self._height) { + self.manager.notifyShapesChanged([self]); + } + return false; + }; + }; + // var _stop_event_propagation = function(e) { + // e.stopImmediatePropagation(); + // } + for (var key in handleIds) { + var hx = handleIds[key][0]; + var hy = handleIds[key][1]; + var handle = this.paper.rect(hx-self.handle_wh/2, hy-self.handle_wh/2, self.handle_wh, self.handle_wh).attr(handle_attrs); + handle.attr({'cursor': key + '-resize'}); // css, E.g. ne-resize + handle.h_id = key; + handle.rect = self; + + if (self.manager.canEdit) { + handle.drag( + _handle_drag(), + _handle_drag_start(), + _handle_drag_end() + ); + } + // handle.mousedown(_stop_event_propagation); + self.handles.push(handle); + } + self.handles.hide(); // show on selection +}; + + + +// Class for creating Lines. +var CreateRect = function CreateRect(options) { + + this.paper = options.paper; + this.manager = options.manager; +}; + +CreateRect.prototype.startDrag = function startDrag(startX, startY) { + + var strokeColor = this.manager.getStrokeColor(), + strokeWidth = this.manager.getStrokeWidth(), + zoom = this.manager.getZoom(); + // Also need to get strokeWidth and zoom/size etc. + + this.startX = startX; + this.startY = startY; + + this.rect = new Rect({ + 'manager': this.manager, + 'paper': this.paper, + 'x': startX, + 'y': startY, + 'width': 0, + 'height': 0, + 'strokeWidth': strokeWidth, + 'zoom': zoom, + 'strokeColor': strokeColor}); +}; + +CreateRect.prototype.drag = function drag(dragX, dragY, shiftKey) { + + var dx = this.startX - dragX, + dy = this.startY - dragY; + + // if shiftKey, constrain to a square + if (shiftKey) { + if (dx * dy > 0) { + if (Math.abs(dx/dy) > 1) { + dy = dx; + } else { + dx = dy; + } + } else { + if (Math.abs(dx/dy) > 1) { + dy = -dx; + } else { + dx = -dy; + } + } + dragX = (dx - this.startX) * -1; + dragY = (dy - this.startY) * -1; + } + + this.rect.setCoords({'x': Math.min(dragX, this.startX), + 'y': Math.min(dragY, this.startY), + 'width': Math.abs(dx), 'height': Math.abs(dy)}); +}; + +CreateRect.prototype.stopDrag = function stopDrag() { + + var coords = this.rect.getCoords(); + if (coords.width < 2 || coords.height < 2) { + this.rect.destroy(); + delete this.rect; + return; + } + // on the 'new:shape' trigger, this shape will already be selected + this.rect.setSelected(true); + this.manager.addShape(this.rect); +}; + +/* globals Raphael: false */ +/* globals console: false */ + +var Ellipse = function Ellipse(options) { + + var self = this; + this.manager = options.manager; + this.paper = options.paper; + + if (options.id) { + this._id = options.id; + } else { + this._id = this.manager.getRandomId(); + } + this._x = options.x; + this._y = options.y; + this._radiusX = options.radiusX; + this._radiusY = options.radiusY; + this._rotation = options.rotation || 0; + + // We handle transform matrix by creating this.Matrix + // This is used as a one-off transform of the handles positions + // when they are created. This then updates the _x, _y, _radiusX, _radiusY & rotation + // of the ellipse itself (see below) + if (options.transform && options.transform.startsWith('matrix')) { + var tt = options.transform.replace('matrix(', '').replace(')', '').split(" "); + var a1 = parseFloat(tt[0]); + var a2 = parseFloat(tt[1]); + var b1 = parseFloat(tt[2]); + var b2 = parseFloat(tt[3]); + var c1 = parseFloat(tt[4]); + var c2 = parseFloat(tt[5]); + this.Matrix = Raphael.matrix(a1, a2, b1, b2, c1, c2); + } + + if (this._radiusX === 0 || this._radiusY === 0) { + this._yxRatio = 0.5; + } else { + this._yxRatio = this._radiusY / this._radiusX; + } + + this._strokeColor = options.strokeColor; + this._strokeWidth = options.strokeWidth || 2; + this._selected = false; + this._zoomFraction = 1; + if (options.zoom) { + this._zoomFraction = options.zoom / 100; + } + this.handle_wh = 6; + + this.element = this.paper.ellipse(); + this.element.attr({'fill-opacity': 0.01, + 'fill': '#fff', + 'cursor': 'pointer'}); + + // Drag handling of ellipse + if (this.manager.canEdit) { + this.element.drag( + function(dx, dy) { + // DRAG, update location and redraw + dx = dx / self._zoomFraction; + dy = dy / self._zoomFraction; + + var offsetX = dx - this.prevX; + var offsetY = dy - this.prevY; + this.prevX = dx; + this.prevY = dy; + + // Manager handles move and redraw + self.manager.moveSelectedShapes(offsetX, offsetY, true); + return false; + }, + function() { + // START drag: note the start location + self._handleMousedown(); + this.prevX = 0; + this.prevY = 0; + return false; + }, + function() { + // STOP + // notify changed if moved + if (this.prevX !== 0 || this.prevY !== 0) { + self.manager.notifySelectedShapesChanged(); + } + return false; + } + ); + } + + // create handles, applying this.Matrix if set + this.createHandles(); + // update x, y, radiusX, radiusY & rotation + // If we have Matrix, recalculate width/height ratio based on all handles + var resizeWidth = !!this.Matrix; + this.updateShapeFromHandles(resizeWidth); + // and draw the Ellipse + this.drawShape(); +}; + +Ellipse.prototype.toJson = function toJson() { + var rv = { + 'type': "Ellipse", + 'x': this._x, + 'y': this._y, + 'radiusX': this._radiusX, + 'radiusY': this._radiusY, + 'rotation': this._rotation, + 'strokeWidth': this._strokeWidth, + 'strokeColor': this._strokeColor + }; + if (this._id) { + rv.id = this._id; + } + return rv; +}; + +Ellipse.prototype.compareCoords = function compareCoords(json) { + + var selfJson = this.toJson(), + match = true; + if (json.type !== selfJson.type) { + return false; + } + ['x', 'y', 'radiusX', 'radiusY', 'rotation'].forEach(function(c){ + if (Math.round(json[c]) !== Math.round(selfJson[c])) { + match = false; + } + }); + return match; +}; + +// Useful for pasting json with an offset +Ellipse.prototype.offsetCoords = function offsetCoords(json, dx, dy) { + json.x = json.x + dx; + json.y = json.y + dy; + return json; +}; + +// Shift this shape by dx and dy +Ellipse.prototype.offsetShape = function offsetShape(dx, dy) { + this._x = this._x + dx; + this._y = this._y + dy; + this.drawShape(); +}; + +// handle start of drag by selecting this shape +// if not already selected +Ellipse.prototype._handleMousedown = function _handleMousedown() { + if (!this._selected) { + this.manager.selectShapes([this]); + } +}; + +Ellipse.prototype.setColor = function setColor(strokeColor) { + this._strokeColor = strokeColor; + this.drawShape(); +}; + +Ellipse.prototype.getStrokeColor = function getStrokeColor() { + return this._strokeColor; +}; + +Ellipse.prototype.setStrokeColor = function setStrokeColor(strokeColor) { + this._strokeColor = strokeColor; + this.drawShape(); +}; + +Ellipse.prototype.setStrokeWidth = function setStrokeWidth(strokeWidth) { + this._strokeWidth = strokeWidth; + this.drawShape(); +}; + +Ellipse.prototype.getStrokeWidth = function getStrokeWidth() { + return this._strokeWidth; +}; + +Ellipse.prototype.destroy = function destroy() { + this.element.remove(); + this.handles.remove(); +}; + +Ellipse.prototype.intersectRegion = function intersectRegion(region) { + var path = this.manager.regionToPath(region, this._zoomFraction * 100); + var f = this._zoomFraction, + x = parseInt(this._x * f, 10), + y = parseInt(this._y * f, 10); + + if (Raphael.isPointInsidePath(path, x, y)) { + return true; + } + var path2 = this.getPath(), + i = Raphael.pathIntersection(path, path2); + return (i.length > 0); +}; + +Ellipse.prototype.getPath = function getPath() { + + // Adapted from https://github.com/poilu/raphael-boolean + var a = this.element.attrs, + radiusX = a.radiusX, + radiusY = a.radiusY, + cornerPoints = [ + [a.x - radiusX, a.y - radiusY], + [a.x + radiusX, a.y - radiusY], + [a.x + radiusX, a.y + radiusY], + [a.x - radiusX, a.y + radiusY] + ], + path = []; + var radiusShift = [ + [ + [0, 1], + [1, 0] + ], + [ + [-1, 0], + [0, 1] + ], + [ + [0, -1], + [-1, 0] + ], + [ + [1, 0], + [0, -1] + ] + ]; + + //iterate all corners + for (var i = 0; i <= 3; i++) { + //insert starting point + if (i === 0) { + path.push(["M", cornerPoints[0][0], cornerPoints[0][1] + radiusY]); + } + + //insert "curveto" (radius factor .446 is taken from Inkscape) + var c1 = [cornerPoints[i][0] + radiusShift[i][0][0] * radiusX * 0.446, cornerPoints[i][1] + radiusShift[i][0][1] * radiusY * 0.446]; + var c2 = [cornerPoints[i][0] + radiusShift[i][1][0] * radiusX * 0.446, cornerPoints[i][1] + radiusShift[i][1][1] * radiusY * 0.446]; + var p2 = [cornerPoints[i][0] + radiusShift[i][1][0] * radiusX, cornerPoints[i][1] + radiusShift[i][1][1] * radiusY]; + path.push(["C", c1[0], c1[1], c2[0], c2[1], p2[0], p2[1]]); + } + path.push(["Z"]); + path = path.join(",").replace(/,?([achlmqrstvxz]),?/gi, "$1"); + + if (this._rotation !== 0) { + path = Raphael.transformPath(path, "r" + this._rotation); + } + return path; +}; + +Ellipse.prototype.isSelected = function isSelected() { + return this._selected; +}; + +Ellipse.prototype.setZoom = function setZoom(zoom) { + this._zoomFraction = zoom / 100; + this.drawShape(); +}; + +Ellipse.prototype.updateHandle = function updateHandle(handleId, x, y, shiftKey) { + // Refresh the handle coordinates, then update the specified handle + // using MODEL coordinates + this._handleIds = this.getHandleCoords(); + var h = this._handleIds[handleId]; + h.x = x; + h.y = y; + var resizeWidth = (handleId === "left" || handleId === "right"); + this.updateShapeFromHandles(resizeWidth, shiftKey); +}; + +Ellipse.prototype.updateShapeFromHandles = function updateShapeFromHandles(resizeWidth, shiftKey) { + var hh = this._handleIds, + lengthX = hh.end.x - hh.start.x, + lengthY = hh.end.y - hh.start.y, + widthX = hh.left.x - hh.right.x, + widthY = hh.left.y - hh.right.y, + rot; + // Use the 'start' and 'end' handles to get rotation and length + if (lengthX === 0){ + this._rotation = 90; + } else if (lengthX > 0) { + rot = Math.atan(lengthY / lengthX); + this._rotation = Raphael.deg(rot); + } else if (lengthX < 0) { + rot = Math.atan(lengthY / lengthX); + this._rotation = 180 + Raphael.deg(rot); + } + + // centre is half-way between 'start' and 'end' handles + this._x = (hh.start.x + hh.end.x)/2; + this._y = (hh.start.y + hh.end.y)/2; + // Radius-x is half of distance between handles + this._radiusX = Math.sqrt((lengthX * lengthX) + (lengthY * lengthY)) / 2; + // Radius-y may depend on handles OR on x/y ratio + if (resizeWidth) { + this._radiusY = Math.sqrt((widthX * widthX) + (widthY * widthY)) / 2; + this._yxRatio = this._radiusY / this._radiusX; + } else { + if (shiftKey) { + this._yxRatio = 1; + } + this._radiusY = this._yxRatio * this._radiusX; + } + + this.drawShape(); +}; + +Ellipse.prototype.drawShape = function drawShape() { + + var strokeColor = this._strokeColor, + strokeW = this._strokeWidth; + + var f = this._zoomFraction, + x = this._x * f, + y = this._y * f, + radiusX = this._radiusX * f, + radiusY = this._radiusY * f; + + this.element.attr({'cx': x, + 'cy': y, + 'rx': radiusX, + 'ry': radiusY, + 'stroke': strokeColor, + 'stroke-width': strokeW}); + this.element.transform('r'+ this._rotation); + + if (this.isSelected()) { + this.element.toFront(); + this.handles.show().toFront(); + } else { + this.handles.hide(); + } + + // handles have been updated (model coords) + this._handleIds = this.getHandleCoords(); + var hnd, h_id, hx, hy; + for (var h=0, l=this.handles.length; h -1) { + console.log("Matrix only supports rotation & translation. " + matrixStr + " may contain skew for shape: ", this.toJson()); + } + var mx = this.Matrix.x(hx, hy); + var my = this.Matrix.y(hx, hy); + hx = mx; + hy = my; + // update the source coordinates + this._handleIds[key].x = hx; + this._handleIds[key].y = hy; + } + handle = this.paper.rect(hx-hsize/2, hy-hsize/2, hsize, hsize); + handle.attr({'cursor': 'move'}); + handle.h_id = key; + handle.line = self; + + if (this.manager.canEdit) { + handle.drag( + _handle_drag(), + _handle_drag_start(), + _handle_drag_end() + ); + } + self.handles.push(handle); + } + + self.handles.attr(handleAttrs).hide(); // show on selection +}; + +Ellipse.prototype.getHandleCoords = function getHandleCoords() { + // Returns MODEL coordinates (not zoom coordinates) + var rot = Raphael.rad(this._rotation), + x = this._x, + y = this._y, + radiusX = this._radiusX, + radiusY = this._radiusY, + startX = x - (Math.cos(rot) * radiusX), + startY = y - (Math.sin(rot) * radiusX), + endX = x + (Math.cos(rot) * radiusX), + endY = y + (Math.sin(rot) * radiusX), + leftX = x + (Math.sin(rot) * radiusY), + leftY = y - (Math.cos(rot) * radiusY), + rightX = x - (Math.sin(rot) * radiusY), + rightY = y + (Math.cos(rot) * radiusY); + + return {'start':{x: startX, y: startY}, + 'end':{x: endX, y: endY}, + 'left':{x: leftX, y: leftY}, + 'right':{x: rightX, y: rightY} + }; +}; + + +// Class for creating Lines. +var CreateEllipse = function CreateEllipse(options) { + + this.paper = options.paper; + this.manager = options.manager; +}; + +CreateEllipse.prototype.startDrag = function startDrag(startX, startY) { + + var strokeColor = this.manager.getStrokeColor(), + strokeWidth = this.manager.getStrokeWidth(), + zoom = this.manager.getZoom(); + + this.ellipse = new Ellipse({ + 'manager': this.manager, + 'paper': this.paper, + 'x': startX, + 'y': startY, + 'radiusX': 0, + 'radiusY': 0, + 'rotation': 0, + 'strokeWidth': strokeWidth, + 'zoom': zoom, + 'strokeColor': strokeColor}); +}; + +CreateEllipse.prototype.drag = function drag(dragX, dragY, shiftKey) { + + this.ellipse.updateHandle('end', dragX, dragY, shiftKey); +}; + +CreateEllipse.prototype.stopDrag = function stopDrag() { + + // Don't create ellipse of zero size (click, without drag) + var coords = this.ellipse.toJson(); + if (coords.radiusX < 2) { + this.ellipse.destroy(); + delete this.ellipse; + return; + } + // on the 'new:shape' trigger, this shape will already be selected + this.ellipse.setSelected(true); + this.manager.addShape(this.ellipse); +}; + +/* globals Raphael: false */ +/* globals console: false */ + +var Polygon = function Polygon(options) { + + var self = this; + this.manager = options.manager; + this.paper = options.paper; + + if (options.id) { + this._id = options.id; + } else { + this._id = this.manager.getRandomId(); + } + this._points = options.points; + + this._strokeColor = options.strokeColor; + this._strokeWidth = options.strokeWidth || 2; + this._selected = false; + this._zoomFraction = 1; + if (options.zoom) { + this._zoomFraction = options.zoom / 100; + } + this.handle_wh = 6; + + this.element = this.paper.path(""); + this.element.attr({'fill-opacity': 0.01, + 'fill': '#fff', + 'cursor': 'pointer'}); + + if (this.manager.canEdit) { + // Drag handling of element + this.element.drag( + function(dx, dy) { + if (self._zoomFraction === 0) { + return; // just in case + } + // DRAG, update location and redraw + dx = dx / self._zoomFraction; + dy = dy / self._zoomFraction; + + var offsetX = dx - this.prevX; + var offsetY = dy - this.prevY; + this.prevX = dx; + this.prevY = dy; + + // Manager handles move and redraw + self.manager.moveSelectedShapes(offsetX, offsetY, true); + }, + function() { + self._handleMousedown(); + this.prevX = 0; + this.prevY = 0; + return false; + }, + function() { + // STOP + // notify manager if rectangle has moved + if (this.prevX !== 0 || this.prevY !== 0) { + self.manager.notifySelectedShapesChanged(); + } + return false; + } + ); + } + + // create handles... + this.createHandles(); + // and draw the Polygon + this.drawShape(); +}; + +Polygon.prototype.toJson = function toJson() { + var rv = { + 'type': "Polygon", + 'points': this._points, + 'strokeWidth': this._strokeWidth, + 'strokeColor': this._strokeColor + }; + if (this._id) { + rv.id = this._id; + } + return rv; +}; + +Polygon.prototype.compareCoords = function compareCoords(json) { + + var selfJson = this.toJson(), + match = true; + if (json.type !== selfJson.type) { + return false; + } + return json.points === selfJson.points; +}; + +// Useful for pasting json with an offset +Polygon.prototype.offsetCoords = function offsetCoords(json, dx, dy) { + json.points = json.points.split(" ").map(function(xy){ + return xy.split(",").map(function(c, i){ + return parseFloat(c, 10) + [dx, dy][i] + }).join(",") + }).join(" "); + return json; +}; + +// Shift this shape by dx and dy +Polygon.prototype.offsetShape = function offsetShape(dx, dy) { + // Offset all coords in points string "229,171 195,214 195,265 233,33" + var points = this._points.split(" ").map(function(xy){ + return xy.split(",").map(function(c, i){ + return parseFloat(c, 10) + [dx, dy][i] + }).join(",") + }).join(" "); + this._points = points; + this.drawShape(); +}; + +// handle start of drag by selecting this shape +// if not already selected +Polygon.prototype._handleMousedown = function _handleMousedown() { + if (!this._selected) { + this.manager.selectShapes([this]); + } +}; + +Polygon.prototype.setColor = function setColor(strokeColor) { + this._strokeColor = strokeColor; + this.drawShape(); +}; + +Polygon.prototype.getStrokeColor = function getStrokeColor() { + return this._strokeColor; +}; + +Polygon.prototype.setStrokeColor = function setStrokeColor(strokeColor) { + this._strokeColor = strokeColor; + this.drawShape(); +}; + +Polygon.prototype.setStrokeWidth = function setStrokeWidth(strokeWidth) { + this._strokeWidth = strokeWidth; + this.drawShape(); +}; + +Polygon.prototype.getStrokeWidth = function getStrokeWidth() { + return this._strokeWidth; +}; + +Polygon.prototype.destroy = function destroy() { + this.element.remove(); + this.handles.remove(); +}; + +Polygon.prototype.intersectRegion = function intersectRegion(region) { + // region is {x, y, width, height} - Model coords (not zoomed) + // Compare with model coords of points... + + // Get bounding box from points... + var coords = this._points.split(" ").reduce(function(prev, xy){ + var x = parseInt(xy.split(',')[0], 10); + var y = parseInt(xy.split(',')[1], 10); + if (!prev) { + prev = {'min_x': x, 'min_y': y, 'max_x': x, 'max_y': y}; + } else { + prev.min_x = Math.min(prev.min_x, x); + prev.min_y = Math.min(prev.min_y, y); + prev.max_x = Math.max(prev.max_x, x); + prev.max_y = Math.max(prev.max_y, y); + } + return prev; + }, undefined); + + // check for overlap - NB: this may return True even if no intersection + // since Polygon doesn't fill it's bounding box + if (coords.min_x > (region.x + region.width) || + coords.min_y > (region.y + region.height) || + coords.max_x < region.x || + coords.max_y < region.y) { + return false; + } + return true; +}; + +Polygon.prototype.getPath = function getPath() { + // Convert points string "229,171 195,214 195,265 233,33" + // to Raphael path "M229,171L195,214L195,265L233,33Z" + // Handles scaling by zoomFraction + var f = this._zoomFraction; + var path = this._points.split(" ").map(function(xy){ + return xy.split(",").map(function(c){return parseInt(c, 10) * f}).join(","); + }).join("L"); + path = "M" + path + "Z"; + return path; +}; + +Polygon.prototype.isSelected = function isSelected() { + return this._selected; +}; + +Polygon.prototype.setZoom = function setZoom(zoom) { + this._zoomFraction = zoom / 100; + this.drawShape(); +}; + +Polygon.prototype.updateHandle = function updateHandle(handleIndex, x, y, shiftKey) { + var coords = this._points.split(" "); + coords[handleIndex] = x + "," + y; + this._points = coords.join(" "); +}; + +Polygon.prototype.drawShape = function drawShape() { + + var strokeColor = this._strokeColor, + strokeW = this._strokeWidth; + + var f = this._zoomFraction; + var path = this.getPath(); + + this.element.attr({'path': path, + 'stroke': strokeColor, + 'stroke-width': strokeW}); + + if (this.isSelected()) { + this.element.toFront(); + this.handles.show().toFront(); + } else { + this.handles.hide(); + } + + // handles have been updated (model coords) + var hnd, hx, hy; + this._points.split(" ").forEach(function(xy, i){ + var xy = xy.split(","); + hx = parseInt(xy[0]) * this._zoomFraction; + hy = parseInt(xy[1]) * this._zoomFraction; + hnd = this.handles[i]; + hnd.attr({'x':hx-this.handle_wh/2, 'y':hy-this.handle_wh/2}); + }.bind(this)); +}; + +Polygon.prototype.setSelected = function setSelected(selected) { + this._selected = !!selected; + this.drawShape(); +}; + + +Polygon.prototype.createHandles = function createHandles() { + // ---- Create Handles ----- + + // NB: handleIds are used to calculate coords + // so handledIds are scaled to MODEL coords, not zoomed. + + var self = this, + // map of centre-points for each handle + handleAttrs = {'stroke': '#4b80f9', + 'fill': '#fff', + 'cursor': 'move', + 'fill-opacity': 1.0}; + + // draw handles + self.handles = this.paper.set(); + var _handle_drag = function() { + return function (dx, dy, mouseX, mouseY, event) { + dx = dx / self._zoomFraction; + dy = dy / self._zoomFraction; + // on DRAG... + var absX = dx + this.ox, + absY = dy + this.oy; + self.updateHandle(this.h_id, absX, absY, event.shiftKey); + self.drawShape(); + return false; + }; + }; + var _handle_drag_start = function() { + return function () { + // START drag: simply note the location we started + // we scale by zoom to get the 'model' coordinates + this.ox = (this.attr("x") + this.attr('width')/2) / self._zoomFraction; + this.oy = (this.attr("y") + this.attr('height')/2) / self._zoomFraction; + return false; + }; + }; + var _handle_drag_end = function() { + return function() { + // simply notify manager that shape has changed + self.manager.notifyShapesChanged([self]); + return false; + }; + }; + + var hsize = this.handle_wh, + hx, hy, handle; + this._points.split(" ").forEach(function(xy, i){ + var xy = xy.split(","); + hx = parseInt(xy[0]); + hy = parseInt(xy[1]); + + handle = self.paper.rect(hx-hsize/2, hy-hsize/2, hsize, hsize); + handle.attr({'cursor': 'move'}); + handle.h_id = i; + + if (self.manager.canEdit) { + handle.drag( + _handle_drag(), + _handle_drag_start(), + _handle_drag_end() + ); + } + self.handles.push(handle); + }); + + self.handles.attr(handleAttrs).hide(); // show on selection +}; + + +var Polyline = function Polyline(options) { + var that = new Polygon(options); + + var toJ = that.toJson; + that.toJson = function toJson() { + var shapeJson = toJ.call(that); + shapeJson.type = "Polyline"; + return shapeJson; + }; + + var getPolygonPath = that.getPath; + that.getPath = function getPath() { + var polygonPath = getPolygonPath.call(that); + return polygonPath.replace("Z", ""); + } + + // since we've over-ridden getPath() after it is called + // during new Polygon(options) + // we need to call it again! + that.drawShape(); + return that; +} + +/* globals Raphael: false */ +/* globals CreateRect: false */ +/* globals Rect: false */ +/* globals CreateLine: false */ +/* globals Line: false */ +/* globals CreateArrow: false */ +/* globals Arrow: false */ +/* globals CreateEllipse: false */ +/* globals Ellipse: false */ +/* globals Polygon: false */ +/* globals Polyline: false */ +/* globals console: false */ + +var ShapeManager = function ShapeManager(elementId, width, height, options) { + + var self = this; + options = options || {}; + + // Keep track of state, strokeColor etc + this.STATES = ["SELECT", "RECT", "LINE", "ARROW", "ELLIPSE", "POLYGON"]; + this._state = "SELECT"; + this._strokeColor = "#ff0000"; + this._strokeWidth = 2; + this._orig_width = width; + this._orig_height = height; + this._zoom = 100; + // Don't allow editing of shapes - no drag/click events + this.canEdit = !options.readOnly; + + // Set up Raphael paper... + this.paper = Raphael(elementId, width, height); + + // jQuery element used for .offset() etc. + this.$el = $("#" + elementId); + + // Store all the shapes we create + this._shapes = []; + + // Add a full-size background to cover existing shapes while + // we're creating new shapes, to stop them being selected. + // Mouse events on this will bubble up to svg and are handled below + this.newShapeBg = this.paper.rect(0, 0, width, height); + this.newShapeBg.attr({'fill':'#000', + 'fill-opacity':0.01, + 'stroke-width': 0, + 'cursor': 'default'}); + this.selectRegion = this.paper.rect(0, 0, width, height); + this.selectRegion.hide().attr({'stroke': '#ddd', + 'stroke-width': 0, + 'stroke-dasharray': '- '}); + if (this.canEdit) { + this.newShapeBg.drag( + function(){ + self.drag.apply(self, arguments); + }, + function(){ + self.startDrag.apply(self, arguments); + }, + function(){ + self.stopDrag.apply(self, arguments); + }); + + this.shapeFactories = { + "RECT": new CreateRect({'manager': this, 'paper': this.paper}), + "ELLIPSE": new CreateEllipse({'manager': this, 'paper': this.paper}), + "LINE": new CreateLine({'manager': this, 'paper': this.paper}), + "ARROW": new CreateArrow({'manager': this, 'paper': this.paper}), + }; + + this.createShape = this.shapeFactories.LINE; + } else { + this.shapeFactories = {}; + } +}; + +ShapeManager.prototype.startDrag = function startDrag(x, y, event){ + // clear any existing selected shapes + this.clearSelectedShapes(); + + var offset = this.$el.offset(), + startX = x - offset.left, + startY = y - offset.top; + + if (this.getState() === "SELECT") { + + this._dragStart = {x: startX, y: startY}; + + this.selectRegion.attr({'x': startX, + 'y': startY, + 'width': 0, + 'height': 0}); + this.selectRegion.toFront().show(); + + } else { + // create a new shape with X and Y + // createShape helper can get other details itself + + // correct for zoom before passing coordinates to shape + var zoomFraction = this._zoom / 100; + startX = startX / zoomFraction; + startY = startY / zoomFraction; + this.createShape.startDrag(startX, startY); + } + + // Move this in front of new shape so that drag events don't get lost to the new shape + this.newShapeBg.toFront(); +}; + +ShapeManager.prototype.drag = function drag(dx, dy, x, y, event){ + var offset = this.$el.offset(), + dragX = x - offset.left, + dragY = y - offset.top; + + if (this.getState() === "SELECT") { + + dx = this._dragStart.x - dragX, + dy = this._dragStart.y - dragY; + + this.selectRegion.attr({'x': Math.min(dragX, this._dragStart.x), + 'y': Math.min(dragY, this._dragStart.y), + 'width': Math.abs(dx), + 'height': Math.abs(dy)}); + } else { + + // correct for zoom before passing coordinates to shape + var zoomFraction = this._zoom / 100, + shiftKey = event.shiftKey; + dragX = dragX / zoomFraction; + dragY = dragY / zoomFraction; + this.createShape.drag(dragX, dragY, shiftKey); + } +}; + +ShapeManager.prototype.stopDrag = function stopDrag(x, y, event){ + if (this.getState() === "SELECT") { + + // need to get MODEL coords (correct for zoom) + var region = this.selectRegion.attr(), + f = this._zoom/100, + sx = region.x / f, + sy = region.y / f, + width = region.width / f, + height = region.height / f; + this.selectShapesByRegion({x: sx, y: sy, width: width, height: height}); + + // Hide region and move drag listening element to back again. + this.selectRegion.hide(); + this.newShapeBg.toBack(); + } else { + this.createShape.stopDrag(); + } +}; + +ShapeManager.prototype.setState = function setState(state) { + if (this.STATES.indexOf(state) === -1) { + console.log("Invalid state: ", state, "Needs to be in", this.STATES); + return; + } + // When creating shapes, cover existing shapes with newShapeBg + var shapes = ["RECT", "LINE", "ARROW", "ELLIPSE", "POLYGON"]; + if (shapes.indexOf(state) > -1) { + this.newShapeBg.toFront(); + this.newShapeBg.attr({'cursor': 'crosshair'}); + // clear selected shapes + this.clearSelectedShapes(); + + if (this.shapeFactories[state]) { + this.createShape = this.shapeFactories[state]; + } + } else if (state === "SELECT") { + // Used to handle drag-select events + this.newShapeBg.toBack(); + this.newShapeBg.attr({'cursor': 'default'}); + } + + this._state = state; +}; + +ShapeManager.prototype.getState = function getState() { + return this._state; +}; + +ShapeManager.prototype.setZoom = function setZoom(zoomPercent) { + // var zoom = this.shapeEditor.get('zoom'); + + // var $imgWrapper = $(".image_wrapper"), + // currWidth = $imgWrapper.width(), + // currHeight = $imgWrapper.height(), + // currTop = parseInt($imgWrapper.css('top'), 10), + // currLeft = parseInt($imgWrapper.css('left'), 10); + + // var width = 512 * zoom / 100, + // height = 512 * zoom / 100; + // $("#shapeCanvas").css({'width': width + "px", 'height': height + "px"}); + + this._zoom = zoomPercent; + // Update the svg and our newShapeBg. + // $("svg").css({'width': width + "px", 'height': height + "px"}); + var width = this._orig_width * zoomPercent / 100, + height = this._orig_height * zoomPercent / 100; + this.paper.setSize(width, height); + this.paper.canvas.setAttribute("viewBox", "0 0 "+width+" "+height); + this.newShapeBg.attr({'width': width, 'height': height}); + + // zoom the shapes + this._shapes.forEach(function(shape){ + shape.setZoom(zoomPercent); + }); + + // // image + // $(".image_wrapper").css({'width': width + "px", 'height': height + "px"}); + // // offset + // var deltaTop = (height - currHeight) / 2, + // deltaLeft = (width - currWidth) / 2; + // $(".image_wrapper").css({'left': (currLeft - deltaLeft) + "px", + // 'top': (currTop - deltaTop) + "px"}); +}; + +ShapeManager.prototype.getZoom = function getZoom(zoomPercent) { + return this._zoom; +}; + +ShapeManager.prototype.setStrokeColor = function setStrokeColor(strokeColor) { + this._strokeColor = strokeColor; + var selected = this.getSelectedShapes(); + for (var s=0; s coords + var temp = self.createShapeJson(s); + s = temp.toJson(); + temp.destroy(); + // check if a shape is at the same coordinates... + var match = self.findShapeAtCoords(s); + // if so, keep offsetting until we find a spot... + while(match) { + s = $.extend({}, s); + s = match.offsetCoords(s, 20, 10); + match = self.findShapeAtCoords(s); + } + // Create shape and test if it's in the specified region + var added = self.addShapeJson(s, constrainRegion); + if (added) { + newShapes.push(added); + } else { + allPasted = false; + } + }); + // Select the newly added shapes + this.selectShapes(newShapes); + return allPasted; +}; + +ShapeManager.prototype.addShapesJson = function addShapesJson(jsonShapes, constrainRegion) { + var allAdded = true; + jsonShapes.forEach(function(s){ + var added = this.addShapeJson(s, constrainRegion); + if (!added) { + allAdded = false; + } + }.bind(this)); + return allAdded; +}; + +// Create and add a json shape object +// Use constrainRegion {x, y, width, height} to enforce if it's in the specified region +// constrainRegion = true will use the whole image plane +// Return false if shape didn't get created +ShapeManager.prototype.addShapeJson = function addShapeJson(jsonShape, constrainRegion) { + var newShape = this.createShapeJson(jsonShape); + if (!newShape) { + return; + } + if (constrainRegion) { + if (typeof constrainRegion === "boolean") { + constrainRegion = {x: 0, y: 0, width: this._orig_width, height: this._orig_height}; + } + if (!newShape.intersectRegion(constrainRegion)) { + newShape.destroy(); + return false; + } + } + this._shapes.push(newShape); + return newShape; +}; + +// Create a Shape object from json +ShapeManager.prototype.createShapeJson = function createShapeJson(jsonShape) { + var s = jsonShape, + newShape, + strokeColor = s.strokeColor || this.getStrokeColor(), + strokeWidth = s.strokeWidth || this.getStrokeWidth(), + zoom = this.getZoom(), + options = {'manager': this, + 'paper': this.paper, + 'strokeWidth': strokeWidth, + 'zoom': zoom, + 'strokeColor': strokeColor}; + if (jsonShape.id) { + options.id = jsonShape.id; + } + + if (s.type === 'Ellipse') { + options.x = s.x; + options.y = s.y; + options.radiusX = s.radiusX; + options.radiusY = s.radiusY; + options.rotation = s.rotation || 0; + options.transform = s.transform; + newShape = new Ellipse(options); + } + else if (s.type === 'Rectangle') { + options.x = s.x; + options.y = s.y; + options.width = s.width; + options.height = s.height; + newShape = new Rect(options); + } + else if (s.type === 'Line') { + options.x1 = s.x1; + options.y1 = s.y1; + options.x2 = s.x2; + options.y2 = s.y2; + newShape = new Line(options); + } + else if (s.type === 'Arrow') { + options.x1 = s.x1; + options.y1 = s.y1; + options.x2 = s.x2; + options.y2 = s.y2; + newShape = new Arrow(options); + } + else if (s.type === 'Polygon') { + options.points = s.points; + newShape = new Polygon(options); + } else if (s.type === 'Polyline') { + options.points = s.points; + newShape = new Polyline(options); + } + return newShape; +}; + +// Add a shape object +ShapeManager.prototype.addShape = function addShape(shape) { + this._shapes.push(shape); + this.$el.trigger("new:shape", [shape]); +}; + +ShapeManager.prototype.getShapes = function getShapes() { + return this._shapes; +}; + +ShapeManager.prototype.getShape = function getShape(shapeId) { + var shapes = this.getShapes(); + for (var i=0; i -1) { + s.destroy(); + } else { + notSelected.push(s); + } + }); + this._shapes = notSelected; + this.$el.trigger("change:selected"); +}; + +ShapeManager.prototype.deleteSelectedShapes = function deleteSelectedShapes() { + var notSelected = []; + this.getShapes().forEach(function(s) { + if (s.isSelected()) { + s.destroy(); + } else { + notSelected.push(s); + } + }); + this._shapes = notSelected; + this.$el.trigger("change:selected"); +}; + +ShapeManager.prototype.selectShapesById = function selectShapesById(shapeId) { + + // Clear selected with silent:true, since we notify again below + this.clearSelectedShapes(true); + var toSelect = []; + this.getShapes().forEach(function(shape){ + if (shape.toJson().id === shapeId) { + toSelect.push(shape); + } + }); + this.selectShapes(toSelect); +}; + +ShapeManager.prototype.clearSelectedShapes = function clearSelectedShapes(silent) { + for (var i=0; i 0) + }, + + initialize: function() { + this.panels = new PanelList(); //this.get("shapes")); + + // wrap selection notification in a 'debounce', so that many rapid + // selection changes only trigger a single re-rendering + this.notifySelectionChange = _.debounce( this.notifySelectionChange, 10); + }, + + syncOverride: function(method, model, options, error) { + this.set("unsaved", true); + }, + + load_from_OMERO: function(fileId, success) { + + var load_url = BASE_WEBFIGURE_URL + "load_web_figure/" + fileId + "/", + self = this; + + + $.getJSON(load_url, function(data){ + data.fileId = fileId; + self.load_from_JSON(data); + self.set('unsaved', false); + }); + }, + + load_from_JSON: function(data) { + var self = this; + + // bring older files up-to-date + data = self.version_transform(data); + + var name = data.figureName || "UN-NAMED", + n = {'fileId': data.fileId, + 'figureName': name, + 'groupId': data.group ? data.group.id : undefined, + 'canEdit': data.canEdit, + 'paper_width': data.paper_width, + 'paper_height': data.paper_height, + 'width_mm': data.width_mm, + 'height_mm': data.height_mm, + 'page_size': data.page_size || 'letter', + 'page_count': data.page_count, + 'paper_spacing': data.paper_spacing, + 'page_col_count': data.page_col_count, + 'orientation': data.orientation, + 'legend': data.legend, + 'legend_collapsed': data.legend_collapsed, + 'page_color': data.page_color, + }; + + // For missing attributes, we fill in with defaults + // so as to clear everything from previous figure. + n = $.extend({}, self.defaults, n); + + self.set(n); + + _.each(data.panels, function(p){ + p.selected = false; + self.panels.create(p); + }); + + // wait for undo/redo to handle above, then... + setTimeout(function() { + self.trigger("reset_undo_redo"); + }, 50); + }, + + // take Figure_JSON from a previous version, + // and transform it to latest version + version_transform: function(json) { + var v = json.version || 0; + var self = this; + + // In version 1, we have pixel_size_x and y. + // Earlier versions only have pixel_size. + if (v < 1) { + _.each(json.panels, function(p){ + var ps = p.pixel_size; + p.pixel_size_x = ps; + p.pixel_size_y = ps; + delete p.pixel_size; + }); + } + if (v < 2) { + console.log("Transforming to VERSION 2"); + _.each(json.panels, function(p){ + if (p.shapes) { + p.shapes = p.shapes.map(function(shape){ + // Update to OMERO 5.3.0 model of Ellipse + if (shape.type === "Ellipse") { + shape.x = shape.cx; + shape.y = shape.cy; + shape.radiusX = shape.rx; + shape.radiusY = shape.ry; + delete shape.cx; + delete shape.cy; + delete shape.rx; + delete shape.ry; + } + return shape; + }); + } + }); + } + if (v < 3) { + console.log("Transforming to VERSION 3"); + _.each(json.panels, function(p){ + if (p.export_dpi) { + // rename 'export_dpi' attr to 'min_export_dpi' + p.min_export_dpi = p.export_dpi; + delete p.export_dpi; + } + // update strokeWidth to page pixels/coords instead of + // image pixels. Scale according to size of panel and zoom + if (p.shapes && p.shapes.length > 0) { + var panel = new Panel(p); + var imagePixelsWidth = panel.getViewportAsRect().width; + var pageCoordsWidth = panel.get('width'); + var strokeWidthScale = pageCoordsWidth/imagePixelsWidth; + p.shapes = p.shapes.map(function(shape){ + var strokeWidth = shape.strokeWidth || 1; + strokeWidth = strokeWidth * strokeWidthScale; + // Set stroke-width to 0.25, 0.5, 0.75, 1 or greater + if (strokeWidth > 0.875) { + strokeWidth = parseInt(Math.round(strokeWidth)); + } else if (strokeWidth > 0.625) { + strokeWidth = 0.75; + } else if (strokeWidth > 0.375) { + strokeWidth = 0.5; + } else { + strokeWidth = 0.25; + } + shape.strokeWidth = strokeWidth; + return shape; + }); + } + }); + } + + if (v < 4) { + console.log("Transforming to VERSION 4"); + _.each(json.panels, function(p){ + // rename lineWidth to strokeWidth + if (p.shapes && p.shapes.length > 0) { + p.shapes = p.shapes.map(function(shape){ + shape.strokeWidth = shape.strokeWidth || shape.lineWidth || 1; + if (shape.lineWidth) { + delete shape.lineWidth; + } + return shape; + }); + } + }); + } + + if (v < 5) { + console.log("Transforming to VERSION 5"); + // scalebar now has 'units' attribute. + _.each(json.panels, function(p){ + // rename lineWidth to strokeWidth + if (p.scalebar && !p.scalebar.units) { + var units = p.pixel_size_x_unit || "MICROMETER"; + p.scalebar.units = units; + } + }); + + // Re-load timestamp info with no rounding (previous versions rounded to secs) + // Find IDs of images with deltaT + var iids = []; + _.each(json.panels, function(p){ + if (p.deltaT && iids.indexOf(p.imageId) == -1) { + iids.push(p.imageId) + } + }); + console.log('Load timestamps for images', iids); + if (iids.length > 0) { + var tsUrl = BASE_WEBFIGURE_URL + 'timestamps/'; + tsUrl += '?image=' + iids.join('&image='); + $.getJSON(tsUrl, function(data){ + // Update all panels + // NB: By the time that this callback runs, the panels will have been created + self.panels.forEach(function(p){ + var iid = p.get('imageId'); + if (data[iid] && data[iid].length > 0) { + p.set('deltaT', data[iid]); + } + }); + }); + } + } + + if (v < 6) { + console.log("Transforming to VERSION 6"); + // Adding the Z scale to the model + var iids = []; + _.each(json.panels, function(p) { + if (iids.indexOf(p.imageId) == -1) { + iids.push(p.imageId) + } + }); + if (iids.length > 0) { + zUrl = BASE_WEBFIGURE_URL + 'z_scale/'; + zUrl += '?image=' + iids.join('&image='); + $.getJSON(zUrl, function(data) { + // Update all panels + // NB: By the time that this callback runs, the panels will have been created + self.panels.forEach(function(p){ + var iid = p.get('imageId'); + if (data[iid]) { + p.set('pixel_size_z', data[iid].valueZ); + p.set('pixel_size_z_symbol', data[iid].symbolZ); + p.set('pixel_size_z_unit', data[iid].unitZ); + } + }); + }); + } + + // Converting the time-labels to V6 syntax, all other special label were converted to text + _.each(json.panels, function(p) { + for (var i=0; iThis figure has been recovered from the browser's local storage and + the local storage cleared.

`; + figureConfirmDialog( + "Figure recovered", html, ["OK"]); + } + }, + + save_to_OMERO: function(options) { + + var self = this, + figureJSON = this.figure_toJSON(); + + var url = window.SAVE_WEBFIGURE_URL, + // fileId = self.get('fileId'), + data = {}; + + if (options.fileId) { + data.fileId = options.fileId; + } + if (options.figureName) { + // Include figure name in JSON saved to file + figureJSON.figureName = options.figureName; + } + data.figureJSON = JSON.stringify(figureJSON); + + // Save + $.post( url, data) + .done(function( data ) { + var update = { + 'fileId': +data, + 'unsaved': false, + }; + if (options.figureName) { + update.figureName = options.figureName; + } + self.set(update); + + if (options.success) { + options.success(data); + } + }) + .error(function(rsp){ + console.log('Save Error', rsp.responseText); + + // Save to local storage to avoid data loss + saveFigureToStorage(figureJSON); + + var errorTitle = `Save Error: ${rsp.status}`; + var message = ` +

The current figure has failed to Save to OMERO.

+

A copy has been placed in your browser's local storage for this session + and can be recovered with File > Local Storage or by reloading the app.

+

Reloading will also check your connection to OMERO.

+ `; + var buttons = ['Close', 'Reload in new Tab']; + var callback = function(btnText) { + if (btnText === "Reload in new Tab") { + var recoverUrl = BASE_WEBFIGURE_URL + 'recover/'; + window.open(WEBLOGIN_URL + '?url=' + recoverUrl, '_blank') + } + } + figureConfirmDialog(errorTitle, message, buttons, callback); + }); + }, + + clearFigure: function() { + var figureModel = this; + figureModel.unset('fileId'); + figureModel.unset('groupId'); + figureModel.delete_panels(); + figureModel.unset("figureName"); + figureModel.set(figureModel.defaults); + figureModel.trigger('reset_undo_redo'); + }, + + addImages: function(iIds) { + this.clearSelected(); + + // approx work out number of columns to layout new panels + var paper_width = this.get('paper_width'), + paper_height = this.get('paper_height'), + colCount = Math.ceil(Math.sqrt(iIds.length)), + rowCount = Math.ceil(iIds.length/colCount), + centre = {x: paper_width/2, y: paper_height/2}, + px, py, spacer, scale, + coords = {'px': px, + 'py': py, + 'c': centre, + 'spacer': spacer, + 'colCount': colCount, + 'rowCount': rowCount, + 'paper_width': paper_width}; + + // This loop sets up a load of async imports. + // The first one to return will set all the coords + // and subsequent ones will update coords to position + // new image panels appropriately in a grid. + var invalidIds = []; + for (var i=0; i 0) { + var plural = invalidIds.length > 1 ? "s" : ""; + alert("Could not add image with invalid ID" + plural + ": " + invalidIds.join(", ")); + } + }, + + importImage: function(imgDataUrl, coords, baseUrl, index) { + + var self = this, + callback, + dataType = "json"; + + if (baseUrl) { + callback = "callback"; + dataType = "jsonp"; + } + if (index == undefined) { + index = 0; + } + + this.set('loading_count', this.get('loading_count') + 1); + + // Get the json data for the image... + $.ajax({ + url: imgDataUrl, + jsonp: callback, // 'callback' + dataType: dataType, + // work with the response + success: function( data ) { + if (data.Exception || data.ConcurrencyException) { + // If something went wrong, show error and don't add to figure + message = data.Exception || "ConcurrencyException" + alert(`Image loading from ${imgDataUrl} included an Error: ${message}`); + return; + } + + self.set('loading_count', self.get('loading_count') - 1); + + coords.spacer = coords.spacer || data.size.width/20; + var full_width = (coords.colCount * (data.size.width + coords.spacer)) - coords.spacer, + full_height = (coords.rowCount * (data.size.height + coords.spacer)) - coords.spacer; + coords.scale = coords.paper_width / (full_width + (2 * coords.spacer)); + coords.scale = Math.min(coords.scale, 1); // only scale down + // For the FIRST IMAGE ONLY (coords.px etc undefined), we + // need to work out where to start (px,py) now that we know size of panel + // (assume all panels are same size) + coords.px = coords.px || coords.c.x - (full_width * coords.scale)/2; + coords.py = coords.py || coords.c.y - (full_height * coords.scale)/2; + + // calculate panel coordinates from index... + var row = parseInt(index / coords.colCount, 10); + var col = index % coords.colCount; + var panelX = coords.px + ((data.size.width + coords.spacer) * coords.scale * col); + var panelY = coords.py + ((data.size.height + coords.spacer) * coords.scale * row); + + // ****** This is the Data Model ****** + //------------------------------------- + // Any changes here will create a new version + // of the model and will also have to be applied + // to the 'version_transform()' function so that + // older files can be brought up to date. + // Also check 'previewSetId()' for changes. + var n = { + 'imageId': data.id, + 'name': data.meta.imageName, + 'width': data.size.width * coords.scale, + 'height': data.size.height * coords.scale, + 'sizeZ': data.size.z, + 'theZ': data.rdefs.defaultZ, + 'sizeT': data.size.t, + 'theT': data.rdefs.defaultT, + 'rdefs': {'model': data.rdefs.model}, + 'channels': data.channels, + 'orig_width': data.size.width, + 'orig_height': data.size.height, + 'x': panelX, + 'y': panelY, + 'datasetName': data.meta.datasetName, + 'datasetId': data.meta.datasetId, + 'pixel_size_x': data.pixel_size.valueX, + 'pixel_size_y': data.pixel_size.valueY, + 'pixel_size_z': data.pixel_size.valueZ, + 'pixel_size_x_symbol': data.pixel_size.symbolX, + 'pixel_size_z_symbol': data.pixel_size.symbolZ, + 'pixel_size_x_unit': data.pixel_size.unitX, + 'pixel_size_z_unit': data.pixel_size.unitZ, + 'deltaT': data.deltaT, + }; + if (baseUrl) { + n.baseUrl = baseUrl; + } + // create Panel (and select it) + // We do some additional processing in Panel.parse() + self.panels.create(n, {'parse': true}).set('selected', true); + self.notifySelectionChange(); + }, + + error: function(event) { + self.set('loading_count', self.get('loading_count') - 1); + alert("Image not found on the server, " + + "or you don't have permission to access it at " + imgDataUrl); + }, + }); + }, + + // Used to position the #figure within canvas and also to coordinate svg layout. + getFigureSize: function() { + var pc = this.get('page_count'), + cols = this.get('page_col_count'), + gap = this.get('paper_spacing'), + pw = this.get('paper_width'), + ph = this.get('paper_height'), + rows; + rows = Math.ceil(pc / cols); + var w = cols * pw + (cols - 1) * gap, + h = rows * ph + (rows - 1) * gap; + return {'w': w, 'h': h, 'cols': cols, 'rows': rows} + }, + + getPageOffset: function(coords) { + var gap = this.get('paper_spacing'), + pw = this.get('paper_width'), + ph = this.get('paper_height'); + var xspacing = gap + pw; + var yspacing = gap + ph; + var offset = {}; + if (coords.x !== undefined){ + offset.x = coords.x % xspacing; + } + if (coords.y !== undefined){ + offset.y = coords.y % yspacing; + } + return offset; + }, + + getDefaultFigureName: function() { + const padL = (nr) => `${nr}`.padStart(2, '0'); + var d = new Date(), + dt = [d.getFullYear(), + padL(d.getMonth()+1), + padL(d.getDate())].join('-'), + tm = [padL(d.getHours()), + padL(d.getMinutes()), + padL(d.getSeconds())].join('-'); + return "Figure_" + dt + "_" + tm; + }, + + nudge_right: function() { + this.nudge('x', 10); + }, + + nudge_left: function() { + this.nudge('x', -10); + }, + + nudge_down: function() { + this.nudge('y', 10); + }, + + nudge_up: function() { + this.nudge('y', -10); + }, + + nudge: function(axis, delta) { + var selected = this.getSelected(), + pos; + + selected.forEach(function(p){ + pos = p.get(axis); + p.save(axis, pos + delta); + }); + }, + + align_left: function() { + var selected = this.getSelected(), + x_vals = []; + selected.forEach(function(p){ + x_vals.push(p.get('x')); + }); + var min_x = Math.min.apply(window, x_vals); + + selected.forEach(function(p){ + p.save('x', min_x); + }); + }, + + + align_right: function() { + var selected = this.getSelected(), + x_vals = []; + selected.forEach(function(p){ + x_vals.push(p.get('x') + p.get('width')); + }); + var max_x = Math.max.apply(window, x_vals); + + selected.forEach(function(p){ + p.save('x', max_x - p.get('width')); + }); + }, + + align_top: function() { + var selected = this.getSelected(), + y_vals = []; + selected.forEach(function(p){ + y_vals.push(p.get('y')); + }); + var min_y = Math.min.apply(window, y_vals); + + selected.forEach(function(p){ + p.save('y', min_y); + }); + }, + + align_bottom: function() { + var selected = this.getSelected(), + y_vals = []; + selected.forEach(function(p){ + y_vals.push(p.get('y') + p.get('height')); + }); + var max_y = Math.max.apply(window, y_vals); + + selected.forEach(function(p){ + p.save('y', max_y - p.get('height')); + }); + }, + + align_grid: function(gridGap) { + var sel = this.getSelected(), + top_left = this.get_top_left_panel(sel), + top_x = top_left.get('x'), + top_y = top_left.get('y'), + grid = [], + row = [top_left], + next_panel = top_left; + + // populate the grid, getting neighbouring panel each time + while (next_panel) { + c = next_panel.get_centre(); + next_panel = this.get_panel_at(c.x + next_panel.get('width'), c.y, sel); + + // if next_panel is not found, reached end of row. Try start new row... + if (typeof next_panel == 'undefined') { + grid.push(row); + // next_panel is below the first of the current row + c = row[0].get_centre(); + next_panel = this.get_panel_at(c.x, c.y + row[0].get('height'), sel); + row = []; + } + if (next_panel) { + row.push(next_panel); + } + } + + var spacer = top_left.get('width')/20; + if (!isNaN(parseFloat(gridGap))) { + spacer = parseFloat(gridGap); + } + var new_x = top_x, + new_y = top_y, + max_h = 0; + for (var r=0; r x) && + (p.get('y') < y && (p.get('y')+p.get('height')) > y)); + }); + }, + + get_top_left_panel: function(panels) { + // top-left panel is one where x + y is least + return panels.reduce(function(top_left, p){ + if ((p.get('x') + p.get('y')) < (top_left.get('x') + top_left.get('y'))) { + return p; + } else { + return top_left; + } + }); + }, + + align_size: function(width, height) { + var sel = this.getSelected(), + ref = this.get_top_left_panel(sel), + ref_width = width ? ref.get('width') : false, + ref_height = height ? ref.get('height') : false, + new_w, new_h, + p; + + sel.forEach(function(p){ + if (ref_width && ref_height) { + new_w = ref_width; + new_h = ref_height; + } else if (ref_width) { + new_w = ref_width; + new_h = (ref_width/p.get('width')) * p.get('height'); + } else if (ref_height) { + new_h = ref_height; + new_w = (ref_height/p.get('height')) * p.get('width'); + } + p.set({'width':new_w, 'height':new_h}); + }); + }, + + // Resize panels so they all show same magnification + align_magnification: function() { + var sel = this.getSelected(), + ref = this.get_top_left_panel(sel), + ref_pixSize = ref.get('pixel_size_x'), + targetMag; + if (!ref_pixSize) { + alert('Top-left panel has no pixel size set'); + return; + } + + // This could return an AJAX call if we need to convert units. + // Whenever we use this below, wrap it with $.when().then() + var getPixSizeInMicrons = function(m) { + var unit = m.get("pixel_size_x_unit"), + size = m.get("pixel_size_x"); + if (unit === "MICROMETER") { + return {'value':size}; + } + if (!size) { + return {'value': size} + } + // convert to MICROMETER + var url = BASE_WEBFIGURE_URL + "unit_conversion/" + size + "/" + unit + "/MICROMETER/"; + return $.getJSON(url); + } + + // First, get reference pixel size... + $.when( getPixSizeInMicrons(ref) ).then(function(data){ + ref_pixSize = data.value; + // E.g. 10 microns / inch + targetMag = ref_pixSize * ref.getPanelDpi(); + + // Loop through all selected, updating size of each... + sel.forEach(function(p){ + + // ignore the ref panel + if (p.cid === ref.cid) return; + + $.when( getPixSizeInMicrons(p) ).then(function(data){ + + var dpi = p.getPanelDpi(), + pixSize = data.value; + if (!pixSize) { + return; + } + var panelMag = dpi * pixSize, + scale = panelMag / targetMag, + new_w = p.get('width') * scale, + new_h = p.get('height') * scale; + p.set({'width':new_w, 'height':new_h}); + }); + }); + }); + }, + + // This can come from multi-select Rect OR any selected Panel + // Need to notify ALL panels and Multi-select Rect. + drag_xy: function(dx, dy, save) { + if (dx === 0 && dy === 0) return; + + var minX = 10000, + minY = 10000, + xy; + // First we notidy all Panels + var selected = this.getSelected(); + selected.forEach(function(m){ + xy = m.drag_xy(dx, dy, save); + minX = Math.min(minX, xy.x); + minY = Math.min(minY, xy.y); + }); + // Notify the Multi-select Rect of it's new X and Y + this.trigger('drag_xy', [minX, minY, save]); + }, + + + // This comes from the Multi-Select Rect. + // Simply delegate to all the Panels + multiselectdrag: function(x1, y1, w1, h1, x2, y2, w2, h2, save) { + var selected = this.getSelected(); + selected.forEach(function(m){ + m.multiselectdrag(x1, y1, w1, h1, x2, y2, w2, h2, save); + }); + }, + + // If already selected, do nothing (unless clearOthers is true) + setSelected: function(item, clearOthers) { + if ((!item.get('selected')) || clearOthers) { + this.clearSelected(false); + item.set('selected', true); + this.notifySelectionChange(); + } + }, + + select_all:function() { + this.panels.each(function(p){ + p.set('selected', true); + }); + this.notifySelectionChange(); + }, + + addSelected: function(item) { + item.set('selected', true); + this.notifySelectionChange(); + }, + + clearSelected: function(trigger) { + this.panels.each(function(p){ + p.set('selected', false); + }); + if (trigger !== false) { + this.notifySelectionChange(); + } + }, + + selectByRegion: function(coords) { + this.panels.each(function(p){ + if (p.regionOverlaps(coords)) { + p.set('selected', true); + } + }); + this.notifySelectionChange(); + }, + + getSelected: function() { + return this.panels.getSelected(); + }, + + // Go through all selected and destroy them - trigger selection change + deleteSelected: function() { + var selected = this.getSelected(); + var model; + while (model = selected.first()) { + model.destroy(); + } + this.notifySelectionChange(); + }, + + delete_panels: function() { + // make list that won't change as we destroy + var ps = []; + this.panels.each(function(p){ + ps.push(p); + }); + for (var i=ps.length-1; i>=0; i--) { + ps[i].destroy(); + } + this.notifySelectionChange(); + }, + + getCropCoordinates: function() { + // Get paper size and panel offsets (move to top-left) for cropping + // returns {'paper_width', 'paper_height', 'dx', 'dy'} + var margin = 10; + + // get range of all panel coordinates + var top = Math.min.apply(window, this.panels.map( + function(p){return p.getBoundingBoxTop();})); + var left = Math.min.apply(window, this.panels.map( + function(p){return p.getBoundingBoxLeft();})); + var right = Math.max.apply(window, this.panels.map( + function(p){return p.getBoundingBoxRight()})); + var bottom = Math.max.apply(window, this.panels.map( + function(p){return p.getBoundingBoxBottom()})); + + // Shift panels to top-left corner + var dx = margin - left; + var dy = margin - top; + + return { + 'paper_width': right - left + (2 * margin), + 'paper_height': bottom - top + (2 * margin), + 'dx': dx, + 'dy': dy + }; + }, + + notifySelectionChange: function() { + this.trigger('change:selection'); + } + + }); + + + + // Corresponds to css - allows us to calculate size of labels + var LINE_HEIGHT = 1.43; + + // ------------------------ Panel ----------------------------------------- + // Simple place-holder for each Panel. Will have E.g. imageId, rendering options etc + // Attributes can be added as we need them. + var Panel = Backbone.Model.extend({ + + defaults: { + x: 100, // coordinates on the 'paper' + y: 100, + width: 512, + height: 512, + zoom: 100, + dx: 0, // pan x & y within viewport + dy: 0, + labels: [], + deltaT: [], // list of deltaTs (secs) for tIndexes of movie + rotation: 0, + selected: false, + pixel_size_x_symbol: '\xB5m', // microns by default + pixel_size_x_unit: 'MICROMETER', + rotation_symbol: '\xB0', + max_export_dpi: 1000, + + // 'export_dpi' optional value to resample panel on export + // model includes 'scalebar' object, e.g: + // scalebar: {length: 10, position: 'bottomleft', color: 'FFFFFF', + // show: false, show_label: false; font_size: 10} + }, + + initialize: function() { + + }, + + // When we're creating a Panel, we process the data a little here: + parse: function(data, options) { + var greyscale = data.rdefs.model === "greyscale"; + delete data.rdefs + data.channels = data.channels.map(function(ch){ + // channels: use 'lut' for color if set. Don't save 'lut' + if (ch.lut) { + if (ch.lut.length > 0) { + ch.color = ch.lut; + } + delete ch.lut; + } + // we don't support greyscale, but instead set active channel grey + if (greyscale && ch.active) { + ch.color = "FFFFFF"; + } + return ch; + }); + return data; + }, + + syncOverride: true, + + validate: function(attrs, options) { + // obviously lots more could be added here... + if (attrs.theT >= attrs.sizeT) { + return "theT too big"; + } + if (attrs.theT < 0) { + return "theT too small"; + } + if (attrs.theZ >= attrs.sizeZ) { + return "theZ too big"; + } + if (attrs.theZ < 0) { + return "theZ too small"; + } + if (attrs.z_start !== undefined) { + if (attrs.z_start < 0 || attrs.z_start >= attrs.sizeZ) { + return "z_start out of Z range" + } + } + if (attrs.z_end !== undefined) { + if (attrs.z_end < 0 || attrs.z_end >= attrs.sizeZ) { + return "z_end out of Z range" + } + } + }, + + // Switch some attributes for new image... + setId: function(data) { + + // we replace these attributes... + var newData = {'imageId': data.imageId, + 'name': data.name, + 'sizeZ': data.sizeZ, + 'sizeT': data.sizeT, + 'orig_width': data.orig_width, + 'orig_height': data.orig_height, + 'datasetName': data.datasetName, + 'datasetId': data.datasetId, + 'pixel_size_x': data.pixel_size_x, + 'pixel_size_y': data.pixel_size_y, + 'pixel_size_z': data.pixel_size_z, + 'pixel_size_x_symbol': data.pixel_size_x_symbol, + 'pixel_size_z_symbol': data.pixel_size_z_symbol, + 'pixel_size_x_unit': data.pixel_size_x_unit, + 'pixel_size_z_unit': data.pixel_size_z_unit, + 'deltaT': data.deltaT, + 'zoom':data.zoom, + }; + + // theT and theZ are not changed unless we have to... + if (this.get('theT') >= newData.sizeT) { + newData.theT = newData.sizeT - 1; + } + if (this.get('theZ') >= newData.sizeZ) { + newData.theZ = newData.sizeZ - 1; + } + + // Make sure dx and dy are not outside the new image + if (Math.abs(this.get('dx')) > newData.orig_width/2) { + newData.dx = 0; + } + if (Math.abs(this.get('dy')) > newData.orig_height/2) { + newData.dy = 0; + } + + // new Channels are based on new data, but we keep the + // 'active' state and color from old Channels. + var newCh = [], + oldCh = this.get('channels'), + dataCh = data.channels; + _.each(dataCh, function(ch, i) { + var nc = $.extend(true, {}, dataCh[i]); + nc.active = (i < oldCh.length && oldCh[i].active); + if (i < oldCh.length) { + nc.color = "" + oldCh[i].color; + } + newCh.push(nc); + }); + + newData.channels = newCh; + + this.set(newData); + }, + + hide_scalebar: function() { + // keep all scalebar properties, except 'show' + var sb = $.extend(true, {}, this.get('scalebar')); + sb.show = false; + this.save('scalebar', sb); + }, + + save_scalebar: function(new_sb) { + // update only the attributes of scalebar we're passed + var old_sb = $.extend(true, {}, this.get('scalebar') || {}); + var sb = $.extend(true, old_sb, new_sb); + this.save('scalebar', sb); + }, + + // Simple checking whether shape is in viewport (x, y, width, height) + // Return true if any of the points in shape are within viewport. + is_shape_in_viewport: function(shape, viewport) { + var rect = viewport; + var isPointInRect = function(x, y) { + if (x < rect.x) return false; + if (y < rect.y) return false; + if (x > rect.x + rect.width) return false; + if (y > rect.y + rect.height) return false; + return true; + } + var points; + if (shape.type === "Ellipse") { + points = [[shape.cx, shape.cy]]; + } else if (shape.type === "Rectangle") { + points = [[shape.x, shape.y], + [shape.x, shape.y + shape.height,], + [shape.x + shape.width, shape.y], + [shape.x + shape.width, shape.y + shape.height]]; + } else if (shape.type === "Line" || shape.type === "Arrow") { + points = [[shape.x1, shape.y1], + [shape.x2, shape.y2], + [(shape.x1 + shape.x2)/2, (shape.y1 + shape.y2)/ 2]]; + } else if (shape.type === "Polyline" || shape.type === "Polygon") { + points = shape.points.split(' ').map(function(p){ + return p.split(","); + }); + } + if (points) { + for (var p=0; p0 ? dec_prec+3 : 2; + if (format === "index") { + isNegative = false; + if(!isNaN(shiftIdx) && !(this.get('deltaT')[shiftIdx - 1] == null)) + text = "" + (theT - shiftIdx + 1); + else text = "" + (theT + 1); + } else if (['milliseconds', 'ms'].includes(format)) { + text = (deltaT*1000).toFixed(dec_prec) + " ms"; + } else if (['seconds', 'secs', 's'].includes(format)) { + text = deltaT.toFixed(dec_prec) + " s"; + } else if (['minutes', 'mins', 'm'].includes(format)) { + text = (deltaT / 60).toFixed(dec_prec) + " mins"; + } else if (["mins:secs", "m:s"].includes(format)) { + m = parseInt(deltaT / 60); + s = ((deltaT % 60).toFixed(dec_prec)).padStart(padlen, "0"); + text = m + ":" + s; + } else if (["hrs:mins", "h:m"].includes(format)) { + h = parseInt(deltaT / 3600); + m = (((deltaT % 3600) / 60).toFixed(dec_prec)).padStart(padlen, "0"); + text = h + ":" + m; + } else if (["hrs:mins:secs", "h:m:s"].includes(format)) { + h = parseInt(deltaT / 3600); + m = pad(parseInt((deltaT % 3600) / 60)); + s = ((deltaT % 60).toFixed(dec_prec)).padStart(padlen, "0"); + text = h + ":" + m + ":" + s; + } else { // Format unknown + return "" + } + var dec_str = dec_prec>0 ? "."+"0".repeat(dec_prec) : ""; + if (["0"+dec_str+" s", "0"+dec_str+" mins", "0:00"+dec_str, "0:00:00"+dec_str].indexOf(text) > -1) { + isNegative = false; + } + + return (isNegative ? '-' : '') + text; + }, + + get_zoom_label_text: function() { + var text = "" + this.get('zoom') + " %" + return text; + }, + + + get_name_label_text: function(property, format) { + var text = ""; + if (property === "image") { + if (format === "id") { + text = ""+this.get('imageId'); + } else if (format === "name") { + var pathnames = this.get('name').split('/'); + text = pathnames[pathnames.length-1]; + } + } else if (property === "dataset") { + if (format === "id") { + text = ""+this.get('datasetId'); + } else if (format === "name") { + text = this.get('datasetName') ? this.get('datasetName') : "No/Many Datasets"; + } + } + return text; + }, + + get_view_label_text: function(property, format, ref_idx, dec_prec) { + if (format === "px") format = "pixel"; + + if (property === "w") property = "width"; + else if (property === "h") property = "height"; + else if (property === "rot") property = "rotation"; + + + var x_symbol = this.get('pixel_size_x_symbol'), + z_symbol = this.get('pixel_size_z_symbol'); + z_symbol = z_symbol ? z_symbol : x_symbol // Using x symbol when z not defined + var x_size = this.get('pixel_size_x'), + y_size = this.get('pixel_size_y'), + z_size = this.get('pixel_size_z'); + z_size = z_size ? z_size : 0 + x_size = x_size ? x_size : 0 + y_size = y_size ? y_size : 0 + + dec_prec = parseInt(dec_prec) + dec_prec = dec_prec==null ? 2 : dec_prec; // 2 is the default precision + + var text = ""; + if (property === "z") { + if (this.get('z_projection')) { + var start = this.get('z_start'), + end = this.get('z_end'); + if (format === "pixel") { + text = "" + (start+1) + " - " + (end+1); + } else if (format === "unit") { + start = (start * z_size).toFixed(dec_prec) + end = (end * z_size).toFixed(dec_prec) + text = ""+ start +" "+ z_symbol + + " - " + end +" "+ z_symbol + } + } + else { + var theZ = this.get('theZ'); + var deltaZ = theZ; + + var shift; + if (ref_idx) { + shift = parseInt(ref_idx) + } + if(!isNaN(shift)){ + deltaZ = theZ - shift; + } + if (format === "pixel") { + text = "" + (deltaZ + 1); + } else if (format === "unit") { + text = ""+ (deltaZ * z_size).toFixed(dec_prec) +" "+ z_symbol + } + } + return text + } + + var value = this.getViewportAsRect()[property]; + if (property === "rotation") { + return ""+parseInt(value)+"°"; + } else if (format === "pixel") { + return ""+parseInt(value); + } else if (format === "unit") { + var scale = ['x', 'width'].includes(property) ? x_size : y_size + text = ""+ (value * scale).toFixed(dec_prec) +" "+ x_symbol + } + return text + }, + + get_label_key: function(label) { + var key = label.text + '_' + label.size + '_' + label.color + '_' + label.position; + key = _.escape(key); + return key; + }, + + // labels_map is {labelKey: {size:s, text:t, position:p, color:c}} or {labelKey: false} to delete + // where labelKey specifies the label to edit. "l.text + '_' + l.size + '_' + l.color + '_' + l.position" + edit_labels: function(labels_map) { + + var oldLabs = this.get('labels'); + // Need to clone the list of labels... + var labs = [], + lbl, lbl_key; + for (var i=0; i this.get_label_key(lbl)); + + // get all unique labels based on filtering keys + //(i.e removing duplicate keys based on the index of the first occurrence of the value) + var filtered_lbls = labs.filter((lbl, index) => index == keys.indexOf(this.get_label_key(lbl))); + + // ... so that we get the changed event triggering OK + this.save('labels', filtered_lbls); + }, + + save_channel: function(cIndex, attr, value) { + + var oldChs = this.get('channels'); + // Need to clone the list of channels... + var chs = []; + for (var i=0; i 1 + // If turning projection on... + if (z_projection && !zp && sizeZ > 1) { + + // use existing z_diff interval if set + if (z_start !== undefined && z_end !== undefined) { + z_diff = (z_end - z_start)/2; + z_diff = Math.round(z_diff); + } + // reset z_start & z_end + z_start = Math.max(theZ - z_diff, 0); + z_end = Math.min(theZ + z_diff, sizeZ - 1); + this.set({ + 'z_projection': true, + 'z_start': z_start, + 'z_end': z_end + }); + // If turning z-projection off... + } else if (!z_projection && zp) { + // reset theZ for average of z_start & z_end + if (z_start !== undefined && z_end !== undefined) { + theZ = Math.round((z_end + z_start)/ 2 ); + this.set({'z_projection': false, + 'theZ': theZ}); + } else { + this.set('z_projection', false); + } + } + }, + + // When a multi-select rectangle is drawn around several Panels + // a resize of the rectangle x1, y1, w1, h1 => x2, y2, w2, h2 + // will resize the Panels within it in proportion. + // This might be during a drag, or drag-stop (save=true) + multiselectdrag: function(x1, y1, w1, h1, x2, y2, w2, h2, save) { + + var shift_x = function(startX) { + return ((startX - x1)/w1) * w2 + x2; + }; + var shift_y = function(startY) { + return ((startY - y1)/h1) * h2 + y2; + }; + + var newX = shift_x( this.get('x') ), + newY = shift_y( this.get('y') ), + newW = shift_x( this.get('x')+this.get('width') ) - newX, + newH = shift_y( this.get('y')+this.get('height') ) - newY; + + // Either set the new coordinates... + if (save) { + this.save( {'x':newX, 'y':newY, 'width':newW, 'height':newH} ); + } else { + // ... Or update the UI Panels + // both svg and DOM views listen for this... + this.trigger('drag_resize', [newX, newY, newW, newH] ); + } + }, + + // resize, zoom and pan to show the specified region. + // new panel will fit inside existing panel + // coords is {x:x, y:y, width:w, height:h, rotation?:r} + cropToRoi: function(coords) { + var targetWH = coords.width/coords.height, + currentWH = this.get('width')/this.get('height'), + newW, newH, + targetCx = Math.round(coords.x + (coords.width/2)), + targetCy = Math.round(coords.y + (coords.height/2)), + // centre panel at centre of ROI + dx = (this.get('orig_width')/2) - targetCx, + dy = (this.get('orig_height')/2) - targetCy; + // make panel correct w/h ratio + if (targetWH < currentWH) { + // make it thinner + newH = this.get('height'); + newW = targetWH * newH; + } else { + newW = this.get('width'); + newH = newW / targetWH; + } + // zoom to correct percentage + var xPercent = this.get('orig_width') / coords.width, + yPercent = this.get('orig_height') / coords.height, + zoom = Math.min(xPercent, yPercent) * 100; + + var toSet = { 'width': newW, 'height': newH, 'dx': dx, 'dy': dy, 'zoom': zoom }; + var rotation = coords.rotation || 0; + if (!isNaN(rotation)) { + toSet.rotation = rotation; + } + this.save(toSet); + }, + + // returns the current viewport as a Rect {x, y, width, height} + getViewportAsRect: function(zoom, dx, dy) { + zoom = zoom !== undefined ? zoom : this.get('zoom'); + dx = dx !== undefined ? dx : this.get('dx'); + dy = dy !== undefined ? dy : this.get('dy'); + var rotation = this.get('rotation'); + + var width = this.get('width'), + height = this.get('height'), + orig_width = this.get('orig_width'), + orig_height = this.get('orig_height'); + + // find if scaling is limited by width OR height + var xPercent = width / orig_width, + yPercent = height / orig_height, + scale = Math.max(xPercent, yPercent); + + // if not zoomed or panned and panel shape is approx same as image... + var orig_wh = orig_width / orig_height, + view_wh = width / height; + if (dx === 0 && dy === 0 && zoom == 100 && Math.abs(orig_wh - view_wh) < 0.01) { + // ...ROI is whole image + return {'x': 0, 'y': 0, 'width': orig_width, + 'height': orig_height, 'rotation': rotation} + } + + // Factor in the applied zoom... + scale = scale * zoom / 100; + // ...to get roi width & height + var roiW = width / scale, + roiH = height / scale; + + // Use offset from image centre to calculate ROI position + var cX = orig_width/2 - dx, + cY = orig_height /2 - dy, + roiX = cX - (roiW / 2), + roiY = cY - (roiH / 2); + + return {'x': roiX, 'y': roiY, 'width': roiW, + 'height': roiH, 'rotation': rotation}; + }, + + // Drag resizing - notify the PanelView without saving + drag_resize: function(x, y, w, h) { + this.trigger('drag_resize', [x, y, w, h] ); + }, + + // Drag moving - notify the PanelView & SvgModel with/without saving + drag_xy: function(dx, dy, save) { + // Ignore any drag_stop events from simple clicks (no drag) + if (dx === 0 && dy === 0) { + return; + } + var newX = this.get('x') + dx, + newY = this.get('y') + dy, + w = this.get('width'), + h = this.get('height'); + + // Either set the new coordinates... + if (save) { + this.save( {'x':newX, 'y':newY} ); + } else { + // ... Or update the UI Panels + // both svg and DOM views listen for this... + this.trigger('drag_resize', [newX, newY, w, h] ); + } + + // we return new X and Y so FigureModel knows where panels are + return {'x':newX, 'y':newY}; + }, + + get_centre: function() { + return {'x':this.get('x') + (this.get('width')/2), + 'y':this.get('y') + (this.get('height')/2)}; + }, + + is_big_image: function() { + return this.get('orig_width') * this.get('orig_height') > MAX_PLANE_SIZE; + }, + + get_img_src: function(force_no_padding) { + var chs = this.get('channels'); + var cStrings = chs.map(function(c, i){ + return (c.active ? '' : '-') + (1+i) + "|" + c.window.start + ":" + c.window.end + "$" + c.color; + }); + var maps_json = chs.map(function(c){ + return {'reverse': {'enabled': !!c.reverseIntensity}}; + }); + var renderString = cStrings.join(","), + imageId = this.get('imageId'), + theZ = this.get('theZ'), + theT = this.get('theT'), + baseUrl = this.get('baseUrl'), + // stringify json and remove spaces + maps = '&maps=' + JSON.stringify(maps_json).replace(/ /g, ""), + proj = ""; + if (this.get('z_projection')) { + proj = "&p=intmax|" + this.get('z_start') + ":" + this.get('z_end'); + } + baseUrl = baseUrl || WEBGATEWAYINDEX.slice(0, -1); // remove last / + + // If BIG image, render scaled region + var region = ""; + if (this.is_big_image()) { + baseUrl = BASE_WEBFIGURE_URL + 'render_scaled_region/'; + var rect = this.getViewportAsRect(); + // Render a region that is 1.5 x larger + if (!force_no_padding) { + var length = Math.max(rect.width, rect.height) * 1.5; + rect.x = rect.x - ((length - rect.width) / 2); + rect.y = rect.y - ((length - rect.height) / 2); + rect.width = length; + rect.height = length; + } + var coords = [rect.x, rect.y, rect.width, rect.height].map(function(c){return parseInt(c)}) + region = '®ion=' + coords.join(','); + } else { + baseUrl += '/render_image/'; + } + + return baseUrl + imageId + "/" + theZ + "/" + theT + + '/?c=' + renderString + proj + maps + region + "&m=c"; + }, + + // Turn coordinates into css object with rotation transform + _viewport_css: function(img_x, img_y, img_w, img_h, frame_w, frame_h, rotation) { + var transform_x = 100 * (frame_w/2 - img_x) / img_w, + transform_y = 100 * (frame_h/2 - img_y) / img_h; + if (rotation == undefined) { + rotation = this.get('rotation') || 0; + } + + var css = {'left':img_x, + 'top':img_y, + 'width':img_w, + 'height':img_h, + '-webkit-transform-origin': transform_x + '% ' + transform_y + '%', + 'transform-origin': transform_x + '% ' + transform_y + '%', + '-webkit-transform': 'rotate(' + rotation + 'deg)', + 'transform': 'rotate(' + rotation + 'deg)' + }; + return css; + }, + + // used by the PanelView and ImageViewerView to get the size and + // offset of the img within it's frame + get_vp_img_css: function(zoom, frame_w, frame_h, x, y) { + + // For non-big images, we have the full plane in hand + // css just shows the viewport region + if (!this.is_big_image()) { + return this.get_vp_full_plane_css(zoom, frame_w, frame_h, x, y); + + // For 'big' images, we render just the viewport, so the rendered + // image fully fills the viewport. + } else { + return this.get_vp_big_image_css(zoom, frame_w, frame_h, x, y); + } + }, + + // For BIG images we just render the viewport + // Rendered image will be filling viewport. + // If we're zooming image will be larger. + // If panning, offset from centre by x and y. + // NB: Reshaping (changing aspect ratio) is buggy (so PanelView hides big image while reshaping) + get_vp_big_image_css: function(zoom, frame_w, frame_h, x, y) { + + // Used for static rendering, as well as during zoom, panning, panel resizing + // and panel re-shaping (stretch/squash). + + var zooming = zoom !== this.get('zoom'); + var panning = (x !== undefined && y!== undefined); + + // Need to know what the original offsets are... + // We know that the image is 1.5 * bigger than viewport + var length = Math.max(frame_w, frame_h) * 1.5; + + var img_x; + var img_y; + var img_w = length; + var img_h = length; + + // if we're zooming... + if (zooming) { + img_w = length * zoom / this.get('zoom'); + img_h = length * zoom / this.get('zoom'); + img_y = y || ((frame_h - img_h) / 2); + img_x = x || ((frame_w - img_w) / 2); + return this._viewport_css(img_x, img_y, img_w, img_h, frame_w, frame_h); + } else { + img_x = (frame_w - length) / 2; + img_y = (frame_h - length) / 2; + } + + // if we're resizing width / height.... + var old_w = parseInt(this.get('width'), 10); + var old_h = parseInt(this.get('height'), 10); + frame_w = parseInt(frame_w); + frame_h = parseInt(frame_h); + + var resizing = old_w !== img_w || old_h !== img_h; + if (resizing) { + + img_y = (frame_h - img_h) / 2; + img_x = (frame_w - img_w) / 2; + + // If we're panning... + if (panning) { + // ...we need to simply increment existing offset + img_x += x; + img_y += y; + } + } + + return this._viewport_css(img_x, img_y, img_w, img_h, frame_w, frame_h); + }, + + // get CSS that positions and scales a full image plane so that + // only the 'viewport' shows in the parent container + get_vp_full_plane_css: function(zoom, frame_w, frame_h, x, y) { + + var dx = x; + var dy = y; + + var orig_w = this.get('orig_width'), + orig_h = this.get('orig_height'); + if (typeof dx == 'undefined') dx = this.get('dx'); + if (typeof dy == 'undefined') dy = this.get('dy'); + zoom = zoom || 100; + + var img_x = 0, + img_y = 0, + img_w = frame_w * (zoom/100), + img_h = frame_h * (zoom/100), + orig_ratio = orig_w / orig_h, + vp_ratio = frame_w / frame_h; + if (Math.abs(orig_ratio - vp_ratio) < 0.01) { + // ignore... + // if viewport is wider than orig, offset y + } else if (orig_ratio < vp_ratio) { + img_h = img_w / orig_ratio; + } else { + img_w = img_h * orig_ratio; + } + var vp_scale_x = frame_w / orig_w, + vp_scale_y = frame_h / orig_h, + vp_scale = Math.max(vp_scale_x, vp_scale_y); + + // offsets if image is centered + img_y = (img_h - frame_h)/2; + img_x = (img_w - frame_w)/2; + + // now shift by dx & dy + dx = dx * (zoom/100); + dy = dy * (zoom/100); + img_x = (dx * vp_scale) - img_x; + img_y = (dy * vp_scale) - img_y; + + return this._viewport_css(img_x, img_y, img_w, img_h, frame_w, frame_h); + }, + + getPanelDpi: function(w, h, zoom) { + // page is 72 dpi + w = w || this.get('width'); + h = h || this.get('height'); + zoom = zoom || this.get('zoom'); + var img_width = this.get_vp_full_plane_css(zoom, w, h).width, // not viewport width + orig_width = this.get('orig_width'), + scaling = orig_width / img_width, + dpi = scaling * 72; + return dpi.toFixed(0); + }, + + getBoundingBoxTop: function() { + // get top of panel including 'top' labels + var labels = this.get("labels"); + var y = this.get('y'); + // get labels by position + var top_labels = labels.filter(function(l) {return l.position === 'top'}); + // offset by font-size of each + y = top_labels.reduce(function(prev, l){ + return prev - (LINE_HEIGHT * l.size); + }, y); + return y; + }, + + getBoundingBoxLeft: function() { + // get left of panel including 'leftvert' labels (ignore + // left horizontal labels - hard to calculate width) + var labels = this.get("labels"); + var x = this.get('x'); + // get labels by position + var left_labels = labels.filter(function(l) {return l.position === 'leftvert'}); + // offset by font-size of each + x = left_labels.reduce(function(prev, l){ + return prev - (LINE_HEIGHT * l.size); + }, x); + return x; + }, + + getBoundingBoxRight: function() { + // Ignore right (horizontal) labels since we don't know how long they are + // but include right vertical labels + var labels = this.get("labels"); + var x = this.get('x') + this.get('width'); + // get labels by position + var right_labels = labels.filter(function(l) {return l.position === 'rightvert'}); + // offset by font-size of each + x = right_labels.reduce(function(prev, l){ + return prev + (LINE_HEIGHT * l.size); + }, x); + + return x; + }, + + getBoundingBoxBottom: function() { + // get bottom of panel including 'bottom' labels + var labels = this.get("labels"); + var y = this.get('y') + this.get('height'); + // get labels by position + var bottom_labels = labels.filter(function(l) {return l.position === 'bottom'}); + // offset by font-size of each + y = bottom_labels.reduce(function(prev, l){ + return prev + (LINE_HEIGHT * l.size); + }, y); + return y; + }, + + // True if coords (x,y,width, height) overlap with panel + regionOverlaps: function(coords) { + + var px = this.get('x'), + px2 = px + this.get('width'), + py = this.get('y'), + py2 = py + this.get('height'), + cx = coords.x, + cx2 = cx + coords.width, + cy = coords.y, + cy2 = cy + coords.height; + // overlap needs overlap on x-axis... + return ((px < cx2) && (cx < px2) && (py < cy2) && (cy < py2)); + }, + + }); + + // ------------------------ Panel Collection ------------------------- + var PanelList = Backbone.Collection.extend({ + model: Panel, + + getSelected: function() { + var s = this.filter(function(panel){ + return panel.get('selected'); + }); + return new PanelList(s); + }, + + getAverage: function(attr) { + return this.getSum(attr) / this.length; + }, + + getAverageWH: function() { + var sumWH = this.reduce(function(memo, m){ + return memo + (m.get('width')/ m.get('height')); + }, 0); + return sumWH / this.length; + }, + + getSum: function(attr) { + return this.reduce(function(memo, m){ + return memo + (m.get(attr) || 0); + }, 0); + }, + + getMax: function(attr) { + return this.reduce(function(memo, m){ return Math.max(memo, m.get(attr)); }, 0); + }, + + getMin: function(attr) { + return this.reduce(function(memo, m){ return Math.min(memo, m.get(attr)); }, Infinity); + }, + + allTrue: function(attr) { + return this.reduce(function(memo, m){ + return (memo && m.get(attr)); + }, true); + }, + + // check if all panels have the same value for named attribute + allEqual: function(attr) { + var vals = this.pluck(attr); + return _.max(vals) === _.min(vals); + }, + + // Return the value of named attribute IF it's the same for all panels, otherwise undefined + getIfEqual: function(attr) { + var vals = this.pluck(attr); + if (_.max(vals) === _.min(vals)) { + return _.max(vals); + } + }, + + getDeltaTIfEqual: function() { + var vals = this.map(function(m){ return m.getDeltaT() }); + if (_.max(vals) === _.min(vals)) { + return _.max(vals); + } + }, + + createLabelsFromTags: function(options) { + // Loads Tags for selected images and creates labels + var image_ids = this.map(function(s){return s.get('imageId')}) + image_ids = "image=" + image_ids.join("&image="); + // TODO: Use /api/ when annotations is supported + var url = WEBINDEX_URL + "api/annotations/?type=tag&limit=1000&" + image_ids; + $.getJSON(url, function(data){ + // Map {iid: {id: 'tag'}, {id: 'names'}} + var imageTags = data.annotations.reduce(function(prev, t){ + var iid = t.link.parent.id; + if (!prev[iid]) { + prev[iid] = {}; + } + prev[iid][t.id] = t.textValue.replaceAll("_","\\_"); + return prev; + }, {}); + // Apply tags to panels + this.forEach(function(p){ + var iid = p.get('imageId'); + var labels = _.values(imageTags[iid]).map(function(text){ + return { + 'text': text, + 'size': options.size, + 'position': options.position, + 'color': options.color + } + }); + + p.add_labels(labels); + }); + }.bind(this)); + } + }); + + +var ShapeModel = Backbone.Model.extend({ + + parse: function(shape) { + var lowerFirst = function(text) { + return text[0].toLowerCase() + text.slice(1); + } + var rgbint_to_css = function(signed_integer) { + if (signed_integer < 0) signed_integer = signed_integer >>> 0; + var intAsHex = signed_integer.toString(16); + intAsHex = ("00000000" + intAsHex).slice(-8); + return '#' + intAsHex.substring(0,6); + } + shape.id = shape['@id']; + shape.type = shape['@type'].split('#')[1]; + delete shape['@id'] + delete shape['@type'] + // StrokeWidth - unwrap 'pixel' unit + if (shape.StrokeWidth) { + shape.strokeWidth = shape.StrokeWidth.Value; + delete shape['StrokeWidth']; + } + // handle colors: + _.each(["StrokeColor", "FillColor", ], function(attr) { + if (shape[attr] !== undefined) { + shape[lowerFirst(attr)] = rgbint_to_css(shape[attr]); + delete shape[attr]; + } + }); + // Convert other attributes + _.each(["Points", "MarkerEnd", "MarkerStart", "X", "Y", "RadiusX", "RadiusY", "X1", "Y1", "X2", "Y2", "Width", "Height", "TheZ", "TheT"], function(attr) { + if (shape[attr] !== undefined) { + shape[lowerFirst(attr)] = shape[attr]; + delete shape[attr]; + } + }); + // Handle Arrows... + if (shape.markerEnd === 'Arrow' || shape.markerStart === 'Arrow') { + shape.type = 'Arrow'; + if (shape.markerEnd !== 'Arrow') { + // Only marker start is arrow - reverse direction! + var tmp = {'x1': shape.x1, 'y1': shape.y1, 'x2': shape.x2, 'y2': shape.y2}; + shape.x1 = tmp.x2; + shape.y1 = tmp.y2; + shape.x2 = tmp.x1; + shape.y2 = tmp.y1; + } + } + return shape; + }, + + convertOMEROShape: function() { + // Converts a shape json from OMERO into format taken by Shape-editor + // if shape has Arrow head, shape.type = Arrow + var s = this.toJSON(); + if (s.markerEnd === 'Arrow' || s.markerStart === 'Arrow') { + s.type = 'Arrow'; + if (s.markerEnd !== 'Arrow') { + // Only marker start is arrow - reverse direction! + var tmp = {'x1': s.x1, 'y1': s.y1, 'x2': s.x2, 'y2': s.y2}; + s.x1 = tmp.x2; + s.y1 = tmp.y2; + s.x2 = tmp.x1; + s.y2 = tmp.y1; + } + } + if (s.type === 'Ellipse') { + // If we have < OMERO 5.3, Ellipse has cx, cy, rx, ry + if (s.rx !== undefined) { + s.x = s.cx; + s.y = s.cy; + s.radiusX = s.rx; + s.radiusY = s.ry; + } + } + return s; + }, +}); + +var ShapeList = Backbone.Collection.extend({ + model: ShapeModel +}); + +var RoiModel = Backbone.Model.extend({ + + initialize: function(data) { + this.set('id', data['@id']); + this.shapes = new ShapeList(data.shapes, {'parse': true}); + } +}); + +var RoiList = Backbone.Collection.extend({ + // url: ROIS_JSON_URL + iid + "/", + model: RoiModel, + + deselectShapes: function(){ + this.forEach(function(roi){ + roi.shapes.forEach(function(s){ + if (s.get('selected')) { + s.set('selected', false) + } + }); + }); + }, + + selectShape: function(shapeId){ + var shape, + shapeJson; + this.forEach(function(roi){ + roi.shapes.forEach(function(s){ + if (s.get('id') === shapeId) { + s.set('selected'); + } + }); + }); + shape = this.getShape(shapeId); + if (shape) { + shapeJson = shape.toJSON(); + } + this.trigger('change:selection', [shapeJson]); + }, + + getShape: function(shapeId){ + var shape; + this.forEach(function(roi){ + var s = roi.shapes.get(shapeId); + if (s) { + shape = s; + } + }); + return shape; + } +}); +// --------------- UNDO MANAGER ---------------------- + +// +// Copyright (C) 2014 University of Dundee & Open Microscopy Environment. +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +/*global Backbone:true */ + +var UndoManager = Backbone.Model.extend({ + defaults: function(){ + return { + undo_pointer: -1 + }; + }, + initialize: function(opts) { + this.figureModel = opts.figureModel; // need for setting selection etc + this.figureModel.on("change:figureName change:paper_width change:paper_height change:page_count change:legend", + this.handleChange, this); + this.listenTo(this.figureModel, 'reset_undo_redo', this.resetQueue); + this.undoQueue = []; + this.undoInProgress = false; + //this.undo_pointer = -1; + // Might need to undo/redo multiple panels/objects + this.undo_functions = []; + this.redo_functions = []; + }, + resetQueue: function() { + this.undoQueue = []; + this.set('undo_pointer', -1); + this.canUndo(); + }, + canUndo: function() { + return this.get('undo_pointer') >= 0; + }, + undo: function() { + var pointer = this.get('undo_pointer'); + if (pointer < 0) { + return; + } + this.undoQueue[pointer].undo(); + this.set('undo_pointer',pointer-1); // trigger change + }, + canRedo: function() { + return this.get('undo_pointer')+1 < this.undoQueue.length; + }, + redo: function() { + var pointer = this.get('undo_pointer'); + if (pointer+1 >= this.undoQueue.length) { + return; + } + this.undoQueue[pointer+1].redo(); + this.set('undo_pointer', pointer+1); // trigger change event + }, + postEdit: function(undo) { + var pointer = this.get('undo_pointer'); + // remove any undo ahead of current position + if (this.undoQueue.length > pointer+1) { + this.undoQueue = this.undoQueue.slice(0, pointer+1); + } + this.undoQueue.push(undo); + this.set('undo_pointer', pointer+1); // trigger change event + }, + + // START here - Listen to 'add' events... + listenToCollection: function(collection) { + var self = this; + // Add listener to changes in current models + collection.each(function(m){ + self.listenToModel(m); + }); + collection.on('add', function(m) { + // start listening for change events on the model + self.listenToModel(m); + if (!self.undoInProgress){ + // post an 'undo' + self.handleAdd(m, collection); + } + }); + collection.on('remove', function(m) { + if (!self.undoInProgress){ + // post an 'undo' + self.handleRemove(m, collection); + } + }); + }, + + handleRemove: function(m, collection) { + var self = this; + self.postEdit( { + name: "Undo Remove", + undo: function() { + self.undoInProgress = true; + collection.add(m); + self.figureModel.notifySelectionChange(); + self.undoInProgress = false; + }, + redo: function() { + self.undoInProgress = true; + m.destroy(); + self.figureModel.notifySelectionChange(); + self.undoInProgress = false; + } + }); + }, + + handleAdd: function(m, collection) { + var self = this; + self.postEdit( { + name: "Undo Add", + undo: function() { + self.undoInProgress = true; + m.destroy(); + self.figureModel.notifySelectionChange(); + self.undoInProgress = false; + }, + redo: function() { + self.undoInProgress = true; + collection.add(m); + self.figureModel.notifySelectionChange(); + self.undoInProgress = false; + } + }); + }, + + listenToModel: function(model) { + model.on("change", this.handleChange, this); + }, + + // Here we do most of the work, buiding Undo/Redo Edits when something changes + handleChange: function(m) { + var self = this; + + // Make sure we don't listen to changes coming from Undo/Redo + if (self.undoInProgress) { + return; // Don't undo the undo! + } + + // Ignore changes to certain attributes + var ignore_attrs = ["selected", "id"]; // change in id when new Panel is saved + + var undo_attrs = {}, + redo_attrs = {}, + a; + for (a in m.changed) { + if (ignore_attrs.indexOf(a) < 0) { + undo_attrs[a] = m.previous(a); + redo_attrs[a] = m.get(a); + } + } + + // in case we only got 'ignorable' changes + if (_.size(redo_attrs) === 0) { + return; + } + + // We add each change to undo_functions array, which may contain several + // changes that happen at "the same time" (E.g. multi-drag) + self.undo_functions.push(function(){ + m.save(undo_attrs); + }); + self.redo_functions.push(function(){ + m.save(redo_attrs); + }); + + // this could maybe moved to FigureModel itself + var set_selected = function(selected) { + selected.forEach(function(m, i){ + if (i === 0) { + self.figureModel.setSelected(m, true); + } else { + self.figureModel.addSelected(m); + } + }); + } + + // This is used to copy the undo/redo_functions lists + // into undo / redo operations to go into our Edit below + var createUndo = function(callList) { + var undos = []; + for (var u=0; u 0; + }, + + undo: function(event) { + event.preventDefault(); + if (this.modal_visible()) return; + this.model.undo(); + }, + redo: function(event) { + event.preventDefault(); + if (this.modal_visible()) return; + this.model.redo(); + } +}); + +const SLIDER_INCR_CUTOFF = 100; +// If the max value of a slider is below this, use smaller slider increments + +var ChannelSliderView = Backbone.View.extend({ + + template: JST["src/templates/channel_slider_template.html"], + + initialize: function(opts) { + // This View may apply to a single PanelModel or a list + this.models = opts.models; + var self = this; + this.models.forEach(function(m){ + self.listenTo(m, 'change:channels', self.render); + }); + }, + + events: { + "keyup .ch_start": "handle_channel_input", + "keyup .ch_end": "handle_channel_input", + "click .channel-btn": "toggle_channel", + "click .dropdown-menu a": "pick_color", + }, + + pick_color: function(e) { + var color = e.currentTarget.getAttribute('data-color'), + $colorbtn = $(e.currentTarget).parent().parent(), + oldcolor = $(e.currentTarget).attr('data-oldcolor'), + idx = $colorbtn.attr('data-index'), + self = this; + + if (color == 'colorpicker') { + FigureColorPicker.show({ + 'color': oldcolor, + 'success': function(newColor){ + // remove # from E.g. #ff00ff + newColor = newColor.replace("#", ""); + self.set_color(idx, newColor); + } + }); + } else if (color == 'lutpicker') { + FigureLutPicker.show({ + success: function(lutName){ + // LUT names are handled same as color strings + self.set_color(idx, lutName); + } + }); + } else if (color == 'reverse') { + var reverse = $('span', e.currentTarget).hasClass('glyphicon-check'); + self.models.forEach(function(m){ + m.save_channel(idx, 'reverseIntensity', !reverse); + }); + } else { + this.set_color(idx, color); + } + return false; + }, + + set_color: function(idx, color) { + if (this.models) { + this.models.forEach(function(m){ + m.save_channel(idx, 'color', color); + }); + } + }, + + hexToRgb: function hexToRgb(hex) { + // handle #ff00ff + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + if (result) return { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + }; + // handle #ccc + result = /^#?([a-f\d]{1})([a-f\d]{1})([a-f\d]{1})$/i.exec(hex); + if (result) return { + r: parseInt(result[1]+'0', 16), + g: parseInt(result[2]+'0', 16), + b: parseInt(result[3]+'0', 16) + }; + }, + + isDark: function(color) { + if (color.endsWith('.lut')) { + return false; + } + var c = this.hexToRgb(color); + var min, max, delta; + var v, s, h; + min = Math.min(c.r, c.g, c.b); + max = Math.max(c.r, c.g, c.b); + v = max; + delta = max-min; + if (max !== 0) { + s = delta/max; + } + else { + v = 0; + s = 0; + h = 0; + } + if (delta === 0) { + h = 0; + } else if (c.r==max) { + h = (c.g-c.b)/delta; + } else if (c.g == max) { + h = 2 + (c.b-c.r)/delta; + } else { + h = 4 +(c.r-c.g)/delta; + } + h = h * 60; + if (h < 0) { + h += 360; + } + h = h/360; + v = v/255; + return (v < 0.6 || (h > 0.6 && s > 0.7)); + }, + + toggle_channel: function(e) { + var idx = e.currentTarget.getAttribute('data-index'); + + if (this.model) { + this.model.toggle_channel(idx); + } else if (this.models) { + // 'flat' means that some panels have this channel on, some off + var flat = $('div', e.currentTarget).hasClass('ch-btn-flat'); + this.models.forEach(function(m){ + if(flat) { + m.toggle_channel(idx, true); + } else { + m.toggle_channel(idx); + } + }); + } + return false; + }, + + handle_channel_input: function(event) { + if (event.type === "keyup" && event.which !== 13) { + return; // Ignore keyups except 'Enter' + } + var idx = event.target.getAttribute('data-idx'), + startEnd = event.target.getAttribute('data-window'); // 'start' or 'end' + idx = parseInt(idx, 10); + var value = parseFloat(event.target.value, 10); + if (isNaN(value)) return; + // Make sure 'start' < 'end' value + if (event.target.getAttribute('max') && value > event.target.getAttribute('max')){ + alert("Enter a value less than " + event.target.getAttribute('max')); + return; + } + if (event.target.getAttribute('min') && value < event.target.getAttribute('min')){ + alert("Enter a value greater than " + event.target.getAttribute('min')) + return; + } + var newCh = {}; + newCh[startEnd] = value; + this.models.forEach(function(m) { + m.save_channel_window(idx, newCh); + }); + }, + + clear: function() { + $(".ch_slider").slider("destroy"); + $("#channel_sliders").empty(); + return this; + }, + + render: function() { + var json, + self = this; + + // Helper functions for map & reduce below + var addFn = function (prev, s) { + return prev + s; + }; + var getColor = function(idx) { + return function(ch) { + return ch[idx].color; + } + } + var getLabel = function(idx) { + return function(ch) { + return ch[idx].label; + } + } + var getReverse = function(idx) { + return function(ch) { + // For older figures (created pre 5.3.0) might be undefined + return ch[idx].reverseIntensity === true; + } + } + var getActive = function(idx) { + return function(ch) { + return ch[idx].active === true; + } + } + var windowFn = function (idx, attr) { + return function (ch) { + return ch[idx].window[attr]; + } + }; + var allEqualFn = function(prev, value) { + return value === prev ? prev : undefined; + }; + var reduceFn = function(fn) { + return function(prev, curr) { + return fn(prev, curr); + } + } + + // Comare channels from each Panel Model to see if they are + // compatible, and compile a summary json. + var chData = this.models.map(function(m){ + return m.get('channels'); + }); + // images are compatible if all images have same channel count + var allSameCount = chData.reduce(function(prev, channels){ + return channels.length === prev ? prev : false; + }, chData[0].length); + + if (!allSameCount) { + return this; + } + $(".ch_slider").slider("destroy"); + this.$el.empty(); + + chData[0].forEach(function(d, chIdx) { + // For each channel, summarise all selected images: + // Make list of various channel attributes: + var starts = chData.map(windowFn(chIdx, 'start')); + var ends = chData.map(windowFn(chIdx, 'end')); + var mins = chData.map(windowFn(chIdx, 'min')); + var maxs = chData.map(windowFn(chIdx, 'max')); + var colors = chData.map(getColor(chIdx)); + var reverses = chData.map(getReverse(chIdx)); + var actives = chData.map(getActive(chIdx)); + var labels = chData.map(getLabel(chIdx)); + // Reduce lists into summary for this channel + var startAvg = starts.reduce(addFn, 0) / starts.length; + var endAvg = ends.reduce(addFn, 0) / ends.length; + var startsNotEqual = starts.reduce(allEqualFn, starts[0]) === undefined; + var endsNotEqual = ends.reduce(allEqualFn, ends[0]) === undefined; + var min = mins.reduce(reduceFn(Math.min)); + var max = maxs.reduce(reduceFn(Math.max)); + if (max > SLIDER_INCR_CUTOFF) { + // If we have a large range, use integers, otherwise format to 2dp + startAvg = parseInt(startAvg); + endAvg = parseInt(endAvg); + } else { + startAvg = startAvg.toFixed(2); + endAvg = endAvg.toFixed(2); + } + var color = colors.reduce(allEqualFn, colors[0]) ? colors[0] : 'ccc'; + // allEqualFn for booleans will return undefined if not or equal + var label = labels.reduce(allEqualFn, labels[0]) ? labels[0] : ' '; + var reverse = reverses.reduce(allEqualFn, reverses[0]) ? true : false; + var active = actives.reduce(allEqualFn, actives[0]); + var style = {'background-position': '0 0'} + var sliderClass = ''; + var lutBgPos = FigureLutPicker.getLutBackgroundPosition(color); + if (color.endsWith('.lut')) { + style['background-position'] = lutBgPos; + sliderClass = 'lutBg'; + } else if (color.toUpperCase() === "FFFFFF") { + color = "ccc"; // white slider would be invisible + } + if (reverse) { + style.transform = 'scaleX(-1)'; + } + if (color == "FFFFFF") color = "ccc"; // white slider would be invisible + + // Make sure slider range is increased if needed to include current values + min = Math.min(min, startAvg); + max = Math.max(max, endAvg); + + var sliderHtml = self.template({'idx': chIdx, + 'label': label, + 'startAvg': startAvg, + 'startsNotEqual': startsNotEqual, + 'endAvg': endAvg, + 'endsNotEqual': endsNotEqual, + 'active': active, + 'lutBgPos': lutBgPos, + 'reverse': reverse, + 'color': color, + 'isDark': this.isDark(color)}); + var $div = $(sliderHtml).appendTo(this.$el); + + $div.find('.ch_slider').slider({ + range: true, + min: min, + max: max, + step: (max > SLIDER_INCR_CUTOFF) ? 1 : 0.01, + values: [startAvg, endAvg], + slide: function(event, ui) { + let chStart = (max > SLIDER_INCR_CUTOFF) ? ui.values[0] : ui.values[0].toFixed(2); + let chEnd = (max > SLIDER_INCR_CUTOFF) ? ui.values[1] : ui.values[1].toFixed(2); + $('.ch_start input', $div).val(chStart); + $('.ch_end input', $div).val(chEnd); + }, + stop: function(event, ui) { + self.models.forEach(function(m) { + m.save_channel_window(chIdx, {'start': ui.values[0], 'end': ui.values[1]}); + }); + } + }) + // Need to add background style to newly created div.ui-slider-range + .children('.ui-slider-range').css(style) + .addClass(sliderClass); + + }.bind(this)); + return this; + } +}); + + +var ChgrpModalView = Backbone.View.extend({ + + el: $("#chgrpModal"), + + model: FigureModel, + + imagesByGroup: {}, + omeroGroups: [], + + initialize: function () { + + var self = this; + + // Here we handle init of the dialog when it's shown... + $("#chgrpModal").bind("show.bs.modal", function () { + this.enableSubmit(false); + if (!this.model.get('fileId')) { + $('#chgrpModal .modal-body').html("

Figure not saved. Please Save the Figure first.

"); + } else { + $('#chgrpModal .modal-body').html(""); + self.loadGroups(); + self.loadImageDetails(); + } + }.bind(this)); + }, + + events: { + "click .chgrpForm input": "inputClicked", + "submit .chgrpForm": "handleSubmit" + }, + + loadImageDetails: function() { + var imgIds = this.model.panels.pluck('imageId'); + var url = `${BASE_WEBFIGURE_URL}images_details/?image=${_.uniq(imgIds).join(',')}`; + $.getJSON(url, function (data) { + // Sort images by Group + this.imagesByGroup = data.data.reduce(function(prev, img){ + if (!prev[img.group.id]) { + prev[img.group.id] = []; + } + prev[img.group.id].push(img); + return prev; + }, {}); + this.render(); + }.bind(this)); + }, + + loadGroups: function() { + var url = `${API_BASE_URL_V0}m/experimenters/${USER_ID}/experimentergroups/`; + $.getJSON(url, function(data){ + this.omeroGroups = data.data.map(group => {return {id: group['@id'], name: group.Name}}) + .filter(group => group.name != 'user'); + this.render(); + }.bind(this)); + }, + + inputClicked: function() { + this.enableSubmit(true); + }, + + // we disable Submit when dialog is shown, enable when Group chosen + enableSubmit: function (enabled) { + var $okBtn = $('button[type="submit"]', this.$el); + if (enabled) { + $okBtn.prop('disabled', false); + $okBtn.prop('title', 'Move to Group'); + } else { + $okBtn.prop('disabled', 'disabled'); + $okBtn.prop('title', 'No Group selected'); + } + }, + + handleSubmit: function (event) { + event.preventDefault(); + var group_id = parseInt($('input[name="target_group"]:checked', this.$el).val()); + var group_name = this.omeroGroups.filter(group => group.id === group_id)[0].name; + var fileId = this.model.get('fileId'); + var url = BASE_WEBFIGURE_URL + 'chgrp/'; + this.enableSubmit(false); + setTimeout(function(){ + $('#chgrpModal .modal-body').append("

Moving to Group: " + _.escape(group_name) + "...

"); + }, 1000); + $.post(url, { group_id: group_id, ann_id: fileId}) + .done(function (data) { + $("#chgrpModal").modal('hide'); + if (data.success) { + this.model.set({groupId: group_id}); + figureConfirmDialog("Success", "Figure moved to Group: " + _.escape(group_name), ["OK"]); + } else { + var errorMsg = data.error || "No error message available"; + figureConfirmDialog("Move Failed", errorMsg, ["OK"]); + } + }.bind(this)); + }, + + render: function() { + var html = ''; + var groupId = this.model.get('groupId'); + var currentGroup = this.omeroGroups.filter(group => group.id === groupId)[0]; + if (currentGroup) { + html += '

This figure is currently in Group: ' + _.escape(currentGroup.name) + '.

'; + } + + var groupCount = Object.keys(this.imagesByGroup).length; + html += `

Images in this figure belong to ${groupCount} Group${groupCount == 1 ? '' : 's'}: ` + html += Object.keys(this.imagesByGroup).map(groupId => { + var imgIds = this.imagesByGroup[groupId].map(i => i.id); + var groupName = this.imagesByGroup[groupId][0].group.name; + return ` + ${_.escape(groupName)} + (${imgIds.length} image${imgIds.length == 1 ? '' : 's'})` + } + ).join(", ") + '.

'; + html += `

NB: If a figure contains images from a different group, it is possible that some + users may be able to open the figure but not see those images in it.

` + + var targetGroups = this.omeroGroups.filter(group => group.id != groupId); + if (targetGroups.length === 0) { + html += "

No other Groups available (You are not a member of any other groups)

"; + } else { + html += "

Move to Group...

"; + html += targetGroups.map(group => `
+
`) + .join("\n"); + } + $('.modal-body', this.$el).html(html); + } +}); + + +// +// Copyright (C) 2015 University of Dundee & Open Microscopy Environment. +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + + +// Should only ever have a singleton on this +var ColorPickerView = Backbone.View.extend({ + + el: $("#colorpickerModal"), + + // remember picked colors, for picking again + pickedColors: [], + + initialize:function () { + + var sliders = { + saturation: { + maxLeft: 200, + maxTop: 200, + callLeft: 'setSaturation', + callTop: 'setBrightness' + }, + hue: { + maxLeft: 0, + maxTop: 200, + callLeft: false, + callTop: 'setHue' + }, + alpha: { + maxLeft: 0, + maxTop: 200, + callLeft: false, + callTop: 'setAlpha' + } + }; + + var self = this, + editingRGB = false; // flag to prevent update of r,g,b fields + + this.$submit_btn = $("#colorpickerModal .modal-footer button[type='submit']"); + + var $cp = $('.demo-auto').colorpicker({ + 'sliders': sliders, + 'color': '00ff00', + }); + + // Access the colorpicker object for use below... + var cp = $cp.data('colorpicker'); + + + $cp.on('changeColor', function(event){ + + // In edge-case of starting with 'black', clicking on Hue slider, + // default is to stay 'black', but we want to pick the color + // by setting saturation and brightness. + var c = event.color; + if ((c.toHex() === "#000000" || c.toHex() === "#ffffff") && + cp.currentSlider && cp.currentSlider.callTop === "setHue") { + cp.color.setSaturation(1); + cp.color.setBrightness(0); + cp.update(true); + cp.element.trigger({ + type: 'changeColor', + color: cp.color + }); + // so we don't do this again until next click + cp.currentSlider = undefined; + return; + } + + // enable form submission & show color + self.$submit_btn.prop('disabled', false); + $('.oldNewColors li:first-child').css('background-color', event.color.toHex()); + + // update red, green, blue inputs + if (!editingRGB) { + var rgb = event.color.toRGB(); + $(".rgb-group input[name='red']").val(rgb.r); + $(".rgb-group input[name='green']").val(rgb.g); + $(".rgb-group input[name='blue']").val(rgb.b); + } + }); + + $(".rgb-group input").bind("change keyup", function(){ + var $this = $(this), + value = $.trim($this.val()); + // check it's a number between 0 - 255 + if (value == parseInt(value, 10)) { + value = parseInt(value, 10); + if (value < 0) { + value = 0; + $this.val(value); + } + else if (value > 255) { + value = 255; + $this.val(value); + } + } else { + value = 255 + $this.val(value); + } + + // update colorpicker + var r = $(".rgb-group input[name='red']").val(), + g = $(".rgb-group input[name='green']").val(), + b = $(".rgb-group input[name='blue']").val(), + rgb = "rgb(" + r + "," + g + "," + b + ")"; + + // flag prevents update of r, g, b fields while typing + editingRGB = true; + $('.demo-auto').colorpicker('setValue', rgb); + editingRGB = false; + }); + }, + + + events: { + "submit .colorpickerForm": "handleColorpicker", + "click .pickedColors button": "pickRecentColor", + }, + + // 'Recent colors' buttons have color as their title + pickRecentColor: function(event) { + var color = $(event.target).prop('title'); + $('.demo-auto').colorpicker('setValue', color); + }, + + // submit of the form: call the callback and close dialog + handleColorpicker: function(event) { + event.preventDefault(); + + // var color = $(".colorpickerForm input[name='color']").val(); + var color = $('.demo-auto').colorpicker('getValue'); + + // very basic validation (in case user has edited color field manually) + if (color.length === 0) return; + if (color[0] != "#") { + color = "#" + color; + } + // E.g. must be #f00 or #ff0000 + if (color.length != 7 && color.length != 4) return; + + // remember for later + this.pickedColors.push(color); + + if (this.success) { + this.success(color); + } + + $("#colorpickerModal").modal('hide'); + return false; + }, + + show: function(options) { + + $("#colorpickerModal").modal('show'); + + if (options.color) { + $('.demo-auto').colorpicker('setValue', options.color); + + // compare old and new colors - init with old color + $('.oldNewColors li').css('background-color', "#" + options.color); + + // disable submit button until color is chosen + this.$submit_btn.prop('disabled', 'disabled'); + } + + if (options.pickedColors) { + this.pickedColors = _.uniq(this.pickedColors.concat(options.pickedColors)); + } + + // save callback to use on submit + if (options.success) { + this.success = options.success; + } + + this.render(); + }, + + render:function () { + + // this is a list of strings + var json = {'colors': _.uniq(this.pickedColors)}; + + var t = '' + + '
' + + '<% _.each(colors, function(c, i) { %>' + + '' + + '<% if ((i+1)%4 == 0){ %>
<% } %>' + + '<% }); %>' + + '
'; + + var compiled = _.template(t); + var html = compiled(json); + + $("#pickedColors").html(html); + } +}); + + + +var CropModalView = Backbone.View.extend({ + + el: $("#cropModal"), + + roiTemplate: JST["src/templates/modal_dialogs/crop_modal_roi.html"], + + model: FigureModel, + + roisPageSize: 200, + roisPage: 0, + roisCount: 0, + // not all these ROIs will contain Rects + roisLoaded: 0, + // Rectangles from ROIs + roiRects: [], + + initialize: function() { + + var self = this; + + // Here we handle init of the dialog when it's shown... + $("#cropModal").bind("show.bs.modal", function(){ + // Clone the 'first' selected panel as our reference for everything + self.m = self.model.getSelected().head().clone(); + self.listenTo(self.m, 'change:theZ change:theT', self.render); + + self.cropModel.set({'selected': false, 'width': 0, 'height': 0}); + + // get selected area... + var roi = self.m.getViewportAsRect(); + self.applyRotation(roi); + + // Show as ROI *if* it isn't the whole image + if (roi.x !== 0 || roi.y !== 0 + || roi.width !== self.m.get('orig_width') + || roi.height !== self.m.get('orig_height')) { + self.currentROI = roi; + self.cropModel.set({ + 'selected': true + }); + } + + // ...now zoom out and centre to render whole image + self.m.set({'zoom': 100, 'dx': 0, 'dy': 0}); + + self.zoomToFit(); // includes render() + // disable submit until user chooses a region/ROI + self.enableSubmit(false); + + // Reset ROIs from OMERO... + self.roiRects = []; + self.roisLoaded = 0; + self.roisPage = 0; + self.loadRoiRects(); + // ...along with ROIs from clipboard or on this image in the figure + self.showClipboardFigureRois(); + }); + + // keep track of currently selected ROI + this.currentROI = {'x':0, 'y': 0, 'width': 0, 'height': 0} + + // used by model underlying Rect. + // NB: values in cropModel are scaled by zoom percent + this.cropModel = new Backbone.Model({ + 'x':0, 'y': 0, 'width': 0, 'height': 0, + 'selected': false}); + // since resizes & drags don't actually update cropModel automatically, we do it... + this.cropModel.bind('drag_resize_stop', function(args) { + this.set({'x': args[0], 'y': args[1], 'width': args[2], 'height': args[3]}); + }); + this.cropModel.bind('drag_xy_stop', function(args) { + this.set({'x': args[0] + this.get('x'), 'y': args[1] + this.get('y')}); + }); + + // we also need to update the scaled ROI coords... + this.listenTo(this.cropModel, 'change:x change:y change:width change:height', function(m){ + var scale = self.zoom / 100; + self.currentROI = { + 'x': m.get('x') / scale, + 'y': m.get('y') / scale, + 'width': m.get('width') / scale, + 'height': m.get('height') / scale + } + // No-longer correspond to saved ROI coords + self.currentRoiId = undefined; + // Allow submit of dialog if valid ROI + if (self.regionValid(self.currentROI)) { + self.enableSubmit(true); + } else { + self.enableSubmit(false); + } + }); + + // Now set up Raphael paper... + this.paper = Raphael("crop_paper", 500, 500); + this.rect = new RectView({'model':this.cropModel, 'paper': this.paper}); + this.$cropImg = $('.crop_image', this.$el); + }, + + events: { + "click .roiPickMe": "roiPicked", + "click .loadRoiRects": "loadRoiRects", + "mousedown svg": "mousedown", + "mousemove svg": "mousemove", + "mouseup svg": "mouseup", + "submit .cropModalForm": "handleRoiForm" + }, + + // we disable Submit when dialog is shown, enable when region/ROI chosen + enableSubmit: function(enabled) { + var $okBtn = $('button[type="submit"]', this.$el); + if (enabled) { + $okBtn.prop('disabled', false); + $okBtn.prop('title', 'Crop selected images to chosen region'); + } else { + $okBtn.prop('disabled', 'disabled'); + $okBtn.prop('title', 'No valid region selected'); + } + }, + + // Region is only valid if it has width & height > 1 and + // is at least partially overlapping with the image + regionValid: function(roi) { + + if (roi.width < 2 || roi.height < 2) return false; + if (roi.x > this.m.get('orig_width')) return false; + if (roi.y > this.m.get('orig_height')) return false; + if (roi.x + roi.width < 0) return false; + if (roi.y + roi.height < 0) return false; + return true; + }, + + roiPicked: function(event) { + + var $target = $(event.target), + $tr = $target.parent(); + // $tr might be first if img clicked or if td clicked + // but in either case it will contain the img we need. + var $roi = $tr.find('img.roi_content'), + x = parseInt($roi.attr('data-x'), 10), + y = parseInt($roi.attr('data-y'), 10), + width = parseInt($roi.attr('data-width'), 10), + height = parseInt($roi.attr('data-height'), 10), + rotation = $roi.attr('data-rotation') || 0, + theT = parseInt($roi.attr('data-theT'), 10), + theZ = parseInt($roi.attr('data-theZ'), 10); + + // Rectangle ROIs have NO rotation. Copy of crop might have rotation + rotation = parseInt(rotation); + this.m.set('rotation', rotation); + + this.m.set({'theT': theT, 'theZ': theZ}); + + this.currentROI = { + 'x':x, 'y':y, 'width':width, 'height':height + } + // Update coords based on any rotation (if coords come from rotated crop region) + this.applyRotation(this.currentROI, 1, rotation); + + this.render(); + + this.cropModel.set({ + 'selected': true + }); + + // Save ROI ID + this.currentRoiId = $roi.attr('data-roiId'); + }, + + applyRotation: function(rect, factor=1, rotation) { + // Update the x and y coordinates of a Rectangle ROI to take account of rotation of the + // underlying image around it's centre point. The image is rotated on the canvas, so any + // Rectangle not at the centre will need to be rotated around the centre, updating rect.x and rect.y. + if (rotation === undefined) { + rotation = this.m.get('rotation'); + } + if (rotation != 0) { + var img_cx = this.m.get('orig_width') / 2; + var img_cy = this.m.get('orig_height') / 2; + var rect_cx = rect.x + (rect.width / 2); + var rect_cy = rect.y + (rect.height / 2); + var new_c = rotatePoint(rect_cx, rect_cy, img_cx, img_cy, rotation * factor); + rect.x = new_c.x - (rect.width / 2); + rect.y = new_c.y - (rect.height / 2); + } + }, + + handleRoiForm: function(event) { + event.preventDefault(); + // var json = this.processForm(); + var self = this, + r = this.currentROI, + sel = this.model.getSelected(), + sameT = sel.allEqual('theT'); + // sameZT = sel.allEqual('theT') && sel.allEqual('theT'); + + var getShape = function getShape(z, t) { + + // If all on one T-index, update to the current + // T-index that we're looking at. + if (sameT) { + t = self.m.get('theT'); + } + + self.applyRotation(r, -1); + + var rv = {'x': r.x, + 'y': r.y, + 'width': r.width, + 'height': r.height, + 'theZ': self.m.get('theZ'), + 'theT': t, + } + return rv; + } + + // IF we have an ROI selected (instead of hand-drawn shape) + // then try to use appropriate shape for that plane. + if (this.currentRoiId) { + + getShape = function getShape(currZ, currT) { + + var tzShapeMap = self.cachedRois[self.currentRoiId], + tkeys = _.keys(tzShapeMap).sort(), + zkeys, z, t, s; + + if (tzShapeMap[currT]) { + t = currT; + } else { + t = tkeys[parseInt(tkeys.length/2 ,10)] + } + zkeys = _.keys(tzShapeMap[t]).sort(); + if (tzShapeMap[t][currZ]) { + z = currZ; + } else { + z = zkeys[parseInt(zkeys.length/2, 10)] + } + s = tzShapeMap[t][z] + + // if we have a range of T values, don't change T! + if (!sameT) { + t = currT; + } + + return {'x': s.X, + 'y': s.Y, + 'width': s.Width, + 'height': s.Height, + 'theZ': z, + 'theT': t, + } + }; + } + + $("#cropModal").modal('hide'); + + // prepare callback for below + function cropAndClose(deleteROIs) { + // Don't set Z/T if we already have different Z/T indecies. + sel.each(function(m){ + var sh = getShape(m.get('theZ'), m.get('theT')), + newZ = Math.min(parseInt(sh.theZ, 10), m.get('sizeZ') - 1), + newT = Math.min(parseInt(sh.theT, 10), m.get('sizeT') - 1); + + m.cropToRoi({'x': sh.x, 'y': sh.y, 'width': sh.width, 'height': sh.height}); + if (deleteROIs) { + m.unset('shapes'); + } + // 'save' to trigger 'unsaved': true + m.save({ 'theZ': newZ, 'theT': newT, 'rotation': self.m.get('rotation')}); + }); + } + + // If we have ROIs on the image, ask if we want to delete them + var haveROIs = false, + plural = sel.length > 0 ? "s" : ""; + sel.each(function(p){ + if (p.get('shapes')) haveROIs = true; + }); + if (haveROIs) { + figureConfirmDialog("Delete ROIs?", + "Delete ROIs on the image" + plural + " you are cropping?", + ["Yes", "No", "Cancel"], + function(btnText){ + if (btnText == "Cancel") return; + if (btnText == "Yes") { + cropAndClose(true); + } else { + cropAndClose(); + } + } + ); + } else { + cropAndClose(); + } + }, + + mousedown: function(event) { + this.dragging = true; + var os = $(event.target).offset(); + this.clientX_start = event.clientX; + this.clientY_start = event.clientY; + this.imageX_start = this.clientX_start - os.left; + this.imageY_start = this.clientY_start - os.top; + this.cropModel.set({'x': this.imageX_start, 'y': this.imageY_start, 'width': 0, 'height': 0, 'selected': true}) + return false; + }, + + mouseup: function(event) { + if (this.dragging) { + this.dragging = false; + return false; + } + }, + + mousemove: function(event) { + if (this.dragging) { + var dx = event.clientX - this.clientX_start, + dy = event.clientY - this.clientY_start; + if (event.shiftKey) { + // make region square! + if (Math.abs(dx) > Math.abs(dy)) { + if (dy > 0) dy = Math.abs(dx); + else dy = -1 * Math.abs(dx); + } else { + if (dx > 0) dx = Math.abs(dy); + else dx = -1 * Math.abs(dy); + } + } + var negX = Math.min(0, dx), + negY = Math.min(0, dy); + this.cropModel.set({'x': this.imageX_start + negX, + 'y': this.imageY_start + negY, + 'width': Math.abs(dx), 'height': Math.abs(dy)}); + return false; + } + }, + + showClipboardFigureRois: function() { + // Show Rectangles from clipboard + var clipboardRects = [], + clipboard = this.model.get('clipboard'); + if (clipboard && clipboard.CROP) { + roi = clipboard.CROP; + clipboardRects.push({ + x: roi.x, y: roi.y, width: roi.width, height: roi.height, + rotation: roi.rotation + }); + } else if (clipboard && clipboard.SHAPES) { + clipboard.SHAPES.forEach(function(roi){ + if (roi.type === "Rectangle") { + clipboardRects.push({ + x: roi.x, y: roi.y, width: roi.width, height: roi.height + }); + } + }); + } + var msg = "No Regions copied to clipboard"; + this.renderRois(clipboardRects, ".roisFromClipboard", msg); + + // Show Rectangles from panels in figure + var figureRois = []; + var sel = this.model.getSelected(); + sel.forEach(function(panel) { + var panelRois = panel.get('shapes'); + if (panelRois) { + panelRois.forEach(function(roi){ + if (roi.type === "Rectangle") { + figureRois.push({ + x: roi.x, y: roi.y, width: roi.width, height: roi.height + }); + } + }); + } + }); + msg = "No Rectangular ROIs on selected panel in figure"; + this.renderRois(figureRois, ".roisFromFigure", msg); + }, + + // Load Rectangles from OMERO and render them + loadRoiRects: function(event) { + if (event) { + event.preventDefault(); + } + var self = this, + iid = self.m.get('imageId'); + var offset = this.roisPageSize * this.roisPage; + var url = BASE_WEBFIGURE_URL + 'roiRectangles/' + iid + '/?limit=' + self.roisPageSize + '&offset=' + offset; + $.getJSON(url, function(rsp){ + data = rsp.data; + self.roisLoaded += data.length; + self.roisPage += 1; + self.roisCount = rsp.meta.totalCount; + // get a representative Rect from each ROI. + // Include a z and t index, trying to pick current z/t if ROI includes a shape there + var currT = self.m.get('theT'), + currZ = self.m.get('theZ'); + var cachedRois = {}, // roiId: shapes (z/t dict) + roi, roiId, shape, theT, theZ, z, t, rect, tkeys, zkeys, + minT, maxT, + shapes; // dict of all shapes by z & t index + + for (var r=0; r>0] + } + zkeys = _.keys(shapes[t]) + .map(function(x){return parseInt(x, 10)}) + .sort(function(a, b){return a - b}); // sort numerically + if (shapes[t][currZ]) { + z = currZ; + } else { + z = zkeys[(zkeys.length/2)>>0] + } + shape = shapes[t][z] + self.roiRects.push({'theZ': shape.TheZ, + 'theT': shape.TheT, + 'x': shape.X, + 'y': shape.Y, + 'width': shape.Width, + 'height': shape.Height, + 'roiId': roiId, + 'tStart': minT, + 'tEnd': maxT, + 'zStart': zkeys[0], + 'zEnd': zkeys[zkeys.length-1]}); + } + // Show ROIS from OMERO... + var msg = "No rectangular ROIs found on this image in OMERO"; + self.renderRois(self.roiRects, ".roisFromOMERO", msg); + + if (self.roisLoaded < self.roisCount) { + // Show the 'Load' button if more are available + $(".loadRoiRects", this.$el).show(); + } else { + $(".loadRoiRects", this.$el).hide(); + } + $("#cropRoiMessage").html(`Loaded ${self.roisLoaded} / ${self.roisCount} ROIs`); + + self.cachedRois = cachedRois; + }).error(function(){ + var msg = "No rectangular ROIs found on this image in OMERO"; + self.renderRois([], ".roisFromOMERO", msg); + }); + }, + + renderRois: function(rects, target, msg) { + + var orig_width = this.m.get('orig_width'), + orig_height = this.m.get('orig_height'), + origT = this.m.get('theT'), + origZ = this.m.get('theZ'); + + var html = "", + size = 50, + rect, src, zoom, + top, left, div_w, div_h, img_w, img_h; + + // loop through ROIs, using our cloned model to generate src urls + // first, get the current Z and T of cloned model... + this.m.set('z_projection', false); // in case z_projection is true + + for (var r=0; r -1) this.m.set('theT', rect.theT, {'silent': true}); + if (rect.theZ > -1) this.m.set('theZ', rect.theZ, {'silent': true}); + src = this.m.get_img_src(true); + if (rect.width > rect.height) { + div_w = size; + div_h = (rect.height/rect.width) * div_w; + } else { + div_h = size; + div_w = (rect.width/rect.height) * div_h; + } + zoom = div_w/rect.width; + img_w = orig_width * zoom; + img_h = orig_height * zoom; + top = -(zoom * rect.y); + left = -(zoom * rect.x); + rect.theT = rect.theT !== undefined ? rect.theT : origT; + rect.theZ = rect.theZ !== undefined ? rect.theZ : origZ; + let css = this.m._viewport_css(left, top, img_w, img_h, size, size, rotation); + + var json = { + 'msg': msg, + 'src': src, + 'rect': rect, + 'w': div_w, + 'h': div_h, + 'css': css, + 'top': top, + 'left': left, + 'img_w': img_w, + 'img_h': img_h, + 'theZ': rect.theZ + 1, + 'theT': rect.theT + 1, + 'roiId': rect.roiId, + 'tStart': false, + 'zStart': false, + } + // set start/end indices (1-based) if we have them + if (rect.tStart !== undefined) {json.tStart = (+rect.tStart) + 1} + if (rect.tEnd !== undefined) {json.tEnd = (+rect.tEnd) + 1} + if (rect.zStart !== undefined) {json.zStart = (+rect.zStart) + 1} + if (rect.zEnd !== undefined) {json.zEnd = (+rect.zEnd) + 1} + html += this.roiTemplate(json); + } + if (html.length === 0) { + html = "" + msg + ""; + } + $(target + " tbody", this.$el).html(html); + + // reset Z/T as before + this.m.set({'theT': origT, 'theZ': origZ}); + }, + + zoomToFit: function() { + var max_w = 500, + max_h = 450, + w = this.m.get('orig_width'), + h = this.m.get('orig_height'); + scale = Math.min(max_w/w, max_h/h); + this.setZoom(scale * 100); + }, + + setZoom: function(percent) { + this.zoom = percent; + this.render(); + }, + + render: function() { + var scale = this.zoom / 100, + w = this.m.get('orig_width'), + h = this.m.get('orig_height'); + var newW = w * scale, + newH = h * scale; + this.m.set('zoom', 100); + this.m.set('width', newW); + this.m.set('height', newH); + var src = this.m.get_img_src(true); + var css = this.m.get_vp_full_plane_css(100, newW, newH); + + this.paper.setSize(newW, newH); + $("#crop_paper").css({'height': newH, 'width': newW}); + $("#cropViewer").css({'height': newH, 'width': newW}); + + this.$cropImg.css(css) + .attr('src', src); + + var roiX = this.currentROI.x * scale, + roiY = this.currentROI.y * scale, + roiW = this.currentROI.width * scale, + roiH = this.currentROI.height * scale; + this.cropModel.set({ + 'x': roiX, 'y': roiY, 'width': roiW, 'height': roiH, + 'selected': true + }); + } + }); + + + // -------------------------- Backbone VIEWS ----------------------------------------- + + + // var SelectionView = Backbone.View.extend({ + var FigureView = Backbone.View.extend({ + + el: $("#body"), + + initialize: function(opts) { + + // Delegate some responsibility to other views + new AlignmentToolbarView({model: this.model}); + new AddImagesModalView({model: this.model, figureView: this}); + new SetIdModalView({model: this.model}); + new PaperSetupModalView({model: this.model}); + new CropModalView({model: this.model}); + new ChgrpModalView({ model: this.model }); + new RoiModalView({model: this.model}); + new DpiModalView({model: this.model}); + new LegendView({model: this.model}); + new LabelFromMapsModal({model: this.model}); + + this.figureFiles = new FileList(); + new FileListView({model:this.figureFiles, figureModel: this.model}); + + // set up various elements and we need repeatedly + this.$main = $('main'); + this.$canvas = $("#canvas"); + this.$canvas_wrapper = $("#canvas_wrapper"); + this.$figure = $("#figure"); + this.$copyBtn = $(".copy"); + this.$pasteBtn = $(".paste"); + this.$saveBtn = $(".save_figure.btn"); + this.$saveOption = $("li.save_figure"); + this.$saveAsOption = $("li.save_as"); + this.$deleteOption = $("li.delete_figure"); + + var self = this; + + // Render on changes to the model + this.model.on('change:paper_width change:paper_height change:page_count', this.render, this); + + // If a panel is added... + this.model.panels.on("add", this.addOne, this); + + // Don't leave the page with unsaved changes! + window.onbeforeunload = function() { + var canEdit = self.model.get('canEdit'); + if (self.model.get("unsaved")) { + return "Leave page with unsaved changes?"; + } + }; + + $("#zoom_slider").slider({ + max: 400, + min: 10, + value: 75, + slide: function(event, ui) { + self.model.set('curr_zoom', ui.value); + } + }); + + // respond to zoom changes + this.listenTo(this.model, 'change:curr_zoom', this.renderZoom); + this.listenTo(this.model, 'change:selection', this.renderSelectionChange); + this.listenTo(this.model, 'change:unsaved', this.renderSaveBtn); + this.listenTo(this.model, 'change:figureName', this.renderFigureName); + + // Full render if page_color changes (might need to update labels etc) + this.listenTo(this.model, 'change:page_color', this.render); + this.listenTo(this.model, 'change:page_color', this.renderPanels); + + this.listenTo(this.model, 'change:loading_count', this.renderLoadingSpinner); + + // refresh current UI + this.renderZoom(); + + // 'Auto-render' on init. + this.render(); + this.renderSelectionChange(); + + }, + + events: { + "click .export_pdf": "export_pdf", + "click .export_options li": "export_options", + "click .add_panel": "addPanel", + "click .delete_panel": "deleteSelectedPanels", + "click .copy": "copy_selected_panels", + "click .paste": "paste_panels", + "click .save_figure": "save_figure_event", + "click .save_as": "save_as_event", + "click .new_figure": "goto_newfigure", + "click .open_figure": "open_figure", + "click .export_json": "export_json", + "click .import_json": "import_json", + "click .delete_figure": "delete_figure", + "click .chgrp_figure": "chgrp_figure", + "click .local_storage": "local_storage", + "click .paper_setup": "paper_setup", + "click .export-options a": "select_export_option", + "click .zoom-paper-to-fit": "zoom_paper_to_fit", + "click .about_figure": "show_about_dialog", + "click .figure-title": "start_editing_name", + "keyup .figure-title input": "figuretitle_keyup", + "blur .figure-title input": "stop_editing_name", + "submit .importJsonForm": "import_json_form" + }, + + keyboardEvents: { + 'backspace': 'deleteSelectedPanels', + 'del': 'deleteSelectedPanels', + 'mod+a': 'select_all', + 'mod+c': 'copy_selected_panels', + 'mod+v': 'paste_panels', + 'mod+s': 'save_figure_event', + 'mod+n': 'goto_newfigure', + 'mod+o': 'open_figure', + 'down' : 'nudge_down', + 'up' : 'nudge_up', + 'left' : 'nudge_left', + 'right' : 'nudge_right', + }, + + // If any modal is visible, we want to ignore keyboard events above + // All those methods should use this + modal_visible: function() { + return $("div.modal:visible").length > 0; + }, + + // choose an export option from the drop-down list + export_options: function(event) { + event.preventDefault(); + + var $target = $(event.target); + + // Only show check mark on the selected item. + $(".export_options .glyphicon-ok").css('visibility', 'hidden'); + $(".glyphicon-ok", $target).css('visibility', 'visible'); + + // Update text of main export_pdf button. + var txt = $target.attr('data-export-option'); + $('.export_pdf').text("Export " + txt).attr('data-export-option', txt); + + // Hide download button + $("#pdf_download").hide(); + }, + + paper_setup: function(event) { + event.preventDefault(); + + $("#paperSetupModal").modal(); + }, + + show_about_dialog: function(event) { + event.preventDefault(); + $("#aboutModal").modal(); + }, + + // Editing name workflow... + start_editing_name: function(event) { + var $this = $(event.target); + var name = $this.text(); + // escape any double-quotes + name = name.replace(/"/g, '"'); + $this.html(''); + $('input', $this).focus(); + }, + figuretitle_keyup: function(event) { + // If user hit Enter, stop editing... + if (event.which === 13) { + event.preventDefault(); + this.stop_editing_name(); + } + }, + stop_editing_name: function() { + var $this = $(".figure-title input"); + var new_name = $this.val().trim(); + if (new_name.length === 0) { + alert("Can't have empty name.") + return; + } + $(".figure-title").html(_.escape(new_name)); + // Save name... will renderFigureName only if name changed + this.model.save('figureName', new_name); + + // clear file list (will be re-fetched when needed) + this.figureFiles.reset(); + }, + + // Heavy lifting of PDF generation handled by OMERO.script... + export_pdf: function(event){ + + event.preventDefault(); + + // Status is indicated by showing / hiding 3 buttons + var figureModel = this.model, + $create_figure_pdf = $(event.target), + export_opt = $create_figure_pdf.attr('data-export-option'), + $pdf_inprogress = $("#pdf_inprogress"), + $pdf_download = $("#pdf_download"), + $script_error = $("#script_error"), + exportOption = "PDF"; + $create_figure_pdf.hide(); + $pdf_download.hide(); + $script_error.hide(); + $pdf_inprogress.show(); + + // Map from HTML to script options + opts = {"PDF": "PDF", + "PDF & images": "PDF_IMAGES", + "TIFF": "TIFF", + "TIFF & images": "TIFF_IMAGES", + "to OMERO": "OMERO"}; + exportOption = opts[export_opt]; + + // Get figure as json + var figureJSON = this.model.figure_toJSON(); + + var url = MAKE_WEBFIGURE_URL, + data = { + figureJSON: JSON.stringify(figureJSON), + exportOption: exportOption, + }; + + // Start the Figure_To_Pdf.py script + $.post( url, data).done(function( data ) { + + // {"status": "in progress", "jobId": "ProcessCallback/64be7a9e-2abb-4a48-9c5e-6d0938e1a3e2 -t:tcp -h 192.168.1.64 -p 64592"} + var jobId = data.jobId; + + // E.g. Handle 'No Processor Available'; + if (!jobId) { + if (data.error) { + alert(data.error); + } else { + alert("Error exporting figure"); + } + $create_figure_pdf.show(); + $pdf_inprogress.hide(); + return; + } + + // Now we keep polling for script completion, every second... + + var i = setInterval(function (){ + + $.getJSON(ACTIVITIES_JSON_URL, function(act_data) { + + var pdf_job = act_data[jobId]; + + // We're waiting for this flag... + if (pdf_job.status == "finished") { + clearInterval(i); + + $create_figure_pdf.show(); + $pdf_inprogress.hide(); + + // Show result + if (pdf_job.results.New_Figure) { + var fa_id = pdf_job.results.New_Figure.id; + if (pdf_job.results.New_Figure.type === "FileAnnotation") { + var fa_download = WEBINDEX_URL + "annotation/" + fa_id + "/"; + $pdf_download + .attr({'href': fa_download, 'data-original-title': 'Download Figure'}) + .show() + .children('span').prop('class', 'glyphicon glyphicon-download-alt'); + } else if (pdf_job.results.New_Figure.type === "Image") { + var fa_download = pdf_job.results.New_Figure.browse_url; + $pdf_download + .attr({'href': fa_download, 'data-original-title': 'Go to Figure Image'}) + .show() + .tooltip() + .children('span').prop('class', 'glyphicon glyphicon-share'); + } + } else if (pdf_job.stderr) { + // Only show any errors if NO result + var stderr_url = WEBINDEX_URL + "get_original_file/" + pdf_job.stderr + "/"; + $script_error.attr('href', stderr_url).show(); + } + } + + if (act_data.inprogress === 0) { + clearInterval(i); + } + + }).error(function() { + clearInterval(i); + }); + + }, 1000); + }); + }, + + select_export_option: function(event) { + event.preventDefault(); + var $a = $(event.target), + $span = $a.children('span.glyphicon'); + // We take the from the and place it in the + + + + ` + } else { + html += ``; + } + $("#roiPageControls").html(html).show(); + }, + + render: function() { + + var maxSize = 550, + frame_w = maxSize, + frame_h = maxSize, + wh = this.m.get('width') / this.m.get('height'); + if (wh <= 1) { + frame_h = maxSize; + frame_w = maxSize * wh; + } else { + frame_w = maxSize; + frame_h = maxSize / wh; + } + + // Get css for the image plane + var css = this.m.get_vp_img_css(this.m.get('zoom'), frame_w, frame_h); + this.$roiImg.css(css); + + // Get css for the SVG (full plane) + var svg_css = this.m.get_vp_full_plane_css(this.m.get('zoom'), frame_w, frame_h); + var w = this.m.get('orig_width'), + h = this.m.get('orig_height'); + var scale = svg_css.width / w; + // TODO: add public methods to set w & h + this.shapeManager._orig_width = w; + this.shapeManager._orig_height = h; + this.shapeManager.setZoom(scale * 100); + $("#roi_paper").css(svg_css); + + $("#roiViewer").css({'width': frame_w + 'px', 'height': frame_h + 'px'}); + + this.renderImagePlane(); + this.renderToolbar(); + this.renderSidebar(); + } + }); + + +// Created new for each selection change +var ScalebarFormView = Backbone.View.extend({ + + template: JST["src/templates/scalebar_form_template.html"], + + initialize: function(opts) { + + // prevent rapid repetative rendering, when listening to multiple panels + this.render = _.debounce(this.render); + + this.models = opts.models; + var self = this; + + this.models.forEach(function(m){ + self.listenTo(m, 'change:scalebar change:pixel_size_x change:scalebar_label', self.render); + }); + + // this.$el = $("#scalebar_form"); + }, + + events: { + "submit .scalebar_form": "update_scalebar", + "click .scalebar_label": "update_scalebar", + "change .btn": "dropdown_btn_changed", + "click .hide_scalebar": "hide_scalebar", + "click .pixel_size_display": "edit_pixel_size", + "keypress .pixel_size_input" : "enter_pixel_size", + "blur .pixel_size_input" : "save_pixel_size", + "keyup input[type='text']" : "handle_keyup", + }, + + handle_keyup: function (event) { + // If Enter key - submit form... + if (event.which == 13) { + this.update_scalebar(); + } + }, + + // simply show / hide editing field + edit_pixel_size: function() { + $('.pixel_size_display', this.$el).hide(); + $(".pixel_size_input", this.$el).css('display','inline-block').focus(); + }, + done_pixel_size: function() { + $('.pixel_size_display', this.$el).show(); + $(".pixel_size_input", this.$el).css('display','none').focus(); + }, + + // If you hit `enter`, set pixel_size + enter_pixel_size: function(e) { + if (e.keyCode == 13) { + this.save_pixel_size(e); + } + }, + + // on 'blur' or 'enter' we save... + save_pixel_size: function(e) { + // save will re-render, but only if number has changed - in case not... + this.done_pixel_size(); + + var val = $(e.target).val(); + if (val.length === 0) return; + var pixel_size = parseFloat(val); + if (isNaN(pixel_size)) return; + this.models.forEach(function(m){ + m.save('pixel_size_x', pixel_size); + }); + }, + + // Automatically submit the form when a dropdown is changed + dropdown_btn_changed: function(event) { + $(event.target).closest('form').submit(); + }, + + hide_scalebar: function() { + this.models.forEach(function(m){ + m.hide_scalebar(); + }); + }, + + // called when form changes + update_scalebar: function(event) { + + var $form = $('.scalebar_form '); + + var length = $('.scalebar-length', $form).val(), + units = $('.scalebar-units span:first', $form).attr('data-unit'), + position = $('.label-position span:first', $form).attr('data-position'), + color = $('.label-color span:first', $form).attr('data-color'), + show_label = $('.scalebar_label', $form).prop('checked'), + font_size = $('.scalebar_font_size span:first', $form).text().trim(), + height = parseInt($('.scalebar-height', $form).val()); + + this.models.forEach(function(m){ + var old_sb = m.get('scalebar'); + var sb = {show: true}; + if (length != '-') sb.length = parseFloat(length, 10); + if (units != '-') { + sb.units = units; + } else { + // various images have different units + // keep existing scalebar units OR use image pixel_size units + if (old_sb && old_sb.units) { + sb.units = old_sb.units; + } else if (m.get('pixel_size_x_unit')) { + sb.units = m.get('pixel_size_x_unit'); + } else { + sb.units = "MICROMETER"; + } + } + if (position != '-') sb.position = position; + if (color != '-') sb.color = color; + sb.show_label = show_label; + if (font_size != '-') sb.font_size = font_size; + if (height != '-') sb.height = height; + + m.save_scalebar(sb); + }); + return false; + }, + + render: function() { + var json = {show: false, show_label: true}, + hidden = false, + sb; + + // Turn dict into list of units we can sort by size + var scalebarUnits = ["PICOMETER", "ANGSTROM", "NANOMETER", "MICROMETER", + "MILLIMETER", "CENTIMETER", "METER", "KILOMETER", "MEGAMETER"] + var unit_symbols = Object.keys(window.LENGTH_UNITS) + .filter(function(unit){ + return (scalebarUnits.indexOf(unit) > -1); + }) + .map(function(unit){ + return $.extend({unit: unit}, window.LENGTH_UNITS[unit]); + }); + unit_symbols.sort(function(a, b){ + return a.microns > b.microns ? 1 : -1; + }) + json.unit_symbols = unit_symbols; + + this.models.forEach(function(m){ + // start with json data from first Panel + if (!json.pixel_size_x) { + json.pixel_size_x = m.get('pixel_size_x'); + json.pixel_size_symbol = m.get('pixel_size_x_symbol'); + json.pixel_size_unit = m.get('pixel_size_x_unit'); + } else { + pix_sze = m.get('pixel_size_x'); + // account for floating point imprecision when comparing + if (json.pixel_size_x != '-' && + json.pixel_size_x.toFixed(10) != pix_sze.toFixed(10)) { + json.pixel_size_x = '-'; + } + if (json.pixel_size_symbol != m.get('pixel_size_x_symbol')) { + json.pixel_size_symbol = '-'; + } + if (json.pixel_size_unit != m.get('pixel_size_x_unit')) { + json.pixel_size_unit = '-'; + } + } + sb = m.get('scalebar'); + // if panel has scalebar, combine into json + if (sb) { + // for first panel, json = sb + if (!json.length) { + json.length = sb.length; + json.units = sb.units; + json.position = sb.position; + json.color = sb.color; + json.show_label = sb.show_label; + json.font_size = sb.font_size; + json.height = sb.height; + } + else { + // combine attributes. Use '-' if different values found + if (json.length != sb.length) json.length = '-'; + if (json.units != sb.units) json.units = '-'; + if (json.position != sb.position) json.position = '-'; + if (json.color != sb.color) json.color = '-'; + if (!sb.show_label) json.show_label = false; + if (json.font_size != sb.font_size) json.font_size = '-'; + if (json.height != sb.height) json.height = '-'; + } + } + // if any panels don't have scalebar - we allow to add + if(!sb || !sb.show) hidden = true; + }); + + if (this.models.length === 0 || hidden) { + json.show = true; + } + json.length = json.length || 10; + // If no units chosen, use pixel size units + json.units = json.units || json.pixel_size_unit; + json.units_symbol = '-'; + if (json.units !== '-') { + // find the symbol e.g. 'mm' from units 'MILLIMETER' + json.units_symbol = LENGTH_UNITS[json.units].symbol; + } + json.position = json.position || 'bottomright'; + json.color = json.color || 'FFFFFF'; + json.font_size = json.font_size || 10; + json.pixel_size_symbol = json.pixel_size_symbol || '-'; + json.height = json.height || 3; + + var html = this.template(json); + this.$el.html(html); + this.$el.find("[title]").tooltip(); + + return this; + } +}); + + + // -------------- Selection Overlay Views ---------------------- + + + // SvgView uses ProxyRectModel to manage Svg Rects (raphael) + // This converts between zoomed coordiantes of the html DOM panels + // and the unzoomed SVG overlay. + // Attributes of this model apply to the SVG canvas and are updated from + // the PanelModel. + // The SVG RectView (Raphael) notifies this Model via trigger 'drag' & 'dragStop' + // and this is delegated to the PanelModel via trigger or set respectively. + + // Used by a couple of different models below + var getModelCoords = function(coords) { + var zoom = this.figureModel.get('curr_zoom') * 0.01, + size = this.figureModel.getFigureSize(), + paper_top = (this.figureModel.get('canvas_height') - size.h)/2, + paper_left = (this.figureModel.get('canvas_width') - size.w)/2, + x = (coords.x/zoom) - paper_left - 1, + y = (coords.y/zoom) - paper_top - 1, + w = coords.width/zoom, + h = coords.height/zoom; + return {'x':x>>0, 'y':y>>0, 'width':w>>0, 'height':h>>0}; + }; + + var ProxyRectModel = Backbone.Model.extend({ + + initialize: function(opts) { + this.panelModel = opts.panel; // ref to the genuine PanelModel + this.figureModel = opts.figure; + + this.renderFromModel(); + + // Refresh c + this.listenTo(this.figureModel, 'change:curr_zoom change:paper_width change:paper_height change:page_count', this.renderFromModel); + this.listenTo(this.panelModel, 'change:x change:y change:width change:height', this.renderFromModel); + // when PanelModel is being dragged, but NOT by this ProxyRectModel... + this.listenTo(this.panelModel, 'drag_resize', this.renderFromTrigger); + this.listenTo(this.panelModel, 'change:selected', this.renderSelection); + this.panelModel.on('destroy', this.clear, this); + // listen to a trigger on this Model (triggered from Rect) + this.listenTo(this, 'drag_xy', this.drag_xy); + this.listenTo(this, 'drag_xy_stop', this.drag_xy_stop); + this.listenTo(this, 'drag_resize', this.drag_resize); + // listen to change to this model - update PanelModel + this.listenTo(this, 'drag_resize_stop', this.drag_resize_stop); + + // reduce coupling between this and rect by using triggers to handle click. + this.bind('clicked', function(args) { + this.handleClick(args[0]); + }); + }, + + // return the SVG x, y, w, h (converting from figureModel) + getSvgCoords: function(coords) { + var zoom = this.figureModel.get('curr_zoom') * 0.01, + size = this.figureModel.getFigureSize(), + paper_top = (this.figureModel.get('canvas_height') - size.h)/2, + paper_left = (this.figureModel.get('canvas_width') - size.w)/2, + rect_x = (paper_left + 1 + coords.x) * zoom, + rect_y = (paper_top + 1 + coords.y) * zoom, + rect_w = coords.width * zoom, + rect_h = coords.height * zoom; + return {'x':rect_x, 'y':rect_y, 'width':rect_w, 'height':rect_h}; + }, + + // return the Model x, y, w, h (converting from SVG coords) + getModelCoords: getModelCoords, + + // called on trigger from the RectView, on drag of the whole rect OR handle for resize. + // we simply convert coordinates and delegate to figureModel + drag_xy: function(xy, save) { + var zoom = this.figureModel.get('curr_zoom') * 0.01, + dx = xy[0]/zoom, + dy = xy[1]/zoom; + + this.figureModel.drag_xy(dx, dy, save); + }, + + // As above, but this time we're saving the changes to the Model + drag_xy_stop: function(xy) { + this.drag_xy(xy, true); + }, + + // Called on trigger from the RectView on resize. + // Need to convert from Svg coords to Model and notify the PanelModel without saving. + drag_resize: function(xywh) { + var coords = this.getModelCoords({'x':xywh[0], 'y':xywh[1], 'width':xywh[2], 'height':xywh[3]}); + this.panelModel.drag_resize(coords.x, coords.y, coords.width, coords.height); + }, + + // As above, but need to update the Model on changes to Rect (drag stop etc) + drag_resize_stop: function(xywh) { + var coords = this.getModelCoords({'x':xywh[0], 'y':xywh[1], 'width':xywh[2], 'height':xywh[3]}); + this.panelModel.save(coords); + }, + + // Called when the FigureModel zooms or the PanelModel changes coords. + // Refreshes the RectView since that listens to changes in this ProxyModel + renderFromModel: function() { + this.set( this.getSvgCoords({ + 'x': this.panelModel.get('x'), + 'y': this.panelModel.get('y'), + 'width': this.panelModel.get('width'), + 'height': this.panelModel.get('height') + }) ); + }, + + // While the Panel is being dragged (by the multi-select Rect), we need to keep updating + // from the 'multiselectDrag' trigger on the model. RectView renders on change + renderFromTrigger:function(xywh) { + var c = this.getSvgCoords({ + 'x': xywh[0], + 'y': xywh[1], + 'width': xywh[2], + 'height': xywh[3] + }); + this.set( this.getSvgCoords({ + 'x': xywh[0], + 'y': xywh[1], + 'width': xywh[2], + 'height': xywh[3] + }) ); + }, + + // When PanelModel changes selection - update and RectView will render change + renderSelection: function() { + this.set('selected', this.panelModel.get('selected')); + }, + + // Handle click (mousedown) on the RectView - changing selection. + handleClick: function(event) { + if (event.shiftKey) { + this.figureModel.addSelected(this.panelModel); + } else { + this.figureModel.setSelected(this.panelModel); + } + }, + + clear: function() { + this.destroy(); + } + + }); + + + // This model underlies the Rect that is drawn around multi-selected panels + // (only shown if 2 or more panels selected) + // On drag or resize, we calculate how to move or resize the seleted panels. + var MultiSelectRectModel = ProxyRectModel.extend({ + + defaults: { + x: 0, + y: 0, + width: 0, + height: 0 + }, + + initialize: function(opts) { + this.figureModel = opts.figureModel; + + // listen to a trigger on this Model (triggered from Rect) + this.listenTo(this, 'drag_xy', this.drag_xy); + this.listenTo(this, 'drag_xy_stop', this.drag_xy_stop); + this.listenTo(this, 'drag_resize', this.drag_resize); + this.listenTo(this, 'drag_resize_stop', this.drag_resize_stop); + this.listenTo(this.figureModel, 'change:selection', this.updateSelection); + this.listenTo(this.figureModel, 'change:curr_zoom change:paper_height change:paper_width', + this.updateSelection); + + // also listen for drag_xy coming from a selected panel + this.listenTo(this.figureModel, 'drag_xy', this.update_xy); + }, + + + // Need to re-draw on selection AND zoom changes + updateSelection: function() { + + var selected = this.figureModel.getSelected(); + if (selected.length < 1){ + + this.set({ + 'x': 0, + 'y': 0, + 'width': 0, + 'height': 0, + 'selected': false + }); + return; + } + + var max_x = 0, + max_y = 0; + + selected.forEach(function(panel){ + var x = panel.get('x'), + y = panel.get('y'), + w = panel.get('width'), + h = panel.get('height'); + max_x = Math.max(max_x, x+w); + max_y = Math.max(max_y, y+h); + }); + + min_x = selected.getMin('x'); + min_y = selected.getMin('y'); + + + + this.set( this.getSvgCoords({ + 'x': min_x, + 'y': min_y, + 'width': max_x - min_x, + 'height': max_y - min_y + }) ); + + // Rect SVG will be notified and re-render + this.set('selected', true); + }, + + + // Called when we are notified of drag_xy on one of the Panels + update_xy: function(dxdy) { + if (! this.get('selected')) return; // if we're not visible, ignore + + var svgCoords = this.getSvgCoords({ + 'x': dxdy[0], + 'y': dxdy[1], + 'width': 0, + 'height': 0, + }); + this.set({'x':svgCoords.x, 'y':svgCoords.y}); + }, + + // RectView drag is delegated to Panels to update coords (don't save) + drag_xy: function(dxdy, save) { + // we just get [x,y] but we need [x,y,w,h]... + var x = dxdy[0] + this.get('x'), + y = dxdy[1] + this.get('y'); + var xywh = [x, y, this.get('width'), this.get('height')]; + this.notifyModelofDrag(xywh, save); + }, + + // As above, but Save is true since we're done dragging + drag_xy_stop: function(dxdy, save) { + this.drag_xy(dxdy, true); + // Have to keep our proxy model in sync + this.set({ + 'x': dxdy[0] + this.get('x'), + 'y': dxdy[1] + this.get('y') + }); + }, + + // While the multi-select RectView is being dragged, we need to calculate the new coords + // of all selected Panels, based on the start-coords and the current coords of + // the multi-select Rect. + drag_resize: function(xywh, save) { + this.notifyModelofDrag(xywh, save); + }, + + // RectView dragStop is delegated to Panels to update coords (with save 'true') + drag_resize_stop: function(xywh) { + this.notifyModelofDrag(xywh, true); + + this.set({ + 'x': xywh[0], + 'y': xywh[1], + 'width': xywh[2], + 'height': xywh[3] + }); + }, + + // While the multi-select RectView is being dragged, we need to calculate the new coords + // of all selected Panels, based on the start-coords and the current coords of + // the multi-select Rect. + notifyModelofDrag: function(xywh, save) { + var startCoords = this.getModelCoords({ + 'x': this.get('x'), + 'y': this.get('y'), + 'width': this.get('width'), + 'height': this.get('height') + }); + var dragCoords = this.getModelCoords({ + 'x': xywh[0], + 'y': xywh[1], + 'width': xywh[2], + 'height': xywh[3] + }); + + // var selected = this.figureModel.getSelected(); + // for (var i=0; i canvas + this.raphael_paper = Raphael("canvas_wrapper", canvas_width, canvas_height); + + // this.panelRects = new ProxyRectModelList(); + self.$dragOutline = $("
") + .appendTo("#canvas_wrapper"); + self.outlineStyle = self.$dragOutline.get(0).style; + + + // Add global mouse event handlers + self.dragging = false; + self.drag_start_x = 0; + self.drag_start_y = 0; + $("#canvas_wrapper>svg") + .mousedown(function(event){ + self.dragging = true; + var parentOffset = $(this).parent().offset(); + //or $(this).offset(); if you really just want the current element's offset + self.left = self.drag_start_x = event.pageX - parentOffset.left; + self.top = self.drag_start_y = event.pageY - parentOffset.top; + self.dx = 0; + self.dy = 0; + self.$dragOutline.css({ + 'left': self.drag_start_x, + 'top': self.drag_start_y, + 'width': 0, + 'height': 0 + }).show(); + // return false; + }) + .mousemove(function(event){ + if (self.dragging) { + var parentOffset = $(this).parent().offset(); + //or $(this).offset(); if you really just want the current element's offset + self.left = self.drag_start_x; + self.top = self.drag_start_y; + self.dx = event.pageX - parentOffset.left - self.drag_start_x; + self.dy = event.pageY - parentOffset.top - self.drag_start_y; + if (self.dx < 0) { + self.left = self.left + self.dx; + self.dx = Math.abs(self.dx); + } + if (self.dy < 0) { + self.top = self.top + self.dy; + self.dy = Math.abs(self.dy); + } + self.$dragOutline.css({ + 'left': self.left, + 'top': self.top, + 'width': self.dx, + 'height': self.dy + }); + // .show(); + // self.outlineStyle.left = left + 'px'; + // self.outlineStyle.top = top + 'px'; + // self.outlineStyle.width = dx + 'px'; + // self.outlineStyle.height = dy + 'px'; + } + // return false; + }) + .mouseup(function(event){ + if (self.dragging) { + self.handleClick(event); + self.$dragOutline.hide(); + } + self.dragging = false; + // return false; + }); + + // If a panel is added... + this.model.panels.on("add", this.addOne, this); + this.listenTo(this.model, 'change:curr_zoom', this.renderZoom); + + var multiSelectRect = new MultiSelectRectModel({figureModel: this.model}), + rv = new RectView({'model':multiSelectRect, 'paper':this.raphael_paper, + 'handle_wh':7, 'handles_toFront': true, 'fixed_ratio': true}); + rv.selected_line_attrs = {'stroke-width': 1, 'stroke':'#4b80f9'}; + + // set svg size for current window and zoom + this.renderZoom(); + }, + + // A panel has been added - We add a corresponding Raphael Rect + addOne: function(m) { + + var rectModel = new ProxyRectModel({panel: m, figure:this.model}); + new RectView({'model':rectModel, 'paper':this.raphael_paper, + 'handle_wh':5, 'disable_handles': true, 'fixed_ratio': true}); + }, + + // TODO + remove: function() { + // TODO: remove from svg, remove event handlers etc. + }, + + // We simply re-size the Raphael svg itself - Shapes have their own zoom listeners + renderZoom: function() { + var zoom = this.model.get('curr_zoom') * 0.01, + newWidth = this.model.get('canvas_width') * zoom, + newHeight = this.model.get('canvas_height') * zoom; + + this.raphael_paper.setSize(newWidth, newHeight); + }, + + getModelCoords: getModelCoords, + + // Any mouse click (mouseup) or dragStop that isn't captured by Panel Rect clears selection + handleClick: function(event) { + if (!event.shiftKey) { + this.model.clearSelected(); + } + // select panels overlapping with drag outline + if (this.dx > 0 || this.dy > 0) { + var coords = this.getModelCoords({x: this.left, y: this.top, width:this.dx, height:this.dy}); + this.model.selectByRegion(coords); + } + } + }); + + +// +// Copyright (C) 2014 University of Dundee & Open Microscopy Environment. +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +// http://www.sitepoint.com/javascript-json-serialization/ +JSON.stringify = JSON.stringify || function (obj) { + var t = typeof (obj); + if (t != "object" || obj === null) { + // simple data type + if (t == "string") obj = '"'+obj+'"'; + return String(obj); + } + else { + // recurse array or object + var n, v, json = [], arr = (obj && obj.constructor == Array); + for (n in obj) { + v = obj[n]; t = typeof(v); + if (t == "string") v = '"'+v+'"'; + else if (t == "object" && v !== null) v = JSON.stringify(v); + json.push((arr ? "" : '"' + n + '":') + String(v)); + } + return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}"); + } +}; + + +// Polyfill for IE +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith +if (!String.prototype.endsWith) + String.prototype.endsWith = function(searchStr, Position) { + // This works much better than >= because + // it compensates for NaN: + if (!(Position < this.length)) + Position = this.length; + else + Position |= 0; // round position + return this.substr(Position - searchStr.length, + searchStr.length) === searchStr; + }; + +var showExportAsJsonModal = function(figureJSON) { + var figureText = JSON.stringify(figureJSON); + $('#exportJsonModal').modal('show'); + $('#exportJsonModal textarea').text(figureText); +} + +var saveFigureToStorage = function (figureJSON) { + window.sessionStorage.setItem(LOCAL_STORAGE_RECOVERED_FIGURE, JSON.stringify(figureJSON)); +} + +var clearFigureFromStorage = function() { + window.sessionStorage.removeItem(LOCAL_STORAGE_RECOVERED_FIGURE); +} + +var recoverFigureFromStorage = function() { + var storage = window.sessionStorage; + var recoveredFigure = storage.getItem(LOCAL_STORAGE_RECOVERED_FIGURE); + var figureObject; + try { + figureObject = JSON.parse(recoveredFigure); + } catch (e) { + console.log("recovered Figure not valid JSON " + recoveredFigure); + } + return figureObject; +} + +var figureConfirmDialog = function(title, message, buttons, callback) { + var $confirmModal = $("#confirmModal"), + $title = $(".modal-title", $confirmModal), + $body = $(".modal-body", $confirmModal), + $footer = $(".modal-footer", $confirmModal), + $btn = $(".btn:first", $footer); + + // Update modal with params + $title.html(title); + $body.html('

' + message + '

'); + $footer.empty(); + _.each(buttons, function(txt){ + $btn.clone().text(txt).appendTo($footer); + }); + $(".btn", $footer).removeClass('btn-primary') + .addClass('btn-default') + .last() + .removeClass('btn-default') + .addClass('btn-primary'); + + // show modal + $confirmModal.modal('show'); + + // default handler for 'cancel' or 'close' + $confirmModal.one('hide.bs.modal', function() { + // remove the other 'one' handler below + $("#confirmModal .modal-footer .btn").off('click'); + if (callback) { + callback(); + } + }); + + // handle 'Save' btn click. + $("#confirmModal .modal-footer .btn").one('click', function(event) { + // remove the default 'one' handler above + $confirmModal.off('hide.bs.modal'); + var btnText = $(event.target).text(); + if (callback) { + callback(btnText); + } + }); +}; + +if (OME === undefined) { + var OME = {}; +} + +OPEN_WITH = []; + +OME.setOpenWithEnabledHandler = function(id, fn) { + // look for id in OPEN_WITH + OPEN_WITH.forEach(function(ow){ + if (ow.id === id) { + ow.isEnabled = function() { + // wrap fn with try/catch, since error here will break jsTree menu + var args = Array.from(arguments); + var enabled = false; + try { + enabled = fn.apply(this, args); + } catch (e) { + // Give user a clue as to what went wrong + console.log("Open with " + label + ": " + e); + } + return enabled; + } + } + }); +}; + +// Helper can be used by 'open with' plugins to provide +// a url for the selected objects +OME.setOpenWithUrlProvider = function(id, fn) { + // look for id in OPEN_WITH + OPEN_WITH.forEach(function(ow){ + if (ow.id === id) { + ow.getUrl = fn; + } + }); +}; + + +// Extend the jQuery UI $.slider() function to silence +// keyboard events on the handle, so we don't nudge selected panels +$.prototype.slider_old = $.prototype.slider; +$.prototype.slider = function() { + var result = $.prototype.slider_old.apply(this, arguments); + this.find(".ui-slider-handle").bind("keydown", function(){ + return false; + }); + return result; +} + + +// Get coordinates for point x, y rotated around cx, cy, by rotation degrees +var rotatePoint = function (x, y, cx, cy, rotation) { + let length = Math.sqrt(Math.pow((x - cx), 2) + Math.pow((y - cy), 2)); + let rot = Math.atan2((y - cy), (x - cx)); + rot = rot + (rotation * (Math.PI / 180)); // degrees to rad + let dx = Math.cos(rot) * length; + let dy = Math.sin(rot) * length; + return { x: cx + dx, y: cy + dy }; +} + +$(function(){ + + + $(".draggable-dialog").draggable(); + + $('#previewInfoTabs a').click(function (e) { + e.preventDefault(); + $(this).tab('show'); + }); + + + // Header button tooltips + $('.btn-sm').tooltip({container: 'body', placement:'bottom', toggle:"tooltip"}); + $('.figure-title').tooltip({container: 'body', placement:'bottom', toggle:"tooltip"}); + // Footer button tooltips + $('.btn-xs').tooltip({container: 'body', placement:'top', toggle:"tooltip"}); + + + // If we're on Mac, update dropdown menus for keyboard short cuts: + if (navigator.platform.toUpperCase().indexOf('MAC') > -1) { + $("ul.dropdown-menu li a span").each(function(){ + var $this = $(this); + $this.text($this.text().replace("Ctrl+", "⌘")); + }); + } + + // When we load, setup Open With options + $.getJSON(WEBGATEWAYINDEX + "open_with/", function(data){ + if (data && data.open_with_options) { + OPEN_WITH = data.open_with_options; + // Try to load scripts if specified: + OPEN_WITH.forEach(function(ow){ + if (ow.script_url) { + $.getScript(ow.script_url); + } + }) + } + }); + +}); + +// +// Copyright (C) 2014 University of Dundee & Open Microscopy Environment. +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +$(function(){ + + window.figureModel = new FigureModel(); + + window.FigureColorPicker = new ColorPickerView(); + window.FigureLutPicker = new LutPickerView(); + + // Override 'Backbone.sync'... + Backbone.ajaxSync = Backbone.sync; + + // TODO: - Use the undo/redo queue instead of sync to trigger figureModel.set("unsaved", true); + + // If syncOverride, then instead of actually trying to Save via ajax on model.save(attr, value) + // We simply set the 'unsaved' flag on the figureModel. + // This works for FigureModel and also for Panels collection. + Backbone.getSyncMethod = function(model) { + if(model.syncOverride || (model.collection && model.collection.syncOverride)) + { + return function(method, model, options, error) { + figureModel.set("unsaved", true); + }; + } + return Backbone.ajaxSync; + }; + + // Override 'Backbone.sync' to default to localSync, + // the original 'Backbone.sync' is still available in 'Backbone.ajaxSync' + Backbone.sync = function(method, model, options, error) { + return Backbone.getSyncMethod(model).apply(this, [method, model, options, error]); + }; + + + var view = new FigureView( {model: figureModel}); // uiState: uiState + var svgView = new SvgView( {model: figureModel}); + new RightPanelView({model: figureModel}); + + + // Undo Model and View + var undoManager = new UndoManager({'figureModel':figureModel}), + undoView = new UndoView({model:undoManager}); + // Finally, start listening for changes to panels + undoManager.listenToCollection(figureModel.panels); + + + var FigureRouter = Backbone.Router.extend({ + + routes: { + "": "index", + "new(/)": "newFigure", + "recover(/)": "recoverFigure", + "open(/)": "openFigure", + "file/:id(/)": "loadFigure", + }, + + checkSaveAndClear: function(callback) { + + var doClear = function() { + figureModel.clearFigure(); + if (callback) { + callback(); + } + }; + if (figureModel.get("unsaved")) { + + var saveBtnTxt = "Save", + canEdit = figureModel.get('canEdit'); + if (!canEdit) saveBtnTxt = "Save a Copy"; + // show the confirm dialog... + figureConfirmDialog("Save Changes to Figure?", + "Your changes will be lost if you don't save them", + ["Don't Save", saveBtnTxt], + function(btnTxt){ + if (btnTxt === saveBtnTxt) { + var options = {}; + // Save current figure or New figure... + var fileId = figureModel.get('fileId'); + if (fileId && canEdit) { + options.fileId = fileId; + } else { + var defaultName = figureModel.getDefaultFigureName(); + var figureName = prompt("Enter Figure Name", defaultName); + options.figureName = figureName || defaultName; + } + options.success = doClear; + figureModel.save_to_OMERO(options); + } else if (btnTxt === "Don't Save") { + figureModel.set("unsaved", false); + doClear(); + } else { + doClear(); + } + }); + } else { + doClear(); + } + }, + + index: function() { + $(".modal").modal('hide'); // hide any existing dialogs + var cb = function() { + $('#welcomeModal').modal(); + }; + this.checkSaveAndClear(cb); + }, + + openFigure: function() { + $(".modal").modal('hide'); // hide any existing dialogs + var cb = function() { + $("#openFigureModal").modal(); + }; + this.checkSaveAndClear(cb); + }, + + recoverFigure: function() { + $(".modal").modal('hide'); // hide any existing dialogs + figureModel.recoverFromLocalStorage(); + }, + + newFigure: function() { + $(".modal").modal('hide'); // hide any existing dialogs + var cb = function() { + $('#addImagesModal').modal(); + }; + // Check for ?image=1&image=2 + if (window.location.search.length > 1) { + var params = window.location.search.substring(1).split('&'); + var iids = params.reduce(function(prev, param){ + if (param.split('=')[0] === 'image') { + prev.push(param.split('=')[1]); + } + return prev; + },[]); + if (iids.length > 0) { + cb = function() { + figureModel.addImages(iids); + } + } + } + this.checkSaveAndClear(cb); + }, + + loadFigure: function(id) { + $(".modal").modal('hide'); // hide any existing dialogs + var fileId = parseInt(id, 10); + var cb = function() { + figureModel.load_from_OMERO(fileId); + }; + this.checkSaveAndClear(cb); + } + }); + + app = new FigureRouter(); + Backbone.history.start({pushState: true, root: BASE_WEBFIGURE_URL}); + + // We want 'a' links (E.g. to open_figure) to use app.navigate + $(document).on('click', 'a', function (ev) { + var href = $(this).attr('href'); + // check that links are 'internal' to this app + if (href.substring(0, BASE_WEBFIGURE_URL.length) === BASE_WEBFIGURE_URL) { + ev.preventDefault(); + href = href.replace(BASE_WEBFIGURE_URL, "/"); + app.navigate(href, {trigger: true}); + } + }); + +}); diff --git a/omero_figure/static/figure/templates.js b/omero_figure/static/figure/templates.js new file mode 100644 index 000000000..1f01421eb --- /dev/null +++ b/omero_figure/static/figure/templates.js @@ -0,0 +1,1048 @@ +this["JST"] = this["JST"] || {}; + +this["JST"]["src/templates/channel_slider_template.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = '', __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { +__p += '\r\n

\r\n\r\n\r\n
\r\n \r\n \r\n \r\n

\r\n\r\n \r\n \r\n \r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n'; + +} +return __p +}; + +this["JST"]["src/templates/figure_panel_template.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = ''; +with (obj) { +__p += ' \r\n
\r\n
\r\n \r\n
\r\n
\r\n'; + +} +return __p +}; + +this["JST"]["src/templates/image_display_options_template.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = '', __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { +__p += '\r\n
\r\n \r\n\r\n
\r\n
\r\n\r\n\r\n
\r\n
\r\n\r\n
Z-sections:
\r\n
' + +((__t = ( sizeZ )) == null ? '' : __t) + +'
\r\n
\r\n
Timepoints:
\r\n
' + +((__t = ( sizeT )) == null ? '' : __t) + +'
\r\n
\r\n
Channels:
\r\n
\r\n '; + _.each(channel_labels, function(c, i) { + print(_.escape(c)); print((i < channels.length-1) ? ", " : ""); + }); ; +__p += '\r\n
\r\n \r\n \r\n \r\n'; + +} +return __p +}; + +this["JST"]["src/templates/labels_form_inner_template.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = '', __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { +__p += '\r\n
\r\n \r\n '; + if (!edit){ ; +__p += '\r\n
\r\n \r\n \r\n
\r\n '; + } ; +__p += '\r\n
\r\n\r\n
\r\n \r\n \r\n
\r\n\r\n
\r\n \r\n \r\n
\r\n\r\n \r\n\r\n '; + if (edit){ ; +__p += '\r\n \r\n '; + } else { ; +__p += '\r\n\r\n \r\n\r\n '; + } ; +__p += '\r\n'; + +} +return __p +}; + +this["JST"]["src/templates/labels_form_template.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = '', __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { +__p += '\r\n
\r\n\r\n '; + _.each(labels, function(l, i) { ; +__p += '\r\n\r\n
\r\n\r\n ' + +((__t = ( inner_template({l:l, position:position, edit:true}) )) == null ? '' : __t) + +'\r\n\r\n
\r\n\r\n '; + }); ; +__p += '\r\n
\r\n'; + +} +return __p +}; + +this["JST"]["src/templates/lut_picker.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = '', __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { +__p += '\r\n\r\n
\r\n\r\n '; + _.each(luts, function(lut, i) { ; +__p += '\r\n\r\n \r\n\r\n '; + if (i === parseInt(luts.length/2)) { ; +__p += '\r\n
\r\n
\r\n '; + } ; +__p += '\r\n\r\n '; + }) ; +__p += '\r\n
\r\n'; + +} +return __p +}; + +this["JST"]["src/templates/rois_form_template.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = '', __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { +__p += '\r\n\r\n
ROIs\r\n '; + if (panelCount > 0) { ; +__p += '\r\n ' + +((__t = ( roiCount )) == null ? '' : __t) + +' ROIs selected\r\n '; + } ; +__p += '\r\n
\r\n\r\n'; + if (panelCount > 0) { ; +__p += '\r\n\r\n
\r\n\r\n \r\n\r\n
\r\n \r\n \r\n
\r\n\r\n
\r\n
\r\n \r\n \r\n
\r\n '; + if (show){ ; +__p += '\r\n \r\n '; + } else { ; +__p += '\r\n \r\n '; + } ; +__p += '\r\n
\r\n\r\n
\r\n
\r\n Length\r\n
\r\n
\r\n \r\n
\r\n
\r\n \r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n
\r\n \r\n
\r\n\r\n
\r\n
\r\n Height\r\n
\r\n
\r\n \r\n
\r\n
\r\n px\r\n
\r\n\r\n
\r\n Label\r\n
\r\n
\r\n \r\n \r\n \r\n \r\n
\r\n
\r\n '; + if (show_label) print("pt") ; +__p += '\r\n
\r\n
\r\n '; + +} +return __p +}; + +this["JST"]["src/templates/scalebar_panel_template.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = '', __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { +__p += '\r\n
\r\n '; + if (show_label) { ; +__p += '\r\n
\r\n ' + +((__t = ( length )) == null ? '' : __t) + +' ' + +((__t = ( symbol )) == null ? '' : __t) + +'\r\n
\r\n '; + } ; +__p += '\r\n
\r\n'; + +} +return __p +}; + +this["JST"]["src/templates/viewport_inner_template.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = '', __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { +__p += '\r\n '; + _.each(imgs_css, function(css, i) { ; +__p += '\r\n \r\n '; + }); ; +__p += '\r\n'; + +} +return __p +}; + +this["JST"]["src/templates/viewport_template.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = ''; +with (obj) { +__p += '\r\n
Z
\r\n
\r\n
T
\r\n
\r\n
\r\n
\r\n ' + +((__t = ( inner_template({imgs_css:imgs_css, opacity:opacity}) )) == null ? '' : __t) + +'\r\n
\r\n'; + +} +return __p +}; + +this["JST"]["src/templates/xywh_panel_template.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = '', __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { +__p += '\r\n\r\n\r\n \r\n \r\n \r\n \r\n \r\n
\r\n Panel\r\n\r\n
\r\n
\r\n ' + +((__t = ( dpi )) == null ? '' : __t) + +' dpi\r\n\r\n '; + if (export_dpi != dpi && !isNaN(export_dpi)) { ; +__p += '\r\n (Export at ' + +((__t = ( export_dpi )) == null ? '' : __t) + +' dpi\r\n '; + if (export_dpi > dpi) { ; +__p += '\r\n \r\n '; + } ; +__p += '\r\n \r\n '; + } ; +__p += '\r\n
\r\n \r\n
\r\n \r\n
\r\n
\r\n
\r\n \r\n
\r\n \r\n
\r\n \r\n
\r\n \r\n
\r\n
\r\n
\r\n \r\n
\r\n \r\n
\r\n \r\n
\r\n \r\n
\r\n
\r\n \r\n
\r\n
\r\n
\r\n'; + +} +return __p +}; + +this["JST"]["src/templates/zoom_crop_template.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = '', __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { +__p += '\r\n
\r\n View\r\n \r\n x: ' + +((__t = ( x )) == null ? '' : __t) + +'\r\n y: ' + +((__t = ( y )) == null ? '' : __t) + +'\r\n width: ' + +((__t = ( width )) == null ? '' : __t) + +'\r\n height: ' + +((__t = ( height )) == null ? '' : __t) + +'\r\n \r\n
\r\n\r\n
\r\n \r\n '; + } ; +__p += '\r\n \r\n\r\n\r\n'; + }) ; +__p += '\r\n'; + +} +return __p +}; + +this["JST"]["src/templates/modal_dialogs/roi_modal_shape.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = '', __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { +__p += '\r\n\r\n'; + if (shapes.length > 1) { ; +__p += '\r\n '; + _.each(shapes, function(shape) { ; +__p += '\r\n \r\n '; + } ; +__p += '\r\n \r\n \r\n '; + }) ; +__p += '\r\n'; + } ; +__p += '\r\n'; + +} +return __p +}; + +this["JST"]["src/templates/modal_dialogs/roi_zt_buttons.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = '', __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { +__p += '\r\nZ: ' + +((__t = ( theZ + 1 )) == null ? '' : __t) + +'\r\n'; + if (theZ != origZ) { ; +__p += '\r\n \r\n'; + } ; +__p += '\r\nT: ' + +((__t = ( theT +1 )) == null ? '' : __t) + +'\r\n'; + if (theT != origT) { ; +__p += '\r\n \r\n'; + } ; +__p += '\r\n'; + +} +return __p +}; + +this["JST"]["src/templates/shapes/shape_item_template.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = '', __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { +__p += '\r\n
> 0) + " y1:" + (y1 >> 0) + " x2:" + (x2 >> 0) + " y2:" + (y2 >> 0)) ; +__p += '\r\n\r\n '; + if (type === 'ELLIPSE') print("x:" + (cx >> 0) + " y:" + (cy >> 0) + " rx:" + (rx >> 0) + " ry:" + (ry >> 0)) ; +__p += '\r\n\r\n
\r\n'; + +} +return __p +}; + +this["JST"]["src/templates/shapes/shape_toolbar_template.html"] = function(obj) { +obj || (obj = {}); +var __t, __p = '', __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { +__p += '\r\n\r\n
\r\n \r\n
\r\n\r\n\r\n
\r\n \r\n \r\n \r\n \r\n
\r\n\r\n\r\n\r\n\r\n\r\n
\r\n \r\n \r\n
\r\n\r\n\r\n
\r\n \r\n \r\n
\r\n\r\n\r\n
0) { ; +__p += '\r\n title="Load ' + +((__t = (omeroRoiCount)) == null ? '' : __t) + +' ROIs from OMERO"\r\n '; + } else { ; +__p += '\r\n title="This image has no ROIs on the OMERO server"\r\n '; + } ; +__p += '>\r\n ' + - '<% if ((i+1)%4 == 0){ %>
<% } %>' + - '<% }); %>' + - '
'; - - var compiled = _.template(t); - var html = compiled(json); - - $("#pickedColors").html(html); - } -}); - - - -var CropModalView = Backbone.View.extend({ - - el: $("#cropModal"), - - roiTemplate: JST["src/templates/modal_dialogs/crop_modal_roi.html"], - - model: FigureModel, - - roisPageSize: 200, - roisPage: 0, - roisCount: 0, - // not all these ROIs will contain Rects - roisLoaded: 0, - // Rectangles from ROIs - roiRects: [], - - initialize: function() { - - var self = this; - - // Here we handle init of the dialog when it's shown... - $("#cropModal").bind("show.bs.modal", function(){ - // Clone the 'first' selected panel as our reference for everything - self.m = self.model.getSelected().head().clone(); - self.listenTo(self.m, 'change:theZ change:theT', self.render); - - self.cropModel.set({'selected': false, 'width': 0, 'height': 0}); - - // get selected area... - var roi = self.m.getViewportAsRect(); - self.applyRotation(roi); - - // Show as ROI *if* it isn't the whole image - if (roi.x !== 0 || roi.y !== 0 - || roi.width !== self.m.get('orig_width') - || roi.height !== self.m.get('orig_height')) { - self.currentROI = roi; - self.cropModel.set({ - 'selected': true - }); - } - - // ...now zoom out and centre to render whole image - self.m.set({'zoom': 100, 'dx': 0, 'dy': 0}); - - self.zoomToFit(); // includes render() - // disable submit until user chooses a region/ROI - self.enableSubmit(false); - - // Reset ROIs from OMERO... - self.roiRects = []; - self.roisLoaded = 0; - self.roisPage = 0; - self.loadRoiRects(); - // ...along with ROIs from clipboard or on this image in the figure - self.showClipboardFigureRois(); - }); - - // keep track of currently selected ROI - this.currentROI = {'x':0, 'y': 0, 'width': 0, 'height': 0} - - // used by model underlying Rect. - // NB: values in cropModel are scaled by zoom percent - this.cropModel = new Backbone.Model({ - 'x':0, 'y': 0, 'width': 0, 'height': 0, - 'selected': false}); - // since resizes & drags don't actually update cropModel automatically, we do it... - this.cropModel.bind('drag_resize_stop', function(args) { - this.set({'x': args[0], 'y': args[1], 'width': args[2], 'height': args[3]}); - }); - this.cropModel.bind('drag_xy_stop', function(args) { - this.set({'x': args[0] + this.get('x'), 'y': args[1] + this.get('y')}); - }); - - // we also need to update the scaled ROI coords... - this.listenTo(this.cropModel, 'change:x change:y change:width change:height', function(m){ - var scale = self.zoom / 100; - self.currentROI = { - 'x': m.get('x') / scale, - 'y': m.get('y') / scale, - 'width': m.get('width') / scale, - 'height': m.get('height') / scale - } - // No-longer correspond to saved ROI coords - self.currentRoiId = undefined; - // Allow submit of dialog if valid ROI - if (self.regionValid(self.currentROI)) { - self.enableSubmit(true); - } else { - self.enableSubmit(false); - } - }); - - // Now set up Raphael paper... - this.paper = Raphael("crop_paper", 500, 500); - this.rect = new RectView({'model':this.cropModel, 'paper': this.paper}); - this.$cropImg = $('.crop_image', this.$el); - }, - - events: { - "click .roiPickMe": "roiPicked", - "click .loadRoiRects": "loadRoiRects", - "mousedown svg": "mousedown", - "mousemove svg": "mousemove", - "mouseup svg": "mouseup", - "submit .cropModalForm": "handleRoiForm" - }, - - // we disable Submit when dialog is shown, enable when region/ROI chosen - enableSubmit: function(enabled) { - var $okBtn = $('button[type="submit"]', this.$el); - if (enabled) { - $okBtn.prop('disabled', false); - $okBtn.prop('title', 'Crop selected images to chosen region'); - } else { - $okBtn.prop('disabled', 'disabled'); - $okBtn.prop('title', 'No valid region selected'); - } - }, - - // Region is only valid if it has width & height > 1 and - // is at least partially overlapping with the image - regionValid: function(roi) { - - if (roi.width < 2 || roi.height < 2) return false; - if (roi.x > this.m.get('orig_width')) return false; - if (roi.y > this.m.get('orig_height')) return false; - if (roi.x + roi.width < 0) return false; - if (roi.y + roi.height < 0) return false; - return true; - }, - - roiPicked: function(event) { - - var $target = $(event.target), - $tr = $target.parent(); - // $tr might be first if img clicked or if td clicked - // but in either case it will contain the img we need. - var $roi = $tr.find('img.roi_content'), - x = parseInt($roi.attr('data-x'), 10), - y = parseInt($roi.attr('data-y'), 10), - width = parseInt($roi.attr('data-width'), 10), - height = parseInt($roi.attr('data-height'), 10), - rotation = $roi.attr('data-rotation') || 0, - theT = parseInt($roi.attr('data-theT'), 10), - theZ = parseInt($roi.attr('data-theZ'), 10); - - // Rectangle ROIs have NO rotation. Copy of crop might have rotation - rotation = parseInt(rotation); - this.m.set('rotation', rotation); - - this.m.set({'theT': theT, 'theZ': theZ}); - - this.currentROI = { - 'x':x, 'y':y, 'width':width, 'height':height - } - // Update coords based on any rotation (if coords come from rotated crop region) - this.applyRotation(this.currentROI, 1, rotation); - - this.render(); - - this.cropModel.set({ - 'selected': true - }); - - // Save ROI ID - this.currentRoiId = $roi.attr('data-roiId'); - }, - - applyRotation: function(rect, factor=1, rotation) { - // Update the x and y coordinates of a Rectangle ROI to take account of rotation of the - // underlying image around it's centre point. The image is rotated on the canvas, so any - // Rectangle not at the centre will need to be rotated around the centre, updating rect.x and rect.y. - if (rotation === undefined) { - rotation = this.m.get('rotation'); - } - if (rotation != 0) { - var img_cx = this.m.get('orig_width') / 2; - var img_cy = this.m.get('orig_height') / 2; - var rect_cx = rect.x + (rect.width / 2); - var rect_cy = rect.y + (rect.height / 2); - var new_c = rotatePoint(rect_cx, rect_cy, img_cx, img_cy, rotation * factor); - rect.x = new_c.x - (rect.width / 2); - rect.y = new_c.y - (rect.height / 2); - } - }, - - handleRoiForm: function(event) { - event.preventDefault(); - // var json = this.processForm(); - var self = this, - r = this.currentROI, - sel = this.model.getSelected(), - sameT = sel.allEqual('theT'); - // sameZT = sel.allEqual('theT') && sel.allEqual('theT'); - - var getShape = function getShape(z, t) { - - // If all on one T-index, update to the current - // T-index that we're looking at. - if (sameT) { - t = self.m.get('theT'); - } - - self.applyRotation(r, -1); - - var rv = {'x': r.x, - 'y': r.y, - 'width': r.width, - 'height': r.height, - 'theZ': self.m.get('theZ'), - 'theT': t, - } - return rv; - } - - // IF we have an ROI selected (instead of hand-drawn shape) - // then try to use appropriate shape for that plane. - if (this.currentRoiId) { - - getShape = function getShape(currZ, currT) { - - var tzShapeMap = self.cachedRois[self.currentRoiId], - tkeys = _.keys(tzShapeMap).sort(), - zkeys, z, t, s; - - if (tzShapeMap[currT]) { - t = currT; - } else { - t = tkeys[parseInt(tkeys.length/2 ,10)] - } - zkeys = _.keys(tzShapeMap[t]).sort(); - if (tzShapeMap[t][currZ]) { - z = currZ; - } else { - z = zkeys[parseInt(zkeys.length/2, 10)] - } - s = tzShapeMap[t][z] - - // if we have a range of T values, don't change T! - if (!sameT) { - t = currT; - } - - return {'x': s.X, - 'y': s.Y, - 'width': s.Width, - 'height': s.Height, - 'theZ': z, - 'theT': t, - } - }; - } - - $("#cropModal").modal('hide'); - - // prepare callback for below - function cropAndClose(deleteROIs) { - // Don't set Z/T if we already have different Z/T indecies. - sel.each(function(m){ - var sh = getShape(m.get('theZ'), m.get('theT')), - newZ = Math.min(parseInt(sh.theZ, 10), m.get('sizeZ') - 1), - newT = Math.min(parseInt(sh.theT, 10), m.get('sizeT') - 1); - - m.cropToRoi({'x': sh.x, 'y': sh.y, 'width': sh.width, 'height': sh.height}); - if (deleteROIs) { - m.unset('shapes'); - } - // 'save' to trigger 'unsaved': true - m.save({ 'theZ': newZ, 'theT': newT, 'rotation': self.m.get('rotation')}); - }); - } - - // If we have ROIs on the image, ask if we want to delete them - var haveROIs = false, - plural = sel.length > 0 ? "s" : ""; - sel.each(function(p){ - if (p.get('shapes')) haveROIs = true; - }); - if (haveROIs) { - figureConfirmDialog("Delete ROIs?", - "Delete ROIs on the image" + plural + " you are cropping?", - ["Yes", "No", "Cancel"], - function(btnText){ - if (btnText == "Cancel") return; - if (btnText == "Yes") { - cropAndClose(true); - } else { - cropAndClose(); - } - } - ); - } else { - cropAndClose(); - } - }, - - mousedown: function(event) { - this.dragging = true; - var os = $(event.target).offset(); - this.clientX_start = event.clientX; - this.clientY_start = event.clientY; - this.imageX_start = this.clientX_start - os.left; - this.imageY_start = this.clientY_start - os.top; - this.cropModel.set({'x': this.imageX_start, 'y': this.imageY_start, 'width': 0, 'height': 0, 'selected': true}) - return false; - }, - - mouseup: function(event) { - if (this.dragging) { - this.dragging = false; - return false; - } - }, - - mousemove: function(event) { - if (this.dragging) { - var dx = event.clientX - this.clientX_start, - dy = event.clientY - this.clientY_start; - if (event.shiftKey) { - // make region square! - if (Math.abs(dx) > Math.abs(dy)) { - if (dy > 0) dy = Math.abs(dx); - else dy = -1 * Math.abs(dx); - } else { - if (dx > 0) dx = Math.abs(dy); - else dx = -1 * Math.abs(dy); - } - } - var negX = Math.min(0, dx), - negY = Math.min(0, dy); - this.cropModel.set({'x': this.imageX_start + negX, - 'y': this.imageY_start + negY, - 'width': Math.abs(dx), 'height': Math.abs(dy)}); - return false; - } - }, - - showClipboardFigureRois: function() { - // Show Rectangles from clipboard - var clipboardRects = [], - clipboard = this.model.get('clipboard'); - if (clipboard && clipboard.CROP) { - roi = clipboard.CROP; - clipboardRects.push({ - x: roi.x, y: roi.y, width: roi.width, height: roi.height, - rotation: roi.rotation - }); - } else if (clipboard && clipboard.SHAPES) { - clipboard.SHAPES.forEach(function(roi){ - if (roi.type === "Rectangle") { - clipboardRects.push({ - x: roi.x, y: roi.y, width: roi.width, height: roi.height - }); - } - }); - } - var msg = "No Regions copied to clipboard"; - this.renderRois(clipboardRects, ".roisFromClipboard", msg); - - // Show Rectangles from panels in figure - var figureRois = []; - var sel = this.model.getSelected(); - sel.forEach(function(panel) { - var panelRois = panel.get('shapes'); - if (panelRois) { - panelRois.forEach(function(roi){ - if (roi.type === "Rectangle") { - figureRois.push({ - x: roi.x, y: roi.y, width: roi.width, height: roi.height - }); - } - }); - } - }); - msg = "No Rectangular ROIs on selected panel in figure"; - this.renderRois(figureRois, ".roisFromFigure", msg); - }, - - // Load Rectangles from OMERO and render them - loadRoiRects: function(event) { - if (event) { - event.preventDefault(); - } - var self = this, - iid = self.m.get('imageId'); - var offset = this.roisPageSize * this.roisPage; - var url = BASE_WEBFIGURE_URL + 'roiRectangles/' + iid + '/?limit=' + self.roisPageSize + '&offset=' + offset; - $.getJSON(url, function(rsp){ - data = rsp.data; - self.roisLoaded += data.length; - self.roisPage += 1; - self.roisCount = rsp.meta.totalCount; - // get a representative Rect from each ROI. - // Include a z and t index, trying to pick current z/t if ROI includes a shape there - var currT = self.m.get('theT'), - currZ = self.m.get('theZ'); - var cachedRois = {}, // roiId: shapes (z/t dict) - roi, roiId, shape, theT, theZ, z, t, rect, tkeys, zkeys, - minT, maxT, - shapes; // dict of all shapes by z & t index - - for (var r=0; r>0] - } - zkeys = _.keys(shapes[t]) - .map(function(x){return parseInt(x, 10)}) - .sort(function(a, b){return a - b}); // sort numerically - if (shapes[t][currZ]) { - z = currZ; - } else { - z = zkeys[(zkeys.length/2)>>0] - } - shape = shapes[t][z] - self.roiRects.push({'theZ': shape.TheZ, - 'theT': shape.TheT, - 'x': shape.X, - 'y': shape.Y, - 'width': shape.Width, - 'height': shape.Height, - 'roiId': roiId, - 'tStart': minT, - 'tEnd': maxT, - 'zStart': zkeys[0], - 'zEnd': zkeys[zkeys.length-1]}); - } - // Show ROIS from OMERO... - var msg = "No rectangular ROIs found on this image in OMERO"; - self.renderRois(self.roiRects, ".roisFromOMERO", msg); - - if (self.roisLoaded < self.roisCount) { - // Show the 'Load' button if more are available - $(".loadRoiRects", this.$el).show(); - } else { - $(".loadRoiRects", this.$el).hide(); - } - $("#cropRoiMessage").html(`Loaded ${self.roisLoaded} / ${self.roisCount} ROIs`); - - self.cachedRois = cachedRois; - }).error(function(){ - var msg = "No rectangular ROIs found on this image in OMERO"; - self.renderRois([], ".roisFromOMERO", msg); - }); - }, - - renderRois: function(rects, target, msg) { - - var orig_width = this.m.get('orig_width'), - orig_height = this.m.get('orig_height'), - origT = this.m.get('theT'), - origZ = this.m.get('theZ'); - - var html = "", - size = 50, - rect, src, zoom, - top, left, div_w, div_h, img_w, img_h; - - // loop through ROIs, using our cloned model to generate src urls - // first, get the current Z and T of cloned model... - this.m.set('z_projection', false); // in case z_projection is true - - for (var r=0; r -1) this.m.set('theT', rect.theT, {'silent': true}); - if (rect.theZ > -1) this.m.set('theZ', rect.theZ, {'silent': true}); - src = this.m.get_img_src(true); - if (rect.width > rect.height) { - div_w = size; - div_h = (rect.height/rect.width) * div_w; - } else { - div_h = size; - div_w = (rect.width/rect.height) * div_h; - } - zoom = div_w/rect.width; - img_w = orig_width * zoom; - img_h = orig_height * zoom; - top = -(zoom * rect.y); - left = -(zoom * rect.x); - rect.theT = rect.theT !== undefined ? rect.theT : origT; - rect.theZ = rect.theZ !== undefined ? rect.theZ : origZ; - let css = this.m._viewport_css(left, top, img_w, img_h, size, size, rotation); - - var json = { - 'msg': msg, - 'src': src, - 'rect': rect, - 'w': div_w, - 'h': div_h, - 'css': css, - 'top': top, - 'left': left, - 'img_w': img_w, - 'img_h': img_h, - 'theZ': rect.theZ + 1, - 'theT': rect.theT + 1, - 'roiId': rect.roiId, - 'tStart': false, - 'zStart': false, - } - // set start/end indices (1-based) if we have them - if (rect.tStart !== undefined) {json.tStart = (+rect.tStart) + 1} - if (rect.tEnd !== undefined) {json.tEnd = (+rect.tEnd) + 1} - if (rect.zStart !== undefined) {json.zStart = (+rect.zStart) + 1} - if (rect.zEnd !== undefined) {json.zEnd = (+rect.zEnd) + 1} - html += this.roiTemplate(json); - } - if (html.length === 0) { - html = "" + msg + ""; - } - $(target + " tbody", this.$el).html(html); - - // reset Z/T as before - this.m.set({'theT': origT, 'theZ': origZ}); - }, - - zoomToFit: function() { - var max_w = 500, - max_h = 450, - w = this.m.get('orig_width'), - h = this.m.get('orig_height'); - scale = Math.min(max_w/w, max_h/h); - this.setZoom(scale * 100); - }, - - setZoom: function(percent) { - this.zoom = percent; - this.render(); - }, - - render: function() { - var scale = this.zoom / 100, - w = this.m.get('orig_width'), - h = this.m.get('orig_height'); - var newW = w * scale, - newH = h * scale; - this.m.set('zoom', 100); - this.m.set('width', newW); - this.m.set('height', newH); - var src = this.m.get_img_src(true); - var css = this.m.get_vp_full_plane_css(100, newW, newH); - - this.paper.setSize(newW, newH); - $("#crop_paper").css({'height': newH, 'width': newW}); - $("#cropViewer").css({'height': newH, 'width': newW}); - - this.$cropImg.css(css) - .attr('src', src); - - var roiX = this.currentROI.x * scale, - roiY = this.currentROI.y * scale, - roiW = this.currentROI.width * scale, - roiH = this.currentROI.height * scale; - this.cropModel.set({ - 'x': roiX, 'y': roiY, 'width': roiW, 'height': roiH, - 'selected': true - }); - } - }); - - - // -------------------------- Backbone VIEWS ----------------------------------------- - - - // var SelectionView = Backbone.View.extend({ - var FigureView = Backbone.View.extend({ - - el: $("#body"), - - initialize: function(opts) { - - // Delegate some responsibility to other views - new AlignmentToolbarView({model: this.model}); - new AddImagesModalView({model: this.model, figureView: this}); - new SetIdModalView({model: this.model}); - new PaperSetupModalView({model: this.model}); - new CropModalView({model: this.model}); - new ChgrpModalView({ model: this.model }); - new RoiModalView({model: this.model}); - new DpiModalView({model: this.model}); - new LegendView({model: this.model}); - new LabelFromMapsModal({model: this.model}); - - this.figureFiles = new FileList(); - new FileListView({model:this.figureFiles, figureModel: this.model}); - - // set up various elements and we need repeatedly - this.$main = $('main'); - this.$canvas = $("#canvas"); - this.$canvas_wrapper = $("#canvas_wrapper"); - this.$figure = $("#figure"); - this.$copyBtn = $(".copy"); - this.$pasteBtn = $(".paste"); - this.$saveBtn = $(".save_figure.btn"); - this.$saveOption = $("li.save_figure"); - this.$saveAsOption = $("li.save_as"); - this.$deleteOption = $("li.delete_figure"); - - var self = this; - - // Render on changes to the model - this.model.on('change:paper_width change:paper_height change:page_count', this.render, this); - - // If a panel is added... - this.model.panels.on("add", this.addOne, this); - - // Don't leave the page with unsaved changes! - window.onbeforeunload = function() { - var canEdit = self.model.get('canEdit'); - if (self.model.get("unsaved")) { - return "Leave page with unsaved changes?"; - } - }; - - $("#zoom_slider").slider({ - max: 400, - min: 10, - value: 75, - slide: function(event, ui) { - self.model.set('curr_zoom', ui.value); - } - }); - - // respond to zoom changes - this.listenTo(this.model, 'change:curr_zoom', this.renderZoom); - this.listenTo(this.model, 'change:selection', this.renderSelectionChange); - this.listenTo(this.model, 'change:unsaved', this.renderSaveBtn); - this.listenTo(this.model, 'change:figureName', this.renderFigureName); - - // Full render if page_color changes (might need to update labels etc) - this.listenTo(this.model, 'change:page_color', this.render); - this.listenTo(this.model, 'change:page_color', this.renderPanels); - - this.listenTo(this.model, 'change:loading_count', this.renderLoadingSpinner); - - // refresh current UI - this.renderZoom(); - - // 'Auto-render' on init. - this.render(); - this.renderSelectionChange(); - - }, - - events: { - "click .export_pdf": "export_pdf", - "click .export_options li": "export_options", - "click .add_panel": "addPanel", - "click .delete_panel": "deleteSelectedPanels", - "click .copy": "copy_selected_panels", - "click .paste": "paste_panels", - "click .save_figure": "save_figure_event", - "click .save_as": "save_as_event", - "click .new_figure": "goto_newfigure", - "click .open_figure": "open_figure", - "click .export_json": "export_json", - "click .import_json": "import_json", - "click .delete_figure": "delete_figure", - "click .chgrp_figure": "chgrp_figure", - "click .local_storage": "local_storage", - "click .paper_setup": "paper_setup", - "click .export-options a": "select_export_option", - "click .zoom-paper-to-fit": "zoom_paper_to_fit", - "click .about_figure": "show_about_dialog", - "click .figure-title": "start_editing_name", - "keyup .figure-title input": "figuretitle_keyup", - "blur .figure-title input": "stop_editing_name", - "submit .importJsonForm": "import_json_form" - }, - - keyboardEvents: { - 'backspace': 'deleteSelectedPanels', - 'del': 'deleteSelectedPanels', - 'mod+a': 'select_all', - 'mod+c': 'copy_selected_panels', - 'mod+v': 'paste_panels', - 'mod+s': 'save_figure_event', - 'mod+n': 'goto_newfigure', - 'mod+o': 'open_figure', - 'down' : 'nudge_down', - 'up' : 'nudge_up', - 'left' : 'nudge_left', - 'right' : 'nudge_right', - }, - - // If any modal is visible, we want to ignore keyboard events above - // All those methods should use this - modal_visible: function() { - return $("div.modal:visible").length > 0; - }, - - // choose an export option from the drop-down list - export_options: function(event) { - event.preventDefault(); - - var $target = $(event.target); - - // Only show check mark on the selected item. - $(".export_options .glyphicon-ok").css('visibility', 'hidden'); - $(".glyphicon-ok", $target).css('visibility', 'visible'); - - // Update text of main export_pdf button. - var txt = $target.attr('data-export-option'); - $('.export_pdf').text("Export " + txt).attr('data-export-option', txt); - - // Hide download button - $("#pdf_download").hide(); - }, - - paper_setup: function(event) { - event.preventDefault(); - - $("#paperSetupModal").modal(); - }, - - show_about_dialog: function(event) { - event.preventDefault(); - $("#aboutModal").modal(); - }, - - // Editing name workflow... - start_editing_name: function(event) { - var $this = $(event.target); - var name = $this.text(); - // escape any double-quotes - name = name.replace(/"/g, '"'); - $this.html(''); - $('input', $this).focus(); - }, - figuretitle_keyup: function(event) { - // If user hit Enter, stop editing... - if (event.which === 13) { - event.preventDefault(); - this.stop_editing_name(); - } - }, - stop_editing_name: function() { - var $this = $(".figure-title input"); - var new_name = $this.val().trim(); - if (new_name.length === 0) { - alert("Can't have empty name.") - return; - } - $(".figure-title").html(_.escape(new_name)); - // Save name... will renderFigureName only if name changed - this.model.save('figureName', new_name); - - // clear file list (will be re-fetched when needed) - this.figureFiles.reset(); - }, - - // Heavy lifting of PDF generation handled by OMERO.script... - export_pdf: function(event){ - - event.preventDefault(); - - // Status is indicated by showing / hiding 3 buttons - var figureModel = this.model, - $create_figure_pdf = $(event.target), - export_opt = $create_figure_pdf.attr('data-export-option'), - $pdf_inprogress = $("#pdf_inprogress"), - $pdf_download = $("#pdf_download"), - $script_error = $("#script_error"), - exportOption = "PDF"; - $create_figure_pdf.hide(); - $pdf_download.hide(); - $script_error.hide(); - $pdf_inprogress.show(); - - // Map from HTML to script options - opts = {"PDF": "PDF", - "PDF & images": "PDF_IMAGES", - "TIFF": "TIFF", - "TIFF & images": "TIFF_IMAGES", - "to OMERO": "OMERO"}; - exportOption = opts[export_opt]; - - // Get figure as json - var figureJSON = this.model.figure_toJSON(); - - var url = MAKE_WEBFIGURE_URL, - data = { - figureJSON: JSON.stringify(figureJSON), - exportOption: exportOption, - }; - - // Start the Figure_To_Pdf.py script - $.post( url, data).done(function( data ) { - - // {"status": "in progress", "jobId": "ProcessCallback/64be7a9e-2abb-4a48-9c5e-6d0938e1a3e2 -t:tcp -h 192.168.1.64 -p 64592"} - var jobId = data.jobId; - - // E.g. Handle 'No Processor Available'; - if (!jobId) { - if (data.error) { - alert(data.error); - } else { - alert("Error exporting figure"); - } - $create_figure_pdf.show(); - $pdf_inprogress.hide(); - return; - } - - // Now we keep polling for script completion, every second... - - var i = setInterval(function (){ - - $.getJSON(ACTIVITIES_JSON_URL, function(act_data) { - - var pdf_job = act_data[jobId]; - - // We're waiting for this flag... - if (pdf_job.status == "finished") { - clearInterval(i); - - $create_figure_pdf.show(); - $pdf_inprogress.hide(); - - // Show result - if (pdf_job.results.New_Figure) { - var fa_id = pdf_job.results.New_Figure.id; - if (pdf_job.results.New_Figure.type === "FileAnnotation") { - var fa_download = WEBINDEX_URL + "annotation/" + fa_id + "/"; - $pdf_download - .attr({'href': fa_download, 'data-original-title': 'Download Figure'}) - .show() - .children('span').prop('class', 'glyphicon glyphicon-download-alt'); - } else if (pdf_job.results.New_Figure.type === "Image") { - var fa_download = pdf_job.results.New_Figure.browse_url; - $pdf_download - .attr({'href': fa_download, 'data-original-title': 'Go to Figure Image'}) - .show() - .tooltip() - .children('span').prop('class', 'glyphicon glyphicon-share'); - } - } else if (pdf_job.stderr) { - // Only show any errors if NO result - var stderr_url = WEBINDEX_URL + "get_original_file/" + pdf_job.stderr + "/"; - $script_error.attr('href', stderr_url).show(); - } - } - - if (act_data.inprogress === 0) { - clearInterval(i); - } - - }).error(function() { - clearInterval(i); - }); - - }, 1000); - }); - }, - - select_export_option: function(event) { - event.preventDefault(); - var $a = $(event.target), - $span = $a.children('span.glyphicon'); - // We take the from the and place it in the
") - .css({'left': left, 'top': top}) - .prependTo(this.$figure); - } - $pages = $(".paper"); - } - - $pages.css({'width': page_w, 'height': page_h, 'background-color': '#' + page_color}); - - this.$figure.css({'width': size.w, 'height': size.h, - 'left': figure_left, 'top': figure_top}); - - $("#canvas").css({'width': canvas_w, - 'height': canvas_h}); - - // always want to do this? - this.zoom_paper_to_fit(); - - return this; - } - }); - - - - var AlignmentToolbarView = Backbone.View.extend({ - - el: $("#alignment-toolbars"), - - model:FigureModel, - - events: { - "click .aleft": "align_left", - "click .agrid": "align_grid", - "click .atop": "align_top", - "click .aright": "align_right", - "click .abottom": "align_bottom", - - "click .awidth": "align_width", - "click .aheight": "align_height", - "click .asize": "align_size", - "click .amagnification": "align_magnification", - - "click #custom_grid_gap": "custom_grid_gap", - }, - - initialize: function() { - this.listenTo(this.model, 'change:selection', this.render); - this.$buttons = $(".alignment-buttons button", this.$el); - }, - - align_left: function(event) { - event.preventDefault(); - this.model.align_left(); - }, - - align_grid: function(event) { - event.preventDefault(); - let gridGap = document.querySelector('input[name="grid_gap"]:checked').value; - this.model.align_grid(gridGap); - }, - - align_width: function(event) { - event.preventDefault(); - this.model.align_size(true, false); - }, - - align_height: function(event) { - event.preventDefault(); - this.model.align_size(false, true); - }, - - align_size: function(event) { - event.preventDefault(); - this.model.align_size(true, true); - }, - - align_magnification: function(event) { - event.preventDefault(); - this.model.align_magnification(); - }, - - align_top: function(event) { - event.preventDefault(); - this.model.align_top(); - }, - - align_right: function(event) { - event.preventDefault(); - this.model.align_right(); - }, - - align_bottom: function(event) { - event.preventDefault(); - this.model.align_bottom(); - }, - - custom_grid_gap: function() { - let current = $("#custom_grid_gap").attr("value"); - // simple propmt to ask user for custom grid gap - let gridGap = prompt("Enter grid gap in pixels:", current); - gridGap = parseFloat(gridGap); - if (isNaN(gridGap)) { - alert("Please enter a valid number (of pixels)") - return; - } - // this value will get picked up as radio grid_gap value - $("#custom_grid_gap").attr("value", gridGap); - // Show the value in the drop-down menu - $("#custom_grid_gap_label").text("(" + gridGap + " px)"); - }, - - render: function() { - if (this.model.getSelected().length > 1) { - this.$buttons.removeAttr("disabled"); - } else { - this.$buttons.attr("disabled", "disabled"); - } - } - }); - - -// -// Copyright (C) 2014-2021 University of Dundee & Open Microscopy Environment. -// All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// - -var FigureFile = Backbone.Model.extend({ - - defaults: { - disabled: false, - }, - - initialize: function() { - - var desc = this.get('description'); - if (desc && desc.imageId) { - this.set('imageId', desc.imageId); - } else { - this.set('imageId', 0); - } - if (desc && desc.baseUrl) { - this.set('baseUrl', desc.baseUrl); - } - }, - - isVisible: function(filter) { - if (filter.owner) { - if (this.get('owner').id !== filter.owner) { - return false; - } - } - if (filter.group) { - if (this.get('group').id !== filter.group) { - return false; - } - } - if (filter.name) { - // Search for files that have all words in - var name = this.get('name').toLowerCase(); - var words = $.trim(filter.name).split(" "); - var visible = words.reduce(function(prev, t){ - return prev && name.indexOf(t) > -1 - }, true); - return visible; - } - return true; - } -}); - - -var FileList = Backbone.Collection.extend({ - - model: FigureFile, - - comparator: 'creationDate', - - initialize: function() { - }, - - disable: function(fileId) { - // enable all first - this.where({disabled: true}).forEach(function(f){ - f.set('disabled', false); - }); - - var f = this.get(fileId); - if (f) { - f.set('disabled', true); - } - }, - - deleteFile: function(fileId, name) { - // may not have fetched files... - var f = this.get(fileId), // might be undefined - msg = "Delete '" + name + "'?", - self = this; - if (confirm(msg)) { - $.post( DELETE_WEBFIGURE_URL, { fileId: fileId }) - .done(function(){ - self.remove(f); - app.navigate("", {trigger: true}); - }); - } - }, - - url: function() { - return LIST_WEBFIGURES_URL; - } -}); - - -var FileListView = Backbone.View.extend({ - - el: $("#openFigureModal"), - - initialize:function (options) { - this.$tbody = $('tbody', this.$el); - this.$fileFilter = $('#file-filter'); - this.owner = USER_ID; - if (window.IS_PUBLIC_USER) { - delete this.owner; - } - var self = this; - // we automatically 'sort' on fetch, add etc. - this.model.bind("sync remove sort", this.render, this); - this.$fileFilter.val(""); - - // we only need this to know the currently opened file - this.figureModel = options.figureModel; - - $("#openFigureModal").bind("show.bs.modal", function(){ - // When the dialog opens, we load files... - var currentFileId = self.figureModel.get('fileId'); - if (self.model.length === 0) { - self.refresh_files(); - } else { - self.render(); - } - }); - }, - - events: { - "click .sort-created": "sort_created", - "click .sort-created-reverse": "sort_created_reverse", - "click .sort-name": "sort_name", - "click .sort-name-reverse": "sort_name_reverse", - "click .pick-owner": "pick_owner", - "click .pick-group": "pick_group", - "keyup #file-filter": "filter_files", - "click .refresh-files": "refresh_files", - }, - - refresh_files: function(event) { - // will trigger sort & render() - var loadingHtml = "

Loading Files...

" - this.$tbody.html(loadingHtml); - this.model.fetch(); - }, - - filter_files: function(event) { - // render() will pick the new filter text - this.render(); - }, - - sort_created: function(event) { - this.render_sort_btn(event); - this.model.comparator = 'creationDate'; - this.model.sort(); - }, - - sort_created_reverse: function(event) { - this.render_sort_btn(event); - this.model.comparator = function(left, right) { - var l = left.get('creationDate'), - r = right.get('creationDate'); - return l < r ? 1 : l > r ? -1 : 0; - }; - this.model.sort(); - }, - - sort_name: function(event) { - this.render_sort_btn(event); - this.model.comparator = 'name'; - this.model.sort(); - }, - - sort_name_reverse: function(event) { - this.render_sort_btn(event); - this.model.comparator = function(left, right) { - var l = left.get('name'), - r = right.get('name'); - return l < r ? 1 : l > r ? -1 : 0; - }; - this.model.sort(); - }, - - render_sort_btn: function(event) { - $("th .btn-sm", this.$el).addClass('muted'); - $(event.target).removeClass('muted'); - }, - - pick_owner: function(event) { - event.preventDefault() - var owner = $(event.target).data('id'); - if (owner != -1) { - this.owner = owner; - } else { - delete this.owner; - } - this.render(); - }, - - pick_group: function (event) { - event.preventDefault() - var group = $(event.target).data('id'); - if (group != -1) { - this.group = group; - } else { - delete this.group; - } - this.render(); - }, - - render:function () { - var self = this, - filter = {}, - filterVal = this.$fileFilter.val(), - currentFileId = this.figureModel.get('fileId'); - if (this.owner && this.owner != -1) { - filter.owner = this.owner; - } - if (this.group && this.group != -1) { - filter.group = this.group; - } - if (filterVal.length > 0) { - filter.name = filterVal.toLowerCase(); - } - this.$tbody.empty(); - if (this.model.models.length === 0) { - var msg = "" + - "You have no figures. Start by
creating a new figure" + - ""; - self.$tbody.html(msg); - } - _.each(this.model.models, function (file) { - if (file.isVisible(filter)) { - var disabled = currentFileId === file.get('id') ? true: false; - file.set('disabled', disabled); - var e = new FileListItemView({model:file}).render().el; - self.$tbody.prepend(e); - } - }); - var owners = this.model.pluck("owner"); - var ownersByName = {}; - owners.forEach(function(o){ - let name = o.firstName + " " + o.lastName; - ownersByName[name] = o.id; - }); - var ownersNames = Object.keys(ownersByName); - // Sort by last name - ownersNames.sort(function compare(a, b) { - var aNames = a.split(" "), - aN = aNames[aNames.length - 1], - bNames = b.split(" "), - bN = bNames[bNames.length - 1]; - return aN > bN; - }); - var ownersHtml = "
  • -- Show All --
  • "; - ownersHtml += "
  • "; - _.each(ownersNames, function(name) { - ownersHtml += "
  • " + _.escape(name) + "
  • "; - }); - $("#owner-menu").html(ownersHtml); - - // render groups chooser - var groups = this.model.pluck("group"); - var groupsByName = {}; - groups.forEach(function (g) { - groupsByName[g.name] = g.id; - }) - var groupNames = Object.keys(groupsByName); - groupNames.sort(); - var groupsHtml = "
  • -- Show All --
  • "; - groupsHtml += groupNames.map(function (name) { return "
  • " + _.escape(name) + "
  • "}).join('\n'); - $("#group-menu").html(groupsHtml); - return this; - } -}); - -var FileListItemView = Backbone.View.extend({ - - tagName:"tr", - - template: JST["src/templates/files/figure_file_item.html"], - - initialize:function () { - this.model.bind("change", this.render, this); - this.model.bind("destroy", this.close, this); - }, - - events: { - "click a": "hide_file_chooser" - }, - - hide_file_chooser: function() { - $("#openFigureModal").modal('hide'); - }, - - formatDate: function(secs) { - // if secs is a number, create a Date... - if (secs * 1000) { - var d = new Date(secs * 1000), - s = d.toISOString(); // "2014-02-26T23:09:09.415Z" - s = s.replace("T", " "); - s = s.substr(0, 16); - return s; - } - // handle string - return secs; - }, - - render:function () { - var json = this.model.toJSON(), - baseUrl = json.baseUrl; - if (!json.imageId){ - // Description may be json encoded... - try { - var d = JSON.parse(json.description); - // ...with imageId and name (unicode supported) - if (d.imageId) { - json.imageId = d.imageId; - // we cache this so we don't have to parse() on each render() - this.model.set('imageId', d.imageId); - } - } catch (err) { - console.log('failed to parse json', json.description); - } - } - baseUrl = baseUrl || BASE_WEBFIGURE_URL.slice(0, -1); // remove last / - json.thumbSrc = baseUrl + "/render_thumbnail/" + json.imageId + "/"; - json.url = BASE_WEBFIGURE_URL + "file/" + json.id; - json.formatDate = this.formatDate; - var h = this.template(json); - $(this.el).html(h); - return this; - } - -}); - -var InfoPanelView = Backbone.View.extend({ - - template: JST["src/templates/info_panel_template.html"], - xywh_template: JST["src/templates/xywh_panel_template.html"], - - initialize: function(opts) { - this.render = _.debounce(this.render); - this.figureModel = opts.figureModel; - this.models = opts.models; - if (opts.models.length > 1) { - var self = this; - this.models.forEach(function(m){ - self.listenTo(m, 'change:x change:y change:width change:height change:imageId change:zoom change:min_export_dpi change:max_export_dpi', self.render); - }); - } else if (opts.models.length == 1) { - this.model = opts.models.head(); - this.listenTo(this.model, 'change:x change:y change:width change:height change:zoom change:min_export_dpi change:max_export_dpi', this.render); - this.listenTo(this.model, 'drag_resize', this.drag_resize); - } - }, - - events: { - "click .setId": "setImageId", - "click .set_dpi": "set_dpi", - "click .clear_dpi": "clear_dpi", - "blur .xywh_form input": "handle_xywh", - "keyup .xywh_form input": "handle_xywh", - "click .setAspectRatio": "lockAspectRatio" - }, - - handle_xywh: function(event) { - // Ignore the 2nd blur event generated during render() - if (this.rendering) { - return; - } - if (event.type === "keyup" && event.which !== 13) { - return; - } - var attr = event.target.getAttribute("name"); - var value = parseInt(event.target.value, 10); - if (isNaN(value)) { - return; - } - - var figsize = this.figureModel.getFigureSize(); - // Avoid re-rendering and losing focus everytime there is a Blur event - // set(attr, value) will not cause render() - this.ignoreChange = true; - var aspectRatioStatus = false; - this.models.forEach(function(m) { - if (attr === 'x' || attr ==='y') { - var old = m.get(attr); - var coords = {}; - coords[attr] = old; - var offset = this.figureModel.getPageOffset(coords); - var newValue = old - offset[attr] + value; - // Keep panel within figure limits - if (attr === 'x') { - if (newValue > figsize.w || newValue < 0) { - this.ignoreChange = false; - } - newValue = Math.min(figsize.w, newValue); - } else if (attr === 'y') { - if (newValue > figsize.h || newValue < 0) { - this.ignoreChange = false; - } - newValue = Math.min(figsize.h, newValue); - } - newValue = Math.max(0, newValue); - m.set(attr, newValue); - } - else { - if (value < 1) { - this.render(); - return; - } - - //Check aspect ratio button state - //If selected, check attribute and value and then recalculate other attribute value - //Set both values parallely - var newWidthHeight = {}; - newWidthHeight[attr] = value; - - if ($(".setAspectRatio", this.$el).hasClass("aspectRatioSelected")) { - aspectRatioStatus = true; - var widthCur = m.get('width'); - var heightCur = m.get('height'); - var aspRatio = widthCur/heightCur; - - if (attr === 'width'){ - var heightNew = value/aspRatio; - newWidthHeight['height'] = heightNew; - } - else { - var widthNew = value * aspRatio; - newWidthHeight['width'] = widthNew; - } - this.ignoreChange = false; - } - m.save(newWidthHeight); - } - }.bind(this)); - // Timout for ignoreChange - // Only reset this AFTER render() is called - setTimeout(function(){ - this.ignoreChange = false; - // keep locked status of the aspect ratio button the same, - // when the focus shifts because of a blur event - if (aspectRatioStatus) { - $(".setAspectRatio", this.$el).addClass("aspectRatioSelected"); - } - }.bind(this), 50); - - }, - - set_dpi: function(event) { - event.preventDefault(); - $("#dpiModal").modal('show'); - }, - - // remove optional min_export_dpi attribute from selected panels - clear_dpi: function(event) { - event.preventDefault(); - this.models.forEach(function(m) { - m.unset("min_export_dpi"); - }); - }, - - setImageId: function(event) { - event.preventDefault(); - // Simply show dialog - Everything else handled by SetIdModalView - $("#setIdModal").modal('show'); - $("#setIdModal .imgId").val("").focus(); - }, - - lockAspectRatio: function(event) { - event.preventDefault(); - $(".setAspectRatio", this.$el).toggleClass("aspectRatioSelected"); - }, - - // just update x,y,w,h by rendering ONE template - drag_resize: function(xywh) { - $("#xywh_table").remove(); - var json = {'x': xywh[0].toFixed(0), - 'y': xywh[1].toFixed(0), - 'width': xywh[2].toFixed(0), - 'height': xywh[3].toFixed(0)}; - var offset = this.figureModel.getPageOffset(json); - json.x = offset.x; - json.y = offset.y; - json.dpi = this.model.getPanelDpi(json.width, json.height); - json.export_dpi = this.model.get('min_export_dpi'); - this.$el.append(this.xywh_template(json)); - }, - - getImageLinks: function(remoteUrl, imageIds, imageNames) { - // Link if we have a single remote image, E.g. http://jcb-dataviewer.rupress.org/jcb/img_detail/625679/ - var imageLinks = []; - if (remoteUrl) { - if (imageIds.length == 1) { - imageLinks.push({'text': 'Image viewer', 'url': remoteUrl}); - } - // OR all the images are local... - } else { - imageLinks.push({'text': 'Webclient', 'url': WEBINDEX_URL + "?show=image-" + imageIds.join('|image-')}); - - // Handle other 'Open With' options - OPEN_WITH.forEach(function(v){ - var selectedObjs = imageIds.map(function(id, i){ - return {'id': id, 'name': imageNames[i], 'type': 'image'}; - }); - var enabled = false; - if (typeof v.isEnabled === "function") { - enabled = v.isEnabled(selectedObjs); - } else if (typeof v.supported_objects === "object" && v.supported_objects.length > 0) { - enabled = v.supported_objects.reduce(function(prev, supported){ - // enabled if plugin supports 'images' or 'image' (if we've selected a single image) - return prev || supported === 'images' || (supported === 'image' && imageIds.length === 1); - }, false); - } - if (!enabled) return; - - // Get the link via url provider... - var url = v.url + '?image=' + imageIds.join('&image='); - if (v.getUrl) { - url = v.getUrl(selectedObjs, v.url); - } - // Ignore any 'Open with OMERO.figure' urls - if (url.indexOf(BASE_WEBFIGURE_URL) === 0) { - return; - } - var label = v.label || v.id; - imageLinks.push({'text': label, 'url': url}); - }); - } - return imageLinks; - }, - - // render BOTH templates - render: function() { - // If event comes from handle_xywh() then we don't need to render() - if (this.ignoreChange) { - return; - } - // Flag to ignore blur events caused by $el.html() below - this.rendering = true; - var json, - title = this.models.length + " Panels Selected...", - remoteUrl; - - var imageIds = this.models.pluck('imageId'); - var imageNames = this.models.pluck('name'); - this.models.forEach(function(m) { - if (m.get('baseUrl')) { - // only used when a single image is selected - remoteUrl = m.get('baseUrl') + "/img_detail/" + m.get('imageId') + "/"; - } - // start with json data from first Panel - var this_json = m.toJSON(); - // Format floating point values - _.each(["x", "y", "width", "height"], function(a){ - if (this_json[a] != "-") { - this_json[a] = this_json[a].toFixed(0); - } - }); - var offset = this.figureModel.getPageOffset(this_json); - this_json.x = offset.x; - this_json.y = offset.y; - this_json.dpi = m.getPanelDpi(); - this_json.channel_labels = this_json.channels.map(function(c){return c.label}) - if (!json) { - json = this_json; - } else { - json.name = title; - // compare json summary so far with this Panel - var attrs = ["imageId", "orig_width", "orig_height", "sizeT", "sizeZ", "x", "y", "width", "height", "dpi", "min_export_dpi", "max_export_dpi"]; - _.each(attrs, function(a){ - if (json[a] != this_json[a]) { - if (a === 'x' || a === 'y' || a === 'width' || a === 'height') { - json[a] = ""; - } else { - json[a] = "-"; - } - } - }); - // handle channel names - if (this_json.channels.length != json.channel_labels.length) { - json.channel_labels = ["-"]; - } else { - _.each(this_json.channels, function(c, idx){ - if (json.channel_labels[idx] != c.label) { - json.channel_labels[idx] = '-'; - } - }); - } - - } - }.bind(this)); - - json.export_dpi = Math.min(json.dpi, json.max_export_dpi); - if (!isNaN(json.min_export_dpi)) { - json.export_dpi = Math.max(json.export_dpi, json.min_export_dpi); - } - - json.imageLinks = this.getImageLinks(remoteUrl, imageIds, imageNames); - - // all setId if we have a single Id - json.setImageId = _.uniq(imageIds).length == 1; - - if (json) { - var html = this.template(json), - xywh_html = this.xywh_template(json); - this.$el.html(html + xywh_html); - } - this.rendering = false; - return this; - } -}); - - -var LabelFromMapsModal = Backbone.View.extend({ - - el: $("#labelsFromMapAnns"), - - model: FigureModel, - - // label options: position, size, color - options: {}, - - /** - * Constructor - listen for dialog opening to load data and render - */ - initialize: function() { - // when dialog is shown, load map annotations for selected images - $("#labelsFromMapAnns").bind("show.bs.modal", function(event){ - // event options from the label form: {position: 'top', size: '12', color: '0000'} - this.options = event.relatedTarget; - this.loadMapAnns(); - }.bind(this)); - }, - - events: { - "submit .labelsFromMapAnnsForm": "handleForm", - "change select": "renderExampleLabel", - "change input": "renderExampleLabel", - }, - - /** - * Load the map annotations, then call render() - */ - loadMapAnns() { - let imageIds = this.model.getSelected().map(function(m){return m.get('imageId')}); - this.isLoading = true; - $('select', this.el).html(""); - - var url = WEBINDEX_URL + "api/annotations/?type=map&image="; - url += imageIds.join("&image="); - - $.getJSON(url, function(data) { - this.isLoading = false; - this.annotations = data.annotations; - this.render(); - }.bind(this) - ); - }, - - /** - * Handle submission of the form to create labels and close dialog - * - * @param {Object} event - */ - handleForm: function(event) { - event.preventDefault(); - if (this.isLoading) return; - - var choice = $("input[name='kvpChoice']:checked").val(); - var key; - if(choice === "single-key"){ - key = $('select', this.el).val(); - } - - var includeKey = $("input[name='includeKey']").is(':checked'); - var labelSize = this.options.size || "12"; - var labelPosition = this.options.position || "top"; - var labelColor = this.options.color || "000000"; - - var imageKeyValues = this.annotations.reduce(function(prev, t){ - var iid = t.link.parent.id; - if (!prev[iid]) { - prev[iid] = []; - } - t.values.forEach(function(kv) { - var value = kv[1]; - // If key matches, add to values for the image (no duplicates) - if ((choice === "all-keys" || (kv[0] === key)) && prev[iid].indexOf(value) === -1) { - prev[iid].push(kv); - } - }); - return prev; - }, {}); - - this.model.getSelected().forEach(function(p){ - var iid = p.get('imageId'); - if (imageKeyValues[iid]) { - var labels = imageKeyValues[iid].map(function(value){ - return { - 'text': includeKey ? (value[0].replaceAll("_","\\_") + ': ' + value[1].replaceAll("_","\\_")) : value[1].replaceAll("_","\\_"), - 'size': labelSize, - 'position': labelPosition, - 'color': labelColor, - } - }); - p.add_labels(labels); - } - }); - $("#labelsFromMapAnns").modal('hide'); - return false; - }, - - /** - * Renders the Example label based on currently selected Key and includeKey - */ - renderExampleLabel: function() { - var key = $('select', this.el).val(); - var includeKey = $("input[name='includeKey']").is(':checked'); - // find first annotation with this value - var label; - for (var a=0; a for choosing Key. Also calls renderExampleLabel() - */ - render: function() { - // Get keys for images {'key' : {iid: true}} - var keys = {}; - this.annotations.forEach(function(ann) { - let iid = ann.link.parent.id; - ann.values.forEach(function(kv){ - var key = kv[0]; - if (!keys[key]) { - keys[key] = {}; - } - keys[key][iid] = true; - }) - }); - - // Make a list of keys (and sort) along with counts of images for each key - var keyList = []; - var keyCounts = {}; - for (var key in keys) { - if (keys.hasOwnProperty(key)) { - keyList.push(key); - keyCounts[key] = Object.keys(keys[key]).length; - } - } - keyList.sort(function(a, b) { - return (a.toUpperCase() < b.toUpperCase()) ? -1 : 1; - }); - - var html = keyList.map(function(key) { - return ""; - }).join(""); - if (keyList.length === 0) { - html = ""; - } - $('select', this.el).html(html); - - this.renderExampleLabel(); - } -}); - - -// Copyright (c) 2015 University of Dundee. - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. - -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - -var LegendView = Backbone.View.extend({ - - // Use 'body' to handle Menu: File > Add Figure Legend - el: $("body"), - - model:FigureModel, - - editing: false, - - initialize: function() { - - this.listenTo(this.model, - 'change:x change:legend', this.legendChanged); - - this.render(); - }, - - events: { - "click .edit-legend": "editLegend", - "click .cancel-legend": "cancelLegend", - "click .save-legend": "saveLegend", - "click .collapse-legend": "collapseLegend", - "click .expand-legend": "expandLegend", - "click .markdown-info": "markdownInfo", - "click .panel-body p": "legendClick", - }, - - // Click on the legend

    . Start editing or follow link - legendClick: function(event) { - event.preventDefault(); - // If link, open new window / tab - if (event.target.nodeName.toLowerCase() == "a") { - var href = event.target.getAttribute('href'); - window.open(href, '_blank'); - return false; - // Click on legend text expands then edits legend - } else { - if (this.model.get("legend_collapsed")) { - this.expandLegend(); - } else { - this.editLegend(); - } - } - }, - - markdownInfo: function(event) { - event.preventDefault(); - $("#markdownInfoModal").modal('show'); - }, - - collapseLegend: function(event) { - this.renderCollapsed(true); - this.model.set("legend_collapsed", true); - }, - - expandLegend: function(event) { - this.renderCollapsed(false); - this.model.set("legend_collapsed", false); - }, - - saveLegend: function(event) { - event.preventDefault(); - var legendTxt = $("#js-legend textarea").val(); - this.model.save("legend", legendTxt); - // This will happen anyway if legend has changed, - // but just in case it hasn't.... - this.editing = false; - this.render(); - }, - - editLegend: function(event) { - if (event) event.preventDefault(); - this.editing = true; - if (this.model.get("legend_collapsed")) { - this.model.set("legend_collapsed", false); - } - this.render(); - }, - - cancelLegend: function(event) { - event.preventDefault(); - this.editing = false; - this.render(); - }, - - renderCollapsed: function(collapsed) { - var $panel = $('.panel', self.el); - if (collapsed) { - $panel.addClass('legend-collapsed'); - $panel.removeClass('legend-expanded'); - } else { - $panel.removeClass('legend-collapsed'); - $panel.addClass('legend-expanded'); - } - }, - - // May have chaned by opening a new Figure etc. - legendChanged: function() { - this.editing = false; - this.render(); - }, - - render: function() { - - var self = this, - legendText = this.model.get('legend') || "", - legendCollapsed = this.model.get('legend_collapsed'), - $el = $("#js-legend"), - $edit = $('.edit-legend', $el), - $save = $('.save-legend', $el), - $cancel = $('.cancel-legend', $el), - $panel = $('.panel', $el), - $legend = $('.legend', $el); - - self.renderCollapsed(legendCollapsed); - - // if we're editing... - if (self.editing) { - // show 'cancel' and 'save' buttons - $edit.hide(); - $save.show(); - $cancel.show(); - - $panel.addClass('editing'); - $panel.show(); - var html = ''; - $legend.html(html); - // Show above the right-hand panel when editing - $(".legend-container").css("z-index", 100); - } else { - // ...only show 'edit' button - $edit.show(); - $save.hide(); - $cancel.hide(); - $(".legend-container").css("z-index", 1); - - $panel.removeClass('editing'); - if (legendText.length === 0) { - $panel.hide(); - $edit.text("Add Figure Legend"); - } else { - $panel.show(); - legendText = markdown.toHTML( legendText ); - $legend.html(legendText); - $edit.text("Edit Figure Legend"); - } - } - } - }); - - -// -// Copyright (C) 2016 University of Dundee & Open Microscopy Environment. -// All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// - - -// Should only ever have a singleton on this -var LutPickerView = Backbone.View.extend({ - - el: $("#lutpickerModal"), - - template: JST["src/templates/lut_picker.html"], - - LUT_NAMES: ["16_colors.lut", - "3-3-2_rgb.lut", - "5_ramps.lut", - "6_shades.lut", - "blue_orange_icb.lut", - "brgbcmyw.lut", - "cool.lut", - "cyan_hot.lut", - "edges.lut", - "fire.lut", - "gem.lut", - "glasbey.lut", - "glasbey_inverted.lut", - "glow.lut", - "grays.lut", - "green_fire_blue.lut", - "hilo.lut", - "ica.lut", - "ica2.lut", - "ica3.lut", - "ice.lut", - "magenta_hot.lut", - "orange_hot.lut", - "phase.lut", - "physics.lut", - "pup_br.lut", - "pup_nr.lut", - "rainbow_rgb.lut", - "red-green.lut", - "red_hot.lut", - "royal.lut", - "sepia.lut", - "smart.lut", - "spectrum.lut", - "thal.lut", - "thallium.lut", - "thermal.lut", - "unionjack.lut", - "yellow_hot.lut"], - - initialize:function () { - }, - - - events: { - "click button[type='submit']": "handleSubmit", - "click .lutOption": "pickLut", - }, - - handleSubmit: function() { - this.success(this.pickedLut); - $("#lutpickerModal").modal('hide'); - }, - - pickLut: function(event) { - var lutName = event.currentTarget.getAttribute('data-lut'); - // Save the name - used in handleSubmit(); - this.pickedLut = lutName; - - // Update preview to show LUT - var bgPos = this.getLutBackgroundPosition(lutName); - $(".lutPreview", this.el).css('background-position', bgPos); - // Enable OK button - $("button[type='submit']", this.el).removeAttr('disabled'); - }, - - loadLuts: function() { - var url = WEBGATEWAYINDEX + 'luts/'; - var promise = $.getJSON(url); - return promise; - }, - - getLutBackgroundPosition: function(lutName) { - var lutIndex = this.LUT_NAMES.indexOf(lutName); - var css = {}; - if (lutIndex > -1) { - return '0px -' + ((lutIndex * 50) +2) + 'px'; - } else { - return '0px 100px'; // hides background - } - }, - - formatLutName: function(lutName) { - lutName = lutName.replace(".lut", ""); - lutName = lutName.replace(/_/g, " "); - // Title case - lutName = lutName[0].toUpperCase() + lutName.slice(1); - return lutName; - }, - - show: function(options) { - - $("#lutpickerModal").modal('show'); - - // save callback to use on submit - if (options.success) { - this.success = options.success; - } - - this.loadLuts().done(function(data){ - this.luts = data.luts; - this.render(); - }.bind(this)).fail(function(){ - // E.g. 404 with Older OMERO (pre 5.3.0) - this.render(); - }.bind(this)); - }, - - render:function() { - - var html = "", - luts; - if (!this.luts) { - html = "

    Loading Lookup Tables failed.

    "; - html += "

    Your OMERO version does not support LUTs. Please upgrade.

    "; - } else { - luts = this.luts.map(function(lut) { - // Add css background-position to each lut to offset luts_10.png - return {'bgPos': this.getLutBackgroundPosition(lut.name), - 'name': lut.name, - 'displayName': this.formatLutName(lut.name)}; - }.bind(this)); - html = this.template({'luts': luts}); - } - $(".modal-body", this.el).html(html); - } -}); - - -// Events, show/hide and rendering for various Modal dialogs. - - var DpiModalView = Backbone.View.extend({ - - el: $("#dpiModal"), - - model: FigureModel, - - initialize: function(options) { - - var self = this; - // when dialog is shown, clear and render - $("#dpiModal").bind("show.bs.modal", function(){ - self.render(); - }); - }, - - events: { - "submit .dpiModalForm": "handleDpiForm", - }, - - handleDpiForm: function(event) { - event.preventDefault(); - - var minDpiVal = $(".min_export_dpi", this.el).val(); - var minDpi = parseInt(minDpiVal, 10); - var maxDpiVal = $(".max_export_dpi", this.el).val(); - var maxDpi = parseInt(maxDpiVal, 10); - var sel = this.model.getSelected(); - - // if we have invalid number... - if (isNaN(maxDpi)) { - alert("Need to enter valid integer for dpi values"); - return false; - } - - sel.forEach(function(p) { - var toset = {max_export_dpi: maxDpi}; - if (!isNaN(minDpi)) { - toset.min_export_dpi = minDpi; - } else { - p.unset("min_export_dpi"); - } - p.save(toset); - }); - $("#dpiModal").modal('hide'); - return false; - }, - - render: function() { - var sel = this.model.getSelected(); - var minDpi = sel.getIfEqual('min_export_dpi') || 300; - var maxDpi = sel.getIfEqual('max_export_dpi') || '-'; - - $(".min_export_dpi", this.el).val(minDpi); - $(".max_export_dpi", this.el).val(maxDpi); - } - }); - - var PaperSetupModalView = Backbone.View.extend({ - - el: $("#paperSetupModal"), - - template: JST["src/templates/modal_dialogs/paper_setup_modal_template.html"], - - model:FigureModel, - - events: { - "submit .paperSetupForm": "handlePaperSetup", - "change .paperSizeSelect": "rerender", - // "keyup #dpi": "rerenderDb", - "change input": "rerender", - "click .pageColor": "handlePaperColor", - }, - - handlePaperColor: function(event) { - event.preventDefault(); - - var page_color = $(event.target).val(); - FigureColorPicker.show({ - 'color': page_color, - 'pickedColors': ['#000000', '#ffffff', '#eeeeee'], - 'success': function(newColor){ - // simply update - $('.pageColor', this.$el).val(newColor); - }.bind(this) - }); - - return false; - }, - - initialize: function(options) { - - var self = this; - $("#paperSetupModal").bind("show.bs.modal", function(){ - self.render(); - }); - // don't update while typing - // this.rerenderDb = _.debounce(this.rerender, 1000); - }, - - processForm: function() { - - // On form submit, need to work out paper width & height - var $form = $('form', this.$el), - dpi = 72, - pageCount = $('.pageCountSelect', $form).val(), - size = $('.paperSizeSelect', $form).val(), - orientation = $form.find('input[name="pageOrientation"]:checked').val(), - custom_w = parseInt($("#paperWidth").val(), 10), - custom_h = parseInt($("#paperHeight").val(), 10), - units = $('.wh_units:first', $form).text(), - pageColor = $('.pageColor', $form).val().replace('#', ''), - dx, dy; - - var w_mm, h_m, w_pixels, h_pixels; - if (size == 'A4') { - w_mm = 210; - h_mm = 297; - } else if (size == 'A3') { - w_mm = 297; - h_mm = 420; - } else if (size == 'A2') { - w_mm = 420; - h_mm = 594; - } else if (size == 'A1') { - w_mm = 594; - h_mm = 841; - } else if (size == 'A0') { - w_mm = 841; - h_mm = 1189; - } else if (size == 'letter') { - w_mm = 216; - h_mm = 280; - } else if (size == 'mm') { - // get dims from custom fields and units - w_mm = custom_w; - h_mm = custom_h; - } else if (size == 'crop') { - var coords = this.model.getCropCoordinates(); - w_pixels = coords.paper_width; - h_pixels = coords.paper_height; - dx = coords.dx; - dy = coords.dy; - // Single page is cropped to include ALL panels - pageCount = 1; - } - if (w_mm && h_mm) { - // convert mm -> pixels (inch is 25.4 mm) - w_pixels = Math.round(dpi * w_mm / 25.4); - h_pixels = Math.round(dpi * h_mm / 25.4); - } else { - // convert pixels -> mm - w_mm = Math.round(w_pixels * 25.4 / dpi); - h_mm = Math.round(h_pixels * 25.4 / dpi); - } - - if (orientation == 'horizontal' && size != 'mm') { - var tmp = w_mm; w_mm = h_mm; h_mm = tmp; - tmp = w_pixels; w_pixels = h_pixels; h_pixels = tmp; - } - - var cols = pageCount; - if (pageCount > 3) { - cols = Math.ceil(pageCount/2); - } - - var rv = { - // 'dpi': dpi, - 'page_size': size, - 'orientation': orientation, - 'width_mm': w_mm, - 'height_mm': h_mm, - 'paper_width': w_pixels, - 'paper_height': h_pixels, - 'page_count': pageCount, - 'page_col_count': cols, - 'page_color': pageColor, - }; - if (dx !== undefined || dy !== undefined) { - rv.dx = dx; - rv.dy = dy; - } - return rv; - }, - - handlePaperSetup: function(event) { - event.preventDefault(); - var json = this.processForm(); - - // if 'crop' page to panels - if (json.page_size === 'crop') { - this.model.panels.forEach(function(p){ - p.save({'x': p.get('x') + json.dx, - 'y': p.get('y') + json.dy}); - }); - // paper is now a 'custom' size (not A4 etc) - json.page_size = 'mm'; - // don't need these - delete json.dx; - delete json.dy; - } - - this.model.set(json); - $("#paperSetupModal").modal('hide'); - }, - - rerender: function() { - var json = this.processForm(); - this.render(json); - }, - - render: function(json) { - json = json || this.model.toJSON(); - // if we're not manually setting mm or pixels, disable - json.wh_disabled = (json.page_size != 'mm'); - // json.units = json.page_size == 'mm' ? 'mm' : 'pixels'; - // if (json.page_size == "mm") { - // json.paper_width = json.width_mm; - // json.paper_height = json.height_mm; - // } - - this.$el.find(".modal-body").html(this.template(json)); - }, - }); - - - var SetIdModalView = Backbone.View.extend({ - - el: $("#setIdModal"), - - template: JST["src/templates/modal_dialogs/preview_Id_change_template.html"], - - model:FigureModel, - - events: { - "submit .addIdForm": "previewSetId", - "click .preview": "previewSetId", - "keyup .imgId": "keyPressed", - "click .doSetId": "doSetId", - }, - - initialize: function(options) { - - var self = this; - - // when dialog is shown, clear and render - $("#setIdModal").bind("show.bs.modal", function(){ - delete self.newImg; - self.render(); - }); - }, - - // Only enable submit button when input has a number in it - keyPressed: function() { - var idInput = $('input.imgId', this.$el).val(), - previewBtn = $('button.preview', this.$el), - re = /^\d+$/; - if (re.test(idInput)) { - previewBtn.removeAttr("disabled"); - } else { - previewBtn.attr("disabled", "disabled"); - } - }, - - // handle adding Images to figure - previewSetId: function(event) { - event.preventDefault(); - - var self = this, - idInput = $('input.imgId', this.$el).val(); - - // get image Data - $.getJSON(BASE_WEBFIGURE_URL + 'imgData/' + parseInt(idInput, 10) + '/', function(data){ - - // just pick what we need - var newImg = { - 'imageId': data.id, - 'name': data.meta.imageName, - // 'width': data.size.width, - // 'height': data.size.height, - 'sizeZ': data.size.z, - 'theZ': data.rdefs.defaultZ, - 'sizeT': data.size.t, - // 'theT': data.rdefs.defaultT, - 'channels': data.channels, - 'orig_width': data.size.width, - 'orig_height': data.size.height, - // 'x': px, - // 'y': py, - 'datasetName': data.meta.datasetName, - 'pixel_size_x': data.pixel_size.valueX, - 'pixel_size_y': data.pixel_size.valueY, - 'pixel_size_z': data.pixel_size.valueZ, - 'pixel_size_x_unit': data.pixel_size.unitX, - 'pixel_size_z_unit':data.pixel_size.unitZ, - 'pixel_size_x_symbol': data.pixel_size.symbolX, - 'pixel_size_z_symbol': data.pixel_size.symbolZ, - 'deltaT': data.deltaT, - }; - self.newImg = newImg; - self.render(); - }).fail(function(event) { - alert("Image ID: " + idInput + - " could not be found on the server, or you don't have permission to access it"); - }); - }, - - doSetId: function() { - - var self = this, - sel = this.model.getSelected(); - - if (!self.newImg) return; - - sel.forEach(function(p) { - p.setId(self.newImg); - }); - this.model.set('unsaved', true); - - }, - - render: function() { - - var sel = this.model.getSelected(), - selImg, - json = {}; - - if (sel.length < 1) { - self.selectedImage = null; - return; // shouldn't happen - } - selImg = sel.head(); - json.selImg = selImg.toJSON(); - json.newImg = {}; - json.comp = {}; - json.messages = []; - - json.ok = function(match, match2) { - if (typeof match == 'undefined') return "-"; - if (typeof match2 != 'undefined') { - match = match && match2; - } - var m = match ? "ok" : "flag"; - var rv = ""; - return rv; - }; - - // thumbnail - json.selThumbSrc = WEBGATEWAYINDEX + "render_thumbnail/" + json.selImg.imageId + "/"; - - // minor attributes ('info' only) - var attrs = ["orig_width", "orig_height"], - attrName = ['Width', 'Height']; - - if (this.newImg) { - json.newImg = this.newImg; - // compare attrs above - _.each(attrs, function(a, i) { - if (json.selImg[a] == json.newImg[a]) { - json.comp[a] = true; - } else { - json.comp[a] = false; - json.messages.push({"text":"Mismatch of " + attrName[i] + ": should be OK.", - "status": "success"}); // status correspond to css alert class. - } - }); - // special message for sizeT - if (json.selImg.sizeT != json.newImg.sizeT) { - // check if any existing images have theT > new.sizeT - var tooSmallT = false; - sel.forEach(function(o){ - if (o.get('theT') > json.newImg.sizeT) tooSmallT = true; - }); - if (tooSmallT) { - json.messages.push({"text": "New Image has fewer Timepoints than needed. Check after update.", - "status": "danger"}); - } else { - json.messages.push({"text":"Mismatch of Timepoints: should be OK.", - "status": "success"}); - } - json.comp.sizeT = false; - } else { - json.comp.sizeT = true; - } - - // special message for sizeZ - if (json.selImg.sizeZ != json.newImg.sizeZ) { - // check if any existing images have theZ > new.sizeZ - var tooSmallZ = false; - sel.forEach(function(o){ - if (o.get('theZ') > json.newImg.sizeZ) tooSmallZ = true; - }); - if (tooSmallZ) { - json.messages.push({"text": "New Image has fewer Z slices than needed. Check after update.", - "status": "danger"}); - } else { - json.messages.push({"text":"Mismatch of Z slices: should be OK.", - "status": "success"}); - } - json.comp.sizeZ = false; - } else { - json.comp.sizeZ = true; - } - - // compare channels - json.comp.channels = json.ok(true); - var selC = json.selImg.channels, - newC = json.newImg.channels, - cCount = selC.length; - if (cCount != newC.length) { - json.comp.channels = json.ok(false); - json.messages.push({"text":"New Image has " + newC.length + " channels " + - "instead of " + cCount + ". Check after update.", - "status": "danger"}); - } else { - for (var i=0; i 10) { - iIds = idInput.split('image-').slice(1); - } else if (idInput.indexOf('img_detail') > 0) { - // url of image viewer... - this.importFromRemote(idInput); - return; - } else { - iIds = idInput.split(','); - } - - this.model.addImages(iIds); - }, - - importFromRemote: function(img_detail_url) { - var iid = parseInt(img_detail_url.split('img_detail/')[1], 10), - baseUrl = img_detail_url.split('/img_detail')[0], - // http://jcb-dataviewer.rupress.org/jcb/imgData/25069/ - imgDataUrl = baseUrl + '/imgData/' + iid + "/"; - - var colCount = 1, - rowCount = 1, - paper_width = this.model.get('paper_width'), - c = this.figureView.getCentre(), - col = 0, - row = 0, - px, py, spacer, scale, - coords = {'px': px, - 'py': py, - 'c': c, - 'spacer': spacer, - 'colCount': colCount, - 'rowCount': rowCount, - 'col': col, - 'row': row, - 'paper_width': paper_width}; - - this.model.importImage(imgDataUrl, coords, baseUrl); - }, - }); - - - // -------------------------Panel View ----------------------------------- - // A Panel is a
    , added to the #paper by the FigureView below. - var PanelView = Backbone.View.extend({ - tagName: "div", - className: "imagePanel", - template: JST["src/templates/figure_panel_template.html"], - label_template: JST["src/templates/labels/label_template.html"], - label_vertical_template: JST["src/templates/labels/label_vertical_template.html"], - label_right_vertical_template: JST["src/templates/labels/label_right_vertical_template.html"], - label_table_template: JST["src/templates/labels/label_table_template.html"], - scalebar_template: JST["src/templates/scalebar_panel_template.html"], - - - initialize: function(opts) { - // we render on Changes in the model OR selected shape etc. - this.model.on('destroy', this.remove, this); - this.listenTo(this.model, - 'change:x change:y change:width change:height change:zoom change:dx change:dy change:rotation', - this.render_layout); - this.listenTo(this.model, 'change:scalebar change:pixel_size_x', this.render_scalebar); - this.listenTo(this.model, - 'change:zoom change:dx change:dy change:width change:height change:channels change:theZ change:theT change:z_start change:z_end change:z_projection change:min_export_dpi', - this.render_image); - this.listenTo(this.model, - 'change:channels change:zoom change:dx change:dy change:width change:height change:rotation change:labels change:theT change:deltaT change:theZ change:deltaZ change:z_projection change:z_start change:z_end', - this.render_labels); - this.listenTo(this.model, 'change:shapes', this.render_shapes); - - // During drag or resize, model isn't updated, but we trigger 'drag' - this.model.on('drag_resize', this.drag_resize, this); - - // Used for rendering labels against page_color background - if (opts.page_color) { - this.page_color = opts.page_color; - } - this.render(); - }, - - events: { - // "click .img_panel": "select_panel" - }, - - // During drag, we resize etc - drag_resize: function(xywh) { - var x = xywh[0], - y = xywh[1], - w = xywh[2], - h = xywh[3]; - if (w == this.model.get('width') && h == this.model.get('height')) { - // If we're only dragging - simply update position - this.$el.css({'top': y +'px', 'left': x +'px'}); - } else { - this.update_resize(x, y, w, h); - this.render_shapes(); - } - this.$el.addClass('dragging'); - }, - - render_layout: function() { - var x = this.model.get('x'), - y = this.model.get('y'), - w = this.model.get('width'), - h = this.model.get('height'); - - this.update_resize(x, y, w, h); - this.$el.removeClass('dragging'); - }, - - update_resize: function(x, y, w, h) { - - // update layout of panel on the canvas - this.$el.css({'top': y +'px', - 'left': x +'px', - 'width': w +'px', - 'height': h +'px'}); - - // container needs to be square for rotation to vertical - $('.left_vlabels', this.$el).css('width', 3 * h + 'px'); - $('.right_vlabels', this.$el).css('width', 3 * h + 'px'); - - // update the img within the panel - var zoom = this.model.get('zoom'), - vp_css = this.model.get_vp_img_css(zoom, w, h), - svg_css = this.model.get_vp_full_plane_css(zoom, w, h), - panel_scale = svg_css.width / this.model.get('orig_width'); - - // If we're resizing a BIG image, layout can be buggy, - // so we simply hide while resizing - if (this.model.is_big_image()) { - if (w !== this.model.get('width') || h !== this.model.get('height')) { - vp_css.width = 0; - vp_css.height = 0; - } - } - - // These two elements are siblings under imgContainer and must - // will be exactly on top of each other for non-big images. - this.$img_panel.css(vp_css); - this.$panel_canvas.css(svg_css); - - // panel_canvas contains the shapeManager svg, which we zoom: - if (this.shapeManager) { - this.shapeManager.setZoom(panel_scale * 100); - } - - // update length of scalebar - var sb = this.model.get('scalebar'); - if (sb && sb.show) { - // this.$scalebar.css('width':); - var physical_length = sb.length; - // convert units - var pixel_unit = this.model.get('pixel_size_x_unit'); - var scalebar_unit = sb.units; - var convert_factor = LENGTH_UNITS[scalebar_unit].microns / LENGTH_UNITS[pixel_unit].microns; - var sb_pixels = convert_factor * physical_length / this.model.get('pixel_size_x'); - var sb_width = panel_scale * sb_pixels; - this.$scalebar.css('width', sb_width); - } - }, - - render_shapes: function() { - var shapes = this.model.get('shapes'), - w = this.model.get('orig_width'), - h = this.model.get('orig_height'); - if (shapes) { - // init shapeManager if doesn't exist - if (!this.shapeManager) { - var canvasId = this.$panel_canvas.attr('id'); - this.$panel_canvas.attr({'width': w + 'px', 'height': h + 'px'}); - var panel_scale = this.$panel_canvas.width() / w; - this.shapeManager = new ShapeManager(canvasId, w, h, {'readOnly': true}); - this.shapeManager.setZoom(panel_scale * 100); - } - this.shapeManager.setShapesJson(shapes); - } else { - // delete shapes - if (this.shapeManager) { - this.shapeManager.deleteAllShapes(); - } - } - }, - - render_image: function() { - - // For big images, layout changes will update src to render a new viewport - // But we don't want the previous image showing while we wait... - if (this.model.is_big_image()) { - this.$img_panel.hide(); - $(".glyphicon-refresh", this.$el).show(); - } - this.$img_panel.one("load", function(){ - $(".glyphicon-refresh", this.$el).hide(); - this.$img_panel.show(); - }.bind(this)); - - var src = this.model.get_img_src(); - this.$img_panel.attr('src', src); - - // if a 'reasonable' dpi is set, we don't pixelate - if (this.model.get('min_export_dpi') > 100) { - this.$img_panel.removeClass('pixelated'); - } else { - this.$img_panel.addClass('pixelated'); - } - }, - - render_labels: function() { - - $('.label_layout', this.$el).remove(); // clear existing labels - - var labels = this.model.get('labels'), - self = this, - positions = { - 'top':[], 'bottom':[], 'left':[], 'right':[], - 'leftvert':[],'rightvert':[], - 'topleft':[], 'topright':[], - 'bottomleft':[], 'bottomright':[] - }; - - // group labels by position - _.each(labels, function(l) { - // check if label is dynamic delta-T - var ljson = $.extend(true, {}, l); - // If label is same color as page (and is outside of panel) - if (ljson.color.toLowerCase() == self.page_color.toLowerCase() && - ["top", "bottom", "left", "right", "leftvert", "rightvert"].indexOf(l.position) > -1 ) { - // If black -> white, otherwise -> black - if (ljson.color === '000000') { - ljson.color = 'ffffff'; - } else { - ljson.color = '000000'; - } - } - const matches = [...ljson.text.matchAll(/\[.+?\]/g)]; // Non greedy regex capturing expressions in [] - if (matches.length>0){ - var new_text = ""; - var last_idx = 0; - for (const match of matches) {// Loops on the match to replace in the ljson.text the expression by their values - var new_text = new_text + ljson.text.slice(last_idx, match.index); - - // Label parsing in three steps: - // - split label type.format from other parameters (;) - // - split label type and format (.) - // - grab other parameters (key=value) - var expr = match[0].slice(1,-1).split(";"); - var prop_nf = expr[0].trim().split("."); - var param_dict = {}; - expr.slice(1).forEach(function(value) { - var kv = value.split("="); - if (kv.length > 1) { - param_dict[kv[0].trim()] = parseInt(kv[1].trim()); - } - }); - - var label_value = "", - format, precision; - if (['time', 't'].includes(prop_nf[0])) { - format = prop_nf[1] ? prop_nf[1] : "index"; - precision = param_dict["precision"] !== undefined ? param_dict["precision"] : 0; // decimal places default to 0 - label_value = self.model.get_time_label_text(format, param_dict["offset"], precision); - } else if (['image', 'dataset'].includes(prop_nf[0])){ - format = prop_nf[1] ? prop_nf[1] : "name"; - label_value = self.model.get_name_label_text(prop_nf[0], format); - //Escape the underscore for markdown - label_value = label_value.replaceAll("_", "\\_"); - } else if (['x', 'y', 'z', 'width', 'height', 'w', 'h', 'rotation', 'rot'].includes(prop_nf[0])){ - format = prop_nf[1] ? prop_nf[1] : "pixel"; - precision = param_dict["precision"] !== undefined ? param_dict["precision"] : 2; // decimal places default to 2 - label_value = self.model.get_view_label_text(prop_nf[0], format, param_dict["offset"], precision); - } else if (['channels', 'c'].includes(prop_nf[0])) { - label_value = self.model.get_channels_label_text(); - } else if (['zoom'].includes(prop_nf[0])) { - label_value = self.model.get_zoom_label_text(); - } - - //If label_value hasn't been created (invalid prop_nf[0]) - // or is empty (invalid prop_nf[1]), the expression is kept intact - new_text = new_text + (label_value ? label_value : match[0]); - last_idx = match.index + match[0].length; - } - ljson.text = new_text + ljson.text.slice(last_idx); - } - - // Markdown also escapes all labels so they are safe - ljson.text = markdown.toHTML(ljson.text); - - positions[l.position].push(ljson); - }); - - // Render template for each position and append to Panel.$el - var html = ""; - _.each(positions, function(lbls, p) { - var json = {'position':p, 'labels':lbls}; - if (lbls.length === 0) return; - if (p == 'leftvert') { // vertical - html += self.label_vertical_template(json); - } else if (p == 'rightvert') { - html += self.label_right_vertical_template(json); - } else if (p == 'left' || p == 'right') { - html += self.label_table_template(json); - } else { - html += self.label_template(json); - } - }); - self.$el.append(html); - - // need to force update of vertical labels layout - $('.left_vlabels', self.$el).css('width', 3 * self.$el.height() + 'px'); - $('.right_vlabels', self.$el).css('width', 3 * self.$el.height() + 'px'); - - return this; - }, - - render_scalebar: function() { - - if (this.$scalebar) { - this.$scalebar.remove(); - } - var sb = this.model.get('scalebar'); - if (sb && sb.show) { - var sb_json = {}; - sb_json.position = sb.position; - sb_json.color = sb.color; - sb_json.length = sb.length; - sb_json.height = sb.height; - sb_json.font_size = sb.font_size; - sb_json.show_label = sb.show_label; - sb_json.symbol = sb.units; - // Use global LENGTH_UNITS to get symbol for unit. - if (window.LENGTH_UNITS && window.LENGTH_UNITS[sb.units]){ - sb_json.symbol = window.LENGTH_UNITS[sb.units].symbol; - } - - var sb_html = this.scalebar_template(sb_json); - this.$el.append(sb_html); - } - this.$scalebar = $(".scalebar", this.$el); - - // update scalebar size wrt current sizes - this.render_layout(); - }, - - render: function() { - // This render() is only called when the panel is first created - // to set up the elements. It then calls other render methods to - // set sizes, image src, labels, scalebar etc. - // All subsequent changes to panel attributes are handled directly - // by other appropriate render methods. - - var json = {'randomId': 'r' + Math.random()}; - var html = this.template(json); - this.$el.html(html); - - // cache various elements for later... - this.$img_panel = $(".img_panel", this.$el); - this.$imgContainer = $(".imgContainer", this.$el); - this.$panel_canvas = $(".panel_canvas", this.$el); - - // update src, layout etc. - this.render_image(); - this.render_labels(); - this.render_scalebar(); // also calls render_layout() - - // At this point, element is not ready for Raphael svg - // If we wait a short time, works fine - var self = this; - setTimeout(function(){ - self.render_shapes(); - }, 10); - - return this; - } - }); - - -// -// Copyright (C) 2014 University of Dundee & Open Microscopy Environment. -// All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// - -var RectView = Backbone.View.extend({ - - handle_wh: 6, - default_line_attrs: {'stroke-width':0, 'stroke': '#4b80f9', 'cursor': 'default', 'fill-opacity':0.01, 'fill': '#fff'}, - selected_line_attrs: {'stroke':'#4b80f9', 'stroke-width':2 }, - handle_attrs: {'stroke':'#4b80f9', 'fill':'#fff', 'cursor': 'default', 'fill-opacity':1.0}, - - // make a child on click - events: { - //'mousedown': 'selectShape' // we need to handle this more manually (see below) - }, - initialize: function(options) { - // Here we create the shape itself, the drawing handles and - // bind drag events to all of them to drag/resize the rect. - - var self = this; - this.paper = options.paper; - this.handle_wh = options.handle_wh || this.handle_wh; - this.handles_toFront = options.handles_toFront || false; - this.disable_handles = options.disable_handles || false; - this.fixed_ratio = options.fixed_ratio || false; - // this.manager = options.manager; - - // Set up our 'view' attributes (for rendering without updating model) - this.x = this.model.get("x"); - this.y = this.model.get("y"); - this.width = this.model.get("width"); - this.height = this.model.get("height"); - - // ---- Create Handles ----- - // map of centre-points for each handle - this.handleIds = {'nw': [this.x, this.y], - 'n': [this.x+this.width/2,this.y], - 'ne': [this.x+this.width,this.y], - 'w': [this.x, this.y+this.height/2], - 'e': [this.x+this.width, this.y+this.height/2], - 'sw': [this.x, this.y+this.height], - 's': [this.x+this.width/2, this.y+this.height], - 'se': [this.x+this.width, this.y+this.height] - }; - // draw handles - self.handles = this.paper.set(); - var _handle_drag = function() { - return function (dx, dy, mouseX, mouseY, event) { - if (self.disable_handles) return false; - // on DRAG... - - // If drag on corner handle, retain aspect ratio. dx/dy = aspect - var keep_ratio = self.fixed_ratio || event.shiftKey; - if (keep_ratio && this.h_id.length === 2) { // E.g. handle is corner 'ne' etc - if (this.h_id === 'se' || this.h_id === 'nw') { - if (Math.abs(dx/dy) > this.aspect) { - dy = dx/this.aspect; - } else { - dx = dy*this.aspect; - } - } else { - if (Math.abs(dx/dy) > this.aspect) { - dy = -dx/this.aspect; - } else { - dx = -dy*this.aspect; - } - } - } - // Use dx & dy to update the location of the handle and the corresponding point of the parent - var new_x = this.ox + dx; - var new_y = this.oy + dy; - var newRect = { - x: this.rect.x, - y: this.rect.y, - width: this.rect.width, - height: this.rect.height - }; - if (this.h_id.indexOf('e') > -1) { // if we're dragging an 'EAST' handle, update width - newRect.width = new_x - self.x + self.handle_wh/2; - } - if (this.h_id.indexOf('s') > -1) { // if we're dragging an 'SOUTH' handle, update height - newRect.height = new_y - self.y + self.handle_wh/2; - } - if (this.h_id.indexOf('n') > -1) { // if we're dragging an 'NORTH' handle, update y and height - newRect.y = new_y + self.handle_wh/2; - newRect.height = this.obottom - new_y; - } - if (this.h_id.indexOf('w') > -1) { // if we're dragging an 'WEST' handle, update x and width - newRect.x = new_x + self.handle_wh/2; - newRect.width = this.oright - new_x; - } - // Don't allow zero sized rect. - if (newRect.width < 1 || newRect.height < 1) { - return false; - } - this.rect.x = newRect.x; - this.rect.y = newRect.y; - this.rect.width = newRect.width; - this.rect.height = newRect.height; - this.rect.model.trigger("drag_resize", [this.rect.x, this.rect.y, this.rect.width, this.rect.height]); - this.rect.updateShape(); - return false; - }; - }; - var _handle_drag_start = function() { - return function () { - if (self.disable_handles) return false; - // START drag: simply note the location we started - this.ox = this.attr("x"); - this.oy = this.attr("y"); - this.oright = self.width + this.ox; - this.obottom = self.height + this.oy; - this.aspect = self.model.get('width') / self.model.get('height'); - return false; - }; - }; - var _handle_drag_end = function() { - return function() { - if (self.disable_handles) return false; - this.rect.model.trigger('drag_resize_stop', [this.rect.x, this.rect.y, - this.rect.width, this.rect.height]); - return false; - }; - }; - var _stop_event_propagation = function(e) { - e.stopImmediatePropagation(); - } - for (var key in this.handleIds) { - var hx = this.handleIds[key][0]; - var hy = this.handleIds[key][1]; - var handle = this.paper.rect(hx-self.handle_wh/2, hy-self.handle_wh/2, self.handle_wh, self.handle_wh).attr(self.handle_attrs); - handle.attr({'cursor': key + '-resize'}); // css, E.g. ne-resize - handle.h_id = key; - handle.rect = self; - - handle.drag( - _handle_drag(), - _handle_drag_start(), - _handle_drag_end() - ); - handle.mousedown(_stop_event_propagation); - self.handles.push(handle); - } - self.handles.hide(); // show on selection - - - // ----- Create the rect itself ---- - this.element = this.paper.rect(); - this.element.attr( self.default_line_attrs ); - // set "element" to the raphael node (allows Backbone to handle events) - this.setElement(this.element.node); - this.delegateEvents(this.events); // we need to rebind the events - - // Handle drag - this.element.drag( - function(dx, dy) { - // DRAG, update location and redraw - // TODO - need some way to disable drag if we're not in select state - //if (manager.getState() !== ShapeManager.STATES.SELECT) { - // return; - //} - self.x = dx+this.ox; - self.y = this.oy+dy; - self.dragging = true; - self.model.trigger("drag_xy", [dx, dy]); - self.updateShape(); - return false; - }, - function() { - // START drag: note the location of all points (copy list) - this.ox = this.attr('x'); - this.oy = this.attr('y'); - return false; - }, - function() { - // STOP: save current position to model - self.model.trigger('drag_xy_stop', [self.x-this.ox, self.y-this.oy]); - self.dragging = false; - return false; - } - ); - - // If we're starting DRAG, don't let event propogate up to dragdiv etc. - // https://groups.google.com/forum/?fromgroups=#!topic/raphaeljs/s06GIUCUZLk - this.element.mousedown(function(e){ - e.stopImmediatePropagation(); - self.selectShape(e); - }); - - this.updateShape(); // sync position, selection etc. - - // Finally, we need to render when model changes - this.model.on('change', this.render, this); - this.model.on('destroy', this.destroy, this); - - }, - - // render updates our local attributes from the Model AND updates coordinates - render: function(event) { - if (this.dragging) return; - this.x = this.model.get("x"); - this.y = this.model.get("y"); - this.width = this.model.get("width"); - this.height = this.model.get("height"); - this.updateShape(); - }, - - // used to update during drags etc. Also called by render() - updateShape: function() { - this.element.attr({'x':this.x, 'y':this.y, 'width':this.width, 'height':this.height}); - - // TODO Draw diagonals on init - then simply update here (show if selected) - // var path1 = "M" + this.x +","+ this.y +"l"+ this.width +","+ this.height, - // path2 = "M" + (this.x+this.width) +","+ this.y +"l-"+ this.width +","+ this.height; - // // rectangle plus 2 diagonal lines - // this.paper.path(path1).attr('stroke', '#4b80f9'); - // this.paper.path(path2).attr('stroke', '#4b80f9'); - - // if (this.manager.selected_shape_id === this.model.get("id")) { - if (this.model.get('selected')) { - this.element.attr( this.selected_line_attrs ); //.toFront(); - var self = this; - // If several Rects get selected at the same time, one with handles_toFront will - // end up with the handles at the top - if (this.handles_toFront) { - setTimeout(function(){ - self.handles.show().toFront(); - },50); - } else { - this.handles.show().toFront(); - } - } else { - this.element.attr( this.default_line_attrs ); // this should be the shapes OWN line / fill colour etc. - this.handles.hide(); - } - - this.handleIds = {'nw': [this.x, this.y], - 'n': [this.x+this.width/2,this.y], - 'ne': [this.x+this.width,this.y], - 'w': [this.x, this.y+this.height/2], - 'e': [this.x+this.width, this.y+this.height/2], - 'sw': [this.x, this.y+this.height], - 's': [this.x+this.width/2, this.y+this.height], - 'se': [this.x+this.width, this.y+this.height]}; - var hnd, h_id, hx, hy; - for (var h=0, l=this.handles.length; h 0) { - this.vp = new ImageViewerView({models: selected, figureModel: this.model}); // auto-renders on init - $("#viewportContainer").append(this.vp.el); - } - - if (this.ipv) { - this.ipv.remove(); - } - if (selected.length > 0) { - this.ipv = new InfoPanelView({models: selected, figureModel: this.model}); - this.ipv.render(); - $("#infoTab").append(this.ipv.el); - } - - if (this.ctv) { - this.ctv.clear().remove(); - } - if (selected.length > 0) { - this.ctv = new ImageDisplayOptionsView({models: selected}); - $("#channelToggle").empty().append(this.ctv.render().el); - } - if (this.csv) { - this.csv.clear().remove(); - } - if (selected.length > 0) { - this.csv = new ChannelSliderView({models: selected}); - $("#channel_sliders").empty().append(this.csv.render().el); - } - } - }); - - - var RoisFormView = Backbone.View.extend({ - - model: FigureModel, - - roisTemplate: JST["src/templates/rois_form_template.html"], - - el: $("#labelsTab"), - - initialize: function(opts) { - this.listenTo(this.model, 'change:selection', this.render); - this.listenTo(this.model, 'change:selection', this.addListeners); - this.render(); - }, - - addListeners: function() { - // when selection changes, we need to listen to selected panels - var self = this; - this.model.getSelected().forEach(function(m){ - self.listenTo(m, 'change:shapes', self.render); - }); - }, - - events: { - "click .edit_rois": "editRois", - "click .copyROIs": "copyROIs", - "click .pasteROIs": "pasteROIs", - "click .deleteROIs": "deleteROIs", - // triggered by select_dropdown_option below - "change .shape-color": "changeROIColor", - "change .line-width": "changeLineWidth", - }, - - changeLineWidth: function() { - var width = $('button.line-width span:first', this.$el).attr('data-line-width'), - sel = this.model.getSelected(); - width = parseFloat(width, 10); - - sel.forEach(function(panel){ - panel.setROIStrokeWidth(width); - }); - }, - - changeROIColor: function() { - var color = $('button.shape-color span:first', this.$el).attr('data-color'), - sel = this.model.getSelected(); - - sel.forEach(function(panel){ - panel.setROIColor(color); - }); - }, - - copyROIs: function(event) { - event.preventDefault(); - var sel = this.model.getSelected(), - roiJson = []; - - sel.forEach(function(s){ - var rois = s.get('shapes'); - if (rois) { - rois.forEach(function(r){ - roiJson.push($.extend(true, {}, r)); - }); - } - }); - if (roiJson.length > 0) { - this.model.set('clipboard', {'SHAPES': roiJson}); - } - this.render(); - }, - - rectToPolygon: function(rect, rotation) { - // rotate Rect around centre point - return points "x,y, x,y, x,y, x,y" - let cx = rect.x + (rect.width / 2); - let cy = rect.y + (rect.height / 2); - // topleft - let tl = rotatePoint(rect.x, rect.y, cx, cy, rotation); - // topright - let tr = rotatePoint(rect.x + rect.width, rect.y, cx, cy, rotation); - // bottomright - let br = rotatePoint(rect.x + rect.width, rect.y + rect.height, cx, cy, rotation); - // bottomleft - let bl = rotatePoint(rect.x, rect.y + rect.height, cx, cy, rotation); - return [tl, tr, br, bl].map(point => point.x + ',' + point.y).join(', '); - }, - - pasteROIs: function(event) { - event.preventDefault(); - var sel = this.model.getSelected(), - roiJson = this.model.get('clipboard'), - allOK = true; - if (!roiJson) { - return; - } - // Paste ROIs onto each selected panel... - if (roiJson.SHAPES) { - roiJson = roiJson.SHAPES; - } else if (roiJson.CROP) { - // Need to create Rectangle with current color & line width - var color = $('button.shape-color span:first', this.$el).attr('data-color'), - width = parseFloat($('button.line-width span:first', this.$el).attr('data-line-width')), - rect = roiJson.CROP; - // If rotated, need to create a Polygon since Rectangle doesn't support rotation - if (rect.rotation && !isNaN(rect.rotation)) { - // rotate around centre point - var points = this.rectToPolygon(rect, -rect.rotation); - roiJson = [{ - type: "Polygon", - strokeColor: "#" + color, - points: points, - strokeWidth: width - }] - } else { - roiJson = [{type: "Rectangle", - x: rect.x, - y: rect.y, - width: rect.width, - height: rect.height, - strokeColor: "#" + color, - strokeWidth: width}]; - } - } else { - return; - } - sel.forEach(function(p){ - var ok = p.add_shapes(roiJson); - if (!ok) {allOK = false;} - }); - // If any shapes were outside viewport, show message - var plural = sel.length > 1 ? "s" : ""; - if (!allOK) { - figureConfirmDialog("Paste Failure", - "Some shapes may be outside the visible 'viewport' of panel" + plural + ". " + - "Target image" + plural + " may be too small or zoomed in too much. " + - "Try zooming out before pasting again, or paste to a bigger image.", - ["OK"]); - } - this.render(); - }, - - deleteROIs: function(event) { - event.preventDefault(); - var sel = this.model.getSelected(); - sel.forEach(function(p){ - var ok = p.unset('shapes'); - }); - this.render(); - }, - - editRois: function(event) { - $("#roiModal").modal("show"); - return false; - }, - - render: function() { - - var sel = this.model.getSelected(), - panelCount = this.model.getSelected().length, - roiCount = 0, - clipboard_data = this.model.get('clipboard'), - canPaste = clipboard_data && ('SHAPES' in clipboard_data || 'CROP' in clipboard_data), - color, - width; - - sel.forEach(function(panel){ - var rois = panel.get('shapes'); - if (rois) { - roiCount += rois.length; - // color & width are false unless all rois are same - rois.forEach(function(r){ - if (color === undefined) { - color = r.strokeColor; - } else { - if (color != r.strokeColor) { - color = false; - } - } - if (width === undefined) { - width = r.strokeWidth; - } else { - if (width != r.strokeWidth) { - width = false; - } - } - }); - } - }); - - var json = { - 'panelCount': panelCount, - 'color': color ? color.replace('#', '') : 'FFFFFF', - 'lineWidth': width || 2, - 'roiCount': roiCount, - 'canPaste': canPaste, - } - $('#edit_rois_form').html(this.roisTemplate(json)); - }, - - }); - - - var LabelsPanelView = Backbone.View.extend({ - - model: FigureModel, - - template: JST["src/templates/labels_form_inner_template.html"], - - el: $("#labelsTab"), - - initialize: function(opts) { - this.listenTo(this.model, 'change:selection', this.render); - - // one-off build 'New Label' form, with same template as used for 'Edit Label' forms - var json = {'l': {'text':'', 'size':12, 'color':'000000'}, 'position':'top', 'edit':false}; - $('.new-label-form', this.$el).html(this.template(json)); - $('.btn-sm').tooltip({container: 'body', placement:'bottom', toggle:"tooltip"}); - - this.render(); - }, - - events: { - "submit .new-label-form": "handle_new_label", - "click .dropdown-menu a": "select_dropdown_option", - "click .markdown-info": "markdownInfo", - }, - - markdownInfo: function(event) { - event.preventDefault(); - $("#markdownInfoModal").modal('show'); - }, - - // Handles all the various drop-down menus in the 'New' AND 'Edit Label' forms - // AND for ROI form (since this is also under the #labelsTab) - select_dropdown_option: function(event) { - event.preventDefault(); - var $a = $(event.target), - $span = $a.children('span'); - // For the Label Text, handle this differently... - if ($a.attr('data-label')) { - $('.new-label-form .label-text', this.$el).val( $a.attr('data-label') ); - return; - } - // All others, we take the from the and place it in the - - - -
    ` - } else { - html += ``; - } - $("#roiPageControls").html(html).show(); - }, - - render: function() { - - var maxSize = 550, - frame_w = maxSize, - frame_h = maxSize, - wh = this.m.get('width') / this.m.get('height'); - if (wh <= 1) { - frame_h = maxSize; - frame_w = maxSize * wh; - } else { - frame_w = maxSize; - frame_h = maxSize / wh; - } - - // Get css for the image plane - var css = this.m.get_vp_img_css(this.m.get('zoom'), frame_w, frame_h); - this.$roiImg.css(css); - - // Get css for the SVG (full plane) - var svg_css = this.m.get_vp_full_plane_css(this.m.get('zoom'), frame_w, frame_h); - var w = this.m.get('orig_width'), - h = this.m.get('orig_height'); - var scale = svg_css.width / w; - // TODO: add public methods to set w & h - this.shapeManager._orig_width = w; - this.shapeManager._orig_height = h; - this.shapeManager.setZoom(scale * 100); - $("#roi_paper").css(svg_css); - - $("#roiViewer").css({'width': frame_w + 'px', 'height': frame_h + 'px'}); - - this.renderImagePlane(); - this.renderToolbar(); - this.renderSidebar(); - } - }); - - -// Created new for each selection change -var ScalebarFormView = Backbone.View.extend({ - - template: JST["src/templates/scalebar_form_template.html"], - - initialize: function(opts) { - - // prevent rapid repetative rendering, when listening to multiple panels - this.render = _.debounce(this.render); - - this.models = opts.models; - var self = this; - - this.models.forEach(function(m){ - self.listenTo(m, 'change:scalebar change:pixel_size_x change:scalebar_label', self.render); - }); - - // this.$el = $("#scalebar_form"); - }, - - events: { - "submit .scalebar_form": "update_scalebar", - "click .scalebar_label": "update_scalebar", - "change .btn": "dropdown_btn_changed", - "click .hide_scalebar": "hide_scalebar", - "click .pixel_size_display": "edit_pixel_size", - "keypress .pixel_size_input" : "enter_pixel_size", - "blur .pixel_size_input" : "save_pixel_size", - "keyup input[type='text']" : "handle_keyup", - }, - - handle_keyup: function (event) { - // If Enter key - submit form... - if (event.which == 13) { - this.update_scalebar(); - } - }, - - // simply show / hide editing field - edit_pixel_size: function() { - $('.pixel_size_display', this.$el).hide(); - $(".pixel_size_input", this.$el).css('display','inline-block').focus(); - }, - done_pixel_size: function() { - $('.pixel_size_display', this.$el).show(); - $(".pixel_size_input", this.$el).css('display','none').focus(); - }, - - // If you hit `enter`, set pixel_size - enter_pixel_size: function(e) { - if (e.keyCode == 13) { - this.save_pixel_size(e); - } - }, - - // on 'blur' or 'enter' we save... - save_pixel_size: function(e) { - // save will re-render, but only if number has changed - in case not... - this.done_pixel_size(); - - var val = $(e.target).val(); - if (val.length === 0) return; - var pixel_size = parseFloat(val); - if (isNaN(pixel_size)) return; - this.models.forEach(function(m){ - m.save('pixel_size_x', pixel_size); - }); - }, - - // Automatically submit the form when a dropdown is changed - dropdown_btn_changed: function(event) { - $(event.target).closest('form').submit(); - }, - - hide_scalebar: function() { - this.models.forEach(function(m){ - m.hide_scalebar(); - }); - }, - - // called when form changes - update_scalebar: function(event) { - - var $form = $('.scalebar_form '); - - var length = $('.scalebar-length', $form).val(), - units = $('.scalebar-units span:first', $form).attr('data-unit'), - position = $('.label-position span:first', $form).attr('data-position'), - color = $('.label-color span:first', $form).attr('data-color'), - show_label = $('.scalebar_label', $form).prop('checked'), - font_size = $('.scalebar_font_size span:first', $form).text().trim(), - height = parseInt($('.scalebar-height', $form).val()); - - this.models.forEach(function(m){ - var old_sb = m.get('scalebar'); - var sb = {show: true}; - if (length != '-') sb.length = parseFloat(length, 10); - if (units != '-') { - sb.units = units; - } else { - // various images have different units - // keep existing scalebar units OR use image pixel_size units - if (old_sb && old_sb.units) { - sb.units = old_sb.units; - } else if (m.get('pixel_size_x_unit')) { - sb.units = m.get('pixel_size_x_unit'); - } else { - sb.units = "MICROMETER"; - } - } - if (position != '-') sb.position = position; - if (color != '-') sb.color = color; - sb.show_label = show_label; - if (font_size != '-') sb.font_size = font_size; - if (height != '-') sb.height = height; - - m.save_scalebar(sb); - }); - return false; - }, - - render: function() { - var json = {show: false, show_label: true}, - hidden = false, - sb; - - // Turn dict into list of units we can sort by size - var scalebarUnits = ["PICOMETER", "ANGSTROM", "NANOMETER", "MICROMETER", - "MILLIMETER", "CENTIMETER", "METER", "KILOMETER", "MEGAMETER"] - var unit_symbols = Object.keys(window.LENGTH_UNITS) - .filter(function(unit){ - return (scalebarUnits.indexOf(unit) > -1); - }) - .map(function(unit){ - return $.extend({unit: unit}, window.LENGTH_UNITS[unit]); - }); - unit_symbols.sort(function(a, b){ - return a.microns > b.microns ? 1 : -1; - }) - json.unit_symbols = unit_symbols; - - this.models.forEach(function(m){ - // start with json data from first Panel - if (!json.pixel_size_x) { - json.pixel_size_x = m.get('pixel_size_x'); - json.pixel_size_symbol = m.get('pixel_size_x_symbol'); - json.pixel_size_unit = m.get('pixel_size_x_unit'); - } else { - pix_sze = m.get('pixel_size_x'); - // account for floating point imprecision when comparing - if (json.pixel_size_x != '-' && - json.pixel_size_x.toFixed(10) != pix_sze.toFixed(10)) { - json.pixel_size_x = '-'; - } - if (json.pixel_size_symbol != m.get('pixel_size_x_symbol')) { - json.pixel_size_symbol = '-'; - } - if (json.pixel_size_unit != m.get('pixel_size_x_unit')) { - json.pixel_size_unit = '-'; - } - } - sb = m.get('scalebar'); - // if panel has scalebar, combine into json - if (sb) { - // for first panel, json = sb - if (!json.length) { - json.length = sb.length; - json.units = sb.units; - json.position = sb.position; - json.color = sb.color; - json.show_label = sb.show_label; - json.font_size = sb.font_size; - json.height = sb.height; - } - else { - // combine attributes. Use '-' if different values found - if (json.length != sb.length) json.length = '-'; - if (json.units != sb.units) json.units = '-'; - if (json.position != sb.position) json.position = '-'; - if (json.color != sb.color) json.color = '-'; - if (!sb.show_label) json.show_label = false; - if (json.font_size != sb.font_size) json.font_size = '-'; - if (json.height != sb.height) json.height = '-'; - } - } - // if any panels don't have scalebar - we allow to add - if(!sb || !sb.show) hidden = true; - }); - - if (this.models.length === 0 || hidden) { - json.show = true; - } - json.length = json.length || 10; - // If no units chosen, use pixel size units - json.units = json.units || json.pixel_size_unit; - json.units_symbol = '-'; - if (json.units !== '-') { - // find the symbol e.g. 'mm' from units 'MILLIMETER' - json.units_symbol = LENGTH_UNITS[json.units].symbol; - } - json.position = json.position || 'bottomright'; - json.color = json.color || 'FFFFFF'; - json.font_size = json.font_size || 10; - json.pixel_size_symbol = json.pixel_size_symbol || '-'; - json.height = json.height || 3; - - var html = this.template(json); - this.$el.html(html); - this.$el.find("[title]").tooltip(); - - return this; - } -}); - - - // -------------- Selection Overlay Views ---------------------- - - - // SvgView uses ProxyRectModel to manage Svg Rects (raphael) - // This converts between zoomed coordiantes of the html DOM panels - // and the unzoomed SVG overlay. - // Attributes of this model apply to the SVG canvas and are updated from - // the PanelModel. - // The SVG RectView (Raphael) notifies this Model via trigger 'drag' & 'dragStop' - // and this is delegated to the PanelModel via trigger or set respectively. - - // Used by a couple of different models below - var getModelCoords = function(coords) { - var zoom = this.figureModel.get('curr_zoom') * 0.01, - size = this.figureModel.getFigureSize(), - paper_top = (this.figureModel.get('canvas_height') - size.h)/2, - paper_left = (this.figureModel.get('canvas_width') - size.w)/2, - x = (coords.x/zoom) - paper_left - 1, - y = (coords.y/zoom) - paper_top - 1, - w = coords.width/zoom, - h = coords.height/zoom; - return {'x':x>>0, 'y':y>>0, 'width':w>>0, 'height':h>>0}; - }; - - var ProxyRectModel = Backbone.Model.extend({ - - initialize: function(opts) { - this.panelModel = opts.panel; // ref to the genuine PanelModel - this.figureModel = opts.figure; - - this.renderFromModel(); - - // Refresh c - this.listenTo(this.figureModel, 'change:curr_zoom change:paper_width change:paper_height change:page_count', this.renderFromModel); - this.listenTo(this.panelModel, 'change:x change:y change:width change:height', this.renderFromModel); - // when PanelModel is being dragged, but NOT by this ProxyRectModel... - this.listenTo(this.panelModel, 'drag_resize', this.renderFromTrigger); - this.listenTo(this.panelModel, 'change:selected', this.renderSelection); - this.panelModel.on('destroy', this.clear, this); - // listen to a trigger on this Model (triggered from Rect) - this.listenTo(this, 'drag_xy', this.drag_xy); - this.listenTo(this, 'drag_xy_stop', this.drag_xy_stop); - this.listenTo(this, 'drag_resize', this.drag_resize); - // listen to change to this model - update PanelModel - this.listenTo(this, 'drag_resize_stop', this.drag_resize_stop); - - // reduce coupling between this and rect by using triggers to handle click. - this.bind('clicked', function(args) { - this.handleClick(args[0]); - }); - }, - - // return the SVG x, y, w, h (converting from figureModel) - getSvgCoords: function(coords) { - var zoom = this.figureModel.get('curr_zoom') * 0.01, - size = this.figureModel.getFigureSize(), - paper_top = (this.figureModel.get('canvas_height') - size.h)/2, - paper_left = (this.figureModel.get('canvas_width') - size.w)/2, - rect_x = (paper_left + 1 + coords.x) * zoom, - rect_y = (paper_top + 1 + coords.y) * zoom, - rect_w = coords.width * zoom, - rect_h = coords.height * zoom; - return {'x':rect_x, 'y':rect_y, 'width':rect_w, 'height':rect_h}; - }, - - // return the Model x, y, w, h (converting from SVG coords) - getModelCoords: getModelCoords, - - // called on trigger from the RectView, on drag of the whole rect OR handle for resize. - // we simply convert coordinates and delegate to figureModel - drag_xy: function(xy, save) { - var zoom = this.figureModel.get('curr_zoom') * 0.01, - dx = xy[0]/zoom, - dy = xy[1]/zoom; - - this.figureModel.drag_xy(dx, dy, save); - }, - - // As above, but this time we're saving the changes to the Model - drag_xy_stop: function(xy) { - this.drag_xy(xy, true); - }, - - // Called on trigger from the RectView on resize. - // Need to convert from Svg coords to Model and notify the PanelModel without saving. - drag_resize: function(xywh) { - var coords = this.getModelCoords({'x':xywh[0], 'y':xywh[1], 'width':xywh[2], 'height':xywh[3]}); - this.panelModel.drag_resize(coords.x, coords.y, coords.width, coords.height); - }, - - // As above, but need to update the Model on changes to Rect (drag stop etc) - drag_resize_stop: function(xywh) { - var coords = this.getModelCoords({'x':xywh[0], 'y':xywh[1], 'width':xywh[2], 'height':xywh[3]}); - this.panelModel.save(coords); - }, - - // Called when the FigureModel zooms or the PanelModel changes coords. - // Refreshes the RectView since that listens to changes in this ProxyModel - renderFromModel: function() { - this.set( this.getSvgCoords({ - 'x': this.panelModel.get('x'), - 'y': this.panelModel.get('y'), - 'width': this.panelModel.get('width'), - 'height': this.panelModel.get('height') - }) ); - }, - - // While the Panel is being dragged (by the multi-select Rect), we need to keep updating - // from the 'multiselectDrag' trigger on the model. RectView renders on change - renderFromTrigger:function(xywh) { - var c = this.getSvgCoords({ - 'x': xywh[0], - 'y': xywh[1], - 'width': xywh[2], - 'height': xywh[3] - }); - this.set( this.getSvgCoords({ - 'x': xywh[0], - 'y': xywh[1], - 'width': xywh[2], - 'height': xywh[3] - }) ); - }, - - // When PanelModel changes selection - update and RectView will render change - renderSelection: function() { - this.set('selected', this.panelModel.get('selected')); - }, - - // Handle click (mousedown) on the RectView - changing selection. - handleClick: function(event) { - if (event.shiftKey) { - this.figureModel.addSelected(this.panelModel); - } else { - this.figureModel.setSelected(this.panelModel); - } - }, - - clear: function() { - this.destroy(); - } - - }); - - - // This model underlies the Rect that is drawn around multi-selected panels - // (only shown if 2 or more panels selected) - // On drag or resize, we calculate how to move or resize the seleted panels. - var MultiSelectRectModel = ProxyRectModel.extend({ - - defaults: { - x: 0, - y: 0, - width: 0, - height: 0 - }, - - initialize: function(opts) { - this.figureModel = opts.figureModel; - - // listen to a trigger on this Model (triggered from Rect) - this.listenTo(this, 'drag_xy', this.drag_xy); - this.listenTo(this, 'drag_xy_stop', this.drag_xy_stop); - this.listenTo(this, 'drag_resize', this.drag_resize); - this.listenTo(this, 'drag_resize_stop', this.drag_resize_stop); - this.listenTo(this.figureModel, 'change:selection', this.updateSelection); - this.listenTo(this.figureModel, 'change:curr_zoom change:paper_height change:paper_width', - this.updateSelection); - - // also listen for drag_xy coming from a selected panel - this.listenTo(this.figureModel, 'drag_xy', this.update_xy); - }, - - - // Need to re-draw on selection AND zoom changes - updateSelection: function() { - - var selected = this.figureModel.getSelected(); - if (selected.length < 1){ - - this.set({ - 'x': 0, - 'y': 0, - 'width': 0, - 'height': 0, - 'selected': false - }); - return; - } - - var max_x = 0, - max_y = 0; - - selected.forEach(function(panel){ - var x = panel.get('x'), - y = panel.get('y'), - w = panel.get('width'), - h = panel.get('height'); - max_x = Math.max(max_x, x+w); - max_y = Math.max(max_y, y+h); - }); - - min_x = selected.getMin('x'); - min_y = selected.getMin('y'); - - - - this.set( this.getSvgCoords({ - 'x': min_x, - 'y': min_y, - 'width': max_x - min_x, - 'height': max_y - min_y - }) ); - - // Rect SVG will be notified and re-render - this.set('selected', true); - }, - - - // Called when we are notified of drag_xy on one of the Panels - update_xy: function(dxdy) { - if (! this.get('selected')) return; // if we're not visible, ignore - - var svgCoords = this.getSvgCoords({ - 'x': dxdy[0], - 'y': dxdy[1], - 'width': 0, - 'height': 0, - }); - this.set({'x':svgCoords.x, 'y':svgCoords.y}); - }, - - // RectView drag is delegated to Panels to update coords (don't save) - drag_xy: function(dxdy, save) { - // we just get [x,y] but we need [x,y,w,h]... - var x = dxdy[0] + this.get('x'), - y = dxdy[1] + this.get('y'); - var xywh = [x, y, this.get('width'), this.get('height')]; - this.notifyModelofDrag(xywh, save); - }, - - // As above, but Save is true since we're done dragging - drag_xy_stop: function(dxdy, save) { - this.drag_xy(dxdy, true); - // Have to keep our proxy model in sync - this.set({ - 'x': dxdy[0] + this.get('x'), - 'y': dxdy[1] + this.get('y') - }); - }, - - // While the multi-select RectView is being dragged, we need to calculate the new coords - // of all selected Panels, based on the start-coords and the current coords of - // the multi-select Rect. - drag_resize: function(xywh, save) { - this.notifyModelofDrag(xywh, save); - }, - - // RectView dragStop is delegated to Panels to update coords (with save 'true') - drag_resize_stop: function(xywh) { - this.notifyModelofDrag(xywh, true); - - this.set({ - 'x': xywh[0], - 'y': xywh[1], - 'width': xywh[2], - 'height': xywh[3] - }); - }, - - // While the multi-select RectView is being dragged, we need to calculate the new coords - // of all selected Panels, based on the start-coords and the current coords of - // the multi-select Rect. - notifyModelofDrag: function(xywh, save) { - var startCoords = this.getModelCoords({ - 'x': this.get('x'), - 'y': this.get('y'), - 'width': this.get('width'), - 'height': this.get('height') - }); - var dragCoords = this.getModelCoords({ - 'x': xywh[0], - 'y': xywh[1], - 'width': xywh[2], - 'height': xywh[3] - }); - - // var selected = this.figureModel.getSelected(); - // for (var i=0; i canvas - this.raphael_paper = Raphael("canvas_wrapper", canvas_width, canvas_height); - - // this.panelRects = new ProxyRectModelList(); - self.$dragOutline = $("
    ") - .appendTo("#canvas_wrapper"); - self.outlineStyle = self.$dragOutline.get(0).style; - - - // Add global mouse event handlers - self.dragging = false; - self.drag_start_x = 0; - self.drag_start_y = 0; - $("#canvas_wrapper>svg") - .mousedown(function(event){ - self.dragging = true; - var parentOffset = $(this).parent().offset(); - //or $(this).offset(); if you really just want the current element's offset - self.left = self.drag_start_x = event.pageX - parentOffset.left; - self.top = self.drag_start_y = event.pageY - parentOffset.top; - self.dx = 0; - self.dy = 0; - self.$dragOutline.css({ - 'left': self.drag_start_x, - 'top': self.drag_start_y, - 'width': 0, - 'height': 0 - }).show(); - // return false; - }) - .mousemove(function(event){ - if (self.dragging) { - var parentOffset = $(this).parent().offset(); - //or $(this).offset(); if you really just want the current element's offset - self.left = self.drag_start_x; - self.top = self.drag_start_y; - self.dx = event.pageX - parentOffset.left - self.drag_start_x; - self.dy = event.pageY - parentOffset.top - self.drag_start_y; - if (self.dx < 0) { - self.left = self.left + self.dx; - self.dx = Math.abs(self.dx); - } - if (self.dy < 0) { - self.top = self.top + self.dy; - self.dy = Math.abs(self.dy); - } - self.$dragOutline.css({ - 'left': self.left, - 'top': self.top, - 'width': self.dx, - 'height': self.dy - }); - // .show(); - // self.outlineStyle.left = left + 'px'; - // self.outlineStyle.top = top + 'px'; - // self.outlineStyle.width = dx + 'px'; - // self.outlineStyle.height = dy + 'px'; - } - // return false; - }) - .mouseup(function(event){ - if (self.dragging) { - self.handleClick(event); - self.$dragOutline.hide(); - } - self.dragging = false; - // return false; - }); - - // If a panel is added... - this.model.panels.on("add", this.addOne, this); - this.listenTo(this.model, 'change:curr_zoom', this.renderZoom); - - var multiSelectRect = new MultiSelectRectModel({figureModel: this.model}), - rv = new RectView({'model':multiSelectRect, 'paper':this.raphael_paper, - 'handle_wh':7, 'handles_toFront': true, 'fixed_ratio': true}); - rv.selected_line_attrs = {'stroke-width': 1, 'stroke':'#4b80f9'}; - - // set svg size for current window and zoom - this.renderZoom(); - }, - - // A panel has been added - We add a corresponding Raphael Rect - addOne: function(m) { - - var rectModel = new ProxyRectModel({panel: m, figure:this.model}); - new RectView({'model':rectModel, 'paper':this.raphael_paper, - 'handle_wh':5, 'disable_handles': true, 'fixed_ratio': true}); - }, - - // TODO - remove: function() { - // TODO: remove from svg, remove event handlers etc. - }, - - // We simply re-size the Raphael svg itself - Shapes have their own zoom listeners - renderZoom: function() { - var zoom = this.model.get('curr_zoom') * 0.01, - newWidth = this.model.get('canvas_width') * zoom, - newHeight = this.model.get('canvas_height') * zoom; - - this.raphael_paper.setSize(newWidth, newHeight); - }, - - getModelCoords: getModelCoords, - - // Any mouse click (mouseup) or dragStop that isn't captured by Panel Rect clears selection - handleClick: function(event) { - if (!event.shiftKey) { - this.model.clearSelected(); - } - // select panels overlapping with drag outline - if (this.dx > 0 || this.dy > 0) { - var coords = this.getModelCoords({x: this.left, y: this.top, width:this.dx, height:this.dy}); - this.model.selectByRegion(coords); - } - } - }); - - -// -// Copyright (C) 2014 University of Dundee & Open Microscopy Environment. -// All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// - -// http://www.sitepoint.com/javascript-json-serialization/ -JSON.stringify = JSON.stringify || function (obj) { - var t = typeof (obj); - if (t != "object" || obj === null) { - // simple data type - if (t == "string") obj = '"'+obj+'"'; - return String(obj); - } - else { - // recurse array or object - var n, v, json = [], arr = (obj && obj.constructor == Array); - for (n in obj) { - v = obj[n]; t = typeof(v); - if (t == "string") v = '"'+v+'"'; - else if (t == "object" && v !== null) v = JSON.stringify(v); - json.push((arr ? "" : '"' + n + '":') + String(v)); - } - return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}"); - } -}; - - -// Polyfill for IE -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith -if (!String.prototype.endsWith) - String.prototype.endsWith = function(searchStr, Position) { - // This works much better than >= because - // it compensates for NaN: - if (!(Position < this.length)) - Position = this.length; - else - Position |= 0; // round position - return this.substr(Position - searchStr.length, - searchStr.length) === searchStr; - }; - -var showExportAsJsonModal = function(figureJSON) { - var figureText = JSON.stringify(figureJSON); - $('#exportJsonModal').modal('show'); - $('#exportJsonModal textarea').text(figureText); -} - -var saveFigureToStorage = function (figureJSON) { - window.sessionStorage.setItem(LOCAL_STORAGE_RECOVERED_FIGURE, JSON.stringify(figureJSON)); -} - -var clearFigureFromStorage = function() { - window.sessionStorage.removeItem(LOCAL_STORAGE_RECOVERED_FIGURE); -} - -var recoverFigureFromStorage = function() { - var storage = window.sessionStorage; - var recoveredFigure = storage.getItem(LOCAL_STORAGE_RECOVERED_FIGURE); - var figureObject; - try { - figureObject = JSON.parse(recoveredFigure); - } catch (e) { - console.log("recovered Figure not valid JSON " + recoveredFigure); - } - return figureObject; -} - -var figureConfirmDialog = function(title, message, buttons, callback) { - var $confirmModal = $("#confirmModal"), - $title = $(".modal-title", $confirmModal), - $body = $(".modal-body", $confirmModal), - $footer = $(".modal-footer", $confirmModal), - $btn = $(".btn:first", $footer); - - // Update modal with params - $title.html(title); - $body.html('

    ' + message + '

    '); - $footer.empty(); - _.each(buttons, function(txt){ - $btn.clone().text(txt).appendTo($footer); - }); - $(".btn", $footer).removeClass('btn-primary') - .addClass('btn-default') - .last() - .removeClass('btn-default') - .addClass('btn-primary'); - - // show modal - $confirmModal.modal('show'); - - // default handler for 'cancel' or 'close' - $confirmModal.one('hide.bs.modal', function() { - // remove the other 'one' handler below - $("#confirmModal .modal-footer .btn").off('click'); - if (callback) { - callback(); - } - }); - - // handle 'Save' btn click. - $("#confirmModal .modal-footer .btn").one('click', function(event) { - // remove the default 'one' handler above - $confirmModal.off('hide.bs.modal'); - var btnText = $(event.target).text(); - if (callback) { - callback(btnText); - } - }); -}; - -if (OME === undefined) { - var OME = {}; -} - -OPEN_WITH = []; - -OME.setOpenWithEnabledHandler = function(id, fn) { - // look for id in OPEN_WITH - OPEN_WITH.forEach(function(ow){ - if (ow.id === id) { - ow.isEnabled = function() { - // wrap fn with try/catch, since error here will break jsTree menu - var args = Array.from(arguments); - var enabled = false; - try { - enabled = fn.apply(this, args); - } catch (e) { - // Give user a clue as to what went wrong - console.log("Open with " + label + ": " + e); - } - return enabled; - } - } - }); -}; - -// Helper can be used by 'open with' plugins to provide -// a url for the selected objects -OME.setOpenWithUrlProvider = function(id, fn) { - // look for id in OPEN_WITH - OPEN_WITH.forEach(function(ow){ - if (ow.id === id) { - ow.getUrl = fn; - } - }); -}; - - -// Extend the jQuery UI $.slider() function to silence -// keyboard events on the handle, so we don't nudge selected panels -$.prototype.slider_old = $.prototype.slider; -$.prototype.slider = function() { - var result = $.prototype.slider_old.apply(this, arguments); - this.find(".ui-slider-handle").bind("keydown", function(){ - return false; - }); - return result; -} - - -// Get coordinates for point x, y rotated around cx, cy, by rotation degrees -var rotatePoint = function (x, y, cx, cy, rotation) { - let length = Math.sqrt(Math.pow((x - cx), 2) + Math.pow((y - cy), 2)); - let rot = Math.atan2((y - cy), (x - cx)); - rot = rot + (rotation * (Math.PI / 180)); // degrees to rad - let dx = Math.cos(rot) * length; - let dy = Math.sin(rot) * length; - return { x: cx + dx, y: cy + dy }; -} - -$(function(){ - - - $(".draggable-dialog").draggable(); - - $('#previewInfoTabs a').click(function (e) { - e.preventDefault(); - $(this).tab('show'); - }); - - - // Header button tooltips - $('.btn-sm').tooltip({container: 'body', placement:'bottom', toggle:"tooltip"}); - $('.figure-title').tooltip({container: 'body', placement:'bottom', toggle:"tooltip"}); - // Footer button tooltips - $('.btn-xs').tooltip({container: 'body', placement:'top', toggle:"tooltip"}); - - - // If we're on Mac, update dropdown menus for keyboard short cuts: - if (navigator.platform.toUpperCase().indexOf('MAC') > -1) { - $("ul.dropdown-menu li a span").each(function(){ - var $this = $(this); - $this.text($this.text().replace("Ctrl+", "⌘")); - }); - } - - // When we load, setup Open With options - $.getJSON(WEBGATEWAYINDEX + "open_with/", function(data){ - if (data && data.open_with_options) { - OPEN_WITH = data.open_with_options; - // Try to load scripts if specified: - OPEN_WITH.forEach(function(ow){ - if (ow.script_url) { - $.getScript(ow.script_url); - } - }) - } - }); - -}); - -// -// Copyright (C) 2014 University of Dundee & Open Microscopy Environment. -// All rights reserved. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// - -$(function(){ - - window.figureModel = new FigureModel(); - - window.FigureColorPicker = new ColorPickerView(); - window.FigureLutPicker = new LutPickerView(); - - // Override 'Backbone.sync'... - Backbone.ajaxSync = Backbone.sync; - - // TODO: - Use the undo/redo queue instead of sync to trigger figureModel.set("unsaved", true); - - // If syncOverride, then instead of actually trying to Save via ajax on model.save(attr, value) - // We simply set the 'unsaved' flag on the figureModel. - // This works for FigureModel and also for Panels collection. - Backbone.getSyncMethod = function(model) { - if(model.syncOverride || (model.collection && model.collection.syncOverride)) - { - return function(method, model, options, error) { - figureModel.set("unsaved", true); - }; - } - return Backbone.ajaxSync; - }; - - // Override 'Backbone.sync' to default to localSync, - // the original 'Backbone.sync' is still available in 'Backbone.ajaxSync' - Backbone.sync = function(method, model, options, error) { - return Backbone.getSyncMethod(model).apply(this, [method, model, options, error]); - }; - - - var view = new FigureView( {model: figureModel}); // uiState: uiState - var svgView = new SvgView( {model: figureModel}); - new RightPanelView({model: figureModel}); - - - // Undo Model and View - var undoManager = new UndoManager({'figureModel':figureModel}), - undoView = new UndoView({model:undoManager}); - // Finally, start listening for changes to panels - undoManager.listenToCollection(figureModel.panels); - - - var FigureRouter = Backbone.Router.extend({ - - routes: { - "": "index", - "new(/)": "newFigure", - "recover(/)": "recoverFigure", - "open(/)": "openFigure", - "file/:id(/)": "loadFigure", - }, - - checkSaveAndClear: function(callback) { - - var doClear = function() { - figureModel.clearFigure(); - if (callback) { - callback(); - } - }; - if (figureModel.get("unsaved")) { - - var saveBtnTxt = "Save", - canEdit = figureModel.get('canEdit'); - if (!canEdit) saveBtnTxt = "Save a Copy"; - // show the confirm dialog... - figureConfirmDialog("Save Changes to Figure?", - "Your changes will be lost if you don't save them", - ["Don't Save", saveBtnTxt], - function(btnTxt){ - if (btnTxt === saveBtnTxt) { - var options = {}; - // Save current figure or New figure... - var fileId = figureModel.get('fileId'); - if (fileId && canEdit) { - options.fileId = fileId; - } else { - var defaultName = figureModel.getDefaultFigureName(); - var figureName = prompt("Enter Figure Name", defaultName); - options.figureName = figureName || defaultName; - } - options.success = doClear; - figureModel.save_to_OMERO(options); - } else if (btnTxt === "Don't Save") { - figureModel.set("unsaved", false); - doClear(); - } else { - doClear(); - } - }); - } else { - doClear(); - } - }, - - index: function() { - $(".modal").modal('hide'); // hide any existing dialogs - var cb = function() { - $('#welcomeModal').modal(); - }; - this.checkSaveAndClear(cb); - }, - - openFigure: function() { - $(".modal").modal('hide'); // hide any existing dialogs - var cb = function() { - $("#openFigureModal").modal(); - }; - this.checkSaveAndClear(cb); - }, - - recoverFigure: function() { - $(".modal").modal('hide'); // hide any existing dialogs - figureModel.recoverFromLocalStorage(); - }, - - newFigure: function() { - $(".modal").modal('hide'); // hide any existing dialogs - var cb = function() { - $('#addImagesModal').modal(); - }; - // Check for ?image=1&image=2 - if (window.location.search.length > 1) { - var params = window.location.search.substring(1).split('&'); - var iids = params.reduce(function(prev, param){ - if (param.split('=')[0] === 'image') { - prev.push(param.split('=')[1]); - } - return prev; - },[]); - if (iids.length > 0) { - cb = function() { - figureModel.addImages(iids); - } - } - } - this.checkSaveAndClear(cb); - }, - - loadFigure: function(id) { - $(".modal").modal('hide'); // hide any existing dialogs - var fileId = parseInt(id, 10); - var cb = function() { - figureModel.load_from_OMERO(fileId); - }; - this.checkSaveAndClear(cb); - } - }); - - app = new FigureRouter(); - Backbone.history.start({pushState: true, root: BASE_WEBFIGURE_URL}); - - // We want 'a' links (E.g. to open_figure) to use app.navigate - $(document).on('click', 'a', function (ev) { - var href = $(this).attr('href'); - // check that links are 'internal' to this app - if (href.substring(0, BASE_WEBFIGURE_URL.length) === BASE_WEBFIGURE_URL) { - ev.preventDefault(); - href = href.replace(BASE_WEBFIGURE_URL, "/"); - app.navigate(href, {trigger: true}); - } - }); - -}); diff --git a/omero_figure/static/figure/templates.js b/omero_figure/static/figure/templates.js deleted file mode 100644 index 1f01421eb..000000000 --- a/omero_figure/static/figure/templates.js +++ /dev/null @@ -1,1048 +0,0 @@ -this["JST"] = this["JST"] || {}; - -this["JST"]["src/templates/channel_slider_template.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -with (obj) { -__p += '\r\n

    \r\n\r\n\r\n
    \r\n \r\n \r\n \r\n

    \r\n\r\n \r\n \r\n \r\n
    \r\n
    \r\n \r\n \r\n \r\n
    \r\n'; - -} -return __p -}; - -this["JST"]["src/templates/figure_panel_template.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = ''; -with (obj) { -__p += ' \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n'; - -} -return __p -}; - -this["JST"]["src/templates/image_display_options_template.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -with (obj) { -__p += '\r\n
    \r\n \r\n\r\n
    \r\n
    \r\n\r\n\r\n
    \r\n
    \r\n\r\n
    Z-sections:
    \r\n
    ' + -((__t = ( sizeZ )) == null ? '' : __t) + -'
    \r\n
    \r\n
    Timepoints:
    \r\n
    ' + -((__t = ( sizeT )) == null ? '' : __t) + -'
    \r\n
    \r\n
    Channels:
    \r\n
    \r\n '; - _.each(channel_labels, function(c, i) { - print(_.escape(c)); print((i < channels.length-1) ? ", " : ""); - }); ; -__p += '\r\n
    \r\n \r\n \r\n \r\n'; - -} -return __p -}; - -this["JST"]["src/templates/labels_form_inner_template.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -with (obj) { -__p += '\r\n
    \r\n \r\n '; - if (!edit){ ; -__p += '\r\n
    \r\n \r\n \r\n
    \r\n '; - } ; -__p += '\r\n
    \r\n\r\n
    \r\n \r\n \r\n
    \r\n\r\n
    \r\n \r\n \r\n
    \r\n\r\n \r\n\r\n '; - if (edit){ ; -__p += '\r\n \r\n '; - } else { ; -__p += '\r\n\r\n \r\n\r\n '; - } ; -__p += '\r\n'; - -} -return __p -}; - -this["JST"]["src/templates/labels_form_template.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -with (obj) { -__p += '\r\n
    \r\n\r\n '; - _.each(labels, function(l, i) { ; -__p += '\r\n\r\n
    \r\n\r\n ' + -((__t = ( inner_template({l:l, position:position, edit:true}) )) == null ? '' : __t) + -'\r\n\r\n
    \r\n\r\n '; - }); ; -__p += '\r\n
    \r\n'; - -} -return __p -}; - -this["JST"]["src/templates/lut_picker.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -with (obj) { -__p += '\r\n\r\n
    \r\n\r\n '; - _.each(luts, function(lut, i) { ; -__p += '\r\n\r\n \r\n\r\n '; - if (i === parseInt(luts.length/2)) { ; -__p += '\r\n
    \r\n
    \r\n '; - } ; -__p += '\r\n\r\n '; - }) ; -__p += '\r\n
    \r\n'; - -} -return __p -}; - -this["JST"]["src/templates/rois_form_template.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -with (obj) { -__p += '\r\n\r\n
    ROIs\r\n '; - if (panelCount > 0) { ; -__p += '\r\n ' + -((__t = ( roiCount )) == null ? '' : __t) + -' ROIs selected\r\n '; - } ; -__p += '\r\n
    \r\n\r\n'; - if (panelCount > 0) { ; -__p += '\r\n\r\n
    \r\n\r\n \r\n\r\n
    \r\n \r\n \r\n
    \r\n\r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n '; - if (show){ ; -__p += '\r\n \r\n '; - } else { ; -__p += '\r\n \r\n '; - } ; -__p += '\r\n
    \r\n\r\n
    \r\n
    \r\n Length\r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n \r\n \r\n
    \r\n
    \r\n
    \r\n \r\n \r\n
    \r\n \r\n
    \r\n\r\n
    \r\n
    \r\n Height\r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n px\r\n
    \r\n\r\n
    \r\n Label\r\n
    \r\n
    \r\n \r\n \r\n \r\n \r\n
    \r\n
    \r\n '; - if (show_label) print("pt") ; -__p += '\r\n
    \r\n
    \r\n '; - -} -return __p -}; - -this["JST"]["src/templates/scalebar_panel_template.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -with (obj) { -__p += '\r\n
    \r\n '; - if (show_label) { ; -__p += '\r\n
    \r\n ' + -((__t = ( length )) == null ? '' : __t) + -' ' + -((__t = ( symbol )) == null ? '' : __t) + -'\r\n
    \r\n '; - } ; -__p += '\r\n
    \r\n'; - -} -return __p -}; - -this["JST"]["src/templates/viewport_inner_template.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -with (obj) { -__p += '\r\n '; - _.each(imgs_css, function(css, i) { ; -__p += '\r\n \r\n '; - }); ; -__p += '\r\n'; - -} -return __p -}; - -this["JST"]["src/templates/viewport_template.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = ''; -with (obj) { -__p += '\r\n
    Z
    \r\n
    \r\n
    T
    \r\n
    \r\n
    \r\n
    \r\n ' + -((__t = ( inner_template({imgs_css:imgs_css, opacity:opacity}) )) == null ? '' : __t) + -'\r\n
    \r\n'; - -} -return __p -}; - -this["JST"]["src/templates/xywh_panel_template.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -with (obj) { -__p += '\r\n\r\n\r\n \r\n \r\n \r\n \r\n \r\n
    \r\n Panel\r\n\r\n
    \r\n
    \r\n ' + -((__t = ( dpi )) == null ? '' : __t) + -' dpi\r\n\r\n '; - if (export_dpi != dpi && !isNaN(export_dpi)) { ; -__p += '\r\n (Export at ' + -((__t = ( export_dpi )) == null ? '' : __t) + -' dpi\r\n '; - if (export_dpi > dpi) { ; -__p += '\r\n \r\n '; - } ; -__p += '\r\n \r\n '; - } ; -__p += '\r\n
    \r\n \r\n
    \r\n \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n \r\n
    \r\n \r\n
    \r\n \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n \r\n
    \r\n \r\n
    \r\n \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n
    \r\n'; - -} -return __p -}; - -this["JST"]["src/templates/zoom_crop_template.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -with (obj) { -__p += '\r\n
    \r\n View\r\n \r\n x: ' + -((__t = ( x )) == null ? '' : __t) + -'\r\n y: ' + -((__t = ( y )) == null ? '' : __t) + -'\r\n width: ' + -((__t = ( width )) == null ? '' : __t) + -'\r\n height: ' + -((__t = ( height )) == null ? '' : __t) + -'\r\n \r\n
    \r\n\r\n
    \r\n \r\n '; - } ; -__p += '\r\n \r\n\r\n\r\n'; - }) ; -__p += '\r\n'; - -} -return __p -}; - -this["JST"]["src/templates/modal_dialogs/roi_modal_shape.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -with (obj) { -__p += '\r\n\r\n'; - if (shapes.length > 1) { ; -__p += '\r\n '; - _.each(shapes, function(shape) { ; -__p += '\r\n \r\n '; - } ; -__p += '\r\n \r\n \r\n '; - }) ; -__p += '\r\n'; - } ; -__p += '\r\n'; - -} -return __p -}; - -this["JST"]["src/templates/modal_dialogs/roi_zt_buttons.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -with (obj) { -__p += '\r\nZ: ' + -((__t = ( theZ + 1 )) == null ? '' : __t) + -'\r\n'; - if (theZ != origZ) { ; -__p += '\r\n \r\n'; - } ; -__p += '\r\nT: ' + -((__t = ( theT +1 )) == null ? '' : __t) + -'\r\n'; - if (theT != origT) { ; -__p += '\r\n \r\n'; - } ; -__p += '\r\n'; - -} -return __p -}; - -this["JST"]["src/templates/shapes/shape_item_template.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -with (obj) { -__p += '\r\n
    > 0) + " y1:" + (y1 >> 0) + " x2:" + (x2 >> 0) + " y2:" + (y2 >> 0)) ; -__p += '\r\n\r\n '; - if (type === 'ELLIPSE') print("x:" + (cx >> 0) + " y:" + (cy >> 0) + " rx:" + (rx >> 0) + " ry:" + (ry >> 0)) ; -__p += '\r\n\r\n
    \r\n'; - -} -return __p -}; - -this["JST"]["src/templates/shapes/shape_toolbar_template.html"] = function(obj) { -obj || (obj = {}); -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -with (obj) { -__p += '\r\n\r\n
    \r\n \r\n
    \r\n\r\n\r\n
    \r\n \r\n \r\n \r\n \r\n
    \r\n\r\n\r\n\r\n\r\n\r\n
    \r\n \r\n \r\n
    \r\n\r\n\r\n
    \r\n \r\n \r\n
    \r\n\r\n\r\n
    0) { ; -__p += '\r\n title="Load ' + -((__t = (omeroRoiCount)) == null ? '' : __t) + -' ROIs from OMERO"\r\n '; - } else { ; -__p += '\r\n title="This image has no ROIs on the OMERO server"\r\n '; - } ; -__p += '>\r\n