diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 2c4e944de..000000000 --- a/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*.pyc -*.DS_Store - -_site/* diff --git a/demo/figure.js b/demo/figure.js deleted file mode 100644 index 2179dcd11..000000000 --- a/demo/figure.js +++ /dev/null @@ -1,8490 +0,0 @@ -//! AGPL License. www.openmicroscopy.org - -//! DO NOT EDIT THIS FILE! - Edit under static/figure/js/ -//! figure.js created by $ grunt concat - - - // Version of the json file we're saving. - // This only needs to increment when we make breaking changes (not linked to release versions.) - var VERSION = 2; - - - // ------------------------- Figure Model ----------------------------------- - // Has a PanelList as well as other attributes of the Figure - var FigureModel = Backbone.Model.extend({ - - defaults: { - // 'curr_zoom': 100, - 'canEdit': false, - 'unsaved': false, - 'canvas_width': 13000, - 'canvas_height': 8000, - // w & h from reportlab. - 'paper_width': 595, - 'paper_height': 842, - 'page_color': 'FFFFFF', - 'page_count': 1, - 'page_col_count': 1, // pages laid out in grid - 'paper_spacing': 50, // between each page - 'orientation': 'vertical', - 'page_size': 'A4', // options [A4, letter, mm, pixels] - // see http://www.a4papersize.org/a4-paper-size-in-pixels.php - 'width_mm': 210, // A4 sizes, only used if user chooses page_size: 'mm' - 'height_mm': 297, - 'legend': '', // Figure legend in markdown format. - 'legend_collapsed': true, // collapse or expand legend - }, - - 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 + "static/json/load_web_figure/" + fileId + ".json", - 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, - '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; - - // 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; - }); - } - }); - } - - return json; - }, - - figure_toJSON: function() { - // Turn panels into json - var p_json = [], - self = this; - this.panels.each(function(m) { - p_json.push(m.toJSON()); - }); - - var figureJSON = { - version: VERSION, - panels: p_json, - paper_width: this.get('paper_width'), - paper_height: this.get('paper_height'), - page_size: this.get('page_size'), - page_count: this.get('page_count'), - paper_spacing: this.get('paper_spacing'), - page_col_count: this.get('page_col_count'), - height_mm: this.get('height_mm'), - width_mm: this.get('width_mm'), - orientation: this.get('orientation'), - legend: this.get('legend'), - legend_collapsed: this.get('legend_collapsed'), - page_color: this.get('page_color'), - }; - if (this.get('figureName')){ - figureJSON.figureName = this.get('figureName') - } - if (this.get('fileId')){ - figureJSON.fileId = this.get('fileId') - } - return figureJSON; - }, - - figure_fromJSON: function(data) { - var parsed = JSON.parse(data); - delete parsed.fileId; - this.load_from_JSON(parsed); - this.set('unsaved', true); - }, - - 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); - } - }); - }, - - clearFigure: function() { - var figureModel = this; - figureModel.unset('fileId'); - 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}, - col = 0, - row = 0, - px, py, spacer, scale, - coords = {'px': px, - 'py': py, - 'c': centre, - 'spacer': spacer, - 'colCount': colCount, - 'rowCount': rowCount, - 'col': col, - 'row': row, - '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) { - - var self = this, - callback, - dataType = "json"; - - if (baseUrl) { - callback = "callback"; - dataType = "jsonp"; - } - - // Get the json data for the image... - $.ajax({ - url: imgDataUrl, - jsonp: callback, // 'callback' - dataType: dataType, - // work with the response - success: function( data ) { - - if (data.size.width * data.size.height > 5000 * 5000) { - alert("Image '" + data.meta.imageName + "' is too big for OMERO.figure"); - return; - } - - 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; - - // ****** 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': coords.px, - 'y': coords.py, - 'datasetName': data.meta.datasetName, - 'datasetId': data.meta.datasetId, - 'pixel_size_x': data.pixel_size.valueX, - 'pixel_size_y': data.pixel_size.valueY, - 'pixel_size_x_symbol': data.pixel_size.symbolX, - 'pixel_size_x_unit': data.pixel_size.unitX, - '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(); - - // update px, py for next panel - coords.col += 1; - coords.px += (data.size.width + coords.spacer) * coords.scale; - if (coords.col == coords.colCount) { - coords.row += 1; - coords.col = 0; - coords.py += (data.size.height + coords.spacer) * coords.scale; - coords.px = undefined; // recalculate next time - } - }, - - error: function(event) { - 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() { - var d = new Date(), - dt = d.getFullYear() + "-" + (d.getMonth()+1) + "-" +d.getDate(), - tm = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds(); - 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.set(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.set('x', min_x); - }); - }, - - 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.set('y', min_y); - }); - }, - - align_grid: function() { - 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, - 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', - - // '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, - 'theZ': data.theZ, - 'sizeT': data.sizeT, - 'orig_width': data.orig_width, - 'orig_height': data.orig_height, - 'datasetName': data.datasetName, - 'pixel_size_x': data.pixel_size_x, - 'pixel_size_y': data.pixel_size_y, - 'pixel_size_x_symbol': data.pixel_size_x_symbol, - 'pixel_size_x_unit': data.pixel_size_x_unit, - 'deltaT': data.deltaT, - }; - - // theT is not changed unless we have to... - if (this.get('theT') >= newData.sizeT) { - newData.theT = newData.sizeT - 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]]; - } - if (points) { - for (var p=0; p> 0; - m = pad(Math.round((deltaT % 3600) / 60)); - text = h + ":" + m; - } else if (format === "hrs:mins:secs") { - h = (deltaT / 3600) >> 0; - m = pad(((deltaT % 3600) / 60) >> 0); - s = pad(deltaT % 60); - text = h + ":" + m + ":" + s; - } - return text; - }, - - create_labels_from_time: function(options) { - - this.add_labels([{ - 'time': options.format, - 'size': options.size, - 'position': options.position, - 'color': options.color - }]); - }, - - 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 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 - 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; - - this.set({'width': newW, 'height': newH, 'dx': dx, 'dy': dy, 'zoom': zoom}); - }, - - // 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 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... - if (dx === 0 && dy === 0 && zoom == 100 && Math.abs(xPercent - yPercent) < 0.01) { - // ...ROI is whole image - return {'x': 0, 'y': 0, 'width': orig_width, 'height': orig_height} - } - - // 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}; - }, - - // 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)}; - }, - - get_img_src: function() { - 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 / - - return baseUrl + '/render_image/' + imageId + "/" + theZ + "/" + theT - + '/?c=' + renderString + proj + maps +"&m=c"; - }, - - // 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, dx, dy, fit) { - - 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; - - var transform_x = 100 * (frame_w/2 - img_x) / img_w, - transform_y = 100 * (frame_h/2 - img_y) / img_h, - rotation = this.get('rotation') || 0; - - // option to align image within viewport (not used now) - if (fit) { - img_x = Math.min(img_x, 0); - if (img_x + img_w < frame_w) { - img_x = frame_w - img_w; - } - img_y = Math.min(img_y, 0); - if (img_y + img_h < frame_h) { - img_y = frame_h - img_h; - } - } - - 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; - }, - - 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_img_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 - return this.get('x') + this.get('width'); - }, - - 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(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; - 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)); - } - - // localStorage: new Backbone.LocalStorage("figureShop-backbone") - }); - - -var ShapeModel = Backbone.Model.extend({ - - parse: function(shape) { - // Convert OMERO.server shapes to OMERO.figure shapes - 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; - } - } - if (shape.type === 'Ellipse') { - // If we have < OMERO 5.3, Ellipse has cx, cy, rx, ry - if (shape.rx !== undefined) { - shape.x = shape.cx; - shape.y = shape.cy; - shape.radiusX = shape.rx; - shape.radiusY = shape.ry; - } - } - 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.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(); - } -}); - -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); - }); - } - }, - - 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 = parseInt(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 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)); - // Reduce lists into summary for this channel - var startAvg = parseInt(starts.reduce(addFn, 0) / starts.length, 10); - var endAvg = parseInt(ends.reduce(addFn, 0) / ends.length, 10); - 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)); - var color = colors.reduce(allEqualFn, colors[0]) ? colors[0] : 'ccc'; - // allEqualFn for booleans will return undefined if not or equal - 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, - 'startAvg': startAvg, - 'startsNotEqual': startsNotEqual, - 'endAvg': endAvg, - 'endsNotEqual': endsNotEqual, - 'active': active, - 'lutBgPos': lutBgPos, - 'reverse': reverse, - 'color': color}); - var $div = $(sliderHtml).appendTo(this.$el); - - $div.find('.ch_slider').slider({ - range: true, - min: min, - max: max, - values: [startAvg, endAvg], - slide: function(event, ui) { - $('.ch_start input', $div).val(ui.values[0]); - $('.ch_end input', $div).val(ui.values[1]); - }, - 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; - } -}); - -// -// 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, - - 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(); - - // 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 - }); - } - - self.zoomToFit(); // includes render() - // disable submit until user chooses a region/ROI - self.enableSubmit(false); - - // Load ROIs from OMERO... - self.loadRois(); - // ...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", - "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), - theT = parseInt($roi.attr('data-theT'), 10), - theZ = parseInt($roi.attr('data-theZ'), 10); - - this.m.set({'theT': theT, 'theZ': theZ}); - - this.currentROI = { - 'x':x, 'y':y, 'width':width, 'height':height - } - - this.render(); - - this.cropModel.set({ - 'selected': true - }); - - // Save ROI ID - this.currentRoiId = $roi.attr('data-roiId'); - }, - - handleRoiForm: function(event) { - event.preventDefault(); - // var json = this.processForm(); - var self = this, - r = this.currentROI, - theZ = this.m.get('theZ'), - theT = this.m.get('theT'), - 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'); - } - 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}); - }); - } - - // 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 imageRects = [], - 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 - }); - } 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 - loadRois: function() { - var self = this, - iid = self.m.get('imageId'); - $.getJSON(ROIS_JSON_URL + iid + "/", function(data){ - - // 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 rects = [], - cachedRois = {}, // roiId: shapes (z/t dict) - roi, 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] - rects.push({'theZ': shape.theZ, - 'theT': shape.theT, - 'x': shape.x, - 'y': shape.y, - 'width': shape.width, - 'height': shape.height, - 'roiId': roi.id, - '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(rects, ".roisFromOMERO", msg); - - 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(); - 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; - - var json = { - 'msg': msg, - 'src': src, - 'rect': rect, - 'w': div_w, - 'h': div_h, - '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 $cropViewer = $("#cropViewer"), - viewer_w = $cropViewer.width(), - viewer_h = $cropViewer.height(), - w = this.m.get('orig_width'), - h = this.m.get('orig_height'); - scale = Math.min(viewer_w/w, viewer_h/h); - this.setZoom(scale * 100); - }, - - setZoom: function(percent) { - this.zoom = percent; - this.render(); - }, - - render: function() { - var scale = this.zoom / 100, - roi = this.currentROI, - w = this.m.get('orig_width'), - h = this.m.get('orig_height'); - var newW = w * scale, - newH = h * scale; - var src = this.m.get_img_src() - - this.paper.setSize(newW, newH); - $("#crop_paper").css({'height': newH, 'width': newW}); - - this.$cropImg.css({'height': newH, 'width': newW}) - .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 - }); - } - }); - - - // -------------------------- 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 RoiModalView({model: this.model}); - new DpiModalView({model: this.model}); - new LegendView({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); - - // 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 .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