diff --git a/licenses.txt b/licenses.txt index 9c03bcb3e..58967f99b 100644 --- a/licenses.txt +++ b/licenses.txt @@ -10,17 +10,21 @@ Library: backbone.mousetrap Site: https://github.com/elasticsales/backbone.mousetrap License: MIT -Library: bootstrap3 +Library: bootstrap Site: http://getbootstrap.com/ -License: Apache Software License, Version 2 +License: MIT -Library: bootstrap-colorpicker -Site: http://mjolnic.com/bootstrap-colorpicker/ -License: Apache Software License, Version 2 +Library: bootstrap icons +Site: https://github.com/twbs/icons +License: MIT + +Library: dompurify +Site: https://www.npmjs.com/package/dompurify +License: Apache License Version 2.0, or Mozilla Public License Version 2.0 -Library: json2js -Site: http://www.JSON.org/json2.js -License: Public Domain +Library: jquery +Site: https://jquery.com/ +License: MIT Library: markdown-browser-0.6.0-beta1 Site: https://github.com/evilstreak/markdown-js @@ -34,14 +38,18 @@ Library: mousetrap-1.5.3 Site: craig.is/killing/mice License: Apache Software License, Version 2 -Library: shape-editor-3.0.0 -Site: https://github.com/ome/shape-editor -License: BSD +Library: @popperjs/core +Site: https://www.npmjs.com/package/@popperjs/core +License: MIT Library: raphael Site: https://github.com/fanstatic/js.raphael License: BSD +Library: sortablejs +Site: https://www.npmjs.com/package/sortablejs +License: MIT + Library: underscore Site: https://github.com/jashkenas/underscore License: MIT diff --git a/omero_figure/omeroutils.py b/omero_figure/omeroutils.py index 367c0c779..279d08882 100644 --- a/omero_figure/omeroutils.py +++ b/omero_figure/omeroutils.py @@ -17,6 +17,8 @@ from omero.sys import ParametersI from omero.gateway import PlaneInfoWrapper +from omero.model.enums import UnitsTime +from omero.model import TimeI def get_timestamps(conn, image): @@ -28,13 +30,34 @@ def get_timestamps(conn, image): info_list = conn.getQueryService().findAllByQuery( query, params, conn.SERVICE_OPTS) timemap = {} - for info in info_list: - t_index = info.theT.getValue() - if info.deltaT is not None: - # Use wrapper to help unit conversion - plane_info = PlaneInfoWrapper(conn, info) - delta_t = plane_info.getDeltaT('SECOND') - timemap[t_index] = delta_t.getValue() + # check if any PlaneInfo was found + if len(info_list) > 0: + # get time info from the PlaneInfo + for info in info_list: + t_index = info.theT.getValue() + if info.deltaT is not None: + # Use wrapper to help unit conversion + plane_info = PlaneInfoWrapper(conn, info) + delta_t = plane_info.getDeltaT('SECOND') + timemap[t_index] = delta_t.getValue() + # double check to see if timemap actually got populated + if len(info_list) == 0 or len(timemap) == 0: + # get time info from the timeIncrement of the Pixels + time_increment = 0 + converted_value = 0 + try: + pixels = image.getPrimaryPixels()._obj + time_increment = pixels.getTimeIncrement() + secs_unit = getattr(UnitsTime, "SECOND") + seconds = TimeI(time_increment, secs_unit) + converted_value = seconds.getValue() + + except Exception as error: + print(f"An exception occured: {error}\n" + "maybe the image has no 'timeIncrement' set") + if converted_value != 0: + for i in range(image.getSizeT()): + timemap[i] = i*converted_value time_list = [] for t in range(image.getSizeT()): if t in timemap: diff --git a/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py b/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py index 7a2ec3e10..1ea38267a 100644 --- a/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py +++ b/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py @@ -505,7 +505,7 @@ def draw_ellipse(self, shape): cy = self.page_height - c['y'] rx = shape['radiusX'] * self.scale ry = shape['radiusY'] * self.scale - rotation = (shape['rotation'] + self.panel['rotation']) * -1 + rotation = (shape.get('rotation', 0) + self.panel['rotation']) * -1 r, g, b, a = self.get_rgba(shape['strokeColor']) self.canvas.setStrokeColorRGB(r, g, b, alpha=a) @@ -809,7 +809,7 @@ def draw_ellipse(self, shape): cy = ctr['y'] rx = self.scale * shape['radiusX'] ry = self.scale * shape['radiusY'] - rotation = (shape['rotation'] + self.panel['rotation']) * -1 + rotation = (shape.get('rotation', 0) + self.panel['rotation']) * -1 width = int((rx * 2) + w) height = int((ry * 2) + w) @@ -820,7 +820,9 @@ def draw_ellipse(self, shape): rgba = ShapeToPdfExport.get_rgba_int(shape['strokeColor']) ellipse_draw.ellipse((0, 0, width, height), fill=rgba) rgba = self.get_rgba_int(shape.get('fillColor', '#00000000')) - ellipse_draw.ellipse((w, w, width - w, height - w), fill=rgba) + # when rx is ~zero (for a Point, scaled down) don't need inner ellipse + if (width - w) >= w: + ellipse_draw.ellipse((w, w, width - w, height - w), fill=rgba) temp_ellipse = temp_ellipse.rotate(rotation, resample=Image.BICUBIC, expand=True) # Use ellipse as mask, so transparent part is not pasted diff --git a/setup.py b/setup.py index 07c206651..fca8df84c 100644 --- a/setup.py +++ b/setup.py @@ -90,5 +90,5 @@ def run(self): 'sdist': require_npm(sdist, True), 'develop': require_npm(develop), }, - tests_require=['pytest', 'numpy'], + tests_require=['pytest'], ) diff --git a/src/css/figure.css b/src/css/figure.css index 47f880784..0559d4a8a 100644 --- a/src/css/figure.css +++ b/src/css/figure.css @@ -1267,6 +1267,10 @@ .ellipse-icon{ background-image: url("../images/ellipse-icon-16.png"); } + .point-icon{ + background-image: url("../images/point-icon-24.png"); + background-position: center; + } .polygon-icon{ background-image: url("../images/polygon-icon-16.png"); } diff --git a/src/images/point-icon-24.png b/src/images/point-icon-24.png new file mode 100644 index 000000000..3d5c1d46f Binary files /dev/null and b/src/images/point-icon-24.png differ diff --git a/src/js/models/panel_model.js b/src/js/models/panel_model.js index 374ed7231..99e0de183 100644 --- a/src/js/models/panel_model.js +++ b/src/js/models/panel_model.js @@ -282,7 +282,7 @@ return true; } var points; - if (shape.type === "Ellipse") { + if (shape.type === "Ellipse" || shape.type === "Point") { points = [[shape.cx, shape.cy]]; } else if (shape.type === "Rectangle") { points = [[shape.x, shape.y], @@ -569,7 +569,7 @@ // 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 = [], @@ -595,7 +595,7 @@ // Extract all the keys (even duplicates) var keys = labs.map(lbl => this.get_label_key(lbl)); - // get all unique labels based on filtering keys + // 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))); @@ -617,12 +617,29 @@ this.save('channels', chs); }, - toggle_channel: function(cIndex, active ) { + save_channels: function(channels) { + // channels should be a list of objects [{key:value}, {}..] + var oldChs = this.get('channels'); + // Clone channels, applying changes + var chs = this.get('channels').map((oldCh, idx) => { + return $.extend(true, {}, oldCh, channels[idx]); + }); + this.save('channels', chs); + }, + toggle_channel: function(cIndex, active) { if (typeof active == "undefined") { active = !this.get('channels')[cIndex].active; } - this.save_channel(cIndex, 'active', active); + + if (this.get("hilo_enabled") && active) { + let newChs = this.get('channels').map(function(channel, idx) { + return {'active': idx == cIndex}; + }); + this.save_channels(newChs); + } else { + this.save_channel(cIndex, 'active', active); + } }, save_channel_window: function(cIndex, new_w) { diff --git a/src/js/models/undo.js b/src/js/models/undo.js index ddd642a0d..6e74dec67 100644 --- a/src/js/models/undo.js +++ b/src/js/models/undo.js @@ -193,7 +193,8 @@ export const UndoManager = Backbone.Model.extend({ // into undo / redo operations to go into our Edit below var createUndo = function(callList) { var undos = []; - for (var u=0; u=0; u--) { undos.push(callList[u]); } // get the currently selected panels diff --git a/src/js/shapeEditorTest.js b/src/js/shapeEditorTest.js index 2cb716673..73bb77d39 100644 --- a/src/js/shapeEditorTest.js +++ b/src/js/shapeEditorTest.js @@ -254,6 +254,11 @@ $(function() { "y": 260.5, "x": 419}); + shapeManager.addShapeJson({"type": "Point", + "strokeWidth": 2, + "y": 30, + "x": 30}); + var s = shapeManager.addShapeJson({"type": "Line", "strokeColor": "#00ff00", "strokeWidth": 2, diff --git a/src/js/shape_editor/ellipse.js b/src/js/shape_editor/ellipse.js index d736fd8fd..7823cf3df 100644 --- a/src/js/shape_editor/ellipse.js +++ b/src/js/shape_editor/ellipse.js @@ -476,7 +476,7 @@ Ellipse.prototype.createHandles = function createHandles() { handle = this.paper.rect(hx - hsize / 2, hy - hsize / 2, hsize, hsize); handle.attr({ cursor: "move" }); handle.h_id = key; - handle.line = self; + // handle.line = self; if (this.manager.canEdit) { handle.drag(_handle_drag(), _handle_drag_start(), _handle_drag_end()); @@ -511,7 +511,7 @@ Ellipse.prototype.getHandleCoords = function getHandleCoords() { }; }; -// Class for creating Lines. +// Class for creating Ellipse. var CreateEllipse = function CreateEllipse(options) { this.paper = options.paper; this.manager = options.manager; diff --git a/src/js/shape_editor/point.js b/src/js/shape_editor/point.js new file mode 100644 index 000000000..8aa3367e3 --- /dev/null +++ b/src/js/shape_editor/point.js @@ -0,0 +1,477 @@ +/* +// Copyright (C) 2015-2024 University of Dundee & Open Microscopy Environment. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following +// conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following +// disclaimer in the documentation // and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, +// BUT NOT LIMITED TO, +// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS +// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE // GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +// IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import Raphael from "raphael"; + +const POINT_RADIUS = 5; +var Point = function Point(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 = POINT_RADIUS; + this._radiusY = POINT_RADIUS; + this._rotation = options.rotation || 0; + + 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; + } + if (options.area) { + this._area = options.area; + } else { + this._area = this._radiusX * this._radiusY * Math.PI; + } + this.handle_wh = 6; + + this.element = this.paper.ellipse(); + this.element.attr({ "fill-opacity": 0.01, fill: "#fff", cursor: "pointer" }); + + // Drag handling of point + 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(); +}; + +Point.prototype.toJson = function toJson() { + var rv = { + type: "Point", + x: this._x, + y: this._y, + strokeWidth: this._strokeWidth, + strokeColor: this._strokeColor, + }; + if (this._id) { + rv.id = this._id; + } + return rv; +}; + +Point.prototype.compareCoords = function compareCoords(json) { + var selfJson = this.toJson(), + match = true; + if (json.type !== selfJson.type) { + return false; + } + ["x", "y"].forEach(function (c) { + if (Math.round(json[c]) !== Math.round(selfJson[c])) { + match = false; + } + }); + return match; +}; + +// Useful for pasting json with an offset +Point.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 +Point.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 +Point.prototype._handleMousedown = function _handleMousedown() { + if (!this._selected) { + this.manager.selectShapes([this]); + } +}; + +Point.prototype.setColor = function setColor(strokeColor) { + this._strokeColor = strokeColor; + this.drawShape(); +}; + +Point.prototype.getStrokeColor = function getStrokeColor() { + return this._strokeColor; +}; + +Point.prototype.setStrokeColor = function setStrokeColor(strokeColor) { + this._strokeColor = strokeColor; + this.drawShape(); +}; + +Point.prototype.setStrokeWidth = function setStrokeWidth(strokeWidth) { + this._strokeWidth = strokeWidth; + this.drawShape(); +}; + +Point.prototype.getStrokeWidth = function getStrokeWidth() { + return this._strokeWidth; +}; + +Point.prototype.destroy = function destroy() { + this.element.remove(); + this.handles.remove(); +}; + +Point.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); + + return Raphael.isPointInsidePath(path, x, y); +}; + +Point.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; +}; + +Point.prototype.isSelected = function isSelected() { + return this._selected; +}; + +Point.prototype.setZoom = function setZoom(zoom) { + this._zoomFraction = zoom / 100; + this.drawShape(); +}; + +Point.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); +}; + +Point.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._area = this._radiusX * this._radiusY * Math.PI; + + this.drawShape(); +}; + +Point.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.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 < l; h++) { + hnd = this.handles[h]; + h_id = hnd.h_id; + hx = this._handleIds[h_id].x * this._zoomFraction; + hy = this._handleIds[h_id].y * this._zoomFraction; + hnd.attr({ x: hx - this.handle_wh / 2, y: hy - this.handle_wh / 2 }); + } +}; + +Point.prototype.setSelected = function setSelected(selected) { + this._selected = !!selected; + this.drawShape(); +}; + +Point.prototype.createHandles = function createHandles() { + // ---- Create Handles ----- + + // NB: handleIds are used to calculate ellipse coords + // so handledIds are scaled to MODEL coords, not zoomed. + this._handleIds = this.getHandleCoords(); + + var self = this, + // map of centre-points for each handle + handleAttrs = { + stroke: "#4b80f9", + fill: "#fff", + cursor: "default", + "fill-opacity": 1.0, + }; + + // draw handles - Can't drag handles to resize, but they are useful + // simply to indicate that the Point is selected + self.handles = this.paper.set(); + + var hsize = this.handle_wh, + hx, + hy, + handle; + for (var key in this._handleIds) { + hx = this._handleIds[key].x; + hy = this._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; + self.handles.push(handle); + } + + self.handles.attr(handleAttrs).hide(); // show on selection +}; + +Point.prototype.getHandleCoords = function getHandleCoords() { + // Returns MODEL coordinates (not zoom coordinates) + let margin = 2; + + var rot = Raphael.rad(this._rotation), + x = this._x, + y = this._y, + radiusX = this._radiusX + margin, + radiusY = this._radiusY + margin, + 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 Point. +var CreatePoint = function CreatePoint(options) { + this.paper = options.paper; + this.manager = options.manager; +}; + +CreatePoint.prototype.startDrag = function startDrag(startX, startY) { + var strokeColor = this.manager.getStrokeColor(), + strokeWidth = this.manager.getStrokeWidth(), + zoom = this.manager.getZoom(); + + this.point = new Point({ + manager: this.manager, + paper: this.paper, + x: startX, + y: startY, + radiusX: POINT_RADIUS, + radiusY: POINT_RADIUS, + area: 0, + rotation: 0, + strokeWidth: strokeWidth, + zoom: zoom, + strokeColor: strokeColor, + }); +}; + +CreatePoint.prototype.drag = function drag(dragX, dragY, shiftKey) { + // no drag behaviour on Point creation +}; + +CreatePoint.prototype.stopDrag = function stopDrag() { + // Don't create ellipse of zero size (click, without drag) + var coords = this.point.toJson(); + if (coords.radiusX < 2) { + this.point.destroy(); + delete this.ellipse; + return; + } + // on the 'new:shape' trigger, this shape will already be selected + this.point.setSelected(true); + this.manager.addShape(this.point); +}; + +export { CreatePoint, Point }; diff --git a/src/js/shape_editor/shape_manager.js b/src/js/shape_editor/shape_manager.js index f6c8df0ff..2ca5c3ccd 100644 --- a/src/js/shape_editor/shape_manager.js +++ b/src/js/shape_editor/shape_manager.js @@ -29,6 +29,7 @@ import $ from "jquery"; import { CreateRect, Rect } from "./rect"; import { CreateLine, Line, CreateArrow, Arrow } from "./line"; import { CreateEllipse, Ellipse } from "./ellipse"; +import { CreatePoint, Point } from "./point"; import { Polygon, Polyline } from "./polygon"; var ShapeManager = function ShapeManager(elementId, width, height, options) { @@ -36,7 +37,7 @@ var ShapeManager = function ShapeManager(elementId, width, height, options) { options = options || {}; // Keep track of state, strokeColor etc - this.STATES = ["SELECT", "RECT", "LINE", "ARROW", "ELLIPSE", "POLYGON"]; + this.STATES = ["SELECT", "RECT", "LINE", "ARROW", "ELLIPSE", "POLYGON", "POINT"]; this._state = "SELECT"; this._strokeColor = "#ff0000"; this._strokeWidth = 2; @@ -87,6 +88,7 @@ var ShapeManager = function ShapeManager(elementId, width, height, options) { ELLIPSE: new CreateEllipse({ manager: this, paper: this.paper }), LINE: new CreateLine({ manager: this, paper: this.paper }), ARROW: new CreateArrow({ manager: this, paper: this.paper }), + POINT: new CreatePoint({ manager: this, paper: this.paper }), }; this.createShape = this.shapeFactories.LINE; @@ -172,7 +174,7 @@ ShapeManager.prototype.setState = function setState(state) { return; } // When creating shapes, cover existing shapes with newShapeBg - var shapes = ["RECT", "LINE", "ARROW", "ELLIPSE", "POLYGON"]; + var shapes = ["RECT", "LINE", "ARROW", "ELLIPSE", "POLYGON", "POINT"]; if (shapes.indexOf(state) > -1) { this.newShapeBg.toFront(); this.newShapeBg.attr({ cursor: "crosshair" }); @@ -412,6 +414,11 @@ ShapeManager.prototype.createShapeJson = function createShapeJson(jsonShape) { options.transform = s.transform; options.area = s.radiusX * s.radiusY * Math.PI; newShape = new Ellipse(options); + } else if (s.type === "Point") { + options.x = s.x; + options.y = s.y; + options.area = 0; + newShape = new Point(options); } else if (s.type === "Rectangle") { options.x = s.x; options.y = s.y; diff --git a/src/js/views/channel_slider_view.js b/src/js/views/channel_slider_view.js index 8df7b3ba6..41bab960b 100644 --- a/src/js/views/channel_slider_view.js +++ b/src/js/views/channel_slider_view.js @@ -7,6 +7,7 @@ import FigureLutPicker from "../views/lutpicker"; import FigureColorPicker from "../views/colorpicker"; import channel_slider_template from '../../templates/channel_slider.template.html?raw'; +import checkbox_template from '../../templates/checkbox_template.html?raw'; import lutsPng from "../../images/luts_10.png"; // Need to handle dev vv built (omero-web) paths @@ -18,6 +19,7 @@ const SLIDER_INCR_CUTOFF = 100; var ChannelSliderView = Backbone.View.extend({ template: _.template(channel_slider_template), + checkboxTemplate: _.template(checkbox_template), initialize: function(opts) { // This View may apply to a single PanelModel or a list @@ -37,6 +39,7 @@ var ChannelSliderView = Backbone.View.extend({ "change .ch_slider input": "channel_slider_stop", "mousemove .ch_start_slider": "start_slider_mousemove", "mousemove .ch_end_slider": "end_slider_mousemove", + "change #hiloCheckbox": "handle_hilo_checkbox", }, start_slider_mousemove: function(event) { @@ -268,6 +271,48 @@ var ChannelSliderView = Backbone.View.extend({ return this; }, + handle_hilo_checkbox: function(event) { + var checkboxState = event.target.checked; + this.models.forEach(function(m) { + if (checkboxState && !m.get("hilo_enabled")){ + m.save({ + "hilo_enabled": true, + "hilo_channels_state": m.get('channels').map(function(channel) { + return { + "color": channel.color, + "active": channel.active + }; + }) + }); + let foundActive = false; + let newChs = m.get('channels').map(function(channel, idx) { + // Switch LUT to HiLo for all channels + // Keep only the first active channel active + let new_state = { + 'color': 'hilo.lut', + 'active': (!foundActive && channel.active) + } + foundActive = (foundActive || channel.active); + return new_state; + }); + m.save_channels(newChs); + } else if (!checkboxState && m.get("hilo_enabled")){ + m.save("hilo_enabled", false); + let newChs = m.get('channels').map(function(channel, idx) { + return m.get("hilo_channels_state")[idx]; + }); + m.save_channels(newChs); + } + }) + }, + + loadCheckboxState: function() { + var checkbox = this.$("#hiloCheckbox")[0]; + this.models.forEach(function(m) { + checkbox.checked = m.get('hilo_enabled') || checkbox.checked; + }); + }, + render: function() { var json, self = this; @@ -310,7 +355,6 @@ var ChannelSliderView = Backbone.View.extend({ 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){ @@ -320,13 +364,11 @@ var ChannelSliderView = Backbone.View.extend({ 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: @@ -391,6 +433,11 @@ var ChannelSliderView = Backbone.View.extend({ $(sliderHtml).appendTo(this.$el); }.bind(this)); + // Append the checkbox template + var checkboxHtml = this.checkboxTemplate(); + this.$el.append(checkboxHtml); + // Load checkbox state after rendering + this.loadCheckboxState(); return this; } }); diff --git a/src/js/views/roi_loader_view.js b/src/js/views/roi_loader_view.js index 6022b5ede..04a617059 100644 --- a/src/js/views/roi_loader_view.js +++ b/src/js/views/roi_loader_view.js @@ -32,6 +32,7 @@ var RoiLoaderView = Backbone.View.extend({ 'Line': 'line-icon', 'Arrow': 'arrow-icon', 'Polygon': 'polygon-icon', + 'Point': 'point-icon', 'Polyline': 'polyline-icon'}, addOmeroShape: function(event) { diff --git a/src/shapeEditorTest.html b/src/shapeEditorTest.html index be90dc573..e23c17799 100644 --- a/src/shapeEditorTest.html +++ b/src/shapeEditorTest.html @@ -38,7 +38,9 @@ Line:
Arrow: - +
+ Point: +
diff --git a/src/templates/checkbox_template.html b/src/templates/checkbox_template.html new file mode 100644 index 000000000..2f62fdb4d --- /dev/null +++ b/src/templates/checkbox_template.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/src/templates/shapes/shape_toolbar.template.html b/src/templates/shapes/shape_toolbar.template.html index 0922086c8..6d61a1b4b 100644 --- a/src/templates/shapes/shape_toolbar.template.html +++ b/src/templates/shapes/shape_toolbar.template.html @@ -20,6 +20,9 @@ +